Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/server/src/chat/remote-planner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,9 @@ test("saving a decision the Planner left open starts one turn on the attached Pl
question: "How should the wrong sentence be fixed?",
}, { revision: context.plan.revision });
let turns = answerTurns({
store: room.storage.decisionAnswers,
// Sent when the test asks, as when a Planner attaches.
window: { quietMs: 60_000, longestMs: 60_000 },
start: async (_channelId, compose) => {
let instruction = compose(context.plan);
if (!instruction) return false;
Expand Down Expand Up @@ -440,6 +443,8 @@ test("saving a decision the Planner left open starts one turn on the attached Pl

await save();
await Promise.all(handing);
await turns.pending(context.room);
turns.close();
await context.chat.running;

expect(script.turns.map(turn => turn.prompt)).toEqual([
Expand Down
6 changes: 5 additions & 1 deletion apps/server/src/chat/server-instruction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export type ServerInstructionDeps = {
};

/**
* Start a Planner turn nobody typed, and say whether Chat accepted it (started or queued).
* Start a Planner turn nobody typed, and say whether Chat accepted it (started or queued), or
* already showed the notice of the turn its stable `about.notice` id names.
* It runs only under the document's existing Planner owner and never claims one: nobody asked
* for this turn, so nobody's credentials are taken for it. A room evicted since is reopened.
*/
Expand Down Expand Up @@ -54,6 +55,9 @@ export async function serverInstruction(
let instruction = compose(current);
if (!instruction) return false;
let context = deps.conversation(held.room, undefined, repository);
// Its notice already shows: it started before a restart, and is not started again.
let notice = instruction.about?.notice;
if (notice && context.chat.entries.some(entry => entry.id === notice)) return true;
let before = context.chat.running;
let waiting = context.chat.waiting.length;
await deps.instruct(context, instruction);
Expand Down
12 changes: 9 additions & 3 deletions apps/server/src/chat/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ export type Instruction = {
threadChannel?: string;
/** Kept out of the visible queue: its notice says why, and its text is for the Planner alone. */
quiet?: boolean;
/**
* The transcript notice's id, stable across restarts: a turn whose notice Chat already shows
* was started before and is not started again.
*/
notice?: string;
};

/**
Expand Down Expand Up @@ -1028,10 +1033,11 @@ function instructionNotice(
context: Room,
text: string,
comment?: Instruction["comment"],
id = ulid(),
): void | Promise<void> {
let { chat, room, server } = context;
let entry: Wire.Entry = {
id: ulid(),
id,
author: { kind: "system" },
text,
ts: now(),
Expand Down Expand Up @@ -1097,7 +1103,7 @@ export function instruct(
});
return report(about, { status: "failed" });
}
let { comment: _comment, ...rest } = about;
let { comment: _comment, notice: _notice, ...rest } = about;
chat.waiting.push({
id: ulid(),
handle,
Expand All @@ -1113,7 +1119,7 @@ export function instruct(
if (about.spent?.() || about.signal?.aborted) return report(about, { status: "skipped" });
// Automatic participation replies like any member would, so it announces nothing.
if (!said) return proceed();
let announced = instructionNotice(context, said, about.comment);
let announced = instructionNotice(context, said, about.comment, about.notice);
return announced instanceof Promise ? announced.then(proceed) : proceed();
}

Expand Down
18 changes: 14 additions & 4 deletions apps/server/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,23 +375,31 @@ const spikeSettlement = spikeSettler({
});

/**
* Hand a saved answer to a decision the Planner left open to the Planner, as a turn it acts on.
* Like other server-authored turns it runs only under the existing owner, and under the remote
* harness only while a Planner is attached; one that found no Planner starts once one is.
* Hand saved answers to decisions the Planner left open to the Planner, as one turn it acts on
* for the answers saved close together. Like other server-authored turns it runs only under the
* existing owner, and under the remote harness only while a Planner is attached; answers that
* found no Planner wait in storage and start once one is, after a restart too.
*/
const decisionAnswers = answerTurns({
store: storage.decisionAnswers,
start: async (channelId, compose) => {
if (!config.agent) return "drop";
let channel = await storage.channels.get(channelId);
if (!channel || channel.parentChannelId) return "drop";
if (plannerAttachments && !await plannerAttachments.confirmed(channelId)) return false;
return serverInstruction(channelId, compose);
},
// Answers saved while a Planner turn runs or waits join the turn after it.
busy: channelId => {
let chat = Rooms.get(channelId)?.plan?.chat;
if (!chat || chat.closed || (!chat.busy && chat.waiting.length === 0)) return undefined;
return chat.running ?? Bun.sleep(1_000);
},
});

function answerDecision(channelId: string, id: string, generation: number): void {
void decisionAnswers.answered(channelId, id, generation).catch(err =>
console.error("chopin: could not hand a saved decision to the Planner -", err)
console.error("chopin: could not keep a saved decision for the Planner -", err)
);
}

Expand Down Expand Up @@ -1879,6 +1887,8 @@ function drain(): Promise<void> {
liveSync?.close();
buildReadiness?.close();
spikeScout?.close();
// Answers still collecting stay stored; the next process delivers them.
decisionAnswers.close();
let stoppingJobs = jobRunner?.shutdown();
ownerBindings?.revokeAll();
plannerAttachments?.close();
Expand Down
Loading
Loading