@magnusekdahl/parallix 1.2.0 → 1.3.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/index.js +190 -185
- package/lib/agents/agents.js +737 -838
- package/lib/agents/agents.ts +930 -0
- package/lib/agents/claude-telemetry.js +149 -154
- package/lib/agents/claude-telemetry.ts +233 -0
- package/lib/agents/claude.js +121 -131
- package/lib/agents/claude.ts +156 -0
- package/lib/agents/codex-telemetry.js +176 -164
- package/lib/agents/codex-telemetry.ts +205 -0
- package/lib/agents/codex.js +168 -201
- package/lib/agents/codex.ts +236 -0
- package/lib/agents/limit-hit.js +216 -226
- package/lib/agents/limit-hit.ts +262 -0
- package/lib/agents/mistral-telemetry.js +7 -10
- package/lib/agents/mistral-telemetry.ts +44 -0
- package/lib/agents/mistral.js +40 -58
- package/lib/agents/mistral.ts +84 -0
- package/lib/agents/opencode-export.js +126 -127
- package/lib/agents/opencode-export.ts +160 -0
- package/lib/agents/opencode-telemetry.js +282 -261
- package/lib/agents/opencode-telemetry.ts +350 -0
- package/lib/agents/opencode.js +263 -305
- package/lib/agents/opencode.ts +370 -0
- package/lib/agents/stage-telemetry.js +23 -24
- package/lib/agents/stage-telemetry.ts +47 -0
- package/lib/commands/active.js +502 -579
- package/lib/commands/active.ts +665 -0
- package/lib/commands/checkpoint.js +98 -74
- package/lib/commands/checkpoint.ts +79 -0
- package/lib/commands/config.js +61 -31
- package/lib/commands/config.ts +47 -0
- package/lib/commands/coverage-gate.js +271 -246
- package/lib/commands/coverage-gate.ts +362 -0
- package/lib/commands/diff.js +140 -119
- package/lib/commands/diff.ts +125 -0
- package/lib/commands/draft.js +770 -923
- package/lib/commands/draft.ts +1021 -0
- package/lib/commands/handoff.js +588 -608
- package/lib/commands/handoff.ts +694 -0
- package/lib/commands/integrate.js +1332 -1463
- package/lib/commands/integrate.ts +1688 -0
- package/lib/commands/mission-start.js +279 -237
- package/lib/commands/mission-start.ts +263 -0
- package/lib/commands/rebase.js +606 -582
- package/lib/commands/rebase.ts +635 -0
- package/lib/commands/repair-handoff.js +181 -179
- package/lib/commands/repair-handoff.ts +228 -0
- package/lib/commands/resolve-conflict.js +121 -102
- package/lib/commands/resolve-conflict.ts +112 -0
- package/lib/commands/review.js +3 -4
- package/lib/commands/review.ts +14 -0
- package/lib/commands/setup-review.js +4 -5
- package/lib/commands/setup-review.ts +14 -0
- package/lib/commands/setup.js +3 -3
- package/lib/commands/setup.ts +2 -0
- package/lib/commands/stats-backfill.js +394 -359
- package/lib/commands/stats-backfill.ts +418 -0
- package/lib/commands/stats.js +1424 -1665
- package/lib/commands/stats.ts +2186 -0
- package/lib/commands/status.js +219 -200
- package/lib/commands/status.ts +219 -0
- package/lib/commands/verify.js +3 -1
- package/lib/commands/verify.ts +2 -0
- package/lib/core/fmt.js +140 -211
- package/lib/core/fmt.ts +189 -0
- package/lib/core/git.js +121 -102
- package/lib/core/git.ts +146 -0
- package/lib/core/gitignore.js +59 -106
- package/lib/core/gitignore.ts +103 -0
- package/lib/core/mission-utils.js +833 -875
- package/lib/core/mission-utils.ts +998 -0
- package/lib/core/nels.js +112 -127
- package/lib/core/nels.ts +189 -0
- package/lib/core/persistent-data-migration.js +247 -205
- package/lib/core/persistent-data-migration.ts +255 -0
- package/lib/core/product-config.js +400 -476
- package/lib/core/product-config.ts +518 -0
- package/lib/core/runtime-matrix.js +41 -87
- package/lib/core/runtime-matrix.ts +84 -0
- package/lib/core/spawn-tee.js +152 -192
- package/lib/core/spawn-tee.ts +202 -0
- package/lib/core/state-map.js +92 -92
- package/lib/core/state-map.ts +115 -0
- package/lib/core/storage.js +101 -129
- package/lib/core/storage.ts +182 -0
- package/lib/core/subagent-limit.js +15 -16
- package/lib/core/subagent-limit.ts +25 -0
- package/lib/core/verification.js +148 -138
- package/lib/core/verification.ts +200 -0
- package/lib/index.js +142 -59
- package/lib/index.ts +134 -0
- package/lib/review/rebase.js +142 -145
- package/lib/review/rebase.ts +188 -0
- package/lib/review/review-adapter.js +99 -87
- package/lib/review/review-adapter.ts +198 -0
- package/lib/review/review-artifacts.js +486 -629
- package/lib/review/review-artifacts.ts +622 -0
- package/lib/review/review-commands.js +1220 -1373
- package/lib/review/review-commands.ts +1579 -0
- package/lib/review/review-events.js +544 -886
- package/lib/review/review-events.ts +954 -0
- package/lib/review/review-loop.js +945 -1020
- package/lib/review/review-loop.ts +1174 -0
- package/lib/review/review-polling.js +86 -115
- package/lib/review/review-polling.ts +194 -0
- package/lib/review/review-prompts.js +154 -189
- package/lib/review/review-prompts.ts +182 -0
- package/lib/review/review-state.js +246 -225
- package/lib/review/review-state.ts +353 -0
- package/lib/review/review.js +63 -84
- package/lib/review/review.ts +138 -0
- package/lib/tools/backlog.js +694 -680
- package/lib/tools/backlog.ts +835 -0
- package/lib/tools/forgejo.js +1270 -1405
- package/lib/tools/forgejo.ts +1793 -0
- package/lib/tools/gatekeeper.js +106 -96
- package/lib/tools/gatekeeper.ts +124 -0
- package/lib/tools/redgreen.js +200 -181
- package/lib/tools/redgreen.ts +221 -0
- package/lib/tools/sessions.js +58 -52
- package/lib/tools/sessions.ts +81 -0
- package/lib/tools/setup-review.js +902 -975
- package/lib/tools/setup-review.ts +1152 -0
- package/package.json +7 -5
- package/prompts/draft.md +1 -0
- package/prompts/portfolio.md +1 -1
- package/px.js +226 -210
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { spawnAndTee } from '../core/spawn-tee.js';
|
|
2
|
+
import { extractOpencodeTelemetryFromExport } from './opencode-telemetry.js';
|
|
3
|
+
import { captureOpencodeExport } from './opencode-export.js';
|
|
4
|
+
import { detectLimitHit } from './limit-hit.js';
|
|
5
|
+
// tools/sessions and core/subagent-limit are still CJS (not converted in this
|
|
6
|
+
// wave); require keeps them untyped (any) without pulling non-included .js into
|
|
7
|
+
// the typecheck program.
|
|
8
|
+
const sessions = require('../tools/sessions');
|
|
9
|
+
const { buildSubagentLimitPrefix } = require('../core/subagent-limit');
|
|
10
|
+
|
|
11
|
+
interface BuildOpencodeInvocationOptions {
|
|
12
|
+
prompt: string;
|
|
13
|
+
worktree: string;
|
|
14
|
+
env?: object;
|
|
15
|
+
resume?: boolean;
|
|
16
|
+
sessionId?: string | null;
|
|
17
|
+
model?: string | null;
|
|
18
|
+
preferJson?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface StartOpencodeAgentOptions {
|
|
22
|
+
prompt: string;
|
|
23
|
+
worktree: string;
|
|
24
|
+
env?: object;
|
|
25
|
+
resume?: boolean;
|
|
26
|
+
sessionId?: string | null;
|
|
27
|
+
model?: string | null;
|
|
28
|
+
teeOptions?: object;
|
|
29
|
+
slug?: string | null;
|
|
30
|
+
role?: string | null;
|
|
31
|
+
maxTransientRetries?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Injectable I/O for tests. Production uses the real spawn-tee / export capture.
|
|
35
|
+
let _spawnAndTee: any = spawnAndTee;
|
|
36
|
+
let _captureExport: any = captureOpencodeExport;
|
|
37
|
+
let _sessions: any = sessions;
|
|
38
|
+
|
|
39
|
+
// Test hooks: override the launcher's I/O without touching the public signature.
|
|
40
|
+
function __setSpawnAndTeeForTest(fn: any) { _spawnAndTee = fn || spawnAndTee; }
|
|
41
|
+
function __setExportCaptureForTest(fn: any) { _captureExport = fn || captureOpencodeExport; }
|
|
42
|
+
function __setSessionsForTest(mod: any) { _sessions = mod || sessions; }
|
|
43
|
+
|
|
44
|
+
// Test hook: override the cached feature-detect for `--format json` support.
|
|
45
|
+
// Set to true/false to force inclusion/exclusion of the flag regardless of
|
|
46
|
+
// the real binary. Pass null to reset to the live detect.
|
|
47
|
+
function __setJsonFormatSupportForTest(val: boolean | null) {
|
|
48
|
+
_jsonFormatSupported = val;
|
|
49
|
+
_jsonFormatDetectFn = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Test hook: inject a feature-detect function for tests. This replaces the
|
|
53
|
+
// live shell-out with a stubbed function, making tests hermetic.
|
|
54
|
+
function __setJsonFormatDetectForTest(fn: any) {
|
|
55
|
+
_jsonFormatDetectFn = fn;
|
|
56
|
+
_jsonFormatSupported = null; // invalidate cache so the detect fn runs
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Session-id recovery for telemetry. After session completion `opencode export`
|
|
60
|
+
// is invoked to extract token-usage data (see opencode-telemetry.js), so the
|
|
61
|
+
// session id MUST be recoverable from the launcher's stdout. Stats hooks in
|
|
62
|
+
// active.js and review-loop.js call recordStageStats which defaults to '0' for
|
|
63
|
+
// tokens when telemetry is null.
|
|
64
|
+
//
|
|
65
|
+
// Two shapes are recognized:
|
|
66
|
+
// 1. Legacy footer: "Continue opencode -s ses_<id>" (older opencode TUI).
|
|
67
|
+
// 2. JSON stream: `"sessionID":"ses_<id>"` (opencode v2.0.0
|
|
68
|
+
// `run --format json`). v2.0.0's plain `run` no longer prints the footer,
|
|
69
|
+
// so without the JSON field the id is lost and custom telemetry drops to all
|
|
70
|
+
// zeros (task-1339). We request `--format json` in buildOpencodeInvocation
|
|
71
|
+
// so this field is always present.
|
|
72
|
+
const OPENCODE_SESSION_ID_RE = /opencode\s+-s\s+(ses_\S+)/i;
|
|
73
|
+
const OPENCODE_JSON_SESSION_ID_RE = /"sessionID"\s*:\s*"(ses_[^"]+)"/;
|
|
74
|
+
|
|
75
|
+
function extractOpencodeSessionId(stdout: string) {
|
|
76
|
+
if (!stdout) {return null;}
|
|
77
|
+
const footer = OPENCODE_SESSION_ID_RE.exec(stdout);
|
|
78
|
+
if (footer) {return footer[1];}
|
|
79
|
+
const json = OPENCODE_JSON_SESSION_ID_RE.exec(stdout);
|
|
80
|
+
return json ? json[1] : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Cached feature-detect for `--format json` support. Set to `false` only when
|
|
84
|
+
// a real invocation proves the flag is rejected (older opencode versions).
|
|
85
|
+
// Tests can inject a canned result via __setJsonFormatSupportForTest.
|
|
86
|
+
let _jsonFormatSupported: boolean | null = null;
|
|
87
|
+
|
|
88
|
+
// Injectable feature-detect function for tests. When set, checkJsonFormatSupport()
|
|
89
|
+
// calls this function instead of shelling out, making tests hermetic.
|
|
90
|
+
let _jsonFormatDetectFn: (() => any) | null = null;
|
|
91
|
+
|
|
92
|
+
function resolveOpencodeCommand() {
|
|
93
|
+
return 'opencode';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Lightweight feature-detect: try `opencode --format json --help`.
|
|
97
|
+
// Returns true if the flag is accepted (exit 0 or non-zero for unrelated reasons),
|
|
98
|
+
// false if the flag itself is rejected ("unrecognized" / "unknown option").
|
|
99
|
+
// Results are cached so the check runs at most once in production.
|
|
100
|
+
// Tests can inject a canned result via __setJsonFormatSupportForTest.
|
|
101
|
+
function checkJsonFormatSupport() {
|
|
102
|
+
if (_jsonFormatSupported !== null) {return _jsonFormatSupported;}
|
|
103
|
+
if (_jsonFormatDetectFn) {
|
|
104
|
+
_jsonFormatSupported = _jsonFormatDetectFn();
|
|
105
|
+
return _jsonFormatSupported;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const { spawnSync } = require('node:child_process');
|
|
109
|
+
const result = spawnSync('opencode', ['--format', 'json', '--help'], {
|
|
110
|
+
timeout: 3000,
|
|
111
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
112
|
+
});
|
|
113
|
+
const stderr = (result.stderr && result.stderr.toString()) || '';
|
|
114
|
+
const combined = stderr;
|
|
115
|
+
// Flag rejected → fallback to legacy invocation.
|
|
116
|
+
if (/unrecognized|unknown option|no such option|invalid option/i.test(combined)) {
|
|
117
|
+
_jsonFormatSupported = false;
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
_jsonFormatSupported = true;
|
|
121
|
+
return true;
|
|
122
|
+
} catch (_) {
|
|
123
|
+
// ENOENT or other spawn errors → assume legacy (no JSON flag).
|
|
124
|
+
_jsonFormatSupported = false;
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildOpencodeInvocation({ prompt, worktree, env, resume = false, sessionId = null, model = null, preferJson = true }: BuildOpencodeInvocationOptions) {
|
|
130
|
+
// `--format json` makes opencode stream NDJSON events that each carry a
|
|
131
|
+
// "sessionID":"ses_..." field. opencode v2.0.0's default `run` output no
|
|
132
|
+
// longer prints the "Continue opencode -s ses_..." footer, so JSON is the
|
|
133
|
+
// only reliable way to recover the session id needed for `opencode export`
|
|
134
|
+
// telemetry capture (task-1339).
|
|
135
|
+
//
|
|
136
|
+
// Compatibility guard: feature-detect the flag on first call; older opencode
|
|
137
|
+
// versions that reject `--format json` will silently fall back to the legacy
|
|
138
|
+
// invocation, preserving launch behaviour at the cost of telemetry.
|
|
139
|
+
const useJson = preferJson && checkJsonFormatSupport();
|
|
140
|
+
const args = ['run', '--pure', '--dangerously-skip-permissions'];
|
|
141
|
+
if (useJson) {args.push('--format', 'json');}
|
|
142
|
+
if (model) {args.push('-m', model);}
|
|
143
|
+
// Only resume when the marker explicitly says so (resume=true).
|
|
144
|
+
// sessionId from a stale cross-family marker must be ignored when resume=false.
|
|
145
|
+
if (resume && sessionId) {
|
|
146
|
+
args.push('-s', sessionId);
|
|
147
|
+
} else if (resume) {
|
|
148
|
+
args.push('--continue');
|
|
149
|
+
}
|
|
150
|
+
args.push(prompt);
|
|
151
|
+
return {
|
|
152
|
+
command: resolveOpencodeCommand(),
|
|
153
|
+
args,
|
|
154
|
+
options: {
|
|
155
|
+
stdio: 'inherit',
|
|
156
|
+
cwd: worktree,
|
|
157
|
+
env: { ...process.env, ...env }
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Transient custom/vLLM backend failures that opencode surfaces as a non-zero
|
|
163
|
+
// exit. These are recoverable: the same prompt frequently succeeds on a second
|
|
164
|
+
// attempt, so the launcher retries once in-family before agents.js reroutes to a
|
|
165
|
+
// different agent family. Limit-hit phrases are intentionally NOT listed here —
|
|
166
|
+
// detectLimitHit() in agents.js owns that accounting (lib/agents/limit-hit.js).
|
|
167
|
+
// Hard errors (model-not-found, auth, ENOENT/EACCES) are never retried.
|
|
168
|
+
const TRANSIENT_OPENCODE_PATTERNS = [
|
|
169
|
+
/\bECONNRESET\b/i,
|
|
170
|
+
/\bECONNREFUSED\b/i,
|
|
171
|
+
/\bETIMEDOUT\b/i,
|
|
172
|
+
/\bENETUNREACH\b/i,
|
|
173
|
+
/\bENOTFOUND\b/i,
|
|
174
|
+
/\bEAI_AGAIN\b/i,
|
|
175
|
+
/\bEPIPE\b/i,
|
|
176
|
+
/socket hang ?up/i,
|
|
177
|
+
/\bfetch failed\b/i,
|
|
178
|
+
/network (?:error|timeout)/i,
|
|
179
|
+
/connection (?:error|reset|closed|refused|timed? ?out)/i,
|
|
180
|
+
/\b(?:502|503|504|529)\b/,
|
|
181
|
+
/\bbad gateway\b/i,
|
|
182
|
+
/\bservice (?:is )?unavailable\b/i,
|
|
183
|
+
/\bgateway time-?out\b/i,
|
|
184
|
+
/\b(?:server |api |model )?overloaded\b/i,
|
|
185
|
+
/\btemporarily unavailable\b/i,
|
|
186
|
+
/\bplease (?:try|retry) again\b/i,
|
|
187
|
+
/\bretry[- ]after\b/i,
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
// Hard, non-retryable failures: retrying cannot help and only wastes time/quota.
|
|
191
|
+
const HARD_OPENCODE_PATTERNS = [
|
|
192
|
+
/\bmodel not found\b/i,
|
|
193
|
+
/\bno such model\b/i,
|
|
194
|
+
/\bunknown model\b/i,
|
|
195
|
+
/\binvalid (?:api )?key\b/i,
|
|
196
|
+
/\b401\b[^\n]*\bunauthorized\b/i,
|
|
197
|
+
/\bauthentication (?:failed|error)\b/i,
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
function failureText(result: any) {
|
|
201
|
+
return `${(result && result.stdout) || ''}\n${(result && result.stderr) || ''}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isHardOpencodeFailure(result: any) {
|
|
205
|
+
if (!result) {return false;}
|
|
206
|
+
if (result.error && (result.error.code === 'ENOENT' || result.error.code === 'EACCES')) {return true;}
|
|
207
|
+
const text = failureText(result);
|
|
208
|
+
return HARD_OPENCODE_PATTERNS.some((re) => re.test(text));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Opencode v2.0.0 JSON-mode sometimes exits with code 1 during post-run
|
|
212
|
+
// cleanup (race closing the stdout pipe, EPIPE, or draining pending events).
|
|
213
|
+
// When the transcript contains a valid "reason":"stop" completion event and
|
|
214
|
+
// no "type":"error" event, the exit is spurious – the agent actually
|
|
215
|
+
// completed successfully. See opencode issues #31446 and #33653.
|
|
216
|
+
function isSpuriousOpencodeExit(result: any) {
|
|
217
|
+
if (!result || result.status !== 1) {return false;}
|
|
218
|
+
const text = failureText(result);
|
|
219
|
+
const hasStop = /"reason"\s*:\s*"stop"/.test(text);
|
|
220
|
+
const hasError = /"type"\s*:\s*"error"/.test(text);
|
|
221
|
+
return hasStop && !hasError;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function isTransientOpencodeFailure(result: any) {
|
|
225
|
+
if (!result) {return false;}
|
|
226
|
+
const text = failureText(result);
|
|
227
|
+
return TRANSIENT_OPENCODE_PATTERNS.some((re) => re.test(text));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Decide whether a failed opencode run should be retried once in-family.
|
|
231
|
+
// Only genuine non-zero exits carrying a transient backend signature qualify;
|
|
232
|
+
// clean exits, spawn errors (ENOENT/EACCES), killing signals, limit-hits, and
|
|
233
|
+
// hard errors never retry — so real failures still surface to the reroute loop.
|
|
234
|
+
function shouldRetryOpencodeFailure(result: any) {
|
|
235
|
+
if (!result) {return false;}
|
|
236
|
+
if (result.error && (result.error.code === 'ENOENT' || result.error.code === 'EACCES')) {return false;}
|
|
237
|
+
if (result.signal) {return false;}
|
|
238
|
+
if (!(typeof result.status === 'number' && result.status !== 0)) {return false;}
|
|
239
|
+
if (detectLimitHit({
|
|
240
|
+
agent: 'custom',
|
|
241
|
+
stdout: result.stdout,
|
|
242
|
+
stderr: result.stderr,
|
|
243
|
+
status: result.status,
|
|
244
|
+
signal: result.signal,
|
|
245
|
+
error: result.error,
|
|
246
|
+
})) {return false;}
|
|
247
|
+
if (isHardOpencodeFailure(result)) {return false;}
|
|
248
|
+
return isTransientOpencodeFailure(result);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function startOpencodeAgent({
|
|
252
|
+
prompt,
|
|
253
|
+
worktree,
|
|
254
|
+
env,
|
|
255
|
+
resume = false,
|
|
256
|
+
sessionId = null,
|
|
257
|
+
model = null,
|
|
258
|
+
teeOptions = {},
|
|
259
|
+
slug = null,
|
|
260
|
+
role = null,
|
|
261
|
+
maxTransientRetries = 1
|
|
262
|
+
}: StartOpencodeAgentOptions) {
|
|
263
|
+
// Prepend the subagent-limit advisory prefix to the prompt.
|
|
264
|
+
// When maxParallel is unset/null/zero the prefix is empty (no-op).
|
|
265
|
+
const subagentPrefix = buildSubagentLimitPrefix();
|
|
266
|
+
const injectedPrompt = subagentPrefix + prompt;
|
|
267
|
+
|
|
268
|
+
function isStaleSessionResult(result: any) {
|
|
269
|
+
if (!result) {return false;}
|
|
270
|
+
const stderr = result.stderr || '';
|
|
271
|
+
const stdout = result.stdout || '';
|
|
272
|
+
return stderr.includes('Session not found') || stdout.includes('Session not found');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function runInvocation(invocation: any) {
|
|
276
|
+
const spawnOptions = { ...invocation.options, ...teeOptions };
|
|
277
|
+
return _spawnAndTee(invocation.command, invocation.args, spawnOptions);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function runInvocationWithRetry(invocation: any) {
|
|
281
|
+
let attempts = 0;
|
|
282
|
+
let result = await runInvocation(invocation);
|
|
283
|
+
while (attempts < maxTransientRetries && shouldRetryOpencodeFailure(result)) {
|
|
284
|
+
attempts += 1;
|
|
285
|
+
result = await runInvocation(invocation);
|
|
286
|
+
}
|
|
287
|
+
if (result) {result.transientRetries = attempts;}
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Detect whether `--format json` was rejected at runtime (e.g. older opencode
|
|
292
|
+
// that wasn't caught by the feature-detect). If so, retry the invocation
|
|
293
|
+
// without the flag so the prompt still runs (telemetry will be lost but the
|
|
294
|
+
// agent won't fail outright).
|
|
295
|
+
function isJsonFlagError(result: any) {
|
|
296
|
+
if (!result) {return false;}
|
|
297
|
+
const text = failureText(result);
|
|
298
|
+
return (/\bunrecognized option\b|\bunknown option\b|\bno such option\b|\binvalid option\b|\bunrecognized flag\b|\bunknown flag\b/i).test(text);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function processResult(result: any) {
|
|
302
|
+
if (result && result.stdout) {
|
|
303
|
+
result.sessionId = extractOpencodeSessionId(result.stdout) || undefined;
|
|
304
|
+
}
|
|
305
|
+
if (result && result.sessionId) {
|
|
306
|
+
try {
|
|
307
|
+
const exportJson = await _captureExport(result.sessionId, { worktree, env });
|
|
308
|
+
if (exportJson) {
|
|
309
|
+
const telemetry = extractOpencodeTelemetryFromExport(exportJson, model || undefined);
|
|
310
|
+
if (telemetry) {
|
|
311
|
+
result.telemetry = telemetry;
|
|
312
|
+
if ((telemetry as any).model) {result.model = (telemetry as any).model;}
|
|
313
|
+
if ((telemetry as any).provider) {result.provider = (telemetry as any).provider;}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
} catch (_) {
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function runWithJsonFallback(invocation: any) {
|
|
324
|
+
let result = await runInvocationWithRetry(invocation);
|
|
325
|
+
// Runtime fallback: if the first invocation used --format json but the
|
|
326
|
+
// binary rejected it, retry without the flag so the prompt still executes.
|
|
327
|
+
if (isJsonFlagError(result) && invocation.args.includes('--format')) {
|
|
328
|
+
const legacyInv = buildOpencodeInvocation({
|
|
329
|
+
prompt: injectedPrompt, worktree, env, resume, sessionId, model, preferJson: false,
|
|
330
|
+
});
|
|
331
|
+
result = await runInvocationWithRetry(legacyInv);
|
|
332
|
+
// Mark that we fell back so callers know telemetry may be absent.
|
|
333
|
+
if (result) {result._jsonFallback = true;}
|
|
334
|
+
}
|
|
335
|
+
return result;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function staleSessionHandler(invocation: any) {
|
|
339
|
+
let result = await runWithJsonFallback(invocation);
|
|
340
|
+
if (isStaleSessionResult(result) && worktree && resume) {
|
|
341
|
+
try {
|
|
342
|
+
_sessions.clearSession(worktree, slug || '', role || '');
|
|
343
|
+
} catch (_) { /* best-effort */ }
|
|
344
|
+
const freshInv = buildOpencodeInvocation({ prompt: injectedPrompt, worktree, env, resume: false, sessionId: null, model });
|
|
345
|
+
result = await runWithJsonFallback(freshInv);
|
|
346
|
+
}
|
|
347
|
+
return processResult(result);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const invocation = buildOpencodeInvocation({ prompt: injectedPrompt, worktree, env, resume, sessionId, model });
|
|
351
|
+
const resultPromise = staleSessionHandler(invocation);
|
|
352
|
+
|
|
353
|
+
return { invocation, resultPromise };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export {
|
|
357
|
+
buildOpencodeInvocation,
|
|
358
|
+
extractOpencodeSessionId,
|
|
359
|
+
resolveOpencodeCommand,
|
|
360
|
+
startOpencodeAgent,
|
|
361
|
+
isSpuriousOpencodeExit,
|
|
362
|
+
isTransientOpencodeFailure,
|
|
363
|
+
isHardOpencodeFailure,
|
|
364
|
+
shouldRetryOpencodeFailure,
|
|
365
|
+
__setSpawnAndTeeForTest,
|
|
366
|
+
__setExportCaptureForTest,
|
|
367
|
+
__setSessionsForTest,
|
|
368
|
+
__setJsonFormatSupportForTest,
|
|
369
|
+
__setJsonFormatDetectForTest,
|
|
370
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveStageTelemetry = resolveStageTelemetry;
|
|
3
4
|
// Shared resolution of the most faithful telemetry for a recorded workflow
|
|
4
5
|
// stage (execute/review). Codex's authoritative usage is the windowed sum read
|
|
5
6
|
// from its on-disk rollout — `sinceMs` bounds the read to just this stage — so
|
|
@@ -13,30 +14,28 @@
|
|
|
13
14
|
// fixes was active.js computing `sinceMs` and then discarding it, recording
|
|
14
15
|
// cumulative Codex usage for the execute phase instead of the windowed delta
|
|
15
16
|
// (task-1285 review F1/F6).
|
|
16
|
-
|
|
17
|
-
const { codexHomeRoot, extractCodexTelemetry } = require('./codex');
|
|
18
|
-
|
|
17
|
+
const codex_js_1 = require("./codex.js");
|
|
19
18
|
/**
|
|
20
|
-
* @
|
|
21
|
-
* @returns {object|null} normalized telemetry, or null when none is available.
|
|
19
|
+
* @returns normalized telemetry, or null when none is available.
|
|
22
20
|
*/
|
|
23
|
-
function resolveStageTelemetry({ worktree, result, sinceMs = 0 } =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
21
|
+
function resolveStageTelemetry({ worktree, result, sinceMs = 0 } = {}) {
|
|
22
|
+
if (!result || !result.telemetry) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const codexT = (0, codex_js_1.extractCodexTelemetry)((0, codex_js_1.codexHomeRoot)(worktree), { sinceMs: sinceMs || 0 });
|
|
27
|
+
// Guard against stale Codex rollouts bleeding into non-Codex stages:
|
|
28
|
+
// if the launcher attached telemetry with a provider that differs from
|
|
29
|
+
// what Codex would produce (e.g. anthropic vs openai), discard the Codex
|
|
30
|
+
// read and use the launcher-attached telemetry instead.
|
|
31
|
+
if (codexT && codexT.provider && result.telemetry.provider
|
|
32
|
+
&& codexT.provider.toLowerCase() !== result.telemetry.provider.toLowerCase()) {
|
|
33
|
+
return result.telemetry;
|
|
34
|
+
}
|
|
35
|
+
return codexT || result.telemetry;
|
|
36
|
+
}
|
|
37
|
+
catch (_) {
|
|
38
|
+
// Codex rollout unreadable: fall back to this stage's launcher telemetry.
|
|
39
|
+
return result.telemetry;
|
|
34
40
|
}
|
|
35
|
-
return codexT || result.telemetry;
|
|
36
|
-
} catch (_) {
|
|
37
|
-
// Codex rollout unreadable: fall back to this stage's launcher telemetry.
|
|
38
|
-
return result.telemetry;
|
|
39
|
-
}
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
-
module.exports = { resolveStageTelemetry };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Shared resolution of the most faithful telemetry for a recorded workflow
|
|
4
|
+
// stage (execute/review). Codex's authoritative usage is the windowed sum read
|
|
5
|
+
// from its on-disk rollout — `sinceMs` bounds the read to just this stage — so
|
|
6
|
+
// prefer it when present. For non-Codex agents (e.g. Claude, whose usage the
|
|
7
|
+
// launcher parses from stdout via claude-telemetry.js) no codex rollout exists
|
|
8
|
+
// and extractCodexTelemetry returns null *without throwing*, so we fall back to
|
|
9
|
+
// the telemetry the launcher already attached.
|
|
10
|
+
//
|
|
11
|
+
// This lives in one place so the execute hook (commands/active.js) and the
|
|
12
|
+
// review-loop hook (review/review-loop.js) cannot drift apart — the bug this
|
|
13
|
+
// fixes was active.js computing `sinceMs` and then discarding it, recording
|
|
14
|
+
// cumulative Codex usage for the execute phase instead of the windowed delta
|
|
15
|
+
// (task-1285 review F1/F6).
|
|
16
|
+
|
|
17
|
+
import { codexHomeRoot, extractCodexTelemetry } from './codex.js';
|
|
18
|
+
|
|
19
|
+
interface StageTelemetryOptions {
|
|
20
|
+
worktree: string;
|
|
21
|
+
result: { telemetry?: { provider?: string } & Record<string, any> };
|
|
22
|
+
sinceMs?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @returns normalized telemetry, or null when none is available.
|
|
27
|
+
*/
|
|
28
|
+
function resolveStageTelemetry({ worktree, result, sinceMs = 0 }: StageTelemetryOptions = {} as StageTelemetryOptions) {
|
|
29
|
+
if (!result || !result.telemetry) {return null;}
|
|
30
|
+
try {
|
|
31
|
+
const codexT = extractCodexTelemetry(codexHomeRoot(worktree), { sinceMs: sinceMs || 0 });
|
|
32
|
+
// Guard against stale Codex rollouts bleeding into non-Codex stages:
|
|
33
|
+
// if the launcher attached telemetry with a provider that differs from
|
|
34
|
+
// what Codex would produce (e.g. anthropic vs openai), discard the Codex
|
|
35
|
+
// read and use the launcher-attached telemetry instead.
|
|
36
|
+
if (codexT && codexT.provider && result.telemetry.provider
|
|
37
|
+
&& codexT.provider.toLowerCase() !== result.telemetry.provider.toLowerCase()) {
|
|
38
|
+
return result.telemetry;
|
|
39
|
+
}
|
|
40
|
+
return codexT || result.telemetry;
|
|
41
|
+
} catch (_) {
|
|
42
|
+
// Codex rollout unreadable: fall back to this stage's launcher telemetry.
|
|
43
|
+
return result.telemetry;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { resolveStageTelemetry };
|