@myagentroam/node 0.10.35 → 0.10.37

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 (33) hide show
  1. package/dist/connector.js +115 -18
  2. package/dist/database.js +29 -12
  3. package/dist/main.js +18 -2
  4. package/dist/migrations/v019.js +9 -0
  5. package/dist/migrations/v020.js +46 -0
  6. package/dist/runner/mar-agent/computer-use-probe.js +126 -0
  7. package/dist/runner/mar-agent/computer-use.js +50 -20
  8. package/dist/runner/mar-agent/conversation-normalizer.js +28 -0
  9. package/dist/runner/mar-agent/marctl-skill.js +3 -2
  10. package/dist/runner/mar-agent-runner.js +32 -0
  11. package/dist/runtime-state.js +15 -10
  12. package/dist/service/continuous-run-coordinator.js +58 -5
  13. package/dist/service/direct-transfer-service.js +58 -11
  14. package/dist/service/external-session-resume-service.js +2 -1
  15. package/dist/service/goal-runtime.js +13 -0
  16. package/dist/service/hosted-conversation-node-service.js +5 -4
  17. package/dist/service/native-session-projection-service.js +77 -11
  18. package/dist/service/planned-restart-handoff.js +279 -0
  19. package/dist/service/restart-session-restorer.js +141 -0
  20. package/dist/service/{scheduled-run-authorization-control.js → run-authorization-control.js} +17 -10
  21. package/dist/service/run-event-service.js +2 -0
  22. package/dist/service/scheduled-session-coordinator.js +13 -0
  23. package/dist/service/scheduled-session-store.js +24 -0
  24. package/dist/service/session-catalog-service.js +57 -4
  25. package/dist/service/session-command-service.js +1 -0
  26. package/dist/service/session-lifecycle-service.js +25 -8
  27. package/dist/service/session-message-service.js +80 -15
  28. package/dist/service/session-presentation-service.js +2 -0
  29. package/dist/service/session-query-service.js +14 -9
  30. package/dist/service/workspace-queue-workbench-service.js +15 -8
  31. package/dist/service/workspace-upload-service.js +128 -29
  32. package/dist/supervisor.js +7 -3
  33. package/package.json +4 -4
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { assertScheduleTimezone, nextScheduledAt, parseScheduledCron } from './scheduled-session-recurrence.js';
3
+ export const SCHEDULE_RESTART_OUTCOME_UNKNOWN = 'SCHEDULE_RESTART_OUTCOME_UNKNOWN';
3
4
  export class ScheduledSessionStore {
4
5
  database;
5
6
  now;
@@ -176,6 +177,26 @@ export class ScheduledSessionStore {
176
177
  throw error;
177
178
  }
178
179
  }
180
+ recoverInterrupted() {
181
+ return this.write(() => {
182
+ // FAILED closes scheduling bookkeeping, not the unknown execution or its side effects.
183
+ const rows = this.database
184
+ .prepare(`UPDATE scheduled_session_occurrences SET status='FAILED',code=?,updated_at=?
185
+ WHERE status IN ('CLAIMED','SUBMITTED') RETURNING id,task_id`)
186
+ .all(SCHEDULE_RESTART_OUTCOME_UNKNOWN, this.now());
187
+ const tasks = new Map();
188
+ for (const row of rows) {
189
+ if (tasks.has(row.task_id))
190
+ continue;
191
+ const task = this.getById(row.task_id);
192
+ if (task === undefined)
193
+ throw new Error('SCHEDULE_TASK_NOT_FOUND');
194
+ tasks.set(task.id, task);
195
+ this.pruneTerminal(row.id);
196
+ }
197
+ return rows.map((row) => ({ task: tasks.get(row.task_id), occurrenceId: row.id }));
198
+ });
199
+ }
179
200
  finish(occurrenceId, status, code, runId) {
180
201
  const result = this.database
181
202
  .prepare(`UPDATE scheduled_session_occurrences SET status=?,code=?,run_id=?,updated_at=?
@@ -183,6 +204,9 @@ export class ScheduledSessionStore {
183
204
  .run(status, code ?? null, runId ?? null, this.now(), occurrenceId);
184
205
  if (result.changes === 0 || status === 'SUBMITTED')
185
206
  return;
207
+ this.pruneTerminal(occurrenceId);
208
+ }
209
+ pruneTerminal(occurrenceId) {
186
210
  this.database
187
211
  .prepare(`DELETE FROM scheduled_session_occurrences
188
212
  WHERE task_id=(SELECT task_id FROM scheduled_session_occurrences WHERE id=?)
@@ -11,6 +11,7 @@ export class SessionCatalogService {
11
11
  operations() {
12
12
  return {
13
13
  'session.list': (data) => this.list(record(data)),
14
+ 'session.graph': (data) => this.graph(record(data)),
14
15
  'session.remove': (data) => this.enqueueMutation(() => this.remove(record(data))),
15
16
  'workspace.sessions.cleanup': (data) => this.enqueueMutation(() => this.cleanupWorkspaceSessions(record(data))),
16
17
  'session.rewind': (data) => this.enqueueMutation(() => this.rewind(record(data))),
@@ -18,11 +19,59 @@ export class SessionCatalogService {
18
19
  };
19
20
  }
20
21
  async list(input) {
22
+ const sessions = await this.collect(input);
23
+ return sessionPage(sessions.filter((session) => input.parentSessionId === undefined || session.parentSessionId === input.parentSessionId), input);
24
+ }
25
+ async graph(input) {
26
+ if (typeof input.workspaceId !== 'string')
27
+ throw new Error('WORKSPACE_NOT_FOUND');
28
+ if (typeof input.sessionId !== 'string')
29
+ throw new Error('SESSION_INVALID');
30
+ const limit = input.limit === undefined ? 100 : input.limit;
31
+ if (typeof limit !== 'number' || !Number.isInteger(limit) || limit < 1 || limit > 100)
32
+ throw new Error('SESSION_INVALID');
33
+ const sessions = await this.collect({ workspaceId: input.workspaceId, registeredOnly: true });
34
+ const root = sessions.find((session) => session.id === input.sessionId);
35
+ if (root === undefined)
36
+ throw new Error('SESSION_NOT_FOUND');
37
+ const children = new Map();
38
+ for (const session of sessions) {
39
+ if (session.parentSessionId == null)
40
+ continue;
41
+ const siblings = children.get(session.parentSessionId) ?? [];
42
+ siblings.push(session);
43
+ children.set(session.parentSessionId, siblings);
44
+ }
45
+ const queue = [root];
46
+ const visited = new Set([root.id]);
47
+ const nodes = [];
48
+ for (let index = 0; index < queue.length && index < limit; index += 1) {
49
+ const session = queue[index];
50
+ nodes.push({
51
+ id: session.id,
52
+ parentSessionId: session.parentSessionId ?? null,
53
+ title: session.customTitle ?? session.runnerTitle ?? 'Untitled session',
54
+ runner: session.runner,
55
+ ...('activity' in session ? { activity: session.activity } : {})
56
+ });
57
+ for (const child of children.get(session.id) ?? []) {
58
+ if (visited.has(child.id))
59
+ continue;
60
+ visited.add(child.id);
61
+ queue.push(child);
62
+ }
63
+ }
64
+ return { rootSessionId: root.id, nodes, truncated: queue.length > nodes.length };
65
+ }
66
+ async collect(input) {
21
67
  const workspaceId = typeof input.workspaceId === 'string' ? input.workspaceId : undefined;
68
+ const registeredOnly = input.registeredOnly === true;
69
+ if (registeredOnly && workspaceId === undefined)
70
+ throw new Error('WORKSPACE_NOT_FOUND');
22
71
  const query = typeof input.query === 'string' && input.query.trim().length > 0
23
72
  ? input.query.trim().normalize('NFKC').toLocaleLowerCase()
24
73
  : undefined;
25
- const nativeSessions = workspaceId === undefined
74
+ const nativeSessions = registeredOnly || workspaceId === undefined
26
75
  ? this.options.cachedNative()
27
76
  : query === undefined
28
77
  ? await this.options.cachedWorkspace(workspaceId)
@@ -57,7 +106,7 @@ export class SessionCatalogService {
57
106
  this.options.projectionForNative(candidate) === undefined)
58
107
  ].map((session) => [session.id, session]));
59
108
  if (workspaceId !== undefined)
60
- this.mergePersisted(merged, workspaceId, query === undefined && this.options.discoveryPending(workspaceId));
109
+ this.mergePersisted(merged, workspaceId, !registeredOnly && query === undefined && this.options.discoveryPending(workspaceId));
61
110
  const sessions = [...merged.values()].filter((session) => {
62
111
  if (query === undefined)
63
112
  return true;
@@ -66,9 +115,9 @@ export class SessionCatalogService {
66
115
  .toLocaleLowerCase();
67
116
  return title.includes(query);
68
117
  });
69
- return sessionPage(workspaceId === undefined
118
+ return workspaceId === undefined
70
119
  ? sessions
71
- : sessions.filter((session) => session.workspaceId === workspaceId), input);
120
+ : sessions.filter((session) => session.workspaceId === workspaceId);
72
121
  }
73
122
  mergePersisted(merged, workspaceId, discoveryPending) {
74
123
  const database = this.options.database();
@@ -95,6 +144,7 @@ export class SessionCatalogService {
95
144
  access: persisted.metadata.access,
96
145
  customTitle: persisted.metadata.customTitle,
97
146
  sessionPrompt: persisted.metadata.sessionPrompt ?? null,
147
+ parentSessionId: persisted.metadata.parentSessionId ?? null,
98
148
  runnerTitle: persisted.metadata.runnerTitle,
99
149
  pinnedAt: persisted.pinOrder === null ? null : 1,
100
150
  pinOrder: persisted.pinOrder,
@@ -249,6 +299,7 @@ export class SessionCatalogService {
249
299
  session.lastActivityAt >= cutoff)) ||
250
300
  (session.pinOrder !== null && session.pinOrder !== undefined) ||
251
301
  this.options.isWatching(session.id) ||
302
+ this.options.isRecovering?.(session.id) ||
252
303
  this.options.runtime.sessionQueue(session.id).itemIds.length > 0 ||
253
304
  this.options.runtime
254
305
  .listRunsForSession(session.id)
@@ -301,6 +352,8 @@ export class SessionCatalogService {
301
352
  ? this.options.runtime.beginSessionHistoryMutation(session.id)
302
353
  : undefined;
303
354
  try {
355
+ if (this.options.isRecovering?.(session.id))
356
+ throw new Error('SESSION_RESTART_PENDING');
304
357
  const runner = this.options.runners.require(session.runner);
305
358
  const missingCandidate = requireCleanupSafety && session.externalDiscovered === false;
306
359
  if (missingCandidate && !(await runner.isNativeSessionMissing(session)))
@@ -72,6 +72,7 @@ export class SessionCommandService {
72
72
  return this.options.goal({
73
73
  session,
74
74
  objective: invocation.argument,
75
+ ...(input.__suppressNotifications === true ? { suppressNotifications: true } : {}),
75
76
  ...(input.__usageGrant === undefined
76
77
  ? {}
77
78
  : { usageGrant: modelUsageGrantSchema.parse(input.__usageGrant) }),
@@ -97,6 +97,20 @@ export class SessionLifecycleService {
97
97
  const workspace = database.getWorkspace(input.workspaceId);
98
98
  if (workspace === undefined)
99
99
  throw new Error('WORKSPACE_NOT_FOUND');
100
+ let parentSessionId;
101
+ if (input.parentSessionId !== undefined) {
102
+ if (typeof input.parentSessionId !== 'string' ||
103
+ input.parentSessionId.length === 0 ||
104
+ input.parentSessionId.length > 512)
105
+ throw new Error('SESSION_INVALID');
106
+ const liveParent = this.options.runtime.getAgentSession(input.parentSessionId);
107
+ const parent = liveParent ?? database.getSessionRecord(input.parentSessionId);
108
+ if (parent === undefined || parent.workspaceId !== workspace.id)
109
+ throw new Error('SESSION_NOT_FOUND');
110
+ if (liveParent?.externalSessionId === null)
111
+ throw new Error('SESSION_ACTIVATION_REQUIRED');
112
+ parentSessionId = liveParent?.externalSessionId ?? parent.id;
113
+ }
100
114
  const environment = parseSecretEnvironment(input.secretEnvironment);
101
115
  const saved = database.getRunnerDefaults().find((value) => value.runner === runner.name);
102
116
  const fallback = runner.defaultConfiguration(workspace, environment);
@@ -113,6 +127,7 @@ export class SessionLifecycleService {
113
127
  if (runner.name === 'mar-agent' && !['auto', 'full-access'].includes(configuration.access))
114
128
  throw new Error('SESSION_INVALID');
115
129
  const session = this.options.runtime.createAgentSession({
130
+ ...(parentSessionId === undefined ? {} : { parentSessionId }),
116
131
  workspaceId: input.workspaceId,
117
132
  runner: runner.name,
118
133
  cwd: input.cwd,
@@ -134,24 +149,25 @@ export class SessionLifecycleService {
134
149
  }
135
150
  async updateConfiguration(input) {
136
151
  if (typeof input.sessionId !== 'string' ||
137
- typeof input.model !== 'string' ||
152
+ (input.model === undefined && input.effort === undefined && input.access === undefined) ||
138
153
  !validSessionOption(input.model) ||
139
- typeof input.effort !== 'string' ||
140
154
  !validSessionEffort(input.effort) ||
141
- typeof input.access !== 'string' ||
142
155
  !validSessionOption(input.access))
143
156
  throw new Error('SESSION_INVALID');
144
157
  const current = this.options.runtime.getAgentSession(input.sessionId) ??
145
158
  (await this.options.projection.createMetadataProjection(input.sessionId));
146
- if (current === undefined)
159
+ if (current === undefined ||
160
+ (typeof input.workspaceId === 'string' && input.workspaceId !== current.workspaceId))
147
161
  throw new Error('SESSION_NOT_FOUND');
148
- if (current.runner === 'mar-agent' && !['auto', 'full-access'].includes(input.access))
162
+ if (current.runner === 'mar-agent' &&
163
+ input.access !== undefined &&
164
+ !['auto', 'full-access'].includes(input.access))
149
165
  throw new Error('SESSION_INVALID');
150
166
  const session = this.options.runtime.updateAgentSessionConfiguration({
151
167
  sessionId: current.id,
152
- model: input.model,
153
- effort: input.effort,
154
- access: input.access
168
+ ...(typeof input.model === 'string' ? { model: input.model } : {}),
169
+ ...(typeof input.effort === 'string' ? { effort: input.effort } : {}),
170
+ ...(typeof input.access === 'string' ? { access: input.access } : {})
155
171
  });
156
172
  if (session.externalSessionId !== null)
157
173
  this.options.database().saveNativeSessionMetadata({
@@ -232,6 +248,7 @@ export class SessionLifecycleService {
232
248
  sessionId: messageSessionId,
233
249
  clientMessageId: input.clientMessageId,
234
250
  content: input.content,
251
+ ...(input.__suppressNotifications === true ? { __suppressNotifications: true } : {}),
235
252
  ...(input.__requestUserId === undefined ? {} : { __requestUserId: input.__requestUserId }),
236
253
  ...(input.__usageGrant === undefined ? {} : { __usageGrant: input.__usageGrant }),
237
254
  ...(input.secretEnvironment === undefined
@@ -5,17 +5,29 @@ import { parseSecretEnvironment } from '../util/secret-environment.js';
5
5
  import { personalInstructionsForRunner } from '../util/personal-instructions.js';
6
6
  import { safeErrorCode } from '../util/safe-error.js';
7
7
  import { publicGoalStartText } from './goal-runtime.js';
8
- import { serializeComposerTag, composerVisibleText } from '@myagentroam/protocol';
8
+ import { serializeComposerTag, composerVisibleText, steerComposerMessage } from '@myagentroam/protocol';
9
9
  import { relative } from 'node:path';
10
10
  import { prepareComposerResourceInput, composerResourceInputTags, validateComposerSkills } from './composer-message-input.js';
11
11
  const INTERNAL_GOAL = Symbol('internalGoal');
12
12
  const INTERNAL_SCHEDULE = Symbol('internalSchedule');
13
+ const INTERNAL_RESTORE = Symbol('internalRestore');
13
14
  export class SessionMessageService {
14
15
  options;
15
16
  pendingMessages = new Map();
17
+ preparations = new Set();
18
+ accepting = true;
16
19
  constructor(options) {
17
20
  this.options = options;
18
21
  }
22
+ async drain() {
23
+ await Promise.allSettled([...this.preparations]);
24
+ }
25
+ setAccepting(accepting) {
26
+ this.accepting = accepting;
27
+ }
28
+ restoreQueued(input) {
29
+ return this.message({ ...input, [INTERNAL_RESTORE]: true });
30
+ }
19
31
  operations() {
20
32
  return {
21
33
  'session.message': (data) => this.message(record(data)),
@@ -26,7 +38,11 @@ export class SessionMessageService {
26
38
  submitGoal(input) {
27
39
  return this.message({
28
40
  [INTERNAL_GOAL]: true,
41
+ [INTERNAL_RESTORE]: input.restoreQueued === true,
29
42
  sessionId: input.sessionId,
43
+ __suppressNotifications: input.suppressNotifications === true ||
44
+ (input.kind === 'continue' &&
45
+ this.options.runtime.activeSessionRun(input.sessionId)?.suppressNotifications === true),
30
46
  clientMessageId: `goal:${randomUUID()}`,
31
47
  content: input.kind === 'start'
32
48
  ? publicGoalStartText(input.objective ?? '')
@@ -64,7 +80,14 @@ export class SessionMessageService {
64
80
  marAgentRuntimeScopeId: input.marAgentRuntimeScopeId
65
81
  });
66
82
  }
67
- async message(input) {
83
+ message(input) {
84
+ if (!this.accepting)
85
+ return Promise.reject(new Error('NODE_STOPPED'));
86
+ const task = this.prepareMessage(input);
87
+ this.preparations.add(task);
88
+ return task.finally(() => this.preparations.delete(task));
89
+ }
90
+ async prepareMessage(input) {
68
91
  if (typeof input.sessionId !== 'string' ||
69
92
  typeof input.clientMessageId !== 'string' ||
70
93
  input.clientMessageId.length === 0 ||
@@ -77,7 +100,9 @@ export class SessionMessageService {
77
100
  const content = input.content;
78
101
  const internalGoal = input[INTERNAL_GOAL] === true;
79
102
  const internalSchedule = input[INTERNAL_SCHEDULE] === true;
80
- const internalRunnerContent = (internalGoal || internalSchedule) && typeof input.__runnerContent === 'string'
103
+ const internalRestore = input[INTERNAL_RESTORE] === true;
104
+ const internalRunnerContent = (internalGoal || internalSchedule || internalRestore) &&
105
+ typeof input.__runnerContent === 'string'
81
106
  ? input.__runnerContent
82
107
  : content;
83
108
  const inlineAttachments = parseMessageAttachments(input.attachments);
@@ -112,9 +137,9 @@ export class SessionMessageService {
112
137
  ...(existing.kind === 'queue' ? { queueItemId: existing.item.id } : {}),
113
138
  delivery: existing.kind === 'queue' ? 'QUEUED' : 'STARTED',
114
139
  idempotent: true,
115
- auditReceipt: auditReceipt(session, existing.kind === 'queue' ? 'QUEUE' : 'SEND', existing.run.id, existing.kind === 'queue' ? existing.item.id : undefined)
140
+ auditReceipt: auditReceipt(session, existing.kind === 'queue' ? 'QUEUE' : 'SEND', existing.kind === 'queue' ? existing.item.id : existing.run.id, existing.kind === 'queue' ? existing.item.id : undefined)
116
141
  };
117
- const goalMessage = internalGoal || internalSchedule
142
+ const goalMessage = internalGoal || internalSchedule || internalRestore
118
143
  ? undefined
119
144
  : this.options.goalMessage(session, content, requestedIntent);
120
145
  const runnerContent = goalMessage?.runnerContent ?? internalRunnerContent;
@@ -128,23 +153,48 @@ export class SessionMessageService {
128
153
  const operation = (async () => {
129
154
  const acceptedSession = accepted.session;
130
155
  const session = this.options.runtime.getAgentSession(acceptedSession.id) ?? acceptedSession;
131
- const runId = randomUUID();
156
+ const runId = internalRestore && typeof input.__queueItemId === 'string'
157
+ ? input.__queueItemId
158
+ : randomUUID();
132
159
  const queueBeforeCreate = this.options.runtime.sessionQueue(session.id);
133
160
  const active = this.options.runtime.activeSessionRun(session.id);
134
161
  if (internalSchedule && (active !== undefined || queueBeforeCreate.itemIds.length > 0))
135
162
  throw new Error('SCHEDULE_SESSION_BUSY');
136
163
  const replaceCurrentRun = intent === 'REPLACE_CURRENT' ? active : undefined;
137
- const created = this.options.coordinator.accept({
164
+ const userSteer = requestedIntent === 'REPLACE_CURRENT' &&
165
+ goalMessage?.activated !== true &&
166
+ !internalGoal &&
167
+ !internalSchedule &&
168
+ !internalRestore;
169
+ const steerTarget = userSteer
170
+ ? {
171
+ turnId: active === undefined
172
+ ? undefined
173
+ : this.options.runtime.conversationTurnForRun(active.id)?.id,
174
+ executionId: active === undefined ? undefined : this.options.runtime.currentExecutionId(active.id)
175
+ }
176
+ : undefined;
177
+ const acceptedMessage = {
138
178
  runId,
139
179
  sessionId: session.id,
140
180
  clientMessageId,
141
181
  content: publicContent,
142
182
  ...(internalSchedule ? { source: 'SCHEDULED' } : {}),
183
+ ...(input.__suppressNotifications === true ? { suppressNotifications: true } : {}),
143
184
  ...(Number.isSafeInteger(input.__requestUserId)
144
185
  ? { initiatedByUserId: input.__requestUserId }
145
186
  : {}),
146
187
  ...(inlineAttachments.length === 0 ? {} : { attachments: inlineAttachments })
147
- }, secretEnvironment);
188
+ };
189
+ const created = internalRestore
190
+ ? {
191
+ run: undefined,
192
+ queueItem: this.options.runtime.createQueueItem({
193
+ ...acceptedMessage,
194
+ queueItemId: runId
195
+ }).item
196
+ }
197
+ : this.options.coordinator.accept(acceptedMessage, secretEnvironment);
148
198
  const submissionId = created.queueItem?.id ?? created.run.id;
149
199
  const sessionForPresentation = input.__goal === true
150
200
  ? session
@@ -193,6 +243,13 @@ export class SessionMessageService {
193
243
  else
194
244
  this.options.runtime.updateQueueItemAttachments(created.queueItem.id, attachments, displayInput);
195
245
  this.options.coordinator.prepare(submissionId, {
246
+ restartMessage: {
247
+ content,
248
+ attachments,
249
+ ...(Number.isSafeInteger(input.__requestUserId)
250
+ ? { initiatedByUserId: input.__requestUserId }
251
+ : {})
252
+ },
196
253
  ...(input.__usageGrant === undefined
197
254
  ? {}
198
255
  : { usageGrant: modelUsageGrantSchema.parse(input.__usageGrant) }),
@@ -201,6 +258,11 @@ export class SessionMessageService {
201
258
  workspaceId: currentSession.workspaceId,
202
259
  runner: currentSession.runner,
203
260
  input: runner.prepareMessageInput(runnerInput, planActive ? 'plan' : 'default'),
261
+ ...(userSteer
262
+ ? {
263
+ prepareSteerInput: () => runner.prepareMessageInput(steerComposerMessage(runnerInput), planActive ? 'plan' : 'default')
264
+ }
265
+ : {}),
204
266
  clientMessageId,
205
267
  attachments: runnerImageAttachments,
206
268
  attachmentPaths,
@@ -235,12 +297,15 @@ export class SessionMessageService {
235
297
  !this.options.prepareGoalInsertion(session.id, submissionId, goalMessage.goal))
236
298
  throw new Error('GOAL_REVISION_STALE');
237
299
  this.options.attachments.retainSessionFiles(currentSession.id, submissionId);
238
- const delivery = this.options.coordinator.settle({
239
- submissionId,
240
- intent,
241
- queueBefore: queueBeforeCreate,
242
- ...(replaceCurrentRun === undefined ? {} : { activeRunId: replaceCurrentRun.id })
243
- });
300
+ const delivery = internalRestore
301
+ ? 'QUEUED'
302
+ : this.options.coordinator.settle({
303
+ submissionId,
304
+ intent,
305
+ queueBefore: queueBeforeCreate,
306
+ ...(steerTarget === undefined ? {} : { steerTarget }),
307
+ ...(replaceCurrentRun === undefined ? {} : { activeRunId: replaceCurrentRun.id })
308
+ });
244
309
  reservation?.commit((directories) => this.options.attachments.adoptSessionFiles(currentSession.id, submissionId, directories, currentSession.workspaceId, attachmentPaths));
245
310
  this.options.attachments.acceptSessionInput(currentSession.id);
246
311
  return delivery;
@@ -263,7 +328,7 @@ export class SessionMessageService {
263
328
  ...(created.queueItem === undefined ? {} : { queueItemId: created.queueItem.id }),
264
329
  delivery,
265
330
  idempotent: false,
266
- auditReceipt: auditReceipt(sessionForPresentation, intent === 'QUEUE' ? 'QUEUE' : intent === 'REPLACE_CURRENT' ? 'REPLACE_CURRENT' : 'SEND', created.run.id, created.queueItem?.id)
331
+ auditReceipt: auditReceipt(sessionForPresentation, intent === 'QUEUE' ? 'QUEUE' : intent === 'REPLACE_CURRENT' ? 'REPLACE_CURRENT' : 'SEND', created.run?.id ?? submissionId, created.queueItem?.id)
267
332
  };
268
333
  })();
269
334
  this.pendingMessages.set(pendingKey, operation);
@@ -12,6 +12,7 @@ export class SessionPresentationService {
12
12
  session = {
13
13
  ...session,
14
14
  sessionPrompt: metadata.sessionPrompt ?? null,
15
+ parentSessionId: metadata.parentSessionId ?? session.parentSessionId ?? null,
15
16
  pinnedAt: metadata.pinnedAt,
16
17
  pinOrder: metadata.pinOrder
17
18
  };
@@ -24,6 +25,7 @@ export class SessionPresentationService {
24
25
  });
25
26
  return {
26
27
  ...session,
28
+ parentSessionId: session.parentSessionId ?? null,
27
29
  ...presentation,
28
30
  runtime: {
29
31
  ...presentation.runtime,
@@ -11,7 +11,7 @@ export class SessionQueryService {
11
11
  'session.cli.context': (data) => this.cliContext(record(data)),
12
12
  'session.goal.get': async (data) => {
13
13
  const input = record(data);
14
- const session = await this.requireSession(input.sessionId, input.workspaceId);
14
+ const session = await this.requireSession(input.sessionId, input.workspaceId, input.registeredOnly === true);
15
15
  return { goal: this.options.goal?.(session.id) ?? null };
16
16
  },
17
17
  'session.runs': (data) => this.runs(record(data)),
@@ -24,7 +24,7 @@ export class SessionQueryService {
24
24
  }
25
25
  async get(input) {
26
26
  return {
27
- session: this.options.present(await this.requireSession(input.sessionId, input.workspaceId))
27
+ session: this.options.present(await this.requireSession(input.sessionId, input.workspaceId, input.registeredOnly === true))
28
28
  };
29
29
  }
30
30
  async cliContext(input) {
@@ -35,7 +35,7 @@ export class SessionQueryService {
35
35
  throw new Error('WORKSPACE_NOT_FOUND');
36
36
  const session = input.sessionId === undefined
37
37
  ? undefined
38
- : await this.requireSession(input.sessionId, workspace.id);
38
+ : await this.requireSession(input.sessionId, workspace.id, input.registeredOnly === true);
39
39
  return {
40
40
  workspace: { id: workspace.id, path: workspace.path },
41
41
  session: session === undefined ? null : this.options.present(session),
@@ -43,12 +43,12 @@ export class SessionQueryService {
43
43
  };
44
44
  }
45
45
  async runs(input) {
46
- const session = await this.requireSession(input.sessionId, input.workspaceId);
46
+ const session = await this.requireSession(input.sessionId, input.workspaceId, input.registeredOnly === true);
47
47
  return { runs: this.options.runtime.listRunsForSession(session.id, { includeTerminal: true }) };
48
48
  }
49
49
  async turns(input) {
50
50
  const page = pageInput(input);
51
- const session = await this.requireSession(input.sessionId, input.workspaceId);
51
+ const session = await this.requireSession(input.sessionId, input.workspaceId, input.registeredOnly === true);
52
52
  if (input.unit === 'SEGMENT')
53
53
  return this.options.segments(session, page);
54
54
  if (input.unit !== undefined && input.unit !== 'TURN')
@@ -66,7 +66,7 @@ export class SessionQueryService {
66
66
  return this.options.runtime.listConversationItems({ sessionId: session.id, ...page });
67
67
  }
68
68
  async readToolResource(input) {
69
- const session = await this.requireSession(input.sessionId, input.workspaceId);
69
+ const session = await this.requireSession(input.sessionId, input.workspaceId, input.registeredOnly === true);
70
70
  if (typeof input.workspaceId !== 'string' || input.workspaceId !== session.workspaceId)
71
71
  throw new Error('SESSION_NOT_FOUND');
72
72
  const resource = toolResultResourceInputSchema.parse({
@@ -80,7 +80,7 @@ export class SessionQueryService {
80
80
  return toolResultResourcePageSchema.parse(await runner.readToolResultResource(session, resource));
81
81
  }
82
82
  async readContext(input) {
83
- return this.options.context.read(await this.requireSession(input.sessionId, input.workspaceId));
83
+ return this.options.context.read(await this.requireSession(input.sessionId, input.workspaceId, input.registeredOnly === true));
84
84
  }
85
85
  async discover(input) {
86
86
  const runner = this.options.runners.product(input.runner);
@@ -96,10 +96,15 @@ export class SessionQueryService {
96
96
  const sessions = await this.options.discover(runner.name, workspaceId);
97
97
  return { sessions: sessions.map((session) => this.options.present(session)) };
98
98
  }
99
- async requireSession(sessionId, workspaceId) {
99
+ async requireSession(sessionId, workspaceId, registeredOnly = false) {
100
100
  if (typeof sessionId !== 'string')
101
101
  throw new Error('SESSION_INVALID');
102
- const session = this.options.runtime.getAgentSession(sessionId) ?? (await this.options.resolve(sessionId));
102
+ const session = this.options.runtime.getAgentSession(sessionId) ??
103
+ (await this.options.resolve(sessionId, registeredOnly));
104
+ if (session === undefined && registeredOnly && typeof workspaceId === 'string')
105
+ throw Object.assign(new Error('SESSION_NOT_FOUND'), {
106
+ publicDetails: this.options.similar(sessionId, workspaceId)
107
+ });
103
108
  if (session === undefined || (workspaceId !== undefined && session.workspaceId !== workspaceId))
104
109
  throw new Error('SESSION_NOT_FOUND');
105
110
  return session;
@@ -1,3 +1,4 @@
1
+ import { steerComposerMessage } from '@myagentroam/protocol';
1
2
  import { isPlainRecord, withNonImageAttachmentPrompt } from '../util/node-operation-parsers.js';
2
3
  import { assertQueueItemOwner, assertSessionControlOwner } from './run-ownership-policy.js';
3
4
  export class WorkspaceQueueWorkbenchService {
@@ -63,10 +64,15 @@ export class WorkspaceQueueWorkbenchService {
63
64
  })
64
65
  };
65
66
  }
66
- get(input) {
67
- const session = this.requireSession(input);
67
+ async get(input) {
68
+ if (typeof input.workspaceId !== 'string' || typeof input.sessionId !== 'string')
69
+ throw new Error('WORKSPACE_NOT_FOUND');
70
+ const session = this.options.runtime.getAgentSession(input.sessionId) ??
71
+ (await this.options.resolve(input.sessionId, input.registeredOnly === true));
72
+ if (session?.workspaceId !== input.workspaceId)
73
+ throw new Error('SESSION_NOT_FOUND');
68
74
  return {
69
- queue: this.present(session.workspaceId, session.id)
75
+ queue: this.snapshot(session.id)
70
76
  };
71
77
  }
72
78
  pauseOrResume(mode, input) {
@@ -113,12 +119,13 @@ export class WorkspaceQueueWorkbenchService {
113
119
  const item = this.requireItem(input.queueItemId, input.workspaceId);
114
120
  assertQueueItemOwner(item, input.__requestUserId);
115
121
  const content = input.content.trim();
116
- this.options.runtime.updateQueueItem(item.id, content);
117
122
  const prepared = this.options.coordinator.pending(item.id);
118
- if (prepared !== undefined) {
119
- const runnerInput = withNonImageAttachmentPrompt(content, prepared.attachmentPaths, prepared.attachmentFiles);
120
- this.options.coordinator.updatePreparedInput(item.id, this.options.prepareInput(prepared.runner, runnerInput, prepared.collaborationMode));
121
- }
123
+ const runnerInput = prepared === undefined
124
+ ? undefined
125
+ : this.options.prepareInput(prepared.runner, withNonImageAttachmentPrompt(prepared.steering ? steerComposerMessage(content) : content, prepared.attachmentPaths, prepared.attachmentFiles), prepared.collaborationMode);
126
+ this.options.runtime.updateQueueItem(item.id, content);
127
+ if (runnerInput !== undefined)
128
+ this.options.coordinator.updatePreparedInput(item.id, runnerInput);
122
129
  this.options.emit(item.workspaceId, item.sessionId);
123
130
  return {
124
131
  queue: this.present(item.workspaceId, item.sessionId),