@myagentroam/node 0.11.44 → 0.11.46

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 (49) 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 +4 -11
  7. package/dist/runner/claude/native-transcript.js +1 -1
  8. package/dist/runner/claude/tool-normalizer.js +16 -11
  9. package/dist/runner/claude-code-runner.js +1 -1
  10. package/dist/runner/codex/conversation-parser.js +29 -38
  11. package/dist/runner/codex/managed-run-controller.js +5 -5
  12. package/dist/runner/codex/native-transcript.js +5 -3
  13. package/dist/runner/codex/tool-normalizer.js +13 -17
  14. package/dist/runner/codex-runner.js +10 -5
  15. package/dist/runner/execution-presentation.js +8 -51
  16. package/dist/runner/mar-agent/agent-network-extension.js +80 -24
  17. package/dist/runner/mar-agent/agent-operation-presentation.js +167 -0
  18. package/dist/runner/mar-agent/conversation-normalizer.js +118 -107
  19. package/dist/runner/mar-agent/history-projection.js +1 -1
  20. package/dist/runner/mar-agent/hosted-conversation-runtime.js +1 -1
  21. package/dist/runner/mar-agent/managed-run-controller.js +2 -2
  22. package/dist/runner/mar-agent-runner.js +5 -5
  23. package/dist/runner/normalization.js +5 -0
  24. package/dist/runner/opencode/managed-run-controller.js +2 -2
  25. package/dist/runner/opencode/tool-normalizer.js +10 -10
  26. package/dist/runner/opencode-runner.js +5 -5
  27. package/dist/runner-profiles.js +1 -1
  28. package/dist/runtime-state.js +7 -7
  29. package/dist/service/agent-network-bindings.js +10 -6
  30. package/dist/service/agent-network-service.js +2 -1
  31. package/dist/service/continuous-run-coordinator.js +1 -1
  32. package/dist/service/external-session-resume-service.js +1 -1
  33. package/dist/service/goal-continuation-coordinator.js +3 -3
  34. package/dist/service/native-session-watch-service.js +6 -3
  35. package/dist/service/planned-restart-handoff.js +24 -5
  36. package/dist/service/restart-session-restorer.js +2 -2
  37. package/dist/service/session-recent-activity.js +1 -1
  38. package/dist/session/native-session-projection-service.js +8 -8
  39. package/dist/session/runtime/conversation-projection.js +10 -3
  40. package/dist/session/session-catalog-service.js +2 -2
  41. package/dist/session/session-command-service.js +1 -0
  42. package/dist/session/session-context-service.js +1 -1
  43. package/dist/session/session-lifecycle-service.js +12 -10
  44. package/dist/session/session-message-service.js +4 -3
  45. package/dist/workspace/git.js +44 -1
  46. package/dist/workspace/workspace-change-service.js +39 -1
  47. package/dist/workspace/workspace-session-service.js +1 -1
  48. package/dist/workspace/workspace-workbench-service.js +8 -0
  49. package/package.json +4 -4
@@ -239,7 +239,7 @@ export function supportsRunnerConfiguration(input) {
239
239
  const profile = input.runner === 'claude-code'
240
240
  ? claudeProfile(input.environment ?? process.env)
241
241
  : definitions[input.runner];
242
- const model = profile.models.find((candidate) => candidate.id === input.model);
242
+ const model = profile.models.find((candidate) => candidate.id === input.modelId);
243
243
  return (model !== undefined &&
244
244
  model.supportedEfforts.includes(input.effort) &&
245
245
  profile.accessOptions.some((candidate) => candidate.id === input.access));
@@ -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,
@@ -179,9 +179,10 @@ export function createNodeAgentNetwork(options) {
179
179
  cwd: parent?.cwd ?? workspace.path,
180
180
  content: input.content,
181
181
  clientMessageId: input.clientMessageId,
182
+ __suppressNotifications: true,
182
183
  ...(!terminal && parent ? { __marctlOrigin: origin(parent) } : {}),
183
184
  ...(input.title === undefined ? {} : { title: input.title }),
184
- ...(model ? { model } : {}),
185
+ ...(model ? { modelId: model } : {}),
185
186
  ...(input.effort === undefined ? {} : { effort: input.effort }),
186
187
  ...(input.attachments === undefined ? {} : { attachments: input.attachments }),
187
188
  ...(input.attachmentIds === undefined ? {} : { attachmentIds: input.attachmentIds })
@@ -194,6 +195,7 @@ export function createNodeAgentNetwork(options) {
194
195
  objective: input.goal.kind === 'set' ? input.goal.objective : '',
195
196
  secretEnvironment: childEnvironment(actor),
196
197
  messageContent: input.content,
198
+ suppressNotifications: true,
197
199
  ...(!terminal && parent ? { messageOrigin: origin(parent) } : {}),
198
200
  clientMessageId: input.clientMessageId,
199
201
  ...(input.attachments === undefined ? {} : { attachments: input.attachments }),
@@ -227,12 +229,13 @@ export function createNodeAgentNetwork(options) {
227
229
  const current = await options.resume.resolveForMessage(session.id);
228
230
  const executionContext = options.runners
229
231
  .require(current.runner)
230
- .prepareExecutionContext(executionInput(actor, current.runner, current.model, current.parentSessionId != null));
232
+ .prepareExecutionContext(executionInput(actor, current.runner, current.modelId, current.parentSessionId != null));
231
233
  signal?.throwIfAborted();
232
234
  const result = await options.withGrant(actor, () => options.coordinator.start({
233
235
  session: current,
234
236
  objective: change.objective,
235
237
  executionContext,
238
+ suppressNotifications: true,
236
239
  secretEnvironment: childEnvironment(actor),
237
240
  ...(actor.requestUserId === undefined ? {} : { initiatedByUserId: actor.requestUserId }),
238
241
  ...(actor.personalInstructions === undefined
@@ -259,10 +262,11 @@ export function createNodeAgentNetwork(options) {
259
262
  const parent = terminalCalls.has(actor) ? undefined : await actorSession(actor);
260
263
  signal?.throwIfAborted();
261
264
  const result = await options.withGrant(actor, () => options.messages.operations()['session.message']({
262
- ...executionInput(actor, session.runner, session.model, session.parentSessionId != null),
265
+ ...executionInput(actor, session.runner, session.modelId, session.parentSessionId != null),
263
266
  ...input,
264
267
  ...(signal ? { [SESSION_ADMISSION_SIGNAL]: signal } : {}),
265
268
  sessionId: session.id,
269
+ __suppressNotifications: true,
266
270
  ...(parent ? { __marctlOrigin: origin(parent) } : {})
267
271
  }));
268
272
  return publicReceipt(result);
@@ -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) ||
@@ -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,
@@ -89,7 +89,7 @@ export class NativeSessionProjectionService {
89
89
  nativeControl: 'EXTERNAL',
90
90
  channelToken: null,
91
91
  cwd: workspace.path,
92
- model: record.metadata.model,
92
+ modelId: record.metadata.modelId,
93
93
  effort: record.metadata.effort,
94
94
  access: record.metadata.access,
95
95
  customTitle: record.metadata.customTitle,
@@ -123,7 +123,7 @@ export class NativeSessionProjectionService {
123
123
  cwd: direct.cwd,
124
124
  createdAt: direct.createdAt,
125
125
  lastActivityAt: direct.lastActivityAt,
126
- ...(direct.model === null ? {} : { model: direct.model }),
126
+ ...(direct.modelId === null ? {} : { modelId: direct.modelId }),
127
127
  ...(direct.effort === null ? {} : { effort: direct.effort }),
128
128
  ...(direct.access === null ? {} : { access: direct.access }),
129
129
  ...(direct.runnerTitle === null ? {} : { title: direct.runnerTitle }),
@@ -142,9 +142,9 @@ export class NativeSessionProjectionService {
142
142
  const fallback = saved ?? runner.defaultConfiguration(workspace);
143
143
  const configuration = {
144
144
  runner: session.runner,
145
- model: typeof session.model === 'string' && session.model.trim().length > 0
146
- ? session.model
147
- : fallback.model,
145
+ modelId: typeof session.modelId === 'string' && session.modelId.trim().length > 0
146
+ ? session.modelId
147
+ : fallback.modelId,
148
148
  effort: typeof session.effort === 'string' ? session.effort : fallback.effort,
149
149
  access: runner.restoreStoredAccess(session.access, fallback.access)
150
150
  };
@@ -152,13 +152,13 @@ export class NativeSessionProjectionService {
152
152
  }
153
153
  ensureResumedConfiguration(session) {
154
154
  const configuration = this.resumedConfiguration(session);
155
- if (session.model === configuration.model &&
155
+ if (session.modelId === configuration.modelId &&
156
156
  session.effort === configuration.effort &&
157
157
  session.access === configuration.access)
158
158
  return session;
159
159
  return this.options.runtime.updateAgentSessionConfiguration({
160
160
  sessionId: session.id,
161
- model: configuration.model,
161
+ modelId: configuration.modelId,
162
162
  effort: configuration.effort,
163
163
  access: configuration.access
164
164
  });
@@ -214,7 +214,7 @@ export class NativeSessionProjectionService {
214
214
  nativeControl: 'EXTERNAL',
215
215
  channelToken: null,
216
216
  cwd: input.cwd,
217
- model: metadata?.model ?? null,
217
+ modelId: metadata?.modelId ?? null,
218
218
  effort: metadata?.effort ?? null,
219
219
  access: metadata?.access ?? null,
220
220
  customTitle: metadata?.customTitle ?? null,
@@ -1,4 +1,4 @@
1
- import { appendExecutionPresentation, permissionStopSchema } from '@myagentroam/protocol';
1
+ import { appendToolOutput, retainExecutionInputTruncation, toolOutputSchema, permissionStopSchema } from '@myagentroam/protocol';
2
2
  import { projectConversationWorkProcesses } from './work-process.js';
3
3
  function isRecord(value) {
4
4
  return typeof value === 'object' && value !== null;
@@ -292,9 +292,16 @@ export function mergeConversationItemPayload(existing, next) {
292
292
  }
293
293
  if (next.append === 'output' &&
294
294
  isRecord(next.payload.output) &&
295
- typeof next.payload.output.text === 'string') {
295
+ Array.isArray(next.payload.output.blocks)) {
296
+ const update = toolOutputSchema.parse(next.payload.output);
297
+ const prior = base.output === undefined ? undefined : toolOutputSchema.parse(base.output);
298
+ value.output = appendToolOutput(prior, update);
296
299
  if (next.payload.toolPresentation !== undefined)
297
- value.toolPresentation = appendExecutionPresentation(base.toolPresentation, next.payload.toolPresentation);
300
+ value.toolPresentation = retainExecutionInputTruncation(base.toolPresentation, next.payload.toolPresentation);
301
+ }
302
+ if (next.append === 'output' &&
303
+ isRecord(next.payload.output) &&
304
+ typeof next.payload.output.text === 'string') {
298
305
  const prior = isRecord(base.output) && typeof base.output.text === 'string' ? base.output : undefined;
299
306
  const text = `${prior?.text ?? ''}${next.payload.output.text}`;
300
307
  value.output = {
@@ -187,7 +187,7 @@ export class SessionCatalogService {
187
187
  nativeControl: 'EXTERNAL',
188
188
  channelToken: null,
189
189
  cwd: workspace.path,
190
- model: persisted.metadata.model,
190
+ modelId: persisted.metadata.modelId,
191
191
  effort: persisted.metadata.effort,
192
192
  access: persisted.metadata.access,
193
193
  customTitle: persisted.metadata.customTitle,
@@ -283,7 +283,7 @@ export class SessionCatalogService {
283
283
  cwd,
284
284
  nativeControl: 'MAR_MANAGED',
285
285
  ...(nativeId === null ? {} : { externalSessionId: nativeId }),
286
- ...(source.model === null ? {} : { model: source.model }),
286
+ ...(source.modelId === null ? {} : { modelId: source.modelId }),
287
287
  ...(source.effort === null ? {} : { effort: source.effort }),
288
288
  ...(source.access === null ? {} : { access: source.access }),
289
289
  title
@@ -31,6 +31,7 @@ export class SessionCommandService {
31
31
  }
32
32
  async sessionCommand(input) {
33
33
  if (typeof input.sessionId !== 'string' ||
34
+ Object.hasOwn(input, 'model') ||
34
35
  typeof input.commandId !== 'string' ||
35
36
  input.commandId.length === 0 ||
36
37
  typeof input.input !== 'string' ||
@@ -20,7 +20,7 @@ export class SessionContextService {
20
20
  return { context: null, pending: false };
21
21
  }
22
22
  const context = {
23
- model: usage.model ?? session.model,
23
+ model: usage.model ?? session.modelId,
24
24
  usedTokens: usage.usedTokens,
25
25
  maxTokens: usage.maxTokens,
26
26
  percentage: Math.round((usage.usedTokens / usage.maxTokens) * 1000) / 10
@@ -120,7 +120,8 @@ export class SessionLifecycleService {
120
120
  (input.externalSessionId !== undefined && typeof input.externalSessionId !== 'string') ||
121
121
  !validSessionTitle(input.title) ||
122
122
  !validSessionTitle(input.customTitle) ||
123
- !validSessionOption(input.model) ||
123
+ Object.hasOwn(input, 'model') ||
124
+ !validSessionOption(input.modelId) ||
124
125
  !validSessionEffort(input.effort) ||
125
126
  !validSessionOption(input.access))
126
127
  throw new Error('SESSION_INVALID');
@@ -172,7 +173,7 @@ export class SessionLifecycleService {
172
173
  const fallback = runner.defaultConfiguration(workspace, environment, executionContext);
173
174
  const configuration = {
174
175
  runner: runner.name,
175
- model: typeof input.model === 'string' ? input.model : (saved?.model ?? fallback.model),
176
+ modelId: typeof input.modelId === 'string' ? input.modelId : (saved?.modelId ?? fallback.modelId),
176
177
  effort: typeof input.effort === 'string' ? input.effort : (saved?.effort ?? fallback.effort),
177
178
  access: typeof input.access === 'string'
178
179
  ? input.access
@@ -183,7 +184,7 @@ export class SessionLifecycleService {
183
184
  if (runner.name === 'mar-agent' && !['auto', 'full-access'].includes(configuration.access))
184
185
  throw new Error('SESSION_INVALID');
185
186
  if (parentSessionId !== undefined)
186
- runner.assertChildModel(configuration.model, executionContext);
187
+ runner.assertChildModel(configuration.modelId, executionContext);
187
188
  const session = this.options.runtime.createAgentSession({
188
189
  ...(parentSessionId === undefined ? {} : { parentSessionId }),
189
190
  workspaceId: input.workspaceId,
@@ -192,7 +193,7 @@ export class SessionLifecycleService {
192
193
  ...(typeof input.externalSessionId === 'string'
193
194
  ? { externalSessionId: input.externalSessionId }
194
195
  : {}),
195
- model: configuration.model,
196
+ modelId: configuration.modelId,
196
197
  effort: configuration.effort,
197
198
  access: configuration.access,
198
199
  ...(typeof input.title === 'string' ? { title: input.title } : {}),
@@ -208,8 +209,9 @@ export class SessionLifecycleService {
208
209
  }
209
210
  async updateConfiguration(input) {
210
211
  if (typeof input.sessionId !== 'string' ||
211
- (input.model === undefined && input.effort === undefined && input.access === undefined) ||
212
- !validSessionOption(input.model) ||
212
+ (input.modelId === undefined && input.effort === undefined && input.access === undefined) ||
213
+ Object.hasOwn(input, 'model') ||
214
+ !validSessionOption(input.modelId) ||
213
215
  !validSessionEffort(input.effort) ||
214
216
  !validSessionOption(input.access))
215
217
  throw new Error('SESSION_INVALID');
@@ -222,13 +224,13 @@ export class SessionLifecycleService {
222
224
  input.access !== undefined &&
223
225
  !['auto', 'full-access'].includes(input.access))
224
226
  throw new Error('SESSION_INVALID');
225
- if (current.parentSessionId != null && typeof input.model === 'string') {
227
+ if (current.parentSessionId != null && typeof input.modelId === 'string') {
226
228
  const runner = this.options.runners.require(current.runner);
227
- runner.assertChildModel(input.model, runner.prepareExecutionContext(input));
229
+ runner.assertChildModel(input.modelId, runner.prepareExecutionContext(input));
228
230
  }
229
231
  const session = this.options.runtime.updateAgentSessionConfiguration({
230
232
  sessionId: current.id,
231
- ...(typeof input.model === 'string' ? { model: input.model } : {}),
233
+ ...(typeof input.modelId === 'string' ? { modelId: input.modelId } : {}),
232
234
  ...(typeof input.effort === 'string' ? { effort: input.effort } : {}),
233
235
  ...(typeof input.access === 'string' ? { access: input.access } : {})
234
236
  });
@@ -237,7 +239,7 @@ export class SessionLifecycleService {
237
239
  runner: session.runner,
238
240
  externalSessionId: session.externalSessionId,
239
241
  workspaceId: session.workspaceId,
240
- model: session.model,
242
+ modelId: session.modelId,
241
243
  effort: session.effort,
242
244
  access: session.access
243
245
  });
@@ -91,7 +91,8 @@ export class SessionMessageService {
91
91
  }
92
92
  async prepareMessage(input) {
93
93
  checkSessionAdmission(input);
94
- if (typeof input.sessionId !== 'string' ||
94
+ if (Object.hasOwn(input, 'model') ||
95
+ typeof input.sessionId !== 'string' ||
95
96
  typeof input.clientMessageId !== 'string' ||
96
97
  input.clientMessageId.length === 0 ||
97
98
  typeof input.content !== 'string' ||
@@ -148,7 +149,7 @@ export class SessionMessageService {
148
149
  if (session.parentSessionId != null)
149
150
  this.options.runners
150
151
  .require(session.runner)
151
- .assertChildModel(session.model, executionContext);
152
+ .assertChildModel(session.modelId, executionContext);
152
153
  const sourceContent = withSessionMessageOrigin(content, input.__marctlOrigin, session.parentSessionId);
153
154
  const goalMessage = internalGoal || internalSchedule || internalRestore
154
155
  ? undefined
@@ -296,7 +297,7 @@ export class SessionMessageService {
296
297
  cwd,
297
298
  workspacePath,
298
299
  externalSessionId: currentSession.externalSessionId,
299
- model: currentSession.model,
300
+ modelId: currentSession.modelId,
300
301
  effort: currentSession.effort,
301
302
  access: currentSession.access,
302
303
  collaborationMode: planActive ? 'plan' : 'default',
@@ -107,6 +107,23 @@ export async function readRestrictedDiff(workspacePath, fromRef, toRef) {
107
107
  }
108
108
  return { binary: false, text: result.text, truncated: result.truncated };
109
109
  }
110
+ /** Fetches refs only; failures never expose remote URLs or authentication diagnostics. */
111
+ export async function fetchGitRepository(workspacePath, timeoutMs) {
112
+ if (timeoutMs <= 0)
113
+ return false;
114
+ try {
115
+ const result = await runGitAllowFailure(workspacePath, ['fetch', '--all', '--quiet', '--no-recurse-submodules', '--no-write-fetch-head'], {
116
+ timeoutMs,
117
+ processGroup: true,
118
+ includeStderrInTimeout: false,
119
+ environment: { GIT_TERMINAL_PROMPT: '0', GCM_INTERACTIVE: 'never' }
120
+ });
121
+ return result.code === 0;
122
+ }
123
+ catch {
124
+ return false;
125
+ }
126
+ }
110
127
  function currentBranchFromStatusHeader(header) {
111
128
  const value = header.slice(3);
112
129
  if (value.startsWith('HEAD '))
@@ -898,10 +915,12 @@ async function runGitAllowFailure(workspacePath, args, options = {}) {
898
915
  cwd: workspacePath,
899
916
  shell: false,
900
917
  windowsHide: true,
918
+ detached: options.processGroup === true && process.platform !== 'win32',
901
919
  stdio: ['ignore', 'pipe', 'pipe'],
902
920
  env: {
903
921
  ...process.env,
904
922
  ...gitOperationEnvironment.getStore(),
923
+ ...options.environment,
905
924
  ...(options.readOnly === true ? { GIT_OPTIONAL_LOCKS: '0' } : {})
906
925
  }
907
926
  });
@@ -911,6 +930,7 @@ async function runGitAllowFailure(workspacePath, args, options = {}) {
911
930
  let errorSize = 0;
912
931
  let truncated = false;
913
932
  let timedOut = false;
933
+ let forcedTerminationTimer;
914
934
  const timer = setTimeout(() => {
915
935
  timedOut = true;
916
936
  if (process.platform === 'win32') {
@@ -932,6 +952,26 @@ async function runGitAllowFailure(workspacePath, args, options = {}) {
932
952
  });
933
953
  return;
934
954
  }
955
+ if (options.processGroup === true && child.pid !== undefined) {
956
+ const pid = child.pid;
957
+ try {
958
+ // Give Git a chance to release transaction locks before bounded escalation.
959
+ process.kill(-pid, 'SIGTERM');
960
+ forcedTerminationTimer = setTimeout(() => {
961
+ try {
962
+ process.kill(-pid, 'SIGKILL');
963
+ }
964
+ catch {
965
+ // The process group may have completed its cleanup already.
966
+ }
967
+ }, 250);
968
+ forcedTerminationTimer.unref();
969
+ return;
970
+ }
971
+ catch {
972
+ // The group may have exited before the timeout fired.
973
+ }
974
+ }
935
975
  child.kill('SIGKILL');
936
976
  }, options.timeoutMs ?? GIT_TIMEOUT_MS);
937
977
  child.stdout.on('data', (chunk) => {
@@ -953,6 +993,7 @@ async function runGitAllowFailure(workspacePath, args, options = {}) {
953
993
  });
954
994
  child.once('error', (error) => {
955
995
  clearTimeout(timer);
996
+ clearTimeout(forcedTerminationTimer);
956
997
  logNodeCommandFailure(`git ${args[0] ?? 'unknown'}`, {
957
998
  reason: error.code ?? 'START_FAILED'
958
999
  });
@@ -960,9 +1001,11 @@ async function runGitAllowFailure(workspacePath, args, options = {}) {
960
1001
  });
961
1002
  child.once('close', (code) => {
962
1003
  clearTimeout(timer);
1004
+ clearTimeout(forcedTerminationTimer);
963
1005
  const stderr = Buffer.concat(errors).toString('utf8');
964
1006
  if (timedOut) {
965
- reject(gitCommandFailure(code === null || code === 0 ? 1 : code, `Git command timed out after ${options.timeoutMs ?? GIT_TIMEOUT_MS} ms.${stderr ? `\n${stderr}` : ''}`, args[0] ?? 'unknown', Math.round(performance.now() - startedAt), 'TIMEOUT'));
1007
+ const details = options.includeStderrInTimeout === false ? '' : stderr;
1008
+ reject(gitCommandFailure(code === null || code === 0 ? 1 : code, `Git command timed out after ${options.timeoutMs ?? GIT_TIMEOUT_MS} ms.${details ? `\n${details}` : ''}`, args[0] ?? 'unknown', Math.round(performance.now() - startedAt), 'TIMEOUT'));
966
1009
  return;
967
1010
  }
968
1011
  const output = Buffer.concat(chunks);