@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
package/dist/connector.js CHANGED
@@ -1343,7 +1343,7 @@ export class NodeConnector {
1343
1343
  this.workspaceCoordinator.clearWatches(workspaceId);
1344
1344
  this.workspaceWorkbenchService.clearWorkspace(workspaceId);
1345
1345
  this.promptCompletion?.invalidateWorkspace(workspaceId);
1346
- });
1346
+ }, (workspaceId) => this.workspaceCoordinator.gitPolicyChanged(workspaceId));
1347
1347
  this.workspaceWorkbenchService = new WorkspaceWorkbenchService({
1348
1348
  requireWorkspace: (workspaceId) => this.workspaceCoordinator.require(workspaceId),
1349
1349
  inspectWorkspace: (workspace) => this.workspaceCoordinator.inspectMetadata(workspace),
package/dist/database.js CHANGED
@@ -2,6 +2,7 @@ import { NODE_SCHEMA_MIGRATION_V17 } from './migrations/v017.js';
2
2
  import { NODE_SCHEMA_MIGRATION_V18 } from './migrations/v018.js';
3
3
  import { NODE_SCHEMA_MIGRATION_V19 } from './migrations/v019.js';
4
4
  import { NODE_SCHEMA_MIGRATION_V20 } from './migrations/v020.js';
5
+ import { NODE_SCHEMA_MIGRATION_V21 } from './migrations/v021.js';
5
6
  import { normalizeSessionPrompt } from './util/personal-instructions.js';
6
7
  import { randomUUID } from 'node:crypto';
7
8
  import { closeSync, existsSync, mkdirSync, openSync, rmSync } from 'node:fs';
@@ -23,7 +24,8 @@ export const NODE_SCHEMA_MIGRATIONS = [
23
24
  NODE_SCHEMA_MIGRATION_V17,
24
25
  NODE_SCHEMA_MIGRATION_V18,
25
26
  NODE_SCHEMA_MIGRATION_V19,
26
- NODE_SCHEMA_MIGRATION_V20
27
+ NODE_SCHEMA_MIGRATION_V20,
28
+ NODE_SCHEMA_MIGRATION_V21
27
29
  ];
28
30
  const LEGACY_NODE_SCHEMA_V12 = NODE_SCHEMA_VERSION_V13 - 1;
29
31
  const OBSOLETE_NODE_SCHEMA_V16 = 16;
@@ -71,27 +73,22 @@ export class NodeDatabase {
71
73
  this.raw.close();
72
74
  }
73
75
  getRunnerDefaults() {
74
- const rows = this.raw
75
- .prepare('SELECT runner, model, effort, access FROM runner_defaults ORDER BY runner')
76
- .all();
77
- return rows.flatMap((row) => {
78
- const parsed = runnerDefaultConfigurationSchema.safeParse({
79
- runner: stringValue(row, 'runner'),
80
- model: stringValue(row, 'model'),
81
- effort: stringValue(row, 'effort'),
82
- access: stringValue(row, 'access')
83
- });
76
+ return Object.entries(this.getNodeSettings().runnerDefaults ?? {})
77
+ .sort(([a], [b]) => a.localeCompare(b))
78
+ .flatMap(([runner, settings]) => {
79
+ const parsed = runnerDefaultConfigurationSchema.safeParse({ runner, ...settings });
84
80
  return parsed.success ? [parsed.data] : [];
85
81
  });
86
82
  }
83
+ getNodeSettings() {
84
+ const row = this.raw.prepare('SELECT settings FROM node_settings WHERE id = 1').get();
85
+ return JSON.parse(stringValue(row, 'settings'));
86
+ }
87
87
  saveRunnerDefaults(input) {
88
+ const { runner, ...settings } = runnerDefaultConfigurationSchema.parse(input);
88
89
  this.raw
89
- .prepare(`INSERT INTO runner_defaults (runner, model, effort, access)
90
- VALUES (?, ?, ?, ?)
91
- ON CONFLICT(runner) DO UPDATE SET
92
- model = excluded.model, effort = excluded.effort,
93
- access = excluded.access`)
94
- .run(input.runner, input.model, input.effort, input.access);
90
+ .prepare(`UPDATE node_settings SET settings = json_patch(settings, ?) WHERE id = 1`)
91
+ .run(JSON.stringify({ runnerDefaults: { [runner]: settings } }));
95
92
  return input;
96
93
  }
97
94
  listMcpInstallations(targetKind, targetId = '') {
@@ -188,45 +185,64 @@ export class NodeDatabase {
188
185
  }
189
186
  saveWorkspace(input) {
190
187
  const current = this.raw
191
- .prepare('SELECT id, preserve_git_config FROM workspaces WHERE path = ?')
188
+ .prepare('SELECT id, settings FROM workspaces WHERE path = ?')
192
189
  .get(input.path);
190
+ const settings = current === undefined
191
+ ? {
192
+ git: {
193
+ preserveGitConfig: input.preserveGitConfig ?? false,
194
+ includeParentRepository: input.includeParentRepository ?? true
195
+ }
196
+ }
197
+ : JSON.parse(stringValue(current, 'settings'));
193
198
  const workspace = {
194
199
  id: current === undefined ? (input.id ?? randomUUID()) : stringValue(current, 'id'),
195
200
  path: input.path,
196
201
  kind: input.kind,
197
202
  gitSummary: input.gitSummary,
198
- preserveGitConfig: current === undefined
199
- ? (input.preserveGitConfig ?? false)
200
- : Number(current.preserve_git_config) === 1
203
+ settings,
204
+ includeParentRepository: settings.git?.includeParentRepository ?? true,
205
+ preserveGitConfig: settings.git?.preserveGitConfig ?? false
201
206
  };
202
207
  const timestamp = this.now();
203
208
  this.raw
204
- .prepare(`INSERT INTO workspaces (id, path, kind, git_summary, created_at, updated_at, preserve_git_config)
209
+ .prepare(`INSERT INTO workspaces (id, path, kind, git_summary, created_at, updated_at, settings)
205
210
  VALUES (?, ?, ?, ?, ?, ?, ?)
206
211
  ON CONFLICT(path) DO UPDATE SET kind = excluded.kind,
207
212
  git_summary = excluded.git_summary, updated_at = excluded.updated_at`)
208
- .run(workspace.id, workspace.path, workspace.kind, workspace.gitSummary === null ? null : JSON.stringify(workspace.gitSummary), timestamp, timestamp, workspace.preserveGitConfig ? 1 : 0);
213
+ .run(workspace.id, workspace.path, workspace.kind, workspace.gitSummary === null ? null : JSON.stringify(workspace.gitSummary), timestamp, timestamp, JSON.stringify(settings));
209
214
  return workspace;
210
215
  }
211
216
  listWorkspaces() {
212
217
  return this.raw
213
- .prepare('SELECT id, path, kind, git_summary, preserve_git_config FROM workspaces ORDER BY created_at ASC')
218
+ .prepare('SELECT id, path, kind, git_summary, settings FROM workspaces ORDER BY created_at ASC')
214
219
  .all()
215
220
  .map((row) => workspace(row));
216
221
  }
217
222
  getWorkspace(id) {
218
223
  const row = this.raw
219
- .prepare('SELECT id, path, kind, git_summary, preserve_git_config FROM workspaces WHERE id = ?')
224
+ .prepare('SELECT id, path, kind, git_summary, settings FROM workspaces WHERE id = ?')
220
225
  .get(id);
221
226
  return row === undefined ? undefined : workspace(row);
222
227
  }
223
- /** Unregisters a Workspace registration without deleting its local directory. */
224
- setWorkspaceGitPolicy(id, preserveGitConfig) {
228
+ getWorkspaceByPath(path) {
229
+ const row = this.raw
230
+ .prepare('SELECT id, path, kind, git_summary, settings FROM workspaces WHERE path = ?')
231
+ .get(path);
232
+ return row === undefined ? undefined : workspace(row);
233
+ }
234
+ /** Updates only supplied policy fields without replacing Workspace metadata. */
235
+ setWorkspaceGitPolicy(id, preserveGitConfig, includeParentRepository) {
225
236
  if (this.getWorkspace(id) === undefined)
226
237
  throw new Error('WORKSPACE_NOT_FOUND');
227
238
  this.raw
228
- .prepare('UPDATE workspaces SET preserve_git_config=?, updated_at=? WHERE id=?')
229
- .run(preserveGitConfig ? 1 : 0, this.now(), id);
239
+ .prepare('UPDATE workspaces SET settings=json_patch(settings, ?), updated_at=? WHERE id=?')
240
+ .run(JSON.stringify({
241
+ git: {
242
+ preserveGitConfig,
243
+ ...(includeParentRepository === undefined ? {} : { includeParentRepository })
244
+ }
245
+ }), this.now(), id);
230
246
  return this.getWorkspace(id);
231
247
  }
232
248
  /** Unregisters a Workspace registration without deleting its local directory. */
@@ -474,7 +490,10 @@ export function migrate(database, catalog = NODE_SCHEMA_MIGRATIONS) {
474
490
  throw new Error('NODE_DATABASE_VERSION_UNSUPPORTED');
475
491
  if (migratedVersions.some((version, index) => version !== NODE_SCHEMA_VERSION_V13 + index))
476
492
  throw new Error('NODE_DATABASE_MIGRATION_HISTORY_INVALID');
477
- assertNodeSchemaV13(database);
493
+ if (current < NODE_SCHEMA_MIGRATION_V21.version)
494
+ assertNodeSchemaV13(database);
495
+ else
496
+ catalog[current - NODE_SCHEMA_VERSION_V13 - 1].assert(database);
478
497
  for (const entry of catalog) {
479
498
  if (entry.version <= current)
480
499
  continue;
@@ -623,9 +642,12 @@ function acquireNodeMigrationLock(databasePath) {
623
642
  }
624
643
  function workspace(row) {
625
644
  const summary = nullableString(row, 'git_summary');
645
+ const settings = JSON.parse(stringValue(row, 'settings'));
626
646
  return {
627
647
  id: stringValue(row, 'id'),
628
- preserveGitConfig: Number(row.preserve_git_config) === 1,
648
+ settings,
649
+ preserveGitConfig: settings.git?.preserveGitConfig ?? false,
650
+ includeParentRepository: settings.git?.includeParentRepository ?? true,
629
651
  path: stringValue(row, 'path'),
630
652
  kind: stringValue(row, 'kind'),
631
653
  gitSummary: summary === null ? null : JSON.parse(summary)
@@ -0,0 +1,64 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { NODE_SCHEMA_V13 } from './v013.js';
3
+ import { NODE_SCHEMA_MIGRATION_V14 } from './v014.js';
4
+ import { NODE_SCHEMA_MIGRATION_V15 } from './v015.js';
5
+ import { NODE_SCHEMA_MIGRATION_V16 } from './v016.js';
6
+ import { NODE_SCHEMA_MIGRATION_V17 } from './v017.js';
7
+ import { NODE_SCHEMA_MIGRATION_V18 } from './v018.js';
8
+ import { NODE_SCHEMA_MIGRATION_V19 } from './v019.js';
9
+ import { NODE_SCHEMA_MIGRATION_V20 } from './v020.js';
10
+ const sql = `ALTER TABLE workspaces ADD COLUMN settings TEXT NOT NULL DEFAULT '{}'
11
+ CHECK (json_valid(settings) AND json_type(settings) = 'object');
12
+ UPDATE workspaces SET settings = json_object('git', json_object(
13
+ 'preserveGitConfig', json(CASE WHEN preserve_git_config = 1 THEN 'true' ELSE 'false' END),
14
+ 'includeParentRepository', json('true')));
15
+ ALTER TABLE workspaces DROP COLUMN preserve_git_config;
16
+ CREATE TABLE node_settings (
17
+ id INTEGER PRIMARY KEY CHECK (id = 1),
18
+ settings TEXT NOT NULL CHECK (json_valid(settings) AND json_type(settings) = 'object')
19
+ ) STRICT;
20
+ INSERT INTO node_settings (id, settings)
21
+ SELECT 1, json_object('runnerDefaults', json_group_object(runner,
22
+ json_object('model', model, 'effort', effort, 'access', access))) FROM runner_defaults;
23
+ DROP TABLE runner_defaults;`;
24
+ export const NODE_SCHEMA_MIGRATION_V21 = {
25
+ version: 21,
26
+ sql,
27
+ preflight: (database) => NODE_SCHEMA_MIGRATION_V20.assert(database),
28
+ assert(database) {
29
+ const reference = new DatabaseSync(':memory:');
30
+ try {
31
+ reference.exec(NODE_SCHEMA_V13);
32
+ for (const migration of [
33
+ NODE_SCHEMA_MIGRATION_V14,
34
+ NODE_SCHEMA_MIGRATION_V15,
35
+ NODE_SCHEMA_MIGRATION_V16,
36
+ NODE_SCHEMA_MIGRATION_V17,
37
+ NODE_SCHEMA_MIGRATION_V18,
38
+ NODE_SCHEMA_MIGRATION_V19,
39
+ NODE_SCHEMA_MIGRATION_V20
40
+ ])
41
+ reference.exec(migration.sql);
42
+ reference.exec(sql);
43
+ const schema = (db) => db
44
+ .prepare("SELECT type,name,sql FROM sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%' ORDER BY type,name")
45
+ .all()
46
+ .map((row) => [
47
+ row.type,
48
+ row.name,
49
+ row.name === 'node_schema_migrations'
50
+ ? String(row.sql).replace(/\s+/g, '')
51
+ : String(row.sql).replace(/\s+/g, ' ').trim()
52
+ ]);
53
+ const expected = schema(reference);
54
+ const actual = schema(database);
55
+ const mismatch = expected.find((row) => !actual.some((value) => JSON.stringify(value) === JSON.stringify(row))) ??
56
+ actual.find((row) => !expected.some((value) => JSON.stringify(value) === JSON.stringify(row)));
57
+ if (mismatch !== undefined)
58
+ throw new Error(`NODE_DATABASE_SCHEMA_INVALID:${String(mismatch[1])}`);
59
+ }
60
+ finally {
61
+ reference.close();
62
+ }
63
+ }
64
+ };
@@ -7,6 +7,8 @@ import { nodeLog } from './operational.js';
7
7
  import { stripClaudePlanTag } from './runner-command-engine.js';
8
8
  import { stripGoalStartPrompt } from './service/goal-runtime.js';
9
9
  import { parseCodexComposerImageMarkers } from './runner/codex/composer-image-markers.js';
10
+ import { codexTranscriptSourceItemId } from './runner/codex/conversation-parser.js';
11
+ import { claudeAssistantBlocks, extractClaudeThinkingText } from './runner/claude/conversation-parser.js';
10
12
  import { extractClaudeToolResults } from './runner/claude/tool-normalizer.js';
11
13
  const DISCOVERY_READ_BYTES = 32 * 1024;
12
14
  const TRANSCRIPT_HEADER_READ_BYTES = 32 * 1024;
@@ -477,9 +479,11 @@ async function readTranscript(path, runner, byteLimit, page, targetLines = Numbe
477
479
  const generatedImage = codexGeneratedImageEvent(value);
478
480
  if (generatedImage !== undefined)
479
481
  items.push(generatedImage);
482
+ const sourceItemId = runner === 'codex' ? codexTranscriptSourceItemId(value, codexTurnId) : undefined;
480
483
  for (const parsedItem of transcriptItems(runner, value)) {
481
484
  const item = {
482
485
  ...parsedItem,
486
+ ...(sourceItemId === undefined ? {} : { sourceItemId }),
483
487
  ...(recordId === undefined ? {} : { nativeId: recordId }),
484
488
  ...(runner === 'codex' &&
485
489
  parsedItem.kind === 'message' &&
@@ -615,6 +619,32 @@ function positiveInteger(value) {
615
619
  return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
616
620
  }
617
621
  function transcriptItems(runner, record) {
622
+ if (runner === 'claude-code' && record.type === 'assistant') {
623
+ return claudeAssistantBlocks(record).flatMap(({ record: blockRecord, blockIndex, sourceItemId }) => {
624
+ const block = contentBlocks(blockRecord)[0];
625
+ if (block?.type === 'thinking') {
626
+ const text = extractClaudeThinkingText(blockRecord)[0] ?? '';
627
+ return text.length === 0
628
+ ? []
629
+ : [
630
+ {
631
+ kind: 'reasoning_summary',
632
+ representation: 'visible_thinking',
633
+ sections: [{ index: blockIndex, text }],
634
+ createdAt: timestampOf(record),
635
+ ...(sourceItemId === undefined ? {} : { sourceItemId })
636
+ }
637
+ ];
638
+ }
639
+ return transcriptRecordItems(runner, blockRecord).map((item) => ({
640
+ ...item,
641
+ ...(sourceItemId === undefined ? {} : { sourceItemId })
642
+ }));
643
+ });
644
+ }
645
+ return transcriptRecordItems(runner, record);
646
+ }
647
+ function transcriptRecordItems(runner, record) {
618
648
  const createdAt = timestampOf(record);
619
649
  const results = [...toolResultsOf(record, createdAt), ...codexToolResultsOf(record, createdAt)];
620
650
  if (results.length > 0)
@@ -689,7 +719,7 @@ function codexReasoningSummariesOf(record, createdAt) {
689
719
  return text.length === 0 ? [] : [{ index, text }];
690
720
  })
691
721
  : [];
692
- return [{ kind: 'reasoning_summary', sections, createdAt }];
722
+ return [{ kind: 'reasoning_summary', representation: 'summary', sections, createdAt }];
693
723
  }
694
724
  function unsupportedTranscriptItems(record, createdAt) {
695
725
  return contentBlocks(record).flatMap((block) => {
@@ -0,0 +1,42 @@
1
+ import { boundedConversationItemId, isPlainRecord, claudeContentBlocks, compactRunnerText } from '../../util/node-operation-parsers.js';
2
+ export function extractClaudeThinkingText(record) {
3
+ return claudeContentBlocks(record).flatMap((block) => {
4
+ // Completed public blocks only; signatures and redacted/encrypted data are never read.
5
+ if (block.type !== 'thinking' || typeof block.thinking !== 'string')
6
+ return [];
7
+ const text = compactRunnerText(block.thinking.trim(), 10_000);
8
+ return text.length === 0 ? [] : [text];
9
+ });
10
+ }
11
+ /** Preserve native block order and identity across SDK callbacks and transcript reads. */
12
+ export function claudeAssistantBlocks(record) {
13
+ const message = isPlainRecord(record.message) ? record.message : undefined;
14
+ if (message === undefined)
15
+ return [];
16
+ const content = typeof message.content === 'string'
17
+ ? [{ type: 'text', text: message.content }]
18
+ : Array.isArray(message.content)
19
+ ? message.content
20
+ : [];
21
+ const messageId = typeof record.uuid === 'string'
22
+ ? record.uuid
23
+ : typeof message.id === 'string'
24
+ ? message.id
25
+ : undefined;
26
+ return content.flatMap((block, blockIndex) => {
27
+ if (!isPlainRecord(block))
28
+ return [];
29
+ const sourceItemId = block.type === 'tool_use' && typeof block.id === 'string'
30
+ ? boundedConversationItemId('claude-tool', block.id)
31
+ : messageId === undefined
32
+ ? undefined
33
+ : JSON.stringify(['claude', messageId, blockIndex]);
34
+ return [
35
+ {
36
+ record: { ...record, message: { ...message, content: [block] } },
37
+ blockIndex,
38
+ ...(sourceItemId === undefined ? {} : { sourceItemId })
39
+ }
40
+ ];
41
+ });
42
+ }
@@ -1,8 +1,9 @@
1
+ import { claudeAssistantBlocks, extractClaudeThinkingText } from './conversation-parser.js';
1
2
  import { ClaudeUsageNormalizer } from './tool-normalizer.js';
2
3
  import { createEnvelope } from '@myagentroam/protocol';
3
4
  import { ClaudeChannelGate } from '../../claude-channel.js';
4
5
  import { nodeExecutionEnvironment } from '../../node-execution-environment.js';
5
- import { boundedConversationItemId, claudePlanText, compactRunnerText, extractClaudeText, extractClaudeThinkingSummaries, extractClaudeToolUses } from '../codex/conversation-parser.js';
6
+ import { boundedConversationItemId, claudePlanText, compactRunnerText, extractClaudeText, extractClaudeToolUses } from '../codex/conversation-parser.js';
6
7
  import { normalizeClaudeTool, extractClaudeToolResults } from './tool-normalizer.js';
7
8
  import { claudeContentBlocks } from '../../util/node-operation-parsers.js';
8
9
  import { parseSecretEnvironment } from '../../util/secret-environment.js';
@@ -344,67 +345,84 @@ export class ClaudeManagedRunController {
344
345
  finalOutputTokens.set(snapshotId, snapshotUsage.output_tokens);
345
346
  settleGeneration(snapshotId, receiptAt);
346
347
  }
347
- const text = extractClaudeText(record);
348
- if (mainTask && (text.length > 0 || extractClaudeThinkingSummaries(record).length > 0))
349
- observeResponse(typeof record.message?.id === 'string'
350
- ? record.message.id
351
- : undefined);
352
- const planText = claudePlanText(text);
353
- if (planText !== undefined)
354
- this.host.emitItem(runId, {
355
- itemId: boundedConversationItemId('claude-plan', typeof record.uuid === 'string' ? record.uuid : `${runId}:plan`),
356
- kind: 'plan',
357
- status: 'COMPLETED',
358
- payload: { explanation: compactRunnerText(planText, 20_000), steps: [] }
359
- });
360
- else if (text.length > 0)
361
- this.host.emit(runId, 'text.delta', { text });
362
- for (const [index, text] of extractClaudeThinkingSummaries(record).entries()) {
363
- this.host.emitItem(runId, {
364
- itemId: boundedConversationItemId('claude-reasoning', `${typeof record.uuid === 'string' ? record.uuid : runId}:${index}`),
365
- kind: 'reasoning_summary',
366
- status: 'COMPLETED',
367
- payload: { sections: [{ index, text }] }
368
- });
369
- }
370
- for (const tool of extractClaudeToolUses(record)) {
371
- const latencyMs = mainTask ? responseLatency.toolStarted(tool.id) : undefined;
372
- if (latencyMs !== undefined)
373
- this.host.emit(runId, 'run.performance', { latencyMs });
374
- if (tool.name === 'AskUserQuestion') {
375
- toolKinds.set(tool.id, 'user_input_request');
376
- continue;
348
+ const fallbackMessageId = crypto.randomUUID();
349
+ for (const { record: blockRecord, blockIndex, sourceItemId } of claudeAssistantBlocks(record)) {
350
+ const text = extractClaudeText(blockRecord);
351
+ if (mainTask &&
352
+ (text.length > 0 || extractClaudeThinkingText(blockRecord).length > 0))
353
+ observeResponse(typeof record.message?.id === 'string'
354
+ ? record.message.id
355
+ : undefined);
356
+ const planText = claudePlanText(text);
357
+ if (planText !== undefined)
358
+ this.host.emitItem(runId, {
359
+ itemId: boundedConversationItemId('claude-plan', sourceItemId ?? `${fallbackMessageId}:${blockIndex}`),
360
+ ...(sourceItemId === undefined ? {} : { sourceItemId }),
361
+ kind: 'plan',
362
+ status: 'COMPLETED',
363
+ payload: { explanation: compactRunnerText(planText, 20_000), steps: [] }
364
+ });
365
+ else if (text.length > 0)
366
+ this.host.emitItem(runId, {
367
+ itemId: boundedConversationItemId('claude-text', sourceItemId ?? `${fallbackMessageId}:${blockIndex}`),
368
+ ...(sourceItemId === undefined ? {} : { sourceItemId }),
369
+ kind: 'assistant_message',
370
+ status: 'COMPLETED',
371
+ payload: { text, format: 'markdown', model: null }
372
+ });
373
+ for (const text of extractClaudeThinkingText(blockRecord)) {
374
+ this.host.emitItem(runId, {
375
+ itemId: boundedConversationItemId('claude-reasoning', sourceItemId ?? `${fallbackMessageId}:${blockIndex}`),
376
+ ...(sourceItemId === undefined ? {} : { sourceItemId }),
377
+ kind: 'reasoning',
378
+ status: 'COMPLETED',
379
+ payload: {
380
+ representation: 'visible_thinking',
381
+ sections: [{ index: blockIndex, text }]
382
+ }
383
+ });
377
384
  }
378
- const itemId = boundedConversationItemId('claude-tool', tool.id);
379
- toolInputs.set(tool.id, tool);
380
- const normalized = normalizeClaudeTool(tool.name, tool.input, null, false, tool.inputTruncated, knownSubagents);
381
- const kind = normalized.kind;
382
- toolKinds.set(tool.id, kind);
383
- this.host.emitItem(runId, {
384
- itemId,
385
- kind,
386
- status: 'IN_PROGRESS',
387
- payload: normalized.payload
388
- });
389
- }
390
- for (const [index, block] of claudeContentBlocks(record).entries()) {
391
- const type = typeof block.type === 'string' ? block.type : 'unknown';
392
- if (type === 'text' ||
393
- type === 'thinking' ||
394
- type === 'redacted_thinking' ||
395
- type === 'encrypted_thinking' ||
396
- type === 'tool_use' ||
397
- type === 'tool_result')
398
- continue;
399
- this.host.emitItem(runId, {
400
- itemId: boundedConversationItemId('claude-item', `${typeof record.uuid === 'string' ? record.uuid : runId}:${index}`),
401
- kind: 'unknown',
402
- status: 'COMPLETED',
403
- payload: {
404
- sourceEventType: `claude-code:${type}`,
405
- label: `Claude Code ${compactRunnerText(type, 128)}`
385
+ for (const tool of extractClaudeToolUses(blockRecord)) {
386
+ const latencyMs = mainTask ? responseLatency.toolStarted(tool.id) : undefined;
387
+ if (latencyMs !== undefined)
388
+ this.host.emit(runId, 'run.performance', { latencyMs });
389
+ if (tool.name === 'AskUserQuestion') {
390
+ toolKinds.set(tool.id, 'user_input_request');
391
+ continue;
406
392
  }
407
- });
393
+ const itemId = boundedConversationItemId('claude-tool', tool.id);
394
+ toolInputs.set(tool.id, tool);
395
+ const normalized = normalizeClaudeTool(tool.name, tool.input, null, false, tool.inputTruncated, knownSubagents);
396
+ const kind = normalized.kind;
397
+ toolKinds.set(tool.id, kind);
398
+ this.host.emitItem(runId, {
399
+ itemId,
400
+ sourceItemId: itemId,
401
+ kind,
402
+ status: 'IN_PROGRESS',
403
+ payload: normalized.payload
404
+ });
405
+ }
406
+ for (const block of claudeContentBlocks(blockRecord)) {
407
+ const type = typeof block.type === 'string' ? block.type : 'unknown';
408
+ if (type === 'text' ||
409
+ type === 'thinking' ||
410
+ type === 'redacted_thinking' ||
411
+ type === 'encrypted_thinking' ||
412
+ type === 'tool_use' ||
413
+ type === 'tool_result')
414
+ continue;
415
+ this.host.emitItem(runId, {
416
+ itemId: boundedConversationItemId('claude-item', sourceItemId ?? `${fallbackMessageId}:${blockIndex}`),
417
+ ...(sourceItemId === undefined ? {} : { sourceItemId }),
418
+ kind: 'unknown',
419
+ status: 'COMPLETED',
420
+ payload: {
421
+ sourceEventType: `claude-code:${type}`,
422
+ label: `Claude Code ${compactRunnerText(type, 128)}`
423
+ }
424
+ });
425
+ }
408
426
  }
409
427
  }
410
428
  if (record.type === 'tool_progress' && typeof record.tool_use_id === 'string') {
@@ -135,6 +135,16 @@ export function normalizeClaudeTool(toolName, input, output, failed, truncated,
135
135
  progressLabel: null,
136
136
  errorCode: failed ? 'TOOL_EXECUTION_FAILED' : null,
137
137
  truncated: outputTruncated,
138
+ ...(toolName === 'Read'
139
+ ? {
140
+ toolPresentation: {
141
+ kind: 'READ',
142
+ paths: typeof values?.file_path === 'string' && values.file_path.length > 0
143
+ ? [compactRunnerText(values.file_path, 4096)]
144
+ : []
145
+ }
146
+ }
147
+ : {}),
138
148
  ...subagent
139
149
  }
140
150
  };
@@ -629,6 +629,20 @@ export function codexPlanText(value) {
629
629
  * A final Claude tagged Plan Mode plan is fully wrapped in `<proposed_plan>` tags;
630
630
  * text with missing or unclosed tags remains a regular assistant message.
631
631
  */
632
+ export function codexSourceItemId(nativeTurnId, itemId) {
633
+ return JSON.stringify([nativeTurnId, itemId]);
634
+ }
635
+ export function codexTranscriptSourceItemId(record, nativeTurnId) {
636
+ if (nativeTurnId === undefined ||
637
+ record.type !== 'response_item' ||
638
+ !isPlainRecord(record.payload))
639
+ return undefined;
640
+ const payload = record.payload;
641
+ const id = typeof payload.call_id === 'string' ? payload.call_id : payload.id;
642
+ return typeof id === 'string'
643
+ ? codexSourceItemId(nativeTurnId, boundedConversationItemId('codex', id))
644
+ : undefined;
645
+ }
632
646
  export function codexOfficialItem(session, nativeTurnId, value, turnSequence, itemSequence, startedAt, completedAt) {
633
647
  if (!isPlainRecord(value) || typeof value.type !== 'string' || value.type === 'subAgentActivity')
634
648
  return undefined;
@@ -645,6 +659,7 @@ export function codexOfficialItem(session, nativeTurnId, value, turnSequence, it
645
659
  : `${nativeTurnId}:${itemSequence}`;
646
660
  const base = {
647
661
  id: `${session.id}:official:${nativeTurnId}:${id}`,
662
+ sourceItemId: codexSourceItemId(nativeTurnId, boundedConversationItemId('codex', id)),
648
663
  sessionId: session.id,
649
664
  turnId: `${session.id}:official:${nativeTurnId}`,
650
665
  runId: null,
@@ -689,7 +704,7 @@ export function codexOfficialItem(session, nativeTurnId, value, turnSequence, it
689
704
  ? [{ index, text: compactRunnerText(text, 10_000) }]
690
705
  : [])
691
706
  : [];
692
- return { ...base, kind: 'reasoning_summary', payload: { sections } };
707
+ return { ...base, kind: 'reasoning', payload: { representation: 'summary', sections } };
693
708
  }
694
709
  if (value.type === 'commandExecution') {
695
710
  const output = typeof value.aggregatedOutput === 'string' ? value.aggregatedOutput : '';
@@ -835,9 +850,9 @@ export function codexLiveConversationItem(value, completed, imageAttachments = [
835
850
  : [];
836
851
  return {
837
852
  itemId,
838
- kind: 'reasoning_summary',
853
+ kind: 'reasoning',
839
854
  status: completed ? 'COMPLETED' : 'IN_PROGRESS',
840
- payload: { sections }
855
+ payload: { representation: 'summary', sections }
841
856
  };
842
857
  }
843
858
  const planText = codexPlanText(value);