brl-subagent 2.3.5 → 2.3.7
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/AGENT.md +7 -31
- package/README.md +1 -12
- package/package.json +6 -6
- package/src/index.ts +93 -89
- package/src/logging.ts +39 -15
- package/src/notify-completion.ts +38 -7
- package/src/runner.ts +2 -1
- package/src/session-manager.ts +199 -37
- package/src/state.ts +0 -10
- package/src/templates.ts +1 -1
- package/src/transcript.ts +11 -6
- package/src/tui.ts +13 -79
- package/src/types.ts +143 -17
- package/src/update.ts +0 -109
package/src/session-manager.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync } from 'fs';
|
|
4
|
-
import type { BackgroundAgent, AgentStatus, GitMode, SubagentResult, SubagentRun, ThinkingLevel, SubagentToolOptions, UsageStats } from './types';
|
|
5
|
-
import { EMPTY_USAGE, CUSTOM_ENTRY_TYPES, classifyError, SUBAGENT_ABORTED_MESSAGE } from './types';
|
|
4
|
+
import type { BackgroundAgent, AgentStatus, GitMode, SubagentResult, SubagentRun, ThinkingLevel, SubagentToolOptions, UsageStats, ErrorCategory } from './types';
|
|
5
|
+
import { EMPTY_USAGE, CUSTOM_ENTRY_TYPES, classifyError, classifyTerminalOutcome, isSubagentError, isProviderError, SUBAGENT_ABORTED_MESSAGE } from './types';
|
|
6
6
|
import { accumulateUsage } from './runner';
|
|
7
7
|
import * as eventBus from './event-bus';
|
|
8
8
|
import * as transcript from './transcript';
|
|
@@ -230,7 +230,10 @@ export function setAgentResult(id: string, result: SubagentResult): BackgroundAg
|
|
|
230
230
|
if (!agent) return null;
|
|
231
231
|
|
|
232
232
|
agent.result = result;
|
|
233
|
-
|
|
233
|
+
// Issue #179 (D1): honor the full terminal reason (stopReason error/aborted/
|
|
234
|
+
// length/incomplete), not exitCode alone — a mid-run provider death resolves
|
|
235
|
+
// with exitCode 0 and was previously recorded as 'completed'.
|
|
236
|
+
agent.status = isSubagentError(result) ? 'failed' : 'completed';
|
|
234
237
|
agent.completedAt = Date.now();
|
|
235
238
|
|
|
236
239
|
agents.set(id, agent);
|
|
@@ -238,6 +241,56 @@ export function setAgentResult(id: string, result: SubagentResult): BackgroundAg
|
|
|
238
241
|
return agent;
|
|
239
242
|
}
|
|
240
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Issue #179 (D4): when a run ends in a failure, the prose extracted by
|
|
246
|
+
* extractFinalOutput may come from an EARLIER turn — the failing turn carries
|
|
247
|
+
* no text of its own. Prefixing the failure reason makes the output honest
|
|
248
|
+
* instead of a plausible-looking success narrative.
|
|
249
|
+
*/
|
|
250
|
+
export function buildFailureHeadline(reason: string, output: string): string {
|
|
251
|
+
const trimmed = output.trim();
|
|
252
|
+
return trimmed ? `${reason}\n\n${trimmed}` : reason;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Issue #179 (D3/D4): a run that ended without a terminal answer must still
|
|
257
|
+
* record WHY. When the provider supplied no message, synthesize one from the
|
|
258
|
+
* stopReason so the run entry never claims a failure with an empty reason.
|
|
259
|
+
*/
|
|
260
|
+
function defaultTerminalMessage(stopReason: string | undefined): string {
|
|
261
|
+
switch (stopReason) {
|
|
262
|
+
case 'error':
|
|
263
|
+
return 'Run ended with a provider error (stopReason "error")';
|
|
264
|
+
case 'length':
|
|
265
|
+
return 'Run ended early: output length limit reached (stopReason "length")';
|
|
266
|
+
case 'toolUse':
|
|
267
|
+
return 'Run ended mid-turn without a final answer (stopReason "toolUse")';
|
|
268
|
+
case 'deferred':
|
|
269
|
+
return 'Run ended deferred without a final answer (stopReason "deferred")';
|
|
270
|
+
case 'pending':
|
|
271
|
+
return 'Run ended while still pending (stopReason "pending")';
|
|
272
|
+
default:
|
|
273
|
+
return 'Run ended without completing';
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Issue #179 (C1 fix): a failed run entry/result must carry a terminal reason
|
|
279
|
+
* that AGREES with its status — never undefined, never "stop". Any reason the
|
|
280
|
+
* classifier already treats as a failure (error / length / toolUse / deferred
|
|
281
|
+
* / pending / aborted) is coherent and kept; a missing or success reason is
|
|
282
|
+
* coerced to the honest failure reason derived from the classified category.
|
|
283
|
+
*/
|
|
284
|
+
function coherentFailureReason(
|
|
285
|
+
stopReason: string | undefined,
|
|
286
|
+
errorCategory: ErrorCategory | undefined,
|
|
287
|
+
): string {
|
|
288
|
+
if (stopReason && classifyTerminalOutcome(stopReason).status !== 'completed') {
|
|
289
|
+
return stopReason;
|
|
290
|
+
}
|
|
291
|
+
return errorCategory === 'aborted' ? 'aborted' : 'error';
|
|
292
|
+
}
|
|
293
|
+
|
|
241
294
|
/**
|
|
242
295
|
* Extract the final assistant text from a session's messages.
|
|
243
296
|
* Falls back to the LAST assistant message with non-empty text content — the
|
|
@@ -531,29 +584,51 @@ export async function spawnBackgroundSession(
|
|
|
531
584
|
// ("done"/"failed"). The guard keeps the entry single-finalized even when a
|
|
532
585
|
// settle handler throws after finalizing (markTerminalBestEffort catch-all).
|
|
533
586
|
let runFinalized = false;
|
|
534
|
-
const finalizeRunEntry = (
|
|
587
|
+
const finalizeRunEntry = (
|
|
588
|
+
status: "done" | "failed",
|
|
589
|
+
error?: string,
|
|
590
|
+
terminal?: { stopReason?: string; errorCategory?: ErrorCategory },
|
|
591
|
+
): void => {
|
|
535
592
|
if (runFinalized) return;
|
|
536
593
|
runFinalized = true;
|
|
537
594
|
const usage = agent.result?.usage ?? EMPTY_USAGE;
|
|
538
595
|
const finalOutput = agent.finalOutput ?? "";
|
|
539
|
-
// Issue #
|
|
540
|
-
//
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
// invisible to the SLA errorCategoryBreakdown and to failure diagnostics.
|
|
596
|
+
// Issue #179 (D1/D2): the classifier's explicit category wins (it knows the
|
|
597
|
+
// terminal stopReason); otherwise fall back to the legacy message-based
|
|
598
|
+
// classification (refused spawns, sync throws, preflight rejections, the
|
|
599
|
+
// catch-all) so no failure is ever left uncategorized.
|
|
544
600
|
const errorCategory =
|
|
545
|
-
status === "failed"
|
|
546
|
-
?
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
601
|
+
status === "failed"
|
|
602
|
+
? terminal?.errorCategory ??
|
|
603
|
+
(error
|
|
604
|
+
? classifyError({
|
|
605
|
+
messages: [],
|
|
606
|
+
usage: { ...EMPTY_USAGE },
|
|
607
|
+
exitCode: 1,
|
|
608
|
+
stderr: "",
|
|
609
|
+
errorMessage: error,
|
|
610
|
+
})
|
|
611
|
+
: undefined)
|
|
553
612
|
: undefined;
|
|
613
|
+
const stopReason =
|
|
614
|
+
status === "failed"
|
|
615
|
+
? coherentFailureReason(
|
|
616
|
+
terminal?.stopReason ?? agent.result?.stopReason,
|
|
617
|
+
errorCategory,
|
|
618
|
+
)
|
|
619
|
+
: terminal?.stopReason ?? agent.result?.stopReason;
|
|
554
620
|
const entry: SubagentRun = {
|
|
555
621
|
...run,
|
|
556
622
|
status,
|
|
623
|
+
// Issue #179 (D2): the run entry carries the CLASSIFIED reason, not just
|
|
624
|
+
// an opaque error string.
|
|
625
|
+
stopReason,
|
|
626
|
+
errorCategory,
|
|
627
|
+
// Issue #179 (D3): work volume + terminal-error signal. `turns`/`tokensOut`
|
|
628
|
+
// are already in the usage data; recording them here makes "did nothing"
|
|
629
|
+
// (turns 0) explicit versus "did work then died" (turns > 0, error).
|
|
630
|
+
turns: usage.turns,
|
|
631
|
+
finalTurnError: isProviderError(stopReason),
|
|
557
632
|
errorMessage: error,
|
|
558
633
|
finishedAt: new Date().toISOString(),
|
|
559
634
|
durationMs: Date.now() - startedAtMs,
|
|
@@ -570,6 +645,17 @@ export async function spawnBackgroundSession(
|
|
|
570
645
|
: run.originalParams,
|
|
571
646
|
};
|
|
572
647
|
pi.appendEntry(CUSTOM_ENTRY_TYPES.run, entry);
|
|
648
|
+
// Issue #179 (D6): the explicit settle line — the log's job is to record the
|
|
649
|
+
// CLASSIFIED OUTCOME, which cwd alone can never convey.
|
|
650
|
+
log.info("Background run settled", {
|
|
651
|
+
agentId: id,
|
|
652
|
+
status,
|
|
653
|
+
stopReason,
|
|
654
|
+
turns: usage.turns,
|
|
655
|
+
tokensOut: usage.output,
|
|
656
|
+
errorCategory,
|
|
657
|
+
error: error ?? null,
|
|
658
|
+
});
|
|
573
659
|
};
|
|
574
660
|
|
|
575
661
|
// Start transcript for this agent
|
|
@@ -582,8 +668,16 @@ export async function spawnBackgroundSession(
|
|
|
582
668
|
// final output while the session is still live, THEN release the ref — the
|
|
583
669
|
// capture must always precede the release or the output dies with the
|
|
584
670
|
// session graph. Single helper for all four settlement paths.
|
|
585
|
-
|
|
586
|
-
|
|
671
|
+
//
|
|
672
|
+
// Issue #179 (D4): an optional failureHeadline is prepended when the run did
|
|
673
|
+
// NOT complete — the extracted prose may come from an earlier turn, so it
|
|
674
|
+
// must not be presented as the run's output unqualified.
|
|
675
|
+
const captureAndReleaseSession = (failureHeadline?: string): void => {
|
|
676
|
+
const output = extractFinalOutput(session);
|
|
677
|
+
setAgentFinalOutput(
|
|
678
|
+
id,
|
|
679
|
+
failureHeadline ? buildFailureHeadline(failureHeadline, output) : output,
|
|
680
|
+
);
|
|
587
681
|
agent._sessionRef = undefined;
|
|
588
682
|
};
|
|
589
683
|
|
|
@@ -895,7 +989,8 @@ export async function spawnBackgroundSession(
|
|
|
895
989
|
// 1. stopAgent pre-set status to 'stopped' before calling abort();
|
|
896
990
|
// 2. the last assistant message carries stopReason "aborted".
|
|
897
991
|
const lastAssistant = [...(session.messages ?? [])].reverse().find(m => m.role === 'assistant');
|
|
898
|
-
const
|
|
992
|
+
const terminalStopReason = lastAssistant?.stopReason;
|
|
993
|
+
const aborted = agent.status === 'stopped' || terminalStopReason === 'aborted';
|
|
899
994
|
if (aborted) {
|
|
900
995
|
// Session was aborted — mark stopped. If stopAgent already flipped the
|
|
901
996
|
// status (pre-set 'stopped' + completedAt + event + transcript), only
|
|
@@ -931,13 +1026,70 @@ export async function spawnBackgroundSession(
|
|
|
931
1026
|
// (stop_subagent) sets no error, so fall back to the honest user-abort
|
|
932
1027
|
// stamp (→ 'aborted') instead of the old ambiguous 'Aborted or timed out'.
|
|
933
1028
|
recordSessionUsage();
|
|
934
|
-
finalizeRunEntry('failed', agent.error ?? SUBAGENT_ABORTED_MESSAGE
|
|
1029
|
+
finalizeRunEntry('failed', agent.error ?? SUBAGENT_ABORTED_MESSAGE, {
|
|
1030
|
+
stopReason:
|
|
1031
|
+
terminalStopReason && terminalStopReason !== 'stop'
|
|
1032
|
+
? terminalStopReason
|
|
1033
|
+
: 'aborted',
|
|
1034
|
+
});
|
|
935
1035
|
// Issue #31: capture the final output while the session is still
|
|
936
1036
|
// live, then release the ref on the terminal path (memory retention;
|
|
937
1037
|
// the poller treats a nulled ref on a terminal agent as expected).
|
|
938
1038
|
captureAndReleaseSession();
|
|
939
1039
|
return;
|
|
940
1040
|
}
|
|
1041
|
+
// Issue #179 (D1/D2): prompt() RESOLVED — but a resolved prompt is NOT
|
|
1042
|
+
// proof of success. The SDK's runWithLifecycle catches a mid-run provider
|
|
1043
|
+
// death internally and resolves with an assistant message carrying
|
|
1044
|
+
// stopReason "error"; the completion branch below would record it as a
|
|
1045
|
+
// false success (exitCode 0, no category). Classify the terminal outcome
|
|
1046
|
+
// from the last assistant message + the session error, reusing
|
|
1047
|
+
// classifyError for the provider-error case.
|
|
1048
|
+
const providerErrorMessage =
|
|
1049
|
+
lastAssistant?.errorMessage ?? session.state?.errorMessage;
|
|
1050
|
+
const outcome = classifyTerminalOutcome(terminalStopReason, providerErrorMessage);
|
|
1051
|
+
|
|
1052
|
+
if (outcome.status === 'failed') {
|
|
1053
|
+
// Mid-run provider death / truncation / incomplete turn.
|
|
1054
|
+
agent.status = 'failed';
|
|
1055
|
+
agent.completedAt = Date.now();
|
|
1056
|
+
const sanitizedError = sanitizeErrorMessage(
|
|
1057
|
+
providerErrorMessage ?? defaultTerminalMessage(terminalStopReason),
|
|
1058
|
+
effectiveCwd,
|
|
1059
|
+
);
|
|
1060
|
+
agent.error = sanitizedError;
|
|
1061
|
+
const gitInfo = cleanupWorkBranch();
|
|
1062
|
+
agent.result = {
|
|
1063
|
+
...(agent.result ?? {}),
|
|
1064
|
+
exitCode: 1,
|
|
1065
|
+
messages: agent.result?.messages ?? [],
|
|
1066
|
+
stderr: agent.result?.stderr ?? '',
|
|
1067
|
+
usage: agent.result?.usage ?? EMPTY_USAGE,
|
|
1068
|
+
stopReason: terminalStopReason,
|
|
1069
|
+
errorMessage: sanitizedError,
|
|
1070
|
+
errorCategory: outcome.errorCategory,
|
|
1071
|
+
...(gitInfo.gitBranch
|
|
1072
|
+
? { gitBranch: gitInfo.gitBranch, gitDiff: gitInfo.gitDiff }
|
|
1073
|
+
: {}),
|
|
1074
|
+
} as SubagentResult;
|
|
1075
|
+
// Issue #31: capture the final output while the session is still live.
|
|
1076
|
+
// Issue #179 (D4): surface the failure as the HEADLINE — the extracted
|
|
1077
|
+
// prose may come from an earlier turn, so it must not be presented as an
|
|
1078
|
+
// unqualified success narrative.
|
|
1079
|
+
captureAndReleaseSession(sanitizedError);
|
|
1080
|
+
// Issue #122: fold the real usage (turns/tokens) the run burned.
|
|
1081
|
+
recordSessionUsage();
|
|
1082
|
+
agents.set(id, agent);
|
|
1083
|
+
persistAgent(agent);
|
|
1084
|
+
finalizeRunEntry('failed', sanitizedError, {
|
|
1085
|
+
stopReason: terminalStopReason,
|
|
1086
|
+
errorCategory: outcome.errorCategory,
|
|
1087
|
+
});
|
|
1088
|
+
transcript.completeTranscript(id, 'failed');
|
|
1089
|
+
eventBus.emit(eventBus.createEvent('subagent:failed', id, { error: sanitizedError }));
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
941
1093
|
// Session completed
|
|
942
1094
|
agent.status = 'completed';
|
|
943
1095
|
agent.completedAt = Date.now();
|
|
@@ -951,6 +1103,7 @@ export async function spawnBackgroundSession(
|
|
|
951
1103
|
messages: [],
|
|
952
1104
|
stderr: '',
|
|
953
1105
|
usage: agent.result?.usage ?? EMPTY_USAGE,
|
|
1106
|
+
stopReason: terminalStopReason ?? 'stop',
|
|
954
1107
|
gitBranch: gitInfo.gitBranch,
|
|
955
1108
|
gitDiff: gitInfo.gitDiff,
|
|
956
1109
|
} as SubagentResult;
|
|
@@ -965,7 +1118,7 @@ export async function spawnBackgroundSession(
|
|
|
965
1118
|
agents.set(id, agent);
|
|
966
1119
|
persistAgent(agent);
|
|
967
1120
|
// Issue #98: mirror the agent-record flip in the session run entry.
|
|
968
|
-
finalizeRunEntry('done');
|
|
1121
|
+
finalizeRunEntry('done', undefined, { stopReason: terminalStopReason ?? 'stop' });
|
|
969
1122
|
transcript.completeTranscript(id, 'completed');
|
|
970
1123
|
eventBus.emit(eventBus.createEvent('subagent:completed', id, {}));
|
|
971
1124
|
} catch (err) {
|
|
@@ -1009,7 +1162,9 @@ export async function spawnBackgroundSession(
|
|
|
1009
1162
|
// agent.error ('Timed out...' → 'timeout'); a user cancel has no error
|
|
1010
1163
|
// and stamps 'Subagent aborted by user' (→ 'aborted').
|
|
1011
1164
|
recordSessionUsage();
|
|
1012
|
-
finalizeRunEntry('failed', agent.error ?? SUBAGENT_ABORTED_MESSAGE
|
|
1165
|
+
finalizeRunEntry('failed', agent.error ?? SUBAGENT_ABORTED_MESSAGE, {
|
|
1166
|
+
stopReason: 'aborted',
|
|
1167
|
+
});
|
|
1013
1168
|
// Issue #31: the stopped path is terminal too — capture + release.
|
|
1014
1169
|
captureAndReleaseSession();
|
|
1015
1170
|
transcript.completeTranscript(id, 'stopped');
|
|
@@ -1022,29 +1177,36 @@ export async function spawnBackgroundSession(
|
|
|
1022
1177
|
// result errorMessage, and the subagent:failed event (DRY, PR #63 review).
|
|
1023
1178
|
const sanitizedError = sanitizeErrorMessage(err.message, effectiveCwd);
|
|
1024
1179
|
agent.error = sanitizedError;
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1180
|
+
// Issue #179: record the failure on the result UNCONDITIONALLY (not only
|
|
1181
|
+
// on the git-diff path) so a failed run never persists exitCode 0.
|
|
1182
|
+
agent.result = {
|
|
1183
|
+
...(agent.result ?? {}),
|
|
1184
|
+
exitCode: 1,
|
|
1185
|
+
messages: agent.result?.messages ?? [],
|
|
1186
|
+
stderr: agent.result?.stderr ?? '',
|
|
1187
|
+
usage: agent.result?.usage ?? EMPTY_USAGE,
|
|
1188
|
+
// Issue #179 (C1 fix): a genuine preflight/run failure always carries
|
|
1189
|
+
// the failure reason — never leave the result with an undefined
|
|
1190
|
+
// stopReason that downstream display/aggregate paths read as success.
|
|
1191
|
+
stopReason: 'error',
|
|
1192
|
+
errorMessage: sanitizedError,
|
|
1193
|
+
...(gitInfo.gitBranch
|
|
1194
|
+
? { gitBranch: gitInfo.gitBranch, gitDiff: gitInfo.gitDiff }
|
|
1195
|
+
: {}),
|
|
1196
|
+
} as SubagentResult;
|
|
1037
1197
|
// Issue #31: capture the final output while the session is still live,
|
|
1038
1198
|
// then release the ref before the branch's persist — the persisted
|
|
1039
1199
|
// record is consistent and the live session graph is freed.
|
|
1040
|
-
|
|
1200
|
+
// Issue #179 (D4): lead with the failure reason (the prose may be from an
|
|
1201
|
+
// earlier turn).
|
|
1202
|
+
captureAndReleaseSession(sanitizedError);
|
|
1041
1203
|
// Issue #122: fold any assistant turns the preflight/run did produce —
|
|
1042
1204
|
// usually none (auth/model rejection) → honest zeros, never a crash.
|
|
1043
1205
|
recordSessionUsage();
|
|
1044
1206
|
agents.set(id, agent);
|
|
1045
1207
|
persistAgent(agent);
|
|
1046
1208
|
// Issue #98: mirror the agent-record flip in the session run entry.
|
|
1047
|
-
finalizeRunEntry('failed', sanitizedError);
|
|
1209
|
+
finalizeRunEntry('failed', sanitizedError, { stopReason: 'error' });
|
|
1048
1210
|
transcript.completeTranscript(id, 'failed');
|
|
1049
1211
|
eventBus.emit(eventBus.createEvent('subagent:failed', id, { error: sanitizedError }));
|
|
1050
1212
|
} catch (err) {
|
package/src/state.ts
CHANGED
|
@@ -142,8 +142,6 @@ export class SessionState {
|
|
|
142
142
|
circuitBreaker: this.defaultCircuitBreaker(),
|
|
143
143
|
slaTrackingEnabled: false,
|
|
144
144
|
slaWindowSize: 50,
|
|
145
|
-
updateCheckEnabled: true,
|
|
146
|
-
lastUpdateCheck: 0,
|
|
147
145
|
};
|
|
148
146
|
}
|
|
149
147
|
|
|
@@ -168,8 +166,6 @@ export class SessionState {
|
|
|
168
166
|
slaTrackingEnabled: this.config.slaTrackingEnabled,
|
|
169
167
|
slaWindowSize: this.config.slaWindowSize,
|
|
170
168
|
lastSLAMetrics: this.config.lastSLAMetrics,
|
|
171
|
-
updateCheckEnabled: this.config.updateCheckEnabled,
|
|
172
|
-
lastUpdateCheck: this.config.lastUpdateCheck,
|
|
173
169
|
});
|
|
174
170
|
}
|
|
175
171
|
|
|
@@ -258,10 +254,6 @@ export class SessionState {
|
|
|
258
254
|
this.config.lastSLAMetrics = data.lastSLAMetrics as import("./types").SLAMetrics;
|
|
259
255
|
}
|
|
260
256
|
|
|
261
|
-
// Restore update check fields
|
|
262
|
-
if (typeof data.updateCheckEnabled === "boolean") this.config.updateCheckEnabled = data.updateCheckEnabled;
|
|
263
|
-
if (typeof data.lastUpdateCheck === "number" && data.lastUpdateCheck >= 0) this.config.lastUpdateCheck = data.lastUpdateCheck;
|
|
264
|
-
|
|
265
257
|
this.log?.info("State restored from session", {
|
|
266
258
|
model: data.model ? `${data.model.provider}/${data.model.id}` : "none",
|
|
267
259
|
thinkingLevel: data.maxThinkingLevel,
|
|
@@ -508,8 +500,6 @@ export class SessionState {
|
|
|
508
500
|
this.config.slaTrackingEnabled = false;
|
|
509
501
|
this.config.slaWindowSize = 50;
|
|
510
502
|
this.config.lastSLAMetrics = undefined;
|
|
511
|
-
this.config.updateCheckEnabled = true;
|
|
512
|
-
this.config.lastUpdateCheck = 0;
|
|
513
503
|
}
|
|
514
504
|
}
|
|
515
505
|
|
package/src/templates.ts
CHANGED
|
@@ -407,7 +407,7 @@ export function validateTemplatePresetRefs(
|
|
|
407
407
|
dangling++;
|
|
408
408
|
log?.warn(
|
|
409
409
|
`Template "${template.name}" references preset "${template.preset}" which does not exist ` +
|
|
410
|
-
"— delegation will run preset-less with auto-route suppressed
|
|
410
|
+
"— delegation will run preset-less with auto-route suppressed",
|
|
411
411
|
{ template: template.name, preset: template.preset },
|
|
412
412
|
);
|
|
413
413
|
}
|
package/src/transcript.ts
CHANGED
|
@@ -19,8 +19,9 @@ export function __setOutputDir(dir: string): void {
|
|
|
19
19
|
* Ensure output directory exists
|
|
20
20
|
*/
|
|
21
21
|
function ensureOutputDir(): void {
|
|
22
|
-
// F6 (issue #29): transcripts
|
|
23
|
-
//
|
|
22
|
+
// F6 (issue #29): transcripts may hold task/steering text — the dir must be
|
|
23
|
+
// owner-only (0o700) so other local users cannot list the files. NOTE: the
|
|
24
|
+
// transcript is NOT the full conversation (see appendEntry below).
|
|
24
25
|
mkdirSync(OUTPUT_DIR, { recursive: true, mode: 0o700 });
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -52,8 +53,9 @@ export function startTranscript(agentId: string, task: string): string {
|
|
|
52
53
|
metadata: { task },
|
|
53
54
|
};
|
|
54
55
|
|
|
55
|
-
// F6 (issue #29):
|
|
56
|
-
//
|
|
56
|
+
// F6 (issue #29): append owner-only (0o600) on file CREATE (mode is not
|
|
57
|
+
// retroactive). The file holds the system start entry plus any steering
|
|
58
|
+
// entries appended below — NOT the full conversation.
|
|
57
59
|
appendFileSync(path, JSON.stringify(entry) + '\n', { encoding: 'utf-8', mode: 0o600 });
|
|
58
60
|
return path;
|
|
59
61
|
}
|
|
@@ -79,8 +81,11 @@ export function appendEntry(
|
|
|
79
81
|
metadata,
|
|
80
82
|
};
|
|
81
83
|
|
|
82
|
-
// F6 (issue #29):
|
|
83
|
-
//
|
|
84
|
+
// F6 (issue #29): append owner-only (0o600) on file CREATE (mode is not
|
|
85
|
+
// retroactive). NOTE (D5 / issue #179): this is NOT a conversation recorder —
|
|
86
|
+
// the only caller is steering (steerAgent), so the transcript holds the
|
|
87
|
+
// system start entry and steering lines. Do NOT describe it as the full
|
|
88
|
+
// conversation without actually recording one.
|
|
84
89
|
appendFileSync(path, JSON.stringify(entry) + '\n', { encoding: 'utf-8', mode: 0o600 });
|
|
85
90
|
}
|
|
86
91
|
|
package/src/tui.ts
CHANGED
|
@@ -337,28 +337,6 @@ export async function showCompletionNotifySelector(
|
|
|
337
337
|
onConfigChanged(ctx, `Completion notify mode set to ${result}`);
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
-
// ---------------------------------------------------------------------------
|
|
341
|
-
// Update check toggle
|
|
342
|
-
// ---------------------------------------------------------------------------
|
|
343
|
-
|
|
344
|
-
export async function showUpdateCheckToggle(
|
|
345
|
-
ctx: ExtensionContext,
|
|
346
|
-
state: SessionState,
|
|
347
|
-
onConfigChanged: (ctx: ExtensionContext, msg: string) => void,
|
|
348
|
-
): Promise<void> {
|
|
349
|
-
const items: SelectItem[] = [
|
|
350
|
-
{ value: "enabled", label: "Enabled", description: "Check for updates on session start" },
|
|
351
|
-
{ value: "disabled", label: "Disabled", description: "Skip update check" },
|
|
352
|
-
];
|
|
353
|
-
|
|
354
|
-
const result = await showSelectList(ctx, "Update Check", items, 5);
|
|
355
|
-
if (!result) return;
|
|
356
|
-
|
|
357
|
-
state.config.updateCheckEnabled = result === "enabled";
|
|
358
|
-
onConfigChanged(ctx, `Update check ${result}`);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// ---------------------------------------------------------------------------
|
|
362
340
|
// ---------------------------------------------------------------------------
|
|
363
341
|
// E4: SLA tracking configuration
|
|
364
342
|
// ---------------------------------------------------------------------------
|
|
@@ -1199,11 +1177,6 @@ export function getConfigMenuItems(state: SessionState): SelectItem[] {
|
|
|
1199
1177
|
? `Enabled (window: ${state.config.slaWindowSize})`
|
|
1200
1178
|
: "Disabled",
|
|
1201
1179
|
},
|
|
1202
|
-
{
|
|
1203
|
-
value: "update-check",
|
|
1204
|
-
label: "Update Check",
|
|
1205
|
-
description: state.config.updateCheckEnabled ? "Enabled" : "Disabled",
|
|
1206
|
-
},
|
|
1207
1180
|
];
|
|
1208
1181
|
}
|
|
1209
1182
|
|
|
@@ -2217,10 +2190,7 @@ function renderSubTaskSummary(
|
|
|
2217
2190
|
},
|
|
2218
2191
|
maxLines: number,
|
|
2219
2192
|
): string {
|
|
2220
|
-
const isError =
|
|
2221
|
-
result.exitCode !== 0 ||
|
|
2222
|
-
result.stopReason === "error" ||
|
|
2223
|
-
result.stopReason === "aborted";
|
|
2193
|
+
const isError = isSubagentError(result);
|
|
2224
2194
|
const icon = isError
|
|
2225
2195
|
? theme.fg("error", "\u2717")
|
|
2226
2196
|
: theme.fg("success", "\u2713");
|
|
@@ -2240,12 +2210,7 @@ function renderCollapsedChain(
|
|
|
2240
2210
|
details: ChainDetails,
|
|
2241
2211
|
theme: Theme,
|
|
2242
2212
|
): string {
|
|
2243
|
-
const allSucceeded = details.results.every(
|
|
2244
|
-
(r) =>
|
|
2245
|
-
r.exitCode === 0 &&
|
|
2246
|
-
r.stopReason !== "error" &&
|
|
2247
|
-
r.stopReason !== "aborted",
|
|
2248
|
-
);
|
|
2213
|
+
const allSucceeded = details.results.every((r) => !isSubagentError(r));
|
|
2249
2214
|
const icon = allSucceeded
|
|
2250
2215
|
? theme.fg("success", "\u2713")
|
|
2251
2216
|
: theme.fg("error", "\u2717");
|
|
@@ -2253,10 +2218,7 @@ function renderCollapsedChain(
|
|
|
2253
2218
|
let text = `${icon} ${theme.fg("toolTitle", theme.bold("chain"))} ${theme.fg("muted", `${details.completedSteps}/${details.totalSteps} steps${stoppedEarly}`)}`;
|
|
2254
2219
|
|
|
2255
2220
|
for (const r of details.results) {
|
|
2256
|
-
const isErr =
|
|
2257
|
-
r.exitCode !== 0 ||
|
|
2258
|
-
r.stopReason === "error" ||
|
|
2259
|
-
r.stopReason === "aborted";
|
|
2221
|
+
const isErr = isSubagentError(r);
|
|
2260
2222
|
const statusIcon = isErr
|
|
2261
2223
|
? theme.fg("error", "\u2717")
|
|
2262
2224
|
: theme.fg("success", "\u2713");
|
|
@@ -2290,10 +2252,7 @@ function renderCollapsedParallel(
|
|
|
2290
2252
|
let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel"))} ${theme.fg("muted", `${details.succeeded}/${details.failed}/${details.results.length} total`)}`;
|
|
2291
2253
|
|
|
2292
2254
|
for (const r of details.results) {
|
|
2293
|
-
const isErr =
|
|
2294
|
-
r.exitCode !== 0 ||
|
|
2295
|
-
r.stopReason === "error" ||
|
|
2296
|
-
r.stopReason === "aborted";
|
|
2255
|
+
const isErr = isSubagentError(r);
|
|
2297
2256
|
const statusIcon = isErr
|
|
2298
2257
|
? theme.fg("error", "\u2717")
|
|
2299
2258
|
: theme.fg("success", "\u2713");
|
|
@@ -2339,10 +2298,7 @@ function renderExpandedChain(
|
|
|
2339
2298
|
container.addChild(new Spacer(1));
|
|
2340
2299
|
|
|
2341
2300
|
for (const r of details.results) {
|
|
2342
|
-
const isErr =
|
|
2343
|
-
r.exitCode !== 0 ||
|
|
2344
|
-
r.stopReason === "error" ||
|
|
2345
|
-
r.stopReason === "aborted";
|
|
2301
|
+
const isErr = isSubagentError(r);
|
|
2346
2302
|
const statusIcon = isErr
|
|
2347
2303
|
? theme.fg("error", "\u2717")
|
|
2348
2304
|
: theme.fg("success", "\u2713");
|
|
@@ -2424,10 +2380,7 @@ function renderExpandedParallel(
|
|
|
2424
2380
|
container.addChild(new Spacer(1));
|
|
2425
2381
|
|
|
2426
2382
|
for (const r of details.results) {
|
|
2427
|
-
const isErr =
|
|
2428
|
-
r.exitCode !== 0 ||
|
|
2429
|
-
r.stopReason === "error" ||
|
|
2430
|
-
r.stopReason === "aborted";
|
|
2383
|
+
const isErr = isSubagentError(r);
|
|
2431
2384
|
const statusIcon = isErr
|
|
2432
2385
|
? theme.fg("error", "\u2717")
|
|
2433
2386
|
: theme.fg("success", "\u2713");
|
|
@@ -2801,12 +2754,7 @@ function renderCollapsedGraph(
|
|
|
2801
2754
|
): string {
|
|
2802
2755
|
const totalTasks = details.waves.reduce((s, w) => s + w.tasks.length, 0);
|
|
2803
2756
|
const allSucceeded = details.waves.every((w) =>
|
|
2804
|
-
w.tasks.every(
|
|
2805
|
-
(r) =>
|
|
2806
|
-
r.exitCode === 0 &&
|
|
2807
|
-
r.stopReason !== "error" &&
|
|
2808
|
-
r.stopReason !== "aborted",
|
|
2809
|
-
),
|
|
2757
|
+
w.tasks.every((r) => !isSubagentError(r)),
|
|
2810
2758
|
);
|
|
2811
2759
|
const icon = allSucceeded
|
|
2812
2760
|
? theme.fg("success", "\u2713")
|
|
@@ -2817,22 +2765,16 @@ function renderCollapsedGraph(
|
|
|
2817
2765
|
const wavesToRender = details.waves.slice(0, maxWavesToShow);
|
|
2818
2766
|
|
|
2819
2767
|
for (const wave of wavesToRender) {
|
|
2820
|
-
const waveOk = wave.tasks.every(
|
|
2821
|
-
(r) =>
|
|
2822
|
-
r.exitCode === 0 &&
|
|
2823
|
-
r.stopReason !== "error" &&
|
|
2824
|
-
r.stopReason !== "aborted",
|
|
2825
|
-
);
|
|
2768
|
+
const waveOk = wave.tasks.every((r) => !isSubagentError(r));
|
|
2826
2769
|
const waveIcon = waveOk
|
|
2827
2770
|
? theme.fg("success", "\u2713")
|
|
2828
2771
|
: theme.fg("error", "\u2717");
|
|
2829
2772
|
const modeLabel = wave.parallel ? "parallel" : "serial";
|
|
2830
2773
|
const taskLabels = wave.tasks.map((r) => {
|
|
2831
2774
|
const label = r.label || r.task.slice(0, 30);
|
|
2832
|
-
const taskIcon =
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
: theme.fg("error", "\u2717");
|
|
2775
|
+
const taskIcon = !isSubagentError(r)
|
|
2776
|
+
? theme.fg("success", "\u2713")
|
|
2777
|
+
: theme.fg("error", "\u2717");
|
|
2836
2778
|
return `${taskIcon} ${theme.fg("dim", label)}`;
|
|
2837
2779
|
});
|
|
2838
2780
|
text += `
|
|
@@ -2870,12 +2812,7 @@ function renderExpandedGraph(
|
|
|
2870
2812
|
container.addChild(new Spacer(1));
|
|
2871
2813
|
|
|
2872
2814
|
for (const wave of details.waves) {
|
|
2873
|
-
const waveOk = wave.tasks.every(
|
|
2874
|
-
(r) =>
|
|
2875
|
-
r.exitCode === 0 &&
|
|
2876
|
-
r.stopReason !== "error" &&
|
|
2877
|
-
r.stopReason !== "aborted",
|
|
2878
|
-
);
|
|
2815
|
+
const waveOk = wave.tasks.every((r) => !isSubagentError(r));
|
|
2879
2816
|
const waveIcon = waveOk
|
|
2880
2817
|
? theme.fg("success", "\u2713")
|
|
2881
2818
|
: theme.fg("error", "\u2717");
|
|
@@ -2889,10 +2826,7 @@ function renderExpandedGraph(
|
|
|
2889
2826
|
);
|
|
2890
2827
|
|
|
2891
2828
|
for (const r of wave.tasks) {
|
|
2892
|
-
const isErr =
|
|
2893
|
-
r.exitCode !== 0 ||
|
|
2894
|
-
r.stopReason === "error" ||
|
|
2895
|
-
r.stopReason === "aborted";
|
|
2829
|
+
const isErr = isSubagentError(r);
|
|
2896
2830
|
const statusIcon = isErr
|
|
2897
2831
|
? theme.fg("error", "\u2717")
|
|
2898
2832
|
: theme.fg("success", "\u2713");
|