instar 1.3.730 → 1.3.732

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +161 -1
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  5. package/dist/config/ConfigDefaults.js +26 -0
  6. package/dist/config/ConfigDefaults.js.map +1 -1
  7. package/dist/core/CoherenceGate.d.ts +119 -0
  8. package/dist/core/CoherenceGate.d.ts.map +1 -1
  9. package/dist/core/CoherenceGate.js +342 -19
  10. package/dist/core/CoherenceGate.js.map +1 -1
  11. package/dist/core/CoherenceReviewer.d.ts +30 -0
  12. package/dist/core/CoherenceReviewer.d.ts.map +1 -1
  13. package/dist/core/CoherenceReviewer.js +14 -0
  14. package/dist/core/CoherenceReviewer.js.map +1 -1
  15. package/dist/core/ConversationRegistry.d.ts +260 -0
  16. package/dist/core/ConversationRegistry.d.ts.map +1 -0
  17. package/dist/core/ConversationRegistry.js +1151 -0
  18. package/dist/core/ConversationRegistry.js.map +1 -0
  19. package/dist/core/FileClassifier.d.ts.map +1 -1
  20. package/dist/core/FileClassifier.js +7 -0
  21. package/dist/core/FileClassifier.js.map +1 -1
  22. package/dist/core/PostUpdateMigrator.d.ts +13 -0
  23. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  24. package/dist/core/PostUpdateMigrator.js +105 -0
  25. package/dist/core/PostUpdateMigrator.js.map +1 -1
  26. package/dist/core/ResponseReviewDecisionLog.d.ts +45 -0
  27. package/dist/core/ResponseReviewDecisionLog.d.ts.map +1 -0
  28. package/dist/core/ResponseReviewDecisionLog.js +85 -0
  29. package/dist/core/ResponseReviewDecisionLog.js.map +1 -0
  30. package/dist/core/WriteDomainRegistry.d.ts.map +1 -1
  31. package/dist/core/WriteDomainRegistry.js +21 -0
  32. package/dist/core/WriteDomainRegistry.js.map +1 -1
  33. package/dist/core/conversationContextWiring.d.ts +51 -0
  34. package/dist/core/conversationContextWiring.d.ts.map +1 -0
  35. package/dist/core/conversationContextWiring.js +78 -0
  36. package/dist/core/conversationContextWiring.js.map +1 -0
  37. package/dist/core/conversationIdentity.d.ts +112 -0
  38. package/dist/core/conversationIdentity.d.ts.map +1 -0
  39. package/dist/core/conversationIdentity.js +141 -0
  40. package/dist/core/conversationIdentity.js.map +1 -0
  41. package/dist/core/deliverToConversation.d.ts +62 -0
  42. package/dist/core/deliverToConversation.d.ts.map +1 -0
  43. package/dist/core/deliverToConversation.js +85 -0
  44. package/dist/core/deliverToConversation.js.map +1 -0
  45. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  46. package/dist/core/devGatedFeatures.js +6 -0
  47. package/dist/core/devGatedFeatures.js.map +1 -1
  48. package/dist/core/reviewers/conversational-tone.d.ts.map +1 -1
  49. package/dist/core/reviewers/conversational-tone.js +15 -1
  50. package/dist/core/reviewers/conversational-tone.js.map +1 -1
  51. package/dist/core/types.d.ts +76 -0
  52. package/dist/core/types.d.ts.map +1 -1
  53. package/dist/core/types.js.map +1 -1
  54. package/dist/core/untrustedConversationContext.d.ts +77 -0
  55. package/dist/core/untrustedConversationContext.d.ts.map +1 -0
  56. package/dist/core/untrustedConversationContext.js +134 -0
  57. package/dist/core/untrustedConversationContext.js.map +1 -0
  58. package/dist/monitoring/ReviewCanaryBattery.d.ts +160 -0
  59. package/dist/monitoring/ReviewCanaryBattery.d.ts.map +1 -0
  60. package/dist/monitoring/ReviewCanaryBattery.js +416 -0
  61. package/dist/monitoring/ReviewCanaryBattery.js.map +1 -0
  62. package/dist/scaffold/templates.d.ts.map +1 -1
  63. package/dist/scaffold/templates.js +15 -0
  64. package/dist/scaffold/templates.js.map +1 -1
  65. package/dist/server/AgentServer.d.ts +7 -0
  66. package/dist/server/AgentServer.d.ts.map +1 -1
  67. package/dist/server/AgentServer.js +13 -0
  68. package/dist/server/AgentServer.js.map +1 -1
  69. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  70. package/dist/server/CapabilityIndex.js +15 -0
  71. package/dist/server/CapabilityIndex.js.map +1 -1
  72. package/dist/server/routes.d.ts +10 -0
  73. package/dist/server/routes.d.ts.map +1 -1
  74. package/dist/server/routes.js +114 -1
  75. package/dist/server/routes.js.map +1 -1
  76. package/package.json +1 -1
  77. package/src/data/builtin-manifest.json +64 -64
  78. package/src/scaffold/templates/jobs/instar/review-canary-battery.md +31 -0
  79. package/src/scaffold/templates.ts +15 -0
  80. package/upgrades/1.3.731.md +97 -0
  81. package/upgrades/1.3.732.md +107 -0
  82. package/upgrades/side-effects/context-aware-outbound-review.md +62 -0
  83. package/upgrades/side-effects/durable-conversation-identity-increment1.md +89 -0
@@ -0,0 +1,97 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: minor -->
5
+
6
+ ## What Changed
7
+
8
+ The response-review pipeline's watch-mode data vetoed the planned enforcement
9
+ flip: of 12 reviewed real messages, 2 would have been WRONGLY blocked — both
10
+ technical content the operator had EXPLICITLY asked for (one was the worktree
11
+ keep/delete list the user requested). The reviewer judges each message in
12
+ isolation by construction: its own prompt allows "code the user explicitly
13
+ asked to see," but `ReviewContext` carries NO conversation — the carve-out was
14
+ prose with no input.
15
+
16
+ Per `docs/specs/context-aware-outbound-review.md` (review-converged round 4,
17
+ approved), the opted-in reviewer now receives a bounded, untrusted-data-
18
+ enveloped slice of recent conversation, and the flip evidence becomes durable
19
+ and two-sided-measured:
20
+
21
+ - **Context channel (§D1/§D2):** an injected synchronous provider
22
+ (TopicMemory rows + the topic's verified-operator binding →
23
+ `buildConversationContext`), rendered by ONE shared envelope
24
+ (`untrustedConversationContext.ts` — per-call random boundary, JSON-encoded
25
+ scrubbed bodies, the structural `ask-license mode` line, the four-clause
26
+ carve-out contract as ONE ATOMIC block). Budget-clamped (6 msgs / 500 chars /
27
+ 4000 total).
28
+ - **Principal binding (§D4, Know Your Principal):** role + `verifiedOperator`
29
+ tags + the window's ask-license mode are computed at the WIRING layer from
30
+ AUTHENTICATED uids only; any uid-less user row in an unbound window degrades
31
+ AWAY from licensing (fail-closed).
32
+ - **Structural scoping (§D3):** only `conversational-tone` (v1), only for
33
+ primary-user recipients, receives an augmented ctx copy; every other
34
+ reviewer structurally cannot render conversation. The carve-out is ONE-WAY
35
+ (can only move a would-block toward PASS), content-class-bounded (never
36
+ credentials/PII), and never touches the deterministic PEL layer.
37
+ - **Total containment (§D5):** the HTTP seam above the pipeline fails OPEN on
38
+ a crash, so every new context path is individually contained — any failure
39
+ degrades to today's exact (stricter) behavior.
40
+ - **Durable flip evidence (§D8):** `logs/response-review-decisions.jsonl` —
41
+ one size-rotated row per evaluated turn (outcome, llmVerdict, violations,
42
+ contextMeta, 200 scrubbed chars; never conversation bodies), surviving the
43
+ restarts that erased the in-memory history.
44
+ - **The one-way property is MEASURED (§D9.4/4b):** soak-only counterfactual
45
+ re-reviews of qualifying would-blocks (shared `pairId`), plus a daily
46
+ adversarial `ReviewCanaryBattery` (PEL-missable secret/PII fixtures with
47
+ covering asks, seeded into reserved NEGATIVE topic ids, baseline +
48
+ with-context arms, reviewer-level assertions, finally-cleanup) behind the
49
+ Bearer-gated `POST /review/canary-battery/run` + the off-by-default
50
+ `review-canary-battery` job. Every run outcome — including refusals —
51
+ writes a `batterySummary` row; a silent skip is impossible.
52
+ - Ships dev-gated dark (`responseReview.conversationalContext` — `enabled`
53
+ OMITTED → resolveDevAgentGate; LIVE on dev agents, DARK on the fleet).
54
+ Kill-switch `enabled: false` applies at the NEXT evaluate via the new
55
+ liveConfig wiring — no restart. The enforcement flip
56
+ (`observeOnly: false`) remains the operator's manual action, gated on the
57
+ §D9 clean-day criteria.
58
+
59
+ ## What to Tell Your User
60
+
61
+ <!-- audience: agent-only, maturity: experimental -->
62
+ - **My message reviewer can now see what you asked for (experimental, dark):
63
+ ** the safety reviewer that checks my replies used to judge each message in
64
+ isolation — so when you explicitly asked me for a technical list, it wanted
65
+ to block my answer. On development agents it now reads the last few
66
+ messages of our conversation (wrapped as quoted data, never instructions),
67
+ so "you asked for this" finally counts. It can only ever RESCUE a message
68
+ from a wrong block — it can never become a new reason to block, and it never
69
+ licenses secrets. Blocking stays off until the operator flips it after a
70
+ measured clean day.
71
+
72
+ ## Summary of New Capabilities
73
+
74
+ | Capability | How to Use |
75
+ |-----------|-----------|
76
+ | Context-aware reviewer carve-out (dev-gated dark) | `responseReview.conversationalContext` in `.instar/config.json` (enabled omitted → live on dev agents, dark on fleet; `enabled:false` = live kill-switch, no restart) |
77
+ | Durable would-block decision log | `logs/response-review-decisions.jsonl` (one row per reviewed turn; `contextMeta` on `GET /review/history` entries too) |
78
+ | Daily anti-laundering canary battery | `POST /review/canary-battery/run` (Bearer; 503 while dark) + the off-by-default `review-canary-battery` job |
79
+ | Canary tag plumbing (test route only) | `POST /review/test` accepts `canary` + `fixtureId`; `/review/evaluate` ignores them (a real turn cannot self-tag) |
80
+
81
+ ## Evidence
82
+
83
+ - Veto data grounded live (2026-07-02 watch mode): 2/12 real messages would
84
+ have been wrongly blocked — both operator-requested technical content; both
85
+ are now permanent §D9.2 regression fixtures AND the battery's pass-side
86
+ controls.
87
+ - Spec converged round 4 (0 CRITICAL / 0 MAJOR; internal panel + both external
88
+ cross-model doors verified the fold table 5/5), approved under standing
89
+ Session-A preapproval (topic 29836), tag commit dfdb7b21a.
90
+ - All three test tiers shipped and green: unit (57 new tests across 5 files —
91
+ every §5 boundary both sides, incl. the throw fixtures at fetch/tag/render
92
+ time, the uid-less fail-closed rule, the counterfactual + canary-tag
93
+ exclusion, the battery outcome table), integration (8 — real auth
94
+ middleware + real TopicMemory SQLite through the full HTTP pipeline), e2e
95
+ lifecycle (6 — production init path with the REAL LiveConfig + dev-gate
96
+ funnel + TopicOperatorStore: dev boot alive, on-disk kill-switch applies
97
+ without restart, fleet boot byte-identical + battery 503).
@@ -0,0 +1,107 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: minor -->
5
+
6
+ ## What Changed
7
+
8
+ Per `docs/specs/durable-conversation-identity.md` (review-converged round 11 —
9
+ 0 CRITICAL / 0 MAJOR — approved under the standing Session-A preapproval,
10
+ topic 29836), the Phase-1 structural refactor's first increment lands: a
11
+ durable, channel-agnostic identity for conversations.
12
+
13
+ - **`ConversationRegistry` (§3):** the sparse join table between canonical key
14
+ (`slack:<team>:<channel>[:<thread>]`), structured tuple, and a stable minted
15
+ NEGATIVE id. Telegram positive ids pass through unregistered forever
16
+ (back-compat by construction). The mint candidate is the legacy hash value
17
+ (zero-loss adoption + mixed-fleet skew convergence); the registry is the
18
+ collision authority — probe DOWN through ONE shared displacement
19
+ implementation, bounded at `MAX_PROBE_DISTANCE = 64`.
20
+ - **The WAL journal (§3.4):** `conversation-registry.jsonl` at the stateDir
21
+ ROOT (the one glob shape the deployed backup resolver expands — R3-C4).
22
+ Probed/durable-binding mints fsync ONE line BEFORE the id returns; the O(N)
23
+ snapshot is batched with a size-adaptive interval (the CommitmentTracker
24
+ freeze shape is structurally avoided). Torn-tail discard; non-tail
25
+ corruption fails CLOSED (quarantine-aside + attention + a durability
26
+ incident); unknown ops (a rollback across a newer version) are
27
+ skipped-and-preserved with snapshot-flush SUSPENSION until re-upgrade;
28
+ single global monotonic `seq` across rotations and restarts.
29
+ - **Eager mint at Slack inbound (§6.3):** the dispatch mints (get-or-create)
30
+ the resolved routing key's conversation id on every inbound and carries it
31
+ as `conversationId` in message metadata + the session bootstrap context —
32
+ the identity durable state can finally bind to. Fail-toward-delivery: a
33
+ mint degradation never blocks a message.
34
+ - **Safety rails:** per-channel mint-rate breaker (speculative drops re-mint
35
+ free; the durable-binding carve-out has its OWN cap + typed
36
+ `conversation-registration-capacity` refusal); the D1 runtime kill-switch
37
+ (`conversationIdentity.recording.enabled: false` → legacy-identical
38
+ in-memory hashing, no redeploy; minted-id durable binds refused typed);
39
+ §3.1 workspace pinning with per-machine `multi-workspace-unsupported`
40
+ refusal and `_`→teamId in-place upgrade (local authenticated source only).
41
+ - **The `deliverToConversation` funnel SKELETON (§5):** typed, non-exceptional
42
+ §5.1 results (dark/dryRun return the SAME `not-delivered` shape as
43
+ unresolvable + a would-deliver audit line — NEVER success-shaped); local-
44
+ origin-only resolution (KYP); system-channel suppression inside the funnel.
45
+ ZERO consumers migrate in this increment; the E1 idempotency guard, P17
46
+ budgets, and the deterministic gate-exempt arm land with their §6.1
47
+ increments before any consumer rides it.
48
+ - **Read-only observability (§8):** `GET /conversations`,
49
+ `/conversations/:id`, `/conversations/resolve`, `/conversations/health`
50
+ (labels escaped on render; no write routes exist by design).
51
+ - **Migration parity (§6.2/§9):** backup manifest gains BOTH
52
+ `state/conversation-registry.json` AND the `conversation-registry.jsonl*`
53
+ glob; config defaults arrive via the add-missing merge (`recording.enabled:
54
+ true`; `followThrough.dryRun: true` with `enabled` OMITTED — the
55
+ developmentAgent gate resolves it); a dev-gate strip migration removes a
56
+ default-shaped `followThrough.enabled: false`; the CLAUDE.md capability
57
+ section reaches new agents (template) AND existing agents (migrator +
58
+ Codex/Gemini shadow markers).
59
+
60
+ Behavior-identical for existing flows: recording is additive, delivery is
61
+ dark, the three legacy hash copies keep computing the same values the registry
62
+ records (the §4 callsite consolidation is the next increment; a grep ratchet
63
+ pins the copy set so a fourth is a CI failure).
64
+
65
+ ## What to Tell Your User
66
+
67
+ <!-- audience: agent-only, maturity: experimental -->
68
+ - **Slack conversations now have a durable identity (foundation only, no
69
+ visible change yet):** every Slack channel or thread I talk in is written
70
+ into a permanent address book with a stable number the moment a message
71
+ arrives, so upcoming increments can make promises/reminders/notices in
72
+ Slack survive restarts the way they do on Telegram. A negative topic id in
73
+ my state is one of these minted conversation ids — I can look it up in the
74
+ address book — not an error. Delivery to Slack through this identity stays
75
+ switched off (dry-run first on development agents).
76
+
77
+ ## Summary of New Capabilities
78
+
79
+ | Capability | How to Use |
80
+ |-----------|-----------|
81
+ | Conversation inventory | `GET /conversations` (Bearer; `?platform=slack`, `?limit=N`) |
82
+ | Resolve an id | `GET /conversations/:id` (positive → Telegram pass-through; unknown negative → honest 404) |
83
+ | Forward lookup (mints nothing) | `GET /conversations/resolve?key=…` or `?sessionKey=…` |
84
+ | Health + growth/suspension tripwires | `GET /conversations/health` (the Tier-3 alive target) |
85
+ | Recording kill-switch (D1) | `conversationIdentity.recording.enabled: false` (live-read, no restart) |
86
+ | Delivery gate (dark) | `conversationIdentity.followThrough` (dev-gated; `dryRun: true` first) |
87
+
88
+ ## Evidence
89
+
90
+ - Spec converged round 11 (0 CRITICAL / 0 MAJOR; internal multi-lens panel +
91
+ two external cross-model doors per round), approved under the standing
92
+ Session-A operator preapproval (topic 29836); tag commit aa5086eb8.
93
+ - All three test tiers shipped and green: unit (79 tests across 5 files —
94
+ golden parity with the legacy hash, frozen schema-v1 constants, the crafted
95
+ in-charset collision pair probing to distinct ids in either order, WAL
96
+ crash-durability without a snapshot flush, torn-tail/corruption/unknown-op
97
+ replay rules incl. snapshot suspension, rotation-spanning replay with one
98
+ global seq, breaker both budgets, recording kill-switch both arms,
99
+ workspace pinning, adoption gating, funnel §5.1 typed contract, the
100
+ mint-idiom grep ratchet), integration (7 — real authMiddleware + real
101
+ registry through the full HTTP pipeline incl. the label-escape pin and the
102
+ read-only resolve), migration-parity unit (7 — defaults never materialize
103
+ `followThrough.enabled`, the #1001 strip, the backup glob expanding through
104
+ the REAL BackupManager with every expanded file in the snapshot), e2e
105
+ lifecycle (4 — production init path answers `/conversations/health` 200,
106
+ the mint→restart→same-id cycle via journal replay, Telegram pass-through
107
+ sparseness).
@@ -0,0 +1,62 @@
1
+ # Side-Effects Review — Context-Aware Outbound Review (S2 context-aware reviewers)
2
+
3
+ **Spec:** docs/specs/context-aware-outbound-review.md (converged r4, approved under standing Session-A preapproval, topic 29836; tag commit dfdb7b21a). **Parent:** S2 enforcement-readiness track — the enforcement flip of `responseReview.observeOnly` is gated on this spec's §D9 criteria and stays a manual operator action.
4
+ **Ships DARK on the fleet, LIVE on development agents** via `responseReview.conversationalContext` (`enabled` OMITTED from defaults → `resolveDevAgentGate` at the WIRING layer; explicit false force-darks, explicit true is the fleet flip).
5
+ **Files:** src/core/untrustedConversationContext.ts (new), src/core/conversationContextWiring.ts (new), src/core/ResponseReviewDecisionLog.ts (new), src/monitoring/ReviewCanaryBattery.ts (new), src/core/CoherenceGate.ts, src/core/CoherenceReviewer.ts, src/core/reviewers/conversational-tone.ts, src/core/types.ts, src/server/routes.ts, src/server/AgentServer.ts, src/server/CapabilityIndex.ts, src/commands/server.ts, src/scaffold/templates.ts, src/core/PostUpdateMigrator.ts, src/scaffold/templates/jobs/instar/review-canary-battery.md (new, ships OFF)
6
+
7
+ ## What changed
8
+
9
+ 1. **untrustedConversationContext.ts (new):** the ONE shared untrusted-data envelope (§D2) — `clampConversation` (§D6 budget clamps: 6 msgs / 500 chars / 4000 total, oldest dropped first) + `renderUntrustedConversation` (per-call random `CTX_BOUNDARY`, JSON-encoded bodies, credential scrub per body, role labels + `USER(verified-operator)` principal tag, the structural `ask-license mode` line, and the §D3 four-clause prompt contract rendered as ONE ATOMIC block). Never throws; per-message scrub failure drops that message; any render failure drops the whole section.
10
+ 2. **conversationContextWiring.ts (new):** `buildConversationContext(rows, operator)` — the wiring-layer principal computation (§D4): role from `fromUser`, `verifiedOperator` tags from authenticated-uid-vs-binding match, and the window `askLicenseMode` (bound → `verified-operator`; unbound → `single-sender` ONLY when every user row carries the same single authenticated uid; ANY uid-less user row or 2+ uids → `weak-corroboration-only`, fail-closed per R3-M2/R4-L1).
11
+ 3. **ResponseReviewDecisionLog.ts (new):** the durable §D8 flip-evidence JSONL (`logs/response-review-decisions.jsonl`) — append-only, size-rotated (10MB → single `.1` archive via SafeFsExecutor), every write failure swallowed (telemetry never gates delivery).
12
+ 4. **CoherenceGate.ts:** optional `conversationContextProvider` + widened `liveConfig` getter (now carries the wiring-RESOLVED `conversationalContext` block; ABSENT getter ⇒ DARK even against an enabled snapshot — round-2 L4 precedence); acquisition once per `_evaluate` inside its own try/catch, only for primary-user recipients with a numeric topicId; augmented SHALLOW-COPY fan-out (base ctx never carries conversation — §D3 structural availability); `EvaluateRequest.telemetry` (test-route-only canary tags) + `EvaluateResponse._contextMeta`; §D8 rows written at the `logAudit` seam for EVERY outcome (textHead = 200 scrubbed chars; canary/fixtureId stamped by the writer); §D9.4 counterfactual re-review (observeOnly-only, fire-and-forget, one context-stripped re-review per opted-in violating reviewer, `counterfactual:true` + shared `pairId`); `appendDecisionRow` exposes the single writer to the battery; in-memory audit entries gain `contextMeta` (§6). `DynamicReviewer` honors the `'recent-conversation'` contextRequirements opt-in key.
13
+ 5. **conversational-tone.ts:** renders the atomic block ONLY when the gate handed it the augmented ctx; absent fields ⇒ prompt BYTE-IDENTICAL to feature-dark (the pre-existing static exception stands).
14
+ 6. **routes.ts:** `/review/test` accepts `canary` + `fixtureId` (forwarded as `telemetry`; response gains `contextMeta`); `POST /review/evaluate` does NOT read them (a real turn cannot self-tag — boundary 13B, pinned by test); new Bearer-gated `POST /review/canary-battery/run` (503 while dark).
15
+ 7. **ReviewCanaryBattery.ts (new):** the §D9.4b(a) daily battery driver — refuse-or-(pre-clean → seed reserved NEGATIVE topic ids with per-run-unique messageIds + uid-carrying user rows → replay both arms via the Bearer `/review/test` route → reviewer-level PEL-unmaskable assertions → finally-cleanup) → a `batterySummary` row on EVERY outcome including refusals.
16
+ 8. **server.ts wiring:** the `resolveConversationalContext` LIVE closure (LiveConfig + `resolveDevAgentGate` — the kill-switch applies at the NEXT evaluate, no restart), the provider over real TopicMemory + `_agentServerRef` TopicOperatorStore, and the battery construction (localhost Bearer self-call to `/review/test`).
17
+ 9. **Job template `review-canary-battery.md` (new):** ships `enabled: false` everywhere; operator enables it only on the soaking dev agent (spec §4.5 — no fleet migration carries it on).
18
+ 10. **templates.ts + PostUpdateMigrator.ts:** `CONTEXT_AWARE_REVIEW_CLAUDEMD_SECTION` in `generateClaudeMd` (new agents) + an idempotent content-sniffed `migrateClaudeMd` append (existing agents), carrying the house dark-feature honesty phrasing (round-1 m5: `/review/history` 501s on most installs).
19
+
20
+ ## Blast radius
21
+
22
+ - **Fleet default: byte-identical.** `enabled` omitted + not a dev agent ⇒ the resolver returns false ⇒ no acquisition, no section, prompts byte-identical (pinned by unit boundary 3/4 and the e2e fleet boot). The battery route 503s; the job ships OFF.
23
+ - **Total containment (round-1 M6, load-bearing):** the HTTP seam above the pipeline fails OPEN on a crash, so EVERY new context path — fetch, tagging, clamp, render, meta, D8 write, counterfactual — is individually contained; any failure degrades to "no section" (the STRICTER current posture), never to a throw escaping `_evaluate`. Pinned by throw fixtures at fetch/tag/render time.
24
+ - **Structural exposure bound:** conversation fields ride ONLY the augmented copies handed to the resolved opt-in set (`conversational-tone` alone in v1) for primary-user recipients; `information-leakage` and every other reviewer receive a base ctx that cannot render them (round-1 M1 + round-2 m2). Widening requires a spec revision, never config alone.
25
+ - **One-way risk direction:** context can only move a would-block toward PASS (measured by the counterfactual pairs + the canary battery, both soak-only); PEL is untouched and runs before all reviewers.
26
+
27
+ ## Risk + mitigation
28
+
29
+ - **Risk:** the context channel launders a credential/PII paste past the reviewer. **Mitigation:** the §D3.3 bounded-scope clause + the daily adversarial canary battery (PEL-missable fixtures, baseline + with-context arms, reviewer-level assertions) — a laundering event FAILS the soak and resets the §D9 clock.
30
+ - **Risk:** an attacker-influenceable context body carries instructions. **Mitigation:** the proven §Design-4 envelope (random boundary + JSON-encoded bodies + corroborating-only preamble), boundary-7 injection tests.
31
+ - **Risk:** uid-less legacy rows fake a single-sender license. **Mitigation:** R3-M2 fail-closed rule (any uid-less user row in an unbound window ⇒ weak-corroboration-only), pinned both sides in boundary 6.
32
+ - **Risk:** the D8 log grows unbounded or leaks conversation. **Mitigation:** size rotation; only `contextMeta` + 200 scrubbed chars persist — bodies never (at-rest honesty note carried in the spec + docs).
33
+ - **Risk:** the battery strands fixture rows in the production TopicMemory. **Mitigation:** reserved NEGATIVE topic ids (collision structurally impossible), pre-clean + finally-cleanup + per-run-unique messageIds + seed-count assertion (R4-m4), all pinned by unit tests.
34
+
35
+ ## Migration parity
36
+
37
+ - **Config:** NO `migrateConfig` entry on purpose — every key is optional with in-code defaults and the dev-gate convention REQUIRES `enabled` absent (a migration writing `enabled:false` would recreate the PR #1001 bug). Documented on the `ConversationalContextConfig` type.
38
+ - **CLAUDE.md:** section ships in `generateClaudeMd` + content-sniffed idempotent `migrateClaudeMd` append.
39
+ - **Hook scripts:** none changed — `response-review.js` already sends topicId + transcriptPath and is always-overwritten anyway.
40
+ - **Job:** new template installs through the normal built-in job path (`installBuiltinJobs` refresh), `enabled:false` everywhere.
41
+
42
+ ## Rollback
43
+
44
+ - `responseReview.conversationalContext.enabled: false` — context off, reviewers byte-identical to pre-spec behavior, applied at the NEXT evaluate through the liveConfig wiring (no restart; pinned by boundary 12 + the e2e on-disk flip test).
45
+ - The D8 JSONL is additive telemetry; deleting it loses history, breaks nothing. `observeOnly` remains the orthogonal enforcement lever.
46
+
47
+ ## Tests
48
+
49
+ - **Tier 1 (unit, 71 tests):** `untrusted-conversation-context.test.ts` (12 — envelope/clamps/scrub/atomicity, boundaries 7-9), `conversation-context-wiring.test.ts` (8 — boundary 6 both sides incl. the uid-less fail-closed rule), `coherence-gate-conversation-context.test.ts` (12 — boundaries 1-5, 12, 14 + the §D9.2 veto-day regression fixtures pinned both directions), `response-review-decision-log.test.ts` (9 — boundary 10 + boundary 11 counterfactual both sides, incl. canary-tag exclusion), `review-canary-battery.test.ts` (16 — boundary 13: outcome table, refuse conditions, seed-then-cleanup contract, failed-outranks-inconclusive).
50
+ - **Tier 2 (integration, 8 tests):** full HTTP pipeline over the real AgentServer auth middleware + real TopicMemory SQLite — enveloped ask in the captured prompt, byte-identical no-context path, provider-throw → 200 never fail-open, `/review/history` carries contextMeta and never bodies, canary tag plumbing test-route-only, battery route Bearer-gated + live/dark.
51
+ - **Tier 3 (e2e, 6 tests):** production init path with the REAL LiveConfig + resolveDevAgentGate + TopicMemory + TopicOperatorStore — dev boot ALIVE (context section + verified-operator principal tag from a real binding + battery 200), on-disk kill-switch flip applies with no restart, fleet boot byte-identical + 503 (Maturation Path dark side), Bearer gating.
52
+ - Suite health: tsc clean; `npm run lint` clean; `check-repo-invariants` green; `no-silent-fallbacks` green (every new catch tagged with a real justification); docs-coverage class ratchet back at floor with the new architecture page.
53
+
54
+ ## Post-rebase conformance addendum (2026-07-03, PR #1343)
55
+
56
+ Rebasing onto post-#1341/#1342 main put this build under two guards that did not exist at its commit ceremony; conforming produced two additional side effects:
57
+
58
+ 1. **Write-domain classification (src/core/WriteDomainRegistry.ts):** `POST /review/canary-battery/run` is now classified `machine-local` with an `ephemeral-rebuildable` story (standby-write-reconciliation §3.5 ratchet). Runtime delta: under the (dev-gated, dry-run) WriteAdmission layer the route resolves to a domain instead of null — machine-local ⇒ admit everywhere, which matches reality (the battery exercises THIS machine's review pipeline; fixtures are finally-cleaned; the D8 log is per-machine soak evidence). NOT added to the TODO-classify baseline (the ratchet forbids growth).
59
+ 2. **FileClassifier sync exclusions (src/core/FileClassifier.ts):** `.instar/topic-memory.db` and `logs/response-review-decisions.jsonl` are now `git-sync-excluded` — the I9 second-axis file-level arm, same pattern as the wave-1 evolution/attention entries. Behavior delta: if any writer ever queued these paths for git-sync auto-commit, they are now skipped with a DegradationReporter breadcrumb instead of swept into a commit. A live SQLite binary and a per-machine JSONL must never ride git-sync (the round-2 S1 lesson); no current code path queues either, so the practical fleet delta is zero.
60
+ 3. **Job template header (src/scaffold/templates/jobs/instar/review-canary-battery.md):** the trigger curl now resolves `AGENT_ID` and sends `X-Instar-AgentId`, conforming to the template-agent-id-header standard (bearer-only localhost calls are deprecated). No behavior change beyond the header; the job still ships `enabled: false`.
61
+
62
+ Rollback for this addendum rides the parent levers: the registry entry and exclusions are inert data rows (removing them restores the prior null-classification / sync-eligible state); the template header is cosmetic to auth (the server accepts bearer-only with a deprecation log).
@@ -0,0 +1,89 @@
1
+ # Side-Effects Review — Durable Conversation Identity, Increment 1 (registry + crash-proof journal + eager mint)
2
+
3
+ **Spec:** docs/specs/durable-conversation-identity.md (CONVERGED round 11 — 0 CRITICAL / 0 MAJOR; approved under the standing Session-A operator preapproval, topic 29836). **Parent:** Structure beats Willpower — durable identity must be a registry, not a convention three copies of a hash function remember.
4
+ **Behavior-identical for existing flows.** The foundation RECORDS identity (always-on, kill-switchable); DELIVERY stays dark-gated: `conversationIdentity.followThrough` omits `enabled` (the developmentAgent gate resolves it — live-on-dev, dark-fleet) with `dryRun: true` FIRST, exactly as spec §9 prescribes.
5
+ **Files:** src/core/conversationIdentity.ts (new), src/core/ConversationRegistry.ts (new), src/core/deliverToConversation.ts (new), src/server/routes.ts (read-only `GET /conversations*`), src/commands/server.ts (registry construction + adoption pass + §6.3 eager mint), src/core/types.ts + src/config/ConfigDefaults.ts (`conversationIdentity` block), src/core/devGatedFeatures.ts (followThrough registration), src/core/PostUpdateMigrator.ts (backup manifest + config defaults + CLAUDE.md), src/scaffold/templates.ts (Agent Awareness).
6
+
7
+ ## What changed
8
+
9
+ 1. **conversationIdentity.ts (new):** the SINGLE hash + identity surface (§4). The frozen 32-bit sum-shift hash, the mint candidate `-(abs(hash)+1)` (golden-parity with `slackRoutingKeySyntheticId`), the v1 structured tuple `(platform, channelId, threadTs?)`, canonical-key/tupleKey forms, the frozen schema-v1 constants (`MAX_PROBE_DISTANCE=64`, probe DOWN, `HLC_ABS_MIN/MAX`), shape clamps, and `walkDisplacement` — the ONE shared displacement implementation §3.3 and the future §3.5.1 merge both call.
10
+ 2. **ConversationRegistry.ts (new):** the sparse join table (canonical key ⇄ tuple ⇄ minted negative id). Synchronous in-memory assignment (probe included; returned == will-persist), the §3.4 WAL journal at the stateDir ROOT (`conversation-registry.jsonl` — the one glob shape the deployed `BackupManager.expandGlob` expands, R3-C4): probed/durable-binding mints append+fsync ONE line BEFORE the id returns; speculative non-probed mints ride the batched snapshot only. Torn-tail truncate-discard; NON-tail corruption fails CLOSED (quarantine-aside + attention + durability incident — R7-minor-3); UNKNOWN-op skip-and-preserve with snapshot-flush SUSPENSION (R8-minor-2/R9-M1/R10-M1) surfaced on health (R11-low-1); single global monotonic `seq` across rotations + restarts (R3-M14); rotation 8 MB/50k lines with fully-superseded-only pruning behind a 7-day retention floor; size-adaptive batched snapshot (2 s base → 60 s max; off-loop write past 20k entries/2 MB); snapshot completeness for bind-pins / ambiguous-sends / send-intents (R4-M2/R6-M1 — the op enum + replay ship NOW so later increments' records and rollbacks across them replay correctly; their WRITERS land with §6.1 steps 2+). Mint-rate breaker (per-channel windows, speculative drop-to-nowhere + durable-binding carve-out with its OWN cap → typed `conversation-registration-capacity`, adversarial-B), the D1 `recording.enabled` kill-switch (live-read; degraded = in-memory candidate + collision-checked read B6; minted-id durable binds refused typed), §3.1 workspace pinning (config pin authoritative; local-observed self-corroborating; per-machine `multi-workspace-unsupported` refusal; `_`→teamId in-place upgrade by the LOCAL authenticated adapter only), §6.2 authorized-traffic-gated adoption pass, and the §8 health surface.
11
+ 3. **deliverToConversation.ts (new):** the §5 funnel SKELETON — typed, non-exceptional §5.1 results (dark/dryRun return the SAME `not-delivered` shape as unresolvable, plus a would-deliver audit line; NEVER success-shaped), `id>0` → today's Telegram path, `id<0` → local-origin-only resolution (KYP), system-channel suppression inside the funnel (§4), in-thread Slack delivery when live. ZERO consumers migrate in this increment; the E1 guard, P17 budgets, `deterministicKind` arm, and permanent-error classification land WITH their §6.1 increments (2/3/5) before any consumer rides the funnel.
12
+ 4. **Read-only routes (§8):** `GET /conversations`, `/conversations/health` (the e2e alive target), `/conversations/resolve`, `/conversations/:id` — Bearer-gated, labels HTML-escaped on render (B3), no write routes (mint happens only at internal chokepoints — §7).
13
+ 5. **§6.3 eager mint at Slack inbound:** the dispatch mints (get-or-create) the resolved routing key's conversation id on every inbound and carries it as the pinned `conversationId` field in message metadata + the session bootstrap context. Wrapped fail-toward-delivery: a mint degradation never blocks a message.
14
+ 6. **Config/rollout (§9):** `conversationIdentity.recording.enabled: true` default (existence-checked in migrateConfig, NEVER materialized false — the #1001 mechanism); `followThrough.enabled` OMITTED + registered in DEV_GATED_FEATURES; `followThrough.dryRun: true`. PostUpdateMigrator adds the backup-manifest entries (`state/conversation-registry.json` + the top-level glob `conversation-registry.jsonl*`) and the CLAUDE.md Capabilities entry; `generateClaudeMd` carries it for new agents.
15
+
16
+ ## Blast radius
17
+
18
+ - **Recording is additive observability.** No existing store changes shape; no store version bump; the registry file is inert data to old code (verified: zero old-code reads of `state/conversation-registry.json`). Rollback-by-revert for the code; `recording.enabled:false` is the runtime kill-switch that restores byte-identical legacy hashing without a redeploy.
19
+ - **Delivery cannot fire on the fleet.** The funnel has zero callers in this increment AND the `id<0` arm is dev-gated + dryRun-first; on a dev agent it returns typed non-deliveries with audit lines until a deliberate `dryRun:false` flip.
20
+ - **The eager mint adds one in-memory map hit per Slack inbound** (post-first-mint) plus, for a NEW conversation, one O(1) journal append (fsync only when probed). The O(N) snapshot is batched off the hot path — the CommitmentTracker freeze shape is structurally avoided (§3.4).
21
+ - **Hash copies are NOT rewired in this increment.** The three legacy copies keep computing the same values the registry records (value-identical by golden parity); the §4 callsite consolidation is the next increment. The mint-idiom grep ratchet pins the current copy set so a FOURTH copy is a CI failure.
22
+
23
+ ## Risk + mitigation
24
+
25
+ - **Risk:** registry failure costs a message. **Mitigation:** every mint path is typed-degrading (unparseable key / breaker drop / recording-off / probe overflow → "no durable id" or a collision-checked read); the inbound dispatch wraps the mint in try/catch. Identity never costs a message (§3.6) — pinned by unit tests.
26
+ - **Risk:** a crash loses a probed/thread-level id a durable consumer bound to. **Mitigation:** the WAL rule — fsynced journal line BEFORE the id returns; replay restores it (pinned by the crash tests, no snapshot flush needed).
27
+ - **Risk:** journal corruption silently loses committed records. **Mitigation:** non-tail corruption HALTS replay into quarantine-aside + ONE attention item + a durability-incident record (the §3.7 SQLite-migration tripwire input) — never skip-and-continue.
28
+ - **Risk:** a rollback across a future op-enum extension trips the corruption machinery or composes wrongly. **Mitigation:** unknown-op skip-and-preserve + snapshot-flush suspension (the pre-skew snapshot stays put; prune keys on the static pre-skew watermark) — pinned by the suspension unit test.
29
+ - **Risk:** a mint flood grows the store unboundedly (auto-join, thread storms). **Mitigation:** the per-channel mint-rate breaker (speculative drops re-mint for free; durable carve-out has its own cap + typed refusal), the adoption pass's authorized-traffic gate (security-B8), and the 80%-of-ceiling health tripwire.
30
+
31
+ ## Migration parity
32
+
33
+ - migrateConfig: adds `conversationIdentity.recording.enabled: true` ONLY when absent; NEVER writes `followThrough.enabled` or a literal `false` anywhere in the block (pinned by a unit test — the #1001 mechanism).
34
+ - Backup manifest: `state/conversation-registry.json` + top-level glob `conversation-registry.jsonl*` join `config.backup.includeFiles` via the existing idempotent set-union migrator (stateDir-relative; the Tier-2 test drives the REAL BackupManager and asserts the glob's expanded set lands in the snapshot — R3-C4).
35
+ - CLAUDE.md: `GET /conversations*` Capabilities entry via `migrateClaudeMd()` (existing agents) AND `generateClaudeMd()` (new agents) — P3 + P5 both.
36
+
37
+ ## Rollback
38
+
39
+ - Runtime: `conversationIdentity.recording.enabled: false` (kill-switch — legacy-identical degradation); `followThrough` stays dark on the fleet by default. Full revert: delete the three new modules + the route block + the dispatch mint lines + the config/migrator entries; `state/conversation-registry.json` + `conversation-registry.jsonl*` are inert to old code. A later RE-enable needs no special path — the idempotent boot adoption pass + journal replay compose over whatever is on disk (R8-low-1).
40
+
41
+ ## Tests
42
+
43
+ - `tests/unit/conversation-identity.test.ts` — golden parity with the legacy hash (channel + thread), frozen schema-v1 constants, tuple/key forms + shape clamps, the shared displacement walk + its MAX_PROBE_DISTANCE bound.
44
+ - `tests/unit/conversation-registry.test.ts` — mint idempotency (incl. across re-open), the crafted in-charset collision pair probing to DISTINCT ids in either order, WAL crash-durability (probed + durable-binding survive with NO snapshot flush; speculative writes no synchronous journal line), torn-tail discard, non-tail-corruption fail-closed, unknown-op suspension, rotation-spanning replay with one global seq, idempotent replay, snapshot completeness (bind-pins/ambiguous-sends/send-intents), alias one-hop + the boot assignment-beats-alias invariant, breaker (speculative drop + re-mint, durable carve-out + typed capacity refusal), recording kill-switch both arms, workspace pinning (self-corroboration, multi-workspace refusal, config-pin authority, `_`→teamId in-place upgrade), adoption-pass gating + idempotency, resolution surfaces, health shape.
45
+ - `tests/unit/deliver-to-conversation.test.ts` — §5.1 typed contract (dark/dry/unresolvable/replicated-only/system-channel/no-adapter/transport-error all non-exceptional, never success-shaped; dryRun audit line), in-thread delivery, Telegram passthrough.
46
+ - `tests/unit/conversation-identity-mint-idiom-ratchet.test.ts` — the §10 wiring-integrity grep ratchet (a fourth mint-idiom copy is a CI failure).
47
+ - Integration (`tests/integration/conversation-registry-routes.test.ts`) — the `GET /conversations*` HTTP pipeline (auth, 404 semantics, label escaping, resolve), migrateConfig never-materializes assertions, the backup-manifest-through-real-BackupManager test.
48
+ - E2E (`tests/e2e/conversation-registry-lifecycle.test.ts`) — Phase-1 "feature is alive": production-init wiring answers `GET /conversations/health` 200 (not 503), adoption pass state present, inbound→mint→resolve cycle.
49
+
50
+ ## Agent awareness
51
+
52
+ - A "Durable Conversation Identity" Capabilities entry (GET /conversations*, the health surface, what a negative topicId means) ships in `generateClaudeMd` + an idempotent content-sniffed `migrateClaudeMd` section. <!-- tracked: durable-conversation-identity -->
53
+
54
+ ## Part-2 landing note (wiring)
55
+
56
+ Part 1 landed the three core modules + Tier-1 tests. Part 2 (this commit)
57
+ lands the wiring: the read-only `GET /conversations*` routes (labels escaped —
58
+ the only Phase-1 render surface), the AgentServer plumbing (bootstrap instance
59
+ takes precedence; a read-only fallback keeps the health surface alive on any
60
+ init path — it can never become a second journal writer because eager mint
61
+ uses the bootstrap instance only), the server-bootstrap construction (live
62
+ kill-switch reads, late-bound workspace source, attention wiring), the §6.3
63
+ eager mint + bootstrap-context carry, the §6.2 adoption pass (session-registry
64
+ membership as the authorized-traffic record), config types/defaults +
65
+ DEV_GATED_FEATURES registration, the PostUpdateMigrator additions (backup
66
+ manifest, dev-gate strip, CLAUDE.md section + shadow markers), the scaffold
67
+ template section, and the Tier-2/Tier-3 tests + docs-coverage artifacts
68
+ (release fragment + architecture page).
69
+
70
+ ## Part-3 landing note (surface classification + test parity)
71
+
72
+ CapabilityIndex gains the `/conversations` prefix classification (agent-facing
73
+ capability — surfaced in /capabilities discovery, not INTERNAL_PREFIXES), and
74
+ the backup-manifest parity test learns the two new manifest entries (the
75
+ predictable behavior-change-breaks-old-tests category, swept before push).
76
+
77
+ ## Part-4 landing note (release-fragment polish)
78
+
79
+ The release fragment's "What to Tell Your User" line drops inline code
80
+ formatting (the pre-push gate's plain-conversational rule).
81
+
82
+ ## Part-5 landing note (fail-safe tagging)
83
+
84
+ Every intentional fail-safe catch this increment introduced carries an
85
+ explicit `@silent-fallback-ok` tag with its safe-direction justification
86
+ (registry: fsync best-effort / prune-retains / workspace-degrades-to-
87
+ placeholder / shutdown teardown; wiring: fail-toward-delivery on boot-load,
88
+ eager mint, adoption pass, attention raise) — the no-silent-fallbacks ratchet
89
+ stays at its 491 baseline with zero untagged swallows from this PR.