maka-agent 0.2.0-dev.76.20261009 → 0.2.0-dev.78.20261010

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 (78) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +3 -3
  3. package/dist/pi-tui-pickers.js +5 -2
  4. package/dist/pi-tui-runner.js +211 -33
  5. package/dist/runtime-host-session-driver.js +91 -13
  6. package/dist/session-catalog-limits.js +22 -0
  7. package/dist/session-driver.js +10 -0
  8. package/dist/tui-copy-catalog.js +15 -0
  9. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  10. package/node_modules/@maka/core/dist/bot-chat-settings.js +3 -0
  11. package/node_modules/@maka/core/dist/bot-events.js +1 -8
  12. package/node_modules/@maka/core/dist/local-memory.js +38 -1
  13. package/node_modules/@maka/core/dist/scheduled-task.js +14 -15
  14. package/node_modules/@maka/core/dist/session.js +3 -0
  15. package/node_modules/@maka/core/dist/text-sanitize.js +8 -3
  16. package/node_modules/@maka/runtime/dist/agent-graph-supervisor-wake.js +115 -17
  17. package/node_modules/@maka/runtime/dist/agent-run.js +11 -0
  18. package/node_modules/@maka/runtime/dist/ai-sdk-backend.js +11 -0
  19. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +13 -5
  20. package/node_modules/@maka/runtime/dist/bots/proxied-fetch.js +29 -1
  21. package/node_modules/@maka/runtime/dist/computer-use-observation-text.js +22 -0
  22. package/node_modules/@maka/runtime/dist/conversation-copy-agent-output.js +227 -0
  23. package/node_modules/@maka/runtime/dist/conversation-copy.js +176 -46
  24. package/node_modules/@maka/runtime/dist/file-stable-write.js +15 -3
  25. package/node_modules/@maka/runtime/dist/filesystem-worker/resource-resolver.js +3 -1
  26. package/node_modules/@maka/runtime/dist/model-adapter.js +20 -1
  27. package/node_modules/@maka/runtime/dist/network/proxy-test.js +28 -1
  28. package/node_modules/@maka/runtime/dist/network/scoped-fetch-transport.js +13 -2
  29. package/node_modules/@maka/runtime/dist/owned-child-process.js +209 -0
  30. package/node_modules/@maka/runtime/dist/owned-process-main.js +183 -0
  31. package/node_modules/@maka/runtime/dist/pipe-process-driver.js +19 -25
  32. package/node_modules/@maka/runtime/dist/runtime-kernel.js +216 -8
  33. package/node_modules/@maka/runtime/dist/sandbox-boundary-tool.js +12 -0
  34. package/node_modules/@maka/runtime/dist/session-manager.js +156 -17
  35. package/node_modules/@maka/runtime/dist/shell-exec.js +18 -20
  36. package/node_modules/@maka/runtime/dist/shell-run-manager.js +2 -1
  37. package/node_modules/@maka/runtime/dist/stream-graph-coordinator.js +279 -8
  38. package/node_modules/@maka/runtime/dist/stream-graph-schedule-reconcile.js +209 -37
  39. package/node_modules/@maka/runtime/dist/tool-availability.js +55 -18
  40. package/node_modules/@maka/runtime/dist/tool-call-snapshot.js +142 -0
  41. package/node_modules/@maka/runtime/dist/tool-runtime.js +27 -84
  42. package/node_modules/@maka/runtime/dist/workers/{filesystem-worker.js → filesystem-worker.mjs} +12 -1
  43. package/node_modules/@maka/runtime-host/dist/client/connect-or-spawn.js +30 -6
  44. package/node_modules/@maka/runtime-host/dist/client/index.js +1 -1
  45. package/node_modules/@maka/runtime-host/dist/client/reconnect-lifecycle.js +44 -13
  46. package/node_modules/@maka/runtime-host/dist/client/session-catalog-summary.js +6 -0
  47. package/node_modules/@maka/runtime-host/dist/client/wsl-environment.js +4 -0
  48. package/node_modules/@maka/runtime-host/dist/protocol/index.js +5 -1
  49. package/node_modules/@maka/runtime-host/dist/protocol/message.js +1 -13
  50. package/node_modules/@maka/runtime-host/dist/protocol/session-catalog.js +46 -9
  51. package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +1 -13
  52. package/node_modules/@maka/runtime-host/dist/protocol/turn.js +1 -13
  53. package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +4 -7
  54. package/node_modules/@maka/runtime-host/dist/server/archive-retention-coordinator.js +81 -105
  55. package/node_modules/@maka/runtime-host/dist/server/candidate.js +47 -40
  56. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +87 -67
  57. package/node_modules/@maka/runtime-host/dist/server/execution-service.js +5 -1
  58. package/node_modules/@maka/runtime-host/dist/server/host-maintenance-guards.js +95 -0
  59. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +18 -8
  60. package/node_modules/@maka/runtime-host/dist/server/interactive-run-composer.js +32 -3
  61. package/node_modules/@maka/runtime-host/dist/server/project-catalog-coordinator.js +2 -0
  62. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +109 -8
  63. package/node_modules/@maka/runtime-host/dist/server/scheduled-task-coordinator.js +20 -3
  64. package/node_modules/@maka/runtime-host/dist/server/session-background-activity.js +71 -0
  65. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +8 -4
  66. package/node_modules/@maka/runtime-host/dist/server/session-interaction-activity.js +78 -0
  67. package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +56 -0
  68. package/node_modules/@maka/runtime-host/dist/server/session-revision-graph-references.js +16 -2
  69. package/node_modules/@maka/runtime-host/dist/server/skill-catalog-coordinator.js +2 -0
  70. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +19 -24
  71. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +74 -19
  72. package/node_modules/@maka/runtime-host/dist/server/workhub-result-coordinator.js +1 -1
  73. package/node_modules/@maka/runtime-host/dist/server/workhub-result-runtime.js +6 -99
  74. package/node_modules/@maka/runtime-host/dist/server/workhub-target-execution-authority.js +14 -126
  75. package/node_modules/@maka/storage/dist/context-offload-store.js +8 -3
  76. package/node_modules/@maka/storage/dist/git-entry.js +142 -5
  77. package/node_modules/@maka/storage/dist/session-bundle-policy.js +54 -19
  78. package/package.json +2 -2
@@ -366,8 +366,30 @@ function header(observation, elementCount, query, shownCount) {
366
366
  if (observation.truncated === true) {
367
367
  parts.push('truncated=true(the tree was cut short; an element you expect may exist but not be listed)');
368
368
  }
369
+ const selectedText = selectedTextCaption(observation);
370
+ if (selectedText)
371
+ parts.push(selectedText);
369
372
  return parts.join(' ');
370
373
  }
374
+ /**
375
+ * Window selection is application text. A password field that currently holds
376
+ * keys must not leak that text through this caption; otherwise truncate like
377
+ * `value` and keep the executor's own truncated flag.
378
+ */
379
+ function selectedTextCaption(observation) {
380
+ const selected = observation.selectedText;
381
+ if (!selected)
382
+ return undefined;
383
+ const focused = observation.elements.find((element) => element.focused === true);
384
+ const role = `${focused?.role ?? ''} ${focused?.subrole ?? ''}`;
385
+ if (/secure/i.test(role)) {
386
+ return 'selected_text=withheld(secure_field)';
387
+ }
388
+ const shown = truncate(selected.text);
389
+ return selected.truncated === true
390
+ ? `selected_text=${quote(shown)}(truncated=true)`
391
+ : `selected_text=${quote(shown)}`;
392
+ }
371
393
  /**
372
394
  * A subrole earns its place when it is telling the model something the rest of
373
395
  * the line does not.
@@ -0,0 +1,227 @@
1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ import { isRecord, isStringArray } from '@maka/core/record-schema';
20
+ import { decodeAgentRunEvent } from '@maka/core/agent-run';
21
+ import { decodeRuntimeEvent, decodeRuntimeInvocationOpened, } from '@maka/core/runtime-event';
22
+ import { runtimeInvocationOutcome } from '@maka/core/runtime-invocation';
23
+ import { stableJsonStringify } from '@maka/core/tool-args-identity';
24
+ import { runtimeInvocationFailureClass } from './runtime-event-read-model.js';
25
+ export function isConversationCopyAgentOutputSnapshot(value) {
26
+ return (isRecord(value) &&
27
+ value.kind === 'maka.agent_output_snapshot' &&
28
+ isResultPayload(value) &&
29
+ Object.keys(value).every((key) => [
30
+ 'kind',
31
+ 'schemaVersion',
32
+ 'status',
33
+ 'text',
34
+ 'textTruncated',
35
+ 'artifactIds',
36
+ 'omittedArtifactIds',
37
+ 'failureClass',
38
+ ].includes(key)));
39
+ }
40
+ function isResultPayload(value) {
41
+ return (value.schemaVersion === 1 &&
42
+ resultStatus(value.status) &&
43
+ isStringArray(value.artifactIds) &&
44
+ value.artifactIds.every(nonempty) &&
45
+ typeof value.textTruncated === 'boolean' &&
46
+ typeof value.omittedArtifactIds === 'number' &&
47
+ Number.isSafeInteger(value.omittedArtifactIds) &&
48
+ value.omittedArtifactIds >= 0 &&
49
+ (value.text === undefined || typeof value.text === 'string') &&
50
+ (value.failureClass === undefined || typeof value.failureClass === 'string'));
51
+ }
52
+ /**
53
+ * Recognize the defined result envelope, never arbitrary nested JSON ids.
54
+ * These are reference claims, not authority: the Host still checks them against
55
+ * the retained child's metadata, invocation ledger, and Artifact ownership.
56
+ */
57
+ export function conversationCopyAgentOutput(value) {
58
+ if (!isRecord(value))
59
+ return undefined;
60
+ const { execution, invocation, budget } = value;
61
+ if (!isRecord(execution) ||
62
+ execution.kind !== 'child_session' ||
63
+ !nonempty(execution.sessionId) ||
64
+ !isRecord(invocation) ||
65
+ invocation.sessionId !== execution.sessionId ||
66
+ !nonempty(invocation.runId) ||
67
+ !nonempty(invocation.turnId) ||
68
+ !nonempty(invocation.invocationId) ||
69
+ (execution.currentRunId !== undefined && execution.currentRunId !== invocation.runId) ||
70
+ !isRecord(budget) ||
71
+ typeof budget.view !== 'string' ||
72
+ !['result', 'events', 'runtime_events', 'all'].includes(budget.view) ||
73
+ !Array.isArray(value.events) ||
74
+ !Array.isArray(value.runtimeEvents) ||
75
+ !Array.isArray(value.diagnostics) ||
76
+ !Array.isArray(value.artifacts))
77
+ return undefined;
78
+ try {
79
+ const opening = decodeRuntimeInvocationOpened(invocation.opening);
80
+ const terminal = invocation.terminalEvent === undefined
81
+ ? undefined
82
+ : decodeRuntimeEvent(invocation.terminalEvent);
83
+ if (terminal &&
84
+ (terminal.sessionId !== execution.sessionId ||
85
+ terminal.runId !== invocation.runId ||
86
+ terminal.turnId !== invocation.turnId ||
87
+ terminal.invocationId !== invocation.invocationId ||
88
+ terminal.partial))
89
+ return undefined;
90
+ let result = value.result;
91
+ if (budget.view === 'result') {
92
+ if (value.events.length ||
93
+ value.runtimeEvents.length ||
94
+ value.diagnostics.length ||
95
+ value.artifacts.length)
96
+ return undefined;
97
+ }
98
+ else {
99
+ // Diagnostic views have no committed-result field. Their invocation
100
+ // still identifies the observation, and their Artifact list can contain
101
+ // outputs omitted by a separately bounded view=result read.
102
+ if (result !== undefined ||
103
+ (budget.view === 'events' && value.runtimeEvents.length > 0) ||
104
+ (budget.view === 'runtime_events' && value.events.length > 0))
105
+ return undefined;
106
+ const sameRun = (event) => event.sessionId === execution.sessionId &&
107
+ event.runId === invocation.runId &&
108
+ event.turnId === invocation.turnId;
109
+ const events = value.events.map(decodeAgentRunEvent);
110
+ const runtimeEvents = value.runtimeEvents.map(decodeRuntimeEvent);
111
+ if (!events.every(sameRun) ||
112
+ !runtimeEvents.every((event) => sameRun(event) && event.invocationId === invocation.invocationId) ||
113
+ !value.diagnostics.every((item) => isRecord(item) &&
114
+ item.runId === invocation.runId &&
115
+ item.turnId === invocation.turnId &&
116
+ nonempty(item.code) &&
117
+ typeof item.message === 'string') ||
118
+ !value.artifacts.every((item) => isRecord(item) &&
119
+ nonempty(item.id) &&
120
+ item.sessionId === execution.sessionId &&
121
+ item.turnId === invocation.turnId))
122
+ return undefined;
123
+ const text = [
124
+ ...runtimeEvents.flatMap(diagnosticRuntimeEventText),
125
+ ...events.flatMap((event) => (event.message ? [event.message] : [])),
126
+ ...value.diagnostics.map((item) => item.message),
127
+ ].join('\n');
128
+ const failureClass = runtimeInvocationFailureClass({ terminalEvent: terminal });
129
+ result = {
130
+ schemaVersion: 1,
131
+ status: runtimeInvocationOutcome({ terminalEvent: terminal }) ?? 'running',
132
+ ...(text ? { text } : {}),
133
+ // The bounded diagnostic view cannot establish a complete final text
134
+ // or count omitted Artifacts. Retain its readable excerpt and known ids.
135
+ textTruncated: true,
136
+ artifactIds: value.artifacts.map((item) => item.id),
137
+ omittedArtifactIds: 0,
138
+ ...(failureClass ? { failureClass } : {}),
139
+ };
140
+ }
141
+ if (!isRecord(result) || !isResultPayload(result))
142
+ return undefined;
143
+ if (terminal &&
144
+ (runtimeInvocationOutcome({ terminalEvent: terminal }) !== result.status ||
145
+ (result.terminalRuntimeEventId !== undefined &&
146
+ result.terminalRuntimeEventId !== terminal.id)))
147
+ return undefined;
148
+ if (!terminal && result.status !== 'running' && result.status !== 'waiting_for_user')
149
+ return undefined;
150
+ let graph;
151
+ if (result.graph !== undefined) {
152
+ if (!isRecord(result.graph) ||
153
+ !nonempty(result.graph.graphId) ||
154
+ !nonempty(result.graph.workId) ||
155
+ !nonempty(result.graph.operatorId))
156
+ return undefined;
157
+ graph = {
158
+ graphId: result.graph.graphId,
159
+ workId: result.graph.workId,
160
+ operatorId: result.graph.operatorId,
161
+ };
162
+ }
163
+ return {
164
+ reference: {
165
+ childSessionId: execution.sessionId,
166
+ runId: invocation.runId,
167
+ turnId: invocation.turnId,
168
+ status: result.status,
169
+ artifactIds: result.artifactIds,
170
+ ...(result.failureClass ? { failureClass: result.failureClass } : {}),
171
+ ...(opening.lineage?.resumedFromRunId
172
+ ? { resumedFromRunId: opening.lineage.resumedFromRunId }
173
+ : {}),
174
+ ...(terminal ? { terminalEventId: terminal.id } : {}),
175
+ ...(graph ? { graph } : {}),
176
+ },
177
+ // An independent Side Conversation keeps a result snapshot, without the
178
+ // original execution, invocation, Graph, or event identities.
179
+ snapshot: {
180
+ kind: 'maka.agent_output_snapshot',
181
+ schemaVersion: 1,
182
+ status: result.status,
183
+ ...(typeof result.text === 'string' ? { text: result.text } : {}),
184
+ textTruncated: result.textTruncated,
185
+ artifactIds: result.artifactIds,
186
+ omittedArtifactIds: result.omittedArtifactIds,
187
+ ...(result.failureClass ? { failureClass: result.failureClass } : {}),
188
+ },
189
+ };
190
+ }
191
+ catch {
192
+ return undefined;
193
+ }
194
+ }
195
+ /** Keep tool evidence as static text without copying event or tool-call identities. */
196
+ function diagnosticRuntimeEventText(event) {
197
+ if (event.partial)
198
+ return [];
199
+ const content = event.content;
200
+ switch (content?.kind) {
201
+ case 'text':
202
+ return [content.text];
203
+ case 'function_call':
204
+ return [stableJsonStringify({ kind: content.kind, name: content.name, args: content.args })];
205
+ case 'function_response':
206
+ return [
207
+ stableJsonStringify({
208
+ kind: content.kind,
209
+ name: content.name,
210
+ result: content.result,
211
+ ...(content.isError !== undefined ? { isError: content.isError } : {}),
212
+ }),
213
+ ];
214
+ default:
215
+ return [];
216
+ }
217
+ }
218
+ function nonempty(value) {
219
+ return typeof value === 'string' && value.length > 0;
220
+ }
221
+ function resultStatus(value) {
222
+ return (value === 'completed' ||
223
+ value === 'failed' ||
224
+ value === 'cancelled' ||
225
+ value === 'running' ||
226
+ value === 'waiting_for_user');
227
+ }
@@ -40,6 +40,8 @@ import { baseToolResultProjection, decodeLedgerTransition, reduceEffectiveModelP
40
40
  import { archivedToolResultProjection } from './tool-result-archive-transition.js';
41
41
  import { serializeToolResultProjectionV1 } from './tool-result-archive-encoding.js';
42
42
  import { createHash } from 'node:crypto';
43
+ import { encodeCanonicalRuntimeEvent } from '@maka/core/canonical-runtime-event';
44
+ import { conversationCopyAgentOutput, isConversationCopyAgentOutputSnapshot, } from './conversation-copy-agent-output.js';
43
45
  import { readToolResultPage, readableToolResult, resolveReadInput, READ_PAGE_MAX_BYTES, } from './read-page.js';
44
46
  /** Walks every typed StorageRef site reached by conversation-copy rewriting. */
45
47
  function collectConversationCopyStorageRefs(input) {
@@ -52,7 +54,7 @@ function collectConversationCopyStorageRefs(input) {
52
54
  if (isArchivedToolResultPlaceholder(value))
53
55
  return;
54
56
  try {
55
- addContent(decodePersistedToolResultContent(markPersisted(value)));
57
+ addContent(decodeConversationCopyToolResult(value));
56
58
  }
57
59
  catch {
58
60
  // Opaque tool results carry no typed StorageRef.
@@ -141,7 +143,7 @@ export function rewriteConversationCopyMessage(message, references) {
141
143
  if (message.type === 'tool_result') {
142
144
  return {
143
145
  ...message,
144
- content: rewriteToolResultContent(message.content, references),
146
+ content: rewriteToolResultContent(message.content, references, references.toolNamesByCall?.get(conversationCopyToolCallKey(message.turnId, message.toolUseId))),
145
147
  };
146
148
  }
147
149
  if (message.type === 'token_usage' && message.providerRequestTraceId) {
@@ -153,7 +155,8 @@ export function rewriteConversationCopyMessage(message, references) {
153
155
  return message;
154
156
  }
155
157
  function rewriteAttachmentResourceRefs(text, artifactIds) {
156
- return text.replace(/maka:\/\/runtime\/attachments\/[^\s)\]}>`'",;:!]+/g, (candidate) => {
158
+ // Snapshot tool evidence can contain JSON-escaped quotes and newlines.
159
+ return text.replace(/maka:\/\/runtime\/attachments\/[^\\\s)\]}>`'",;:!]+/g, (candidate) => {
157
160
  const parsed = parseAttachmentResourceRef(candidate);
158
161
  const artifactId = parsed ? artifactIds.get(parsed.artifactId) : undefined;
159
162
  return artifactId ? `maka://runtime/attachments/${artifactId}` : candidate;
@@ -320,6 +323,10 @@ export async function cloneConversationRuntimeLedger(input) {
320
323
  const operationIds = toolOperationIdMap(flattenedPlans, targetInvocationIds);
321
324
  const references = {
322
325
  ...input.referenceMap,
326
+ toolNamesByCall: conversationCopyToolNames({
327
+ messages: input.copiedMessages,
328
+ runtimeEvents: flattenedPlans.flatMap((plan) => plan.events),
329
+ }),
323
330
  runIds,
324
331
  invocationIds,
325
332
  operationIds,
@@ -589,6 +596,14 @@ function rewriteReadInput(input, references, clonedEvents) {
589
596
  : rewriteLedgerArchiveText(rewriteAttachmentResourceRefs(original, references.artifactIds), references);
590
597
  if (!input.path.startsWith('maka://read/'))
591
598
  return { ...input, path };
599
+ if (path !== original &&
600
+ !eventId &&
601
+ references.ledgerArchives?.has(original) &&
602
+ parseToolResultArchiveResourceRef(original)?.storage !== 'ledger') {
603
+ // A retired v1 body has become a rewritten ledger snapshot. Its previous
604
+ // byte offset and digest cannot address the new resource.
605
+ return { path };
606
+ }
592
607
  const url = new URL(input.path);
593
608
  if (eventId && path !== original) {
594
609
  const copied = clonedEvents.get(eventId);
@@ -674,7 +689,21 @@ async function loadConversationCopyRunEvents(sourceRuns, sourceEvents, copyTurnI
674
689
  ];
675
690
  }));
676
691
  }
677
- export function conversationCopyLinkedChildReferences(content) {
692
+ // Legacy archive bodies and provider projections can store the raw result;
693
+ // RuntimeEvents and transcript messages normally carry the JSON wrapper.
694
+ function decodeConversationCopyToolResult(value, toolName) {
695
+ if (toolName === 'agent_output' &&
696
+ (conversationCopyAgentOutput(value) || isConversationCopyAgentOutputSnapshot(value)))
697
+ return { kind: 'json', value };
698
+ return decodePersistedToolResultContent(markPersisted(value));
699
+ }
700
+ export function conversationCopyLinkedChildReferences(content, toolName) {
701
+ if (content.kind === 'json') {
702
+ if (toolName !== 'agent_output')
703
+ return [];
704
+ const output = conversationCopyAgentOutput(content.value);
705
+ return output ? [output.reference] : [];
706
+ }
678
707
  if (content.kind === 'subagent') {
679
708
  if (!content.childSessionId)
680
709
  return [];
@@ -706,27 +735,56 @@ export function conversationCopyLinkedChildReferences(content) {
706
735
  : []);
707
736
  }
708
737
  export function collectConversationCopyLinkedChildReferences(input) {
709
- const references = [];
710
- const add = (value) => {
738
+ return conversationCopyToolResults(input).flatMap(({ content, toolName }) => conversationCopyLinkedChildReferences(content, toolName));
739
+ }
740
+ /** Pair transcript results with call metadata, including historical ledger-only calls. */
741
+ function conversationCopyToolNames(input) {
742
+ const names = new Map();
743
+ const add = (turnId, callId, name) => {
744
+ const key = conversationCopyToolCallKey(turnId, callId);
745
+ names.set(key, names.has(key) && names.get(key) !== name ? undefined : name);
746
+ };
747
+ for (const message of input.messages) {
748
+ if (message.type === 'tool_call')
749
+ add(message.turnId, message.id, message.toolName);
750
+ }
751
+ for (const event of input.runtimeEvents) {
752
+ if (event.content?.kind === 'function_call' || event.content?.kind === 'function_response') {
753
+ add(event.turnId, event.content.id, event.content.name);
754
+ }
755
+ }
756
+ return names;
757
+ }
758
+ function conversationCopyToolCallKey(turnId, callId) {
759
+ return JSON.stringify([turnId, callId]);
760
+ }
761
+ function conversationCopyToolResults(input) {
762
+ const results = [];
763
+ const toolNames = conversationCopyToolNames(input);
764
+ const add = (value, toolName) => {
711
765
  if (isArchivedToolResultPlaceholder(value))
712
766
  return;
713
767
  try {
714
- references.push(...conversationCopyLinkedChildReferences(decodePersistedToolResultContent(markPersisted(value))));
768
+ results.push({ content: decodeConversationCopyToolResult(value, toolName), toolName });
715
769
  }
716
770
  catch {
717
- // Opaque tool results have no typed linked-child references.
771
+ // Opaque tool results have no typed conversation-copy references.
718
772
  }
719
773
  };
720
774
  for (const message of input.messages) {
721
775
  if (message.type === 'tool_result') {
722
- references.push(...conversationCopyLinkedChildReferences(message.content));
776
+ results.push({
777
+ content: message.content,
778
+ toolName: toolNames.get(conversationCopyToolCallKey(message.turnId, message.toolUseId)),
779
+ });
723
780
  }
724
781
  }
725
782
  for (const event of input.runtimeEvents) {
726
- if (event.content?.kind === 'function_response')
727
- add(event.content.result);
783
+ if (event.content?.kind === 'function_response') {
784
+ add(event.content.result, toolNames.get(conversationCopyToolCallKey(event.turnId, event.content.id)));
785
+ }
728
786
  }
729
- return references;
787
+ return results;
730
788
  }
731
789
  /**
732
790
  * Collects the `relativePath` of every `session_file` StorageRef that the copied
@@ -737,6 +795,8 @@ export function collectConversationCopyLinkedChildReferences(input) {
737
795
  * their refs resolve in `rewriteStorageRef`. Walks exactly the ref sites reached
738
796
  * by `rewriteStorageRef`: user-message attachments, tool_result image refs, text
739
797
  * runtime-event attachments, and function_response / archived tool-result images.
798
+ * Static agent_output snapshots also own their Artifacts; those can retain a
799
+ * child Turn id outside the copied parent's turn closure and need explicit inclusion.
740
800
  */
741
801
  export function collectConversationCopySessionFileRefs(input) {
742
802
  const refs = new Set();
@@ -745,6 +805,14 @@ export function collectConversationCopySessionFileRefs(input) {
745
805
  refs.add(ref.relativePath);
746
806
  }
747
807
  }
808
+ for (const { content, toolName } of conversationCopyToolResults(input)) {
809
+ if (toolName === 'agent_output' &&
810
+ content.kind === 'json' &&
811
+ isConversationCopyAgentOutputSnapshot(content.value)) {
812
+ for (const artifactId of content.value.artifactIds)
813
+ refs.add(artifactId);
814
+ }
815
+ }
748
816
  return refs;
749
817
  }
750
818
  function cloneAgentRunEvent(event, ids, references, sourceCompactableEvents, clonedRuntimeEvents, checkpointIds, providerTraceIds, logicalCallIds, clonedTransitions) {
@@ -830,10 +898,9 @@ function cloneAgentRunEvent(event, ids, references, sourceCompactableEvents, clo
830
898
  }
831
899
  else if (event.type === MODEL_PROJECTION_TRANSITION_EVENT_TYPE) {
832
900
  const cloned = clonedTransitions.get(event);
833
- // Every transition whose target is in the copied slice was gathered into
834
- // this run's ledger, wherever it was recorded. So a transition that finds no
835
- // cloned target has genuinely lost its target as well, and dropping it
836
- // cannot bring replaced content back.
901
+ // A transition is omitted only when its target was excluded or its archive
902
+ // wrapper was collapsed into an already copied archive. Neither case can
903
+ // bring replaced content back into the model-visible history.
837
904
  if (!cloned)
838
905
  return null;
839
906
  data = { ...event.data, transition: cloned, runtimeEventId: cloned.target.runtimeEventId };
@@ -873,37 +940,57 @@ function cloneModelProjectionTransition(event, references, clonedRuntimeEvents,
873
940
  }
874
941
  const placeholder = rawPlaceholder;
875
942
  const existing = transitionState.get(clonedTarget.id);
876
- const sourceProjection = existing?.projection ?? baseToolResultProjection(clonedTarget);
943
+ // RuntimeEvent storage canonicalizes JSON key order. Hash the bytes a later
944
+ // archive read will reconstruct, including newly built snapshot projections.
945
+ const sourceProjection = existing?.projection ??
946
+ baseToolResultProjection(encodeCanonicalRuntimeEvent(clonedTarget).event);
877
947
  if (!sourceProjection) {
878
948
  throw new Error(`Cannot copy model projection transition ${event.id} onto its target`);
879
949
  }
880
- // The copied event already holds the target's rewritten result, so every
881
- // archive, including a legacy Artifact-backed one, is rebuilt as a ledger
882
- // archive of that event instead of pointing at the source's archived body.
950
+ const previousTransitionId = source.previousTransitionId
951
+ ? requiredMappedId(transitionIds, source.previousTransitionId, 'model projection transition')
952
+ : undefined;
953
+ // Persisted placeholders can use a public event address or an exact legacy
954
+ // ledger identity. Both must resolve to the same rebuilt archive on recopy.
955
+ const sourceRefs = new Set([
956
+ buildToolResultArchiveResourceRef(placeholder),
957
+ ...(placeholder.resourceRef ? [placeholder.resourceRef] : []),
958
+ ]);
959
+ const previousPlaceholder = existing?.projection.kind === 'json' ? existing.projection.value : undefined;
960
+ if (existing &&
961
+ isArchivedToolResultPlaceholder(previousPlaceholder) &&
962
+ previousPlaceholder.rewriteVersion === 2) {
963
+ if (previousTransitionId !== existing.transitionId)
964
+ throw new Error(`Cannot collapse disconnected archive transition ${event.id}`);
965
+ // Public Read addresses identify an event, not a transition. Archiving
966
+ // this placeholder again would make that address return itself. Keep the
967
+ // first archive of the copied body and alias every later wrapper to it.
968
+ for (const sourceRef of sourceRefs)
969
+ references.ledgerArchives?.set(sourceRef, previousPlaceholder);
970
+ transitionIds.set(source.transitionId, existing.transitionId);
971
+ return null;
972
+ }
973
+ if (clonedTarget.content?.kind === 'function_response' && !clonedTarget.content.modelProjection)
974
+ clonedTarget.content.modelProjection = sourceProjection;
975
+ // Rebuild every archive over the copied ledger, including legacy v1 bodies.
883
976
  const serialized = serializeToolResultProjectionV1(sourceProjection);
884
977
  const rewritten = buildLedgerArchivedToolResultPlaceholder({
885
978
  storage: 'ledger',
886
979
  runtimeEventId: clonedTarget.id,
887
980
  toolCallId: placeholder.toolCallId,
888
981
  toolName: placeholder.toolName,
982
+ originalEstimatedTokens: placeholder.originalEstimatedTokens,
983
+ reason: placeholder.reason,
889
984
  sourceProjectionDigest: durableToolResultProjectionDigest(sourceProjection),
890
985
  bodySha256: createHash('sha256').update(serialized).digest('hex'),
891
986
  originalBytes: Buffer.byteLength(serialized),
892
- originalEstimatedTokens: placeholder.originalEstimatedTokens,
893
- reason: placeholder.reason,
894
- ...(source.previousTransitionId
895
- ? {
896
- previousTransitionId: requiredMappedId(transitionIds, source.previousTransitionId, 'model projection transition'),
897
- }
898
- : {}),
987
+ ...(previousTransitionId ? { previousTransitionId } : {}),
899
988
  });
900
989
  if (placeholder.page) {
901
990
  rewritten.page = readToolResultPage(serialized, { path: rewritten.resourceRef }, READ_PAGE_MAX_BYTES - Buffer.byteLength(JSON.stringify(rewritten)) - 32);
902
991
  }
903
- // Earlier Reads of a legacy archive keep naming its copied Artifact.
904
- if (placeholder.rewriteVersion === 2) {
905
- references.ledgerArchives?.set(placeholder.resourceRef ?? buildToolResultArchiveResourceRef(placeholder), rewritten);
906
- }
992
+ for (const sourceRef of sourceRefs)
993
+ references.ledgerArchives?.set(sourceRef, rewritten);
907
994
  const transition = buildModelProjectionTransition({
908
995
  sessionId: references.targetSessionId,
909
996
  target: {
@@ -917,11 +1004,7 @@ function cloneModelProjectionTransition(event, references, clonedRuntimeEvents,
917
1004
  // The applied chain is copied in fold order, so a predecessor is always
918
1005
  // rebuilt before its successor. An unmapped one means the chain broke, and
919
1006
  // rooting the successor instead would change what the fold decides.
920
- ...(source.previousTransitionId
921
- ? {
922
- previousTransitionId: requiredMappedId(transitionIds, source.previousTransitionId, 'model projection transition'),
923
- }
924
- : {}),
1007
+ ...(previousTransitionId ? { previousTransitionId } : {}),
925
1008
  now: source.createdAt,
926
1009
  });
927
1010
  transitionIds.set(source.transitionId, transition.transitionId);
@@ -1135,6 +1218,9 @@ function rewriteInvocationOpening(opening, references) {
1135
1218
  };
1136
1219
  }
1137
1220
  function rewriteRuntimeEventReferences(event, references) {
1221
+ const toolName = event.content?.kind === 'function_response'
1222
+ ? references.toolNamesByCall?.get(conversationCopyToolCallKey(event.turnId, event.content.id))
1223
+ : undefined;
1138
1224
  const content = event.content?.kind === 'text'
1139
1225
  ? {
1140
1226
  ...event.content,
@@ -1151,10 +1237,10 @@ function rewriteRuntimeEventReferences(event, references) {
1151
1237
  : event.content?.kind === 'function_response'
1152
1238
  ? {
1153
1239
  ...event.content,
1154
- result: rewriteRuntimeToolResult(event.content.result, references),
1240
+ result: rewriteRuntimeToolResult(event.content.result, references, toolName),
1155
1241
  ...(event.content.modelProjection
1156
1242
  ? {
1157
- modelProjection: rewriteDurableToolResultProjectionArtifactRefs(event.content.modelProjection, (ref) => rewriteProjectionArtifactRef(ref, references)),
1243
+ modelProjection: rewriteConversationCopyModelProjection(event.content.modelProjection, references, toolName),
1158
1244
  }
1159
1245
  : {}),
1160
1246
  }
@@ -1249,7 +1335,24 @@ function rewriteToolRecoveryFact(recovery, operationId, references) {
1249
1335
  },
1250
1336
  };
1251
1337
  }
1252
- function rewriteToolResultContent(content, references) {
1338
+ function rewriteToolResultContent(content, references, toolName) {
1339
+ if (toolName === 'agent_output' && content.kind === 'json') {
1340
+ if (isConversationCopyAgentOutputSnapshot(content.value)) {
1341
+ return rewriteAgentOutputSnapshot(content.value, references);
1342
+ }
1343
+ const output = conversationCopyAgentOutput(content.value);
1344
+ if (output) {
1345
+ const shared = sharedChild(output.reference.childSessionId, references);
1346
+ if (!shared) {
1347
+ return rewriteAgentOutputSnapshot(output.snapshot, references);
1348
+ }
1349
+ // Revision children remain owned by their original physical Session.
1350
+ // Validate those external references without rewriting their identities.
1351
+ rewriteLinkedRunId(output.reference.runId, shared, references, 'AgentRun');
1352
+ rewriteLinkedArtifactIds(output.reference.artifactIds, shared, references);
1353
+ return content;
1354
+ }
1355
+ }
1253
1356
  if (content.kind === 'image') {
1254
1357
  return { ...content, ref: rewriteStorageRef(content.ref, references) };
1255
1358
  }
@@ -1320,17 +1423,44 @@ function rewriteToolResultContent(content, references) {
1320
1423
  }
1321
1424
  return content;
1322
1425
  }
1323
- function rewriteRuntimeToolResult(value, references) {
1426
+ function rewriteAgentOutputSnapshot(snapshot, references) {
1427
+ return {
1428
+ kind: 'json',
1429
+ value: {
1430
+ ...snapshot,
1431
+ ...(snapshot.text !== undefined
1432
+ ? { text: rewriteAttachmentResourceRefs(snapshot.text, references.artifactIds) }
1433
+ : {}),
1434
+ artifactIds: rewriteArtifactIds(snapshot.artifactIds, references),
1435
+ },
1436
+ };
1437
+ }
1438
+ function rewriteConversationCopyModelProjection(projection, references, toolName) {
1439
+ if (toolName === 'agent_output' &&
1440
+ projection.kind === 'json' &&
1441
+ (conversationCopyAgentOutput(projection.value) ||
1442
+ isConversationCopyAgentOutputSnapshot(projection.value))) {
1443
+ const content = rewriteToolResultContent({ kind: 'json', value: projection.value }, references, toolName);
1444
+ if (content.kind === 'json')
1445
+ return { ...projection, value: content.value };
1446
+ }
1447
+ return rewriteDurableToolResultProjectionArtifactRefs(projection, (ref) => rewriteProjectionArtifactRef(ref, references));
1448
+ }
1449
+ function rewriteRuntimeToolResult(value, references, toolName) {
1324
1450
  if (isArchivedToolResultPlaceholder(value))
1325
1451
  return rewriteArchivedToolResult(value, references);
1326
1452
  let content;
1327
1453
  try {
1328
- content = decodePersistedToolResultContent(markPersisted(value));
1454
+ content = decodeConversationCopyToolResult(value, toolName);
1329
1455
  }
1330
1456
  catch {
1331
1457
  return value;
1332
1458
  }
1333
- return rewriteToolResultContent(content, references);
1459
+ const rewritten = rewriteToolResultContent(content, references, toolName);
1460
+ return (conversationCopyAgentOutput(value) || isConversationCopyAgentOutputSnapshot(value)) &&
1461
+ rewritten.kind === 'json'
1462
+ ? rewritten.value
1463
+ : rewritten;
1334
1464
  }
1335
1465
  function rewriteArtifactIds(artifactIds, references) {
1336
1466
  return artifactIds.flatMap((artifactId) => {
@@ -1401,11 +1531,11 @@ function rewriteProjectionArtifactRef(ref, references) {
1401
1531
  return rewritten;
1402
1532
  }
1403
1533
  function rewriteArchivedToolResult(value, references) {
1534
+ const migrated = references.ledgerArchives?.get(buildToolResultArchiveResourceRef(value));
1535
+ if (migrated)
1536
+ return migrated;
1404
1537
  if (value.rewriteVersion === 2) {
1405
- const rewritten = references.ledgerArchives?.get(buildToolResultArchiveResourceRef(value));
1406
- if (!rewritten)
1407
- throw new Error('Cannot copy ledger archive without its committed transition');
1408
- return rewritten;
1538
+ throw new Error('Cannot copy ledger archive without its committed transition');
1409
1539
  }
1410
1540
  const artifactId = rewriteOwnedArtifactId(value.artifactId, references);
1411
1541
  const resource = value.resourceRef