@sema-agent/core 5.23.0 → 5.25.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 (35) hide show
  1. package/CHANGELOG.md +192 -1
  2. package/dist/core/checkpoint-store.d.ts +84 -10
  3. package/dist/core/checkpoint-store.js +3 -1
  4. package/dist/core/hooks.d.ts +18 -5
  5. package/dist/core/hooks.js +28 -4
  6. package/dist/core/memory-engine/engine.js +18 -3
  7. package/dist/core/permission-rule-org.d.ts +9 -0
  8. package/dist/core/permission-rule-org.js +12 -5
  9. package/dist/core/permission-rule-store.d.ts +25 -14
  10. package/dist/core/permission-rule-store.js +5 -1
  11. package/dist/core/runner/prepare-task.d.ts +2 -0
  12. package/dist/core/runner/prepare-task.js +40 -9
  13. package/dist/core/runner/runtask.js +49 -6
  14. package/dist/core/runner/session-file-state-replay.js +3 -0
  15. package/dist/core/tool-policy.d.ts +11 -0
  16. package/dist/core/tool-policy.js +17 -15
  17. package/dist/core/tool-result-store.d.ts +8 -0
  18. package/dist/core/tool-result-store.js +77 -3
  19. package/dist/core/types.d.ts +7 -5
  20. package/dist/index.d.ts +13 -7
  21. package/dist/index.js +2 -2
  22. package/dist/stores/file/adoption/adopt.d.ts +23 -3
  23. package/dist/stores/file/adoption/adopt.js +4 -8
  24. package/dist/stores/file/adoption/marker.d.ts +41 -18
  25. package/dist/stores/file/adoption/marker.js +14 -7
  26. package/dist/stores/file/permission-rule-store.d.ts +15 -1
  27. package/dist/stores/file/permission-rule-store.js +4 -1
  28. package/dist/stores/file/session-policy-store.d.ts +11 -1
  29. package/dist/stores/file/session-policy-store.js +8 -3
  30. package/dist/stores/file/task-list-store.d.ts +15 -1
  31. package/dist/stores/file/task-list-store.js +2 -2
  32. package/dist/tools/fs/fs-bash.js +7 -4
  33. package/dist/tools/fs/fs-shared.js +23 -7
  34. package/dist/tools/monitor.js +3 -3
  35. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -88,7 +88,7 @@ export { HAND_TOOL_EFFECTS, bashReversibilityProbe, BASH_READONLY_DEFAULT_ALLOW,
88
88
  export { classifyCompoundReadonlyDetailed, formatOutOfRootReadApprovalOption, type BashReadonlyRootBoundary, type CompoundReadonlyVerdict, } from "./tools/fs/index.js";
89
89
  export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
90
90
  export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, assertSafeToolResultRef, buildToolResultRef, type ToolResultStore, type ToolResultSlice, } from "./core/tool-result-store.js";
91
- export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingTokens, winnerFromOutcome, validatePendingSteer, readPendingSteerQueue, appendPendingSteer, MAX_PENDING_STEER_CHARS, MAX_PENDING_STEER_ENTRIES, PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES, PENDING_STEER_FROZEN_FIELDS, ACTOR_ASSERTION_FROZEN_FIELDS, MAX_ACTOR_FIELD_CHARS, MAX_STEER_INPUT_ID_CHARS, LEGACY_PENDING_STEER_INPUT_ID, type ActorAssertion, type PendingSteerEntry, type PendingSteerInput, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, type RiskDescriptor, type CheckpointStore, type CheckpointSummary, type Checkpoint, type CheckpointToken, type CheckpointGate, type CheckpointState, type SerializedCheckpointState, type CheckpointFaultMode, type PendingAction, type ResumeOutcome, type ResolvedOutcome, type ReopenReason, type ResolveExpectation, type SafetyAxis, type ResourceLedger, type ResourceLimitReason, type PlatformLimitReason, } from "./core/checkpoint-store.js";
91
+ export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingTokens, winnerFromOutcome, validatePendingSteer, readPendingSteerQueue, appendPendingSteer, MAX_PENDING_STEER_CHARS, MAX_PENDING_STEER_ENTRIES, PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES, PENDING_STEER_FROZEN_FIELDS, ACTOR_ASSERTION_FROZEN_FIELDS, MAX_ACTOR_FIELD_CHARS, MAX_STEER_INPUT_ID_CHARS, LEGACY_PENDING_STEER_INPUT_ID, type ActorAssertion, type PendingSteerEntry, type PendingSteerInput, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, type RiskDescriptor, type CheckpointStore, type CheckpointSummary, type Checkpoint, type CheckpointToken, type CheckpointGate, type CheckpointState, type SerializedCheckpointState, type CheckpointFaultMode, type PendingAction, type ResumeOutcome, type ResolvedOutcome, type ReopenReason, type ResolveExpectation, type SafetyAxis, type RealApprovalGateBit, type ResourceLedger, type ResourceLimitReason, type PlatformLimitReason, } from "./core/checkpoint-store.js";
92
92
  export { InMemoryUsageWindowStore, GLOBAL_USAGE_KEY, EMPTY_USAGE_WINDOW_RECORD, chargeUsageRecord, readUsageRecord, usageRetryAfterMs, resolveUsageWindows, type UsageWindow, type UsageWindowStore, type UsageWindowReading, type UsageWindowRecord, type UsageSlot, type UsageBucketRow, } from "./core/usage-window-store.js";
93
93
  export { FileUsageWindowStore } from "./stores/file/usage-window-store.js";
94
94
  export { ENV_LIFETIME_SUSPEND_MARGIN_MS, USAGE_WINDOW_REAP_MARGIN_MS } from "./core/runner/prepare-task.js";
@@ -136,14 +136,20 @@ export { createPermissionRulePolicy, validatePermissionRules, parsePermissionRul
136
136
  /**
137
137
  * design/179 — persisted ALLOW rules: the standing form of approvals a person already gave.
138
138
  *
139
- * Note what is NOT here. `PermissionRuleWriter` is core-private and never exported, so a deployment has
140
- * no API path that puts a rule in the store; every rule that enters through the engine comes out of the
141
- * consent protocol below (a durable approval record, an authenticated confirmation, a principal-bound
142
- * redemption). Removal is exported without that ceremony, because narrowing on a user's behalf is allowed
143
- * and widening is not.
139
+ * The consent boundary, restated precisely (ruled 2026-08-10): the ENGINE's only write
140
+ * path into a rule store is the consent protocol below (a durable approval record, an authenticated
141
+ * confirmation, a principal-bound redemption). That fact is a property of the engine's wiring, not of
142
+ * what this file exports a deployment always owned its own storage bytes. The BACKEND CONTRACT
143
+ * (writer types, the writer handle key, and the fold pure functions) is therefore exported further
144
+ * down, so an out-of-repo store implementation (the server's SQL twin) is built against the same
145
+ * definitions as the file backend instead of mirroring them — mirrored types are how comment/contract
146
+ * drift starts. What stays deliberately absent is any convenience that mints rules AROUND consent:
147
+ * exporting the contract gives a store implementer the same powers it already had over its own bytes,
148
+ * and nothing more. Removal is exported without ceremony, because narrowing on a user's behalf is
149
+ * allowed and widening is not.
144
150
  */
145
151
  export { parseAllowRuleText, formatAllowRuleText, ruleAdmitsCommand, findAdmittingRule, suggestRulesForCommand, scopeCoversCwd, pathWithinRoot, isRuleLive, BARE_INTERPRETER_NAMES, MAX_RULE_TEXT_CHARS, type PersistedAllowRule, type RuleTombstone, type RuleScope, type RuleDot, type RuleAdd, type RuleAddOrigin, type RuleSuggestion, type RuleReject, type RuleRejectCode, type ParsedAllowRule, type PersistedRuleTool, type PersistedRuleMatch, } from "./core/permission-rule-model.js";
146
- export { removePersistedRule, applyTombstones, sameScope, InMemoryPermissionRuleStore, EMPTY_RULE_STORE, type PermissionRuleStore, type PermissionRuleStoreProvider, type StoredAllowRules, type RemoveResult, type PutResult, joinRuleStates, screenRuleSyncState, collectBelowFrontier, ruleSyncVector, joinFrontiers, dotAtOrBelowFrontier, sameRuleOwner, type RuleSyncState, type RuleSyncFrontier, type RuleSyncDrop, type RuleSyncLandingReport, type RuleOwner, type QuarantinedRuleAdd, } from "./core/permission-rule-store.js";
152
+ export { removePersistedRule, applyTombstones, sameScope, InMemoryPermissionRuleStore, EMPTY_RULE_STORE, type PermissionRuleStore, type PermissionRuleStoreProvider, type StoredAllowRules, type RemoveResult, type PutResult, joinRuleStates, screenRuleSyncState, collectBelowFrontier, ruleSyncVector, joinFrontiers, dotAtOrBelowFrontier, sameRuleOwner, type RuleSyncState, type RuleSyncFrontier, type RuleSyncDrop, type RuleSyncLandingReport, type RuleOwner, type QuarantinedRuleAdd, PERMISSION_RULE_WRITER, writerOf, foldDelta, addDotsOf, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, type PermissionRuleWriter, type WritablePermissionRuleStore, type RuleWriteDelta, type RuleAddDelta, type RuleDeleteDelta, type RuleSyncJoinDelta, type RawRuleSyncState, type RedemptionAuthorization, } from "./core/permission-rule-store.js";
147
153
  export { syncPermissionRules, parseRuleSyncResponse, PERMISSION_RULE_SYNC_PATH, LOCAL_OWNER_UNSYNCABLE_CODE, type PermissionRuleSyncTransport, type PermissionRuleSyncResult, type RuleSyncRequestBody, type RuleSyncResponseBody, } from "./core/permission-rule-sync.js";
148
154
  export { createOrgRuleOverlay, orgRuleVerdictFor, effectivePermissionRules, orgRuleStatePersistenceOf, ORG_UNAVAILABLE_DECISION_REASON, ORG_RULE_DECISION_REASON, ORG_ADJUDICATION_TIMEOUT_MS, type OrgPermissionRule, type OrgRuleSnapshot, type OrgRuleSnapshotProvider, type OrgRuleStatePersistence, type PersistedOrgRuleState, type OrgRuleOverlay, type OrgOverlayResolution, type OrgOverlayStatus, type EffectivePermissionRule, } from "./core/permission-rule-org.js";
149
155
  export { RULE_SYNC_DROP_CODES, type RuleSyncDropReason, type RuleQuarantineReason } from "./core/governance-codes.js";
package/dist/index.js CHANGED
@@ -69,7 +69,7 @@ export { HAND_TOOL_EFFECTS, bashReversibilityProbe, BASH_READONLY_DEFAULT_ALLOW,
69
69
  export { classifyCompoundReadonlyDetailed, formatOutOfRootReadApprovalOption, } from "./tools/fs/index.js";
70
70
  export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
71
71
  export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, assertSafeToolResultRef, buildToolResultRef, } from "./core/tool-result-store.js";
72
- export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingTokens, winnerFromOutcome, validatePendingSteer, readPendingSteerQueue, appendPendingSteer, MAX_PENDING_STEER_CHARS, MAX_PENDING_STEER_ENTRIES, PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES, PENDING_STEER_FROZEN_FIELDS, ACTOR_ASSERTION_FROZEN_FIELDS, MAX_ACTOR_FIELD_CHARS, MAX_STEER_INPUT_ID_CHARS, LEGACY_PENDING_STEER_INPUT_ID, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, } from "./core/checkpoint-store.js";
72
+ export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingTokens, winnerFromOutcome, validatePendingSteer, readPendingSteerQueue, appendPendingSteer, MAX_PENDING_STEER_CHARS, MAX_PENDING_STEER_ENTRIES, PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES, PENDING_STEER_FROZEN_FIELDS, ACTOR_ASSERTION_FROZEN_FIELDS, MAX_ACTOR_FIELD_CHARS, MAX_STEER_INPUT_ID_CHARS, LEGACY_PENDING_STEER_INPUT_ID, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, } from "./core/checkpoint-store.js";
73
73
  export { InMemoryUsageWindowStore, GLOBAL_USAGE_KEY, EMPTY_USAGE_WINDOW_RECORD, chargeUsageRecord, readUsageRecord, usageRetryAfterMs, resolveUsageWindows, } from "./core/usage-window-store.js";
74
74
  export { FileUsageWindowStore } from "./stores/file/usage-window-store.js";
75
75
  export { ENV_LIFETIME_SUSPEND_MARGIN_MS, USAGE_WINDOW_REAP_MARGIN_MS } from "./core/runner/prepare-task.js";
@@ -112,7 +112,7 @@ export { buildAutoModePrompt, renderAutoModeWindow, renderAutoModeAction, AUTO_M
112
112
  export { AUTO_MODE_BASE_PROMPT, AUTO_MODE_PERMISSIONS_EXTERNAL } from "./core/auto-mode-prompt-assets.js";
113
113
  export { createPermissionRulePolicy, validatePermissionRules, parsePermissionRule, wildcardMatch, } from "./core/permission-rules.js";
114
114
  export { parseAllowRuleText, formatAllowRuleText, ruleAdmitsCommand, findAdmittingRule, suggestRulesForCommand, scopeCoversCwd, pathWithinRoot, isRuleLive, BARE_INTERPRETER_NAMES, MAX_RULE_TEXT_CHARS, } from "./core/permission-rule-model.js";
115
- export { removePersistedRule, applyTombstones, sameScope, InMemoryPermissionRuleStore, EMPTY_RULE_STORE, joinRuleStates, screenRuleSyncState, collectBelowFrontier, ruleSyncVector, joinFrontiers, dotAtOrBelowFrontier, sameRuleOwner, } from "./core/permission-rule-store.js";
115
+ export { removePersistedRule, applyTombstones, sameScope, InMemoryPermissionRuleStore, EMPTY_RULE_STORE, joinRuleStates, screenRuleSyncState, collectBelowFrontier, ruleSyncVector, joinFrontiers, dotAtOrBelowFrontier, sameRuleOwner, PERMISSION_RULE_WRITER, writerOf, foldDelta, addDotsOf, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, } from "./core/permission-rule-store.js";
116
116
  export { syncPermissionRules, parseRuleSyncResponse, PERMISSION_RULE_SYNC_PATH, LOCAL_OWNER_UNSYNCABLE_CODE, } from "./core/permission-rule-sync.js";
117
117
  export { createOrgRuleOverlay, orgRuleVerdictFor, effectivePermissionRules, orgRuleStatePersistenceOf, ORG_UNAVAILABLE_DECISION_REASON, ORG_RULE_DECISION_REASON, ORG_ADJUDICATION_TIMEOUT_MS, } from "./core/permission-rule-org.js";
118
118
  export { RULE_SYNC_DROP_CODES } from "./core/governance-codes.js";
@@ -24,9 +24,29 @@
24
24
  * ⑥ the marker is atomically rewritten into its PERMANENT terminal record carrying the immutable
25
25
  * report. Never deleted; a re-run short-circuits to it.
26
26
  *
27
- * FREEZE (design/183 §8 load-bearing premise): from the moment ② lands, the whole root is read-only
28
- * for everyone but this machine live writers are excluded by the locks, and across crashes every
29
- * file store constructor refuses the in-flight marker (invariant I6, `assertAdoptionBootGate`).
27
+ * FREEZE (design/183 §8 load-bearing premise): from the moment ② lands, the root is closed to new
28
+ * writers and it is worth being exact about which mechanism covers what, because the two halves
29
+ * meet at a seam rather than overlapping (backlog #136⑧):
30
+ * · the LOCKS cover the boot-locked shape: this machine holds `LOCK` (the engine's own boot lock,
31
+ * taken by `FileStorageBackend`) and `ADOPTION-LOCK` for the whole arc, so a live backend refuses
32
+ * the adoption instead of interleaving with it, and a second adoption refuses too;
33
+ * · invariant I6 (`assertAdoptionBootGate`) covers CONSTRUCTION over a DATA-ROOT ANCHOR: while the
34
+ * marker is in flight, a store constructor handed the data root refuses it — including across a
35
+ * crash, when no lock survives.
36
+ * What NEITHER covers, enumerated rather than implied:
37
+ * · an ALREADY-constructed instance over this root through a path that does not hold `LOCK` (the
38
+ * independent store faces — `new FileMemoryStore(root)`, the mailbox family). It keeps its open
39
+ * handles, and I6 is a construction-time gate, not a write-time one;
40
+ * · a NEW construction whose anchor is not the data root. Two faces take a directory that need not
41
+ * BE the data root — `FilePermissionRuleStoreProvider` (the rule bucket) and
42
+ * `createFileTaskListStore` (the document's own directory) — so each takes its root anchor as an
43
+ * explicit option, and a caller that omits it constructs unGATED during a freeze. That default is
44
+ * what this arc's own nested rule leg rides (it must reach the bucket while the marker is in
45
+ * flight); a deployment wanting the gate passes the anchor. Both arms are pinned.
46
+ * That is the same boundary the file family draws everywhere else (one live writer per data root is
47
+ * the deployment's contract, mechanically enforced only for the backend-factory shape), and it is why
48
+ * "stop the engine before adopting" remains an operational instruction and not merely a machine-
49
+ * checked one.
30
50
  * Idempotent resume ("re-run completes the unfinished legs") is only sound because no per-store row
31
51
  * moves underneath it; the freeze is the premise of idempotence, not an implementation detail.
32
52
  *
@@ -249,6 +249,7 @@ const ZERO_ACTION_STORES = [
249
249
  "tool-result",
250
250
  "file-snapshot",
251
251
  "memory",
252
+ "memory-engine",
252
253
  "task-list",
253
254
  "mailbox",
254
255
  "workflow-run",
@@ -500,8 +501,8 @@ export async function adoptLocalDataRoot(opts) {
500
501
  };
501
502
  }
502
503
  }
503
- if (opts.from.kind === "principal" && legBits[SESSION_POLICY_LEG]?.done !== true) {
504
- const rows = rewriteSessionPolicyRows(opts.root, opts.from.principal, opts.toPrincipal);
504
+ if (legBits[SESSION_POLICY_LEG]?.done !== true) {
505
+ const rows = rewriteSessionPolicyRows(opts.root, opts.from.kind === "principal" ? opts.from.principal : null, opts.toPrincipal);
505
506
  legBits = { ...legBits, [SESSION_POLICY_LEG]: { done: true, rows } };
506
507
  publishMarker(opts.root, adoptionId, opts.from, opts.toPrincipal, 2, plan, legBits);
507
508
  }
@@ -557,12 +558,7 @@ export async function adoptLocalDataRoot(opts) {
557
558
  }
558
559
  if (plan.rulesDir !== undefined)
559
560
  legs.push({ store: "permission-rule", action: "bucket-rebind" });
560
- if (opts.from.kind === "principal") {
561
- legs.push({ store: SESSION_POLICY_LEG, action: "row-rewrite", rows: legBits[SESSION_POLICY_LEG]?.rows ?? 0 });
562
- }
563
- else {
564
- legs.push({ store: SESSION_POLICY_LEG, action: "none" });
565
- }
561
+ legs.push({ store: SESSION_POLICY_LEG, action: "row-rewrite", rows: legBits[SESSION_POLICY_LEG]?.rows ?? 0 });
566
562
  const carried = Object.entries(legBits)
567
563
  .filter(([k]) => k.startsWith(CARRIAGE_BIT_PREFIX))
568
564
  .map(([k, bit]) => ({
@@ -5,8 +5,11 @@
5
5
  * Two shapes live in `<dataRoot>/adoption.json`:
6
6
  * - the IN-FLIGHT marker (`marker`): a monotonically-advanced `phase` plus per-leg completion bits
7
7
  * for the one leg class without a self-attesting completion predicate (row rewrites). While this
8
- * shape is on disk the root is FROZEN: every file store constructor refuses to build over it
9
- * ({@link assertAdoptionBootGate} — invariant I6), so no writer can serve a half-migrated root.
8
+ * shape is on disk the root is FROZEN: every file store constructor ANCHORED TO THE DATA ROOT
9
+ * refuses to build over it ({@link assertAdoptionBootGate} — invariant I6). Coverage is stated
10
+ * precisely at the gate's own docstring: two faces whose constructor argument is not the root
11
+ * take an optional `dataRoot` anchor, and a caller omitting it is outside the gate (the seam is
12
+ * documented and pinned where those faces live).
10
13
  * - the PERMANENT terminal record (`adopted`): never deleted. It is the idempotence anchor (a re-run
11
14
  * with the same (from → toPrincipal) short-circuits to the recorded receipt) and it carries the
12
15
  * IMMUTABLE report — the byte-stable half of every receipt.
@@ -153,18 +156,26 @@ export type RootAdoptionFile = {
153
156
  export declare const ROOT_ADOPTION_FILE = "adoption.json";
154
157
  /**
155
158
  * design/183 §6 (r4) — the CLOSED SET of by-design-not-migrated assets, verbatim from the design's
156
- * written list. Lives HERE (beside the reader) because the terminal validator enforces EXACT
157
- * equality: the report is the account's recovery source, and a reduced-or-grown list at read time
158
- * is either damage or drift — both refused (§10.5 ①).
159
+ * written list. The WRITER emits exactly this list and the drift pin (§10.5 ① — the design-vs-code
160
+ * two-cell guard) lives in the TEST GRID, where a ruled growth of the set is a deliberate edit.
161
+ *
162
+ * #133: the terminal READ validator deliberately does NOT compare a stored report against this
163
+ * constant. A persisted report is a snapshot of the set AS RULED AT ADOPTION TIME; the set is
164
+ * allowed to grow by ruling (§10.5 ① names that road), so an exact-equality read check would mark
165
+ * every already-adopted root corrupt on the release after any growth (and again on rollback) — 13
166
+ * construction sites all refusing to serve. The read validator checks SHAPE and integrity
167
+ * (non-empty, well-formed entries, no duplicate assets — what an empty/mangled account rebuild
168
+ * actually needs), never version-crossed content equality.
159
169
  */
160
170
  export declare const NOT_MIGRATED_BY_DESIGN: ReadonlyArray<{
161
171
  asset: string;
162
172
  ruling: string;
163
173
  }>;
164
- /** The five baseline obligation IDENTITIES (design/183 §6 minimum face). The terminal validator
165
- * requires them all: a "reduced but shaped" report would otherwise rebuild a thinner account.
166
- * The writer's `baselineConfigs` emits exactly these any drift breaks the writer's own
167
- * read-back immediately (self-checking pair). */
174
+ /** The five baseline obligation IDENTITIES (design/183 §6 minimum face). Since #133 the terminal READ
175
+ * validator deliberately does NOT compare a stored report against this table (the stored report is a
176
+ * snapshot of the set as ruled at adoption time; see the validator's own note) — the enforcement that
177
+ * the WRITER emits exactly this set lives in the test grid (test/file-adoption-root.test.ts pins the
178
+ * fresh report's identities against this constant), where a drift is a deliberate, reviewable edit. */
168
179
  export declare const BASELINE_CONFIG_IDENTITIES: ReadonlyArray<{
169
180
  deployment: string;
170
181
  key: string;
@@ -178,17 +189,29 @@ export declare function readRootAdoptionFile(root: string): RootAdoptionFile | u
178
189
  /** Atomically publish the root marker (write-temp → fsync → rename, same discipline as every store). */
179
190
  export declare function writeRootAdoptionFile(root: string, content: RootAdoptionFile): void;
180
191
  /**
181
- * Invariant I6 (design/183 §3.1) — the adoption BOOT GATE, called by every file store constructor:
182
- * an in-flight root marker means the root is mid-adoption (possibly after a crash), and serving a
183
- * half-migrated root is refused LOUDLY. The refusal names the in-flight arc and the way forward
184
- * (resume `adoptLocalDataRoot` to completion). A terminal record passes: a completed adoption is a
185
- * normal, readable root. A corrupt marker throws (fail-closed, see {@link readRootAdoptionFile}).
192
+ * Invariant I6 (design/183 §3.1) — the adoption BOOT GATE: an in-flight root marker means the root is
193
+ * mid-adoption (possibly after a crash), and serving a half-migrated root is refused LOUDLY. The
194
+ * refusal names the in-flight arc and the way forward (resume `adoptLocalDataRoot` to completion). A
195
+ * terminal record passes: a completed adoption is a normal, readable root. A corrupt marker throws
196
+ * (fail-closed, see {@link readRootAdoptionFile}).
197
+ *
198
+ * WHO calls it, precisely: every store constructor whose argument IS the data root (that is most of
199
+ * the family — they derive their own subdirectory from it). The two faces whose argument is a
200
+ * caller-chosen directory instead — `FilePermissionRuleStoreProvider` (a rule bucket) and
201
+ * `createFileTaskListStore` (the document's directory) — cannot anchor on their argument, so each
202
+ * takes the data root as an explicit option and is gated only when given one. `root` here is always
203
+ * the ANCHOR, never "wherever this store keeps its bytes": passing a subdirectory reads a marker that
204
+ * is not there and passes unconditionally.
186
205
  *
187
206
  * This gate is what turns "the engine must be stopped during adoption" from an operational assumption
188
207
  * into a machine-checked invariant across crashes: the adoption's own locks die with its process, but
189
- * the marker (and this gate) survive. Boundary, stated honestly: the gate fires at CONSTRUCTION time.
190
- * A store instance constructed BEFORE the marker landed in another OS process is outside it cross-
191
- * process sharing of one data dir is the file family's documented UNSUPPORTED shape (task-list F-14,
192
- * mailbox RB-249); adoption adds no new promise there.
208
+ * the marker (and this gate) survive. Boundary, stated honestly: the gate fires at CONSTRUCTION time,
209
+ * and it is the ONLY thing this function does it never reaches an object that already exists. So a
210
+ * store instance constructed BEFORE the marker landed keeps writing for as long as it is held, in
211
+ * ANOTHER OS process (cross-process sharing of one data dir is the file family's documented
212
+ * UNSUPPORTED shape — task-list F-14, mailbox RB-249) and, symmetrically, in THIS one (an independent
213
+ * store face built before the adoption started; the arc's `LOCK` acquisition excludes a live
214
+ * `FileStorageBackend`, which is the only shape that takes that lock). Adoption adds no new promise on
215
+ * either side of that seam — see the freeze note on `adoptLocalDataRoot` for the full division.
193
216
  */
194
217
  export declare function assertAdoptionBootGate(root: string, storeName: string): void;
@@ -74,7 +74,7 @@ function isRootAdoptionFileShape(v) {
74
74
  }
75
75
  }
76
76
  const legs = (m.legs ?? {});
77
- if (m.phase >= 3 && m.from.kind === "principal" && legs["session-policy"]?.done !== true)
77
+ if (m.phase >= 3 && legs["session-policy"]?.done !== true)
78
78
  return false;
79
79
  if (m.phase === 5) {
80
80
  for (const store of p.carriage) {
@@ -123,14 +123,21 @@ function isRootAdoptionFileShape(v) {
123
123
  return false;
124
124
  configIds.add(id);
125
125
  return true;
126
- }) &&
127
- BASELINE_CONFIG_IDENTITIES.every((b) => configIds.has(JSON.stringify([b.deployment, b.key])));
126
+ });
128
127
  const nmd = r.notMigratedByDesign;
128
+ const nmdAssets = new Set();
129
129
  const closedSetOk = Array.isArray(nmd) &&
130
- nmd.length === NOT_MIGRATED_BY_DESIGN.length &&
131
- NOT_MIGRATED_BY_DESIGN.every((m, i) => {
132
- const e = nmd[i];
133
- return (e !== null && typeof e === "object" && e.asset === m.asset && e.ruling === m.ruling);
130
+ nmd.length > 0 &&
131
+ nmd.every((e) => {
132
+ if (e === null || typeof e !== "object")
133
+ return false;
134
+ const m = e;
135
+ if (typeof m.asset !== "string" || m.asset === "" || typeof m.ruling !== "string" || m.ruling === "")
136
+ return false;
137
+ if (nmdAssets.has(m.asset))
138
+ return false;
139
+ nmdAssets.add(m.asset);
140
+ return true;
134
141
  });
135
142
  const legsOk = Array.isArray(r.legs) &&
136
143
  r.legs.every((l) => {
@@ -157,12 +157,26 @@ declare class FilePermissionRuleStore implements WritablePermissionRuleStore {
157
157
  *
158
158
  * An absent principal resolves to a store that reads zero rules and refuses to write — an unauthenticated
159
159
  * task does not share anyone's bucket.
160
+ *
161
+ * ROOT FREEZE (design/183 invariant I6, backlog #136⑤): every sibling file store gates construction on
162
+ * the DATA ROOT's adoption marker, but this provider is built over the RULE BUCKET directory, which is
163
+ * a caller-chosen path that need not sit under (or equal) the data root — so `dir` is not a usable
164
+ * anchor for that gate and the bucket was writable straight through a root freeze. The anchor is
165
+ * therefore taken EXPLICITLY, via `opts.dataRoot`: supply it whenever the rule bucket belongs to a data
166
+ * root that `adoptLocalDataRoot` may adopt, and construction is refused while that root is mid-adoption
167
+ * exactly like `FileStorageBackend`'s. Omitting it keeps the previous behavior (no root gate) — which is
168
+ * also the EXEMPTION the adoption machine itself rides: its nested design/182 rule leg constructs this
169
+ * provider without a `dataRoot` precisely because it runs while the marker is in flight, and the
170
+ * bucket-level marker (read in `forPrincipal`/`forLocalOwner`) is what governs writes inside that arc.
160
171
  */
161
172
  export declare class FilePermissionRuleStoreProvider implements PermissionRuleStoreProvider {
162
173
  private readonly dir;
163
174
  private readonly onError?;
164
175
  private lock;
165
- constructor(dir: string, onError?: ((message: string) => void) | undefined);
176
+ constructor(dir: string, onError?: ((message: string) => void) | undefined, opts?: {
177
+ /** The data root whose adoption marker gates construction (design/183 I6). Absent ⇒ no root gate. */
178
+ dataRoot?: string;
179
+ });
166
180
  /** Take the writer lock, once, on first use of a write face. Reading never calls this. */
167
181
  private acquireWriteLock;
168
182
  forPrincipal(principal: string | undefined): PermissionRuleStore;
@@ -4,6 +4,7 @@ import { createHash, randomBytes } from "node:crypto";
4
4
  import { PERMISSION_RULE_WRITER, applySyncJoin, applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, foldDelta, } from "../../core/permission-rule-store.js";
5
5
  import { canonicalize } from "../../core/canonical-json.js";
6
6
  import { BootLock } from "./fs-atomic.js";
7
+ import { assertAdoptionBootGate } from "./adoption/marker.js";
7
8
  const LOCAL_OWNER_FILE = "local-owner.json";
8
9
  const ADOPTION_FILE = "PERMISSION-RULES-ADOPTION.json";
9
10
  export function readRuleAdoptionFile(dir) {
@@ -298,9 +299,11 @@ export class FilePermissionRuleStoreProvider {
298
299
  dir;
299
300
  onError;
300
301
  lock;
301
- constructor(dir, onError) {
302
+ constructor(dir, onError, opts) {
302
303
  this.dir = dir;
303
304
  this.onError = onError;
305
+ if (opts?.dataRoot !== undefined)
306
+ assertAdoptionBootGate(opts.dataRoot, "FilePermissionRuleStoreProvider");
304
307
  }
305
308
  acquireWriteLock = () => {
306
309
  if (this.lock !== undefined)
@@ -31,6 +31,15 @@ export interface FileSessionPolicyStoreOptions {
31
31
  export declare class FileSessionPolicyStore implements SessionPolicyStore {
32
32
  private readonly dir;
33
33
  private readonly onCorruptRead;
34
+ /** #132 — the ADOPTED identity of a local-owner root's anonymous lane, or undefined on an
35
+ * unadopted (or principal-adopted) root. A local-owner adoption rebinds the anonymous
36
+ * `[sid,null]` estate rows to `toPrincipal`; until the deployment's principal wiring lands
37
+ * (`REQUIRE_PRINCIPAL` et al. — recorded `migrated:false` in the config account, an operator
38
+ * action), the runtime still queries anonymously, and ENOENT there would silently drop the very
39
+ * tighten-only deny rules the rebind moved. Adoption is a transfer of the whole anonymous
40
+ * identity, so on such a root the anonymous key IS the adopted principal — reads and writes
41
+ * alike (a window-era anonymous write must not mint a fresh orphan row). */
42
+ private readonly anonymousAlias;
34
43
  constructor(root: string, opts?: FileSessionPolicyStoreOptions);
35
44
  /** The one delivery point for {@link onCorruptRead}. Swallow-guarded here so no caller has to remember. */
36
45
  private disclose;
@@ -41,7 +50,8 @@ export declare class FileSessionPolicyStore implements SessionPolicyStore {
41
50
  * (swallow-guarded; never on a plain ENOENT, which really is absence). */
42
51
  private discloseCorrupt;
43
52
  /** `(sessionId, principal)` → a safe, INJECTIVE filename (sanitizeScope appends the full sha256 of the raw
44
- * composite key, so distinct keys never collide on disk). */
53
+ * composite key, so distinct keys never collide on disk). On a local-owner-adopted root the
54
+ * anonymous lane resolves to the adopted principal's key (#132 — see {@link anonymousAlias}). */
45
55
  private pathFor;
46
56
  private read;
47
57
  getRules(sessionId: string, principal?: string): Promise<StoredSessionRules | null>;
@@ -2,15 +2,19 @@ import { readFileSync, readdirSync, unlinkSync } from "node:fs";
2
2
  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
- import { assertAdoptionBootGate } from "./adoption/marker.js";
5
+ import { assertAdoptionBootGate, readRootAdoptionFile } from "./adoption/marker.js";
6
6
  export class FileSessionPolicyStore {
7
7
  dir;
8
8
  onCorruptRead;
9
+ anonymousAlias;
9
10
  constructor(root, opts) {
10
11
  assertAdoptionBootGate(root, "FileSessionPolicyStore");
11
12
  this.dir = join(root, "session-policy");
12
13
  ensureDir(this.dir);
13
14
  this.onCorruptRead = opts?.onCorruptRead;
15
+ const adoption = readRootAdoptionFile(root);
16
+ this.anonymousAlias =
17
+ adoption !== undefined && "adopted" in adoption && adoption.adopted.from.kind === "local-owner" ? adoption.adopted.toPrincipal : undefined;
14
18
  }
15
19
  disclose(info) {
16
20
  try {
@@ -23,7 +27,8 @@ export class FileSessionPolicyStore {
23
27
  this.disclose({ sessionId, ...(principal !== undefined ? { principal } : {}), path: this.pathFor(sessionId, principal), reason });
24
28
  }
25
29
  pathFor(sessionId, principal) {
26
- const composite = JSON.stringify([sessionId, principal ?? null]);
30
+ const effective = principal ?? this.anonymousAlias;
31
+ const composite = JSON.stringify([sessionId, effective ?? null]);
27
32
  return join(this.dir, `${sanitizeScope(composite)}.json`);
28
33
  }
29
34
  read(sessionId, principal) {
@@ -78,7 +83,7 @@ export class FileSessionPolicyStore {
78
83
  }
79
84
  }
80
85
  const next = { ...clean, rev: (prior?.rev ?? 0) + 1 };
81
- atomicWriteFile(join(this.dir, "tmp"), this.pathFor(sessionId, principal), JSON.stringify({ ...next, __sid: sessionId, __principal: principal ?? null }));
86
+ atomicWriteFile(join(this.dir, "tmp"), this.pathFor(sessionId, principal), JSON.stringify({ ...next, __sid: sessionId, __principal: (principal ?? this.anonymousAlias) ?? null }));
82
87
  return next;
83
88
  }
84
89
  async listBySession(sessionId) {
@@ -1,2 +1,16 @@
1
1
  import type { TaskListStore } from "../../tools/task-list.js";
2
- export declare function createFileTaskListStore(root: string): TaskListStore;
2
+ export declare function createFileTaskListStore(root: string, opts?: {
3
+ /**
4
+ * The DATA ROOT whose adoption marker gates construction (design/183 I6). Most of the file family
5
+ * takes the data root and derives `join(root, "<family>")` itself; this factory is one of the TWO
6
+ * faces that instead take a caller-chosen directory (the other being
7
+ * `FilePermissionRuleStoreProvider`, which takes a rule bucket) — here, the document's own
8
+ * directory. So when a deployment hands in a SUBDIRECTORY the I6 check would look for the marker
9
+ * inside that subdirectory, find nothing, and pass unconditionally: a gate that is always open
10
+ * (backlog #136⑦). Supply the data root here and the gate reads the anchor the root-taking faces
11
+ * read. Absent ⇒ the directory argument doubles as the anchor, which is exact when it IS the data
12
+ * root (the conventional wiring) and is the previous behavior otherwise; the adoption freeze note
13
+ * on `adoptLocalDataRoot` lists that default among the seams it does not cover.
14
+ */
15
+ dataRoot?: string;
16
+ }): TaskListStore;
@@ -4,9 +4,9 @@ import { assertJsonMetadata, normalizeTaskShape } from "../../tools/task-list.js
4
4
  import { canonicalStoreKey, atomicWriteFile, ensureDir } from "./fs-atomic.js";
5
5
  import { assertAdoptionBootGate } from "./adoption/marker.js";
6
6
  const dirLocks = new Map();
7
- export function createFileTaskListStore(root) {
7
+ export function createFileTaskListStore(root, opts) {
8
8
  const dir = resolve(root);
9
- assertAdoptionBootGate(dir, "createFileTaskListStore");
9
+ assertAdoptionBootGate(opts?.dataRoot !== undefined ? resolve(opts.dataRoot) : dir, "createFileTaskListStore");
10
10
  const tmpDir = join(dir, "tmp");
11
11
  const path = join(dir, "task-list.json");
12
12
  ensureDir(dir);
@@ -14,9 +14,12 @@ import { BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadon
14
14
  export function bashReversibilityProbe(allow, boundary) {
15
15
  const allowSet = new Set(allow ?? BASH_READONLY_DEFAULT_ALLOW);
16
16
  return (args) => {
17
- const command = args?.command;
17
+ const a = args;
18
+ const command = a?.command;
18
19
  if (typeof command !== "string")
19
20
  return { reversible: false };
21
+ if (a?.run_in_background === true)
22
+ return { reversible: false };
20
23
  const resolved = typeof boundary === "function" ? boundary() : boundary;
21
24
  if (classifyCompoundReadonly(command, allowSet, resolved) === undefined)
22
25
  return { reversible: true };
@@ -555,8 +558,8 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
555
558
  const bgCappedByMax = appliedTimeoutSec !== undefined && requestedTimeoutSec !== undefined && typeof bgCaps.maxBgTimeoutSec === "number" && requestedTimeoutSec > bgCaps.maxBgTimeoutSec;
556
559
  const budgetNote = appliedTimeoutSec !== undefined
557
560
  ? bgCappedByMax
558
- ? ` Time budget: requested ${requestedTimeoutSec}s, capped at ${bgCaps.maxBgTimeoutSec}s (env ceiling: requests above ${bgCaps.maxBgTimeoutSec}s are reduced to it) — auto-terminates if still running after ${appliedTimeoutSec}s.`
559
- : ` Time budget: auto-terminates if still running after ${appliedTimeoutSec}s (hard cap ${bgCaps.maxBgTimeoutSec}s).`
561
+ ? ` Time budget: requested ${requestedTimeoutSec}s, capped at ${bgCaps.maxBgTimeoutSec}s (env ceiling: requests above ${bgCaps.maxBgTimeoutSec}s are reduced to it) — the process may run up to ${appliedTimeoutSec}s.`
562
+ : ` Time budget: the process may run up to ${appliedTimeoutSec}s (hard cap ${bgCaps.maxBgTimeoutSec}s).`
560
563
  : "";
561
564
  const lifetimeNote = backgroundLifetimeNote(bgCaps, bgSessionScoped, bgEnvIsolatedOwned);
562
565
  const registry = taskOpts.taskRegistry;
@@ -655,7 +658,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
655
658
  const tail = stdoutSoFar.slice(-1_000);
656
659
  const adoptCaps = detachEnv.backgroundCapabilities;
657
660
  const adoptBudgetNote = typeof adoptCaps.defaultBgTimeoutSec === "number" && typeof adoptCaps.maxBgTimeoutSec === "number"
658
- ? ` Time budget: auto-terminates if still running after ${Math.min(adoptCaps.defaultBgTimeoutSec, adoptCaps.maxBgTimeoutSec)}s (hard cap ${adoptCaps.maxBgTimeoutSec}s).`
661
+ ? ` Time budget: the process may run up to ${Math.min(adoptCaps.defaultBgTimeoutSec, adoptCaps.maxBgTimeoutSec)}s (hard cap ${adoptCaps.maxBgTimeoutSec}s).`
659
662
  : "";
660
663
  return {
661
664
  content: (cause === "timeout"
@@ -89,12 +89,19 @@ export function envErrorDetail(message) {
89
89
  }
90
90
  const announcedTimeoutConfig = new Set();
91
91
  function announceDiscardedTimeout(knob, raw, usedMs) {
92
- if (!Number.isFinite(raw) || raw <= 0)
93
- return;
94
- const defect = bashTimeoutDefect(Math.floor(raw));
95
- if (defect === undefined)
92
+ if (typeof raw === "string" && raw === "")
96
93
  return;
97
- const line = `${knob}=${raw} was ignored it ${defect}. Using ${usedMs}ms instead.`;
94
+ const n = typeof raw === "number" ? raw : Number(raw);
95
+ let line;
96
+ if (!Number.isFinite(n) || n <= 0) {
97
+ line = `${knob}=${String(raw)} was ignored — not a positive finite millisecond count. Using ${usedMs}ms instead.`;
98
+ }
99
+ else {
100
+ const defect = bashTimeoutDefect(Math.floor(n));
101
+ if (defect === undefined)
102
+ return;
103
+ line = `${knob}=${String(raw)} was ignored — it ${defect}. Using ${usedMs}ms instead.`;
104
+ }
98
105
  if (announcedTimeoutConfig.has(line))
99
106
  return;
100
107
  announcedTimeoutConfig.add(line);
@@ -126,11 +133,20 @@ export function resolveBashTimeoutCaps(opts) {
126
133
  if (opts?.bashDefaultTimeoutMs !== undefined)
127
134
  announceDiscardedTimeout("bashDefaultTimeoutMs", opts.bashDefaultTimeoutMs, defaultMs);
128
135
  else if (process.env.BASH_DEFAULT_TIMEOUT_MS !== undefined)
129
- announceDiscardedTimeout("BASH_DEFAULT_TIMEOUT_MS", Number(process.env.BASH_DEFAULT_TIMEOUT_MS), defaultMs);
136
+ announceDiscardedTimeout("BASH_DEFAULT_TIMEOUT_MS", process.env.BASH_DEFAULT_TIMEOUT_MS, defaultMs);
130
137
  if (opts?.bashMaxTimeoutMs !== undefined)
131
138
  announceDiscardedTimeout("bashMaxTimeoutMs", opts.bashMaxTimeoutMs, maxMs);
132
139
  else if (process.env.BASH_MAX_TIMEOUT_MS !== undefined)
133
- announceDiscardedTimeout("BASH_MAX_TIMEOUT_MS", Number(process.env.BASH_MAX_TIMEOUT_MS), maxMs);
140
+ announceDiscardedTimeout("BASH_MAX_TIMEOUT_MS", process.env.BASH_MAX_TIMEOUT_MS, maxMs);
141
+ const requestedCap = validTimeoutMs(opts?.bashMaxTimeoutMs) ?? validTimeoutMs(Number(process.env.BASH_MAX_TIMEOUT_MS));
142
+ if (requestedCap !== undefined && requestedCap < defaultMs) {
143
+ const knob = opts?.bashMaxTimeoutMs !== undefined ? "bashMaxTimeoutMs" : "BASH_MAX_TIMEOUT_MS";
144
+ const line = `${knob}=${requestedCap} is below the resolved default budget (${defaultMs}ms) — the ceiling was raised to ${maxMs}ms (the default always fits under the cap).`;
145
+ if (!announcedTimeoutConfig.has(line)) {
146
+ announcedTimeoutConfig.add(line);
147
+ console.warn(line);
148
+ }
149
+ }
134
150
  return { defaultMs, maxMs };
135
151
  }
136
152
  export function __resetBashTimeoutAnnouncements() {
@@ -121,7 +121,7 @@ export function createMonitorTool(env, opts) {
121
121
  const envWallMs = bgTimeout !== undefined ? bgTimeout * 1000 : undefined;
122
122
  const envWallWins = !isPersistent && envWallMs !== undefined && envWallMs < timeoutMs;
123
123
  const persistentBudget = bgTimeout !== undefined
124
- ? `the execution environment kills background processes after ${bgTimeout}s`
124
+ ? `the execution environment's background time budget ends the watch after ${bgTimeout}s`
125
125
  : `the execution environment's background time budget still applies`;
126
126
  const retainedByEnv = env.backgroundCapabilities.retainBackgroundProcesses === true;
127
127
  const retainedBySpec = opts.retainBackgroundProcesses === true;
@@ -142,8 +142,8 @@ export function createMonitorTool(env, opts) {
142
142
  const lifetime = isPersistent
143
143
  ? `It is persistent: no watch timeout of its own${timeoutIgnoredNote}, but ${persistentBudget}${persistentAnchor}`
144
144
  : envWallWins
145
- ? `It will be killed after ${envWallMs}ms if still running — the execution environment's background time budget (${bgTimeout}s) is shorter than the ${timeoutMs}ms watch timeout${clampNote}, and the shorter of the two wins.`
146
- : `It will be killed after ${timeoutMs}ms${clampNote} if still running.`;
145
+ ? `The watch may run up to ${envWallMs}ms — the execution environment's background time budget (${bgTimeout}s) is shorter than the ${timeoutMs}ms watch timeout${clampNote}, and the shorter of the two wins.`
146
+ : `The watch may run up to ${timeoutMs}ms${clampNote}.`;
147
147
  const content = onNotify !== undefined
148
148
  ? `Monitoring in background; task_id=${taskId}. Each stdout line becomes a notification event (lines within ` +
149
149
  `~200ms are batched); you'll get a final notification with the exit code when it ends — do not poll. ` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "5.23.0",
3
+ "version": "5.25.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",