mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(agent): require configured lanes for branch forks
Reject data-only source branches, preserve tree-scope support for them, and update fork fixtures to provide complete lane configuration and state.
This commit is contained in:
parent
f2eae920c6
commit
1081eb2126
7 changed files with 79 additions and 22 deletions
|
|
@ -151,6 +151,9 @@ function validateForkSourceSnapshot(
|
|||
if ((configuration === undefined) !== (state === undefined)) {
|
||||
throw new Error(`Source session branch ${JSON.stringify(tip.address.key)} has incomplete lane state`);
|
||||
}
|
||||
if (options.scope === "branch" && tip.address.key === options.branch && configuration === undefined) {
|
||||
throw new Error(`Source branch ${JSON.stringify(options.branch)} is not a configured AgentLane`);
|
||||
}
|
||||
if (
|
||||
(source.entriesComplete !== false || options.scope === "tree") &&
|
||||
tip.value !== null &&
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { BACKGROUND_CONTEXT } from "../../../context.ts";
|
||||
import type { SessionMetadata, SessionRepo } from "../../types.ts";
|
||||
import { branchTip, setValue } from "../../values.ts";
|
||||
import { branchTip, laneConfig, laneState, setValue } from "../../values.ts";
|
||||
import { STORAGE_BENCHMARK_DATASETS, type StorageBenchmarkDataset } from "./datasets.ts";
|
||||
import { generateStorageBenchmarkSeedTransactions } from "./storage.ts";
|
||||
|
||||
|
|
@ -142,7 +142,19 @@ export async function seedSessionRepoForkBenchmark(
|
|||
await session.mutate((mutator) => mutator.commit(transaction, BACKGROUND_CONTEXT), BACKGROUND_CONTEXT);
|
||||
}
|
||||
await session.mutate(
|
||||
(mutator) => mutator.commit([setValue(branchTip("main"), dataset.tipId)], BACKGROUND_CONTEXT),
|
||||
(mutator) =>
|
||||
mutator.commit(
|
||||
[
|
||||
setValue(branchTip("main"), dataset.tipId),
|
||||
setValue(laneConfig("main"), {
|
||||
model: { provider: "benchmark", modelId: "benchmark" },
|
||||
thinkingLevel: "off",
|
||||
activeToolNames: [],
|
||||
}),
|
||||
setValue(laneState("main"), { currentOperationId: null, lastOperationId: null, inbox: [] }),
|
||||
],
|
||||
BACKGROUND_CONTEXT,
|
||||
),
|
||||
BACKGROUND_CONTEXT,
|
||||
);
|
||||
return session.metadata;
|
||||
|
|
|
|||
|
|
@ -334,6 +334,21 @@ export function createSessionRepoForkBehaviorConformance<TMetadata extends Sessi
|
|||
});
|
||||
await Promise.all([source.close(BACKGROUND_CONTEXT), fork.close(BACKGROUND_CONTEXT)]);
|
||||
}),
|
||||
createCase(factory, "forks", "rejects a data-only branch and releases its destination id", async ({ repo }) => {
|
||||
const source = await repo.create({ id: "source" }, BACKGROUND_CONTEXT);
|
||||
await source.createBranch("data", null, BACKGROUND_CONTEXT);
|
||||
|
||||
await rejects(
|
||||
repo.fork(source.metadata, { id: "destination", scope: "branch", branch: "data" }, BACKGROUND_CONTEXT),
|
||||
);
|
||||
deepStrictEqual(
|
||||
(await repo.list(undefined, BACKGROUND_CONTEXT)).map(({ id }) => id),
|
||||
["source"],
|
||||
);
|
||||
|
||||
const destination = await repo.create({ id: "destination" }, BACKGROUND_CONTEXT);
|
||||
await Promise.all([source.close(BACKGROUND_CONTEXT), destination.close(BACKGROUND_CONTEXT)]);
|
||||
}),
|
||||
createCase(
|
||||
factory,
|
||||
"forks",
|
||||
|
|
@ -571,6 +586,8 @@ export function createSessionRepoForkBehaviorConformance<TMetadata extends Sessi
|
|||
customType: "root",
|
||||
}),
|
||||
setValue(branchTip("main"), ROOT_ID),
|
||||
setValue(laneConfig("main"), configuration),
|
||||
setValue(laneState("main"), idleLaneState),
|
||||
setValue(applicationValue, "excluded"),
|
||||
appendList(applicationList, "excluded"),
|
||||
],
|
||||
|
|
@ -586,8 +603,8 @@ export function createSessionRepoForkBehaviorConformance<TMetadata extends Sessi
|
|||
BACKGROUND_CONTEXT,
|
||||
);
|
||||
strictEqual(await getBranchTip(fork), ROOT_ID);
|
||||
strictEqual(await fork.getValue(laneConfig("main"), BACKGROUND_CONTEXT), undefined);
|
||||
strictEqual(await fork.getValue(laneState("main"), BACKGROUND_CONTEXT), undefined);
|
||||
deepStrictEqual((await fork.getValue(laneConfig("main"), BACKGROUND_CONTEXT))?.value, configuration);
|
||||
deepStrictEqual((await fork.getValue(laneState("main"), BACKGROUND_CONTEXT))?.value, idleLaneState);
|
||||
strictEqual(await fork.getValue(applicationValue, BACKGROUND_CONTEXT), undefined);
|
||||
deepStrictEqual(await fork.readList(applicationList, undefined, BACKGROUND_CONTEXT), []);
|
||||
await fork.close(BACKGROUND_CONTEXT);
|
||||
|
|
@ -713,6 +730,8 @@ export function createSessionRepoForkSourceSnapshotConformance<TMetadata extends
|
|||
customType: "first",
|
||||
}),
|
||||
setValue(branchTip("main"), ROOT_ID),
|
||||
setValue(laneConfig("main"), configuration),
|
||||
setValue(laneState("main"), idleLaneState),
|
||||
setValue(sessionName, "first name"),
|
||||
setValue(entryLabel(ROOT_ID), "first label"),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -560,8 +560,12 @@ export interface SessionCreateOptions {
|
|||
|
||||
export type ForkOptions =
|
||||
| {
|
||||
/**
|
||||
* Copy one path from a complete configured source AgentLane under the same
|
||||
* Branch name, with copied configuration and fresh idle lane state.
|
||||
*/
|
||||
scope: "branch";
|
||||
/** Source Branch to copy under the same name in the destination. */
|
||||
/** Source Branch to copy. */
|
||||
branch: string;
|
||||
/** Entry on the source Branch's current tip ancestry. Defaults to the current tip. */
|
||||
entryId?: string;
|
||||
|
|
|
|||
|
|
@ -276,11 +276,7 @@ describe("JSONL v3 migration", () => {
|
|||
it("forks a closed source into a complete v4 destination without rewriting it", async () => {
|
||||
const { path, content, metadata } = await writeForkFixture();
|
||||
|
||||
const fork = await repo.fork(
|
||||
metadata,
|
||||
{ id: "closed-fork", scope: "branch", branch: "main" },
|
||||
BACKGROUND_CONTEXT,
|
||||
);
|
||||
const fork = await repo.fork(metadata, { id: "closed-fork", scope: "tree" }, BACKGROUND_CONTEXT);
|
||||
|
||||
expect(getOrThrow(await fileSystem.readTextFile(path, BACKGROUND_CONTEXT))).toBe(content);
|
||||
expect(fork.metadata.id).toBe("closed-fork");
|
||||
|
|
@ -313,11 +309,7 @@ describe("JSONL v3 migration", () => {
|
|||
const sourceEntries = await source.findEntries({ order: "asc" }, BACKGROUND_CONTEXT);
|
||||
const sourceStats = await source.getStats(BACKGROUND_CONTEXT);
|
||||
|
||||
const fork = await repo.fork(
|
||||
metadata,
|
||||
{ id: "open-fork", scope: "branch", branch: "main" },
|
||||
BACKGROUND_CONTEXT,
|
||||
);
|
||||
const fork = await repo.fork(metadata, { id: "open-fork", scope: "tree" }, BACKGROUND_CONTEXT);
|
||||
const forkEntries = await expectForkedState(fork);
|
||||
|
||||
expect(fork.metadata.id).toBe("open-fork");
|
||||
|
|
|
|||
|
|
@ -98,6 +98,16 @@ describe("MemorySessionRepo metadata", () => {
|
|||
customType: "child",
|
||||
}),
|
||||
storedValues.setValue(storedValues.branchTip("main"), childId),
|
||||
storedValues.setValue(storedValues.laneConfig("main"), {
|
||||
model: { provider: "test", modelId: "test" },
|
||||
thinkingLevel: "off",
|
||||
activeToolNames: [],
|
||||
}),
|
||||
storedValues.setValue(storedValues.laneState("main"), {
|
||||
currentOperationId: null,
|
||||
lastOperationId: null,
|
||||
inbox: [],
|
||||
}),
|
||||
],
|
||||
BACKGROUND_CONTEXT,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,17 @@ import { describe, expect, it } from "vitest";
|
|||
import type { SqliteDatabase, SqliteDatabaseFactory, SqliteStatement } from "../src/index.ts";
|
||||
import { createNodeSqliteFactory, SqliteSessionRepo, sql } from "../src/index.ts";
|
||||
|
||||
const TEST_LANE_CONFIGURATION = {
|
||||
model: { provider: "test", modelId: "test" },
|
||||
thinkingLevel: "off",
|
||||
activeToolNames: [],
|
||||
} satisfies storedValues.LaneConfiguration;
|
||||
const IDLE_LANE_STATE = {
|
||||
currentOperationId: null,
|
||||
lastOperationId: null,
|
||||
inbox: [],
|
||||
} satisfies storedValues.LaneState;
|
||||
|
||||
async function withTempDir<T>(run: (directory: string) => Promise<T>): Promise<T> {
|
||||
const directory = await mkdtemp(join(tmpdir(), "pi-sqlite-session-"));
|
||||
try {
|
||||
|
|
@ -202,23 +213,23 @@ function commitLaterSourceState(db: SqliteDatabase): void {
|
|||
const label = storedValues.entryLabel("root");
|
||||
db.transaction(() => {
|
||||
sql`INSERT INTO entries (session_id, id, parent_id, seq, type, custom_type, timestamp, payload)
|
||||
VALUES (${"source"}, ${"child"}, ${"root"}, ${5}, ${"message"}, ${null}, ${2}, ${JSON.stringify({ message: { role: "user", content: "after", timestamp: 2 } })})`.run(
|
||||
VALUES (${"source"}, ${"child"}, ${"root"}, ${7}, ${"message"}, ${null}, ${2}, ${JSON.stringify({ message: { role: "user", content: "after", timestamp: 2 } })})`.run(
|
||||
db,
|
||||
);
|
||||
sql`INSERT INTO branch_entries (session_id, branch_id, entry_id, entry_seq, entry_type)
|
||||
VALUES (${"source"}, ${"root"}, ${"child"}, ${5}, ${"message"})`.run(db);
|
||||
sql`UPDATE branch_meta SET tip_entry_id = ${"child"}, tip_seq = ${5}
|
||||
VALUES (${"source"}, ${"root"}, ${"child"}, ${7}, ${"message"})`.run(db);
|
||||
sql`UPDATE branch_meta SET tip_entry_id = ${"child"}, tip_seq = ${7}
|
||||
WHERE session_id = ${"source"} AND branch_id = ${"root"}`.run(db);
|
||||
for (const [namespace, key, seq, value] of [
|
||||
[tip.namespace, tip.key, 6, "child"],
|
||||
[name.namespace, name.key, 7, "after"],
|
||||
[label.namespace, label.key, 8, "after-label"],
|
||||
[tip.namespace, tip.key, 8, "child"],
|
||||
[name.namespace, name.key, 9, "after"],
|
||||
[label.namespace, label.key, 10, "after-label"],
|
||||
] as const) {
|
||||
sql`INSERT INTO scalar_values (session_id, namespace, key, seq, value)
|
||||
VALUES (${"source"}, ${namespace}, ${key}, ${seq}, ${JSON.stringify(value)})
|
||||
ON CONFLICT(session_id, namespace, key) DO UPDATE SET seq = excluded.seq, value = excluded.value`.run(db);
|
||||
}
|
||||
sql`UPDATE sessions SET message_count = ${1}, next_seq = ${9} WHERE id = ${"source"}`.run(db);
|
||||
sql`UPDATE sessions SET message_count = ${1}, next_seq = ${11} WHERE id = ${"source"}`.run(db);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -558,6 +569,8 @@ describe("SqliteSessionRepo", () => {
|
|||
message: { role: "user", content: "child", timestamp: 1 },
|
||||
}),
|
||||
storedValues.setValue(storedValues.branchTip("main"), "child"),
|
||||
storedValues.setValue(storedValues.laneConfig("main"), TEST_LANE_CONFIGURATION),
|
||||
storedValues.setValue(storedValues.laneState("main"), IDLE_LANE_STATE),
|
||||
sessionWrites.insertUsage({
|
||||
id: "usage",
|
||||
adjustment: false,
|
||||
|
|
@ -756,6 +769,8 @@ describe("SqliteSessionRepo", () => {
|
|||
[
|
||||
sessionWrites.insertEntry({ id: "left-root", parentId: null, type: "custom", customType: "left" }),
|
||||
storedValues.setValue(storedValues.branchTip("main"), "left-root"),
|
||||
storedValues.setValue(storedValues.laneConfig("main"), TEST_LANE_CONFIGURATION),
|
||||
storedValues.setValue(storedValues.laneState("main"), IDLE_LANE_STATE),
|
||||
storedValues.setValue(storedValues.sessionName, "left"),
|
||||
],
|
||||
BACKGROUND_CONTEXT,
|
||||
|
|
@ -813,6 +828,8 @@ describe("SqliteSessionRepo", () => {
|
|||
[
|
||||
sessionWrites.insertEntry({ id: "root", parentId: null, type: "custom", customType: "root" }),
|
||||
storedValues.setValue(storedValues.branchTip("main"), "root"),
|
||||
storedValues.setValue(storedValues.laneConfig("main"), TEST_LANE_CONFIGURATION),
|
||||
storedValues.setValue(storedValues.laneState("main"), IDLE_LANE_STATE),
|
||||
storedValues.setValue(storedValues.sessionName, "before"),
|
||||
storedValues.setValue(storedValues.entryLabel("root"), "before-label"),
|
||||
],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue