@sema-agent/core 5.46.0 → 5.47.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 +41 -0
  2. package/dist/agents/subagent.js +129 -2
  3. package/dist/core/governance-codes.d.ts +13 -0
  4. package/dist/core/governance-codes.js +33 -0
  5. package/dist/core/memory-engine/delegation-settlement.d.ts +318 -0
  6. package/dist/core/memory-engine/delegation-settlement.js +661 -0
  7. package/dist/core/memory-engine/engine.d.ts +159 -1
  8. package/dist/core/memory-engine/engine.js +699 -15
  9. package/dist/core/memory-engine/file-backend.d.ts +1 -0
  10. package/dist/core/memory-engine/file-backend.js +3 -1
  11. package/dist/core/memory-engine/frontmatter.d.ts +46 -19
  12. package/dist/core/memory-engine/frontmatter.js +91 -77
  13. package/dist/core/memory-engine/index.d.ts +4 -3
  14. package/dist/core/memory-engine/index.js +3 -2
  15. package/dist/core/memory-engine/layout.d.ts +14 -0
  16. package/dist/core/memory-engine/layout.js +2 -2
  17. package/dist/core/memory-engine/memory-backend-contract.js +43 -0
  18. package/dist/core/memory-engine/origin-clearance.d.ts +66 -0
  19. package/dist/core/memory-engine/origin-clearance.js +84 -0
  20. package/dist/core/memory-engine/provenance-wording.d.ts +50 -0
  21. package/dist/core/memory-engine/provenance-wording.js +15 -0
  22. package/dist/core/memory-engine/tools.d.ts +61 -7
  23. package/dist/core/memory-engine/tools.js +34 -9
  24. package/dist/core/memory-engine/types.d.ts +70 -2
  25. package/dist/core/runner/prepare-memory.js +50 -15
  26. package/dist/core/runner/prepare-task.d.ts +24 -0
  27. package/dist/core/runner/prepare-task.js +80 -10
  28. package/dist/core/session-reconcile.js +3 -2
  29. package/dist/core/types.d.ts +38 -3
  30. package/dist/core/types.js +3 -0
  31. package/dist/index.d.ts +2 -1
  32. package/dist/index.js +2 -1
  33. package/dist/tools/task-list.d.ts +5 -1
  34. package/package.json +1 -1
  35. package/test/export-surface.snapshot.json +10 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.47.0 — 2026-08-20
4
+
5
+ ### Added
6
+ - design/336 slices 2+3 — the F-020 deep fix completes:
7
+ - **Slice 2 (settlement + hold)**: a delegation's memory-pollution verdict is a durable
8
+ SETTLEMENT ACCOUNT (three strict side-ledgers; write-ahead launch rows; crash-idempotent
9
+ retry; fork/bg lane parity), and instruction-form files under exposure ride a three-state
10
+ HOLD (held → released/disposed, loud timeout `config.memory_hold_timeout`;
11
+ `MemoryEngineOptions.holdSettleTimeoutMs`). `HarvestReport.containment` (#331) and the
12
+ `memory.hold_*` notice family land with it. The origin representation family is
13
+ serialization-closed (carrier tokenization + round-trip property laws).
14
+ - **Slice 3 (read side + human faces)**: `memory_search` returns two exposure bands (clean
15
+ first; `MemoryEntryHeader.exposure`; `MemorySearchHit` becomes a discriminated union
16
+ CleanHit | ExposedHit — see BREAKING); exposed entries render as opaque handles (zero
17
+ model-authored text on the passive face); `memory_get` delivery carries a fact-form banner
18
+ and marks the session derived (`onTaintedDelivery` — mechanical closure of source-scope
19
+ laundering); the memory-dir Read lane head-parses committed origin. Human faces (host API
20
+ only, deliberately no model tool): `listExternalOriginEntries` + `clearEntryOrigin` (audited
21
+ un-mark: write-ahead custody → committed tombstone → same-id re-record;
22
+ `listOriginClearances`; `memory.origin_clear_*` family). A live behavioral probe pins that a
23
+ marked entry in context is still used normally (deepseek-v4-flash, zero refusal).
24
+ - `memoryProvenance: "off"` keeps every read face byte-identical to 5.46 (pinned directly).
25
+ - `NOTICE_AUDIENCE` + `noticeAudienceOf` — the presentation-tier registry (who a notice code is
26
+ for): "user" for the session-memory posture disclosures, everything else defaults "operator".
27
+ `EngineNotice.sessionId` typed top-level key, lifted from `detail.sessionId` at the delivery
28
+ throat (mint sites untouched). `ambiguousOriginRepresentation` exported (the origin ambiguity
29
+ predicate a third-party backend refuses multi-carrier representations with).
30
+ - The session-reconcile never-started texts condition their re-issue cue on the blocking
31
+ approval/gate being resolved first (#352 — the side-effect statement stayed true; the action
32
+ cue no longer amplifies a blocked-approval loop).
33
+
34
+ ### Changed
35
+ - **BREAKING (types)**: `MemorySearchHit` is now a discriminated union (CleanHit | ExposedHit)
36
+ instead of a single interface. Backend contract: the conformance suite gains two cases
37
+ (exposure carriage + two-band truncation boundary) — a backend implementing the pre-336 shape
38
+ fails them (upgrade-order duty: adopt the new clauses before taking 5.47 in a mixed fleet).
39
+ `memory_get` tool contract revision 3→4, `memory_search` 2→3 (execute-visible text changes).
40
+ - The kill-tree descendant-reap suite's liveness probe reads a ZOMBIE as dead (a landed SIGKILL
41
+ whose parent died first waits for init to reap it; under load that outlasted any fixed poll
42
+ window — the 5.30.0/5.46.0 publish-gate flake, root-caused and fixed).
43
+
3
44
  ## 5.46.0 — 2026-08-19
4
45
 
5
46
  ### Added
@@ -3,6 +3,8 @@ import { isAbsolute } from "node:path";
3
3
  import { withDelegationProvenance } from "../core/tool-policy.js";
4
4
  import { isHighSurrogate, isLowSurrogate } from "../core/surrogate-safe-slice.js";
5
5
  import { newDelegationProvenanceAggregate, reduceDelegationAttestation } from "../core/memory-engine/delegation-provenance.js";
6
+ import { registerDelegationLaunch, replayExternalSettlementEffects, settleDelegation } from "../core/memory-engine/delegation-settlement.js";
7
+ import { enqueueMemoryAnnouncement } from "../core/memory-engine/layout.js";
6
8
  import { resolveModel, resolveModelDisplayLabel } from "../core/roles.js";
7
9
  import { OUTPUT_TOOL_NAME, REPORT_BLOCKED_TOOL_NAME } from "../core/runner/synthetic-tools.js";
8
10
  import { TOOL_SEARCH_NAME } from "../core/runner/tool-disclosure.js";
@@ -1935,6 +1937,59 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1935
1937
  const shortDesc = `fork: ${(typeof a.description === "string" && a.description.trim() ? a.description.trim() : prompt).slice(0, 180)}`;
1936
1938
  const bgOwner = sessionScopedBg ? ctx.sessionId : ctx.taskId ?? bg.owner;
1937
1939
  const bgScope = treeScope;
1940
+ const forkSettlementSeat = ctx.delegationSettlement?.();
1941
+ const forkSettleId = forkSettlementSeat !== undefined ? `bg-${uuidv7()}` : undefined;
1942
+ if (forkSettlementSeat !== undefined && forkSettleId !== undefined) {
1943
+ try {
1944
+ registerDelegationLaunch(forkSettlementSeat.controlDir, { settleId: forkSettleId, sessionId: forkSettlementSeat.sessionId, now: Date.now, ...(ctx.toolCallId !== undefined ? { toolUseId: ctx.toolCallId } : {}) });
1945
+ }
1946
+ catch (e) {
1947
+ dropHostAbortListener();
1948
+ const wt = await finishWorktree();
1949
+ return {
1950
+ isError: true,
1951
+ content: `Sub-agent not started in background: the delegation settlement account could not record the launch (${(e instanceof Error ? e.message : String(e)).slice(0, 300)}) — fail-closed; repair the memory control plane or retry.${wt ? `\n${wt}` : ""}`,
1952
+ details: { error: "settlement_write_ahead_failed" },
1953
+ };
1954
+ }
1955
+ }
1956
+ const settleForkRow = (status) => {
1957
+ if (forkSettlementSeat === undefined || forkSettleId === undefined)
1958
+ return;
1959
+ const att = status !== undefined ? childAttestation(status) : undefined;
1960
+ const verdict = att === "external" ? "external" : att === "clean" ? "clean" : "unattestable";
1961
+ let settled = false;
1962
+ let lastErr;
1963
+ for (let attempt = 0; attempt < 3 && !settled; attempt++) {
1964
+ try {
1965
+ settleDelegation(forkSettlementSeat.controlDir, { settleId: forkSettleId, status: verdict, now: Date.now });
1966
+ settled = true;
1967
+ }
1968
+ catch (e) {
1969
+ lastErr = e;
1970
+ }
1971
+ }
1972
+ if (settled && verdict === "external") {
1973
+ try {
1974
+ replayExternalSettlementEffects(forkSettlementSeat.controlDir, { carry: true, now: Date.now });
1975
+ }
1976
+ catch {
1977
+ }
1978
+ }
1979
+ if (!settled) {
1980
+ const detail = lastErr instanceof Error ? lastErr.message : String(lastErr);
1981
+ try {
1982
+ enqueueMemoryAnnouncement(forkSettlementSeat.controlDir, {
1983
+ kind: "gate",
1984
+ at: Date.now(),
1985
+ items: [`delegation settlement: the terminal observation for a background fork of session ${JSON.stringify(forkSettlementSeat.sessionId)} could NOT be recorded (verdict ${verdict}) — the row stays pending and expires as UNPROVEN at the settlement window (fail-closed floor): ${detail.slice(0, 200)}`],
1986
+ });
1987
+ }
1988
+ catch {
1989
+ console.warn(`[sema] delegation settlement terminal write failed (fork, verdict ${verdict}, session ${forkSettlementSeat.sessionId}): ${detail}`);
1990
+ }
1991
+ }
1992
+ };
1938
1993
  let taskId;
1939
1994
  try {
1940
1995
  taskId = bg.registry.registerBackgroundAgent({
@@ -1962,6 +2017,13 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1962
2017
  catch {
1963
2018
  }
1964
2019
  const wt = await finishWorktree();
2020
+ if (forkSettlementSeat !== undefined && forkSettleId !== undefined) {
2021
+ try {
2022
+ settleDelegation(forkSettlementSeat.controlDir, { settleId: forkSettleId, status: "void", now: Date.now, note: "registration failed before invoke (not dispatched)" });
2023
+ }
2024
+ catch {
2025
+ }
2026
+ }
1965
2027
  return { isError: true, content: `Sub-agent not started in background: ${e instanceof Error ? e.message : String(e)}${wt ? `\n${wt}` : ""}`, details: { error: "register_failed" } };
1966
2028
  }
1967
2029
  childInternals.peerSelfRef?.addAxis("h", taskId);
@@ -2106,6 +2168,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2106
2168
  void opts.runner
2107
2169
  .runTask(bgForkSpec, bgForkInternals)
2108
2170
  .then(async (child) => {
2171
+ settleForkRow(child.status);
2109
2172
  dropHostAbortListener();
2110
2173
  await finishWorktree();
2111
2174
  try {
@@ -2202,6 +2265,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2202
2265
  }
2203
2266
  })
2204
2267
  .catch((e) => {
2268
+ settleForkRow("failed");
2205
2269
  dropHostAbortListener();
2206
2270
  void finishWorktree();
2207
2271
  const killed = abort.signal.aborted;
@@ -2268,7 +2332,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2268
2332
  : `Note: it is stopped automatically (killed) if still running when this task ends — do not promise the user results beyond this task.`,
2269
2333
  ],
2270
2334
  }),
2271
- details: { type: "agent", subagent_type: FORK_SUBAGENT_TYPE, status: "async_launched", isAsync: true, task_id: taskId, description: shortDesc, prompt },
2335
+ details: { type: "agent", subagent_type: FORK_SUBAGENT_TYPE, status: "async_launched", isAsync: true, task_id: taskId, description: shortDesc, prompt, ...(forkSettleId !== undefined ? { settle_id: forkSettleId } : {}) },
2272
2336
  };
2273
2337
  }
2274
2338
  const bgIgnoredNote = a.run_in_background === true
@@ -2368,6 +2432,60 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2368
2432
  const shortDesc = reviveRow?.description ?? String(a.description ?? "sub-agent").slice(0, 200);
2369
2433
  const bgOwner = reviveRow !== undefined ? reviveRow.owner : sessionScopedBg ? ctx.sessionId : ctx.taskId ?? bg.owner;
2370
2434
  const bgScope = treeScope;
2435
+ const settlementSeat = ctx.delegationSettlement?.();
2436
+ const bgSettleId = settlementSeat !== undefined ? `bg-${uuidv7()}` : undefined;
2437
+ if (settlementSeat !== undefined && bgSettleId !== undefined) {
2438
+ try {
2439
+ registerDelegationLaunch(settlementSeat.controlDir, { settleId: bgSettleId, sessionId: settlementSeat.sessionId, now: Date.now, ...(ctx.toolCallId !== undefined ? { toolUseId: ctx.toolCallId } : {}) });
2440
+ }
2441
+ catch (e) {
2442
+ dropHostAbortListener();
2443
+ await cancelObserver();
2444
+ const wt = await finishWorktree();
2445
+ return {
2446
+ isError: true,
2447
+ content: `Sub-agent not started in background: the delegation settlement account could not record the launch (${(e instanceof Error ? e.message : String(e)).slice(0, 300)}) — fail-closed; repair the memory control plane or retry.${wt ? `\n${wt}` : ""}`,
2448
+ details: { error: "settlement_write_ahead_failed" },
2449
+ };
2450
+ }
2451
+ }
2452
+ const settleDelegationRow = (status) => {
2453
+ if (settlementSeat === undefined || bgSettleId === undefined)
2454
+ return;
2455
+ const att = status !== undefined ? childAttestation(status) : undefined;
2456
+ const verdict = att === "external" ? "external" : att === "clean" ? "clean" : "unattestable";
2457
+ let settled = false;
2458
+ let lastErr;
2459
+ for (let attempt = 0; attempt < 3 && !settled; attempt++) {
2460
+ try {
2461
+ settleDelegation(settlementSeat.controlDir, { settleId: bgSettleId, status: verdict, now: Date.now });
2462
+ settled = true;
2463
+ }
2464
+ catch (e) {
2465
+ lastErr = e;
2466
+ }
2467
+ }
2468
+ if (settled && verdict === "external") {
2469
+ try {
2470
+ replayExternalSettlementEffects(settlementSeat.controlDir, { carry: true, now: Date.now });
2471
+ }
2472
+ catch {
2473
+ }
2474
+ }
2475
+ if (!settled) {
2476
+ const detail = lastErr instanceof Error ? lastErr.message : String(lastErr);
2477
+ try {
2478
+ enqueueMemoryAnnouncement(settlementSeat.controlDir, {
2479
+ kind: "gate",
2480
+ at: Date.now(),
2481
+ items: [`delegation settlement: the terminal observation for a background delegation of session ${JSON.stringify(settlementSeat.sessionId)} could NOT be recorded (verdict ${verdict}) — the row stays pending and expires as UNPROVEN at the settlement window (fail-closed floor): ${detail.slice(0, 200)}`],
2482
+ });
2483
+ }
2484
+ catch {
2485
+ console.warn(`[sema] delegation settlement terminal write failed (verdict ${verdict}, session ${settlementSeat.sessionId}): ${detail} — the pending row expires as unproven at the settlement window`);
2486
+ }
2487
+ }
2488
+ };
2371
2489
  let taskId;
2372
2490
  try {
2373
2491
  taskId = bg.registry.registerBackgroundAgent(reviveRow !== undefined
@@ -2427,6 +2545,13 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2427
2545
  dropHostAbortListener();
2428
2546
  await cancelObserver();
2429
2547
  const wt = await finishWorktree();
2548
+ if (settlementSeat !== undefined && bgSettleId !== undefined) {
2549
+ try {
2550
+ settleDelegation(settlementSeat.controlDir, { settleId: bgSettleId, status: "void", now: Date.now, note: "registration failed before invoke (not dispatched)" });
2551
+ }
2552
+ catch {
2553
+ }
2554
+ }
2430
2555
  return { isError: true, content: `Sub-agent not started in background: ${e instanceof Error ? e.message : String(e)}${wt ? `\n${wt}` : ""}`, details: { error: "register_failed" } };
2431
2556
  }
2432
2557
  childInternals.peerSelfRef?.addAxis("h", taskId);
@@ -2744,6 +2869,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2744
2869
  : opts.runner.runTask(bgSpec, bgInternals);
2745
2870
  void bgChildPromise
2746
2871
  .then(async (child) => {
2872
+ settleDelegationRow(child.status);
2747
2873
  closeObserverWindow(child.status);
2748
2874
  dropHostAbortListener();
2749
2875
  await finishWorktree();
@@ -2938,6 +3064,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2938
3064
  }
2939
3065
  })
2940
3066
  .catch(async (e) => {
3067
+ settleDelegationRow("failed");
2941
3068
  closeObserverWindow("failed");
2942
3069
  dropHostAbortListener();
2943
3070
  void finishWorktree();
@@ -3043,7 +3170,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
3043
3170
  : `Note: it is stopped automatically (killed) if still running when this task ends — do not promise the user results beyond this task.`,
3044
3171
  ],
3045
3172
  }),
3046
- details: { type: "agent", status: "async_launched", isAsync: true, task_id: taskId, description: shortDesc, prompt },
3173
+ details: { type: "agent", status: "async_launched", isAsync: true, task_id: taskId, description: shortDesc, prompt, ...(bgSettleId !== undefined ? { settle_id: bgSettleId } : {}) },
3047
3174
  };
3048
3175
  }
3049
3176
  let child;
@@ -67,3 +67,16 @@ export type RuleSyncDropReason = keyof typeof RULE_SYNC_DROP_CODES;
67
67
  /** The subset that may appear on a LOCAL quarantined row (design/182 §8.1 `quarantine` instruction /
68
68
  * fence arm / local screening). `own_actor_forged` is inbound-only by construction. */
69
69
  export type RuleQuarantineReason = Exclude<RuleSyncDropReason, "own_actor_forged">;
70
+ /**
71
+ * The presentation-tier registry (the server's wire whitelist retires into this
72
+ * table once shipped): WHO a notice code is for. `"user"` = a session-scoped disclosure the end
73
+ * user of that session should see (safe to project onto the session's event stream); `"operator"`
74
+ * = a deployment/config/ops fact for whoever runs the process. Presentation tier is a property of
75
+ * the CODE (closed set, one code one tier) — never of an individual emission, which is why this is
76
+ * a registry and not an EngineNotice field. Codes absent from the table read as `"operator"`
77
+ * (the conservative default: never push an unclassified code at an end user).
78
+ * Orthogonal to {@link NON_GOVERNANCE_MEMORY_CODES} (a retry-semantics table, not presentation).
79
+ */
80
+ export declare const NOTICE_AUDIENCE: Readonly<Record<string, "user" | "operator">>;
81
+ /** The audience for `code` — table lookup with the conservative `"operator"` default. */
82
+ export declare function noticeAudienceOf(code: string): "user" | "operator";
@@ -29,6 +29,28 @@ export const NON_GOVERNANCE_MEMORY_CODES = new Set([
29
29
  "memory.erasure_index_residue",
30
30
  "memory.export_incomplete",
31
31
  "memory.import_rejected",
32
+ "memory.hold_opened",
33
+ "memory.hold_released",
34
+ "memory.hold_disposed",
35
+ "memory.settlement_resolve_unattributed",
36
+ "memory.settlement_resolve_invalid",
37
+ "memory.settlement_resolve_unknown",
38
+ "memory.session_account_resolve_unattributed",
39
+ "memory.session_account_resolve_unknown",
40
+ "memory.hold_resolve_unattributed",
41
+ "memory.hold_resolve_invalid",
42
+ "memory.hold_resolve_unknown",
43
+ "memory.hold_resolve_invalid_state",
44
+ "memory.settlement_record_failed",
45
+ "memory.session_account_failed",
46
+ "memory.origin_clear_unattributed",
47
+ "memory.origin_clear_invalid",
48
+ "memory.origin_clear_unknown",
49
+ "memory.origin_clear_not_marked",
50
+ "memory.origin_clear_challenged",
51
+ "memory.origin_clear_pending",
52
+ "memory.origin_clear_conflict",
53
+ "memory.origin_clear_failed",
32
54
  ]);
33
55
  export function governanceRetryClass(code) {
34
56
  if (Object.prototype.hasOwnProperty.call(GOVERNANCE_CODES, code)) {
@@ -46,3 +68,14 @@ export const RULE_SYNC_DROP_CODES = {
46
68
  below_gc_frontier: "local-quarantined",
47
69
  server_rejected: "local-quarantined",
48
70
  };
71
+ export const NOTICE_AUDIENCE = {
72
+ "memory.session_polluted": "user",
73
+ "memory.harvest_quarantined": "user",
74
+ "memory.delegation_static_mark_waived": "user",
75
+ "memory.hold_opened": "user",
76
+ "memory.hold_released": "user",
77
+ "memory.hold_disposed": "user",
78
+ };
79
+ export function noticeAudienceOf(code) {
80
+ return NOTICE_AUDIENCE[code] ?? "operator";
81
+ }
@@ -0,0 +1,318 @@
1
+ import type { MemoryOriginCause } from "./types.js";
2
+ export declare const DELEGATION_SETTLEMENT_FILE = "delegation-settlement.json";
3
+ /** A row's FIRST-WRITE status. Rows are immutable after their first write — every later transition
4
+ * is an append-only event ({@link SettlementEvent}); the effective status is the fold. */
5
+ export type SettlementRowStatus = "pending" | "unattestable";
6
+ /** The effective (folded) status vocabulary. */
7
+ export type SettlementStatus = "pending" | "clean" | "external" | "expired" | "void" | "unattestable";
8
+ /** One delegation's launch fact (write-ahead: this row lands BEFORE the delegation registers or
9
+ * invokes — a launch that cannot be accounted must not run). */
10
+ export interface DelegationSettlementRow {
11
+ /** Background lane: minted at the write-ahead point (the launch site holds it thereafter).
12
+ * Sync lane: the injective `(sessionId, toolCallId)` encoding ({@link syncSettleId}). */
13
+ settleId: string;
14
+ sessionId: string;
15
+ launchedAt: number;
16
+ lane: "background" | "sync";
17
+ status: SettlementRowStatus;
18
+ /** The delegation TOOL CALL that launched this row (background lane) — the receipt-exemption
19
+ * anchor: a sync delivery claiming to be a background launch receipt is honored only when a
20
+ * write-ahead row for ITS OWN toolUseId exists (a status string alone must not buy the
21
+ * exemption — adversarial round 2's forged-receipt vehicle). */
22
+ toolUseId?: string;
23
+ }
24
+ /** One append-only transition/annotation event. Idempotent per eventId. */
25
+ export interface SettlementEvent {
26
+ settleId: string;
27
+ eventId: string;
28
+ at: number;
29
+ kind: "settle" | "admin-resolve" | "annotation";
30
+ /** For `settle`: the observed terminal (`"unattestable"` = the delegation terminated but its
31
+ * conduct is permanently unprovable — a recorder-less or incomplete chain). For `admin-resolve`:
32
+ * the host's ruling ("clean"|"external"). */
33
+ status?: "clean" | "external" | "expired" | "void" | "unattestable";
34
+ /** Audit attribution for `admin-resolve` (who asked — required there, refused absent). */
35
+ requestId?: string;
36
+ note?: string;
37
+ }
38
+ /** The injective sync-lane settleId: `encodeURIComponent` on both members keeps the separator
39
+ * unambiguous (neither side can smuggle a `:`), so `(sessionId, toolCallId)` is one-to-one. */
40
+ export declare function syncSettleId(sessionId: string, toolCallId: string): string;
41
+ /**
42
+ * §3.3 write-ahead — land the pending row for a BACKGROUND delegation, before registration and
43
+ * before invoke (行→注册→invoke). Idempotent per settleId (a revive cycle re-registers under a
44
+ * fresh settleId; a crash-retry of the same launch replays the same row). Throws on an unwritable
45
+ * or corrupt ledger — the launch site fails the launch loudly (a delegation that cannot be
46
+ * accounted must not run: the whole point is that no child exists outside the account).
47
+ */
48
+ export declare function registerDelegationLaunch(controlDir: string, input: {
49
+ settleId: string;
50
+ sessionId: string;
51
+ now: () => number;
52
+ toolUseId?: string;
53
+ }): void;
54
+ /**
55
+ * §3.5 (r5-3) — the SYNC lane's unattestable row: a synchronous delegation delivered with a
56
+ * missing/unknown attestation under the "attested-only" evidence standard. The row is a durable
57
+ * TERMINAL at first write (there is nothing left to settle — the window is already closed
58
+ * unprovable), and it must commit BEFORE the result enters the transcript: the caller treats a
59
+ * throw here as "deliver an error instead" (fail-closed — unattested content must not ride into
60
+ * the transcript of a session whose account could not record the fact).
61
+ */
62
+ export declare function recordSyncUnattestable(controlDir: string, input: {
63
+ sessionId: string;
64
+ toolCallId: string;
65
+ now: () => number;
66
+ }): void;
67
+ /**
68
+ * §3.3 settle — append one observed terminal (idempotent per eventId; the default eventId keys on
69
+ * (settleId, status) so an at-least-once observer replays into one event). Transition legality is
70
+ * the FOLD's question, not the append's: an event that the fold rules out (e.g. `clean` arriving
71
+ * after `expired`) still lands — as an event row the fold reads as an annotation-grade fact — so
72
+ * the account never loses an observation (事件不丢), it just doesn't let it rewrite history.
73
+ */
74
+ export declare function settleDelegation(controlDir: string, input: {
75
+ settleId: string;
76
+ status: "clean" | "external" | "expired" | "void" | "unattestable";
77
+ now: () => number;
78
+ eventId?: string;
79
+ note?: string;
80
+ }): void;
81
+ /**
82
+ * §3.3 host valve — `resolveSettlement(settleId, "clean"|"external", requestId)`. Advisory, same
83
+ * trust plane as the host API family; `requestId` is the audit anchor and is REQUIRED (a
84
+ * settlement resolution without attribution is refused loudly, #123 posture). Effective-state
85
+ * semantics live in the fold: an admin `clean` flips the EFFECTIVE state (releases post-expiry)
86
+ * but never the history bit, and a LATER observed `external` overrides it (观测证据>admin 裁决).
87
+ */
88
+ export declare function resolveSettlementRecord(controlDir: string, input: {
89
+ settleId: string;
90
+ to: "clean" | "external";
91
+ requestId: string;
92
+ now: () => number;
93
+ }): void;
94
+ /** One settlement's folded (effective) view. */
95
+ export interface EffectiveSettlement {
96
+ row: DelegationSettlementRow;
97
+ /** The effective status after the fold (终局序: clean/external 互斥首达胜; external 可覆写
98
+ * expired; admin clean 翻 effective 不翻史; admin 之后的 observed external 再覆写). */
99
+ effective: SettlementStatus;
100
+ /** Monotonic history bit: an `external` was EVER observed (never cleared, admin included not —
101
+ * admin resolves are rulings, not observations). */
102
+ everExternal: boolean;
103
+ /** The terminal event whose identity keys the external-effects replay (undefined while pending). */
104
+ terminalEventId?: string;
105
+ }
106
+ /** Lock-less folded read of the whole ledger (journal-aware, fail-closed on corruption). */
107
+ export declare function effectiveSettlements(controlDir: string): EffectiveSettlement[];
108
+ /** One session's folded rows (the harvest classifier's input). */
109
+ export declare function sessionSettlements(controlDir: string, sessionId: string): EffectiveSettlement[];
110
+ /**
111
+ * §3.1 — the session exposure classifier over the settlement account (the durable pollution marker
112
+ * is the OTHER input; the caller ORs them). `exposed` ⇔ any {external, unattestable, expired}
113
+ * effective row; `pending` ⇔ not exposed ∧ any pending row. The mint cause follows the strongest
114
+ * evidence: an external row is an observed fact; unattestable/expired are evidence windows that
115
+ * closed unprovable (the static-standard cause).
116
+ */
117
+ export declare function classifySessionSettlements(rows: readonly EffectiveSettlement[]): {
118
+ state: "clean" | "pending" | "exposed";
119
+ cause?: MemoryOriginCause;
120
+ reason?: string;
121
+ };
122
+ /**
123
+ * §3.3-4 — expire overdue pending rows (reconciliation's first leg). Every expiry is an event
124
+ * (idempotent by (settleId, "expired") key); returns the settleIds newly expired this pass.
125
+ */
126
+ export declare function expireOverdueSettlements(controlDir: string, input: {
127
+ timeoutMs: number;
128
+ now: () => number;
129
+ }): string[];
130
+ /**
131
+ * §3.3-2 — replay the EXTERNAL rows' side effects (账先落、副作用可重放): ensure the durable
132
+ * pollution marker is in place (wx, first-write-wins) and the retroaction challenge sweep has
133
+ * covered the session's unmarked committed contributions (eventId keyed to the terminal event —
134
+ * a replay lands the SAME generation, a later terminal re-arms). Also enqueues the next-session
135
+ * announcement once per terminal event. Idempotent throughout; the watermark only skips work.
136
+ * `carry` scopes the sweep domain exactly like the engine's own sweeps (marked rows carry their
137
+ * account under "carry"; the "off" width challenges everything).
138
+ */
139
+ export declare function replayExternalSettlementEffects(controlDir: string, input: {
140
+ carry: boolean;
141
+ now: () => number;
142
+ }): Array<{
143
+ settleId: string;
144
+ sessionId: string;
145
+ }>;
146
+ export declare const SESSION_ACCOUNTS_FILE = "session-accounts.json";
147
+ export interface SessionAccountRow {
148
+ sessionId: string;
149
+ openedAt: number;
150
+ /** The STICKY unattributed set (rel paths), frozen at materialize (§3.6 归属判据): pre-existing,
151
+ * uncommitted files seen while a FOREIGN dangling open row stood. Membership never shrinks on
152
+ * later writes (the r3-5 sticky law); it clears only when the row is re-opened with a fresh
153
+ * classification (the residue was adjudicated by an intervening harvest). */
154
+ unattributed: string[];
155
+ closedAt?: number;
156
+ /** r7-3 — the host valve closed this row WITHOUT adjudication: it stops dangling but keeps
157
+ * triggering the residue arm until the next FULL-domain harvest closes an account normally
158
+ * (an unadjudicated close must not launder the residue window it covered). */
159
+ unadjudicated?: true;
160
+ }
161
+ /** The dangling-trigger read: open rows (and unadjudicated closes) belonging to OTHER sessions.
162
+ * These are what arm the unattributed residue classification at a session's materialize. */
163
+ export declare function foreignDanglingSessionAccounts(controlDir: string, selfSessionId: string): SessionAccountRow[];
164
+ /** Open (or re-open, on a same-session resume) the session's account row, persisting the sticky
165
+ * unattributed classification frozen at THIS materialize. */
166
+ export declare function openSessionAccount(controlDir: string, input: {
167
+ sessionId: string;
168
+ now: () => number;
169
+ unattributed: readonly string[];
170
+ }): void;
171
+ /** The current session's sticky unattributed set (the harvest's residue-arm input). */
172
+ export declare function sessionUnattributedSet(controlDir: string, sessionId: string): Set<string>;
173
+ /** Close the session's account row — called ONLY after a FULL-domain harvest (zero deferred
174
+ * files): a partial harvest's close would launder the deferred residue window (§3.6 序则②).
175
+ * A normal full close also clears every standing `unadjudicated` flag (r7-3: the valve's
176
+ * conservative window ends when a full harvest has adjudicated the plane). */
177
+ export declare function closeSessionAccount(controlDir: string, input: {
178
+ sessionId: string;
179
+ now: () => number;
180
+ }): void;
181
+ /** The host valve for a dangling row (advisory; §3.6 — dangling rows never auto-expire). The
182
+ * close is `closed-unadjudicated` (r7-3): it stops the row dangling but the residue arm keeps
183
+ * firing until a full-domain harvest closes normally. `requestId` required (audit, #123). */
184
+ export declare function resolveSessionAccountRecord(controlDir: string, input: {
185
+ sessionId: string;
186
+ requestId: string;
187
+ now: () => number;
188
+ }): void;
189
+ export declare const HOLDS_FILE = "holds.json";
190
+ export declare const HOLD_DIR = "hold";
191
+ export type HoldStatus = "capturing" | "held" | "released" | "disposed";
192
+ export type HoldTerminal = "dirty" | "expired" | "conflict" | "discarded" | "capture_lost";
193
+ export interface HoldRow {
194
+ holdId: string;
195
+ /** The WRITER session (lineage attribution on release records this id — the true author). */
196
+ sessionId: string;
197
+ /** The committed entry this hold's file addressed, when it addressed one (update form). */
198
+ entryId?: string;
199
+ op: "add" | "update";
200
+ /** CAS anchor at capture time (update form): release refuses to blind-write over a later edit. */
201
+ baseRev?: string;
202
+ /** Path relative to the memory dir (the plane seat the file was removed from). */
203
+ relPath: string;
204
+ /** The entry slug (path relative to the scope dir, sans `.md`) — the release leg's projection
205
+ * identity and its filename-scan input. */
206
+ slug: string;
207
+ /** The write scope the capture belonged to — the release commits into it. */
208
+ scope: string;
209
+ /** The custody file name under `hold/`. */
210
+ captureName: string;
211
+ /** sha256 over the captured bytes (minted from the ONE read buffer — the same buffer writes the
212
+ * custody file and, verified, is what release commits: no verify-reread window). */
213
+ contentDigest: string;
214
+ capturedAt: number;
215
+ /** The settlement rows whose outcome this hold waits on (the session's pending set at capture). */
216
+ settleIds: string[];
217
+ status: HoldStatus;
218
+ disposition?: {
219
+ terminal: HoldTerminal;
220
+ quarantineName?: string;
221
+ };
222
+ /** Host valve verdict awaiting the next harvest ("release" commits with cause "static"). */
223
+ resolved?: "release" | "discard";
224
+ }
225
+ export declare function readHolds(controlDir: string): HoldRow[];
226
+ /** ATOMIC no-replace restore of a staged file onto a plane path (adversarial round 2: an
227
+ * existsSync-then-rename pair is a TOCTOU — POSIX rename REPLACES a destination created between
228
+ * the check and the call, clobbering a third writer's newer bytes). `link` refuses EEXIST
229
+ * atomically: on success the staging name is dropped (one inode, two names → one); on EEXIST the
230
+ * staging file is KEPT (custody — never overwrite the plane's newer legal bytes). */
231
+ export declare function noReplaceRestore(stagingPath: string, destPath: string): "restored" | "kept" | "stranded";
232
+ /**
233
+ * §4.2 — open one hold over an instruction-form file: the two-stage journal.
234
+ * ① append row `status:"capturing"` (deterministic captureName + contentDigest minted from the
235
+ * one read buffer) — the account exists before any byte moves;
236
+ * ② write the custody file FROM THE SAME BUFFER, read it back, verify the digest;
237
+ * ③ remove the plane file RENAME-FIRST (atomic move into `hold/<staging>`), then hash what
238
+ * actually moved: equal ⇒ the staging duplicate is dropped (custody already holds verified
239
+ * bytes); unequal ⇒ a third writer replaced the file between read and rename — NO-CLOBBER
240
+ * rollback (the newer bytes go back ONLY if the plane path is still absent; a re-created path
241
+ * keeps both copies, staging stays in custody and the caller hears "conflict" — never an
242
+ * overwrite of newer legal bytes);
243
+ * ④ flip the row to `"held"`.
244
+ * Every failure path returns a structured outcome (never throws past the row append): the caller
245
+ * fail-closes the file through the ordinary quarantine containment instead — an instruction file
246
+ * must never stay on the plane because its hold failed. EXDEV on the rename is one such refusal
247
+ * (cross-filesystem staging gives up rename atomicity — refused, not degraded to copy+delete).
248
+ */
249
+ export declare function openInstructionHold(controlDir: string, input: {
250
+ sessionId: string;
251
+ relPath: string;
252
+ absPath: string;
253
+ content: string;
254
+ entryId?: string;
255
+ op: "add" | "update";
256
+ baseRev?: string;
257
+ slug: string;
258
+ scope: string;
259
+ settleIds: readonly string[];
260
+ now: () => number;
261
+ }): {
262
+ ok: true;
263
+ holdId: string;
264
+ thirdWriterStranded?: string;
265
+ } | {
266
+ ok: false;
267
+ holdId?: string;
268
+ reason: string;
269
+ };
270
+ /**
271
+ * §4.2 reconciliation — converge half-way holds (a crash between the journal stages):
272
+ * - `capturing` + custody file present & digest-equal ⇒ flip held (the crash was after ②);
273
+ * - `capturing` + custody absent + plane file present & digest-equal ⇒ complete the capture and
274
+ * continue the two-stage walk; digest-UNEQUAL ⇒ the bytes were replaced — `capture_lost`
275
+ * (the file stays on the plane and walks the ordinary/§3.6 arms; never "capture whatever is
276
+ * there now" — that would hold a third writer's bytes under the first writer's account);
277
+ * - `capturing` + custody absent + plane file gone ⇒ `capture_lost` (nothing to hold).
278
+ * - `held` + plane file REAPPEARED at the relPath ⇒ left alone (a fresh write is a fresh file —
279
+ * the next harvest judges it on its own; the hold's custody bytes stay the hold's).
280
+ */
281
+ export declare function reconcileHolds(controlDir: string, input: {
282
+ memoryDir: string;
283
+ now: () => number;
284
+ }): void;
285
+ /** Dispose one held row: custody bytes move to the quarantine directory (host-auditable), the row
286
+ * records its terminal + quarantineName (from-row-to-bytes addressing survives every terminal). */
287
+ export declare function disposeHold(controlDir: string, input: {
288
+ holdId: string;
289
+ terminal: HoldTerminal;
290
+ now: () => number;
291
+ }): {
292
+ ok: boolean;
293
+ quarantineName?: string;
294
+ };
295
+ /** Flip one row to released (the caller — the harvest release leg — has already committed the
296
+ * custody bytes through the full gate set; the custody file is dropped after the flip). */
297
+ export declare function markHoldReleased(controlDir: string, input: {
298
+ holdId: string;
299
+ now: () => number;
300
+ }): void;
301
+ /** The host valve — `resolveHold(holdId, "release"|"discard", requestId)`: records the verdict on
302
+ * the row; the NEXT harvest reconciliation executes it ("release" re-walks the full gates and
303
+ * commits WITH cause "static" — a timeout release is not proof of cleanliness; "discard"
304
+ * disposes to quarantine). requestId required (audit, #123). */
305
+ export declare function resolveHoldRecord(controlDir: string, input: {
306
+ holdId: string;
307
+ action: "release" | "discard";
308
+ requestId: string;
309
+ now: () => number;
310
+ }): void;
311
+ /** Read one hold's custody bytes (release leg + expired-release valve). Returns undefined when
312
+ * the custody file is gone or fails its digest — the caller records capture_lost, never commits
313
+ * unverified bytes. For an expired-released row the bytes may already sit in quarantine
314
+ * (disposition.quarantineName) — both seats are tried, digest-verified either way. */
315
+ export declare function readHoldCustody(controlDir: string, row: HoldRow): string | undefined;
316
+ /** True ⇔ the session's pollution marker or any non-clean settlement row blocks a clean release
317
+ * (§4.2-1: any non-clean row in the session's account keeps the hold held/disposed). */
318
+ export declare function sessionBlocksCleanRelease(controlDir: string, sessionId: string): boolean;