@myagentroam/node 0.9.2 → 0.9.4

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 (81) hide show
  1. package/dist/codex-app-server.d.ts +24 -0
  2. package/dist/codex-app-server.js +100 -5
  3. package/dist/connector.js +18 -15
  4. package/dist/control-outbound-scheduler.d.ts +4 -1
  5. package/dist/control-outbound-scheduler.js +39 -6
  6. package/dist/database.d.ts +2 -19
  7. package/dist/database.js +1 -62
  8. package/dist/event-buffer.d.ts +2 -5
  9. package/dist/event-buffer.js +3 -12
  10. package/dist/native-session-history.js +33 -12
  11. package/dist/runner/claude/managed-run-controller.d.ts +0 -1
  12. package/dist/runner/claude/managed-run-controller.js +17 -12
  13. package/dist/runner/codex/conversation-parser.d.ts +1 -1
  14. package/dist/runner/codex/conversation-parser.js +3 -0
  15. package/dist/runner/codex/managed-run-controller.d.ts +11 -2
  16. package/dist/runner/codex/managed-run-controller.js +79 -24
  17. package/dist/runner/codex-runner.d.ts +3 -0
  18. package/dist/runner/codex-runner.js +42 -3
  19. package/dist/runner/opencode/conversation-parser.js +1 -1
  20. package/dist/runner/opencode/managed-run-controller.d.ts +7 -2
  21. package/dist/runner/opencode/managed-run-controller.js +72 -14
  22. package/dist/runner/opencode-runner.d.ts +3 -0
  23. package/dist/runner/opencode-runner.js +25 -0
  24. package/dist/runner-profiles.js +10 -6
  25. package/dist/runtime-state.d.ts +7 -12
  26. package/dist/runtime-state.js +43 -53
  27. package/dist/service/attachment-domain-state.d.ts +2 -0
  28. package/dist/service/attachment-domain-state.js +2 -0
  29. package/dist/service/conversation-history-service.d.ts +8 -0
  30. package/dist/service/conversation-history-service.js +181 -9
  31. package/dist/service/conversation-segment-service.js +17 -10
  32. package/dist/service/external-session-resume-service.js +9 -0
  33. package/dist/service/fake-managed-run-service.d.ts +0 -1
  34. package/dist/service/fake-managed-run-service.js +0 -2
  35. package/dist/service/native-session-projection-service.js +5 -0
  36. package/dist/service/native-session-watch-service.d.ts +8 -2
  37. package/dist/service/native-session-watch-service.js +28 -10
  38. package/dist/service/node-connection-lifecycle-service.d.ts +0 -2
  39. package/dist/service/node-connection-lifecycle-service.js +0 -1
  40. package/dist/service/node-control-channel.js +1 -1
  41. package/dist/service/node-control-message-service.d.ts +0 -1
  42. package/dist/service/node-control-message-service.js +9 -6
  43. package/dist/service/node-request-service.js +2 -1
  44. package/dist/service/node-workspace-coordinator.d.ts +1 -4
  45. package/dist/service/node-workspace-coordinator.js +2 -12
  46. package/dist/service/run-attachment-service.js +30 -6
  47. package/dist/service/run-event-service.d.ts +1 -0
  48. package/dist/service/run-event-service.js +80 -0
  49. package/dist/service/run-workbench-service.d.ts +1 -1
  50. package/dist/service/run-workbench-service.js +1 -5
  51. package/dist/service/runner-interaction-service.d.ts +1 -2
  52. package/dist/service/runner-interaction-service.js +1 -4
  53. package/dist/service/session-context-service.d.ts +1 -0
  54. package/dist/service/session-context-service.js +14 -0
  55. package/dist/service/session-domain-state.d.ts +1 -0
  56. package/dist/service/session-domain-state.js +1 -0
  57. package/dist/service/session-lifecycle-service.js +4 -12
  58. package/dist/service/session-message-service.d.ts +1 -0
  59. package/dist/service/session-message-service.js +109 -85
  60. package/dist/service/skill-directory-service.d.ts +3 -1
  61. package/dist/service/skill-directory-service.js +28 -4
  62. package/dist/service/skill-node-operation-service.d.ts +1 -1
  63. package/dist/service/skill-node-operation-service.js +4 -2
  64. package/dist/service/workbench-event-service.d.ts +1 -13
  65. package/dist/service/workbench-event-service.js +2 -36
  66. package/dist/service/workbench-manifest-service.d.ts +0 -1
  67. package/dist/service/workbench-manifest-service.js +0 -1
  68. package/dist/service/workspace-change-service.js +1 -1
  69. package/dist/service/workspace-domain-state.d.ts +1 -0
  70. package/dist/service/workspace-queue-workbench-service.d.ts +13 -2
  71. package/dist/service/workspace-queue-workbench-service.js +8 -2
  72. package/dist/service/workspace-upload-service.d.ts +2 -0
  73. package/dist/service/workspace-upload-service.js +28 -6
  74. package/dist/service/workspace-workbench-service.d.ts +0 -3
  75. package/dist/service/workspace-workbench-service.js +0 -17
  76. package/dist/util/runner-error.d.ts +6 -0
  77. package/dist/util/runner-error.js +52 -0
  78. package/dist/util/runner-native-session-parsers.d.ts +2 -1
  79. package/dist/util/runner-native-session-parsers.js +63 -15
  80. package/dist/workspace.js +33 -8
  81. package/package.json +2 -2
@@ -3,6 +3,7 @@ const INTERVAL_MS = 2_000;
3
3
  const TTL_MS = 45_000;
4
4
  const MAX_FAILURES = 3;
5
5
  const INITIAL_TURN_LIMIT = 30;
6
+ const EMITTED_TURN_LIMIT = 10;
6
7
  export class NativeSessionWatchService {
7
8
  options;
8
9
  watches = new Map();
@@ -32,7 +33,7 @@ export class NativeSessionWatchService {
32
33
  return { renewed: true };
33
34
  }
34
35
  const watch = this.watch(session);
35
- const turnSnapshot = watch === undefined ? undefined : await this.refresh(session.id, INITIAL_TURN_LIMIT);
36
+ const turnSnapshot = watch === undefined ? undefined : await this.refresh(session.id, INITIAL_TURN_LIMIT, false);
36
37
  if (watch !== undefined && turnSnapshot === undefined) {
37
38
  this.stop(session.id);
38
39
  throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
@@ -68,6 +69,9 @@ export class NativeSessionWatchService {
68
69
  refresh: undefined,
69
70
  sessionSignature: undefined,
70
71
  signature: undefined,
72
+ page: undefined,
73
+ observedPage: undefined,
74
+ authority: undefined,
71
75
  failures: 0,
72
76
  timer: undefined
73
77
  };
@@ -110,7 +114,7 @@ export class NativeSessionWatchService {
110
114
  get size() {
111
115
  return this.watches.size;
112
116
  }
113
- async refresh(sessionId, limit = 10) {
117
+ async refresh(sessionId, limit = 10, emit = true) {
114
118
  const watch = this.watches.get(sessionId);
115
119
  if (watch === undefined)
116
120
  return undefined;
@@ -120,7 +124,7 @@ export class NativeSessionWatchService {
120
124
  this.stop(sessionId);
121
125
  return undefined;
122
126
  }
123
- const refresh = this.performRefresh(sessionId, watch, limit);
127
+ const refresh = this.performRefresh(sessionId, watch, limit, emit);
124
128
  watch.refresh = refresh;
125
129
  try {
126
130
  return await refresh;
@@ -130,7 +134,7 @@ export class NativeSessionWatchService {
130
134
  watch.refresh = undefined;
131
135
  }
132
136
  }
133
- async performRefresh(sessionId, watch, limit) {
137
+ async performRefresh(sessionId, watch, limit, emit) {
134
138
  try {
135
139
  const session = await this.options.resolve(sessionId);
136
140
  if (session === undefined ||
@@ -148,14 +152,28 @@ export class NativeSessionWatchService {
148
152
  }
149
153
  const page = await this.options.readPage(session, limit);
150
154
  watch.failures = 0;
151
- const turns = page.turns.length <= 10 ? page.turns : page.turns.slice(-10);
152
- const signature = JSON.stringify(turns);
155
+ const turns = page.turns.length <= EMITTED_TURN_LIMIT
156
+ ? page.turns
157
+ : page.turns.slice(-EMITTED_TURN_LIMIT);
158
+ const observed = { ...page, turns };
159
+ const convergence = this.options.convergePage(watch.page, watch.observedPage, observed, watch.authority);
160
+ const converged = convergence.page;
161
+ const accepted = converged.turns.length <= EMITTED_TURN_LIMIT + 1
162
+ ? converged
163
+ : {
164
+ ...converged,
165
+ turns: converged.turns.slice(-(EMITTED_TURN_LIMIT + 1))
166
+ };
167
+ watch.observedPage = observed;
168
+ watch.authority = convergence.authority;
169
+ const signature = JSON.stringify({ source: accepted.source, turns: accepted.turns });
153
170
  if (signature === watch.signature)
154
- return page;
171
+ return accepted;
155
172
  watch.signature = signature;
156
- for (const turn of turns)
157
- this.options.emitTurn(turn);
158
- return page;
173
+ watch.page = accepted;
174
+ if (emit)
175
+ this.options.emitPage(session, accepted);
176
+ return emit ? accepted : page;
159
177
  }
160
178
  catch {
161
179
  watch.failures += 1;
@@ -3,7 +3,6 @@ import { type NodeConfig } from '../config.js';
3
3
  import type { NodeMetrics } from '../operational.js';
4
4
  import type { NodeControlChannel } from './node-control-channel.js';
5
5
  import type { NodeTerminalChannel } from './node-terminal-channel.js';
6
- import type { WorkbenchEventService } from './workbench-event-service.js';
7
6
  interface NodeConnectionLifecycleServiceOptions {
8
7
  readonly config: () => NodeConfig | undefined;
9
8
  readonly setConfig: (config: NodeConfig) => void;
@@ -15,7 +14,6 @@ interface NodeConnectionLifecycleServiceOptions {
15
14
  readonly setCapabilities: (capabilities: NodeCapabilities) => void;
16
15
  readonly control: NodeControlChannel;
17
16
  readonly terminal: NodeTerminalChannel;
18
- readonly events: WorkbenchEventService;
19
17
  readonly metrics: NodeMetrics;
20
18
  readonly heartbeatMs: number;
21
19
  readonly refreshMs: number;
@@ -48,7 +48,6 @@ export class NodeConnectionLifecycleService {
48
48
  this.options.terminal.connect();
49
49
  this.options.control.send('heartbeat', {});
50
50
  this.options.metrics.heartbeatSent();
51
- this.options.events.replay();
52
51
  this.heartbeat = setInterval(() => {
53
52
  this.options.metrics.heartbeatSent();
54
53
  this.options.control.send('heartbeat', {});
@@ -5,7 +5,7 @@ import { nodeConnectEndpoint } from '../runner/codex/conversation-parser.js';
5
5
  export class NodeControlChannel {
6
6
  options;
7
7
  socket;
8
- outbound = new ControlOutboundScheduler(() => this.socket);
8
+ outbound = new ControlOutboundScheduler(() => this.socket, () => this.socket?.close(1013, 'NODE_CONTROL_BACKPRESSURE_TIMEOUT'));
9
9
  reconnectTimer;
10
10
  attempts = 0;
11
11
  constructor(options) {
@@ -8,7 +8,6 @@ interface NodeControlMessageServiceOptions {
8
8
  readonly close: (code: number, reason: string) => void;
9
9
  readonly inspectWorkspace: (envelope: NodeEnvelope) => Promise<void>;
10
10
  readonly handleNodeRequest: (envelope: NodeEnvelope) => Promise<void>;
11
- readonly acknowledgeWorkbenchEvent: (replyTo: string | undefined) => void;
12
11
  readonly reconcileRuns: (envelope: NodeEnvelope) => void;
13
12
  readonly runners: RunnerRegistry;
14
13
  readonly deliverSessionChannelMessage: (envelope: NodeEnvelope) => void;
@@ -33,12 +33,8 @@ export class NodeControlMessageService {
33
33
  void this.options.handleNodeRequest(envelope);
34
34
  return;
35
35
  }
36
- if (envelope.type === 'ack') {
37
- const payload = envelope.payload;
38
- if (payload.type === 'workbench.event')
39
- this.options.acknowledgeWorkbenchEvent(envelope.replyTo);
36
+ if (envelope.type === 'ack')
40
37
  return;
41
- }
42
38
  if (envelope.type === 'runs.reconcile') {
43
39
  this.options.reconcileRuns(envelope);
44
40
  return;
@@ -49,7 +45,14 @@ export class NodeControlMessageService {
49
45
  }
50
46
  if (envelope.type === 'run.start') {
51
47
  this.options.send('ack', { type: envelope.type }, envelope.id);
52
- this.options.runners.startManagedRun(envelope);
48
+ try {
49
+ this.options.runners.startManagedRun(envelope);
50
+ }
51
+ catch (error) {
52
+ // A delayed internal start may arrive after the terminal projection was removed.
53
+ if (!(error instanceof Error) || error.message !== 'RUN_TERMINAL')
54
+ throw error;
55
+ }
53
56
  return;
54
57
  }
55
58
  if (envelope.type === 'session.channel.message') {
@@ -23,7 +23,8 @@ export class NodeRequestService {
23
23
  ? {
24
24
  ...payload.data,
25
25
  __workspaceWatchSessionHash: payload.userAuthorization
26
- .sessionHash
26
+ .sessionHash,
27
+ __requestUserId: payload.userAuthorization.userId
27
28
  }
28
29
  : payload.data;
29
30
  const result = await this.options.execute(payload.operation, data);
@@ -1,7 +1,6 @@
1
1
  import type { NodeEnvelope } from '@myagentroam/protocol';
2
2
  import type { NodeConfig } from '../config.js';
3
3
  import type { NodeAgentSession, NodeDatabase, NodeWorkspace } from '../database.js';
4
- import type { NodeRuntimeState } from '../runtime-state.js';
5
4
  import type { TerminalSummary } from '../terminal.js';
6
5
  import type { WorkspaceChangeService } from './workspace-change-service.js';
7
6
  import type { WorkspaceSessionService } from './workspace-session-service.js';
@@ -9,14 +8,13 @@ import type { WorkspaceWatchService } from './workspace-watch-service.js';
9
8
  interface NodeWorkspaceCoordinatorOptions {
10
9
  readonly config: () => NodeConfig | undefined;
11
10
  readonly database: () => NodeDatabase | undefined;
12
- readonly runtime: NodeRuntimeState;
13
11
  readonly watches: WorkspaceWatchService;
14
12
  readonly sessions: WorkspaceSessionService;
15
13
  readonly changes: WorkspaceChangeService;
16
14
  readonly emit: (workspaceId: string, topic: 'files' | 'changes', revision: number) => void;
17
15
  readonly send: (type: string, payload: unknown, replyTo?: string) => void;
18
16
  }
19
- /** Coordinates Workspace invalidation, inspection, snapshots and persisted Terminal summaries. */
17
+ /** Coordinates Workspace invalidation, inspection and persisted Terminal summaries. */
20
18
  export declare class NodeWorkspaceCoordinator {
21
19
  private readonly options;
22
20
  constructor(options: NodeWorkspaceCoordinatorOptions);
@@ -27,6 +25,5 @@ export declare class NodeWorkspaceCoordinator {
27
25
  invalidateChanges(workspaceId: string): void;
28
26
  persistTerminal(summary: TerminalSummary): void;
29
27
  inspect(envelope: NodeEnvelope): Promise<void>;
30
- captureRunSnapshot(runId: string, cwd: string): Promise<void>;
31
28
  }
32
29
  export {};
@@ -1,5 +1,5 @@
1
- import { inspectWorkspace, readGitSummary } from '../workspace.js';
2
- /** Coordinates Workspace invalidation, inspection, snapshots and persisted Terminal summaries. */
1
+ import { inspectWorkspace } from '../workspace.js';
2
+ /** Coordinates Workspace invalidation, inspection and persisted Terminal summaries. */
3
3
  export class NodeWorkspaceCoordinator {
4
4
  options;
5
5
  constructor(options) {
@@ -67,14 +67,4 @@ export class NodeWorkspaceCoordinator {
67
67
  this.options.send('workspace.inspect.result', { error: 'WORKSPACE_INVALID' }, envelope.id);
68
68
  }
69
69
  }
70
- async captureRunSnapshot(runId, cwd) {
71
- try {
72
- const snapshot = await readGitSummary(cwd);
73
- this.options.runtime.saveSnapshot(runId, snapshot);
74
- this.options.send('workspace.snapshot', { runId, snapshot });
75
- }
76
- catch {
77
- // Ordinary directories have no Git snapshot and keep the existing Run result.
78
- }
79
- }
80
70
  }
@@ -4,6 +4,7 @@ import { lstat, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:f
4
4
  import { AttachmentDomainState } from './attachment-domain-state.js';
5
5
  const MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
6
6
  const IMAGE_CACHE_TTL_MS = 60 * 60_000;
7
+ const NATIVE_IMAGE_INDEX_TTL_MS = 60 * 60_000;
7
8
  export class RunAttachmentService {
8
9
  nativeImageRoots;
9
10
  state = new AttachmentDomainState();
@@ -43,21 +44,28 @@ export class RunAttachmentService {
43
44
  throw new Error('MESSAGE_ATTACHMENT_DIRECTORY_INVALID');
44
45
  const runDirectory = join(filesDirectory, runId);
45
46
  await mkdir(runDirectory, { mode: 0o700 });
47
+ this.state.runFileDirectories.set(runId, runDirectory);
46
48
  try {
47
49
  await Promise.all(attachments.map((attachment) => writeFile(join(runDirectory, attachment.name), Buffer.from(attachment.dataBase64, 'base64'), { flag: 'wx', mode: 0o600 })));
48
50
  return attachments.map((attachment) => `.mar/temp-files/${runId}/${attachment.name}`);
49
51
  }
50
52
  catch (error) {
53
+ this.state.runFileDirectories.delete(runId);
51
54
  await rm(runDirectory, { recursive: true, force: true });
52
55
  throw error;
53
56
  }
54
57
  }
55
58
  cleanup(runId) {
56
59
  const directory = this.state.runDirectories.get(runId);
57
- if (directory === undefined)
58
- return;
59
- this.state.runDirectories.delete(runId);
60
- void rm(directory, { recursive: true, force: true }).catch(() => undefined);
60
+ if (directory !== undefined) {
61
+ this.state.runDirectories.delete(runId);
62
+ void rm(directory, { recursive: true, force: true }).catch(() => undefined);
63
+ }
64
+ const fileDirectory = this.state.runFileDirectories.get(runId);
65
+ if (fileDirectory !== undefined) {
66
+ this.state.runFileDirectories.delete(runId);
67
+ void rm(fileDirectory, { recursive: true, force: true }).catch(() => undefined);
68
+ }
61
69
  }
62
70
  cache(runId, images) {
63
71
  if (images.length === 0)
@@ -122,8 +130,18 @@ export class RunAttachmentService {
122
130
  }
123
131
  attachments.push({ name: image.name, mime: image.mime, runId, imageIndex });
124
132
  }
125
- if (sources.length > 0)
133
+ if (sources.length > 0) {
126
134
  this.state.nativeHistoryImages.set(runId, sources);
135
+ const previousTimer = this.state.nativeHistoryImageTimers.get(runId);
136
+ if (previousTimer !== undefined)
137
+ clearTimeout(previousTimer);
138
+ const timer = setTimeout(() => {
139
+ this.state.nativeHistoryImageTimers.delete(runId);
140
+ this.state.nativeHistoryImages.delete(runId);
141
+ }, NATIVE_IMAGE_INDEX_TTL_MS);
142
+ timer.unref();
143
+ this.state.nativeHistoryImageTimers.set(runId, timer);
144
+ }
127
145
  return attachments;
128
146
  }
129
147
  async readNative(runId, imageIndex) {
@@ -161,10 +179,16 @@ export class RunAttachmentService {
161
179
  this.state.runImages.delete(runId);
162
180
  }
163
181
  dispose() {
164
- for (const runId of this.state.runDirectories.keys())
182
+ for (const runId of new Set([
183
+ ...this.state.runDirectories.keys(),
184
+ ...this.state.runFileDirectories.keys()
185
+ ]))
165
186
  this.cleanup(runId);
166
187
  for (const runId of this.state.runImages.keys())
167
188
  this.clear(runId);
189
+ for (const timer of this.state.nativeHistoryImageTimers.values())
190
+ clearTimeout(timer);
191
+ this.state.nativeHistoryImageTimers.clear();
168
192
  this.state.nativeHistoryImages.clear();
169
193
  }
170
194
  }
@@ -29,4 +29,5 @@ export declare class RunEventService {
29
29
  private enqueueText;
30
30
  private flushText;
31
31
  private emitNow;
32
+ private notificationPayload;
32
33
  }
@@ -50,6 +50,9 @@ export class RunEventService {
50
50
  this.emitNow(runId, 'text.delta', { text: pending.text });
51
51
  }
52
52
  emitNow(runId, eventType, payload, status) {
53
+ if (this.options.runtime.getRun(runId) === undefined)
54
+ return;
55
+ const notificationPayload = this.notificationPayload(runId, eventType, payload, status);
53
56
  const event = {
54
57
  runId,
55
58
  sequence: (this.sequences.get(runId) ?? 0) + 1,
@@ -76,6 +79,7 @@ export class RunEventService {
76
79
  /* A stale relayed run may race the storage boundary. */
77
80
  }
78
81
  if (status !== undefined && isTerminalRunStatus(status)) {
82
+ this.options.state.active.delete(runId);
79
83
  this.options.state.interruptRequested.delete(runId);
80
84
  const terminalRun = this.options.runtime.getRun(runId);
81
85
  if (terminalRun !== undefined && this.options.isBootstrapSession(terminalRun.sessionId))
@@ -91,6 +95,19 @@ export class RunEventService {
91
95
  this.options.emitWorkbench('conversation', {
92
96
  event: { ...event, createdAt: persisted?.createdAt ?? Date.now() }
93
97
  });
98
+ if (notificationPayload !== undefined) {
99
+ const occurredAt = persisted?.createdAt ?? Date.now();
100
+ nodeLog('run.event', { runId, eventType: 'notification' });
101
+ this.options.emitWorkbench('conversation', {
102
+ event: {
103
+ runId,
104
+ sequence: event.sequence,
105
+ eventType: 'notification',
106
+ payload: { ...notificationPayload, occurredAt },
107
+ createdAt: occurredAt
108
+ }
109
+ });
110
+ }
94
111
  if (status === undefined || !isTerminalRunStatus(status))
95
112
  return;
96
113
  const turn = this.options.runtime.conversationTurnForRun(runId);
@@ -102,7 +119,70 @@ export class RunEventService {
102
119
  const run = this.options.runtime.getRun(runId);
103
120
  if (run !== undefined) {
104
121
  this.options.scheduleContextRefresh(run.sessionId);
122
+ this.options.runtime.removeTerminalRun(runId);
123
+ this.sequences.delete(runId);
105
124
  void this.options.dispatchQueue(run.sessionId);
106
125
  }
107
126
  }
127
+ notificationPayload(runId, eventType, payload, status) {
128
+ if (eventType === 'notification')
129
+ return undefined;
130
+ const run = this.options.runtime.getRun(runId);
131
+ if (run === undefined)
132
+ return undefined;
133
+ const session = this.options.runtime.getAgentSession(run.sessionId);
134
+ if (session === undefined)
135
+ return undefined;
136
+ const base = {
137
+ userId: run.initiatedByUserId,
138
+ workspaceId: run.workspaceId,
139
+ sessionId: run.sessionId,
140
+ sessionTitle: session.customTitle ?? session.runnerTitle ?? session.id,
141
+ runId
142
+ };
143
+ if (!Number.isSafeInteger(base.userId))
144
+ return undefined;
145
+ if (status !== undefined && isTerminalRunStatus(status)) {
146
+ if (this.options.state.interruptRequested.has(runId))
147
+ return undefined;
148
+ if (status === 'SUCCEEDED' &&
149
+ this.options.runtime.sessionQueue(run.sessionId).runIds.length > 0)
150
+ return undefined;
151
+ return {
152
+ ...base,
153
+ notificationId: `run:${run.workspaceId}:${runId}`,
154
+ kind: 'RUN_TERMINAL',
155
+ outcome: status
156
+ };
157
+ }
158
+ if (eventType === 'approval.request' && isPlainRecord(payload)) {
159
+ const approvalId = payload.approvalId;
160
+ if (typeof approvalId !== 'string')
161
+ return undefined;
162
+ return {
163
+ ...base,
164
+ notificationId: `approval:${run.workspaceId}:${runId}:${approvalId}`,
165
+ kind: 'APPROVAL_REQUIRED'
166
+ };
167
+ }
168
+ if (eventType !== 'conversation.item' || !isPlainRecord(payload))
169
+ return undefined;
170
+ if (payload.kind === 'plan' && typeof payload.itemId === 'string')
171
+ return {
172
+ ...base,
173
+ notificationId: `plan:${run.workspaceId}:${runId}:${payload.itemId}`,
174
+ kind: 'PLAN_READY'
175
+ };
176
+ if (payload.kind === 'user_input_request' &&
177
+ typeof payload.itemId === 'string' &&
178
+ isPlainRecord(payload.payload) &&
179
+ Array.isArray(payload.payload.questions) &&
180
+ payload.payload.questions.length > 0)
181
+ return {
182
+ ...base,
183
+ notificationId: `user-input:${run.workspaceId}:${runId}:${payload.itemId}`,
184
+ kind: 'USER_INPUT_REQUIRED'
185
+ };
186
+ return undefined;
187
+ }
108
188
  }
@@ -6,6 +6,7 @@ export interface RunWorkbenchServiceOptions {
6
6
  readonly respondUserInput: (runId: string, requestId: string, answers: Record<string, unknown>) => void;
7
7
  readonly deleteQueuedStart: (runId: string) => void;
8
8
  readonly clearImages: (runId: string) => void;
9
+ readonly cleanupAttachments: (runId: string) => void;
9
10
  readonly emitQueue: (workspaceId: string, sessionId: string) => void;
10
11
  readonly emitRun: (runId: string, eventType: string, payload: unknown, status?: 'CANCELLED') => void;
11
12
  readonly control: (operation: string, payload: Record<string, unknown>) => void;
@@ -16,7 +17,6 @@ export declare class RunWorkbenchService {
16
17
  constructor(options: RunWorkbenchServiceOptions);
17
18
  operations(): Readonly<Record<string, NodeOperationHandler>>;
18
19
  private get;
19
- private snapshot;
20
20
  private image;
21
21
  private respond;
22
22
  private interruptSession;
@@ -8,7 +8,6 @@ export class RunWorkbenchService {
8
8
  operations() {
9
9
  return {
10
10
  'run.get': (data) => this.get(record(data)),
11
- 'run.snapshot': (data) => this.snapshot(record(data)),
12
11
  'run.image.read': (data) => this.image(record(data)),
13
12
  'run.user-input.respond': (data) => this.respond(record(data)),
14
13
  'session.interrupt': (data) => this.interruptSession(record(data)),
@@ -21,10 +20,6 @@ export class RunWorkbenchService {
21
20
  get(input) {
22
21
  return { run: this.requireRun(input.runId) };
23
22
  }
24
- snapshot(input) {
25
- const run = this.requireRun(input.runId);
26
- return { snapshot: this.options.runtime.getSnapshot(run.id) ?? null };
27
- }
28
23
  async image(input) {
29
24
  if (typeof input.runId !== 'string' ||
30
25
  typeof input.imageIndex !== 'number' ||
@@ -63,6 +58,7 @@ export class RunWorkbenchService {
63
58
  this.options.runtime.deleteQueuedRun(run.id);
64
59
  this.options.deleteQueuedStart(run.id);
65
60
  this.options.clearImages(run.id);
61
+ this.options.cleanupAttachments(run.id);
66
62
  this.options.emitQueue(run.workspaceId, run.sessionId);
67
63
  return { cancelled: true };
68
64
  }
@@ -13,7 +13,6 @@ interface RunnerInteractionServiceOptions {
13
13
  readonly runners: RunnerRegistry;
14
14
  readonly emitRun: (runId: string, eventType: string, payload: unknown, status?: 'FAILED' | 'CANCELLED' | 'INTERRUPTED') => void;
15
15
  readonly emitItem: (runId: string, item: PublicConversationItem) => void;
16
- readonly captureSnapshot: (runId: string, cwd: string) => Promise<void>;
17
16
  readonly approvalTimeoutMs: number;
18
17
  readonly publishApproval: (input: {
19
18
  readonly runId: string;
@@ -33,7 +32,7 @@ export declare class RunnerInteractionService {
33
32
  cancelClaudeRun(runId: string, intent: 'CANCEL' | 'INTERRUPT'): boolean;
34
33
  cancelCodexTurn(runId: string, threadId: string, turnId: string, cwd: string, terminalStatus: 'CANCELLED' | 'INTERRUPTED'): Promise<void>;
35
34
  respondUserInput(runId: string, requestId: string, rawAnswers: Record<string, unknown>): void;
36
- completeCancelledRun(runId: string, cwd: string | undefined): boolean;
35
+ completeCancelledRun(runId: string): boolean;
37
36
  private respondCodexUserInput;
38
37
  private answers;
39
38
  }
@@ -162,7 +162,6 @@ export class RunnerInteractionService {
162
162
  codex.execution.runs.delete(runId);
163
163
  codex.releaseEnvironment(runId);
164
164
  this.options.state.active.delete(runId);
165
- void this.options.captureSnapshot(runId, cwd);
166
165
  }
167
166
  }
168
167
  respondUserInput(runId, requestId, rawAnswers) {
@@ -194,7 +193,7 @@ export class RunnerInteractionService {
194
193
  merge: true
195
194
  });
196
195
  }
197
- completeCancelledRun(runId, cwd) {
196
+ completeCancelledRun(runId) {
198
197
  const run = this.options.runtime.getRun(runId);
199
198
  if (run === undefined)
200
199
  return false;
@@ -206,8 +205,6 @@ export class RunnerInteractionService {
206
205
  return false;
207
206
  this.options.codex().execution.runs.delete(runId);
208
207
  this.options.state.active.delete(runId);
209
- if (cwd !== undefined)
210
- void this.options.captureSnapshot(runId, cwd);
211
208
  return true;
212
209
  }
213
210
  respondCodexUserInput(runId, requestId, rawAnswers) {
@@ -24,6 +24,7 @@ export interface SessionContextServiceOptions {
24
24
  export declare class SessionContextService {
25
25
  private readonly options;
26
26
  private readonly snapshots;
27
+ private readonly snapshotTimers;
27
28
  private readonly timers;
28
29
  private readonly versions;
29
30
  constructor(options: SessionContextServiceOptions);
@@ -1,7 +1,9 @@
1
1
  const REFRESH_DELAYS_MS = [100, 400, 1_000];
2
+ const CONTEXT_SNAPSHOT_TTL_MS = 30 * 60_000;
2
3
  export class SessionContextService {
3
4
  options;
4
5
  snapshots = new Map();
6
+ snapshotTimers = new Map();
5
7
  timers = new Map();
6
8
  versions = new Map();
7
9
  constructor(options) {
@@ -20,6 +22,15 @@ export class SessionContextService {
20
22
  percentage: Math.round((usage.usedTokens / usage.maxTokens) * 1000) / 10
21
23
  };
22
24
  this.snapshots.set(session.id, context);
25
+ const previousTimer = this.snapshotTimers.get(session.id);
26
+ if (previousTimer !== undefined)
27
+ clearTimeout(previousTimer);
28
+ const timer = setTimeout(() => {
29
+ this.snapshotTimers.delete(session.id);
30
+ this.snapshots.delete(session.id);
31
+ }, CONTEXT_SNAPSHOT_TTL_MS);
32
+ timer.unref();
33
+ this.snapshotTimers.set(session.id, timer);
23
34
  return { context, pending: false };
24
35
  }
25
36
  schedule(sessionId) {
@@ -71,7 +82,10 @@ export class SessionContextService {
71
82
  dispose() {
72
83
  for (const timer of this.timers.values())
73
84
  clearTimeout(timer);
85
+ for (const timer of this.snapshotTimers.values())
86
+ clearTimeout(timer);
74
87
  this.timers.clear();
88
+ this.snapshotTimers.clear();
75
89
  this.versions.clear();
76
90
  this.snapshots.clear();
77
91
  }
@@ -5,4 +5,5 @@ export declare class SessionDomainState<TSession, TActivity> {
5
5
  readonly resumeFailures: Map<string, string>;
6
6
  readonly messageResumeFailures: Map<string, string>;
7
7
  readonly resumeFailureDetails: Map<string, string>;
8
+ readonly failureTimers: Map<string, NodeJS.Timeout>;
8
9
  }
@@ -5,4 +5,5 @@ export class SessionDomainState {
5
5
  resumeFailures = new Map();
6
6
  messageResumeFailures = new Map();
7
7
  resumeFailureDetails = new Map();
8
+ failureTimers = new Map();
8
9
  }
@@ -112,25 +112,16 @@ export class SessionLifecycleService {
112
112
  async updateConfiguration(input) {
113
113
  if (typeof input.sessionId !== 'string' ||
114
114
  typeof input.model !== 'string' ||
115
+ !validSessionOption(input.model) ||
115
116
  typeof input.effort !== 'string' ||
116
117
  !validSessionEffort(input.effort) ||
117
- typeof input.access !== 'string')
118
+ typeof input.access !== 'string' ||
119
+ !validSessionOption(input.access))
118
120
  throw new Error('SESSION_INVALID');
119
121
  const current = this.options.runtime.getAgentSession(input.sessionId) ??
120
122
  (await this.options.projection.createMetadataProjection(input.sessionId));
121
123
  if (current === undefined)
122
124
  throw new Error('SESSION_NOT_FOUND');
123
- const configuration = {
124
- runner: current.runner,
125
- model: input.model,
126
- effort: input.effort,
127
- access: input.access
128
- };
129
- const workspace = this.options.database().getWorkspace(current.workspaceId);
130
- const environment = parseSecretEnvironment(input.secretEnvironment);
131
- if (workspace === undefined ||
132
- !this.options.runners.supportsConfiguration(configuration, workspace, environment))
133
- throw new Error('RUNNER_CONFIGURATION_UNSUPPORTED');
134
125
  const session = this.options.runtime.updateAgentSessionConfiguration({
135
126
  sessionId: current.id,
136
127
  model: input.model,
@@ -209,6 +200,7 @@ export class SessionLifecycleService {
209
200
  sessionId: messageSessionId,
210
201
  clientMessageId: input.clientMessageId,
211
202
  content: input.content,
203
+ ...(input.__requestUserId === undefined ? {} : { __requestUserId: input.__requestUserId }),
212
204
  ...(input.secretEnvironment === undefined
213
205
  ? {}
214
206
  : { secretEnvironment: input.secretEnvironment }),
@@ -23,6 +23,7 @@ interface SessionMessageServiceOptions {
23
23
  }
24
24
  export declare class SessionMessageService {
25
25
  private readonly options;
26
+ private readonly pendingMessages;
26
27
  constructor(options: SessionMessageServiceOptions);
27
28
  operations(): Readonly<Record<string, NodeOperationHandler>>;
28
29
  private message;