@sema-agent/core 5.48.0 → 5.50.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 (61) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/dist/agents/agent-transcript-tool.d.ts +1 -1
  3. package/dist/agents/agent-transcript-tool.js +1 -1
  4. package/dist/agents/roster-store.js +4 -1
  5. package/dist/agents/send-message-tool.d.ts +2 -2
  6. package/dist/agents/send-message-tool.js +2 -2
  7. package/dist/agents/subagent.d.ts +6 -0
  8. package/dist/agents/subagent.js +126 -1
  9. package/dist/agents/teacher.d.ts +25 -1
  10. package/dist/agents/teacher.js +89 -13
  11. package/dist/brain/anthropic.js +11 -20
  12. package/dist/brain/open-responses.js +6 -14
  13. package/dist/brain/openai.js +6 -18
  14. package/dist/brain/reasoning.d.ts +100 -8
  15. package/dist/brain/reasoning.js +39 -15
  16. package/dist/brain/request-params.d.ts +37 -1
  17. package/dist/brain/request-params.js +40 -2
  18. package/dist/core/background-agent-store.d.ts +1 -1
  19. package/dist/core/background-agent-store.js +5 -4
  20. package/dist/core/mcp.d.ts +7 -1
  21. package/dist/core/mcp.js +64 -8
  22. package/dist/core/memory-engine/delegation-settlement.d.ts +27 -0
  23. package/dist/core/memory-engine/delegation-settlement.js +31 -4
  24. package/dist/core/memory-engine/dual-root.js +11 -0
  25. package/dist/core/memory-engine/engine.d.ts +36 -2
  26. package/dist/core/memory-engine/engine.js +354 -38
  27. package/dist/core/memory-engine/layout.d.ts +43 -0
  28. package/dist/core/memory-engine/layout.js +59 -0
  29. package/dist/core/memory-engine/memory-backend-contract.js +120 -0
  30. package/dist/core/memory-engine/origin-clearance.d.ts +19 -0
  31. package/dist/core/memory-engine/origin-clearance.js +10 -0
  32. package/dist/core/memory-engine/provenance-wording.d.ts +15 -1
  33. package/dist/core/memory-engine/provenance-wording.js +1 -0
  34. package/dist/core/memory-engine/tools.js +6 -4
  35. package/dist/core/memory-engine/types.d.ts +13 -1
  36. package/dist/core/runner/prepare-task.js +22 -8
  37. package/dist/core/runner/runtask.d.ts +26 -1
  38. package/dist/core/runner/runtask.js +18 -2
  39. package/dist/core/strategy-store.d.ts +180 -3
  40. package/dist/core/strategy-store.js +172 -23
  41. package/dist/core/task-registry-agent.js +6 -0
  42. package/dist/core/types.d.ts +24 -1
  43. package/dist/index.d.ts +2 -2
  44. package/dist/index.js +2 -2
  45. package/dist/orchestration/run-workflow-tool.d.ts +12 -0
  46. package/dist/orchestration/run-workflow-tool.js +1 -1
  47. package/dist/orchestration/workflow-governance.d.ts +27 -0
  48. package/dist/orchestration/workflow-governance.js +13 -0
  49. package/dist/orchestration/workflow-primitives.d.ts +8 -1
  50. package/dist/orchestration/workflow-primitives.js +11 -3
  51. package/dist/stores/file/file-snapshot-store.js +7 -1
  52. package/dist/stores/file/index.d.ts +8 -0
  53. package/dist/stores/file/index.js +12 -0
  54. package/dist/stores/file/session-policy-store.d.ts +0 -13
  55. package/dist/stores/file/session-policy-store.js +7 -1
  56. package/dist/stores/file/session-store.d.ts +4 -1
  57. package/dist/stores/file/session-store.js +7 -1
  58. package/dist/stores/file/strategy-store.d.ts +97 -0
  59. package/dist/stores/file/strategy-store.js +340 -0
  60. package/package.json +1 -1
  61. package/test/export-surface.snapshot.json +8 -1
@@ -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
+ }
@@ -233,6 +233,12 @@ export async function reapDurableAgentsLane(core, scope, deps, policy) {
233
233
  continue;
234
234
  }
235
235
  rowsReaped++;
236
+ {
237
+ const staleInProc = core.handles.get(r.handle);
238
+ if (staleInProc !== undefined && staleInProc.status !== "running" && staleInProc.status !== "pending" && staleInProc.status !== "parked") {
239
+ core.handles.delete(r.handle);
240
+ }
241
+ }
236
242
  core.reapedHandles.add(r.handle);
237
243
  if (deps.mailbox !== undefined) {
238
244
  try {
@@ -4730,6 +4730,15 @@ export interface EngineNotice {
4730
4730
  * `detail: { total, stripped: [{ key, reason }], omitted? }`, the rendered key list bounded in count
4731
4731
  * and length because the names come from the untrusted script. One aggregated notice per governed
4732
4732
  * child build, not de-duplicated across builds: each spec is a distinct fact.
4733
+ * - `"mcp.revocation_probe_failed"` (design/338) — the deployment's `mcpRevocations.isRevoked`
4734
+ * probe threw; MCP dispatch FAILS OPEN (revocation is a tightening face) and this announces
4735
+ * once per run. `detail: { message }`. The refusal itself (`mcp.server_revoked`) is a tool
4736
+ * RESULT code, not a notice.
4737
+ * - `"config.models_swapped"` — `Runner.swapModels` replaced the model catalog generation
4738
+ * (zero-restart model switching). `detail: { models, tiers }` — key COUNTS only, never the
4739
+ * catalog itself. In-flight tasks finish on the models they resolved at prepare (natural
4740
+ * snapshot); every later prepare resolves against the new generation. A failed swap (illegal
4741
+ * tier binding) throws atomically and mints nothing.
4733
4742
  * - `"config.read_face_deployment_clamped"` (#237) — a deployment-wide `readFace: "open"` is not
4734
4743
  * in force beside a read-only (verifier) mount: it clamps to "roots" without throwing
4735
4744
  * (stricter-wins; the clamp verdict stands, only its occurrence was undisclosed). Announced
@@ -4833,7 +4842,8 @@ export interface EngineNotice {
4833
4842
  * - `"delegation.transcript_integrity"` (subagent transcript persistence) — a durable agent row
4834
4843
  * with a BOUND transcript sessionId met a session store that attests `not_found` for it: the
4835
4844
  * 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 /
4845
+ * most once per (scope, handle, process) `detail: { handle, scope? }`, scope = the resolved
4846
+ * access scope of the read that found the gap — from the continuation read faces (SendMessage preflight /
4837
4847
  * AgentTranscript's durable leg); the per-call honest refusals are unchanged, and the declared
4838
4848
  * tier is NOT auto-downgraded (declaration-制 — observation reports, it never re-adjudicates);
4839
4849
  * `detail: { handle }`.
@@ -4892,6 +4902,19 @@ export interface RunnerDeps {
4892
4902
  brain: Brain;
4893
4903
  /** Catalog used to resolve string ModelRefs to Model objects. */
4894
4904
  models?: Record<string, Model>;
4905
+ /**
4906
+ * design/338 (mid-turn MCP revocation) — the HOST's revocation ledger, probed synchronously at
4907
+ * every MCP dispatch (tool call + the three resource tools) BEFORE the transport. The engine
4908
+ * never caches the answer: the ledger's one authority lives on the host (a cached copy would be
4909
+ * a split-state second authority). A revoked server's calls settle as the coded refusal
4910
+ * `mcp.server_revoked` with known-not-executed wording; in-flight calls a revocation raced are
4911
+ * deliberately not chased (the threat shape is "new calls after removal"). Absent seat = the
4912
+ * pre-338 semantics. A THROWING probe fails open (revocation is a tightening face — a broken
4913
+ * probe must not brick every MCP call) with a once-per-run `mcp.revocation_probe_failed` notice.
4914
+ */
4915
+ mcpRevocations?: {
4916
+ isRevoked(serverName: string): boolean;
4917
+ };
4895
4918
  /**
4896
4919
  * design/147 S1c (clay ruling 2026-07-18) — the DURABLE name→agent roster behind explicit-name
4897
4920
  * addressing, a storage-tier seam like the checkpoint store: core bundles `MemoryRosterStore`
package/dist/index.d.ts CHANGED
@@ -40,7 +40,7 @@ export type { RepetitionEvent, RepetitionInspection } from "./brain/repetition.j
40
40
  export { computeCostMicroUsd, modelCostToPricing, type ModelPricing, type TokenCounts, } from "./core/pricing.js";
41
41
  export { cacheFamilyOf, promptTokensOf, uncachedInputTokensOf, type CacheFamily } from "./core/runner/usage-accounting.js";
42
42
  export { emitTrace, type ToolDisclosureManifest, type TraceEvent, type TracerHook } from "./core/trace.js";
43
- export { InMemoryStrategyStore, type StrategyStore, type StoredStrategy } from "./core/strategy-store.js";
43
+ export { InMemoryStrategyStore, seedStrategies, type StrategyStore, type StoredStrategy, type StrategyOrigin, type StrategyStoreIncident, type SeedStrategyEntry, type SeedStrategiesReport, } from "./core/strategy-store.js";
44
44
  export { createSqlTool, validateReadOnlySql, type SqlToolOptions } from "./tools/sql.js";
45
45
  export { runWithTeacher, parseTeacherAdvice, TEACHER_PROMPT, type TeacherConfig, type TeacherAdvice, type EscalationRecord, type EscalationTrigger, type TeacherRunResult, } from "./agents/teacher.js";
46
46
  export { runWithVerification, resumeWithVerification, verifyCompleted, runDeveloperTask, VERIFICATION_PROMPT, STATIC_VERIFICATION_PROMPT, VerdictSchema, type Verdict, type VerifyConfig, type UnverifiedReason, type VerificationOutcome, type VerificationResult, type DeveloperTaskConfig, } from "./agents/verify.js";
@@ -102,7 +102,7 @@ export { ENV_LIFETIME_SUSPEND_MARGIN_MS, USAGE_WINDOW_REAP_MARGIN_MS } from "./c
102
102
  export { InMemoryFileSnapshotStore, DEFAULT_SNAPSHOT_BOUNDS } from "./core/file-snapshot-store.js";
103
103
  export { captureManifest, applyManifest } from "./core/file-snapshot-store.js";
104
104
  export type { FileSnapshotStore, FileSnapshotResult, FileSnapshotError, FileSnapshotBounds } from "./core/file-snapshot-store.js";
105
- export { FileStorageBackend, FileSessionRepo, FileCheckpointStore, FileMemoryStore, FileToolResultStore, FileSessionPolicyStore, FileFileSnapshotStore, FileWorkflowJournalStore, MAX_JOURNAL_RESULT_BYTES, oversizeJournalResult, resolveDataRoot, sanitizeScope, sanitizePathComponent, createFileConsolidationLock, atomicWriteFile, writeThenLink, ensureDir, readJsonlRecords, AppendLog, type FileStorageBackendOptions, type FileStorageCorruptReadInfo, type FileSessionRepoOptions, type FileFileSnapshotStoreOptions, type FileCheckpointStoreOptions, } from "./stores/file/index.js";
105
+ export { FileStorageBackend, FileSessionRepo, FileCheckpointStore, FileMemoryStore, FileToolResultStore, FileSessionPolicyStore, FileFileSnapshotStore, FileStrategyStore, FileWorkflowJournalStore, MAX_JOURNAL_RESULT_BYTES, oversizeJournalResult, resolveDataRoot, sanitizeScope, sanitizePathComponent, createFileConsolidationLock, atomicWriteFile, writeThenLink, ensureDir, readJsonlRecords, AppendLog, type FileStorageBackendOptions, type FileStorageCorruptReadInfo, type FileStrategyStoreOptions, type FileSessionRepoOptions, type FileFileSnapshotStoreOptions, type FileCheckpointStoreOptions, } from "./stores/file/index.js";
106
106
  export { CacheBreakDetector, type CacheBreakFinding, type ToolFingerprintInput } from "./core/cache-break-detector.js";
107
107
  export { maybeCompact, type MaybeCompactOptions, type CompactionWindowSafetyInfo } from "./core/auto-compaction.js";
108
108
  export { brainToRuntime } from "./core/runtime.js";
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ export { looksDegenerate, inspectDegenerate, trimDegenerateTail } from "./brain/
28
28
  export { computeCostMicroUsd, modelCostToPricing, } from "./core/pricing.js";
29
29
  export { cacheFamilyOf, promptTokensOf, uncachedInputTokensOf } from "./core/runner/usage-accounting.js";
30
30
  export { emitTrace } from "./core/trace.js";
31
- export { InMemoryStrategyStore } from "./core/strategy-store.js";
31
+ export { InMemoryStrategyStore, seedStrategies, } from "./core/strategy-store.js";
32
32
  export { createSqlTool, validateReadOnlySql } from "./tools/sql.js";
33
33
  export { runWithTeacher, parseTeacherAdvice, TEACHER_PROMPT, } from "./agents/teacher.js";
34
34
  export { runWithVerification, resumeWithVerification, verifyCompleted, runDeveloperTask, VERIFICATION_PROMPT, STATIC_VERIFICATION_PROMPT, VerdictSchema, } from "./agents/verify.js";
@@ -81,7 +81,7 @@ export { FileUsageWindowStore } from "./stores/file/usage-window-store.js";
81
81
  export { ENV_LIFETIME_SUSPEND_MARGIN_MS, USAGE_WINDOW_REAP_MARGIN_MS } from "./core/runner/prepare-task.js";
82
82
  export { InMemoryFileSnapshotStore, DEFAULT_SNAPSHOT_BOUNDS } from "./core/file-snapshot-store.js";
83
83
  export { captureManifest, applyManifest } from "./core/file-snapshot-store.js";
84
- export { FileStorageBackend, FileSessionRepo, FileCheckpointStore, FileMemoryStore, FileToolResultStore, FileSessionPolicyStore, FileFileSnapshotStore, FileWorkflowJournalStore, MAX_JOURNAL_RESULT_BYTES, oversizeJournalResult, resolveDataRoot, sanitizeScope, sanitizePathComponent, createFileConsolidationLock, atomicWriteFile, writeThenLink, ensureDir, readJsonlRecords, AppendLog, } from "./stores/file/index.js";
84
+ export { FileStorageBackend, FileSessionRepo, FileCheckpointStore, FileMemoryStore, FileToolResultStore, FileSessionPolicyStore, FileFileSnapshotStore, FileStrategyStore, FileWorkflowJournalStore, MAX_JOURNAL_RESULT_BYTES, oversizeJournalResult, resolveDataRoot, sanitizeScope, sanitizePathComponent, createFileConsolidationLock, atomicWriteFile, writeThenLink, ensureDir, readJsonlRecords, AppendLog, } from "./stores/file/index.js";
85
85
  export { CacheBreakDetector } from "./core/cache-break-detector.js";
86
86
  export { maybeCompact } from "./core/auto-compaction.js";
87
87
  export { brainToRuntime } from "./core/runtime.js";
@@ -234,6 +234,18 @@ export interface RunWorkflowToolDeps {
234
234
  parentReadFace?: () => import("../core/types.js").TaskSpec["readFace"];
235
235
  /** Twin of the above for the deny-set additions (union, not replace). */
236
236
  parentReadDenyPatterns?: () => import("../core/types.js").TaskSpec["readDenyPatterns"];
237
+ /** #345 ③ — the HOST task's write-hands clamp, the read-face clamp's sibling on this lane (the
238
+ * subagent delegation lane has carried it as `ToolExecuteContext.handsReadOnly` since its own
239
+ * tighten-only ruling; the workflow lane was the remaining gap — a read-only host's workflow
240
+ * children spawned with full write hands). Filled ONLY when the clamp is ON (`true`), exactly
241
+ * like the ctx seat, so an unclamped deployment's mount gains no key. Known at prepare-time
242
+ * (frozen TaskSpec snapshot — no lazy getter needed, the `parentCheckpointStoreDisabled` shape). */
243
+ parentHandsReadOnly?: true;
244
+ /** #345 ③ — the HOST task's hard-headless clamp (`interactiveTools: false`), same carriage rules
245
+ * as `parentHandsReadOnly` above: only the DISABLING value travels, mirroring the subagent lane's
246
+ * `ToolExecuteContext.interactiveTools` seat — without it a headless-clamped host's workflow
247
+ * children could re-mount the interactive tool the host run disabled. */
248
+ parentInteractiveTools?: false;
237
249
  /** [1238](A) — call-time getter for the HOST task's RESOLVED Model object: a spawned agent whose
238
250
  * fold chain produced no model inherits the parent's full object (baseUrl/key routing included),
239
251
  * mirroring the subagent lane's ctx.model semantics. */
@@ -468,7 +468,7 @@ export async function createRunWorkflowTool(d) {
468
468
  }
469
469
  : governance;
470
470
  const scriptFn = (wfCtx) => {
471
- const primitives = buildWorkflowPrimitives(wfCtx, runGovernance, d.onAgentSpawn, d.parentThinking, principal, ctx.checkpointStoreDisabledForChildren === true || d.parentCheckpointStoreDisabled === true, d.parentReadFace, d.parentReadDenyPatterns);
471
+ const primitives = buildWorkflowPrimitives(wfCtx, runGovernance, d.onAgentSpawn, d.parentThinking, principal, ctx.checkpointStoreDisabledForChildren === true || d.parentCheckpointStoreDisabled === true, d.parentReadFace, d.parentReadDenyPatterns, ctx.handsReadOnly === true || d.parentHandsReadOnly === true, ctx.interactiveTools === false || d.parentInteractiveTools === false);
472
472
  return d.scriptRunner.run({ scriptSource: script, primitives, scriptArgs: effectiveArgs, signal: wfCtx.signal }).then((r) => r.result);
473
473
  };
474
474
  if (ctx.signal?.aborted) {