@sema-agent/core 7.9.2 → 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 +38 -0
- package/dist/agents/child-model-seat.d.ts +31 -1
- package/dist/agents/child-model-seat.js +12 -0
- package/dist/agents/subagent.d.ts +2 -2
- package/dist/agents/subagent.js +6 -4
- 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/runner/contracts.d.ts +26 -2
- 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 +13 -3
- 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 +33 -32
- package/dist/core/runner/runtask.js +63 -33
- 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 +295 -44
- package/dist/stores/file/workflow-journal-store.js +10 -3
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +51 -5
|
@@ -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
|
|
@@ -88,38 +92,35 @@ export declare class Runner implements RunnerSelfSeat {
|
|
|
88
92
|
/** Acquire the lock for a sessionId; returns a release fn. New (undefined) sessions need no lock. */
|
|
89
93
|
private acquireSessionLock;
|
|
90
94
|
/**
|
|
91
|
-
* Hot-swap the
|
|
92
|
-
*
|
|
93
|
-
*
|
|
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.
|
|
94
100
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* -
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* the notice directory) for same-name entries whose URL moved while the Model-visible
|
|
117
|
-
* 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.
|
|
118
122
|
*/
|
|
119
|
-
|
|
120
|
-
models: Record<string, Model>;
|
|
121
|
-
tiers?: Record<string, ModelRef>;
|
|
122
|
-
}): void;
|
|
123
|
+
swapDeps(next: SwappableDeps): void;
|
|
123
124
|
/** one-shot brain-routed utility query (see {@link runSideQuery} for the full contract):
|
|
124
125
|
* preserves system/multi-turn messages/tool DEFINITIONS, routes through the deployment's brain with
|
|
125
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();
|
|
@@ -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
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deployment seats a Runner may HOT-SWAP after construction — the vocabulary behind the ONE door,
|
|
3
|
+
* `Runner.swapDeps` (runtask.ts).
|
|
4
|
+
*
|
|
5
|
+
* WHY ONE DOOR. Each hot-swappable seat used to be a verb of its own (`swapModels` was the first), and a
|
|
6
|
+
* verb per seat restates the same three laws every time — validate the candidate BEFORE anything is
|
|
7
|
+
* written, write the deps object ONCE (a swap is atomic: a refused candidate leaves the current deps
|
|
8
|
+
* byte-identical), announce the change under a notice code — while the question "which seats may be
|
|
9
|
+
* swapped at all" was answered by the list of verbs, i.e. by nothing a compiler could see. With one door
|
|
10
|
+
* the laws are stated once, the swappable set is THIS closed list, and every seat carries its rows in
|
|
11
|
+
* the tables beside it: the compiler refuses a member without its rows ({@link AssertAllKeysHandled}),
|
|
12
|
+
* the door refuses a key outside the set at run time (never a silent ignore — a hot-config driver
|
|
13
|
+
* handing `{ brain }` must learn that the brain is not a swappable seat, not watch nothing happen).
|
|
14
|
+
*
|
|
15
|
+
* WHAT A SWAP MEANS — the natural snapshot, one sentence for every seat: a running task reads the deps
|
|
16
|
+
* object it was handed at its prepare and holds that reference; a swap replaces the Runner's object, so
|
|
17
|
+
* every LATER prepare reads the new seat and every in-flight leg keeps the value it prepared under.
|
|
18
|
+
* Nothing is re-routed, re-priced or re-fenced mid-run; the divergence window is exactly the in-flight
|
|
19
|
+
* legs' lifetime, by design (the `swapModels` precedent, now the rule for the whole set).
|
|
20
|
+
*
|
|
21
|
+
* KEY PRESENCE IS THE INSTRUCTION: a key present on the candidate is REPLACED (an explicit `undefined`
|
|
22
|
+
* CLEARS an optional seat back to its engine default — `readFace: undefined` ⇒ the deployment declares
|
|
23
|
+
* no read face, `tiers: undefined` ⇒ no tier bindings); a key absent is KEPT. `Object.hasOwn`, never
|
|
24
|
+
* truthiness, so the two spellings cannot be confused. An EMPTY candidate is refused: a swap that names
|
|
25
|
+
* no seat is a caller defect, not a no-op.
|
|
26
|
+
*/
|
|
27
|
+
import type { AssertAllKeysHandled } from "./ask-origin.js";
|
|
28
|
+
import type { RunnerDeps } from "./runner-deps.js";
|
|
29
|
+
/**
|
|
30
|
+
* The closed set of hot-swappable deployment seats.
|
|
31
|
+
* - `models` — the model catalog generation (the same-name redirect, re-pricing and retirement rules
|
|
32
|
+
* of the former `swapModels`: a string ref resolves against the new catalog at the next
|
|
33
|
+
* prepare; a `Model` object a leg already holds is untouched).
|
|
34
|
+
* - `tiers` — the tier bindings, a BINDING OVER a catalog: it may only be swapped together with
|
|
35
|
+
* `models` (the door refuses `tiers` alone — re-expanding bindings over an already
|
|
36
|
+
* expanded catalog would keep the retired tier words as ordinary keys, which is exactly
|
|
37
|
+
* the stale-generation shape the swap exists to end). Omitted beside `models` ⇒ the
|
|
38
|
+
* current bindings are re-applied over the new catalog; present (including `undefined`)
|
|
39
|
+
* ⇒ replaced.
|
|
40
|
+
* - `readFace` — the deployment's read-face declaration ({@link RunnerDeps.readFace}); screened by the
|
|
41
|
+
* same value gate every prepare applies (`assertReadFaceValue`: `"open"` | `"roots"`
|
|
42
|
+
* | absent, anything else refused loudly). The RESOLVED face of a leg is still the
|
|
43
|
+
* resolver's business (task seat, governance, the read-only mount); the swap changes the
|
|
44
|
+
* deployment seat that resolution reads.
|
|
45
|
+
*/
|
|
46
|
+
export declare const SWAPPABLE_DEP_SEATS: readonly ["models", "tiers", "readFace"];
|
|
47
|
+
export type SwappableDepSeat = (typeof SWAPPABLE_DEP_SEATS)[number];
|
|
48
|
+
/** The candidate a swap takes: the swappable seats of {@link RunnerDeps}, each optional (presence = instruction). */
|
|
49
|
+
export type SwappableDeps = Pick<RunnerDeps, SwappableDepSeat>;
|
|
50
|
+
/** Whether a key names a swappable seat. */
|
|
51
|
+
export declare function isSwappableDepSeat(k: unknown): k is SwappableDepSeat;
|
|
52
|
+
/**
|
|
53
|
+
* Which notice a seat's successful swap is announced under — the disposition table over the set (a seat
|
|
54
|
+
* with no row does not compile: {@link SwapNoticeTableCoversEverySeat}). `models` and `tiers` share the
|
|
55
|
+
* catalog-generation line (one swap, one line, both counts); the read face has its own.
|
|
56
|
+
*/
|
|
57
|
+
export declare const SWAP_SEAT_NOTICE: {
|
|
58
|
+
readonly models: "config.models_swapped";
|
|
59
|
+
readonly tiers: "config.models_swapped";
|
|
60
|
+
readonly readFace: "config.read_face_swapped";
|
|
61
|
+
};
|
|
62
|
+
/** The fence over the notice table: `never` while every seat has a row. */
|
|
63
|
+
export type SwapNoticeTableCoversEverySeat = AssertAllKeysHandled<Exclude<SwappableDepSeat, keyof typeof SWAP_SEAT_NOTICE>>;
|
|
64
|
+
/** What the screen hands back: the seats present (set order) and the ONE read of each present seat's value,
|
|
65
|
+
* copied at screen time — the door writes from THIS object and never re-reads the caller's. */
|
|
66
|
+
export interface ScreenedSwappableDeps {
|
|
67
|
+
readonly seats: readonly SwappableDepSeat[];
|
|
68
|
+
/** A fresh plain object: the screened value of every present seat (`models`/`tiers` shallow-copied,
|
|
69
|
+
* `readFace` the screened word; an explicit `undefined` is kept as a PRESENT own key so the door can
|
|
70
|
+
* tell "clear this seat" from "not named"). */
|
|
71
|
+
readonly candidate: SwappableDeps;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Screen a swap candidate BEFORE anything is written: the object shape, the key set (every own key must
|
|
75
|
+
* be a member of {@link SWAPPABLE_DEP_SEATS}; at least one), the `tiers`-rides-with-`models` rule, and
|
|
76
|
+
* each present seat's own value gate. Returns the seats present (set order) and a SNAPSHOT of their values
|
|
77
|
+
* — the one read. Throws on the first defect; the caller has written nothing yet, so the current deps stay
|
|
78
|
+
* in force byte-identical.
|
|
79
|
+
*
|
|
80
|
+
* ONE READ, BY CONSTRUCTION: every seat value is read off the caller's object exactly once, here, and the
|
|
81
|
+
* screened copy is what the door writes and announces. A getter-backed or concurrently-mutated candidate
|
|
82
|
+
* therefore cannot present a legal value to the screen and a different one to the write (or to the
|
|
83
|
+
* announcement, which runs AFTER the write — a throw there would have left a half-announced swap in
|
|
84
|
+
* force). The prepare doors read their deps seats the same way.
|
|
85
|
+
*
|
|
86
|
+
* The per-seat gate is the SAME one the seat's prepare-time reader applies (`readFace` ⇒
|
|
87
|
+
* `assertReadFaceValue` with the deployment-seat name a prepare door uses — one value, one loudness, one
|
|
88
|
+
* seat name on every leg), so a value the door admits is a value every later prepare admits.
|
|
89
|
+
*/
|
|
90
|
+
export declare function screenSwappableDeps(next: unknown): ScreenedSwappableDeps;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { assertReadFaceValue } from "../tools/fs/read-face.js";
|
|
2
|
+
export const SWAPPABLE_DEP_SEATS = ["models", "tiers", "readFace"];
|
|
3
|
+
const SWAPPABLE_DEP_SEAT_SET = new Set(SWAPPABLE_DEP_SEATS);
|
|
4
|
+
export function isSwappableDepSeat(k) {
|
|
5
|
+
return SWAPPABLE_DEP_SEAT_SET.has(k);
|
|
6
|
+
}
|
|
7
|
+
export const SWAP_SEAT_NOTICE = {
|
|
8
|
+
models: "config.models_swapped",
|
|
9
|
+
tiers: "config.models_swapped",
|
|
10
|
+
readFace: "config.read_face_swapped",
|
|
11
|
+
};
|
|
12
|
+
export function screenSwappableDeps(next) {
|
|
13
|
+
if (next === null || typeof next !== "object" || Array.isArray(next)) {
|
|
14
|
+
throw new Error(`swapDeps: the candidate must be a plain object naming the seats to swap (got ${next === null ? "null" : Array.isArray(next) ? "array" : typeof next}) — the current deps stay in force`);
|
|
15
|
+
}
|
|
16
|
+
const keys = Object.keys(next);
|
|
17
|
+
const unknown = keys.filter((k) => !isSwappableDepSeat(k));
|
|
18
|
+
if (unknown.length > 0) {
|
|
19
|
+
throw new Error(`swapDeps: ${unknown.map((k) => JSON.stringify(k)).join(", ")} is not a hot-swappable seat (the closed set is ${SWAPPABLE_DEP_SEATS.join(" | ")}) — the current deps stay in force`);
|
|
20
|
+
}
|
|
21
|
+
const present = SWAPPABLE_DEP_SEATS.filter((seat) => Object.hasOwn(next, seat));
|
|
22
|
+
if (present.length === 0) {
|
|
23
|
+
throw new Error(`swapDeps: the candidate names no seat (an empty swap is a caller defect, not a no-op) — the current deps stay in force`);
|
|
24
|
+
}
|
|
25
|
+
const raw = next;
|
|
26
|
+
const candidate = {};
|
|
27
|
+
for (const seat of present) {
|
|
28
|
+
const v = raw[seat];
|
|
29
|
+
switch (seat) {
|
|
30
|
+
case "models":
|
|
31
|
+
if (v === null || typeof v !== "object" || Array.isArray(v)) {
|
|
32
|
+
throw new Error(`swapDeps: models must be a plain Record<string, Model> (got ${v === null ? "null" : Array.isArray(v) ? "array" : typeof v}) — the current generation stays in force`);
|
|
33
|
+
}
|
|
34
|
+
candidate.models = { ...v };
|
|
35
|
+
break;
|
|
36
|
+
case "tiers":
|
|
37
|
+
if (!present.includes("models")) {
|
|
38
|
+
throw new Error(`swapDeps: tiers is a binding over models — pass the catalog it binds in the same swap (\`{ models, tiers }\`); the current generation stays in force`);
|
|
39
|
+
}
|
|
40
|
+
if (v !== undefined && (v === null || typeof v !== "object" || Array.isArray(v))) {
|
|
41
|
+
throw new Error(`swapDeps: tiers must be a plain Record<string, ModelRef> or undefined (got ${v === null ? "null" : Array.isArray(v) ? "array" : typeof v}) — the current generation stays in force`);
|
|
42
|
+
}
|
|
43
|
+
candidate.tiers = v === undefined ? undefined : { ...v };
|
|
44
|
+
break;
|
|
45
|
+
case "readFace":
|
|
46
|
+
candidate.readFace = assertReadFaceValue(v, "readFace (deployment seat)");
|
|
47
|
+
break;
|
|
48
|
+
default: {
|
|
49
|
+
const _exhaustive = seat;
|
|
50
|
+
throw new Error(`swapDeps: unhandled seat ${String(_exhaustive)}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { seats: present, candidate: candidate };
|
|
55
|
+
}
|
|
@@ -204,6 +204,23 @@ export type PermissionResult = {
|
|
|
204
204
|
* same safe direction as `matchedAskRule`); it cannot state a window of its own, because the
|
|
205
205
|
* route it would be a window for has not been chosen yet. */
|
|
206
206
|
denialLimitFallback?: import("./auto-mode.js").UnarmedDenialLimitFallback;
|
|
207
|
+
/** #616 (additive): the auto-mode classifier was CONSULTED on this ask and could not run — the ask
|
|
208
|
+
* flows the original chain exactly as it would have (routing, origin, bit and members unchanged),
|
|
209
|
+
* carrying the station FACT beside them so a card can say "asked because the classifier was
|
|
210
|
+
* unavailable (timeout)" instead of reading as ordinary hesitation. `cause` is the verdict's own
|
|
211
|
+
* word ({@link import("./auto-mode.js").AutoModeUnavailableCause}: `error` / `timeout` /
|
|
212
|
+
* `breaker_open`). A FACT, not an origin: CC 2.1.250 marks the same condition as a denial KIND on
|
|
213
|
+
* the outcome (`automode-unavailable`) beside the decision's provenance, never in place of it, and
|
|
214
|
+
* this engine's divergence (the ask reaches a person instead of being denied) does not move the
|
|
215
|
+
* fact onto the origin axis either. ENGINE-STAMPED at the classifier stations (the gate's own and
|
|
216
|
+
* the inherited-lane arms) only when the verdict was `unavailable`; `parse_error` stamps nothing
|
|
217
|
+
* (the classifier ran and answered outside its contract — a different sentence). Display metadata:
|
|
218
|
+
* nothing reads it to decide anything, so a policy that self-declares it can only put its own
|
|
219
|
+
* sentence on its own card. Carried onto the approval request and the durable row by the carry
|
|
220
|
+
* stations. */
|
|
221
|
+
classifierUnavailable?: {
|
|
222
|
+
readonly cause: import("./auto-mode.js").AutoModeUnavailableCause;
|
|
223
|
+
};
|
|
207
224
|
/** #144 disclosure (additive): a persisted allow rule MATCHED this call but could not clear the
|
|
208
225
|
* ask, because the ask is MANDATED (operator shellGate:"always", or the tool's own
|
|
209
226
|
* egress/irreversibility marks) rather than a classifier's hesitation — "allow rules silence
|
|
@@ -1191,6 +1208,15 @@ export interface AskRequest {
|
|
|
1191
1208
|
* that forwards a decision's member here does not compile, which is the point: the window is the
|
|
1192
1209
|
* route's fact, and forwarding it silently is how a configured window becomes a wait with none. */
|
|
1193
1210
|
readonly denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
|
|
1211
|
+
/** #616 (additive) — present ⇔ the auto-mode classifier was consulted on this ask and could not run (see
|
|
1212
|
+
* the {@link PermissionResult} ask-arm member of the same name): the card's "asked because the
|
|
1213
|
+
* classifier was unavailable" fact, with the verdict's own cause word. Absent on every ask the classifier
|
|
1214
|
+
* answered, was not eligible for, or was not wired for — read presence, never absence. Filled by the
|
|
1215
|
+
* carry stations from the decision, never a caller/worker-settable field; the durable park row carries
|
|
1216
|
+
* the same member (`PendingAction.tool_approval.classifierUnavailable`). */
|
|
1217
|
+
readonly classifierUnavailable?: {
|
|
1218
|
+
readonly cause: import("./auto-mode.js").AutoModeUnavailableCause;
|
|
1219
|
+
};
|
|
1194
1220
|
/** (additive) WHICH AUTHORITY raised this ask — the wire twin of the {@link PermissionResult} ask-arm
|
|
1195
1221
|
* member of the same name, one word from the closed {@link import("./ask-origin.js").AskOrigin} set.
|
|
1196
1222
|
* The gate's own mint station copies the engine-stamped word; the three inherited-lane stations
|
package/dist/core/tool-policy.js
CHANGED
|
@@ -410,6 +410,7 @@ export function combinePolicies(...policies) {
|
|
|
410
410
|
let ruleAskText;
|
|
411
411
|
let probeMandateSeen = false;
|
|
412
412
|
let fallbackSeen;
|
|
413
|
+
let classifierUnavailableSeen;
|
|
413
414
|
for (const p of policies) {
|
|
414
415
|
const d = refuseOutOfContractDecision(await p.check(current, signal));
|
|
415
416
|
if (d.action === "deny") {
|
|
@@ -440,13 +441,16 @@ export function combinePolicies(...policies) {
|
|
|
440
441
|
probeMandateSeen = true;
|
|
441
442
|
if (d.action === "ask" && d.denialLimitFallback !== undefined && fallbackSeen === undefined)
|
|
442
443
|
fallbackSeen = d.denialLimitFallback;
|
|
444
|
+
if (d.action === "ask" && d.classifierUnavailable !== undefined && classifierUnavailableSeen === undefined)
|
|
445
|
+
classifierUnavailableSeen = d.classifierUnavailable;
|
|
443
446
|
}
|
|
444
447
|
if (asked) {
|
|
445
448
|
const merged = rewrite?.updatedInput;
|
|
446
449
|
const withRuleAsk = ruleAskText !== undefined && asked.matchedAskRule === undefined ? { ...asked, matchedAskRule: ruleAskText } : asked;
|
|
447
450
|
const withMark = probeMandateSeen && withRuleAsk.probeMandated !== true ? { ...withRuleAsk, probeMandated: true } : withRuleAsk;
|
|
448
451
|
const withFallback = fallbackSeen !== undefined && withMark.denialLimitFallback === undefined ? { ...withMark, denialLimitFallback: fallbackSeen, requiresRealApproval: true } : withMark;
|
|
449
|
-
|
|
452
|
+
const withFact = classifierUnavailableSeen !== undefined && withFallback.classifierUnavailable === undefined ? { ...withFallback, classifierUnavailable: { cause: classifierUnavailableSeen.cause } } : withFallback;
|
|
453
|
+
return merged !== undefined ? { ...withFact, updatedInput: merged } : withFact;
|
|
450
454
|
}
|
|
451
455
|
const allowed = rewrite ?? ALLOW;
|
|
452
456
|
if (settledAllow === undefined)
|