{"version":3,"sources":["../src/agentPause.ts"],"sourcesContent":["// Pause + resume for a run whose surface the user cannot see (R3-562;\n// AGENT_RUN_DURABILITY_SPEC §7 R-ARD-20a).\n//\n// WHY. The host now keeps a chrome region MOUNTED and merely hides it when the user\n// switches activity, so coming back does not reboot the app's iframe and destroy the\n// run (§7 R-ARD-20 — the T1 teardown). But hiding stops a frame PAINTING, not\n// executing, and a loop that keeps executing behind `display:none` + `inert` breaks a\n// requirement `LLM_AND_AGENTS_SPEC` §3.3 calls normative: the **loop observability\n// contract** wants a streaming transcript, a tool-call log rendered in order, a\n// reachable **stop button**, and every write visible at or before contribute. None of\n// that survives an inert subtree. It also contradicts R-ARD-15 one section earlier,\n// which forbids an agent writing files with no human present.\n//\n// So: kept-mounted, not kept-executing. The run pauses at its next turn boundary and\n// continues when the region is revealed. It costs nothing — the frame, the in-memory\n// loop state and the transcript all survive, so a reveal continues with zero waste, no\n// repair pass and no resume gate. The run is never torn down; it is only not advancing\n// while nobody can see or stop it.\n//\n// THE THREE VERBS ARE NOT THE SAME.\n//   * STOP   — `RunAgentOptions.signal`. Ends the run. The transcript so far is kept.\n//   * STEER  — `agentSteering.ts`. Ends the current TURN, injects a correction, CONTINUES.\n//   * PAUSE  — this controller. Ends nothing and injects nothing: the loop simply does\n//              not start its next turn until the pause lifts. The user did not ask for\n//              it and never sees it as an interruption — it is the host's fact about\n//              whether anyone is watching, so it leaves no mark in the transcript.\n//\n// WHY IT NEVER INTERRUPTS A TURN. The loop checks this at the TOP of an iteration, after\n// the previous iteration's tool batch has appended every `tool_result`. Pausing anywhere\n// else could leave a `tool_use` block without its pair, and the next request would then be\n// malformed and rejected by the provider for the whole conversation — the same property\n// `agentSteering` states for an interrupt, and for the same reason.\n\n/** What the loop needs from a pause source (so it can be faked in tests). */\nexport interface PauseSource {\n  /** Is the run currently not allowed to advance? Read at each turn boundary. */\n  isPaused(): boolean;\n  /**\n   * Resolves when the pause lifts — or immediately if it is already lifted.\n   *\n   * `signal` is the RUN's stop signal, and passing it is not optional in practice: a run\n   * stopped (or signed out) while its region is hidden would otherwise await a resume\n   * that never comes, and the awaited promise would keep the whole loop — transcript,\n   * tools, client — alive for the life of the tab. Resolving on abort hands control back\n   * to the loop, which re-checks `signal.aborted` and breaks.\n   */\n  whenResumed(signal?: AbortSignal): Promise<void>;\n}\n\n/**\n * The live pause state. Owned by the app (which sets it from the host's\n * `onRegionVisibilityChange`) and read by the loop.\n *\n * Deliberately NOT wired to the visibility channel in here: the controller is the\n * mechanism and knows nothing about why a run is paused, which keeps it usable for a\n * second reason later (a phone backgrounding, a user \"hold\" button) without this file\n * having an opinion about which reasons exist.\n */\nexport class PauseController implements PauseSource {\n  private paused = false;\n  private waiters = new Set<() => void>();\n  private listeners = new Set<(paused: boolean) => void>();\n\n  /** Set the pause state. Idempotent — setting the value it already holds does nothing. */\n  set(paused: boolean): void {\n    if (this.paused === paused) return;\n    this.paused = paused;\n    if (!paused) {\n      // Copy before draining: a waiter's continuation may synchronously pause again,\n      // and iterating the live set while it is being added to is how that turns into a\n      // lost or double-fired wake.\n      const waiting = [...this.waiters];\n      this.waiters.clear();\n      for (const w of waiting) w();\n    }\n    for (const l of this.listeners) l(this.paused);\n  }\n\n  isPaused(): boolean {\n    return this.paused;\n  }\n\n  whenResumed(signal?: AbortSignal): Promise<void> {\n    if (!this.paused || signal?.aborted) return Promise.resolve();\n    return new Promise<void>((resolve) => {\n      const done = (): void => {\n        this.waiters.delete(done);\n        signal?.removeEventListener('abort', done);\n        resolve();\n      };\n      this.waiters.add(done);\n      signal?.addEventListener('abort', done, { once: true });\n    });\n  }\n\n  /** Subscribe to pause changes (a UI showing \"paused — this view is hidden\"). */\n  onChange(listener: (paused: boolean) => void): () => void {\n    this.listeners.add(listener);\n    return () => {\n      this.listeners.delete(listener);\n    };\n  }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA0DO,MAAM,gBAAuC;AAAA,EAA7C;AACL,SAAQ,SAAS;AACjB,SAAQ,UAAU,oBAAI,IAAgB;AACtC,SAAQ,YAAY,oBAAI,IAA+B;AAAA;AAAA;AAAA,EAGvD,IAAI,QAAuB;AACzB,QAAI,KAAK,WAAW,OAAQ;AAC5B,SAAK,SAAS;AACd,QAAI,CAAC,QAAQ;AAIX,YAAM,UAAU,CAAC,GAAG,KAAK,OAAO;AAChC,WAAK,QAAQ,MAAM;AACnB,iBAAW,KAAK,QAAS,GAAE;AAAA,IAC7B;AACA,eAAW,KAAK,KAAK,UAAW,GAAE,KAAK,MAAM;AAAA,EAC/C;AAAA,EAEA,WAAoB;AAClB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,YAAY,QAAqC;AAC/C,QAAI,CAAC,KAAK,UAAU,QAAQ,QAAS,QAAO,QAAQ,QAAQ;AAC5D,WAAO,IAAI,QAAc,CAAC,YAAY;AACpC,YAAM,OAAO,MAAY;AACvB,aAAK,QAAQ,OAAO,IAAI;AACxB,gBAAQ,oBAAoB,SAAS,IAAI;AACzC,gBAAQ;AAAA,MACV;AACA,WAAK,QAAQ,IAAI,IAAI;AACrB,cAAQ,iBAAiB,SAAS,MAAM,EAAE,MAAM,KAAK,CAAC;AAAA,IACxD,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,SAAS,UAAiD;AACxD,SAAK,UAAU,IAAI,QAAQ;AAC3B,WAAO,MAAM;AACX,WAAK,UAAU,OAAO,QAAQ;AAAA,IAChC;AAAA,EACF;AACF;","names":[]}