@sema-agent/core 5.33.0 → 5.35.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 +98 -0
- package/dist/core/checkpoint-store.d.ts +33 -4
- package/dist/core/hooks.d.ts +98 -3
- package/dist/core/hooks.js +146 -8
- package/dist/core/park-selfcheck.d.ts +156 -0
- package/dist/core/park-selfcheck.js +251 -0
- package/dist/core/push-queue.d.ts +4 -1
- package/dist/core/push-queue.js +2 -1
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +6 -0
- package/dist/core/runner/prepare-acquire-reconcile.js +2 -1
- package/dist/core/runner/prepare-task.d.ts +25 -4
- package/dist/core/runner/prepare-task.js +60 -19
- package/dist/core/runner/runtask.d.ts +6 -1
- package/dist/core/runner/runtask.js +77 -19
- package/dist/core/safe-notify.d.ts +9 -0
- package/dist/core/safe-notify.js +9 -0
- package/dist/core/tool-errors.d.ts +2 -2
- package/dist/core/tool-policy.d.ts +125 -0
- package/dist/core/tool-policy.js +35 -2
- package/dist/core/types.d.ts +71 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/orchestration/workflow.d.ts +1 -1
- package/package.json +2 -1
- package/test/export-surface.snapshot.json +1568 -0
|
@@ -20,7 +20,7 @@ import { createSubagentWorktreeHelper, forkGovernanceDenial } from "../../agents
|
|
|
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, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, describeThrown, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets, tryCloneArgs } from "../tool-policy.js";
|
|
23
|
+
import { askApproverIdentity, checkToolPolicyProjection, combinePolicies, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, 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";
|
|
@@ -88,6 +88,7 @@ import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
|
|
|
88
88
|
import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, encodeAtFidelity, remainingBudgetMicroUsd, resolveCheckpointStore, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
|
|
89
89
|
import { boundInputHashOf } from "../canonical-json.js";
|
|
90
90
|
import { countElicitOptIns, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam } from "../wiring-manifest.js";
|
|
91
|
+
import { durableParkGapFor } from "../park-selfcheck.js";
|
|
91
92
|
import { GLOBAL_USAGE_KEY, usageRetryAfterMs } from "../usage-window-store.js";
|
|
92
93
|
import { deliverEngineNotice } from "../types.js";
|
|
93
94
|
const announcedMaterializeEnv = new Set();
|
|
@@ -192,6 +193,46 @@ async function forgetQuietly(sessions, sessionId) {
|
|
|
192
193
|
catch {
|
|
193
194
|
}
|
|
194
195
|
}
|
|
196
|
+
function screenGateSettlement(result, settling) {
|
|
197
|
+
const defects = [];
|
|
198
|
+
const reportedBy = result.settledBy;
|
|
199
|
+
let settledBy;
|
|
200
|
+
if (reportedBy !== undefined) {
|
|
201
|
+
if (!isApprovalSettledBy(reportedBy) || (!settling && reportedBy !== "human")) {
|
|
202
|
+
defects.push(`a tool-gate settlement reported settledBy "${String(reportedBy)}" on ${settling ? "a blocked" : "an executing"} call — ` +
|
|
203
|
+
`it is one of "human" / "timeout" / "aborted", and only "human" can be the source of a call that runs; the frame carries no source`);
|
|
204
|
+
}
|
|
205
|
+
else
|
|
206
|
+
settledBy = reportedBy;
|
|
207
|
+
}
|
|
208
|
+
const attribution = screenApproverAttribution(result.approver);
|
|
209
|
+
if (attribution.defect !== undefined) {
|
|
210
|
+
defects.push(`a tool-gate settlement reported an attribution this engine refuses: ${attribution.defect}; the frame carries no approver`);
|
|
211
|
+
}
|
|
212
|
+
const record = {
|
|
213
|
+
...(settledBy !== undefined ? { settledBy } : {}),
|
|
214
|
+
...(attribution.approver !== undefined ? { approver: attribution.approver } : {}),
|
|
215
|
+
};
|
|
216
|
+
return { ...(settledBy !== undefined || attribution.approver !== undefined ? { record } : {}), defects };
|
|
217
|
+
}
|
|
218
|
+
function inheritedAskRuleEvidence(deps) {
|
|
219
|
+
const org = deps.permissionRuleOrg === undefined ? "not_wired" : "not_adjudicated";
|
|
220
|
+
const personal = deps.permissionRuleStore === undefined ? "not_wired" : "not_adjudicated";
|
|
221
|
+
return Object.freeze({ orgRevisionAbsent: org, orgRuleAbsent: org, personalRuleDotsAbsent: personal });
|
|
222
|
+
}
|
|
223
|
+
function orgRevisionEvidenceOf(resolution, onDefect) {
|
|
224
|
+
const reported = resolution.revision;
|
|
225
|
+
if (reported === undefined)
|
|
226
|
+
return {};
|
|
227
|
+
if (typeof reported === "number" && Number.isFinite(reported))
|
|
228
|
+
return { revision: reported };
|
|
229
|
+
onDefect(`the org rule overlay reported revision ${JSON.stringify(reported)} — a snapshot revision is a finite number; ` +
|
|
230
|
+
`the adjudication stands, but the ask carries no revision evidence for this call`);
|
|
231
|
+
return {};
|
|
232
|
+
}
|
|
233
|
+
function persistedRuleHitOf(admitting) {
|
|
234
|
+
return admitting === undefined ? undefined : { rule: admitting.rule, dots: admitting.adds.map((a) => ({ actor: a.dot.actor, counter: a.dot.counter })) };
|
|
235
|
+
}
|
|
195
236
|
export async function prepareTask(spec, deps, sessions, resume, internals, runnerSelf) {
|
|
196
237
|
const doors = prepareConfigDoors({ spec, deps, sessions, resume, internals });
|
|
197
238
|
spec = doors.spec;
|
|
@@ -204,7 +245,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
204
245
|
e.code = "resume.session_not_found";
|
|
205
246
|
throw e;
|
|
206
247
|
}
|
|
207
|
-
const { acquired, session, conflictRef, wakeRecovered, resumeAtBeforeParentId } = await prepareAcquireReconcile({ sessions, spec, resume, toolEffects });
|
|
248
|
+
const { acquired, session, conflictRef, wakeRecovered, resumeAtBeforeParentId } = await prepareAcquireReconcile({ sessions, spec, resume, toolEffects, ...(() => { const g = durableParkGapFor(deps, spec); return g !== undefined ? { durableParkGap: g } : {}; })() });
|
|
208
249
|
const sessionId = acquired.sessionId;
|
|
209
250
|
const hostTaskId = spec.taskId ?? sessionId;
|
|
210
251
|
if (compModel !== undefined) {
|
|
@@ -2778,6 +2819,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2778
2819
|
return {};
|
|
2779
2820
|
return { riskAxes: { ...(irreversible !== undefined ? { irreversible } : {}), ...(egress !== undefined ? { egress } : {}) } };
|
|
2780
2821
|
};
|
|
2822
|
+
const inheritedAskEvidence = inheritedAskRuleEvidence(deps);
|
|
2781
2823
|
const permissionRuleLane = (() => {
|
|
2782
2824
|
const provider = deps.permissionRuleStore;
|
|
2783
2825
|
const localOwnerDeclared = deps.localOwnerRules === true;
|
|
@@ -2817,9 +2859,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2817
2859
|
message: err instanceof Error ? err.message : String(err),
|
|
2818
2860
|
ts: Date.now(),
|
|
2819
2861
|
}));
|
|
2820
|
-
return
|
|
2862
|
+
return { unreadable: true };
|
|
2821
2863
|
}
|
|
2822
|
-
return findAdmittingRule(listed.rules, { tool: req.toolName, command, cwd: root })
|
|
2864
|
+
return persistedRuleHitOf(findAdmittingRule(listed.rules, { tool: req.toolName, command, cwd: root }));
|
|
2823
2865
|
},
|
|
2824
2866
|
};
|
|
2825
2867
|
})();
|
|
@@ -2840,11 +2882,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2840
2882
|
}
|
|
2841
2883
|
if (resolution.status === "unavailable")
|
|
2842
2884
|
return { status: "unavailable", disclosures: resolution.disclosures };
|
|
2885
|
+
const revisionCell = orgRevisionEvidenceOf(resolution, (message) => deps.onError?.(new Error(message), { phase: "config", sessionId }));
|
|
2843
2886
|
const command = req.args?.command;
|
|
2844
2887
|
if (req.toolName !== PERSISTED_RULE_TOOL || typeof command !== "string")
|
|
2845
|
-
return { status: "available" };
|
|
2888
|
+
return { status: "available", ...revisionCell };
|
|
2846
2889
|
const verdict = orgRuleVerdictFor(resolution.rules, { tool: req.toolName, command });
|
|
2847
|
-
return verdict === undefined ? { status: "available" } : { status: "available", verdict };
|
|
2890
|
+
return verdict === undefined ? { status: "available", ...revisionCell } : { status: "available", verdict, ...revisionCell };
|
|
2848
2891
|
},
|
|
2849
2892
|
};
|
|
2850
2893
|
})();
|
|
@@ -2926,6 +2969,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2926
2969
|
...riskAxesOf(creq.toolName),
|
|
2927
2970
|
...(re.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
2928
2971
|
...(re.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: re.persistedRuleShadowed } : {}),
|
|
2972
|
+
ruleEvidence: inheritedAskEvidence,
|
|
2929
2973
|
}, onAskOf, csignal ?? abortController.signal);
|
|
2930
2974
|
if (rr.action !== "allow")
|
|
2931
2975
|
return rr;
|
|
@@ -3011,6 +3055,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3011
3055
|
...riskAxesOf(creq.toolName),
|
|
3012
3056
|
...(first.action === "ask" && first.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
3013
3057
|
...(first.action === "ask" && first.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: first.persistedRuleShadowed } : {}),
|
|
3058
|
+
ruleEvidence: inheritedAskEvidence,
|
|
3014
3059
|
}, pc.onAsk, csignal ?? abortController.signal);
|
|
3015
3060
|
const askWaitMs = Math.max(0, now() - askT0);
|
|
3016
3061
|
if (resolved.action === "deny" && resolved.approverUnavailable === true) {
|
|
@@ -3098,6 +3143,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3098
3143
|
...riskAxesOf(creq.toolName),
|
|
3099
3144
|
...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
3100
3145
|
...(decision.action === "ask" && decision.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: decision.persistedRuleShadowed } : {}),
|
|
3146
|
+
ruleEvidence: inheritedAskEvidence,
|
|
3101
3147
|
}, pc.onAsk, csignal ?? abortController.signal);
|
|
3102
3148
|
const askWaitMs = Math.max(0, now() - askT0);
|
|
3103
3149
|
if (resolved.action === "deny" && resolved.approverUnavailable === true) {
|
|
@@ -3239,7 +3285,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3239
3285
|
}
|
|
3240
3286
|
const preToolContexts = new Map();
|
|
3241
3287
|
const blockedToolCalls = new Set();
|
|
3242
|
-
const
|
|
3288
|
+
const approvalSettlement = new Map();
|
|
3243
3289
|
const blockedTracked = Boolean(hooks?.postToolUse || hooks?.preToolUse || hooks?.postToolUseFailure || hooks?.postToolBatch);
|
|
3244
3290
|
const restoreSurfaceGap = ownedEnv !== undefined && isRemoteExecutionEnv(ownedEnv) && ownedEnv.capabilities.suspendable ? missingRestoreSurface(ownedEnv) : [];
|
|
3245
3291
|
const incompleteSuspendAdapter = restoreSurfaceGap.length > 0 ? restoreSurfaceGap : undefined;
|
|
@@ -3413,6 +3459,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3413
3459
|
...(decision.action === "ask" && decision.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: decision.persistedRuleShadowed } : {}),
|
|
3414
3460
|
...(decision.action === "ask" && decision.probeReason !== undefined ? { probeReason: decision.probeReason } : {}),
|
|
3415
3461
|
...(decision.action === "ask" && decision.probeCause !== undefined ? { probeCause: decision.probeCause } : {}),
|
|
3462
|
+
...(decision.action === "ask" && decision.ruleEvidence !== undefined ? { ruleEvidence: decision.ruleEvidence } : {}),
|
|
3416
3463
|
}, onAsk, abortController.signal);
|
|
3417
3464
|
const waitMs = Math.max(0, now() - t0);
|
|
3418
3465
|
if (resolved.approverUnavailable !== true) {
|
|
@@ -4230,17 +4277,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4230
4277
|
if (blockedTracked && (result.block || result.suspend)) {
|
|
4231
4278
|
blockedToolCalls.add(e.toolCallId);
|
|
4232
4279
|
}
|
|
4233
|
-
const
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
`it is one of "human" / "timeout" / "aborted", and only "human" can be the source of a call that runs; the frame carries no source`), { phase: "config", sessionId });
|
|
4239
|
-
}
|
|
4240
|
-
else {
|
|
4241
|
-
approvalSettledBy.set(e.toolCallId, reported);
|
|
4242
|
-
}
|
|
4243
|
-
}
|
|
4280
|
+
const settlement = screenGateSettlement(result, result.block === true);
|
|
4281
|
+
for (const defect of settlement.defects)
|
|
4282
|
+
deps.onError?.(new Error(defect), { phase: "config", sessionId });
|
|
4283
|
+
if (settlement.record !== undefined)
|
|
4284
|
+
approvalSettlement.set(e.toolCallId, settlement.record);
|
|
4244
4285
|
return result.block
|
|
4245
4286
|
? { block: true, reason: result.reason }
|
|
4246
4287
|
: result.updatedInput !== undefined
|
|
@@ -4552,7 +4593,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4552
4593
|
const effectiveReadFaceObserved = carrierReadFace();
|
|
4553
4594
|
const effectiveReadDenyObserved = readDenyAdditionsNormalized.length > 0 ? readDenyAdditionsNormalized.map((e) => ({ ...e })) : undefined;
|
|
4554
4595
|
const preparedHolder = {};
|
|
4555
|
-
const buildPrepared = () => ({ harness, session, sessionId, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls,
|
|
4596
|
+
const buildPrepared = () => ({ harness, session, sessionId, 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 } : {}), 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, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
|
|
4556
4597
|
const prepared = buildPrepared();
|
|
4557
4598
|
preparedHolder.current = prepared;
|
|
4558
4599
|
return prepared;
|
|
@@ -80,13 +80,18 @@ declare function toolEndBodyFrom(result: unknown, isError: boolean,
|
|
|
80
80
|
* a parameter and never derived from `result`: a tool's own `details` (which post-tool hooks may
|
|
81
81
|
* also replace) is writable by layers that adjudicate nothing, so reading provenance out of it would
|
|
82
82
|
* let a failing tool claim a person approved it. Omitted ⇒ this call settled no approval. */
|
|
83
|
-
settledBy?: ApprovalSettledBy
|
|
83
|
+
settledBy?: ApprovalSettledBy,
|
|
84
|
+
/** design/252 G-7 — WHOSE settlement, from the same caller and the same channel as `settledBy`, and
|
|
85
|
+
* for the same reason it is a parameter: an attribution read out of a tool's own result would let a
|
|
86
|
+
* tool name the person who approved it. Omitted ⇒ this call's settlement named nobody. */
|
|
87
|
+
approver?: string): {
|
|
84
88
|
output?: unknown;
|
|
85
89
|
truncated?: boolean;
|
|
86
90
|
totalChars?: number;
|
|
87
91
|
structured?: unknown;
|
|
88
92
|
errorCode?: string;
|
|
89
93
|
settledBy?: ApprovalSettledBy;
|
|
94
|
+
approver?: string;
|
|
90
95
|
};
|
|
91
96
|
/**
|
|
92
97
|
* scan-1/A1 — the BODY of the synthetic `tool_end` that closes a reconcile-recovered orphan. ONE
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { persistedReadDenyEntryProblem } from "../../tools/fs/read-deny.js";
|
|
2
|
-
import { createSafeNotifier } from "../safe-notify.js";
|
|
2
|
+
import { createSafeNotifier, observeThenableRejection } from "../safe-notify.js";
|
|
3
3
|
import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
|
|
4
4
|
import { snapshotActorAssertion } from "../../internal/llm.js";
|
|
5
|
-
import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
|
|
5
|
+
import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, LEGACY_PENDING_STEER_INPUT_ID, MAX_STEER_INPUT_ID_CHARS, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
|
|
6
6
|
import { engineVersion } from "../version.js";
|
|
7
7
|
import { CONFIG_CATALOG_VERSION, declarationReasons, resolveEffectiveConfig } from "../../config/catalog.js";
|
|
8
8
|
import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
|
|
@@ -41,7 +41,7 @@ import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntr
|
|
|
41
41
|
import { reconcileInterruptedSession } from "../session-reconcile.js";
|
|
42
42
|
import { RunnerSharedToolResultStore } from "../tool-result-store.js";
|
|
43
43
|
import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
|
|
44
|
-
import { checkToolPolicyProjection, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, refuseOutOfContractDecision, toolPolicyNameSets } from "../tool-policy.js";
|
|
44
|
+
import { checkToolPolicyProjection, constraintChainDigest, constraintChainEntryOf, isApprovalSettledBy, refuseOutOfContractDecision, screenApproverAttribution, toolPolicyNameSets } from "../tool-policy.js";
|
|
45
45
|
import { defaultTaskRegistry } from "../task-registry.js";
|
|
46
46
|
import { discloseDroppedPending, isDelegatedAgentTerminal, PendingSessionNotifications, renderTaskNotificationXml, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
|
|
47
47
|
import { ToolDetachHub } from "../tool-detach.js";
|
|
@@ -68,6 +68,13 @@ function nextHumanInputSeq(key) {
|
|
|
68
68
|
}
|
|
69
69
|
return ++box.n;
|
|
70
70
|
}
|
|
71
|
+
function sameAcceptedSteerInput(a, b) {
|
|
72
|
+
return (a.payload === b.payload &&
|
|
73
|
+
a.trusted === b.trusted &&
|
|
74
|
+
a.actor?.id === b.actor?.id &&
|
|
75
|
+
a.actor?.hostAsserted === b.actor?.hostAsserted &&
|
|
76
|
+
a.actor?.issuer === b.actor?.issuer);
|
|
77
|
+
}
|
|
71
78
|
const MAX_CONSECUTIVE_COMPACTION_FAILURES = 3;
|
|
72
79
|
const STOP_HOOK_BLOCK_CAP = 8;
|
|
73
80
|
const COMPACTION_REGROWTH_FACTOR = 1.5;
|
|
@@ -130,7 +137,7 @@ function resumeDecisionWasNegative(resume) {
|
|
|
130
137
|
}
|
|
131
138
|
const DEFERRED_REISSUE = "[DEFERRED] This tool call shared a batch with a call that suspended for durable approval, so it was " +
|
|
132
139
|
"NOT executed on resume. If you still need it, issue it again now.";
|
|
133
|
-
function toolEndBodyFrom(result, isError, settledBy) {
|
|
140
|
+
function toolEndBodyFrom(result, isError, settledBy, approver) {
|
|
134
141
|
const o = toolOutputFrom(result);
|
|
135
142
|
const st = structuredFrom(result);
|
|
136
143
|
const det = isError ? result?.details : undefined;
|
|
@@ -142,6 +149,7 @@ function toolEndBodyFrom(result, isError, settledBy) {
|
|
|
142
149
|
...(st !== undefined ? { structured: st } : {}),
|
|
143
150
|
...(typeof code === "string" ? { errorCode: code } : {}),
|
|
144
151
|
...(settledBy !== undefined ? { settledBy } : {}),
|
|
152
|
+
...(approver !== undefined ? { approver } : {}),
|
|
145
153
|
};
|
|
146
154
|
}
|
|
147
155
|
export function reconciledToolEndBody(orphan) {
|
|
@@ -1154,7 +1162,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
|
|
|
1154
1162
|
rs.counters.groundingSignalPostR9 = true;
|
|
1155
1163
|
}
|
|
1156
1164
|
const activityArg = primaryActivityArg(event.args);
|
|
1157
|
-
internalsNotifier.notify(() => internals?.onActivity?.({ phase: "start", toolCallId: event.toolCallId, toolName: event.toolName, at: startAt, ...(activityArg !== undefined ? { arg: activityArg } : {}) }), "runtask.onActivity.start");
|
|
1165
|
+
internalsNotifier.notify(() => observeThenableRejection(internals?.onActivity?.({ phase: "start", toolCallId: event.toolCallId, toolName: event.toolName, at: startAt, ...(activityArg !== undefined ? { arg: activityArg } : {}) }), internalsNotifier, "runtask.onActivity.start"), "runtask.onActivity.start");
|
|
1158
1166
|
pushContent({
|
|
1159
1167
|
type: "tool_start",
|
|
1160
1168
|
toolCallId: event.toolCallId,
|
|
@@ -1203,7 +1211,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
|
|
|
1203
1211
|
ok: !event.isError,
|
|
1204
1212
|
ts: toolNow,
|
|
1205
1213
|
}));
|
|
1206
|
-
internalsNotifier.notify(() => internals?.onActivity?.({ phase: "end", toolCallId: event.toolCallId, toolName: event.toolName, isError: event.isError, at: toolNow }), "runtask.onActivity.end");
|
|
1214
|
+
internalsNotifier.notify(() => observeThenableRejection(internals?.onActivity?.({ phase: "end", toolCallId: event.toolCallId, toolName: event.toolName, isError: event.isError, at: toolNow }), internalsNotifier, "runtask.onActivity.end"), "runtask.onActivity.end");
|
|
1207
1215
|
if (prepared.lspDiagnostics && !event.isError) {
|
|
1208
1216
|
const name = event.toolName;
|
|
1209
1217
|
if (name === "Edit" || name === "Write" || name === "NotebookEdit") {
|
|
@@ -1213,16 +1221,16 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
|
|
|
1213
1221
|
prepared.lspDiagnostics.nudge(p);
|
|
1214
1222
|
}
|
|
1215
1223
|
}
|
|
1216
|
-
const
|
|
1217
|
-
if (
|
|
1218
|
-
prepared.
|
|
1224
|
+
const settlement = prepared.approvalSettlement.get(event.toolCallId);
|
|
1225
|
+
if (settlement !== undefined)
|
|
1226
|
+
prepared.approvalSettlement.delete(event.toolCallId);
|
|
1219
1227
|
pushContent({
|
|
1220
1228
|
type: "tool_end",
|
|
1221
1229
|
toolCallId: event.toolCallId,
|
|
1222
1230
|
toolName: event.toolName,
|
|
1223
1231
|
...(toolLabels.get(event.toolName) !== undefined ? { label: toolLabels.get(event.toolName) } : {}),
|
|
1224
1232
|
isError: event.isError,
|
|
1225
|
-
...toolEndBodyFrom(event.result, event.isError, settledBy),
|
|
1233
|
+
...toolEndBodyFrom(event.result, event.isError, settlement?.settledBy, settlement?.approver),
|
|
1226
1234
|
...ident(),
|
|
1227
1235
|
});
|
|
1228
1236
|
announceWorkspaceMove();
|
|
@@ -1444,6 +1452,7 @@ export class Runner {
|
|
|
1444
1452
|
};
|
|
1445
1453
|
let reapHandle;
|
|
1446
1454
|
let steerChain = Promise.resolve();
|
|
1455
|
+
const acceptedSteerInputs = new Map();
|
|
1447
1456
|
const notifyRef = {};
|
|
1448
1457
|
const manualCompactRef = { requested: false, waiters: [] };
|
|
1449
1458
|
const drainManualCompactWaiters = (outcome) => {
|
|
@@ -1565,6 +1574,7 @@ export class Runner {
|
|
|
1565
1574
|
queue.push({ type: "done", result: resultValue });
|
|
1566
1575
|
queue.close();
|
|
1567
1576
|
});
|
|
1577
|
+
void settled.then(() => acceptedSteerInputs.clear(), () => acceptedSteerInputs.clear());
|
|
1568
1578
|
const steeringError = (msg, code = "steering.not_running") => {
|
|
1569
1579
|
const e = new Error(`cannot steer: ${msg}`);
|
|
1570
1580
|
e.code = code;
|
|
@@ -1613,22 +1623,40 @@ export class Runner {
|
|
|
1613
1623
|
return suggestionsDone.catch(() => []);
|
|
1614
1624
|
},
|
|
1615
1625
|
steer: async (text, options) => {
|
|
1616
|
-
|
|
1626
|
+
const trusted = options?.trusted ? true : false;
|
|
1627
|
+
if (trusted && sanitizeUntrustedText(text) !== text) {
|
|
1617
1628
|
throw steeringError("trusted steering text must not contain a </system-reminder> tag", "steering.invalid_content");
|
|
1618
1629
|
}
|
|
1619
|
-
const
|
|
1630
|
+
const inputId = options?.inputId;
|
|
1631
|
+
if (inputId !== undefined) {
|
|
1632
|
+
if (typeof inputId !== "string") {
|
|
1633
|
+
throw steeringError("inputId must be a string when supplied", "steering.invalid_content");
|
|
1634
|
+
}
|
|
1635
|
+
if (inputId === "" || inputId.length > MAX_STEER_INPUT_ID_CHARS) {
|
|
1636
|
+
throw steeringError(`inputId must be a non-empty string of at most ${MAX_STEER_INPUT_ID_CHARS} characters`, "steering.invalid_content");
|
|
1637
|
+
}
|
|
1638
|
+
if (inputId === LEGACY_PENDING_STEER_INPUT_ID) {
|
|
1639
|
+
throw steeringError(`inputId "${LEGACY_PENDING_STEER_INPUT_ID}" is reserved for a pre-queue parked steer and cannot be supplied by a caller`, "steering.invalid_content");
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
const actorIn = options?.actor;
|
|
1643
|
+
const actor = actorIn === undefined ? undefined : snapshotActorAssertion(actorIn);
|
|
1620
1644
|
const projected = projectHumanInput({ text, actor, source: "steer" });
|
|
1621
|
-
const payload =
|
|
1645
|
+
const payload = trusted ? formatHookFeedback(projected) : projected;
|
|
1622
1646
|
const mintsAFrame = payload.trim().length !== 0;
|
|
1623
|
-
const
|
|
1647
|
+
const replay = { payload, trusted, ...(actor !== undefined ? { actor } : {}) };
|
|
1648
|
+
const noteAccepted = (h) => {
|
|
1624
1649
|
if (!mintsAFrame)
|
|
1625
1650
|
return;
|
|
1651
|
+
if (typeof inputId === "string")
|
|
1652
|
+
acceptedSteerInputs.set(inputId, replay);
|
|
1626
1653
|
queue.push({
|
|
1627
1654
|
...buildHumanInputEvent({
|
|
1628
1655
|
carrier: "steer",
|
|
1629
1656
|
source: "steer",
|
|
1630
1657
|
delivery: "queued",
|
|
1631
1658
|
sessionSeq: nextHumanInputSeq(h.harness),
|
|
1659
|
+
...(typeof inputId === "string" ? { inputId } : {}),
|
|
1632
1660
|
...(actor !== undefined ? { actor } : {}),
|
|
1633
1661
|
...(actor?.issuer !== undefined ? { issuer: actor.issuer } : {}),
|
|
1634
1662
|
...(spec.principal !== undefined ? { principal: spec.principal } : {}),
|
|
@@ -1645,9 +1673,21 @@ export class Runner {
|
|
|
1645
1673
|
const h = handle ?? (await orTimeout(ready));
|
|
1646
1674
|
if (!h)
|
|
1647
1675
|
throw steeringError("the task is not running");
|
|
1676
|
+
if (typeof inputId === "string") {
|
|
1677
|
+
const prior = acceptedSteerInputs.get(inputId);
|
|
1678
|
+
if (prior !== undefined) {
|
|
1679
|
+
if (resultValue !== undefined || h.loop.ended)
|
|
1680
|
+
throw steeringError("the task is no longer running");
|
|
1681
|
+
if (!sameAcceptedSteerInput(prior, replay)) {
|
|
1682
|
+
throw steeringError("a different steering instruction was already accepted under this inputId — re-issue this one with a fresh inputId " +
|
|
1683
|
+
"(an identical payload would have been an idempotent retry)", "steering.duplicate_input_id");
|
|
1684
|
+
}
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1648
1688
|
try {
|
|
1649
1689
|
await h.harness.steer(payload, { provenance: "engine-note", ...(actor !== undefined ? { actor } : {}) });
|
|
1650
|
-
|
|
1690
|
+
noteAccepted(h);
|
|
1651
1691
|
return;
|
|
1652
1692
|
}
|
|
1653
1693
|
catch (e) {
|
|
@@ -1658,7 +1698,7 @@ export class Runner {
|
|
|
1658
1698
|
while (resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) {
|
|
1659
1699
|
try {
|
|
1660
1700
|
await h.harness.steer(payload, { provenance: "engine-note", ...(actor !== undefined ? { actor } : {}) });
|
|
1661
|
-
|
|
1701
|
+
noteAccepted(h);
|
|
1662
1702
|
return;
|
|
1663
1703
|
}
|
|
1664
1704
|
catch (e2) {
|
|
@@ -2329,8 +2369,11 @@ export class Runner {
|
|
|
2329
2369
|
queue.push({ type: "message_committed", entryId, role, ...(toolCallId !== undefined ? { toolCallId } : {}), ...ident() });
|
|
2330
2370
|
};
|
|
2331
2371
|
const subagentName = parentToolCallId !== undefined && internals?.agentName !== undefined ? inlineUntrusted(internals.agentName.slice(0, 320), 80) : undefined;
|
|
2372
|
+
const statusSinkNotifier = createSafeNotifier({
|
|
2373
|
+
onError: (f) => console.warn(`[sema-core] ${f.site}: run-internals status sink threw (contained; further failures counted, not re-disclosed): ${f.error.message}`),
|
|
2374
|
+
});
|
|
2332
2375
|
const statusEmit = (s) => {
|
|
2333
|
-
|
|
2376
|
+
const frame = {
|
|
2334
2377
|
type: "status",
|
|
2335
2378
|
phase: s.phase,
|
|
2336
2379
|
...(s.detail !== undefined ? { detail: s.detail } : {}),
|
|
@@ -2340,7 +2383,12 @@ export class Runner {
|
|
|
2340
2383
|
...(s.maxRetries !== undefined ? { maxRetries: s.maxRetries } : {}),
|
|
2341
2384
|
...(s.errClass !== undefined ? { errClass: s.errClass } : {}),
|
|
2342
2385
|
...ident(),
|
|
2343
|
-
}
|
|
2386
|
+
};
|
|
2387
|
+
Object.freeze(frame);
|
|
2388
|
+
const accepted = queue.push(frame);
|
|
2389
|
+
if (accepted && internals?.onStatusEvent !== undefined) {
|
|
2390
|
+
statusSinkNotifier.notify(() => observeThenableRejection(internals.onStatusEvent?.(frame), statusSinkNotifier, "runtask.onStatusEvent"), "runtask.onStatusEvent");
|
|
2391
|
+
}
|
|
2344
2392
|
};
|
|
2345
2393
|
const telemetryEmit = (t) => {
|
|
2346
2394
|
emitTrace(rs.telemetry.tracer, () => t.kind === "failover"
|
|
@@ -3656,6 +3704,15 @@ export class Runner {
|
|
|
3656
3704
|
throw new CheckpointError("checkpoint.invalid_outcome", `resume carries decision "allow" with settledBy "${settledBy}" — a non-human settlement is a fail-closed end (nobody answered), so it cannot be the source of an approval that EXECUTES; ` +
|
|
3657
3705
|
"supply \"human\", or omit the field if the allow came from configuration rather than a person; refusing pre-CAS, the checkpoint stays pending", { field: "settledBy" });
|
|
3658
3706
|
}
|
|
3707
|
+
const approver = decide.approver;
|
|
3708
|
+
const attribution = screenApproverAttribution(approver);
|
|
3709
|
+
if (attribution.defect !== undefined) {
|
|
3710
|
+
throw new CheckpointError("checkpoint.invalid_outcome", `resume carries an approver attribution this engine refuses: ${attribution.defect}; refusing pre-CAS, the checkpoint stays pending`, { field: "approver" });
|
|
3711
|
+
}
|
|
3712
|
+
if (attribution.approver !== undefined && settledBy === undefined) {
|
|
3713
|
+
throw new CheckpointError("checkpoint.invalid_outcome", `resume names an approver but no settledBy — an attribution says WHOSE settlement it was and needs the word that says what KIND of end it was beside it; ` +
|
|
3714
|
+
`supply settledBy ("human" / "timeout" / "aborted"), or omit the approver; refusing pre-CAS, the checkpoint stays pending`, { field: "approver" });
|
|
3715
|
+
}
|
|
3659
3716
|
const reason = decide.reason;
|
|
3660
3717
|
assertOutcomeText(reason, "reason");
|
|
3661
3718
|
plainPolicyOutcome = {
|
|
@@ -3667,6 +3724,7 @@ export class Runner {
|
|
|
3667
3724
|
...(reason !== undefined ? { reason } : {}),
|
|
3668
3725
|
...(redeemedAnswer !== undefined ? { answer: redeemedAnswer } : {}),
|
|
3669
3726
|
...(settledBy !== undefined ? { settledBy } : {}),
|
|
3727
|
+
...(attribution.approver !== undefined ? { approver: attribution.approver } : {}),
|
|
3670
3728
|
};
|
|
3671
3729
|
if (plainPolicyOutcome.decision === "deny" && plainPolicyOutcome.reason && sanitizeUntrustedText(plainPolicyOutcome.reason) !== plainPolicyOutcome.reason) {
|
|
3672
3730
|
throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
|
|
@@ -4029,7 +4087,7 @@ export class Runner {
|
|
|
4029
4087
|
const resolvedArgs = pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME ? pendingAction.args : (outcome.updatedInput !== undefined ? outcome.updatedInput : pendingAction.args);
|
|
4030
4088
|
const pendingLabel = (() => { const l = prepared.tools.find((t) => t.name === pendingAction.toolName)?.label; return l !== undefined && l !== pendingAction.toolName ? { label: l } : {}; })();
|
|
4031
4089
|
emit({ type: "tool_start", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, args: resolvedArgs });
|
|
4032
|
-
const emitEnd = (isError, result) => emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError, outcome.settledBy) });
|
|
4090
|
+
const emitEnd = (isError, result) => emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError, outcome.settledBy, outcome.approver) });
|
|
4033
4091
|
if (outcome.decision === "deny") {
|
|
4034
4092
|
const defaultDenial = outcome.settledBy === "timeout"
|
|
4035
4093
|
? `No one answered the approval request for the pending tool call "${pendingAction.toolName}" — the approval window elapsed with no answer, so it was not executed.`
|
|
@@ -79,3 +79,12 @@ export interface SafeNotifier {
|
|
|
79
79
|
* notifier whose callbacks never throw costs one object and nothing per call beyond the `try`.
|
|
80
80
|
*/
|
|
81
81
|
export declare function createSafeNotifier(opts?: SafeNotifierOptions): SafeNotifier;
|
|
82
|
+
/**
|
|
83
|
+
* Observe a `=> void` host callback's RETURN value for the async dialect: TypeScript accepts an
|
|
84
|
+
* `async` function at a void seat, and a rejected one would escape {@link SafeNotifier.notify}
|
|
85
|
+
* (which deliberately ignores return values — see the scope note above) as an unhandled rejection.
|
|
86
|
+
* Call this with the callback's return value INSIDE the notify thunk: a thenable's rejection is
|
|
87
|
+
* routed back through the same notifier/site, so async and sync failures share one count and one
|
|
88
|
+
* bounded disclosure, and neither can fault the engine's control flow.
|
|
89
|
+
*/
|
|
90
|
+
export declare function observeThenableRejection(r: unknown, notifier: SafeNotifier, site: string): void;
|
package/dist/core/safe-notify.js
CHANGED
|
@@ -57,3 +57,12 @@ export function createSafeNotifier(opts) {
|
|
|
57
57
|
},
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
|
+
export function observeThenableRejection(r, notifier, site) {
|
|
61
|
+
if (typeof r?.then === "function") {
|
|
62
|
+
r.then(undefined, (err) => {
|
|
63
|
+
notifier.notify(() => {
|
|
64
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
65
|
+
}, site);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -117,8 +117,8 @@ export type WorkerErrorClass = "budget" | "limit" | "output" | "suspend" | "revi
|
|
|
117
117
|
* are NOT all terminal-errorCode-shaped — see {@link EXACT_CODE_CLASS}'s comment), plus the flat 1.36 codes.
|
|
118
118
|
* The `limits.` namespace splits into the `budget` and `limit` classes by exact code — see
|
|
119
119
|
* {@link EXACT_CODE_CLASS}. Operation-level dotted codes that are surfaced to a *caller* and never become a
|
|
120
|
-
* task outcome are deliberately OUT of scope: `steering.*` (`steering.not_running`/`steering.invalid_content
|
|
121
|
-
* rejected to the `steer()` caller) and `mcp.*` (`mcp.server_unavailable`, an `onWarn` warning code) — neither
|
|
120
|
+
* task outcome are deliberately OUT of scope: `steering.*` (`steering.not_running`/`steering.invalid_content`/
|
|
121
|
+
* `steering.duplicate_input_id`, rejected to the `steer()` caller) and `mcp.*` (`mcp.server_unavailable`, an `onWarn` warning code) — neither
|
|
122
122
|
* reaches `TaskResult.errorCode`, so a caller will never pass them here. A genuinely unmapped code → `"unknown"` (which therefore means
|
|
123
123
|
* "known-but-foreign or no code", e.g. a leaked fs/Node code, NOT "an OUR terminal class we forgot to add").
|
|
124
124
|
*/
|