@myagentroam/node 0.10.6 → 0.10.8

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 (33) hide show
  1. package/dist/connector.js +60 -5
  2. package/dist/database.js +3 -1
  3. package/dist/migrations/v017.js +46 -0
  4. package/dist/native-session-history.js +7 -15
  5. package/dist/runner/abstract-runner.js +4 -0
  6. package/dist/runner/claude/managed-run-controller.js +15 -3
  7. package/dist/runner/claude/tool-normalizer.js +100 -6
  8. package/dist/runner/codex/conversation-parser.js +41 -30
  9. package/dist/runner/codex/managed-run-controller.js +12 -1
  10. package/dist/runner/codex/tool-normalizer.js +5 -2
  11. package/dist/runner/mar-agent/compact-extensions.js +1 -0
  12. package/dist/runner/mar-agent/conversation-normalizer.js +43 -78
  13. package/dist/runner/mar-agent/hosted-conversation-runtime.js +27 -0
  14. package/dist/runner/mar-agent/managed-run-controller.js +13 -1
  15. package/dist/runner/mar-agent-runner.js +15 -0
  16. package/dist/runner/opencode/conversation-parser.js +49 -0
  17. package/dist/runner/opencode/managed-run-controller.js +12 -0
  18. package/dist/service/continuous-run-coordinator.js +1 -0
  19. package/dist/service/hosted-conversation-node-service.js +29 -2
  20. package/dist/service/model-usage-outbox.js +167 -0
  21. package/dist/service/model-usage-service.js +61 -0
  22. package/dist/service/node-control-message-service.js +7 -2
  23. package/dist/service/node-request-service.js +3 -1
  24. package/dist/service/node-run-event-bridge.js +6 -0
  25. package/dist/service/runner-usage-tracker.js +62 -0
  26. package/dist/service/session-command-service.js +4 -0
  27. package/dist/service/session-message-service.js +6 -0
  28. package/dist/service/session-query-service.js +16 -0
  29. package/dist/service/workspace-change-service.js +2 -0
  30. package/dist/util/node-operation-parsers.js +0 -21
  31. package/dist/util/runner-native-session-parsers.js +9 -4
  32. package/dist/workspace.js +20 -1
  33. package/package.json +5 -5
package/dist/connector.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { normalizeMarAgentUsage } from './runner/mar-agent/conversation-normalizer.js';
2
+ import { ModelUsageOutbox } from './service/model-usage-outbox.js';
3
+ import { NodeModelUsageService } from './service/model-usage-service.js';
1
4
  import { dirname, resolve } from 'node:path';
2
5
  import { createEnvelope, composerVisibleText, directNodeCommandSchema } from '@myagentroam/protocol';
3
6
  import { hostedConversationStorePath, loadNodeConfig, computerUseRuntimeExecutable, directTransferUdpPortRange, nodeDataDirectory, nodeDatabasePath } from './config.js';
@@ -125,6 +128,7 @@ const HOSTED_CONVERSATION_OPERATIONS = [
125
128
  'conversation.hosted.config.update',
126
129
  'conversation.hosted.metadata.update',
127
130
  'conversation.hosted.image.read',
131
+ 'conversation.hosted.tool-resource.read',
128
132
  'conversation.hosted.files.list',
129
133
  'conversation.hosted.file.read',
130
134
  'conversation.hosted.file.delete',
@@ -157,6 +161,8 @@ function upgradeMetadata(metadata, status) {
157
161
  return next;
158
162
  }
159
163
  export class NodeConnector {
164
+ usageOutbox;
165
+ modelUsage = new NodeModelUsageService(() => this.usageOutbox, () => this.config?.nodeId);
160
166
  nodeGeneration = crypto.randomUUID().replace(/-/g, '');
161
167
  controlChannel;
162
168
  controlMessages;
@@ -417,6 +423,7 @@ export class NodeConnector {
417
423
  void this.enqueueGoalContinuation({
418
424
  sessionId: retry.sessionId,
419
425
  goal: pending.goal,
426
+ ...(pending.usageGrant === undefined ? {} : { usageGrant: pending.usageGrant }),
420
427
  secretEnvironment: pending.secretEnvironment,
421
428
  ...(pending.personalInstructions === undefined
422
429
  ? {}
@@ -442,6 +449,7 @@ export class NodeConnector {
442
449
  retry.timer.unref();
443
450
  },
444
451
  releaseExecution: (executionId) => {
452
+ this.modelUsage.release(executionId);
445
453
  this.goalExecutionProgress.delete(executionId);
446
454
  this.goalMcpServer.revoke(executionId);
447
455
  this.scheduledTaskMcpServer.revoke(executionId);
@@ -797,6 +805,7 @@ export class NodeConnector {
797
805
  ...(run?.initiatedByUserId === undefined
798
806
  ? {}
799
807
  : { initiatedByUserId: run.initiatedByUserId }),
808
+ ...(input.usageGrant === undefined ? {} : { usageGrant: input.usageGrant }),
800
809
  secretEnvironment: input.secretEnvironment,
801
810
  ...(input.personalInstructions === undefined
802
811
  ? {}
@@ -986,7 +995,7 @@ export class NodeConnector {
986
995
  emitSession: (session) => this.emitSessionWorkbenchEvent({
987
996
  session: this.sessionPresentationService.present(session)
988
997
  }),
989
- execute: (operation, data) => this.executeNodeOperation(operation, data),
998
+ execute: (operation, data) => this.modelUsage.withGrant(isPlainRecord(data) ? data.__usageGrant : undefined, () => this.executeNodeOperation(operation, data)),
990
999
  interceptedSessionId: (clientMessageId) => this.fakeRunner === undefined ? undefined : `fake:${clientMessageId}`
991
1000
  });
992
1001
  this.externalSessionResumeService = new ExternalSessionResumeService({
@@ -1044,7 +1053,7 @@ export class NodeConnector {
1044
1053
  goalAccessChanged: prepared.changed
1045
1054
  };
1046
1055
  },
1047
- goal: async ({ session, objective, initiatedByUserId, secretEnvironment, personalInstructions, marAgentModel, marAgentModels, marAgentCompact, marAgentRuntimeScopeId }) => {
1056
+ goal: async ({ session, objective, initiatedByUserId, secretEnvironment, personalInstructions, marAgentModel, marAgentModels, marAgentCompact, marAgentRuntimeScopeId, usageGrant }) => {
1048
1057
  this.invalidateGoalContinuation(session.id);
1049
1058
  const prepared = this.prepareGoalSession(session);
1050
1059
  const goalSession = prepared.session;
@@ -1064,6 +1073,7 @@ export class NodeConnector {
1064
1073
  objective: goal.objective,
1065
1074
  deliveryIntent: replacing ? 'REPLACE_CURRENT' : 'SEND',
1066
1075
  goal: goalRef,
1076
+ ...(usageGrant === undefined ? {} : { usageGrant }),
1067
1077
  ...(initiatedByUserId === undefined ? {} : { initiatedByUserId }),
1068
1078
  secretEnvironment,
1069
1079
  ...(personalInstructions === undefined ? {} : { personalInstructions }),
@@ -1283,6 +1293,7 @@ export class NodeConnector {
1283
1293
  onReconnectScheduled: () => this.metrics.reconnectScheduled(),
1284
1294
  onMessage: (raw) => this.handleMessage(raw),
1285
1295
  onClose: (code, reason) => {
1296
+ this.usageOutbox?.disconnected();
1286
1297
  this.directTransferService?.close();
1287
1298
  this.runtimeCredentials.disconnected();
1288
1299
  this.chatgptLogins.close();
@@ -1300,8 +1311,24 @@ export class NodeConnector {
1300
1311
  });
1301
1312
  this.controlMessages = new NodeControlMessageService({
1302
1313
  config: () => this.config,
1303
- register: (nodeId, credential) => this.connectionLifecycle.register(nodeId, credential),
1304
- connected: () => this.connectionLifecycle.connected(),
1314
+ register: async (nodeId, credential, payload) => {
1315
+ await this.connectionLifecycle.register(nodeId, credential);
1316
+ this.usageOutbox?.connected(payload);
1317
+ },
1318
+ usageAck: (payload) => this.usageOutbox?.handle(createEnvelope('usage.ack', payload)),
1319
+ bindUsage: (payload) => {
1320
+ if (!isPlainRecord(payload) ||
1321
+ typeof payload.runId !== 'string' ||
1322
+ typeof payload.sessionId !== 'string')
1323
+ return;
1324
+ const runner = this.runtime.getAgentSession(payload.sessionId)?.runner;
1325
+ if (runner)
1326
+ this.modelUsage.bind(payload.runId, payload.usageGrant, payload.sessionId, runner);
1327
+ },
1328
+ connected: (payload) => {
1329
+ this.usageOutbox?.connected(payload);
1330
+ this.connectionLifecycle.connected();
1331
+ },
1305
1332
  close: (code, reason) => this.controlChannel.close(code, reason),
1306
1333
  inspectWorkspace: (envelope) => this.workspaceCoordinator.inspect(envelope),
1307
1334
  handleNodeRequest: (envelope) => this.nodeRequests.handle(envelope),
@@ -1323,7 +1350,7 @@ export class NodeConnector {
1323
1350
  this.nodeRequests = new NodeRequestService({
1324
1351
  nodeId: () => this.config?.nodeId,
1325
1352
  databaseAvailable: () => this.database !== undefined,
1326
- execute: (operation, data) => this.executeNodeOperation(operation, data),
1353
+ execute: (operation, data) => this.modelUsage.withGrant(isPlainRecord(data) ? data.__usageGrant : undefined, () => this.executeNodeOperation(operation, data)),
1327
1354
  send: (type, payload, replyTo) => this.send(type, payload, replyTo)
1328
1355
  });
1329
1356
  this.workspaceCoordinator = new NodeWorkspaceCoordinator({
@@ -1336,6 +1363,13 @@ export class NodeConnector {
1336
1363
  send: (type, payload, replyTo) => this.send(type, payload, replyTo)
1337
1364
  });
1338
1365
  this.runEventBridge = new NodeRunEventBridge({
1366
+ bindModelUsage: (runId) => {
1367
+ const run = this.runtime.getRun(runId);
1368
+ const session = run ? this.runtime.getAgentSession(run.sessionId) : undefined;
1369
+ if (session)
1370
+ this.modelUsage.bindInherited(runId, session.id, session.runner);
1371
+ },
1372
+ modelUsage: (runId, payload) => this.modelUsage.record(runId, payload),
1339
1373
  runtime: this.runtime,
1340
1374
  state: this.runState,
1341
1375
  events: this.runEventService,
@@ -1414,6 +1448,7 @@ export class NodeConnector {
1414
1448
  marAgentModel: context.marAgentModel,
1415
1449
  marAgentModels: context.marAgentModels,
1416
1450
  marAgentCompact: context.marAgentCompact,
1451
+ ...(context.usageGrant === undefined ? {} : { usageGrant: context.usageGrant }),
1417
1452
  marAgentRuntimeScopeId: context.marAgentRuntimeScopeId
1418
1453
  }));
1419
1454
  return { runId: result.run.id };
@@ -1593,6 +1628,12 @@ export class NodeConnector {
1593
1628
  })
1594
1629
  });
1595
1630
  hostedService = new HostedConversationNodeService({
1631
+ modelUsage: (grant, usage) => {
1632
+ const runId = usage.executionId ?? usage.requestId;
1633
+ this.modelUsage.bind(runId, grant, usage.sessionId, 'mar-agent');
1634
+ this.modelUsage.record(runId, normalizeMarAgentUsage(usage));
1635
+ this.modelUsage.release(runId);
1636
+ },
1596
1637
  database: this.database,
1597
1638
  store,
1598
1639
  runtime,
@@ -1684,6 +1725,16 @@ export class NodeConnector {
1684
1725
  await this.upgradeCoordinator.loadResult(resolve(dirname(nodeDatabasePath(this.config, this.configPath)), 'upgrade-result.json'));
1685
1726
  await assertLocalSqlitePath(nodeDatabasePath(this.config, this.configPath));
1686
1727
  this.database ??= new NodeDatabase(nodeDatabasePath(this.config, this.configPath), () => this.config?.nodeId ?? 'unregistered');
1728
+ this.usageOutbox?.disconnected();
1729
+ this.usageOutbox = new ModelUsageOutbox({
1730
+ database: () => this.database?.raw,
1731
+ namespace: () => this.config?.nodeId
1732
+ ? JSON.stringify([this.config.serverUrl, this.config.nodeId])
1733
+ : undefined,
1734
+ send: (type, payload) => this.send(type, payload),
1735
+ diagnose: (code) => nodeLog('model.usage.diagnostic', { code })
1736
+ });
1737
+ this.usageOutbox.recoverInterrupted();
1687
1738
  this.database.closeRunningTerminalSessions();
1688
1739
  this.scheduledSessions.start();
1689
1740
  await this.initializeConversationHosting();
@@ -1872,6 +1923,7 @@ export class NodeConnector {
1872
1923
  ...(input.initiatedByUserId === undefined
1873
1924
  ? {}
1874
1925
  : { initiatedByUserId: input.initiatedByUserId }),
1926
+ ...(input.usageGrant === undefined ? {} : { usageGrant: input.usageGrant }),
1875
1927
  secretEnvironment: input.secretEnvironment,
1876
1928
  ...(input.personalInstructions === undefined
1877
1929
  ? {}
@@ -2095,6 +2147,9 @@ export class NodeConnector {
2095
2147
  await hostedConversationRuntime?.dispose().catch(() => undefined);
2096
2148
  await drains;
2097
2149
  await this.gitExecutionService?.close();
2150
+ this.usageOutbox?.disconnected();
2151
+ this.usageOutbox = undefined;
2152
+ this.modelUsage.clear();
2098
2153
  database?.close();
2099
2154
  }
2100
2155
  })();
package/dist/database.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { NODE_SCHEMA_MIGRATION_V17 } from './migrations/v017.js';
1
2
  import { randomUUID } from 'node:crypto';
2
3
  import { closeSync, existsSync, mkdirSync, openSync, rmSync } from 'node:fs';
3
4
  import { dirname } from 'node:path';
@@ -14,7 +15,8 @@ import { assertNodeNoForeignKeys, assertNodeRelations } from './database-relatio
14
15
  export const NODE_SCHEMA_MIGRATIONS = [
15
16
  NODE_SCHEMA_MIGRATION_V14,
16
17
  NODE_SCHEMA_MIGRATION_V15,
17
- NODE_SCHEMA_MIGRATION_V16
18
+ NODE_SCHEMA_MIGRATION_V16,
19
+ NODE_SCHEMA_MIGRATION_V17
18
20
  ];
19
21
  const LEGACY_NODE_SCHEMA_V12 = NODE_SCHEMA_VERSION_V13 - 1;
20
22
  const OBSOLETE_NODE_SCHEMA_V16 = 16;
@@ -0,0 +1,46 @@
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
+ const sql = `CREATE TABLE model_usage_outbox (
7
+ namespace TEXT NOT NULL,
8
+ record_id TEXT NOT NULL,
9
+ revision INTEGER NOT NULL,
10
+ payload TEXT NOT NULL,
11
+ bytes INTEGER NOT NULL,
12
+ updated_at INTEGER NOT NULL,
13
+ ended_at INTEGER,
14
+ PRIMARY KEY(namespace, record_id)
15
+ ) STRICT;
16
+ CREATE INDEX model_usage_outbox_updated ON model_usage_outbox(updated_at);`;
17
+ export const NODE_SCHEMA_MIGRATION_V17 = {
18
+ version: 17,
19
+ sql,
20
+ preflight: (database) => NODE_SCHEMA_MIGRATION_V16.assert(database),
21
+ assert(database) {
22
+ const reference = new DatabaseSync(':memory:');
23
+ try {
24
+ reference.exec(NODE_SCHEMA_V13);
25
+ reference.exec(NODE_SCHEMA_MIGRATION_V14.sql);
26
+ reference.exec(NODE_SCHEMA_MIGRATION_V15.sql);
27
+ reference.exec(NODE_SCHEMA_MIGRATION_V16.sql);
28
+ reference.exec(sql);
29
+ const schema = (db) => db
30
+ .prepare("SELECT type,name,sql FROM sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%' ORDER BY type,name")
31
+ .all()
32
+ .map((row) => [
33
+ row.type,
34
+ row.name,
35
+ row.name === 'node_schema_migrations'
36
+ ? String(row.sql).replace(/\s+/g, '')
37
+ : String(row.sql).replace(/\s+/g, ' ').trim()
38
+ ]);
39
+ if (JSON.stringify(schema(reference)) !== JSON.stringify(schema(database)))
40
+ throw new Error('NODE_DATABASE_SCHEMA_INVALID:model_usage');
41
+ }
42
+ finally {
43
+ reference.close();
44
+ }
45
+ }
46
+ };
@@ -7,6 +7,7 @@ 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 { extractClaudeToolResults } from './runner/claude/tool-normalizer.js';
10
11
  const DISCOVERY_READ_BYTES = 32 * 1024;
11
12
  const TRANSCRIPT_HEADER_READ_BYTES = 32 * 1024;
12
13
  const TRANSCRIPT_READ_MAX_BYTES = 16 * 1024 * 1024;
@@ -740,21 +741,11 @@ function toolUsesOf(record, createdAt) {
740
741
  });
741
742
  }
742
743
  function toolResultsOf(record, createdAt) {
743
- return contentBlocks(record).flatMap((block) => {
744
- if (block['type'] !== 'tool_result' || typeof block['tool_use_id'] !== 'string')
745
- return [];
746
- const output = compactValue(block['content'], 10_000);
747
- return [
748
- {
749
- kind: 'tool_result',
750
- toolUseId: block['tool_use_id'],
751
- output: output.text,
752
- failed: block['is_error'] === true,
753
- truncated: output.truncated,
754
- createdAt
755
- }
756
- ];
757
- });
744
+ return extractClaudeToolResults(record).map((result) => ({
745
+ ...result,
746
+ kind: 'tool_result',
747
+ createdAt
748
+ }));
758
749
  }
759
750
  /**
760
751
  * Codex persists custom and standard function tools as paired `response_item`
@@ -973,6 +964,7 @@ function withToolResult(call, result) {
973
964
  ...call,
974
965
  outputSummary: result.output,
975
966
  outputTruncated: result.truncated,
967
+ ...(result.claudeResult === undefined ? {} : { claudeResult: result.claudeResult }),
976
968
  ...(imageAttachments === undefined ? {} : { imageAttachments }),
977
969
  failed: result.failed
978
970
  };
@@ -126,6 +126,10 @@ export class AbstractRunner {
126
126
  void session;
127
127
  return undefined;
128
128
  }
129
+ async readToolResultResource(session, input) {
130
+ void [session, input];
131
+ throw new Error('TOOL_RESOURCE_UNSUPPORTED');
132
+ }
129
133
  acceptsTruncatedNativeHistory() {
130
134
  return true;
131
135
  }
@@ -1,8 +1,9 @@
1
+ import { ClaudeUsageNormalizer } from './tool-normalizer.js';
1
2
  import { createEnvelope } from '@myagentroam/protocol';
2
3
  import { ClaudeChannelGate } from '../../claude-channel.js';
3
4
  import { nodeExecutionEnvironment } from '../../node-execution-environment.js';
4
- import { boundedConversationItemId, claudePlanText, compactRunnerText, extractClaudeText, extractClaudeThinkingSummaries, extractClaudeToolResults, extractClaudeToolUses } from '../codex/conversation-parser.js';
5
- import { normalizeClaudeTool } from './tool-normalizer.js';
5
+ import { boundedConversationItemId, claudePlanText, compactRunnerText, extractClaudeText, extractClaudeThinkingSummaries, extractClaudeToolUses } from '../codex/conversation-parser.js';
6
+ import { normalizeClaudeTool, extractClaudeToolResults } from './tool-normalizer.js';
6
7
  import { claudeContentBlocks } from '../../util/node-operation-parsers.js';
7
8
  import { parseSecretEnvironment } from '../../util/secret-environment.js';
8
9
  import { safeErrorCode } from '../../util/safe-error.js';
@@ -134,6 +135,9 @@ export class ClaudeManagedRunController {
134
135
  let terminal = false;
135
136
  let handle;
136
137
  const toolKinds = new Map();
138
+ const toolInputs = new Map();
139
+ const knownSubagents = new Map();
140
+ const usage = new ClaudeUsageNormalizer(runId, (value) => this.host.emit(runId, 'run.usage', value));
137
141
  const finish = (status, eventType, eventPayload, failure, fallbackCode = 'CLAUDE_RUN_FAILED') => {
138
142
  if (terminal)
139
143
  return;
@@ -157,6 +161,7 @@ export class ClaudeManagedRunController {
157
161
  payload: { phase: 'COMPLETED', summary: null },
158
162
  merge: true
159
163
  });
164
+ usage.finish(status === 'SUCCEEDED' ? 'succeeded' : status === 'FAILED' ? 'failed' : 'cancelled');
160
165
  this.host.emit(runId, eventType, projected ?? eventPayload, status);
161
166
  this.runner.execution.runs.delete(runId);
162
167
  handle?.cancel();
@@ -233,6 +238,7 @@ export class ClaudeManagedRunController {
233
238
  onMessage: (message) => {
234
239
  if (terminal)
235
240
  return;
241
+ usage.observe(message);
236
242
  const receiptAt = generationTiming.timestamp();
237
243
  const record = message;
238
244
  const nativeSessionId = typeof record.session_id === 'string' ? record.session_id : undefined;
@@ -373,7 +379,8 @@ export class ClaudeManagedRunController {
373
379
  continue;
374
380
  }
375
381
  const itemId = boundedConversationItemId('claude-tool', tool.id);
376
- const normalized = normalizeClaudeTool(tool.name, tool.input, null, false, tool.inputTruncated);
382
+ toolInputs.set(tool.id, tool);
383
+ const normalized = normalizeClaudeTool(tool.name, tool.input, null, false, tool.inputTruncated, knownSubagents);
377
384
  const kind = normalized.kind;
378
385
  toolKinds.set(tool.id, kind);
379
386
  this.host.emitItem(runId, {
@@ -436,6 +443,10 @@ export class ClaudeManagedRunController {
436
443
  const kind = toolKinds.get(result.toolUseId) ?? 'tool_call';
437
444
  if (kind === 'user_input_request')
438
445
  continue;
446
+ const tool = toolInputs.get(result.toolUseId);
447
+ if (tool === undefined)
448
+ continue;
449
+ const normalized = normalizeClaudeTool(tool.name, tool.input, result.output, result.failed, tool.inputTruncated || result.truncated, knownSubagents, result.claudeResult);
439
450
  this.host.emitItem(runId, {
440
451
  itemId: boundedConversationItemId('claude-tool', result.toolUseId),
441
452
  kind,
@@ -450,6 +461,7 @@ export class ClaudeManagedRunController {
450
461
  : kind === 'file_change'
451
462
  ? {}
452
463
  : {
464
+ ...normalized.payload,
453
465
  outputSummary: result.output,
454
466
  progressLabel: null,
455
467
  errorCode: result.failed ? 'TOOL_EXECUTION_FAILED' : null,
@@ -1,10 +1,90 @@
1
- import { compactRunnerText, compactRunnerValue } from '../../util/node-operation-parsers.js';
2
- export function normalizeClaudeTool(toolName, input, output, failed, truncated) {
1
+ import { claudeContentBlocks, compactRunnerText, compactRunnerValue } from '../../util/node-operation-parsers.js';
2
+ import { RunnerUsageTracker, usageToken, usageTokenSum } from '../../service/runner-usage-tracker.js';
3
+ export class ClaudeUsageNormalizer {
4
+ tracker;
5
+ messages = new Map();
6
+ constructor(identity, publish) {
7
+ this.tracker = new RunnerUsageTracker(identity, publish);
8
+ }
9
+ observe(value) {
10
+ const envelope = record(value);
11
+ if (!envelope)
12
+ return;
13
+ const parent = typeof envelope.parent_tool_use_id === 'string' ? envelope.parent_tool_use_id : '';
14
+ const event = envelope.type === 'stream_event' ? record(envelope.event) : undefined;
15
+ const message = record(envelope.type === 'assistant' ? envelope.message : event?.message);
16
+ const previous = this.messages.get(parent);
17
+ const id = typeof message?.id === 'string' ? message.id : previous?.id;
18
+ if (!id || message?.model === '<synthetic>')
19
+ return;
20
+ const usage = record(message?.usage ?? event?.usage);
21
+ let values = {
22
+ ...(previous?.id === id ? previous.values : {}),
23
+ purpose: 'conversation',
24
+ agentRole: parent ? 'subagent' : 'root',
25
+ ...(typeof message?.model === 'string' ? { actualModel: message.model } : {})
26
+ };
27
+ if (usage) {
28
+ const input = usageToken(usage.input_tokens), read = usageToken(usage.cache_read_input_tokens), write = usageToken(usage.cache_creation_input_tokens), output = usageToken(usage.output_tokens);
29
+ values = {
30
+ ...values,
31
+ ...(input === undefined
32
+ ? {}
33
+ : { inputTokens: usageTokenSum(input, read ?? '0', write ?? '0') }),
34
+ ...(output === undefined ? {} : { outputTokens: output }),
35
+ ...(read === undefined ? {} : { cacheReadTokens: read }),
36
+ ...(write === undefined ? {} : { cacheWriteTokens: write })
37
+ };
38
+ }
39
+ this.messages.set(parent, { id, values });
40
+ if (usage || event?.type === 'message_stop')
41
+ this.tracker.snapshot(id, values, event?.type === 'message_stop' ||
42
+ typeof record(event?.delta)?.stop_reason === 'string' ||
43
+ typeof message?.stop_reason === 'string', 'partial');
44
+ }
45
+ finish(state) {
46
+ this.tracker.finish(state);
47
+ }
48
+ }
49
+ export function restoreClaudeSubagentTitles(calls) {
50
+ const titles = new Map();
51
+ for (const call of calls)
52
+ claudeSubagentProjection(call.toolName, call.input, call.claudeResult ?? call.outputSummary, titles);
53
+ return titles;
54
+ }
55
+ export function extractClaudeToolResults(message) {
56
+ // This ID describes the owning subagent, not the tool call producing the result.
57
+ if (message.parent_tool_use_id !== undefined && message.parent_tool_use_id !== null)
58
+ return [];
59
+ const blocks = claudeContentBlocks(message).filter((block) => block.type === 'tool_result' && typeof block.tool_use_id === 'string');
60
+ return blocks.map((block) => {
61
+ const output = compactRunnerValue(block.content, 10_000);
62
+ // A message-level structured result cannot be assigned to multiple calls.
63
+ const structured = blocks.length === 1 ? structuredRecord(message.tool_use_result) : undefined;
64
+ const fields = structured ?? structuredRecord(block.content);
65
+ const agentId = boundedId(fields?.agentId);
66
+ const taskId = boundedId(fields?.task_id);
67
+ const taskType = boundedId(fields?.task_type);
68
+ const claudeResult = {
69
+ ...(agentId === undefined ? {} : { agentId }),
70
+ ...(taskId === undefined ? {} : { task_id: taskId }),
71
+ ...(taskType === undefined ? {} : { task_type: taskType })
72
+ };
73
+ return {
74
+ toolUseId: block.tool_use_id,
75
+ output: output.text,
76
+ failed: block.is_error === true,
77
+ truncated: output.truncated,
78
+ ...(Object.keys(claudeResult).length === 0 ? {} : { claudeResult })
79
+ };
80
+ });
81
+ }
82
+ export function normalizeClaudeTool(toolName, input, output, failed, truncated, knownSubagents = new Map(), resultMetadata) {
3
83
  const values = record(input);
4
84
  const normalizedToolName = compactRunnerText(toolName, 128) || 'unknown';
5
85
  const normalizedOutput = output === null ? null : compactRunnerText(output, 10_000);
6
86
  const outputTruncated = truncated || (output?.length ?? 0) > 10_000;
7
- const subagent = claudeSubagentProjection(toolName, input, output);
87
+ const subagent = claudeSubagentProjection(toolName, input, resultMetadata ?? output, knownSubagents);
8
88
  if (isCommandTool(toolName))
9
89
  return {
10
90
  kind: 'command_execution',
@@ -70,7 +150,7 @@ function record(value) {
70
150
  ? value
71
151
  : undefined;
72
152
  }
73
- function claudeSubagentProjection(toolName, input, output) {
153
+ function claudeSubagentProjection(toolName, input, output, knownSubagents) {
74
154
  const normalizedName = toolName.toLowerCase();
75
155
  const operation = normalizedName === 'agent'
76
156
  ? 'START'
@@ -90,12 +170,26 @@ function claudeSubagentProjection(toolName, input, output) {
90
170
  : normalizedName === 'sendmessage'
91
171
  ? boundedId(inputData?.to)
92
172
  : (boundedId(inputData?.task_id) ?? boundedId(outputData?.task_id));
173
+ if (normalizedName === 'taskoutput' || normalizedName === 'taskstop') {
174
+ const taskType = outputData?.task_type;
175
+ if (typeof taskType === 'string') {
176
+ if (taskType !== 'local_agent' && taskType !== 'remote_agent')
177
+ return undefined;
178
+ }
179
+ else if (subagentId === undefined || !knownSubagents.has(subagentId))
180
+ return undefined;
181
+ }
93
182
  const title = normalizedName === 'agent' && typeof inputData?.description === 'string'
94
183
  ? compactRunnerText(inputData.description, 500)
95
- : '';
184
+ : subagentId === undefined
185
+ ? ''
186
+ : (knownSubagents.get(subagentId) ?? '');
187
+ if (subagentId !== undefined && normalizedName === 'agent')
188
+ knownSubagents.set(subagentId, title);
96
189
  return {
97
190
  subagentOperation: operation,
98
- subagentId: subagentId ?? null,
191
+ subagentId: subagentId === '*' ? null : (subagentId ?? null),
192
+ ...(operation === 'STATUS' ? { subagentWait: inputData?.block === true } : {}),
99
193
  ...(title.length === 0 ? {} : { title })
100
194
  };
101
195
  }
@@ -1,3 +1,41 @@
1
+ import { RunnerUsageTracker, usageToken } from '../../service/runner-usage-tracker.js';
2
+ export class CodexUsageNormalizer {
3
+ requestedModel;
4
+ tracker;
5
+ constructor(identity, publish, requestedModel) {
6
+ this.requestedModel = requestedModel;
7
+ this.tracker = new RunnerUsageTracker(identity, publish);
8
+ }
9
+ observe(method, params, compact) {
10
+ // TokenCount.last can replay context/compaction snapshots without a request identity.
11
+ // Only raw response completions are independently additive request facts.
12
+ if (method !== 'rawResponse/completed' || typeof params.responseId !== 'string')
13
+ return;
14
+ const usage = isPlainRecord(params.usage) ? params.usage : {};
15
+ const inputTokens = usageToken(usage.inputTokens ?? usage.input_tokens);
16
+ const outputTokens = usageToken(usage.outputTokens ?? usage.output_tokens);
17
+ const cacheReadTokens = usageToken(usage.cachedInputTokens ??
18
+ (isPlainRecord(usage.input_tokens_details)
19
+ ? usage.input_tokens_details.cached_tokens
20
+ : undefined));
21
+ const reasoningTokens = usageToken(usage.reasoningOutputTokens ??
22
+ (isPlainRecord(usage.output_tokens_details)
23
+ ? usage.output_tokens_details.reasoning_tokens
24
+ : undefined));
25
+ const values = {
26
+ purpose: compact ? 'compact' : 'conversation',
27
+ agentRole: 'root',
28
+ ...(this.requestedModel ? { requestedModel: this.requestedModel } : {}),
29
+ ...(typeof params.model === 'string' ? { actualModel: params.model } : {}),
30
+ responseId: params.responseId,
31
+ ...(inputTokens === undefined ? {} : { inputTokens }),
32
+ ...(outputTokens === undefined ? {} : { outputTokens }),
33
+ ...(cacheReadTokens === undefined ? {} : { cacheReadTokens }),
34
+ ...(reasoningTokens === undefined ? {} : { reasoningTokens })
35
+ };
36
+ this.tracker.snapshot(params.responseId, values, true, 'partial');
37
+ }
38
+ }
1
39
  import { basename, extname } from 'node:path';
2
40
  import { runnerErrorProjection } from '../../util/runner-error.js';
3
41
  import { conversationImageAttachmentSchema } from '@myagentroam/protocol';
@@ -503,7 +541,7 @@ export function codexPlanText(value) {
503
541
  * text with missing or unclosed tags remains a regular assistant message.
504
542
  */
505
543
  export function codexOfficialItem(session, nativeTurnId, value, turnSequence, itemSequence, startedAt, completedAt) {
506
- if (!isPlainRecord(value) || typeof value.type !== 'string')
544
+ if (!isPlainRecord(value) || typeof value.type !== 'string' || value.type === 'subAgentActivity')
507
545
  return undefined;
508
546
  const id = (value.type === 'custom_tool_call' ||
509
547
  value.type === 'custom_tool_call_output' ||
@@ -686,11 +724,11 @@ export function codexOfficialItem(session, nativeTurnId, value, turnSequence, it
686
724
  : {
687
725
  ...base,
688
726
  ...special,
689
- status: value.type === 'collabAgentToolCall' ? codexItemStatus(value.status, true) : base.status
727
+ status: base.status
690
728
  };
691
729
  }
692
730
  export function codexLiveConversationItem(value, completed, imageAttachments = []) {
693
- if (typeof value.type !== 'string')
731
+ if (typeof value.type !== 'string' || value.type === 'subAgentActivity')
694
732
  return undefined;
695
733
  const nativeId = typeof value.call_id === 'string'
696
734
  ? value.call_id
@@ -936,33 +974,6 @@ function codexSpecialItem(value) {
936
974
  inputSummary: typeof value.path === 'string' ? compactRunnerText(value.path, 4_096) : null
937
975
  }).payload
938
976
  };
939
- if (value.type === 'subAgentActivity') {
940
- const activity = typeof value.kind === 'string' ? value.kind : '';
941
- const operation = activity === 'started'
942
- ? 'START'
943
- : activity === 'interrupted'
944
- ? 'CANCEL'
945
- : activity === 'interacted' || activity === 'completed'
946
- ? 'STATUS'
947
- : undefined;
948
- if (operation === undefined)
949
- return undefined;
950
- const agentId = typeof value.agentThreadId === 'string' ? compactRunnerText(value.agentThreadId, 128) : '';
951
- const agentPath = typeof value.agentPath === 'string' ? compactRunnerText(value.agentPath, 500) : '';
952
- return {
953
- kind: 'tool_call',
954
- payload: {
955
- ...normalizeCodexTool({
956
- name: 'subagent_activity',
957
- namespace: 'codex',
958
- outputSummary: activity
959
- }).payload,
960
- ...(agentPath ? { title: agentPath } : {}),
961
- subagentOperation: operation,
962
- subagentId: agentId || null
963
- }
964
- };
965
- }
966
977
  if (value.type === 'collabAgentToolCall') {
967
978
  const names = {
968
979
  spawnAgent: 'spawn_agent',
@@ -5,7 +5,7 @@ import { safeErrorCode } from '../../util/safe-error.js';
5
5
  import { runnerErrorProjection } from '../../util/runner-error.js';
6
6
  import { ResponseLatencyTracker } from '../../service/response-latency-tracker.js';
7
7
  import { GenerationTimingTracker } from '../../service/generation-timing-tracker.js';
8
- import { boundedConversationItemId, codexImageAttachments, codexLiveConversationItem, codexUserInputQuestions, compactRunnerText, isPlainRecord } from './conversation-parser.js';
8
+ import { boundedConversationItemId, codexImageAttachments, codexLiveConversationItem, CodexUsageNormalizer, codexUserInputQuestions, compactRunnerText, isPlainRecord } from './conversation-parser.js';
9
9
  export function resolveCodexNotificationRun(runs, input) {
10
10
  const entries = [...runs.entries()];
11
11
  if (input.turnId !== undefined) {
@@ -28,6 +28,7 @@ export function resolveCodexNotificationRun(runs, input) {
28
28
  export class CodexManagedRunController {
29
29
  runner;
30
30
  host;
31
+ usage = new Map();
31
32
  constructor(runner, host) {
32
33
  this.runner = runner;
33
34
  this.host = host;
@@ -113,6 +114,7 @@ export class CodexManagedRunController {
113
114
  catch (error) {
114
115
  this.emitError(runId, error, 'CODEX_COMPACT_FAILED');
115
116
  this.host.emit(runId, 'run.failed', runnerErrorProjection(error, 'CODEX_COMPACT_FAILED'), 'FAILED');
117
+ this.usage.delete(runId);
116
118
  this.runner.execution.runs.delete(runId);
117
119
  this.runner.releaseEnvironment(runId);
118
120
  this.host.removeActive(runId);
@@ -138,6 +140,12 @@ export class CodexManagedRunController {
138
140
  if (run === undefined)
139
141
  return;
140
142
  const [runId, activeRun] = run;
143
+ let usage = this.usage.get(runId);
144
+ if (!usage) {
145
+ usage = new CodexUsageNormalizer(runId, (value) => this.host.emit(runId, 'run.usage', value), this.host.session(activeRun.sessionId)?.model ?? undefined);
146
+ this.usage.set(runId, usage);
147
+ }
148
+ usage.observe(notification.method, params, activeRun.generationCompacting === true);
141
149
  const receiptAt = activeRun.generationTiming?.timestamp();
142
150
  if (notification.method === 'turn/started' && turnId !== undefined) {
143
151
  const active = this.runner.execution.runs.get(runId);
@@ -332,6 +340,7 @@ export class CodexManagedRunController {
332
340
  this.host.session(active.sessionId)?.titleSource !== 'AUTO'
333
341
  ? undefined
334
342
  : this.host.syncTitle(active.sessionId, active.threadId, active.cwd);
343
+ this.usage.delete(runId);
335
344
  this.runner.execution.runs.delete(runId);
336
345
  if (titleRefresh === undefined)
337
346
  this.runner.releaseEnvironment(runId);
@@ -624,6 +633,7 @@ export class CodexManagedRunController {
624
633
  break;
625
634
  }
626
635
  catch (error) {
636
+ this.usage.delete(runId);
627
637
  this.runner.execution.runs.delete(runId);
628
638
  if (attempt === 0 && (await this.runner.recoverMissingRollout(runId, error)))
629
639
  continue;
@@ -663,6 +673,7 @@ export class CodexManagedRunController {
663
673
  }
664
674
  this.emitError(runId, error, 'CODEX_RUN_FAILED');
665
675
  this.host.emit(runId, 'run.failed', runnerErrorProjection(error, 'CODEX_RUN_FAILED'), 'FAILED');
676
+ this.usage.delete(runId);
666
677
  this.runner.execution.runs.delete(runId);
667
678
  this.runner.releaseEnvironment(runId);
668
679
  this.host.removeActive(runId);