@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.
- package/dist/agent-loop.js +26 -7
- package/dist/agent.d.ts +3 -0
- package/dist/agent.js +2 -0
- package/dist/conformance/index.d.ts +42 -0
- package/dist/conformance/index.js +23 -0
- package/dist/conformance/mapping.d.ts +24 -0
- package/dist/conformance/mapping.js +62 -0
- package/dist/conformance/registry.d.ts +15 -0
- package/dist/conformance/registry.js +75 -0
- package/dist/conformance/runner.d.ts +27 -0
- package/dist/conformance/runner.js +23 -0
- package/dist/conformance/suite-hash.d.ts +12 -0
- package/dist/conformance/suite-hash.js +20 -0
- package/dist/durable/store-admission.d.ts +7 -0
- package/dist/durable/store-admission.js +11 -0
- package/dist/durable/store-memory.d.ts +15 -0
- package/dist/durable/store-memory.js +140 -0
- package/dist/durable/store.d.ts +116 -0
- package/dist/durable/store.js +14 -0
- package/dist/durable/testing/kill-store.d.ts +13 -0
- package/dist/durable/testing/kill-store.js +77 -0
- package/dist/durable/testing/kill-wake.d.ts +12 -0
- package/dist/durable/testing/kill-wake.js +123 -0
- package/dist/durable/testing/manual-timers.d.ts +8 -0
- package/dist/durable/testing/manual-timers.js +31 -0
- package/dist/durable/testing/multi-host-cases.d.ts +6 -0
- package/dist/durable/testing/multi-host-cases.js +32 -0
- package/dist/durable/testing/store-cases.d.ts +21 -0
- package/dist/durable/testing/store-cases.js +168 -0
- package/dist/durable/testing/store-harness.d.ts +23 -0
- package/dist/durable/testing/store-harness.js +64 -0
- package/dist/durable/testing/wake-cases.d.ts +6 -0
- package/dist/durable/testing/wake-cases.js +100 -0
- package/dist/durable/testing/wake-harness.d.ts +64 -0
- package/dist/durable/testing/wake-harness.js +127 -0
- package/dist/durable/testing/wake-test-loop.d.ts +13 -0
- package/dist/durable/testing/wake-test-loop.js +40 -0
- package/dist/durable/wake-host-bun-timer.d.ts +31 -0
- package/dist/durable/wake-host-bun-timer.js +66 -0
- package/dist/durable/wake-host-store-alarm.d.ts +13 -0
- package/dist/durable/wake-host-store-alarm.js +20 -0
- package/dist/durable/wake-host.d.ts +68 -0
- package/dist/durable/wake-host.js +70 -0
- package/dist/harness/utils/read-folders/index.d.ts +1 -1
- package/dist/harness/utils/read-folders/index.js +1 -1
- package/dist/harness/utils/read-folders/tree-sitter/engine.d.ts +1 -1
- package/dist/harness/utils/read-folders/tree-sitter/engine.js +38 -19
- package/dist/stream-keepalive.d.ts +28 -0
- package/dist/stream-keepalive.js +42 -0
- package/dist/testing/win32-sim.d.ts +76 -0
- package/dist/testing/win32-sim.js +197 -0
- package/dist/transport/in-process-carrier.d.ts +19 -0
- package/dist/transport/in-process-carrier.js +63 -0
- package/dist/transport/local-endpoint.d.ts +13 -0
- package/dist/transport/local-endpoint.js +20 -0
- package/dist/transport/reliable-transport.d.ts +40 -0
- package/dist/transport/reliable-transport.js +232 -0
- package/dist/transport/testing/fault-cases.d.ts +6 -0
- package/dist/transport/testing/fault-cases.js +30 -0
- package/dist/transport/testing/fault-transport.d.ts +44 -0
- package/dist/transport/testing/fault-transport.js +144 -0
- package/dist/transport/testing/multi-cases.d.ts +6 -0
- package/dist/transport/testing/multi-cases.js +113 -0
- package/dist/transport/testing/transport-cases.d.ts +6 -0
- package/dist/transport/testing/transport-cases.js +60 -0
- package/dist/transport/testing/transport-harness.d.ts +53 -0
- package/dist/transport/testing/transport-harness.js +127 -0
- package/dist/transport/transport.d.ts +60 -0
- package/dist/transport/transport.js +15 -0
- package/dist/types.d.ts +6 -0
- package/package.json +11 -3
package/dist/agent-loop.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createTerminalFailureAssistantMessage, isStreamIdleTimeoutError, normal
|
|
|
7
7
|
import { createResponseTimingRecorder } from "./response-timing.js";
|
|
8
8
|
import { internalProgressSink } from "./internal-events.js";
|
|
9
9
|
import { getDefaultStreamFn, withEmptyAssistantRecovery } from "./stream-fn.js";
|
|
10
|
+
import { createTransportLiveness, StreamKeepAliveTimeoutError } from "./stream-keepalive.js";
|
|
10
11
|
import { prepareAgentToolCallArguments } from "./tool-arguments.js";
|
|
11
12
|
import { resolveCallTool, withToolNameCorrection } from "./tool-name-alias.js";
|
|
12
13
|
/**
|
|
@@ -465,6 +466,7 @@ async function streamAssistantResponse(context, config, signal, emit, streamFunc
|
|
|
465
466
|
// Dedicated controller for the provider request so the loop can tear the
|
|
466
467
|
// request down itself (idle timeout), not only when the caller aborts.
|
|
467
468
|
const requestAbortController = new AbortController();
|
|
469
|
+
const liveness = createTransportLiveness(normalizeTimeoutMs(config.streamKeepAliveTimeoutMs));
|
|
468
470
|
let detachCallerAbort;
|
|
469
471
|
if (signal !== undefined) {
|
|
470
472
|
if (signal.aborted) {
|
|
@@ -490,6 +492,10 @@ async function streamAssistantResponse(context, config, signal, emit, streamFunc
|
|
|
490
492
|
streamKind: "main",
|
|
491
493
|
apiKey: resolvedApiKey,
|
|
492
494
|
signal: requestAbortController.signal,
|
|
495
|
+
onTransportActivity: () => {
|
|
496
|
+
liveness.noteActivity();
|
|
497
|
+
config.onTransportActivity?.();
|
|
498
|
+
},
|
|
493
499
|
// Cursor exec bridging (ignored by every other provider): handlers
|
|
494
500
|
// execute mid-stream; their paired results buffer here.
|
|
495
501
|
...(config.cursorExecHandlers
|
|
@@ -515,7 +521,7 @@ async function streamAssistantResponse(context, config, signal, emit, streamFunc
|
|
|
515
521
|
const sink = config[internalProgressSink];
|
|
516
522
|
const records = sink ? assistantRecords(response) : response;
|
|
517
523
|
const iterator = records[Symbol.asyncIterator]();
|
|
518
|
-
const eventReader = createAssistantEventReader(iterator, streamIdleTimeoutMs, requestAbortController.signal, (error) => requestAbortController.abort(error), preStream.remainingStartMs(), response, normalizeTimeoutMs(config.streamStartTimeoutMs));
|
|
524
|
+
const eventReader = createAssistantEventReader(iterator, streamIdleTimeoutMs, requestAbortController.signal, (error) => requestAbortController.abort(error), preStream.remainingStartMs(), response, normalizeTimeoutMs(config.streamStartTimeoutMs), liveness);
|
|
519
525
|
try {
|
|
520
526
|
while (true) {
|
|
521
527
|
const next = await eventReader.next();
|
|
@@ -660,7 +666,10 @@ async function streamAssistantResponse(context, config, signal, emit, streamFunc
|
|
|
660
666
|
// A step that rethrows the caller's own reason (`throwIfAborted()`, a fetch rejected with it) shows that reason
|
|
661
667
|
// through the same shaping and cap (#3510).
|
|
662
668
|
const callerAbortedPastBound = signal?.aborted === true &&
|
|
663
|
-
(error instanceof StreamStartTimeoutError ||
|
|
669
|
+
(error instanceof StreamStartTimeoutError ||
|
|
670
|
+
error instanceof StreamKeepAliveTimeoutError ||
|
|
671
|
+
isStreamIdleTimeoutError(error) ||
|
|
672
|
+
error === signal.reason);
|
|
664
673
|
const finalMessage = createTerminalFailureAssistantMessage(config.model, signal?.aborted ? "aborted" : "error", callerAbortedPastBound ? abortError(signal.reason) : error, partialMessage);
|
|
665
674
|
stampTiming(finalMessage);
|
|
666
675
|
if (addedPartial) {
|
|
@@ -763,7 +772,7 @@ function normalizeTimeoutMs(timeoutMs) {
|
|
|
763
772
|
function createAssistantEventReader(iterator, timeoutMs, signal, onIdleTimeout, streamStartTimeoutMs, stream,
|
|
764
773
|
// The bound the error names, when the timer only gets what is left of it (the steps before the stream used
|
|
765
774
|
// the rest); the error must name the configured bound, which users raise.
|
|
766
|
-
configuredStartTimeoutMs) {
|
|
775
|
+
configuredStartTimeoutMs, liveness) {
|
|
767
776
|
const idleTimeoutMs = normalizeTimeoutMs(timeoutMs);
|
|
768
777
|
const startTimeoutMs = normalizeTimeoutMs(streamStartTimeoutMs);
|
|
769
778
|
const reportedStartTimeoutMs = configuredStartTimeoutMs ?? startTimeoutMs;
|
|
@@ -798,20 +807,24 @@ configuredStartTimeoutMs) {
|
|
|
798
807
|
? (ms) => new StreamStartTimeoutError(reportedStartTimeoutMs ?? ms)
|
|
799
808
|
: (ms) => new StreamIdleTimeoutError(idleTimeoutMs ?? ms);
|
|
800
809
|
const readStartedAt = Date.now();
|
|
801
|
-
|
|
810
|
+
// Keep-alives are judged against the whole configured bound, not the share of it left for this read.
|
|
811
|
+
const windowMs = useStartBound ? (reportedStartTimeoutMs ?? startTimeoutMs) : idleTimeoutMs;
|
|
812
|
+
const result = await readNextAssistantEvent(iterator, readTimeoutMs === undefined ? undefined : Math.max(1, readTimeoutMs - silentMs), makeTimeoutError, abortPromise, onIdleTimeout, stream, signal, liveness && windowMs !== undefined ? { liveness, windowMs } : undefined);
|
|
802
813
|
if (!result.done && isAssistantProgress(result.value))
|
|
803
814
|
silentMs += Date.now() - readStartedAt;
|
|
804
815
|
else {
|
|
805
816
|
silentMs = 0;
|
|
806
|
-
if (!result.done)
|
|
817
|
+
if (!result.done) {
|
|
807
818
|
sawFirstEvent = true;
|
|
819
|
+
liveness?.contentArrived();
|
|
820
|
+
}
|
|
808
821
|
}
|
|
809
822
|
return result;
|
|
810
823
|
},
|
|
811
824
|
dispose: () => removeAbortListener?.(),
|
|
812
825
|
};
|
|
813
826
|
}
|
|
814
|
-
async function readNextAssistantEvent(iterator, idleTimeoutMs, makeTimeoutError, abortPromise, onIdleTimeout, stream, signal) {
|
|
827
|
+
async function readNextAssistantEvent(iterator, idleTimeoutMs, makeTimeoutError, abortPromise, onIdleTimeout, stream, signal, keepAlive) {
|
|
815
828
|
if (idleTimeoutMs === undefined && abortPromise === undefined) {
|
|
816
829
|
return iterator.next();
|
|
817
830
|
}
|
|
@@ -837,7 +850,13 @@ async function readNextAssistantEvent(iterator, idleTimeoutMs, makeTimeoutError,
|
|
|
837
850
|
timeout = setTimeout(onIdleDeadline, idleTimeoutMs);
|
|
838
851
|
return;
|
|
839
852
|
}
|
|
840
|
-
|
|
853
|
+
// A provider that accepted the request and is still sending keep-alive bytes is working, not dead.
|
|
854
|
+
const verdict = keepAlive?.liveness.onSilenceDeadline(keepAlive.windowMs);
|
|
855
|
+
if (verdict?.kind === "extend") {
|
|
856
|
+
timeout = setTimeout(onIdleDeadline, verdict.delayMs);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const error = verdict?.kind === "keep-alive-exhausted" ? verdict.error : makeTimeoutError(idleTimeoutMs);
|
|
841
860
|
closeAssistantIterator(iterator);
|
|
842
861
|
settle(() => reject(error));
|
|
843
862
|
// Abort after settling so the failure surfaces as an idle timeout,
|
package/dist/agent.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ export interface AgentOptions {
|
|
|
34
34
|
transport?: Transport;
|
|
35
35
|
timeoutMs?: number;
|
|
36
36
|
streamStartTimeoutMs?: number;
|
|
37
|
+
streamKeepAliveTimeoutMs?: number;
|
|
37
38
|
maxRetryDelayMs?: number;
|
|
38
39
|
toolExecution?: ToolExecutionMode;
|
|
39
40
|
removedToolHints?: Record<string, string>;
|
|
@@ -106,6 +107,8 @@ export declare class Agent {
|
|
|
106
107
|
timeoutMs?: number;
|
|
107
108
|
/** Optional bound on the wait for the first provider stream event. */
|
|
108
109
|
streamStartTimeoutMs?: number;
|
|
110
|
+
/** Optional bound on how long keep-alives alone may hold a provider request without content. */
|
|
111
|
+
streamKeepAliveTimeoutMs?: number;
|
|
109
112
|
/** Optional cap for provider-requested retry delays. */
|
|
110
113
|
maxRetryDelayMs?: number;
|
|
111
114
|
/** Tool execution strategy for assistant messages that contain multiple tool calls. */
|
package/dist/agent.js
CHANGED
|
@@ -163,6 +163,7 @@ export class Agent {
|
|
|
163
163
|
this.transport = runtimeOptions.transport ?? "auto";
|
|
164
164
|
this.timeoutMs = runtimeOptions.timeoutMs;
|
|
165
165
|
this.streamStartTimeoutMs = runtimeOptions.streamStartTimeoutMs;
|
|
166
|
+
this.streamKeepAliveTimeoutMs = runtimeOptions.streamKeepAliveTimeoutMs;
|
|
166
167
|
this.maxRetryDelayMs = runtimeOptions.maxRetryDelayMs;
|
|
167
168
|
this.toolExecution = runtimeOptions.toolExecution ?? "parallel";
|
|
168
169
|
this.removedToolHints = runtimeOptions.removedToolHints ?? {};
|
|
@@ -473,6 +474,7 @@ export class Agent {
|
|
|
473
474
|
thinkingBudgets: this.thinkingBudgets,
|
|
474
475
|
timeoutMs: this.timeoutMs,
|
|
475
476
|
streamStartTimeoutMs: this.streamStartTimeoutMs,
|
|
477
|
+
streamKeepAliveTimeoutMs: this.streamKeepAliveTimeoutMs,
|
|
476
478
|
initialRequestTimeoutMs: options.initialRequestTimeoutMs,
|
|
477
479
|
initialRequestStreamStartTimeoutMs: options.initialRequestStreamStartTimeoutMs,
|
|
478
480
|
maxRetryDelayMs: this.maxRetryDelayMs,
|
|
@@ -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
|