@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
@@ -25,6 +25,9 @@ export class RunEventService {
25
25
  clearTimeout(pending.timer);
26
26
  this.pendingText.clear();
27
27
  }
28
+ flushPendingText(runId) {
29
+ this.flushText(runId);
30
+ }
28
31
  enqueueText(runId, text) {
29
32
  if (text.length === 0)
30
33
  return;
@@ -47,6 +50,9 @@ export class RunEventService {
47
50
  this.emitNow(runId, 'text.delta', { text: pending.text });
48
51
  }
49
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);
50
56
  const event = {
51
57
  runId,
52
58
  sequence: (this.sequences.get(runId) ?? 0) + 1,
@@ -59,8 +65,9 @@ export class RunEventService {
59
65
  nodeLog('run.event', { runId, eventType, status });
60
66
  if (status === 'FAILED')
61
67
  this.options.metrics.runnerFailed();
68
+ let persisted = undefined;
62
69
  try {
63
- this.options.runtime.appendRunEvent({
70
+ persisted = this.options.runtime.appendRunEvent({
64
71
  runId,
65
72
  sequence: event.sequence,
66
73
  eventType,
@@ -72,6 +79,7 @@ export class RunEventService {
72
79
  /* A stale relayed run may race the storage boundary. */
73
80
  }
74
81
  if (status !== undefined && isTerminalRunStatus(status)) {
82
+ this.options.state.active.delete(runId);
75
83
  this.options.state.interruptRequested.delete(runId);
76
84
  const terminalRun = this.options.runtime.getRun(runId);
77
85
  if (terminalRun !== undefined && this.options.isBootstrapSession(terminalRun.sessionId))
@@ -84,7 +92,14 @@ export class RunEventService {
84
92
  if (session !== undefined)
85
93
  this.options.emitWorkbench('session', { session: this.options.presentSession(session) });
86
94
  }
87
- this.options.emitWorkbench('conversation', { event });
95
+ this.options.emitWorkbench('conversation', {
96
+ event: { ...event, createdAt: persisted?.createdAt ?? Date.now() }
97
+ });
98
+ if (notificationPayload !== undefined)
99
+ this.emitNow(runId, 'notification', {
100
+ ...notificationPayload,
101
+ occurredAt: persisted?.createdAt ?? Date.now()
102
+ });
88
103
  if (status === undefined || !isTerminalRunStatus(status))
89
104
  return;
90
105
  const turn = this.options.runtime.conversationTurnForRun(runId);
@@ -96,7 +111,67 @@ export class RunEventService {
96
111
  const run = this.options.runtime.getRun(runId);
97
112
  if (run !== undefined) {
98
113
  this.options.scheduleContextRefresh(run.sessionId);
114
+ this.options.runtime.removeTerminalRun(runId);
115
+ this.sequences.delete(runId);
99
116
  void this.options.dispatchQueue(run.sessionId);
100
117
  }
101
118
  }
119
+ notificationPayload(runId, eventType, payload, status) {
120
+ if (eventType === 'notification')
121
+ return undefined;
122
+ const run = this.options.runtime.getRun(runId);
123
+ if (run === undefined)
124
+ return undefined;
125
+ const session = this.options.runtime.getAgentSession(run.sessionId);
126
+ if (session === undefined)
127
+ return undefined;
128
+ const base = {
129
+ workspaceId: run.workspaceId,
130
+ sessionId: run.sessionId,
131
+ sessionTitle: session.customTitle ?? session.runnerTitle ?? session.id,
132
+ runId
133
+ };
134
+ if (status !== undefined && isTerminalRunStatus(status)) {
135
+ if (this.options.state.interruptRequested.has(runId))
136
+ return undefined;
137
+ if (status === 'SUCCEEDED' &&
138
+ this.options.runtime.sessionQueue(run.sessionId).runIds.length > 0)
139
+ return undefined;
140
+ return {
141
+ ...base,
142
+ notificationId: `run:${run.workspaceId}:${runId}`,
143
+ kind: 'RUN_TERMINAL',
144
+ outcome: status
145
+ };
146
+ }
147
+ if (eventType === 'approval.request' && isPlainRecord(payload)) {
148
+ const approvalId = payload.approvalId;
149
+ if (typeof approvalId !== 'string')
150
+ return undefined;
151
+ return {
152
+ ...base,
153
+ notificationId: `approval:${run.workspaceId}:${runId}:${approvalId}`,
154
+ kind: 'APPROVAL_REQUIRED'
155
+ };
156
+ }
157
+ if (eventType !== 'conversation.item' || !isPlainRecord(payload))
158
+ return undefined;
159
+ if (payload.kind === 'plan' && typeof payload.itemId === 'string')
160
+ return {
161
+ ...base,
162
+ notificationId: `plan:${run.workspaceId}:${run.sessionId}`,
163
+ kind: 'PLAN_READY'
164
+ };
165
+ if (payload.kind === 'user_input_request' &&
166
+ typeof payload.itemId === 'string' &&
167
+ isPlainRecord(payload.payload) &&
168
+ Array.isArray(payload.payload.questions) &&
169
+ payload.payload.questions.length > 0)
170
+ return {
171
+ ...base,
172
+ notificationId: `user-input:${run.workspaceId}:${runId}:${payload.itemId}`,
173
+ kind: 'USER_INPUT_REQUIRED'
174
+ };
175
+ return undefined;
176
+ }
102
177
  }
@@ -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) {
@@ -1,5 +1,6 @@
1
1
  import { removeNativeSession } from '../native-session-history.js';
2
2
  import { isTerminalRunStatus, sessionPage } from '../util/node-operation-parsers.js';
3
+ import { latestSessionActivityAt } from './session-presentation-service.js';
3
4
  export class SessionCatalogService {
4
5
  options;
5
6
  rewindQueue = Promise.resolve();
@@ -35,7 +36,7 @@ export class SessionCatalogService {
35
36
  pinOrder: source.pinOrder ?? null,
36
37
  externalDiscovered: true,
37
38
  titleSource: session.customTitle === null ? source.titleSource : session.titleSource,
38
- lastActivityAt: source.lastActivityAt
39
+ lastActivityAt: latestSessionActivityAt(session.lastActivityAt, source.lastActivityAt)
39
40
  });
40
41
  });
41
42
  const merged = new Map([
@@ -100,8 +101,7 @@ export class SessionCatalogService {
100
101
  delivery: resumed === undefined ? 'REJECTED' : 'RESUMABLE',
101
102
  ...(resumed === undefined
102
103
  ? {
103
- reasonCode: failure?.code ?? 'SESSION_RESUME_REJECTED',
104
- ...(failure?.detail === undefined ? {} : { reasonDetail: failure.detail })
104
+ reasonCode: failure?.code ?? 'SESSION_RESUME_REJECTED'
105
105
  }
106
106
  : {})
107
107
  };
@@ -130,7 +130,7 @@ export class SessionCatalogService {
130
130
  const expected = input.position === 'BEFORE' ? turn?.rewind?.before : turn?.rewind?.after;
131
131
  if (turn === undefined || expected !== input.boundary)
132
132
  throw new Error('SESSION_REWIND_TARGET_INVALID');
133
- const title = rewindTitle(source.customTitle ?? source.runnerTitle ?? '未命名会话', this.options.runtime
133
+ const title = rewindTitle(source.customTitle ?? source.runnerTitle ?? 'Untitled session', this.options.runtime
134
134
  .listAgentSessions()
135
135
  .flatMap((candidate) => [candidate.customTitle ?? candidate.runnerTitle].filter(isString)));
136
136
  const runner = this.options.runners.require(source.runner);
@@ -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' ||
@@ -30,4 +30,5 @@ export declare class SessionPresentationService {
30
30
  present(session: NodeAgentSession): PresentedSession;
31
31
  presentDiscovered(sessions: readonly NodeAgentSession[]): readonly PresentedSession[];
32
32
  }
33
+ export declare function latestSessionActivityAt(current: number, discovered: number): number;
33
34
  export {};
@@ -30,7 +30,14 @@ export class SessionPresentationService {
30
30
  const synchronized = session.runnerTitle === null
31
31
  ? projected
32
32
  : this.options.runtime.setRunnerTitleFromNative(projected.id, session.runnerTitle);
33
- return this.present({ ...synchronized, lastActivityAt: session.lastActivityAt });
33
+ return this.present({
34
+ ...synchronized,
35
+ lastActivityAt: latestSessionActivityAt(synchronized.lastActivityAt, session.lastActivityAt)
36
+ });
34
37
  });
35
38
  }
36
39
  }
40
+ export function latestSessionActivityAt(current, discovered) {
41
+ const known = [current, discovered].filter((value) => Number.isFinite(value) && value > 0);
42
+ return known.length === 0 ? 0 : Math.max(...known);
43
+ }
@@ -13,6 +13,10 @@ export interface SessionQueryServiceOptions {
13
13
  readonly cursor?: string;
14
14
  readonly limit?: number;
15
15
  }) => Promise<unknown>;
16
+ readonly segments: (session: NodeAgentSession, input: {
17
+ readonly cursor?: string;
18
+ readonly limit?: number;
19
+ }) => Promise<unknown>;
16
20
  readonly context: SessionContextService;
17
21
  readonly discover: (runner: RunnerName, workspaceId?: string) => Promise<readonly NodeAgentSession[]>;
18
22
  readonly workspaceIdForCwd: (cwd: string) => string | undefined;
@@ -23,7 +23,12 @@ export class SessionQueryService {
23
23
  }
24
24
  async turns(input) {
25
25
  const page = pageInput(input);
26
- return this.options.history(await this.requireSession(input.sessionId), page);
26
+ const session = await this.requireSession(input.sessionId);
27
+ if (input.unit === 'SEGMENT')
28
+ return this.options.segments(session, page);
29
+ if (input.unit !== undefined && input.unit !== 'TURN')
30
+ throw new Error('EVENT_CURSOR_INVALID');
31
+ return this.options.history(session, page);
27
32
  }
28
33
  events(input) {
29
34
  const page = pageInput(input);
@@ -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
  }
@@ -1,27 +1,32 @@
1
- import { lstat, open, readFile, readdir, readlink, realpath } from 'node:fs/promises';
2
- import { execFile } from 'node:child_process';
3
- import { promisify } from 'node:util';
1
+ import { lstat, open, readdir, readlink, realpath } from 'node:fs/promises';
4
2
  import path from 'node:path';
5
3
  import { skillInstallMetadataSchema } from '@myagentroam/protocol';
4
+ import { hasBrokenWorkspaceSkillGitExclude } from './workspace-git-exclude-service.js';
6
5
  const MAX_METADATA_BYTES = 64 * 1024;
7
6
  const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
8
7
  const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/u;
9
8
  export class SkillDirectoryService {
10
- async inspectNodeHome(home) {
9
+ async inspectNodeHome(home, options = {}) {
11
10
  const agentsRoot = path.join(home, '.agents', 'skills');
12
11
  const claudeRoot = path.join(home, '.claude', 'skills');
13
- 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
+ : [];
14
17
  return {
15
18
  targetKind: 'NODE',
16
- compatibilityMode: await nodeCompatibilityMode(agentsRoot, claudeRoot),
17
- skills
19
+ compatibilityMode,
20
+ skills: options.includeClaudeCompatibility === true
21
+ ? mergeNodeCompatibilityRoots(agents, claude)
22
+ : agents.map(withoutSource)
18
23
  };
19
24
  }
20
25
  async inspectWorkspace(workspace) {
21
26
  const agents = await scanRoot(path.join(workspace, '.agents', 'skills'), 'AGENTS');
22
27
  const claude = await scanRoot(path.join(workspace, '.claude', 'skills'), 'CLAUDE');
23
28
  const skills = mergeWorkspaceCopies(agents, claude);
24
- const gitExcludeBroken = await hasBrokenGitExclude(workspace, skills.map((skill) => skill.name));
29
+ const gitExcludeBroken = await hasBrokenWorkspaceSkillGitExclude(workspace, skills.map((skill) => skill.name));
25
30
  return {
26
31
  targetKind: 'WORKSPACE',
27
32
  compatibilityMode: 'MANAGED_COPY',
@@ -31,28 +36,6 @@ export class SkillDirectoryService {
31
36
  };
32
37
  }
33
38
  }
34
- async function hasBrokenGitExclude(workspace, names) {
35
- if (names.length === 0)
36
- return false;
37
- try {
38
- await lstat(path.join(workspace, '.git'));
39
- }
40
- catch (error) {
41
- if (isMissing(error))
42
- return false;
43
- return true;
44
- }
45
- try {
46
- const result = await promisify(execFile)('git', ['-C', workspace, 'rev-parse', '--git-path', 'info/exclude'], { timeout: 5_000, windowsHide: true });
47
- const exclude = path.resolve(workspace, result.stdout.trim());
48
- const content = await readFile(exclude, 'utf8');
49
- return names.some((name) => !content.includes(`/.agents/skills/${name}/`) ||
50
- !content.includes(`/.claude/skills/${name}/`));
51
- }
52
- catch {
53
- return true;
54
- }
55
- }
56
39
  async function scanRoot(root, source) {
57
40
  let entries;
58
41
  try {
@@ -170,6 +153,24 @@ function mergeWorkspaceCopies(agents, claude) {
170
153
  return withoutSource(copies.agents);
171
154
  });
172
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
+ }
173
174
  function withoutSource(entry) {
174
175
  return {
175
176
  name: entry.name,
@@ -4,6 +4,7 @@ import { promisify } from 'node:util';
4
4
  import { lstat, mkdir, readFile, readdir, readlink, realpath, rename, rm, symlink, writeFile } from 'node:fs/promises';
5
5
  import path from 'node:path';
6
6
  import { skillInstallMetadataSchema } from '@myagentroam/protocol';
7
+ import { rewriteWorkspaceGitExclude, updateWorkspaceSkillGitExclude } from './workspace-git-exclude-service.js';
7
8
  export class SkillInstallService {
8
9
  async recoverNodeHome(home) {
9
10
  const agentsRoot = path.join(home, '.agents', 'skills');
@@ -58,7 +59,7 @@ export class SkillInstallService {
58
59
  await mkdir(claudeRoot, { recursive: true });
59
60
  await assertContainedDirectory(workspace, agentsRoot);
60
61
  await assertContainedDirectory(workspace, claudeRoot);
61
- await replacePair(agentsRoot, claudeRoot, name, bundle, metadata, () => updateGitExclude(workspace, name, true), () => updateGitExclude(workspace, name, false));
62
+ await replacePair(agentsRoot, claudeRoot, name, bundle, metadata, () => updateWorkspaceSkillGitExclude(workspace, name, true), () => updateWorkspaceSkillGitExclude(workspace, name, false));
62
63
  }
63
64
  async removeWorkspace(workspace, name) {
64
65
  assertName(name);
@@ -66,7 +67,7 @@ export class SkillInstallService {
66
67
  const claudeRoot = path.join(workspace, '.claude', 'skills');
67
68
  await assertExistingContainedDirectory(workspace, agentsRoot);
68
69
  await assertExistingContainedDirectory(workspace, claudeRoot);
69
- await removePair(agentsRoot, claudeRoot, name, () => updateGitExclude(workspace, name, false), () => updateGitExclude(workspace, name, true));
70
+ await removePair(agentsRoot, claudeRoot, name, () => updateWorkspaceSkillGitExclude(workspace, name, false), () => updateWorkspaceSkillGitExclude(workspace, name, true));
70
71
  }
71
72
  async removeNodeHome(home, name) {
72
73
  assertName(name);
@@ -100,7 +101,7 @@ export class SkillInstallService {
100
101
  if (!isMissing(error))
101
102
  throw error;
102
103
  }
103
- await rewriteGitExclude(workspace, names);
104
+ await rewriteWorkspaceGitExclude(workspace, names);
104
105
  }
105
106
  }
106
107
  export async function workspaceSkillTracked(workspace, name) {
@@ -405,74 +406,6 @@ async function ensureClaudeRoot(agentsRoot, claudeRoot) {
405
406
  }
406
407
  }
407
408
  }
408
- async function updateGitExclude(workspace, name, add) {
409
- await mutateGitExclude(workspace, (entries) => {
410
- for (const root of ['.agents', '.claude']) {
411
- const line = `/${root}/skills/${name}/`;
412
- if (add)
413
- entries.add(line);
414
- else
415
- entries.delete(line);
416
- }
417
- });
418
- }
419
- async function rewriteGitExclude(workspace, names) {
420
- await mutateGitExclude(workspace, (entries) => {
421
- entries.clear();
422
- for (const name of names)
423
- for (const root of ['.agents', '.claude'])
424
- entries.add(`/${root}/skills/${name}/`);
425
- });
426
- }
427
- async function mutateGitExclude(workspace, mutate) {
428
- const git = path.join(workspace, '.git');
429
- if (!(await exists(git)))
430
- return;
431
- let exclude;
432
- try {
433
- const result = await promisify(execFile)('git', ['-C', workspace, 'rev-parse', '--git-path', 'info/exclude', '--git-common-dir'], { timeout: 5_000, windowsHide: true });
434
- const [excludeOutput, commonOutput] = result.stdout.trim().split(/\r?\n/u);
435
- if (!excludeOutput || !commonOutput)
436
- throw new Error('WORKSPACE_SKILL_GIT_EXCLUDE_FAILED');
437
- exclude = path.resolve(workspace, excludeOutput);
438
- const common = path.resolve(workspace, commonOutput);
439
- const relative = path.relative(common, exclude);
440
- if (relative.startsWith('..') || path.isAbsolute(relative))
441
- throw new Error('WORKSPACE_SKILL_GIT_EXCLUDE_FAILED');
442
- }
443
- catch {
444
- throw new Error('WORKSPACE_SKILL_GIT_EXCLUDE_FAILED');
445
- }
446
- await mkdir(path.dirname(exclude), { recursive: true });
447
- const begin = '# BEGIN MyAgentRoam Skills';
448
- const end = '# END MyAgentRoam Skills';
449
- let content = '';
450
- try {
451
- const excludeStat = await lstat(exclude);
452
- if (!excludeStat.isFile() || excludeStat.isSymbolicLink())
453
- throw new Error('WORKSPACE_SKILL_GIT_EXCLUDE_FAILED');
454
- content = await readFile(exclude, 'utf8');
455
- }
456
- catch (error) {
457
- if (!isMissing(error))
458
- throw error;
459
- }
460
- const pattern = new RegExp(`${escapeRegExp(begin)}[\\s\\S]*?${escapeRegExp(end)}\\n?`, 'u');
461
- const existing = pattern.exec(content)?.[0] ?? '';
462
- const entries = new Set(existing.split(/\r?\n/u).filter((line) => line.startsWith('/.')));
463
- mutate(entries);
464
- const block = entries.size === 0 ? '' : `${begin}\n${[...entries].sort().join('\n')}\n${end}\n`;
465
- const next = content.replace(pattern, '').replace(/\s*$/u, '\n') + block;
466
- const staging = path.join(path.dirname(exclude), `.mar-git-exclude-${randomUUID()}.tmp`);
467
- try {
468
- await writeFile(staging, next, { encoding: 'utf8', flag: 'wx' });
469
- await rename(staging, exclude);
470
- }
471
- catch (error) {
472
- await rm(staging, { force: true });
473
- throw error;
474
- }
475
- }
476
409
  function assertName(name) {
477
410
  if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(name) ||
478
411
  /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/u.test(name))
@@ -499,6 +432,3 @@ async function exists(value) {
499
432
  function isMissing(error) {
500
433
  return (error instanceof Error && 'code' in error && error.code === 'ENOENT');
501
434
  }
502
- function escapeRegExp(value) {
503
- return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
504
- }
@@ -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}`;
@@ -3,6 +3,7 @@ import { createHash, randomUUID } from 'node:crypto';
3
3
  import { skillInstallMetadataSchema } from '@myagentroam/protocol';
4
4
  import { SkillDirectoryService } from './skill-directory-service.js';
5
5
  import { SkillInstallService, workspaceSkillTracked } from './skill-install-service.js';
6
+ import { safeErrorCode } from '../util/safe-error.js';
6
7
  export class SkillNodeOperationService {
7
8
  database;
8
9
  config;
@@ -16,9 +17,11 @@ export class SkillNodeOperationService {
16
17
  }
17
18
  operations() {
18
19
  return {
19
- 'node.skills.inspect': async () => {
20
+ 'node.skills.inspect': async (data) => {
20
21
  await this.installer.recoverNodeHome(homedir());
21
- return this.directories.inspectNodeHome(homedir());
22
+ return this.directories.inspectNodeHome(homedir(), {
23
+ includeClaudeCompatibility: record(data)['includeClaudeCompatibility'] === true
24
+ });
22
25
  },
23
26
  'node.skills.install': (data) => this.install('NODE', data),
24
27
  'node.skills.remove': (data) => this.remove('NODE', data),
@@ -76,7 +79,7 @@ export class SkillNodeOperationService {
76
79
  catch (error) {
77
80
  terminal = {
78
81
  status: 'FAILED',
79
- errorCode: error instanceof Error ? error.message : 'SKILL_INSTALL_FAILED'
82
+ errorCode: safeErrorCode(error, 'SKILL_INSTALL_FAILED')
80
83
  };
81
84
  }
82
85
  this.activeTargets.delete(targetKey);