@myagentroam/node 0.1.0

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 (68) hide show
  1. package/README.md +17 -0
  2. package/dist/capabilities.d.ts +5 -0
  3. package/dist/capabilities.js +23 -0
  4. package/dist/capabilities.js.map +1 -0
  5. package/dist/claude-agent-sdk.d.ts +101 -0
  6. package/dist/claude-agent-sdk.js +341 -0
  7. package/dist/claude-agent-sdk.js.map +1 -0
  8. package/dist/claude-channel.d.ts +28 -0
  9. package/dist/claude-channel.js +45 -0
  10. package/dist/claude-channel.js.map +1 -0
  11. package/dist/codex-app-server.d.ts +96 -0
  12. package/dist/codex-app-server.js +361 -0
  13. package/dist/codex-app-server.js.map +1 -0
  14. package/dist/config.d.ts +17 -0
  15. package/dist/config.js +51 -0
  16. package/dist/config.js.map +1 -0
  17. package/dist/connector.d.ts +343 -0
  18. package/dist/connector.js +5525 -0
  19. package/dist/connector.js.map +1 -0
  20. package/dist/database.d.ts +282 -0
  21. package/dist/database.js +1347 -0
  22. package/dist/database.js.map +1 -0
  23. package/dist/event-buffer.d.ts +12 -0
  24. package/dist/event-buffer.js +29 -0
  25. package/dist/event-buffer.js.map +1 -0
  26. package/dist/fake-runner.d.ts +42 -0
  27. package/dist/fake-runner.js +130 -0
  28. package/dist/fake-runner.js.map +1 -0
  29. package/dist/health.d.ts +4 -0
  30. package/dist/health.js +4 -0
  31. package/dist/health.js.map +1 -0
  32. package/dist/main.d.ts +2 -0
  33. package/dist/main.js +47 -0
  34. package/dist/main.js.map +1 -0
  35. package/dist/native-session-history.d.ts +72 -0
  36. package/dist/native-session-history.js +647 -0
  37. package/dist/native-session-history.js.map +1 -0
  38. package/dist/operational.d.ts +30 -0
  39. package/dist/operational.js +82 -0
  40. package/dist/operational.js.map +1 -0
  41. package/dist/process-tree.d.ts +9 -0
  42. package/dist/process-tree.js +19 -0
  43. package/dist/process-tree.js.map +1 -0
  44. package/dist/runner-command-engine.d.ts +19 -0
  45. package/dist/runner-command-engine.js +47 -0
  46. package/dist/runner-command-engine.js.map +1 -0
  47. package/dist/runner-profiles.d.ts +11 -0
  48. package/dist/runner-profiles.js +138 -0
  49. package/dist/runner-profiles.js.map +1 -0
  50. package/dist/runner-usage.d.ts +33 -0
  51. package/dist/runner-usage.js +193 -0
  52. package/dist/runner-usage.js.map +1 -0
  53. package/dist/runtime-state.d.ts +174 -0
  54. package/dist/runtime-state.js +957 -0
  55. package/dist/runtime-state.js.map +1 -0
  56. package/dist/service.d.ts +4 -0
  57. package/dist/service.js +39 -0
  58. package/dist/service.js.map +1 -0
  59. package/dist/storage.d.ts +2 -0
  60. package/dist/storage.js +33 -0
  61. package/dist/storage.js.map +1 -0
  62. package/dist/terminal.d.ts +132 -0
  63. package/dist/terminal.js +417 -0
  64. package/dist/terminal.js.map +1 -0
  65. package/dist/workspace.d.ts +116 -0
  66. package/dist/workspace.js +732 -0
  67. package/dist/workspace.js.map +1 -0
  68. package/package.json +36 -0
@@ -0,0 +1,957 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ /**
3
+ * Deliberately process-local runtime domain. No method in this class writes
4
+ * into Node SQLite: a Node restart loses active runs, approvals, leases and
5
+ * managed-session aliases by design. Durable conversation history belongs to
6
+ * the Runner/native transcript, never to MAR.
7
+ */
8
+ export class NodeRuntimeState {
9
+ nodeId;
10
+ getWorkspace;
11
+ now;
12
+ sessions = new Map();
13
+ runs = new Map();
14
+ messageRuns = new Map();
15
+ events = new Map();
16
+ turns = new Map();
17
+ approvals = new Map();
18
+ workspaceEvents = new Map();
19
+ /** A Session owns at most one running turn; Workspace sessions may run concurrently. */
20
+ leases = new Map();
21
+ /** Session FIFO metadata is deliberately process-local with the rest of run state. */
22
+ queuedRuns = new Map();
23
+ queueVersions = new Map();
24
+ pausedQueues = new Set();
25
+ snapshots = new Map();
26
+ nextEventId = 1;
27
+ nextWorkspaceEventId = 1;
28
+ constructor(nodeId, getWorkspace, now = Date.now) {
29
+ this.nodeId = nodeId;
30
+ this.getWorkspace = getWorkspace;
31
+ this.now = now;
32
+ }
33
+ activeWorkspaceLeaseCount() {
34
+ return this.leases.size;
35
+ }
36
+ activeSessionRun(sessionId) {
37
+ const runId = this.leases.get(sessionId);
38
+ return runId === undefined ? undefined : this.runs.get(runId);
39
+ }
40
+ hasActiveSessionLease(sessionId) {
41
+ return this.leases.has(sessionId);
42
+ }
43
+ createAgentSession(input) {
44
+ if (this.getWorkspace(input.workspaceId) === undefined)
45
+ throw new Error('WORKSPACE_NOT_FOUND');
46
+ const now = this.now();
47
+ const id = input.externalSessionId ?? randomUUID();
48
+ if (this.sessions.has(id))
49
+ throw new Error('SESSION_ID_CONFLICT');
50
+ const value = {
51
+ id,
52
+ nodeId: this.nodeId(),
53
+ workspaceId: input.workspaceId,
54
+ runner: input.runner,
55
+ externalSessionId: input.externalSessionId ?? null,
56
+ nativeControl: input.externalSessionId === undefined ? 'MAR_MANAGED' : (input.nativeControl ?? 'EXTERNAL'),
57
+ channelToken: null,
58
+ cwd: input.cwd,
59
+ model: input.model ?? null,
60
+ effort: input.effort ?? null,
61
+ access: input.access ?? null,
62
+ customTitle: null,
63
+ runnerTitle: input.title?.trim().slice(0, 160) || null,
64
+ pinnedAt: null,
65
+ // A first user prompt is only a process-local temporary title. A
66
+ // Runner/native title can replace it on the next discovery refresh.
67
+ titleSource: 'AUTO',
68
+ lastActivityAt: now,
69
+ createdAt: now
70
+ };
71
+ this.sessions.set(value.id, value);
72
+ return value;
73
+ }
74
+ /** Internal envelope compatibility for a Run already accepted in this process. */
75
+ adoptRun(input) {
76
+ let session = this.sessions.get(input.sessionId);
77
+ if (session === undefined) {
78
+ const now = this.now();
79
+ session = {
80
+ id: input.sessionId,
81
+ nodeId: this.nodeId(),
82
+ workspaceId: input.workspaceId ?? `runtime:${input.cwd}`,
83
+ runner: input.runner,
84
+ externalSessionId: input.externalSessionId ?? null,
85
+ nativeControl: input.externalSessionId === undefined ? 'MAR_MANAGED' : 'EXTERNAL',
86
+ channelToken: null,
87
+ cwd: input.cwd,
88
+ model: null,
89
+ effort: null,
90
+ access: null,
91
+ customTitle: null,
92
+ runnerTitle: null,
93
+ pinnedAt: null,
94
+ titleSource: 'AUTO',
95
+ lastActivityAt: now,
96
+ createdAt: now
97
+ };
98
+ this.sessions.set(session.id, session);
99
+ }
100
+ const existing = this.runs.get(input.runId);
101
+ if (existing !== undefined)
102
+ return existing;
103
+ const now = this.now();
104
+ const run = {
105
+ id: input.runId,
106
+ sessionId: session.id,
107
+ nodeId: this.nodeId(),
108
+ workspaceId: session.workspaceId,
109
+ runner: session.runner,
110
+ status: 'QUEUED',
111
+ recoveryState: 'ACTIVE',
112
+ version: 0,
113
+ clientMessageId: input.clientMessageId ?? input.runId,
114
+ createdAt: now,
115
+ updatedAt: now
116
+ };
117
+ this.runs.set(run.id, run);
118
+ this.leases.set(run.sessionId, run.id);
119
+ return run;
120
+ }
121
+ getAgentSession(id) {
122
+ return (this.sessions.get(id) ??
123
+ [...this.sessions.values()].find((value) => value.externalSessionId === id));
124
+ }
125
+ listAgentSessions() {
126
+ return [...this.sessions.values()].sort((a, b) => b.lastActivityAt - a.lastActivityAt);
127
+ }
128
+ findAgentSessionByExternalSession(runner, externalSessionId) {
129
+ return [...this.sessions.values()].find((value) => value.runner === runner && value.externalSessionId === externalSessionId);
130
+ }
131
+ updateAgentSessionConfiguration(input) {
132
+ return this.replaceSession(input.sessionId, {
133
+ model: input.model,
134
+ effort: input.effort,
135
+ access: input.access
136
+ });
137
+ }
138
+ setNativeControl(id, nativeControl) {
139
+ this.replaceSession(id, { nativeControl });
140
+ }
141
+ setExternalSessionId(id, externalSessionId) {
142
+ if (externalSessionId.length === 0)
143
+ throw new Error('SESSION_EXTERNAL_ID_INVALID');
144
+ this.promoteSessionIdentity(id, externalSessionId);
145
+ }
146
+ /**
147
+ * Atomically replaces the bootstrap Session key with the Runner-native ID.
148
+ * No caller may migrate the individual runtime maps independently.
149
+ */
150
+ promoteSessionIdentity(id, nativeSessionId) {
151
+ if (nativeSessionId.length === 0)
152
+ throw new Error('SESSION_EXTERNAL_ID_INVALID');
153
+ const current = this.requireSession(id);
154
+ if (id === nativeSessionId)
155
+ return this.replaceSession(id, {
156
+ externalSessionId: nativeSessionId,
157
+ nativeControl: 'MAR_MANAGED'
158
+ });
159
+ const collision = this.sessions.get(nativeSessionId);
160
+ if (collision !== undefined && collision.id !== current.id)
161
+ throw new Error('SESSION_ID_CONFLICT');
162
+ const next = {
163
+ ...current,
164
+ id: nativeSessionId,
165
+ externalSessionId: nativeSessionId,
166
+ nativeControl: 'MAR_MANAGED'
167
+ };
168
+ const runs = this.listRunsForSession(id);
169
+ const turns = this.turns.get(id) ?? [];
170
+ const queue = this.queuedRuns.get(id);
171
+ const queueVersion = this.queueVersions.get(id);
172
+ const lease = this.leases.get(id);
173
+ const paused = this.pausedQueues.has(id);
174
+ this.sessions.delete(id);
175
+ this.sessions.set(nativeSessionId, next);
176
+ for (const run of runs)
177
+ this.runs.set(run.id, { ...run, sessionId: nativeSessionId });
178
+ this.turns.delete(id);
179
+ this.turns.set(nativeSessionId, turns.map((turn) => ({
180
+ ...turn,
181
+ sessionId: nativeSessionId,
182
+ items: turn.items.map((item) => ({ ...item, sessionId: nativeSessionId }))
183
+ })));
184
+ if (queue !== undefined) {
185
+ this.queuedRuns.delete(id);
186
+ this.queuedRuns.set(nativeSessionId, queue);
187
+ }
188
+ if (queueVersion !== undefined) {
189
+ this.queueVersions.delete(id);
190
+ this.queueVersions.set(nativeSessionId, queueVersion);
191
+ }
192
+ if (lease !== undefined) {
193
+ this.leases.delete(id);
194
+ this.leases.set(nativeSessionId, lease);
195
+ }
196
+ if (paused) {
197
+ this.pausedQueues.delete(id);
198
+ this.pausedQueues.add(nativeSessionId);
199
+ }
200
+ for (const [key, runId] of [...this.messageRuns]) {
201
+ if (!key.startsWith(`${id}\u0000`))
202
+ continue;
203
+ this.messageRuns.delete(key);
204
+ this.messageRuns.set(`${nativeSessionId}${key.slice(id.length)}`, runId);
205
+ }
206
+ return next;
207
+ }
208
+ setChannelToken(id, channelToken) {
209
+ if (channelToken.length < 16)
210
+ throw new Error('SESSION_CHANNEL_TOKEN_INVALID');
211
+ this.replaceSession(id, { channelToken });
212
+ }
213
+ updateSessionMetadata(input) {
214
+ const current = this.requireSession(input.sessionId);
215
+ const title = input.customTitle === undefined ? current.customTitle : input.customTitle?.trim() || null;
216
+ if (title !== null && title.length > 160)
217
+ throw new Error('SESSION_TITLE_INVALID');
218
+ return this.replaceSession(current.id, {
219
+ customTitle: title,
220
+ pinnedAt: input.pinned === undefined ? current.pinnedAt : input.pinned ? this.now() : null,
221
+ titleSource: title === null ? 'AUTO' : 'CUSTOM'
222
+ });
223
+ }
224
+ /**
225
+ * Give a just-created managed Session a readable local title before the
226
+ * Runner has written its own transcript title. This intentionally never
227
+ * replaces a user rename or a Runner-provided title.
228
+ */
229
+ setTemporaryTitleIfMissing(id, title) {
230
+ const current = this.requireSession(id);
231
+ const value = title.trim().replace(/\s+/g, ' ').slice(0, 160);
232
+ if (value.length === 0 || current.customTitle !== null || current.runnerTitle !== null)
233
+ return current;
234
+ return this.replaceSession(id, { runnerTitle: value, titleSource: 'AUTO' });
235
+ }
236
+ /**
237
+ * Runner/native discovery is authoritative for its own title. Keep it on
238
+ * the managed Session so later `session.get` polls cannot regress to an
239
+ * empty fallback after a discovery response has already shown the title.
240
+ */
241
+ setRunnerTitleFromNative(id, title) {
242
+ const current = this.requireSession(id);
243
+ const value = title.trim().replace(/\s+/g, ' ').slice(0, 160);
244
+ if (value.length === 0 || current.customTitle !== null || current.runnerTitle === value)
245
+ return current;
246
+ return this.replaceSession(id, { runnerTitle: value, titleSource: 'RUNNER' });
247
+ }
248
+ createMessageRun(input) {
249
+ const key = `${input.sessionId}\u0000${input.clientMessageId}`;
250
+ const duplicate = this.messageRuns.get(key);
251
+ if (duplicate !== undefined)
252
+ return { run: this.requireRun(duplicate), created: false };
253
+ const session = this.requireSession(input.sessionId);
254
+ const now = this.now();
255
+ const run = {
256
+ id: randomUUID(),
257
+ sessionId: session.id,
258
+ nodeId: this.nodeId(),
259
+ workspaceId: session.workspaceId,
260
+ runner: session.runner,
261
+ status: 'QUEUED',
262
+ recoveryState: 'ACTIVE',
263
+ version: 0,
264
+ clientMessageId: input.clientMessageId,
265
+ createdAt: now,
266
+ updatedAt: now
267
+ };
268
+ let imageIndex = 0;
269
+ const attachments = (input.attachments ?? []).map(({ name, mime, dataBase64 }) => {
270
+ const attachment = {
271
+ name,
272
+ mime,
273
+ byteSize: Buffer.byteLength(dataBase64, 'base64'),
274
+ runId: run.id
275
+ };
276
+ if (!mime.startsWith('image/'))
277
+ return attachment;
278
+ return { ...attachment, imageIndex: imageIndex++ };
279
+ });
280
+ const user = {
281
+ id: randomUUID(),
282
+ sessionId: session.id,
283
+ turnId: '',
284
+ runId: run.id,
285
+ parentItemId: null,
286
+ sourceSequence: null,
287
+ revision: 0,
288
+ kind: 'user_message',
289
+ status: 'COMPLETED',
290
+ payload: {
291
+ clientMessageId: input.clientMessageId,
292
+ text: input.content,
293
+ contexts: [],
294
+ attachments,
295
+ delivery: 'ACCEPTED'
296
+ },
297
+ startedAt: now,
298
+ completedAt: now
299
+ };
300
+ const turn = {
301
+ id: randomUUID(),
302
+ sessionId: session.id,
303
+ runId: run.id,
304
+ userItemId: user.id,
305
+ status: 'QUEUED',
306
+ model: session.model,
307
+ effort: session.effort,
308
+ access: session.access,
309
+ items: [user],
310
+ startedAt: null,
311
+ completedAt: null
312
+ };
313
+ const fixedTurn = { ...turn, items: [{ ...user, turnId: turn.id }] };
314
+ this.runs.set(run.id, run);
315
+ this.messageRuns.set(key, run.id);
316
+ this.queuedRuns.set(session.id, [...(this.queuedRuns.get(session.id) ?? []), run.id]);
317
+ this.bumpQueueVersion(session.id);
318
+ this.turns.set(session.id, [...(this.turns.get(session.id) ?? []), fixedTurn]);
319
+ this.recordWorkspaceActivity(run.workspaceId, 'run.created', {
320
+ runId: run.id,
321
+ sessionId: session.id,
322
+ runner: run.runner
323
+ });
324
+ this.replaceSession(session.id, { lastActivityAt: now });
325
+ return { run, created: true };
326
+ }
327
+ sessionQueue(sessionId) {
328
+ return {
329
+ version: this.queueVersions.get(sessionId) ?? 0,
330
+ paused: this.pausedQueues.has(sessionId),
331
+ runIds: this.queuedRuns.get(sessionId) ?? []
332
+ };
333
+ }
334
+ isQueuedRun(runId) {
335
+ const run = this.requireRun(runId);
336
+ return (this.queuedRuns.get(run.sessionId) ?? []).includes(runId);
337
+ }
338
+ claimNextQueuedRun(sessionId) {
339
+ if (this.leases.has(sessionId) || this.pausedQueues.has(sessionId))
340
+ return undefined;
341
+ const queue = this.queuedRuns.get(sessionId) ?? [];
342
+ const runId = queue[0];
343
+ if (runId === undefined)
344
+ return undefined;
345
+ this.queuedRuns.set(sessionId, queue.slice(1));
346
+ this.bumpQueueVersion(sessionId);
347
+ const run = this.requireRun(runId);
348
+ this.leases.set(sessionId, run.id);
349
+ return run;
350
+ }
351
+ pauseSessionQueue(sessionId) {
352
+ this.pausedQueues.add(sessionId);
353
+ this.bumpQueueVersion(sessionId);
354
+ }
355
+ resumeSessionQueue(sessionId) {
356
+ if (!this.pausedQueues.delete(sessionId))
357
+ return;
358
+ this.bumpQueueVersion(sessionId);
359
+ }
360
+ reorderSessionQueue(sessionId, expectedVersion, runIds) {
361
+ const current = this.queuedRuns.get(sessionId) ?? [];
362
+ if (this.sessionQueue(sessionId).version !== expectedVersion ||
363
+ current.length !== runIds.length ||
364
+ new Set(current).size !== current.length ||
365
+ current.some((runId) => !runIds.includes(runId)))
366
+ throw new Error('QUEUE_VERSION_CONFLICT');
367
+ if (runIds.some((runId) => this.requireRun(runId).status !== 'QUEUED'))
368
+ throw new Error('QUEUE_ITEM_INVALID');
369
+ this.queuedRuns.set(sessionId, [...runIds]);
370
+ this.bumpQueueVersion(sessionId);
371
+ }
372
+ prioritizeQueuedRun(runId) {
373
+ const run = this.requireRun(runId);
374
+ const queue = this.queuedRuns.get(run.sessionId) ?? [];
375
+ if (!queue.includes(runId) || run.status !== 'QUEUED')
376
+ throw new Error('QUEUE_ITEM_INVALID');
377
+ this.queuedRuns.set(run.sessionId, [
378
+ runId,
379
+ ...queue.filter((candidate) => candidate !== runId)
380
+ ]);
381
+ this.bumpQueueVersion(run.sessionId);
382
+ return run;
383
+ }
384
+ updateQueuedMessage(runId, content) {
385
+ const run = this.requireRun(runId);
386
+ if (!(this.queuedRuns.get(run.sessionId) ?? []).includes(runId) || run.status !== 'QUEUED')
387
+ throw new Error('QUEUE_ITEM_INVALID');
388
+ this.updateTurnForRun(runId, (turn) => ({
389
+ ...turn,
390
+ items: turn.items.map((item) => item.id === turn.userItemId && isRecord(item.payload)
391
+ ? { ...item, payload: { ...item.payload, text: content } }
392
+ : item)
393
+ }));
394
+ return run;
395
+ }
396
+ deleteQueuedRun(runId) {
397
+ const run = this.requireRun(runId);
398
+ const queue = this.queuedRuns.get(run.sessionId) ?? [];
399
+ if (!queue.includes(runId) || run.status !== 'QUEUED')
400
+ throw new Error('QUEUE_ITEM_INVALID');
401
+ this.queuedRuns.set(run.sessionId, queue.filter((candidate) => candidate !== runId));
402
+ this.bumpQueueVersion(run.sessionId);
403
+ this.runs.delete(runId);
404
+ this.events.delete(runId);
405
+ this.snapshots.delete(runId);
406
+ this.turns.set(run.sessionId, (this.turns.get(run.sessionId) ?? []).filter((turn) => turn.runId !== runId));
407
+ for (const [key, candidate] of this.messageRuns)
408
+ if (candidate === runId)
409
+ this.messageRuns.delete(key);
410
+ }
411
+ queuedMessageText(runId) {
412
+ const run = this.requireRun(runId);
413
+ const turn = (this.turns.get(run.sessionId) ?? []).find((candidate) => candidate.runId === runId);
414
+ const item = turn?.items.find((candidate) => candidate.id === turn.userItemId);
415
+ return isRecord(item?.payload) && typeof item.payload.text === 'string'
416
+ ? item.payload.text
417
+ : '';
418
+ }
419
+ getRun(id) {
420
+ return this.runs.get(id);
421
+ }
422
+ findMessageRun(sessionId, clientMessageId) {
423
+ const runId = this.messageRuns.get(`${sessionId}\u0000${clientMessageId}`);
424
+ return runId === undefined ? undefined : this.runs.get(runId);
425
+ }
426
+ listRunsForSession(sessionId) {
427
+ return [...this.runs.values()]
428
+ .filter((run) => run.sessionId === sessionId)
429
+ .sort((a, b) => b.createdAt - a.createdAt);
430
+ }
431
+ transitionRun(id, next) {
432
+ const current = this.requireRun(id);
433
+ if (!transitionAllowed(current.status, next))
434
+ throw new Error('RUN_STATE_INVALID');
435
+ const value = { ...current, status: next, version: current.version + 1, updatedAt: this.now() };
436
+ this.runs.set(id, value);
437
+ if (terminal(next) && this.leases.get(value.sessionId) === value.id)
438
+ this.leases.delete(value.sessionId);
439
+ this.updateTurnForRun(id, (turn) => ({
440
+ ...turn,
441
+ status: next,
442
+ completedAt: terminal(next) ? this.now() : turn.completedAt,
443
+ items: terminal(next)
444
+ ? turn.items.map((item) => activeConversationItem(item.status)
445
+ ? { ...item, status: completedStatusForRun(next), completedAt: this.now() }
446
+ : item)
447
+ : turn.items
448
+ }));
449
+ return value;
450
+ }
451
+ appendRunEvent(input) {
452
+ this.requireRun(input.runId);
453
+ const prior = this.events.get(input.runId) ?? [];
454
+ if (prior.some((event) => event.sequence === input.sequence))
455
+ return undefined;
456
+ if (input.sequence > (prior.at(-1)?.sequence ?? 0) + 1)
457
+ throw new Error('EVENT_SEQUENCE_GAP');
458
+ const value = {
459
+ id: this.nextEventId++,
460
+ runId: input.runId,
461
+ sequence: input.sequence,
462
+ eventType: input.eventType,
463
+ payload: input.payload,
464
+ ...(input.status === undefined ? {} : { status: input.status }),
465
+ createdAt: this.now()
466
+ };
467
+ this.events.set(input.runId, [...prior, value]);
468
+ if (input.status !== undefined)
469
+ this.transitionRun(input.runId, input.status);
470
+ this.projectEvent(value);
471
+ const run = this.requireRun(input.runId);
472
+ this.recordWorkspaceActivity(run.workspaceId, 'run.updated', {
473
+ runId: run.id,
474
+ eventType: input.eventType,
475
+ status: input.status ?? null
476
+ });
477
+ return value;
478
+ }
479
+ listRunEvents(id, after = 0) {
480
+ return (this.events.get(id) ?? []).filter((event) => event.sequence > after);
481
+ }
482
+ listConversationTurns(input) {
483
+ this.requireSession(input.sessionId);
484
+ // Queued turns have not received `run.started` yet. Their user item still
485
+ // has the acceptance timestamp, so use it rather than sorting every
486
+ // queued turn as time zero. The insertion index makes same-millisecond
487
+ // sends deterministic; callers receive newest first and render reverse.
488
+ const all = [...(this.turns.get(input.sessionId) ?? [])]
489
+ .map((turn, index) => ({ turn, index }))
490
+ .sort((a, b) => {
491
+ const timestamp = (value) => value.startedAt ??
492
+ value.items.find((item) => item.id === value.userItemId)?.startedAt ??
493
+ 0;
494
+ return timestamp(b.turn) - timestamp(a.turn) || b.index - a.index;
495
+ })
496
+ .map(({ turn }) => turn);
497
+ const start = decodeCursor(input.cursor, input.sessionId);
498
+ const limit = Math.min(Math.max(input.limit ?? 30, 1), 500);
499
+ const slice = all.slice(start, start + limit);
500
+ return {
501
+ turns: slice,
502
+ nextCursor: start + limit < all.length ? encodeCursor(input.sessionId, start + limit) : null
503
+ };
504
+ }
505
+ conversationTurnForRun(runId) {
506
+ for (const turns of this.turns.values()) {
507
+ const turn = turns.find((candidate) => candidate.runId === runId);
508
+ if (turn !== undefined)
509
+ return turn;
510
+ }
511
+ return undefined;
512
+ }
513
+ listConversationItems(input) {
514
+ const turns = this.listConversationTurns(input).turns;
515
+ return { items: turns.flatMap((turn) => turn.items), nextCursor: null };
516
+ }
517
+ recordCommandExecution(input) {
518
+ const session = this.requireSession(input.sessionId);
519
+ const now = this.now();
520
+ const item = {
521
+ id: randomUUID(),
522
+ sessionId: input.sessionId,
523
+ turnId: '',
524
+ runId: null,
525
+ parentItemId: null,
526
+ sourceSequence: null,
527
+ revision: 1,
528
+ kind: 'command_execution',
529
+ status: input.failed ? 'FAILED' : 'COMPLETED',
530
+ payload: {
531
+ command: input.command,
532
+ cwd: input.cwd,
533
+ outputPreview: input.outputPreview.slice(0, 10_000),
534
+ outputRef: null,
535
+ exitCode: input.failed ? 1 : 0,
536
+ durationMs: 0,
537
+ truncated: false
538
+ },
539
+ startedAt: now,
540
+ completedAt: now
541
+ };
542
+ const turn = {
543
+ id: randomUUID(),
544
+ sessionId: input.sessionId,
545
+ runId: null,
546
+ userItemId: null,
547
+ status: input.failed ? 'FAILED' : 'SUCCEEDED',
548
+ model: session.model,
549
+ effort: session.effort,
550
+ access: session.access,
551
+ items: [],
552
+ startedAt: now,
553
+ completedAt: now
554
+ };
555
+ const completedItem = { ...item, turnId: turn.id };
556
+ const completedTurn = { ...turn, items: [completedItem] };
557
+ this.turns.set(input.sessionId, [...(this.turns.get(input.sessionId) ?? []), completedTurn]);
558
+ this.replaceSession(session.id, { lastActivityAt: now });
559
+ return completedTurn;
560
+ }
561
+ listWorkspaceActivity(input) {
562
+ const all = this.workspaceEvents.get(input.workspaceId) ?? [];
563
+ const start = decodeCursor(input.cursor, input.workspaceId);
564
+ const limit = Math.min(Math.max(input.limit ?? 30, 1), 500);
565
+ const events = all.slice(start, start + limit);
566
+ return {
567
+ events,
568
+ nextCursor: start + limit < all.length ? encodeCursor(input.workspaceId, start + limit) : null
569
+ };
570
+ }
571
+ createApproval(input) {
572
+ this.requireRun(input.runId);
573
+ if (this.approvals.has(input.approvalId))
574
+ throw new Error('APPROVAL_ALREADY_EXISTS');
575
+ if (input.expiresAt <= this.now())
576
+ throw new Error('APPROVAL_EXPIRED');
577
+ const value = {
578
+ id: input.approvalId,
579
+ runId: input.runId,
580
+ status: 'PENDING',
581
+ payload: input.payload,
582
+ expiresAt: input.expiresAt,
583
+ createdAt: this.now(),
584
+ decidedAt: null
585
+ };
586
+ this.approvals.set(value.id, value);
587
+ return value;
588
+ }
589
+ getApproval(id) {
590
+ const value = this.approvals.get(id);
591
+ if (value?.status === 'PENDING' && value.expiresAt <= this.now())
592
+ return this.expireApproval(id);
593
+ return value;
594
+ }
595
+ listApprovals(runId) {
596
+ return [...this.approvals.values()]
597
+ .filter((value) => value.runId === runId)
598
+ .map((value) => this.getApproval(value.id));
599
+ }
600
+ expireApproval(id) {
601
+ const value = this.requireApproval(id);
602
+ if (value.status !== 'PENDING')
603
+ return value;
604
+ if (value.expiresAt > this.now())
605
+ throw new Error('APPROVAL_NOT_EXPIRED');
606
+ const next = { ...value, status: 'EXPIRED', decidedAt: this.now() };
607
+ this.approvals.set(id, next);
608
+ return next;
609
+ }
610
+ decideApproval(id, decision) {
611
+ const value = this.getApproval(id);
612
+ if (value === undefined)
613
+ throw new Error('APPROVAL_NOT_FOUND');
614
+ if (value.status === 'EXPIRED')
615
+ throw new Error('APPROVAL_EXPIRED');
616
+ if (value.status !== 'PENDING')
617
+ throw new Error('APPROVAL_ALREADY_DECIDED');
618
+ const next = { ...value, status: decision, decidedAt: this.now() };
619
+ this.approvals.set(id, next);
620
+ return next;
621
+ }
622
+ expireDueApprovals() {
623
+ return [...this.approvals.values()]
624
+ .filter((value) => value.status === 'PENDING' && value.expiresAt <= this.now())
625
+ .map((value) => this.expireApproval(value.id));
626
+ }
627
+ listNonterminalRuns() {
628
+ return [...this.runs.values()].filter((run) => !terminal(run.status));
629
+ }
630
+ lastRunSequence(id) {
631
+ return this.events.get(id)?.at(-1)?.sequence ?? 0;
632
+ }
633
+ saveSnapshot(id, value) {
634
+ this.snapshots.set(id, value);
635
+ }
636
+ getSnapshot(id) {
637
+ return this.snapshots.get(id);
638
+ }
639
+ hasActiveWorkspaceLease(workspaceId) {
640
+ return [...this.leases.values()]
641
+ .map((runId) => this.runs.get(runId))
642
+ .some((run) => run?.workspaceId === workspaceId);
643
+ }
644
+ /** Removes a terminal Session's process-local projection after its native history is deleted. */
645
+ removeAgentSession(sessionId) {
646
+ this.requireSession(sessionId);
647
+ const runs = this.listRunsForSession(sessionId);
648
+ if (runs.some((run) => !terminal(run.status)))
649
+ throw new Error('SESSION_ACTIVE');
650
+ const runIds = new Set(runs.map((run) => run.id));
651
+ this.sessions.delete(sessionId);
652
+ this.turns.delete(sessionId);
653
+ this.queuedRuns.delete(sessionId);
654
+ this.queueVersions.delete(sessionId);
655
+ this.pausedQueues.delete(sessionId);
656
+ this.leases.delete(sessionId);
657
+ for (const runId of runIds) {
658
+ this.runs.delete(runId);
659
+ this.events.delete(runId);
660
+ this.snapshots.delete(runId);
661
+ }
662
+ for (const [key, runId] of this.messageRuns)
663
+ if (runIds.has(runId))
664
+ this.messageRuns.delete(key);
665
+ for (const [approvalId, approval] of this.approvals)
666
+ if (runIds.has(approval.runId))
667
+ this.approvals.delete(approvalId);
668
+ }
669
+ removeWorkspace(workspaceId) {
670
+ if (this.hasActiveWorkspaceLease(workspaceId))
671
+ throw new Error('WORKSPACE_BUSY');
672
+ const sessionIds = new Set([...this.sessions.values()]
673
+ .filter((session) => session.workspaceId === workspaceId)
674
+ .map((session) => session.id));
675
+ const runIds = new Set([...this.runs.values()].filter((run) => run.workspaceId === workspaceId).map((run) => run.id));
676
+ for (const sessionId of sessionIds) {
677
+ this.sessions.delete(sessionId);
678
+ this.turns.delete(sessionId);
679
+ }
680
+ for (const runId of runIds) {
681
+ this.runs.delete(runId);
682
+ this.events.delete(runId);
683
+ this.snapshots.delete(runId);
684
+ }
685
+ for (const [key, runId] of this.messageRuns)
686
+ if (runIds.has(runId))
687
+ this.messageRuns.delete(key);
688
+ for (const [approvalId, approval] of this.approvals)
689
+ if (runIds.has(approval.runId))
690
+ this.approvals.delete(approvalId);
691
+ this.workspaceEvents.delete(workspaceId);
692
+ for (const sessionId of sessionIds) {
693
+ this.queuedRuns.delete(sessionId);
694
+ this.queueVersions.delete(sessionId);
695
+ this.pausedQueues.delete(sessionId);
696
+ this.leases.delete(sessionId);
697
+ }
698
+ }
699
+ reconcileRun(id, active) {
700
+ // There is no cross-restart run recovery: this process-local state is
701
+ // discarded on restart and cannot be reconciled from SQLite.
702
+ void id;
703
+ void active;
704
+ }
705
+ replaceSession(id, patch) {
706
+ const value = this.requireSession(id);
707
+ const next = { ...value, ...patch };
708
+ this.sessions.set(id, next);
709
+ return next;
710
+ }
711
+ bumpQueueVersion(sessionId) {
712
+ this.queueVersions.set(sessionId, (this.queueVersions.get(sessionId) ?? 0) + 1);
713
+ }
714
+ requireSession(id) {
715
+ const value = this.sessions.get(id);
716
+ if (value === undefined)
717
+ throw new Error('SESSION_NOT_FOUND');
718
+ return value;
719
+ }
720
+ requireRun(id) {
721
+ const value = this.runs.get(id);
722
+ if (value === undefined)
723
+ throw new Error('RUN_NOT_FOUND');
724
+ return value;
725
+ }
726
+ requireApproval(id) {
727
+ const value = this.approvals.get(id);
728
+ if (value === undefined)
729
+ throw new Error('APPROVAL_NOT_FOUND');
730
+ return value;
731
+ }
732
+ recordWorkspaceActivity(workspaceId, eventType, payload) {
733
+ const event = {
734
+ id: this.nextWorkspaceEventId++,
735
+ workspaceId,
736
+ eventType,
737
+ payload,
738
+ createdAt: this.now()
739
+ };
740
+ this.workspaceEvents.set(workspaceId, [
741
+ ...(this.workspaceEvents.get(workspaceId) ?? []),
742
+ event
743
+ ]);
744
+ }
745
+ updateTurnForRun(runId, update) {
746
+ const run = this.requireRun(runId);
747
+ const current = this.turns.get(run.sessionId) ?? [];
748
+ this.turns.set(run.sessionId, current.map((turn) => (turn.runId === runId ? update(turn) : turn)));
749
+ }
750
+ projectEvent(event) {
751
+ const run = this.requireRun(event.runId);
752
+ this.updateTurnForRun(event.runId, (turn) => {
753
+ if (event.eventType === 'text.delta' &&
754
+ isRecord(event.payload) &&
755
+ typeof event.payload.text === 'string') {
756
+ const existing = turn.items.find((item) => item.id === `${event.runId}:assistant`);
757
+ const text = `${existing === undefined ? '' : readableText(existing.payload)}${event.payload.text}`;
758
+ const item = {
759
+ id: `${event.runId}:assistant`,
760
+ sessionId: run.sessionId,
761
+ turnId: turn.id,
762
+ runId: event.runId,
763
+ parentItemId: null,
764
+ sourceSequence: event.sequence,
765
+ revision: event.sequence,
766
+ kind: 'assistant_message',
767
+ status: 'IN_PROGRESS',
768
+ payload: { text, format: 'markdown', model: null },
769
+ startedAt: existing?.startedAt ?? event.createdAt,
770
+ completedAt: null
771
+ };
772
+ return {
773
+ ...turn,
774
+ status: 'RUNNING',
775
+ startedAt: turn.startedAt ?? event.createdAt,
776
+ items: [...turn.items.filter((value) => value.id !== item.id), item]
777
+ };
778
+ }
779
+ const projected = conversationItemFromEvent(event);
780
+ if (projected !== undefined) {
781
+ const existing = turn.items.find((item) => item.id === `${event.runId}:${projected.itemId}`);
782
+ const status = projected.status;
783
+ const item = {
784
+ id: `${event.runId}:${projected.itemId}`,
785
+ sessionId: run.sessionId,
786
+ turnId: turn.id,
787
+ runId: event.runId,
788
+ parentItemId: null,
789
+ sourceSequence: event.sequence,
790
+ revision: (existing?.revision ?? -1) + 1,
791
+ kind: projected.kind,
792
+ status,
793
+ payload: mergeConversationItemPayload(existing?.payload, projected),
794
+ startedAt: existing?.startedAt ?? event.createdAt,
795
+ completedAt: completedConversationItem(status) ? event.createdAt : null
796
+ };
797
+ return {
798
+ ...turn,
799
+ status: terminal(run.status) ? run.status : 'RUNNING',
800
+ startedAt: turn.startedAt ?? event.createdAt,
801
+ items: existing === undefined
802
+ ? [...turn.items, item]
803
+ : turn.items.map((value) => (value.id === item.id ? item : value))
804
+ };
805
+ }
806
+ if (event.eventType === 'run.started' || event.eventType === 'run.running')
807
+ return { ...turn, status: 'RUNNING', startedAt: turn.startedAt ?? event.createdAt };
808
+ if (event.eventType === 'run.completed' ||
809
+ event.eventType === 'run.failed' ||
810
+ event.eventType === 'run.rejected') {
811
+ return {
812
+ ...turn,
813
+ status: this.requireRun(event.runId).status,
814
+ completedAt: event.createdAt,
815
+ items: turn.items.map((item) => activeConversationItem(item.status)
816
+ ? {
817
+ ...item,
818
+ status: completedStatusForRun(this.requireRun(event.runId).status),
819
+ completedAt: event.createdAt
820
+ }
821
+ : item)
822
+ };
823
+ }
824
+ return turn;
825
+ });
826
+ }
827
+ }
828
+ function terminal(status) {
829
+ return ['SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED', 'LOST'].includes(status);
830
+ }
831
+ function transitionAllowed(from, to) {
832
+ return !terminal(from) || from === to;
833
+ }
834
+ function isRecord(value) {
835
+ return typeof value === 'object' && value !== null;
836
+ }
837
+ function readableText(value) {
838
+ return isRecord(value) && typeof value.text === 'string' ? value.text : '';
839
+ }
840
+ function conversationItemFromEvent(event) {
841
+ if (event.eventType !== 'conversation.item' || !isRecord(event.payload))
842
+ return undefined;
843
+ const itemId = event.payload.itemId;
844
+ const kind = event.payload.kind;
845
+ const status = event.payload.status;
846
+ const payload = event.payload.payload;
847
+ if (typeof itemId !== 'string' ||
848
+ itemId.length === 0 ||
849
+ itemId.length > 88 ||
850
+ typeof kind !== 'string' ||
851
+ !projectableConversationKind(kind) ||
852
+ typeof status !== 'string' ||
853
+ !projectableConversationStatus(status) ||
854
+ !isRecord(payload))
855
+ return undefined;
856
+ const append = event.payload.append;
857
+ return {
858
+ itemId,
859
+ kind,
860
+ status,
861
+ payload,
862
+ merge: event.payload.merge === true,
863
+ ...(append === 'sections' || append === 'outputPreview' || append === 'outputSummary'
864
+ ? { append }
865
+ : {})
866
+ };
867
+ }
868
+ function projectableConversationKind(value) {
869
+ return (value === 'reasoning_summary' ||
870
+ value === 'tool_call' ||
871
+ value === 'command_execution' ||
872
+ value === 'user_input_request' ||
873
+ value === 'plan' ||
874
+ value === 'file_change' ||
875
+ value === 'context_compaction' ||
876
+ value === 'warning' ||
877
+ value === 'error');
878
+ }
879
+ function projectableConversationStatus(value) {
880
+ return [
881
+ 'PENDING',
882
+ 'IN_PROGRESS',
883
+ 'COMPLETED',
884
+ 'FAILED',
885
+ 'DECLINED',
886
+ 'CANCELLED',
887
+ 'INTERRUPTED'
888
+ ].includes(value);
889
+ }
890
+ function mergeConversationItemPayload(existing, next) {
891
+ const base = next.merge && isRecord(existing) ? existing : {};
892
+ const value = { ...base, ...next.payload };
893
+ if (next.append === 'sections') {
894
+ const prior = Array.isArray(base.sections) ? base.sections : [];
895
+ const incoming = Array.isArray(next.payload.sections) ? next.payload.sections : [];
896
+ const sections = new Map();
897
+ for (const section of [...prior, ...incoming]) {
898
+ if (!isRecord(section) ||
899
+ typeof section.index !== 'number' ||
900
+ !Number.isInteger(section.index) ||
901
+ section.index < 0 ||
902
+ typeof section.text !== 'string')
903
+ continue;
904
+ sections.set(section.index, appendProjectionText(sections.get(section.index) ?? '', section.text));
905
+ }
906
+ value.sections = [...sections]
907
+ .sort(([left], [right]) => left - right)
908
+ .map(([index, text]) => ({ index, text }));
909
+ }
910
+ if (next.append === 'outputPreview') {
911
+ const prior = typeof base.outputPreview === 'string' ? base.outputPreview : '';
912
+ const incoming = typeof next.payload.outputPreview === 'string' ? next.payload.outputPreview : '';
913
+ value.outputPreview = appendProjectionText(prior, incoming);
914
+ }
915
+ if (next.append === 'outputSummary') {
916
+ const prior = typeof base.outputSummary === 'string' ? base.outputSummary : '';
917
+ const incoming = typeof next.payload.outputSummary === 'string' ? next.payload.outputSummary : '';
918
+ value.outputSummary = appendProjectionText(prior, incoming);
919
+ }
920
+ return value;
921
+ }
922
+ function appendProjectionText(previous, next) {
923
+ const text = `${previous}${next}`;
924
+ return text.length <= 10_000 ? text : `${text.slice(0, 9_999)}…`;
925
+ }
926
+ function activeConversationItem(status) {
927
+ return status === 'PENDING' || status === 'IN_PROGRESS';
928
+ }
929
+ function completedConversationItem(status) {
930
+ return !activeConversationItem(status);
931
+ }
932
+ function completedStatusForRun(status) {
933
+ if (status === 'INTERRUPTED')
934
+ return 'INTERRUPTED';
935
+ if (status === 'CANCELLED')
936
+ return 'CANCELLED';
937
+ if (status === 'FAILED')
938
+ return 'FAILED';
939
+ return 'COMPLETED';
940
+ }
941
+ function encodeCursor(scope, value) {
942
+ return Buffer.from(JSON.stringify({ scope, value })).toString('base64url');
943
+ }
944
+ function decodeCursor(cursor, scope) {
945
+ if (cursor === undefined)
946
+ return 0;
947
+ try {
948
+ const value = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
949
+ return value.scope === scope && Number.isInteger(value.value) && value.value >= 0
950
+ ? value.value
951
+ : 0;
952
+ }
953
+ catch {
954
+ return 0;
955
+ }
956
+ }
957
+ //# sourceMappingURL=runtime-state.js.map