@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,197 @@
1
+ import * as fsp from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ /** Node's Windows errno values (libuv `UV_E*` on win32). */
4
+ const WIN32_ERRNO = { EBUSY: -4082, EPERM: -4048, ENOSYS: -4054 };
5
+ const WIN32_ERROR_DESCRIPTION = {
6
+ EBUSY: "resource busy or locked",
7
+ EPERM: "operation not permitted",
8
+ ENOSYS: "function not implemented",
9
+ };
10
+ /** Builds the error Node throws on Windows for `code` from `syscall`. */
11
+ export function win32Error(code, syscall, filePath, dest) {
12
+ let target = "";
13
+ if (filePath !== undefined)
14
+ target = dest === undefined ? ` '${filePath}'` : ` '${filePath}' -> '${dest}'`;
15
+ const error = new Error(`${code}: ${WIN32_ERROR_DESCRIPTION[code]}, ${syscall}${target}`);
16
+ error.code = code;
17
+ error.errno = WIN32_ERRNO[code];
18
+ error.syscall = syscall;
19
+ if (filePath !== undefined)
20
+ error.path = filePath;
21
+ if (dest !== undefined)
22
+ error.dest = dest;
23
+ return error;
24
+ }
25
+ /** Compares two Windows names (path segments, paths or pipe names) the way Windows does: case-insensitively. */
26
+ export function win32NamesEqual(left, right) {
27
+ return left.toUpperCase() === right.toUpperCase();
28
+ }
29
+ const DRIVE_LETTER = /^[A-Za-z]$/;
30
+ const DRIVE_PATH = /^([A-Za-z]):\\(.*)$/s;
31
+ /** Turns a relative POSIX-style path (`dir/file`) into a drive-letter Windows path (`C:\dir\file`). */
32
+ export function toWin32Path(relativePath, drive = "C") {
33
+ if (!DRIVE_LETTER.test(drive))
34
+ throw new RangeError(`win32 sim: drive must be one letter, got ${JSON.stringify(drive)}`);
35
+ if (relativePath.startsWith("/")) {
36
+ throw new RangeError(`win32 sim: expected a relative path, got ${JSON.stringify(relativePath)}`);
37
+ }
38
+ return path.win32.join(`${drive.toUpperCase()}:\\`, ...relativePath.split("/"));
39
+ }
40
+ /** True for an absolute drive-letter path such as `C:\dir` (forward slashes are accepted, as on Windows). */
41
+ export function isWin32DrivePath(candidate) {
42
+ return DRIVE_PATH.test(path.win32.normalize(candidate));
43
+ }
44
+ export const WIN32_PIPE_PREFIX = "\\\\.\\pipe\\";
45
+ /** Windows limits a full pipe name to 256 characters. */
46
+ const WIN32_PIPE_MAX_LENGTH = 256;
47
+ /** Builds the named-pipe endpoint (`\\.\pipe\<name>`) a Windows transport listens on instead of a socket file. */
48
+ export function win32PipeEndpoint(name) {
49
+ if (name.length === 0 || name.includes("\\")) {
50
+ throw new RangeError(`win32 sim: a pipe name must be non-empty and contain no backslash, got ${JSON.stringify(name)}`);
51
+ }
52
+ const endpoint = `${WIN32_PIPE_PREFIX}${name}`;
53
+ if (endpoint.length > WIN32_PIPE_MAX_LENGTH) {
54
+ throw new RangeError(`win32 sim: a pipe endpoint is at most ${WIN32_PIPE_MAX_LENGTH} characters`);
55
+ }
56
+ return endpoint;
57
+ }
58
+ /** True when `endpoint` is a local named-pipe endpoint; the prefix is matched case-insensitively, as on Windows. */
59
+ export function isWin32PipeEndpoint(endpoint) {
60
+ const name = endpoint.slice(WIN32_PIPE_PREFIX.length);
61
+ return (win32NamesEqual(endpoint.slice(0, WIN32_PIPE_PREFIX.length), WIN32_PIPE_PREFIX) &&
62
+ name.length > 0 &&
63
+ !name.includes("\\"));
64
+ }
65
+ function hasErrorCode(error, code) {
66
+ return error instanceof Error && "code" in error && error.code === code;
67
+ }
68
+ /**
69
+ * A filesystem that behaves like Windows over a backing directory: it takes only drive-letter paths, resolves every
70
+ * segment case-insensitively while keeping the case a name was created with, and refuses to rename or delete an open
71
+ * file with Windows errors.
72
+ */
73
+ export class Win32SimFs {
74
+ #root;
75
+ #renameOpenFileError;
76
+ #deleteOpenFileError;
77
+ #openCounts = new Map();
78
+ constructor(options) {
79
+ this.#root = options.root;
80
+ this.#renameOpenFileError = options.renameOpenFileError ?? "EPERM";
81
+ this.#deleteOpenFileError = options.deleteOpenFileError ?? "EBUSY";
82
+ }
83
+ /** Maps a drive-letter path to its backing path, matching existing names case-insensitively. */
84
+ async resolve(win32Path) {
85
+ const match = DRIVE_PATH.exec(path.win32.normalize(win32Path));
86
+ if (match === null) {
87
+ throw new TypeError(`win32 sim: expected a drive-letter path such as C:\\dir\\file, got ${JSON.stringify(win32Path)}`);
88
+ }
89
+ const [, drive = "", rest = ""] = match;
90
+ const segments = rest.split("\\").filter((segment) => segment.length > 0);
91
+ let current = path.join(this.#root, drive.toUpperCase());
92
+ for (const [index, segment] of segments.entries()) {
93
+ let entries;
94
+ try {
95
+ entries = await fsp.readdir(current);
96
+ }
97
+ catch (error) {
98
+ if (hasErrorCode(error, "ENOENT") || hasErrorCode(error, "ENOTDIR")) {
99
+ return path.join(current, ...segments.slice(index));
100
+ }
101
+ throw error;
102
+ }
103
+ current = path.join(current, entries.find((entry) => win32NamesEqual(entry, segment)) ?? segment);
104
+ }
105
+ return current;
106
+ }
107
+ async mkdir(win32Path) {
108
+ await fsp.mkdir(await this.resolve(win32Path), { recursive: true });
109
+ }
110
+ async writeFile(win32Path, data) {
111
+ await fsp.writeFile(await this.resolve(win32Path), data);
112
+ }
113
+ async readFile(win32Path) {
114
+ return fsp.readFile(await this.resolve(win32Path));
115
+ }
116
+ /** Names in a directory, in the case each was created with. */
117
+ async readdir(win32Path) {
118
+ return (await fsp.readdir(await this.resolve(win32Path))).sort();
119
+ }
120
+ async exists(win32Path) {
121
+ try {
122
+ await fsp.stat(await this.resolve(win32Path));
123
+ return true;
124
+ }
125
+ catch (error) {
126
+ if (hasErrorCode(error, "ENOENT"))
127
+ return false;
128
+ throw error;
129
+ }
130
+ }
131
+ /** Opens a file; until `close()` it cannot be renamed or deleted. */
132
+ async open(win32Path, flags = "r") {
133
+ const backing = await this.resolve(win32Path);
134
+ const handle = await fsp.open(backing, flags);
135
+ this.#openCounts.set(backing, (this.#openCounts.get(backing) ?? 0) + 1);
136
+ let closed = false;
137
+ return {
138
+ handle,
139
+ close: async () => {
140
+ if (closed)
141
+ return;
142
+ closed = true;
143
+ const remaining = (this.#openCounts.get(backing) ?? 1) - 1;
144
+ if (remaining === 0)
145
+ this.#openCounts.delete(backing);
146
+ else
147
+ this.#openCounts.set(backing, remaining);
148
+ await handle.close();
149
+ },
150
+ };
151
+ }
152
+ /** Renames or replaces a file; a rename that only changes a name's case keeps one entry with the new case. */
153
+ async rename(from, to) {
154
+ const source = await this.resolve(from);
155
+ let target = await this.resolve(to);
156
+ if (this.#isOpen(source) || this.#isOpen(target))
157
+ throw win32Error(this.#renameOpenFileError, "rename", from, to);
158
+ if (target === source)
159
+ target = path.join(path.dirname(source), path.win32.basename(path.win32.normalize(to)));
160
+ await fsp.rename(source, target);
161
+ }
162
+ async unlink(win32Path) {
163
+ const backing = await this.resolve(win32Path);
164
+ if (this.#isOpen(backing))
165
+ throw win32Error(this.#deleteOpenFileError, "unlink", win32Path);
166
+ await fsp.unlink(backing);
167
+ }
168
+ /** Like `fs.rm` with `recursive`; fails with the delete error while any file at or under the path is open. */
169
+ async rm(win32Path) {
170
+ const backing = await this.resolve(win32Path);
171
+ const inside = `${backing}${path.sep}`;
172
+ for (const open of this.#openCounts.keys()) {
173
+ if (open === backing || open.startsWith(inside))
174
+ throw win32Error(this.#deleteOpenFileError, "rm", win32Path);
175
+ }
176
+ await fsp.rm(backing, { recursive: true });
177
+ }
178
+ #isOpen(backing) {
179
+ return this.#openCounts.has(backing);
180
+ }
181
+ }
182
+ /** The signals Node accepts on Windows; every one of them terminates the process outright. */
183
+ const WIN32_KILL_SIGNALS = new Set(["SIGKILL", "SIGTERM", "SIGINT", "SIGQUIT"]);
184
+ /**
185
+ * Sends `signal` with Windows semantics. Signal `0` only probes liveness. A supported signal terminates the process
186
+ * immediately, so no signal handler runs (the POSIX child is sent `SIGKILL` and reports it). Any other signal throws
187
+ * `ENOSYS` and leaves the process running.
188
+ */
189
+ export function win32Kill(target, signal = "SIGTERM") {
190
+ if (signal !== 0 && !WIN32_KILL_SIGNALS.has(signal))
191
+ throw win32Error("ENOSYS", "kill");
192
+ const posixSignal = signal === 0 ? 0 : "SIGKILL";
193
+ if (typeof target === "number")
194
+ return process.kill(target, posixSignal);
195
+ return target.kill(posixSignal);
196
+ }
197
+ //# sourceMappingURL=win32-sim.js.map
@@ -0,0 +1,19 @@
1
+ import { type LocalEndpoint } from "./local-endpoint.ts";
2
+ import type { CarrierSide } from "./transport.ts";
3
+ /** One dialed link between two carrier sides; it can drop and re-attach. */
4
+ export interface CarrierLink {
5
+ connect(): void;
6
+ drop(): void;
7
+ readonly connected: boolean;
8
+ }
9
+ export interface InProcessNetworkOptions {
10
+ /** Runs one message hand-off later; default `queueMicrotask`. Tests pass manual timers. */
11
+ readonly schedule?: (deliver: () => void) => void;
12
+ }
13
+ /** In-process carriers addressed by local endpoints, so fused and local runs need no socket. */
14
+ export interface InProcessNetwork {
15
+ listen(endpoint: LocalEndpoint, side: CarrierSide): () => void;
16
+ dial(endpoint: LocalEndpoint, side: CarrierSide): CarrierLink;
17
+ }
18
+ export declare function createInProcessNetwork(options?: InProcessNetworkOptions): InProcessNetwork;
19
+ //# sourceMappingURL=in-process-carrier.d.ts.map
@@ -0,0 +1,63 @@
1
+ import { endpointKey } from "./local-endpoint.js";
2
+ function createLink(a, b, schedule) {
3
+ let generation = 0;
4
+ let up = false;
5
+ const drop = () => {
6
+ if (!up)
7
+ return;
8
+ up = false;
9
+ generation += 1;
10
+ a.detached();
11
+ b.detached();
12
+ };
13
+ const half = (to, at) => ({
14
+ send: (message) => {
15
+ schedule(() => {
16
+ if (up && generation === at)
17
+ to.message(message);
18
+ });
19
+ return true;
20
+ },
21
+ close: () => {
22
+ if (generation === at)
23
+ drop();
24
+ },
25
+ });
26
+ return {
27
+ connect() {
28
+ if (up)
29
+ return;
30
+ up = true;
31
+ generation += 1;
32
+ a.attached(half(b, generation));
33
+ b.attached(half(a, generation));
34
+ },
35
+ drop,
36
+ get connected() {
37
+ return up;
38
+ },
39
+ };
40
+ }
41
+ export function createInProcessNetwork(options = {}) {
42
+ const schedule = options.schedule ?? queueMicrotask;
43
+ const listeners = new Map();
44
+ return {
45
+ listen(endpoint, side) {
46
+ const key = endpointKey(endpoint);
47
+ if (listeners.has(key))
48
+ throw new Error(`in-process carrier: ${endpoint.address} already has a listener`);
49
+ listeners.set(key, { side, dialed: false });
50
+ return () => void listeners.delete(key);
51
+ },
52
+ dial(endpoint, side) {
53
+ const listener = listeners.get(endpointKey(endpoint));
54
+ if (listener === undefined)
55
+ throw new Error(`in-process carrier: no listener at ${endpoint.address}`);
56
+ if (listener.dialed)
57
+ throw new Error(`in-process carrier: ${endpoint.address} is already dialed`);
58
+ listener.dialed = true;
59
+ return createLink(side, listener.side, schedule);
60
+ },
61
+ };
62
+ }
63
+ //# sourceMappingURL=in-process-carrier.js.map
@@ -0,0 +1,13 @@
1
+ /** Where a local transport listens: an owner-only Unix socket on POSIX, a named pipe (no file, no mode) on Windows. */
2
+ export type LocalEndpoint = {
3
+ readonly kind: "unix-socket";
4
+ readonly address: string;
5
+ readonly mode: number;
6
+ } | {
7
+ readonly kind: "named-pipe";
8
+ readonly address: string;
9
+ };
10
+ export declare function localEndpoint(platform: string, name: string, socketDir: string): LocalEndpoint;
11
+ /** The identity of an endpoint: Windows pipe names compare case-insensitively, socket paths exactly. */
12
+ export declare function endpointKey(endpoint: LocalEndpoint): string;
13
+ //# sourceMappingURL=local-endpoint.d.ts.map
@@ -0,0 +1,20 @@
1
+ const PIPE_PREFIX = "\\\\.\\pipe\\";
2
+ const PIPE_MAX_LENGTH = 256;
3
+ const OWNER_ONLY = 0o600;
4
+ export function localEndpoint(platform, name, socketDir) {
5
+ if (name.length === 0 || /[\\/]/.test(name)) {
6
+ throw new RangeError(`local endpoint: a name must be non-empty with no path separator, got ${JSON.stringify(name)}`);
7
+ }
8
+ if (platform === "win32") {
9
+ const address = `${PIPE_PREFIX}${name}`;
10
+ if (address.length > PIPE_MAX_LENGTH)
11
+ throw new RangeError(`local endpoint: a pipe name is at most ${PIPE_MAX_LENGTH} characters`);
12
+ return { kind: "named-pipe", address };
13
+ }
14
+ return { kind: "unix-socket", address: `${socketDir.replace(/\/+$/, "")}/${name}.sock`, mode: OWNER_ONLY };
15
+ }
16
+ /** The identity of an endpoint: Windows pipe names compare case-insensitively, socket paths exactly. */
17
+ export function endpointKey(endpoint) {
18
+ return endpoint.kind === "named-pipe" ? endpoint.address.toUpperCase() : endpoint.address;
19
+ }
20
+ //# sourceMappingURL=local-endpoint.js.map
@@ -0,0 +1,40 @@
1
+ import { type TransportClock, type TransportFrame, type TransportSide } from "./transport.ts";
2
+ /** The reference transport's wire messages: per-lane sequence numbers from 1, and cumulative ACKs of delivered frames. */
3
+ export type ReferencePacket = {
4
+ readonly kind: "data";
5
+ readonly lane: string;
6
+ readonly seq: number;
7
+ readonly payload: Uint8Array;
8
+ } | {
9
+ readonly kind: "ack";
10
+ readonly lane: string;
11
+ readonly seq: number;
12
+ };
13
+ export interface ReliableTransportOptions {
14
+ readonly clock: TransportClock;
15
+ /** Frames per lane that may be sent and unacknowledged at once. Default 32. */
16
+ readonly window?: number;
17
+ /**
18
+ * Without ACK progress on a lane for this long, that lane's unacknowledged frames are sent again from its last ACK.
19
+ * Each lane keeps its own deadline, so progress on one lane never postpones another's retransmit. Default 1000.
20
+ */
21
+ readonly retransmitMs?: number;
22
+ /** A receiver that throws: the frame still counts as delivered (it is never delivered twice). */
23
+ readonly onHandlerError: (error: unknown, frame: TransportFrame) => void;
24
+ }
25
+ /** The decoder a conformance host uses to watch the reference transport's wire. */
26
+ export declare function decodeReferencePacket(message: unknown): {
27
+ readonly kind: "data" | "ack";
28
+ readonly lane: string;
29
+ readonly seq: number;
30
+ } | {
31
+ readonly kind: "other";
32
+ };
33
+ /**
34
+ * The reference transport: per-lane send windows (backpressure), round-robin transmission across lanes (fairness),
35
+ * cumulative ACKs sent only after the receiver's handler settles, and go-back-N from each lane's last ACK on re-attach or
36
+ * on that lane's own retransmit timeout. A receiver drops anything that is not the next frame of its lane, so it delivers each frame
37
+ * once and in order over any carrier that may drop, lose, delay or reorder messages.
38
+ */
39
+ export declare function createReliableTransport(options: ReliableTransportOptions): TransportSide;
40
+ //# sourceMappingURL=reliable-transport.d.ts.map
@@ -0,0 +1,232 @@
1
+ import { TransportClosedError, } from "./transport.js";
2
+ function isReferencePacket(message) {
3
+ if (typeof message !== "object" || message === null)
4
+ return false;
5
+ const { kind, lane, seq } = message;
6
+ if (typeof lane !== "string" || typeof seq !== "number" || !Number.isSafeInteger(seq) || seq < 0)
7
+ return false;
8
+ if (kind === "ack")
9
+ return true;
10
+ return kind === "data" && message.payload instanceof Uint8Array;
11
+ }
12
+ /** The decoder a conformance host uses to watch the reference transport's wire. */
13
+ export function decodeReferencePacket(message) {
14
+ return isReferencePacket(message) ? { kind: message.kind, lane: message.lane, seq: message.seq } : { kind: "other" };
15
+ }
16
+ /**
17
+ * The reference transport: per-lane send windows (backpressure), round-robin transmission across lanes (fairness),
18
+ * cumulative ACKs sent only after the receiver's handler settles, and go-back-N from each lane's last ACK on re-attach or
19
+ * on that lane's own retransmit timeout. A receiver drops anything that is not the next frame of its lane, so it delivers each frame
20
+ * once and in order over any carrier that may drop, lose, delay or reorder messages.
21
+ */
22
+ export function createReliableTransport(options) {
23
+ const window = options.window ?? 32;
24
+ const retransmitMs = options.retransmitMs ?? 1000;
25
+ const outLanes = new Map();
26
+ const inLanes = new Map();
27
+ const order = [];
28
+ const ackDue = new Set();
29
+ let next = 0;
30
+ let connection;
31
+ let writable = false;
32
+ let closed = false;
33
+ let handler;
34
+ const outLane = (lane) => {
35
+ let state = outLanes.get(lane);
36
+ if (state === undefined) {
37
+ state = { nextSeq: 1, acked: 0, transmitted: 0, outbox: new Map(), waiters: [], cancelRetransmit: undefined };
38
+ outLanes.set(lane, state);
39
+ order.push(lane);
40
+ }
41
+ return state;
42
+ };
43
+ const inLane = (lane) => {
44
+ let state = inLanes.get(lane);
45
+ if (state === undefined) {
46
+ state = { received: 0, delivered: 0, busy: false, inbox: [] };
47
+ inLanes.set(lane, state);
48
+ }
49
+ return state;
50
+ };
51
+ const admit = (state) => {
52
+ while (state.nextSeq - 1 - state.acked < window) {
53
+ const waiter = state.waiters.shift();
54
+ if (waiter === undefined)
55
+ return;
56
+ state.outbox.set(state.nextSeq, waiter.payload);
57
+ state.nextSeq += 1;
58
+ waiter.resolve();
59
+ }
60
+ };
61
+ const emit = (packet) => {
62
+ if (connection === undefined || !writable)
63
+ return false;
64
+ if (connection.send(packet))
65
+ return true;
66
+ writable = false;
67
+ return false;
68
+ };
69
+ const disarm = (state) => {
70
+ state.cancelRetransmit?.();
71
+ state.cancelRetransmit = undefined;
72
+ };
73
+ const disarmAll = () => {
74
+ for (const state of outLanes.values())
75
+ disarm(state);
76
+ };
77
+ const arm = () => {
78
+ if (connection === undefined)
79
+ return;
80
+ for (const state of outLanes.values()) {
81
+ if (state.cancelRetransmit !== undefined || state.outbox.size === 0)
82
+ continue;
83
+ state.cancelRetransmit = options.clock.setTimer(() => {
84
+ state.cancelRetransmit = undefined;
85
+ state.transmitted = state.acked;
86
+ pump();
87
+ }, retransmitMs);
88
+ }
89
+ };
90
+ function pump() {
91
+ if (connection === undefined || !writable)
92
+ return;
93
+ for (const lane of [...ackDue]) {
94
+ if (!emit({ kind: "ack", lane, seq: inLane(lane).delivered }))
95
+ return;
96
+ ackDue.delete(lane);
97
+ }
98
+ for (let idle = 0; idle < order.length;) {
99
+ const lane = order[next % order.length];
100
+ if (lane === undefined)
101
+ return;
102
+ const state = outLane(lane);
103
+ const seq = state.transmitted + 1;
104
+ const payload = state.outbox.get(seq);
105
+ if (payload === undefined) {
106
+ next += 1;
107
+ idle += 1;
108
+ continue;
109
+ }
110
+ if (!emit({ kind: "data", lane, seq, payload }))
111
+ return;
112
+ state.transmitted = seq;
113
+ next += 1;
114
+ idle = 0;
115
+ }
116
+ arm();
117
+ }
118
+ async function deliver(state, lane) {
119
+ if (state.busy || handler === undefined)
120
+ return;
121
+ state.busy = true;
122
+ try {
123
+ for (let frame = state.inbox.shift(); frame !== undefined && !closed; frame = state.inbox.shift()) {
124
+ try {
125
+ await handler(frame);
126
+ }
127
+ catch (error) {
128
+ options.onHandlerError(error, frame);
129
+ }
130
+ state.delivered += 1;
131
+ ackDue.add(lane);
132
+ pump();
133
+ }
134
+ }
135
+ finally {
136
+ state.busy = false;
137
+ }
138
+ }
139
+ const onData = (packet) => {
140
+ const state = inLane(packet.lane);
141
+ if (packet.seq === state.received + 1) {
142
+ state.received = packet.seq;
143
+ state.inbox.push({ lane: packet.lane, payload: packet.payload });
144
+ void deliver(state, packet.lane);
145
+ }
146
+ else {
147
+ ackDue.add(packet.lane);
148
+ }
149
+ pump();
150
+ };
151
+ const onAck = (packet) => {
152
+ const state = outLanes.get(packet.lane);
153
+ if (state === undefined || packet.seq <= state.acked || packet.seq >= state.nextSeq)
154
+ return;
155
+ for (let seq = state.acked + 1; seq <= packet.seq; seq++)
156
+ state.outbox.delete(seq);
157
+ state.acked = packet.seq;
158
+ state.transmitted = Math.max(state.transmitted, state.acked);
159
+ disarm(state);
160
+ admit(state);
161
+ pump();
162
+ };
163
+ return {
164
+ send(lane, payload) {
165
+ if (closed)
166
+ return Promise.reject(new TransportClosedError());
167
+ const state = outLane(lane);
168
+ return new Promise((resolve, reject) => {
169
+ state.waiters.push({ payload, resolve, reject });
170
+ admit(state);
171
+ pump();
172
+ });
173
+ },
174
+ onFrame(frameHandler) {
175
+ handler = frameHandler;
176
+ for (const [lane, state] of inLanes)
177
+ void deliver(state, lane);
178
+ },
179
+ inFlight: (lane) => {
180
+ const state = outLanes.get(lane);
181
+ return state === undefined ? 0 : state.nextSeq - 1 - state.acked;
182
+ },
183
+ close() {
184
+ if (closed)
185
+ return;
186
+ closed = true;
187
+ disarmAll();
188
+ for (const state of outLanes.values()) {
189
+ for (const waiter of state.waiters.splice(0))
190
+ waiter.reject(new TransportClosedError());
191
+ }
192
+ const current = connection;
193
+ connection = undefined;
194
+ current?.close();
195
+ },
196
+ attached(newConnection) {
197
+ if (closed) {
198
+ newConnection.close();
199
+ return;
200
+ }
201
+ connection = newConnection;
202
+ writable = true;
203
+ for (const state of outLanes.values())
204
+ state.transmitted = state.acked;
205
+ for (const [lane, state] of inLanes)
206
+ if (state.delivered > 0)
207
+ ackDue.add(lane);
208
+ disarmAll();
209
+ pump();
210
+ },
211
+ message(message) {
212
+ if (!isReferencePacket(message))
213
+ return;
214
+ if (message.kind === "data")
215
+ onData(message);
216
+ else
217
+ onAck(message);
218
+ },
219
+ writable() {
220
+ if (connection === undefined)
221
+ return;
222
+ writable = true;
223
+ pump();
224
+ },
225
+ detached() {
226
+ connection = undefined;
227
+ writable = false;
228
+ disarmAll();
229
+ },
230
+ };
231
+ }
232
+ //# sourceMappingURL=reliable-transport.js.map
@@ -0,0 +1,6 @@
1
+ import type { CaseImplementation } from "../../conformance/runner.ts";
2
+ import { type TransportCaseAdapters } from "./transport-harness.ts";
3
+ type FaultCase = CaseImplementation<TransportCaseAdapters>;
4
+ export declare const FAULT_TRANSPORT_CASES: ReadonlyMap<string, FaultCase>;
5
+ export {};
6
+ //# sourceMappingURL=fault-cases.d.ts.map
@@ -0,0 +1,30 @@
1
+ import { createCarrierFault, FAULT_PROFILE_NAMES } from "./fault-transport.js";
2
+ import { allDelivered, expectExactDelivery, openLink, range } from "./transport-harness.js";
3
+ const LANES = ["a", "b", "c"];
4
+ const FRAMES = 30;
5
+ const PACE_MS = 20;
6
+ const applied = {
7
+ slow: (stats) => stats.refused > 0,
8
+ lossy: (stats) => stats.lost > 0,
9
+ laggy: (stats) => stats.reordered > 0,
10
+ flap: (stats) => stats.drops >= 2,
11
+ "drop-reattach": (stats) => stats.drops >= 2,
12
+ };
13
+ const underProfile = (name) => async (adapters) => {
14
+ const stats = { refused: 0, lost: 0, reordered: 0, drops: 0 };
15
+ const link = openLink(adapters, createCarrierFault(name, stats));
16
+ const counts = Object.fromEntries(LANES.map((lane) => [lane, FRAMES]));
17
+ const sends = [];
18
+ for (const n of range(1, FRAMES)) {
19
+ sends.push(...LANES.map((lane) => link.send(lane, n)));
20
+ await link.advance(PACE_MS);
21
+ }
22
+ await link.runUntil(allDelivered(link, counts), `FAULT-TRANSPORT ${name}: every frame delivered`, 600_000);
23
+ await link.awaitAll(sends, `FAULT-TRANSPORT ${name}`);
24
+ await link.advance(2000);
25
+ expectExactDelivery(link, counts, `FAULT-TRANSPORT ${name}`);
26
+ if (!applied[name](stats))
27
+ throw new Error(`FAULT-TRANSPORT ${name}: the profile never acted (${JSON.stringify(stats)})`);
28
+ };
29
+ export const FAULT_TRANSPORT_CASES = new Map(FAULT_PROFILE_NAMES.map((name, index) => [`FAULT-TRANSPORT-0${index + 1}`, underProfile(name)]));
30
+ //# sourceMappingURL=fault-cases.js.map
@@ -0,0 +1,44 @@
1
+ import type { ManualTimers } from "../../durable/testing/manual-timers.ts";
2
+ import type { CarrierFault } from "./transport-harness.ts";
3
+ /**
4
+ * The five QX2 fault profiles as test parameters (not product settings). The plan fixes their shape; the numbers here are
5
+ * this suite's until M0 records final values in the gate's fault-profiles.json.
6
+ */
7
+ declare const PROFILES: {
8
+ readonly slow: {
9
+ readonly perTick: 1;
10
+ readonly tickMs: 10;
11
+ };
12
+ readonly lossy: {
13
+ readonly lossRate: 0.2;
14
+ readonly seed: 7;
15
+ };
16
+ readonly laggy: {
17
+ readonly latencyMs: 40;
18
+ readonly jitterMs: 60;
19
+ readonly seed: 11;
20
+ };
21
+ readonly flap: {
22
+ readonly upMs: 100;
23
+ readonly downMs: 60;
24
+ readonly cycles: 8;
25
+ };
26
+ readonly "drop-reattach": {
27
+ readonly everyMessages: 9;
28
+ readonly reattachMs: 30;
29
+ readonly repetitions: 6;
30
+ };
31
+ };
32
+ export type FaultProfileName = keyof typeof PROFILES;
33
+ export declare const FAULT_PROFILE_NAMES: readonly FaultProfileName[];
34
+ /** How often the profile actually acted, so a case can refuse to pass on a fault that never happened. */
35
+ export interface FaultStats {
36
+ refused: number;
37
+ lost: number;
38
+ reordered: number;
39
+ drops: number;
40
+ }
41
+ /** Builds one profile's fault for the link a case opens. */
42
+ export declare function createCarrierFault(name: FaultProfileName, stats: FaultStats): (timers: ManualTimers) => CarrierFault;
43
+ export {};
44
+ //# sourceMappingURL=fault-transport.d.ts.map