instar 1.3.769 → 1.3.770

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.
@@ -78,4 +78,24 @@ export declare function parseSlackConversationKey(key: string): string | null;
78
78
  * Slack↔numeric bridging (PresenceProxy, resume heartbeat).
79
79
  */
80
80
  export { candidateIdForRoutingKey as slackRoutingKeySyntheticId } from './conversationIdentity.js';
81
+ /**
82
+ * slack-respawn-bind-token fix: resolve the `bootstrapConversationIds` for a Slack
83
+ * session RESPAWN (the /sessions/refresh, quota-swap, restart, restart-all paths, all
84
+ * funneling through SessionRefresh → slackRespawner) from its `routingKey`.
85
+ *
86
+ * A FRESH Slack spawn passes `bootstrapConversationIds: [conversationId]` so the session
87
+ * mints `INSTAR_BIND_TOKEN` + `INSTAR_CONVERSATION_ID` (durable-conversation-identity §7)
88
+ * and can open durable state (a commitment) bound to its minted conversation id. The
89
+ * respawn path previously OMITTED it, so a refreshed/quota-swapped Slack session came up
90
+ * token-less and its durable binds were refused (fail-closed) → the follow-through fell
91
+ * back to a fragile session-local timer that dies on the next restart (the live-proven
92
+ * S7 gap). This restores parity. `mintForInbound` is an idempotent get-or-create, so it
93
+ * returns the SAME id the fresh dispatch resolved for this key.
94
+ *
95
+ * Fail-toward-respawn: any resolution error → `undefined` (the prior token-less
96
+ * behavior), NEVER throws — a refresh must never be blocked by id resolution.
97
+ */
98
+ export declare function slackRespawnBootstrapIds(routingKey: string, mintForInbound: (key: string) => {
99
+ id: number | null;
100
+ }): number[] | undefined;
81
101
  //# sourceMappingURL=slackRefreshBinding.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"slackRefreshBinding.d.ts","sourceRoot":"","sources":["../../src/core/slackRefreshBinding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,WAAW,mBAAmB;IAClC;qEACiE;IACjE,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACzD;;;oCAGgC;IAChC,gCAAgC,CAAC,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACtE;;;+BAG2B;IAC3B,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/C;AAED;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,cAAc,GAAG,CAC3B,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,WAAW,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,KACtD,OAAO,CAAC,MAAM,CAAC,CAAC;AAErB,eAAO,MAAM,6BAA6B,WAAW,CAAC;AAEtD,sFAAsF;AACtF,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,gFAAgF;AAChF,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIpE;AAED;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,wBAAwB,IAAI,0BAA0B,EAAE,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"slackRefreshBinding.d.ts","sourceRoot":"","sources":["../../src/core/slackRefreshBinding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,WAAW,mBAAmB;IAClC;qEACiE;IACjE,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACzD;;;oCAGgC;IAChC,gCAAgC,CAAC,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACtE;;;+BAG2B;IAC3B,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/C;AAED;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,cAAc,GAAG,CAC3B,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,WAAW,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,KACtD,OAAO,CAAC,MAAM,CAAC,CAAC;AAErB,eAAO,MAAM,6BAA6B,WAAW,CAAC;AAEtD,sFAAsF;AACtF,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,gFAAgF;AAChF,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIpE;AAED;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,wBAAwB,IAAI,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AAEnG;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK;IAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GACrD,MAAM,EAAE,GAAG,SAAS,CAYtB"}
@@ -43,4 +43,35 @@ export function parseSlackConversationKey(key) {
43
43
  * Slack↔numeric bridging (PresenceProxy, resume heartbeat).
44
44
  */
45
45
  export { candidateIdForRoutingKey as slackRoutingKeySyntheticId } from './conversationIdentity.js';
46
+ /**
47
+ * slack-respawn-bind-token fix: resolve the `bootstrapConversationIds` for a Slack
48
+ * session RESPAWN (the /sessions/refresh, quota-swap, restart, restart-all paths, all
49
+ * funneling through SessionRefresh → slackRespawner) from its `routingKey`.
50
+ *
51
+ * A FRESH Slack spawn passes `bootstrapConversationIds: [conversationId]` so the session
52
+ * mints `INSTAR_BIND_TOKEN` + `INSTAR_CONVERSATION_ID` (durable-conversation-identity §7)
53
+ * and can open durable state (a commitment) bound to its minted conversation id. The
54
+ * respawn path previously OMITTED it, so a refreshed/quota-swapped Slack session came up
55
+ * token-less and its durable binds were refused (fail-closed) → the follow-through fell
56
+ * back to a fragile session-local timer that dies on the next restart (the live-proven
57
+ * S7 gap). This restores parity. `mintForInbound` is an idempotent get-or-create, so it
58
+ * returns the SAME id the fresh dispatch resolved for this key.
59
+ *
60
+ * Fail-toward-respawn: any resolution error → `undefined` (the prior token-less
61
+ * behavior), NEVER throws — a refresh must never be blocked by id resolution.
62
+ */
63
+ export function slackRespawnBootstrapIds(routingKey, mintForInbound) {
64
+ try {
65
+ const id = mintForInbound(routingKey).id;
66
+ return typeof id === 'number' ? [id] : undefined;
67
+ }
68
+ catch {
69
+ /* @silent-fallback-ok: fail-toward-respawn — id resolution must NEVER block a Slack
70
+ session refresh/quota-swap/restart; a resolution error degrades to `undefined`
71
+ (the prior token-less behavior), exactly the safe direction. The absence of a bind
72
+ token only means this respawned session can't open durable state until its next
73
+ clean spawn — never a lost refresh. */
74
+ return undefined;
75
+ }
76
+ }
46
77
  //# sourceMappingURL=slackRefreshBinding.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"slackRefreshBinding.js","sourceRoot":"","sources":["../../src/core/slackRefreshBinding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAgDH,MAAM,CAAC,MAAM,6BAA6B,GAAG,QAAQ,CAAC;AAEtD,sFAAsF;AACtF,MAAM,UAAU,oBAAoB,CAAC,UAAkB;IACrD,OAAO,GAAG,6BAA6B,GAAG,UAAU,EAAE,CAAC;AACzD,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,yBAAyB,CAAC,GAAW;IACnD,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,6BAA6B,CAAC;QAAE,OAAO,IAAI,CAAC;IAChE,MAAM,UAAU,GAAG,GAAG,CAAC,KAAK,CAAC,6BAA6B,CAAC,MAAM,CAAC,CAAC;IACnE,OAAO,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC;AACnD,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,wBAAwB,IAAI,0BAA0B,EAAE,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"slackRefreshBinding.js","sourceRoot":"","sources":["../../src/core/slackRefreshBinding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAgDH,MAAM,CAAC,MAAM,6BAA6B,GAAG,QAAQ,CAAC;AAEtD,sFAAsF;AACtF,MAAM,UAAU,oBAAoB,CAAC,UAAkB;IACrD,OAAO,GAAG,6BAA6B,GAAG,UAAU,EAAE,CAAC;AACzD,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,yBAAyB,CAAC,GAAW;IACnD,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,6BAA6B,CAAC;QAAE,OAAO,IAAI,CAAC;IAChE,MAAM,UAAU,GAAG,GAAG,CAAC,KAAK,CAAC,6BAA6B,CAAC,MAAM,CAAC,CAAC;IACnE,OAAO,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC;AACnD,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,wBAAwB,IAAI,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AAEnG;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,wBAAwB,CACtC,UAAkB,EAClB,cAAsD;IAEtD,IAAI,CAAC;QACH,MAAM,EAAE,GAAG,cAAc,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC;QACzC,OAAO,OAAO,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACP;;;;iDAIyC;QACzC,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.769",
3
+ "version": "1.3.770",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-07-05T02:48:30.887Z",
5
- "instarVersion": "1.3.769",
4
+ "generatedAt": "2026-07-05T03:36:48.298Z",
5
+ "instarVersion": "1.3.770",
6
6
  "entryCount": 202,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -0,0 +1,40 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Fixed the final live-proven S7 gap: a Slack session that gets **respawned** (via `/sessions/refresh`,
9
+ a quota-swap, a restart, or restart-all — all funnel through `SessionRefresh` → `slackRespawner`) now
10
+ re-mints its conversation bind token. A **fresh** Slack spawn passes `bootstrapConversationIds` so the
11
+ session gets `INSTAR_BIND_TOKEN` + `INSTAR_CONVERSATION_ID` and can open durable state (a commitment)
12
+ bound to its minted conversation id; the respawn path **omitted** it, so a refreshed/quota-swapped Slack
13
+ session came up token-less, its durable binds were refused (fail-closed), and the follow-through fell
14
+ back to a fragile session-local timer that dies on the next restart. The respawn now resolves the
15
+ conversation id from the routing key (idempotent get-or-create) and passes it, restoring parity with the
16
+ fresh spawn. Telegram was unaffected (it has a `telegramTopicId` fallback; Slack had none).
17
+
18
+ ## What to Tell Your User
19
+
20
+ Nothing proactive — this is an internal robustness fix. If a user ever asks why a Slack promise didn't
21
+ survive a session swap or restart, the answer is that a restarted Slack session used to lose the small
22
+ security key it needs to save a durable, restart-proof promise, so it fell back to a flimsy timer; now
23
+ a restarted Slack session keeps that key, so the durable follow-through survives restarts.
24
+
25
+ ## Summary of New Capabilities
26
+
27
+ - A refreshed / quota-swapped / restarted Slack session now registers durable, restart-surviving
28
+ follow-through (a commitment bound to the Slack conversation), matching a fresh spawn.
29
+ - Fail-open: if the conversation-id lookup errors, the respawn proceeds (prior token-less behavior),
30
+ never blocked.
31
+ - No config, no persistent state, no Telegram change.
32
+
33
+ ## Evidence
34
+
35
+ - `tests/unit/slack-respawn-bootstrap-ids.test.ts` — 4 cases (id → `[id]`, full `channel:thread` key,
36
+ `null` → `undefined`, throwing registry → `undefined` / no rethrow).
37
+ - Existing `tests/unit/sessionRefresh-slack.test.ts` (22) stay green; `tsc` clean.
38
+ - Live proof it closes: `docs/investigations/s7-slack-delivery-repro-2026-07-04.md` §9 — Round-1
39
+ (already-running session) durable bind refused; Round-2 (fresh session) durable `CMT-1922` bound to
40
+ the minted id. This fix makes the respawn path behave like Round-2.
@@ -0,0 +1,46 @@
1
+ # ELI16 — when a Slack session restarts, it now keeps its "durable-memory key"
2
+
3
+ ## The story
4
+
5
+ There's a safety feature: when you ask the agent (over Slack) to do something later — "post a note in
6
+ 5 minutes" — it writes that promise down as a **durable commitment** so it survives even if the agent's
7
+ session restarts. To write it down, the session needs a small **security key** (a bind token) that
8
+ proves it's allowed to save state for *that specific Slack conversation*. The key is handed to the
9
+ session the moment it starts up.
10
+
11
+ The bug: a Slack session gets restarted fairly often — when the agent swaps to a different account to
12
+ avoid a rate limit, when it's refreshed, or after a crash. Every time a *fresh* session starts for a
13
+ Slack channel, it correctly gets the key. But every time a session was *restarted*, the code forgot to
14
+ hand it the key again. So a restarted Slack session came up **without** its key, couldn't write down the
15
+ durable promise (the save was refused for safety), and fell back to a flimsy in-memory timer instead —
16
+ one that vanishes if the session restarts again. That's exactly the failure we've been chasing: a Slack
17
+ promise that quietly loses its restart-proof backup after the session gets swapped.
18
+
19
+ We proved this live earlier today, as a real Slack user: a message to an *already-restarted* session →
20
+ the durable save was refused → timer fallback ("if my session had died, this note would not have been
21
+ delivered"); a message to a *fresh* session → durable save worked. Same code, one path missing the key.
22
+
23
+ ## What this change does
24
+
25
+ It makes the restart path hand the session its key again — the same way the fresh-start path already
26
+ does. When a Slack session restarts, the code now looks up which Slack conversation it belongs to (a
27
+ cheap, repeatable lookup that returns the same id every time) and gives the new session a key scoped to
28
+ that conversation. So a restarted Slack session can write down durable, restart-proof promises again,
29
+ just like a fresh one.
30
+
31
+ ## Why it's safe
32
+
33
+ - It only *restores* a capability that was accidentally dropped; it adds no new powers. The security
34
+ gate that checks the key is unchanged — this just makes sure the rightful session actually gets its
35
+ own key.
36
+ - If the lookup ever fails, it quietly falls back to the old (keyless) behavior instead of crashing —
37
+ a restart must never be blocked.
38
+ - Telegram was never affected (it had a different fallback); this only fixes the Slack restart path.
39
+ - It's a plain code change with no saved data, so rolling it back is trivial.
40
+
41
+ ## How we know it works
42
+
43
+ Four small tests check the key-lookup helper: it returns the right conversation id, it passes the full
44
+ channel/thread key through, it returns "no key" when there's no id, and — importantly — it never throws
45
+ even if the lookup blows up (so a refresh can't be blocked). The 22 existing Slack-refresh tests all
46
+ still pass.
@@ -0,0 +1,121 @@
1
+ # Side-Effects Review — Slack session respawn re-mints the conversation bind token
2
+
3
+ **Version / slug:** `slack-respawn-bind-token`
4
+ **Date:** `2026-07-04`
5
+ **Author:** `Echo`
6
+ **Second-pass reviewer:** `not required (Tier-1)`
7
+
8
+ ## Summary of the change
9
+
10
+ A FRESH Slack channel spawn passes `bootstrapConversationIds: [conversationId]` to
11
+ `spawnInteractiveSession`, so the session mints `INSTAR_BIND_TOKEN` + `INSTAR_CONVERSATION_ID`
12
+ (durable-conversation-identity §7) and can open durable state — a commitment bound to its minted
13
+ (negative) conversation id. The Slack session **respawn** closure (`slackRespawner` in
14
+ `src/commands/server.ts`, used by `/sessions/refresh`, quota-swap, restart, and restart-all — all
15
+ funnel through `SessionRefresh` → `slackRespawner`) **omitted** `bootstrapConversationIds`. So a
16
+ refreshed/quota-swapped Slack session came up **token-less** (and without `INSTAR_CONVERSATION_ID`),
17
+ its durable commitment binds were **refused fail-closed**, and the follow-through fell back to a
18
+ fragile session-local timer that dies on the next restart. This is the live-proven S7 gap (Round-1 of
19
+ the 2026-07-04 test-as-self proof: an already-running/refreshed Slack session couldn't register a
20
+ durable commitment; a fresh spawn — Round-2 — could, registering `CMT-1922` bound to `-1734007126`).
21
+ Telegram's respawn is unaffected because it passes `telegramTopicId`, which the bind-token env
22
+ resolver uses as a fallback; Slack has no such fallback. Fix: the respawn closure resolves the
23
+ conversation id from the routing key (`conversationRegistry.mintForInbound(routingKey).id`, idempotent
24
+ get-or-create → the SAME id the fresh dispatch resolves) via a new unit-tested helper
25
+ `slackRespawnBootstrapIds` and passes it as `bootstrapConversationIds` — restoring parity with the
26
+ fresh spawn. Files: `src/commands/server.ts` (closure + import), `src/core/slackRefreshBinding.ts`
27
+ (helper), + test.
28
+
29
+ ## Decision-point inventory
30
+
31
+ - `slackRespawner` closure spawn options (`src/commands/server.ts`) — **modify** — now passes
32
+ `bootstrapConversationIds` resolved from the routing key.
33
+ - `slackRespawnBootstrapIds` (`src/core/slackRefreshBinding.ts`) — **add** — the pure, tested resolver.
34
+
35
+ ## 1. Over-block / 2. Under-block
36
+
37
+ No block/allow surface. This RESTORES a capability (durable bind on a respawned Slack session) that was
38
+ mistakenly dropped. Over/under-block not applicable.
39
+
40
+ ## 3. Level-of-abstraction fit
41
+
42
+ Right layer — the fix is a one-option addition at the respawn spawn site, mirroring the fresh-spawn
43
+ site exactly; the resolution is a tiny pure helper in the existing `slackRefreshBinding` module. No new
44
+ machinery, no new authority.
45
+
46
+ ## 4. Signal vs authority compliance
47
+
48
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
49
+
50
+ - [x] No — no runtime block/allow surface. The bind token it restores is enforced by the EXISTING §7
51
+ `conversationBindGate` (unchanged); this change only ensures the legitimately-authorized session
52
+ actually receives its own token on respawn.
53
+
54
+ ## 5. Interactions
55
+
56
+ - **Shadowing:** none — additive spawn option, mirroring the fresh path.
57
+ - **Double-fire:** none. `mintForInbound` is idempotent (get-or-create), so re-resolving on respawn
58
+ returns the same id the fresh dispatch used; no duplicate conversation is minted.
59
+ - **Races:** none new — the respawn already spawned a session; this only adds one option to that call.
60
+ - **Feedback loops:** none.
61
+ - **Fail posture:** `slackRespawnBootstrapIds` FAILS TOWARD RESPAWN — any resolution error → `undefined`
62
+ (the prior token-less behavior), never throws, so a refresh can never be blocked by id resolution.
63
+
64
+ ## 6. External surfaces
65
+
66
+ - **Install base / agents:** ships with the server; a refreshed Slack session now carries its bind
67
+ token. No config migration. No behavior change for a session that already had the token (fresh
68
+ spawns) or for Telegram (unaffected — separate fallback).
69
+ - **External systems:** Slack unchanged (same channel session, same delivery). No new API.
70
+ - **Persistent state:** none — `INSTAR_BIND_TOKEN` lives only in the tmux `-e` env at spawn (stateless,
71
+ self-authenticating). This change sets it correctly on respawn; nothing is persisted.
72
+
73
+ ## 6b. Operator-surface quality
74
+
75
+ No operator surface — not applicable.
76
+
77
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
78
+
79
+ **machine-local BY DESIGN** — `machine-local-justification: physical-credential-locality`. A Slack
80
+ session is a tmux process on the machine that owns the Slack connection; its bind token is minted into
81
+ that process's env at spawn on that machine (the token is a per-session, per-machine credential — the
82
+ §7 secret is machine-local plaintext, same posture as authToken). The respawn happens on the owning
83
+ machine. There is no cross-machine state, notice, durable record, or URL introduced; the fix only
84
+ corrects a spawn-option omission on the local respawn path.
85
+
86
+ ## 8. Rollback cost
87
+
88
+ Pure code change — revert the closure option, the helper, the import, and the test. No persistent
89
+ state; a rollback returns to the (buggy) token-less respawn. Zero-cost back-out.
90
+
91
+ ## Conclusion
92
+
93
+ Closes the final live-proven S7 gap: a refreshed/quota-swapped Slack session now re-mints its
94
+ conversation bind token, so durable, restart-surviving follow-through works even after a session
95
+ churn — matching the fresh-spawn path that the 2026-07-04 test-as-self proof showed already works.
96
+ Minimal, parity-restoring, fail-open, unit-tested. Clear to ship.
97
+
98
+ ## Second-pass review (if required)
99
+
100
+ **Reviewer:** not required (Tier-1)
101
+
102
+ ## Evidence pointers
103
+
104
+ - `tests/unit/slack-respawn-bootstrap-ids.test.ts` — 4 cases: resolves id → `[id]`, passes the full
105
+ `channel:thread` routing key, `null` id → `undefined`, throwing registry → `undefined` (fails toward
106
+ respawn).
107
+ - Existing `tests/unit/sessionRefresh-slack.test.ts` (22) stay green; `tsc` clean.
108
+ - Live proof: `docs/investigations/s7-slack-delivery-repro-2026-07-04.md` §9 (Round-1 refused vs
109
+ Round-2 durable `CMT-1922`).
110
+
111
+ ## Class-Closure Declaration (display-only mirror)
112
+
113
+ - **`defectClass`** — `spawn-option-asymmetry` (`novel`; nearestExistingClass: `feature-un-enablable`;
114
+ includes: a spawn/respawn path that omits an option the parallel fresh-spawn path passes, silently
115
+ degrading a capability; excludes: an intentionally-different respawn option). Enters
116
+ `status:"unconfirmed"`, so this fix carries `closure: gap`.
117
+ - **`closure`** — `gap` — a class-level guard (a lint/test asserting respawn spawn-option parity with
118
+ the fresh-spawn site) is out of scope for this fix.
119
+ - **`guardEvidence`** — n/a for `closure: gap`.
120
+ - **`gap`** — tracked follow-up: "respawn/fresh-spawn option-parity guard — assert the Slack (and other
121
+ platform) respawn paths pass the same identity/bind options as their fresh-spawn counterparts."