one file: blocks.py folded into memo; sizes editable with memo config
This commit is contained in:
parent
efde13f0c1
commit
d25f09ddc0
5 changed files with 221 additions and 138 deletions
68
README.md
68
README.md
|
|
@ -32,28 +32,70 @@ background.
|
||||||
|
|
||||||
```
|
```
|
||||||
~/.optmem/
|
~/.optmem/
|
||||||
memo the tool (Python 3, no dependencies)
|
memo the tool: one file of Python 3, no dependencies
|
||||||
blocks.py which memories to read, and which to merge
|
|
||||||
memory/
|
memory/
|
||||||
LOG.txt every memory, one per line, append-only, never edited
|
LOG.txt every memory, one per line, append-only, never edited
|
||||||
TREE/ the summaries: a cache, rebuildable from the log alone
|
TREE/ the summaries: a cache, rebuildable from the log alone
|
||||||
config the sizes, all commented out
|
config the sizes, written by `memo config`
|
||||||
```
|
```
|
||||||
|
|
||||||
`WAKE_LINES` is the only size worth touching: how many lines `wake` prints
|
```sh
|
||||||
(208 ≈ 16k tokens). It is a reading budget, not a storage budget — change it
|
memo config # show the sizes
|
||||||
whenever, in either direction, and nothing is recomputed.
|
memo config WAKE_LINES=300 # how many lines wake prints (208 ≈ 16k tokens)
|
||||||
|
memo config WAKE_LINES= # back to the default
|
||||||
|
```
|
||||||
|
|
||||||
|
`WAKE_LINES` is the only size worth touching, and it is a reading budget, not
|
||||||
|
a storage budget: change it whenever, in either direction, and nothing is
|
||||||
|
recomputed.
|
||||||
|
|
||||||
Records are fixed width, so position *is* identity and every lookup is one
|
Records are fixed width, so position *is* identity and every lookup is one
|
||||||
seek. At a million memories (607 MB), `wake` takes 0.03s.
|
seek. At a million memories (607 MB), `wake` takes 0.03s.
|
||||||
|
|
||||||
Set `$MEMORY_DIR` to keep `memory/` elsewhere — a synced folder, a git repo.
|
Set `$MEMORY_DIR` to keep `memory/` elsewhere — a synced folder, a git repo.
|
||||||
|
|
||||||
## Limitations
|
## The prompt
|
||||||
|
|
||||||
Recency is the only axis: an old memory fades however important it was, and
|
This is what the installer prints, and the whole of the integration.
|
||||||
the one defence is rehearsal — note it again and it is recent again. `recall`
|
|
||||||
is regex, not semantic search. Summaries are written by the agent out of other
|
```markdown
|
||||||
summaries, so a bad one spreads upward until you `forget` it. And a wake costs
|
## Memory
|
||||||
~16k tokens, which is deliberate but not free. If you need a fact database,
|
|
||||||
use a wiki — this is for *who the agent is*.
|
Your memory is OptMem:
|
||||||
|
- The tool is `~/.optmem/memo`
|
||||||
|
- The data is `~/.optmem/memory`
|
||||||
|
|
||||||
|
OptMem survives every new session, every compaction and every change
|
||||||
|
of model or vendor. Without it you do not know who you are, or what
|
||||||
|
was already decided and tried.
|
||||||
|
|
||||||
|
### At startup: activating OptMem (mandatory)
|
||||||
|
|
||||||
|
Run `~/.optmem/memo wake` before any other tool call, in every session.
|
||||||
|
It prints in numbered parts, each ordering the next; run every one until
|
||||||
|
a part says `You are awake.` Do not stop early: part 1 is your distant
|
||||||
|
past, the last part is this week. If wake refuses because compressions
|
||||||
|
are pending, do them and run `~/.optmem/memo wake` again.
|
||||||
|
|
||||||
|
### While working: register memories (mandatory)
|
||||||
|
|
||||||
|
Call `~/.optmem/memo note "<1 line, max 280 chars>"` whenever you learn
|
||||||
|
something new, or something worth keeping happens. That covers a task
|
||||||
|
worth real effort, a fact or insight the user teaches you, anything you
|
||||||
|
learn about their life (even indirectly), any event of lasting effect.
|
||||||
|
|
||||||
|
Do not register redundant memories.
|
||||||
|
|
||||||
|
If `~/.optmem/memo note` asks a compression: do it before your next action.
|
||||||
|
|
||||||
|
Use `~/.optmem/memo recall <regex>` when a memory you need is too vague.
|
||||||
|
|
||||||
|
Never edit or delete anything under `~/.optmem/memory`: the tool manages it.
|
||||||
|
|
||||||
|
### If you're a subagent: skip everything above
|
||||||
|
|
||||||
|
Parallel sessions on this machine are all you, and may all write memories.
|
||||||
|
A subagent is not: it must never run `memo`, because it cannot judge what
|
||||||
|
is already known, and its notes would arrive duplicated and incorrectly.
|
||||||
|
Start every brief you send one with `You are a subagent. Don't run memo.`
|
||||||
|
```
|
||||||
|
|
|
||||||
75
blocks.py
75
blocks.py
|
|
@ -1,75 +0,0 @@
|
||||||
"""Block math for OptMem.
|
|
||||||
|
|
||||||
A BLOCK is an aligned power-of-two range of memories, [lo, hi), written as one
|
|
||||||
line of at most ENTRY_CHARS characters. Blocks form a binary merge tree over
|
|
||||||
LOG.txt: block [lo,hi) is the compression of [lo,mid) and [mid,hi).
|
|
||||||
|
|
||||||
Two pure functions matter:
|
|
||||||
|
|
||||||
cover(T, budget) which blocks `memo wake` prints
|
|
||||||
complete(T) every block that CAN be built, smallest first
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def _cover(T, alpha):
|
|
||||||
"""Tile [0,T) with aligned power-of-two blocks; keep a block whole iff its
|
|
||||||
size is at most `alpha` times its age. Bigger alpha = coarser = fewer lines."""
|
|
||||||
root = 1
|
|
||||||
while root < T:
|
|
||||||
root *= 2
|
|
||||||
out, stack = [], [(0, root)]
|
|
||||||
while stack:
|
|
||||||
lo, hi = stack.pop()
|
|
||||||
if lo >= T:
|
|
||||||
continue
|
|
||||||
size = hi - lo
|
|
||||||
if size > 1 and (hi > T or size > alpha * (T - lo)):
|
|
||||||
mid = (lo + hi) // 2
|
|
||||||
stack.append((mid, hi))
|
|
||||||
stack.append((lo, mid))
|
|
||||||
else:
|
|
||||||
out.append((lo, hi))
|
|
||||||
out.sort()
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def cover(T, budget):
|
|
||||||
"""The blocks `memo wake` prints: at most `budget` of them, finest near T.
|
|
||||||
|
|
||||||
Detail decays with age, so recent memories stay verbatim and ancient ones
|
|
||||||
collapse. If everything fits, nothing is compressed at all."""
|
|
||||||
if T <= 0:
|
|
||||||
return []
|
|
||||||
if T <= budget:
|
|
||||||
return [(i, i + 1) for i in range(T)]
|
|
||||||
lo, hi = 0.0, 1.0
|
|
||||||
for _ in range(60):
|
|
||||||
mid = (lo + hi) / 2
|
|
||||||
if len(_cover(T, mid)) > budget:
|
|
||||||
lo = mid
|
|
||||||
else:
|
|
||||||
hi = mid
|
|
||||||
out = _cover(T, hi)
|
|
||||||
# Block sizes jump in powers of two, so alpha alone can undershoot the
|
|
||||||
# budget. Spend what is left on the present, where detail is worth most.
|
|
||||||
while len(out) < budget:
|
|
||||||
i = max((i for i, b in enumerate(out) if b[1] - b[0] > 1), default=None)
|
|
||||||
if i is None:
|
|
||||||
break
|
|
||||||
lo_, hi_ = out[i]
|
|
||||||
mid = (lo_ + hi_) // 2
|
|
||||||
out[i:i + 1] = [(lo_, mid), (mid, hi_)]
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def complete(T):
|
|
||||||
"""Every block buildable from T memories, smallest first (so a block's
|
|
||||||
halves always come before it). This is the whole of the work that exists:
|
|
||||||
if all of these are in TREE.txt, there is nothing left to do."""
|
|
||||||
out = []
|
|
||||||
size = 2
|
|
||||||
while size <= T:
|
|
||||||
for i in range(T // size):
|
|
||||||
out.append((i * size, (i + 1) * size))
|
|
||||||
size *= 2
|
|
||||||
return out
|
|
||||||
|
|
@ -6,13 +6,10 @@
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
DIR="$HOME/.optmem"
|
DIR="$HOME/.optmem"
|
||||||
SRC="https://raw.githubusercontent.com/VictorTaelin/OptMem/main"
|
|
||||||
|
|
||||||
mkdir -p "$DIR"
|
mkdir -p "$DIR"
|
||||||
for f in memo blocks.py; do
|
curl -fsSL https://raw.githubusercontent.com/VictorTaelin/OptMem/main/memo -o "$DIR/memo.new"
|
||||||
curl -fsSL "$SRC/$f" -o "$DIR/$f.new"
|
mv "$DIR/memo.new" "$DIR/memo"
|
||||||
mv "$DIR/$f.new" "$DIR/$f"
|
|
||||||
done
|
|
||||||
chmod +x "$DIR/memo"
|
chmod +x "$DIR/memo"
|
||||||
|
|
||||||
exec "$DIR/memo" init
|
exec "$DIR/memo" init
|
||||||
|
|
|
||||||
181
memo
181
memo
|
|
@ -3,10 +3,11 @@
|
||||||
|
|
||||||
memo init create this machine's memory, print the setup block.
|
memo init create this machine's memory, print the setup block.
|
||||||
memo wake [part [T]] read your memory. Run first, every session.
|
memo wake [part [T]] read your memory. Run first, every session.
|
||||||
memo note "..." record one memory: one line, at most 280 chars.
|
memo note "..." record one memory: one short line.
|
||||||
memo sleep [id "..."] do the pending compressions.
|
memo sleep [id "..."] do the pending compressions.
|
||||||
memo recall <regex> search every memory ever recorded.
|
memo recall <regex> search every memory ever recorded.
|
||||||
memo forget <lo>-<hi> drop a bad summary; sleep rebuilds it.
|
memo forget <lo>-<hi> drop a bad summary; sleep rebuilds it.
|
||||||
|
memo config [NAME=N] show this memory's sizes, or change one.
|
||||||
memo import <file> bulk-load dated memories (bootstrap only).
|
memo import <file> bulk-load dated memories (bootstrap only).
|
||||||
|
|
||||||
The memories live in ~/.optmem/memory, or in $MEMORY_DIR if set. See README.md.
|
The memories live in ~/.optmem/memory, or in $MEMORY_DIR if set. See README.md.
|
||||||
|
|
@ -18,19 +19,25 @@ import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
|
# The sizes a memory may override in its own `config` file: the default, and
|
||||||
from blocks import cover # noqa: E402
|
# what it means. `memo config` shows and edits them. The globals below start
|
||||||
|
# at these defaults, so a memory that overrides nothing follows the tool.
|
||||||
ENTRY_CHARS = 280
|
KNOBS = {
|
||||||
WAKE_LINES = 208 # ~16k tokens of dense text, in 3 parts
|
"WAKE_LINES": (208, "the memory context: how many lines wake prints"),
|
||||||
RAW_MAX = 16 # blocks up to this many memories compress from the raw log
|
"ENTRY_CHARS": (280, "the longest one memory may be, in bytes"),
|
||||||
|
"PART_CHARS": (20000, "output paging: largest part, in bytes"),
|
||||||
|
"PART_LINES": (500, "output paging: largest part, in lines"),
|
||||||
|
}
|
||||||
|
WAKE_LINES = KNOBS["WAKE_LINES"][0] # ~16k tokens of dense text, in 3 parts
|
||||||
|
ENTRY_CHARS = KNOBS["ENTRY_CHARS"][0]
|
||||||
# Every harness truncates a command that prints too much, and each drops a
|
# Every harness truncates a command that prints too much, and each drops a
|
||||||
# different piece: Claude Code cuts the middle at 30,000 chars, pi cuts the
|
# different piece: Claude Code cuts the middle at 30,000 chars, pi cuts the
|
||||||
# head at 50 KB, Codex budgets 10,000 tokens. So the memory is handed over in
|
# head at 50 KB, Codex budgets 10,000 tokens. So the memory is handed over in
|
||||||
# parts that fit all of them. These are transport limits, not memory limits.
|
# parts that fit all of them. These are transport limits, not memory limits.
|
||||||
PART_CHARS = 20000
|
PART_CHARS = KNOBS["PART_CHARS"][0]
|
||||||
PART_LINES = 500
|
PART_LINES = KNOBS["PART_LINES"][0]
|
||||||
|
|
||||||
|
RAW_MAX = 16 # blocks up to this many memories compress from the raw log
|
||||||
|
|
||||||
|
|
||||||
# Records are FIXED WIDTH, so a memory or a block is found by seeking to its
|
# Records are FIXED WIDTH, so a memory or a block is found by seeking to its
|
||||||
|
|
@ -41,6 +48,64 @@ LOG_REC = 320
|
||||||
TREE_REC = 288
|
TREE_REC = 288
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- blocks
|
||||||
|
|
||||||
|
# A BLOCK is an aligned power-of-two range of memories, [lo,hi), compressed
|
||||||
|
# into one line. Blocks form a binary merge tree over LOG.txt: block [lo,hi)
|
||||||
|
# is the compression of [lo,mid) and [mid,hi).
|
||||||
|
|
||||||
|
def _cover(T, alpha):
|
||||||
|
"""Tile [0,T) with aligned power-of-two blocks; keep a block whole iff its
|
||||||
|
size is at most `alpha` times its age. Bigger alpha = coarser = fewer
|
||||||
|
lines."""
|
||||||
|
root = 1
|
||||||
|
while root < T:
|
||||||
|
root *= 2
|
||||||
|
out, stack = [], [(0, root)]
|
||||||
|
while stack:
|
||||||
|
lo, hi = stack.pop()
|
||||||
|
if lo >= T:
|
||||||
|
continue
|
||||||
|
size = hi - lo
|
||||||
|
if size > 1 and (hi > T or size > alpha * (T - lo)):
|
||||||
|
mid = (lo + hi) // 2
|
||||||
|
stack.append((mid, hi))
|
||||||
|
stack.append((lo, mid))
|
||||||
|
else:
|
||||||
|
out.append((lo, hi))
|
||||||
|
out.sort()
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def cover(T, budget):
|
||||||
|
"""The blocks `memo wake` prints: at most `budget` of them, finest near T.
|
||||||
|
|
||||||
|
Detail decays with age, so recent memories stay verbatim and ancient ones
|
||||||
|
collapse. If everything fits, nothing is compressed at all."""
|
||||||
|
if T <= 0:
|
||||||
|
return []
|
||||||
|
if T <= budget:
|
||||||
|
return [(i, i + 1) for i in range(T)]
|
||||||
|
lo, hi = 0.0, 1.0
|
||||||
|
for _ in range(60):
|
||||||
|
mid = (lo + hi) / 2
|
||||||
|
if len(_cover(T, mid)) > budget:
|
||||||
|
lo = mid
|
||||||
|
else:
|
||||||
|
hi = mid
|
||||||
|
out = _cover(T, hi)
|
||||||
|
# Block sizes jump in powers of two, so alpha alone can undershoot the
|
||||||
|
# budget. Spend what is left on the present, where detail is worth most.
|
||||||
|
while len(out) < budget:
|
||||||
|
i = max((i for i, b in enumerate(out) if b[1] - b[0] > 1), default=None)
|
||||||
|
if i is None:
|
||||||
|
break
|
||||||
|
lo_, hi_ = out[i]
|
||||||
|
mid = (lo_ + hi_) // 2
|
||||||
|
out[i:i + 1] = [(lo_, mid), (mid, hi_)]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- store
|
# ---------------------------------------------------------------- store
|
||||||
|
|
||||||
def memory_dir():
|
def memory_dir():
|
||||||
|
|
@ -64,30 +129,51 @@ def store():
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
def config(d):
|
def size(k, v):
|
||||||
"""Optional overrides in the memory directory's `config`. `memo init`
|
"""Validate one knob, wherever it came from: the config file or argv."""
|
||||||
writes it fully commented out: an uncommented copy of the defaults would
|
if not v.isdigit() or int(v) < 1:
|
||||||
freeze them, and updating the tool would stop changing how it behaves."""
|
die("%s must be a positive whole number, not '%s'." % (k, v))
|
||||||
global ENTRY_CHARS, WAKE_LINES, PART_CHARS, PART_LINES
|
top = min(TREE_REC - 8, LOG_REC - 40)
|
||||||
|
if k == "ENTRY_CHARS" and int(v) > top:
|
||||||
|
die("ENTRY_CHARS is at most %d: a memory has to fit the fixed-width "
|
||||||
|
"records." % top)
|
||||||
|
return int(v)
|
||||||
|
|
||||||
|
|
||||||
|
def overrides(d):
|
||||||
|
"""The knobs this memory sets for itself, read from its `config` file."""
|
||||||
|
out = {}
|
||||||
p = os.path.join(d, "config")
|
p = os.path.join(d, "config")
|
||||||
if not os.path.exists(p):
|
if not os.path.exists(p):
|
||||||
return
|
return out
|
||||||
for line in open(p):
|
for line in open(p):
|
||||||
line = line.split("#")[0].strip()
|
line = line.split("#")[0].strip()
|
||||||
if "=" not in line:
|
if "=" not in line:
|
||||||
continue
|
continue
|
||||||
k, v = (s.strip() for s in line.split("=", 1))
|
k, v = (s.strip() for s in line.split("=", 1))
|
||||||
if k == "ENTRY_CHARS":
|
if k not in KNOBS:
|
||||||
ENTRY_CHARS = int(v)
|
die("config: %s is not a size. Run: memo config" % k)
|
||||||
elif k == "WAKE_LINES":
|
out[k] = size(k, v)
|
||||||
WAKE_LINES = int(v)
|
return out
|
||||||
elif k == "PART_CHARS":
|
|
||||||
PART_CHARS = int(v)
|
|
||||||
elif k == "PART_LINES":
|
def config(d):
|
||||||
PART_LINES = int(v)
|
"""Apply this memory's overrides. A knob it does not set keeps the tool's
|
||||||
if ENTRY_CHARS > min(TREE_REC - 8, LOG_REC - 40):
|
default, so updating the tool still changes how it behaves."""
|
||||||
die("config: ENTRY_CHARS=%d does not fit the %d/%d-byte records."
|
for k, v in overrides(d).items():
|
||||||
% (ENTRY_CHARS, LOG_REC, TREE_REC))
|
globals()[k] = v # a knob's name IS the name of the global it sets
|
||||||
|
|
||||||
|
|
||||||
|
def write_config(d, over):
|
||||||
|
"""Rewrite `config`: every knob on its own line, commented out unless this
|
||||||
|
memory overrides it."""
|
||||||
|
out = ["# OptMem sizes for this memory. A commented line means: follow the",
|
||||||
|
"# tool's default. Edit with `memo config NAME=VALUE`.", ""]
|
||||||
|
for k, (default, what) in KNOBS.items():
|
||||||
|
out.append("%-2s%-12s = %-6d # %s"
|
||||||
|
% ("" if k in over else "# ", k, over.get(k, default), what))
|
||||||
|
with open(os.path.join(d, "config"), "w") as f:
|
||||||
|
f.write("\n".join(out) + "\n")
|
||||||
|
|
||||||
|
|
||||||
def log_path(d):
|
def log_path(d):
|
||||||
|
|
@ -317,16 +403,6 @@ def next_nap(d, T):
|
||||||
|
|
||||||
# ---------------------------------------------------------------- commands
|
# ---------------------------------------------------------------- commands
|
||||||
|
|
||||||
CONFIG = """\
|
|
||||||
# OptMem sizes for this memory. Uncomment a line to override it; an
|
|
||||||
# absent line tracks the tool's default.
|
|
||||||
#
|
|
||||||
# WAKE_LINES=208 # the memory context: how many lines wake prints (~16k tokens)
|
|
||||||
# ENTRY_CHARS=280 # the longest a single memory may be, in bytes
|
|
||||||
# PART_CHARS=20000 # output paging: largest part, in bytes
|
|
||||||
# PART_LINES=500 # output paging: largest part, in lines
|
|
||||||
"""
|
|
||||||
|
|
||||||
TEMPLATE = """\
|
TEMPLATE = """\
|
||||||
## Memory
|
## Memory
|
||||||
|
|
||||||
|
|
@ -380,10 +456,8 @@ def cmd_init(d, args):
|
||||||
fresh = not os.path.isdir(d)
|
fresh = not os.path.isdir(d)
|
||||||
os.makedirs(os.path.join(d, "TREE"), exist_ok=True)
|
os.makedirs(os.path.join(d, "TREE"), exist_ok=True)
|
||||||
open(log_path(d), "a").close()
|
open(log_path(d), "a").close()
|
||||||
cfg = os.path.join(d, "config")
|
if not os.path.exists(os.path.join(d, "config")):
|
||||||
if not os.path.exists(cfg):
|
write_config(d, {})
|
||||||
with open(cfg, "w") as f:
|
|
||||||
f.write(CONFIG)
|
|
||||||
config(d)
|
config(d)
|
||||||
home = os.path.expanduser("~")
|
home = os.path.expanduser("~")
|
||||||
|
|
||||||
|
|
@ -511,6 +585,29 @@ def cmd_sleep(d, args):
|
||||||
print(("\n" if said else "") + nap)
|
print(("\n" if said else "") + nap)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_config(d, args):
|
||||||
|
"""Show this memory's sizes, or change one. An empty value restores the
|
||||||
|
default. Sizes only select what is printed, so changing one is free: no
|
||||||
|
memory is touched and nothing is recomputed."""
|
||||||
|
over = overrides(d)
|
||||||
|
for a in args:
|
||||||
|
k, eq, v = a.partition("=")
|
||||||
|
k = k.strip().upper()
|
||||||
|
if not eq or k not in KNOBS:
|
||||||
|
die("usage: memo config [NAME=VALUE ...] # NAME one of %s"
|
||||||
|
% ", ".join(KNOBS))
|
||||||
|
if v.strip():
|
||||||
|
over[k] = size(k, v.strip())
|
||||||
|
else:
|
||||||
|
over.pop(k, None)
|
||||||
|
if args:
|
||||||
|
write_config(d, over)
|
||||||
|
for k, (default, what) in KNOBS.items():
|
||||||
|
print("%-12s %-7d %s%s" % (k, over.get(k, default), what,
|
||||||
|
"" if k not in over else
|
||||||
|
" (default %d)" % default))
|
||||||
|
|
||||||
|
|
||||||
def cmd_forget(d, args):
|
def cmd_forget(d, args):
|
||||||
"""A summary can be wrong -- mistyped, or a bad compression. Drop it and
|
"""A summary can be wrong -- mistyped, or a bad compression. Drop it and
|
||||||
everything built on top of it; the next sleep computes them again. The log
|
everything built on top of it; the next sleep computes them again. The log
|
||||||
|
|
@ -599,7 +696,7 @@ def cmd_import(d, args):
|
||||||
|
|
||||||
COMMANDS = {"init": cmd_init, "wake": cmd_wake, "note": cmd_note,
|
COMMANDS = {"init": cmd_init, "wake": cmd_wake, "note": cmd_note,
|
||||||
"sleep": cmd_sleep, "recall": cmd_recall, "forget": cmd_forget,
|
"sleep": cmd_sleep, "recall": cmd_recall, "forget": cmd_forget,
|
||||||
"import": cmd_import}
|
"config": cmd_config, "import": cmd_import}
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
|
||||||
28
test.py
28
test.py
|
|
@ -16,11 +16,21 @@ import tempfile
|
||||||
from importlib.machinery import SourceFileLoader
|
from importlib.machinery import SourceFileLoader
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.realpath(__file__))
|
HERE = os.path.dirname(os.path.realpath(__file__))
|
||||||
sys.path.insert(0, HERE)
|
|
||||||
from blocks import complete, cover # noqa: E402
|
|
||||||
|
|
||||||
MEMO = os.path.join(HERE, "memo")
|
MEMO = os.path.join(HERE, "memo")
|
||||||
cli = SourceFileLoader("memo_cli", MEMO).load_module()
|
cli = SourceFileLoader("memo_cli", MEMO).load_module()
|
||||||
|
cover = cli.cover
|
||||||
|
|
||||||
|
|
||||||
|
def complete(T):
|
||||||
|
"""Every block buildable from T memories, smallest first. The oracle for
|
||||||
|
the tool's `pending()`: written straight from the definition, so a bug in
|
||||||
|
the fast version (which reads level lengths, never scanning) shows up."""
|
||||||
|
out, size = [], 2
|
||||||
|
while size <= T:
|
||||||
|
out += [(i * size, (i + 1) * size) for i in range(T // size)]
|
||||||
|
size *= 2
|
||||||
|
return out
|
||||||
|
|
||||||
# The shipped defaults. A fresh process starts from these, so an in-process
|
# The shipped defaults. A fresh process starts from these, so an in-process
|
||||||
# call must too, or one store's config would leak into the next.
|
# call must too, or one store's config would leak into the next.
|
||||||
DEFAULTS = {k: getattr(cli, k) for k in
|
DEFAULTS = {k: getattr(cli, k) for k in
|
||||||
|
|
@ -400,6 +410,18 @@ def fingerprint(path):
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# `memo config` is how a size is changed: it writes the file the tool reads
|
||||||
|
# back, an empty value restores the default, and a wake obeys immediately --
|
||||||
|
# nothing is recomputed, because a size only selects what gets printed.
|
||||||
|
r = run("config", "WAKE_LINES=12")
|
||||||
|
check("12" in r.stdout and "default 208" in r.stdout, "config did not set:\n" + r.stdout)
|
||||||
|
check(len(run("wake").stdout.splitlines()) <= 13, "wake ignored the new size")
|
||||||
|
r = run("config", "WAKE_LINES=")
|
||||||
|
check("default" not in r.stdout, "an empty value did not restore the default")
|
||||||
|
check(len(run("wake").stdout.splitlines()) > 13, "the default did not come back")
|
||||||
|
for bad in ("WAKE_LINES=0", "WAKE_LINES=x", "ENTRY_CHARS=999", "NOPE=1", "WAKE_LINES"):
|
||||||
|
check(run("config", bad).returncode == 1, "config accepted %s" % bad)
|
||||||
|
|
||||||
with open(os.path.join(d, "config"), "a") as f:
|
with open(os.path.join(d, "config"), "a") as f:
|
||||||
f.write("WAKE_LINES=120\n") # a size the user tuned by hand
|
f.write("WAKE_LINES=120\n") # a size the user tuned by hand
|
||||||
before = fingerprint(d)
|
before = fingerprint(d)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue