@sema-agent/core 7.53.0 → 7.54.1

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 (249) hide show
  1. package/CHANGELOG.md +291 -1
  2. package/LICENSE +3 -1
  3. package/dist/agents/peer-session-drain.d.ts +0 -1
  4. package/dist/agents/peer-session-drain.js +0 -2
  5. package/dist/agents/send-message-tool.js +3 -4
  6. package/dist/agents/subagent.d.ts +3 -5
  7. package/dist/agents/subagent.js +18 -11
  8. package/dist/agents/tool-filter.d.ts +7 -4
  9. package/dist/agents/tool-filter.js +7 -3
  10. package/dist/core/a2a.js +4 -2
  11. package/dist/core/allow-entry.d.ts +1 -0
  12. package/dist/core/allow-entry.js +1 -0
  13. package/dist/core/approval-object.d.ts +87 -0
  14. package/dist/core/approval-object.js +85 -0
  15. package/dist/core/arg-summary.d.ts +3 -2
  16. package/dist/core/arg-summary.js +17 -11
  17. package/dist/core/ask-origin.d.ts +43 -27
  18. package/dist/core/ask-origin.js +20 -11
  19. package/dist/core/ask-question.d.ts +0 -1
  20. package/dist/core/ask-question.js +4 -67
  21. package/dist/core/ask-unresolvable-notice.d.ts +3 -2
  22. package/dist/core/auto-compaction.d.ts +49 -50
  23. package/dist/core/auto-compaction.js +41 -51
  24. package/dist/core/auto-mode-prompt.d.ts +4 -2
  25. package/dist/core/auto-mode-prompt.js +4 -3
  26. package/dist/core/canonical-json.d.ts +69 -0
  27. package/dist/core/canonical-json.js +130 -8
  28. package/dist/core/checkpoint-requirements.d.ts +13 -7
  29. package/dist/core/checkpoint-requirements.js +4 -1
  30. package/dist/core/checkpoint-store.d.ts +33 -18
  31. package/dist/core/checkpoint-store.js +5 -4
  32. package/dist/core/child-clamp-seats.d.ts +45 -6
  33. package/dist/core/child-clamp-seats.js +56 -4
  34. package/dist/core/consolidate-scope.d.ts +1 -1
  35. package/dist/core/cross-boundary-taint.d.ts +213 -0
  36. package/dist/core/cross-boundary-taint.js +196 -0
  37. package/dist/core/decision-receipt.d.ts +44 -1
  38. package/dist/core/decision-receipt.js +16 -0
  39. package/dist/core/effective-path-target.d.ts +3 -0
  40. package/dist/core/effective-path-target.js +4 -0
  41. package/dist/core/engine-notice.d.ts +34 -2
  42. package/dist/core/engine-notice.js +17 -1
  43. package/dist/core/engine-storage-backend.d.ts +73 -0
  44. package/dist/core/engine-storage-backend.js +1 -0
  45. package/dist/core/file-history-store.d.ts +6 -2
  46. package/dist/core/file-history-store.js +1 -0
  47. package/dist/core/gate-fold.js +10 -2
  48. package/dist/core/gate-lanes.js +30 -21
  49. package/dist/core/gate-outcome.d.ts +7 -3
  50. package/dist/core/governance-codes.d.ts +7 -2
  51. package/dist/core/governance-codes.js +6 -0
  52. package/dist/core/hooks.d.ts +14 -14
  53. package/dist/core/hooks.js +1 -2
  54. package/dist/core/locked-config.d.ts +28 -1
  55. package/dist/core/locked-config.js +30 -3
  56. package/dist/core/lsp.d.ts +1 -1
  57. package/dist/core/mcp-server-spec.d.ts +66 -9
  58. package/dist/core/mcp-server-spec.js +52 -19
  59. package/dist/core/mcp.js +5 -4
  60. package/dist/core/memory-engine/engine.js +2 -1
  61. package/dist/core/memory-engine/export-bundle.d.ts +1 -1
  62. package/dist/core/memory-engine/export-bundle.js +1 -1
  63. package/dist/core/memory-engine/file-backend.d.ts +2 -6
  64. package/dist/core/memory-engine/file-backend.js +1 -12
  65. package/dist/core/memory.d.ts +3 -3
  66. package/dist/core/mode-policy.d.ts +13 -8
  67. package/dist/core/mode-policy.js +3 -2
  68. package/dist/core/mods/kernel/receive.d.ts +2 -1
  69. package/dist/core/mods/kernel/receive.js +7 -2
  70. package/dist/core/parent-spec-seat.d.ts +10 -13
  71. package/dist/core/parent-spec-seat.js +14 -6
  72. package/dist/core/pause-registry.d.ts +35 -1
  73. package/dist/core/pause-registry.js +10 -0
  74. package/dist/core/permission-rule-consent.d.ts +18 -5
  75. package/dist/core/permission-rule-consent.js +13 -8
  76. package/dist/core/permission-rule-model.d.ts +29 -6
  77. package/dist/core/permission-rule-model.js +6 -1
  78. package/dist/core/permission-rule-org.d.ts +37 -0
  79. package/dist/core/permission-rule-org.js +34 -42
  80. package/dist/core/permission-rule-session.js +2 -2
  81. package/dist/core/permission-rule-store.d.ts +8 -0
  82. package/dist/core/permission-rule-store.js +1 -0
  83. package/dist/core/permission-rule-sync.js +3 -2
  84. package/dist/core/read-only-shell.d.ts +1 -1
  85. package/dist/core/read-only-shell.js +30 -9
  86. package/dist/core/remote-env.d.ts +41 -1
  87. package/dist/core/remote-env.js +18 -0
  88. package/dist/core/resume-startup-cause.d.ts +18 -0
  89. package/dist/core/resume-startup-cause.js +12 -0
  90. package/dist/core/runner/approval-feedback.d.ts +46 -0
  91. package/dist/core/runner/approval-feedback.js +41 -0
  92. package/dist/core/runner/compaction-call-options.d.ts +3 -3
  93. package/dist/core/runner/compaction-call-options.js +2 -2
  94. package/dist/core/runner/contracts.d.ts +33 -41
  95. package/dist/core/runner/cross-boundary-lane.d.ts +87 -0
  96. package/dist/core/runner/cross-boundary-lane.js +190 -0
  97. package/dist/core/runner/decide-continuation.d.ts +4 -4
  98. package/dist/core/runner/decide-continuation.js +3 -1
  99. package/dist/core/runner/gate-exit.d.ts +32 -12
  100. package/dist/core/runner/gate-exit.js +75 -33
  101. package/dist/core/runner/inherited-ask-grants.js +2 -2
  102. package/dist/core/runner/person-path-rows.d.ts +2 -2
  103. package/dist/core/runner/prepare-acquire-reconcile.d.ts +7 -9
  104. package/dist/core/runner/prepare-acquire-reconcile.js +8 -6
  105. package/dist/core/runner/prepare-ask-lane.js +3 -3
  106. package/dist/core/runner/prepare-caps-and-workflow.d.ts +6 -5
  107. package/dist/core/runner/prepare-caps-and-workflow.js +3 -3
  108. package/dist/core/runner/prepare-config-doors.d.ts +12 -15
  109. package/dist/core/runner/prepare-config-doors.js +8 -3
  110. package/dist/core/runner/prepare-context-lane.js +6 -3
  111. package/dist/core/runner/prepare-delegation-surface.d.ts +3 -2
  112. package/dist/core/runner/prepare-delegation-surface.js +2 -3
  113. package/dist/core/runner/prepare-execution-env.d.ts +1 -2
  114. package/dist/core/runner/prepare-execution-env.js +2 -1
  115. package/dist/core/runner/prepare-gate-stations.d.ts +2 -0
  116. package/dist/core/runner/prepare-gate-stations.js +4 -3
  117. package/dist/core/runner/prepare-hands-readface.d.ts +2 -2
  118. package/dist/core/runner/prepare-hands-readface.js +1 -1
  119. package/dist/core/runner/prepare-inherited-gate.d.ts +2 -0
  120. package/dist/core/runner/prepare-inherited-gate.js +1 -0
  121. package/dist/core/runner/prepare-park-ask.d.ts +1 -1
  122. package/dist/core/runner/prepare-park-ask.js +9 -3
  123. package/dist/core/runner/prepare-policy-chain.js +2 -7
  124. package/dist/core/runner/prepare-safety-scan.d.ts +7 -2
  125. package/dist/core/runner/prepare-safety-scan.js +34 -27
  126. package/dist/core/runner/prepare-suspend-saga.d.ts +2 -2
  127. package/dist/core/runner/prepare-suspend-saga.js +6 -2
  128. package/dist/core/runner/prepare-task.js +13 -10
  129. package/dist/core/runner/prepare-turn-wiring.d.ts +10 -5
  130. package/dist/core/runner/prepare-turn-wiring.js +14 -49
  131. package/dist/core/runner/prepare-wiring-manifest.d.ts +2 -2
  132. package/dist/core/runner/prepare-wiring-manifest.js +2 -2
  133. package/dist/core/runner/prepare-workspace-restore.js +2 -2
  134. package/dist/core/runner/resume-admission.js +9 -9
  135. package/dist/core/runner/resume-apply.js +4 -4
  136. package/dist/core/runner/resume-policy-outcome.d.ts +2 -2
  137. package/dist/core/runner/resume-policy-outcome.js +18 -15
  138. package/dist/core/runner/resume-preflight.js +2 -2
  139. package/dist/core/runner/resume-review-outcome.js +3 -3
  140. package/dist/core/runner/run-leg.js +57 -33
  141. package/dist/core/runner/run-settle-and-teardown.js +2 -0
  142. package/dist/core/runner/run-terminal-adoption.js +15 -4
  143. package/dist/core/runner/session-file-state-replay.d.ts +5 -2
  144. package/dist/core/runner/session-file-state-replay.js +3 -2
  145. package/dist/core/runner/stream-steer-verb.js +2 -2
  146. package/dist/core/runner/tool-face-overlay.d.ts +24 -6
  147. package/dist/core/runner/tool-face-overlay.js +24 -15
  148. package/dist/core/runner-deps.d.ts +9 -9
  149. package/dist/core/session-store.d.ts +35 -16
  150. package/dist/core/session-store.js +20 -12
  151. package/dist/core/{shell-lexer.d.ts → shell-run-candidates.d.ts} +36 -12
  152. package/dist/core/{shell-lexer.js → shell-run-candidates.js} +145 -225
  153. package/dist/core/shell-wrapper-table.d.ts +5 -3
  154. package/dist/core/shell-wrapper-table.js +1 -1
  155. package/dist/core/store-contracts/checkpoint-store-contract.js +2 -2
  156. package/dist/core/store-contracts/session-store-contract.d.ts +49 -0
  157. package/dist/core/store-contracts/session-store-contract.js +683 -0
  158. package/dist/core/store-contracts/session-store-incarnation-contract.d.ts +12 -0
  159. package/dist/core/store-contracts/session-store-incarnation-contract.js +5 -2
  160. package/dist/core/task-event.d.ts +6 -4
  161. package/dist/core/task-registry-agent.js +1 -0
  162. package/dist/core/task-registry-shared.d.ts +5 -0
  163. package/dist/core/task-registry.d.ts +7 -0
  164. package/dist/core/task-registry.js +17 -0
  165. package/dist/core/task-result.d.ts +1 -1
  166. package/dist/core/task-spec.d.ts +6 -6
  167. package/dist/core/tighten-task-spec.d.ts +6 -4
  168. package/dist/core/tighten-task-spec.js +2 -2
  169. package/dist/core/tool-catalog-entries.d.ts +1 -1
  170. package/dist/core/tool-catalog-entries.js +52 -52
  171. package/dist/core/tool-face-reader.d.ts +49 -0
  172. package/dist/core/tool-face-reader.js +94 -0
  173. package/dist/core/tool-face.d.ts +17 -8
  174. package/dist/core/tool-face.js +1 -0
  175. package/dist/core/tool-policy.d.ts +64 -23
  176. package/dist/core/tool-policy.js +62 -56
  177. package/dist/core/tool-roster.d.ts +46 -28
  178. package/dist/core/tool-roster.js +91 -14
  179. package/dist/core/tool-spec-members.js +2 -2
  180. package/dist/core/tool-spec.d.ts +20 -4
  181. package/dist/core/tools.js +2 -0
  182. package/dist/core/trace.d.ts +24 -2
  183. package/dist/core/types.d.ts +2 -2
  184. package/dist/core/types.js +1 -1
  185. package/dist/core/untrusted-text.d.ts +7 -0
  186. package/dist/core/untrusted-text.js +3 -8
  187. package/dist/core/user-attachment.d.ts +43 -0
  188. package/dist/core/user-attachment.js +92 -15
  189. package/dist/engine/compaction/compaction.d.ts +2 -2
  190. package/dist/engine/compaction/utils.d.ts +2 -2
  191. package/dist/engine/harness/types.d.ts +7 -4
  192. package/dist/engine/llm/types.d.ts +7 -2
  193. package/dist/engine/llm/types.js +2 -1
  194. package/dist/engine/session/parent-chain-guard.d.ts +10 -0
  195. package/dist/engine/session/parent-chain-guard.js +10 -0
  196. package/dist/engine/session/repo-utils.d.ts +5 -0
  197. package/dist/engine/session/repo-utils.js +9 -0
  198. package/dist/engine/session/storage-base.js +5 -2
  199. package/dist/index.d.ts +23 -9
  200. package/dist/index.js +19 -8
  201. package/dist/internal/harness.d.ts +1 -0
  202. package/dist/internal/harness.js +1 -0
  203. package/dist/internal/llm.d.ts +1 -1
  204. package/dist/internal/llm.js +1 -1
  205. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  206. package/dist/orchestration/run-workflow-tool.js +1 -0
  207. package/dist/orchestration/workflow-governance.d.ts +1 -1
  208. package/dist/orchestration/workflow.d.ts +3 -0
  209. package/dist/orchestration/workflow.js +2 -1
  210. package/dist/prompt-assembly/catalog/schema.d.ts +3 -0
  211. package/dist/prompt-assembly/catalog/schema.js +3 -0
  212. package/dist/prompt-assembly/overlay.js +2 -2
  213. package/dist/prompt-assembly/select.js +2 -2
  214. package/dist/prompts/default.d.ts +1 -1
  215. package/dist/shell/lex.d.ts +87 -8
  216. package/dist/shell/lex.js +283 -49
  217. package/dist/stores/file/index.d.ts +8 -6
  218. package/dist/tools/artifact/artifact-tool.js +10 -0
  219. package/dist/tools/fs/bash-program-position.d.ts +3 -15
  220. package/dist/tools/fs/bash-program-position.js +3 -10
  221. package/dist/tools/fs/bash-readonly-classifier.js +2 -0
  222. package/dist/tools/fs/fs-bash.d.ts +3 -3
  223. package/dist/tools/fs/fs-bash.js +72 -101
  224. package/dist/tools/fs/fs-read.d.ts +12 -0
  225. package/dist/tools/fs/fs-read.js +10 -9
  226. package/dist/tools/fs/fs-shared.d.ts +27 -31
  227. package/dist/tools/fs/fs-shared.js +12 -15
  228. package/dist/tools/fs/fs-write.js +1 -1
  229. package/dist/tools/fs/index.js +2 -2
  230. package/dist/tools/fs/read-deny-home.d.ts +1 -1
  231. package/dist/tools/fs/read-deny-home.js +5 -4
  232. package/dist/tools/fs/read-deny.d.ts +16 -3
  233. package/dist/tools/fs/read-deny.js +12 -9
  234. package/dist/tools/fs/safety.d.ts +7 -6
  235. package/dist/tools/fs/shell-read-bindings.d.ts +5 -3
  236. package/dist/tools/fs/shell-read-bindings.js +15 -16
  237. package/dist/tools/fs/shell-read-device.js +1 -1
  238. package/dist/tools/fs/shell-read-plan.js +36 -47
  239. package/dist/tools/fs/shell-read-word.d.ts +1 -6
  240. package/dist/tools/fs/shell-read-word.js +2 -20
  241. package/dist/tools/fs/shell-reentry-text.js +1 -1
  242. package/dist/tools/web.d.ts +2 -2
  243. package/package.json +4 -1
  244. package/test/export-surface.snapshot.json +5654 -5520
  245. package/wire-changes/core@7.53.0.json +30 -8
  246. package/wire-changes/core@7.54.0.json +2870 -0
  247. package/wire-changes/core@7.54.1.json +37 -0
  248. package/dist/core/shell-scan.d.ts +0 -60
  249. package/dist/core/shell-scan.js +0 -183
@@ -0,0 +1,683 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { uuidv7 } from "../../internal/harness.js";
3
+ import { hasSessionFork, isSessionConflict } from "../session.js";
4
+ import { readMember } from "../thrown-value.js";
5
+ import { receiptRefusal, receiveOwnProperties } from "../own-property-receipt.js";
6
+ import { beginContract } from "./contract-harness.js";
7
+ import { registerSessionIncarnationCases } from "./session-store-incarnation-contract.js";
8
+ const AT = "2026-10-18T00:00:00.000Z";
9
+ const AT_MS = Date.parse(AT);
10
+ const STAMP = { api: "openai-completions", provider: "contract-kit", model: "contract-kit-model" };
11
+ const ZERO_USAGE = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } };
12
+ function userEntry(id, parentId, text = `t-${id}`) {
13
+ return { id, parentId, type: "message", timestamp: AT, message: { role: "user", content: [{ type: "text", text }], timestamp: AT_MS } };
14
+ }
15
+ function assistantEntry(id, parentId, toolCall) {
16
+ const content = toolCall !== undefined ? [{ type: "toolCall", id: toolCall, name: "Read", arguments: { file_path: "/kit/x" } }] : [{ type: "text", text: `a-${id}` }];
17
+ return { id, parentId, type: "message", timestamp: AT, message: { role: "assistant", content, ...STAMP, usage: ZERO_USAGE, stopReason: toolCall !== undefined ? "toolUse" : "stop", timestamp: AT_MS } };
18
+ }
19
+ function toolResultEntry(id, parentId, toolCallId) {
20
+ return { id, parentId, type: "message", timestamp: AT, message: { role: "toolResult", toolCallId, toolName: "Read", content: [{ type: "text", text: "ok" }], isError: false, timestamp: AT_MS } };
21
+ }
22
+ function compactionEntry(id, parentId, firstKeptEntryId) {
23
+ return { id, parentId, type: "compaction", timestamp: AT, summary: "summary of the turns above the floor", firstKeptEntryId, tokensBefore: 1000 };
24
+ }
25
+ const freshId = () => uuidv7();
26
+ function placementFor(scope, handle = `a${uuidv7().slice(-12)}`) {
27
+ return { kind: "subagent", scope: scope ?? `kit-${uuidv7().slice(-12)}`, ...(handle !== null ? { handle } : {}) };
28
+ }
29
+ const tick = () => new Promise((resolve) => setTimeout(resolve, 20));
30
+ async function append(session, entry, opts) {
31
+ await session.getStorage().appendEntry(entry, opts);
32
+ }
33
+ async function pathIds(session, leafId) {
34
+ const storage = session.getStorage();
35
+ const leaf = leafId === undefined ? await storage.getLeafId() : leafId;
36
+ return (await storage.getPathToRoot(leaf)).map((e) => e.id);
37
+ }
38
+ async function entryIds(session) {
39
+ return (await session.getStorage().getEntries()).map((e) => e.id);
40
+ }
41
+ async function thrownBy(fn, what) {
42
+ try {
43
+ await fn();
44
+ }
45
+ catch (err) {
46
+ return err;
47
+ }
48
+ assert.fail(`${what}: expected a refusal, got a result`);
49
+ }
50
+ async function rejectsWithCode(fn, code, what) {
51
+ const err = await thrownBy(fn, what);
52
+ assert.equal(readMember(err, "code"), code, `${what}: the refusal must carry code ${code}`);
53
+ }
54
+ function conflictMiss(err) {
55
+ if (isSessionConflict(err))
56
+ return undefined;
57
+ return readMember(err, "code") === "conflict"
58
+ ? `the refusal carries code "conflict" but is not this package's SessionError — a second copy of @sema-agent/core, or a hand-rolled error; the engine's reconcile retry does not recognize it either`
59
+ : `the refusal must be the engine's session conflict (SessionError "conflict"), got code ${String(readMember(err, "code"))}`;
60
+ }
61
+ async function rejectsWithConflict(fn, what) {
62
+ const miss = conflictMiss(await thrownBy(fn, what));
63
+ assert.equal(miss, undefined, `${what}: ${miss}`);
64
+ }
65
+ const LOOP_WALK_BUDGET_MS = 2000;
66
+ async function refusedAsLoop(storage, leafId, loopMembers, what) {
67
+ let timer;
68
+ const expiry = new Promise((_, reject) => {
69
+ timer = setTimeout(() => reject(new assert.AssertionError({ message: `${what}: no answer within ${LOOP_WALK_BUDGET_MS} ms — the walk does not end on a parent chain that loops back on itself` })), LOOP_WALK_BUDGET_MS);
70
+ });
71
+ const settled = Promise.resolve()
72
+ .then(() => storage.getPathToRoot(leafId))
73
+ .then((path) => ({ path }), (err) => ({ err }));
74
+ const outcome = await Promise.race([settled, expiry]).finally(() => clearTimeout(timer));
75
+ if ("path" in outcome)
76
+ assert.fail(`${what}: a parent chain that loops back on itself answered a path of ${outcome.path.length} entries instead of refusing`);
77
+ assert.equal(readMember(outcome.err, "code"), "invalid_session", `${what}: the refusal must carry code invalid_session`);
78
+ const sentence = String(readMember(outcome.err, "message"));
79
+ assert.ok(loopMembers.some((id) => sentence.includes(id)), `${what}: the refusal must name an entry of the loop (${loopMembers.join(" / ")}), got "${sentence}"`);
80
+ }
81
+ function rowOf(rows, sessionId) {
82
+ return rows.find((r) => r.sessionId === sessionId);
83
+ }
84
+ function assertNewestFirst(rows) {
85
+ for (let i = 1; i < rows.length; i++) {
86
+ assert.ok(rows[i - 1].lastActiveAt >= rows[i].lastActiveAt, `list() must be newest-first by lastActiveAt (row ${i - 1} ${rows[i - 1].lastActiveAt} before row ${i} ${rows[i].lastActiveAt})`);
87
+ }
88
+ }
89
+ function registerUntyped(store, sessionId, owner) {
90
+ return Reflect.apply(store.register, store, [sessionId, owner]);
91
+ }
92
+ const hasOwnership = (s) => typeof s.ownerOf === "function" && typeof s.register === "function";
93
+ const declaresPlacements = (s) => s.placements?.subagent !== undefined;
94
+ async function honorsRequireExisting(store) {
95
+ const probe = freshId();
96
+ await store.acquire(probe);
97
+ try {
98
+ await store.acquire(probe, { requireExisting: true });
99
+ return true;
100
+ }
101
+ catch {
102
+ return false;
103
+ }
104
+ }
105
+ async function readBack(store, sessionId) {
106
+ return (await store.acquire(sessionId, (await honorsRequireExisting(store)) ? { requireExisting: true } : undefined)).session;
107
+ }
108
+ function assertActivityOrder(rows, newerToOlder, what) {
109
+ for (let i = 1; i < newerToOlder.length; i++) {
110
+ const newer = rowOf(rows, newerToOlder[i - 1]);
111
+ const older = rowOf(rows, newerToOlder[i]);
112
+ assert.ok(newer !== undefined && older !== undefined, `${what}: both sessions are listed`);
113
+ assert.ok(newer.lastActiveAt >= older.lastActiveAt, `${what}: ${newerToOlder[i - 1]} was active after ${newerToOlder[i]} (lastActiveAt ${newer.lastActiveAt} < ${older.lastActiveAt})`);
114
+ }
115
+ }
116
+ const HOOK_WORDS = { subject: "SessionStoreContractHooks", unit: "hook", moment: "at sessionStoreContract" };
117
+ const REQUIRED_HOOKS = ["deleteSession", "reopen"];
118
+ const OPTIONAL_HOOKS = ["replica", "runAssertion", "onOptionalMember"];
119
+ function receiveHooks(make, hooks) {
120
+ if (typeof make !== "function")
121
+ throw receiptRefusal(`sessionStoreContract: make must be a function that returns a fresh SessionStore, got ${typeof make}`);
122
+ if (typeof hooks !== "object" || hooks === null)
123
+ throw receiptRefusal(`sessionStoreContract: hooks must be a ${HOOK_WORDS.subject} record, got ${hooks === null ? "null" : typeof hooks}`);
124
+ const got = receiveOwnProperties(hooks, HOOK_WORDS);
125
+ for (const name of REQUIRED_HOOKS) {
126
+ const hook = readMember(got, name);
127
+ if (typeof hook !== "function")
128
+ throw receiptRefusal(`${HOOK_WORDS.subject}.${name} is required and must be a function, got ${typeof hook}`);
129
+ }
130
+ for (const name of OPTIONAL_HOOKS) {
131
+ const hook = readMember(got, name);
132
+ if (hook !== undefined && typeof hook !== "function")
133
+ throw receiptRefusal(`${HOOK_WORDS.subject}.${name} must be a function when present, got ${typeof hook}`);
134
+ }
135
+ return got;
136
+ }
137
+ export function sessionStoreContract(make, hooks) {
138
+ return runSessionStoreContract(make, receiveHooks(make, hooks));
139
+ }
140
+ async function runSessionStoreContract(make, hooks) {
141
+ const { run: runRaw, settle } = beginContract(hooks.runAssertion);
142
+ const report = (member, status) => hooks.onOptionalMember?.({ member, status });
143
+ const run = (name, fn) => runRaw(name, async () => {
144
+ const store = make();
145
+ const extra = [];
146
+ const open = async (s) => {
147
+ const opened = await s;
148
+ extra.push(opened);
149
+ return opened;
150
+ };
151
+ try {
152
+ await fn(store, open);
153
+ }
154
+ finally {
155
+ for (const s of extra.reverse())
156
+ await s.dispose();
157
+ await store.dispose();
158
+ }
159
+ });
160
+ const FACE_PRESENT = {
161
+ requireExisting: honorsRequireExisting,
162
+ forget: (s) => typeof s.forget === "function",
163
+ fork: hasSessionFork,
164
+ list: (s) => typeof s.list === "function",
165
+ noteTaskRun: (s) => typeof s.noteTaskRun === "function",
166
+ listPlaced: (s) => declaresPlacements(s) && typeof s.listPlaced === "function",
167
+ placements: declaresPlacements,
168
+ ownership: hasOwnership,
169
+ incarnationOf: (s) => typeof s.incarnationOf === "function",
170
+ replica: () => hooks.replica !== undefined,
171
+ };
172
+ const faceRun = (name, needs, fn) => run(name, async (store, open) => {
173
+ const missing = [];
174
+ for (const m of needs)
175
+ if (!(await FACE_PRESENT[m](store)))
176
+ missing.push(m);
177
+ if (missing.length > 0) {
178
+ for (const m of missing)
179
+ report(m, "absent");
180
+ return;
181
+ }
182
+ await fn(store, open);
183
+ for (const m of needs)
184
+ report(m, "verified");
185
+ });
186
+ run("[acquire] acquire() with no id mints a fresh, empty session whose id is the one handed back; two calls are two sessions", async (store) => {
187
+ const first = await store.acquire();
188
+ assert.equal(typeof first.sessionId, "string");
189
+ assert.notEqual(first.sessionId, "", "a minted id names a session");
190
+ assert.equal((await first.session.getMetadata()).id, first.sessionId, "the id acquire hands back is the session's own");
191
+ assert.equal(await first.session.getStorage().getLeafId(), null, "a fresh session has no leaf");
192
+ assert.deepEqual(await entryIds(first.session), [], "a fresh session has no entries");
193
+ const second = await store.acquire();
194
+ assert.notEqual(second.sessionId, first.sessionId, "two id-less acquires are two sessions");
195
+ });
196
+ run("[acquire] acquire(id) creates on a miss and reopens on a hit — the same history, never a wipe", async (store) => {
197
+ const id = freshId();
198
+ const created = await store.acquire(id);
199
+ assert.equal(created.sessionId, id);
200
+ assert.equal((await created.session.getMetadata()).id, id, "the created session carries the id it was asked for");
201
+ await append(created.session, userEntry("e1", null));
202
+ assert.deepEqual(await entryIds((await store.acquire(id)).session), ["e1"], "a re-acquire reopens the history");
203
+ assert.deepEqual(await entryIds(await readBack(store, id)), ["e1"], "a require-existing reopen (where honored) reads the same history");
204
+ });
205
+ run("[acquire] acquire(id, {requireExisting:true}) never creates: on a missing id it rejects not_found — or, on a store that rejects the option, it is refused on every id", async (store) => {
206
+ const id = freshId();
207
+ const honored = await honorsRequireExisting(store);
208
+ if (honored) {
209
+ await rejectsWithCode(() => store.acquire(id, { requireExisting: true }), "not_found", "require-existing on a missing id");
210
+ await rejectsWithCode(() => store.acquire(id, { requireExisting: true }), "not_found", "the refused acquire must not have created the session");
211
+ }
212
+ else {
213
+ await thrownBy(() => store.acquire(id, { requireExisting: true }), "a store that rejects the option, on a missing id");
214
+ }
215
+ if (typeof store.list === "function")
216
+ assert.equal(rowOf(await store.list(), id), undefined, "the refused acquire must not surface on the session list");
217
+ if (typeof store.incarnationOf === "function")
218
+ assert.equal(await store.incarnationOf(id), undefined, "nor have a row");
219
+ report("requireExisting", honored ? "verified" : "absent");
220
+ });
221
+ run("[acquire] two concurrent acquires of one id are one session: one log, and the second write on the same parent loses the CAS", async (store) => {
222
+ const id = freshId();
223
+ const [a, b] = await Promise.all([store.acquire(id), store.acquire(id)]);
224
+ assert.equal(a.sessionId, id);
225
+ assert.equal(b.sessionId, id);
226
+ await append(a.session, userEntry("e1", null), { expectedLeafId: null });
227
+ await rejectsWithConflict(() => append(b.session, userEntry("e2", null), { expectedLeafId: null }), "a second write on the empty session through the other caller's handle");
228
+ assert.deepEqual(await entryIds((await readBack(store, id))), ["e1"], "two callers must not have created two sessions (the second wiping the first)");
229
+ });
230
+ run("[acquire] a create-capable acquire racing a require-existing acquire of the same missing id still creates (the not_found is the require-existing caller's alone)", async (store) => {
231
+ for (const order of ["require-first", "create-first"]) {
232
+ const id = freshId();
233
+ const calls = order === "require-first" ? [store.acquire(id, { requireExisting: true }), store.acquire(id)] : [store.acquire(id), store.acquire(id, { requireExisting: true })];
234
+ const settled = await Promise.allSettled(calls);
235
+ const createCapable = settled[order === "require-first" ? 1 : 0];
236
+ assert.equal(createCapable.status, "fulfilled", `${order}: the create-capable acquire must create on the miss, never inherit the require-existing caller's not_found`);
237
+ assert.equal(createCapable.value.sessionId, id);
238
+ }
239
+ });
240
+ run("[tree] a fresh session has no leaf and an empty path; appends advance the leaf and the path walks root → leaf", async (store) => {
241
+ const { session } = await store.acquire(freshId());
242
+ const storage = session.getStorage();
243
+ assert.equal(await storage.getLeafId(), null);
244
+ assert.deepEqual(await storage.getPathToRoot(null), [], "the path to no leaf is empty");
245
+ await append(session, userEntry("e1", null));
246
+ await append(session, assistantEntry("e2", "e1"));
247
+ assert.equal(await storage.getLeafId(), "e2", "an append moves the leaf onto the appended entry");
248
+ const path = await storage.getPathToRoot("e2");
249
+ assert.deepEqual(path.map((e) => e.id), ["e1", "e2"]);
250
+ assert.equal(path[0].parentId, null, "an unbounded path starts at a root");
251
+ assert.equal((await storage.getEntry("e1"))?.id, "e1");
252
+ assert.equal(await storage.getEntry("kit-never-written"), undefined, "an id the session never held reads as absent");
253
+ });
254
+ run("[tree] setLeafId moves the active leaf, a later append branches from it, and the log keeps both branches", async (store) => {
255
+ const { session } = await store.acquire(freshId());
256
+ const storage = session.getStorage();
257
+ await append(session, userEntry("e1", null));
258
+ await append(session, assistantEntry("e2", "e1"));
259
+ await storage.setLeafId("e1");
260
+ assert.equal(await storage.getLeafId(), "e1");
261
+ await append(session, userEntry("e3", "e1"));
262
+ assert.equal(await storage.getLeafId(), "e3");
263
+ assert.deepEqual(await pathIds(session, "e3"), ["e1", "e3"], "the new branch");
264
+ assert.deepEqual(await pathIds(session, "e2"), ["e1", "e2"], "the abandoned branch is still in the log");
265
+ const ids = await entryIds(session);
266
+ for (const id of ["e1", "e2", "e3"])
267
+ assert.ok(ids.includes(id), `getEntries keeps ${id}`);
268
+ assert.ok(ids.indexOf("e1") < ids.indexOf("e2") && ids.indexOf("e2") < ids.indexOf("e3"), "getEntries is oldest-first");
269
+ });
270
+ run("[tree] a leaf this session never held is refused — setLeafId and getPathToRoot answer not_found, never an empty path, and the leaf stays", async (store) => {
271
+ const { session } = await store.acquire(freshId());
272
+ const storage = session.getStorage();
273
+ await append(session, userEntry("e1", null));
274
+ await rejectsWithCode(() => storage.setLeafId("kit-never-written"), "not_found", "setLeafId onto an id the session never held");
275
+ assert.equal(await storage.getLeafId(), "e1", "a refused setLeafId leaves the leaf where it was");
276
+ await rejectsWithCode(() => storage.getPathToRoot("kit-never-written"), "not_found", "getPathToRoot from an id the session never held");
277
+ });
278
+ run("[tree] a path stops at a root or at the storage's declared floor; a parent missing above the floor reads invalid_session, never a truncated path", async (store) => {
279
+ const { session } = await store.acquire(freshId());
280
+ await append(session, userEntry("e1", null));
281
+ let accepted = true;
282
+ try {
283
+ await append(session, userEntry("orphan", "kit-absent-parent"));
284
+ }
285
+ catch {
286
+ accepted = false;
287
+ }
288
+ if (accepted) {
289
+ await rejectsWithCode(() => session.getStorage().getPathToRoot("orphan"), "invalid_session", "the path through an entry whose parent was never written");
290
+ }
291
+ const head = (await session.getStorage().getPathToRoot("e1"))[0];
292
+ assert.ok(head.parentId === null || head.id === session.getStorage().getFloorId?.(), "a path's head is a root or the declared floor");
293
+ });
294
+ run("[tree] a parent chain that loops back on itself — an entry that is its own parent, two entries that parent each other, a leaf that walks into such a pair — reads invalid_session naming an entry of the loop, and the walk ends (also for a ring of three)", async (store) => {
295
+ const { session } = await store.acquire(freshId());
296
+ await append(session, userEntry("e1", null));
297
+ const accepted = async (...entries) => {
298
+ try {
299
+ for (const e of entries)
300
+ await append(session, e);
301
+ return true;
302
+ }
303
+ catch {
304
+ return false;
305
+ }
306
+ };
307
+ const storage = session.getStorage();
308
+ if (await accepted(userEntry("kit-loop-self", "kit-loop-self"))) {
309
+ await refusedAsLoop(storage, "kit-loop-self", ["kit-loop-self"], "the path from an entry that is its own parent");
310
+ }
311
+ if (await accepted(userEntry("kit-loop-p", "kit-loop-q"), userEntry("kit-loop-q", "kit-loop-p"))) {
312
+ await refusedAsLoop(storage, "kit-loop-q", ["kit-loop-p", "kit-loop-q"], "the path from one of two entries that parent each other");
313
+ if (await accepted(userEntry("kit-loop-t", "kit-loop-p"))) {
314
+ await refusedAsLoop(storage, "kit-loop-t", ["kit-loop-p", "kit-loop-q"], "the path from a leaf outside the loop that walks into it");
315
+ }
316
+ }
317
+ if (await accepted(userEntry("kit-ring-1", "kit-ring-3"), userEntry("kit-ring-2", "kit-ring-1"), userEntry("kit-ring-3", "kit-ring-2"))) {
318
+ await refusedAsLoop(storage, "kit-ring-3", ["kit-ring-1", "kit-ring-2", "kit-ring-3"], "the path from one of three entries that parent each other in a ring");
319
+ }
320
+ assert.deepEqual(await pathIds(session, "e1"), ["e1"], "a path that does not touch the loop still reads");
321
+ });
322
+ run("[tree] the write CAS: an append or setLeafId whose expectedLeafId is stale is refused with the engine's conflict and writes nothing", async (store) => {
323
+ const { session } = await store.acquire(freshId());
324
+ const storage = session.getStorage();
325
+ await append(session, userEntry("e1", null), { expectedLeafId: null });
326
+ await rejectsWithConflict(() => append(session, userEntry("e2", null), { expectedLeafId: null }), "an append expecting an empty session after e1 landed");
327
+ await rejectsWithConflict(() => storage.setLeafId("e1", { expectedLeafId: "kit-stale-leaf" }), "a setLeafId expecting a leaf the session does not have");
328
+ assert.deepEqual(await entryIds(session), ["e1"], "a refused write leaves no entry behind");
329
+ assert.equal(await storage.getLeafId(), "e1");
330
+ await append(session, userEntry("e2", "e1"), { expectedLeafId: "e1" });
331
+ assert.equal(await storage.getLeafId(), "e2", "a current expectedLeafId writes");
332
+ });
333
+ run("[tree] two concurrent appends on one parent: exactly one lands, the other is refused with the engine's conflict", async (store) => {
334
+ const id = freshId();
335
+ const { session } = await store.acquire(id);
336
+ await append(session, userEntry("e1", null));
337
+ const settled = await Promise.allSettled([append(session, userEntry("x", "e1"), { expectedLeafId: "e1" }), append(session, userEntry("y", "e1"), { expectedLeafId: "e1" })]);
338
+ const won = settled.flatMap((s, i) => (s.status === "fulfilled" ? [i === 0 ? "x" : "y"] : []));
339
+ assert.equal(won.length, 1, "two writers on one parent: one wins, never both (a silent fork) and never neither");
340
+ const lost = settled.find((s) => s.status === "rejected");
341
+ const miss = conflictMiss(lost.reason);
342
+ assert.equal(miss, undefined, `the loser: ${miss}`);
343
+ assert.deepEqual(await pathIds((await readBack(store, id))), ["e1", won[0]], "the log holds the winner's branch");
344
+ });
345
+ faceRun("[tree] a handle held across forget and a fresh handle from the re-acquire share one CAS", ["forget"], async (store) => {
346
+ const id = freshId();
347
+ const held = (await store.acquire(id)).session;
348
+ await append(held, userEntry("e1", null));
349
+ await store.forget(id);
350
+ const fresh = (await readBack(store, id));
351
+ await append(held, userEntry("x", "e1"), { expectedLeafId: "e1" });
352
+ await rejectsWithConflict(() => append(fresh, userEntry("y", "e1"), { expectedLeafId: "e1" }), "the fresh handle writing on the parent the held handle already advanced");
353
+ assert.deepEqual(await pathIds((await readBack(store, id))), ["e1", "x"]);
354
+ });
355
+ faceRun("[tree] a write through one replica and a write through another on the same parent: one lands, the other conflicts", ["replica"], async (store, open) => {
356
+ const id = freshId();
357
+ const other = await open(hooks.replica(store));
358
+ const here = (await store.acquire(id)).session;
359
+ await append(here, userEntry("e1", null));
360
+ const there = (await readBack(other, id));
361
+ await append(here, userEntry("x", "e1"), { expectedLeafId: "e1" });
362
+ await rejectsWithConflict(() => append(there, userEntry("y", "e1"), { expectedLeafId: "e1" }), "the second replica writing on the parent the first replica already advanced");
363
+ assert.deepEqual(await pathIds((await readBack(other, id))), ["e1", "x"], "the replica reads the winner's branch");
364
+ });
365
+ run("[rows] a session lives in the rows, not in an instance: entries, a branch switch and the leaf read back through require-existing on a reopened store", async (store, open) => {
366
+ const id = freshId();
367
+ const { session } = await store.acquire(id);
368
+ await append(session, userEntry("e1", null));
369
+ await append(session, assistantEntry("e2", "e1"));
370
+ await session.getStorage().setLeafId("e1");
371
+ await append(session, userEntry("e3", "e1"));
372
+ const before = await entryIds(session);
373
+ const second = await open(hooks.reopen(store));
374
+ const woken = (await readBack(second, id));
375
+ assert.equal(await woken.getStorage().getLeafId(), "e3", "the leaf a branch switch set survives the reopen");
376
+ assert.deepEqual(await pathIds(woken), ["e1", "e3"]);
377
+ const after = await entryIds(woken);
378
+ for (const e of ["e1", "e2", "e3"])
379
+ assert.ok(after.includes(e), `the reopened log keeps ${e}`);
380
+ assert.deepEqual(after.filter((e) => before.includes(e)), before.filter((e) => after.includes(e)), "the reopened log keeps the written order");
381
+ });
382
+ run("[rows] a leaf switch persists by itself: setLeafId onto an earlier entry, then onto null, each reads back on a reopened store with nothing appended after it", async (store, open) => {
383
+ const id = freshId();
384
+ const { session } = await store.acquire(id);
385
+ await append(session, userEntry("e1", null));
386
+ await append(session, assistantEntry("e2", "e1"));
387
+ await session.getStorage().setLeafId("e1");
388
+ const second = await open(hooks.reopen(store));
389
+ const woken = await readBack(second, id);
390
+ assert.equal(await woken.getStorage().getLeafId(), "e1", "setLeafId records the active leaf in the rows — a rewind must survive the instance");
391
+ assert.deepEqual(await pathIds(woken), ["e1"]);
392
+ await woken.getStorage().setLeafId(null);
393
+ const third = await open(hooks.reopen(second));
394
+ assert.equal(await (await readBack(third, id)).getStorage().getLeafId(), null, "a switch to no leaf persists too");
395
+ });
396
+ run("[rows] a reopened session builds the context the writer saw; its path is a suffix of the written one that starts at a root or at the declared floor", async (store, open) => {
397
+ const id = freshId();
398
+ const { session } = await store.acquire(id);
399
+ await append(session, userEntry("u1", null));
400
+ await append(session, assistantEntry("a1", "u1"));
401
+ await append(session, userEntry("u2", "a1"));
402
+ await append(session, assistantEntry("a2", "u2"));
403
+ await append(session, compactionEntry("c1", "a2", "u2"));
404
+ await append(session, userEntry("u3", "c1"));
405
+ const written = await pathIds(session);
406
+ const context = await session.buildContext();
407
+ const second = await open(hooks.reopen(store));
408
+ const woken = (await readBack(second, id));
409
+ const path = await woken.getStorage().getPathToRoot(await woken.getStorage().getLeafId());
410
+ const floor = woken.getStorage().getFloorId?.() ?? null;
411
+ assert.ok(path[0].parentId === null || path[0].id === floor, "a woken path's head is a root or the floor the storage declares — a headless window it does not declare is a corrupt log");
412
+ assert.deepEqual(path.map((e) => e.id), written.slice(written.length - path.length), "the woken path is the written path, or its tail from the floor");
413
+ assert.deepEqual(await woken.buildContext(), context, "waking from the rows (bounded or whole) rebuilds the same context");
414
+ });
415
+ faceRun("[rows] forget drops only the cached view: the history survives and the next acquire reopens it", ["forget"], async (store) => {
416
+ const id = freshId();
417
+ await append((await store.acquire(id)).session, userEntry("e1", null));
418
+ await store.forget(id);
419
+ assert.deepEqual(await entryIds((await readBack(store, id))), ["e1"], "forget must never delete history");
420
+ });
421
+ faceRun("[fork] fork copies the committed history into a new id stamped with its source, and the two logs are independent", ["fork"], async (store) => {
422
+ const src = freshId();
423
+ const source = (await store.acquire(src)).session;
424
+ await append(source, userEntry("e1", null));
425
+ await append(source, assistantEntry("e2", "e1"));
426
+ const forkedId = await store.fork(src);
427
+ assert.ok(forkedId !== null && forkedId !== src, "a fork of an existing session is a new id");
428
+ const forked = (await readBack(store, forkedId));
429
+ assert.deepEqual(await pathIds(forked), ["e1", "e2"], "the fork carries the source's committed history");
430
+ assert.equal((await forked.getMetadata()).forkedFrom, src, "the fork names its source");
431
+ await append(forked, userEntry("f3", "e2"));
432
+ await append(source, userEntry("s3", "e2"));
433
+ assert.deepEqual(await pathIds((await readBack(store, src))), ["e1", "e2", "s3"], "writes to the fork never reach the source");
434
+ assert.deepEqual(await pathIds((await readBack(store, forkedId))), ["e1", "e2", "f3"], "writes to the source never reach the fork");
435
+ });
436
+ faceRun("[fork] fork of a missing source answers null and creates nothing", ["fork"], async (store) => {
437
+ const missing = freshId();
438
+ assert.equal(await store.fork(missing), null);
439
+ if (await honorsRequireExisting(store))
440
+ await rejectsWithCode(() => store.acquire(missing, { requireExisting: true }), "not_found", "a fork of a missing source must not create it");
441
+ if (typeof store.incarnationOf === "function")
442
+ assert.equal(await store.incarnationOf(missing), undefined, "a fork of a missing source must not create it");
443
+ });
444
+ faceRun("[fork] a fork never inherits an in-flight assistant turn: a leaf turn whose tool call has no result yet (with or without sibling results already in) is left out", ["fork"], async (store) => {
445
+ const parallel = freshId();
446
+ const p = (await store.acquire(parallel)).session;
447
+ await append(p, userEntry("u1", null));
448
+ await append(p, assistantEntry("a1", "u1", "call-1"));
449
+ const pf = await store.fork(parallel);
450
+ assert.deepEqual(await pathIds((await readBack(store, pf))), ["u1"], "a leaf assistant turn with an unanswered call is in flight");
451
+ const sequential = freshId();
452
+ const q = (await store.acquire(sequential)).session;
453
+ await append(q, userEntry("u1", null));
454
+ await append(q, assistantEntry("a1", "u1", "call-1"));
455
+ await append(q, toolResultEntry("r1", "a1", "call-1"));
456
+ const qf = await store.fork(sequential);
457
+ assert.deepEqual(await pathIds((await readBack(store, qf))), ["u1"], "a sibling call's committed result does not settle the turn");
458
+ const settled = freshId();
459
+ const s = (await store.acquire(settled)).session;
460
+ await append(s, userEntry("u1", null));
461
+ await append(s, assistantEntry("a1", "u1"));
462
+ const sf = await store.fork(settled);
463
+ assert.deepEqual(await pathIds((await readBack(store, sf))), ["u1", "a1"], "a settled text turn forks whole");
464
+ });
465
+ faceRun("[list] list projects each live session once, newest-first by lastActiveAt, and a touch counts as activity", ["list"], async (store) => {
466
+ const [a, b, c] = [freshId(), freshId(), freshId()];
467
+ await store.acquire(a);
468
+ await tick();
469
+ await store.acquire(b);
470
+ await tick();
471
+ await store.acquire(c);
472
+ let rows = await store.list();
473
+ assertNewestFirst(rows);
474
+ for (const id of [a, b, c])
475
+ assert.equal(rows.filter((r) => r.sessionId === id).length, 1, `session ${id} is listed exactly once`);
476
+ assertActivityOrder(rows, [c, b, a], "acquire is activity");
477
+ for (const r of rows)
478
+ assert.ok(r.createdAt === undefined || (typeof r.createdAt === "string" && r.createdAt !== ""), "createdAt is a real timestamp or absent, never an empty placeholder");
479
+ const before = rowOf(rows, a).lastActiveAt;
480
+ await tick();
481
+ await store.touch(a);
482
+ rows = await store.list();
483
+ assertNewestFirst(rows);
484
+ assert.ok(rowOf(rows, a).lastActiveAt >= before, "a touch never moves a session's activity back");
485
+ assertActivityOrder(rows, [a, c, b], "a touch makes a session the most recently active");
486
+ });
487
+ faceRun("[list] a fork's summary names its source; an unforked session's names none", ["list", "fork"], async (store) => {
488
+ const src = freshId();
489
+ await append((await store.acquire(src)).session, userEntry("e1", null));
490
+ const forkedId = await store.fork(src);
491
+ const rows = await store.list();
492
+ assert.equal(rowOf(rows, forkedId)?.forkedFrom, src, "the fork's row carries its lineage");
493
+ assert.equal(rowOf(rows, src)?.forkedFrom, undefined, "the source is no fork");
494
+ });
495
+ faceRun("[list] noteTaskRun lands on the summary and counts as activity; a call without a run id clears lastTaskRunId; an unknown id never throws", ["noteTaskRun"], async (store) => {
496
+ await store.noteTaskRun(freshId(), "kit-task", "kit-run");
497
+ if (typeof store.list !== "function")
498
+ return;
499
+ const [a, b] = [freshId(), freshId()];
500
+ await store.acquire(a);
501
+ await tick();
502
+ await store.acquire(b);
503
+ await tick();
504
+ await store.noteTaskRun(a, "task-1", "run-1");
505
+ let rows = await store.list();
506
+ assert.equal(rowOf(rows, a)?.lastTaskId, "task-1");
507
+ assert.equal(rowOf(rows, a)?.lastTaskRunId, "run-1");
508
+ assertActivityOrder(rows, [a, b], "a run is activity: the session that just ran is the most recently active");
509
+ await store.noteTaskRun(a, "task-2");
510
+ rows = await store.list();
511
+ assert.equal(rowOf(rows, a)?.lastTaskId, "task-2");
512
+ assert.equal(rowOf(rows, a)?.lastTaskRunId, undefined, "the two seats describe one run: a newer task with no run id must not keep the older run's id");
513
+ });
514
+ faceRun("[placements] a placed session stays off the host list — before and after a restart, also when read back with require-existing", ["placements"], async (store, open) => {
515
+ const placed = freshId();
516
+ const plain = freshId();
517
+ await store.acquire(placed, { placement: placementFor() });
518
+ await store.acquire(plain);
519
+ if (typeof store.list !== "function")
520
+ return;
521
+ let rows = await store.list();
522
+ assert.equal(rowOf(rows, placed), undefined, "obligation 2: a placed session is not a host session");
523
+ assert.ok(rowOf(rows, plain) !== undefined, "an ordinary session is listed");
524
+ const second = await open(hooks.reopen(store));
525
+ await second.acquire(placed, { requireExisting: true });
526
+ await readBack(second, plain);
527
+ rows = await second.list();
528
+ assert.equal(rowOf(rows, placed), undefined, "obligation 2 is restart-stable, read-back included");
529
+ });
530
+ faceRun("[placements] a claim-form acquire of an existing placed id is refused placement_refused — on a cache hit, after forget, and after a restart", ["placements"], async (store, open) => {
531
+ const id = freshId();
532
+ await store.acquire(id, { placement: placementFor() });
533
+ await rejectsWithCode(() => store.acquire(id), "placement_refused", "obligation 6 on the instance that created it");
534
+ if (typeof store.forget === "function") {
535
+ await store.forget(id);
536
+ await rejectsWithCode(() => store.acquire(id), "placement_refused", "obligation 6 after the cached view was dropped");
537
+ }
538
+ const second = await open(hooks.reopen(store));
539
+ await rejectsWithCode(() => second.acquire(id), "placement_refused", "obligation 6 after a restart");
540
+ });
541
+ faceRun("[placements] require-existing re-opens a placed session — with no placement, its own, or a foreign one — on the same history, and rewrites nothing", ["placements"], async (store) => {
542
+ const id = freshId();
543
+ const own = placementFor();
544
+ await append((await store.acquire(id, { placement: own })).session, userEntry("e1", null));
545
+ assert.deepEqual(await entryIds((await store.acquire(id, { requireExisting: true })).session), ["e1"], "obligation 5: the read form");
546
+ assert.deepEqual(await entryIds((await store.acquire(id, { requireExisting: true, placement: own })).session), ["e1"]);
547
+ assert.deepEqual(await entryIds((await store.acquire(id, { requireExisting: true, placement: placementFor() })).session), ["e1"], "a foreign placement on the read form attaches to the same session");
548
+ await store.acquire(id, { placement: own });
549
+ await rejectsWithCode(() => store.acquire(id), "placement_refused", "and the session is still placed");
550
+ });
551
+ faceRun("[placements] release of a placed id is a real deletion, whatever the evict policy — not_found here and after a restart", ["placements"], async (store, open) => {
552
+ const id = freshId();
553
+ await append((await store.acquire(id, { placement: placementFor() })).session, userEntry("e1", null));
554
+ await store.release(id);
555
+ await rejectsWithCode(() => store.acquire(id, { requireExisting: true }), "not_found", "obligation 4: the released transcript is gone");
556
+ const second = await open(hooks.reopen(store));
557
+ await rejectsWithCode(() => second.acquire(id, { requireExisting: true }), "not_found", "obligation 4 holds on the durable rows, not only in the cache");
558
+ });
559
+ faceRun("[placements] an acquire carrying a placement onto an existing ordinary id, or onto one placed under another tuple, is refused placement_refused; the same tuple re-acquires", ["placements"], async (store, open) => {
560
+ const ordinary = freshId();
561
+ await store.acquire(ordinary);
562
+ await rejectsWithCode(() => store.acquire(ordinary, { placement: placementFor() }), "placement_refused", "a placement onto a session that exists outside the partition (cache hit)");
563
+ if (typeof store.forget === "function") {
564
+ await store.forget(ordinary);
565
+ await rejectsWithCode(() => store.acquire(ordinary, { placement: placementFor() }), "placement_refused", "the same after the cached view was dropped");
566
+ }
567
+ const tuple = placementFor();
568
+ const placed = freshId();
569
+ await store.acquire(placed, { placement: tuple });
570
+ await rejectsWithCode(() => store.acquire(placed, { placement: { ...tuple, handle: `${tuple.handle}-other` } }), "placement_refused", "a placement whose handle differs from the persisted one");
571
+ await rejectsWithCode(() => store.acquire(placed, { placement: { ...tuple, scope: `${tuple.scope}-other` } }), "placement_refused", "a placement whose scope differs from the persisted one");
572
+ await store.acquire(placed, { placement: tuple });
573
+ const second = await open(hooks.reopen(store));
574
+ await rejectsWithCode(() => second.acquire(ordinary, { placement: placementFor() }), "placement_refused", "the same after a restart");
575
+ await second.acquire(placed, { placement: tuple });
576
+ });
577
+ faceRun("[placements] a placed creation racing a claim-form acquire of the same id: exactly one of them gets the id, and what it got is what later reads see", ["placements"], async (store) => {
578
+ const id = freshId();
579
+ const tuple = placementFor();
580
+ const [placedCall, claimCall] = await Promise.allSettled([store.acquire(id, { placement: tuple }), store.acquire(id)]);
581
+ const winners = [placedCall, claimCall].filter((s) => s.status === "fulfilled").length;
582
+ assert.equal(winners, 1, "the placed creator and the claimant can never both attach (a squat or a claim of a transcript), nor both be refused");
583
+ if (placedCall.status === "fulfilled") {
584
+ await rejectsWithCode(() => store.acquire(id), "placement_refused", "the placed creator won: the id is a transcript");
585
+ }
586
+ else {
587
+ assert.equal(readMember(placedCall.reason, "code"), "placement_refused");
588
+ await rejectsWithCode(() => store.acquire(id, { placement: tuple }), "placement_refused", "the claimant won: the id is an ordinary session");
589
+ }
590
+ });
591
+ run("[placements] a store that does not declare placements still creates the session for an acquire carrying one (ignoring it is legal, refusing the creation is not)", async (store) => {
592
+ if (declaresPlacements(store))
593
+ return;
594
+ const id = freshId();
595
+ const acquired = await store.acquire(id, { placement: placementFor() });
596
+ assert.equal(acquired.sessionId, id);
597
+ await append(acquired.session, userEntry("e1", null));
598
+ assert.deepEqual(await entryIds((await readBack(store, id))), ["e1"]);
599
+ });
600
+ faceRun("[placements] listPlaced returns each placed session with its full (scope, handle) tuple or the honest tupleIncomplete, filters by scope and age, and never lists an ordinary session", ["listPlaced"], async (store) => {
601
+ const scope = `kit-${uuidv7().slice(-12)}`;
602
+ const full = placementFor(scope);
603
+ const [complete, incomplete, ordinary, elsewhere] = [freshId(), freshId(), freshId(), freshId()];
604
+ await store.acquire(complete, { placement: full });
605
+ await store.acquire(incomplete, { placement: placementFor(scope, null) });
606
+ await store.acquire(ordinary);
607
+ await store.acquire(elsewhere, { placement: placementFor() });
608
+ const rows = await store.listPlaced("subagent");
609
+ const row = (id) => rows.find((r) => r.sessionId === id);
610
+ const c = row(complete);
611
+ assert.ok(c !== undefined && !("tupleIncomplete" in c), "a placed session with both join halves is a full row");
612
+ assert.equal(c.scope, full.scope);
613
+ assert.equal(c.handle, full.handle);
614
+ assert.equal(typeof c.placedAt, "number");
615
+ const i = row(incomplete);
616
+ assert.ok(i !== undefined && "tupleIncomplete" in i && i.tupleIncomplete === true, "a placed session missing a join half is the tupleIncomplete row, never a guessed tuple");
617
+ assert.equal(row(ordinary), undefined, "an ordinary session is not in the placed partition");
618
+ assert.ok(row(elsewhere) !== undefined);
619
+ const scoped = (await store.listPlaced("subagent", { scope })).map((r) => r.sessionId);
620
+ assert.ok(scoped.includes(complete) && scoped.includes(incomplete) && !scoped.includes(elsewhere), "scope narrows to one tenant");
621
+ assert.ok(!(await store.listPlaced("subagent", { olderThanMs: 60 * 60 * 1000 })).some((r) => r.sessionId === complete), "a session placed just now is not an hour old");
622
+ });
623
+ faceRun("[ownership] ownerOf answers three states that never collapse: an id never registered ⇒ undefined, anonymous ⇒ null, owned ⇒ the owner", ["ownership"], async (store) => {
624
+ const [unknown, owned, anonymous] = [freshId(), freshId(), freshId()];
625
+ assert.equal(await store.ownerOf(unknown), undefined, "no ownership row ⇒ undefined, never null");
626
+ await store.register(owned, "kit-alice");
627
+ await store.register(anonymous, null);
628
+ assert.equal(await store.ownerOf(owned), "kit-alice");
629
+ assert.equal(await store.ownerOf(anonymous), null, "an anonymous row ⇒ null, never undefined");
630
+ });
631
+ faceRun("[ownership] register is first-write-wins: later calls neither overwrite nor throw, whatever they carry", ["ownership"], async (store) => {
632
+ const [owned, anonymous] = [freshId(), freshId()];
633
+ await store.register(owned, "kit-alice");
634
+ await store.register(owned, "kit-bob");
635
+ await store.register(owned, null);
636
+ await registerUntyped(store, owned, undefined);
637
+ assert.equal(await store.ownerOf(owned), "kit-alice");
638
+ await store.register(anonymous, null);
639
+ await store.register(anonymous, "kit-bob");
640
+ assert.equal(await store.ownerOf(anonymous), null, "an anonymous row is not claimable by a later register");
641
+ });
642
+ faceRun("[ownership] a FIRST register whose owner is neither a string nor null is refused loudly and leaves no row behind", ["ownership"], async (store) => {
643
+ const id = freshId();
644
+ await thrownBy(() => registerUntyped(store, id, undefined), "a first register with owner undefined");
645
+ await thrownBy(() => registerUntyped(store, id, 42), "a first register with a numeric owner");
646
+ assert.equal(await store.ownerOf(id), undefined, "the refused writes stored nothing");
647
+ await store.register(id, "kit-carol");
648
+ assert.equal(await store.ownerOf(id), "kit-carol", "the id is still ownable — no row that reads as 'no row' was minted");
649
+ });
650
+ faceRun("[ownership] two concurrent first registers: each caller's own re-read sees the same winner, one of the two", ["ownership"], async (store) => {
651
+ const id = freshId();
652
+ const caller = async (owner) => {
653
+ await store.register(id, owner);
654
+ return store.ownerOf(id);
655
+ };
656
+ const [x, y] = await Promise.all([caller("kit-x"), caller("kit-y")]);
657
+ assert.equal(x, y, "the loser must see the winner on its own re-read, or both callers attach to one session");
658
+ assert.ok(x === "kit-x" || x === "kit-y");
659
+ assert.equal(await store.ownerOf(id), x, "and the row keeps that winner");
660
+ });
661
+ faceRun("[ownership] the ownership row outlives the cached view: a registered session that is forgotten still reads its owner", ["ownership", "forget"], async (store) => {
662
+ const id = freshId();
663
+ await store.register(id, "kit-alice");
664
+ await append((await store.acquire(id)).session, userEntry("e1", null));
665
+ await store.forget(id);
666
+ assert.equal(await store.ownerOf(id), "kit-alice", "a live session reading undefined would be re-claimable by anyone");
667
+ });
668
+ faceRun("[ownership] a fork's ownership agrees with its source: no owner or null ⇒ the source's (an owned copy is never downgraded), an explicit owner ⇒ that owner, an anonymous source ⇒ anonymous", ["ownership", "fork"], async (store) => {
669
+ const owned = freshId();
670
+ await store.register(owned, "kit-alice");
671
+ await append((await store.acquire(owned)).session, userEntry("e1", null));
672
+ assert.equal(await store.ownerOf((await store.fork(owned))), "kit-alice", "no owner argument ⇒ the source's owner");
673
+ assert.equal(await store.ownerOf((await store.fork(owned, null))), "kit-alice", "an explicit null never downgrades a copy of an owned history to anonymous");
674
+ assert.equal(await store.ownerOf((await store.fork(owned, "kit-bob"))), "kit-bob", "an explicit owner scopes the fork");
675
+ const anonymous = freshId();
676
+ await store.register(anonymous, null);
677
+ await append((await store.acquire(anonymous)).session, userEntry("e1", null));
678
+ assert.equal(await store.ownerOf((await store.fork(anonymous))), null);
679
+ assert.equal(await store.ownerOf((await store.fork(anonymous, null))), null);
680
+ });
681
+ registerSessionIncarnationCases((name, fn) => faceRun(`[incarnation] ${name}`, ["incarnationOf", "requireExisting"], (store) => fn(store)), hooks);
682
+ await settle();
683
+ }