instar 0.28.88 → 0.28.90

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 (46) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +91 -17
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/SessionRefresh.d.ts +108 -0
  5. package/dist/core/SessionRefresh.d.ts.map +1 -0
  6. package/dist/core/SessionRefresh.js +157 -0
  7. package/dist/core/SessionRefresh.js.map +1 -0
  8. package/dist/monitoring/CommitmentTracker.d.ts +65 -2
  9. package/dist/monitoring/CommitmentTracker.d.ts.map +1 -1
  10. package/dist/monitoring/CommitmentTracker.js +113 -0
  11. package/dist/monitoring/CommitmentTracker.js.map +1 -1
  12. package/dist/server/AgentServer.d.ts +6 -0
  13. package/dist/server/AgentServer.d.ts.map +1 -1
  14. package/dist/server/AgentServer.js +3 -0
  15. package/dist/server/AgentServer.js.map +1 -1
  16. package/dist/server/routes.d.ts +10 -0
  17. package/dist/server/routes.d.ts.map +1 -1
  18. package/dist/server/routes.js +92 -1
  19. package/dist/server/routes.js.map +1 -1
  20. package/dist/threadline/SalienceGate.d.ts +87 -0
  21. package/dist/threadline/SalienceGate.d.ts.map +1 -0
  22. package/dist/threadline/SalienceGate.js +100 -0
  23. package/dist/threadline/SalienceGate.js.map +1 -0
  24. package/dist/threadline/ThreadResumeMap.d.ts +14 -0
  25. package/dist/threadline/ThreadResumeMap.d.ts.map +1 -1
  26. package/dist/threadline/ThreadResumeMap.js.map +1 -1
  27. package/dist/threadline/ThreadlineMCPServer.d.ts +4 -0
  28. package/dist/threadline/ThreadlineMCPServer.d.ts.map +1 -1
  29. package/dist/threadline/ThreadlineMCPServer.js +10 -0
  30. package/dist/threadline/ThreadlineMCPServer.js.map +1 -1
  31. package/dist/threadline/ThreadlineRouter.d.ts +13 -0
  32. package/dist/threadline/ThreadlineRouter.d.ts.map +1 -1
  33. package/dist/threadline/ThreadlineRouter.js +47 -0
  34. package/dist/threadline/ThreadlineRouter.js.map +1 -1
  35. package/dist/threadline/TopicLinkageHandler.d.ts +161 -0
  36. package/dist/threadline/TopicLinkageHandler.d.ts.map +1 -0
  37. package/dist/threadline/TopicLinkageHandler.js +518 -0
  38. package/dist/threadline/TopicLinkageHandler.js.map +1 -0
  39. package/dist/threadline/mcp-stdio-entry.js +5 -0
  40. package/dist/threadline/mcp-stdio-entry.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/data/builtin-manifest.json +47 -47
  43. package/upgrades/0.28.89.md +68 -0
  44. package/upgrades/0.28.90.md +52 -0
  45. package/upgrades/side-effects/session-self-refresh.md +382 -0
  46. package/upgrades/side-effects/thread-topic-linkage.md +243 -0
@@ -0,0 +1,108 @@
1
+ /**
2
+ * SessionRefresh — orchestrates an agent-initiated session respawn.
3
+ *
4
+ * When an agent installs a new MCP server or skill mid-session, Claude Code
5
+ * only attaches the new tools at session start. The agent triggers this class
6
+ * to kill its current tmux session and respawn it with `claude --resume <uuid>`,
7
+ * which loads the freshly installed tools while preserving full conversation
8
+ * state.
9
+ *
10
+ * The respawn lifecycle is owned end-to-end here:
11
+ * detect — sanity-check session, topic binding, resume UUID exist
12
+ * attempt — apply rate guard, persist resume UUID, kill tmux, respawn
13
+ * verify — new session is registered for the topic
14
+ * finalize — return structured result
15
+ *
16
+ * Rate guard is a structural rate-counter (allowed-detector category per
17
+ * docs/signal-vs-authority.md "safety guards on irreversible actions"
18
+ * carve-out) — prevents infinite-respawn loops. Not a judgment call.
19
+ *
20
+ * v1 scope: Telegram-bound sessions only. Non-Telegram-bound respawn is a
21
+ * v2 follow-up — returns { ok: false, code: 'not_telegram_bound' } for now.
22
+ */
23
+ import type { SessionManager } from './SessionManager.js';
24
+ import type { StateManager } from './StateManager.js';
25
+ import type { TelegramAdapter } from '../messaging/TelegramAdapter.js';
26
+ import type { TopicResumeMap } from './TopicResumeMap.js';
27
+ export interface SessionRefreshDeps {
28
+ sessionManager: SessionManager;
29
+ state: StateManager;
30
+ telegram: TelegramAdapter | null;
31
+ topicResumeMap: TopicResumeMap | null;
32
+ /**
33
+ * Respawn callback. Wired by server bootstrap to call
34
+ * respawnSessionForTopic with the right closure (sessionManager,
35
+ * telegram, topicMemory, etc). Kept as a callback to keep this class
36
+ * decoupled from the server-internal respawn helper.
37
+ *
38
+ * IMPORTANT: respawnSessionForTopic does NOT kill the old tmux session
39
+ * — it only spawns a new one and re-registers the topic mapping.
40
+ * SessionRefresh.refreshSession is responsible for killing the old
41
+ * session via sessionManager.killSession BEFORE calling the respawner,
42
+ * which also triggers the beforeSessionKill listener that persists
43
+ * the resume UUID.
44
+ *
45
+ * Resolves to the new tmux session name on success.
46
+ */
47
+ respawner: (sessionName: string, topicId: number, followUpPrompt: string | undefined) => Promise<string>;
48
+ /** Rate-guard config. Defaults: 5 refreshes per 10-minute rolling window. */
49
+ rateLimit?: {
50
+ maxPerWindow: number;
51
+ windowMs: number;
52
+ };
53
+ /** Injectable clock for tests. Defaults to Date.now. */
54
+ clock?: () => number;
55
+ }
56
+ export type RefreshResult = {
57
+ ok: true;
58
+ newSessionName: string;
59
+ topicId: number;
60
+ } | {
61
+ ok: false;
62
+ code: RefreshFailureCode;
63
+ message: string;
64
+ };
65
+ export type RefreshFailureCode = 'rate_limited' | 'session_not_found' | 'not_telegram_bound' | 'no_telegram_adapter' | 'refresh_in_progress';
66
+ export interface RefreshOptions {
67
+ sessionName: string;
68
+ followUpPrompt?: string;
69
+ reason?: string;
70
+ }
71
+ export declare class SessionRefresh {
72
+ private readonly deps;
73
+ private readonly maxPerWindow;
74
+ private readonly windowMs;
75
+ private readonly clock;
76
+ private readonly recentRefreshes;
77
+ /** In-flight refresh guard — prevents the race where a second call
78
+ * fires before the first's kill+spawn completes, which would spawn
79
+ * two parallel sessions for the same topic. */
80
+ private readonly inFlight;
81
+ constructor(deps: SessionRefreshDeps);
82
+ /**
83
+ * Refresh a session: kill its tmux session (which fires beforeSessionKill
84
+ * so the existing listener persists the Claude UUID via TopicResumeMap),
85
+ * then respawn via the injected respawner which spawns a fresh tmux that
86
+ * runs `claude --resume <uuid>` — picking up newly installed MCPs/skills
87
+ * while preserving the full conversation.
88
+ *
89
+ * Returns a structured result; never throws on the expected failure modes
90
+ * (rate-limit, session-not-found, non-Telegram-bound). Throws only on
91
+ * unexpected internal errors from the respawner callback.
92
+ */
93
+ refreshSession(opts: RefreshOptions): Promise<RefreshResult>;
94
+ /**
95
+ * Returns true if a fresh refresh is allowed under the rolling window cap.
96
+ * Pure read of the recent-refresh ledger; does not mutate.
97
+ */
98
+ private checkRateLimit;
99
+ /** Append a timestamp to the rolling window for this session. */
100
+ private recordRefresh;
101
+ /**
102
+ * Structured log when the rate guard blocks a request. Logged at warn
103
+ * level so over-blocks are detectable in operations (per
104
+ * docs/signal-vs-authority.md "Authorities must log their decisions").
105
+ */
106
+ private logRateLimit;
107
+ }
108
+ //# sourceMappingURL=SessionRefresh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionRefresh.d.ts","sourceRoot":"","sources":["../../src/core/SessionRefresh.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,MAAM,WAAW,kBAAkB;IACjC,cAAc,EAAE,cAAc,CAAC;IAC/B,KAAK,EAAE,YAAY,CAAC;IACpB,QAAQ,EAAE,eAAe,GAAG,IAAI,CAAC;IACjC,cAAc,EAAE,cAAc,GAAG,IAAI,CAAC;IACtC;;;;;;;;;;;;;;OAcG;IACH,SAAS,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,SAAS,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACzG,6EAA6E;IAC7E,SAAS,CAAC,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACvD,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;CACtB;AAED,MAAM,MAAM,aAAa,GACrB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,cAAc,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACrD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,kBAAkB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7D,MAAM,MAAM,kBAAkB,GAC1B,cAAc,GACd,mBAAmB,GACnB,oBAAoB,GACpB,qBAAqB,GACrB,qBAAqB,CAAC;AAE1B,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAKD,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAqB;IAC1C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAoC;IACpE;;oDAEgD;IAChD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA0B;gBAEvC,IAAI,EAAE,kBAAkB;IAOpC;;;;;;;;;;OAUG;IACG,cAAc,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IAoFlE;;;OAGG;IACH,OAAO,CAAC,cAAc;IAWtB,iEAAiE;IACjE,OAAO,CAAC,aAAa;IAOrB;;;;OAIG;IACH,OAAO,CAAC,YAAY;CAKrB"}
@@ -0,0 +1,157 @@
1
+ /**
2
+ * SessionRefresh — orchestrates an agent-initiated session respawn.
3
+ *
4
+ * When an agent installs a new MCP server or skill mid-session, Claude Code
5
+ * only attaches the new tools at session start. The agent triggers this class
6
+ * to kill its current tmux session and respawn it with `claude --resume <uuid>`,
7
+ * which loads the freshly installed tools while preserving full conversation
8
+ * state.
9
+ *
10
+ * The respawn lifecycle is owned end-to-end here:
11
+ * detect — sanity-check session, topic binding, resume UUID exist
12
+ * attempt — apply rate guard, persist resume UUID, kill tmux, respawn
13
+ * verify — new session is registered for the topic
14
+ * finalize — return structured result
15
+ *
16
+ * Rate guard is a structural rate-counter (allowed-detector category per
17
+ * docs/signal-vs-authority.md "safety guards on irreversible actions"
18
+ * carve-out) — prevents infinite-respawn loops. Not a judgment call.
19
+ *
20
+ * v1 scope: Telegram-bound sessions only. Non-Telegram-bound respawn is a
21
+ * v2 follow-up — returns { ok: false, code: 'not_telegram_bound' } for now.
22
+ */
23
+ const DEFAULT_MAX_PER_WINDOW = 5;
24
+ const DEFAULT_WINDOW_MS = 10 * 60 * 1000;
25
+ export class SessionRefresh {
26
+ deps;
27
+ maxPerWindow;
28
+ windowMs;
29
+ clock;
30
+ recentRefreshes = new Map();
31
+ /** In-flight refresh guard — prevents the race where a second call
32
+ * fires before the first's kill+spawn completes, which would spawn
33
+ * two parallel sessions for the same topic. */
34
+ inFlight = new Set();
35
+ constructor(deps) {
36
+ this.deps = deps;
37
+ this.maxPerWindow = deps.rateLimit?.maxPerWindow ?? DEFAULT_MAX_PER_WINDOW;
38
+ this.windowMs = deps.rateLimit?.windowMs ?? DEFAULT_WINDOW_MS;
39
+ this.clock = deps.clock ?? Date.now;
40
+ }
41
+ /**
42
+ * Refresh a session: kill its tmux session (which fires beforeSessionKill
43
+ * so the existing listener persists the Claude UUID via TopicResumeMap),
44
+ * then respawn via the injected respawner which spawns a fresh tmux that
45
+ * runs `claude --resume <uuid>` — picking up newly installed MCPs/skills
46
+ * while preserving the full conversation.
47
+ *
48
+ * Returns a structured result; never throws on the expected failure modes
49
+ * (rate-limit, session-not-found, non-Telegram-bound). Throws only on
50
+ * unexpected internal errors from the respawner callback.
51
+ */
52
+ async refreshSession(opts) {
53
+ const { sessionName, followUpPrompt, reason } = opts;
54
+ // ── detect ─────────────────────────────────────────────────────────
55
+ if (!this.deps.telegram) {
56
+ return {
57
+ ok: false,
58
+ code: 'no_telegram_adapter',
59
+ message: 'No Telegram adapter wired — self-refresh requires a Telegram-bound session in v1.',
60
+ };
61
+ }
62
+ const topicId = this.deps.telegram.getTopicForSession(sessionName);
63
+ if (topicId === null) {
64
+ // TODO(v2): handle non-Telegram-bound sessions (Slack, iMessage,
65
+ // headless). Today only Telegram-bound is supported because the
66
+ // respawn path is built around topicId → context routing.
67
+ return {
68
+ ok: false,
69
+ code: 'not_telegram_bound',
70
+ message: `Session "${sessionName}" is not bound to a Telegram topic; cannot self-refresh in v1.`,
71
+ };
72
+ }
73
+ // Look up the state session by tmux name — needed for killSession,
74
+ // which takes the state session id, not the tmux session name.
75
+ const stateSession = this.deps.state.listSessions({ status: 'running' })
76
+ .find(s => s.tmuxSession === sessionName);
77
+ if (!stateSession) {
78
+ return {
79
+ ok: false,
80
+ code: 'session_not_found',
81
+ message: `No running session found for tmux name "${sessionName}".`,
82
+ };
83
+ }
84
+ // ── in-flight guard ────────────────────────────────────────────────
85
+ if (this.inFlight.has(sessionName)) {
86
+ return {
87
+ ok: false,
88
+ code: 'refresh_in_progress',
89
+ message: `A refresh is already in progress for "${sessionName}".`,
90
+ };
91
+ }
92
+ // ── rate guard ─────────────────────────────────────────────────────
93
+ if (!this.checkRateLimit(sessionName)) {
94
+ this.logRateLimit(sessionName, reason);
95
+ return {
96
+ ok: false,
97
+ code: 'rate_limited',
98
+ message: `Refresh rate limit exceeded (${this.maxPerWindow} per ${Math.round(this.windowMs / 60000)} minutes) for session "${sessionName}".`,
99
+ };
100
+ }
101
+ // ── attempt ────────────────────────────────────────────────────────
102
+ // Record the attempt against the rate guard window BEFORE the work —
103
+ // we count attempts, not successes, so a flapping respawner can't
104
+ // bypass the cap. Also mark in-flight so a parallel call refuses.
105
+ this.recordRefresh(sessionName);
106
+ this.inFlight.add(sessionName);
107
+ try {
108
+ // Kill via sessionManager so the beforeSessionKill listener fires
109
+ // and persists the resume UUID using session.claudeSessionId. This
110
+ // replaces the previous SessionRefresh-side findUuidForSession+save
111
+ // dance, which would silently no-op (findUuidForSession requires
112
+ // claudeSessionId as second arg; without it, the mtime fallback was
113
+ // removed deliberately and the call returns null).
114
+ this.deps.sessionManager.killSession(stateSession.id);
115
+ // The respawner spawns a fresh tmux session that runs `claude
116
+ // --resume <uuid>` (resolved by spawnSessionForTopic via the saved
117
+ // TopicResumeMap entry) and re-registers the topic mapping.
118
+ const newSessionName = await this.deps.respawner(sessionName, topicId, followUpPrompt);
119
+ // ── verify + finalize ────────────────────────────────────────────
120
+ console.log(`[SessionRefresh] Refreshed "${sessionName}" → "${newSessionName}" (topic ${topicId})${reason ? ` reason="${reason}"` : ''}`);
121
+ return { ok: true, newSessionName, topicId };
122
+ }
123
+ finally {
124
+ this.inFlight.delete(sessionName);
125
+ }
126
+ }
127
+ /**
128
+ * Returns true if a fresh refresh is allowed under the rolling window cap.
129
+ * Pure read of the recent-refresh ledger; does not mutate.
130
+ */
131
+ checkRateLimit(sessionName) {
132
+ const now = this.clock();
133
+ const recent = this.recentRefreshes.get(sessionName) ?? [];
134
+ const fresh = recent.filter(ts => now - ts < this.windowMs);
135
+ if (fresh.length !== recent.length) {
136
+ // Prune stale entries opportunistically.
137
+ this.recentRefreshes.set(sessionName, fresh);
138
+ }
139
+ return fresh.length < this.maxPerWindow;
140
+ }
141
+ /** Append a timestamp to the rolling window for this session. */
142
+ recordRefresh(sessionName) {
143
+ const now = this.clock();
144
+ const recent = this.recentRefreshes.get(sessionName) ?? [];
145
+ recent.push(now);
146
+ this.recentRefreshes.set(sessionName, recent);
147
+ }
148
+ /**
149
+ * Structured log when the rate guard blocks a request. Logged at warn
150
+ * level so over-blocks are detectable in operations (per
151
+ * docs/signal-vs-authority.md "Authorities must log their decisions").
152
+ */
153
+ logRateLimit(sessionName, reason) {
154
+ console.warn(`[SessionRefresh] rate_limited sessionName=${sessionName} window=${this.windowMs}ms cap=${this.maxPerWindow}${reason ? ` reason=${reason}` : ''}`);
155
+ }
156
+ }
157
+ //# sourceMappingURL=SessionRefresh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SessionRefresh.js","sourceRoot":"","sources":["../../src/core/SessionRefresh.ts"],"names":[],"mappings":"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"}
@@ -56,9 +56,26 @@ export interface Commitment {
56
56
  /** For behavioral/one-time: when this commitment expires (null = forever) */
57
57
  expiresAt?: string;
58
58
  /** For one-time-action: verification method */
59
- verificationMethod?: 'config-value' | 'file-exists' | 'manual';
59
+ verificationMethod?: 'config-value' | 'file-exists' | 'manual' | 'threadline-reply';
60
60
  /** For one-time-action with file-exists: the path to check */
61
61
  verificationPath?: string;
62
+ /**
63
+ * For verificationMethod === 'threadline-reply': the thread we're waiting on.
64
+ * Resolved externally by ThreadlineRouter when the awaited reply arrives —
65
+ * the periodic verify sweep is a no-op for this method.
66
+ * Per THREAD-TOPIC-LINKAGE-SPEC.md.
67
+ */
68
+ relatedThreadId?: string;
69
+ /** For verificationMethod === 'threadline-reply': the remote agent. */
70
+ relatedAgent?: string;
71
+ /**
72
+ * For verificationMethod === 'threadline-reply': ISO timestamp of the most
73
+ * recently delivered reply on this thread. Used by the salience gate's
74
+ * first-reply detection (a thread is "first contact" iff no `lastReplyAt`
75
+ * has been recorded yet). Distinct from `heartbeatCount`, which counts
76
+ * beacon emissions, not reply arrivals.
77
+ */
78
+ lastReplyAt?: string;
62
79
  /** Number of times auto-correction has fired for this commitment */
63
80
  correctionCount: number;
64
81
  /** Timestamps of recent corrections (for pattern detection) */
@@ -167,6 +184,13 @@ export declare class CommitmentTracker extends EventEmitter {
167
184
  */
168
185
  private mutateQueues;
169
186
  private mutateRunning;
187
+ /**
188
+ * Bounded thread→commitment index for fast O(1) findByThreadId lookups.
189
+ * Keyed by relatedThreadId, value is the commitment id. Only includes
190
+ * threadline-reply commitments in non-terminal states. Rebuilt on load,
191
+ * maintained on every mutate.
192
+ */
193
+ private threadIdIndex;
170
194
  constructor(config: CommitmentTrackerConfig);
171
195
  /**
172
196
  * One-shot migration: any pre-existing one-time-action that is stuck
@@ -192,8 +216,10 @@ export declare class CommitmentTracker extends EventEmitter {
192
216
  configExpectedValue?: unknown;
193
217
  behavioralRule?: string;
194
218
  expiresAt?: string;
195
- verificationMethod?: 'config-value' | 'file-exists' | 'manual';
219
+ verificationMethod?: 'config-value' | 'file-exists' | 'manual' | 'threadline-reply';
196
220
  verificationPath?: string;
221
+ relatedThreadId?: string;
222
+ relatedAgent?: string;
197
223
  beaconEnabled?: boolean;
198
224
  cadenceMs?: number;
199
225
  nextUpdateDueAt?: string;
@@ -214,6 +240,43 @@ export declare class CommitmentTracker extends EventEmitter {
214
240
  * heartbeats, no more verify attempts.
215
241
  */
216
242
  deliver(id: string, deliveryMessageId?: string): Commitment | null;
243
+ /**
244
+ * Record that a reply arrived on a threadline-reply commitment. Updates
245
+ * `lastReplyAt` without transitioning status. Used by the salience gate's
246
+ * first-reply detection — `heartbeatCount` is incremented by PromiseBeacon
247
+ * for "still waiting" emissions, not by reply arrivals, so we need a
248
+ * separate signal here.
249
+ *
250
+ * No-op on missing commitment or wrong type. Goes through mutateSync so
251
+ * concurrent writes serialize cleanly.
252
+ */
253
+ markReplyArrived(id: string, replyAt?: string): Commitment | null;
254
+ /**
255
+ * Find the active threadline-reply commitment for a given threadId.
256
+ *
257
+ * Used by ThreadlineRouter when an inbound reply arrives: looks up the
258
+ * commitment so the router can transition it to `delivered` and pull the
259
+ * stated purpose / topicId. Skips terminal states (delivered/expired/
260
+ * withdrawn) so late replies don't reopen closed commitments. Returns
261
+ * null on miss.
262
+ *
263
+ * Uses a thread→commitment-id index maintained alongside the store so the
264
+ * lookup is O(1) plus one O(commitment) status check, bounded by active
265
+ * threadline-reply commitments (not lifetime commitment count). Per
266
+ * scalability review F1 — the lifetime store can grow unboundedly while
267
+ * the index stays bounded by active threads.
268
+ */
269
+ findByThreadId(threadId: string): Commitment | null;
270
+ /**
271
+ * Maintain the threadId→commitmentId index. Called after any mutation that
272
+ * affects a threadline-reply commitment's state. Bounded by active count.
273
+ */
274
+ private refreshThreadIdIndex;
275
+ /**
276
+ * Build the threadId→commitmentId index from the persisted store. Called
277
+ * once at startup. Linear in store size, but only runs at load time.
278
+ */
279
+ private rebuildThreadIdIndex;
217
280
  /**
218
281
  * Get all active commitments (pending or verified, not expired).
219
282
  *
@@ -1 +1 @@
1
- {"version":3,"file":"CommitmentTracker.d.ts","sourceRoot":"","sources":["../../src/monitoring/CommitmentTracker.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"CommitmentTracker.d.ts","sourceRoot":"","sources":["../../src/monitoring/CommitmentTracker.ts"],"names":[],"mappings":"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"}
@@ -41,6 +41,13 @@ export class CommitmentTracker extends EventEmitter {
41
41
  */
42
42
  mutateQueues = new Map();
43
43
  mutateRunning = new Set();
44
+ /**
45
+ * Bounded thread→commitment index for fast O(1) findByThreadId lookups.
46
+ * Keyed by relatedThreadId, value is the commitment id. Only includes
47
+ * threadline-reply commitments in non-terminal states. Rebuilt on load,
48
+ * maintained on every mutate.
49
+ */
50
+ threadIdIndex = new Map();
44
51
  constructor(config) {
45
52
  super();
46
53
  this.config = config;
@@ -49,6 +56,7 @@ export class CommitmentTracker extends EventEmitter {
49
56
  this.store = this.loadStore();
50
57
  this.nextId = this.computeNextId();
51
58
  this.backfillUnverifiableOneTimeActions();
59
+ this.rebuildThreadIdIndex();
52
60
  }
53
61
  /**
54
62
  * One-shot migration: any pre-existing one-time-action that is stuck
@@ -151,6 +159,8 @@ export class CommitmentTracker extends EventEmitter {
151
159
  expiresAt: input.expiresAt,
152
160
  verificationMethod: input.verificationMethod,
153
161
  verificationPath: input.verificationPath,
162
+ relatedThreadId: input.relatedThreadId,
163
+ relatedAgent: input.relatedAgent,
154
164
  correctionCount: 0,
155
165
  correctionHistory: [],
156
166
  escalated: false,
@@ -174,6 +184,7 @@ export class CommitmentTracker extends EventEmitter {
174
184
  if (input.type === 'behavioral') {
175
185
  this.writeBehavioralRules();
176
186
  }
187
+ this.refreshThreadIdIndex(commitment);
177
188
  console.log(`[CommitmentTracker] Recorded ${id}: "${input.userRequest}" (${input.type})`);
178
189
  this.emit('recorded', commitment);
179
190
  // Run immediate verification for config-change commitments.
@@ -202,6 +213,7 @@ export class CommitmentTracker extends EventEmitter {
202
213
  if (updated.type === 'behavioral') {
203
214
  this.writeBehavioralRules();
204
215
  }
216
+ this.refreshThreadIdIndex(updated);
205
217
  console.log(`[CommitmentTracker] Withdrawn ${id}: ${reason}`);
206
218
  this.emit('withdrawn', updated);
207
219
  return true;
@@ -225,10 +237,97 @@ export class CommitmentTracker extends EventEmitter {
225
237
  resolvedAt: new Date().toISOString(),
226
238
  deliveryMessageId: deliveryMessageId ?? c.deliveryMessageId,
227
239
  }));
240
+ this.refreshThreadIdIndex(updated);
228
241
  console.log(`[CommitmentTracker] Delivered ${id}`);
229
242
  this.emit('delivered', updated);
230
243
  return updated;
231
244
  }
245
+ /**
246
+ * Record that a reply arrived on a threadline-reply commitment. Updates
247
+ * `lastReplyAt` without transitioning status. Used by the salience gate's
248
+ * first-reply detection — `heartbeatCount` is incremented by PromiseBeacon
249
+ * for "still waiting" emissions, not by reply arrivals, so we need a
250
+ * separate signal here.
251
+ *
252
+ * No-op on missing commitment or wrong type. Goes through mutateSync so
253
+ * concurrent writes serialize cleanly.
254
+ */
255
+ markReplyArrived(id, replyAt) {
256
+ const existing = this.store.commitments.find(c => c.id === id);
257
+ if (!existing)
258
+ return null;
259
+ if (existing.type !== 'one-time-action')
260
+ return null;
261
+ if (existing.verificationMethod !== 'threadline-reply')
262
+ return null;
263
+ return this.mutateSync(id, c => ({
264
+ ...c,
265
+ lastReplyAt: replyAt ?? new Date().toISOString(),
266
+ }));
267
+ }
268
+ /**
269
+ * Find the active threadline-reply commitment for a given threadId.
270
+ *
271
+ * Used by ThreadlineRouter when an inbound reply arrives: looks up the
272
+ * commitment so the router can transition it to `delivered` and pull the
273
+ * stated purpose / topicId. Skips terminal states (delivered/expired/
274
+ * withdrawn) so late replies don't reopen closed commitments. Returns
275
+ * null on miss.
276
+ *
277
+ * Uses a thread→commitment-id index maintained alongside the store so the
278
+ * lookup is O(1) plus one O(commitment) status check, bounded by active
279
+ * threadline-reply commitments (not lifetime commitment count). Per
280
+ * scalability review F1 — the lifetime store can grow unboundedly while
281
+ * the index stays bounded by active threads.
282
+ */
283
+ findByThreadId(threadId) {
284
+ if (!threadId)
285
+ return null;
286
+ const idxId = this.threadIdIndex.get(threadId);
287
+ if (idxId) {
288
+ const c = this.store.commitments.find(x => x.id === idxId);
289
+ if (c &&
290
+ c.type === 'one-time-action' &&
291
+ c.verificationMethod === 'threadline-reply' &&
292
+ c.status !== 'delivered' &&
293
+ c.status !== 'withdrawn' &&
294
+ c.status !== 'expired') {
295
+ return c;
296
+ }
297
+ // Stale index entry — drop it so the next call doesn't re-fetch.
298
+ this.threadIdIndex.delete(threadId);
299
+ }
300
+ return null;
301
+ }
302
+ /**
303
+ * Maintain the threadId→commitmentId index. Called after any mutation that
304
+ * affects a threadline-reply commitment's state. Bounded by active count.
305
+ */
306
+ refreshThreadIdIndex(commitment) {
307
+ if (commitment.type !== 'one-time-action' ||
308
+ commitment.verificationMethod !== 'threadline-reply' ||
309
+ !commitment.relatedThreadId) {
310
+ return;
311
+ }
312
+ if (commitment.status === 'delivered' ||
313
+ commitment.status === 'withdrawn' ||
314
+ commitment.status === 'expired') {
315
+ this.threadIdIndex.delete(commitment.relatedThreadId);
316
+ }
317
+ else {
318
+ this.threadIdIndex.set(commitment.relatedThreadId, commitment.id);
319
+ }
320
+ }
321
+ /**
322
+ * Build the threadId→commitmentId index from the persisted store. Called
323
+ * once at startup. Linear in store size, but only runs at load time.
324
+ */
325
+ rebuildThreadIdIndex() {
326
+ this.threadIdIndex.clear();
327
+ for (const c of this.store.commitments) {
328
+ this.refreshThreadIdIndex(c);
329
+ }
330
+ }
232
331
  /**
233
332
  * Get all active commitments (pending or verified, not expired).
234
333
  *
@@ -382,6 +481,15 @@ export class CommitmentTracker extends EventEmitter {
382
481
  commitment.status === 'expired' ||
383
482
  commitment.status === 'delivered')
384
483
  return null;
484
+ // Threadline-reply commitments are externally resolved by ThreadlineRouter
485
+ // when the awaited reply arrives. The periodic verify sweep is a no-op —
486
+ // we neither increment violations nor mark delivered. Per
487
+ // THREAD-TOPIC-LINKAGE-SPEC.md §4.1. The `expiresAt` handler still applies
488
+ // and will mark the commitment `expired` if no reply arrives within window.
489
+ if (commitment.type === 'one-time-action' &&
490
+ commitment.verificationMethod === 'threadline-reply') {
491
+ return { passed: false, detail: 'Awaiting threadline reply' };
492
+ }
385
493
  // One-time-actions with no automated verification path cannot keep
386
494
  // being "violated" on every sweep — that's how a single commitment
387
495
  // accumulated 51,000+ violation ticks. Transition once to
@@ -554,6 +662,11 @@ export class CommitmentTracker extends EventEmitter {
554
662
  case 'manual':
555
663
  // Manual commitments stay pending until explicitly resolved
556
664
  return { passed: false, detail: 'Awaiting manual verification' };
665
+ case 'threadline-reply':
666
+ // Externally resolved by ThreadlineRouter. verifyOne() short-circuits
667
+ // before reaching this switch (see verifyOne); included here as a
668
+ // belt-and-braces fallback so the `default` branch can't be hit.
669
+ return { passed: false, detail: 'Awaiting threadline reply' };
557
670
  default:
558
671
  return { passed: false, detail: `Unknown verification method: ${commitment.verificationMethod}` };
559
672
  }