diff --git a/memo b/memo index b7e63df..7668862 100755 --- a/memo +++ b/memo @@ -366,13 +366,13 @@ from another agent, you are that subagent: skip this section. """ -def cmd_init(args): +def cmd_init(d, args): """The one command that may create the memory directory, and the whole setup: make the store, write the size knobs, print the block the user - pastes into their agent's instruction file.""" + pastes into their agent's instruction file. Re-running it is safe: it + only ever creates what is missing, and never rewrites what is there.""" if args: die("usage: memo init") - d = memory_dir() fresh = not os.path.isdir(d) os.makedirs(os.path.join(d, "TREE"), exist_ok=True) open(log_path(d), "a").close() @@ -595,24 +595,29 @@ def cmd_import(d, args): print("%s pending. Run: memo sleep" % plural(n, "compression")) -COMMANDS = {"wake": cmd_wake, "note": cmd_note, "sleep": cmd_sleep, - "recall": cmd_recall, "forget": cmd_forget, "import": cmd_import} +COMMANDS = {"init": cmd_init, "wake": cmd_wake, "note": cmd_note, + "sleep": cmd_sleep, "recall": cmd_recall, "forget": cmd_forget, + "import": cmd_import} def main(): if len(sys.argv) < 2: print(__doc__.strip()) sys.exit(0) - if sys.argv[1] == "init": - cmd_init(sys.argv[2:]) - return - if sys.argv[1] not in COMMANDS: - print("No such command: %s\n" % sys.argv[1], file=sys.stderr) + cmd = sys.argv[1] + if cmd not in COMMANDS: + print("No such command: %s\n" % cmd, file=sys.stderr) print(__doc__.strip(), file=sys.stderr) sys.exit(1) + # `init` is the only command that may run without an existing memory: + # it is the one that creates it. Every other command refuses, so a typo + # in MEMORY_DIR is an error instead of a second, empty identity. + if cmd == "init": + cmd_init(memory_dir(), sys.argv[2:]) + return d = store() config(d) - COMMANDS[sys.argv[1]](d, sys.argv[2:]) + COMMANDS[cmd](d, sys.argv[2:]) if __name__ == "__main__": diff --git a/test.py b/test.py index c7ff845..5d73c22 100755 --- a/test.py +++ b/test.py @@ -385,6 +385,32 @@ r = run("wake", store=d2) check(r.stdout.rstrip().endswith("You are awake."), "a one-part wake never says `You are awake.`:\n" + r.stdout) +# re-running init on a lived-in store must not touch one byte of it: the +# whole setup is idempotent, so it is safe on every provision. `init` only +# ever makedirs(exist_ok), opens LOG.txt for APPEND, and writes `config` +# when absent -- never truncating, never overwriting a tuned config. +def fingerprint(path): + out = {} + for root, _, files in os.walk(path): + for f in files: + if f == ".lock": + continue + p = os.path.join(root, f) + out[os.path.relpath(p, path)] = open(p, "rb").read() + return out + + +with open(os.path.join(d, "config"), "a") as f: + f.write("WAKE_LINES=120\n") # a size the user tuned by hand +before = fingerprint(d) +check(len(before) > 3 and before["LOG.txt"], "the store under test is empty") +for _ in range(3): + r = run("init") + check(r.returncode == 0 and "Found" in r.stdout, "init on a live store failed") +check(fingerprint(d) == before, "init modified an existing memory") +r = run("wake") +check(r.stdout.rstrip().endswith("You are awake."), "wake broke after re-init") + shutil.rmtree(d2) shutil.rmtree(d) print("\n%d passed, %d failed" % (ok, fail))