maka-agent 0.2.0-dev.12.20260901 → 0.2.0-dev.14.20260902

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 (105) hide show
  1. package/THIRD_PARTY_NOTICES.txt +2 -2
  2. package/dist/acp/maka-acp-agent.js +6 -3
  3. package/dist/acp/session-registry.js +307 -0
  4. package/dist/acp/stdio-server.js +20 -1
  5. package/dist/cli-core.js +15 -2
  6. package/dist/pi-transcript.js +1 -1
  7. package/dist/pi-tui-layout.js +1 -1
  8. package/dist/pi-tui-mcp-status.js +1 -1
  9. package/dist/pi-tui-runner.js +1 -1
  10. package/dist/runtime-host-access-command.js +37 -4
  11. package/dist/runtime-host-capability-provider-command.js +16 -2
  12. package/dist/runtime-host-cli-context.js +73 -3
  13. package/dist/runtime-host-cli.js +37 -6
  14. package/dist/runtime-host-run-command.js +3 -1
  15. package/dist/runtime-host-update-command.js +59 -31
  16. package/dist/tui-copy-catalog.js +4 -2
  17. package/native/runtime-host-peer/prebuilds/darwin-arm64/maka_runtime_host_peer.node +0 -0
  18. package/native/runtime-host-peer/prebuilds/linux-arm64/maka_runtime_host_peer.node +0 -0
  19. package/native/runtime-host-peer/prebuilds/linux-x64/maka_runtime_host_peer.node +0 -0
  20. package/native/runtime-host-peer/prebuilds/win32-x64/maka_runtime_host_peer.node +0 -0
  21. package/node_modules/@maka/core/dist/attachments.js +12 -0
  22. package/node_modules/@maka/core/dist/connection-error-copy.js +17 -34
  23. package/node_modules/@maka/core/dist/events.js +0 -13
  24. package/node_modules/@maka/core/dist/maka-wordmark.js +23 -0
  25. package/node_modules/@maka/core/dist/model-catalog.js +22 -1
  26. package/node_modules/@maka/core/dist/runtime-event.js +1 -0
  27. package/node_modules/@maka/core/dist/runtime-policy/connection-catalog-codec.js +50 -1
  28. package/node_modules/@maka/core/dist/runtime-policy/credential-vault-codec.js +9 -1
  29. package/node_modules/@maka/core/dist/runtime-policy/model-catalog-entry-codec.js +10 -1
  30. package/node_modules/@maka/core/dist/runtime-policy/policy-codec.js +75 -0
  31. package/node_modules/@maka/core/dist/runtime-policy.js +4 -2
  32. package/node_modules/@maka/core/dist/session.js +1 -0
  33. package/node_modules/@maka/core/dist/settings.js +10 -2
  34. package/node_modules/@maka/core/dist/usage-record-schema.js +11 -1
  35. package/node_modules/@maka/core/package.json +2 -1
  36. package/node_modules/@maka/mcp/dist/index.js +1 -0
  37. package/node_modules/@maka/runtime/dist/ai-sdk-backend.js +52 -80
  38. package/node_modules/@maka/runtime/dist/ai-sdk-compaction.js +178 -140
  39. package/node_modules/@maka/runtime/dist/archive-read-tool.js +26 -5
  40. package/node_modules/@maka/runtime/dist/builtin-tools.js +27 -2
  41. package/node_modules/@maka/runtime/dist/context-budget-policy.js +0 -12
  42. package/node_modules/@maka/runtime/dist/conversation-copy.js +66 -43
  43. package/node_modules/@maka/runtime/dist/durable-tool-result-projection.js +31 -0
  44. package/node_modules/@maka/runtime/dist/history-compact-error.js +10 -2
  45. package/node_modules/@maka/runtime/dist/history-compact-input-fit.js +7 -1
  46. package/node_modules/@maka/runtime/dist/history-compaction.js +13 -22
  47. package/node_modules/@maka/runtime/dist/model-history.js +41 -16
  48. package/node_modules/@maka/runtime/dist/provider-image-overflow-recovery.js +12 -31
  49. package/node_modules/@maka/runtime/dist/runtime-event-backfill.js +3 -0
  50. package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +13 -2
  51. package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +3 -6
  52. package/node_modules/@maka/runtime/dist/shell-tools.js +77 -1
  53. package/node_modules/@maka/runtime/dist/tool-availability.js +74 -5
  54. package/node_modules/@maka/runtime/dist/tool-result-archive-resource.js +333 -52
  55. package/node_modules/@maka/runtime/dist/tool-result-archive-transition.js +34 -3
  56. package/node_modules/@maka/runtime/dist/tool-runtime.js +12 -0
  57. package/node_modules/@maka/runtime-host/dist/adapter/session-projector.js +0 -3
  58. package/node_modules/@maka/runtime-host/dist/client/catalog-reader.js +40 -13
  59. package/node_modules/@maka/runtime-host/dist/client/connection.js +50 -31
  60. package/node_modules/@maka/runtime-host/dist/client/host-profile.js +48 -4
  61. package/node_modules/@maka/runtime-host/dist/client/index.js +2 -2
  62. package/node_modules/@maka/runtime-host/dist/client/owner-connection-code.js +32 -1
  63. package/node_modules/@maka/runtime-host/dist/client/peer-client.js +22 -4
  64. package/node_modules/@maka/runtime-host/dist/operator/access-management-frame.js +10 -1
  65. package/node_modules/@maka/runtime-host/dist/peer-mesh/node.js +56 -12
  66. package/node_modules/@maka/runtime-host/dist/peer-mesh/owner.js +3 -0
  67. package/node_modules/@maka/runtime-host/dist/protocol/configuration.js +55 -9
  68. package/node_modules/@maka/runtime-host/dist/protocol/host-status.js +37 -0
  69. package/node_modules/@maka/runtime-host/dist/protocol/index.js +17 -1
  70. package/node_modules/@maka/runtime-host/dist/protocol/network-proxy.js +1 -7
  71. package/node_modules/@maka/runtime-host/dist/protocol/operations.js +2 -0
  72. package/node_modules/@maka/runtime-host/dist/protocol/runtime-policy.js +48 -1
  73. package/node_modules/@maka/runtime-host/dist/protocol/session-collaboration.js +4 -2
  74. package/node_modules/@maka/runtime-host/dist/protocol/turn.js +2 -12
  75. package/node_modules/@maka/runtime-host/dist/server/access-authority.js +2 -1
  76. package/node_modules/@maka/runtime-host/dist/server/agent-graph-execution-coordinator.js +1 -2
  77. package/node_modules/@maka/runtime-host/dist/server/canonical-turn-snapshot.js +0 -9
  78. package/node_modules/@maka/runtime-host/dist/server/configuration-coordinator.js +20 -1
  79. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +46 -24
  80. package/node_modules/@maka/runtime-host/dist/server/execution-service.js +4 -1
  81. package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +15 -0
  82. package/node_modules/@maka/runtime-host/dist/server/listener-set.js +8 -4
  83. package/node_modules/@maka/runtime-host/dist/server/network-proxy-coordinator.js +0 -1
  84. package/node_modules/@maka/runtime-host/dist/server/peer-listener.js +3 -0
  85. package/node_modules/@maka/runtime-host/dist/server/runtime-policy-coordinator.js +28 -1
  86. package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +3 -3
  87. package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +35 -3
  88. package/node_modules/@maka/runtime-host/dist/server/session-sidecar-purge.js +16 -0
  89. package/node_modules/@maka/storage/dist/runtime-policy/coordinator.js +189 -6
  90. package/node_modules/@maka/storage/dist/runtime-policy-stores.js +4 -1
  91. package/node_modules/@maka/storage/dist/settings-store.js +16 -25
  92. package/node_modules/fast-uri/index.js +215 -47
  93. package/node_modules/fast-uri/lib/schemes.js +9 -4
  94. package/node_modules/fast-uri/lib/utils.js +396 -92
  95. package/node_modules/fast-uri/package.json +1 -1
  96. package/node_modules/qs/.github/THREAT_MODEL.md +3 -3
  97. package/node_modules/qs/CHANGELOG.md +15 -0
  98. package/node_modules/qs/README.md +19 -1
  99. package/node_modules/qs/dist/qs.js +21 -21
  100. package/node_modules/qs/eslint.config.mjs +1 -1
  101. package/node_modules/qs/lib/parse.js +3 -4
  102. package/node_modules/qs/lib/stringify.js +23 -8
  103. package/node_modules/qs/lib/utils.js +10 -3
  104. package/node_modules/qs/package.json +4 -4
  105. package/package.json +1 -1
@@ -27,28 +27,38 @@ export function buildArchiveReadTool(reader) {
27
27
  .string()
28
28
  .describe('A maka://archive/... ref returned in an archived tool-result placeholder'),
29
29
  operation: z
30
- .enum(['inspect', 'read', 'query'])
30
+ .enum(['inspect', 'read', 'query', 'search'])
31
31
  .default('inspect')
32
- .describe('inspect lists archive metadata/items; query reads one structured item; read returns one bounded raw page'),
32
+ .describe('inspect lists archive metadata/items and a preview; query reads one structured item; read returns one bounded page (by character or line); search locates a substring'),
33
+ unit: z
34
+ .enum(['char', 'line'])
35
+ .default('char')
36
+ .describe('For read: whether offset/limit count characters (default) or whole lines'),
33
37
  offset: z
34
38
  .number()
35
39
  .int()
36
40
  .nonnegative()
37
41
  .optional()
38
- .describe('Zero-based character offset for read/query pagination'),
42
+ .describe('Zero-based start for pagination: character or line offset for read (per unit), character offset for query, or the character position to resume search from'),
39
43
  limit: z
40
44
  .number()
41
45
  .int()
42
46
  .positive()
43
47
  .max(TOOL_RESULT_ARCHIVE_MAX_LIMIT)
44
48
  .optional()
45
- .describe(`Maximum returned characters, capped at ${TOOL_RESULT_ARCHIVE_MAX_LIMIT}`),
49
+ .describe(`Maximum returned characters (or lines when unit is "line"), capped at ${TOOL_RESULT_ARCHIVE_MAX_LIMIT}`),
46
50
  itemId: z
47
51
  .string()
48
52
  .min(1)
49
53
  .max(256)
50
54
  .optional()
51
55
  .describe('Structured item id returned by inspect; required for query'),
56
+ pattern: z
57
+ .string()
58
+ .min(1)
59
+ .max(256)
60
+ .optional()
61
+ .describe('Literal, case-insensitive substring to locate; required for search'),
52
62
  })
53
63
  .strip()
54
64
  .superRefine((value, ctx) => {
@@ -59,13 +69,20 @@ export function buildArchiveReadTool(reader) {
59
69
  message: 'itemId is required for query',
60
70
  });
61
71
  }
72
+ if (value.operation === 'search' && !value.pattern) {
73
+ ctx.addIssue({
74
+ code: 'custom',
75
+ path: ['pattern'],
76
+ message: 'pattern is required for search',
77
+ });
78
+ }
62
79
  }));
63
80
  const providerSchema = zodSchema(parameters);
64
81
  return {
65
82
  name: ARCHIVE_READ_TOOL_NAME,
66
83
  displayName: 'Read archived result',
67
84
  activityKind: 'read',
68
- description: 'Inspect or page through a tool-result archive returned as a maka://archive/... ref. Start with inspect. For agent_swarm archives, query one itemId at a time. Results are strictly bounded so reading an archive cannot immediately trigger another archive.',
85
+ description: 'Inspect, search, or page through a tool-result archive returned as a maka://archive/... ref. Start with inspect for a preview and the char/line coordinate space. Use operation "search" with a pattern to locate text, operation "read" with unit "line" for line-oriented terminal output, or operation "query" with an itemId for one agent_swarm item. Results are strictly bounded so reading an archive cannot immediately trigger another archive.',
69
86
  parameters: jsonSchema(async () => await providerSchema.jsonSchema, {
70
87
  validate: async (value) => {
71
88
  const result = await parameters.safeParseAsync(value);
@@ -84,6 +101,10 @@ function cleanArchiveReadInput(input) {
84
101
  const operation = cleaned.operation ?? 'inspect';
85
102
  if (operation !== 'query')
86
103
  delete cleaned.itemId;
104
+ if (operation !== 'search')
105
+ delete cleaned.pattern;
106
+ if (operation !== 'read')
107
+ delete cleaned.unit;
87
108
  if (operation === 'inspect') {
88
109
  delete cleaned.offset;
89
110
  delete cleaned.limit;
@@ -27,6 +27,7 @@ import { tmpdir } from 'node:os';
27
27
  import { basename, dirname } from 'node:path';
28
28
  import { compilePermissionProfile } from '@maka/core/permission-profile-compiler';
29
29
  import { parseAttachmentResourceRef } from '@maka/core/attachments';
30
+ import { isStorageRef } from '@maka/core/events';
30
31
  import { bashToolResultToModelOutput } from './bash-model-output.js';
31
32
  import { fileWriteToolResultToModelOutput } from './file-tool-model-output.js';
32
33
  import { openAiApplyPatchInputSchema } from './openai-apply-patch.js';
@@ -236,6 +237,28 @@ export function buildBuiltinTools(options = {}) {
236
237
  description: readDescription,
237
238
  parameters: readParameters,
238
239
  executionFacts,
240
+ ...(options.releaseImageSnapshot
241
+ ? {
242
+ compensateDurableOutcomeCommitFailure: async (input) => {
243
+ const result = input.result;
244
+ if (!result ||
245
+ typeof result !== 'object' ||
246
+ result.kind !== 'image') {
247
+ return;
248
+ }
249
+ const ref = result.ref;
250
+ if (!isStorageRef(ref) ||
251
+ ref.kind !== 'session_context' ||
252
+ ref.sessionId !== input.sessionId) {
253
+ return;
254
+ }
255
+ await options.releaseImageSnapshot({
256
+ sessionId: ref.sessionId,
257
+ refId: ref.refId,
258
+ });
259
+ },
260
+ }
261
+ : {}),
239
262
  impl: async (input, ctx) => {
240
263
  const { cwd, sessionId, abortSignal } = ctx;
241
264
  if ('ref' in input) {
@@ -273,10 +296,12 @@ export function buildBuiltinTools(options = {}) {
273
296
  if (result.kind === 'read_image') {
274
297
  if (!options.snapshotImage)
275
298
  throw new Error('Read image snapshots are not available in this toolset.');
299
+ if (!ctx.operationId) {
300
+ throw new Error('Read image snapshots require a durable tool operation identity.');
301
+ }
276
302
  const ref = await options.snapshotImage({
277
303
  sessionId,
278
- turnId: ctx.turnId,
279
- name: basename(path),
304
+ ownerId: ctx.operationId,
280
305
  bytes: result.bytes,
281
306
  mimeType: result.mimeType,
282
307
  });
@@ -18,7 +18,6 @@
18
18
  */
19
19
  import { lookupModelMetadata } from '@maka/core/model-metadata';
20
20
  import { relayModelProfile } from '@maka/core/model-thinking';
21
- import { finitePositive } from './context-budget-helpers.js';
22
21
  export function buildDefaultContextBudgetPolicy(connection, options = {}) {
23
22
  const contextWindow = resolveSelectedModelContextWindow(connection, options.modelId);
24
23
  const reserveTokens = defaultCompactReserveTokens(contextWindow);
@@ -98,14 +97,3 @@ function narrowestPositiveLimit(...values) {
98
97
  const positiveValues = values.filter((value) => typeof value === 'number' && Number.isFinite(value) && value > 0);
99
98
  return positiveValues.length > 0 ? Math.min(...positiveValues) : undefined;
100
99
  }
101
- export function resolveContextBudgetCapacity(connection, modelId, policy) {
102
- const selectedWindow = resolveSelectedModelContextWindow(connection, modelId);
103
- if (selectedWindow !== undefined) {
104
- return { tokens: selectedWindow, source: 'selected_model' };
105
- }
106
- const historyBudget = finitePositive(policy?.maxHistoryEstimatedTokens);
107
- const reserveTokens = finitePositive(policy?.historyCompact?.midTurn?.reserveTokens);
108
- if (historyBudget === undefined || reserveTokens === undefined)
109
- return undefined;
110
- return { tokens: historyBudget + reserveTokens, source: 'policy_fallback' };
111
- }
@@ -34,6 +34,56 @@ import { rewriteDurableToolResultProjectionArtifactRefs } from './durable-tool-r
34
34
  import { buildModelProjectionTransition, decodeModelProjectionTransition, MODEL_PROJECTION_TRANSITION_EVENT_TYPE, } from '@maka/core/model-projection-transition';
35
35
  import { baseToolResultProjection, decodeLedgerTransition, reduceEffectiveModelProjections, } from './model-projection-transition-ledger.js';
36
36
  import { archivedToolResultProjection } from './tool-result-archive-transition.js';
37
+ /** Walks every typed StorageRef site reached by conversation-copy rewriting. */
38
+ function collectConversationCopyStorageRefs(input) {
39
+ const refs = [];
40
+ const addContent = (content) => {
41
+ if (content.kind === 'image')
42
+ refs.push(content.ref);
43
+ };
44
+ const addSerialized = (value) => {
45
+ if (isArchivedToolResultPlaceholder(value))
46
+ return;
47
+ try {
48
+ addContent(decodePersistedToolResultContent(markPersisted(value)));
49
+ }
50
+ catch {
51
+ // Opaque tool results carry no typed StorageRef.
52
+ }
53
+ };
54
+ for (const message of input.messages) {
55
+ if (message.type === 'user' && message.attachments) {
56
+ for (const attachment of message.attachments)
57
+ refs.push(attachment.ref);
58
+ }
59
+ else if (message.type === 'tool_result') {
60
+ addContent(message.content);
61
+ }
62
+ }
63
+ for (const event of input.runtimeEvents) {
64
+ if (event.content?.kind === 'text' && event.content.attachments) {
65
+ for (const attachment of event.content.attachments)
66
+ refs.push(attachment.ref);
67
+ }
68
+ else if (event.content?.kind === 'function_response') {
69
+ addSerialized(event.content.result);
70
+ }
71
+ }
72
+ for (const serializedResult of input.archivedResults) {
73
+ addSerialized(deserializeToolResultArchive(serializedResult));
74
+ }
75
+ return refs;
76
+ }
77
+ /** Finds durable Session context references that the exact copy will rewrite. */
78
+ export function collectConversationCopySessionContextRefIds(input) {
79
+ const refIds = new Set();
80
+ for (const ref of collectConversationCopyStorageRefs(input)) {
81
+ if (ref.kind === 'session_context' && ref.sessionId === input.sourceSessionId) {
82
+ refIds.add(ref.refId);
83
+ }
84
+ }
85
+ return [...refIds].sort();
86
+ }
37
87
  export function createConversationCopySlice(messages, sourceTurnId, boundary) {
38
88
  const turnOrder = [];
39
89
  const seen = new Set();
@@ -373,7 +423,8 @@ export function archivedToolResultContainsConversationOwnedReferences(serialized
373
423
  if (content.kind === 'archived_tool_result')
374
424
  return true;
375
425
  if (content.kind === 'image') {
376
- return content.ref.kind === 'session_file' && content.ref.sessionId === sourceSessionId;
426
+ return ((content.ref.kind === 'session_file' || content.ref.kind === 'session_context') &&
427
+ content.ref.sessionId === sourceSessionId);
377
428
  }
378
429
  if (content.kind === 'subagent') {
379
430
  const [linked] = conversationCopyLinkedChildReferences(content);
@@ -473,45 +524,10 @@ export function collectConversationCopyLinkedChildReferences(input) {
473
524
  */
474
525
  export function collectConversationCopySessionFileRefs(input) {
475
526
  const refs = new Set();
476
- const addRef = (ref) => {
527
+ for (const ref of collectConversationCopyStorageRefs(input)) {
477
528
  if (ref.kind === 'session_file' && ref.sessionId === input.sourceSessionId) {
478
529
  refs.add(ref.relativePath);
479
530
  }
480
- };
481
- const addContent = (content) => {
482
- if (content.kind === 'image')
483
- addRef(content.ref);
484
- };
485
- const addSerialized = (value) => {
486
- if (isArchivedToolResultPlaceholder(value))
487
- return;
488
- try {
489
- addContent(decodePersistedToolResultContent(markPersisted(value)));
490
- }
491
- catch {
492
- // Opaque tool results carry no typed Session file reference.
493
- }
494
- };
495
- for (const message of input.messages) {
496
- if (message.type === 'user' && message.attachments) {
497
- for (const attachment of message.attachments)
498
- addRef(attachment.ref);
499
- }
500
- else if (message.type === 'tool_result') {
501
- addContent(message.content);
502
- }
503
- }
504
- for (const event of input.runtimeEvents) {
505
- if (event.content?.kind === 'text' && event.content.attachments) {
506
- for (const attachment of event.content.attachments)
507
- addRef(attachment.ref);
508
- }
509
- else if (event.content?.kind === 'function_response') {
510
- addSerialized(event.content.result);
511
- }
512
- }
513
- for (const serializedResult of input.archivedResults) {
514
- addSerialized(deserializeToolResultArchive(serializedResult));
515
531
  }
516
532
  return refs;
517
533
  }
@@ -1218,15 +1234,22 @@ function linkedChildReferencesAreExternal(linked, externalChildReferences) {
1218
1234
  linked.artifactIds.every((artifactId) => external.artifactIds.has(artifactId)));
1219
1235
  }
1220
1236
  function rewriteStorageRef(ref, references) {
1221
- if (ref.kind === 'session_context' && ref.sessionId === references.sourceSessionId) {
1222
- if (references.mode === 'preserve_external')
1223
- return ref;
1224
- throw new Error('Conversation copy does not support Session context references yet');
1225
- }
1226
- if (ref.kind !== 'session_file' || ref.sessionId !== references.sourceSessionId)
1237
+ if ((ref.kind !== 'session_file' && ref.kind !== 'session_context') ||
1238
+ ref.sessionId !== references.sourceSessionId) {
1227
1239
  return ref;
1240
+ }
1228
1241
  if (references.mode === 'preserve_external')
1229
1242
  return ref;
1243
+ if (ref.kind === 'session_context') {
1244
+ const refId = references.contextRefs?.get(ref.refId);
1245
+ if (!refId)
1246
+ throw new Error(`Conversation copy is missing Session context ${ref.refId}`);
1247
+ return {
1248
+ ...ref,
1249
+ sessionId: references.targetSessionId,
1250
+ refId,
1251
+ };
1252
+ }
1230
1253
  const artifactId = references.artifactIds.get(ref.relativePath);
1231
1254
  if (artifactId) {
1232
1255
  return {
@@ -121,6 +121,37 @@ export function durableProjectionToToolResultOutput(projection) {
121
121
  return { type: 'error-text', value: projection.message };
122
122
  }
123
123
  }
124
+ /** The images the artifact parts of one durable projection put in the request. */
125
+ export function projectionArtifactMedia(projection) {
126
+ if (projection.kind !== 'content')
127
+ return [];
128
+ return projection.parts.flatMap((part) => part.kind === 'artifact'
129
+ ? [
130
+ {
131
+ label: part.ref.kind === 'session_context' ? part.ref.refId : part.ref.relativePath,
132
+ },
133
+ ]
134
+ : []);
135
+ }
136
+ /**
137
+ * The images one Tool Result puts in the request: the artifact parts of its
138
+ * durable projection, or — for a pre-artifact image the decoder still hands to
139
+ * materialization raw — that legacy result.
140
+ */
141
+ export function effectiveToolResultMedia(effective, sessionId) {
142
+ if (effective.kind === 'projection')
143
+ return projectionArtifactMedia(effective.projection);
144
+ if (effective.kind !== 'legacy_output')
145
+ return [];
146
+ const image = sessionImageResult(effective.output, sessionId);
147
+ if (!image)
148
+ return [];
149
+ return [
150
+ {
151
+ label: image.ref.kind === 'session_context' ? image.ref.refId : image.ref.relativePath,
152
+ },
153
+ ];
154
+ }
124
155
  /**
125
156
  * The one pure decision of WHICH source a replayed Tool Result materializes
126
157
  * from: a durable projection wins, and only a response that has none (legacy
@@ -16,9 +16,17 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
- import { isContextBudgetExhaustedDetail, } from '@maka/core/events';
19
+ /**
20
+ * Ways a summarizer's own output can be unusable. Owned here, in the
21
+ * history-compaction domain that produces and repairs them.
22
+ */
23
+ const MALFORMED_HISTORY_COMPACT_SUMMARY_REASONS = [
24
+ 'malformed_summary_missing_section',
25
+ 'malformed_summary_truncated',
26
+ 'malformed_summary_too_small_for_fold',
27
+ ];
20
28
  export function isMalformedHistoryCompactSummaryReason(reason) {
21
- return isContextBudgetExhaustedDetail(reason) && reason.startsWith('malformed_summary_');
29
+ return MALFORMED_HISTORY_COMPACT_SUMMARY_REASONS.includes(reason);
22
30
  }
23
31
  export class HistoryCompactSummarizerError extends Error {
24
32
  reason;
@@ -38,6 +38,12 @@ export function fitHistoryCompactMessages(messages, options) {
38
38
  const message = bounded[messageIndex];
39
39
  if (message.role !== 'tool')
40
40
  continue;
41
+ // Accumulate replacements across the parts of this one tool message. A
42
+ // message that batches several tool results (one per parallel tool call)
43
+ // must keep every earlier placeholder, not just the last one — otherwise
44
+ // the returned history still carries full-size payloads even though the
45
+ // budget accounting already credited their removal.
46
+ let content = message.content;
41
47
  for (let partIndex = 0; partIndex < message.content.length; partIndex += 1) {
42
48
  const part = message.content[partIndex];
43
49
  if (part.type !== 'tool-result')
@@ -53,7 +59,7 @@ export function fitHistoryCompactMessages(messages, options) {
53
59
  const replacementChars = stableJsonLength(replacement);
54
60
  if (replacementChars >= originalChars)
55
61
  continue;
56
- const content = [...message.content];
62
+ content = [...content];
57
63
  content[partIndex] = replacement;
58
64
  bounded = [...bounded];
59
65
  bounded[messageIndex] = { ...message, content };
@@ -23,29 +23,24 @@ import { HistoryCompactSummarizerError, } from './history-compact-error.js';
23
23
  import { findCheckpointSummaryDefect } from './history-compact-summary-validation.js';
24
24
  import { buildHistoryCompactCheckpoint, historyCompactCheckpointToRuntimeEvent, matchHistoryCompactCheckpointPrefix, midTurnHeadAnchorEvent, projectHistoryCompactCheckpointReplay, } from './history-compact-checkpoint.js';
25
25
  /**
26
- * Estimate the token size of the next provider request. Anchors on the last
27
- * step's real usage plus a signed char/4 payload delta for content the provider
28
- * has not yet counted (or no longer carries); cold-start (no usage) is a pure
29
- * char/4 estimate of the whole payload. This mirrors how surveyed peers avoid
30
- * pure character guessing.
26
+ * Estimate the token size of the next provider request: the last request's real
27
+ * usage plus a signed char/4 payload delta for content the provider has not yet
28
+ * counted (or no longer carries). Without a usable usage sample the caller
29
+ * passes the whole payload as the delta against a zero baseline, so this stays
30
+ * one formula. This mirrors how surveyed peers avoid pure character guessing.
31
31
  */
32
32
  export function estimateNextRequestTokens(input) {
33
33
  const charsPerToken = Math.max(1, input.charsPerToken ?? 4);
34
- if (input.priorUsageTokens !== undefined && Number.isFinite(input.priorUsageTokens)) {
35
- return Math.max(0, Math.max(0, Math.floor(input.priorUsageTokens)) +
36
- estimateSignedChars(input.appendedChars, charsPerToken));
37
- }
38
- return Math.max(0, estimateSignedChars(input.coldStartChars ?? input.appendedChars, charsPerToken));
34
+ const prior = input.priorUsageTokens !== undefined && Number.isFinite(input.priorUsageTokens)
35
+ ? Math.max(0, Math.floor(input.priorUsageTokens))
36
+ : 0;
37
+ return Math.max(0, prior + estimateSignedChars(input.appendedChars, charsPerToken));
39
38
  }
40
39
  /** Proactive threshold: the next request would cross `contextWindow - reserve`. */
41
40
  export function exceedsHighWater(estimatedTokens, contextWindow, reserveTokens) {
42
41
  const highWater = Math.max(1, contextWindow - Math.max(0, reserveTokens));
43
42
  return estimatedTokens > highWater;
44
43
  }
45
- /** Hard cap: the estimate exceeds the raw context window even before the reserve. */
46
- export function exceedsContextWindow(estimatedTokens, contextWindow) {
47
- return estimatedTokens > contextWindow;
48
- }
49
44
  /**
50
45
  * Select the largest contiguous covered prefix that is safe to fold:
51
46
  *
@@ -55,8 +50,8 @@ export function exceedsContextWindow(estimatedTokens, contextWindow) {
55
50
  * - it leaves at least `reserveTailEvents` trailing events as the verbatim tail.
56
51
  *
57
52
  * Returns `no_safe_completed_span` when no such cut exists (e.g. the remaining
58
- * pool is a single atomic call/result pair), which the caller surfaces as an
59
- * explicit `context_budget_exhausted` outcome rather than a provider error.
53
+ * pool is a single atomic call/result pair); the caller then fails open and
54
+ * sends the request unchanged.
60
55
  */
61
56
  export function selectSafeCompactionPrefix(events, options = {}) {
62
57
  const reserveTail = Math.max(0, Math.floor(options.reserveTailEvents ?? 0));
@@ -129,12 +124,8 @@ function estimateSignedChars(chars, charsPerToken) {
129
124
  * Execute a triggered compaction command by deterministically folding the
130
125
  * largest safe prefix. Trigger policy is owned by callers; once this function
131
126
  * is called it always attempts the transaction. This plan is a pure shaper:
132
- * when it cannot fold a safe
133
- * completed prefix it FAILS OPEN (keep the raw projection + diagnostic) and
134
- * never terminates the turn itself. The two failure tiers — fail open under
135
- * the window, explicit `context_budget_exhausted` over it — are applied by the
136
- * backend's final-request estimate owner, which re-measures the actual outgoing
137
- * payload after all shaping (including this fold) has been applied.
127
+ * when it cannot fold a safe completed prefix it FAILS OPEN (keep the raw
128
+ * projection + diagnostic) and the request goes out unchanged.
138
129
  */
139
130
  export async function planHistoryCompaction(input) {
140
131
  const phase = input.phase ?? 'mid_turn';
@@ -52,7 +52,8 @@
52
52
  */
53
53
  import { isPartialRuntimeEvent, isTerminalRuntimeEvent, runtimeEventHasModelVisibleContent, } from '@maka/core/runtime-event';
54
54
  import { formatAttachmentResourceRef } from '@maka/core/attachments';
55
- import { decodeEffectiveToolResultProjection, durableProjectionToToolResultOutput, } from './durable-tool-result-projection.js';
55
+ import { decodeEffectiveToolResultProjection, durableProjectionToToolResultOutput, effectiveToolResultMedia, } from './durable-tool-result-projection.js';
56
+ import { MATERIALIZED_IMAGE_TOKENS } from '@maka/core/attachments';
56
57
  import { estimateTokens, stableJsonLength, turnKey } from './context-budget-helpers.js';
57
58
  export const PROVIDER_REPLAY_PROJECTION_VERSION = 2;
58
59
  /**
@@ -84,9 +85,9 @@ export function admitProviderReasoningReplayItems(items, providerReasoningReplay
84
85
  // Effective model-history sizing
85
86
  // ============================================================================
86
87
  /**
87
- * Model-visible character count of a `function_response`'s EFFECTIVE value —
88
- * the durable projection when the response has one, never the raw execution
89
- * fact the projection already bounded or redacted.
88
+ * Size of a `function_response`'s EFFECTIVE value — the durable projection
89
+ * when the response has one, never the raw execution fact the projection
90
+ * already bounded or redacted.
90
91
  *
91
92
  * Memoized on the content object because a legacy response (no durable
92
93
  * projection) is re-derived through the whole compatibility codec, and one
@@ -94,7 +95,7 @@ export function admitProviderReasoningReplayItems(items, providerReasoningReplay
94
95
  * compactable-content filter, and checkpoint prefix matching all walk the
95
96
  * history. Content is immutable once committed, so identity is a sound key.
96
97
  */
97
- const effectiveToolResultChars = new WeakMap();
98
+ const effectiveToolResultSizes = new WeakMap();
98
99
  /** Model-visible character count of one materialized Tool Result output. */
99
100
  function estimateToolResultOutputChars(output) {
100
101
  switch (output.type) {
@@ -110,18 +111,24 @@ function estimateToolResultOutputChars(output) {
110
111
  return output.reason?.length ?? 0;
111
112
  }
112
113
  }
113
- export function estimateEffectiveToolResultChars(content, sessionId) {
114
- const memoized = effectiveToolResultChars.get(content);
114
+ function effectiveToolResultSize(content, sessionId) {
115
+ const memoized = effectiveToolResultSizes.get(content);
115
116
  if (memoized !== undefined)
116
117
  return memoized;
117
118
  const effective = decodeEffectiveToolResultProjection(content, sessionId);
118
- const chars = effective.kind === 'projection'
119
- ? estimateToolResultOutputChars(durableProjectionToToolResultOutput(effective.projection))
120
- : effective.kind === 'invalid_legacy'
121
- ? effective.message.length
122
- : stableJsonLength(effective.output);
123
- effectiveToolResultChars.set(content, chars);
124
- return chars;
119
+ const size = {
120
+ chars: effective.kind === 'projection'
121
+ ? estimateToolResultOutputChars(durableProjectionToToolResultOutput(effective.projection))
122
+ : effective.kind === 'invalid_legacy'
123
+ ? effective.message.length
124
+ : stableJsonLength(effective.output),
125
+ mediaTokens: effectiveToolResultMedia(effective, sessionId).length * MATERIALIZED_IMAGE_TOKENS,
126
+ };
127
+ effectiveToolResultSizes.set(content, size);
128
+ return size;
129
+ }
130
+ export function estimateEffectiveToolResultChars(content, sessionId) {
131
+ return effectiveToolResultSize(content, sessionId).chars;
125
132
  }
126
133
  export function estimateRuntimeEventChars(event) {
127
134
  let total = 0;
@@ -136,9 +143,27 @@ export function estimateRuntimeEventChars(event) {
136
143
  total += content.message.length;
137
144
  return total;
138
145
  }
146
+ /**
147
+ * Tokens one event costs beyond its characters. Stays in tokens instead of
148
+ * folding into the char count: `charsPerToken` calibrates text, and applying it
149
+ * to media would make an image cheaper on a session with a low text ratio.
150
+ */
151
+ function estimateRuntimeEventMediaTokens(event) {
152
+ const content = event.content;
153
+ return content?.kind === 'function_response'
154
+ ? effectiveToolResultSize(content, event.sessionId).mediaTokens
155
+ : 0;
156
+ }
139
157
  export function estimateRuntimeEventsTokens(events, charsPerToken = 4) {
140
- const chars = events.reduce((total, event) => event.modelVisibility === 'hidden' ? total : total + estimateRuntimeEventChars(event), 0);
141
- return estimateTokens(chars, charsPerToken);
158
+ let chars = 0;
159
+ let mediaTokens = 0;
160
+ for (const event of events) {
161
+ if (event.modelVisibility === 'hidden')
162
+ continue;
163
+ chars += estimateRuntimeEventChars(event);
164
+ mediaTokens += estimateRuntimeEventMediaTokens(event);
165
+ }
166
+ return estimateTokens(chars, charsPerToken) + mediaTokens;
142
167
  }
143
168
  export function groupEventsByTurn(events, charsPerToken) {
144
169
  const order = [];
@@ -16,50 +16,31 @@
16
16
  * specific language governing permissions and limitations
17
17
  * under the License.
18
18
  */
19
+ import { decodeEffectiveToolResultProjection, effectiveToolResultMedia, } from './durable-tool-result-projection.js';
19
20
  function isRecord(value) {
20
21
  return value !== null && typeof value === 'object';
21
22
  }
22
- function storageRefLabel(value) {
23
- if (!isRecord(value) || typeof value.kind !== 'string')
24
- return undefined;
25
- if ((value.kind === 'session_file' || value.kind === 'workspace_file') &&
26
- typeof value.relativePath === 'string' &&
27
- value.relativePath.length > 0) {
28
- return value.relativePath;
29
- }
30
- if (value.kind === 'session_context' &&
31
- typeof value.refId === 'string' &&
32
- value.refId.length > 0) {
33
- return value.refId;
34
- }
35
- if (value.kind === 'external_file' &&
36
- typeof value.absolutePath === 'string' &&
37
- value.absolutePath.length > 0) {
38
- return value.absolutePath;
39
- }
40
- return undefined;
41
- }
23
+ /**
24
+ * The image Tool Results a rejected request can give back, read through the
25
+ * same decode the budget measures — not from the raw execution fact, which the
26
+ * durable projection may already have bounded or redacted.
27
+ */
42
28
  export function collectHistoricalImageToolResults(events) {
43
29
  const collected = new Map();
44
30
  for (const event of events) {
45
31
  const content = event.content;
46
32
  if (content?.kind !== 'function_response' || content.isError === true)
47
33
  continue;
48
- const result = content.result;
49
- if (!isRecord(result) ||
50
- result.kind !== 'image' ||
51
- typeof result.mimeType !== 'string' ||
52
- result.mimeType.length === 0) {
53
- continue;
54
- }
55
- const artifactLabel = storageRefLabel(result.ref);
56
- if (!artifactLabel)
34
+ const effective = decodeEffectiveToolResultProjection(content, event.sessionId);
35
+ const [media] = effectiveToolResultMedia(effective, event.sessionId);
36
+ if (!media || media.label.length === 0)
57
37
  continue;
58
- collected.set(content.id, { toolName: content.name, artifactLabel });
38
+ collected.set(content.id, { toolName: content.name, artifactLabel: media.label });
59
39
  }
60
40
  return collected;
61
41
  }
62
- function isInlineImageFilePart(value) {
42
+ /** A `file` part carrying inline image bytes, not a URL the provider fetches. */
43
+ export function isInlineImageFilePart(value) {
63
44
  if (!isRecord(value) ||
64
45
  value.type !== 'file' ||
65
46
  typeof value.mediaType !== 'string' ||
@@ -446,6 +446,9 @@ function tokenUsageFromMessage(message) {
446
446
  : {}),
447
447
  ...(message.promptSegments !== undefined ? { promptSegments: message.promptSegments } : {}),
448
448
  ...(message.contextBudget !== undefined ? { contextBudget: message.contextBudget } : {}),
449
+ ...(message.lastRequestAnchor !== undefined
450
+ ? { lastRequestAnchor: message.lastRequestAnchor }
451
+ : {}),
449
452
  };
450
453
  }
451
454
  function messageTurnId(message) {
@@ -742,6 +742,9 @@ function projectTokenUsage(event, state, messages) {
742
742
  : {}),
743
743
  ...(usage.promptSegments !== undefined ? { promptSegments: usage.promptSegments } : {}),
744
744
  ...(usage.contextBudget !== undefined ? { contextBudget: usage.contextBudget } : {}),
745
+ ...(usage.lastRequestAnchor !== undefined
746
+ ? { lastRequestAnchor: usage.lastRequestAnchor }
747
+ : {}),
745
748
  ...(event.refs?.providerRequestTraceId !== undefined
746
749
  ? { providerRequestTraceId: event.refs.providerRequestTraceId }
747
750
  : {}),
@@ -857,13 +860,20 @@ function abortSourceFromRuntime(event, header) {
857
860
  stringRecordValue(header, 'abortSource'));
858
861
  }
859
862
  function failureClassFromRuntimeEvent(event, header) {
860
- return (stringStateDelta(event, 'failureClass') ??
863
+ const failureClass = stringStateDelta(event, 'failureClass') ??
861
864
  stringStateDelta(event, 'errorClass') ??
862
865
  stringStateDelta(event, 'reason') ??
863
866
  stringStateDelta(event, 'code') ??
864
867
  (event.content?.kind === 'error' ? nonEmptyString(event.content.reason) : undefined) ??
865
868
  (event.content?.kind === 'error' ? nonEmptyString(event.content.code) : undefined) ??
866
- header.failureClass);
869
+ header.failureClass;
870
+ // Retired outcome. The runtime no longer decides locally that a request
871
+ // cannot be shaped to fit — the provider rejects it and recovery compacts and
872
+ // retries — so a turn that ends over the window is a context overflow like any
873
+ // other. Sessions written before that still carry the old name; fold it here,
874
+ // at the one place the durable ledger is read, so nothing downstream has to
875
+ // know two names for one outcome.
876
+ return failureClass === 'context_budget_exhausted' ? 'context_overflow' : failureClass;
867
877
  }
868
878
  function stringRecordValue(value, key) {
869
879
  if (!value || typeof value !== 'object')
@@ -1107,6 +1117,7 @@ function semanticMessage(message) {
1107
1117
  promptSegments: message.promptSegments,
1108
1118
  contextBudget: message.contextBudget,
1109
1119
  providerRequestTraceId: message.providerRequestTraceId,
1120
+ lastRequestAnchor: message.lastRequestAnchor,
1110
1121
  };
1111
1122
  case 'turn_state':
1112
1123
  return {