open-walnut 0.4.0 → 0.4.1
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 +13 -1
- package/bin/open-walnut.js +14 -0
- package/dist/cli.js +582 -102
- package/dist/daemon-binaries/acp-worker.js.version +1 -1
- package/dist/web/server.js +555 -102
- package/dist/web/static/assets/{OfficePreview-DUTDx4ef.js → OfficePreview-C8OMbtiq.js} +2 -2
- package/dist/web/static/assets/{docx-preview-Qj-aUywE.js → docx-preview-hDgtrOpv.js} +1 -1
- package/dist/web/static/assets/{index-D6haQ3tX.js → index-B2-N8jTH.js} +1 -1
- package/dist/web/static/assets/{index-sF1ObMS0.js → index-B35fidy6.js} +1 -1
- package/dist/web/static/assets/{index-BvxwKQUT.js → index-BAb6UVet.js} +1 -1
- package/dist/web/static/assets/{index-BmDSVLwR.js → index-C5-vT4kT.js} +1 -1
- package/dist/web/static/assets/{index-fk2aCKPB.js → index-CBHRNnCk.js} +1 -1
- package/dist/web/static/assets/{index-CEVKci-j.js → index-CPHXL-wy.js} +1 -1
- package/dist/web/static/assets/{index-xu9Gy-xP.js → index-CWIydpPr.js} +1 -1
- package/dist/web/static/assets/{index-CfC-vHYG.js → index-Caj09uf7.js} +1 -1
- package/dist/web/static/assets/{index-oIyzyrLY.js → index-D1uUk0AQ.js} +273 -273
- package/dist/web/static/assets/{index-CzP6rbk7.js → index-D81YKipC.js} +1 -1
- package/dist/web/static/assets/{index-Bke7ZOIJ.js → index-DtRt21Zi.js} +1 -1
- package/dist/web/static/assets/{index-CSssA5eI.js → index-EowLf_Xf.js} +1 -1
- package/dist/web/static/assets/{index-y7f5PVUr.js → index-KY36bfpA.js} +1 -1
- package/dist/web/static/assets/{index-CeR0xElr.js → index-VcSnwuHG.js} +1 -1
- package/dist/web/static/assets/{index-CCa56u0e.js → index-bfZj1PAc.js} +1 -1
- package/dist/web/static/assets/{index-DqY9Z9j5.js → index-gv-TJwHT.js} +1 -1
- package/dist/web/static/assets/{index-Dv42N8tm.js → index-nRBC-NJN.js} +1 -1
- package/dist/web/static/assets/{index-BV9e5-KR.js → index-ph8Hxff-.js} +1 -1
- package/dist/web/static/assets/{jszip.min-C3K7iJBk.js → jszip.min-DyrlNEg4.js} +1 -1
- package/dist/web/static/assets/{pptx-preview.es-UhsuLLNP.js → pptx-preview.es-DjND6y6O.js} +1 -1
- package/dist/web/static/index.html +1 -1
- package/package.json +5 -4
package/dist/cli.js
CHANGED
|
@@ -7366,6 +7366,23 @@ function findRepoRoot() {
|
|
|
7366
7366
|
}
|
|
7367
7367
|
return null;
|
|
7368
7368
|
}
|
|
7369
|
+
function bunAvailable() {
|
|
7370
|
+
const home = process.env.HOME ?? "";
|
|
7371
|
+
const candidates = [
|
|
7372
|
+
...(process.env.PATH ?? "").split(path14.delimiter).filter(Boolean).map((d8) => path14.join(d8, "bun")),
|
|
7373
|
+
path14.join(home, ".bun", "bin", "bun"),
|
|
7374
|
+
"/opt/homebrew/bin/bun",
|
|
7375
|
+
"/usr/local/bin/bun"
|
|
7376
|
+
];
|
|
7377
|
+
return candidates.some((p6) => {
|
|
7378
|
+
try {
|
|
7379
|
+
fs12.accessSync(p6, fs12.constants.X_OK);
|
|
7380
|
+
return true;
|
|
7381
|
+
} catch {
|
|
7382
|
+
return false;
|
|
7383
|
+
}
|
|
7384
|
+
});
|
|
7385
|
+
}
|
|
7369
7386
|
function readBuiltVersions() {
|
|
7370
7387
|
const out = /* @__PURE__ */ new Map();
|
|
7371
7388
|
try {
|
|
@@ -7389,6 +7406,10 @@ function verifyDaemonBinaryVersion() {
|
|
|
7389
7406
|
return true;
|
|
7390
7407
|
}
|
|
7391
7408
|
const built = readBuiltVersions();
|
|
7409
|
+
if (built.size === 0 && !bunAvailable()) {
|
|
7410
|
+
log.session.info("no daemon binaries and no Bun on this machine; sessions use the source daemon", { expected });
|
|
7411
|
+
return true;
|
|
7412
|
+
}
|
|
7392
7413
|
if (built.size === 0) {
|
|
7393
7414
|
return handleMismatch({
|
|
7394
7415
|
reason: "no daemon binaries built",
|
|
@@ -7447,6 +7468,12 @@ function handleMismatch(ctx) {
|
|
|
7447
7468
|
return false;
|
|
7448
7469
|
}
|
|
7449
7470
|
const builtAfter = readBuiltVersions();
|
|
7471
|
+
if (builtAfter.size === 0) {
|
|
7472
|
+
log.session.warn("daemon rebuild produced no binaries (no Bun?); continuing with the source daemon", { expected: ctx.expected });
|
|
7473
|
+
console.error(` Daemon binaries were not built (Bun is not installed). Sessions use the source daemon. Continuing startup.
|
|
7474
|
+
`);
|
|
7475
|
+
return true;
|
|
7476
|
+
}
|
|
7450
7477
|
const stillOff = [...builtAfter].filter(([, v3]) => v3 !== ctx.expected);
|
|
7451
7478
|
if (stillOff.length > 0) {
|
|
7452
7479
|
log.session.error(
|
|
@@ -8788,7 +8815,25 @@ function reapSession(sid, code, reason) {
|
|
|
8788
8815
|
let jsonlAgeMs = null;
|
|
8789
8816
|
try { jsonlAgeMs = Date.now() - fs.statSync(session.jsonlPath).mtimeMs; } catch {}
|
|
8790
8817
|
const cleanExit = isTurnCompleteExit(session.jsonlPath);
|
|
8791
|
-
|
|
8818
|
+
// An INTENTIONAL daemon-initiated reclamation is not a failure, and the JSONL
|
|
8819
|
+
// tail cannot tell us it happened. The idle scanner stamps idleReclaimAt
|
|
8820
|
+
// before it signals; the death then lands here through whichever path notices
|
|
8821
|
+
// it (usually orphan-poll ESRCH \u2192 code -1). Verified production case: the last
|
|
8822
|
+
// line was a control_response (the reply to a Walnut-issued control request \u2014
|
|
8823
|
+
// a routine shape), so isTurnCompleteExit said false, exitCode stayed -1, and
|
|
8824
|
+
// the projection painted a red "Error" over a healthy, fully resumable session
|
|
8825
|
+
// we reclaimed on purpose. Checked FIRST and unconditionally: the flag carries
|
|
8826
|
+
// information the tail does not contain, which is why isTurnCompleteExit is
|
|
8827
|
+
// left alone. Keep in sync with daemon-core.ts reapSession.
|
|
8828
|
+
const intentionalReclaim = session.idleReclaimAt != null;
|
|
8829
|
+
if (intentionalReclaim && code !== 0) {
|
|
8830
|
+
logMsg('info', 'reapSession: intentional idle reclamation, normalizing exit code', {
|
|
8831
|
+
sid, pid: session.pid, originalCode: code, originalReason: reason,
|
|
8832
|
+
jsonlAgeMs, cleanExit, idleReclaimAt: session.idleReclaimAt,
|
|
8833
|
+
});
|
|
8834
|
+
code = 0;
|
|
8835
|
+
reason = reason + '+intentional-idle-reclaim';
|
|
8836
|
+
} else if (code !== 0 && cleanExit) {
|
|
8792
8837
|
logMsg('info', 'reapSession: turn-complete detected, normalizing exit code', {
|
|
8793
8838
|
sid, pid: session.pid, originalCode: code, originalReason: reason, jsonlAgeMs,
|
|
8794
8839
|
});
|
|
@@ -14553,6 +14598,14 @@ function scanIdleSessions() {
|
|
|
14553
14598
|
}
|
|
14554
14599
|
const idleMinutes = Math.round(idleMs / 60000);
|
|
14555
14600
|
logMsg('warn', 'idle scan: killing idle session (no subscribers, no output)', { sid, pid, idleMinutes, protectedBy: prot.source, thresholdMs: prot.killMs, detail: prot.detail });
|
|
14601
|
+
// Record the intent BEFORE signalling: the reap arrives asynchronously
|
|
14602
|
+
// (the orphan poll sees ESRCH and reports 'orphan-poll-dead', code -1), so
|
|
14603
|
+
// by then nothing remembers that WE asked for this. Without the stamp
|
|
14604
|
+
// reapSession can only consult the JSONL tail, which says "not a clean turn
|
|
14605
|
+
// end" for any session whose last line isn't a type:result \u2014 and the
|
|
14606
|
+
// projection then shows a red Error for our own routine reclamation.
|
|
14607
|
+
// Keep in sync with daemon-standalone.ts scanIdleSessions.
|
|
14608
|
+
session.idleReclaimAt = now;
|
|
14556
14609
|
killSessionProcessGroup(pid, sid);
|
|
14557
14610
|
}
|
|
14558
14611
|
}
|
|
@@ -15236,7 +15289,20 @@ function createDaemonCore(deps) {
|
|
|
15236
15289
|
jsonlAgeMs = clock() - fs121.statSync(session2.jsonlPath).mtimeMs;
|
|
15237
15290
|
} catch {
|
|
15238
15291
|
}
|
|
15239
|
-
|
|
15292
|
+
const intentionalReclaim = session2.idleReclaimAt != null;
|
|
15293
|
+
if (intentionalReclaim && code !== 0) {
|
|
15294
|
+
logger2("info", "reapSession: intentional idle reclamation, normalizing exit code", {
|
|
15295
|
+
sid,
|
|
15296
|
+
pid: session2.pid,
|
|
15297
|
+
originalCode: code,
|
|
15298
|
+
originalReason: reason,
|
|
15299
|
+
jsonlAgeMs,
|
|
15300
|
+
cleanExit,
|
|
15301
|
+
idleReclaimAt: session2.idleReclaimAt
|
|
15302
|
+
});
|
|
15303
|
+
code = 0;
|
|
15304
|
+
reason = reason + "+intentional-idle-reclaim";
|
|
15305
|
+
} else if (cleanExit && code !== 0) {
|
|
15240
15306
|
logger2("info", "reapSession: turn-complete detected, normalizing exit code", {
|
|
15241
15307
|
sid,
|
|
15242
15308
|
pid: session2.pid,
|
|
@@ -16707,6 +16773,22 @@ var init_local_daemon = __esm({
|
|
|
16707
16773
|
});
|
|
16708
16774
|
|
|
16709
16775
|
// src/core/session-error-kind.ts
|
|
16776
|
+
var session_error_kind_exports = {};
|
|
16777
|
+
__export(session_error_kind_exports, {
|
|
16778
|
+
INFRA_STATUS_REASONS: () => INFRA_STATUS_REASONS,
|
|
16779
|
+
RESCUABLE_STOPPED_WINDOW_MS: () => RESCUABLE_STOPPED_WINDOW_MS,
|
|
16780
|
+
TERMINAL_STATUS_REASONS: () => TERMINAL_STATUS_REASONS,
|
|
16781
|
+
assertsHostUnreachable: () => assertsHostUnreachable,
|
|
16782
|
+
classifySessionError: () => classifySessionError,
|
|
16783
|
+
classifyStatusReasonKind: () => classifyStatusReasonKind,
|
|
16784
|
+
isInfraSessionError: () => isInfraSessionError,
|
|
16785
|
+
isRecoverableSessionError: () => isRecoverableSessionError,
|
|
16786
|
+
isRescuableStoppedRecord: () => isRescuableStoppedRecord
|
|
16787
|
+
});
|
|
16788
|
+
function assertsHostUnreachable(record) {
|
|
16789
|
+
if (!record.errorMessage) return false;
|
|
16790
|
+
return typeof record.status_reason === "string" && REACHABILITY_CLAIM_REASONS.has(record.status_reason);
|
|
16791
|
+
}
|
|
16710
16792
|
function classifyStatusReasonKind(reason) {
|
|
16711
16793
|
if (typeof reason !== "string") return void 0;
|
|
16712
16794
|
if (INFRA_STATUS_REASONS.has(reason)) return "infra";
|
|
@@ -16735,7 +16817,7 @@ function isRescuableStoppedRecord(record, nowMs = Date.now()) {
|
|
|
16735
16817
|
if (!Number.isFinite(changedAt)) return false;
|
|
16736
16818
|
return nowMs - changedAt <= RESCUABLE_STOPPED_WINDOW_MS;
|
|
16737
16819
|
}
|
|
16738
|
-
var INFRA_STATUS_REASONS, TERMINAL_STATUS_REASONS, INFRA_TEXT_SIGNATURES, RESCUABLE_STOPPED_WINDOW_MS;
|
|
16820
|
+
var INFRA_STATUS_REASONS, TERMINAL_STATUS_REASONS, INFRA_TEXT_SIGNATURES, REACHABILITY_CLAIM_REASONS, RESCUABLE_STOPPED_WINDOW_MS;
|
|
16739
16821
|
var init_session_error_kind = __esm({
|
|
16740
16822
|
"src/core/session-error-kind.ts"() {
|
|
16741
16823
|
"use strict";
|
|
@@ -16779,6 +16861,9 @@ var init_session_error_kind = __esm({
|
|
|
16779
16861
|
"ssh tunnel",
|
|
16780
16862
|
"broken pipe"
|
|
16781
16863
|
];
|
|
16864
|
+
REACHABILITY_CLAIM_REASONS = /* @__PURE__ */ new Set([
|
|
16865
|
+
"remote_unreachable"
|
|
16866
|
+
]);
|
|
16782
16867
|
RESCUABLE_STOPPED_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
16783
16868
|
}
|
|
16784
16869
|
});
|
|
@@ -67231,8 +67316,22 @@ var init_token_truth = __esm({
|
|
|
67231
67316
|
});
|
|
67232
67317
|
|
|
67233
67318
|
// src/utils/image-compress.ts
|
|
67234
|
-
|
|
67319
|
+
async function loadSharp() {
|
|
67320
|
+
sharpLoad ??= import("sharp").then(
|
|
67321
|
+
(m6) => m6.default ?? m6,
|
|
67322
|
+
(err2) => {
|
|
67323
|
+
log.agent.warn("sharp is not installed; images are sent uncompressed", {
|
|
67324
|
+
error: err2 instanceof Error ? err2.message : String(err2),
|
|
67325
|
+
fix: "a machine with glibc 2.28+ installs sharp prebuilt; otherwise install libvips and re-run npm install"
|
|
67326
|
+
});
|
|
67327
|
+
return null;
|
|
67328
|
+
}
|
|
67329
|
+
);
|
|
67330
|
+
return sharpLoad;
|
|
67331
|
+
}
|
|
67235
67332
|
async function clampImageDimensions(buffer2, mimeType) {
|
|
67333
|
+
const sharp = await loadSharp();
|
|
67334
|
+
if (!sharp) return { buffer: buffer2, mimeType };
|
|
67236
67335
|
try {
|
|
67237
67336
|
const meta = await sharp(buffer2).metadata();
|
|
67238
67337
|
const longest = Math.max(meta.width ?? 0, meta.height ?? 0);
|
|
@@ -67254,6 +67353,8 @@ async function compressForApi(inputBuffer, inputMimeType) {
|
|
|
67254
67353
|
if (buffer2.toString("base64").length <= MAX_BASE64_BYTES) {
|
|
67255
67354
|
return { buffer: buffer2, mimeType };
|
|
67256
67355
|
}
|
|
67356
|
+
const sharp = await loadSharp();
|
|
67357
|
+
if (!sharp) return { buffer: buffer2, mimeType };
|
|
67257
67358
|
try {
|
|
67258
67359
|
if (mimeType === "image/gif") {
|
|
67259
67360
|
const webp = await sharp(buffer2, { animated: true }).webp({ quality: 80 }).toBuffer();
|
|
@@ -67291,10 +67392,11 @@ async function compressForApi(inputBuffer, inputMimeType) {
|
|
|
67291
67392
|
return { buffer: buffer2, mimeType };
|
|
67292
67393
|
}
|
|
67293
67394
|
}
|
|
67294
|
-
var MAX_BASE64_BYTES, MAX_IMAGE_DIMENSION;
|
|
67395
|
+
var sharpLoad, MAX_BASE64_BYTES, MAX_IMAGE_DIMENSION;
|
|
67295
67396
|
var init_image_compress = __esm({
|
|
67296
67397
|
"src/utils/image-compress.ts"() {
|
|
67297
67398
|
"use strict";
|
|
67399
|
+
init_logging();
|
|
67298
67400
|
MAX_BASE64_BYTES = 5e6;
|
|
67299
67401
|
MAX_IMAGE_DIMENSION = 1568;
|
|
67300
67402
|
}
|
|
@@ -70285,9 +70387,20 @@ async function applySnapshot(sessionId, snapshot2, source) {
|
|
|
70285
70387
|
updates.errorKind = void 0;
|
|
70286
70388
|
clearSuppressedErrorReason(sessionId);
|
|
70287
70389
|
} else {
|
|
70390
|
+
let droppedDisproven = false;
|
|
70391
|
+
if (assertsHostUnreachable(record)) {
|
|
70392
|
+
updates.errorMessage = void 0;
|
|
70393
|
+
droppedDisproven = true;
|
|
70394
|
+
log.session.info("snapshot projection dropped disproven unreachability diagnosis", {
|
|
70395
|
+
sessionId,
|
|
70396
|
+
priorReason: record.status_reason ?? null,
|
|
70397
|
+
projected,
|
|
70398
|
+
source
|
|
70399
|
+
});
|
|
70400
|
+
}
|
|
70288
70401
|
const stashed = takeSuppressedErrorReason(sessionId);
|
|
70289
70402
|
if (stashed) {
|
|
70290
|
-
if (!record.errorMessage && stashed.message) updates.errorMessage = stashed.message;
|
|
70403
|
+
if ((!record.errorMessage || droppedDisproven) && stashed.message) updates.errorMessage = stashed.message;
|
|
70291
70404
|
if (!record.errorKind && stashed.kind) updates.errorKind = stashed.kind;
|
|
70292
70405
|
log.session.info("snapshot projection adopted suppressed error reason", {
|
|
70293
70406
|
sessionId,
|
|
@@ -70340,6 +70453,7 @@ var init_session_snapshot_apply = __esm({
|
|
|
70340
70453
|
"use strict";
|
|
70341
70454
|
init_logging();
|
|
70342
70455
|
init_session_snapshot_gate();
|
|
70456
|
+
init_session_error_kind();
|
|
70343
70457
|
init_engine_registry();
|
|
70344
70458
|
init_session_snapshot_gate();
|
|
70345
70459
|
TERMINAL = /* @__PURE__ */ new Set(["stopped", "error"]);
|
|
@@ -72954,9 +73068,15 @@ var init_acp_session = __esm({
|
|
|
72954
73068
|
}
|
|
72955
73069
|
async resolveArtifacts() {
|
|
72956
73070
|
if (this.cfg.artifacts) return this.cfg.artifacts;
|
|
72957
|
-
|
|
73071
|
+
const artifacts = resolveAcpArtifacts(this.engineId, {
|
|
72958
73072
|
configuredAdapterCmd: await this.resolveConfiguredAdapterCmd()
|
|
72959
73073
|
});
|
|
73074
|
+
if (!fsModule.existsSync(artifacts.workerCmd[1])) {
|
|
73075
|
+
throw new Error(
|
|
73076
|
+
`Engine '${this.engineId}' runs on the ACP worker, which is not built (${artifacts.workerCmd[1]}). Install Bun (https://bun.sh) and run \`npm run build:daemon\`, or install Walnut from npm, which ships it.`
|
|
73077
|
+
);
|
|
73078
|
+
}
|
|
73079
|
+
return artifacts;
|
|
72960
73080
|
}
|
|
72961
73081
|
async establishProviderSession() {
|
|
72962
73082
|
await this.seedReplayCursor();
|
|
@@ -76493,6 +76613,7 @@ __export(session_lifecycle_exports, {
|
|
|
76493
76613
|
persistSessionModeChange: () => persistSessionModeChange,
|
|
76494
76614
|
readProviderSessionHistory: () => readProviderSessionHistory,
|
|
76495
76615
|
readSessionRichHistory: () => readSessionRichHistory,
|
|
76616
|
+
recheckSession: () => recheckSession,
|
|
76496
76617
|
respondSessionPermission: () => respondSessionPermission,
|
|
76497
76618
|
restartSession: () => restartSession,
|
|
76498
76619
|
retrySession: () => retrySession,
|
|
@@ -76830,7 +76951,7 @@ async function retrySession(sessionId) {
|
|
|
76830
76951
|
return { status: "reconnected", sessionId };
|
|
76831
76952
|
}
|
|
76832
76953
|
if (await resumeConversationExists(record)) {
|
|
76833
|
-
const {
|
|
76954
|
+
const { getQueue: getQueue2, unparkMessage: unparkMessage2 } = await Promise.resolve().then(() => (init_session_message_queue(), session_message_queue_exports));
|
|
76834
76955
|
const pendingMsgs = await getQueue2(sessionId);
|
|
76835
76956
|
for (const parked of pendingMsgs.filter((m6) => m6.status === "parked")) {
|
|
76836
76957
|
await unparkMessage2(sessionId, parked.id);
|
|
@@ -76840,9 +76961,22 @@ async function retrySession(sessionId) {
|
|
|
76840
76961
|
log.session.info("session retry: re-processing pending queue messages", { sessionId, taskId: record.taskId, count: pendingMsgs.length });
|
|
76841
76962
|
return { status: "resuming", sessionId, restoredMessages: pendingMsgs.length };
|
|
76842
76963
|
}
|
|
76843
|
-
await
|
|
76844
|
-
|
|
76845
|
-
|
|
76964
|
+
const relabelled = await updateSessionRecord2(sessionId, {
|
|
76965
|
+
process_status: "stopped",
|
|
76966
|
+
errorMessage: void 0,
|
|
76967
|
+
errorKind: void 0,
|
|
76968
|
+
activity: void 0,
|
|
76969
|
+
last_status_change: (/* @__PURE__ */ new Date()).toISOString(),
|
|
76970
|
+
status_reason: "retry_reconnect",
|
|
76971
|
+
status_changed_by: "user"
|
|
76972
|
+
});
|
|
76973
|
+
emitSessionStatusChanged2(relabelled, {}, ["*"]);
|
|
76974
|
+
log.session.info("session retry: reconnected \u2014 conversation preserved, no message sent", {
|
|
76975
|
+
sessionId,
|
|
76976
|
+
taskId: record.taskId,
|
|
76977
|
+
host: record.host ?? "__local__"
|
|
76978
|
+
});
|
|
76979
|
+
return { status: "resumable", sessionId };
|
|
76846
76980
|
}
|
|
76847
76981
|
log.session.warn("session retry: conversation never persisted \u2014 starting fresh instead of --resume", {
|
|
76848
76982
|
sessionId,
|
|
@@ -76900,6 +77034,91 @@ async function retrySession(sessionId) {
|
|
|
76900
77034
|
});
|
|
76901
77035
|
return { status: "pending", taskId: task.id, oldSessionId: sessionId };
|
|
76902
77036
|
}
|
|
77037
|
+
async function withDeadline(p6, ms) {
|
|
77038
|
+
let timer3;
|
|
77039
|
+
try {
|
|
77040
|
+
return await Promise.race([
|
|
77041
|
+
p6,
|
|
77042
|
+
new Promise((resolve) => {
|
|
77043
|
+
timer3 = setTimeout(() => resolve(RECHECK_TIMED_OUT), ms);
|
|
77044
|
+
if (timer3 && typeof timer3 === "object" && "unref" in timer3) timer3.unref();
|
|
77045
|
+
})
|
|
77046
|
+
]);
|
|
77047
|
+
} finally {
|
|
77048
|
+
if (timer3) clearTimeout(timer3);
|
|
77049
|
+
}
|
|
77050
|
+
}
|
|
77051
|
+
async function recheckSession(sessionId) {
|
|
77052
|
+
const { getSessionByClaudeId: getSessionByClaudeId2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
|
|
77053
|
+
const record = await getSessionByClaudeId2(sessionId);
|
|
77054
|
+
if (!record) throw new SessionControlError("Session not found", 404);
|
|
77055
|
+
const hostKey2 = record.host ?? "__local__";
|
|
77056
|
+
const { classifySessionError: classifySessionError2 } = await Promise.resolve().then(() => (init_session_error_kind(), session_error_kind_exports));
|
|
77057
|
+
const kind = classifySessionError2(record);
|
|
77058
|
+
const base = { sessionId, processStatus: record.process_status, infraClaim: kind === "infra" };
|
|
77059
|
+
const { isDaemonConnected: isDaemonConnected2, getPooledSnapshotConnection: getPooledSnapshotConnection2 } = await Promise.resolve().then(() => (init_daemon_connection(), daemon_connection_exports));
|
|
77060
|
+
const reachable = isDaemonConnected2(hostKey2);
|
|
77061
|
+
if (kind === "terminal") return { ...base, checked: false, reachable, reason: "terminal_error" };
|
|
77062
|
+
if (record.archived) return { ...base, checked: false, reachable, reason: "archived" };
|
|
77063
|
+
const conn = getPooledSnapshotConnection2(record.host);
|
|
77064
|
+
if (!conn) {
|
|
77065
|
+
log.session.info("session recheck: no pooled snapshot connection \u2014 nothing to ask", {
|
|
77066
|
+
sessionId,
|
|
77067
|
+
host: hostKey2,
|
|
77068
|
+
reachable
|
|
77069
|
+
});
|
|
77070
|
+
return { ...base, checked: false, reachable, reason: "no_pooled_connection" };
|
|
77071
|
+
}
|
|
77072
|
+
let raced;
|
|
77073
|
+
try {
|
|
77074
|
+
raced = await withDeadline(
|
|
77075
|
+
conn.send("getState", { sid: sessionId }, RECHECK_RPC_TIMEOUT_MS),
|
|
77076
|
+
RECHECK_RPC_TIMEOUT_MS
|
|
77077
|
+
);
|
|
77078
|
+
} catch (err2) {
|
|
77079
|
+
log.session.info("session recheck: getState failed", {
|
|
77080
|
+
sessionId,
|
|
77081
|
+
host: hostKey2,
|
|
77082
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
77083
|
+
});
|
|
77084
|
+
return { ...base, checked: false, reachable, reason: "rpc_failed" };
|
|
77085
|
+
}
|
|
77086
|
+
if (raced === RECHECK_TIMED_OUT) {
|
|
77087
|
+
log.session.warn("session recheck: getState timed out", {
|
|
77088
|
+
sessionId,
|
|
77089
|
+
host: hostKey2,
|
|
77090
|
+
timeoutMs: RECHECK_RPC_TIMEOUT_MS
|
|
77091
|
+
});
|
|
77092
|
+
return { ...base, checked: false, reachable, reason: "timeout" };
|
|
77093
|
+
}
|
|
77094
|
+
const snapshot2 = raced?.ok ? raced.snapshot : void 0;
|
|
77095
|
+
if (!snapshot2) {
|
|
77096
|
+
return { ...base, checked: false, reachable: true, reason: "no_snapshot" };
|
|
77097
|
+
}
|
|
77098
|
+
const { applySnapshot: applySnapshot2 } = await Promise.resolve().then(() => (init_session_snapshot_apply(), session_snapshot_apply_exports));
|
|
77099
|
+
const outcome = await applySnapshot2(sessionId, snapshot2, "recheck-open");
|
|
77100
|
+
const after = await getSessionByClaudeId2(sessionId);
|
|
77101
|
+
const processStatus = after?.process_status ?? record.process_status;
|
|
77102
|
+
log.session.info("session recheck: reconciled against host", {
|
|
77103
|
+
sessionId,
|
|
77104
|
+
host: hostKey2,
|
|
77105
|
+
previous: record.process_status,
|
|
77106
|
+
processStatus,
|
|
77107
|
+
cliState: snapshot2.cliState,
|
|
77108
|
+
applyOutcome: outcome.outcome
|
|
77109
|
+
});
|
|
77110
|
+
return {
|
|
77111
|
+
sessionId,
|
|
77112
|
+
checked: true,
|
|
77113
|
+
reachable: true,
|
|
77114
|
+
alive: snapshot2.cliState !== "dead",
|
|
77115
|
+
processStatus,
|
|
77116
|
+
// Re-classify against the CURRENT record: a converged row may no longer
|
|
77117
|
+
// carry the claim, and the client uses this to decide whether the sentence
|
|
77118
|
+
// on screen is still true.
|
|
77119
|
+
infraClaim: classifySessionError2(after ?? record) === "infra"
|
|
77120
|
+
};
|
|
77121
|
+
}
|
|
76903
77122
|
async function respondSessionPermission(sessionId, requestId, allow, denyMessage, optionId, answers) {
|
|
76904
77123
|
if (typeof requestId !== "string" || !requestId || typeof allow !== "boolean") {
|
|
76905
77124
|
throw new SessionControlError("requestId (string) and allow (boolean) are required", 400);
|
|
@@ -77193,7 +77412,7 @@ async function readSessionRichHistory(sessionId, tail) {
|
|
|
77193
77412
|
...historyWindowed ? { windowed: true } : {}
|
|
77194
77413
|
};
|
|
77195
77414
|
}
|
|
77196
|
-
var CLAUDE_SESSION_MODES, MAX_PINNED_MESSAGES, MAX_PIN_LABEL_CHARS, MAX_PIN_ID_CHARS, GIT_BASES, MAX_FORK_DEPTH, HISTORY_STARTUP_GRACE_MS;
|
|
77415
|
+
var CLAUDE_SESSION_MODES, MAX_PINNED_MESSAGES, MAX_PIN_LABEL_CHARS, MAX_PIN_ID_CHARS, RECHECK_RPC_TIMEOUT_MS, RECHECK_TIMED_OUT, GIT_BASES, MAX_FORK_DEPTH, HISTORY_STARTUP_GRACE_MS;
|
|
77197
77416
|
var init_session_lifecycle = __esm({
|
|
77198
77417
|
"src/core/sessions/session-lifecycle.ts"() {
|
|
77199
77418
|
"use strict";
|
|
@@ -77207,6 +77426,8 @@ var init_session_lifecycle = __esm({
|
|
|
77207
77426
|
MAX_PINNED_MESSAGES = 200;
|
|
77208
77427
|
MAX_PIN_LABEL_CHARS = 300;
|
|
77209
77428
|
MAX_PIN_ID_CHARS = 200;
|
|
77429
|
+
RECHECK_RPC_TIMEOUT_MS = 5e3;
|
|
77430
|
+
RECHECK_TIMED_OUT = /* @__PURE__ */ Symbol("recheck-timeout");
|
|
77210
77431
|
GIT_BASES = /* @__PURE__ */ new Set(["uncommitted", "previous", "remote"]);
|
|
77211
77432
|
MAX_FORK_DEPTH = 5;
|
|
77212
77433
|
HISTORY_STARTUP_GRACE_MS = 2 * 60 * 1e3;
|
|
@@ -86589,7 +86810,7 @@ function isRevealSecretPath(resolved) {
|
|
|
86589
86810
|
];
|
|
86590
86811
|
return denied.some((d8) => resolved === d8 || resolved.startsWith(d8 + path69.sep)) || /(^|\/)config\.ya?ml$/.test(resolved);
|
|
86591
86812
|
}
|
|
86592
|
-
var filesRouter, MAX_ENTRIES2, REMOTE_TIMEOUT_MS3, REMOTE_RESOLVE_BUDGET_MS, FilesOpError, MAX_UPWARD_LEVELS2, MAX_DOWNWARD_DEPTH, REFERENCE_SEARCH_BUDGET_MS, SYMBOL_RE2, REVEAL_APP_DENY_EXTS;
|
|
86813
|
+
var filesRouter, MAX_ENTRIES2, REMOTE_TIMEOUT_MS3, REMOTE_RESOLVE_BUDGET_MS, FilesOpError, MAX_UPWARD_LEVELS2, MAX_DOWNWARD_DEPTH, REFERENCE_SEARCH_BUDGET_MS, SYMBOL_RE2, LIST_MANY_MAX_PATHS, LIST_MANY_CONCURRENCY, LIST_MANY_DEADLINE_MS, REVEAL_APP_DENY_EXTS;
|
|
86593
86814
|
var init_files2 = __esm({
|
|
86594
86815
|
"src/web/routes/files.ts"() {
|
|
86595
86816
|
"use strict";
|
|
@@ -86722,6 +86943,54 @@ var init_files2 = __esm({
|
|
|
86722
86943
|
next(err2);
|
|
86723
86944
|
}
|
|
86724
86945
|
});
|
|
86946
|
+
LIST_MANY_MAX_PATHS = 64;
|
|
86947
|
+
LIST_MANY_CONCURRENCY = 8;
|
|
86948
|
+
LIST_MANY_DEADLINE_MS = 8e3;
|
|
86949
|
+
filesRouter.post("/list-many", async (req, res, next) => {
|
|
86950
|
+
try {
|
|
86951
|
+
const body3 = req.body ?? {};
|
|
86952
|
+
const raw = Array.isArray(body3.paths) ? body3.paths : null;
|
|
86953
|
+
if (!raw) {
|
|
86954
|
+
res.status(400).json({ error: "paths (string[]) is required" });
|
|
86955
|
+
return;
|
|
86956
|
+
}
|
|
86957
|
+
const host = typeof body3.host === "string" && body3.host ? body3.host : void 0;
|
|
86958
|
+
const showHidden = body3.showHidden === true || body3.showHidden === "1";
|
|
86959
|
+
const paths = [...new Set(
|
|
86960
|
+
raw.filter((p6) => typeof p6 === "string" && !!p6)
|
|
86961
|
+
)];
|
|
86962
|
+
const truncated = paths.length > LIST_MANY_MAX_PATHS;
|
|
86963
|
+
const wanted = paths.slice(0, LIST_MANY_MAX_PATHS);
|
|
86964
|
+
const deadline4 = Date.now() + LIST_MANY_DEADLINE_MS;
|
|
86965
|
+
const listings = [];
|
|
86966
|
+
let timedOut = false;
|
|
86967
|
+
let next_ = 0;
|
|
86968
|
+
const worker = async () => {
|
|
86969
|
+
for (; ; ) {
|
|
86970
|
+
const i8 = next_++;
|
|
86971
|
+
if (i8 >= wanted.length) return;
|
|
86972
|
+
if (Date.now() >= deadline4) {
|
|
86973
|
+
timedOut = true;
|
|
86974
|
+
return;
|
|
86975
|
+
}
|
|
86976
|
+
const dirPath = wanted[i8];
|
|
86977
|
+
try {
|
|
86978
|
+
listings.push(await listSessionFiles(dirPath, host, showHidden));
|
|
86979
|
+
} catch (err2) {
|
|
86980
|
+
listings.push({ path: dirPath, error: err2 instanceof Error ? err2.message : String(err2) });
|
|
86981
|
+
}
|
|
86982
|
+
}
|
|
86983
|
+
};
|
|
86984
|
+
await Promise.all(Array.from({ length: Math.min(LIST_MANY_CONCURRENCY, wanted.length) }, worker));
|
|
86985
|
+
res.json({ listings, truncated, timedOut });
|
|
86986
|
+
} catch (err2) {
|
|
86987
|
+
if (err2 instanceof FilesOpError) {
|
|
86988
|
+
res.status(err2.statusCode).json({ error: err2.message });
|
|
86989
|
+
return;
|
|
86990
|
+
}
|
|
86991
|
+
next(err2);
|
|
86992
|
+
}
|
|
86993
|
+
});
|
|
86725
86994
|
REVEAL_APP_DENY_EXTS = /* @__PURE__ */ new Set([
|
|
86726
86995
|
"command",
|
|
86727
86996
|
"sh",
|
|
@@ -102616,6 +102885,19 @@ var init_sessions2 = __esm({
|
|
|
102616
102885
|
next(err2);
|
|
102617
102886
|
}
|
|
102618
102887
|
});
|
|
102888
|
+
sessionsRouter.post("/:sessionId/recheck", async (req, res, next) => {
|
|
102889
|
+
try {
|
|
102890
|
+
const sessionId = req.params.sessionId;
|
|
102891
|
+
const result = await recheckSession(sessionId);
|
|
102892
|
+
res.json(result);
|
|
102893
|
+
} catch (err2) {
|
|
102894
|
+
if (err2 instanceof SessionControlError) {
|
|
102895
|
+
res.status(err2.statusCode).json({ error: err2.message });
|
|
102896
|
+
return;
|
|
102897
|
+
}
|
|
102898
|
+
next(err2);
|
|
102899
|
+
}
|
|
102900
|
+
});
|
|
102619
102901
|
sessionsRouter.post("/:sessionId/restart", async (req, res, next) => {
|
|
102620
102902
|
const sessionId = req.params.sessionId;
|
|
102621
102903
|
const startedAt = Date.now();
|
|
@@ -105319,9 +105601,13 @@ var init_credential_verify = __esm({
|
|
|
105319
105601
|
var config_exports = {};
|
|
105320
105602
|
__export(config_exports, {
|
|
105321
105603
|
buildProvidersPayload: () => buildProvidersPayload,
|
|
105322
|
-
configRouter: () => configRouter
|
|
105604
|
+
configRouter: () => configRouter,
|
|
105605
|
+
setStaticRootReporter: () => setStaticRootReporter
|
|
105323
105606
|
});
|
|
105324
105607
|
import { Router as Router14 } from "express";
|
|
105608
|
+
function setStaticRootReporter(fn) {
|
|
105609
|
+
staticRootReporter = fn;
|
|
105610
|
+
}
|
|
105325
105611
|
async function fetchOllamaModels(baseUrl) {
|
|
105326
105612
|
if (Date.now() - _ollamaCache.ts < OLLAMA_CACHE_TTL) return _ollamaCache.models;
|
|
105327
105613
|
const url = (baseUrl ?? DEFAULT_BASE_URLS["ollama"] ?? "http://localhost:11434") + "/api/tags";
|
|
@@ -105454,7 +105740,7 @@ async function buildProvidersPayload() {
|
|
|
105454
105740
|
}
|
|
105455
105741
|
return { providers };
|
|
105456
105742
|
}
|
|
105457
|
-
var configRouter, _ollamaCache, OLLAMA_CACHE_TTL;
|
|
105743
|
+
var configRouter, staticRootReporter, _ollamaCache, OLLAMA_CACHE_TTL;
|
|
105458
105744
|
var init_config11 = __esm({
|
|
105459
105745
|
"src/web/routes/config.ts"() {
|
|
105460
105746
|
"use strict";
|
|
@@ -105469,6 +105755,7 @@ var init_config11 = __esm({
|
|
|
105469
105755
|
init_defaults();
|
|
105470
105756
|
init_config_redact();
|
|
105471
105757
|
configRouter = Router14();
|
|
105758
|
+
staticRootReporter = null;
|
|
105472
105759
|
configRouter.get("/", async (_req, res, next) => {
|
|
105473
105760
|
try {
|
|
105474
105761
|
const config = await getConfig();
|
|
@@ -105495,7 +105782,8 @@ var init_config11 = __esm({
|
|
|
105495
105782
|
remoteIdUniquenessGaps = getExtIndexUniquenessGaps2();
|
|
105496
105783
|
} catch {
|
|
105497
105784
|
}
|
|
105498
|
-
|
|
105785
|
+
const webAssets = staticRootReporter?.() ?? null;
|
|
105786
|
+
res.json({ config: CLOUD_MODE ? redactConfig(config) : config, envTokenHint, installDir: CLOUD_MODE ? null : WALNUT_INSTALL_DIR, notesDir: CLOUD_MODE ? null : NOTES_DIR, processNice, memory, canRevealLocalFiles, remoteIdUniquenessGaps, cloud: CLOUD_MODE, webAssets });
|
|
105499
105787
|
} catch (err2) {
|
|
105500
105788
|
next(err2);
|
|
105501
105789
|
}
|
|
@@ -110364,6 +110652,9 @@ var init_session_chat = __esm({
|
|
|
110364
110652
|
|
|
110365
110653
|
// src/web/routes/browser-logs.ts
|
|
110366
110654
|
import { Router as Router25 } from "express";
|
|
110655
|
+
function entrySubsystem(entry) {
|
|
110656
|
+
return entry.subsystem && ENTRY_SUBSYSTEMS.has(entry.subsystem) ? entry.subsystem : "browser";
|
|
110657
|
+
}
|
|
110367
110658
|
function checkRate(client, entryCount) {
|
|
110368
110659
|
const now = Date.now();
|
|
110369
110660
|
let state9 = clientRates.get(client);
|
|
@@ -110405,10 +110696,11 @@ function writeEntries(entries) {
|
|
|
110405
110696
|
if (entry.args) meta.args = truncate(entry.args, 1e3);
|
|
110406
110697
|
if (entry.url) meta.url = entry.url;
|
|
110407
110698
|
if (entry.count && entry.count > 1) meta.count = entry.count;
|
|
110699
|
+
if (entry.client === "mac-app") meta.client = entry.client;
|
|
110408
110700
|
writeLogEntry({
|
|
110409
110701
|
time: entry.time,
|
|
110410
110702
|
level: mapLevel(entry.level),
|
|
110411
|
-
subsystem:
|
|
110703
|
+
subsystem: entrySubsystem(entry),
|
|
110412
110704
|
message: truncate(entry.message, 2e3),
|
|
110413
110705
|
browserLevel: entry.level,
|
|
110414
110706
|
// preserve original level (e.g. 'log' vs 'info')
|
|
@@ -110431,13 +110723,14 @@ function registerBrowserLogsRpc() {
|
|
|
110431
110723
|
writeEntries(entries);
|
|
110432
110724
|
});
|
|
110433
110725
|
}
|
|
110434
|
-
var RATE_WINDOW_MS, RATE_LIMIT, clientRates, REST_MAX_ENTRIES, browserLogsRouter;
|
|
110726
|
+
var ENTRY_SUBSYSTEMS, RATE_WINDOW_MS, RATE_LIMIT, clientRates, REST_MAX_ENTRIES, browserLogsRouter;
|
|
110435
110727
|
var init_browser_logs = __esm({
|
|
110436
110728
|
"src/web/routes/browser-logs.ts"() {
|
|
110437
110729
|
"use strict";
|
|
110438
110730
|
init_handler();
|
|
110439
110731
|
init_logging();
|
|
110440
110732
|
init_logger();
|
|
110733
|
+
ENTRY_SUBSYSTEMS = /* @__PURE__ */ new Set(["browser", "desktop"]);
|
|
110441
110734
|
RATE_WINDOW_MS = 1e4;
|
|
110442
110735
|
RATE_LIMIT = 500;
|
|
110443
110736
|
clientRates = /* @__PURE__ */ new WeakMap();
|
|
@@ -118518,6 +118811,7 @@ var init_session_health_monitor = __esm({
|
|
|
118518
118811
|
log.session.info("health monitor: drained orphan dead-pool", { orphanCount: orphanIds.length, written: written.length });
|
|
118519
118812
|
}
|
|
118520
118813
|
if (sessions3.length === 0) {
|
|
118814
|
+
if (!ctx.overBudget()) await this.checkSnapshotPull(sessions3, ctx, allSessions);
|
|
118521
118815
|
const total = Date.now() - checkT0;
|
|
118522
118816
|
if (total > 500) log.session.warn("health monitor: check() slow (no active sessions)", { totalMs: total, orphanMs: tOrphan - checkT0, recoverMs: tRecover - tOrphan });
|
|
118523
118817
|
return;
|
|
@@ -118547,7 +118841,7 @@ var init_session_health_monitor = __esm({
|
|
|
118547
118841
|
await this.reconcilePendingBackgroundTasks(sessions3);
|
|
118548
118842
|
endPhase("pendingBgTasks");
|
|
118549
118843
|
if (ctx.overBudget()) return;
|
|
118550
|
-
await this.checkSnapshotPull(sessions3, ctx);
|
|
118844
|
+
await this.checkSnapshotPull(sessions3, ctx, allSessions);
|
|
118551
118845
|
endPhase("snapshotPull");
|
|
118552
118846
|
if (ctx.overBudget()) return;
|
|
118553
118847
|
const reconciledIds = await this.reconcileStuckRunningSessions(sessions3, taskMap);
|
|
@@ -119041,15 +119335,39 @@ var init_session_health_monitor = __esm({
|
|
|
119041
119335
|
bus.emit(EventNames.SESSION_WILL_REAP, payload, ["*"], { source: "health-monitor" });
|
|
119042
119336
|
}
|
|
119043
119337
|
/**
|
|
119044
|
-
* C2 snapshot pull channel (contract §5): every 30s tick,
|
|
119045
|
-
*
|
|
119046
|
-
*
|
|
119047
|
-
*
|
|
119048
|
-
*
|
|
119338
|
+
* C2 snapshot pull channel (contract §5): every 30s tick, PULL getState from
|
|
119339
|
+
* the host's POOLED CONNECTED DaemonConnection advertising 'snapshot-v1' and
|
|
119340
|
+
* feed the answer to applySnapshot. NEVER dials a new connection, always
|
|
119341
|
+
* sequential. TWO candidate classes with SEPARATE budgets and cadences:
|
|
119342
|
+
*
|
|
119343
|
+
* - LIVE — {running, idle}: 10 sids/tick, 25s per-sid spacing. The steady
|
|
119344
|
+
* state channel.
|
|
119345
|
+
* - RE-EXAMINATION — {error, stopped} whose cause is NOT positively terminal
|
|
119346
|
+
* (classifySessionError) and whose last_status_change is inside
|
|
119347
|
+
* RESCUABLE_STOPPED_WINDOW_MS: 5 sids/tick, 5 min per-sid spacing. Same
|
|
119348
|
+
* engine/provider/awaiting_spawn/archived exclusions as the live class.
|
|
119349
|
+
*
|
|
119350
|
+
* WHY RE-EXAMINATION EXISTS — do not "optimize" the error class back out. A
|
|
119351
|
+
* wrongly-'error' record was a two-sided deadlock: this lane is the ONE writer
|
|
119352
|
+
* the enforce-mode gate sanctions for a snapshot-covered session, and while it
|
|
119353
|
+
* looked at live records only it never re-derived status for an 'error' one.
|
|
119354
|
+
* recoverInfraFailedSessions DOES look, but the fix it writes
|
|
119355
|
+
* (('health-monitor','auto_recovered_dead')) is itself category-①, so the gate
|
|
119356
|
+
* drops the whole patch — the lane allowed to write refused to look, and the
|
|
119357
|
+
* lane that looked was not allowed to write. Records froze in 'error' with
|
|
119358
|
+
* "Connection lost — unable to reach remote host" for hours after their host
|
|
119359
|
+
* came back (2026-09-03, a remote dev host whose tunnel flapped: prose still on
|
|
119360
|
+
* the record 2h after the reconnect), burning a 30s log-churn loop the whole
|
|
119361
|
+
* time. Terminality is decided STRUCTURALLY, never by matching errorMessage
|
|
119362
|
+
* prose — see the header of session-error-kind.ts for why that ban exists.
|
|
119363
|
+
*
|
|
119364
|
+
* Re-examination candidates come from a WIDER pool than the live ones: the
|
|
119365
|
+
* tick's own working set drops every 'error' row (isTerminalSession), so the
|
|
119366
|
+
* caller passes the unfiltered health-scan set for this class only.
|
|
119049
119367
|
*
|
|
119050
|
-
*
|
|
119368
|
+
* THREE fairness/budget properties both classes must keep (C8/C9/C12/C29):
|
|
119051
119369
|
*
|
|
119052
|
-
* -
|
|
119370
|
+
* - The TickContext budget is honored INSIDE the loop (same discipline as
|
|
119053
119371
|
* checkHungSessions). Each iteration is a real daemon RPC with a
|
|
119054
119372
|
* probe-timeout ceiling, so 10 slow hosts could burn 10 × the probe
|
|
119055
119373
|
* timeout sequentially BEFORE the authoritative reconcile phases that run
|
|
@@ -119058,70 +119376,101 @@ var init_session_health_monitor = __esm({
|
|
|
119058
119376
|
* next tick (see the rotation below).
|
|
119059
119377
|
*
|
|
119060
119378
|
* - Candidates are ordered by lastPullAt ASCENDING (never-pulled first),
|
|
119061
|
-
* which turns
|
|
119062
|
-
*
|
|
119063
|
-
*
|
|
119064
|
-
*
|
|
119379
|
+
* which turns each cap into a round-robin. In list order the SAME first ten
|
|
119380
|
+
* sids were pulled every tick and — because the 25s spacing is shorter than
|
|
119381
|
+
* the 30s cadence — sids 11+ were never pulled at all: the pull channel
|
|
119382
|
+
* silently did not exist for them. Each class rotates on its OWN timestamps.
|
|
119383
|
+
*
|
|
119384
|
+
* - The classes cannot spend each other's budget. Re-examination is
|
|
119385
|
+
* speculative ("is this record still true?") and a settled-death cohort
|
|
119386
|
+
* dwarfs the live set (the live DB measured 100 stopped candidates against
|
|
119387
|
+
* 4 error rows), so it must cost a handful of cheap RPCs per tick and
|
|
119388
|
+
* nothing more — the same reasoning as MAX_STOPPED_PROBES_PER_TICK below.
|
|
119065
119389
|
*/
|
|
119066
119390
|
snapshotPullAt = /* @__PURE__ */ new Map();
|
|
119067
|
-
|
|
119391
|
+
/** Separate from snapshotPullAt on purpose: the two classes rotate
|
|
119392
|
+
* independently, so a live pull can never reset the slow re-examination
|
|
119393
|
+
* cadence (or vice versa) when a record moves between them. */
|
|
119394
|
+
snapshotReexamPullAt = /* @__PURE__ */ new Map();
|
|
119395
|
+
async checkSnapshotPull(sessions3, ctx, reexamPool = sessions3) {
|
|
119068
119396
|
const PULL_MIN_GAP_MS = 25e3;
|
|
119069
119397
|
const MAX_PULLS_PER_TICK = 10;
|
|
119398
|
+
const REEXAM_MIN_GAP_MS = 5 * 6e4;
|
|
119399
|
+
const MAX_REEXAM_PULLS_PER_TICK = 5;
|
|
119070
119400
|
try {
|
|
119071
119401
|
const { getSnapshotStatusMode: getSnapshotStatusMode2 } = await Promise.resolve().then(() => (init_session_snapshot_gate(), session_snapshot_gate_exports));
|
|
119072
119402
|
if (getSnapshotStatusMode2() === "off") return;
|
|
119073
119403
|
const { getPooledSnapshotConnection: getPooledSnapshotConnection2 } = await Promise.resolve().then(() => (init_daemon_connection(), daemon_connection_exports));
|
|
119074
119404
|
const { applySnapshot: applySnapshot2 } = await Promise.resolve().then(() => (init_session_snapshot_apply(), session_snapshot_apply_exports));
|
|
119075
119405
|
const now = Date.now();
|
|
119076
|
-
const
|
|
119077
|
-
if (s4.
|
|
119406
|
+
const pullable = (s4) => {
|
|
119407
|
+
if (s4.archived) return false;
|
|
119078
119408
|
if (!engineCaps(s4.engine).snapshotPull) return false;
|
|
119079
119409
|
if (s4.provider === "embedded" || s4.provider === "sdk") return false;
|
|
119080
|
-
|
|
119081
|
-
|
|
119082
|
-
|
|
119083
|
-
|
|
119084
|
-
|
|
119085
|
-
|
|
119086
|
-
|
|
119087
|
-
|
|
119088
|
-
|
|
119089
|
-
|
|
119090
|
-
|
|
119091
|
-
|
|
119092
|
-
|
|
119093
|
-
|
|
119094
|
-
|
|
119095
|
-
|
|
119096
|
-
|
|
119097
|
-
|
|
119098
|
-
|
|
119099
|
-
|
|
119100
|
-
|
|
119101
|
-
|
|
119102
|
-
|
|
119103
|
-
|
|
119104
|
-
|
|
119105
|
-
|
|
119106
|
-
|
|
119107
|
-
|
|
119108
|
-
|
|
119109
|
-
|
|
119110
|
-
|
|
119111
|
-
|
|
119112
|
-
|
|
119113
|
-
|
|
119114
|
-
|
|
119115
|
-
|
|
119116
|
-
|
|
119410
|
+
return s4.status_reason !== "awaiting_spawn";
|
|
119411
|
+
};
|
|
119412
|
+
const byPullAge = (at) => (a8, b8) => (at.get(a8.claudeSessionId) ?? 0) - (at.get(b8.claudeSessionId) ?? 0);
|
|
119413
|
+
const live = sessions3.filter((s4) => {
|
|
119414
|
+
if (s4.process_status !== "running" && s4.process_status !== "idle") return false;
|
|
119415
|
+
if (!pullable(s4)) return false;
|
|
119416
|
+
return now - (this.snapshotPullAt.get(s4.claudeSessionId) ?? 0) >= PULL_MIN_GAP_MS;
|
|
119417
|
+
}).sort(byPullAge(this.snapshotPullAt));
|
|
119418
|
+
const reexam = reexamPool.filter((s4) => {
|
|
119419
|
+
if (s4.process_status !== "error" && s4.process_status !== "stopped") return false;
|
|
119420
|
+
if (!pullable(s4)) return false;
|
|
119421
|
+
if (classifySessionError(s4) === "terminal") return false;
|
|
119422
|
+
const changedAt = s4.last_status_change ? Date.parse(s4.last_status_change) : NaN;
|
|
119423
|
+
if (!Number.isFinite(changedAt)) return false;
|
|
119424
|
+
if (now - changedAt > RESCUABLE_STOPPED_WINDOW_MS) return false;
|
|
119425
|
+
return now - (this.snapshotReexamPullAt.get(s4.claudeSessionId) ?? 0) >= REEXAM_MIN_GAP_MS;
|
|
119426
|
+
}).sort(byPullAge(this.snapshotReexamPullAt));
|
|
119427
|
+
let abandoned = false;
|
|
119428
|
+
const runClass = async (candidates, cap2, at, candidateClass) => {
|
|
119429
|
+
let pulled = 0;
|
|
119430
|
+
for (const s4 of candidates) {
|
|
119431
|
+
if (ctx?.overBudget()) {
|
|
119432
|
+
abandoned = true;
|
|
119433
|
+
log.session.warn("health monitor: checkSnapshotPull abandoned mid-loop (over budget)", {
|
|
119434
|
+
pulled,
|
|
119435
|
+
candidateCount: candidates.length,
|
|
119436
|
+
candidateClass
|
|
119437
|
+
});
|
|
119438
|
+
break;
|
|
119439
|
+
}
|
|
119440
|
+
const conn = getPooledSnapshotConnection2(s4.host);
|
|
119441
|
+
if (!conn) continue;
|
|
119442
|
+
if (pulled >= cap2) {
|
|
119443
|
+
log.session.info("health monitor: snapshot pull capped this tick", {
|
|
119444
|
+
cap: cap2,
|
|
119445
|
+
sessionCount: sessions3.length,
|
|
119446
|
+
candidateCount: candidates.length,
|
|
119447
|
+
candidateClass
|
|
119448
|
+
});
|
|
119449
|
+
break;
|
|
119450
|
+
}
|
|
119451
|
+
pulled++;
|
|
119452
|
+
at.set(s4.claudeSessionId, now);
|
|
119453
|
+
try {
|
|
119454
|
+
const resp = await probeWithTimeout(
|
|
119455
|
+
conn.send("getState", { sid: s4.claudeSessionId }),
|
|
119456
|
+
null,
|
|
119457
|
+
"snapshot-pull-getState",
|
|
119458
|
+
s4.claudeSessionId
|
|
119459
|
+
);
|
|
119460
|
+
const snapshot2 = resp?.ok ? resp.snapshot : void 0;
|
|
119461
|
+
if (snapshot2) await applySnapshot2(s4.claudeSessionId, snapshot2, "pull-30s");
|
|
119462
|
+
} catch (err2) {
|
|
119463
|
+
log.session.debug("health monitor: snapshot pull failed", {
|
|
119464
|
+
sessionId: s4.claudeSessionId,
|
|
119465
|
+
host: s4.host ?? "__local__",
|
|
119466
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
119467
|
+
});
|
|
119468
|
+
}
|
|
119117
119469
|
}
|
|
119118
|
-
}
|
|
119119
|
-
|
|
119120
|
-
|
|
119121
|
-
|
|
119122
|
-
sessionCount: sessions3.length,
|
|
119123
|
-
candidateCount: candidates.length
|
|
119124
|
-
});
|
|
119470
|
+
};
|
|
119471
|
+
await runClass(live, MAX_PULLS_PER_TICK, this.snapshotPullAt, "live");
|
|
119472
|
+
if (!abandoned) {
|
|
119473
|
+
await runClass(reexam, MAX_REEXAM_PULLS_PER_TICK, this.snapshotReexamPullAt, "reexam");
|
|
119125
119474
|
}
|
|
119126
119475
|
if (this.snapshotPullAt.size > 200) {
|
|
119127
119476
|
const liveIds = new Set(sessions3.map((s4) => s4.claudeSessionId));
|
|
@@ -119129,6 +119478,12 @@ var init_session_health_monitor = __esm({
|
|
|
119129
119478
|
if (!liveIds.has(sid)) this.snapshotPullAt.delete(sid);
|
|
119130
119479
|
}
|
|
119131
119480
|
}
|
|
119481
|
+
if (this.snapshotReexamPullAt.size > 200) {
|
|
119482
|
+
const poolIds = new Set(reexamPool.map((s4) => s4.claudeSessionId));
|
|
119483
|
+
for (const sid of this.snapshotReexamPullAt.keys()) {
|
|
119484
|
+
if (!poolIds.has(sid)) this.snapshotReexamPullAt.delete(sid);
|
|
119485
|
+
}
|
|
119486
|
+
}
|
|
119132
119487
|
} catch (err2) {
|
|
119133
119488
|
log.session.warn("health monitor: checkSnapshotPull failed", {
|
|
119134
119489
|
error: err2 instanceof Error ? err2.message : String(err2)
|
|
@@ -119291,6 +119646,31 @@ var init_session_health_monitor = __esm({
|
|
|
119291
119646
|
});
|
|
119292
119647
|
}
|
|
119293
119648
|
}
|
|
119649
|
+
/**
|
|
119650
|
+
* Can a scheduled auto-recovery actually FIRE for this record?
|
|
119651
|
+
*
|
|
119652
|
+
* fire() (session-auto-recover) hard-requires the linked task to still be
|
|
119653
|
+
* IN_PROGRESS — AGENT_COMPLETE means the work was already handed back and
|
|
119654
|
+
* resuming would talk over the human. Arming a recovery that cannot pass that
|
|
119655
|
+
* check is not free: the 30s tick re-armed it forever and each arming logged
|
|
119656
|
+
* an abort 20s later (measured running for 2+ hours on one session), while
|
|
119657
|
+
* `if (armed) continue` also skipped the phase sync that IS appropriate there.
|
|
119658
|
+
*
|
|
119659
|
+
* A lookup that cannot answer (no task id, deleted task, DB error) returns
|
|
119660
|
+
* true: only a KNOWN non-IN_PROGRESS phase blocks arming, so a transient read
|
|
119661
|
+
* failure can never disable recovery. schedule()'s own guards still apply.
|
|
119662
|
+
*/
|
|
119663
|
+
async autoRecoverCanFire(s4) {
|
|
119664
|
+
if (!s4.taskId) return true;
|
|
119665
|
+
try {
|
|
119666
|
+
const { getTask: getTask2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
|
|
119667
|
+
const task = await getTask2(s4.taskId);
|
|
119668
|
+
if (!task) return true;
|
|
119669
|
+
return task.phase === "IN_PROGRESS";
|
|
119670
|
+
} catch {
|
|
119671
|
+
return true;
|
|
119672
|
+
}
|
|
119673
|
+
}
|
|
119294
119674
|
/**
|
|
119295
119675
|
* Auto-recover remote sessions stuck in 'error' with "Connection lost" message.
|
|
119296
119676
|
*
|
|
@@ -119377,28 +119757,31 @@ var init_session_health_monitor = __esm({
|
|
|
119377
119757
|
} else if (wasStopped) {
|
|
119378
119758
|
continue;
|
|
119379
119759
|
} else {
|
|
119380
|
-
const
|
|
119381
|
-
|
|
119382
|
-
|
|
119383
|
-
|
|
119384
|
-
|
|
119385
|
-
|
|
119386
|
-
|
|
119387
|
-
|
|
119388
|
-
|
|
119389
|
-
|
|
119390
|
-
|
|
119391
|
-
|
|
119392
|
-
|
|
119393
|
-
|
|
119394
|
-
|
|
119395
|
-
|
|
119396
|
-
|
|
119397
|
-
|
|
119398
|
-
|
|
119399
|
-
|
|
119760
|
+
const { isSnapshotCovered: isSnapshotCovered2 } = await Promise.resolve().then(() => (init_session_snapshot_gate(), session_snapshot_gate_exports));
|
|
119761
|
+
if (!isSnapshotCovered2(s4.claudeSessionId)) {
|
|
119762
|
+
const updated = await updateSessionRecord2(s4.claudeSessionId, {
|
|
119763
|
+
process_status: "stopped",
|
|
119764
|
+
errorMessage: void 0,
|
|
119765
|
+
errorKind: "infra",
|
|
119766
|
+
activity: void 0,
|
|
119767
|
+
last_status_change: now,
|
|
119768
|
+
status_reason: "auto_recovered_dead",
|
|
119769
|
+
status_changed_by: "health-monitor"
|
|
119770
|
+
});
|
|
119771
|
+
emitSessionStatusChanged(
|
|
119772
|
+
updated,
|
|
119773
|
+
{},
|
|
119774
|
+
["*"],
|
|
119775
|
+
{ source: "health-monitor", urgency: "urgent" }
|
|
119776
|
+
);
|
|
119777
|
+
log.session.info("health monitor: auto-recovered connection-lost session (process dead)", {
|
|
119778
|
+
sessionId: s4.claudeSessionId,
|
|
119779
|
+
host: s4.host,
|
|
119780
|
+
alive: false
|
|
119781
|
+
});
|
|
119782
|
+
}
|
|
119400
119783
|
const { scheduleSessionAutoRecover: scheduleSessionAutoRecover2 } = await Promise.resolve().then(() => (init_session_auto_recover(), session_auto_recover_exports));
|
|
119401
|
-
const armed = scheduleSessionAutoRecover2(s4, s4.status_reason ?? "daemon_reported_exit");
|
|
119784
|
+
const armed = await this.autoRecoverCanFire(s4) && scheduleSessionAutoRecover2(s4, s4.status_reason ?? "daemon_reported_exit");
|
|
119402
119785
|
if (armed) continue;
|
|
119403
119786
|
if (s4.taskId) {
|
|
119404
119787
|
try {
|
|
@@ -132723,6 +133106,20 @@ var init_session_lifecycle_v1 = __esm({
|
|
|
132723
133106
|
next(err2);
|
|
132724
133107
|
}
|
|
132725
133108
|
});
|
|
133109
|
+
sessionLifecycleV1Router.post("/sessions/:id/recheck", async (req, res, next) => {
|
|
133110
|
+
try {
|
|
133111
|
+
const sessionId = validSid2(req, res);
|
|
133112
|
+
if (!sessionId) return;
|
|
133113
|
+
if (CLOUD_MODE) {
|
|
133114
|
+
await relayControlAction(res, "recheck", sessionId, void 0, 200);
|
|
133115
|
+
return;
|
|
133116
|
+
}
|
|
133117
|
+
const { recheckSession: recheckSession2 } = await Promise.resolve().then(() => (init_session_lifecycle(), session_lifecycle_exports));
|
|
133118
|
+
await runLocal(res, next, 200, () => recheckSession2(sessionId));
|
|
133119
|
+
} catch (err2) {
|
|
133120
|
+
next(err2);
|
|
133121
|
+
}
|
|
133122
|
+
});
|
|
132726
133123
|
sessionLifecycleV1Router.post("/sessions/:id/permission", async (req, res, next) => {
|
|
132727
133124
|
try {
|
|
132728
133125
|
const sessionId = validSid2(req, res);
|
|
@@ -149378,6 +149775,16 @@ async function resolveCredentialHealth() {
|
|
|
149378
149775
|
return { hasReadyProvider: false, source: "none" };
|
|
149379
149776
|
}
|
|
149380
149777
|
}
|
|
149778
|
+
async function isWalnutAnsweringOn(port) {
|
|
149779
|
+
try {
|
|
149780
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/system/health`, { signal: AbortSignal.timeout(1500) });
|
|
149781
|
+
if (!res.ok) return false;
|
|
149782
|
+
const body3 = await res.json();
|
|
149783
|
+
return !!body3 && typeof body3 === "object" && "hasReadyProvider" in body3;
|
|
149784
|
+
} catch {
|
|
149785
|
+
return false;
|
|
149786
|
+
}
|
|
149787
|
+
}
|
|
149381
149788
|
async function startServer(options = {}) {
|
|
149382
149789
|
if (httpServer) throw new Error("Server already running. Call stopServer() first.");
|
|
149383
149790
|
bootCompleted = false;
|
|
@@ -149904,6 +150311,7 @@ async function startServer(options = {}) {
|
|
|
149904
150311
|
});
|
|
149905
150312
|
if (!dev) {
|
|
149906
150313
|
const staticDir = (() => {
|
|
150314
|
+
if (process.env.WALNUT_WEB_STATIC_DIR) return process.env.WALNUT_WEB_STATIC_DIR;
|
|
149907
150315
|
let dir2 = path157.dirname(fileURLToPath13(import.meta.url));
|
|
149908
150316
|
for (let i8 = 0; i8 < 5; i8++) {
|
|
149909
150317
|
const candidate = path157.join(dir2, "web", "static", "index.html");
|
|
@@ -149920,11 +150328,65 @@ async function startServer(options = {}) {
|
|
|
149920
150328
|
}
|
|
149921
150329
|
return path157.join(process.cwd(), "dist", "web", "static");
|
|
149922
150330
|
})();
|
|
150331
|
+
const mirrorDir = process.env.WALNUT_WEB_STATIC_MIRROR || path157.join(WALNUT_HOME, "cache", "web-static");
|
|
150332
|
+
let mirrorReady = false;
|
|
150333
|
+
try {
|
|
150334
|
+
const want = fs98.readFileSync(path157.join(staticDir, "index.html"), "utf-8");
|
|
150335
|
+
let have = null;
|
|
150336
|
+
try {
|
|
150337
|
+
have = fs98.readFileSync(path157.join(mirrorDir, "index.html"), "utf-8");
|
|
150338
|
+
} catch {
|
|
150339
|
+
}
|
|
150340
|
+
if (have !== want) {
|
|
150341
|
+
fs98.rmSync(mirrorDir, { recursive: true, force: true });
|
|
150342
|
+
fs98.mkdirSync(path157.dirname(mirrorDir), { recursive: true });
|
|
150343
|
+
fs98.cpSync(staticDir, mirrorDir, { recursive: true });
|
|
150344
|
+
}
|
|
150345
|
+
} catch (err2) {
|
|
150346
|
+
log.web.warn("could not refresh the web-asset mirror", {
|
|
150347
|
+
mirrorDir,
|
|
150348
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
150349
|
+
});
|
|
150350
|
+
}
|
|
150351
|
+
try {
|
|
150352
|
+
mirrorReady = fs98.statSync(path157.join(mirrorDir, "index.html")).isFile();
|
|
150353
|
+
} catch {
|
|
150354
|
+
mirrorReady = false;
|
|
150355
|
+
}
|
|
149923
150356
|
app.use(express3.static(staticDir));
|
|
150357
|
+
if (mirrorReady) app.use(express3.static(mirrorDir));
|
|
150358
|
+
const checkStaticRoot = () => {
|
|
150359
|
+
try {
|
|
150360
|
+
return fs98.statSync(path157.join(staticDir, "index.html")).isFile();
|
|
150361
|
+
} catch {
|
|
150362
|
+
return false;
|
|
150363
|
+
}
|
|
150364
|
+
};
|
|
150365
|
+
let staticRootOk = checkStaticRoot();
|
|
150366
|
+
if (!staticRootOk) {
|
|
150367
|
+
log.web.error("web assets are NOT servable at startup", { staticDir, mirrorReady });
|
|
150368
|
+
}
|
|
150369
|
+
const staticRootTimer = setInterval(() => {
|
|
150370
|
+
const ok2 = checkStaticRoot();
|
|
150371
|
+
if (ok2 === staticRootOk) return;
|
|
150372
|
+
staticRootOk = ok2;
|
|
150373
|
+
if (ok2) log.web.info("web assets are servable again", { staticDir });
|
|
150374
|
+
else log.web.error("web assets VANISHED from under the running server", { staticDir, mirrorReady });
|
|
150375
|
+
}, 6e4);
|
|
150376
|
+
staticRootTimer.unref();
|
|
150377
|
+
setStaticRootReporter(() => ({ staticDir, ok: staticRootOk, mirrorDir, mirrorReady }));
|
|
149924
150378
|
app.use((req, res, next) => {
|
|
149925
150379
|
if (req.method !== "GET" || req.path.startsWith("/api/")) return next();
|
|
149926
150380
|
if (isStaticAssetPath(req.path)) return next();
|
|
149927
|
-
res.sendFile("index.html", { root: staticDir })
|
|
150381
|
+
res.sendFile("index.html", { root: staticDir }, (err2) => {
|
|
150382
|
+
if (!err2) return;
|
|
150383
|
+
if (mirrorReady) {
|
|
150384
|
+
res.sendFile("index.html", { root: mirrorDir }, () => {
|
|
150385
|
+
});
|
|
150386
|
+
return;
|
|
150387
|
+
}
|
|
150388
|
+
next(err2);
|
|
150389
|
+
});
|
|
149928
150390
|
});
|
|
149929
150391
|
}
|
|
149930
150392
|
app.use("/api", notFoundHandler);
|
|
@@ -149944,9 +150406,21 @@ async function startServer(options = {}) {
|
|
|
149944
150406
|
}
|
|
149945
150407
|
httpServer = createServer(app);
|
|
149946
150408
|
attachWss(httpServer);
|
|
149947
|
-
|
|
149948
|
-
|
|
149949
|
-
|
|
150409
|
+
try {
|
|
150410
|
+
await new Promise((resolve, reject) => {
|
|
150411
|
+
httpServer.once("error", reject);
|
|
150412
|
+
httpServer.listen(port, () => resolve());
|
|
150413
|
+
});
|
|
150414
|
+
} catch (err2) {
|
|
150415
|
+
if (err2?.code !== "EADDRINUSE") throw err2;
|
|
150416
|
+
const already = await isWalnutAnsweringOn(port);
|
|
150417
|
+
throw new Error(
|
|
150418
|
+
already ? `Walnut is already running at http://localhost:${port} (EADDRINUSE). Open that address; there is nothing to start.` : `Port ${port} is in use by another program (EADDRINUSE). Start Walnut on a free one instead, for example:
|
|
150419
|
+
open-walnut web --port ${port + 1}`
|
|
150420
|
+
);
|
|
150421
|
+
}
|
|
150422
|
+
httpServer.on("error", (err2) => {
|
|
150423
|
+
log.web.error("http server error after listen", { error: err2 instanceof Error ? err2.message : String(err2) });
|
|
149950
150424
|
});
|
|
149951
150425
|
const label = dev ? "dev" : "production";
|
|
149952
150426
|
log.web.info(`server listening on http://localhost:${port}`, { mode: label, port });
|
|
@@ -149956,6 +150430,7 @@ async function startServer(options = {}) {
|
|
|
149956
150430
|
const { updateInstanceLockPort: updateInstanceLockPort2 } = await Promise.resolve().then(() => (init_instance_lock(), instance_lock_exports));
|
|
149957
150431
|
updateInstanceLockPort2(bound.port);
|
|
149958
150432
|
setPluginApiBase(`http://127.0.0.1:${bound.port}`);
|
|
150433
|
+
process.env.WALNUT_SERVER_URL = `http://localhost:${bound.port}`;
|
|
149959
150434
|
}
|
|
149960
150435
|
}
|
|
149961
150436
|
if (CLOUD_MODE) {
|
|
@@ -158532,6 +159007,11 @@ async function handleSessionControlRelay(action, sessionId, params) {
|
|
|
158532
159007
|
result = await retrySession2(sessionId);
|
|
158533
159008
|
break;
|
|
158534
159009
|
}
|
|
159010
|
+
case "recheck": {
|
|
159011
|
+
const { recheckSession: recheckSession2 } = await Promise.resolve().then(() => (init_session_lifecycle(), session_lifecycle_exports));
|
|
159012
|
+
result = await recheckSession2(sessionId);
|
|
159013
|
+
break;
|
|
159014
|
+
}
|
|
158535
159015
|
case "permission": {
|
|
158536
159016
|
const { respondSessionPermission: respondSessionPermission2 } = await Promise.resolve().then(() => (init_session_lifecycle(), session_lifecycle_exports));
|
|
158537
159017
|
result = await respondSessionPermission2(sessionId, p6.requestId, p6.allow, p6.message, p6.optionId, p6.answers);
|