instar 1.3.488 → 1.3.490
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.
- package/dashboard/index.html +7 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +979 -28
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +23 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/CodexResumeMap.d.ts +95 -0
- package/dist/core/CodexResumeMap.d.ts.map +1 -0
- package/dist/core/CodexResumeMap.js +283 -0
- package/dist/core/CodexResumeMap.js.map +1 -0
- package/dist/core/MeshRpc.d.ts +3 -0
- package/dist/core/MeshRpc.d.ts.map +1 -1
- package/dist/core/MeshRpc.js +5 -0
- package/dist/core/MeshRpc.js.map +1 -1
- package/dist/core/ModelSwapService.d.ts +26 -11
- package/dist/core/ModelSwapService.d.ts.map +1 -1
- package/dist/core/ModelSwapService.js +59 -21
- package/dist/core/ModelSwapService.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +16 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +62 -2
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/SessionManager.d.ts +4 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +3 -0
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/SessionRefresh.d.ts +36 -7
- package/dist/core/SessionRefresh.d.ts.map +1 -1
- package/dist/core/SessionRefresh.js +90 -29
- package/dist/core/SessionRefresh.js.map +1 -1
- package/dist/core/TopicProfileOrchestrator.d.ts +480 -0
- package/dist/core/TopicProfileOrchestrator.d.ts.map +1 -0
- package/dist/core/TopicProfileOrchestrator.js +1404 -0
- package/dist/core/TopicProfileOrchestrator.js.map +1 -0
- package/dist/core/TopicProfileResolver.d.ts +104 -0
- package/dist/core/TopicProfileResolver.d.ts.map +1 -0
- package/dist/core/TopicProfileResolver.js +231 -0
- package/dist/core/TopicProfileResolver.js.map +1 -0
- package/dist/core/TopicProfileStore.d.ts +308 -0
- package/dist/core/TopicProfileStore.d.ts.map +1 -0
- package/dist/core/TopicProfileStore.js +781 -0
- package/dist/core/TopicProfileStore.js.map +1 -0
- package/dist/core/TopicProfileTransferCarrier.d.ts +227 -0
- package/dist/core/TopicProfileTransferCarrier.d.ts.map +1 -0
- package/dist/core/TopicProfileTransferCarrier.js +533 -0
- package/dist/core/TopicProfileTransferCarrier.js.map +1 -0
- package/dist/core/TopicResumeMap.d.ts +67 -1
- package/dist/core/TopicResumeMap.d.ts.map +1 -1
- package/dist/core/TopicResumeMap.js +114 -1
- package/dist/core/TopicResumeMap.js.map +1 -1
- package/dist/core/classifyProfileChange.d.ts +83 -0
- package/dist/core/classifyProfileChange.d.ts.map +1 -0
- package/dist/core/classifyProfileChange.js +274 -0
- package/dist/core/classifyProfileChange.js.map +1 -0
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +6 -0
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/core/frameworkSessionLaunch.d.ts +26 -0
- package/dist/core/frameworkSessionLaunch.d.ts.map +1 -1
- package/dist/core/frameworkSessionLaunch.js +58 -0
- package/dist/core/frameworkSessionLaunch.js.map +1 -1
- package/dist/core/slackRefreshBinding.d.ts +83 -0
- package/dist/core/slackRefreshBinding.d.ts.map +1 -0
- package/dist/core/slackRefreshBinding.js +54 -0
- package/dist/core/slackRefreshBinding.js.map +1 -0
- package/dist/core/topicProfileIngress.d.ts +128 -0
- package/dist/core/topicProfileIngress.d.ts.map +1 -0
- package/dist/core/topicProfileIngress.js +249 -0
- package/dist/core/topicProfileIngress.js.map +1 -0
- package/dist/core/topicProfileValidation.d.ts +109 -0
- package/dist/core/topicProfileValidation.d.ts.map +1 -0
- package/dist/core/topicProfileValidation.js +247 -0
- package/dist/core/topicProfileValidation.js.map +1 -0
- package/dist/core/topicProfileWriteSurface.d.ts +222 -0
- package/dist/core/topicProfileWriteSurface.d.ts.map +1 -0
- package/dist/core/topicProfileWriteSurface.js +631 -0
- package/dist/core/topicProfileWriteSurface.js.map +1 -0
- package/dist/core/types.d.ts +37 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/lifeline/TelegramLifeline.d.ts.map +1 -1
- package/dist/lifeline/TelegramLifeline.js +6 -0
- package/dist/lifeline/TelegramLifeline.js.map +1 -1
- package/dist/messaging/TelegramAdapter.d.ts +10 -1
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +41 -1
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +9 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +32 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +52 -3
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +1 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts +17 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +271 -1
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +66 -66
- package/src/scaffold/templates.ts +9 -0
- package/upgrades/1.3.489.md +23 -0
- package/upgrades/1.3.490.md +34 -0
- package/upgrades/side-effects/pool-tile-status-filter.md +46 -0
- package/upgrades/side-effects/topic-profile.md +142 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Every conversation topic can now carry a durable **profile** that pins its baseline framework (`claude-code` / `codex-cli` / …), model (an explicit id OR a tier — never both), and thinking depth (`off` / `low` / `medium` / `high` / `max`). The pin survives restarts and follows the topic. When a pin changes, a new orchestrator picks the gentlest way to apply it: a within-framework Claude model-tier change on an idle session swaps in flight with zero loss; otherwise the session is restarted via `claude --resume` (no conversation lost) or, when no resume point is capturable, continued from recent history + memory — with the real cost disclosed honestly in each case. Protected and mid-task sessions are never interrupted (a busy session applies the switch when it goes idle, or immediately if the operator says "switch now").
|
|
9
|
+
|
|
10
|
+
The conversational surface is primary: "use codex here", "pin this topic to Fable", "set high thinking on this topic" — the agent proposes the change back in plain words, confirms, and the pin is durable from then on. A `/topic-profile` HTTP route and a power-user `/topic` command exist for the dashboard, but the agent never instructs the user to type a command.
|
|
11
|
+
|
|
12
|
+
Ships **dark** behind a dev-agent gate (dry-run by default); the fleet serves 503 until it graduates.
|
|
13
|
+
|
|
14
|
+
## What to Tell Your User
|
|
15
|
+
|
|
16
|
+
If you run more than one topic and want some of them on a different model, a different agent framework, or a different thinking depth — and you want that choice to STICK — you can now just say so in that topic ("use codex here", "pin this to Fable", "set high thinking"). I'll confirm it and remember it across restarts. If a topic is mid-task when you change it, I won't interrupt the work — I'll apply the change when it finishes, or right away if you tell me to "switch now". I'll always tell you honestly whether applying the change costs anything (most of the time it doesn't).
|
|
17
|
+
|
|
18
|
+
## Summary of New Capabilities
|
|
19
|
+
|
|
20
|
+
- Durable per-topic `{ framework, model, thinkingMode }` profile that survives restarts and follows the topic.
|
|
21
|
+
- Conversational set/change/read of a topic's profile via the propose-confirm flow (verified-operator–gated).
|
|
22
|
+
- Gentlest-swap orchestration: in-flight model-tier swap (idle Claude) → `claude --resume` restart → continuation, each with honest loss disclosure.
|
|
23
|
+
- Protected/busy/autonomous sessions are never profile-killed; "switch now" overrides busy (never protection).
|
|
24
|
+
- A `/topic-profile` operator/dashboard read-write route and a `/topic` power-user command.
|
|
25
|
+
- Cross-machine: a topic's profile is pulled to whichever machine acquires the topic (mesh `topic-profile-pull` verb).
|
|
26
|
+
|
|
27
|
+
## Evidence
|
|
28
|
+
|
|
29
|
+
- `pnpm build` EXIT 0 (clean). no-silent-fallbacks ratchet 5/5 (baseline untouched).
|
|
30
|
+
- Unit tier green; integration 278 files / 2592 passed; topic-profile e2e lifecycle 8/8.
|
|
31
|
+
- Composition-root wiring locked by `tests/unit/topic-profile-server-wiring.test.ts` (20 assertions).
|
|
32
|
+
- Swap-method decision covered both canary arms in `tests/unit/classifyProfileChange.test.ts`.
|
|
33
|
+
- Side-effects review + required second-pass review (session lifecycle + gates): `upgrades/side-effects/topic-profile.md` (reviewer concurred — no ship-blocking concerns).
|
|
34
|
+
- Spec: `docs/specs/TOPIC-PROFILE-SPEC.md` (converged round 16, approved).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Side-Effects Review — Pool dashboard tiles: filter remote sessions to live statuses
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `pool-tile-status-filter`
|
|
4
|
+
**Date:** `2026-06-11`
|
|
5
|
+
**Author:** `echo (instar-dev agent)`
|
|
6
|
+
**Second-pass reviewer:** `not required` (one-line client-side display filter; no lifecycle, no gates, no messaging surface)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
The dashboard's pool poll fed `renderSessionList` every record a peer's plain `GET /sessions` returned — and that endpoint returns the peer's FULL registry, completed/killed records included — while local tiles are built from `listRunningSessions()` (running only). Result, observed live 2026-06-11 (topic 13481): five Mac Mini sessions closed hours earlier reappeared on the laptop dashboard as live "click to stream" tiles. One-line fix in `dashboard/index.html`: the remote merge now filters to `status === 'running' || status === 'starting'`, matching the local sidebar's definition of "live". New source-assert test (`tests/unit/dashboard-poolTileStatusFilter.test.ts`, following the established `dashboard-sessionMachineBadge.test.ts` at-rest inspection pattern) proven failing on the unfixed HTML first.
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
No decision points. A client-side display filter; the API response is unchanged and remains faithful (full registry, accurate statuses).
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. Over-block
|
|
19
|
+
A peer session in a transient status other than running/starting would be hidden — the only other statuses are terminal (`completed`/`failed`/`killed`), which is exactly what should be hidden. No issue identified.
|
|
20
|
+
|
|
21
|
+
## 2. Under-block
|
|
22
|
+
A peer whose registry wrongly marks a dead session `running` still renders a tile — that is the upstream ghost-record problem, fixed separately at the registry funnel (PR #1067); the two fixes are complementary layers, not overlap.
|
|
23
|
+
|
|
24
|
+
## 3. Level-of-abstraction fit
|
|
25
|
+
Correct layer: the SERVER's pool response stays a faithful full-registry view (other consumers may legitimately want terminal records, e.g. forensics); "which records are live tiles" is a display decision, made where display happens, identically to how local tiles already decide it.
|
|
26
|
+
|
|
27
|
+
## 4. Signal vs authority compliance
|
|
28
|
+
- [x] No — this change has no block/allow surface. (Display filtering only.)
|
|
29
|
+
|
|
30
|
+
## 5. Interactions
|
|
31
|
+
The remote-tile click path, machine badges, row namespacing, and stream subscriptions all operate on the filtered set — strictly fewer dead tiles to mis-click. The 15s pool poll cadence is unchanged. No shadowing/double-fire/races (pure pure-function filter on a fetch result).
|
|
32
|
+
|
|
33
|
+
## 6. External surfaces
|
|
34
|
+
`dashboard/index.html` ships in the npm package and replaces wholesale on update; no API change, no config, no migration. Mixed-version: an updated dashboard against any peer version works (the filter only reads fields every version already returns).
|
|
35
|
+
|
|
36
|
+
## 7. Rollback cost
|
|
37
|
+
Revert the one line, ship a patch. No state, no migration. Symptom during rollback window: dead peer tiles reappear (cosmetic).
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Conclusion
|
|
42
|
+
|
|
43
|
+
Minimal display-truthfulness fix; the registry-side ghost cleanup (PR #1067) and this filter together close both layers of the "dead sessions on the dashboard" symptom: records that should not say running, and tiles that should not render non-running records. Clear to ship.
|
|
44
|
+
|
|
45
|
+
**Phase 1 principle check (recorded):** no decision point — display filter.
|
|
46
|
+
**Phase 2 plan (recorded):** fresh worktree `.worktrees/fix-pool-tile-status-filter` off `JKHeadley/main` @ `e6c21fa8e` (v1.3.487), agent identity set, canonical remote verified. Rollback: revert (above).
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Side-Effects Review Artifact — Topic Profile.
|
|
3
|
+
Driven by docs/specs/TOPIC-PROFILE-SPEC.md (review-convergence 2026-06-11; approved: true).
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
# Side-Effects Review — Topic Profile (sticky per-topic framework / model / thinking-mode)
|
|
7
|
+
|
|
8
|
+
**Version / slug:** `topic-profile`
|
|
9
|
+
**Date:** `2026-06-12`
|
|
10
|
+
**Author:** `echo`
|
|
11
|
+
**Second-pass reviewer:** `echo (dedicated reviewer subagent — session lifecycle + gates, see below)`
|
|
12
|
+
|
|
13
|
+
## Summary of the change
|
|
14
|
+
|
|
15
|
+
Topic Profile gives every conversation topic a durable, sticky profile that pins its BASELINE framework (`claude-code`/`codex-cli`/…), model (an explicit id OR a tier — never both), and thinking depth (`off`/`low`/`medium`/`high`/`max`). Pins survive restarts and follow the topic. The profile is resolved at session spawn; when a pin changes, an orchestrator (`TopicProfileOrchestrator`) classifies the change (`classifyProfileChange`) and picks the GENTLEST swap path: a within-framework Claude model-tier change on a confirmed-idle session swaps in-flight (zero loss, only when the §11 canary verifies an independent thinking-control read — which v1 ships OFF, so it degrades to resume); otherwise a kill + `claude --resume` (none-loss) or, when no resume is capturable, a CONTINUATION (recent-history + memory). The conversational surface is PRIMARY ("use codex here" / "pin this topic to Fable" via the propose-confirm ingress); the `/topic-profile` HTTP route + `/topic` command are operator/power-user surfaces. The feature ships DARK behind a dev-agent gate (`resolveDevAgentGate`, dryRun default true) — the fleet serves 503.
|
|
16
|
+
|
|
17
|
+
Primary files: `src/core/TopicProfileStore.ts`, `TopicProfileResolver.ts`, `TopicProfileOrchestrator.ts`, `TopicProfileTransferCarrier.ts`, `classifyProfileChange.ts`, `CodexResumeMap.ts`, `topicProfileIngress.ts`, `topicProfileWriteSurface.ts`, `topicProfileValidation.ts`, `slackRefreshBinding.ts`; wiring in `src/commands/server.ts` (composition root) + `src/server/routes.ts` + `AgentServer.ts`; `SessionRefresh.ts` (§10.5 Slack respawn); migrations in `PostUpdateMigrator.ts`; awareness in `scaffold/templates.ts`; classification in `server/CapabilityIndex.ts`.
|
|
18
|
+
|
|
19
|
+
## Decision-point inventory
|
|
20
|
+
|
|
21
|
+
- `classifyProfileChange()` (swap-method decision) — **add** — pure function: maps (lastApplied, resolved, idle/canary/resume verification state) → `none | in-flight | resume | continuation`, with protected-session deferral. The source of truth; tested across every §7/§11 matrix row both canary arms.
|
|
22
|
+
- `TopicProfileOrchestrator` §8 kill/respawn orchestration — **add** — debounced, globally-capped (K=2) respawn engine; protected sessions never profile-killed; busy/autonomous sessions defer (switch-now overrides busy, never protection); §10.4 circuit breaker parks the pin + reverts after N attributable failures.
|
|
23
|
+
- Write-surface operator gate — **add** — every profile WRITE requires the topic's verified bound operator + the `X-Instar-Request` intent header (route) / authorized-sender (ingress); deny-by-default.
|
|
24
|
+
- Dev-agent dark gate (`resolveDevAgentGate`) — **add** — the whole feature resolves LIVE per-call; dark arm returns 503 (route) / no-op (orchestrator).
|
|
25
|
+
- §8(2) resume-map write-gate chokepoint — **modify** — ALL `_topicResumeMap` writers now pass through `orchestrator.claudeResumeWriteGate(topicId)` so a park/unpark during an in-flight swap can't be clobbered.
|
|
26
|
+
- Obligation-7 ingress `switch-now` → `orchestrator.handleSwitchNow` bridge — **add** — routes an operator's "switch now" reply to the orchestrator's own armed confirm when no write-surface slot is armed (precedence-preserving).
|
|
27
|
+
- Migrations: CLAUDE.md awareness section, additive config defaults, `/topic-profile` capability classification, framework-shadow markers — **add** — Migration-Parity for existing agents.
|
|
28
|
+
- Legacy `topic-frameworks.json` mirror — **modify (read-only seed)** — one-directional: the new store seeds FROM it once; the store never writes back. Full retirement is deferred and tracked (CMT-1368).
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 1. Over-block
|
|
33
|
+
|
|
34
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
35
|
+
|
|
36
|
+
The only block/allow surface is the profile WRITE gate. Concrete over-block shapes:
|
|
37
|
+
- A legitimate operator says "use codex here" in a topic that has NO verified bound operator yet → the write is refused (403 `no-bound-operator`). This is intentional (Know-Your-Principal: an unverified principal can't set routing), but it IS an over-block in the narrow sense that a real operator's intent is declined until their identity is bound from an authenticated send. Mitigation: operator binding is automatic from the authenticated sender, so in practice the first authorized message binds them; the refusal only fires for a genuinely unbound topic.
|
|
38
|
+
- A `/topic-profile` route write without the `X-Instar-Request` header → 403. Intentional CSRF-class guard; a same-origin dashboard/agent caller always sends it.
|
|
39
|
+
- A malformed topic key (non-numeric) at the route boundary → 400 (clamped). Correct.
|
|
40
|
+
|
|
41
|
+
No conversational MESSAGE is ever blocked — a non-trigger turn falls straight through to normal conversation; only an explicit profile-trigger phrase from an authorized sender is acted on.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 2. Under-block
|
|
46
|
+
|
|
47
|
+
**What failure modes does this still miss?**
|
|
48
|
+
|
|
49
|
+
- The §11 thinking-control canary ships OFF (`claudeThinkingControlAvailable:false`), so the orchestrator NEVER claims an in-flight thinking swap it can't verify — it degrades to resume. This is deliberately under-claiming (the safe direction); the "miss" is that a thinking-mode change always costs a respawn rather than a zero-loss in-flight swap, until the canary is built. Disclosed in the swap-quality matrix.
|
|
50
|
+
- `readIdle` is three-valued and fails toward BUSY (an unconfirmed pane read defers the kill). A session that is actually idle but reads `unconfirmed` will defer a respawn to the next tick — a latency miss, never a wrong kill.
|
|
51
|
+
- The in-flight swap, on an `unconfirmed` swap result, never guesses again — it falls back to kill+resume on the next confirmed-idle window. A swap that genuinely succeeded but reported `unconfirmed` would cost an unnecessary respawn (none-loss), not a wrong state.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 3. Level-of-abstraction fit
|
|
56
|
+
|
|
57
|
+
**Is this at the right layer?**
|
|
58
|
+
|
|
59
|
+
The swap DECISION is a pure low-level classifier (`classifyProfileChange`) — cheap, deterministic, fully unit-tested both arms. The ORCHESTRATION (kill/respawn/in-flight/defer) is a stateful authority that consumes that classification plus live idle/verification reads. This is the correct split: the brittle part (which swap method) is a pure function with no side effects; the authority part (when to actually kill) layers protection/busy/breaker disciplines on top and is the only thing that touches sessions. It FEEDS the existing session-spawn path (`spawnSessionForTopic`) and the existing model-swap route (`ModelSwapService.swap`) rather than re-implementing them; it USES the existing `_topicResumeMap` (gated at one chokepoint) rather than a parallel resume store. No lower-level primitive is re-implemented.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 4. Signal vs authority compliance
|
|
64
|
+
|
|
65
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
66
|
+
|
|
67
|
+
**Does this change hold blocking authority with brittle logic?**
|
|
68
|
+
|
|
69
|
+
- [x] No — this change has no block/allow surface on the hot path. A profile change is a ROUTING decision (P2: "a profile change is a routing decision, never a block"). The orchestrator produces RESPAWNS, never a block of a user message or an operation.
|
|
70
|
+
|
|
71
|
+
The one allow/deny surface (the write gate) is not brittle: it is the existing verified-operator binding (an authenticated-sender fact, not a heuristic) plus a static intent-header check. No LLM, no brittle classifier owns any block authority. The orchestrator's kill decisions are disciplined by hard invariants (protected never killed — fails CLOSED to protected on a read fault; busy defers; breaker parks), not by a guess.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 5. Interactions
|
|
76
|
+
|
|
77
|
+
- **Shadowing:** the §8(2) resume-map write-gate sits in front of EVERY `_topicResumeMap` writer. Verified it gates (park/unpark coordination) without dropping writes — an ungated write during an in-flight swap was the clobber risk; the chokepoint serializes it. The ingress `switch-now` bridge runs only in the empty-write-surface-slot branch, so it never shadows the propose-confirm/reapply handlers (precedence preserved; pinned by `topic-profile-server-wiring.test.ts`).
|
|
78
|
+
- **Double-fire:** the spawn-path success recorder is guarded by `!_orchestratorSpawnInFlight.has(topicId)` so an orchestrator-initiated respawn does not double-count into the §10.4 breaker (which it is mid-evaluating). Verified.
|
|
79
|
+
- **Races:** the store is a single-writer CAS (`mutate()`); concurrent writes serialize. Protected-session reads fail CLOSED. The 30s orchestrator tick and the carrier tick are unref'd and best-effort (a fault is swallowed and retried next tick — `@silent-fallback-ok` justified). Disclosures route through the adapter directly, bypassing the `/telegram/reply` exact-duplicate window, and carry an audit sequence so consecutive notices are never byte-identical (interaction with the duplicate-message suppression — confirmed non-colliding).
|
|
80
|
+
- **Feedback loops:** none. A respawn re-resolves the (now-updated) pin and applies it once; `recordApplied` marks it so the next reconcile sees convergence, not a re-trigger.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 6. External surfaces
|
|
85
|
+
|
|
86
|
+
- **Other agents (same machine / mesh):** adds one MeshRpc verb `topic-profile-pull` (capability-gated; a peer must advertise it). The transfer carrier pulls a topic's profile at acquire-time; a local durable write cancels a pending REPLACE pull. No effect on peers that don't advertise the capability.
|
|
87
|
+
- **Install base (existing agents):** receives the CLAUDE.md awareness section, additive config defaults (add-missing-only), the `/topic-profile` capability classification, and framework-shadow markers — all via `PostUpdateMigrator` (idempotent). New agents get them via `init`.
|
|
88
|
+
- **External systems:** Telegram disclosures (the §8 propose-confirm / switch-now / breaker notices) post to the topic via the adapter. No Slack/GitHub/Cloudflare surface beyond the existing §10.5 Slack session-refresh path.
|
|
89
|
+
- **Persistent state:** new additive files — `state/topic-profiles.json` (the pin store), `state/topic-profile-orchestrator.json` (orchestrator slots), `logs/`-side audit appends (`appendTopicProfileAudit`). The legacy `topic-frameworks.json` is read ONLY (one-directional seed). No existing file's schema changes.
|
|
90
|
+
- **Timing:** a profile change on a busy/autonomous session defers to the next idle window — user-visible only as "I'll apply the switch the moment it goes idle, or say 'switch now'."
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 7. Rollback cost
|
|
95
|
+
|
|
96
|
+
- **Hot-fix:** the feature ships DARK behind `resolveDevAgentGate` + `dryRun:true`. The instant back-out is config (no code revert needed): the dark arm already serves 503 / no-op on the fleet. A code revert is a clean patch — the new modules are additive; the only modification to existing hot paths is the resume-map write-gate chokepoint (reverts to ungated) and the ingress switch-now branch (reverts to the plain no-op reply).
|
|
97
|
+
- **Data migration:** none required. The new state files are additive and self-initializing; deleting them resets profiles to defaults with no corruption. The legacy mirror is untouched (read-only).
|
|
98
|
+
- **Agent state repair:** none — existing agents that received the migration keep an awareness section + inert config defaults that no-op while the gate is dark.
|
|
99
|
+
- **User visibility:** none during rollback — a dark feature going darker is invisible; a dev-agent that had it enabled would simply stop applying new pins (existing sessions unaffected; pins persist in the store for when it re-enables).
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Conclusion
|
|
104
|
+
|
|
105
|
+
This review produced no design changes to the core orchestration (it was already converged over 16 rounds), but the integrating session DID change one thing as a direct result of re-examining the deferral surface: obligation 7 (the ingress `switch-now` confirm bridge) was found to be a genuine dead-end (the orchestrator disclosed "say 'switch now'" while the ingress replied "nothing pending") and was BUILT rather than shipped deferred — precedence-preserving, so no existing confirm flow changed behavior. The feature is clear to ship: it holds no brittle block authority (a profile change is a routing decision), the one allow/deny surface is the existing verified-operator gate, every kill decision is disciplined by hard invariants that fail in the safe direction, and the whole feature is dark-gated with a config-only back-out. Residual deferrals (legacy-mirror retirement, maturation-track sink, the §11 in-flight thinking canary) are tracked via durable commitments CMT-1368/CMT-1369 and the spec's §11 contingency.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Second-pass review (if required)
|
|
110
|
+
|
|
111
|
+
**Reviewer:** echo (dedicated reviewer subagent — required: the change touches session lifecycle (kill/respawn) AND gate/auth surfaces)
|
|
112
|
+
**Independent read of the artifact: concur** — I traced the real code on all six review axes and the artifact's safety claims hold; the change is shippable with the minor advisory follow-ups noted below (none ship-blocking).
|
|
113
|
+
|
|
114
|
+
Evidence verified against the code:
|
|
115
|
+
|
|
116
|
+
- **Session-lifecycle safety (PASS).** Protection is a hard precondition. `respawnPhase` checks `classification.protectedDeferral` and defers (`TopicProfileOrchestrator.ts:943-948`) BEFORE it ever computes `switchNow` (`:950`) or reaches the kill path (`:993+`), so "switch now" structurally cannot kill a protected session. `protectedDeferral` is sourced from `session.isProtected` in the classifier (`classifyProfileChange.ts:157, 74-75`), and the dep FAILS CLOSED: `isProtectedSession` returns `true` on a read fault (`server.ts:13361-13368`, `catch { return true }`). The global respawn cap K is real and enforced — `maxConcurrentProfileRespawns: 2` (`server.ts:13386`) gates the FIFO drain loop (`TopicProfileOrchestrator.ts:794`). An `unconfirmed` idle read defers rather than kills: `readIdle` returns `'unconfirmed'` on a null tail (`server.ts:13260`), and at kill time `deferUntilIdle = busyish = !confirmedIdle` treats unconfirmed as busy (`classifyProfileChange.ts:151-153`, `:976` defers). The in-flight (no-kill) row also honors `protectedDeferral` (`:943` runs before the in-flight branch at `:953`).
|
|
117
|
+
|
|
118
|
+
- **Gate / auth surface (PASS).** Every write requires a verified bound operator: `authorize()` refuses operator writes when unbound or sender≠bound-operator, and refuses token writes when no operator is bound (`topicProfileWriteSurface.ts:697-734`). The HTTP route is token-trust (`principal: { kind: 'token' }`, `routes.ts:5467`) with body `updatedBy` ignored by construction. The dark gate is consulted LIVE per-call, not a cached literal: `regime`/`getConfig` are closures calling `resolveDevAgentGate(cfg?.enabled, config)` on every invocation (`server.ts:3877-3883, 13377-13390`), and `resolveDevAgentGate` is a pure `explicitEnabled ?? !!config.developmentAgent` over the live config object (`devAgentGate.ts:40-45`) → DARK on the fleet. The route refuses without the intent header — `requireTopicProfileWrite` returns 403 when `req.headers['x-instar-request'] !== '1'` (`routes.ts:5432-5433`), applied to every mutating `/topic-profile/*` route.
|
|
119
|
+
|
|
120
|
+
- **§8(2) resume-map write-gate chokepoint (PASS).** `_topicResumeMap.setWriteGate(...)` is wired to `orchestrator.claudeResumeWriteGate` (`server.ts:13412-13414`). The gate is enforced INSIDE the map, not at callsites: `TopicResumeMap.save()` early-returns on a refused gate (`TopicResumeMap.ts:181`) and `refreshResumeMappings()` skips refused topics (`:347`). Since the ~20 `_topicResumeMap.save()` callsites in server.ts all funnel through that one method, the "ALL writers pass through the gate" claim is true — I found no `save`/`refreshResumeMappings` path that bypasses `gateAllows`.
|
|
121
|
+
|
|
122
|
+
- **Obligation-7 switch-now bridge (PASS, precedence-preserving).** `case 'switch-now'` peeks the write-surface slot FIRST (`server.ts:1593`); only the empty-slot branch (`!armedSlot`) consults `_topicProfileOrchestrator.handleSwitchNow` (`:1603-1606`); an armed write-surface slot routes to `handleProfileConfirm` UNCHANGED (`:1612`). `handleSwitchNow` only fires a slot whose `kind === 'switch-now'` (`:1224-1231`), and `executeSwitchNow` sets `switchNowOverride` but the respawn phase still re-checks protection first — so the bridge cannot override protection.
|
|
123
|
+
|
|
124
|
+
- **Signal-vs-authority (PASS).** The only allow/deny surface is the write gate, which is a verified-operator fact + a static `x-instar-request` header check — no LLM, no heuristic owns block authority on the hot path. Every kill decision is disciplined by hard invariants (protected fail-closed; busy/unconfirmed defer; breaker parks), consistent with the artifact's §4 claim.
|
|
125
|
+
|
|
126
|
+
Advisory follow-ups (NOT ship-blocking; the feature is dark-gated):
|
|
127
|
+
|
|
128
|
+
- **Codex resume gate is defined but unwired.** `orchestrator.codexResumeWriteGate` (`TopicProfileOrchestrator.ts:1524`) is never installed on `_codexResumeMap` — `CodexResumeMap` has no `setWriteGate` mechanism at all, and `_codexResumeMap` has zero external save callsites in server.ts (it is orchestrator-private, written only by `captureAtKill` under the respawn lock). So there is no actual mid-switch re-poison vector to gate today, and the absence is SAFE — but the artifact's §8(2) framing ("ALL resume-map writers pass through the gate") is precisely true only for the Claude map. Recommend a one-line note in the build report that the codex gate is dormant-by-design until codex resume is captured on the natural spawn path (the matching obligation to CMT-1368's codex work).
|
|
129
|
+
|
|
130
|
+
- **`gateAllows` fails OPEN on a gate throw** (`TopicResumeMap.ts:163-172`, `catch { return true }`). This is the deliberate "a broken gate must not silence resume capture" direction and is correctly distinct from the session-kill protection (which fails CLOSED). Worth one explicit sentence in the artifact's §2/§4 so the two opposite fail-directions (resume-write fails-open vs kill-protection fails-closed) are not conflated by a future reader. No code change needed.
|
|
131
|
+
|
|
132
|
+
Both follow-ups are documentation precision, not behavior defects. No protection-bypass, auth hole, or data-loss race found.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Evidence pointers
|
|
137
|
+
|
|
138
|
+
- Build: `pnpm build` EXIT 0 (clean).
|
|
139
|
+
- Tests: unit tier green; integration 278 files / 2592 passed; topic-profile e2e lifecycle 8/8; my changed unit files 285/285 (`TopicProfileOrchestrator`, `capabilities-discoverability`, `topic-profile-server-wiring`, `feature-delivery-completeness`, `no-silent-fallbacks` ratchet 5/5).
|
|
140
|
+
- Wiring integrity: `tests/unit/topic-profile-server-wiring.test.ts` (20 assertions over the composition root — construction, object-identity late-bind, real-deps-not-noops, lifecycle hooks, carrier mesh verb, the §11 conservative-canary flags, the obligation-7 bridge).
|
|
141
|
+
- Canary both arms: `tests/unit/classifyProfileChange.test.ts` (canary-passed→in-flight, canary-off→resume, idle-unconfirmed→never-in-flight, cross-model/level/off↔on/codex-rollout rows).
|
|
142
|
+
- Recovery ledger: `docs/specs/reports/topic-profile-BUILD-PROGRESS.local.md`.
|