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/web/server.js
CHANGED
|
@@ -10199,7 +10199,25 @@ function reapSession(sid, code, reason) {
|
|
|
10199
10199
|
let jsonlAgeMs = null;
|
|
10200
10200
|
try { jsonlAgeMs = Date.now() - fs.statSync(session.jsonlPath).mtimeMs; } catch {}
|
|
10201
10201
|
const cleanExit = isTurnCompleteExit(session.jsonlPath);
|
|
10202
|
-
|
|
10202
|
+
// An INTENTIONAL daemon-initiated reclamation is not a failure, and the JSONL
|
|
10203
|
+
// tail cannot tell us it happened. The idle scanner stamps idleReclaimAt
|
|
10204
|
+
// before it signals; the death then lands here through whichever path notices
|
|
10205
|
+
// it (usually orphan-poll ESRCH \u2192 code -1). Verified production case: the last
|
|
10206
|
+
// line was a control_response (the reply to a Walnut-issued control request \u2014
|
|
10207
|
+
// a routine shape), so isTurnCompleteExit said false, exitCode stayed -1, and
|
|
10208
|
+
// the projection painted a red "Error" over a healthy, fully resumable session
|
|
10209
|
+
// we reclaimed on purpose. Checked FIRST and unconditionally: the flag carries
|
|
10210
|
+
// information the tail does not contain, which is why isTurnCompleteExit is
|
|
10211
|
+
// left alone. Keep in sync with daemon-core.ts reapSession.
|
|
10212
|
+
const intentionalReclaim = session.idleReclaimAt != null;
|
|
10213
|
+
if (intentionalReclaim && code !== 0) {
|
|
10214
|
+
logMsg('info', 'reapSession: intentional idle reclamation, normalizing exit code', {
|
|
10215
|
+
sid, pid: session.pid, originalCode: code, originalReason: reason,
|
|
10216
|
+
jsonlAgeMs, cleanExit, idleReclaimAt: session.idleReclaimAt,
|
|
10217
|
+
});
|
|
10218
|
+
code = 0;
|
|
10219
|
+
reason = reason + '+intentional-idle-reclaim';
|
|
10220
|
+
} else if (code !== 0 && cleanExit) {
|
|
10203
10221
|
logMsg('info', 'reapSession: turn-complete detected, normalizing exit code', {
|
|
10204
10222
|
sid, pid: session.pid, originalCode: code, originalReason: reason, jsonlAgeMs,
|
|
10205
10223
|
});
|
|
@@ -15964,6 +15982,14 @@ function scanIdleSessions() {
|
|
|
15964
15982
|
}
|
|
15965
15983
|
const idleMinutes = Math.round(idleMs / 60000);
|
|
15966
15984
|
logMsg('warn', 'idle scan: killing idle session (no subscribers, no output)', { sid, pid, idleMinutes, protectedBy: prot.source, thresholdMs: prot.killMs, detail: prot.detail });
|
|
15985
|
+
// Record the intent BEFORE signalling: the reap arrives asynchronously
|
|
15986
|
+
// (the orphan poll sees ESRCH and reports 'orphan-poll-dead', code -1), so
|
|
15987
|
+
// by then nothing remembers that WE asked for this. Without the stamp
|
|
15988
|
+
// reapSession can only consult the JSONL tail, which says "not a clean turn
|
|
15989
|
+
// end" for any session whose last line isn't a type:result \u2014 and the
|
|
15990
|
+
// projection then shows a red Error for our own routine reclamation.
|
|
15991
|
+
// Keep in sync with daemon-standalone.ts scanIdleSessions.
|
|
15992
|
+
session.idleReclaimAt = now;
|
|
15967
15993
|
killSessionProcessGroup(pid, sid);
|
|
15968
15994
|
}
|
|
15969
15995
|
}
|
|
@@ -16647,7 +16673,20 @@ function createDaemonCore(deps) {
|
|
|
16647
16673
|
jsonlAgeMs = clock() - fs114.statSync(session2.jsonlPath).mtimeMs;
|
|
16648
16674
|
} catch {
|
|
16649
16675
|
}
|
|
16650
|
-
|
|
16676
|
+
const intentionalReclaim = session2.idleReclaimAt != null;
|
|
16677
|
+
if (intentionalReclaim && code !== 0) {
|
|
16678
|
+
logger2("info", "reapSession: intentional idle reclamation, normalizing exit code", {
|
|
16679
|
+
sid,
|
|
16680
|
+
pid: session2.pid,
|
|
16681
|
+
originalCode: code,
|
|
16682
|
+
originalReason: reason,
|
|
16683
|
+
jsonlAgeMs,
|
|
16684
|
+
cleanExit,
|
|
16685
|
+
idleReclaimAt: session2.idleReclaimAt
|
|
16686
|
+
});
|
|
16687
|
+
code = 0;
|
|
16688
|
+
reason = reason + "+intentional-idle-reclaim";
|
|
16689
|
+
} else if (cleanExit && code !== 0) {
|
|
16651
16690
|
logger2("info", "reapSession: turn-complete detected, normalizing exit code", {
|
|
16652
16691
|
sid,
|
|
16653
16692
|
pid: session2.pid,
|
|
@@ -18118,6 +18157,22 @@ var init_local_daemon = __esm({
|
|
|
18118
18157
|
});
|
|
18119
18158
|
|
|
18120
18159
|
// src/core/session-error-kind.ts
|
|
18160
|
+
var session_error_kind_exports = {};
|
|
18161
|
+
__export(session_error_kind_exports, {
|
|
18162
|
+
INFRA_STATUS_REASONS: () => INFRA_STATUS_REASONS,
|
|
18163
|
+
RESCUABLE_STOPPED_WINDOW_MS: () => RESCUABLE_STOPPED_WINDOW_MS,
|
|
18164
|
+
TERMINAL_STATUS_REASONS: () => TERMINAL_STATUS_REASONS,
|
|
18165
|
+
assertsHostUnreachable: () => assertsHostUnreachable,
|
|
18166
|
+
classifySessionError: () => classifySessionError,
|
|
18167
|
+
classifyStatusReasonKind: () => classifyStatusReasonKind,
|
|
18168
|
+
isInfraSessionError: () => isInfraSessionError,
|
|
18169
|
+
isRecoverableSessionError: () => isRecoverableSessionError,
|
|
18170
|
+
isRescuableStoppedRecord: () => isRescuableStoppedRecord
|
|
18171
|
+
});
|
|
18172
|
+
function assertsHostUnreachable(record) {
|
|
18173
|
+
if (!record.errorMessage) return false;
|
|
18174
|
+
return typeof record.status_reason === "string" && REACHABILITY_CLAIM_REASONS.has(record.status_reason);
|
|
18175
|
+
}
|
|
18121
18176
|
function classifyStatusReasonKind(reason) {
|
|
18122
18177
|
if (typeof reason !== "string") return void 0;
|
|
18123
18178
|
if (INFRA_STATUS_REASONS.has(reason)) return "infra";
|
|
@@ -18146,7 +18201,7 @@ function isRescuableStoppedRecord(record, nowMs = Date.now()) {
|
|
|
18146
18201
|
if (!Number.isFinite(changedAt)) return false;
|
|
18147
18202
|
return nowMs - changedAt <= RESCUABLE_STOPPED_WINDOW_MS;
|
|
18148
18203
|
}
|
|
18149
|
-
var INFRA_STATUS_REASONS, TERMINAL_STATUS_REASONS, INFRA_TEXT_SIGNATURES, RESCUABLE_STOPPED_WINDOW_MS;
|
|
18204
|
+
var INFRA_STATUS_REASONS, TERMINAL_STATUS_REASONS, INFRA_TEXT_SIGNATURES, REACHABILITY_CLAIM_REASONS, RESCUABLE_STOPPED_WINDOW_MS;
|
|
18150
18205
|
var init_session_error_kind = __esm({
|
|
18151
18206
|
"src/core/session-error-kind.ts"() {
|
|
18152
18207
|
"use strict";
|
|
@@ -18190,6 +18245,9 @@ var init_session_error_kind = __esm({
|
|
|
18190
18245
|
"ssh tunnel",
|
|
18191
18246
|
"broken pipe"
|
|
18192
18247
|
];
|
|
18248
|
+
REACHABILITY_CLAIM_REASONS = /* @__PURE__ */ new Set([
|
|
18249
|
+
"remote_unreachable"
|
|
18250
|
+
]);
|
|
18193
18251
|
RESCUABLE_STOPPED_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
18194
18252
|
}
|
|
18195
18253
|
});
|
|
@@ -68462,8 +68520,22 @@ var init_token_truth = __esm({
|
|
|
68462
68520
|
});
|
|
68463
68521
|
|
|
68464
68522
|
// src/utils/image-compress.ts
|
|
68465
|
-
|
|
68523
|
+
async function loadSharp() {
|
|
68524
|
+
sharpLoad ??= import("sharp").then(
|
|
68525
|
+
(m6) => m6.default ?? m6,
|
|
68526
|
+
(err2) => {
|
|
68527
|
+
log.agent.warn("sharp is not installed; images are sent uncompressed", {
|
|
68528
|
+
error: err2 instanceof Error ? err2.message : String(err2),
|
|
68529
|
+
fix: "a machine with glibc 2.28+ installs sharp prebuilt; otherwise install libvips and re-run npm install"
|
|
68530
|
+
});
|
|
68531
|
+
return null;
|
|
68532
|
+
}
|
|
68533
|
+
);
|
|
68534
|
+
return sharpLoad;
|
|
68535
|
+
}
|
|
68466
68536
|
async function clampImageDimensions(buffer2, mimeType) {
|
|
68537
|
+
const sharp = await loadSharp();
|
|
68538
|
+
if (!sharp) return { buffer: buffer2, mimeType };
|
|
68467
68539
|
try {
|
|
68468
68540
|
const meta = await sharp(buffer2).metadata();
|
|
68469
68541
|
const longest = Math.max(meta.width ?? 0, meta.height ?? 0);
|
|
@@ -68485,6 +68557,8 @@ async function compressForApi(inputBuffer, inputMimeType) {
|
|
|
68485
68557
|
if (buffer2.toString("base64").length <= MAX_BASE64_BYTES) {
|
|
68486
68558
|
return { buffer: buffer2, mimeType };
|
|
68487
68559
|
}
|
|
68560
|
+
const sharp = await loadSharp();
|
|
68561
|
+
if (!sharp) return { buffer: buffer2, mimeType };
|
|
68488
68562
|
try {
|
|
68489
68563
|
if (mimeType === "image/gif") {
|
|
68490
68564
|
const webp = await sharp(buffer2, { animated: true }).webp({ quality: 80 }).toBuffer();
|
|
@@ -68522,10 +68596,11 @@ async function compressForApi(inputBuffer, inputMimeType) {
|
|
|
68522
68596
|
return { buffer: buffer2, mimeType };
|
|
68523
68597
|
}
|
|
68524
68598
|
}
|
|
68525
|
-
var MAX_BASE64_BYTES, MAX_IMAGE_DIMENSION;
|
|
68599
|
+
var sharpLoad, MAX_BASE64_BYTES, MAX_IMAGE_DIMENSION;
|
|
68526
68600
|
var init_image_compress = __esm({
|
|
68527
68601
|
"src/utils/image-compress.ts"() {
|
|
68528
68602
|
"use strict";
|
|
68603
|
+
init_logging();
|
|
68529
68604
|
MAX_BASE64_BYTES = 5e6;
|
|
68530
68605
|
MAX_IMAGE_DIMENSION = 1568;
|
|
68531
68606
|
}
|
|
@@ -71516,9 +71591,20 @@ async function applySnapshot(sessionId, snapshot2, source) {
|
|
|
71516
71591
|
updates.errorKind = void 0;
|
|
71517
71592
|
clearSuppressedErrorReason(sessionId);
|
|
71518
71593
|
} else {
|
|
71594
|
+
let droppedDisproven = false;
|
|
71595
|
+
if (assertsHostUnreachable(record)) {
|
|
71596
|
+
updates.errorMessage = void 0;
|
|
71597
|
+
droppedDisproven = true;
|
|
71598
|
+
log.session.info("snapshot projection dropped disproven unreachability diagnosis", {
|
|
71599
|
+
sessionId,
|
|
71600
|
+
priorReason: record.status_reason ?? null,
|
|
71601
|
+
projected,
|
|
71602
|
+
source
|
|
71603
|
+
});
|
|
71604
|
+
}
|
|
71519
71605
|
const stashed = takeSuppressedErrorReason(sessionId);
|
|
71520
71606
|
if (stashed) {
|
|
71521
|
-
if (!record.errorMessage && stashed.message) updates.errorMessage = stashed.message;
|
|
71607
|
+
if ((!record.errorMessage || droppedDisproven) && stashed.message) updates.errorMessage = stashed.message;
|
|
71522
71608
|
if (!record.errorKind && stashed.kind) updates.errorKind = stashed.kind;
|
|
71523
71609
|
log.session.info("snapshot projection adopted suppressed error reason", {
|
|
71524
71610
|
sessionId,
|
|
@@ -71571,6 +71657,7 @@ var init_session_snapshot_apply = __esm({
|
|
|
71571
71657
|
"use strict";
|
|
71572
71658
|
init_logging();
|
|
71573
71659
|
init_session_snapshot_gate();
|
|
71660
|
+
init_session_error_kind();
|
|
71574
71661
|
init_engine_registry();
|
|
71575
71662
|
init_session_snapshot_gate();
|
|
71576
71663
|
TERMINAL = /* @__PURE__ */ new Set(["stopped", "error"]);
|
|
@@ -74185,9 +74272,15 @@ var init_acp_session = __esm({
|
|
|
74185
74272
|
}
|
|
74186
74273
|
async resolveArtifacts() {
|
|
74187
74274
|
if (this.cfg.artifacts) return this.cfg.artifacts;
|
|
74188
|
-
|
|
74275
|
+
const artifacts = resolveAcpArtifacts(this.engineId, {
|
|
74189
74276
|
configuredAdapterCmd: await this.resolveConfiguredAdapterCmd()
|
|
74190
74277
|
});
|
|
74278
|
+
if (!fsModule.existsSync(artifacts.workerCmd[1])) {
|
|
74279
|
+
throw new Error(
|
|
74280
|
+
`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.`
|
|
74281
|
+
);
|
|
74282
|
+
}
|
|
74283
|
+
return artifacts;
|
|
74191
74284
|
}
|
|
74192
74285
|
async establishProviderSession() {
|
|
74193
74286
|
await this.seedReplayCursor();
|
|
@@ -77724,6 +77817,7 @@ __export(session_lifecycle_exports, {
|
|
|
77724
77817
|
persistSessionModeChange: () => persistSessionModeChange,
|
|
77725
77818
|
readProviderSessionHistory: () => readProviderSessionHistory,
|
|
77726
77819
|
readSessionRichHistory: () => readSessionRichHistory,
|
|
77820
|
+
recheckSession: () => recheckSession,
|
|
77727
77821
|
respondSessionPermission: () => respondSessionPermission,
|
|
77728
77822
|
restartSession: () => restartSession,
|
|
77729
77823
|
retrySession: () => retrySession,
|
|
@@ -78061,7 +78155,7 @@ async function retrySession(sessionId) {
|
|
|
78061
78155
|
return { status: "reconnected", sessionId };
|
|
78062
78156
|
}
|
|
78063
78157
|
if (await resumeConversationExists(record)) {
|
|
78064
|
-
const {
|
|
78158
|
+
const { getQueue: getQueue2, unparkMessage: unparkMessage2 } = await Promise.resolve().then(() => (init_session_message_queue(), session_message_queue_exports));
|
|
78065
78159
|
const pendingMsgs = await getQueue2(sessionId);
|
|
78066
78160
|
for (const parked of pendingMsgs.filter((m6) => m6.status === "parked")) {
|
|
78067
78161
|
await unparkMessage2(sessionId, parked.id);
|
|
@@ -78071,9 +78165,22 @@ async function retrySession(sessionId) {
|
|
|
78071
78165
|
log.session.info("session retry: re-processing pending queue messages", { sessionId, taskId: record.taskId, count: pendingMsgs.length });
|
|
78072
78166
|
return { status: "resuming", sessionId, restoredMessages: pendingMsgs.length };
|
|
78073
78167
|
}
|
|
78074
|
-
await
|
|
78075
|
-
|
|
78076
|
-
|
|
78168
|
+
const relabelled = await updateSessionRecord2(sessionId, {
|
|
78169
|
+
process_status: "stopped",
|
|
78170
|
+
errorMessage: void 0,
|
|
78171
|
+
errorKind: void 0,
|
|
78172
|
+
activity: void 0,
|
|
78173
|
+
last_status_change: (/* @__PURE__ */ new Date()).toISOString(),
|
|
78174
|
+
status_reason: "retry_reconnect",
|
|
78175
|
+
status_changed_by: "user"
|
|
78176
|
+
});
|
|
78177
|
+
emitSessionStatusChanged2(relabelled, {}, ["*"]);
|
|
78178
|
+
log.session.info("session retry: reconnected \u2014 conversation preserved, no message sent", {
|
|
78179
|
+
sessionId,
|
|
78180
|
+
taskId: record.taskId,
|
|
78181
|
+
host: record.host ?? "__local__"
|
|
78182
|
+
});
|
|
78183
|
+
return { status: "resumable", sessionId };
|
|
78077
78184
|
}
|
|
78078
78185
|
log.session.warn("session retry: conversation never persisted \u2014 starting fresh instead of --resume", {
|
|
78079
78186
|
sessionId,
|
|
@@ -78131,6 +78238,91 @@ async function retrySession(sessionId) {
|
|
|
78131
78238
|
});
|
|
78132
78239
|
return { status: "pending", taskId: task.id, oldSessionId: sessionId };
|
|
78133
78240
|
}
|
|
78241
|
+
async function withDeadline(p6, ms) {
|
|
78242
|
+
let timer3;
|
|
78243
|
+
try {
|
|
78244
|
+
return await Promise.race([
|
|
78245
|
+
p6,
|
|
78246
|
+
new Promise((resolve) => {
|
|
78247
|
+
timer3 = setTimeout(() => resolve(RECHECK_TIMED_OUT), ms);
|
|
78248
|
+
if (timer3 && typeof timer3 === "object" && "unref" in timer3) timer3.unref();
|
|
78249
|
+
})
|
|
78250
|
+
]);
|
|
78251
|
+
} finally {
|
|
78252
|
+
if (timer3) clearTimeout(timer3);
|
|
78253
|
+
}
|
|
78254
|
+
}
|
|
78255
|
+
async function recheckSession(sessionId) {
|
|
78256
|
+
const { getSessionByClaudeId: getSessionByClaudeId2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
|
|
78257
|
+
const record = await getSessionByClaudeId2(sessionId);
|
|
78258
|
+
if (!record) throw new SessionControlError("Session not found", 404);
|
|
78259
|
+
const hostKey2 = record.host ?? "__local__";
|
|
78260
|
+
const { classifySessionError: classifySessionError2 } = await Promise.resolve().then(() => (init_session_error_kind(), session_error_kind_exports));
|
|
78261
|
+
const kind = classifySessionError2(record);
|
|
78262
|
+
const base = { sessionId, processStatus: record.process_status, infraClaim: kind === "infra" };
|
|
78263
|
+
const { isDaemonConnected: isDaemonConnected2, getPooledSnapshotConnection: getPooledSnapshotConnection2 } = await Promise.resolve().then(() => (init_daemon_connection(), daemon_connection_exports));
|
|
78264
|
+
const reachable = isDaemonConnected2(hostKey2);
|
|
78265
|
+
if (kind === "terminal") return { ...base, checked: false, reachable, reason: "terminal_error" };
|
|
78266
|
+
if (record.archived) return { ...base, checked: false, reachable, reason: "archived" };
|
|
78267
|
+
const conn = getPooledSnapshotConnection2(record.host);
|
|
78268
|
+
if (!conn) {
|
|
78269
|
+
log.session.info("session recheck: no pooled snapshot connection \u2014 nothing to ask", {
|
|
78270
|
+
sessionId,
|
|
78271
|
+
host: hostKey2,
|
|
78272
|
+
reachable
|
|
78273
|
+
});
|
|
78274
|
+
return { ...base, checked: false, reachable, reason: "no_pooled_connection" };
|
|
78275
|
+
}
|
|
78276
|
+
let raced;
|
|
78277
|
+
try {
|
|
78278
|
+
raced = await withDeadline(
|
|
78279
|
+
conn.send("getState", { sid: sessionId }, RECHECK_RPC_TIMEOUT_MS),
|
|
78280
|
+
RECHECK_RPC_TIMEOUT_MS
|
|
78281
|
+
);
|
|
78282
|
+
} catch (err2) {
|
|
78283
|
+
log.session.info("session recheck: getState failed", {
|
|
78284
|
+
sessionId,
|
|
78285
|
+
host: hostKey2,
|
|
78286
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
78287
|
+
});
|
|
78288
|
+
return { ...base, checked: false, reachable, reason: "rpc_failed" };
|
|
78289
|
+
}
|
|
78290
|
+
if (raced === RECHECK_TIMED_OUT) {
|
|
78291
|
+
log.session.warn("session recheck: getState timed out", {
|
|
78292
|
+
sessionId,
|
|
78293
|
+
host: hostKey2,
|
|
78294
|
+
timeoutMs: RECHECK_RPC_TIMEOUT_MS
|
|
78295
|
+
});
|
|
78296
|
+
return { ...base, checked: false, reachable, reason: "timeout" };
|
|
78297
|
+
}
|
|
78298
|
+
const snapshot2 = raced?.ok ? raced.snapshot : void 0;
|
|
78299
|
+
if (!snapshot2) {
|
|
78300
|
+
return { ...base, checked: false, reachable: true, reason: "no_snapshot" };
|
|
78301
|
+
}
|
|
78302
|
+
const { applySnapshot: applySnapshot2 } = await Promise.resolve().then(() => (init_session_snapshot_apply(), session_snapshot_apply_exports));
|
|
78303
|
+
const outcome = await applySnapshot2(sessionId, snapshot2, "recheck-open");
|
|
78304
|
+
const after = await getSessionByClaudeId2(sessionId);
|
|
78305
|
+
const processStatus = after?.process_status ?? record.process_status;
|
|
78306
|
+
log.session.info("session recheck: reconciled against host", {
|
|
78307
|
+
sessionId,
|
|
78308
|
+
host: hostKey2,
|
|
78309
|
+
previous: record.process_status,
|
|
78310
|
+
processStatus,
|
|
78311
|
+
cliState: snapshot2.cliState,
|
|
78312
|
+
applyOutcome: outcome.outcome
|
|
78313
|
+
});
|
|
78314
|
+
return {
|
|
78315
|
+
sessionId,
|
|
78316
|
+
checked: true,
|
|
78317
|
+
reachable: true,
|
|
78318
|
+
alive: snapshot2.cliState !== "dead",
|
|
78319
|
+
processStatus,
|
|
78320
|
+
// Re-classify against the CURRENT record: a converged row may no longer
|
|
78321
|
+
// carry the claim, and the client uses this to decide whether the sentence
|
|
78322
|
+
// on screen is still true.
|
|
78323
|
+
infraClaim: classifySessionError2(after ?? record) === "infra"
|
|
78324
|
+
};
|
|
78325
|
+
}
|
|
78134
78326
|
async function respondSessionPermission(sessionId, requestId, allow, denyMessage, optionId, answers) {
|
|
78135
78327
|
if (typeof requestId !== "string" || !requestId || typeof allow !== "boolean") {
|
|
78136
78328
|
throw new SessionControlError("requestId (string) and allow (boolean) are required", 400);
|
|
@@ -78424,7 +78616,7 @@ async function readSessionRichHistory(sessionId, tail) {
|
|
|
78424
78616
|
...historyWindowed ? { windowed: true } : {}
|
|
78425
78617
|
};
|
|
78426
78618
|
}
|
|
78427
|
-
var CLAUDE_SESSION_MODES, MAX_PINNED_MESSAGES, MAX_PIN_LABEL_CHARS, MAX_PIN_ID_CHARS, GIT_BASES, MAX_FORK_DEPTH, HISTORY_STARTUP_GRACE_MS;
|
|
78619
|
+
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;
|
|
78428
78620
|
var init_session_lifecycle = __esm({
|
|
78429
78621
|
"src/core/sessions/session-lifecycle.ts"() {
|
|
78430
78622
|
"use strict";
|
|
@@ -78438,6 +78630,8 @@ var init_session_lifecycle = __esm({
|
|
|
78438
78630
|
MAX_PINNED_MESSAGES = 200;
|
|
78439
78631
|
MAX_PIN_LABEL_CHARS = 300;
|
|
78440
78632
|
MAX_PIN_ID_CHARS = 200;
|
|
78633
|
+
RECHECK_RPC_TIMEOUT_MS = 5e3;
|
|
78634
|
+
RECHECK_TIMED_OUT = /* @__PURE__ */ Symbol("recheck-timeout");
|
|
78441
78635
|
GIT_BASES = /* @__PURE__ */ new Set(["uncommitted", "previous", "remote"]);
|
|
78442
78636
|
MAX_FORK_DEPTH = 5;
|
|
78443
78637
|
HISTORY_STARTUP_GRACE_MS = 2 * 60 * 1e3;
|
|
@@ -87820,7 +88014,7 @@ function isRevealSecretPath(resolved) {
|
|
|
87820
88014
|
];
|
|
87821
88015
|
return denied.some((d8) => resolved === d8 || resolved.startsWith(d8 + path70.sep)) || /(^|\/)config\.ya?ml$/.test(resolved);
|
|
87822
88016
|
}
|
|
87823
|
-
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;
|
|
88017
|
+
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;
|
|
87824
88018
|
var init_files2 = __esm({
|
|
87825
88019
|
"src/web/routes/files.ts"() {
|
|
87826
88020
|
"use strict";
|
|
@@ -87953,6 +88147,54 @@ var init_files2 = __esm({
|
|
|
87953
88147
|
next(err2);
|
|
87954
88148
|
}
|
|
87955
88149
|
});
|
|
88150
|
+
LIST_MANY_MAX_PATHS = 64;
|
|
88151
|
+
LIST_MANY_CONCURRENCY = 8;
|
|
88152
|
+
LIST_MANY_DEADLINE_MS = 8e3;
|
|
88153
|
+
filesRouter.post("/list-many", async (req, res, next) => {
|
|
88154
|
+
try {
|
|
88155
|
+
const body3 = req.body ?? {};
|
|
88156
|
+
const raw = Array.isArray(body3.paths) ? body3.paths : null;
|
|
88157
|
+
if (!raw) {
|
|
88158
|
+
res.status(400).json({ error: "paths (string[]) is required" });
|
|
88159
|
+
return;
|
|
88160
|
+
}
|
|
88161
|
+
const host = typeof body3.host === "string" && body3.host ? body3.host : void 0;
|
|
88162
|
+
const showHidden = body3.showHidden === true || body3.showHidden === "1";
|
|
88163
|
+
const paths = [...new Set(
|
|
88164
|
+
raw.filter((p6) => typeof p6 === "string" && !!p6)
|
|
88165
|
+
)];
|
|
88166
|
+
const truncated = paths.length > LIST_MANY_MAX_PATHS;
|
|
88167
|
+
const wanted = paths.slice(0, LIST_MANY_MAX_PATHS);
|
|
88168
|
+
const deadline4 = Date.now() + LIST_MANY_DEADLINE_MS;
|
|
88169
|
+
const listings = [];
|
|
88170
|
+
let timedOut = false;
|
|
88171
|
+
let next_ = 0;
|
|
88172
|
+
const worker = async () => {
|
|
88173
|
+
for (; ; ) {
|
|
88174
|
+
const i8 = next_++;
|
|
88175
|
+
if (i8 >= wanted.length) return;
|
|
88176
|
+
if (Date.now() >= deadline4) {
|
|
88177
|
+
timedOut = true;
|
|
88178
|
+
return;
|
|
88179
|
+
}
|
|
88180
|
+
const dirPath = wanted[i8];
|
|
88181
|
+
try {
|
|
88182
|
+
listings.push(await listSessionFiles(dirPath, host, showHidden));
|
|
88183
|
+
} catch (err2) {
|
|
88184
|
+
listings.push({ path: dirPath, error: err2 instanceof Error ? err2.message : String(err2) });
|
|
88185
|
+
}
|
|
88186
|
+
}
|
|
88187
|
+
};
|
|
88188
|
+
await Promise.all(Array.from({ length: Math.min(LIST_MANY_CONCURRENCY, wanted.length) }, worker));
|
|
88189
|
+
res.json({ listings, truncated, timedOut });
|
|
88190
|
+
} catch (err2) {
|
|
88191
|
+
if (err2 instanceof FilesOpError) {
|
|
88192
|
+
res.status(err2.statusCode).json({ error: err2.message });
|
|
88193
|
+
return;
|
|
88194
|
+
}
|
|
88195
|
+
next(err2);
|
|
88196
|
+
}
|
|
88197
|
+
});
|
|
87956
88198
|
REVEAL_APP_DENY_EXTS = /* @__PURE__ */ new Set([
|
|
87957
88199
|
"command",
|
|
87958
88200
|
"sh",
|
|
@@ -115976,6 +116218,11 @@ async function handleSessionControlRelay(action, sessionId, params) {
|
|
|
115976
116218
|
result = await retrySession2(sessionId);
|
|
115977
116219
|
break;
|
|
115978
116220
|
}
|
|
116221
|
+
case "recheck": {
|
|
116222
|
+
const { recheckSession: recheckSession2 } = await Promise.resolve().then(() => (init_session_lifecycle(), session_lifecycle_exports));
|
|
116223
|
+
result = await recheckSession2(sessionId);
|
|
116224
|
+
break;
|
|
116225
|
+
}
|
|
115979
116226
|
case "permission": {
|
|
115980
116227
|
const { respondSessionPermission: respondSessionPermission2 } = await Promise.resolve().then(() => (init_session_lifecycle(), session_lifecycle_exports));
|
|
115981
116228
|
result = await respondSessionPermission2(sessionId, p6.requestId, p6.allow, p6.message, p6.optionId, p6.answers);
|
|
@@ -153537,6 +153784,19 @@ var init_sessions = __esm({
|
|
|
153537
153784
|
next(err2);
|
|
153538
153785
|
}
|
|
153539
153786
|
});
|
|
153787
|
+
sessionsRouter.post("/:sessionId/recheck", async (req, res, next) => {
|
|
153788
|
+
try {
|
|
153789
|
+
const sessionId = req.params.sessionId;
|
|
153790
|
+
const result = await recheckSession(sessionId);
|
|
153791
|
+
res.json(result);
|
|
153792
|
+
} catch (err2) {
|
|
153793
|
+
if (err2 instanceof SessionControlError) {
|
|
153794
|
+
res.status(err2.statusCode).json({ error: err2.message });
|
|
153795
|
+
return;
|
|
153796
|
+
}
|
|
153797
|
+
next(err2);
|
|
153798
|
+
}
|
|
153799
|
+
});
|
|
153540
153800
|
sessionsRouter.post("/:sessionId/restart", async (req, res, next) => {
|
|
153541
153801
|
const sessionId = req.params.sessionId;
|
|
153542
153802
|
const startedAt = Date.now();
|
|
@@ -155260,9 +155520,13 @@ var init_credential_verify = __esm({
|
|
|
155260
155520
|
var config_exports = {};
|
|
155261
155521
|
__export(config_exports, {
|
|
155262
155522
|
buildProvidersPayload: () => buildProvidersPayload,
|
|
155263
|
-
configRouter: () => configRouter
|
|
155523
|
+
configRouter: () => configRouter,
|
|
155524
|
+
setStaticRootReporter: () => setStaticRootReporter
|
|
155264
155525
|
});
|
|
155265
155526
|
import { Router as Router20 } from "express";
|
|
155527
|
+
function setStaticRootReporter(fn) {
|
|
155528
|
+
staticRootReporter = fn;
|
|
155529
|
+
}
|
|
155266
155530
|
async function fetchOllamaModels(baseUrl) {
|
|
155267
155531
|
if (Date.now() - _ollamaCache.ts < OLLAMA_CACHE_TTL) return _ollamaCache.models;
|
|
155268
155532
|
const url = (baseUrl ?? DEFAULT_BASE_URLS["ollama"] ?? "http://localhost:11434") + "/api/tags";
|
|
@@ -155395,7 +155659,7 @@ async function buildProvidersPayload() {
|
|
|
155395
155659
|
}
|
|
155396
155660
|
return { providers };
|
|
155397
155661
|
}
|
|
155398
|
-
var configRouter, _ollamaCache, OLLAMA_CACHE_TTL;
|
|
155662
|
+
var configRouter, staticRootReporter, _ollamaCache, OLLAMA_CACHE_TTL;
|
|
155399
155663
|
var init_config11 = __esm({
|
|
155400
155664
|
"src/web/routes/config.ts"() {
|
|
155401
155665
|
"use strict";
|
|
@@ -155410,6 +155674,7 @@ var init_config11 = __esm({
|
|
|
155410
155674
|
init_defaults();
|
|
155411
155675
|
init_config_redact();
|
|
155412
155676
|
configRouter = Router20();
|
|
155677
|
+
staticRootReporter = null;
|
|
155413
155678
|
configRouter.get("/", async (_req, res, next) => {
|
|
155414
155679
|
try {
|
|
155415
155680
|
const config = await getConfig();
|
|
@@ -155436,7 +155701,8 @@ var init_config11 = __esm({
|
|
|
155436
155701
|
remoteIdUniquenessGaps = getExtIndexUniquenessGaps2();
|
|
155437
155702
|
} catch {
|
|
155438
155703
|
}
|
|
155439
|
-
|
|
155704
|
+
const webAssets = staticRootReporter?.() ?? null;
|
|
155705
|
+
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 });
|
|
155440
155706
|
} catch (err2) {
|
|
155441
155707
|
next(err2);
|
|
155442
155708
|
}
|
|
@@ -159226,6 +159492,9 @@ var init_session_chat = __esm({
|
|
|
159226
159492
|
|
|
159227
159493
|
// src/web/routes/browser-logs.ts
|
|
159228
159494
|
import { Router as Router31 } from "express";
|
|
159495
|
+
function entrySubsystem(entry) {
|
|
159496
|
+
return entry.subsystem && ENTRY_SUBSYSTEMS.has(entry.subsystem) ? entry.subsystem : "browser";
|
|
159497
|
+
}
|
|
159229
159498
|
function checkRate(client, entryCount) {
|
|
159230
159499
|
const now = Date.now();
|
|
159231
159500
|
let state9 = clientRates.get(client);
|
|
@@ -159267,10 +159536,11 @@ function writeEntries(entries) {
|
|
|
159267
159536
|
if (entry.args) meta.args = truncate(entry.args, 1e3);
|
|
159268
159537
|
if (entry.url) meta.url = entry.url;
|
|
159269
159538
|
if (entry.count && entry.count > 1) meta.count = entry.count;
|
|
159539
|
+
if (entry.client === "mac-app") meta.client = entry.client;
|
|
159270
159540
|
writeLogEntry({
|
|
159271
159541
|
time: entry.time,
|
|
159272
159542
|
level: mapLevel(entry.level),
|
|
159273
|
-
subsystem:
|
|
159543
|
+
subsystem: entrySubsystem(entry),
|
|
159274
159544
|
message: truncate(entry.message, 2e3),
|
|
159275
159545
|
browserLevel: entry.level,
|
|
159276
159546
|
// preserve original level (e.g. 'log' vs 'info')
|
|
@@ -159293,13 +159563,14 @@ function registerBrowserLogsRpc() {
|
|
|
159293
159563
|
writeEntries(entries);
|
|
159294
159564
|
});
|
|
159295
159565
|
}
|
|
159296
|
-
var RATE_WINDOW_MS, RATE_LIMIT, clientRates, REST_MAX_ENTRIES, browserLogsRouter;
|
|
159566
|
+
var ENTRY_SUBSYSTEMS, RATE_WINDOW_MS, RATE_LIMIT, clientRates, REST_MAX_ENTRIES, browserLogsRouter;
|
|
159297
159567
|
var init_browser_logs = __esm({
|
|
159298
159568
|
"src/web/routes/browser-logs.ts"() {
|
|
159299
159569
|
"use strict";
|
|
159300
159570
|
init_handler();
|
|
159301
159571
|
init_logging();
|
|
159302
159572
|
init_logger();
|
|
159573
|
+
ENTRY_SUBSYSTEMS = /* @__PURE__ */ new Set(["browser", "desktop"]);
|
|
159303
159574
|
RATE_WINDOW_MS = 1e4;
|
|
159304
159575
|
RATE_LIMIT = 500;
|
|
159305
159576
|
clientRates = /* @__PURE__ */ new WeakMap();
|
|
@@ -163643,6 +163914,7 @@ var init_session_health_monitor = __esm({
|
|
|
163643
163914
|
log.session.info("health monitor: drained orphan dead-pool", { orphanCount: orphanIds.length, written: written.length });
|
|
163644
163915
|
}
|
|
163645
163916
|
if (sessions3.length === 0) {
|
|
163917
|
+
if (!ctx.overBudget()) await this.checkSnapshotPull(sessions3, ctx, allSessions);
|
|
163646
163918
|
const total = Date.now() - checkT0;
|
|
163647
163919
|
if (total > 500) log.session.warn("health monitor: check() slow (no active sessions)", { totalMs: total, orphanMs: tOrphan - checkT0, recoverMs: tRecover - tOrphan });
|
|
163648
163920
|
return;
|
|
@@ -163672,7 +163944,7 @@ var init_session_health_monitor = __esm({
|
|
|
163672
163944
|
await this.reconcilePendingBackgroundTasks(sessions3);
|
|
163673
163945
|
endPhase("pendingBgTasks");
|
|
163674
163946
|
if (ctx.overBudget()) return;
|
|
163675
|
-
await this.checkSnapshotPull(sessions3, ctx);
|
|
163947
|
+
await this.checkSnapshotPull(sessions3, ctx, allSessions);
|
|
163676
163948
|
endPhase("snapshotPull");
|
|
163677
163949
|
if (ctx.overBudget()) return;
|
|
163678
163950
|
const reconciledIds = await this.reconcileStuckRunningSessions(sessions3, taskMap);
|
|
@@ -164166,15 +164438,39 @@ var init_session_health_monitor = __esm({
|
|
|
164166
164438
|
bus.emit(EventNames.SESSION_WILL_REAP, payload, ["*"], { source: "health-monitor" });
|
|
164167
164439
|
}
|
|
164168
164440
|
/**
|
|
164169
|
-
* C2 snapshot pull channel (contract §5): every 30s tick,
|
|
164170
|
-
*
|
|
164171
|
-
*
|
|
164172
|
-
*
|
|
164173
|
-
*
|
|
164441
|
+
* C2 snapshot pull channel (contract §5): every 30s tick, PULL getState from
|
|
164442
|
+
* the host's POOLED CONNECTED DaemonConnection advertising 'snapshot-v1' and
|
|
164443
|
+
* feed the answer to applySnapshot. NEVER dials a new connection, always
|
|
164444
|
+
* sequential. TWO candidate classes with SEPARATE budgets and cadences:
|
|
164445
|
+
*
|
|
164446
|
+
* - LIVE — {running, idle}: 10 sids/tick, 25s per-sid spacing. The steady
|
|
164447
|
+
* state channel.
|
|
164448
|
+
* - RE-EXAMINATION — {error, stopped} whose cause is NOT positively terminal
|
|
164449
|
+
* (classifySessionError) and whose last_status_change is inside
|
|
164450
|
+
* RESCUABLE_STOPPED_WINDOW_MS: 5 sids/tick, 5 min per-sid spacing. Same
|
|
164451
|
+
* engine/provider/awaiting_spawn/archived exclusions as the live class.
|
|
164174
164452
|
*
|
|
164175
|
-
*
|
|
164453
|
+
* WHY RE-EXAMINATION EXISTS — do not "optimize" the error class back out. A
|
|
164454
|
+
* wrongly-'error' record was a two-sided deadlock: this lane is the ONE writer
|
|
164455
|
+
* the enforce-mode gate sanctions for a snapshot-covered session, and while it
|
|
164456
|
+
* looked at live records only it never re-derived status for an 'error' one.
|
|
164457
|
+
* recoverInfraFailedSessions DOES look, but the fix it writes
|
|
164458
|
+
* (('health-monitor','auto_recovered_dead')) is itself category-①, so the gate
|
|
164459
|
+
* drops the whole patch — the lane allowed to write refused to look, and the
|
|
164460
|
+
* lane that looked was not allowed to write. Records froze in 'error' with
|
|
164461
|
+
* "Connection lost — unable to reach remote host" for hours after their host
|
|
164462
|
+
* came back (2026-09-03, a remote dev host whose tunnel flapped: prose still on
|
|
164463
|
+
* the record 2h after the reconnect), burning a 30s log-churn loop the whole
|
|
164464
|
+
* time. Terminality is decided STRUCTURALLY, never by matching errorMessage
|
|
164465
|
+
* prose — see the header of session-error-kind.ts for why that ban exists.
|
|
164176
164466
|
*
|
|
164177
|
-
*
|
|
164467
|
+
* Re-examination candidates come from a WIDER pool than the live ones: the
|
|
164468
|
+
* tick's own working set drops every 'error' row (isTerminalSession), so the
|
|
164469
|
+
* caller passes the unfiltered health-scan set for this class only.
|
|
164470
|
+
*
|
|
164471
|
+
* THREE fairness/budget properties both classes must keep (C8/C9/C12/C29):
|
|
164472
|
+
*
|
|
164473
|
+
* - The TickContext budget is honored INSIDE the loop (same discipline as
|
|
164178
164474
|
* checkHungSessions). Each iteration is a real daemon RPC with a
|
|
164179
164475
|
* probe-timeout ceiling, so 10 slow hosts could burn 10 × the probe
|
|
164180
164476
|
* timeout sequentially BEFORE the authoritative reconcile phases that run
|
|
@@ -164183,70 +164479,101 @@ var init_session_health_monitor = __esm({
|
|
|
164183
164479
|
* next tick (see the rotation below).
|
|
164184
164480
|
*
|
|
164185
164481
|
* - Candidates are ordered by lastPullAt ASCENDING (never-pulled first),
|
|
164186
|
-
* which turns
|
|
164187
|
-
*
|
|
164188
|
-
*
|
|
164189
|
-
*
|
|
164482
|
+
* which turns each cap into a round-robin. In list order the SAME first ten
|
|
164483
|
+
* sids were pulled every tick and — because the 25s spacing is shorter than
|
|
164484
|
+
* the 30s cadence — sids 11+ were never pulled at all: the pull channel
|
|
164485
|
+
* silently did not exist for them. Each class rotates on its OWN timestamps.
|
|
164486
|
+
*
|
|
164487
|
+
* - The classes cannot spend each other's budget. Re-examination is
|
|
164488
|
+
* speculative ("is this record still true?") and a settled-death cohort
|
|
164489
|
+
* dwarfs the live set (the live DB measured 100 stopped candidates against
|
|
164490
|
+
* 4 error rows), so it must cost a handful of cheap RPCs per tick and
|
|
164491
|
+
* nothing more — the same reasoning as MAX_STOPPED_PROBES_PER_TICK below.
|
|
164190
164492
|
*/
|
|
164191
164493
|
snapshotPullAt = /* @__PURE__ */ new Map();
|
|
164192
|
-
|
|
164494
|
+
/** Separate from snapshotPullAt on purpose: the two classes rotate
|
|
164495
|
+
* independently, so a live pull can never reset the slow re-examination
|
|
164496
|
+
* cadence (or vice versa) when a record moves between them. */
|
|
164497
|
+
snapshotReexamPullAt = /* @__PURE__ */ new Map();
|
|
164498
|
+
async checkSnapshotPull(sessions3, ctx, reexamPool = sessions3) {
|
|
164193
164499
|
const PULL_MIN_GAP_MS = 25e3;
|
|
164194
164500
|
const MAX_PULLS_PER_TICK = 10;
|
|
164501
|
+
const REEXAM_MIN_GAP_MS = 5 * 6e4;
|
|
164502
|
+
const MAX_REEXAM_PULLS_PER_TICK = 5;
|
|
164195
164503
|
try {
|
|
164196
164504
|
const { getSnapshotStatusMode: getSnapshotStatusMode2 } = await Promise.resolve().then(() => (init_session_snapshot_gate(), session_snapshot_gate_exports));
|
|
164197
164505
|
if (getSnapshotStatusMode2() === "off") return;
|
|
164198
164506
|
const { getPooledSnapshotConnection: getPooledSnapshotConnection2 } = await Promise.resolve().then(() => (init_daemon_connection(), daemon_connection_exports));
|
|
164199
164507
|
const { applySnapshot: applySnapshot2 } = await Promise.resolve().then(() => (init_session_snapshot_apply(), session_snapshot_apply_exports));
|
|
164200
164508
|
const now = Date.now();
|
|
164201
|
-
const
|
|
164202
|
-
if (s4.
|
|
164509
|
+
const pullable = (s4) => {
|
|
164510
|
+
if (s4.archived) return false;
|
|
164203
164511
|
if (!engineCaps(s4.engine).snapshotPull) return false;
|
|
164204
164512
|
if (s4.provider === "embedded" || s4.provider === "sdk") return false;
|
|
164205
|
-
|
|
164206
|
-
|
|
164207
|
-
|
|
164208
|
-
|
|
164209
|
-
|
|
164210
|
-
|
|
164211
|
-
|
|
164212
|
-
|
|
164213
|
-
|
|
164214
|
-
|
|
164215
|
-
|
|
164216
|
-
|
|
164217
|
-
|
|
164218
|
-
|
|
164219
|
-
|
|
164220
|
-
|
|
164221
|
-
|
|
164222
|
-
|
|
164223
|
-
|
|
164224
|
-
|
|
164225
|
-
|
|
164226
|
-
|
|
164227
|
-
|
|
164228
|
-
|
|
164229
|
-
|
|
164230
|
-
|
|
164231
|
-
|
|
164232
|
-
|
|
164233
|
-
|
|
164234
|
-
|
|
164235
|
-
|
|
164236
|
-
|
|
164237
|
-
|
|
164238
|
-
|
|
164239
|
-
|
|
164240
|
-
|
|
164241
|
-
|
|
164513
|
+
return s4.status_reason !== "awaiting_spawn";
|
|
164514
|
+
};
|
|
164515
|
+
const byPullAge = (at) => (a8, b8) => (at.get(a8.claudeSessionId) ?? 0) - (at.get(b8.claudeSessionId) ?? 0);
|
|
164516
|
+
const live = sessions3.filter((s4) => {
|
|
164517
|
+
if (s4.process_status !== "running" && s4.process_status !== "idle") return false;
|
|
164518
|
+
if (!pullable(s4)) return false;
|
|
164519
|
+
return now - (this.snapshotPullAt.get(s4.claudeSessionId) ?? 0) >= PULL_MIN_GAP_MS;
|
|
164520
|
+
}).sort(byPullAge(this.snapshotPullAt));
|
|
164521
|
+
const reexam = reexamPool.filter((s4) => {
|
|
164522
|
+
if (s4.process_status !== "error" && s4.process_status !== "stopped") return false;
|
|
164523
|
+
if (!pullable(s4)) return false;
|
|
164524
|
+
if (classifySessionError(s4) === "terminal") return false;
|
|
164525
|
+
const changedAt = s4.last_status_change ? Date.parse(s4.last_status_change) : NaN;
|
|
164526
|
+
if (!Number.isFinite(changedAt)) return false;
|
|
164527
|
+
if (now - changedAt > RESCUABLE_STOPPED_WINDOW_MS) return false;
|
|
164528
|
+
return now - (this.snapshotReexamPullAt.get(s4.claudeSessionId) ?? 0) >= REEXAM_MIN_GAP_MS;
|
|
164529
|
+
}).sort(byPullAge(this.snapshotReexamPullAt));
|
|
164530
|
+
let abandoned = false;
|
|
164531
|
+
const runClass = async (candidates, cap2, at, candidateClass) => {
|
|
164532
|
+
let pulled = 0;
|
|
164533
|
+
for (const s4 of candidates) {
|
|
164534
|
+
if (ctx?.overBudget()) {
|
|
164535
|
+
abandoned = true;
|
|
164536
|
+
log.session.warn("health monitor: checkSnapshotPull abandoned mid-loop (over budget)", {
|
|
164537
|
+
pulled,
|
|
164538
|
+
candidateCount: candidates.length,
|
|
164539
|
+
candidateClass
|
|
164540
|
+
});
|
|
164541
|
+
break;
|
|
164542
|
+
}
|
|
164543
|
+
const conn = getPooledSnapshotConnection2(s4.host);
|
|
164544
|
+
if (!conn) continue;
|
|
164545
|
+
if (pulled >= cap2) {
|
|
164546
|
+
log.session.info("health monitor: snapshot pull capped this tick", {
|
|
164547
|
+
cap: cap2,
|
|
164548
|
+
sessionCount: sessions3.length,
|
|
164549
|
+
candidateCount: candidates.length,
|
|
164550
|
+
candidateClass
|
|
164551
|
+
});
|
|
164552
|
+
break;
|
|
164553
|
+
}
|
|
164554
|
+
pulled++;
|
|
164555
|
+
at.set(s4.claudeSessionId, now);
|
|
164556
|
+
try {
|
|
164557
|
+
const resp = await probeWithTimeout(
|
|
164558
|
+
conn.send("getState", { sid: s4.claudeSessionId }),
|
|
164559
|
+
null,
|
|
164560
|
+
"snapshot-pull-getState",
|
|
164561
|
+
s4.claudeSessionId
|
|
164562
|
+
);
|
|
164563
|
+
const snapshot2 = resp?.ok ? resp.snapshot : void 0;
|
|
164564
|
+
if (snapshot2) await applySnapshot2(s4.claudeSessionId, snapshot2, "pull-30s");
|
|
164565
|
+
} catch (err2) {
|
|
164566
|
+
log.session.debug("health monitor: snapshot pull failed", {
|
|
164567
|
+
sessionId: s4.claudeSessionId,
|
|
164568
|
+
host: s4.host ?? "__local__",
|
|
164569
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
164570
|
+
});
|
|
164571
|
+
}
|
|
164242
164572
|
}
|
|
164243
|
-
}
|
|
164244
|
-
|
|
164245
|
-
|
|
164246
|
-
|
|
164247
|
-
sessionCount: sessions3.length,
|
|
164248
|
-
candidateCount: candidates.length
|
|
164249
|
-
});
|
|
164573
|
+
};
|
|
164574
|
+
await runClass(live, MAX_PULLS_PER_TICK, this.snapshotPullAt, "live");
|
|
164575
|
+
if (!abandoned) {
|
|
164576
|
+
await runClass(reexam, MAX_REEXAM_PULLS_PER_TICK, this.snapshotReexamPullAt, "reexam");
|
|
164250
164577
|
}
|
|
164251
164578
|
if (this.snapshotPullAt.size > 200) {
|
|
164252
164579
|
const liveIds = new Set(sessions3.map((s4) => s4.claudeSessionId));
|
|
@@ -164254,6 +164581,12 @@ var init_session_health_monitor = __esm({
|
|
|
164254
164581
|
if (!liveIds.has(sid)) this.snapshotPullAt.delete(sid);
|
|
164255
164582
|
}
|
|
164256
164583
|
}
|
|
164584
|
+
if (this.snapshotReexamPullAt.size > 200) {
|
|
164585
|
+
const poolIds = new Set(reexamPool.map((s4) => s4.claudeSessionId));
|
|
164586
|
+
for (const sid of this.snapshotReexamPullAt.keys()) {
|
|
164587
|
+
if (!poolIds.has(sid)) this.snapshotReexamPullAt.delete(sid);
|
|
164588
|
+
}
|
|
164589
|
+
}
|
|
164257
164590
|
} catch (err2) {
|
|
164258
164591
|
log.session.warn("health monitor: checkSnapshotPull failed", {
|
|
164259
164592
|
error: err2 instanceof Error ? err2.message : String(err2)
|
|
@@ -164416,6 +164749,31 @@ var init_session_health_monitor = __esm({
|
|
|
164416
164749
|
});
|
|
164417
164750
|
}
|
|
164418
164751
|
}
|
|
164752
|
+
/**
|
|
164753
|
+
* Can a scheduled auto-recovery actually FIRE for this record?
|
|
164754
|
+
*
|
|
164755
|
+
* fire() (session-auto-recover) hard-requires the linked task to still be
|
|
164756
|
+
* IN_PROGRESS — AGENT_COMPLETE means the work was already handed back and
|
|
164757
|
+
* resuming would talk over the human. Arming a recovery that cannot pass that
|
|
164758
|
+
* check is not free: the 30s tick re-armed it forever and each arming logged
|
|
164759
|
+
* an abort 20s later (measured running for 2+ hours on one session), while
|
|
164760
|
+
* `if (armed) continue` also skipped the phase sync that IS appropriate there.
|
|
164761
|
+
*
|
|
164762
|
+
* A lookup that cannot answer (no task id, deleted task, DB error) returns
|
|
164763
|
+
* true: only a KNOWN non-IN_PROGRESS phase blocks arming, so a transient read
|
|
164764
|
+
* failure can never disable recovery. schedule()'s own guards still apply.
|
|
164765
|
+
*/
|
|
164766
|
+
async autoRecoverCanFire(s4) {
|
|
164767
|
+
if (!s4.taskId) return true;
|
|
164768
|
+
try {
|
|
164769
|
+
const { getTask: getTask2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
|
|
164770
|
+
const task = await getTask2(s4.taskId);
|
|
164771
|
+
if (!task) return true;
|
|
164772
|
+
return task.phase === "IN_PROGRESS";
|
|
164773
|
+
} catch {
|
|
164774
|
+
return true;
|
|
164775
|
+
}
|
|
164776
|
+
}
|
|
164419
164777
|
/**
|
|
164420
164778
|
* Auto-recover remote sessions stuck in 'error' with "Connection lost" message.
|
|
164421
164779
|
*
|
|
@@ -164502,28 +164860,31 @@ var init_session_health_monitor = __esm({
|
|
|
164502
164860
|
} else if (wasStopped) {
|
|
164503
164861
|
continue;
|
|
164504
164862
|
} else {
|
|
164505
|
-
const
|
|
164506
|
-
|
|
164507
|
-
|
|
164508
|
-
|
|
164509
|
-
|
|
164510
|
-
|
|
164511
|
-
|
|
164512
|
-
|
|
164513
|
-
|
|
164514
|
-
|
|
164515
|
-
|
|
164516
|
-
|
|
164517
|
-
|
|
164518
|
-
|
|
164519
|
-
|
|
164520
|
-
|
|
164521
|
-
|
|
164522
|
-
|
|
164523
|
-
|
|
164524
|
-
|
|
164863
|
+
const { isSnapshotCovered: isSnapshotCovered2 } = await Promise.resolve().then(() => (init_session_snapshot_gate(), session_snapshot_gate_exports));
|
|
164864
|
+
if (!isSnapshotCovered2(s4.claudeSessionId)) {
|
|
164865
|
+
const updated = await updateSessionRecord2(s4.claudeSessionId, {
|
|
164866
|
+
process_status: "stopped",
|
|
164867
|
+
errorMessage: void 0,
|
|
164868
|
+
errorKind: "infra",
|
|
164869
|
+
activity: void 0,
|
|
164870
|
+
last_status_change: now,
|
|
164871
|
+
status_reason: "auto_recovered_dead",
|
|
164872
|
+
status_changed_by: "health-monitor"
|
|
164873
|
+
});
|
|
164874
|
+
emitSessionStatusChanged(
|
|
164875
|
+
updated,
|
|
164876
|
+
{},
|
|
164877
|
+
["*"],
|
|
164878
|
+
{ source: "health-monitor", urgency: "urgent" }
|
|
164879
|
+
);
|
|
164880
|
+
log.session.info("health monitor: auto-recovered connection-lost session (process dead)", {
|
|
164881
|
+
sessionId: s4.claudeSessionId,
|
|
164882
|
+
host: s4.host,
|
|
164883
|
+
alive: false
|
|
164884
|
+
});
|
|
164885
|
+
}
|
|
164525
164886
|
const { scheduleSessionAutoRecover: scheduleSessionAutoRecover2 } = await Promise.resolve().then(() => (init_session_auto_recover(), session_auto_recover_exports));
|
|
164526
|
-
const armed = scheduleSessionAutoRecover2(s4, s4.status_reason ?? "daemon_reported_exit");
|
|
164887
|
+
const armed = await this.autoRecoverCanFire(s4) && scheduleSessionAutoRecover2(s4, s4.status_reason ?? "daemon_reported_exit");
|
|
164527
164888
|
if (armed) continue;
|
|
164528
164889
|
if (s4.taskId) {
|
|
164529
164890
|
try {
|
|
@@ -172673,6 +173034,20 @@ var init_session_lifecycle_v1 = __esm({
|
|
|
172673
173034
|
next(err2);
|
|
172674
173035
|
}
|
|
172675
173036
|
});
|
|
173037
|
+
sessionLifecycleV1Router.post("/sessions/:id/recheck", async (req, res, next) => {
|
|
173038
|
+
try {
|
|
173039
|
+
const sessionId = validSid2(req, res);
|
|
173040
|
+
if (!sessionId) return;
|
|
173041
|
+
if (CLOUD_MODE) {
|
|
173042
|
+
await relayControlAction(res, "recheck", sessionId, void 0, 200);
|
|
173043
|
+
return;
|
|
173044
|
+
}
|
|
173045
|
+
const { recheckSession: recheckSession2 } = await Promise.resolve().then(() => (init_session_lifecycle(), session_lifecycle_exports));
|
|
173046
|
+
await runLocal(res, next, 200, () => recheckSession2(sessionId));
|
|
173047
|
+
} catch (err2) {
|
|
173048
|
+
next(err2);
|
|
173049
|
+
}
|
|
173050
|
+
});
|
|
172676
173051
|
sessionLifecycleV1Router.post("/sessions/:id/permission", async (req, res, next) => {
|
|
172677
173052
|
try {
|
|
172678
173053
|
const sessionId = validSid2(req, res);
|
|
@@ -184587,6 +184962,16 @@ async function resolveCredentialHealth() {
|
|
|
184587
184962
|
return { hasReadyProvider: false, source: "none" };
|
|
184588
184963
|
}
|
|
184589
184964
|
}
|
|
184965
|
+
async function isWalnutAnsweringOn(port) {
|
|
184966
|
+
try {
|
|
184967
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/system/health`, { signal: AbortSignal.timeout(1500) });
|
|
184968
|
+
if (!res.ok) return false;
|
|
184969
|
+
const body3 = await res.json();
|
|
184970
|
+
return !!body3 && typeof body3 === "object" && "hasReadyProvider" in body3;
|
|
184971
|
+
} catch {
|
|
184972
|
+
return false;
|
|
184973
|
+
}
|
|
184974
|
+
}
|
|
184590
184975
|
async function startServer(options = {}) {
|
|
184591
184976
|
if (httpServer) throw new Error("Server already running. Call stopServer() first.");
|
|
184592
184977
|
bootCompleted = false;
|
|
@@ -185113,6 +185498,7 @@ async function startServer(options = {}) {
|
|
|
185113
185498
|
});
|
|
185114
185499
|
if (!dev) {
|
|
185115
185500
|
const staticDir = (() => {
|
|
185501
|
+
if (process.env.WALNUT_WEB_STATIC_DIR) return process.env.WALNUT_WEB_STATIC_DIR;
|
|
185116
185502
|
let dir2 = path184.dirname(fileURLToPath13(import.meta.url));
|
|
185117
185503
|
for (let i8 = 0; i8 < 5; i8++) {
|
|
185118
185504
|
const candidate = path184.join(dir2, "web", "static", "index.html");
|
|
@@ -185129,11 +185515,65 @@ async function startServer(options = {}) {
|
|
|
185129
185515
|
}
|
|
185130
185516
|
return path184.join(process.cwd(), "dist", "web", "static");
|
|
185131
185517
|
})();
|
|
185518
|
+
const mirrorDir = process.env.WALNUT_WEB_STATIC_MIRROR || path184.join(WALNUT_HOME, "cache", "web-static");
|
|
185519
|
+
let mirrorReady = false;
|
|
185520
|
+
try {
|
|
185521
|
+
const want = fs113.readFileSync(path184.join(staticDir, "index.html"), "utf-8");
|
|
185522
|
+
let have = null;
|
|
185523
|
+
try {
|
|
185524
|
+
have = fs113.readFileSync(path184.join(mirrorDir, "index.html"), "utf-8");
|
|
185525
|
+
} catch {
|
|
185526
|
+
}
|
|
185527
|
+
if (have !== want) {
|
|
185528
|
+
fs113.rmSync(mirrorDir, { recursive: true, force: true });
|
|
185529
|
+
fs113.mkdirSync(path184.dirname(mirrorDir), { recursive: true });
|
|
185530
|
+
fs113.cpSync(staticDir, mirrorDir, { recursive: true });
|
|
185531
|
+
}
|
|
185532
|
+
} catch (err2) {
|
|
185533
|
+
log.web.warn("could not refresh the web-asset mirror", {
|
|
185534
|
+
mirrorDir,
|
|
185535
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
185536
|
+
});
|
|
185537
|
+
}
|
|
185538
|
+
try {
|
|
185539
|
+
mirrorReady = fs113.statSync(path184.join(mirrorDir, "index.html")).isFile();
|
|
185540
|
+
} catch {
|
|
185541
|
+
mirrorReady = false;
|
|
185542
|
+
}
|
|
185132
185543
|
app.use(express3.static(staticDir));
|
|
185544
|
+
if (mirrorReady) app.use(express3.static(mirrorDir));
|
|
185545
|
+
const checkStaticRoot = () => {
|
|
185546
|
+
try {
|
|
185547
|
+
return fs113.statSync(path184.join(staticDir, "index.html")).isFile();
|
|
185548
|
+
} catch {
|
|
185549
|
+
return false;
|
|
185550
|
+
}
|
|
185551
|
+
};
|
|
185552
|
+
let staticRootOk = checkStaticRoot();
|
|
185553
|
+
if (!staticRootOk) {
|
|
185554
|
+
log.web.error("web assets are NOT servable at startup", { staticDir, mirrorReady });
|
|
185555
|
+
}
|
|
185556
|
+
const staticRootTimer = setInterval(() => {
|
|
185557
|
+
const ok = checkStaticRoot();
|
|
185558
|
+
if (ok === staticRootOk) return;
|
|
185559
|
+
staticRootOk = ok;
|
|
185560
|
+
if (ok) log.web.info("web assets are servable again", { staticDir });
|
|
185561
|
+
else log.web.error("web assets VANISHED from under the running server", { staticDir, mirrorReady });
|
|
185562
|
+
}, 6e4);
|
|
185563
|
+
staticRootTimer.unref();
|
|
185564
|
+
setStaticRootReporter(() => ({ staticDir, ok: staticRootOk, mirrorDir, mirrorReady }));
|
|
185133
185565
|
app.use((req, res, next) => {
|
|
185134
185566
|
if (req.method !== "GET" || req.path.startsWith("/api/")) return next();
|
|
185135
185567
|
if (isStaticAssetPath(req.path)) return next();
|
|
185136
|
-
res.sendFile("index.html", { root: staticDir })
|
|
185568
|
+
res.sendFile("index.html", { root: staticDir }, (err2) => {
|
|
185569
|
+
if (!err2) return;
|
|
185570
|
+
if (mirrorReady) {
|
|
185571
|
+
res.sendFile("index.html", { root: mirrorDir }, () => {
|
|
185572
|
+
});
|
|
185573
|
+
return;
|
|
185574
|
+
}
|
|
185575
|
+
next(err2);
|
|
185576
|
+
});
|
|
185137
185577
|
});
|
|
185138
185578
|
}
|
|
185139
185579
|
app.use("/api", notFoundHandler);
|
|
@@ -185153,9 +185593,21 @@ async function startServer(options = {}) {
|
|
|
185153
185593
|
}
|
|
185154
185594
|
httpServer = createServer(app);
|
|
185155
185595
|
attachWss(httpServer);
|
|
185156
|
-
|
|
185157
|
-
|
|
185158
|
-
|
|
185596
|
+
try {
|
|
185597
|
+
await new Promise((resolve, reject) => {
|
|
185598
|
+
httpServer.once("error", reject);
|
|
185599
|
+
httpServer.listen(port, () => resolve());
|
|
185600
|
+
});
|
|
185601
|
+
} catch (err2) {
|
|
185602
|
+
if (err2?.code !== "EADDRINUSE") throw err2;
|
|
185603
|
+
const already = await isWalnutAnsweringOn(port);
|
|
185604
|
+
throw new Error(
|
|
185605
|
+
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:
|
|
185606
|
+
open-walnut web --port ${port + 1}`
|
|
185607
|
+
);
|
|
185608
|
+
}
|
|
185609
|
+
httpServer.on("error", (err2) => {
|
|
185610
|
+
log.web.error("http server error after listen", { error: err2 instanceof Error ? err2.message : String(err2) });
|
|
185159
185611
|
});
|
|
185160
185612
|
const label = dev ? "dev" : "production";
|
|
185161
185613
|
log.web.info(`server listening on http://localhost:${port}`, { mode: label, port });
|
|
@@ -185165,6 +185617,7 @@ async function startServer(options = {}) {
|
|
|
185165
185617
|
const { updateInstanceLockPort: updateInstanceLockPort2 } = await Promise.resolve().then(() => (init_instance_lock(), instance_lock_exports));
|
|
185166
185618
|
updateInstanceLockPort2(bound.port);
|
|
185167
185619
|
setPluginApiBase(`http://127.0.0.1:${bound.port}`);
|
|
185620
|
+
process.env.WALNUT_SERVER_URL = `http://localhost:${bound.port}`;
|
|
185168
185621
|
}
|
|
185169
185622
|
}
|
|
185170
185623
|
if (CLOUD_MODE) {
|