Skip to content

Commit 99cd75c

Browse files
Merge pull request #1074 from corbitsdev/cl-7953-leftovers-0-3-27
refactor(tui): fold delivery guard into delivery-queue
2 parents abc5efe + 6835e75 commit 99cd75c

9 files changed

Lines changed: 127 additions & 130 deletions

‎src/tui/deliver-agent-message.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
deliverAgentMessage,
55
deliveryResultNotice,
66
runGenerationGuardedDeliver,
7-
} from "./deliver-agent-message.js";
7+
} from "./delivery-queue.js";
88

99
describe("deliverAgentMessage", () => {
1010
test("reports session-unavailable without calling deliver when rebuild failed", async () => {

‎src/tui/deliver-agent-message.ts‎

Lines changed: 0 additions & 117 deletions
This file was deleted.

‎src/tui/delivery-queue.ts‎

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,128 @@
1717
* and no product chord wires it anymore — leave the path for tests/API only.
1818
*/
1919

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

26+
export type AgentDeliveryNotDeliveredReason =
27+
| "agent-closed"
28+
| "session-unavailable"
29+
| "superseded"
30+
| "preparation-failed";
31+
32+
export type AgentDeliveryResult =
33+
| { readonly status: "accepted" }
34+
| {
35+
readonly status: "not-delivered";
36+
readonly reason: AgentDeliveryNotDeliveredReason;
37+
readonly detail: string;
38+
}
39+
| {
40+
readonly status: "uncertain";
41+
readonly detail: string;
42+
};
43+
44+
export interface DeliverAgentMessageDeps {
45+
getFatalBuildError: () => Error | null;
46+
deliverToLiveAgent: () => void;
47+
}
48+
49+
/**
50+
* Guards a queued/steer deliver against a mid-rebuild or closed agent. The
51+
* shell paints the delivered row and pops the queue item before this runs, so
52+
* the caller must settle ownership from the structured result — a swallowed
53+
* failure here means the transcript claims delivery for a message that never
54+
* reached the agent.
55+
*/
56+
export async function deliverAgentMessage(
57+
deps: DeliverAgentMessageDeps,
58+
): Promise<AgentDeliveryResult> {
59+
const fatal = deps.getFatalBuildError();
60+
if (fatal !== null) {
61+
return {
62+
status: "not-delivered",
63+
reason: "session-unavailable",
64+
detail: fatal.message,
65+
};
66+
}
67+
try {
68+
deps.deliverToLiveAgent();
69+
return { status: "accepted" };
70+
} catch (err) {
71+
if (err instanceof AgentClosedError) {
72+
return {
73+
status: "not-delivered",
74+
reason: "agent-closed",
75+
detail: err.message,
76+
};
77+
}
78+
return {
79+
status: "uncertain",
80+
detail: err instanceof Error ? err.message : String(err),
81+
};
82+
}
83+
}
84+
85+
/**
86+
* Settles a deliver that was enqueued on the serial operation queue against
87+
* the shoot generation captured at enqueue time. The queue is FIFO with no
88+
* preemption, so a deliver queued ahead of a reload still executes after the
89+
* reload has replaced the agent — the generation must be re-checked when the
90+
* queued closure runs, not just when it enqueues. A stale deliver takes the
91+
* `onStale` path (the caller reports `not-delivered`); a current deliver runs
92+
* the real settle. This is what closes the reload-vs-async-deliver race: a
93+
* reload that lands while a continuation answer is queued wins, and the stale
94+
* answer is dropped instead of reaching the replaced agent.
95+
*/
96+
export async function runGenerationGuardedDeliver(options: {
97+
stillCurrent: () => boolean;
98+
run: () => Promise<AgentDeliveryResult>;
99+
onStale: () => AgentDeliveryResult;
100+
}): Promise<AgentDeliveryResult> {
101+
if (!options.stillCurrent()) {
102+
return options.onStale();
103+
}
104+
return options.run();
105+
}
106+
107+
/** Operator-facing copy for a settled delivery that did not accept. */
108+
export function deliveryResultNotice(
109+
result: Exclude<AgentDeliveryResult, { status: "accepted" }>,
110+
disposition: "restored" | "deferred" | "none" = "none",
111+
): string {
112+
if (result.status === "uncertain") {
113+
const base = `Delivery failed: ${result.detail}. Delivery status is uncertain; review the transcript before sending again.`;
114+
return appendDisposition(base, disposition);
115+
}
116+
if (result.reason === "agent-closed") {
117+
if (disposition === "restored") {
118+
return "Message not delivered because the agent closed. It is back in the prompt; press Enter to send it.";
119+
}
120+
if (disposition === "deferred") {
121+
return "Message not delivered because the agent closed. Your current draft is unchanged; the message will return to the prompt after you send it.";
122+
}
123+
return "Message not delivered because the agent closed.";
124+
}
125+
const base = `Message not delivered: ${result.detail}`;
126+
return appendDisposition(base, disposition);
127+
}
128+
129+
function appendDisposition(
130+
base: string,
131+
disposition: "restored" | "deferred" | "none",
132+
): string {
133+
if (disposition === "restored") {
134+
return `${base} It is back in the prompt; press Enter to send it.`;
135+
}
136+
if (disposition === "deferred") {
137+
return `${base} Your current draft is unchanged; the message will return to the prompt after you send it.`;
138+
}
139+
return base;
140+
}
141+
26142
export type QueueKind = "queue" | "steer";
27143

28144
export interface QueueItem {

‎src/tui/queued-delivery-hop.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ import {
1212
createLiveSteerDeliver,
1313
createSessionOperationQueue,
1414
routeQueuedDelivery,
15+
type AgentDeliveryResult,
1516
type DeliverySettle,
1617
type QueueItem,
1718
} from "./delivery-queue.js";
18-
import type { AgentDeliveryResult } from "./deliver-agent-message.js";
1919

2020
function lastHopPort(bridgeRef: { current: SessionBridge | undefined }) {
2121
const sends: string[] = [];

‎src/tui/queued-delivery.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import {
88
createSessionOperationQueue,
99
routeQueuedDelivery,
1010
SESSION_IDENTITY_ABORT_REASON,
11+
type AgentDeliveryResult,
1112
} from "./delivery-queue.js";
12-
import type { AgentDeliveryResult } from "./deliver-agent-message.js";
1313

1414
const image: PendingImageAttachment = {
1515
id: "img-1",

‎src/tui/runner/session.ts‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ import { createSessionCostAccumulator } from "../../cost/session-cost.js";
7979
import {
8080
createDeliveryGeneration,
8181
createSessionOperationQueue,
82+
deliverAgentMessage,
83+
deliveryResultNotice,
84+
runGenerationGuardedDeliver,
85+
type AgentDeliveryResult,
8286
} from "../delivery-queue.js";
8387
import { createCorrelationAcceptance } from "../correlation-acceptance.js";
8488
import { createApprovalDeliverer } from "../approval-delivery.js";
@@ -92,12 +96,6 @@ import { detectLanguageServerAvailable } from "../../agent/lsp-availability.js";
9296
import type { SessionMode } from "../../config/session-mode.js";
9397
import { WorkflowHost, type WorkflowHostState } from "../../workflows/host.js";
9498
import type { ToolWatchdogConfig } from "../tool-execution-watchdog.js";
95-
import {
96-
deliverAgentMessage,
97-
deliveryResultNotice,
98-
runGenerationGuardedDeliver,
99-
type AgentDeliveryResult,
100-
} from "../deliver-agent-message.js";
10199
import { createProviderFailureAttemptTracker } from "../provider/failure-attempt.js";
102100
import { getTelemetry, liveTelemetry } from "../../telemetry/singleton.js";
103101
import {

‎src/tui/runner/state.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import type { ProviderFailureAttempt } from "../provider/failure-attempt.js";
3232
import type { ScopedApproval } from "../../permission/admin.js";
3333
import type { ConnectedMcpServer, RunState } from "../../session/state.js";
3434
import type { PendingImageAttachment } from "../image-attachments.js";
35-
import type { AgentDeliveryResult } from "../deliver-agent-message.js";
35+
import type { AgentDeliveryResult } from "../delivery-queue.js";
3636
import type { SubmitOutcome } from "./submit.js";
3737
import type { mountRunnerHost } from "./host.js";
3838
import { EventEmitter } from "node:events";

‎src/tui/runner/submit.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ import {
3535
createLeftoverSend,
3636
createLiveSteerDeliver,
3737
routeQueuedDelivery,
38+
type AgentDeliveryResult,
3839
} from "../delivery-queue.js";
39-
import type { AgentDeliveryResult } from "../deliver-agent-message.js";
4040
import type { InferenceAttemptIdentity } from "./state.js";
4141
import { tuiSendFailureMessage } from "./send-failure-message.js";
4242
import type { ProviderFailureAttempt } from "../provider/failure-attempt.js";

‎src/tui/runtime-bridge.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ import {
7272
import {
7373
deliveryResultNotice,
7474
type AgentDeliveryResult,
75-
} from "./deliver-agent-message.js";
75+
} from "./delivery-queue.js";
7676
import { toolCallRow } from "./diff.js";
7777
import { toolResultRow } from "./mcp-view.js";
7878
import {

0 commit comments

Comments
 (0)