@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
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";
@@ -90,7 +90,7 @@ export { createFsWriteGatePolicy } from "./core/fs-write-gate-policy.js";
90
90
  export { RETIRED_TOOL_NAMES } from "./core/tool-name-aliases.js";
91
91
  export { DEFAULT_SUBAGENT_TOOL_NAME } from "./agents/subagent.js";
92
92
  export { renderTaskNotificationXml, taskNotificationDedupKey, isDelegatedAgentTerminal, SystemInjectionQueue, } from "./core/task-notification.js";
93
- export { describeStaticWiring, deriveWiringManifest, deriveAskEffective, resolveDeclaredDurability, resolveAskSeamForm, resolveQuestionSeam, countElicitOptIns, } from "./core/wiring-manifest.js";
93
+ export { describeStaticWiring, deriveWiringManifest, deriveAskEffective, resolveDeclaredDurability, resolveSubagentTranscriptTier, resolveAskSeamForm, resolveQuestionSeam, countElicitOptIns, } from "./core/wiring-manifest.js";
94
94
  export { probeParkRoundTrip, durableParkGapOf, durableParkGapFor, PARK_SELFCHECK_SCOPE_PREFIX, } from "./core/park-selfcheck.js";
95
95
  export {} from "./core/checkpoint-store.js";
96
96
  export {} from "./core/checkpoint-store.js";
@@ -193,10 +193,11 @@ export { summarizeRedactions } from "./core/untrusted-egress.js";
193
193
  export { MemoryRosterStore, FileRosterStore } from "./agents/roster-store.js";
194
194
  export { CONFIG_CATALOG_VERSION, describeConfigCatalog, resolveEffectiveConfig, } from "./config/catalog.js";
195
195
  export { normalizeAgentName } from "./core/task-registry.js";
196
+ export { DELEGATION_MAX_CONCURRENT_DEFAULT, DELEGATION_MAX_PER_SESSION_DEFAULT, ORPHAN_ADOPT_WINDOW_MS_DEFAULT, ORPHAN_ADOPT_MAX_DEFAULT, SUBAGENT_TRANSCRIPT_RETENTION_DAYS_DEFAULT, } from "./config/defaults.js";
196
197
  export { COORDINATOR_ROLE_PROMPT, TEAMMATE_COMMUNICATION_ADDENDUM, TEAMMATE_TASK_LIST_ADDENDUM } from "./prompts/coordinator.js";
197
- export { createSubagentTool, FORK_SUBAGENT_TYPE, GENERAL_PURPOSE_SUBAGENT_TYPE, agentWhenToUseText, FORK_DIRECTIVE_FRAME, SUBAGENT_SYSTEM_NOTE, } from "./agents/subagent.js";
198
+ export { createSubagentTool, FORK_SUBAGENT_TYPE, GENERAL_PURPOSE_SUBAGENT_TYPE, agentWhenToUseText, FORK_DIRECTIVE_FRAME, SUBAGENT_SYSTEM_NOTE, resolveDelegationEntryCaps, } from "./agents/subagent.js";
198
199
  export { getSessionRetainLedger, releaseSessionRetainLedger, } from "./agents/retain-ledger.js";
199
- export { createSendMessageTool, SEND_MESSAGE_TOOL_NAME } from "./agents/send-message-tool.js";
200
+ export { createSendMessageTool, createAgentContinuationVerb, SEND_MESSAGE_TOOL_NAME } from "./agents/send-message-tool.js";
200
201
  export { createPeerAdmission, peerAdmissionFor, judgePeerAdmission, resolvePeerAdmissionConfig, PEER_ADMISSION_DEFAULTS, PEER_HOP_CHAIN_WINDOW, PEER_MESSAGE_NOTICE, createPeerSelfRef, createPeerInboundChainRef, peerAxisToken, appendHopToken, } from "./agents/peer-admission.js";
201
202
  export { createAgentTranscriptTool, AGENT_TRANSCRIPT_TOOL_NAME, } from "./agents/agent-transcript-tool.js";
202
203
  export { defineAgent } from "./agents/agent-definition.js";
@@ -14,3 +14,4 @@ export type { SessionWriteOptions, CompactionEntry } from "../engine/harness/typ
14
14
  export type { ActiveWorktreeSession, WorkspaceState } from "../engine/harness/types.js";
15
15
  export type { GitAnnouncementKind, GitAnnouncementState } from "../engine/harness/types.js";
16
16
  export type { SessionForkOptions } from "../engine/harness/types.js";
17
+ export type { SessionPlacementRecord, SessionCreateOptions } from "../engine/harness/types.js";
@@ -5,6 +5,12 @@ import { applyManifest, captureManifest, DEFAULT_SNAPSHOT_BOUNDS, } from "../../
5
5
  import { canonicalStoreKey, ensureDir, sanitizePathComponent, sanitizeScope, writeThenLink } from "./fs-atomic.js";
6
6
  import { assertAdoptionBootGate } from "./adoption/marker.js";
7
7
  const sharedInFlight = new Map();
8
+ function containSinkThenable(r) {
9
+ if (typeof r?.then === "function") {
10
+ r.then(undefined, () => {
11
+ });
12
+ }
13
+ }
8
14
  export class FileFileSnapshotStore {
9
15
  base;
10
16
  blobsDir;
@@ -37,7 +43,7 @@ export class FileFileSnapshotStore {
37
43
  }
38
44
  disclose(path, reason) {
39
45
  try {
40
- this.onCorruptRead?.({ path, reason });
46
+ containSinkThenable(this.onCorruptRead?.({ path, reason }));
41
47
  }
42
48
  catch {
43
49
  }
@@ -4,6 +4,10 @@ import type { ToolResultStore } from "../../core/tool-result-store.js";
4
4
  import { type EvictPolicy } from "../../core/session-store.js";
5
5
  import type { SessionStore } from "../../core/session.js";
6
6
  import { type FileCheckpointStoreOptions } from "./checkpoint-store.js";
7
+ import type { StrategyStore } from "../../core/strategy-store.js";
8
+ import type { BackgroundAgentStore } from "../../core/background-agent-store.js";
9
+ import type { MailboxStore } from "../../core/mailbox-store.js";
10
+ import type { RosterStore } from "../../agents/roster-store.js";
7
11
  import type { SessionPolicyStore } from "../../core/session-policy-store.js";
8
12
  import type { FileSnapshotStore } from "../../core/file-snapshot-store.js";
9
13
  import type { WorkflowJournalStore } from "../../core/workflow-journal-store.js";
@@ -16,6 +20,7 @@ export { FileSessionPolicyStore, type FileSessionPolicyStoreOptions, type Sessio
16
20
  export { FileFileSnapshotStore, type FileFileSnapshotStoreOptions } from "./file-snapshot-store.js";
17
21
  export { FileWorkflowJournalStore, MAX_JOURNAL_RESULT_BYTES, oversizeJournalResult } from "./workflow-journal-store.js";
18
22
  export { FileUsageWindowStore } from "./usage-window-store.js";
23
+ export { FileStrategyStore, type FileStrategyStoreOptions } from "./strategy-store.js";
19
24
  export { resolveDataRoot, sanitizeScope, sanitizePathComponent, createFileConsolidationLock } from "./fs-atomic.js";
20
25
  export { atomicWriteFile, writeThenLink, ensureDir, readJsonlRecords, AppendLog } from "./fs-atomic.js";
21
26
  export { AdoptionError, assertAdoptionBootGate, readRootAdoptionFile, writeRootAdoptionFile, ROOT_ADOPTION_FILE, type AdoptionErrorCode, type AdoptionSource, type AdoptionReport, type AdoptionReceipt, type AdoptionLegReport, type AffectedDeploymentConfig, type RootAdoptionFile, } from "./adoption/marker.js";
@@ -76,9 +81,8 @@ export interface FileStorageBackendOptions {
76
81
  * Reach TODAY: {@link FileStorageBackend.sessionPolicyStore} (rules read as ABSENT ⇒ an unconstrained
77
82
  * task), {@link FileStorageBackend.sessionStore}'s repo (a session listing that silently omits what it
78
83
  * could not read) and {@link FileStorageBackend.fileSnapshotStore} (a snapshot/scope read as missing,
79
- * and a blob GC that aborted). The roster and CC-mailbox stores also carry the seat, but a deployment
80
- * builds those itself and passes the sink directly. A store that grows the seat later is wired here in
81
- * the same constructor.
84
+ * and a blob GC that aborted). The aggregated roster and mailbox stores receive the same sink. A
85
+ * store that grows the seat later is wired here in the same constructor.
82
86
  *
83
87
  * The payload is the UNION shape ({@link FileStorageCorruptReadInfo}): `path` + `reason` always,
84
88
  * with the session-policy face's `sessionId`/`principal` present only when the reading store keys by
@@ -125,6 +129,24 @@ export declare class FileStorageBackend {
125
129
  * `RunnerDeps.usageWindowStore` so a deployment's `usageWindows` survive a restart (an in-memory
126
130
  * ledger silently re-grants every allowance on every boot). */
127
131
  readonly usageWindowStore: UsageWindowStore;
132
+ /** design/151 S1a / subagent transcript persistence — the durable background-agent execution
133
+ * ledger over the same data root. Wire into BOTH `RunnerDeps.backgroundAgentStore` AND (for a
134
+ * deployment-composed Agent tool) `SubagentToolOptions.background.agentStore` — SAME instance
135
+ * (the RB-37 pairing discipline). Aggregated here so the TOC full-persistence assembly is one
136
+ * constructor, the same reason the session/checkpoint/tool-result trio are. */
137
+ readonly backgroundAgentStore: BackgroundAgentStore;
138
+ /** design/151 §7 S3c — the durable SendMessage mailbox (tier-3 parking lane), same data root.
139
+ * Wire into `RunnerDeps.mailboxStore` (single-instance pairing, same as above). */
140
+ readonly mailboxStore: MailboxStore;
141
+ /** File-backed teacher-mode strategy repository (`root/strategies`) — pass as
142
+ * `TeacherConfig.strategyStore` (with a `scope`) so escalation strategies survive a restart;
143
+ * the in-memory default forgets everything each process, which zeroes the cross-session reuse
144
+ * the repository exists for. Aggregated here so the TOC full-persistence assembly stays one
145
+ * constructor (the same half-wiring hazard as the delegation trio). */
146
+ readonly strategyStore: StrategyStore;
147
+ /** design/147 S1c — the durable name→agent roster (layer 0.5 resolution), `root/roster.json`.
148
+ * Wire into `RunnerDeps.rosterStore` so name-addressing survives a restart alongside the rows. */
149
+ readonly rosterStore: RosterStore;
128
150
  /**
129
151
  * design/84 Seam B (TOC) — the per-scope consolidation lock for `consolidateScope`'s `acquire` injection
130
152
  * point: `consolidateScope(scope, { store: backend.memoryStore, llm, acquire: backend.consolidationLock })`.
@@ -133,6 +155,8 @@ export declare class FileStorageBackend {
133
155
  */
134
156
  readonly consolidationLock: (scope: string) => (() => void) | undefined;
135
157
  private readonly lock;
158
+ private readonly fileAgentRows;
159
+ private readonly fileMailbox;
136
160
  private readonly fileCheckpoints;
137
161
  private readonly fileMemory;
138
162
  private readonly ttl;
@@ -11,6 +11,10 @@ import { FileSessionPolicyStore } from "./session-policy-store.js";
11
11
  import { FileFileSnapshotStore } from "./file-snapshot-store.js";
12
12
  import { FileWorkflowJournalStore } from "./workflow-journal-store.js";
13
13
  import { FileUsageWindowStore } from "./usage-window-store.js";
14
+ import { FileBackgroundAgentStore } from "./background-agent-store.js";
15
+ import { FileMailboxStore } from "./mailbox-store.js";
16
+ import { FileRosterStore } from "../../agents/roster-store.js";
17
+ import { FileStrategyStore } from "./strategy-store.js";
14
18
  export { FileCheckpointStore } from "./checkpoint-store.js";
15
19
  export { FileMemoryStore } from "./memory-store.js";
16
20
  export { FileSessionRepo } from "./session-store.js";
@@ -19,6 +23,7 @@ export { FileSessionPolicyStore } from "./session-policy-store.js";
19
23
  export { FileFileSnapshotStore } from "./file-snapshot-store.js";
20
24
  export { FileWorkflowJournalStore, MAX_JOURNAL_RESULT_BYTES, oversizeJournalResult } from "./workflow-journal-store.js";
21
25
  export { FileUsageWindowStore } from "./usage-window-store.js";
26
+ export { FileStrategyStore } from "./strategy-store.js";
22
27
  export { resolveDataRoot, sanitizeScope, sanitizePathComponent, createFileConsolidationLock } from "./fs-atomic.js";
23
28
  export { atomicWriteFile, writeThenLink, ensureDir, readJsonlRecords, AppendLog } from "./fs-atomic.js";
24
29
  export { AdoptionError, assertAdoptionBootGate, readRootAdoptionFile, writeRootAdoptionFile, ROOT_ADOPTION_FILE, } from "./adoption/marker.js";
@@ -33,8 +38,14 @@ export class FileStorageBackend {
33
38
  fileSnapshotStore;
34
39
  workflowJournalStore;
35
40
  usageWindowStore;
41
+ backgroundAgentStore;
42
+ mailboxStore;
43
+ strategyStore;
44
+ rosterStore;
36
45
  consolidationLock;
37
46
  lock;
47
+ fileAgentRows;
48
+ fileMailbox;
38
49
  fileCheckpoints;
39
50
  fileMemory;
40
51
  ttl;
@@ -49,7 +60,7 @@ export class FileStorageBackend {
49
60
  const corruptRead = opts.onCorruptRead !== undefined ? { onCorruptRead: opts.onCorruptRead } : undefined;
50
61
  const repo = new FileSessionRepo(this.root, corruptRead);
51
62
  this.fileSessions = repo;
52
- this.ttl = new TtlSessionStore({ repo, evict: opts.evict ?? "forget", durability: "durable" });
63
+ this.ttl = new TtlSessionStore({ repo, evict: opts.evict ?? "forget", durability: "durable", placements: { subagent: { durability: "durable" } } });
53
64
  this.sessionStore = this.ttl;
54
65
  this.fileCheckpoints = new FileCheckpointStore(this.root, opts.checkpoint);
55
66
  this.checkpointStore = this.fileCheckpoints;
@@ -61,6 +72,20 @@ export class FileStorageBackend {
61
72
  this.fileWorkflowJournal = new FileWorkflowJournalStore(this.root);
62
73
  this.workflowJournalStore = this.fileWorkflowJournal;
63
74
  this.usageWindowStore = new FileUsageWindowStore(this.root);
75
+ this.fileAgentRows = new FileBackgroundAgentStore(this.root);
76
+ this.backgroundAgentStore = this.fileAgentRows;
77
+ this.fileMailbox = new FileMailboxStore(this.root, corruptRead ?? {});
78
+ this.mailboxStore = this.fileMailbox;
79
+ this.rosterStore = new FileRosterStore(join(this.root, "roster.json"), corruptRead ?? {});
80
+ const strategiesRoot = join(this.root, "strategies");
81
+ this.strategyStore = new FileStrategyStore({
82
+ root: strategiesRoot,
83
+ ...(opts.onCorruptRead !== undefined
84
+ ? {
85
+ onIncident: (i) => opts.onCorruptRead?.({ path: i.path ?? strategiesRoot, reason: `strategy ${i.op}: ${i.error}` }),
86
+ }
87
+ : {}),
88
+ });
64
89
  this.consolidationLock = createFileConsolidationLock(join(this.root, "consolidation-locks"));
65
90
  }
66
91
  catch (err) {
@@ -87,6 +112,16 @@ export class FileStorageBackend {
87
112
  }
88
113
  this.fileMemory.close();
89
114
  this.fileWorkflowJournal.dispose();
115
+ try {
116
+ this.fileAgentRows.close();
117
+ }
118
+ catch {
119
+ }
120
+ try {
121
+ this.fileMailbox.close();
122
+ }
123
+ catch {
124
+ }
90
125
  this.lock.release();
91
126
  }
92
127
  }
@@ -1,17 +1,4 @@
1
1
  import { type PutRulesOptions, type SessionPermissionRules, type SessionPolicyStore, type SessionRulesRecord, type StoredSessionRules } from "../../core/session-policy-store.js";
2
- /**
3
- * design/99 §E6 — file-backed {@link SessionPolicyStore} for the local (TOC) backend: ONE JSON file per
4
- * `(sessionId, principal)`, semantics **byte-for-byte identical to `InMemorySessionPolicyStore`** (the
5
- * cross-backend equivalence contract). CAS-rev OCC + tighten-only reuse the SAME pure helpers
6
- * (`loosenReasons`/`normalizeRules`/`stripRev`) as core — no re-implemented rule logic that could drift.
7
- *
8
- * Atomicity: the file backend is single-process (the `FileStorageBackend` boot lock guarantees ONE writer per
9
- * data dir), and `getRules`/`putRules` read-check-write SYNCHRONOUSLY (no await between the rev read and the
10
- * atomic write), so the read-modify-write is atomic in the one event loop — exactly the InMemory store's premise.
11
- * Cross-process CORRECT concurrency is the Pg/TiDB backend's job (a CAS WHERE clause), by design.
12
- */
13
- /** Coordinates of one corrupt-treated-as-absent policy read (the {@link FileSessionPolicyStoreOptions.onCorruptRead}
14
- * payload). Named rather than inline so the backend option that forwards it names the SAME shape. */
15
2
  export interface SessionPolicyCorruptReadInfo {
16
3
  /** The session whose read observed the corrupt row. On the enumeration face this is the session being
17
4
  * ENUMERATED — a corrupt row's own `__sid` is by definition unreadable, so it cannot be attributed. */
@@ -3,6 +3,12 @@ import { join } from "node:path";
3
3
  import { loosenReasons, normalizeRules, stripRev, SessionPolicyError, } from "../../core/session-policy-store.js";
4
4
  import { atomicWriteFile, ensureDir, sanitizeScope } from "./fs-atomic.js";
5
5
  import { assertAdoptionBootGate, readRootAdoptionFile } from "./adoption/marker.js";
6
+ function containSinkThenable(r) {
7
+ if (typeof r?.then === "function") {
8
+ r.then(undefined, () => {
9
+ });
10
+ }
11
+ }
6
12
  export class FileSessionPolicyStore {
7
13
  dir;
8
14
  onCorruptRead;
@@ -18,7 +24,7 @@ export class FileSessionPolicyStore {
18
24
  }
19
25
  disclose(info) {
20
26
  try {
21
- this.onCorruptRead?.(info);
27
+ containSinkThenable(this.onCorruptRead?.(info));
22
28
  }
23
29
  catch {
24
30
  }
@@ -1,4 +1,5 @@
1
- import type { Session, SessionForkOptions, SessionMetadata, SessionRepo, SessionTreeEntry } from "../../internal/harness.js";
1
+ import type { Session, SessionCreateOptions, SessionForkOptions, SessionMetadata, SessionPlacementRecord, SessionRepo, SessionTreeEntry } from "../../internal/harness.js";
2
+ import type { PlacedSessionRow } from "../../core/session.js";
2
3
  export interface FileSessionRepoOptions {
3
4
  /**
4
5
  * Disclosure sink for a durable read this repo treats as ABSENT (ruled 2026-08-03). Same name and
@@ -28,7 +29,10 @@ export declare class FileSessionRepo implements SessionRepo {
28
29
  private readonly joined;
29
30
  constructor(root: string, opts?: FileSessionRepoOptions);
30
31
  /** The one delivery point for {@link FileSessionRepoOptions.onCorruptRead}; swallow-guarded here so
31
- * no call site has to remember. */
32
+ * no call site has to remember. The sink seat is void-typed but a host may hand it an async
33
+ * function — an async sink's rejection is observed off its returned thenable (same containment
34
+ * as the strategy store's incident sink), so neither a sync throw nor an async rejection can
35
+ * re-introduce the failure mode the fail-open avoids. */
32
36
  private disclose;
33
37
  private pathFor;
34
38
  /** Read + torn-tail-recover a session file into (meta, entries); a missing file → not_found. */
@@ -47,11 +51,24 @@ export declare class FileSessionRepo implements SessionRepo {
47
51
  * means a stale holder can never revoke the rebuilt entry.
48
52
  */
49
53
  dispose(): Promise<void>;
50
- create(options?: {
51
- id?: string;
52
- }): Promise<Session>;
54
+ create(options?: SessionCreateOptions): Promise<Session>;
53
55
  open(metadata: SessionMetadata): Promise<Session>;
54
56
  list(): Promise<SessionMetadata[]>;
57
+ /**
58
+ * Subagent transcript persistence — the PLACED partition's own enumeration (the joint reap's
59
+ * partition-leg input; `TtlSessionStore` re-exposes it verbatim). Age (`olderThanMs`) is judged on
60
+ * the session FILE's mtime — the transcript's last append — falling back to `placedAt` when the
61
+ * stat fails (mtime is also the CC orphan-adoption window's basis, so the two age reads agree).
62
+ * Rows missing either `(scope, handle)` join half return the honest `tupleIncomplete` variant
63
+ * (consumers fail closed on it — never age-reaped, see the SessionStore contract).
64
+ */
65
+ listPlaced(kind: "subagent", opts?: {
66
+ olderThanMs?: number;
67
+ scope?: string;
68
+ }): Promise<PlacedSessionRow[]>;
69
+ /** Cheap placement probe for one id (the TtlSessionStore's cold-cache release consults it so the
70
+ * placed real-deletion obligation holds after a restart). `undefined` = missing file OR unplaced. */
71
+ placementOf(sessionId: string): Promise<SessionPlacementRecord | undefined>;
55
72
  delete(metadata: SessionMetadata): Promise<void>;
56
73
  fork(sourceMetadata: SessionMetadata, options?: SessionForkOptions): Promise<Session>;
57
74
  /** 2c session-sync ([275], the FILE half of [266]②): the FULL oldest-first log for export. The file backend
@@ -1,8 +1,14 @@
1
- import { existsSync, readdirSync, rmSync } from "node:fs";
1
+ import { existsSync, readdirSync, rmSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { BaseSessionStorage, StoredSession, SessionError, getEntriesToFork, uuidv7, validateEntriesForImport, } from "../../internal/harness.js";
4
4
  import { canonicalStoreKey, AppendLog, atomicWriteFile, ensureDir, readJsonlRecords, sanitizePathComponent } from "./fs-atomic.js";
5
5
  import { assertAdoptionBootGate } from "./adoption/marker.js";
6
+ function containSinkThenable(r) {
7
+ if (typeof r?.then === "function") {
8
+ r.then(undefined, () => {
9
+ });
10
+ }
11
+ }
6
12
  const SUFFIX = ".jsonl";
7
13
  const sharedSessionStorages = new Map();
8
14
  const sessionStorageFinalizer = new FinalizationRegistry(({ canonical, log }) => {
@@ -67,7 +73,7 @@ export class FileSessionRepo {
67
73
  }
68
74
  disclose(path, reason) {
69
75
  try {
70
- this.onCorruptRead?.({ path, reason });
76
+ containSinkThenable(this.onCorruptRead?.({ path, reason }));
71
77
  }
72
78
  catch {
73
79
  }
@@ -83,18 +89,20 @@ export class FileSessionRepo {
83
89
  const lines = readJsonlRecords(path, (info) => this.disclose(info.path, info.reason));
84
90
  let createdAt = "";
85
91
  let forkedFrom;
92
+ let placement;
86
93
  const entries = [];
87
94
  for (const line of lines) {
88
95
  if (line.kind === "meta") {
89
96
  createdAt = line.createdAt;
90
97
  forkedFrom = line.forkedFrom;
98
+ placement = line.placement;
91
99
  }
92
100
  else
93
101
  entries.push(line.entry);
94
102
  }
95
- return { createdAt, ...(forkedFrom !== undefined ? { forkedFrom } : {}), entries };
103
+ return { createdAt, ...(forkedFrom !== undefined ? { forkedFrom } : {}), ...(placement !== undefined ? { placement } : {}), entries };
96
104
  }
97
- storage(id, createdAt, entries, forkedFrom) {
105
+ storage(id, createdAt, entries, forkedFrom, placement) {
98
106
  const canonical = canonicalStoreKey(this.pathFor(id));
99
107
  const live = sharedSessionStorages.get(canonical)?.deref();
100
108
  if (live !== undefined) {
@@ -103,7 +111,7 @@ export class FileSessionRepo {
103
111
  return live;
104
112
  }
105
113
  const log = new AppendLog(this.pathFor(id));
106
- const created = new FileSessionStorage(log, { id, createdAt, ...(forkedFrom !== undefined ? { forkedFrom } : {}) }, entries, canonical);
114
+ const created = new FileSessionStorage(log, { id, createdAt, ...(forkedFrom !== undefined ? { forkedFrom } : {}), ...(placement !== undefined ? { placement } : {}) }, entries, canonical);
107
115
  sharedSessionStorages.set(canonical, new WeakRef(created));
108
116
  created.addHolder(this);
109
117
  this.joined.set(canonical, new WeakRef(created));
@@ -128,15 +136,16 @@ export class FileSessionRepo {
128
136
  const path = this.pathFor(id);
129
137
  if (!existsSync(path)) {
130
138
  evictSharedSessionStorage(canonicalStoreKey(path));
131
- atomicWriteFile(this.tmpDir, path, `${JSON.stringify({ kind: "meta", id, createdAt })}\n`);
132
- return new StoredSession(this.storage(id, createdAt, []));
139
+ const placement = options.placement !== undefined ? { ...options.placement, placedAt: options.placement.placedAt ?? Date.now() } : undefined;
140
+ atomicWriteFile(this.tmpDir, path, `${JSON.stringify({ kind: "meta", id, createdAt, ...(placement !== undefined ? { placement } : {}) })}\n`);
141
+ return new StoredSession(this.storage(id, createdAt, [], undefined, placement));
133
142
  }
134
- const { createdAt: existingCreatedAt, forkedFrom, entries } = this.read(id);
135
- return new StoredSession(this.storage(id, existingCreatedAt || createdAt, entries, forkedFrom));
143
+ const { createdAt: existingCreatedAt, forkedFrom, placement, entries } = this.read(id);
144
+ return new StoredSession(this.storage(id, existingCreatedAt || createdAt, entries, forkedFrom, placement));
136
145
  }
137
146
  async open(metadata) {
138
- const { createdAt, forkedFrom, entries } = this.read(metadata.id);
139
- return new StoredSession(this.storage(metadata.id, createdAt, entries, forkedFrom));
147
+ const { createdAt, forkedFrom, placement, entries } = this.read(metadata.id);
148
+ return new StoredSession(this.storage(metadata.id, createdAt, entries, forkedFrom, placement));
140
149
  }
141
150
  async list() {
142
151
  let names;
@@ -155,7 +164,9 @@ export class FileSessionRepo {
155
164
  continue;
156
165
  const id = name.slice(0, -SUFFIX.length);
157
166
  try {
158
- const { createdAt, forkedFrom } = this.read(id);
167
+ const { createdAt, forkedFrom, placement } = this.read(id);
168
+ if (placement !== undefined)
169
+ continue;
159
170
  out.push({ id, createdAt, ...(forkedFrom !== undefined ? { forkedFrom } : {}) });
160
171
  }
161
172
  catch (err) {
@@ -166,12 +177,68 @@ export class FileSessionRepo {
166
177
  }
167
178
  return out.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
168
179
  }
180
+ async listPlaced(kind, opts) {
181
+ let names;
182
+ try {
183
+ names = readdirSync(this.dir);
184
+ }
185
+ catch (err) {
186
+ if (err.code !== "ENOENT") {
187
+ this.disclose(this.dir, `session directory unreadable (${err.code ?? "unknown"}) — placed listing degraded to empty`);
188
+ }
189
+ return [];
190
+ }
191
+ const now = Date.now();
192
+ const out = [];
193
+ for (const name of names) {
194
+ if (!name.endsWith(SUFFIX))
195
+ continue;
196
+ const id = name.slice(0, -SUFFIX.length);
197
+ let placement;
198
+ try {
199
+ placement = this.read(id).placement;
200
+ }
201
+ catch (err) {
202
+ if (!(err instanceof SessionError && err.code === "not_found")) {
203
+ this.disclose(this.pathFor(id), `session log unreadable (${err instanceof Error ? err.message : String(err)}) — skipped from the placed listing`);
204
+ }
205
+ continue;
206
+ }
207
+ if (placement === undefined || placement.kind !== kind)
208
+ continue;
209
+ if (opts?.scope !== undefined && placement.scope !== opts.scope)
210
+ continue;
211
+ if (opts?.olderThanMs !== undefined) {
212
+ let ageAnchor = placement.placedAt;
213
+ try {
214
+ ageAnchor = statSync(this.pathFor(id)).mtimeMs;
215
+ }
216
+ catch {
217
+ }
218
+ if (now - ageAnchor <= opts.olderThanMs)
219
+ continue;
220
+ }
221
+ out.push(placement.scope !== undefined && placement.handle !== undefined
222
+ ? { sessionId: id, placedAt: placement.placedAt, scope: placement.scope, handle: placement.handle }
223
+ : { sessionId: id, placedAt: placement.placedAt, tupleIncomplete: true });
224
+ }
225
+ return out;
226
+ }
227
+ async placementOf(sessionId) {
228
+ try {
229
+ return this.read(sessionId).placement;
230
+ }
231
+ catch {
232
+ return undefined;
233
+ }
234
+ }
169
235
  async delete(metadata) {
170
236
  evictSharedSessionStorage(canonicalStoreKey(this.pathFor(metadata.id)));
171
237
  try {
172
238
  rmSync(this.pathFor(metadata.id), { force: true });
173
239
  }
174
- catch {
240
+ catch (err) {
241
+ throw new SessionError("storage", `session ${metadata.id} could not be deleted: ${err instanceof Error ? err.message : String(err)}`, err instanceof Error ? err : undefined);
175
242
  }
176
243
  }
177
244
  async fork(sourceMetadata, options = {}) {
@@ -0,0 +1,97 @@
1
+ import { type StoredStrategy, type StrategyStore, type StrategyStoreIncident } from "../../core/strategy-store.js";
2
+ /**
3
+ * File-backed {@link StrategyStore} — the persistence twin of `InMemoryStrategyStore`, so the teacher
4
+ * mode's cross-session reuse premise actually holds on a local (TOC) deployment where every session is
5
+ * a fresh process (an in-memory strategy repository there re-learns everything, every time).
6
+ *
7
+ * DESIGN STANCE — this is a **cache, not an authority store**: every entry is regenerable (worst case:
8
+ * ask the teacher again), so losing one never loses correctness. It therefore deliberately skips the
9
+ * journal/shadow armor of the memory file backend and does NOT fsync-harden beyond the shared
10
+ * atomic-write primitive, does NOT take a cross-process lock, and accepts a narrow multi-process race
11
+ * on save-side dedup (two processes can each write one copy of the same normalized entry — the read
12
+ * side dedups, and a lost max-confidence merge is cache-grade). What it does NOT relax: scope
13
+ * isolation and loud-refusal discipline, which are held to the same standard as the durable stores.
14
+ *
15
+ * Layout: `<root>/<scopeDir>/<id>.json`, one strategy per file, whole-file atomic replace on write.
16
+ * `scopeDir = slug(scope) + "-" + sha256(scope).slice(0,24)` — the slug is a strict `[a-z0-9-]`
17
+ * whitelist purely for readability; IDENTITY lives in the 96-bit hash, so no scope value, however
18
+ * hostile, can traverse out of the root or collide another scope's directory. Ids are gated to
19
+ * `[A-Za-z0-9_-]{1,64}` at save (the escalation loop mints UUIDs; the interface is public, so a
20
+ * hand-rolled id must not be able to name a path).
21
+ *
22
+ * `scope` is a NAMESPACE, not an authorization boundary — the store cannot authenticate its caller;
23
+ * whoever holds the instance can address any scope (same posture as the memory store). Authorization
24
+ * is the host's obligation.
25
+ */
26
+ export interface FileStrategyStoreOptions {
27
+ /** Directory to keep strategies under (created `0o700` if absent; a non-directory or unwritable
28
+ * path is refused loudly at construction — a store that cannot persist must not pretend to). */
29
+ root: string;
30
+ /** Per-scope capacity cap (entries), evicting the lowest-scoring on overflow. Default 100. */
31
+ maxPerScope?: number;
32
+ /**
33
+ * Disclosure sink for contained store incidents (a corrupt entry quarantined, an eviction that
34
+ * failed after a successful save, a degraded read). Absent ⇒ `console.warn`, once per op kind —
35
+ * a contained fault must be loud somewhere, but must never flood.
36
+ */
37
+ onIncident?: (i: StrategyStoreIncident) => void;
38
+ }
39
+ export declare class FileStrategyStore implements StrategyStore {
40
+ private readonly root;
41
+ private readonly maxPerScope;
42
+ private readonly onIncident?;
43
+ /** Ops that already warned on the absent-sink fallback (bounded disclosure — never flood). */
44
+ private readonly warnedOps;
45
+ /** Isolation scope for the host incident sink (sync throw AND async rejection contained). */
46
+ private readonly sinkNotifier;
47
+ /** Scope dirs whose stale temps this instance already swept (once per scope per instance). */
48
+ private readonly sweptTemps;
49
+ constructor(opts: FileStrategyStoreOptions);
50
+ /** `slug(scope)-sha256(scope)[0..24]` — slug is readability only; the hash is the injective key. */
51
+ private scopeDirName;
52
+ private scopeDirPath;
53
+ /** The scope dir must be a REAL directory — a symlink here means the store's namespace was tampered
54
+ * with (a link could point retrieval at another scope's data, or writes out of the root). The
55
+ * check-then-use window against a same-uid local attacker is out of the threat model: such an
56
+ * attacker already holds the same rights as the store itself. */
57
+ private assertScopeDirSafe;
58
+ private incident;
59
+ /** Sweep crashed writers' stale temps (once per scope per instance). Fresh temps are left alone —
60
+ * they may belong to a live concurrent writer. */
61
+ private sweepStaleTemps;
62
+ /** Load every VALID entry of a scope dir. Corrupt files are quarantined (`.bad` rename) with one
63
+ * disclosure; entries that violate the read-side invariants (foreign scope / id≠filename) are
64
+ * skipped with disclosure but NOT quarantined (a copied-in file may be someone's valid data);
65
+ * normalized duplicates collapse to the best-scoring copy. */
66
+ private loadScope;
67
+ /** Rename a corrupt file to `<name>.bad` — one disclosure now, zero parse cost on every later read
68
+ * (the suffix no longer matches the entry-file grammar). Never faults the calling operation. */
69
+ private quarantine;
70
+ /** Serialize + write one entry, refusing a record whose SERIALIZED form exceeds the read bound.
71
+ * The field caps bound RAW string bytes, but JSON escaping expands control characters up to 6x —
72
+ * without this door a save could succeed and then self-quarantine on the very next read (the
73
+ * worst possible shape: an accepted write the store itself later refuses to serve). */
74
+ private writeEntry;
75
+ save(s: StoredStrategy): void;
76
+ private evict;
77
+ find(scope: string, query: string, limit: number): StoredStrategy[];
78
+ /** Bounded candidate view for the RETRIEVAL-adjacent paths (find/hasStrategy/save): an externally
79
+ * inflated directory must not make a hot operation unbounded. MAINTENANCE passes Infinity — see
80
+ * {@link prune}. */
81
+ private retrievalReadCap;
82
+ /**
83
+ * Trims to `maxSize` over an UNCAPPED enumeration: prune is the reconciliation verb, so it must see
84
+ * every entry file — under the bounded retrieval view, a capacity SHRINK across restarts (or
85
+ * `maxPerScope: 0`) left files beyond the window untouched, reporting success while the "removed"
86
+ * strategies sat on disk ready to resurrect under a later, larger capacity. Host-invoked
87
+ * maintenance accepts the O(all files) cost the retrieval path refuses.
88
+ */
89
+ prune(scope: string, maxSize: number): void;
90
+ /** Uncapped for the same reason as {@link prune}: this face feeds the seed capacity door, and an
91
+ * under-count there turns "refuse what cannot fit" into silent eviction of fresh seeds. */
92
+ scopeUsage(scope: string): {
93
+ used: number;
94
+ capacity: number;
95
+ };
96
+ hasStrategy(scope: string, entry: Pick<StoredStrategy, "problem" | "strategy">): boolean;
97
+ }