@myagentroam/node 0.9.95 → 0.9.97

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.
@@ -9,22 +9,11 @@ import { createInterface } from 'node:readline';
9
9
  import { minimalRunnerEnvironment, platformCliInvocation, runRunnerProbe } from './operational.js';
10
10
  const REQUEST_TIMEOUT_MS = 15_000;
11
11
  /**
12
- * Maps the facts that the Node can actually establish to user-visible
13
- * controls. In particular, seeing an external thread never implies that it
14
- * is safe to write to its active turn.
12
+ * Maps the facts that the Node can establish to the unified session controls.
13
+ * Native-session activation is validated atomically with the first message.
15
14
  */
16
15
  export function codexSessionControl(input) {
17
- if (!input.available)
18
- return { level: 'UNAVAILABLE', capabilities: [] };
19
- if (input.sharedControl) {
20
- return {
21
- level: 'SHARED_CONTROL',
22
- capabilities: ['history.read', 'events.live', 'turn.start', 'turn.steer', 'turn.interrupt']
23
- };
24
- }
25
- if (input.nativeThreadId !== null && input.nativeControl === 'EXTERNAL') {
26
- return { level: 'OBSERVE_ONLY', capabilities: ['history.read'] };
27
- }
16
+ void [input.available, input.nativeThreadId, input.nativeControl];
28
17
  if (input.activeTurn) {
29
18
  return {
30
19
  level: 'MANAGED',
@@ -32,7 +21,7 @@ export function codexSessionControl(input) {
32
21
  };
33
22
  }
34
23
  return {
35
- level: 'RESUMABLE',
24
+ level: 'MANAGED',
36
25
  capabilities: ['history.read', 'turn.start']
37
26
  };
38
27
  }
package/dist/connector.js CHANGED
@@ -635,7 +635,6 @@ export class NodeConnector {
635
635
  runners: this.runners,
636
636
  capabilities: () => this.capabilities,
637
637
  hasManagedActiveRun: (session) => hasManagedActiveRun(this.runtime, session),
638
- externalActivity: (sessionId) => this.sessionState.externalActivity.get(sessionId),
639
638
  projectionForNative: (session) => this.nativeSessionProjectionService.projectionForNative(session)
640
639
  });
641
640
  this.nativeSessionProjectionService = new NativeSessionProjectionService({
@@ -851,28 +850,12 @@ export class NodeConnector {
851
850
  configuration: (session) => this.nativeSessionProjectionService.resumedConfiguration(session),
852
851
  workspacePath: (workspaceId) => this.database?.getWorkspace(workspaceId)?.path,
853
852
  rememberNative: (nativeSessionId, sessionId) => this.managedRunnerSessions.remember(nativeSessionId, sessionId),
854
- stage: (session) => {
855
- const configuration = this.nativeSessionProjectionService.resumedConfiguration(session);
856
- return this.runtime.createAgentSession({
857
- workspaceId: session.workspaceId,
858
- runner: session.runner,
859
- externalSessionId: session.externalSessionId ?? session.id,
860
- nativeControl: 'EXTERNAL',
861
- cwd: session.cwd,
862
- model: configuration.model,
863
- effort: configuration.effort,
864
- access: configuration.access,
865
- ...(session.runnerTitle === null ? {} : { title: session.runnerTitle }),
866
- ...(session.customTitle === null ? {} : { customTitle: session.customTitle })
867
- });
868
- },
869
853
  resolve: (sessionId) => this.nativeSessionProjectionService.resolve(sessionId)
870
854
  });
871
855
  this.sessionCommandService = new SessionCommandService({
872
856
  runtime: this.runtime,
873
857
  runners: this.runners,
874
858
  projection: this.nativeSessionProjectionService,
875
- resume: this.externalSessionResumeService,
876
859
  profiles: () => this.runnerService.profiles(),
877
860
  present: (session) => this.sessionPresentationService.present(session),
878
861
  resumeQueueAfterCommandRun: (sessionId) => this.workspaceQueueWorkbenchService.resumeAfterCommandRun(sessionId),
@@ -1018,7 +1001,6 @@ export class NodeConnector {
1018
1001
  runtime: this.runtime,
1019
1002
  runners: this.runners,
1020
1003
  projection: this.nativeSessionProjectionService,
1021
- resume: this.externalSessionResumeService,
1022
1004
  cachedNative: () => [...this.sessionState.directNative.values()],
1023
1005
  cachedWorkspace: (workspaceId) => this.workspaceCoordinator.cachedSessions(workspaceId),
1024
1006
  discoverWorkspace: (workspaceId) => this.nativeSessionProjectionService.discoverWorkspace(workspaceId),
@@ -115,7 +115,7 @@ export class ClaudeManagedRunController {
115
115
  return;
116
116
  if (typeof payload.externalSessionId === 'string' &&
117
117
  !this.runner.execution.managedNativeIds.has(payload.externalSessionId)) {
118
- this.host.emit(runId, 'run.rejected', { code: 'EXTERNAL_SESSION_OBSERVE_ONLY' }, 'FAILED');
118
+ this.host.emit(runId, 'run.rejected', { code: 'SESSION_ACTIVATION_REQUIRED' }, 'FAILED');
119
119
  return;
120
120
  }
121
121
  this.host.active.add(runId);
@@ -147,62 +147,28 @@ export class ClaudeCodeRunner extends AbstractRunner {
147
147
  : this.client.forkSession(session.externalSessionId, session.cwd, boundary, title);
148
148
  }
149
149
  presentSession(session, capabilities, context) {
150
- if (capabilities.claudeCode.available && session.nativeControl === 'MAR_MANAGED') {
151
- const runtimeCapabilities = [
152
- 'history.read',
153
- 'events.live',
154
- 'turn.start',
155
- 'turn.queue',
156
- 'turn.interrupt',
157
- 'run.cancel',
158
- 'approval.respond',
159
- 'session.rename',
160
- 'session.remove',
161
- 'session.pin',
162
- ...(session.externalSessionId === null ? [] : ['session.rewind'])
163
- ];
164
- return {
165
- activity: context.managedActive ? 'MANAGED_ACTIVE' : 'IDLE',
166
- control: {
167
- level: 'MANAGED',
168
- capabilities: ['history.read', 'events.live', 'turn.start', 'turn.interrupt']
169
- },
170
- commandStates: context.commandStates,
171
- runtime: { level: 'MANAGED', capabilities: runtimeCapabilities, reasonCode: null }
172
- };
173
- }
174
- if (capabilities.claudeCode.available && session.externalSessionId !== null) {
175
- const runtimeCapabilities = [
176
- 'history.read',
177
- 'turn.start',
178
- 'session.rename',
179
- 'session.remove',
180
- 'session.pin',
181
- 'session.rewind'
182
- ];
183
- return {
184
- activity: 'IDLE',
185
- control: { level: 'RESUMABLE', capabilities: ['history.read', 'turn.start'] },
186
- commandStates: context.commandStates,
187
- runtime: { level: 'RESUMABLE', capabilities: runtimeCapabilities, reasonCode: null }
188
- };
189
- }
150
+ void capabilities;
190
151
  const runtimeCapabilities = [
191
152
  'history.read',
192
153
  'events.live',
154
+ 'turn.start',
155
+ 'turn.queue',
156
+ 'turn.interrupt',
157
+ 'run.cancel',
158
+ 'approval.respond',
193
159
  'session.rename',
194
160
  'session.remove',
195
- 'session.pin'
161
+ 'session.pin',
162
+ ...(session.externalSessionId === null ? [] : ['session.rewind'])
196
163
  ];
197
164
  return {
198
- activity: 'UNAVAILABLE',
199
- control: { level: 'OBSERVE_ONLY', capabilities: ['history.read', 'events.live'] },
165
+ activity: context.managedActive ? 'MANAGED_ACTIVE' : 'IDLE',
166
+ control: {
167
+ level: 'MANAGED',
168
+ capabilities: ['history.read', 'events.live', 'turn.start', 'turn.interrupt']
169
+ },
200
170
  commandStates: context.commandStates,
201
- runtime: {
202
- level: 'OBSERVE_ONLY',
203
- capabilities: runtimeCapabilities,
204
- reasonCode: 'EXTERNAL_SESSION_OBSERVE_ONLY'
205
- }
171
+ runtime: { level: 'MANAGED', capabilities: runtimeCapabilities, reasonCode: null }
206
172
  };
207
173
  }
208
174
  prepareMessageInput(input, collaborationMode) {
@@ -357,7 +357,7 @@ export class CodexManagedRunController {
357
357
  }
358
358
  if (typeof payload.externalSessionId === 'string' &&
359
359
  (session === undefined || session.nativeControl !== 'MAR_MANAGED')) {
360
- this.host.emit(runId, 'run.rejected', { code: 'EXTERNAL_THREAD_OBSERVE_ONLY' }, 'FAILED');
360
+ this.host.emit(runId, 'run.rejected', { code: 'SESSION_ACTIVATION_REQUIRED' }, 'FAILED');
361
361
  return;
362
362
  }
363
363
  try {
@@ -309,10 +309,7 @@ export class CodexRunner extends AbstractRunner {
309
309
  return { activity: 'IDLE', session: context.create(external) };
310
310
  }
311
311
  catch {
312
- return {
313
- activity: 'UNAVAILABLE',
314
- failureCode: 'RUNNER_RESUME_FAILED'
315
- };
312
+ return { failureCode: 'RUNNER_RESUME_FAILED' };
316
313
  }
317
314
  }
318
315
  async discoverSessions(context, workspace) {
@@ -391,13 +388,7 @@ export class CodexRunner extends AbstractRunner {
391
388
  nativeControl: session.nativeControl,
392
389
  activeTurn
393
390
  });
394
- const activity = !capabilities.codex.available
395
- ? 'UNAVAILABLE'
396
- : context.managedActive
397
- ? 'MANAGED_ACTIVE'
398
- : session.nativeControl === 'EXTERNAL'
399
- ? (context.externalActivity ?? 'IDLE')
400
- : 'IDLE';
391
+ const activity = context.managedActive ? 'MANAGED_ACTIVE' : 'IDLE';
401
392
  return {
402
393
  control,
403
394
  activity,
@@ -406,13 +397,15 @@ export class CodexRunner extends AbstractRunner {
406
397
  level: control.level,
407
398
  capabilities: [
408
399
  ...control.capabilities,
409
- ...(control.level === 'MANAGED' ? ['turn.queue', 'run.cancel', 'approval.respond'] : []),
400
+ 'turn.queue',
401
+ 'run.cancel',
402
+ 'approval.respond',
410
403
  'session.rename',
411
404
  'session.remove',
412
405
  'session.pin',
413
406
  ...(session.externalSessionId === null ? [] : ['session.rewind'])
414
407
  ],
415
- reasonCode: control.level === 'UNAVAILABLE' ? 'RUNNER_UNAVAILABLE' : null
408
+ reasonCode: null
416
409
  }
417
410
  };
418
411
  }
@@ -170,14 +170,7 @@ export class MarAgentRunner extends AbstractRunner {
170
170
  };
171
171
  }
172
172
  presentSession(session, capabilities, context) {
173
- if (!this.available(capabilities))
174
- return {
175
- activity: 'UNAVAILABLE',
176
- control: { level: 'UNAVAILABLE', capabilities: [] },
177
- runtime: { level: 'UNAVAILABLE', capabilities: [], reasonCode: 'RUNNER_UNAVAILABLE' },
178
- commandStates: context.commandStates
179
- };
180
- const managed = session.nativeControl === 'MAR_MANAGED';
173
+ void capabilities;
181
174
  const activity = context.managedActive ? 'MANAGED_ACTIVE' : 'IDLE';
182
175
  const runtimeCapabilities = [
183
176
  'history.read',
@@ -195,11 +188,11 @@ export class MarAgentRunner extends AbstractRunner {
195
188
  return {
196
189
  activity,
197
190
  control: {
198
- level: managed ? 'MANAGED' : 'RESUMABLE',
191
+ level: 'MANAGED',
199
192
  capabilities: ['history.read', 'events.live', 'turn.start', 'turn.interrupt']
200
193
  },
201
194
  runtime: {
202
- level: managed ? 'MANAGED' : 'RESUMABLE',
195
+ level: 'MANAGED',
203
196
  capabilities: runtimeCapabilities,
204
197
  reasonCode: null
205
198
  },
@@ -206,7 +206,7 @@ export class OpenCodeRunner extends AbstractRunner {
206
206
  const projection = context.projectionFor(external);
207
207
  const session = projection === undefined ? context.create(external) : context.promote(projection.id);
208
208
  return session === undefined
209
- ? { activity: 'UNAVAILABLE', failureCode: 'RUNNER_RESUME_FAILED' }
209
+ ? { failureCode: 'RUNNER_RESUME_FAILED' }
210
210
  : { activity: 'IDLE', session: context.ensureConfiguration(session) };
211
211
  }
212
212
  async readOfficialConversation(session, input, managedTurn) {
@@ -227,18 +227,8 @@ export class OpenCodeRunner extends AbstractRunner {
227
227
  return this.managedTurns.get(nativeTurnId);
228
228
  }
229
229
  presentSession(session, capabilities, context) {
230
- const available = capabilities.openCode?.available === true;
231
- const managed = session.nativeControl === 'MAR_MANAGED';
232
- const activity = context.managedActive
233
- ? 'MANAGED_ACTIVE'
234
- : (context.externalActivity ?? (available ? 'IDLE' : 'UNAVAILABLE'));
235
- if (!available)
236
- return {
237
- activity: 'UNAVAILABLE',
238
- control: { level: 'UNAVAILABLE', capabilities: [] },
239
- runtime: { level: 'UNAVAILABLE', capabilities: [], reasonCode: 'RUNNER_UNAVAILABLE' },
240
- commandStates: context.commandStates
241
- };
230
+ void capabilities;
231
+ const activity = context.managedActive ? 'MANAGED_ACTIVE' : 'IDLE';
242
232
  const runtimeCapabilities = [
243
233
  'history.read',
244
234
  'events.live',
@@ -256,15 +246,13 @@ export class OpenCodeRunner extends AbstractRunner {
256
246
  return {
257
247
  activity,
258
248
  control: {
259
- level: managed ? 'MANAGED' : activity === 'IDLE' ? 'RESUMABLE' : 'OBSERVE_ONLY',
260
- capabilities: managed || activity === 'IDLE'
261
- ? ['history.read', 'events.live', 'turn.start', 'turn.interrupt']
262
- : ['history.read', 'events.live']
249
+ level: 'MANAGED',
250
+ capabilities: ['history.read', 'events.live', 'turn.start', 'turn.interrupt']
263
251
  },
264
252
  runtime: {
265
- level: managed ? 'MANAGED' : activity === 'IDLE' ? 'RESUMABLE' : 'OBSERVE_ONLY',
253
+ level: 'MANAGED',
266
254
  capabilities: runtimeCapabilities,
267
- reasonCode: activity === 'EXTERNAL_ACTIVE' ? 'EXTERNAL_SESSION_OBSERVE_ONLY' : null
255
+ reasonCode: null
268
256
  },
269
257
  commandStates: context.commandStates
270
258
  };
@@ -317,7 +317,7 @@ export class ConversationHistoryService {
317
317
  async refreshExternalActivity(session) {
318
318
  const runner = this.options.runners.require(session.runner);
319
319
  if (!runner.available(this.options.capabilities())) {
320
- this.options.setExternalActivity(session.id, 'UNAVAILABLE');
320
+ this.options.setExternalActivity(session.id, 'IDLE');
321
321
  return;
322
322
  }
323
323
  const activity = await runner.readExternalActivity(session);
@@ -31,6 +31,7 @@ export class ExternalSessionResumeService {
31
31
  return this.options.ensureConfiguration(prior);
32
32
  const runner = this.options.runners.product(external.runner);
33
33
  if (runner === undefined) {
34
+ this.options.state.externalActivity.set(external.id, 'IDLE');
34
35
  this.setFailure(external.id, 'RUNNER_UNAVAILABLE');
35
36
  return undefined;
36
37
  }
@@ -78,6 +79,8 @@ export class ExternalSessionResumeService {
78
79
  }
79
80
  if (result.activity !== undefined)
80
81
  this.options.state.externalActivity.set(external.id, result.activity);
82
+ else if (result.failureCode === 'RUNNER_UNAVAILABLE')
83
+ this.options.state.externalActivity.set(external.id, 'IDLE');
81
84
  if (result.session !== undefined) {
82
85
  this.options.state.resumedByDirectId.set(external.id, result.session.id);
83
86
  return result.session;
@@ -102,32 +105,12 @@ export class ExternalSessionResumeService {
102
105
  const resumed = await this.resume(current);
103
106
  if (resumed !== undefined)
104
107
  return resumed;
105
- if (this.options.state.externalActivity.get(current.id) === 'EXTERNAL_ACTIVE')
108
+ if (this.options.state.externalActivity.get(current.id) === 'BUSY')
106
109
  throw new Error('EXTERNAL_SESSION_ACTIVE');
107
110
  throw new Error(this.options.state.messageResumeFailures.get(current.id) ?? this.failure(current.id).code);
108
111
  }
109
112
  async acceptForMessage(sessionId) {
110
- const resumedId = this.options.state.resumedByDirectId.get(sessionId);
111
- const current = this.options.runtime.getAgentSession(resumedId ?? sessionId) ??
112
- (await this.options.resolve(sessionId));
113
- if (current === undefined)
114
- throw new Error('SESSION_NOT_FOUND');
115
- if (current.nativeControl !== 'EXTERNAL')
116
- return { session: current, ready: Promise.resolve(current), deferred: false };
117
- const staged = this.options.projectionFor(current) ?? this.options.stage(current);
118
- const ready = this.resume(current).then((session) => {
119
- if (session !== undefined)
120
- return session;
121
- if (this.options.state.externalActivity.get(current.id) === 'EXTERNAL_ACTIVE')
122
- throw new Error('EXTERNAL_SESSION_ACTIVE');
123
- throw new Error(this.options.state.messageResumeFailures.get(current.id) ?? this.failure(current.id).code);
124
- });
125
- return { session: staged, ready, deferred: true };
126
- }
127
- delivery(session) {
128
- return session.nativeControl === 'EXTERNAL' && session.externalSessionId !== null
129
- ? 'RESUMABLE'
130
- : 'REJECTED';
113
+ return { session: await this.resolveForMessage(sessionId) };
131
114
  }
132
115
  clearFailure(sessionId) {
133
116
  const timer = this.options.state.failureTimers.get(sessionId);
@@ -68,7 +68,7 @@ export class NodeControlMessageService {
68
68
  this.cancelRun(envelope);
69
69
  return;
70
70
  }
71
- if (envelope.type === 'session.resume' || envelope.type === 'lease.reconcile') {
71
+ if (envelope.type === 'lease.reconcile') {
72
72
  this.options.send('ack', { type: envelope.type }, envelope.id);
73
73
  return;
74
74
  }
@@ -62,7 +62,6 @@ export class NodeUpgradeCoordinator {
62
62
  blocks(operation) {
63
63
  return (this.pending() &&
64
64
  (operation === 'session.start' ||
65
- operation === 'session.resume' ||
66
65
  operation === 'session.message' ||
67
66
  operation === 'session.command.execute' ||
68
67
  operation === 'runner.command.execute' ||
@@ -11,7 +11,6 @@ export class SessionCatalogService {
11
11
  operations() {
12
12
  return {
13
13
  'session.list': (data) => this.list(record(data)),
14
- 'session.resume': (data) => this.resume(record(data)),
15
14
  'session.remove': (data) => this.enqueueMutation(() => this.remove(record(data))),
16
15
  'workspace.sessions.cleanup': (data) => this.enqueueMutation(() => this.cleanupWorkspaceSessions(record(data))),
17
16
  'session.rewind': (data) => this.enqueueMutation(() => this.rewind(record(data))),
@@ -100,30 +99,6 @@ export class SessionCatalogService {
100
99
  }));
101
100
  }
102
101
  }
103
- async resume(input) {
104
- if (typeof input.sessionId !== 'string')
105
- throw new Error('SESSION_INVALID');
106
- const value = this.options.runtime.getAgentSession(input.sessionId) ??
107
- (await this.options.projection.resolve(input.sessionId));
108
- if (value === undefined)
109
- throw new Error('SESSION_NOT_FOUND');
110
- if (value.nativeControl !== 'EXTERNAL')
111
- return {
112
- session: this.options.present(value),
113
- delivery: this.options.resume.delivery(value)
114
- };
115
- const resumed = await this.options.resume.resume(value);
116
- const failure = resumed === undefined ? this.options.resume.failure(value.id) : undefined;
117
- return {
118
- session: this.options.present(resumed ?? value),
119
- delivery: resumed === undefined ? 'REJECTED' : 'RESUMABLE',
120
- ...(resumed === undefined
121
- ? {
122
- reasonCode: failure?.code ?? 'SESSION_RESUME_REJECTED'
123
- }
124
- : {})
125
- };
126
- }
127
102
  enqueueMutation(operation) {
128
103
  const result = this.rewindQueue.then(operation);
129
104
  this.rewindQueue = result.then(() => undefined, () => undefined);
@@ -263,7 +238,7 @@ export class SessionCatalogService {
263
238
  this.options.runtime
264
239
  .listRunsForSession(session.id)
265
240
  .some((run) => !isTerminalRunStatus(run.status)) ||
266
- this.options.externalActivity(session.id) === 'EXTERNAL_ACTIVE') {
241
+ this.options.externalActivity(session.id) === 'BUSY') {
267
242
  skipped += 1;
268
243
  continue;
269
244
  }
@@ -317,7 +292,7 @@ export class SessionCatalogService {
317
292
  throw new Error('SESSION_ACTIVE');
318
293
  if (session.nativeControl === 'EXTERNAL') {
319
294
  await this.options.refreshActivity(session);
320
- if (this.options.externalActivity(session.id) === 'EXTERNAL_ACTIVE')
295
+ if (this.options.externalActivity(session.id) === 'BUSY')
321
296
  throw new Error('SESSION_ACTIVE');
322
297
  }
323
298
  if (requireCleanupSafety) {
@@ -39,9 +39,9 @@ export class SessionCommandService {
39
39
  (await this.options.projection.resolveCurrent(input.sessionId));
40
40
  if (resolved === undefined)
41
41
  throw new Error('SESSION_NOT_FOUND');
42
- const session = resolved.nativeControl === 'EXTERNAL' ? await this.options.resume.resume(resolved) : resolved;
43
- if (session === undefined)
44
- throw new Error('RUNNER_COMMAND_UNAVAILABLE');
42
+ if (resolved.nativeControl === 'EXTERNAL')
43
+ throw new Error('SESSION_ACTIVATION_REQUIRED');
44
+ const session = resolved;
45
45
  const command = this.options
46
46
  .profiles()
47
47
  .find((profile) => profile.runner === session.runner)
@@ -88,7 +88,7 @@ export class SessionMessageService {
88
88
  const publicContent = missionMessage?.activated === true ? 'Mission Start' : content;
89
89
  const intent = missionMessage === undefined ? requestedIntent : 'REPLACE_CURRENT';
90
90
  const operation = (async () => {
91
- const acceptedSession = accepted.deferred ? accepted.session : await accepted.ready;
91
+ const acceptedSession = accepted.session;
92
92
  const session = this.options.runtime.getAgentSession(acceptedSession.id) ?? acceptedSession;
93
93
  const runId = randomUUID();
94
94
  const queueBeforeCreate = this.options.runtime.sessionQueue(session.id);
@@ -108,16 +108,12 @@ export class SessionMessageService {
108
108
  const sessionForPresentation = input.__mission === true
109
109
  ? session
110
110
  : this.options.runtime.setTemporaryTitleIfMissing(session.id, sessionTitleFromFirstMessage(content));
111
- const deferPreparation = accepted.deferred && attachmentIds.length === 0 && inlineAttachments.length === 0;
112
111
  this.options.emitSession(sessionForPresentation);
113
112
  if (created.queueItem === undefined)
114
113
  this.options.coordinator.publishCurrentTurn(created.run.id);
115
114
  const prepare = async () => {
116
115
  try {
117
- const readySession = await accepted.ready;
118
- const currentSession = this.options.runtime.getAgentSession(readySession.id) ?? readySession;
119
- if (deferPreparation)
120
- this.options.emitSession(currentSession);
116
+ const currentSession = this.options.runtime.getAgentSession(session.id) ?? session;
121
117
  const uploaded = await this.options.uploads.resolve(session.workspaceId, attachmentIds);
122
118
  const attachments = attachmentIds.length === 0
123
119
  ? inlineAttachments
@@ -190,15 +186,7 @@ export class SessionMessageService {
190
186
  throw error;
191
187
  }
192
188
  };
193
- if (deferPreparation)
194
- void prepare().catch(() => undefined);
195
- const delivery = deferPreparation
196
- ? created.queueItem === undefined
197
- ? 'STARTED'
198
- : intent === 'REPLACE_CURRENT'
199
- ? 'RESTARTING'
200
- : 'QUEUED'
201
- : await prepare();
189
+ const delivery = await prepare();
202
190
  return {
203
191
  session: this.options.present(sessionForPresentation),
204
192
  run: created.run,
@@ -14,7 +14,6 @@ export class SessionPresentationService {
14
14
  .require(session.runner)
15
15
  .presentSession(session, this.options.capabilities(), {
16
16
  managedActive: this.options.hasManagedActiveRun(session),
17
- externalActivity: this.options.externalActivity(session.id),
18
17
  commandStates: this.options.commands.list(session.id)
19
18
  });
20
19
  return {
@@ -88,6 +88,7 @@ const buildWaiters = [];
88
88
  export class WorkspaceCodeNavigationService {
89
89
  options;
90
90
  builds = new Map();
91
+ preparations = new Map();
91
92
  candidateRefreshes = new Map();
92
93
  locations = new Map();
93
94
  failures = new Map();
@@ -96,6 +97,49 @@ export class WorkspaceCodeNavigationService {
96
97
  constructor(options) {
97
98
  this.options = options;
98
99
  }
100
+ async prepare(workspace, input) {
101
+ if (typeof input.path !== 'string' || input.path.length === 0 || input.path.length > 4096)
102
+ throw new Error('SYMBOL_DEFINITION_INVALID');
103
+ if (!isNavigablePath(input.path))
104
+ return { state: 'READY' };
105
+ if (!(await this.available()))
106
+ return { state: 'UNAVAILABLE' };
107
+ const location = await this.indexLocation(workspace);
108
+ this.locations.set(workspace.id, location);
109
+ this.forgotten.delete(location.directory);
110
+ const manifest = await readManifest(location.manifest);
111
+ const tags = manifest === undefined
112
+ ? undefined
113
+ : await tagsPath(location, manifest).catch(() => undefined);
114
+ const reusable = manifest !== undefined &&
115
+ tags !== undefined &&
116
+ (await tagsAvailable(tags)) &&
117
+ manifest.sourceFiles !== undefined;
118
+ const current = reusable &&
119
+ manifest.schemaVersion === INDEX_SCHEMA_VERSION &&
120
+ manifest.optionsVersion === INDEX_OPTIONS_VERSION &&
121
+ manifest.ctagsVersion === BUNDLED_CTAGS_VERSION;
122
+ const cachedCandidates = this.options.files.index(workspace).peekContentCandidates();
123
+ const forceRefreshGeneration = this.forceFreshCandidates.get(workspace.id);
124
+ if (reusable) {
125
+ if (!current || cachedCandidates === undefined || forceRefreshGeneration !== undefined)
126
+ this.refreshCandidatesInBackground(workspace, location, manifest, {
127
+ requireFresh: forceRefreshGeneration !== undefined,
128
+ mustBuild: !current || forceRefreshGeneration !== undefined,
129
+ forceRefreshGeneration
130
+ });
131
+ return { state: current ? 'READY' : 'STALE' };
132
+ }
133
+ const failure = this.failures.get(location.directory);
134
+ if (failure !== undefined &&
135
+ !this.builds.has(location.directory) &&
136
+ !this.preparations.has(location.directory)) {
137
+ this.failures.delete(location.directory);
138
+ return { state: 'FAILED', failureCode: failure };
139
+ }
140
+ this.startInitialBuild(workspace, location);
141
+ return { state: 'BUILDING' };
142
+ }
99
143
  async definition(workspace, input) {
100
144
  const path = input.path;
101
145
  const symbol = input.symbol;
@@ -276,15 +320,32 @@ export class WorkspaceCodeNavigationService {
276
320
  };
277
321
  }
278
322
  startBuild(input, location, previous) {
279
- void this.ensureBuild(input, location, previous).catch((error) => {
280
- const failure = buildFailure(error);
281
- this.failures.set(location.directory, failure);
282
- nodeLog('workspace.code_navigation.index.failed', {
283
- nodeId: this.options.nodeId(),
284
- failureCode: failure,
285
- sourceFiles: input.entries.length,
286
- candidateScanMs: input.candidateScanMs
287
- });
323
+ void this.ensureBuild(input, location, previous).catch((error) => this.recordBuildFailure(location, error, input));
324
+ }
325
+ startInitialBuild(workspace, location) {
326
+ if (this.builds.has(location.directory) || this.preparations.has(location.directory))
327
+ return;
328
+ const preparation = this.indexInput(workspace, true)
329
+ .then((input) => {
330
+ if (this.forgotten.has(location.directory))
331
+ return;
332
+ return this.ensureBuild(input, location);
333
+ })
334
+ .catch((error) => this.recordBuildFailure(location, error))
335
+ .finally(() => {
336
+ if (this.preparations.get(location.directory) === preparation)
337
+ this.preparations.delete(location.directory);
338
+ });
339
+ this.preparations.set(location.directory, preparation);
340
+ }
341
+ recordBuildFailure(location, error, input) {
342
+ const failure = buildFailure(error);
343
+ this.failures.set(location.directory, failure);
344
+ nodeLog('workspace.code_navigation.index.failed', {
345
+ nodeId: this.options.nodeId(),
346
+ failureCode: failure,
347
+ sourceFiles: input?.entries.length ?? 0,
348
+ candidateScanMs: input?.candidateScanMs ?? 0
288
349
  });
289
350
  }
290
351
  refreshCandidatesInBackground(workspace, location, manifest, options = {
@@ -53,29 +53,38 @@ export class WorkspaceContentSearchService {
53
53
  search(workspace, input) {
54
54
  if (typeof input.query !== 'string' || input.query.length < 1 || input.query.length > 256)
55
55
  throw new Error('FILE_CONTENT_SEARCH_INVALID');
56
- const key = `${workspace.id}\0${input.query.toLocaleLowerCase()}`;
56
+ if (input.regex !== undefined && typeof input.regex !== 'boolean')
57
+ throw new Error('FILE_CONTENT_SEARCH_INVALID');
58
+ const search = { query: input.query, regex: input.regex === true };
59
+ const queryKey = search.regex ? search.query : search.query.toLocaleLowerCase();
60
+ const key = `${workspace.id}\0${search.regex}\0${queryKey}`;
57
61
  const existing = this.pending.get(key);
58
62
  if (existing !== undefined)
59
63
  return existing;
60
- const task = this.perform(workspace, input.query).finally(() => this.pending.delete(key));
64
+ const task = this.perform(workspace, search).finally(() => this.pending.delete(key));
61
65
  this.pending.set(key, task);
62
66
  return task;
63
67
  }
64
- async perform(workspace, query) {
68
+ async perform(workspace, search) {
65
69
  const deadline = Date.now() + TIMEOUT_MS;
66
70
  const release = await acquireSearch(deadline);
67
71
  try {
72
+ if (search.regex && this.ripgrep === null)
73
+ throw new Error('RIPGREP_RESOURCE_UNAVAILABLE');
68
74
  const prepared = await this.candidates(workspace, deadline);
69
- if (prepared.files.length === 0)
75
+ if (prepared.files.length === 0) {
76
+ if (search.regex)
77
+ await this.validateNativeRegex(workspace.path, search, deadline);
70
78
  return {
71
79
  matches: [],
72
80
  engine: 'NODE',
73
81
  truncated: prepared.truncated || Date.now() >= deadline,
74
82
  skippedFiles: prepared.skippedFiles
75
83
  };
84
+ }
76
85
  if (this.ripgrep !== null) {
77
86
  try {
78
- const matches = await this.nativeSearch('RIPGREP', workspace.path, prepared.files, query, deadline);
87
+ const matches = await this.nativeSearch('RIPGREP', workspace.path, prepared.files, search, deadline);
79
88
  return {
80
89
  matches: matches.slice(0, RESULT_LIMIT),
81
90
  engine: 'RIPGREP',
@@ -83,11 +92,13 @@ export class WorkspaceContentSearchService {
83
92
  skippedFiles: prepared.skippedFiles
84
93
  };
85
94
  }
86
- catch {
95
+ catch (error) {
96
+ if (search.regex)
97
+ throw error;
87
98
  // Missing, incompatible or failed bundled resources safely fall through.
88
99
  }
89
100
  }
90
- const fallback = await nodeSearch(prepared.files, query, deadline);
101
+ const fallback = await nodeSearch(prepared.files, search.query, deadline);
91
102
  return {
92
103
  matches: fallback.matches.slice(0, RESULT_LIMIT),
93
104
  engine: 'NODE',
@@ -99,6 +110,14 @@ export class WorkspaceContentSearchService {
99
110
  release();
100
111
  }
101
112
  }
113
+ async validateNativeRegex(root, search, deadline) {
114
+ const command = commandFor('RIPGREP', [], search, this.ripgrep ?? undefined);
115
+ const result = await runCommand(command.file, [...command.args, '-'], root, deadline);
116
+ if (result.code === 2)
117
+ throw new Error('FILE_CONTENT_SEARCH_INVALID');
118
+ if (!command.validCodes.includes(result.code))
119
+ throw new Error('CONTENT_SEARCH_COMMAND_FAILED');
120
+ }
102
121
  async candidates(workspace, deadline) {
103
122
  const root = await realpath(workspace.path);
104
123
  let candidates;
@@ -139,21 +158,24 @@ export class WorkspaceContentSearchService {
139
158
  }
140
159
  return { files, skippedFiles, truncated };
141
160
  }
142
- async nativeSearch(engine, root, files, query, deadline) {
161
+ async nativeSearch(engine, root, files, search, deadline) {
143
162
  const matches = [];
144
163
  const argumentBatchBytes = this.platform === 'win32' ? WINDOWS_ARGUMENT_BATCH_BYTES : POSIX_ARGUMENT_BATCH_BYTES;
145
164
  for (const batch of batches(files, argumentBatchBytes)) {
146
165
  if (Date.now() >= deadline || matches.length > RESULT_LIMIT)
147
166
  break;
148
- const command = commandFor(engine, batch, query, this.ripgrep ?? undefined);
167
+ const command = commandFor(engine, batch, search, this.ripgrep ?? undefined);
149
168
  const result = await runCommand(command.file, command.args, root, deadline, engine === 'RIPGREP');
150
- if (!command.validCodes.includes(result.code))
169
+ if (!command.validCodes.includes(result.code)) {
170
+ if (search.regex && result.code === 2)
171
+ throw new Error('FILE_CONTENT_SEARCH_INVALID');
151
172
  throw new Error('CONTENT_SEARCH_COMMAND_FAILED');
173
+ }
152
174
  const paths = new Map(batch.flatMap((file) => [
153
175
  [normalizeResultPath(file.path, this.platform), file.path],
154
176
  [normalizeResultPath(file.absolutePath, this.platform), file.path]
155
177
  ]));
156
- matches.push(...parseOutput(engine, result.stdout, query).map((match) => ({
178
+ matches.push(...parseOutput(engine, result.stdout, search).map((match) => ({
157
179
  ...match,
158
180
  path: paths.get(normalizeResultPath(match.path, this.platform)) ??
159
181
  (() => {
@@ -213,7 +235,7 @@ function batches(files, maximumBytes) {
213
235
  result.push(current);
214
236
  return result;
215
237
  }
216
- function commandFor(engine, files, query, ripgrep) {
238
+ function commandFor(engine, files, search, ripgrep) {
217
239
  const paths = files.map((file) => file.path);
218
240
  if (engine === 'RIPGREP')
219
241
  return {
@@ -224,12 +246,12 @@ function commandFor(engine, files, query, ripgrep) {
224
246
  args: [
225
247
  '--json',
226
248
  '--no-config',
227
- '--fixed-strings',
228
249
  '--ignore-case',
229
250
  '--max-count',
230
251
  String(PER_FILE_LIMIT),
252
+ ...(search.regex ? [] : ['--fixed-strings']),
231
253
  '--',
232
- query,
254
+ search.query,
233
255
  ...paths
234
256
  ],
235
257
  validCodes: [0, 1]
@@ -301,14 +323,14 @@ async function runCommand(file, args, cwd, deadline, stopAfterResultLimit = fals
301
323
  });
302
324
  });
303
325
  }
304
- function parseOutput(engine, output, query) {
326
+ function parseOutput(engine, output, search) {
305
327
  if (engine === 'RIPGREP')
306
- return parseRipgrep(output, query);
328
+ return parseRipgrep(output, search);
307
329
  if (engine === 'GREP')
308
- return parseGrep(output, query);
309
- return parsePowerShell(output, query);
330
+ return parseGrep(output, search);
331
+ return parsePowerShell(output, search);
310
332
  }
311
- function parseRipgrep(output, query) {
333
+ function parseRipgrep(output, search) {
312
334
  return output
313
335
  .toString('utf8')
314
336
  .split('\n')
@@ -320,7 +342,9 @@ function parseRipgrep(output, query) {
320
342
  return [];
321
343
  const data = value.data;
322
344
  const preview = String(data?.lines?.text).replace(/[\r\n]+$/u, '');
323
- const column = lowerIndex(preview, query);
345
+ const column = search.regex
346
+ ? regexRipgrepIndex(preview, data?.submatches)
347
+ : lowerIndex(preview, search.query);
324
348
  if (typeof data?.path?.text !== 'string' || !Number.isInteger(data.line_number) || column < 0)
325
349
  throw new Error('CONTENT_SEARCH_OUTPUT_INVALID');
326
350
  return [
@@ -333,7 +357,7 @@ function parseRipgrep(output, query) {
333
357
  ];
334
358
  });
335
359
  }
336
- function parseGrep(output, query) {
360
+ function parseGrep(output, search) {
337
361
  const matches = [];
338
362
  let offset = 0;
339
363
  while (offset < output.length) {
@@ -345,7 +369,7 @@ function parseGrep(output, query) {
345
369
  const record = output.subarray(separator + 1, end).toString('utf8');
346
370
  const colon = record.indexOf(':');
347
371
  const preview = record.slice(colon + 1).replace(/\r$/u, '');
348
- const column = lowerIndex(preview, query);
372
+ const column = lowerIndex(preview, search.query);
349
373
  if (colon < 1 || column < 0)
350
374
  throw new Error('CONTENT_SEARCH_OUTPUT_INVALID');
351
375
  matches.push({
@@ -358,7 +382,7 @@ function parseGrep(output, query) {
358
382
  }
359
383
  return matches;
360
384
  }
361
- function parsePowerShell(output, query) {
385
+ function parsePowerShell(output, search) {
362
386
  return output
363
387
  .toString('utf8')
364
388
  .split(/\r?\n/u)
@@ -370,7 +394,7 @@ function parsePowerShell(output, query) {
370
394
  !Number.isInteger(match.line) ||
371
395
  typeof match.preview !== 'string')
372
396
  throw new Error('CONTENT_SEARCH_OUTPUT_INVALID');
373
- const column = lowerIndex(match.preview, query);
397
+ const column = lowerIndex(match.preview, search.query);
374
398
  if (column < 0)
375
399
  throw new Error('CONTENT_SEARCH_OUTPUT_INVALID');
376
400
  return [
@@ -445,6 +469,12 @@ async function nodeSearch(files, query, deadline) {
445
469
  function lowerIndex(source, query) {
446
470
  return source.toLocaleLowerCase().indexOf(query.toLocaleLowerCase());
447
471
  }
472
+ function regexRipgrepIndex(preview, submatches) {
473
+ const start = submatches?.[0]?.start;
474
+ if (typeof start !== 'number' || !Number.isInteger(start) || start < 0)
475
+ return -1;
476
+ return Buffer.from(preview, 'utf8').subarray(0, start).toString('utf8').length;
477
+ }
448
478
  function limitPerFile(matches) {
449
479
  const counts = new Map();
450
480
  return matches.filter((match) => {
@@ -1,7 +1,7 @@
1
1
  import { tmpdir } from 'node:os';
2
2
  import { isAbsolute, win32 } from 'node:path';
3
3
  import { listWorkspaceFiles, mutateWorkspaceFile, normalizeFilePreviewPath, readAllowedFileContentRange, readWorkspaceFileContentRange, readAllowedTextFile, readWorkspaceTextFile, writeWorkspaceTextFile, WorkspaceFileIndex } from '../workspace.js';
4
- const INDEX_CACHE_LIMIT = 8;
4
+ const INDEX_CACHE_LIMIT = 32;
5
5
  export class WorkspaceFileService {
6
6
  state;
7
7
  allowedRoots;
@@ -37,6 +37,7 @@ export class WorkspaceWorkbenchService {
37
37
  'workspace.files.search': (data) => this.searchFiles(record(data)),
38
38
  'workspace.files.mutate': (data) => this.mutateFile(record(data)),
39
39
  'workspace.content.search': (data) => this.searchContent(record(data)),
40
+ 'workspace.symbol.prepare': (data) => this.prepareSymbolIndex(record(data)),
40
41
  'workspace.symbol.definition': (data) => this.symbolDefinition(record(data)),
41
42
  'workspace.files.upload.preflight': (data) => this.preflight(record(data)),
42
43
  'workspace.files.upload.create': (data) => this.createUpload(record(data)),
@@ -116,6 +117,10 @@ export class WorkspaceWorkbenchService {
116
117
  const workspace = this.options.requireWorkspace(input.workspaceId);
117
118
  return { definition: await this.codeNavigation.definition(workspace, input) };
118
119
  }
120
+ async prepareSymbolIndex(input) {
121
+ const workspace = this.options.requireWorkspace(input.workspaceId);
122
+ return { preparation: await this.codeNavigation.prepare(workspace, input) };
123
+ }
119
124
  async preflight(input) {
120
125
  return {
121
126
  uploads: await this.options.uploads.preflight(this.options.requireWorkspace(input.workspaceId), input)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.9.95",
3
+ "version": "0.9.97",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "license": "Apache-2.0 AND GPL-2.0-or-later",
6
6
  "type": "module",
@@ -30,8 +30,8 @@
30
30
  "npm": "11.12.1",
31
31
  "ws": "^8.21.3",
32
32
  "zod": "4.4.3",
33
- "@myagentroam/agent": "0.9.95",
34
- "@myagentroam/protocol": "0.9.95"
33
+ "@myagentroam/agent": "0.9.97",
34
+ "@myagentroam/protocol": "0.9.97"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/ws": "^8.18.1"