@sema-agent/core 7.0.1 → 7.1.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 +33 -0
- package/dist/agents/repair-loop.d.ts +8 -7
- package/dist/agents/roster-store.d.ts +7 -2
- package/dist/agents/subagent.js +29 -5
- package/dist/brain/errors.d.ts +18 -0
- package/dist/brain/errors.js +3 -0
- package/dist/brain/stream-engine.js +6 -4
- package/dist/core/context-edit.d.ts +3 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +4 -0
- package/dist/core/hooks.d.ts +26 -6
- package/dist/core/hooks.js +8 -5
- package/dist/core/image-downsample.d.ts +4 -3
- package/dist/core/memory-engine/engine.d.ts +62 -5
- package/dist/core/memory-engine/engine.js +90 -19
- package/dist/core/memory-engine/index.d.ts +1 -1
- package/dist/core/memory-engine/layout.d.ts +11 -3
- package/dist/core/roles.d.ts +36 -9
- package/dist/core/roles.js +19 -6
- package/dist/core/runner/prepare-memory.js +29 -22
- package/dist/core/runner/prepare-task.d.ts +5 -3
- package/dist/core/runner/prepare-task.js +88 -47
- package/dist/core/runner/runtask.d.ts +12 -3
- package/dist/core/runner/runtask.js +32 -4
- package/dist/core/safety-axis-vocab.d.ts +1 -1
- package/dist/core/strategy-store.d.ts +4 -1
- package/dist/core/task-registry-shared.d.ts +7 -3
- package/dist/core/tool-errors.d.ts +1 -1
- package/dist/core/tool-policy.d.ts +40 -7
- package/dist/core/tool-policy.js +63 -9
- package/dist/core/types.d.ts +97 -11
- package/dist/engine/compaction/compaction.js +6 -2
- package/dist/engine/harness/agent-harness.d.ts +28 -6
- package/dist/engine/harness/agent-harness.js +34 -2
- package/dist/engine/harness/messages.js +4 -0
- package/dist/engine/harness/types.d.ts +37 -0
- package/dist/engine/harness/types.js +5 -0
- package/dist/engine/session/session.js +3 -2
- package/dist/index.d.ts +1 -1
- package/dist/internal/harness.d.ts +1 -0
- package/dist/internal/harness.js +1 -0
- package/dist/orchestration/builtin-workflows.d.ts +17 -9
- package/dist/orchestration/run-workflow-tool.d.ts +9 -1
- package/dist/orchestration/run-workflow-tool.js +18 -8
- package/dist/orchestration/workflow-governance.js +1 -1
- package/dist/orchestration/workflow-types.d.ts +1 -0
- package/dist/orchestration/workflow.d.ts +9 -1
- package/dist/orchestration/workflow.js +5 -5
- package/dist/stores/file/mailbox-store.d.ts +2 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 7.1.0 — 2026-09-01
|
|
4
|
+
|
|
5
|
+
### BREAKING (timing) — design/384 slice 1 (#513): turn interrupts reach the ask waiting side
|
|
6
|
+
- **`resolveAsk` no longer waits for the approver to settle once the abort signal fires.** The onAsk await is raced against the composed `AbortSignal.any([run, call])`; the wait settles as the abort-family deny on its own. The prior guarantee "no return before onAsk settles" is REMOVED. Detached discipline: the still-pending approver promise is swallow-guarded; a late APPROVE becomes the `task.late_approval` engine notice; a late REJECT reaches `onError(phase:"hook")` — disclosed, never silent. This also closes the pre-existing run-abort deaf-approver exposure (a walltime/cancel end can no longer be held open by an approver ignoring its signal).
|
|
7
|
+
- **Signal thread (additive seats)**: the loop's turn-scoped signal rides the `tool_call` hook event as an optional `signal` seat; `ToolGateInput.callSignal` (optional) hands it to the three synchronous wait seats (`adjudicate`/`resolveAsk`/`resolveContentAsk`) as a new optional trailing argument — same-arity guarantee when absent (conditional invocation, no explicit trailing `undefined`).
|
|
8
|
+
- **`ENGINE_NOTICE_CODES` closed-set +2** (downstream full-set pins go red): `task.halt_unconsumed` (halt verb answered while the run's ending was already owned by its own abort — nothing cut, `haltedByUser` unsigned, this notice is the halt's only trace) and `task.late_approval` (the detached approver's late yes ran nothing; NON-CONSUMPTION semantics — the engine claims neither who decided nor which came first). Both audience `user`.
|
|
9
|
+
- **`task_aborted` widened by DEFINITION, zero closed-set additions**: the deny resolution now names the run's OR turn's abort signal; the deny message widens to `approval aborted for X (run or turn interrupted)` at all four family sites (probes pinning the old `(task ended)` bytes re-pinned).
|
|
10
|
+
- **Ledger narrowing (named)**: the humanReview gates ledger stops booking abort-family denies (`task_aborted` joins the `approverUnavailable` skip — interrupts are not denials; also covers the pre-existing run-abort booking distortion).
|
|
11
|
+
- **steer `priority:"now"` revokes a pending card and the run continues** (CC-aligned; same seat as `interruptTurn`, pinned e2e).
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- **Terminal API failure now persists in the transcript** (#506㋐, CC 2.1.250 re-anchor): the harness mints a SEPARATE assistant record (reserved `<synthetic>` model sentinel + `isApiErrorMessage` + one bounded `API Error:` text block ≤2000 chars, `apiErrorStatus`/`requestId` twins, honest `"error"` stop + `usageMissing` — deliberately NOT copying CC's lying `stop_sequence`/zero-usage fields). `convertToLlm` drops every such record from every request view (the invalid-sequence class stays closed), with the exclusion carried at four further seats: auto-mode classifier window, prompt-suggestions grounding tail, compaction cut points, session derived-model tracking. ABORT arm untouched.
|
|
15
|
+
- **`BrainStatus.errorStatus`** (#506㋑): the HTTP status that named a retry's failure now rides the status channel in `api_retry` context (single `namesTheFailure` predicate governs both this seat and `apiErrorStatus`; 0-sentinel and 2xx never publish). WIRE EXTENSION — server/cli consumption named in the ship post.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **#512 model-vocabulary batch** (six items): run_workflow card stops teaching a tier-word claim false on tiers-configured deployments (real two-part gate = allowlist ∩ catalog); `parseModelMention` JSDoc recipe corrected to the EXPANDED table; four prototype-chain reads own-gated (live leak: `resolveModelDisplayLabel` returned Object.prototype functions for "toString" onto `agent_start.model`; `__proto__` writes now `Object.defineProperty` own data properties — a codex-caught setter regression); `suggestNextPrompts` bare catch now announces through `onError(phase:"suggestions")` on an explicit unresolvable `role`; roster/durable-registry `model` re-documented as the RECORD KEY; `agent_end.modelResolved` added to the WorkflowEvent union (additive — emitter always wrote it).
|
|
19
|
+
- **`scripts/verify-debts.mjs` two-evidence gate told the truth** (ledger-audit A-03): CLOSED rows now require two DISTINCT addressable coordinates; the old gate passed on a single commit OR post number.
|
|
20
|
+
|
|
21
|
+
### Docs
|
|
22
|
+
- Self-scan doc-rot batch (33 findings triaged, 13 confirmed + extensions): every rotted `file.ts:NNN` cross-reference in src re-spelled by symbol (line-anchor ratchet banks the cleanup); six orphaned JSDoc blocks reattached to their symbols (incl. the public `Runner` class and `expandTiers`); the stale `agent-harness` abort-clears-queues contract sentence corrected to the #389 truth; release-docs gate register drift fixed in seven places; REVIEW-BACKLOG stale status cells refreshed against the tree.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## 7.0.2 — 2026-08-30
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- **`MemoryEngine.sessionMemoryStatus(sessionId)`** (#511①, design/383 §S-7 — the signed-design read face missed at build): `{captureOptedOut?, committedCount?, foldedCount?, optOutSource?, lastCaptureAt?}`. Every key optional — absence means that fact's source was unreadable, never a coined false/0 stand-in; a store fault answers `optOutSource:"fault"` with `captureOptedOut` ABSENT (the indeterminate axis projected); `foldedCount` counts evidence-only input rows over the full product blocks; the face never throws. New exported type `SessionMemoryStatus` (export surface 1841→1842). Server projects the wire endpoint as a pure derivation ([5786]).
|
|
29
|
+
- **`SessionCaptureRecordStore` dual form** (#511②): the three legs widen to `T | Promise<T>` — a SQL/async backend plugs in with Promise signatures while every existing sync implementation compiles untouched (the additive mechanical proof). The dual form propagates: sync deployments stay sync-transparent end to end; the sync-facing public seats (`MemoryEngineSession.captureOptOut.*`, `ToolExecuteContext.memoryCaptureOptedOut/…Indeterminate`, `parentMemoryCaptureState`) go dual with the await law documented — per-leg failure mapping preserved (mark rejection ⇒ unpersisted, read rejection ⇒ the indeterminate fault axis).
|
|
30
|
+
- **`best` model alias** (#511③, CC 250 $L parity): with `RunnerDeps.tiers` configured, `"best"` resolves to the first DIRECTLY bound tier walking `ultra→max→pro→flash→lite` from the head ("best available", deliberately not a fixed alias-table row). Explicit spellings win over the dynamic walk: a deployment catalog key named `best`, or an explicit `tiers.best` binding. No tiers ⇒ the word stays an unknown ref (refused, never coined).
|
|
31
|
+
|
|
32
|
+
### Changed — narrowing (named)
|
|
33
|
+
- **Agent tool `model` argument: non-string values on the schema-外 direct-call lane refuse typed** (#511④, test [5781] codex observation): a number/array/boolean/garbage-object now refuses `model.invalid` (closed-set ADDITION beside `model.empty`/`model.unknown` — downstream pinning the details.error set gains a row) instead of crashing or silently no-oping; a legal Model-shape object (string id+api) passes through as the per-call model; strings keep every existing arm byte-identical.
|
|
34
|
+
- **`modelNote` fork-ignored and roster-less arms defuse the model-authored word** (#511⑤, census P2): both arms now run the same `inlineUntrusted` fold (newline fold + fence/envelope-tag neutralization + 80 cap) the family's other three arms already ran — a hostile `model` word can no longer carry raw newlines or fence sentinels into a top-level report line. Clean short words render byte-identical.
|
|
35
|
+
|
|
3
36
|
## 7.0.1 — 2026-08-30
|
|
4
37
|
|
|
5
38
|
### Added
|
|
@@ -13,7 +13,7 @@ import { type OracleIsolationVerdict } from "../core/oracle-isolation.js";
|
|
|
13
13
|
* `oracleIsolation` boundary that this slice deliberately does not build — SAFE-tier escalates to a human
|
|
14
14
|
* (`needs_human_oracle`) or surfaces a candidate, but never clears its own work.
|
|
15
15
|
*
|
|
16
|
-
* Like {@link verifyCompleted}, this is a **thin composition** over `runner.runTask` (verify.ts
|
|
16
|
+
* Like {@link verifyCompleted}, this is a **thin composition** over `runner.runTask` (verify.ts's DEPLOYMENT POSTURE section)
|
|
17
17
|
* — it adds no Runner-core surface, touches no vendored code, and is called by a leader/profile (the sibling
|
|
18
18
|
* of {@link runWithVerification}). The oracle internals (which gate/judge/property-harness to compose) live
|
|
19
19
|
* in the PROFILE-injected {@link RepairOracle} closure; core only fixes the {@link OracleResult} shape and the
|
|
@@ -75,12 +75,13 @@ export interface OracleResult {
|
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
77
77
|
* The PROFILE-injected oracle (§1 裁决①). The profile composes the actual grading inside this closure —
|
|
78
|
-
* `runExecGate(graderEnv, steps)` (exec-gate.ts
|
|
79
|
-
* grader) → `verifyCompleted` (verify.ts
|
|
80
|
-
* (property-harness.ts
|
|
78
|
+
* `runExecGate(graderEnv, steps)` (core/exec-gate.ts, env is the FIRST param = L2 provenance bound to the
|
|
79
|
+
* grader) → `verifyCompleted` (agents/verify.ts, the read-only L3 judge) → `checkInvariants`
|
|
80
|
+
* (core/property-harness.ts, explore-only) — and maps the composite to an {@link OracleResult}. Core never
|
|
81
81
|
* composes these internals (so the oracle type can't grow a long discriminated union); it only fixes the
|
|
82
82
|
* `OracleResult` shape and the read-only/identity contract. `graderEnv` is the isolated grader env; `evidence`
|
|
83
|
-
* is the diff/results to judge (recompute it from the post-resume working tree on a resume —
|
|
83
|
+
* is the diff/results to judge (recompute it from the post-resume working tree on a resume — threat BUG5, the
|
|
84
|
+
* same freshness duty verify.ts states on its own `evidence` config field).
|
|
84
85
|
*/
|
|
85
86
|
export type RepairOracle = (graderEnv: ExecutionEnv, evidence: string | undefined) => Promise<OracleResult>;
|
|
86
87
|
/**
|
|
@@ -166,12 +167,12 @@ export interface RepairLoopConfig {
|
|
|
166
167
|
*/
|
|
167
168
|
resumeBundle?: RepairBundle;
|
|
168
169
|
/**
|
|
169
|
-
* Stop the loop once cumulative cost reaches this (verify.ts
|
|
170
|
+
* Stop the loop once cumulative cost reaches this (verify.ts's `costCeilingMicroUsd` backstop). HRD-AGO-6 — "cumulative" is
|
|
170
171
|
* every leg the loop pays for: each attempt's own + nested `runTask` cost PLUS each verdict's
|
|
171
172
|
* {@link OracleResult.costMicroUsd} (an LLM-judge oracle is charged per attempt too).
|
|
172
173
|
*/
|
|
173
174
|
costCeilingMicroUsd?: number;
|
|
174
|
-
/** Overall ACTIVE wall-clock ceiling for the whole loop (verify.ts
|
|
175
|
+
/** Overall ACTIVE wall-clock ceiling for the whole loop (verify.ts's `totalTimeoutMs` backstop). F1: both ceilings
|
|
175
176
|
* are CROSS-RESUME — the carried {@link RepairBundle.spentMicroUsd}/{@link RepairBundle.activeElapsedMs}
|
|
176
177
|
* re-seed the accounts, so a durable resume continues the budget rather than restarting it. */
|
|
177
178
|
totalTimeoutMs?: number;
|
|
@@ -12,8 +12,13 @@ export interface RosterEntry {
|
|
|
12
12
|
* key; equals the spawner's session at depth 1). Stored verbatim; no predicate arm consumes it
|
|
13
13
|
* yet (enumeration/recovery is the reader). */
|
|
14
14
|
rootSessionId?: string;
|
|
15
|
-
/** design/151 S3b — the
|
|
16
|
-
*
|
|
15
|
+
/** design/151 S3b — the model RECORD KEY at spawn (revival lookup key; display/model-routing hints
|
|
16
|
+
* only, never a serialized spec). NOT the resolved model id: a spawn that named a model in WORDS
|
|
17
|
+
* records the caller's spelling VERBATIM (a catalog key, tier word or CC alias), because a revival
|
|
18
|
+
* re-resolves this value against the catalog in force at WAKE time and a resolved id would only
|
|
19
|
+
* ever resolve again on deployments whose catalog keys happen to equal model ids. A spawn that
|
|
20
|
+
* carried a Model OBJECT (definition/tool-level/inherited ref) records that object's id, which on
|
|
21
|
+
* such a deployment is not a catalog key and degrades softly on the revival lane. */
|
|
17
22
|
model?: string;
|
|
18
23
|
/** Ruled 2026-08-05: the spawn requested a model word that did not bind — the row runs on its
|
|
19
24
|
* inherited default. Closed set, single member today; absent = bound normally (or no word). */
|
package/dist/agents/subagent.js
CHANGED
|
@@ -1590,6 +1590,19 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1590
1590
|
};
|
|
1591
1591
|
}
|
|
1592
1592
|
}
|
|
1593
|
+
const modelObjectForm = typeof a.model === "object" &&
|
|
1594
|
+
a.model !== null &&
|
|
1595
|
+
!Array.isArray(a.model) &&
|
|
1596
|
+
typeof a.model.id === "string" &&
|
|
1597
|
+
typeof a.model.api === "string";
|
|
1598
|
+
if (a.model !== undefined && typeof a.model !== "string" && !modelObjectForm) {
|
|
1599
|
+
const typeDesc = Array.isArray(a.model) ? "an array" : a.model === null ? "null" : typeof a.model === "object" ? "an object" : `a ${typeof a.model}`;
|
|
1600
|
+
return {
|
|
1601
|
+
isError: true,
|
|
1602
|
+
content: `Sub-agent not started: \`model\` must be a model name (string) from this deployment's catalog — got ${typeDesc}. Omit the parameter to run the agent type's own model.`,
|
|
1603
|
+
details: { error: "model.invalid" },
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1593
1606
|
if (typeof a.model === "string" && a.model.trim() === "" && !wantsFork && reviveClaim === undefined) {
|
|
1594
1607
|
return {
|
|
1595
1608
|
isError: true,
|
|
@@ -1602,6 +1615,14 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1602
1615
|
let modelNote;
|
|
1603
1616
|
const judgedModels = activeModels();
|
|
1604
1617
|
const rosterKeys = judgedModels !== undefined ? Object.keys(judgedModels) : [];
|
|
1618
|
+
if (modelObjectForm) {
|
|
1619
|
+
if (wantsFork) {
|
|
1620
|
+
modelNote = `note: model "${inlineUntrusted(a.model.id, 80)}" was ignored — a fork always runs on the caller's model.`;
|
|
1621
|
+
}
|
|
1622
|
+
else {
|
|
1623
|
+
perCallModel = a.model;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1605
1626
|
if (typeof a.model === "string" && requestedModel === undefined) {
|
|
1606
1627
|
modelNote = wantsFork
|
|
1607
1628
|
? `note: model "" was ignored — a fork always runs on the caller's model.`
|
|
@@ -1609,10 +1630,10 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1609
1630
|
}
|
|
1610
1631
|
else if (requestedModel !== undefined) {
|
|
1611
1632
|
if (wantsFork) {
|
|
1612
|
-
modelNote = `note: model "${requestedModel}" was ignored — a fork always runs on the caller's model.`;
|
|
1633
|
+
modelNote = `note: model "${inlineUntrusted(requestedModel, 80)}" was ignored — a fork always runs on the caller's model.`;
|
|
1613
1634
|
}
|
|
1614
1635
|
else if (rosterKeys.length === 0) {
|
|
1615
|
-
modelNote = `note: model "${requestedModel}" was NOT applied — this delegation tool has no model roster configured; the sub-agent ran on its default (inherited) model.`;
|
|
1636
|
+
modelNote = `note: model "${inlineUntrusted(requestedModel, 80)}" was NOT applied — this delegation tool has no model roster configured; the sub-agent ran on its default (inherited) model.`;
|
|
1616
1637
|
}
|
|
1617
1638
|
else {
|
|
1618
1639
|
try {
|
|
@@ -1783,7 +1804,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1783
1804
|
}
|
|
1784
1805
|
})()
|
|
1785
1806
|
: childModel?.id;
|
|
1786
|
-
const rowModelKey = perCallModel !== undefined ? requestedModel : typeof childModel === "string" ? childModel : childModel?.id;
|
|
1807
|
+
const rowModelKey = perCallModel !== undefined ? requestedModel ?? perCallModel.id : typeof childModel === "string" ? childModel : childModel?.id;
|
|
1787
1808
|
const childSystemPrompt = def?.systemPrompt ?? opts.systemPrompt;
|
|
1788
1809
|
const childDefaultPersona = childSystemPrompt === undefined && !wantsFork ? SUBAGENT_PROMPT : undefined;
|
|
1789
1810
|
const defMaxTurns = typeof def?.maxTurns === "number" && Number.isFinite(def.maxTurns) && def.maxTurns > 0 ? def.maxTurns : undefined;
|
|
@@ -2027,6 +2048,9 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2027
2048
|
const provenanceRequest = ctx.delegationProvenanceForChildren?.();
|
|
2028
2049
|
const childProvenanceRef = provenanceRequest !== undefined ? { current: newDelegationProvenanceAggregate() } : undefined;
|
|
2029
2050
|
const childAttestation = (status) => childProvenanceRef !== undefined ? reduceDelegationAttestation(childProvenanceRef.current, { completed: status === "completed" }) : undefined;
|
|
2051
|
+
const liveCaptureFloor = reviveClaim === undefined
|
|
2052
|
+
? { optedOut: (await ctx.memoryCaptureOptedOut) === true, indeterminate: (await ctx.memoryCaptureIndeterminate) === true }
|
|
2053
|
+
: undefined;
|
|
2030
2054
|
const childInternals = {
|
|
2031
2055
|
...(inheritedManifestScope ? { inheritedManifestScope } : {}),
|
|
2032
2056
|
...(childProvenanceRef !== undefined && provenanceRequest !== undefined ? { delegationProvenance: { ref: childProvenanceRef, contentSafety: provenanceRequest } } : {}),
|
|
@@ -2045,8 +2069,8 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2045
2069
|
...(ctx.interactionPosture !== undefined ? { parentInteractionPosture: ctx.interactionPosture } : {}),
|
|
2046
2070
|
...(reviveClaim === undefined
|
|
2047
2071
|
? {
|
|
2048
|
-
...(
|
|
2049
|
-
...(
|
|
2072
|
+
...(liveCaptureFloor?.optedOut === true ? { memoryCaptureFloor: true } : {}),
|
|
2073
|
+
...(liveCaptureFloor?.optedOut !== true && liveCaptureFloor?.indeterminate === true ? { memoryCaptureFloorIndeterminate: true } : {}),
|
|
2050
2074
|
...(ctx.memoryCaptureControlDir !== undefined ? { memoryCaptureQueryDir: ctx.memoryCaptureControlDir } : {}),
|
|
2051
2075
|
...(ctx.memoryCaptureAncestors !== undefined ? { memoryCaptureAncestors: ctx.memoryCaptureAncestors } : {}),
|
|
2052
2076
|
}
|
package/dist/brain/errors.d.ts
CHANGED
|
@@ -82,6 +82,24 @@ export declare function readProviderRequestId(res: {
|
|
|
82
82
|
get?: (name: string) => string | null;
|
|
83
83
|
};
|
|
84
84
|
} | undefined): string | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* Does this HTTP status NAME the failure it is being attached to? The one predicate governing every
|
|
87
|
+
* seat on which this engine publishes a provider status — the terminal assistant frame's
|
|
88
|
+
* `apiErrorStatus` and the retry-wait status frame's `errorStatus` (#506 ㋑). One helper on purpose:
|
|
89
|
+
* both seats promise the same three-state discipline, and two copies of the rule would drift.
|
|
90
|
+
*
|
|
91
|
+
* Two distinct shapes are refused, and both are reachable:
|
|
92
|
+
* · The engine's "no status" SENTINEL, 0 — the terminal HTTP throw carries `r?.status ?? 0` when a
|
|
93
|
+
* transport hands back neither a response nor a throw. Publishing it would be absence rendered as a
|
|
94
|
+
* number a consumer may format, which is worse than silence.
|
|
95
|
+
* · A SUCCESS status. The connect loop only streams a response that is `ok` AND has a body, so an
|
|
96
|
+
* `ok` response with a NULL body (an empty 200 from a proxy, a 204) reaches the terminal throw
|
|
97
|
+
* carrying 200 — and a provider's own `x-should-retry` verdict can send that same 2xx into a RETRY
|
|
98
|
+
* wait. The boundary really did fail, but the status is not what failed, and a consumer routing or
|
|
99
|
+
* rendering by a 2xx is exactly the misreading these seats exist to prevent.
|
|
100
|
+
* Stalls, connect failures and in-band error frames inside a 200 leave the seat absent by the same rule.
|
|
101
|
+
*/
|
|
102
|
+
export declare function namesTheFailure(status: number | undefined): status is number;
|
|
85
103
|
/** Map an HTTP status to an error class. 401/403 = auth (don't retry); 429 = rate limit; 5xx = server. */
|
|
86
104
|
export declare function classifyHttp(status: number): BrainErrorCode;
|
|
87
105
|
/** Lift the machine-readable code back out of a `[code] …` prefixed `errorMessage` (the single shared
|
package/dist/brain/errors.js
CHANGED
|
@@ -45,6 +45,9 @@ export function readProviderRequestId(res) {
|
|
|
45
45
|
}
|
|
46
46
|
return undefined;
|
|
47
47
|
}
|
|
48
|
+
export function namesTheFailure(status) {
|
|
49
|
+
return status !== undefined && status >= 100 && !(status >= 200 && status < 300);
|
|
50
|
+
}
|
|
48
51
|
export function classifyHttp(status) {
|
|
49
52
|
if (status === 401 || status === 403)
|
|
50
53
|
return "auth";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createAssistantMessageEventStream, } from "../internal/llm.js";
|
|
2
2
|
import { FLOOR_OUTPUT_TOKENS, parseContextOverflow, planOutputCapAdjustment } from "./context-overflow.js";
|
|
3
|
-
import { BrainError, classifyConnectFailure, classifyHttp, describeNetworkError, readProviderRequestId } from "./errors.js";
|
|
3
|
+
import { BrainError, classifyConnectFailure, classifyHttp, describeNetworkError, namesTheFailure, readProviderRequestId } from "./errors.js";
|
|
4
4
|
import { classifyInputTooLong } from "./input-too-long.js";
|
|
5
5
|
import { FAST_MAX_BACKOFF_MS, providerWaitHint, retryBackoffMs } from "./retry.js";
|
|
6
6
|
import { emitBrainStatus, emitBrainTelemetry } from "./status-sink.js";
|
|
@@ -267,9 +267,8 @@ export function runStreamingBrain(args) {
|
|
|
267
267
|
errorMsg.inputTooLongRuledOut = true;
|
|
268
268
|
if (err.apiError === true) {
|
|
269
269
|
errorMsg.isApiErrorMessage = true;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
errorMsg.apiErrorStatus = st;
|
|
270
|
+
if (namesTheFailure(err.status))
|
|
271
|
+
errorMsg.apiErrorStatus = err.status;
|
|
273
272
|
if (requestIdSeen !== undefined)
|
|
274
273
|
errorMsg.requestId = requestIdSeen;
|
|
275
274
|
}
|
|
@@ -376,6 +375,7 @@ export function runStreamingBrain(args) {
|
|
|
376
375
|
attempt: attempt + 1,
|
|
377
376
|
maxRetries,
|
|
378
377
|
errClass: "output_cap",
|
|
378
|
+
...(namesTheFailure(r?.status) ? { errorStatus: r.status } : {}),
|
|
379
379
|
});
|
|
380
380
|
continue;
|
|
381
381
|
}
|
|
@@ -428,6 +428,7 @@ export function runStreamingBrain(args) {
|
|
|
428
428
|
discardResponseBody(r);
|
|
429
429
|
cc.abort();
|
|
430
430
|
cc.dispose();
|
|
431
|
+
const retryStatus = namesTheFailure(r?.status) ? r.status : undefined;
|
|
431
432
|
await sleepAnnouncingRetry(delayMs, signal, (remainingMs) => ({
|
|
432
433
|
phase: statusPhase,
|
|
433
434
|
detail: statusPhase === "rate_limited"
|
|
@@ -440,6 +441,7 @@ export function runStreamingBrain(args) {
|
|
|
440
441
|
attempt: attempt + 1,
|
|
441
442
|
maxRetries: laneMaxRetries,
|
|
442
443
|
errClass,
|
|
444
|
+
...(retryStatus !== undefined ? { errorStatus: retryStatus } : {}),
|
|
443
445
|
}));
|
|
444
446
|
continue;
|
|
445
447
|
}
|
|
@@ -257,6 +257,9 @@ export interface ContextEditOptions {
|
|
|
257
257
|
* append-only sessions cannot be rewritten. Dropping the junk from the REQUEST VIEW restores a legal
|
|
258
258
|
* transcript (TR3 rejoins its batch). Deterministic ⇒ byte-stable per request ⇒ prefix-cache safe.
|
|
259
259
|
* New sessions never contain these (the loop exits cleanly on abort; the harness skips persisting them).
|
|
260
|
+
* #506 ㋐ did NOT re-open this class: the API-failure record the harness now writes carries `API Error: …`
|
|
261
|
+
* TEXT, so it is not an empty failure assistant and this heal correctly leaves it alone. That record is
|
|
262
|
+
* excluded one layer later, at `convertToLlm`, by its own three-key identity (`isSyntheticApiErrorMessage`).
|
|
260
263
|
*/
|
|
261
264
|
export declare function dropEmptyFailureAssistants(messages: AgentMessage[]): AgentMessage[];
|
|
262
265
|
/**
|
|
@@ -103,7 +103,7 @@ export type NoticeAudience = "user" | "operator";
|
|
|
103
103
|
* src/ for notice mint shapes and names any code that is minted but unregistered, or registered but
|
|
104
104
|
* no longer minted.
|
|
105
105
|
*/
|
|
106
|
-
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "delegation.transcript_integrity", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "tool_result.offload_put_failed"];
|
|
106
|
+
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "delegation.transcript_integrity", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "tool_result.offload_put_failed"];
|
|
107
107
|
/** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
|
|
108
108
|
* `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
|
|
109
109
|
* sink is a supported shape, and narrowing that field would break it. */
|
|
@@ -124,6 +124,8 @@ export const ENGINE_NOTICE_CODES = [
|
|
|
124
124
|
"task.user_followup_undrained",
|
|
125
125
|
"steering.parked_input_blocked",
|
|
126
126
|
"task.turn_interrupted",
|
|
127
|
+
"task.halt_unconsumed",
|
|
128
|
+
"task.late_approval",
|
|
127
129
|
"memory.capture_opted_out",
|
|
128
130
|
"memory.capture_optout_unpersisted",
|
|
129
131
|
"tool_result.offload_put_failed",
|
|
@@ -139,6 +141,8 @@ const NOTICE_AUDIENCE_TABLE = {
|
|
|
139
141
|
"task.user_followup_undrained": "user",
|
|
140
142
|
"task.turn_interrupted": "user",
|
|
141
143
|
"steering.parked_input_blocked": "user",
|
|
144
|
+
"task.halt_unconsumed": "user",
|
|
145
|
+
"task.late_approval": "user",
|
|
142
146
|
"memory.capture_opted_out": "user",
|
|
143
147
|
"memory.capture_optout_unpersisted": "user",
|
|
144
148
|
"memory.consolidation_withheld": "user",
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -1169,7 +1169,10 @@ export declare function normalizePersistedRuleHit(hit: PersistedRuleAnswer): {
|
|
|
1169
1169
|
unreadable?: true;
|
|
1170
1170
|
coverage?: readonly import("./permission-rule-model.js").SegmentCoverage[];
|
|
1171
1171
|
};
|
|
1172
|
-
/** Inputs to the two-phase tool gate. `adjudicate`/`resolveAsk` are pre-bound to the task abort
|
|
1172
|
+
/** Inputs to the two-phase tool gate. `adjudicate`/`resolveAsk` are pre-bound to the task abort
|
|
1173
|
+
* signal; when the caller also supplies {@link ToolGateInput.callSignal}, the Runner's closures
|
|
1174
|
+
* additionally bind their waits to that per-call signal (`AbortSignal.any` of the two), so a turn
|
|
1175
|
+
* interrupt releases a pending ask instead of leaving it hanging on the run's lifetime alone. */
|
|
1173
1176
|
export interface ToolGateInput {
|
|
1174
1177
|
/** RB-473 batch 2b — bounded disclosure for a HOST callback that throws inside this gate call's
|
|
1175
1178
|
* isolation scope (onHookError itself, the deny observer, the durable-park observer). The runner
|
|
@@ -1214,13 +1217,28 @@ export interface ToolGateInput {
|
|
|
1214
1217
|
* builds ({@link HookEnvCapabilities}). Built ONCE per task by the runner (after the env is minted) and
|
|
1215
1218
|
* passed through unchanged; omitted when the deployment wired no execution environment. */
|
|
1216
1219
|
hookEnv?: HookEnvCapabilities;
|
|
1217
|
-
/**
|
|
1218
|
-
|
|
1220
|
+
/**
|
|
1221
|
+
* design/384 slice 1 — the PER-CALL abort signal, carried in from the `tool_call` hook event's
|
|
1222
|
+
* own `signal` seat (the loop's turn-scoped controller; the run abort is composed into it). The
|
|
1223
|
+
* gate itself never awaits on it — it hands the signal verbatim to the three synchronous-wait
|
|
1224
|
+
* seats below (`adjudicate`, `resolveAsk`, `resolveContentAsk`) as their optional trailing
|
|
1225
|
+
* argument, and the Runner's closures compose it with the run signal so a turn interrupt (bare
|
|
1226
|
+
* halt / steer-now) releases the wait. Absent (a host driving the gate directly, or an unthreaded
|
|
1227
|
+
* emit) ⇒ every seat is invoked exactly as before — same arity, no trailing `undefined` (the
|
|
1228
|
+
* threading is a conditional invocation) — and waits stay bound to the run signal alone: the
|
|
1229
|
+
* historical shape, byte-identical.
|
|
1230
|
+
*/
|
|
1231
|
+
callSignal?: AbortSignal;
|
|
1232
|
+
/** The combined tool-policy check (abort-bound), or undefined when no policy is wired (→ allow).
|
|
1233
|
+
* The optional trailing `callSignal` (design/384) is {@link ToolGateInput.callSignal}, passed
|
|
1234
|
+
* through verbatim; an implementation that ignores it keeps the run-level binding. */
|
|
1235
|
+
adjudicate?: (req: ToolCallRequest, callSignal?: AbortSignal) => Promise<PermissionResult>;
|
|
1219
1236
|
/** Resolve an `ask` to allow/deny via `onAsk` (abort-bound). Required iff a decision can be `ask`.
|
|
1220
1237
|
* G1 three-value: the result may carry `approverUnavailable` — the live approver affirmed no
|
|
1221
1238
|
* operator is reachable for THIS ask; the gate then re-routes onto the durable park leg (below),
|
|
1222
|
-
* keeping the carried fail-closed deny when no park is armed.
|
|
1223
|
-
|
|
1239
|
+
* keeping the carried fail-closed deny when no park is armed. The optional trailing `callSignal`
|
|
1240
|
+
* (design/384) is {@link ToolGateInput.callSignal}, passed through verbatim. */
|
|
1241
|
+
resolveAsk: (decision: PermissionResult, req: ToolCallRequest, callSignal?: AbortSignal) => Promise<ResolvedAsk>;
|
|
1224
1242
|
/**
|
|
1225
1243
|
* design/45: route a policy `ask` to a **durable suspension** instead of the synchronous `resolveAsk`
|
|
1226
1244
|
* (onAsk) path. Called in the `ask` branch with the FINAL post-hook args; if it returns a suspend
|
|
@@ -1309,8 +1327,10 @@ export interface ToolGateInput {
|
|
|
1309
1327
|
* from "nobody home" — the two have opposite re-routing rules). Passed UNCONDITIONALLY by the runner,
|
|
1310
1328
|
* including on runs with no live face: it then answers `unavailable` without contacting anything, which
|
|
1311
1329
|
* is exactly the pre-174 park/refusal behavior. Absent entirely ⇒ the pre-174 path verbatim.
|
|
1330
|
+
* The optional trailing `callSignal` (design/384) is {@link ToolGateInput.callSignal}, passed
|
|
1331
|
+
* through verbatim; an implementation that ignores it keeps the run-level binding.
|
|
1312
1332
|
*/
|
|
1313
|
-
resolveContentAsk?: (req: ToolCallRequest) => Promise<ContentAskOutcome>;
|
|
1333
|
+
resolveContentAsk?: (req: ToolCallRequest, callSignal?: AbortSignal) => Promise<ContentAskOutcome>;
|
|
1314
1334
|
/**
|
|
1315
1335
|
* design/70: the called tool is egress-marked (`ToolSpec.egress` — an external write: open a PR,
|
|
1316
1336
|
* push, send). The gate tightens a surviving `allow` to `ask` so an egress tool is NEVER
|
package/dist/core/hooks.js
CHANGED
|
@@ -426,6 +426,7 @@ export function persistedRuleMandateOf(marks) {
|
|
|
426
426
|
export async function runToolGate(input) {
|
|
427
427
|
const { event, preToolUse, adjudicate, resolveAsk, suspendAsk } = input;
|
|
428
428
|
const { toolCallId, toolName } = event;
|
|
429
|
+
const callSignal = input.callSignal;
|
|
429
430
|
const hookCtx = (seatSignal) => ({
|
|
430
431
|
toolCallId,
|
|
431
432
|
toolName,
|
|
@@ -506,7 +507,7 @@ export async function runToolGate(input) {
|
|
|
506
507
|
}
|
|
507
508
|
}
|
|
508
509
|
const req = { toolName, args: currentInput, toolCallId };
|
|
509
|
-
let decision = adjudicate ? await adjudicate(req) : { action: "allow" };
|
|
510
|
+
let decision = adjudicate ? await (callSignal !== undefined ? adjudicate(req, callSignal) : adjudicate(req)) : { action: "allow" };
|
|
510
511
|
let denySource = "policy";
|
|
511
512
|
let policyRewrite;
|
|
512
513
|
if ((decision.action === "allow" || decision.action === "ask") && decision.updatedInput !== undefined) {
|
|
@@ -867,7 +868,7 @@ export async function runToolGate(input) {
|
|
|
867
868
|
}
|
|
868
869
|
}
|
|
869
870
|
if (decision.action === "ask" && req.toolName === ASK_USER_QUESTION_TOOL_NAME) {
|
|
870
|
-
const outcome = input.resolveContentAsk !== undefined ? await input.resolveContentAsk(req) : { kind: "unavailable", parkDeclined: false };
|
|
871
|
+
const outcome = input.resolveContentAsk !== undefined ? await (callSignal !== undefined ? input.resolveContentAsk(req, callSignal) : input.resolveContentAsk(req)) : { kind: "unavailable", parkDeclined: false };
|
|
871
872
|
if (outcome.kind === "answered") {
|
|
872
873
|
decision = { action: "allow", updatedInput: outcome.presentedInput };
|
|
873
874
|
}
|
|
@@ -902,7 +903,7 @@ export async function runToolGate(input) {
|
|
|
902
903
|
}
|
|
903
904
|
if (decision.action === "ask") {
|
|
904
905
|
const askBeforeResolve = decision;
|
|
905
|
-
const resolved = await resolveAsk(decision, req);
|
|
906
|
+
const resolved = await (callSignal !== undefined ? resolveAsk(decision, req, callSignal) : resolveAsk(decision, req));
|
|
906
907
|
if (resolved.action !== "ask" && resolved.approver !== undefined)
|
|
907
908
|
resolvedApprover = resolved.approver;
|
|
908
909
|
if (resolved.action === "deny" && isAskDenyResolution(resolved.resolution))
|
|
@@ -978,7 +979,7 @@ export async function runToolGate(input) {
|
|
|
978
979
|
let recheck = { action: "allow" };
|
|
979
980
|
if (adjudicate) {
|
|
980
981
|
try {
|
|
981
|
-
recheck = await adjudicate({ toolName, args: editArgs, toolCallId });
|
|
982
|
+
recheck = await (callSignal !== undefined ? adjudicate({ toolName, args: editArgs, toolCallId }, callSignal) : adjudicate({ toolName, args: editArgs, toolCallId }));
|
|
982
983
|
}
|
|
983
984
|
catch (err) {
|
|
984
985
|
recheck = {
|
|
@@ -1014,7 +1015,9 @@ export async function runToolGate(input) {
|
|
|
1014
1015
|
editArgs = recheck.updatedInput;
|
|
1015
1016
|
editRewrittenSinceHuman = true;
|
|
1016
1017
|
}
|
|
1017
|
-
const
|
|
1018
|
+
const editAskDecision = { ...recheck, ruleEvidence: mintRuleEvidence({ dotsAbsent: "not_adjudicated" }) };
|
|
1019
|
+
const editAskReq = { toolName, args: editArgs, toolCallId };
|
|
1020
|
+
const rr = await (callSignal !== undefined ? resolveAsk(editAskDecision, editAskReq, callSignal) : resolveAsk(editAskDecision, editAskReq));
|
|
1018
1021
|
resolvedApprover = rr.action !== "ask" ? rr.approver : undefined;
|
|
1019
1022
|
if (rr.action !== "allow") {
|
|
1020
1023
|
if (rr.action === "deny" && isAskDenyResolution(rr.resolution))
|
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
export declare const MCP_IMAGE_MAX_BASE64: number;
|
|
16
16
|
/**
|
|
17
17
|
* 批③ image-pipeline — CC 2.1.x `constants/apiLimits.ts` values, shared by the MCP inline-image bound
|
|
18
|
-
* AND the Read tool's image branch (tools/fs — internal consistency: ONE yardstick per limit).
|
|
19
|
-
* `tools/fs/
|
|
20
|
-
*
|
|
18
|
+
* AND the Read tool's image branch (tools/fs — internal consistency: ONE yardstick per limit). That other
|
|
19
|
+
* consumers are the file tools (`tools/fs/fs-read.ts`'s image branch, plus `fs-pdf`/`fs-bash`), which IMPORT
|
|
20
|
+
* {@link MCP_IMAGE_MAX_BASE64} / {@link IMAGE_TARGET_RAW_SIZE} from here (through core/mcp.js's re-export)
|
|
21
|
+
* rather than re-deriving them — one limit, every consumer reading it, no drift.
|
|
21
22
|
* - `IMAGE_TARGET_RAW_SIZE` (apiLimits.ts:29): raw-byte target that guarantees the base64 encoding stays
|
|
22
23
|
* under {@link MCP_IMAGE_MAX_BASE64} (raw × 4/3 = base64 → 3.75MB raw = 5MB base64).
|
|
23
24
|
* - `IMAGE_MAX_WIDTH/HEIGHT` (apiLimits.ts:42-43): client-side resize box. The API internally resizes
|
|
@@ -796,6 +796,27 @@ export declare function memoryConsolidationRefusedNotice(input: {
|
|
|
796
796
|
reason?: string;
|
|
797
797
|
occurrenceId?: string;
|
|
798
798
|
}): EngineNotice;
|
|
799
|
+
/**
|
|
800
|
+
* design/383 §S-7 (#511 件1) — {@link MemoryEngine.sessionMemoryStatus}'s answer. Every key is
|
|
801
|
+
* optional; ABSENCE means the fact's source could not be read (a fault never coins a `false`/`0`
|
|
802
|
+
* stand-in) — see the method doc for each key's exact absence law.
|
|
803
|
+
*/
|
|
804
|
+
export interface SessionMemoryStatus {
|
|
805
|
+
/** TRUE = a standing capture opt-out record; FALSE = store readable, no record (capture on).
|
|
806
|
+
* ABSENT = the record store faulted — indeterminate (`optOutSource: "fault"` accompanies). */
|
|
807
|
+
captureOptedOut?: boolean;
|
|
808
|
+
/** Committed entries carrying this session's lineage contribution. Absent = ledger unreadable. */
|
|
809
|
+
committedCount?: number;
|
|
810
|
+
/** Of those, entries already folded into consolidation products (lineage × `distilled.inputs`).
|
|
811
|
+
* Absent = ledger / scope enumeration / product read unreadable. */
|
|
812
|
+
foldedCount?: number;
|
|
813
|
+
/** WHY `captureOptedOut` reads as it does: `"record"` = a standing one-way record;
|
|
814
|
+
* `"fault"` = the store faulted and the capture state is INDETERMINATE (no boolean is coined). */
|
|
815
|
+
optOutSource?: "record" | "fault";
|
|
816
|
+
/** Newest lineage `lastAt` for this session (ms epoch) — rides the same single ledger read as
|
|
817
|
+
* `committedCount`. Absent = ledger unreadable, or no committed contribution exists at all. */
|
|
818
|
+
lastCaptureAt?: number;
|
|
819
|
+
}
|
|
799
820
|
export declare class MemoryEngine {
|
|
800
821
|
private readonly backend;
|
|
801
822
|
private readonly memoryDir;
|
|
@@ -910,13 +931,17 @@ export declare class MemoryEngine {
|
|
|
910
931
|
* first cross-process resume, and "私密 only until the next resume" is a promise this engine
|
|
911
932
|
* refuses to imply — the deliberate divergence from the pollution marker's best-effort arm.
|
|
912
933
|
*/
|
|
913
|
-
markSessionCaptureOptOut(sessionId: string, reason: string): SessionCaptureOptOutMarkOutcome
|
|
934
|
+
markSessionCaptureOptOut(sessionId: string, reason: string): SessionCaptureOptOutMarkOutcome | Promise<SessionCaptureOptOutMarkOutcome>;
|
|
914
935
|
/** The session's capture opt-out record (in-process first, then the durable store) — undefined =
|
|
915
936
|
* capture is on. Side-effect-free OBSERVER face; a THROWING store read degrades to the
|
|
916
937
|
* in-process answer here. Every consumer whose answer decides whether bytes COMMIT must use
|
|
917
938
|
* {@link sessionCaptureOptOutOrFault} instead — this face cannot distinguish "no record" from
|
|
918
|
-
* "store outage", and on that distinction the fail direction flips.
|
|
919
|
-
|
|
939
|
+
* "store outage", and on that distinction the fail direction flips.
|
|
940
|
+
* DUAL FORM (#511 件2, the whole capture-face family — this one, `sessionCaptureOptOutOrFault`,
|
|
941
|
+
* `markSessionCaptureOptOut`, `listCaptureOptOutSessions`): over a sync store the answer is the
|
|
942
|
+
* same synchronous value as always; over a Promise-form {@link SessionCaptureRecordStore} the
|
|
943
|
+
* face answers a Promise of the identical shape. `await` is always correct on either arm. */
|
|
944
|
+
sessionCaptureOptOut(sessionId: string): SessionCaptureOptOutRecord | undefined | Promise<SessionCaptureOptOutRecord | undefined>;
|
|
920
945
|
/** {@link sessionCaptureOptOut} with the FAULT axis preserved (codex review, 亲核 adopted):
|
|
921
946
|
* `fault: true` ⇔ the durable store THREW — the record state is INDETERMINATE, which the
|
|
922
947
|
* commit-deciding consumers (harvest opening read, pre-commit re-read, the runner's read
|
|
@@ -926,11 +951,14 @@ export declare class MemoryEngine {
|
|
|
926
951
|
sessionCaptureOptOutOrFault(sessionId: string): {
|
|
927
952
|
record?: SessionCaptureOptOutRecord;
|
|
928
953
|
fault: boolean;
|
|
929
|
-
}
|
|
954
|
+
} | Promise<{
|
|
955
|
+
record?: SessionCaptureOptOutRecord;
|
|
956
|
+
fault: boolean;
|
|
957
|
+
}>;
|
|
930
958
|
/** Every capture-opted-out session id (durable roster ∪ in-process marks). THROWS on an
|
|
931
959
|
* enumeration failure — the one consumer (the consolidation eligibility arm, §2.4) is
|
|
932
960
|
* fail-closed by design: "roster unknown" must refuse the run, never read as "no one opted out". */
|
|
933
|
-
listCaptureOptOutSessions(): Set<string
|
|
961
|
+
listCaptureOptOutSessions(): Set<string> | Promise<Set<string>>;
|
|
934
962
|
/**
|
|
935
963
|
* design/383 §2.3 — the mid-session flip's BOUNDARY-ISOLATION SWEEP over the write plane: files
|
|
936
964
|
* under the writable root that this session window added or changed (vs the materialize baseline)
|
|
@@ -1445,6 +1473,35 @@ export declare class MemoryEngine {
|
|
|
1445
1473
|
* registry THROWS fail-closed — the same reason an unsupported one never reads as empty.
|
|
1446
1474
|
*/
|
|
1447
1475
|
listMemoryScopes(): Promise<MemoryScopeEnumeration>;
|
|
1476
|
+
/**
|
|
1477
|
+
* design/383 §S-7 (#511 件1) — the HOST's per-session memory-status read face: the data supply
|
|
1478
|
+
* for the "memory capture is off / N committed, M folded" disclosure family and the
|
|
1479
|
+
* resume-visibility answer (state face, not event face — notice dedup semantics are untouched).
|
|
1480
|
+
* A server projects it onto a wire endpoint as a PURE derivation of these keys.
|
|
1481
|
+
*
|
|
1482
|
+
* EVERY key is optional and its ABSENCE means "that fact's source could not be read" — a fault
|
|
1483
|
+
* never coins a `false`/`0` stand-in (the §3.1 fault-transit law):
|
|
1484
|
+
* - `captureOptedOut` — the capture opt-out state. Present `true` (a standing record — with
|
|
1485
|
+
* `optOutSource: "record"`) or present `false` (the store answered and no one-way record
|
|
1486
|
+
* exists — genuine "capture on"). ABSENT ⇔ the record store faulted: the capture state is
|
|
1487
|
+
* INDETERMINATE and `optOutSource: "fault"` says so (the 383 片2/3 `captureIndeterminate`
|
|
1488
|
+
* axis, projected — never a boolean).
|
|
1489
|
+
* - `committedCount` — how many committed entries carry this session's lineage contribution
|
|
1490
|
+
* (the lineage ledger's committed set). Absent ⇔ the ledger is unreadable.
|
|
1491
|
+
* - `foldedCount` — of those, how many appear among the `distilled.inputs` of committed
|
|
1492
|
+
* consolidation products (the lineage × distilled.inputs intersection — "already folded into
|
|
1493
|
+
* long-term memory"). Absent ⇔ the ledger, the scope enumeration (a backend without
|
|
1494
|
+
* `listScopes` cannot name the header universe), or the product read is unreadable.
|
|
1495
|
+
* - `lastCaptureAt` — the newest `lastAt` over this session's lineage contributions (ms epoch).
|
|
1496
|
+
* CHEAP by construction — it rides the SAME single ledger read as `committedCount`, so it is
|
|
1497
|
+
* carried rather than dropped; absent ⇔ the ledger is unreadable OR the session has no
|
|
1498
|
+
* committed contribution at all (there is no such moment to name — the one key whose absence
|
|
1499
|
+
* also covers "no fact exists", stated here so consumers need not guess).
|
|
1500
|
+
*
|
|
1501
|
+
* Side-effect-free committed reads throughout (capture face + lineage sidecar + audit-face
|
|
1502
|
+
* headers/getByIds); never throws — an unreadable source is an absent key, which IS the answer.
|
|
1503
|
+
*/
|
|
1504
|
+
sessionMemoryStatus(sessionId: string): Promise<SessionMemoryStatus>;
|
|
1448
1505
|
/** The committed, side-effect-free audit read: the adoption-restricted committed view when the
|
|
1449
1506
|
* backend offers one (zero-copy File — ledger+shadow, no disk adoption), else the non-adopting
|
|
1450
1507
|
* retrieval view (copy-out File), else the backend itself (Pg/TiDB shapes — naturally committed
|