@code-yeongyu/senpi-agent-core 2026.10.27-2 → 2026.10.30-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.
- package/dist/conformance/index.d.ts +42 -0
- package/dist/conformance/index.js +23 -0
- package/dist/conformance/mapping.d.ts +24 -0
- package/dist/conformance/mapping.js +62 -0
- package/dist/conformance/registry.d.ts +15 -0
- package/dist/conformance/registry.js +75 -0
- package/dist/conformance/runner.d.ts +27 -0
- package/dist/conformance/runner.js +23 -0
- package/dist/conformance/suite-hash.d.ts +12 -0
- package/dist/conformance/suite-hash.js +20 -0
- package/dist/durable/store-admission.d.ts +7 -0
- package/dist/durable/store-admission.js +11 -0
- package/dist/durable/store-memory.d.ts +15 -0
- package/dist/durable/store-memory.js +140 -0
- package/dist/durable/store.d.ts +116 -0
- package/dist/durable/store.js +14 -0
- package/dist/durable/testing/kill-store.d.ts +13 -0
- package/dist/durable/testing/kill-store.js +77 -0
- package/dist/durable/testing/kill-wake.d.ts +12 -0
- package/dist/durable/testing/kill-wake.js +123 -0
- package/dist/durable/testing/manual-timers.d.ts +8 -0
- package/dist/durable/testing/manual-timers.js +31 -0
- package/dist/durable/testing/multi-host-cases.d.ts +6 -0
- package/dist/durable/testing/multi-host-cases.js +32 -0
- package/dist/durable/testing/store-cases.d.ts +21 -0
- package/dist/durable/testing/store-cases.js +168 -0
- package/dist/durable/testing/store-harness.d.ts +23 -0
- package/dist/durable/testing/store-harness.js +64 -0
- package/dist/durable/testing/wake-cases.d.ts +6 -0
- package/dist/durable/testing/wake-cases.js +100 -0
- package/dist/durable/testing/wake-harness.d.ts +64 -0
- package/dist/durable/testing/wake-harness.js +127 -0
- package/dist/durable/testing/wake-test-loop.d.ts +13 -0
- package/dist/durable/testing/wake-test-loop.js +40 -0
- package/dist/durable/wake-host-bun-timer.d.ts +31 -0
- package/dist/durable/wake-host-bun-timer.js +66 -0
- package/dist/durable/wake-host-store-alarm.d.ts +13 -0
- package/dist/durable/wake-host-store-alarm.js +20 -0
- package/dist/durable/wake-host.d.ts +68 -0
- package/dist/durable/wake-host.js +70 -0
- package/dist/harness/utils/read-folders/index.d.ts +1 -1
- package/dist/harness/utils/read-folders/index.js +1 -1
- package/dist/harness/utils/read-folders/tree-sitter/engine.d.ts +1 -1
- package/dist/harness/utils/read-folders/tree-sitter/engine.js +38 -19
- package/dist/testing/win32-sim.d.ts +76 -0
- package/dist/testing/win32-sim.js +197 -0
- package/dist/transport/in-process-carrier.d.ts +19 -0
- package/dist/transport/in-process-carrier.js +63 -0
- package/dist/transport/local-endpoint.d.ts +13 -0
- package/dist/transport/local-endpoint.js +20 -0
- package/dist/transport/reliable-transport.d.ts +40 -0
- package/dist/transport/reliable-transport.js +232 -0
- package/dist/transport/testing/fault-cases.d.ts +6 -0
- package/dist/transport/testing/fault-cases.js +30 -0
- package/dist/transport/testing/fault-transport.d.ts +44 -0
- package/dist/transport/testing/fault-transport.js +144 -0
- package/dist/transport/testing/multi-cases.d.ts +6 -0
- package/dist/transport/testing/multi-cases.js +113 -0
- package/dist/transport/testing/transport-cases.d.ts +6 -0
- package/dist/transport/testing/transport-cases.js +60 -0
- package/dist/transport/testing/transport-harness.d.ts +53 -0
- package/dist/transport/testing/transport-harness.js +127 -0
- package/dist/transport/transport.d.ts +60 -0
- package/dist/transport/transport.js +15 -0
- package/package.json +11 -3
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { expectEqual } from "./store-harness.js";
|
|
2
|
+
import { createSlot, HEARTBEAT_MS, restartHost } from "./wake-harness.js";
|
|
3
|
+
const TEN_MINUTES = 600_000;
|
|
4
|
+
const preArm = async (adapters) => {
|
|
5
|
+
const slot = await createSlot(adapters);
|
|
6
|
+
const session = await slot.instance();
|
|
7
|
+
expectEqual(slot.trace, [`set:+${HEARTBEAT_MS}`, "resume", "clear"], "start arms a fallback before it resumes");
|
|
8
|
+
slot.trace.length = 0;
|
|
9
|
+
await session.host.admit(async () => {
|
|
10
|
+
slot.trace.push(`work alarm=+${await slot.alarmAt()}`);
|
|
11
|
+
});
|
|
12
|
+
expectEqual(slot.trace, [`set:+${HEARTBEAT_MS}`, `work alarm=+${HEARTBEAT_MS}`, "clear"], "admit arms durably before work starts, and arms from the idle loop's wake when the work ends");
|
|
13
|
+
const failing = session.host.admit(async () => {
|
|
14
|
+
throw new Error("work failed");
|
|
15
|
+
});
|
|
16
|
+
await failing.then(() => {
|
|
17
|
+
throw new Error("a failing admission resolved");
|
|
18
|
+
}, () => undefined);
|
|
19
|
+
expectEqual(await slot.alarmAt(), HEARTBEAT_MS, "work that fails leaves the recovery alarm armed");
|
|
20
|
+
await session.host.admit(() => session.loop.schedule("late", slot.base + TEN_MINUTES));
|
|
21
|
+
slot.kill();
|
|
22
|
+
slot.trace.length = 0;
|
|
23
|
+
await slot.instance();
|
|
24
|
+
expectEqual(slot.trace, [`set:+${HEARTBEAT_MS}`, "resume", `set:+${TEN_MINUTES}`], "a new instance lowers a later alarm to the fallback before resuming");
|
|
25
|
+
await (await slot.instance()).host.update({ active: false, at: slot.base + 30_000 });
|
|
26
|
+
slot.kill();
|
|
27
|
+
slot.trace.length = 0;
|
|
28
|
+
await slot.instance();
|
|
29
|
+
expectEqual(slot.trace, ["resume", `set:+${TEN_MINUTES}`], "an earlier alarm is kept as the fallback");
|
|
30
|
+
};
|
|
31
|
+
const update = async (adapters) => {
|
|
32
|
+
const slot = await createSlot(adapters);
|
|
33
|
+
const { host, loop } = await slot.instance();
|
|
34
|
+
const schedule = (id, offset) => host.admit(() => loop.schedule(id, slot.base + offset));
|
|
35
|
+
slot.trace.length = 0;
|
|
36
|
+
await schedule("a", TEN_MINUTES);
|
|
37
|
+
expectEqual(slot.trace, [`set:+${HEARTBEAT_MS}`, `set:+${HEARTBEAT_MS}`, `set:+${TEN_MINUTES}`], "the commit's update keeps the heartbeat while the admission is in flight; after it, the alarm is the wake");
|
|
38
|
+
await schedule("b", 300_000);
|
|
39
|
+
expectEqual(await slot.alarmAt(), 300_000, "an earlier wake lowers the alarm");
|
|
40
|
+
await schedule("c", 900_000);
|
|
41
|
+
expectEqual(await slot.alarmAt(), 300_000, "a later wake never raises it past the earliest");
|
|
42
|
+
await host.update({ active: true, at: slot.base + 300_000 });
|
|
43
|
+
expectEqual(await slot.alarmAt(), HEARTBEAT_MS, "while active, a heartbeat earlier than the wake");
|
|
44
|
+
await host.update({ active: true, at: slot.base + 30_000 });
|
|
45
|
+
expectEqual(await slot.alarmAt(), 30_000, "never later than the earliest wake");
|
|
46
|
+
await host.update({ active: false });
|
|
47
|
+
expectEqual(await slot.alarmAt(), undefined, "cleared only when the loop is idle");
|
|
48
|
+
expectEqual(await slot.ran(), [], "updates run nothing");
|
|
49
|
+
};
|
|
50
|
+
const resume = async (adapters) => {
|
|
51
|
+
const slot = await createSlot(adapters);
|
|
52
|
+
const { host, loop } = await slot.instance();
|
|
53
|
+
for (const [id, offset] of [
|
|
54
|
+
["a", 120_000],
|
|
55
|
+
["b", 120_000],
|
|
56
|
+
["c", TEN_MINUTES],
|
|
57
|
+
]) {
|
|
58
|
+
await host.admit(() => loop.schedule(id, slot.base + offset));
|
|
59
|
+
}
|
|
60
|
+
slot.trace.length = 0;
|
|
61
|
+
await adapters.wake.advanceTo(slot.base + 120_000);
|
|
62
|
+
expectEqual(slot.fires(), 1, "one fire at the wake");
|
|
63
|
+
expectEqual(slot.trace.slice(0, 2), [`set:+${120_000 + HEARTBEAT_MS}`, "resume"], "a fire re-arms before resume");
|
|
64
|
+
expectEqual(await slot.ran(), ["a", "b"], "the due tasks run once, in order");
|
|
65
|
+
expectEqual(await slot.alarmAt(), TEN_MINUTES, "then the alarm is the next wake");
|
|
66
|
+
await host.update({ active: true });
|
|
67
|
+
await adapters.wake.advanceTo(slot.base + 120_000 + HEARTBEAT_MS);
|
|
68
|
+
expectEqual(await slot.ran(), ["a", "b"], "an early alarm runs nothing");
|
|
69
|
+
expectEqual(await slot.alarmAt(), TEN_MINUTES, "and re-arms from the loop's wake");
|
|
70
|
+
await adapters.wake.advanceTo(slot.base + TEN_MINUTES);
|
|
71
|
+
expectEqual(await slot.ran(), ["a", "b", "c"], "the last task runs at its wake");
|
|
72
|
+
expectEqual(await slot.alarmAt(), undefined, "an idle loop leaves no alarm");
|
|
73
|
+
expectEqual(slot.fires(), 3, "no fire beyond the alarms armed");
|
|
74
|
+
};
|
|
75
|
+
const retention = async (adapters) => {
|
|
76
|
+
const slot = await createSlot(adapters);
|
|
77
|
+
const { host, loop } = await slot.instance();
|
|
78
|
+
await host.admit(() => loop.schedule("a", slot.base + TEN_MINUTES));
|
|
79
|
+
await restartHost(adapters, [slot]);
|
|
80
|
+
expectEqual(await slot.alarmAt(), TEN_MINUTES, "the alarm is durable across a host restart");
|
|
81
|
+
await slot.instance();
|
|
82
|
+
slot.kill();
|
|
83
|
+
await slot.instance();
|
|
84
|
+
expectEqual(await slot.alarmAt(), TEN_MINUTES, "reopened instances keep it");
|
|
85
|
+
await adapters.wake.advanceTo(slot.base + TEN_MINUTES - 1);
|
|
86
|
+
expectEqual(slot.fires(), 0, "nothing fires early, whatever instances came and went");
|
|
87
|
+
await adapters.wake.advanceTo(slot.base + TEN_MINUTES);
|
|
88
|
+
expectEqual(slot.fires(), 1, "it fires once");
|
|
89
|
+
expectEqual(await slot.ran(), ["a"], "the task runs once");
|
|
90
|
+
expectEqual(await slot.alarmAt(), undefined, "and the alarm is not retained after the work");
|
|
91
|
+
await adapters.wake.advanceTo(slot.base + 3 * TEN_MINUTES);
|
|
92
|
+
expectEqual(slot.fires(), 1, "a consumed alarm never fires again");
|
|
93
|
+
};
|
|
94
|
+
export const WAKE_CASES = new Map([
|
|
95
|
+
["WAKE-01", preArm],
|
|
96
|
+
["WAKE-02", update],
|
|
97
|
+
["WAKE-03", resume],
|
|
98
|
+
["WAKE-RET-01", retention],
|
|
99
|
+
]);
|
|
100
|
+
//# sourceMappingURL=wake-cases.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { DurableStore, DurableStoreOptions, StoreWritePoint } from "../store.ts";
|
|
2
|
+
import { type WakeAlarm, type WakeAlarmOptions, type WakeAlarmPoint, type WakeHost, type WakePoint } from "../wake-host.ts";
|
|
3
|
+
import type { StoreCaseAdapters } from "./store-harness.ts";
|
|
4
|
+
import { type TestLoop } from "./wake-test-loop.ts";
|
|
5
|
+
/** The session a host's alarm belongs to. Hosts that persist the alarm in the store use `store` under `epoch`. */
|
|
6
|
+
export interface WakeAlarmSession {
|
|
7
|
+
readonly medium: string;
|
|
8
|
+
readonly store: DurableStore;
|
|
9
|
+
readonly epoch: number;
|
|
10
|
+
readonly onFire: () => Promise<void>;
|
|
11
|
+
readonly options?: WakeAlarmOptions;
|
|
12
|
+
}
|
|
13
|
+
/** What a host provides to the WAKE, KILL-WAKE and MULTI-HOST cases, next to its store adapter. */
|
|
14
|
+
export interface WakeConformanceAdapter {
|
|
15
|
+
/** The host clock, epoch milliseconds; only `advanceTo` moves it. */
|
|
16
|
+
now(): number;
|
|
17
|
+
/** Move the clock to `at`, delivering every alarm that falls due on the way, in time order, each awaited. */
|
|
18
|
+
advanceTo(at: number): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* The durable alarm of one session. Opening it again (a new instance after a kill) replaces the earlier instance:
|
|
21
|
+
* fires go to the newest `onFire` only. The armed time outlives instances and host restarts.
|
|
22
|
+
*/
|
|
23
|
+
openAlarm(session: WakeAlarmSession): Promise<WakeAlarm>;
|
|
24
|
+
/** The host process restarts: instances and in-memory timers are lost, durable alarms are kept. */
|
|
25
|
+
restart(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
export interface WakeCaseAdapters extends StoreCaseAdapters {
|
|
28
|
+
readonly wake: WakeConformanceAdapter;
|
|
29
|
+
}
|
|
30
|
+
export declare const HEARTBEAT_MS = 60000;
|
|
31
|
+
export declare const cutStore: (op: StoreWritePoint["op"], at: StoreWritePoint["at"], nth?: number) => DurableStoreOptions;
|
|
32
|
+
export declare const cutAlarm: (op: WakeAlarmPoint["op"], at: WakeAlarmPoint["at"], nth?: number) => WakeAlarmOptions;
|
|
33
|
+
export declare const cutWake: (op: WakePoint["op"], nth?: number) => ((point: WakePoint) => void);
|
|
34
|
+
export declare function expectWakeCut(operation: Promise<unknown>, what: string): Promise<void>;
|
|
35
|
+
export interface SessionInstance {
|
|
36
|
+
readonly loop: TestLoop;
|
|
37
|
+
readonly host: WakeHost;
|
|
38
|
+
}
|
|
39
|
+
export interface BootOptions {
|
|
40
|
+
readonly store?: DurableStoreOptions;
|
|
41
|
+
readonly alarm?: WakeAlarmOptions;
|
|
42
|
+
readonly onWakePoint?: (point: WakePoint) => void;
|
|
43
|
+
}
|
|
44
|
+
/** One session on the host: its medium, its live instance (if any), and what the cases observe. */
|
|
45
|
+
export interface SessionSlot {
|
|
46
|
+
/** The clock time the slot was created at; trace times are relative to it. */
|
|
47
|
+
readonly base: number;
|
|
48
|
+
/** Durable alarm writes (`set:+ms` / `clear`) and `resume` calls, in order, across instances. */
|
|
49
|
+
readonly trace: string[];
|
|
50
|
+
/** The live instance; with none, boot one (open + `start`) with `options`. */
|
|
51
|
+
instance(options?: BootOptions): Promise<SessionInstance>;
|
|
52
|
+
/** Drop the live instance, as an eviction or crash does. */
|
|
53
|
+
kill(): void;
|
|
54
|
+
/** Alarm fires delivered to this session. */
|
|
55
|
+
fires(): number;
|
|
56
|
+
/** Task IDs the loop committed as run, in commit order. */
|
|
57
|
+
ran(): Promise<string[]>;
|
|
58
|
+
/** The durable alarm, relative to `base`; undefined when cleared. */
|
|
59
|
+
alarmAt(): Promise<number | undefined>;
|
|
60
|
+
}
|
|
61
|
+
export declare function createSlot(adapters: WakeCaseAdapters, epoch?: number): Promise<SessionSlot>;
|
|
62
|
+
/** Restart the host process: every slot loses its live instance; durable state stays. */
|
|
63
|
+
export declare function restartHost(adapters: WakeCaseAdapters, slots: readonly SessionSlot[]): Promise<void>;
|
|
64
|
+
//# sourceMappingURL=wake-harness.d.ts.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createWakeHost, } from "../wake-host.js";
|
|
2
|
+
import { openTestLoop } from "./wake-test-loop.js";
|
|
3
|
+
export const HEARTBEAT_MS = 60_000;
|
|
4
|
+
/** A simulated kill inside a wake case; every cut in these cases throws it. */
|
|
5
|
+
class WakeCut extends Error {
|
|
6
|
+
constructor(where) {
|
|
7
|
+
super(`cut at ${where}`);
|
|
8
|
+
this.name = "WakeCut";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const isCut = (error) => error instanceof WakeCut;
|
|
12
|
+
/** Throw a WakeCut the `nth` time `matches` sees a point (1-based). */
|
|
13
|
+
function cutNth(matches, nth) {
|
|
14
|
+
let seen = 0;
|
|
15
|
+
return (point) => {
|
|
16
|
+
if (matches(point) && ++seen === nth)
|
|
17
|
+
throw new WakeCut(JSON.stringify(point));
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export const cutStore = (op, at, nth = 1) => ({
|
|
21
|
+
onWritePoint: cutNth((point) => point.op === op && point.at === at, nth),
|
|
22
|
+
});
|
|
23
|
+
export const cutAlarm = (op, at, nth = 1) => ({
|
|
24
|
+
onAlarmPoint: cutNth((point) => point.op === op && point.at === at, nth),
|
|
25
|
+
});
|
|
26
|
+
export const cutWake = (op, nth = 1) => cutNth((point) => point.op === op, nth);
|
|
27
|
+
export async function expectWakeCut(operation, what) {
|
|
28
|
+
try {
|
|
29
|
+
await operation;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (isCut(error))
|
|
33
|
+
return;
|
|
34
|
+
throw new Error(`${what}: expected the cut to surface, got ${String(error)}`);
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`${what}: expected the cut to surface, but it resolved`);
|
|
37
|
+
}
|
|
38
|
+
export async function createSlot(adapters, epoch = 1) {
|
|
39
|
+
const { store: storeAdapter, wake } = adapters;
|
|
40
|
+
const medium = await storeAdapter.createMedium();
|
|
41
|
+
const reader = await storeAdapter.open(medium);
|
|
42
|
+
for (let next = 1; next <= epoch; next++)
|
|
43
|
+
await reader.claim(next - 1, next);
|
|
44
|
+
const base = wake.now();
|
|
45
|
+
const trace = [];
|
|
46
|
+
let live;
|
|
47
|
+
let alarm;
|
|
48
|
+
let fires = 0;
|
|
49
|
+
const record = (options) => ({
|
|
50
|
+
onAlarmPoint: (point) => {
|
|
51
|
+
options?.onAlarmPoint?.(point);
|
|
52
|
+
if (point.at === "durable")
|
|
53
|
+
trace.push(point.op === "clear" ? "clear" : `set:+${(point.time ?? 0) - base}`);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
async function boot(options = {}) {
|
|
57
|
+
const store = await storeAdapter.open(medium, options.store);
|
|
58
|
+
let host;
|
|
59
|
+
const loop = await openTestLoop(store, epoch, wake.now, async () => host?.update(loop.wake()));
|
|
60
|
+
alarm = await wake.openAlarm({
|
|
61
|
+
medium,
|
|
62
|
+
store: await storeAdapter.open(medium),
|
|
63
|
+
epoch,
|
|
64
|
+
onFire: dispatch,
|
|
65
|
+
options: record(options.alarm),
|
|
66
|
+
});
|
|
67
|
+
host = createWakeHost({
|
|
68
|
+
alarm,
|
|
69
|
+
now: wake.now,
|
|
70
|
+
wake: () => loop.wake(),
|
|
71
|
+
resume: async () => {
|
|
72
|
+
trace.push("resume");
|
|
73
|
+
await loop.resume();
|
|
74
|
+
},
|
|
75
|
+
heartbeatMs: HEARTBEAT_MS,
|
|
76
|
+
onWakePoint: options.onWakePoint,
|
|
77
|
+
});
|
|
78
|
+
const instance = { loop, host };
|
|
79
|
+
live = instance;
|
|
80
|
+
await host.start();
|
|
81
|
+
return instance;
|
|
82
|
+
}
|
|
83
|
+
async function dispatch() {
|
|
84
|
+
fires++;
|
|
85
|
+
try {
|
|
86
|
+
const instance = live ?? (await boot());
|
|
87
|
+
await instance.host.fire();
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (!isCut(error))
|
|
91
|
+
throw error;
|
|
92
|
+
live = undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
base,
|
|
97
|
+
trace,
|
|
98
|
+
async instance(options) {
|
|
99
|
+
if (live !== undefined)
|
|
100
|
+
return live;
|
|
101
|
+
try {
|
|
102
|
+
return await boot(options);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (isCut(error))
|
|
106
|
+
live = undefined;
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
kill: () => {
|
|
111
|
+
live = undefined;
|
|
112
|
+
},
|
|
113
|
+
fires: () => fires,
|
|
114
|
+
ran: async () => (await reader.scanRecords({ kind: "ran" })).map((record) => record.id.slice("ran:".length)),
|
|
115
|
+
alarmAt: async () => {
|
|
116
|
+
const at = await alarm?.read();
|
|
117
|
+
return at === undefined ? undefined : at - base;
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** Restart the host process: every slot loses its live instance; durable state stays. */
|
|
122
|
+
export async function restartHost(adapters, slots) {
|
|
123
|
+
await adapters.wake.restart();
|
|
124
|
+
for (const slot of slots)
|
|
125
|
+
slot.kill();
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=wake-harness.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DurableStore } from "../store.ts";
|
|
2
|
+
import type { Wake } from "../wake-host.ts";
|
|
3
|
+
/** A minimal loop over the store: tasks due at a time, each run in one commit. Enough to observe wake ordering. */
|
|
4
|
+
export interface TestLoop {
|
|
5
|
+
/** Schedule task `id` at `due`, in one commit. */
|
|
6
|
+
schedule(id: string, due: number): Promise<void>;
|
|
7
|
+
/** Run every due task, one commit each: a `ran` record and the task's removal, atomically. */
|
|
8
|
+
resume(): Promise<void>;
|
|
9
|
+
/** `active` while a task is due now; `at`, the earliest task due later. */
|
|
10
|
+
wake(): Wake;
|
|
11
|
+
}
|
|
12
|
+
export declare function openTestLoop(store: DurableStore, epoch: number, now: () => number, committed: () => Promise<void>): Promise<TestLoop>;
|
|
13
|
+
//# sourceMappingURL=wake-test-loop.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createAdmissionLine } from "../store-admission.js";
|
|
2
|
+
const TASK_PREFIX = "task/";
|
|
3
|
+
export async function openTestLoop(store, epoch, now, committed) {
|
|
4
|
+
const line = createAdmissionLine();
|
|
5
|
+
const tasks = new Map();
|
|
6
|
+
for (const document of await store.scanDocuments(TASK_PREFIX)) {
|
|
7
|
+
if (typeof document.data === "number")
|
|
8
|
+
tasks.set(document.key.slice(TASK_PREFIX.length), document.data);
|
|
9
|
+
}
|
|
10
|
+
const commit = async (writes, adopt) => {
|
|
11
|
+
await store.commit(epoch, writes, adopt);
|
|
12
|
+
await committed();
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
schedule: (id, due) => line.admit(() => commit([{ op: "put", key: TASK_PREFIX + id, data: due }], () => void tasks.set(id, due))),
|
|
16
|
+
resume: () => line.admit(async () => {
|
|
17
|
+
const due = [...tasks].filter(([, at]) => at <= now()).sort(([a, x], [b, y]) => x - y || (a < b ? -1 : 1));
|
|
18
|
+
for (const [id] of due) {
|
|
19
|
+
const writes = [
|
|
20
|
+
{ op: "append", id: `ran:${id}`, kind: "ran", data: { id } },
|
|
21
|
+
{ op: "delete", key: TASK_PREFIX + id },
|
|
22
|
+
];
|
|
23
|
+
await commit(writes, () => void tasks.delete(id));
|
|
24
|
+
}
|
|
25
|
+
}),
|
|
26
|
+
wake: () => {
|
|
27
|
+
const time = now();
|
|
28
|
+
let active = false;
|
|
29
|
+
let at;
|
|
30
|
+
for (const due of tasks.values()) {
|
|
31
|
+
if (due <= time)
|
|
32
|
+
active = true;
|
|
33
|
+
else if (at === undefined || due < at)
|
|
34
|
+
at = due;
|
|
35
|
+
}
|
|
36
|
+
return at === undefined ? { active } : { active, at };
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=wake-test-loop.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { WakeAlarm, WakeAlarmOptions } from "./wake-host.ts";
|
|
2
|
+
import type { AlarmPersistence } from "./wake-host-store-alarm.ts";
|
|
3
|
+
/** Schedule `callback` after `ms`; returns its cancel. */
|
|
4
|
+
export type SetTimer = (callback: () => unknown, ms: number) => () => void;
|
|
5
|
+
export interface BunTimerHostOptions {
|
|
6
|
+
readonly now?: () => number;
|
|
7
|
+
readonly setTimer?: SetTimer;
|
|
8
|
+
/**
|
|
9
|
+
* The longest single timer. Monotonic timers stop while a machine sleeps, so a far alarm re-checks the wall clock
|
|
10
|
+
* at least this often and an alarm that fell due during a sleep fires within this bound of waking. Default 60 s.
|
|
11
|
+
*/
|
|
12
|
+
readonly maxDelayMs?: number;
|
|
13
|
+
/** A fire handler rejected. The alarm stays armed and fires again after `maxDelayMs`. */
|
|
14
|
+
readonly onFireError: (key: string, error: unknown) => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The Bun process timer host: one instance serves every session of the process, each under its own key, with its
|
|
18
|
+
* alarm persisted (normally `storeAlarmPersistence`). After a process restart the embedding host opens its sessions'
|
|
19
|
+
* alarms again; an alarm that fell due meanwhile fires at once.
|
|
20
|
+
*/
|
|
21
|
+
export interface BunTimerHost {
|
|
22
|
+
/**
|
|
23
|
+
* The alarm of session `key`, read from `persistence`. Opening a key again replaces the earlier instance: fires go
|
|
24
|
+
* to the newest `onFire`, and the replaced instance's writes are refused.
|
|
25
|
+
*/
|
|
26
|
+
openAlarm(key: string, persistence: AlarmPersistence, onFire: () => Promise<void>, options?: WakeAlarmOptions): Promise<WakeAlarm>;
|
|
27
|
+
/** Cancel every timer. Persisted alarms stay. */
|
|
28
|
+
close(): void;
|
|
29
|
+
}
|
|
30
|
+
export declare function createBunTimerHost(options: BunTimerHostOptions): BunTimerHost;
|
|
31
|
+
//# sourceMappingURL=wake-host-bun-timer.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const DEFAULT_MAX_DELAY_MS = 60_000;
|
|
2
|
+
const defaultSetTimer = (callback, ms) => {
|
|
3
|
+
const handle = setTimeout(callback, ms);
|
|
4
|
+
return () => clearTimeout(handle);
|
|
5
|
+
};
|
|
6
|
+
export function createBunTimerHost(options) {
|
|
7
|
+
const now = options.now ?? Date.now;
|
|
8
|
+
const setTimer = options.setTimer ?? defaultSetTimer;
|
|
9
|
+
const maxDelayMs = options.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
10
|
+
const entries = new Map();
|
|
11
|
+
const delivering = new Set();
|
|
12
|
+
function schedule(key, entry, retry = false) {
|
|
13
|
+
entry.cancel?.();
|
|
14
|
+
entry.cancel = undefined;
|
|
15
|
+
if (entry.at === undefined || entries.get(key) !== entry || delivering.has(key))
|
|
16
|
+
return;
|
|
17
|
+
const delay = retry ? maxDelayMs : Math.min(Math.max(0, entry.at - now()), maxDelayMs);
|
|
18
|
+
entry.cancel = setTimer(() => tick(key, entry), delay);
|
|
19
|
+
}
|
|
20
|
+
async function tick(key, entry) {
|
|
21
|
+
entry.cancel = undefined;
|
|
22
|
+
const firedAt = entry.at;
|
|
23
|
+
if (firedAt === undefined || entries.get(key) !== entry)
|
|
24
|
+
return;
|
|
25
|
+
if (now() < firedAt)
|
|
26
|
+
return schedule(key, entry);
|
|
27
|
+
delivering.add(key);
|
|
28
|
+
try {
|
|
29
|
+
await entry.onFire();
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
options.onFireError(key, error);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
delivering.delete(key);
|
|
36
|
+
const current = entries.get(key);
|
|
37
|
+
if (current !== undefined)
|
|
38
|
+
schedule(key, current, current === entry && current.at === firedAt);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
async openAlarm(key, persistence, onFire, alarmOptions = {}) {
|
|
43
|
+
const entry = { at: await persistence.read(), cancel: undefined, onFire };
|
|
44
|
+
entries.get(key)?.cancel?.();
|
|
45
|
+
entries.set(key, entry);
|
|
46
|
+
schedule(key, entry);
|
|
47
|
+
const write = async (at) => {
|
|
48
|
+
if (entries.get(key) !== entry)
|
|
49
|
+
throw new Error(`the alarm instance of ${key} was replaced by a newer one`);
|
|
50
|
+
const point = (stage) => alarmOptions.onAlarmPoint?.(at === undefined ? { op: "clear", at: stage } : { op: "set", at: stage, time: at });
|
|
51
|
+
point("before");
|
|
52
|
+
await persistence.write(at);
|
|
53
|
+
entry.at = at;
|
|
54
|
+
schedule(key, entry);
|
|
55
|
+
point("durable");
|
|
56
|
+
};
|
|
57
|
+
return { read: () => persistence.read(), set: (at) => write(at), clear: () => write(undefined) };
|
|
58
|
+
},
|
|
59
|
+
close() {
|
|
60
|
+
for (const entry of entries.values())
|
|
61
|
+
entry.cancel?.();
|
|
62
|
+
entries.clear();
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=wake-host-bun-timer.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DurableStore } from "./store.ts";
|
|
2
|
+
/** Where a timer host keeps one session's armed alarm, durably. */
|
|
3
|
+
export interface AlarmPersistence {
|
|
4
|
+
read(): Promise<number | undefined>;
|
|
5
|
+
/** Durably arm at `at`, or clear with `undefined`. */
|
|
6
|
+
write(at: number | undefined): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The alarm as a document in the session's own DurableStore, written under the owner `epoch`: an old owner's alarm
|
|
10
|
+
* write is refused (`stale-epoch`) like any other write of it, and the alarm moves with the store on claim + import.
|
|
11
|
+
*/
|
|
12
|
+
export declare function storeAlarmPersistence(store: DurableStore, epoch: number): AlarmPersistence;
|
|
13
|
+
//# sourceMappingURL=wake-host-store-alarm.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const WAKE_ALARM_DOCUMENT = "durable.wake.alarm";
|
|
2
|
+
/**
|
|
3
|
+
* The alarm as a document in the session's own DurableStore, written under the owner `epoch`: an old owner's alarm
|
|
4
|
+
* write is refused (`stale-epoch`) like any other write of it, and the alarm moves with the store on claim + import.
|
|
5
|
+
*/
|
|
6
|
+
export function storeAlarmPersistence(store, epoch) {
|
|
7
|
+
return {
|
|
8
|
+
read: async () => {
|
|
9
|
+
const at = await store.getDocument(WAKE_ALARM_DOCUMENT);
|
|
10
|
+
return typeof at === "number" ? at : undefined;
|
|
11
|
+
},
|
|
12
|
+
write: async (at) => {
|
|
13
|
+
const write = at === undefined
|
|
14
|
+
? { op: "delete", key: WAKE_ALARM_DOCUMENT }
|
|
15
|
+
: { op: "put", key: WAKE_ALARM_DOCUMENT, data: at };
|
|
16
|
+
await store.commit(epoch, [write], () => { });
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=wake-host-store-alarm.js.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WakeHost contract of the durable core (packages/agent/docs/durable-core-adr.md): wake ordering for hosts that can
|
|
3
|
+
* be evicted (a Durable Object) or put to sleep (a laptop). A process timer is lost with its host and runs late across
|
|
4
|
+
* a sleep, so the host keeps a durable alarm and follows one ordering, shared by every host here:
|
|
5
|
+
*
|
|
6
|
+
* - admit: arm a recovery heartbeat durably BEFORE work that can commit starts. A commit whose host dies before the
|
|
7
|
+
* alarm update would otherwise strand its wake; an early alarm is harmless, a missing one loses work. The host counts
|
|
8
|
+
* admissions in flight and arms as active while any is, so no concurrent update or fire can clear that heartbeat
|
|
9
|
+
* before the admitted work commits; when the last admitted work succeeds, it arms from the loop's wake.
|
|
10
|
+
* - update: after each commit, arm min(heartbeat if active, the loop's earliest wake); clear only when idle.
|
|
11
|
+
* - fire: re-arm the heartbeat before resuming, so a resume that dies is retried; then arm from the loop's wake.
|
|
12
|
+
* - start (a new instance): arm a fallback before anything can throw, then resume what a lost instance left.
|
|
13
|
+
*
|
|
14
|
+
* Timers inside the loop are not the WakeHost's. Where a host can neither be evicted nor sleep it reduces to a timer.
|
|
15
|
+
*/
|
|
16
|
+
/** The loop's wake state: `active` while some work can run now or is running; `at`, the earliest later wake. */
|
|
17
|
+
export interface Wake {
|
|
18
|
+
readonly active: boolean;
|
|
19
|
+
readonly at?: number;
|
|
20
|
+
}
|
|
21
|
+
/** A host's durable alarm for one session; the armed time (epoch ms) survives instance loss and host restarts. */
|
|
22
|
+
export interface WakeAlarm {
|
|
23
|
+
read(): Promise<number | undefined>;
|
|
24
|
+
set(at: number): Promise<void>;
|
|
25
|
+
clear(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
/** A point inside an alarm write. `before`: nothing is durable yet. `durable`: the write is durable. */
|
|
28
|
+
export type WakeAlarmPoint = {
|
|
29
|
+
readonly op: "set";
|
|
30
|
+
readonly at: "before" | "durable";
|
|
31
|
+
readonly time: number;
|
|
32
|
+
} | {
|
|
33
|
+
readonly op: "clear";
|
|
34
|
+
readonly at: "before" | "durable";
|
|
35
|
+
readonly time?: undefined;
|
|
36
|
+
};
|
|
37
|
+
export interface WakeAlarmOptions {
|
|
38
|
+
/** Test seam for cuts and kills: a throw at `before` leaves the alarm unchanged, at `durable` leaves it written. */
|
|
39
|
+
readonly onAlarmPoint?: (point: WakeAlarmPoint) => void;
|
|
40
|
+
}
|
|
41
|
+
/** The point after an operation's arm is durable and before its work (`admit`) or resume (`start`, `fire`) runs. */
|
|
42
|
+
export interface WakePoint {
|
|
43
|
+
readonly op: "start" | "admit" | "fire";
|
|
44
|
+
readonly at: "armed";
|
|
45
|
+
}
|
|
46
|
+
export interface WakeHostOptions {
|
|
47
|
+
readonly alarm: WakeAlarm;
|
|
48
|
+
/** The host clock, epoch milliseconds; the same clock the loop's wake times use. */
|
|
49
|
+
readonly now: () => number;
|
|
50
|
+
readonly wake: () => Wake;
|
|
51
|
+
/** Run what the loop can run now: due wakes and work a lost instance left. Resolves when it has. */
|
|
52
|
+
readonly resume: () => Promise<void>;
|
|
53
|
+
/** The recovery interval while work is active or admitted. Default 60 s. */
|
|
54
|
+
readonly heartbeatMs?: number;
|
|
55
|
+
/** Test seam for kills between an arm and the work it protects. */
|
|
56
|
+
readonly onWakePoint?: (point: WakePoint) => void;
|
|
57
|
+
}
|
|
58
|
+
export interface WakeHost {
|
|
59
|
+
/** A new instance: arm a fallback, resume, then arm from the loop's wake. */
|
|
60
|
+
start(): Promise<void>;
|
|
61
|
+
/** Run work that can commit, after a recovery heartbeat is durable. */
|
|
62
|
+
admit<T>(work: () => Promise<T>): Promise<T>;
|
|
63
|
+
update(wake: Wake): Promise<void>;
|
|
64
|
+
/** The alarm fired: re-arm the heartbeat, resume, then arm from the loop's wake. */
|
|
65
|
+
fire(): Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
export declare function createWakeHost(options: WakeHostOptions): WakeHost;
|
|
68
|
+
//# sourceMappingURL=wake-host.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WakeHost contract of the durable core (packages/agent/docs/durable-core-adr.md): wake ordering for hosts that can
|
|
3
|
+
* be evicted (a Durable Object) or put to sleep (a laptop). A process timer is lost with its host and runs late across
|
|
4
|
+
* a sleep, so the host keeps a durable alarm and follows one ordering, shared by every host here:
|
|
5
|
+
*
|
|
6
|
+
* - admit: arm a recovery heartbeat durably BEFORE work that can commit starts. A commit whose host dies before the
|
|
7
|
+
* alarm update would otherwise strand its wake; an early alarm is harmless, a missing one loses work. The host counts
|
|
8
|
+
* admissions in flight and arms as active while any is, so no concurrent update or fire can clear that heartbeat
|
|
9
|
+
* before the admitted work commits; when the last admitted work succeeds, it arms from the loop's wake.
|
|
10
|
+
* - update: after each commit, arm min(heartbeat if active, the loop's earliest wake); clear only when idle.
|
|
11
|
+
* - fire: re-arm the heartbeat before resuming, so a resume that dies is retried; then arm from the loop's wake.
|
|
12
|
+
* - start (a new instance): arm a fallback before anything can throw, then resume what a lost instance left.
|
|
13
|
+
*
|
|
14
|
+
* Timers inside the loop are not the WakeHost's. Where a host can neither be evicted nor sleep it reduces to a timer.
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_HEARTBEAT_MS = 60_000;
|
|
17
|
+
/** The alarm time for a wake state: a heartbeat while active, never later than the earliest wake. */
|
|
18
|
+
function recoveryAlarmAt(wake, now, heartbeatMs) {
|
|
19
|
+
const heartbeat = wake.active ? now + heartbeatMs : undefined;
|
|
20
|
+
if (heartbeat === undefined)
|
|
21
|
+
return wake.at;
|
|
22
|
+
return wake.at === undefined ? heartbeat : Math.min(heartbeat, wake.at);
|
|
23
|
+
}
|
|
24
|
+
export function createWakeHost(options) {
|
|
25
|
+
const { alarm, now, wake, resume } = options;
|
|
26
|
+
const heartbeatMs = options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
|
|
27
|
+
const point = (op) => options.onWakePoint?.({ op, at: "armed" });
|
|
28
|
+
let admitted = 0;
|
|
29
|
+
const arm = async (state) => {
|
|
30
|
+
const at = recoveryAlarmAt(admitted > 0 ? { ...state, active: true } : state, now(), heartbeatMs);
|
|
31
|
+
if (at === undefined)
|
|
32
|
+
await alarm.clear();
|
|
33
|
+
else
|
|
34
|
+
await alarm.set(at);
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
async start() {
|
|
38
|
+
const fallback = now() + heartbeatMs;
|
|
39
|
+
const current = await alarm.read();
|
|
40
|
+
if (current === undefined || current > fallback)
|
|
41
|
+
await alarm.set(fallback);
|
|
42
|
+
point("start");
|
|
43
|
+
await resume();
|
|
44
|
+
await arm(wake());
|
|
45
|
+
},
|
|
46
|
+
async admit(work) {
|
|
47
|
+
admitted++;
|
|
48
|
+
let result;
|
|
49
|
+
try {
|
|
50
|
+
await arm({ ...wake(), active: true });
|
|
51
|
+
point("admit");
|
|
52
|
+
result = await work();
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
admitted--;
|
|
56
|
+
}
|
|
57
|
+
if (admitted === 0)
|
|
58
|
+
await arm(wake());
|
|
59
|
+
return result;
|
|
60
|
+
},
|
|
61
|
+
update: (state) => arm(state),
|
|
62
|
+
async fire() {
|
|
63
|
+
await arm({ ...wake(), active: true });
|
|
64
|
+
point("fire");
|
|
65
|
+
await resume();
|
|
66
|
+
await arm(wake());
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=wake-host.js.map
|
|
@@ -3,7 +3,7 @@ export * from "./types.ts";
|
|
|
3
3
|
/** Frozen measured selection, not a runtime registry that can enable unmeasured grammars. */
|
|
4
4
|
export declare const READ_FOLDER_SELECTION: Readonly<{
|
|
5
5
|
readonly head: "6da8ef1361ef73928df8685e071f2a1826d3b17e";
|
|
6
|
-
readonly selectionSha256: "
|
|
6
|
+
readonly selectionSha256: "dd4b1fa66ed151e330cd7efaf7ca649d244257d52e2a79422e868da60c0edd8b";
|
|
7
7
|
readonly wasm: true;
|
|
8
8
|
readonly rawReasons: Readonly<{
|
|
9
9
|
readonly ts: "wasm_candidate_below_threshold";
|
|
@@ -4,7 +4,7 @@ export * from "./types.js";
|
|
|
4
4
|
/** Frozen measured selection, not a runtime registry that can enable unmeasured grammars. */
|
|
5
5
|
export const READ_FOLDER_SELECTION = Object.freeze({
|
|
6
6
|
head: "6da8ef1361ef73928df8685e071f2a1826d3b17e",
|
|
7
|
-
selectionSha256: "
|
|
7
|
+
selectionSha256: "dd4b1fa66ed151e330cd7efaf7ca649d244257d52e2a79422e868da60c0edd8b",
|
|
8
8
|
wasm: true,
|
|
9
9
|
rawReasons: Object.freeze({
|
|
10
10
|
ts: "wasm_candidate_below_threshold",
|
|
@@ -11,7 +11,7 @@ export type TreeSitterFolderOptions = {
|
|
|
11
11
|
readonly parseBudgetMs?: number;
|
|
12
12
|
readonly resolveGrammar?: GrammarResolver;
|
|
13
13
|
readonly resolveRuntime?: () => Promise<string | undefined>;
|
|
14
|
-
/**
|
|
14
|
+
/** Controls grammar/parser sharing; runtime initialization is always shared by resolved path. */
|
|
15
15
|
readonly cache?: boolean;
|
|
16
16
|
};
|
|
17
17
|
/**
|