maka-agent 0.2.0-dev.77.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 (50) hide show
  1. package/README.zh-CN.md +2 -2
  2. package/dist/pi-tui-pickers.js +5 -2
  3. package/dist/pi-tui-runner.js +211 -33
  4. package/dist/runtime-host-session-driver.js +91 -13
  5. package/dist/session-catalog-limits.js +22 -0
  6. package/dist/session-driver.js +10 -0
  7. package/dist/tui-copy-catalog.js +15 -0
  8. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  9. package/node_modules/@maka/core/dist/bot-chat-settings.js +3 -0
  10. package/node_modules/@maka/core/dist/bot-events.js +1 -8
  11. package/node_modules/@maka/core/dist/local-memory.js +38 -1
  12. package/node_modules/@maka/core/dist/scheduled-task.js +14 -15
  13. package/node_modules/@maka/core/dist/session.js +3 -0
  14. package/node_modules/@maka/core/dist/text-sanitize.js +8 -3
  15. package/node_modules/@maka/runtime/dist/ai-sdk-backend.js +11 -0
  16. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +13 -5
  17. package/node_modules/@maka/runtime/dist/bots/proxied-fetch.js +29 -1
  18. package/node_modules/@maka/runtime/dist/file-stable-write.js +15 -3
  19. package/node_modules/@maka/runtime/dist/model-adapter.js +20 -1
  20. package/node_modules/@maka/runtime/dist/network/proxy-test.js +28 -1
  21. package/node_modules/@maka/runtime/dist/network/scoped-fetch-transport.js +13 -2
  22. package/node_modules/@maka/runtime/dist/runtime-kernel.js +92 -5
  23. package/node_modules/@maka/runtime/dist/sandbox-boundary-tool.js +12 -0
  24. package/node_modules/@maka/runtime/dist/session-manager.js +31 -5
  25. package/node_modules/@maka/runtime/dist/tool-availability.js +22 -16
  26. package/node_modules/@maka/runtime/dist/tool-call-snapshot.js +142 -0
  27. package/node_modules/@maka/runtime/dist/tool-runtime.js +25 -83
  28. package/node_modules/@maka/runtime/dist/workers/filesystem-worker.mjs +12 -1
  29. package/node_modules/@maka/runtime-host/dist/client/connect-or-spawn.js +30 -6
  30. package/node_modules/@maka/runtime-host/dist/client/index.js +1 -1
  31. package/node_modules/@maka/runtime-host/dist/protocol/index.js +2 -1
  32. package/node_modules/@maka/runtime-host/dist/protocol/message.js +1 -13
  33. package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +1 -13
  34. package/node_modules/@maka/runtime-host/dist/protocol/turn.js +1 -13
  35. package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +4 -7
  36. package/node_modules/@maka/runtime-host/dist/server/archive-retention-coordinator.js +81 -105
  37. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +17 -43
  38. package/node_modules/@maka/runtime-host/dist/server/host-maintenance-guards.js +95 -0
  39. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +18 -8
  40. package/node_modules/@maka/runtime-host/dist/server/interactive-run-composer.js +32 -3
  41. package/node_modules/@maka/runtime-host/dist/server/project-catalog-coordinator.js +2 -0
  42. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +68 -3
  43. package/node_modules/@maka/runtime-host/dist/server/skill-catalog-coordinator.js +2 -0
  44. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +19 -24
  45. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +74 -19
  46. package/node_modules/@maka/runtime-host/dist/server/workhub-result-coordinator.js +1 -1
  47. package/node_modules/@maka/runtime-host/dist/server/workhub-result-runtime.js +3 -98
  48. package/node_modules/@maka/runtime-host/dist/server/workhub-target-execution-authority.js +14 -126
  49. package/node_modules/@maka/storage/dist/git-entry.js +142 -5
  50. package/package.json +1 -1
@@ -130,14 +130,7 @@ export function plaintextHelpReply() {
130
130
  ].join('\n');
131
131
  }
132
132
  export function formatBotMessageForSession(message) {
133
- return `[${botDisplayLabel(message.platform)}:${sanitizeBotUserName(message.userName)}] ${message.text.trim()}`;
134
- }
135
- function sanitizeBotUserName(value) {
136
- return (value
137
- .replace(/[\u0000-\u001F\u007F-\u009F]+/g, ' ')
138
- .replace(/[\p{Cf}]+/gu, ' ')
139
- .replace(/\s+/g, ' ')
140
- .trim() || 'unknown');
133
+ return `[${botDisplayLabel(message.platform)}] ${message.text.trim()}`;
141
134
  }
142
135
  /**
143
136
  * Non-error `BotStatus.reason` values. These states have their own UI surface,
@@ -318,8 +318,11 @@ export function findLocalMemoryEntryDraftRange(input, entryId) {
318
318
  end: lineStarts[endLineIndex] ?? input.length,
319
319
  };
320
320
  };
321
+ const isCodeFenceLine = createMemoryCodeFenceTracker();
321
322
  for (let index = 0; index < lines.length; index += 1) {
322
323
  const line = lines[index] ?? '';
324
+ if (isCodeFenceLine(line))
325
+ continue;
323
326
  const heading = /^##\s+(.+?)\s*$/.exec(line);
324
327
  if (heading) {
325
328
  const matched = matchCurrent(index);
@@ -421,7 +424,12 @@ function parseLocalMemoryMarkdownRaw(input) {
421
424
  }
422
425
  current = null;
423
426
  };
427
+ const isCodeFenceLine = createMemoryCodeFenceTracker();
424
428
  for (const line of lines) {
429
+ if (isCodeFenceLine(line)) {
430
+ current?.body.push(line);
431
+ continue;
432
+ }
425
433
  const heading = /^##\s+(.+?)\s*$/.exec(line);
426
434
  if (heading) {
427
435
  flush();
@@ -455,8 +463,29 @@ function stripPromptContent(entry) {
455
463
  const { promptContent: _promptContent, ...preview } = entry;
456
464
  return preview;
457
465
  }
466
+ // All entry readers and editors share fence state so code examples cannot split entries.
467
+ function createMemoryCodeFenceTracker() {
468
+ let fence;
469
+ return (line) => {
470
+ const match = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
471
+ if (fence) {
472
+ if (match &&
473
+ match[1][0] === fence.marker &&
474
+ match[1].length >= fence.length &&
475
+ match[2].trim() === '') {
476
+ fence = undefined;
477
+ }
478
+ return true;
479
+ }
480
+ if (!match || (match[1][0] === '`' && match[2].includes('`')))
481
+ return false;
482
+ fence = { marker: match[1][0], length: match[1].length };
483
+ return true;
484
+ };
485
+ }
458
486
  function parseMetaComment(line) {
459
- const match = /^<!--\s*maka-memory:\s*(.*?)\s*-->$/.exec(line.trim());
487
+ // Four-space or tab indentation denotes a code example, not entry identity or state.
488
+ const match = /^ {0,3}<!--\s*maka-memory:\s*(.*?)\s*-->[ \t]*$/.exec(line);
460
489
  if (!match)
461
490
  return null;
462
491
  const meta = {};
@@ -482,8 +511,11 @@ function findLocalMemoryEntrySection(input, entryId) {
482
511
  const proposalId = current.meta?.proposalId;
483
512
  return id === entryId || proposalId === entryId ? { id, ...current } : null;
484
513
  };
514
+ const isCodeFenceLine = createMemoryCodeFenceTracker();
485
515
  for (let index = 0; index < lines.length; index += 1) {
486
516
  const line = lines[index] ?? '';
517
+ if (isCodeFenceLine(line))
518
+ continue;
487
519
  const heading = /^##\s+(.+?)\s*$/.exec(line);
488
520
  if (heading) {
489
521
  const matched = matchCurrent();
@@ -534,8 +566,13 @@ function findLocalMemoryEntryFullSection(input, entryId) {
534
566
  },
535
567
  };
536
568
  };
569
+ const isCodeFenceLine = createMemoryCodeFenceTracker();
537
570
  for (let index = 0; index < lines.length; index += 1) {
538
571
  const line = lines[index] ?? '';
572
+ if (isCodeFenceLine(line)) {
573
+ current?.body.push(line);
574
+ continue;
575
+ }
539
576
  const heading = /^##\s+(.+?)\s*$/.exec(line);
540
577
  if (heading) {
541
578
  const matched = matchCurrent(index);
@@ -493,23 +493,22 @@ function asFiniteNumber(value) {
493
493
  return value;
494
494
  return null;
495
495
  }
496
+ /**
497
+ * Calendar recurrence uses Date's local-time rules: shift gaps forward and take
498
+ * the first repeated time. Rebuild from the anchor so gap shifts do not carry
499
+ * into later cycles; cron instead skips nonexistent local times.
500
+ */
496
501
  function nextCalendarFireAt(schedule, after) {
497
502
  const anchor = new Date(schedule.anchorAt);
498
- if (schedule.recurrence === 'daily') {
499
- const next = new Date(after);
500
- next.setHours(anchor.getHours(), anchor.getMinutes(), anchor.getSeconds(), anchor.getMilliseconds());
501
- if (next.getTime() <= after)
502
- next.setDate(next.getDate() + 1);
503
- return next.getTime();
504
- }
505
- if (schedule.recurrence === 'weekly') {
506
- const next = new Date(after);
507
- next.setHours(anchor.getHours(), anchor.getMinutes(), anchor.getSeconds(), anchor.getMilliseconds());
508
- const dayDelta = (anchor.getDay() - next.getDay() + 7) % 7;
509
- next.setDate(next.getDate() + dayDelta);
510
- if (next.getTime() <= after)
511
- next.setDate(next.getDate() + 7);
512
- return next.getTime();
503
+ if (schedule.recurrence === 'daily' || schedule.recurrence === 'weekly') {
504
+ const base = new Date(after);
505
+ const dayOffset = schedule.recurrence === 'daily' ? 0 : (anchor.getDay() - base.getDay() + 7) % 7;
506
+ const periodDays = schedule.recurrence === 'daily' ? 1 : 7;
507
+ // Build each occurrence from the anchor: a DST gap may normalize today's
508
+ // wall time, but that adjusted hour must not carry into another date.
509
+ const occurrence = (offset) => new Date(base.getFullYear(), base.getMonth(), base.getDate() + offset, anchor.getHours(), anchor.getMinutes(), anchor.getSeconds(), anchor.getMilliseconds()).getTime();
510
+ const next = occurrence(dayOffset);
511
+ return next > after ? next : occurrence(dayOffset + periodDays);
513
512
  }
514
513
  const afterDate = new Date(after);
515
514
  for (let offset = 0; offset <= 480; offset += 1) {
@@ -467,6 +467,7 @@ const WORKHUB_DELEGATION_ASSIGNED_MESSAGE_SHAPE = defineObjectShape()([
467
467
  'attachments',
468
468
  'targetAttachments',
469
469
  'returnResults',
470
+ 'executionPermissionMode',
470
471
  'create',
471
472
  'steered',
472
473
  'replacesActionId',
@@ -797,6 +798,8 @@ function isWorkHubCoordinationMessage(message) {
797
798
  typeof message.targetSessionName === 'string' &&
798
799
  message.targetSessionName.trim().length > 0 &&
799
800
  (message.returnResults === undefined || message.returnResults === true) &&
801
+ (message.executionPermissionMode === undefined ||
802
+ isPermissionMode(message.executionPermissionMode)) &&
800
803
  (message.steered === undefined || message.steered === true) &&
801
804
  ((message.schemaVersion === WORKHUB_COORDINATION_RECORD_SCHEMA_VERSION &&
802
805
  message.replacesActionId === undefined &&
@@ -84,6 +84,11 @@ const ZERO_WIDTH_REGEX = /[\u200B-\u200D\u2060-\u2064\uFEFF]/g;
84
84
  */
85
85
  export function sanitizeUnicodeText(text, opts) {
86
86
  const suffix = opts.truncatedSuffix ?? '…';
87
+ // A negative cap is clamped to zero: without it the loop never breaks (the
88
+ // whole input is expanded) and the slice's negative end keeps nearly the
89
+ // whole string — the opposite of the "hard cap" this option promises. Same
90
+ // boundary contract as truncateUtf16Safe's `maxUnits <= 0` guard.
91
+ const maxCodePoints = Math.max(0, opts.maxCodePoints);
87
92
  const cleaned = text
88
93
  .normalize('NFC')
89
94
  .replace(CONTROL_CHARS_REGEX, ' ')
@@ -95,12 +100,12 @@ export function sanitizeUnicodeText(text, opts) {
95
100
  for (const point of cleaned) {
96
101
  points.push(point);
97
102
  // One extra point detects truncation without expanding the entire input.
98
- if (opts.maxCodePoints >= 0 && points.length > opts.maxCodePoints)
103
+ if (points.length > maxCodePoints)
99
104
  break;
100
105
  }
101
- if (points.length <= opts.maxCodePoints)
106
+ if (points.length <= maxCodePoints)
102
107
  return cleaned;
103
- return points.slice(0, opts.maxCodePoints).join('') + suffix;
108
+ return points.slice(0, maxCodePoints).join('') + suffix;
104
109
  }
105
110
  /**
106
111
  * Truncate to at most `maxUnits` UTF-16 code units without ending on an
@@ -90,6 +90,16 @@ export class AiSdkBackend {
90
90
  * read back as absent.
91
91
  */
92
92
  activeTurns = new Set();
93
+ /**
94
+ * Deferred-tool activations for this Session backend. `tool_search` only
95
+ * adds names; Turn end and compaction do not clear them. Clearing at Turn
96
+ * end would change the provider tool list and miss the prompt cache on the
97
+ * next Turn. Compaction already invalidates that cache, so clearing there
98
+ * would change the prefix a second time. Concurrent Runs on this backend
99
+ * share the map, so a search in one Run is visible on a sibling Run's next
100
+ * step. A rebuilt backend starts empty.
101
+ */
102
+ sessionActiveTools = new Map();
93
103
  compaction;
94
104
  turnSessionState = {};
95
105
  constructor(input) {
@@ -254,6 +264,7 @@ export class AiSdkBackend {
254
264
  providerTelemetry: this.providerTelemetry,
255
265
  compaction: this.compaction,
256
266
  snapshotToolAvailability: () => this.snapshotToolAvailability(),
267
+ sessionActiveTools: this.sessionActiveTools,
257
268
  codeCellAdmission: this.codeCellAdmission,
258
269
  resolvedProviderOptions: this.resolvedProviderOptions,
259
270
  session: this.turnSessionState,
@@ -37,7 +37,7 @@ import { contextDiagnosticsCompactionOf, } from './context-diagnostics.js';
37
37
  import { hasBlockingReplayDiagnostics } from './ai-sdk-compaction.js';
38
38
  import { portableApplyPatchTool } from './apply-patch-profile.js';
39
39
  import { RunTrace } from './run-trace.js';
40
- import { REQUEST_SANDBOX_BOUNDARY_TOOL_NAME, SANDBOX_BOUNDARY_DENIED_FOR_TURN, SANDBOX_BOUNDARY_FINALIZATION_PROMPT, } from './sandbox-boundary-tool.js';
40
+ import { REQUEST_SANDBOX_BOUNDARY_TOOL_NAME, SANDBOX_BOUNDARY_DENIED_FOR_TURN, SANDBOX_BOUNDARY_FINALIZATION_PROMPT, requiredSandboxBoundaryToolNames, } from './sandbox-boundary-tool.js';
41
41
  import { buildRuntimeEventModelReplayPlan, appendPriorUnknownToolResponses, buildSteeringEnvelope, collectToolActivityTurnIds, compatibleProviderReasoningReplayEventIds, formatTextWithInlineRefs, inspectPriorUnknownToolOutcomes, steeringMessagesMissingFromBase, steeringModelMessage, } from './model-history.js';
42
42
  import { toolSchemaCharsForDiagnostics, requestCompositionToolSchemas, stableHash, toolCatalogHash, } from './request-shape.js';
43
43
  import { toolAvailabilityHash } from './tool-availability.js';
@@ -374,13 +374,15 @@ function providerRetryDelayMs(failedAttempt, retryAfterMs) {
374
374
  *
375
375
  * Each turn owns its ToolRuntime for the same reason: gating, the loop gate,
376
376
  * the subagent and child-run limiters, durable attempts, and step admission are
377
- * all per-turn facts.
377
+ * all per-turn facts. Deferred-tool activation is the exception: it lives on
378
+ * the Session backend so later Turns — and concurrent Runs on the same
379
+ * backend — keep the same provider tool list.
378
380
  */
379
381
  export class AiSdkTurn {
380
382
  deps;
381
383
  request;
382
384
  abortController = new AbortController();
383
- activeTools = new Map();
385
+ activeTools;
384
386
  aborted = false;
385
387
  loopStopRequested = false;
386
388
  loopStopReason;
@@ -412,6 +414,7 @@ export class AiSdkTurn {
412
414
  request.orchestration ??
413
415
  resolveEffectiveOrchestration(deps.backend.header.orchestrationMode, undefined);
414
416
  this.toolRuntime = deps.createToolRuntime(this);
417
+ this.activeTools = deps.sessionActiveTools;
415
418
  }
416
419
  async *run() {
417
420
  yield* this.runWithinScope(this.request);
@@ -805,7 +808,7 @@ export class AiSdkTurn {
805
808
  }
806
809
  // --- Build the provider-visible schema set. Tool execution stays in Runtime. ---
807
810
  // Each logical step freezes its own scoped catalog and search projection.
808
- // Mutable activation belongs to this turn and follows contribution identity.
811
+ // Mutable activation is the Session backend map and follows contribution identity.
809
812
  const requiredOrchestrationTools = this.orchestration.mode === 'swarm'
810
813
  ? new Set([
811
814
  'agent_list',
@@ -834,7 +837,11 @@ export class AiSdkTurn {
834
837
  if (toolMode === 'code_mode' && snapshot.hostTools.some((tool) => tool.name === 'exec')) {
835
838
  throw new Error('Tool name "exec" is reserved for Code Mode.');
836
839
  }
837
- const basePlan = snapshot.runtime.prepare(this.activeTools, requiredOrchestrationTools);
840
+ const requiredTools = new Set([
841
+ ...requiredOrchestrationTools,
842
+ ...requiredSandboxBoundaryToolNames(snapshot.hostTools),
843
+ ]);
844
+ const basePlan = snapshot.runtime.prepare(this.activeTools, requiredTools);
838
845
  const nestedTools = nestableToolSnapshot(basePlan.providerTools, basePlan.activeTools);
839
846
  const plan = projectToolModePlan(basePlan, toolMode, codeModeExecTool);
840
847
  const modelTools = {};
@@ -1231,6 +1238,7 @@ export class AiSdkTurn {
1231
1238
  result = await this.deps.modelAdapter.startStream({
1232
1239
  model,
1233
1240
  messages: dispatchMessages,
1241
+ historyMessageCount: attemptMessages.length,
1234
1242
  tools: modelTools,
1235
1243
  activeTools: activeToolsForRequest,
1236
1244
  onStreamActivity: () => requestWatchdog?.markActivity(),
@@ -22,6 +22,11 @@ import { buildProxyDispatcher } from '../network/proxy-dispatcher.js';
22
22
  import { resolveActiveProxy } from '../network/active-proxy-state.js';
23
23
  import { FETCH_PROXY_SNAPSHOT } from '../network/scoped-fetch-transport.js';
24
24
  const DEFAULT_TIMEOUT_MS = 15_000;
25
+ // Bounded wait for dispatcher teardown in the error path: a graceful close
26
+ // that never settles must not delay the rejection the caller is owed. The
27
+ // success path keeps its fire-and-forget close, which owns the streaming
28
+ // body lifecycle and is not bounded here.
29
+ const DISPATCHER_CLOSE_GRACE_MS = 1_000;
25
30
  export async function proxiedFetch(input, init = {}) {
26
31
  const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
27
32
  const proxy = resolveActiveProxy();
@@ -70,8 +75,31 @@ export async function proxiedFetch(input, init = {}) {
70
75
  catch (error) {
71
76
  if (timer)
72
77
  clearTimeout(timer);
78
+ // Mirror the shared transport teardown ordering: begin dispatcher
79
+ // teardown before aborting so the graceful close can retire live
80
+ // CONNECT tunnels, with abort still cancelling pending connects. Keep
81
+ // the wait bounded so a stalled close cannot hold the rejection.
82
+ const teardown = disposeDispatcher(timedOut);
73
83
  controller.abort(error);
74
- await disposeDispatcher(timedOut);
84
+ let graceTimer;
85
+ const graceExpired = await Promise.race([
86
+ teardown.then(() => false),
87
+ new Promise((resolve) => {
88
+ graceTimer = setTimeout(() => resolve(true), DISPATCHER_CLOSE_GRACE_MS);
89
+ }),
90
+ ]);
91
+ if (graceTimer)
92
+ clearTimeout(graceTimer);
93
+ if (graceExpired) {
94
+ // The graceful close never settled within the grace, so destroy the
95
+ // dispatcher instead of leaking it. Fire-and-forget with the error
96
+ // swallowed: teardown must never replace the rejection the caller is
97
+ // owed.
98
+ const disposable = dispatcher;
99
+ if (typeof disposable.destroy === 'function') {
100
+ void Promise.resolve(disposable.destroy.call(dispatcher, new Error('Dispatcher close grace expired'))).catch(() => { });
101
+ }
102
+ }
75
103
  throw error;
76
104
  }
77
105
  // Headers are in: return the original Fetch Response now. close() is
@@ -134,13 +134,25 @@ async function openExistingNoTruncate(path) {
134
134
  * only after the identity was validated on the fd. Write-step failures
135
135
  * (ENOSPC/EIO/EDQUOT/EFBIG) can leave the file truncated or half-written, so
136
136
  * they surface as `outcome_unknown` — the file's state is genuinely unknown.
137
+ * Short writes complete the remaining bytes; zero progress also reports outcome_unknown.
137
138
  */
138
139
  export async function writeThroughHandle(handle, content) {
140
+ const bytes = Buffer.from(content, 'utf8');
139
141
  try {
140
142
  await handle.truncate(0);
141
- // Position 0 explicitly: a prior readFile leaves the fd position at EOF,
142
- // and a positionless write would create a NUL-prefixed sparse file.
143
- await handle.write(content, 0, 'utf8');
143
+ // Use an explicit offset from zero: a previous readFile may leave the fd at
144
+ // EOF, where an implicit write would create a sparse prefix.
145
+ // A successful write may consume only part of the buffer. Keep byte
146
+ // offsets (not string indices) so a short write inside UTF-8 is lossless.
147
+ let offset = 0;
148
+ while (offset < bytes.length) {
149
+ const { bytesWritten } = await handle.write(bytes, offset, bytes.length - offset, offset);
150
+ if (bytesWritten <= 0) {
151
+ throw new StableWriteFailure('outcome_unknown', 'The write stopped making progress; the file may be truncated. ' +
152
+ 'Re-read the file before writing to it again.');
153
+ }
154
+ offset += bytesWritten;
155
+ }
144
156
  }
145
157
  catch (error) {
146
158
  const code = error.code;
@@ -181,7 +181,8 @@ export class ModelAdapter {
181
181
  sdkTools[TOOL_SEARCH_PROVIDER_NAME] = sdkTools[TOOL_SEARCH_NAME];
182
182
  delete sdkTools[TOOL_SEARCH_NAME];
183
183
  }
184
- const fullMessages = this.runtime.wire === 'openai-chat' ? lowerChatToolImages(input.messages) : input.messages;
184
+ const messages = withAnthropicHistoryCacheBreakpoint(input.messages, input.historyMessageCount, this.input.providerOptions);
185
+ const fullMessages = this.runtime.wire === 'openai-chat' ? lowerChatToolImages(messages) : messages;
185
186
  const responsesLane = input.continuationKey && usesNativeOpenAiResponses(this.input.connection, this.runtime)
186
187
  ? input.continuationKey
187
188
  : undefined;
@@ -895,6 +896,24 @@ function translateChunk(chunk, openAiChatReasoningTransportState, runtime, runti
895
896
  return [];
896
897
  }
897
898
  }
899
+ function withAnthropicHistoryCacheBreakpoint(messages, historyMessageCount, providerOptions) {
900
+ const cacheControl = providerOptions?.anthropic
901
+ ?.cacheControl;
902
+ if (cacheControl === undefined ||
903
+ historyMessageCount === undefined ||
904
+ historyMessageCount >= messages.length) {
905
+ return messages;
906
+ }
907
+ return messages.map((message, index) => index === historyMessageCount - 1
908
+ ? {
909
+ ...message,
910
+ providerOptions: {
911
+ ...message.providerOptions,
912
+ anthropic: { ...message.providerOptions?.anthropic, cacheControl },
913
+ },
914
+ }
915
+ : message);
916
+ }
898
917
  function lowerChatToolImages(messages) {
899
918
  const result = [];
900
919
  let images = [];
@@ -21,6 +21,10 @@ import { buildProxyDispatcher } from './proxy-dispatcher.js';
21
21
  import { fetch } from 'undici';
22
22
  const DEFAULT_PROBE_URL = 'https://icanhazip.com';
23
23
  const DEFAULT_TIMEOUT_MS = 8_000;
24
+ // Bounded wait for dispatcher teardown: a graceful close that never settles
25
+ // (observed behind real proxies after an abort) must not keep this call —
26
+ // and the serialized settings lane it runs on — pending forever.
27
+ const DISPATCHER_CLOSE_GRACE_MS = 1_000;
24
28
  export async function testProxyConnection(input = {}, storedProxy) {
25
29
  const proxy = parseProxyConfig(input.proxy ?? storedProxy);
26
30
  const probeUrl = input.url ?? DEFAULT_PROBE_URL;
@@ -81,8 +85,31 @@ export async function testProxyConnection(input = {}, storedProxy) {
81
85
  };
82
86
  }
83
87
  finally {
88
+ // Mirror the shared transport teardown ordering: begin dispatcher
89
+ // teardown before aborting, so the graceful close can retire live
90
+ // CONNECT tunnels before the signal kills their sockets; abort still
91
+ // cancels any pending connect or handshake. Keep the wait bounded so a
92
+ // stalled close cannot hold this call past its result.
93
+ const teardown = disposeDispatcher(timedOut);
84
94
  controller.abort();
85
- await disposeDispatcher(timedOut);
95
+ let graceTimer;
96
+ const graceExpired = await Promise.race([
97
+ teardown.then(() => false),
98
+ new Promise((resolve) => {
99
+ graceTimer = setTimeout(() => resolve(true), DISPATCHER_CLOSE_GRACE_MS);
100
+ }),
101
+ ]);
102
+ if (graceTimer)
103
+ clearTimeout(graceTimer);
104
+ if (graceExpired) {
105
+ // The graceful close never settled within the grace, so destroy the
106
+ // dispatcher instead of leaking it. Fire-and-forget with the error
107
+ // swallowed: teardown must never replace the result this call owes.
108
+ const disposable = dispatcher;
109
+ if (typeof disposable.destroy === 'function') {
110
+ void Promise.resolve(disposable.destroy.call(dispatcher, new Error('Dispatcher close grace expired'))).catch(() => { });
111
+ }
112
+ }
86
113
  }
87
114
  }
88
115
  async function lookupCountry(ip, dispatcher, signal) {
@@ -21,6 +21,7 @@ import { matchesBypassList } from './bypass-matcher.js';
21
21
  import { buildProxyDispatcher } from './proxy-dispatcher.js';
22
22
  import { buildAbortableConnector } from './abortable-connector.js';
23
23
  export const FETCH_PROXY_SNAPSHOT = Symbol.for('maka.fetch.proxy-snapshot');
24
+ const TRANSPORT_CLOSE_GRACE_MS = 1_000;
24
25
  export function inheritFetchProxySnapshot(fetch, source) {
25
26
  const descriptor = Object.getOwnPropertyDescriptor(source, FETCH_PROXY_SNAPSHOT);
26
27
  if (descriptor)
@@ -62,8 +63,7 @@ export function createProxiedFetchTransport(proxy) {
62
63
  if (closePromise)
63
64
  return closePromise;
64
65
  closed = true;
65
- connections.abort(new Error('Connection effect fetch transport closed'));
66
- closePromise = Promise.all([
66
+ const destroyed = Promise.all([
67
67
  directDispatcher
68
68
  .destroy(new Error('Connection effect fetch transport closed'))
69
69
  .catch(() => { }),
@@ -71,6 +71,17 @@ export function createProxiedFetchTransport(proxy) {
71
71
  ?.destroy(new Error('Connection effect fetch transport closed'))
72
72
  .catch(() => { }),
73
73
  ]).then(() => undefined);
74
+ // Let dispatchers begin teardown before aborting CONNECT tunnels, so Undici
75
+ // can retire HTTP/2 sessions before their sockets disappear. Abort still
76
+ // cancels pending connects and handshakes; retain a bound for stalled teardown.
77
+ connections.abort(new Error('Connection effect fetch transport closed'));
78
+ let deadline;
79
+ closePromise = Promise.race([
80
+ destroyed,
81
+ new Promise((resolve) => {
82
+ deadline = setTimeout(resolve, TRANSPORT_CLOSE_GRACE_MS);
83
+ }),
84
+ ]).finally(() => clearTimeout(deadline));
74
85
  return closePromise;
75
86
  };
76
87
  return { fetch, close };
@@ -21,6 +21,7 @@ import { randomUUID } from 'node:crypto';
21
21
  import { decodeRuntimeBoundaryCursor, } from '@maka/core/runtime-boundary';
22
22
  import { isTerminalRuntimeEvent, } from '@maka/core/runtime-event';
23
23
  import { isDeepStrictEqual } from 'node:util';
24
+ import { createGenesisExecutionBoundary } from '@maka/core/sandbox-boundary';
24
25
  import { resolveEffectiveOrchestration, } from '@maka/core/orchestration';
25
26
  import { AgentRun, ContinuationStartCommitError, } from './agent-run.js';
26
27
  import { createSessionEventMapMemory, isLiveBackendSessionEvent, mapSessionEventToRuntimeEvent, } from './session-event-runtime-mapper.js';
@@ -320,7 +321,17 @@ export class RuntimeKernel {
320
321
  const execution = this.takeExecutionClaim(sessionId, options.execution);
321
322
  try {
322
323
  await this.enterExecutionClaim(execution);
323
- const header = await this.readBackendHeader(execution);
324
+ execution.executionPolicy = options.executionPolicy;
325
+ const existing = this.active.get(sessionId);
326
+ if (existing && !isDeepStrictEqual(existing.executionPolicy, options.executionPolicy)) {
327
+ if (existing.activeRuns.size > 0)
328
+ throw new Error('Cannot change execution policy while a Turn is running');
329
+ await this.disposeBackend(sessionId);
330
+ }
331
+ const storedHeader = await this.readBackendHeader(execution);
332
+ const header = options.executionPolicy
333
+ ? { ...storedHeader, permissionMode: options.executionPolicy.permissionMode }
334
+ : storedHeader;
324
335
  let workspaceIdentity;
325
336
  if (this.deps.inspectContinuationSafety) {
326
337
  try {
@@ -355,7 +366,12 @@ export class RuntimeKernel {
355
366
  return active;
356
367
  },
357
368
  unregisterRun: (active, activeRun) => this.unregisterParentRun(active, activeRun),
358
- updateHeader: (targetSessionId, patch) => this.updateHeader(targetSessionId, patch),
369
+ updateHeader: async (targetSessionId, patch) => {
370
+ const updated = await this.updateHeader(targetSessionId, patch);
371
+ return options.executionPolicy
372
+ ? { ...updated, permissionMode: options.executionPolicy.permissionMode }
373
+ : updated;
374
+ },
359
375
  updateStatus: (targetSessionId, status, blockedReason, ts) => this.updateStatus(targetSessionId, status, blockedReason, ts),
360
376
  ...this.messageProjectionHook(),
361
377
  },
@@ -410,7 +426,14 @@ export class RuntimeKernel {
410
426
  (this.executionClaims.get(continuation.sessionId)?.size ?? 0) > 1) {
411
427
  throw new Error('Cannot continue while another run is active');
412
428
  }
413
- const header = await this.readBackendHeader(execution);
429
+ execution.executionPolicy = options.executionPolicy;
430
+ const existing = this.active.get(continuation.sessionId);
431
+ if (existing && !isDeepStrictEqual(existing.executionPolicy, options.executionPolicy))
432
+ await this.disposeBackend(continuation.sessionId);
433
+ const storedHeader = await this.readBackendHeader(execution);
434
+ const header = options.executionPolicy
435
+ ? { ...storedHeader, permissionMode: options.executionPolicy.permissionMode }
436
+ : storedHeader;
414
437
  const sessionRuns = await this.deps.runtimeEventStore.listSessionInvocations(continuation.sessionId);
415
438
  const sourceRun = sessionRuns.find((run) => run.runId === continuation.sourceRunId);
416
439
  if (!sourceRun) {
@@ -564,7 +587,12 @@ export class RuntimeKernel {
564
587
  return active;
565
588
  },
566
589
  unregisterRun: (active, activeRun) => this.unregisterParentRun(active, activeRun),
567
- updateHeader: (targetSessionId, patch) => this.updateHeader(targetSessionId, patch),
590
+ updateHeader: async (targetSessionId, patch) => {
591
+ const updated = await this.updateHeader(targetSessionId, patch);
592
+ return options.executionPolicy
593
+ ? { ...updated, permissionMode: options.executionPolicy.permissionMode }
594
+ : updated;
595
+ },
568
596
  updateStatus: (targetSessionId, status, blockedReason, ts) => this.updateStatus(targetSessionId, status, blockedReason, ts),
569
597
  ...this.messageProjectionHook(),
570
598
  },
@@ -1689,6 +1717,32 @@ export class RuntimeKernel {
1689
1717
  hasActiveRun(sessionId, runId, turnId) {
1690
1718
  return this.activeRunsFor(sessionId).some((run) => run.runId === runId && (turnId === undefined || run.turnId === turnId));
1691
1719
  }
1720
+ readExecutionPolicy(sessionId, runId) {
1721
+ for (const generation of this.backendGenerationsFor(sessionId)) {
1722
+ if (generation.activeRuns.has(runId))
1723
+ return generation.executionPolicy;
1724
+ }
1725
+ return [...(this.executionClaims.get(sessionId) ?? [])].find((execution) => execution.run?.runId === runId)?.executionPolicy;
1726
+ }
1727
+ returnExecutionQuestions(sessionId, runId) {
1728
+ for (const generation of this.backendGenerationsFor(sessionId)) {
1729
+ const run = generation.activeRuns.get(runId);
1730
+ if (!run)
1731
+ continue;
1732
+ const policy = {
1733
+ permissionMode: run.headerSnapshot().permissionMode,
1734
+ questions: 'return',
1735
+ };
1736
+ generation.executionPolicy = policy;
1737
+ for (const execution of this.executionClaims.get(sessionId) ?? []) {
1738
+ if (execution.run?.runId === runId)
1739
+ execution.executionPolicy = policy;
1740
+ }
1741
+ return;
1742
+ }
1743
+ // A terminal backend can precede the Host's terminal admission. Its queued
1744
+ // steering is recovered into a successor with the durable delegated policy.
1745
+ }
1692
1746
  requestRunHandoff(sessionId, runId, pause, signal) {
1693
1747
  if (!this.deps.inspectContinuationSafety)
1694
1748
  return undefined;
@@ -1987,11 +2041,17 @@ export class RuntimeKernel {
1987
2041
  }));
1988
2042
  execution.run?.bindProviderStateIdentity(prepared.providerStateIdentity);
1989
2043
  const subagent = await this.resolveSubagentActivation(header);
2044
+ const kernel = this;
1990
2045
  const backend = await prepared.build({
1991
2046
  sessionId,
1992
2047
  workspaceRoot: header.workspaceRoot,
1993
2048
  header,
1994
- store: this.deps.store,
2049
+ store: execution.executionPolicy
2050
+ ? this.executionPolicyStore(sessionId, execution.executionPolicy)
2051
+ : this.deps.store,
2052
+ get executionPolicy() {
2053
+ return kernel.active.get(sessionId)?.executionPolicy ?? execution.executionPolicy;
2054
+ },
1995
2055
  abortSignal: execution.abortController.signal,
1996
2056
  ...(subagent
1997
2057
  ? {
@@ -2007,12 +2067,39 @@ export class RuntimeKernel {
2007
2067
  });
2008
2068
  await this.rejectCancelledBackendActivation(backend, header, execution);
2009
2069
  const generation = this.createBackendGeneration(sessionId, backend, header, prepared.providerStateIdentity);
2070
+ generation.executionPolicy = execution.executionPolicy;
2010
2071
  this.active.set(sessionId, generation);
2011
2072
  return generation;
2012
2073
  });
2013
2074
  entry.cachedHeader = header;
2014
2075
  return entry;
2015
2076
  }
2077
+ executionPolicyStore(targetSessionId, policy) {
2078
+ const store = this.deps.store;
2079
+ // Host stores are frozen. Proxy invariants forbid replacing their own
2080
+ // non-configurable methods, so intercept on an independent facade instead.
2081
+ return new Proxy(Object.create(store), {
2082
+ get(_facade, property) {
2083
+ if (property === 'readHeader')
2084
+ return async (sessionId) => {
2085
+ const header = await store.readHeader(sessionId);
2086
+ return sessionId === targetSessionId
2087
+ ? { ...header, permissionMode: policy.permissionMode }
2088
+ : header;
2089
+ };
2090
+ if (property === 'readExecutionBoundary')
2091
+ return async (sessionId) => {
2092
+ const boundary = await store.readExecutionBoundary(sessionId);
2093
+ if (sessionId === targetSessionId &&
2094
+ (policy.permissionMode === 'bypass' || boundary.kind === 'bypass'))
2095
+ return createGenesisExecutionBoundary(policy.permissionMode);
2096
+ return boundary;
2097
+ };
2098
+ const value = Reflect.get(store, property);
2099
+ return typeof value === 'function' ? value.bind(store) : value;
2100
+ },
2101
+ });
2102
+ }
2016
2103
  async shareBackendActivation(activationKey, activate) {
2017
2104
  let activation = this.backendActivationBuilds.get(activationKey);
2018
2105
  if (!activation) {
@@ -31,6 +31,18 @@ export const SANDBOX_BOUNDARY_UNAVAILABLE = 'request_sandbox_boundary is not ava
31
31
  'Retrying will fail the same way — redo the work inside the paths already allowed, or tell the ' +
32
32
  'user which path needs access.';
33
33
  export const REQUEST_SANDBOX_BOUNDARY_TOOL_NAME = 'request_sandbox_boundary';
34
+ /**
35
+ * Session-stable required names for the widening tool. Host only binds it when
36
+ * the Session is not Full access. Passing the name through `requiredToolNames`
37
+ * keeps the schema visible without a `tool_search` hop, and without depending
38
+ * on a live boundary prompt that would change the provider prefix per Turn
39
+ * (#5879). Returns empty when the tool is not bound.
40
+ */
41
+ export function requiredSandboxBoundaryToolNames(hostTools) {
42
+ return hostTools.some((tool) => tool.name === REQUEST_SANDBOX_BOUNDARY_TOOL_NAME)
43
+ ? new Set([REQUEST_SANDBOX_BOUNDARY_TOOL_NAME])
44
+ : new Set();
45
+ }
34
46
  export const SANDBOX_BOUNDARY_DENIED_FOR_TURN = 'The user denied a sandbox boundary expansion for this Turn. Do not request another expansion. ' +
35
47
  'Continue only with the authority already available, or explain the remaining blocker.';
36
48
  export const SANDBOX_BOUNDARY_FINALIZATION_PROMPT = [