@myagentroam/node 0.11.45 → 0.11.47

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 (37) hide show
  1. package/dist/connector.js +4 -4
  2. package/dist/database.js +7 -5
  3. package/dist/marctl-guidance.js +2 -2
  4. package/dist/migrations/v023.js +25 -0
  5. package/dist/runner/abstract-runner.js +1 -1
  6. package/dist/runner/claude/managed-run-controller.js +1 -1
  7. package/dist/runner/claude-code-runner.js +1 -1
  8. package/dist/runner/codex/managed-run-controller.js +1 -1
  9. package/dist/runner/codex-runner.js +10 -5
  10. package/dist/runner/mar-agent/agent-network-extension.js +54 -14
  11. package/dist/runner/mar-agent/conversation-normalizer.js +43 -3
  12. package/dist/runner/mar-agent/history-projection.js +1 -1
  13. package/dist/runner/mar-agent/hosted-conversation-runtime.js +1 -1
  14. package/dist/runner/mar-agent/managed-run-controller.js +2 -2
  15. package/dist/runner/mar-agent-runner.js +5 -5
  16. package/dist/runner/opencode/managed-run-controller.js +2 -2
  17. package/dist/runner/opencode-runner.js +5 -5
  18. package/dist/runner-profiles.js +1 -1
  19. package/dist/runtime-state.js +7 -7
  20. package/dist/service/agent-network-bindings.js +8 -7
  21. package/dist/service/agent-network-invoke.js +9 -9
  22. package/dist/service/agent-network-service.js +12 -7
  23. package/dist/service/continuous-run-coordinator.js +1 -1
  24. package/dist/service/external-session-resume-service.js +1 -1
  25. package/dist/service/goal-continuation-coordinator.js +3 -3
  26. package/dist/service/native-session-watch-service.js +6 -3
  27. package/dist/service/planned-restart-handoff.js +24 -5
  28. package/dist/service/restart-session-restorer.js +2 -2
  29. package/dist/service/session-recent-activity.js +1 -1
  30. package/dist/session/native-session-projection-service.js +39 -30
  31. package/dist/session/session-catalog-service.js +116 -90
  32. package/dist/session/session-command-service.js +1 -0
  33. package/dist/session/session-context-service.js +1 -1
  34. package/dist/session/session-lifecycle-service.js +12 -10
  35. package/dist/session/session-message-service.js +4 -3
  36. package/dist/workspace/workspace-session-service.js +1 -1
  37. package/package.json +4 -4
@@ -103,7 +103,7 @@ export class NodeRuntimeState {
103
103
  nativeControl: input.externalSessionId === undefined ? 'MAR_MANAGED' : (input.nativeControl ?? 'EXTERNAL'),
104
104
  channelToken: null,
105
105
  cwd: input.cwd,
106
- model: input.model ?? null,
106
+ modelId: input.modelId ?? null,
107
107
  effort: input.effort ?? null,
108
108
  access: input.access ?? null,
109
109
  customTitle,
@@ -139,7 +139,7 @@ export class NodeRuntimeState {
139
139
  nativeControl: input.externalSessionId === undefined ? 'MAR_MANAGED' : 'EXTERNAL',
140
140
  channelToken: null,
141
141
  cwd: input.cwd,
142
- model: null,
142
+ modelId: null,
143
143
  effort: null,
144
144
  access: null,
145
145
  customTitle: null,
@@ -214,7 +214,7 @@ export class NodeRuntimeState {
214
214
  }
215
215
  updateAgentSessionConfiguration(input) {
216
216
  return this.replaceSession(input.sessionId, {
217
- ...(input.model === undefined ? {} : { model: input.model }),
217
+ ...(input.modelId === undefined ? {} : { modelId: input.modelId }),
218
218
  ...(input.effort === undefined ? {} : { effort: input.effort }),
219
219
  ...(input.access === undefined ? {} : { access: input.access })
220
220
  });
@@ -456,7 +456,7 @@ export class NodeRuntimeState {
456
456
  runId: run.id,
457
457
  userItemId: user.id,
458
458
  status: 'PREPARING',
459
- model: session.model,
459
+ model: session.modelId,
460
460
  effort: session.effort,
461
461
  access: session.access,
462
462
  items: [user],
@@ -1059,7 +1059,7 @@ export class NodeRuntimeState {
1059
1059
  runId: null,
1060
1060
  userItemId: null,
1061
1061
  status: input.failed ? 'FAILED' : 'SUCCEEDED',
1062
- model: session.model,
1062
+ model: session.modelId,
1063
1063
  effort: session.effort,
1064
1064
  access: session.access,
1065
1065
  items: [],
@@ -1374,7 +1374,7 @@ export class NodeRuntimeState {
1374
1374
  runId: run.id,
1375
1375
  userItemId: null,
1376
1376
  status: terminal(run.status) ? run.status : 'RUNNING',
1377
- model: session.model,
1377
+ model: session.modelId,
1378
1378
  effort: session.effort,
1379
1379
  access: session.access,
1380
1380
  items: [],
@@ -1416,7 +1416,7 @@ export class NodeRuntimeState {
1416
1416
  runId: run.id,
1417
1417
  userItemId: userId,
1418
1418
  status: 'PREPARING',
1419
- model: session.model,
1419
+ model: session.modelId,
1420
1420
  effort: session.effort,
1421
1421
  access: session.access,
1422
1422
  items: [user],
@@ -167,9 +167,9 @@ export function createNodeAgentNetwork(options) {
167
167
  const workspace = workspaceId === undefined ? undefined : database?.getWorkspace(workspaceId);
168
168
  if (!runnerAdapter || !database || !workspace)
169
169
  throw new Error('SESSION_INVALID');
170
- const model = input.model ??
171
- database.getRunnerDefaults().find((entry) => entry.runner === runner)?.model ??
172
- runnerAdapter.defaultConfiguration(workspace, actor.secretEnvironment ?? {}, runnerAdapter.prepareExecutionContext(actor.runnerConfiguration ?? {})).model;
170
+ const model = input.modelId ??
171
+ database.getRunnerDefaults().find((entry) => entry.runner === runner)?.modelId ??
172
+ runnerAdapter.defaultConfiguration(workspace, actor.secretEnvironment ?? {}, runnerAdapter.prepareExecutionContext(actor.runnerConfiguration ?? {})).modelId;
173
173
  const request = {
174
174
  ...executionInput(actor, runner, model, parent !== undefined),
175
175
  workspaceId: workspace.id,
@@ -182,7 +182,7 @@ export function createNodeAgentNetwork(options) {
182
182
  __suppressNotifications: true,
183
183
  ...(!terminal && parent ? { __marctlOrigin: origin(parent) } : {}),
184
184
  ...(input.title === undefined ? {} : { title: input.title }),
185
- ...(model ? { model } : {}),
185
+ ...(model ? { modelId: model } : {}),
186
186
  ...(input.effort === undefined ? {} : { effort: input.effort }),
187
187
  ...(input.attachments === undefined ? {} : { attachments: input.attachments }),
188
188
  ...(input.attachmentIds === undefined ? {} : { attachmentIds: input.attachmentIds })
@@ -229,7 +229,7 @@ export function createNodeAgentNetwork(options) {
229
229
  const current = await options.resume.resolveForMessage(session.id);
230
230
  const executionContext = options.runners
231
231
  .require(current.runner)
232
- .prepareExecutionContext(executionInput(actor, current.runner, current.model, current.parentSessionId != null));
232
+ .prepareExecutionContext(executionInput(actor, current.runner, current.modelId, current.parentSessionId != null));
233
233
  signal?.throwIfAborted();
234
234
  const result = await options.withGrant(actor, () => options.coordinator.start({
235
235
  session: current,
@@ -262,7 +262,7 @@ export function createNodeAgentNetwork(options) {
262
262
  const parent = terminalCalls.has(actor) ? undefined : await actorSession(actor);
263
263
  signal?.throwIfAborted();
264
264
  const result = await options.withGrant(actor, () => options.messages.operations()['session.message']({
265
- ...executionInput(actor, session.runner, session.model, session.parentSessionId != null),
265
+ ...executionInput(actor, session.runner, session.modelId, session.parentSessionId != null),
266
266
  ...input,
267
267
  ...(signal ? { [SESSION_ADMISSION_SIGNAL]: signal } : {}),
268
268
  sessionId: session.id,
@@ -383,10 +383,11 @@ export function createNodeAgentNetwork(options) {
383
383
  truncated: page.nextCursor !== null
384
384
  };
385
385
  },
386
- view: async (actor, input) => {
386
+ view: async (actor, input, onResolved) => {
387
387
  const source = terminalCalls.has(actor) ? undefined : await actorSession(actor);
388
388
  const session = input.agentId === source?.id ? source : await target(actor, input.agentId);
389
389
  assertSessionControlOwner(options.runtime, session.id, actor.requestUserId);
390
+ await onResolved?.([session]);
390
391
  return {
391
392
  session: marctlSessionDetail(options.runtime, session, options.runtime.sessionQueue(session.id).itemIds.length),
392
393
  goal: publicGoal(options.goals.get(session.id)) ?? null,
@@ -4,9 +4,9 @@ import { randomUUID } from 'node:crypto';
4
4
  import { modelUsageGrantSchema } from '@myagentroam/protocol';
5
5
  /** Bind each native invocation to the actual live Node Execution and its captured resources. */
6
6
  export function agentNetworkInvoke(input) {
7
- return async (executionId, action, payload, context) => {
7
+ return async (executionId, action, payload, context, onResolved) => {
8
8
  const actor = executionAgentActor({ ...input, executionId });
9
- return invokeAgentNetwork(input.service, actor, action, payload, context);
9
+ return invokeAgentNetwork(input.service, actor, action, payload, context, undefined, onResolved);
10
10
  };
11
11
  }
12
12
  /** Read actor resources from the current Node-owned Execution, not caller arguments. */
@@ -51,7 +51,7 @@ export function executionAgentActor(input) {
51
51
  };
52
52
  }
53
53
  /** Shared command dispatch; callers must bind actor authority at their trusted boundary. */
54
- export async function invokeAgentNetwork(service, actor, action, payload, context, requestSignal) {
54
+ export async function invokeAgentNetwork(service, actor, action, payload, context, requestSignal, onResolved) {
55
55
  const signal = context?.signal ?? requestSignal;
56
56
  switch (action) {
57
57
  case 'start':
@@ -63,16 +63,16 @@ export async function invokeAgentNetwork(service, actor, action, payload, contex
63
63
  typeof payload.title === 'string' &&
64
64
  !payload.title.trim()) {
65
65
  const { title: _title, ...withoutTitle } = payload;
66
- return service.start(actor, withoutTitle, signal);
66
+ return service.start(actor, withoutTitle, signal, onResolved);
67
67
  }
68
68
  return service.start(actor, {
69
69
  ...payload,
70
70
  ...(typeof payload.title === 'string' ? { title: payload.title.trim() } : {})
71
- }, signal);
71
+ }, signal, onResolved);
72
72
  case 'send':
73
- return service.send(actor, payload, signal);
73
+ return service.send(actor, payload, signal, onResolved);
74
74
  case 'stop':
75
- return service.stop(actor, payload);
75
+ return service.stop(actor, payload, onResolved);
76
76
  case 'wait':
77
77
  return service.wait(actor, {
78
78
  ...payload,
@@ -81,11 +81,11 @@ export async function invokeAgentNetwork(service, actor, action, payload, contex
81
81
  : requestSignal
82
82
  ? { signal: requestSignal }
83
83
  : {})
84
- });
84
+ }, onResolved);
85
85
  case 'list':
86
86
  return service.list(actor, payload);
87
87
  case 'view':
88
- return service.view(actor, payload);
88
+ return service.view(actor, payload, onResolved);
89
89
  }
90
90
  }
91
91
  /** Node broker injects the caller only after validating the Server-issued Execution credential. */
@@ -12,10 +12,10 @@ export class AgentNetworkService {
12
12
  list(actor, input = {}) {
13
13
  return this.options.list(actor, input);
14
14
  }
15
- view(actor, input) {
16
- return this.options.view(actor, input);
15
+ view(actor, input, onResolved) {
16
+ return this.options.view(actor, input, onResolved);
17
17
  }
18
- async start(actor, input, signal) {
18
+ async start(actor, input, signal, onResolved) {
19
19
  signal?.throwIfAborted();
20
20
  const goal = parseAgentGoal(input.goal);
21
21
  const content = input.content ?? '';
@@ -27,7 +27,8 @@ export class AgentNetworkService {
27
27
  input.attachmentIds.some((id) => typeof id !== 'string' || !id))) ||
28
28
  (!!input.attachments?.length && !!input.attachmentIds?.length) ||
29
29
  !validOptionalString(input.runner) ||
30
- !validOptionalString(input.model) ||
30
+ 'model' in input ||
31
+ !validOptionalString(input.modelId) ||
31
32
  !validOptionalString(input.effort) ||
32
33
  !validOptionalString(input.title) ||
33
34
  !validOptionalString(input.parentSessionId) ||
@@ -40,6 +41,7 @@ export class AgentNetworkService {
40
41
  clientMessageId: input.clientMessageId ?? randomUUID(),
41
42
  goal
42
43
  }, signal);
44
+ await onResolved?.([created.session]);
43
45
  try {
44
46
  if (created.session.parentSessionId === actor.sessionId)
45
47
  this.options.observe(actor, created.session);
@@ -53,7 +55,7 @@ export class AgentNetworkService {
53
55
  };
54
56
  }
55
57
  }
56
- async send(actor, input, signal) {
58
+ async send(actor, input, signal, onResolved) {
57
59
  signal?.throwIfAborted();
58
60
  const goal = parseAgentGoal(input.goal);
59
61
  const content = input.content ?? '';
@@ -91,6 +93,7 @@ export class AgentNetworkService {
91
93
  if (input.approval !== undefined)
92
94
  await this.options.validateApproval(actor, session, input.approval);
93
95
  signal?.throwIfAborted();
96
+ await onResolved?.([session]);
94
97
  if (session.parentSessionId === actor.sessionId)
95
98
  this.options.observe(actor, session);
96
99
  const approvalResult = input.approval === undefined
@@ -190,7 +193,7 @@ export class AgentNetworkService {
190
193
  };
191
194
  }
192
195
  }
193
- async stop(actor, input) {
196
+ async stop(actor, input, onResolved) {
194
197
  if (typeof input.agentId !== 'string' ||
195
198
  !input.agentId ||
196
199
  typeof input.executionId !== 'string' ||
@@ -198,6 +201,7 @@ export class AgentNetworkService {
198
201
  throw new Error('AGENT_STOP_INVALID');
199
202
  const session = await this.options.target(actor, input.agentId);
200
203
  await this.options.authorize(actor, session, 'stop');
204
+ await onResolved?.([session]);
201
205
  const goalBefore = this.options.goal(session.id);
202
206
  try {
203
207
  const result = this.options.stop(actor, session, input.executionId);
@@ -223,7 +227,7 @@ export class AgentNetworkService {
223
227
  };
224
228
  }
225
229
  }
226
- async wait(actor, input) {
230
+ async wait(actor, input, onResolved) {
227
231
  validateAgentWaitOptions(input);
228
232
  if ((input.mode !== undefined && input.agentIds === undefined) ||
229
233
  (input.agentId !== undefined && input.agentIds !== undefined) ||
@@ -267,6 +271,7 @@ export class AgentNetworkService {
267
271
  if (!targets.length)
268
272
  throw new Error('AGENT_EXECUTION_NOT_FOUND');
269
273
  const selected = new Set(targets.map((target) => target.agentId));
274
+ await onResolved?.(targets.map((target) => sessions.find((session) => session.id === target.agentId)));
270
275
  const directChildren = new Set();
271
276
  for (const session of sessions)
272
277
  if (selected.has(session.id) && session.parentSessionId === actor.sessionId) {
@@ -717,7 +717,7 @@ export class ContinuousRunCoordinator {
717
717
  ...(pending.externalSessionId === null
718
718
  ? {}
719
719
  : { externalSessionId: pending.externalSessionId }),
720
- ...(pending.model === null ? {} : { model: pending.model }),
720
+ ...(pending.modelId === null ? {} : { model: pending.modelId }),
721
721
  ...(pending.effort === null ? {} : { effort: pending.effort }),
722
722
  ...(pending.access === null ? {} : { access: pending.access }),
723
723
  collaborationMode: pending.collaborationMode,
@@ -106,7 +106,7 @@ export class ExternalSessionResumeService {
106
106
  cwd,
107
107
  createdAt: session.createdAt,
108
108
  lastActivityAt: session.lastActivityAt,
109
- model: configuration.model,
109
+ modelId: configuration.modelId,
110
110
  effort: configuration.effort,
111
111
  access: configuration.access,
112
112
  ...(session.runnerTitle === null ? {} : { title: session.runnerTitle }),
@@ -184,7 +184,7 @@ export class GoalContinuationCoordinator {
184
184
  this.options
185
185
  .runners()
186
186
  .require(session.runner)
187
- .assertChildModel(session.model, executionContext);
187
+ .assertChildModel(session.modelId, executionContext);
188
188
  this.invalidateGoalContinuation(session.id);
189
189
  const prepared = this.prepareGoalSession(session);
190
190
  const goalSession = prepared.session;
@@ -558,13 +558,13 @@ export class GoalContinuationCoordinator {
558
558
  : session;
559
559
  if (changed &&
560
560
  prepared.externalSessionId !== null &&
561
- prepared.model !== null &&
561
+ prepared.modelId !== null &&
562
562
  prepared.effort !== null)
563
563
  this.options.database()?.saveNativeSessionMetadata({
564
564
  runner: prepared.runner,
565
565
  externalSessionId: prepared.externalSessionId,
566
566
  workspaceId: prepared.workspaceId,
567
- model: prepared.model,
567
+ modelId: prepared.modelId,
568
568
  effort: prepared.effort,
569
569
  access
570
570
  });
@@ -5,6 +5,7 @@ import { SessionWatchSlotCoordinator } from './session-watch-slot-coordinator.js
5
5
  const INTERVAL_MS = 2_000;
6
6
  const TTL_MS = 45_000;
7
7
  const MAX_FAILURES = 3;
8
+ const INITIAL_SEGMENT_LIMIT = 3;
8
9
  export class NativeSessionWatchService {
9
10
  options;
10
11
  watches = new Map();
@@ -323,7 +324,9 @@ export class NativeSessionWatchService {
323
324
  return;
324
325
  stage = 'project_snapshot';
325
326
  const page = update.page;
326
- const payload = lease.unit === 'SEGMENT' ? projectInitialPage(session, page, 'SEGMENT', 10) : page;
327
+ const payload = lease.unit === 'SEGMENT'
328
+ ? projectInitialPage(session, page, 'SEGMENT', INITIAL_SEGMENT_LIMIT)
329
+ : page;
327
330
  this.emitSnapshot(lease, session, payload);
328
331
  this.releaseSnapshotPendingEvents(lease, page);
329
332
  const hydration = watch.observation.hydrate();
@@ -405,7 +408,7 @@ export class NativeSessionWatchService {
405
408
  if (!lease.ready)
406
409
  continue;
407
410
  const payload = lease.unit === 'SEGMENT'
408
- ? this.options.projectInitialPage?.(session, page, 'SEGMENT', 10)
411
+ ? this.options.projectInitialPage?.(session, page, 'SEGMENT', INITIAL_SEGMENT_LIMIT)
409
412
  : page;
410
413
  if (payload === undefined)
411
414
  continue;
@@ -431,7 +434,7 @@ export class NativeSessionWatchService {
431
434
  if (!lease.snapshotSent)
432
435
  continue;
433
436
  const payload = lease.unit === 'SEGMENT'
434
- ? this.options.projectInitialPage?.(session, page, 'SEGMENT', 10)
437
+ ? this.options.projectInitialPage?.(session, page, 'SEGMENT', INITIAL_SEGMENT_LIMIT)
435
438
  : page;
436
439
  if (payload === undefined)
437
440
  continue;
@@ -51,7 +51,7 @@ const sessionSchema = z
51
51
  id: identifier,
52
52
  workspaceId: identifier,
53
53
  cwd: z.string().min(1),
54
- model: z.string().nullable(),
54
+ modelId: z.string().nullable(),
55
55
  effort: z.string().nullable(),
56
56
  access: z.string().nullable()
57
57
  })
@@ -68,7 +68,7 @@ const sessionSchema = z
68
68
  .strict();
69
69
  export const plannedRestartSchema = z
70
70
  .object({
71
- schemaVersion: z.literal(1),
71
+ schemaVersion: z.literal(2),
72
72
  id: identifier,
73
73
  nodeId: identifier,
74
74
  serverUrl: z.string().min(1),
@@ -76,6 +76,14 @@ export const plannedRestartSchema = z
76
76
  sessions: z.array(sessionSchema)
77
77
  })
78
78
  .strict();
79
+ const legacyPlannedRestartSchema = plannedRestartSchema.extend({
80
+ schemaVersion: z.literal(1),
81
+ sessions: z.array(sessionSchema.extend({
82
+ session: sessionSchema.shape.session.omit({ modelId: true }).extend({
83
+ model: z.string().nullable()
84
+ })
85
+ }))
86
+ });
79
87
  export const RESTART_NOTICE = 'The previous execution was interrupted by a system restart. Check the session history and current workspace, then continue unfinished work. Verify completed or uncertain actions before proceeding; do not blindly repeat actions with external side effects.';
80
88
  /** A one-shot orderly-shutdown handoff, not a crash-recovery journal. */
81
89
  export class PlannedRestartHandoff {
@@ -129,7 +137,18 @@ export class PlannedRestartHandoff {
129
137
  return;
130
138
  }
131
139
  try {
132
- const value = plannedRestartSchema.parse(JSON.parse(text));
140
+ const saved = JSON.parse(text);
141
+ const legacy = legacyPlannedRestartSchema.safeParse(saved);
142
+ const value = plannedRestartSchema.parse(legacy.success
143
+ ? {
144
+ ...legacy.data,
145
+ schemaVersion: 2,
146
+ sessions: legacy.data.sessions.map((entry) => {
147
+ const { model, ...session } = entry.session;
148
+ return { ...entry, session: { ...session, modelId: model } };
149
+ })
150
+ }
151
+ : saved);
133
152
  const identity = this.options.identity();
134
153
  if (value.nodeId !== identity.nodeId || value.serverUrl !== identity.serverUrl)
135
154
  throw new Error('RESTART_HANDOFF_IDENTITY_MISMATCH');
@@ -190,7 +209,7 @@ export class PlannedRestartHandoff {
190
209
  id: session.externalSessionId,
191
210
  workspaceId: session.workspaceId,
192
211
  cwd: session.cwd,
193
- model: session.model,
212
+ modelId: session.modelId,
194
213
  effort: session.effort,
195
214
  access: session.access
196
215
  },
@@ -207,7 +226,7 @@ export class PlannedRestartHandoff {
207
226
  });
208
227
  }
209
228
  return plannedRestartSchema.parse({
210
- schemaVersion: 1,
229
+ schemaVersion: 2,
211
230
  id: randomUUID(),
212
231
  ...this.options.identity(),
213
232
  createdAt: Date.now(),
@@ -18,7 +18,7 @@ export class RestartSessionRestorer {
18
18
  workspaceId: entry.session.workspaceId,
19
19
  ownerUserId,
20
20
  runner: 'mar-agent',
21
- model: entry.session.model
21
+ modelId: entry.session.modelId
22
22
  }));
23
23
  return contexts.get(ownerUserId);
24
24
  };
@@ -35,7 +35,7 @@ export class RestartSessionRestorer {
35
35
  throw new Error('RESTART_SESSION_MISMATCH');
36
36
  session = this.options.runtime.updateAgentSessionConfiguration({
37
37
  sessionId: session.id,
38
- model: entry.session.model ?? session.model,
38
+ modelId: entry.session.modelId ?? session.modelId,
39
39
  effort: entry.session.effort ?? session.effort,
40
40
  access: entry.session.access ?? session.access
41
41
  });
@@ -127,7 +127,7 @@ export function marctlSessionDetail(runtime, session, queuedCount) {
127
127
  ...identity(session),
128
128
  status,
129
129
  runId,
130
- model: session.model,
130
+ modelId: session.modelId,
131
131
  effort: session.effort,
132
132
  queuedCount,
133
133
  latestMessage,
@@ -29,11 +29,19 @@ export class NativeSessionProjectionService {
29
29
  return accepted === undefined ? [] : [accepted];
30
30
  });
31
31
  }
32
+ const candidates = new Map(ordered.map((session) => [session.id, session]));
32
33
  return ordered.flatMap((session) => {
33
34
  const record = database.getSessionRecord(session.id);
34
- if (record === undefined && session.parentSessionId != null)
35
+ if (record === undefined && !this.validParent(session, candidates))
35
36
  return [];
36
- return [{ ...session, parentSessionId: record?.metadata.parentSessionId ?? null }];
37
+ return [
38
+ {
39
+ ...session,
40
+ parentSessionId: record === undefined
41
+ ? (session.parentSessionId ?? null)
42
+ : (record.metadata.parentSessionId ?? null)
43
+ }
44
+ ];
37
45
  });
38
46
  }
39
47
  async discoverCodex(workspaceId) {
@@ -89,7 +97,7 @@ export class NativeSessionProjectionService {
89
97
  nativeControl: 'EXTERNAL',
90
98
  channelToken: null,
91
99
  cwd: workspace.path,
92
- model: record.metadata.model,
100
+ modelId: record.metadata.modelId,
93
101
  effort: record.metadata.effort,
94
102
  access: record.metadata.access,
95
103
  customTitle: record.metadata.customTitle,
@@ -123,7 +131,7 @@ export class NativeSessionProjectionService {
123
131
  cwd: direct.cwd,
124
132
  createdAt: direct.createdAt,
125
133
  lastActivityAt: direct.lastActivityAt,
126
- ...(direct.model === null ? {} : { model: direct.model }),
134
+ ...(direct.modelId === null ? {} : { modelId: direct.modelId }),
127
135
  ...(direct.effort === null ? {} : { effort: direct.effort }),
128
136
  ...(direct.access === null ? {} : { access: direct.access }),
129
137
  ...(direct.runnerTitle === null ? {} : { title: direct.runnerTitle }),
@@ -142,9 +150,9 @@ export class NativeSessionProjectionService {
142
150
  const fallback = saved ?? runner.defaultConfiguration(workspace);
143
151
  const configuration = {
144
152
  runner: session.runner,
145
- model: typeof session.model === 'string' && session.model.trim().length > 0
146
- ? session.model
147
- : fallback.model,
153
+ modelId: typeof session.modelId === 'string' && session.modelId.trim().length > 0
154
+ ? session.modelId
155
+ : fallback.modelId,
148
156
  effort: typeof session.effort === 'string' ? session.effort : fallback.effort,
149
157
  access: runner.restoreStoredAccess(session.access, fallback.access)
150
158
  };
@@ -152,13 +160,13 @@ export class NativeSessionProjectionService {
152
160
  }
153
161
  ensureResumedConfiguration(session) {
154
162
  const configuration = this.resumedConfiguration(session);
155
- if (session.model === configuration.model &&
163
+ if (session.modelId === configuration.modelId &&
156
164
  session.effort === configuration.effort &&
157
165
  session.access === configuration.access)
158
166
  return session;
159
167
  return this.options.runtime.updateAgentSessionConfiguration({
160
168
  sessionId: session.id,
161
- model: configuration.model,
169
+ modelId: configuration.modelId,
162
170
  effort: configuration.effort,
163
171
  access: configuration.access
164
172
  });
@@ -214,7 +222,7 @@ export class NativeSessionProjectionService {
214
222
  nativeControl: 'EXTERNAL',
215
223
  channelToken: null,
216
224
  cwd: input.cwd,
217
- model: metadata?.model ?? null,
225
+ modelId: metadata?.modelId ?? null,
218
226
  effort: metadata?.effort ?? null,
219
227
  access: metadata?.access ?? null,
220
228
  customTitle: metadata?.customTitle ?? null,
@@ -237,29 +245,30 @@ export class NativeSessionProjectionService {
237
245
  createdAt: timestamp
238
246
  };
239
247
  }
248
+ validParent(session, candidates) {
249
+ const database = this.options.database();
250
+ const visited = new Set([session.id]);
251
+ let parentId = session.parentSessionId ?? null;
252
+ let depth = 1;
253
+ while (parentId !== null) {
254
+ if (visited.has(parentId) || ++depth > 3)
255
+ return false;
256
+ visited.add(parentId);
257
+ const stored = database?.getSessionRecord(parentId);
258
+ const candidate = stored === undefined ? candidates.get(parentId) : undefined;
259
+ if ((stored?.workspaceId ?? candidate?.workspaceId) !== session.workspaceId)
260
+ return false;
261
+ parentId =
262
+ stored === undefined
263
+ ? (candidate?.parentSessionId ?? null)
264
+ : (stored.metadata.parentSessionId ?? null);
265
+ }
266
+ return true;
267
+ }
240
268
  remember(sessions) {
241
269
  const database = this.options.database();
242
270
  const candidates = new Map(sessions.map((session) => [session.id, session]));
243
- const validParent = (session) => {
244
- const visited = new Set([session.id]);
245
- let parentId = session.parentSessionId ?? null;
246
- let depth = 1;
247
- while (parentId !== null) {
248
- if (visited.has(parentId) || ++depth > 3)
249
- return false;
250
- visited.add(parentId);
251
- const stored = database?.getSessionRecord(parentId);
252
- const candidate = stored === undefined ? candidates.get(parentId) : undefined;
253
- if ((stored?.workspaceId ?? candidate?.workspaceId) !== session.workspaceId)
254
- return false;
255
- parentId =
256
- stored === undefined
257
- ? (candidate?.parentSessionId ?? null)
258
- : (stored.metadata.parentSessionId ?? null);
259
- }
260
- return true;
261
- };
262
- const valid = new Map(sessions.map((session) => [session.id, validParent(session)]));
271
+ const valid = new Map(sessions.map((session) => [session.id, this.validParent(session, candidates)]));
263
272
  const accepted = sessions.flatMap((proposed) => {
264
273
  const metadata = database === undefined || proposed.externalSessionId === null
265
274
  ? undefined