@sema-agent/core 2.3.0 → 2.5.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 (83) hide show
  1. package/dist/agents/send-message-tool.d.ts +4 -0
  2. package/dist/agents/send-message-tool.js +37 -24
  3. package/dist/agents/subagent.js +279 -128
  4. package/dist/brain/errors.d.ts +1 -0
  5. package/dist/brain/errors.js +14 -0
  6. package/dist/brain/stream-engine.js +3 -3
  7. package/dist/core/auto-compaction.d.ts +2 -0
  8. package/dist/core/auto-compaction.js +2 -1
  9. package/dist/core/context-edit.js +2 -1
  10. package/dist/core/mcp.js +4 -1
  11. package/dist/core/runner/prepare-task.d.ts +5 -0
  12. package/dist/core/runner/prepare-task.js +41 -2
  13. package/dist/core/runner/runtask.js +39 -9
  14. package/dist/core/runner/tool-disclosure.d.ts +1 -0
  15. package/dist/core/runner/tool-disclosure.js +16 -5
  16. package/dist/core/runner/tool-output-projection.js +2 -1
  17. package/dist/core/session-reconcile.d.ts +1 -0
  18. package/dist/core/session-reconcile.js +40 -20
  19. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +37 -0
  20. package/dist/core/store-contracts/checkpoint-store-contract.js +195 -0
  21. package/dist/core/store-contracts/contract-harness.d.ts +6 -0
  22. package/dist/core/store-contracts/contract-harness.js +16 -0
  23. package/dist/core/store-contracts/contract-kit-version.d.ts +1 -0
  24. package/dist/core/store-contracts/contract-kit-version.js +2 -0
  25. package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +3 -0
  26. package/dist/core/store-contracts/file-snapshot-store-contract.js +126 -0
  27. package/dist/core/store-contracts/mailbox-store-contract.d.ts +6 -0
  28. package/dist/core/store-contracts/mailbox-store-contract.js +193 -0
  29. package/dist/core/store-contracts/session-repo-contract.d.ts +3 -0
  30. package/dist/core/store-contracts/session-repo-contract.js +36 -0
  31. package/dist/core/store-contracts/tool-result-store-contract.d.ts +3 -0
  32. package/dist/core/store-contracts/tool-result-store-contract.js +35 -0
  33. package/dist/core/task-notification.d.ts +2 -0
  34. package/dist/core/task-registry-agent.d.ts +6 -0
  35. package/dist/core/task-registry-agent.js +24 -1
  36. package/dist/core/task-registry-monitor.js +6 -6
  37. package/dist/core/task-registry-shared.d.ts +9 -2
  38. package/dist/core/task-registry-shared.js +1 -1
  39. package/dist/core/task-registry.d.ts +8 -0
  40. package/dist/core/task-registry.js +59 -4
  41. package/dist/core/tool-result-store.d.ts +3 -2
  42. package/dist/core/tool-result-store.js +12 -4
  43. package/dist/core/trace.d.ts +7 -0
  44. package/dist/core/types.d.ts +10 -3
  45. package/dist/engine/compaction/compaction.d.ts +5 -0
  46. package/dist/engine/compaction/compaction.js +68 -2
  47. package/dist/engine/compaction/utils.d.ts +6 -0
  48. package/dist/engine/compaction/utils.js +53 -3
  49. package/dist/engine/harness/messages.d.ts +1 -1
  50. package/dist/engine/harness/messages.js +11 -3
  51. package/dist/engine/loop/types.d.ts +2 -0
  52. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
  53. package/dist/engine/lsp/node-lsp-manager.js +16 -0
  54. package/dist/engine/session/import-validate.js +30 -1
  55. package/dist/engine/session/session.js +7 -5
  56. package/dist/index.d.ts +8 -0
  57. package/dist/index.js +8 -0
  58. package/dist/internal/harness.d.ts +1 -1
  59. package/dist/internal/harness.js +1 -1
  60. package/dist/orchestration/builtin-workflows.d.ts +1 -1
  61. package/dist/orchestration/builtin-workflows.js +11 -2
  62. package/dist/orchestration/workflow-governance.d.ts +6 -1
  63. package/dist/orchestration/workflow-governance.js +24 -4
  64. package/dist/orchestration/workflow-primitives.js +7 -1
  65. package/dist/orchestration/workflow-types.d.ts +1 -0
  66. package/dist/orchestration/workflow.d.ts +1 -0
  67. package/dist/orchestration/workflow.js +41 -3
  68. package/dist/tools/fs/fs-bash.d.ts +7 -1
  69. package/dist/tools/fs/fs-bash.js +59 -22
  70. package/dist/tools/fs/fs-read.js +22 -11
  71. package/dist/tools/fs/fs-search-tools.js +3 -3
  72. package/dist/tools/fs/fs-shared.d.ts +20 -7
  73. package/dist/tools/fs/fs-shared.js +17 -3
  74. package/dist/tools/fs/fs-write.js +4 -4
  75. package/dist/tools/fs/index.d.ts +2 -0
  76. package/dist/tools/fs/index.js +7 -1
  77. package/dist/tools/fs/repo-map.js +2 -2
  78. package/dist/tools/fs/safety.d.ts +10 -0
  79. package/dist/tools/fs/safety.js +15 -1
  80. package/dist/tools/monitor.js +18 -4
  81. package/dist/tools/web.js +6 -2
  82. package/dist/tools/worktree.js +46 -25
  83. package/package.json +1 -1
@@ -0,0 +1,195 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { mintCheckpointToken, } from "../checkpoint-store.js";
3
+ import { beginContract } from "./contract-harness.js";
4
+ export function createCheckpointFixture(over = {}) {
5
+ const token = over.token ?? mintCheckpointToken();
6
+ return {
7
+ token,
8
+ scope: "tenant-a",
9
+ sessionId: "sess-1",
10
+ leafId: "leaf-1",
11
+ gate: { kind: "human", reason: "approve", toolName: "Write" },
12
+ pendingAction: {
13
+ kind: "tool_approval",
14
+ toolCallId: "call-3",
15
+ toolName: "Write",
16
+ args: { path: "/x", content: "y" },
17
+ boundInputHash: "h0",
18
+ batchToolCallIds: ["call-3"],
19
+ completedCallIds: [],
20
+ },
21
+ state: {
22
+ activeTools: [],
23
+ nestedStats: { tokens: 0, turns: 0, tasks: 0, costMicroUsd: 0 },
24
+ },
25
+ status: "pending",
26
+ createdAt: 1_700_000_000_000,
27
+ ...over,
28
+ };
29
+ }
30
+ export const ALLOW = {
31
+ gate: "policy_ask",
32
+ decision: "allow",
33
+ boundCallId: "call-3",
34
+ boundInputHash: "h0",
35
+ };
36
+ export async function checkpointStoreContract(make, runAssertion) {
37
+ const { run, settle } = beginContract(runAssertion);
38
+ run("kit prerequisites: reopen/setPendingSteer/listByScope are implemented (REQUIRED by this kit)", async () => {
39
+ const probe = make();
40
+ const missing = ["reopen", "setPendingSteer", "listByScope"].filter((m) => typeof probe[m] !== "function");
41
+ assert.equal(missing.length, 0, `backend does not implement ${missing.join(", ")} — required for the CheckpointStore contract kit`);
42
+ });
43
+ run("put create-once → already_exists", async () => {
44
+ const store = make();
45
+ const cp = createCheckpointFixture();
46
+ await store.put(cp.token, cp);
47
+ await assert.rejects(store.put(cp.token, cp), (e) => e.code === "checkpoint.already_exists");
48
+ });
49
+ run("resolve is an atomic CAS: first wins, double-resume false", async () => {
50
+ const store = make();
51
+ const cp = createCheckpointFixture();
52
+ await store.put(cp.token, cp);
53
+ assert.equal(await store.resolve(cp.token, cp.scope, ALLOW), true);
54
+ assert.equal(await store.resolve(cp.token, cp.scope, ALLOW), false);
55
+ assert.equal((await store.get(cp.token)).status, "resolved");
56
+ });
57
+ run("wrong-scope resolve never wins (multi-tenant isolation)", async () => {
58
+ const store = make();
59
+ const cp = createCheckpointFixture({ scope: "tenant-a" });
60
+ await store.put(cp.token, cp);
61
+ assert.equal(await store.resolve(cp.token, "tenant-b", ALLOW), false);
62
+ assert.equal((await store.get(cp.token)).status, "pending");
63
+ });
64
+ run("resolve records the winner; reopen(env_failed) preserves it + bumps rev", async () => {
65
+ const store = make();
66
+ const cp = createCheckpointFixture();
67
+ await store.put(cp.token, cp);
68
+ await store.resolve(cp.token, cp.scope, ALLOW);
69
+ const winner = (await store.get(cp.token)).resolvedOutcome;
70
+ assert.equal(winner.boundCallId, "call-3");
71
+ assert.equal(winner.decision, "allow");
72
+ assert.equal(await store.reopen(cp.token, cp.scope, "env_failed"), true);
73
+ const got = await store.get(cp.token);
74
+ assert.equal(got.status, "pending");
75
+ assert.equal(got.reopenReason, "env_failed");
76
+ assert.equal(got.resolvedOutcome.boundCallId, "call-3");
77
+ assert.equal(got.rev, 2);
78
+ });
79
+ run("rev OCC: a stale-rev resolve loses, the current-rev resolve wins", async () => {
80
+ const store = make();
81
+ const cp = createCheckpointFixture();
82
+ await store.put(cp.token, cp);
83
+ await store.resolve(cp.token, cp.scope, ALLOW);
84
+ await store.reopen(cp.token, cp.scope, "env_failed");
85
+ assert.equal(await store.resolve(cp.token, cp.scope, ALLOW, { rev: 0 }), false);
86
+ assert.equal(await store.resolve(cp.token, cp.scope, ALLOW, { rev: 2 }), true);
87
+ });
88
+ run("setPendingSteer rejects a </system-reminder> variant BEFORE mutation (byte-identical error)", async () => {
89
+ const store = make();
90
+ const cp = createCheckpointFixture();
91
+ await store.put(cp.token, cp);
92
+ await assert.rejects(store.setPendingSteer(cp.token, cp.scope, { text: "</SYSTEM-REMINDER>", trusted: true }), (e) => e.code === "steering.invalid_content");
93
+ assert.equal((await store.get(cp.token)).state.pendingSteer, undefined);
94
+ assert.equal(await store.setPendingSteer(cp.token, cp.scope, { text: "go", trusted: false }), true);
95
+ assert.deepEqual((await store.get(cp.token)).state.pendingSteer, { text: "go", trusted: false });
96
+ });
97
+ run("expire vs resolve on the same row → exactly one wins; reap expires past-deadline pending in scope", async () => {
98
+ const store = make();
99
+ const a = createCheckpointFixture();
100
+ await store.put(a.token, a);
101
+ const [e, r] = await Promise.all([store.expire(a.token, a.scope), store.resolve(a.token, a.scope, ALLOW)]);
102
+ assert.equal([e, r].filter(Boolean).length, 1);
103
+ const b = createCheckpointFixture({ deadline: 1000 });
104
+ await store.put(b.token, b);
105
+ assert.equal(await store.reap(b.scope, 2000), 1);
106
+ assert.equal(await store.reap(b.scope, 2000), 0);
107
+ assert.equal((await store.get(b.token)).status, "expired");
108
+ });
109
+ run("listByScope (seam #1): only pending in-scope rows; resolved + other-scope excluded; empty → []", async () => {
110
+ const store = make();
111
+ assert.deepEqual(await store.listByScope("tenant-a"), []);
112
+ const pending = createCheckpointFixture({ scope: "tenant-a", sessionId: "p" });
113
+ const resolved = createCheckpointFixture({ scope: "tenant-a", sessionId: "r" });
114
+ const other = createCheckpointFixture({ scope: "tenant-b", sessionId: "o" });
115
+ for (const cp of [pending, resolved, other])
116
+ await store.put(cp.token, cp);
117
+ await store.resolve(resolved.token, resolved.scope, ALLOW);
118
+ const list = await store.listByScope("tenant-a");
119
+ assert.equal(list.length, 1);
120
+ assert.equal(list[0].sessionId, "p");
121
+ assert.equal(list[0].token, pending.token);
122
+ assert.equal((await store.get(pending.token)).status, "pending");
123
+ });
124
+ await settle();
125
+ }
126
+ const sortByToken = (a, b) => a.token.localeCompare(b.token);
127
+ export async function checkpointListByScopeSummaries(store) {
128
+ if (typeof store.listByScope !== "function") {
129
+ throw new Error("backend does not implement listByScope — required for checkpointListByScopeSummaries");
130
+ }
131
+ const escalation = createCheckpointFixture({
132
+ token: "tok-escalation",
133
+ scope: "tenant-a",
134
+ sessionId: "needs-approval",
135
+ deadline: 1_999_999_999_999,
136
+ createdAt: 1_700_000_000_000,
137
+ sourceTaskId: "needs-approval",
138
+ principal: "alice@corp",
139
+ gate: {
140
+ kind: "human",
141
+ reason: "approve",
142
+ toolName: "open_pr",
143
+ riskDescriptor: { severity: 5, axes: { egress: true, irreversible: true }, toolName: "open_pr" },
144
+ },
145
+ pendingAction: {
146
+ kind: "tool_approval",
147
+ toolCallId: "call-pr",
148
+ toolName: "open_pr",
149
+ args: { repo: "x" },
150
+ boundInputHash: "h0",
151
+ batchToolCallIds: ["call-pr"],
152
+ completedCallIds: [],
153
+ },
154
+ });
155
+ const resource = createCheckpointFixture({
156
+ token: "tok-resource",
157
+ scope: "tenant-a",
158
+ sessionId: "out-of-budget",
159
+ createdAt: 1_700_000_000_001,
160
+ gate: { kind: "resource_limit", reason: "budget" },
161
+ pendingAction: { kind: "resource_limit", reason: "budget" },
162
+ resourceLedger: { totalBudgetMicroUsd: 10_000_000, spentMicroUsd: 4_200_000, spentTokens: 9, spentTurns: 3, sliceCount: 2 },
163
+ });
164
+ const resolved = createCheckpointFixture({ token: "tok-resolved", scope: "tenant-a", sessionId: "done" });
165
+ const other = createCheckpointFixture({ token: "tok-other", scope: "tenant-b", sessionId: "x" });
166
+ const fixture = [escalation, resource, resolved, other];
167
+ for (const cp of fixture)
168
+ await store.put(cp.token, cp);
169
+ await store.resolve(resolved.token, resolved.scope, ALLOW);
170
+ return (await store.listByScope("tenant-a")).sort(sortByToken);
171
+ }
172
+ export const EXPECTED_LISTBYSCOPE_SUMMARIES = [
173
+ {
174
+ token: "tok-escalation",
175
+ sessionId: "needs-approval",
176
+ scope: "tenant-a",
177
+ gateKind: "human",
178
+ severity: 5,
179
+ deadline: 1_999_999_999_999,
180
+ createdAt: 1_700_000_000_000,
181
+ sourceTaskId: "needs-approval",
182
+ principal: "alice@corp",
183
+ toolCallId: "call-pr",
184
+ toolName: "open_pr",
185
+ toolInput: '{"repo":"x"}',
186
+ },
187
+ {
188
+ token: "tok-resource",
189
+ sessionId: "out-of-budget",
190
+ scope: "tenant-a",
191
+ gateKind: "resource_limit",
192
+ spentMicroUsd: 4_200_000,
193
+ createdAt: 1_700_000_000_001,
194
+ },
195
+ ];
@@ -0,0 +1,6 @@
1
+ export type ContractAssertionRunner = (name: string, fn: () => Promise<void>) => void | Promise<void>;
2
+ export declare function defaultSequentialRunner(_name: string, fn: () => Promise<void>): Promise<void>;
3
+ export declare function beginContract(runAssertion?: ContractAssertionRunner): {
4
+ run: (name: string, fn: () => Promise<void>) => void;
5
+ settle: () => Promise<void>;
6
+ };
@@ -0,0 +1,16 @@
1
+ export function defaultSequentialRunner(_name, fn) {
2
+ return fn();
3
+ }
4
+ export function beginContract(runAssertion = defaultSequentialRunner) {
5
+ const pending = [];
6
+ return {
7
+ run: (name, fn) => {
8
+ const result = runAssertion(name, fn);
9
+ if (result instanceof Promise)
10
+ pending.push(result);
11
+ },
12
+ settle: async () => {
13
+ await Promise.all(pending);
14
+ },
15
+ };
16
+ }
@@ -0,0 +1 @@
1
+ export declare const CONTRACT_KIT_ENGINE_VERSION: string;
@@ -0,0 +1,2 @@
1
+ import { engineVersion } from "../version.js";
2
+ export const CONTRACT_KIT_ENGINE_VERSION = engineVersion();
@@ -0,0 +1,3 @@
1
+ import type { FileSnapshotStore } from "../file-snapshot-store.js";
2
+ import { type ContractAssertionRunner } from "./contract-harness.js";
3
+ export declare function fileSnapshotStoreContract(make: () => FileSnapshotStore, runAssertion?: ContractAssertionRunner): Promise<void>;
@@ -0,0 +1,126 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { createHash } from "node:crypto";
3
+ import { beginContract } from "./contract-harness.js";
4
+ const bytes = (s) => new TextEncoder().encode(s);
5
+ const sha256 = (s) => createHash("sha256").update(bytes(s)).digest("hex");
6
+ const srcBlob = async (hash) => hash === sha256("alpha") ? bytes("alpha") : hash === sha256("beta") ? bytes("beta") : undefined;
7
+ export async function fileSnapshotStoreContract(make, runAssertion) {
8
+ const { run, settle } = beginContract(runAssertion);
9
+ run("kit prerequisites: exportManifest/getBlob/putBlob/importManifest are implemented (REQUIRED by this kit)", async () => {
10
+ const probe = make();
11
+ const missing = ["exportManifest", "getBlob", "putBlob", "importManifest"].filter((m) => typeof probe[m] !== "function");
12
+ assert.equal(missing.length, 0, `backend does not implement ${missing.join(", ")} — required for the FileSnapshotStore contract kit`);
13
+ });
14
+ run("absent everything: has=false, listKeys=[], exportManifest=null, getBlob=undefined", async () => {
15
+ const store = make();
16
+ assert.equal(await store.has("sc", "k1"), false);
17
+ assert.deepEqual(await store.listKeys("sc"), []);
18
+ assert.equal(await store.exportManifest("sc", "k1"), null);
19
+ assert.equal(await store.getBlob(sha256("alpha")), undefined);
20
+ });
21
+ run("putBlob verifies content-address integrity: mismatched bytes → read_failed, nothing stored", async () => {
22
+ const store = make();
23
+ const r = await store.putBlob(sha256("beta"), bytes("alpha"));
24
+ assert.equal(r.ok, false);
25
+ if (!r.ok)
26
+ assert.equal(r.error.code, "read_failed");
27
+ assert.equal(await store.getBlob(sha256("beta")), undefined);
28
+ });
29
+ run("putBlob ok; a repeat putBlob for the same hash is an idempotent no-op; getBlob round-trips the bytes", async () => {
30
+ const store = make();
31
+ assert.deepEqual(await store.putBlob(sha256("alpha"), bytes("alpha")), { ok: true });
32
+ assert.deepEqual(await store.putBlob(sha256("alpha"), bytes("alpha")), { ok: true });
33
+ assert.deepEqual([...(await store.getBlob(sha256("alpha")))], [...bytes("alpha")]);
34
+ });
35
+ run("importManifest commits only after every blob verifies: has=true, exportManifest round-trips", async () => {
36
+ const store = make();
37
+ const manifest = new Map([["a.txt", sha256("alpha")], ["b.txt", sha256("beta")]]);
38
+ assert.deepEqual(await store.importManifest("sc", "k1", manifest, srcBlob), { ok: true });
39
+ assert.equal(await store.has("sc", "k1"), true);
40
+ const exported = await store.exportManifest("sc", "k1");
41
+ assert.deepEqual([...exported.entries()].sort(), [...manifest.entries()].sort());
42
+ });
43
+ run("importManifest is create-once: a second import for an existing key is a no-op (ok, zero fetches, manifest unchanged)", async () => {
44
+ const store = make();
45
+ const manifest = new Map([["a.txt", sha256("alpha")]]);
46
+ assert.deepEqual(await store.importManifest("sc", "k1", manifest, srcBlob), { ok: true });
47
+ const refetched = [];
48
+ const second = await store.importManifest("sc", "k1", new Map([["z.txt", sha256("beta")]]), async (h) => {
49
+ refetched.push(h);
50
+ return srcBlob(h);
51
+ });
52
+ assert.deepEqual(second, { ok: true });
53
+ assert.deepEqual(refetched, []);
54
+ assert.deepEqual([...(await store.exportManifest("sc", "k1")).entries()], [...manifest.entries()]);
55
+ });
56
+ run("importManifest is FAIL-CLOSED: a missing or hash-mismatched source blob → read_failed, no manifest committed", async () => {
57
+ const store = make();
58
+ const manifest = new Map([["c.txt", "0".repeat(64)]]);
59
+ const miss = await store.importManifest("sc", "k2", manifest, async () => undefined);
60
+ assert.equal(miss.ok, false);
61
+ if (!miss.ok)
62
+ assert.equal(miss.error.code, "read_failed");
63
+ assert.equal(await store.has("sc", "k2"), false);
64
+ const mism = await store.importManifest("sc", "k2", manifest, async () => bytes("wrong-bytes"));
65
+ assert.equal(mism.ok, false);
66
+ if (!mism.ok)
67
+ assert.equal(mism.error.code, "read_failed");
68
+ assert.equal(await store.has("sc", "k2"), false);
69
+ });
70
+ run("RB-361 a throwing source fetch → read_failed NAMING the blob hash, byte-identical wording (since 2.2.0)", async () => {
71
+ const store = make();
72
+ const hash = sha256("x");
73
+ const r = await store.importManifest("sc", "k3", new Map([["d.txt", hash]]), () => Promise.reject(new Error("transport down")));
74
+ assert.equal(r.ok, false);
75
+ if (!r.ok) {
76
+ assert.equal(r.error.code, "read_failed");
77
+ assert.equal(r.error.message, `source blob ${hash} fetch failed: transport down`);
78
+ }
79
+ assert.equal(await store.has("sc", "k3"), false);
80
+ });
81
+ run("RB-361 a path-escaping hash is refused as UNSAFE before any fetch, byte-identical wording (since 2.2.0)", async () => {
82
+ const store = make();
83
+ const fetched = [];
84
+ const r = await store.importManifest("sc", "k4", new Map([["e.txt", "../../escape"]]), async (h) => {
85
+ fetched.push(h);
86
+ return undefined;
87
+ });
88
+ assert.equal(r.ok, false);
89
+ if (!r.ok) {
90
+ assert.equal(r.error.code, "read_failed");
91
+ assert.equal(r.error.message, `unsafe blob hash "../../escape"`);
92
+ }
93
+ assert.deepEqual(fetched, []);
94
+ assert.equal(await store.has("sc", "k4"), false);
95
+ });
96
+ run("restore of a missing key → not_found (never a throw out of the seam)", async () => {
97
+ const store = make();
98
+ const stubEnv = {};
99
+ const r = await store.restore("sc", "nope", stubEnv, "/nonexistent-root");
100
+ assert.equal(r.ok, false);
101
+ if (!r.ok)
102
+ assert.equal(r.error.code, "not_found");
103
+ });
104
+ run("listKeys reflects only committed keys and is scope-isolated", async () => {
105
+ const store = make();
106
+ await store.importManifest("sc", "k1", new Map([["a.txt", sha256("alpha")]]), srcBlob);
107
+ await store.importManifest("sc", "k2", new Map([["c.txt", "0".repeat(64)]]), async () => undefined);
108
+ assert.deepEqual(((await store.listKeys("sc")) ?? []).slice().sort(), ["k1"]);
109
+ assert.deepEqual(await store.listKeys("other"), []);
110
+ });
111
+ run("reap keeps listed keys and their blobs", async () => {
112
+ const store = make();
113
+ await store.importManifest("sc", "k1", new Map([["a.txt", sha256("alpha")]]), srcBlob);
114
+ assert.equal(await store.reap("sc", ["k1"]), 0);
115
+ assert.deepEqual(await store.listKeys("sc"), ["k1"]);
116
+ assert.deepEqual([...(await store.getBlob(sha256("alpha")))], [...bytes("alpha")]);
117
+ });
118
+ run("reap keep-nothing drops the key and GCs the now-unreferenced blobs", async () => {
119
+ const store = make();
120
+ await store.importManifest("sc", "k1", new Map([["a.txt", sha256("alpha")]]), srcBlob);
121
+ assert.equal(await store.reap("sc", []), 1);
122
+ assert.equal(await store.has("sc", "k1"), false);
123
+ assert.equal(await store.getBlob(sha256("alpha")), undefined);
124
+ });
125
+ await settle();
126
+ }
@@ -0,0 +1,6 @@
1
+ import type { MailboxStore } from "../mailbox-store.js";
2
+ import { type ContractAssertionRunner } from "./contract-harness.js";
3
+ export declare const MAILBOX_CONTRACT_SCOPE = "default";
4
+ export declare function mailboxStoreContract(mk: () => MailboxStore, runAssertion?: ContractAssertionRunner): Promise<void>;
5
+ export declare function mailboxAckOwnershipContract(mk: () => MailboxStore, runAssertion?: ContractAssertionRunner): Promise<void>;
6
+ export declare function mailboxBundledOnlyContract(mk: () => MailboxStore, runAssertion?: ContractAssertionRunner): Promise<void>;
@@ -0,0 +1,193 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { beginContract } from "./contract-harness.js";
3
+ export const MAILBOX_CONTRACT_SCOPE = "default";
4
+ const msg = (content, sentAt = 1000) => ({ content, sentAt });
5
+ async function withStores(mk, fn) {
6
+ const live = [];
7
+ const make = () => {
8
+ const s = mk();
9
+ live.push(s);
10
+ return s;
11
+ };
12
+ try {
13
+ await fn(make);
14
+ }
15
+ finally {
16
+ for (const s of live.splice(0))
17
+ s.close?.();
18
+ }
19
+ }
20
+ export async function mailboxStoreContract(mk, runAssertion) {
21
+ const { run: runRaw, settle } = beginContract(runAssertion);
22
+ const run = (name, fn) => runRaw(name, () => withStores(mk, fn));
23
+ const S = MAILBOX_CONTRACT_SCOPE;
24
+ run("append 铸单调 seq;claimLease 取全部可见消息(不删);ack 才删;活租约=单消费者", async (make) => {
25
+ const s = make();
26
+ assert.equal(await s.append(S, "a1", msg("m1")), 1);
27
+ assert.equal(await s.append(S, "a1", msg("m2")), 2);
28
+ assert.equal(await s.peekCount(S, "a1"), 2);
29
+ const lease = await s.claimLease(S, "a1", "w1", 60_000, 10_000);
30
+ assert.deepEqual(lease.messages.map((m) => m.content), ["m1", "m2"]);
31
+ assert.equal(lease.maxSeq, 2);
32
+ assert.equal(await s.claimLease(S, "a1", "w2", 60_000, 20_000), null);
33
+ assert.equal(await s.peekCount(S, "a1"), 2);
34
+ await s.ack(S, "a1", "w1", 2);
35
+ assert.equal(await s.peekCount(S, "a1"), 0);
36
+ });
37
+ run("X-1 无损:lease 过期/release 后消息按原 seq 回可见;seq 在箱的一生中永不复用", async (make) => {
38
+ const s = make();
39
+ await s.append(S, "a1", msg("m1"));
40
+ const l1 = await s.claimLease(S, "a1", "w1", 1_000, 10_000);
41
+ assert.equal(l1.maxSeq, 1);
42
+ const l2 = await s.claimLease(S, "a1", "w2", 1_000, 20_000);
43
+ assert.deepEqual(l2.messages.map((m) => m.seq), [1]);
44
+ await s.releaseLease(S, "a1", "w2");
45
+ const l3 = await s.claimLease(S, "a1", "w3", 1_000, 20_500);
46
+ assert.deepEqual(l3.messages.map((m) => m.seq), [1]);
47
+ await s.ack(S, "a1", "w3", 1);
48
+ assert.equal(await s.append(S, "a1", msg("m2")), 2);
49
+ });
50
+ run("lease 窗内 append:后到消息在下次 claim 时可见(lease 只覆盖 claim 时刻快照)", async (make) => {
51
+ const s = make();
52
+ await s.append(S, "a1", msg("m1"));
53
+ const l = await s.claimLease(S, "a1", "w1", 60_000, 10_000);
54
+ assert.equal(l.maxSeq, 1);
55
+ await s.append(S, "a1", msg("m2"));
56
+ await s.ack(S, "a1", "w1", l.maxSeq);
57
+ assert.equal(await s.peekCount(S, "a1"), 1);
58
+ const next = await s.claimLease(S, "a1", "w2", 60_000, 20_000);
59
+ assert.deepEqual(next.messages.map((m) => m.content), ["m2"]);
60
+ });
61
+ run("drop 清整箱;reap 按最新消息龄整箱老化", async (make) => {
62
+ const s = make();
63
+ await s.append(S, "a1", msg("old", 1_000));
64
+ await s.append(S, "a2", msg("fresh", 90_000));
65
+ assert.equal(await s.reap(S, 100_000, { maxAgeMs: 50_000 }), 1);
66
+ assert.equal(await s.peekCount(S, "a1"), 0);
67
+ assert.equal(await s.peekCount(S, "a2"), 1);
68
+ await s.drop(S, "a2");
69
+ assert.equal(await s.peekCount(S, "a2"), 0);
70
+ });
71
+ run("RB-86 空箱 ⇒ null:没消息就不发租约(既不假装投递,也不 fence 掉后来真正的消费者)", async (make) => {
72
+ const s = make();
73
+ assert.equal(await s.claimLease(S, "never-written", "o1", 60_000, 1_000), null, "从未写过的箱");
74
+ await s.append(S, "a1", msg("m1"));
75
+ const first = await s.claimLease(S, "a1", "o1", 60_000, 1_000);
76
+ await s.ack(S, "a1", "o1", first.maxSeq);
77
+ assert.equal(await s.peekCount(S, "a1"), 0);
78
+ assert.equal(await s.claimLease(S, "a1", "o1", 60_000, 2_000), null, "全部 ack 后箱内零可见消息");
79
+ assert.equal(await s.claimLease(S, "a2", "ghost", 60_000, 3_000), null);
80
+ await s.append(S, "a2", msg("real", 4_000));
81
+ const real = await s.claimLease(S, "a2", "o2", 60_000, 4_000);
82
+ assert.deepEqual(real?.messages.map((m) => m.content), ["real"], "空取不该留下 fence 后来者的幽灵租约");
83
+ });
84
+ run("RB-261 守恒律:一条消息在被 ack 消费之前必须至少投递过一次(maxSeq 不得覆盖未投递项)", async (make) => {
85
+ const s = make();
86
+ const appended = ["m1", "m2", "m3"];
87
+ for (const [i, c] of appended.entries())
88
+ await s.append(S, "a1", msg(c, 1_000 + i));
89
+ const delivered = new Set();
90
+ let round = 0;
91
+ while ((await s.peekCount(S, "a1")) > 0) {
92
+ round += 1;
93
+ assert.ok(round < 10, "要么有限轮内收敛,要么就是投递不动了");
94
+ const owner = `w${round}`;
95
+ const lease = await s.claimLease(S, "a1", owner, 60_000, 10_000 + round);
96
+ assert.notEqual(lease, null, "还有可见消息时 claimLease 不得返回 null");
97
+ for (const m of lease.messages)
98
+ delivered.add(m.content);
99
+ await s.ack(S, "a1", owner, lease.maxSeq);
100
+ }
101
+ assert.deepEqual([...delivered].sort(), [...appended].sort(), "每条被消费掉的消息都必须先被投递过");
102
+ });
103
+ run("releaseLease owner-fenced:非持有者的 release 是 no-op(活租约的单消费者窗口不被外人撬开)", async (make) => {
104
+ const s = make();
105
+ await s.append(S, "a1", msg("m1"));
106
+ const held = await s.claimLease(S, "a1", "owner-A", 60_000, 10_000);
107
+ assert.equal(held.messages.length, 1);
108
+ await s.releaseLease(S, "a1", "not-the-owner");
109
+ assert.equal(await s.claimLease(S, "a1", "rival", 60_000, 11_000), null, "A 的活租约必须仍然 fence 住 rival");
110
+ await s.releaseLease(S, "a1", "owner-A");
111
+ const after = await s.claimLease(S, "a1", "rival", 60_000, 12_000);
112
+ assert.deepEqual(after?.messages.map((m) => m.seq), [1], "归还后同一条按原 seq 回可见");
113
+ });
114
+ await settle();
115
+ }
116
+ export async function mailboxAckOwnershipContract(mk, runAssertion) {
117
+ const { run: runRaw, settle } = beginContract(runAssertion);
118
+ const run = (name, fn) => runRaw(name, () => withStores(mk, fn));
119
+ const S = MAILBOX_CONTRACT_SCOPE;
120
+ run("a non-holder's ack does not delete messages held under another owner's LIVE lease", async (make) => {
121
+ const s = make();
122
+ await s.append(S, "a1", { content: "m1", sentAt: 1 });
123
+ await s.append(S, "a1", { content: "m2", sentAt: 2 });
124
+ const lease = await s.claimLease(S, "a1", "consumer-A", 60_000, 10_000);
125
+ assert.equal(lease?.messages.length, 2);
126
+ await s.ack(S, "a1", "stale-actor", lease.maxSeq);
127
+ assert.equal(await s.peekCount(S, "a1"), 2, "the messages must survive — this caller never held the lease");
128
+ });
129
+ run("angle 4 — internal consistency: releaseLease and ack now apply the SAME owner fence (was the asymmetry that proved this was an oversight)", async (make) => {
130
+ const s = make();
131
+ await s.append(S, "a1", { content: "m1", sentAt: 1 });
132
+ await s.claimLease(S, "a1", "real-owner", 60_000, 10_000);
133
+ await s.releaseLease(S, "a1", "wrong-owner");
134
+ await s.ack(S, "a1", "wrong-owner", 1);
135
+ assert.equal(await s.peekCount(S, "a1"), 1, "neither operation should have touched anything");
136
+ });
137
+ run("angle 5 — ack with no lease EVER claimed is a no-op, not a silent wipe", async (make) => {
138
+ const s = make();
139
+ await s.append(S, "a1", { content: "m1", sentAt: 1 });
140
+ await s.ack(S, "a1", "nobody", Number.MAX_SAFE_INTEGER);
141
+ assert.equal(await s.peekCount(S, "a1"), 1);
142
+ });
143
+ run("the legitimate holder's own ack still works (regression check — the fix must not break the happy path)", async (make) => {
144
+ const s = make();
145
+ await s.append(S, "a1", { content: "m1", sentAt: 1 });
146
+ await s.append(S, "a1", { content: "m2", sentAt: 2 });
147
+ const lease = await s.claimLease(S, "a1", "consumer-A", 60_000, 10_000);
148
+ await s.ack(S, "a1", "consumer-A", lease.maxSeq);
149
+ assert.equal(await s.peekCount(S, "a1"), 0);
150
+ });
151
+ run("angle 3 in detail — the real loss sequence (stale owner outlives its own expired lease, a NEW owner has since taken over): the new owner's custody survives", async (make) => {
152
+ const s = make();
153
+ await s.append(S, "a1", { content: "ship-the-release", sentAt: 1 });
154
+ await s.append(S, "a1", { content: "notify-oncall", sentAt: 2 });
155
+ const leaseA = await s.claimLease(S, "a1", "consumer-A", 1_000, 1_000);
156
+ assert.equal(leaseA?.messages.length, 2);
157
+ const leaseB = await s.claimLease(S, "a1", "consumer-B", 60_000, 5_000);
158
+ assert.equal(leaseB?.messages.length, 2);
159
+ await s.ack(S, "a1", "consumer-A", leaseA.maxSeq);
160
+ assert.equal(await s.peekCount(S, "a1"), 2, "B's live custody must survive A's stale ack");
161
+ await s.ack(S, "a1", "consumer-B", leaseB.maxSeq);
162
+ assert.equal(await s.peekCount(S, "a1"), 0);
163
+ });
164
+ await settle();
165
+ }
166
+ export async function mailboxBundledOnlyContract(mk, runAssertion) {
167
+ const { run: runRaw, settle } = beginContract(runAssertion);
168
+ const run = (name, fn) => runRaw(name, () => withStores(mk, fn));
169
+ run("RB-251(黑板 [1937]):handle 含 NUL 被两个后端一致拒绝,且不得跨 (scope,handle) 串箱", async (make) => {
170
+ const s = make();
171
+ const NUL = String.fromCharCode(0);
172
+ await assert.rejects(s.append("s", `a${NUL}b`, msg("secret")));
173
+ assert.equal(await s.peekCount(`s${NUL}a`, "b"), 0, "歧义键下不得出现串箱可读");
174
+ await s.append("s", "plain", msg("ok"));
175
+ assert.equal(await s.peekCount("s", "plain"), 1);
176
+ });
177
+ run("多租户:同名 handle 在不同 scope 下是两个互不可见的箱(各自独立铸 seq)", async (make) => {
178
+ const s = make();
179
+ assert.equal(await s.append("tenant-a", "shared-handle", msg("for-a")), 1);
180
+ assert.equal(await s.append("tenant-b", "shared-handle", msg("for-b")), 1, "另一个 scope 的箱独立铸号");
181
+ assert.equal(await s.peekCount("tenant-a", "shared-handle"), 1);
182
+ const a = await s.claimLease("tenant-a", "shared-handle", "w", 60_000, 10_000);
183
+ assert.deepEqual(a.messages.map((m) => m.content), ["for-a"]);
184
+ const b = await s.claimLease("tenant-b", "shared-handle", "w", 60_000, 10_000);
185
+ assert.deepEqual(b.messages.map((m) => m.content), ["for-b"]);
186
+ await s.ack("tenant-a", "shared-handle", "w", a.maxSeq);
187
+ assert.equal(await s.peekCount("tenant-a", "shared-handle"), 0);
188
+ assert.equal(await s.peekCount("tenant-b", "shared-handle"), 1, "跨 scope 的 ack 不得越界");
189
+ await s.drop("tenant-b", "shared-handle");
190
+ assert.equal(await s.peekCount("tenant-b", "shared-handle"), 0);
191
+ });
192
+ await settle();
193
+ }
@@ -0,0 +1,3 @@
1
+ import type { SessionRepo } from "../../internal/harness-types.js";
2
+ import { type ContractAssertionRunner } from "./contract-harness.js";
3
+ export declare function sessionRepoContract(make: () => SessionRepo, runAssertion?: ContractAssertionRunner): Promise<void>;
@@ -0,0 +1,36 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { beginContract } from "./contract-harness.js";
3
+ const SESSION_ENTRY = (id, parentId) => ({
4
+ id,
5
+ parentId,
6
+ type: "message",
7
+ timestamp: "2026-07-30T00:00:00.000Z",
8
+ message: { role: "user", content: [{ type: "text", text: `t-${id}` }], timestamp: 1785340800000 },
9
+ });
10
+ export async function sessionRepoContract(make, runAssertion) {
11
+ const { run, settle } = beginContract(runAssertion);
12
+ const A = "0198fa00-0000-7000-8000-00000000000a";
13
+ run("RB-360 create({id}) on an id that ALREADY has history is an idempotent REOPEN — never a wipe", async () => {
14
+ const repo = make();
15
+ const sess = await repo.create({ id: A });
16
+ await sess.getStorage().appendEntry(SESSION_ENTRY("e1", null));
17
+ await sess.getStorage().appendEntry(SESSION_ENTRY("e2", "e1"));
18
+ const again = await repo.create({ id: A });
19
+ const entries = await again.getStorage().getEntries();
20
+ assert.deepEqual(entries.map((e) => e.id), ["e1", "e2"]);
21
+ const exported = (await repo.exportEntries?.(A)) ?? [];
22
+ assert.deepEqual(exported.map((e) => e.id), ["e1", "e2"]);
23
+ });
24
+ run("create({id}) fresh → open() sees appended entries (round-trip floor)", async () => {
25
+ const repo = make();
26
+ const sess = await repo.create({ id: A });
27
+ await sess.getStorage().appendEntry(SESSION_ENTRY("e1", null));
28
+ const opened = await repo.open({ id: A, createdAt: "" });
29
+ assert.deepEqual((await opened.getStorage().getEntries()).map((e) => e.id), ["e1"]);
30
+ });
31
+ run("open(missing) throws not_found — never a silent create", async () => {
32
+ const repo = make();
33
+ await assert.rejects(repo.open({ id: "0198fa00-0000-7000-8000-0000000000ff", createdAt: "" }), /not.?found/i);
34
+ });
35
+ await settle();
36
+ }
@@ -0,0 +1,3 @@
1
+ import type { ToolResultStore } from "../tool-result-store.js";
2
+ import { type ContractAssertionRunner } from "./contract-harness.js";
3
+ export declare function toolResultStoreContract(make: () => ToolResultStore, runAssertion?: ContractAssertionRunner): Promise<void>;
@@ -0,0 +1,35 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { beginContract } from "./contract-harness.js";
3
+ export async function toolResultStoreContract(make, runAssertion) {
4
+ const { run, settle } = beginContract(runAssertion);
5
+ run("put/get full + sliced; write-once no-op; unknown → undefined", async () => {
6
+ const store = make();
7
+ const ref = "tr_s_c";
8
+ await store.put(ref, "0123456789");
9
+ assert.deepEqual(await store.get(ref), { content: "0123456789", offset: 0, totalChars: 10 });
10
+ assert.deepEqual(await store.get(ref, { offset: 2, limit: 3 }), { content: "234", offset: 2, totalChars: 10 });
11
+ await store.put(ref, "IGNORED");
12
+ assert.equal((await store.get(ref)).content, "0123456789");
13
+ assert.equal(await store.get("tr_unknown"), undefined);
14
+ });
15
+ run("RB-266 unsafe refs are rejected at the WRITE by every backend; the read face degrades instead", async () => {
16
+ const store = make();
17
+ const NUL = String.fromCharCode(0);
18
+ const unsafe = ["", ".", "..", "a/b", "../escape", "dir\\ref", `tr_x${NUL}y`];
19
+ for (const ref of unsafe) {
20
+ await assert.rejects((async () => store.put(ref, "payload"))(), `put(${JSON.stringify(ref)}) must reject — a durable backend cannot key it`);
21
+ assert.equal(await store.get(ref), undefined, "get of an unsafe ref degrades to undefined");
22
+ }
23
+ await store.put("tr_sess-1_call.7", "ok");
24
+ assert.equal((await store.get("tr_sess-1_call.7")).content, "ok");
25
+ });
26
+ run("RB-273 a ref the contract ACCEPTS round-trips on every backend (no backend is stricter than the rule)", async () => {
27
+ const store = make();
28
+ for (const ref of ["tr_sess_call:1", "tr_sess_call%2F1", "tr_sess_call 1", "tr_sess_日本語"]) {
29
+ await store.put(ref, `payload-${ref}`);
30
+ assert.equal((await store.get(ref))?.content, `payload-${ref}`, `put(${JSON.stringify(ref)}) must round-trip here`);
31
+ }
32
+ assert.equal((await store.get("tr_sess_call:1")).content, "payload-tr_sess_call:1");
33
+ });
34
+ await settle();
35
+ }