@myagentroam/node 0.11.1 → 0.11.3

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 (36) hide show
  1. package/dist/connector.js +1 -1
  2. package/dist/database.js +53 -31
  3. package/dist/migrations/v021.js +64 -0
  4. package/dist/native-session-history.js +31 -1
  5. package/dist/runner/claude/conversation-parser.js +42 -0
  6. package/dist/runner/claude/managed-run-controller.js +78 -60
  7. package/dist/runner/claude/tool-normalizer.js +10 -0
  8. package/dist/runner/codex/conversation-parser.js +18 -3
  9. package/dist/runner/codex/managed-run-controller.js +25 -10
  10. package/dist/runner/mar-agent/conversation-normalizer.js +23 -9
  11. package/dist/runner/mar-agent/history-projection.js +3 -3
  12. package/dist/runner/mar-agent/managed-run-controller.js +1 -0
  13. package/dist/runner/opencode/conversation-parser.js +16 -3
  14. package/dist/runner/opencode/managed-run-controller.js +9 -4
  15. package/dist/runner/opencode/tool-normalizer.js +10 -0
  16. package/dist/runtime-state.js +47 -4
  17. package/dist/service/conversation-history-service.js +2 -1
  18. package/dist/service/conversation-segment-service.js +3 -1
  19. package/dist/service/conversation-work-process.js +102 -0
  20. package/dist/service/node-workspace-coordinator.js +21 -3
  21. package/dist/service/run-event-service.js +9 -2
  22. package/dist/service/skill-directory-service.js +2 -2
  23. package/dist/service/skill-install-service.js +15 -7
  24. package/dist/service/skill-node-operation-service.js +5 -4
  25. package/dist/service/workbench-event-projection.js +2 -0
  26. package/dist/service/workbench-manifest-service.js +1 -1
  27. package/dist/service/workspace-change-service.js +29 -13
  28. package/dist/service/workspace-file-service.js +3 -1
  29. package/dist/service/workspace-git-exclude-service.js +59 -32
  30. package/dist/service/workspace-service.js +34 -9
  31. package/dist/service/workspace-watch-snapshot-service.js +35 -5
  32. package/dist/service/workspace-workbench-service.js +23 -11
  33. package/dist/util/node-operation-parsers.js +0 -10
  34. package/dist/util/runner-native-session-parsers.js +15 -6
  35. package/dist/workspace.js +27 -3
  36. package/package.json +4 -4
@@ -6,7 +6,7 @@ import { safeErrorCode } from '../../util/safe-error.js';
6
6
  import { runnerErrorProjection } from '../../util/runner-error.js';
7
7
  import { ResponseLatencyTracker } from '../../service/response-latency-tracker.js';
8
8
  import { GenerationTimingTracker } from '../../service/generation-timing-tracker.js';
9
- import { boundedConversationItemId, codexImageAttachments, codexLiveConversationItem, CodexUsageNormalizer, codexUserInputQuestions, compactRunnerText, isPlainRecord } from './conversation-parser.js';
9
+ import { boundedConversationItemId, codexImageAttachments, codexLiveConversationItem, codexSourceItemId, CodexUsageNormalizer, codexUserInputQuestions, compactRunnerText, isPlainRecord } from './conversation-parser.js';
10
10
  export function resolveCodexNotificationRun(runs, input) {
11
11
  const entries = [...runs.entries()];
12
12
  if (input.turnId !== undefined) {
@@ -35,6 +35,15 @@ export class CodexManagedRunController {
35
35
  this.runner = runner;
36
36
  this.host = host;
37
37
  }
38
+ emitItem(runId, item) {
39
+ const nativeTurnId = this.runner.execution.runs.get(runId)?.turnId;
40
+ this.host.emitItem(runId, nativeTurnId === undefined
41
+ ? item
42
+ : {
43
+ ...item,
44
+ sourceItemId: codexSourceItemId(nativeTurnId, item.itemId)
45
+ });
46
+ }
38
47
  async executeCommand(session, command, input, context) {
39
48
  if (command.id === 'plan') {
40
49
  const environmentLeaseId = crypto.randomUUID();
@@ -222,6 +231,9 @@ export class CodexManagedRunController {
222
231
  : undefined;
223
232
  this.host.emit(runId, 'text.delta', {
224
233
  text: params.delta,
234
+ ...(itemId === undefined || turnId === undefined
235
+ ? {}
236
+ : { sourceItemId: codexSourceItemId(turnId, itemId) }),
225
237
  ...(itemId === undefined ? {} : { itemId })
226
238
  });
227
239
  if (params.delta.length > 0)
@@ -245,11 +257,14 @@ export class CodexManagedRunController {
245
257
  const index = typeof params.summaryIndex === 'number' && Number.isInteger(params.summaryIndex)
246
258
  ? params.summaryIndex
247
259
  : 0;
248
- this.host.emitItem(runId, {
260
+ this.emitItem(runId, {
249
261
  itemId: boundedConversationItemId('codex', params.itemId),
250
- kind: 'reasoning_summary',
262
+ kind: 'reasoning',
251
263
  status: 'IN_PROGRESS',
252
- payload: { sections: [{ index, text: compactRunnerText(params.delta, 10_000) }] },
264
+ payload: {
265
+ representation: 'summary',
266
+ sections: [{ index, text: compactRunnerText(params.delta, 10_000) }]
267
+ },
253
268
  merge: true,
254
269
  append: 'sections'
255
270
  });
@@ -262,7 +277,7 @@ export class CodexManagedRunController {
262
277
  if (notification.method === 'item/commandExecution/outputDelta' &&
263
278
  typeof params.itemId === 'string' &&
264
279
  typeof params.delta === 'string') {
265
- this.host.emitItem(runId, {
280
+ this.emitItem(runId, {
266
281
  itemId: boundedConversationItemId('codex', params.itemId),
267
282
  kind: 'command_execution',
268
283
  status: 'IN_PROGRESS',
@@ -275,7 +290,7 @@ export class CodexManagedRunController {
275
290
  if (notification.method === 'item/mcpToolCall/progress' &&
276
291
  typeof params.itemId === 'string' &&
277
292
  typeof params.message === 'string') {
278
- this.host.emitItem(runId, {
293
+ this.emitItem(runId, {
279
294
  itemId: boundedConversationItemId('codex', params.itemId),
280
295
  kind: 'tool_call',
281
296
  status: 'IN_PROGRESS',
@@ -289,7 +304,7 @@ export class CodexManagedRunController {
289
304
  const attachments = this.host.appendImages(runId, codexImageAttachments(params.item));
290
305
  const item = codexLiveConversationItem(params.item, notification.method === 'item/completed', attachments);
291
306
  if (item !== undefined) {
292
- this.host.emitItem(runId, item);
307
+ this.emitItem(runId, item);
293
308
  if (item.kind === 'context_compaction') {
294
309
  activeRun.generationCompacting = notification.method === 'item/started';
295
310
  if (activeRun.generationCompacting) {
@@ -317,7 +332,7 @@ export class CodexManagedRunController {
317
332
  this.host.emit(runId, 'run.performance', { latencyMs });
318
333
  }
319
334
  }
320
- else if (['assistant_message', 'reasoning_summary', 'plan'].includes(item.kind)) {
335
+ else if (['assistant_message', 'reasoning', 'plan'].includes(item.kind)) {
321
336
  // A started reasoning item is itself a semantic response, even before its summary.
322
337
  if (notification.method === 'item/started')
323
338
  this.beginGeneration(runId, receiptAt);
@@ -527,7 +542,7 @@ export class CodexManagedRunController {
527
542
  allowOther: question.allowOther
528
543
  }))
529
544
  });
530
- this.host.emitItem(runId, {
545
+ this.emitItem(runId, {
531
546
  itemId: boundedConversationItemId('codex-input', requestId),
532
547
  kind: 'user_input_request',
533
548
  status: 'PENDING',
@@ -722,7 +737,7 @@ export class CodexManagedRunController {
722
737
  }
723
738
  emitError(runId, error, fallbackCode) {
724
739
  const projected = runnerErrorProjection(error, fallbackCode);
725
- this.host.emitItem(runId, {
740
+ this.emitItem(runId, {
726
741
  itemId: boundedConversationItemId('runner-error', runId),
727
742
  kind: 'error',
728
743
  status: 'FAILED',
@@ -52,11 +52,11 @@ export class MarAgentConversationNormalizer {
52
52
  }
53
53
  ];
54
54
  }
55
- if (event.type === 'message.completed') {
55
+ if (event.type === 'message.started' || event.type === 'message.completed') {
56
56
  const modelName = record(event)?.modelName;
57
57
  const itemId = this.messageItemId(event.itemId);
58
58
  const payload = {
59
- text: event.text,
59
+ text: event.type === 'message.completed' ? event.text : '',
60
60
  format: 'markdown',
61
61
  model: typeof modelName === 'string' ? modelName : null
62
62
  };
@@ -64,7 +64,7 @@ export class MarAgentConversationNormalizer {
64
64
  {
65
65
  itemId,
66
66
  kind: 'assistant_message',
67
- status: 'COMPLETED',
67
+ status: event.type === 'message.completed' ? 'COMPLETED' : 'IN_PROGRESS',
68
68
  payload
69
69
  }
70
70
  ];
@@ -78,20 +78,24 @@ export class MarAgentConversationNormalizer {
78
78
  payload: { explanation: event.text, steps: [] }
79
79
  }
80
80
  ];
81
- if (event.type === 'reasoning.summary')
81
+ if (event.type === 'reasoning.completed')
82
82
  return [
83
83
  {
84
- itemId: itemId('reasoning', event.eventId),
85
- kind: 'reasoning_summary',
84
+ itemId: itemId('reasoning', event.itemId),
85
+ kind: 'reasoning',
86
86
  status: 'COMPLETED',
87
- payload: { sections: [{ index: 0, text: compactRunnerText(event.text, 10_000) }] }
87
+ payload: {
88
+ representation: event.representation,
89
+ sections: [{ index: 0, text: compactRunnerText(event.text, 10_000) }]
90
+ }
88
91
  }
89
92
  ];
90
93
  if (event.type === 'tool.started' ||
91
94
  event.type === 'tool.completed' ||
92
95
  event.type === 'tool.failed') {
93
- // Native web calls and generated artifacts stay visible independently of cell observations.
96
+ // Read/web calls and generated artifacts stay visible independently of cell observations.
94
97
  if (event.parentCallId !== undefined &&
98
+ event.toolName !== 'read' &&
95
99
  event.toolName !== 'web_search' &&
96
100
  event.toolName !== 'web_fetch' &&
97
101
  !isImageGenerationTool(event.toolName))
@@ -352,7 +356,7 @@ export class MarAgentConversationNormalizer {
352
356
  if (observation.id !== undefined)
353
357
  this.#codeModeObservationIds.add(observation.id);
354
358
  this.#codeModeSourceCallIds.add(observation.source.callId);
355
- items.push(...projected);
359
+ items.push(...projected.map((item) => 'callId' in event ? { ...item, parentItemId: itemId('tool', event.callId) } : item));
356
360
  }
357
361
  return items;
358
362
  }
@@ -665,6 +669,16 @@ function genericToolItem(id, toolName, status, input, outputSummary = null, erro
665
669
  };
666
670
  }
667
671
  function projectToolPresentation(toolName, input, output, startedAt) {
672
+ if (toolName === 'read') {
673
+ const reads = record(input)?.reads;
674
+ const paths = (Array.isArray(reads) ? reads : [])
675
+ .flatMap((entry) => {
676
+ const path = record(entry)?.path;
677
+ return typeof path === 'string' && path.length > 0 ? [compactRunnerText(path, 4096)] : [];
678
+ })
679
+ .slice(0, 16);
680
+ return { kind: 'READ', paths };
681
+ }
668
682
  const category = toolName === 'web_search'
669
683
  ? 'WEB_SEARCH'
670
684
  : toolName === 'web_fetch'
@@ -102,7 +102,7 @@ export function projectMarAgentHistory(session, entries, options = {}) {
102
102
  const attachments = projected.generatedImages === undefined
103
103
  ? []
104
104
  : (options.registerGeneratedImages?.(event.sessionId, projected.generatedImages) ?? []);
105
- replace(projectionTurn, historyItem(session, projectionTurn, projected.itemId, projected.kind, projected.status, attachments.length === 0 ? projected.payload : { ...projected.payload, attachments }, at, projected.permissionStop));
105
+ replace(projectionTurn, historyItem(session, projectionTurn, projected.itemId, projected.kind, projected.status, attachments.length === 0 ? projected.payload : { ...projected.payload, attachments }, at, projected.permissionStop, projected.parentItemId));
106
106
  }
107
107
  }
108
108
  return [...turns.values()];
@@ -129,13 +129,13 @@ function record(value) {
129
129
  ? value
130
130
  : undefined;
131
131
  }
132
- function historyItem(session, turn, id, kind, status, payload, at, permissionStop) {
132
+ function historyItem(session, turn, id, kind, status, payload, at, permissionStop, parentItemId) {
133
133
  return {
134
134
  id,
135
135
  sessionId: session.id,
136
136
  turnId: turn.id,
137
137
  runId: turn.runId,
138
- parentItemId: null,
138
+ parentItemId: parentItemId ?? null,
139
139
  sourceSequence: null,
140
140
  revision: 0,
141
141
  kind,
@@ -709,6 +709,7 @@ export class MarAgentManagedRunController {
709
709
  []);
710
710
  this.host.emitItem(runId, {
711
711
  itemId: item.itemId,
712
+ ...(item.parentItemId === undefined ? {} : { parentItemId: item.parentItemId }),
712
713
  kind: item.kind,
713
714
  status: item.status,
714
715
  payload: attachments.length === 0 ? item.payload : { ...item.payload, attachments },
@@ -139,7 +139,12 @@ function openCodeTurn(session, user, userParts, assistants, sequence, managed) {
139
139
  for (const part of assistant.parts) {
140
140
  const projected = partItem(session, nativeTurnId, part, assistant.info.mode === 'plan', startedAt, completedAt, sequence);
141
141
  if (projected !== undefined)
142
- items.push(projected);
142
+ items.push({
143
+ ...projected,
144
+ ...(typeof assistant.info.id === 'string' && typeof record(part)?.id === 'string'
145
+ ? { sourceItemId: openCodeSourceItemId(assistant.info.id, record(part).id) }
146
+ : {})
147
+ });
143
148
  }
144
149
  }
145
150
  if (items.length === 0)
@@ -173,12 +178,17 @@ function partItem(session, turnId, raw, planMode, startedAt, completedAt, sequen
173
178
  const part = record(raw);
174
179
  if (typeof part?.id !== 'string' || typeof part.type !== 'string')
175
180
  return undefined;
181
+ if (part.type === 'reasoning' && (typeof part.text !== 'string' || !part.text.trim()))
182
+ return undefined;
176
183
  if ((part.type === 'text' || part.type === 'reasoning') && typeof part.text === 'string')
177
- return item(session, turnId, part.id, part.type === 'text' ? (planMode ? 'plan' : 'assistant_message') : 'reasoning_summary', completedAt === null ? 'IN_PROGRESS' : 'COMPLETED', part.type === 'text'
184
+ return item(session, turnId, part.id, part.type === 'text' ? (planMode ? 'plan' : 'assistant_message') : 'reasoning', completedAt === null ? 'IN_PROGRESS' : 'COMPLETED', part.type === 'text'
178
185
  ? planMode
179
186
  ? { explanation: compactRunnerText(part.text, 20_000), steps: [] }
180
187
  : { text: compactRunnerText(part.text, 20_000), format: 'markdown', model: null }
181
- : { sections: [{ index: 0, text: compactRunnerText(part.text, 10_000) }] }, startedAt, completedAt, sequence);
188
+ : {
189
+ representation: 'unspecified',
190
+ sections: [{ index: 0, text: compactRunnerText(part.text, 10_000) }]
191
+ }, startedAt, completedAt, sequence);
182
192
  if (part.type === 'tool' && typeof part.tool === 'string') {
183
193
  const state = record(part.state);
184
194
  if (part.tool === 'question' && typeof part.callID === 'string') {
@@ -258,6 +268,9 @@ export function openCodePatchToolPayload() {
258
268
  truncated: false
259
269
  };
260
270
  }
271
+ export function openCodeSourceItemId(messageId, partId) {
272
+ return JSON.stringify(['opencode', messageId, partId]);
273
+ }
261
274
  function item(session, turnId, nativeId, kind, status, payload, startedAt, completedAt, sequence) {
262
275
  return {
263
276
  id: boundedConversationItemId(`${session.id}:official:${turnId}`, nativeId),
@@ -1,4 +1,4 @@
1
- import { OpenCodeUsageNormalizer } from './conversation-parser.js';
1
+ import { OpenCodeUsageNormalizer, openCodeSourceItemId } from './conversation-parser.js';
2
2
  import { parseComposerTags } from '@myagentroam/protocol';
3
3
  import { nodeExecutionEnvironment } from '../../node-execution-environment.js';
4
4
  import { parseSecretEnvironment } from '../../util/secret-environment.js';
@@ -440,7 +440,10 @@ export class OpenCodeManagedRunController {
440
440
  }
441
441
  const item = openCodePart(part, this.runner.execution.get(runId)?.agent === 'plan');
442
442
  if (item !== undefined)
443
- this.host.emitItem(runId, item);
443
+ this.host.emitItem(runId, {
444
+ ...item,
445
+ sourceItemId: openCodeSourceItemId(part.messageID, part.id)
446
+ });
444
447
  if (typeof part.id === 'string') {
445
448
  const itemId = boundedConversationItemId('opencode', part.id);
446
449
  const responseLatency = this.currentResponseMessage(runId, part.messageID)
@@ -688,6 +691,8 @@ function openCodePart(part, planMode = false) {
688
691
  const id = typeof part?.id === 'string' ? part.id : undefined;
689
692
  if (id === undefined || typeof part?.type !== 'string')
690
693
  return undefined;
694
+ if (part.type === 'reasoning' && (typeof part.text !== 'string' || !part.text.trim()))
695
+ return undefined;
691
696
  const itemId = boundedConversationItemId('opencode', id);
692
697
  if (part.type === 'text' && typeof part.text === 'string')
693
698
  return {
@@ -702,9 +707,9 @@ function openCodePart(part, planMode = false) {
702
707
  if (part.type === 'reasoning' && typeof part.text === 'string')
703
708
  return {
704
709
  itemId,
705
- kind: 'reasoning_summary',
710
+ kind: 'reasoning',
706
711
  status: record(part.time)?.end === undefined ? 'IN_PROGRESS' : 'COMPLETED',
707
- payload: { sections: [{ index: 0, text: part.text }] },
712
+ payload: { representation: 'unspecified', sections: [{ index: 0, text: part.text }] },
708
713
  merge: true
709
714
  };
710
715
  if (part.type === 'tool' && typeof part.tool === 'string') {
@@ -38,6 +38,16 @@ export function normalizeOpenCodeTool(toolName, state) {
38
38
  progressLabel: null,
39
39
  errorCode: status === 'FAILED' ? 'OPENCODE_TOOL_FAILED' : null,
40
40
  truncated: false,
41
+ ...(normalizedName === 'read'
42
+ ? {
43
+ toolPresentation: {
44
+ kind: 'READ',
45
+ paths: typeof input?.filePath === 'string' && input.filePath.length > 0
46
+ ? [compactRunnerText(input.filePath, 4096)]
47
+ : []
48
+ }
49
+ }
50
+ : {}),
41
51
  ...subagent
42
52
  }
43
53
  };
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { projectConversationWorkProcesses } from './service/conversation-work-process.js';
2
3
  import { nodeLog } from './operational.js';
3
4
  import { safeErrorCode } from './util/safe-error.js';
4
5
  import { permissionStopSchema } from '@myagentroam/protocol';
@@ -1383,6 +1384,7 @@ export class NodeRuntimeState {
1383
1384
  this.currentTurnForRun(event.runId) === undefined &&
1384
1385
  conversationItemFromEvent(event) !== undefined)
1385
1386
  this.createItemOnlyTurn(run, event.createdAt);
1387
+ let needsProcessProjection = false;
1386
1388
  const update = (turn) => {
1387
1389
  if (event.eventType === 'text.delta' &&
1388
1390
  isRecord(event.payload) &&
@@ -1398,8 +1400,14 @@ export class NodeRuntimeState {
1398
1400
  : undefined
1399
1401
  : turn.items.find((item) => item.id === stableItemId && item.kind === 'assistant_message');
1400
1402
  const text = `${existing === undefined ? '' : readableText(existing.payload)}${event.payload.text}`;
1403
+ const sourceItemId = conversationSourceItemId(event.payload) ?? existing?.sourceItemId;
1404
+ needsProcessProjection =
1405
+ existing === undefined ||
1406
+ existing.sourceItemId !== sourceItemId ||
1407
+ (!readableText(existing.payload).trim() && !!text.trim());
1401
1408
  const item = {
1402
1409
  id: existing?.id ?? stableItemId ?? `${event.runId}:assistant:${event.sequence}`,
1410
+ ...(sourceItemId === undefined ? {} : { sourceItemId }),
1403
1411
  sessionId: run.sessionId,
1404
1412
  turnId: turn.id,
1405
1413
  runId: event.runId,
@@ -1407,6 +1415,7 @@ export class NodeRuntimeState {
1407
1415
  sourceSequence: existing?.sourceSequence ?? event.sequence,
1408
1416
  revision: event.sequence,
1409
1417
  kind: 'assistant_message',
1418
+ ...(existing?.processGroup === undefined ? {} : { processGroup: existing.processGroup }),
1410
1419
  updatedAt: event.createdAt,
1411
1420
  status: 'IN_PROGRESS',
1412
1421
  payload: { text, format: 'markdown', model: null },
@@ -1447,15 +1456,30 @@ export class NodeRuntimeState {
1447
1456
  ? trailingItem
1448
1457
  : undefined);
1449
1458
  const status = projected.status;
1459
+ const sourceItemId = projected.sourceItemId ?? existing?.sourceItemId;
1460
+ const parentItemId = projected.parentItemId === undefined
1461
+ ? (existing?.parentItemId ?? null)
1462
+ : runConversationItemId(event.runId, projected.parentItemId);
1463
+ needsProcessProjection =
1464
+ existing === undefined ||
1465
+ existing.kind !== projected.kind ||
1466
+ projected.kind === 'context_compaction' ||
1467
+ existing.parentItemId !== parentItemId ||
1468
+ existing.sourceItemId !== sourceItemId ||
1469
+ (projected.kind === 'assistant_message' &&
1470
+ !!readableText(existing.payload).trim() !== !!readableText(projected.payload).trim()) ||
1471
+ (projected.kind === 'reasoning' && existing.processGroup === undefined);
1450
1472
  const item = {
1451
1473
  id: itemId,
1452
1474
  sessionId: run.sessionId,
1453
1475
  turnId: turn.id,
1454
1476
  runId: event.runId,
1455
- parentItemId: null,
1477
+ parentItemId,
1478
+ ...(sourceItemId === undefined ? {} : { sourceItemId }),
1456
1479
  sourceSequence: existing?.sourceSequence ?? event.sequence,
1457
1480
  revision: (existing?.revision ?? -1) + 1,
1458
1481
  kind: projected.kind,
1482
+ ...(existing?.processGroup === undefined ? {} : { processGroup: existing.processGroup }),
1459
1483
  updatedAt: event.createdAt,
1460
1484
  status,
1461
1485
  payload: mergeConversationItemPayload(existing?.payload, projected),
@@ -1495,11 +1519,16 @@ export class NodeRuntimeState {
1495
1519
  }
1496
1520
  return turn;
1497
1521
  };
1522
+ const project = (turn) => {
1523
+ const next = update(turn);
1524
+ // Ordinary token/output updates retain membership; only structural input needs a scan.
1525
+ return needsProcessProjection ? projectConversationWorkProcesses(next) : next;
1526
+ };
1498
1527
  if (turnId === undefined)
1499
- this.updateCurrentTurnForRun(event.runId, update);
1528
+ this.updateCurrentTurnForRun(event.runId, project);
1500
1529
  else {
1501
1530
  const current = this.turns.get(run.sessionId) ?? [];
1502
- this.turns.set(run.sessionId, current.map((turn) => (turn.id === turnId ? update(turn) : turn)));
1531
+ this.turns.set(run.sessionId, current.map((turn) => (turn.id === turnId ? project(turn) : turn)));
1503
1532
  }
1504
1533
  }
1505
1534
  }
@@ -1575,12 +1604,19 @@ function conversationItemFromEvent(event) {
1575
1604
  !isRecord(payload))
1576
1605
  return undefined;
1577
1606
  const append = event.payload.append;
1607
+ const sourceItemId = conversationSourceItemId(event.payload);
1578
1608
  const permissionStop = permissionStopSchema.safeParse(event.payload.permissionStop);
1579
1609
  return {
1580
1610
  itemId,
1581
1611
  kind,
1582
1612
  status,
1583
1613
  payload,
1614
+ ...(sourceItemId === undefined ? {} : { sourceItemId }),
1615
+ ...(typeof event.payload.parentItemId === 'string' &&
1616
+ event.payload.parentItemId.length > 0 &&
1617
+ event.payload.parentItemId.length <= 88
1618
+ ? { parentItemId: event.payload.parentItemId }
1619
+ : {}),
1584
1620
  ...(permissionStop.success ? { permissionStop: permissionStop.data } : {}),
1585
1621
  merge: event.payload.merge === true,
1586
1622
  ...(append === 'sections' || append === 'outputPreview' || append === 'outputSummary'
@@ -1588,10 +1624,17 @@ function conversationItemFromEvent(event) {
1588
1624
  : {})
1589
1625
  };
1590
1626
  }
1627
+ function conversationSourceItemId(payload) {
1628
+ return typeof payload.sourceItemId === 'string' &&
1629
+ payload.sourceItemId.length > 0 &&
1630
+ payload.sourceItemId.length <= 1024
1631
+ ? payload.sourceItemId
1632
+ : undefined;
1633
+ }
1591
1634
  function projectableConversationKind(value) {
1592
1635
  return (value === 'user_message' ||
1593
1636
  value === 'assistant_message' ||
1594
- value === 'reasoning_summary' ||
1637
+ value === 'reasoning' ||
1595
1638
  value === 'tool_call' ||
1596
1639
  value === 'command_execution' ||
1597
1640
  value === 'approval_request' ||
@@ -1,3 +1,4 @@
1
+ import { projectConversationWorkProcesses } from './conversation-work-process.js';
1
2
  import { discoverNativeSessions, readNativeSession } from '../native-session-history.js';
2
3
  import { nativeConversationPage } from '../util/runner-native-session-parsers.js';
3
4
  import { isPlainRecord } from '../util/node-operation-parsers.js';
@@ -552,7 +553,7 @@ export class ConversationHistoryService {
552
553
  page(nextCursor, turns, metadata) {
553
554
  return {
554
555
  ...metadata,
555
- turns,
556
+ turns: turns.map(projectConversationWorkProcesses),
556
557
  nextCursor,
557
558
  latestVisibleAt: Math.max(0, ...turns.map((turn) => Math.max(turn.startedAt ?? 0, turn.completedAt ?? 0)))
558
559
  };
@@ -1,3 +1,4 @@
1
+ import { projectConversationWorkProcesses } from './conversation-work-process.js';
1
2
  const MAX_INITIAL_TURN_PAGE_SIZE = 30;
2
3
  function conversationSegmentPrefix(turn) {
3
4
  const user = turn.items.find((item) => item.kind === 'user_message');
@@ -10,6 +11,7 @@ function conversationSegmentPrefix(turn) {
10
11
  : turn.id;
11
12
  }
12
13
  export function conversationSegments(turn) {
14
+ turn = projectConversationWorkProcesses(turn);
13
15
  const prefix = conversationSegmentPrefix(turn);
14
16
  const groups = [];
15
17
  let current = [];
@@ -25,7 +27,7 @@ export function conversationSegments(turn) {
25
27
  return groups.map((items, index) => {
26
28
  const id = `${prefix}:segment:${index}`;
27
29
  const latest = index === groups.length - 1;
28
- const projectedItems = items.map((item) => ({
30
+ const projectedItems = items.map(({ sourceItemId: _sourceItemId, ...item }) => ({
29
31
  ...item,
30
32
  runId: turn.runId,
31
33
  segmentId: id,
@@ -0,0 +1,102 @@
1
+ import { createHash } from 'node:crypto';
2
+ const members = new Set([
3
+ 'reasoning',
4
+ 'tool_call',
5
+ 'command_execution',
6
+ 'file_change',
7
+ 'todo_list'
8
+ ]);
9
+ const boundaries = new Set([
10
+ 'user_message',
11
+ 'assistant_message',
12
+ 'approval_request',
13
+ 'user_input_request',
14
+ 'plan',
15
+ 'turn_summary'
16
+ ]);
17
+ function record(value) {
18
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
19
+ ? value
20
+ : {};
21
+ }
22
+ function visible(item) {
23
+ const payload = record(item.payload);
24
+ if (item.kind === 'assistant_message')
25
+ return typeof payload.text === 'string' && !!payload.text.trim();
26
+ if (item.kind === 'reasoning')
27
+ return (Array.isArray(payload.sections) &&
28
+ payload.sections.some((part) => {
29
+ const text = record(part).text;
30
+ return typeof text === 'string' && !!text.trim();
31
+ }));
32
+ return true;
33
+ }
34
+ /** Runtime owns this exact prefix; native history already carries the unprefixed identity. */
35
+ function sourceIdentity(item) {
36
+ if (item.sourceItemId !== undefined)
37
+ return item.sourceItemId;
38
+ const prefix = item.runId === null ? undefined : `${item.runId}:`;
39
+ return prefix !== undefined && item.id.startsWith(prefix)
40
+ ? item.id.slice(prefix.length)
41
+ : item.id;
42
+ }
43
+ /** Project complete public input before transport segmentation, without clocks or stored groups. */
44
+ export function projectConversationWorkProcesses(turn) {
45
+ const positions = new Map(turn.items.map((item, index) => [item.id, index]));
46
+ const anchored = new Map();
47
+ const relocated = new Set();
48
+ for (const item of turn.items) {
49
+ const payload = record(item.payload);
50
+ if (item.kind !== 'context_compaction' ||
51
+ !['BACKGROUND', 'BLOCKING'].includes(String(payload.mode)) ||
52
+ typeof payload.anchorItemId !== 'string' ||
53
+ !positions.has(payload.anchorItemId) ||
54
+ payload.anchorItemId === item.id ||
55
+ turn.items[positions.get(payload.anchorItemId)]?.kind === 'context_compaction')
56
+ continue;
57
+ const list = anchored.get(payload.anchorItemId) ?? [];
58
+ list.push(item);
59
+ anchored.set(payload.anchorItemId, list);
60
+ relocated.add(item.id);
61
+ }
62
+ const ordered = turn.items.flatMap((item) => relocated.has(item.id) ? [] : [item, ...(anchored.get(item.id) ?? [])]);
63
+ let knownBoundary = turn.runId !== null || turn.items.some((item) => item.id === turn.userItemId);
64
+ let current;
65
+ const assigned = new Map();
66
+ const items = ordered.map((item) => {
67
+ const payload = record(item.payload);
68
+ let group;
69
+ let isStart = false;
70
+ const independent = boundaries.has(item.kind) ||
71
+ (item.kind === 'context_compaction' && payload.mode !== 'BACKGROUND') ||
72
+ (item.kind === 'error' && item.parentItemId === null);
73
+ if (visible(item)) {
74
+ if (independent) {
75
+ current = undefined;
76
+ knownBoundary = true;
77
+ }
78
+ else if (members.has(item.kind) || ['warning', 'error', 'unknown'].includes(item.kind)) {
79
+ group = item.parentItemId === null ? undefined : assigned.get(item.parentItemId);
80
+ if (group === undefined) {
81
+ group = current;
82
+ if (group === undefined && knownBoundary && members.has(item.kind)) {
83
+ group = `process:${createHash('sha256')
84
+ .update(`${turn.sessionId}\0${sourceIdentity(item)}`)
85
+ .digest('hex')}`;
86
+ current = group;
87
+ isStart = true;
88
+ }
89
+ }
90
+ if (group !== undefined)
91
+ assigned.set(item.id, group);
92
+ }
93
+ }
94
+ if (item.processGroup?.id === group && (item.processGroup?.isStart === true) === isStart)
95
+ return item;
96
+ const { processGroup: _previous, ...rest } = item;
97
+ return group === undefined
98
+ ? rest
99
+ : { ...rest, processGroup: { id: group, ...(isStart ? { isStart: true } : {}) } };
100
+ });
101
+ return items.every((item, index) => item === turn.items[index]) ? turn : { ...turn, items };
102
+ }
@@ -1,3 +1,5 @@
1
+ import { realpath } from 'node:fs/promises';
2
+ import { nodeLog } from '../operational.js';
1
3
  import { inspectWorkspace, normalizeWorkspacePath } from '../workspace.js';
2
4
  import { ensureWorkspaceGitExclude } from './workspace-git-exclude-service.js';
3
5
  /** Coordinates Workspace invalidation, inspection and persisted Terminal summaries. */
@@ -18,7 +20,7 @@ export class NodeWorkspaceCoordinator {
18
20
  const config = this.options.config();
19
21
  if (config === undefined)
20
22
  throw new Error('NODE_CONFIG_UNAVAILABLE');
21
- const inspection = await inspectWorkspace(workspace.path, config.allowedRoots);
23
+ const inspection = await inspectWorkspace(workspace.path, config.allowedRoots, workspace.includeParentRepository ?? true);
22
24
  if (normalizeWorkspacePath(inspection.path) !== normalizeWorkspacePath(workspace.path))
23
25
  throw new Error('WORKSPACE_NOT_ALLOWED');
24
26
  return inspection;
@@ -29,8 +31,14 @@ export class NodeWorkspaceCoordinator {
29
31
  throw new Error('NODE_DATABASE_UNAVAILABLE');
30
32
  if (normalizeWorkspacePath(inspection.path) !== normalizeWorkspacePath(workspace.path))
31
33
  throw new Error('WORKSPACE_NOT_ALLOWED');
34
+ const current = this.require(workspace.id);
35
+ if ((current.includeParentRepository ?? true) !== (workspace.includeParentRepository ?? true))
36
+ return current;
32
37
  if (workspace.kind !== 'GIT_WORKSPACE' && inspection.kind === 'GIT_WORKSPACE')
33
- await ensureWorkspaceGitExclude(workspace.path);
38
+ await ensureWorkspaceGitExclude(workspace.path, workspace.includeParentRepository ?? true);
39
+ if ((this.require(workspace.id).includeParentRepository ?? true) !==
40
+ (workspace.includeParentRepository ?? true))
41
+ return this.require(workspace.id);
34
42
  const saved = database.saveWorkspace({
35
43
  id: workspace.id,
36
44
  path: workspace.path,
@@ -57,6 +65,14 @@ export class NodeWorkspaceCoordinator {
57
65
  invalidateChanges(workspaceId) {
58
66
  this.options.changes.invalidate(workspaceId);
59
67
  }
68
+ gitPolicyChanged(workspaceId) {
69
+ this.invalidate(workspaceId);
70
+ const workspace = this.require(workspaceId);
71
+ void this.inspectMetadata(workspace)
72
+ .then((inspection) => this.commitMetadata(workspace, inspection))
73
+ .then(() => this.invalidate(workspaceId))
74
+ .catch(() => nodeLog('workspace.git-policy.refresh-failed', { workspaceId }));
75
+ }
60
76
  persistTerminal(summary) {
61
77
  const database = this.options.database();
62
78
  if (database === undefined)
@@ -87,7 +103,9 @@ export class NodeWorkspaceCoordinator {
87
103
  return;
88
104
  }
89
105
  try {
90
- const inspection = await inspectWorkspace(requestedPath, config.allowedRoots);
106
+ const path = await realpath(requestedPath);
107
+ const current = this.options.database()?.getWorkspaceByPath(path);
108
+ const inspection = await inspectWorkspace(path, config.allowedRoots, current?.includeParentRepository ?? true);
91
109
  this.options.send('workspace.inspect.result', inspection, envelope.id);
92
110
  }
93
111
  catch {