@sema-agent/core 7.24.1 → 7.24.2

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 CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 7.24.2 — 2026-09-20
6
+
7
+ ### A query whose separators are not the deployment's has no names to speak (#974; P1; security-facing narrowing of a rendered string; no wire key; @server @cli @client-core)
8
+ - **Before.** `displayUrlOf` withholds every query VALUE and keeps every NAME — names ride so that a refusal about two roots differing only in their query cannot read "targets X … paired with X". But `&` and `=` are the query's separators only while no VALUE carries its own. A value holding an un-encoded URL owns them: the split lands where that value says, and whatever ends up left of an `=` was printed as a name. `?next=https://u:&pw@h=` printed `pw`; `?a=1&https://u:sekret@h/=x` printed the whole embedded URL, password included.
9
+ - **Two fixes were tried and measured away before this one.** Requiring names to be identifier-SHAPED fails on three spellings whose tails are perfect identifiers (`…u:&SeCrEt123=x@h`, and its `-`/`_` variants). Testing the raw query for the markers `://` and `@` is a PROXY, and an adversarial round produced `?next=https%3A%2F%2Fu%3A&SeCrEt123=x%40h` — neither marker present literally, `SeCrEt123` printed — with double encoding behind it.
10
+ - **After — purity, asked directly.** A query is PURE iff **every** `name=value` pair, **decoded to its fixed point** (≤3 rounds; a malformed escape stops the decode and leaves its `%` in place), is plain text: `[A-Za-z0-9._~[\]-+,*!'()$ ]`, with a non-empty name. One impure pair and the whole query renders as a single `?«redacted»` with no names at all, and the refusal's parenthetical (`queryDifferenceNote`, the SECOND exit — it used to list what the split left in the name position) reports **positions** instead: "the query differs at segments 2, 3 of 3", or a count comparison when the lengths differ (SEGMENTS, because the count is of `&`-separated segments — a bare token and a trailing separator are segments and not parameters). A segment with no `=` renders as before — a nameless placeholder — but it is JUDGED: a third adversarial round showed that exempting it leaves the hole open (`?next=user&SeCrEt123=x&y@example.com` — the `@` lands in the nameless segment, the second segment then reads as a plain pair whose NAME is the credential, measured). A bare token cannot name anything; it can prove the split was not the deployment's.
11
+ - **Why purity rather than a marker.** The proxy asks "does this look like it contains a URL"; purity asks the question the split actually depends on, at every encoding depth, and needs no list of the shapes structure can take. The character set is generous where a real deployment lives (`?q=hello world` from `%20` or `+`, `?sort=-created_at,id`, `?filter[name]=x` and its `%5B` spelling) and strict exactly where a split goes wrong: where an embedded URL is what mis-split the query, its own prefix pair carries `:`/`/`/`@` and fails the test.
12
+ - **Version context, stated exactly.** 7.24.0 made the **userinfo** position strictly safer (the whole userinfo drops; 7.23.5 still rendered the username) and left the **query name** position looser for a value carrying an un-encoded URL. This release closes the EMBEDDED-URL class of that. It does not close — and cannot — a bare secret that carries its own `&` and `=` (`?password=hunter&SeCrEt=123` is byte-identical to two ordinary parameters): `KL-974b` records it with the deployment discipline (percent-encode a secret written into a query value) and a READ-form pin so nobody reads the purity law as covering it.
13
+ - **The cost.** A legitimate structured value now costs its query the names as well: `?redirect_uri=https://x/cb` and `?email=a@b.com` render as one placeholder at any encoding depth, and the operator gets the differing segment's POSITION instead of its name. `KL-974a` records the trade; `KL-950a`'s "a parameter's NAME is not withheld" item is rewritten to this rule; `KL-974c` accepts the position note's segment-equality disclosure; `KL-974d` records two parse shapes the servable gate deliberately does not second-guess; `KL-974e` records that the tree-wide guarantee covers `src/` only.
14
+ - **Reach.** `displayUrlOf` is exported and its output rides `RouteRefusalDetail.{entryBaseUrl, requestBaseUrl, configBaseUrl}`, `routeRefusalText`/`fixHint`, and the `route.base_url_changed_key_unchanged` notice. No key, code, posture or shape changes; one rendered string gets stricter, and the renderer never throws (a throwing renderer is one a caller wraps in a fallback, and the fallback is the leaking path — pinned). `test/backlog974-display-url-query-ambiguity.test.ts` pins both exits, the measured leak spellings including the partially- and doubly-encoded ones, the decode bound and malformed escapes, the position/count note in both forms, and the plain-text positive controls a downstream consumer named.
15
+
16
+ ### A lost response is not a logic error (#964; P2; wire VALUE flip on `extras.error.retryable`; @cli @client-core)
17
+ - **Before.** `SUBAGENT_ERROR_KIND_BY_CODE` carried rows for the transport codes and nothing for the three IN-BAND terminal-output codes, so `stream_torn` / `refusal` / `length_empty` fell through to `logic` together with `auth`, `output.*` and every code the table does not recognize. That verdict does not stay inside the engine: it rides the delegation card as `extras.error.error_kind` / `extras.error.retryable`, which is what a host renderer shows and a retry scheduler reads. A child whose response was **dropped in transit** — a stream that ended with no content and no finish/stop reason — told its parent "re-delegating cannot fix this".
18
+ - **After.** Each of the three has a row of its own. `stream_torn` → the transient `network` class (`retryable: true`): a lost response is the textbook re-issuable failure. `refusal` (the provider's policy plane withheld the output) and `length_empty` (the output cap was spent before any answer text) → `logic` (`retryable: false`) — the same verdict the fall-through produced, with the difference that it is now DECIDED: a code with a row is classified, a code without one is merely unrecognized.
19
+ - **The fix is in the TABLE, not at a seat.** A special case at one reader would have left the same defect for the next code, and there are three readers (the delivery card, the background notification faces, the team seat). Rules: +3 rows, −1 class of silent mis-verdicts; no reader changed.
20
+ - **Reach — a value flip on an existing key.** `extras.error.{error_kind, retryable}` are unchanged in shape and presence; for a `stream_torn` child they now read `network` / `true` instead of `logic` / `false`. `@cli` / `@client-core` render that pair — a "not retryable" badge on a dropped stream becomes "retryable". `@server` has no read point (it forwards the card), so nothing to change there. `team.ts`'s `canDifferOnRetry` is deliberately **unchanged**: it stops re-dispatch only for terminals judged deterministic (`blocked`, and a code the governance table adjudicated terminal), and reading `retryable` directly there would hang "never retry" on the classifier's else-arms, which is the defect #941 exists to prevent. `test/backlog964-subagent-inband-error-class.test.ts` pins the card through a real child run (report text and `details`), the untouched governance/else-arm readings, and the #941 narrowing.
21
+ - **One hardening from the adversarial round.** The code→class lookup read the table with a plain index, so a terminal code spelled `constructor` or `toString` (a terminal code is a string from a host runner's own taxonomy, not only from the brains' closed prefix list) resolved a PROTOTYPE member and classified the child as a function value with `retryable: undefined` on its card. It now asks own-property membership, the same reading the governance table has always used.
22
+ - `docs/KNOWN-LIMITS.md` KL-942d records the defect and its fix rather than being deleted.
23
+
24
+ ### Joining an endpoint onto a request root is a URL operation, not a string one (#961; P2; narrowing: a base URL may no longer carry a fragment; additive export `joinRequestUrl`; @server @cli @client-core)
25
+ - **Before.** Four builders composed `${root}${suffix}` by hand — the three first-party brains (`/chat/completions`, `/v1/messages`, `/responses`) and the memory-distillation chat seat. For a root with no query that is exactly what a URL join produces, which is why it survived. For the root a gateway deployment really declares — `https://h/v1?api-version=2024-06-01`, the Azure shape — the suffix landed **inside the last query value** (`api-version=2024-06-01/chat/completions`) and the request went to `/v1`. For a root carrying a fragment it was worse: `fetch` never sends one, so the whole suffix landed in the discarded part and every call silently hit the root's own path. `assertServableRequestRoot` accepted both spellings and returned the raw text, which is how they reached the builders (registered under 7.24.0 as "not fixed here").
26
+ - **After.** One `joinRequestUrl(root, suffix)` beside the rest of the request-root law: the suffix joins the **path** with exactly one separator, the **query** rides byte-preserved after it (a deployment declares it once for every request), and a **fragment** is refused rather than composed. The refusal is where the value arrives — `assertServableRequestRoot`, so it fires at brain construction for `config.baseUrl`, at request build for `Model.baseUrl`, and at the distillation seat's construction — and it is re-asked inside the join, so a root that reached a builder by some other path cannot compose a wrong URL in silence. The refusal **names the part and withholds its content**: implicit-OAuth puts a bearer token in exactly that position, and the display spelling drops fragments outright, so the sentence spells `#…` instead of rendering it.
27
+ - **The key's fold followed the wire, as it must.** `routeKeyOf` now also folds the trailing slashes of the **path**: once a suffix joins the path, `https://h/v1/?tenant=a` and `https://h/v1?tenant=a` are byte-identical requests, and a key that still called them two endpoints would refuse a correctly paired credential over a difference the wire cannot see. The invariant **two roots share a key iff their requests are the same request** is re-pinned as a table against the real composition (`test/route-adjudication.test.ts`), and its one registered exception — a root CARRYING a fragment — is **closed**: there is no longer a pair to disagree about, because the root is refused before any request is composed. The key still folds fragments, which now reaches only the advisory read face (it judges a root without asking whether that root is servable at all, exactly as it tolerates an unparseable spelling).
28
+ - **One fold rule, after an adversarial round found two.** `resolveRequestRoot` stripped trailing slashes off the SERIALIZED root and `routeKeyOf` did the same before parsing — which eats the last byte of a query VALUE. Measured: a deployment declaring `https://h/v1?tenant=a/` sent `tenant=a`, while a caller using the exported `joinRequestUrl` directly sent `tenant=a/`; and the key equated the two, so a deployment credential could be judged `paired` with a root that targets a different endpoint. The fold now lives in one place (`canonicalRootSpelling`: trailing slashes belong to the PATH, the query rides byte-for-byte) and the resolver, the key and the join all answer with it. Rules: three normalizations become one.
29
+ - **A suffix that leaves the root is refused, and it is VERIFIED rather than screened.** The exported join re-reads the composed URL with the same parser `fetch` uses and requires the result to still be under the root's path: `joinRequestUrl("https://h/a", "/../responses")` and its `%2e%2e` spelling both normalize to `https://h/responses` and are now `TypeError`s. A blacklist would have had to enumerate `..`, `%2e%2e`, a backslash and whatever the next revision of the URL standard folds; the re-read asks the only question that matters — does the request still target the root the pairing law verified a credential against. The three brains pass fixed literals and are unaffected; the check exists because the function is exported and a custom brain's suffix is not fixed. Positive controls (an ENCODED slash, a single dot, a doubled separator) still compose.
30
+ - **Reach — narrowing.** A deployment whose `config.baseUrl` / `Model.baseUrl` carries a `#fragment` now fails loudly at construction or request build instead of silently calling the wrong path; there is no compensating knob, because every such call was already going to the wrong place. A deployment whose root carries a **query** keeps working and now actually reaches its endpoint. Additive export `joinRequestUrl` (+1, tier advanced); no wire frame key; no change to the pairing verdicts, postures or refusal codes. `test/backlog961-request-url-join.test.ts` pins the four builders on the captured request URL of a real `fetchImpl` (not a helper's return value), the two refusal seats, the "no site concatenates any more" structural half (five files — the custom-brain EXAMPLE is a site too, and it cannot teach a law it composes around), and the join's corners (trailing slashes, empty query, `%2F`, IPv6 authority).
31
+
32
+ ### The checkpoint version ladder is one declaration table (#959; P2; no wire key; no export change)
33
+ - **Before.** "How high a version does this row need" was answered by three hand-written ternary ladders (the resource park, the plan-review park, the approval park — 13 conditional nodes over eight per-feature constants), each a copy of the precedence between features, and only the approval ladder had the real-approval arm because only there is the bit in scope. Adding a feature meant a new constant, a new arm in every ladder, a hand-raised ceiling, and a resume rung comparing against the new constant — the same precedent replayed seven times, with v4 burnt along the way.
34
+ - **After.** `CHECKPOINT_FEATURES` is the one table: a row per feature that ever raised the format, carrying its version, its READ-face mark (`recordedOn`: the persisted member whose presence says the feature was written on a row) and its WRITE-face obligation (`writes`) as two separate columns, plus where it came from. Minting is `checkpointVersionFor(presence)` — the highest version among the features a site declares present; the presence record has a MANDATORY key per non-baseline row, so a new row is a compile error at each of the three mint sites until answered. The eight exported constants are unchanged in name and value and now read the rows (`= CHECKPOINT_FEATURES.<row>.version`); the ceiling is the top row (a tripwire pins `max(rows) === MAX_SUPPORTED_CHECKPOINT_VERSION`). The resume screen's version-keyed rungs compare against the rows and probe presence through `row.recordedOn`; the four rule bodies and their order stay in the screen (two rule families — sectioned features with a forgery arm, declared features without one — and the F-012 rung's seat after the re-supply checks are properties of the screen, not of a row). v4 is a deliberate gap in the table: the retired axis is refused by FIELD at any version, as before.
35
+ - **Reach.** Zero behavior change: every stamped number and every resume verdict is what the ladders produced (`test/backlog959-checkpoint-feature-table.test.ts` keeps the three ladders and the version-keyed rungs verbatim as its reference — 64 presence combinations for the mint, a 27 000-row version × payload matrix through the real policy lane and checkpoint screen for the resume). No export added or removed; the table and `checkpointVersionFor` are module exports (no downstream consumer needs them). `CheckpointFeature` is registered as a `keys:` closed set. Rules: 13 ladder nodes → 0; the feature precedence spelled three times → the version numbers themselves; the hand-raised ceiling and baseline → derived. Recorded for the next feature: above v9 the read-face section is no longer implied by the stamp, so v10 must carry a mark of its own (or the row a per-feature presence record) — the law is in the table's doc.
36
+
3
37
  ## 7.24.1 — 2026-09-19
4
38
 
5
39
  ### The model-role vocabulary ships its runtime twin (#965; P3; additive export `MODEL_ROLES`; @server @client-core)
@@ -362,6 +362,9 @@ const SUBAGENT_ERROR_KIND_BY_CODE = {
362
362
  server: "network",
363
363
  network: "network",
364
364
  "limits.max_walltime_exceeded": "timeout",
365
+ stream_torn: "network",
366
+ refusal: "logic",
367
+ length_empty: "logic",
365
368
  };
366
369
  const SUBAGENT_RETRYABLE = {
367
370
  rate_limit: true,
@@ -377,7 +380,7 @@ export function classifySubagentError(terminal) {
377
380
  const code = terminal.code;
378
381
  if (code === undefined)
379
382
  return { errorKind: "unknown", retryable: SUBAGENT_RETRYABLE.unknown };
380
- const own = SUBAGENT_ERROR_KIND_BY_CODE[code];
383
+ const own = Object.prototype.hasOwnProperty.call(SUBAGENT_ERROR_KIND_BY_CODE, code) ? SUBAGENT_ERROR_KIND_BY_CODE[code] : undefined;
381
384
  if (own !== undefined)
382
385
  return { errorKind: own, retryable: SUBAGENT_RETRYABLE[own] };
383
386
  const governanceClass = governanceRetryClass(code);
@@ -7,7 +7,7 @@ import { mintFallbackToolCallId } from "./tool-call-id.js";
7
7
  import { emitBrainTelemetry, reportReasoningWireFacts } from "./status-sink.js";
8
8
  import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, modelSupportsVision, sendableImages } from "./media-degrade.js";
9
9
  import { ANTHROPIC_RESERVED, OUTPUT_CAP_KEYS, applyExtraBody, effectiveOutputCap, lockHeader, mergeHeaders, takeHeaderCasefold } from "./request-params.js";
10
- import { adjudicateModelRoute, applyRouteCredentialHeaders, assertServableRequestRoot, createBrainRouteJudge, resolveRequestRoot, resolveRouteCredential, routeRefusalText } from "./route-adjudicator.js";
10
+ import { adjudicateModelRoute, applyRouteCredentialHeaders, assertServableRequestRoot, createBrainRouteJudge, joinRequestUrl, resolveRequestRoot, resolveRouteCredential, routeRefusalText } from "./route-adjudicator.js";
11
11
  import { MIN_THINKING_TOKENS, budgetCapSkipsThinking, declaredEffortLevels, reasoningBudgetShare, reasoningRequestCarried, resolveEffort } from "./reasoning.js";
12
12
  import { runStreamingBrain } from "./stream-engine.js";
13
13
  function thinkingBudget(maxTokens, share, fixed, hardCap = false) {
@@ -347,7 +347,7 @@ export function createAnthropicBrain(config = {}) {
347
347
  : "config-default";
348
348
  const wire = applyExtraBody(body, model.extraBody, ANTHROPIC_RESERVED);
349
349
  return {
350
- url: `${root}/v1/messages`,
350
+ url: joinRequestUrl(root, "/v1/messages"),
351
351
  headers,
352
352
  body: JSON.stringify(wire),
353
353
  outputCapTokens: effectiveOutputCap(wire, OUTPUT_CAP_KEYS.anthropic),
@@ -14,10 +14,12 @@ export declare const ZERO_USAGE: Usage;
14
14
  *
15
15
  * The last three are IN-BAND terminal-output codes: stamped on errored assistant MESSAGES
16
16
  * (never thrown as BrainError) so `TaskResult.errorCode` covers the error family end-to-end and
17
- * downstream consumers can bucket without string-matching. Deliberately behavior-neutral everywhere
18
- * else: the circuit breaker's default countCodes excludes them (model-output problems, not provider
19
- * outages), degradation maps them to nothing, and subagent retry classification keeps them in the
20
- * non-retryable "logic" bucket — exactly how these previously-uncoded messages already behaved.
17
+ * downstream consumers can bucket without string-matching. Behavior-neutral on the transport side:
18
+ * the circuit breaker's default countCodes excludes them (model-output problems, not provider
19
+ * outages) and degradation maps them to nothing. Subagent retry classification gives each of them a
20
+ * ROW of its own (`SUBAGENT_ERROR_KIND_BY_CODE`): a lost stream is the transient `network` class, a
21
+ * policy refusal and a spent output cap are `logic`. They rode the `logic` fall-through together
22
+ * until then, which told a parent "do not re-delegate" about a dropped response.
21
23
  * - `stream_torn` — the stream ended with no content and no finish/stop reason (response lost)
22
24
  * - `refusal` — policy refusal / provider content filter withheld the output
23
25
  * - `length_empty` — max_tokens exhausted with no answer text (pairs with errorKind "length_empty") */
@@ -6,7 +6,7 @@ import { mintFallbackToolCallId } from "./tool-call-id.js";
6
6
  import { emitBrainTelemetry } from "./status-sink.js";
7
7
  import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, imagesOmittedNoVisionNote, modelSupportsVision, sendableImages } from "./media-degrade.js";
8
8
  import { OUTPUT_CAP_KEYS, RESPONSES_RESERVED, applyExtraBody, effectiveOutputCap, lockHeader, mergeHeaders } from "./request-params.js";
9
- import { adjudicateModelRoute, applyRouteCredentialHeaders, assertServableRequestRoot, createBrainRouteJudge, resolveRequestRoot, resolveRouteCredential, routeRefusalText } from "./route-adjudicator.js";
9
+ import { adjudicateModelRoute, applyRouteCredentialHeaders, assertServableRequestRoot, createBrainRouteJudge, joinRequestUrl, resolveRequestRoot, resolveRouteCredential, routeRefusalText } from "./route-adjudicator.js";
10
10
  import { declaredOffSpelling, mintEffortWireValue, reasoningRequestCarried } from "./reasoning.js";
11
11
  import { runStreamingBrain } from "./stream-engine.js";
12
12
  const DEGENERATE_POLL_CHARS = 64;
@@ -286,7 +286,7 @@ export function createOpenResponsesBrain(config = {}) {
286
286
  headers["authorization"] = `Bearer ${apiKey}`;
287
287
  const wire = applyExtraBody(body, model.extraBody, RESPONSES_RESERVED);
288
288
  return {
289
- url: `${root}/responses`,
289
+ url: joinRequestUrl(root, "/responses"),
290
290
  headers,
291
291
  body: JSON.stringify(wire),
292
292
  outputCapTokens: effectiveOutputCap(wire, OUTPUT_CAP_KEYS.responses),
@@ -7,7 +7,7 @@ import { emitBrainTelemetry } from "./status-sink.js";
7
7
  import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, imagesOmittedNoVisionNote, modelSupportsVision, sendableImages } from "./media-degrade.js";
8
8
  import { OPENAI_RESERVED, OUTPUT_CAP_KEYS, applyExtraBody, effectiveOutputCap, lockHeader, mergeHeaders } from "./request-params.js";
9
9
  import { BrainError } from "./errors.js";
10
- import { adjudicateModelRoute, applyRouteCredentialHeaders, assertServableRequestRoot, createBrainRouteJudge, resolveRequestRoot, resolveRouteCredential, routeRefusalText } from "./route-adjudicator.js";
10
+ import { adjudicateModelRoute, applyRouteCredentialHeaders, assertServableRequestRoot, createBrainRouteJudge, joinRequestUrl, resolveRequestRoot, resolveRouteCredential, routeRefusalText } from "./route-adjudicator.js";
11
11
  import { declaredOffSpelling, mintEffortWireValue, reasoningRequestCarried } from "./reasoning.js";
12
12
  import { runStreamingBrain } from "./stream-engine.js";
13
13
  function closeToolCallAccum(acc) {
@@ -346,7 +346,7 @@ export function createOpenAIBrain(config = {}) {
346
346
  headers["authorization"] = `Bearer ${apiKey}`;
347
347
  const wire = applyExtraBody(body, model.extraBody, OPENAI_RESERVED);
348
348
  return {
349
- url: `${root}/chat/completions`,
349
+ url: joinRequestUrl(root, "/chat/completions"),
350
350
  headers,
351
351
  body: JSON.stringify(wire),
352
352
  outputCapTokens: effectiveOutputCap(wire, OUTPUT_CAP_KEYS.openai),
@@ -42,9 +42,10 @@ import type { RouteAdjudication, RouteCredential, RoutePairingConfig, RoutePairi
42
42
  import type { Brain, EngineNotice } from "../core/types.js";
43
43
  /** A base URL's ROUTE KEY — the value the pairing law COMPARES, folded to WIRE equivalence: the
44
44
  * halves the wire itself treats as case-insensitive (scheme and host, RFC 9110) fold, the scheme's
45
- * default port elides, and PATH and QUERY ride byte-preserved (a query selects the endpoint as
46
- * surely as a path does; folding either would compare two different endpoints equal — the widening
47
- * this module exists to stop). An unparseable value keeps its raw trimmed spelling: fail-closed raw
45
+ * default port elides, and PATH and QUERY ride byte-preserved apart from the trailing slashes the
46
+ * request composition itself folds (a query selects the endpoint as surely as a path does; folding
47
+ * anything the wire keeps would compare two different endpoints equal — the widening this module
48
+ * exists to stop). An unparseable value keeps its raw trimmed spelling: fail-closed raw
48
49
  * comparison. Non-string input throws — see the module note on garbage.
49
50
  *
50
51
  * **Never render this.** Byte-preserving on the query means `?api_key=<secret>` rides it verbatim;
@@ -61,29 +62,56 @@ export declare function routeKeyOf(url: string | undefined): string;
61
62
  * - a BARE token (`?<token>`) renders as one placeholder with NO name: `URLSearchParams` calls it a
62
63
  * name with an empty value, and that reading put the payload itself on screen. "Every value is
63
64
  * withheld" protects only what IS a value, so the raw segment decides what rides.
65
+ * - a name rides only where {@link readQuery} finds the query PURE. A query holding any structured
66
+ * value — a URL, an e-mail address, anything that is not plain text once decoded — owns the `&`
67
+ * and `=` the split relies on, so the bytes left of an `=` are not names and are never spoken:
68
+ * such a query renders as ONE placeholder. A deployment's ordinary names (`tenant`, `api-version`,
69
+ * `x-request-id`, `filter[name]`, `x.y`) are unaffected, which is what keeps a refusal actionable.
64
70
  * Answers `""` for an undeclared root, like its key twin. */
65
71
  export declare function displayUrlOf(url: string | undefined): string;
66
72
  /**
67
- * The bad-value refusal every base-URL seat owes: **a request root must be an absolute http(s) URL, and
68
- * it may not carry the credential itself.**
73
+ * The bad-value refusal every base-URL seat owes: **a request root must be an absolute http(s) URL, it
74
+ * may not carry the credential itself, and it may not carry a fragment.**
69
75
  *
70
- * Both halves fail the same way if they are not caught here — as a TRANSPORT failure, indistinguishable
76
+ * The first two fail the same way if they are not caught here — as a TRANSPORT failure, indistinguishable
71
77
  * from an outage, retried for the whole backoff ladder. Measured: `file:`/`ftp:` reject with
72
78
  * `TypeError("fetch failed")` over a causeless "unknown scheme", the exact shell a refused connection uses.
73
79
  * Stripping userinfo instead of refusing would send an UNAUTHENTICATED request to the host the operator
74
- * thought they had authenticated to. Refused where the value ARRIVES — construction for `config.baseUrl`,
75
- * request build for `Model.baseUrl`. An undeclared root is not this rule's question; the PAIRING law keeps
76
- * its tolerance for an unparseable spelling ({@link routeKeyOf}). Answers the value it judged.
80
+ * thought they had authenticated to. A FRAGMENT fails worse than loudly: `fetch` never sends one, so a root
81
+ * carrying it silently targets the root's own path and every endpoint suffix appended after it lands inside
82
+ * the discarded part (`https://h/v1#note` + `/chat/completions` → a request to `/v1`). Refused where the
83
+ * value ARRIVES — construction for `config.baseUrl`, request build for `Model.baseUrl` — and again where a
84
+ * request URL is composed ({@link joinRequestUrl}), so a root that reached a builder by some other path
85
+ * cannot compose a wrong URL in silence. An undeclared root is not this rule's question; the PAIRING law
86
+ * keeps its tolerance for an unparseable spelling ({@link routeKeyOf}). Answers the value it judged.
77
87
  */
78
88
  export declare function assertServableRequestRoot(url: string | undefined, seat: string): string | undefined;
89
+ /**
90
+ * The request URL itself: a request ROOT joined with the endpoint SUFFIX its lane speaks. The one
91
+ * composition — `${root}${suffix}` put the suffix wherever the root's last character happened to leave it,
92
+ * which for a root carrying a query (`https://h/v1?api-version=2024-06-01`, the gateway shape a deployment
93
+ * really declares) appended it INSIDE the last query VALUE and sent the request to `/v1`.
94
+ *
95
+ * The rule, stated once for all four builders: the suffix joins the **path** (exactly one separator,
96
+ * whatever trailing slashes the root carries); the **query** rides byte-preserved AFTER it, because a query
97
+ * selects the endpoint as surely as a path does and a deployment declares it once for every request; a
98
+ * **fragment** never reaches here ({@link assertServableRequestRoot} refuses the root, re-asked here so a
99
+ * root that arrived by some other path is refused rather than composed).
100
+ *
101
+ * Composed from the same parse `fetch` itself would make, so the spelling carries no normalization the wire
102
+ * would not have done anyway. A suffix carrying its own `?`/`#` is a programmer error, not an input: the
103
+ * query belongs to the root, and the suffix names a path.
104
+ */
105
+ export declare function joinRequestUrl(root: string | undefined, suffix: string): string;
79
106
  /** Remove the userinfo from every authority in free text, WHOLE — the general redactor keeps a short or
80
107
  * unprefixed username, and `TOKEN@host` / `TOKEN:x-oauth-basic@host` are the canonical PAT spellings. */
81
108
  export declare function stripUrlCredentials(text: string): string;
82
109
  /** The request ROOT a brain targets — `Model.baseUrl` else `config.baseUrl` else the lane's default,
83
- * trailing slashes stripped — with {@link assertServableRequestRoot} applied to both declared
84
- * halves on the way. The three brains each carried a copy of this coalesce-and-strip line, so a rule
85
- * about base URLs had three homes; now it has one. Answers `""` when nothing is declared and the lane
86
- * has no default — whether that is a refusal is the caller's. */
110
+ * in its canonical spelling ({@link canonicalRootSpelling}: trailing PATH slashes folded, query
111
+ * byte-preserved) — with {@link assertServableRequestRoot} applied to both declared halves on the way.
112
+ * The three brains each carried a copy of this coalesce-and-strip line, so a rule about base URLs had
113
+ * three homes; now it has one. Answers `""` when nothing is declared and the lane has no default —
114
+ * whether that is a refusal is the caller's. */
87
115
  export declare function resolveRequestRoot(modelBaseUrl: string | undefined, configBaseUrl: string | undefined, laneDefault?: string): string;
88
116
  /** Whether a header bag carries an auth-bearing header (any case). */
89
117
  export declare function hasAuthCarrier(headers: Record<string, string> | undefined): boolean;
@@ -28,21 +28,51 @@ export function routeKeyOf(url) {
28
28
  if (typeof url !== "string") {
29
29
  throw new TypeError(`route pairing: baseUrl must be a string (got ${typeof url})`);
30
30
  }
31
- const raw = url.trim().replace(/\/+$/, "");
31
+ const raw = url.trim();
32
32
  if (raw === "")
33
33
  return "";
34
34
  const u = servableParse(raw);
35
35
  if (u === undefined)
36
- return raw;
37
- return `${u.protocol}//${u.host}${u.pathname}${u.search}`.replace(/\/+$/, "");
36
+ return raw.replace(/\/+$/, "");
37
+ return canonicalRootSpelling(u);
38
38
  }
39
- function querySegments(search) {
39
+ function canonicalRootSpelling(u) {
40
+ return `${u.protocol}//${u.host}${u.pathname.replace(/\/+$/, "")}${u.search}`;
41
+ }
42
+ const QUERY_PURE_TEXT = /^[A-Za-z0-9._~[\]\-+,*!'()$ ]*$/;
43
+ const QUERY_DECODE_ROUNDS = 3;
44
+ function decodedToFixedPoint(text) {
45
+ let cur = text;
46
+ for (let round = 0; round < QUERY_DECODE_ROUNDS; round++) {
47
+ if (!cur.includes("%"))
48
+ return cur;
49
+ let next;
50
+ try {
51
+ next = decodeURIComponent(cur);
52
+ }
53
+ catch {
54
+ return cur;
55
+ }
56
+ if (next === cur)
57
+ return cur;
58
+ cur = next;
59
+ }
60
+ return cur;
61
+ }
62
+ function readQuery(search) {
40
63
  if (search === "" || search === "?")
41
- return [];
42
- return search.replace(/^\?/, "").split("&").map((raw) => {
64
+ return { pure: true, segments: [] };
65
+ const segments = search.replace(/^\?/, "").split("&").map((raw) => {
43
66
  const eq = raw.indexOf("=");
44
67
  return eq === -1 ? { raw } : { raw, name: raw.slice(0, eq), value: raw.slice(eq + 1) };
45
68
  });
69
+ const pure = segments.every((sg) => {
70
+ if (sg.name === undefined)
71
+ return QUERY_PURE_TEXT.test(decodedToFixedPoint(sg.raw));
72
+ const name = decodedToFixedPoint(sg.name);
73
+ return name !== "" && QUERY_PURE_TEXT.test(name) && QUERY_PURE_TEXT.test(decodedToFixedPoint(sg.value ?? ""));
74
+ });
75
+ return { pure, segments };
46
76
  }
47
77
  export function displayUrlOf(url) {
48
78
  const key = routeKeyOf(url);
@@ -56,8 +86,12 @@ export function displayUrlOf(url) {
56
86
  const shown = at === -1 ? head : `${REDACTED_VALUE}@${head.slice(at + 1)}`;
57
87
  return cut === -1 ? shown : `${shown}${key[cut]}${REDACTED_VALUE}`;
58
88
  }
59
- const segs = querySegments(u.search);
60
- const query = segs.length === 0 ? "" : `?${segs.map((sg) => (sg.raw === "" ? "" : sg.name === undefined ? REDACTED_VALUE : `${sg.name}=${REDACTED_VALUE}`)).join("&")}`;
89
+ const read = readQuery(u.search);
90
+ const query = read.segments.length === 0
91
+ ? ""
92
+ : !read.pure
93
+ ? `?${REDACTED_VALUE}`
94
+ : `?${read.segments.map((sg) => (sg.raw === "" ? "" : sg.name === undefined ? REDACTED_VALUE : `${sg.name}=${REDACTED_VALUE}`)).join("&")}`;
61
95
  return `${u.protocol}//${u.host}${u.pathname}${query}`.replace(/\/+$/, "");
62
96
  }
63
97
  function queryDifferenceNote(requestKey, declaredKey) {
@@ -67,8 +101,19 @@ function queryDifferenceNote(requestKey, declaredKey) {
67
101
  const ua = composableParse(requestKey);
68
102
  const ub = composableParse(declaredKey);
69
103
  if (ua !== undefined && ub !== undefined) {
70
- const a = querySegments(ua.search);
71
- const b = querySegments(ub.search);
104
+ const ra = readQuery(ua.search);
105
+ const rb = readQuery(ub.search);
106
+ if (!ra.pure || !rb.pure) {
107
+ if (ra.segments.length !== rb.segments.length) {
108
+ return ` (same rendering — the query differs in segment count (${ra.segments.length} vs ${rb.segments.length}); a value here is not plain text, so this line withholds every name)`;
109
+ }
110
+ const at = ra.segments.flatMap((sg, i) => (sg.raw === rb.segments[i]?.raw ? [] : [i + 1]));
111
+ return at.length === 0
112
+ ? ` (same rendering — the difference is in a part this line withholds or folds)`
113
+ : ` (same rendering — the query differs at segment${at.length > 1 ? "s" : ""} ${at.join(", ")} of ${ra.segments.length}; a value here is not plain text, so this line withholds every name)`;
114
+ }
115
+ const a = ra.segments;
116
+ const b = rb.segments;
72
117
  const valuesOf = (segs, n) => segs.filter((sg) => sg.name === n).map((sg) => sg.value).join("\u0000");
73
118
  for (const n of new Set([...a, ...b].flatMap((sg) => (sg.name === undefined ? [] : [sg.name])))) {
74
119
  if (valuesOf(a, n) !== valuesOf(b, n))
@@ -80,6 +125,9 @@ function queryDifferenceNote(requestKey, declaredKey) {
80
125
  : ` (same rendering — the difference is in a part this line withholds or folds)`;
81
126
  }
82
127
  export function assertServableRequestRoot(url, seat) {
128
+ return judgeRequestRoot(url, seat)?.trimmed;
129
+ }
130
+ function judgeRequestRoot(url, seat) {
83
131
  if (typeof url !== "string" || url.trim() === "")
84
132
  return undefined;
85
133
  const trimmed = url.trim();
@@ -90,19 +138,50 @@ export function assertServableRequestRoot(url, seat) {
90
138
  `or a spelling that is not a URL at all, fails at the transport in a shape indistinguishable from an ` +
91
139
  `outage, so it would be retried for the whole backoff ladder instead of refused here.`);
92
140
  }
93
- if (parsed.username === "" && parsed.password === "")
94
- return trimmed;
95
- throw new BrainError("invalid_request", `${seat} carries credentials in its authority (${parsed.protocol}//${REDACTED_CREDENTIALS}@${parsed.host}${parsed.pathname}) — ` +
96
- `a request cannot be built from a URL with userinfo, so every call through this brain would fail: put the ` +
97
- `credential on the apiKey knob or an auth header and leave the URL bare`);
141
+ if (parsed.username !== "" || parsed.password !== "") {
142
+ throw new BrainError("invalid_request", `${seat} carries credentials in its authority (${parsed.protocol}//${REDACTED_CREDENTIALS}@${parsed.host}${parsed.pathname}) — ` +
143
+ `a request cannot be built from a URL with userinfo, so every call through this brain would fail: put the ` +
144
+ `credential on the apiKey knob or an auth header and leave the URL bare`);
145
+ }
146
+ if (parsed.hash !== "") {
147
+ throw new BrainError("invalid_request", `${seat} carries a URL fragment (${displayUrlOf(trimmed)}#…) — a fragment is never sent on the wire, so the ` +
148
+ `request would silently target the root's own path and the endpoint suffix would land inside the discarded ` +
149
+ `part: drop everything from the "#" on. A per-deployment QUERY is kept and rides every request ` +
150
+ `(a gateway's \`?api-version=…\` is supported); a fragment cannot be.`);
151
+ }
152
+ return { trimmed, parsed };
153
+ }
154
+ export function joinRequestUrl(root, suffix) {
155
+ if (typeof suffix !== "string" || /[?#]/.test(suffix)) {
156
+ throw new TypeError(`request URL: the endpoint suffix must be a path (got ${typeof suffix === "string" ? JSON.stringify(suffix) : typeof suffix}) — ` +
157
+ `a request's query belongs to its ROOT, which declares it once for every request`);
158
+ }
159
+ const judged = judgeRequestRoot(root, "the request root");
160
+ if (judged === undefined) {
161
+ throw new BrainError("invalid_request", `no request root is declared, so no request URL can be built for "${suffix}" — set config.baseUrl or Model.baseUrl`);
162
+ }
163
+ const { parsed } = judged;
164
+ const tail = suffix.replace(/^\/+/, "");
165
+ if (tail === "")
166
+ return canonicalRootSpelling(parsed);
167
+ const base = parsed.pathname.replace(/\/+$/, "");
168
+ const composed = `${parsed.protocol}//${parsed.host}${base}/${tail}${parsed.search}`;
169
+ const reread = servableParse(composed);
170
+ if (reread === undefined || !reread.pathname.startsWith(base === "" ? "/" : `${base}/`)) {
171
+ throw new TypeError(`request URL: the endpoint suffix ${JSON.stringify(suffix)} does not stay under the request root's path ` +
172
+ `(${base === "" ? "/" : base}) — a suffix names an endpoint of that root, and one that walks out of it would ` +
173
+ `send the request, and the credential paired with that root, somewhere the pairing law never judged`);
174
+ }
175
+ return composed;
98
176
  }
99
177
  export function stripUrlCredentials(text) {
100
178
  return text.replace(/([A-Za-z][A-Za-z0-9+.\-]*:\/\/)[^/\s"'@]*@/g, `$1${REDACTED_CREDENTIALS}@`);
101
179
  }
102
180
  export function resolveRequestRoot(modelBaseUrl, configBaseUrl, laneDefault = "") {
103
- const entry = assertServableRequestRoot(modelBaseUrl, "Model.baseUrl");
104
- const declared = assertServableRequestRoot(configBaseUrl, "config.baseUrl");
105
- return (entry ?? declared ?? laneDefault).replace(/\/+$/, "");
181
+ const entry = judgeRequestRoot(modelBaseUrl, "Model.baseUrl");
182
+ const declared = judgeRequestRoot(configBaseUrl, "config.baseUrl");
183
+ const chosen = (entry ?? declared)?.parsed ?? servableParse(laneDefault.trim());
184
+ return chosen === undefined ? laneDefault : canonicalRootSpelling(chosen);
106
185
  }
107
186
  export function hasAuthCarrier(headers) {
108
187
  if (!headers)
@@ -1288,110 +1288,135 @@ export type SerializedCheckpointState = {
1288
1288
  [K in keyof Required<CheckpointState>]: CheckpointState[K];
1289
1289
  };
1290
1290
  /**
1291
- * The BASELINE {@link Checkpoint} schema version (design/49 §3, code-ready council round-2): the
1292
- * shape a plain human-approval suspend carried before any feature raised the format. It is NOT the highest
1293
- * a resume accepts — that is {@link MAX_SUPPORTED_CHECKPOINT_VERSION}, which the per-feature stamps
1294
- * (RESOURCE / BINDING / TOKEN / ORG_ADMISSION) have each raised on their own, deliberately without moving
1295
- * this constant. Resume stays forward-compatible by `<=` against MAX, never `===`. Every live mint site
1296
- * stamps one of the feature versions, so nothing in-tree produces this value today; it is kept as the
1297
- * floor of the version ladder and as the reading for a row written before any feature raised the format.
1298
- */
1299
- export declare const CURRENT_CHECKPOINT_VERSION = 1;
1300
- /** design/74 R3-B: the schema version a `resource_limit` suspend stamps (v2 — it adds `resourceLedger` and a
1301
- * `resource_limit` `pendingAction`/`gate` an old worker can't handle). An old worker (`MAX_SUPPORTED`=1)
1302
- * rejects it pre-CAS (stays `pending`, retryable on a new worker). */
1303
- export declare const RESOURCE_CHECKPOINT_VERSION = 2;
1304
- /**
1305
- * design/80 D-1 (version-skew downgrade fix): the schema version a **binding-bearing** human/irreversible_ask
1306
- * `tool_approval` suspend stamps. These checkpoints carry the decision-action binding (`boundCallId` +
1307
- * `boundInputHash`) whose enforcement lives ENTIRELY in the resuming worker's resume path. A pre-D-1 worker
1308
- * (released 1.100.0: `MAX_SUPPORTED`=2, and its resume code has ZERO binding logic) would otherwise resume a
1309
- * D-1-minted v1 checkpoint and execute the pending tool with NO decision-action verification — the exact
1310
- * "approve vendor-A $5 → execute vendor-B $5000" circumvent the binding exists to prevent (council BLOCKER #1).
1311
- * Stamping these at **v3 (> the old worker's MAX of 2)** forces a pre-D-1 worker to reject them PRE-CAS
1312
- * (`unsupported_version`, stays `pending`, retried on a binding-enforcing worker) instead of silently voiding
1313
- * the binding. A resource_limit suspend keeps stamping v2; a pre-binding (legacy 1.100.0) checkpoint is v1.
1314
- */
1315
- export declare const BINDING_CHECKPOINT_VERSION = 3;
1316
- /**
1317
- * design/164 (the BINDING_CHECKPOINT_VERSION precedent replayed a third time): the schema version a suspend
1318
- * stamps when its ledger CARRIES a cross-slice TOKEN allocation (`resourceLedger.totalTokens` set). The
1319
- * enforcement — window clamp against the remaining allocation, exhausted-resume fail-fast — lives ENTIRELY
1320
- * in the resuming worker, so a pre-164 worker (MAX_SUPPORTED=4) would accept the row, ignore
1321
- * `spentTokens` as a bound, and grant a fresh full window. Stamping v5 forces it to reject PRE-CAS
1322
- * (`unsupported_version`, stays `pending`, retried on an enforcing worker). A ledger with NO token total
1323
- * keeps the historic stamps (v2 resource / v3 binding) — deployments not using it see zero version movement.
1291
+ * The checkpoint FEATURE TABLE — one row per feature that ever raised the row format. A row carries the
1292
+ * feature's schema version, the persisted mark by which a reader tells the feature was written on a row
1293
+ * (`recordedOn`, the READ face), and what a mint owes when the feature is present (`writes`, the WRITE
1294
+ * face — the two faces are deliberately separate columns). Minting is derived from the rows
1295
+ * ({@link checkpointVersionFor}: the highest version among the features present), the per-feature
1296
+ * constants below ARE the rows' versions, and the resume screen reads the rows — so adding a feature is
1297
+ * one row here plus an explicit answer at each mint site (the presence record's keys are mandatory).
1324
1298
  *
1325
- * v4 (the retired wall-clock total) is deliberately NOT reused: this worker still READS v4 rows, and one
1326
- * whose ledger carries the retired axis is refused with its own typed code (see
1327
- * {@link CheckpointError} `checkpoint.walltime_axis_retired`) instead of silently resuming under a bound
1328
- * nobody enforces any more.
1329
- */
1330
- export declare const TOKEN_CHECKPOINT_VERSION = 5;
1331
- /**
1332
- * Org-memory admission state (falsification-style round 2, 2026-08-05 — the BINDING_CHECKPOINT_VERSION
1333
- * precedent replayed a fourth time): the schema version a suspend stamps when its state carries
1334
- * org-admission freeze fields (`inheritedGate.admittedOrgScopes` / `ownAdmittedOrgScopes` /
1335
- * `ownAdmittedOrgWriteScope` — the adjudicated-empty record included). The enforcement — the
1336
- * session/delegation freeze on resume, the tenant-admission door itself — lives ENTIRELY in the
1337
- * resuming worker, so a pre-admission worker (MAX_SUPPORTED=5) would accept the row, ignore the
1338
- * freeze fields, and remount the raw org scopes with no admission at all. Stamping v6 forces it to
1339
- * reject PRE-CAS (`unsupported_version`, stays `pending`, retried on an enforcing worker). A
1340
- * checkpoint with NO org-admission state keeps the historic stamps — deployments not using org
1341
- * memory see zero version movement.
1342
- */
1343
- export declare const ORG_ADMISSION_CHECKPOINT_VERSION = 6;
1344
- /**
1345
- * F-012 (2026-08-09 — the ORG_ADMISSION_CHECKPOINT_VERSION precedent replayed a fifth time
1346
- * falsification-style finding): the schema version a suspend stamps when its state carries the F-012 durable
1347
- * enforcement fields — `inheritedGate.constraintChain`/`constraintDigest` (the frozen ancestor
1348
- * projection chain the resume-edit re-adjudication executes) or `delegationProvenance` (the monotonic
1349
- * provenance aggregate a resume re-seeds). Both enforcements live ENTIRELY in the resuming worker: a
1350
- * pre-F-012 worker (MAX_SUPPORTED=6) would accept the row, ignore constraintChain (so an approver-edited
1351
- * resume executes WITHOUT the frozen ancestor deny), and restart the aggregate clean (false-clean after
1352
- * pre-suspend external activity). Stamping v7 forces such a worker to reject PRE-CAS
1353
- * (`unsupported_version`, stays `pending`, retried on an enforcing worker). A checkpoint with NO F-012
1354
- * state keeps the historic stamps — deployments not delegating see zero version movement.
1355
- */
1356
- export declare const F012_CHECKPOINT_VERSION = 7;
1357
- /**
1358
- * #130/#120 (2026-08-10 — the F012_CHECKPOINT_VERSION precedent replayed a sixth time): the schema
1359
- * version a suspend stamps when its gate carries {@link RealApprovalGateBit}. The enforcement lives
1360
- * ENTIRELY in the resuming worker (the no-org-wiring refusal, the org_unavailable resume-belt
1361
- * distinction): a pre-131 worker (MAX_SUPPORTED=7) would accept the row, ignore `realApproval`, and
1362
- * either redeem a governed row with no org wiring at all (#120's exact hole, replayed through version
1363
- * skew) or burn the approval on the unavailable belt the bit exists to soften. Stamping v8 forces it
1364
- * to reject PRE-CAS (`unsupported_version`, stays `pending`, retried on an enforcing worker). A gate
1365
- * with NO `realApproval` keeps the historic stamps. NOTE (erratum 2026-08-10, downstream-measured):
1366
- * "zero version movement for ungoverned deployments" is TOO WIDE — the two always-mounted integrity
1367
- * policies (transcript-integrity, unverifiable-delete) mint `requiresRealApproval` asks, so a park on
1368
- * one of THOSE stamps v8 with `origin:"policy"` on ANY durable deployment, org-governed or not. What
1369
- * stays true: rows whose gate carries no realApproval bit keep their historic stamps everywhere.
1370
- */
1371
- export declare const REAL_APPROVAL_CHECKPOINT_VERSION = 8;
1372
- /**
1373
- * design/199 (2026-08-13 — the precedent replayed a seventh time): the schema version a suspend
1374
- * stamps when its state carries the READ-FACE section ({@link CheckpointState.readFace} — the
1375
- * resolved open face and/or deny-set additions). The enforcement lives ENTIRELY in the resuming
1376
- * worker (the stricter-wins face fold + the deny-entry union at prepare): a pre-199 worker
1377
- * (MAX_SUPPORTED=8) would accept the row, ignore the section, and resume the leg under its own
1378
- * defaults — silently DROPPING the frozen deny additions (a widening), and re-deriving the face
1379
- * from live config alone. Stamping v9 forces it to reject PRE-CAS (`unsupported_version`, stays
1380
- * `pending`, retried on an enforcing worker). A row whose task ran the default posture (face roots,
1381
- * no additions) carries NO section and keeps the historic stamps — such a resume on ANY worker
1382
- * folds to roots + built-ins, which is exactly the semantics it was cut under.
1299
+ * Because the stamp is a maximum, the version records only the HIGHEST feature on a row; whether a lower
1300
+ * feature is present is said by that feature's own mark, never by the number. A resume rule keyed on the
1301
+ * version alone is therefore exact to the version at which its feature was the top: `realApproval` is
1302
+ * version-recorded at v8 and marker-recorded (`readFace.realApproval`) from v9 on. The same law binds the
1303
+ * NEXT feature: once a row can be stamped above v9, "v9+ ⇒ read-face section present" no longer follows
1304
+ * from the stamp, and the face needs a mark of its own (or the row a per-feature presence record).
1305
+ *
1306
+ * v4 is a GAP, deliberately: the retired cross-slice wall-clock allocation. This worker still reads v4
1307
+ * rows; a ledger carrying the retired axis is refused by FIELD (`walltime_axis_retired`) at any version.
1383
1308
  */
1384
- export declare const FACE_CHECKPOINT_VERSION = 9;
1385
- /** The highest {@link Checkpoint.version} `runner.resume` will act on; a higher one is rejected pre-CAS with
1386
- * {@link CheckpointError} `unsupported_version` (the checkpoint stays `pending`, retryable on a newer worker).
1387
- * Raised to 8 for realApproval-bearing gates — this worker reads v1 (legacy human), v2 (resource), v3
1388
- * (binding human/irreversible_ask), v4 (a pre-164 row, refused only when it carries the retired
1389
- * wall-clock allocation), v5 (token-allocation-bearing resource/approval), v6 (org-admission freeze
1390
- * state), v7 (F-012 constraint-chain / delegation-provenance enforcement state), v8 (a
1391
- * non-budgetable `realApproval` gate bit with its org-origin resume semantics), v9 (the
1392
- * design/199 read-face section: resolved face + deny-set additions, folded stricter-wins on
1393
- * resume). */
1394
- export declare const MAX_SUPPORTED_CHECKPOINT_VERSION = 9;
1309
+ export declare const CHECKPOINT_FEATURES: {
1310
+ /** The baseline shape: a plain human-approval suspend before any feature raised the format. No live
1311
+ * mint stamps it today; it is the floor of the ladder and the reading of a row written before any feature. */
1312
+ readonly baseline: {
1313
+ readonly version: 1;
1314
+ readonly introduced: "design/49";
1315
+ readonly recordedOn: (_cp: Checkpoint) => boolean;
1316
+ readonly writes: "the row itself";
1317
+ };
1318
+ /** A `resource_limit` suspend: `resourceLedger` plus a `resource_limit` gate/pendingAction an older worker
1319
+ * cannot handle (it rejects pre-CAS; the row stays `pending`, retryable on a newer worker). */
1320
+ readonly resource: {
1321
+ readonly version: 2;
1322
+ readonly introduced: "design/74";
1323
+ readonly recordedOn: (cp: Checkpoint) => boolean;
1324
+ readonly writes: "resourceLedger; gate/pendingAction kind resource_limit";
1325
+ };
1326
+ /** A HUMAN-DECISION park (tool approval or plan_review) of the binding era: the decision-action binding
1327
+ * (`boundCallId` + `boundInputHash` on a tool_approval) is enforced ENTIRELY by the resuming worker, so a
1328
+ * pre-binding worker must reject the row rather than execute the pending tool unverified. Like `resource`,
1329
+ * this row is a park-kind FLOOR: a plan_review park carries it with no binding (it was introduced above it);
1330
+ * whether a tool_approval row carries its hash is the resume binding rung's own conjunction. */
1331
+ readonly binding: {
1332
+ readonly version: 3;
1333
+ readonly introduced: "design/80";
1334
+ readonly recordedOn: (cp: Checkpoint) => boolean;
1335
+ readonly writes: "pendingAction.boundCallId + boundInputHash on a tool_approval; a plan_review carries the stamp only";
1336
+ };
1337
+ /** A ledger carrying a cross-slice TOKEN allocation: the window clamp and the exhausted-resume fail-fast
1338
+ * live in the resuming worker. A ledger with no token total keeps the historic stamps. */
1339
+ readonly token: {
1340
+ readonly version: 5;
1341
+ readonly introduced: "design/164";
1342
+ readonly recordedOn: (cp: Checkpoint) => boolean;
1343
+ readonly writes: "resourceLedger.totalTokens";
1344
+ };
1345
+ /** Org-memory admission freeze state (the adjudicated-empty record included): the session/delegation
1346
+ * freeze on resume and the tenant-admission door live in the resuming worker. */
1347
+ readonly orgAdmission: {
1348
+ readonly version: 6;
1349
+ readonly introduced: "org-admission";
1350
+ readonly recordedOn: (cp: Checkpoint) => boolean;
1351
+ readonly writes: "inheritedGate.admittedOrgScopes / ownAdmittedOrgScopes / ownAdmittedOrgWriteScope / orgAdmissionGoverned";
1352
+ };
1353
+ /** The F-012 durable enforcement fields: the frozen ancestor projection chain + its digest (one write, the
1354
+ * resume-edit re-adjudication executes it) and/or the delegation-provenance aggregate a resume re-seeds. */
1355
+ readonly f012: {
1356
+ readonly version: 7;
1357
+ readonly introduced: "F-012";
1358
+ readonly recordedOn: (cp: Checkpoint) => boolean;
1359
+ readonly writes: "inheritedGate.constraintChain + constraintDigest together; delegationProvenance";
1360
+ };
1361
+ /** A gate carrying {@link RealApprovalGateBit}: the no-org-wiring refusal and the org_unavailable resume
1362
+ * belt live in the resuming worker. Minted at ONE site (the approval park); the two always-mounted
1363
+ * integrity policies mint it on any durable deployment, org-governed or not. */
1364
+ readonly realApproval: {
1365
+ readonly version: 8;
1366
+ readonly introduced: "#130/#120";
1367
+ readonly recordedOn: (cp: Checkpoint) => boolean;
1368
+ readonly writes: "gate.realApproval (a well-formed bit) + pendingAction.requiresRealApproval; from v9: readFace.realApproval = true beside it";
1369
+ };
1370
+ /** The READ-face section ({@link CheckpointState.readFace}): resolved face + normalized deny-set additions,
1371
+ * written only for a non-default posture; the stricter-wins fold lives in the resuming worker. Carries the
1372
+ * redundant governance marker for `realApproval` (see the table note above). Absent ⇒ default posture. */
1373
+ readonly face: {
1374
+ readonly version: 9;
1375
+ readonly introduced: "design/199";
1376
+ readonly recordedOn: (cp: Checkpoint) => boolean;
1377
+ readonly writes: "state.readFace { face, denyEntries? } for a non-default posture; realApproval: true only at the mint whose gate carries the bit (a hands-less re-park never carries the seed's marker forward)";
1378
+ };
1379
+ };
1380
+ /** One row of {@link CHECKPOINT_FEATURES}. */
1381
+ export interface CheckpointFeatureRow {
1382
+ readonly version: number;
1383
+ /** Where the feature came from — a design or ticket tag, documentation only. */
1384
+ readonly introduced: string;
1385
+ /** READ face: true iff the row carries the feature's persisted mark. */
1386
+ readonly recordedOn: (cp: Checkpoint) => boolean;
1387
+ /** WRITE face: what a mint must write when the feature is present (prose; the mint sites are the code). */
1388
+ readonly writes: string;
1389
+ }
1390
+ /** The feature words — the row keys of {@link CHECKPOINT_FEATURES}. */
1391
+ export type CheckpointFeature = keyof typeof CHECKPOINT_FEATURES;
1392
+ /** The BASELINE {@link Checkpoint} schema version — the `baseline` row. NOT the highest a resume accepts (that
1393
+ * is {@link MAX_SUPPORTED_CHECKPOINT_VERSION}); resume stays forward-compatible by `<=` against MAX, never `===`. */
1394
+ export declare const CURRENT_CHECKPOINT_VERSION: 1;
1395
+ /** The `resource` row's version. */
1396
+ export declare const RESOURCE_CHECKPOINT_VERSION: 2;
1397
+ /** The `binding` row's version. */
1398
+ export declare const BINDING_CHECKPOINT_VERSION: 3;
1399
+ /** The `token` row's version. v4 (the retired wall-clock total) is deliberately not reused. */
1400
+ export declare const TOKEN_CHECKPOINT_VERSION: 5;
1401
+ /** The `orgAdmission` row's version. */
1402
+ export declare const ORG_ADMISSION_CHECKPOINT_VERSION: 6;
1403
+ /** The `f012` row's version. */
1404
+ export declare const F012_CHECKPOINT_VERSION: 7;
1405
+ /** The `realApproval` row's version. */
1406
+ export declare const REAL_APPROVAL_CHECKPOINT_VERSION: 8;
1407
+ /** The `face` row's version. */
1408
+ export declare const FACE_CHECKPOINT_VERSION: 9;
1409
+ /** The highest {@link Checkpoint.version} `runner.resume` will act on — the top row of the table; a higher one
1410
+ * is rejected pre-CAS with {@link CheckpointError} `unsupported_version` (the row stays `pending`, retryable on
1411
+ * a newer worker). This is the CEILING entrance: the storage-faithfulness probe stamps it directly on a
1412
+ * synthetic row that is never resumed, and does not go through feature detection. */
1413
+ export declare const MAX_SUPPORTED_CHECKPOINT_VERSION: 9;
1414
+ /** What a mint site answers for: every row but the baseline, every key MANDATORY — a new row is a compile
1415
+ * error at each mint site until that site says whether its rows carry the feature. */
1416
+ export type CheckpointFeaturePresence = Record<Exclude<CheckpointFeature, "baseline">, boolean>;
1417
+ /** The schema version a mint stamps: the highest version among the features PRESENT on the row, the baseline
1418
+ * when none is. One rule for every mint site — the precedence between features is the version numbers. */
1419
+ export declare function checkpointVersionFor(presence: CheckpointFeaturePresence): number;
1395
1420
  /**
1396
1421
  * Read a checkpoint's schema version, defaulting an absent field to **legacy `0`** (a 1.67-era checkpoint
1397
1422
  * written before the field existed — it carries no `workspaceHandle`, so resuming it the v1 way is safe).
@@ -186,15 +186,39 @@ export function realApprovalOrgFact(bit) {
186
186
  return bit.org;
187
187
  return bit.origin === "org_rule" ? "rule" : bit.origin === "org_unavailable" ? "unavailable" : undefined;
188
188
  }
189
- export const CURRENT_CHECKPOINT_VERSION = 1;
190
- export const RESOURCE_CHECKPOINT_VERSION = 2;
191
- export const BINDING_CHECKPOINT_VERSION = 3;
192
- export const TOKEN_CHECKPOINT_VERSION = 5;
193
- export const ORG_ADMISSION_CHECKPOINT_VERSION = 6;
194
- export const F012_CHECKPOINT_VERSION = 7;
195
- export const REAL_APPROVAL_CHECKPOINT_VERSION = 8;
196
- export const FACE_CHECKPOINT_VERSION = 9;
197
- export const MAX_SUPPORTED_CHECKPOINT_VERSION = 9;
189
+ export const CHECKPOINT_FEATURES = {
190
+ baseline: { version: 1, introduced: "design/49", recordedOn: (_cp) => true, writes: "the row itself" },
191
+ resource: { version: 2, introduced: "design/74", recordedOn: (cp) => cp.gate.kind === "resource_limit", writes: "resourceLedger; gate/pendingAction kind resource_limit" },
192
+ binding: { version: 3, introduced: "design/80", recordedOn: (cp) => cp.pendingAction.kind === "tool_approval" || cp.pendingAction.kind === "plan_review", writes: "pendingAction.boundCallId + boundInputHash on a tool_approval; a plan_review carries the stamp only" },
193
+ token: { version: 5, introduced: "design/164", recordedOn: (cp) => cp.resourceLedger?.totalTokens !== undefined, writes: "resourceLedger.totalTokens" },
194
+ orgAdmission: {
195
+ version: 6,
196
+ introduced: "org-admission",
197
+ recordedOn: (cp) => cp.state.inheritedGate?.admittedOrgScopes !== undefined || cp.state.inheritedGate?.ownAdmittedOrgScopes !== undefined || cp.state.inheritedGate?.ownAdmittedOrgWriteScope !== undefined || cp.state.inheritedGate?.orgAdmissionGoverned === true,
198
+ writes: "inheritedGate.admittedOrgScopes / ownAdmittedOrgScopes / ownAdmittedOrgWriteScope / orgAdmissionGoverned",
199
+ },
200
+ f012: { version: 7, introduced: "F-012", recordedOn: (cp) => cp.state.inheritedGate?.constraintChain !== undefined || cp.state.delegationProvenance !== undefined, writes: "inheritedGate.constraintChain + constraintDigest together; delegationProvenance" },
201
+ realApproval: { version: 8, introduced: "#130/#120", recordedOn: (cp) => cp.gate.kind === "irreversible_ask" && cp.gate.realApproval !== undefined, writes: "gate.realApproval (a well-formed bit) + pendingAction.requiresRealApproval; from v9: readFace.realApproval = true beside it" },
202
+ face: { version: 9, introduced: "design/199", recordedOn: (cp) => cp.state.readFace !== undefined, writes: "state.readFace { face, denyEntries? } for a non-default posture; realApproval: true only at the mint whose gate carries the bit (a hands-less re-park never carries the seed's marker forward)" },
203
+ };
204
+ export const CURRENT_CHECKPOINT_VERSION = CHECKPOINT_FEATURES.baseline.version;
205
+ export const RESOURCE_CHECKPOINT_VERSION = CHECKPOINT_FEATURES.resource.version;
206
+ export const BINDING_CHECKPOINT_VERSION = CHECKPOINT_FEATURES.binding.version;
207
+ export const TOKEN_CHECKPOINT_VERSION = CHECKPOINT_FEATURES.token.version;
208
+ export const ORG_ADMISSION_CHECKPOINT_VERSION = CHECKPOINT_FEATURES.orgAdmission.version;
209
+ export const F012_CHECKPOINT_VERSION = CHECKPOINT_FEATURES.f012.version;
210
+ export const REAL_APPROVAL_CHECKPOINT_VERSION = CHECKPOINT_FEATURES.realApproval.version;
211
+ export const FACE_CHECKPOINT_VERSION = CHECKPOINT_FEATURES.face.version;
212
+ export const MAX_SUPPORTED_CHECKPOINT_VERSION = CHECKPOINT_FEATURES.face.version;
213
+ export function checkpointVersionFor(presence) {
214
+ let version = CHECKPOINT_FEATURES.baseline.version;
215
+ for (const feature of Object.keys(CHECKPOINT_FEATURES)) {
216
+ const row = CHECKPOINT_FEATURES[feature];
217
+ if (presence[feature] === true && row.version > version)
218
+ version = row.version;
219
+ }
220
+ return version;
221
+ }
198
222
  export function checkpointVersionOf(cp) {
199
223
  return cp.version ?? 0;
200
224
  }
@@ -1,5 +1,5 @@
1
1
  import { readMember, thrownText } from "../thrown-value.js";
2
- import { assertServableRequestRoot } from "../../brain/route-adjudicator.js";
2
+ import { assertServableRequestRoot, joinRequestUrl } from "../../brain/route-adjudicator.js";
3
3
  export const DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT = 32;
4
4
  const SIZE_CLAUSE = {
5
5
  "dl-2": "- the index should be as small as it can be made without losing either of the above.",
@@ -146,7 +146,7 @@ export function openAiCompatChatSeat(seat) {
146
146
  throw distillerConfigRefusal("memory consolidation driver chat seat needs a non-empty baseUrl.");
147
147
  let requestRoot;
148
148
  try {
149
- requestRoot = (assertServableRequestRoot(seat.baseUrl, "memory consolidation driver chat seat baseUrl") ?? "").replace(/\/+$/, "");
149
+ requestRoot = assertServableRequestRoot(seat.baseUrl, "memory consolidation driver chat seat baseUrl") ?? "";
150
150
  }
151
151
  catch (e) {
152
152
  throw distillerConfigRefusal(thrownText(e), e);
@@ -157,7 +157,7 @@ export function openAiCompatChatSeat(seat) {
157
157
  throw distillerConfigRefusal(`memory consolidation driver chat seat needs an EXPLICIT model id, got ${JSON.stringify(seat.model)} — an alias can be re-pointed upstream and would move the driver with no trace in the plan.`);
158
158
  }
159
159
  return async (req) => {
160
- const res = await fetch(`${requestRoot}/chat/completions`, {
160
+ const res = await fetch(joinRequestUrl(requestRoot, "/chat/completions"), {
161
161
  method: "POST",
162
162
  headers: { "content-type": "application/json", authorization: `Bearer ${seat.apiKey}` },
163
163
  body: JSON.stringify({
@@ -1,5 +1,5 @@
1
1
  import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
2
- import { BINDING_CHECKPOINT_VERSION, debitLedger, F012_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, mintCheckpointId, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, } from "../checkpoint-store.js";
2
+ import { checkpointVersionFor, debitLedger, mintCheckpointId, mintCheckpointToken, } from "../checkpoint-store.js";
3
3
  import { defaultTaskRegistry } from "../task-registry.js";
4
4
  import { checkpointScopeOf } from "./checkpoint-scope.js";
5
5
  import { DEFAULT_RESOURCE_TTL_MS, publishCommittedSuspend, sanitizedTtlMs, USAGE_WINDOW_REAP_MARGIN_MS } from "./park-commit.js";
@@ -47,7 +47,7 @@ export function prepareBoundaryParks(input) {
47
47
  const cp = {
48
48
  token,
49
49
  checkpointId: mintCheckpointId(),
50
- version: faceCheckpointState() ? FACE_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : resourceLedgerOut.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : RESOURCE_CHECKPOINT_VERSION,
50
+ version: checkpointVersionFor({ resource: true, binding: false, token: resourceLedgerOut.totalTokens !== undefined, orgAdmission: orgAdmissionCheckpointState(), f012: f012CheckpointState(), realApproval: false, face: faceCheckpointState() }),
51
51
  scope,
52
52
  sessionId,
53
53
  leafId,
@@ -131,7 +131,7 @@ export function prepareBoundaryParks(input) {
131
131
  const cp = {
132
132
  token,
133
133
  checkpointId: mintCheckpointId(),
134
- version: faceCheckpointState() ? FACE_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : reviewLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
134
+ version: checkpointVersionFor({ resource: false, binding: true, token: reviewLedger.totalTokens !== undefined, orgAdmission: orgAdmissionCheckpointState(), f012: f012CheckpointState(), realApproval: false, face: faceCheckpointState() }),
135
135
  scope,
136
136
  sessionId,
137
137
  leafId,
@@ -3,7 +3,7 @@ import { Value } from "typebox/value";
3
3
  import { ASK_USER_QUESTION_TOOL_NAME, classifyQuestionOutcome, validateAskQuestions } from "../ask-question.js";
4
4
  import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
5
5
  import { boundInputHashOf } from "../canonical-json.js";
6
- import { BINDING_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, encodeAtFidelity, F012_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, mintCheckpointId, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, resolveDeclaredFidelity, samePlainValue, TOKEN_CHECKPOINT_VERSION, } from "../checkpoint-store.js";
6
+ import { buildRiskDescriptor, checkpointVersionFor, debitLedger, encodeAtFidelity, mintCheckpointId, mintCheckpointToken, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
7
7
  import { pauseOf } from "../pause-registry.js";
8
8
  import { askCarryRowMembers } from "../hooks.js";
9
9
  import { defaultTaskRegistry } from "../task-registry.js";
@@ -272,7 +272,7 @@ export function prepareParkAsk(input) {
272
272
  cp = {
273
273
  token,
274
274
  checkpointId: mintCheckpointId(),
275
- version: faceCheckpointState() ? FACE_CHECKPOINT_VERSION : realApproval !== undefined ? REAL_APPROVAL_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : approvalLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
275
+ version: checkpointVersionFor({ resource: false, binding: true, token: approvalLedger.totalTokens !== undefined, orgAdmission: orgAdmissionCheckpointState(), f012: f012CheckpointState(), realApproval: realApproval !== undefined, face: faceCheckpointState() }),
276
276
  scope,
277
277
  sessionId,
278
278
  leafId,
@@ -1,26 +1,27 @@
1
1
  import { persistedReadDenyEntryProblem } from "../../tools/fs/read-deny.js";
2
2
  import { isAskOrigin } from "../ask-origin.js";
3
- import { CheckpointError, checkpointVersionOf, F012_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, realApprovalOrgFact } from "../checkpoint-store.js";
3
+ import { CHECKPOINT_FEATURES, CheckpointError, checkpointVersionOf, MAX_SUPPORTED_CHECKPOINT_VERSION, realApprovalOrgFact } from "../checkpoint-store.js";
4
4
  import { constraintChainDigest, constraintChainEntryOfSuppliedLayer } from "../tool-policy.js";
5
5
  export function resumeCheckpointScreen(input) {
6
6
  const { cp, resumeSignal, internals, runner } = input;
7
+ const { realApproval: realApprovalRow, face: faceRow, f012: f012Row } = CHECKPOINT_FEATURES;
7
8
  if (checkpointVersionOf(cp) > MAX_SUPPORTED_CHECKPOINT_VERSION) {
8
9
  throw new CheckpointError("checkpoint.unsupported_version", `checkpoint version ${checkpointVersionOf(cp)} is newer than this worker supports (max ${MAX_SUPPORTED_CHECKPOINT_VERSION})`, { reason: "version_newer" });
9
10
  }
10
11
  const preCasGateBit = cp.gate.kind === "irreversible_ask" ? cp.gate.realApproval : undefined;
11
12
  const preCasBitWellFormed = preCasGateBit !== undefined && typeof preCasGateBit === "object" && isAskOrigin(preCasGateBit.origin);
12
13
  const faceSaysGoverned = (cp.state.readFace?.realApproval) === true;
13
- const preCasBitViolation = checkpointVersionOf(cp) === REAL_APPROVAL_CHECKPOINT_VERSION
14
+ const preCasBitViolation = checkpointVersionOf(cp) === realApprovalRow.version
14
15
  ? !preCasBitWellFormed
15
- : checkpointVersionOf(cp) < REAL_APPROVAL_CHECKPOINT_VERSION
16
+ : checkpointVersionOf(cp) < realApprovalRow.version
16
17
  ? preCasGateBit !== undefined
17
18
  : faceSaysGoverned
18
19
  ? !preCasBitWellFormed
19
20
  : preCasGateBit !== undefined && !preCasBitWellFormed;
20
21
  if (preCasBitViolation) {
21
- throw new CheckpointError("checkpoint.invalid_outcome", checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION
22
- ? `a v${checkpointVersionOf(cp)} checkpoint's realApproval gate bit must be well-formed (origin a member of the closed ask-origin set)${checkpointVersionOf(cp) === REAL_APPROVAL_CHECKPOINT_VERSION ? " and present on an irreversible_ask gate" : ""} — this row's is not; refusing to resume a damaged real-approval row (corruption / downgrade guard), the checkpoint stays pending`
23
- : `a v${checkpointVersionOf(cp)} checkpoint carries a realApproval gate bit no release of that version ever minted — refusing to honor a fabricated origin (corruption / forgery guard), the checkpoint stays pending`, { reason: checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION ? "real_approval_damaged" : "real_approval_forged" });
22
+ throw new CheckpointError("checkpoint.invalid_outcome", checkpointVersionOf(cp) >= realApprovalRow.version
23
+ ? `a v${checkpointVersionOf(cp)} checkpoint's realApproval gate bit must be well-formed (origin a member of the closed ask-origin set)${checkpointVersionOf(cp) === realApprovalRow.version ? " and present on an irreversible_ask gate" : ""} — this row's is not; refusing to resume a damaged real-approval row (corruption / downgrade guard), the checkpoint stays pending`
24
+ : `a v${checkpointVersionOf(cp)} checkpoint carries a realApproval gate bit no release of that version ever minted — refusing to honor a fabricated origin (corruption / forgery guard), the checkpoint stays pending`, { reason: checkpointVersionOf(cp) >= realApprovalRow.version ? "real_approval_damaged" : "real_approval_forged" });
24
25
  }
25
26
  {
26
27
  const faceSectionRaw = cp.state.readFace;
@@ -33,10 +34,10 @@ export function resumeCheckpointScreen(input) {
33
34
  (faceCandidate.denyEntries === undefined ||
34
35
  (Array.isArray(faceCandidate.denyEntries) &&
35
36
  faceCandidate.denyEntries.every((e) => persistedReadDenyEntryProblem(e) === null)));
36
- if (checkpointVersionOf(cp) >= FACE_CHECKPOINT_VERSION ? !faceWellFormed : faceSectionRaw !== undefined) {
37
- throw new CheckpointError("checkpoint.invalid_outcome", checkpointVersionOf(cp) >= FACE_CHECKPOINT_VERSION
37
+ if (checkpointVersionOf(cp) >= faceRow.version ? !faceWellFormed : faceSectionRaw !== undefined) {
38
+ throw new CheckpointError("checkpoint.invalid_outcome", checkpointVersionOf(cp) >= faceRow.version
38
39
  ? `a v${checkpointVersionOf(cp)} checkpoint must carry a well-formed read-face section (state.readFace) — this row's is missing or malformed; refusing to resume a damaged read-face row (corruption / downgrade guard), the checkpoint stays pending`
39
- : `a v${checkpointVersionOf(cp)} checkpoint carries a read-face section no release of that version ever minted — refusing to honor a fabricated read-face state (corruption / forgery guard), the checkpoint stays pending`, { reason: checkpointVersionOf(cp) >= FACE_CHECKPOINT_VERSION ? "read_face_damaged" : "read_face_forged" });
40
+ : `a v${checkpointVersionOf(cp)} checkpoint carries a read-face section no release of that version ever minted — refusing to honor a fabricated read-face state (corruption / forgery guard), the checkpoint stays pending`, { reason: checkpointVersionOf(cp) >= faceRow.version ? "read_face_damaged" : "read_face_forged" });
40
41
  }
41
42
  const writeFaceSectionRaw = cp.state.writeFace;
42
43
  if (writeFaceSectionRaw !== undefined) {
@@ -80,7 +81,7 @@ export function resumeCheckpointScreen(input) {
80
81
  "re-supply would run the resumed leg under a different ancestor chain than it suspended with; " +
81
82
  "rejected pre-CAS (the checkpoint stays pending) — re-resume with the full original chain");
82
83
  }
83
- if (checkpointVersionOf(cp) >= F012_CHECKPOINT_VERSION &&
84
+ if (checkpointVersionOf(cp) >= f012Row.version &&
84
85
  (cp.state.inheritedGate.constraintChain === undefined || cp.state.inheritedGate.constraintDigest === undefined)) {
85
86
  throw new CheckpointError("checkpoint.invalid_outcome", `a v${checkpointVersionOf(cp)} checkpoint that requires parent constraints must carry BOTH the frozen constraintChain and its constraintDigest (they are minted in one write with the version stamp) — this row carries neither or only one; refusing to fall back to the count-only contract on a damaged row (corruption / downgrade guard), the checkpoint stays pending`, { reason: "constraint_chain_missing" });
86
87
  }
@@ -1,5 +1,5 @@
1
1
  import { canonicalizeCapturedPlainData, classifyQuestionOutcome, isLiveQuestionFace } from "../ask-question.js";
2
- import { BINDING_CHECKPOINT_VERSION, CheckpointError, checkpointVersionOf, winnerFromOutcome } from "../checkpoint-store.js";
2
+ import { CHECKPOINT_FEATURES, CheckpointError, checkpointVersionOf, winnerFromOutcome } from "../checkpoint-store.js";
3
3
  import { screenApproverAttribution } from "../tool-policy.js";
4
4
  import { sanitizeUntrustedText } from "../untrusted-text.js";
5
5
  import { assertOutcomeText, describeSuppliedValue, pendingContentAskCallId, sameWinner } from "./decide-continuation.js";
@@ -90,7 +90,7 @@ export function resumePolicyOutcome(input) {
90
90
  throw new CheckpointError("checkpoint.invalid_outcome", "resume boundInputHash does not match the checkpoint's pending tool call input — the decision-action input binding (design/80 D-1 §2) failed; refusing to apply an approval bound to a different input (TOCTOU re-mint guard)", { field: "boundInputHash" });
91
91
  }
92
92
  }
93
- else if (checkpointVersionOf(cp) >= BINDING_CHECKPOINT_VERSION) {
93
+ else if (checkpointVersionOf(cp) >= CHECKPOINT_FEATURES.binding.version) {
94
94
  throw new CheckpointError("checkpoint.invalid_outcome", `resume of a binding-version (v${checkpointVersionOf(cp)}) checkpoint whose pending tool call has no persisted boundInputHash — refusing to resume an unbound binding-version checkpoint (corruption / downgrade guard)`);
95
95
  }
96
96
  if (cp.reopenReason === "env_failed") {
package/dist/index.d.ts CHANGED
@@ -325,7 +325,7 @@ export { createCircuitBreakerBrain, type CircuitBreakerOptions, type BreakerStat
325
325
  export { createDegradingBrain, readDegradation, DEGRADED_DIAGNOSTIC_TYPE, type DegradingBrainOptions, type DegradeReason, type DegradationInfo, } from "./brain/degrading.js";
326
326
  export { retryBackoffMs, parseRetryAfter } from "./brain/retry.js";
327
327
  export { WAITING_FIRST_TOKEN_AFTER_MS, WAITING_FIRST_TOKEN_EVERY_MS } from "./brain/stream-engine.js";
328
- export { adjudicateModelRoute, resolveRouteCredential, routeRefusalText, routePairingStatus, routeKeyOf, displayUrlOf, resolveRequestRoot, hasAuthCarrier, type RoutePairingStatus, } from "./brain/route-adjudicator.js";
328
+ export { adjudicateModelRoute, resolveRouteCredential, routeRefusalText, routePairingStatus, routeKeyOf, displayUrlOf, resolveRequestRoot, joinRequestUrl, hasAuthCarrier, type RoutePairingStatus, } from "./brain/route-adjudicator.js";
329
329
  export type { RouteAdjudication, RouteCredential, RouteCredentialSource, RoutePairingConfig, RoutePairingPosture, RouteRefusalCode, RouteRefusalDetail, } from "./internal/llm.js";
330
330
  export { ROUTE_ADJUDICATION_CONFORMANCE_CORPUS, type RouteAdjudicationVector } from "./brain/route-conformance.js";
331
331
  export { type BrainTimeoutConfig } from "./brain/timeout.js";
package/dist/index.js CHANGED
@@ -279,7 +279,7 @@ export { createCircuitBreakerBrain, CIRCUIT_OPEN_MARKER, } from "./brain/circuit
279
279
  export { createDegradingBrain, readDegradation, DEGRADED_DIAGNOSTIC_TYPE, } from "./brain/degrading.js";
280
280
  export { retryBackoffMs, parseRetryAfter } from "./brain/retry.js";
281
281
  export { WAITING_FIRST_TOKEN_AFTER_MS, WAITING_FIRST_TOKEN_EVERY_MS } from "./brain/stream-engine.js";
282
- export { adjudicateModelRoute, resolveRouteCredential, routeRefusalText, routePairingStatus, routeKeyOf, displayUrlOf, resolveRequestRoot, hasAuthCarrier, } from "./brain/route-adjudicator.js";
282
+ export { adjudicateModelRoute, resolveRouteCredential, routeRefusalText, routePairingStatus, routeKeyOf, displayUrlOf, resolveRequestRoot, joinRequestUrl, hasAuthCarrier, } from "./brain/route-adjudicator.js";
283
283
  export { ROUTE_ADJUDICATION_CONFORMANCE_CORPUS } from "./brain/route-conformance.js";
284
284
  export {} from "./brain/timeout.js";
285
285
  export { createAssistantMessageEventStream } from "./internal/llm.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.24.1",
3
+ "version": "7.24.2",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "_comment": "design/87 L3 — frozen public export surface of src/index.ts (name -> kind). DO NOT edit by hand to silence a red test. A removed/changed entry = a SemVer-BREAKING change; bump MAJOR and update this fixture in the SAME commit (design/87 §4.2 / §5.2). Regenerate via REGEN in test/export-surface.test.ts.",
3
3
  "_tierComment": "#435 v1 — machine-readable layering of the public surface: stable = demonstrated by README.md / src/examples; internal = an `Internal`-marked name or a runner/engine deep-subtree declaration (the model seam src/engine/llm is excluded — it is the BYOM contract, not an engine internal); advanced = a supported export the front door does not walk you through. THIS IS AN INITIAL HEURISTIC, derived mechanically and expected to be refined ticket by ticket: no human reviewed these 1700+ entries one by one, and nothing here claims otherwise. Known bias: a short or English-word export name (ok, err, Result, Usage) can match ordinary prose in README.md and land `stable` on a coincidence. Every export MUST carry a tier — a new export with no row fails the gate in export-surface.test.ts.",
4
- "count": 2398,
4
+ "count": 2399,
5
5
  "exports": {
6
6
  "A2ATaskState": "type",
7
7
  "A2ATaskStateReversal": "type",
@@ -2013,6 +2013,7 @@
2013
2013
  "isolationPermitsAutoAccept": "function",
2014
2014
  "jaccardDistance": "function",
2015
2015
  "joinFrontiers": "function",
2016
+ "joinRequestUrl": "function",
2016
2017
  "joinRuleStates": "function",
2017
2018
  "judgeCrossSessionInbound": "function",
2018
2019
  "judgeParkedToolIdentity": "function",
@@ -4413,6 +4414,7 @@
4413
4414
  "isolationPermitsAutoAccept": "advanced",
4414
4415
  "jaccardDistance": "stable",
4415
4416
  "joinFrontiers": "advanced",
4417
+ "joinRequestUrl": "stable",
4416
4418
  "joinRuleStates": "advanced",
4417
4419
  "judgeCrossSessionInbound": "advanced",
4418
4420
  "judgeParkedToolIdentity": "advanced",
@@ -44,21 +44,7 @@
44
44
  "criteriaCovered": false
45
45
  }
46
46
  ],
47
- "closedSetsUnreadable": [
48
- {
49
- "set": "ModelRole",
50
- "from": "literal",
51
- "to": "declared",
52
- "unreadable": true,
53
- "at": [
54
- "src/core/model-seat.ts:44",
55
- "src/core/roles.ts:6",
56
- "src/core/types.ts:20",
57
- "src/index.ts:243"
58
- ],
59
- "reason": "no longer a spelled-out vocabulary (declared at src/core/model-seat.ts:44, src/core/roles.ts:6, src/core/types.ts:20, src/index.ts:243)"
60
- }
61
- ],
47
+ "closedSetsUnreadable": [],
62
48
  "typeKeys": [],
63
49
  "declarationMoves": [],
64
50
  "mintMoves": [],
@@ -77,9 +63,9 @@
77
63
  ],
78
64
  "server": [],
79
65
  "sdk": [],
80
- "note": "scanned cli@ae64fcaf server@277e6a15 sdk@5ded40c — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it"
66
+ "note": "scanned cli@6f99707c server@277e6a15 sdk@5ded40c — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it"
81
67
  },
82
68
  "breakingCount": 0,
83
69
  "breakingItems": [],
84
- "toResolvedAt": "d552b285c747d987758c66504ec8cba98fab04de"
70
+ "toResolvedAt": "f93e37f388d831881d2ebf4f9c28eca1499896a3"
85
71
  }
@@ -0,0 +1,81 @@
1
+ {
2
+ "repo": "core",
3
+ "version": "7.24.2",
4
+ "from": "f93e37f388d831881d2ebf4f9c28eca1499896a3",
5
+ "to": "v7.24.2",
6
+ "added": [
7
+ {
8
+ "name": "joinRequestUrl",
9
+ "kind": "function",
10
+ "tier": "stable",
11
+ "at": [
12
+ "src/brain/route-adjudicator.ts:370",
13
+ "src/index.ts:3212"
14
+ ],
15
+ "criteriaCovered": false,
16
+ "breaking": false
17
+ }
18
+ ],
19
+ "removed": [],
20
+ "changed": [],
21
+ "narrowed": [
22
+ {
23
+ "id": "F1",
24
+ "source": "behaviorFlips.summary",
25
+ "text": "NARROWING of a rendered string (#974, P1): `displayUrlOf` speaks query NAMES only where the query is PURE — every `name=value` pair decoded to its fixed point (≤3 rounds; a malformed escape leaves its `%`) is plain text `[A-Za-z0-9._~[\\]-+,*!'()$ ]`; any impure pair (a value carrying an un-encoded URL, an e-mail, a bare segment with `@`/`:`/`/`, residual `%`) collapses the WHOLE query to one `«redacted»`. WHO NEEDS IT: a deployment whose gateway root carries a query value with an embedded URL whose password contains `&` — the fragment after `&` and before `=` landed in the NAME position and was rendered verbatim (server report, main reproduced 8 forms, test 3 boundaries). WHO IS AFFECTED: operators reading refusals for roots with a legitimate structured value (`?redirect_uri=https://…`, `?email=a@b.com`) now get the differing SEGMENT position instead of its name."
26
+ },
27
+ {
28
+ "id": "F2",
29
+ "source": "behaviorFlips.summary",
30
+ "text": "NARROWING at construction (#961, P2): a request root may no longer carry a `#fragment` — `assertServableRequestRoot` and `joinRequestUrl` refuse it with a `displayUrlOf`-rendered message that names `#…` without its content; and joining an endpoint onto the root is a URL operation: the suffix joins the PATH, the query rides byte-preserved after it. WHO NEEDS IT: Azure-style gateways (`?api-version=`) whose requests previously went out as `…/v1?api-version=x/chat/completions`. WHO IS AFFECTED: a deployment whose `baseUrl` carried a fragment now fails loudly at construction instead of silently calling the root path."
31
+ }
32
+ ],
33
+ "closedSetMembers": [],
34
+ "closedSetsUnreadable": [],
35
+ "typeKeys": [],
36
+ "declarationMoves": [],
37
+ "mintMoves": [],
38
+ "criteriaObserves": [
39
+ {
40
+ "kind": "taskEventField",
41
+ "name": "tool_end.structured",
42
+ "assertions": [
43
+ "G3.a"
44
+ ]
45
+ },
46
+ {
47
+ "kind": "typeMember",
48
+ "name": "Checkpoint.version",
49
+ "assertions": [
50
+ "G4.a",
51
+ "G4.b"
52
+ ]
53
+ },
54
+ {
55
+ "kind": "typeMember",
56
+ "name": "Model.baseUrl",
57
+ "assertions": [
58
+ "G2.a",
59
+ "G2.b"
60
+ ]
61
+ },
62
+ {
63
+ "kind": "typeMember",
64
+ "name": "RouteRefusalDetail.requestBaseUrl",
65
+ "assertions": [
66
+ "G1.a",
67
+ "G1.b",
68
+ "G1.c"
69
+ ]
70
+ }
71
+ ],
72
+ "consumersHint": {
73
+ "cli": [],
74
+ "server": [],
75
+ "sdk": [],
76
+ "note": "scanned cli@9405bc53 server@5ad9cfda sdk@6fe2f08 — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it"
77
+ },
78
+ "breakingCount": 0,
79
+ "breakingItems": [],
80
+ "toResolvedAt": "111dfebb7ef8517bbb3d2217dfb60277488a010d"
81
+ }