@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.
Files changed (73) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/dist/agents/agent-transcript-tool.d.ts +4 -0
  3. package/dist/agents/agent-transcript-tool.js +10 -3
  4. package/dist/agents/send-message-tool.d.ts +43 -1
  5. package/dist/agents/send-message-tool.js +50 -11
  6. package/dist/agents/subagent.d.ts +18 -0
  7. package/dist/agents/subagent.js +102 -2
  8. package/dist/agents/teacher.d.ts +25 -1
  9. package/dist/agents/teacher.js +85 -12
  10. package/dist/config/defaults.d.ts +20 -0
  11. package/dist/config/defaults.js +5 -0
  12. package/dist/core/background-agent-store.d.ts +1 -0
  13. package/dist/core/background-agent-store.js +14 -0
  14. package/dist/core/mcp.d.ts +6 -1
  15. package/dist/core/mcp.js +34 -7
  16. package/dist/core/memory-engine/delegation-settlement.d.ts +27 -0
  17. package/dist/core/memory-engine/delegation-settlement.js +31 -4
  18. package/dist/core/memory-engine/dual-root.js +11 -0
  19. package/dist/core/memory-engine/engine.d.ts +6 -1
  20. package/dist/core/memory-engine/engine.js +136 -21
  21. package/dist/core/memory-engine/memory-backend-contract.js +33 -0
  22. package/dist/core/memory-engine/origin-clearance.d.ts +19 -0
  23. package/dist/core/memory-engine/origin-clearance.js +10 -0
  24. package/dist/core/memory-engine/provenance-wording.d.ts +15 -1
  25. package/dist/core/memory-engine/provenance-wording.js +1 -0
  26. package/dist/core/memory-engine/tools.js +6 -4
  27. package/dist/core/reminder-disclosure.d.ts +90 -0
  28. package/dist/core/reminder-disclosure.js +64 -0
  29. package/dist/core/runner/prepare-acquire-reconcile.d.ts +6 -0
  30. package/dist/core/runner/prepare-acquire-reconcile.js +1 -1
  31. package/dist/core/runner/prepare-hands-readface.d.ts +4 -0
  32. package/dist/core/runner/prepare-hands-readface.js +1 -0
  33. package/dist/core/runner/prepare-task.d.ts +15 -0
  34. package/dist/core/runner/prepare-task.js +51 -33
  35. package/dist/core/runner/runtask.d.ts +26 -1
  36. package/dist/core/runner/runtask.js +21 -3
  37. package/dist/core/session-store.d.ts +59 -1
  38. package/dist/core/session-store.js +82 -14
  39. package/dist/core/session.d.ts +83 -1
  40. package/dist/core/strategy-store.d.ts +180 -3
  41. package/dist/core/strategy-store.js +172 -23
  42. package/dist/core/task-registry-agent.d.ts +28 -0
  43. package/dist/core/task-registry-agent.js +63 -2
  44. package/dist/core/task-registry.d.ts +21 -0
  45. package/dist/core/task-registry.js +4 -1
  46. package/dist/core/types.d.ts +66 -0
  47. package/dist/core/untrusted-text.d.ts +63 -0
  48. package/dist/core/untrusted-text.js +48 -0
  49. package/dist/core/wiring-manifest.d.ts +35 -0
  50. package/dist/core/wiring-manifest.js +21 -1
  51. package/dist/engine/harness/types.d.ts +36 -1
  52. package/dist/index.d.ts +7 -6
  53. package/dist/index.js +6 -5
  54. package/dist/internal/harness-types.d.ts +1 -0
  55. package/dist/stores/file/file-snapshot-store.js +7 -1
  56. package/dist/stores/file/index.d.ts +27 -3
  57. package/dist/stores/file/index.js +36 -1
  58. package/dist/stores/file/session-policy-store.d.ts +0 -13
  59. package/dist/stores/file/session-policy-store.js +7 -1
  60. package/dist/stores/file/session-store.d.ts +22 -5
  61. package/dist/stores/file/session-store.js +80 -13
  62. package/dist/stores/file/strategy-store.d.ts +97 -0
  63. package/dist/stores/file/strategy-store.js +340 -0
  64. package/dist/tools/fs/fs-pdf.d.ts +12 -1
  65. package/dist/tools/fs/fs-pdf.js +17 -3
  66. package/dist/tools/fs/fs-read.d.ts +2 -1
  67. package/dist/tools/fs/fs-read.js +33 -5
  68. package/dist/tools/fs/fs-shared.d.ts +6 -2
  69. package/dist/tools/fs/index.d.ts +7 -0
  70. package/dist/tools/fs/index.js +1 -1
  71. package/dist/tools/web.js +21 -2
  72. package/package.json +3 -2
  73. package/test/export-surface.snapshot.json +22 -1
@@ -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
- * Default in-memory store. Retrieval: a stored problem must contain **every significant** query token
49
- * (and there must be ≥1) near-exact on the meaningful words, ignoring stopword noise. High precision
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(`InMemoryStrategyStore ${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
+ 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(`InMemoryStrategyStore ${name} must be a non-negative integer (got ${String(cap)})`, "config.strategy_max_size_invalid");
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
- function tokens(s) {
30
- return s.toLowerCase().match(/[a-z0-9]+/g) ?? [];
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 tokens(s).filter((t) => t.length >= 3 && !STOP.has(t));
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 normalize(s) {
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 score(s) {
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 np = normalize(s.problem);
57
- const ng = normalize(s.strategy);
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
- dup.confidence = Number.isFinite(s.confidence) ? Math.max(dup.confidence, s.confidence) : dup.confidence;
61
- dup.ts = s.ts;
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) => score(b) - score(a));
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 qSig = significant(query);
80
- if (qSig.length === 0) {
161
+ const matches = compileStrategyQuery(query);
162
+ if (matches === null) {
81
163
  return [];
82
164
  }
83
- const matches = arr.filter((s) => {
84
- const probTokens = new Set(tokens(s.problem));
85
- return qSig.every((t) => probTokens.has(t));
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) => score(b) - score(a));
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