mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
fix(cli): rig acp serve puts no question to the person after a cancel
- After a cancel, the turn's last pass still reads open questions but shows none of them, even one that landed while that read was in flight. Before, the pass the cancel woke could put a new question on screen after the person had pressed stop. - The guide's Limits now say sessions are found among the 2,000 newest rows the caller sent, so a caller that sends a lot can't list or reopen its oldest sessions. - A choice made in the app is recorded as a click in Slack would be. The guide and the code comment no longer say "or the TUI": the TUI records its clicks as the person at the terminal, and this records the caller's address.
This commit is contained in:
1 parent
38d11a553b
commit
1f0e37be5b
4 files changed
+29
-4
No files matched your search
@@ -205,8 +205,7 @@ app that starts the agent from the ACP registry passes no arguments, so that's h
|
||||
- **A question the team asks you** while a turn is open comes to the app as a permission request, with the ask's own
|
||||
choices, when its row is tagged for that chat (`acp-session:<id>`; the task's row tells the team how). An untagged
|
||||
question from a seat the work reached is named in the chat with where to answer it, because it could be another
|
||||
chat's. Your choice is recorded as your answer, exactly as a click in Slack or the TUI would be, and the seat that
|
||||
asked gets it. Dismissing it, or cancelling the turn, records nothing: the question stays open in the TUI and Slack.
|
||||
chat's. Your choice is recorded as your answer, as a click in Slack would be, and the seat that asked gets it. Dismissing it, or cancelling the turn, records nothing: the question stays open in the TUI and Slack.
|
||||
A question that wants an answer in words is shown with where to answer it.
|
||||
- **Sessions are kept in the queue:** the app can list your earlier sessions (`session/list`) and reopen one
|
||||
(`session/load`). Reopening replays each prompt, its work and the reply as they stand now, and a new prompt continues it.
|
||||
@@ -233,3 +232,5 @@ Other apps take the same command and arguments wherever they configure a custom
|
||||
- Questions show only while a turn is open; one asked while no turn is open waits in your usual places (the TUI, Slack).
|
||||
- Prompts are text. Links and embedded text files are passed on as written; images and audio aren't accepted.
|
||||
- Progress is read every 2 seconds while a turn is open (`--poll-seconds`).
|
||||
- Sessions are found among the 2,000 newest rows you've sent, so a caller that sends a lot, such as a busy seat, can't
|
||||
list or reopen its oldest sessions.
|
||||
@@ -433,7 +433,9 @@ export function serveAcpAgent(peer: JsonRpcPeer, options: AcpAgentOptions): void
|
||||
say(session.id, state.reply);
|
||||
return { stopReason: "end_turn" };
|
||||
}
|
||||
for (const ask of await team.queue.openAsks(team.caller)) {
|
||||
const asks = await team.queue.openAsks(team.caller);
|
||||
// After a cancel, even one that landed during that read, no new question goes to the person.
|
||||
for (const ask of cancelled ? [] : asks) {
|
||||
if (seen.has(ask.qitemId)) continue;
|
||||
// An ask tagged for a chat belongs to that chat alone. An untagged one from a seat this work reached might be
|
||||
// another chat's, so it isn't asked here; it's named, with where to answer it.
|
||||
|
||||
@@ -103,7 +103,7 @@ export function daemonTeamQueue(client: DaemonClient): TeamQueue {
|
||||
}));
|
||||
},
|
||||
async answer(qitemId, questionId, optionId) {
|
||||
// The person's answer path, as the TUI records one: the daemon takes the actor from this client's caller.
|
||||
// The person's answer path, as a click in Slack records one: the daemon takes the actor from this client's caller.
|
||||
const bearer = process.env.OPENRIG_AUTH_BEARER_TOKEN;
|
||||
const response = await client.post<Record<string, unknown>>("/api/mission-control/action", { verb: "answer", qitemId, questionId, optionId },
|
||||
bearer ? { headers: { Authorization: `Bearer ${bearer}` } } : undefined);
|
||||
|
||||
@@ -386,6 +386,28 @@ describe("rig acp serve", () => {
|
||||
expect(db.prepare("SELECT human_answers FROM queue_items WHERE qitem_id = 'qitem-ask-theme'").get()).toEqual({ human_answers: null });
|
||||
});
|
||||
|
||||
it("after a cancel no new question goes to the app, even one already waiting when the cancel wakes the turn", async () => {
|
||||
// The poll never fires on its own here, so the only pass after the first is the one the cancel's wake runs.
|
||||
let scans = 0;
|
||||
let shown = false;
|
||||
const hiddenUntilCancel = (q: TeamQueue): TeamQueue => ({ ...q, openAsks: async (person) => { scans += 1; return shown ? q.openAsks(person) : []; } });
|
||||
const { client, updates } = connect({ pollMs: 60_000, sleep: (ms) => new Promise<void>((resolve) => { if (ms !== 60_000) setTimeout(resolve, ms); }) },
|
||||
"asker5@team", hiddenUntilCancel);
|
||||
const asked: string[] = [];
|
||||
client.onRequest("session/request_permission", async (params) => { asked.push((params as any).toolCall.toolCallId); return { outcome: { outcome: "cancelled" } }; });
|
||||
const sessionId = await open(client);
|
||||
const turn = client.request("session/prompt", prompt(sessionId, "Pick a font"));
|
||||
await until(() => (scans > 0 ? true : undefined), "the first read of open questions");
|
||||
await move("asker5@team", "/api/queue/create", { qitemId: "qitem-ask-after-cancel", destinationSession: CALLER, body: "Serif or sans", summary: "Font?", nudge: false,
|
||||
tags: [`acp-session:${sessionId}`], humanQuestions: [{ id: "q1", question: "Which font?", options: [{ id: "s", label: "Serif" }, { id: "n", label: "Sans" }] }] });
|
||||
shown = true;
|
||||
client.notify("session/cancel", { sessionId });
|
||||
expect(await turn).toEqual({ stopReason: "cancelled" });
|
||||
expect(asked).toEqual([]);
|
||||
expect(updates.some((u) => String(u.toolCallId ?? "").startsWith("qitem-ask-after-cancel"))).toBe(false);
|
||||
expect(db.prepare("SELECT state, human_answers FROM queue_items WHERE qitem_id = 'qitem-ask-after-cancel'").get()).toEqual({ state: "pending", human_answers: null });
|
||||
});
|
||||
|
||||
it("when the work closes with a question still on screen, the app is told before the turn ends, and a later reply answers nothing", async () => {
|
||||
const { client, inbound } = connect({}, "asker4@team");
|
||||
let answerLate!: () => void;
|
||||
|
||||
Reference in new issue
Block a user