@myagentroam/node 0.9.0 → 0.9.3

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 (98) hide show
  1. package/dist/connector.d.ts +2 -0
  2. package/dist/connector.js +50 -25
  3. package/dist/control-outbound-scheduler.d.ts +4 -1
  4. package/dist/control-outbound-scheduler.js +39 -6
  5. package/dist/database.d.ts +0 -19
  6. package/dist/database.js +1 -62
  7. package/dist/event-buffer.d.ts +2 -5
  8. package/dist/event-buffer.js +3 -12
  9. package/dist/native-session-history.js +105 -26
  10. package/dist/runner/abstract-runner.d.ts +1 -1
  11. package/dist/runner/abstract-runner.js +2 -2
  12. package/dist/runner/claude/managed-run-controller.d.ts +0 -1
  13. package/dist/runner/claude/managed-run-controller.js +32 -17
  14. package/dist/runner/claude-code-runner.js +3 -4
  15. package/dist/runner/codex/conversation-parser.d.ts +3 -3
  16. package/dist/runner/codex/conversation-parser.js +33 -12
  17. package/dist/runner/codex/managed-run-controller.d.ts +2 -2
  18. package/dist/runner/codex/managed-run-controller.js +26 -10
  19. package/dist/runner/codex-runner.d.ts +3 -1
  20. package/dist/runner/codex-runner.js +40 -10
  21. package/dist/runner/opencode/conversation-parser.d.ts +11 -7
  22. package/dist/runner/opencode/conversation-parser.js +16 -16
  23. package/dist/runner/opencode/managed-run-controller.d.ts +8 -2
  24. package/dist/runner/opencode/managed-run-controller.js +94 -22
  25. package/dist/runner/opencode-runner.d.ts +3 -0
  26. package/dist/runner/opencode-runner.js +25 -0
  27. package/dist/runner-command-engine.js +1 -1
  28. package/dist/runner-profiles.js +29 -17
  29. package/dist/runner-usage.js +5 -5
  30. package/dist/runtime-state.d.ts +5 -12
  31. package/dist/runtime-state.js +32 -46
  32. package/dist/service/attachment-domain-state.d.ts +2 -0
  33. package/dist/service/attachment-domain-state.js +2 -0
  34. package/dist/service/conversation-history-service.js +55 -24
  35. package/dist/service/conversation-segment-service.d.ts +20 -0
  36. package/dist/service/conversation-segment-service.js +156 -0
  37. package/dist/service/external-session-resume-service.js +9 -0
  38. package/dist/service/fake-managed-run-service.d.ts +0 -1
  39. package/dist/service/fake-managed-run-service.js +0 -2
  40. package/dist/service/mcp-installation-verifier.js +1 -1
  41. package/dist/service/native-session-projection-service.js +5 -0
  42. package/dist/service/native-session-watch-service.d.ts +1 -0
  43. package/dist/service/native-session-watch-service.js +21 -4
  44. package/dist/service/node-connection-lifecycle-service.d.ts +0 -2
  45. package/dist/service/node-connection-lifecycle-service.js +0 -1
  46. package/dist/service/node-control-channel.js +1 -1
  47. package/dist/service/node-control-message-service.d.ts +0 -1
  48. package/dist/service/node-control-message-service.js +9 -6
  49. package/dist/service/node-request-service.js +2 -1
  50. package/dist/service/node-workspace-coordinator.d.ts +1 -4
  51. package/dist/service/node-workspace-coordinator.js +2 -12
  52. package/dist/service/run-attachment-service.d.ts +3 -2
  53. package/dist/service/run-attachment-service.js +51 -17
  54. package/dist/service/run-event-service.d.ts +2 -0
  55. package/dist/service/run-event-service.js +77 -2
  56. package/dist/service/run-workbench-service.d.ts +1 -1
  57. package/dist/service/run-workbench-service.js +1 -5
  58. package/dist/service/runner-interaction-service.d.ts +1 -2
  59. package/dist/service/runner-interaction-service.js +1 -4
  60. package/dist/service/session-catalog-service.js +4 -4
  61. package/dist/service/session-context-service.d.ts +1 -0
  62. package/dist/service/session-context-service.js +14 -0
  63. package/dist/service/session-domain-state.d.ts +1 -0
  64. package/dist/service/session-domain-state.js +1 -0
  65. package/dist/service/session-message-service.js +5 -10
  66. package/dist/service/session-presentation-service.d.ts +1 -0
  67. package/dist/service/session-presentation-service.js +8 -1
  68. package/dist/service/session-query-service.d.ts +4 -0
  69. package/dist/service/session-query-service.js +6 -1
  70. package/dist/service/skill-directory-service.d.ts +3 -1
  71. package/dist/service/skill-directory-service.js +31 -30
  72. package/dist/service/skill-install-service.js +4 -74
  73. package/dist/service/skill-node-operation-service.d.ts +1 -1
  74. package/dist/service/skill-node-operation-service.js +6 -3
  75. package/dist/service/workbench-event-service.d.ts +1 -13
  76. package/dist/service/workbench-event-service.js +2 -36
  77. package/dist/service/workbench-manifest-service.d.ts +0 -1
  78. package/dist/service/workbench-manifest-service.js +0 -1
  79. package/dist/service/workspace-domain-state.d.ts +1 -0
  80. package/dist/service/workspace-git-exclude-service.d.ts +4 -0
  81. package/dist/service/workspace-git-exclude-service.js +119 -0
  82. package/dist/service/workspace-queue-workbench-service.d.ts +1 -0
  83. package/dist/service/workspace-queue-workbench-service.js +4 -2
  84. package/dist/service/workspace-service.js +2 -0
  85. package/dist/service/workspace-upload-service.d.ts +2 -0
  86. package/dist/service/workspace-upload-service.js +28 -6
  87. package/dist/service/workspace-workbench-service.d.ts +0 -3
  88. package/dist/service/workspace-workbench-service.js +0 -17
  89. package/dist/supervisor.js +2 -1
  90. package/dist/terminal.js +1 -1
  91. package/dist/util/node-operation-parsers.js +2 -2
  92. package/dist/util/runner-error.d.ts +6 -0
  93. package/dist/util/runner-error.js +52 -0
  94. package/dist/util/runner-native-session-parsers.d.ts +2 -3
  95. package/dist/util/runner-native-session-parsers.js +48 -20
  96. package/dist/util/safe-error.d.ts +2 -0
  97. package/dist/util/safe-error.js +5 -0
  98. package/package.json +2 -2
@@ -0,0 +1,156 @@
1
+ const TURN_PAGE_SIZE = 30;
2
+ export function conversationSegments(turn) {
3
+ const groups = [];
4
+ let current = [];
5
+ for (const item of turn.items) {
6
+ if (current.at(-1)?.kind === 'assistant_message' && item.kind !== 'assistant_message') {
7
+ groups.push(current);
8
+ current = [];
9
+ }
10
+ current.push(item);
11
+ }
12
+ if (current.length > 0 || groups.length === 0)
13
+ groups.push(current);
14
+ return groups.map((items, index) => {
15
+ const id = `${turn.id}:segment:${index}`;
16
+ const latest = index === groups.length - 1;
17
+ const projectedItems = items.map((item) => ({
18
+ ...item,
19
+ segmentId: id,
20
+ segmentIndex: index
21
+ }));
22
+ return {
23
+ id,
24
+ sessionId: turn.sessionId,
25
+ turnId: turn.id,
26
+ runId: turn.runId,
27
+ index,
28
+ status: (latest ? turn.status : 'SUCCEEDED'),
29
+ ...(latest && turn.rewind !== undefined ? { rewind: turn.rewind } : {}),
30
+ items: projectedItems,
31
+ startedAt: projectedItems[0]?.startedAt ?? turn.startedAt,
32
+ completedAt: projectedItems.at(-1)?.completedAt ?? null
33
+ };
34
+ });
35
+ }
36
+ export function conversationSegmentIdentityForItem(turn, itemId, itemKind) {
37
+ let index = 0;
38
+ const runtimeItemId = turn.runId === null ? itemId : `${turn.runId}:${itemId}`;
39
+ for (const [itemIndex, item] of turn.items.entries()) {
40
+ const previous = turn.items[itemIndex - 1];
41
+ if (previous?.kind === 'assistant_message' && item.kind !== 'assistant_message')
42
+ index += 1;
43
+ if (item.id === itemId || item.id === runtimeItemId)
44
+ return { id: `${turn.id}:segment:${index}`, index };
45
+ }
46
+ if (turn.items.at(-1)?.kind === 'assistant_message' && itemKind !== 'assistant_message')
47
+ index += 1;
48
+ return { id: `${turn.id}:segment:${index}`, index };
49
+ }
50
+ export class ConversationSegmentService {
51
+ readTurns;
52
+ constructor(readTurns) {
53
+ this.readTurns = readTurns;
54
+ }
55
+ projectInitial(session, page, limit) {
56
+ const all = page.turns.flatMap(conversationSegments);
57
+ const segments = all.slice(-limit);
58
+ return {
59
+ snapshotSequence: page.snapshotSequence,
60
+ source: page.source,
61
+ readAt: page.readAt,
62
+ latestVisibleAt: page.latestVisibleAt,
63
+ unit: 'SEGMENT',
64
+ segments,
65
+ nextCursor: segments.length === 0 || (segments.length === all.length && page.nextCursor === null)
66
+ ? null
67
+ : encodeCursor({
68
+ version: 1,
69
+ sessionId: session.id,
70
+ beforeSegmentId: segments[0].id
71
+ })
72
+ };
73
+ }
74
+ async read(session, input) {
75
+ const limit = Math.min(Math.max(input.limit ?? 30, 1), 500);
76
+ const boundary = input.cursor === undefined ? undefined : decodeCursor(input.cursor, session.id);
77
+ const collected = [];
78
+ const visited = new Set();
79
+ let turnCursor;
80
+ let locatingBoundary = boundary !== undefined;
81
+ let hasOlder = false;
82
+ let metadata;
83
+ while (collected.length < limit) {
84
+ const page = await this.readTurns(session, {
85
+ ...(turnCursor === undefined ? {} : { cursor: turnCursor }),
86
+ limit: TURN_PAGE_SIZE
87
+ });
88
+ metadata ??= {
89
+ snapshotSequence: page.snapshotSequence,
90
+ source: page.source,
91
+ readAt: page.readAt,
92
+ latestVisibleAt: page.latestVisibleAt
93
+ };
94
+ const pageSegments = page.turns.flatMap(conversationSegments);
95
+ let eligible = pageSegments;
96
+ if (locatingBoundary) {
97
+ const boundaryIndex = pageSegments.findIndex((segment) => segment.id === boundary?.beforeSegmentId);
98
+ if (boundaryIndex >= 0) {
99
+ eligible = pageSegments.slice(0, boundaryIndex);
100
+ locatingBoundary = false;
101
+ }
102
+ else
103
+ eligible = [];
104
+ }
105
+ if (!locatingBoundary && eligible.length > 0) {
106
+ const remaining = Math.max(0, limit - collected.length);
107
+ const take = eligible.slice(-remaining);
108
+ hasOlder = eligible.length > take.length || page.nextCursor !== null;
109
+ collected.unshift(...take);
110
+ }
111
+ if (collected.length >= limit || page.nextCursor === null)
112
+ break;
113
+ if (visited.has(page.nextCursor))
114
+ throw new Error('EVENT_CURSOR_INVALID');
115
+ visited.add(page.nextCursor);
116
+ turnCursor = page.nextCursor;
117
+ }
118
+ if (locatingBoundary)
119
+ throw new Error('EVENT_CURSOR_INVALID');
120
+ const nextCursor = collected.length === 0 || !hasOlder
121
+ ? null
122
+ : encodeCursor({
123
+ version: 1,
124
+ sessionId: session.id,
125
+ beforeSegmentId: collected[0].id
126
+ });
127
+ return {
128
+ ...(metadata ?? {
129
+ snapshotSequence: 0,
130
+ source: 'runtime',
131
+ readAt: Date.now(),
132
+ latestVisibleAt: 0
133
+ }),
134
+ unit: 'SEGMENT',
135
+ segments: collected,
136
+ nextCursor
137
+ };
138
+ }
139
+ }
140
+ function encodeCursor(cursor) {
141
+ return Buffer.from(JSON.stringify(cursor)).toString('base64url');
142
+ }
143
+ function decodeCursor(value, sessionId) {
144
+ try {
145
+ const decoded = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
146
+ if (decoded.version !== 1 ||
147
+ decoded.sessionId !== sessionId ||
148
+ typeof decoded.beforeSegmentId !== 'string' ||
149
+ decoded.beforeSegmentId.length === 0)
150
+ throw new Error('EVENT_CURSOR_INVALID');
151
+ return decoded;
152
+ }
153
+ catch {
154
+ throw new Error('EVENT_CURSOR_INVALID');
155
+ }
156
+ }
@@ -1,3 +1,4 @@
1
+ const RESUME_FAILURE_TTL_MS = 30 * 60_000;
1
2
  export class ExternalSessionResumeService {
2
3
  options;
3
4
  constructor(options) {
@@ -84,15 +85,23 @@ export class ExternalSessionResumeService {
84
85
  : 'REJECTED';
85
86
  }
86
87
  clearFailure(sessionId) {
88
+ const timer = this.options.state.failureTimers.get(sessionId);
89
+ if (timer !== undefined)
90
+ clearTimeout(timer);
91
+ this.options.state.failureTimers.delete(sessionId);
87
92
  this.options.state.resumeFailures.delete(sessionId);
88
93
  this.options.state.resumeFailureDetails.delete(sessionId);
89
94
  this.options.state.messageResumeFailures.delete(sessionId);
90
95
  }
91
96
  setFailure(sessionId, code, detail) {
97
+ this.clearFailure(sessionId);
92
98
  this.options.state.resumeFailures.set(sessionId, code);
93
99
  if (detail === undefined)
94
100
  this.options.state.resumeFailureDetails.delete(sessionId);
95
101
  else
96
102
  this.options.state.resumeFailureDetails.set(sessionId, detail);
103
+ const timer = setTimeout(() => this.clearFailure(sessionId), RESUME_FAILURE_TTL_MS);
104
+ timer.unref();
105
+ this.options.state.failureTimers.set(sessionId, timer);
97
106
  }
98
107
  }
@@ -17,7 +17,6 @@ interface FakeManagedRunServiceOptions {
17
17
  readonly payload: unknown;
18
18
  readonly expiresAt: number;
19
19
  }) => void;
20
- readonly captureSnapshot: (runId: string, cwd: string) => Promise<void>;
21
20
  readonly disconnect: () => void;
22
21
  }
23
22
  /** Test-only managed Run adapter; production Runner paths never depend on it. */
@@ -32,8 +32,6 @@ export class FakeManagedRunService {
32
32
  this.options.emitRun(runId, eventType, eventPayload, status);
33
33
  if (status !== undefined && isTerminalRunStatus(status)) {
34
34
  this.options.state.active.delete(runId);
35
- if (cwd !== undefined)
36
- void this.options.captureSnapshot(runId, cwd);
37
35
  }
38
36
  },
39
37
  approval: (approvalId, approvalPayload, expiresAt) => {
@@ -4,7 +4,7 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
4
4
  const INSTALL_TIMEOUT_MS = 90_000;
5
5
  export class McpInstallationVerifier {
6
6
  async verify(input) {
7
- const client = new Client({ name: 'myagentroam-node-installer', version: '0.9.0' });
7
+ const client = new Client({ name: 'myagentroam-node-installer', version: '0.9.1' });
8
8
  try {
9
9
  await withTimeout((async () => {
10
10
  if (input.entry.runtime.transport === 'STDIO') {
@@ -238,7 +238,12 @@ export class NativeSessionProjectionService {
238
238
  this.options.state.externalActivity.delete(directId);
239
239
  this.options.state.resumedByDirectId.delete(directId);
240
240
  this.options.state.resumeFailures.delete(directId);
241
+ this.options.state.messageResumeFailures.delete(directId);
241
242
  this.options.state.resumeFailureDetails.delete(directId);
243
+ const timer = this.options.state.failureTimers.get(directId);
244
+ if (timer !== undefined)
245
+ clearTimeout(timer);
246
+ this.options.state.failureTimers.delete(directId);
242
247
  }
243
248
  }
244
249
  for (const [directId, resumedId] of this.options.state.resumedByDirectId)
@@ -8,6 +8,7 @@ export interface NativeSessionWatchOptions<TPage extends {
8
8
  readonly refreshActivity: (session: NodeAgentSession) => Promise<void>;
9
9
  readonly present: (session: NodeAgentSession) => unknown;
10
10
  readonly readPage: (session: NodeAgentSession, limit: number) => Promise<TPage>;
11
+ readonly projectInitialPage?: (session: NodeAgentSession, page: TPage, unit: 'SEGMENT', limit: number) => unknown;
11
12
  readonly emitSession: (session: unknown) => void;
12
13
  readonly emitTurn: (turn: unknown) => void;
13
14
  }
@@ -15,7 +15,12 @@ export class NativeSessionWatchService {
15
15
  };
16
16
  }
17
17
  async executeWatch(input) {
18
- if (typeof input.sessionId !== 'string' || (input.mode !== 'initial' && input.mode !== 'renew'))
18
+ const projectInitialPage = this.options.projectInitialPage;
19
+ if (input.unit === 'SEGMENT' && projectInitialPage === undefined)
20
+ throw new Error('SESSION_INVALID');
21
+ if (typeof input.sessionId !== 'string' ||
22
+ (input.mode !== 'initial' && input.mode !== 'renew') ||
23
+ (input.unit !== undefined && input.unit !== 'SEGMENT'))
19
24
  throw new Error('SESSION_INVALID');
20
25
  const session = await this.options.resolve(input.sessionId);
21
26
  if (session === undefined || session.externalSessionId === null)
@@ -27,9 +32,21 @@ export class NativeSessionWatchService {
27
32
  return { renewed: true };
28
33
  }
29
34
  const watch = this.watch(session);
30
- const snapshot = watch === undefined ? undefined : await this.refresh(session.id, INITIAL_TURN_LIMIT);
31
- if (watch !== undefined && snapshot === undefined)
35
+ const turnSnapshot = watch === undefined ? undefined : await this.refresh(session.id, INITIAL_TURN_LIMIT);
36
+ if (watch !== undefined && turnSnapshot === undefined) {
37
+ this.stop(session.id);
32
38
  throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
39
+ }
40
+ let snapshot = turnSnapshot;
41
+ if (turnSnapshot !== undefined && input.unit === 'SEGMENT') {
42
+ try {
43
+ snapshot = projectInitialPage(session, turnSnapshot, 'SEGMENT', 10);
44
+ }
45
+ catch (error) {
46
+ this.stop(session.id);
47
+ throw error;
48
+ }
49
+ }
33
50
  return {
34
51
  session: this.options.present(session),
35
52
  watch: watch === undefined ? null : { expiresAt: watch.expiresAt },
@@ -131,7 +148,7 @@ export class NativeSessionWatchService {
131
148
  }
132
149
  const page = await this.options.readPage(session, limit);
133
150
  watch.failures = 0;
134
- const turns = page.turns.length <= 10 ? page.turns : page.turns.slice(0, 10);
151
+ const turns = page.turns.length <= 10 ? page.turns : page.turns.slice(-10);
135
152
  const signature = JSON.stringify(turns);
136
153
  if (signature === watch.signature)
137
154
  return page;
@@ -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') {
@@ -1,5 +1,6 @@
1
1
  import { nodeLog } from '../operational.js';
2
2
  import { validUserAuthorization } from '../runner/codex/conversation-parser.js';
3
+ import { safeErrorCode } from '../util/safe-error.js';
3
4
  /** Validates and executes the closed Admin-facing Node operation envelope. */
4
5
  export class NodeRequestService {
5
6
  options;
@@ -44,7 +45,7 @@ export class NodeRequestService {
44
45
  durationMs: Math.round(performance.now() - startedAt),
45
46
  error: error instanceof Error ? error.message : 'INTERNAL_ERROR'
46
47
  });
47
- this.options.send('node.response', { error: error instanceof Error ? error.message : 'INTERNAL_ERROR' }, envelope.id);
48
+ this.options.send('node.response', { error: safeErrorCode(error, 'INTERNAL_ERROR') }, envelope.id);
48
49
  }
49
50
  }
50
51
  }
@@ -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
  }
@@ -11,14 +11,15 @@ export interface NativeImageSource {
11
11
  readonly name: string;
12
12
  readonly mime: string;
13
13
  readonly path: string;
14
+ readonly root: string;
14
15
  }
15
16
  export type RunnerImageAttachment = NodeMessageAttachmentInput & {
16
17
  readonly mime: 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp';
17
18
  };
18
19
  export declare class RunAttachmentService {
19
- private readonly nativeImageRoot;
20
+ private readonly nativeImageRoots;
20
21
  readonly state: AttachmentDomainState<CachedRunImages, NativeImageSource>;
21
- constructor(nativeImageRoot: (session: NodeAgentSession) => string | undefined);
22
+ constructor(nativeImageRoots: (session: NodeAgentSession) => readonly string[]);
22
23
  materializeCodex(runId: string, attachments: readonly RunnerImageAttachment[]): Promise<readonly Extract<CodexComposerInput, {
23
24
  readonly type: 'localImage';
24
25
  }>[]>;
@@ -1,14 +1,15 @@
1
1
  import { tmpdir } from 'node:os';
2
2
  import { isAbsolute, join, relative, resolve } from 'node:path';
3
- import { lstat, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { lstat, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises';
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
- nativeImageRoot;
9
+ nativeImageRoots;
9
10
  state = new AttachmentDomainState();
10
- constructor(nativeImageRoot) {
11
- this.nativeImageRoot = nativeImageRoot;
11
+ constructor(nativeImageRoots) {
12
+ this.nativeImageRoots = nativeImageRoots;
12
13
  }
13
14
  async materializeCodex(runId, attachments) {
14
15
  if (attachments.length === 0)
@@ -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)
@@ -95,8 +103,8 @@ export class RunAttachmentService {
95
103
  return cached.images[imageIndex];
96
104
  }
97
105
  registerNative(session, images) {
98
- const root = this.nativeImageRoot(session);
99
- if (root === undefined)
106
+ const roots = this.nativeImageRoots(session).map((root) => resolve(root));
107
+ if (roots.length === 0)
100
108
  return [];
101
109
  const runId = `native-history:${session.id}`;
102
110
  const sources = this.state.nativeHistoryImages.get(runId) ?? [];
@@ -107,20 +115,33 @@ export class RunAttachmentService {
107
115
  continue;
108
116
  }
109
117
  const path = resolve(image.localPath);
110
- const relation = relative(root, path);
111
- if (relation.length === 0 || relation.startsWith('..') || isAbsolute(relation)) {
118
+ const root = roots.find((candidate) => {
119
+ const relation = relative(candidate, path);
120
+ return relation.length > 0 && !relation.startsWith('..') && !isAbsolute(relation);
121
+ });
122
+ if (root === undefined) {
112
123
  attachments.push({ name: image.name, mime: image.mime });
113
124
  continue;
114
125
  }
115
126
  let imageIndex = sources.findIndex((source) => source.path === path);
116
127
  if (imageIndex < 0) {
117
128
  imageIndex = sources.length;
118
- sources.push({ name: image.name, mime: image.mime, path });
129
+ sources.push({ name: image.name, mime: image.mime, path, root });
119
130
  }
120
131
  attachments.push({ name: image.name, mime: image.mime, runId, imageIndex });
121
132
  }
122
- if (sources.length > 0)
133
+ if (sources.length > 0) {
123
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
+ }
124
145
  return attachments;
125
146
  }
126
147
  async readNative(runId, imageIndex) {
@@ -128,10 +149,17 @@ export class RunAttachmentService {
128
149
  if (source === undefined)
129
150
  return undefined;
130
151
  try {
131
- const info = await lstat(source.path);
152
+ const [actualRoot, actualPath] = await Promise.all([
153
+ realpath(source.root),
154
+ realpath(source.path)
155
+ ]);
156
+ const relation = relative(actualRoot, actualPath);
157
+ if (relation.length === 0 || relation.startsWith('..') || isAbsolute(relation))
158
+ return undefined;
159
+ const info = await lstat(actualPath);
132
160
  if (!info.isFile() || info.size <= 0 || info.size > MAX_ATTACHMENT_BYTES)
133
161
  return undefined;
134
- return { ...source, dataBase64: (await readFile(source.path)).toString('base64') };
162
+ return { ...source, dataBase64: (await readFile(actualPath)).toString('base64') };
135
163
  }
136
164
  catch {
137
165
  return undefined;
@@ -151,10 +179,16 @@ export class RunAttachmentService {
151
179
  this.state.runImages.delete(runId);
152
180
  }
153
181
  dispose() {
154
- 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
+ ]))
155
186
  this.cleanup(runId);
156
187
  for (const runId of this.state.runImages.keys())
157
188
  this.clear(runId);
189
+ for (const timer of this.state.nativeHistoryImageTimers.values())
190
+ clearTimeout(timer);
191
+ this.state.nativeHistoryImageTimers.clear();
158
192
  this.state.nativeHistoryImages.clear();
159
193
  }
160
194
  }
@@ -25,7 +25,9 @@ export declare class RunEventService {
25
25
  constructor(options: RunEventServiceOptions);
26
26
  emit(runId: string, eventType: string, payload: unknown, status?: FakeRunnerStatus): void;
27
27
  clear(): void;
28
+ flushPendingText(runId: string): void;
28
29
  private enqueueText;
29
30
  private flushText;
30
31
  private emitNow;
32
+ private notificationPayload;
31
33
  }