@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
@@ -0,0 +1,167 @@
1
+ import { MODEL_USAGE_BATCH_BYTES, MODEL_USAGE_BATCH_LIMIT, MODEL_USAGE_OUTBOX_RETENTION_MS, modelUsageAckSchema, modelUsageGrantSchema, modelUsageRecordSchema } from '@myagentroam/protocol';
2
+ const MAX_BYTES = 32 * 1024 * 1024;
3
+ /** Durable, bounded, at-least-once delivery independent of browser subscriptions. */
4
+ export class ModelUsageOutbox {
5
+ options;
6
+ enabled = false;
7
+ timer;
8
+ inFlight;
9
+ attempts = 0;
10
+ now;
11
+ constructor(options) {
12
+ this.options = options;
13
+ this.now = options.now ?? Date.now;
14
+ }
15
+ connected(payload) {
16
+ this.disconnected();
17
+ const capability = payload?.modelUsage;
18
+ this.enabled = capability?.apiVersion === 1;
19
+ if (this.enabled) {
20
+ this.recover();
21
+ this.schedule(0);
22
+ }
23
+ }
24
+ disconnected() {
25
+ this.enabled = false;
26
+ if (this.timer)
27
+ clearTimeout(this.timer);
28
+ this.timer = undefined;
29
+ this.inFlight = undefined;
30
+ this.attempts = 0;
31
+ }
32
+ record(grant, value) {
33
+ try {
34
+ const database = this.options.database();
35
+ const namespace = this.options.namespace();
36
+ if (!database || !namespace)
37
+ return;
38
+ const record = modelUsageRecordSchema.parse(value);
39
+ const parsedGrant = modelUsageGrantSchema.parse(grant);
40
+ const payload = JSON.stringify({ grant: parsedGrant, record });
41
+ const bytes = Buffer.byteLength(payload);
42
+ this.expire(database);
43
+ const current = database
44
+ .prepare('SELECT revision,bytes FROM model_usage_outbox WHERE namespace=? AND record_id=?')
45
+ .get(namespace, record.recordId);
46
+ if (current && Number(current.revision) >= record.revision)
47
+ return;
48
+ const total = Number(database.prepare('SELECT COALESCE(SUM(bytes),0) AS bytes FROM model_usage_outbox').get()
49
+ ?.bytes ?? 0);
50
+ if (bytes > MODEL_USAGE_BATCH_BYTES / 2 ||
51
+ total - Number(current?.bytes ?? 0) + bytes > MAX_BYTES) {
52
+ this.options.diagnose('USAGE_OUTBOX_FULL');
53
+ return;
54
+ }
55
+ database
56
+ .prepare(`INSERT INTO model_usage_outbox(namespace,record_id,revision,payload,bytes,updated_at,ended_at) VALUES (?,?,?,?,?,?,?)
57
+ ON CONFLICT(namespace,record_id) DO UPDATE SET revision=excluded.revision,payload=excluded.payload,bytes=excluded.bytes,updated_at=excluded.updated_at,ended_at=excluded.ended_at`)
58
+ .run(namespace, record.recordId, record.revision, payload, bytes, this.now(), record.endedAt ?? null);
59
+ if (this.enabled && !this.inFlight && !this.timer)
60
+ this.schedule(250);
61
+ }
62
+ catch {
63
+ this.options.diagnose('USAGE_OUTBOX_WRITE_FAILED');
64
+ }
65
+ }
66
+ handle(envelope) {
67
+ if (envelope.type !== 'usage.ack')
68
+ return false;
69
+ const parsed = modelUsageAckSchema.safeParse(envelope.payload);
70
+ const namespace = this.options.namespace();
71
+ const database = this.options.database();
72
+ if (!parsed.success || !this.inFlight || !namespace || !database)
73
+ return true;
74
+ try {
75
+ for (const result of parsed.data.results) {
76
+ if (this.inFlight.get(result.recordId) !== result.revision)
77
+ continue;
78
+ database
79
+ .prepare('DELETE FROM model_usage_outbox WHERE namespace=? AND record_id=? AND revision=?')
80
+ .run(namespace, result.recordId, result.revision);
81
+ if (result.status === 'rejected')
82
+ this.options.diagnose(result.code ?? 'USAGE_REJECTED');
83
+ }
84
+ this.inFlight = undefined;
85
+ this.attempts = 0;
86
+ this.schedule(250);
87
+ }
88
+ catch {
89
+ this.options.diagnose('USAGE_OUTBOX_ACK_FAILED');
90
+ }
91
+ return true;
92
+ }
93
+ schedule(delay) {
94
+ if (this.timer)
95
+ clearTimeout(this.timer);
96
+ this.timer = setTimeout(() => {
97
+ this.timer = undefined;
98
+ this.flush();
99
+ }, delay);
100
+ this.timer.unref();
101
+ }
102
+ flush() {
103
+ const database = this.options.database();
104
+ const namespace = this.options.namespace();
105
+ if (!this.enabled || !database || !namespace)
106
+ return;
107
+ try {
108
+ this.expire(database);
109
+ const rows = database
110
+ .prepare('SELECT payload FROM model_usage_outbox WHERE namespace=? AND ended_at IS NOT NULL ORDER BY updated_at,record_id LIMIT ?')
111
+ .all(namespace, MODEL_USAGE_BATCH_LIMIT);
112
+ const entries = [];
113
+ let bytes = 100;
114
+ for (const row of rows) {
115
+ bytes += Buffer.byteLength(String(row.payload)) + 1;
116
+ if (bytes > MODEL_USAGE_BATCH_BYTES)
117
+ break;
118
+ entries.push(JSON.parse(String(row.payload)));
119
+ }
120
+ if (!entries.length) {
121
+ this.inFlight = undefined;
122
+ return;
123
+ }
124
+ this.inFlight = new Map(entries.map(({ record }) => [record.recordId, record.revision]));
125
+ this.options.send('usage.batch', { apiVersion: 1, entries });
126
+ this.schedule(Math.min(60_000, 1000 * 2 ** Math.min(this.attempts++, 6)));
127
+ }
128
+ catch {
129
+ this.options.diagnose('USAGE_OUTBOX_SEND_FAILED');
130
+ this.schedule(30_000);
131
+ }
132
+ }
133
+ recover() {
134
+ const database = this.options.database();
135
+ const namespace = this.options.namespace();
136
+ if (!database || !namespace)
137
+ return;
138
+ // Reconnect must not finalize requests that are still running.
139
+ this.expire(database);
140
+ }
141
+ recoverInterrupted() {
142
+ const database = this.options.database();
143
+ const namespace = this.options.namespace();
144
+ if (!database || !namespace)
145
+ return;
146
+ this.expire(database);
147
+ const rows = database
148
+ .prepare('SELECT payload FROM model_usage_outbox WHERE namespace=? AND ended_at IS NULL')
149
+ .all(namespace);
150
+ for (const row of rows) {
151
+ const entry = JSON.parse(String(row.payload));
152
+ this.record(entry.grant, {
153
+ ...entry.record,
154
+ revision: entry.record.revision + 1,
155
+ state: 'interrupted',
156
+ endedAt: Math.max(entry.record.startedAt, this.now())
157
+ });
158
+ }
159
+ }
160
+ expire(database) {
161
+ const result = database
162
+ .prepare('DELETE FROM model_usage_outbox WHERE updated_at<?')
163
+ .run(this.now() - MODEL_USAGE_OUTBOX_RETENTION_MS);
164
+ if (result.changes)
165
+ this.options.diagnose('USAGE_OUTBOX_EXPIRED');
166
+ }
167
+ }
@@ -0,0 +1,61 @@
1
+ import { modelUsageGrantSchema, modelUsageGrantClaimsSchema, modelUsageRecordSchema } from '@myagentroam/protocol';
2
+ import { nodeLog } from '../operational.js';
3
+ import { AsyncLocalStorage } from 'node:async_hooks';
4
+ /** Execution-private attribution never enters Run projections or browser Watch events. */
5
+ export class NodeModelUsageService {
6
+ outbox;
7
+ nodeId;
8
+ scope = new AsyncLocalStorage();
9
+ bindings = new Map();
10
+ constructor(outbox, nodeId) {
11
+ this.outbox = outbox;
12
+ this.nodeId = nodeId;
13
+ }
14
+ withGrant(grant, action) {
15
+ return this.scope.run(grant, action);
16
+ }
17
+ bindInherited(runId, sessionId, runner) {
18
+ if (!this.bindings.has(runId))
19
+ this.bind(runId, this.scope.getStore(), sessionId, runner);
20
+ }
21
+ bind(runId, grant, sessionId, runner) {
22
+ this.bindings.delete(runId);
23
+ if (grant === undefined)
24
+ return;
25
+ try {
26
+ const parsedGrant = modelUsageGrantSchema.parse(grant);
27
+ const claims = modelUsageGrantClaimsSchema.parse(JSON.parse(Buffer.from(parsedGrant.token.split('.')[0], 'base64url').toString()));
28
+ if (claims.nodeId !== this.nodeId() || claims.scopeId !== parsedGrant.scopeId)
29
+ return;
30
+ this.bindings.set(runId, { grant: parsedGrant, scopeId: claims.scopeId, sessionId, runner });
31
+ }
32
+ catch {
33
+ nodeLog('model.usage.invalid', { code: 'USAGE_GRANT_INVALID' });
34
+ }
35
+ }
36
+ release(runId) {
37
+ this.bindings.delete(runId);
38
+ }
39
+ clear() {
40
+ this.bindings.clear();
41
+ }
42
+ record(runId, value) {
43
+ const binding = this.bindings.get(runId);
44
+ if (!binding || !value || typeof value !== 'object')
45
+ return;
46
+ const parsed = modelUsageRecordSchema.safeParse({
47
+ ...value,
48
+ scopeId: binding.scopeId,
49
+ runner: binding.runner,
50
+ runId,
51
+ sessionId: typeof value.sessionId === 'string'
52
+ ? value.sessionId
53
+ : binding.sessionId
54
+ });
55
+ if (!parsed.success) {
56
+ nodeLog('model.usage.invalid', { code: 'USAGE_RECORD_INVALID' });
57
+ return;
58
+ }
59
+ this.outbox()?.record(binding.grant, parsed.data);
60
+ }
61
+ }
@@ -22,7 +22,7 @@ export class NodeControlMessageService {
22
22
  return;
23
23
  }
24
24
  if (envelope.type === 'authenticated') {
25
- this.options.connected();
25
+ this.options.connected(envelope.payload);
26
26
  return;
27
27
  }
28
28
  if (envelope.type === 'workspace.inspect') {
@@ -45,6 +45,10 @@ export class NodeControlMessageService {
45
45
  if (envelope.type.startsWith('terminal.direct.') &&
46
46
  this.options.handleTerminalDirect?.(envelope) === true)
47
47
  return;
48
+ if (envelope.type === 'usage.ack') {
49
+ this.options.usageAck?.(envelope.payload);
50
+ return;
51
+ }
48
52
  if (envelope.type === 'ack')
49
53
  return;
50
54
  if (envelope.type === 'runs.reconcile') {
@@ -58,6 +62,7 @@ export class NodeControlMessageService {
58
62
  if (envelope.type === 'run.start') {
59
63
  this.options.send('ack', { type: envelope.type }, envelope.id);
60
64
  try {
65
+ this.options.bindUsage?.(envelope.payload);
61
66
  this.options.runners.startManagedRun(envelope);
62
67
  }
63
68
  catch (error) {
@@ -90,7 +95,7 @@ export class NodeControlMessageService {
90
95
  this.options.close(4002, 'MESSAGE_INVALID');
91
96
  return;
92
97
  }
93
- void this.options.register(payload.nodeId, payload.credential);
98
+ void this.options.register(payload.nodeId, payload.credential, envelope.payload);
94
99
  }
95
100
  decideApproval(envelope) {
96
101
  const payload = envelope.payload;
@@ -19,12 +19,14 @@ export class NodeRequestService {
19
19
  }
20
20
  const startedAt = performance.now();
21
21
  try {
22
+ const usageGrant = payload.userAuthorization.usageGrant;
22
23
  const data = payload.data !== null && typeof payload.data === 'object'
23
24
  ? {
24
25
  ...payload.data,
25
26
  __workspaceWatchSessionHash: payload.userAuthorization
26
27
  .sessionHash,
27
- __requestUserId: payload.userAuthorization.userId
28
+ __requestUserId: payload.userAuthorization.userId,
29
+ ...(usageGrant === undefined ? {} : { __usageGrant: usageGrant })
28
30
  }
29
31
  : payload.data;
30
32
  const result = await this.options.execute(payload.operation, data);
@@ -113,6 +113,12 @@ export class NodeRunEventBridge {
113
113
  this.emitRun(input.runId, 'approval.request', input);
114
114
  }
115
115
  emitRun(runId, eventType, payload, status) {
116
+ if (eventType === 'run.started')
117
+ this.options.bindModelUsage?.(runId);
118
+ if (eventType === 'run.usage') {
119
+ this.options.modelUsage?.(runId, payload);
120
+ return;
121
+ }
116
122
  this.options.events.emit(runId, eventType, payload, status);
117
123
  }
118
124
  emitQueue(workspaceId, sessionId) {
@@ -0,0 +1,62 @@
1
+ import { createHash } from 'node:crypto';
2
+ export function usageToken(value) {
3
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0
4
+ ? String(value)
5
+ : undefined;
6
+ }
7
+ export function usageTokenSum(...values) {
8
+ return values.some((value) => value === undefined)
9
+ ? undefined
10
+ : values.reduce((sum, value) => sum + BigInt(value), 0n).toString();
11
+ }
12
+ /** Neutral revision tracking; native token semantics remain inside each Runner normalizer. */
13
+ export class RunnerUsageTracker {
14
+ identity;
15
+ publish;
16
+ now;
17
+ records = new Map();
18
+ constructor(identity, publish, now = Date.now) {
19
+ this.identity = identity;
20
+ this.publish = publish;
21
+ this.now = now;
22
+ }
23
+ snapshot(id, values, complete, coverage = 'request') {
24
+ const previous = this.records.get(id);
25
+ const timestamp = this.now();
26
+ const recordId = createHash('sha256')
27
+ .update(JSON.stringify([this.identity, id]))
28
+ .digest('hex');
29
+ const record = {
30
+ ...previous,
31
+ ...values,
32
+ schemaVersion: 1,
33
+ recordId,
34
+ requestId: recordId,
35
+ revision: (previous?.revision ?? 0) + 1,
36
+ startedAt: previous?.startedAt ?? timestamp,
37
+ state: complete ? 'succeeded' : (previous?.state ?? 'running'),
38
+ coverage,
39
+ ...(complete ? { endedAt: previous?.endedAt ?? timestamp } : {})
40
+ };
41
+ if (previous &&
42
+ JSON.stringify({ ...record, revision: 0 }) === JSON.stringify({ ...previous, revision: 0 }))
43
+ return;
44
+ this.records.set(id, record);
45
+ if (!previous || record.state !== 'running')
46
+ this.publish(record);
47
+ }
48
+ finish(state) {
49
+ for (const [id, previous] of this.records) {
50
+ if (previous.state !== 'running')
51
+ continue;
52
+ const record = {
53
+ ...previous,
54
+ revision: previous.revision + 1,
55
+ state,
56
+ endedAt: Math.max(previous.startedAt, this.now())
57
+ };
58
+ this.records.set(id, record);
59
+ this.publish(record);
60
+ }
61
+ }
62
+ }
@@ -1,3 +1,4 @@
1
+ import { modelUsageGrantSchema } from '@myagentroam/protocol';
1
2
  import { parseCommandInvocation } from '../runner-command-engine.js';
2
3
  import { isPlainRecord } from '../util/node-operation-parsers.js';
3
4
  import { parseSecretEnvironment } from '../util/secret-environment.js';
@@ -66,6 +67,9 @@ export class SessionCommandService {
66
67
  return this.options.goal({
67
68
  session,
68
69
  objective: invocation.argument,
70
+ ...(input.__usageGrant === undefined
71
+ ? {}
72
+ : { usageGrant: modelUsageGrantSchema.parse(input.__usageGrant) }),
69
73
  ...(Number.isSafeInteger(input.__requestUserId)
70
74
  ? { initiatedByUserId: input.__requestUserId }
71
75
  : {}),
@@ -1,3 +1,4 @@
1
+ import { modelUsageGrantSchema } from '@myagentroam/protocol';
1
2
  import { randomUUID } from 'node:crypto';
2
3
  import { isImageAttachment, isRunnerImageAttachment, parseComposerAttachmentIds, parseMessageAttachments, sessionTitleFromFirstMessage, withNonImageAttachmentPrompt } from '../util/node-operation-parsers.js';
3
4
  import { parseSecretEnvironment } from '../util/secret-environment.js';
@@ -41,6 +42,7 @@ export class SessionMessageService {
41
42
  marAgentModel: input.marAgentModel,
42
43
  marAgentModels: input.marAgentModels,
43
44
  marAgentCompact: input.marAgentCompact,
45
+ __usageGrant: input.usageGrant,
44
46
  marAgentRuntimeScopeId: input.marAgentRuntimeScopeId
45
47
  });
46
48
  }
@@ -58,6 +60,7 @@ export class SessionMessageService {
58
60
  marAgentModel: input.marAgentModel,
59
61
  marAgentModels: input.marAgentModels,
60
62
  marAgentCompact: input.marAgentCompact,
63
+ __usageGrant: input.usageGrant,
61
64
  marAgentRuntimeScopeId: input.marAgentRuntimeScopeId
62
65
  });
63
66
  }
@@ -188,6 +191,9 @@ export class SessionMessageService {
188
191
  else
189
192
  this.options.runtime.updateQueueItemAttachments(created.queueItem.id, attachments, displayInput);
190
193
  this.options.coordinator.prepare(submissionId, {
194
+ ...(input.__usageGrant === undefined
195
+ ? {}
196
+ : { usageGrant: modelUsageGrantSchema.parse(input.__usageGrant) }),
191
197
  runId,
192
198
  sessionId: currentSession.id,
193
199
  workspaceId: currentSession.workspaceId,
@@ -1,4 +1,5 @@
1
1
  import { isPlainRecord } from '../runner/codex/conversation-parser.js';
2
+ import { toolResultResourceInputSchema, toolResultResourcePageSchema } from '@myagentroam/protocol';
2
3
  export class SessionQueryService {
3
4
  options;
4
5
  constructor(options) {
@@ -11,6 +12,7 @@ export class SessionQueryService {
11
12
  'session.turns.list': (data) => this.turns(record(data)),
12
13
  'session.events.read': (data) => this.events(record(data)),
13
14
  'session.context.read': (data) => this.readContext(record(data)),
15
+ 'session.tool-resource.read': (data) => this.readToolResource(record(data)),
14
16
  'session.discover': (data) => this.discover(record(data))
15
17
  };
16
18
  }
@@ -39,6 +41,20 @@ export class SessionQueryService {
39
41
  throw new Error('SESSION_NOT_FOUND');
40
42
  return this.options.runtime.listConversationItems({ sessionId: session.id, ...page });
41
43
  }
44
+ async readToolResource(input) {
45
+ const session = await this.requireSession(input.sessionId);
46
+ if (typeof input.workspaceId !== 'string' || input.workspaceId !== session.workspaceId)
47
+ throw new Error('SESSION_NOT_FOUND');
48
+ const resource = toolResultResourceInputSchema.parse({
49
+ resourceRef: input.resourceRef,
50
+ cursor: input.cursor,
51
+ maxBytes: input.maxBytes
52
+ });
53
+ const runner = this.options.runners.product(session.runner);
54
+ if (!runner)
55
+ throw new Error('TOOL_RESOURCE_UNSUPPORTED');
56
+ return toolResultResourcePageSchema.parse(await runner.readToolResultResource(session, resource));
57
+ }
42
58
  async readContext(input) {
43
59
  return this.options.context.read(await this.requireSession(input.sessionId));
44
60
  }
@@ -83,6 +83,7 @@ export class WorkspaceChangeService {
83
83
  repositoryPath,
84
84
  repositoryName: repositoryPath === '' ? basename(repository) : posix.basename(repositoryPath),
85
85
  branch: summary.branch,
86
+ ...(summary.tracking === undefined ? {} : { tracking: summary.tracking }),
86
87
  dirty: summary.changes.length > 0,
87
88
  status: summary.changes.length > 0 ? 'DIRTY' : 'CLEAN',
88
89
  changes
@@ -186,6 +187,7 @@ export class WorkspaceChangeService {
186
187
  ...metadata,
187
188
  dirty: repository.dirty,
188
189
  branch: repository.branch,
190
+ ...(repository.tracking === undefined ? {} : { tracking: repository.tracking }),
189
191
  status: repository.dirty ? 'DIRTY' : 'CLEAN',
190
192
  changes: repository.changes
191
193
  };
@@ -160,27 +160,6 @@ export function extractClaudeToolUses(message) {
160
160
  ];
161
161
  });
162
162
  }
163
- export function extractClaudeToolResults(message) {
164
- const parentId = typeof message.parent_tool_use_id === 'string' ? message.parent_tool_use_id : null;
165
- const direct = compactRunnerValue(message.tool_use_result, 10_000);
166
- if (parentId !== null && direct.text.length > 0)
167
- return [
168
- { toolUseId: parentId, output: direct.text, failed: false, truncated: direct.truncated }
169
- ];
170
- return claudeContentBlocks(message).flatMap((block) => {
171
- if (block.type !== 'tool_result' || typeof block.tool_use_id !== 'string')
172
- return [];
173
- const result = compactRunnerValue(block.content, 10_000);
174
- return [
175
- {
176
- toolUseId: block.tool_use_id,
177
- output: result.text,
178
- failed: block.is_error === true,
179
- truncated: result.truncated
180
- }
181
- ];
182
- });
183
- }
184
163
  export function claudeContentBlocks(message) {
185
164
  const content = message.message;
186
165
  if (!isPlainRecord(content) || !Array.isArray(content.content))
@@ -4,7 +4,7 @@ import { stripClaudePlanTag } from '../runner-command-engine.js';
4
4
  import { publicGoalHistoryText } from '../service/goal-runtime.js';
5
5
  import { parseCodexComposerImageMarkers } from '../runner/codex/composer-image-markers.js';
6
6
  import { normalizeCodexTool } from '../runner/codex/tool-normalizer.js';
7
- import { normalizeClaudeTool } from '../runner/claude/tool-normalizer.js';
7
+ import { normalizeClaudeTool, restoreClaudeSubagentTitles } from '../runner/claude/tool-normalizer.js';
8
8
  import { compactRunnerText, isPlainRecord, nativePageEnd } from './node-operation-parsers.js';
9
9
  export function extractId(result, key) {
10
10
  if (typeof result === 'object' &&
@@ -148,6 +148,11 @@ export function nativeConversationPage(session, history, input, runtimeTurns = [
148
148
  const nativeTurnIds = nativeTranscriptTurnIds(session.id, nativeTurns);
149
149
  const end = nativePageEnd(effectiveCursor, session.id, nativeTurns.length);
150
150
  const start = Math.max(0, end - limit);
151
+ const knownClaudeSubagents = restoreClaudeSubagentTitles(session.runner === 'claude-code'
152
+ ? history.items
153
+ .slice(0, nativeTurns[start]?.[0]?.index ?? 0)
154
+ .filter((entry) => entry.kind === 'tool_call')
155
+ : []);
151
156
  const nativePageTurns = nativeTurns
152
157
  .slice(start, end)
153
158
  .map((entries, relativeIndex) => {
@@ -161,7 +166,7 @@ export function nativeConversationPage(session, history, input, runtimeTurns = [
161
166
  const turnId = runtimeTurn?.id ?? nativeTurnIds[start + relativeIndex];
162
167
  const itemIdentities = nativeTranscriptItemIdentities(entries);
163
168
  const nativeItems = entries.map(({ entry, index }, entryIndex) => ({
164
- ...nativeTranscriptConversationItem(session, entry, index, itemIdentities[entryIndex], turnId, runtimeTurns, registerImages),
169
+ ...nativeTranscriptConversationItem(session, entry, index, itemIdentities[entryIndex], turnId, runtimeTurns, registerImages, knownClaudeSubagents),
165
170
  runId: runtimeTurn?.runId ?? null
166
171
  }));
167
172
  const runtimeErrors = (runtimeTurn?.items ?? [])
@@ -299,14 +304,14 @@ function nativeTranscriptTurns(items) {
299
304
  turns.push(current);
300
305
  return turns;
301
306
  }
302
- function nativeTranscriptConversationItem(session, entry, index, itemIdentity, turnId, runtimeTurns, registerImages) {
307
+ function nativeTranscriptConversationItem(session, entry, index, itemIdentity, turnId, runtimeTurns, registerImages, knownClaudeSubagents) {
303
308
  const time = entry.createdAt;
304
309
  const isToolCall = entry.kind === 'tool_call';
305
310
  const isUnknown = entry.kind === 'unknown';
306
311
  const isReasoning = entry.kind === 'reasoning_summary';
307
312
  const isContextCompaction = entry.kind === 'context_compaction';
308
313
  const normalizedClaudeTool = session.runner === 'claude-code' && isToolCall
309
- ? normalizeClaudeTool(entry.toolName, entry.input, entry.outputSummary, entry.failed, entry.inputTruncated || entry.outputTruncated)
314
+ ? normalizeClaudeTool(entry.toolName, entry.input, entry.outputSummary, entry.failed, entry.inputTruncated || entry.outputTruncated, knownClaudeSubagents, entry.claudeResult)
310
315
  : undefined;
311
316
  const normalizedCodexTool = session.runner === 'codex' && isToolCall
312
317
  ? normalizeCodexTool({
package/dist/workspace.js CHANGED
@@ -1222,6 +1222,16 @@ function currentBranchFromStatusHeader(header) {
1222
1222
  return unborn[1] ?? null;
1223
1223
  return value.split('...')[0]?.split(' ')[0] ?? null;
1224
1224
  }
1225
+ function trackingFromStatusHeader(header) {
1226
+ const match = /^## [^ ]+\.\.\.([^ ]+)(?: \[(ahead \d+)?(?:, )?(behind \d+)?\])?$/.exec(header);
1227
+ if (match?.[1] === undefined)
1228
+ return undefined;
1229
+ return {
1230
+ upstream: match[1],
1231
+ ahead: Number(match[2]?.slice(6) ?? 0),
1232
+ behind: Number(match[3]?.slice(7) ?? 0)
1233
+ };
1234
+ }
1225
1235
  export async function listGitWorktrees(workspacePath) {
1226
1236
  const options = {
1227
1237
  timeoutMs: GIT_STATUS_TIMEOUT_MS,
@@ -1297,7 +1307,14 @@ async function parseGitWorktreeList(workspacePath, result, nulDelimited) {
1297
1307
  return { entries, truncated };
1298
1308
  }
1299
1309
  export async function readCurrentChangesSummary(workspacePath, isolateSubmoduleFailures = false) {
1300
- const args = ['status', '--porcelain=v1', '-z', '--branch', '--untracked-files=all'];
1310
+ const args = [
1311
+ 'status',
1312
+ '--porcelain=v1',
1313
+ '-z',
1314
+ '--branch',
1315
+ '--ahead-behind',
1316
+ '--untracked-files=all'
1317
+ ];
1301
1318
  const options = { timeoutMs: GIT_STATUS_TIMEOUT_MS, outputLimit: GIT_LARGE_OUTPUT_LIMIT };
1302
1319
  const result = await runReadOnlyGit(workspacePath, args, options).catch((error) => {
1303
1320
  if (!isolateSubmoduleFailures || !(error instanceof GitCommandError))
@@ -1332,8 +1349,10 @@ export async function readCurrentChangesSummary(workspacePath, isolateSubmoduleF
1332
1349
  if (indexState === 'R' || indexState === 'C' || worktreeState === 'R' || worktreeState === 'C')
1333
1350
  index += 1;
1334
1351
  }
1352
+ const tracking = trackingFromStatusHeader(header);
1335
1353
  return {
1336
1354
  branch: header.startsWith('## ') ? currentBranchFromStatusHeader(header) : null,
1355
+ ...(tracking === undefined ? {} : { tracking }),
1337
1356
  changes: output
1338
1357
  };
1339
1358
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.10.6",
3
+ "version": "0.10.8",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "license": "Apache-2.0 AND GPL-2.0-or-later",
6
6
  "type": "module",
@@ -37,10 +37,10 @@
37
37
  "playwright": "1.62.1",
38
38
  "ws": "^8.21.3",
39
39
  "zod": "4.4.3",
40
- "@myagentroam/agent": "0.10.6",
41
- "@myagentroam/code-navigation": "0.10.1",
42
- "@myagentroam/protocol": "0.10.6",
43
- "@myagentroam/marctl": "0.10.2"
40
+ "@myagentroam/agent": "0.10.8",
41
+ "@myagentroam/marctl": "0.10.2",
42
+ "@myagentroam/protocol": "0.10.8",
43
+ "@myagentroam/code-navigation": "0.10.1"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/ws": "^8.18.1"