fix(p2): a report's origin scans past mid-run error answers and compaction markers

This commit is contained in:
Mario Zechner 2026-11-05 07:38:16 +00:00
parent fe68bf23e8
commit 5be85f3be3
3 changed files with 18 additions and 5 deletions

View file

@ -1073,7 +1073,7 @@ translated phone drawer and intercept chat taps.
by its author in the entry data, an agent-written one by its `origin`. Agent-written inputs record the writing by its author in the entry data, an agent-written one by its `origin`. Agent-written inputs record the writing
turn's origin: subagent tasks and one-shot agents' prompts (`p2.subagent-task`), `team` messages and reports turn's origin: subagent tasks and one-shot agents' prompts (`p2.subagent-task`), `team` messages and reports
(`p2.agent-message`; a report has the origin of the subagent's run that answered, `answerOrigin`: its inputs after (`p2.agent-message`; a report has the origin of the subagent's run that answered, `answerOrigin`: its inputs after
the previous run's last answer), and peer messages (`p2.peer-message`). A guest's input steered into a run takes the the previous run's final answer, `stop` or `length`, past mid-run error answers and compaction markers), and peer messages (`p2.peer-message`). A guest's input steered into a run takes the
owner's tools away for the rest of it; a guest-started chain never reaches them. Known gap: inputs of earlier runs owner's tools away for the rest of it; a guest-started chain never reaches them. Known gap: inputs of earlier runs
still in the context are not considered. `test/p2/inputs.test.ts`, `dev-tools.test.ts`, `tool-access.test.ts`. still in the context are not considered. `test/p2/inputs.test.ts`, `dev-tools.test.ts`, `tool-access.test.ts`.
- **Code mode** (`plugins/codemode`): the worker facet registers Durable's `codemode` extension - **Code mode** (`plugins/codemode`): the worker facet registers Durable's `codemode` extension

View file

@ -48,7 +48,11 @@ export function entryOrigin(entry: EntryRecord | undefined): ConversationOrigin
/** /**
* The origin of the run that ended with the answer `answer` of `conversationId`, read after it ended (a subagent's * The origin of the run that ended with the answer `answer` of `conversationId`, read after it ended (a subagent's
* report): its inputs are the input entries after the previous run's last answer. The owner's only if all are. * report, when `pi.live.run` is gone): its inputs are the input entries after the previous run's final answer. The
* owner's only if all are. Only a final answer (`stop`, `length`) bounds the scan: a retried model error appends an
* `error` answer and an overflow compaction a head marker inside a run, and scanning past a failed or aborted run only
* adds inputs, so the result can only be stricter. (An `onYield` continuation after a final answer would hide the
* inputs before it; p2 installs no such hook.)
*/ */
export async function answerOrigin( export async function answerOrigin(
tx: Tx, tx: Tx,
@ -61,9 +65,9 @@ export async function answerOrigin(
const page = await tx.scanEntries({ conversationId, maxEntryId: (answer - 1) as EntryId }, 100, cursor); const page = await tx.scanEntries({ conversationId, maxEntryId: (answer - 1) as EntryId }, 100, cursor);
for (const entry of page.items) { for (const entry of page.items) {
const message = entry.model?.[0]; const message = entry.model?.[0];
// The previous run ended at its last answer; a reset or compaction marker starts the context afresh. if (message?.role === "assistant" && (message.stopReason === "stop" || message.stopReason === "length"))
if (message?.role === "assistant" && message.stopReason !== "toolUse") return found ? "owner" : undefined; return found ? "owner" : undefined;
if (entry.head !== undefined) return found ? "owner" : undefined; // Durable's own entries with user messages (compaction summaries, handoffs) are not inputs.
if (message?.role !== "user" || (entry.kind.startsWith("pi.") && entry.kind !== "pi.user")) continue; if (message?.role !== "user" || (entry.kind.startsWith("pi.") && entry.kind !== "pi.user")) continue;
if (entryOrigin(entry) !== "owner") return undefined; if (entryOrigin(entry) !== "owner") return undefined;
found = true; found = true;

View file

@ -163,5 +163,14 @@ describe("structured inputs", () => {
// The guest's input before the previous answer belongs to the previous run. // The guest's input before the previous answer belongs to the previous run.
assert.equal(await answerOrigin(tx([]), 1 as ConversationId, 6 as EntryId), "owner"); assert.equal(await answerOrigin(tx([]), 1 as ConversationId, 6 as EntryId), "owner");
assert.equal(await answerOrigin(tx([]), 1 as ConversationId, 3 as EntryId), undefined, "no input of its own"); assert.equal(await answerOrigin(tx([]), 1 as ConversationId, 3 as EntryId), undefined, "no input of its own");
// A retried model error and an overflow compaction's marker are inside the run: the guest's input before them
// counts.
const midRun = [
entry({ id: 6, kind: "p2.input", data: { author: ALICE, text: "x", attachments: [] }, model: user("x") }),
entry({ id: 7, kind: "pi.assistant", model: [{ role: "assistant", stopReason: "error", content: [] }] }),
entry({ id: 8, kind: "pi.compaction", head: 8, model: user("summary") }),
entry({ id: 9, kind: "pi.assistant", model: [{ role: "assistant", stopReason: "aborted", content: [] }] }),
];
assert.equal(await answerOrigin(tx(midRun), 1 as ConversationId, 10 as EntryId), undefined);
}); });
}); });