github-router 0.3.150 → 0.3.151

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.
@@ -2673,6 +2673,464 @@ var MergedFleetRegistry = class {
2673
2673
  }
2674
2674
  };
2675
2675
 
2676
+ //#endregion
2677
+ //#region src/lib/fleet/driver.ts
2678
+ /** ai-or-die named keys (raw:false maps these; never a literal "\r"). C4. */
2679
+ const SUBMIT_KEY = "enter";
2680
+ const INTERRUPT_KEY = "ctrl-c";
2681
+ /** The control events that RELIABLY mark a turn boundary (transcript-derived). */
2682
+ const TURN_SETTLE_KINDS = ["turn_ended", "waiting_input"];
2683
+ const DEFAULT_READY_POLL_MS = 500;
2684
+ const DEFAULT_TURN_POLL_MS = 25e3;
2685
+ const DEFAULT_PRIME_TIMEOUT_MS = 0;
2686
+ const DEFAULT_IDLE_WAIT_MS = 2e3;
2687
+ const DEFAULT_RECOVER_MS = 15e3;
2688
+ const DEFAULT_TAIL_LINES = 200;
2689
+ function realSleep(ms) {
2690
+ if (ms <= 0) return Promise.resolve();
2691
+ return new Promise((resolve) => setTimeout(resolve, ms));
2692
+ }
2693
+ /** Map a named op to the ai-or-die named key. `submit` = Enter, `interrupt` = Ctrl-C.
2694
+ * Callers send the returned key with raw:false so ai-or-die interprets the NAMED
2695
+ * key — never a literal control byte. */
2696
+ function mapNamedKeyOp(op) {
2697
+ switch (op) {
2698
+ case "submit": return SUBMIT_KEY;
2699
+ case "interrupt": return INTERRUPT_KEY;
2700
+ }
2701
+ }
2702
+ function isNamedKeyOp(value) {
2703
+ return value === "submit" || value === "interrupt";
2704
+ }
2705
+ /** Read `awaiting.kind` off a status defensively (the shape is `{ kind, ... }`). */
2706
+ function readAwaitingKind(status) {
2707
+ const awaiting = status?.awaiting;
2708
+ if (awaiting && typeof awaiting === "object") {
2709
+ const kind = awaiting.kind;
2710
+ if (typeof kind === "string" && kind.trim() !== "") return kind;
2711
+ }
2712
+ }
2713
+ /**
2714
+ * Classify whether a free-text message may be submitted to a session RIGHT NOW.
2715
+ * Ready only when the session is idle or explicitly awaiting the next message.
2716
+ * A pending non-message prompt (plan_approval / choice_question / tool_approval /
2717
+ * trust_prompt) is `awaiting_other` — the caller should use `respond`, not a raw
2718
+ * message. `busy` / `terminal` are hard refusals; `unknown` carries no positive
2719
+ * evidence of a busy composer (the caller fails OPEN and lets the send surface its
2720
+ * own transport result).
2721
+ */
2722
+ function classifyMessageReadiness(status) {
2723
+ const interactionState = typeof status?.interactionState === "string" ? status.interactionState : void 0;
2724
+ const awaitingKind = readAwaitingKind(status);
2725
+ if (interactionState === "busy") return {
2726
+ ready: false,
2727
+ reason: "busy",
2728
+ interactionState,
2729
+ awaitingKind
2730
+ };
2731
+ if (interactionState === "exited" || interactionState === "crashed") return {
2732
+ ready: false,
2733
+ reason: "terminal",
2734
+ interactionState
2735
+ };
2736
+ if (awaitingKind !== void 0 && awaitingKind !== "next_message") return {
2737
+ ready: false,
2738
+ reason: "awaiting_other",
2739
+ interactionState,
2740
+ awaitingKind
2741
+ };
2742
+ if (interactionState === "idle") return {
2743
+ ready: true,
2744
+ reason: "idle",
2745
+ interactionState
2746
+ };
2747
+ if (awaitingKind === "next_message" || interactionState === "waiting_input") return {
2748
+ ready: true,
2749
+ reason: "awaiting_message",
2750
+ interactionState,
2751
+ awaitingKind
2752
+ };
2753
+ return {
2754
+ ready: false,
2755
+ reason: "unknown",
2756
+ interactionState,
2757
+ awaitingKind
2758
+ };
2759
+ }
2760
+ /** A refusal reason with POSITIVE evidence the composer must not be typed into. */
2761
+ function isHardNotReady(reason) {
2762
+ return reason === "busy" || reason === "awaiting_other" || reason === "terminal";
2763
+ }
2764
+ /**
2765
+ * Poll `/status` until the session is ready for a message or the wait budget is
2766
+ * spent. A status probe that THROWS fails OPEN (reports `unknown` and returns) so a
2767
+ * transient status hiccup never wedges a legitimate send — the send itself carries
2768
+ * ai-or-die's own submission signal.
2769
+ */
2770
+ async function waitForMessageReady(client, localId, options = {}) {
2771
+ const now = options.now ?? Date.now;
2772
+ const sleep$1 = options.sleep ?? realSleep;
2773
+ const waitMs = Math.max(0, options.waitMs ?? 0);
2774
+ const pollMs = Math.max(1, options.pollMs ?? DEFAULT_READY_POLL_MS);
2775
+ const deadline = now() + waitMs;
2776
+ let last = {
2777
+ ready: false,
2778
+ reason: "unknown"
2779
+ };
2780
+ for (;;) {
2781
+ let status;
2782
+ try {
2783
+ status = (await client.status(localId, options.signal)).status;
2784
+ } catch {
2785
+ return {
2786
+ ready: false,
2787
+ readiness: {
2788
+ ready: false,
2789
+ reason: "unknown"
2790
+ },
2791
+ statusError: true
2792
+ };
2793
+ }
2794
+ last = classifyMessageReadiness(status);
2795
+ if (last.ready) return {
2796
+ ready: true,
2797
+ readiness: last
2798
+ };
2799
+ const remaining = deadline - now();
2800
+ if (remaining <= 0) return {
2801
+ ready: false,
2802
+ readiness: last
2803
+ };
2804
+ await sleep$1(Math.min(pollMs, remaining));
2805
+ }
2806
+ }
2807
+ /**
2808
+ * Classify a batch of already-stamped events per session for the `await_turn`
2809
+ * summary. `turn_ended` -> completed, `waiting_input` -> awaiting_input (both
2810
+ * reliable / transcript-derived); a bare `became_idle` is surfaced as `idle_flicker`
2811
+ * with reliable:false so a caller NEVER mistakes the PTY heuristic for completion.
2812
+ * `became_busy` and other kinds are ignored (not a settle signal).
2813
+ */
2814
+ function classifyTurnEvents(events$1) {
2815
+ const rank = {
2816
+ completed: 3,
2817
+ awaiting_input: 2,
2818
+ idle_flicker: 1
2819
+ };
2820
+ const best = /* @__PURE__ */ new Map();
2821
+ for (const event of events$1) {
2822
+ const sessionId = typeof event.sessionId === "string" ? event.sessionId : void 0;
2823
+ const kind = typeof event.kind === "string" ? event.kind : void 0;
2824
+ if (sessionId === void 0 || kind === void 0) continue;
2825
+ let status;
2826
+ if (kind === "turn_ended") status = "completed";
2827
+ else if (kind === "waiting_input") status = "awaiting_input";
2828
+ else if (kind === "became_idle") status = "idle_flicker";
2829
+ if (status === void 0) continue;
2830
+ const prior = best.get(sessionId);
2831
+ if (prior === void 0 || rank[status] > rank[prior]) best.set(sessionId, status);
2832
+ }
2833
+ return [...best.entries()].map(([sessionId, status]) => ({
2834
+ sessionId,
2835
+ status,
2836
+ reliable: status !== "idle_flicker"
2837
+ }));
2838
+ }
2839
+ function pickSettleEvent(events$1, localId) {
2840
+ let awaiting;
2841
+ for (const event of events$1) {
2842
+ if (event.sessionId !== localId) continue;
2843
+ if (event.kind === "turn_ended") return event;
2844
+ if (event.kind === "waiting_input" && awaiting === void 0) awaiting = event;
2845
+ }
2846
+ return awaiting;
2847
+ }
2848
+ /** Client-side backoff after a transient waitEvents failure so the loop cannot
2849
+ * hot-spin (hammer the control plane) when polls fail fast (network down). */
2850
+ const TURN_POLL_ERROR_BACKOFF_MS = 250;
2851
+ /** Attempts to obtain a starting cursor before a drive send (see driveTask step 2). */
2852
+ const PRIME_CURSOR_ATTEMPTS = 3;
2853
+ /**
2854
+ * Obtain a starting `/events` cursor before sending, retrying a bounded number of
2855
+ * times. A cursor is what prevents a stale prior-turn `turn_ended` from satisfying
2856
+ * the post-send wait; a cursorless request may replay history. Retries are cheap
2857
+ * (a zero/short poll). Returns undefined only if every attempt fails (rare) or the
2858
+ * signal aborts — the caller then proceeds best-effort.
2859
+ */
2860
+ async function primeTurnCursor(client, localId, timeoutMs, signal) {
2861
+ for (let attempt = 0; attempt < PRIME_CURSOR_ATTEMPTS; attempt++) {
2862
+ if (signal?.aborted) return void 0;
2863
+ try {
2864
+ return (await client.waitEvents({
2865
+ sessionIds: [localId],
2866
+ kinds: [...TURN_SETTLE_KINDS],
2867
+ timeoutMs
2868
+ }, signal)).cursor;
2869
+ } catch {}
2870
+ }
2871
+ }
2872
+ /**
2873
+ * Wait until the CURRENT turn actually ends (`turn_ended`) or the session is
2874
+ * awaiting input (`waiting_input`), long-polling `/events` filtered to those kinds
2875
+ * and advancing the server cursor across windows. Returns `{settled:false,
2876
+ * reason:"timeout"}` when neither fires within `timeoutMs`. It NEVER settles on
2877
+ * `became_idle`. Prime a cursor BEFORE the send (a zero/short poll) and pass it in
2878
+ * so a stale prior-turn event cannot satisfy the wait.
2879
+ */
2880
+ async function waitForTurnSettled(client, localId, options) {
2881
+ const now = options.now ?? Date.now;
2882
+ const sleep$1 = options.sleep ?? realSleep;
2883
+ const pollTimeoutMs = Math.max(1, options.pollTimeoutMs ?? DEFAULT_TURN_POLL_MS);
2884
+ const budget = Number.isFinite(options.timeoutMs) ? Math.max(0, options.timeoutMs) : 0;
2885
+ const deadline = now() + budget;
2886
+ let cursor = options.cursor;
2887
+ do {
2888
+ if (options.signal?.aborted) return {
2889
+ settled: false,
2890
+ reason: "aborted",
2891
+ cursor
2892
+ };
2893
+ const remaining = deadline - now();
2894
+ const poll = remaining <= 0 ? 0 : Math.min(pollTimeoutMs, remaining);
2895
+ let response;
2896
+ try {
2897
+ response = await client.waitEvents({
2898
+ sessionIds: [localId],
2899
+ kinds: [...TURN_SETTLE_KINDS],
2900
+ timeoutMs: poll,
2901
+ cursor
2902
+ }, options.signal);
2903
+ } catch {
2904
+ if (options.signal?.aborted) return {
2905
+ settled: false,
2906
+ reason: "aborted",
2907
+ cursor
2908
+ };
2909
+ if (now() >= deadline) return {
2910
+ settled: false,
2911
+ reason: "timeout",
2912
+ cursor
2913
+ };
2914
+ await sleep$1(Math.min(TURN_POLL_ERROR_BACKOFF_MS, Math.max(0, deadline - now())));
2915
+ continue;
2916
+ }
2917
+ cursor = response.cursor;
2918
+ const hit = pickSettleEvent(response.events, localId);
2919
+ if (hit) return {
2920
+ settled: true,
2921
+ reason: hit.kind === "turn_ended" ? "turn_ended" : "waiting_input",
2922
+ cursor,
2923
+ event: hit
2924
+ };
2925
+ } while (now() < deadline);
2926
+ return {
2927
+ settled: false,
2928
+ reason: "timeout",
2929
+ cursor
2930
+ };
2931
+ }
2932
+ const OPERATOR_REPORT_HEADER = "=== OPERATOR REPORT ===";
2933
+ const OPERATOR_REPORT_FOOTER = "=== END OPERATOR REPORT ===";
2934
+ const OPERATOR_REPORT_LABELS = [
2935
+ "REPORT_ID",
2936
+ "STATE",
2937
+ "SUMMARY",
2938
+ "ASK",
2939
+ "ARTIFACT"
2940
+ ];
2941
+ /**
2942
+ * Parse the LAST OPERATOR REPORT trailer in a transcript tail into typed fields.
2943
+ * Robust to: no trailer (returns state:"unknown", found:false), a missing footer,
2944
+ * multi-line field values (a value runs until the next known label or the footer),
2945
+ * and case-insensitive labels. Lines before the first label are ignored.
2946
+ */
2947
+ function parseOperatorReport(text) {
2948
+ const raw = text ?? "";
2949
+ const headerIdx = raw.lastIndexOf(OPERATOR_REPORT_HEADER);
2950
+ if (headerIdx === -1) return {
2951
+ state: "unknown",
2952
+ raw,
2953
+ found: false
2954
+ };
2955
+ let block = raw.slice(headerIdx + 23);
2956
+ const footerIdx = block.indexOf(OPERATOR_REPORT_FOOTER);
2957
+ if (footerIdx !== -1) block = block.slice(0, footerIdx);
2958
+ const values = {};
2959
+ let current;
2960
+ for (const line of block.split(/\r?\n/)) {
2961
+ const match = /^\s*([A-Za-z_]+)\s*:\s*(.*)$/.exec(line);
2962
+ const label = match?.[1]?.toUpperCase();
2963
+ if (match && label && OPERATOR_REPORT_LABELS.includes(label)) {
2964
+ current = label;
2965
+ values[current] = [match[2] ?? ""];
2966
+ } else if (current) values[current].push(line);
2967
+ }
2968
+ const join$1 = (key) => {
2969
+ const parts = values[key];
2970
+ if (parts === void 0) return void 0;
2971
+ const joined = parts.join("\n").trim();
2972
+ if (joined === "") return void 0;
2973
+ if (/^<[^>\n]*\s[^>\n]*>$/.test(joined)) return void 0;
2974
+ return joined;
2975
+ };
2976
+ return {
2977
+ state: join$1("STATE") ?? "unknown",
2978
+ summary: join$1("SUMMARY"),
2979
+ ask: join$1("ASK"),
2980
+ artifact: join$1("ARTIFACT"),
2981
+ reportId: join$1("REPORT_ID"),
2982
+ raw,
2983
+ found: true
2984
+ };
2985
+ }
2986
+ /** The trailer instruction `drive_task` appends when `expectReport` is on, so a
2987
+ * driven session ends its turn with a parseable {@link parseOperatorReport} block.
2988
+ * When `reportId` is supplied it is embedded as a `REPORT_ID` line the session must
2989
+ * copy verbatim, so the driver can confirm the parsed report is for THIS turn and not
2990
+ * a stale prior-turn trailer still inside the transcript tail window. */
2991
+ function operatorReportInstruction(reportId) {
2992
+ const lines = [
2993
+ "",
2994
+ reportId !== void 0 ? "When you have completely finished this task, end your FINAL message with EXACTLY this trailer. Copy the REPORT_ID line verbatim and fill in each other field:" : "When you have completely finished this task, end your FINAL message with EXACTLY this trailer, filling in each field:",
2995
+ OPERATOR_REPORT_HEADER
2996
+ ];
2997
+ if (reportId !== void 0) lines.push(`REPORT_ID: ${reportId}`);
2998
+ lines.push("STATE: <done | blocked | needs_input | in_progress>", "SUMMARY: <1-3 sentence summary of what you did>", "ASK: <what you need from the operator, or 'none'>", "ARTIFACT: <path or URL to the primary artifact, or 'none'>", OPERATOR_REPORT_FOOTER);
2999
+ return lines.join("\n");
3000
+ }
3001
+ function notReadyState(reason) {
3002
+ switch (reason) {
3003
+ case "busy": return "busy";
3004
+ case "awaiting_other": return "awaiting_other";
3005
+ case "terminal": return "dead";
3006
+ default: return "unknown";
3007
+ }
3008
+ }
3009
+ async function readTail(client, localId, lines, signal) {
3010
+ try {
3011
+ const response = await client.readSession(localId, lines, signal);
3012
+ return typeof response.text === "string" ? response.text : "";
3013
+ } catch {
3014
+ return "";
3015
+ }
3016
+ }
3017
+ /**
3018
+ * Drive one prompt on a session to completion and return the parsed operator report.
3019
+ * Flow: ensure the composer is idle (C1) -> send + surface whether the bytes reached
3020
+ * the composer (C2 `submitted`; a delivered-but-unconfirmed send still proceeds, since
3021
+ * the turn wait + timeout recovery covers the "nothing landed" case) -> wait for
3022
+ * `turn_ended`/`waiting_input` (C2) -> read the transcript tail -> parse the operator
3023
+ * report trailer -> if the turn did not settle in `timeoutMs`, AUTO-RECOVER via a
3024
+ * Ctrl-C interrupt (C4) rather than blocking (~10 min stop-hook hang), then re-wait
3025
+ * briefly and re-read. Robust to a busy session, a missing trailer (state:"unknown"),
3026
+ * and a hung stop hook.
3027
+ */
3028
+ async function driveTask(deps) {
3029
+ const { client, localId, prompt, timeoutMs, expectReport, idempotencyKey, interruptKey, reportId, signal } = deps;
3030
+ const now = deps.now ?? Date.now;
3031
+ const sleep$1 = deps.sleep ?? realSleep;
3032
+ const tailLines = deps.tailLines ?? DEFAULT_TAIL_LINES;
3033
+ const pollTimeoutMs = deps.pollTimeoutMs;
3034
+ const readyResult = await waitForMessageReady(client, localId, {
3035
+ waitMs: deps.idleWaitMs ?? DEFAULT_IDLE_WAIT_MS,
3036
+ now,
3037
+ sleep: sleep$1,
3038
+ signal
3039
+ });
3040
+ if (!readyResult.ready && isHardNotReady(readyResult.readiness.reason)) return {
3041
+ submitted: false,
3042
+ delivered: false,
3043
+ settled: "timeout",
3044
+ state: notReadyState(readyResult.readiness.reason),
3045
+ raw: "",
3046
+ reportFound: false,
3047
+ interrupted: false,
3048
+ recovered: false,
3049
+ notReady: true,
3050
+ readiness: readyResult.readiness,
3051
+ error: "not_ready"
3052
+ };
3053
+ let cursor = await primeTurnCursor(client, localId, deps.primeTimeoutMs ?? DEFAULT_PRIME_TIMEOUT_MS, signal);
3054
+ const cursorPrimed = cursor !== void 0;
3055
+ const message = expectReport ? `${prompt}\n${operatorReportInstruction(reportId)}` : prompt;
3056
+ const send = await client.sendMessage(localId, {
3057
+ message,
3058
+ idempotencyKey,
3059
+ awaitMs: 0
3060
+ }, signal);
3061
+ const submitted = send.submission?.status === "submitted";
3062
+ if (send.delivered === false || send.delivery?.status === "failed" || send.delivery?.status === "error") return {
3063
+ submitted: false,
3064
+ delivered: false,
3065
+ settled: "timeout",
3066
+ state: "send_failed",
3067
+ raw: "",
3068
+ reportFound: false,
3069
+ interrupted: false,
3070
+ recovered: false,
3071
+ cursorPrimed,
3072
+ sendConfirmation: send.confirmation,
3073
+ error: "delivery_failed"
3074
+ };
3075
+ let settle = await waitForTurnSettled(client, localId, {
3076
+ timeoutMs,
3077
+ pollTimeoutMs,
3078
+ cursor,
3079
+ now,
3080
+ sleep: sleep$1,
3081
+ signal
3082
+ });
3083
+ cursor = settle.cursor;
3084
+ const isCurrentReport = (r) => expectReport && r.found && r.reportId !== void 0 && r.reportId === reportId;
3085
+ let tail = signal?.aborted ? "" : await readTail(client, localId, tailLines, signal);
3086
+ let report = parseOperatorReport(tail);
3087
+ let interrupted = false;
3088
+ let recovered = false;
3089
+ const aborted = settle.reason === "aborted" || signal?.aborted === true;
3090
+ if (!settle.settled && !aborted) {
3091
+ interrupted = true;
3092
+ await client.sendKeys(localId, {
3093
+ keys: INTERRUPT_KEY,
3094
+ idempotencyKey: interruptKey,
3095
+ raw: false
3096
+ }, signal).catch(() => {});
3097
+ const recovery = await waitForTurnSettled(client, localId, {
3098
+ timeoutMs: deps.recoverTimeoutMs ?? DEFAULT_RECOVER_MS,
3099
+ pollTimeoutMs,
3100
+ cursor,
3101
+ now,
3102
+ sleep: sleep$1,
3103
+ signal
3104
+ });
3105
+ recovered = recovery.settled;
3106
+ if (recovery.settled) settle = recovery;
3107
+ const recoveredTail = await readTail(client, localId, tailLines, signal);
3108
+ const recoveredReport = parseOperatorReport(recoveredTail);
3109
+ if (recoveredTail !== "" && (isCurrentReport(recoveredReport) || !isCurrentReport(report))) {
3110
+ tail = recoveredTail;
3111
+ report = recoveredReport;
3112
+ }
3113
+ }
3114
+ const settledReason = settle.settled ? settle.reason : settle.reason === "aborted" || signal?.aborted === true ? "aborted" : "timeout";
3115
+ const settleDerivedState = settledReason === "waiting_input" ? "awaiting_input" : settledReason === "aborted" ? "aborted" : settledReason === "timeout" ? "timeout" : "unknown";
3116
+ const reportCurrent = isCurrentReport(report);
3117
+ return {
3118
+ submitted,
3119
+ delivered: true,
3120
+ settled: settledReason,
3121
+ state: settledReason === "waiting_input" ? "awaiting_input" : reportCurrent && report.state !== "unknown" ? report.state : settleDerivedState,
3122
+ summary: reportCurrent ? report.summary : void 0,
3123
+ ask: reportCurrent ? report.ask : void 0,
3124
+ artifact: reportCurrent ? report.artifact : void 0,
3125
+ raw: tail,
3126
+ reportFound: reportCurrent,
3127
+ interrupted,
3128
+ recovered,
3129
+ cursorPrimed,
3130
+ sendConfirmation: send.confirmation
3131
+ };
3132
+ }
3133
+
2676
3134
  //#endregion
2677
3135
  //#region src/lib/fleet/tools.ts
2678
3136
  const FLEET_GROUP = "fleet";
@@ -2681,6 +3139,7 @@ const INSTANCE_PROBE_CACHE_TTL_MS = 5e3;
2681
3139
  const CAPABILITIES_CACHE_TTL_MS = 6e4;
2682
3140
  const AWAIT_TURN_DEFAULT_TIMEOUT_MS = 3e4;
2683
3141
  const AWAIT_TURN_TIMEOUT_SLACK_MS = 5e3;
3142
+ const DRIVE_TASK_DEFAULT_TIMEOUT_MS = 12e4;
2684
3143
  const LIST_INSTANCES_FANOUT_CONCURRENCY = 16;
2685
3144
  const AWAIT_TURN_FANOUT_CONCURRENCY = 256;
2686
3145
  const INSTANCE_PROBE_RATE_LIMIT_MAX_RETRIES = 1;
@@ -2874,22 +3333,47 @@ function createFleetTools(options = {}) {
2874
3333
  sessionId: globalId
2875
3334
  });
2876
3335
  }),
2877
- tool$1("send_message", "Send a message to a fleet session. isError reflects DELIVERY ONLY: it is true only when the message could not be delivered to the session (transport/precondition failure). A delivered message whose confirmation did not arrive within awaitMs is NOT an error — it returns delivered:true with confirmationPending/confirmationTimedOut, because a long turn legitimately outruns awaitMs. Recommended pattern: send with awaitMs:0 for a fast delivery ack that never blocks on confirmation, then call await_turn (filtered to this sessionId) to observe the session's actual turn completion. The idempotencyKey makes a retried send safe (a retry never re-types the message).", objectSchema$1({
3336
+ tool$1("send_message", "Send a message to a fleet session. By DEFAULT it first checks the session is idle / awaiting the next message and REFUSES (structured notReady, isError) rather than blind-type into a busy composer or a pending prompt — set requireIdle:false to force the legacy unconditional send, or waitForIdleMs to wait briefly for idle first. isError reflects DELIVERY: true when the message was not delivered (transport/precondition failure) OR refused as notReady. A delivered message whose confirmation did not arrive within awaitMs is NOT an error — it returns delivered:true with confirmationPending/confirmationTimedOut, because a long turn legitimately outruns awaitMs. The additive `submitted` field is true only when ai-or-die's submission sub-status proves the message reached the composer. Recommended pattern: send with awaitMs:0 for a fast delivery ack, then call await_turn (filtered to this sessionId) to observe the session's actual turn completion. The idempotencyKey makes a retried send safe (a retry never re-types the message).", objectSchema$1({
2878
3337
  sessionId: stringProp$1("Global session id in the form instanceId:localSessionId."),
2879
3338
  instance: stringProp$1("Optional instance id/label; when supplied it must agree with sessionId."),
2880
3339
  message: stringProp$1("Message text to deliver to the session."),
2881
3340
  idempotencyKey: stringProp$1("Optional caller idempotency key; AUTO-GENERATED when omitted, so you normally never pass it. Supply your OWN stable key only when you will retry the SAME send and need the upstream to dedupe it."),
2882
- awaitMs: numberProp$1("Optional best-effort confirmation wait (ms) — NOT a deadline. Prefer awaitMs:0 plus await_turn; a turn that outruns awaitMs returns confirmationPending, not an error.")
3341
+ awaitMs: numberProp$1("Optional best-effort confirmation wait (ms) — NOT a deadline. Prefer awaitMs:0 plus await_turn; a turn that outruns awaitMs returns confirmationPending, not an error."),
3342
+ requireIdle: booleanProp("Default true: check status and refuse a busy/awaiting-prompt/dead session with a structured notReady result. Set false to force an unconditional send (unsafe: may type into a busy composer)."),
3343
+ waitForIdleMs: numberProp$1("When requireIdle, wait up to this many ms for the session to become idle before deciding (default 0 = decide immediately).")
2883
3344
  }, ["sessionId", "message"]), async (args, signal) => {
2884
3345
  const { instance, localId, globalId } = await resolveSession(args);
2885
3346
  const awaitMs = optionalNumber$1(args, "awaitMs");
2886
- const response = await clientFor(instance).sendMessage(localId, {
3347
+ const requireIdle = optionalBoolean(args, "requireIdle") ?? true;
3348
+ const client = clientFor(instance);
3349
+ if (requireIdle) {
3350
+ const readyResult = await waitForMessageReady(client, localId, {
3351
+ waitMs: optionalNumber$1(args, "waitForIdleMs") ?? 0,
3352
+ signal
3353
+ });
3354
+ if (!readyResult.ready && isHardNotReady(readyResult.readiness.reason)) {
3355
+ const advise = readyResult.readiness.reason === "awaiting_other" ? " The session is awaiting a prompt — use `respond`, not a free-text message." : readyResult.readiness.reason === "terminal" ? " The session has exited." : " Wait for it to go idle (await_turn) or set requireIdle:false to force.";
3356
+ return jsonResult$1({
3357
+ resolvedInstance: publicInstance(instance),
3358
+ sessionId: globalId,
3359
+ delivered: false,
3360
+ submitted: false,
3361
+ notReady: true,
3362
+ reason: readyResult.readiness.reason,
3363
+ interactionState: readyResult.readiness.interactionState,
3364
+ awaitingKind: readyResult.readiness.awaitingKind,
3365
+ message: `not sent: session is not ready for a message (${readyResult.readiness.reason}).${advise}`
3366
+ }, true);
3367
+ }
3368
+ }
3369
+ const response = await client.sendMessage(localId, {
2887
3370
  message: requiredString$1(args, "message"),
2888
3371
  idempotencyKey: optionalString$1(args, "idempotencyKey") ?? randomUUID(),
2889
3372
  ...awaitMs === void 0 ? {} : { awaitMs }
2890
3373
  }, signal);
2891
3374
  const delivered = !(response.delivered === false || response.delivery?.status === "failed" || response.delivery?.status === "error");
2892
3375
  const confirmed = delivered && response.confirmed === true;
3376
+ const submitted = delivered && response.submission?.status === "submitted";
2893
3377
  const confirmationTimedOut = delivered && !confirmed && (awaitMs !== void 0 && awaitMs > 0 || response.confirmationTimedOut === true);
2894
3378
  const isError = !delivered;
2895
3379
  return jsonResult$1({
@@ -2898,6 +3382,7 @@ function createFleetTools(options = {}) {
2898
3382
  ...response,
2899
3383
  delivered,
2900
3384
  confirmed,
3385
+ submitted,
2901
3386
  ...confirmationTimedOut ? {
2902
3387
  confirmationPending: true,
2903
3388
  confirmationTimedOut: true
@@ -2905,23 +3390,41 @@ function createFleetTools(options = {}) {
2905
3390
  ...isError ? { message: "message was not delivered to the session by the upstream instance" } : confirmationTimedOut ? { message: "delivered; turn completion not confirmed in the await window. Use await_turn filtered to this sessionId to observe completion (the idempotencyKey makes a retried send safe)." } : {}
2906
3391
  }, isError);
2907
3392
  }),
2908
- tool$1("send_keys", "Send key input to a fleet session.", objectSchema$1({
3393
+ tool$1("send_keys", "Send key input to a fleet session. Prefer the higher-level `op`: 'submit' presses Enter and 'interrupt' sends Ctrl-C, each mapped to ai-or-die's NAMED key (never a literal control byte like \"\\r\"). Use `keys` only for literal input; `raw` is strictly for literal bytes. Provide exactly one of `op` or `keys`.", objectSchema$1({
2909
3394
  sessionId: stringProp$1("Global session id in the form instanceId:localSessionId."),
2910
3395
  instance: stringProp$1("Optional instance id/label; when supplied it must agree with sessionId."),
2911
- keys: stringProp$1("Key sequence to send."),
3396
+ op: stringProp$1("Higher-level named op: 'submit' (Enter) or 'interrupt' (Ctrl-C). Mapped to the ai-or-die named key with raw off. Do NOT also pass keys."),
3397
+ keys: stringProp$1("Literal key sequence to send. Provide instead of op."),
2912
3398
  idempotencyKey: stringProp$1("Optional caller idempotency key; auto-generated when omitted."),
2913
- raw: booleanProp("Pass keys through as raw input when the instance supports it.")
2914
- }, ["sessionId", "keys"]), async (args, signal) => {
3399
+ raw: booleanProp("Pass keys through as raw literal bytes when the instance supports it. Ignored when op is set.")
3400
+ }, ["sessionId"]), async (args, signal) => {
2915
3401
  const { instance, localId, globalId } = await resolveSession(args);
2916
- const raw = optionalBoolean(args, "raw");
3402
+ const op = optionalString$1(args, "op");
3403
+ const literalKeys = optionalString$1(args, "keys");
3404
+ if (op !== void 0 && literalKeys !== void 0) throw new FleetToolInputError("INVALID_ARGUMENT", "provide either arguments.op or arguments.keys, not both");
3405
+ if (op === void 0 && literalKeys === void 0) throw new FleetToolInputError("INVALID_ARGUMENT", "one of arguments.op or arguments.keys is required");
3406
+ let keys;
3407
+ let raw;
3408
+ if (op !== void 0) {
3409
+ if (!isNamedKeyOp(op)) throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.op must be 'submit' or 'interrupt' (got ${JSON.stringify(op)})`);
3410
+ keys = mapNamedKeyOp(op);
3411
+ raw = false;
3412
+ } else {
3413
+ keys = literalKeys;
3414
+ raw = optionalBoolean(args, "raw");
3415
+ }
2917
3416
  const response = await clientFor(instance).sendKeys(localId, {
2918
- keys: requiredString$1(args, "keys"),
3417
+ keys,
2919
3418
  idempotencyKey: optionalString$1(args, "idempotencyKey") ?? randomUUID(),
2920
3419
  ...raw === void 0 ? {} : { raw }
2921
3420
  }, signal);
2922
3421
  return ok$1({
2923
3422
  resolvedInstance: publicInstance(instance),
2924
3423
  sessionId: globalId,
3424
+ ...op === void 0 ? {} : {
3425
+ op,
3426
+ mappedKeys: keys
3427
+ },
2925
3428
  ...response
2926
3429
  });
2927
3430
  }),
@@ -2956,15 +3459,18 @@ function createFleetTools(options = {}) {
2956
3459
  start: booleanProp("Whether the remote instance should start the session immediately."),
2957
3460
  readyTimeoutMs: numberProp$1("F17: bounded ms to wait for the agent to become driveable before returning. The response carries ready/bound/blocker."),
2958
3461
  permissionMode: stringProp$1("F10 (claude only): permission mode the launched agent starts in — one of plan | acceptEdits | default | bypassPermissions. Rejected with BAD_REQUEST if unknown or if agentArgs also sets it."),
2959
- agentArgs: arrayProp("F10 (claude only): extra launcher args appended after the github-router prefix. Must NOT include --permission-mode or --dangerously-skip-permissions (use permissionMode) — rejected with BAD_REQUEST.")
3462
+ agentArgs: arrayProp("F10 (claude only): extra launcher args appended after the github-router prefix. Must NOT include --permission-mode or --dangerously-skip-permissions (use permissionMode) — rejected with BAD_REQUEST."),
3463
+ disableStopGate: booleanProp("C3 (claude only): disable the structural Stop-gate on the launched session by injecting --no-stop-gate into agentArgs, so a driven session's turn-end never hangs on a blocking Stop hook. Requires a remote github-router that understands the flag (uses the agent_args capability).")
2960
3464
  }, ["instance", "agent"]), async (args, signal) => {
2961
3465
  const instance = await resolve(requiredString$1(args, "instance"));
2962
3466
  const agent = requiredString$1(args, "agent");
2963
3467
  const idempotencyKey = optionalString$1(args, "idempotencyKey") ?? randomUUID();
2964
3468
  const permissionMode = optionalString$1(args, "permissionMode");
2965
- const agentArgs = optionalStringArray(args, "agentArgs");
3469
+ const disableStopGate = optionalBoolean(args, "disableStopGate") === true;
3470
+ const requestedAgentArgs = optionalStringArray(args, "agentArgs");
3471
+ const agentArgs = disableStopGate ? [...requestedAgentArgs ?? [], "--no-stop-gate"] : requestedAgentArgs;
2966
3472
  if (permissionMode !== void 0) await assertCapability(instance, "permission_mode", "permissionMode", signal);
2967
- if (agentArgs !== void 0) await assertCapability(instance, "agent_args", "agentArgs", signal);
3473
+ if (agentArgs !== void 0) await assertCapability(instance, "agent_args", disableStopGate ? "disableStopGate" : "agentArgs", signal);
2968
3474
  const response = await clientFor(instance).createSession(definedObject({
2969
3475
  agent,
2970
3476
  name: optionalString$1(args, "name"),
@@ -3052,10 +3558,12 @@ function createFleetTools(options = {}) {
3052
3558
  instance: publicInstance(instance),
3053
3559
  ...gap
3054
3560
  })));
3561
+ const settled = classifyTurnEvents(events$1);
3055
3562
  return ok$1({
3056
3563
  resolvedInstances: target.instances.map(publicInstance),
3057
3564
  events: events$1,
3058
3565
  gaps,
3566
+ ...settled.length > 0 ? { settled } : {},
3059
3567
  cursors: responses.map(({ instance, response }) => ({
3060
3568
  instance: publicInstance(instance),
3061
3569
  cursor: response.cursor
@@ -3064,6 +3572,31 @@ function createFleetTools(options = {}) {
3064
3572
  ...errors.length > 0 ? { errors } : {}
3065
3573
  });
3066
3574
  }),
3575
+ tool$1("drive_task", "Drive one prompt on a session to completion and return the parsed operator report. Composes the reliable path: ensure the composer is idle (else return a structured busy/not-ready result), send and surface whether the message reached the composer (submitted; a delivered-but-unconfirmed send still proceeds), wait for the RELIABLE turn boundary (turn_ended / waiting_input — never the became_idle flicker), read the transcript tail, and parse the OPERATOR REPORT trailer into {state, summary, ask, artifact, raw}. A per-call REPORT_ID nonce is embedded in the trailer instruction and the parsed report is trusted ONLY when it echoes that nonce, so a stale prior-turn trailer left in the tail can never be returned as this turn's result. A reliable waiting_input outranks the model's self-reported STATE (a still-blocked session is never reported as done). If the turn does not end within timeoutMs (e.g. a blocking Stop hook that would otherwise hang ~10 min), it AUTO-RECOVERS with a Ctrl-C interrupt rather than blocking, then re-waits briefly and re-reads; a caller ABORT is distinct (settled:'aborted', state:'aborted') and never injects a Ctrl-C. Read `state` TOGETHER with `settled`/`interrupted`/`recovered`: state:'done' with settled:'timeout' + interrupted:true means the model reported done but the turn had to be interrupted to recover, so treat it as needs-verification rather than a clean completion; `submitted` is a best-effort positive signal that CAN be false even on a successful turn. Robust to a busy session (state:'busy'), a missing/stale/placeholder trailer (state falls back to the settle-derived value, reportFound:false), and a hung hook (interrupted:true, recovered:true/false). By default it appends the trailer instruction so the driven session emits a parseable report; set expectReport:false to send the prompt verbatim (a trailer left in the tail is then never trusted).", objectSchema$1({
3576
+ sessionId: stringProp$1("Global session id in the form instanceId:localSessionId."),
3577
+ instance: stringProp$1("Optional instance id/label; when supplied it must agree with sessionId."),
3578
+ prompt: stringProp$1("The task/prompt to drive on the session."),
3579
+ timeoutMs: numberProp$1(`Ms to wait for the turn to end before auto-recovering via interrupt (default ${DRIVE_TASK_DEFAULT_TIMEOUT_MS}). Set generously — exceeding it triggers a Ctrl-C recovery.`),
3580
+ expectReport: booleanProp("Default true: append the OPERATOR REPORT trailer instruction so the driven session ends its turn with a parseable {state,summary,ask,artifact}. Set false to send the prompt verbatim.")
3581
+ }, ["sessionId", "prompt"]), async (args, signal) => {
3582
+ const { instance, localId, globalId } = await resolveSession(args);
3583
+ const result = await driveTask({
3584
+ client: clientFor(instance),
3585
+ localId,
3586
+ prompt: requiredString$1(args, "prompt"),
3587
+ timeoutMs: optionalNumber$1(args, "timeoutMs") ?? DRIVE_TASK_DEFAULT_TIMEOUT_MS,
3588
+ expectReport: optionalBoolean(args, "expectReport") ?? true,
3589
+ idempotencyKey: randomUUID(),
3590
+ interruptKey: randomUUID(),
3591
+ reportId: randomUUID(),
3592
+ signal
3593
+ });
3594
+ return jsonResult$1({
3595
+ resolvedInstance: publicInstance(instance),
3596
+ sessionId: globalId,
3597
+ ...result
3598
+ }, result.error !== void 0);
3599
+ }),
3067
3600
  tool$1("read_file", "Read a file from one fleet instance via its existing /api/files/content endpoint.", objectSchema$1({
3068
3601
  instance: stringProp$1("Instance id or label. Defaults to the registry default, or the sole instance."),
3069
3602
  path: stringProp$1("Remote file path to read.")
@@ -24305,6 +24838,16 @@ function stopGateId(env = process.env) {
24305
24838
  const v = (env.GH_ROUTER_STOP_GATE_ID ?? "").trim();
24306
24839
  return v.length > 0 ? v : "default-ci";
24307
24840
  }
24841
+ /**
24842
+ * C3: whether the structural Stop-gate is disabled for THIS launch. True when the
24843
+ * `GH_ROUTER_DISABLE_STOP_GATE` env is set OR the `--no-stop-gate` launcher flag was
24844
+ * passed (`args["no-stop-gate"] === true`). A DRIVEN session sets the flag so a
24845
+ * blocking Stop hook never hangs its turn-end (~10 min) waiting on the fleet driver.
24846
+ * Pure so it is unit-testable without the live launch path.
24847
+ */
24848
+ function stopGateDisabled(args, env = process.env) {
24849
+ return parseBoolEnv(env.GH_ROUTER_DISABLE_STOP_GATE) === true || args["no-stop-gate"] === true;
24850
+ }
24308
24851
  /** True when a settings `Stop` entry already registers `command` (so the merge
24309
24852
  * is idempotent across re-launches). */
24310
24853
  function entryHasCommand(entry, command) {
@@ -26346,5 +26889,5 @@ async function runStandInToolCall(args, signal) {
26346
26889
  }
26347
26890
 
26348
26891
  //#endregion
26349
- export { readIteratorWithTimeout as $, copilotHeaders as $t, DEFAULT_MODEL as A, UPSTREAM_INACTIVITY_TIMEOUT_MS as At, toolbeltSkipSet as B, cacheModels as Bt, repoRoot as C, collapsePathKeys as Ct, resolveSealedGate as D, DEFAULT_CODEX_MODEL_FALLBACKS as Dt, trustRepo as E, DEFAULT_CODEX_MODEL as Et, runWorkerAgent as F, setupCopilotToken as Ft, ADVISOR_INTERNAL_TOOL_NAME as G, resolveModel as Gt, TOOLBELT_TOOLS$1 as H, filterBetaHeader as Ht, withNoOutputRetry as I, setupGitHubAgentToken as It, injectAdvisorTool as J, fetchWithTransientRetry as Jt, ADVISOR_TOOL_INSTRUCTIONS as K, sleep as Kt, availableToolCommands as L, setupGitHubToken as Lt, PLAN_DEFAULT_MODEL as M, pickClaudeDefault as Mt, REVIEW_DEFAULT_MODEL as N, getPackageVersion as Nt, liveExec as O, DEFAULT_PORT as Ot, appendPlanReminder as P, withInstallLock as Pt, logStreamError as Q, copilotBaseUrl as Qt, buildToolbeltAwareness as R, tryRefreshAndRetry as Rt, repoFingerprint as S, ArtifactClient as St, stopReviewStateDir as T, DEFAULT_CLAUDE_MODEL_FALLBACKS as Tt, assetFor as U, isNullish as Ut, vscodeRipgrepPath as V, cacheVSCodeVersion as Vt, searchWeb as W, resolveCodexModel as Wt, buildOpenAIErrorEvent as X, forwardError as Xt, isAdvisorRequested as Y, HTTPError as Yt, isControllerClosedError as Z, GITHUB_API_BASE_URL as Zt, fileBaselineStore as _, hasSupportedBrowserInstalled as _t, buildPeerAwarenessSnippet as a, fleetToolsEnabled as at, fileReviewDebounce as b, extractZipMember as bt, buildSessionBindHookCommand as c, countTokens as ct, decideStopHook as d, createResponses as dt, githubHeaders as en, relayAnthropicStream as et, fileBlockBudget as f, createChatCompletions as ft, stopReviewEnabled as g, provisionBrowserAssets as gt, stopGateId as h, parseJsonOrDiagnose as ht, buildAgentPrompt as i, browserToolsEnabled as it, IMPLEMENT_DEFAULT_MODEL as j, generateRandomPort as jt, BROWSE_DEFAULT_MODEL as k, UPSTREAM_FETCH_TIMEOUT_MS as kt, buildStopHookCommand as l, createMessages as lt, launchBaselineKey as m, readResponseBodyCapped as mt, MCP_GROUPS as n, handleMcpPost as nt, personasFor as o, standInToolEnabled as ot, injectStopHookIntoSettingsFile as p, MAX_RESPONSE_BODY_BYTES as pt, buildAdvisorStream as q, getModels as qt, assertMcpToolSurfaceConsistent as r, agentToolsEnabled as rt, buildArtifactOpenHookCommand as s, workerToolsEnabled as st, GROUP_META as t, state as tn, handleMcpDelete as tt, captureLaunchBaseline as u, getTokenCount as ut, fileFindingsStore as v, provisionAndIndexColbert as vt, stopGateEnabledForRepo as w, toolbeltPathOverride as wt, isSubagentContext as x, shouldUseInsecureTls as xt, fileLastPromptStore as y, extractTarGzMember as yt, toolbeltEnabled as z, cacheCopilotVersion as zt };
26350
- //# sourceMappingURL=peer-mcp-personas-D_HyWhUb.js.map
26892
+ export { logStreamError as $, copilotBaseUrl as $t, BROWSE_DEFAULT_MODEL as A, UPSTREAM_FETCH_TIMEOUT_MS as At, toolbeltEnabled as B, cacheCopilotVersion as Bt, repoFingerprint as C, ArtifactClient as Ct, trustRepo as D, DEFAULT_CODEX_MODEL as Dt, stopReviewStateDir as E, DEFAULT_CLAUDE_MODEL_FALLBACKS as Et, appendPlanReminder as F, withInstallLock as Ft, searchWeb as G, resolveCodexModel as Gt, vscodeRipgrepPath as H, cacheVSCodeVersion as Ht, runWorkerAgent as I, setupCopilotToken as It, buildAdvisorStream as J, getModels as Jt, ADVISOR_INTERNAL_TOOL_NAME as K, resolveModel as Kt, withNoOutputRetry as L, setupGitHubAgentToken as Lt, IMPLEMENT_DEFAULT_MODEL as M, generateRandomPort as Mt, PLAN_DEFAULT_MODEL as N, pickClaudeDefault as Nt, resolveSealedGate as O, DEFAULT_CODEX_MODEL_FALLBACKS as Ot, REVIEW_DEFAULT_MODEL as P, getPackageVersion as Pt, isControllerClosedError as Q, GITHUB_API_BASE_URL as Qt, availableToolCommands as R, setupGitHubToken as Rt, isSubagentContext as S, shouldUseInsecureTls as St, stopGateEnabledForRepo as T, toolbeltPathOverride as Tt, TOOLBELT_TOOLS$1 as U, filterBetaHeader as Ut, toolbeltSkipSet as V, cacheModels as Vt, assetFor as W, isNullish as Wt, isAdvisorRequested as X, HTTPError as Xt, injectAdvisorTool as Y, fetchWithTransientRetry as Yt, buildOpenAIErrorEvent as Z, forwardError as Zt, stopReviewEnabled as _, provisionBrowserAssets as _t, buildPeerAwarenessSnippet as a, browserToolsEnabled as at, fileLastPromptStore as b, extractTarGzMember as bt, buildSessionBindHookCommand as c, workerToolsEnabled as ct, decideStopHook as d, getTokenCount as dt, copilotHeaders as en, readIteratorWithTimeout as et, fileBlockBudget as f, createResponses as ft, stopGateId as g, parseJsonOrDiagnose as gt, stopGateDisabled as h, readResponseBodyCapped as ht, buildAgentPrompt as i, agentToolsEnabled as it, DEFAULT_MODEL as j, UPSTREAM_INACTIVITY_TIMEOUT_MS as jt, liveExec as k, DEFAULT_PORT as kt, buildStopHookCommand as l, countTokens as lt, launchBaselineKey as m, MAX_RESPONSE_BODY_BYTES as mt, MCP_GROUPS as n, state as nn, handleMcpDelete as nt, personasFor as o, fleetToolsEnabled as ot, injectStopHookIntoSettingsFile as p, createChatCompletions as pt, ADVISOR_TOOL_INSTRUCTIONS as q, sleep as qt, assertMcpToolSurfaceConsistent as r, handleMcpPost as rt, buildArtifactOpenHookCommand as s, standInToolEnabled as st, GROUP_META as t, githubHeaders as tn, relayAnthropicStream as tt, captureLaunchBaseline as u, createMessages as ut, fileBaselineStore as v, hasSupportedBrowserInstalled as vt, repoRoot as w, collapsePathKeys as wt, fileReviewDebounce as x, extractZipMember as xt, fileFindingsStore as y, provisionAndIndexColbert as yt, buildToolbeltAwareness as z, tryRefreshAndRetry as zt };
26893
+ //# sourceMappingURL=peer-mcp-personas-Dm3UCpXz.js.map