harness: a new PM run reads current launch state and open tasks before each turn, and old messages show their age, row 109 (#1603)

The broker gains a read-only state verb (launch state with the event that set it, open tasks capped). The runner fetches it before each turn and puts a "Current state" block at the top of the request; a failed read fails the turn. turnRequest() adds the message time. roles/pm.md gets one rule: the block is the truth for launch state and task status. Seat session, seat sandbox and cli launcher tests re-anchored to the new request shape.

Built by Researcher, packet 875c515e9. Filbert approved round 3, comment 28333, candidate manifest 0a62225e (11 paths). Sage gate on 9774d70db plus row109.patch: every package node suite and every scripts/test-*.sh green on the first run, skipped 0 in bus, harness and seat. Vikunja #259.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-13 19:12:54 -05:00
co-authored by Claude Opus 5.5
parent 848ceb68a4
commit badfa52b9d
11 changed files with 299 additions and 11 deletions
+37 -2
View File
@@ -62,7 +62,10 @@ export function founderCheck(policy, env) {
// The request text one message becomes.
export function turnRequest(m) {
const head = [`Message ${m.id} from ${m.from_role}`, `class ${m.class}`];
const head = [`Message ${m.id} from ${m.from_role}`];
// Row 109 (#1603): when the sender sent it, so a message that waited reads as old.
if (m.at) head.push(`sent ${m.at}`);
head.push(`class ${m.class}`);
if (m.in_reply_to) head.push(`in reply to ${m.in_reply_to}`);
if (m.decision) head.push(`citing decision ${m.decision}`);
// Row 90 (#1584): the human input id task_create cites as its request.
@@ -70,6 +73,22 @@ export function turnRequest(m) {
return `${head.join(", ")}:\n\n${m.body}`;
}
// Row 109 (#1603): the block at the top of a turn's request. It is what the broker said at `readAt`,
// the truth for launch state and task status over any older message.
export function stateBlock(state, readAt) {
const { launch, tasks } = state;
const since = launch.at ? ` since ${launch.at} (event ${launch.event})` : " (never switched)";
const shown = tasks.open.length < tasks.total ? `, the first ${tasks.open.length} of cap ${tasks.cap}` : "";
const open = tasks.open.map((t) => `- ${t.task_ref} ${t.title ?? ""}`.trimEnd());
return [
`Current state, read at ${readAt}:`,
`Launches: ${launch.state}${since}.`,
`Open tasks: ${tasks.total}${shown}.`,
...open,
"",
].join("\n");
}
function clip(text) {
return text.length > BODY_MAX ? text.slice(0, BODY_MAX) + "\n[answer cut at 32000 characters]" : text;
}
@@ -339,7 +358,23 @@ export async function main(runDir, { stdin = process.stdin, env = process.env, l
}
for (const m of messages) {
if (stopping) break loop;
const turn = await runTurn({ session, request: turnRequest(m), n: ++n, env: turnEnv, onChild: (c) => (child = c) });
// Row 109 (#1603): read the state now, not when the message was sent. A failed read fails the turn.
let block = null;
let unread = null;
try {
block = stateBlock(await client.call("state"), new Date().toISOString());
} catch (e) {
if (SESSION_GONE.has(e.code)) {
log(`runner: session stopped working: ${e.code}`);
code = EXIT.session;
break loop;
}
unread = `could not read the current state: ${e.code ?? e.message}`;
}
++n;
const turn = unread
? { ok: false, reason: unread }
: await runTurn({ session, request: `${block}\n${turnRequest(m)}`, n, env: turnEnv, onChild: (c) => (child = c) });
if (stopping) break loop;
if (!turn.ok) log(`runner: turn ${n} for message ${m.id} failed: ${turn.reason}`);
try {