agent-relay-runner 0.38.0 → 0.40.0
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.
- package/package.json +2 -2
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/src/adapter.ts +5 -1
- package/src/adapters/claude.ts +53 -1
- package/src/adapters/codex.ts +38 -7
- package/src/runner.ts +70 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-runner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "Unified provider lifecycle runner for Agent Relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"directory": "runner"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"agent-relay-sdk": "0.2.
|
|
23
|
+
"agent-relay-sdk": "0.2.25"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/bun": "latest",
|
package/src/adapter.ts
CHANGED
|
@@ -36,13 +36,17 @@ export type ProviderStatusUpdate = SemanticStatus | ProviderStatusEvent;
|
|
|
36
36
|
* same lane Claude's transcript capture uses. Provider-independent boundary.
|
|
37
37
|
*/
|
|
38
38
|
export interface ProviderSessionEvent {
|
|
39
|
-
type: "prompt" | "response" | "reasoning" | "tool";
|
|
39
|
+
type: "prompt" | "response" | "narration" | "reasoning" | "tool";
|
|
40
40
|
body: string;
|
|
41
41
|
origin?: "chat" | "terminal" | "provider";
|
|
42
42
|
turnId?: string;
|
|
43
43
|
label?: string;
|
|
44
44
|
status?: "running" | "completed" | "failed";
|
|
45
45
|
streaming?: boolean;
|
|
46
|
+
/** Stable provider-side step id (Codex app-server item id). Carried into
|
|
47
|
+
* MessageSessionMeta.stepId so the server upserts the step's row in place instead of
|
|
48
|
+
* appending a duplicate (a tool's running→completed, a streamed reasoning row). */
|
|
49
|
+
stepId?: string;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
export interface ProviderConfig {
|
package/src/adapters/claude.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
|
-
import type
|
|
5
|
+
import { extractClaudeModelUnavailableMessage, type Message } from "agent-relay-sdk";
|
|
6
6
|
import { shellEscape as shellQuote } from "agent-relay-sdk/shell-utils";
|
|
7
7
|
import { tmuxCommand, tmuxHasSession } from "agent-relay-sdk/tmux-utils";
|
|
8
8
|
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
@@ -21,6 +21,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
21
21
|
private statusCb: (status: ProviderStatusUpdate) => void = () => {};
|
|
22
22
|
private tmuxWatcher?: Timer;
|
|
23
23
|
private turnWatcher?: Timer;
|
|
24
|
+
private modelUnavailableReported = false;
|
|
24
25
|
|
|
25
26
|
onStatusChange(cb: (status: ProviderStatusUpdate) => void): void {
|
|
26
27
|
this.statusCb = cb;
|
|
@@ -285,6 +286,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
285
286
|
|
|
286
287
|
private async spawnHeadless(config: RunnerSpawnConfig, spawnArgs: SpawnArgs): Promise<ManagedProcess> {
|
|
287
288
|
const { sessionName, socketName, args: tmuxArgs } = this.buildTmuxArgs(config, spawnArgs);
|
|
289
|
+
this.modelUnavailableReported = false;
|
|
288
290
|
|
|
289
291
|
Bun.spawnSync(tmuxCommand(socketName, "kill-session", "-t", sessionName), {
|
|
290
292
|
stdin: "ignore", stdout: "ignore", stderr: "ignore",
|
|
@@ -332,6 +334,19 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
332
334
|
clearInterval(this.tmuxWatcher!);
|
|
333
335
|
this.tmuxWatcher = undefined;
|
|
334
336
|
this.statusCb("offline");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (this.modelUnavailableReported) return;
|
|
340
|
+
let pane = "";
|
|
341
|
+
try {
|
|
342
|
+
pane = captureTmuxPane(sessionName, socketName);
|
|
343
|
+
} catch {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const status = claudeModelUnavailableStatus(pane, sessionName);
|
|
347
|
+
if (status) {
|
|
348
|
+
this.modelUnavailableReported = true;
|
|
349
|
+
this.statusCb(status);
|
|
335
350
|
}
|
|
336
351
|
}, 2000);
|
|
337
352
|
}
|
|
@@ -505,6 +520,43 @@ export function claudePaneIsBusy(text: string): boolean {
|
|
|
505
520
|
return CLAUDE_BUSY_SPINNER_RE.test(text) || text.includes("esc to interrupt");
|
|
506
521
|
}
|
|
507
522
|
|
|
523
|
+
export function claudeModelUnavailableStatus(text: string, sessionName?: string): ProviderStatusUpdate | null {
|
|
524
|
+
const message = extractClaudeModelUnavailableMessage(text);
|
|
525
|
+
if (!message) return null;
|
|
526
|
+
return {
|
|
527
|
+
status: "error",
|
|
528
|
+
clear: ["provider-turn", "subagent"],
|
|
529
|
+
providerState: {
|
|
530
|
+
state: "failed",
|
|
531
|
+
reason: "model-unavailable",
|
|
532
|
+
message,
|
|
533
|
+
source: "claude-pane",
|
|
534
|
+
terminal: true,
|
|
535
|
+
...(sessionName ? { sessionName } : {}),
|
|
536
|
+
recommendedAction: "Choose a different Claude model before restarting this agent.",
|
|
537
|
+
},
|
|
538
|
+
metadata: {
|
|
539
|
+
terminalFailureReason: "model-unavailable",
|
|
540
|
+
terminalFailureMessage: message,
|
|
541
|
+
},
|
|
542
|
+
timeline: {
|
|
543
|
+
status: "provider.restart_decision",
|
|
544
|
+
id: `provider-model-unavailable-${Date.now()}`,
|
|
545
|
+
timestamp: Date.now(),
|
|
546
|
+
title: "Provider restart skipped",
|
|
547
|
+
body: message,
|
|
548
|
+
icon: "ti-player-stop",
|
|
549
|
+
metadata: {
|
|
550
|
+
eventType: "provider.restart_decision",
|
|
551
|
+
decision: "stop-surface",
|
|
552
|
+
reason: "model-unavailable",
|
|
553
|
+
modelUnavailable: true,
|
|
554
|
+
modelUnavailableMessage: message,
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
508
560
|
async function waitForClaudeInputReady(sessionName: string, timeoutMs = CLAUDE_TMUX_READY_TIMEOUT_MS, socketName?: string): Promise<void> {
|
|
509
561
|
const deadline = Date.now() + timeoutMs;
|
|
510
562
|
while (Date.now() < deadline) {
|
package/src/adapters/codex.ts
CHANGED
|
@@ -476,11 +476,18 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
476
476
|
const type = stringValue(item.type);
|
|
477
477
|
const turnId = this.activeTurnId;
|
|
478
478
|
const itemId = codexItemId(item);
|
|
479
|
+
// A completed non-reasoning item ends the reasoning segment that preceded it — flush the
|
|
480
|
+
// buffered reasoning first so it lands in transcript order, ahead of this tool/message.
|
|
481
|
+
if (type !== "reasoning") this.flushBufferedReasoning();
|
|
479
482
|
if (type === "agentMessage") {
|
|
480
483
|
const text = (stringValue(item.text) ?? (itemId ? this.itemTextBuffers.get(itemId) : undefined))?.trim();
|
|
481
484
|
if (text) {
|
|
482
485
|
this.turnMessages.push(text);
|
|
483
486
|
this.recordInsightEvent({ type: "turn" }); // a substantive assistant turn
|
|
487
|
+
// Stream the assistant text into the trace as narration (Claude parity). The closing
|
|
488
|
+
// response bubble at turn end repeats the final block; the dashboard suppresses the
|
|
489
|
+
// matching narration so it isn't shown twice.
|
|
490
|
+
this.sessionEventCb({ type: "narration", origin: "provider", body: text, ...(turnId ? { turnId } : {}) });
|
|
484
491
|
}
|
|
485
492
|
if (itemId) this.itemTextBuffers.delete(itemId);
|
|
486
493
|
if (itemId) this.itemTextBufferTypes.delete(itemId);
|
|
@@ -495,23 +502,43 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
495
502
|
return;
|
|
496
503
|
}
|
|
497
504
|
if (type === "reasoning") {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
if (itemId)
|
|
505
|
+
// Codex has no reliable reasoning item/completed carrying text — the stream IS the deltas.
|
|
506
|
+
// Keep the longer of (accumulated deltas, item.content) in the buffer and emit at the next
|
|
507
|
+
// boundary (next item / turn end), the same coarse signal the Claude reasoning tail uses.
|
|
508
|
+
const fromItem = codexReasoningText(item);
|
|
509
|
+
if (itemId && fromItem) {
|
|
510
|
+
const existing = this.itemTextBuffers.get(itemId) ?? "";
|
|
511
|
+
if (fromItem.length >= existing.length) this.itemTextBuffers.set(itemId, fromItem);
|
|
512
|
+
this.itemTextBufferTypes.set(itemId, "reasoning");
|
|
513
|
+
}
|
|
503
514
|
return;
|
|
504
515
|
}
|
|
505
516
|
const tool = codexToolSummary(type, item);
|
|
506
517
|
if (tool) {
|
|
507
518
|
this.recordInsightEvent({ type: "tool", name: codexInsightToolName(type, item) });
|
|
508
519
|
if (codexItemFailed(item)) this.recordInsightEvent({ type: "tool_error" });
|
|
509
|
-
|
|
520
|
+
// stepId = the app-server item id: the server upserts the running step emitted on
|
|
521
|
+
// item/started into this completed state IN PLACE, so the tool persists as ONE row.
|
|
522
|
+
this.sessionEventCb({ type: "tool", origin: "provider", body: tool.body, label: tool.label, status: "completed", ...(turnId ? { turnId } : {}), ...(itemId ? { stepId: itemId } : {}) });
|
|
510
523
|
}
|
|
511
524
|
if (itemId) this.itemTextBuffers.delete(itemId);
|
|
512
525
|
if (itemId) this.itemTextBufferTypes.delete(itemId);
|
|
513
526
|
}
|
|
514
527
|
|
|
528
|
+
// Emit any buffered reasoning as discrete trace blocks (appended, in transcript order). Codex
|
|
529
|
+
// streams reasoning as deltas with no reliable completion event, so we flush coarsely at item
|
|
530
|
+
// and turn boundaries — the signal the Claude reasoning tail uses — never a codex-only timer.
|
|
531
|
+
private flushBufferedReasoning(): void {
|
|
532
|
+
const turnId = this.activeTurnId;
|
|
533
|
+
for (const [itemId, text] of [...this.itemTextBuffers.entries()]) {
|
|
534
|
+
if (this.itemTextBufferTypes.get(itemId) !== "reasoning") continue;
|
|
535
|
+
this.itemTextBuffers.delete(itemId);
|
|
536
|
+
this.itemTextBufferTypes.delete(itemId);
|
|
537
|
+
const body = text.trim();
|
|
538
|
+
if (body) this.sessionEventCb({ type: "reasoning", origin: "provider", body, ...(turnId ? { turnId } : {}) });
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
515
542
|
// #183/#184: append to the session-event log with a soft cap. On overflow we drop the
|
|
516
543
|
// oldest half; the runner detects the resulting length shrink and resets its segment
|
|
517
544
|
// cursor (worst case: one slightly-truncated datapoint on a pathologically long session).
|
|
@@ -537,8 +564,11 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
537
564
|
const turnId = this.activeTurnId;
|
|
538
565
|
|
|
539
566
|
if (method.includes("/started") || method.includes(".started")) {
|
|
567
|
+
// A new item starting ends the prior reasoning segment — flush it ahead of this step.
|
|
568
|
+
this.flushBufferedReasoning();
|
|
540
569
|
const tool = codexToolSummary(type, item ?? params ?? {});
|
|
541
|
-
|
|
570
|
+
// stepId = item id so the server upserts this running step → completed IN PLACE (one row).
|
|
571
|
+
if (tool) this.sessionEventCb({ type: "tool", origin: "provider", body: tool.body, label: tool.label, status: "running", streaming: true, ...(turnId ? { turnId } : {}), ...(itemId ? { stepId: itemId } : {}) });
|
|
542
572
|
return;
|
|
543
573
|
}
|
|
544
574
|
|
|
@@ -570,6 +600,7 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
570
600
|
}
|
|
571
601
|
|
|
572
602
|
private finishMainTurn(): void {
|
|
603
|
+
this.flushBufferedReasoning(); // surface any trailing reasoning before the closing response
|
|
573
604
|
this.flushTurnResponse();
|
|
574
605
|
const turnId = this.activeTurnId;
|
|
575
606
|
this.activeTurnId = undefined;
|
package/src/runner.ts
CHANGED
|
@@ -251,6 +251,12 @@ export class AgentRunner {
|
|
|
251
251
|
// its final response. Set when a provider-turn starts, cleared when it ends.
|
|
252
252
|
private currentTurnId?: string;
|
|
253
253
|
private currentTurnStartedAt?: number;
|
|
254
|
+
// True while a turn that was already in flight is being compacted. Claude's PostCompact
|
|
255
|
+
// hook posts a single `idle`, but a mid-turn compaction RESUMES the turn afterward — so
|
|
256
|
+
// treating that idle as a turn end (flip idle, kill the reasoning tail) makes chat go dark
|
|
257
|
+
// until the next prompt. Set when `compacting` arrives with a turn running; cleared on the
|
|
258
|
+
// genuine end (a plain idle with no compaction timeline).
|
|
259
|
+
private compactionMidTurn = false;
|
|
254
260
|
// Prompt-echo dedup: a short, time-bounded queue of prompts the runner itself
|
|
255
261
|
// injected (chat box or initial prompt) that are still awaiting their matching
|
|
256
262
|
// UserPromptSubmit echo. A single slot dropped earlier entries when several prompts
|
|
@@ -269,6 +275,7 @@ export class AgentRunner {
|
|
|
269
275
|
// Tracks whether the provider is in a legitimate blocked/approval state, so the
|
|
270
276
|
// busy reconciler doesn't mistake a permission prompt for a stuck-busy turn.
|
|
271
277
|
private providerBlocked = false;
|
|
278
|
+
private terminalFailure?: { reason: string; message: string; providerState?: Record<string, unknown> };
|
|
272
279
|
// Reasoning tailer (item 5): streams the in-flight turn's reasoning/tool steps
|
|
273
280
|
// from the Claude transcript into chat as discreet session events.
|
|
274
281
|
private reasoningTail?: { timer: ReturnType<typeof setInterval>; seen: Set<string> };
|
|
@@ -1092,10 +1099,47 @@ export class AgentRunner {
|
|
|
1092
1099
|
} else if (status === "idle") {
|
|
1093
1100
|
this.providerBlocked = false;
|
|
1094
1101
|
}
|
|
1102
|
+
if (typeof update !== "string" && status === "error") {
|
|
1103
|
+
const terminalReason = typeof update.metadata?.terminalFailureReason === "string"
|
|
1104
|
+
? update.metadata.terminalFailureReason
|
|
1105
|
+
: typeof update.providerState?.reason === "string"
|
|
1106
|
+
? update.providerState.reason
|
|
1107
|
+
: "provider-error";
|
|
1108
|
+
const terminalMessage = typeof update.metadata?.terminalFailureMessage === "string"
|
|
1109
|
+
? update.metadata.terminalFailureMessage
|
|
1110
|
+
: typeof update.providerState?.message === "string"
|
|
1111
|
+
? update.providerState.message
|
|
1112
|
+
: "Provider reported an unrecoverable error.";
|
|
1113
|
+
this.terminalFailure = {
|
|
1114
|
+
reason: terminalReason,
|
|
1115
|
+
message: terminalMessage,
|
|
1116
|
+
...(update.providerState ? { providerState: update.providerState } : {}),
|
|
1117
|
+
};
|
|
1118
|
+
} else if (status === "idle" || status === "busy") {
|
|
1119
|
+
this.terminalFailure = undefined;
|
|
1120
|
+
}
|
|
1121
|
+
// Compaction lifecycle (Claude PreCompact→`compacting` busy / PostCompact→`compacted`
|
|
1122
|
+
// idle). A `compacted` idle for a turn that predated compaction is a hook artifact — the
|
|
1123
|
+
// turn resumes — so it must NOT end the turn. Discriminate on whether a turn was running,
|
|
1124
|
+
// captured BEFORE the busy logic below mints a currentTurnId (it does for /compact at idle).
|
|
1125
|
+
const timelineStatus = typeof update !== "string" ? update.timeline?.status : undefined;
|
|
1126
|
+
if (timelineStatus === "compacting") {
|
|
1127
|
+
this.compactionMidTurn = this.currentTurnId !== undefined;
|
|
1128
|
+
} else if (timelineStatus === "compacted") {
|
|
1129
|
+
this.publishCompactionNotice();
|
|
1130
|
+
if (this.compactionMidTurn) {
|
|
1131
|
+
// Keep the turn (and its live reasoning tail) alive; the genuine Stop hook ends it.
|
|
1132
|
+
// pendingTimelineEvent (the marker) was set above and is consumed by publishStatus.
|
|
1133
|
+
this.sessionLog(`compaction completed mid-turn (turn ${this.currentTurnId ?? "?"}) — staying busy`);
|
|
1134
|
+
this.publishStatus();
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1095
1138
|
if (status === "busy" && reason === "provider-turn") {
|
|
1096
1139
|
if (!this.currentTurnId) {
|
|
1097
1140
|
this.currentTurnId = typeof update !== "string" && update.id ? update.id : crypto.randomUUID();
|
|
1098
1141
|
this.currentTurnStartedAt = Date.now();
|
|
1142
|
+
this.compactionMidTurn = false;
|
|
1099
1143
|
this.sessionLog(`turn started (turn ${this.currentTurnId})`);
|
|
1100
1144
|
}
|
|
1101
1145
|
this.armBusyReconciler();
|
|
@@ -1103,6 +1147,7 @@ export class AgentRunner {
|
|
|
1103
1147
|
if (this.currentTurnId) this.sessionLog(`turn ended via provider idle (turn ${this.currentTurnId})`);
|
|
1104
1148
|
this.currentTurnId = undefined;
|
|
1105
1149
|
this.currentTurnStartedAt = undefined;
|
|
1150
|
+
this.compactionMidTurn = false;
|
|
1106
1151
|
this.disarmBusyReconciler();
|
|
1107
1152
|
this.stopReasoningTail();
|
|
1108
1153
|
}
|
|
@@ -1219,8 +1264,12 @@ export class AgentRunner {
|
|
|
1219
1264
|
// retried until it lands. occurredAt is stamped now so a queued event reports when it
|
|
1220
1265
|
// truly happened, not when the server finally accepted it. Routed through the fast-lane
|
|
1221
1266
|
// sessionOutbox (#332) so a transient trace failure can't head-of-line block real messages.
|
|
1267
|
+
// A stepId-bearing step (Codex tool running→completed, streamed reasoning/response) uses a
|
|
1268
|
+
// STABLE idempotency key so the server upserts the row in place instead of appending a dup.
|
|
1269
|
+
const stepId = input.session.stepId;
|
|
1222
1270
|
this.sessionOutbox.enqueue({
|
|
1223
1271
|
kind: "session-message",
|
|
1272
|
+
...(stepId ? { idempotencyKey: `session-step:${input.from}:${input.session.turnId ?? ""}:${stepId}` } : {}),
|
|
1224
1273
|
payload: {
|
|
1225
1274
|
from: input.from,
|
|
1226
1275
|
to: input.to,
|
|
@@ -1514,6 +1563,7 @@ export class AgentRunner {
|
|
|
1514
1563
|
...(event.label ? { label: event.label } : {}),
|
|
1515
1564
|
...(event.status ? { status: event.status } : {}),
|
|
1516
1565
|
...(event.streaming !== undefined ? { streaming: event.streaming } : {}),
|
|
1566
|
+
...(event.stepId ? { stepId: event.stepId } : {}),
|
|
1517
1567
|
},
|
|
1518
1568
|
});
|
|
1519
1569
|
}
|
|
@@ -1597,6 +1647,7 @@ export class AgentRunner {
|
|
|
1597
1647
|
this.sessionLog(`force-clearing stuck provider-turn (${reason})`);
|
|
1598
1648
|
this.claims.clearWorkKind("provider-turn");
|
|
1599
1649
|
this.currentTurnId = undefined;
|
|
1650
|
+
this.compactionMidTurn = false;
|
|
1600
1651
|
this.publishStatus();
|
|
1601
1652
|
}
|
|
1602
1653
|
|
|
@@ -1677,13 +1728,26 @@ export class AgentRunner {
|
|
|
1677
1728
|
this.reasoningTail = undefined;
|
|
1678
1729
|
}
|
|
1679
1730
|
|
|
1731
|
+
// Mirror a discreet, durable "context compacted" marker into chat via the existing
|
|
1732
|
+
// session-mirror lane (not a parallel channel). `notice` renders as an inline timeline
|
|
1733
|
+
// marker (never a bubble) and survives reload, unlike the ephemeral timeline-status one.
|
|
1734
|
+
private publishCompactionNotice(): void {
|
|
1735
|
+
this.publishSessionEvent({
|
|
1736
|
+
from: this.agentId,
|
|
1737
|
+
to: "user",
|
|
1738
|
+
body: "🗜 Context compacted",
|
|
1739
|
+
session: { type: "notice", origin: "provider", label: "compacted", ...(this.currentTurnId ? { turnId: this.currentTurnId } : {}) },
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1680
1743
|
private publishStatus(): void {
|
|
1681
1744
|
this.claims.expire();
|
|
1682
1745
|
const status = this.claims.currentStatus();
|
|
1683
1746
|
const agentStatus = runnerAgentStatus(status);
|
|
1684
1747
|
const activeWork = this.claims.activeWork();
|
|
1685
1748
|
const activeSubagents = activeWork.filter((item) => item.kind === "subagent");
|
|
1686
|
-
const
|
|
1749
|
+
const terminalFailure = this.terminalFailure;
|
|
1750
|
+
const providerState = terminalFailure?.providerState ?? providerStateFromActiveWork(activeWork);
|
|
1687
1751
|
this.bus.setSemanticStatus(status === "offline" || status === "error" ? "idle" : status);
|
|
1688
1752
|
const timelineEvent = this.pendingTimelineEvent;
|
|
1689
1753
|
this.pendingTimelineEvent = undefined;
|
|
@@ -1704,6 +1768,11 @@ export class AgentRunner {
|
|
|
1704
1768
|
lifecycleAction: this.lifecycleAction ?? null,
|
|
1705
1769
|
profile: this.options.profile ?? null,
|
|
1706
1770
|
...(status === "error" ? { terminalStatus: "error" } : {}),
|
|
1771
|
+
...(terminalFailure ? {
|
|
1772
|
+
lastError: terminalFailure.message,
|
|
1773
|
+
terminalFailureReason: terminalFailure.reason,
|
|
1774
|
+
terminalFailureMessage: terminalFailure.message,
|
|
1775
|
+
} : {}),
|
|
1707
1776
|
busyReasons: this.claims.reasons(),
|
|
1708
1777
|
activeWork,
|
|
1709
1778
|
activeSubagents,
|