@sema-agent/core 5.13.0 → 5.14.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 +296 -0
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +4 -0
- package/dist/agents/subagent.js +133 -41
- package/dist/brain/anthropic.js +33 -10
- package/dist/brain/context-overflow.d.ts +20 -0
- package/dist/brain/context-overflow.js +58 -0
- package/dist/brain/open-responses.js +24 -10
- package/dist/brain/openai.js +29 -11
- package/dist/brain/request-params.d.ts +2 -0
- package/dist/brain/request-params.js +16 -0
- package/dist/brain/stream-engine.d.ts +9 -1
- package/dist/brain/stream-engine.js +256 -27
- package/dist/brain/timeout.d.ts +1 -0
- package/dist/brain/timeout.js +1 -0
- package/dist/core/a2a.d.ts +2 -2
- package/dist/core/a2a.js +3 -3
- package/dist/core/ask-question.d.ts +47 -2
- package/dist/core/ask-question.js +209 -28
- package/dist/core/background-agent-store.d.ts +2 -0
- package/dist/core/checkpoint-store.d.ts +41 -17
- package/dist/core/checkpoint-store.js +114 -3
- package/dist/core/hooks.d.ts +24 -2
- package/dist/core/hooks.js +97 -10
- package/dist/core/human-input-projection.d.ts +12 -0
- package/dist/core/human-input-projection.js +27 -0
- package/dist/core/mcp.d.ts +7 -2
- package/dist/core/mcp.js +7 -7
- package/dist/core/memory-admission.d.ts +4 -0
- package/dist/core/memory-admission.js +3 -0
- package/dist/core/runner/assemble-result.d.ts +1 -0
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/prepare-task.d.ts +16 -6
- package/dist/core/runner/prepare-task.js +301 -24
- package/dist/core/runner/runtask.d.ts +3 -6
- package/dist/core/runner/runtask.js +186 -36
- package/dist/core/runner/tool-output-projection.js +1 -0
- package/dist/core/session-store.d.ts +3 -0
- package/dist/core/session-store.js +4 -0
- package/dist/core/session.d.ts +1 -0
- package/dist/core/store-contracts/background-agent-store-contract.js +19 -0
- package/dist/core/store-contracts/checkpoint-store-contract.js +62 -3
- package/dist/core/task-notification.d.ts +2 -0
- package/dist/core/task-notification.js +5 -3
- package/dist/core/task-registry-agent.d.ts +1 -0
- package/dist/core/task-registry-agent.js +6 -0
- package/dist/core/task-registry.d.ts +1 -0
- package/dist/core/task-registry.js +4 -1
- package/dist/core/tool-policy.d.ts +5 -0
- package/dist/core/tool-policy.js +2 -1
- package/dist/core/types.d.ts +32 -1
- package/dist/core/wiring-manifest.d.ts +97 -0
- package/dist/core/wiring-manifest.js +186 -0
- package/dist/engine/compaction/compaction.js +2 -2
- package/dist/engine/harness/agent-harness.d.ts +2 -1
- package/dist/engine/harness/agent-harness.js +8 -1
- package/dist/engine/harness/types.d.ts +3 -1
- package/dist/engine/llm/types.d.ts +7 -0
- package/dist/engine/llm/types.js +8 -1
- package/dist/engine/session/import-validate.d.ts +6 -1
- package/dist/engine/session/import-validate.js +29 -6
- package/dist/engine/session/memory-repo.d.ts +3 -1
- package/dist/engine/session/memory-repo.js +2 -2
- package/dist/index.d.ts +7 -4
- package/dist/index.js +7 -4
- package/dist/internal/harness-types.d.ts +1 -1
- package/dist/internal/llm.d.ts +2 -2
- package/dist/internal/llm.js +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +4 -0
- package/dist/orchestration/run-workflow-tool.js +3 -0
- package/dist/orchestration/workflow-types.d.ts +8 -0
- package/dist/orchestration/workflow-types.js +14 -0
- package/dist/orchestration/workflow.d.ts +4 -0
- package/dist/orchestration/workflow.js +134 -5
- package/dist/prompts/default.js +1 -1
- package/dist/stores/file/checkpoint-store.d.ts +3 -5
- package/dist/stores/file/checkpoint-store.js +31 -2
- package/dist/stores/file/index.js +1 -1
- package/dist/stores/file/session-store.d.ts +3 -1
- package/dist/stores/file/session-store.js +2 -2
- package/dist/stores/file/shared-ledger.js +8 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +3 -0
- package/dist/tools/fs/bash-readonly-classifier.js +94 -0
- package/dist/tools/fs/fs-bash.js +31 -12
- package/dist/tools/fs/safety.js +34 -10
- package/package.json +1 -1
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
|
|
2
|
-
import {
|
|
2
|
+
import { snapshotActorAssertion } from "../../internal/llm.js";
|
|
3
|
+
import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, MAX_SUPPORTED_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
|
|
3
4
|
import { engineVersion } from "../version.js";
|
|
4
5
|
import { CONFIG_CATALOG_VERSION, declarationReasons, resolveEffectiveConfig } from "../../config/catalog.js";
|
|
5
6
|
import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
|
|
6
7
|
import { DEFAULT_COMPACTION_INSTRUCTIONS, createRapidRefillState, isCompactionManualCancel, maybeCompact, nextTrimForceBackoff, recordCompactionAndCheckRapidRefill, sanitizeCompactionSettings } from "../auto-compaction.js";
|
|
7
|
-
import { ASK_USER_QUESTION_TOOL_NAME,
|
|
8
|
+
import { ASK_USER_QUESTION_TOOL_NAME, canonicalizeCapturedPlainData, classifyQuestionOutcome, isLiveQuestionFace, markBoundOnlyQuestionFace } from "../ask-question.js";
|
|
9
|
+
import { boundInputHashOf } from "../canonical-json.js";
|
|
8
10
|
import { computeCostMicroUsd, modelCostToPricing } from "../pricing.js";
|
|
9
11
|
import { emitTrace } from "../trace.js";
|
|
10
12
|
import { emitTaskOutcome } from "../task-outcome.js";
|
|
@@ -31,6 +33,7 @@ import { hasVerifiableStructureSignal } from "./grounding-signal.js";
|
|
|
31
33
|
import { hasDestroy, isIsolated } from "../remote-env.js";
|
|
32
34
|
import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
|
|
33
35
|
import { formatHookFeedback } from "../hooks.js";
|
|
36
|
+
import { buildHumanInputEvent, projectHumanInput } from "../human-input-projection.js";
|
|
34
37
|
import { delimitUntrusted, inlineUntrusted, sanitizeUntrustedText } from "../untrusted-text.js";
|
|
35
38
|
import { reconcileInterruptedSession } from "../session-reconcile.js";
|
|
36
39
|
import { RunnerSharedToolResultStore } from "../tool-result-store.js";
|
|
@@ -52,6 +55,15 @@ function createRunState() {
|
|
|
52
55
|
attach: { attachmentsCfg: undefined, agentListingOn: false, skillsListingOn: false, attachState: undefined, dateState: undefined, instrProbe: undefined, instrState: undefined, sizeGuidelineState: undefined, attachmentsInjected: 0 },
|
|
53
56
|
};
|
|
54
57
|
}
|
|
58
|
+
const humanInputOrdinals = new WeakMap();
|
|
59
|
+
function nextHumanInputSeq(key) {
|
|
60
|
+
let box = humanInputOrdinals.get(key);
|
|
61
|
+
if (box === undefined) {
|
|
62
|
+
box = { n: 0 };
|
|
63
|
+
humanInputOrdinals.set(key, box);
|
|
64
|
+
}
|
|
65
|
+
return ++box.n;
|
|
66
|
+
}
|
|
55
67
|
const MAX_CONSECUTIVE_COMPACTION_FAILURES = 3;
|
|
56
68
|
const STOP_HOOK_BLOCK_CAP = 8;
|
|
57
69
|
const COMPACTION_REGROWTH_FACTOR = 1.5;
|
|
@@ -139,7 +151,15 @@ function deepJsonEqual(a, b) {
|
|
|
139
151
|
if (aArr && bArr) {
|
|
140
152
|
if (a.length !== b.length)
|
|
141
153
|
return false;
|
|
142
|
-
|
|
154
|
+
for (let i = 0; i < a.length; i++) {
|
|
155
|
+
if (!deepJsonEqual(a[i], b[i]))
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
const aExtra = Object.keys(a).filter((k) => !isCanonicalIndexKey(k, a.length));
|
|
159
|
+
const bExtra = Object.keys(b).filter((k) => !isCanonicalIndexKey(k, b.length));
|
|
160
|
+
if (aExtra.length !== bExtra.length)
|
|
161
|
+
return false;
|
|
162
|
+
return aExtra.every((k) => Object.prototype.hasOwnProperty.call(b, k) && deepJsonEqual(Reflect.get(a, k), Reflect.get(b, k)));
|
|
143
163
|
}
|
|
144
164
|
const ao = a;
|
|
145
165
|
const bo = b;
|
|
@@ -149,6 +169,9 @@ function deepJsonEqual(a, b) {
|
|
|
149
169
|
return false;
|
|
150
170
|
return aKeys.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && deepJsonEqual(ao[k], bo[k]));
|
|
151
171
|
}
|
|
172
|
+
function isCanonicalIndexKey(key, length) {
|
|
173
|
+
return /^(0|[1-9]\d*)$/.test(key) && Number(key) < length;
|
|
174
|
+
}
|
|
152
175
|
function sameWinner(a, b) {
|
|
153
176
|
return (a.boundCallId === b.boundCallId &&
|
|
154
177
|
a.decision === b.decision &&
|
|
@@ -160,14 +183,18 @@ function pendingContentAskCallId(cp) {
|
|
|
160
183
|
? cp.pendingAction.toolCallId
|
|
161
184
|
: undefined;
|
|
162
185
|
}
|
|
163
|
-
function answerFaceForRedeemedCall(answer, redeemedCallId, base) {
|
|
164
|
-
|
|
165
|
-
|
|
186
|
+
function answerFaceForRedeemedCall(answer, redeemedCallId, base, redeemedQuestionsHash) {
|
|
187
|
+
let consumed = false;
|
|
188
|
+
const face = async (req, signal) => {
|
|
189
|
+
if (!consumed && req.toolCallId === redeemedCallId && boundInputHashOf(req.questions) === redeemedQuestionsHash) {
|
|
190
|
+
consumed = true;
|
|
166
191
|
return answer;
|
|
192
|
+
}
|
|
167
193
|
if (base !== undefined)
|
|
168
194
|
return base(req, signal);
|
|
169
195
|
throw new Error("this resumed leg's answer was bound to the decided question only — a new question has no answer on this leg");
|
|
170
196
|
};
|
|
197
|
+
return isLiveQuestionFace(base) ? face : markBoundOnlyQuestionFace(face);
|
|
171
198
|
}
|
|
172
199
|
function writeFamilyOfCanonical(name) {
|
|
173
200
|
if (name === "TaskCreate" || name === "TaskUpdate")
|
|
@@ -1166,6 +1193,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
|
|
|
1166
1193
|
const progress = {
|
|
1167
1194
|
type: "task_progress",
|
|
1168
1195
|
taskId: rs.telemetry.taskId,
|
|
1196
|
+
...(internals?.delegationTaskType !== undefined ? { taskType: internals.delegationTaskType } : {}),
|
|
1169
1197
|
...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
|
|
1170
1198
|
...(subagentName ? { name: subagentName } : {}),
|
|
1171
1199
|
usage: { totalTokens: stats.tokens, toolUses: stats.toolCalls, durationMs: Date.now() - rs.telemetry.taskStart },
|
|
@@ -1332,6 +1360,7 @@ export class Runner {
|
|
|
1332
1360
|
return runSideQuery(spec, { brain: this.deps.brain, models: this.deps.models, roles: this.deps.roles });
|
|
1333
1361
|
}
|
|
1334
1362
|
runTaskStream(spec, resume, internals) {
|
|
1363
|
+
const entryActor = spec.actor === undefined ? undefined : snapshotActorAssertion(spec.actor);
|
|
1335
1364
|
const queue = new PushQueue();
|
|
1336
1365
|
const detachHub = new ToolDetachHub();
|
|
1337
1366
|
let resultValue;
|
|
@@ -1386,7 +1415,7 @@ export class Runner {
|
|
|
1386
1415
|
publishReady(h);
|
|
1387
1416
|
}, (s) => {
|
|
1388
1417
|
reapHandle = s;
|
|
1389
|
-
}, manualCompactRef, taskIdRef, resume, { ...(internals ?? {}), detachHub }, notifyRef);
|
|
1418
|
+
}, manualCompactRef, taskIdRef, resume, { ...(internals ?? {}), detachHub }, notifyRef, entryActor);
|
|
1390
1419
|
}
|
|
1391
1420
|
finally {
|
|
1392
1421
|
releaseLock?.();
|
|
@@ -1523,7 +1552,29 @@ export class Runner {
|
|
|
1523
1552
|
if (options?.trusted && sanitizeUntrustedText(text) !== text) {
|
|
1524
1553
|
throw steeringError("trusted steering text must not contain a </system-reminder> tag", "steering.invalid_content");
|
|
1525
1554
|
}
|
|
1526
|
-
const
|
|
1555
|
+
const actor = options?.actor === undefined ? undefined : snapshotActorAssertion(options.actor);
|
|
1556
|
+
const projected = projectHumanInput({ text, actor, source: "steer" });
|
|
1557
|
+
const payload = options?.trusted ? formatHookFeedback(projected) : projected;
|
|
1558
|
+
const mintsAFrame = payload.trim().length !== 0;
|
|
1559
|
+
const emitAccepted = (h) => {
|
|
1560
|
+
if (!mintsAFrame)
|
|
1561
|
+
return;
|
|
1562
|
+
queue.push({
|
|
1563
|
+
...buildHumanInputEvent({
|
|
1564
|
+
carrier: "steer",
|
|
1565
|
+
source: "steer",
|
|
1566
|
+
delivery: "queued",
|
|
1567
|
+
sessionSeq: nextHumanInputSeq(h.harness),
|
|
1568
|
+
...(actor !== undefined ? { actor } : {}),
|
|
1569
|
+
...(actor?.issuer !== undefined ? { issuer: actor.issuer } : {}),
|
|
1570
|
+
...(spec.principal !== undefined ? { principal: spec.principal } : {}),
|
|
1571
|
+
}),
|
|
1572
|
+
eventId: uuidv7(),
|
|
1573
|
+
...(internals?.parentToolCallId !== undefined
|
|
1574
|
+
? { parentToolCallId: internals.parentToolCallId, ...(spec.taskId !== undefined ? { sourceTaskId: spec.taskId } : {}) }
|
|
1575
|
+
: {}),
|
|
1576
|
+
});
|
|
1577
|
+
};
|
|
1527
1578
|
const deliver = async () => {
|
|
1528
1579
|
if (resultValue)
|
|
1529
1580
|
throw steeringError("the task has already finished");
|
|
@@ -1531,7 +1582,8 @@ export class Runner {
|
|
|
1531
1582
|
if (!h)
|
|
1532
1583
|
throw steeringError("the task is not running");
|
|
1533
1584
|
try {
|
|
1534
|
-
await h.harness.steer(payload, { provenance: "engine-note" });
|
|
1585
|
+
await h.harness.steer(payload, { provenance: "engine-note", ...(actor !== undefined ? { actor } : {}) });
|
|
1586
|
+
emitAccepted(h);
|
|
1535
1587
|
return;
|
|
1536
1588
|
}
|
|
1537
1589
|
catch (e) {
|
|
@@ -1541,7 +1593,8 @@ export class Runner {
|
|
|
1541
1593
|
const birthDeadline = Date.now() + READY_TIMEOUT_MS;
|
|
1542
1594
|
while (resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) {
|
|
1543
1595
|
try {
|
|
1544
|
-
await h.harness.steer(payload, { provenance: "engine-note" });
|
|
1596
|
+
await h.harness.steer(payload, { provenance: "engine-note", ...(actor !== undefined ? { actor } : {}) });
|
|
1597
|
+
emitAccepted(h);
|
|
1545
1598
|
return;
|
|
1546
1599
|
}
|
|
1547
1600
|
catch (e2) {
|
|
@@ -1658,7 +1711,7 @@ export class Runner {
|
|
|
1658
1711
|
})()),
|
|
1659
1712
|
};
|
|
1660
1713
|
}
|
|
1661
|
-
async runLocked(spec, queue, setResult, onSuggestions, onReady, onSuspend, manualCompactRef, taskIdRef, resume, internals, notifyRef) {
|
|
1714
|
+
async runLocked(spec, queue, setResult, onSuggestions, onReady, onSuspend, manualCompactRef, taskIdRef, resume, internals, notifyRef, entryActor) {
|
|
1662
1715
|
const prepareResume = resume
|
|
1663
1716
|
? {
|
|
1664
1717
|
leafId: resume.cp.leafId,
|
|
@@ -1671,6 +1724,12 @@ export class Runner {
|
|
|
1671
1724
|
executesApprovedAction: resume.cp.pendingAction.kind === "tool_approval" &&
|
|
1672
1725
|
resume.outcome.gate !== "wake" &&
|
|
1673
1726
|
resume.outcome.decision === "allow",
|
|
1727
|
+
...(resume.outcome.gate !== "wake" && resume.outcome.decision === "allow" && pendingContentAskCallId(resume.cp) !== undefined
|
|
1728
|
+
? {
|
|
1729
|
+
redeemedContentAskCallId: pendingContentAskCallId(resume.cp),
|
|
1730
|
+
redeemedContentAskQuestionsHash: boundInputHashOf(resume.cp.pendingAction.args?.questions),
|
|
1731
|
+
}
|
|
1732
|
+
: {}),
|
|
1674
1733
|
}
|
|
1675
1734
|
: undefined;
|
|
1676
1735
|
const taskNotificationQueue = new SystemInjectionQueue();
|
|
@@ -1759,6 +1818,7 @@ export class Runner {
|
|
|
1759
1818
|
const parentToolCallId = internals?.parentToolCallId;
|
|
1760
1819
|
const ident = () => parentToolCallId !== undefined ? { eventId: uuidv7(), parentToolCallId, sourceTaskId: runSourceTaskId } : { eventId: uuidv7() };
|
|
1761
1820
|
notificationIdent = ident;
|
|
1821
|
+
queue.push({ type: "wiring_manifest", manifest: prepared.wiringManifest, ...ident() });
|
|
1762
1822
|
manualCompactRef.emitMooted = (reason) => {
|
|
1763
1823
|
queue.push({ type: "compaction_outcome", outcome: "mooted", trigger: "manual", reason, ...ident() });
|
|
1764
1824
|
};
|
|
@@ -2595,6 +2655,7 @@ export class Runner {
|
|
|
2595
2655
|
let final;
|
|
2596
2656
|
let threw;
|
|
2597
2657
|
let abortedLive = false;
|
|
2658
|
+
let strandedHumanAnswers = [];
|
|
2598
2659
|
try {
|
|
2599
2660
|
if (prepared.abortController.signal.aborted) {
|
|
2600
2661
|
const e = new Error("run aborted");
|
|
@@ -2647,15 +2708,31 @@ export class Runner {
|
|
|
2647
2708
|
}
|
|
2648
2709
|
}
|
|
2649
2710
|
const engineSegments = continuation.length > 0 ? [{ start: 0, end: continuation.length }] : [];
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2711
|
+
const resumeFrames = [
|
|
2712
|
+
...readPendingSteerQueue(resume.cp.state).map((entry) => ({ entry, source: "steer" })),
|
|
2713
|
+
...(resume.wakeMessage !== undefined ? [{ entry: resume.wakeMessage, source: "wake" }] : []),
|
|
2714
|
+
];
|
|
2715
|
+
for (const { entry: steer, source } of resumeFrames) {
|
|
2653
2716
|
const start = continuation.length;
|
|
2717
|
+
const projected = projectHumanInput({ text: steer.text, actor: steer.actor, source });
|
|
2654
2718
|
continuation +=
|
|
2655
2719
|
"\n\n" +
|
|
2656
|
-
(steer.trusted ? formatHookFeedback(
|
|
2720
|
+
(steer.trusted ? formatHookFeedback(projected) : delimitUntrusted("supervisor steering message", projected));
|
|
2657
2721
|
if (steer.trusted)
|
|
2658
2722
|
engineSegments.push({ start, end: continuation.length });
|
|
2723
|
+
queue.push({
|
|
2724
|
+
...buildHumanInputEvent({
|
|
2725
|
+
carrier: source,
|
|
2726
|
+
source,
|
|
2727
|
+
delivery: "applied",
|
|
2728
|
+
sessionSeq: nextHumanInputSeq(prepared.harness),
|
|
2729
|
+
inputId: steer.inputId,
|
|
2730
|
+
...(steer.actor !== undefined ? { actor: steer.actor } : {}),
|
|
2731
|
+
...(steer.actor?.issuer !== undefined ? { issuer: steer.actor.issuer } : {}),
|
|
2732
|
+
...(spec.principal !== undefined ? { principal: spec.principal } : {}),
|
|
2733
|
+
}),
|
|
2734
|
+
...ident(),
|
|
2735
|
+
});
|
|
2659
2736
|
}
|
|
2660
2737
|
if (resume !== undefined)
|
|
2661
2738
|
resume.decisionDelivered = true;
|
|
@@ -2663,7 +2740,9 @@ export class Runner {
|
|
|
2663
2740
|
}
|
|
2664
2741
|
}
|
|
2665
2742
|
else {
|
|
2666
|
-
|
|
2743
|
+
const objectiveActor = entryActor;
|
|
2744
|
+
const projectedObjective = projectHumanInput({ text: spec.objective, actor: objectiveActor, source: "objective" });
|
|
2745
|
+
let effectiveObjective = projectedObjective;
|
|
2667
2746
|
let promptBlocked = false;
|
|
2668
2747
|
const userPromptSubmit = (spec.hooks ?? this.deps.hooks)?.userPromptSubmit;
|
|
2669
2748
|
if (userPromptSubmit) {
|
|
@@ -2674,7 +2753,7 @@ export class Runner {
|
|
|
2674
2753
|
promptBlocked = true;
|
|
2675
2754
|
}
|
|
2676
2755
|
else if (decision?.additionalContext) {
|
|
2677
|
-
effectiveObjective = `${formatHookFeedback(decision.additionalContext)}\n\n${
|
|
2756
|
+
effectiveObjective = `${formatHookFeedback(decision.additionalContext)}\n\n${projectedObjective}`;
|
|
2678
2757
|
}
|
|
2679
2758
|
}
|
|
2680
2759
|
catch (hookErr) {
|
|
@@ -2775,10 +2854,23 @@ export class Runner {
|
|
|
2775
2854
|
rs.attach.attachmentsInjected += firstFrames.length;
|
|
2776
2855
|
if (firstFrames.length > 0)
|
|
2777
2856
|
await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
|
|
2778
|
-
const enginePrefixChars = effectiveObjective.length -
|
|
2857
|
+
const enginePrefixChars = effectiveObjective.length - projectedObjective.length;
|
|
2858
|
+
queue.push({
|
|
2859
|
+
...buildHumanInputEvent({
|
|
2860
|
+
carrier: "objective",
|
|
2861
|
+
source: "objective",
|
|
2862
|
+
delivery: "applied",
|
|
2863
|
+
sessionSeq: nextHumanInputSeq(prepared.harness),
|
|
2864
|
+
...(objectiveActor !== undefined ? { actor: objectiveActor } : {}),
|
|
2865
|
+
...(objectiveActor?.issuer !== undefined ? { issuer: objectiveActor.issuer } : {}),
|
|
2866
|
+
...(spec.principal !== undefined ? { principal: spec.principal } : {}),
|
|
2867
|
+
}),
|
|
2868
|
+
...ident(),
|
|
2869
|
+
});
|
|
2779
2870
|
final = await withBrainSinks(() => prepared.harness.prompt(effectiveObjective, {
|
|
2780
2871
|
...(images && images.length > 0 ? { images } : {}),
|
|
2781
2872
|
...(enginePrefixChars > 0 ? { enginePrefixChars } : {}),
|
|
2873
|
+
...(objectiveActor !== undefined ? { actor: objectiveActor } : {}),
|
|
2782
2874
|
}));
|
|
2783
2875
|
}
|
|
2784
2876
|
}
|
|
@@ -2801,6 +2893,7 @@ export class Runner {
|
|
|
2801
2893
|
prepared.lspDiagnostics?.registry.releaseRun(prepared.lspDiagnostics.runIdent);
|
|
2802
2894
|
prepared.abortController.abort();
|
|
2803
2895
|
prepared.releaseSignal();
|
|
2896
|
+
strandedHumanAnswers = prepared.settleContentAskBindings();
|
|
2804
2897
|
try {
|
|
2805
2898
|
prepared.harness.recoverUndrainedEngineNotes();
|
|
2806
2899
|
}
|
|
@@ -2939,6 +3032,7 @@ export class Runner {
|
|
|
2939
3032
|
model: prepared.model.id,
|
|
2940
3033
|
unpricedSpend: rs.telemetry.unpricedSpend,
|
|
2941
3034
|
rewindNotes: prepared.rewindNotes,
|
|
3035
|
+
strandedHumanAnswers,
|
|
2942
3036
|
remoteEnvFailures: prepared.remoteEnvFailures,
|
|
2943
3037
|
retryAfterMs: rs.limits.platformTerminal?.retryAfterMs,
|
|
2944
3038
|
abortedForTimeout: timeout.fired,
|
|
@@ -3075,6 +3169,7 @@ export class Runner {
|
|
|
3075
3169
|
const terminalTick = {
|
|
3076
3170
|
type: "task_progress",
|
|
3077
3171
|
taskId: rs.telemetry.taskId,
|
|
3172
|
+
...(internals?.delegationTaskType !== undefined ? { taskType: internals.delegationTaskType } : {}),
|
|
3078
3173
|
...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
|
|
3079
3174
|
...(subagentName ? { name: subagentName } : {}),
|
|
3080
3175
|
usage: { totalTokens: stats.tokens, toolUses: stats.toolCalls, durationMs: Date.now() - rs.telemetry.taskStart },
|
|
@@ -3335,7 +3430,7 @@ export class Runner {
|
|
|
3335
3430
|
if (outcome.message !== undefined) {
|
|
3336
3431
|
wakeMessage = validatePendingSteer(outcome.message);
|
|
3337
3432
|
}
|
|
3338
|
-
else if (cp.state.
|
|
3433
|
+
else if (readPendingSteerQueue(cp.state).length === 0) {
|
|
3339
3434
|
throw new CheckpointError("wake.nothing_to_deliver", "cannot wake: no message was supplied and the checkpoint holds no parked pendingSteer — an empty " +
|
|
3340
3435
|
"wake would burn the checkpoint on a blank continuation; supply `message` or park a steer first");
|
|
3341
3436
|
}
|
|
@@ -3349,8 +3444,7 @@ export class Runner {
|
|
|
3349
3444
|
if (!gateMatch) {
|
|
3350
3445
|
throw new CheckpointError("checkpoint.gate_mismatch", `resume outcome (gate "${outcome.gate}") does not match checkpoint gate "${cp.gate.kind}" — resume serves human/policy_ask, irreversible_ask/policy_ask, resource_limit/resource_limit, needs_review/dry_run_review, and plan_review/plan_review`);
|
|
3351
3446
|
}
|
|
3352
|
-
const reasonBearing = (outcome.gate === "
|
|
3353
|
-
(outcome.gate === "dry_run_review" && outcome.decision === "reject") ||
|
|
3447
|
+
const reasonBearing = (outcome.gate === "dry_run_review" && outcome.decision === "reject") ||
|
|
3354
3448
|
(outcome.gate === "plan_review" && outcome.decision === "reject");
|
|
3355
3449
|
if (reasonBearing && outcome.reason && sanitizeUntrustedText(outcome.reason) !== outcome.reason) {
|
|
3356
3450
|
throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
|
|
@@ -3374,14 +3468,60 @@ export class Runner {
|
|
|
3374
3468
|
throw new CheckpointError("checkpoint.reopen_revote", "an env_failed reopen replays the ALREADY-RECORDED review decision — refusing a re-vote whose edited plan differs from the recorded one");
|
|
3375
3469
|
}
|
|
3376
3470
|
}
|
|
3377
|
-
|
|
3471
|
+
let suppliedAnswer;
|
|
3472
|
+
let redeemedAnswer;
|
|
3473
|
+
let plainPolicyOutcome;
|
|
3474
|
+
if (cp.gate.kind === "human" || cp.gate.kind === "irreversible_ask") {
|
|
3475
|
+
const decide = outcome;
|
|
3476
|
+
suppliedAnswer = decide.answer;
|
|
3477
|
+
if (suppliedAnswer !== undefined) {
|
|
3478
|
+
const reading = classifyQuestionOutcome(suppliedAnswer);
|
|
3479
|
+
if (reading.shape !== "answered") {
|
|
3480
|
+
throw new CheckpointError("checkpoint.invalid_outcome", "resume carried a content-ask `answer` that does not read as an answer (malformed, sparse, or not capturable as plain data) — " +
|
|
3481
|
+
"refusing pre-CAS rather than fabricating an empty human response; the checkpoint stays pending", { field: "answer" });
|
|
3482
|
+
}
|
|
3483
|
+
redeemedAnswer = reading.answer;
|
|
3484
|
+
}
|
|
3485
|
+
const updatedInputRaw = decide.updatedInput;
|
|
3486
|
+
let updatedInput;
|
|
3487
|
+
try {
|
|
3488
|
+
updatedInput = updatedInputRaw === undefined ? undefined : structuredClone(updatedInputRaw);
|
|
3489
|
+
}
|
|
3490
|
+
catch {
|
|
3491
|
+
throw new CheckpointError("checkpoint.invalid_outcome", "resume carried an `updatedInput` that could not be captured as plain data — refusing pre-CAS; the checkpoint stays pending");
|
|
3492
|
+
}
|
|
3493
|
+
if (Object.is(updatedInput, -0))
|
|
3494
|
+
updatedInput = 0;
|
|
3495
|
+
if (updatedInput !== undefined && !canonicalizeCapturedPlainData(updatedInput)) {
|
|
3496
|
+
throw new CheckpointError("checkpoint.invalid_outcome", "resume carried an `updatedInput` outside the plain-JSON value domain (a Map/Set/Date/buffer, a sparse or expando-carrying array, a non-finite number, or a cycle) — " +
|
|
3497
|
+
"such a value reads differently on the two sides of the durable store; refusing pre-CAS, the checkpoint stays pending");
|
|
3498
|
+
}
|
|
3499
|
+
const decision = decide.decision;
|
|
3500
|
+
if (decision !== "allow" && decision !== "deny") {
|
|
3501
|
+
throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${String(decision)}" is outside the policy_ask domain — a decide is exactly "allow" or "deny"; refusing pre-CAS, the checkpoint stays pending`);
|
|
3502
|
+
}
|
|
3503
|
+
const reason = decide.reason;
|
|
3504
|
+
plainPolicyOutcome = {
|
|
3505
|
+
gate: "policy_ask",
|
|
3506
|
+
boundCallId: decide.boundCallId,
|
|
3507
|
+
boundInputHash: decide.boundInputHash,
|
|
3508
|
+
decision,
|
|
3509
|
+
...(updatedInput !== undefined ? { updatedInput } : {}),
|
|
3510
|
+
...(reason !== undefined ? { reason } : {}),
|
|
3511
|
+
...(redeemedAnswer !== undefined ? { answer: redeemedAnswer } : {}),
|
|
3512
|
+
};
|
|
3513
|
+
if (plainPolicyOutcome.decision === "deny" && plainPolicyOutcome.reason && sanitizeUntrustedText(plainPolicyOutcome.reason) !== plainPolicyOutcome.reason) {
|
|
3514
|
+
throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
if (plainPolicyOutcome !== undefined) {
|
|
3378
3518
|
const boundTo = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.toolCallId : undefined;
|
|
3379
|
-
if (
|
|
3380
|
-
throw new CheckpointError("checkpoint.invalid_outcome", `resume boundCallId "${
|
|
3519
|
+
if (plainPolicyOutcome.boundCallId !== boundTo) {
|
|
3520
|
+
throw new CheckpointError("checkpoint.invalid_outcome", `resume boundCallId "${plainPolicyOutcome.boundCallId}" does not match the checkpoint's pending tool call "${boundTo ?? "(none)"}" — the decision-action binding (design/80 D-1) failed; refusing to apply a decision bound to a different action`, { field: "boundCallId" });
|
|
3381
3521
|
}
|
|
3382
3522
|
const boundHash = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.boundInputHash : undefined;
|
|
3383
3523
|
if (boundHash !== undefined) {
|
|
3384
|
-
if (
|
|
3524
|
+
if (plainPolicyOutcome.boundInputHash !== boundHash) {
|
|
3385
3525
|
throw new CheckpointError("checkpoint.invalid_outcome", "resume boundInputHash does not match the checkpoint's pending tool call input — the decision-action input binding (design/80 D-1 §2) failed; refusing to apply an approval bound to a different input (TOCTOU re-mint guard)", { field: "boundInputHash" });
|
|
3386
3526
|
}
|
|
3387
3527
|
}
|
|
@@ -3392,20 +3532,26 @@ export class Runner {
|
|
|
3392
3532
|
if (cp.resolvedOutcome === undefined) {
|
|
3393
3533
|
throw new CheckpointError("checkpoint.reopen_revote", "checkpoint was reopened after an env-restore failure (env_failed) but carries no persisted winner to replay — refusing to resume (inconsistent row, fail-closed)");
|
|
3394
3534
|
}
|
|
3395
|
-
const incoming = winnerFromOutcome(
|
|
3396
|
-
|
|
3535
|
+
const incoming = winnerFromOutcome(plainPolicyOutcome);
|
|
3536
|
+
const persistedWinner = (() => {
|
|
3537
|
+
const w = cp.resolvedOutcome;
|
|
3538
|
+
if (w?.answer === undefined)
|
|
3539
|
+
return w;
|
|
3540
|
+
const reading = classifyQuestionOutcome(w.answer);
|
|
3541
|
+
return reading.shape === "answered" ? { ...w, answer: reading.answer } : w;
|
|
3542
|
+
})();
|
|
3543
|
+
if (incoming === undefined || !sameWinner(incoming, persistedWinner)) {
|
|
3397
3544
|
throw new CheckpointError("checkpoint.reopen_revote", `this checkpoint was reopened after an env-restore failure (env_failed) — a re-resume is a system retry of the already-approved action, not a re-vote; the supplied decision must replay the persisted winner (boundCallId "${cp.resolvedOutcome.boundCallId}", decision "${cp.resolvedOutcome.decision}"), refusing a different decision`);
|
|
3398
3545
|
}
|
|
3399
3546
|
}
|
|
3400
3547
|
const contentAskCallId = pendingContentAskCallId(cp);
|
|
3401
|
-
if (
|
|
3548
|
+
if (suppliedAnswer !== undefined && (contentAskCallId === undefined || plainPolicyOutcome.decision !== "allow")) {
|
|
3402
3549
|
throw new CheckpointError("checkpoint.invalid_outcome", contentAskCallId === undefined
|
|
3403
3550
|
? "resume carried a content-ask `answer` but the checkpoint's pending call is not the reserved question tool — an answer has no consumer on a side-effecting tool's approval; refusing rather than dropping it silently"
|
|
3404
3551
|
: "resume carried a content-ask `answer` on a `deny` — a denial injects a refusal, never an answer; refusing rather than dropping it silently", { field: "answer" });
|
|
3405
3552
|
}
|
|
3406
|
-
if (contentAskCallId !== undefined &&
|
|
3407
|
-
|
|
3408
|
-
if (liveFace === undefined || liveFace === QUESTION_AWAITS_RESUME) {
|
|
3553
|
+
if (contentAskCallId !== undefined && plainPolicyOutcome.decision === "allow" && suppliedAnswer === undefined) {
|
|
3554
|
+
if (!isLiveQuestionFace(taskConfig.onQuestion ?? this.deps.onQuestion)) {
|
|
3409
3555
|
throw new CheckpointError("checkpoint.invalid_outcome", "resume approved a content-ask (the reserved question tool) without an `answer`, and this resume has no live answering face — " +
|
|
3410
3556
|
'executing the question against nothing would hand the model a fabricated "no human is available" default while consuming the approval; ' +
|
|
3411
3557
|
"re-resume with the operator's answer on the outcome (or deny it), the checkpoint stays pending", { field: "answer" });
|
|
@@ -3442,7 +3588,8 @@ export class Runner {
|
|
|
3442
3588
|
"rejected pre-CAS (the checkpoint stays pending) — re-resume with the full original chain");
|
|
3443
3589
|
}
|
|
3444
3590
|
}
|
|
3445
|
-
const
|
|
3591
|
+
const outcomeForStore = plainPolicyOutcome ?? outcome;
|
|
3592
|
+
const won = await store.resolve(token, cp.scope, outcomeForStore, { rev: cp.rev ?? 0 });
|
|
3446
3593
|
if (!won) {
|
|
3447
3594
|
const live = await store.get(token);
|
|
3448
3595
|
if (live?.status === "pending") {
|
|
@@ -3456,8 +3603,8 @@ export class Runner {
|
|
|
3456
3603
|
await internals.afterCheckpointResolve();
|
|
3457
3604
|
consumeFlipDone = true;
|
|
3458
3605
|
}
|
|
3459
|
-
const answerFace =
|
|
3460
|
-
? answerFaceForRedeemedCall(
|
|
3606
|
+
const answerFace = plainPolicyOutcome !== undefined && redeemedAnswer !== undefined
|
|
3607
|
+
? answerFaceForRedeemedCall(redeemedAnswer, plainPolicyOutcome.boundCallId, taskConfig.onQuestion ?? this.deps.onQuestion, boundInputHashOf(cp.pendingAction.args?.questions))
|
|
3461
3608
|
: undefined;
|
|
3462
3609
|
const spec = {
|
|
3463
3610
|
...taskConfig,
|
|
@@ -3496,7 +3643,7 @@ export class Runner {
|
|
|
3496
3643
|
? new CheckpointError("checkpoint.reopen_failed", "the resume was aborted before the approved action could run AND the store refused to reopen the checkpoint — the approval is terminally consumed and the suspended work was not executed; a retry needs a fresh approval")
|
|
3497
3644
|
: new CheckpointError("checkpoint.reopen_failed", "the resume was aborted before the approved action could run and the reopen attempt FAILED IN FLIGHT — the checkpoint's state is unprovable from here: it may already be pending again. Re-read it before deciding; do NOT issue a fresh approval on the assumption the old one is dead (the approved action did NOT run either way)");
|
|
3498
3645
|
}
|
|
3499
|
-
return this.runTaskStream(spec, { cp, outcome, onEnvRestoreFailed, ...(wakeMessage !== undefined ? { wakeMessage } : {}) }, internals);
|
|
3646
|
+
return this.runTaskStream(spec, { cp, outcome: plainPolicyOutcome ?? outcome, onEnvRestoreFailed, ...(wakeMessage !== undefined ? { wakeMessage } : {}) }, internals);
|
|
3500
3647
|
}
|
|
3501
3648
|
async applyResumeDecision(prepared, resume, emit, emitCommitted, onResolvedToolSuccess, onExecuteStart) {
|
|
3502
3649
|
const { pendingAction } = resume.cp;
|
|
@@ -3552,7 +3699,7 @@ export class Runner {
|
|
|
3552
3699
|
const { outcome } = resume;
|
|
3553
3700
|
if (pendingAction.kind !== "tool_approval" || outcome.gate !== "policy_ask")
|
|
3554
3701
|
return;
|
|
3555
|
-
const resolvedArgs = pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME ? pendingAction.args : (outcome.updatedInput
|
|
3702
|
+
const resolvedArgs = pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME ? pendingAction.args : (outcome.updatedInput !== undefined ? outcome.updatedInput : pendingAction.args);
|
|
3556
3703
|
const pendingLabel = (() => { const l = prepared.tools.find((t) => t.name === pendingAction.toolName)?.label; return l !== undefined && l !== pendingAction.toolName ? { label: l } : {}; })();
|
|
3557
3704
|
emit({ type: "tool_start", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, args: resolvedArgs });
|
|
3558
3705
|
const emitEnd = (isError, result) => emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError) });
|
|
@@ -3563,6 +3710,9 @@ export class Runner {
|
|
|
3563
3710
|
emitCommitted(eid, "toolResult", pendingAction.toolCallId);
|
|
3564
3711
|
return;
|
|
3565
3712
|
}
|
|
3713
|
+
if (outcome.decision !== "allow") {
|
|
3714
|
+
throw new CheckpointError("checkpoint.invalid_outcome", `pending action reached the resolver with decision "${String(outcome.decision)}" — only "allow" executes and only "deny" injects a denial; refusing to execute`);
|
|
3715
|
+
}
|
|
3566
3716
|
if (outcome.decision === "allow" && outcome.updatedInput !== undefined && prepared.basePolicyForResumeEdit) {
|
|
3567
3717
|
const rechecked = refuseOutOfContractDecision(await prepared.basePolicyForResumeEdit.check({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId }, prepared.abortController.signal));
|
|
3568
3718
|
if (rechecked.action === "deny") {
|
|
@@ -57,6 +57,7 @@ export const toolOutputFrom = (result) => {
|
|
|
57
57
|
return { output: raw, truncated: true, totalChars };
|
|
58
58
|
};
|
|
59
59
|
const CC_DETAIL_TYPES = new Set([
|
|
60
|
+
"ask-question",
|
|
60
61
|
"edit", "create", "update", "bash", "notebook-edit", "notebook", "file_unchanged", "worktree", "text", "grep", "glob", "mcp",
|
|
61
62
|
"agent", "task", "task-list", "task-output", "workflow-run",
|
|
62
63
|
"web-fetch", "web-search", "todo", "cron-create", "cron-delete", "cron-list", "image",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type SessionRepo } from "../internal/harness.js";
|
|
2
2
|
import { type AcquiredSession, type SessionStore, type SessionStoreSummary } from "./session.js";
|
|
3
|
+
import type { StoreDurability } from "./checkpoint-store.js";
|
|
3
4
|
import { SESSION_DEFAULT_TTL_DAYS } from "../config/defaults.js";
|
|
4
5
|
export { SESSION_DEFAULT_TTL_DAYS };
|
|
5
6
|
export type EvictPolicy = "delete" | "forget";
|
|
@@ -8,9 +9,11 @@ export interface TtlSessionStoreOptions {
|
|
|
8
9
|
sweepIntervalMs?: number;
|
|
9
10
|
repo?: SessionRepo;
|
|
10
11
|
evict?: EvictPolicy;
|
|
12
|
+
durability?: StoreDurability;
|
|
11
13
|
}
|
|
12
14
|
export declare class TtlSessionStore implements SessionStore {
|
|
13
15
|
readonly retention: "none";
|
|
16
|
+
readonly durability?: StoreDurability;
|
|
14
17
|
private repo;
|
|
15
18
|
private entries;
|
|
16
19
|
private owners;
|
|
@@ -5,6 +5,7 @@ import { SESSION_DEFAULT_TTL_DAYS } from "../config/defaults.js";
|
|
|
5
5
|
export { SESSION_DEFAULT_TTL_DAYS };
|
|
6
6
|
export class TtlSessionStore {
|
|
7
7
|
retention = "none";
|
|
8
|
+
durability;
|
|
8
9
|
repo;
|
|
9
10
|
entries = new Map();
|
|
10
11
|
owners = new Map();
|
|
@@ -15,6 +16,9 @@ export class TtlSessionStore {
|
|
|
15
16
|
sweepTimer;
|
|
16
17
|
constructor(opts = {}) {
|
|
17
18
|
this.repo = opts.repo ?? new InMemorySessionRepo();
|
|
19
|
+
const durability = opts.durability ?? (opts.repo === undefined ? "process-local" : undefined);
|
|
20
|
+
if (durability !== undefined)
|
|
21
|
+
this.durability = durability;
|
|
18
22
|
this.defaultTtlMs = (opts.defaultTtlDays ?? SESSION_DEFAULT_TTL_DAYS) * DAY_MS;
|
|
19
23
|
this.evictPolicy = opts.evict ?? (opts.repo ? "forget" : "delete");
|
|
20
24
|
if (opts.sweepIntervalMs !== undefined && opts.sweepIntervalMs > 0) {
|
package/dist/core/session.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface SessionStoreSummary {
|
|
|
20
20
|
forkedFrom?: string;
|
|
21
21
|
}
|
|
22
22
|
export interface SessionStore {
|
|
23
|
+
readonly durability?: import("./checkpoint-store.js").StoreDurability;
|
|
23
24
|
acquire(sessionId?: string, opts?: {
|
|
24
25
|
requireExisting?: boolean;
|
|
25
26
|
}): Promise<AcquiredSession>;
|
|
@@ -52,6 +52,25 @@ export async function backgroundAgentStoreContract(mk, runAssertion) {
|
|
|
52
52
|
assert.equal(await store.get("a1", "tenant-C"), null, "an unrelated scope must not see the row");
|
|
53
53
|
assert.equal(await store.get("a-missing", S), null);
|
|
54
54
|
});
|
|
55
|
+
run("the org-admission fields round-trip, and ABSENT stays absent (a backend must not invent a verdict)", async (make) => {
|
|
56
|
+
const store = make();
|
|
57
|
+
await store.put(record({ handle: "a1", admittedOrgScopes: ["org:acme", "org:beta"], admittedOrgWriteScope: "org:acme" }));
|
|
58
|
+
const withVerdict = await held(store, "a1");
|
|
59
|
+
assert.deepEqual(withVerdict.admittedOrgScopes, ["org:acme", "org:beta"]);
|
|
60
|
+
assert.equal(withVerdict.admittedOrgWriteScope, "org:acme");
|
|
61
|
+
await store.put(record({ handle: "a2", admittedOrgScopes: [], admittedOrgWriteScope: null }));
|
|
62
|
+
const readOnly = await held(store, "a2");
|
|
63
|
+
assert.deepEqual(readOnly.admittedOrgScopes, []);
|
|
64
|
+
assert.equal(readOnly.admittedOrgWriteScope, null);
|
|
65
|
+
await store.put(record({ handle: "a3" }));
|
|
66
|
+
const untouched = await held(store, "a3");
|
|
67
|
+
assert.equal(untouched.admittedOrgScopes, undefined);
|
|
68
|
+
assert.equal(untouched.admittedOrgWriteScope, undefined);
|
|
69
|
+
await store.update("a3", S, { ...untouched, admittedOrgScopes: ["org:acme"], admittedOrgWriteScope: null }, { rev: untouched.rev });
|
|
70
|
+
const afterCas = await held(store, "a3");
|
|
71
|
+
assert.deepEqual(afterCas.admittedOrgScopes, ["org:acme"]);
|
|
72
|
+
assert.equal(afterCas.admittedOrgWriteScope, null);
|
|
73
|
+
});
|
|
55
74
|
run("put refuses a scopeless row (a row with no tenant partition would be world-readable)", async (make) => {
|
|
56
75
|
const store = make();
|
|
57
76
|
await assert.rejects(store.put(record({ scope: "" })), "an empty scope must be refused loudly, not stored");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { strict as assert } from "node:assert";
|
|
2
|
-
import { mintCheckpointToken, } from "../checkpoint-store.js";
|
|
2
|
+
import { MAX_PENDING_STEER_ENTRIES, mintCheckpointToken, readPendingSteerQueue, } from "../checkpoint-store.js";
|
|
3
3
|
import { beginContract } from "./contract-harness.js";
|
|
4
4
|
export function createCheckpointFixture(over = {}) {
|
|
5
5
|
const token = over.token ?? mintCheckpointToken();
|
|
@@ -90,9 +90,68 @@ export async function checkpointStoreContract(make, runAssertion) {
|
|
|
90
90
|
const cp = createCheckpointFixture();
|
|
91
91
|
await store.put(cp.token, cp);
|
|
92
92
|
await assert.rejects(store.setPendingSteer(cp.token, cp.scope, { text: "</SYSTEM-REMINDER>", trusted: true }), (e) => e.code === "steering.invalid_content");
|
|
93
|
-
assert.
|
|
93
|
+
assert.deepEqual(readPendingSteerQueue((await store.get(cp.token)).state), []);
|
|
94
94
|
assert.equal(await store.setPendingSteer(cp.token, cp.scope, { text: "go", trusted: false }), true);
|
|
95
|
-
|
|
95
|
+
const q = readPendingSteerQueue((await store.get(cp.token)).state);
|
|
96
|
+
assert.equal(q.length, 1);
|
|
97
|
+
assert.equal(q[0].text, "go");
|
|
98
|
+
assert.equal(q[0].trusted, false);
|
|
99
|
+
assert.equal(q[0].seq, 1);
|
|
100
|
+
assert.equal((await store.get(cp.token)).state.pendingSteer, undefined);
|
|
101
|
+
});
|
|
102
|
+
run("setPendingSteer APPENDS: two steers both survive in issue order, each keeping its frozen trust", async () => {
|
|
103
|
+
const store = make();
|
|
104
|
+
const cp = createCheckpointFixture();
|
|
105
|
+
await store.put(cp.token, cp);
|
|
106
|
+
assert.equal(await store.setPendingSteer(cp.token, cp.scope, { text: "first", trusted: false }), true);
|
|
107
|
+
assert.equal(await store.setPendingSteer(cp.token, cp.scope, { text: "second", trusted: true }), true);
|
|
108
|
+
const q = readPendingSteerQueue((await store.get(cp.token)).state);
|
|
109
|
+
assert.deepEqual(q.map((e) => e.text), ["first", "second"]);
|
|
110
|
+
assert.deepEqual(q.map((e) => e.trusted), [false, true]);
|
|
111
|
+
assert.deepEqual(q.map((e) => e.seq), [1, 2]);
|
|
112
|
+
});
|
|
113
|
+
run("setPendingSteer is idempotent on inputId (a retry with the caller's own message id adds nothing)", async () => {
|
|
114
|
+
const store = make();
|
|
115
|
+
const cp = createCheckpointFixture();
|
|
116
|
+
await store.put(cp.token, cp);
|
|
117
|
+
assert.equal(await store.setPendingSteer(cp.token, cp.scope, { text: "once", trusted: false, inputId: "m1" }), true);
|
|
118
|
+
assert.equal(await store.setPendingSteer(cp.token, cp.scope, { text: "once", trusted: false, inputId: "m1" }), true);
|
|
119
|
+
assert.equal(readPendingSteerQueue((await store.get(cp.token)).state).length, 1);
|
|
120
|
+
});
|
|
121
|
+
run("the queue bound is fail-loud steering.queue_full and evicts nothing", async () => {
|
|
122
|
+
const store = make();
|
|
123
|
+
const cp = createCheckpointFixture();
|
|
124
|
+
await store.put(cp.token, cp);
|
|
125
|
+
for (let i = 0; i < MAX_PENDING_STEER_ENTRIES; i++) {
|
|
126
|
+
assert.equal(await store.setPendingSteer(cp.token, cp.scope, { text: `s${i}`, trusted: false }), true);
|
|
127
|
+
}
|
|
128
|
+
await assert.rejects(store.setPendingSteer(cp.token, cp.scope, { text: "overflow", trusted: false }), (e) => e.code === "steering.queue_full");
|
|
129
|
+
const q = readPendingSteerQueue((await store.get(cp.token)).state);
|
|
130
|
+
assert.equal(q.length, MAX_PENDING_STEER_ENTRIES);
|
|
131
|
+
assert.deepEqual(q.map((e) => e.text), Array.from({ length: MAX_PENDING_STEER_ENTRIES }, (_, i) => `s${i}`));
|
|
132
|
+
});
|
|
133
|
+
run("a queue entry round-trips every FROZEN field (actor/inputId/priority included)", async () => {
|
|
134
|
+
const store = make();
|
|
135
|
+
const cp = createCheckpointFixture();
|
|
136
|
+
await store.put(cp.token, cp);
|
|
137
|
+
assert.equal(await store.setPendingSteer(cp.token, cp.scope, {
|
|
138
|
+
text: "attributed",
|
|
139
|
+
trusted: true,
|
|
140
|
+
actor: { id: "slack:U1", hostAsserted: true, issuer: "slack-app:A1" },
|
|
141
|
+
inputId: "m-42",
|
|
142
|
+
priority: "now",
|
|
143
|
+
}), true);
|
|
144
|
+
const q = readPendingSteerQueue((await store.get(cp.token)).state);
|
|
145
|
+
assert.deepEqual(q, [
|
|
146
|
+
{ text: "attributed", trusted: true, actor: { id: "slack:U1", hostAsserted: true, issuer: "slack-app:A1" }, inputId: "m-42", priority: "now", seq: 1 },
|
|
147
|
+
]);
|
|
148
|
+
});
|
|
149
|
+
run("an unknown steering field is REFUSED (a producer must not have its new field silently dropped)", async () => {
|
|
150
|
+
const store = make();
|
|
151
|
+
const cp = createCheckpointFixture();
|
|
152
|
+
await store.put(cp.token, cp);
|
|
153
|
+
await assert.rejects(store.setPendingSteer(cp.token, cp.scope, { text: "go", trusted: false, futureField: 1 }), (e) => e.code === "steering.invalid_content");
|
|
154
|
+
assert.deepEqual(readPendingSteerQueue((await store.get(cp.token)).state), []);
|
|
96
155
|
});
|
|
97
156
|
run("expire vs resolve on the same row → exactly one wins; reap expires past-deadline pending in scope", async () => {
|
|
98
157
|
const store = make();
|
|
@@ -41,6 +41,8 @@ export interface SystemInjection<TPayload = unknown> {
|
|
|
41
41
|
}
|
|
42
42
|
export declare function taskNotificationDedupKey(n: Pick<TaskNotificationPayload, "task_id" | "task_type" | "status" | "seq">): string;
|
|
43
43
|
export declare function taskNotificationLaneKey(n: Pick<TaskNotificationPayload, "task_id" | "task_type">): string;
|
|
44
|
+
export declare function attrEscape(value: string): string;
|
|
45
|
+
export declare const EXTERNAL_SOURCE_MAX = 120;
|
|
44
46
|
export declare function renderTaskNotificationXml(n: TaskNotificationPayload): string;
|
|
45
47
|
export declare const MAX_PENDING_EVENTS_PER_TASK = 50;
|
|
46
48
|
export declare const MAX_PENDING_EVENTS_PER_SESSION = 200;
|