Repository navigation
fix(remember): propagate decisions written under an agent context to team scope (#561) - #565
Merged
Merged
Conversation
Owner
Author
|
ZETETIC-REVIEW: APPROVE Reviewed at afc89e5 against #561 and the loop's standard.
|
…team scope (#561) ADR-0200's Transactive Memory rule (a decision written under an agent context is marked is_global) lived only in core/memory_ingest.py, which only benchmarks and tests call. The live remember path never applied it, so the SessionStart "Team Decisions" block and the subagent briefing, which both require is_protected AND is_global AND agent_context, found almost nothing: on the live store, 233 current protected agent-scoped rows were local and 1 was global (2026-09-15). - core/global_detector: propagates_to_team (the ADR-0200 predicate) and resolve_global_scope (explicit > team decision > content detector). memory_ingest uses the same predicate instead of an inline copy. - handlers/remember: resolves is_global through resolve_global_scope. The decision cue only counts for origins allowed a content-derived privilege (capture_origin: deliberate, local_action), so a fetched page carrying a decision cue cannot inject itself into every project. - infrastructure/team_scope_backfill: one-shot, idempotent backfill of rows written before the fix, applied by the PG DDL list and at SQLite store init. Same origin rule; anchored, benchmark, superseded, legacy and unknown-origin rows keep their scope. - hooks/session_start: the Team Decisions docstring now names the real write-time mechanism. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Unattended captures (write_class auto) and connection-rooted scopes tag every write with an agent topic; a decision cue in tool output is not a considered decision. The write path and the backfill now require the deliberate class, alongside the trusted-origin rule. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The attested-decision predicate (deliberate class, trusted origin, decision cue, agent context) is business logic: it moves out of the handler into a pure core module next to propagates_to_team, which keeps remember.py from growing further past its baselined size and gives the rule direct unit tests. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
cdeust
force-pushed
the
fix/561-decision-team-scope
branch
from
September 15, 2026 02:06
afc89e5 to
58c92e5
Compare
Owner
Author
|
ZETETIC-REVIEW: APPROVE Re-verified at 58c92e5, the same three commits rebased onto 1d5a534 (#566, docs only, no file in common). The new tests, tests_py/invariants and the bulk-oracle tests pass on the rebased state: 144 passed on PostgreSQL. The craftsmanship gate passes. CI is green on the rebased head: 24 passed, 4 skipped. |
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ADR-0200's Transactive Memory rule (a decision written under an agent context is marked
is_global) lived only incore/memory_ingest.py, which only benchmarks and tests call. The liverememberpath never applied it, so the SessionStart "Team Decisions" block and the subagent briefing, which both selectis_protected AND is_global AND agent_context <> ''(hooks/session_start.py:207-239), found almost nothing. Measured on the live store on 2026-09-15: 233 current protected agent-scoped rows were local, 1 was global.Closes #561
What changed
core/team_scope.py(new, pure):propagates_to_team(is_decision, agent_context)is the ADR-0200 predicate;is_team_decision(content, origin, write_class, agent_context)counts a decision cue only on a deliberate write (write_class = deliberate) from an origin allowed a content-derived privilege (capture_origin.may_bypass_write_gate_on_content:deliberate,local_action). Without the origin guard, a fetched page carrying a decision cue and written under an agent topic would be injected into every project's SessionStart; without the class guard, a connection-rooted server (which tags every write, captures included, with its agent topic) would promote tool output to team decisions. Both are pinned by tests.core/memory_ingest.pynow callspropagates_to_teaminstead of an inline copy (behaviour unchanged there).core/global_detector.py:resolve_global_scope(content, tags, *, explicit, team_decision)orders the three sources: explicit request, then team decision, then the existing content detector.handlers/remember.py:is_globalgoes throughresolve_global_scope(..., team_decision=is_team_decision(...)).infrastructure/team_scope_backfill.py: a one-shot, idempotent backfill for rows written before the fix. PostgreSQL runs it fromget_all_ddl()right afterMIGRATIONS_DDL(same pattern as the ADR-0537is_benchmarkbackfill); SQLite runs it at store init after the column migrations. Same origin and class rules as the write path. Anchored rows (_anchor, explicit act with its ownis_globalargument), benchmark rows, superseded rows, andlegacy/unknownorigins keep their scope.hooks/session_start.py: the Team Decisions docstring names the real write-time mechanism.tests_py/handlers/_remember_bulk_fakes.py: the frozen W3-4 reference body ofrememberstill callsdetect_global, which the live module no longer imports; the loader supplies it explicitly so the oracle runs unchanged.tests_py/invariants/test_I2_canonical_writer.py: the three SQLiteheat_basewriter lines moved by 4 (import + backfill call); list updated, no new writer.Migration decision for existing rows
The backfill uses
is_protectedas the stored trace of the decision cue: on the write pathis_protectedis set from the sameis_decision_contentthe rule reads. On the live store the PostgreSQL statement matches 131 rows (allcapture_origin = 'deliberate',write_class = 'deliberate'); 97legacyand 5unknownrows stay local, since their origin cannot be shown to be trusted.Test plan
Gate stated before the change: the three new test files fail on the old code and pass on the new.
2 failed, 3 passed, 1 skippedfor the handler and backfill files; the core test file fails at import (ImportError: cannot import name 'propagates_to_team').CORTEX_MEMORY_STORE_BACKEND=sqlite, the three new files plustest_remember.py):49 passed, 1 skipped(the PG-only backfill test skips).CORTEX_TEST_DATABASE_URL=postgresql://127.0.0.1:5432/cortex_test, the three new files):21 passed.8585 passed, 8 skipped, 459 subtests passed. An earlier full run had failedtest_I2_canonical_writer(the SQLite writer lines it pins had shifted); the list is updated in this PR.ruff check,ruff format --check(ruff 0.16.6 as inrequirements/lint.txt),python scripts/check_craftsmanship.pyOK, pyright in the CONTRIBUTING.md environment:0 errors, 0 warnings, 0 informations.check_doc_claims.py,check_project_wiki.py,check_no_deps_invariant.py,generate_repo_badges.py --checkOK.Failure modes reasoned about
test_network_origin_decision_never_propagatesandtest_auto_capture_decision_never_propagatespin it. The SQL literal duplicating the origin allow-list is pinned againstcapture_origin._ORIGINS_ALLOWED_CONTENT_BYPASSbytest_sql_origin_list_matches_the_write_path(infrastructure cannot import core).is_global=False. Indistinguishable from the default in_parse_args(the argument defaults toFalse), exactly as inmemory_ingest. A caller who needs a decision kept local omitsagent_topic.UPDATEin the schema transaction; re-running it matches no row (idempotence measured below).is_decision_content, already run on the insert path) perremember, O(len(content)). Backfill: one sequential scan. PostgreSQL runs it only when the DDL is (re)applied; on the live storeEXPLAINshows a single seq scan onmemories. SQLite runs it at every store init: measured 3.0 ms on 50 000 rows, then 3.4 ms and 0 rows updated on the second run (in-memory database, this machine, 2026-09-15).Coding-standards compliance
remember.pyandsqlite_store.pywere already over the 300-line cap (baselined) and grow by 4 lines each (380 → 384, the call; 908 → 912, import and backfill call);global_detector.pystays under the cap (279 lines).🤖 Generated with Claude Code