@sema-agent/core 7.9.1 → 7.10.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 +54 -0
- package/dist/agents/child-model-seat.d.ts +81 -0
- package/dist/agents/child-model-seat.js +46 -0
- package/dist/agents/subagent.d.ts +5 -3
- package/dist/agents/subagent.js +20 -14
- package/dist/core/ask-unresolvable-notice.d.ts +52 -0
- package/dist/core/ask-unresolvable-notice.js +25 -0
- package/dist/core/auto-mode.d.ts +62 -3
- package/dist/core/auto-mode.js +31 -0
- package/dist/core/checkpoint-store.d.ts +14 -0
- package/dist/core/checkpoint-store.js +2 -1
- package/dist/core/engine-notice.d.ts +28 -7
- package/dist/core/gate-lanes.js +15 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +4 -0
- package/dist/core/hooks.d.ts +24 -1
- package/dist/core/hooks.js +2 -0
- package/dist/core/permission-rule-model.d.ts +51 -16
- package/dist/core/permission-rule-model.js +55 -21
- package/dist/core/permission-rules.d.ts +6 -4
- package/dist/core/permission-rules.js +14 -14
- package/dist/core/roles.d.ts +8 -0
- package/dist/core/runner/contracts.d.ts +29 -4
- package/dist/core/runner/denial-limit-arms.d.ts +14 -3
- package/dist/core/runner/denial-limit-arms.js +15 -5
- package/dist/core/runner/permission-rule-lanes.d.ts +7 -1
- package/dist/core/runner/permission-rule-lanes.js +9 -3
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +1 -1
- package/dist/core/runner/prepare-caps-and-workflow.js +14 -4
- package/dist/core/runner/prepare-gate-stations.d.ts +3 -2
- package/dist/core/runner/prepare-gate-stations.js +3 -0
- package/dist/core/runner/prepare-policy-chain.js +7 -6
- package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -1
- package/dist/core/runner/prepare-wiring-manifest.js +8 -1
- package/dist/core/runner/runtask.d.ts +34 -32
- package/dist/core/runner/runtask.js +64 -34
- package/dist/core/runner-deps.d.ts +9 -2
- package/dist/core/swappable-deps.d.ts +90 -0
- package/dist/core/swappable-deps.js +55 -0
- package/dist/core/tool-policy.d.ts +26 -0
- package/dist/core/tool-policy.js +5 -1
- package/dist/core/wiring-manifest.d.ts +15 -1
- package/dist/core/wiring-manifest.js +10 -2
- package/dist/core/workflow-journal-store.d.ts +21 -2
- package/dist/core/workflow-journal-store.js +1 -1
- package/dist/engine/execution-env/node-execution-env.d.ts +2 -0
- package/dist/engine/execution-env/node-execution-env.js +2 -1
- package/dist/engine/harness/types.d.ts +11 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +5 -3
- package/dist/orchestration/run-workflow-tool.d.ts +17 -0
- package/dist/orchestration/run-workflow-tool.js +12 -0
- package/dist/orchestration/workflow-observe.d.ts +1 -1
- package/dist/orchestration/workflow-observe.js +2 -0
- package/dist/orchestration/workflow-types.d.ts +37 -2
- package/dist/orchestration/workflow-types.js +16 -0
- package/dist/orchestration/workflow.d.ts +41 -2
- package/dist/orchestration/workflow.js +316 -48
- package/dist/stores/file/workflow-journal-store.js +10 -3
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +51 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AutoModeDecider, type AutoModeDenialLimitOptions, type AutoModeDenialTracker, type DenialLimitFallback, type DenialLimitFallbackFace, type UnarmedDenialLimitFallback } from "../auto-mode.js";
|
|
1
|
+
import { type AutoModeDecider, type AutoModeDenialLimitOptions, type AutoModeDenialTracker, type AutoModeUnavailableCause, type DenialLimitFallback, type DenialLimitFallbackFace, type UnarmedDenialLimitFallback } from "../auto-mode.js";
|
|
2
2
|
import { type AutoModeArmingRecipe } from "../auto-mode-arming.js";
|
|
3
3
|
import type { PermissionResult, ResolvedAsk, ToolCallRequest } from "../tool-policy.js";
|
|
4
4
|
import { type EngineNotice } from "../types.js";
|
|
@@ -84,10 +84,14 @@ export type InheritedAsk = Extract<PermissionResult, {
|
|
|
84
84
|
}>;
|
|
85
85
|
/** The members of a surviving ask that ride onto the approval request with a COMPUTED value — what the
|
|
86
86
|
* four request mint stations (the gate's own and the three inherited-lane ones) spread after the seats
|
|
87
|
-
* they spell themselves: the ask's origin word
|
|
87
|
+
* they spell themselves: the ask's origin word, its denial-limit fallback and the classifier-unavailable
|
|
88
|
+
* fact (#616). */
|
|
88
89
|
export interface AskRequestCarry {
|
|
89
90
|
origin?: AskOrigin;
|
|
90
91
|
denialLimitFallback?: DenialLimitFallback;
|
|
92
|
+
classifierUnavailable?: {
|
|
93
|
+
readonly cause: AutoModeUnavailableCause;
|
|
94
|
+
};
|
|
91
95
|
}
|
|
92
96
|
/** The GATE's own mint station: both members are read off the surviving decision, which is the gate's
|
|
93
97
|
* OWN snapshot (its stamp point spreads the policy's object and stamps the word onto the copy), so the
|
|
@@ -99,7 +103,9 @@ export declare function gateAskCarry(decision: InheritedAsk, liveApprover: boole
|
|
|
99
103
|
* fallback member it captured — each read ONCE there and threaded here, never re-read off the
|
|
100
104
|
* caller-owned object (a stateful accessor could otherwise answer the card with one value and the
|
|
101
105
|
* settlement with another; a self-declared `origin` on the ancestor's decision is never read). */
|
|
102
|
-
export declare function inheritedAskCarry(
|
|
106
|
+
export declare function inheritedAskCarry(judged: Pick<Extract<InheritedClassifierJudgment<InheritedAsk>, {
|
|
107
|
+
kind: "resolve";
|
|
108
|
+
}>, "origin" | "fallback" | "ask">, liveApprover: boolean, tracker: AutoModeDenialTracker | undefined): AskRequestCarry;
|
|
103
109
|
/** What {@link judgeInheritedClassifier} hands back to the arm that called it. */
|
|
104
110
|
export type InheritedClassifierJudgment<A extends InheritedAsk> =
|
|
105
111
|
/** The frozen classifier allowed — the arm returns its own allow shape (with or without the ask's rewrite). */
|
|
@@ -118,6 +124,11 @@ export type InheritedClassifierJudgment<A extends InheritedAsk> =
|
|
|
118
124
|
* unchanged-ask branches it is the SAME read the exclusion judged by (a caller-owned decision with a
|
|
119
125
|
* stateful accessor cannot answer the exclusion with one word and the card with another); on the
|
|
120
126
|
* bound branch it is derived from the re-spoken ask this station minted (its own object). */
|
|
127
|
+
/** `ask` is the incoming ask unchanged — EXCEPT on an UNAVAILABLE round, where it is an engine-owned copy
|
|
128
|
+
* carrying `classifierUnavailable: { cause }` (#616): the fact rides the ASK because the ask is what the
|
|
129
|
+
* wrapper arms hand back to the gate when the frozen approver answers unavailable (the inherited-unavailable
|
|
130
|
+
* float), and the gate's own park carry reads the fact off the decision it parks — a side member on this
|
|
131
|
+
* judgment would have been lost on that route (codex r1). The request mint reads the same object. */
|
|
121
132
|
| {
|
|
122
133
|
kind: "resolve";
|
|
123
134
|
ask: A;
|
|
@@ -60,10 +60,18 @@ function fallbackCarry(fallback, liveApprover, tracker) {
|
|
|
60
60
|
return { denialLimitFallback: liveApprover && tracker !== undefined ? tracker.armTimedWindow(fallback) : unarmedWindow(fallback) };
|
|
61
61
|
}
|
|
62
62
|
export function gateAskCarry(decision, liveApprover, tracker) {
|
|
63
|
-
return {
|
|
63
|
+
return {
|
|
64
|
+
...(decision.origin !== undefined ? { origin: decision.origin } : {}),
|
|
65
|
+
...fallbackCarry(decision.denialLimitFallback, liveApprover, tracker),
|
|
66
|
+
...(decision.classifierUnavailable !== undefined ? { classifierUnavailable: { cause: decision.classifierUnavailable.cause } } : {}),
|
|
67
|
+
};
|
|
64
68
|
}
|
|
65
|
-
export function inheritedAskCarry(
|
|
66
|
-
return {
|
|
69
|
+
export function inheritedAskCarry(judged, liveApprover, tracker) {
|
|
70
|
+
return {
|
|
71
|
+
origin: judged.origin,
|
|
72
|
+
...fallbackCarry(judged.fallback, liveApprover, tracker),
|
|
73
|
+
...(judged.ask.classifierUnavailable !== undefined ? { classifierUnavailable: { cause: judged.ask.classifierUnavailable.cause } } : {}),
|
|
74
|
+
};
|
|
67
75
|
}
|
|
68
76
|
export async function judgeInheritedClassifier(opts) {
|
|
69
77
|
const { autoMode, ask, req } = opts;
|
|
@@ -77,8 +85,10 @@ export async function judgeInheritedClassifier(opts) {
|
|
|
77
85
|
autoMode.denialTracking?.recordAllow();
|
|
78
86
|
return { kind: "allow" };
|
|
79
87
|
}
|
|
80
|
-
if (verdict.kind !== "block")
|
|
81
|
-
|
|
88
|
+
if (verdict.kind !== "block") {
|
|
89
|
+
const resolved = verdict.kind === "unavailable" ? { ...ask, classifierUnavailable: { cause: verdict.cause } } : ask;
|
|
90
|
+
return { kind: "resolve", ask: resolved, fallback: ask.denialLimitFallback, mintedHere: false, origin };
|
|
91
|
+
}
|
|
82
92
|
const reason = verdict.reason ? inlineUntrusted(verdict.reason) : "";
|
|
83
93
|
const category = verdict.category ? inlineUntrusted(verdict.category) : "";
|
|
84
94
|
const tracked = autoMode.denialTracking?.recordBlock();
|
|
@@ -107,7 +107,8 @@ export declare function pathRuleLaneAnswer(table: readonly PersistedRule[], req:
|
|
|
107
107
|
root: string | undefined;
|
|
108
108
|
sessionId: string | undefined;
|
|
109
109
|
liveCwd: string | undefined;
|
|
110
|
-
|
|
110
|
+
home: string | undefined;
|
|
111
|
+
}): PersistedRuleHit | PersistedRuleUnreadable | undefined;
|
|
111
112
|
/** The two lanes prepare hands the gate. Each is `undefined` when its partition is not configured, so
|
|
112
113
|
* a deployment without one keeps a decision path byte-identical to a build without the feature. */
|
|
113
114
|
export interface PermissionRuleLanes {
|
|
@@ -137,6 +138,11 @@ export declare function createPermissionRuleLanes(cfg: {
|
|
|
137
138
|
/** The LIVE tracked working directory (adversarial-review P1): the base a relative cd resolves against
|
|
138
139
|
* after an earlier observable `cd`; read per call. */
|
|
139
140
|
liveCwd: () => string | undefined;
|
|
141
|
+
/** #644 — the EXECUTION ENVIRONMENT's home directory (`ExecutionEnv.homeDir`, adapter-declared), the base a
|
|
142
|
+
* `~/` path rule resolves against. `undefined` = the adapter declares none: a `~/` deny/ask row then reads
|
|
143
|
+
* UNREADABLE for every call (a fail-closed ask), never the engine process's home. A declared value that is
|
|
144
|
+
* not an absolute path is refused HERE, loudly (a bad declaration is a wiring defect, not an absence). */
|
|
145
|
+
home: string | undefined;
|
|
140
146
|
/** True iff the reserved question-tool NAME resolves to the ENGINE's own content-ask tool on this leg. */
|
|
141
147
|
questionToolMounted: boolean;
|
|
142
148
|
questionToolName: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { persistedRuleMandateOf } from "../hooks.js";
|
|
2
2
|
import { adjudicatePersistedPathRules, adjudicatePersistedRules, ruleToolGrammarOf, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
|
|
3
|
+
import { isAbsolutePathForm } from "../../tools/fs/safety.js";
|
|
3
4
|
import { effectivePathTargetOf } from "../effective-path-target.js";
|
|
4
5
|
import { declaredPathTargetOf, protectivePathTargetOf } from "../tool-registry.js";
|
|
5
6
|
import { orgRuleVerdictFor } from "../permission-rule-org.js";
|
|
@@ -31,15 +32,20 @@ export function persistedRuleAnswerOf(verdict) {
|
|
|
31
32
|
return persistedRuleHitOf(verdict);
|
|
32
33
|
}
|
|
33
34
|
export function pathRuleLaneAnswer(table, req, ctx) {
|
|
34
|
-
const bases = { ...(ctx.root !== undefined ? { root: ctx.root } : {}), ...(ctx.liveCwd !== undefined ? { cwd: ctx.liveCwd } : {}) };
|
|
35
|
+
const bases = { ...(ctx.root !== undefined ? { root: ctx.root } : {}), ...(ctx.liveCwd !== undefined ? { cwd: ctx.liveCwd } : {}), ...(ctx.home !== undefined ? { home: ctx.home } : {}) };
|
|
35
36
|
const tighten = effectivePathTargetOf(req, protectivePathTargetOf(req), bases);
|
|
36
37
|
const allow = effectivePathTargetOf(req, declaredPathTargetOf(req), bases);
|
|
37
38
|
if (tighten === undefined && allow === undefined)
|
|
38
39
|
return undefined;
|
|
39
|
-
return
|
|
40
|
+
return persistedRuleAnswerOf(adjudicatePersistedPathRules(table, { tool: req.toolName, cwd: ctx.root, sessionId: ctx.sessionId }, { ...(tighten !== undefined ? { tighten } : {}), ...(allow !== undefined ? { allow } : {}) }, bases));
|
|
40
41
|
}
|
|
41
42
|
export function createPermissionRuleLanes(cfg) {
|
|
42
43
|
const provider = cfg.provider;
|
|
44
|
+
if (cfg.home !== undefined && !isAbsolutePathForm(cfg.home)) {
|
|
45
|
+
const e = new Error(`the execution environment declares homeDir ${JSON.stringify(cfg.home)}, which is not an absolute path — a \`~/\` permission rule could not be resolved against it; declare an absolute home directory or none`);
|
|
46
|
+
e.code = "config.execution_env_home_dir_invalid";
|
|
47
|
+
throw e;
|
|
48
|
+
}
|
|
43
49
|
if (cfg.localOwnerDeclared) {
|
|
44
50
|
if (provider === undefined || !provider.partitions.durable) {
|
|
45
51
|
throw new Error("RunnerDeps.localOwnerRules is declared but no durable permission-rule partition is wired — there is no bucket for the local owner to hold rules in; refusing rather than running as if the declaration were absent");
|
|
@@ -127,7 +133,7 @@ export function createPermissionRuleLanes(cfg) {
|
|
|
127
133
|
const table = view.rules;
|
|
128
134
|
const liveCwd = cfg.liveCwd();
|
|
129
135
|
if (grammar === "path") {
|
|
130
|
-
return pathRuleLaneAnswer(table, req, { root: cfg.root, sessionId: cfg.sessionId, liveCwd });
|
|
136
|
+
return pathRuleLaneAnswer(table, req, { root: cfg.root, sessionId: cfg.sessionId, liveCwd, home: cfg.home });
|
|
131
137
|
}
|
|
132
138
|
const command = req.args?.command;
|
|
133
139
|
if (typeof command !== "string")
|
|
@@ -49,7 +49,7 @@ export interface PrepareCapsAndWorkflowInput {
|
|
|
49
49
|
deps: RunnerDeps;
|
|
50
50
|
/** borrowed-readonly — the trusted spawn-side channel; the Workflow mount forwards `rootSessionId`, `placementRoot`,
|
|
51
51
|
* `onTaskNotification`, `workflowDepth`, and the capture-floor getter reads `memoryCaptureAncestors`. */
|
|
52
|
-
internals: Pick<RunInternals, "rootSessionId" | "placementRoot" | "onTaskNotification" | "workflowDepth" | "memoryCaptureAncestors"> | undefined;
|
|
52
|
+
internals: Pick<RunInternals, "rootSessionId" | "placementRoot" | "onTaskNotification" | "workflowDepth" | "memoryCaptureAncestors" | "workflowParkedResume" | "autoModeBreakerLedger"> | undefined;
|
|
53
53
|
/** borrowed-readonly — the acquired session; the classifier leg rebuilds its transcript window from `buildContext`. */
|
|
54
54
|
session: Pick<StoredSession, "buildContext">;
|
|
55
55
|
/** borrowed-readonly — the acquired session id (operator-line context, the refusal codes' phase record). */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isSyntheticApiErrorMessage } from "../../internal/harness.js";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { forkGovernanceDenial } from "../../agents/subagent.js";
|
|
3
|
+
import { childThinkingSeat, deploymentSubagentThinking, deploymentSubagentTierModel, forkGovernanceDenial } from "../../agents/subagent.js";
|
|
4
4
|
import { CROSS_SESSION_CLASSIFIER_RULE } from "../../agents/cross-session-envelope.js";
|
|
5
5
|
import { createRunWorkflowTool } from "../../orchestration/run-workflow-tool.js";
|
|
6
6
|
import { isSelfOrchestrationActive } from "../../orchestration/workflow-script-runner.js";
|
|
@@ -170,7 +170,10 @@ export async function prepareCapsAndWorkflow(input) {
|
|
|
170
170
|
autoModeDecider = createAutoModeDecider({
|
|
171
171
|
...(am.timeoutMs !== undefined ? { timeoutMs: am.timeoutMs } : {}),
|
|
172
172
|
...(am.failureThreshold !== undefined ? { failureThreshold: am.failureThreshold } : {}),
|
|
173
|
-
|
|
173
|
+
onBreakerOpen: (info) => {
|
|
174
|
+
internals?.autoModeBreakerLedger?.record(sessionId, { openedAtMs: Date.now(), lastCause: info.lastCause, failures: info.consecutiveFailures, runId });
|
|
175
|
+
am.onBreakerOpen?.(info);
|
|
176
|
+
},
|
|
174
177
|
onClassified: (info) => emitTrace(deps.tracer, () => ({
|
|
175
178
|
kind: "auto_mode.classified",
|
|
176
179
|
version: 1,
|
|
@@ -243,9 +246,16 @@ export async function prepareCapsAndWorkflow(input) {
|
|
|
243
246
|
taskOwner: hostTaskId,
|
|
244
247
|
limits: deps.workflowLimits,
|
|
245
248
|
sourceTaskId: hostTaskId,
|
|
246
|
-
parentModel: () => harnessRef.current?.getModel(),
|
|
249
|
+
parentModel: () => deploymentSubagentTierModel(runnerSelf.agentCatalog) ?? harnessRef.current?.getModel(),
|
|
247
250
|
...(spec.getApiKeyAndHeaders !== undefined ? { parentGetApiKeyAndHeaders: spec.getApiKeyAndHeaders } : {}),
|
|
248
251
|
...(frozenOnAsk !== undefined ? { parentOnAsk: frozenOnAsk } : {}),
|
|
252
|
+
...(spec.durableApproval !== undefined ? { parentDurableApproval: { ...spec.durableApproval } } : {}),
|
|
253
|
+
...(internals?.workflowParkedResume !== undefined
|
|
254
|
+
? { parkedResume: (resumeFromRunId) => {
|
|
255
|
+
const mine = internals.workflowParkedResume.filter((d) => d.runId === resumeFromRunId).map(({ runId: _r, ...rest }) => rest);
|
|
256
|
+
return mine.length > 0 ? mine : undefined;
|
|
257
|
+
} }
|
|
258
|
+
: {}),
|
|
249
259
|
principal: spec.principal,
|
|
250
260
|
oneShot: spec.oneShot,
|
|
251
261
|
...(resolvedInteractionPosture !== undefined ? { parentInteractionPosture: resolvedInteractionPosture } : {}),
|
|
@@ -258,7 +268,7 @@ export async function prepareCapsAndWorkflow(input) {
|
|
|
258
268
|
autoModeReview: () => (autoModeDecider !== undefined ? { decider: autoModeDecider } : undefined),
|
|
259
269
|
workflowDepth: internals?.workflowDepth,
|
|
260
270
|
parentCwd: taskRootFinal,
|
|
261
|
-
parentThinking: () => harnessRef.current?.getThinkingLevel() ?? thinking,
|
|
271
|
+
parentThinking: () => childThinkingSeat(undefined, deploymentSubagentThinking(runnerSelf.agentCatalog?.roles), harnessRef.current?.getThinkingLevel() ?? thinking).thinking,
|
|
262
272
|
parentReadFace: () => carrierReadFace(),
|
|
263
273
|
parentReadDenyPatterns: () => {
|
|
264
274
|
const readDenyAdditionsNormalized = input.readDenyAdditionsNormalized();
|
|
@@ -49,8 +49,9 @@ export interface PrepareGateStationsInput {
|
|
|
49
49
|
* first-party tool's declared effect (never overrides a scanned one); the plan-mode deny reads it per call. */
|
|
50
50
|
toolEffects: Map<string, ToolEffect>;
|
|
51
51
|
/** borrowed-readonly — the deployment seats these stations read, as a Pick over the SAME `deps` object (receiver
|
|
52
|
-
* preserved for `deps.onError?.()`): the write-protection table and its data root, the tracer, the error face
|
|
53
|
-
|
|
52
|
+
* preserved for `deps.onError?.()`): the write-protection table and its data root, the tracer, the error face, and
|
|
53
|
+
* the notice sink (#648: the gate's unresolvable-ask exit delivers `delegation.ask_unresolvable` through it). */
|
|
54
|
+
deps: Pick<RunnerDeps, "writeProtectedPaths" | "memoryEngineDir" | "tracer" | "onError" | "onNotice">;
|
|
54
55
|
/** borrowed-mutable — the protocol-tools phase's arming belt. Writer here: `armed` ← the registration predicate, the
|
|
55
56
|
* one write; the refresh seam reads it. */
|
|
56
57
|
toolCallGateArmedRef: {
|
|
@@ -5,6 +5,8 @@ import { PRESENT_PLAN_TOOL_NAME } from "../present-plan-tool.js";
|
|
|
5
5
|
import { screenGateOutcome } from "../gate-outcome.js";
|
|
6
6
|
import { directDeny } from "./gate-exit.js";
|
|
7
7
|
import { emitTrace } from "../trace.js";
|
|
8
|
+
import { deliverEngineNotice } from "../engine-notice.js";
|
|
9
|
+
import { askUnresolvableNotice } from "../ask-unresolvable-notice.js";
|
|
8
10
|
import { createWriteProtectionCheck } from "../write-protect.js";
|
|
9
11
|
import { PATH_CONFINABLE_WRITE_TOOLS } from "./session-rule-policy.js";
|
|
10
12
|
import { toolCallFaceOf } from "../tool-roster.js";
|
|
@@ -120,6 +122,7 @@ export function prepareGateStations(input) {
|
|
|
120
122
|
shellGated: (e.toolName === "Bash" && shellGatedBash) || (e.toolName === "Monitor" && shellGatedMonitor),
|
|
121
123
|
...(autoModeDecider ? { autoMode: { decider: autoModeDecider, ...(autoModeDenialTracking !== undefined ? { denialTracking: autoModeDenialTracking } : {}) } } : {}),
|
|
122
124
|
onHeadlessDenialLimit: stopForDenialLimit,
|
|
125
|
+
onAskUnresolvable: (info) => deliverEngineNotice(deps.onNotice, askUnresolvableNotice({ sessionId, ...info })),
|
|
123
126
|
...(permissionRuleLane
|
|
124
127
|
? {
|
|
125
128
|
persistedRules: {
|
|
@@ -252,6 +252,7 @@ export async function preparePolicyChain(input) {
|
|
|
252
252
|
sessionId,
|
|
253
253
|
root: taskRootFinal,
|
|
254
254
|
liveCwd: () => handsCwdRef?.current,
|
|
255
|
+
home: executionEnv.homeDir,
|
|
255
256
|
questionToolMounted,
|
|
256
257
|
questionToolName: ASK_USER_QUESTION_TOOL_NAME,
|
|
257
258
|
onRevisionDefect: (message) => deps.onError?.(new Error(message), { phase: "config", sessionId }),
|
|
@@ -299,7 +300,7 @@ export async function preparePolicyChain(input) {
|
|
|
299
300
|
return { action: "allow", updatedInput: editArgs };
|
|
300
301
|
if (judged.kind === "deny")
|
|
301
302
|
return judged.result;
|
|
302
|
-
const { ask: editAsk, fallback: editFallback, mintedHere: editMintedHere
|
|
303
|
+
const { ask: editAsk, fallback: editFallback, mintedHere: editMintedHere } = judged;
|
|
303
304
|
re = editAsk;
|
|
304
305
|
const rr = await resolveAsk({
|
|
305
306
|
toolName: creq.toolName,
|
|
@@ -311,7 +312,7 @@ export async function preparePolicyChain(input) {
|
|
|
311
312
|
...riskAxesOf(creq.toolName),
|
|
312
313
|
...(re.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
313
314
|
...(re.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: re.persistedRuleShadowed } : {}),
|
|
314
|
-
...inheritedAskCarry(
|
|
315
|
+
...inheritedAskCarry(judged, isLiveApproverSeat(onAskOf), ancestorTracker),
|
|
315
316
|
ruleEvidence: inheritedAskEvidence,
|
|
316
317
|
}, onAskOf, csignal ?? abortController.signal, lateAskSettlementObserver({ toolName: creq.toolName, toolCallId: creq.toolCallId, sessionId, runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), onNotice: deps.onNotice, onError: deps.onError }));
|
|
317
318
|
settleDenialLimitFallback({ fallback: editFallback, mintedHere: editMintedHere, tracker: ancestorTracker, resolved: rr, headless: headlessDenyAtRecheck, stop: stopForDenialLimit, toolName: creq.toolName, toolCallId: creq.toolCallId });
|
|
@@ -372,7 +373,7 @@ export async function preparePolicyChain(input) {
|
|
|
372
373
|
return { action: "allow" };
|
|
373
374
|
if (judged.kind === "deny")
|
|
374
375
|
return judged.result;
|
|
375
|
-
const { ask: inheritedAsk, fallback, mintedHere
|
|
376
|
+
const { ask: inheritedAsk, fallback, mintedHere } = judged;
|
|
376
377
|
if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq.toolName)) {
|
|
377
378
|
recordAncestorSandboxAdmission(creq.toolCallId, creq.toolName);
|
|
378
379
|
return { action: "allow" };
|
|
@@ -389,7 +390,7 @@ export async function preparePolicyChain(input) {
|
|
|
389
390
|
...riskAxesOf(creq.toolName),
|
|
390
391
|
...(inheritedAsk.action === "ask" && inheritedAsk.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
391
392
|
...(inheritedAsk.action === "ask" && inheritedAsk.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: inheritedAsk.persistedRuleShadowed } : {}),
|
|
392
|
-
...inheritedAskCarry(
|
|
393
|
+
...inheritedAskCarry(judged, isLiveApproverSeat(pc.onAsk), pc.autoMode?.denialTracking),
|
|
393
394
|
ruleEvidence: inheritedAskEvidence,
|
|
394
395
|
}, pc.onAsk, csignal ?? abortController.signal, lateAskSettlementObserver({ toolName: creq.toolName, toolCallId: creq.toolCallId, sessionId, runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), onNotice: deps.onNotice, onError: deps.onError }));
|
|
395
396
|
const askWaitMs = Math.max(0, now() - askT0);
|
|
@@ -452,7 +453,7 @@ export async function preparePolicyChain(input) {
|
|
|
452
453
|
return decision.updatedInput !== undefined ? { action: "allow", updatedInput: decision.updatedInput } : { action: "allow" };
|
|
453
454
|
if (judged.kind === "deny")
|
|
454
455
|
return judged.result;
|
|
455
|
-
const { ask: inheritedAsk, fallback, mintedHere
|
|
456
|
+
const { ask: inheritedAsk, fallback, mintedHere } = judged;
|
|
456
457
|
if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq.toolName)) {
|
|
457
458
|
recordAncestorSandboxAdmission(creq.toolCallId, creq.toolName);
|
|
458
459
|
return decision.updatedInput !== undefined ? { action: "allow", updatedInput: decision.updatedInput } : { action: "allow" };
|
|
@@ -468,7 +469,7 @@ export async function preparePolicyChain(input) {
|
|
|
468
469
|
...riskAxesOf(creq.toolName),
|
|
469
470
|
...(inheritedAsk.action === "ask" && inheritedAsk.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
470
471
|
...(inheritedAsk.action === "ask" && inheritedAsk.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: inheritedAsk.persistedRuleShadowed } : {}),
|
|
471
|
-
...inheritedAskCarry(
|
|
472
|
+
...inheritedAskCarry(judged, isLiveApproverSeat(pc.onAsk), pc.autoMode?.denialTracking),
|
|
472
473
|
ruleEvidence: inheritedAskEvidence,
|
|
473
474
|
}, pc.onAsk, csignal ?? abortController.signal, lateAskSettlementObserver({ toolName: creq.toolName, toolCallId: creq.toolCallId, sessionId, runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), onNotice: deps.onNotice, onError: deps.onError }));
|
|
474
475
|
const askWaitMs = Math.max(0, now() - askT0);
|
|
@@ -46,7 +46,7 @@ export interface PrepareWiringManifestInput {
|
|
|
46
46
|
deps: RunnerDeps;
|
|
47
47
|
/** borrowed-readonly — the trusted spawn-side channel: `questionFaceStripped`, `isDelegatedChild`, `onTaskNotification`,
|
|
48
48
|
* `peerSelfRef`, `explicitAgentName` (the drain), `insideFork` / `agentName` / `parentToolCallId` (the identity). */
|
|
49
|
-
internals: Pick<RunInternals, "questionFaceStripped" | "isDelegatedChild" | "onTaskNotification" | "peerSelfRef" | "explicitAgentName" | "insideFork" | "agentName" | "parentToolCallId"> | undefined;
|
|
49
|
+
internals: Pick<RunInternals, "questionFaceStripped" | "isDelegatedChild" | "onTaskNotification" | "peerSelfRef" | "explicitAgentName" | "insideFork" | "agentName" | "parentToolCallId" | "autoModeBreakerLedger"> | undefined;
|
|
50
50
|
/** borrowed-readonly — the resume leg, or undefined (the manifest's leg derivation reads presence only). */
|
|
51
51
|
resume: Pick<PrepareResume, "seed"> | undefined;
|
|
52
52
|
/** borrowed-readonly — the acquired session id. */
|
|
@@ -199,7 +199,14 @@ export function prepareWiringManifest(input) {
|
|
|
199
199
|
memoryAdmissionWired: deps.memoryScopeAdmission !== undefined,
|
|
200
200
|
retentionPolicyWired: deps.retentionPolicy !== undefined,
|
|
201
201
|
...(modelGateManifest !== undefined ? { modelGate: modelGateManifest } : {}),
|
|
202
|
-
autoMode: {
|
|
202
|
+
autoMode: {
|
|
203
|
+
armed: autoModeArmReason === "armed",
|
|
204
|
+
reason: autoModeArmReason,
|
|
205
|
+
...(() => {
|
|
206
|
+
const trip = internals?.autoModeBreakerLedger?.lastTrip(sessionId);
|
|
207
|
+
return trip !== undefined ? { breaker: trip } : {};
|
|
208
|
+
})(),
|
|
209
|
+
},
|
|
203
210
|
mcp: mcpManifestEntries(lockedPreflight.mcp, mcp.statuses),
|
|
204
211
|
tools: toolRoster,
|
|
205
212
|
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { type Model } from "../../internal/llm.js";
|
|
2
2
|
import { type CheckpointToken, type ResumeOutcome } from "../checkpoint-store.js";
|
|
3
3
|
import { type TaskOutcome } from "../task-outcome.js";
|
|
4
|
+
import { type SwappableDeps } from "../swappable-deps.js";
|
|
4
5
|
import { type SideQuerySpec, type SideQueryResult } from "../side-query.js";
|
|
5
6
|
import type { SessionStore } from "../session.js";
|
|
6
|
-
import type { AgentDefinition,
|
|
7
|
+
import type { AgentDefinition, ModelRoles, RunnerDeps, TaskEvent, TaskResult, TaskSpec, TaskStream } from "../types.js";
|
|
7
8
|
import type { ResumeRun, ResumeTaskConfig, RunInternals, RunnerSelfSeat } from "./contracts.js";
|
|
8
9
|
export type { ResumeTaskConfig } from "./contracts.js";
|
|
9
10
|
/**
|
|
@@ -20,6 +21,9 @@ export declare class Runner implements RunnerSelfSeat {
|
|
|
20
21
|
* authority: a durable resume's checkpoint seed wins on its leg, and a miss (cross-process,
|
|
21
22
|
* evicted) falls back to the transcript-replay rung. See {@link SessionReadFileStates}. */
|
|
22
23
|
private readonly sessionReadStates;
|
|
24
|
+
/** #616 — the per-session breaker read face's record (see {@link RunInternals.autoModeBreakerLedger}). Runner-lived:
|
|
25
|
+
* a trip in one leg is what the session's next leg reports. */
|
|
26
|
+
private readonly autoModeBreakerLedger;
|
|
23
27
|
/** task notifications born BETWEEN turns (run torn down / harness already idle), parked
|
|
24
28
|
* per session and drained into the session's next run at its first boundary. Runner-lived (outlives any
|
|
25
29
|
* single run, like the registry handles that produce into it); bounded + drop-disclosing, see
|
|
@@ -57,7 +61,7 @@ export declare class Runner implements RunnerSelfSeat {
|
|
|
57
61
|
private readonly locallyClaimedTokens;
|
|
58
62
|
private static readonly PARENT_CONSTRAINT_REGISTRY_CAP;
|
|
59
63
|
/** The lanes' LIVE view of `deps` ({@link RunnerDepsSeat}): a getter, never a captured object, because
|
|
60
|
-
* `
|
|
64
|
+
* `swapDeps` replaces the object and a rung reading after its own await must see the replacement. */
|
|
61
65
|
private readonly depsSeat;
|
|
62
66
|
constructor(deps: RunnerDeps);
|
|
63
67
|
/** design/73 §1 v1 — fire `RunnerDeps.onTaskOutcome` through the single swallow-guarded chokepoint
|
|
@@ -74,6 +78,7 @@ export declare class Runner implements RunnerSelfSeat {
|
|
|
74
78
|
agents?: AgentDefinition[];
|
|
75
79
|
builtinAgents?: boolean;
|
|
76
80
|
models?: Record<string, Model>;
|
|
81
|
+
roles?: ModelRoles;
|
|
77
82
|
};
|
|
78
83
|
/** design/125 (实现审 B1) — the deployment-level gating baseline, exposed READ-ONLY so thin
|
|
79
84
|
* assemblers over the Runner (`runSpec`) can COMPOSE with it. The task-level engine semantic is a
|
|
@@ -87,38 +92,35 @@ export declare class Runner implements RunnerSelfSeat {
|
|
|
87
92
|
/** Acquire the lock for a sessionId; returns a release fn. New (undefined) sessions need no lock. */
|
|
88
93
|
private acquireSessionLock;
|
|
89
94
|
/**
|
|
90
|
-
* Hot-swap the
|
|
91
|
-
*
|
|
92
|
-
*
|
|
95
|
+
* Hot-swap the deployment seats a running Runner may change without a restart — the ONE door for every
|
|
96
|
+
* hot-swappable seat ({@link SWAPPABLE_DEP_SEATS}: the model catalog generation `models` + its `tiers`
|
|
97
|
+
* bindings, and the deployment's read-face declaration `readFace`), and the ONLY sanctioned way to
|
|
98
|
+
* change any of them after construction. It replaces the former `swapModels` verb (retired, no alias):
|
|
99
|
+
* one verb per seat restated the same laws with a list of verbs as the only record of what is swappable.
|
|
93
100
|
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* -
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* the notice directory) for same-name entries whose URL moved while the Model-visible
|
|
116
|
-
* credential fingerprint did not.
|
|
101
|
+
* The laws, stated once (see the vocabulary module for the seat-by-seat meaning):
|
|
102
|
+
* - **Screened first, written once — atomic.** The candidate is screened whole
|
|
103
|
+
* ({@link screenSwappableDeps}: the key set is the closed one, at least one seat, `tiers` rides with
|
|
104
|
+
* `models`, each seat's own value gate — `readFace` through the very `assertReadFaceValue` every
|
|
105
|
+
* prepare door applies) and the catalog is tier-expanded on the candidate (an illegal binding throws
|
|
106
|
+
* exactly like the constructor's boot error) BEFORE the deps object is replaced. On any throw the
|
|
107
|
+
* current deps stay in force, byte-identical.
|
|
108
|
+
* - **In-flight legs keep their snapshot.** A running task read `this.deps` at its prepare and holds
|
|
109
|
+
* that object: its `Model`, its resolved read face, everything. The swap changes what FUTURE prepares
|
|
110
|
+
* (and auto-sourced agents' string refs — {@link agentCatalog} copies per read) resolve. Same-name
|
|
111
|
+
* redirects never re-route or re-price a task mid-run; a read-face change never re-fences a mounted
|
|
112
|
+
* band; the divergence window is exactly the in-flight legs' lifetime, by design.
|
|
113
|
+
* - **Presence is the instruction.** A key present on the candidate is replaced (an explicit
|
|
114
|
+
* `undefined` clears an optional seat: `tiers: undefined` ⇒ no bindings, `readFace: undefined` ⇒ the
|
|
115
|
+
* engine default); a key absent is kept. `tiers` omitted beside `models` ⇒ the current bindings are
|
|
116
|
+
* re-applied over the new catalog.
|
|
117
|
+
* - **Announced per seat.** `config.models_swapped` (models/tiers counts — key material only, never the
|
|
118
|
+
* catalog itself; plus the advisory `route.base_url_changed_key_unchanged` for same-name entries
|
|
119
|
+
* whose URL moved while the Model-visible credential fingerprint did not) and
|
|
120
|
+
* `config.read_face_swapped` (`{ from, to, occurrenceId }`, the seat's previous and new value, `null`
|
|
121
|
+
* = no declaration) — the table {@link SWAP_SEAT_NOTICE} binds every seat to its line.
|
|
117
122
|
*/
|
|
118
|
-
|
|
119
|
-
models: Record<string, Model>;
|
|
120
|
-
tiers?: Record<string, ModelRef>;
|
|
121
|
-
}): void;
|
|
123
|
+
swapDeps(next: SwappableDeps): void;
|
|
122
124
|
/** one-shot brain-routed utility query (see {@link runSideQuery} for the full contract):
|
|
123
125
|
* preserves system/multi-turn messages/tool DEFINITIONS, routes through the deployment's brain with
|
|
124
126
|
* the same model resolution as tasks, returns real usage/model. No session, no tool execution, no
|
|
@@ -23,6 +23,8 @@ import { resolveReasoning } from "../../brain/reasoning.js";
|
|
|
23
23
|
import { adjudicateDerivedRoute, authCarrierFingerprint, fallbackToPrimaryNotice, normalizeBaseUrl, sameRouteIdentity } from "../../brain/route-adjudicator.js";
|
|
24
24
|
import { runWithBrainTelemetry, runWithReasoningWireFacts, runWithStatusSink } from "../../brain/status-sink.js";
|
|
25
25
|
import { expandTiers, resolveModel, resolveTaskModel } from "../roles.js";
|
|
26
|
+
import { screenSwappableDeps } from "../swappable-deps.js";
|
|
27
|
+
import { AutoModeBreakerLedger } from "../auto-mode.js";
|
|
26
28
|
import { runSideQuery } from "../side-query.js";
|
|
27
29
|
import { generatePromptSuggestions } from "./prompt-suggestions.js";
|
|
28
30
|
import { PushQueue } from "../push-queue.js";
|
|
@@ -82,6 +84,7 @@ export class Runner {
|
|
|
82
84
|
sessions;
|
|
83
85
|
sessionLocks = new Map();
|
|
84
86
|
sessionReadStates = new SessionReadFileStates();
|
|
87
|
+
autoModeBreakerLedger = new AutoModeBreakerLedger();
|
|
85
88
|
pendingSessionNotifications = new PendingSessionNotifications();
|
|
86
89
|
parentConstraintRegistry = new Map();
|
|
87
90
|
suspendedEnvReaps = new Map();
|
|
@@ -138,7 +141,7 @@ export class Runner {
|
|
|
138
141
|
return {
|
|
139
142
|
...(this.deps.agents ? { agents: [...this.deps.agents] } : {}),
|
|
140
143
|
...(this.deps.builtinAgents !== undefined ? { builtinAgents: this.deps.builtinAgents } : {}),
|
|
141
|
-
...(this.deps.models ? { models: { ...this.deps.models } } : {}),
|
|
144
|
+
...(this.deps.models ? { models: { ...this.deps.models } } : {}), ...(this.deps.roles ? { roles: { ...this.deps.roles } } : {}),
|
|
142
145
|
};
|
|
143
146
|
}
|
|
144
147
|
get gateBaseline() {
|
|
@@ -213,41 +216,67 @@ export class Runner {
|
|
|
213
216
|
});
|
|
214
217
|
};
|
|
215
218
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
219
|
+
swapDeps(next) {
|
|
220
|
+
const screened = screenSwappableDeps(next);
|
|
221
|
+
const { seats, candidate } = screened;
|
|
222
|
+
const catalogSwap = seats.includes("models");
|
|
223
|
+
let expanded;
|
|
224
|
+
let tiers;
|
|
222
225
|
const movedEntries = [];
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
226
|
+
if (catalogSwap) {
|
|
227
|
+
const models = candidate.models;
|
|
228
|
+
tiers = Object.hasOwn(candidate, "tiers") ? candidate.tiers : this.deps.tiers;
|
|
229
|
+
expanded = tiers && Object.keys(tiers).length > 0 ? expandTiers({ ...models }, tiers) : { ...models };
|
|
230
|
+
for (const [name, nextModel] of Object.entries(expanded ?? {})) {
|
|
231
|
+
const prior = this.deps.models?.[name];
|
|
232
|
+
if (!prior)
|
|
233
|
+
continue;
|
|
234
|
+
const from = normalizeBaseUrl(prior.baseUrl);
|
|
235
|
+
const to = normalizeBaseUrl(nextModel.baseUrl);
|
|
236
|
+
if (from !== to && authCarrierFingerprint(prior.headers) === authCarrierFingerprint(nextModel.headers)) {
|
|
237
|
+
movedEntries.push({ modelId: name, from, to });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const faceSwap = seats.includes("readFace");
|
|
242
|
+
const priorFace = this.deps.readFace;
|
|
243
|
+
const written = {
|
|
244
|
+
...this.deps,
|
|
245
|
+
...(expanded !== undefined ? { models: expanded } : {}),
|
|
246
|
+
...(catalogSwap && tiers !== undefined ? { tiers } : {}),
|
|
247
|
+
...(faceSwap && candidate.readFace !== undefined ? { readFace: candidate.readFace } : {}),
|
|
248
|
+
};
|
|
249
|
+
if (catalogSwap && tiers === undefined)
|
|
250
|
+
delete written.tiers;
|
|
251
|
+
if (faceSwap && candidate.readFace === undefined)
|
|
252
|
+
delete written.readFace;
|
|
253
|
+
this.deps = written;
|
|
254
|
+
if (catalogSwap) {
|
|
255
|
+
const modelCount = Object.keys(candidate.models).length;
|
|
256
|
+
deliverEngineNotice(this.deps.onNotice, {
|
|
257
|
+
code: "config.models_swapped",
|
|
258
|
+
message: `model catalog swapped: ${modelCount} model(s), ${tiers ? Object.keys(tiers).length : 0} tier binding(s); in-flight tasks finish on their resolved models, new tasks resolve against the new catalog`,
|
|
259
|
+
detail: { models: modelCount, tiers: tiers ? Object.keys(tiers).length : 0, occurrenceId: uuidv7() },
|
|
260
|
+
});
|
|
261
|
+
if (movedEntries.length > 0) {
|
|
262
|
+
const RENDER_CAP = 8;
|
|
263
|
+
const rendered = movedEntries.slice(0, RENDER_CAP);
|
|
264
|
+
deliverEngineNotice(this.deps.onNotice, {
|
|
265
|
+
code: "route.base_url_changed_key_unchanged",
|
|
266
|
+
message: `model catalog swap moved ${movedEntries.length} entry/entries to a new baseUrl while the Model-visible credential half stayed unchanged: ` +
|
|
267
|
+
rendered.map((e) => `"${e.modelId}" ${e.from || "(config root)"} → ${e.to || "(config root)"}`).join("; ") +
|
|
268
|
+
(movedEntries.length > rendered.length ? `; +${movedEntries.length - rendered.length} more` : "") +
|
|
269
|
+
" — if the provider changed (not just its domain), update the credential reference in the same step",
|
|
270
|
+
detail: { entries: rendered, total: movedEntries.length, occurrenceId: uuidv7() },
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (faceSwap) {
|
|
275
|
+
const to = candidate.readFace ?? null;
|
|
244
276
|
deliverEngineNotice(this.deps.onNotice, {
|
|
245
|
-
code: "
|
|
246
|
-
message: `
|
|
247
|
-
|
|
248
|
-
(movedEntries.length > rendered.length ? `; +${movedEntries.length - rendered.length} more` : "") +
|
|
249
|
-
" — if the provider changed (not just its domain), update the credential reference in the same step",
|
|
250
|
-
detail: { entries: rendered, total: movedEntries.length, occurrenceId: uuidv7() },
|
|
277
|
+
code: "config.read_face_swapped",
|
|
278
|
+
message: `deployment read face swapped: ${priorFace ?? "(no declaration)"} → ${to ?? "(no declaration)"}; in-flight legs keep the face they prepared under, new legs resolve against the new declaration`,
|
|
279
|
+
detail: { from: priorFace ?? null, to, occurrenceId: uuidv7() },
|
|
251
280
|
});
|
|
252
281
|
}
|
|
253
282
|
}
|
|
@@ -962,6 +991,7 @@ export class Runner {
|
|
|
962
991
|
peerInboundChainRef,
|
|
963
992
|
detachHub,
|
|
964
993
|
sessionReadStates: this.sessionReadStates,
|
|
994
|
+
autoModeBreakerLedger: this.autoModeBreakerLedger,
|
|
965
995
|
onTaskNotification: (notification, opts) => {
|
|
966
996
|
try {
|
|
967
997
|
upstreamTaskNotification?.(notification, opts);
|
|
@@ -400,6 +400,11 @@ export interface RunnerDeps {
|
|
|
400
400
|
* may not. Beside a read-only (verifier) mount this seat's "open" silently CLAMPS to roots (a
|
|
401
401
|
* deployment default cannot override a load-bearing containment wall) — TaskSpec.readFace's own
|
|
402
402
|
* "open" there is the genuine per-task contradiction, and that one still refuses loudly (#123).
|
|
403
|
+
*
|
|
404
|
+
* A LIVE seat: `Runner.swapDeps({ readFace })` replaces it without a restart (the same natural
|
|
405
|
+
* snapshot the model catalog has — a leg reads the seat at its prepare and keeps that reading; every
|
|
406
|
+
* later prepare reads the new declaration; announced as `config.read_face_swapped`). A host whose
|
|
407
|
+
* declaration arrives late (a governance source read after boot) swaps it in rather than restarting.
|
|
403
408
|
*/
|
|
404
409
|
readFace?: import("../tools/fs/read-face.js").ReadFace;
|
|
405
410
|
/**
|
|
@@ -686,10 +691,12 @@ export interface RunnerDeps {
|
|
|
686
691
|
timeoutMs?: number;
|
|
687
692
|
/** Consecutive-failure threshold opening the one-way session breaker (default 3). */
|
|
688
693
|
failureThreshold?: number;
|
|
689
|
-
/** Fired ONCE when the session breaker opens (the "退回非 auto" alarm — surface it to the operator).
|
|
694
|
+
/** Fired ONCE when the session breaker opens (the "退回非 auto" alarm — surface it to the operator). The engine
|
|
695
|
+
* records the trip on the session's breaker read face (`WiringManifest.autoMode.breaker`, #616) BEFORE calling
|
|
696
|
+
* this hook, so a deployment that wires nothing here still gets the read face. */
|
|
690
697
|
onBreakerOpen?: (info: {
|
|
691
698
|
consecutiveFailures: number;
|
|
692
|
-
lastCause:
|
|
699
|
+
lastCause: import("./auto-mode.js").AutoModeBreakerCause;
|
|
693
700
|
}) => void;
|
|
694
701
|
/**
|
|
695
702
|
* The classifier DENIAL LIMIT (CC 2.1.250 `FO`/`AKe`): a run whose classifier keeps blocking falls
|