Commit graph

42 commits

Author SHA1 Message Date
Victor Taelin
1fb164cf39
Merge pull request #4 from julianss/say-bytes-not-chars
say "bytes", not "characters", where the limit is bytes
2026-07-30 23:19:57 -03:00
julianss
f5b6a24a59 say "bytes", not "characters", where the limit is bytes
`check()` enforces the entry limit on `len(text.encode())`, but every place
that asks the agent for a line calls it characters:

  - the `nap` compression prompt ("at most %d characters")
  - `note`'s usage line ("at most %d chars")
  - the installed prompt in TEMPLATE ("max {chars} chars")

An agent budgeting characters overshoots on any non-ASCII text and only
finds out from the error, one round trip per attempt. The error message
already explains the byte cost -- this just says it up front, at no cost
to the prompt.

Five words, no behaviour change. 109099 tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 13:53:12 -06:00
victortaelin
428d2ecb89 the memory context halves: 96 lines, ~8k tokens, 2 parts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 11:25:36 -03:00
Victor Taelin
e36da55815
Merge pull request #2 from rarf/windows-native-support
fix: native Windows support (msvcrt lock instead of fcntl)
2026-07-27 18:51:42 -03:00
CPRoptmem
3411344db6 fix: native Windows support (msvcrt lock instead of fcntl)
fcntl does not exist on Windows, so memo crashed on import. Guard the
import and use msvcrt advisory locking with spin/backoff; open the lock
file in append mode so parallel sessions don't break each other's locks.

Verified: 8 parallel processes x 200 notes = 1600/1600 records on
native Windows (no WSL).
2026-07-27 08:31:11 +01:00
victortaelin
d618a3a626 the store is UTF-8, so the tool is too: no locale can crash a wake 2026-07-26 18:03:37 -03:00
victortaelin
4b72c6ee7b zoom is the primitive: one node, its two halves; the agent navigates 2026-07-26 17:36:19 -03:00
victortaelin
14286cba3e zoom: an agent navigates its own tree instead of grepping it 2026-07-26 17:20:15 -03:00
victortaelin
8c444ad991 audit: no dead end survives a corrupt record, no lie survives a filesystem error
- a blank summary record bricked wake forever: it refused with 'Do the 0
  compressions below' + a literal None, while nap said nothing was left.
  Both sites (wake's cover, nap's half) now name the one exit: forget.
- tree_get/count/repair swallowed every OSError as 'not built yet', so an
  unreadable level was reported as pending work that could not be done.
  Only FileNotFoundError means absent; real failures surface with the path.
- import accepted 2026-99-99, poisoning the store's date order forever.
- the wake T error now says 'the log holds N memories', not 'entries'.
2026-07-26 17:01:41 -03:00
victortaelin
cf26dbeb44 README: the PATH note fits one line 2026-07-26 16:58:29 -03:00
victortaelin
b5041404c0 correctness pass: every printed command runs, no traceback ever reaches an agent
- the tool names itself in every command it prints. After `curl | sh`
  nothing is on PATH, so `Run: memo nap 0-1 ...` was command-not-found for
  every new user: the whole note -> merge -> nap loop died on the first turn.
- a typo in `config` stopped every command AND the recovery it named
  (`memo config`) read the same file, so nothing could fix it. It now names
  the file and the line.
- a filesystem error (read-only store, MEMORY_DIR at a file) printed a Python
  traceback. One handler reports it in the tool's own voice.
- `nap 1-2` answered 'already settled': an unaligned id read a different
  block's record. nap and forget now share one block-id parser.
- recall held the whole log in memory: 618 MB at a million memories, and a
  vague regex held every match too. One streaming pass, 16 MB.
- install.sh says so when the machine has no python3, instead of installing
  and then failing in env(1).
- anim: package.json was npm-init boilerplate; a comment named deleted blocks.py.
2026-07-26 16:34:25 -03:00
victortaelin
bfdeae2170 the merge command is nap, not sleep: it never was a nightly job 2026-07-26 15:50:16 -03:00
victortaelin
e61c97df36 merge prompt: prune by lasting effect, not by a checklist 2026-07-26 15:41:24 -03:00
victortaelin
c785447de1 merge prompt: drop the checklist line; wake header names its snapshot 2026-07-26 15:39:27 -03:00
victortaelin
7b9e582dc9 wake refuses only when the document itself needs the merge; otherwise it hands it over after the read 2026-07-26 15:34:09 -03:00
victortaelin
6987d45a94 README: the pitch names the prompt's size 2026-07-26 15:30:26 -03:00
victortaelin
2742e57fd0 prompt: startup is two lines; wake's own footer carries the paging order 2026-07-26 15:28:48 -03:00
victortaelin
5a50e4f321 README: shorter pitch line 2026-07-26 15:24:31 -03:00
victortaelin
18584b5039 prompt: 'your memories are in', matching the directory's name 2026-07-26 15:23:18 -03:00
victortaelin
f9497be6d5 prompt: shorter subagent line 2026-07-26 15:22:19 -03:00
victortaelin
0506f4919b prompt: the subagent line is a spawn-time instruction, not a per-message prefix 2026-07-26 15:21:46 -03:00
victortaelin
fd97a81429 README: one-line pitch 2026-07-26 15:20:52 -03:00
victortaelin
d25f09ddc0 one file: blocks.py folded into memo; sizes editable with memo config 2026-07-26 15:12:41 -03:00
victortaelin
efde13f0c1 One home ~/.optmem, one curl to install; new AGENTS.md block; README cut to 59 lines 2026-07-26 15:07:10 -03:00
victortaelin
2dc0e0ba0b README: the film teaches it, the README ships it; inline GIF instead of a broken click-to-watch 2026-07-26 14:45:37 -03:00
victortaelin
2623c95294 init is idempotent by test; put it in the command table 2026-07-26 14:39:55 -03:00
victortaelin
bd2a799500 Revert name to OptMem 2026-07-26 14:35:22 -03:00
victortaelin
15130b9cad Rename to AmalgaMem: new README, memo init setup, explainer animation 2026-07-26 14:28:25 -03:00
victortaelin
58019db0db audit: recall matches the whole line; races answered honestly
- recall searched only the text, so a date or an id found nothing;
  it now matches the rendered line '#id date text'
- a wake refusal read pending twice, so a nap paid in parallel between
  the reads printed a literal None; the nap itself now decides
- a session resubmitting a block another session settled was told
  'Wrong block' (rc 1) for doing what its prompt ordered; it now hears
  'already settled' (rc 0), and the tree_put race message no longer
  claims 'written' when the block may have been forgotten
2026-07-25 19:26:48 -03:00
victortaelin
a6abc264c5 the wake budget is 208 lines: 16k tokens measured, not 16k assumed
WAKE_LINES=256 was converted from Taelin's 16k-token budget at 4 bytes per
token. Dense technical memories run 3.36, so the real document was 19,015
tokens on the live store -- 19% over the number he set, and it needed a
fourth tool call to deliver a 17-line stub.

  256 lines  63,922 bytes  19,015 tokens  4 parts
  208 lines  54,011 bytes  16,393 tokens  3 parts   <- shipped

Nothing is recomputed: the budget only selects which existing lines print.
test.py now reads the budget from memo instead of keeping its own copy.
2026-07-25 18:49:37 -03:00
victortaelin
909e28fec9 sleep: no blank first line when there is nothing to report before the nap 2026-07-25 18:48:22 -03:00
victortaelin
50b688ab68 audit: a paid compression no longer breaks a snapshot wake
Found by battle-testing the real flow: wake part 1, note something, pay the
compression the note creates, then continue the wake. pending_count subtracted
per level without clamping, so a level that had moved PAST the snapshot
counted as negative work:

  Cannot wake: -1 compressions pending. Do them, then run memo wake again.
  None

The rest of the memory was unreadable for the rest of the session. Clamped at
zero, with a test that pending_count always equals len(pending).

Also: MEMORY_DIR is never created (a typo opened a second, empty identity
instead of failing); the config file is never written (a store froze the
defaults at creation, so updating the tool stopped changing behaviour);
missing import file reports instead of raising; unknown command says so;
a nap prompt with nothing left after it drops the '0 compressions remain'
line; a missing half dies instead of compressing a '?'; plurals.
2026-07-25 18:41:26 -03:00
victortaelin
c08119e701 note criteria: new + important + lasting, and never redundant 2026-07-25 17:53:22 -03:00
victortaelin
1f1ec1511e prompts re-derived from scratch: inclusive block ids, honest terminators, orders with recovery
- Block ids are inclusive everywhere (16-31 = memories #16..#31), matching
  what the wake document prints. Before, wake showed #16-31 but sleep and
  forget demanded 16-32: an agent copying its own document was rejected.
- Only a successful wake says 'You are awake.' Sleep ends with 'Nothing left
  to compress.' Before, the final sleep of a refused wake claimed the agent
  was awake when it had read zero memories.
- An empty store's wake also ends with 'You are awake.' (it never did, so a
  fresh agent could not satisfy its own instructions).
- Wake's refusal closes the loop: 'Do them, then run memo wake again.'
- Nap prompt names its object first and labels merged halves with their ids.
- Every error ends with the recovery command.
- README synced to reality (it still quoted the pre-terseness prompts,
  TREE.txt, and 'four commands').
2026-07-25 17:38:56 -03:00
victortaelin
af17f94c2f every command handed to an agent is an order, not a label
'next: memo wake 2 246' is an annotation, not an instruction. ASD-STE100:
an instruction is an imperative, verb first. Same defect in the nap prompt,
where the command sat on a bare line with nothing telling the agent to run it.

The wake terminator is 'You are awake.', the same words memo sleep already
used, instead of a bare 'awake.' -- one term, one meaning.

test.py now asserts that every offered command starts with 'Run: '.
2026-07-25 17:20:01 -03:00
victortaelin
dc5339c55b test: 100s -> 1.6s
The suite spawned a python interpreter per nap, ~2000 of them. The CLI now
runs in-process; real processes are kept only for the cross-process lock
test, which is their subject. The 3000-step cover sweep is sampled: the
property is structural, not a function of the exact T.
2026-07-25 17:15:33 -03:00
victortaelin
fdcc92eeea wake always ends with 'awake.', even when the memory fits one part
AGENTS.md tells the agent to run parts until one says 'awake.' A one-part
memory printed no terminator, so the word never arrived.
2026-07-25 17:12:11 -03:00
victortaelin
672cbbdb9c terse prompts; fix id race, torn writes, wake races, unbounded recall
Prompts were verbose and repeated the same story in every tool result, which
floods context and eats the output budget. They are now instructions only,
stated once. The 4-line wake footer is 'next: memo wake 2 246'.

Real harness caps, verified from source (Claude Code 30,000 chars, pi 50 KB /
2000 lines, Codex 10,000 tokens): the old PART_CHARS=8000 was sized against a
wrong 10 KiB figure and cost 8 calls per wake. 20000 costs 4.

Bugs found by audit:
- note assigned its id outside the lock: parallel sessions could collide
- a torn record from a crash misaligned every later record, permanently
- a note landing between two wake parts could shift a boundary and drop a line
  (wake parts now render as of an explicit T)
- recall printed unboundedly and was silently truncated by the harness
2026-07-25 15:43:08 -03:00
Victor Taelin
363ff72d8c audit fixes: UTF-8 record corruption, byte-vs-char limits
- log_slice decoded the whole buffer then sliced at 320-CHARACTER offsets;
  one multi-byte char (memory #42: 'Gestão') shifted every later record and
  crashed recall on the live store. Records are bytes: slice, then decode.
- limits counted characters while records are bytes, so 150 'ã's passed
  check() and died inside pad() mid-append. check/import/paginate now count
  UTF-8 bytes; config rejects an ENTRY_CHARS the records cannot fit.
- tests were ASCII-only (why 239k assertions missed it): UTF-8 cases added.
2026-07-25 14:49:34 -03:00
Victor Taelin
e123c89317 fixed-width records: O(1) lookup, no scanning
Finding a block meant parsing the whole tree: 1.3s at a million memories, and
the Minilins write fast enough to get there. Records are now fixed width, so
position is identity -- memory i at i*320 of LOG.txt, block [ks,(k+1)s) at
k*288 of TREE/s. One seek, no index file to keep in sync.

A directory of one file per block was the obvious alternative and is worse:
25x disk (781 MB for 31 MB of text) and finding pending work still scans.
Here a level file is a dense prefix, so its length IS the watermark: pending
costs one stat per level. Measured at 1M memories: wake 0.96s -> 0.03s,
note 1.30s -> 0.02s, disk 370 MB -> 607 MB.
2026-07-25 14:34:29 -03:00
Victor Taelin
e544f6548a wake in parts: a single-shot dump is silently truncated by every harness
Measured: Codex CLI cuts tool output at 10 KiB or 256 lines, Claude Code at
30,000 chars, pi at 50 KB -- and each drops a different piece. A 320-line
memory is ~79 KB, so waking was losing memories everywhere, silently. Observed
live on pi: memories #0-#41, the compressed ancient past, vanished.

memo wake now pages the document into parts under the strictest cap and each
part names the exact command for the next. No harness is special-cased.
2026-07-25 14:22:29 -03:00
Victor Taelin
7e3d55dad6 OptMem: a permanent, append-only memory for AI agents 2026-07-25 13:58:35 -03:00