@the-open-engine/zeroshot 6.36.0 → 6.38.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.
Files changed (81) hide show
  1. package/cli/index.js +99 -29
  2. package/cli/lib/setup-scanner-worker.js +2 -9
  3. package/lib/agent-cli-provider/adapters/claude-recovery.d.ts.map +1 -1
  4. package/lib/agent-cli-provider/adapters/claude-recovery.js +17 -3
  5. package/lib/agent-cli-provider/adapters/claude-recovery.js.map +1 -1
  6. package/lib/agent-cli-provider/adapters/index.d.ts.map +1 -1
  7. package/lib/agent-cli-provider/adapters/index.js +7 -0
  8. package/lib/agent-cli-provider/adapters/index.js.map +1 -1
  9. package/lib/agent-cli-provider/adapters/pi.d.ts.map +1 -1
  10. package/lib/agent-cli-provider/adapters/pi.js +86 -97
  11. package/lib/agent-cli-provider/adapters/pi.js.map +1 -1
  12. package/lib/agent-cli-provider/contract-parse.d.ts.map +1 -1
  13. package/lib/agent-cli-provider/contract-parse.js +5 -1
  14. package/lib/agent-cli-provider/contract-parse.js.map +1 -1
  15. package/lib/agent-cli-provider/invoke-evidence.d.ts.map +1 -1
  16. package/lib/agent-cli-provider/invoke-evidence.js +8 -1
  17. package/lib/agent-cli-provider/invoke-evidence.js.map +1 -1
  18. package/lib/agent-cli-provider/pi/credentials.d.ts +2 -0
  19. package/lib/agent-cli-provider/pi/credentials.d.ts.map +1 -0
  20. package/lib/agent-cli-provider/pi/credentials.js +51 -0
  21. package/lib/agent-cli-provider/pi/credentials.js.map +1 -0
  22. package/lib/agent-cli-provider/pi/events.d.ts +5 -0
  23. package/lib/agent-cli-provider/pi/events.d.ts.map +1 -0
  24. package/lib/agent-cli-provider/pi/events.js +124 -0
  25. package/lib/agent-cli-provider/pi/events.js.map +1 -0
  26. package/lib/agent-cli-provider/pi/protocol-state.d.ts +6 -0
  27. package/lib/agent-cli-provider/pi/protocol-state.d.ts.map +1 -0
  28. package/lib/agent-cli-provider/pi/protocol-state.js +138 -0
  29. package/lib/agent-cli-provider/pi/protocol-state.js.map +1 -0
  30. package/lib/agent-cli-provider/pi/release.d.ts +4 -0
  31. package/lib/agent-cli-provider/pi/release.d.ts.map +1 -0
  32. package/lib/agent-cli-provider/pi/release.js +7 -0
  33. package/lib/agent-cli-provider/pi/release.js.map +1 -0
  34. package/lib/agent-cli-provider/probe-availability.d.ts +5 -0
  35. package/lib/agent-cli-provider/probe-availability.d.ts.map +1 -0
  36. package/lib/agent-cli-provider/probe-availability.js +11 -0
  37. package/lib/agent-cli-provider/probe-availability.js.map +1 -0
  38. package/lib/agent-cli-provider/provider-registry.d.ts +13 -9
  39. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  40. package/lib/agent-cli-provider/provider-registry.js +42 -8
  41. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  42. package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
  43. package/lib/agent-cli-provider/single-agent-runtime.js +15 -5
  44. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  45. package/lib/agent-cli-provider/types.d.ts +3 -0
  46. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  47. package/lib/agent-cli-provider/types.js.map +1 -1
  48. package/lib/cluster-worker/engine-adapter.js +9 -1
  49. package/lib/docker-config.js +6 -1
  50. package/lib/provider-credential-path.js +36 -0
  51. package/lib/start-cluster.js +8 -0
  52. package/npm-shrinkwrap.json +2 -2
  53. package/package.json +1 -1
  54. package/src/agent/agent-lifecycle.js +25 -76
  55. package/src/agent/agent-liveness-poll.js +93 -0
  56. package/src/agent/agent-task-executor.js +98 -23
  57. package/src/agent/output-extraction.js +115 -0
  58. package/src/agent/pi-terminal-lifecycle.js +242 -0
  59. package/src/agent/provider-control-plane.js +26 -0
  60. package/src/agent/provider-terminal-failure.js +49 -24
  61. package/src/agent-cli-provider/adapters/claude-recovery.ts +15 -2
  62. package/src/agent-cli-provider/adapters/index.ts +6 -0
  63. package/src/agent-cli-provider/adapters/pi.ts +105 -125
  64. package/src/agent-cli-provider/contract-parse.ts +8 -1
  65. package/src/agent-cli-provider/invoke-evidence.ts +11 -1
  66. package/src/agent-cli-provider/pi/credentials.ts +47 -0
  67. package/src/agent-cli-provider/pi/events.ts +148 -0
  68. package/src/agent-cli-provider/pi/protocol-state.ts +163 -0
  69. package/src/agent-cli-provider/pi/release.ts +4 -0
  70. package/src/agent-cli-provider/probe-availability.ts +19 -0
  71. package/src/agent-cli-provider/provider-registry.ts +48 -9
  72. package/src/agent-cli-provider/single-agent-runtime.ts +27 -6
  73. package/src/agent-cli-provider/types.ts +3 -0
  74. package/src/config-validator.js +1 -3
  75. package/src/foreground-benchmark-files.js +81 -0
  76. package/src/foreground-benchmark-result.js +233 -0
  77. package/src/foreground-benchmark-run.js +72 -0
  78. package/src/isolation-manager.js +43 -29
  79. package/src/orchestrator.js +33 -1
  80. package/task-lib/runner.js +7 -0
  81. package/task-lib/watcher-output-runtime.js +16 -5
@@ -347,6 +347,7 @@ function extractClaudeVertexModelError(output, { useVertex = false } = {}) {
347
347
  * - Codex: {type:"turn.failed", error:{message:"msg"}}
348
348
  * - Gemini: {type:"result", status:"error", error:{message:"msg"}} or {type:"error", severity:"error"}
349
349
  * - Opencode: {type:"session.error", error:{message:"msg"}}
350
+ * - Pi: latest assistant message_end, finalized only by a later agent_settled
350
351
  *
351
352
  * @param {string} providerName - Active provider whose terminal errors may be inspected
352
353
  * @returns {{error: string, provider: string}|null} Error info or null
@@ -397,6 +398,22 @@ function opencodeFailureFromObject(obj) {
397
398
  };
398
399
  }
399
400
 
401
+ function piFailureFromMessage(message) {
402
+ const stopReason = message.stopReason;
403
+ const errorMessage = message.errorMessage;
404
+ if (stopReason === 'deferred') {
405
+ return {
406
+ ...cliErrorDetail('Pi turn deferred without completing.', 'Pi turn deferred'),
407
+ provider: 'pi',
408
+ };
409
+ }
410
+ if (stopReason !== 'error' && stopReason !== 'aborted' && !errorMessage) return null;
411
+ return {
412
+ ...cliErrorDetail(errorMessage || stopReason, 'Pi turn failed'),
413
+ provider: 'pi',
414
+ };
415
+ }
416
+
400
417
  function failureFromProviderObject(obj, providerName) {
401
418
  if (providerName === 'claude') return claudeFailureFromObject(obj);
402
419
  if (providerName === 'codex') return codexFailureFromObject(obj);
@@ -405,7 +422,105 @@ function failureFromProviderObject(obj, providerName) {
405
422
  return null;
406
423
  }
407
424
 
425
+ function piProtocolFailure(message) {
426
+ return { ...cliErrorDetail(message, message), provider: 'pi' };
427
+ }
428
+
429
+ const PI_STOP_REASONS = new Set(['stop', 'length', 'toolUse', 'error', 'aborted', 'deferred']);
430
+ const PI_USAGE_FIELDS = ['input', 'output', 'cacheRead', 'cacheWrite', 'totalTokens'];
431
+ const PI_COST_FIELDS = ['input', 'output', 'cacheRead', 'cacheWrite', 'total'];
432
+
433
+ function parsePiProtocolObject(content) {
434
+ if (!content.startsWith('{')) return null;
435
+ try {
436
+ const parsed = JSON.parse(content);
437
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
438
+ } catch {
439
+ return null;
440
+ }
441
+ }
442
+
443
+ function isPiRecord(value) {
444
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
445
+ }
446
+
447
+ function hasPiNumericFields(value, fields) {
448
+ return isPiRecord(value) && fields.every((field) => typeof value[field] === 'number');
449
+ }
450
+
451
+ function isValidPiUsage(usage) {
452
+ if (!hasPiNumericFields(usage, PI_USAGE_FIELDS)) return false;
453
+ if (!hasPiNumericFields(usage.cost, PI_COST_FIELDS)) return false;
454
+ return (
455
+ (usage.cacheWrite1h === undefined || typeof usage.cacheWrite1h === 'number') &&
456
+ (usage.reasoning === undefined || typeof usage.reasoning === 'number')
457
+ );
458
+ }
459
+
460
+ function isValidPiAssistantMessage(message) {
461
+ return (
462
+ isPiRecord(message) &&
463
+ message.role === 'assistant' &&
464
+ Array.isArray(message.content) &&
465
+ isValidPiUsage(message.usage) &&
466
+ PI_STOP_REASONS.has(message.stopReason)
467
+ );
468
+ }
469
+
470
+ function piMessageRole(message) {
471
+ if (!message || typeof message !== 'object' || Array.isArray(message)) return null;
472
+ return typeof message.role === 'string' ? message.role : null;
473
+ }
474
+
475
+ function applyPiProtocolLine(content, state) {
476
+ if (!content) return null;
477
+ if (
478
+ content.startsWith('[ZEROSHOT] Earlier provider output omitted') ||
479
+ content.startsWith('[ZEROSHOT] Provider output record retained') ||
480
+ content.startsWith('[ZEROSHOT][PROVIDER_STDERR] ')
481
+ ) {
482
+ return null;
483
+ }
484
+ if (state.settled) return piProtocolFailure('Pi emitted output after agent_settled.');
485
+ const parsed = parsePiProtocolObject(content);
486
+ if (parsed === null) return piProtocolFailure('Pi JSON stream contained malformed output.');
487
+ if (typeof parsed.type !== 'string') {
488
+ return piProtocolFailure('Pi JSON stream contained an event without a valid type.');
489
+ }
490
+ if (parsed.type === 'message_end') {
491
+ const role = piMessageRole(parsed.message);
492
+ if (role === null) return piProtocolFailure('Pi message_end did not contain a valid message.');
493
+ if (role !== 'assistant') return null;
494
+ if (!isValidPiAssistantMessage(parsed.message)) {
495
+ return piProtocolFailure('Pi message_end did not contain a valid assistant message.');
496
+ }
497
+ state.latestAssistant = parsed.message;
498
+ } else if (parsed.type === 'agent_settled') {
499
+ state.settled = true;
500
+ }
501
+ return null;
502
+ }
503
+
504
+ function extractSettledPiFailure(lines) {
505
+ const state = { latestAssistant: null, settled: false };
506
+ for (const line of lines) {
507
+ const content = stripTimestamp(line);
508
+ const failure = applyPiProtocolLine(content, state);
509
+ if (failure) return failure;
510
+ }
511
+
512
+ if (!state.settled) return piProtocolFailure('Pi JSON stream ended before agent_settled.');
513
+ if (state.latestAssistant === null) {
514
+ return piProtocolFailure('Pi settled without a valid assistant message_end.');
515
+ }
516
+ return piFailureFromMessage(state.latestAssistant);
517
+ }
518
+
408
519
  function extractCliFailure(output, providerName = 'claude') {
520
+ if (providerName === 'pi') {
521
+ const lines = typeof output === 'string' ? output.split('\n') : [];
522
+ return extractSettledPiFailure(lines);
523
+ }
409
524
  if (!output || typeof output !== 'string') return null;
410
525
 
411
526
  const lines = output.split('\n');
@@ -0,0 +1,242 @@
1
+ // @ts-nocheck
2
+
3
+ const { parseProviderEvent, providerFailureFields } = require('./provider-control-plane');
4
+
5
+ const ZEROSHOT_PI_METADATA_PREFIXES = [
6
+ '[ZEROSHOT] Earlier provider output omitted',
7
+ '[ZEROSHOT][PROVIDER_STDERR] ',
8
+ '[ZEROSHOT][FATAL] ',
9
+ ];
10
+ const ZEROSHOT_RETAINED_RECORD_PREFIX = '[ZEROSHOT] Provider output record retained';
11
+ const PI_STOP_REASONS = new Set(['stop', 'length', 'toolUse', 'error', 'aborted', 'deferred']);
12
+ const PI_REQUIRED_TOKEN_FIELDS = ['input', 'output', 'cacheRead', 'cacheWrite', 'totalTokens'];
13
+ const PI_OPTIONAL_TOKEN_FIELDS = ['cacheWrite1h', 'reasoning'];
14
+ const PI_TOKEN_FIELDS = [...PI_REQUIRED_TOKEN_FIELDS, ...PI_OPTIONAL_TOKEN_FIELDS];
15
+ const PI_COST_FIELDS = ['input', 'output', 'cacheRead', 'cacheWrite', 'total'];
16
+ const PI_PENDING_EVENT_TYPES = new Set([
17
+ 'turn_end',
18
+ 'agent_end',
19
+ 'auto_retry_start',
20
+ 'auto_retry_end',
21
+ ]);
22
+
23
+ function pendingFailureMetadata(failure) {
24
+ return {
25
+ zeroshot_pending_failure: {
26
+ provider: failure.provider,
27
+ category: failure.category,
28
+ kind: failure.classification.kind,
29
+ retryable: failure.classification.retryable,
30
+ diagnostic: failure.diagnostic,
31
+ },
32
+ };
33
+ }
34
+
35
+ function numericFields(fields, source) {
36
+ return Object.fromEntries(
37
+ fields.map((field) => [field, typeof source[field] === 'number' ? source[field] : 0])
38
+ );
39
+ }
40
+
41
+ function redactedUsage(usage) {
42
+ const value = usage && typeof usage === 'object' && !Array.isArray(usage) ? usage : {};
43
+ return {
44
+ ...numericFields(PI_REQUIRED_TOKEN_FIELDS, value),
45
+ ...(typeof value.cacheWrite1h === 'number' ? { cacheWrite1h: value.cacheWrite1h } : {}),
46
+ ...(typeof value.reasoning === 'number' ? { reasoning: value.reasoning } : {}),
47
+ cost: numericFields(PI_COST_FIELDS, isRecord(value.cost) ? value.cost : {}),
48
+ };
49
+ }
50
+
51
+ function isRecord(value) {
52
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
53
+ }
54
+
55
+ function hasNumericFields(value, fields) {
56
+ return isRecord(value) && fields.every((field) => typeof value[field] === 'number');
57
+ }
58
+
59
+ function addNumericFields(target, source, fields) {
60
+ for (const field of fields) {
61
+ if (typeof source[field] === 'number') target[field] = (target[field] || 0) + source[field];
62
+ }
63
+ }
64
+
65
+ function rememberTokenUsage(state, usage) {
66
+ if (!hasNumericFields(usage, PI_REQUIRED_TOKEN_FIELDS)) return false;
67
+ if (!hasNumericFields(usage.cost, PI_COST_FIELDS)) return false;
68
+ if (
69
+ !PI_OPTIONAL_TOKEN_FIELDS.every(
70
+ (field) => usage[field] === undefined || typeof usage[field] === 'number'
71
+ )
72
+ ) {
73
+ return false;
74
+ }
75
+ state.piUsage = state.piUsage || { tokens: {}, cost: {} };
76
+ addNumericFields(state.piUsage.tokens, usage, PI_TOKEN_FIELDS);
77
+ addNumericFields(state.piUsage.cost, usage.cost, PI_COST_FIELDS);
78
+ return true;
79
+ }
80
+
81
+ function getPiTokenUsage(state) {
82
+ if (!state?.piUsage) return null;
83
+ const { tokens, cost } = state.piUsage;
84
+ return {
85
+ inputTokens: tokens.input || 0,
86
+ outputTokens: tokens.output || 0,
87
+ cacheReadInputTokens: tokens.cacheRead || 0,
88
+ cacheCreationInputTokens: tokens.cacheWrite || 0,
89
+ totalCostUsd: cost.total || 0,
90
+ durationMs: null,
91
+ modelUsage: { ...tokens, cost: { ...cost } },
92
+ };
93
+ }
94
+
95
+ function isValidAssistantMessage(message) {
96
+ return Array.isArray(message?.content) && PI_STOP_REASONS.has(message.stopReason);
97
+ }
98
+
99
+ function redactPendingFailure(failure, parsed) {
100
+ const pending = pendingFailureMetadata(failure);
101
+ if (parsed.type === 'agent_end') {
102
+ return JSON.stringify({
103
+ type: parsed.type,
104
+ messages: [],
105
+ willRetry: parsed.willRetry === true,
106
+ ...pending,
107
+ });
108
+ }
109
+ if (parsed.type === 'auto_retry_end') {
110
+ return JSON.stringify({ type: parsed.type, success: false, ...pending });
111
+ }
112
+ if (parsed.type === 'auto_retry_start') {
113
+ return JSON.stringify({
114
+ type: parsed.type,
115
+ ...numericFields(
116
+ ['attempt', 'maxAttempts', 'delayMs'].filter((field) => typeof parsed[field] === 'number'),
117
+ parsed
118
+ ),
119
+ ...pending,
120
+ });
121
+ }
122
+ return JSON.stringify({
123
+ type: parsed.type,
124
+ message: {
125
+ role: 'assistant',
126
+ content: [],
127
+ usage: redactedUsage(parsed.message?.usage),
128
+ stopReason: 'error',
129
+ errorMessage: 'Pi provider turn failed; awaiting agent settlement.',
130
+ },
131
+ ...(parsed.type === 'turn_end' ? { toolResults: [] } : {}),
132
+ ...pending,
133
+ });
134
+ }
135
+
136
+ function finalFailureEnvelope(failure) {
137
+ return JSON.stringify({
138
+ type: 'agent_settled',
139
+ ...providerFailureFields(failure),
140
+ });
141
+ }
142
+
143
+ function rememberProtocolFailure(state, content, extractProviderFailure) {
144
+ const failure = extractProviderFailure(content, 'pi');
145
+ if (failure) {
146
+ state.piProtocolFailure = failure;
147
+ state.providerFailure = failure;
148
+ }
149
+ return failure;
150
+ }
151
+
152
+ function failProtocolLine(state, content, extractProviderFailure) {
153
+ const failure = rememberProtocolFailure(state, content, extractProviderFailure);
154
+ return failure ? finalFailureEnvelope(failure) : content;
155
+ }
156
+
157
+ function handleMessageEnd(state, content, parsed, extractProviderFailure) {
158
+ const role = parsed.message?.role;
159
+ if (typeof role !== 'string') return failProtocolLine(state, content, extractProviderFailure);
160
+ const hasCountedUsage = role === 'assistant' || parsed.message?.usage !== undefined;
161
+ if (
162
+ ['assistant', 'toolResult'].includes(role) &&
163
+ hasCountedUsage &&
164
+ !rememberTokenUsage(state, parsed.message?.usage)
165
+ ) {
166
+ return failProtocolLine(state, 'invalid Pi usage', extractProviderFailure);
167
+ }
168
+ if (role !== 'assistant') return content;
169
+ if (!isValidAssistantMessage(parsed.message)) {
170
+ return failProtocolLine(state, content, extractProviderFailure);
171
+ }
172
+ const failure = extractProviderFailure(
173
+ `${content}\n${JSON.stringify({ type: 'agent_settled' })}`,
174
+ 'pi'
175
+ );
176
+ state.piLatestAssistantObserved = true;
177
+ state.pendingPiFailure = failure;
178
+ if (failure) return redactPendingFailure(failure, parsed);
179
+ if (!state.piProtocolFailure) state.providerFailure = null;
180
+ return content;
181
+ }
182
+
183
+ function usageForEvent(parsed) {
184
+ if (parsed.type === 'compaction_end' && parsed.result?.usage !== undefined) {
185
+ return parsed.result.usage;
186
+ }
187
+ if (
188
+ parsed.type === 'entry_appended' &&
189
+ parsed.entry?.type === 'branch_summary' &&
190
+ parsed.entry.usage !== undefined
191
+ ) {
192
+ return parsed.entry.usage;
193
+ }
194
+ return undefined;
195
+ }
196
+
197
+ function handleSettlement(state, content, extractProviderFailure) {
198
+ state.piProtocolSettled = true;
199
+ const missingAssistantFailure = state.piLatestAssistantObserved
200
+ ? null
201
+ : extractProviderFailure(JSON.stringify({ type: 'agent_settled' }), 'pi');
202
+ const failure = state.piProtocolFailure || state.pendingPiFailure || missingAssistantFailure;
203
+ state.pendingPiFailure = null;
204
+ state.providerFailure = failure || null;
205
+ return failure ? finalFailureEnvelope(failure) : content;
206
+ }
207
+
208
+ function redactPiFailureForControlPlane(state, content, extractProviderFailure) {
209
+ if (content.startsWith(ZEROSHOT_RETAINED_RECORD_PREFIX)) {
210
+ state.piProtocolPrefixOmitted = true;
211
+ return content;
212
+ }
213
+ if (ZEROSHOT_PI_METADATA_PREFIXES.some((prefix) => content.startsWith(prefix))) return content;
214
+ const parsed = parseProviderEvent(content);
215
+ if (parsed === null || typeof parsed.type !== 'string') {
216
+ return failProtocolLine(state, content, extractProviderFailure);
217
+ }
218
+ if (state.piProtocolSettled) {
219
+ return failProtocolLine(
220
+ state,
221
+ `${JSON.stringify({ type: 'agent_settled' })}\n${content}`,
222
+ extractProviderFailure
223
+ );
224
+ }
225
+ state.piProtocolObserved = true;
226
+
227
+ if (parsed.type === 'message_end') {
228
+ return handleMessageEnd(state, content, parsed, extractProviderFailure);
229
+ }
230
+ const usage = usageForEvent(parsed);
231
+ if (usage !== undefined && !rememberTokenUsage(state, usage)) {
232
+ return failProtocolLine(state, 'invalid Pi usage', extractProviderFailure);
233
+ }
234
+ if (state.pendingPiFailure && PI_PENDING_EVENT_TYPES.has(parsed.type)) {
235
+ return redactPendingFailure(state.pendingPiFailure, parsed);
236
+ }
237
+ return parsed.type === 'agent_settled'
238
+ ? handleSettlement(state, content, extractProviderFailure)
239
+ : content;
240
+ }
241
+
242
+ module.exports = { getPiTokenUsage, redactPiFailureForControlPlane };
@@ -0,0 +1,26 @@
1
+ // @ts-nocheck
2
+
3
+ function parseProviderEvent(content) {
4
+ try {
5
+ const parsed = JSON.parse(content);
6
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
7
+ } catch {
8
+ return null;
9
+ }
10
+ }
11
+
12
+ function providerFailureFields(failure) {
13
+ return {
14
+ error: { message: failure.error },
15
+ zeroshot_failure: {
16
+ provider: failure.provider,
17
+ event: failure.event,
18
+ category: failure.category,
19
+ kind: failure.classification.kind,
20
+ retryable: failure.classification.retryable,
21
+ diagnostic: failure.diagnostic,
22
+ },
23
+ };
24
+ }
25
+
26
+ module.exports = { parseProviderEvent, providerFailureFields };
@@ -3,12 +3,23 @@
3
3
  const { getProvider } = require('../providers');
4
4
  const { isCriticalAgent } = require('./critical-agent-policy');
5
5
  const { extractCliFailure } = require('./output-extraction');
6
+ const { getPiTokenUsage, redactPiFailureForControlPlane } = require('./pi-terminal-lifecycle');
7
+ const { parseProviderEvent, providerFailureFields } = require('./provider-control-plane');
8
+
9
+ const AUTHENTICATION_FAILURE_PATTERNS = [
10
+ /invalid[_ -]?api[_ -]?key/i,
11
+ /api[_ -]?key.*invalid/i,
12
+ /(?:missing|no) api key/i,
13
+ /run\s*\/login/i,
14
+ /unauthori[sz]ed/i,
15
+ /forbidden|authentication|permission denied/i,
16
+ ];
6
17
 
7
18
  function categoryForProviderFailure(error, classification) {
8
19
  const isPermanent = classification.retryable === false;
9
- const authenticationPattern =
10
- /(?:invalid[_ -]?api[_ -]?key|api[_ -]?key.*invalid|unauthori[sz]ed|forbidden|authentication|permission denied)/i;
11
- if (isPermanent && authenticationPattern.test(error)) return 'authentication';
20
+ if (isPermanent && AUTHENTICATION_FAILURE_PATTERNS.some((pattern) => pattern.test(error))) {
21
+ return 'authentication';
22
+ }
12
23
 
13
24
  const quotaPattern = /(?:insufficient[_ -]?quota|quota exceeded|resource_exhausted)/i;
14
25
  if (isPermanent && quotaPattern.test(error)) return 'quota';
@@ -30,6 +41,12 @@ function classifyProviderFailure(providerName, error) {
30
41
  };
31
42
  }
32
43
 
44
+ function providerFailureEvent(providerName) {
45
+ if (providerName === 'codex') return 'turn.failed';
46
+ if (providerName === 'pi') return 'agent_settled';
47
+ return 'terminal_error';
48
+ }
49
+
33
50
  function extractProviderFailure(output, providerName) {
34
51
  const cliError = extractCliFailure(output, providerName);
35
52
  if (!cliError) return null;
@@ -39,41 +56,47 @@ function extractProviderFailure(output, providerName) {
39
56
  return {
40
57
  error: `Provider ${cliError.provider} failed (${category}; ${classification.kind})`,
41
58
  provider: cliError.provider,
42
- event: cliError.provider === 'codex' ? 'turn.failed' : 'terminal_error',
59
+ event: providerFailureEvent(cliError.provider),
43
60
  category,
44
61
  classification,
45
62
  diagnostic: cliError.diagnostic,
46
63
  };
47
64
  }
48
65
 
49
- function redactTerminalFailureForControlPlane(state, providerName, content) {
50
- const failure = extractProviderFailure(content, providerName);
51
- if (!failure) return content;
52
-
53
- state.providerFailure = failure;
54
- let eventType = 'provider.failure';
55
- try {
56
- const parsed = JSON.parse(content);
57
- if (typeof parsed?.type === 'string') eventType = parsed.type;
58
- } catch {
59
- // extractProviderFailure already proved a supported terminal envelope.
66
+ function extractProviderCompletionFailure(output, providerName, state = {}) {
67
+ if (providerName !== 'pi') return extractProviderFailure(output, providerName);
68
+ if (state.providerFailure) return state.providerFailure;
69
+ if (state.piProtocolPrefixOmitted) return extractProviderFailure('', 'pi');
70
+ if (!state.piProtocolObserved) return extractProviderFailure(output, 'pi');
71
+ if (!state.piProtocolSettled || !state.piLatestAssistantObserved) {
72
+ return extractProviderFailure('', 'pi');
60
73
  }
74
+ return null;
75
+ }
76
+
77
+ function redactedFailureEnvelope(failure, providerName, eventType) {
61
78
  return JSON.stringify({
62
79
  type: eventType,
63
80
  ...(providerName === 'claude' ? { is_error: true } : {}),
64
81
  ...(providerName === 'gemini' ? { status: 'error', severity: 'error' } : {}),
65
- error: { message: failure.error },
66
- zeroshot_failure: {
67
- provider: failure.provider,
68
- event: failure.event,
69
- category: failure.category,
70
- kind: failure.classification.kind,
71
- retryable: failure.classification.retryable,
72
- diagnostic: failure.diagnostic,
73
- },
82
+ ...providerFailureFields(failure),
74
83
  });
75
84
  }
76
85
 
86
+ function redactTerminalFailureForControlPlane(state, providerName, content) {
87
+ if (providerName === 'pi') {
88
+ return redactPiFailureForControlPlane(state, content, extractProviderFailure);
89
+ }
90
+
91
+ const failure = extractProviderFailure(content, providerName);
92
+ if (!failure) return content;
93
+
94
+ state.providerFailure = failure;
95
+ const parsed = parseProviderEvent(content);
96
+ const eventType = typeof parsed?.type === 'string' ? parsed.type : 'provider.failure';
97
+ return redactedFailureEnvelope(failure, providerName, eventType);
98
+ }
99
+
77
100
  function decorateError(error, failure) {
78
101
  if (!failure) return error;
79
102
  error.provider = failure.provider || null;
@@ -176,7 +199,9 @@ function buildFinalFailureInfo({
176
199
  module.exports = {
177
200
  buildFinalFailureInfo,
178
201
  decorateError,
202
+ extractProviderCompletionFailure,
179
203
  extractProviderFailure,
204
+ getPiTokenUsage,
180
205
  publishCriticalFailure,
181
206
  receiptFields,
182
207
  redactTerminalFailureForControlPlane,
@@ -25,8 +25,9 @@ export function detectProviderStreamingModeError(
25
25
  }
26
26
 
27
27
  export function detectProviderFatalError(provider: string, line: unknown): string | null {
28
- if (!isClaudeProvider(provider)) return null;
29
- return detectClaudeFatalError(line);
28
+ if (isClaudeProvider(provider)) return detectClaudeFatalError(line);
29
+ if (provider.toLowerCase() === 'pi') return detectPiFatalError(line);
30
+ return null;
30
31
  }
31
32
 
32
33
  export function recoverProviderStructuredOutput(
@@ -84,6 +85,18 @@ function detectClaudeFatalError(line: unknown): string | null {
84
85
  return null;
85
86
  }
86
87
 
88
+ function detectPiFatalError(line: unknown): string | null {
89
+ if (typeof line !== 'string') return null;
90
+ const message = line.trim();
91
+ if (/\bno api key found\b/i.test(message)) {
92
+ return 'Pi authentication required: run /login';
93
+ }
94
+ if (/\bmodel\b.*\bnot found\b/i.test(message)) {
95
+ return 'Pi model not found: run pi --list-models';
96
+ }
97
+ return null;
98
+ }
99
+
87
100
  function findSessionJsonlPath(sessionId: string): string | null {
88
101
  const claudeDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
89
102
  const projectsDir = join(claudeDir, 'projects');
@@ -90,6 +90,12 @@ export function parseProviderChunk(
90
90
  }
91
91
  }
92
92
 
93
+ const finalEvent = adapter.finishParsing?.(state);
94
+ if (finalEvent) {
95
+ if (isOutputEventArray(finalEvent)) events.push(...finalEvent);
96
+ else events.push(finalEvent);
97
+ }
98
+
93
99
  return events;
94
100
  }
95
101