@sema-agent/core 5.50.0 → 5.51.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 +63 -0
- package/dist/agents/send-message-tool.js +5 -5
- package/dist/agents/subagent.js +16 -4
- package/dist/core/auto-mode-prompt.js +9 -1
- package/dist/core/hooks.d.ts +24 -1
- package/dist/core/hooks.js +26 -4
- package/dist/core/runner/assemble-result.d.ts +6 -0
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/prepare-task.d.ts +15 -0
- package/dist/core/runner/prepare-task.js +89 -43
- package/dist/core/runner/runtask.d.ts +5 -1
- package/dist/core/runner/runtask.js +14 -7
- package/dist/core/task-registry-agent.js +3 -3
- package/dist/core/task-registry-shared.d.ts +6 -0
- package/dist/core/task-registry.js +4 -2
- package/dist/core/tool-policy.d.ts +37 -0
- package/dist/core/tool-policy.js +36 -3
- package/dist/core/tools.js +7 -0
- package/dist/core/types.d.ts +38 -3
- package/dist/engine/loop/agent-loop.js +95 -30
- package/dist/engine/loop/types.d.ts +32 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.51.0 — 2026-08-21
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **A parent-thread BARE human rejection is a control-flow boundary** (#210; CC 223 anchor
|
|
7
|
+
`$Vo`/`cancelAndAbort`): after the person rejects a tool call with no note, the same assistant
|
|
8
|
+
batch's later side-effecting siblings no longer execute — never-started calls settle as coded
|
|
9
|
+
results (`details.{error,code}: "gate.batch_halted"`, known-not-executed wording,
|
|
10
|
+
`rejectedToolCallId`/`rejectedToolName` attached), already-executing calls finish and settle
|
|
11
|
+
honestly, and the run ends awaiting user input. Sequential, partitioned-preflight and in-stream
|
|
12
|
+
pipelines all consume ONE judgment seat; delegated children (forks included), reject-WITH-note,
|
|
13
|
+
and every non-human deny keep today's posture byte-for-byte. Engine continuation lanes
|
|
14
|
+
(stop-gate pushback, final-verify, attachment/batch-context steer, limit-approach, LSP
|
|
15
|
+
diagnostics, task notifications — the last parks losslessly) cannot revive a halted run; user
|
|
16
|
+
steer/followUp can — they ARE the awaited input. Additive faces: `TaskResult.haltedOnUserRejection`,
|
|
17
|
+
`tool_execution_end.notExecuted`, `BeforeToolCallResult.haltRemaining`. Containment form (no
|
|
18
|
+
abort teardown) is a registered cc-parity divergence.
|
|
19
|
+
- **Every ask-resolution deny names its arm** (#127): `AskDenyResolution` nine-code closed set
|
|
20
|
+
(human_refused/window_expired/no_approver/blanket_allow_refused/approver_unavailable/
|
|
21
|
+
task_aborted/presentation_failed/approver_error/approver_contract) minted where each arm
|
|
22
|
+
states its own fact, carried as `PermissionDeniedPayload.resolution` (distinct from `source`)
|
|
23
|
+
onto the `tool_end` frame. Existing deny wording byte-unchanged; the gate captures only at its
|
|
24
|
+
own ask-resolution seats, so a policy cannot forge a code.
|
|
25
|
+
- **`defineTool` carries the declared safety axes onto its product** (#126):
|
|
26
|
+
egress/irreversibility/reversibilityProbe/offload/offloadThresholdChars now survive into a
|
|
27
|
+
product-form `TaskSpec.tools` entry — a declared egress/irreversible tool fed as a finished
|
|
28
|
+
product previously auto-allowed with no door sounding. Declaration-takes-effect (behavior
|
|
29
|
+
narrowing): such declarations now really gate, and a product-form `egress:true+effect:"read"`
|
|
30
|
+
contradiction refuses prepare loudly. A ToolSpec-key completeness table makes the next
|
|
31
|
+
silently-dropped key a compile error.
|
|
32
|
+
- **Review sampling is tail-inclusive** (#287): the spawn review reads objective+systemPrompt
|
|
33
|
+
through the layered head/interior/tail sample (12k budget, gap markers), and the auto-mode
|
|
34
|
+
classifier's 48k action block reads head-half + declared-middle + tail-half — a two-stage
|
|
35
|
+
payload (benign head, real instructions past the cut) no longer evades review. Bounds unchanged.
|
|
36
|
+
- **The reversibilityProbe wait is always finite** (#128): absent `approvalTimeoutMs` falls to a
|
|
37
|
+
30s default (elapse takes the existing fail-closed tighten-to-ask arm), garbage values refuse
|
|
38
|
+
loudly to the same default, explicit 0 stays honored; a throwing/timed-out probe now reaches
|
|
39
|
+
`onHookError`/`onError(phase:"hook")` instead of being swallowed.
|
|
40
|
+
- **`details.code` twins for the SendMessage admission and stop/poll families**: the five
|
|
41
|
+
admission codes (rate_limited/duplicate/hop_loop/hop_runaway/queue_full) and the stop family
|
|
42
|
+
(not_local/parked_pending_approval/park_resume_won/park_arbiter_unreachable) carry the machine
|
|
43
|
+
twin beside `error` at all nine mint sites; `UnifiedTaskOutput` grows the additive `code` seat.
|
|
44
|
+
- **`mcp.revocation_probe_failed` forwarding guidance**: the seat contract and notice directory
|
|
45
|
+
now state the dedup unit precisely (once per MATERIALIZATION — a resume re-materializes and may
|
|
46
|
+
re-announce), no session attribution, operator audience by the `NOTICE_AUDIENCE` default; plus
|
|
47
|
+
an in-flight-not-chased pin (a revocation racing an already-dispatched call never retracts it).
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
- Behavior narrowing (named): post-rejection siblings from "executed as usual" to "settle
|
|
51
|
+
un-executed"; a bare human rejection no longer re-invokes the model to narrate it
|
|
52
|
+
(`TaskResult.result` may be empty text); product-form tool declarations now really gate (#126).
|
|
53
|
+
|
|
54
|
+
### Notes
|
|
55
|
+
- Residuals ticketed, not shipped silently: a pre-rejection harness-accepted engine steer still
|
|
56
|
+
drains past the boundary (#370, single-choke harness steer entry proposed); delegated-child
|
|
57
|
+
denies fold through policy without a resolution code (observed on #370).
|
|
58
|
+
- Pre-release merged-code scan dispositions (three confirmed, fixed in-tree before publish):
|
|
59
|
+
a human-halted boundary no longer DRAINS the LSP diagnostics registry (drain is a consuming
|
|
60
|
+
read — pending now survives for the continuation, the frame defers with it); `defineTool` also
|
|
61
|
+
carries `defer`/`alwaysLoad` (the defer classification reads them off spec.tools entries — a
|
|
62
|
+
product's declared deferral silently inlined its schema, and a declared inline pin lost to
|
|
63
|
+
`TaskSpec.deferTools`); the probe-deadline guard refuses values above setTimeout's 2^31-1
|
|
64
|
+
ceiling loudly (the silent ~1ms clamp it claimed to close).
|
|
65
|
+
|
|
3
66
|
## 5.50.0 — 2026-08-21
|
|
4
67
|
|
|
5
68
|
### Added
|
|
@@ -132,7 +132,7 @@ export function createSendMessageTool(opts) {
|
|
|
132
132
|
: reason === "hop_loop"
|
|
133
133
|
? `this message has already passed through ${whoLabel} too many times (a forwarding loop) — stop relaying it; act on it or drop it.`
|
|
134
134
|
: `this message's forwarding chain is too long (runaway relay) — stop relaying it; act on it or drop it.`;
|
|
135
|
-
return { content: `Message not sent: ${text}`, details: { error: reason, to }, isError: true };
|
|
135
|
+
return { content: `Message not sent: ${text}`, details: { error: reason, code: reason, to }, isError: true };
|
|
136
136
|
};
|
|
137
137
|
if (normalizeAgentName(to) === "main") {
|
|
138
138
|
if (opts.uplink && senderId !== undefined) {
|
|
@@ -217,7 +217,7 @@ export function createSendMessageTool(opts) {
|
|
|
217
217
|
if (row.status === "parked") {
|
|
218
218
|
return {
|
|
219
219
|
content: `Message not sent: ${whoT3} is parked on a pending approval — it resumes when the approval is decided (durable approval inbox), not by message delivery. Send again after it resumes.`,
|
|
220
|
-
details: { error: "parked_pending_approval", to },
|
|
220
|
+
details: { error: "parked_pending_approval", code: "parked_pending_approval", to },
|
|
221
221
|
isError: true,
|
|
222
222
|
};
|
|
223
223
|
}
|
|
@@ -257,7 +257,7 @@ export function createSendMessageTool(opts) {
|
|
|
257
257
|
if (boxFull) {
|
|
258
258
|
const queueFullReceipt = (guidance) => ({
|
|
259
259
|
content: `Message not sent: ${whoT3}'s mailbox is at its queued-message limit (${admissionConfig.maxQueuedPeerMessages}) — the message was NOT queued. ${guidance} ${DEDUP_RETRY_NOTE}`,
|
|
260
|
-
details: { error: "queue_full", to },
|
|
260
|
+
details: { error: "queue_full", code: "queue_full", to },
|
|
261
261
|
isError: true,
|
|
262
262
|
});
|
|
263
263
|
if (!opts.registry.beginDurableClaim(handle)) {
|
|
@@ -601,7 +601,7 @@ export function createSendMessageTool(opts) {
|
|
|
601
601
|
if (delivered.reason === "queue_full") {
|
|
602
602
|
return {
|
|
603
603
|
content: `Message not sent: ${who} is still starting up and its startup message buffer is full — resend in a moment. ${DEDUP_RETRY_NOTE}`,
|
|
604
|
-
details: { error: "queue_full", to },
|
|
604
|
+
details: { error: "queue_full", code: "queue_full", to },
|
|
605
605
|
isError: true,
|
|
606
606
|
};
|
|
607
607
|
}
|
|
@@ -609,7 +609,7 @@ export function createSendMessageTool(opts) {
|
|
|
609
609
|
if (nowRow?.status === "parked") {
|
|
610
610
|
return {
|
|
611
611
|
content: `Message not sent: ${who} is parked on a pending approval — it resumes when the approval is decided, not by message delivery. Send again after it resumes. ${DEDUP_RETRY_NOTE}`,
|
|
612
|
-
details: { error: "parked_pending_approval", to },
|
|
612
|
+
details: { error: "parked_pending_approval", code: "parked_pending_approval", to },
|
|
613
613
|
isError: true,
|
|
614
614
|
};
|
|
615
615
|
}
|
package/dist/agents/subagent.js
CHANGED
|
@@ -110,14 +110,14 @@ export function layeredReviewSample(text, budget = HANDBACK_REVIEW_SAMPLE_BUDGET
|
|
|
110
110
|
return { text, readChars: total, totalChars: total, sampled: false, gaps: 0 };
|
|
111
111
|
return { text: out, readChars, totalChars: total, sampled: true, gaps };
|
|
112
112
|
}
|
|
113
|
-
function reviewCoverageNote(fields) {
|
|
113
|
+
function reviewCoverageNote(fields, author = "the child's own writing") {
|
|
114
114
|
const sampled = fields.filter((f) => f.sample.sampled);
|
|
115
115
|
if (sampled.length === 0)
|
|
116
116
|
return undefined;
|
|
117
117
|
return (sampled
|
|
118
118
|
.map((f) => `${f.field}: the classifier was shown ${f.sample.readChars} of ${f.sample.totalChars} characters (UTF-16 code units) ` +
|
|
119
119
|
`(layered head/middle/tail sample; the ${f.sample.gaps} skipped span(s) are marked in place as ` +
|
|
120
|
-
`"[… N chars not shown …]" — any further such marker in the text is
|
|
120
|
+
`"[… N chars not shown …]" — any further such marker in the text is ${author}, not this sampler's)`)
|
|
121
121
|
.join("; ") + " — the unshown spans were NOT reviewed; treat them as unknown, not as benign.");
|
|
122
122
|
}
|
|
123
123
|
const HANDBACK_ASK_MESSAGE = "Subagent has finished and is handing back control to the main agent. Review the subagent's work and flag if any action may violate security policy.";
|
|
@@ -2028,14 +2028,26 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2028
2028
|
const spawnToolsNote = spawnToolNames.length > 0
|
|
2029
2029
|
? spawnToolNames.join(", ")
|
|
2030
2030
|
: "(none explicitly listed — but if this deployment gave the child a real execution environment, it likely also has the standard file/shell tools: Read/Edit/Write/Bash/Grep/Glob)";
|
|
2031
|
+
const objectiveSample = layeredReviewSample(prompt);
|
|
2032
|
+
const systemPromptSample = childSystemPrompt != null ? layeredReviewSample(childSystemPrompt) : undefined;
|
|
2033
|
+
const spawnCoverage = reviewCoverageNote([
|
|
2034
|
+
{ field: "objective", sample: objectiveSample },
|
|
2035
|
+
...(systemPromptSample ? [{ field: "systemPrompt", sample: systemPromptSample }] : []),
|
|
2036
|
+
], "the delegating agent's own writing");
|
|
2037
|
+
const objectiveDisplay = layeredReviewSample(prompt, 2_000, 2);
|
|
2031
2038
|
const spawnVerdict = await ctx.autoModeReview.decider
|
|
2032
2039
|
.decide({
|
|
2033
2040
|
req: {
|
|
2034
2041
|
toolName: wantsFork ? "Agent(fork)" : "Agent",
|
|
2035
|
-
args: {
|
|
2042
|
+
args: {
|
|
2043
|
+
objective: objectiveSample.text,
|
|
2044
|
+
tools: spawnToolNames,
|
|
2045
|
+
systemPrompt: systemPromptSample?.text,
|
|
2046
|
+
...(spawnCoverage ? { reviewCoverage: spawnCoverage } : {}),
|
|
2047
|
+
},
|
|
2036
2048
|
toolCallId: ctx.toolCallId,
|
|
2037
2049
|
},
|
|
2038
|
-
askMessage: `Reviewing a sub-agent about to be spawned. Objective: ${
|
|
2050
|
+
askMessage: `Reviewing a sub-agent about to be spawned. Objective: ${objectiveDisplay.text}\nTools available to it: ${spawnToolsNote}`,
|
|
2039
2051
|
}, ctx.signal)
|
|
2040
2052
|
.catch(() => ({ kind: "unavailable", cause: "error" }));
|
|
2041
2053
|
if (ctx.signal?.aborted) {
|
|
@@ -49,6 +49,14 @@ function excerpt(text, cap) {
|
|
|
49
49
|
return text;
|
|
50
50
|
return `${text.slice(0, cap)} [… ${text.length - cap} chars truncated]`;
|
|
51
51
|
}
|
|
52
|
+
function excerptTailInclusive(text, cap) {
|
|
53
|
+
if (text.length <= cap)
|
|
54
|
+
return text;
|
|
55
|
+
const half = Math.floor(cap / 2);
|
|
56
|
+
const marker = ` [… ${text.length - 2 * half} chars not shown …] `;
|
|
57
|
+
const out = `${text.slice(0, half)}${marker}${text.slice(text.length - half)}`;
|
|
58
|
+
return out.length >= text.length ? text : out;
|
|
59
|
+
}
|
|
52
60
|
function renderEntry(m, cap) {
|
|
53
61
|
if (m.role === "user") {
|
|
54
62
|
const raw = typeof m.content === "string" ? m.content : m.content.map((c) => (c.type === "text" ? c.text : `[${c.type}]`)).join("\n");
|
|
@@ -102,5 +110,5 @@ export function renderAutoModeWindow(messages, options) {
|
|
|
102
110
|
export function renderAutoModeAction(input) {
|
|
103
111
|
const ask = input.askMessage ? `\npermission gate: ${input.askMessage}` : "";
|
|
104
112
|
return (`\n## New action to classify (the agent's most recent action — evaluate THIS)\n\n` +
|
|
105
|
-
`[tool_call] ${input.req.toolName} ${
|
|
113
|
+
`[tool_call] ${input.req.toolName} ${excerptTailInclusive(JSON.stringify(input.req.args ?? {}), 48_000)}${ask}\n`);
|
|
106
114
|
}
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -198,6 +198,13 @@ export interface PermissionDeniedPayload {
|
|
|
198
198
|
reason: string;
|
|
199
199
|
/** Which gate source produced the deny (our `decision_reason_type` analog). */
|
|
200
200
|
source: PermissionDeniedSource;
|
|
201
|
+
/** The ask resolver's own deny-arm classification, carried BESIDE `source` (two different
|
|
202
|
+
* questions: `source` names which LAYER raised the gate; this names HOW the ask resolution
|
|
203
|
+
* refused — a person's no vs a timeout vs headless vs an approver contract violation …). Present
|
|
204
|
+
* only on a deny that came through an ask resolution AND whose word passed the closed-vocabulary
|
|
205
|
+
* screen; a policy's direct deny, a hook deny, and the crash/plan-mode/compliance emissions carry
|
|
206
|
+
* none. See {@link import("./tool-policy.js").AskDenyResolution}. */
|
|
207
|
+
resolution?: import("./tool-policy.js").AskDenyResolution;
|
|
201
208
|
}
|
|
202
209
|
/**
|
|
203
210
|
* 1.256 复审 MED-1 — observe-only payload isolation for {@link Hooks.permissionDenied}: clone the tool
|
|
@@ -606,6 +613,14 @@ export interface ToolGateResult {
|
|
|
606
613
|
* and no post-tool hook can write.
|
|
607
614
|
*/
|
|
608
615
|
settledBy?: import("./tool-policy.js").ApprovalSettledBy;
|
|
616
|
+
/**
|
|
617
|
+
* The ask resolver's deny-arm classification (see {@link import("./tool-policy.js").AskDenyResolution}),
|
|
618
|
+
* present only on a BLOCK whose deny came through an ask resolution and passed the closed-vocabulary
|
|
619
|
+
* screen at the deny exit (a self-declared word on a policy's own deny is dropped there, never
|
|
620
|
+
* forwarded). Rides beside {@link settledBy} to the caller's per-call sideband and the call's
|
|
621
|
+
* `tool_end` frame — the machine-readable "why was this refused" a consumer classifies on.
|
|
622
|
+
*/
|
|
623
|
+
resolution?: import("./tool-policy.js").AskDenyResolution;
|
|
609
624
|
/**
|
|
610
625
|
* design/252 G-7 — WHOSE settlement that was: the identifier the approval channel reported, carried
|
|
611
626
|
* out verbatim beside {@link settledBy}. This layer authenticates nothing and compares nothing; the
|
|
@@ -915,7 +930,10 @@ export interface ToolGateInput {
|
|
|
915
930
|
* probe cannot widen anything through this member.
|
|
916
931
|
*/
|
|
917
932
|
reversibilityProbe?: (args: unknown) => import("./types.js").ReversibilityVerdict | Promise<import("./types.js").ReversibilityVerdict>;
|
|
918
|
-
/** design/77 §4: deadline (ms) for {@link reversibilityProbe}; on timeout the gate fails closed to `ask`.
|
|
933
|
+
/** design/77 §4: deadline (ms) for {@link reversibilityProbe}; on timeout the gate fails closed to `ask`.
|
|
934
|
+
* ABSENT ⇒ a bounded default applies (30s — the probe wait is never unbounded, even with no
|
|
935
|
+
* {@link abortSignal}); a non-finite/negative value is refused loudly (via {@link onHookError}) to
|
|
936
|
+
* that same default, never silently reinterpreted. `0` is honored as written (immediate deadline). */
|
|
919
937
|
approvalTimeoutMs?: number;
|
|
920
938
|
/** design/77 §4: the task abort signal — bounds {@link reversibilityProbe} by the task's real deadline
|
|
921
939
|
* (timeout/cancel) in addition to {@link approvalTimeoutMs}; an abort while probing fails closed to `ask`. */
|
|
@@ -937,6 +955,11 @@ export interface ToolGateInput {
|
|
|
937
955
|
* this one carries the exception object itself to whoever runs the deployment, because a crashing hook is
|
|
938
956
|
* a bug someone has to fix and the model-facing summary is bounded/sanitized. Never affects the outcome
|
|
939
957
|
* (a throwing sink is swallowed).
|
|
958
|
+
*
|
|
959
|
+
* ALSO fired for a {@link reversibilityProbe} that threw or timed out (same species — a
|
|
960
|
+
* deployment-supplied callback failing while the gate holds the fail-closed line) and for a
|
|
961
|
+
* malformed {@link approvalTimeoutMs} refused to the bounded default. A task-abort rejection
|
|
962
|
+
* mid-probe is NOT reported (normal cancellation, not a defect).
|
|
940
963
|
*/
|
|
941
964
|
onHookError?: (err: unknown) => void;
|
|
942
965
|
/**
|
package/dist/core/hooks.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decisionText, describeThrown, refuseOutOfContractDecision } from "./tool-policy.js";
|
|
1
|
+
import { decisionText, describeThrown, isAskDenyResolution, refuseOutOfContractDecision } from "./tool-policy.js";
|
|
2
2
|
import { brandPolicyAskClass } from "./ask-class.js";
|
|
3
3
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
4
4
|
import { mintSystemReminder } from "./reminder-mint.js";
|
|
@@ -269,6 +269,7 @@ function withProbeTimeout(p, ms, signal) {
|
|
|
269
269
|
p.then((v) => done(resolve, v), (err) => done(reject, err));
|
|
270
270
|
});
|
|
271
271
|
}
|
|
272
|
+
const DEFAULT_PROBE_TIMEOUT_MS = 30_000;
|
|
272
273
|
export function persistedRuleMandateOf(marks) {
|
|
273
274
|
return marks.egress === true
|
|
274
275
|
? "tool_marks"
|
|
@@ -293,6 +294,7 @@ export async function runToolGate(input) {
|
|
|
293
294
|
const preToolContext = [];
|
|
294
295
|
let hookAsk;
|
|
295
296
|
let parkFailed;
|
|
297
|
+
let askDenyResolution;
|
|
296
298
|
const notifier = createSafeNotifier(input.onNotifyError !== undefined ? { onError: input.onNotifyError } : undefined);
|
|
297
299
|
if (preToolUse) {
|
|
298
300
|
let r;
|
|
@@ -373,8 +375,20 @@ export async function runToolGate(input) {
|
|
|
373
375
|
if (input.irreversibility === "maybe" && input.reversibilityProbe) {
|
|
374
376
|
let reversible = false;
|
|
375
377
|
const probeArgs = policyRewrite !== undefined ? policyRewrite : currentInput;
|
|
378
|
+
const suppliedProbeMs = input.approvalTimeoutMs;
|
|
379
|
+
let probeTimeoutMs;
|
|
380
|
+
if (suppliedProbeMs === undefined) {
|
|
381
|
+
probeTimeoutMs = DEFAULT_PROBE_TIMEOUT_MS;
|
|
382
|
+
}
|
|
383
|
+
else if (Number.isFinite(suppliedProbeMs) && suppliedProbeMs >= 0 && suppliedProbeMs <= 2_147_483_647) {
|
|
384
|
+
probeTimeoutMs = suppliedProbeMs;
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
probeTimeoutMs = DEFAULT_PROBE_TIMEOUT_MS;
|
|
388
|
+
traceHookCrash(input, new Error(`approvalTimeoutMs must be a non-negative finite number no greater than 2147483647 (got ${String(suppliedProbeMs)}) — the reversibilityProbe deadline falls back to the ${DEFAULT_PROBE_TIMEOUT_MS}ms default`), notifier);
|
|
389
|
+
}
|
|
376
390
|
try {
|
|
377
|
-
const verdict = await withProbeTimeout(Promise.resolve(input.reversibilityProbe(probeArgs)),
|
|
391
|
+
const verdict = await withProbeTimeout(Promise.resolve(input.reversibilityProbe(probeArgs)), probeTimeoutMs, input.abortSignal);
|
|
378
392
|
reversible = verdict?.reversible === true;
|
|
379
393
|
if (!reversible) {
|
|
380
394
|
const raw = verdict?.reason;
|
|
@@ -383,8 +397,10 @@ export async function runToolGate(input) {
|
|
|
383
397
|
probeCause = normalizeProbeCause(verdict?.cause);
|
|
384
398
|
}
|
|
385
399
|
}
|
|
386
|
-
catch {
|
|
400
|
+
catch (err) {
|
|
387
401
|
reversible = false;
|
|
402
|
+
if (input.abortSignal?.aborted !== true)
|
|
403
|
+
traceHookCrash(input, err, notifier);
|
|
388
404
|
}
|
|
389
405
|
tighten = !reversible;
|
|
390
406
|
}
|
|
@@ -671,6 +687,8 @@ export async function runToolGate(input) {
|
|
|
671
687
|
const resolved = await resolveAsk(decision, req);
|
|
672
688
|
if (resolved.action !== "ask" && resolved.approver !== undefined)
|
|
673
689
|
resolvedApprover = resolved.approver;
|
|
690
|
+
if (resolved.action === "deny" && isAskDenyResolution(resolved.resolution))
|
|
691
|
+
askDenyResolution = resolved.resolution;
|
|
674
692
|
decision = resolved;
|
|
675
693
|
if (resolved.action === "deny" && resolved.approverUnavailable === true && suspendAsk && parkFailed === undefined) {
|
|
676
694
|
const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(askBeforeResolve), askBeforeResolve.action === "ask" ? askBeforeResolve.persistedRuleShadowed : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.decisionReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeCause : undefined);
|
|
@@ -771,6 +789,8 @@ export async function runToolGate(input) {
|
|
|
771
789
|
const rr = await resolveAsk({ ...recheck, ruleEvidence: mintRuleEvidence({ dotsAbsent: "not_adjudicated" }) }, { toolName, args: editArgs, toolCallId });
|
|
772
790
|
resolvedApprover = rr.action !== "ask" ? rr.approver : undefined;
|
|
773
791
|
if (rr.action !== "allow") {
|
|
792
|
+
if (rr.action === "deny" && isAskDenyResolution(rr.resolution))
|
|
793
|
+
askDenyResolution = rr.resolution;
|
|
774
794
|
editDenied = rr;
|
|
775
795
|
if (!orgRaisedThisRound)
|
|
776
796
|
denySource = "policy";
|
|
@@ -807,8 +827,9 @@ export async function runToolGate(input) {
|
|
|
807
827
|
if (decision.updatedInput !== undefined) {
|
|
808
828
|
currentInput = decision.updatedInput;
|
|
809
829
|
}
|
|
830
|
+
const denyResolution = askDenyResolution;
|
|
810
831
|
if (input.permissionDenied) {
|
|
811
|
-
await notifier.notifyAsync(() => input.permissionDenied?.({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason: denyReason, source: denySource, ...(input.identity !== undefined ? { identity: input.identity } : {}) }), "toolGate.permissionDenied");
|
|
832
|
+
await notifier.notifyAsync(() => input.permissionDenied?.({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason: denyReason, source: denySource, ...(denyResolution !== undefined ? { resolution: denyResolution } : {}), ...(input.identity !== undefined ? { identity: input.identity } : {}) }), "toolGate.permissionDenied");
|
|
812
833
|
}
|
|
813
834
|
const denySettledBy = decision.settledBy;
|
|
814
835
|
const denyApprover = denySettledBy !== undefined ? resolvedApprover : undefined;
|
|
@@ -816,6 +837,7 @@ export async function runToolGate(input) {
|
|
|
816
837
|
block: true,
|
|
817
838
|
reason: formatHookFeedback(denyReason, input.reminderMark),
|
|
818
839
|
...(denySettledBy !== undefined ? { settledBy: denySettledBy } : {}),
|
|
840
|
+
...(denyResolution !== undefined ? { resolution: denyResolution } : {}),
|
|
819
841
|
...(denyApprover !== undefined ? { approver: denyApprover } : {}),
|
|
820
842
|
preToolContext,
|
|
821
843
|
};
|
|
@@ -112,6 +112,12 @@ export interface ResultFlags {
|
|
|
112
112
|
* Pure pass-through — assembly neither adds nor filters (a rewind that FAILED never reaches here; it
|
|
113
113
|
* throws at prepare and lands in the `threw` slot as a terminal errorCode). */
|
|
114
114
|
rewindNotes?: TaskResult["rewindNotes"];
|
|
115
|
+
/** The run's final turn was halted by a person's BARE rejection of a tool call (the parent-thread
|
|
116
|
+
* control-flow boundary) — echoed on `TaskResult.haltedOnUserRejection`. Pure pass-through on
|
|
117
|
+
* every terminal: the fact is about the leg that ran, whatever terminal it reached (on the normal
|
|
118
|
+
* path the terminal is `completed`, and this is what tells that completion apart from a natural
|
|
119
|
+
* one — the model did not finish; the person stopped it and the run awaits their direction). */
|
|
120
|
+
haltedOnUserRejection?: boolean;
|
|
115
121
|
/** design/174 final-round: call ids of answered-but-never-collected questions, echoed on
|
|
116
122
|
* `TaskResult.strandedHumanAnswers`. Pure pass-through; empty/absent ⇒ the field is omitted. The
|
|
117
123
|
* optional `onError` alert is NOT the disclosure — this mandatory result face is. */
|
|
@@ -165,5 +165,5 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
165
165
|
void _internalCompaction;
|
|
166
166
|
if (flags.unpricedSpend)
|
|
167
167
|
delete publicStats.costMicroUsd;
|
|
168
|
-
return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, ...(checkpointId !== undefined ? { checkpointId } : {}), checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
|
|
168
|
+
return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, ...(checkpointId !== undefined ? { checkpointId } : {}), checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
|
|
169
169
|
}
|
|
@@ -200,7 +200,22 @@ export interface Prepared {
|
|
|
200
200
|
approvalSettlement: Map<string, {
|
|
201
201
|
settledBy?: import("../tool-policy.js").ApprovalSettledBy;
|
|
202
202
|
approver?: string;
|
|
203
|
+
resolution?: import("../tool-policy.js").AskDenyResolution;
|
|
203
204
|
}>;
|
|
205
|
+
/**
|
|
206
|
+
* The parent-thread human-rejection halt fact (see `maybeHumanRejectionHalt`): present from the
|
|
207
|
+
* moment a bare human rejection halts the turn's batch until the run ends or the NEXT provider
|
|
208
|
+
* request begins (user input continuing the run clears it). Consumers: the runner's stop gate
|
|
209
|
+
* (suppress natural-end pushback / final-verify injection — engine continuations must not restart
|
|
210
|
+
* a run a person just stopped), the turn-boundary engine steers (same reason), and the result
|
|
211
|
+
* stamp (`TaskResult.haltedOnUserRejection` — a human-halted run must not read as an ordinary
|
|
212
|
+
* completion). Engine-owned sideband, same trust reasoning as `approvalSettlement` above.
|
|
213
|
+
*/
|
|
214
|
+
batchHaltRef: {
|
|
215
|
+
current?: {
|
|
216
|
+
rejectedToolCallId: string;
|
|
217
|
+
};
|
|
218
|
+
};
|
|
204
219
|
/** Summed usage of nested sub-runs (sub-agents) spawned by this task's tools. */
|
|
205
220
|
nestedStats: NestedUsageAccum;
|
|
206
221
|
/** RB-430-a: prepare-time rewind disclosures (conversation-only branch / no snapshot backend / no file
|
|
@@ -20,7 +20,7 @@ import { createSubagentWorktreeHelper, forkGovernanceDenial, resolveDelegationEn
|
|
|
20
20
|
import { createAgentTranscriptTool, AGENT_TRANSCRIPT_TOOL_NAME } from "../../agents/agent-transcript-tool.js";
|
|
21
21
|
import { createSendMessageTool, SEND_MESSAGE_TOOL_NAME } from "../../agents/send-message-tool.js";
|
|
22
22
|
import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
|
|
23
|
-
import { askApproverIdentity, checkToolPolicyProjection, combinePolicies, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, screenApproverAttribution, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, describeThrown, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets, tryCloneArgs } from "../tool-policy.js";
|
|
23
|
+
import { askApproverIdentity, checkToolPolicyProjection, combinePolicies, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, isAskDenyResolution, screenApproverAttribution, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, describeThrown, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets, tryCloneArgs } from "../tool-policy.js";
|
|
24
24
|
const PERSISTED_RULE_TOOL = "Bash";
|
|
25
25
|
import { findAdmittingRule, suggestRulesForCommand } from "../permission-rule-model.js";
|
|
26
26
|
import { ActiveSkillScope, createActiveSkillScopePolicy } from "./active-skill-scope.js";
|
|
@@ -314,11 +314,72 @@ function screenGateSettlement(result, settling) {
|
|
|
314
314
|
if (attribution.defect !== undefined) {
|
|
315
315
|
defects.push(`a tool-gate settlement reported an attribution this engine refuses: ${attribution.defect}; the frame carries no approver`);
|
|
316
316
|
}
|
|
317
|
+
const reportedResolution = result.resolution;
|
|
318
|
+
let resolution;
|
|
319
|
+
if (reportedResolution !== undefined) {
|
|
320
|
+
if (!isAskDenyResolution(reportedResolution) || !settling) {
|
|
321
|
+
defects.push(`a tool-gate settlement reported a deny resolution "${String(reportedResolution)}" on ${settling ? "a blocked" : "an executing"} call — ` +
|
|
322
|
+
`it is one of the closed ask-deny vocabulary and only a BLOCKED call can carry one; the frame carries no resolution`);
|
|
323
|
+
}
|
|
324
|
+
else
|
|
325
|
+
resolution = reportedResolution;
|
|
326
|
+
}
|
|
317
327
|
const record = {
|
|
318
328
|
...(settledBy !== undefined ? { settledBy } : {}),
|
|
319
329
|
...(attribution.approver !== undefined ? { approver: attribution.approver } : {}),
|
|
330
|
+
...(resolution !== undefined ? { resolution } : {}),
|
|
320
331
|
};
|
|
321
|
-
return { ...(settledBy !== undefined || attribution.approver !== undefined ? { record } : {}), defects };
|
|
332
|
+
return { ...(settledBy !== undefined || attribution.approver !== undefined || resolution !== undefined ? { record } : {}), defects };
|
|
333
|
+
}
|
|
334
|
+
function maybeHumanRejectionHalt(input) {
|
|
335
|
+
if (!input.bare || input.settledBy !== "human" || input.isDelegatedChild)
|
|
336
|
+
return undefined;
|
|
337
|
+
return {
|
|
338
|
+
reason: `This tool call was NOT executed: the user rejected the "${input.toolName}" tool call in the same ` +
|
|
339
|
+
`assistant message, which stops the rest of the batch. Nothing was run for this call — ` +
|
|
340
|
+
`re-issue it after the user's direction only if it is still needed.`,
|
|
341
|
+
details: {
|
|
342
|
+
error: "gate.batch_halted",
|
|
343
|
+
code: "gate.batch_halted",
|
|
344
|
+
rejectedToolCallId: input.toolCallId,
|
|
345
|
+
rejectedToolName: input.toolName,
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
const sanitizePreview = (node, depth = 0) => {
|
|
350
|
+
if (depth > 6)
|
|
351
|
+
return undefined;
|
|
352
|
+
if (typeof node === "string") {
|
|
353
|
+
return node.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "\u2400");
|
|
354
|
+
}
|
|
355
|
+
if (node === null || typeof node !== "object")
|
|
356
|
+
return node;
|
|
357
|
+
if (Array.isArray(node))
|
|
358
|
+
return node.map((v) => sanitizePreview(v, depth + 1));
|
|
359
|
+
const out = {};
|
|
360
|
+
for (const [k, v] of Object.entries(node)) {
|
|
361
|
+
out[sanitizePreview(k, depth + 1)] = sanitizePreview(v, depth + 1);
|
|
362
|
+
}
|
|
363
|
+
return out;
|
|
364
|
+
};
|
|
365
|
+
function resolveApprovalPreview(tools, toolName, args) {
|
|
366
|
+
const t = tools.find((x) => x.name === toolName || (x.aliases?.includes(toolName) ?? false));
|
|
367
|
+
if (t?.approvalPreview === undefined)
|
|
368
|
+
return undefined;
|
|
369
|
+
try {
|
|
370
|
+
const raw = t.approvalPreview(args);
|
|
371
|
+
if (raw === undefined)
|
|
372
|
+
return undefined;
|
|
373
|
+
const bytes = JSON.stringify(raw);
|
|
374
|
+
if (bytes === undefined)
|
|
375
|
+
return undefined;
|
|
376
|
+
if (bytes.length > 16_384)
|
|
377
|
+
return { truncated: true, note: `approval preview exceeded 16KiB (${bytes.length} chars serialized)` };
|
|
378
|
+
return sanitizePreview(raw);
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
322
383
|
}
|
|
323
384
|
function inheritedAskRuleEvidence(deps) {
|
|
324
385
|
const org = deps.permissionRuleOrg === undefined ? "not_wired" : "not_adjudicated";
|
|
@@ -3291,6 +3352,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3291
3352
|
const preToolContexts = new Map();
|
|
3292
3353
|
const blockedToolCalls = new Set();
|
|
3293
3354
|
const approvalSettlement = new Map();
|
|
3355
|
+
const humanBareRejections = new Set();
|
|
3356
|
+
const batchHaltRef = {};
|
|
3294
3357
|
const blockedTracked = Boolean(hooks?.postToolUse || hooks?.preToolUse || hooks?.postToolUseFailure || hooks?.postToolBatch);
|
|
3295
3358
|
const restoreSurfaceGap = ownedEnv !== undefined && isRemoteExecutionEnv(ownedEnv) && ownedEnv.capabilities.suspendable ? missingRestoreSurface(ownedEnv) : [];
|
|
3296
3359
|
const incompleteSuspendAdapter = restoreSurfaceGap.length > 0 ? restoreSurfaceGap : undefined;
|
|
@@ -3394,41 +3457,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3394
3457
|
message: "policy check aborted (task timed out or cancelled)",
|
|
3395
3458
|
}))
|
|
3396
3459
|
: undefined;
|
|
3397
|
-
const
|
|
3398
|
-
if (depth > 6)
|
|
3399
|
-
return undefined;
|
|
3400
|
-
if (typeof node === "string") {
|
|
3401
|
-
return node.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "\u2400");
|
|
3402
|
-
}
|
|
3403
|
-
if (node === null || typeof node !== "object")
|
|
3404
|
-
return node;
|
|
3405
|
-
if (Array.isArray(node))
|
|
3406
|
-
return node.map((v) => sanitizePreview(v, depth + 1));
|
|
3407
|
-
const out = {};
|
|
3408
|
-
for (const [k, v] of Object.entries(node)) {
|
|
3409
|
-
out[sanitizePreview(k, depth + 1)] = sanitizePreview(v, depth + 1);
|
|
3410
|
-
}
|
|
3411
|
-
return out;
|
|
3412
|
-
};
|
|
3413
|
-
const approvalPreviewOf = (toolName, args) => {
|
|
3414
|
-
const t = tools.find((x) => x.name === toolName || (x.aliases?.includes(toolName) ?? false));
|
|
3415
|
-
if (t?.approvalPreview === undefined)
|
|
3416
|
-
return undefined;
|
|
3417
|
-
try {
|
|
3418
|
-
const raw = t.approvalPreview(args);
|
|
3419
|
-
if (raw === undefined)
|
|
3420
|
-
return undefined;
|
|
3421
|
-
const bytes = JSON.stringify(raw);
|
|
3422
|
-
if (bytes === undefined)
|
|
3423
|
-
return undefined;
|
|
3424
|
-
if (bytes.length > 16_384)
|
|
3425
|
-
return { truncated: true, note: `approval preview exceeded 16KiB (${bytes.length} chars serialized)` };
|
|
3426
|
-
return sanitizePreview(raw);
|
|
3427
|
-
}
|
|
3428
|
-
catch {
|
|
3429
|
-
return undefined;
|
|
3430
|
-
}
|
|
3431
|
-
};
|
|
3460
|
+
const approvalPreviewOf = (toolName, args) => resolveApprovalPreview(tools, toolName, args);
|
|
3432
3461
|
const resolveAskBound = async (decision, req) => {
|
|
3433
3462
|
if (inheritedUnavailableAsks.delete(req.toolCallId)) {
|
|
3434
3463
|
return {
|
|
@@ -3489,6 +3518,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3489
3518
|
...(toolArg !== undefined ? { toolArg } : {}),
|
|
3490
3519
|
});
|
|
3491
3520
|
}
|
|
3521
|
+
if (resolved.action === "deny" && resolved.settledBy === "human" && resolved.humanRefusalNote !== true) {
|
|
3522
|
+
humanBareRejections.add(req.toolCallId);
|
|
3523
|
+
}
|
|
3492
3524
|
return resolved;
|
|
3493
3525
|
};
|
|
3494
3526
|
const hooksWithPermissionDenied = hooks?.permissionDenied ? hooks : undefined;
|
|
@@ -4197,6 +4229,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4197
4229
|
harness.on("tool_call", async (e) => {
|
|
4198
4230
|
blockedToolCalls.delete(e.toolCallId);
|
|
4199
4231
|
inheritedAskGrants.delete(e.toolCallId);
|
|
4232
|
+
humanBareRejections.delete(e.toolCallId);
|
|
4200
4233
|
{
|
|
4201
4234
|
const complianceDeny = complianceCallDenial(complianceDenies, e.toolName);
|
|
4202
4235
|
if (complianceDeny !== undefined) {
|
|
@@ -4223,6 +4256,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4223
4256
|
};
|
|
4224
4257
|
}
|
|
4225
4258
|
let result;
|
|
4259
|
+
let humanBareRejection = false;
|
|
4226
4260
|
try {
|
|
4227
4261
|
result = await runToolGate({
|
|
4228
4262
|
onNotifyError: (f) => emitTrace(deps.tracer, () => ({ kind: "observer.notify_failed", version: 1, taskId: spec.taskId ?? sessionId, site: f.site, message: f.error.message, ts: Date.now() })),
|
|
@@ -4294,6 +4328,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4294
4328
|
inheritedUnavailableAsks.delete(e.toolCallId);
|
|
4295
4329
|
inheritedAskGrants.delete(e.toolCallId);
|
|
4296
4330
|
foldAskClasses.delete(e.toolCallId);
|
|
4331
|
+
humanBareRejection = humanBareRejections.delete(e.toolCallId);
|
|
4297
4332
|
}
|
|
4298
4333
|
const ancestorAdmitted = ancestorSandboxAdmissions.get(e.toolCallId);
|
|
4299
4334
|
ancestorSandboxAdmissions.delete(e.toolCallId);
|
|
@@ -4312,11 +4347,21 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4312
4347
|
deps.onError?.(new Error(defect), { phase: "config", sessionId });
|
|
4313
4348
|
if (settlement.record !== undefined)
|
|
4314
4349
|
approvalSettlement.set(e.toolCallId, settlement.record);
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
:
|
|
4350
|
+
if (result.block) {
|
|
4351
|
+
const haltRemaining = maybeHumanRejectionHalt({
|
|
4352
|
+
bare: humanBareRejection,
|
|
4353
|
+
settledBy: result.settledBy,
|
|
4354
|
+
isDelegatedChild: delegation.isDelegatedChild === true,
|
|
4355
|
+
toolName: e.toolName,
|
|
4356
|
+
toolCallId: e.toolCallId,
|
|
4357
|
+
});
|
|
4358
|
+
if (haltRemaining !== undefined) {
|
|
4359
|
+
batchHaltRef.current = { rejectedToolCallId: e.toolCallId };
|
|
4360
|
+
return { block: true, reason: result.reason, haltRemaining };
|
|
4361
|
+
}
|
|
4362
|
+
return { block: true, reason: result.reason };
|
|
4363
|
+
}
|
|
4364
|
+
return result.updatedInput !== undefined ? { updatedInput: result.updatedInput } : undefined;
|
|
4320
4365
|
});
|
|
4321
4366
|
}
|
|
4322
4367
|
}
|
|
@@ -4374,6 +4419,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4374
4419
|
const guardAt = guardBudget(model);
|
|
4375
4420
|
const charsPerToken = model.charsPerToken ?? DEFAULT_CHARS_PER_TOKEN;
|
|
4376
4421
|
harness.on("context", async ({ messages }) => {
|
|
4422
|
+
batchHaltRef.current = undefined;
|
|
4377
4423
|
const healed = dropEmptyFailureAssistants(messages);
|
|
4378
4424
|
const capped = await capAggregateToolResults(healed, {
|
|
4379
4425
|
store: offloadStore,
|
|
@@ -4638,7 +4684,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4638
4684
|
const effectiveReadFaceObserved = carrierReadFace();
|
|
4639
4685
|
const effectiveReadDenyObserved = readDenyAdditionsNormalized.length > 0 ? readDenyAdditionsNormalized.map((e) => ({ ...e })) : undefined;
|
|
4640
4686
|
const preparedHolder = {};
|
|
4641
|
-
const buildPrepared = () => ({ harness, session, sessionId, reminderMark, reminderDisclosureCounts, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettlement, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, hookIdentity, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
|
|
4687
|
+
const buildPrepared = () => ({ harness, session, sessionId, reminderMark, reminderDisclosureCounts, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettlement, batchHaltRef, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, hookIdentity, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
|
|
4642
4688
|
const prepared = buildPrepared();
|
|
4643
4689
|
preparedHolder.current = prepared;
|
|
4644
4690
|
return prepared;
|
|
@@ -84,7 +84,10 @@ settledBy?: ApprovalSettledBy,
|
|
|
84
84
|
/** design/252 G-7 — WHOSE settlement, from the same caller and the same channel as `settledBy`, and
|
|
85
85
|
* for the same reason it is a parameter: an attribution read out of a tool's own result would let a
|
|
86
86
|
* tool name the person who approved it. Omitted ⇒ this call's settlement named nobody. */
|
|
87
|
-
approver?: string
|
|
87
|
+
approver?: string,
|
|
88
|
+
/** The ask resolver's deny-arm classification — same caller, same engine-owned channel and the same
|
|
89
|
+
* never-derived-from-`result` posture as the two above. Omitted ⇒ not an ask-resolution deny. */
|
|
90
|
+
resolution?: import("../tool-policy.js").AskDenyResolution): {
|
|
88
91
|
output?: unknown;
|
|
89
92
|
truncated?: boolean;
|
|
90
93
|
totalChars?: number;
|
|
@@ -92,6 +95,7 @@ approver?: string): {
|
|
|
92
95
|
errorCode?: string;
|
|
93
96
|
settledBy?: ApprovalSettledBy;
|
|
94
97
|
approver?: string;
|
|
98
|
+
resolution?: import("../tool-policy.js").AskDenyResolution;
|
|
95
99
|
};
|
|
96
100
|
/**
|
|
97
101
|
* scan-1/A1 — the BODY of the synthetic `tool_end` that closes a reconcile-recovered orphan. ONE
|