Open History to see the last commit
1import {
2 latestExecutedRun,
3 latestRootProviderFailure,
4 latestUnheldRun,
5 threadErrorSummary,
6 usageLimitRunPresentedAsLatest,
7} from "@t3tools/shared/orchestrationV2ThreadError";
8import {
9 isOrchestrationV2WorkActive,
10 isProviderNativeSubagentThread,
11 latestProviderTurnForAttempt,
12 PROVIDER_DISPLAY_NAMES,
13 type ModelSelection,
14 type OrchestrationV2NotificationSource,
15 type OrchestrationV2PendingBackgroundTask,
16 type OrchestrationV2ProviderGoal,
17 type OrchestrationV2ProviderSession,
18 type ServerProvider,
19 type ServerProviderModel,
20 type OrchestrationV2ExecutionNode,
21 type OrchestrationV2LimitRecovery,
22 type OrchestrationV2ThreadProjection,
23 orchestrationV2RunWorkStartedAt,
24 type RunId,
25 type ThreadId,
26} from "@t3tools/contracts";
27import {
28 backgroundWorkHoldsCompletion,
29 derivePendingBackgroundWork,
30} from "@t3tools/shared/orchestrationV2PendingBackgroundWork";
31import {
32 getModelSelectionStringOptionValue,
33 getProviderOptionCurrentLabel,
34 getProviderOptionStatedValue,
35 resolveSelectableModel,
36 withLegacyUltracodeEffort,
37} from "@t3tools/shared/model";
38import { formatDuration } from "@t3tools/shared/orchestrationTiming";
39import * as DateTime from "effect/DateTime";
40
41import {
42 threadRuntimeIsActive,
43 type ThreadRunSummary,
44 type ThreadRuntimeSummary,
45 type ThreadStructure,
46 type ThreadStructureRuntime,
47 type ThreadStructureTurnItem,
48} from "./models.ts";
49import { commandDisplayText } from "../work-log/commandLabel.ts";
50import { formatSubagentDisplayTitle } from "./subagentDisplay.ts";
51
52const ACTIVITY_RUN_STATUSES = new Set(["preparing", "starting", "running", "waiting"]);
53const INTERRUPTIBLE_RUN_STATUSES = new Set(["preparing", "starting", "running"]);
54// Preparing runs show their workspace setup instead.
55const PROVIDER_STARTING_RUN_STATUSES = new Set(["starting", "running"]);
56
57const EMPTY_EXTENSION_STATUSES: NonNullable<OrchestrationV2ProviderSession["extensionStatuses"]> =
58 [];
59
60/** Only the selected provider's current session can supply transient extension status. */
61export function deriveProviderExtensionStatuses(
62 projection:
63 | (Pick<ThreadStructure, "thread"> & {
64 readonly providerThreads: ReadonlyArray<
65 Pick<
66 ThreadStructure["providerThreads"][number],
67 "id" | "providerSessionId" | "providerInstanceId"
68 >
69 >;
70 readonly providerSessions: ReadonlyArray<
71 Pick<
72 OrchestrationV2ProviderSession,
73 "id" | "providerInstanceId" | "status" | "extensionStatuses"
74 >
75 >;
76 })
77 | null,
78) {
79 if (projection === null) return EMPTY_EXTENSION_STATUSES;
80 const instanceId = projection.thread.modelSelection.instanceId;
81 const activeThread = projection.providerThreads.find(
82 (thread) => thread.id === projection.thread.activeProviderThreadId,
83 );
84 if (activeThread !== undefined && activeThread.providerInstanceId !== instanceId)
85 return EMPTY_EXTENSION_STATUSES;
86 const session = projection.providerSessions.findLast(
87 (session) =>
88 session.providerInstanceId === instanceId &&
89 (activeThread === undefined || session.id === activeThread.providerSessionId),
90 );
91 return session === undefined || session.status === "stopped"
92 ? EMPTY_EXTENSION_STATUSES
93 : (session.extensionStatuses ?? EMPTY_EXTENSION_STATUSES);
94}
95
96function latestMatchingRun(
97 projection: ThreadStructure,
98 predicate: (run: OrchestrationV2ThreadProjection["runs"][number]) => boolean,
99): OrchestrationV2ThreadProjection["runs"][number] | null {
100 return projection.runs.reduce<OrchestrationV2ThreadProjection["runs"][number] | null>(
101 (latest, candidate) =>
102 predicate(candidate) && (latest === null || candidate.ordinal > latest.ordinal)
103 ? candidate
104 : latest,
105 null,
106 );
107}
108
109function providerStartingLabel(
110 projection: ThreadStructure,
111 run: OrchestrationV2ThreadProjection["runs"][number],
112): string {
113 const driver = projection.providerSessions.findLast(
114 (session) => session.providerInstanceId === run.providerInstanceId,
115 )?.driver;
116 const name = driver === undefined ? undefined : PROVIDER_DISPLAY_NAMES[driver];
117 return name === undefined ? "Starting" : `Starting ${name}`;
118}
119
120function summarizeThreadRun(
121 projection: ThreadStructure,
122 run: OrchestrationV2ThreadProjection["runs"][number],
123): ThreadRunSummary {
124 const turnStatus = ACTIVITY_RUN_STATUSES.has(run.status)
125 ? latestProviderTurnForAttempt(projection.providerTurns, run.activeAttemptId)?.status
126 : undefined;
127 return {
128 runId: run.id,
129 status: run.status,
130 requestedAt: DateTime.formatIso(run.requestedAt),
131 startedAt: run.startedAt === null ? null : DateTime.formatIso(run.startedAt),
132 completedAt: run.completedAt === null ? null : DateTime.formatIso(run.completedAt),
133 assistantMessageId:
134 projection.messages.findLast(
135 (message) => message.runId === run.id && message.role === "assistant",
136 )?.id ?? null,
137 // A run counts as running before its provider opens; until a turn appears,
138 // the provider can still be launching for minutes.
139 ...(turnStatus === undefined
140 ? PROVIDER_STARTING_RUN_STATUSES.has(run.status)
141 ? { providerStartingLabel: providerStartingLabel(projection, run) }
142 : {}
143 : turnStatus === "pending" || turnStatus === "running"
144 ? {}
145 : { turnEnded: true }),
146 ...(run.sourcePlanRef === undefined ? {} : { sourcePlanRef: run.sourcePlanRef }),
147 };
148}
149
150function presentedUsageLimitRun(
151 projection: ThreadStructure,
152): OrchestrationV2ThreadProjection["runs"][number] | null {
153 const providerSession = projection.providerSessions.findLast(
154 (session) => session.providerInstanceId === projection.thread.providerInstanceId,
155 );
156 return usageLimitRunPresentedAsLatest(
157 projection.runs,
158 projection.turnItems,
159 providerSession?.lastError ?? null,
160 );
161}
162
163function presentedLatestRun(
164 projection: ThreadStructure,
165): OrchestrationV2ThreadProjection["runs"][number] | null {
166 return presentedUsageLimitRun(projection) ?? latestUnheldRun(projection.runs);
167}
168
169export function deriveLatestThreadRun(projection: ThreadStructure): ThreadRunSummary | null {
170 const run = presentedLatestRun(projection);
171 return run === null ? null : summarizeThreadRun(projection, run);
172}
173
174export function deriveThreadActivityRun(projection: ThreadStructure): ThreadRunSummary | null {
175 const run =
176 latestMatchingRun(projection, (candidate) => ACTIVITY_RUN_STATUSES.has(candidate.status)) ??
177 presentedLatestRun(projection);
178 return run === null ? null : summarizeThreadRun(projection, run);
179}
180
181/**
182 * Provider-native subagent threads never get app runs: their work is a runless
183 * root turn whose status follows the subagent.
184 */
185export function deriveRunlessWorkStartedAt(projection: ThreadStructure): string | null {
186 const status = deriveProviderSubagentStatus(projection);
187 return status !== null && isOrchestrationV2WorkActive(status.status) ? status.startedAt : null;
188}
189
190export interface ProviderSubagentStatus {
191 readonly status: OrchestrationV2ExecutionNode["status"];
192 readonly startedAt: string | null;
193 readonly completedAt: string | null;
194}
195
196export function deriveProviderSubagentStatus(
197 projection: ThreadStructure,
198): ProviderSubagentStatus | null {
199 if (!isProviderNativeSubagentThread(projection.thread)) return null;
200 const node = projection.nodes.findLast(
201 (candidate) => candidate.kind === "root_turn" && candidate.runId === null,
202 );
203 if (node === undefined) return null;
204 return {
205 status: node.status,
206 startedAt: node.startedAt === null ? null : DateTime.formatIso(node.startedAt),
207 completedAt: node.completedAt === null ? null : DateTime.formatIso(node.completedAt),
208 };
209}
210
211export function deriveReportedModelSelection(projection: ThreadStructure): ModelSelection | null {
212 const providerThread = projection.providerThreads.find(
213 (candidate) =>
214 candidate.id === projection.thread.activeProviderThreadId &&
215 candidate.providerInstanceId === projection.thread.modelSelection.instanceId,
216 );
217 return providerThread?.nativeMetadata?.modelSelection ?? null;
218}
219
220export const REASONING_EFFORT_OPTION_IDS: ReadonlyArray<string> = [
221 "reasoningEffort",
222 "effort",
223 "reasoning",
224 "variant",
225 "reasoning_effort",
226 "thinking_level",
227];
228
229export function formatModelSelectionEffort(
230 selection: ModelSelection,
231 models: ReadonlyArray<ServerProviderModel> = [],
232 reportedSelection?: ModelSelection | null,
233): string | null {
234 const caps = models.find((model) => model.slug === selection.model)?.capabilities;
235 if (!caps) return null;
236 const options = withLegacyUltracodeEffort(caps.optionDescriptors, selection.options);
237 const current = options ? { ...selection, options } : selection;
238 for (const id of REASONING_EFFORT_OPTION_IDS) {
239 const descriptor = caps.optionDescriptors?.find((candidate) => candidate.id === id);
240 if (descriptor?.type !== "select") continue;
241 const value = getProviderOptionStatedValue(id, current, reportedSelection);
242 if (value === undefined && id !== "variant") continue;
243 if (typeof value === "boolean") continue;
244 // Bypass picker normalization so an obsolete choice cannot become a catalog default.
245 const label = getProviderOptionCurrentLabel(
246 { ...descriptor, currentValue: value },
247 current,
248 reportedSelection,
249 );
250 if (label) return label;
251 }
252 return null;
253}
254
255/** A reported model the picker catalog does not know, such as `claude-opus-5[1m]`, keeps its raw effort. */
256export function formatSubagentEffort(input: {
257 readonly modelSlug: string | null;
258 readonly modelSelection: ModelSelection | undefined;
259 readonly provider: Pick<ServerProvider, "driver" | "models"> | undefined;
260}): string | null {
261 const { modelSlug, modelSelection: selection, provider } = input;
262 if (!provider || !selection || !modelSlug) return null;
263 const selectionSlug =
264 resolveSelectableModel(provider.driver, selection.model, provider.models) ??
265 selection.model.trim();
266 if (selectionSlug !== modelSlug) return null;
267 if (provider.models.some((model) => model.slug === modelSlug)) {
268 return formatModelSelectionEffort({ ...selection, model: modelSlug }, provider.models);
269 }
270 return (
271 REASONING_EFFORT_OPTION_IDS.map((id) => getModelSelectionStringOptionValue(selection, id)).find(
272 Boolean,
273 ) ?? null
274 );
275}
276
277const SUBAGENT_STATUS_LABELS: Record<OrchestrationV2ExecutionNode["status"], string> = {
278 idle: "Idle",
279 pending: "Working",
280 running: "Working",
281 waiting: "Waiting",
282 completed: "Completed",
283 interrupted: "Interrupted",
284 failed: "Failed",
285 cancelled: "Cancelled",
286 rolled_back: "Cancelled",
287};
288
289export function formatProviderSubagentStatus(
290 status: ProviderSubagentStatus | null,
291 nowMs: number,
292): string {
293 if (status === null) return "Starting";
294 const label = SUBAGENT_STATUS_LABELS[status.status];
295 const live = isOrchestrationV2WorkActive(status.status);
296 if (!live && status.status !== "completed") return label;
297 const start = status.startedAt === null ? Number.NaN : Date.parse(status.startedAt);
298 const end = live
299 ? nowMs
300 : status.completedAt === null
301 ? Number.NaN
302 : Date.parse(status.completedAt);
303 if (!Number.isFinite(start) || !Number.isFinite(end)) return label;
304 const elapsed = formatDuration(Math.max(1_000, Math.floor((end - start) / 1_000) * 1_000));
305 return live ? `${label} ${elapsed}` : `${label} in ${elapsed}`;
306}
307
308/** The run a manual continuation resumes; the server holds the queue until it does. */
309export function deriveResumableRunId(
310 projection: Pick<ThreadStructure, "runs" | "turnItems">,
311 runtime: Pick<ThreadStructureRuntime, "lastErrorClass"> | null,
312): RunId | null {
313 const run = latestExecutedRun(projection.runs);
314 if (run?.status === "interrupted") return run.id;
315 return run?.status === "failed" &&
316 runtime?.lastErrorClass === "usage_limit" &&
317 latestRootProviderFailure(run, projection.turnItems)?.class === "usage_limit"
318 ? run.id
319 : null;
320}
321
322/**
323 * Whether Resume can start: the run's continuation, else the held queue. The server refuses that
324 * continuation while any request is pending, so the queue must not be released in its place.
325 */
326export function canResumeThread(
327 projection: Pick<ThreadStructure, "runtimeRequests">,
328 resumableRunId: RunId | null,
329 queueHeld: boolean,
330): boolean {
331 if (resumableRunId === null) return queueHeld;
332 return !projection.runtimeRequests.some((request) => request.status === "pending");
333}
334
335export function deriveThreadRuntime(
336 projection: ThreadStructure & Pick<OrchestrationV2ThreadProjection, "updatedAt">,
337): ThreadRuntimeSummary | null {
338 const runtime = deriveThreadStructureRuntime(projection);
339 return runtime === null
340 ? null
341 : { ...runtime, updatedAt: DateTime.formatIso(projection.updatedAt) };
342}
343
344/** The runtime summary a thread's structure determines: all of it but `updatedAt`. */
345export function deriveThreadStructureRuntime(
346 projection: ThreadStructure,
347): ThreadStructureRuntime | null {
348 const latestRun = deriveLatestThreadRun(projection);
349 const providerSession = projection.providerSessions.findLast(
350 (session) => session.providerInstanceId === projection.thread.providerInstanceId,
351 );
352 const usageLimitedRun = presentedUsageLimitRun(projection);
353 const latestRunProjection = presentedLatestRun(projection);
354 const activityRun = deriveThreadActivityRun(projection);
355 const liveActivityRun = latestMatchingRun(projection, (run) =>
356 ACTIVITY_RUN_STATUSES.has(run.status),
357 );
358 const backgroundWorkHoldsRun = backgroundWorkHoldsCompletion(
359 derivePendingBackgroundWork({
360 latestRun: latestRunProjection,
361 providerThreads: projection.providerThreads,
362 turnItems: projection.turnItems,
363 activeProviderThreadId: projection.thread.activeProviderThreadId,
364 runs: projection.runs,
365 pullRequests: projection.thread.pullRequests,
366 }),
367 );
368 // A pull request watch can hold a thread that never ran.
369 if (
370 latestRun === null &&
371 projection.thread.activeProviderThreadId === null &&
372 !backgroundWorkHoldsRun
373 ) {
374 return null;
375 }
376 const activeRunId =
377 latestMatchingRun(projection, (run) => INTERRUPTIBLE_RUN_STATUSES.has(run.status))?.id ?? null;
378 return {
379 status: usageLimitedRun
380 ? "failed"
381 : backgroundWorkHoldsRun && latestRunProjection?.status !== "failed"
382 ? "idle"
383 : (activityRun?.status ?? "idle"),
384 activeRunId,
385 activityStartedAt:
386 liveActivityRun === null
387 ? null
388 : DateTime.formatIso(orchestrationV2RunWorkStartedAt(liveActivityRun)),
389 providerInstanceId: projection.thread.providerInstanceId,
390 providerName: providerSession?.driver ?? null,
391 ...threadErrorSummary(
392 latestRootProviderFailure(latestRunProjection, projection.turnItems),
393 providerSession?.lastError ?? null,
394 ),
395 };
396}
397
398export interface UsageLimitNoticeRows {
399 readonly itemIds: ReadonlySet<ThreadStructureTurnItem["id"]>;
400 /** Hidden only when its text reads as the provider's limit message; see PROVIDER_LIMIT_NOTICE. */
401 readonly providerNoticeItemId: ThreadStructureTurnItem["id"] | null;
402}
403
404// Claude Code's own limit messages, such as "You've hit your 5-hour limit · resets 7:10am".
405const PROVIDER_LIMIT_NOTICE =
406 /^(?:You[’']ve hit your (?:(?:5-hour|session|weekly|usage) )?limit|You[’']re out of extra usage|Claude AI usage limit reached)(?: · resets (?:[1-9]|1[0-2])(?::[0-5]\d)?(?:am|pm)(?: \([A-Za-z0-9_+-]+(?:\/[A-Za-z0-9_+-]+)*\))?)?\.?$/i;
407
408/**
409 * The timeline rows that repeat a usage-limit pause while its recovery notice shows: the run's
410 * limit failure, Claude's pause notices, and the provider's own limit message. They return to
411 * history once another run executes.
412 */
413export function deriveUsageLimitNoticeRows(
414 projection: ThreadStructure,
415): UsageLimitNoticeRows | null {
416 const runtime = deriveThreadStructureRuntime(projection);
417 const run = presentedLatestRun(projection);
418 if (runtime?.status !== "failed" || runtime.lastErrorClass !== "usage_limit" || run === null) {
419 return null;
420 }
421 const failure = latestRootProviderFailure(run, projection.turnItems);
422 const itemIds = new Set<ThreadStructureTurnItem["id"]>();
423 let failedProviderTurnId: ThreadStructureTurnItem["providerTurnId"] = null;
424 for (const item of projection.turnItems) {
425 if (item.runId !== run.id) continue;
426 if (
427 item.type === "error" &&
428 item.status === "failed" &&
429 item.nodeId === run.rootNodeId &&
430 item.failure.class === "usage_limit"
431 ) {
432 itemIds.add(item.id);
433 if (item.failure === failure) failedProviderTurnId = item.providerTurnId;
434 }
435 }
436 let providerNotice: ThreadStructureTurnItem | null = null;
437 if (failedProviderTurnId !== null) {
438 for (const item of projection.turnItems) {
439 if (item.runId !== run.id || item.providerTurnId !== failedProviderTurnId) continue;
440 if (
441 item.type === "system_notice" &&
442 item.nodeId === run.rootNodeId &&
443 item.parentItemId === null &&
444 item.nativeItemRef?.driver === "claudeAgent" &&
445 item.nativeItemRef.nativeId?.startsWith(`usage-limit:${failedProviderTurnId}:`) === true
446 ) {
447 itemIds.add(item.id);
448 }
449 if (
450 item.type === "assistant_message" &&
451 item.parentItemId === null &&
452 (providerNotice === null || item.ordinal > providerNotice.ordinal)
453 ) {
454 providerNotice = item;
455 }
456 }
457 }
458 return { itemIds, providerNoticeItemId: providerNotice?.id ?? null };
459}
460
461export function withoutUsageLimitNoticeRows<
462 Row extends OrchestrationV2ThreadProjection["visibleTurnItems"][number],
463>(rows: ReadonlyArray<Row>, notice: UsageLimitNoticeRows | null): ReadonlyArray<Row> {
464 if (notice === null) return rows;
465 const hidden = ({ item }: Row) =>
466 notice.itemIds.has(item.id) ||
467 (item.id === notice.providerNoticeItemId &&
468 item.type === "assistant_message" &&
469 PROVIDER_LIMIT_NOTICE.test(item.text.trim()));
470 return rows.some(hidden) ? rows.filter((row) => !hidden(row)) : rows;
471}
472
473/** Whether a usage-limit recovery holds the thread's snooze, so its notice offers Wake now. */
474export function limitRecoveryOwnsSnooze(input: {
475 readonly runId: RunId | null | undefined;
476 readonly resetAt: string | null | undefined;
477 readonly recovery: OrchestrationV2LimitRecovery | null | undefined;
478 readonly snoozedUntil: string | null | undefined;
479}): boolean {
480 const { runId, resetAt, recovery, snoozedUntil } = input;
481 return (
482 recovery?.snooze === true &&
483 runId != null &&
484 recovery.runId === runId &&
485 resetAt != null &&
486 recovery.resetAt === resetAt &&
487 snoozedUntil != null &&
488 Date.parse(snoozedUntil) === Date.parse(resetAt)
489 );
490}
491
492export function threadRuntimeHasInterruptibleRun(
493 runtime: ThreadStructureRuntime | null | undefined,
494): boolean {
495 return (
496 threadRuntimeIsActive(runtime) &&
497 runtime?.activeRunId !== null &&
498 runtime?.activeRunId !== undefined
499 );
500}
501
502type BackgroundWorkKind = OrchestrationV2PendingBackgroundTask["kind"];
503
504const BACKGROUND_WORK_KINDS: Record<
505 BackgroundWorkKind,
506 { readonly order: number; readonly singular: string; readonly plural: string }
507> = {
508 subagent: { order: 0, singular: "subagent", plural: "subagents" },
509 command: { order: 1, singular: "command", plural: "commands" },
510 monitor: { order: 2, singular: "monitor", plural: "monitors" },
511 background_task: { order: 3, singular: "background task", plural: "background tasks" },
512};
513
514export interface RunStopPresentation {
515 /** `unresponsive`: the provider did not confirm the stop in time, so Force stop is offered. */
516 readonly state: "stopping" | "unresponsive";
517 readonly runId: ThreadStructure["runs"][number]["id"];
518 readonly requestedAt: string;
519 /** The run already ended; the Stop targeted its background work. */
520 readonly background: boolean;
521}
522
523/**
524 * A Stop the provider has not finished yet. It stays `stopping` while the agent winds down,
525 * since some providers confirm the request before the turn actually ends. On a run that
526 * already ended, only an unanswered Stop of its background work counts.
527 */
528export function presentRunStop(
529 projection: Pick<ThreadStructure, "runs" | "turnItems">,
530): RunStopPresentation | null {
531 let active: ThreadStructure["runs"][number] | null = null;
532 let latest: ThreadStructure["runs"][number] | null = null;
533 for (const candidate of projection.runs) {
534 // Queued follow-ups never ran, so they hold no Stop of their own.
535 if (candidate.status !== "queued" && candidate.ordinal > (latest?.ordinal ?? -1))
536 latest = candidate;
537 if (
538 INTERRUPTIBLE_RUN_STATUSES.has(candidate.status) &&
539 candidate.ordinal > (active?.ordinal ?? -1)
540 )
541 active = candidate;
542 }
543 const run = active ?? latest;
544 if (run === null) return null;
545 let request:
546 | Extract<ThreadStructure["turnItems"][number], { type: "run_interrupt_request" }>
547 | undefined;
548 for (const item of projection.turnItems) {
549 if (item.runId !== run.id) continue;
550 if (item.type === "run_interrupt_result") return null;
551 if (item.type === "run_interrupt_request") request = item;
552 }
553 if (request === undefined || (active === null && request.unresponsiveAt === undefined))
554 return null;
555 return {
556 state: request.unresponsiveAt === undefined ? "stopping" : "unresponsive",
557 runId: run.id,
558 requestedAt: DateTime.formatIso(request.startedAt ?? request.updatedAt),
559 background: active === null,
560 };
561}
562
563export interface PendingBackgroundWorkItem {
564 readonly taskId: string;
565 readonly kind: BackgroundWorkKind;
566 readonly label: string;
567 readonly childThreadId: ThreadId | undefined;
568}
569
570export interface PendingBackgroundWorkPresentation {
571 readonly title: string;
572 readonly items: ReadonlyArray<PendingBackgroundWorkItem>;
573 readonly waiting: boolean;
574}
575
576function joinWithAnd(parts: ReadonlyArray<string>): string {
577 if (parts.length <= 1) return parts.join("");
578 return `${parts.slice(0, -1).join(", ")} and ${parts.at(-1)}`;
579}
580
581// The Claude adapter forces a stuck query closed after 10 seconds.
582export const BACKGROUND_WORK_STOP_RETRY_AFTER_MS = 10_000;
583
584export interface BackgroundWorkStopRequest {
585 readonly threadKey: string;
586 readonly taskIds: ReadonlyArray<string>;
587 readonly retryAt: number | null;
588}
589
590/** Acceptance does not end the tasks, so an accepted Stop stays pending while they are listed. */
591export function isBackgroundWorkStopPending(
592 request: BackgroundWorkStopRequest | null,
593 threadKey: string,
594 tasks: ReadonlyArray<Pick<OrchestrationV2PendingBackgroundTask, "taskId">>,
595 retryExpired: boolean,
596): boolean {
597 if (request === null || request.threadKey !== threadKey || retryExpired) return false;
598 return request.retryAt === null || tasks.some((task) => request.taskIds.includes(task.taskId));
599}
600
601export function presentPendingBackgroundWork(
602 tasks: ReadonlyArray<OrchestrationV2PendingBackgroundTask>,
603): PendingBackgroundWorkPresentation | null {
604 if (tasks.length === 0) return null;
605 const waiting = backgroundWorkHoldsCompletion(tasks);
606 const items = tasks
607 .map((task): PendingBackgroundWorkItem => {
608 const description = task.description?.trim();
609 const label =
610 description === undefined
611 ? undefined
612 : task.kind === "subagent"
613 ? formatSubagentDisplayTitle(description).trim()
614 : task.kind === "command" || task.kind === "monitor"
615 ? commandDisplayText(description)
616 : description;
617 return {
618 taskId: task.taskId,
619 kind: task.kind,
620 label:
621 label === undefined || label.length === 0
622 ? BACKGROUND_WORK_KINDS[task.kind].singular
623 : label,
624 childThreadId: task.kind === "subagent" ? task.childThreadId : undefined,
625 };
626 })
627 // `map` returned a new array; Hermes has no `toSorted`.
628 .sort(
629 (left, right) =>
630 BACKGROUND_WORK_KINDS[left.kind].order - BACKGROUND_WORK_KINDS[right.kind].order,
631 );
632 const [only] = items;
633 if (items.length === 1 && only !== undefined) {
634 const noun = BACKGROUND_WORK_KINDS[only.kind].singular;
635 const named = only.label !== noun;
636 const title = named
637 ? `${waiting ? "Waiting" : "Running"} · ${only.label}`
638 : waiting
639 ? `Waiting on a ${noun}`
640 : `Running a ${noun}`;
641 return { title, items, waiting };
642 }
643 const counts = new Map<BackgroundWorkKind, number>();
644 for (const item of items) counts.set(item.kind, (counts.get(item.kind) ?? 0) + 1);
645 const groups = Array.from(counts, ([kind, count]) => {
646 const { singular, plural } = BACKGROUND_WORK_KINDS[kind];
647 return `${count} ${count === 1 ? singular : plural}`;
648 });
649 return { title: `${waiting ? "Waiting on" : "Running"} ${joinWithAnd(groups)}`, items, waiting };
650}
651
652export interface ProviderGoalPresentation {
653 readonly title: string;
654 readonly objective: string;
655 readonly usage: string | null;
656 readonly canResume: boolean;
657}
658
659const PROVIDER_GOAL_TITLES: Record<OrchestrationV2ProviderGoal["status"], string> = {
660 active: "Pursuing goal",
661 paused: "Goal paused",
662 blocked: "Goal blocked",
663 usage_limited: "Goal hit a usage limit",
664 budget_limited: "Goal reached its token budget",
665 complete: "Goal complete",
666};
667
668function formatGoalTokens(tokens: number): string {
669 if (tokens < 1_000) return `${tokens}`;
670 const thousands = Math.round(tokens / 1_000);
671 if (thousands < 1_000) return `${thousands}k`;
672 return `${(tokens / 1_000_000).toFixed(1).replace(/\.0$/, "")}m`;
673}
674
675export function presentProviderGoal(
676 goal: OrchestrationV2ProviderGoal,
677 working: boolean,
678): ProviderGoalPresentation {
679 const usage: Array<string> = [];
680 if (goal.tokensUsed !== undefined && goal.tokensUsed > 0) {
681 usage.push(
682 goal.tokenBudget == null
683 ? `${formatGoalTokens(goal.tokensUsed)} tokens`
684 : `${formatGoalTokens(goal.tokensUsed)} / ${formatGoalTokens(goal.tokenBudget)} tokens`,
685 );
686 }
687 if (goal.timeUsedSeconds !== undefined && goal.timeUsedSeconds >= 60) {
688 usage.push(formatDuration(goal.timeUsedSeconds * 1_000));
689 }
690 if (goal.checks !== undefined && goal.checks > 0) {
691 usage.push(`${goal.checks} ${goal.checks === 1 ? "check" : "checks"}`);
692 }
693 return {
694 title: goal.status === "active" && !working ? "Goal set" : PROVIDER_GOAL_TITLES[goal.status],
695 objective: goal.objective,
696 usage: usage.length === 0 ? null : usage.join(" · "),
697 canResume: goal.status !== "active" && goal.status !== "complete",
698 };
699}
700
701export function notificationChildThreadId(
702 source: OrchestrationV2NotificationSource,
703): ThreadId | undefined {
704 switch (source.kind) {
705 case "subagent":
706 case "delegated_task":
707 return source.childThreadId;
708 case "command":
709 case "monitor":
710 case "background_task":
711 case "system":
712 return undefined;
713 default:
714 source satisfies never;
715 return undefined;
716 }
717}