Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/tui/deliver-agent-message.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
deliverAgentMessage,
deliveryResultNotice,
runGenerationGuardedDeliver,
} from "./deliver-agent-message.js";
} from "./delivery-queue.js";

describe("deliverAgentMessage", () => {
test("reports session-unavailable without calling deliver when rebuild failed", async () => {
Expand Down
117 changes: 0 additions & 117 deletions src/tui/deliver-agent-message.ts

This file was deleted.

118 changes: 117 additions & 1 deletion src/tui/delivery-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,128 @@
* and no product chord wires it anymore — leave the path for tests/API only.
*/

import { AgentClosedError } from "@intx/agent";
import type { PendingImageAttachment } from "./image-attachments.js";
import type { AgentDeliveryResult } from "./deliver-agent-message.js";
import type { ProductHostDeliver } from "./product-host.js";
import { ASK_DIRECTOR_WAKE_PREFIX } from "../subagent/fleet-report.js";
import { MAILBOX_MAIL_WAKE_PREFIX } from "../subagent/mailbox-mail-drive.js";

export type AgentDeliveryNotDeliveredReason =
| "agent-closed"
| "session-unavailable"
| "superseded"
| "preparation-failed";

export type AgentDeliveryResult =
| { readonly status: "accepted" }
| {
readonly status: "not-delivered";
readonly reason: AgentDeliveryNotDeliveredReason;
readonly detail: string;
}
| {
readonly status: "uncertain";
readonly detail: string;
};

export interface DeliverAgentMessageDeps {
getFatalBuildError: () => Error | null;
deliverToLiveAgent: () => void;
}

/**
* Guards a queued/steer deliver against a mid-rebuild or closed agent. The
* shell paints the delivered row and pops the queue item before this runs, so
* the caller must settle ownership from the structured result — a swallowed
* failure here means the transcript claims delivery for a message that never
* reached the agent.
*/
export async function deliverAgentMessage(
deps: DeliverAgentMessageDeps,
): Promise<AgentDeliveryResult> {
const fatal = deps.getFatalBuildError();
if (fatal !== null) {
return {
status: "not-delivered",
reason: "session-unavailable",
detail: fatal.message,
};
}
try {
deps.deliverToLiveAgent();
return { status: "accepted" };
} catch (err) {
if (err instanceof AgentClosedError) {
return {
status: "not-delivered",
reason: "agent-closed",
detail: err.message,
};
}
return {
status: "uncertain",
detail: err instanceof Error ? err.message : String(err),
};
}
}

/**
* Settles a deliver that was enqueued on the serial operation queue against
* the shoot generation captured at enqueue time. The queue is FIFO with no
* preemption, so a deliver queued ahead of a reload still executes after the
* reload has replaced the agent — the generation must be re-checked when the
* queued closure runs, not just when it enqueues. A stale deliver takes the
* `onStale` path (the caller reports `not-delivered`); a current deliver runs
* the real settle. This is what closes the reload-vs-async-deliver race: a
* reload that lands while a continuation answer is queued wins, and the stale
* answer is dropped instead of reaching the replaced agent.
*/
export async function runGenerationGuardedDeliver(options: {
stillCurrent: () => boolean;
run: () => Promise<AgentDeliveryResult>;
onStale: () => AgentDeliveryResult;
}): Promise<AgentDeliveryResult> {
if (!options.stillCurrent()) {
return options.onStale();
}
return options.run();
}

/** Operator-facing copy for a settled delivery that did not accept. */
export function deliveryResultNotice(
result: Exclude<AgentDeliveryResult, { status: "accepted" }>,
disposition: "restored" | "deferred" | "none" = "none",
): string {
if (result.status === "uncertain") {
const base = `Delivery failed: ${result.detail}. Delivery status is uncertain; review the transcript before sending again.`;
return appendDisposition(base, disposition);
}
if (result.reason === "agent-closed") {
if (disposition === "restored") {
return "Message not delivered because the agent closed. It is back in the prompt; press Enter to send it.";
}
if (disposition === "deferred") {
return "Message not delivered because the agent closed. Your current draft is unchanged; the message will return to the prompt after you send it.";
}
return "Message not delivered because the agent closed.";
}
const base = `Message not delivered: ${result.detail}`;
return appendDisposition(base, disposition);
}

function appendDisposition(
base: string,
disposition: "restored" | "deferred" | "none",
): string {
if (disposition === "restored") {
return `${base} It is back in the prompt; press Enter to send it.`;
}
if (disposition === "deferred") {
return `${base} Your current draft is unchanged; the message will return to the prompt after you send it.`;
}
return base;
}

export type QueueKind = "queue" | "steer";

export interface QueueItem {
Expand Down
2 changes: 1 addition & 1 deletion src/tui/queued-delivery-hop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import {
createLiveSteerDeliver,
createSessionOperationQueue,
routeQueuedDelivery,
type AgentDeliveryResult,
type DeliverySettle,
type QueueItem,
} from "./delivery-queue.js";
import type { AgentDeliveryResult } from "./deliver-agent-message.js";

function lastHopPort(bridgeRef: { current: SessionBridge | undefined }) {
const sends: string[] = [];
Expand Down
2 changes: 1 addition & 1 deletion src/tui/queued-delivery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
createSessionOperationQueue,
routeQueuedDelivery,
SESSION_IDENTITY_ABORT_REASON,
type AgentDeliveryResult,
} from "./delivery-queue.js";
import type { AgentDeliveryResult } from "./deliver-agent-message.js";

const image: PendingImageAttachment = {
id: "img-1",
Expand Down
10 changes: 4 additions & 6 deletions src/tui/runner/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ import { createSessionCostAccumulator } from "../../cost/session-cost.js";
import {
createDeliveryGeneration,
createSessionOperationQueue,
deliverAgentMessage,
deliveryResultNotice,
runGenerationGuardedDeliver,
type AgentDeliveryResult,
} from "../delivery-queue.js";
import { createCorrelationAcceptance } from "../correlation-acceptance.js";
import { createApprovalDeliverer } from "../approval-delivery.js";
Expand All @@ -92,12 +96,6 @@ import { detectLanguageServerAvailable } from "../../agent/lsp-availability.js";
import type { SessionMode } from "../../config/session-mode.js";
import { WorkflowHost, type WorkflowHostState } from "../../workflows/host.js";
import type { ToolWatchdogConfig } from "../tool-execution-watchdog.js";
import {
deliverAgentMessage,
deliveryResultNotice,
runGenerationGuardedDeliver,
type AgentDeliveryResult,
} from "../deliver-agent-message.js";
import { createProviderFailureAttemptTracker } from "../provider/failure-attempt.js";
import { getTelemetry, liveTelemetry } from "../../telemetry/singleton.js";
import {
Expand Down
2 changes: 1 addition & 1 deletion src/tui/runner/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import type { ProviderFailureAttempt } from "../provider/failure-attempt.js";
import type { ScopedApproval } from "../../permission/admin.js";
import type { ConnectedMcpServer, RunState } from "../../session/state.js";
import type { PendingImageAttachment } from "../image-attachments.js";
import type { AgentDeliveryResult } from "../deliver-agent-message.js";
import type { AgentDeliveryResult } from "../delivery-queue.js";
import type { SubmitOutcome } from "./submit.js";
import type { mountRunnerHost } from "./host.js";
import { EventEmitter } from "node:events";
Expand Down
2 changes: 1 addition & 1 deletion src/tui/runner/submit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ import {
createLeftoverSend,
createLiveSteerDeliver,
routeQueuedDelivery,
type AgentDeliveryResult,
} from "../delivery-queue.js";
import type { AgentDeliveryResult } from "../deliver-agent-message.js";
import type { InferenceAttemptIdentity } from "./state.js";
import { tuiSendFailureMessage } from "./send-failure-message.js";
import type { ProviderFailureAttempt } from "../provider/failure-attempt.js";
Expand Down
2 changes: 1 addition & 1 deletion src/tui/runtime-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ import {
import {
deliveryResultNotice,
type AgentDeliveryResult,
} from "./deliver-agent-message.js";
} from "./delivery-queue.js";
import { toolCallRow } from "./diff.js";
import { toolResultRow } from "./mcp-view.js";
import {
Expand Down
Loading