@sema-agent/core 5.47.0 → 5.48.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 +52 -0
- package/dist/agents/agent-transcript-tool.d.ts +4 -0
- package/dist/agents/agent-transcript-tool.js +10 -3
- package/dist/agents/send-message-tool.d.ts +43 -1
- package/dist/agents/send-message-tool.js +50 -11
- package/dist/agents/subagent.d.ts +18 -0
- package/dist/agents/subagent.js +102 -2
- package/dist/config/defaults.d.ts +20 -0
- package/dist/config/defaults.js +5 -0
- package/dist/core/background-agent-store.d.ts +1 -0
- package/dist/core/background-agent-store.js +13 -0
- package/dist/core/mcp.d.ts +6 -1
- package/dist/core/mcp.js +34 -7
- package/dist/core/reminder-disclosure.d.ts +90 -0
- package/dist/core/reminder-disclosure.js +64 -0
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +6 -0
- package/dist/core/runner/prepare-acquire-reconcile.js +1 -1
- package/dist/core/runner/prepare-hands-readface.d.ts +4 -0
- package/dist/core/runner/prepare-hands-readface.js +1 -0
- package/dist/core/runner/prepare-task.d.ts +15 -0
- package/dist/core/runner/prepare-task.js +48 -30
- package/dist/core/runner/runtask.js +3 -1
- package/dist/core/session-store.d.ts +59 -1
- package/dist/core/session-store.js +82 -14
- package/dist/core/session.d.ts +83 -1
- package/dist/core/task-registry-agent.d.ts +28 -0
- package/dist/core/task-registry-agent.js +63 -2
- package/dist/core/task-registry.d.ts +21 -0
- package/dist/core/task-registry.js +4 -1
- package/dist/core/types.d.ts +60 -0
- package/dist/core/untrusted-text.d.ts +63 -0
- package/dist/core/untrusted-text.js +48 -0
- package/dist/core/wiring-manifest.d.ts +35 -0
- package/dist/core/wiring-manifest.js +21 -1
- package/dist/engine/harness/types.d.ts +36 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -3
- package/dist/internal/harness-types.d.ts +1 -0
- package/dist/stores/file/index.d.ts +19 -3
- package/dist/stores/file/index.js +24 -1
- package/dist/stores/file/session-store.d.ts +18 -4
- package/dist/stores/file/session-store.js +73 -12
- package/dist/tools/fs/fs-pdf.d.ts +12 -1
- package/dist/tools/fs/fs-pdf.js +17 -3
- package/dist/tools/fs/fs-read.d.ts +2 -1
- package/dist/tools/fs/fs-read.js +33 -5
- package/dist/tools/fs/fs-shared.d.ts +6 -2
- package/dist/tools/fs/index.d.ts +7 -0
- package/dist/tools/fs/index.js +1 -1
- package/dist/tools/web.js +21 -2
- package/package.json +3 -2
- package/test/export-surface.snapshot.json +15 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { InMemorySessionRepo, uuidv7, } from "../internal/harness.js";
|
|
1
|
+
import { InMemorySessionRepo, SessionError, uuidv7, } from "../internal/harness.js";
|
|
2
2
|
import { isSessionConflict } from "./session.js";
|
|
3
3
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
4
4
|
import { SESSION_DEFAULT_TTL_DAYS } from "../config/defaults.js";
|
|
@@ -6,10 +6,14 @@ export { SESSION_DEFAULT_TTL_DAYS };
|
|
|
6
6
|
export class TtlSessionStore {
|
|
7
7
|
retention = "none";
|
|
8
8
|
durability;
|
|
9
|
+
placements;
|
|
10
|
+
listPlaced;
|
|
9
11
|
repo;
|
|
10
12
|
entries = new Map();
|
|
11
13
|
owners = new Map();
|
|
12
14
|
pending = new Map();
|
|
15
|
+
pendingPlacedDeletes = new Set();
|
|
16
|
+
deletingPlaced = new Map();
|
|
13
17
|
pinned = new Set();
|
|
14
18
|
defaultTtlMs;
|
|
15
19
|
evictPolicy;
|
|
@@ -21,6 +25,12 @@ export class TtlSessionStore {
|
|
|
21
25
|
this.durability = durability;
|
|
22
26
|
this.defaultTtlMs = (opts.defaultTtlDays ?? SESSION_DEFAULT_TTL_DAYS) * DAY_MS;
|
|
23
27
|
this.evictPolicy = opts.evict ?? (opts.repo ? "forget" : "delete");
|
|
28
|
+
if (opts.placements !== undefined)
|
|
29
|
+
this.placements = opts.placements;
|
|
30
|
+
const repoListPlaced = this.repo.listPlaced;
|
|
31
|
+
if (typeof repoListPlaced === "function") {
|
|
32
|
+
this.listPlaced = (kind, o) => repoListPlaced.call(this.repo, kind, o);
|
|
33
|
+
}
|
|
24
34
|
if (opts.sweepIntervalMs !== undefined && opts.sweepIntervalMs > 0) {
|
|
25
35
|
this.sweepTimer = setInterval(() => this.sweep(), opts.sweepIntervalMs);
|
|
26
36
|
this.sweepTimer.unref?.();
|
|
@@ -29,8 +39,12 @@ export class TtlSessionStore {
|
|
|
29
39
|
async acquire(sessionId, opts) {
|
|
30
40
|
this.sweep();
|
|
31
41
|
if (sessionId) {
|
|
42
|
+
const inflightPlacedDelete = this.deletingPlaced.get(sessionId);
|
|
43
|
+
if (inflightPlacedDelete !== undefined)
|
|
44
|
+
await inflightPlacedDelete;
|
|
32
45
|
const existing = this.entries.get(sessionId);
|
|
33
46
|
if (existing) {
|
|
47
|
+
this.assertPlacementAdmission(sessionId, existing.placement, opts);
|
|
34
48
|
existing.lastActiveAt = Date.now();
|
|
35
49
|
return { session: existing.session, sessionId };
|
|
36
50
|
}
|
|
@@ -39,26 +53,46 @@ export class TtlSessionStore {
|
|
|
39
53
|
}
|
|
40
54
|
const inflight = this.pending.get(sessionId);
|
|
41
55
|
if (inflight) {
|
|
42
|
-
|
|
56
|
+
const session = await inflight;
|
|
57
|
+
this.assertPlacementAdmission(sessionId, this.entries.get(sessionId)?.placement, opts);
|
|
58
|
+
return { session, sessionId };
|
|
43
59
|
}
|
|
44
|
-
const p = this.openOrCreate(sessionId, false);
|
|
60
|
+
const p = this.openOrCreate(sessionId, false, opts?.placement);
|
|
45
61
|
this.pending.set(sessionId, p);
|
|
46
62
|
try {
|
|
47
|
-
|
|
63
|
+
const session = await p;
|
|
64
|
+
this.assertPlacementAdmission(sessionId, this.entries.get(sessionId)?.placement, opts);
|
|
65
|
+
return { session, sessionId };
|
|
48
66
|
}
|
|
49
67
|
finally {
|
|
50
68
|
this.pending.delete(sessionId);
|
|
51
69
|
}
|
|
52
70
|
}
|
|
53
|
-
const session = await this.createAndStore(undefined);
|
|
71
|
+
const session = await this.createAndStore(undefined, opts?.placement);
|
|
54
72
|
const meta = await session.getMetadata();
|
|
55
73
|
return { session, sessionId: meta.id };
|
|
56
74
|
}
|
|
57
|
-
|
|
75
|
+
assertPlacementAdmission(sessionId, placement, opts) {
|
|
76
|
+
if (placement !== undefined && opts?.requireExisting !== true && opts?.placement === undefined) {
|
|
77
|
+
throw new SessionError("placement_refused", `session ${sessionId} belongs to the ${placement.kind} placement partition — a claim-form acquire cannot attach to it (read it with requireExisting, or address the agent through its a* handle)`);
|
|
78
|
+
}
|
|
79
|
+
if (opts?.placement !== undefined && opts.requireExisting !== true && this.placements?.subagent !== undefined) {
|
|
80
|
+
const want = opts.placement;
|
|
81
|
+
if (placement === undefined) {
|
|
82
|
+
throw new SessionError("placement_refused", `session ${sessionId} exists OUTSIDE the ${want.kind} placement partition it was declared into — refusing to run a subagent transcript on it (a concurrent claim-form create won this id, or the repo dropped the tuple)`);
|
|
83
|
+
}
|
|
84
|
+
if (placement.kind !== want.kind || placement.scope !== want.scope || placement.handle !== want.handle) {
|
|
85
|
+
throw new SessionError("placement_refused", `session ${sessionId}'s persisted placement does not match the declared one (persisted ${placement.kind}/${placement.scope ?? "-"}/${placement.handle ?? "-"} vs declared ${want.kind}/${want.scope ?? "-"}/${want.handle ?? "-"}) — refusing a cross-tuple attach`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async openOrCreate(id, requireExisting = false, placement) {
|
|
58
90
|
try {
|
|
59
91
|
const session = await this.repo.open({ id, createdAt: "" });
|
|
60
92
|
const meta = await session.getMetadata();
|
|
61
|
-
|
|
93
|
+
if (meta.placement === undefined)
|
|
94
|
+
this.pendingPlacedDeletes.delete(id);
|
|
95
|
+
this.entries.set(id, { session, lastActiveAt: Date.now(), createdAt: meta.createdAt, ...(meta.forkedFrom !== undefined ? { forkedFrom: meta.forkedFrom } : {}), ...(meta.placement !== undefined ? { placement: meta.placement } : {}) });
|
|
62
96
|
return session;
|
|
63
97
|
}
|
|
64
98
|
catch (err) {
|
|
@@ -68,13 +102,15 @@ export class TtlSessionStore {
|
|
|
68
102
|
if (requireExisting && isNotFound(err)) {
|
|
69
103
|
throw err;
|
|
70
104
|
}
|
|
71
|
-
return this.createAndStore(id);
|
|
105
|
+
return this.createAndStore(id, placement);
|
|
72
106
|
}
|
|
73
107
|
}
|
|
74
|
-
async createAndStore(id) {
|
|
75
|
-
const session = await this.repo.create(id ? { id } : {});
|
|
108
|
+
async createAndStore(id, placement) {
|
|
109
|
+
const session = await this.repo.create({ ...(id !== undefined ? { id } : {}), ...(placement !== undefined ? { placement } : {}) });
|
|
76
110
|
const meta = await session.getMetadata();
|
|
77
|
-
|
|
111
|
+
if (meta.placement === undefined)
|
|
112
|
+
this.pendingPlacedDeletes.delete(meta.id);
|
|
113
|
+
this.entries.set(meta.id, { session, lastActiveAt: Date.now(), createdAt: meta.createdAt, ...(meta.forkedFrom !== undefined ? { forkedFrom: meta.forkedFrom } : {}), ...(meta.placement !== undefined ? { placement: meta.placement } : {}) });
|
|
78
114
|
return session;
|
|
79
115
|
}
|
|
80
116
|
touch(sessionId) {
|
|
@@ -92,6 +128,7 @@ export class TtlSessionStore {
|
|
|
92
128
|
}
|
|
93
129
|
async list() {
|
|
94
130
|
return [...this.entries.entries()]
|
|
131
|
+
.filter(([, e]) => e.placement === undefined)
|
|
95
132
|
.map(([sessionId, e]) => ({
|
|
96
133
|
sessionId,
|
|
97
134
|
lastActiveAt: e.lastActiveAt,
|
|
@@ -137,12 +174,43 @@ export class TtlSessionStore {
|
|
|
137
174
|
return;
|
|
138
175
|
}
|
|
139
176
|
const e = this.entries.get(sessionId);
|
|
140
|
-
if (
|
|
177
|
+
if (e !== undefined && e.placement === undefined)
|
|
178
|
+
this.pendingPlacedDeletes.delete(sessionId);
|
|
179
|
+
let placed = e !== undefined ? e.placement !== undefined : this.pendingPlacedDeletes.has(sessionId);
|
|
180
|
+
if (!placed && e === undefined && this.placements !== undefined) {
|
|
181
|
+
const probe = this.repo.placementOf;
|
|
182
|
+
if (typeof probe === "function") {
|
|
183
|
+
try {
|
|
184
|
+
placed = (await probe.call(this.repo, sessionId)) !== undefined;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
placed = false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (!e && !placed) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (placed) {
|
|
195
|
+
this.pendingPlacedDeletes.add(sessionId);
|
|
196
|
+
this.entries.delete(sessionId);
|
|
197
|
+
let settleGate;
|
|
198
|
+
this.deletingPlaced.set(sessionId, new Promise((r) => (settleGate = r)));
|
|
199
|
+
try {
|
|
200
|
+
const meta = e !== undefined ? await e.session.getMetadata() : { id: sessionId, createdAt: "" };
|
|
201
|
+
await this.repo.delete(meta);
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
this.deletingPlaced.delete(sessionId);
|
|
205
|
+
settleGate();
|
|
206
|
+
}
|
|
207
|
+
this.pendingPlacedDeletes.delete(sessionId);
|
|
208
|
+
this.owners.delete(sessionId);
|
|
141
209
|
return;
|
|
142
210
|
}
|
|
143
211
|
this.entries.delete(sessionId);
|
|
144
212
|
if (this.evictPolicy === "delete") {
|
|
145
|
-
await this.repo.delete(await e.session.getMetadata());
|
|
213
|
+
await this.repo.delete(e !== undefined ? await e.session.getMetadata() : { id: sessionId, createdAt: "" });
|
|
146
214
|
this.owners.delete(sessionId);
|
|
147
215
|
}
|
|
148
216
|
}
|
|
@@ -174,7 +242,7 @@ export class TtlSessionStore {
|
|
|
174
242
|
}
|
|
175
243
|
if (now - e.lastActiveAt > this.defaultTtlMs) {
|
|
176
244
|
this.entries.delete(id);
|
|
177
|
-
if (this.evictPolicy === "delete") {
|
|
245
|
+
if (this.evictPolicy === "delete" && e.placement === undefined) {
|
|
178
246
|
void this.repo.delete({ id, createdAt: "" }).then(() => {
|
|
179
247
|
this.owners.delete(id);
|
|
180
248
|
}, () => { });
|
package/dist/core/session.d.ts
CHANGED
|
@@ -7,8 +7,43 @@ export { SessionError } from "../internal/harness.js";
|
|
|
7
7
|
export type { SessionWriteOptions } from "../internal/harness.js";
|
|
8
8
|
import type { Session } from "../internal/harness.js";
|
|
9
9
|
import type { SessionTreeEntry } from "../internal/harness.js";
|
|
10
|
+
export type { SessionPlacementRecord } from "../internal/harness.js";
|
|
10
11
|
/** True when `err` is a session optimistic-lock conflict (a concurrent writer won the branch leaf). */
|
|
11
12
|
export declare function isSessionConflict(err: unknown): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Subagent transcript persistence — the ACQUIRE-form placement declaration a trusted spawner passes
|
|
15
|
+
* when it creates a subagent's transcript session: the session is created INTO the store's subagent
|
|
16
|
+
* partition instead of the host lane. The minter reports the fact; the store never guesses residency
|
|
17
|
+
* from call shape. `placedAt` is stamped by the store at creation (see
|
|
18
|
+
* {@link SessionPlacementRecord}). Reaches stores ONLY through the engine's trusted internals chain
|
|
19
|
+
* (never a public TaskSpec key — a public key would let any caller push arbitrary sessions into the
|
|
20
|
+
* partition and poison store-side retention/enumeration).
|
|
21
|
+
*/
|
|
22
|
+
export interface SessionPlacement {
|
|
23
|
+
kind: "subagent";
|
|
24
|
+
/** Tenant key, same source as the durable agent row's `scope`. */
|
|
25
|
+
scope?: string;
|
|
26
|
+
/** The spawning host session (retrieval/partition metadata; authorization stays on the row predicate). */
|
|
27
|
+
parentSessionId?: string;
|
|
28
|
+
/** The delegation tree's root host session (fixed point). */
|
|
29
|
+
rootSessionId?: string;
|
|
30
|
+
/** The durable agent row key (`a…`) — the store-side join key for orphan GC. */
|
|
31
|
+
handle?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* One row of {@link SessionStore.listPlaced}. The row-store join key is the FULL `(scope, handle)`
|
|
35
|
+
* tuple — a listing that cannot supply both returns the discriminated `tupleIncomplete` variant
|
|
36
|
+
* instead (consumers fail closed on it: never age-reaped, reported honestly).
|
|
37
|
+
*/
|
|
38
|
+
export type PlacedSessionRow = {
|
|
39
|
+
sessionId: string;
|
|
40
|
+
placedAt: number;
|
|
41
|
+
} & ({
|
|
42
|
+
scope: string;
|
|
43
|
+
handle: string;
|
|
44
|
+
} | {
|
|
45
|
+
tupleIncomplete: true;
|
|
46
|
+
});
|
|
12
47
|
/** A session handed back by a {@link SessionStore}: the harness `Session` plus its resolved id. */
|
|
13
48
|
export interface AcquiredSession {
|
|
14
49
|
session: Session;
|
|
@@ -66,13 +101,47 @@ export interface SessionStore {
|
|
|
66
101
|
* fact.
|
|
67
102
|
*/
|
|
68
103
|
readonly durability?: import("./checkpoint-store.js").StoreDurability;
|
|
104
|
+
/**
|
|
105
|
+
* Subagent transcript persistence — the store's PER-PLACEMENT durability declaration: "this store
|
|
106
|
+
* understands and honors placement semantics (partitioning + the obligations below), and sessions
|
|
107
|
+
* placed into the `subagent` partition have THIS durability". Declaration, never duck-typing (the
|
|
108
|
+
* wiring manifest relays it verbatim). The store-level {@link durability} continues to describe
|
|
109
|
+
* only UN-placed sessions — a routing/composite store whose transient host lane and durable
|
|
110
|
+
* subagent partition differ can therefore be honest on both axes. ABSENT = the store does not
|
|
111
|
+
* understand placement: an `acquire` carrying one is legally IGNORED (the session is then an
|
|
112
|
+
* ordinary one, and the deployment's delegation-durability tier reads as rows-only).
|
|
113
|
+
*
|
|
114
|
+
* Declaring this commits the store to the placement obligations:
|
|
115
|
+
* 1. a placed session enjoys the declared durability (`acquire(id, {requireExisting:true})`
|
|
116
|
+
* reaches it after a restart when `"durable"`);
|
|
117
|
+
* 2. placed sessions do NOT appear on host enumeration faces (`list()` and every deployment
|
|
118
|
+
* listing path — restart-stable);
|
|
119
|
+
* 3. acquire semantics for sessions WITHOUT a placement are byte-unchanged;
|
|
120
|
+
* 4. `release(placedId)` is a REAL deletion (durable history included) — the joint reap's
|
|
121
|
+
* "row and session both gone" depends on it;
|
|
122
|
+
* 5. placement is FIRST-WRITE IMMUTABLE — a re-open (`requireExisting`, with or without a
|
|
123
|
+
* placement argument) returns the same session and never rewrites the tuple;
|
|
124
|
+
* 6. a placed id cannot be CLAIMED: a later claim-form acquire (no `requireExisting`, no
|
|
125
|
+
* creation placement) of an existing placed id refuses loudly
|
|
126
|
+
* (`SessionError("placement_refused")`) instead of attaching.
|
|
127
|
+
*/
|
|
128
|
+
readonly placements?: {
|
|
129
|
+
subagent?: {
|
|
130
|
+
durability: "durable" | "process-local";
|
|
131
|
+
};
|
|
132
|
+
};
|
|
69
133
|
/** Get an existing session by id, or create one (optionally with a caller-supplied id). design/114 Phase3:
|
|
70
134
|
* `opts.requireExisting` ⇒ a store MUST fail loud (throw a `not_found` {@link SessionError}) on a genuinely
|
|
71
135
|
* missing id rather than create-on-miss — so a reuse-style warm-resume of a gone session errors instead of
|
|
72
136
|
* silently starting a fresh empty run. A store that cannot honor it MUST still not silently create (either
|
|
73
|
-
* implement the check or reject the option).
|
|
137
|
+
* implement the check or reject the option).
|
|
138
|
+
* `opts.placement` (subagent transcript persistence): CREATE the session into the named placement
|
|
139
|
+
* partition (see {@link SessionPlacement} — trusted-internals channel only). Ignored by stores
|
|
140
|
+
* that do not declare {@link placements}; on a store that does, the creating call is exempt from
|
|
141
|
+
* obligation 6 above and idempotent re-acquire by the same trusted chain stays legal. */
|
|
74
142
|
acquire(sessionId?: string, opts?: {
|
|
75
143
|
requireExisting?: boolean;
|
|
144
|
+
placement?: SessionPlacement;
|
|
76
145
|
}): Promise<AcquiredSession>;
|
|
77
146
|
/** Mark a session recently active (resets idle TTL where applicable). May be async for durable stores. */
|
|
78
147
|
touch(sessionId: string): void | Promise<void>;
|
|
@@ -115,6 +184,19 @@ export interface SessionStore {
|
|
|
115
184
|
* that cannot enumerate omits this (the shell then has no session-list affordance — honest degrade).
|
|
116
185
|
*/
|
|
117
186
|
list?(): Promise<SessionStoreSummary[]>;
|
|
187
|
+
/**
|
|
188
|
+
* Subagent transcript persistence — enumerate the PLACED partition (the input to the joint reap's
|
|
189
|
+
* partition leg: sessions whose owning agent row is gone are the orphan shape it collects).
|
|
190
|
+
* **Optional**: absent = the partition-GC leg is simply unavailable on this store (documented,
|
|
191
|
+
* fail-closed — placed sessions then age out only through the row-joined reap path). `olderThanMs`
|
|
192
|
+
* filters by session AGE (last activity where knowable, else `placedAt`); `scope` narrows to one
|
|
193
|
+
* tenant. Rows return the FULL `(scope, handle)` join tuple or the honest `tupleIncomplete`
|
|
194
|
+
* variant (see {@link PlacedSessionRow}) — consumers must skip incomplete tuples, never guess.
|
|
195
|
+
*/
|
|
196
|
+
listPlaced?(kind: "subagent", opts?: {
|
|
197
|
+
olderThanMs?: number;
|
|
198
|
+
scope?: string;
|
|
199
|
+
}): Promise<PlacedSessionRow[]>;
|
|
118
200
|
/**
|
|
119
201
|
* design/110 — FORK a session: copy `sourceId`'s durable history (root→leaf) into a NEW session and return its
|
|
120
202
|
* id (or `null` if the source doesn't exist). The forked session is independent (writes to it don't touch the
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { type BackgroundAgentRecord, type BackgroundAgentStore } from "./background-agent-store.js";
|
|
2
2
|
import { type StopSource, type TaskAccess, type TaskRetrievalStatus, type UnifiedTaskOutput, type UnifiedTaskResult, type BackgroundAgentTaskHandle, type DurableAgentCore, type ParkedClaimTicket, type RegisterBackgroundAgentInput } from "./task-registry-shared.js";
|
|
3
3
|
import { type ToolResultStore } from "./tool-result-store.js";
|
|
4
|
+
/**
|
|
5
|
+
* Subagent transcript persistence (delegation entry caps) — the ACTIVE handles of one delegation
|
|
6
|
+
* tree in THIS process's registry: `background_agent` handles whose status is running/pending
|
|
7
|
+
* (parked does not burn a concurrency slot — a suspension is not concurrency; terminal handles are
|
|
8
|
+
* not active) under the `(scope, rootSessionId)` key (full depth: a grandchild's row carries the
|
|
9
|
+
* tree's fixed-point root, so the whole tree shares one pool; a depth-1 row whose rootSessionId
|
|
10
|
+
* predates the floor falls back to its parentSessionId — same fallback the access predicate's root
|
|
11
|
+
* arm rides). SYNCHRONOUS by contract: the caps enforcement runs check→register in one synchronous
|
|
12
|
+
* segment, which is what makes the reservation race-free in-process.
|
|
13
|
+
*/
|
|
14
|
+
export declare function activeDelegationHandlesLane(core: DurableAgentCore, scope: string, rootSessionId: string): string[];
|
|
4
15
|
export declare function ensureDurableHeartbeatLane(core: DurableAgentCore): void;
|
|
5
16
|
/** design/151 S1a — enqueue one durable-row write (see {@link DurableAgentLane} for the lane
|
|
6
17
|
* contract). `patch` is captured at CALL time (the settle-site values), applied in chain order.
|
|
@@ -66,9 +77,25 @@ export declare function endDurableClaimLane(core: DurableAgentCore, id: string):
|
|
|
66
77
|
* transcript lives on unanchored — the exact strand this orchestration exists to prevent. */
|
|
67
78
|
export declare function reapDurableAgentsLane(core: DurableAgentCore, scope: string, deps: {
|
|
68
79
|
store: BackgroundAgentStore;
|
|
80
|
+
/** `listPlaced` (subagent transcript persistence, optional): arms the PARTITION leg below —
|
|
81
|
+
* placed transcript sessions whose owning row is GONE (row never landed / deleted-but-release-
|
|
82
|
+
* failed) are the orphan shape only a session-side enumeration can find. Absent = that leg is
|
|
83
|
+
* unavailable (documented; the row-joined path still runs). */
|
|
69
84
|
sessions?: {
|
|
70
85
|
unpin?(sessionId: string): unknown;
|
|
71
86
|
release(sessionId: string): Promise<void> | void;
|
|
87
|
+
listPlaced?(kind: "subagent", opts?: {
|
|
88
|
+
olderThanMs?: number;
|
|
89
|
+
scope?: string;
|
|
90
|
+
}): Promise<Array<{
|
|
91
|
+
sessionId: string;
|
|
92
|
+
placedAt: number;
|
|
93
|
+
} & ({
|
|
94
|
+
scope: string;
|
|
95
|
+
handle: string;
|
|
96
|
+
} | {
|
|
97
|
+
tupleIncomplete: true;
|
|
98
|
+
})>>;
|
|
72
99
|
};
|
|
73
100
|
/** design/151 §7.7 (F-13) — the row's mailbox dies with the row: a WINNING delete also drops
|
|
74
101
|
* the (scope, handle) mailbox (advisory — a mailbox fault never blocks the reap; an orphaned
|
|
@@ -86,6 +113,7 @@ export declare function reapDurableAgentsLane(core: DurableAgentCore, scope: str
|
|
|
86
113
|
rowsReaped: number;
|
|
87
114
|
sessionsReleased: number;
|
|
88
115
|
skippedNoSessions: number;
|
|
116
|
+
orphanPlacedReleased: number;
|
|
89
117
|
}>;
|
|
90
118
|
/** codex 终审 C-4 half — after a probe-false RELEASE the row must stop claiming a transcript:
|
|
91
119
|
* the heartbeat's F-1 arm keeps re-driving a flush-failed lane, so a later successful flush
|
|
@@ -7,6 +7,23 @@ import { delimitUntrusted } from "./untrusted-text.js";
|
|
|
7
7
|
import { boundedRedactedSummary } from "./untrusted-egress.js";
|
|
8
8
|
import { mintCompletionId, commitCompletionIdIfEmpty, clipTaskOutput, assertOwnership, sleepPollStep, alreadyTerminalStopNote, canAccess, normalizeAgentName, closestName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR, } from "./task-registry-shared.js";
|
|
9
9
|
import { buildToolResultRef, OFFLOAD_TOOL_NAME, toolResultProvenanceOf } from "./tool-result-store.js";
|
|
10
|
+
export function activeDelegationHandlesLane(core, scope, rootSessionId) {
|
|
11
|
+
const out = [];
|
|
12
|
+
for (const h of core.handles.values()) {
|
|
13
|
+
if (h.type !== "background_agent")
|
|
14
|
+
continue;
|
|
15
|
+
if (h.status !== "running" && h.status !== "pending")
|
|
16
|
+
continue;
|
|
17
|
+
if (h.scope !== scope)
|
|
18
|
+
continue;
|
|
19
|
+
const bh = h;
|
|
20
|
+
const root = bh.rootSessionId ?? bh.parentSessionId ?? (bh.sessionScoped ? bh.owner : undefined);
|
|
21
|
+
if (root !== rootSessionId)
|
|
22
|
+
continue;
|
|
23
|
+
out.push(h.id);
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
10
27
|
export function ensureDurableHeartbeatLane(core) {
|
|
11
28
|
if (core.durableHeartbeatTimer !== undefined)
|
|
12
29
|
return;
|
|
@@ -170,7 +187,7 @@ export async function reapDurableAgentsLane(core, scope, deps, policy) {
|
|
|
170
187
|
await deps.store.reap(scope, now, { staleRunningMaxAgeMs: policy.staleRunningMaxAgeMs });
|
|
171
188
|
}
|
|
172
189
|
if (policy.maxAgeMs === undefined && policy.keep === undefined)
|
|
173
|
-
return { rowsReaped: 0, sessionsReleased: 0, skippedNoSessions: 0 };
|
|
190
|
+
return { rowsReaped: 0, sessionsReleased: 0, skippedNoSessions: 0, orphanPlacedReleased: 0 };
|
|
174
191
|
const terminal = (await deps.store.listByScope(scope)).filter((r) => r.status !== "running" && r.status !== "parked");
|
|
175
192
|
terminal.sort((a, b) => b.spawnedAt - a.spawnedAt);
|
|
176
193
|
const doomed = new Map();
|
|
@@ -239,7 +256,51 @@ export async function reapDurableAgentsLane(core, scope, deps, policy) {
|
|
|
239
256
|
core.reapedHandles.delete(r.handle);
|
|
240
257
|
}
|
|
241
258
|
}
|
|
242
|
-
|
|
259
|
+
let orphanPlacedReleased = 0;
|
|
260
|
+
if (policy.maxAgeMs !== undefined && deps.sessions !== undefined && typeof deps.sessions.listPlaced === "function") {
|
|
261
|
+
let placed = [];
|
|
262
|
+
try {
|
|
263
|
+
placed = await deps.sessions.listPlaced("subagent", { olderThanMs: policy.maxAgeMs, scope });
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
placed = [];
|
|
267
|
+
}
|
|
268
|
+
for (const p of placed) {
|
|
269
|
+
if ("tupleIncomplete" in p)
|
|
270
|
+
continue;
|
|
271
|
+
if (p.scope !== scope)
|
|
272
|
+
continue;
|
|
273
|
+
if (core.claimingHandles.has(p.handle) || core.reapingHandles.has(p.handle))
|
|
274
|
+
continue;
|
|
275
|
+
let row;
|
|
276
|
+
try {
|
|
277
|
+
row = await deps.store.get(p.handle, scope);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (row !== null)
|
|
283
|
+
continue;
|
|
284
|
+
const inProc = core.handles.get(p.handle);
|
|
285
|
+
if (inProc !== undefined && (inProc.status === "running" || inProc.status === "pending" || inProc.status === "parked"))
|
|
286
|
+
continue;
|
|
287
|
+
try {
|
|
288
|
+
await deps.sessions.unpin?.(p.sessionId);
|
|
289
|
+
await deps.sessions.release(p.sessionId);
|
|
290
|
+
orphanPlacedReleased++;
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
}
|
|
294
|
+
if (deps.mailbox !== undefined) {
|
|
295
|
+
try {
|
|
296
|
+
await deps.mailbox.drop(scope, p.handle);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return { rowsReaped, sessionsReleased, skippedNoSessions, orphanPlacedReleased };
|
|
243
304
|
}
|
|
244
305
|
export function releaseDurableTranscriptAnchorLane(core, id) {
|
|
245
306
|
const handle = core.handles.get(id);
|
|
@@ -166,9 +166,24 @@ export declare class TaskRegistry {
|
|
|
166
166
|
endDurableClaim(id: string): void;
|
|
167
167
|
reapDurableAgents(scope: string, deps: {
|
|
168
168
|
store: BackgroundAgentStore;
|
|
169
|
+
/** `listPlaced` (subagent transcript persistence, optional) arms the partition-orphan leg —
|
|
170
|
+
* see {@link reapDurableAgentsLane}. Pass the deployment's SessionStore directly (the
|
|
171
|
+
* TtlSessionStore over a placement-capable repo exposes it). */
|
|
169
172
|
sessions?: {
|
|
170
173
|
unpin?(sessionId: string): unknown;
|
|
171
174
|
release(sessionId: string): Promise<void> | void;
|
|
175
|
+
listPlaced?(kind: "subagent", opts?: {
|
|
176
|
+
olderThanMs?: number;
|
|
177
|
+
scope?: string;
|
|
178
|
+
}): Promise<Array<{
|
|
179
|
+
sessionId: string;
|
|
180
|
+
placedAt: number;
|
|
181
|
+
} & ({
|
|
182
|
+
scope: string;
|
|
183
|
+
handle: string;
|
|
184
|
+
} | {
|
|
185
|
+
tupleIncomplete: true;
|
|
186
|
+
})>>;
|
|
172
187
|
};
|
|
173
188
|
/** design/151 §7.7 (F-13) — the row's mailbox dies with the row: a WINNING delete also drops
|
|
174
189
|
* the (scope, handle) mailbox (advisory — a mailbox fault never blocks the reap; an orphaned
|
|
@@ -186,6 +201,7 @@ export declare class TaskRegistry {
|
|
|
186
201
|
rowsReaped: number;
|
|
187
202
|
sessionsReleased: number;
|
|
188
203
|
skippedNoSessions: number;
|
|
204
|
+
orphanPlacedReleased: number;
|
|
189
205
|
}>;
|
|
190
206
|
releaseDurableTranscriptAnchor(id: string): void;
|
|
191
207
|
bindBackgroundAgentSession(id: string, sessionId: string): void;
|
|
@@ -270,6 +286,11 @@ export declare class TaskRegistry {
|
|
|
270
286
|
/** #258 — the row's current stop-cycle counter, read by the spawn lanes right after registering to
|
|
271
287
|
* thread into the child's `RunInternals.cycleSeq`; see {@link backgroundAgentCycleSeqLane}. */
|
|
272
288
|
backgroundAgentCycleSeq(id: string): number | undefined;
|
|
289
|
+
/** Subagent transcript persistence (delegation entry caps) — one delegation tree's ACTIVE
|
|
290
|
+
* (running/pending) a* handles in THIS process, keyed `(scope, rootSessionId)`; SYNCHRONOUS by
|
|
291
|
+
* contract (the caps check and the registration form one atomic segment). See
|
|
292
|
+
* {@link activeDelegationHandlesLane}. */
|
|
293
|
+
activeDelegationHandles(scope: string, rootSessionId: string): string[];
|
|
273
294
|
/** ASYNC (revive arbitration) — the durable half is a guarded ownership claim on the row (awaited before the
|
|
274
295
|
* in-memory flip), so this leg and a cross-process claim arbitrate in one domain instead of both
|
|
275
296
|
* believing they own the cycle. See {@link reviveBackgroundAgentLane}. */
|
|
@@ -10,7 +10,7 @@ import { registerWorkflowLane, pollWorkflowLane, stopWorkflowLane } from "./task
|
|
|
10
10
|
import { mintCompletionId, canAccessWorkflowRun, formatWorkflowRun, clipTaskOutput, assertOwnership, sleepPollStep, statusFromBackground, rollSpoolText, accountDroppedBytes, renderSpoolBody, spoolDropNote, droppedGapNote, alreadyTerminalStopNote, terminalTaskSummary, TASK_OUTPUT_MAX_CHARS, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccess, DURABLE_AGENT_HANDLE_RE, } from "./task-registry-shared.js";
|
|
11
11
|
export { normalizeAgentName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR } from "./task-registry-shared.js";
|
|
12
12
|
import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_CONTRACT, TASK_STOP_CONTRACT, TASK_OUTPUT_MISSING_ID_MESSAGE, TASK_STOP_MISSING_ID_MESSAGE, TASK_STOP_PARAMS, resolveTaskIdArg, REGISTRY_TASK_TOOL_CAPS, composeTaskOutputDescription, composeTaskOutputParams, composeTaskStopDescription, } from "./task-tool-shape.js";
|
|
13
|
-
import { durableAgentArmedLane, durableAgentRowProbeLane, beginDurableClaimLane, endDurableClaimLane, reapDurableAgentsLane, noteBackgroundAgentActivityLane, reapStaleSessionBackgroundAgentsLane, releaseDurableTranscriptAnchorLane, bindBackgroundAgentSessionLane, registerBackgroundAgentLane, recordBackgroundAgentOrgAdmissionLane, parkBackgroundAgentLane, reconcileParkedAgentsLane, claimParkedAgentLane, rollbackParkedClaimLane, consumeParkedFlipLane, finalizeParkedResumeLane, settleBackgroundAgentLane, abortBackgroundAgentsForOwnerLane, serveDurableAgentRowLane, resolveBackgroundAgentByNameLane, backgroundAgentCycleSeqLane, markRetainedContinuationLane, reviveBackgroundAgentLane, settleRevivedAgentLane, unmarkRetainedContinuationLane, attachAgentNotifyLane, deliverToRunningAgentLane, runningBackgroundAgentLabelsLane, runningAgentFooterLane, notFoundRunningAgentsTail, pollBackgroundAgentLane, stopBackgroundAgentLane, } from "./task-registry-agent.js";
|
|
13
|
+
import { durableAgentArmedLane, durableAgentRowProbeLane, beginDurableClaimLane, endDurableClaimLane, reapDurableAgentsLane, noteBackgroundAgentActivityLane, reapStaleSessionBackgroundAgentsLane, releaseDurableTranscriptAnchorLane, bindBackgroundAgentSessionLane, registerBackgroundAgentLane, recordBackgroundAgentOrgAdmissionLane, parkBackgroundAgentLane, reconcileParkedAgentsLane, claimParkedAgentLane, rollbackParkedClaimLane, consumeParkedFlipLane, finalizeParkedResumeLane, settleBackgroundAgentLane, abortBackgroundAgentsForOwnerLane, serveDurableAgentRowLane, resolveBackgroundAgentByNameLane, backgroundAgentCycleSeqLane, activeDelegationHandlesLane, markRetainedContinuationLane, reviveBackgroundAgentLane, settleRevivedAgentLane, unmarkRetainedContinuationLane, attachAgentNotifyLane, deliverToRunningAgentLane, runningBackgroundAgentLabelsLane, runningAgentFooterLane, notFoundRunningAgentsTail, pollBackgroundAgentLane, stopBackgroundAgentLane, } from "./task-registry-agent.js";
|
|
14
14
|
export { canAccessWorkflowRun, clipTaskOutput, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE };
|
|
15
15
|
const BLOCK_DEFAULT_TIMEOUT_MS = 30_000;
|
|
16
16
|
const BLOCK_MAX_TIMEOUT_MS = 600_000;
|
|
@@ -194,6 +194,9 @@ export class TaskRegistry {
|
|
|
194
194
|
backgroundAgentCycleSeq(id) {
|
|
195
195
|
return backgroundAgentCycleSeqLane(this.core, id);
|
|
196
196
|
}
|
|
197
|
+
activeDelegationHandles(scope, rootSessionId) {
|
|
198
|
+
return activeDelegationHandlesLane(this.core, scope, rootSessionId);
|
|
199
|
+
}
|
|
197
200
|
reviveBackgroundAgent(id, access, abort) {
|
|
198
201
|
return reviveBackgroundAgentLane(this.core, id, access, abort);
|
|
199
202
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -643,6 +643,27 @@ export interface ToolExecuteContext {
|
|
|
643
643
|
* Undefined when the tool runs outside a Runner task.
|
|
644
644
|
*/
|
|
645
645
|
reminderMark?: string;
|
|
646
|
+
/**
|
|
647
|
+
* design/319 (B ticket) — the RUNNING task's reminder-disclosure trigger counters
|
|
648
|
+
* ({@link import("./reminder-disclosure.js").ReminderDisclosureCounts}), Runner-filled on the same
|
|
649
|
+
* trusted seat as {@link reminderMark}. Caller-mounted verbatim outlets that run the
|
|
650
|
+
* detect-and-disclose pipeline (the web tools' exact-mark defuse arm) bump their `<outlet>.<form>`
|
|
651
|
+
* keys here; the Runner folds the non-zero result into
|
|
652
|
+
* `TaskResult.stats.mechanisms.reminderDisclosures`. Mutable by design (a counter seat), but only
|
|
653
|
+
* ever additive — never a decision input. Undefined outside a Runner task.
|
|
654
|
+
*/
|
|
655
|
+
reminderDisclosureCounts?: import("./reminder-disclosure.js").ReminderDisclosureCounts;
|
|
656
|
+
/**
|
|
657
|
+
* Subagent transcript persistence — the RESOLVED delegation entry caps for this run
|
|
658
|
+
* ({@link RunnerDeps.delegationEntryCaps} after prepare's loud validation; both members always
|
|
659
|
+
* present). Runner-filled trusted seat, never a model argument — the Agent tool's background lane
|
|
660
|
+
* reads it at its registration point. Undefined outside a Runner task (the lane then applies the
|
|
661
|
+
* exported defaults itself, so a directly-driven tool is bounded too).
|
|
662
|
+
*/
|
|
663
|
+
delegationEntryCaps?: {
|
|
664
|
+
maxConcurrent: number;
|
|
665
|
+
maxCumulativePerSession: number;
|
|
666
|
+
};
|
|
646
667
|
/**
|
|
647
668
|
* Report usage spent in a nested run this tool spawned (e.g. a sub-agent). The Runner accumulates
|
|
648
669
|
* it into the parent task's `TaskResult.stats.nested`, so delegated cost — the multi-agent "~15×"
|
|
@@ -3295,6 +3316,15 @@ export interface TaskResult {
|
|
|
3295
3316
|
reps: number;
|
|
3296
3317
|
segment: string;
|
|
3297
3318
|
}>;
|
|
3319
|
+
/** design/319 (B ticket, G9② observation seat) — reminder-disclosure trigger counts for the
|
|
3320
|
+
* leg, keyed `<outlet>.<form>`: outlets `read` / `notebook` / `pdf` / `mcp` / `webFetch` /
|
|
3321
|
+
* `webSearch`; forms `bare` (bare-form trailer appended), `marked` (marked-form trailer —
|
|
3322
|
+
* never throttled), `bare_throttled` (a bare trailer suppressed by the 60s per-key window),
|
|
3323
|
+
* `defused` (an MCP/web segment's exact-mark bytes were rewritten — the lane's one sanctioned
|
|
3324
|
+
* byte change, always paired with a `marked` disclosure). Present only when ≥1 key is
|
|
3325
|
+
* non-zero. This is the D-4/D-6 re-ruling data (defuse/trailer widening to Read/Bash/Grep):
|
|
3326
|
+
* a reading, never a gate. */
|
|
3327
|
+
reminderDisclosures?: Record<string, number>;
|
|
3298
3328
|
};
|
|
3299
3329
|
/**
|
|
3300
3330
|
* design/91 — **human-review burden** (design/89 §2.4 C2 axis). The wall-clock time a task spent waiting
|
|
@@ -4800,6 +4830,14 @@ export interface EngineNotice {
|
|
|
4800
4830
|
* mark would have carried, neutralized/length-bounded (tool and agent-type names are
|
|
4801
4831
|
* host/model-controlled inputs).
|
|
4802
4832
|
*
|
|
4833
|
+
* - `"delegation.transcript_integrity"` (subagent transcript persistence) — a durable agent row
|
|
4834
|
+
* with a BOUND transcript sessionId met a session store that attests `not_found` for it: the
|
|
4835
|
+
* deployment's declared transcript durability is being contradicted by reality. Announced at
|
|
4836
|
+
* most once per (handle, process) from the continuation read faces (SendMessage preflight /
|
|
4837
|
+
* AgentTranscript's durable leg); the per-call honest refusals are unchanged, and the declared
|
|
4838
|
+
* tier is NOT auto-downgraded (declaration-制 — observation reports, it never re-adjudicates);
|
|
4839
|
+
* `detail: { handle }`.
|
|
4840
|
+
*
|
|
4803
4841
|
* Deliberately NOT a notice family: brain retry/reconnect liveness (a rate limit, a 5xx, a
|
|
4804
4842
|
* transient network failure being retried). Those are per-attempt liveness frames with their own
|
|
4805
4843
|
* frequency semantics and ride the wire `status` channel ({@link BrainStatus}), whose sink the
|
|
@@ -5305,6 +5343,28 @@ export interface RunnerDeps {
|
|
|
5305
5343
|
* Same single-instance pairing discipline as `backgroundAgentStore` (RB-37): a deployment that
|
|
5306
5344
|
* composes its own tools must thread the SAME instance everywhere. */
|
|
5307
5345
|
mailboxStore?: import("./mailbox-store.js").MailboxStore;
|
|
5346
|
+
/**
|
|
5347
|
+
* Subagent transcript persistence — the delegation ENTRY caps (CC parity values: 20 concurrent /
|
|
5348
|
+
* 200 cumulative per session tree; defaults exported as `DELEGATION_MAX_CONCURRENT_DEFAULT` /
|
|
5349
|
+
* `DELEGATION_MAX_PER_SESSION_DEFAULT`). Key = `(scope, rootSessionId)`, full depth (grandchildren
|
|
5350
|
+
* share the tree's pool). `maxConcurrent` bounds running/pending a* handles in this process's
|
|
5351
|
+
* registry (parked does not burn a slot — a suspension is not concurrency; a revival claim counts
|
|
5352
|
+
* like a spawn); `maxCumulativePerSession` bounds the RETAINED-WINDOW cumulative count (registry-
|
|
5353
|
+
* retained + store-retained rows — a reaped row returns its quota; deliberately NOT CC's lifetime-
|
|
5354
|
+
* monotonic session counter, which would require a persistent counting surface this economic bound
|
|
5355
|
+
* does not justify — registered divergence). Refusals are coded (`delegation.concurrency_cap` /
|
|
5356
|
+
* `delegation.session_cap`) with the current value and this knob's name in the text.
|
|
5357
|
+
*
|
|
5358
|
+
* BAD VALUES REFUSE LOUDLY at prepare (`config.delegation_entry_caps`, the #123 posture): a
|
|
5359
|
+
* non-positive/non-integer/NaN member, or a resolved pair where `maxConcurrent` exceeds
|
|
5360
|
+
* `maxCumulativePerSession` (you cannot run more at once than you may ever create) — never a
|
|
5361
|
+
* silent fold to the defaults. Per-replica bound (multi-replica deployments are each honestly
|
|
5362
|
+
* bounded; row-level CAS owns correctness, this cap owns economics).
|
|
5363
|
+
*/
|
|
5364
|
+
delegationEntryCaps?: {
|
|
5365
|
+
maxConcurrent?: number;
|
|
5366
|
+
maxCumulativePerSession?: number;
|
|
5367
|
+
};
|
|
5308
5368
|
/** design/176 — deployment tuning for the always-on peer-message admission gate (SendMessage entry
|
|
5309
5369
|
* judgment: rate/dedup/hop-chain/queue bounds). Per-field range-validated against the upstream
|
|
5310
5370
|
* table with out-of-range values falling back to that field's default; there is NO off switch —
|