@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
package/lib/agents/opencode.js
CHANGED
|
@@ -1,43 +1,48 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildOpencodeInvocation = buildOpencodeInvocation;
|
|
4
|
+
exports.extractOpencodeSessionId = extractOpencodeSessionId;
|
|
5
|
+
exports.resolveOpencodeCommand = resolveOpencodeCommand;
|
|
6
|
+
exports.startOpencodeAgent = startOpencodeAgent;
|
|
7
|
+
exports.isSpuriousOpencodeExit = isSpuriousOpencodeExit;
|
|
8
|
+
exports.isTransientOpencodeFailure = isTransientOpencodeFailure;
|
|
9
|
+
exports.isHardOpencodeFailure = isHardOpencodeFailure;
|
|
10
|
+
exports.shouldRetryOpencodeFailure = shouldRetryOpencodeFailure;
|
|
11
|
+
exports.__setSpawnAndTeeForTest = __setSpawnAndTeeForTest;
|
|
12
|
+
exports.__setExportCaptureForTest = __setExportCaptureForTest;
|
|
13
|
+
exports.__setSessionsForTest = __setSessionsForTest;
|
|
14
|
+
exports.__setJsonFormatSupportForTest = __setJsonFormatSupportForTest;
|
|
15
|
+
exports.__setJsonFormatDetectForTest = __setJsonFormatDetectForTest;
|
|
16
|
+
const spawn_tee_js_1 = require("../core/spawn-tee.js");
|
|
17
|
+
const opencode_telemetry_js_1 = require("./opencode-telemetry.js");
|
|
18
|
+
const opencode_export_js_1 = require("./opencode-export.js");
|
|
19
|
+
const limit_hit_js_1 = require("./limit-hit.js");
|
|
20
|
+
// tools/sessions and core/subagent-limit are still CJS (not converted in this
|
|
21
|
+
// wave); require keeps them untyped (any) without pulling non-included .js into
|
|
22
|
+
// the typecheck program.
|
|
4
23
|
const sessions = require('../tools/sessions');
|
|
5
|
-
const { detectLimitHit } = require('./limit-hit');
|
|
6
24
|
const { buildSubagentLimitPrefix } = require('../core/subagent-limit');
|
|
7
|
-
|
|
8
25
|
// Injectable I/O for tests. Production uses the real spawn-tee / export capture.
|
|
9
|
-
|
|
10
|
-
let
|
|
11
|
-
/** @type {Function} */
|
|
12
|
-
let _captureExport = captureOpencodeExport;
|
|
13
|
-
/** @type {object} */
|
|
26
|
+
let _spawnAndTee = spawn_tee_js_1.spawnAndTee;
|
|
27
|
+
let _captureExport = opencode_export_js_1.captureOpencodeExport;
|
|
14
28
|
let _sessions = sessions;
|
|
15
|
-
|
|
16
29
|
// Test hooks: override the launcher's I/O without touching the public signature.
|
|
17
|
-
|
|
18
|
-
function
|
|
19
|
-
/** @param {Function} fn */
|
|
20
|
-
function __setExportCaptureForTest(fn) { _captureExport = fn || captureOpencodeExport; }
|
|
21
|
-
/** @param {object} mod */
|
|
30
|
+
function __setSpawnAndTeeForTest(fn) { _spawnAndTee = fn || spawn_tee_js_1.spawnAndTee; }
|
|
31
|
+
function __setExportCaptureForTest(fn) { _captureExport = fn || opencode_export_js_1.captureOpencodeExport; }
|
|
22
32
|
function __setSessionsForTest(mod) { _sessions = mod || sessions; }
|
|
23
|
-
|
|
24
33
|
// Test hook: override the cached feature-detect for `--format json` support.
|
|
25
34
|
// Set to true/false to force inclusion/exclusion of the flag regardless of
|
|
26
35
|
// the real binary. Pass null to reset to the live detect.
|
|
27
|
-
/** @param {boolean|null} val */
|
|
28
36
|
function __setJsonFormatSupportForTest(val) {
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
_jsonFormatSupported = val;
|
|
38
|
+
_jsonFormatDetectFn = null;
|
|
31
39
|
}
|
|
32
|
-
|
|
33
40
|
// Test hook: inject a feature-detect function for tests. This replaces the
|
|
34
41
|
// live shell-out with a stubbed function, making tests hermetic.
|
|
35
|
-
/** @param {Function} fn */
|
|
36
42
|
function __setJsonFormatDetectForTest(fn) {
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
_jsonFormatDetectFn = fn;
|
|
44
|
+
_jsonFormatSupported = null; // invalidate cache so the detect fn runs
|
|
39
45
|
}
|
|
40
|
-
|
|
41
46
|
// Session-id recovery for telemetry. After session completion `opencode export`
|
|
42
47
|
// is invoked to extract token-usage data (see opencode-telemetry.js), so the
|
|
43
48
|
// session id MUST be recoverable from the launcher's stdout. Stats hooks in
|
|
@@ -53,100 +58,99 @@ function __setJsonFormatDetectForTest(fn) {
|
|
|
53
58
|
// so this field is always present.
|
|
54
59
|
const OPENCODE_SESSION_ID_RE = /opencode\s+-s\s+(ses_\S+)/i;
|
|
55
60
|
const OPENCODE_JSON_SESSION_ID_RE = /"sessionID"\s*:\s*"(ses_[^"]+)"/;
|
|
56
|
-
|
|
57
|
-
/** @param {string} stdout */
|
|
58
61
|
function extractOpencodeSessionId(stdout) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
if (!stdout) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const footer = OPENCODE_SESSION_ID_RE.exec(stdout);
|
|
66
|
+
if (footer) {
|
|
67
|
+
return footer[1];
|
|
68
|
+
}
|
|
69
|
+
const json = OPENCODE_JSON_SESSION_ID_RE.exec(stdout);
|
|
70
|
+
return json ? json[1] : null;
|
|
64
71
|
}
|
|
65
|
-
|
|
66
72
|
// Cached feature-detect for `--format json` support. Set to `false` only when
|
|
67
73
|
// a real invocation proves the flag is rejected (older opencode versions).
|
|
68
74
|
// Tests can inject a canned result via __setJsonFormatSupportForTest.
|
|
69
|
-
/** @type {boolean | null} */
|
|
70
75
|
let _jsonFormatSupported = null;
|
|
71
|
-
|
|
72
76
|
// Injectable feature-detect function for tests. When set, checkJsonFormatSupport()
|
|
73
77
|
// calls this function instead of shelling out, making tests hermetic.
|
|
74
|
-
/** @type {Function | null} */
|
|
75
78
|
let _jsonFormatDetectFn = null;
|
|
76
|
-
|
|
77
79
|
function resolveOpencodeCommand() {
|
|
78
|
-
|
|
80
|
+
return 'opencode';
|
|
79
81
|
}
|
|
80
|
-
|
|
81
82
|
// Lightweight feature-detect: try `opencode --format json --help`.
|
|
82
83
|
// Returns true if the flag is accepted (exit 0 or non-zero for unrelated reasons),
|
|
83
84
|
// false if the flag itself is rejected ("unrecognized" / "unknown option").
|
|
84
85
|
// Results are cached so the check runs at most once in production.
|
|
85
86
|
// Tests can inject a canned result via __setJsonFormatSupportForTest.
|
|
86
87
|
function checkJsonFormatSupport() {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
88
|
+
if (_jsonFormatSupported !== null) {
|
|
89
|
+
return _jsonFormatSupported;
|
|
90
|
+
}
|
|
91
|
+
if (_jsonFormatDetectFn) {
|
|
92
|
+
_jsonFormatSupported = _jsonFormatDetectFn();
|
|
93
|
+
return _jsonFormatSupported;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const { spawnSync } = require('node:child_process');
|
|
97
|
+
const result = spawnSync('opencode', ['--format', 'json', '--help'], {
|
|
98
|
+
timeout: 3000,
|
|
99
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
100
|
+
});
|
|
101
|
+
const stderr = (result.stderr && result.stderr.toString()) || '';
|
|
102
|
+
const combined = stderr;
|
|
103
|
+
// Flag rejected → fallback to legacy invocation.
|
|
104
|
+
if (/unrecognized|unknown option|no such option|invalid option/i.test(combined)) {
|
|
105
|
+
_jsonFormatSupported = false;
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
_jsonFormatSupported = true;
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
catch (_) {
|
|
112
|
+
// ENOENT or other spawn errors → assume legacy (no JSON flag).
|
|
113
|
+
_jsonFormatSupported = false;
|
|
114
|
+
return false;
|
|
104
115
|
}
|
|
105
|
-
_jsonFormatSupported = true;
|
|
106
|
-
return true;
|
|
107
|
-
} catch (_) {
|
|
108
|
-
// ENOENT or other spawn errors → assume legacy (no JSON flag).
|
|
109
|
-
_jsonFormatSupported = false;
|
|
110
|
-
return false;
|
|
111
|
-
}
|
|
112
116
|
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* @param {{ prompt: string, worktree: string, env?: object, resume?: boolean, sessionId?: string | null, model?: string | null, preferJson?: boolean }} opts
|
|
116
|
-
*/
|
|
117
117
|
function buildOpencodeInvocation({ prompt, worktree, env, resume = false, sessionId = null, model = null, preferJson = true }) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
options: {
|
|
143
|
-
stdio: 'inherit',
|
|
144
|
-
cwd: worktree,
|
|
145
|
-
env: { ...process.env, ...env }
|
|
118
|
+
// `--format json` makes opencode stream NDJSON events that each carry a
|
|
119
|
+
// "sessionID":"ses_..." field. opencode v2.0.0's default `run` output no
|
|
120
|
+
// longer prints the "Continue opencode -s ses_..." footer, so JSON is the
|
|
121
|
+
// only reliable way to recover the session id needed for `opencode export`
|
|
122
|
+
// telemetry capture (task-1339).
|
|
123
|
+
//
|
|
124
|
+
// Compatibility guard: feature-detect the flag on first call; older opencode
|
|
125
|
+
// versions that reject `--format json` will silently fall back to the legacy
|
|
126
|
+
// invocation, preserving launch behaviour at the cost of telemetry.
|
|
127
|
+
const useJson = preferJson && checkJsonFormatSupport();
|
|
128
|
+
const args = ['run', '--pure', '--dangerously-skip-permissions'];
|
|
129
|
+
if (useJson) {
|
|
130
|
+
args.push('--format', 'json');
|
|
131
|
+
}
|
|
132
|
+
if (model) {
|
|
133
|
+
args.push('-m', model);
|
|
134
|
+
}
|
|
135
|
+
// Only resume when the marker explicitly says so (resume=true).
|
|
136
|
+
// sessionId from a stale cross-family marker must be ignored when resume=false.
|
|
137
|
+
if (resume && sessionId) {
|
|
138
|
+
args.push('-s', sessionId);
|
|
139
|
+
}
|
|
140
|
+
else if (resume) {
|
|
141
|
+
args.push('--continue');
|
|
146
142
|
}
|
|
147
|
-
|
|
143
|
+
args.push(prompt);
|
|
144
|
+
return {
|
|
145
|
+
command: resolveOpencodeCommand(),
|
|
146
|
+
args,
|
|
147
|
+
options: {
|
|
148
|
+
stdio: 'inherit',
|
|
149
|
+
cwd: worktree,
|
|
150
|
+
env: { ...process.env, ...env }
|
|
151
|
+
}
|
|
152
|
+
};
|
|
148
153
|
}
|
|
149
|
-
|
|
150
154
|
// Transient custom/vLLM backend failures that opencode surfaces as a non-zero
|
|
151
155
|
// exit. These are recoverable: the same prompt frequently succeeds on a second
|
|
152
156
|
// attempt, so the launcher retries once in-family before agents.js reroutes to a
|
|
@@ -154,242 +158,196 @@ function buildOpencodeInvocation({ prompt, worktree, env, resume = false, sessio
|
|
|
154
158
|
// detectLimitHit() in agents.js owns that accounting (lib/agents/limit-hit.js).
|
|
155
159
|
// Hard errors (model-not-found, auth, ENOENT/EACCES) are never retried.
|
|
156
160
|
const TRANSIENT_OPENCODE_PATTERNS = [
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
161
|
+
/\bECONNRESET\b/i,
|
|
162
|
+
/\bECONNREFUSED\b/i,
|
|
163
|
+
/\bETIMEDOUT\b/i,
|
|
164
|
+
/\bENETUNREACH\b/i,
|
|
165
|
+
/\bENOTFOUND\b/i,
|
|
166
|
+
/\bEAI_AGAIN\b/i,
|
|
167
|
+
/\bEPIPE\b/i,
|
|
168
|
+
/socket hang ?up/i,
|
|
169
|
+
/\bfetch failed\b/i,
|
|
170
|
+
/network (?:error|timeout)/i,
|
|
171
|
+
/connection (?:error|reset|closed|refused|timed? ?out)/i,
|
|
172
|
+
/\b(?:502|503|504|529)\b/,
|
|
173
|
+
/\bbad gateway\b/i,
|
|
174
|
+
/\bservice (?:is )?unavailable\b/i,
|
|
175
|
+
/\bgateway time-?out\b/i,
|
|
176
|
+
/\b(?:server |api |model )?overloaded\b/i,
|
|
177
|
+
/\btemporarily unavailable\b/i,
|
|
178
|
+
/\bplease (?:try|retry) again\b/i,
|
|
179
|
+
/\bretry[- ]after\b/i,
|
|
176
180
|
];
|
|
177
|
-
|
|
178
181
|
// Hard, non-retryable failures: retrying cannot help and only wastes time/quota.
|
|
179
182
|
const HARD_OPENCODE_PATTERNS = [
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
/\bmodel not found\b/i,
|
|
184
|
+
/\bno such model\b/i,
|
|
185
|
+
/\bunknown model\b/i,
|
|
186
|
+
/\binvalid (?:api )?key\b/i,
|
|
187
|
+
/\b401\b[^\n]*\bunauthorized\b/i,
|
|
188
|
+
/\bauthentication (?:failed|error)\b/i,
|
|
186
189
|
];
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* @param {{stdout?: string, stderr?: string}} result
|
|
190
|
-
*/
|
|
191
190
|
function failureText(result) {
|
|
192
|
-
|
|
191
|
+
return `${(result && result.stdout) || ''}\n${(result && result.stderr) || ''}`;
|
|
193
192
|
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* @param {{stdout?: string, stderr?: string, error?: {code?: string}}} result
|
|
197
|
-
*/
|
|
198
193
|
function isHardOpencodeFailure(result) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
194
|
+
if (!result) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
if (result.error && (result.error.code === 'ENOENT' || result.error.code === 'EACCES')) {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
const text = failureText(result);
|
|
201
|
+
return HARD_OPENCODE_PATTERNS.some((re) => re.test(text));
|
|
203
202
|
}
|
|
204
|
-
|
|
205
203
|
// Opencode v2.0.0 JSON-mode sometimes exits with code 1 during post-run
|
|
206
204
|
// cleanup (race closing the stdout pipe, EPIPE, or draining pending events).
|
|
207
205
|
// When the transcript contains a valid "reason":"stop" completion event and
|
|
208
206
|
// no "type":"error" event, the exit is spurious – the agent actually
|
|
209
207
|
// completed successfully. See opencode issues #31446 and #33653.
|
|
210
|
-
/**
|
|
211
|
-
* @param {{status?: number, stdout?: string, stderr?: string}} result
|
|
212
|
-
*/
|
|
213
208
|
function isSpuriousOpencodeExit(result) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
209
|
+
if (!result || result.status !== 1) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
const text = failureText(result);
|
|
213
|
+
const hasStop = /"reason"\s*:\s*"stop"/.test(text);
|
|
214
|
+
const hasError = /"type"\s*:\s*"error"/.test(text);
|
|
215
|
+
return hasStop && !hasError;
|
|
219
216
|
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* @param {{stdout?: string, stderr?: string, error?: {code?: string}, signal?: string, status?: number}} result
|
|
223
|
-
*/
|
|
224
217
|
function isTransientOpencodeFailure(result) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
218
|
+
if (!result) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
const text = failureText(result);
|
|
222
|
+
return TRANSIENT_OPENCODE_PATTERNS.some((re) => re.test(text));
|
|
228
223
|
}
|
|
229
|
-
|
|
230
224
|
// Decide whether a failed opencode run should be retried once in-family.
|
|
231
225
|
// Only genuine non-zero exits carrying a transient backend signature qualify;
|
|
232
226
|
// clean exits, spawn errors (ENOENT/EACCES), killing signals, limit-hits, and
|
|
233
227
|
// hard errors never retry — so real failures still surface to the reroute loop.
|
|
234
|
-
/** @param {{stdout?: string, stderr?: string, status?: number | null, signal?: string | null, error?: {code?: string}, sessionId?: string | null}} result */
|
|
235
228
|
function shouldRetryOpencodeFailure(result) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
229
|
+
if (!result) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
if (result.error && (result.error.code === 'ENOENT' || result.error.code === 'EACCES')) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
if (result.signal) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
if (!(typeof result.status === 'number' && result.status !== 0)) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
if ((0, limit_hit_js_1.detectLimitHit)({
|
|
242
|
+
agent: 'custom',
|
|
243
|
+
stdout: result.stdout,
|
|
244
|
+
stderr: result.stderr,
|
|
245
|
+
status: result.status,
|
|
246
|
+
signal: result.signal,
|
|
247
|
+
error: result.error,
|
|
248
|
+
})) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
if (isHardOpencodeFailure(result)) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
return isTransientOpencodeFailure(result);
|
|
250
255
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
slug = null,
|
|
264
|
-
role = null,
|
|
265
|
-
maxTransientRetries = 1
|
|
266
|
-
}) {
|
|
267
|
-
// Prepend the subagent-limit advisory prefix to the prompt.
|
|
268
|
-
// When maxParallel is unset/null/zero the prefix is empty (no-op).
|
|
269
|
-
const subagentPrefix = buildSubagentLimitPrefix();
|
|
270
|
-
const injectedPrompt = subagentPrefix + prompt;
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* @param {{stdout?: string, stderr?: string}} result
|
|
274
|
-
*/
|
|
275
|
-
function isStaleSessionResult(result) {
|
|
276
|
-
if (!result) {return false;}
|
|
277
|
-
const stderr = result.stderr || '';
|
|
278
|
-
const stdout = result.stdout || '';
|
|
279
|
-
return stderr.includes('Session not found') || stdout.includes('Session not found');
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* @param {{command: string, args: string[], options?: object}} invocation
|
|
284
|
-
*/
|
|
285
|
-
function runInvocation(invocation) {
|
|
286
|
-
const spawnOptions = { ...invocation.options, ...teeOptions };
|
|
287
|
-
return _spawnAndTee(invocation.command, invocation.args, spawnOptions);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* @param {{command: string, args: string[], options?: object}} invocation
|
|
292
|
-
*/
|
|
293
|
-
async function runInvocationWithRetry(invocation) {
|
|
294
|
-
let attempts = 0;
|
|
295
|
-
let result = await runInvocation(invocation);
|
|
296
|
-
while (attempts < maxTransientRetries && shouldRetryOpencodeFailure(result)) {
|
|
297
|
-
attempts += 1;
|
|
298
|
-
result = await runInvocation(invocation);
|
|
256
|
+
function startOpencodeAgent({ prompt, worktree, env, resume = false, sessionId = null, model = null, teeOptions = {}, slug = null, role = null, maxTransientRetries = 1 }) {
|
|
257
|
+
// Prepend the subagent-limit advisory prefix to the prompt.
|
|
258
|
+
// When maxParallel is unset/null/zero the prefix is empty (no-op).
|
|
259
|
+
const subagentPrefix = buildSubagentLimitPrefix();
|
|
260
|
+
const injectedPrompt = subagentPrefix + prompt;
|
|
261
|
+
function isStaleSessionResult(result) {
|
|
262
|
+
if (!result) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
const stderr = result.stderr || '';
|
|
266
|
+
const stdout = result.stdout || '';
|
|
267
|
+
return stderr.includes('Session not found') || stdout.includes('Session not found');
|
|
299
268
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
// Detect whether `--format json` was rejected at runtime (e.g. older opencode
|
|
305
|
-
// that wasn't caught by the feature-detect). If so, retry the invocation
|
|
306
|
-
// without the flag so the prompt still runs (telemetry will be lost but the
|
|
307
|
-
// agent won't fail outright).
|
|
308
|
-
/**
|
|
309
|
-
* @param {{stdout?: string, stderr?: string}} result
|
|
310
|
-
*/
|
|
311
|
-
function isJsonFlagError(result) {
|
|
312
|
-
if (!result) {return false;}
|
|
313
|
-
const text = failureText(result);
|
|
314
|
-
return (/\bunrecognized option\b|\bunknown option\b|\bno such option\b|\binvalid option\b|\bunrecognized flag\b|\bunknown flag\b/i).test(text);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* @param {{stdout?: string, sessionId?: string, telemetry?: object, model?: string, provider?: string, transientRetries?: number, _jsonFallback?: boolean}} result
|
|
319
|
-
*/
|
|
320
|
-
async function processResult(result) {
|
|
321
|
-
if (result && result.stdout) {
|
|
322
|
-
result.sessionId = extractOpencodeSessionId(result.stdout) || undefined;
|
|
269
|
+
function runInvocation(invocation) {
|
|
270
|
+
const spawnOptions = { ...invocation.options, ...teeOptions };
|
|
271
|
+
return _spawnAndTee(invocation.command, invocation.args, spawnOptions);
|
|
323
272
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
273
|
+
async function runInvocationWithRetry(invocation) {
|
|
274
|
+
let attempts = 0;
|
|
275
|
+
let result = await runInvocation(invocation);
|
|
276
|
+
while (attempts < maxTransientRetries && shouldRetryOpencodeFailure(result)) {
|
|
277
|
+
attempts += 1;
|
|
278
|
+
result = await runInvocation(invocation);
|
|
279
|
+
}
|
|
280
|
+
if (result) {
|
|
281
|
+
result.transientRetries = attempts;
|
|
334
282
|
}
|
|
335
|
-
} catch (_) {
|
|
336
283
|
return result;
|
|
337
|
-
}
|
|
338
284
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if (isJsonFlagError(result) && invocation.args.includes('--format')) {
|
|
350
|
-
const legacyInv = buildOpencodeInvocation({
|
|
351
|
-
prompt: injectedPrompt, worktree, env, resume, sessionId, model, preferJson: false,
|
|
352
|
-
});
|
|
353
|
-
result = await runInvocationWithRetry(legacyInv);
|
|
354
|
-
// Mark that we fell back so callers know telemetry may be absent.
|
|
355
|
-
if (result) {result._jsonFallback = true;}
|
|
285
|
+
// Detect whether `--format json` was rejected at runtime (e.g. older opencode
|
|
286
|
+
// that wasn't caught by the feature-detect). If so, retry the invocation
|
|
287
|
+
// without the flag so the prompt still runs (telemetry will be lost but the
|
|
288
|
+
// agent won't fail outright).
|
|
289
|
+
function isJsonFlagError(result) {
|
|
290
|
+
if (!result) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
const text = failureText(result);
|
|
294
|
+
return (/\bunrecognized option\b|\bunknown option\b|\bno such option\b|\binvalid option\b|\bunrecognized flag\b|\bunknown flag\b/i).test(text);
|
|
356
295
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
296
|
+
async function processResult(result) {
|
|
297
|
+
if (result && result.stdout) {
|
|
298
|
+
result.sessionId = extractOpencodeSessionId(result.stdout) || undefined;
|
|
299
|
+
}
|
|
300
|
+
if (result && result.sessionId) {
|
|
301
|
+
try {
|
|
302
|
+
const exportJson = await _captureExport(result.sessionId, { worktree, env });
|
|
303
|
+
if (exportJson) {
|
|
304
|
+
const telemetry = (0, opencode_telemetry_js_1.extractOpencodeTelemetryFromExport)(exportJson, model || undefined);
|
|
305
|
+
if (telemetry) {
|
|
306
|
+
result.telemetry = telemetry;
|
|
307
|
+
if (telemetry.model) {
|
|
308
|
+
result.model = telemetry.model;
|
|
309
|
+
}
|
|
310
|
+
if (telemetry.provider) {
|
|
311
|
+
result.provider = telemetry.provider;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch (_) {
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
async function runWithJsonFallback(invocation) {
|
|
323
|
+
let result = await runInvocationWithRetry(invocation);
|
|
324
|
+
// Runtime fallback: if the first invocation used --format json but the
|
|
325
|
+
// binary rejected it, retry without the flag so the prompt still executes.
|
|
326
|
+
if (isJsonFlagError(result) && invocation.args.includes('--format')) {
|
|
327
|
+
const legacyInv = buildOpencodeInvocation({
|
|
328
|
+
prompt: injectedPrompt, worktree, env, resume, sessionId, model, preferJson: false,
|
|
329
|
+
});
|
|
330
|
+
result = await runInvocationWithRetry(legacyInv);
|
|
331
|
+
// Mark that we fell back so callers know telemetry may be absent.
|
|
332
|
+
if (result) {
|
|
333
|
+
result._jsonFallback = true;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return result;
|
|
337
|
+
}
|
|
338
|
+
async function staleSessionHandler(invocation) {
|
|
339
|
+
let result = await runWithJsonFallback(invocation);
|
|
340
|
+
if (isStaleSessionResult(result) && worktree && resume) {
|
|
341
|
+
try {
|
|
342
|
+
_sessions.clearSession(worktree, slug || '', role || '');
|
|
343
|
+
}
|
|
344
|
+
catch (_) { /* best-effort */ }
|
|
345
|
+
const freshInv = buildOpencodeInvocation({ prompt: injectedPrompt, worktree, env, resume: false, sessionId: null, model });
|
|
346
|
+
result = await runWithJsonFallback(freshInv);
|
|
347
|
+
}
|
|
348
|
+
return processResult(result);
|
|
371
349
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const invocation = buildOpencodeInvocation({ prompt: injectedPrompt, worktree, env, resume, sessionId, model });
|
|
376
|
-
const resultPromise = staleSessionHandler(invocation);
|
|
377
|
-
|
|
378
|
-
return { invocation, resultPromise };
|
|
350
|
+
const invocation = buildOpencodeInvocation({ prompt: injectedPrompt, worktree, env, resume, sessionId, model });
|
|
351
|
+
const resultPromise = staleSessionHandler(invocation);
|
|
352
|
+
return { invocation, resultPromise };
|
|
379
353
|
}
|
|
380
|
-
|
|
381
|
-
module.exports = {
|
|
382
|
-
buildOpencodeInvocation,
|
|
383
|
-
extractOpencodeSessionId,
|
|
384
|
-
resolveOpencodeCommand,
|
|
385
|
-
startOpencodeAgent,
|
|
386
|
-
isSpuriousOpencodeExit,
|
|
387
|
-
isTransientOpencodeFailure,
|
|
388
|
-
isHardOpencodeFailure,
|
|
389
|
-
shouldRetryOpencodeFailure,
|
|
390
|
-
__setSpawnAndTeeForTest,
|
|
391
|
-
__setExportCaptureForTest,
|
|
392
|
-
__setSessionsForTest,
|
|
393
|
-
__setJsonFormatSupportForTest,
|
|
394
|
-
__setJsonFormatDetectForTest,
|
|
395
|
-
};
|