feat(sessions): deliver ask_user answers

Integrate the pending-ask store into the session service so an open ask is
visible, observable, and closable.

- `statusFromSession` projects `pendingAsk`, so a browser rehydrates an open
  ask from `GET /sessions/:sessionId/status` after reload or a web/API restart.
- `openAsk` publishes `ask.opened`, and publishes `ask.closed` first when the
  new ask supersedes an unanswered one.
- `submitAsk` / `cancelAsk` close the ask and hand the outcome to the model as
  a `pi-web.ask.answers` follow-up custom message (`triggerTurn`,
  `deliverAs: "followUp"`), the same delivery subsession notices use. A stale
  ask id is reported, not thrown: losing the race against a supersede or
  another browser is ordinary. Cancel still reports every question as
  unanswered so the model is not left waiting for a promised message.
- The open ask is forgotten when its runtime closes; nothing is left to
  receive the answers.
- `POST /sessions/:sessionId/ask/{submit,cancel}` behind the existing
  `/api/sessions/*` daemon proxy, allowlisted for machine federation.
This commit is contained in:
Federico Jaramillo Martinez
2026-07-26 22:38:15 +02:00
parent 07bdd7ad6b
commit 51ebfe4c00
7 changed files with 451 additions and 12 deletions
+21
View File
@@ -440,6 +440,12 @@ export interface QueuedSessionMessage {
text: string;
}
/**
* `customType` of the follow-up custom message that carries a closed ask back to
* the model and into the transcript. Its `details` are an {@link AskUserOutcome}.
*/
export const ASK_USER_ANSWERS_CUSTOM_TYPE = "pi-web.ask.answers";
/** Largest question set one `ask_user` call may post. */
export const ASK_USER_QUESTION_LIMIT = 20;
/** Largest option list one question may offer. */
@@ -544,6 +550,21 @@ export interface AskUserOutcome {
summary: string;
}
/**
* Result of the browser closing an ask by submitting or cancelling it.
*
* `"stale"` is an ordinary race rather than an error: the named ask was already
* submitted, superseded by a newer one, or gone with its session runtime. The
* browser drops its card and trusts `sessionStatus`, which is returned in both
* cases so closing an ask needs no follow-up status request.
*/
export interface AskUserCloseResponse {
result: "closed" | "stale";
/** Present only when this call is the one that closed the ask. */
outcome?: AskUserOutcome;
sessionStatus: SessionStatus;
}
/**
* Progress of the session startup window, where the daemon is still
* constructing the agent session and no `PiAgentSession` exists yet, so