32db56c0dffdc19ef6640c9174ea294329b50080
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0e10f6bb8a |
feat(rules)!: retire the always-on tier — every rule arrives by retrieval (#394)
CI & Build / Python lint (push) Failing after 3s
CI & Build / Plugin hooks (push) Failing after 12s
CI & Build / integration (push) Failing after 27s
CI & Build / TypeScript typecheck (push) Failing after 35s
CI & Build / Python tests (push) Failing after 37s
CI & Build / Build & push image (push) Skipped
Milestone 394, steps 5-8. Operator: "remove the always on rule functionality as the goal was to not have it at all since it didn't seem to work as expected." Unconditional preload had three failures the retrieval arms do not. It could not be MEASURED — a resident rule is in the context whether or not it mattered, so nothing distinguished "this governed the act" from "this was scenery", and it was the one surface structurally exempt from the scoreboard judging every other. It was SUMMARISED AWAY by compaction while the session went on believing it held the rules. And it CROWDED OUT the few rules that applied with the thirty that did not. WHAT GOES Schema (0100): rules.tier + ck_rules_tier, rule_versions.tier, rulebooks.always_on, and project_rulebook_exclusions — a table recording a project's opt-out of something that no longer binds it unasked. Tools: list_always_on_rules, exclude_always_on_rulebook, include_always_on_rulebook. Service: the same three plus rules_etag_for, _valid_tier and the whole etag family. The SessionStart preload and the write-path staleness arm go with them: nothing is resident, so nothing can have drifted since a session loaded it. THREE CALLS WORTH REVIEWING enter_project got NARROWER, not wider. Its filter was `always_on OR area-tagged`; dropping the tier arm leaves the deterministic half, so a project with no canonical-tagged Systems gets no bulk rules and reaches them by retrieval instead. Dropping the whole clause would have made that payload bigger than the preload this milestone deletes. Backups import tolerantly. A pre-394 archive carries tier, always_on and the retired inception choice; none is read, and the exclusion key is DROPPED rather than remapped, because restoring it would write data that validate_inception now rejects as unknown. The migration is irreversible in the way that matters and says so: downgrade recreates the columns at their defaults and cannot restore which rules were always-on. A value invented to fill a hole is not a measurement. THE INSTRUCTION SURFACES SAY THE HARDER THING Deleting "call list_always_on_rules()" is easy; replacing it is not, because the new model asks a session to trust something it cannot see. All three surfaces now say a session holds nothing, that rules arrive when work matches them, and — the half that got dangerous — that "no rule arrived" means "nothing matched", never "there is no rule". Under residency an empty session was rare and suspicious; it is now the ordinary state of most turns, so reading it as permission is wrong on nearly every turn rather than occasionally. That is #3720's defect at session scale. test_instruction_surfaces_agree is repointed rather than retired: its two halves collapsed into one instruction, and it gains a guard that every surface states what absence means. _INSTRUCTIONS is back at 1999/2000 — the inception clause paid for the longer HOW line. UI (rule 27, and the opportunity step 8 named) The tier selector is gone, and what replaces it is the point: `when_to_apply` is now the field that decides whether a rule is ever seen, so the editor marks it required, warns while it is empty, and both rule lists badge a trigger-less rule "never surfaces". A rule without one is not quiet, it is unreachable. TESTS Two files deleted outright — test_rules_etag.py and test_inception_rules.py tested subsystems that no longer exist. Elsewhere obsolete cases were removed and the rest repointed. One deserves naming: the wiring test asserted the act arms pass no `tier`, which had become an assertion that could not fail. It is repointed onto `kind`, which does still exist and where the same claim is live — a preference must reach a write exactly as a rule does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011cPyzNnegXHr5iRMzzy5KJ |
||
|
|
c1aa1d8e92 |
feat(plugin): rule exclusions age, so salience decays without a context event (#3751)
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 38s
CI & Build / integration (push) Successful in 45s
CI & Build / Python tests (push) Successful in 1m9s
CI & Build / Build & push image (push) Successful in 27s
#3749 clears the ledger when an EVENT destroys context — a compaction, a /clear. This is the case with no event at all: a long session where a rule was named two hundred turns ago and has simply fallen out of attention. It is #3702's argument at the tier level — present in context and salient at the moment are different properties — applied to time instead of to tier. FORMAT: `id<TAB>epoch`, per entry. Not a whole-file mtime: that is one line of shell and wrong in exactly the session that needs it, since a single recent write keeps every stale id alive, and the ids that go stale first come from the rules that fire most. Not a turn counter, though it would be the truer model — an idle session does not forget. A hook has no turn number without keeping its own, which is a second piece of session state to write, read, clear on compaction and get wrong. Wall time costs a `date` call. The failure it accepts is a session left idle over lunch treating its rules as forgotten, worth one extra full line per rule and nothing else. TTL 2700s (45 minutes), reasoned rather than picked (rule 32). About one working stretch on a single task: long enough that a rule does not re-announce itself while you are still doing the thing it governs, short enough that a multi-hour session gets a refresh rather than one 9am mention. It leans short because since #3750 being wrong on the short side is the cheaper error — an expired entry costs one full line instead of one short one, and the exclusion re-arms the moment it is spent. There is no data on this yet; #3807's near-miss listing is what should revise it. BOTH READERS THROUGH ONE HELPER, in scribe_defs.sh. The two hooks share one ledger so a rule named by one arm is not re-offered by the other; a format only one of them understood would break that on the first read. The flat `tr '\n' ','` read would now send `156<TAB>1789002860` as an exclude id — verified, which is why this is not a per-hook edit. THE LAST ENTRY FOR AN ID WINS. The file is append-only, so a rule that ages out, is surfaced fresh and is appended again has two lines. Reading the first leaves it permanently expired, and it then re-announces itself on every call for the rest of the session — the mechanism meant to quieten things becoming the loudest thing in the hint. A BARE ID IS LIVE. That is the pre-#3751 format, and every session in flight when this ships has a ledger full of them. Reading unknown as EXPIRED would make all of those sessions re-announce every rule they had already been told, at once — the exact noise this prevents, delivered by the feature on the day it ships. Unknown means "not measured", never "old", the same discipline the nullable retrieval_logs columns use. GUARDS (tests/test_rule_ledger_ageing.py, real shell, no credentials) - old ages out AND recent survives, in ONE assertion (rule 167): either half alone passes against a broken helper — "old is gone" passes against one returning nothing, "recent survives" passes against the flat read this replaces, i.e. against the defect itself. - the ping-pong case, which is the one that costs the most to get wrong. - a bare id is live, including beside a stale stamped one. - missing/empty ledger excludes nothing. - an id repeated in the ledger appears once, with no empty list element. - structural: neither hook reads the rule ledger flat again — pinned on the flat-read SHAPE, so a rename of the helper is not a failure and a hook that ages correctly some other way is not either. The TTL's VALUE is deliberately not asserted. The tests read the constant out of the shell and assert the property around it, so a later tuning change stays a tuning change instead of a red build. Dropped a boundary test (`ttl` vs `ttl + 1`) before committing: racy by construction, since a ledger written at T is read at T+n and the two cases swap. A one-second distinction on a 45-minute window is also not observable behaviour, so it pinned a flake rather than a property. Plugin version minted to 2026.09.10.0221 — this is entirely hook-side, so without the bump the cache would never pick it up and the merge would ship nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011cPyzNnegXHr5iRMzzy5KJ |