@evident-ai/cli 3.4.1-dev.9c22b93 → 3.4.1-dev.ab61560
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/README.md +3 -0
- package/dist/index.js +373 -56
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -119,6 +119,9 @@ Options:
|
|
|
119
119
|
- `--opencode-config-overlay <path>` — Apply a runner-provided OpenCode config
|
|
120
120
|
before starting OpenCode. Relative paths are resolved from the working directory;
|
|
121
121
|
an existing `opencode.jsonc` is replaced before `opencode.json`.
|
|
122
|
+
- `--credential-sync-marker <path>` — Own the interval credential sync and write
|
|
123
|
+
this marker once the shutdown flush has finished, so the runner image's lifecycle
|
|
124
|
+
hooks can wait on it.
|
|
122
125
|
- `--claude-usage-reporting <mode>` — Whether to report the local Claude Code
|
|
123
126
|
subscription's rate-limit usage to Evident, so it shows on the runner page:
|
|
124
127
|
`auto` (default) reports it when a usable Claude Code login is found on this
|
package/dist/index.js
CHANGED
|
@@ -1183,7 +1183,7 @@ async function claudeUsage() {
|
|
|
1183
1183
|
}
|
|
1184
1184
|
|
|
1185
1185
|
// src/commands/run.ts
|
|
1186
|
-
import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as
|
|
1186
|
+
import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
1187
1187
|
import { homedir as homedir5 } from "os";
|
|
1188
1188
|
import { isAbsolute as isAbsolute3, join as join9, parse, resolve as resolvePath2 } from "path";
|
|
1189
1189
|
import chalk6 from "chalk";
|
|
@@ -1728,10 +1728,14 @@ function runSynchroniser(args, opts) {
|
|
|
1728
1728
|
let stderr = "";
|
|
1729
1729
|
let settled = false;
|
|
1730
1730
|
const timer = {};
|
|
1731
|
+
let abortListener;
|
|
1732
|
+
let spawnListener;
|
|
1731
1733
|
const finish = (result) => {
|
|
1732
1734
|
if (settled) return;
|
|
1733
1735
|
settled = true;
|
|
1734
1736
|
if (timer.handle) clearTimeout(timer.handle);
|
|
1737
|
+
if (abortListener) opts.signal?.removeEventListener("abort", abortListener);
|
|
1738
|
+
if (spawnListener) child.removeListener("spawn", spawnListener);
|
|
1735
1739
|
resolve4(result);
|
|
1736
1740
|
};
|
|
1737
1741
|
try {
|
|
@@ -1757,6 +1761,25 @@ function runSynchroniser(args, opts) {
|
|
|
1757
1761
|
child.once("close", (code) => {
|
|
1758
1762
|
finish({ code, stdout, stderr, timedOut: false });
|
|
1759
1763
|
});
|
|
1764
|
+
if (opts.signal) {
|
|
1765
|
+
const killChild = () => {
|
|
1766
|
+
if (child.pid === void 0) {
|
|
1767
|
+
if (!spawnListener) {
|
|
1768
|
+
spawnListener = killChild;
|
|
1769
|
+
child.once("spawn", spawnListener);
|
|
1770
|
+
}
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
child.kill("SIGKILL");
|
|
1774
|
+
};
|
|
1775
|
+
abortListener = killChild;
|
|
1776
|
+
if (opts.signal.aborted) {
|
|
1777
|
+
abortListener();
|
|
1778
|
+
} else {
|
|
1779
|
+
opts.signal.addEventListener("abort", abortListener, { once: true });
|
|
1780
|
+
if (opts.signal.aborted) abortListener();
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1760
1783
|
timer.handle = setTimeout(
|
|
1761
1784
|
() => {
|
|
1762
1785
|
child.kill("SIGKILL");
|
|
@@ -3844,8 +3867,8 @@ function connectTunnel(options) {
|
|
|
3844
3867
|
try {
|
|
3845
3868
|
message = JSON.parse(data.toString());
|
|
3846
3869
|
} catch (error2) {
|
|
3847
|
-
const
|
|
3848
|
-
onError?.(`Failed to handle message: ${
|
|
3870
|
+
const errorMessage2 = error2 instanceof Error ? error2.message : "Unknown error";
|
|
3871
|
+
onError?.(`Failed to handle message: ${errorMessage2}`);
|
|
3849
3872
|
return;
|
|
3850
3873
|
}
|
|
3851
3874
|
if (isStreamFrame(message)) {
|
|
@@ -5249,6 +5272,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5249
5272
|
* and stops opencode.
|
|
5250
5273
|
*/
|
|
5251
5274
|
stopped = false;
|
|
5275
|
+
recycleRequestedFlag = false;
|
|
5252
5276
|
constructor(config) {
|
|
5253
5277
|
this.agentId = config.agentId;
|
|
5254
5278
|
this.port = config.port;
|
|
@@ -5354,6 +5378,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5354
5378
|
let dispatched = 0;
|
|
5355
5379
|
try {
|
|
5356
5380
|
const conversations = await this.getPendingConversations();
|
|
5381
|
+
if (this.recycleRequestedFlag) {
|
|
5382
|
+
this.stop();
|
|
5383
|
+
}
|
|
5357
5384
|
if (conversations.length > 0) {
|
|
5358
5385
|
const total = conversations.reduce((sum, c) => sum + (c.pending_message_count ?? 0), 0);
|
|
5359
5386
|
this.log({
|
|
@@ -5482,6 +5509,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5482
5509
|
stop() {
|
|
5483
5510
|
this.stopped = true;
|
|
5484
5511
|
}
|
|
5512
|
+
/**
|
|
5513
|
+
* The server clears this request when a new MicroVM identity is recorded, so a
|
|
5514
|
+
* same-VM tunnel reconnect does not consume it. This is a plain read rather
|
|
5515
|
+
* than a consume; `run.ts` guards the action once-only.
|
|
5516
|
+
*/
|
|
5517
|
+
get recycleRequested() {
|
|
5518
|
+
return this.recycleRequestedFlag;
|
|
5519
|
+
}
|
|
5485
5520
|
/**
|
|
5486
5521
|
* Wait (up to `timeoutMs`) for in-flight watcher work to settle during a
|
|
5487
5522
|
* graceful shutdown, so a turn whose reply is ready — or completes within the
|
|
@@ -5619,7 +5654,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5619
5654
|
this.signalDispatchNotStarted(conv, message, "session_existence_unknown");
|
|
5620
5655
|
break;
|
|
5621
5656
|
}
|
|
5622
|
-
const
|
|
5657
|
+
const errorMessage2 = err instanceof Error ? err.message : String(err);
|
|
5623
5658
|
this.sessions.delete(conv.id);
|
|
5624
5659
|
this.supersede(conv.id, sessionId);
|
|
5625
5660
|
this.log({
|
|
@@ -5628,7 +5663,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5628
5663
|
conversation_id: conv.id,
|
|
5629
5664
|
message_id: message.id
|
|
5630
5665
|
});
|
|
5631
|
-
await this.markFailed(conv.id, message.id, null,
|
|
5666
|
+
await this.markFailed(conv.id, message.id, null, errorMessage2).catch((markErr) => {
|
|
5632
5667
|
this.log({
|
|
5633
5668
|
level: "warn",
|
|
5634
5669
|
message: `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
@@ -5639,7 +5674,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5639
5674
|
});
|
|
5640
5675
|
this.log({
|
|
5641
5676
|
level: "error",
|
|
5642
|
-
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${
|
|
5677
|
+
message: `Message ${message.id.slice(0, 8)} dispatch failed: ${errorMessage2}`,
|
|
5643
5678
|
conversation_id: conv.id,
|
|
5644
5679
|
message_id: message.id
|
|
5645
5680
|
});
|
|
@@ -5660,14 +5695,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5660
5695
|
this.unconfirmedDispatchFailures.delete(message.id);
|
|
5661
5696
|
this.sessions.delete(conv.id);
|
|
5662
5697
|
this.supersede(conv.id, sessionId);
|
|
5663
|
-
const
|
|
5698
|
+
const errorMessage2 = `OpenCode accepted this message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
|
|
5664
5699
|
this.log({
|
|
5665
5700
|
level: "error",
|
|
5666
|
-
message:
|
|
5701
|
+
message: errorMessage2,
|
|
5667
5702
|
conversation_id: conv.id,
|
|
5668
5703
|
message_id: message.id
|
|
5669
5704
|
});
|
|
5670
|
-
await this.markFailed(conv.id, message.id, null,
|
|
5705
|
+
await this.markFailed(conv.id, message.id, null, errorMessage2).catch((markErr) => {
|
|
5671
5706
|
this.log({
|
|
5672
5707
|
level: "warn",
|
|
5673
5708
|
message: `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
@@ -7631,14 +7666,14 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7631
7666
|
this.unconfirmedDispatchFailures.delete(row.id);
|
|
7632
7667
|
this.sessions.delete(readoptConv.id);
|
|
7633
7668
|
this.supersede(readoptConv.id, sessionId);
|
|
7634
|
-
const
|
|
7669
|
+
const errorMessage2 = `OpenCode accepted this re-dispatched message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
|
|
7635
7670
|
this.log({
|
|
7636
7671
|
level: "error",
|
|
7637
|
-
message:
|
|
7672
|
+
message: errorMessage2,
|
|
7638
7673
|
conversation_id: row.conversation_id,
|
|
7639
7674
|
message_id: row.id
|
|
7640
7675
|
});
|
|
7641
|
-
await this.markFailed(row.conversation_id, row.id, null,
|
|
7676
|
+
await this.markFailed(row.conversation_id, row.id, null, errorMessage2).catch((markErr) => {
|
|
7642
7677
|
this.log({
|
|
7643
7678
|
level: "warn",
|
|
7644
7679
|
message: `markFailed PATCH for message ${row.id.slice(0, 8)} (conversation ${row.conversation_id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
@@ -8253,6 +8288,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8253
8288
|
throw new Error(`Failed to get pending conversations: HTTP ${res.status}`);
|
|
8254
8289
|
}
|
|
8255
8290
|
const data = await res.json();
|
|
8291
|
+
this.recycleRequestedFlag = data.recycle_requested === true;
|
|
8256
8292
|
let conversations = data.conversations;
|
|
8257
8293
|
if (this.conversationFilter) {
|
|
8258
8294
|
conversations = conversations.filter((c) => c.id === this.conversationFilter);
|
|
@@ -9079,6 +9115,241 @@ function applyRunnerOpenCodeConfig({
|
|
|
9079
9115
|
log3(`Applied runner OpenCode config ${source} to ${join8(cwd, target)}`);
|
|
9080
9116
|
}
|
|
9081
9117
|
|
|
9118
|
+
// src/lib/credential-sync.ts
|
|
9119
|
+
import { renameSync, writeFileSync as writeFileSync5 } from "fs";
|
|
9120
|
+
var CREDENTIAL_SYNC_TICK_TIMEOUT_MS = 3e4;
|
|
9121
|
+
var CREDENTIAL_FLUSH_DEADLINE_MS = 8e3;
|
|
9122
|
+
var CREDENTIAL_FLUSH_ABORT_GRACE_MS = 500;
|
|
9123
|
+
var DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS = 60;
|
|
9124
|
+
var STORES = ["claude", "opencode"];
|
|
9125
|
+
var MAX_FLUSH_PASSES = 2;
|
|
9126
|
+
function outcomesWith(outcome) {
|
|
9127
|
+
return { claude: outcome, opencode: outcome };
|
|
9128
|
+
}
|
|
9129
|
+
function errorMessage(error2) {
|
|
9130
|
+
return error2 instanceof Error ? error2.message : String(error2);
|
|
9131
|
+
}
|
|
9132
|
+
function waitForSettlement(promise, timeoutMs) {
|
|
9133
|
+
return new Promise((resolve4) => {
|
|
9134
|
+
let settled = false;
|
|
9135
|
+
const timer = setTimeout(() => finish(false), Math.max(0, timeoutMs));
|
|
9136
|
+
const finish = (value) => {
|
|
9137
|
+
if (settled) return;
|
|
9138
|
+
settled = true;
|
|
9139
|
+
clearTimeout(timer);
|
|
9140
|
+
resolve4(value);
|
|
9141
|
+
};
|
|
9142
|
+
promise.then(
|
|
9143
|
+
() => finish(true),
|
|
9144
|
+
() => finish(true)
|
|
9145
|
+
);
|
|
9146
|
+
});
|
|
9147
|
+
}
|
|
9148
|
+
function writeMarker(markerPath, outcomes, log3) {
|
|
9149
|
+
const body = `${STORES.map((store) => `${store}=${outcomes[store]}`).join("\n")}
|
|
9150
|
+
`;
|
|
9151
|
+
const temporaryPath = `${markerPath}.tmp`;
|
|
9152
|
+
try {
|
|
9153
|
+
writeFileSync5(temporaryPath, body, { mode: 384 });
|
|
9154
|
+
renameSync(temporaryPath, markerPath);
|
|
9155
|
+
} catch (error2) {
|
|
9156
|
+
log3(`CREDENTIAL-FLUSH-MARKER-UNWRITABLE: ${markerPath}: ${errorMessage(error2)}`, "warn");
|
|
9157
|
+
}
|
|
9158
|
+
}
|
|
9159
|
+
function intervalSeconds(env, log3) {
|
|
9160
|
+
const raw = env.CREDS_SYNC_INTERVAL;
|
|
9161
|
+
if (raw === void 0 || /^[1-9][0-9]*$/.test(raw) && Number.isSafeInteger(Number(raw))) {
|
|
9162
|
+
return raw === void 0 ? DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS : Number(raw);
|
|
9163
|
+
}
|
|
9164
|
+
log3(
|
|
9165
|
+
`CREDENTIAL-SYNC-INTERVAL-INVALID: CREDS_SYNC_INTERVAL='${raw}' is not a positive integer; using ${DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS}s`,
|
|
9166
|
+
"warn"
|
|
9167
|
+
);
|
|
9168
|
+
return DEFAULT_CREDENTIAL_SYNC_INTERVAL_SECONDS;
|
|
9169
|
+
}
|
|
9170
|
+
async function runFlushStore(store, env, synchroniserRunner, log3, deadlineAt) {
|
|
9171
|
+
const remainingMs = deadlineAt - Date.now();
|
|
9172
|
+
if (remainingMs <= 0) return { outcome: "timeout", orphaned: false };
|
|
9173
|
+
const controller = new AbortController();
|
|
9174
|
+
let result;
|
|
9175
|
+
let failed = false;
|
|
9176
|
+
const completion = Promise.resolve().then(
|
|
9177
|
+
() => synchroniserRunner(["sync-once", store], {
|
|
9178
|
+
timeoutMs: remainingMs,
|
|
9179
|
+
env,
|
|
9180
|
+
signal: controller.signal
|
|
9181
|
+
})
|
|
9182
|
+
).then(
|
|
9183
|
+
(value) => {
|
|
9184
|
+
result = value;
|
|
9185
|
+
},
|
|
9186
|
+
(error2) => {
|
|
9187
|
+
failed = true;
|
|
9188
|
+
log3(`CREDENTIAL-FLUSH-ERROR: ${store}: ${errorMessage(error2)}`, "warn");
|
|
9189
|
+
}
|
|
9190
|
+
);
|
|
9191
|
+
const abortTimer = setTimeout(() => controller.abort(), remainingMs);
|
|
9192
|
+
const settledBeforeDeadline = await waitForSettlement(completion, remainingMs);
|
|
9193
|
+
clearTimeout(abortTimer);
|
|
9194
|
+
if (!settledBeforeDeadline) {
|
|
9195
|
+
controller.abort();
|
|
9196
|
+
const settledAfterAbort = await waitForSettlement(completion, CREDENTIAL_FLUSH_ABORT_GRACE_MS);
|
|
9197
|
+
if (!settledAfterAbort) return { outcome: "timeout", orphaned: true };
|
|
9198
|
+
return { outcome: "timeout", orphaned: false };
|
|
9199
|
+
}
|
|
9200
|
+
if (failed || !result) return { outcome: "failed", orphaned: false };
|
|
9201
|
+
if (result.timedOut || Date.now() >= deadlineAt) {
|
|
9202
|
+
return { outcome: "timeout", orphaned: false };
|
|
9203
|
+
}
|
|
9204
|
+
return { outcome: result.code === 0 ? "ok" : "failed", orphaned: false };
|
|
9205
|
+
}
|
|
9206
|
+
function createCredentialSync({
|
|
9207
|
+
markerPath,
|
|
9208
|
+
env,
|
|
9209
|
+
log: log3,
|
|
9210
|
+
synchroniserRunner = runSynchroniser
|
|
9211
|
+
}) {
|
|
9212
|
+
const persistenceDisabled = !env.PERSISTENCE_BUCKET;
|
|
9213
|
+
let disabled = persistenceDisabled;
|
|
9214
|
+
let armed = false;
|
|
9215
|
+
let stopped = false;
|
|
9216
|
+
let timer;
|
|
9217
|
+
let inFlight;
|
|
9218
|
+
let activeTickAbort;
|
|
9219
|
+
let lastTickFailed;
|
|
9220
|
+
let flushPromise;
|
|
9221
|
+
const scheduleTick = (intervalMs, startTick2) => {
|
|
9222
|
+
if (stopped) return;
|
|
9223
|
+
timer = setTimeout(() => {
|
|
9224
|
+
timer = void 0;
|
|
9225
|
+
startTick2();
|
|
9226
|
+
}, intervalMs);
|
|
9227
|
+
};
|
|
9228
|
+
const startTick = (intervalMs) => {
|
|
9229
|
+
if (stopped) return;
|
|
9230
|
+
const controller = new AbortController();
|
|
9231
|
+
activeTickAbort = controller;
|
|
9232
|
+
const tick = (async () => {
|
|
9233
|
+
const outcomes = {
|
|
9234
|
+
claude: "failed",
|
|
9235
|
+
opencode: "failed"
|
|
9236
|
+
};
|
|
9237
|
+
for (const store of STORES) {
|
|
9238
|
+
if (controller.signal.aborted) break;
|
|
9239
|
+
try {
|
|
9240
|
+
const result = await synchroniserRunner(["sync-once", store], {
|
|
9241
|
+
timeoutMs: CREDENTIAL_SYNC_TICK_TIMEOUT_MS,
|
|
9242
|
+
env,
|
|
9243
|
+
signal: controller.signal
|
|
9244
|
+
});
|
|
9245
|
+
outcomes[store] = !result.timedOut && result.code === 0 ? "ok" : "failed";
|
|
9246
|
+
} catch (error2) {
|
|
9247
|
+
outcomes[store] = "failed";
|
|
9248
|
+
log3(`CREDENTIAL-SYNC-TICK-ERROR: ${store}: ${errorMessage(error2)}`, "debug");
|
|
9249
|
+
}
|
|
9250
|
+
}
|
|
9251
|
+
const failed = STORES.some((store) => outcomes[store] === "failed");
|
|
9252
|
+
log3(
|
|
9253
|
+
`CREDENTIAL-SYNC-TICK: claude=${outcomes.claude}, opencode=${outcomes.opencode}`,
|
|
9254
|
+
"debug"
|
|
9255
|
+
);
|
|
9256
|
+
if (failed && lastTickFailed !== true) {
|
|
9257
|
+
log3(
|
|
9258
|
+
"CREDENTIAL-SYNC-FAILED: an interval credential sync failed; retrying on the next tick",
|
|
9259
|
+
"warn"
|
|
9260
|
+
);
|
|
9261
|
+
} else if (!failed && lastTickFailed === true) {
|
|
9262
|
+
log3("CREDENTIAL-SYNC-RECOVERED: interval credential sync succeeded again", "warn");
|
|
9263
|
+
}
|
|
9264
|
+
lastTickFailed = failed;
|
|
9265
|
+
})().finally(() => {
|
|
9266
|
+
if (activeTickAbort === controller) activeTickAbort = void 0;
|
|
9267
|
+
if (inFlight === tick) inFlight = void 0;
|
|
9268
|
+
scheduleTick(intervalMs, () => startTick(intervalMs));
|
|
9269
|
+
});
|
|
9270
|
+
inFlight = tick;
|
|
9271
|
+
};
|
|
9272
|
+
const performFlush = async () => {
|
|
9273
|
+
stopped = true;
|
|
9274
|
+
if (timer) {
|
|
9275
|
+
clearTimeout(timer);
|
|
9276
|
+
timer = void 0;
|
|
9277
|
+
}
|
|
9278
|
+
const deadlineAt = Date.now() + CREDENTIAL_FLUSH_DEADLINE_MS;
|
|
9279
|
+
if (inFlight) {
|
|
9280
|
+
const settled = await waitForSettlement(inFlight, CREDENTIAL_FLUSH_DEADLINE_MS);
|
|
9281
|
+
if (!settled) {
|
|
9282
|
+
activeTickAbort?.abort();
|
|
9283
|
+
const settledAfterAbort = await waitForSettlement(
|
|
9284
|
+
inFlight,
|
|
9285
|
+
CREDENTIAL_FLUSH_ABORT_GRACE_MS
|
|
9286
|
+
);
|
|
9287
|
+
if (!settledAfterAbort) {
|
|
9288
|
+
log3(
|
|
9289
|
+
"CREDENTIAL-FLUSH-ORPHANED-TICK: a sync-once child could not be proven settled; skipping the boundary flush rather than becoming a second writer",
|
|
9290
|
+
"warn"
|
|
9291
|
+
);
|
|
9292
|
+
return { outcomes: outcomesWith("timeout"), orphaned: true };
|
|
9293
|
+
}
|
|
9294
|
+
}
|
|
9295
|
+
}
|
|
9296
|
+
if (disabled) return { outcomes: outcomesWith("skipped"), orphaned: false };
|
|
9297
|
+
const outcomes = outcomesWith("timeout");
|
|
9298
|
+
for (const store of STORES) {
|
|
9299
|
+
const result = await runFlushStore(store, env, synchroniserRunner, log3, deadlineAt);
|
|
9300
|
+
if (result.orphaned) {
|
|
9301
|
+
log3(
|
|
9302
|
+
"CREDENTIAL-FLUSH-ORPHANED-CHILD: a flush sync-once child could not be proven settled; refusing another flush pass rather than becoming a second writer",
|
|
9303
|
+
"warn"
|
|
9304
|
+
);
|
|
9305
|
+
return { outcomes: outcomesWith("timeout"), orphaned: true };
|
|
9306
|
+
}
|
|
9307
|
+
outcomes[store] = result.outcome;
|
|
9308
|
+
}
|
|
9309
|
+
return { outcomes, orphaned: false };
|
|
9310
|
+
};
|
|
9311
|
+
let flushPasses = 0;
|
|
9312
|
+
let lastFlush;
|
|
9313
|
+
return {
|
|
9314
|
+
arm() {
|
|
9315
|
+
if (stopped || armed) return;
|
|
9316
|
+
armed = true;
|
|
9317
|
+
if (persistenceDisabled) {
|
|
9318
|
+
disabled = true;
|
|
9319
|
+
log3(
|
|
9320
|
+
"CREDENTIAL-SYNC-DISABLED: LITESTREAM_BUCKET/LITESTREAM_PREFIX are not both set; no interval credential sync this boot",
|
|
9321
|
+
"warn"
|
|
9322
|
+
);
|
|
9323
|
+
return;
|
|
9324
|
+
}
|
|
9325
|
+
disabled = false;
|
|
9326
|
+
const intervalMs = intervalSeconds(env, log3) * 1e3;
|
|
9327
|
+
scheduleTick(intervalMs, () => startTick(intervalMs));
|
|
9328
|
+
},
|
|
9329
|
+
async stopAndFlush(publish) {
|
|
9330
|
+
let result;
|
|
9331
|
+
const runningFlush = flushPromise;
|
|
9332
|
+
if (runningFlush) {
|
|
9333
|
+
result = await runningFlush;
|
|
9334
|
+
} else if (flushPasses >= MAX_FLUSH_PASSES || lastFlush?.orphaned) {
|
|
9335
|
+
result = lastFlush ?? { outcomes: outcomesWith("timeout"), orphaned: true };
|
|
9336
|
+
} else {
|
|
9337
|
+
flushPasses++;
|
|
9338
|
+
const currentFlush = performFlush();
|
|
9339
|
+
flushPromise = currentFlush;
|
|
9340
|
+
try {
|
|
9341
|
+
result = await currentFlush;
|
|
9342
|
+
lastFlush = result;
|
|
9343
|
+
} finally {
|
|
9344
|
+
if (flushPromise === currentFlush) flushPromise = void 0;
|
|
9345
|
+
}
|
|
9346
|
+
}
|
|
9347
|
+
if (publish) writeMarker(markerPath, result.outcomes, log3);
|
|
9348
|
+
return result.outcomes;
|
|
9349
|
+
}
|
|
9350
|
+
};
|
|
9351
|
+
}
|
|
9352
|
+
|
|
9082
9353
|
// src/commands/run.ts
|
|
9083
9354
|
var MAX_ACTIVITY_LOG_ENTRIES = 10;
|
|
9084
9355
|
var CHANNEL_POLL_INTERVAL_MS = Number(process.env.EVIDENT_CHANNEL_POLL_INTERVAL_MS) || 2e3;
|
|
@@ -9376,6 +9647,10 @@ async function driveChannels(state, driver) {
|
|
|
9376
9647
|
consecutiveDrainFailures = 0;
|
|
9377
9648
|
unreachableMs = 0;
|
|
9378
9649
|
state.messageCount += processed;
|
|
9650
|
+
if (driver.recycleRequested) {
|
|
9651
|
+
await beginGracefulShutdown(state, "recycle");
|
|
9652
|
+
return;
|
|
9653
|
+
}
|
|
9379
9654
|
const proxiedActivity = state.lastProxiedActivityAt !== lastSeenProxiedActivityAt;
|
|
9380
9655
|
lastSeenProxiedActivityAt = state.lastProxiedActivityAt;
|
|
9381
9656
|
const fileActivitySnapshot = driver.fileSyncActivity();
|
|
@@ -9418,8 +9693,8 @@ async function driveChannels(state, driver) {
|
|
|
9418
9693
|
state.running = false;
|
|
9419
9694
|
break;
|
|
9420
9695
|
}
|
|
9421
|
-
const
|
|
9422
|
-
logActivity(state, { type: "error", error: `Channel processing error: ${
|
|
9696
|
+
const errorMessage2 = error2 instanceof Error ? error2.message : String(error2);
|
|
9697
|
+
logActivity(state, { type: "error", error: `Channel processing error: ${errorMessage2}` });
|
|
9423
9698
|
if (state.interactive) displayStatus(state);
|
|
9424
9699
|
if (driver.hasInFlightWatchers()) {
|
|
9425
9700
|
consecutiveDrainFailures = 0;
|
|
@@ -9889,21 +10164,39 @@ async function cleanup(state, opts = {}) {
|
|
|
9889
10164
|
clearTimeout(state.resourceUsageTimer);
|
|
9890
10165
|
state.resourceUsageTimer = null;
|
|
9891
10166
|
}
|
|
10167
|
+
const credentialSync = state.credentialSync;
|
|
10168
|
+
const flushCredentials = credentialSync ? async (phase, publish) => {
|
|
10169
|
+
await timeShutdownPhase(state, durations, phase, async () => {
|
|
10170
|
+
const outcomes = await credentialSync.stopAndFlush(publish);
|
|
10171
|
+
const level = Object.values(outcomes).every((outcome) => outcome === "ok") ? "info" : "warn";
|
|
10172
|
+
log2(
|
|
10173
|
+
state,
|
|
10174
|
+
`Credential flush: claude=${outcomes.claude}, opencode=${outcomes.opencode}`,
|
|
10175
|
+
level
|
|
10176
|
+
);
|
|
10177
|
+
});
|
|
10178
|
+
} : void 0;
|
|
10179
|
+
let drainSettled = true;
|
|
9892
10180
|
if (opts.graceful && state.channelDriver) {
|
|
9893
10181
|
state.channelDriver.stop();
|
|
10182
|
+
}
|
|
10183
|
+
if (flushCredentials) {
|
|
10184
|
+
await flushCredentials("credential_flush", !(opts.graceful && state.channelDriver));
|
|
10185
|
+
}
|
|
10186
|
+
if (opts.graceful && state.channelDriver) {
|
|
9894
10187
|
log2(state, "Draining in-flight channel work before shutdown...");
|
|
9895
10188
|
if (state.interactive) {
|
|
9896
10189
|
logActivity(state, { type: "info", message: "Draining in-flight work before shutdown..." });
|
|
9897
10190
|
displayStatus(state);
|
|
9898
10191
|
}
|
|
9899
10192
|
const driver = state.channelDriver;
|
|
9900
|
-
|
|
10193
|
+
drainSettled = await timeShutdownPhase(
|
|
9901
10194
|
state,
|
|
9902
10195
|
durations,
|
|
9903
10196
|
"drain",
|
|
9904
10197
|
() => driver.waitForInFlight(SHUTDOWN_DRAIN_TIMEOUT_MS)
|
|
9905
10198
|
);
|
|
9906
|
-
if (!
|
|
10199
|
+
if (!drainSettled) {
|
|
9907
10200
|
logActivity(state, {
|
|
9908
10201
|
type: "info",
|
|
9909
10202
|
message: "Shutdown drain timed out with work still in flight \u2014 leaving it for restart recovery"
|
|
@@ -9911,6 +10204,9 @@ async function cleanup(state, opts = {}) {
|
|
|
9911
10204
|
if (state.interactive) displayStatus(state);
|
|
9912
10205
|
}
|
|
9913
10206
|
}
|
|
10207
|
+
if (opts.graceful && state.channelDriver && flushCredentials && drainSettled) {
|
|
10208
|
+
await flushCredentials("credential_flush_final", true);
|
|
10209
|
+
}
|
|
9914
10210
|
await timeShutdownPhase(state, durations, "offline_notify", () => notifyOffline(state));
|
|
9915
10211
|
if (state.connection) {
|
|
9916
10212
|
const connection = state.connection;
|
|
@@ -9946,6 +10242,44 @@ async function cleanup(state, opts = {}) {
|
|
|
9946
10242
|
}
|
|
9947
10243
|
return durations;
|
|
9948
10244
|
}
|
|
10245
|
+
async function beginGracefulShutdown(state, trigger) {
|
|
10246
|
+
if (state.shuttingDown) return;
|
|
10247
|
+
state.shuttingDown = true;
|
|
10248
|
+
const shutdownStartedAt = Date.now();
|
|
10249
|
+
const shutdownMessage = trigger === "recycle" ? "Recycle requested \u2014 finishing in-flight work, then exiting" : "Shutting down...";
|
|
10250
|
+
if (state.interactive) {
|
|
10251
|
+
logActivity(state, { type: "info", message: shutdownMessage });
|
|
10252
|
+
displayStatus(state);
|
|
10253
|
+
} else {
|
|
10254
|
+
log2(state, shutdownMessage);
|
|
10255
|
+
}
|
|
10256
|
+
const durations = await cleanup(state, { graceful: true });
|
|
10257
|
+
const telemetryBudgetMs = Number(process.env.EVIDENT_TELEMETRY_SHUTDOWN_MS) || TELEMETRY_SHUTDOWN_TIMEOUT_MS;
|
|
10258
|
+
await timeShutdownPhase(state, durations, "telemetry_flush", async () => {
|
|
10259
|
+
let timer;
|
|
10260
|
+
const flushed = shutdownTelemetry().then(
|
|
10261
|
+
() => true,
|
|
10262
|
+
(error2) => {
|
|
10263
|
+
log2(
|
|
10264
|
+
state,
|
|
10265
|
+
`Telemetry flush failed during shutdown: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
10266
|
+
"warn"
|
|
10267
|
+
);
|
|
10268
|
+
return true;
|
|
10269
|
+
}
|
|
10270
|
+
);
|
|
10271
|
+
const timedOut = new Promise((resolve4) => {
|
|
10272
|
+
timer = setTimeout(() => resolve4(false), telemetryBudgetMs);
|
|
10273
|
+
});
|
|
10274
|
+
if (!await Promise.race([flushed, timedOut])) {
|
|
10275
|
+
log2(state, `Telemetry flush exceeded ${telemetryBudgetMs}ms \u2014 exiting anyway`, "warn");
|
|
10276
|
+
}
|
|
10277
|
+
clearTimeout(timer);
|
|
10278
|
+
});
|
|
10279
|
+
const breakdown = Object.entries(durations).map(([phase, ms]) => `${phase}=${ms}ms`).join(" ");
|
|
10280
|
+
log2(state, `Shutdown complete in ${Date.now() - shutdownStartedAt}ms (${breakdown})`);
|
|
10281
|
+
process.exit(0);
|
|
10282
|
+
}
|
|
9949
10283
|
async function run(options) {
|
|
9950
10284
|
const interactive = isInteractive(options.json);
|
|
9951
10285
|
let logLevel;
|
|
@@ -9997,9 +10331,23 @@ async function run(options) {
|
|
|
9997
10331
|
openaiUsageTimer: null,
|
|
9998
10332
|
openaiUsageRearm: null,
|
|
9999
10333
|
resourceUsageTimer: null,
|
|
10334
|
+
credentialSync: null,
|
|
10000
10335
|
authHeader: ""
|
|
10001
10336
|
};
|
|
10002
10337
|
setTelemetryAuthProvider(() => ({ authHeader: state.authHeader, agentId: state.agentId }));
|
|
10338
|
+
if (options.credentialSyncMarker) {
|
|
10339
|
+
state.credentialSync = createCredentialSync({
|
|
10340
|
+
markerPath: options.credentialSyncMarker,
|
|
10341
|
+
env: process.env,
|
|
10342
|
+
log: (message, level = "info") => {
|
|
10343
|
+
if (level === "error") {
|
|
10344
|
+
logActivity(state, { type: "error", error: message });
|
|
10345
|
+
} else {
|
|
10346
|
+
logActivity(state, { type: "info", level, message });
|
|
10347
|
+
}
|
|
10348
|
+
}
|
|
10349
|
+
});
|
|
10350
|
+
}
|
|
10003
10351
|
if (fileSyncDirectories.length > 0) {
|
|
10004
10352
|
log2(state, `File sync enabled for: ${fileSyncDirectories.join(", ")}`);
|
|
10005
10353
|
} else {
|
|
@@ -10025,43 +10373,7 @@ async function run(options) {
|
|
|
10025
10373
|
"warn"
|
|
10026
10374
|
);
|
|
10027
10375
|
}
|
|
10028
|
-
const handleSignal =
|
|
10029
|
-
if (state.shuttingDown) return;
|
|
10030
|
-
state.shuttingDown = true;
|
|
10031
|
-
const shutdownStartedAt = Date.now();
|
|
10032
|
-
if (state.interactive) {
|
|
10033
|
-
logActivity(state, { type: "info", message: "Shutting down..." });
|
|
10034
|
-
displayStatus(state);
|
|
10035
|
-
} else {
|
|
10036
|
-
log2(state, "Shutting down...");
|
|
10037
|
-
}
|
|
10038
|
-
const durations = await cleanup(state, { graceful: true });
|
|
10039
|
-
const telemetryBudgetMs = Number(process.env.EVIDENT_TELEMETRY_SHUTDOWN_MS) || TELEMETRY_SHUTDOWN_TIMEOUT_MS;
|
|
10040
|
-
await timeShutdownPhase(state, durations, "telemetry_flush", async () => {
|
|
10041
|
-
let timer;
|
|
10042
|
-
const flushed = shutdownTelemetry().then(
|
|
10043
|
-
() => true,
|
|
10044
|
-
(error2) => {
|
|
10045
|
-
log2(
|
|
10046
|
-
state,
|
|
10047
|
-
`Telemetry flush failed during shutdown: ${error2 instanceof Error ? error2.message : String(error2)}`,
|
|
10048
|
-
"warn"
|
|
10049
|
-
);
|
|
10050
|
-
return true;
|
|
10051
|
-
}
|
|
10052
|
-
);
|
|
10053
|
-
const timedOut = new Promise((resolve4) => {
|
|
10054
|
-
timer = setTimeout(() => resolve4(false), telemetryBudgetMs);
|
|
10055
|
-
});
|
|
10056
|
-
if (!await Promise.race([flushed, timedOut])) {
|
|
10057
|
-
log2(state, `Telemetry flush exceeded ${telemetryBudgetMs}ms \u2014 exiting anyway`, "warn");
|
|
10058
|
-
}
|
|
10059
|
-
clearTimeout(timer);
|
|
10060
|
-
});
|
|
10061
|
-
const breakdown = Object.entries(durations).map(([phase, ms]) => `${phase}=${ms}ms`).join(" ");
|
|
10062
|
-
log2(state, `Shutdown complete in ${Date.now() - shutdownStartedAt}ms (${breakdown})`);
|
|
10063
|
-
process.exit(0);
|
|
10064
|
-
};
|
|
10376
|
+
const handleSignal = () => beginGracefulShutdown(state, "signal");
|
|
10065
10377
|
process.on("SIGINT", handleSignal);
|
|
10066
10378
|
process.on("SIGTERM", handleSignal);
|
|
10067
10379
|
try {
|
|
@@ -10207,6 +10519,7 @@ async function run(options) {
|
|
|
10207
10519
|
await restoreCredentialStores(credentialContext);
|
|
10208
10520
|
if (githubTokenPopulated) await configureGitHubAccess(credentialContext);
|
|
10209
10521
|
}
|
|
10522
|
+
state.credentialSync?.arm();
|
|
10210
10523
|
let sessionDbVerifyFatal = false;
|
|
10211
10524
|
if (!options.restoreSessionDb) {
|
|
10212
10525
|
log2(state, "Skipping session-DB restore: --restore-session-db was not passed", "debug");
|
|
@@ -10276,7 +10589,7 @@ async function run(options) {
|
|
|
10276
10589
|
state.opencodeVersion = oc.version;
|
|
10277
10590
|
if (options.opencodePidFile && oc.process?.pid !== void 0) {
|
|
10278
10591
|
try {
|
|
10279
|
-
|
|
10592
|
+
writeFileSync6(options.opencodePidFile, `${oc.process.pid}
|
|
10280
10593
|
`, { mode: 384 });
|
|
10281
10594
|
chmodSync3(options.opencodePidFile, 384);
|
|
10282
10595
|
} catch (error2) {
|
|
@@ -10392,7 +10705,7 @@ async function run(options) {
|
|
|
10392
10705
|
});
|
|
10393
10706
|
try {
|
|
10394
10707
|
if (litestreamProcess.pid !== void 0) {
|
|
10395
|
-
|
|
10708
|
+
writeFileSync6(options.litestreamPidFile, `${litestreamProcess.pid}
|
|
10396
10709
|
`, {
|
|
10397
10710
|
mode: 384
|
|
10398
10711
|
});
|
|
@@ -10722,6 +11035,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
10722
11035
|
).option(
|
|
10723
11036
|
"--opencode-config-overlay <path>",
|
|
10724
11037
|
"Apply this runner-provided OpenCode config before starting OpenCode."
|
|
11038
|
+
).option(
|
|
11039
|
+
"--credential-sync-marker <path>",
|
|
11040
|
+
"Own the interval credential sync and write this marker once the shutdown flush has finished, so the runner image's lifecycle hooks can wait on it."
|
|
10725
11041
|
).action(
|
|
10726
11042
|
(options) => {
|
|
10727
11043
|
run({
|
|
@@ -10760,7 +11076,8 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
10760
11076
|
sessionDbNoReplicateMarker: options.sessionDbNoReplicateMarker,
|
|
10761
11077
|
restoreSessionDb: options.restoreSessionDb,
|
|
10762
11078
|
restoreRunnerCredentials: options.restoreRunnerCredentials,
|
|
10763
|
-
opencodeConfigOverlay: options.opencodeConfigOverlay
|
|
11079
|
+
opencodeConfigOverlay: options.opencodeConfigOverlay,
|
|
11080
|
+
credentialSyncMarker: options.credentialSyncMarker
|
|
10764
11081
|
});
|
|
10765
11082
|
}
|
|
10766
11083
|
);
|