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.
- package/README.zh-CN.md +2 -2
- package/dist/pi-tui-pickers.js +5 -2
- package/dist/pi-tui-runner.js +211 -33
- package/dist/runtime-host-session-driver.js +91 -13
- package/dist/session-catalog-limits.js +22 -0
- package/dist/session-driver.js +10 -0
- package/dist/tui-copy-catalog.js +15 -0
- package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/core/dist/bot-chat-settings.js +3 -0
- package/node_modules/@maka/core/dist/bot-events.js +1 -8
- package/node_modules/@maka/core/dist/local-memory.js +38 -1
- package/node_modules/@maka/core/dist/scheduled-task.js +14 -15
- package/node_modules/@maka/core/dist/session.js +3 -0
- package/node_modules/@maka/core/dist/text-sanitize.js +8 -3
- package/node_modules/@maka/runtime/dist/ai-sdk-backend.js +11 -0
- package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +13 -5
- package/node_modules/@maka/runtime/dist/bots/proxied-fetch.js +29 -1
- package/node_modules/@maka/runtime/dist/file-stable-write.js +15 -3
- package/node_modules/@maka/runtime/dist/model-adapter.js +20 -1
- package/node_modules/@maka/runtime/dist/network/proxy-test.js +28 -1
- package/node_modules/@maka/runtime/dist/network/scoped-fetch-transport.js +13 -2
- package/node_modules/@maka/runtime/dist/runtime-kernel.js +92 -5
- package/node_modules/@maka/runtime/dist/sandbox-boundary-tool.js +12 -0
- package/node_modules/@maka/runtime/dist/session-manager.js +31 -5
- package/node_modules/@maka/runtime/dist/tool-availability.js +22 -16
- package/node_modules/@maka/runtime/dist/tool-call-snapshot.js +142 -0
- package/node_modules/@maka/runtime/dist/tool-runtime.js +25 -83
- package/node_modules/@maka/runtime/dist/workers/filesystem-worker.mjs +12 -1
- package/node_modules/@maka/runtime-host/dist/client/connect-or-spawn.js +30 -6
- package/node_modules/@maka/runtime-host/dist/client/index.js +1 -1
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +2 -1
- package/node_modules/@maka/runtime-host/dist/protocol/message.js +1 -13
- package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +1 -13
- package/node_modules/@maka/runtime-host/dist/protocol/turn.js +1 -13
- package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +4 -7
- package/node_modules/@maka/runtime-host/dist/server/archive-retention-coordinator.js +81 -105
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +17 -43
- package/node_modules/@maka/runtime-host/dist/server/host-maintenance-guards.js +95 -0
- package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +18 -8
- package/node_modules/@maka/runtime-host/dist/server/interactive-run-composer.js +32 -3
- package/node_modules/@maka/runtime-host/dist/server/project-catalog-coordinator.js +2 -0
- package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +68 -3
- package/node_modules/@maka/runtime-host/dist/server/skill-catalog-coordinator.js +2 -0
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +19 -24
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +74 -19
- package/node_modules/@maka/runtime-host/dist/server/workhub-result-coordinator.js +1 -1
- package/node_modules/@maka/runtime-host/dist/server/workhub-result-runtime.js +3 -98
- package/node_modules/@maka/runtime-host/dist/server/workhub-target-execution-authority.js +14 -126
- package/node_modules/@maka/storage/dist/git-entry.js +142 -5
- 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)}
|
|
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
|
-
|
|
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
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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 (
|
|
103
|
+
if (points.length > maxCodePoints)
|
|
99
104
|
break;
|
|
100
105
|
}
|
|
101
|
-
if (points.length <=
|
|
106
|
+
if (points.length <= maxCodePoints)
|
|
102
107
|
return cleaned;
|
|
103
|
-
return points.slice(0,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
142
|
-
//
|
|
143
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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) =>
|
|
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:
|
|
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 = [
|