@myagentroam/node 0.9.2 → 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 (69) hide show
  1. package/dist/connector.js +12 -13
  2. package/dist/control-outbound-scheduler.d.ts +4 -1
  3. package/dist/control-outbound-scheduler.js +39 -6
  4. package/dist/database.d.ts +0 -19
  5. package/dist/database.js +1 -62
  6. package/dist/event-buffer.d.ts +2 -5
  7. package/dist/event-buffer.js +3 -12
  8. package/dist/native-session-history.js +33 -12
  9. package/dist/runner/claude/managed-run-controller.d.ts +0 -1
  10. package/dist/runner/claude/managed-run-controller.js +17 -12
  11. package/dist/runner/codex/conversation-parser.d.ts +1 -1
  12. package/dist/runner/codex/conversation-parser.js +3 -0
  13. package/dist/runner/codex/managed-run-controller.d.ts +2 -2
  14. package/dist/runner/codex/managed-run-controller.js +22 -7
  15. package/dist/runner/codex-runner.d.ts +2 -0
  16. package/dist/runner/codex-runner.js +30 -2
  17. package/dist/runner/opencode/conversation-parser.js +1 -1
  18. package/dist/runner/opencode/managed-run-controller.d.ts +7 -2
  19. package/dist/runner/opencode/managed-run-controller.js +72 -14
  20. package/dist/runner/opencode-runner.d.ts +3 -0
  21. package/dist/runner/opencode-runner.js +25 -0
  22. package/dist/runner-profiles.js +10 -6
  23. package/dist/runtime-state.d.ts +5 -12
  24. package/dist/runtime-state.js +32 -46
  25. package/dist/service/attachment-domain-state.d.ts +2 -0
  26. package/dist/service/attachment-domain-state.js +2 -0
  27. package/dist/service/conversation-segment-service.js +10 -9
  28. package/dist/service/external-session-resume-service.js +9 -0
  29. package/dist/service/fake-managed-run-service.d.ts +0 -1
  30. package/dist/service/fake-managed-run-service.js +0 -2
  31. package/dist/service/native-session-projection-service.js +5 -0
  32. package/dist/service/node-connection-lifecycle-service.d.ts +0 -2
  33. package/dist/service/node-connection-lifecycle-service.js +0 -1
  34. package/dist/service/node-control-channel.js +1 -1
  35. package/dist/service/node-control-message-service.d.ts +0 -1
  36. package/dist/service/node-control-message-service.js +9 -6
  37. package/dist/service/node-workspace-coordinator.d.ts +1 -4
  38. package/dist/service/node-workspace-coordinator.js +2 -12
  39. package/dist/service/run-attachment-service.js +30 -6
  40. package/dist/service/run-event-service.d.ts +1 -0
  41. package/dist/service/run-event-service.js +69 -0
  42. package/dist/service/run-workbench-service.d.ts +1 -1
  43. package/dist/service/run-workbench-service.js +1 -5
  44. package/dist/service/runner-interaction-service.d.ts +1 -2
  45. package/dist/service/runner-interaction-service.js +1 -4
  46. package/dist/service/session-context-service.d.ts +1 -0
  47. package/dist/service/session-context-service.js +14 -0
  48. package/dist/service/session-domain-state.d.ts +1 -0
  49. package/dist/service/session-domain-state.js +1 -0
  50. package/dist/service/session-message-service.js +5 -10
  51. package/dist/service/skill-directory-service.d.ts +3 -1
  52. package/dist/service/skill-directory-service.js +28 -4
  53. package/dist/service/skill-node-operation-service.d.ts +1 -1
  54. package/dist/service/skill-node-operation-service.js +4 -2
  55. package/dist/service/workbench-event-service.d.ts +1 -13
  56. package/dist/service/workbench-event-service.js +2 -36
  57. package/dist/service/workbench-manifest-service.d.ts +0 -1
  58. package/dist/service/workbench-manifest-service.js +0 -1
  59. package/dist/service/workspace-domain-state.d.ts +1 -0
  60. package/dist/service/workspace-queue-workbench-service.d.ts +1 -0
  61. package/dist/service/workspace-queue-workbench-service.js +1 -0
  62. package/dist/service/workspace-upload-service.d.ts +2 -0
  63. package/dist/service/workspace-upload-service.js +28 -6
  64. package/dist/service/workspace-workbench-service.d.ts +0 -3
  65. package/dist/service/workspace-workbench-service.js +0 -17
  66. package/dist/util/runner-error.d.ts +6 -0
  67. package/dist/util/runner-error.js +52 -0
  68. package/dist/util/runner-native-session-parsers.js +43 -6
  69. package/package.json +2 -2
@@ -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
  }
@@ -68,11 +68,13 @@ export class SessionMessageService {
68
68
  this.options.attachments.cache(created.run.id, imageAttachments);
69
69
  let attachmentPaths;
70
70
  try {
71
- attachmentPaths = await this.persistAttachments(session, created.run.id, attachmentIds, attachments, uploaded);
71
+ attachmentPaths = await this.persistAttachments(session, created.run.id, attachments);
72
+ await this.options.uploads.consume(attachmentIds);
72
73
  }
73
74
  catch (error) {
74
75
  this.options.runtime.deleteQueuedRun(created.run.id);
75
76
  this.options.attachments.clear(created.run.id);
77
+ this.options.attachments.cleanup(created.run.id);
76
78
  throw error;
77
79
  }
78
80
  const runnerInput = withNonImageAttachmentPrompt(input.content, attachmentPaths);
@@ -117,15 +119,8 @@ export class SessionMessageService {
117
119
  idempotent: false
118
120
  };
119
121
  }
120
- async persistAttachments(session, runId, attachmentIds, attachments, uploaded) {
121
- return [
122
- ...uploaded
123
- .filter(({ attachment }) => !isRunnerImageAttachment(attachment))
124
- .map(({ path }) => path),
125
- ...(await this.options.attachments.persistFiles(session.cwd, runId, attachmentIds.length === 0
126
- ? attachments.filter((attachment) => !isRunnerImageAttachment(attachment))
127
- : []))
128
- ];
122
+ async persistAttachments(session, runId, attachments) {
123
+ return this.options.attachments.persistFiles(session.cwd, runId, attachments.filter((attachment) => !isRunnerImageAttachment(attachment)));
129
124
  }
130
125
  channelMessage(input) {
131
126
  if (typeof input.sessionId !== 'string' ||
@@ -12,6 +12,8 @@ export interface LocalSkillTargetInspection {
12
12
  readonly skills: readonly LocalSkillInspection[];
13
13
  }
14
14
  export declare class SkillDirectoryService {
15
- inspectNodeHome(home: string): Promise<LocalSkillTargetInspection>;
15
+ inspectNodeHome(home: string, options?: {
16
+ readonly includeClaudeCompatibility?: boolean;
17
+ }): Promise<LocalSkillTargetInspection>;
16
18
  inspectWorkspace(workspace: string): Promise<LocalSkillTargetInspection>;
17
19
  }
@@ -6,14 +6,20 @@ const MAX_METADATA_BYTES = 64 * 1024;
6
6
  const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
7
7
  const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/u;
8
8
  export class SkillDirectoryService {
9
- async inspectNodeHome(home) {
9
+ async inspectNodeHome(home, options = {}) {
10
10
  const agentsRoot = path.join(home, '.agents', 'skills');
11
11
  const claudeRoot = path.join(home, '.claude', 'skills');
12
- const skills = await scanRoot(agentsRoot, 'AGENTS');
12
+ const compatibilityMode = await nodeCompatibilityMode(agentsRoot, claudeRoot);
13
+ const agents = await scanRoot(agentsRoot, 'AGENTS');
14
+ const claude = options.includeClaudeCompatibility === true && compatibilityMode !== 'UNAVAILABLE'
15
+ ? await scanRoot(claudeRoot, 'CLAUDE')
16
+ : [];
13
17
  return {
14
18
  targetKind: 'NODE',
15
- compatibilityMode: await nodeCompatibilityMode(agentsRoot, claudeRoot),
16
- skills
19
+ compatibilityMode,
20
+ skills: options.includeClaudeCompatibility === true
21
+ ? mergeNodeCompatibilityRoots(agents, claude)
22
+ : agents.map(withoutSource)
17
23
  };
18
24
  }
19
25
  async inspectWorkspace(workspace) {
@@ -147,6 +153,24 @@ function mergeWorkspaceCopies(agents, claude) {
147
153
  return withoutSource(copies.agents);
148
154
  });
149
155
  }
156
+ function mergeNodeCompatibilityRoots(agents, claude) {
157
+ const byName = new Map();
158
+ for (const item of agents)
159
+ byName.set(item.name, { agents: item });
160
+ for (const item of claude)
161
+ byName.set(item.name, { ...byName.get(item.name), claude: item });
162
+ return [...byName.entries()]
163
+ .sort(([left], [right]) => left.localeCompare(right))
164
+ .map(([name, copies]) => {
165
+ if (copies.agents === undefined || copies.claude === undefined)
166
+ return withoutSource((copies.agents ?? copies.claude));
167
+ if (copies.agents.description !== copies.claude.description ||
168
+ copies.agents.localStatus !== copies.claude.localStatus ||
169
+ JSON.stringify(copies.agents.install) !== JSON.stringify(copies.claude.install))
170
+ return { name, description: copies.agents.description, localStatus: 'PARTIAL' };
171
+ return withoutSource(copies.agents);
172
+ });
173
+ }
150
174
  function withoutSource(entry) {
151
175
  return {
152
176
  name: entry.name,
@@ -9,7 +9,7 @@ export declare class SkillNodeOperationService {
9
9
  private readonly jobs;
10
10
  constructor(database: () => NodeDatabase, config: () => NodeConfig);
11
11
  operations(): {
12
- 'node.skills.inspect': () => Promise<import("./skill-directory-service.js").LocalSkillTargetInspection>;
12
+ 'node.skills.inspect': (data: unknown) => Promise<import("./skill-directory-service.js").LocalSkillTargetInspection>;
13
13
  'node.skills.install': (data: unknown) => Promise<{
14
14
  accepted: boolean;
15
15
  installJobId: `${string}-${string}-${string}-${string}-${string}`;
@@ -17,9 +17,11 @@ export class SkillNodeOperationService {
17
17
  }
18
18
  operations() {
19
19
  return {
20
- 'node.skills.inspect': async () => {
20
+ 'node.skills.inspect': async (data) => {
21
21
  await this.installer.recoverNodeHome(homedir());
22
- return this.directories.inspectNodeHome(homedir());
22
+ return this.directories.inspectNodeHome(homedir(), {
23
+ includeClaudeCompatibility: record(data)['includeClaudeCompatibility'] === true
24
+ });
23
25
  },
24
26
  'node.skills.install': (data) => this.install('NODE', data),
25
27
  'node.skills.remove': (data) => this.remove('NODE', data),
@@ -1,21 +1,9 @@
1
- import type { NodeEnvelope, WorkbenchEvent, WorkbenchEventCategory } from '@myagentroam/protocol';
2
- import type { NodeOperationHandler } from '../connector/node-operation-router.js';
1
+ import { type NodeEnvelope, type WorkbenchEventCategory } from '@myagentroam/protocol';
3
2
  export declare class WorkbenchEventService {
4
3
  private readonly send;
5
4
  private readonly accepts;
6
- private readonly buffer;
7
5
  private sequence;
8
- private readonly history;
9
6
  constructor(send: (envelope: NodeEnvelope) => void, accepts: (category: WorkbenchEventCategory, payload: unknown) => boolean);
10
7
  emit(category: WorkbenchEventCategory, payload: unknown): void;
11
- read(after: number): {
12
- readonly events: readonly WorkbenchEvent[];
13
- readonly latestSequence: number;
14
- readonly resyncRequired: boolean;
15
- };
16
- operations(): Readonly<Record<string, NodeOperationHandler>>;
17
8
  snapshotSequence(): number;
18
- acknowledge(replyTo: string | undefined): boolean;
19
- replay(): void;
20
- get pendingCount(): number;
21
9
  }
@@ -1,10 +1,8 @@
1
- import { ReliableWorkbenchEventBuffer } from '../event-buffer.js';
1
+ import { createEnvelope } from '@myagentroam/protocol';
2
2
  export class WorkbenchEventService {
3
3
  send;
4
4
  accepts;
5
- buffer = new ReliableWorkbenchEventBuffer();
6
5
  sequence = 0;
7
- history = [];
8
6
  constructor(send, accepts) {
9
7
  this.send = send;
10
8
  this.accepts = accepts;
@@ -13,41 +11,9 @@ export class WorkbenchEventService {
13
11
  if (!this.accepts(category, payload))
14
12
  return;
15
13
  const event = { sequence: ++this.sequence, category, payload };
16
- this.history.push(event);
17
- if (this.history.length > 1_000)
18
- this.history.shift();
19
- this.send(this.buffer.queue(event));
20
- }
21
- read(after) {
22
- const first = this.history[0]?.sequence;
23
- const reset = after > this.sequence || (first !== undefined && after < first - 1);
24
- return {
25
- events: reset ? [] : this.history.filter((event) => event.sequence > after),
26
- latestSequence: this.sequence,
27
- resyncRequired: reset
28
- };
29
- }
30
- operations() {
31
- return {
32
- 'workbench.events.read': (data) => {
33
- const afterValue = data?.after;
34
- const after = typeof afterValue === 'number' && Number.isInteger(afterValue) && afterValue >= 0
35
- ? afterValue
36
- : 0;
37
- return this.read(after);
38
- }
39
- };
14
+ this.send(createEnvelope('workbench.event', event));
40
15
  }
41
16
  snapshotSequence() {
42
17
  return this.sequence;
43
18
  }
44
- acknowledge(replyTo) {
45
- return this.buffer.acknowledge(replyTo);
46
- }
47
- replay() {
48
- this.buffer.replay(this.send);
49
- }
50
- get pendingCount() {
51
- return this.buffer.size;
52
- }
53
19
  }
@@ -21,7 +21,6 @@ export declare class WorkbenchManifestService {
21
21
  reasonCode: null;
22
22
  itemVersions: number[];
23
23
  supportsSessionCursor: boolean;
24
- supportsWorkspaceActivityCursor: boolean;
25
24
  supportsHistoryPaging: boolean;
26
25
  supportsItemFinalSnapshots: boolean;
27
26
  maxPageSize: number;
@@ -30,7 +30,6 @@ export class WorkbenchManifestService {
30
30
  reasonCode: null,
31
31
  itemVersions: [1],
32
32
  supportsSessionCursor: true,
33
- supportsWorkspaceActivityCursor: true,
34
33
  supportsHistoryPaging: true,
35
34
  supportsItemFinalSnapshots: true,
36
35
  maxPageSize: 500
@@ -42,6 +42,7 @@ export interface CompletedComposerAttachment {
42
42
  readonly name: string;
43
43
  readonly mime: string;
44
44
  readonly size: number;
45
+ readonly timer: NodeJS.Timeout;
45
46
  }
46
47
  export declare class WorkspaceDomainState {
47
48
  readonly uploads: Map<string, ActiveWorkspaceUpload>;
@@ -23,6 +23,7 @@ export interface WorkspaceQueueWorkbenchServiceOptions {
23
23
  readonly runtime: NodeRuntimeState;
24
24
  readonly emit: (workspaceId: string, sessionId: string) => void;
25
25
  readonly clearImages: (runId: string) => void;
26
+ readonly cleanupAttachments: (runId: string) => void;
26
27
  readonly emitRun: (runId: string, eventType: string, payload: unknown, status?: 'FAILED' | 'CANCELLED') => void;
27
28
  readonly emitConversation: (turn: unknown) => void;
28
29
  readonly control: (operation: string, payload: Record<string, unknown>) => void;
@@ -80,6 +80,7 @@ export class WorkspaceQueueWorkbenchService {
80
80
  this.options.runtime.deleteQueuedRun(run.id);
81
81
  this.remove(run.id);
82
82
  this.options.clearImages(run.id);
83
+ this.options.cleanupAttachments(run.id);
83
84
  this.options.emit(run.workspaceId, run.sessionId);
84
85
  return { queue: this.present(run.workspaceId, run.sessionId) };
85
86
  }
@@ -23,9 +23,11 @@ export declare class WorkspaceUploadService {
23
23
  complete(uploadId: unknown): Promise<UploadStatus>;
24
24
  cancel(uploadId: unknown): Promise<void>;
25
25
  resolve(workspaceId: string, ids: readonly string[]): Promise<readonly ResolvedComposerAttachment[]>;
26
+ consume(ids: readonly string[]): Promise<void>;
26
27
  shutdown(): Promise<void>;
27
28
  private require;
28
29
  private remove;
30
+ private removeCompleted;
29
31
  private attachmentTarget;
30
32
  }
31
33
  interface UploadStatus {
@@ -133,16 +133,25 @@ export class WorkspaceUploadService {
133
133
  clearTimeout(upload.timer);
134
134
  this.state.uploads.delete(upload.id);
135
135
  this.onCompleted(upload.workspaceId);
136
- if (upload.composerAttachment !== undefined)
137
- this.state.composerAttachments.set(upload.composerAttachment.id, {
136
+ if (upload.composerAttachment !== undefined) {
137
+ const attachmentId = upload.composerAttachment.id;
138
+ const timer = setTimeout(() => {
139
+ const attachment = this.state.composerAttachments.get(attachmentId);
140
+ if (attachment !== undefined)
141
+ void this.removeCompleted(attachment);
142
+ }, TTL_MS);
143
+ timer.unref();
144
+ this.state.composerAttachments.set(attachmentId, {
138
145
  id: upload.composerAttachment.id,
139
146
  workspaceId: upload.workspaceId,
140
147
  path: upload.path,
141
148
  targetPath: upload.targetPath,
142
149
  name: upload.composerAttachment.name,
143
150
  mime: upload.composerAttachment.mime,
144
- size: upload.size
151
+ size: upload.size,
152
+ timer
145
153
  });
154
+ }
146
155
  return {
147
156
  uploadId: upload.id,
148
157
  path: upload.path,
@@ -159,8 +168,7 @@ export class WorkspaceUploadService {
159
168
  const attachment = this.state.composerAttachments.get(uploadId);
160
169
  if (attachment === undefined)
161
170
  throw new Error('UPLOAD_NOT_FOUND');
162
- this.state.composerAttachments.delete(uploadId);
163
- await rm(dirname(attachment.targetPath), { recursive: true, force: true });
171
+ await this.removeCompleted(attachment);
164
172
  }
165
173
  async resolve(workspaceId, ids) {
166
174
  return Promise.all(ids.map(async (id) => {
@@ -184,10 +192,17 @@ export class WorkspaceUploadService {
184
192
  };
185
193
  }));
186
194
  }
195
+ async consume(ids) {
196
+ await Promise.all(ids.map(async (id) => {
197
+ const attachment = this.state.composerAttachments.get(id);
198
+ if (attachment !== undefined)
199
+ await this.removeCompleted(attachment);
200
+ }));
201
+ }
187
202
  async shutdown() {
188
203
  await Promise.allSettled([...this.state.uploads.values()].map((upload) => this.remove(upload)));
189
204
  for (const attachment of this.state.composerAttachments.values())
190
- await rm(dirname(attachment.targetPath), { recursive: true, force: true });
205
+ await this.removeCompleted(attachment);
191
206
  this.state.composerAttachments.clear();
192
207
  }
193
208
  require(id) {
@@ -206,6 +221,13 @@ export class WorkspaceUploadService {
206
221
  if (upload.composerAttachment !== undefined)
207
222
  await rm(dirname(upload.targetPath), { recursive: true, force: true }).catch(() => undefined);
208
223
  }
224
+ async removeCompleted(attachment) {
225
+ if (this.state.composerAttachments.get(attachment.id) !== attachment)
226
+ return;
227
+ this.state.composerAttachments.delete(attachment.id);
228
+ clearTimeout(attachment.timer);
229
+ await rm(dirname(attachment.targetPath), { recursive: true, force: true }).catch(() => undefined);
230
+ }
209
231
  async attachmentTarget(workspacePath, id, name) {
210
232
  const mar = resolve(workspacePath, '.mar');
211
233
  await mkdir(mar, { recursive: true, mode: 0o700 });
@@ -1,13 +1,11 @@
1
1
  import type { NodeOperationHandler } from '../connector/node-operation-router.js';
2
2
  import type { NodeWorkspace } from '../database.js';
3
- import { NodeRuntimeState } from '../runtime-state.js';
4
3
  import { WorkspaceChangeService } from './workspace-change-service.js';
5
4
  import { WorkspaceFileService } from './workspace-file-service.js';
6
5
  import { WorkspaceUploadService } from './workspace-upload-service.js';
7
6
  import { WorkspaceWatchService } from './workspace-watch-service.js';
8
7
  export interface WorkspaceWorkbenchServiceOptions {
9
8
  readonly requireWorkspace: (workspaceId: unknown) => NodeWorkspace;
10
- readonly runtime: NodeRuntimeState;
11
9
  readonly watch: WorkspaceWatchService;
12
10
  readonly files: WorkspaceFileService;
13
11
  readonly uploads: WorkspaceUploadService;
@@ -20,7 +18,6 @@ export declare class WorkspaceWorkbenchService {
20
18
  private readonly contentSearch;
21
19
  constructor(options: WorkspaceWorkbenchServiceOptions);
22
20
  operations(): Readonly<Record<string, NodeOperationHandler>>;
23
- private activity;
24
21
  private watch;
25
22
  private unwatch;
26
23
  private listFiles;
@@ -9,7 +9,6 @@ export class WorkspaceWorkbenchService {
9
9
  }
10
10
  operations() {
11
11
  return {
12
- 'workspace.activity.read': (data) => this.activity(record(data)),
13
12
  'workspace.watch': (data) => this.watch(record(data)),
14
13
  'workspace.unwatch': (data) => this.unwatch(record(data)),
15
14
  'workspace.files.list': (data) => this.listFiles(record(data)),
@@ -35,20 +34,6 @@ export class WorkspaceWorkbenchService {
35
34
  'workspace.diff': (data) => this.diff(record(data))
36
35
  };
37
36
  }
38
- activity(input) {
39
- if (typeof input.workspaceId !== 'string' ||
40
- (input.cursor !== undefined && typeof input.cursor !== 'string') ||
41
- (input.limit !== undefined &&
42
- (!Number.isInteger(input.limit) ||
43
- input.limit < 1 ||
44
- input.limit > 500)))
45
- throw new Error('EVENT_CURSOR_INVALID');
46
- return this.options.runtime.listWorkspaceActivity({
47
- workspaceId: input.workspaceId,
48
- ...(typeof input.cursor === 'string' ? { cursor: input.cursor } : {}),
49
- ...(typeof input.limit === 'number' ? { limit: input.limit } : {})
50
- });
51
- }
52
37
  async watch(input) {
53
38
  if (typeof input.workspaceId !== 'string' ||
54
39
  typeof input.watchId !== 'string' ||
@@ -143,8 +128,6 @@ export class WorkspaceWorkbenchService {
143
128
  (input.state !== 'STAGED' && input.state !== 'UNSTAGED' && input.state !== 'UNTRACKED'))
144
129
  throw new Error('GIT_CHANGE_RESTORE_UNAVAILABLE');
145
130
  const workspace = this.gitWorkspace(input.workspaceId);
146
- if (this.options.runtime.hasActiveWorkspaceLease(workspace.id))
147
- throw new Error('WORKSPACE_BUSY');
148
131
  const repository = await resolveGitRepository(workspace.path, input.repositoryPath ?? '');
149
132
  await restoreCurrentChange(repository, input.path, input.state);
150
133
  this.options.invalidate(workspace.id);
@@ -0,0 +1,6 @@
1
+ export interface RunnerErrorProjection {
2
+ readonly code: string;
3
+ readonly message: string;
4
+ readonly httpStatus: number | null;
5
+ }
6
+ export declare function runnerErrorProjection(error: unknown, fallbackCode: string): RunnerErrorProjection;
@@ -0,0 +1,52 @@
1
+ const MAX_RUNNER_ERROR_MESSAGE = 1000;
2
+ export function runnerErrorProjection(error, fallbackCode) {
3
+ const httpStatus = findHttpStatus(error);
4
+ const message = findMessage(error) ?? fallbackCode;
5
+ return {
6
+ code: httpStatus === null ? fallbackCode : `HTTP_${httpStatus}`,
7
+ message: message.slice(0, MAX_RUNNER_ERROR_MESSAGE),
8
+ httpStatus
9
+ };
10
+ }
11
+ function findHttpStatus(value, depth = 0) {
12
+ if (depth > 3 || value === null || typeof value !== 'object')
13
+ return null;
14
+ const record = value;
15
+ for (const key of ['status', 'statusCode', 'status_code', 'httpStatus', 'http_status']) {
16
+ const candidate = record[key];
17
+ if (((typeof candidate === 'number' && Number.isInteger(candidate)) ||
18
+ (typeof candidate === 'string' && /^\d{3}$/u.test(candidate))) &&
19
+ Number(candidate) >= 100 &&
20
+ Number(candidate) <= 599)
21
+ return Number(candidate);
22
+ }
23
+ const match = findMessage(value)?.match(/(?:^|\b(?:http(?:\s+status)?|status(?:\s+code)?|error)\s*[:=_-]?\s*)([1-5]\d{2})\b/iu);
24
+ if (match !== null && match !== undefined)
25
+ return Number(match[1]);
26
+ for (const key of ['error', 'cause', 'response', 'data']) {
27
+ const nested = findHttpStatus(record[key], depth + 1);
28
+ if (nested !== null)
29
+ return nested;
30
+ }
31
+ return null;
32
+ }
33
+ function findMessage(value, depth = 0) {
34
+ if (typeof value === 'string')
35
+ return value.trim() || undefined;
36
+ if (value instanceof Error && value.message.trim())
37
+ return value.message.trim();
38
+ if (depth > 3 || value === null || typeof value !== 'object')
39
+ return undefined;
40
+ const record = value;
41
+ for (const key of ['message', 'error_description', 'detail', 'result']) {
42
+ const candidate = record[key];
43
+ if (typeof candidate === 'string' && candidate.trim())
44
+ return candidate.trim();
45
+ }
46
+ for (const key of ['error', 'cause', 'response', 'data']) {
47
+ const nested = findMessage(record[key], depth + 1);
48
+ if (nested !== undefined)
49
+ return nested;
50
+ }
51
+ return undefined;
52
+ }
@@ -146,23 +146,49 @@ export function deduplicateDirectSessions(sessions) {
146
146
  export function nativeConversationPage(session, history, input, runtimeTurns = [], registerImages = () => []) {
147
147
  const limit = Math.min(Math.max(input.limit ?? 30, 1), 500);
148
148
  const nativeTurns = nativeTranscriptTurns(history.items);
149
+ const representedClientMessageIds = new Set(nativeTurns.flatMap((entries) => entries.flatMap(({ entry, index }) => {
150
+ if (entry.kind !== 'message' || entry.role !== 'USER')
151
+ return [];
152
+ const clientMessageId = nativeUserClientMessageId(entry, entry.createdAt ?? index, runtimeTurns);
153
+ return clientMessageId === null ? [] : [clientMessageId];
154
+ })));
155
+ const missingRuntimeErrorTurns = input.cursor === undefined
156
+ ? runtimeTurns.filter((turn) => {
157
+ const clientMessageId = runtimeTurnClientMessageId(turn);
158
+ return (turn.items.some((item) => item.kind === 'error') &&
159
+ (clientMessageId === undefined || !representedClientMessageIds.has(clientMessageId)));
160
+ })
161
+ : [];
149
162
  const end = nativePageEnd(input.cursor, session.id, nativeTurns.length);
150
163
  const start = Math.max(0, end - limit);
151
- const turns = nativeTurns
164
+ const nativePageTurns = nativeTurns
152
165
  .slice(start, end)
153
166
  .map((entries, relativeIndex) => {
154
167
  const firstIndex = entries[0]?.index ?? 0;
155
168
  const turnId = `${session.id}:native:${firstIndex}`;
156
- const items = entries.map(({ entry, index }) => nativeTranscriptConversationItem(session, entry, index, turnId, runtimeTurns, registerImages));
169
+ const nativeItems = entries.map(({ entry, index }) => nativeTranscriptConversationItem(session, entry, index, turnId, runtimeTurns, registerImages));
170
+ const clientMessageId = nativeItems.flatMap((item) => item.kind === 'user_message' && isPlainRecord(item.payload)
171
+ ? [item.payload.clientMessageId]
172
+ : [])[0];
173
+ const runtimeTurn = typeof clientMessageId === 'string'
174
+ ? runtimeTurns.find((turn) => runtimeTurnClientMessageId(turn) === clientMessageId)
175
+ : undefined;
176
+ const runtimeErrors = (runtimeTurn?.items ?? [])
177
+ .filter((item) => item.kind === 'error')
178
+ .map((item) => ({ ...item, turnId, runId: runtimeTurn?.runId ?? null }));
179
+ const items = [
180
+ ...nativeItems,
181
+ ...runtimeErrors.filter((error) => !nativeItems.some((item) => item.id === error.id))
182
+ ];
157
183
  const startedAt = items[0]?.startedAt ?? firstIndex;
158
184
  const completedAt = items.at(-1)?.completedAt ?? startedAt;
159
185
  const after = lastNativeItemId(entries);
160
186
  return {
161
187
  id: turnId,
162
188
  sessionId: session.id,
163
- runId: null,
189
+ runId: runtimeTurn?.runId ?? null,
164
190
  userItemId: items.find((item) => item.kind === 'user_message')?.id ?? null,
165
- status: 'SUCCEEDED',
191
+ status: runtimeErrors.length > 0 ? 'FAILED' : 'SUCCEEDED',
166
192
  model: null,
167
193
  effort: null,
168
194
  access: null,
@@ -179,13 +205,24 @@ export function nativeConversationPage(session, history, input, runtimeTurns = [
179
205
  completedAt
180
206
  };
181
207
  });
208
+ const combined = [...nativePageTurns, ...missingRuntimeErrorTurns].sort((left, right) => (left.startedAt ?? 0) - (right.startedAt ?? 0));
209
+ const dropped = combined.slice(0, Math.max(0, combined.length - limit));
210
+ const turns = combined.slice(-limit);
211
+ const nativePageIds = new Set(nativePageTurns.map((turn) => turn.id));
212
+ const nextEnd = start + dropped.filter((turn) => nativePageIds.has(turn.id)).length;
182
213
  return {
183
214
  turns,
184
- nextCursor: start > 0
185
- ? Buffer.from(JSON.stringify({ sessionId: session.id, end: start })).toString('base64url')
215
+ nextCursor: nextEnd > 0
216
+ ? Buffer.from(JSON.stringify({ sessionId: session.id, end: nextEnd })).toString('base64url')
186
217
  : null
187
218
  };
188
219
  }
220
+ function runtimeTurnClientMessageId(turn) {
221
+ const user = turn.items.find((item) => item.kind === 'user_message');
222
+ return isPlainRecord(user?.payload) && typeof user.payload.clientMessageId === 'string'
223
+ ? user.payload.clientMessageId
224
+ : undefined;
225
+ }
189
226
  function lastNativeItemId(entries) {
190
227
  return entries.findLast(({ entry }) => entry.nativeId !== undefined)?.entry.nativeId;
191
228
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "type": "module",
6
6
  "files": [
@@ -24,7 +24,7 @@
24
24
  "node-pty": "1.1.0",
25
25
  "ws": "^8.21.3",
26
26
  "zod": "4.4.3",
27
- "@myagentroam/protocol": "^0.9.2"
27
+ "@myagentroam/protocol": "^0.9.3"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/ws": "^8.18.1"