@wichayutdew/pi-workflows 0.2.2 → 0.2.3

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.
@@ -0,0 +1,799 @@
1
+ import { constants } from 'node:fs';
2
+ import { lstat, open, realpath } from 'node:fs/promises';
3
+ import {
4
+ basename,
5
+ dirname,
6
+ isAbsolute,
7
+ join,
8
+ relative,
9
+ resolve,
10
+ sep,
11
+ } from 'node:path';
12
+ import { authorizeBash } from '../../policy/bash.ts';
13
+
14
+ const SESSION_FILE_NAME = 'session.jsonl';
15
+ const SESSION_RUN_DIRECTORY = /^run-\d+$/;
16
+ const SESSION_FILE_SUFFIX = '.jsonl';
17
+ const MAX_SESSION_TAIL_BYTES = 1024 * 1024;
18
+ const MAX_DIAGNOSTIC_FIELD_CHARS = 1_600;
19
+ const TRUNCATION_MARKER = '… [truncated] …';
20
+ const REPLAY_SAFE_TOOLS = new Set([
21
+ 'find',
22
+ 'grep',
23
+ 'ls',
24
+ 'read',
25
+ 'structured_output',
26
+ ]);
27
+ const PRE_EXECUTION_BASH_FAILURES = [
28
+ 'command does not match this step',
29
+ 'environment assignments are not allowed',
30
+ 'not enabled by subagent',
31
+ 'shell operators, substitutions, expansions, and comments are not allowed',
32
+ 'shell wrapper',
33
+ 'substitutions and escapes are not allowed inside double quotes',
34
+ 'trailing bash escape is not allowed',
35
+ 'unterminated bash quote',
36
+ 'unquoted pathname and tilde expansion are not allowed',
37
+ ] as const;
38
+ const HIDDEN_BASH_FATAL_PATTERNS = [
39
+ /command not found/i,
40
+ /permission denied/i,
41
+ /no such file or directory/i,
42
+ /segmentation fault/i,
43
+ /killed|terminated/i,
44
+ /out of memory/i,
45
+ /connection refused/i,
46
+ /timeout/i,
47
+ ] as const;
48
+ const HIDDEN_BASH_EXIT_PATTERN =
49
+ /exit(?:ed)?\s*(?:with\s*)?(?:code|status)?\s*[:\s]?\s*(\d+)/i;
50
+
51
+ export interface ToolFailureDiagnostic {
52
+ tool: string;
53
+ call?: string;
54
+ output?: string;
55
+ replaySafe?: true;
56
+ completionAfterFailure?: true;
57
+ completionValue?: Record<string, unknown>;
58
+ transcriptToolCount?: number;
59
+ transcriptTurnCount?: number;
60
+ correlation?:
61
+ 'latest-before-completion' | 'successful-output-before-completion';
62
+ }
63
+
64
+ export interface SubagentSessionIdentity {
65
+ runId: string;
66
+ childIndex: number;
67
+ }
68
+
69
+ interface RecordedToolCall {
70
+ id: string;
71
+ order: number;
72
+ tool: string;
73
+ call?: string;
74
+ completionValue?: Record<string, unknown>;
75
+ }
76
+
77
+ interface RecordedToolFailure extends ToolFailureDiagnostic {
78
+ callId?: string;
79
+ order: number;
80
+ }
81
+
82
+ interface RecordedToolSuccess {
83
+ order: number;
84
+ tool: string;
85
+ call?: string;
86
+ output?: string;
87
+ detectorOutput?: string;
88
+ }
89
+
90
+ interface RecordedCompletion {
91
+ order: number;
92
+ value: Record<string, unknown>;
93
+ }
94
+
95
+ interface RecordedMessage {
96
+ order: number;
97
+ value: Record<string, unknown>;
98
+ }
99
+
100
+ interface SessionTail {
101
+ content: string;
102
+ truncated: boolean;
103
+ }
104
+
105
+ function isRecord(value: unknown): value is Record<string, unknown> {
106
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
107
+ }
108
+
109
+ function bounded(value: string): string {
110
+ if (value.length <= MAX_DIAGNOSTIC_FIELD_CHARS) return value;
111
+ const available = MAX_DIAGNOSTIC_FIELD_CHARS - TRUNCATION_MARKER.length - 2;
112
+ const startLength = Math.ceil(available / 2);
113
+ const endLength = Math.floor(available / 2);
114
+ return `${value.slice(0, startLength)}\n${TRUNCATION_MARKER}\n${value.slice(-endLength)}`;
115
+ }
116
+
117
+ function textContent(value: unknown): string | undefined {
118
+ if (!Array.isArray(value)) return undefined;
119
+ const text = value
120
+ .flatMap((item) =>
121
+ isRecord(item) && item.type === 'text' && typeof item.text === 'string'
122
+ ? [item.text]
123
+ : [],
124
+ )
125
+ .join('\n')
126
+ .trim();
127
+ return text ? bounded(text) : undefined;
128
+ }
129
+
130
+ function firstTextContent(value: unknown): string | undefined {
131
+ if (!Array.isArray(value)) return undefined;
132
+ const text = value.find(
133
+ (item) =>
134
+ isRecord(item) && item.type === 'text' && typeof item.text === 'string',
135
+ );
136
+ return isRecord(text) && typeof text.text === 'string'
137
+ ? text.text
138
+ : undefined;
139
+ }
140
+
141
+ function toolCallText(
142
+ tool: string,
143
+ argumentsValue: unknown,
144
+ ): string | undefined {
145
+ if (!isRecord(argumentsValue)) return undefined;
146
+ if (tool === 'bash') {
147
+ const command = argumentsValue.command ?? argumentsValue.cmd;
148
+ if (typeof command === 'string' && command.trim()) {
149
+ return bounded(command.trim());
150
+ }
151
+ }
152
+ return bounded(JSON.stringify(argumentsValue));
153
+ }
154
+
155
+ export function failedToolName(error: string | undefined): string | undefined {
156
+ return error?.match(/\b([a-z][\w-]*) failed(?:\s*\(|:)/i)?.[1];
157
+ }
158
+
159
+ export function parseToolFailureDiagnostic(
160
+ transcript: string,
161
+ expectedTool?: string,
162
+ terminalError?: string,
163
+ allowCompletionProof = true,
164
+ ): ToolFailureDiagnostic | undefined {
165
+ const calls = new Map<string, RecordedToolCall>();
166
+ const recordedCalls: RecordedToolCall[] = [];
167
+ const diagnostics: RecordedToolFailure[] = [];
168
+ const successfulResults: RecordedToolSuccess[] = [];
169
+ const successfulCompletions: RecordedCompletion[] = [];
170
+ const recordedMessages: RecordedMessage[] = [];
171
+ const resultCallIds = new Set<string>();
172
+ let falsePositiveProofValid = true;
173
+ let lastInteractionOrder = 0;
174
+ let order = 0;
175
+
176
+ for (const line of transcript.split('\n')) {
177
+ order += 1;
178
+ if (!line.trim()) continue;
179
+ let entry: unknown;
180
+ try {
181
+ entry = JSON.parse(line);
182
+ } catch {
183
+ falsePositiveProofValid = false;
184
+ continue;
185
+ }
186
+ if (!isRecord(entry) || entry.type !== 'message') continue;
187
+ const message = entry.message;
188
+ if (!isRecord(message)) {
189
+ falsePositiveProofValid = false;
190
+ continue;
191
+ }
192
+ recordedMessages.push({ order, value: message });
193
+ if (
194
+ message.role === 'assistant' &&
195
+ ((typeof message.errorMessage === 'string' &&
196
+ message.errorMessage.trim().length > 0) ||
197
+ message.stopReason === 'error' ||
198
+ message.stopReason === 'aborted')
199
+ ) {
200
+ falsePositiveProofValid = false;
201
+ }
202
+
203
+ if (message.role === 'assistant') {
204
+ if (!Array.isArray(message.content)) {
205
+ falsePositiveProofValid = false;
206
+ continue;
207
+ }
208
+ lastInteractionOrder = order;
209
+ const toolCalls = message.content.filter(
210
+ (item): item is Record<string, unknown> =>
211
+ isRecord(item) &&
212
+ item.type === 'toolCall' &&
213
+ typeof item.id === 'string' &&
214
+ typeof item.name === 'string',
215
+ );
216
+ for (const item of message.content) {
217
+ if (
218
+ !isRecord(item) ||
219
+ item.type !== 'toolCall' ||
220
+ typeof item.id !== 'string' ||
221
+ typeof item.name !== 'string'
222
+ ) {
223
+ if (isRecord(item) && item.type === 'toolCall') {
224
+ falsePositiveProofValid = false;
225
+ }
226
+ continue;
227
+ }
228
+ if (calls.has(item.id)) falsePositiveProofValid = false;
229
+ const call = toolCallText(item.name, item.arguments);
230
+ const completionIsExclusive =
231
+ toolCalls.length === 1 &&
232
+ message.content.every(
233
+ (contentItem) =>
234
+ isRecord(contentItem) &&
235
+ (contentItem.type === 'thinking' ||
236
+ contentItem.type === 'toolCall'),
237
+ );
238
+ const completionValue =
239
+ item.name === 'structured_output' && completionIsExclusive
240
+ ? structuredCompletionValue(item.arguments)
241
+ : undefined;
242
+ const recordedCall: RecordedToolCall = {
243
+ id: item.id,
244
+ order,
245
+ tool: item.name,
246
+ ...(call ? { call } : {}),
247
+ ...(completionValue ? { completionValue } : {}),
248
+ };
249
+ calls.set(item.id, recordedCall);
250
+ recordedCalls.push(recordedCall);
251
+ }
252
+ continue;
253
+ }
254
+
255
+ if (message.role !== 'toolResult' || typeof message.toolName !== 'string') {
256
+ if (message.role === 'toolResult') falsePositiveProofValid = false;
257
+ continue;
258
+ }
259
+ lastInteractionOrder = order;
260
+ if (
261
+ typeof message.toolCallId !== 'string' ||
262
+ typeof message.isError !== 'boolean' ||
263
+ !Array.isArray(message.content) ||
264
+ resultCallIds.has(message.toolCallId)
265
+ ) {
266
+ falsePositiveProofValid = false;
267
+ }
268
+ if (typeof message.toolCallId === 'string') {
269
+ resultCallIds.add(message.toolCallId);
270
+ }
271
+ const recorded =
272
+ typeof message.toolCallId === 'string'
273
+ ? calls.get(message.toolCallId)
274
+ : undefined;
275
+ const callMatchesResult = recorded?.tool === message.toolName;
276
+ if (!callMatchesResult) falsePositiveProofValid = false;
277
+ if (
278
+ message.toolName === 'structured_output' &&
279
+ message.isError === false &&
280
+ callMatchesResult &&
281
+ recorded?.completionValue
282
+ ) {
283
+ successfulCompletions.push({
284
+ order,
285
+ value: recorded.completionValue,
286
+ });
287
+ }
288
+ if (
289
+ message.isError === false &&
290
+ message.toolName !== 'structured_output' &&
291
+ callMatchesResult
292
+ ) {
293
+ const output = textContent(message.content);
294
+ const detectorOutput = firstTextContent(message.content);
295
+ successfulResults.push({
296
+ order,
297
+ tool: message.toolName,
298
+ ...(recorded.call ? { call: recorded.call } : {}),
299
+ ...(output ? { output } : {}),
300
+ ...(detectorOutput !== undefined ? { detectorOutput } : {}),
301
+ });
302
+ }
303
+ if (message.isError !== true) continue;
304
+ const output = textContent(message.content);
305
+ const diagnostic: ToolFailureDiagnostic = {
306
+ tool: message.toolName,
307
+ ...(callMatchesResult && recorded.call ? { call: recorded.call } : {}),
308
+ ...(output ? { output } : {}),
309
+ };
310
+ diagnostics.push({
311
+ ...diagnostic,
312
+ ...(callMatchesResult && typeof message.toolCallId === 'string'
313
+ ? { callId: message.toolCallId }
314
+ : {}),
315
+ order,
316
+ });
317
+ }
318
+
319
+ const matchingTool = (diagnostic: { tool: string }): boolean =>
320
+ expectedTool === undefined ||
321
+ diagnostic.tool.toLowerCase() === expectedTool.toLowerCase();
322
+ let selected: RecordedToolFailure | undefined;
323
+ if (terminalError) {
324
+ selected = latestMatching(
325
+ diagnostics,
326
+ (diagnostic) =>
327
+ matchingTool(diagnostic) &&
328
+ diagnosticMatchesTerminalError(diagnostic, terminalError),
329
+ );
330
+ if (!selected) {
331
+ const fallback = latestMatching(diagnostics, matchingTool);
332
+ const latestFailureOrder = diagnostics.at(-1)?.order;
333
+ if (
334
+ fallback &&
335
+ latestFailureOrder !== undefined &&
336
+ finalCompletion(
337
+ successfulCompletions,
338
+ latestFailureOrder,
339
+ lastInteractionOrder,
340
+ allowCompletionProof,
341
+ )
342
+ ) {
343
+ return publicDiagnostic(
344
+ fallback,
345
+ diagnostics,
346
+ recordedCalls,
347
+ successfulCompletions,
348
+ lastInteractionOrder,
349
+ allowCompletionProof,
350
+ 'latest-before-completion',
351
+ );
352
+ }
353
+ }
354
+ if (!selected && diagnostics.length === 0) {
355
+ const falsePositive = reproduceHiddenBashFalsePositive(
356
+ recordedMessages,
357
+ successfulResults,
358
+ );
359
+ const completion = falsePositive
360
+ ? finalCompletion(
361
+ successfulCompletions,
362
+ falsePositive.result.order,
363
+ lastInteractionOrder,
364
+ allowCompletionProof,
365
+ )
366
+ : undefined;
367
+ if (
368
+ falsePositiveProofValid &&
369
+ recordedCalls.length === resultCallIds.size &&
370
+ recordedCalls.every((call) => resultCallIds.has(call.id)) &&
371
+ recordedCalls.filter((call) => call.tool === 'structured_output')
372
+ .length === 1 &&
373
+ expectedTool?.toLowerCase() === 'bash' &&
374
+ falsePositive &&
375
+ terminalError === falsePositive.terminalError &&
376
+ completion
377
+ ) {
378
+ const successfulOutput = falsePositive.result;
379
+ return {
380
+ tool: successfulOutput.tool,
381
+ ...(successfulOutput.call ? { call: successfulOutput.call } : {}),
382
+ ...(successfulOutput.output
383
+ ? { output: successfulOutput.output }
384
+ : {}),
385
+ completionAfterFailure: true,
386
+ completionValue: completion.value,
387
+ transcriptToolCount: recordedCalls.length,
388
+ transcriptTurnCount: recordedMessages.filter(
389
+ ({ value }) => value.role === 'assistant',
390
+ ).length,
391
+ correlation: 'successful-output-before-completion',
392
+ };
393
+ }
394
+ }
395
+ } else {
396
+ selected = latestMatching(diagnostics, matchingTool);
397
+ }
398
+ return selected
399
+ ? publicDiagnostic(
400
+ selected,
401
+ diagnostics,
402
+ recordedCalls,
403
+ successfulCompletions,
404
+ lastInteractionOrder,
405
+ allowCompletionProof,
406
+ )
407
+ : undefined;
408
+ }
409
+
410
+ function reproduceHiddenBashFalsePositive(
411
+ messages: RecordedMessage[],
412
+ successfulResults: RecordedToolSuccess[],
413
+ ):
414
+ | {
415
+ result: RecordedToolSuccess;
416
+ terminalError: string;
417
+ }
418
+ | undefined {
419
+ let lastAssistantTextIndex = -1;
420
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
421
+ const message = messages[index]?.value;
422
+ if (
423
+ message?.role === 'assistant' &&
424
+ Array.isArray(message.content) &&
425
+ message.content.some(
426
+ (item) =>
427
+ isRecord(item) &&
428
+ item.type === 'text' &&
429
+ typeof item.text === 'string' &&
430
+ item.text.trim().length > 0,
431
+ )
432
+ ) {
433
+ lastAssistantTextIndex = index;
434
+ break;
435
+ }
436
+ }
437
+
438
+ const scanStart =
439
+ lastAssistantTextIndex >= 0 ? lastAssistantTextIndex + 1 : 0;
440
+ for (let index = messages.length - 1; index >= scanStart; index -= 1) {
441
+ const recordedMessage = messages[index];
442
+ const message = recordedMessage?.value;
443
+ if (
444
+ !recordedMessage ||
445
+ message?.role !== 'toolResult' ||
446
+ message.toolName !== 'bash' ||
447
+ message.isError !== false
448
+ ) {
449
+ continue;
450
+ }
451
+ const output = firstTextContent(message.content);
452
+ if (output === undefined) continue;
453
+
454
+ const exitMatch = output.match(HIDDEN_BASH_EXIT_PATTERN);
455
+ const exitCode = exitMatch ? Number.parseInt(exitMatch[1]!, 10) : undefined;
456
+ const detectedExitCode =
457
+ exitCode !== undefined && exitCode !== 0
458
+ ? exitCode
459
+ : HIDDEN_BASH_FATAL_PATTERNS.some((pattern) => pattern.test(output))
460
+ ? 1
461
+ : undefined;
462
+ if (detectedExitCode === undefined) continue;
463
+
464
+ const result = successfulResults.find(
465
+ (candidate) =>
466
+ candidate.order === recordedMessage.order &&
467
+ candidate.tool === 'bash' &&
468
+ candidate.detectorOutput === output,
469
+ );
470
+ if (!result) return undefined;
471
+ const details = output.slice(0, 200);
472
+ return {
473
+ result,
474
+ terminalError: `bash failed (exit ${detectedExitCode}): ${details}`,
475
+ };
476
+ }
477
+ return undefined;
478
+ }
479
+
480
+ function structuredCompletionValue(
481
+ argumentsValue: unknown,
482
+ ): Record<string, unknown> | undefined {
483
+ if (!isRecord(argumentsValue)) return undefined;
484
+ if (
485
+ Object.keys(argumentsValue).length !== 1 ||
486
+ !Object.hasOwn(argumentsValue, 'value') ||
487
+ !isRecord(argumentsValue.value)
488
+ ) {
489
+ return undefined;
490
+ }
491
+ return argumentsValue.value;
492
+ }
493
+
494
+ function normalized(value: string): string {
495
+ return value.replace(/\s+/g, ' ').trim().toLowerCase();
496
+ }
497
+
498
+ function comparableFragments(value: string): string[] {
499
+ const normalizedValue = normalized(value);
500
+ const lines = value
501
+ .split(/\r?\n/)
502
+ .map(normalized)
503
+ .filter((line) => line.length >= 8);
504
+ return [...new Set([normalizedValue, ...lines])].filter(
505
+ (fragment) => fragment.length >= 8,
506
+ );
507
+ }
508
+
509
+ function diagnosticMatchesTerminalError(
510
+ diagnostic: ToolFailureDiagnostic,
511
+ terminalError: string,
512
+ ): boolean {
513
+ if (!diagnostic.output) return false;
514
+ const detail =
515
+ terminalError.match(
516
+ /\b[a-z][\w-]* failed(?:\s*\([^)]*\))?\s*:\s*([\s\S]+)/i,
517
+ )?.[1] ?? terminalError;
518
+ const outputFragments = comparableFragments(diagnostic.output);
519
+ const errorFragments = comparableFragments(`${terminalError}\n${detail}`);
520
+ return outputFragments.some((output) =>
521
+ errorFragments.some(
522
+ (error) => output.includes(error) || error.includes(output),
523
+ ),
524
+ );
525
+ }
526
+
527
+ function latestMatching(
528
+ diagnostics: RecordedToolFailure[],
529
+ predicate: (diagnostic: RecordedToolFailure) => boolean,
530
+ ): RecordedToolFailure | undefined {
531
+ for (let index = diagnostics.length - 1; index >= 0; index -= 1) {
532
+ const diagnostic = diagnostics[index];
533
+ if (diagnostic && predicate(diagnostic)) return diagnostic;
534
+ }
535
+ return undefined;
536
+ }
537
+
538
+ function preExecutionBashFailure(output: string | undefined): boolean {
539
+ if (!output) return false;
540
+ const normalizedOutput = output.toLowerCase();
541
+ return PRE_EXECUTION_BASH_FAILURES.some((fragment) =>
542
+ normalizedOutput.includes(fragment),
543
+ );
544
+ }
545
+
546
+ function replaySafeToolCall(
547
+ call: RecordedToolCall,
548
+ diagnostics: RecordedToolFailure[],
549
+ ): boolean {
550
+ const tool = call.tool.toLowerCase();
551
+ if (REPLAY_SAFE_TOOLS.has(tool)) return true;
552
+ if (tool !== 'bash' || !call.call) return false;
553
+ if (
554
+ authorizeBash(call.call, { mode: 'read-only', allow: [] }).allowed === true
555
+ ) {
556
+ return true;
557
+ }
558
+ const failure = diagnostics.find(
559
+ (diagnostic) => diagnostic.callId === call.id,
560
+ );
561
+ return preExecutionBashFailure(failure?.output);
562
+ }
563
+
564
+ function transcriptReplaySafe(
565
+ calls: RecordedToolCall[],
566
+ diagnostics: RecordedToolFailure[],
567
+ completeTranscript: boolean,
568
+ ): boolean {
569
+ return (
570
+ completeTranscript &&
571
+ calls.length > 0 &&
572
+ calls.every((call) => replaySafeToolCall(call, diagnostics))
573
+ );
574
+ }
575
+
576
+ function publicDiagnostic(
577
+ diagnostic: RecordedToolFailure,
578
+ diagnostics: RecordedToolFailure[],
579
+ recordedCalls: RecordedToolCall[],
580
+ successfulCompletions: RecordedCompletion[],
581
+ lastInteractionOrder: number,
582
+ allowCompletionProof: boolean,
583
+ correlation?: ToolFailureDiagnostic['correlation'],
584
+ ): ToolFailureDiagnostic {
585
+ const result: ToolFailureDiagnostic = {
586
+ tool: diagnostic.tool,
587
+ ...(diagnostic.call ? { call: diagnostic.call } : {}),
588
+ ...(diagnostic.output ? { output: diagnostic.output } : {}),
589
+ };
590
+ const { order } = diagnostic;
591
+ const latestFailureOrder = diagnostics.at(-1)?.order ?? order;
592
+ const completion = finalCompletion(
593
+ successfulCompletions,
594
+ latestFailureOrder,
595
+ lastInteractionOrder,
596
+ allowCompletionProof,
597
+ );
598
+ return {
599
+ ...result,
600
+ ...(transcriptReplaySafe(recordedCalls, diagnostics, allowCompletionProof)
601
+ ? { replaySafe: true as const }
602
+ : {}),
603
+ ...(completion
604
+ ? {
605
+ completionAfterFailure: true as const,
606
+ completionValue: completion.value,
607
+ }
608
+ : {}),
609
+ ...(correlation ? { correlation } : {}),
610
+ };
611
+ }
612
+
613
+ function finalCompletion(
614
+ completions: RecordedCompletion[],
615
+ latestFailureOrder: number,
616
+ lastInteractionOrder: number,
617
+ allowCompletionProof: boolean,
618
+ ): RecordedCompletion | undefined {
619
+ if (!allowCompletionProof || completions.length !== 1) return undefined;
620
+ const completion = completions[0];
621
+ return completion &&
622
+ completion.order > latestFailureOrder &&
623
+ completion.order === lastInteractionOrder
624
+ ? completion
625
+ : undefined;
626
+ }
627
+
628
+ function isSessionFilePath(path: string): boolean {
629
+ return (
630
+ isAbsolute(path) &&
631
+ !path.includes('\0') &&
632
+ basename(path) === SESSION_FILE_NAME &&
633
+ SESSION_RUN_DIRECTORY.test(basename(dirname(path)))
634
+ );
635
+ }
636
+
637
+ function pathWithin(root: string, candidate: string): boolean {
638
+ const fromRoot = relative(resolve(root), resolve(candidate));
639
+ return (
640
+ fromRoot !== '' &&
641
+ fromRoot !== '..' &&
642
+ !fromRoot.startsWith(`..${sep}`) &&
643
+ !isAbsolute(fromRoot)
644
+ );
645
+ }
646
+
647
+ async function readContainedSessionTail(
648
+ sessionFile: string,
649
+ trustedRoot: string,
650
+ identity: SubagentSessionIdentity,
651
+ ): Promise<SessionTail | undefined> {
652
+ if (
653
+ !isSessionFilePath(sessionFile) ||
654
+ !isAbsolute(trustedRoot) ||
655
+ trustedRoot.includes('\0') ||
656
+ !pathWithin(trustedRoot, sessionFile) ||
657
+ !isValidSessionIdentity(identity) ||
658
+ resolve(sessionFile) !==
659
+ resolve(
660
+ trustedRoot,
661
+ identity.runId,
662
+ `run-${identity.childIndex}`,
663
+ SESSION_FILE_NAME,
664
+ )
665
+ ) {
666
+ return undefined;
667
+ }
668
+
669
+ const resolvedSessionFile = resolve(sessionFile);
670
+ const inspected = await lstat(resolvedSessionFile);
671
+ if (inspected.isSymbolicLink() || !inspected.isFile()) return undefined;
672
+
673
+ const [canonicalRoot, canonicalSessionFile] = await Promise.all([
674
+ realpath(trustedRoot),
675
+ realpath(resolvedSessionFile),
676
+ ]);
677
+ if (!pathWithin(canonicalRoot, canonicalSessionFile)) return undefined;
678
+
679
+ const handle = await open(
680
+ canonicalSessionFile,
681
+ constants.O_RDONLY | constants.O_NOFOLLOW,
682
+ );
683
+ try {
684
+ const opened = await handle.stat();
685
+ if (!opened.isFile()) return undefined;
686
+ const bytesToRead = Math.min(opened.size, MAX_SESSION_TAIL_BYTES);
687
+ if (bytesToRead === 0) return { content: '', truncated: false };
688
+ const start = opened.size - bytesToRead;
689
+ const buffer = Buffer.alloc(bytesToRead);
690
+ const { bytesRead } = await handle.read(buffer, 0, bytesToRead, start);
691
+ const afterRead = await handle.stat();
692
+ if (
693
+ afterRead.dev !== opened.dev ||
694
+ afterRead.ino !== opened.ino ||
695
+ afterRead.size !== opened.size ||
696
+ afterRead.mtimeMs !== opened.mtimeMs
697
+ ) {
698
+ return undefined;
699
+ }
700
+ let content = buffer.subarray(0, bytesRead).toString('utf8');
701
+ if (start > 0) {
702
+ const firstNewline = content.indexOf('\n');
703
+ content = firstNewline === -1 ? '' : content.slice(firstNewline + 1);
704
+ }
705
+ return { content, truncated: start > 0 };
706
+ } finally {
707
+ await handle.close();
708
+ }
709
+ }
710
+
711
+ function isValidSessionIdentity(identity: SubagentSessionIdentity): boolean {
712
+ return (
713
+ identity.runId.length > 0 &&
714
+ !identity.runId.includes('\0') &&
715
+ basename(identity.runId) === identity.runId &&
716
+ identity.runId !== '.' &&
717
+ identity.runId !== '..' &&
718
+ Number.isSafeInteger(identity.childIndex) &&
719
+ identity.childIndex >= 0
720
+ );
721
+ }
722
+
723
+ export function deriveSubagentSessionRoot(
724
+ parentSessionFile: string | undefined,
725
+ ): string | undefined {
726
+ if (
727
+ !parentSessionFile ||
728
+ !isAbsolute(parentSessionFile) ||
729
+ parentSessionFile.includes('\0')
730
+ ) {
731
+ return undefined;
732
+ }
733
+ const parentName = basename(parentSessionFile);
734
+ if (
735
+ !parentName.endsWith(SESSION_FILE_SUFFIX) ||
736
+ parentName === SESSION_FILE_SUFFIX
737
+ ) {
738
+ return undefined;
739
+ }
740
+ return join(
741
+ dirname(parentSessionFile),
742
+ parentName.slice(0, -SESSION_FILE_SUFFIX.length),
743
+ );
744
+ }
745
+
746
+ export async function readToolFailureDiagnostic(
747
+ sessionFile: string | undefined,
748
+ trustedRoot: string | undefined,
749
+ identity: SubagentSessionIdentity | undefined,
750
+ expectedTool?: string,
751
+ terminalError?: string,
752
+ ): Promise<ToolFailureDiagnostic | undefined> {
753
+ if (!sessionFile || !trustedRoot || !identity) return undefined;
754
+ try {
755
+ const tail = await readContainedSessionTail(
756
+ sessionFile,
757
+ trustedRoot,
758
+ identity,
759
+ );
760
+ return parseToolFailureDiagnostic(
761
+ tail?.content ?? '',
762
+ expectedTool,
763
+ terminalError,
764
+ tail?.truncated !== true,
765
+ );
766
+ } catch {
767
+ return undefined;
768
+ }
769
+ }
770
+
771
+ export function formatToolFailureDiagnostic(
772
+ diagnostic: ToolFailureDiagnostic,
773
+ ): string[] {
774
+ const successfulOutputCorrelation =
775
+ diagnostic.correlation === 'successful-output-before-completion';
776
+ return [
777
+ `${successfulOutputCorrelation ? 'Terminal-reported tool' : 'Failed tool'}: ${diagnostic.tool}`,
778
+ ...(diagnostic.call
779
+ ? [
780
+ `${diagnostic.tool === 'bash' ? 'Command' : 'Arguments'}: ${diagnostic.call}`,
781
+ ]
782
+ : []),
783
+ ...(diagnostic.output
784
+ ? [
785
+ `${successfulOutputCorrelation ? 'Successful tool output' : 'Tool error'}: ${diagnostic.output}`,
786
+ ]
787
+ : []),
788
+ ...(diagnostic.correlation === 'latest-before-completion'
789
+ ? [
790
+ 'Correlation: latest failed tool call before successful structured_output; terminal text did not identify the call',
791
+ ]
792
+ : []),
793
+ ...(successfulOutputCorrelation
794
+ ? [
795
+ 'Correlation: terminal error text came from a successful tool result before the final structured_output',
796
+ ]
797
+ : []),
798
+ ];
799
+ }