@code-yeongyu/senpi-agent-core 2026.10.27-1 → 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.
Files changed (72) hide show
  1. package/dist/agent-loop.js +1 -0
  2. package/dist/agent.js +3 -1
  3. package/dist/assistant-terminal-state.js +5 -2
  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/pico3/kinds/generation.js +3 -1
  45. package/dist/harness/utils/read-folders/index.d.ts +1 -1
  46. package/dist/harness/utils/read-folders/index.js +1 -1
  47. package/dist/harness/utils/read-folders/tree-sitter/engine.d.ts +1 -1
  48. package/dist/harness/utils/read-folders/tree-sitter/engine.js +38 -19
  49. package/dist/internal-events.d.ts +1 -0
  50. package/dist/internal-events.js +1 -0
  51. package/dist/testing/win32-sim.d.ts +76 -0
  52. package/dist/testing/win32-sim.js +197 -0
  53. package/dist/transport/in-process-carrier.d.ts +19 -0
  54. package/dist/transport/in-process-carrier.js +63 -0
  55. package/dist/transport/local-endpoint.d.ts +13 -0
  56. package/dist/transport/local-endpoint.js +20 -0
  57. package/dist/transport/reliable-transport.d.ts +40 -0
  58. package/dist/transport/reliable-transport.js +232 -0
  59. package/dist/transport/testing/fault-cases.d.ts +6 -0
  60. package/dist/transport/testing/fault-cases.js +30 -0
  61. package/dist/transport/testing/fault-transport.d.ts +44 -0
  62. package/dist/transport/testing/fault-transport.js +144 -0
  63. package/dist/transport/testing/multi-cases.d.ts +6 -0
  64. package/dist/transport/testing/multi-cases.js +113 -0
  65. package/dist/transport/testing/transport-cases.d.ts +6 -0
  66. package/dist/transport/testing/transport-cases.js +60 -0
  67. package/dist/transport/testing/transport-harness.d.ts +53 -0
  68. package/dist/transport/testing/transport-harness.js +127 -0
  69. package/dist/transport/transport.d.ts +60 -0
  70. package/dist/transport/transport.js +15 -0
  71. package/dist/types.d.ts +1 -0
  72. package/package.json +11 -3
@@ -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
@@ -1,4 +1,4 @@
1
- import { AssistantMessageFrameEncoder, isRetryableAssistantError } from "@earendil-works/pi-ai";
1
+ import { AssistantMessageFrameEncoder, isRetryableAssistantError, sanitizeProviderErrorMessage, } from "@earendil-works/pi-ai";
2
2
  import { estimateContextTokens } from "@earendil-works/pi-ai/utils/estimate";
3
3
  import { planManagedEntry, prepareDraft, sameSnapshot, takeSnapshot } from "../system.js";
4
4
  import { toStored, } from "../types.js";
@@ -291,6 +291,8 @@ async function stream(cp, model, messages, rt, ctx) {
291
291
  errorMessage: String(error),
292
292
  timestamp: rt.now(),
293
293
  };
294
+ // A thrown provider error never passed the stream boundary: classify and redact it the same way (senpi#4282).
295
+ sanitizeProviderErrorMessage(terminal);
294
296
  }
295
297
  if (pending.length > 0)
296
298
  await flush();
@@ -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: "d7c502a1597e652f584877c0004ba6cde7c9393c8661ccbc388e3661ae66a634";
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: "d7c502a1597e652f584877c0004ba6cde7c9393c8661ccbc388e3661ae66a634",
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
- /** Off for tests that inject resolvers; the shipped read path always shares one loaded grammar. */
14
+ /** Controls grammar/parser sharing; runtime initialization is always shared by resolved path. */
15
15
  readonly cache?: boolean;
16
16
  };
17
17
  /**
@@ -5,32 +5,47 @@ import { foldRangesFromSyntax } from "./syntax.js";
5
5
  export const TREE_SITTER_PARSE_BUDGET_MS = 250;
6
6
  export const TREE_SITTER_FOLDER_ID = "tree-sitter-wasm";
7
7
  export const TREE_SITTER_FOLDER_VERSION = "1";
8
- let runtime;
8
+ const runtimes = new Map();
9
9
  const grammars = new Map();
10
- async function loadRuntime(resolveRuntime) {
11
- try {
10
+ // Parser.init and Language.load share process-global state, even across cache resets.
11
+ let runtimeLoads = Promise.resolve();
12
+ function loadRuntime(wasm) {
13
+ const existing = runtimes.get(wasm);
14
+ if (existing)
15
+ return existing;
16
+ const pending = runtimeLoads
17
+ .then(async () => {
12
18
  const module = await import("web-tree-sitter");
13
- const wasm = await resolveRuntime();
14
19
  await module.Parser.init(wasm ? { locateFile: () => wasm } : undefined);
15
20
  return module;
16
- }
17
- catch {
21
+ })
22
+ .catch(() => {
23
+ if (runtimes.get(wasm) === pending)
24
+ runtimes.delete(wasm);
18
25
  return undefined;
19
- }
26
+ });
27
+ runtimes.set(wasm, pending);
28
+ return pending;
20
29
  }
21
30
  async function loadParser(language, options) {
22
- const resolveGrammar = options.resolveGrammar ?? resolveGrammarPath;
23
- const resolveRuntime = options.resolveRuntime ?? resolveRuntimePath;
24
- const grammar = await resolveGrammar(language);
25
- if (!grammar)
31
+ try {
32
+ const grammar = await (options.resolveGrammar ?? resolveGrammarPath)(language);
33
+ // A missing grammar is a stable negative cache entry, not a failed load.
34
+ if (!grammar)
35
+ return null;
36
+ const wasm = await (options.resolveRuntime ?? resolveRuntimePath)();
37
+ const runtime = loadRuntime(wasm);
38
+ const pending = runtimeLoads.then(async () => {
39
+ const module = await runtime;
40
+ return module && instantiate(module, grammar, language);
41
+ });
42
+ // Reserve the whole init/grammar operation before another path can enqueue an init.
43
+ runtimeLoads = pending.then(() => undefined);
44
+ return pending;
45
+ }
46
+ catch {
26
47
  return undefined;
27
- if (options.cache === false) {
28
- const module = await loadRuntime(resolveRuntime);
29
- return module && instantiate(module, grammar, language);
30
48
  }
31
- runtime ??= loadRuntime(resolveRuntime);
32
- const module = await runtime;
33
- return module && instantiate(module, grammar, language);
34
49
  }
35
50
  async function instantiate(module, grammar, language) {
36
51
  try {
@@ -76,7 +91,11 @@ export async function loadTreeSitterFolder(language, options) {
76
91
  ? load()
77
92
  : (grammars.get(language) ??
78
93
  (() => {
79
- const pending = load();
94
+ const pending = load().then((loaded) => {
95
+ if (loaded === undefined && grammars.get(language) === pending)
96
+ grammars.delete(language);
97
+ return loaded;
98
+ });
80
99
  grammars.set(language, pending);
81
100
  return pending;
82
101
  })()));
@@ -91,7 +110,7 @@ export async function loadTreeSitterFolder(language, options) {
91
110
  }
92
111
  /** Test seam: drop the memoized runtime/grammars so a resolver change is observable. */
93
112
  export function resetTreeSitterGrammars() {
94
- runtime = undefined;
113
+ runtimes.clear();
95
114
  grammars.clear();
96
115
  }
97
116
  //# sourceMappingURL=engine.js.map
@@ -1,3 +1,4 @@
1
+ /** Internal to senpi packages; not a public API; may change without notice. */
1
2
  import { type AssistantProgress } from "@earendil-works/pi-ai";
2
3
  import type { AgentEvent, AgentLoopConfig } from "./types.ts";
3
4
  export declare const internalProgressSink: unique symbol;
@@ -1,3 +1,4 @@
1
+ /** Internal to senpi packages; not a public API; may change without notice. */
1
2
  import { subscribeAssistantCapture } from "@earendil-works/pi-ai";
2
3
  export const internalProgressSink = Symbol("internalProgressSink");
3
4
  const channels = new WeakMap();
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Win32 simulation profile for code-level Windows tests on any host (test support only; no product code imports it).
3
+ *
4
+ * It simulates five Windows behaviors:
5
+ * - `\` paths with a drive letter (`C:\dir\file`), mapped onto a backing POSIX directory;
6
+ * - case-insensitive, case-preserving names;
7
+ * - named-pipe endpoints (`\\.\pipe\<name>`), which are not filesystem paths;
8
+ * - `EBUSY`/`EPERM` on rename or delete of a file that is open;
9
+ * - signal-less process termination: a supported signal terminates at once and no handler runs.
10
+ */
11
+ import type { ChildProcess } from "node:child_process";
12
+ import * as fsp from "node:fs/promises";
13
+ type Win32ErrorCode = "EBUSY" | "EPERM" | "ENOSYS";
14
+ export type Win32OpenFileErrorCode = "EBUSY" | "EPERM";
15
+ /** The errno error shape Node throws on Windows, with `dest` for two-path calls such as rename. */
16
+ export interface Win32ErrnoException extends NodeJS.ErrnoException {
17
+ dest?: string;
18
+ }
19
+ /** Builds the error Node throws on Windows for `code` from `syscall`. */
20
+ export declare function win32Error(code: Win32ErrorCode, syscall: string, filePath?: string, dest?: string): Win32ErrnoException;
21
+ /** Compares two Windows names (path segments, paths or pipe names) the way Windows does: case-insensitively. */
22
+ export declare function win32NamesEqual(left: string, right: string): boolean;
23
+ /** Turns a relative POSIX-style path (`dir/file`) into a drive-letter Windows path (`C:\dir\file`). */
24
+ export declare function toWin32Path(relativePath: string, drive?: string): string;
25
+ /** True for an absolute drive-letter path such as `C:\dir` (forward slashes are accepted, as on Windows). */
26
+ export declare function isWin32DrivePath(candidate: string): boolean;
27
+ export declare const WIN32_PIPE_PREFIX = "\\\\.\\pipe\\";
28
+ /** Builds the named-pipe endpoint (`\\.\pipe\<name>`) a Windows transport listens on instead of a socket file. */
29
+ export declare function win32PipeEndpoint(name: string): string;
30
+ /** True when `endpoint` is a local named-pipe endpoint; the prefix is matched case-insensitively, as on Windows. */
31
+ export declare function isWin32PipeEndpoint(endpoint: string): boolean;
32
+ export interface Win32SimFsOptions {
33
+ /** Backing POSIX directory; drive `C:` maps to `<root>/C`. */
34
+ root: string;
35
+ /** Error for a rename whose source or destination is open. Default `EPERM`. */
36
+ renameOpenFileError?: Win32OpenFileErrorCode;
37
+ /** Error for an unlink or rm that would remove an open file. Default `EBUSY`. */
38
+ deleteOpenFileError?: Win32OpenFileErrorCode;
39
+ }
40
+ /** An open file in a {@link Win32SimFs}; the file counts as open until `close()`. */
41
+ export interface Win32SimFileHandle {
42
+ readonly handle: fsp.FileHandle;
43
+ close(): Promise<void>;
44
+ }
45
+ /**
46
+ * A filesystem that behaves like Windows over a backing directory: it takes only drive-letter paths, resolves every
47
+ * segment case-insensitively while keeping the case a name was created with, and refuses to rename or delete an open
48
+ * file with Windows errors.
49
+ */
50
+ export declare class Win32SimFs {
51
+ #private;
52
+ constructor(options: Win32SimFsOptions);
53
+ /** Maps a drive-letter path to its backing path, matching existing names case-insensitively. */
54
+ resolve(win32Path: string): Promise<string>;
55
+ mkdir(win32Path: string): Promise<void>;
56
+ writeFile(win32Path: string, data: string | Uint8Array): Promise<void>;
57
+ readFile(win32Path: string): Promise<Buffer>;
58
+ /** Names in a directory, in the case each was created with. */
59
+ readdir(win32Path: string): Promise<string[]>;
60
+ exists(win32Path: string): Promise<boolean>;
61
+ /** Opens a file; until `close()` it cannot be renamed or deleted. */
62
+ open(win32Path: string, flags?: string): Promise<Win32SimFileHandle>;
63
+ /** Renames or replaces a file; a rename that only changes a name's case keeps one entry with the new case. */
64
+ rename(from: string, to: string): Promise<void>;
65
+ unlink(win32Path: string): Promise<void>;
66
+ /** Like `fs.rm` with `recursive`; fails with the delete error while any file at or under the path is open. */
67
+ rm(win32Path: string): Promise<void>;
68
+ }
69
+ /**
70
+ * Sends `signal` with Windows semantics. Signal `0` only probes liveness. A supported signal terminates the process
71
+ * immediately, so no signal handler runs (the POSIX child is sent `SIGKILL` and reports it). Any other signal throws
72
+ * `ENOSYS` and leaves the process running.
73
+ */
74
+ export declare function win32Kill(target: ChildProcess | number, signal?: NodeJS.Signals | 0): boolean;
75
+ export {};
76
+ //# sourceMappingURL=win32-sim.d.ts.map
@@ -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