@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
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The five QX2 fault profiles as test parameters (not product settings). The plan fixes their shape; the numbers here are
|
|
3
|
+
* this suite's until M0 records final values in the gate's fault-profiles.json.
|
|
4
|
+
*/
|
|
5
|
+
const PROFILES = {
|
|
6
|
+
slow: { perTick: 1, tickMs: 10 },
|
|
7
|
+
lossy: { lossRate: 0.2, seed: 7 },
|
|
8
|
+
laggy: { latencyMs: 40, jitterMs: 60, seed: 11 },
|
|
9
|
+
flap: { upMs: 100, downMs: 60, cycles: 8 },
|
|
10
|
+
"drop-reattach": { everyMessages: 9, reattachMs: 30, repetitions: 6 },
|
|
11
|
+
};
|
|
12
|
+
export const FAULT_PROFILE_NAMES = ["slow", "lossy", "laggy", "flap", "drop-reattach"];
|
|
13
|
+
const mulberry32 = (seed) => {
|
|
14
|
+
let state = seed >>> 0;
|
|
15
|
+
return () => {
|
|
16
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
17
|
+
let t = state;
|
|
18
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
19
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
20
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
function wrapWith(send) {
|
|
24
|
+
return (side) => {
|
|
25
|
+
let current;
|
|
26
|
+
return {
|
|
27
|
+
attached: (connection) => {
|
|
28
|
+
current = connection;
|
|
29
|
+
const live = () => current === connection;
|
|
30
|
+
side.attached({
|
|
31
|
+
send: (message) => send(connection, side, message, live),
|
|
32
|
+
close: () => connection.close(),
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
message: (message) => side.message(message),
|
|
36
|
+
writable: () => side.writable(),
|
|
37
|
+
detached: () => {
|
|
38
|
+
current = undefined;
|
|
39
|
+
side.detached();
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function slow(timers, stats) {
|
|
45
|
+
const { perTick, tickMs } = PROFILES.slow;
|
|
46
|
+
const used = new WeakMap();
|
|
47
|
+
return (inner, side, message, live) => {
|
|
48
|
+
const tick = Math.floor(timers.now() / tickMs);
|
|
49
|
+
const state = used.get(side) ?? { tick, count: 0, waiting: false };
|
|
50
|
+
if (state.tick !== tick)
|
|
51
|
+
Object.assign(state, { tick, count: 0 });
|
|
52
|
+
used.set(side, state);
|
|
53
|
+
if (state.count < perTick) {
|
|
54
|
+
state.count += 1;
|
|
55
|
+
return inner.send(message);
|
|
56
|
+
}
|
|
57
|
+
stats.refused += 1;
|
|
58
|
+
if (!state.waiting) {
|
|
59
|
+
state.waiting = true;
|
|
60
|
+
timers.setTimer(() => {
|
|
61
|
+
state.waiting = false;
|
|
62
|
+
if (live())
|
|
63
|
+
side.writable();
|
|
64
|
+
}, (tick + 1) * tickMs - timers.now());
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function lossy(stats) {
|
|
70
|
+
const random = mulberry32(PROFILES.lossy.seed);
|
|
71
|
+
return (inner, _side, message) => {
|
|
72
|
+
if (random() < PROFILES.lossy.lossRate) {
|
|
73
|
+
stats.lost += 1;
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
return inner.send(message);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function laggy(timers, stats) {
|
|
80
|
+
const { latencyMs, jitterMs, seed } = PROFILES.laggy;
|
|
81
|
+
const random = mulberry32(seed);
|
|
82
|
+
const order = new WeakMap();
|
|
83
|
+
return (inner, side, message, live) => {
|
|
84
|
+
const state = order.get(side) ?? { sent: 0, highest: 0 };
|
|
85
|
+
order.set(side, state);
|
|
86
|
+
state.sent += 1;
|
|
87
|
+
const index = state.sent;
|
|
88
|
+
timers.setTimer(() => {
|
|
89
|
+
if (!live())
|
|
90
|
+
return;
|
|
91
|
+
if (index < state.highest)
|
|
92
|
+
stats.reordered += 1;
|
|
93
|
+
state.highest = Math.max(state.highest, index);
|
|
94
|
+
inner.send(message);
|
|
95
|
+
}, latencyMs + Math.floor(random() * jitterMs));
|
|
96
|
+
return true;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function flap(timers, stats, link) {
|
|
100
|
+
const { upMs, downMs, cycles } = PROFILES.flap;
|
|
101
|
+
for (let cycle = 0; cycle < cycles; cycle++) {
|
|
102
|
+
const start = timers.now() + cycle * (upMs + downMs);
|
|
103
|
+
timers.setTimer(() => {
|
|
104
|
+
stats.drops += 1;
|
|
105
|
+
link.drop();
|
|
106
|
+
}, start + upMs - timers.now());
|
|
107
|
+
timers.setTimer(() => link.connect(), start + upMs + downMs - timers.now());
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Builds one profile's fault for the link a case opens. */
|
|
111
|
+
export function createCarrierFault(name, stats) {
|
|
112
|
+
return (timers) => {
|
|
113
|
+
let link;
|
|
114
|
+
const passThrough = (inner, _side, message) => inner.send(message);
|
|
115
|
+
let sent = 0;
|
|
116
|
+
const dropReattach = (inner, _side, message) => {
|
|
117
|
+
const { everyMessages, reattachMs, repetitions } = PROFILES["drop-reattach"];
|
|
118
|
+
sent += 1;
|
|
119
|
+
if (sent % everyMessages === 0 && stats.drops < repetitions && link !== undefined) {
|
|
120
|
+
const target = link;
|
|
121
|
+
stats.drops += 1;
|
|
122
|
+
timers.setTimer(() => target.drop(), 0);
|
|
123
|
+
timers.setTimer(() => target.connect(), reattachMs);
|
|
124
|
+
}
|
|
125
|
+
return inner.send(message);
|
|
126
|
+
};
|
|
127
|
+
const sends = {
|
|
128
|
+
slow: slow(timers, stats),
|
|
129
|
+
lossy: lossy(stats),
|
|
130
|
+
laggy: laggy(timers, stats),
|
|
131
|
+
flap: passThrough,
|
|
132
|
+
"drop-reattach": dropReattach,
|
|
133
|
+
};
|
|
134
|
+
return {
|
|
135
|
+
wrap: wrapWith(sends[name]),
|
|
136
|
+
start(started) {
|
|
137
|
+
link = started;
|
|
138
|
+
if (name === "flap")
|
|
139
|
+
flap(timers, stats, started);
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=fault-transport.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CaseImplementation } from "../../conformance/runner.ts";
|
|
2
|
+
import { type TransportCaseAdapters } from "./transport-harness.ts";
|
|
3
|
+
type MultiCase = CaseImplementation<TransportCaseAdapters>;
|
|
4
|
+
export declare const MULTI_CASES: ReadonlyMap<string, MultiCase>;
|
|
5
|
+
export {};
|
|
6
|
+
//# sourceMappingURL=multi-cases.d.ts.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createCarrierFault } from "./fault-transport.js";
|
|
2
|
+
import { allDelivered, expectExactDelivery, openLink, range, } from "./transport-harness.js";
|
|
3
|
+
const manyLanes = async (adapters) => {
|
|
4
|
+
const link = openLink(adapters);
|
|
5
|
+
const lanes = range(1, 8).map((n) => `scope-${n}`);
|
|
6
|
+
const counts = Object.fromEntries(lanes.map((lane) => [lane, 25]));
|
|
7
|
+
const sends = range(1, 25).flatMap((n) => lanes.map((lane) => link.send(lane, n)));
|
|
8
|
+
await link.runUntil(allDelivered(link, counts), "MULTI-LANE-01 every scope delivered");
|
|
9
|
+
await link.awaitAll(sends, "MULTI-LANE-01");
|
|
10
|
+
await link.advance(1000);
|
|
11
|
+
expectExactDelivery(link, counts, "MULTI-LANE-01");
|
|
12
|
+
const lanesInLog = link.log.map((entry) => entry.slice(0, entry.indexOf(":")));
|
|
13
|
+
for (const lane of lanes) {
|
|
14
|
+
const first = lanesInLog.indexOf(lane);
|
|
15
|
+
const lastOfOthers = Math.min(...lanes.filter((other) => other !== lane).map((other) => lanesInLog.lastIndexOf(other)));
|
|
16
|
+
if (first > lastOfOthers)
|
|
17
|
+
throw new Error(`MULTI-LANE-01: ${lane} started only after another scope had finished`);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const stalledLaneDoesNotHoldOthers = async (adapters) => {
|
|
21
|
+
const link = openLink(adapters);
|
|
22
|
+
const release = link.gate("A");
|
|
23
|
+
const aSends = range(1, 100).map((n) => link.send("A", n));
|
|
24
|
+
const bSends = range(1, 10).map((n) => link.send("B", n));
|
|
25
|
+
await link.runUntil(allDelivered(link, { B: 10 }), "MULTI-FAIR-01 B delivered while A's receiver is stalled", 20_000);
|
|
26
|
+
await link.awaitAll(bSends, "MULTI-FAIR-01 B");
|
|
27
|
+
if (link.delivered("A").length > 0)
|
|
28
|
+
throw new Error("MULTI-FAIR-01: A delivered through a stalled receiver");
|
|
29
|
+
release();
|
|
30
|
+
await link.runUntil(allDelivered(link, { A: 100 }), "MULTI-FAIR-01 A delivered after release");
|
|
31
|
+
await link.awaitAll(aSends, "MULTI-FAIR-01 A");
|
|
32
|
+
await link.advance(1000);
|
|
33
|
+
expectExactDelivery(link, { A: 100, B: 10 }, "MULTI-FAIR-01 stalled lane");
|
|
34
|
+
};
|
|
35
|
+
const MAX_A_FRAMES_BEFORE_B = 4;
|
|
36
|
+
const longLaneDoesNotStarveAnother = async (adapters) => {
|
|
37
|
+
const stats = { refused: 0, lost: 0, reordered: 0, drops: 0 };
|
|
38
|
+
const link = openLink(adapters, createCarrierFault("slow", stats));
|
|
39
|
+
const aSends = range(1, 300).map((n) => link.send("A", n));
|
|
40
|
+
await link.runUntil(() => link.delivered("A").length >= 20, "MULTI-FAIR-01 A's long lane under way");
|
|
41
|
+
const before = link.delivered("A").length;
|
|
42
|
+
const bSend = link.send("B", 1);
|
|
43
|
+
await link.runUntil(allDelivered(link, { B: 1 }), "MULTI-FAIR-01 B delivered behind A's long lane");
|
|
44
|
+
const aheadOfB = link.delivered("A").length - before;
|
|
45
|
+
if (aheadOfB > MAX_A_FRAMES_BEFORE_B) {
|
|
46
|
+
throw new Error(`MULTI-FAIR-01: ${aheadOfB} of A's frames went ahead of B's one frame (at most ${MAX_A_FRAMES_BEFORE_B})`);
|
|
47
|
+
}
|
|
48
|
+
await link.runUntil(allDelivered(link, { A: 300 }), "MULTI-FAIR-01 A's long lane delivered", 600_000);
|
|
49
|
+
await link.awaitAll([...aSends, bSend], "MULTI-FAIR-01 long lane");
|
|
50
|
+
await link.advance(1000);
|
|
51
|
+
expectExactDelivery(link, { A: 300, B: 1 }, "MULTI-FAIR-01 long lane");
|
|
52
|
+
if (stats.refused === 0)
|
|
53
|
+
throw new Error("MULTI-FAIR-01: the slow carrier never pushed back");
|
|
54
|
+
};
|
|
55
|
+
const fairness = async (adapters) => {
|
|
56
|
+
await stalledLaneDoesNotHoldOthers(adapters);
|
|
57
|
+
await longLaneDoesNotStarveAnother(adapters);
|
|
58
|
+
};
|
|
59
|
+
/** Loses the first data message of `lane`#`seq` on the carrier, once; every other message passes. */
|
|
60
|
+
function loseOnce(decode, lane, seq, stats) {
|
|
61
|
+
return {
|
|
62
|
+
wrap: (side) => ({
|
|
63
|
+
attached: (connection) => side.attached({
|
|
64
|
+
send: (message) => {
|
|
65
|
+
const view = decode(message);
|
|
66
|
+
if (stats.lost === 0 && view.kind === "data" && view.lane === lane && view.seq === seq) {
|
|
67
|
+
stats.lost += 1;
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return connection.send(message);
|
|
71
|
+
},
|
|
72
|
+
close: () => connection.close(),
|
|
73
|
+
}),
|
|
74
|
+
message: (message) => side.message(message),
|
|
75
|
+
writable: () => side.writable(),
|
|
76
|
+
detached: () => side.detached(),
|
|
77
|
+
}),
|
|
78
|
+
start: () => { },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const LONG_LANE_FRAMES = 400;
|
|
82
|
+
const LONG_LANE_PACE_MS = 10;
|
|
83
|
+
/**
|
|
84
|
+
* B's only frame is lost while A's paced receiver keeps acknowledging A's long lane. B's resend must not wait for A: the
|
|
85
|
+
* loss is recovered on B's own deadline, before half of A's frames have gone ahead of it.
|
|
86
|
+
*/
|
|
87
|
+
const lossOnOneLaneIsNotHeldByAnother = async (adapters) => {
|
|
88
|
+
const stats = { refused: 0, lost: 0, reordered: 0, drops: 0 };
|
|
89
|
+
const decode = (message) => adapters.transport.decode(message);
|
|
90
|
+
const link = openLink(adapters, () => loseOnce(decode, "B", 1, stats));
|
|
91
|
+
link.pace("A", LONG_LANE_PACE_MS);
|
|
92
|
+
const aSends = range(1, LONG_LANE_FRAMES).map((n) => link.send("A", n));
|
|
93
|
+
await link.runUntil(() => link.delivered("A").length >= 10, "MULTI-FAIR-02 A's long lane under way");
|
|
94
|
+
const before = link.delivered("A").length;
|
|
95
|
+
const bSend = link.send("B", 1);
|
|
96
|
+
await link.runUntil(allDelivered(link, { B: 1 }), "MULTI-FAIR-02 B delivered after its loss", 600_000);
|
|
97
|
+
if (stats.lost !== 1)
|
|
98
|
+
throw new Error(`MULTI-FAIR-02: B#1 was not lost on the carrier (${JSON.stringify(stats)})`);
|
|
99
|
+
const aheadOfB = link.delivered("A").length - before;
|
|
100
|
+
if (aheadOfB >= LONG_LANE_FRAMES / 2) {
|
|
101
|
+
throw new Error(`MULTI-FAIR-02: B's lost frame was resent only after ${aheadOfB} of A's frames (fewer than ${LONG_LANE_FRAMES / 2})`);
|
|
102
|
+
}
|
|
103
|
+
await link.runUntil(allDelivered(link, { A: LONG_LANE_FRAMES }), "MULTI-FAIR-02 A delivered", 600_000);
|
|
104
|
+
await link.awaitAll([...aSends, bSend], "MULTI-FAIR-02");
|
|
105
|
+
await link.advance(2000);
|
|
106
|
+
expectExactDelivery(link, { A: LONG_LANE_FRAMES, B: 1 }, "MULTI-FAIR-02");
|
|
107
|
+
};
|
|
108
|
+
export const MULTI_CASES = new Map([
|
|
109
|
+
["MULTI-LANE-01", manyLanes],
|
|
110
|
+
["MULTI-FAIR-01", fairness],
|
|
111
|
+
["MULTI-FAIR-02", lossOnOneLaneIsNotHeldByAnother],
|
|
112
|
+
]);
|
|
113
|
+
//# sourceMappingURL=multi-cases.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CaseImplementation } from "../../conformance/runner.ts";
|
|
2
|
+
import { type TransportCaseAdapters } from "./transport-harness.ts";
|
|
3
|
+
type TransportCase = CaseImplementation<TransportCaseAdapters>;
|
|
4
|
+
export declare const TRANSPORT_CASES: ReadonlyMap<string, TransportCase>;
|
|
5
|
+
export {};
|
|
6
|
+
//# sourceMappingURL=transport-cases.d.ts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { expectEqual } from "../../durable/testing/store-harness.js";
|
|
2
|
+
import { allDelivered, expectExactDelivery, openLink, range } from "./transport-harness.js";
|
|
3
|
+
const order = async (adapters) => {
|
|
4
|
+
const link = openLink(adapters);
|
|
5
|
+
const counts = { a: 40, b: 40, c: 40 };
|
|
6
|
+
const sends = range(1, 40).flatMap((n) => ["a", "b", "c"].map((lane) => link.send(lane, n)));
|
|
7
|
+
await link.runUntil(allDelivered(link, counts), "TRANSPORT-01 every frame delivered");
|
|
8
|
+
await link.awaitAll(sends, "TRANSPORT-01");
|
|
9
|
+
await link.advance(1000);
|
|
10
|
+
expectExactDelivery(link, counts, "TRANSPORT-01");
|
|
11
|
+
};
|
|
12
|
+
const resume = async (adapters) => {
|
|
13
|
+
const link = openLink(adapters);
|
|
14
|
+
const counts = { a: 30, b: 30 };
|
|
15
|
+
const sends = range(1, 10).flatMap((n) => [link.send("a", n), link.send("b", n)]);
|
|
16
|
+
await link.runUntil(() => link.delivered("a").length >= 4, "TRANSPORT-02 first frames delivered");
|
|
17
|
+
link.link.drop();
|
|
18
|
+
sends.push(...range(11, 20).flatMap((n) => [link.send("a", n), link.send("b", n)]));
|
|
19
|
+
await link.advance(500);
|
|
20
|
+
if (link.sender.inFlight("a") + link.sender.inFlight("b") === 0) {
|
|
21
|
+
throw new Error("TRANSPORT-02: expected unacknowledged frames while the carrier is down");
|
|
22
|
+
}
|
|
23
|
+
link.link.connect();
|
|
24
|
+
await link.runUntil(() => link.delivered("a").length >= 15, "TRANSPORT-02 resumed after the first re-attach");
|
|
25
|
+
link.link.drop();
|
|
26
|
+
sends.push(...range(21, 30).flatMap((n) => [link.send("a", n), link.send("b", n)]));
|
|
27
|
+
link.link.connect();
|
|
28
|
+
await link.runUntil(allDelivered(link, counts), "TRANSPORT-02 every frame delivered after two re-attaches");
|
|
29
|
+
await link.awaitAll(sends, "TRANSPORT-02");
|
|
30
|
+
await link.advance(1000);
|
|
31
|
+
expectExactDelivery(link, counts, "TRANSPORT-02");
|
|
32
|
+
};
|
|
33
|
+
const backpressure = async (adapters) => {
|
|
34
|
+
const link = openLink(adapters);
|
|
35
|
+
const release = link.gate("slow");
|
|
36
|
+
const resolved = [];
|
|
37
|
+
const sends = range(1, 200).map((n) => link.send("slow", n).then(() => void resolved.push(n)));
|
|
38
|
+
await link.advance(2000);
|
|
39
|
+
const admitted = resolved.length;
|
|
40
|
+
if (admitted === 0 || admitted >= 200) {
|
|
41
|
+
throw new Error(`TRANSPORT-03: a stalled receiver must bound the senders, but ${admitted} of 200 sends resolved`);
|
|
42
|
+
}
|
|
43
|
+
await link.advance(2000);
|
|
44
|
+
expectEqual(resolved.length, admitted, "TRANSPORT-03 the bound holds while the receiver stays stalled");
|
|
45
|
+
expectEqual(link.delivered("slow"), [], "TRANSPORT-03 nothing delivered while the receiver is stalled");
|
|
46
|
+
if (link.sender.inFlight("slow") > admitted)
|
|
47
|
+
throw new Error("TRANSPORT-03: more in flight than admitted");
|
|
48
|
+
release();
|
|
49
|
+
await link.runUntil(allDelivered(link, { slow: 200 }), "TRANSPORT-03 every frame delivered after release");
|
|
50
|
+
await link.awaitAll(sends, "TRANSPORT-03");
|
|
51
|
+
expectEqual(resolved, range(1, 200), "TRANSPORT-03 sends resolve in send order");
|
|
52
|
+
await link.advance(1000);
|
|
53
|
+
expectExactDelivery(link, { slow: 200 }, "TRANSPORT-03");
|
|
54
|
+
};
|
|
55
|
+
export const TRANSPORT_CASES = new Map([
|
|
56
|
+
["TRANSPORT-01", order],
|
|
57
|
+
["TRANSPORT-02", resume],
|
|
58
|
+
["TRANSPORT-03", backpressure],
|
|
59
|
+
]);
|
|
60
|
+
//# sourceMappingURL=transport-cases.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ManualTimers } from "../../durable/testing/manual-timers.ts";
|
|
2
|
+
import { type CarrierLink } from "../in-process-carrier.ts";
|
|
3
|
+
import type { CarrierSide, TransportClock, TransportSide } from "../transport.ts";
|
|
4
|
+
/** What a conformance host's decoder reports about one carrier message; data and ACK sequence numbers start at 1. */
|
|
5
|
+
export type WireView = {
|
|
6
|
+
readonly kind: "data" | "ack";
|
|
7
|
+
readonly lane: string;
|
|
8
|
+
readonly seq: number;
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: "other";
|
|
11
|
+
};
|
|
12
|
+
/** What a host provides to the TRANSPORT, MULTI-LANE/FAIR and FAULT-TRANSPORT cases. */
|
|
13
|
+
export interface TransportConformanceAdapter {
|
|
14
|
+
/** A new transport endpoint scheduling on `clock`; the cases attach two of them through an in-process carrier. */
|
|
15
|
+
createSide(clock: TransportClock): TransportSide;
|
|
16
|
+
/** Reads the lane and sequence number of a carrier message, so the cases can check resume from the last ACK. */
|
|
17
|
+
decode(message: unknown): WireView;
|
|
18
|
+
}
|
|
19
|
+
export interface TransportCaseAdapters {
|
|
20
|
+
readonly transport: TransportConformanceAdapter;
|
|
21
|
+
/** The platform whose local endpoint naming the link uses (`win32` names a pipe). */
|
|
22
|
+
readonly platform: string;
|
|
23
|
+
}
|
|
24
|
+
/** A fault on the carrier path: it wraps each side's view of the carrier and may drive the link. */
|
|
25
|
+
export interface CarrierFault {
|
|
26
|
+
wrap(side: CarrierSide): CarrierSide;
|
|
27
|
+
start(link: CarrierLink): void;
|
|
28
|
+
}
|
|
29
|
+
export interface TestLink {
|
|
30
|
+
readonly sender: TransportSide;
|
|
31
|
+
readonly link: CarrierLink;
|
|
32
|
+
readonly timers: ManualTimers;
|
|
33
|
+
/** Global delivery order, as `lane:n`. */
|
|
34
|
+
readonly log: readonly string[];
|
|
35
|
+
/** Every data frame sent again after the sender had seen its ACK. */
|
|
36
|
+
readonly violations: readonly string[];
|
|
37
|
+
send(lane: string, n: number): Promise<void>;
|
|
38
|
+
delivered(lane: string): readonly number[];
|
|
39
|
+
/** Holds the receiver on `lane` (from its next frame) until the returned release is called. */
|
|
40
|
+
gate(lane: string): () => void;
|
|
41
|
+
/** Makes the receiver take `ms` of transport time for each frame on `lane` (from its next frame). */
|
|
42
|
+
pace(lane: string, ms: number): void;
|
|
43
|
+
runUntil(done: () => boolean, what: string, budgetMs?: number): Promise<void>;
|
|
44
|
+
/** Advances transport time until every promise settles; a rejection fails the case. */
|
|
45
|
+
awaitAll(promises: readonly Promise<unknown>[], what: string): Promise<void>;
|
|
46
|
+
advance(ms: number): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
export declare function openLink(adapters: TransportCaseAdapters, fault?: (timers: ManualTimers) => CarrierFault): TestLink;
|
|
49
|
+
export declare const range: (from: number, to: number) => number[];
|
|
50
|
+
/** Every lane got exactly 1..count, in order, once; nothing is left unacknowledged; nothing acknowledged was re-sent. */
|
|
51
|
+
export declare function expectExactDelivery(link: TestLink, counts: Readonly<Record<string, number>>, what: string): void;
|
|
52
|
+
export declare const allDelivered: (link: TestLink, counts: Readonly<Record<string, number>>) => () => boolean;
|
|
53
|
+
//# sourceMappingURL=transport-harness.d.ts.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createManualTimers } from "../../durable/testing/manual-timers.js";
|
|
2
|
+
import { expectEqual } from "../../durable/testing/store-harness.js";
|
|
3
|
+
import { createInProcessNetwork } from "../in-process-carrier.js";
|
|
4
|
+
import { localEndpoint } from "../local-endpoint.js";
|
|
5
|
+
const TICK_MS = 5;
|
|
6
|
+
const DRAIN_TURNS = 32;
|
|
7
|
+
const drainMicrotasks = async () => {
|
|
8
|
+
for (let turn = 0; turn < DRAIN_TURNS; turn++)
|
|
9
|
+
await Promise.resolve();
|
|
10
|
+
};
|
|
11
|
+
function tap(side, decode, violations) {
|
|
12
|
+
const acked = new Map();
|
|
13
|
+
return {
|
|
14
|
+
attached: (connection) => side.attached({
|
|
15
|
+
send: (message) => {
|
|
16
|
+
const view = decode(message);
|
|
17
|
+
if (view.kind === "data" && view.seq <= (acked.get(view.lane) ?? 0)) {
|
|
18
|
+
violations.push(`${view.lane}#${view.seq} sent again after its ACK`);
|
|
19
|
+
}
|
|
20
|
+
return connection.send(message);
|
|
21
|
+
},
|
|
22
|
+
close: () => connection.close(),
|
|
23
|
+
}),
|
|
24
|
+
message: (message) => {
|
|
25
|
+
const view = decode(message);
|
|
26
|
+
if (view.kind === "ack")
|
|
27
|
+
acked.set(view.lane, Math.max(acked.get(view.lane) ?? 0, view.seq));
|
|
28
|
+
side.message(message);
|
|
29
|
+
},
|
|
30
|
+
writable: () => side.writable(),
|
|
31
|
+
detached: () => side.detached(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function openLink(adapters, fault) {
|
|
35
|
+
const timers = createManualTimers(0);
|
|
36
|
+
const network = createInProcessNetwork({ schedule: (deliver) => void timers.setTimer(deliver, 0) });
|
|
37
|
+
const sender = adapters.transport.createSide(timers);
|
|
38
|
+
const receiver = adapters.transport.createSide(timers);
|
|
39
|
+
const violations = [];
|
|
40
|
+
const log = [];
|
|
41
|
+
const received = new Map();
|
|
42
|
+
const gates = new Map();
|
|
43
|
+
const paces = new Map();
|
|
44
|
+
const installed = fault?.(timers);
|
|
45
|
+
const outer = (side) => {
|
|
46
|
+
const tapped = tap(side, (message) => adapters.transport.decode(message), violations);
|
|
47
|
+
return installed === undefined ? tapped : installed.wrap(tapped);
|
|
48
|
+
};
|
|
49
|
+
const endpoint = localEndpoint(adapters.platform, "transport-conformance", "/run/senpi-conformance");
|
|
50
|
+
network.listen(endpoint, outer(receiver));
|
|
51
|
+
const link = network.dial(endpoint, outer(sender));
|
|
52
|
+
const decoder = new TextDecoder();
|
|
53
|
+
receiver.onFrame(async (frame) => {
|
|
54
|
+
await gates.get(frame.lane)?.promise;
|
|
55
|
+
const paceMs = paces.get(frame.lane);
|
|
56
|
+
if (paceMs !== undefined)
|
|
57
|
+
await new Promise((resolve) => void timers.setTimer(resolve, paceMs));
|
|
58
|
+
const n = Number(decoder.decode(frame.payload));
|
|
59
|
+
received.set(frame.lane, [...(received.get(frame.lane) ?? []), n]);
|
|
60
|
+
log.push(`${frame.lane}:${n}`);
|
|
61
|
+
});
|
|
62
|
+
link.connect();
|
|
63
|
+
installed?.start(link);
|
|
64
|
+
const advance = async (ms) => {
|
|
65
|
+
const end = timers.now() + ms;
|
|
66
|
+
while (timers.now() < end) {
|
|
67
|
+
await timers.advanceTo(Math.min(end, timers.now() + TICK_MS));
|
|
68
|
+
await drainMicrotasks();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const encoder = new TextEncoder();
|
|
72
|
+
const runUntil = async (done, what, budgetMs = 120_000) => {
|
|
73
|
+
const end = timers.now() + budgetMs;
|
|
74
|
+
while (!done()) {
|
|
75
|
+
if (timers.now() >= end) {
|
|
76
|
+
const counts = [...received].map(([lane, ns]) => `${lane}=${ns.length}`).join(" ");
|
|
77
|
+
throw new Error(`${what}: not reached within ${budgetMs} ms of transport time (delivered ${counts})`);
|
|
78
|
+
}
|
|
79
|
+
await advance(TICK_MS);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
sender,
|
|
84
|
+
link,
|
|
85
|
+
timers,
|
|
86
|
+
log,
|
|
87
|
+
violations,
|
|
88
|
+
send: (lane, n) => sender.send(lane, encoder.encode(String(n))),
|
|
89
|
+
delivered: (lane) => received.get(lane) ?? [],
|
|
90
|
+
gate(lane) {
|
|
91
|
+
const gate = Promise.withResolvers();
|
|
92
|
+
gates.set(lane, gate);
|
|
93
|
+
return () => {
|
|
94
|
+
gates.delete(lane);
|
|
95
|
+
gate.resolve();
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
pace(lane, ms) {
|
|
99
|
+
paces.set(lane, ms);
|
|
100
|
+
},
|
|
101
|
+
runUntil,
|
|
102
|
+
async awaitAll(promises, what) {
|
|
103
|
+
let settled = 0;
|
|
104
|
+
let failure;
|
|
105
|
+
for (const promise of promises) {
|
|
106
|
+
promise.then(() => void (settled += 1), (error) => {
|
|
107
|
+
settled += 1;
|
|
108
|
+
failure ??= error;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
await runUntil(() => settled === promises.length, `${what}: every send settled`);
|
|
112
|
+
if (failure !== undefined)
|
|
113
|
+
throw failure;
|
|
114
|
+
},
|
|
115
|
+
advance,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export const range = (from, to) => Array.from({ length: to - from + 1 }, (_, i) => from + i);
|
|
119
|
+
/** Every lane got exactly 1..count, in order, once; nothing is left unacknowledged; nothing acknowledged was re-sent. */
|
|
120
|
+
export function expectExactDelivery(link, counts, what) {
|
|
121
|
+
const lanes = Object.keys(counts);
|
|
122
|
+
expectEqual(lanes.map((lane) => link.delivered(lane)), lanes.map((lane) => range(1, counts[lane] ?? 0)), `${what}: each lane delivered in order, once`);
|
|
123
|
+
expectEqual(lanes.map((lane) => link.sender.inFlight(lane)), lanes.map(() => 0), `${what}: nothing in flight`);
|
|
124
|
+
expectEqual(link.violations, [], `${what}: resume starts after each lane's last ACK`);
|
|
125
|
+
}
|
|
126
|
+
export const allDelivered = (link, counts) => () => Object.entries(counts).every(([lane, count]) => link.delivered(lane).length >= count);
|
|
127
|
+
//# sourceMappingURL=transport-harness.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Transport contract of the durable core (packages/agent/docs/durable-core-adr.md, "Transport").
|
|
3
|
+
*
|
|
4
|
+
* A transport delivers opaque payloads between two endpoints on named lanes (one lane per scope). It owns per-lane order,
|
|
5
|
+
* exactly-once delivery of each frame while both endpoints live, resume after the carrier drops and re-attaches, and
|
|
6
|
+
* backpressure. Loops, hands, command IDs, epochs, deduplication of commands, consent, fencing and credentials are
|
|
7
|
+
* endpoint duties; a transport never inspects a payload.
|
|
8
|
+
*/
|
|
9
|
+
/** One delivered frame: the lane it was sent on and its opaque payload. */
|
|
10
|
+
export interface TransportFrame {
|
|
11
|
+
readonly lane: string;
|
|
12
|
+
readonly payload: Uint8Array;
|
|
13
|
+
}
|
|
14
|
+
/** Receives frames. A lane's next frame is delivered only after this settles for the previous one; lanes are independent. */
|
|
15
|
+
export type FrameHandler = (frame: TransportFrame) => void | Promise<void>;
|
|
16
|
+
/** One endpoint of a transport link. */
|
|
17
|
+
export interface Transport {
|
|
18
|
+
/**
|
|
19
|
+
* Queues `payload` on `lane`. Resolves once the frame is inside the lane's send window, so a lane whose receiver falls
|
|
20
|
+
* behind holds its own senders and nobody else's. Rejects with {@link TransportClosedError} after `close()`.
|
|
21
|
+
*/
|
|
22
|
+
send(lane: string, payload: Uint8Array): Promise<void>;
|
|
23
|
+
/** Sets the receiver. Frames that arrived earlier are delivered once it is set. */
|
|
24
|
+
onFrame(handler: FrameHandler): void;
|
|
25
|
+
/** Frames on `lane` that were sent and not yet acknowledged by the peer. */
|
|
26
|
+
inFlight(lane: string): number;
|
|
27
|
+
close(): void;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A message channel under a transport (in process, a local socket, the relay). It is ordered while it lasts, may drop
|
|
31
|
+
* at any time, and under faults may lose or delay messages. Messages are opaque to it.
|
|
32
|
+
*/
|
|
33
|
+
export interface CarrierConnection {
|
|
34
|
+
/** Hands `message` to the carrier. `false` means it was not taken; the side waits for `writable()` and sends again. */
|
|
35
|
+
send(message: unknown): boolean;
|
|
36
|
+
close(): void;
|
|
37
|
+
}
|
|
38
|
+
/** What a carrier calls on the endpoint it serves. */
|
|
39
|
+
export interface CarrierSide {
|
|
40
|
+
/** A new connection is up (the first, or a re-attach after a drop). */
|
|
41
|
+
attached(connection: CarrierConnection): void;
|
|
42
|
+
message(message: unknown): void;
|
|
43
|
+
/** The current connection takes messages again after a refused `send`. */
|
|
44
|
+
writable(): void;
|
|
45
|
+
/** The current connection is gone; anything in flight on it is lost. */
|
|
46
|
+
detached(): void;
|
|
47
|
+
}
|
|
48
|
+
/** A transport endpoint as a carrier sees it. */
|
|
49
|
+
export interface TransportSide extends Transport, CarrierSide {
|
|
50
|
+
}
|
|
51
|
+
/** The clock a transport schedules on; tests pass manual timers. */
|
|
52
|
+
export interface TransportClock {
|
|
53
|
+
now(): number;
|
|
54
|
+
/** Runs `callback` after `ms`; the returned function cancels it. */
|
|
55
|
+
setTimer(callback: () => unknown, ms: number): () => void;
|
|
56
|
+
}
|
|
57
|
+
export declare class TransportClosedError extends Error {
|
|
58
|
+
constructor();
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Transport contract of the durable core (packages/agent/docs/durable-core-adr.md, "Transport").
|
|
3
|
+
*
|
|
4
|
+
* A transport delivers opaque payloads between two endpoints on named lanes (one lane per scope). It owns per-lane order,
|
|
5
|
+
* exactly-once delivery of each frame while both endpoints live, resume after the carrier drops and re-attaches, and
|
|
6
|
+
* backpressure. Loops, hands, command IDs, epochs, deduplication of commands, consent, fencing and credentials are
|
|
7
|
+
* endpoint duties; a transport never inspects a payload.
|
|
8
|
+
*/
|
|
9
|
+
export class TransportClosedError extends Error {
|
|
10
|
+
constructor() {
|
|
11
|
+
super("transport closed");
|
|
12
|
+
this.name = "TransportClosedError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=transport.js.map
|
package/dist/types.d.ts
CHANGED
|
@@ -185,6 +185,12 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
185
185
|
* Unset or non-positive values disable the start bound.
|
|
186
186
|
*/
|
|
187
187
|
streamStartTimeoutMs?: number;
|
|
188
|
+
/**
|
|
189
|
+
* Bytes the provider reports through `onTransportActivity` (SSE pings, keep-alive comments) keep the start and
|
|
190
|
+
* idle bounds from firing while they keep arriving within one bound. This caps how long keep-alives alone may
|
|
191
|
+
* hold a request without any assistant event. Unset or non-positive values leave keep-alives unbounded.
|
|
192
|
+
*/
|
|
193
|
+
streamKeepAliveTimeoutMs?: number;
|
|
188
194
|
/** Provider/SDK timeout override for only the first request in this loop invocation. */
|
|
189
195
|
initialRequestTimeoutMs?: number;
|
|
190
196
|
/** Stream-start timeout override for only the first request in this loop invocation. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-agent-core",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.30-2",
|
|
4
4
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
"types": "./dist/node.d.ts",
|
|
15
15
|
"import": "./dist/node.js"
|
|
16
16
|
},
|
|
17
|
+
"./conformance": {
|
|
18
|
+
"types": "./dist/conformance/index.d.ts",
|
|
19
|
+
"import": "./dist/conformance/index.js"
|
|
20
|
+
},
|
|
17
21
|
"./harness/context": {
|
|
18
22
|
"types": "./dist/harness/context.d.ts",
|
|
19
23
|
"import": "./dist/harness/context.js"
|
|
@@ -38,6 +42,10 @@
|
|
|
38
42
|
"types": "./dist/harness/session/testing/index.d.ts",
|
|
39
43
|
"import": "./dist/harness/session/testing/index.js"
|
|
40
44
|
},
|
|
45
|
+
"./testing/win32-sim": {
|
|
46
|
+
"types": "./dist/testing/win32-sim.d.ts",
|
|
47
|
+
"import": "./dist/testing/win32-sim.js"
|
|
48
|
+
},
|
|
41
49
|
"./package.json": "./package.json"
|
|
42
50
|
},
|
|
43
51
|
"files": [
|
|
@@ -62,8 +70,8 @@
|
|
|
62
70
|
},
|
|
63
71
|
"dependencies": {
|
|
64
72
|
"@earendil-works/chord": "0.99.1",
|
|
65
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.
|
|
66
|
-
"@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.10.
|
|
73
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.10.30-2",
|
|
74
|
+
"@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.10.30-2",
|
|
67
75
|
"diff": "9.0.0",
|
|
68
76
|
"ignore": "7.0.9",
|
|
69
77
|
"typebox": "1.3.34",
|