mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(agent): define rootless v3 label handling
Document and test that v3 labels are skipped when their target has no retained ancestor.
This commit is contained in:
parent
e92294d443
commit
56cca20ea0
3 changed files with 46 additions and 3 deletions
|
|
@ -3757,7 +3757,7 @@ One corruption assertion constructs an `aborted` response with running control d
|
|||
"v3" in this appendix names the legacy coding-agent JSONL session format, not this document. Old coding-agent v3 JSONL files must open unchanged and restore idle. Normalization on load:
|
||||
|
||||
- `custom_message` becomes a custom agent message.
|
||||
- `label` and `session_info` become session-name/entry-label values (latest by file position wins) and leave the tree. A label targets its nearest retained parent.
|
||||
- `label` and `session_info` become session-name/entry-label values (latest by file position wins) and leave the tree. A label target resolves through discarded nodes to its nearest retained ancestor; if no retained ancestor exists and resolution produces `null`, the label is skipped.
|
||||
- Legacy `model_change`, `thinking_level_change`, and `active_tools_change` nodes disappear. They do **not** initialize or alter `LaneConfiguration`; a normalized `main` uses the immutable options seed.
|
||||
- Each retained child of a discarded node is reparented to its nearest retained ancestor.
|
||||
- `main`'s leaf is the final physical node resolved through discarded nodes to its nearest retained ancestor.
|
||||
|
|
@ -3766,7 +3766,7 @@ One corruption assertion constructs an `aborted` response with running control d
|
|||
- v3 ISO timestamps convert to Unix milliseconds.
|
||||
- A v3 `parentSession` path resolves to an available parent header id; otherwise metadata and first-write conversion preserve it as `legacyParentSessionPath`.
|
||||
- On first format-4 write, append one aggregate adjustment usage row with `details: { source: "v3-import" }`, summing v3 node usage so ledger-derived totals remain unchanged.
|
||||
- Legacy v3 ids are re-minted at import: each entry gets a UUIDv7 whose prefix is the legacy entry's own timestamp (random tail for uniqueness), preserving time order and §1.2's every-id-is-time-prefixed property. All references the format knows are remapped — parent chains, `main`'s leaf, label keys, non-null `fromId`, usage `entryId`. Ids embedded in opaque payloads (custom entry data, `details`, message text) are not rewritten; the opaque-payload contract (§1.2) already covers them.
|
||||
- Legacy v3 ids are re-minted at import: each entry gets a UUIDv7 whose prefix is the legacy entry's own timestamp (random tail for uniqueness), preserving time order and §1.2's every-id-is-time-prefixed property. All references the format knows are remapped — parent chains, `main`'s leaf, surviving label keys, non-null `fromId`, usage `entryId`. Ids embedded in opaque payloads (custom entry data, `details`, message text) are not rewritten; the opaque-payload contract (§1.2) already covers them.
|
||||
|
||||
Read-only open leaves the file unchanged and computes stats from normalized entry snapshots. The first format-4 write persists normalization through a temporary file and atomic rename over the original path, including the aggregate adjustment so subsequent stats are ledger-derived, and stamps the current `storageVersion` (§7.3). A fork from an unconfigured read-only v3 session follows §2.7 and leaves destination `main` for first harness attachment to seed.
|
||||
|
||||
|
|
|
|||
|
|
@ -380,7 +380,7 @@ function normalizeLegacyV3Values(
|
|||
for (const entry of entries) {
|
||||
if (entry.type !== "label") continue;
|
||||
const targetId = resolver.resolve(entry.targetId);
|
||||
// TODO: Decide whether labels whose targets normalize to the root should import as root labels.
|
||||
// Labels have no current address when their target has no retained ancestor.
|
||||
if (targetId === null) continue;
|
||||
// Legacy v3 treated both undefined and the empty string as clearing a label.
|
||||
if (entry.label) labels.set(targetId, entry.label);
|
||||
|
|
|
|||
|
|
@ -333,6 +333,49 @@ describe("JSONL v3 migration", () => {
|
|||
await session.close(BACKGROUND_CONTEXT);
|
||||
});
|
||||
|
||||
it("skips a label whose target has no retained ancestor", async () => {
|
||||
const message = {
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "retained message" }],
|
||||
timestamp: NOW + 3_000,
|
||||
} satisfies AgentMessage;
|
||||
await writeLegacyV3Fixture([
|
||||
{
|
||||
type: "session_info",
|
||||
id: "root-session-info",
|
||||
parentId: null,
|
||||
timestamp: new Date(NOW + 1_000).toISOString(),
|
||||
},
|
||||
{
|
||||
type: "label",
|
||||
id: "root-label",
|
||||
parentId: "root-session-info",
|
||||
timestamp: new Date(NOW + 2_000).toISOString(),
|
||||
targetId: "root-session-info",
|
||||
label: "Skipped label",
|
||||
},
|
||||
{
|
||||
type: "message",
|
||||
id: "message-1",
|
||||
parentId: "root-label",
|
||||
timestamp: new Date(NOW + 3_000).toISOString(),
|
||||
message,
|
||||
},
|
||||
]);
|
||||
const [metadata] = await repo.list({ cwd: "/workspace" }, BACKGROUND_CONTEXT);
|
||||
if (metadata === undefined) throw new Error("Legacy fixture was not discovered");
|
||||
|
||||
const session = await repo.open(metadata, BACKGROUND_CONTEXT);
|
||||
const entries = await session.findEntries({ order: "asc" }, BACKGROUND_CONTEXT);
|
||||
expect(entries).toHaveLength(1);
|
||||
const [entry] = entries;
|
||||
if (entry === undefined) throw new Error("Legacy message was not imported");
|
||||
expect(entry.parentId).toBeNull();
|
||||
expect(await session.getLabel(entry.id, BACKGROUND_CONTEXT)).toBeUndefined();
|
||||
expect(await session.getLeafId(BACKGROUND_CONTEXT)).toBe(entry.id);
|
||||
await session.close(BACKGROUND_CONTEXT);
|
||||
});
|
||||
|
||||
it("uses the latest label after remapping discarded targets", async () => {
|
||||
const firstMessage = {
|
||||
role: "user",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue