@sema-agent/core 5.47.0 → 5.49.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 +115 -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/agents/teacher.d.ts +25 -1
- package/dist/agents/teacher.js +85 -12
- 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 +14 -0
- package/dist/core/mcp.d.ts +6 -1
- package/dist/core/mcp.js +34 -7
- package/dist/core/memory-engine/delegation-settlement.d.ts +27 -0
- package/dist/core/memory-engine/delegation-settlement.js +31 -4
- package/dist/core/memory-engine/dual-root.js +11 -0
- package/dist/core/memory-engine/engine.d.ts +6 -1
- package/dist/core/memory-engine/engine.js +136 -21
- package/dist/core/memory-engine/memory-backend-contract.js +33 -0
- package/dist/core/memory-engine/origin-clearance.d.ts +19 -0
- package/dist/core/memory-engine/origin-clearance.js +10 -0
- package/dist/core/memory-engine/provenance-wording.d.ts +15 -1
- package/dist/core/memory-engine/provenance-wording.js +1 -0
- package/dist/core/memory-engine/tools.js +6 -4
- 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 +51 -33
- package/dist/core/runner/runtask.d.ts +26 -1
- package/dist/core/runner/runtask.js +21 -3
- 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/strategy-store.d.ts +180 -3
- package/dist/core/strategy-store.js +172 -23
- 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 +66 -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 +7 -6
- package/dist/index.js +6 -5
- package/dist/internal/harness-types.d.ts +1 -0
- package/dist/stores/file/file-snapshot-store.js +7 -1
- package/dist/stores/file/index.d.ts +27 -3
- package/dist/stores/file/index.js +36 -1
- package/dist/stores/file/session-policy-store.d.ts +0 -13
- package/dist/stores/file/session-policy-store.js +7 -1
- package/dist/stores/file/session-store.d.ts +22 -5
- package/dist/stores/file/session-store.js +80 -13
- package/dist/stores/file/strategy-store.d.ts +97 -0
- package/dist/stores/file/strategy-store.js +340 -0
- 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 +22 -1
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
|
|
@@ -6,8 +6,17 @@
|
|
|
6
6
|
* while a missed strategy just falls back to asking the teacher (baseline). `scope` is a **mandatory
|
|
7
7
|
* isolation boundary** (multi-tenant: one user's strategies must never leak to another).
|
|
8
8
|
*/
|
|
9
|
+
/** How a strategy first entered its scope. `"earned"` (or absent — older data predates the field) =
|
|
10
|
+
* stored by the escalation loop after a verified success; `"seeded"` = pre-installed via
|
|
11
|
+
* {@link seedStrategies}. An audit trail, NOT a security property: the store cannot authenticate
|
|
12
|
+
* its caller, so a direct `save` can claim either value — trust in seed material is the caller's
|
|
13
|
+
* decision, this field only records what the caller declared. */
|
|
14
|
+
export type StrategyOrigin = "earned" | "seeded";
|
|
9
15
|
export interface StoredStrategy {
|
|
10
|
-
/** Stable id.
|
|
16
|
+
/** Stable id. File-backed stores and {@link seedStrategies} require `[A-Za-z0-9_-]{1,64}` (the
|
|
17
|
+
* escalation loop mints UUIDs, which conform). NOTE: ids flow into host telemetry
|
|
18
|
+
* ({@link import("../agents/teacher.js").TeacherRunResult}`.strategiesInjected`) as join keys —
|
|
19
|
+
* do not encode sensitive text into an id. */
|
|
11
20
|
id: string;
|
|
12
21
|
/** The raw task objective that triggered the teacher's help — matched against future objectives. */
|
|
13
22
|
problem: string;
|
|
@@ -23,6 +32,23 @@ export interface StoredStrategy {
|
|
|
23
32
|
teacherModel?: string;
|
|
24
33
|
/** Reserved for a future generalized signature (v3 semantic matching). */
|
|
25
34
|
signature?: string;
|
|
35
|
+
/** Provenance stamp (see {@link StrategyOrigin}). Absent = earned (backward compatible). */
|
|
36
|
+
origin?: StrategyOrigin;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* One contained strategy-store incident, as disclosed by a persistent store's `onIncident` seat and by
|
|
40
|
+
* `TeacherConfig.onStrategyStoreIncident`. Phases are split so "the write never happened" (`save`) is
|
|
41
|
+
* distinguishable from "the write landed but housekeeping failed" (`evict`), and a quarantined corrupt
|
|
42
|
+
* entry (`parse`) from a retrieval that degraded (`find`).
|
|
43
|
+
*/
|
|
44
|
+
export interface StrategyStoreIncident {
|
|
45
|
+
op: "find" | "save" | "evict" | "parse";
|
|
46
|
+
/** What was observed and what the operation degraded to. */
|
|
47
|
+
error: string;
|
|
48
|
+
/** The strategy id involved, when one is known. */
|
|
49
|
+
id?: string;
|
|
50
|
+
/** Absolute path of the offending file, when the incident is file-level (e.g. a `parse` quarantine). */
|
|
51
|
+
path?: string;
|
|
26
52
|
}
|
|
27
53
|
export interface StrategyStore {
|
|
28
54
|
/** Persist a strategy (implementations may merge an exact near-duplicate and cap per-scope size). */
|
|
@@ -43,10 +69,113 @@ export interface StrategyStore {
|
|
|
43
69
|
* (ruled 2026-08-04): this is a capacity cap, and a capacity cap can be widened but never turned off.
|
|
44
70
|
*/
|
|
45
71
|
prune?(scope: string, maxSize: number): Promise<void> | void;
|
|
72
|
+
/**
|
|
73
|
+
* Optional: how full a scope is (`used` entries out of `capacity`). {@link seedStrategies} uses it to
|
|
74
|
+
* refuse a batch that would not fit — silently evicting freshly seeded entries would be the most
|
|
75
|
+
* misleading possible "success". A store that cannot report usage simply omits this.
|
|
76
|
+
*/
|
|
77
|
+
scopeUsage?(scope: string): Promise<{
|
|
78
|
+
used: number;
|
|
79
|
+
capacity: number;
|
|
80
|
+
}> | {
|
|
81
|
+
used: number;
|
|
82
|
+
capacity: number;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Optional: is a content-identical entry (same normalized problem+strategy — the save-side merge
|
|
86
|
+
* key) already stored in `scope`? {@link seedStrategies} uses it to count only NET-NEW rows against
|
|
87
|
+
* the remaining capacity, so REPLAYING a batch whose entries already landed is never refused for
|
|
88
|
+
* "not fitting" space it does not need. A store that omits it gets the conservative whole-batch
|
|
89
|
+
* check (sound, but a replay into a full scope is falsely refused).
|
|
90
|
+
*/
|
|
91
|
+
hasStrategy?(scope: string, entry: Pick<StoredStrategy, "problem" | "strategy">): Promise<boolean> | boolean;
|
|
46
92
|
}
|
|
47
93
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
94
|
+
* Validate a retrieval cap fail-loud (ruled 2026-08-04, same posture as `WebFetchConfig.maxBytes` and
|
|
95
|
+
* the workflow resilience knobs). The cap was previously applied as
|
|
96
|
+
* `slice(0, Math.max(0, Number.isFinite(limit) ? limit : 0))`, which mapped NaN, `Infinity` and every
|
|
97
|
+
* negative onto the SAME value — 0 — so the two opposite intentions ("no cap" and "a fumbled unit")
|
|
98
|
+
* both came out as "return nothing", the most misleading possible answer for a high-precision store.
|
|
99
|
+
* `Infinity` is HONORED here (a query cap can legitimately be absent), unlike a capacity cap.
|
|
100
|
+
*/
|
|
101
|
+
export declare function resolveFindLimit(limit: number): number;
|
|
102
|
+
/**
|
|
103
|
+
* Validate a per-scope CAPACITY cap fail-loud (ruled 2026-08-04). Two silent failures lived here: a
|
|
104
|
+
* negative `maxSize` was clamped to 0 and quietly wiped the whole scope, and a non-finite one made the
|
|
105
|
+
* `arr.length > cap` comparison permanently false — eviction disabled, unbounded growth, no signal.
|
|
106
|
+
* `Infinity` is refused by name (the "turn the cap off" misconception) rather than silently behaving
|
|
107
|
+
* like the unbounded store it would create. Shared by the in-memory and file-backed stores.
|
|
108
|
+
*/
|
|
109
|
+
export declare function resolveCapacityCap(name: string, cap: number): number;
|
|
110
|
+
/** Id grammar for persisted/seeded strategies: filename-safe, no traversal, bounded. */
|
|
111
|
+
export declare const STRATEGY_ID_RE: RegExp;
|
|
112
|
+
/** Byte cap on `problem` (the matched objective text). */
|
|
113
|
+
export declare const MAX_STRATEGY_PROBLEM_BYTES = 8192;
|
|
114
|
+
/** Byte cap on `strategy` (the injected guidance — the teacher contract is 1-2 sentences). */
|
|
115
|
+
export declare const MAX_STRATEGY_TEXT_BYTES = 4096;
|
|
116
|
+
/** Byte caps on the remaining serialized string fields, so a legitimate save cannot mint an entry
|
|
117
|
+
* whose on-disk record dwarfs the payload caps above (the file store also bounds what it will READ,
|
|
118
|
+
* and these write-side caps are what make that read bound compatible with every honest record). */
|
|
119
|
+
export declare const MAX_STRATEGY_SCOPE_BYTES = 4096;
|
|
120
|
+
export declare const MAX_STRATEGY_TEACHER_MODEL_BYTES = 512;
|
|
121
|
+
export declare const MAX_STRATEGY_SIGNATURE_BYTES = 4096;
|
|
122
|
+
/** A real ISO-8601 timestamp is ~24-35 bytes; 64 leaves headroom for exotic offsets while keeping a
|
|
123
|
+
* "string that happens to sit in the ts seat" from bloating a serialized record. */
|
|
124
|
+
export declare const MAX_STRATEGY_TS_BYTES = 64;
|
|
125
|
+
/** A stored `ts` may not sit further in the future than this — a far-future timestamp would pin the
|
|
126
|
+
* entry at maximum recency forever (a rank-to-the-top vector). Small clock skew stays legal. */
|
|
127
|
+
export declare const MAX_STRATEGY_TS_FUTURE_MS: number;
|
|
128
|
+
/** Total byte budget across the strategy texts one retrieval hands to a prompt. Enforced by the file
|
|
129
|
+
* store's `find` (close to the tamperable data) AND at the escalation loop's injection site (so the
|
|
130
|
+
* bound holds for ANY backend — the in-memory store deliberately accepts legacy-shaped entries). */
|
|
131
|
+
export declare const MAX_STRATEGY_INJECTION_TOTAL_BYTES = 16384;
|
|
132
|
+
/**
|
|
133
|
+
* Structural validity of a (possibly untrusted, e.g. read-from-disk) value as a {@link StoredStrategy}:
|
|
134
|
+
* strings where strings are promised, id grammar (filename safety), byte caps, confidence domain
|
|
135
|
+
* (finite, 0-3 per the declared contract), scope non-emptiness, origin closed set. Returns a reason
|
|
136
|
+
* string when invalid, `null` when acceptable. Deliberately does NOT judge the timestamp's VALUE —
|
|
137
|
+
* that is a write-side rule only ({@link validateStrategyForWrite}): applying it on read would let a
|
|
138
|
+
* host clock stepping backwards mass-invalidate entries that were legal when written. A MALFORMED
|
|
139
|
+
* (unparseable) `ts` is also allowed through — read-side ranking clamps it to "oldest", so it
|
|
140
|
+
* self-buries instead of pinning.
|
|
141
|
+
*/
|
|
142
|
+
export declare function storedStrategyShapeIssue(v: unknown): string | null;
|
|
143
|
+
/**
|
|
144
|
+
* The write-side door: refuse an entry a persistent store must not accept — the structural checks of
|
|
145
|
+
* {@link storedStrategyShapeIssue} PLUS the future-timestamp bound (a far-future `ts` would pin the
|
|
146
|
+
* entry at maximum recency; refusing it at write keeps the rank honest, while the read side merely
|
|
147
|
+
* clamps so a backwards clock step cannot invalidate history).
|
|
148
|
+
*/
|
|
149
|
+
export declare function validateStrategyForWrite(s: StoredStrategy): void;
|
|
150
|
+
/**
|
|
151
|
+
* Compile a query into the shared high-precision match predicate. Returns `null` when the query has
|
|
152
|
+
* no significant terms (nothing meaningful to match on → don't inject random strategies). Single
|
|
153
|
+
* source for the in-memory and file-backed stores, so retrieval semantics cannot fork between
|
|
154
|
+
* backends.
|
|
155
|
+
*
|
|
156
|
+
* TWO acceptance arms, OR-combined, so the CJK upgrade can only ADD hits, never lose one:
|
|
157
|
+
* - full arm — every significant term (alphanumeric AND CJK) present in the problem;
|
|
158
|
+
* - legacy arm — every significant ALPHANUMERIC term present (and there is ≥1). This is exactly the
|
|
159
|
+
* pre-CJK predicate: it used to DISCARD CJK from the query, so "请帮我 extract tables from
|
|
160
|
+
* report.pdf" matched an English-only stored problem. Requiring the CJK boilerplate too would
|
|
161
|
+
* have silently un-matched that pair — a recall regression hiding inside a recall fix. Keeping
|
|
162
|
+
* the legacy arm costs precision only relative to a strictness nobody ever had.
|
|
163
|
+
*
|
|
164
|
+
* ACCEPTED COST (documented, pinned): via the legacy arm, a mixed query whose alphanumeric terms all
|
|
165
|
+
* match can hit a problem whose CJK terms CONFLICT (提取 X vs 删除 X) — exactly what the pre-CJK
|
|
166
|
+
* predicate always did, retained because losing old hits is the one regression this upgrade must not
|
|
167
|
+
* make. The mitigation is the surrounding depth (negation preamble, verify-before-use instruction,
|
|
168
|
+
* injection audit); tightening precision is a measured-recalibration change, not a retrieval patch.
|
|
169
|
+
*/
|
|
170
|
+
export declare function compileStrategyQuery(query: string): ((problem: string) => boolean) | null;
|
|
171
|
+
export declare function normalizeStrategyText(s: string): string;
|
|
172
|
+
/** Duplicate key for save-side merge and read-side dedup: same normalized problem AND strategy. */
|
|
173
|
+
export declare function strategyDedupKey(s: Pick<StoredStrategy, "problem" | "strategy">): string;
|
|
174
|
+
/** Ranking score: confidence weighted by recency (no `uses` — avoids a reinforcement feedback loop). */
|
|
175
|
+
export declare function scoreStoredStrategy(s: StoredStrategy): number;
|
|
176
|
+
/**
|
|
177
|
+
* Default in-memory store. Retrieval: a stored problem must contain **every significant** query term
|
|
178
|
+
* (and there must be ≥1) — near-exact on the meaningful terms, ignoring stopword noise. High precision
|
|
50
179
|
* by design. Ranked by `confidence × recency`. Per-scope capacity cap with eviction on save.
|
|
51
180
|
*/
|
|
52
181
|
export declare class InMemoryStrategyStore implements StrategyStore {
|
|
@@ -56,6 +185,54 @@ export declare class InMemoryStrategyStore implements StrategyStore {
|
|
|
56
185
|
save(s: StoredStrategy): void;
|
|
57
186
|
find(scope: string, query: string, limit: number): StoredStrategy[];
|
|
58
187
|
prune(scope: string, maxSize: number): void;
|
|
188
|
+
scopeUsage(scope: string): {
|
|
189
|
+
used: number;
|
|
190
|
+
capacity: number;
|
|
191
|
+
};
|
|
192
|
+
hasStrategy(scope: string, entry: Pick<StoredStrategy, "problem" | "strategy">): boolean;
|
|
59
193
|
/** Total stored strategies (across scopes); for tests/observability. */
|
|
60
194
|
get size(): number;
|
|
61
195
|
}
|
|
196
|
+
/** One entry to seed: a {@link StoredStrategy} minus the fields the seeder stamps (`scope` comes from
|
|
197
|
+
* the call, `origin` is forced to `"seeded"`, `ts` defaults to now). */
|
|
198
|
+
export type SeedStrategyEntry = Omit<StoredStrategy, "scope" | "origin" | "ts"> & {
|
|
199
|
+
ts?: string;
|
|
200
|
+
};
|
|
201
|
+
/** Result of {@link seedStrategies}: how many entries landed, and where the write phase stopped if it
|
|
202
|
+
* did. `failedAt` present = a PARTIAL seed (entries before it are in the store); the batch is
|
|
203
|
+
* idempotent by content, so re-running the same call is the supported recovery. */
|
|
204
|
+
export interface SeedStrategiesReport {
|
|
205
|
+
written: number;
|
|
206
|
+
failedAt?: {
|
|
207
|
+
id: string;
|
|
208
|
+
error: string;
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Seed a batch of strategies into `scope`, in two phases:
|
|
213
|
+
*
|
|
214
|
+
* 1. **Validate the whole batch first** — any invalid entry (shape/id grammar/byte caps/timestamp
|
|
215
|
+
* discipline, see {@link validateStrategyForWrite}), a duplicate id OR duplicate normalized
|
|
216
|
+
* content within the batch (two content-identical entries would merge into one, making `written`
|
|
217
|
+
* over-report and one seeded id unfindable), or a batch larger than the scope's remaining
|
|
218
|
+
* capacity (when the store reports usage) refuses the WHOLE batch loudly with ZERO writes.
|
|
219
|
+
* Silently evicting freshly seeded entries would be the most misleading possible "success", so a
|
|
220
|
+
* batch that cannot fully fit does not start.
|
|
221
|
+
* 2. **Write one by one** — a mid-batch failure returns a `{written, failedAt}` report instead of
|
|
222
|
+
* pretending to be a transaction; entries are idempotent (same id + content re-saves merge), so
|
|
223
|
+
* replaying the same batch completes the remainder.
|
|
224
|
+
*
|
|
225
|
+
* HONEST LIMITS (cache-store posture — no locks, no reservations):
|
|
226
|
+
* - the capacity preflight is a snapshot, not a reservation: with a CONCURRENT writer on the same
|
|
227
|
+
* scope, entries can still be evicted between the check and the writes (single-writer deployments
|
|
228
|
+
* — the aggregated backend under its boot lock — get the full guarantee);
|
|
229
|
+
* - an entry content-identical to one ALREADY STORED merges into the existing entry — the STORED id
|
|
230
|
+
* and the STORED `origin` stamp win (content earned before seeding stays `earned`; the requested
|
|
231
|
+
* id is not created). This counts as written: the content IS in the store, and replaying a batch
|
|
232
|
+
* relies on exactly that merge. Injection audit rows always carry the stored id, so joins hold.
|
|
233
|
+
*
|
|
234
|
+
* Every entry is stamped `origin: "seeded"`. That stamp is an AUDIT clue, not a security property —
|
|
235
|
+
* the store cannot authenticate the seeder, and a caller with store access can write anything; trust
|
|
236
|
+
* in the seed material is the caller's decision.
|
|
237
|
+
*/
|
|
238
|
+
export declare function seedStrategies(store: StrategyStore, scope: string, entries: readonly SeedStrategyEntry[]): Promise<SeedStrategiesReport>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { termSet } from "./memory-vector.js";
|
|
1
2
|
function invalidKnob(message, code) {
|
|
2
3
|
const e = new Error(message);
|
|
3
4
|
e.code = code;
|
|
4
5
|
return e;
|
|
5
6
|
}
|
|
6
|
-
function resolveFindLimit(limit) {
|
|
7
|
+
export function resolveFindLimit(limit) {
|
|
7
8
|
if (limit === Number.POSITIVE_INFINITY)
|
|
8
9
|
return limit;
|
|
9
10
|
if (!Number.isInteger(limit) || limit < 0) {
|
|
@@ -11,30 +12,109 @@ function resolveFindLimit(limit) {
|
|
|
11
12
|
}
|
|
12
13
|
return limit;
|
|
13
14
|
}
|
|
14
|
-
function resolveCapacityCap(name, cap) {
|
|
15
|
+
export function resolveCapacityCap(name, cap) {
|
|
15
16
|
if (cap === Number.POSITIVE_INFINITY) {
|
|
16
|
-
throw invalidKnob(`
|
|
17
|
+
throw invalidKnob(`strategy store ${name} cannot be Infinity — a per-scope capacity cap can be widened but not turned off; pass a large finite integer instead`, "config.strategy_max_size_invalid");
|
|
17
18
|
}
|
|
18
19
|
if (!Number.isInteger(cap) || cap < 0) {
|
|
19
|
-
throw invalidKnob(`
|
|
20
|
+
throw invalidKnob(`strategy store ${name} must be a non-negative integer (got ${String(cap)})`, "config.strategy_max_size_invalid");
|
|
20
21
|
}
|
|
21
22
|
return cap;
|
|
22
23
|
}
|
|
24
|
+
export const STRATEGY_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
25
|
+
export const MAX_STRATEGY_PROBLEM_BYTES = 8192;
|
|
26
|
+
export const MAX_STRATEGY_TEXT_BYTES = 4096;
|
|
27
|
+
export const MAX_STRATEGY_SCOPE_BYTES = 4096;
|
|
28
|
+
export const MAX_STRATEGY_TEACHER_MODEL_BYTES = 512;
|
|
29
|
+
export const MAX_STRATEGY_SIGNATURE_BYTES = 4096;
|
|
30
|
+
export const MAX_STRATEGY_TS_BYTES = 64;
|
|
31
|
+
export const MAX_STRATEGY_TS_FUTURE_MS = 24 * 60 * 60 * 1000;
|
|
32
|
+
export const MAX_STRATEGY_INJECTION_TOTAL_BYTES = 16384;
|
|
33
|
+
function invalidEntry(message, id) {
|
|
34
|
+
const e = new Error(message);
|
|
35
|
+
e.code = "strategy.entry_invalid";
|
|
36
|
+
if (id !== undefined)
|
|
37
|
+
e.strategyId = id;
|
|
38
|
+
return e;
|
|
39
|
+
}
|
|
40
|
+
export function storedStrategyShapeIssue(v) {
|
|
41
|
+
if (v === null || typeof v !== "object")
|
|
42
|
+
return "not an object";
|
|
43
|
+
const s = v;
|
|
44
|
+
if (typeof s.id !== "string" || !STRATEGY_ID_RE.test(s.id)) {
|
|
45
|
+
return `id must match [A-Za-z0-9_-]{1,64} (got ${JSON.stringify(String(s.id).slice(0, 80))})`;
|
|
46
|
+
}
|
|
47
|
+
if (typeof s.scope !== "string" || s.scope.length === 0)
|
|
48
|
+
return "scope must be a non-empty string";
|
|
49
|
+
if (Buffer.byteLength(s.scope, "utf8") > MAX_STRATEGY_SCOPE_BYTES)
|
|
50
|
+
return `scope exceeds ${MAX_STRATEGY_SCOPE_BYTES} bytes`;
|
|
51
|
+
if (typeof s.problem !== "string" || typeof s.strategy !== "string")
|
|
52
|
+
return "problem/strategy must be strings";
|
|
53
|
+
if (Buffer.byteLength(s.problem, "utf8") > MAX_STRATEGY_PROBLEM_BYTES) {
|
|
54
|
+
return `problem exceeds ${MAX_STRATEGY_PROBLEM_BYTES} bytes`;
|
|
55
|
+
}
|
|
56
|
+
if (Buffer.byteLength(s.strategy, "utf8") > MAX_STRATEGY_TEXT_BYTES) {
|
|
57
|
+
return `strategy text exceeds ${MAX_STRATEGY_TEXT_BYTES} bytes`;
|
|
58
|
+
}
|
|
59
|
+
if (typeof s.confidence !== "number" || !Number.isFinite(s.confidence) || s.confidence < 0 || s.confidence > 3) {
|
|
60
|
+
return `confidence must be a finite number in [0,3] (got ${String(s.confidence)})`;
|
|
61
|
+
}
|
|
62
|
+
if (typeof s.ts !== "string")
|
|
63
|
+
return "ts must be an ISO timestamp string";
|
|
64
|
+
if (Buffer.byteLength(s.ts, "utf8") > MAX_STRATEGY_TS_BYTES)
|
|
65
|
+
return `ts exceeds ${MAX_STRATEGY_TS_BYTES} bytes`;
|
|
66
|
+
if (s.teacherModel !== undefined && (typeof s.teacherModel !== "string" || Buffer.byteLength(s.teacherModel, "utf8") > MAX_STRATEGY_TEACHER_MODEL_BYTES)) {
|
|
67
|
+
return `teacherModel must be a string of at most ${MAX_STRATEGY_TEACHER_MODEL_BYTES} bytes when present`;
|
|
68
|
+
}
|
|
69
|
+
if (s.signature !== undefined && (typeof s.signature !== "string" || Buffer.byteLength(s.signature, "utf8") > MAX_STRATEGY_SIGNATURE_BYTES)) {
|
|
70
|
+
return `signature must be a string of at most ${MAX_STRATEGY_SIGNATURE_BYTES} bytes when present`;
|
|
71
|
+
}
|
|
72
|
+
if (s.origin !== undefined && s.origin !== "earned" && s.origin !== "seeded") {
|
|
73
|
+
return `origin must be "earned" or "seeded" when present (got ${String(s.origin)})`;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
export function validateStrategyForWrite(s) {
|
|
78
|
+
const issue = storedStrategyShapeIssue(s);
|
|
79
|
+
if (issue !== null) {
|
|
80
|
+
throw invalidEntry(`strategy ${issue}`, typeof s.id === "string" ? s.id : undefined);
|
|
81
|
+
}
|
|
82
|
+
const t = new Date(s.ts).getTime();
|
|
83
|
+
if (!Number.isNaN(t) && t > Date.now() + MAX_STRATEGY_TS_FUTURE_MS) {
|
|
84
|
+
throw invalidEntry(`strategy ts is in the future beyond the allowed skew (${s.ts})`, s.id);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
23
87
|
const STOP = new Set([
|
|
24
88
|
"the", "and", "for", "with", "from", "this", "that", "into", "your", "you", "are", "was", "were", "has",
|
|
25
89
|
"have", "had", "not", "but", "all", "any", "can", "use", "using", "via", "then", "than", "out", "get",
|
|
26
90
|
"got", "its", "it's", "their", "them", "they", "what", "when", "which", "who", "how", "why", "a", "an",
|
|
27
91
|
"of", "to", "in", "on", "is", "it", "or", "as", "at", "by", "be", "do", "if", "so", "no", "up", "we",
|
|
28
92
|
]);
|
|
29
|
-
|
|
30
|
-
|
|
93
|
+
const CJK_LEAD = /^[\u3040-\u30FF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7A3\uF900-\uFAFF]/;
|
|
94
|
+
function terms(s) {
|
|
95
|
+
return termSet(s);
|
|
31
96
|
}
|
|
32
97
|
function significant(s) {
|
|
33
|
-
return
|
|
98
|
+
return [...terms(s)].filter((t) => (CJK_LEAD.test(t) ? true : t.length >= 3 && !STOP.has(t)));
|
|
99
|
+
}
|
|
100
|
+
export function compileStrategyQuery(query) {
|
|
101
|
+
const qSig = significant(query);
|
|
102
|
+
if (qSig.length === 0)
|
|
103
|
+
return null;
|
|
104
|
+
const qAlnum = qSig.filter((t) => !CJK_LEAD.test(t));
|
|
105
|
+
return (problem) => {
|
|
106
|
+
const probTerms = terms(problem);
|
|
107
|
+
if (qSig.every((t) => probTerms.has(t)))
|
|
108
|
+
return true;
|
|
109
|
+
return qAlnum.length > 0 && qAlnum.every((t) => probTerms.has(t));
|
|
110
|
+
};
|
|
34
111
|
}
|
|
35
|
-
function
|
|
112
|
+
export function normalizeStrategyText(s) {
|
|
36
113
|
return s.trim().toLowerCase().replace(/\s+/g, " ");
|
|
37
114
|
}
|
|
115
|
+
export function strategyDedupKey(s) {
|
|
116
|
+
return `${normalizeStrategyText(s.problem)}\u0000${normalizeStrategyText(s.strategy)}`;
|
|
117
|
+
}
|
|
38
118
|
function ageDays(tsISO) {
|
|
39
119
|
const t = new Date(tsISO).getTime();
|
|
40
120
|
if (Number.isNaN(t)) {
|
|
@@ -42,7 +122,7 @@ function ageDays(tsISO) {
|
|
|
42
122
|
}
|
|
43
123
|
return Math.max(0, Date.now() - t) / 86_400_000;
|
|
44
124
|
}
|
|
45
|
-
function
|
|
125
|
+
export function scoreStoredStrategy(s) {
|
|
46
126
|
return (s.confidence + 1) * (1 / (1 + ageDays(s.ts)));
|
|
47
127
|
}
|
|
48
128
|
export class InMemoryStrategyStore {
|
|
@@ -53,19 +133,21 @@ export class InMemoryStrategyStore {
|
|
|
53
133
|
}
|
|
54
134
|
save(s) {
|
|
55
135
|
const arr = this.byScope.get(s.scope) ?? [];
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
const dup = arr.find((e) => normalize(e.problem) === np && normalize(e.strategy) === ng);
|
|
136
|
+
const key = strategyDedupKey(s);
|
|
137
|
+
const dup = arr.find((e) => strategyDedupKey(e) === key);
|
|
59
138
|
if (dup) {
|
|
60
|
-
|
|
61
|
-
dup.
|
|
139
|
+
const incoming = Number.isFinite(s.confidence) ? s.confidence : undefined;
|
|
140
|
+
if (incoming !== undefined && incoming >= dup.confidence) {
|
|
141
|
+
dup.confidence = incoming;
|
|
142
|
+
dup.ts = s.ts;
|
|
143
|
+
}
|
|
62
144
|
dup.teacherModel = s.teacherModel ?? dup.teacherModel;
|
|
63
145
|
}
|
|
64
146
|
else {
|
|
65
147
|
arr.push(s);
|
|
66
148
|
}
|
|
67
149
|
if (arr.length > this.maxPerScope) {
|
|
68
|
-
arr.sort((a, b) =>
|
|
150
|
+
arr.sort((a, b) => scoreStoredStrategy(b) - scoreStoredStrategy(a));
|
|
69
151
|
arr.length = this.maxPerScope;
|
|
70
152
|
}
|
|
71
153
|
this.byScope.set(s.scope, arr);
|
|
@@ -76,24 +158,30 @@ export class InMemoryStrategyStore {
|
|
|
76
158
|
if (!arr || arr.length === 0) {
|
|
77
159
|
return [];
|
|
78
160
|
}
|
|
79
|
-
const
|
|
80
|
-
if (
|
|
161
|
+
const matches = compileStrategyQuery(query);
|
|
162
|
+
if (matches === null) {
|
|
81
163
|
return [];
|
|
82
164
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return matches.sort((a, b) => score(b) - score(a)).slice(0, cap);
|
|
165
|
+
return arr
|
|
166
|
+
.filter((s) => matches(s.problem))
|
|
167
|
+
.sort((a, b) => scoreStoredStrategy(b) - scoreStoredStrategy(a))
|
|
168
|
+
.slice(0, cap);
|
|
88
169
|
}
|
|
89
170
|
prune(scope, maxSize) {
|
|
90
171
|
const n = resolveCapacityCap("maxSize", maxSize);
|
|
91
172
|
const arr = this.byScope.get(scope);
|
|
92
173
|
if (arr && arr.length > n) {
|
|
93
|
-
arr.sort((a, b) =>
|
|
174
|
+
arr.sort((a, b) => scoreStoredStrategy(b) - scoreStoredStrategy(a));
|
|
94
175
|
arr.length = n;
|
|
95
176
|
}
|
|
96
177
|
}
|
|
178
|
+
scopeUsage(scope) {
|
|
179
|
+
return { used: this.byScope.get(scope)?.length ?? 0, capacity: this.maxPerScope };
|
|
180
|
+
}
|
|
181
|
+
hasStrategy(scope, entry) {
|
|
182
|
+
const key = strategyDedupKey(entry);
|
|
183
|
+
return (this.byScope.get(scope) ?? []).some((e) => strategyDedupKey(e) === key);
|
|
184
|
+
}
|
|
97
185
|
get size() {
|
|
98
186
|
let n = 0;
|
|
99
187
|
for (const arr of this.byScope.values())
|
|
@@ -101,3 +189,64 @@ export class InMemoryStrategyStore {
|
|
|
101
189
|
return n;
|
|
102
190
|
}
|
|
103
191
|
}
|
|
192
|
+
function seedRefused(message) {
|
|
193
|
+
const e = new Error(message);
|
|
194
|
+
e.code = "strategy.seed_refused";
|
|
195
|
+
return e;
|
|
196
|
+
}
|
|
197
|
+
export async function seedStrategies(store, scope, entries) {
|
|
198
|
+
if (typeof scope !== "string" || scope.length === 0) {
|
|
199
|
+
throw seedRefused("seedStrategies: scope must be a non-empty string");
|
|
200
|
+
}
|
|
201
|
+
const now = new Date().toISOString();
|
|
202
|
+
const full = [];
|
|
203
|
+
const ids = new Set();
|
|
204
|
+
const contentKeys = new Set();
|
|
205
|
+
for (let i = 0; i < entries.length; i++) {
|
|
206
|
+
const e = entries[i];
|
|
207
|
+
const s = { ...e, ts: e.ts ?? now, scope, origin: "seeded" };
|
|
208
|
+
try {
|
|
209
|
+
validateStrategyForWrite(s);
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
throw seedRefused(`seedStrategies: entry ${i} refused, zero entries written — ${err.message}`);
|
|
213
|
+
}
|
|
214
|
+
if (ids.has(s.id)) {
|
|
215
|
+
throw seedRefused(`seedStrategies: duplicate id ${JSON.stringify(s.id)} within the batch, zero entries written`);
|
|
216
|
+
}
|
|
217
|
+
ids.add(s.id);
|
|
218
|
+
const key = strategyDedupKey(s);
|
|
219
|
+
if (contentKeys.has(key)) {
|
|
220
|
+
throw seedRefused(`seedStrategies: entry ${i} (${JSON.stringify(s.id)}) duplicates another batch entry's content, zero entries written`);
|
|
221
|
+
}
|
|
222
|
+
contentKeys.add(key);
|
|
223
|
+
full.push(s);
|
|
224
|
+
}
|
|
225
|
+
if (store.scopeUsage) {
|
|
226
|
+
const usage = await store.scopeUsage(scope);
|
|
227
|
+
let netNew = full.length;
|
|
228
|
+
if (store.hasStrategy) {
|
|
229
|
+
netNew = 0;
|
|
230
|
+
for (const s of full) {
|
|
231
|
+
if (!(await store.hasStrategy(scope, s)))
|
|
232
|
+
netNew++;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const remaining = Math.max(0, usage.capacity - usage.used);
|
|
236
|
+
if (netNew > remaining) {
|
|
237
|
+
throw seedRefused(`seedStrategies: ${netNew} net-new entries exceed the scope's remaining capacity ${remaining} ` +
|
|
238
|
+
`(${usage.used}/${usage.capacity} used), zero entries written — widen the capacity or trim the batch`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
let written = 0;
|
|
242
|
+
for (const s of full) {
|
|
243
|
+
try {
|
|
244
|
+
await store.save(s);
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
return { written, failedAt: { id: s.id, error: String(err.message ?? err) } };
|
|
248
|
+
}
|
|
249
|
+
written++;
|
|
250
|
+
}
|
|
251
|
+
return { written };
|
|
252
|
+
}
|
|
@@ -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
|