@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
@@ -535,6 +535,7 @@ async function streamAssistantResponse(context, config, signal, emit, streamFunc
535
535
  await emit(carryAssistantCapture(event, {
536
536
  type: "message_start",
537
537
  message: { ...partialMessage },
538
+ streaming: true,
538
539
  }));
539
540
  break;
540
541
  case "text_start":
package/dist/agent.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readProviderDiagnostic, sanitizeProviderDiagnostic, } from "@earendil-works/pi-ai";
1
+ import { readProviderDiagnostic, sanitizeProviderDiagnostic, sanitizeProviderErrorMessage, } from "@earendil-works/pi-ai";
2
2
  import { buildProviderContext as buildProviderContextFromAgentContext, runAgentLoop, runAgentLoopContinue, } from "./agent-loop.js";
3
3
  import { ProviderRetryWatchdogAbortError } from "./assistant-terminal-state.js";
4
4
  import { dispatchInternalAgent, internalLoopSink, } from "./internal-events.js";
@@ -600,6 +600,8 @@ export class Agent {
600
600
  ...(!aborted && isLocalFailure(error) ? { failureSource: "local" } : {}),
601
601
  timestamp: Date.now(),
602
602
  };
603
+ // A thrown provider error never passed the stream boundary: classify and redact it the same way (senpi#4282).
604
+ sanitizeProviderErrorMessage(failureMessage);
603
605
  await this.processEvents({ type: "message_start", message: failureMessage });
604
606
  await this.processEvents({ type: "message_end", message: failureMessage });
605
607
  await this.processEvents({ type: "turn_end", message: failureMessage, toolResults: [] });
@@ -1,4 +1,4 @@
1
- import { isClassifierRefusal, isCursorExecResolved, readProviderDiagnostic, } from "@earendil-works/pi-ai";
1
+ import { isClassifierRefusal, isCursorExecResolved, readProviderDiagnostic, sanitizeProviderErrorMessage, } from "@earendil-works/pi-ai";
2
2
  import { isOAuthRefreshUnavailableError } from "@earendil-works/pi-ai/utils/oauth-refresh-error";
3
3
  import { OAUTH_REFRESH_UNAVAILABLE_DIAGNOSTIC } from "@earendil-works/pi-ai/utils/retry";
4
4
  const EMPTY_USAGE = {
@@ -69,7 +69,7 @@ export function shouldFinalizeIdleAsStop(partialMessage, providerToolResults) {
69
69
  export function createTerminalFailureAssistantMessage(model, reason, error, partialMessage) {
70
70
  const errorMessage = error instanceof Error ? error.message : String(error);
71
71
  const providerDiagnostic = reason === "error" ? readProviderDiagnostic(error) : undefined;
72
- return {
72
+ const message = {
73
73
  role: "assistant",
74
74
  content: partialMessage?.content ?? [{ type: "text", text: "" }],
75
75
  api: partialMessage?.api ?? model.api,
@@ -94,6 +94,9 @@ export function createTerminalFailureAssistantMessage(model, reason, error, part
94
94
  ...(error instanceof ProviderRetryWatchdogAbortError ? { abortSource: "provider" } : {}),
95
95
  timestamp: partialMessage?.timestamp ?? Date.now(),
96
96
  };
97
+ // A thrown provider error never passed the stream boundary: classify and redact it the same way (senpi#4282).
98
+ sanitizeProviderErrorMessage(message);
99
+ return message;
97
100
  }
98
101
  export function normalizeTerminalAssistantMessage(message, event) {
99
102
  if (event.type === "done")
@@ -0,0 +1,42 @@
1
+ export type { ConformanceManifest, ManifestCase, MappingProblem, MappingProblemCode } from "./mapping.ts";
2
+ export { buildManifest, findImplementationGaps, findMappingProblems } from "./mapping.ts";
3
+ export type { AdapterKind, RegisteredCase } from "./registry.ts";
4
+ export { ADAPTER_KINDS, CONFORMANCE_REGISTRY } from "./registry.ts";
5
+ export type { CaseImplementation, CaseResult, ConformanceHost, HostReport } from "./runner.ts";
6
+ export { runSuite } from "./runner.ts";
7
+ export type { SuiteHashEntry } from "./suite-hash.ts";
8
+ export { computeSuiteHash, sha256Hex } from "./suite-hash.ts";
9
+ export type { CommitResult, DurableStore, DurableStoreErrorCode, DurableStoreOptions, RecordScan, StoreDocument, StoreOperation, StoreRecord, StoreSnapshot, StoreValue, StoreWrite, StoreWritePoint, } from "../durable/store.ts";
10
+ export { DurableStoreError } from "../durable/store.ts";
11
+ export type { KillDeclaration } from "../durable/testing/kill-store.ts";
12
+ export { KILL_STORE_CASES, KILL_STORE_DECLARATIONS } from "../durable/testing/kill-store.ts";
13
+ export { STORE_CASES } from "../durable/testing/store-cases.ts";
14
+ export type { StoreCaseAdapters, StoreConformanceAdapter } from "../durable/testing/store-harness.ts";
15
+ export type { KillWakeDeclaration } from "../durable/testing/kill-wake.ts";
16
+ export { KILL_WAKE_CASES, KILL_WAKE_DECLARATIONS } from "../durable/testing/kill-wake.ts";
17
+ export type { ManualTimers } from "../durable/testing/manual-timers.ts";
18
+ export { createManualTimers } from "../durable/testing/manual-timers.ts";
19
+ export { MULTI_HOST_CASES } from "../durable/testing/multi-host-cases.ts";
20
+ export { WAKE_CASES } from "../durable/testing/wake-cases.ts";
21
+ export type { WakeAlarmSession, WakeCaseAdapters, WakeConformanceAdapter } from "../durable/testing/wake-harness.ts";
22
+ export type { Wake, WakeAlarm, WakeAlarmOptions, WakeAlarmPoint, WakeHost, WakeHostOptions, WakePoint, } from "../durable/wake-host.ts";
23
+ export { createWakeHost } from "../durable/wake-host.ts";
24
+ export type { BunTimerHost, BunTimerHostOptions, SetTimer } from "../durable/wake-host-bun-timer.ts";
25
+ export { createBunTimerHost } from "../durable/wake-host-bun-timer.ts";
26
+ export type { AlarmPersistence } from "../durable/wake-host-store-alarm.ts";
27
+ export { storeAlarmPersistence } from "../durable/wake-host-store-alarm.ts";
28
+ export type { CarrierLink, InProcessNetwork, InProcessNetworkOptions } from "../transport/in-process-carrier.ts";
29
+ export { createInProcessNetwork } from "../transport/in-process-carrier.ts";
30
+ export type { LocalEndpoint } from "../transport/local-endpoint.ts";
31
+ export { localEndpoint } from "../transport/local-endpoint.ts";
32
+ export type { ReferencePacket, ReliableTransportOptions } from "../transport/reliable-transport.ts";
33
+ export { createReliableTransport, decodeReferencePacket } from "../transport/reliable-transport.ts";
34
+ export { FAULT_TRANSPORT_CASES } from "../transport/testing/fault-cases.ts";
35
+ export type { FaultProfileName } from "../transport/testing/fault-transport.ts";
36
+ export { FAULT_PROFILE_NAMES } from "../transport/testing/fault-transport.ts";
37
+ export { MULTI_CASES } from "../transport/testing/multi-cases.ts";
38
+ export { TRANSPORT_CASES } from "../transport/testing/transport-cases.ts";
39
+ export type { CarrierFault, TransportCaseAdapters, TransportConformanceAdapter, WireView, } from "../transport/testing/transport-harness.ts";
40
+ export type { CarrierConnection, CarrierSide, FrameHandler, Transport, TransportClock, TransportFrame, TransportSide, } from "../transport/transport.ts";
41
+ export { TransportClosedError } from "../transport/transport.ts";
42
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,23 @@
1
+ export { buildManifest, findImplementationGaps, findMappingProblems } from "./mapping.js";
2
+ export { ADAPTER_KINDS, CONFORMANCE_REGISTRY } from "./registry.js";
3
+ export { runSuite } from "./runner.js";
4
+ export { computeSuiteHash, sha256Hex } from "./suite-hash.js";
5
+ export { DurableStoreError } from "../durable/store.js";
6
+ export { KILL_STORE_CASES, KILL_STORE_DECLARATIONS } from "../durable/testing/kill-store.js";
7
+ export { STORE_CASES } from "../durable/testing/store-cases.js";
8
+ export { KILL_WAKE_CASES, KILL_WAKE_DECLARATIONS } from "../durable/testing/kill-wake.js";
9
+ export { createManualTimers } from "../durable/testing/manual-timers.js";
10
+ export { MULTI_HOST_CASES } from "../durable/testing/multi-host-cases.js";
11
+ export { WAKE_CASES } from "../durable/testing/wake-cases.js";
12
+ export { createWakeHost } from "../durable/wake-host.js";
13
+ export { createBunTimerHost } from "../durable/wake-host-bun-timer.js";
14
+ export { storeAlarmPersistence } from "../durable/wake-host-store-alarm.js";
15
+ export { createInProcessNetwork } from "../transport/in-process-carrier.js";
16
+ export { localEndpoint } from "../transport/local-endpoint.js";
17
+ export { createReliableTransport, decodeReferencePacket } from "../transport/reliable-transport.js";
18
+ export { FAULT_TRANSPORT_CASES } from "../transport/testing/fault-cases.js";
19
+ export { FAULT_PROFILE_NAMES } from "../transport/testing/fault-transport.js";
20
+ export { MULTI_CASES } from "../transport/testing/multi-cases.js";
21
+ export { TRANSPORT_CASES } from "../transport/testing/transport-cases.js";
22
+ export { TransportClosedError } from "../transport/transport.js";
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,24 @@
1
+ import type { RegisteredCase } from "./registry.ts";
2
+ export type MappingProblemCode = "duplicate-id" | "malformed-id" | "malformed-impl" | "missing-intro" | "missing-adapters" | "duplicate-adapter" | "missing-expected";
3
+ export interface MappingProblem {
4
+ readonly id: string;
5
+ readonly code: MappingProblemCode;
6
+ }
7
+ export interface ManifestCase extends RegisteredCase {
8
+ readonly impl_hash: string;
9
+ }
10
+ /** The exported suite: what every host runs and every host report is compared against. */
11
+ export interface ConformanceManifest {
12
+ readonly suite_hash: string;
13
+ readonly cases: readonly ManifestCase[];
14
+ }
15
+ /** Every reason the registry is not a complete, unambiguous mapping; empty when it is. */
16
+ export declare function findMappingProblems(cases: readonly RegisteredCase[]): MappingProblem[];
17
+ /** Registered cases with no implementation, and implementations with no registered case. */
18
+ export declare function findImplementationGaps(cases: readonly Pick<RegisteredCase, "id">[], implementations: ReadonlyMap<string, unknown>): {
19
+ unimplemented: string[];
20
+ unregistered: string[];
21
+ };
22
+ /** Build the manifest export, hashing each implementation file through `readImpl` (repository-relative path). */
23
+ export declare function buildManifest(cases: readonly RegisteredCase[], readImpl: (impl: string) => Promise<Uint8Array>): Promise<ConformanceManifest>;
24
+ //# sourceMappingURL=mapping.d.ts.map
@@ -0,0 +1,62 @@
1
+ import { computeSuiteHash, sha256Hex } from "./suite-hash.js";
2
+ const CASE_ID = /^[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+$/;
3
+ const isRepoRelativeTs = (path) => path.endsWith(".ts") && !path.startsWith("/") && !path.includes("\\") && !path.split("/").includes("..");
4
+ function problemsOf(entry) {
5
+ const codes = [];
6
+ if (!CASE_ID.test(entry.id))
7
+ codes.push("malformed-id");
8
+ if (!isRepoRelativeTs(entry.impl))
9
+ codes.push("malformed-impl");
10
+ if (entry.intro.trim() === "")
11
+ codes.push("missing-intro");
12
+ if (entry.adapters.length === 0)
13
+ codes.push("missing-adapters");
14
+ if (new Set(entry.adapters).size !== entry.adapters.length)
15
+ codes.push("duplicate-adapter");
16
+ if (entry.expected.trim() === "")
17
+ codes.push("missing-expected");
18
+ return codes;
19
+ }
20
+ /** Every reason the registry is not a complete, unambiguous mapping; empty when it is. */
21
+ export function findMappingProblems(cases) {
22
+ const problems = [];
23
+ const seen = new Set();
24
+ for (const entry of cases) {
25
+ if (seen.has(entry.id))
26
+ problems.push({ id: entry.id, code: "duplicate-id" });
27
+ seen.add(entry.id);
28
+ for (const code of problemsOf(entry))
29
+ problems.push({ id: entry.id, code });
30
+ }
31
+ return problems;
32
+ }
33
+ /** Registered cases with no implementation, and implementations with no registered case. */
34
+ export function findImplementationGaps(cases, implementations) {
35
+ const registered = new Set(cases.map((entry) => entry.id));
36
+ return {
37
+ unimplemented: [...registered].filter((id) => !implementations.has(id)),
38
+ unregistered: [...implementations.keys()].filter((id) => !registered.has(id)),
39
+ };
40
+ }
41
+ /** Build the manifest export, hashing each implementation file through `readImpl` (repository-relative path). */
42
+ export async function buildManifest(cases, readImpl) {
43
+ const problems = findMappingProblems(cases);
44
+ if (problems.length > 0) {
45
+ throw new Error(`conformance registry mapping problems: ${problems.map((p) => `${p.id} ${p.code}`).join(", ")}`);
46
+ }
47
+ const manifestCases = [];
48
+ for (const entry of cases) {
49
+ let bytes;
50
+ try {
51
+ bytes = await readImpl(entry.impl);
52
+ }
53
+ catch (error) {
54
+ throw new Error(`conformance case ${entry.id}: cannot read its implementation ${entry.impl}`, {
55
+ cause: error,
56
+ });
57
+ }
58
+ manifestCases.push({ ...entry, impl_hash: await sha256Hex(bytes) });
59
+ }
60
+ return { suite_hash: await computeSuiteHash(manifestCases), cases: manifestCases };
61
+ }
62
+ //# sourceMappingURL=mapping.js.map
@@ -0,0 +1,15 @@
1
+ export declare const ADAPTER_KINDS: readonly ["store", "wake", "transport", "executor", "host"];
2
+ export type AdapterKind = (typeof ADAPTER_KINDS)[number];
3
+ /** One conformance case: where it is implemented, which change introduced it, what it needs, and what it proves. */
4
+ export interface RegisteredCase {
5
+ /** Unique upper-case ID, for example `STORE-01` or `KILL-STORE-01`. */
6
+ readonly id: string;
7
+ /** Repository-relative path of the TypeScript file that implements the case. */
8
+ readonly impl: string;
9
+ /** The change that introduced the case (a pull request such as `#4727`). */
10
+ readonly intro: string;
11
+ readonly adapters: readonly AdapterKind[];
12
+ readonly expected: string;
13
+ }
14
+ export declare const CONFORMANCE_REGISTRY: readonly RegisteredCase[];
15
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1,75 @@
1
+ export const ADAPTER_KINDS = ["store", "wake", "transport", "executor", "host"];
2
+ /**
3
+ * Every case of the durable-core conformance suite (packages/agent/docs/durable-core-adr.md). A change that adds cases
4
+ * registers each of them here, with its mapping, in the same change.
5
+ */
6
+ const STORE_CASES_IMPL = "packages/agent/src/durable/testing/store-cases.ts";
7
+ const KILL_STORE_IMPL = "packages/agent/src/durable/testing/kill-store.ts";
8
+ const WAKE_CASES_IMPL = "packages/agent/src/durable/testing/wake-cases.ts";
9
+ const MULTI_HOST_IMPL = "packages/agent/src/durable/testing/multi-host-cases.ts";
10
+ const KILL_WAKE_IMPL = "packages/agent/src/durable/testing/kill-wake.ts";
11
+ const TRANSPORT_CASES_IMPL = "packages/agent/src/transport/testing/transport-cases.ts";
12
+ const MULTI_CASES_IMPL = "packages/agent/src/transport/testing/multi-cases.ts";
13
+ const FAULT_CASES_IMPL = "packages/agent/src/transport/testing/fault-cases.ts";
14
+ const transportCase = (id, impl, expected) => ({
15
+ id,
16
+ impl,
17
+ intro: "#4741",
18
+ adapters: ["transport"],
19
+ expected,
20
+ });
21
+ const faultCase = (n, profile) => transportCase(`FAULT-TRANSPORT-0${n}`, FAULT_CASES_IMPL, `${profile} profile: lane order kept, committed frames delivered once, resume from the last ACK`);
22
+ const wakeCase = (id, impl, expected) => ({
23
+ id,
24
+ impl,
25
+ intro: "#4735",
26
+ adapters: ["store", "wake"],
27
+ expected,
28
+ });
29
+ const storeCase = (id, impl, expected) => ({
30
+ id,
31
+ impl,
32
+ intro: "#4730",
33
+ adapters: ["store"],
34
+ expected,
35
+ });
36
+ export const CONFORMANCE_REGISTRY = [
37
+ storeCase("STORE-01", STORE_CASES_IMPL, "a commit adopts once, after its batch is durable; every instance sees each batch whole"),
38
+ storeCase("STORE-02", STORE_CASES_IMPL, "a cut at every write point of a commit leaves nothing of it durable, with no adopt and no seq burned"),
39
+ storeCase("STORE-03", STORE_CASES_IMPL, "concurrent admissions run one at a time in admission order; a refused mutation releases the line"),
40
+ storeCase("STORE-04", STORE_CASES_IMPL, "an old owner's delayed commit or claim after the new owner's first commit is refused and changes nothing"),
41
+ storeCase("STORE-05", STORE_CASES_IMPL, "claim + import across hosts is whole, idempotent and tail-only; a diverged log is refused atomically"),
42
+ storeCase("STORE-06", STORE_CASES_IMPL, "minted IDs are unique; record and document scans, deletes and duplicate-ID refusal behave as specified"),
43
+ storeCase("STORE-07", STORE_CASES_IMPL, "a claim raises the epoch by exactly one; a claim that skips an epoch is refused and changes nothing"),
44
+ storeCase("KILL-STORE-01", KILL_STORE_IMPL, "everywhere: kill before a commit; after restart nothing is durable and the batch commits with the same IDs"),
45
+ storeCase("KILL-STORE-02", KILL_STORE_IMPL, "everywhere: kill after a commit is durable; after restart it is whole, stable IDs, and a retry applies nothing"),
46
+ storeCase("KILL-STORE-03", KILL_STORE_IMPL, "everywhere: kill before a claim; after restart the epoch is unchanged and the claim succeeds"),
47
+ storeCase("KILL-STORE-04", KILL_STORE_IMPL, "everywhere: kill after a claim is durable; after restart the new epoch holds and the old one is fenced"),
48
+ storeCase("KILL-STORE-05", KILL_STORE_IMPL, "everywhere: kill before an import; after restart nothing is imported and the import succeeds"),
49
+ storeCase("KILL-STORE-06", KILL_STORE_IMPL, "everywhere: kill after an import is durable; after restart it is whole and a re-import adds nothing"),
50
+ wakeCase("WAKE-01", WAKE_CASES_IMPL, "pre-arm: a recovery alarm is durable before admitted work starts and before a new instance resumes"),
51
+ wakeCase("WAKE-02", WAKE_CASES_IMPL, "update: after each commit the alarm is min(heartbeat if active, earliest wake), cleared only when idle"),
52
+ wakeCase("WAKE-03", WAKE_CASES_IMPL, "resume: a fire re-arms before resume, runs due work once, and an early fire is harmless"),
53
+ wakeCase("WAKE-RET-01", WAKE_CASES_IMPL, "retention: an alarm survives host restarts and reopened instances, fires once, and is cleared after the work"),
54
+ wakeCase("MULTI-HOST-01", MULTI_HOST_IMPL, "many sessions on one host instance: records and wakes stay per session"),
55
+ wakeCase("KILL-WAKE-01", KILL_WAKE_IMPL, "everywhere: kill during admit's alarm write; nothing is durable and a retried admission runs once"),
56
+ wakeCase("KILL-WAKE-02", KILL_WAKE_IMPL, "everywhere: kill after admit's arm, before work; the alarm fires harmlessly and ends cleared"),
57
+ wakeCase("KILL-WAKE-03", KILL_WAKE_IMPL, "everywhere: kill between a commit and its alarm update; the pre-armed heartbeat recovers the wake, run once"),
58
+ wakeCase("KILL-WAKE-04", KILL_WAKE_IMPL, "everywhere: eviction during the update's alarm write; the heartbeat recovers the wake, run once"),
59
+ wakeCase("KILL-WAKE-05", KILL_WAKE_IMPL, "everywhere: kill after a fire's re-arm, before resume; the next fire runs the due work once"),
60
+ wakeCase("KILL-WAKE-06", KILL_WAKE_IMPL, "everywhere: kill after a resumed task's commit is durable; the task never runs twice and the alarm clears"),
61
+ wakeCase("KILL-WAKE-07", KILL_WAKE_IMPL, "everywhere: eviction during the alarm clear after the work; the next fire is harmless and clears it"),
62
+ wakeCase("KILL-WAKE-08", KILL_WAKE_IMPL, "everywhere: a concurrent fire's updates cannot clear an uncommitted admission's heartbeat; a kill after its commit recovers the wake, run once"),
63
+ transportCase("TRANSPORT-01", TRANSPORT_CASES_IMPL, "order: frames on each lane are delivered in send order, each once"),
64
+ transportCase("TRANSPORT-02", TRANSPORT_CASES_IMPL, "resume: after the carrier drops and re-attaches, each lane continues from its last ACK; nothing lost or repeated"),
65
+ transportCase("TRANSPORT-03", TRANSPORT_CASES_IMPL, "backpressure: a stalled receiver bounds its lane's senders; sends resolve in order once it drains"),
66
+ transportCase("MULTI-LANE-01", MULTI_CASES_IMPL, "N scopes over one transport keep per-scope order and interleave"),
67
+ transportCase("MULTI-FAIR-01", MULTI_CASES_IMPL, "a stalled or long lane in A does not starve B: B is delivered within a bounded number of A's frames"),
68
+ transportCase("MULTI-FAIR-02", MULTI_CASES_IMPL, "a frame lost on one lane is resent on that lane's own deadline: another lane's ACK progress never postpones it"),
69
+ faultCase(1, "slow"),
70
+ faultCase(2, "lossy"),
71
+ faultCase(3, "laggy"),
72
+ faultCase(4, "flap"),
73
+ faultCase(5, "drop-reattach"),
74
+ ];
75
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1,27 @@
1
+ import type { ConformanceManifest } from "./mapping.ts";
2
+ import type { AdapterKind } from "./registry.ts";
3
+ /** A host under test: its name in reports, the adapter kinds it provides, and the adapters handed to each case. */
4
+ export interface ConformanceHost<TAdapters> {
5
+ readonly name: string;
6
+ readonly provides: readonly AdapterKind[];
7
+ readonly adapters: TAdapters;
8
+ }
9
+ /** A case passes when it resolves and fails when it rejects. */
10
+ export type CaseImplementation<TAdapters> = (adapters: TAdapters) => Promise<void>;
11
+ export type CaseResult = {
12
+ readonly id: string;
13
+ readonly status: "pass";
14
+ } | {
15
+ readonly id: string;
16
+ readonly status: "fail";
17
+ readonly error: string;
18
+ };
19
+ /** One host's run of the manifest. There is no skip: a case that cannot run on the host fails. */
20
+ export interface HostReport {
21
+ readonly host: string;
22
+ readonly suite_hash: string;
23
+ readonly results: readonly CaseResult[];
24
+ }
25
+ /** Run every manifest case on `host`, one at a time in manifest order, and report under the manifest's suite hash. */
26
+ export declare function runSuite<TAdapters>(manifest: ConformanceManifest, host: ConformanceHost<TAdapters>, implementations: ReadonlyMap<string, CaseImplementation<TAdapters>>): Promise<HostReport>;
27
+ //# sourceMappingURL=runner.d.ts.map
@@ -0,0 +1,23 @@
1
+ async function runCase(id, adapters, host, implementation) {
2
+ if (implementation === undefined)
3
+ return { id, status: "fail", error: "no implementation is registered for this case" };
4
+ const missing = adapters.filter((kind) => !host.provides.includes(kind));
5
+ if (missing.length > 0)
6
+ return { id, status: "fail", error: `the host does not provide adapter: ${missing.join(", ")}` };
7
+ try {
8
+ await implementation(host.adapters);
9
+ return { id, status: "pass" };
10
+ }
11
+ catch (error) {
12
+ return { id, status: "fail", error: error instanceof Error ? error.message : String(error) };
13
+ }
14
+ }
15
+ /** Run every manifest case on `host`, one at a time in manifest order, and report under the manifest's suite hash. */
16
+ export async function runSuite(manifest, host, implementations) {
17
+ const results = [];
18
+ for (const entry of manifest.cases) {
19
+ results.push(await runCase(entry.id, entry.adapters, host, implementations.get(entry.id)));
20
+ }
21
+ return { host: host.name, suite_hash: manifest.suite_hash, results };
22
+ }
23
+ //# sourceMappingURL=runner.js.map
@@ -0,0 +1,12 @@
1
+ export interface SuiteHashEntry {
2
+ readonly id: string;
3
+ readonly impl_hash: string;
4
+ }
5
+ /** Lower-case hex SHA-256 through Web Crypto, so the same code runs on Bun, Node and a Durable Object. */
6
+ export declare function sha256Hex(bytes: Uint8Array): Promise<string>;
7
+ /**
8
+ * The suite hash: SHA-256 over one `id<TAB>impl_hash` line per case, sorted by ID. It changes when a case is added,
9
+ * removed or renamed, or when any case implementation changes, and never with registration order.
10
+ */
11
+ export declare function computeSuiteHash(entries: readonly SuiteHashEntry[]): Promise<string>;
12
+ //# sourceMappingURL=suite-hash.d.ts.map
@@ -0,0 +1,20 @@
1
+ /** Lower-case hex SHA-256 through Web Crypto, so the same code runs on Bun, Node and a Durable Object. */
2
+ export async function sha256Hex(bytes) {
3
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array(bytes)));
4
+ return Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
5
+ }
6
+ /**
7
+ * The suite hash: SHA-256 over one `id<TAB>impl_hash` line per case, sorted by ID. It changes when a case is added,
8
+ * removed or renamed, or when any case implementation changes, and never with registration order.
9
+ */
10
+ export async function computeSuiteHash(entries) {
11
+ const ids = new Set();
12
+ for (const { id } of entries) {
13
+ if (ids.has(id))
14
+ throw new Error(`duplicate conformance case ID in the suite hash: ${id}`);
15
+ ids.add(id);
16
+ }
17
+ const lines = entries.map(({ id, impl_hash }) => `${id}\t${impl_hash}\n`).sort();
18
+ return sha256Hex(new TextEncoder().encode(lines.join("")));
19
+ }
20
+ //# sourceMappingURL=suite-hash.js.map
@@ -0,0 +1,7 @@
1
+ /** The single admission line above a DurableStore: mutations run one at a time, in admission order. */
2
+ export interface AdmissionLine {
3
+ /** Run `mutation` after every earlier admission settles. A rejected mutation releases the line. */
4
+ admit<T>(mutation: () => Promise<T>): Promise<T>;
5
+ }
6
+ export declare function createAdmissionLine(): AdmissionLine;
7
+ //# sourceMappingURL=store-admission.d.ts.map
@@ -0,0 +1,11 @@
1
+ export function createAdmissionLine() {
2
+ let tail = Promise.resolve();
3
+ return {
4
+ admit(mutation) {
5
+ const run = tail.then(() => mutation());
6
+ tail = run.then(() => undefined, () => undefined);
7
+ return run;
8
+ },
9
+ };
10
+ }
11
+ //# sourceMappingURL=store-admission.js.map
@@ -0,0 +1,15 @@
1
+ import { type DurableStore, type DurableStoreOptions, type StoreRecord, type StoreValue } from "./store.ts";
2
+ interface MediumState {
3
+ readonly epoch: number;
4
+ readonly records: readonly StoreRecord[];
5
+ readonly documents: ReadonlyMap<string, StoreValue>;
6
+ }
7
+ /** The memory backend's durable medium. It outlives every store opened on it, so a kill is dropping an instance. */
8
+ export interface MemoryStoreMedium {
9
+ current: MediumState;
10
+ }
11
+ export declare function createMemoryStoreMedium(): MemoryStoreMedium;
12
+ /** A DurableStore over `medium`. Each operation stages its writes and publishes them in one assignment: atomic. */
13
+ export declare function openMemoryStore(medium: MemoryStoreMedium, options?: DurableStoreOptions): DurableStore;
14
+ export {};
15
+ //# sourceMappingURL=store-memory.d.ts.map
@@ -0,0 +1,140 @@
1
+ import { DurableStoreError, } from "./store.js";
2
+ export function createMemoryStoreMedium() {
3
+ return { current: { epoch: 0, records: [], documents: new Map() } };
4
+ }
5
+ const copy = (value) => structuredClone(value);
6
+ const copyRecord = (record) => ({ ...record, data: copy(record.data) });
7
+ const isList = (value) => Array.isArray(value);
8
+ function sameValue(a, b) {
9
+ if (a === undefined || b === undefined || a === null || b === null)
10
+ return a === b;
11
+ if (typeof a !== "object" || typeof b !== "object")
12
+ return a === b;
13
+ if (isList(a) || isList(b)) {
14
+ return isList(a) && isList(b) && a.length === b.length && a.every((item, index) => sameValue(item, b[index]));
15
+ }
16
+ const keys = Object.keys(a);
17
+ return (keys.length === Object.keys(b).length && keys.every((key) => Object.hasOwn(b, key) && sameValue(a[key], b[key])));
18
+ }
19
+ const sameRecord = (a, b) => b !== undefined && a.seq === b.seq && a.id === b.id && a.kind === b.kind && sameValue(a.data, b.data);
20
+ function requireOwner(state, epoch) {
21
+ if (state.epoch === 0)
22
+ throw new DurableStoreError("unclaimed", "the store has no owner; claim it first");
23
+ if (epoch !== state.epoch) {
24
+ throw new DurableStoreError("stale-epoch", `epoch ${epoch} is not the owner epoch ${state.epoch}`);
25
+ }
26
+ }
27
+ function importedTail(state, snapshot) {
28
+ const ids = new Set();
29
+ const wellFormed = snapshot.head === snapshot.records.length &&
30
+ snapshot.records.every((record, index) => record.seq === index + 1 && !ids.has(record.id) && ids.add(record.id));
31
+ const extendsLog = state.records.length <= snapshot.records.length &&
32
+ state.records.every((record, index) => sameRecord(record, snapshot.records[index]));
33
+ if (!wellFormed || !extendsLog) {
34
+ throw new DurableStoreError("import-conflict", "the snapshot's log does not extend this store's log");
35
+ }
36
+ return snapshot.records.slice(state.records.length);
37
+ }
38
+ function scan(records, query) {
39
+ const matching = records.filter((record) => (query.fromSeq === undefined || record.seq >= query.fromSeq) &&
40
+ (query.toSeq === undefined || record.seq <= query.toSeq) &&
41
+ (query.kind === undefined || record.kind === query.kind));
42
+ const ordered = query.order === "desc" ? matching.reverse() : matching;
43
+ return (query.limit === undefined ? ordered : ordered.slice(0, query.limit)).map(copyRecord);
44
+ }
45
+ const documentsOf = (state, prefix) => [...state.documents]
46
+ .filter(([key]) => key.startsWith(prefix))
47
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
48
+ .map(([key, data]) => ({ key, data: copy(data) }));
49
+ /** A DurableStore over `medium`. Each operation stages its writes and publishes them in one assignment: atomic. */
50
+ export function openMemoryStore(medium, options = {}) {
51
+ const point = (at) => options.onWritePoint?.(at);
52
+ return {
53
+ readEpoch: async () => medium.current.epoch,
54
+ readHead: async () => medium.current.records.length,
55
+ async claim(expectedEpoch, nextEpoch) {
56
+ point({ op: "claim", at: "before" });
57
+ const state = medium.current;
58
+ if (!Number.isSafeInteger(nextEpoch) || nextEpoch !== expectedEpoch + 1) {
59
+ throw new DurableStoreError("invalid-claim", `a claim must raise the epoch to exactly ${expectedEpoch + 1}`);
60
+ }
61
+ if (state.epoch !== expectedEpoch) {
62
+ throw new DurableStoreError("stale-epoch", `expected epoch ${expectedEpoch}, the owner epoch is ${state.epoch}`);
63
+ }
64
+ medium.current = { ...state, epoch: nextEpoch };
65
+ point({ op: "claim", at: "durable" });
66
+ },
67
+ async commit(epoch, writes, adopt) {
68
+ point({ op: "commit", at: "before" });
69
+ const state = medium.current;
70
+ requireOwner(state, epoch);
71
+ const records = [...state.records];
72
+ const ids = new Set(records.map((record) => record.id));
73
+ const documents = new Map(state.documents);
74
+ const seqs = [];
75
+ writes.forEach((write, index) => {
76
+ point({ op: "commit", at: "write", index });
77
+ switch (write.op) {
78
+ case "append": {
79
+ if (ids.has(write.id))
80
+ throw new DurableStoreError("duplicate-record-id", `record ${write.id} exists`);
81
+ ids.add(write.id);
82
+ const seq = records.length + 1;
83
+ records.push({ seq, id: write.id, kind: write.kind, data: copy(write.data) });
84
+ seqs.push(seq);
85
+ break;
86
+ }
87
+ case "put":
88
+ documents.set(write.key, copy(write.data));
89
+ break;
90
+ case "delete":
91
+ documents.delete(write.key);
92
+ break;
93
+ }
94
+ });
95
+ medium.current = { epoch: state.epoch, records, documents };
96
+ point({ op: "commit", at: "durable" });
97
+ const result = { seqs, head: records.length };
98
+ adopt(result);
99
+ return result;
100
+ },
101
+ mintId: () => crypto.randomUUID(),
102
+ scanRecords: async (query = {}) => scan(medium.current.records, query),
103
+ getDocument: async (key) => {
104
+ const data = medium.current.documents.get(key);
105
+ return data === undefined ? undefined : copy(data);
106
+ },
107
+ scanDocuments: async (prefix) => documentsOf(medium.current, prefix),
108
+ exportSnapshot: async () => {
109
+ const state = medium.current;
110
+ return {
111
+ epoch: state.epoch,
112
+ head: state.records.length,
113
+ records: state.records.map(copyRecord),
114
+ documents: documentsOf(state, ""),
115
+ };
116
+ },
117
+ async importSnapshot(epoch, snapshot) {
118
+ point({ op: "import", at: "before" });
119
+ const state = medium.current;
120
+ requireOwner(state, epoch);
121
+ if (epoch <= snapshot.epoch) {
122
+ throw new DurableStoreError("invalid-claim", `epoch ${epoch} is not above the source epoch ${snapshot.epoch}`);
123
+ }
124
+ const records = [...state.records];
125
+ const documents = new Map();
126
+ let index = 0;
127
+ for (const record of importedTail(state, snapshot)) {
128
+ point({ op: "import", at: "write", index: index++ });
129
+ records.push(copyRecord(record));
130
+ }
131
+ for (const document of snapshot.documents) {
132
+ point({ op: "import", at: "write", index: index++ });
133
+ documents.set(document.key, copy(document.data));
134
+ }
135
+ medium.current = { epoch: state.epoch, records, documents };
136
+ point({ op: "import", at: "durable" });
137
+ },
138
+ };
139
+ }
140
+ //# sourceMappingURL=store-memory.js.map