Skip to content

Compare changes

Choose two branches to see what one has that the other does not, then open a pull request for it.

Open a pull request

1 commit

21 files+318−230/21 viewed
+13−4
113113 "was asked a question by the agent on #41" and "answered the question from
114114 the agent on #41".
115115
116−If the agent asked is not at work, it will not answer soon. The response to
117−`message_agent` says so, in `hint`, and points the asking agent at the
118−other pull request's change to read with `get_pull_request` and
119−`get_pull_request_changes` instead.
116+If the agent asked is not at work, because its change is done and waiting
117+for review or a merge, g1t wakes it to answer. It starts a short run in that
118+pull request's sandbox with the agent's own change in front of it and what
119+it was asked; the agent reads its code, answers with `answer_message`, and,
120+for a handoff it takes on, commits the work. Its pull request is noted "g1t
121+woke g1t-agent to answer the agent on #41", and nothing else starts on it
122+until everything it was asked is answered, or 20 minutes pass. The response
123+to `message_agent` says so in `hint`, and points the asking agent at the
124+other pull request's change to read meanwhile with `get_pull_request` and
125+`get_pull_request_changes`.
126+
127+An agent g1t has stopped on (its pull request needs a person) is not woken;
128+the hint then says it will not answer soon.
120129
121130 On an [outcome's page](/guides/outcomes/#agents-talking), **Agents talking**
122131 lists every exchange between its agents with where it stands: waiting to be
+1−0
7070 | `workflow.completed` | A GitHub Actions run finished. `data.workflow`, `data.conclusion`, `data.runId`, `data.sha`, `data.pull`. |
7171 | `queue.changed` | The merge queue gained, lost or settled an entry. |
7272 | `session.appended` | An agent's session grew. Busy: choose it only if you need it. |
73+| `agent.asked` | An agent asked the agent on another pull request a question, or handed it work, while that one was not at work; g1t wakes it to answer. |
7374
7475 ## Check the signature
7576
+3−0
44 CircleSlash,
55 GitMerge,
66 GitPullRequest,
7+ MessageCircleQuestion,
78 MessageSquare,
89 Play,
910 Terminal,
9495 };
9596 case "pull.merged":
9697 return { icon: <GitMerge size={14} />, tone: "text-accent", actor, text: <>landed {ref(event.data.number)} on main</> };
98+ case "agent.asked":
99+ return { icon: <MessageCircleQuestion size={14} />, tone: "text-merged", actor, text: <>asked the agent on {ref(event.data.number)}, and g1t woke it to answer</> };
97100 case "pull.merge_requested":
98101 return { icon: <GitMerge size={14} />, tone: "text-muted", actor, text: <>is bringing {ref(event.data.number)} up to date</> };
99102 default:
+4−0
1111 checking: 1,
1212 reviewing: 2,
1313 catching_up: 3,
14+ // Its change is made; it is answering another agent.
15+ answering: 2,
1416 ready: 4,
1517 queued: 4,
1618 };
2224 reviewing: "In review",
2325 revising: "Revising",
2426 catching_up: "Catching up",
27+ answering: "Answering an agent",
2528 queued: "In the merge queue",
2629 ready: "Ready to merge",
2730 needs_you: "Needs you",
5558 reviewing: "g1t is seeing this through",
5659 revising: "g1t is seeing this through",
5760 catching_up: "g1t is seeing this through",
61+ answering: "g1t is seeing this through",
5862 queued: "In the merge queue",
5963 ready: "Ready to merge",
6064 needs_you: "Needs you",
+9−1
5252 bar: "bg-merged",
5353 live: true,
5454 },
55+ answering: {
56+ label: "Answering an agent",
57+ icon: <Sparkles size={13} />,
58+ ring: "ring-merged/60",
59+ text: "text-merged",
60+ bar: "bg-merged",
61+ live: true,
62+ },
5563 catching_up: {
5664 label: "Catching up",
5765 icon: <Loader2 size={13} className="animate-spin" />,
214222 const live = plan.progress.filter((item) => look(item.state).live).length;
215223 const needsYou = count(["needs_you"]);
216224 const blocked = count(["blocked", "waiting", "open"]);
217− const order = ["landed", "queued", "ready", "reviewing", "checking", "working", "revising", "catching_up", "needs_you", "waiting", "blocked", "open", "closed"];
225+ const order = ["landed", "queued", "ready", "reviewing", "checking", "working", "revising", "catching_up", "answering", "needs_you", "waiting", "blocked", "open", "closed"];
218226 const sorted = [...plan.progress].sort((a, b) => order.indexOf(a.state) - order.indexOf(b.state));
219227
220228 return (
+1−1
2323 events: ["pull.opened", "pull.ready", "pull.updated", "pull.merge_requested", "pull.merged", "pull.closed"],
2424 },
2525 { title: "Checks, reviews and the queue", events: ["checks.completed", "workflow.completed", "review.completed", "queue.changed"] },
26− { title: "Agents", events: ["session.appended"] },
26+ { title: "Agents", events: ["session.appended", "agent.asked"] },
2727 ];
2828
2929 function StatusDot({ status }: { status: string | null }) {
+1−1
437437 {/* Steering: while its agent works, people can tell it things. */}
438438 {canManage &&
439439 pull.runtime === "hosted" &&
440− (working || ["working", "revising", "catching_up"].includes(lifecycle?.stage ?? "")) && (
440+ (working || ["working", "revising", "catching_up", "answering"].includes(lifecycle?.stage ?? "")) && (
441441 <Form method="post" className="mt-4 rounded-2xl bg-surface p-4 ring-1 ring-merged/30">
442442 <p className="flex items-center gap-2 text-sm font-medium">
443443 <Sparkles size={15} className="text-merged" />
+2−1
176176 pull request a question, or hands it work, with `message_agent` (`kind`
177177 `question` or `handoff`, and `from_number`, its own pull request). The
178178 other agent replies with `answer_message`
179− (`POST {repo}/messages/{id}/answer`). Plans show these exchanges under
179+ (`POST {repo}/messages/{id}/answer`); one that is not at work is woken
180+ to answer, in its own pull request's sandbox. Plans show these exchanges under
180181 "Agents talking". From any other caller, `message_agent` sends a plain
181182 message.
182183
+2−1
1515 use crate::{User, Viewer};
1616
1717 /// Every event a webhook can be sent, in the order people are shown them.
18−pub const EVENT_TYPES: [&str; 20] = [
18+pub const EVENT_TYPES: [&str; 21] = [
1919 "git.push",
2020 "repo.created",
2121 "repo.forked",
3131 "pull.merge_requested",
3232 "pull.merged",
3333 "pull.closed",
34+ "agent.asked",
3435 "checks.completed",
3536 "review.completed",
3637 "workflow.completed",
+23−1
542542 Revising,
543543 /// The agent is merging in the branch it would land on, which moved.
544544 CatchingUp,
545+ /// Woken to answer a question another agent asked it, or a handoff.
546+ Answering,
545547 /// In the repository's merge queue, being tested with what is ahead of
546548 /// it before it lands.
547549 Queued,
683685 pub pull_id: String,
684686 }
685687
688+/// `wake_for_messages`: the agent on a pull request was asked a question
689+/// or handed work while it was not at work. Claims a short step for it to
690+/// answer, and hands over what it was sent, marked read. Null when there
691+/// is nothing waiting, or the pull request cannot take a step now.
692+/// Returns `Option<Wake>`.
693+#[derive(Debug, Serialize, Deserialize)]
694+#[serde(rename_all = "camelCase")]
695+pub struct WakeForMessagesArgs {
696+ pub pull_id: String,
697+}
698+
699+/// What an agent woken to answer needs: its pull request, and what it was
700+/// sent, oldest first.
701+#[derive(Debug, Serialize, Deserialize)]
702+#[serde(rename_all = "camelCase")]
703+pub struct Wake {
704+ pub job: LifecycleJob,
705+ pub messages: Vec<AgentMessage>,
706+}
707+
686708 /// `stall`: records that a step could not be carried out, so that g1t
687709 /// stops and a person is asked. Returns `bool`.
688710 #[derive(Debug, Serialize, Deserialize)]
924946 /// `blocked` (waiting on issues it depends on), `waiting` (for an
925947 /// agent), `open` (nobody on it), one of the lifecycle stages
926948 /// (`working`, `checking`, `reviewing`, `revising`, `catching_up`,
927− /// `queued`, `ready`, `needs_you`), `landed` or `closed`.
949+ /// `answering`, `queued`, `ready`, `needs_you`), `landed` or `closed`.
928950 pub state: String,
929951 /// One sentence about where it stands.
930952 pub detail: String,
+5−0
152152 }
153153 let head = git(workdir, &["rev-parse", "HEAD"])?;
154154 if head == start {
155+ // An agent woken to answer usually only answers.
156+ if std::env::var("MODE").as_deref() == Ok("answer") {
157+ return Ok(summary);
158+ }
155159 bail!("the agent finished without changing anything");
156160 }
157161 git(
181185 Ok("update") => std::process::exit(update::main()),
182186 Ok("review") => std::process::exit(review::main()),
183187 Ok("revise") => std::process::exit(revise::main()),
188+ Ok("answer") => std::process::exit(revise::answer()),
184189 Ok("plan") => std::process::exit(plan::main()),
185190 Ok("queue") => std::process::exit(queue::main()),
186191 Ok("steer") => std::process::exit(steer::main()),
+12−2
99
1010 use crate::report::{Entry, Reporter};
1111
12+/// The author woken to answer what other agents asked it while it was not
13+/// at work: the same run, which pushes only if it took on handed-over work.
14+pub fn answer() -> i32 {
15+ finish(crate::run, "Answering failed")
16+}
17+
1218 pub fn main() -> i32 {
19+ finish(crate::run, "The revision failed")
20+}
21+
22+fn finish(run: fn(&mut Reporter) -> anyhow::Result<String>, failed: &str) -> i32 {
1323 let mut reporter = match Reporter::from_env() {
1424 Ok(reporter) => reporter,
1525 Err(error) => {
1727 return 2;
1828 }
1929 };
20− let outcome = crate::run(&mut reporter);
30+ let outcome = run(&mut reporter);
2131 // On success the agent's account is already in the session: the harness
2232 // records its messages as they arrive.
2333 if let Err(error) = &outcome {
2434 eprintln!("g1t-runner: {error:#}");
2535 reporter.record(Entry::new(
2636 "note",
27− &format!("The revision failed: {error:#}. Nothing was pushed."),
37+ &format!("{failed}: {error:#}. Nothing was pushed."),
2838 ));
2939 }
3040 reporter.flush();
+4−2
785785 set number of agents on one issue is dropped: choosing how many agents to
786786 use is not something people should have to do.
787787
788−1. **Handoffs and questions between agents** as states (offered, accepted,
789− declined, done) on the outcome page, not only comments.
788+1. ~~**Handoffs and questions between agents** as states on the outcome
789+ page.~~ Done: questions and handoffs show as waiting, read, answered,
790+ taken on or declined; since 2026-10-03 an agent asked while it is not at
791+ work is woken to answer, where before the question waited forever.
790792 2. **The large run.** Dozens of agents on a real repository, end to end, for
791793 the video; g1t hosted on g1t.
792794 3. **Polish for judges trying it in a minute:** a seeded demo workspace, the
+1−0
143143 removeFromQueue: (actor, repo, number) => call("remove_from_queue", { actor, repo, number }),
144144 messageAgent: (actor, repo, number, body) => call("message_agent", { actor, repo, number, body }),
145145 catchUpJob: (pullId) => call("catch_up_job", { pullId }),
146+ wakeForMessages: (pullId) => call("wake_for_messages", { pullId }),
146147 getSettings: (repo, viewer) => call("get_settings", { repo, viewer }),
147148 updateSettings: (actor, repo, settings) =>
148149 call("update_settings", { actor, repo, settings }),
+2−0
3737 /** A merge was asked for while the pull request was behind; it has to catch up first. */
3838 "pull.merge_requested": { pullId: string; repoId: string; number: number; issue?: number };
3939 "pull.closed": { pullId: string; repoId: string; number: number; issue?: number };
40+ /** Another agent asked the agent on a pull request, which was not at work, a question or handed it work. */
41+ "agent.asked": { pullId: string; repoId: string; number: number; issue?: number };
4042 "pull.merged": { pullId: string; repoId: string; number: number; issue?: number; commit: string };
4143 /** A run of the acceptance checks finished. `commit` is what was checked. */
4244 "checks.completed": {
+1−0
2424 "pull.merge_requested",
2525 "pull.merged",
2626 "pull.closed",
27+ "agent.asked",
2728 "checks.completed",
2829 "review.completed",
2930 "workflow.completed",
+13−1
306306 * to merge. g1t takes each one without being asked: `working` (the agent is
307307 * making the change), `checking`, `reviewing`, `revising` (the agent is
308308 * addressing failed checks or a review), `catching_up` (merging in the
309− * branch it would land on), then `ready` for a person to merge. `needs_you`
309+ * branch it would land on), `answering` (woken to answer another agent),
310+ * then `ready` for a person to merge. `needs_you`
310311 * means g1t has stopped and a person decides what happens next.
311312 */
312313 export type Stage =
315316 | "reviewing"
316317 | "revising"
317318 | "catching_up"
319+ | "answering"
318320 | "queued"
319321 | "ready"
320322 | "needs_you";
348350 round: number;
349351 };
350352
353+/** An agent woken to answer what other agents sent it while it was not at work. */
354+export type Wake = { job: LifecycleJob; messages: AgentMessage[] };
355+
351356 /**
352357 * `planning` while an agent reads the repository and writes it; `ready` for
353358 * a person to read and apply; `failed` if it could not be written;
589594 * merge of it was asked for. Null if none was.
590595 */
591596 catchUpJob(pullId: string): Promise<LifecycleJob | null>;
597+ /**
598+ * Claims a short step for the agent on a pull request to answer the
599+ * questions and handoffs it was sent while not at work, and hands them
600+ * over, marked read. Null when there is nothing waiting or it cannot
601+ * take a step now.
602+ */
603+ wakeForMessages(pullId: string): Promise<Wake | null>;
592604
593605 /**
594606 * Opens a pull request: a draft with a fork to push to, or, given a
+90−0
22 import { WorkerEntrypoint } from "cloudflare:workers";
33
44 import {
5+ type AgentMessage,
56 type CheckJob,
67 type G1tEvent,
78 type Issue,
9798 | { kind: "update"; pullId?: string }
9899 /** The author sent back to address failed checks or a review. */
99100 | { kind: "revise"; pullId: string }
101+ /** The author woken to answer other agents; nothing to undo if it fails. */
102+ | { kind: "answer"; pullId: string }
100103 /** An agent turning an outcome into a plan. */
101104 | { kind: "plan"; planId: string; token: string }
102105 /** One combined state of a merge queue, being built and checked. */
161164 await work.failPlan(run.planId, run.token, "The sandbox stopped before the plan was written.");
162165 return;
163166 }
167+ // An answer that never came: the claim lapses and the asker reads the
168+ // change instead, as it was told it could.
169+ if (run.kind === "answer") return;
164170 if (run.kind === "update" || run.kind === "revise") {
165171 if (run.pullId) {
166172 await work.stall(
330336 return parts.filter(Boolean).join("\n\n");
331337 }
332338
339+/**
340+ * What the agent on a pull request is told when g1t wakes it to answer the
341+ * questions and handoffs other agents sent while it was not at work.
342+ */
343+function buildAnswerPrompt(job: LifecycleJob, messages: AgentMessage[], inFlight: string | null): string {
344+ const asked = messages
345+ .filter((message) => message.kind === "question" || message.kind === "handoff")
346+ .map((message) => {
347+ const from = message.fromNumber != null ? `the agent on #${message.fromNumber}` : message.author;
348+ const what = message.kind === "handoff" ? "Work handed over" : "Question";
349+ return `${what} from ${from} (id ${message.id}):\n${message.body}`;
350+ });
351+ const said = messages
352+ .filter((message) => message.kind === "message" || message.kind === "answer")
353+ .map((message) => `From ${message.fromNumber != null ? `the agent on #${message.fromNumber}` : message.author}: ${message.body}`);
354+ const parts = [
355+ `You are a coding agent working in the git repository checked out in the current directory. It holds a change you made earlier, which is open as pull request #${job.number}. Your work on it is done for now; you have been woken because other agents in this repository asked you something.`,
356+ job.issue
357+ ? `Your pull request is for issue #${job.issue.number}: ${job.issue.title}\n\n${job.issue.body}`
358+ : `Your pull request: ${job.title}`,
359+ job.description && `What you said you changed:\n\n${job.description}`,
360+ asked.join("\n\n"),
361+ said.length > 0 && `Also sent to you:\n\n${said.join("\n\n")}`,
362+ inFlight,
363+ WORKING_WITH_OTHERS,
364+ "Answer each question and handoff above with answer_message and its id, from what your change actually does: read your own code and history (git log, git diff against the default branch) before you answer, and be specific, with names, signatures and files. For a handoff, take it on only if the work belongs in your pull request; then make the change, commit it with a clear message, and answer saying what you did. Otherwise answer with decline set and say where it belongs. Do not push; that is done for you. Change nothing else. Finish with one or two plain sentences on what you answered.",
365+ ];
366+ return parts.filter(Boolean).join("\n\n");
367+}
368+
333369 function buildPrompt(
334370 issue: Issue,
335371 instructions: string,
654690 case "pull.merged":
655691 await this.advanceAll(event.data.repoId);
656692 break;
693+ // Another agent asked one that is not at work: wake it to answer.
694+ case "agent.asked":
695+ await this.wakeForMessages(event.data.pullId);
696+ break;
657697 // Something an issue was waiting on has finished, or an agent has
658698 // stopped and left room for another.
659699 case "issue.closed":
858898 return token;
859899 }
860900
901+ /**
902+ * Wakes the agent on a pull request to answer the questions and handoffs
903+ * other agents sent it while it was not at work. The work service claims
904+ * the step, so a second event starts nothing.
905+ */
906+ private async wakeForMessages(pullId: string): Promise<void> {
907+ const work = workClient(this.env.WORK);
908+ const wake = await work.wakeForMessages(pullId);
909+ if (!wake) return;
910+ const { job, messages } = wake;
911+ try {
912+ if (!this.modelsReachable() || !(await this.workspaceAllowed(job.repo.namespace))) {
913+ throw new Error("g1t agents are not enabled for this workspace.");
914+ }
915+ const { token } = await identityClient(this.env.IDENTITY).createAccessToken(
916+ job.author,
917+ `g1t agent answering on ${job.repo.namespace}/${job.repo.name}#${job.number}`,
918+ TOKEN_TTL_SECONDS,
919+ );
920+ const sandbox = this.env.SANDBOX.get(this.env.SANDBOX.idFromName(`answer-${job.pullId}-${messages[0]?.id ?? Date.now()}`));
921+ await sandbox.run({
922+ kind: "answer",
923+ pullId: job.pullId,
924+ envVars: {
925+ // Answered from its change as it stands: no merging in of the
926+ // default branch, which would push a commit for a question.
927+ MODE: "answer",
928+ G1T_API: "https://api.g1t.sh",
929+ G1T_TOKEN: token,
930+ G1T_USER: job.author.username,
931+ G1T_REPO: `${job.repo.namespace}/${job.repo.name}`,
932+ PULL_NUMBER: String(job.number),
933+ GIT_REMOTE: `https://g1t.sh/${job.source.namespace}/${job.source.name}.git`,
934+ COMMIT_MESSAGE: `Take on work handed over to #${job.number}`,
935+ G1T_AGENT_TOKEN: await this.agentToken(job.author, job.repo),
936+ PROMPT: buildAnswerPrompt(job, messages, await this.inFlight(job.author, job.repo, job.number)),
937+ ...(await this.modelEnvOrThrow("implement", job.repo, job.number)),
938+ },
939+ });
940+ } catch (error) {
941+ // Said on the pull request; the askers were told to read the change.
942+ await work.appendSession(job.author, job.repo, job.number, [
943+ {
944+ kind: "note",
945+ text: `g1t could not wake the agent to answer: ${error instanceof Error ? error.message : String(error)}`,
946+ },
947+ ]);
948+ }
949+ }
950+
861951 private async startRevision(job: LifecycleJob): Promise<void> {
862952 const { token } = await identityClient(this.env.IDENTITY).createAccessToken(
863953 job.author,
+1−0
18151815 "locate_pull" => reply(&work.locate_pull(args(body)?).await?),
18161816 "answer_message" => reply(&work.answer_message(args(body)?).await?),
18171817 "take_messages" => reply(&work.take_messages(args(body)?).await?),
1818+ "wake_for_messages" => reply(&work.wake_for_messages(args(body)?).await?),
18181819 "catch_up_job" => reply(&work.catch_up_job(args(body)?).await?),
18191820 "get_settings" => reply(&work.get_settings(args(body)?).await?),
18201821 "update_settings" => reply(&work.update_settings(args(body)?).await?),
+7−1
212212 "The agent is merging in the branch this will land on, which has moved.",
213213 );
214214 }
215+ Some("answer") => {
216+ return wait(
217+ Stage::Answering,
218+ "The agent is answering what another agent asked it.",
219+ );
220+ }
215221 Some("merge") => return wait(Stage::Ready, "Merging."),
216222 Some(_) => return wait(Stage::Reviewing, "A g1t agent is reviewing the change."),
217223 None => {}
563569
564570 /// Takes a step for a pull request, if nobody else has. One statement,
565571 /// so that two callers cannot both take it.
566− async fn claim(&self, pull_id: &str, step: &str, minutes: u64, revising: bool) -> Result<bool> {
572+ pub(crate) async fn claim(&self, pull_id: &str, step: &str, minutes: u64, revising: bool) -> Result<bool> {
567573 let now = now_ms();
568574 let revision = if revising {
569575 ", revisions = revisions + 1, revised_at = ?1"
+123−7
1818 /// Kinds an agent may send another.
1919 const ASKS: [&str; 2] = ["question", "handoff"];
2020
21+/// How long an agent woken to answer holds its pull request: nothing else
22+/// starts on it meanwhile. Answering everything lets go sooner.
23+const ANSWER_MINUTES: u64 = 20;
24+
2125 #[derive(Deserialize)]
2226 struct MessageRow {
2327 id: String,
196200 self.insert_message(&repo.id, &pull.id, &a.actor.id, &message).await?;
197201 let mut message = message;
198202 if from_agent && !at_work {
199− message.hint = Some(format!(
200− "The agent on #{} is not at work right now, so it will not answer soon. Its change is there to read: use get_pull_request and get_pull_request_changes on #{}, and decide from that.",
201− pull.number, pull.number
202− ));
203+ // An open pull request that g1t has not stopped on: its agent is
204+ // woken to answer (see `wake_for_messages`).
205+ let wakeable = pull.status == PullStatus::Open
206+ && self
207+ .db
208+ .prepare("SELECT 1 AS value FROM pulls WHERE id = ? AND stalled IS NULL")
209+ .bind(&[pull.id.as_str().into()])?
210+ .first::<u32>(Some("value"))
211+ .await?
212+ .is_some();
213+ message.hint = Some(if wakeable {
214+ self.publish("agent.asked", &repo.id, &a.actor, Self::pull_event(&pull)).await?;
215+ format!(
216+ "The agent on #{} was not at work, so g1t is waking it to answer; the answer reaches you at a later step. Its change is there to read meanwhile: get_pull_request and get_pull_request_changes on #{}.",
217+ pull.number, pull.number
218+ )
219+ } else {
220+ format!(
221+ "The agent on #{} is not at work right now, so it will not answer soon. Its change is there to read: use get_pull_request and get_pull_request_changes on #{}, and decide from that.",
222+ pull.number, pull.number
223+ )
224+ });
203225 }
204226 let said = match (message.kind.as_str(), message.from_number) {
205227 ("question", Some(from)) => format!("was asked a question by the agent on #{from}"),
288310 .await?;
289311 asked.answer = Some(body.clone());
290312 asked.declined = a.decline;
313+ // An agent woken to answer lets go of its pull request once nothing
314+ // it was asked is left unanswered.
315+ self.db
316+ .prepare(
317+ "UPDATE pulls SET working_on = NULL, working_until = NULL
318+ WHERE id = (SELECT pull_id FROM agent_messages WHERE id = ?1)
319+ AND working_on = 'answer'
320+ AND NOT EXISTS (
321+ SELECT 1 FROM agent_messages
322+ WHERE pull_id = pulls.id AND kind IN ('question', 'handoff') AND answer IS NULL)",
323+ )
324+ .bind(&[asked.id.as_str().into()])?
325+ .run()
326+ .await?;
291327 // Back to whoever asked: the agent on the other pull request.
292328 if let Some(from) = asked.from_number {
293329 if let Some(back) = self.pull(&repo.id, from).await?.filter(|pull| pull.status.is_active()) {
363399 Outcome::Ok(found) => found,
364400 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
365401 };
402+ Ok(Outcome::Ok(self.deliver(&pull).await?))
403+ }
404+
405+ /// Wakes the agent on a pull request to answer what it was asked while
406+ /// it was not at work: claims a short step, and hands over its messages.
407+ pub(crate) async fn wake_for_messages(&self, a: WakeForMessagesArgs) -> Result<Option<Wake>> {
408+ let Some(pull) = self.pull_by_id(&a.pull_id).await? else {
409+ return Ok(None);
410+ };
411+ // Only another agent's question or handoff wakes it; what people
412+ // say waits for its next step.
413+ let waiting = self
414+ .db
415+ .prepare(
416+ "SELECT 1 AS value FROM agent_messages
417+ WHERE pull_id = ? AND delivered_at IS NULL AND kind IN ('question', 'handoff')
418+ LIMIT 1",
419+ )
420+ .bind(&[pull.id.as_str().into()])?
421+ .first::<u32>(Some("value"))
422+ .await?
423+ .is_some();
424+ if !waiting || !self.claim(&pull.id, "answer", ANSWER_MINUTES, false).await? {
425+ return Ok(None);
426+ }
427+ let repo: Outcome<g1t_contracts::repos::Repo> = g1t_kit::call(
428+ &self.repos,
429+ "get_by_id",
430+ &g1t_contracts::repos::GetByIdArgs {
431+ id: pull.repo_id.clone(),
432+ viewer: self.author_viewer(&pull).await?,
433+ },
434+ )
435+ .await?;
436+ let Outcome::Ok(repo) = repo else {
437+ return Ok(None);
438+ };
439+ let path = g1t_contracts::repos::RepoPath {
440+ namespace: repo.namespace,
441+ name: repo.name,
442+ };
443+ let issue = match pull.issue {
444+ Some(number) => self.issue(&pull.repo_id, number).await?,
445+ None => None,
446+ };
447+ let messages = self.deliver(&pull).await?;
448+ let asking: Vec<String> = messages
449+ .iter()
450+ .filter_map(|message| message.from_number.map(|from| format!("#{from}")))
451+ .collect();
452+ self.note(
453+ &pull.repo_id,
454+ pull.number,
455+ (crate::lifecycle::POLICY_ACTOR_ID, crate::lifecycle::POLICY_ACTOR_NAME),
456+ &format!("woke g1t-agent to answer the agent on {}", asking.join(", ")),
457+ )
458+ .await?;
459+ Ok(Some(Wake {
460+ job: LifecycleJob {
461+ pull_id: pull.id,
462+ source: pull.fork.unwrap_or_else(|| path.clone()),
463+ repo: path,
464+ number: pull.number,
465+ author: pull.author,
466+ branch: pull.branch,
467+ default_branch: repo.default_branch,
468+ title: pull.title,
469+ description: pull.body.unwrap_or_default(),
470+ issue,
471+ feedback: String::new(),
472+ round: 0,
473+ },
474+ messages,
475+ }))
476+ }
477+
478+ /// Marks a pull request's undelivered messages delivered, records them
479+ /// in its session, and returns them, oldest first.
480+ async fn deliver(&self, pull: &Pull) -> Result<Vec<AgentMessage>> {
366481 let now = rfc3339(now_ms());
367− let taken: Vec<AgentMessage> = self
482+ let mut taken: Vec<AgentMessage> = self
368483 .db
369484 .prepare(
370485 "UPDATE agent_messages SET delivered_at = ?
378493 .into_iter()
379494 .map(AgentMessage::from)
380495 .collect();
496+ taken.sort_by(|a, b| a.created_at.cmp(&b.created_at));
381497 if !taken.is_empty() {
382498 let entries: Vec<NewSessionEntry> = taken
383499 .iter()
393509 commit: None,
394510 })
395511 .collect();
396− self.append_entries(&pull, &entries).await?;
512+ self.append_entries(pull, &entries).await?;
397513 }
398− Ok(Outcome::Ok(taken))
514+ Ok(taken)
399515 }
400516 }