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.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +91 -17
- package/dist/commands/server.js.map +1 -1
- package/dist/core/SessionRefresh.d.ts +108 -0
- package/dist/core/SessionRefresh.d.ts.map +1 -0
- package/dist/core/SessionRefresh.js +157 -0
- package/dist/core/SessionRefresh.js.map +1 -0
- package/dist/monitoring/CommitmentTracker.d.ts +65 -2
- package/dist/monitoring/CommitmentTracker.d.ts.map +1 -1
- package/dist/monitoring/CommitmentTracker.js +113 -0
- package/dist/monitoring/CommitmentTracker.js.map +1 -1
- package/dist/server/AgentServer.d.ts +6 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +3 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +10 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +92 -1
- package/dist/server/routes.js.map +1 -1
- package/dist/threadline/SalienceGate.d.ts +87 -0
- package/dist/threadline/SalienceGate.d.ts.map +1 -0
- package/dist/threadline/SalienceGate.js +100 -0
- package/dist/threadline/SalienceGate.js.map +1 -0
- package/dist/threadline/ThreadResumeMap.d.ts +14 -0
- package/dist/threadline/ThreadResumeMap.d.ts.map +1 -1
- package/dist/threadline/ThreadResumeMap.js.map +1 -1
- package/dist/threadline/ThreadlineMCPServer.d.ts +4 -0
- package/dist/threadline/ThreadlineMCPServer.d.ts.map +1 -1
- package/dist/threadline/ThreadlineMCPServer.js +10 -0
- package/dist/threadline/ThreadlineMCPServer.js.map +1 -1
- package/dist/threadline/ThreadlineRouter.d.ts +13 -0
- package/dist/threadline/ThreadlineRouter.d.ts.map +1 -1
- package/dist/threadline/ThreadlineRouter.js +47 -0
- package/dist/threadline/ThreadlineRouter.js.map +1 -1
- package/dist/threadline/TopicLinkageHandler.d.ts +161 -0
- package/dist/threadline/TopicLinkageHandler.d.ts.map +1 -0
- package/dist/threadline/TopicLinkageHandler.js +518 -0
- package/dist/threadline/TopicLinkageHandler.js.map +1 -0
- package/dist/threadline/mcp-stdio-entry.js +5 -0
- package/dist/threadline/mcp-stdio-entry.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +47 -47
- package/upgrades/0.28.89.md +68 -0
- package/upgrades/0.28.90.md +52 -0
- package/upgrades/side-effects/session-self-refresh.md +382 -0
- 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":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAI3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAMxD,MAAM,MAAM,cAAc,GAAG,eAAe,GAAG,YAAY,GAAG,iBAAiB,CAAC;AAChF,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,CAAC;AAE3G,MAAM,WAAW,UAAU;IACzB,kCAAkC;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,kCAAkC;IAClC,aAAa,EAAE,MAAM,CAAC;IACtB,uDAAuD;IACvD,IAAI,EAAE,cAAc,CAAC;IACrB,qBAAqB;IACrB,MAAM,EAAE,gBAAgB,CAAC;IACzB,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oDAAoD;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yBAAyB;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,oCAAoC;IACpC,cAAc,EAAE,MAAM,CAAC;IACvB,sDAAsD;IACtD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gFAAgF;IAChF,MAAM,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAC;IAIzC,4DAA4D;IAC5D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAE9B,2DAA2D;IAC3D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,+CAA+C;IAC/C,kBAAkB,CAAC,EAAE,cAAc,GAAG,aAAa,GAAG,QAAQ,CAAC;
|
|
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
|
}
|