@sema-agent/core 5.60.0 → 5.61.0
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/CHANGELOG.md +79 -0
- package/dist/agents/subagent.d.ts +4 -2
- package/dist/agents/subagent.js +9 -9
- package/dist/core/checkpoint-store.d.ts +56 -6
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -2
- package/dist/core/mcp.d.ts +4 -5
- package/dist/core/memory-engine/consolidation-driver.d.ts +19 -1
- package/dist/core/memory-engine/consolidation-driver.js +75 -3
- package/dist/core/memory-engine/consolidation.d.ts +52 -5
- package/dist/core/memory-engine/consolidation.js +3 -1
- package/dist/core/memory-engine/distiller.d.ts +89 -1
- package/dist/core/memory-engine/distiller.js +94 -5
- package/dist/core/memory-engine/engine.d.ts +13 -6
- package/dist/core/memory-engine/engine.js +51 -8
- package/dist/core/memory-engine/index.d.ts +1 -1
- package/dist/core/memory-engine/index.js +1 -1
- package/dist/core/runner/prepare-task.d.ts +6 -3
- package/dist/core/runner/prepare-task.js +1 -0
- package/dist/core/runner/runtask.js +113 -31
- package/dist/core/task-notification.d.ts +50 -23
- package/dist/core/task-notification.js +20 -4
- package/dist/core/tool-policy.d.ts +8 -2
- package/dist/core/types.d.ts +76 -19
- package/dist/engine/harness/agent-harness.d.ts +58 -2
- package/dist/engine/harness/agent-harness.js +115 -5
- package/dist/engine/loop/agent-loop.js +153 -15
- package/dist/engine/loop/types.d.ts +32 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/orchestration/run-workflow-tool.d.ts +7 -2
- package/dist/orchestration/run-workflow-tool.js +1 -1
- package/dist/tools/monitor.d.ts +3 -3
- package/dist/tools/monitor.js +1 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +11 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { persistedReadDenyEntryProblem } from "../../tools/fs/read-deny.js";
|
|
2
2
|
import { createSafeNotifier, observeThenableRejection } from "../safe-notify.js";
|
|
3
3
|
import { mintSystemReminder, openSystemReminder } from "../reminder-mint.js";
|
|
4
|
+
import { GLOBAL_USAGE_KEY, resolveUsageWindows, usageRetryAfterMs } from "../usage-window-store.js";
|
|
4
5
|
import { deliverDelegationLifecycle, deliverEngineNotice, undrainedUserInputNotices } from "../types.js";
|
|
5
6
|
import { planRejectionClears, resolveTriggerWindow } from "../context-edit.js";
|
|
6
7
|
import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
|
|
@@ -49,7 +50,7 @@ import { RunnerSharedToolResultStore } from "../tool-result-store.js";
|
|
|
49
50
|
import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
|
|
50
51
|
import { checkToolPolicyProjection, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, refuseOutOfContractDecision, screenApproverAttribution, toolPolicyNameSets } from "../tool-policy.js";
|
|
51
52
|
import { defaultTaskRegistry } from "../task-registry.js";
|
|
52
|
-
import { discloseDroppedPending, isDelegatedAgentTerminal, isSystemInjectionPriority, PendingSessionNotifications, renderTaskNotificationXml, SYSTEM_INJECTION_PRIORITIES, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
|
|
53
|
+
import { discloseDroppedPending, isDelegatedAgentTerminal, isSystemInjectionPriority, isTerminalTaskNotification, PendingSessionNotifications, renderTaskNotificationXml, SYSTEM_INJECTION_PRIORITIES, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
|
|
53
54
|
import { ToolDetachHub } from "../tool-detach.js";
|
|
54
55
|
import { createPeerInboundChainRef, createPeerSelfRef } from "../../agents/peer-admission.js";
|
|
55
56
|
import { workflowSizeGuidelineChangeNotice } from "../../orchestration/workflow-size-guideline.js";
|
|
@@ -77,6 +78,7 @@ function nextHumanInputSeq(key) {
|
|
|
77
78
|
function sameAcceptedSteerInput(a, b) {
|
|
78
79
|
return (a.payload === b.payload &&
|
|
79
80
|
a.trusted === b.trusted &&
|
|
81
|
+
a.priority === b.priority &&
|
|
80
82
|
a.actor?.id === b.actor?.id &&
|
|
81
83
|
a.actor?.hostAsserted === b.actor?.hostAsserted &&
|
|
82
84
|
a.actor?.issuer === b.actor?.issuer);
|
|
@@ -344,9 +346,11 @@ export async function raceUntilDeadline(p, deadline) {
|
|
|
344
346
|
function platformLimitTerminal(reason, retryAfterMs, moment = "turn_boundary") {
|
|
345
347
|
const message = moment === "entry"
|
|
346
348
|
? `a deployment usage window is exhausted (RunnerDeps.usageWindows), so the task was refused before its first model call — nothing ran and nothing was spent. There is no checkpoint to suspend into at this point, so the caller re-submits after the window frees, in ${String(retryAfterMs ?? 0)}ms.`
|
|
347
|
-
:
|
|
348
|
-
?
|
|
349
|
-
:
|
|
349
|
+
: moment === "resume"
|
|
350
|
+
? `a deployment usage window is exhausted (RunnerDeps.usageWindows), so the resumed leg was stopped BEFORE its first model call — nothing was delivered or executed on this leg. The window frees in ${String(retryAfterMs ?? 0)}ms.`
|
|
351
|
+
: reason === "env_lifetime"
|
|
352
|
+
? "the execution environment's declared lifetime is expiring and the run could not be suspended durably (not eligible, or the suspend was refused — see remoteEnvFailures / onError). It was stopped at a clean turn boundary instead of being killed mid-turn when the platform reclaims the environment."
|
|
353
|
+
: `a deployment usage window is exhausted (RunnerDeps.usageWindows) and the run could not be suspended durably, so it was stopped at a clean turn boundary. The window frees in ${String(retryAfterMs ?? 0)}ms.`;
|
|
350
354
|
const e = new Error(message);
|
|
351
355
|
e.code = reason === "env_lifetime" ? "env.lifetime_expired" : "usage.window_exhausted";
|
|
352
356
|
if (retryAfterMs !== undefined)
|
|
@@ -1826,6 +1830,11 @@ export class Runner {
|
|
|
1826
1830
|
throw steeringError(`inputId "${LEGACY_PENDING_STEER_INPUT_ID}" is reserved for a pre-queue parked steer and cannot be supplied by a caller`, "steering.invalid_content");
|
|
1827
1831
|
}
|
|
1828
1832
|
}
|
|
1833
|
+
const priorityIn = options?.priority;
|
|
1834
|
+
if (priorityIn !== undefined && !isSystemInjectionPriority(priorityIn)) {
|
|
1835
|
+
throw steeringError(`priority must be one of ${SYSTEM_INJECTION_PRIORITIES.join("/")} when supplied`, "steering.invalid_content");
|
|
1836
|
+
}
|
|
1837
|
+
const priority = priorityIn ?? "next";
|
|
1829
1838
|
const actorIn = options?.actor;
|
|
1830
1839
|
const actor = actorIn === undefined ? undefined : snapshotActorAssertion(actorIn);
|
|
1831
1840
|
const projected = projectHumanInput({ text, actor, source: "steer" });
|
|
@@ -1834,6 +1843,7 @@ export class Runner {
|
|
|
1834
1843
|
text,
|
|
1835
1844
|
trusted,
|
|
1836
1845
|
inputId: effectiveInputId,
|
|
1846
|
+
...(priorityIn !== undefined ? { priority } : {}),
|
|
1837
1847
|
...(actor !== undefined ? { actor } : {}),
|
|
1838
1848
|
};
|
|
1839
1849
|
let payload;
|
|
@@ -1869,7 +1879,7 @@ export class Runner {
|
|
|
1869
1879
|
throw steeringError("the task is not running");
|
|
1870
1880
|
payload = trusted ? formatHookFeedback(projected, h.reminderMark) : frameMidTurnUserInput(projected);
|
|
1871
1881
|
mintsAFrame = payload.trim().length !== 0;
|
|
1872
|
-
replay = { payload, trusted, ...(actor !== undefined ? { actor } : {}) };
|
|
1882
|
+
replay = { payload, trusted, priority, ...(actor !== undefined ? { actor } : {}) };
|
|
1873
1883
|
if (typeof inputId === "string") {
|
|
1874
1884
|
const prior = acceptedSteerInputs.get(inputId);
|
|
1875
1885
|
if (prior !== undefined) {
|
|
@@ -1882,9 +1892,32 @@ export class Runner {
|
|
|
1882
1892
|
return;
|
|
1883
1893
|
}
|
|
1884
1894
|
}
|
|
1885
|
-
|
|
1886
|
-
|
|
1895
|
+
const injectFramed = async () => {
|
|
1896
|
+
const noteOptions = { provenance: "engine-note", callerAuthored: true, parkRecord, ...(actor !== undefined ? { actor } : {}) };
|
|
1897
|
+
if (priority === "later") {
|
|
1898
|
+
await h.harness.followUp(payload, noteOptions);
|
|
1899
|
+
noteAccepted(h);
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
const frame = await h.harness.steer(payload, { ...noteOptions, ...(priority === "now" ? { immediate: true } : {}) });
|
|
1887
1903
|
noteAccepted(h);
|
|
1904
|
+
if (priority === "now" && frame !== undefined && h.harness.interruptTurn(frame)) {
|
|
1905
|
+
deliverEngineNotice(this.deps.onNotice, {
|
|
1906
|
+
code: "task.turn_interrupted",
|
|
1907
|
+
message: "a caller-provenance steer with priority \"now\" interrupted the running turn: in-flight work was cut at " +
|
|
1908
|
+
"a manufactured boundary (finished tool calls keep their real results; never-started ones settle as " +
|
|
1909
|
+
"interrupted) and the run continues with the steer at the queue head.",
|
|
1910
|
+
detail: {
|
|
1911
|
+
inputId: effectiveInputId,
|
|
1912
|
+
sessionId: h.sessionId,
|
|
1913
|
+
...(actor?.id !== undefined ? { actorId: actor.id } : {}),
|
|
1914
|
+
...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}),
|
|
1915
|
+
},
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
};
|
|
1919
|
+
try {
|
|
1920
|
+
await injectFramed();
|
|
1888
1921
|
return;
|
|
1889
1922
|
}
|
|
1890
1923
|
catch (e) {
|
|
@@ -1894,8 +1927,7 @@ export class Runner {
|
|
|
1894
1927
|
const birthDeadline = Date.now() + READY_TIMEOUT_MS;
|
|
1895
1928
|
while (resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) {
|
|
1896
1929
|
try {
|
|
1897
|
-
await
|
|
1898
|
-
noteAccepted(h);
|
|
1930
|
+
await injectFramed();
|
|
1899
1931
|
return;
|
|
1900
1932
|
}
|
|
1901
1933
|
catch (e2) {
|
|
@@ -1935,9 +1967,14 @@ export class Runner {
|
|
|
1935
1967
|
if (input.source !== undefined && typeof input.source !== "string") {
|
|
1936
1968
|
throw notifyError("input.source must be a string when present", "notify.invalid_payload");
|
|
1937
1969
|
}
|
|
1938
|
-
|
|
1970
|
+
const priorityIn = opts?.priority;
|
|
1971
|
+
if (priorityIn !== undefined && !isSystemInjectionPriority(priorityIn)) {
|
|
1939
1972
|
throw notifyError(`opts.priority must be one of ${SYSTEM_INJECTION_PRIORITIES.join("/")} when present`, "notify.invalid_payload");
|
|
1940
1973
|
}
|
|
1974
|
+
const priority = priorityIn;
|
|
1975
|
+
if (priority === "now") {
|
|
1976
|
+
throw notifyError('priority "now" (turn interrupt) is not a notification-lane power — it belongs to the steer face; use "next" for earliest-boundary delivery', "notify.invalid_priority");
|
|
1977
|
+
}
|
|
1941
1978
|
const payload = {
|
|
1942
1979
|
task_id: input.task_id,
|
|
1943
1980
|
task_type: "external",
|
|
@@ -1951,7 +1988,7 @@ export class Runner {
|
|
|
1951
1988
|
if (!h || notifyRef.inject === undefined) {
|
|
1952
1989
|
throw notifyError("the task is not running", "notify.not_running");
|
|
1953
1990
|
}
|
|
1954
|
-
notifyRef.inject(payload,
|
|
1991
|
+
notifyRef.inject(payload, priority !== undefined ? { priority } : undefined);
|
|
1955
1992
|
},
|
|
1956
1993
|
compact: async (opts) => {
|
|
1957
1994
|
if (resultValue)
|
|
@@ -2047,7 +2084,7 @@ export class Runner {
|
|
|
2047
2084
|
let descendantAnchorDisclosed = false;
|
|
2048
2085
|
const parkTaskNotification = (payload, priority) => {
|
|
2049
2086
|
if (notificationSessionId !== undefined)
|
|
2050
|
-
this.pendingSessionNotifications.pend(notificationSessionId, payload);
|
|
2087
|
+
this.pendingSessionNotifications.pend(notificationSessionId, payload, priority);
|
|
2051
2088
|
if (!isDelegatedAgentTerminal(payload))
|
|
2052
2089
|
return;
|
|
2053
2090
|
const uplink = internals?.parentNotify;
|
|
@@ -2062,7 +2099,7 @@ export class Runner {
|
|
|
2062
2099
|
}
|
|
2063
2100
|
const rootAnchor = internals?.rootSessionId;
|
|
2064
2101
|
if (rootAnchor !== undefined && rootAnchor !== notificationSessionId) {
|
|
2065
|
-
this.pendingSessionNotifications.pend(rootAnchor, payload);
|
|
2102
|
+
this.pendingSessionNotifications.pend(rootAnchor, payload, priority);
|
|
2066
2103
|
return;
|
|
2067
2104
|
}
|
|
2068
2105
|
if (internals?.parentSessionId !== undefined && !descendantAnchorDisclosed) {
|
|
@@ -2071,10 +2108,17 @@ export class Runner {
|
|
|
2071
2108
|
}
|
|
2072
2109
|
};
|
|
2073
2110
|
const unsubscribeTaskNotifications = taskNotificationQueue.subscribe((item) => {
|
|
2074
|
-
queue.push({ type: "task_notification", notification: item.payload, ...notificationIdent() });
|
|
2111
|
+
queue.push({ type: "task_notification", notification: item.payload, priority: item.priority, ...notificationIdent() });
|
|
2075
2112
|
if (notificationHarness && prepared.batchHaltRef.current === undefined) {
|
|
2076
2113
|
const xml = renderTaskNotificationXml(item.payload);
|
|
2077
|
-
const
|
|
2114
|
+
const noteOptions = {
|
|
2115
|
+
provenance: "engine-note",
|
|
2116
|
+
enginePayload: item.payload,
|
|
2117
|
+
...(item.payload.task_type !== "external" && isTerminalTaskNotification(item.payload) ? { capPreferred: true } : {}),
|
|
2118
|
+
};
|
|
2119
|
+
const deliver = item.priority === "later"
|
|
2120
|
+
? notificationHarness.followUp(xml, noteOptions)
|
|
2121
|
+
: notificationHarness.steer(xml, { ...noteOptions, ...(item.priority === "now" ? { immediate: true } : {}) });
|
|
2078
2122
|
void deliver.then(() => item.onDisposition?.("queued"), () => {
|
|
2079
2123
|
parkTaskNotification(item.payload, item.priority);
|
|
2080
2124
|
item.onDisposition?.("parked");
|
|
@@ -2087,18 +2131,7 @@ export class Runner {
|
|
|
2087
2131
|
});
|
|
2088
2132
|
const upstreamTaskNotification = internals?.onTaskNotification;
|
|
2089
2133
|
const deliveredAtTurnOpen = new Set();
|
|
2090
|
-
let priorityNowDisclosed = false;
|
|
2091
2134
|
const injectTaskNotification = (notification, opts) => {
|
|
2092
|
-
if (opts?.priority === "now" && !priorityNowDisclosed) {
|
|
2093
|
-
priorityNowDisclosed = true;
|
|
2094
|
-
deliverEngineNotice(this.deps.onNotice, {
|
|
2095
|
-
code: "task.injection_priority_unimplemented",
|
|
2096
|
-
message: `a notification was injected with priority "now", which this engine does not implement: all injection ` +
|
|
2097
|
-
`priorities deliver at the NEXT turn boundary and none aborts the running turn. The notification is ` +
|
|
2098
|
-
`delivered — only the interrupting semantics are absent.`,
|
|
2099
|
-
detail: { priority: "now", ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), sessionId: prepared.sessionId },
|
|
2100
|
-
});
|
|
2101
|
-
}
|
|
2102
2135
|
if (deliveredAtTurnOpen.has(taskNotificationDedupKey(notification)))
|
|
2103
2136
|
return Promise.resolve("dropped_duplicate");
|
|
2104
2137
|
if (!notificationLaneLive) {
|
|
@@ -2193,16 +2226,17 @@ export class Runner {
|
|
|
2193
2226
|
const pendingIdle = this.pendingSessionNotifications.drain(prepared.sessionId);
|
|
2194
2227
|
if (pendingIdle !== undefined) {
|
|
2195
2228
|
for (const payload of discloseDroppedPending(pendingIdle)) {
|
|
2229
|
+
const parkedPriority = pendingIdle.priorities?.get(payload);
|
|
2196
2230
|
deliveredAtTurnOpen.add(taskNotificationDedupKey(payload));
|
|
2197
|
-
queue.push({ type: "task_notification", notification: payload, ...ident() });
|
|
2231
|
+
queue.push({ type: "task_notification", notification: payload, ...(parkedPriority !== undefined ? { priority: parkedPriority } : {}), ...ident() });
|
|
2198
2232
|
void prepared.harness.nextTurn(renderTaskNotificationXml(payload), { provenance: "engine-note", enginePayload: payload }).catch(() => {
|
|
2199
|
-
this.pendingSessionNotifications.pend(prepared.sessionId, payload);
|
|
2233
|
+
this.pendingSessionNotifications.pend(prepared.sessionId, payload, parkedPriority);
|
|
2200
2234
|
});
|
|
2201
2235
|
}
|
|
2202
2236
|
}
|
|
2203
2237
|
}
|
|
2204
2238
|
const loopLatch = { ended: false, userInterrupted: false };
|
|
2205
|
-
onReady({ harness: prepared.harness, abortController: prepared.abortController, loop: loopLatch, reminderMark: prepared.reminderMark });
|
|
2239
|
+
onReady({ harness: prepared.harness, abortController: prepared.abortController, loop: loopLatch, reminderMark: prepared.reminderMark, sessionId: prepared.sessionId });
|
|
2206
2240
|
const stats = { turns: 0, tokens: 0, toolCalls: 0, promptTokens: 0, totalInputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cacheWriteTokensLong: 0, outputTokens: 0, costMicroUsd: 0 };
|
|
2207
2241
|
prepared.liveSpendRef.get = () => ({ costMicroUsd: stats.costMicroUsd, tokens: stats.tokens, turns: stats.turns, walltimeMs: Math.round(performance.now() - rs.telemetry.taskStartMonotonic) });
|
|
2208
2242
|
if (resume &&
|
|
@@ -3186,7 +3220,29 @@ export class Runner {
|
|
|
3186
3220
|
}
|
|
3187
3221
|
if (resume) {
|
|
3188
3222
|
const walltimeExhaustedResume = effectiveTimeoutMs !== undefined && effectiveTimeoutMs <= 0;
|
|
3189
|
-
|
|
3223
|
+
const resumeWindowRetryAfterMs = !walltimeExhaustedResume && prepared.usageGovernance !== undefined ? await prepared.usageGovernance.check(Date.now()) : undefined;
|
|
3224
|
+
if (resumeWindowRetryAfterMs !== undefined) {
|
|
3225
|
+
const owesNothing = resume.outcome.gate === "resource_limit" &&
|
|
3226
|
+
readPendingSteerQueue(resume.cp.state).length === 0 &&
|
|
3227
|
+
(resume.cp.state.runningBackgroundTasks?.length ?? 0) === 0;
|
|
3228
|
+
const committed = owesNothing &&
|
|
3229
|
+
prepared.suspendForPlatformLimit !== undefined &&
|
|
3230
|
+
(await prepared.suspendForPlatformLimit("usage_window", { costMicroUsd: 0, tokens: 0, turns: 0, walltimeMs: Math.round(performance.now() - rs.telemetry.taskStartMonotonic) }, { resumeAfterMs: resumeWindowRetryAfterMs }));
|
|
3231
|
+
if (committed) {
|
|
3232
|
+
rs.limits.turnsExceeded = false;
|
|
3233
|
+
rs.limits.budgetHit = undefined;
|
|
3234
|
+
}
|
|
3235
|
+
else {
|
|
3236
|
+
const terminal = platformLimitTerminal("usage_window", resumeWindowRetryAfterMs, "resume");
|
|
3237
|
+
rs.limits.platformTerminal = terminal;
|
|
3238
|
+
try {
|
|
3239
|
+
this.deps.onError?.(terminal, { phase: "config", sessionId: prepared.sessionId });
|
|
3240
|
+
}
|
|
3241
|
+
catch {
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
if (resumeWindowRetryAfterMs === undefined && !walltimeExhaustedResume && resume.outcome.gate !== "wake") {
|
|
3190
3246
|
await this.applyResumeDecision(prepared, resume, (e) => pushContent({ ...e, ...ident() }), emitCommitted, (toolName, details) => {
|
|
3191
3247
|
if (rs.attach.attachState === undefined)
|
|
3192
3248
|
return;
|
|
@@ -3205,7 +3261,9 @@ export class Runner {
|
|
|
3205
3261
|
resume.decisionDelivered = true;
|
|
3206
3262
|
announceWorkspaceMove();
|
|
3207
3263
|
}
|
|
3208
|
-
if (
|
|
3264
|
+
if (resumeWindowRetryAfterMs !== undefined) {
|
|
3265
|
+
}
|
|
3266
|
+
else if (walltimeExhaustedResume) {
|
|
3209
3267
|
timeout.fired = true;
|
|
3210
3268
|
timeout.clear();
|
|
3211
3269
|
void prepared.harness.abort();
|
|
@@ -4455,6 +4513,29 @@ export class Runner {
|
|
|
4455
4513
|
throw new CheckpointError("checkpoint.invalid_outcome", "TaskSpec.basePolicyForResumeEdit is administratively locked by this deployment (locked key \"toolPolicy\") — a task-supplied " +
|
|
4456
4514
|
"resume-edit policy is refused pre-CAS (the checkpoint stays pending); remove the field or change the deployment's lock configuration");
|
|
4457
4515
|
}
|
|
4516
|
+
{
|
|
4517
|
+
const rowPrincipal = cp.principal || undefined;
|
|
4518
|
+
const suppliedPrincipal = taskConfig.principal || undefined;
|
|
4519
|
+
if (rowPrincipal !== undefined && suppliedPrincipal !== undefined && suppliedPrincipal !== rowPrincipal) {
|
|
4520
|
+
throw new CheckpointError("resume.principal_mismatch", `the resume config carries principal ${JSON.stringify(suppliedPrincipal)} but this checkpoint was suspended under principal ${JSON.stringify(rowPrincipal)} — ` +
|
|
4521
|
+
"running the resumed leg under a different identity would move its usage-ledger bucket, scope derivation and attribution; " +
|
|
4522
|
+
"refused pre-CAS (the checkpoint stays pending): re-resume with the original principal, or omit the field to inherit the recorded one");
|
|
4523
|
+
}
|
|
4524
|
+
const owesDelivery = outcomeGate !== "resource_limit" ||
|
|
4525
|
+
readPendingSteerQueue(cp.state).length > 0 ||
|
|
4526
|
+
(cp.state.runningBackgroundTasks?.length ?? 0) > 0;
|
|
4527
|
+
if (owesDelivery && this.deps.usageWindowStore !== undefined) {
|
|
4528
|
+
const preCasWindows = resolveUsageWindows(this.deps.usageWindows);
|
|
4529
|
+
if (preCasWindows !== undefined && preCasWindows.length > 0) {
|
|
4530
|
+
const ledgerKey = (suppliedPrincipal ?? rowPrincipal) || GLOBAL_USAGE_KEY;
|
|
4531
|
+
const wait = usageRetryAfterMs(await this.deps.usageWindowStore.read(ledgerKey, preCasWindows, Date.now()));
|
|
4532
|
+
if (wait !== undefined) {
|
|
4533
|
+
throw new CheckpointError("resume.usage_window_exhausted", `a deployment usage window for ledger key ${JSON.stringify(ledgerKey)} is exhausted, and this checkpoint still owes a delivery a re-mint cannot carry — ` +
|
|
4534
|
+
`refused pre-CAS (nothing consumed, nothing unpinned): the same token and the same decision are redeemable once the window frees, in ${String(wait)}ms`, { retryAfterMs: wait });
|
|
4535
|
+
}
|
|
4536
|
+
}
|
|
4537
|
+
}
|
|
4538
|
+
}
|
|
4458
4539
|
if (plainPolicyOutcome !== undefined &&
|
|
4459
4540
|
plainPolicyOutcome.decision === "allow" &&
|
|
4460
4541
|
plainPolicyOutcome.updatedInput !== undefined &&
|
|
@@ -4571,6 +4652,7 @@ export class Runner {
|
|
|
4571
4652
|
: undefined;
|
|
4572
4653
|
const spec = {
|
|
4573
4654
|
...taskConfig,
|
|
4655
|
+
...((taskConfig.principal || undefined) === undefined && (cp.principal || undefined) !== undefined ? { principal: cp.principal } : {}),
|
|
4574
4656
|
objective: "",
|
|
4575
4657
|
sessionId: cp.sessionId,
|
|
4576
4658
|
requireExistingSession: true,
|
|
@@ -148,28 +148,28 @@ export interface ExternalNotificationInput {
|
|
|
148
148
|
export interface SystemInjection<TPayload = unknown> {
|
|
149
149
|
kind: "task_notification";
|
|
150
150
|
/**
|
|
151
|
-
* design/
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
151
|
+
* design/373 — the injection ladder is LIVE (all three values carry delivery semantics, the CC
|
|
152
|
+
* 2.1.223 form; the flat "every priority delivers at the next boundary" era ended with this
|
|
153
|
+
* design — its H-1 anchor-correction note is preserved in the design archive, and the ruled
|
|
154
|
+
* changes it named are exactly what landed here):
|
|
155
155
|
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
156
|
+
* - `"next"` — the running turn's NEXT boundary (mid-work included), arrival order, consecutive
|
|
157
|
+
* engine-note frames batch. The pre-373 behavior, byte-identical (the regression baseline).
|
|
158
|
+
* - `"later"` — never folded into the work in progress: delivered at the run's natural
|
|
159
|
+
* would-otherwise-stop seat (its own closing turn). A run that never reaches that seat
|
|
160
|
+
* (abort/maxTurns) re-pends the frame per session — the session's NEXT run delivers it at
|
|
161
|
+
* turn-open. Structural honesty note: once parked, delivery depends on a next run HAPPENING
|
|
162
|
+
* (this engine does not own an idle process the way the CC client does) — a recorded
|
|
163
|
+
* weakening, not a bug.
|
|
164
|
+
* - `"now"` — `"next"`'s delivery guarantee PLUS the boundary is manufactured early: on the
|
|
165
|
+
* notification lane the frame takes the queue's class head and the earliest natural boundary
|
|
166
|
+
* (NO turn interrupt — interrupt authority belongs exclusively to the caller-provenance
|
|
167
|
+
* steer face, `TaskStream.steer({ priority: "now" })`; the external `notify()` verb REFUSES
|
|
168
|
+
* `"now"` typed). An unknown value is refused at every entry (bad-value loudness).
|
|
165
169
|
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* `later` = "do not fold into the running turn" arm is a behavior-face change and `now` = "abort
|
|
170
|
-
* the running turn" is a new capability; both are ruled changes, not silent ones. Until then the
|
|
171
|
-
* gap is DISCLOSED at the injection funnel rather than left as a silently inert knob (`now` is
|
|
172
|
-
* announced, an unknown value is refused) — the bad-value loudness rule.
|
|
170
|
+
* Park/uplink: the pend store carries the frame's priority ({@link PendingSessionNotifications})
|
|
171
|
+
* and the turn-open batch delivers priority-major (pend order within a class); a record with no
|
|
172
|
+
* carried priority ranks AS `later` and its wire field stays honestly absent.
|
|
173
173
|
*/
|
|
174
174
|
priority: SystemInjectionPriority;
|
|
175
175
|
dedupKey: string;
|
|
@@ -180,6 +180,13 @@ export interface SystemInjection<TPayload = unknown> {
|
|
|
180
180
|
* fire-and-forget producer (no receipt to honor). */
|
|
181
181
|
onDisposition?: (d: "queued" | "parked") => void;
|
|
182
182
|
}
|
|
183
|
+
/** design/373 (adversarial r3, recorded rule): the key deliberately EXCLUDES the injection tier.
|
|
184
|
+
* The key names an EVENT's identity; `priority` is delivery metadata about one submission of it.
|
|
185
|
+
* A repeat under an in-flight key therefore FOLDS regardless of the repeat's tier — the tier of
|
|
186
|
+
* record is the FIRST accept's, and the folded caller's "queued" receipt is true of that standing
|
|
187
|
+
* entry. A producer that wants a NEW occurrence delivered under a different tier mints a fresh
|
|
188
|
+
* `seq` (the documented repeat discipline); silently migrating a queued frame between lanes on a
|
|
189
|
+
* repeat, or refusing the repeat typed, would each turn a dedup fold into a delivery mutation. */
|
|
183
190
|
export declare function taskNotificationDedupKey(n: Pick<TaskNotificationPayload, "task_id" | "task_type" | "status" | "seq">): string;
|
|
184
191
|
/**
|
|
185
192
|
* RB-142 — the lane-scoped identity of a task, extracted from {@link taskNotificationDedupKey} so every
|
|
@@ -247,9 +254,27 @@ export declare const MAX_PENDING_SESSIONS = 100;
|
|
|
247
254
|
* preference applies, so "terminal" can never mean two things in this module.
|
|
248
255
|
*/
|
|
249
256
|
export declare function isDelegatedAgentTerminal(n: Pick<TaskNotificationPayload, "task_type" | "status">): boolean;
|
|
257
|
+
/**
|
|
258
|
+
* design/373 (#445 saturation prerequisite) — is this frame a TERMINAL notification (any lane)?
|
|
259
|
+
* Reuses {@link TERMINAL_STATUSES} — the same terminal-vs-event rule the pending store's eviction
|
|
260
|
+
* preference applies, so "terminal" can never mean two things in this module. The runner marks
|
|
261
|
+
* INTERNAL-lane terminal frames cap-preferred at the delivery queue's mouth: a watcher's event
|
|
262
|
+
* storm may delay its own batches, never crowd a completion out of the run waiting on it. The
|
|
263
|
+
* external lane never gets the preference (its `status` is caller-supplied — an untrusted injector
|
|
264
|
+
* must not be able to claim the internal completions' reservation); this predicate itself stays
|
|
265
|
+
* lane-blind, the trust cut is the marking site's.
|
|
266
|
+
*/
|
|
267
|
+
export declare function isTerminalTaskNotification(n: Pick<TaskNotificationPayload, "status">): boolean;
|
|
250
268
|
export interface DrainedPendingNotifications {
|
|
251
|
-
/**
|
|
269
|
+
/** Payloads still held when the session's next run drained. design/373 §3.5: batch order is
|
|
270
|
+
* PRIORITY-MAJOR (now < next < later; a record with no carried priority ranks AS later —
|
|
271
|
+
* the conservative seat), pend order (chronological) within a class — the idle-dequeue form. */
|
|
252
272
|
items: TaskNotificationPayload[];
|
|
273
|
+
/** design/373 §3.5 — the parked priority per item, keyed by payload identity, present exactly for
|
|
274
|
+
* records whose pend CARRIED one. A missing key is a fact (a tier-unknown park — e.g.
|
|
275
|
+
* the harness's undrained sweep hands payloads back without their lane's priority): the wire
|
|
276
|
+
* field stays honestly absent rather than fabricating `later`. */
|
|
277
|
+
priorities?: Map<TaskNotificationPayload, SystemInjectionPriority>;
|
|
253
278
|
/** task_id → notifications evicted by the bounds while pending (never delivered). `taskType` remembers
|
|
254
279
|
* the victim's lane so a survivors-none disclosure can still render an honest synthetic payload. */
|
|
255
280
|
/** RB-142: keyed by {@link taskNotificationLaneKey}, NOT the bare `task_id` — the external lane's ids are
|
|
@@ -288,8 +313,10 @@ export declare class PendingSessionNotifications {
|
|
|
288
313
|
/** RB-143: whole-session losses whose tombstone was itself evicted — the count survives, the attribution
|
|
289
314
|
* does not. Surfaced on the next session-level disclosure so it is never simply forgotten. */
|
|
290
315
|
private unattributedDrops;
|
|
291
|
-
pend(sessionId: string, n: TaskNotificationPayload): void;
|
|
292
|
-
/** Remove and return the session's pendings (one-shot — the next run consumes them exactly once).
|
|
316
|
+
pend(sessionId: string, n: TaskNotificationPayload, priority?: SystemInjectionPriority): void;
|
|
317
|
+
/** Remove and return the session's pendings (one-shot — the next run consumes them exactly once).
|
|
318
|
+
* design/373 §3.5: the batch comes out PRIORITY-MAJOR (stable sort — pend order within a class;
|
|
319
|
+
* a no-priority record ranks as later), the idle-dequeue min-value-first form. */
|
|
293
320
|
drain(sessionId: string): DrainedPendingNotifications | undefined;
|
|
294
321
|
get size(): number;
|
|
295
322
|
}
|
|
@@ -92,12 +92,15 @@ const TERMINAL_STATUSES = new Set(["completed", "failed", "killed", "cancelled"]
|
|
|
92
92
|
export function isDelegatedAgentTerminal(n) {
|
|
93
93
|
return n.task_type === "background_agent" && TERMINAL_STATUSES.has(n.status);
|
|
94
94
|
}
|
|
95
|
+
export function isTerminalTaskNotification(n) {
|
|
96
|
+
return TERMINAL_STATUSES.has(n.status);
|
|
97
|
+
}
|
|
95
98
|
export class PendingSessionNotifications {
|
|
96
99
|
sessions = new Map();
|
|
97
100
|
droppedSessions = 0;
|
|
98
101
|
evictedSessions = new Map();
|
|
99
102
|
unattributedDrops = 0;
|
|
100
|
-
pend(sessionId, n) {
|
|
103
|
+
pend(sessionId, n, priority) {
|
|
101
104
|
let s = this.sessions.get(sessionId);
|
|
102
105
|
if (s === undefined) {
|
|
103
106
|
if (this.sessions.size >= MAX_PENDING_SESSIONS) {
|
|
@@ -119,7 +122,7 @@ export class PendingSessionNotifications {
|
|
|
119
122
|
}
|
|
120
123
|
}
|
|
121
124
|
}
|
|
122
|
-
s = { items: [], dropped: new Map(), keys: new Set() };
|
|
125
|
+
s = { items: [], dropped: new Map(), keys: new Set(), priorities: new Map() };
|
|
123
126
|
this.sessions.set(sessionId, s);
|
|
124
127
|
}
|
|
125
128
|
const key = taskNotificationDedupKey(n);
|
|
@@ -127,6 +130,8 @@ export class PendingSessionNotifications {
|
|
|
127
130
|
return;
|
|
128
131
|
s.keys.add(key);
|
|
129
132
|
s.items.push(n);
|
|
133
|
+
if (priority !== undefined)
|
|
134
|
+
s.priorities?.set(n, priority);
|
|
130
135
|
const lane = taskNotificationLaneKey(n);
|
|
131
136
|
let mine = 0;
|
|
132
137
|
for (const i of s.items)
|
|
@@ -139,8 +144,14 @@ export class PendingSessionNotifications {
|
|
|
139
144
|
}
|
|
140
145
|
drain(sessionId) {
|
|
141
146
|
const s = this.sessions.get(sessionId);
|
|
142
|
-
if (s !== undefined)
|
|
147
|
+
if (s !== undefined) {
|
|
143
148
|
this.sessions.delete(sessionId);
|
|
149
|
+
const rank = (n) => {
|
|
150
|
+
const p = s.priorities?.get(n);
|
|
151
|
+
return p === "now" ? 0 : p === "next" ? 1 : 2;
|
|
152
|
+
};
|
|
153
|
+
s.items = [...s.items].sort((a, b) => rank(a) - rank(b));
|
|
154
|
+
}
|
|
144
155
|
const tomb = this.evictedSessions.get(sessionId);
|
|
145
156
|
if (tomb === undefined && this.unattributedDrops === 0)
|
|
146
157
|
return s;
|
|
@@ -163,6 +174,7 @@ function evictOldest(s, match) {
|
|
|
163
174
|
return;
|
|
164
175
|
const victim = s.items.splice(idx, 1)[0];
|
|
165
176
|
s.keys.delete(taskNotificationDedupKey(victim));
|
|
177
|
+
s.priorities?.delete(victim);
|
|
166
178
|
const laneKey = taskNotificationLaneKey(victim);
|
|
167
179
|
const prior = s.dropped.get(laneKey);
|
|
168
180
|
s.dropped.set(laneKey, { count: (prior?.count ?? 0) + 1, taskType: victim.task_type, taskId: victim.task_id });
|
|
@@ -190,10 +202,14 @@ export function discloseDroppedPending(drained) {
|
|
|
190
202
|
if (dropped === undefined || disclosed.has(lane))
|
|
191
203
|
return n;
|
|
192
204
|
disclosed.add(lane);
|
|
193
|
-
|
|
205
|
+
const annotated = {
|
|
194
206
|
...n,
|
|
195
207
|
summary: `[${n.task_id}] ${dropped.count} earlier pending notification(s) from this task were dropped (pending-queue overflow). ${n.summary}`,
|
|
196
208
|
};
|
|
209
|
+
const priority = drained.priorities?.get(n);
|
|
210
|
+
if (priority !== undefined)
|
|
211
|
+
drained.priorities?.set(annotated, priority);
|
|
212
|
+
return annotated;
|
|
197
213
|
});
|
|
198
214
|
for (const [lane, dropped] of drained.dropped) {
|
|
199
215
|
if (disclosed.has(lane))
|
|
@@ -413,8 +413,14 @@ export interface ToolPolicyProjection {
|
|
|
413
413
|
*
|
|
414
414
|
* Exported for the OTHER name-keyed lane in this engine — the persisted session/ancestor rules in
|
|
415
415
|
* `session-rule-policy.ts`. Not part of the public API (`src/index.ts` re-exports by name and does not
|
|
416
|
-
* list these):
|
|
417
|
-
* way, because the prepare-time audit's exemption for these spellings speaks for
|
|
416
|
+
* list these two): every publisher of {@link ToolPolicyNameSets} must resolve a covering entry the same
|
|
417
|
+
* way, because the prepare-time audit's exemption for these spellings speaks for ALL published name sets
|
|
418
|
+
* at once — the four in-engine lanes do so by sharing this module's matcher. #450: that premise is NOT
|
|
419
|
+
* structural for an OUT-OF-ENGINE publisher (a host compiling its own settings into an exact-match ask
|
|
420
|
+
* closure is fail-open on these spellings while the audit stays silent) — which is why the underlying
|
|
421
|
+
* judgment predicates (`isNamespacedCoveringRuleName` / `namespacedRuleNameCovers`) ARE exported from
|
|
422
|
+
* the package index: an external lane either compiles through `createPermissionRulePolicy` or
|
|
423
|
+
* self-checks with those.
|
|
418
424
|
*/
|
|
419
425
|
export declare function namespacedCoveringEntries(entries: readonly string[] | undefined): readonly string[];
|
|
420
426
|
/** Does any covering entry reach `toolName`? See {@link namespacedCoveringEntries}. */
|