@myagentroam/node 0.10.35 → 0.10.36

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.
@@ -0,0 +1,141 @@
1
+ import { serializeComposerTag } from '@myagentroam/protocol';
2
+ import { parseMessageAttachments } from '../util/node-operation-parsers.js';
3
+ import { RESTART_NOTICE } from './planned-restart-handoff.js';
4
+ /** Rebuild inputs through the existing preparation and FIFO boundary. */
5
+ export class RestartSessionRestorer {
6
+ options;
7
+ constructor(options) {
8
+ this.options = options;
9
+ }
10
+ async restore(entry, handoffId) {
11
+ const contexts = new Map();
12
+ const authorize = async (ownerUserId) => {
13
+ if (!contexts.has(ownerUserId))
14
+ contexts.set(ownerUserId, await this.options.authorize({
15
+ handoffId,
16
+ sessionId: entry.session.id,
17
+ workspaceId: entry.session.workspaceId,
18
+ ownerUserId,
19
+ runner: 'mar-agent',
20
+ model: entry.session.model
21
+ }));
22
+ return contexts.get(ownerUserId);
23
+ };
24
+ await authorize(entry.ownerUserId);
25
+ for (const input of [...entry.queue, ...(entry.current ? [entry.current] : [])]) {
26
+ parseMessageAttachments(input.attachments);
27
+ await authorize(input.initiatedByUserId);
28
+ }
29
+ let session = await this.options.resolve(entry.session.id);
30
+ if (session.runner !== 'mar-agent' ||
31
+ session.workspaceId !== entry.session.workspaceId ||
32
+ session.cwd !== entry.session.cwd ||
33
+ session.externalSessionId !== entry.session.id)
34
+ throw new Error('RESTART_SESSION_MISMATCH');
35
+ session = this.options.runtime.updateAgentSessionConfiguration({
36
+ sessionId: session.id,
37
+ model: entry.session.model ?? session.model,
38
+ effort: entry.session.effort ?? session.effort,
39
+ access: entry.session.access ?? session.access
40
+ });
41
+ const { runtime, coordinator, goals, commands } = this.options;
42
+ runtime.pauseSessionQueue(session.id);
43
+ if (entry.plan)
44
+ commands.set(session.id, {
45
+ commandId: 'plan',
46
+ active: true,
47
+ label: 'Plan',
48
+ detail: null,
49
+ closable: true,
50
+ closeInput: '/plan'
51
+ });
52
+ if (entry.goal) {
53
+ if (entry.goal.goal.sessionId !== session.id)
54
+ throw new Error('RESTART_GOAL_SESSION_MISMATCH');
55
+ goals.restoreRestartState(entry.goal);
56
+ this.options.publishGoal(session, entry.goal.goal);
57
+ }
58
+ else if (entry.awaitingGoal)
59
+ commands.set(session.id, {
60
+ commandId: 'goal',
61
+ active: true,
62
+ label: 'Goal',
63
+ detail: null,
64
+ closable: true,
65
+ closeInput: '/goal --clear'
66
+ });
67
+ const submit = async (input, content = input.content) => {
68
+ const context = contexts.get(input.initiatedByUserId);
69
+ await this.options.withGrant(context, () => this.options.messages.restoreQueued({
70
+ sessionId: session.id,
71
+ content,
72
+ clientMessageId: input.clientMessageId,
73
+ ...(content.startsWith('<mar_restart>')
74
+ ? { __runnerContent: `${content}${RESTART_NOTICE}` }
75
+ : {}),
76
+ attachments: input.attachments,
77
+ __queueItemId: input.id,
78
+ __requestUserId: input.initiatedByUserId,
79
+ secretEnvironment: context.secretEnvironment,
80
+ personalInstructions: context.personalInstructions,
81
+ marAgentModel: context.marAgentModel,
82
+ marAgentModels: context.marAgentModels,
83
+ marAgentCompact: context.marAgentCompact,
84
+ marAgentRuntimeScopeId: context.marAgentRuntimeScopeId,
85
+ __usageGrant: context.usageGrant
86
+ }));
87
+ if (input.immediate)
88
+ coordinator.restoreImmediate(input.id);
89
+ };
90
+ try {
91
+ if (entry.current &&
92
+ entry.goal?.goal.status !== 'active' &&
93
+ !entry.queue.some((item) => item.immediate)) {
94
+ const state = await this.options.inputState(entry, entry.current);
95
+ if (state === 'paused')
96
+ entry.paused = true;
97
+ if (state === 'missing')
98
+ await submit(entry.current);
99
+ else if (state !== 'completed')
100
+ await submit({
101
+ ...entry.current,
102
+ id: `restart:${handoffId}:${session.id}`,
103
+ clientMessageId: `restart:${handoffId}:${session.id}`,
104
+ attachments: []
105
+ }, serializeComposerTag('mar_restart', {}));
106
+ }
107
+ for (const input of entry.queue)
108
+ await submit(input);
109
+ if (entry.goal?.goal.status === 'active') {
110
+ const context = contexts.get(entry.ownerUserId);
111
+ await this.options.withGrant(context, () => this.options.messages.submitGoal({
112
+ sessionId: session.id,
113
+ kind: 'continue',
114
+ deliveryIntent: 'QUEUE',
115
+ content: `${goals.prompt(session.id, 'continue')}
116
+ ${RESTART_NOTICE}`,
117
+ goal: { id: entry.goal.goal.id, revision: entry.goal.goal.revision, epoch: 0 },
118
+ initiatedByUserId: entry.ownerUserId,
119
+ secretEnvironment: context.secretEnvironment,
120
+ restoreQueued: true,
121
+ ...(context.usageGrant === undefined ? {} : { usageGrant: context.usageGrant }),
122
+ ...(typeof context.personalInstructions === 'string'
123
+ ? { personalInstructions: context.personalInstructions }
124
+ : {}),
125
+ marAgentModel: context.marAgentModel,
126
+ marAgentModels: context.marAgentModels,
127
+ marAgentCompact: context.marAgentCompact,
128
+ marAgentRuntimeScopeId: context.marAgentRuntimeScopeId
129
+ }));
130
+ }
131
+ if (!entry.paused)
132
+ coordinator.resume(session.id);
133
+ this.options.publishQueue(session);
134
+ }
135
+ catch (error) {
136
+ runtime.pauseSessionQueue(session.id);
137
+ this.options.publishQueue(session);
138
+ throw error;
139
+ }
140
+ }
141
+ }
@@ -1,12 +1,17 @@
1
- import { createEnvelope, scheduledRunAuthorizeRequestSchema, scheduledRunAuthorizeResponseSchema } from '@myagentroam/protocol';
2
- export class ScheduledRunAuthorizationControl {
1
+ import { createEnvelope, scheduledRunAuthorizeRequestSchema, scheduledRunAuthorizeResponseSchema, restartRunAuthorizeRequestSchema } from '@myagentroam/protocol';
2
+ export class RunAuthorizationControl {
3
3
  send;
4
+ purpose;
4
5
  #pending = new Map();
5
6
  #generation = 0;
6
7
  #connected = false;
7
8
  #closed = false;
8
- constructor(send) {
9
+ constructor(send, purpose = 'schedule') {
9
10
  this.send = send;
11
+ this.purpose = purpose;
12
+ }
13
+ code(suffix) {
14
+ return `${this.purpose.toUpperCase()}_${suffix}`;
10
15
  }
11
16
  connected() {
12
17
  this.disconnected();
@@ -16,29 +21,31 @@ export class ScheduledRunAuthorizationControl {
16
21
  this.#connected = false;
17
22
  this.#generation++;
18
23
  for (const pending of [...this.#pending.values()])
19
- pending.reject(new Error('SCHEDULE_SERVER_UNAVAILABLE'));
24
+ pending.reject(new Error(this.code('SERVER_UNAVAILABLE')));
20
25
  }
21
26
  close() {
22
27
  this.#closed = true;
23
28
  this.disconnected();
24
29
  }
25
30
  handle(envelope) {
26
- if (envelope.type !== 'schedule.authorize.response')
31
+ if (envelope.type !== `${this.purpose}.authorize.response`)
27
32
  return false;
28
33
  const pending = envelope.replyTo === undefined ? undefined : this.#pending.get(envelope.replyTo);
29
34
  if (pending === undefined || pending.generation !== this.#generation)
30
35
  return true;
31
36
  const parsed = scheduledRunAuthorizeResponseSchema.safeParse(envelope.payload);
32
37
  if (!parsed.success)
33
- pending.reject(new Error('SCHEDULE_AUTH_RESPONSE_INVALID'));
38
+ pending.reject(new Error(this.code('AUTH_RESPONSE_INVALID')));
34
39
  else
35
40
  pending.resolve(parsed.data);
36
41
  return true;
37
42
  }
38
43
  authorize(request) {
39
44
  if (!this.#connected || this.#closed)
40
- return Promise.reject(new Error('SCHEDULE_SERVER_UNAVAILABLE'));
41
- const envelope = createEnvelope('schedule.authorize', scheduledRunAuthorizeRequestSchema.parse(request));
45
+ return Promise.reject(new Error(this.code('SERVER_UNAVAILABLE')));
46
+ const envelope = createEnvelope(`${this.purpose}.authorize`, (this.purpose === 'schedule'
47
+ ? scheduledRunAuthorizeRequestSchema
48
+ : restartRunAuthorizeRequestSchema).parse(request));
42
49
  return new Promise((resolve, reject) => {
43
50
  const settle = (action) => {
44
51
  const pending = this.#pending.get(envelope.id);
@@ -48,7 +55,7 @@ export class ScheduledRunAuthorizationControl {
48
55
  clearTimeout(pending.timer);
49
56
  action();
50
57
  };
51
- const timer = setTimeout(() => settle(() => reject(new Error('SCHEDULE_AUTH_TIMEOUT'))), 15_000);
58
+ const timer = setTimeout(() => settle(() => reject(new Error(this.code('AUTH_TIMEOUT')))), 15_000);
52
59
  timer.unref();
53
60
  this.#pending.set(envelope.id, {
54
61
  generation: this.#generation,
@@ -65,7 +72,7 @@ export class ScheduledRunAuthorizationControl {
65
72
  this.send(envelope);
66
73
  }
67
74
  catch {
68
- settle(() => reject(new Error('SCHEDULE_SERVER_UNAVAILABLE')));
75
+ settle(() => reject(new Error(this.code('SERVER_UNAVAILABLE'))));
69
76
  }
70
77
  });
71
78
  }
@@ -232,6 +232,8 @@ export class RunEventService {
232
232
  const run = this.options.runtime.getRun(runId);
233
233
  if (run === undefined)
234
234
  return undefined;
235
+ if (run.suppressNotifications === true)
236
+ return undefined;
235
237
  if (run.source === 'SCHEDULED' && eventType === 'approval.request')
236
238
  return undefined;
237
239
  const session = this.options.runtime.getAgentSession(run.sessionId);
@@ -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
@@ -10,12 +10,24 @@ 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,35 @@ 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 acceptedMessage = {
138
165
  runId,
139
166
  sessionId: session.id,
140
167
  clientMessageId,
141
168
  content: publicContent,
142
169
  ...(internalSchedule ? { source: 'SCHEDULED' } : {}),
170
+ ...(input.__suppressNotifications === true ? { suppressNotifications: true } : {}),
143
171
  ...(Number.isSafeInteger(input.__requestUserId)
144
172
  ? { initiatedByUserId: input.__requestUserId }
145
173
  : {}),
146
174
  ...(inlineAttachments.length === 0 ? {} : { attachments: inlineAttachments })
147
- }, secretEnvironment);
175
+ };
176
+ const created = internalRestore
177
+ ? {
178
+ run: undefined,
179
+ queueItem: this.options.runtime.createQueueItem({
180
+ ...acceptedMessage,
181
+ queueItemId: runId
182
+ }).item
183
+ }
184
+ : this.options.coordinator.accept(acceptedMessage, secretEnvironment);
148
185
  const submissionId = created.queueItem?.id ?? created.run.id;
149
186
  const sessionForPresentation = input.__goal === true
150
187
  ? session
@@ -193,6 +230,13 @@ export class SessionMessageService {
193
230
  else
194
231
  this.options.runtime.updateQueueItemAttachments(created.queueItem.id, attachments, displayInput);
195
232
  this.options.coordinator.prepare(submissionId, {
233
+ restartMessage: {
234
+ content,
235
+ attachments,
236
+ ...(Number.isSafeInteger(input.__requestUserId)
237
+ ? { initiatedByUserId: input.__requestUserId }
238
+ : {})
239
+ },
196
240
  ...(input.__usageGrant === undefined
197
241
  ? {}
198
242
  : { usageGrant: modelUsageGrantSchema.parse(input.__usageGrant) }),
@@ -235,12 +279,14 @@ export class SessionMessageService {
235
279
  !this.options.prepareGoalInsertion(session.id, submissionId, goalMessage.goal))
236
280
  throw new Error('GOAL_REVISION_STALE');
237
281
  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
- });
282
+ const delivery = internalRestore
283
+ ? 'QUEUED'
284
+ : this.options.coordinator.settle({
285
+ submissionId,
286
+ intent,
287
+ queueBefore: queueBeforeCreate,
288
+ ...(replaceCurrentRun === undefined ? {} : { activeRunId: replaceCurrentRun.id })
289
+ });
244
290
  reservation?.commit((directories) => this.options.attachments.adoptSessionFiles(currentSession.id, submissionId, directories, currentSession.workspaceId, attachmentPaths));
245
291
  this.options.attachments.acceptSessionInput(currentSession.id);
246
292
  return delivery;
@@ -263,7 +309,7 @@ export class SessionMessageService {
263
309
  ...(created.queueItem === undefined ? {} : { queueItemId: created.queueItem.id }),
264
310
  delivery,
265
311
  idempotent: false,
266
- auditReceipt: auditReceipt(sessionForPresentation, intent === 'QUEUE' ? 'QUEUE' : intent === 'REPLACE_CURRENT' ? 'REPLACE_CURRENT' : 'SEND', created.run.id, created.queueItem?.id)
312
+ auditReceipt: auditReceipt(sessionForPresentation, intent === 'QUEUE' ? 'QUEUE' : intent === 'REPLACE_CURRENT' ? 'REPLACE_CURRENT' : 'SEND', created.run?.id ?? submissionId, created.queueItem?.id)
267
313
  };
268
314
  })();
269
315
  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,