@code-yeongyu/senpi-agent-core 2026.10.27-2 → 2026.10.30-2

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 (71) hide show
  1. package/dist/agent-loop.js +26 -7
  2. package/dist/agent.d.ts +3 -0
  3. package/dist/agent.js +2 -0
  4. package/dist/conformance/index.d.ts +42 -0
  5. package/dist/conformance/index.js +23 -0
  6. package/dist/conformance/mapping.d.ts +24 -0
  7. package/dist/conformance/mapping.js +62 -0
  8. package/dist/conformance/registry.d.ts +15 -0
  9. package/dist/conformance/registry.js +75 -0
  10. package/dist/conformance/runner.d.ts +27 -0
  11. package/dist/conformance/runner.js +23 -0
  12. package/dist/conformance/suite-hash.d.ts +12 -0
  13. package/dist/conformance/suite-hash.js +20 -0
  14. package/dist/durable/store-admission.d.ts +7 -0
  15. package/dist/durable/store-admission.js +11 -0
  16. package/dist/durable/store-memory.d.ts +15 -0
  17. package/dist/durable/store-memory.js +140 -0
  18. package/dist/durable/store.d.ts +116 -0
  19. package/dist/durable/store.js +14 -0
  20. package/dist/durable/testing/kill-store.d.ts +13 -0
  21. package/dist/durable/testing/kill-store.js +77 -0
  22. package/dist/durable/testing/kill-wake.d.ts +12 -0
  23. package/dist/durable/testing/kill-wake.js +123 -0
  24. package/dist/durable/testing/manual-timers.d.ts +8 -0
  25. package/dist/durable/testing/manual-timers.js +31 -0
  26. package/dist/durable/testing/multi-host-cases.d.ts +6 -0
  27. package/dist/durable/testing/multi-host-cases.js +32 -0
  28. package/dist/durable/testing/store-cases.d.ts +21 -0
  29. package/dist/durable/testing/store-cases.js +168 -0
  30. package/dist/durable/testing/store-harness.d.ts +23 -0
  31. package/dist/durable/testing/store-harness.js +64 -0
  32. package/dist/durable/testing/wake-cases.d.ts +6 -0
  33. package/dist/durable/testing/wake-cases.js +100 -0
  34. package/dist/durable/testing/wake-harness.d.ts +64 -0
  35. package/dist/durable/testing/wake-harness.js +127 -0
  36. package/dist/durable/testing/wake-test-loop.d.ts +13 -0
  37. package/dist/durable/testing/wake-test-loop.js +40 -0
  38. package/dist/durable/wake-host-bun-timer.d.ts +31 -0
  39. package/dist/durable/wake-host-bun-timer.js +66 -0
  40. package/dist/durable/wake-host-store-alarm.d.ts +13 -0
  41. package/dist/durable/wake-host-store-alarm.js +20 -0
  42. package/dist/durable/wake-host.d.ts +68 -0
  43. package/dist/durable/wake-host.js +70 -0
  44. package/dist/harness/utils/read-folders/index.d.ts +1 -1
  45. package/dist/harness/utils/read-folders/index.js +1 -1
  46. package/dist/harness/utils/read-folders/tree-sitter/engine.d.ts +1 -1
  47. package/dist/harness/utils/read-folders/tree-sitter/engine.js +38 -19
  48. package/dist/stream-keepalive.d.ts +28 -0
  49. package/dist/stream-keepalive.js +42 -0
  50. package/dist/testing/win32-sim.d.ts +76 -0
  51. package/dist/testing/win32-sim.js +197 -0
  52. package/dist/transport/in-process-carrier.d.ts +19 -0
  53. package/dist/transport/in-process-carrier.js +63 -0
  54. package/dist/transport/local-endpoint.d.ts +13 -0
  55. package/dist/transport/local-endpoint.js +20 -0
  56. package/dist/transport/reliable-transport.d.ts +40 -0
  57. package/dist/transport/reliable-transport.js +232 -0
  58. package/dist/transport/testing/fault-cases.d.ts +6 -0
  59. package/dist/transport/testing/fault-cases.js +30 -0
  60. package/dist/transport/testing/fault-transport.d.ts +44 -0
  61. package/dist/transport/testing/fault-transport.js +144 -0
  62. package/dist/transport/testing/multi-cases.d.ts +6 -0
  63. package/dist/transport/testing/multi-cases.js +113 -0
  64. package/dist/transport/testing/transport-cases.d.ts +6 -0
  65. package/dist/transport/testing/transport-cases.js +60 -0
  66. package/dist/transport/testing/transport-harness.d.ts +53 -0
  67. package/dist/transport/testing/transport-harness.js +127 -0
  68. package/dist/transport/transport.d.ts +60 -0
  69. package/dist/transport/transport.js +15 -0
  70. package/dist/types.d.ts +6 -0
  71. package/package.json +11 -3
@@ -0,0 +1,140 @@
1
+ import { DurableStoreError, } from "./store.js";
2
+ export function createMemoryStoreMedium() {
3
+ return { current: { epoch: 0, records: [], documents: new Map() } };
4
+ }
5
+ const copy = (value) => structuredClone(value);
6
+ const copyRecord = (record) => ({ ...record, data: copy(record.data) });
7
+ const isList = (value) => Array.isArray(value);
8
+ function sameValue(a, b) {
9
+ if (a === undefined || b === undefined || a === null || b === null)
10
+ return a === b;
11
+ if (typeof a !== "object" || typeof b !== "object")
12
+ return a === b;
13
+ if (isList(a) || isList(b)) {
14
+ return isList(a) && isList(b) && a.length === b.length && a.every((item, index) => sameValue(item, b[index]));
15
+ }
16
+ const keys = Object.keys(a);
17
+ return (keys.length === Object.keys(b).length && keys.every((key) => Object.hasOwn(b, key) && sameValue(a[key], b[key])));
18
+ }
19
+ const sameRecord = (a, b) => b !== undefined && a.seq === b.seq && a.id === b.id && a.kind === b.kind && sameValue(a.data, b.data);
20
+ function requireOwner(state, epoch) {
21
+ if (state.epoch === 0)
22
+ throw new DurableStoreError("unclaimed", "the store has no owner; claim it first");
23
+ if (epoch !== state.epoch) {
24
+ throw new DurableStoreError("stale-epoch", `epoch ${epoch} is not the owner epoch ${state.epoch}`);
25
+ }
26
+ }
27
+ function importedTail(state, snapshot) {
28
+ const ids = new Set();
29
+ const wellFormed = snapshot.head === snapshot.records.length &&
30
+ snapshot.records.every((record, index) => record.seq === index + 1 && !ids.has(record.id) && ids.add(record.id));
31
+ const extendsLog = state.records.length <= snapshot.records.length &&
32
+ state.records.every((record, index) => sameRecord(record, snapshot.records[index]));
33
+ if (!wellFormed || !extendsLog) {
34
+ throw new DurableStoreError("import-conflict", "the snapshot's log does not extend this store's log");
35
+ }
36
+ return snapshot.records.slice(state.records.length);
37
+ }
38
+ function scan(records, query) {
39
+ const matching = records.filter((record) => (query.fromSeq === undefined || record.seq >= query.fromSeq) &&
40
+ (query.toSeq === undefined || record.seq <= query.toSeq) &&
41
+ (query.kind === undefined || record.kind === query.kind));
42
+ const ordered = query.order === "desc" ? matching.reverse() : matching;
43
+ return (query.limit === undefined ? ordered : ordered.slice(0, query.limit)).map(copyRecord);
44
+ }
45
+ const documentsOf = (state, prefix) => [...state.documents]
46
+ .filter(([key]) => key.startsWith(prefix))
47
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
48
+ .map(([key, data]) => ({ key, data: copy(data) }));
49
+ /** A DurableStore over `medium`. Each operation stages its writes and publishes them in one assignment: atomic. */
50
+ export function openMemoryStore(medium, options = {}) {
51
+ const point = (at) => options.onWritePoint?.(at);
52
+ return {
53
+ readEpoch: async () => medium.current.epoch,
54
+ readHead: async () => medium.current.records.length,
55
+ async claim(expectedEpoch, nextEpoch) {
56
+ point({ op: "claim", at: "before" });
57
+ const state = medium.current;
58
+ if (!Number.isSafeInteger(nextEpoch) || nextEpoch !== expectedEpoch + 1) {
59
+ throw new DurableStoreError("invalid-claim", `a claim must raise the epoch to exactly ${expectedEpoch + 1}`);
60
+ }
61
+ if (state.epoch !== expectedEpoch) {
62
+ throw new DurableStoreError("stale-epoch", `expected epoch ${expectedEpoch}, the owner epoch is ${state.epoch}`);
63
+ }
64
+ medium.current = { ...state, epoch: nextEpoch };
65
+ point({ op: "claim", at: "durable" });
66
+ },
67
+ async commit(epoch, writes, adopt) {
68
+ point({ op: "commit", at: "before" });
69
+ const state = medium.current;
70
+ requireOwner(state, epoch);
71
+ const records = [...state.records];
72
+ const ids = new Set(records.map((record) => record.id));
73
+ const documents = new Map(state.documents);
74
+ const seqs = [];
75
+ writes.forEach((write, index) => {
76
+ point({ op: "commit", at: "write", index });
77
+ switch (write.op) {
78
+ case "append": {
79
+ if (ids.has(write.id))
80
+ throw new DurableStoreError("duplicate-record-id", `record ${write.id} exists`);
81
+ ids.add(write.id);
82
+ const seq = records.length + 1;
83
+ records.push({ seq, id: write.id, kind: write.kind, data: copy(write.data) });
84
+ seqs.push(seq);
85
+ break;
86
+ }
87
+ case "put":
88
+ documents.set(write.key, copy(write.data));
89
+ break;
90
+ case "delete":
91
+ documents.delete(write.key);
92
+ break;
93
+ }
94
+ });
95
+ medium.current = { epoch: state.epoch, records, documents };
96
+ point({ op: "commit", at: "durable" });
97
+ const result = { seqs, head: records.length };
98
+ adopt(result);
99
+ return result;
100
+ },
101
+ mintId: () => crypto.randomUUID(),
102
+ scanRecords: async (query = {}) => scan(medium.current.records, query),
103
+ getDocument: async (key) => {
104
+ const data = medium.current.documents.get(key);
105
+ return data === undefined ? undefined : copy(data);
106
+ },
107
+ scanDocuments: async (prefix) => documentsOf(medium.current, prefix),
108
+ exportSnapshot: async () => {
109
+ const state = medium.current;
110
+ return {
111
+ epoch: state.epoch,
112
+ head: state.records.length,
113
+ records: state.records.map(copyRecord),
114
+ documents: documentsOf(state, ""),
115
+ };
116
+ },
117
+ async importSnapshot(epoch, snapshot) {
118
+ point({ op: "import", at: "before" });
119
+ const state = medium.current;
120
+ requireOwner(state, epoch);
121
+ if (epoch <= snapshot.epoch) {
122
+ throw new DurableStoreError("invalid-claim", `epoch ${epoch} is not above the source epoch ${snapshot.epoch}`);
123
+ }
124
+ const records = [...state.records];
125
+ const documents = new Map();
126
+ let index = 0;
127
+ for (const record of importedTail(state, snapshot)) {
128
+ point({ op: "import", at: "write", index: index++ });
129
+ records.push(copyRecord(record));
130
+ }
131
+ for (const document of snapshot.documents) {
132
+ point({ op: "import", at: "write", index: index++ });
133
+ documents.set(document.key, copy(document.data));
134
+ }
135
+ medium.current = { epoch: state.epoch, records, documents };
136
+ point({ op: "import", at: "durable" });
137
+ },
138
+ };
139
+ }
140
+ //# sourceMappingURL=store-memory.js.map
@@ -0,0 +1,116 @@
1
+ /**
2
+ * The DurableStore contract of the durable core (packages/agent/docs/durable-core-adr.md). Every backend (memory, Bun
3
+ * SQLite, Durable Object SQLite) implements it and passes the STORE and KILL-STORE conformance cases.
4
+ *
5
+ * The store does not serialize concurrent writers: a single admission line above it (store-admission.ts) does.
6
+ */
7
+ /** A JSON value as the store persists it. */
8
+ export type StoreValue = null | boolean | number | string | readonly StoreValue[] | {
9
+ readonly [key: string]: StoreValue;
10
+ };
11
+ /** One log record. `seq` is assigned by the store: gapless from 1, in commit order. `id` is unique in the log. */
12
+ export interface StoreRecord {
13
+ readonly seq: number;
14
+ readonly id: string;
15
+ readonly kind: string;
16
+ readonly data: StoreValue;
17
+ }
18
+ export interface StoreDocument {
19
+ readonly key: string;
20
+ readonly data: StoreValue;
21
+ }
22
+ export type StoreWrite = {
23
+ readonly op: "append";
24
+ readonly id: string;
25
+ readonly kind: string;
26
+ readonly data: StoreValue;
27
+ } | {
28
+ readonly op: "put";
29
+ readonly key: string;
30
+ readonly data: StoreValue;
31
+ } | {
32
+ readonly op: "delete";
33
+ readonly key: string;
34
+ };
35
+ export interface CommitResult {
36
+ /** The sequence numbers of the batch's appends, in write order. */
37
+ readonly seqs: readonly number[];
38
+ /** The last committed sequence number after this batch (0 for an empty log). */
39
+ readonly head: number;
40
+ }
41
+ /** Everything a store holds; the unit of claim + import between hosts. */
42
+ export interface StoreSnapshot {
43
+ readonly epoch: number;
44
+ readonly head: number;
45
+ /** Every record, ascending by `seq`. */
46
+ readonly records: readonly StoreRecord[];
47
+ /** Every document, ascending by `key`. */
48
+ readonly documents: readonly StoreDocument[];
49
+ }
50
+ export interface RecordScan {
51
+ readonly fromSeq?: number;
52
+ readonly toSeq?: number;
53
+ readonly kind?: string;
54
+ readonly order?: "asc" | "desc";
55
+ readonly limit?: number;
56
+ }
57
+ export type StoreOperation = "commit" | "claim" | "import";
58
+ /**
59
+ * A point inside a durable write. `before`: nothing of the operation is durable yet. `write`: inside the atomic unit,
60
+ * before write `index` is staged. `durable`: the operation is durable, before `adopt` runs and before it resolves.
61
+ */
62
+ export type StoreWritePoint = {
63
+ readonly op: StoreOperation;
64
+ readonly at: "before";
65
+ } | {
66
+ readonly op: "commit" | "import";
67
+ readonly at: "write";
68
+ readonly index: number;
69
+ } | {
70
+ readonly op: StoreOperation;
71
+ readonly at: "durable";
72
+ };
73
+ export interface DurableStoreOptions {
74
+ /**
75
+ * Test seam for cuts and kills: called at every write point. A throw at `before` or `write` leaves nothing of the
76
+ * operation durable; a throw at `durable` leaves all of it durable and skips `adopt`.
77
+ */
78
+ readonly onWritePoint?: (point: StoreWritePoint) => void;
79
+ }
80
+ export type DurableStoreErrorCode = "unclaimed" | "stale-epoch" | "invalid-claim" | "duplicate-record-id" | "import-conflict";
81
+ export declare class DurableStoreError extends Error {
82
+ readonly code: DurableStoreErrorCode;
83
+ constructor(code: DurableStoreErrorCode, message: string);
84
+ }
85
+ export interface DurableStore {
86
+ /** The current owner epoch; 0 when the store was never claimed. */
87
+ readEpoch(): Promise<number>;
88
+ /** The last committed sequence number; 0 for an empty log. */
89
+ readHead(): Promise<number>;
90
+ /**
91
+ * Compare-and-set the owner epoch from `expectedEpoch` to `nextEpoch`, which must be exactly `expectedEpoch + 1`; any
92
+ * other `nextEpoch` (equal, lower, or skipping an epoch) is refused (`invalid-claim`) and changes nothing. A claimant
93
+ * whose expected epoch is no longer current is refused (`stale-epoch`), so an old owner can never take the store back.
94
+ */
95
+ claim(expectedEpoch: number, nextEpoch: number): Promise<void>;
96
+ /**
97
+ * Commit a batch atomically under the owner `epoch`: all of it becomes durable, or none of it. `adopt` runs exactly
98
+ * once, after the batch is durable and before the commit resolves, and never for a batch that did not become
99
+ * durable. A commit from any epoch but the current one is refused (`stale-epoch`) and leaves the store unchanged.
100
+ */
101
+ commit(epoch: number, writes: readonly StoreWrite[], adopt: (result: CommitResult) => void): Promise<CommitResult>;
102
+ /** A new ID, unique across instances, reopens and hosts. */
103
+ mintId(): string;
104
+ scanRecords(query?: RecordScan): Promise<StoreRecord[]>;
105
+ getDocument(key: string): Promise<StoreValue | undefined>;
106
+ /** Documents whose key starts with `prefix`, ascending by key. */
107
+ scanDocuments(prefix: string): Promise<StoreDocument[]>;
108
+ exportSnapshot(): Promise<StoreSnapshot>;
109
+ /**
110
+ * Import another host's snapshot under this store's current owner `epoch`, which must be above the snapshot's.
111
+ * Atomic. The store's records must be a prefix of the snapshot's: records past the store's head are imported once,
112
+ * in order, with their seqs and IDs; documents become the snapshot's. Importing the same snapshot again is a no-op.
113
+ */
114
+ importSnapshot(epoch: number, snapshot: StoreSnapshot): Promise<void>;
115
+ }
116
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The DurableStore contract of the durable core (packages/agent/docs/durable-core-adr.md). Every backend (memory, Bun
3
+ * SQLite, Durable Object SQLite) implements it and passes the STORE and KILL-STORE conformance cases.
4
+ *
5
+ * The store does not serialize concurrent writers: a single admission line above it (store-admission.ts) does.
6
+ */
7
+ export class DurableStoreError extends Error {
8
+ constructor(code, message) {
9
+ super(message);
10
+ this.name = "DurableStoreError";
11
+ this.code = code;
12
+ }
13
+ }
14
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1,13 @@
1
+ import type { CaseImplementation } from "../../conformance/runner.ts";
2
+ import type { StoreOperation } from "../store.ts";
3
+ import { type StoreCaseAdapters } from "./store-harness.ts";
4
+ /** Where a KILL case applies; a host report logs it in kill[] with the registry's expected outcome. */
5
+ export interface KillDeclaration {
6
+ readonly id: string;
7
+ readonly op: StoreOperation;
8
+ readonly at: "before" | "durable";
9
+ readonly placement: "everywhere";
10
+ }
11
+ export declare const KILL_STORE_DECLARATIONS: readonly KillDeclaration[];
12
+ export declare const KILL_STORE_CASES: ReadonlyMap<string, CaseImplementation<StoreCaseAdapters>>;
13
+ //# sourceMappingURL=kill-store.d.ts.map
@@ -0,0 +1,77 @@
1
+ import { append, claimedStore, put, stored } from "./store-cases.js";
2
+ import { adoptLog, cutOnceAt, expectCut, expectEqual, expectStoreError, } from "./store-harness.js";
3
+ const killCase = (id, op, at, body) => ({ id, op, at, placement: "everywhere", run: ({ store }) => body(store, { op, at }) });
4
+ async function seeded(adapter) {
5
+ const { medium, store } = await claimedStore(adapter);
6
+ await store.commit(1, [append("r1"), put("doc", 1)], () => { });
7
+ return { medium, baseline: await store.exportSnapshot() };
8
+ }
9
+ async function killThenReopen(adapter, medium, kill, operation) {
10
+ await expectCut(operation(await adapter.open(medium, cutOnceAt(kill))), `kill ${kill.at} ${kill.op}`);
11
+ return adapter.open(medium);
12
+ }
13
+ const content = ({ head, records, documents }) => ({ head, records, documents });
14
+ const SECOND_BATCH = [append("r2"), put("doc", 2)];
15
+ const KILL_CASES = [
16
+ killCase("KILL-STORE-01", "commit", "before", async (adapter, kill) => {
17
+ const { medium, baseline } = await seeded(adapter);
18
+ const log = adoptLog();
19
+ const restarted = await killThenReopen(adapter, medium, kill, (s) => s.commit(1, SECOND_BATCH, log.adopt));
20
+ expectEqual(await restarted.exportSnapshot(), baseline, "nothing of the batch is durable");
21
+ expectEqual(await restarted.commit(1, SECOND_BATCH, log.adopt), { seqs: [2], head: 2 }, "same IDs, next seq");
22
+ expectEqual(log.calls, ["adopt:2"], "only the committed batch adopts");
23
+ }),
24
+ killCase("KILL-STORE-02", "commit", "durable", async (adapter, kill) => {
25
+ const { medium, baseline } = await seeded(adapter);
26
+ const log = adoptLog();
27
+ const restarted = await killThenReopen(adapter, medium, kill, (s) => s.commit(1, SECOND_BATCH, log.adopt));
28
+ const expected = {
29
+ ...baseline,
30
+ head: 2,
31
+ records: [stored(1, "r1"), stored(2, "r2")],
32
+ documents: [{ key: "doc", data: 2 }],
33
+ };
34
+ expectEqual(log.calls, [], "the killed process never adopted");
35
+ expectEqual(await restarted.exportSnapshot(), expected, "the whole batch is durable with its IDs and seqs");
36
+ await expectStoreError(restarted.commit(1, SECOND_BATCH, log.adopt), "duplicate-record-id", "a retried batch");
37
+ expectEqual(await restarted.exportSnapshot(), expected, "the retry applies nothing twice");
38
+ }),
39
+ killCase("KILL-STORE-03", "claim", "before", async (adapter, kill) => {
40
+ const { medium, baseline } = await seeded(adapter);
41
+ const restarted = await killThenReopen(adapter, medium, kill, (s) => s.claim(1, 2));
42
+ expectEqual(await restarted.exportSnapshot(), baseline, "the epoch and log are unchanged");
43
+ await restarted.claim(1, 2);
44
+ expectEqual(await restarted.readEpoch(), 2, "the claim succeeds after restart");
45
+ }),
46
+ killCase("KILL-STORE-04", "claim", "durable", async (adapter, kill) => {
47
+ const { medium, baseline } = await seeded(adapter);
48
+ const restarted = await killThenReopen(adapter, medium, kill, (s) => s.claim(1, 2));
49
+ expectEqual(await restarted.exportSnapshot(), { ...baseline, epoch: 2 }, "the claim is durable; the log is intact");
50
+ await expectStoreError(restarted.claim(1, 2), "stale-epoch", "the claim cannot be taken twice");
51
+ await expectStoreError(restarted.commit(1, [append("late")], () => { }), "stale-epoch", "the old epoch is fenced");
52
+ }),
53
+ killCase("KILL-STORE-05", "import", "before", async (adapter, kill) => {
54
+ const { baseline } = await seeded(adapter);
55
+ const { medium } = await claimedStore(adapter, 2);
56
+ const restarted = await killThenReopen(adapter, medium, kill, (s) => s.importSnapshot(2, baseline));
57
+ expectEqual(await restarted.exportSnapshot(), { epoch: 2, head: 0, records: [], documents: [] }, "nothing imported");
58
+ await restarted.importSnapshot(2, baseline);
59
+ expectEqual(content(await restarted.exportSnapshot()), content(baseline), "the import succeeds after restart");
60
+ }),
61
+ killCase("KILL-STORE-06", "import", "durable", async (adapter, kill) => {
62
+ const { baseline } = await seeded(adapter);
63
+ const { medium } = await claimedStore(adapter, 2);
64
+ const restarted = await killThenReopen(adapter, medium, kill, (s) => s.importSnapshot(2, baseline));
65
+ expectEqual(content(await restarted.exportSnapshot()), content(baseline), "the whole import is durable");
66
+ await restarted.importSnapshot(2, baseline);
67
+ expectEqual(content(await restarted.exportSnapshot()), content(baseline), "re-import adds nothing twice");
68
+ }),
69
+ ];
70
+ export const KILL_STORE_DECLARATIONS = KILL_CASES.map(({ id, op, at, placement }) => ({
71
+ id,
72
+ op,
73
+ at,
74
+ placement,
75
+ }));
76
+ export const KILL_STORE_CASES = new Map(KILL_CASES.map(({ id, run }) => [id, run]));
77
+ //# sourceMappingURL=kill-store.js.map
@@ -0,0 +1,12 @@
1
+ import type { CaseImplementation } from "../../conformance/runner.ts";
2
+ import { type WakeCaseAdapters } from "./wake-harness.ts";
3
+ /** Where a KILL-WAKE case applies; a host report logs it in kill[] with the registry's expected outcome. */
4
+ export interface KillWakeDeclaration {
5
+ readonly id: string;
6
+ readonly operation: "admit" | "commit" | "update" | "fire";
7
+ readonly point: string;
8
+ readonly placement: "everywhere";
9
+ }
10
+ export declare const KILL_WAKE_DECLARATIONS: readonly KillWakeDeclaration[];
11
+ export declare const KILL_WAKE_CASES: ReadonlyMap<string, CaseImplementation<WakeCaseAdapters>>;
12
+ //# sourceMappingURL=kill-wake.d.ts.map
@@ -0,0 +1,123 @@
1
+ import { expectEqual } from "./store-harness.js";
2
+ import { createSlot, cutAlarm, cutStore, cutWake, expectWakeCut, HEARTBEAT_MS, } from "./wake-harness.js";
3
+ const killCase = (id, operation, point, run) => ({ id, operation, point, placement: "everywhere", run });
4
+ const LATER = 600_000;
5
+ const DUE = 120_000;
6
+ async function killedAdmission(adapters, cut) {
7
+ const slot = await createSlot(adapters);
8
+ const { host, loop } = await slot.instance(cut);
9
+ await expectWakeCut(host.admit(() => loop.schedule("a", slot.base + LATER)), "the killed admission");
10
+ slot.kill();
11
+ return slot;
12
+ }
13
+ /** Recovery with no request from anyone: only the armed alarm brings the session back. */
14
+ async function recoversThroughHeartbeat(adapters, slot) {
15
+ expectEqual(await slot.alarmAt(), HEARTBEAT_MS, "the pre-armed heartbeat survived the kill");
16
+ await adapters.wake.advanceTo(slot.base + HEARTBEAT_MS);
17
+ expectEqual(await slot.alarmAt(), LATER, "the heartbeat's instance armed the committed wake");
18
+ await adapters.wake.advanceTo(slot.base + LATER);
19
+ expectEqual(await slot.ran(), ["a"], "the committed task ran exactly once");
20
+ expectEqual(await slot.alarmAt(), undefined, "and the alarm is cleared");
21
+ }
22
+ async function killedFire(adapters, cut) {
23
+ const slot = await createSlot(adapters);
24
+ const { host, loop } = await slot.instance(cut);
25
+ await host.admit(() => loop.schedule("a", slot.base + DUE));
26
+ await adapters.wake.advanceTo(slot.base + DUE);
27
+ expectEqual(slot.fires(), 1, "the alarm fired into the instance that is killed");
28
+ return slot;
29
+ }
30
+ async function recoversAfterKilledFire(adapters, slot) {
31
+ expectEqual(await slot.alarmAt(), DUE + HEARTBEAT_MS, "the fire's re-arm survived the kill");
32
+ await adapters.wake.advanceTo(slot.base + DUE + HEARTBEAT_MS);
33
+ expectEqual(await slot.ran(), ["a"], "the task ran exactly once");
34
+ expectEqual(await slot.alarmAt(), undefined, "and the alarm is cleared");
35
+ await adapters.wake.advanceTo(slot.base + LATER);
36
+ expectEqual(slot.fires(), 2, "no fire beyond the recovery");
37
+ }
38
+ /**
39
+ * An admission B runs while a fire resumes other work on the same instance. The fire's updates see an idle loop (B's
40
+ * task is not committed yet), so they must not clear the heartbeat B armed; B is killed after its commit is durable,
41
+ * before its own update. The session runs at epoch 2.
42
+ */
43
+ async function admissionDuringFire(adapters) {
44
+ const fireDone = Promise.withResolvers();
45
+ let admission;
46
+ let session;
47
+ const slot = await createSlot(adapters, 2);
48
+ const first = await slot.instance({
49
+ store: cutStore("commit", "durable", 3),
50
+ onWakePoint: (point) => {
51
+ if (point.op !== "fire" || session === undefined || admission !== undefined)
52
+ return;
53
+ const { host, loop } = session;
54
+ admission = host.admit(async () => {
55
+ await fireDone.promise;
56
+ await loop.schedule("b", slot.base + LATER);
57
+ });
58
+ },
59
+ });
60
+ session = first;
61
+ await first.host.admit(() => first.loop.schedule("a", slot.base + DUE));
62
+ await adapters.wake.advanceTo(slot.base + DUE);
63
+ expectEqual(await slot.ran(), ["a"], "the fire resumed its due task");
64
+ if (admission === undefined)
65
+ throw new Error("the admission did not start during the fire");
66
+ fireDone.resolve();
67
+ await expectWakeCut(admission, "the admission killed after its commit");
68
+ slot.kill();
69
+ return slot;
70
+ }
71
+ const KILL_CASES = [
72
+ killCase("KILL-WAKE-01", "admit", "alarm write before durable", async (adapters) => {
73
+ const slot = await killedAdmission(adapters, { alarm: cutAlarm("set", "before", 2) });
74
+ expectEqual(await slot.alarmAt(), undefined, "nothing of the admission is durable");
75
+ const { host, loop } = await slot.instance();
76
+ await host.admit(() => loop.schedule("a", slot.base + LATER));
77
+ await adapters.wake.advanceTo(slot.base + LATER);
78
+ expectEqual(await slot.ran(), ["a"], "the retried admission runs once");
79
+ }),
80
+ killCase("KILL-WAKE-02", "admit", "after the arm, before work", async (adapters) => {
81
+ const slot = await killedAdmission(adapters, { onWakePoint: cutWake("admit") });
82
+ expectEqual(await slot.alarmAt(), HEARTBEAT_MS, "the arm is durable");
83
+ await adapters.wake.advanceTo(slot.base + HEARTBEAT_MS);
84
+ expectEqual(await slot.ran(), [], "an alarm without work runs nothing");
85
+ expectEqual(await slot.alarmAt(), undefined, "and ends cleared");
86
+ expectEqual(slot.fires(), 1, "one harmless fire");
87
+ }),
88
+ killCase("KILL-WAKE-03", "commit", "after the commit is durable, before the alarm update", async (adapters) => {
89
+ const slot = await killedAdmission(adapters, { store: cutStore("commit", "durable") });
90
+ await recoversThroughHeartbeat(adapters, slot);
91
+ }),
92
+ killCase("KILL-WAKE-04", "update", "eviction during the alarm write", async (adapters) => {
93
+ const slot = await killedAdmission(adapters, { alarm: cutAlarm("set", "before", 3) });
94
+ await recoversThroughHeartbeat(adapters, slot);
95
+ }),
96
+ killCase("KILL-WAKE-05", "fire", "after the re-arm, before resume", async (adapters) => {
97
+ const slot = await killedFire(adapters, { onWakePoint: cutWake("fire") });
98
+ expectEqual(await slot.ran(), [], "nothing ran before the kill");
99
+ await recoversAfterKilledFire(adapters, slot);
100
+ }),
101
+ killCase("KILL-WAKE-06", "fire", "after a resumed task's commit is durable", async (adapters) => {
102
+ const slot = await killedFire(adapters, { store: cutStore("commit", "durable", 2) });
103
+ expectEqual(await slot.ran(), ["a"], "the task's commit is durable");
104
+ await recoversAfterKilledFire(adapters, slot);
105
+ }),
106
+ killCase("KILL-WAKE-07", "fire", "eviction during the alarm clear after the work", async (adapters) => {
107
+ const slot = await killedFire(adapters, { alarm: cutAlarm("clear", "before", 2) });
108
+ expectEqual(await slot.ran(), ["a"], "the work is durable");
109
+ await recoversAfterKilledFire(adapters, slot);
110
+ }),
111
+ killCase("KILL-WAKE-08", "admit", "a concurrent fire's updates while the admission is uncommitted, then a kill after its commit", async (adapters) => {
112
+ const slot = await admissionDuringFire(adapters);
113
+ expectEqual(await slot.alarmAt(), DUE + HEARTBEAT_MS, "the admission's heartbeat outlived the fire's updates");
114
+ await adapters.wake.advanceTo(slot.base + DUE + HEARTBEAT_MS);
115
+ expectEqual(await slot.alarmAt(), LATER, "the heartbeat's instance armed the committed wake");
116
+ await adapters.wake.advanceTo(slot.base + LATER);
117
+ expectEqual(await slot.ran(), ["a", "b"], "each task ran exactly once");
118
+ expectEqual(await slot.alarmAt(), undefined, "and the alarm is cleared");
119
+ }),
120
+ ];
121
+ export const KILL_WAKE_DECLARATIONS = KILL_CASES.map(({ id, operation, point, placement }) => ({ id, operation, point, placement }));
122
+ export const KILL_WAKE_CASES = new Map(KILL_CASES.map(({ id, run }) => [id, run]));
123
+ //# sourceMappingURL=kill-wake.js.map
@@ -0,0 +1,8 @@
1
+ /** Manual timers for a host under test: a clock that moves only through `advanceTo`, which runs due timers in order. */
2
+ export interface ManualTimers {
3
+ now(): number;
4
+ setTimer(callback: () => unknown, ms: number): () => void;
5
+ advanceTo(at: number): Promise<void>;
6
+ }
7
+ export declare function createManualTimers(start: number): ManualTimers;
8
+ //# sourceMappingURL=manual-timers.d.ts.map
@@ -0,0 +1,31 @@
1
+ export function createManualTimers(start) {
2
+ let time = start;
3
+ let nextId = 1;
4
+ const timers = new Map();
5
+ return {
6
+ now: () => time,
7
+ setTimer: (callback, ms) => {
8
+ const id = nextId++;
9
+ timers.set(id, { due: time + Math.max(0, ms), callback });
10
+ return () => void timers.delete(id);
11
+ },
12
+ async advanceTo(at) {
13
+ for (let step = 0;; step++) {
14
+ if (step > 10_000)
15
+ throw new Error("manual timers: more than 10000 timers fired in one advance");
16
+ let next;
17
+ for (const entry of timers) {
18
+ if (entry[1].due <= at && (next === undefined || entry[1].due < next[1].due))
19
+ next = entry;
20
+ }
21
+ if (next === undefined)
22
+ break;
23
+ timers.delete(next[0]);
24
+ time = Math.max(time, next[1].due);
25
+ await next[1].callback();
26
+ }
27
+ time = Math.max(time, at);
28
+ },
29
+ };
30
+ }
31
+ //# sourceMappingURL=manual-timers.js.map
@@ -0,0 +1,6 @@
1
+ import type { CaseImplementation } from "../../conformance/runner.ts";
2
+ import { type WakeCaseAdapters } from "./wake-harness.ts";
3
+ type MultiHostCase = CaseImplementation<WakeCaseAdapters>;
4
+ export declare const MULTI_HOST_CASES: ReadonlyMap<string, MultiHostCase>;
5
+ export {};
6
+ //# sourceMappingURL=multi-host-cases.d.ts.map
@@ -0,0 +1,32 @@
1
+ import { expectEqual } from "./store-harness.js";
2
+ import { createSlot } from "./wake-harness.js";
3
+ async function schedule(slot, id, offset) {
4
+ const { host, loop } = await slot.instance();
5
+ await host.admit(() => loop.schedule(id, slot.base + offset));
6
+ }
7
+ const observe = async (slots) => ({
8
+ fires: slots.map((slot) => slot.fires()),
9
+ ran: await Promise.all(slots.map((slot) => slot.ran())),
10
+ alarms: await Promise.all(slots.map((slot) => slot.alarmAt())),
11
+ });
12
+ const manySessionsOnOneHost = async (adapters) => {
13
+ const slots = [await createSlot(adapters), await createSlot(adapters), await createSlot(adapters)];
14
+ const [first, second, third] = slots;
15
+ if (first === undefined || second === undefined || third === undefined)
16
+ throw new Error("three sessions");
17
+ await schedule(first, "x", 120_000);
18
+ await schedule(first, "y", 300_000);
19
+ await schedule(second, "x", 200_000);
20
+ await third.instance();
21
+ expectEqual(await observe(slots), { fires: [0, 0, 0], ran: [[], [], []], alarms: [120_000, 200_000, undefined] }, "armed per session");
22
+ second.kill();
23
+ await adapters.wake.advanceTo(first.base + 120_000);
24
+ expectEqual(await observe(slots), { fires: [1, 0, 0], ran: [["x"], [], []], alarms: [300_000, 200_000, undefined] }, "a fire resumes only its own session; the same task ID elsewhere is untouched");
25
+ third.kill();
26
+ await third.instance();
27
+ expectEqual((await observe(slots)).alarms, [300_000, 200_000, undefined], "restarting one session moves no other session's alarm");
28
+ await adapters.wake.advanceTo(first.base + 300_000);
29
+ expectEqual(await observe(slots), { fires: [2, 1, 0], ran: [["x", "y"], ["x"], []], alarms: [undefined, undefined, undefined] }, "an evicted session comes back on its own alarm; records and wakes stay per session");
30
+ };
31
+ export const MULTI_HOST_CASES = new Map([["MULTI-HOST-01", manySessionsOnOneHost]]);
32
+ //# sourceMappingURL=multi-host-cases.js.map
@@ -0,0 +1,21 @@
1
+ import type { CaseImplementation } from "../../conformance/runner.ts";
2
+ import type { StoreValue, StoreWrite } from "../store.ts";
3
+ import { type StoreCaseAdapters, type StoreConformanceAdapter } from "./store-harness.ts";
4
+ type StoreCase = CaseImplementation<StoreCaseAdapters>;
5
+ export declare const append: (id: string, kind?: string) => StoreWrite;
6
+ export declare const put: (key: string, data: StoreValue) => StoreWrite;
7
+ export declare const stored: (seq: number, id: string, kind?: string) => {
8
+ seq: number;
9
+ id: string;
10
+ kind: string;
11
+ data: {
12
+ id: string;
13
+ };
14
+ };
15
+ export declare function claimedStore(adapter: StoreConformanceAdapter, epoch?: number): Promise<{
16
+ medium: string;
17
+ store: import("../store.ts").DurableStore;
18
+ }>;
19
+ export declare const STORE_CASES: ReadonlyMap<string, StoreCase>;
20
+ export {};
21
+ //# sourceMappingURL=store-cases.d.ts.map