@a-t-h-i/bot-lobby 0.6.6 → 0.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +183 -11
- package/package.json +1 -1
- package/prompts/master.md +21 -0
- package/prompts/panel.md +4 -0
- package/prompts/planner.md +5 -0
- package/prompts/pr-review.md +62 -0
- package/prompts/splitter.md +58 -0
- package/src/ask/dialog.ts +23 -7
- package/src/ask/state.ts +18 -3
- package/src/ask/tool.ts +9 -2
- package/src/ask/view.ts +8 -4
- package/src/classifier/instance.ts +6 -0
- package/src/classifier/knowledge.ts +158 -0
- package/src/classifier/review.ts +146 -0
- package/src/execution/agent-runner.ts +23 -1
- package/src/execution/fallback.ts +75 -0
- package/src/execution/workspace.ts +185 -0
- package/src/knowledge/edit.ts +109 -0
- package/src/knowledge/notes.ts +143 -0
- package/src/knowledge/selector.ts +1 -1
- package/src/knowledge/store.ts +16 -2
- package/src/lobby/ask.ts +42 -21
- package/src/lobby/feed.ts +7 -0
- package/src/lobby/issues.ts +1 -1
- package/src/lobby/knowledge.ts +179 -0
- package/src/lobby/layout.ts +99 -1
- package/src/lobby/mini.ts +159 -0
- package/src/lobby/planner.ts +209 -10
- package/src/lobby/pr-review.ts +360 -0
- package/src/lobby/pulls.ts +250 -0
- package/src/lobby/quickfix.ts +17 -3
- package/src/lobby/runtime.ts +142 -17
- package/src/lobby/split.ts +230 -0
- package/src/lobby/tabs/git.ts +162 -0
- package/src/lobby/tabs/knowledge.ts +135 -0
- package/src/lobby/tabs/plan.ts +3 -1
- package/src/lobby/tabs/tasks.ts +11 -2
- package/src/lobby/view.ts +450 -35
- package/src/master/master.ts +40 -14
- package/src/master/research.ts +1 -0
- package/src/pi/commands.ts +12 -7
- package/src/pi/events.ts +2 -0
- package/src/pi/master-fallback.ts +61 -0
- package/src/pi/model-support.ts +27 -3
- package/src/pi/plan-checklist.ts +42 -0
- package/src/pi/route.ts +2 -1
- package/src/pi/settings-ui.ts +104 -11
- package/src/pi/start-flags.ts +14 -0
- package/src/pi/start-task.ts +56 -5
- package/src/pi/tools.ts +4 -2
- package/src/schemas/configuration.ts +74 -11
- package/src/schemas/findings.ts +2 -0
- package/src/schemas/task.ts +15 -0
- package/src/state/backlog.ts +27 -4
- package/src/state/metrics.ts +2 -2
- package/src/state/persistence.ts +5 -5
- package/src/text.ts +38 -0
- package/src/workflow/workflow.ts +23 -2
package/src/lobby/planner.ts
CHANGED
|
@@ -20,15 +20,19 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { loadPrompt } from "../prompts/loader.ts";
|
|
22
22
|
import { compilePrompt } from "../prompts/compiler.ts";
|
|
23
|
+
import { withFallback } from "../execution/fallback.ts";
|
|
23
24
|
import { runPiAgent, spawnPiProcess, type PiStreamEvent, type ProcessRunner } from "../execution/pi-runner.ts";
|
|
24
25
|
import { describeToolCall } from "../pi/activity.ts";
|
|
25
26
|
import { appendMetrics, type MetricRecord } from "../state/metrics.ts";
|
|
26
27
|
import { savePlannedTask, type IssueRef, type PlannedTask } from "../state/backlog.ts";
|
|
28
|
+
import { planSteps } from "../pi/plan-checklist.ts";
|
|
29
|
+
import { KEEP_WHOLE, MAX_SPLIT_REVISIONS, partBrief, partInfo, parseSplit, splitFailedQuestion, splitLabel, splitProblems, splitPrompt, splitQuestion, splitRequest, type SplitProposal } from "./split.ts";
|
|
30
|
+
import type { AskQuestion, AskResult } from "../ask/types.ts";
|
|
27
31
|
import { PANEL_MEMBERS, type PanelMember } from "../schemas/configuration.ts";
|
|
28
32
|
import { truncate } from "../text.ts";
|
|
29
33
|
import type { LobbyFeed } from "./feed.ts";
|
|
30
34
|
import type { QuickFixProfile } from "./quickfix.ts";
|
|
31
|
-
import { MAX_QUESTIONS
|
|
35
|
+
import { MAX_QUESTIONS } from "./ask.ts";
|
|
32
36
|
import type { Classifier } from "../classifier/classifier.ts";
|
|
33
37
|
import { chooseSeats } from "../classifier/seats.ts";
|
|
34
38
|
import { autoAnswer, type AutoAnswer } from "../classifier/answers.ts";
|
|
@@ -70,6 +74,13 @@ export interface PanelQuestion extends AskedQuestion {
|
|
|
70
74
|
from: string;
|
|
71
75
|
}
|
|
72
76
|
|
|
77
|
+
/** One of the panel's questions as the user's reply settled it: answered, or left open (`answer` absent). */
|
|
78
|
+
export interface SettledQuestion {
|
|
79
|
+
from: string;
|
|
80
|
+
question: string;
|
|
81
|
+
answer?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
73
84
|
export interface PlannerMessage {
|
|
74
85
|
role: "you" | "planner";
|
|
75
86
|
text: string;
|
|
@@ -78,6 +89,8 @@ export interface PlannerMessage {
|
|
|
78
89
|
questions?: PanelQuestion[];
|
|
79
90
|
/** Questions the classifier answered with their recommended option instead of asking. */
|
|
80
91
|
decided?: AutoAnswer[];
|
|
92
|
+
/** On the user's reply: which of the questions before it were answered, so they are never asked again. */
|
|
93
|
+
settled?: SettledQuestion[];
|
|
81
94
|
}
|
|
82
95
|
|
|
83
96
|
/** The oracle's reply: its verdict, title, own questions and the draft plan. */
|
|
@@ -336,6 +349,8 @@ export function plannerTranscript(messages: readonly PlannerMessage[], seed?: Pl
|
|
|
336
349
|
lines.push(message.role === "you" ? "### User" : "### Panel", "", body, "");
|
|
337
350
|
if (message.decided && message.decided.length > 0) lines.push(decidedBlock(message.decided), "");
|
|
338
351
|
}
|
|
352
|
+
const settled = messages.flatMap((message) => message.settled ?? []);
|
|
353
|
+
if (settled.length > 0) lines.push(settledBlock(settled), "");
|
|
339
354
|
if (draft) lines.push("## The oracle's current draft plan", "", truncate(draft, 8000), "");
|
|
340
355
|
lines.push(closing);
|
|
341
356
|
return lines.join("\n");
|
|
@@ -355,6 +370,55 @@ export function decidedBlock(decided: readonly AutoAnswer[]): string {
|
|
|
355
370
|
].join("\n");
|
|
356
371
|
}
|
|
357
372
|
|
|
373
|
+
/** One line of at most `max` characters, for the activity log. */
|
|
374
|
+
function brief(text: string, max: number): string {
|
|
375
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
376
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Two questions this alike (by shared words) ask the same thing. */
|
|
380
|
+
const SAME_QUESTION = 0.8;
|
|
381
|
+
|
|
382
|
+
/** Words that carry no topic: a rewording changes them without changing the question. */
|
|
383
|
+
const FILLER = new Set(["a", "an", "the", "of", "to", "in", "on", "at", "for", "with", "and", "or", "is", "are", "be", "been", "it", "its", "this", "that", "do", "does", "did", "we", "you", "i", "should", "would", "could", "can", "will", "shall", "must", "which", "what", "how", "so", "as", "by", "from", "than", "then", "us", "our", "your"]);
|
|
384
|
+
|
|
385
|
+
function questionWords(text: string): Set<string> {
|
|
386
|
+
const words = text.toLowerCase().replace(/[^a-z0-9\s]+/g, " ").split(/\s+/).filter(Boolean);
|
|
387
|
+
const content = words.filter((word) => !FILLER.has(word));
|
|
388
|
+
return new Set(content.length > 0 ? content : words);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Whether two questions ask the same thing: the same topic words in any order, or nearly all of them. */
|
|
392
|
+
export function sameQuestion(a: string, b: string): boolean {
|
|
393
|
+
const left = questionWords(a);
|
|
394
|
+
const right = questionWords(b);
|
|
395
|
+
if (left.size === 0 || right.size === 0) return false;
|
|
396
|
+
let shared = 0;
|
|
397
|
+
for (const word of left) if (right.has(word)) shared += 1;
|
|
398
|
+
if (shared === left.size && shared === right.size) return true;
|
|
399
|
+
return Math.min(left.size, right.size) >= 3 && shared / (left.size + right.size - shared) >= SAME_QUESTION;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Split a round's questions into those still to ask and the repeats of ones
|
|
404
|
+
* the user already settled (answered, or left for the oracle to decide): the
|
|
405
|
+
* user never gets the same question twice, whatever the models write.
|
|
406
|
+
*/
|
|
407
|
+
export function withoutSettled(questions: readonly PanelQuestion[], settled: readonly SettledQuestion[]): { asked: PanelQuestion[]; repeats: PanelQuestion[] } {
|
|
408
|
+
const asked: PanelQuestion[] = [];
|
|
409
|
+
const repeats: PanelQuestion[] = [];
|
|
410
|
+
for (const question of questions) (settled.some((entry) => sameQuestion(entry.question, question.text)) ? repeats : asked).push(question);
|
|
411
|
+
return { asked, repeats };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** What the user settled, as every seat and the oracle read it: closed questions, never to be asked again. */
|
|
415
|
+
export function settledBlock(settled: readonly SettledQuestion[]): string {
|
|
416
|
+
return [
|
|
417
|
+
"## Already settled with the user (closed: never ask these again, in any wording)",
|
|
418
|
+
...settled.map((entry) => `- [${entry.from}] ${entry.question} → ${entry.answer ? entry.answer.replace(/\s*\n\s*/g, " / ") : "(left unanswered: decide it yourself with its recommended option and list it under Assumptions)"}`),
|
|
419
|
+
].join("\n");
|
|
420
|
+
}
|
|
421
|
+
|
|
358
422
|
/** The seats' output for the oracle: each seat's status, questions and notes, or its failure. */
|
|
359
423
|
export function panelSection(outcomes: readonly MemberOutcome[]): string {
|
|
360
424
|
if (outcomes.length === 0) return "## Panel this round\n\nNo domain seats this round; you are planning alone.";
|
|
@@ -438,6 +502,8 @@ export class PlanningSession {
|
|
|
438
502
|
saved?: PlannedTask;
|
|
439
503
|
/** Questionnaires answered so far for this round's questions, so stopping one resumes where it left off. */
|
|
440
504
|
answered: AskResult[] = [];
|
|
505
|
+
/** The tasks the plan was saved as when it was split (`saved` is the first). */
|
|
506
|
+
savedParts: PlannedTask[] = [];
|
|
441
507
|
/** Comments on draft lines, sent with the user's next turn. */
|
|
442
508
|
lineComments: LineComment[] = [];
|
|
443
509
|
/** How the latest round ran under the round limit. */
|
|
@@ -497,21 +563,30 @@ export class PlanningSession {
|
|
|
497
563
|
return this.seats.has(member);
|
|
498
564
|
}
|
|
499
565
|
|
|
500
|
-
/**
|
|
501
|
-
|
|
566
|
+
/**
|
|
567
|
+
* Add the user's message (the idea, or answers), with any line comments, and
|
|
568
|
+
* run a round. `settled` says which of the open questions the message
|
|
569
|
+
* answered (or left), so the panel is never allowed to ask them again.
|
|
570
|
+
*/
|
|
571
|
+
async send(text: string, settled?: readonly SettledQuestion[]): Promise<void> {
|
|
502
572
|
this.autoContinued = false;
|
|
503
|
-
await this.post(text);
|
|
573
|
+
await this.post(text, settled);
|
|
504
574
|
}
|
|
505
575
|
|
|
506
|
-
private async post(text: string): Promise<void> {
|
|
576
|
+
private async post(text: string, settled?: readonly SettledQuestion[]): Promise<void> {
|
|
507
577
|
const body = [text.trim(), commentBlock(this.lineComments)].filter(Boolean).join("\n\n");
|
|
508
578
|
if (!body) return;
|
|
509
579
|
if (this.busy) throw new Error("the panel is still thinking");
|
|
510
580
|
this.lineComments = [];
|
|
511
|
-
this.messages = [...this.messages, { role: "you", text: body, at: Date.now() }];
|
|
581
|
+
this.messages = [...this.messages, { role: "you", text: body, at: Date.now(), ...(settled && settled.length > 0 ? { settled: [...settled] } : {}) }];
|
|
512
582
|
await this.turn();
|
|
513
583
|
}
|
|
514
584
|
|
|
585
|
+
/** Every question the user has settled so far, oldest first. */
|
|
586
|
+
get settled(): SettledQuestion[] {
|
|
587
|
+
return this.messages.flatMap((message) => message.settled ?? []);
|
|
588
|
+
}
|
|
589
|
+
|
|
515
590
|
/** The round's questions still wait for answers. */
|
|
516
591
|
get awaitingAnswers(): boolean {
|
|
517
592
|
return !this.busy && this.questions.length > 0;
|
|
@@ -573,6 +648,120 @@ export class PlanningSession {
|
|
|
573
648
|
return this.saved;
|
|
574
649
|
}
|
|
575
650
|
|
|
651
|
+
/** Save each part of a split plan as its own pending task, in order, each knowing the others. */
|
|
652
|
+
saveParts(proposal: SplitProposal, now = new Date()): PlannedTask[] {
|
|
653
|
+
const plan = this.reply?.plan;
|
|
654
|
+
if (!plan) throw new Error("there is no draft plan to save yet");
|
|
655
|
+
const steps = planSteps(plan);
|
|
656
|
+
const group = `SPLIT-${now.getTime().toString(36)}`;
|
|
657
|
+
// Part 1 is saved last of all timestamps' newest, so the newest-first pending list reads in part order.
|
|
658
|
+
const parts = proposal.tasks.map((task, index) => savePlannedTask(this.deps.root, this.deps.configDir, {
|
|
659
|
+
title: task.title,
|
|
660
|
+
brief: partBrief({ plan, steps, proposal, index }),
|
|
661
|
+
...(this.seed ? { issue: this.seed.issue } : {}),
|
|
662
|
+
split: partInfo(proposal, index, group),
|
|
663
|
+
}, new Date(now.getTime() + (proposal.tasks.length - index))));
|
|
664
|
+
this.savedParts = parts;
|
|
665
|
+
this.saved = parts[0];
|
|
666
|
+
this.deps.feed?.log(ORACLE_LABEL, `split the plan into ${parts.length} tasks: ${parts.map((part) => part.id).join(", ")}`, "success");
|
|
667
|
+
this.deps.onChange?.();
|
|
668
|
+
return parts;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* The oracle's proposal for splitting the plan: run, read, and checked
|
|
673
|
+
* against the rules (once more with the problems when it breaks one).
|
|
674
|
+
* `feedback` is the user's change request on an earlier proposal. A string
|
|
675
|
+
* is the reason there is no proposal.
|
|
676
|
+
*/
|
|
677
|
+
private async proposeSplit(plan: string, steps: readonly string[], feedback: { previous: SplitProposal; words: string } | undefined, signal: AbortSignal): Promise<SplitProposal | string> {
|
|
678
|
+
const profile = this.deps.profile();
|
|
679
|
+
const title = this.title ?? "the plan";
|
|
680
|
+
let rejected: string[] = [];
|
|
681
|
+
let reason = "";
|
|
682
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
683
|
+
this.step = attempt === 0 ? "deciding how to split the plan" : "fixing the split";
|
|
684
|
+
this.deps.onChange?.();
|
|
685
|
+
const lead = await this.run(ORACLE_LABEL, "planner", profile, {
|
|
686
|
+
task: splitRequest({ title, plan, steps, ...(feedback ? { revision: feedback } : {}), ...(rejected.length > 0 ? { rejected } : {}) }),
|
|
687
|
+
systemPrompt: splitPrompt(profile.instructions),
|
|
688
|
+
tools: this.tools(PLANNER_TOOLS),
|
|
689
|
+
}, signal, (step) => (this.step = step));
|
|
690
|
+
if (signal.aborted) throw new Error("stopped");
|
|
691
|
+
if (lead.status !== "success") return lead.error ?? lead.status;
|
|
692
|
+
try {
|
|
693
|
+
const proposal = parseSplit(lead.output);
|
|
694
|
+
rejected = splitProblems(proposal, steps.length);
|
|
695
|
+
if (rejected.length === 0) return proposal;
|
|
696
|
+
reason = rejected[0]!;
|
|
697
|
+
} catch (error) {
|
|
698
|
+
rejected = [(error as Error).message];
|
|
699
|
+
reason = rejected[0]!;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return reason;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Save the plan as a pending task; when it has more steps than
|
|
707
|
+
* `splitAbove`, first let the oracle propose splitting it into up to five
|
|
708
|
+
* tasks and let the user decide: take it, keep the plan whole, or say what to
|
|
709
|
+
* change (the oracle revises, a few times). Nothing the user leaves open is
|
|
710
|
+
* decided for them: a questionnaire they put away saves nothing. Returns the
|
|
711
|
+
* notice for the lobby.
|
|
712
|
+
*/
|
|
713
|
+
async saveWithSplit(ask: (questions: AskQuestion[], signal: AbortSignal) => Promise<AskResult>, options: { splitAbove: number }): Promise<string> {
|
|
714
|
+
const plan = this.reply?.plan;
|
|
715
|
+
if (!plan) throw new Error("there is no draft plan to save yet");
|
|
716
|
+
const steps = planSteps(plan);
|
|
717
|
+
const unagreed = this.reply?.status === "ready" ? "" : " (the panel had not agreed yet)";
|
|
718
|
+
const whole = () => `saved ${this.save().id} to the pending tasks — start it from the Tasks tab${unagreed}`;
|
|
719
|
+
if (options.splitAbove <= 0 || steps.length <= options.splitAbove) return whole();
|
|
720
|
+
if (this.busy) return "the panel is still working — save again once it is done, and the oracle will look at splitting this plan";
|
|
721
|
+
const controller = new AbortController();
|
|
722
|
+
this.controller = controller;
|
|
723
|
+
this.status = "thinking";
|
|
724
|
+
this.error = undefined;
|
|
725
|
+
this.deps.onChange?.();
|
|
726
|
+
try {
|
|
727
|
+
let proposal = await this.proposeSplit(plan, steps, undefined, controller.signal);
|
|
728
|
+
if (typeof proposal === "string") {
|
|
729
|
+
const reason = proposal;
|
|
730
|
+
this.deps.feed?.log(ORACLE_LABEL, `could not split the plan — ${reason.split("\n")[0]}`, "warning");
|
|
731
|
+
this.step = "waiting for you";
|
|
732
|
+
const answer = (await ask([splitFailedQuestion(reason)], controller.signal)).answers[0];
|
|
733
|
+
if (controller.signal.aborted) throw new Error("stopped");
|
|
734
|
+
return answer?.answer === "Save it as one task" ? whole() : "the plan was not saved — ctrl+s tries again";
|
|
735
|
+
}
|
|
736
|
+
for (let revisions = 0; ; revisions += 1) {
|
|
737
|
+
this.step = "waiting for you";
|
|
738
|
+
this.deps.onChange?.();
|
|
739
|
+
const result = await ask([splitQuestion({ stepCount: steps.length, proposal, steps, revisions })], controller.signal);
|
|
740
|
+
// A stopped questionnaire comes back empty too; it was stopped, not left open.
|
|
741
|
+
if (controller.signal.aborted) throw new Error("stopped");
|
|
742
|
+
const answer = result.answers[0];
|
|
743
|
+
if (!answer) return "the split question was left open, so nothing was saved — ctrl+s asks again";
|
|
744
|
+
if (answer.kind === "option") {
|
|
745
|
+
if (answer.answer === KEEP_WHOLE) return whole();
|
|
746
|
+
const parts = this.saveParts(proposal);
|
|
747
|
+
return `split into ${parts.length} tasks — ${parts.map((part) => part.id).join(", ")}; start them from the Tasks tab, in order${unagreed}`;
|
|
748
|
+
}
|
|
749
|
+
if (revisions >= MAX_SPLIT_REVISIONS) return `that was the last revision, and nothing was saved — ctrl+s asks again and the oracle starts over`;
|
|
750
|
+
const revised = await this.proposeSplit(plan, steps, { previous: proposal, words: answer.answer ?? "" }, controller.signal);
|
|
751
|
+
if (typeof revised === "string") return `the oracle could not apply that (${revised.split("\n")[0]}) — nothing was saved; ctrl+s asks again`;
|
|
752
|
+
proposal = revised;
|
|
753
|
+
}
|
|
754
|
+
} catch (error) {
|
|
755
|
+
if (controller.signal.aborted) return "stopped — nothing was saved";
|
|
756
|
+
return `could not save the plan: ${(error as Error).message}`;
|
|
757
|
+
} finally {
|
|
758
|
+
this.status = "idle";
|
|
759
|
+
this.step = undefined;
|
|
760
|
+
this.controller = undefined;
|
|
761
|
+
this.deps.onChange?.();
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
576
765
|
private stepKey(who: string): string {
|
|
577
766
|
return `plan-${who}-${this.attempts}`;
|
|
578
767
|
}
|
|
@@ -593,12 +782,12 @@ export class PlanningSession {
|
|
|
593
782
|
const startedAt = Date.now();
|
|
594
783
|
let outcome: RunOutcome;
|
|
595
784
|
try {
|
|
596
|
-
const
|
|
785
|
+
const attempt = (model: string | undefined, thinking: string) => runPiAgent(
|
|
597
786
|
{
|
|
598
787
|
cwd: this.deps.cwd,
|
|
599
788
|
...request,
|
|
600
|
-
model
|
|
601
|
-
thinking
|
|
789
|
+
model,
|
|
790
|
+
thinking,
|
|
602
791
|
timeoutMs: profile.timeoutMs,
|
|
603
792
|
signal,
|
|
604
793
|
stallTimeoutMs: this.deps.stallTimeoutMs,
|
|
@@ -607,6 +796,12 @@ export class PlanningSession {
|
|
|
607
796
|
},
|
|
608
797
|
this.deps.runProcess ?? spawnPiProcess,
|
|
609
798
|
);
|
|
799
|
+
// A model that is out of usage hands the turn to the fallback the settings name.
|
|
800
|
+
const { result, switchedFrom } = await withFallback(profile.model, profile.thinking, profile.fallback, attempt);
|
|
801
|
+
if (switchedFrom && profile.fallback) {
|
|
802
|
+
this.deps.feed?.log(label, `${switchedFrom} is out of usage or unavailable; ran on ${profile.fallback.model}`, "warning");
|
|
803
|
+
profile = { ...profile, model: profile.fallback.model, thinking: profile.fallback.thinking };
|
|
804
|
+
}
|
|
610
805
|
outcome = { status: result.status, output: result.output, ...(result.error ? { error: result.error } : {}), ...(result.model ? { model: result.model } : {}), usage: result.usage };
|
|
611
806
|
} catch (error) {
|
|
612
807
|
outcome = { status: "failed", output: "", error: (error as Error).message, usage: { input: 0, output: 0, cost: 0, turns: 0 } };
|
|
@@ -679,6 +874,8 @@ export class PlanningSession {
|
|
|
679
874
|
this.turns += 1;
|
|
680
875
|
this.attempts += 1;
|
|
681
876
|
this.answered = [];
|
|
877
|
+
// The user's reply answers whatever was open: a round that then fails or is stopped must not put those questions to them again.
|
|
878
|
+
this.questions = [];
|
|
682
879
|
const limit = this.limit;
|
|
683
880
|
const mode = roundMode(this.turns, limit);
|
|
684
881
|
this.mode = mode;
|
|
@@ -817,7 +1014,9 @@ export class PlanningSession {
|
|
|
817
1014
|
private finishRound(outcomes: readonly MemberOutcome[], lead: RunOutcome, mode: RoundMode): void {
|
|
818
1015
|
const reply = lead.status === "success" ? parsePlannerReply(lead.output) : undefined;
|
|
819
1016
|
const seatQuestions = outcomes.flatMap((outcome) => (outcome.reply?.questions ?? []).map((question) => ({ ...question, from: MEMBER_LABELS[outcome.member] })));
|
|
820
|
-
|
|
1017
|
+
const { asked, repeats } = withoutSettled(roundQuestions(reply, seatQuestions), this.settled);
|
|
1018
|
+
let questions = asked;
|
|
1019
|
+
if (repeats.length > 0) this.deps.feed?.log(ORACLE_LABEL, `held back ${repeats.length} question${repeats.length === 1 ? "" : "s"} you already settled: ${repeats.map((question) => brief(question.text, 60)).join("; ")}`, "info");
|
|
821
1020
|
const seatsReady = outcomes.every((outcome) => outcome.reply?.status === "ready");
|
|
822
1021
|
let ready = Boolean(reply && reply.status === "ready" && seatsReady);
|
|
823
1022
|
if (reply) {
|