amicus 4.5.1 → 4.5.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.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +75 -0
- package/README.md +142 -93
- package/bin/amicus.js +23 -0
- package/docs/configuration.md +16 -0
- package/docs/usage.md +1 -1
- package/package.json +3 -2
- package/schemas/council-verdict.schema.json +183 -29
- package/src/cli.js +21 -5
- package/src/council/run-assemble.js +7 -3
- package/src/council/run.js +2 -1
- package/src/council/verdict.js +44 -1
- package/src/headless.js +6 -0
- package/src/opencode-client.js +79 -2
- package/src/sidecar/session-utils.js +4 -0
- package/src/sidecar/unzip.js +16 -1
- package/src/utils/known-flags.js +90 -0
- package/src/utils/server-setup.js +74 -7
|
@@ -89,6 +89,28 @@ function ensurePortAvailable(port = DEFAULT_PORT) {
|
|
|
89
89
|
*/
|
|
90
90
|
const LOCK_CLASS_START_FAILURE = /database is locked|database table is locked|SQLITE_BUSY/i;
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* A start failure that is a TIMEOUT, not a deterministic error.
|
|
94
|
+
*
|
|
95
|
+
* `@opencode-ai/sdk` rejects with `Timeout waiting for server to start after
|
|
96
|
+
* ${timeout}ms` when OpenCode has not printed its listening line inside the
|
|
97
|
+
* caller-supplied window (SDK default: 5000ms — see AMICUS_SERVER_START_TIMEOUT_MS
|
|
98
|
+
* in src/opencode-client.js for why amicus no longer accepts that default).
|
|
99
|
+
*
|
|
100
|
+
* ⚠️ ADDED v4.5.2 from a field report. A start timeout is TRANSIENT — a cold
|
|
101
|
+
* SQLite open on a sync-backed volume with an AV scanner attached simply takes
|
|
102
|
+
* longer than the window — and is therefore *more* retryable than a lock race,
|
|
103
|
+
* since retrying costs nothing but the backoff. Before this, it matched no
|
|
104
|
+
* alternative in LOCK_CLASS_START_FAILURE and so fell straight through
|
|
105
|
+
* `retryOnLockRace` with ZERO retries. A reporter's council degraded to per-wave
|
|
106
|
+
* servers on this error and then lost its entire Stage-1 bench
|
|
107
|
+
* (`COUNCIL_QUORUM: Only 0 Stage-1 review(s) survived`).
|
|
108
|
+
*
|
|
109
|
+
* Deliberately anchored to "…server to start". A REQUEST timeout, an ETIMEDOUT
|
|
110
|
+
* connect, and a generic "timeout" are NOT this class and must not sleep here.
|
|
111
|
+
*/
|
|
112
|
+
const TIMEOUT_CLASS_START_FAILURE = /Timeout waiting for server to start/i;
|
|
113
|
+
|
|
92
114
|
/**
|
|
93
115
|
* Backoff between start attempts; 5 attempts total, ≤3.75s of added latency.
|
|
94
116
|
*
|
|
@@ -109,12 +131,48 @@ const LOCK_RETRY_DELAYS_MS = [250, 500, 1000, 2000];
|
|
|
109
131
|
* @returns {boolean} true only for a lock-class (retryable) start failure
|
|
110
132
|
*/
|
|
111
133
|
function isLockClassStartFailure(error) {
|
|
134
|
+
return matchesStartFailure(error, LOCK_CLASS_START_FAILURE);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @param {Error|null} error
|
|
139
|
+
* @returns {boolean} true only for a timeout-class (retryable) start failure
|
|
140
|
+
*/
|
|
141
|
+
function isTimeoutClassStartFailure(error) {
|
|
142
|
+
return matchesStartFailure(error, TIMEOUT_CLASS_START_FAILURE);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The union the retry actually applies to: lock-class OR timeout-class.
|
|
147
|
+
*
|
|
148
|
+
* Kept separate from the two predicates so each class keeps its own narrow,
|
|
149
|
+
* accurate meaning — `isLockClassStartFailure` still answers "was this a lock
|
|
150
|
+
* race?" and nothing else, so its docblock does not quietly become a lie.
|
|
151
|
+
*
|
|
152
|
+
* @param {Error|null} error
|
|
153
|
+
* @returns {boolean} true for any transient (retryable) start failure
|
|
154
|
+
*/
|
|
155
|
+
function isRetryableStartFailure(error) {
|
|
156
|
+
return isLockClassStartFailure(error) || isTimeoutClassStartFailure(error);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Test `pattern` against every carrier an error might arrive on.
|
|
161
|
+
*
|
|
162
|
+
* The real failure arrives as a message with the server's own stdout inlined
|
|
163
|
+
* ("Server exited with code 1 / Server output: … database is locked"), and
|
|
164
|
+
* amicus prefixes it again at the fanout boundary (`Failed to start server:
|
|
165
|
+
* …`), so check the usual carriers too — a wrapped/spawn-shaped error still
|
|
166
|
+
* has to match.
|
|
167
|
+
*
|
|
168
|
+
* @param {Error|null} error
|
|
169
|
+
* @param {RegExp} pattern
|
|
170
|
+
* @returns {boolean}
|
|
171
|
+
*/
|
|
172
|
+
function matchesStartFailure(error, pattern) {
|
|
112
173
|
if (!error) { return false; }
|
|
113
|
-
// The real failure arrives as a message with the server's own stdout inlined
|
|
114
|
-
// ("Server exited with code 1 / Server output: … database is locked"), but
|
|
115
|
-
// check the usual carriers too so a wrapped/spawn-shaped error still matches.
|
|
116
174
|
const carriers = [error.message, error.stderr, error.stdout, error.cause && error.cause.message];
|
|
117
|
-
return carriers.some(c => typeof c === 'string' &&
|
|
175
|
+
return carriers.some(c => typeof c === 'string' && pattern.test(c));
|
|
118
176
|
}
|
|
119
177
|
|
|
120
178
|
/**
|
|
@@ -141,9 +199,16 @@ async function retryOnLockRace(attempt, opts = {}) {
|
|
|
141
199
|
try {
|
|
142
200
|
return await attempt(i);
|
|
143
201
|
} catch (error) {
|
|
144
|
-
if (i >= delays.length || !
|
|
145
|
-
logger.warn('OpenCode server start
|
|
146
|
-
attempt: i + 1,
|
|
202
|
+
if (i >= delays.length || !isRetryableStartFailure(error)) { throw error; }
|
|
203
|
+
logger.warn('OpenCode server start failed transiently — retrying', {
|
|
204
|
+
attempt: i + 1,
|
|
205
|
+
of: delays.length + 1,
|
|
206
|
+
delayMs: delays[i],
|
|
207
|
+
// Name WHICH transient class fired: a run that retried on `timeout`
|
|
208
|
+
// wants a bigger AMICUS_SERVER_START_TIMEOUT_MS, one that retried on
|
|
209
|
+
// `lock` wants less concurrency. Same retry, different operator action.
|
|
210
|
+
failureClass: isLockClassStartFailure(error) ? 'lock' : 'timeout',
|
|
211
|
+
error: error.message,
|
|
147
212
|
});
|
|
148
213
|
await new Promise(resolve => setTimeout(resolve, delays[i]));
|
|
149
214
|
}
|
|
@@ -158,5 +223,7 @@ module.exports = {
|
|
|
158
223
|
killPortProcess,
|
|
159
224
|
ensurePortAvailable,
|
|
160
225
|
isLockClassStartFailure,
|
|
226
|
+
isTimeoutClassStartFailure,
|
|
227
|
+
isRetryableStartFailure,
|
|
161
228
|
retryOnLockRace
|
|
162
229
|
};
|