@morlay/session-rdb 0.0.15 → 0.0.16-alpha.0
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/README.md +11 -13
- package/dist/artifact.d.mts +6 -16
- package/dist/artifact.mjs +3 -3
- package/dist/{import-DFed8DBt.mjs → import-CKrzjXVB.mjs} +73 -83
- package/dist/import.d.mts +2 -3
- package/dist/import.mjs +2 -2
- package/dist/index-CgGDHQSK.d.mts +225 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +694 -169
- package/dist/log-DTJsxMud.mjs +314 -0
- package/dist/{schema-CFXZURX7.d.mts → schema-COK7-wRV.d.mts} +3 -15
- package/dist/sqlite-DCy4VTD8.mjs +698 -0
- package/dist/storage.d.mts +2 -7
- package/dist/storage.mjs +2 -3
- package/dist/testing.d.mts +30 -1
- package/dist/testing.mjs +676 -1991
- package/drizzle/postgres/20260908072805_v2_initial/migration.sql +58 -0
- package/drizzle/postgres/20260908072805_v2_initial/snapshot.json +686 -0
- package/drizzle/postgres/20260908072806_v3_drop_original_seq/migration.sql +1 -0
- package/drizzle/postgres/20260908072806_v3_drop_original_seq/snapshot.json +673 -0
- package/drizzle/sqlite/20260908072751_v2_initial/migration.sql +53 -0
- package/drizzle/sqlite/20260908072751_v2_initial/snapshot.json +492 -0
- package/drizzle/sqlite/20260908072752_v3_drop_original_seq/migration.sql +6 -0
- package/drizzle/sqlite/20260908072752_v3_drop_original_seq/snapshot.json +513 -0
- package/package.json +17 -12
- package/src/adapters/index.ts +10 -2
- package/src/adapters/to-postgres.ts +19 -15
- package/src/adapters/to-sqlite.ts +20 -14
- package/src/{entities → adapters}/types.ts +7 -8
- package/src/backend.ts +0 -7
- package/src/branch.ts +29 -110
- package/src/drizzle/postgres-v2.ts +11 -0
- package/src/drizzle/postgres-v3.ts +11 -0
- package/src/drizzle/sqlite-v2.ts +10 -0
- package/src/drizzle/sqlite-v3.ts +11 -0
- package/src/entities/index.ts +2 -30
- package/src/entities/v2/index.ts +20 -0
- package/src/entities/v2/session-events.ts +11 -0
- package/src/entities/v3/events.ts +27 -0
- package/src/entities/v3/index.ts +27 -0
- package/src/entities/v3/persistence-state.ts +10 -0
- package/src/entities/v3/schema-meta.ts +9 -0
- package/src/entities/v3/session-events.ts +26 -0
- package/src/entities/v3/sessions.ts +20 -0
- package/src/import.ts +65 -57
- package/src/index.ts +868 -223
- package/src/invariant.ts +0 -2
- package/src/legacy.ts +67 -0
- package/src/log.ts +41 -87
- package/src/postgres.ts +75 -93
- package/src/schema.ts +3 -14
- package/src/sqlite.ts +59 -46
- package/src/testing/contract.ts +460 -375
- package/src/testing/coordinator-contract.ts +108 -1374
- package/src/testing.ts +1 -6
- package/dist/index-uUvn6gYp.d.mts +0 -111
- package/dist/schema-vwzwEXNE.mjs +0 -878
- package/dist/sqlite-CG_Qcx5C.mjs +0 -356
- package/src/adapters/ddl.ts +0 -77
- package/src/entities/events.ts +0 -27
- package/src/entities/persistence-state.ts +0 -10
- package/src/entities/schema-meta.ts +0 -9
- package/src/entities/session-events.ts +0 -29
- package/src/entities/sessions.ts +0 -20
- package/src/migrate.ts +0 -137
package/dist/testing.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { SessionAlreadyExistsError, SessionAlreadyOwnedError, SessionFormatUnsupportedError, SessionHandleClosedError, SessionPersistenceNotFoundError, SessionReadOnlyError } from "@deepseek-ai/dsh-session-persistence";
|
|
2
|
+
import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, SessionSeq } from "@deepseek-ai/dsh-session";
|
|
2
3
|
import { SettingsProvider } from "@deepseek-ai/dsh-settings";
|
|
3
|
-
import { MessageId,
|
|
4
|
+
import { MessageId, createUserMessage, freezeMessage } from "@deepseek-ai/dsh-llm";
|
|
4
5
|
import { Context } from "@deepseek-ai/cordis";
|
|
5
|
-
import { scopeTarget } from "@deepseek-ai/dsh-scope";
|
|
6
6
|
//#region src/testing/helpers.ts
|
|
7
7
|
var EmptySettings = class extends SettingsProvider {
|
|
8
8
|
constructor(ctx) {
|
|
@@ -5116,7 +5116,7 @@ function manageArtifactAttachment(attachment) {
|
|
|
5116
5116
|
if (attachment.body != null) attachment.bodyEncoding ??= "base64";
|
|
5117
5117
|
}
|
|
5118
5118
|
//#endregion
|
|
5119
|
-
//#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.
|
|
5119
|
+
//#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.0_happy-dom@20.13.2_vite@8.2.2__9bde3b9553ba3388013323e9f29b314e/node_modules/vitest/dist/chunks/utils.BX5Fg8C4.js
|
|
5120
5120
|
const NAME_WORKER_STATE = "__vitest_worker__";
|
|
5121
5121
|
function getWorkerState() {
|
|
5122
5122
|
const workerState = globalThis[NAME_WORKER_STATE];
|
|
@@ -10019,10 +10019,10 @@ function offsetToLineNumber(source, offset) {
|
|
|
10019
10019
|
return line + 1;
|
|
10020
10020
|
}
|
|
10021
10021
|
//#endregion
|
|
10022
|
-
//#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.
|
|
10022
|
+
//#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.0_happy-dom@20.13.2_vite@8.2.2__9bde3b9553ba3388013323e9f29b314e/node_modules/vitest/dist/chunks/_commonjsHelpers.D26ty3Ew.js
|
|
10023
10023
|
var commonjsGlobal = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : {};
|
|
10024
10024
|
//#endregion
|
|
10025
|
-
//#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.
|
|
10025
|
+
//#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.0_happy-dom@20.13.2_vite@8.2.2__9bde3b9553ba3388013323e9f29b314e/node_modules/vitest/dist/chunks/rpc.MzXet3jl.js
|
|
10026
10026
|
const RealDate = Date;
|
|
10027
10027
|
let now = null;
|
|
10028
10028
|
var MockDate = class MockDate extends RealDate {
|
|
@@ -10987,7 +10987,7 @@ function raceWith(promise, other) {
|
|
|
10987
10987
|
}))]);
|
|
10988
10988
|
}
|
|
10989
10989
|
//#endregion
|
|
10990
|
-
//#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.
|
|
10990
|
+
//#region ../../node_modules/.pnpm/vitest@4.1.11_@opentelemetry+api@1.9.1_@types+node@26.5.0_happy-dom@20.13.2_vite@8.2.2__9bde3b9553ba3388013323e9f29b314e/node_modules/vitest/dist/chunks/test.DNmyFkvJ.js
|
|
10991
10991
|
var fakeTimersSrc = {};
|
|
10992
10992
|
var global$1;
|
|
10993
10993
|
var hasRequiredGlobal;
|
|
@@ -13675,6 +13675,7 @@ Object.defineProperty(globalThis, GLOBAL_EXPECT, {
|
|
|
13675
13675
|
});
|
|
13676
13676
|
//#endregion
|
|
13677
13677
|
//#region src/testing/contract.ts
|
|
13678
|
+
/** Build a minimal {@link SessionHeader} for a session id. */
|
|
13678
13679
|
function meta(id, cwd) {
|
|
13679
13680
|
return {
|
|
13680
13681
|
version: SESSION_FORMAT_VERSION,
|
|
@@ -13684,6 +13685,7 @@ function meta(id, cwd) {
|
|
|
13684
13685
|
...cwd === void 0 ? {} : { cwd }
|
|
13685
13686
|
};
|
|
13686
13687
|
}
|
|
13688
|
+
/** A well-formed one-turn event log (contiguous seqs from 0). */
|
|
13687
13689
|
function oneTurnLog() {
|
|
13688
13690
|
return [
|
|
13689
13691
|
{
|
|
@@ -13735,7 +13737,45 @@ function oneTurnLog() {
|
|
|
13735
13737
|
provider: "mock",
|
|
13736
13738
|
model: "mock"
|
|
13737
13739
|
}
|
|
13738
|
-
})
|
|
13740
|
+
}),
|
|
13741
|
+
stream: [
|
|
13742
|
+
{
|
|
13743
|
+
type: "chunk",
|
|
13744
|
+
time: 3,
|
|
13745
|
+
chunk: {
|
|
13746
|
+
type: "block-start",
|
|
13747
|
+
index: 0,
|
|
13748
|
+
blockType: "text"
|
|
13749
|
+
}
|
|
13750
|
+
},
|
|
13751
|
+
{
|
|
13752
|
+
type: "text-chunks",
|
|
13753
|
+
time0: 3,
|
|
13754
|
+
index: 0,
|
|
13755
|
+
dt: [],
|
|
13756
|
+
texts: ["hello"]
|
|
13757
|
+
},
|
|
13758
|
+
{
|
|
13759
|
+
type: "chunk",
|
|
13760
|
+
time: 4,
|
|
13761
|
+
chunk: {
|
|
13762
|
+
type: "block-end",
|
|
13763
|
+
index: 0,
|
|
13764
|
+
block: {
|
|
13765
|
+
type: "text",
|
|
13766
|
+
text: "hello"
|
|
13767
|
+
}
|
|
13768
|
+
}
|
|
13769
|
+
},
|
|
13770
|
+
{
|
|
13771
|
+
type: "chunk",
|
|
13772
|
+
time: 4,
|
|
13773
|
+
chunk: {
|
|
13774
|
+
type: "finish",
|
|
13775
|
+
reason: { kind: "stop" }
|
|
13776
|
+
}
|
|
13777
|
+
}
|
|
13778
|
+
]
|
|
13739
13779
|
},
|
|
13740
13780
|
surfaceOp: "append"
|
|
13741
13781
|
},
|
|
@@ -13759,6 +13799,23 @@ function oneTurnLog() {
|
|
|
13759
13799
|
}
|
|
13760
13800
|
];
|
|
13761
13801
|
}
|
|
13802
|
+
/** A contiguous second-turn batch continuing {@link oneTurnLog}. */
|
|
13803
|
+
function secondTurn(startSeq = 6) {
|
|
13804
|
+
return [{
|
|
13805
|
+
type: "turn/start",
|
|
13806
|
+
seq: SessionSeq(startSeq),
|
|
13807
|
+
time: 9,
|
|
13808
|
+
data: { turn: 2 }
|
|
13809
|
+
}, {
|
|
13810
|
+
type: "turn/end",
|
|
13811
|
+
seq: SessionSeq(startSeq + 1),
|
|
13812
|
+
time: 10,
|
|
13813
|
+
data: {
|
|
13814
|
+
turn: 2,
|
|
13815
|
+
reason: { kind: "completed" }
|
|
13816
|
+
}
|
|
13817
|
+
}];
|
|
13818
|
+
}
|
|
13762
13819
|
function appendLog(session, events) {
|
|
13763
13820
|
for (const e of events) {
|
|
13764
13821
|
const se = e;
|
|
@@ -13771,123 +13828,115 @@ function appendLog(session, events) {
|
|
|
13771
13828
|
} else session.append(e.type, e.data);
|
|
13772
13829
|
}
|
|
13773
13830
|
}
|
|
13831
|
+
/**
|
|
13832
|
+
* Run the backend-agnostic handle contract suite. `make()` MUST return a
|
|
13833
|
+
* fresh backend over fresh, empty storage each call.
|
|
13834
|
+
* @param name - suite label, e.g. `jsonl-none` / `sqlite`.
|
|
13835
|
+
* @param make - factory producing one fresh {@link ContractBackend} per test.
|
|
13836
|
+
*/
|
|
13774
13837
|
function runPersistenceContract(name, make) {
|
|
13775
13838
|
describe(`SessionPersistence contract: ${name}`, () => {
|
|
13776
|
-
it("round-trips
|
|
13839
|
+
it("round-trips through one write handle: append, self-read, offset/length defaults", async () => {
|
|
13777
13840
|
const { persistence, dispose } = await make();
|
|
13778
13841
|
try {
|
|
13779
|
-
const m = meta("
|
|
13842
|
+
const m = meta("round-trip", "/work");
|
|
13780
13843
|
const log = oneTurnLog();
|
|
13781
|
-
await persistence.create(m);
|
|
13782
|
-
|
|
13783
|
-
|
|
13784
|
-
globalExpect(
|
|
13785
|
-
|
|
13786
|
-
|
|
13787
|
-
|
|
13788
|
-
|
|
13789
|
-
globalExpect(
|
|
13844
|
+
const handle = await persistence.create(m);
|
|
13845
|
+
globalExpect(handle.access).toBe("write");
|
|
13846
|
+
globalExpect(handle.id).toBe(m.id);
|
|
13847
|
+
globalExpect(handle.header).toMatchObject(m);
|
|
13848
|
+
await handle.append(log);
|
|
13849
|
+
await handle.append([]);
|
|
13850
|
+
const full = await handle.read();
|
|
13851
|
+
globalExpect(full.events).toEqual(log);
|
|
13852
|
+
globalExpect((await handle.read(3)).events).toEqual(log.slice(3));
|
|
13853
|
+
globalExpect((await handle.read(0, 2)).events).toEqual(log.slice(0, 2));
|
|
13854
|
+
globalExpect((await handle.read(1, 3)).events).toEqual(log.slice(1, 4));
|
|
13855
|
+
globalExpect((await handle.read(log.length)).events).toEqual([]);
|
|
13856
|
+
globalExpect((await handle.read(log.length + 100)).events).toEqual([]);
|
|
13857
|
+
await handle.flush();
|
|
13858
|
+
await handle.close();
|
|
13790
13859
|
} finally {
|
|
13791
13860
|
await dispose();
|
|
13792
13861
|
}
|
|
13793
13862
|
});
|
|
13794
|
-
it("rejects
|
|
13863
|
+
it("read rejects negative or fractional offsets and lengths", async () => {
|
|
13795
13864
|
const { persistence, dispose } = await make();
|
|
13796
13865
|
try {
|
|
13797
|
-
const
|
|
13798
|
-
|
|
13799
|
-
|
|
13800
|
-
|
|
13801
|
-
await
|
|
13802
|
-
const valid = meta("fractional-created-at");
|
|
13803
|
-
await persistence.create(valid);
|
|
13804
|
-
await persistence.append(valid.id, oneTurnLog());
|
|
13805
|
-
globalExpect((await persistence.load(valid.id)).meta.createdAt).toBe(valid.createdAt);
|
|
13866
|
+
const handle = await persistence.create(meta("read-args"));
|
|
13867
|
+
await globalExpect(handle.read(-1)).rejects.toThrow(/non-negative safe integer/);
|
|
13868
|
+
await globalExpect(handle.read(1.5)).rejects.toThrow(/non-negative safe integer/);
|
|
13869
|
+
await globalExpect(handle.read(0, -1)).rejects.toThrow(/non-negative safe integer/);
|
|
13870
|
+
await handle.close();
|
|
13806
13871
|
} finally {
|
|
13807
13872
|
await dispose();
|
|
13808
13873
|
}
|
|
13809
13874
|
});
|
|
13810
|
-
it("
|
|
13875
|
+
it("duplicate create rejects against a live pending session and allows the id after an erasing close", async () => {
|
|
13811
13876
|
const { persistence, dispose } = await make();
|
|
13812
13877
|
try {
|
|
13813
|
-
const
|
|
13814
|
-
await persistence.create(
|
|
13815
|
-
await
|
|
13816
|
-
await persistence.
|
|
13817
|
-
|
|
13818
|
-
|
|
13819
|
-
|
|
13820
|
-
|
|
13821
|
-
|
|
13822
|
-
|
|
13823
|
-
|
|
13824
|
-
|
|
13825
|
-
|
|
13826
|
-
|
|
13827
|
-
|
|
13828
|
-
|
|
13829
|
-
|
|
13830
|
-
|
|
13831
|
-
|
|
13832
|
-
|
|
13833
|
-
|
|
13834
|
-
|
|
13835
|
-
|
|
13836
|
-
|
|
13837
|
-
|
|
13838
|
-
|
|
13839
|
-
|
|
13840
|
-
|
|
13841
|
-
|
|
13842
|
-
|
|
13843
|
-
|
|
13844
|
-
|
|
13845
|
-
|
|
13846
|
-
|
|
13847
|
-
|
|
13848
|
-
|
|
13849
|
-
|
|
13850
|
-
|
|
13851
|
-
|
|
13852
|
-
|
|
13853
|
-
|
|
13854
|
-
|
|
13855
|
-
|
|
13856
|
-
|
|
13857
|
-
|
|
13858
|
-
|
|
13859
|
-
|
|
13860
|
-
|
|
13861
|
-
|
|
13862
|
-
|
|
13863
|
-
|
|
13864
|
-
|
|
13865
|
-
|
|
13866
|
-
|
|
13867
|
-
|
|
13868
|
-
|
|
13869
|
-
|
|
13870
|
-
|
|
13871
|
-
|
|
13872
|
-
|
|
13873
|
-
|
|
13874
|
-
globalExpect(
|
|
13875
|
-
await persistence.append(m.id, [{
|
|
13876
|
-
type: "turn/start",
|
|
13877
|
-
seq: SessionSeq(10),
|
|
13878
|
-
time: 9,
|
|
13879
|
-
data: { turn: 3 }
|
|
13880
|
-
}, {
|
|
13881
|
-
type: "turn/end",
|
|
13882
|
-
seq: SessionSeq(11),
|
|
13883
|
-
time: 10,
|
|
13884
|
-
data: {
|
|
13885
|
-
turn: 3,
|
|
13886
|
-
reason: { kind: "completed" }
|
|
13887
|
-
}
|
|
13888
|
-
}]);
|
|
13889
|
-
const reloaded = await persistence.load(m.id);
|
|
13890
|
-
globalExpect(reloaded.events.map((e) => e.seq)).toEqual([
|
|
13878
|
+
const first = await persistence.create(meta("dup-pending"));
|
|
13879
|
+
await globalExpect(persistence.create(meta("dup-pending"))).rejects.toBeInstanceOf(SessionAlreadyExistsError);
|
|
13880
|
+
await first.close();
|
|
13881
|
+
await (await persistence.create(meta("dup-pending"))).close();
|
|
13882
|
+
} finally {
|
|
13883
|
+
await dispose();
|
|
13884
|
+
}
|
|
13885
|
+
});
|
|
13886
|
+
it("concurrent duplicate creates: one wins, the loser rejects SessionAlreadyExistsError", async () => {
|
|
13887
|
+
const { persistence, dispose } = await make();
|
|
13888
|
+
try {
|
|
13889
|
+
const m = meta("dup-race");
|
|
13890
|
+
const results = await Promise.allSettled([persistence.create(m), persistence.create(m)]);
|
|
13891
|
+
const winners = results.filter((r) => r.status === "fulfilled");
|
|
13892
|
+
const losers = results.filter((r) => r.status === "rejected");
|
|
13893
|
+
globalExpect(winners).toHaveLength(1);
|
|
13894
|
+
globalExpect(losers).toHaveLength(1);
|
|
13895
|
+
globalExpect(losers[0].reason).toBeInstanceOf(SessionAlreadyExistsError);
|
|
13896
|
+
await winners[0].value.close();
|
|
13897
|
+
} finally {
|
|
13898
|
+
await dispose();
|
|
13899
|
+
}
|
|
13900
|
+
});
|
|
13901
|
+
it("duplicate create rejects against a materialized artifact seen by a fresh instance", async () => {
|
|
13902
|
+
const backend = await make();
|
|
13903
|
+
try {
|
|
13904
|
+
if (backend.reopen === void 0) return;
|
|
13905
|
+
const m = meta("dup-stored", "/work");
|
|
13906
|
+
const handle = await backend.persistence.create(m);
|
|
13907
|
+
await handle.append(oneTurnLog());
|
|
13908
|
+
await handle.close();
|
|
13909
|
+
const reopened = await backend.reopen();
|
|
13910
|
+
try {
|
|
13911
|
+
await globalExpect(reopened.persistence.create(meta("dup-stored", "/work"))).rejects.toBeInstanceOf(SessionAlreadyExistsError);
|
|
13912
|
+
} finally {
|
|
13913
|
+
await reopened.dispose();
|
|
13914
|
+
}
|
|
13915
|
+
} finally {
|
|
13916
|
+
await backend.dispose();
|
|
13917
|
+
}
|
|
13918
|
+
});
|
|
13919
|
+
it("open of an absent session rejects with SessionPersistenceNotFoundError for both accesses", async () => {
|
|
13920
|
+
const { persistence, dispose } = await make();
|
|
13921
|
+
try {
|
|
13922
|
+
await globalExpect(persistence.open(SessionId("absent"), "read")).rejects.toBeInstanceOf(SessionPersistenceNotFoundError);
|
|
13923
|
+
await globalExpect(persistence.open(SessionId("absent"), "write")).rejects.toBeInstanceOf(SessionPersistenceNotFoundError);
|
|
13924
|
+
} finally {
|
|
13925
|
+
await dispose();
|
|
13926
|
+
}
|
|
13927
|
+
});
|
|
13928
|
+
it("write ownership is single-holder per instance and released by close", async () => {
|
|
13929
|
+
const { persistence, dispose } = await make();
|
|
13930
|
+
try {
|
|
13931
|
+
const m = meta("owned");
|
|
13932
|
+
const creator = await persistence.create(m);
|
|
13933
|
+
await globalExpect(persistence.open(m.id, "write")).rejects.toBeInstanceOf(SessionAlreadyOwnedError);
|
|
13934
|
+
await creator.append(oneTurnLog());
|
|
13935
|
+
await globalExpect(persistence.open(m.id, "write")).rejects.toBeInstanceOf(SessionAlreadyOwnedError);
|
|
13936
|
+
await creator.close();
|
|
13937
|
+
const writer = await persistence.open(m.id, "write");
|
|
13938
|
+
await writer.append(secondTurn());
|
|
13939
|
+
globalExpect((await writer.read()).events.map((e) => e.seq)).toEqual([
|
|
13891
13940
|
0,
|
|
13892
13941
|
1,
|
|
13893
13942
|
2,
|
|
@@ -13895,331 +13944,371 @@ function runPersistenceContract(name, make) {
|
|
|
13895
13944
|
4,
|
|
13896
13945
|
5,
|
|
13897
13946
|
6,
|
|
13898
|
-
7
|
|
13899
|
-
8,
|
|
13900
|
-
9,
|
|
13901
|
-
10,
|
|
13902
|
-
11
|
|
13947
|
+
7
|
|
13903
13948
|
]);
|
|
13949
|
+
await writer.close();
|
|
13904
13950
|
} finally {
|
|
13905
13951
|
await dispose();
|
|
13906
13952
|
}
|
|
13907
13953
|
});
|
|
13908
|
-
it("
|
|
13954
|
+
it("a read handle refuses append and flush with SessionReadOnlyError", async () => {
|
|
13909
13955
|
const { persistence, dispose } = await make();
|
|
13910
13956
|
try {
|
|
13911
|
-
const m = meta("
|
|
13912
|
-
await persistence.create(m);
|
|
13913
|
-
await
|
|
13914
|
-
await
|
|
13915
|
-
|
|
13916
|
-
|
|
13917
|
-
|
|
13918
|
-
|
|
13919
|
-
|
|
13920
|
-
|
|
13921
|
-
{
|
|
13922
|
-
type: "step/start",
|
|
13923
|
-
seq: SessionSeq(7),
|
|
13924
|
-
time: 8,
|
|
13925
|
-
data: {
|
|
13926
|
-
turn: 2,
|
|
13927
|
-
step: 1
|
|
13928
|
-
}
|
|
13929
|
-
},
|
|
13930
|
-
{
|
|
13931
|
-
type: "assistant/message",
|
|
13932
|
-
seq: SessionSeq(8),
|
|
13933
|
-
time: 9,
|
|
13934
|
-
data: {
|
|
13935
|
-
turn: 2,
|
|
13936
|
-
step: 1,
|
|
13937
|
-
message: createMessage({
|
|
13938
|
-
role: "assistant",
|
|
13939
|
-
content: [{
|
|
13940
|
-
type: "tool-call",
|
|
13941
|
-
id: ToolCallId("call-x"),
|
|
13942
|
-
name: "bash",
|
|
13943
|
-
arguments: "{}"
|
|
13944
|
-
}],
|
|
13945
|
-
source: {
|
|
13946
|
-
kind: "model",
|
|
13947
|
-
provider: "mock",
|
|
13948
|
-
model: "mock"
|
|
13949
|
-
}
|
|
13950
|
-
})
|
|
13951
|
-
},
|
|
13952
|
-
surfaceOp: "append"
|
|
13953
|
-
}
|
|
13954
|
-
]);
|
|
13955
|
-
const loaded = await persistence.load(m.id);
|
|
13956
|
-
globalExpect(loaded.events.map((e) => e.type)).toEqual([
|
|
13957
|
-
"turn/start",
|
|
13958
|
-
"user/message",
|
|
13959
|
-
"step/start",
|
|
13960
|
-
"assistant/message",
|
|
13961
|
-
"step/end",
|
|
13962
|
-
"turn/end",
|
|
13963
|
-
"turn/start",
|
|
13964
|
-
"step/start",
|
|
13965
|
-
"assistant/message",
|
|
13966
|
-
"tool/result",
|
|
13967
|
-
"step/end",
|
|
13968
|
-
"turn/end"
|
|
13969
|
-
]);
|
|
13970
|
-
const synthetic = loaded.events.find((e) => e.type === "tool/result");
|
|
13971
|
-
globalExpect(synthetic?.type === "tool/result" && synthetic.data).toMatchObject({
|
|
13972
|
-
message: {
|
|
13973
|
-
source: {
|
|
13974
|
-
kind: "tool",
|
|
13975
|
-
callId: ToolCallId("call-x")
|
|
13976
|
-
},
|
|
13977
|
-
content: [{
|
|
13978
|
-
type: "tool-result",
|
|
13979
|
-
toolCallId: ToolCallId("call-x"),
|
|
13980
|
-
isError: true
|
|
13981
|
-
}]
|
|
13982
|
-
},
|
|
13983
|
-
error: { code: TOOL_NOT_STARTED }
|
|
13984
|
-
});
|
|
13985
|
-
const call = loaded.events.findLast((e) => e.type === "assistant/message");
|
|
13986
|
-
const callId = call?.type === "assistant/message" && call.data.message.content.find((b) => b.type === "tool-call");
|
|
13987
|
-
globalExpect(callId && callId.type === "tool-call" && callId.id).toBe(ToolCallId("call-x"));
|
|
13957
|
+
const m = meta("read-only");
|
|
13958
|
+
const writer = await persistence.create(m);
|
|
13959
|
+
await writer.append(oneTurnLog());
|
|
13960
|
+
await writer.close();
|
|
13961
|
+
const reader = await persistence.open(m.id, "read");
|
|
13962
|
+
globalExpect(reader.access).toBe("read");
|
|
13963
|
+
await globalExpect(reader.append(secondTurn())).rejects.toBeInstanceOf(SessionReadOnlyError);
|
|
13964
|
+
await globalExpect(reader.flush()).rejects.toBeInstanceOf(SessionReadOnlyError);
|
|
13965
|
+
globalExpect((await reader.read()).events).toEqual(oneTurnLog());
|
|
13966
|
+
await reader.close();
|
|
13988
13967
|
} finally {
|
|
13989
13968
|
await dispose();
|
|
13990
13969
|
}
|
|
13991
13970
|
});
|
|
13992
|
-
it("
|
|
13971
|
+
it("operations on a closed handle reject; close is idempotent; asyncDispose releases ownership", async () => {
|
|
13993
13972
|
const { persistence, dispose } = await make();
|
|
13994
13973
|
try {
|
|
13995
|
-
const m = meta("
|
|
13996
|
-
await persistence.create(m);
|
|
13997
|
-
await
|
|
13998
|
-
|
|
13999
|
-
|
|
14000
|
-
|
|
14001
|
-
|
|
14002
|
-
|
|
14003
|
-
|
|
14004
|
-
|
|
14005
|
-
|
|
14006
|
-
|
|
14007
|
-
|
|
14008
|
-
data: {
|
|
14009
|
-
turn: 1,
|
|
14010
|
-
step: 1
|
|
14011
|
-
}
|
|
14012
|
-
},
|
|
14013
|
-
{
|
|
14014
|
-
type: "assistant/message",
|
|
14015
|
-
seq: SessionSeq(2),
|
|
14016
|
-
time: 3,
|
|
14017
|
-
data: {
|
|
14018
|
-
turn: 1,
|
|
14019
|
-
step: 1,
|
|
14020
|
-
message: createMessage({
|
|
14021
|
-
role: "assistant",
|
|
14022
|
-
content: [{
|
|
14023
|
-
type: "tool-call",
|
|
14024
|
-
id: ToolCallId("call-risk"),
|
|
14025
|
-
name: "write",
|
|
14026
|
-
arguments: "{}"
|
|
14027
|
-
}],
|
|
14028
|
-
source: {
|
|
14029
|
-
kind: "model",
|
|
14030
|
-
provider: "mock",
|
|
14031
|
-
model: "mock"
|
|
14032
|
-
}
|
|
14033
|
-
})
|
|
14034
|
-
},
|
|
14035
|
-
surfaceOp: "append"
|
|
14036
|
-
},
|
|
14037
|
-
{
|
|
14038
|
-
type: "tool/call",
|
|
14039
|
-
seq: SessionSeq(3),
|
|
14040
|
-
time: 4,
|
|
14041
|
-
data: {
|
|
14042
|
-
turn: 1,
|
|
14043
|
-
step: 1,
|
|
14044
|
-
callId: ToolCallId("call-risk"),
|
|
14045
|
-
name: "write",
|
|
14046
|
-
arguments: "{}"
|
|
14047
|
-
}
|
|
14048
|
-
}
|
|
14049
|
-
]);
|
|
14050
|
-
const loaded = await persistence.load(m.id);
|
|
14051
|
-
const synthetic = loaded.events.find((e) => e.type === "tool/result");
|
|
14052
|
-
globalExpect(synthetic?.type === "tool/result" && synthetic.data.error).toEqual({
|
|
14053
|
-
name: "ToolOutcomeUnknownError",
|
|
14054
|
-
code: TOOL_OUTCOME_UNKNOWN
|
|
14055
|
-
});
|
|
14056
|
-
if (synthetic?.type !== "tool/result" || synthetic.data.message.content[0].content[0]?.type !== "text") throw new Error("expected a text tool result");
|
|
14057
|
-
globalExpect(synthetic.data.message.content[0].content[0].text).toContain("retry only if the operation is read-only or idempotent");
|
|
14058
|
-
globalExpect(synthetic.data.message.content[0].content[0].text).toContain("if it may have side effects, first verify external state or ask the user");
|
|
14059
|
-
const resumedResult = Session.create(m.id, loaded.events, loaded.meta, loaded.inheritedEventCount).deriveMessages().find((message) => message.content.some((block) => block.type === "tool-result"));
|
|
14060
|
-
globalExpect(resumedResult?.content[0]).toMatchObject({
|
|
14061
|
-
type: "tool-result",
|
|
14062
|
-
toolCallId: ToolCallId("call-risk"),
|
|
14063
|
-
isError: true
|
|
14064
|
-
});
|
|
13974
|
+
const m = meta("closed");
|
|
13975
|
+
const handle = await persistence.create(m);
|
|
13976
|
+
await handle.append(oneTurnLog());
|
|
13977
|
+
await handle.close();
|
|
13978
|
+
await handle.close();
|
|
13979
|
+
await globalExpect(handle.read()).rejects.toBeInstanceOf(SessionHandleClosedError);
|
|
13980
|
+
await globalExpect(handle.append(secondTurn())).rejects.toBeInstanceOf(SessionHandleClosedError);
|
|
13981
|
+
await globalExpect(handle.flush()).rejects.toBeInstanceOf(SessionHandleClosedError);
|
|
13982
|
+
{
|
|
13983
|
+
await using writer = await persistence.open(m.id, "write");
|
|
13984
|
+
await writer.append(secondTurn());
|
|
13985
|
+
}
|
|
13986
|
+
await (await persistence.open(m.id, "write")).close();
|
|
14065
13987
|
} finally {
|
|
14066
13988
|
await dispose();
|
|
14067
13989
|
}
|
|
14068
13990
|
});
|
|
14069
|
-
it("
|
|
14070
|
-
const
|
|
13991
|
+
it("service-level flush materializes every active write handle and counts a closing one as flushed", async () => {
|
|
13992
|
+
const backend = await make();
|
|
13993
|
+
try {
|
|
13994
|
+
const materialized = await backend.persistence.create(meta("flush-all"));
|
|
13995
|
+
const closing = (await backend.persistence.create(meta("flush-all-closing"))).close();
|
|
13996
|
+
await backend.persistence.flush();
|
|
13997
|
+
await closing;
|
|
13998
|
+
if (backend.reopen !== void 0) {
|
|
13999
|
+
const reopened = await backend.reopen();
|
|
14000
|
+
try {
|
|
14001
|
+
globalExpect(await reopened.persistence.stat(SessionId("flush-all"))).toBeDefined();
|
|
14002
|
+
globalExpect(await reopened.persistence.stat(SessionId("flush-all-closing"))).toBeUndefined();
|
|
14003
|
+
} finally {
|
|
14004
|
+
await reopened.dispose();
|
|
14005
|
+
}
|
|
14006
|
+
}
|
|
14007
|
+
await materialized.close();
|
|
14008
|
+
} finally {
|
|
14009
|
+
await backend.dispose();
|
|
14010
|
+
}
|
|
14011
|
+
});
|
|
14012
|
+
it("a created-but-unappended session is visible to this instance and invisible to a fresh one", async () => {
|
|
14013
|
+
const backend = await make();
|
|
14071
14014
|
try {
|
|
14072
|
-
|
|
14073
|
-
|
|
14074
|
-
globalExpect((await
|
|
14015
|
+
const m = meta("lazy", "/work");
|
|
14016
|
+
const creator = await backend.persistence.create(m);
|
|
14017
|
+
globalExpect((await creator.read()).events).toEqual([]);
|
|
14018
|
+
const snapshot = await backend.persistence.stat(m.id);
|
|
14019
|
+
globalExpect(snapshot?.header).toMatchObject(m);
|
|
14020
|
+
globalExpect((await backend.persistence.list()).map((s) => s.header.id)).toContain(m.id);
|
|
14021
|
+
const reader = await backend.persistence.open(m.id, "read");
|
|
14022
|
+
globalExpect((await reader.read()).events).toEqual([]);
|
|
14023
|
+
await reader.close();
|
|
14024
|
+
if (backend.reopen !== void 0) {
|
|
14025
|
+
const reopened = await backend.reopen();
|
|
14026
|
+
try {
|
|
14027
|
+
globalExpect(await reopened.persistence.stat(m.id)).toBeUndefined();
|
|
14028
|
+
globalExpect((await reopened.persistence.list()).map((s) => s.header.id)).not.toContain(m.id);
|
|
14029
|
+
await globalExpect(reopened.persistence.open(m.id, "read")).rejects.toBeInstanceOf(SessionPersistenceNotFoundError);
|
|
14030
|
+
} finally {
|
|
14031
|
+
await reopened.dispose();
|
|
14032
|
+
}
|
|
14033
|
+
}
|
|
14034
|
+
await creator.close();
|
|
14075
14035
|
} finally {
|
|
14076
|
-
await dispose();
|
|
14036
|
+
await backend.dispose();
|
|
14077
14037
|
}
|
|
14078
14038
|
});
|
|
14079
|
-
it("
|
|
14039
|
+
it("close without an append erases the created session from this instance", async () => {
|
|
14080
14040
|
const { persistence, dispose } = await make();
|
|
14081
14041
|
try {
|
|
14082
|
-
const
|
|
14083
|
-
|
|
14084
|
-
await
|
|
14085
|
-
|
|
14086
|
-
await globalExpect(persistence.
|
|
14087
|
-
await globalExpect(persistence.listSnapshots(controller.signal)).rejects.toBe(reason);
|
|
14088
|
-
await globalExpect(persistence.inspect(SessionId("cancelled-inspect"), controller.signal)).rejects.toBe(reason);
|
|
14089
|
-
await globalExpect(persistence.readFrom(SessionId("cancelled-read-from"), SessionLogOffset(0), controller.signal)).rejects.toBe(reason);
|
|
14042
|
+
const m = meta("never-was");
|
|
14043
|
+
await (await persistence.create(m)).close();
|
|
14044
|
+
globalExpect(await persistence.stat(m.id)).toBeUndefined();
|
|
14045
|
+
globalExpect((await persistence.list()).map((s) => s.header.id)).not.toContain(m.id);
|
|
14046
|
+
await globalExpect(persistence.open(m.id, "read")).rejects.toBeInstanceOf(SessionPersistenceNotFoundError);
|
|
14090
14047
|
} finally {
|
|
14091
14048
|
await dispose();
|
|
14092
14049
|
}
|
|
14093
14050
|
});
|
|
14094
|
-
it("
|
|
14095
|
-
const
|
|
14051
|
+
it("flush materializes an empty session durably for a fresh instance", async () => {
|
|
14052
|
+
const backend = await make();
|
|
14096
14053
|
try {
|
|
14097
|
-
|
|
14098
|
-
const
|
|
14099
|
-
await persistence.create(m);
|
|
14100
|
-
await
|
|
14101
|
-
|
|
14102
|
-
|
|
14103
|
-
|
|
14104
|
-
|
|
14105
|
-
|
|
14106
|
-
|
|
14107
|
-
|
|
14108
|
-
|
|
14109
|
-
|
|
14110
|
-
|
|
14111
|
-
|
|
14112
|
-
await persistence.append(m.id, [{
|
|
14113
|
-
type: "turn/start",
|
|
14114
|
-
seq: SessionSeq(6),
|
|
14115
|
-
time: 7,
|
|
14116
|
-
data: { turn: 2 }
|
|
14117
|
-
}]);
|
|
14118
|
-
const tail = await persistence.readFrom(m.id, SessionLogOffset(6));
|
|
14119
|
-
globalExpect(tail.events.map((event) => event.type)).toEqual(["turn/start"]);
|
|
14120
|
-
await globalExpect(persistence.readFrom(SessionId("absent-read-from"), SessionLogOffset(0))).rejects.toThrow("not found");
|
|
14121
|
-
await globalExpect(persistence.readFrom(m.id, -1)).rejects.toThrow("non-negative safe integer");
|
|
14122
|
-
await globalExpect(persistence.readFrom(m.id, 1.5)).rejects.toThrow("non-negative safe integer");
|
|
14054
|
+
if (backend.reopen === void 0) return;
|
|
14055
|
+
const m = meta("durable-empty", "/work");
|
|
14056
|
+
const creator = await backend.persistence.create(m);
|
|
14057
|
+
await creator.flush();
|
|
14058
|
+
await creator.close();
|
|
14059
|
+
const reopened = await backend.reopen();
|
|
14060
|
+
try {
|
|
14061
|
+
globalExpect((await reopened.persistence.list()).map((s) => s.header.id)).toContain(m.id);
|
|
14062
|
+
globalExpect((await reopened.persistence.stat(m.id))?.header).toMatchObject(m);
|
|
14063
|
+
const reader = await reopened.persistence.open(m.id, "read");
|
|
14064
|
+
globalExpect((await reader.read()).events).toEqual([]);
|
|
14065
|
+
await reader.close();
|
|
14066
|
+
} finally {
|
|
14067
|
+
await reopened.dispose();
|
|
14068
|
+
}
|
|
14123
14069
|
} finally {
|
|
14124
|
-
await dispose();
|
|
14070
|
+
await backend.dispose();
|
|
14125
14071
|
}
|
|
14126
14072
|
});
|
|
14127
|
-
it("
|
|
14073
|
+
it("freshness: reads started after an append resolves observe that prefix on any handle", async () => {
|
|
14128
14074
|
const { persistence, dispose } = await make();
|
|
14129
14075
|
try {
|
|
14130
|
-
const m = meta("
|
|
14131
|
-
await persistence.create(m);
|
|
14132
|
-
await
|
|
14133
|
-
|
|
14134
|
-
|
|
14135
|
-
|
|
14136
|
-
globalExpect(
|
|
14137
|
-
|
|
14138
|
-
|
|
14139
|
-
|
|
14140
|
-
|
|
14141
|
-
|
|
14142
|
-
|
|
14143
|
-
|
|
14144
|
-
|
|
14145
|
-
|
|
14076
|
+
const m = meta("fresh");
|
|
14077
|
+
const writer = await persistence.create(m);
|
|
14078
|
+
await writer.append(oneTurnLog());
|
|
14079
|
+
const before = await persistence.open(m.id, "read");
|
|
14080
|
+
globalExpect((await before.read()).events).toEqual(oneTurnLog());
|
|
14081
|
+
await writer.append(secondTurn());
|
|
14082
|
+
globalExpect((await before.read()).events.map((e) => e.seq)).toEqual([
|
|
14083
|
+
0,
|
|
14084
|
+
1,
|
|
14085
|
+
2,
|
|
14086
|
+
3,
|
|
14087
|
+
4,
|
|
14088
|
+
5,
|
|
14089
|
+
6,
|
|
14090
|
+
7
|
|
14091
|
+
]);
|
|
14092
|
+
const after = await persistence.open(m.id, "read");
|
|
14093
|
+
globalExpect((await after.read()).events.map((e) => e.seq)).toEqual([
|
|
14094
|
+
0,
|
|
14095
|
+
1,
|
|
14096
|
+
2,
|
|
14097
|
+
3,
|
|
14098
|
+
4,
|
|
14099
|
+
5,
|
|
14100
|
+
6,
|
|
14101
|
+
7
|
|
14102
|
+
]);
|
|
14103
|
+
await before.close();
|
|
14104
|
+
await after.close();
|
|
14105
|
+
await writer.close();
|
|
14146
14106
|
} finally {
|
|
14147
14107
|
await dispose();
|
|
14148
14108
|
}
|
|
14149
14109
|
});
|
|
14150
|
-
it("
|
|
14110
|
+
it("a fresh instance continues the stored log at the committed next-seq", async () => {
|
|
14111
|
+
const backend = await make();
|
|
14112
|
+
try {
|
|
14113
|
+
if (backend.reopen === void 0) return;
|
|
14114
|
+
const m = meta("continue", "/work");
|
|
14115
|
+
const creator = await backend.persistence.create(m);
|
|
14116
|
+
await creator.append(oneTurnLog());
|
|
14117
|
+
await creator.close();
|
|
14118
|
+
const reopened = await backend.reopen();
|
|
14119
|
+
try {
|
|
14120
|
+
const writer = await reopened.persistence.open(m.id, "write");
|
|
14121
|
+
await writer.append(secondTurn());
|
|
14122
|
+
globalExpect((await writer.read()).events.map((e) => e.seq)).toEqual([
|
|
14123
|
+
0,
|
|
14124
|
+
1,
|
|
14125
|
+
2,
|
|
14126
|
+
3,
|
|
14127
|
+
4,
|
|
14128
|
+
5,
|
|
14129
|
+
6,
|
|
14130
|
+
7
|
|
14131
|
+
]);
|
|
14132
|
+
await writer.close();
|
|
14133
|
+
} finally {
|
|
14134
|
+
await reopened.dispose();
|
|
14135
|
+
}
|
|
14136
|
+
} finally {
|
|
14137
|
+
await backend.dispose();
|
|
14138
|
+
}
|
|
14139
|
+
});
|
|
14140
|
+
it("append rejects a batch that does not contiguously continue the log, naming the expected seq", async () => {
|
|
14151
14141
|
const { persistence, dispose } = await make();
|
|
14152
14142
|
try {
|
|
14153
|
-
const m = meta("
|
|
14154
|
-
await persistence.create(m);
|
|
14155
|
-
await
|
|
14156
|
-
|
|
14157
|
-
|
|
14143
|
+
const m = meta("contiguity");
|
|
14144
|
+
const handle = await persistence.create(m);
|
|
14145
|
+
await handle.append(oneTurnLog());
|
|
14146
|
+
await globalExpect(handle.append(oneTurnLog())).rejects.toThrow(/expected 6/);
|
|
14147
|
+
const gapped = [{
|
|
14148
|
+
type: "turn/start",
|
|
14149
|
+
seq: SessionSeq(6),
|
|
14150
|
+
time: 9,
|
|
14151
|
+
data: { turn: 2 }
|
|
14152
|
+
}, {
|
|
14153
|
+
type: "turn/end",
|
|
14154
|
+
seq: SessionSeq(8),
|
|
14155
|
+
time: 10,
|
|
14156
|
+
data: {
|
|
14157
|
+
turn: 2,
|
|
14158
|
+
reason: { kind: "completed" }
|
|
14159
|
+
}
|
|
14160
|
+
}];
|
|
14161
|
+
await globalExpect(handle.append(gapped)).rejects.toThrow(/expected 7/);
|
|
14162
|
+
globalExpect((await handle.read()).events.map((e) => e.seq)).toEqual([
|
|
14163
|
+
0,
|
|
14164
|
+
1,
|
|
14165
|
+
2,
|
|
14166
|
+
3,
|
|
14167
|
+
4,
|
|
14168
|
+
5
|
|
14169
|
+
]);
|
|
14170
|
+
await handle.close();
|
|
14158
14171
|
} finally {
|
|
14159
14172
|
await dispose();
|
|
14160
14173
|
}
|
|
14161
14174
|
});
|
|
14162
|
-
it("append rejects
|
|
14175
|
+
it("append rejects non-JSON-serializable event data without storing anything", async () => {
|
|
14163
14176
|
const { persistence, dispose } = await make();
|
|
14164
14177
|
try {
|
|
14165
|
-
const m = meta("
|
|
14166
|
-
await persistence.create(m);
|
|
14167
|
-
const
|
|
14178
|
+
const m = meta("non-json");
|
|
14179
|
+
const handle = await persistence.create(m);
|
|
14180
|
+
const bad = (extra) => [{
|
|
14168
14181
|
type: "turn/start",
|
|
14169
14182
|
seq: SessionSeq(0),
|
|
14170
14183
|
time: 1,
|
|
14171
|
-
data: { turn: 1 }
|
|
14172
|
-
}, {
|
|
14173
|
-
type: "step/start",
|
|
14174
|
-
seq: SessionSeq(2),
|
|
14175
|
-
time: 2,
|
|
14176
14184
|
data: {
|
|
14177
14185
|
turn: 1,
|
|
14178
|
-
|
|
14186
|
+
extra
|
|
14179
14187
|
}
|
|
14180
14188
|
}];
|
|
14181
|
-
await globalExpect(
|
|
14189
|
+
await globalExpect(handle.append(bad(1n))).rejects.toThrow(TypeError);
|
|
14190
|
+
await globalExpect(handle.append(bad(1n))).rejects.toThrow(/losslessly JSON-serializable/);
|
|
14191
|
+
await globalExpect(handle.append(bad(void 0))).rejects.toThrow(/losslessly JSON-serializable/);
|
|
14192
|
+
await handle.append(oneTurnLog());
|
|
14193
|
+
globalExpect((await handle.read()).events).toEqual(oneTurnLog());
|
|
14194
|
+
await handle.close();
|
|
14182
14195
|
} finally {
|
|
14183
14196
|
await dispose();
|
|
14184
14197
|
}
|
|
14185
14198
|
});
|
|
14186
|
-
it("
|
|
14199
|
+
it("vocabulary fail-closed: an unknown event type refuses append and stored reads", async () => {
|
|
14187
14200
|
const { persistence, dispose } = await make();
|
|
14188
14201
|
try {
|
|
14189
|
-
const
|
|
14190
|
-
|
|
14191
|
-
|
|
14192
|
-
|
|
14193
|
-
|
|
14194
|
-
|
|
14195
|
-
|
|
14196
|
-
|
|
14197
|
-
|
|
14198
|
-
|
|
14199
|
-
|
|
14200
|
-
|
|
14201
|
-
|
|
14202
|
-
|
|
14203
|
-
|
|
14204
|
-
|
|
14205
|
-
|
|
14206
|
-
|
|
14207
|
-
|
|
14208
|
-
|
|
14209
|
-
|
|
14202
|
+
const m = meta("foreign-vocabulary");
|
|
14203
|
+
const handle = await persistence.create(m);
|
|
14204
|
+
await handle.append(oneTurnLog());
|
|
14205
|
+
await globalExpect(handle.append([{
|
|
14206
|
+
type: "mystery/event",
|
|
14207
|
+
seq: SessionSeq(6),
|
|
14208
|
+
time: 7,
|
|
14209
|
+
data: { payload: true }
|
|
14210
|
+
}])).rejects.toBeInstanceOf(SessionFormatUnsupportedError);
|
|
14211
|
+
await handle.close();
|
|
14212
|
+
const reader = await persistence.open(m.id, "read");
|
|
14213
|
+
try {
|
|
14214
|
+
const { events } = await reader.read();
|
|
14215
|
+
globalExpect(events).toHaveLength(6);
|
|
14216
|
+
} finally {
|
|
14217
|
+
await reader.close();
|
|
14218
|
+
}
|
|
14219
|
+
const writer = await persistence.open(m.id, "write");
|
|
14220
|
+
try {
|
|
14221
|
+
await writer.append([{
|
|
14222
|
+
type: "turn/end",
|
|
14223
|
+
seq: SessionSeq(6),
|
|
14224
|
+
time: 7,
|
|
14210
14225
|
data: {
|
|
14211
|
-
|
|
14212
|
-
|
|
14213
|
-
content: [{
|
|
14214
|
-
type: "text",
|
|
14215
|
-
text: "x"
|
|
14216
|
-
}],
|
|
14217
|
-
source: { kind: "user" },
|
|
14218
|
-
extra: bad
|
|
14226
|
+
turn: 2,
|
|
14227
|
+
reason: { kind: "completed" }
|
|
14219
14228
|
}
|
|
14220
|
-
}];
|
|
14221
|
-
|
|
14229
|
+
}]);
|
|
14230
|
+
} finally {
|
|
14231
|
+
await writer.close();
|
|
14232
|
+
}
|
|
14233
|
+
} finally {
|
|
14234
|
+
await dispose();
|
|
14235
|
+
}
|
|
14236
|
+
});
|
|
14237
|
+
it("a torn physical tail is never served and is durably truncated by the write path", async () => {
|
|
14238
|
+
const backend = await make();
|
|
14239
|
+
try {
|
|
14240
|
+
if (backend.reopen === void 0 || backend.corruptTail === void 0) return;
|
|
14241
|
+
const m = meta("torn", "/work");
|
|
14242
|
+
const creator = await backend.persistence.create(m);
|
|
14243
|
+
await creator.append(oneTurnLog());
|
|
14244
|
+
await creator.close();
|
|
14245
|
+
await backend.corruptTail(m.id, m.cwd);
|
|
14246
|
+
const readerInstance = await backend.reopen();
|
|
14247
|
+
try {
|
|
14248
|
+
const reader = await readerInstance.persistence.open(m.id, "read");
|
|
14249
|
+
globalExpect((await reader.read()).events).toEqual(oneTurnLog());
|
|
14250
|
+
await reader.close();
|
|
14251
|
+
const writer = await readerInstance.persistence.open(m.id, "write");
|
|
14252
|
+
await writer.append(secondTurn());
|
|
14253
|
+
globalExpect((await writer.read()).events.map((e) => e.seq)).toEqual([
|
|
14254
|
+
0,
|
|
14255
|
+
1,
|
|
14256
|
+
2,
|
|
14257
|
+
3,
|
|
14258
|
+
4,
|
|
14259
|
+
5,
|
|
14260
|
+
6,
|
|
14261
|
+
7
|
|
14262
|
+
]);
|
|
14263
|
+
await writer.close();
|
|
14264
|
+
} finally {
|
|
14265
|
+
await readerInstance.dispose();
|
|
14266
|
+
}
|
|
14267
|
+
const verifyInstance = await backend.reopen();
|
|
14268
|
+
try {
|
|
14269
|
+
const verify = await verifyInstance.persistence.open(m.id, "read");
|
|
14270
|
+
globalExpect((await verify.read()).events.map((e) => e.seq)).toEqual([
|
|
14271
|
+
0,
|
|
14272
|
+
1,
|
|
14273
|
+
2,
|
|
14274
|
+
3,
|
|
14275
|
+
4,
|
|
14276
|
+
5,
|
|
14277
|
+
6,
|
|
14278
|
+
7
|
|
14279
|
+
]);
|
|
14280
|
+
await verify.close();
|
|
14281
|
+
} finally {
|
|
14282
|
+
await verifyInstance.dispose();
|
|
14222
14283
|
}
|
|
14284
|
+
} finally {
|
|
14285
|
+
await backend.dispose();
|
|
14286
|
+
}
|
|
14287
|
+
});
|
|
14288
|
+
it("stat and list agree on stable revisions that change after an append", async () => {
|
|
14289
|
+
const { persistence, dispose } = await make();
|
|
14290
|
+
try {
|
|
14291
|
+
const m = meta("revisions", "/work");
|
|
14292
|
+
const writer = await persistence.create(m);
|
|
14293
|
+
await writer.append(oneTurnLog());
|
|
14294
|
+
const statFirst = await persistence.stat(m.id);
|
|
14295
|
+
const statAgain = await persistence.stat(m.id);
|
|
14296
|
+
const listFirst = (await persistence.list()).find((s) => s.header.id === m.id);
|
|
14297
|
+
globalExpect(statFirst).toBeDefined();
|
|
14298
|
+
globalExpect(statAgain?.revision).toBe(statFirst?.revision);
|
|
14299
|
+
globalExpect(listFirst?.revision).toBe(statFirst?.revision);
|
|
14300
|
+
await writer.append(secondTurn());
|
|
14301
|
+
const statChanged = await persistence.stat(m.id);
|
|
14302
|
+
globalExpect(statChanged?.revision).not.toBe(statFirst?.revision);
|
|
14303
|
+
const listChanged = (await persistence.list()).find((s) => s.header.id === m.id);
|
|
14304
|
+
globalExpect(listChanged?.revision).toBe(statChanged?.revision);
|
|
14305
|
+
const reader = await persistence.open(m.id, "read");
|
|
14306
|
+
globalExpect(statChanged?.header).toEqual(reader.header);
|
|
14307
|
+
globalExpect(listChanged?.header).toEqual(reader.header);
|
|
14308
|
+
globalExpect(statChanged?.header).toMatchObject(m);
|
|
14309
|
+
await reader.close();
|
|
14310
|
+
await writer.close();
|
|
14311
|
+
globalExpect(await persistence.stat(SessionId("absent-stat"))).toBeUndefined();
|
|
14223
14312
|
} finally {
|
|
14224
14313
|
await dispose();
|
|
14225
14314
|
}
|
|
@@ -14229,398 +14318,16 @@ function runPersistenceContract(name, make) {
|
|
|
14229
14318
|
//#endregion
|
|
14230
14319
|
//#region src/testing/coordinator-contract.ts
|
|
14231
14320
|
const WORK = "/w";
|
|
14232
|
-
const OTHER = "/other";
|
|
14233
14321
|
function send(session, events) {
|
|
14234
14322
|
appendLog(session, events);
|
|
14235
14323
|
}
|
|
14236
|
-
|
|
14237
|
-
|
|
14238
|
-
|
|
14239
|
-
|
|
14240
|
-
|
|
14241
|
-
time: 1,
|
|
14242
|
-
data: { turn: 1 }
|
|
14243
|
-
},
|
|
14244
|
-
{
|
|
14245
|
-
type: "user/message",
|
|
14246
|
-
seq: SessionSeq(1),
|
|
14247
|
-
time: 2,
|
|
14248
|
-
data: {
|
|
14249
|
-
content: [{
|
|
14250
|
-
type: "text",
|
|
14251
|
-
text: "hi"
|
|
14252
|
-
}],
|
|
14253
|
-
source: { kind: "user" }
|
|
14254
|
-
},
|
|
14255
|
-
surfaceOp: "append"
|
|
14256
|
-
},
|
|
14257
|
-
{
|
|
14258
|
-
type: "step/start",
|
|
14259
|
-
seq: SessionSeq(2),
|
|
14260
|
-
time: 3,
|
|
14261
|
-
data: {
|
|
14262
|
-
turn: 1,
|
|
14263
|
-
step: 1
|
|
14264
|
-
}
|
|
14265
|
-
},
|
|
14266
|
-
{
|
|
14267
|
-
type: "assistant/message",
|
|
14268
|
-
seq: SessionSeq(3),
|
|
14269
|
-
time: 4,
|
|
14270
|
-
data: {
|
|
14271
|
-
turn: 1,
|
|
14272
|
-
step: 1,
|
|
14273
|
-
content: [{
|
|
14274
|
-
type: "tool-call",
|
|
14275
|
-
id: "call-1",
|
|
14276
|
-
name: "read",
|
|
14277
|
-
arguments: "{}"
|
|
14278
|
-
}],
|
|
14279
|
-
provenance: {
|
|
14280
|
-
provider: "mock",
|
|
14281
|
-
model: "mock"
|
|
14282
|
-
}
|
|
14283
|
-
},
|
|
14284
|
-
surfaceOp: "append"
|
|
14285
|
-
},
|
|
14286
|
-
{
|
|
14287
|
-
type: "tool/call",
|
|
14288
|
-
seq: SessionSeq(4),
|
|
14289
|
-
time: 5,
|
|
14290
|
-
data: {
|
|
14291
|
-
turn: 1,
|
|
14292
|
-
step: 1,
|
|
14293
|
-
callId: "call-1",
|
|
14294
|
-
name: "read",
|
|
14295
|
-
arguments: "{}"
|
|
14296
|
-
}
|
|
14297
|
-
},
|
|
14298
|
-
{
|
|
14299
|
-
type: "tool/result",
|
|
14300
|
-
seq: SessionSeq(5),
|
|
14301
|
-
time: 6,
|
|
14302
|
-
data: {
|
|
14303
|
-
turn: 1,
|
|
14304
|
-
step: 1,
|
|
14305
|
-
callId: "call-1",
|
|
14306
|
-
content: [{
|
|
14307
|
-
type: "text",
|
|
14308
|
-
text: "full result"
|
|
14309
|
-
}],
|
|
14310
|
-
isError: false
|
|
14311
|
-
},
|
|
14312
|
-
sourceEventSeqs: [4],
|
|
14313
|
-
surfaceOp: "append"
|
|
14314
|
-
},
|
|
14315
|
-
{
|
|
14316
|
-
type: "tool/result",
|
|
14317
|
-
seq: SessionSeq(6),
|
|
14318
|
-
time: 8,
|
|
14319
|
-
data: {
|
|
14320
|
-
turn: 1,
|
|
14321
|
-
step: 1,
|
|
14322
|
-
callId: "call-1",
|
|
14323
|
-
content: [{
|
|
14324
|
-
type: "text",
|
|
14325
|
-
text: "pruned"
|
|
14326
|
-
}],
|
|
14327
|
-
isError: false
|
|
14328
|
-
},
|
|
14329
|
-
sourceEventSeqs: [5],
|
|
14330
|
-
surfaceOp: {
|
|
14331
|
-
op: "replace",
|
|
14332
|
-
start: 5,
|
|
14333
|
-
end: 5
|
|
14334
|
-
}
|
|
14335
|
-
},
|
|
14336
|
-
{
|
|
14337
|
-
type: "step/end",
|
|
14338
|
-
seq: SessionSeq(7),
|
|
14339
|
-
time: 9,
|
|
14340
|
-
data: {
|
|
14341
|
-
turn: 1,
|
|
14342
|
-
step: 1
|
|
14343
|
-
}
|
|
14344
|
-
},
|
|
14345
|
-
{
|
|
14346
|
-
type: "turn/end",
|
|
14347
|
-
seq: SessionSeq(8),
|
|
14348
|
-
time: 10,
|
|
14349
|
-
data: {
|
|
14350
|
-
turn: 1,
|
|
14351
|
-
reason: { kind: "completed" }
|
|
14352
|
-
}
|
|
14353
|
-
}
|
|
14354
|
-
];
|
|
14355
|
-
}
|
|
14356
|
-
function preReactLoopLog() {
|
|
14357
|
-
const prompt = createUserMessage({
|
|
14358
|
-
content: [{
|
|
14359
|
-
type: "text",
|
|
14360
|
-
text: "old prompt"
|
|
14361
|
-
}],
|
|
14362
|
-
source: { kind: "user" }
|
|
14363
|
-
});
|
|
14364
|
-
const steering = createUserMessage({
|
|
14365
|
-
content: [{
|
|
14366
|
-
type: "text",
|
|
14367
|
-
text: "old steering"
|
|
14368
|
-
}],
|
|
14369
|
-
source: { kind: "user" }
|
|
14370
|
-
});
|
|
14371
|
-
return [
|
|
14372
|
-
{
|
|
14373
|
-
type: "turn/start",
|
|
14374
|
-
seq: SessionSeq(0),
|
|
14375
|
-
time: 1,
|
|
14376
|
-
data: {
|
|
14377
|
-
turn: 1,
|
|
14378
|
-
trigger: {
|
|
14379
|
-
kind: "message",
|
|
14380
|
-
source: { kind: "user" }
|
|
14381
|
-
}
|
|
14382
|
-
}
|
|
14383
|
-
},
|
|
14384
|
-
{
|
|
14385
|
-
type: "user/message",
|
|
14386
|
-
seq: SessionSeq(1),
|
|
14387
|
-
time: 2,
|
|
14388
|
-
data: prompt,
|
|
14389
|
-
surfaceOp: "append"
|
|
14390
|
-
},
|
|
14391
|
-
{
|
|
14392
|
-
type: "step/start",
|
|
14393
|
-
seq: SessionSeq(2),
|
|
14394
|
-
time: 3,
|
|
14395
|
-
data: {
|
|
14396
|
-
turn: 1,
|
|
14397
|
-
step: 1
|
|
14398
|
-
}
|
|
14399
|
-
},
|
|
14400
|
-
{
|
|
14401
|
-
type: "steering/message",
|
|
14402
|
-
seq: SessionSeq(3),
|
|
14403
|
-
time: 4,
|
|
14404
|
-
data: {
|
|
14405
|
-
turn: 1,
|
|
14406
|
-
message: steering
|
|
14407
|
-
},
|
|
14408
|
-
surfaceOp: "append"
|
|
14409
|
-
},
|
|
14410
|
-
{
|
|
14411
|
-
type: "step/end",
|
|
14412
|
-
seq: SessionSeq(4),
|
|
14413
|
-
time: 5,
|
|
14414
|
-
data: {
|
|
14415
|
-
turn: 1,
|
|
14416
|
-
step: 1
|
|
14417
|
-
}
|
|
14418
|
-
},
|
|
14419
|
-
{
|
|
14420
|
-
type: "turn/end",
|
|
14421
|
-
seq: SessionSeq(5),
|
|
14422
|
-
time: 6,
|
|
14423
|
-
data: {
|
|
14424
|
-
turn: 1,
|
|
14425
|
-
reason: { kind: "completed" }
|
|
14426
|
-
}
|
|
14427
|
-
},
|
|
14428
|
-
{
|
|
14429
|
-
type: "turn/start",
|
|
14430
|
-
seq: SessionSeq(6),
|
|
14431
|
-
time: 7,
|
|
14432
|
-
data: {
|
|
14433
|
-
turn: 2,
|
|
14434
|
-
trigger: { kind: "retry" }
|
|
14435
|
-
}
|
|
14436
|
-
},
|
|
14437
|
-
{
|
|
14438
|
-
type: "step/start",
|
|
14439
|
-
seq: SessionSeq(7),
|
|
14440
|
-
time: 8,
|
|
14441
|
-
data: {
|
|
14442
|
-
turn: 2,
|
|
14443
|
-
step: 1
|
|
14444
|
-
}
|
|
14445
|
-
},
|
|
14446
|
-
{
|
|
14447
|
-
type: "step/end",
|
|
14448
|
-
seq: SessionSeq(8),
|
|
14449
|
-
time: 9,
|
|
14450
|
-
data: {
|
|
14451
|
-
turn: 2,
|
|
14452
|
-
step: 1
|
|
14453
|
-
}
|
|
14454
|
-
},
|
|
14455
|
-
{
|
|
14456
|
-
type: "turn/end",
|
|
14457
|
-
seq: SessionSeq(9),
|
|
14458
|
-
time: 10,
|
|
14459
|
-
data: {
|
|
14460
|
-
turn: 2,
|
|
14461
|
-
reason: {
|
|
14462
|
-
kind: "error",
|
|
14463
|
-
step: 1,
|
|
14464
|
-
failure: {
|
|
14465
|
-
message: "old provider failure",
|
|
14466
|
-
code: "SERVER"
|
|
14467
|
-
}
|
|
14468
|
-
}
|
|
14469
|
-
}
|
|
14470
|
-
},
|
|
14471
|
-
{
|
|
14472
|
-
type: "turn/start",
|
|
14473
|
-
seq: SessionSeq(10),
|
|
14474
|
-
time: 11,
|
|
14475
|
-
data: {
|
|
14476
|
-
turn: 3,
|
|
14477
|
-
trigger: {
|
|
14478
|
-
kind: "message",
|
|
14479
|
-
source: { kind: "user" }
|
|
14480
|
-
}
|
|
14481
|
-
}
|
|
14482
|
-
},
|
|
14483
|
-
{
|
|
14484
|
-
type: "turn/end",
|
|
14485
|
-
seq: SessionSeq(11),
|
|
14486
|
-
time: 12,
|
|
14487
|
-
data: {
|
|
14488
|
-
turn: 3,
|
|
14489
|
-
reason: { kind: "aborted" }
|
|
14490
|
-
}
|
|
14491
|
-
},
|
|
14492
|
-
{
|
|
14493
|
-
type: "turn/start",
|
|
14494
|
-
seq: SessionSeq(12),
|
|
14495
|
-
time: 13,
|
|
14496
|
-
data: {
|
|
14497
|
-
turn: 4,
|
|
14498
|
-
trigger: {
|
|
14499
|
-
kind: "message",
|
|
14500
|
-
source: { kind: "user" }
|
|
14501
|
-
}
|
|
14502
|
-
}
|
|
14503
|
-
},
|
|
14504
|
-
{
|
|
14505
|
-
type: "turn/end",
|
|
14506
|
-
seq: SessionSeq(13),
|
|
14507
|
-
time: 14,
|
|
14508
|
-
data: {
|
|
14509
|
-
turn: 4,
|
|
14510
|
-
reason: { kind: "disposed" }
|
|
14511
|
-
}
|
|
14512
|
-
},
|
|
14513
|
-
{
|
|
14514
|
-
type: "turn/start",
|
|
14515
|
-
seq: SessionSeq(14),
|
|
14516
|
-
time: 15,
|
|
14517
|
-
data: {
|
|
14518
|
-
turn: 5,
|
|
14519
|
-
trigger: {
|
|
14520
|
-
kind: "message",
|
|
14521
|
-
source: { kind: "user" }
|
|
14522
|
-
}
|
|
14523
|
-
}
|
|
14524
|
-
},
|
|
14525
|
-
{
|
|
14526
|
-
type: "step/start",
|
|
14527
|
-
seq: SessionSeq(15),
|
|
14528
|
-
time: 16,
|
|
14529
|
-
data: {
|
|
14530
|
-
turn: 5,
|
|
14531
|
-
step: 1
|
|
14532
|
-
}
|
|
14533
|
-
},
|
|
14534
|
-
{
|
|
14535
|
-
type: "step/end",
|
|
14536
|
-
seq: SessionSeq(16),
|
|
14537
|
-
time: 17,
|
|
14538
|
-
data: {
|
|
14539
|
-
turn: 5,
|
|
14540
|
-
step: 1
|
|
14541
|
-
}
|
|
14542
|
-
},
|
|
14543
|
-
{
|
|
14544
|
-
type: "turn/end",
|
|
14545
|
-
seq: SessionSeq(17),
|
|
14546
|
-
time: 18,
|
|
14547
|
-
data: {
|
|
14548
|
-
turn: 5,
|
|
14549
|
-
reason: {
|
|
14550
|
-
kind: "error",
|
|
14551
|
-
step: 1,
|
|
14552
|
-
message: "old thrown value"
|
|
14553
|
-
}
|
|
14554
|
-
}
|
|
14555
|
-
},
|
|
14556
|
-
{
|
|
14557
|
-
type: "turn/start",
|
|
14558
|
-
seq: SessionSeq(18),
|
|
14559
|
-
time: 19,
|
|
14560
|
-
data: {
|
|
14561
|
-
turn: 6,
|
|
14562
|
-
trigger: {
|
|
14563
|
-
kind: "message",
|
|
14564
|
-
source: { kind: "user" }
|
|
14565
|
-
}
|
|
14566
|
-
}
|
|
14567
|
-
},
|
|
14568
|
-
{
|
|
14569
|
-
type: "turn/end",
|
|
14570
|
-
seq: SessionSeq(19),
|
|
14571
|
-
time: 20,
|
|
14572
|
-
data: {
|
|
14573
|
-
turn: 6,
|
|
14574
|
-
reason: {
|
|
14575
|
-
kind: "error",
|
|
14576
|
-
step: 0,
|
|
14577
|
-
failure: {
|
|
14578
|
-
message: "old detailed provider failure",
|
|
14579
|
-
code: "RATE_LIMIT",
|
|
14580
|
-
status: 429,
|
|
14581
|
-
providerRetryAfterMs: 1e3,
|
|
14582
|
-
requestId: "request-1"
|
|
14583
|
-
}
|
|
14584
|
-
}
|
|
14585
|
-
}
|
|
14586
|
-
},
|
|
14587
|
-
{
|
|
14588
|
-
type: "turn/start",
|
|
14589
|
-
seq: SessionSeq(20),
|
|
14590
|
-
time: 21,
|
|
14591
|
-
data: {
|
|
14592
|
-
turn: 7,
|
|
14593
|
-
trigger: {
|
|
14594
|
-
kind: "message",
|
|
14595
|
-
source: { kind: "user" }
|
|
14596
|
-
}
|
|
14597
|
-
}
|
|
14598
|
-
},
|
|
14599
|
-
{
|
|
14600
|
-
type: "turn/end",
|
|
14601
|
-
seq: SessionSeq(21),
|
|
14602
|
-
time: 22,
|
|
14603
|
-
data: {
|
|
14604
|
-
turn: 7,
|
|
14605
|
-
reason: {
|
|
14606
|
-
kind: "error",
|
|
14607
|
-
step: 0,
|
|
14608
|
-
message: "old coded error",
|
|
14609
|
-
code: "CODED"
|
|
14610
|
-
}
|
|
14611
|
-
}
|
|
14612
|
-
}
|
|
14613
|
-
];
|
|
14614
|
-
}
|
|
14615
|
-
async function liveSessionInFiber(ctx, id, cwd) {
|
|
14616
|
-
let session;
|
|
14617
|
-
await ctx.plugin(Object.assign((inner) => {
|
|
14618
|
-
session = inner.sessions.create(SessionId(id), cwd !== void 0 ? { meta: { cwd } } : void 0);
|
|
14619
|
-
}, { inject: ["sessions"] }));
|
|
14620
|
-
return session;
|
|
14621
|
-
}
|
|
14324
|
+
/**
|
|
14325
|
+
* RDB 特有行为契约:live 会话驱动持久化、seed 边界、fork/resume、HMR
|
|
14326
|
+
* dispose drain、冲突拒绝、torn-tail。coordinator 内部状态(ownerless /
|
|
14327
|
+
* per-id chain / legacy 迁移)已由新版 handle 模型承担,不再单独测试。
|
|
14328
|
+
*/
|
|
14622
14329
|
function runCoordinatorContract(name, makeFixture) {
|
|
14623
|
-
describe(`
|
|
14330
|
+
describe(`SessionPersistenceRdb orchestration: ${name}`, () => {
|
|
14624
14331
|
async function freshCtx(fix) {
|
|
14625
14332
|
const ctx = new Context();
|
|
14626
14333
|
await ctx.plugin(SessionStore);
|
|
@@ -14629,6 +14336,18 @@ function runCoordinatorContract(name, makeFixture) {
|
|
|
14629
14336
|
fiber: await fix.mount(ctx)
|
|
14630
14337
|
};
|
|
14631
14338
|
}
|
|
14339
|
+
async function readAll(ctx, id) {
|
|
14340
|
+
const handle = await ctx.sessionPersistence.open(id, "read");
|
|
14341
|
+
try {
|
|
14342
|
+
const { events } = await handle.read();
|
|
14343
|
+
return {
|
|
14344
|
+
meta: handle.header,
|
|
14345
|
+
events
|
|
14346
|
+
};
|
|
14347
|
+
} finally {
|
|
14348
|
+
await handle.close();
|
|
14349
|
+
}
|
|
14350
|
+
}
|
|
14632
14351
|
it("persists a live session driven through the store, surviving reload", async () => {
|
|
14633
14352
|
const fix = await makeFixture();
|
|
14634
14353
|
const { ctx, fiber } = await freshCtx(fix);
|
|
@@ -14636,7 +14355,7 @@ function runCoordinatorContract(name, makeFixture) {
|
|
|
14636
14355
|
const session = ctx.sessions.create(SessionId("live"), { meta: { cwd: WORK } });
|
|
14637
14356
|
send(session, oneTurnLog());
|
|
14638
14357
|
await ctx.sessions.flush(session);
|
|
14639
|
-
const loaded = await ctx
|
|
14358
|
+
const loaded = await readAll(ctx, SessionId("live"));
|
|
14640
14359
|
globalExpect(loaded.events).toHaveLength(6);
|
|
14641
14360
|
globalExpect(loaded.meta.cwd).toBe(WORK);
|
|
14642
14361
|
} finally {
|
|
@@ -14644,106 +14363,31 @@ function runCoordinatorContract(name, makeFixture) {
|
|
|
14644
14363
|
await fix.cleanup();
|
|
14645
14364
|
}
|
|
14646
14365
|
});
|
|
14647
|
-
it("
|
|
14366
|
+
it("round-trips the seed boundary (inheritedEventCount) through persistence", async () => {
|
|
14648
14367
|
const fix = await makeFixture();
|
|
14649
14368
|
const { ctx, fiber } = await freshCtx(fix);
|
|
14650
|
-
let session;
|
|
14651
|
-
const sessionFiber = await ctx.plugin(Object.assign((inner) => {
|
|
14652
|
-
session = inner.sessions.create(SessionId("live-load"), { meta: { cwd: WORK } });
|
|
14653
|
-
}, { inject: ["sessions"] }));
|
|
14654
14369
|
try {
|
|
14655
|
-
session
|
|
14656
|
-
await ctx.
|
|
14657
|
-
|
|
14658
|
-
|
|
14370
|
+
let session;
|
|
14371
|
+
const sessionFiber = await ctx.plugin(Object.assign((inner) => {
|
|
14372
|
+
session = inner.sessions.create(SessionId("forked-child"), {
|
|
14373
|
+
seed: oneTurnLog().slice(0, 3),
|
|
14374
|
+
meta: {
|
|
14375
|
+
cwd: WORK,
|
|
14376
|
+
isSeeded: true
|
|
14377
|
+
},
|
|
14378
|
+
inheritedEventCount: SessionLogOffset(3)
|
|
14379
|
+
});
|
|
14380
|
+
}, { inject: ["sessions"] }));
|
|
14381
|
+
send(session, oneTurnLog().slice(3));
|
|
14659
14382
|
await ctx.sessions.flush(session);
|
|
14660
14383
|
await sessionFiber.dispose();
|
|
14661
|
-
await
|
|
14662
|
-
|
|
14663
|
-
globalExpect(
|
|
14664
|
-
globalExpect(
|
|
14665
|
-
|
|
14666
|
-
|
|
14667
|
-
|
|
14668
|
-
});
|
|
14669
|
-
} finally {
|
|
14670
|
-
await sessionFiber.dispose();
|
|
14671
|
-
await fiber.dispose();
|
|
14672
|
-
await fix.cleanup();
|
|
14673
|
-
}
|
|
14674
|
-
});
|
|
14675
|
-
it("rechecks live ownership after a cold load enters the per-id chain", async () => {
|
|
14676
|
-
const fix = await makeFixture();
|
|
14677
|
-
const { ctx, fiber } = await freshCtx(fix);
|
|
14678
|
-
try {
|
|
14679
|
-
const id = SessionId("queued-load-live-race");
|
|
14680
|
-
const header = meta(id, WORK);
|
|
14681
|
-
const start = {
|
|
14682
|
-
type: "turn/start",
|
|
14683
|
-
seq: SessionSeq(0),
|
|
14684
|
-
time: 1,
|
|
14685
|
-
data: { turn: 1 }
|
|
14686
|
-
};
|
|
14687
|
-
await ctx.sessionPersistence.create(header);
|
|
14688
|
-
await ctx.sessionPersistence.append(id, [start]);
|
|
14689
|
-
const loading = ctx.sessionPersistence.load(id);
|
|
14690
|
-
const live = ctx.sessions.create(id, {
|
|
14691
|
-
seed: [start],
|
|
14692
|
-
meta: header
|
|
14693
|
-
});
|
|
14694
|
-
await globalExpect(loading).rejects.toThrow(/live turn is open/);
|
|
14695
|
-
live.append("turn/end", {
|
|
14696
|
-
turn: 1,
|
|
14697
|
-
reason: { kind: "completed" }
|
|
14698
|
-
});
|
|
14699
|
-
await ctx.sessions.flush(live);
|
|
14700
|
-
const loaded = await ctx.sessionPersistence.load(id);
|
|
14701
|
-
globalExpect(loaded.events.map((event) => event.type)).toEqual([
|
|
14702
|
-
"turn/start",
|
|
14703
|
-
"session/end-seed",
|
|
14704
|
-
"turn/end"
|
|
14705
|
-
]);
|
|
14706
|
-
globalExpect(loaded.events.at(-1)).toMatchObject({
|
|
14707
|
-
type: "turn/end",
|
|
14708
|
-
data: { reason: { kind: "completed" } }
|
|
14709
|
-
});
|
|
14710
|
-
} finally {
|
|
14711
|
-
await fiber.dispose();
|
|
14712
|
-
await fix.cleanup();
|
|
14713
|
-
}
|
|
14714
|
-
});
|
|
14715
|
-
it("does not load an unmaterialized empty live session", async () => {
|
|
14716
|
-
const fix = await makeFixture();
|
|
14717
|
-
const { ctx, fiber } = await freshCtx(fix);
|
|
14718
|
-
try {
|
|
14719
|
-
const session = ctx.sessions.create(SessionId("empty-live"), { meta: { cwd: WORK } });
|
|
14720
|
-
await globalExpect(ctx.sessionPersistence.load(session.id)).rejects.toThrow(/not found/);
|
|
14721
|
-
} finally {
|
|
14722
|
-
await fiber.dispose();
|
|
14723
|
-
await fix.cleanup();
|
|
14724
|
-
}
|
|
14725
|
-
});
|
|
14726
|
-
it("round-trips the seed boundary (inheritedEventCount) through persistence", async () => {
|
|
14727
|
-
const fix = await makeFixture();
|
|
14728
|
-
const { ctx, fiber } = await freshCtx(fix);
|
|
14729
|
-
try {
|
|
14730
|
-
let session;
|
|
14731
|
-
const sessionFiber = await ctx.plugin(Object.assign((inner) => {
|
|
14732
|
-
session = inner.sessions.create(SessionId("forked-child"), {
|
|
14733
|
-
seed: oneTurnLog().slice(0, 3),
|
|
14734
|
-
meta: {
|
|
14735
|
-
cwd: WORK,
|
|
14736
|
-
isSeeded: true
|
|
14737
|
-
},
|
|
14738
|
-
inheritedEventCount: SessionLogOffset(3)
|
|
14739
|
-
});
|
|
14740
|
-
}, { inject: ["sessions"] }));
|
|
14741
|
-
send(session, oneTurnLog().slice(3));
|
|
14742
|
-
await ctx.sessions.flush(session);
|
|
14743
|
-
await sessionFiber.dispose();
|
|
14744
|
-
const loaded = await ctx.sessionPersistence.load(SessionId("forked-child"));
|
|
14745
|
-
globalExpect(loaded.meta.isSeeded).toBe(true);
|
|
14746
|
-
globalExpect(loaded.inheritedEventCount).toBe(3);
|
|
14384
|
+
const handle = await ctx.sessionPersistence.open(SessionId("forked-child"), "read");
|
|
14385
|
+
try {
|
|
14386
|
+
globalExpect(handle.header.isSeeded).toBe(true);
|
|
14387
|
+
globalExpect(handle.inheritedEventCount).toBe(3);
|
|
14388
|
+
} finally {
|
|
14389
|
+
await handle.close();
|
|
14390
|
+
}
|
|
14747
14391
|
} finally {
|
|
14748
14392
|
await fiber.dispose();
|
|
14749
14393
|
await fix.cleanup();
|
|
@@ -14753,986 +14397,102 @@ function runCoordinatorContract(name, makeFixture) {
|
|
|
14753
14397
|
const fix = await makeFixture();
|
|
14754
14398
|
const { ctx, fiber } = await freshCtx(fix);
|
|
14755
14399
|
try {
|
|
14756
|
-
let session;
|
|
14757
|
-
const sessionFiber = await ctx.plugin(Object.assign((inner) => {
|
|
14758
|
-
session = inner.sessions.create(SessionId("delegated-child"), { meta: {
|
|
14759
|
-
cwd: WORK,
|
|
14760
|
-
parentSession: SessionId("root"),
|
|
14761
|
-
delegationDepth: 2
|
|
14762
|
-
} });
|
|
14763
|
-
}, { inject: ["sessions"] }));
|
|
14764
|
-
send(session, oneTurnLog());
|
|
14765
|
-
await ctx.parallel("session/flush", session);
|
|
14766
|
-
await sessionFiber.dispose();
|
|
14767
|
-
const loaded = await ctx.sessionPersistence.load(SessionId("delegated-child"));
|
|
14768
|
-
globalExpect(loaded.meta.delegationDepth).toBe(2);
|
|
14769
|
-
} finally {
|
|
14770
|
-
await fiber.dispose();
|
|
14771
|
-
await fix.cleanup();
|
|
14772
|
-
}
|
|
14773
|
-
});
|
|
14774
|
-
it("source-frozen events cannot be mutated after buffering and persist unchanged", async () => {
|
|
14775
|
-
const fix = await makeFixture();
|
|
14776
|
-
const { ctx, fiber } = await freshCtx(fix);
|
|
14777
|
-
try {
|
|
14778
|
-
const session = ctx.sessions.create(SessionId("mutate"), { meta: { cwd: WORK } });
|
|
14779
|
-
session.append("turn/start", { turn: 1 });
|
|
14780
|
-
const ev = session.append("user/message", createUserMessage({
|
|
14781
|
-
content: [{
|
|
14782
|
-
type: "text",
|
|
14783
|
-
text: "original"
|
|
14784
|
-
}],
|
|
14785
|
-
source: { kind: "user" }
|
|
14786
|
-
}), { surfaceOp: "append" });
|
|
14787
|
-
globalExpect(() => {
|
|
14788
|
-
ev.data.content[0].text = "HACKED";
|
|
14789
|
-
}).toThrow(TypeError);
|
|
14790
|
-
session.append("turn/end", {
|
|
14791
|
-
turn: 1,
|
|
14792
|
-
reason: { kind: "completed" }
|
|
14793
|
-
});
|
|
14794
|
-
await ctx.sessions.flush(session);
|
|
14795
|
-
const message = (await ctx.sessionPersistence.load(SessionId("mutate"))).events.find((event) => event.type === "user/message");
|
|
14796
|
-
globalExpect(message?.type === "user/message" && message.data.content[0].text).toBe("original");
|
|
14797
|
-
} finally {
|
|
14798
|
-
await fiber.dispose();
|
|
14799
|
-
await fix.cleanup();
|
|
14800
|
-
}
|
|
14801
|
-
});
|
|
14802
|
-
it("load and inspect return immutable identified-message snapshots", async () => {
|
|
14803
|
-
const fix = await makeFixture();
|
|
14804
|
-
const { ctx, fiber } = await freshCtx(fix);
|
|
14805
|
-
try {
|
|
14806
|
-
const id = SessionId("immutable-read");
|
|
14807
|
-
const session = ctx.sessions.create(id, { meta: { cwd: WORK } });
|
|
14808
|
-
send(session, oneTurnLog());
|
|
14809
|
-
await ctx.sessions.flush(session);
|
|
14810
|
-
for (const snapshot of [await ctx.sessionPersistence.load(id), await ctx.sessionPersistence.inspect(id)]) {
|
|
14811
|
-
const event = snapshot.events.find((candidate) => candidate.type === "user/message");
|
|
14812
|
-
if (event?.type !== "user/message") throw new Error("fixture lacks user/message");
|
|
14813
|
-
globalExpect(Object.isFrozen(event.data)).toBe(true);
|
|
14814
|
-
globalExpect(Object.isFrozen(event.data.content)).toBe(true);
|
|
14815
|
-
globalExpect(() => {
|
|
14816
|
-
event.data.id = "rewritten";
|
|
14817
|
-
}).toThrow(TypeError);
|
|
14818
|
-
globalExpect(() => {
|
|
14819
|
-
event.data.content[0].text = "rewritten";
|
|
14820
|
-
}).toThrow(TypeError);
|
|
14821
|
-
}
|
|
14822
|
-
} finally {
|
|
14823
|
-
await fiber.dispose();
|
|
14824
|
-
await fix.cleanup();
|
|
14825
|
-
}
|
|
14826
|
-
});
|
|
14827
|
-
it("loads pre-identity message logs into resumable current sessions", async () => {
|
|
14828
|
-
const fix = await makeFixture();
|
|
14829
|
-
const { ctx, fiber } = await freshCtx(fix);
|
|
14830
|
-
try {
|
|
14831
|
-
const id = SessionId("legacy-message-load");
|
|
14832
|
-
await ctx.sessionPersistence.create(meta(id, WORK));
|
|
14833
|
-
await ctx.sessionPersistence.append(id, legacyMessageLog());
|
|
14834
|
-
for (const snapshot of [await ctx.sessionPersistence.inspect(id), await ctx.sessionPersistence.load(id)]) {
|
|
14835
|
-
const messages = [];
|
|
14836
|
-
for (const event of snapshot.events) if (event.type === "user/message") messages.push(event.data);
|
|
14837
|
-
else if (event.type === "assistant/message" || event.type === "tool/result") messages.push(event.data.message);
|
|
14838
|
-
globalExpect(messages.map((message) => message.id)).toEqual([
|
|
14839
|
-
`legacy-message:${id}:1`,
|
|
14840
|
-
`legacy-message:${id}:3`,
|
|
14841
|
-
`legacy-message:${id}:5`,
|
|
14842
|
-
`legacy-message:${id}:5`
|
|
14843
|
-
]);
|
|
14844
|
-
globalExpect(messages.every((message) => Object.isFrozen(message))).toBe(true);
|
|
14845
|
-
const resumed = Session.create(id, snapshot.events, snapshot.meta, snapshot.inheritedEventCount);
|
|
14846
|
-
globalExpect(resumed.deriveMessages().map((message) => message.id)).toEqual([
|
|
14847
|
-
`legacy-message:${id}:1`,
|
|
14848
|
-
`legacy-message:${id}:3`,
|
|
14849
|
-
`legacy-message:${id}:5`
|
|
14850
|
-
]);
|
|
14851
|
-
}
|
|
14852
|
-
const replacementSuffix = await ctx.sessionPersistence.readFrom(id, SessionLogOffset(6));
|
|
14853
|
-
globalExpect(replacementSuffix.events[0]).toMatchObject({
|
|
14854
|
-
type: "tool/result",
|
|
14855
|
-
seq: SessionSeq(6),
|
|
14856
|
-
data: { message: { id: `legacy-message:${id}:5` } }
|
|
14857
|
-
});
|
|
14858
|
-
} finally {
|
|
14859
|
-
await fiber.dispose();
|
|
14860
|
-
await fix.cleanup();
|
|
14861
|
-
}
|
|
14862
|
-
});
|
|
14863
|
-
it("loads pre-react-loop session logs into resumable current sessions", async () => {
|
|
14864
|
-
const fix = await makeFixture();
|
|
14865
|
-
const { ctx, fiber } = await freshCtx(fix);
|
|
14866
|
-
try {
|
|
14867
|
-
const id = SessionId("pre-react-loop-load");
|
|
14868
|
-
const log = preReactLoopLog();
|
|
14869
|
-
const legacySteering = log[3];
|
|
14870
|
-
await ctx.sessionPersistence.create(meta(id, WORK));
|
|
14871
|
-
await ctx.sessionPersistence.append(id, log);
|
|
14872
|
-
const snapshots = [
|
|
14873
|
-
await ctx.sessionPersistence.inspect(id),
|
|
14874
|
-
await ctx.sessionPersistence.readFrom(id, SessionLogOffset(0)),
|
|
14875
|
-
await ctx.sessionPersistence.load(id)
|
|
14876
|
-
];
|
|
14877
|
-
for (const snapshot of snapshots) {
|
|
14878
|
-
globalExpect(snapshot.events.some((event) => event.type === "steering/message")).toBe(false);
|
|
14879
|
-
globalExpect(snapshot.events.filter((event) => event.type === "turn/start").map((event) => event.data)).toEqual([
|
|
14880
|
-
{ turn: 1 },
|
|
14881
|
-
{ turn: 2 },
|
|
14882
|
-
{ turn: 3 },
|
|
14883
|
-
{ turn: 4 },
|
|
14884
|
-
{ turn: 5 },
|
|
14885
|
-
{ turn: 6 },
|
|
14886
|
-
{ turn: 7 }
|
|
14887
|
-
]);
|
|
14888
|
-
globalExpect(snapshot.events.filter((event) => event.type === "turn/end").map((event) => event.data)).toEqual([
|
|
14889
|
-
{
|
|
14890
|
-
turn: 1,
|
|
14891
|
-
reason: { kind: "completed" }
|
|
14892
|
-
},
|
|
14893
|
-
{
|
|
14894
|
-
turn: 2,
|
|
14895
|
-
reason: {
|
|
14896
|
-
kind: "error",
|
|
14897
|
-
error: {
|
|
14898
|
-
message: "old provider failure",
|
|
14899
|
-
code: "SERVER"
|
|
14900
|
-
}
|
|
14901
|
-
}
|
|
14902
|
-
},
|
|
14903
|
-
{
|
|
14904
|
-
turn: 3,
|
|
14905
|
-
reason: {
|
|
14906
|
-
kind: "aborted",
|
|
14907
|
-
reason: { kind: "legacy" }
|
|
14908
|
-
}
|
|
14909
|
-
},
|
|
14910
|
-
{
|
|
14911
|
-
turn: 4,
|
|
14912
|
-
reason: {
|
|
14913
|
-
kind: "aborted",
|
|
14914
|
-
reason: { kind: "disposed" }
|
|
14915
|
-
}
|
|
14916
|
-
},
|
|
14917
|
-
{
|
|
14918
|
-
turn: 5,
|
|
14919
|
-
reason: {
|
|
14920
|
-
kind: "error",
|
|
14921
|
-
error: {
|
|
14922
|
-
message: "old thrown value",
|
|
14923
|
-
code: "UNKNOWN"
|
|
14924
|
-
}
|
|
14925
|
-
}
|
|
14926
|
-
},
|
|
14927
|
-
{
|
|
14928
|
-
turn: 6,
|
|
14929
|
-
reason: {
|
|
14930
|
-
kind: "error",
|
|
14931
|
-
error: {
|
|
14932
|
-
message: "old detailed provider failure",
|
|
14933
|
-
code: "RATE_LIMIT",
|
|
14934
|
-
status: 429,
|
|
14935
|
-
providerRetryAfterMs: 1e3,
|
|
14936
|
-
requestId: "request-1"
|
|
14937
|
-
}
|
|
14938
|
-
}
|
|
14939
|
-
},
|
|
14940
|
-
{
|
|
14941
|
-
turn: 7,
|
|
14942
|
-
reason: {
|
|
14943
|
-
kind: "error",
|
|
14944
|
-
error: {
|
|
14945
|
-
message: "old coded error",
|
|
14946
|
-
code: "CODED"
|
|
14947
|
-
}
|
|
14948
|
-
}
|
|
14949
|
-
}
|
|
14950
|
-
]);
|
|
14951
|
-
const resumed = Session.create(id, snapshot.events, snapshot.meta, snapshot.inheritedEventCount);
|
|
14952
|
-
globalExpect(resumed.deriveMessages().map((message) => message.content)).toEqual([[{
|
|
14953
|
-
type: "text",
|
|
14954
|
-
text: "old prompt"
|
|
14955
|
-
}], [{
|
|
14956
|
-
type: "text",
|
|
14957
|
-
text: "old steering"
|
|
14958
|
-
}]]);
|
|
14959
|
-
}
|
|
14960
|
-
const suffix = await ctx.sessionPersistence.readFrom(id, SessionLogOffset(3));
|
|
14961
|
-
globalExpect(suffix.events[0]).toMatchObject({
|
|
14962
|
-
type: "user/message",
|
|
14963
|
-
seq: SessionSeq(3),
|
|
14964
|
-
data: { id: legacySteering.data.message.id }
|
|
14965
|
-
});
|
|
14966
|
-
globalExpect(suffix.events.filter((event) => event.type === "turn/end").every((event) => !Object.hasOwn(event.data, "step"))).toBe(true);
|
|
14967
|
-
const flatId = SessionId("pre-react-loop-flat-steering");
|
|
14968
|
-
await ctx.sessionPersistence.create(meta(flatId, WORK));
|
|
14969
|
-
await ctx.sessionPersistence.append(flatId, [{
|
|
14970
|
-
type: "steering/message",
|
|
14971
|
-
seq: SessionSeq(0),
|
|
14972
|
-
time: 1,
|
|
14973
|
-
data: {
|
|
14974
|
-
turn: 1,
|
|
14975
|
-
content: [{
|
|
14976
|
-
type: "text",
|
|
14977
|
-
text: "flat steering"
|
|
14978
|
-
}],
|
|
14979
|
-
source: { kind: "user" }
|
|
14980
|
-
},
|
|
14981
|
-
surfaceOp: "append"
|
|
14982
|
-
}]);
|
|
14983
|
-
globalExpect((await ctx.sessionPersistence.inspect(flatId)).events[0]).toMatchObject({
|
|
14984
|
-
type: "user/message",
|
|
14985
|
-
data: {
|
|
14986
|
-
id: `legacy-message:${flatId}:0`,
|
|
14987
|
-
role: "user",
|
|
14988
|
-
content: [{
|
|
14989
|
-
type: "text",
|
|
14990
|
-
text: "flat steering"
|
|
14991
|
-
}]
|
|
14992
|
-
}
|
|
14993
|
-
});
|
|
14994
|
-
const extendedId = SessionId("current-extended-turn-end");
|
|
14995
|
-
await ctx.sessionPersistence.create(meta(extendedId, WORK));
|
|
14996
|
-
await ctx.sessionPersistence.append(extendedId, [{
|
|
14997
|
-
type: "turn/start",
|
|
14998
|
-
seq: SessionSeq(0),
|
|
14999
|
-
time: 1,
|
|
15000
|
-
data: { turn: 1 }
|
|
15001
|
-
}, {
|
|
15002
|
-
type: "turn/end",
|
|
15003
|
-
seq: SessionSeq(1),
|
|
15004
|
-
time: 2,
|
|
15005
|
-
data: {
|
|
15006
|
-
turn: 1,
|
|
15007
|
-
reason: { kind: "extension-reason" }
|
|
15008
|
-
}
|
|
15009
|
-
}]);
|
|
15010
|
-
globalExpect((await ctx.sessionPersistence.inspect(extendedId)).events[1]).toMatchObject({
|
|
15011
|
-
type: "turn/end",
|
|
15012
|
-
data: { reason: { kind: "extension-reason" } }
|
|
15013
|
-
});
|
|
15014
|
-
} finally {
|
|
15015
|
-
await fiber.dispose();
|
|
15016
|
-
await fix.cleanup();
|
|
15017
|
-
}
|
|
15018
|
-
});
|
|
15019
|
-
it("rejects malformed persisted message events before returning them", async () => {
|
|
15020
|
-
const fix = await makeFixture();
|
|
15021
|
-
const { ctx, fiber } = await freshCtx(fix);
|
|
15022
|
-
try {
|
|
15023
|
-
const id = SessionId("invalid-message-read");
|
|
15024
|
-
await ctx.sessionPersistence.create(meta(id, WORK));
|
|
15025
|
-
await ctx.sessionPersistence.append(id, [{
|
|
15026
|
-
type: "user/message",
|
|
15027
|
-
seq: SessionSeq(0),
|
|
15028
|
-
time: 1,
|
|
15029
|
-
surfaceOp: "append",
|
|
15030
|
-
data: {
|
|
15031
|
-
id: "wrong-role",
|
|
15032
|
-
role: "assistant",
|
|
15033
|
-
content: [{
|
|
15034
|
-
type: "text",
|
|
15035
|
-
text: "wrong"
|
|
15036
|
-
}],
|
|
15037
|
-
source: { kind: "user" }
|
|
15038
|
-
}
|
|
15039
|
-
}]);
|
|
15040
|
-
await globalExpect(ctx.sessionPersistence.inspect(id)).rejects.toThrow("message must have role \"user\"");
|
|
15041
|
-
await globalExpect(ctx.sessionPersistence.load(id)).rejects.toThrow("message must have role \"user\"");
|
|
15042
|
-
const malformedLegacy = [
|
|
15043
|
-
{
|
|
15044
|
-
id: "invalid-old-turn-start",
|
|
15045
|
-
event: {
|
|
15046
|
-
type: "turn/start",
|
|
15047
|
-
seq: SessionSeq(0),
|
|
15048
|
-
time: 1,
|
|
15049
|
-
data: {
|
|
15050
|
-
turn: 1,
|
|
15051
|
-
trigger: null
|
|
15052
|
-
}
|
|
15053
|
-
},
|
|
15054
|
-
message: "malformed pre-react-loop turn/start"
|
|
15055
|
-
},
|
|
15056
|
-
{
|
|
15057
|
-
id: "invalid-old-steering",
|
|
15058
|
-
event: {
|
|
15059
|
-
type: "steering/message",
|
|
15060
|
-
seq: SessionSeq(0),
|
|
15061
|
-
time: 1,
|
|
15062
|
-
surfaceOp: "append",
|
|
15063
|
-
data: {
|
|
15064
|
-
turn: 1,
|
|
15065
|
-
content: [],
|
|
15066
|
-
source: { kind: "user" },
|
|
15067
|
-
extra: true
|
|
15068
|
-
}
|
|
15069
|
-
},
|
|
15070
|
-
message: "malformed pre-react-loop steering/message"
|
|
15071
|
-
},
|
|
15072
|
-
{
|
|
15073
|
-
id: "invalid-old-steering-data",
|
|
15074
|
-
event: {
|
|
15075
|
-
type: "steering/message",
|
|
15076
|
-
seq: SessionSeq(0),
|
|
15077
|
-
time: 1,
|
|
15078
|
-
surfaceOp: "append",
|
|
15079
|
-
data: null
|
|
15080
|
-
},
|
|
15081
|
-
message: "malformed pre-react-loop steering/message"
|
|
15082
|
-
},
|
|
15083
|
-
{
|
|
15084
|
-
id: "invalid-old-turn-end",
|
|
15085
|
-
event: {
|
|
15086
|
-
type: "turn/end",
|
|
15087
|
-
seq: SessionSeq(0),
|
|
15088
|
-
time: 1,
|
|
15089
|
-
data: {
|
|
15090
|
-
turn: 1,
|
|
15091
|
-
reason: {
|
|
15092
|
-
kind: "completed",
|
|
15093
|
-
extra: true
|
|
15094
|
-
}
|
|
15095
|
-
}
|
|
15096
|
-
},
|
|
15097
|
-
message: "malformed pre-react-loop turn/end"
|
|
15098
|
-
},
|
|
15099
|
-
{
|
|
15100
|
-
id: "invalid-old-turn-end-reason",
|
|
15101
|
-
event: {
|
|
15102
|
-
type: "turn/end",
|
|
15103
|
-
seq: SessionSeq(0),
|
|
15104
|
-
time: 1,
|
|
15105
|
-
data: {
|
|
15106
|
-
turn: 1,
|
|
15107
|
-
reason: null
|
|
15108
|
-
}
|
|
15109
|
-
},
|
|
15110
|
-
message: "malformed pre-react-loop turn/end"
|
|
15111
|
-
},
|
|
15112
|
-
{
|
|
15113
|
-
id: "unsupported-intermediate-turn-end-step",
|
|
15114
|
-
event: {
|
|
15115
|
-
type: "turn/end",
|
|
15116
|
-
seq: SessionSeq(0),
|
|
15117
|
-
time: 1,
|
|
15118
|
-
data: {
|
|
15119
|
-
turn: 1,
|
|
15120
|
-
step: 1,
|
|
15121
|
-
reason: { kind: "completed" }
|
|
15122
|
-
}
|
|
15123
|
-
},
|
|
15124
|
-
message: "malformed pre-react-loop turn/end"
|
|
15125
|
-
},
|
|
15126
|
-
{
|
|
15127
|
-
id: "invalid-old-turn-end-aborted",
|
|
15128
|
-
event: {
|
|
15129
|
-
type: "turn/end",
|
|
15130
|
-
seq: SessionSeq(0),
|
|
15131
|
-
time: 1,
|
|
15132
|
-
data: {
|
|
15133
|
-
turn: 1,
|
|
15134
|
-
reason: {
|
|
15135
|
-
kind: "aborted",
|
|
15136
|
-
extra: true
|
|
15137
|
-
}
|
|
15138
|
-
}
|
|
15139
|
-
},
|
|
15140
|
-
message: "malformed pre-react-loop turn/end"
|
|
15141
|
-
},
|
|
15142
|
-
{
|
|
15143
|
-
id: "invalid-old-turn-end-disposed",
|
|
15144
|
-
event: {
|
|
15145
|
-
type: "turn/end",
|
|
15146
|
-
seq: SessionSeq(0),
|
|
15147
|
-
time: 1,
|
|
15148
|
-
data: {
|
|
15149
|
-
turn: 1,
|
|
15150
|
-
reason: {
|
|
15151
|
-
kind: "disposed",
|
|
15152
|
-
extra: true
|
|
15153
|
-
}
|
|
15154
|
-
}
|
|
15155
|
-
},
|
|
15156
|
-
message: "malformed pre-react-loop turn/end"
|
|
15157
|
-
},
|
|
15158
|
-
{
|
|
15159
|
-
id: "invalid-old-turn-end-error-step",
|
|
15160
|
-
event: {
|
|
15161
|
-
type: "turn/end",
|
|
15162
|
-
seq: SessionSeq(0),
|
|
15163
|
-
time: 1,
|
|
15164
|
-
data: {
|
|
15165
|
-
turn: 1,
|
|
15166
|
-
reason: {
|
|
15167
|
-
kind: "error",
|
|
15168
|
-
step: -1,
|
|
15169
|
-
message: "bad step"
|
|
15170
|
-
}
|
|
15171
|
-
}
|
|
15172
|
-
},
|
|
15173
|
-
message: "malformed pre-react-loop turn/end"
|
|
15174
|
-
},
|
|
15175
|
-
{
|
|
15176
|
-
id: "invalid-old-turn-end-error-code",
|
|
15177
|
-
event: {
|
|
15178
|
-
type: "turn/end",
|
|
15179
|
-
seq: SessionSeq(0),
|
|
15180
|
-
time: 1,
|
|
15181
|
-
data: {
|
|
15182
|
-
turn: 1,
|
|
15183
|
-
reason: {
|
|
15184
|
-
kind: "error",
|
|
15185
|
-
step: 0,
|
|
15186
|
-
message: "bad code",
|
|
15187
|
-
code: 1
|
|
15188
|
-
}
|
|
15189
|
-
}
|
|
15190
|
-
},
|
|
15191
|
-
message: "malformed pre-react-loop turn/end"
|
|
15192
|
-
}
|
|
15193
|
-
];
|
|
15194
|
-
for (const malformed of malformedLegacy) {
|
|
15195
|
-
const malformedId = SessionId(malformed.id);
|
|
15196
|
-
await ctx.sessionPersistence.create(meta(malformedId, WORK));
|
|
15197
|
-
await ctx.sessionPersistence.append(malformedId, [malformed.event]);
|
|
15198
|
-
await globalExpect(ctx.sessionPersistence.inspect(malformedId)).rejects.toThrow(malformed.message);
|
|
15199
|
-
await globalExpect(ctx.sessionPersistence.readFrom(malformedId, SessionLogOffset(0))).rejects.toThrow(malformed.message);
|
|
15200
|
-
}
|
|
15201
|
-
for (const type of ["tool/result"]) {
|
|
15202
|
-
const malformedId = SessionId(`invalid-${type}`);
|
|
15203
|
-
await ctx.sessionPersistence.create(meta(malformedId, WORK));
|
|
15204
|
-
await ctx.sessionPersistence.append(malformedId, [{
|
|
15205
|
-
type,
|
|
15206
|
-
seq: SessionSeq(0),
|
|
15207
|
-
time: 1,
|
|
15208
|
-
surfaceOp: "append",
|
|
15209
|
-
data: { message: null }
|
|
15210
|
-
}]);
|
|
15211
|
-
await globalExpect(ctx.sessionPersistence.inspect(malformedId)).rejects.toThrow("lacks an identified message");
|
|
15212
|
-
}
|
|
15213
|
-
const pluginId = SessionId("non-object-plugin-event");
|
|
15214
|
-
await ctx.sessionPersistence.create(meta(pluginId, WORK));
|
|
15215
|
-
await ctx.sessionPersistence.append(pluginId, [{
|
|
15216
|
-
type: "plugin/test",
|
|
15217
|
-
seq: SessionSeq(0),
|
|
15218
|
-
time: 1,
|
|
15219
|
-
data: null,
|
|
15220
|
-
ignorable: true
|
|
15221
|
-
}]);
|
|
15222
|
-
await globalExpect(ctx.sessionPersistence.inspect(pluginId)).rejects.toThrow(/not found/);
|
|
15223
|
-
await globalExpect(ctx.sessionPersistence.readFrom(pluginId, SessionLogOffset(0))).rejects.toThrow(/not found/);
|
|
15224
|
-
const requiredId = SessionId("non-object-required-plugin-event");
|
|
15225
|
-
await ctx.sessionPersistence.create(meta(requiredId, WORK));
|
|
15226
|
-
await ctx.sessionPersistence.append(requiredId, [{
|
|
15227
|
-
type: "plugin/required",
|
|
15228
|
-
seq: SessionSeq(0),
|
|
15229
|
-
time: 1,
|
|
15230
|
-
data: null
|
|
15231
|
-
}]);
|
|
15232
|
-
await globalExpect(ctx.sessionPersistence.inspect(requiredId)).rejects.toThrow(/unknown to this harness/);
|
|
15233
|
-
for (const type of ["user/message", "assistant/message"]) {
|
|
15234
|
-
const missingContentId = SessionId(`invalid-${type}-without-content`);
|
|
15235
|
-
await ctx.sessionPersistence.create(meta(missingContentId, WORK));
|
|
15236
|
-
await ctx.sessionPersistence.append(missingContentId, [{
|
|
15237
|
-
type,
|
|
15238
|
-
seq: SessionSeq(0),
|
|
15239
|
-
time: 1,
|
|
15240
|
-
surfaceOp: "append",
|
|
15241
|
-
data: {}
|
|
15242
|
-
}]);
|
|
15243
|
-
await globalExpect(ctx.sessionPersistence.readFrom(missingContentId, SessionLogOffset(0))).rejects.toThrow("lacks an identified message");
|
|
15244
|
-
}
|
|
15245
|
-
} finally {
|
|
15246
|
-
await fiber.dispose();
|
|
15247
|
-
await fix.cleanup();
|
|
15248
|
-
}
|
|
15249
|
-
});
|
|
15250
|
-
it("append snapshots the batch: mutating the caller array/events after the call is ignored", async () => {
|
|
15251
|
-
const fix = await makeFixture();
|
|
15252
|
-
const { ctx, fiber } = await freshCtx(fix);
|
|
15253
|
-
try {
|
|
15254
|
-
const m = meta("snapshot", WORK);
|
|
15255
|
-
await ctx.sessionPersistence.create(m);
|
|
15256
|
-
const events = structuredClone(oneTurnLog());
|
|
15257
|
-
const userMsg = events[1];
|
|
15258
|
-
const p = ctx.sessionPersistence.append(m.id, events);
|
|
15259
|
-
events.push({
|
|
15260
|
-
type: "turn/start",
|
|
15261
|
-
seq: SessionSeq(6),
|
|
15262
|
-
time: 99,
|
|
15263
|
-
data: { turn: 2 }
|
|
15264
|
-
});
|
|
15265
|
-
if (userMsg?.type === "user/message") userMsg.data.content = [{
|
|
15266
|
-
type: "text",
|
|
15267
|
-
text: "MUTATED"
|
|
15268
|
-
}];
|
|
15269
|
-
await p;
|
|
15270
|
-
const loaded = await ctx.sessionPersistence.load(m.id);
|
|
15271
|
-
globalExpect(loaded.events.map((e) => e.seq)).toEqual([
|
|
15272
|
-
0,
|
|
15273
|
-
1,
|
|
15274
|
-
2,
|
|
15275
|
-
3,
|
|
15276
|
-
4,
|
|
15277
|
-
5
|
|
15278
|
-
]);
|
|
15279
|
-
const persisted = JSON.stringify(loaded.events);
|
|
15280
|
-
globalExpect(persisted).toContain("hi");
|
|
15281
|
-
globalExpect(persisted).not.toContain("MUTATED");
|
|
15282
|
-
} finally {
|
|
15283
|
-
await fiber.dispose();
|
|
15284
|
-
await fix.cleanup();
|
|
15285
|
-
}
|
|
15286
|
-
});
|
|
15287
|
-
it("fork: a seeded new session persists its seed once (no double-write on a no-op flush)", async () => {
|
|
15288
|
-
const fix = await makeFixture();
|
|
15289
|
-
const { ctx, fiber } = await freshCtx(fix);
|
|
15290
|
-
try {
|
|
15291
|
-
const seed = oneTurnLog();
|
|
15292
|
-
const forked = ctx.sessions.create(SessionId("forked"), {
|
|
15293
|
-
seed,
|
|
15294
|
-
meta: { cwd: WORK }
|
|
15295
|
-
});
|
|
15296
|
-
await ctx.sessions.flush(forked);
|
|
15297
|
-
const loaded = await ctx.sessionPersistence.load(SessionId("forked"));
|
|
15298
|
-
globalExpect(loaded.events.slice(0, seed.length)).toEqual(seed);
|
|
15299
|
-
globalExpect(loaded.events.at(-1)).toMatchObject({
|
|
15300
|
-
type: "session/end-seed",
|
|
15301
|
-
seq: seed.length
|
|
15302
|
-
});
|
|
15303
|
-
await ctx.sessions.flush(forked);
|
|
15304
|
-
const reloaded = await ctx.sessionPersistence.load(SessionId("forked"));
|
|
15305
|
-
globalExpect(reloaded.events).toEqual(loaded.events);
|
|
15306
|
-
} finally {
|
|
15307
|
-
await fiber.dispose();
|
|
15308
|
-
await fix.cleanup();
|
|
15309
|
-
}
|
|
15310
|
-
});
|
|
15311
|
-
it("resume: a re-created session seeded with the loaded log does not re-append its seed and continues the seq", async () => {
|
|
15312
|
-
const fix = await makeFixture();
|
|
15313
|
-
const first = await freshCtx(fix);
|
|
15314
|
-
try {
|
|
15315
|
-
const s1 = first.ctx.sessions.create(SessionId("resumed"), { meta: { cwd: WORK } });
|
|
15316
|
-
send(s1, oneTurnLog());
|
|
15317
|
-
await first.ctx.sessions.flush(s1);
|
|
15318
|
-
} finally {
|
|
15319
|
-
await first.fiber.dispose();
|
|
15320
|
-
}
|
|
15321
|
-
const second = await freshCtx(fix);
|
|
15322
|
-
try {
|
|
15323
|
-
const loaded = await second.ctx.sessionPersistence.load(SessionId("resumed"));
|
|
15324
|
-
const s2 = second.ctx.sessions.create(SessionId("resumed"), {
|
|
15325
|
-
seed: loaded.events,
|
|
15326
|
-
meta: { cwd: WORK }
|
|
15327
|
-
});
|
|
15328
|
-
await second.ctx.sessions.flush(s2);
|
|
15329
|
-
s2.append("turn/start", { turn: 2 });
|
|
15330
|
-
s2.append("turn/end", {
|
|
15331
|
-
turn: 2,
|
|
15332
|
-
reason: { kind: "completed" }
|
|
15333
|
-
});
|
|
15334
|
-
await second.ctx.sessions.flush(s2);
|
|
15335
|
-
const reloaded = await second.ctx.sessionPersistence.load(SessionId("resumed"));
|
|
15336
|
-
globalExpect(reloaded.events.map((e) => e.seq)).toEqual([
|
|
15337
|
-
0,
|
|
15338
|
-
1,
|
|
15339
|
-
2,
|
|
15340
|
-
3,
|
|
15341
|
-
4,
|
|
15342
|
-
5,
|
|
15343
|
-
6,
|
|
15344
|
-
7,
|
|
15345
|
-
8
|
|
15346
|
-
]);
|
|
15347
|
-
globalExpect(reloaded.events[6]).toMatchObject({ type: "session/end-seed" });
|
|
15348
|
-
} finally {
|
|
15349
|
-
await second.fiber.dispose();
|
|
15350
|
-
await fix.cleanup();
|
|
15351
|
-
}
|
|
15352
|
-
});
|
|
15353
|
-
it("HMR: applying the plugin seeds existing live sessions", async () => {
|
|
15354
|
-
const fix = await makeFixture();
|
|
15355
|
-
const ctx = new Context();
|
|
15356
|
-
await ctx.plugin(SessionStore);
|
|
15357
|
-
const session = ctx.sessions.create(SessionId("pre-existing"), { meta: { cwd: WORK } });
|
|
15358
|
-
session.append("turn/start", { turn: 1 });
|
|
15359
|
-
session.append("user/message", createUserMessage({
|
|
15360
|
-
content: [{
|
|
15361
|
-
type: "text",
|
|
15362
|
-
text: "hi"
|
|
15363
|
-
}],
|
|
15364
|
-
source: { kind: "user" }
|
|
15365
|
-
}), { surfaceOp: "append" });
|
|
15366
|
-
session.append("turn/end", {
|
|
15367
|
-
turn: 1,
|
|
15368
|
-
reason: { kind: "completed" }
|
|
15369
|
-
});
|
|
15370
|
-
const fiber = await fix.mount(ctx);
|
|
15371
|
-
try {
|
|
15372
|
-
await ctx.sessions.flush(session);
|
|
15373
|
-
const loaded = await ctx.sessionPersistence.load(SessionId("pre-existing"));
|
|
15374
|
-
globalExpect(loaded.events.length).toBeGreaterThanOrEqual(2);
|
|
15375
|
-
} finally {
|
|
15376
|
-
await fiber.dispose();
|
|
15377
|
-
await fix.cleanup();
|
|
15378
|
-
}
|
|
15379
|
-
});
|
|
15380
|
-
it("HMR: dispose drains remaining buffers", async () => {
|
|
15381
|
-
const fix = await makeFixture();
|
|
15382
|
-
const ctx = new Context();
|
|
15383
|
-
await ctx.plugin(SessionStore);
|
|
15384
|
-
const fiber = await fix.mount(ctx);
|
|
15385
|
-
const session = await liveSessionInFiber(ctx, "drain", WORK);
|
|
15386
|
-
session.append("turn/start", { turn: 1 });
|
|
15387
|
-
session.append("user/message", createUserMessage({
|
|
15388
|
-
content: [{
|
|
15389
|
-
type: "text",
|
|
15390
|
-
text: "buffered"
|
|
15391
|
-
}],
|
|
15392
|
-
source: { kind: "user" }
|
|
15393
|
-
}), { surfaceOp: "append" });
|
|
15394
|
-
session.append("turn/end", {
|
|
15395
|
-
turn: 1,
|
|
15396
|
-
reason: { kind: "completed" }
|
|
15397
|
-
});
|
|
15398
|
-
await fiber.dispose();
|
|
15399
|
-
const second = await freshCtx(fix);
|
|
15400
|
-
try {
|
|
15401
|
-
const loaded = await second.ctx.sessionPersistence.load(SessionId("drain"));
|
|
15402
|
-
globalExpect(loaded.events.length).toBeGreaterThanOrEqual(2);
|
|
15403
|
-
} finally {
|
|
15404
|
-
await second.fiber.dispose();
|
|
15405
|
-
await fix.cleanup();
|
|
15406
|
-
}
|
|
15407
|
-
});
|
|
15408
|
-
it("HMR: reloading the backend adopts a still-live, already-materialized session", async () => {
|
|
15409
|
-
const fix = await makeFixture();
|
|
15410
|
-
const ctx = new Context();
|
|
15411
|
-
await ctx.plugin(SessionStore);
|
|
15412
|
-
const session = await liveSessionInFiber(ctx, "hmr-adopt", WORK);
|
|
15413
|
-
try {
|
|
15414
|
-
const backend1 = await fix.mount(ctx);
|
|
15415
|
-
session.append("turn/start", { turn: 1 });
|
|
15416
|
-
session.append("user/message", createUserMessage({
|
|
15417
|
-
content: [{
|
|
15418
|
-
type: "text",
|
|
15419
|
-
text: "hi"
|
|
15420
|
-
}],
|
|
15421
|
-
source: { kind: "user" }
|
|
15422
|
-
}), { surfaceOp: "append" });
|
|
15423
|
-
session.append("turn/end", {
|
|
15424
|
-
turn: 1,
|
|
15425
|
-
reason: { kind: "completed" }
|
|
15426
|
-
});
|
|
15427
|
-
await ctx.sessions.flush(session);
|
|
15428
|
-
await backend1.dispose();
|
|
15429
|
-
await fix.mount(ctx);
|
|
15430
|
-
session.append("turn/start", { turn: 2 });
|
|
15431
|
-
session.append("user/message", createUserMessage({
|
|
15432
|
-
content: [{
|
|
15433
|
-
type: "text",
|
|
15434
|
-
text: "again"
|
|
15435
|
-
}],
|
|
15436
|
-
source: { kind: "user" }
|
|
15437
|
-
}), { surfaceOp: "append" });
|
|
15438
|
-
session.append("turn/end", {
|
|
15439
|
-
turn: 2,
|
|
15440
|
-
reason: { kind: "completed" }
|
|
15441
|
-
});
|
|
15442
|
-
await globalExpect(ctx.sessions.flush(session)).resolves.not.toThrow();
|
|
15443
|
-
const loaded = await ctx.sessionPersistence.load(SessionId("hmr-adopt"));
|
|
15444
|
-
globalExpect(loaded.events.filter((e) => e.type === "turn/start")).toHaveLength(2);
|
|
15445
|
-
} finally {
|
|
15446
|
-
await ctx.fiber.dispose();
|
|
15447
|
-
await fix.cleanup();
|
|
15448
|
-
}
|
|
15449
|
-
});
|
|
15450
|
-
it("HMR: adoption persists the live SUFFIX that was ahead of the stored prefix", async () => {
|
|
15451
|
-
const fix = await makeFixture();
|
|
15452
|
-
const ctx = new Context();
|
|
15453
|
-
await ctx.plugin(SessionStore);
|
|
15454
|
-
const session = await liveSessionInFiber(ctx, "hmr-suffix", WORK);
|
|
15455
|
-
try {
|
|
15456
|
-
const backend1 = await fix.mount(ctx);
|
|
15457
|
-
session.append("turn/start", { turn: 1 });
|
|
15458
|
-
session.append("turn/end", {
|
|
15459
|
-
turn: 1,
|
|
15460
|
-
reason: { kind: "completed" }
|
|
15461
|
-
});
|
|
15462
|
-
await ctx.sessions.flush(session);
|
|
15463
|
-
await backend1.dispose();
|
|
15464
|
-
session.append("turn/start", { turn: 2 });
|
|
15465
|
-
session.append("turn/end", {
|
|
15466
|
-
turn: 2,
|
|
15467
|
-
reason: { kind: "completed" }
|
|
15468
|
-
});
|
|
15469
|
-
await fix.mount(ctx);
|
|
15470
|
-
await ctx.sessions.flush(session);
|
|
15471
|
-
const loaded = await ctx.sessionPersistence.load(SessionId("hmr-suffix"));
|
|
15472
|
-
globalExpect(loaded.events.map((e) => e.seq)).toEqual([
|
|
15473
|
-
0,
|
|
15474
|
-
1,
|
|
15475
|
-
2,
|
|
15476
|
-
3
|
|
15477
|
-
]);
|
|
15478
|
-
globalExpect(loaded.events.filter((e) => e.type === "turn/start")).toHaveLength(2);
|
|
15479
|
-
} finally {
|
|
15480
|
-
await ctx.fiber.dispose();
|
|
15481
|
-
await fix.cleanup();
|
|
15482
|
-
}
|
|
15483
|
-
});
|
|
15484
|
-
it("HMR adoption does NOT crash-repair an active open turn as interrupted (truncate without closers)", async () => {
|
|
15485
|
-
const fix = await makeFixture();
|
|
15486
|
-
const ctx = new Context();
|
|
15487
|
-
await ctx.plugin(SessionStore);
|
|
15488
|
-
const session = await liveSessionInFiber(ctx, "hmr-open", WORK);
|
|
15489
|
-
try {
|
|
15490
|
-
const first = await fix.mount(ctx);
|
|
15491
|
-
session.append("turn/start", { turn: 1 });
|
|
15492
|
-
session.append("step/start", {
|
|
15493
|
-
turn: 1,
|
|
15494
|
-
step: 1
|
|
15495
|
-
});
|
|
15496
|
-
await ctx.sessions.flush(session);
|
|
15497
|
-
await first.dispose();
|
|
15498
|
-
if (fix.corruptTail) await fix.corruptTail(SessionId("hmr-open"), WORK);
|
|
15499
|
-
const second = await fix.mount(ctx);
|
|
15500
|
-
session.append("step/end", {
|
|
15501
|
-
turn: 1,
|
|
15502
|
-
step: 1
|
|
15503
|
-
});
|
|
15504
|
-
session.append("turn/end", {
|
|
15505
|
-
turn: 1,
|
|
15506
|
-
reason: { kind: "completed" }
|
|
15507
|
-
});
|
|
15508
|
-
await ctx.sessions.flush(session);
|
|
15509
|
-
const loaded = await ctx.sessionPersistence.load(SessionId("hmr-open"));
|
|
15510
|
-
globalExpect(loaded.events.map((e) => e.type)).toEqual([
|
|
15511
|
-
"turn/start",
|
|
15512
|
-
"step/start",
|
|
15513
|
-
"step/end",
|
|
15514
|
-
"turn/end"
|
|
15515
|
-
]);
|
|
15516
|
-
globalExpect(loaded.events.at(-1)).toMatchObject({
|
|
15517
|
-
type: "turn/end",
|
|
15518
|
-
data: { reason: { kind: "completed" } }
|
|
15519
|
-
});
|
|
15520
|
-
await second.dispose();
|
|
15521
|
-
} finally {
|
|
15522
|
-
await ctx.fiber.dispose();
|
|
15523
|
-
await fix.cleanup();
|
|
15524
|
-
}
|
|
15525
|
-
});
|
|
15526
|
-
it("a NEW live session colliding on a persisted id is rejected, not silently adopted", async () => {
|
|
15527
|
-
const fix = await makeFixture();
|
|
15528
|
-
const first = await freshCtx(fix);
|
|
15529
|
-
try {
|
|
15530
|
-
const s1 = first.ctx.sessions.create(SessionId("collide"), { meta: { cwd: WORK } });
|
|
15531
|
-
send(s1, oneTurnLog());
|
|
15532
|
-
await first.ctx.sessions.flush(s1);
|
|
15533
|
-
} finally {
|
|
15534
|
-
await first.fiber.dispose();
|
|
15535
|
-
}
|
|
15536
|
-
const second = await freshCtx(fix);
|
|
15537
|
-
try {
|
|
15538
|
-
const s2 = second.ctx.sessions.create(SessionId("collide"), { meta: { cwd: WORK } });
|
|
15539
|
-
s2.append("turn/start", { turn: 1 });
|
|
15540
|
-
await globalExpect(second.ctx.sessions.flush(s2)).rejects.toThrow(/already has a persisted log|id collision/);
|
|
15541
|
-
} finally {
|
|
15542
|
-
await second.fiber.dispose();
|
|
15543
|
-
await fix.cleanup();
|
|
15544
|
-
}
|
|
15545
|
-
});
|
|
15546
|
-
it("an abandoned lazy session (never materialized) releases its id for reuse", async () => {
|
|
15547
|
-
const fix = await makeFixture();
|
|
15548
|
-
const { ctx, fiber } = await freshCtx(fix);
|
|
15549
|
-
try {
|
|
15550
|
-
let firstSession;
|
|
15551
|
-
const firstFiber = await ctx.plugin(Object.assign((inner) => {
|
|
15552
|
-
firstSession = inner.sessions.create(SessionId("abandoned"), { meta: { cwd: WORK } });
|
|
15553
|
-
}, { inject: ["sessions"] }));
|
|
15554
|
-
await ctx.sessions.flush(firstSession);
|
|
15555
|
-
await firstFiber.dispose();
|
|
15556
|
-
let reuse;
|
|
15557
|
-
await ctx.plugin(Object.assign((inner) => {
|
|
15558
|
-
reuse = inner.sessions.create(SessionId("abandoned"), { meta: { cwd: WORK } });
|
|
15559
|
-
}, { inject: ["sessions"] }));
|
|
15560
|
-
await globalExpect(ctx.sessions.flush(reuse)).resolves.toBe(true);
|
|
15561
|
-
reuse.append("turn/start", { turn: 1 });
|
|
15562
|
-
reuse.append("turn/end", {
|
|
15563
|
-
turn: 1,
|
|
15564
|
-
reason: { kind: "completed" }
|
|
15565
|
-
});
|
|
15566
|
-
await ctx.sessions.flush(reuse);
|
|
15567
|
-
const loaded = await ctx.sessionPersistence.load(SessionId("abandoned"));
|
|
15568
|
-
globalExpect(loaded.events.map((e) => e.seq)).toEqual([0, 1]);
|
|
15569
|
-
} finally {
|
|
15570
|
-
await fiber.dispose();
|
|
15571
|
-
await fix.cleanup();
|
|
15572
|
-
}
|
|
15573
|
-
});
|
|
15574
|
-
it("session disposal drains buffered events before retiring ownership", async () => {
|
|
15575
|
-
const fix = await makeFixture();
|
|
15576
|
-
const { ctx, fiber } = await freshCtx(fix);
|
|
15577
|
-
try {
|
|
15578
|
-
let first;
|
|
15579
|
-
const firstFiber = await ctx.plugin(Object.assign((inner) => {
|
|
15580
|
-
first = inner.sessions.create(SessionId("buffered"), { meta: { cwd: WORK } });
|
|
15581
|
-
}, { inject: ["sessions"] }));
|
|
15582
|
-
await ctx.sessions.flush(first);
|
|
15583
|
-
first.append("turn/start", { turn: 1 });
|
|
15584
|
-
first.append("turn/end", {
|
|
15585
|
-
turn: 1,
|
|
15586
|
-
reason: { kind: "completed" }
|
|
15587
|
-
});
|
|
15588
|
-
await firstFiber.dispose();
|
|
15589
|
-
await vi.waitFor(async () => {
|
|
15590
|
-
globalExpect((await ctx.sessionPersistence.list()).map((meta) => meta.id)).toContain(SessionId("buffered"));
|
|
15591
|
-
});
|
|
15592
|
-
globalExpect((await ctx.sessionPersistence.load(SessionId("buffered"))).events.map((event) => event.seq)).toEqual([0, 1]);
|
|
15593
|
-
let reuse;
|
|
15594
|
-
await ctx.plugin(Object.assign((inner) => {
|
|
15595
|
-
reuse = inner.sessions.create(SessionId("buffered"), { meta: { cwd: WORK } });
|
|
14400
|
+
let session;
|
|
14401
|
+
const sessionFiber = await ctx.plugin(Object.assign((inner) => {
|
|
14402
|
+
session = inner.sessions.create(SessionId("delegated-child"), { meta: {
|
|
14403
|
+
cwd: WORK,
|
|
14404
|
+
parentSession: SessionId("root"),
|
|
14405
|
+
delegationDepth: 2
|
|
14406
|
+
} });
|
|
15596
14407
|
}, { inject: ["sessions"] }));
|
|
15597
|
-
|
|
14408
|
+
send(session, oneTurnLog());
|
|
14409
|
+
await ctx.parallel("session/flush", session);
|
|
14410
|
+
await sessionFiber.dispose();
|
|
14411
|
+
const loaded = await readAll(ctx, SessionId("delegated-child"));
|
|
14412
|
+
globalExpect(loaded.meta.delegationDepth).toBe(2);
|
|
15598
14413
|
} finally {
|
|
15599
14414
|
await fiber.dispose();
|
|
15600
14415
|
await fix.cleanup();
|
|
15601
14416
|
}
|
|
15602
14417
|
});
|
|
15603
|
-
it("
|
|
14418
|
+
it("source-frozen events cannot be mutated after buffering and persist unchanged", async () => {
|
|
15604
14419
|
const fix = await makeFixture();
|
|
15605
14420
|
const { ctx, fiber } = await freshCtx(fix);
|
|
15606
14421
|
try {
|
|
15607
|
-
const session = ctx.sessions.create(SessionId("
|
|
14422
|
+
const session = ctx.sessions.create(SessionId("mutate"), { meta: { cwd: WORK } });
|
|
15608
14423
|
session.append("turn/start", { turn: 1 });
|
|
15609
|
-
session.append("user/message", createUserMessage({
|
|
14424
|
+
const ev = session.append("user/message", createUserMessage({
|
|
15610
14425
|
content: [{
|
|
15611
14426
|
type: "text",
|
|
15612
|
-
text: "
|
|
14427
|
+
text: "original"
|
|
15613
14428
|
}],
|
|
15614
14429
|
source: { kind: "user" }
|
|
15615
14430
|
}), { surfaceOp: "append" });
|
|
14431
|
+
globalExpect(() => {
|
|
14432
|
+
ev.data.content[0].text = "HACKED";
|
|
14433
|
+
}).toThrow(TypeError);
|
|
15616
14434
|
session.append("turn/end", {
|
|
15617
14435
|
turn: 1,
|
|
15618
14436
|
reason: { kind: "completed" }
|
|
15619
14437
|
});
|
|
15620
14438
|
await ctx.sessions.flush(session);
|
|
15621
|
-
ctx.
|
|
15622
|
-
|
|
15623
|
-
const loaded = await ctx.sessionPersistence.load(SessionId("idem"));
|
|
15624
|
-
globalExpect(loaded.events).toHaveLength(3);
|
|
14439
|
+
const message = (await readAll(ctx, SessionId("mutate"))).events.find((event) => event.type === "user/message");
|
|
14440
|
+
globalExpect(message?.type === "user/message" && message.data.content[0].text).toBe("original");
|
|
15625
14441
|
} finally {
|
|
15626
14442
|
await fiber.dispose();
|
|
15627
14443
|
await fix.cleanup();
|
|
15628
14444
|
}
|
|
15629
14445
|
});
|
|
15630
|
-
it("a
|
|
14446
|
+
it("fork: a seeded new session persists its seed once (no double-write on a no-op flush)", async () => {
|
|
15631
14447
|
const fix = await makeFixture();
|
|
15632
14448
|
const { ctx, fiber } = await freshCtx(fix);
|
|
15633
14449
|
try {
|
|
15634
|
-
|
|
15635
|
-
const
|
|
15636
|
-
seed
|
|
14450
|
+
const seed = oneTurnLog();
|
|
14451
|
+
const forked = ctx.sessions.create(SessionId("forked"), {
|
|
14452
|
+
seed,
|
|
15637
14453
|
meta: { cwd: WORK }
|
|
15638
14454
|
});
|
|
15639
|
-
await
|
|
15640
|
-
const loaded = await ctx
|
|
15641
|
-
globalExpect(loaded.events.
|
|
15642
|
-
|
|
15643
|
-
|
|
15644
|
-
|
|
15645
|
-
|
|
15646
|
-
|
|
15647
|
-
|
|
15648
|
-
|
|
15649
|
-
]);
|
|
15650
|
-
globalExpect(loaded.events.at(-1)).toMatchObject({ type: "session/end-seed" });
|
|
14455
|
+
await ctx.sessions.flush(forked);
|
|
14456
|
+
const loaded = await readAll(ctx, SessionId("forked"));
|
|
14457
|
+
globalExpect(loaded.events.slice(0, seed.length)).toEqual(seed);
|
|
14458
|
+
globalExpect(loaded.events.at(-1)).toMatchObject({
|
|
14459
|
+
type: "session/end-seed",
|
|
14460
|
+
seq: seed.length
|
|
14461
|
+
});
|
|
14462
|
+
await ctx.sessions.flush(forked);
|
|
14463
|
+
const reloaded = await readAll(ctx, SessionId("forked"));
|
|
14464
|
+
globalExpect(reloaded.events).toEqual(loaded.events);
|
|
15651
14465
|
} finally {
|
|
15652
14466
|
await fiber.dispose();
|
|
15653
14467
|
await fix.cleanup();
|
|
15654
14468
|
}
|
|
15655
14469
|
});
|
|
15656
|
-
it("a
|
|
14470
|
+
it("resume: a re-created session seeded with the loaded log does not re-append its seed and continues the seq", async () => {
|
|
15657
14471
|
const fix = await makeFixture();
|
|
15658
|
-
const
|
|
14472
|
+
const first = await freshCtx(fix);
|
|
15659
14473
|
try {
|
|
15660
|
-
|
|
15661
|
-
|
|
15662
|
-
await ctx.
|
|
15663
|
-
let fresh;
|
|
15664
|
-
await ctx.plugin(Object.assign((inner) => {
|
|
15665
|
-
fresh = inner.sessions.create(SessionId("preview"), { meta: { cwd: WORK } });
|
|
15666
|
-
}, { inject: ["sessions"] }));
|
|
15667
|
-
await globalExpect(ctx.sessions.flush(fresh)).rejects.toThrow(/do not match this live session|already has a persisted log|id collision/);
|
|
14474
|
+
const s1 = first.ctx.sessions.create(SessionId("resumed"), { meta: { cwd: WORK } });
|
|
14475
|
+
send(s1, oneTurnLog());
|
|
14476
|
+
await first.ctx.sessions.flush(s1);
|
|
15668
14477
|
} finally {
|
|
15669
|
-
await fiber.dispose();
|
|
15670
|
-
await fix.cleanup();
|
|
14478
|
+
await first.fiber.dispose();
|
|
15671
14479
|
}
|
|
15672
|
-
|
|
15673
|
-
it("a live session whose complete seed matches loaded ownerless state claims it without appending", async () => {
|
|
15674
|
-
const fix = await makeFixture();
|
|
15675
|
-
const { ctx, fiber } = await freshCtx(fix);
|
|
14480
|
+
const second = await freshCtx(fix);
|
|
15676
14481
|
try {
|
|
15677
|
-
const
|
|
15678
|
-
const
|
|
15679
|
-
|
|
15680
|
-
seq: SessionSeq(6),
|
|
15681
|
-
time: 7,
|
|
15682
|
-
data: {}
|
|
15683
|
-
}];
|
|
15684
|
-
await ctx.sessionPersistence.create(meta(id, WORK));
|
|
15685
|
-
await ctx.sessionPersistence.append(id, completeSeed);
|
|
15686
|
-
const { events } = await ctx.sessionPersistence.load(id);
|
|
15687
|
-
const live = ctx.sessions.create(id, {
|
|
15688
|
-
seed: events,
|
|
14482
|
+
const loaded = await readAll(second.ctx, SessionId("resumed"));
|
|
14483
|
+
const s2 = second.ctx.sessions.create(SessionId("resumed"), {
|
|
14484
|
+
seed: loaded.events,
|
|
15689
14485
|
meta: { cwd: WORK }
|
|
15690
14486
|
});
|
|
15691
|
-
await
|
|
15692
|
-
|
|
15693
|
-
|
|
15694
|
-
|
|
15695
|
-
|
|
15696
|
-
|
|
15697
|
-
|
|
15698
|
-
|
|
15699
|
-
|
|
15700
|
-
const { ctx, fiber } = await freshCtx(fix);
|
|
15701
|
-
try {
|
|
15702
|
-
const storedMeta = meta("claim", WORK);
|
|
15703
|
-
await ctx.sessionPersistence.create(storedMeta);
|
|
15704
|
-
await ctx.sessionPersistence.append(SessionId("claim"), oneTurnLog());
|
|
15705
|
-
const { events, meta: durableMeta } = await ctx.sessionPersistence.load(SessionId("claim"));
|
|
15706
|
-
let cont;
|
|
15707
|
-
const contFiber = await ctx.plugin(Object.assign((inner) => {
|
|
15708
|
-
cont = inner.sessions.create(SessionId("claim"), {
|
|
15709
|
-
seed: [
|
|
15710
|
-
...events,
|
|
15711
|
-
{
|
|
15712
|
-
type: "turn/start",
|
|
15713
|
-
seq: SessionSeq(6),
|
|
15714
|
-
time: 7,
|
|
15715
|
-
data: { turn: 2 }
|
|
15716
|
-
},
|
|
15717
|
-
{
|
|
15718
|
-
type: "turn/end",
|
|
15719
|
-
seq: SessionSeq(7),
|
|
15720
|
-
time: 8,
|
|
15721
|
-
data: {
|
|
15722
|
-
turn: 2,
|
|
15723
|
-
reason: { kind: "completed" }
|
|
15724
|
-
}
|
|
15725
|
-
}
|
|
15726
|
-
],
|
|
15727
|
-
meta: {
|
|
15728
|
-
cwd: WORK,
|
|
15729
|
-
createdAt: 2e3
|
|
15730
|
-
}
|
|
15731
|
-
});
|
|
15732
|
-
}, { inject: ["sessions"] }));
|
|
15733
|
-
await ctx.sessions.flush(cont);
|
|
15734
|
-
const loaded = await ctx.sessionPersistence.load(SessionId("claim"));
|
|
15735
|
-
globalExpect(loaded.events.map((e) => e.seq)).toEqual([
|
|
14487
|
+
await second.ctx.sessions.flush(s2);
|
|
14488
|
+
s2.append("turn/start", { turn: 2 });
|
|
14489
|
+
s2.append("turn/end", {
|
|
14490
|
+
turn: 2,
|
|
14491
|
+
reason: { kind: "completed" }
|
|
14492
|
+
});
|
|
14493
|
+
await second.ctx.sessions.flush(s2);
|
|
14494
|
+
const reloaded = await readAll(second.ctx, SessionId("resumed"));
|
|
14495
|
+
globalExpect(reloaded.events.map((e) => e.seq)).toEqual([
|
|
15736
14496
|
0,
|
|
15737
14497
|
1,
|
|
15738
14498
|
2,
|
|
@@ -15743,60 +14503,111 @@ function runCoordinatorContract(name, makeFixture) {
|
|
|
15743
14503
|
7,
|
|
15744
14504
|
8
|
|
15745
14505
|
]);
|
|
15746
|
-
globalExpect(
|
|
15747
|
-
globalExpect(loaded.meta).toEqual(durableMeta);
|
|
15748
|
-
globalExpect(loaded.meta.createdAt).toBe(1e3);
|
|
15749
|
-
await contFiber.dispose();
|
|
15750
|
-
await vi.waitFor(async () => {
|
|
15751
|
-
globalExpect((await ctx.sessionPersistence.load(SessionId("claim"))).meta).toEqual(durableMeta);
|
|
15752
|
-
});
|
|
14506
|
+
globalExpect(reloaded.events[6]).toMatchObject({ type: "session/end-seed" });
|
|
15753
14507
|
} finally {
|
|
15754
|
-
await fiber.dispose();
|
|
14508
|
+
await second.fiber.dispose();
|
|
15755
14509
|
await fix.cleanup();
|
|
15756
14510
|
}
|
|
15757
14511
|
});
|
|
15758
|
-
it("
|
|
14512
|
+
it("HMR: applying the plugin seeds existing live sessions", async () => {
|
|
15759
14513
|
const fix = await makeFixture();
|
|
15760
|
-
const
|
|
14514
|
+
const ctx = new Context();
|
|
14515
|
+
await ctx.plugin(SessionStore);
|
|
14516
|
+
const session = ctx.sessions.create(SessionId("pre-existing"), { meta: { cwd: WORK } });
|
|
14517
|
+
session.append("turn/start", { turn: 1 });
|
|
14518
|
+
session.append("user/message", createUserMessage({
|
|
14519
|
+
content: [{
|
|
14520
|
+
type: "text",
|
|
14521
|
+
text: "hi"
|
|
14522
|
+
}],
|
|
14523
|
+
source: { kind: "user" }
|
|
14524
|
+
}), { surfaceOp: "append" });
|
|
14525
|
+
session.append("turn/end", {
|
|
14526
|
+
turn: 1,
|
|
14527
|
+
reason: { kind: "completed" }
|
|
14528
|
+
});
|
|
14529
|
+
const fiber = await fix.mount(ctx);
|
|
15761
14530
|
try {
|
|
15762
|
-
await ctx.
|
|
15763
|
-
const
|
|
15764
|
-
|
|
15765
|
-
meta: { cwd: WORK }
|
|
15766
|
-
});
|
|
15767
|
-
await globalExpect(ctx.sessions.flush(live)).rejects.toThrow(/different cwd|id collision/);
|
|
14531
|
+
await ctx.sessions.flush(session);
|
|
14532
|
+
const loaded = await readAll(ctx, SessionId("pre-existing"));
|
|
14533
|
+
globalExpect(loaded.events.length).toBeGreaterThanOrEqual(2);
|
|
15768
14534
|
} finally {
|
|
15769
14535
|
await fiber.dispose();
|
|
15770
14536
|
await fix.cleanup();
|
|
15771
14537
|
}
|
|
15772
14538
|
});
|
|
15773
|
-
it("
|
|
14539
|
+
it("HMR: dispose drains remaining buffers", async () => {
|
|
14540
|
+
const fix = await makeFixture();
|
|
14541
|
+
const ctx = new Context();
|
|
14542
|
+
await ctx.plugin(SessionStore);
|
|
14543
|
+
const fiber = await fix.mount(ctx);
|
|
14544
|
+
const session = ctx.sessions.create(SessionId("drain"), { meta: { cwd: WORK } });
|
|
14545
|
+
session.append("turn/start", { turn: 1 });
|
|
14546
|
+
session.append("user/message", createUserMessage({
|
|
14547
|
+
content: [{
|
|
14548
|
+
type: "text",
|
|
14549
|
+
text: "buffered"
|
|
14550
|
+
}],
|
|
14551
|
+
source: { kind: "user" }
|
|
14552
|
+
}), { surfaceOp: "append" });
|
|
14553
|
+
session.append("turn/end", {
|
|
14554
|
+
turn: 1,
|
|
14555
|
+
reason: { kind: "completed" }
|
|
14556
|
+
});
|
|
14557
|
+
await fiber.dispose();
|
|
14558
|
+
const second = await freshCtx(fix);
|
|
14559
|
+
try {
|
|
14560
|
+
const loaded = await readAll(second.ctx, SessionId("drain"));
|
|
14561
|
+
globalExpect(loaded.events.length).toBeGreaterThanOrEqual(2);
|
|
14562
|
+
} finally {
|
|
14563
|
+
await second.fiber.dispose();
|
|
14564
|
+
await fix.cleanup();
|
|
14565
|
+
}
|
|
14566
|
+
});
|
|
14567
|
+
it("a NEW live session colliding on a persisted id is rejected, not silently adopted", async () => {
|
|
15774
14568
|
const fix = await makeFixture();
|
|
15775
14569
|
const { ctx, fiber } = await freshCtx(fix);
|
|
15776
14570
|
try {
|
|
15777
|
-
|
|
15778
|
-
await ctx.
|
|
15779
|
-
|
|
15780
|
-
|
|
15781
|
-
|
|
15782
|
-
|
|
15783
|
-
|
|
15784
|
-
|
|
14571
|
+
let first;
|
|
14572
|
+
const firstFiber = await ctx.plugin(Object.assign((inner) => {
|
|
14573
|
+
first = inner.sessions.create(SessionId("collide"), { meta: { cwd: WORK } });
|
|
14574
|
+
}, { inject: ["sessions"] }));
|
|
14575
|
+
send(first, oneTurnLog());
|
|
14576
|
+
await ctx.sessions.flush(first);
|
|
14577
|
+
await firstFiber.dispose();
|
|
14578
|
+
let second;
|
|
14579
|
+
await ctx.plugin(Object.assign((inner) => {
|
|
14580
|
+
second = inner.sessions.create(SessionId("collide"), { meta: { cwd: "/other" } });
|
|
14581
|
+
}, { inject: ["sessions"] }));
|
|
14582
|
+
await globalExpect(ctx.sessions.flush(second)).rejects.toThrow(/different cwd/);
|
|
15785
14583
|
} finally {
|
|
15786
14584
|
await fiber.dispose();
|
|
15787
14585
|
await fix.cleanup();
|
|
15788
14586
|
}
|
|
15789
14587
|
});
|
|
15790
|
-
it("
|
|
14588
|
+
it("an abandoned lazy session (never materialized) releases its id for reuse", async () => {
|
|
15791
14589
|
const fix = await makeFixture();
|
|
15792
14590
|
const { ctx, fiber } = await freshCtx(fix);
|
|
15793
14591
|
try {
|
|
15794
|
-
|
|
15795
|
-
const
|
|
15796
|
-
|
|
15797
|
-
|
|
14592
|
+
let firstSession;
|
|
14593
|
+
const firstFiber = await ctx.plugin(Object.assign((inner) => {
|
|
14594
|
+
firstSession = inner.sessions.create(SessionId("abandoned"), { meta: { cwd: WORK } });
|
|
14595
|
+
}, { inject: ["sessions"] }));
|
|
14596
|
+
await ctx.sessions.flush(firstSession);
|
|
14597
|
+
await firstFiber.dispose();
|
|
14598
|
+
let reuse;
|
|
14599
|
+
await ctx.plugin(Object.assign((inner) => {
|
|
14600
|
+
reuse = inner.sessions.create(SessionId("abandoned"), { meta: { cwd: WORK } });
|
|
14601
|
+
}, { inject: ["sessions"] }));
|
|
14602
|
+
await globalExpect(ctx.sessions.flush(reuse)).resolves.toBe(true);
|
|
14603
|
+
reuse.append("turn/start", { turn: 1 });
|
|
14604
|
+
reuse.append("turn/end", {
|
|
14605
|
+
turn: 1,
|
|
14606
|
+
reason: { kind: "completed" }
|
|
15798
14607
|
});
|
|
15799
|
-
await
|
|
14608
|
+
await ctx.sessions.flush(reuse);
|
|
14609
|
+
const loaded = await readAll(ctx, SessionId("abandoned"));
|
|
14610
|
+
globalExpect(loaded.events.map((e) => e.seq)).toEqual([0, 1]);
|
|
15800
14611
|
} finally {
|
|
15801
14612
|
await fiber.dispose();
|
|
15802
14613
|
await fix.cleanup();
|
|
@@ -15804,17 +14615,14 @@ function runCoordinatorContract(name, makeFixture) {
|
|
|
15804
14615
|
});
|
|
15805
14616
|
it("append adopts a storage-only session (fresh instance) and continues the seq", async () => {
|
|
15806
14617
|
const fix = await makeFixture();
|
|
15807
|
-
const
|
|
15808
|
-
try {
|
|
15809
|
-
const m = meta("adopt-append", WORK);
|
|
15810
|
-
await first.ctx.sessionPersistence.create(m);
|
|
15811
|
-
await first.ctx.sessionPersistence.append(m.id, oneTurnLog());
|
|
15812
|
-
} finally {
|
|
15813
|
-
await first.fiber.dispose();
|
|
15814
|
-
}
|
|
15815
|
-
const second = await freshCtx(fix);
|
|
14618
|
+
const { ctx, fiber } = await freshCtx(fix);
|
|
15816
14619
|
try {
|
|
15817
|
-
|
|
14620
|
+
const m = meta("storage-only", WORK);
|
|
14621
|
+
const handle = await ctx.sessionPersistence.create(m);
|
|
14622
|
+
await handle.append(oneTurnLog());
|
|
14623
|
+
await handle.close();
|
|
14624
|
+
const writer = await ctx.sessionPersistence.open(SessionId("storage-only"), "write");
|
|
14625
|
+
await writer.append([{
|
|
15818
14626
|
type: "turn/start",
|
|
15819
14627
|
seq: SessionSeq(6),
|
|
15820
14628
|
time: 7,
|
|
@@ -15828,7 +14636,8 @@ function runCoordinatorContract(name, makeFixture) {
|
|
|
15828
14636
|
reason: { kind: "completed" }
|
|
15829
14637
|
}
|
|
15830
14638
|
}]);
|
|
15831
|
-
|
|
14639
|
+
await writer.close();
|
|
14640
|
+
const loaded = await readAll(ctx, SessionId("storage-only"));
|
|
15832
14641
|
globalExpect(loaded.events.map((e) => e.seq)).toEqual([
|
|
15833
14642
|
0,
|
|
15834
14643
|
1,
|
|
@@ -15839,68 +14648,21 @@ function runCoordinatorContract(name, makeFixture) {
|
|
|
15839
14648
|
6,
|
|
15840
14649
|
7
|
|
15841
14650
|
]);
|
|
15842
|
-
} finally {
|
|
15843
|
-
await second.fiber.dispose();
|
|
15844
|
-
await fix.cleanup();
|
|
15845
|
-
}
|
|
15846
|
-
});
|
|
15847
|
-
it("append of an empty batch is a no-op (stays lazy)", async () => {
|
|
15848
|
-
const fix = await makeFixture();
|
|
15849
|
-
const { ctx, fiber } = await freshCtx(fix);
|
|
15850
|
-
try {
|
|
15851
|
-
const m = meta("empty-batch", WORK);
|
|
15852
|
-
await ctx.sessionPersistence.create(m);
|
|
15853
|
-
await ctx.sessionPersistence.append(m.id, []);
|
|
15854
|
-
globalExpect((await ctx.sessionPersistence.list()).map((h) => h.id)).not.toContain(m.id);
|
|
15855
|
-
} finally {
|
|
15856
|
-
await fiber.dispose();
|
|
15857
|
-
await fix.cleanup();
|
|
15858
|
-
}
|
|
15859
|
-
});
|
|
15860
|
-
it("load and inspect reject a missing session", async () => {
|
|
15861
|
-
const fix = await makeFixture();
|
|
15862
|
-
const { ctx, fiber } = await freshCtx(fix);
|
|
15863
|
-
try {
|
|
15864
|
-
await globalExpect(ctx.sessionPersistence.load(SessionId("nope"))).rejects.toThrow(/not found/);
|
|
15865
|
-
await globalExpect(ctx.sessionPersistence.inspect(SessionId("nope"))).rejects.toThrow(/not found/);
|
|
15866
14651
|
} finally {
|
|
15867
14652
|
await fiber.dispose();
|
|
15868
14653
|
await fix.cleanup();
|
|
15869
14654
|
}
|
|
15870
14655
|
});
|
|
15871
14656
|
it("create rejects a duplicate id (in memory and on a persisted log)", async () => {
|
|
15872
|
-
const fix = await makeFixture();
|
|
15873
|
-
const first = await freshCtx(fix);
|
|
15874
|
-
try {
|
|
15875
|
-
const m = meta("dup", WORK);
|
|
15876
|
-
await first.ctx.sessionPersistence.create(m);
|
|
15877
|
-
await globalExpect(first.ctx.sessionPersistence.create(m)).rejects.toThrow(/already exists in this backend/);
|
|
15878
|
-
await first.ctx.sessionPersistence.append(m.id, oneTurnLog());
|
|
15879
|
-
} finally {
|
|
15880
|
-
await first.fiber.dispose();
|
|
15881
|
-
}
|
|
15882
|
-
const second = await freshCtx(fix);
|
|
15883
|
-
try {
|
|
15884
|
-
await globalExpect(second.ctx.sessionPersistence.create(meta("dup", WORK))).rejects.toThrow(/already has a persisted log on disk/);
|
|
15885
|
-
} finally {
|
|
15886
|
-
await second.fiber.dispose();
|
|
15887
|
-
await fix.cleanup();
|
|
15888
|
-
}
|
|
15889
|
-
});
|
|
15890
|
-
it("rejects an unknown format version on load (assertVersion)", async () => {
|
|
15891
14657
|
const fix = await makeFixture();
|
|
15892
14658
|
const { ctx, fiber } = await freshCtx(fix);
|
|
15893
14659
|
try {
|
|
15894
|
-
const m =
|
|
15895
|
-
|
|
15896
|
-
|
|
15897
|
-
|
|
15898
|
-
|
|
15899
|
-
|
|
15900
|
-
};
|
|
15901
|
-
await ctx.sessionPersistence.create(m);
|
|
15902
|
-
await ctx.sessionPersistence.append(m.id, oneTurnLog());
|
|
15903
|
-
await globalExpect(ctx.sessionPersistence.load(m.id)).rejects.toThrow(/log format/);
|
|
14660
|
+
const m = meta("dup-id", WORK);
|
|
14661
|
+
const first = await ctx.sessionPersistence.create(m);
|
|
14662
|
+
await globalExpect(ctx.sessionPersistence.create(m)).rejects.toThrow(/already exists/);
|
|
14663
|
+
await first.append(oneTurnLog());
|
|
14664
|
+
await first.close();
|
|
14665
|
+
await globalExpect(ctx.sessionPersistence.create(m)).rejects.toThrow(/already exists/);
|
|
15904
14666
|
} finally {
|
|
15905
14667
|
await fiber.dispose();
|
|
15906
14668
|
await fix.cleanup();
|
|
@@ -15910,123 +14672,49 @@ function runCoordinatorContract(name, makeFixture) {
|
|
|
15910
14672
|
const fix = await makeFixture();
|
|
15911
14673
|
const { ctx, fiber } = await freshCtx(fix);
|
|
15912
14674
|
try {
|
|
15913
|
-
const m =
|
|
15914
|
-
|
|
15915
|
-
|
|
15916
|
-
|
|
15917
|
-
cwd: WORK,
|
|
15918
|
-
parentSession: SessionId("the-parent"),
|
|
15919
|
-
isSeeded: false
|
|
15920
|
-
};
|
|
15921
|
-
await ctx.sessionPersistence.create(m);
|
|
15922
|
-
await ctx.sessionPersistence.append(m.id, oneTurnLog());
|
|
15923
|
-
const loaded = await ctx.sessionPersistence.load(m.id);
|
|
15924
|
-
globalExpect(loaded.meta.parentSession).toBe("the-parent");
|
|
15925
|
-
} finally {
|
|
15926
|
-
await fiber.dispose();
|
|
15927
|
-
await fix.cleanup();
|
|
15928
|
-
}
|
|
15929
|
-
});
|
|
15930
|
-
it("flush before init resolves uses cursor 0", async () => {
|
|
15931
|
-
const fix = await makeFixture();
|
|
15932
|
-
const { ctx, fiber } = await freshCtx(fix);
|
|
15933
|
-
try {
|
|
15934
|
-
const session = ctx.sessions.create(SessionId("flush-nostate"), { meta: { cwd: WORK } });
|
|
15935
|
-
session.append("turn/start", { turn: 1 });
|
|
15936
|
-
session.append("user/message", createUserMessage({
|
|
15937
|
-
content: [{
|
|
15938
|
-
type: "text",
|
|
15939
|
-
text: "q"
|
|
15940
|
-
}],
|
|
15941
|
-
source: { kind: "user" }
|
|
15942
|
-
}), { surfaceOp: "append" });
|
|
15943
|
-
session.append("turn/end", {
|
|
15944
|
-
turn: 1,
|
|
15945
|
-
reason: { kind: "completed" }
|
|
14675
|
+
const m = meta("lineage", WORK);
|
|
14676
|
+
const handle = await ctx.sessionPersistence.create({
|
|
14677
|
+
...m,
|
|
14678
|
+
parentSession: SessionId("parent")
|
|
15946
14679
|
});
|
|
15947
|
-
await
|
|
15948
|
-
|
|
15949
|
-
|
|
14680
|
+
await handle.append(oneTurnLog());
|
|
14681
|
+
await handle.close();
|
|
14682
|
+
const loaded = await readAll(ctx, SessionId("lineage"));
|
|
14683
|
+
globalExpect(loaded.meta.parentSession).toBe(SessionId("parent"));
|
|
15950
14684
|
} finally {
|
|
15951
14685
|
await fiber.dispose();
|
|
15952
14686
|
await fix.cleanup();
|
|
15953
14687
|
}
|
|
15954
14688
|
});
|
|
15955
|
-
it("torn-tail
|
|
14689
|
+
it("torn-tail: a never-committed tail is truncated by the write path", async () => {
|
|
15956
14690
|
const fix = await makeFixture();
|
|
15957
|
-
if (
|
|
15958
|
-
|
|
15959
|
-
await fix.cleanup();
|
|
15960
|
-
return;
|
|
15961
|
-
}
|
|
15962
|
-
const first = await freshCtx(fix);
|
|
14691
|
+
if (fix.corruptTail === void 0) return;
|
|
14692
|
+
const { ctx, fiber } = await freshCtx(fix);
|
|
15963
14693
|
try {
|
|
15964
14694
|
const m = meta("torn", WORK);
|
|
15965
|
-
await
|
|
15966
|
-
await
|
|
15967
|
-
await
|
|
14695
|
+
const creator = await ctx.sessionPersistence.create(m);
|
|
14696
|
+
await creator.append(oneTurnLog());
|
|
14697
|
+
await creator.close();
|
|
14698
|
+
await fix.corruptTail(SessionId("torn"), WORK);
|
|
14699
|
+
const reader = await ctx.sessionPersistence.open(SessionId("torn"), "read");
|
|
14700
|
+
globalExpect((await reader.read()).events).toEqual(oneTurnLog());
|
|
14701
|
+
await reader.close();
|
|
14702
|
+
const writer = await ctx.sessionPersistence.open(SessionId("torn"), "write");
|
|
14703
|
+
await writer.append([{
|
|
15968
14704
|
type: "turn/start",
|
|
15969
14705
|
seq: SessionSeq(6),
|
|
15970
14706
|
time: 7,
|
|
15971
14707
|
data: { turn: 2 }
|
|
15972
14708
|
}, {
|
|
15973
|
-
type: "
|
|
14709
|
+
type: "turn/end",
|
|
15974
14710
|
seq: SessionSeq(7),
|
|
15975
14711
|
time: 8,
|
|
15976
14712
|
data: {
|
|
15977
14713
|
turn: 2,
|
|
15978
|
-
step: 1
|
|
15979
|
-
}
|
|
15980
|
-
}]);
|
|
15981
|
-
} finally {
|
|
15982
|
-
await first.fiber.dispose();
|
|
15983
|
-
}
|
|
15984
|
-
await fix.corruptTail(SessionId("torn"), WORK);
|
|
15985
|
-
const second = await freshCtx(fix);
|
|
15986
|
-
try {
|
|
15987
|
-
const loaded = await second.ctx.sessionPersistence.load(SessionId("torn"));
|
|
15988
|
-
globalExpect(loaded.events.map((e) => e.seq)).toEqual([
|
|
15989
|
-
0,
|
|
15990
|
-
1,
|
|
15991
|
-
2,
|
|
15992
|
-
3,
|
|
15993
|
-
4,
|
|
15994
|
-
5,
|
|
15995
|
-
6,
|
|
15996
|
-
7,
|
|
15997
|
-
8,
|
|
15998
|
-
9
|
|
15999
|
-
]);
|
|
16000
|
-
globalExpect(loaded.events.map((e) => e.type)).toEqual([
|
|
16001
|
-
"turn/start",
|
|
16002
|
-
"user/message",
|
|
16003
|
-
"step/start",
|
|
16004
|
-
"assistant/message",
|
|
16005
|
-
"step/end",
|
|
16006
|
-
"turn/end",
|
|
16007
|
-
"turn/start",
|
|
16008
|
-
"step/start",
|
|
16009
|
-
"step/end",
|
|
16010
|
-
"turn/end"
|
|
16011
|
-
]);
|
|
16012
|
-
const last = loaded.events.at(-1);
|
|
16013
|
-
globalExpect(last.type === "turn/end" && last.data.reason).toEqual({ kind: "interrupted" });
|
|
16014
|
-
await second.ctx.sessionPersistence.append(SessionId("torn"), [{
|
|
16015
|
-
type: "turn/start",
|
|
16016
|
-
seq: SessionSeq(10),
|
|
16017
|
-
time: 9,
|
|
16018
|
-
data: { turn: 3 }
|
|
16019
|
-
}, {
|
|
16020
|
-
type: "turn/end",
|
|
16021
|
-
seq: SessionSeq(11),
|
|
16022
|
-
time: 10,
|
|
16023
|
-
data: {
|
|
16024
|
-
turn: 3,
|
|
16025
14714
|
reason: { kind: "completed" }
|
|
16026
14715
|
}
|
|
16027
14716
|
}]);
|
|
16028
|
-
|
|
16029
|
-
globalExpect(reloaded.events.map((e) => e.seq)).toEqual([
|
|
14717
|
+
globalExpect((await writer.read()).events.map((e) => e.seq)).toEqual([
|
|
16030
14718
|
0,
|
|
16031
14719
|
1,
|
|
16032
14720
|
2,
|
|
@@ -16034,14 +14722,11 @@ function runCoordinatorContract(name, makeFixture) {
|
|
|
16034
14722
|
4,
|
|
16035
14723
|
5,
|
|
16036
14724
|
6,
|
|
16037
|
-
7
|
|
16038
|
-
8,
|
|
16039
|
-
9,
|
|
16040
|
-
10,
|
|
16041
|
-
11
|
|
14725
|
+
7
|
|
16042
14726
|
]);
|
|
14727
|
+
await writer.close();
|
|
16043
14728
|
} finally {
|
|
16044
|
-
await
|
|
14729
|
+
await fiber.dispose();
|
|
16045
14730
|
await fix.cleanup();
|
|
16046
14731
|
}
|
|
16047
14732
|
});
|