feat(hook): propose a time-boxed lock when a directive states its own window (#1315) - #1395
Conversation
There was a problem hiding this comment.
Sorry @robotrocketscience, you have reached your weekly rate limit of 500000 diff characters.
Please try again later or upgrade to continue using Sourcery
|
Warning Review limit reached
Next review available in: 37 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (10)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR-size soft capThis PR is over the advisory size threshold:
Bigger PRs collide with more open work, which under the parallel-session workflow tends to produce repeated This is advisory only — nothing is blocked. If the size is intentional (large refactor, module removal, generated code), apply the |
Reviewer's GuideImplements time-boxed lock suggestions for chat directives that explicitly state a window, by extracting natural-language windows into File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
[claim:review:Garsecg:2026-08-06T15:52:30Z] |
Blocking finding — the widened candidate arm also feeds the autolock path, where nothing asks the userNot a review claim on this PR (it is claimed), and the diff is otherwise clean and green. The defect
# #1315: a directive that STATES its own window is a candidate too,
# whatever its type or origin. The prompt proposes; nothing is
# written until the user runs the command, so a false positive here
# costs a declined suggestion rather than a wrong expiring lock —
# which is why this does not need the H1 precision bar.
return _directive_window_spec(b.content) is not NoneThat reasoning holds for the prompt path only. The predicate has two consumers, not candidates = _collect_lock_candidates(store, session_id)
if candidates:
if _autolock_enabled(env):
_autolock_candidates(store, candidates, serr)
else:
block = _format_stop_prompt(candidates)
b.lock_level = LOCK_USER
b.locked_at = now
b.origin = ORIGIN_USER_STATED
b.lock_expires_at = None
store.update_belief(b)So under
Suggested fix — one parameter, not a redesignThe prompt path and the autolock path want different populations. Give the predicate an def _belief_is_lock_candidate(
b: "Belief", session_id: str, *, include_windowed: bool = True
) -> bool:
...
if not include_windowed:
return False
return _directive_window_spec(b.content) is not Noneand thread it from Second, smaller: the ambiguity refusal has a hole
Also worth knowing before this mergesThe green rollup is not review coverage this week — CodeRabbit is rate-limited |
Review — the confirmation gate did not hold, and the test that was supposed to prove it was watching the wrong databaseBoth defects are fixed and pushed to this branch ( Everything below was verified by running it, not by reading. Fixed — the autolock path wrote these without askingThe new candidate arm feeds This is the claim the rescope rests on — "nothing reaches the store until they do" — failing on a supported path. The outcome is also worse than the case the H1 bar guarded: autolock grants a permanent lock ( Filtered at the Fixed — the load-bearing test inspected a database no write could reachThe So the assertion this file's own docstring calls load-bearing was watching an empty file. The consequence is exactly the design it exists to exclude: making One-line fix, both fixtures now resolve one path. Mutation-checked: the write-first mutation now fails Fixed — ambiguity was blind to half its own grammar
The first contradicts "two distinct windows in one sentence refuse rather than resolving to the first"; the second and third also contradict Both patterns now feed one positional scan, so "first" means first in the sentence. A zero-length window stays in the scan as Also updated Not fixed — the window is attached to the wrong noun, and on live data it is wrong every timeThis one needs your judgment, and it is the reason I have not labelled this ready. The extractor cannot distinguish "remember this for two weeks" from "the rule is: do Y for two weeks". Both halves of the guard pass: it is a directive, and it states a countable window.
Measured rather than argued. Sweeping this repo's own store (46,019 beliefs, read-only) the new arm fires 9 times, and 0 of the 9 state a memory-retention window — they are all subject-matter durations, e.g. The confirmation gate does contain the damage — nothing is written, and with the autolock fix above that is now true on every path. But the PR's framing, that a false positive costs a declined suggestion, holds for the candidate arm and not for the What I need from you: whether to narrow the extractor so the window must attach to a memory verb ( HousekeepingBranch is FF on |
|
[release:review:Garsecg:2026-08-06T16:22:57Z] |
|
[claim:review:Setr:2026-08-06T22:02:12Z] |
Review: the open question is decided, implemented and pushed (
|
| mutation | result |
|---|---|
drop the gate from _directive_window_spec |
6 RED |
| anchor on the verb alone (no self-referential object) | 5 RED |
| drop the clause-break / gap check | 1 RED |
Full suite: 7410 passed, 70 skipped, 71 xfailed.
Two things you should know before this merges
1. The arm is now inert on live data. The same sweep after the gate fires 0 times on 44,679 beliefs. And because _belief_is_lock_candidate's #1315 clause keys on _directive_window_spec(...) is not None, the gate removes candidacy too — not just the --for suffix. So #1315 currently proposes nothing on this store, despite 8,838 active beliefs passing detect_directive.
That is the accepted recall cost, and I flagged it in the option text before the ruling rather than after. But it is worth stating plainly: this ships a correct mechanism that no current data reaches. I disclosed it in the CHANGELOG rather than leaving a reader to find out. If you would rather the candidate arm stay wide and only the suffix be gated, that is a one-line change and a different ruling — say so and I will make it.
2. Your own canonical example was the wrong shape. _DIRECTIVE in the test file, and the headline example in the CHANGELOG, were both "Always use tabs in this repo for the next week." — which states how long to use tabs, not how long to remember the rule. Both are updated. That the PR's own example was a subject-matter window is the clearest evidence available that these two readings are easy to conflate, and it is why the gate is worth its complexity.
Housekeeping
Branch is FF on main, all commits signed, no unresolved threads. Actions is still in a major outage (97 queued, 0 executing) so no required check has run on this head — the suite figure above is local, run as ci.yml invokes it. Not labelling ready-to-merge.
Also confirmed #1396 (the parse_for overflow you split out) is still open and unclaimed.
|
[claim:review:Garsecg:2026-08-06T22:56:36Z] |
|
[release:review:Garsecg:2026-08-06T22:56:41Z] |
762a442 to
0a92de4
Compare
Author pass on the six commits I did not write — five defects, four fixed here, one filed as #1440Picking my own PR back up. The prior reviewer's stated reason for not labelling this was that Before labelling I reviewed what landed on my branch while I was away, and everything below was The confirmation gate held, but the proposal did not survive the fix that made it holdWithholding #1315 candidates from The prompt is a proposal's only surface, and the block itself recommends that flag — so the The prompt described a
|
| mutation | before | after |
|---|---|---|
delete if stated_window_is_ambiguous(...) |
0 red | 5 red |
delete if not detect_directive(...) |
0 red | 1 red |
collapse the candidate arm to extract_stated_window(...) is not None |
0 red | 1 red |
delete if not stated_window_attaches_to_memory(...) |
— | 15 red |
Cause: every ambiguity fixture said Always use tabs …, which is not a memory anchor, so the
attachment gate — added upstream of the ambiguity arm after those tests were written —
refused them one gate early and every assert ... is None passed for the wrong reason. The
file already carried a detect_directive reachability guard written against exactly this trap;
the new gate reopened it somewhere that guard could not see. Fixtures are memory-anchored now
and reachability is asserted for both gates, so the next gate added upstream fails loudly
instead of silently. The candidate arm gained negative controls that state a window and fail an
upstream gate — without them it reduces to "mentions any duration anywhere".
One prose claim was false
keep CI logs for 30 days was described as "the single most common live shape". It appears in
none of the nine measured live hits, and the only beliefs in that store mentioning CI logs
state no duration. The assertion it decorates is correct and load-bearing; only the frequency
claim is gone.
Filed rather than fixed: #1440
A window outside the count/unit vocabulary (sub-day units, spelled counts above ten,
quantifier/range forms) is not recorded as a window at all — not even as unusable — so
extract_stated_window returns the window stated second and stated_window_is_ambiguous
reports one where there are two. That contradicts both functions' documented contracts, and
contradicts the treatment the module already gives the zero-length case for the stated reason.
Not fixed here because the clause-gap fix above already refuses the shapes a user would write
(…for 30 minutes, then for a week has a connective in the gap); what remains reachable at the
caller needs two for … phrases juxtaposed with no connective. So the shipped path is not
wrong today — the violation is on the two exported functions and is inherited by any future
caller that consults them without the attachment gate in front. It wants its own test matrix
rather than a widening of this PR.
Housekeeping
Rebased onto main (7d48c958, picking up #1403 and #1421); FF, 13 commits, all signed, no
unresolved threads. Full suite 7,436 passed, 70 skipped, 71 xfailed. Discretion grep clean
on added lines. The CHANGELOG entry now states the autolock fall-through, the prompt-copy
change and the clause gate, none of which it described before.
Two things a reader should know that the diff does not say. The arm remains inert on live
data — that is the accepted recall cost from the ruling, and the clause gate above makes the
gate slightly stricter still, so it stays inert. And #1396 (the parse_for overflow split
out of this branch) is still open and unclaimed.
|
[claim:review:Garsecg:2026-08-09T03:53:42Z] |
|
[release:review:Garsecg:2026-08-09T03:53:47Z] |
0a92de4 to
f998017
Compare
Ruling 3 implemented: candidacy decoupled from the
|
| coupled (before) | decoupled (now) | |
|---|---|---|
| #1315 candidates | 0 | 3,003 |
of those, rendering a --for |
0 | 0 |
So the recall cost the earlier ruling accepted is real and unchanged — it is
confined to the window. What the decoupling buys is that the feature reaches
data at all. Types of the 3,003: factual/user_transcript 2,224,
requirement/user_transcript 449, factual/unknown 282, rest preference.
That confirms the changelog's existing "factual or requirement" claim rather
than contradicting it.
Mutations, re-run against the decoupled code
| mutation | result |
|---|---|
re-couple candidacy to _directive_window_spec (the superseded shape) |
2 RED |
drop stated_window_attaches_to_memory from the suffix |
16 RED |
drop the _belief_is_correction_class filter feeding autolock |
1 RED |
drop the detect_directive guard inside _directive_window_spec |
1 RED |
Control green. __pycache__ cleared between each — same-second same-size edits
otherwise report the opposite result.
The guard I did not delete, and why
Deleting detect_directive from _directive_window_spec as now-redundant is
the obvious cleanup the decoupling invites, and it is wrong.
_format_stop_prompt renders a suffix for every candidate, and a
correction-class candidate becomes one by type or origin having never been
through the directive arm. A type=correction belief reading "Why would
anyone retain this for two years?" clears attachment and extraction and would
render --for 2y on a question. There is now a test that fails if the guard
goes.
Four false claims removed from prose
CHANGELOG/v4.mdsaid the candidate arm "is inert on live data too". After
the ruling that is the opposite of what ships.docs/concepts/PHILOSOPHY.mdtold a safety auditor to check
_belief_is_lock_candidateas the populationAELF_AUTOLOCK_CORRECTIONS=1
writes. It is_belief_is_correction_class. This was already wrong on this
branch before the decoupling, and the widening turns it into a false alarm
reporting 3,003 proposal-only beliefs as auto-lockable — the direction that
gets a correct safety filter reverted. The#1163changelog entry repeated it
verbatim while claiming to be test-pinned.- That pin did not hold:
test_docs_locks_framing_1163.py's negative control
passes because_mkhard-codes non-directive content, so it cannot see a
candidacy change in either direction. It now carries a directive belief and
asserts the two predicates disagree on it. aelf setup --stop-hookhelp,INSTALL.mdandARCHITECTURE.mdall
described the prompt as correction-class only, and the setup help said the
flag auto-locks instead of prompting. It writes only the correction-class
subset; the rest still print.PRIVACY.mdis deliberately untouched — its
sentence remains exactly true.
Acceptance criteria, dispositioned
Not claiming a clean sweep. Two need an operator call before this closes #1315.
| AC | verdict |
|---|---|
| 1 — proposal names the resolved absolute expiry | DEVIATED. We render the --for spec (1w), never the resolved instant. Deliberate: binding constraints 1 and 2 forbid a second date parser and a second anchor, and resolving at prompt time is exactly the moving-anchor shape that caused this repo's temporal-sort underflow. The AC's literal wording and the constraints cannot both be satisfied. |
2 — confirmation writes what aelf lock --for writes, via parse_for |
Met by construction — confirmation is running that command; there is no second write path. Asserted only to the extent that parse_for accepts every spec we emit. |
| 3 — declining writes nothing | Met, test_proposing_writes_nothing_to_the_store. |
| 4 — ambiguous window refuses and asks | Met, and the decoupling improves it. Coupled, an ambiguous window refused and did not ask. Now the belief is still proposed, with no --for — which is what "refuses and asks" says. |
| 5 — the anchor is persisted with the resolved expiry | APPROXIMATE, and unasserted. _cmd_lock persists locked_at and lock_expires_at, but they come from two separate clock reads — parse_for(…, now=datetime.now(timezone.utc)) before _open_store(), and _utc_now_iso() after it. So the persisted anchor is the resolved anchor plus store-open latency, not the anchor itself. Immaterial at 1w; it is still not what the AC says. This is #1314 code, byte-identical on main, and not this PR's defect. |
| 6 — a test distinguishing proposal from write | Met, and materially stronger now: it guards a 3,003-belief live population instead of an empty one. |
Filed rather than fixed: #1442
_format_stop_prompt bounds neither the candidate count nor any belief's
length, and stop() fires once per assistant turn. Worst live session renders a
3,448,428-byte stderr block of 6,427 entries; longest single belief is
14,360 characters on one aelf lock '…' line.
This is pre-existing, and I want to be precise about that because the
obvious reading is that the widening causes it. It does not: the same sweep over
the pre-#1315 population gives a byte-identical maximum — the worst session's
candidates are all correction-class. p90 actually falls (15,811 → 14,977 B);
p99 rises (90,792 → 126,419 B). So the tail is a property of
_format_stop_prompt on main today and the widening moves the distribution
modestly. Filed as #1442 rather than widened into this PR.
Housekeeping
Rebased onto main (23d4d21d); FF, 17 commits, all signed. Five CHANGELOG
conflicts resolved by hand, one at a time under the standing rule — each
resolution asserted that no pre-existing entry was dropped, and the final
CHANGELOG/v4.md differs from main by exactly one added entry (128 → 129)
plus the #1163 predicate correction.
Full suite: 7,460 passed, 70 skipped, 71 xfailed, plus one flake —
test_uninstall_dotdir.py::test_dotdir_plan_scales_linearly, a wall-clock
scaling assertion that fails ~1 in 3 in isolation and is untouched by this diff.
Not labelling ready-to-merge: #1425 is ruled to merge first, and it
collides with CHANGELOG/v4.md.
|
[claim:review:Garsecg:2026-08-09T06:25:51Z] |
|
[release:review:Garsecg:2026-08-09T06:25:56Z] |
f998017 to
3e0728f
Compare
|
merge-train: blocked branch is not fast-forward on The |
… window Per the operator ruling of 2026-08-06, this lane is confirmation-gated: the detector proposes a pre-filled `aelf lock ... --for <spec>` command and the user runs it. Nothing reaches the store until they do, which is why the H1 precision bar stopped being the blocker — a false positive costs a declined suggestion rather than a silently-wrong expiring lock. Reuses the surface that already exists. The Stop hook renders pre-filled `aelf lock` commands for lock candidates; this appends the window when the belief's own text states one, and admits a windowed directive as a candidate whatever its type or origin. `extract_stated_window` only maps windows the user SPELLED OUT. Every pattern requires an explicit unit word, so "for the trip" and "until I'm back" return None and the caller proposes nothing — inferring an expiry the user did not state is an explicit non-goal, and a guessed window expires their lock on a date they never agreed to. Two distinct windows in one sentence refuse rather than resolving to the first. No second date parser and no second anchor: this renders the `--for` spec, and `aelf lock --for` resolves it to an absolute UTC instant at write time through the existing `parse_for`. Closes #1315.
States why the H1 precision bar no longer applies rather than implying it was met: under confirm-before-write a false positive costs a declined suggestion, not a wrong expiring lock. Refs #1315.
`stated_window_is_ambiguous` scanned only `_STATED_WINDOW_RE`, which requires a count word, so the bare "for the next <unit>" form was invisible to it. A sentence naming one window in each spelling therefore reported a single window and resolved to whichever one the counted pattern could see: "for the next week, then for two days" proposed `2d` — the window stated second, which also contradicts the documented "first stated wins" rule — and "for the next week, and for the next month" proposed `1w` with no refusal at all. Both patterns now feed one positional scan. "First" means first in the sentence rather than first pattern tried, and a zero-length window stays in the scan as `None` so that it still counts as a stated window instead of being silently discarded next to a usable one. Four test arms, each mutation-checked: narrowing the scan back to one pattern fails three, and dropping zero-length windows fails the fourth.
…admits The #1315 arm widened `_belief_is_lock_candidate` beyond correction-class beliefs, but its docstring still said "iff ... correction-class" and listed only the two original signals — so the contract text now excluded the case the new clause exists to admit. Also states that the session and lock guards precede the new arm, which is what makes "an already-locked directive is still not a candidate" true for the right reason rather than by luck of ordering.
… reach The `store` fixture opened `tmp_path/"prop.db"` while the autouse fixture set `AELFRICE_DB` to `tmp_path/"pinned.db"`, and `_open_store()` resolves `$AELFRICE_DB`. So the one assertion the file calls load-bearing — that proposing writes nothing — inspected a database no production write could ever land in. The consequence is the exact design the test exists to exclude: making `_directive_window_spec` open the production store and lock the belief before returning the spec leaves the whole file green. That is the write-first-ask-second shape the H1 precision bar existed to prevent, and the PR's argument for retiring the bar rests on this test. Both fixtures now resolve one path. Mutation-checked: the write-first mutation fails `test_proposing_writes_nothing_to_the_store`, and passed before this change.
The new candidate arm fed `_collect_lock_candidates`, whose output goes
to `_autolock_candidates` as well as to the prompt. With
`AELF_AUTOLOCK_CORRECTIONS=1` a windowed directive was therefore written
with no confirmation at all — A/B against main on one seeded belief:
PR head: auto-locked B1 (factual, origin→user_stated)
lock_level='user' lock_expires_at=None origin='user_stated'
main: lock_level='none' origin='user_transcript' (untouched)
That is the claim the PR rests on ("nothing reaches the store until they
do") failing on a documented, supported path, and the outcome is worse
than the one the H1 precision bar guarded against: autolock grants a
*permanent* lock and discards the window, so "for the next week" becomes
forever, on a detector measured at P=0.665, with the origin laundered to
user_stated.
The filter goes at the `stop()` call site rather than inside
`_autolock_candidates`, which locks exactly what it is handed and has
callers that legitimately pass non-correction beliefs.
Test drives `stop()` end-to-end, since a unit test of the helper cannot
see a defect about which candidates reach it. The correction beside the
directive is the control, so a filter that disabled autolock outright
would fail too. Mutation-checked: dropping the filter fails it.
The entry claimed the design is confirmation-gated by construction and that nothing reaches the store until the user runs the command. That is only true because autolock is now explicitly held to correction-class beliefs, so the published text says so rather than leaving the reader to infer an invariant from its absence.
…tter's Operator ruling 2026-08-06, over the alternatives of shipping the suffix as-is or dropping --for and landing only the candidate arm. The extractor could not tell "remember this for two weeks" from "the rule is: do Y for two weeks". Both are directives and both state a countable window; only the first says how long to remember. Swept over the live store (44,679 active beliefs, read-only) the arm fired 9 times and 0 of the 9 stated a retention window -- Blocked for 9 days, traveling for a week, Results available for 29 days. Realized attachment precision was 0/9 here and 0/90 across other local stores, and the proposal is a copy-pasteable 'aelf lock ... --for 9d'. stated_window_attaches_to_memory requires a memory verb AND a self-referential object, then the window within one short phrase with no clause break. The object is what carries it: the verb alone still admits "keep CI logs for 30 days", the single most common live shape. The anchor deliberately does not absorb trailing words -- an anchor that did would absorb the window too and never match, which is how the first cut of this rejected every input including its own positives. The cost is recall and on current data it is total: the same sweep now fires 0 times. The candidate arm keys on the same spec, so #1315 is inert on live data until someone states a window about retention. That is disclosed in the CHANGELOG rather than left for a reader to discover. The test fixture _DIRECTIVE was itself a subject-matter window ("always use tabs in this repo for the next week"), so it is updated -- the PR's own canonical example being the wrong shape is the clearest evidence available that the two are easy to conflate. The CHANGELOG headline example was the same string. Mutations, __pycache__ cleared between runs: drop the gate -> 6 RED; anchor on the verb alone -> 5 RED; drop the clause-break check -> 1 RED. Full suite 7410 passed.
Withholding a #1315 windowed directive from `_autolock_candidates` was correct, but `stop()` was an if/else, so the excluded belief never reached `_format_stop_prompt` either: under AELF_AUTOLOCK_CORRECTIONS=1 it was neither locked nor suggested. The prompt is a proposal's only surface, and the block itself recommends that flag, so the advice was advertising its own suppression. The correction-class subset is auto-locked as before and the remainder now falls through to the prompt. New on this branch: on main every candidate is correction-class, so the filter is a no-op and no population can be dropped.
Since #1315 the candidate population includes windowed directives, which production `derive()` types as `factual` or `requirement`, so the header announced a `requirement` row as a "correction" while the per-item line below it printed the real type. It also advertised AELF_AUTOLOCK_CORRECTIONS=1 unconditionally, which does not cover that arm: on a list of windowed directives the flag leaves the list exactly as it is. The noun is now neutral, its verb agrees with it, and the autolock line appears only when something listed would actually be auto-locked — with the "does not cover the rest" caveat only when there is a rest.
The attachment gate could be bypassed by prefixing a memory clause to a sentence it had just rejected: `Always cache the index for two weeks.` returns None, and `Always remember this and cache the index for two weeks.` proposed `--for 2w` — still how long to cache, not how long to remember. The gap check was length plus clause-punctuation only, and none of the leaking spellings needs punctuation at all: `and`, `but`, `then`, `while`, `so` and a bare dash all opened a new predicate that then owned the window. The gap must now open no new predicate. Tokens are stripped of punctuation rather than split on it, so the clause-joining bare `-` stays distinct from a compound word like `build-time`.
Three of the four gates in `_directive_window_spec` were unguarded: deleting the ambiguity check, deleting the `detect_directive` check, or collapsing the whole predicate to a bare `extract_stated_window` call each left 44/44 green. Every ambiguity fixture said "use tabs", which is not a memory anchor, so the attachment gate — added upstream of the ambiguity arm after those tests were written — refused them one gate early and the assertions passed for the wrong reason. The file already carried a `detect_directive` reachability guard against exactly this trap; the new gate reopened it somewhere the guard could not see, so the fixtures are memory-anchored now and reachability is asserted for both gates. The candidate arm gains negative controls that state a window and fail an upstream gate, without which it reduces to "mentions any duration". Also drops a frequency claim that was not measured: `keep CI logs for 30 days` appears in none of the nine live hits.
Three behaviour changes since the entry was written: what autolock may not write is now proposed rather than dropped, the prompt block counts beliefs rather than corrections and offers the flag only where it applies, and the attachment gate refuses a window whose governing clause is not the memory one.
Operator ruling of 2026-08-06 (~23:15Z), superseding the narrow reading of the window-attachment ruling earlier the same day. `_belief_is_lock_candidate` no longer keys on `_directive_window_spec(...) is not None`; it keys on `detect_directive` alone. The ambiguity and memory-verb-attachment gates exist to keep a wrong expiry literal off the rendered command, never to withhold the proposal, so they move to the `--for` suffix only. Coupled, the arm proposed nothing: 0 candidates against 44,683 active beliefs on this repo's store, 3,003 of which read as directives. Those 3,003 are now proposed as permanent locks and 0 carry a `--for`, so the recall cost stays confined to the window. `_directive_window_spec` keeps its own `detect_directive` guard even though candidacy applies one upstream. It is not redundant: `_format_stop_prompt` renders a suffix for every candidate, and a correction-class candidate arrives having never been through the directive arm — a correction reading "Why would anyone retain this for two years?" would otherwise render `--for 2y`. Pinned by a test.
The safety paragraph told an auditor to check `hook._belief_is_lock_candidate`. The population `AELF_AUTOLOCK_CORRECTIONS=1` writes is `_belief_is_correction_class` — the filter at the `_autolock_candidates` call site — and the two came apart in #1315. Naming candidacy reports 3,003 proposal-only beliefs as auto-lockable, which is a false alarm in the direction that gets a correct filter reverted. `test_docs_locks_framing_1163.py` claimed to pin this and could not: `_mk` hard-codes non-directive content, so its negative control passes whatever candidacy does. It now carries a directive belief and asserts the two predicates disagree on it.
…ctions `aelf setup --stop-hook` help, INSTALL.md and ARCHITECTURE.md all described the Stop prompt as correction-class only, and the setup help also said AELF_AUTOLOCK_CORRECTIONS=1 auto-locks "instead of" prompting. Since #1315 the prompt covers directive beliefs too, and the flag writes only the correction-class subset — the rest still print. PRIVACY.md is deliberately untouched: "lets the Stop hook auto-lock session corrections at turn end" remains exactly true.
The entry disclosed that the candidate arm keys on the same spec as the suffix and is "inert on live data too". The decoupling ruling reverses that, so the sentence would have shipped as a false claim about a feature that now reaches 3,003 beliefs. Scopes the total-recall-cost figure to the `--for` suffix, records the superseding ruling with its measured population, and corrects the #1163 entry's predicate name to match PHILOSOPHY.md.
3e0728f to
968d9db
Compare
|
merge-train: merged 968d9db → |
Closes #1315. Follow-on to #1314.
Saying "always use tabs in this repo. remember this for the next week" in chat
now surfaces a pre-filled
aelf lock '…' --for 1wat session end. Any directivesurfaces a pre-filled
aelf lock '…'; the--foris added only when the beliefstates a window about its own retention.
Confirmation-gated by construction
Per the operator ruling of 2026-08-06. The detector proposes; the user runs
the command; nothing reaches the store until they do.
That is what dissolved the blocker this issue carried. Directive-detector
precision measured P=0.665 against the H1 bar of 0.80, with no open issue
owning the campaign (#199, #374, #1341 all closed) — four separate sessions
re-derived that same number. Under confirm-before-write the bar stops applying:
a false positive costs a declined suggestion, not a silently-wrong expiring
lock.
The argument is now carrying more weight than when it was written, and that is
worth stating rather than inheriting. Coupled, it covered a population of
zero. Decoupled it covers 3,003 beliefs on this repo's store, so at
P=0.665 roughly a thousand of them are expected to be proposals the user
declines. Each still costs one declined suggestion — the argument holds — but it
should be accepted against the real number, not the old one. The lever if that
volume is judged unacceptable is the block cap in #1442, not the precision bar.
Two rulings, and this branch implements both
First (earlier on 2026-08-06): the
--forwindow must be governed by amemory verb. The extractor could not tell "remember this for two weeks" from
"the rule is: do Y for two weeks", and the user is shown a copy-pasteable
aelf lock '<text>' --for 9d. Measured before the gate: the arm fired 9 timeson a 44,679-belief store and 0 of the 9 stated a retention window — all were
subject-matter durations (
Blocked for 9 days,traveling for a week).Second (~23:15Z, superseding the narrow reading of the first): candidacy
is decoupled from the
--forsuffix. The gates exist to prevent a wrongexpiry literal, never to withhold a proposal. Keying
_belief_is_lock_candidateon
_directive_window_spec(...) is not Nonemade the feature unreachable.--forThe recall cost is real and confined to the window. The decoupling is what makes
the feature reach data.
No second anchor
This renders the
--forspec;aelf lock --forresolves it to an absoluteUTC instant at write time through the existing
parse_for. That is the bindingconstraint from the issue, and the standing lesson from this repo's
temporal-sort incident, where a relative expression against a moving anchor
underflowed once real time passed it. There is no date arithmetic in this
diff — which is also why AC1 is deviated-from rather than met; see the AC
disposition in the review comment.
The extractor refuses far more than it accepts, deliberately
Every pattern requires an explicit unit word:
Nonemeans no window was stated, not use a default — and since thedecoupling it also does not mean no proposal: the belief is still offered, as
a permanent lock, which is what the user would get by typing
aelf lockthemselves.
Tests
Mutation-checked against the decoupled code,
__pycache__cleared between runs:_directive_window_specstated_window_attaches_to_memoryfrom the suffix_belief_is_correction_classfilter feeding autolockdetect_directiveguard inside_directive_window_specThe load-bearing test is
test_proposing_writes_nothing_to_the_store. Assertingonly that a directive yields a lock command would pass just as happily on a
design that writes the lock first and shows the command afterwards — which is
precisely what the precision bar existed to prevent. So it asserts the store is
untouched:
lock_level,lock_expires_at, andcount_feedback_events() == 0.The fourth mutation is the one worth keeping: deleting
detect_directivefrom_directive_window_specas now-redundant is the obvious cleanup the decouplinginvites. It is wrong —
_format_stop_promptrenders a suffix for everycandidate, and a correction-class candidate arrives having never been through
the directive arm, so a correction reading "Why would anyone retain this for
two years?" would render
--for 2yon a question.Prose corrections that came out of this
PHILOSOPHY.mdpointed a safety audit at_belief_is_lock_candidateas thepopulation
AELF_AUTOLOCK_CORRECTIONS=1writes. It is_belief_is_correction_class— already wrong before this branch, and thewidening turns it into a false alarm about 3,003 proposal-only beliefs. The
#1163changelog entry repeated it while claiming to be test-pinned, and thepin could not see it. Also corrected:
aelf setup --stop-hookhelp,INSTALL.md,ARCHITECTURE.md.PRIVACY.mddeliberately untouched.Filed rather than fixed
_format_stop_promptbounds neither candidate count nor contentlength; worst live session is a 3.4 MB stderr block re-emitted every turn.
Pre-existing: the same sweep over the pre-feat: confirmation-gated natural-language capture for time-boxed locks (follow-on to #1314) #1315 population gives a
byte-identical maximum, so the widening moves the distribution, not the tail.
window at all, so the two exported
lock_expiryfunctions violate theirdocumented contracts for any future caller that skips the attachment gate.
parse_forlets a window pastdatetime.maxescape asOverflowError. Byte-identical onmain; now fixed by PR fix(lock-expiry): wrap an unrepresentable --for window in LockExpiryError (#1396) #1411.Merge order
#1425 merges first per the operator ruling — it collides with
CHANGELOG/v4.md. #1411 is the sensible second, since this PR renders--forspecs straight into a command the user pastes and #1411 is what makes an
unrepresentable one fail as a
LockExpiryError.