open-walnut 0.4.0 → 0.4.2
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 +15 -6
- package/bin/open-walnut.js +14 -0
- package/dist/cli-fast.js +69 -46
- package/dist/cli.js +1264 -354
- package/dist/daemon-binaries/acp-worker.js.version +1 -1
- package/dist/web/server.js +1237 -354
- package/dist/web/static/assets/{OfficePreview-DUTDx4ef.js → OfficePreview-CEHO0xkd.js} +2 -2
- package/dist/web/static/assets/{docx-preview-Qj-aUywE.js → docx-preview-CuZwjAMS.js} +1 -1
- package/dist/web/static/assets/{index-xu9Gy-xP.js → index-0MyC-yGG.js} +1 -1
- package/dist/web/static/assets/{index-BvxwKQUT.js → index-BBjg7m-_.js} +1 -1
- package/dist/web/static/assets/{index-oIyzyrLY.js → index-BFF_G72i.js} +294 -294
- package/dist/web/static/assets/{index-CEVKci-j.js → index-BRLxNhyy.js} +1 -1
- package/dist/web/static/assets/{index-CeR0xElr.js → index-Bqg4_WEG.js} +1 -1
- package/dist/web/static/assets/{index-CCa56u0e.js → index-C2YGUYGS.js} +1 -1
- package/dist/web/static/assets/{index-BV9e5-KR.js → index-D1dpOEzW.js} +1 -1
- package/dist/web/static/assets/{index-Dv42N8tm.js → index-D9qPPm7h.js} +1 -1
- package/dist/web/static/assets/{index-CzP6rbk7.js → index-DAOPV34I.js} +1 -1
- package/dist/web/static/assets/{index-fk2aCKPB.js → index-DLjK8dGE.js} +1 -1
- package/dist/web/static/assets/{index-y7f5PVUr.js → index-DOXm2Aye.js} +1 -1
- package/dist/web/static/assets/{index-CSssA5eI.js → index-DR-LSqAg.js} +1 -1
- package/dist/web/static/assets/{index-DqY9Z9j5.js → index-DXtp_ZWh.js} +1 -1
- package/dist/web/static/assets/index-DeYCGmOR.css +32 -0
- package/dist/web/static/assets/{index-D6haQ3tX.js → index-DrWzb338.js} +1 -1
- package/dist/web/static/assets/{index-CfC-vHYG.js → index-DtRwkh8J.js} +1 -1
- package/dist/web/static/assets/{index-BmDSVLwR.js → index-ccs5s80D.js} +1 -1
- package/dist/web/static/assets/{index-Bke7ZOIJ.js → index-mWEwJcqw.js} +1 -1
- package/dist/web/static/assets/{index-sF1ObMS0.js → index-unnPilnK.js} +1 -1
- package/dist/web/static/assets/{jszip.min-C3K7iJBk.js → jszip.min-DYFwy4PE.js} +1 -1
- package/dist/web/static/assets/{pptx-preview.es-UhsuLLNP.js → pptx-preview.es-eu2nX9JC.js} +1 -1
- package/dist/web/static/index.html +2 -2
- package/package.json +5 -4
- package/dist/web/static/assets/index-Btn4AoIy.css +0 -32
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,26 @@ 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
|
+
// A DELIBERATE stop is not a failure, and the JSONL tail cannot tell us it
|
|
8819
|
+
// happened. Both writers (our idle scanner, and any stop RPC via cmdStop)
|
|
8820
|
+
// stamp intentionalStopAt before signalling; the death then lands here through
|
|
8821
|
+
// whichever path notices it (usually orphan-poll ESRCH \u2192 code -1). Verified
|
|
8822
|
+
// production case: the last line was a control_response (the reply to a
|
|
8823
|
+
// Walnut-issued control request \u2014 a routine shape), so isTurnCompleteExit said
|
|
8824
|
+
// false, exitCode stayed -1, and the projection painted a red "Error" over a
|
|
8825
|
+
// healthy, fully resumable session that was stopped on purpose. Checked FIRST
|
|
8826
|
+
// and unconditionally: the flag carries information the tail does not contain,
|
|
8827
|
+
// which is why isTurnCompleteExit is left alone.
|
|
8828
|
+
// Keep in sync with daemon-core.ts reapSession.
|
|
8829
|
+
const intentionalStop = session.intentionalStopAt != null;
|
|
8830
|
+
if (intentionalStop && code !== 0) {
|
|
8831
|
+
logMsg('info', 'reapSession: intentional stop, normalizing exit code', {
|
|
8832
|
+
sid, pid: session.pid, originalCode: code, originalReason: reason,
|
|
8833
|
+
jsonlAgeMs, cleanExit, intentionalStopAt: session.intentionalStopAt,
|
|
8834
|
+
});
|
|
8835
|
+
code = 0;
|
|
8836
|
+
reason = reason + '+intentional-stop';
|
|
8837
|
+
} else if (code !== 0 && cleanExit) {
|
|
8792
8838
|
logMsg('info', 'reapSession: turn-complete detected, normalizing exit code', {
|
|
8793
8839
|
sid, pid: session.pid, originalCode: code, originalReason: reason, jsonlAgeMs,
|
|
8794
8840
|
});
|
|
@@ -12441,6 +12487,15 @@ function cmdStop(ws, id, cmd) {
|
|
|
12441
12487
|
const pid = session.pid;
|
|
12442
12488
|
logMsg('info', 'cmdStop: stopping session (process group kill)', { sid, pid });
|
|
12443
12489
|
|
|
12490
|
+
// Nobody sends stop by accident, so record the intent BEFORE the first signal:
|
|
12491
|
+
// the death lands asynchronously (orphan poll \u2192 ESRCH \u2192 code -1) and by then
|
|
12492
|
+
// nothing remembers this was asked for. This path is how the SERVER's idle
|
|
12493
|
+
// reaper kills (mgr.kill() \u2192 stop RPC), not the daemon's own scan, so it needs
|
|
12494
|
+
// its own stamp: a LOCAL session auto-stopped after 2h idle surfaced as
|
|
12495
|
+
// "Session ended unexpectedly and no cause was recorded" (2026-09-03).
|
|
12496
|
+
// Keep in sync with daemon-standalone.ts cmdStop.
|
|
12497
|
+
session.intentionalStopAt = Date.now();
|
|
12498
|
+
|
|
12444
12499
|
// An explicit stop is a human decision \u2014 never undone by a pending auto-retry
|
|
12445
12500
|
// respawning the CLI minutes later. Cancel the timer AND clear the streak so
|
|
12446
12501
|
// an in-flight result line can't re-arm one either.
|
|
@@ -12477,7 +12532,7 @@ function cmdStop(ws, id, cmd) {
|
|
|
12477
12532
|
|
|
12478
12533
|
// \u2500\u2500 Status \u2500\u2500
|
|
12479
12534
|
function cmdStatus(ws, id, cmd) {
|
|
12480
|
-
const { sid } = cmd;
|
|
12535
|
+
const { sid, includeArgs } = cmd;
|
|
12481
12536
|
if (!sid) return sendError(ws, id, 'status: missing sid');
|
|
12482
12537
|
|
|
12483
12538
|
const session = sessions.get(sid);
|
|
@@ -12508,6 +12563,11 @@ function cmdStatus(ws, id, cmd) {
|
|
|
12508
12563
|
exitCode: session.exitCode,
|
|
12509
12564
|
exitReason: session.exitReason,
|
|
12510
12565
|
pendingCtrl: session.pendingCtrl,
|
|
12566
|
+
// Opt-in: the argv the live process was launched with (a fork copies it to
|
|
12567
|
+
// reproduce the parent's prompt-cache prefix). Off by default because the
|
|
12568
|
+
// health monitor polls status for every session and the argv carries the
|
|
12569
|
+
// multi-KB --append-system-prompt.
|
|
12570
|
+
...(includeArgs && session.args && session.args.length > 0 ? { args: session.args } : {}),
|
|
12511
12571
|
});
|
|
12512
12572
|
}
|
|
12513
12573
|
|
|
@@ -14553,6 +14613,14 @@ function scanIdleSessions() {
|
|
|
14553
14613
|
}
|
|
14554
14614
|
const idleMinutes = Math.round(idleMs / 60000);
|
|
14555
14615
|
logMsg('warn', 'idle scan: killing idle session (no subscribers, no output)', { sid, pid, idleMinutes, protectedBy: prot.source, thresholdMs: prot.killMs, detail: prot.detail });
|
|
14616
|
+
// Record the intent BEFORE signalling: the reap arrives asynchronously
|
|
14617
|
+
// (the orphan poll sees ESRCH and reports 'orphan-poll-dead', code -1), so
|
|
14618
|
+
// by then nothing remembers that WE asked for this. Without the stamp
|
|
14619
|
+
// reapSession can only consult the JSONL tail, which says "not a clean turn
|
|
14620
|
+
// end" for any session whose last line isn't a type:result \u2014 and the
|
|
14621
|
+
// projection then shows a red Error for our own routine reclamation.
|
|
14622
|
+
// Keep in sync with daemon-standalone.ts scanIdleSessions.
|
|
14623
|
+
session.intentionalStopAt = now;
|
|
14556
14624
|
killSessionProcessGroup(pid, sid);
|
|
14557
14625
|
}
|
|
14558
14626
|
}
|
|
@@ -15236,7 +15304,20 @@ function createDaemonCore(deps) {
|
|
|
15236
15304
|
jsonlAgeMs = clock() - fs121.statSync(session2.jsonlPath).mtimeMs;
|
|
15237
15305
|
} catch {
|
|
15238
15306
|
}
|
|
15239
|
-
|
|
15307
|
+
const intentionalStop = session2.intentionalStopAt != null;
|
|
15308
|
+
if (intentionalStop && code !== 0) {
|
|
15309
|
+
logger2("info", "reapSession: intentional stop, normalizing exit code", {
|
|
15310
|
+
sid,
|
|
15311
|
+
pid: session2.pid,
|
|
15312
|
+
originalCode: code,
|
|
15313
|
+
originalReason: reason,
|
|
15314
|
+
jsonlAgeMs,
|
|
15315
|
+
cleanExit,
|
|
15316
|
+
intentionalStopAt: session2.intentionalStopAt
|
|
15317
|
+
});
|
|
15318
|
+
code = 0;
|
|
15319
|
+
reason = reason + "+intentional-stop";
|
|
15320
|
+
} else if (cleanExit && code !== 0) {
|
|
15240
15321
|
logger2("info", "reapSession: turn-complete detected, normalizing exit code", {
|
|
15241
15322
|
sid,
|
|
15242
15323
|
pid: session2.pid,
|
|
@@ -16026,6 +16107,7 @@ var init_daemon_core = __esm({
|
|
|
16026
16107
|
var claude_cli_detect_exports = {};
|
|
16027
16108
|
__export(claude_cli_detect_exports, {
|
|
16028
16109
|
_CLAUDE_CREDENTIALS_HOME: () => _CLAUDE_CREDENTIALS_HOME,
|
|
16110
|
+
describeClaudeCliAuth: () => describeClaudeCliAuth,
|
|
16029
16111
|
detectClaudeCli: () => detectClaudeCli,
|
|
16030
16112
|
hasClaudeSubscriptionAuth: () => hasClaudeSubscriptionAuth,
|
|
16031
16113
|
isClaudeCliInstalled: () => isClaudeCliInstalled,
|
|
@@ -16086,9 +16168,32 @@ function detectClaudeCli() {
|
|
|
16086
16168
|
return {
|
|
16087
16169
|
installed: installed2,
|
|
16088
16170
|
subscriptionAuth,
|
|
16089
|
-
subscriptionReady: installed2 && subscriptionAuth
|
|
16171
|
+
subscriptionReady: installed2 && subscriptionAuth,
|
|
16172
|
+
ready: installed2,
|
|
16173
|
+
auth: installed2 ? describeClaudeCliAuth() : { mode: "unknown", label: "not installed" }
|
|
16090
16174
|
};
|
|
16091
16175
|
}
|
|
16176
|
+
function describeClaudeCliAuth(env3 = process.env, settingsEnv = readClaudeSettingsEnv()) {
|
|
16177
|
+
const merged = { ...env3, ...settingsEnv };
|
|
16178
|
+
const truthy = (v3) => !!v3 && v3 !== "0" && v3.toLowerCase() !== "false";
|
|
16179
|
+
if (truthy(merged.CLAUDE_CODE_USE_BEDROCK)) {
|
|
16180
|
+
const region = merged.AWS_REGION || merged.AWS_DEFAULT_REGION;
|
|
16181
|
+
return { mode: "bedrock", label: region ? `Bedrock (${region})` : "Bedrock" };
|
|
16182
|
+
}
|
|
16183
|
+
if (truthy(merged.CLAUDE_CODE_USE_VERTEX)) return { mode: "vertex", label: "Vertex AI" };
|
|
16184
|
+
if (merged.ANTHROPIC_API_KEY || merged.ANTHROPIC_AUTH_TOKEN) return { mode: "api-key", label: "an Anthropic API key" };
|
|
16185
|
+
if (hasClaudeSubscriptionAuth()) return { mode: "subscription", label: "your Claude subscription" };
|
|
16186
|
+
return { mode: "unknown", label: "its own login" };
|
|
16187
|
+
}
|
|
16188
|
+
function readClaudeSettingsEnv() {
|
|
16189
|
+
try {
|
|
16190
|
+
const raw = fs16.readFileSync(path17.join(os4.homedir(), ".claude", "settings.json"), "utf8");
|
|
16191
|
+
const env3 = JSON.parse(raw).env ?? {};
|
|
16192
|
+
return Object.fromEntries(Object.entries(env3).filter(([, v3]) => typeof v3 === "string"));
|
|
16193
|
+
} catch {
|
|
16194
|
+
return {};
|
|
16195
|
+
}
|
|
16196
|
+
}
|
|
16092
16197
|
var KEYCHAIN_OAUTH_SERVICE, _CLAUDE_CREDENTIALS_HOME;
|
|
16093
16198
|
var init_claude_cli_detect = __esm({
|
|
16094
16199
|
"src/core/claude-cli-detect.ts"() {
|
|
@@ -16707,6 +16812,22 @@ var init_local_daemon = __esm({
|
|
|
16707
16812
|
});
|
|
16708
16813
|
|
|
16709
16814
|
// src/core/session-error-kind.ts
|
|
16815
|
+
var session_error_kind_exports = {};
|
|
16816
|
+
__export(session_error_kind_exports, {
|
|
16817
|
+
INFRA_STATUS_REASONS: () => INFRA_STATUS_REASONS,
|
|
16818
|
+
RESCUABLE_STOPPED_WINDOW_MS: () => RESCUABLE_STOPPED_WINDOW_MS,
|
|
16819
|
+
TERMINAL_STATUS_REASONS: () => TERMINAL_STATUS_REASONS,
|
|
16820
|
+
assertsHostUnreachable: () => assertsHostUnreachable,
|
|
16821
|
+
classifySessionError: () => classifySessionError,
|
|
16822
|
+
classifyStatusReasonKind: () => classifyStatusReasonKind,
|
|
16823
|
+
isInfraSessionError: () => isInfraSessionError,
|
|
16824
|
+
isRecoverableSessionError: () => isRecoverableSessionError,
|
|
16825
|
+
isRescuableStoppedRecord: () => isRescuableStoppedRecord
|
|
16826
|
+
});
|
|
16827
|
+
function assertsHostUnreachable(record) {
|
|
16828
|
+
if (!record.errorMessage) return false;
|
|
16829
|
+
return typeof record.status_reason === "string" && REACHABILITY_CLAIM_REASONS.has(record.status_reason);
|
|
16830
|
+
}
|
|
16710
16831
|
function classifyStatusReasonKind(reason) {
|
|
16711
16832
|
if (typeof reason !== "string") return void 0;
|
|
16712
16833
|
if (INFRA_STATUS_REASONS.has(reason)) return "infra";
|
|
@@ -16735,7 +16856,7 @@ function isRescuableStoppedRecord(record, nowMs = Date.now()) {
|
|
|
16735
16856
|
if (!Number.isFinite(changedAt)) return false;
|
|
16736
16857
|
return nowMs - changedAt <= RESCUABLE_STOPPED_WINDOW_MS;
|
|
16737
16858
|
}
|
|
16738
|
-
var INFRA_STATUS_REASONS, TERMINAL_STATUS_REASONS, INFRA_TEXT_SIGNATURES, RESCUABLE_STOPPED_WINDOW_MS;
|
|
16859
|
+
var INFRA_STATUS_REASONS, TERMINAL_STATUS_REASONS, INFRA_TEXT_SIGNATURES, REACHABILITY_CLAIM_REASONS, RESCUABLE_STOPPED_WINDOW_MS;
|
|
16739
16860
|
var init_session_error_kind = __esm({
|
|
16740
16861
|
"src/core/session-error-kind.ts"() {
|
|
16741
16862
|
"use strict";
|
|
@@ -16779,6 +16900,9 @@ var init_session_error_kind = __esm({
|
|
|
16779
16900
|
"ssh tunnel",
|
|
16780
16901
|
"broken pipe"
|
|
16781
16902
|
];
|
|
16903
|
+
REACHABILITY_CLAIM_REASONS = /* @__PURE__ */ new Set([
|
|
16904
|
+
"remote_unreachable"
|
|
16905
|
+
]);
|
|
16782
16906
|
RESCUABLE_STOPPED_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
16783
16907
|
}
|
|
16784
16908
|
});
|
|
@@ -23439,7 +23563,7 @@ __export(credential_resolver_exports, {
|
|
|
23439
23563
|
DEFAULT_REGION: () => DEFAULT_REGION,
|
|
23440
23564
|
hasResolvableCredential: () => hasResolvableCredential,
|
|
23441
23565
|
readClaudeCredentialExport: () => readClaudeCredentialExport,
|
|
23442
|
-
readClaudeSettingsEnv: () =>
|
|
23566
|
+
readClaudeSettingsEnv: () => readClaudeSettingsEnv2,
|
|
23443
23567
|
resolveCredentialFrom: () => resolveCredentialFrom,
|
|
23444
23568
|
resolveCredentials: () => resolveCredentials,
|
|
23445
23569
|
traceCredentialResolution: () => traceCredentialResolution
|
|
@@ -23594,7 +23718,7 @@ function traceCredentialResolution(inputs) {
|
|
|
23594
23718
|
region: { value: winner.region ?? DEFAULT_REGION, source: regionSource }
|
|
23595
23719
|
};
|
|
23596
23720
|
}
|
|
23597
|
-
function
|
|
23721
|
+
function readClaudeSettingsEnv2(settingsFile = CLAUDE_SETTINGS_FILE) {
|
|
23598
23722
|
try {
|
|
23599
23723
|
const raw = fs30.readFileSync(settingsFile, "utf-8");
|
|
23600
23724
|
const parsed = JSON.parse(raw);
|
|
@@ -23634,7 +23758,7 @@ function probeAwsFiles() {
|
|
|
23634
23758
|
function resolveCredentials(config) {
|
|
23635
23759
|
const result = resolveCredentialFrom({
|
|
23636
23760
|
config,
|
|
23637
|
-
claudeEnv:
|
|
23761
|
+
claudeEnv: readClaudeSettingsEnv2(),
|
|
23638
23762
|
processEnv: process.env,
|
|
23639
23763
|
awsFiles: probeAwsFiles(),
|
|
23640
23764
|
claudeCredentialExport: readClaudeCredentialExport()
|
|
@@ -66416,10 +66540,9 @@ function buildArgs(opts) {
|
|
|
66416
66540
|
// Input stays default (text): we write the whole prompt to stdin and close.
|
|
66417
66541
|
// (stream-json INPUT would force us into a JSON envelope for no gain here.)
|
|
66418
66542
|
"--tools",
|
|
66419
|
-
""
|
|
66543
|
+
""
|
|
66420
66544
|
// disable ALL built-in tools (deny-all)
|
|
66421
|
-
|
|
66422
|
-
FORCE_SUBSCRIPTION_SETTINGS
|
|
66545
|
+
// No --settings override: the user's settings.json (and so their auth) applies as-is.
|
|
66423
66546
|
];
|
|
66424
66547
|
let systemText = systemToText(opts.system).trim();
|
|
66425
66548
|
if (opts.tools && opts.tools.length > 0) {
|
|
@@ -66448,7 +66571,7 @@ function systemToText(system) {
|
|
|
66448
66571
|
}
|
|
66449
66572
|
function buildSpawnEnv(base = process.env) {
|
|
66450
66573
|
const env3 = { ...base };
|
|
66451
|
-
for (const key2 of
|
|
66574
|
+
for (const key2 of ENV_TO_STRIP) {
|
|
66452
66575
|
delete env3[key2];
|
|
66453
66576
|
}
|
|
66454
66577
|
env3.CLAUDE_CODE_ENTRYPOINT = WALNUT_UTILITY_ENTRYPOINT;
|
|
@@ -66510,7 +66633,7 @@ function normalizeCliModel(model) {
|
|
|
66510
66633
|
}
|
|
66511
66634
|
return void 0;
|
|
66512
66635
|
}
|
|
66513
|
-
var
|
|
66636
|
+
var ENV_TO_STRIP, MAX_CONCURRENT_CLI, CLI_TURN_TIMEOUT_MS, SESSION_REUSE_TTL_MS, MAX_TRACKED_SESSIONS, ClaudeCliAdapter;
|
|
66514
66637
|
var init_adapter_claude_cli = __esm({
|
|
66515
66638
|
"src/agent/providers/adapter-claude-cli.ts"() {
|
|
66516
66639
|
"use strict";
|
|
@@ -66520,24 +66643,11 @@ var init_adapter_claude_cli = __esm({
|
|
|
66520
66643
|
init_logging();
|
|
66521
66644
|
init_claude_cli_detect();
|
|
66522
66645
|
init_claude_cli_protocol();
|
|
66523
|
-
|
|
66524
|
-
|
|
66525
|
-
|
|
66526
|
-
|
|
66527
|
-
|
|
66528
|
-
"AWS_ACCESS_KEY_ID",
|
|
66529
|
-
"AWS_SECRET_ACCESS_KEY",
|
|
66530
|
-
"AWS_SESSION_TOKEN",
|
|
66531
|
-
"AWS_PROFILE",
|
|
66532
|
-
"CLAUDE_CODE_USE_BEDROCK",
|
|
66533
|
-
"CLAUDE_CODE_USE_VERTEX",
|
|
66534
|
-
// Nested-session marker — inline-subagent strips this too so the child doesn't
|
|
66535
|
-
// think it's running inside another Claude session.
|
|
66536
|
-
"CLAUDECODE"
|
|
66537
|
-
];
|
|
66538
|
-
FORCE_SUBSCRIPTION_SETTINGS = JSON.stringify({
|
|
66539
|
-
env: { CLAUDE_CODE_USE_BEDROCK: "", CLAUDE_CODE_USE_VERTEX: "" }
|
|
66540
|
-
});
|
|
66646
|
+
ENV_TO_STRIP = ["CLAUDECODE"];
|
|
66647
|
+
MAX_CONCURRENT_CLI = (() => {
|
|
66648
|
+
const n6 = Number(process.env.WALNUT_CLAUDE_CLI_CONCURRENCY);
|
|
66649
|
+
return Number.isFinite(n6) && n6 > 0 ? Math.floor(n6) : 3;
|
|
66650
|
+
})();
|
|
66541
66651
|
CLI_TURN_TIMEOUT_MS = 12e4;
|
|
66542
66652
|
SESSION_REUSE_TTL_MS = 30 * 60 * 1e3;
|
|
66543
66653
|
MAX_TRACKED_SESSIONS = 50;
|
|
@@ -66545,6 +66655,25 @@ var init_adapter_claude_cli = __esm({
|
|
|
66545
66655
|
protocol = "claude-cli";
|
|
66546
66656
|
/** conversationKey → CLI session for --resume chaining. In-process only. */
|
|
66547
66657
|
sessions = /* @__PURE__ */ new Map();
|
|
66658
|
+
/** Process gate: MAX_CONCURRENT_CLI turns at once, the rest queue in order. */
|
|
66659
|
+
inFlight = 0;
|
|
66660
|
+
waiters = [];
|
|
66661
|
+
async withSlot(fn) {
|
|
66662
|
+
if (this.inFlight >= MAX_CONCURRENT_CLI) {
|
|
66663
|
+
await new Promise((resolve) => this.waiters.push(resolve));
|
|
66664
|
+
}
|
|
66665
|
+
this.inFlight++;
|
|
66666
|
+
try {
|
|
66667
|
+
return await fn();
|
|
66668
|
+
} finally {
|
|
66669
|
+
this.inFlight--;
|
|
66670
|
+
this.waiters.shift()?.();
|
|
66671
|
+
}
|
|
66672
|
+
}
|
|
66673
|
+
/** Test hook: how many turns are running / waiting right now. */
|
|
66674
|
+
_gateStateForTesting() {
|
|
66675
|
+
return { inFlight: this.inFlight, waiting: this.waiters.length };
|
|
66676
|
+
}
|
|
66548
66677
|
resetClient() {
|
|
66549
66678
|
this.sessions.clear();
|
|
66550
66679
|
}
|
|
@@ -66622,7 +66751,7 @@ User: ${overridePrompt}` : "");
|
|
|
66622
66751
|
}
|
|
66623
66752
|
if (!prompt.trim()) prompt = "(continue)";
|
|
66624
66753
|
const args = [...buildArgs(opts), session2.flag, session2.id];
|
|
66625
|
-
return this.execCli(command6, args, env3, prompt, opts.signal, onTextDelta).then((result) => {
|
|
66754
|
+
return this.withSlot(() => this.execCli(command6, args, env3, prompt, opts.signal, onTextDelta)).then((result) => {
|
|
66626
66755
|
this.sessions.set(key2, {
|
|
66627
66756
|
sessionId: session2.id,
|
|
66628
66757
|
seenMessages: opts.messages.length,
|
|
@@ -66641,7 +66770,7 @@ User: ${overridePrompt}` : "");
|
|
|
66641
66770
|
User: ${overridePrompt}` : "");
|
|
66642
66771
|
const freshId = randomUUID3();
|
|
66643
66772
|
const freshArgs = [...buildArgs(opts), "--session-id", freshId];
|
|
66644
|
-
return this.execCli(command6, freshArgs, env3, freshPrompt || "(continue)", opts.signal, onTextDelta).then((result) => {
|
|
66773
|
+
return this.withSlot(() => this.execCli(command6, freshArgs, env3, freshPrompt || "(continue)", opts.signal, onTextDelta)).then((result) => {
|
|
66645
66774
|
this.sessions.set(key2, {
|
|
66646
66775
|
sessionId: freshId,
|
|
66647
66776
|
seenMessages: opts.messages.length,
|
|
@@ -66725,7 +66854,7 @@ User: ${overridePrompt}` : "");
|
|
|
66725
66854
|
}
|
|
66726
66855
|
const resultText = finalResult?.result || accumulatedText;
|
|
66727
66856
|
if (resultText && resultText.length < 300 && /^\s*failed to authenticate|^\s*api error: 4\d\d/i.test(resultText)) {
|
|
66728
|
-
reject(new Error(`claude-cli
|
|
66857
|
+
reject(new Error(`claude-cli could not authenticate \u2014 run \`claude\` once in a terminal to sign in (or check its Bedrock settings). CLI said: ${resultText.slice(0, 200)}`));
|
|
66729
66858
|
return;
|
|
66730
66859
|
}
|
|
66731
66860
|
if (code !== 0 && !accumulatedText && !finalResult) {
|
|
@@ -66925,6 +67054,50 @@ var init_registry2 = __esm({
|
|
|
66925
67054
|
}
|
|
66926
67055
|
});
|
|
66927
67056
|
|
|
67057
|
+
// src/agent/providers/default-provider.ts
|
|
67058
|
+
var default_provider_exports = {};
|
|
67059
|
+
__export(default_provider_exports, {
|
|
67060
|
+
CLAUDE_CLI_PROVIDER: () => CLAUDE_CLI_PROVIDER,
|
|
67061
|
+
LEGACY_DEFAULT_PROVIDER: () => LEGACY_DEFAULT_PROVIDER,
|
|
67062
|
+
_resetDefaultProviderCacheForTesting: () => _resetDefaultProviderCacheForTesting,
|
|
67063
|
+
hasExplicitBedrockConfig: () => hasExplicitBedrockConfig,
|
|
67064
|
+
mainProviderIsImplicit: () => mainProviderIsImplicit,
|
|
67065
|
+
resolveMainProviderName: () => resolveMainProviderName
|
|
67066
|
+
});
|
|
67067
|
+
function claudeCliInstalledCached() {
|
|
67068
|
+
const now = Date.now();
|
|
67069
|
+
if (!cliProbe || now - cliProbe.at > CLI_PROBE_TTL_MS) {
|
|
67070
|
+
cliProbe = { at: now, installed: isClaudeCliInstalled() };
|
|
67071
|
+
}
|
|
67072
|
+
return cliProbe.installed;
|
|
67073
|
+
}
|
|
67074
|
+
function _resetDefaultProviderCacheForTesting() {
|
|
67075
|
+
cliProbe = void 0;
|
|
67076
|
+
}
|
|
67077
|
+
function hasExplicitBedrockConfig(config) {
|
|
67078
|
+
const b8 = config.providers?.bedrock;
|
|
67079
|
+
if (b8 && (b8.bearer_token || b8.aws_access_key_id || b8.aws_profile || b8.aws_credential_export)) return true;
|
|
67080
|
+
return !!config.provider?.bedrock_bearer_token;
|
|
67081
|
+
}
|
|
67082
|
+
function resolveMainProviderName(config, claudeInstalled = claudeCliInstalledCached()) {
|
|
67083
|
+
if (config.agent?.main_provider) return config.agent.main_provider;
|
|
67084
|
+
if (claudeInstalled) return CLAUDE_CLI_PROVIDER;
|
|
67085
|
+
return LEGACY_DEFAULT_PROVIDER;
|
|
67086
|
+
}
|
|
67087
|
+
function mainProviderIsImplicit(config) {
|
|
67088
|
+
return !config.agent?.main_provider;
|
|
67089
|
+
}
|
|
67090
|
+
var LEGACY_DEFAULT_PROVIDER, CLAUDE_CLI_PROVIDER, CLI_PROBE_TTL_MS, cliProbe;
|
|
67091
|
+
var init_default_provider = __esm({
|
|
67092
|
+
"src/agent/providers/default-provider.ts"() {
|
|
67093
|
+
"use strict";
|
|
67094
|
+
init_claude_cli_detect();
|
|
67095
|
+
LEGACY_DEFAULT_PROVIDER = "bedrock";
|
|
67096
|
+
CLAUDE_CLI_PROVIDER = "claude_cli";
|
|
67097
|
+
CLI_PROBE_TTL_MS = 6e4;
|
|
67098
|
+
}
|
|
67099
|
+
});
|
|
67100
|
+
|
|
66928
67101
|
// src/agent/model.ts
|
|
66929
67102
|
var model_exports = {};
|
|
66930
67103
|
__export(model_exports, {
|
|
@@ -66959,7 +67132,7 @@ function getContextThreshold(model, percent) {
|
|
|
66959
67132
|
}
|
|
66960
67133
|
async function resolveForCall(config) {
|
|
66961
67134
|
const fullConfig = await getConfig();
|
|
66962
|
-
const providerName = config?.provider ?? fullConfig
|
|
67135
|
+
const providerName = config?.provider ?? resolveMainProviderName(fullConfig);
|
|
66963
67136
|
const hasExplicitProviders = fullConfig.providers && Object.keys(fullConfig.providers).length > 0;
|
|
66964
67137
|
let providers = hasExplicitProviders ? buildProviderMap(fullConfig.providers, fullConfig) : synthesizeFromLegacy(fullConfig);
|
|
66965
67138
|
const resolved = resolveProvider(providerName, providers);
|
|
@@ -67029,6 +67202,7 @@ var init_model = __esm({
|
|
|
67029
67202
|
"use strict";
|
|
67030
67203
|
init_config_manager();
|
|
67031
67204
|
init_registry2();
|
|
67205
|
+
init_default_provider();
|
|
67032
67206
|
init_defaults();
|
|
67033
67207
|
init_model_catalog();
|
|
67034
67208
|
}
|
|
@@ -67231,8 +67405,22 @@ var init_token_truth = __esm({
|
|
|
67231
67405
|
});
|
|
67232
67406
|
|
|
67233
67407
|
// src/utils/image-compress.ts
|
|
67234
|
-
|
|
67408
|
+
async function loadSharp() {
|
|
67409
|
+
sharpLoad ??= import("sharp").then(
|
|
67410
|
+
(m6) => m6.default ?? m6,
|
|
67411
|
+
(err2) => {
|
|
67412
|
+
log.agent.warn("sharp is not installed; images are sent uncompressed", {
|
|
67413
|
+
error: err2 instanceof Error ? err2.message : String(err2),
|
|
67414
|
+
fix: "a machine with glibc 2.28+ installs sharp prebuilt; otherwise install libvips and re-run npm install"
|
|
67415
|
+
});
|
|
67416
|
+
return null;
|
|
67417
|
+
}
|
|
67418
|
+
);
|
|
67419
|
+
return sharpLoad;
|
|
67420
|
+
}
|
|
67235
67421
|
async function clampImageDimensions(buffer2, mimeType) {
|
|
67422
|
+
const sharp = await loadSharp();
|
|
67423
|
+
if (!sharp) return { buffer: buffer2, mimeType };
|
|
67236
67424
|
try {
|
|
67237
67425
|
const meta = await sharp(buffer2).metadata();
|
|
67238
67426
|
const longest = Math.max(meta.width ?? 0, meta.height ?? 0);
|
|
@@ -67254,6 +67442,8 @@ async function compressForApi(inputBuffer, inputMimeType) {
|
|
|
67254
67442
|
if (buffer2.toString("base64").length <= MAX_BASE64_BYTES) {
|
|
67255
67443
|
return { buffer: buffer2, mimeType };
|
|
67256
67444
|
}
|
|
67445
|
+
const sharp = await loadSharp();
|
|
67446
|
+
if (!sharp) return { buffer: buffer2, mimeType };
|
|
67257
67447
|
try {
|
|
67258
67448
|
if (mimeType === "image/gif") {
|
|
67259
67449
|
const webp = await sharp(buffer2, { animated: true }).webp({ quality: 80 }).toBuffer();
|
|
@@ -67291,10 +67481,11 @@ async function compressForApi(inputBuffer, inputMimeType) {
|
|
|
67291
67481
|
return { buffer: buffer2, mimeType };
|
|
67292
67482
|
}
|
|
67293
67483
|
}
|
|
67294
|
-
var MAX_BASE64_BYTES, MAX_IMAGE_DIMENSION;
|
|
67484
|
+
var sharpLoad, MAX_BASE64_BYTES, MAX_IMAGE_DIMENSION;
|
|
67295
67485
|
var init_image_compress = __esm({
|
|
67296
67486
|
"src/utils/image-compress.ts"() {
|
|
67297
67487
|
"use strict";
|
|
67488
|
+
init_logging();
|
|
67298
67489
|
MAX_BASE64_BYTES = 5e6;
|
|
67299
67490
|
MAX_IMAGE_DIMENSION = 1568;
|
|
67300
67491
|
}
|
|
@@ -68452,7 +68643,7 @@ __export(cheap_model_exports, {
|
|
|
68452
68643
|
});
|
|
68453
68644
|
function fastModelFor(config) {
|
|
68454
68645
|
if (config.agent?.fast_model) return config.agent.fast_model;
|
|
68455
|
-
const providerName = config
|
|
68646
|
+
const providerName = resolveMainProviderName(config);
|
|
68456
68647
|
return MODEL_CATALOG[providerName]?.find(
|
|
68457
68648
|
(model) => model.id.toLowerCase().includes("haiku")
|
|
68458
68649
|
)?.id;
|
|
@@ -68464,6 +68655,7 @@ var init_cheap_model = __esm({
|
|
|
68464
68655
|
"src/core/cheap-model.ts"() {
|
|
68465
68656
|
"use strict";
|
|
68466
68657
|
init_model_catalog();
|
|
68658
|
+
init_default_provider();
|
|
68467
68659
|
}
|
|
68468
68660
|
});
|
|
68469
68661
|
|
|
@@ -70283,11 +70475,35 @@ async function applySnapshot(sessionId, snapshot2, source) {
|
|
|
70283
70475
|
if (projected !== "error") {
|
|
70284
70476
|
updates.errorMessage = void 0;
|
|
70285
70477
|
updates.errorKind = void 0;
|
|
70286
|
-
|
|
70478
|
+
const stashedStop = projected === "stopped" ? takeSuppressedErrorReason(sessionId) : null;
|
|
70479
|
+
if (stashedStop?.message) {
|
|
70480
|
+
updates.errorMessage = stashedStop.message;
|
|
70481
|
+
if (stashedStop.kind) updates.errorKind = stashedStop.kind;
|
|
70482
|
+
log.session.info("snapshot projection adopted suppressed stop reason", {
|
|
70483
|
+
sessionId,
|
|
70484
|
+
suppressedReason: stashedStop.reason,
|
|
70485
|
+
kind: stashedStop.kind ?? null,
|
|
70486
|
+
projected,
|
|
70487
|
+
source
|
|
70488
|
+
});
|
|
70489
|
+
} else {
|
|
70490
|
+
clearSuppressedErrorReason(sessionId);
|
|
70491
|
+
}
|
|
70287
70492
|
} else {
|
|
70493
|
+
let droppedDisproven = false;
|
|
70494
|
+
if (assertsHostUnreachable(record)) {
|
|
70495
|
+
updates.errorMessage = void 0;
|
|
70496
|
+
droppedDisproven = true;
|
|
70497
|
+
log.session.info("snapshot projection dropped disproven unreachability diagnosis", {
|
|
70498
|
+
sessionId,
|
|
70499
|
+
priorReason: record.status_reason ?? null,
|
|
70500
|
+
projected,
|
|
70501
|
+
source
|
|
70502
|
+
});
|
|
70503
|
+
}
|
|
70288
70504
|
const stashed = takeSuppressedErrorReason(sessionId);
|
|
70289
70505
|
if (stashed) {
|
|
70290
|
-
if (!record.errorMessage && stashed.message) updates.errorMessage = stashed.message;
|
|
70506
|
+
if ((!record.errorMessage || droppedDisproven) && stashed.message) updates.errorMessage = stashed.message;
|
|
70291
70507
|
if (!record.errorKind && stashed.kind) updates.errorKind = stashed.kind;
|
|
70292
70508
|
log.session.info("snapshot projection adopted suppressed error reason", {
|
|
70293
70509
|
sessionId,
|
|
@@ -70340,6 +70556,7 @@ var init_session_snapshot_apply = __esm({
|
|
|
70340
70556
|
"use strict";
|
|
70341
70557
|
init_logging();
|
|
70342
70558
|
init_session_snapshot_gate();
|
|
70559
|
+
init_session_error_kind();
|
|
70343
70560
|
init_engine_registry();
|
|
70344
70561
|
init_session_snapshot_gate();
|
|
70345
70562
|
TERMINAL = /* @__PURE__ */ new Set(["stopped", "error"]);
|
|
@@ -72954,9 +73171,15 @@ var init_acp_session = __esm({
|
|
|
72954
73171
|
}
|
|
72955
73172
|
async resolveArtifacts() {
|
|
72956
73173
|
if (this.cfg.artifacts) return this.cfg.artifacts;
|
|
72957
|
-
|
|
73174
|
+
const artifacts = resolveAcpArtifacts(this.engineId, {
|
|
72958
73175
|
configuredAdapterCmd: await this.resolveConfiguredAdapterCmd()
|
|
72959
73176
|
});
|
|
73177
|
+
if (!fsModule.existsSync(artifacts.workerCmd[1])) {
|
|
73178
|
+
throw new Error(
|
|
73179
|
+
`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.`
|
|
73180
|
+
);
|
|
73181
|
+
}
|
|
73182
|
+
return artifacts;
|
|
72960
73183
|
}
|
|
72961
73184
|
async establishProviderSession() {
|
|
72962
73185
|
await this.seedReplayCursor();
|
|
@@ -74339,6 +74562,26 @@ var init_transcript_chain = __esm({
|
|
|
74339
74562
|
}
|
|
74340
74563
|
});
|
|
74341
74564
|
|
|
74565
|
+
// src/core/sessions/side-thread-warmup.ts
|
|
74566
|
+
function isCacheWarmupText(text) {
|
|
74567
|
+
return text.startsWith(CACHE_WARMUP_TAG);
|
|
74568
|
+
}
|
|
74569
|
+
function markWarmupTurnPending(sessionId) {
|
|
74570
|
+
pendingWarmupTurns.add(sessionId);
|
|
74571
|
+
}
|
|
74572
|
+
function consumeWarmupTurn(sessionId) {
|
|
74573
|
+
return pendingWarmupTurns.delete(sessionId);
|
|
74574
|
+
}
|
|
74575
|
+
var CACHE_WARMUP_TAG, CACHE_WARMUP_MESSAGE, pendingWarmupTurns;
|
|
74576
|
+
var init_side_thread_warmup = __esm({
|
|
74577
|
+
"src/core/sessions/side-thread-warmup.ts"() {
|
|
74578
|
+
"use strict";
|
|
74579
|
+
CACHE_WARMUP_TAG = "<walnut-cache-warmup>";
|
|
74580
|
+
CACHE_WARMUP_MESSAGE = `${CACHE_WARMUP_TAG}This is a cache warm-up from the tool hosting this side thread, not a user message. Reply with exactly one word: Ready. Use no tools and add nothing else.</walnut-cache-warmup>`;
|
|
74581
|
+
pendingWarmupTurns = /* @__PURE__ */ new Set();
|
|
74582
|
+
}
|
|
74583
|
+
});
|
|
74584
|
+
|
|
74342
74585
|
// src/core/history-disk-cache.ts
|
|
74343
74586
|
var history_disk_cache_exports = {};
|
|
74344
74587
|
__export(history_disk_cache_exports, {
|
|
@@ -76493,11 +76736,13 @@ __export(session_lifecycle_exports, {
|
|
|
76493
76736
|
persistSessionModeChange: () => persistSessionModeChange,
|
|
76494
76737
|
readProviderSessionHistory: () => readProviderSessionHistory,
|
|
76495
76738
|
readSessionRichHistory: () => readSessionRichHistory,
|
|
76739
|
+
recheckSession: () => recheckSession,
|
|
76496
76740
|
respondSessionPermission: () => respondSessionPermission,
|
|
76497
76741
|
restartSession: () => restartSession,
|
|
76498
76742
|
retrySession: () => retrySession,
|
|
76499
76743
|
terminateSession: () => terminateSession
|
|
76500
76744
|
});
|
|
76745
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
76501
76746
|
async function readProviderSessionHistory(sessionId, record, nativeHost, skipSubagents = true, opts) {
|
|
76502
76747
|
if (record && engineCaps(record.engine).historySource === "acp-journal") {
|
|
76503
76748
|
const { readAcpSessionHistoryState: readAcpSessionHistoryState2 } = await Promise.resolve().then(() => (init_acp_session_history(), acp_session_history_exports));
|
|
@@ -76608,6 +76853,33 @@ async function changeSessionMode(existingRecord, sessionId, mode, updates = {})
|
|
|
76608
76853
|
});
|
|
76609
76854
|
return updated;
|
|
76610
76855
|
}
|
|
76856
|
+
function normalizePinQuote(value) {
|
|
76857
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
76858
|
+
throw new SessionControlError("pinned_messages[].quote must be an object", 400);
|
|
76859
|
+
}
|
|
76860
|
+
const quote = value;
|
|
76861
|
+
const exact = quote.exact;
|
|
76862
|
+
if (typeof exact !== "string" || !exact.trim() || exact.length > MAX_PIN_QUOTE_CHARS) {
|
|
76863
|
+
throw new SessionControlError(
|
|
76864
|
+
`pinned_messages[].quote.exact must be a non-empty string (max ${MAX_PIN_QUOTE_CHARS} chars)`,
|
|
76865
|
+
400
|
|
76866
|
+
);
|
|
76867
|
+
}
|
|
76868
|
+
for (const side of ["prefix", "suffix"]) {
|
|
76869
|
+
const context = quote[side];
|
|
76870
|
+
if (context !== void 0 && context !== null && (typeof context !== "string" || context.length > MAX_PIN_QUOTE_CONTEXT_CHARS)) {
|
|
76871
|
+
throw new SessionControlError(
|
|
76872
|
+
`pinned_messages[].quote.${side} must be a string (max ${MAX_PIN_QUOTE_CONTEXT_CHARS} chars)`,
|
|
76873
|
+
400
|
|
76874
|
+
);
|
|
76875
|
+
}
|
|
76876
|
+
}
|
|
76877
|
+
return {
|
|
76878
|
+
exact,
|
|
76879
|
+
...typeof quote.prefix === "string" && quote.prefix ? { prefix: quote.prefix } : {},
|
|
76880
|
+
...typeof quote.suffix === "string" && quote.suffix ? { suffix: quote.suffix } : {}
|
|
76881
|
+
};
|
|
76882
|
+
}
|
|
76611
76883
|
function normalizePinnedMessages(value) {
|
|
76612
76884
|
if (!Array.isArray(value)) {
|
|
76613
76885
|
throw new SessionControlError("pinned_messages must be an array", 400);
|
|
@@ -76616,7 +76888,9 @@ function normalizePinnedMessages(value) {
|
|
|
76616
76888
|
throw new SessionControlError(`pinned_messages holds at most ${MAX_PINNED_MESSAGES} entries`, 400);
|
|
76617
76889
|
}
|
|
76618
76890
|
const out = [];
|
|
76619
|
-
const
|
|
76891
|
+
const seenWholeMessage = /* @__PURE__ */ new Set();
|
|
76892
|
+
const seenQuoteId = /* @__PURE__ */ new Set();
|
|
76893
|
+
const seenQuoteShape = /* @__PURE__ */ new Set();
|
|
76620
76894
|
for (const raw of value) {
|
|
76621
76895
|
if (!raw || typeof raw !== "object") {
|
|
76622
76896
|
throw new SessionControlError("each pinned_messages entry must be an object", 400);
|
|
@@ -76626,13 +76900,40 @@ function normalizePinnedMessages(value) {
|
|
|
76626
76900
|
if (typeof msgId !== "string" || !msgId.trim() || msgId.length > MAX_PIN_ID_CHARS) {
|
|
76627
76901
|
throw new SessionControlError("pinned_messages[].msgId must be a non-empty string", 400);
|
|
76628
76902
|
}
|
|
76629
|
-
|
|
76630
|
-
|
|
76903
|
+
const rawId = entry.id;
|
|
76904
|
+
if (rawId !== void 0 && rawId !== null && (typeof rawId !== "string" || !rawId.trim() || rawId.length > MAX_PIN_ENTRY_ID_CHARS)) {
|
|
76905
|
+
throw new SessionControlError(
|
|
76906
|
+
`pinned_messages[].id must be a non-empty string (max ${MAX_PIN_ENTRY_ID_CHARS} chars)`,
|
|
76907
|
+
400
|
|
76908
|
+
);
|
|
76909
|
+
}
|
|
76910
|
+
const id = typeof rawId === "string" ? rawId : void 0;
|
|
76911
|
+
const quote = entry.quote === void 0 || entry.quote === null ? void 0 : normalizePinQuote(entry.quote);
|
|
76912
|
+
let pinId = id;
|
|
76913
|
+
if (quote) {
|
|
76914
|
+
if (id && seenQuoteId.has(id)) continue;
|
|
76915
|
+
const shape = [msgId, quote.exact, quote.prefix ?? "", quote.suffix ?? ""].join("\0");
|
|
76916
|
+
if (seenQuoteShape.has(shape)) continue;
|
|
76917
|
+
pinId = id ?? randomUUID4();
|
|
76918
|
+
seenQuoteId.add(pinId);
|
|
76919
|
+
seenQuoteShape.add(shape);
|
|
76920
|
+
} else {
|
|
76921
|
+
if (seenWholeMessage.has(msgId)) continue;
|
|
76922
|
+
seenWholeMessage.add(msgId);
|
|
76923
|
+
}
|
|
76631
76924
|
const role = entry.role === "user" || entry.role === "assistant" || entry.role === "system" ? entry.role : "assistant";
|
|
76632
76925
|
const label = typeof entry.label === "string" ? entry.label.trim().slice(0, MAX_PIN_LABEL_CHARS) : "";
|
|
76633
76926
|
const timestamp = typeof entry.timestamp === "string" ? entry.timestamp : void 0;
|
|
76634
76927
|
const pinnedAt = typeof entry.pinnedAt === "string" ? entry.pinnedAt : (/* @__PURE__ */ new Date()).toISOString();
|
|
76635
|
-
out.push({
|
|
76928
|
+
out.push({
|
|
76929
|
+
msgId,
|
|
76930
|
+
label,
|
|
76931
|
+
role,
|
|
76932
|
+
...timestamp ? { timestamp } : {},
|
|
76933
|
+
pinnedAt,
|
|
76934
|
+
...pinId ? { id: pinId } : {},
|
|
76935
|
+
...quote ? { quote } : {}
|
|
76936
|
+
});
|
|
76636
76937
|
}
|
|
76637
76938
|
return out;
|
|
76638
76939
|
}
|
|
@@ -76830,7 +77131,7 @@ async function retrySession(sessionId) {
|
|
|
76830
77131
|
return { status: "reconnected", sessionId };
|
|
76831
77132
|
}
|
|
76832
77133
|
if (await resumeConversationExists(record)) {
|
|
76833
|
-
const {
|
|
77134
|
+
const { getQueue: getQueue2, unparkMessage: unparkMessage2 } = await Promise.resolve().then(() => (init_session_message_queue(), session_message_queue_exports));
|
|
76834
77135
|
const pendingMsgs = await getQueue2(sessionId);
|
|
76835
77136
|
for (const parked of pendingMsgs.filter((m6) => m6.status === "parked")) {
|
|
76836
77137
|
await unparkMessage2(sessionId, parked.id);
|
|
@@ -76840,9 +77141,22 @@ async function retrySession(sessionId) {
|
|
|
76840
77141
|
log.session.info("session retry: re-processing pending queue messages", { sessionId, taskId: record.taskId, count: pendingMsgs.length });
|
|
76841
77142
|
return { status: "resuming", sessionId, restoredMessages: pendingMsgs.length };
|
|
76842
77143
|
}
|
|
76843
|
-
await
|
|
76844
|
-
|
|
76845
|
-
|
|
77144
|
+
const relabelled = await updateSessionRecord2(sessionId, {
|
|
77145
|
+
process_status: "stopped",
|
|
77146
|
+
errorMessage: void 0,
|
|
77147
|
+
errorKind: void 0,
|
|
77148
|
+
activity: void 0,
|
|
77149
|
+
last_status_change: (/* @__PURE__ */ new Date()).toISOString(),
|
|
77150
|
+
status_reason: "retry_reconnect",
|
|
77151
|
+
status_changed_by: "user"
|
|
77152
|
+
});
|
|
77153
|
+
emitSessionStatusChanged2(relabelled, {}, ["*"]);
|
|
77154
|
+
log.session.info("session retry: reconnected \u2014 conversation preserved, no message sent", {
|
|
77155
|
+
sessionId,
|
|
77156
|
+
taskId: record.taskId,
|
|
77157
|
+
host: record.host ?? "__local__"
|
|
77158
|
+
});
|
|
77159
|
+
return { status: "resumable", sessionId };
|
|
76846
77160
|
}
|
|
76847
77161
|
log.session.warn("session retry: conversation never persisted \u2014 starting fresh instead of --resume", {
|
|
76848
77162
|
sessionId,
|
|
@@ -76900,6 +77214,91 @@ async function retrySession(sessionId) {
|
|
|
76900
77214
|
});
|
|
76901
77215
|
return { status: "pending", taskId: task.id, oldSessionId: sessionId };
|
|
76902
77216
|
}
|
|
77217
|
+
async function withDeadline(p6, ms) {
|
|
77218
|
+
let timer3;
|
|
77219
|
+
try {
|
|
77220
|
+
return await Promise.race([
|
|
77221
|
+
p6,
|
|
77222
|
+
new Promise((resolve) => {
|
|
77223
|
+
timer3 = setTimeout(() => resolve(RECHECK_TIMED_OUT), ms);
|
|
77224
|
+
if (timer3 && typeof timer3 === "object" && "unref" in timer3) timer3.unref();
|
|
77225
|
+
})
|
|
77226
|
+
]);
|
|
77227
|
+
} finally {
|
|
77228
|
+
if (timer3) clearTimeout(timer3);
|
|
77229
|
+
}
|
|
77230
|
+
}
|
|
77231
|
+
async function recheckSession(sessionId) {
|
|
77232
|
+
const { getSessionByClaudeId: getSessionByClaudeId2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
|
|
77233
|
+
const record = await getSessionByClaudeId2(sessionId);
|
|
77234
|
+
if (!record) throw new SessionControlError("Session not found", 404);
|
|
77235
|
+
const hostKey2 = record.host ?? "__local__";
|
|
77236
|
+
const { classifySessionError: classifySessionError2 } = await Promise.resolve().then(() => (init_session_error_kind(), session_error_kind_exports));
|
|
77237
|
+
const kind = classifySessionError2(record);
|
|
77238
|
+
const base = { sessionId, processStatus: record.process_status, infraClaim: kind === "infra" };
|
|
77239
|
+
const { isDaemonConnected: isDaemonConnected2, getPooledSnapshotConnection: getPooledSnapshotConnection2 } = await Promise.resolve().then(() => (init_daemon_connection(), daemon_connection_exports));
|
|
77240
|
+
const reachable = isDaemonConnected2(hostKey2);
|
|
77241
|
+
if (kind === "terminal") return { ...base, checked: false, reachable, reason: "terminal_error" };
|
|
77242
|
+
if (record.archived) return { ...base, checked: false, reachable, reason: "archived" };
|
|
77243
|
+
const conn = getPooledSnapshotConnection2(record.host);
|
|
77244
|
+
if (!conn) {
|
|
77245
|
+
log.session.info("session recheck: no pooled snapshot connection \u2014 nothing to ask", {
|
|
77246
|
+
sessionId,
|
|
77247
|
+
host: hostKey2,
|
|
77248
|
+
reachable
|
|
77249
|
+
});
|
|
77250
|
+
return { ...base, checked: false, reachable, reason: "no_pooled_connection" };
|
|
77251
|
+
}
|
|
77252
|
+
let raced;
|
|
77253
|
+
try {
|
|
77254
|
+
raced = await withDeadline(
|
|
77255
|
+
conn.send("getState", { sid: sessionId }, RECHECK_RPC_TIMEOUT_MS),
|
|
77256
|
+
RECHECK_RPC_TIMEOUT_MS
|
|
77257
|
+
);
|
|
77258
|
+
} catch (err2) {
|
|
77259
|
+
log.session.info("session recheck: getState failed", {
|
|
77260
|
+
sessionId,
|
|
77261
|
+
host: hostKey2,
|
|
77262
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
77263
|
+
});
|
|
77264
|
+
return { ...base, checked: false, reachable, reason: "rpc_failed" };
|
|
77265
|
+
}
|
|
77266
|
+
if (raced === RECHECK_TIMED_OUT) {
|
|
77267
|
+
log.session.warn("session recheck: getState timed out", {
|
|
77268
|
+
sessionId,
|
|
77269
|
+
host: hostKey2,
|
|
77270
|
+
timeoutMs: RECHECK_RPC_TIMEOUT_MS
|
|
77271
|
+
});
|
|
77272
|
+
return { ...base, checked: false, reachable, reason: "timeout" };
|
|
77273
|
+
}
|
|
77274
|
+
const snapshot2 = raced?.ok ? raced.snapshot : void 0;
|
|
77275
|
+
if (!snapshot2) {
|
|
77276
|
+
return { ...base, checked: false, reachable: true, reason: "no_snapshot" };
|
|
77277
|
+
}
|
|
77278
|
+
const { applySnapshot: applySnapshot2 } = await Promise.resolve().then(() => (init_session_snapshot_apply(), session_snapshot_apply_exports));
|
|
77279
|
+
const outcome = await applySnapshot2(sessionId, snapshot2, "recheck-open");
|
|
77280
|
+
const after = await getSessionByClaudeId2(sessionId);
|
|
77281
|
+
const processStatus = after?.process_status ?? record.process_status;
|
|
77282
|
+
log.session.info("session recheck: reconciled against host", {
|
|
77283
|
+
sessionId,
|
|
77284
|
+
host: hostKey2,
|
|
77285
|
+
previous: record.process_status,
|
|
77286
|
+
processStatus,
|
|
77287
|
+
cliState: snapshot2.cliState,
|
|
77288
|
+
applyOutcome: outcome.outcome
|
|
77289
|
+
});
|
|
77290
|
+
return {
|
|
77291
|
+
sessionId,
|
|
77292
|
+
checked: true,
|
|
77293
|
+
reachable: true,
|
|
77294
|
+
alive: snapshot2.cliState !== "dead",
|
|
77295
|
+
processStatus,
|
|
77296
|
+
// Re-classify against the CURRENT record: a converged row may no longer
|
|
77297
|
+
// carry the claim, and the client uses this to decide whether the sentence
|
|
77298
|
+
// on screen is still true.
|
|
77299
|
+
infraClaim: classifySessionError2(after ?? record) === "infra"
|
|
77300
|
+
};
|
|
77301
|
+
}
|
|
76903
77302
|
async function respondSessionPermission(sessionId, requestId, allow, denyMessage, optionId, answers) {
|
|
76904
77303
|
if (typeof requestId !== "string" || !requestId || typeof allow !== "boolean") {
|
|
76905
77304
|
throw new SessionControlError("requestId (string) and allow (boolean) are required", 400);
|
|
@@ -77193,7 +77592,7 @@ async function readSessionRichHistory(sessionId, tail) {
|
|
|
77193
77592
|
...historyWindowed ? { windowed: true } : {}
|
|
77194
77593
|
};
|
|
77195
77594
|
}
|
|
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;
|
|
77595
|
+
var CLAUDE_SESSION_MODES, MAX_PINNED_MESSAGES, MAX_PIN_LABEL_CHARS, MAX_PIN_ID_CHARS, MAX_PIN_ENTRY_ID_CHARS, MAX_PIN_QUOTE_CHARS, MAX_PIN_QUOTE_CONTEXT_CHARS, RECHECK_RPC_TIMEOUT_MS, RECHECK_TIMED_OUT, GIT_BASES, MAX_FORK_DEPTH, HISTORY_STARTUP_GRACE_MS;
|
|
77197
77596
|
var init_session_lifecycle = __esm({
|
|
77198
77597
|
"src/core/sessions/session-lifecycle.ts"() {
|
|
77199
77598
|
"use strict";
|
|
@@ -77207,6 +77606,11 @@ var init_session_lifecycle = __esm({
|
|
|
77207
77606
|
MAX_PINNED_MESSAGES = 200;
|
|
77208
77607
|
MAX_PIN_LABEL_CHARS = 300;
|
|
77209
77608
|
MAX_PIN_ID_CHARS = 200;
|
|
77609
|
+
MAX_PIN_ENTRY_ID_CHARS = 64;
|
|
77610
|
+
MAX_PIN_QUOTE_CHARS = 2e3;
|
|
77611
|
+
MAX_PIN_QUOTE_CONTEXT_CHARS = 64;
|
|
77612
|
+
RECHECK_RPC_TIMEOUT_MS = 5e3;
|
|
77613
|
+
RECHECK_TIMED_OUT = /* @__PURE__ */ Symbol("recheck-timeout");
|
|
77210
77614
|
GIT_BASES = /* @__PURE__ */ new Set(["uncommitted", "previous", "remote"]);
|
|
77211
77615
|
MAX_FORK_DEPTH = 5;
|
|
77212
77616
|
HISTORY_STARTUP_GRACE_MS = 2 * 60 * 1e3;
|
|
@@ -77507,7 +77911,7 @@ __export(compact_inject_exports, {
|
|
|
77507
77911
|
extractSessionMeta: () => extractSessionMeta,
|
|
77508
77912
|
injectCompactBoundary: () => injectCompactBoundary
|
|
77509
77913
|
});
|
|
77510
|
-
import { randomUUID as
|
|
77914
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
77511
77915
|
import fsp26 from "fs/promises";
|
|
77512
77916
|
async function extractSessionMeta(jsonlPath) {
|
|
77513
77917
|
const content = await fsp26.readFile(jsonlPath, "utf-8");
|
|
@@ -77559,8 +77963,8 @@ async function injectCompactBoundary(jsonlPath, summary, meta) {
|
|
|
77559
77963
|
log.session.error("Could not find last message UUID in JSONL", { jsonlPath });
|
|
77560
77964
|
return null;
|
|
77561
77965
|
}
|
|
77562
|
-
const boundaryUuid =
|
|
77563
|
-
const summaryUuid =
|
|
77966
|
+
const boundaryUuid = randomUUID5();
|
|
77967
|
+
const summaryUuid = randomUUID5();
|
|
77564
77968
|
const now = /* @__PURE__ */ new Date();
|
|
77565
77969
|
const boundaryTs = now.toISOString();
|
|
77566
77970
|
const summaryTs = new Date(now.getTime() + 1e3).toISOString();
|
|
@@ -86589,7 +86993,7 @@ function isRevealSecretPath(resolved) {
|
|
|
86589
86993
|
];
|
|
86590
86994
|
return denied.some((d8) => resolved === d8 || resolved.startsWith(d8 + path69.sep)) || /(^|\/)config\.ya?ml$/.test(resolved);
|
|
86591
86995
|
}
|
|
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;
|
|
86996
|
+
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
86997
|
var init_files2 = __esm({
|
|
86594
86998
|
"src/web/routes/files.ts"() {
|
|
86595
86999
|
"use strict";
|
|
@@ -86722,6 +87126,54 @@ var init_files2 = __esm({
|
|
|
86722
87126
|
next(err2);
|
|
86723
87127
|
}
|
|
86724
87128
|
});
|
|
87129
|
+
LIST_MANY_MAX_PATHS = 64;
|
|
87130
|
+
LIST_MANY_CONCURRENCY = 8;
|
|
87131
|
+
LIST_MANY_DEADLINE_MS = 8e3;
|
|
87132
|
+
filesRouter.post("/list-many", async (req, res, next) => {
|
|
87133
|
+
try {
|
|
87134
|
+
const body3 = req.body ?? {};
|
|
87135
|
+
const raw = Array.isArray(body3.paths) ? body3.paths : null;
|
|
87136
|
+
if (!raw) {
|
|
87137
|
+
res.status(400).json({ error: "paths (string[]) is required" });
|
|
87138
|
+
return;
|
|
87139
|
+
}
|
|
87140
|
+
const host = typeof body3.host === "string" && body3.host ? body3.host : void 0;
|
|
87141
|
+
const showHidden = body3.showHidden === true || body3.showHidden === "1";
|
|
87142
|
+
const paths = [...new Set(
|
|
87143
|
+
raw.filter((p6) => typeof p6 === "string" && !!p6)
|
|
87144
|
+
)];
|
|
87145
|
+
const truncated = paths.length > LIST_MANY_MAX_PATHS;
|
|
87146
|
+
const wanted = paths.slice(0, LIST_MANY_MAX_PATHS);
|
|
87147
|
+
const deadline4 = Date.now() + LIST_MANY_DEADLINE_MS;
|
|
87148
|
+
const listings = [];
|
|
87149
|
+
let timedOut = false;
|
|
87150
|
+
let next_ = 0;
|
|
87151
|
+
const worker = async () => {
|
|
87152
|
+
for (; ; ) {
|
|
87153
|
+
const i8 = next_++;
|
|
87154
|
+
if (i8 >= wanted.length) return;
|
|
87155
|
+
if (Date.now() >= deadline4) {
|
|
87156
|
+
timedOut = true;
|
|
87157
|
+
return;
|
|
87158
|
+
}
|
|
87159
|
+
const dirPath = wanted[i8];
|
|
87160
|
+
try {
|
|
87161
|
+
listings.push(await listSessionFiles(dirPath, host, showHidden));
|
|
87162
|
+
} catch (err2) {
|
|
87163
|
+
listings.push({ path: dirPath, error: err2 instanceof Error ? err2.message : String(err2) });
|
|
87164
|
+
}
|
|
87165
|
+
}
|
|
87166
|
+
};
|
|
87167
|
+
await Promise.all(Array.from({ length: Math.min(LIST_MANY_CONCURRENCY, wanted.length) }, worker));
|
|
87168
|
+
res.json({ listings, truncated, timedOut });
|
|
87169
|
+
} catch (err2) {
|
|
87170
|
+
if (err2 instanceof FilesOpError) {
|
|
87171
|
+
res.status(err2.statusCode).json({ error: err2.message });
|
|
87172
|
+
return;
|
|
87173
|
+
}
|
|
87174
|
+
next(err2);
|
|
87175
|
+
}
|
|
87176
|
+
});
|
|
86725
87177
|
REVEAL_APP_DENY_EXTS = /* @__PURE__ */ new Set([
|
|
86726
87178
|
"command",
|
|
86727
87179
|
"sh",
|
|
@@ -88296,6 +88748,82 @@ var init_task = __esm({
|
|
|
88296
88748
|
}
|
|
88297
88749
|
});
|
|
88298
88750
|
|
|
88751
|
+
// src/core/sessions/spawn-prefix.ts
|
|
88752
|
+
var spawn_prefix_exports = {};
|
|
88753
|
+
__export(spawn_prefix_exports, {
|
|
88754
|
+
MAX_SPAWN_PROMPT_BYTES: () => MAX_SPAWN_PROMPT_BYTES,
|
|
88755
|
+
parseSpawnPrefixFromArgs: () => parseSpawnPrefixFromArgs,
|
|
88756
|
+
readParentSpawnPrefix: () => readParentSpawnPrefix,
|
|
88757
|
+
spawnPrefixFromRecord: () => spawnPrefixFromRecord
|
|
88758
|
+
});
|
|
88759
|
+
function flagValue(args, flag) {
|
|
88760
|
+
const i8 = args.indexOf(flag);
|
|
88761
|
+
if (i8 < 0 || i8 + 1 >= args.length) return void 0;
|
|
88762
|
+
const v3 = args[i8 + 1];
|
|
88763
|
+
return v3 !== void 0 && !v3.startsWith("--") ? v3 : void 0;
|
|
88764
|
+
}
|
|
88765
|
+
function parseSpawnPrefixFromArgs(args) {
|
|
88766
|
+
const prompt = flagValue(args, "--append-system-prompt");
|
|
88767
|
+
return {
|
|
88768
|
+
appendSystemPrompt: prompt !== void 0 && Buffer.byteLength(prompt, "utf8") <= MAX_SPAWN_PROMPT_BYTES ? prompt : null,
|
|
88769
|
+
...flagValue(args, "--model") ? { model: flagValue(args, "--model") } : {},
|
|
88770
|
+
...EFFORTS.includes(flagValue(args, "--effort") ?? "") ? { effort: flagValue(args, "--effort") } : {},
|
|
88771
|
+
...flagValue(args, "--permission-mode") ? { permissionMode: flagValue(args, "--permission-mode") } : {},
|
|
88772
|
+
source: "live-process"
|
|
88773
|
+
};
|
|
88774
|
+
}
|
|
88775
|
+
function spawnPrefixFromRecord(record) {
|
|
88776
|
+
const stored = record.appliedAppendSystemPrompt;
|
|
88777
|
+
if (stored === void 0 || stored === null) return { appendSystemPrompt: null, source: "unknown" };
|
|
88778
|
+
const usable = stored.length > 0 && Buffer.byteLength(stored, "utf8") <= MAX_SPAWN_PROMPT_BYTES;
|
|
88779
|
+
return { appendSystemPrompt: usable ? stored : null, source: "record" };
|
|
88780
|
+
}
|
|
88781
|
+
async function defaultLiveArgsReader(hostKey2, sessionId) {
|
|
88782
|
+
const { probeDaemonSessionArgs: probeDaemonSessionArgs2 } = await Promise.resolve().then(() => (init_daemon_connection(), daemon_connection_exports));
|
|
88783
|
+
return probeDaemonSessionArgs2(hostKey2, sessionId);
|
|
88784
|
+
}
|
|
88785
|
+
async function readParentSpawnPrefix(parent, deps = {}) {
|
|
88786
|
+
const readLiveArgs = deps.readLiveArgs ?? defaultLiveArgsReader;
|
|
88787
|
+
const hostKey2 = parent.host || "__local__";
|
|
88788
|
+
let live = null;
|
|
88789
|
+
try {
|
|
88790
|
+
live = await readLiveArgs(hostKey2, parent.claudeSessionId);
|
|
88791
|
+
} catch (err2) {
|
|
88792
|
+
log.session.debug("spawn prefix: live argv read threw", {
|
|
88793
|
+
sessionId: parent.claudeSessionId,
|
|
88794
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
88795
|
+
});
|
|
88796
|
+
}
|
|
88797
|
+
if (live && live.length > 0) {
|
|
88798
|
+
const prefix = parseSpawnPrefixFromArgs(live);
|
|
88799
|
+
const remembered = parent.appliedAppendSystemPrompt ?? void 0;
|
|
88800
|
+
const actual = prefix.appendSystemPrompt ?? "";
|
|
88801
|
+
if (remembered !== actual) {
|
|
88802
|
+
const persist4 = deps.persist ?? (async (sid, prompt) => {
|
|
88803
|
+
const { updateSessionRecord: updateSessionRecord2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
|
|
88804
|
+
return updateSessionRecord2(sid, { appliedAppendSystemPrompt: prompt });
|
|
88805
|
+
});
|
|
88806
|
+
void persist4(parent.claudeSessionId, actual).catch((err2) => {
|
|
88807
|
+
log.session.warn("spawn prefix: backfill of applied prompt failed", {
|
|
88808
|
+
sessionId: parent.claudeSessionId,
|
|
88809
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
88810
|
+
});
|
|
88811
|
+
});
|
|
88812
|
+
}
|
|
88813
|
+
return prefix;
|
|
88814
|
+
}
|
|
88815
|
+
return spawnPrefixFromRecord(parent);
|
|
88816
|
+
}
|
|
88817
|
+
var EFFORTS, MAX_SPAWN_PROMPT_BYTES;
|
|
88818
|
+
var init_spawn_prefix = __esm({
|
|
88819
|
+
"src/core/sessions/spawn-prefix.ts"() {
|
|
88820
|
+
"use strict";
|
|
88821
|
+
init_logging();
|
|
88822
|
+
EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
88823
|
+
MAX_SPAWN_PROMPT_BYTES = 65536;
|
|
88824
|
+
}
|
|
88825
|
+
});
|
|
88826
|
+
|
|
88299
88827
|
// src/core/sessions/side-thread-fork.ts
|
|
88300
88828
|
var side_thread_fork_exports = {};
|
|
88301
88829
|
__export(side_thread_fork_exports, {
|
|
@@ -88352,6 +88880,10 @@ async function forkSideThreadSession(parentSid, threadId, opts) {
|
|
|
88352
88880
|
const sessionId = crypto16.randomUUID();
|
|
88353
88881
|
const lane = sideThreadLaneKey(parentSid, threadId);
|
|
88354
88882
|
const mode = parent.mode !== "default" ? parent.mode : void 0;
|
|
88883
|
+
const { readParentSpawnPrefix: readParentSpawnPrefix2 } = await Promise.resolve().then(() => (init_spawn_prefix(), spawn_prefix_exports));
|
|
88884
|
+
const prefix = await readParentSpawnPrefix2(parent);
|
|
88885
|
+
const cliModel = prefix.model ?? parent.cliModel;
|
|
88886
|
+
const effort = prefix.effort ?? parent.effort;
|
|
88355
88887
|
await createSessionRecord(sessionId, "", parent.project ?? "", parent.cwd, {
|
|
88356
88888
|
title,
|
|
88357
88889
|
...mode ? { mode } : {},
|
|
@@ -88359,8 +88891,10 @@ async function forkSideThreadSession(parentSid, threadId, opts) {
|
|
|
88359
88891
|
// The thread inherits the parent's launch bundle verbatim, re-applied from
|
|
88360
88892
|
// the record on every cold resume.
|
|
88361
88893
|
...parent.profile ? { profile: parent.profile } : {},
|
|
88362
|
-
...
|
|
88363
|
-
...
|
|
88894
|
+
...effort ? { effort } : {},
|
|
88895
|
+
...cliModel ? { cliModel } : {},
|
|
88896
|
+
// '' = the parent runs without an append prompt, and so does the thread.
|
|
88897
|
+
appliedAppendSystemPrompt: prefix.appendSystemPrompt ?? "",
|
|
88364
88898
|
lane,
|
|
88365
88899
|
forkedFromSessionId: resumeFrom,
|
|
88366
88900
|
initialProcessStatus: "idle",
|
|
@@ -88376,11 +88910,12 @@ async function forkSideThreadSession(parentSid, threadId, opts) {
|
|
|
88376
88910
|
...mode ? { mode } : {},
|
|
88377
88911
|
...parent.host ? { host: parent.host } : {},
|
|
88378
88912
|
...parent.profile ? { profile: parent.profile } : {},
|
|
88379
|
-
...
|
|
88913
|
+
...effort ? { effort } : {},
|
|
88380
88914
|
// Must be `cliModel` (the parent's verbatim --model arg, [1m] marker
|
|
88381
88915
|
// included): the CLI does NOT inherit a model across --fork-session, and
|
|
88382
88916
|
// the reported `model` has lost the marker.
|
|
88383
|
-
...
|
|
88917
|
+
...cliModel ? { model: cliModel } : {},
|
|
88918
|
+
appendSystemPromptExact: prefix.appendSystemPrompt,
|
|
88384
88919
|
lane,
|
|
88385
88920
|
forkedFromSessionId: resumeFrom
|
|
88386
88921
|
}, ["session-runner"], { source: "side-thread-fork" });
|
|
@@ -88389,7 +88924,9 @@ async function forkSideThreadSession(parentSid, threadId, opts) {
|
|
|
88389
88924
|
threadId,
|
|
88390
88925
|
sessionId,
|
|
88391
88926
|
resumeFrom,
|
|
88392
|
-
initOnly: !opts?.message
|
|
88927
|
+
initOnly: !opts?.message,
|
|
88928
|
+
prefixSource: prefix.source,
|
|
88929
|
+
prefixPromptLength: prefix.appendSystemPrompt?.length ?? 0
|
|
88393
88930
|
});
|
|
88394
88931
|
return { sessionId, resumeFromSessionId: resumeFrom };
|
|
88395
88932
|
}
|
|
@@ -100645,14 +101182,16 @@ function isLive(r8) {
|
|
|
100645
101182
|
function isRevivable(r8) {
|
|
100646
101183
|
return !!r8.outputFile || r8.consumedOffset !== void 0;
|
|
100647
101184
|
}
|
|
100648
|
-
var STANDBY_TTL_MS, MAX_LIVE_THREADS, IDLE_SWEEP_INTERVAL_MS, THREAD_IDLE_MS, SideThreadManager, sideThreadManager;
|
|
101185
|
+
var STANDBY_TTL_MS, WARMED_STANDBY_TTL_MS, MAX_LIVE_THREADS, IDLE_SWEEP_INTERVAL_MS, THREAD_IDLE_MS, SideThreadManager, sideThreadManager;
|
|
100649
101186
|
var init_side_thread_manager = __esm({
|
|
100650
101187
|
"src/core/sessions/side-thread-manager.ts"() {
|
|
100651
101188
|
"use strict";
|
|
100652
101189
|
init_logging();
|
|
100653
101190
|
init_side_thread_fork();
|
|
100654
101191
|
init_session_controls();
|
|
101192
|
+
init_side_thread_warmup();
|
|
100655
101193
|
STANDBY_TTL_MS = 12e4;
|
|
101194
|
+
WARMED_STANDBY_TTL_MS = 15 * 6e4;
|
|
100656
101195
|
MAX_LIVE_THREADS = 3;
|
|
100657
101196
|
IDLE_SWEEP_INTERVAL_MS = 5 * 6e4;
|
|
100658
101197
|
THREAD_IDLE_MS = 30 * 6e4;
|
|
@@ -100663,6 +101202,8 @@ var init_side_thread_manager = __esm({
|
|
|
100663
101202
|
* In-memory only: a server restart loses it, but the boot sweep archives
|
|
100664
101203
|
* every standby anyway, so a record can never outlive its map entry. */
|
|
100665
101204
|
standbyParentOffsets = /* @__PURE__ */ new Map();
|
|
101205
|
+
/** parentSid → standby sid that already ran its cache warm-up turn. */
|
|
101206
|
+
warmedStandbys = /* @__PURE__ */ new Map();
|
|
100666
101207
|
/** One in-flight standby/create op per parent — two concurrent asks must not
|
|
100667
101208
|
* both consume the same standby (they would collide on its lane rename). */
|
|
100668
101209
|
inFlight = /* @__PURE__ */ new Map();
|
|
@@ -100694,6 +101235,7 @@ var init_side_thread_manager = __esm({
|
|
|
100694
101235
|
for (const timer3 of this.standbyTimers.values()) clearTimeout(timer3);
|
|
100695
101236
|
this.standbyTimers.clear();
|
|
100696
101237
|
this.standbyParentOffsets.clear();
|
|
101238
|
+
this.warmedStandbys.clear();
|
|
100697
101239
|
this.inFlight.clear();
|
|
100698
101240
|
}
|
|
100699
101241
|
// ── Standby ────────────────────────────────────────────────────────────────
|
|
@@ -100709,10 +101251,12 @@ var init_side_thread_manager = __esm({
|
|
|
100709
101251
|
const existing = await this.findStandby(parentSid);
|
|
100710
101252
|
if (existing) {
|
|
100711
101253
|
if (await this.isStandbyUsable(parentSid, existing)) {
|
|
100712
|
-
this.
|
|
101254
|
+
const warmed = this.warmedStandbys.get(parentSid) === existing.claudeSessionId;
|
|
101255
|
+
this.armStandbyTtl(parentSid, existing.claudeSessionId, warmed ? WARMED_STANDBY_TTL_MS : STANDBY_TTL_MS);
|
|
100713
101256
|
return existing.claudeSessionId;
|
|
100714
101257
|
}
|
|
100715
101258
|
void this.retireSession(existing.claudeSessionId, "side_thread_standby_stale");
|
|
101259
|
+
this.warmedStandbys.delete(parentSid);
|
|
100716
101260
|
}
|
|
100717
101261
|
const parentOffset = parent.consumedOffset;
|
|
100718
101262
|
const { sessionId } = await forkSideThreadSession(parentSid, STANDBY_THREAD_ID);
|
|
@@ -100738,7 +101282,7 @@ var init_side_thread_manager = __esm({
|
|
|
100738
101282
|
if (!parent) return false;
|
|
100739
101283
|
return parent.consumedOffset === this.standbyParentOffsets.get(parentSid);
|
|
100740
101284
|
}
|
|
100741
|
-
armStandbyTtl(parentSid, sessionId) {
|
|
101285
|
+
armStandbyTtl(parentSid, sessionId, ttlMs = STANDBY_TTL_MS) {
|
|
100742
101286
|
const prev = this.standbyTimers.get(parentSid);
|
|
100743
101287
|
if (prev) clearTimeout(prev);
|
|
100744
101288
|
const timer3 = setTimeout(() => {
|
|
@@ -100748,13 +101292,41 @@ var init_side_thread_manager = __esm({
|
|
|
100748
101292
|
const record = await getSessionByClaudeId2(sessionId).catch(() => null);
|
|
100749
101293
|
if (!record || parseSideLaneKey(record.lane)?.threadId !== STANDBY_THREAD_ID) return;
|
|
100750
101294
|
this.standbyParentOffsets.delete(parentSid);
|
|
101295
|
+
this.warmedStandbys.delete(parentSid);
|
|
100751
101296
|
await this.retireSession(sessionId, "side_thread_standby_ttl");
|
|
100752
101297
|
}).catch(() => {
|
|
100753
101298
|
});
|
|
100754
|
-
},
|
|
101299
|
+
}, ttlMs);
|
|
100755
101300
|
timer3.unref?.();
|
|
100756
101301
|
this.standbyTimers.set(parentSid, timer3);
|
|
100757
101302
|
}
|
|
101303
|
+
/**
|
|
101304
|
+
* Run the standby's cache warm-up turn (see side-thread-warmup.ts). Called
|
|
101305
|
+
* when the user starts TYPING a new question: the fork's first API call pays
|
|
101306
|
+
* the full prefix write no matter what, so paying it while they type turns
|
|
101307
|
+
* the actual question into a cached follow-up. Idempotent per standby; a
|
|
101308
|
+
* missing or stale standby is a no-op (the ask path will fork fresh anyway).
|
|
101309
|
+
*/
|
|
101310
|
+
async warmStandby(parentSid) {
|
|
101311
|
+
return this.serialize(parentSid, async () => {
|
|
101312
|
+
const standby = await this.findStandby(parentSid);
|
|
101313
|
+
if (!standby) return { warmed: false, reason: "no_standby" };
|
|
101314
|
+
if (!await this.isStandbyUsable(parentSid, standby)) return { warmed: false, reason: "stale" };
|
|
101315
|
+
if (this.warmedStandbys.get(parentSid) === standby.claudeSessionId) {
|
|
101316
|
+
return { warmed: true, reason: "already_warm" };
|
|
101317
|
+
}
|
|
101318
|
+
const { sendMessageToSession: sendMessageToSession2 } = await Promise.resolve().then(() => (init_session_message_queue(), session_message_queue_exports));
|
|
101319
|
+
markWarmupTurnPending(standby.claudeSessionId);
|
|
101320
|
+
await sendMessageToSession2(standby.claudeSessionId, CACHE_WARMUP_MESSAGE, { source: "side-thread-warmup" });
|
|
101321
|
+
this.warmedStandbys.set(parentSid, standby.claudeSessionId);
|
|
101322
|
+
this.armStandbyTtl(parentSid, standby.claudeSessionId, WARMED_STANDBY_TTL_MS);
|
|
101323
|
+
log.session.info("side thread: standby cache warm-up sent", {
|
|
101324
|
+
parentSid,
|
|
101325
|
+
standbySid: standby.claudeSessionId
|
|
101326
|
+
});
|
|
101327
|
+
return { warmed: true };
|
|
101328
|
+
});
|
|
101329
|
+
}
|
|
100758
101330
|
// ── Threads ────────────────────────────────────────────────────────────────
|
|
100759
101331
|
/**
|
|
100760
101332
|
* Open a side thread on `parentSid` and ask `question` in it.
|
|
@@ -100765,6 +101337,10 @@ var init_side_thread_manager = __esm({
|
|
|
100765
101337
|
* question as its spawn's first turn instead — a send issued right after
|
|
100766
101338
|
* SESSION_START can lose that race and cold-`--resume` an id the CLI has never
|
|
100767
101339
|
* seen (see personal-ai-lane.ts LaneSession.created).
|
|
101340
|
+
*
|
|
101341
|
+
* `imageContext` (attachment paths for the CLI to Read) rides the MESSAGE only.
|
|
101342
|
+
* The stored entry keeps the plain question, so the chip label, the injected
|
|
101343
|
+
* transcript and a promoted task never carry the machine preamble.
|
|
100768
101344
|
*/
|
|
100769
101345
|
async createThread(parentSid, input) {
|
|
100770
101346
|
const question = input.question?.trim();
|
|
@@ -100772,8 +101348,11 @@ var init_side_thread_manager = __esm({
|
|
|
100772
101348
|
return this.serialize(parentSid, async () => {
|
|
100773
101349
|
const threadId = mintSideThreadId();
|
|
100774
101350
|
const title = input.title?.trim() || void 0;
|
|
101351
|
+
const message = input.imageContext ? `${input.imageContext}
|
|
101352
|
+
|
|
101353
|
+
${question}` : question;
|
|
100775
101354
|
const consumed = await this.consumeStandby(parentSid, threadId, title);
|
|
100776
|
-
const threadSessionId = consumed ?? (await forkSideThreadSession(parentSid, threadId, { message
|
|
101355
|
+
const threadSessionId = consumed ?? (await forkSideThreadSession(parentSid, threadId, { message, ...title ? { title } : {} })).sessionId;
|
|
100777
101356
|
let entry;
|
|
100778
101357
|
try {
|
|
100779
101358
|
const { addSideThread: addSideThread2 } = await Promise.resolve().then(() => (init_side_questions(), side_questions_exports));
|
|
@@ -100785,7 +101364,7 @@ var init_side_thread_manager = __esm({
|
|
|
100785
101364
|
});
|
|
100786
101365
|
if (consumed) {
|
|
100787
101366
|
const { sendMessageToSession: sendMessageToSession2 } = await Promise.resolve().then(() => (init_session_message_queue(), session_message_queue_exports));
|
|
100788
|
-
await sendMessageToSession2(threadSessionId,
|
|
101367
|
+
await sendMessageToSession2(threadSessionId, message, { source: "side-thread" });
|
|
100789
101368
|
}
|
|
100790
101369
|
} catch (err2) {
|
|
100791
101370
|
void this.retireSession(threadSessionId, "side_thread_create_failed");
|
|
@@ -100820,6 +101399,7 @@ var init_side_thread_manager = __esm({
|
|
|
100820
101399
|
if (!await this.isStandbyUsable(parentSid, standby)) {
|
|
100821
101400
|
void this.retireSession(standby.claudeSessionId, "side_thread_standby_stale");
|
|
100822
101401
|
this.standbyParentOffsets.delete(parentSid);
|
|
101402
|
+
this.warmedStandbys.delete(parentSid);
|
|
100823
101403
|
return null;
|
|
100824
101404
|
}
|
|
100825
101405
|
const timer3 = this.standbyTimers.get(parentSid);
|
|
@@ -100828,6 +101408,7 @@ var init_side_thread_manager = __esm({
|
|
|
100828
101408
|
this.standbyTimers.delete(parentSid);
|
|
100829
101409
|
}
|
|
100830
101410
|
this.standbyParentOffsets.delete(parentSid);
|
|
101411
|
+
this.warmedStandbys.delete(parentSid);
|
|
100831
101412
|
const lane = sideThreadLaneKey(parentSid, threadId);
|
|
100832
101413
|
const { updateSessionRecord: updateSessionRecord2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
|
|
100833
101414
|
await updateSessionRecord2(standby.claudeSessionId, {
|
|
@@ -101014,16 +101595,30 @@ async function promoteSideThread(parentSid, threadId, opts) {
|
|
|
101014
101595
|
const threadRecord = await getSessionByClaudeId2(sessionId);
|
|
101015
101596
|
if (!threadRecord) throw new SessionControlError("Side thread session not found", 404);
|
|
101016
101597
|
const parentRecord = await getSessionByClaudeId2(parentSid);
|
|
101017
|
-
const
|
|
101018
|
-
const
|
|
101598
|
+
const siblingOfTaskId = parentRecord?.taskId?.trim() || void 0;
|
|
101599
|
+
const threadLabel = opts?.title?.trim() || entry.title?.trim() || void 0;
|
|
101019
101600
|
const { addTask: addTask2, linkSession: linkSession2, linkSessionSlot: linkSessionSlot2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
|
|
101020
|
-
|
|
101021
|
-
|
|
101022
|
-
|
|
101023
|
-
|
|
101024
|
-
|
|
101025
|
-
|
|
101026
|
-
|
|
101601
|
+
let groupId;
|
|
101602
|
+
let task;
|
|
101603
|
+
if (siblingOfTaskId) {
|
|
101604
|
+
const created = await createForkSiblingTask(siblingOfTaskId, {
|
|
101605
|
+
// A known label short-circuits the background refine; without one the
|
|
101606
|
+
// thread's first question is what gets summarized (same as a fork's message).
|
|
101607
|
+
titlePrefix: threadLabel,
|
|
101608
|
+
prompt: entry.question,
|
|
101609
|
+
description: entry.question,
|
|
101610
|
+
source: "side-thread-promote"
|
|
101611
|
+
});
|
|
101612
|
+
task = created.task;
|
|
101613
|
+
groupId = created.groupId;
|
|
101614
|
+
} else {
|
|
101615
|
+
({ task } = await addTask2({
|
|
101616
|
+
title: threadLabel || entry.question,
|
|
101617
|
+
description: entry.question,
|
|
101618
|
+
// A person clicked "promote" — same board default as any hand-made task.
|
|
101619
|
+
pinned: true
|
|
101620
|
+
}));
|
|
101621
|
+
}
|
|
101027
101622
|
const stamped = await markThreadPromoted2(parentSid, threadId, task.id);
|
|
101028
101623
|
if (!stamped) {
|
|
101029
101624
|
log.session.warn("side thread promote: entry vanished before stamp \u2014 promote proceeds, 409 guard is unarmed", {
|
|
@@ -101037,7 +101632,7 @@ async function promoteSideThread(parentSid, threadId, opts) {
|
|
|
101037
101632
|
project: task.project ?? "",
|
|
101038
101633
|
// Clearing the lane un-hides the session (see the file header).
|
|
101039
101634
|
lane: void 0,
|
|
101040
|
-
title
|
|
101635
|
+
title: task.title
|
|
101041
101636
|
});
|
|
101042
101637
|
try {
|
|
101043
101638
|
const { sessionRunner: sessionRunner2 } = await Promise.resolve().then(() => (init_claude_code_session(), claude_code_session_exports));
|
|
@@ -101056,10 +101651,18 @@ async function promoteSideThread(parentSid, threadId, opts) {
|
|
|
101056
101651
|
error: err2 instanceof Error ? err2.message : String(err2)
|
|
101057
101652
|
});
|
|
101058
101653
|
}
|
|
101059
|
-
log.session.info("side thread: promoted to task", {
|
|
101654
|
+
log.session.info("side thread: promoted to task", {
|
|
101655
|
+
parentSid,
|
|
101656
|
+
threadId,
|
|
101657
|
+
sessionId,
|
|
101658
|
+
taskId: task.id,
|
|
101659
|
+
siblingOfTaskId,
|
|
101660
|
+
groupId
|
|
101661
|
+
});
|
|
101060
101662
|
return {
|
|
101061
101663
|
taskId: task.id,
|
|
101062
|
-
...
|
|
101664
|
+
...siblingOfTaskId ? { siblingOfTaskId } : {},
|
|
101665
|
+
...groupId ? { groupId } : {},
|
|
101063
101666
|
sessionId
|
|
101064
101667
|
};
|
|
101065
101668
|
}
|
|
@@ -101074,7 +101677,7 @@ var init_side_thread_promote = __esm({
|
|
|
101074
101677
|
|
|
101075
101678
|
// src/web/routes/sessions.ts
|
|
101076
101679
|
import { Router as Router10 } from "express";
|
|
101077
|
-
import { randomUUID as
|
|
101680
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
101078
101681
|
import os19 from "os";
|
|
101079
101682
|
import path91 from "path";
|
|
101080
101683
|
function condenseStaleReason(msg) {
|
|
@@ -101420,7 +102023,7 @@ var init_sessions2 = __esm({
|
|
|
101420
102023
|
}
|
|
101421
102024
|
const isNativeEngine = !isAcpEngine(engine);
|
|
101422
102025
|
const clientSessionId = typeof req.body.sessionId === "string" && UUID_RE2.test(req.body.sessionId) ? req.body.sessionId : void 0;
|
|
101423
|
-
const preassignedSessionId = isNativeEngine ? clientSessionId ??
|
|
102026
|
+
const preassignedSessionId = isNativeEngine ? clientSessionId ?? randomUUID6() : void 0;
|
|
101424
102027
|
const updatedTask = await quickStartSession({
|
|
101425
102028
|
message: sessionMessage,
|
|
101426
102029
|
messagePrefix,
|
|
@@ -102395,18 +102998,38 @@ var init_sessions2 = __esm({
|
|
|
102395
102998
|
});
|
|
102396
102999
|
}
|
|
102397
103000
|
});
|
|
103001
|
+
sessionsRouter.post("/:sessionId/side-threads/standby/warm", async (req, res) => {
|
|
103002
|
+
if (refuseSideThreadsOnReplica(res)) return;
|
|
103003
|
+
const sessionId = String(req.params.sessionId);
|
|
103004
|
+
try {
|
|
103005
|
+
const { sideThreadManager: sideThreadManager2 } = await Promise.resolve().then(() => (init_side_thread_manager(), side_thread_manager_exports));
|
|
103006
|
+
res.json(await sideThreadManager2.warmStandby(sessionId));
|
|
103007
|
+
} catch (err2) {
|
|
103008
|
+
log.web.warn("side thread standby warm-up failed", {
|
|
103009
|
+
sessionId,
|
|
103010
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
103011
|
+
});
|
|
103012
|
+
res.json({ warmed: false, reason: "error" });
|
|
103013
|
+
}
|
|
103014
|
+
});
|
|
102398
103015
|
sessionsRouter.post("/:sessionId/side-threads", async (req, res, next) => {
|
|
102399
103016
|
if (refuseSideThreadsOnReplica(res)) return;
|
|
102400
103017
|
try {
|
|
102401
|
-
const { question, title } = req.body ?? {};
|
|
103018
|
+
const { question, title, images } = req.body ?? {};
|
|
102402
103019
|
if (!question || typeof question !== "string" || !question.trim()) {
|
|
102403
103020
|
res.status(400).json({ error: "question (non-empty string) is required" });
|
|
102404
103021
|
return;
|
|
102405
103022
|
}
|
|
103023
|
+
let imageContext = "";
|
|
103024
|
+
if (images && images.length > 0) {
|
|
103025
|
+
const processed = await processAndSaveImages(images);
|
|
103026
|
+
if (processed) imageContext = buildSessionImageContext(processed.savedImages);
|
|
103027
|
+
}
|
|
102406
103028
|
const { sideThreadManager: sideThreadManager2 } = await Promise.resolve().then(() => (init_side_thread_manager(), side_thread_manager_exports));
|
|
102407
103029
|
const thread = await sideThreadManager2.createThread(String(req.params.sessionId), {
|
|
102408
103030
|
question,
|
|
102409
|
-
...typeof title === "string" && title.trim() ? { title } : {}
|
|
103031
|
+
...typeof title === "string" && title.trim() ? { title } : {},
|
|
103032
|
+
...imageContext ? { imageContext } : {}
|
|
102410
103033
|
});
|
|
102411
103034
|
res.json({ thread });
|
|
102412
103035
|
} catch (err2) {
|
|
@@ -102425,7 +103048,8 @@ var init_sessions2 = __esm({
|
|
|
102425
103048
|
);
|
|
102426
103049
|
res.json({
|
|
102427
103050
|
taskId: result.taskId,
|
|
102428
|
-
...result.
|
|
103051
|
+
...result.siblingOfTaskId ? { siblingOfTaskId: result.siblingOfTaskId } : {},
|
|
103052
|
+
...result.groupId ? { groupId: result.groupId } : {},
|
|
102429
103053
|
sessionId: result.sessionId
|
|
102430
103054
|
});
|
|
102431
103055
|
} catch (err2) {
|
|
@@ -102616,6 +103240,19 @@ var init_sessions2 = __esm({
|
|
|
102616
103240
|
next(err2);
|
|
102617
103241
|
}
|
|
102618
103242
|
});
|
|
103243
|
+
sessionsRouter.post("/:sessionId/recheck", async (req, res, next) => {
|
|
103244
|
+
try {
|
|
103245
|
+
const sessionId = req.params.sessionId;
|
|
103246
|
+
const result = await recheckSession(sessionId);
|
|
103247
|
+
res.json(result);
|
|
103248
|
+
} catch (err2) {
|
|
103249
|
+
if (err2 instanceof SessionControlError) {
|
|
103250
|
+
res.status(err2.statusCode).json({ error: err2.message });
|
|
103251
|
+
return;
|
|
103252
|
+
}
|
|
103253
|
+
next(err2);
|
|
103254
|
+
}
|
|
103255
|
+
});
|
|
102619
103256
|
sessionsRouter.post("/:sessionId/restart", async (req, res, next) => {
|
|
102620
103257
|
const sessionId = req.params.sessionId;
|
|
102621
103258
|
const startedAt = Date.now();
|
|
@@ -103107,7 +103744,7 @@ var micro_claude_exports = {};
|
|
|
103107
103744
|
__export(micro_claude_exports, {
|
|
103108
103745
|
runMicroClaude: () => runMicroClaude
|
|
103109
103746
|
});
|
|
103110
|
-
import { randomUUID as
|
|
103747
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
103111
103748
|
async function runMicroClaude(opts) {
|
|
103112
103749
|
if (opts.warm) {
|
|
103113
103750
|
const run10 = await runWarmMicroClaude({
|
|
@@ -103116,7 +103753,7 @@ async function runMicroClaude(opts) {
|
|
|
103116
103753
|
tools: opts.tools ?? [],
|
|
103117
103754
|
prompt: opts.prompt,
|
|
103118
103755
|
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
|
|
103119
|
-
toolUseId: opts.toolUseId ?? `micro-claude-${
|
|
103756
|
+
toolUseId: opts.toolUseId ?? `micro-claude-${randomUUID7()}`,
|
|
103120
103757
|
...opts.onBlock ? { onBlock: opts.onBlock } : {}
|
|
103121
103758
|
});
|
|
103122
103759
|
return { response: run10.response, costUsd: run10.costUsd, durationMs: run10.durationMs };
|
|
@@ -103126,7 +103763,7 @@ async function runMicroClaude(opts) {
|
|
|
103126
103763
|
systemPrompt: opts.system,
|
|
103127
103764
|
model: opts.model ?? DEFAULT_MODEL2,
|
|
103128
103765
|
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
|
|
103129
|
-
toolUseId: opts.toolUseId ?? `micro-claude-${
|
|
103766
|
+
toolUseId: opts.toolUseId ?? `micro-claude-${randomUUID7()}`,
|
|
103130
103767
|
slim: true,
|
|
103131
103768
|
thinking: opts.thinking ?? false,
|
|
103132
103769
|
...opts.tools && opts.tools.length > 0 ? { tools: opts.tools } : {},
|
|
@@ -103951,7 +104588,7 @@ __export(micro_agent_exports, {
|
|
|
103951
104588
|
async function resolveTierModel(tier) {
|
|
103952
104589
|
const { getConfig: getConfig2 } = await Promise.resolve().then(() => (init_config_manager(), config_manager_exports));
|
|
103953
104590
|
const config = await getConfig2();
|
|
103954
|
-
const provider = config
|
|
104591
|
+
const provider = resolveMainProviderName(config);
|
|
103955
104592
|
const { MODEL_CATALOG: MODEL_CATALOG3 } = await Promise.resolve().then(() => (init_model_catalog(), model_catalog_exports));
|
|
103956
104593
|
const entry = MODEL_CATALOG3[provider]?.find((m6) => {
|
|
103957
104594
|
const id = m6.id.toLowerCase();
|
|
@@ -104020,6 +104657,7 @@ var DEFAULT_TIER, DEFAULT_MAX_TOKENS2, DEFAULT_MAX_ROUNDS, DEFAULT_TIMEOUT_MS3;
|
|
|
104020
104657
|
var init_micro_agent = __esm({
|
|
104021
104658
|
"src/agent/micro-agent.ts"() {
|
|
104022
104659
|
"use strict";
|
|
104660
|
+
init_default_provider();
|
|
104023
104661
|
DEFAULT_TIER = "sonnet";
|
|
104024
104662
|
DEFAULT_MAX_TOKENS2 = 2e3;
|
|
104025
104663
|
DEFAULT_MAX_ROUNDS = 3;
|
|
@@ -104035,7 +104673,7 @@ __export(task_search_agent_exports, {
|
|
|
104035
104673
|
prewarmAgentSearchChild: () => prewarmAgentSearchChild,
|
|
104036
104674
|
runTaskSearchAgent: () => runTaskSearchAgent
|
|
104037
104675
|
});
|
|
104038
|
-
import { randomUUID as
|
|
104676
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
104039
104677
|
function useCliEngine() {
|
|
104040
104678
|
return process.env.WALNUT_AGENT_SEARCH_ENGINE !== "inprocess";
|
|
104041
104679
|
}
|
|
@@ -104115,7 +104753,7 @@ async function claudeCliEngine(userPrompt, options) {
|
|
|
104115
104753
|
model: options.model,
|
|
104116
104754
|
timeoutMs: options.timeoutMs,
|
|
104117
104755
|
tools: ["Bash"],
|
|
104118
|
-
toolUseId: `task-search-${
|
|
104756
|
+
toolUseId: `task-search-${randomUUID8()}`,
|
|
104119
104757
|
warm: true,
|
|
104120
104758
|
onBlock
|
|
104121
104759
|
});
|
|
@@ -105319,9 +105957,13 @@ var init_credential_verify = __esm({
|
|
|
105319
105957
|
var config_exports = {};
|
|
105320
105958
|
__export(config_exports, {
|
|
105321
105959
|
buildProvidersPayload: () => buildProvidersPayload,
|
|
105322
|
-
configRouter: () => configRouter
|
|
105960
|
+
configRouter: () => configRouter,
|
|
105961
|
+
setStaticRootReporter: () => setStaticRootReporter
|
|
105323
105962
|
});
|
|
105324
105963
|
import { Router as Router14 } from "express";
|
|
105964
|
+
function setStaticRootReporter(fn) {
|
|
105965
|
+
staticRootReporter = fn;
|
|
105966
|
+
}
|
|
105325
105967
|
async function fetchOllamaModels(baseUrl) {
|
|
105326
105968
|
if (Date.now() - _ollamaCache.ts < OLLAMA_CACHE_TTL) return _ollamaCache.models;
|
|
105327
105969
|
const url = (baseUrl ?? DEFAULT_BASE_URLS["ollama"] ?? "http://localhost:11434") + "/api/tags";
|
|
@@ -105374,10 +106016,11 @@ async function buildProvidersPayload() {
|
|
|
105374
106016
|
providers[name] = {
|
|
105375
106017
|
api: prov.api,
|
|
105376
106018
|
base_url: prov.base_url,
|
|
105377
|
-
status: caps.
|
|
106019
|
+
status: caps.installed ? "ready" : "no_key",
|
|
105378
106020
|
auto_detected: !explicitNames.has(name),
|
|
105379
106021
|
models: getModelsForProvider(name, prov.models),
|
|
105380
|
-
credential_source: caps.
|
|
106022
|
+
credential_source: caps.installed ? `cli_${caps.auth.mode}` : "cli_not_installed",
|
|
106023
|
+
credential_detail: caps.installed ? caps.auth.label : void 0
|
|
105381
106024
|
};
|
|
105382
106025
|
continue;
|
|
105383
106026
|
}
|
|
@@ -105436,10 +106079,11 @@ async function buildProvidersPayload() {
|
|
|
105436
106079
|
const caps = detectClaudeCli2();
|
|
105437
106080
|
providers[name] = {
|
|
105438
106081
|
api: template.api,
|
|
105439
|
-
status: caps.
|
|
106082
|
+
status: caps.installed ? "ready" : "no_key",
|
|
105440
106083
|
auto_detected: false,
|
|
105441
106084
|
models: getModelsForProvider(name),
|
|
105442
|
-
credential_source: caps.
|
|
106085
|
+
credential_source: caps.installed ? `cli_${caps.auth.mode}` : "cli_not_installed",
|
|
106086
|
+
credential_detail: caps.installed ? caps.auth.label : void 0
|
|
105443
106087
|
};
|
|
105444
106088
|
continue;
|
|
105445
106089
|
}
|
|
@@ -105454,7 +106098,7 @@ async function buildProvidersPayload() {
|
|
|
105454
106098
|
}
|
|
105455
106099
|
return { providers };
|
|
105456
106100
|
}
|
|
105457
|
-
var configRouter, _ollamaCache, OLLAMA_CACHE_TTL;
|
|
106101
|
+
var configRouter, staticRootReporter, _ollamaCache, OLLAMA_CACHE_TTL;
|
|
105458
106102
|
var init_config11 = __esm({
|
|
105459
106103
|
"src/web/routes/config.ts"() {
|
|
105460
106104
|
"use strict";
|
|
@@ -105469,6 +106113,7 @@ var init_config11 = __esm({
|
|
|
105469
106113
|
init_defaults();
|
|
105470
106114
|
init_config_redact();
|
|
105471
106115
|
configRouter = Router14();
|
|
106116
|
+
staticRootReporter = null;
|
|
105472
106117
|
configRouter.get("/", async (_req, res, next) => {
|
|
105473
106118
|
try {
|
|
105474
106119
|
const config = await getConfig();
|
|
@@ -105495,7 +106140,8 @@ var init_config11 = __esm({
|
|
|
105495
106140
|
remoteIdUniquenessGaps = getExtIndexUniquenessGaps2();
|
|
105496
106141
|
} catch {
|
|
105497
106142
|
}
|
|
105498
|
-
|
|
106143
|
+
const webAssets = staticRootReporter?.() ?? null;
|
|
106144
|
+
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
106145
|
} catch (err2) {
|
|
105500
106146
|
next(err2);
|
|
105501
106147
|
}
|
|
@@ -105593,7 +106239,7 @@ var init_config11 = __esm({
|
|
|
105593
106239
|
const {
|
|
105594
106240
|
traceCredentialResolution: traceCredentialResolution2,
|
|
105595
106241
|
resolveCredentials: resolveCredentials2,
|
|
105596
|
-
readClaudeSettingsEnv:
|
|
106242
|
+
readClaudeSettingsEnv: readClaudeSettingsEnv3,
|
|
105597
106243
|
readClaudeCredentialExport: readClaudeCredentialExport2
|
|
105598
106244
|
} = await Promise.resolve().then(() => (init_credential_resolver(), credential_resolver_exports));
|
|
105599
106245
|
const fs121 = await import("fs");
|
|
@@ -105609,7 +106255,7 @@ var init_config11 = __esm({
|
|
|
105609
106255
|
};
|
|
105610
106256
|
const trace = traceCredentialResolution2({
|
|
105611
106257
|
config,
|
|
105612
|
-
claudeEnv:
|
|
106258
|
+
claudeEnv: readClaudeSettingsEnv3(),
|
|
105613
106259
|
processEnv: process.env,
|
|
105614
106260
|
awsFiles: {
|
|
105615
106261
|
credentials: exists3(path193.join(home, ".aws", "credentials")),
|
|
@@ -110364,6 +111010,9 @@ var init_session_chat = __esm({
|
|
|
110364
111010
|
|
|
110365
111011
|
// src/web/routes/browser-logs.ts
|
|
110366
111012
|
import { Router as Router25 } from "express";
|
|
111013
|
+
function entrySubsystem(entry) {
|
|
111014
|
+
return entry.subsystem && ENTRY_SUBSYSTEMS.has(entry.subsystem) ? entry.subsystem : "browser";
|
|
111015
|
+
}
|
|
110367
111016
|
function checkRate(client, entryCount) {
|
|
110368
111017
|
const now = Date.now();
|
|
110369
111018
|
let state9 = clientRates.get(client);
|
|
@@ -110405,10 +111054,11 @@ function writeEntries(entries) {
|
|
|
110405
111054
|
if (entry.args) meta.args = truncate(entry.args, 1e3);
|
|
110406
111055
|
if (entry.url) meta.url = entry.url;
|
|
110407
111056
|
if (entry.count && entry.count > 1) meta.count = entry.count;
|
|
111057
|
+
if (entry.client === "mac-app") meta.client = entry.client;
|
|
110408
111058
|
writeLogEntry({
|
|
110409
111059
|
time: entry.time,
|
|
110410
111060
|
level: mapLevel(entry.level),
|
|
110411
|
-
subsystem:
|
|
111061
|
+
subsystem: entrySubsystem(entry),
|
|
110412
111062
|
message: truncate(entry.message, 2e3),
|
|
110413
111063
|
browserLevel: entry.level,
|
|
110414
111064
|
// preserve original level (e.g. 'log' vs 'info')
|
|
@@ -110431,13 +111081,14 @@ function registerBrowserLogsRpc() {
|
|
|
110431
111081
|
writeEntries(entries);
|
|
110432
111082
|
});
|
|
110433
111083
|
}
|
|
110434
|
-
var RATE_WINDOW_MS, RATE_LIMIT, clientRates, REST_MAX_ENTRIES, browserLogsRouter;
|
|
111084
|
+
var ENTRY_SUBSYSTEMS, RATE_WINDOW_MS, RATE_LIMIT, clientRates, REST_MAX_ENTRIES, browserLogsRouter;
|
|
110435
111085
|
var init_browser_logs = __esm({
|
|
110436
111086
|
"src/web/routes/browser-logs.ts"() {
|
|
110437
111087
|
"use strict";
|
|
110438
111088
|
init_handler();
|
|
110439
111089
|
init_logging();
|
|
110440
111090
|
init_logger();
|
|
111091
|
+
ENTRY_SUBSYSTEMS = /* @__PURE__ */ new Set(["browser", "desktop"]);
|
|
110441
111092
|
RATE_WINDOW_MS = 1e4;
|
|
110442
111093
|
RATE_LIMIT = 500;
|
|
110443
111094
|
clientRates = /* @__PURE__ */ new WeakMap();
|
|
@@ -118518,6 +119169,7 @@ var init_session_health_monitor = __esm({
|
|
|
118518
119169
|
log.session.info("health monitor: drained orphan dead-pool", { orphanCount: orphanIds.length, written: written.length });
|
|
118519
119170
|
}
|
|
118520
119171
|
if (sessions3.length === 0) {
|
|
119172
|
+
if (!ctx.overBudget()) await this.checkSnapshotPull(sessions3, ctx, allSessions);
|
|
118521
119173
|
const total = Date.now() - checkT0;
|
|
118522
119174
|
if (total > 500) log.session.warn("health monitor: check() slow (no active sessions)", { totalMs: total, orphanMs: tOrphan - checkT0, recoverMs: tRecover - tOrphan });
|
|
118523
119175
|
return;
|
|
@@ -118547,7 +119199,7 @@ var init_session_health_monitor = __esm({
|
|
|
118547
119199
|
await this.reconcilePendingBackgroundTasks(sessions3);
|
|
118548
119200
|
endPhase("pendingBgTasks");
|
|
118549
119201
|
if (ctx.overBudget()) return;
|
|
118550
|
-
await this.checkSnapshotPull(sessions3, ctx);
|
|
119202
|
+
await this.checkSnapshotPull(sessions3, ctx, allSessions);
|
|
118551
119203
|
endPhase("snapshotPull");
|
|
118552
119204
|
if (ctx.overBudget()) return;
|
|
118553
119205
|
const reconciledIds = await this.reconcileStuckRunningSessions(sessions3, taskMap);
|
|
@@ -119041,15 +119693,39 @@ var init_session_health_monitor = __esm({
|
|
|
119041
119693
|
bus.emit(EventNames.SESSION_WILL_REAP, payload, ["*"], { source: "health-monitor" });
|
|
119042
119694
|
}
|
|
119043
119695
|
/**
|
|
119044
|
-
* C2 snapshot pull channel (contract §5): every 30s tick,
|
|
119045
|
-
*
|
|
119046
|
-
*
|
|
119047
|
-
*
|
|
119048
|
-
*
|
|
119696
|
+
* C2 snapshot pull channel (contract §5): every 30s tick, PULL getState from
|
|
119697
|
+
* the host's POOLED CONNECTED DaemonConnection advertising 'snapshot-v1' and
|
|
119698
|
+
* feed the answer to applySnapshot. NEVER dials a new connection, always
|
|
119699
|
+
* sequential. TWO candidate classes with SEPARATE budgets and cadences:
|
|
119700
|
+
*
|
|
119701
|
+
* - LIVE — {running, idle}: 10 sids/tick, 25s per-sid spacing. The steady
|
|
119702
|
+
* state channel.
|
|
119703
|
+
* - RE-EXAMINATION — {error, stopped} whose cause is NOT positively terminal
|
|
119704
|
+
* (classifySessionError) and whose last_status_change is inside
|
|
119705
|
+
* RESCUABLE_STOPPED_WINDOW_MS: 5 sids/tick, 5 min per-sid spacing. Same
|
|
119706
|
+
* engine/provider/awaiting_spawn/archived exclusions as the live class.
|
|
119707
|
+
*
|
|
119708
|
+
* WHY RE-EXAMINATION EXISTS — do not "optimize" the error class back out. A
|
|
119709
|
+
* wrongly-'error' record was a two-sided deadlock: this lane is the ONE writer
|
|
119710
|
+
* the enforce-mode gate sanctions for a snapshot-covered session, and while it
|
|
119711
|
+
* looked at live records only it never re-derived status for an 'error' one.
|
|
119712
|
+
* recoverInfraFailedSessions DOES look, but the fix it writes
|
|
119713
|
+
* (('health-monitor','auto_recovered_dead')) is itself category-①, so the gate
|
|
119714
|
+
* drops the whole patch — the lane allowed to write refused to look, and the
|
|
119715
|
+
* lane that looked was not allowed to write. Records froze in 'error' with
|
|
119716
|
+
* "Connection lost — unable to reach remote host" for hours after their host
|
|
119717
|
+
* came back (2026-09-03, a remote dev host whose tunnel flapped: prose still on
|
|
119718
|
+
* the record 2h after the reconnect), burning a 30s log-churn loop the whole
|
|
119719
|
+
* time. Terminality is decided STRUCTURALLY, never by matching errorMessage
|
|
119720
|
+
* prose — see the header of session-error-kind.ts for why that ban exists.
|
|
119049
119721
|
*
|
|
119050
|
-
*
|
|
119722
|
+
* Re-examination candidates come from a WIDER pool than the live ones: the
|
|
119723
|
+
* tick's own working set drops every 'error' row (isTerminalSession), so the
|
|
119724
|
+
* caller passes the unfiltered health-scan set for this class only.
|
|
119051
119725
|
*
|
|
119052
|
-
*
|
|
119726
|
+
* THREE fairness/budget properties both classes must keep (C8/C9/C12/C29):
|
|
119727
|
+
*
|
|
119728
|
+
* - The TickContext budget is honored INSIDE the loop (same discipline as
|
|
119053
119729
|
* checkHungSessions). Each iteration is a real daemon RPC with a
|
|
119054
119730
|
* probe-timeout ceiling, so 10 slow hosts could burn 10 × the probe
|
|
119055
119731
|
* timeout sequentially BEFORE the authoritative reconcile phases that run
|
|
@@ -119058,70 +119734,101 @@ var init_session_health_monitor = __esm({
|
|
|
119058
119734
|
* next tick (see the rotation below).
|
|
119059
119735
|
*
|
|
119060
119736
|
* - Candidates are ordered by lastPullAt ASCENDING (never-pulled first),
|
|
119061
|
-
* which turns
|
|
119062
|
-
*
|
|
119063
|
-
*
|
|
119064
|
-
*
|
|
119737
|
+
* which turns each cap into a round-robin. In list order the SAME first ten
|
|
119738
|
+
* sids were pulled every tick and — because the 25s spacing is shorter than
|
|
119739
|
+
* the 30s cadence — sids 11+ were never pulled at all: the pull channel
|
|
119740
|
+
* silently did not exist for them. Each class rotates on its OWN timestamps.
|
|
119741
|
+
*
|
|
119742
|
+
* - The classes cannot spend each other's budget. Re-examination is
|
|
119743
|
+
* speculative ("is this record still true?") and a settled-death cohort
|
|
119744
|
+
* dwarfs the live set (the live DB measured 100 stopped candidates against
|
|
119745
|
+
* 4 error rows), so it must cost a handful of cheap RPCs per tick and
|
|
119746
|
+
* nothing more — the same reasoning as MAX_STOPPED_PROBES_PER_TICK below.
|
|
119065
119747
|
*/
|
|
119066
119748
|
snapshotPullAt = /* @__PURE__ */ new Map();
|
|
119067
|
-
|
|
119749
|
+
/** Separate from snapshotPullAt on purpose: the two classes rotate
|
|
119750
|
+
* independently, so a live pull can never reset the slow re-examination
|
|
119751
|
+
* cadence (or vice versa) when a record moves between them. */
|
|
119752
|
+
snapshotReexamPullAt = /* @__PURE__ */ new Map();
|
|
119753
|
+
async checkSnapshotPull(sessions3, ctx, reexamPool = sessions3) {
|
|
119068
119754
|
const PULL_MIN_GAP_MS = 25e3;
|
|
119069
119755
|
const MAX_PULLS_PER_TICK = 10;
|
|
119756
|
+
const REEXAM_MIN_GAP_MS = 5 * 6e4;
|
|
119757
|
+
const MAX_REEXAM_PULLS_PER_TICK = 5;
|
|
119070
119758
|
try {
|
|
119071
119759
|
const { getSnapshotStatusMode: getSnapshotStatusMode2 } = await Promise.resolve().then(() => (init_session_snapshot_gate(), session_snapshot_gate_exports));
|
|
119072
119760
|
if (getSnapshotStatusMode2() === "off") return;
|
|
119073
119761
|
const { getPooledSnapshotConnection: getPooledSnapshotConnection2 } = await Promise.resolve().then(() => (init_daemon_connection(), daemon_connection_exports));
|
|
119074
119762
|
const { applySnapshot: applySnapshot2 } = await Promise.resolve().then(() => (init_session_snapshot_apply(), session_snapshot_apply_exports));
|
|
119075
119763
|
const now = Date.now();
|
|
119076
|
-
const
|
|
119077
|
-
if (s4.
|
|
119764
|
+
const pullable = (s4) => {
|
|
119765
|
+
if (s4.archived) return false;
|
|
119078
119766
|
if (!engineCaps(s4.engine).snapshotPull) return false;
|
|
119079
119767
|
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
|
-
|
|
119768
|
+
return s4.status_reason !== "awaiting_spawn";
|
|
119769
|
+
};
|
|
119770
|
+
const byPullAge = (at) => (a8, b8) => (at.get(a8.claudeSessionId) ?? 0) - (at.get(b8.claudeSessionId) ?? 0);
|
|
119771
|
+
const live = sessions3.filter((s4) => {
|
|
119772
|
+
if (s4.process_status !== "running" && s4.process_status !== "idle") return false;
|
|
119773
|
+
if (!pullable(s4)) return false;
|
|
119774
|
+
return now - (this.snapshotPullAt.get(s4.claudeSessionId) ?? 0) >= PULL_MIN_GAP_MS;
|
|
119775
|
+
}).sort(byPullAge(this.snapshotPullAt));
|
|
119776
|
+
const reexam = reexamPool.filter((s4) => {
|
|
119777
|
+
if (s4.process_status !== "error" && s4.process_status !== "stopped") return false;
|
|
119778
|
+
if (!pullable(s4)) return false;
|
|
119779
|
+
if (classifySessionError(s4) === "terminal") return false;
|
|
119780
|
+
const changedAt = s4.last_status_change ? Date.parse(s4.last_status_change) : NaN;
|
|
119781
|
+
if (!Number.isFinite(changedAt)) return false;
|
|
119782
|
+
if (now - changedAt > RESCUABLE_STOPPED_WINDOW_MS) return false;
|
|
119783
|
+
return now - (this.snapshotReexamPullAt.get(s4.claudeSessionId) ?? 0) >= REEXAM_MIN_GAP_MS;
|
|
119784
|
+
}).sort(byPullAge(this.snapshotReexamPullAt));
|
|
119785
|
+
let abandoned = false;
|
|
119786
|
+
const runClass = async (candidates, cap2, at, candidateClass) => {
|
|
119787
|
+
let pulled = 0;
|
|
119788
|
+
for (const s4 of candidates) {
|
|
119789
|
+
if (ctx?.overBudget()) {
|
|
119790
|
+
abandoned = true;
|
|
119791
|
+
log.session.warn("health monitor: checkSnapshotPull abandoned mid-loop (over budget)", {
|
|
119792
|
+
pulled,
|
|
119793
|
+
candidateCount: candidates.length,
|
|
119794
|
+
candidateClass
|
|
119795
|
+
});
|
|
119796
|
+
break;
|
|
119797
|
+
}
|
|
119798
|
+
const conn = getPooledSnapshotConnection2(s4.host);
|
|
119799
|
+
if (!conn) continue;
|
|
119800
|
+
if (pulled >= cap2) {
|
|
119801
|
+
log.session.info("health monitor: snapshot pull capped this tick", {
|
|
119802
|
+
cap: cap2,
|
|
119803
|
+
sessionCount: sessions3.length,
|
|
119804
|
+
candidateCount: candidates.length,
|
|
119805
|
+
candidateClass
|
|
119806
|
+
});
|
|
119807
|
+
break;
|
|
119808
|
+
}
|
|
119809
|
+
pulled++;
|
|
119810
|
+
at.set(s4.claudeSessionId, now);
|
|
119811
|
+
try {
|
|
119812
|
+
const resp = await probeWithTimeout(
|
|
119813
|
+
conn.send("getState", { sid: s4.claudeSessionId }),
|
|
119814
|
+
null,
|
|
119815
|
+
"snapshot-pull-getState",
|
|
119816
|
+
s4.claudeSessionId
|
|
119817
|
+
);
|
|
119818
|
+
const snapshot2 = resp?.ok ? resp.snapshot : void 0;
|
|
119819
|
+
if (snapshot2) await applySnapshot2(s4.claudeSessionId, snapshot2, "pull-30s");
|
|
119820
|
+
} catch (err2) {
|
|
119821
|
+
log.session.debug("health monitor: snapshot pull failed", {
|
|
119822
|
+
sessionId: s4.claudeSessionId,
|
|
119823
|
+
host: s4.host ?? "__local__",
|
|
119824
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
119825
|
+
});
|
|
119826
|
+
}
|
|
119117
119827
|
}
|
|
119118
|
-
}
|
|
119119
|
-
|
|
119120
|
-
|
|
119121
|
-
|
|
119122
|
-
sessionCount: sessions3.length,
|
|
119123
|
-
candidateCount: candidates.length
|
|
119124
|
-
});
|
|
119828
|
+
};
|
|
119829
|
+
await runClass(live, MAX_PULLS_PER_TICK, this.snapshotPullAt, "live");
|
|
119830
|
+
if (!abandoned) {
|
|
119831
|
+
await runClass(reexam, MAX_REEXAM_PULLS_PER_TICK, this.snapshotReexamPullAt, "reexam");
|
|
119125
119832
|
}
|
|
119126
119833
|
if (this.snapshotPullAt.size > 200) {
|
|
119127
119834
|
const liveIds = new Set(sessions3.map((s4) => s4.claudeSessionId));
|
|
@@ -119129,6 +119836,12 @@ var init_session_health_monitor = __esm({
|
|
|
119129
119836
|
if (!liveIds.has(sid)) this.snapshotPullAt.delete(sid);
|
|
119130
119837
|
}
|
|
119131
119838
|
}
|
|
119839
|
+
if (this.snapshotReexamPullAt.size > 200) {
|
|
119840
|
+
const poolIds = new Set(reexamPool.map((s4) => s4.claudeSessionId));
|
|
119841
|
+
for (const sid of this.snapshotReexamPullAt.keys()) {
|
|
119842
|
+
if (!poolIds.has(sid)) this.snapshotReexamPullAt.delete(sid);
|
|
119843
|
+
}
|
|
119844
|
+
}
|
|
119132
119845
|
} catch (err2) {
|
|
119133
119846
|
log.session.warn("health monitor: checkSnapshotPull failed", {
|
|
119134
119847
|
error: err2 instanceof Error ? err2.message : String(err2)
|
|
@@ -119291,6 +120004,31 @@ var init_session_health_monitor = __esm({
|
|
|
119291
120004
|
});
|
|
119292
120005
|
}
|
|
119293
120006
|
}
|
|
120007
|
+
/**
|
|
120008
|
+
* Can a scheduled auto-recovery actually FIRE for this record?
|
|
120009
|
+
*
|
|
120010
|
+
* fire() (session-auto-recover) hard-requires the linked task to still be
|
|
120011
|
+
* IN_PROGRESS — AGENT_COMPLETE means the work was already handed back and
|
|
120012
|
+
* resuming would talk over the human. Arming a recovery that cannot pass that
|
|
120013
|
+
* check is not free: the 30s tick re-armed it forever and each arming logged
|
|
120014
|
+
* an abort 20s later (measured running for 2+ hours on one session), while
|
|
120015
|
+
* `if (armed) continue` also skipped the phase sync that IS appropriate there.
|
|
120016
|
+
*
|
|
120017
|
+
* A lookup that cannot answer (no task id, deleted task, DB error) returns
|
|
120018
|
+
* true: only a KNOWN non-IN_PROGRESS phase blocks arming, so a transient read
|
|
120019
|
+
* failure can never disable recovery. schedule()'s own guards still apply.
|
|
120020
|
+
*/
|
|
120021
|
+
async autoRecoverCanFire(s4) {
|
|
120022
|
+
if (!s4.taskId) return true;
|
|
120023
|
+
try {
|
|
120024
|
+
const { getTask: getTask2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
|
|
120025
|
+
const task = await getTask2(s4.taskId);
|
|
120026
|
+
if (!task) return true;
|
|
120027
|
+
return task.phase === "IN_PROGRESS";
|
|
120028
|
+
} catch {
|
|
120029
|
+
return true;
|
|
120030
|
+
}
|
|
120031
|
+
}
|
|
119294
120032
|
/**
|
|
119295
120033
|
* Auto-recover remote sessions stuck in 'error' with "Connection lost" message.
|
|
119296
120034
|
*
|
|
@@ -119377,28 +120115,31 @@ var init_session_health_monitor = __esm({
|
|
|
119377
120115
|
} else if (wasStopped) {
|
|
119378
120116
|
continue;
|
|
119379
120117
|
} else {
|
|
119380
|
-
const
|
|
119381
|
-
|
|
119382
|
-
|
|
119383
|
-
|
|
119384
|
-
|
|
119385
|
-
|
|
119386
|
-
|
|
119387
|
-
|
|
119388
|
-
|
|
119389
|
-
|
|
119390
|
-
|
|
119391
|
-
|
|
119392
|
-
|
|
119393
|
-
|
|
119394
|
-
|
|
119395
|
-
|
|
119396
|
-
|
|
119397
|
-
|
|
119398
|
-
|
|
119399
|
-
|
|
120118
|
+
const { isSnapshotCovered: isSnapshotCovered2 } = await Promise.resolve().then(() => (init_session_snapshot_gate(), session_snapshot_gate_exports));
|
|
120119
|
+
if (!isSnapshotCovered2(s4.claudeSessionId)) {
|
|
120120
|
+
const updated = await updateSessionRecord2(s4.claudeSessionId, {
|
|
120121
|
+
process_status: "stopped",
|
|
120122
|
+
errorMessage: void 0,
|
|
120123
|
+
errorKind: "infra",
|
|
120124
|
+
activity: void 0,
|
|
120125
|
+
last_status_change: now,
|
|
120126
|
+
status_reason: "auto_recovered_dead",
|
|
120127
|
+
status_changed_by: "health-monitor"
|
|
120128
|
+
});
|
|
120129
|
+
emitSessionStatusChanged(
|
|
120130
|
+
updated,
|
|
120131
|
+
{},
|
|
120132
|
+
["*"],
|
|
120133
|
+
{ source: "health-monitor", urgency: "urgent" }
|
|
120134
|
+
);
|
|
120135
|
+
log.session.info("health monitor: auto-recovered connection-lost session (process dead)", {
|
|
120136
|
+
sessionId: s4.claudeSessionId,
|
|
120137
|
+
host: s4.host,
|
|
120138
|
+
alive: false
|
|
120139
|
+
});
|
|
120140
|
+
}
|
|
119400
120141
|
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");
|
|
120142
|
+
const armed = await this.autoRecoverCanFire(s4) && scheduleSessionAutoRecover2(s4, s4.status_reason ?? "daemon_reported_exit");
|
|
119402
120143
|
if (armed) continue;
|
|
119403
120144
|
if (s4.taskId) {
|
|
119404
120145
|
try {
|
|
@@ -132723,6 +133464,20 @@ var init_session_lifecycle_v1 = __esm({
|
|
|
132723
133464
|
next(err2);
|
|
132724
133465
|
}
|
|
132725
133466
|
});
|
|
133467
|
+
sessionLifecycleV1Router.post("/sessions/:id/recheck", async (req, res, next) => {
|
|
133468
|
+
try {
|
|
133469
|
+
const sessionId = validSid2(req, res);
|
|
133470
|
+
if (!sessionId) return;
|
|
133471
|
+
if (CLOUD_MODE) {
|
|
133472
|
+
await relayControlAction(res, "recheck", sessionId, void 0, 200);
|
|
133473
|
+
return;
|
|
133474
|
+
}
|
|
133475
|
+
const { recheckSession: recheckSession2 } = await Promise.resolve().then(() => (init_session_lifecycle(), session_lifecycle_exports));
|
|
133476
|
+
await runLocal(res, next, 200, () => recheckSession2(sessionId));
|
|
133477
|
+
} catch (err2) {
|
|
133478
|
+
next(err2);
|
|
133479
|
+
}
|
|
133480
|
+
});
|
|
132726
133481
|
sessionLifecycleV1Router.post("/sessions/:id/permission", async (req, res, next) => {
|
|
132727
133482
|
try {
|
|
132728
133483
|
const sessionId = validSid2(req, res);
|
|
@@ -132935,7 +133690,7 @@ __export(session_requests_exports, {
|
|
|
132935
133690
|
settleNotified: () => settleNotified,
|
|
132936
133691
|
settleReplied: () => settleReplied
|
|
132937
133692
|
});
|
|
132938
|
-
import { createHash as createHash18, randomUUID as
|
|
133693
|
+
import { createHash as createHash18, randomUUID as randomUUID9 } from "crypto";
|
|
132939
133694
|
import path137 from "path";
|
|
132940
133695
|
function oneLine(text, max = 160) {
|
|
132941
133696
|
const flat = text.replace(/\s+/g, " ").trim();
|
|
@@ -132959,7 +133714,7 @@ function clampReplyTimeoutSecs(secs) {
|
|
|
132959
133714
|
}
|
|
132960
133715
|
async function createSessionRequest(input) {
|
|
132961
133716
|
const request3 = {
|
|
132962
|
-
id: `rq-${
|
|
133717
|
+
id: `rq-${randomUUID9().replace(/-/g, "").slice(0, 12)}`,
|
|
132963
133718
|
fromSessionId: input.fromSessionId,
|
|
132964
133719
|
...input.toSessionId ? { toSessionId: input.toSessionId } : {},
|
|
132965
133720
|
...input.toTaskId ? { toTaskId: input.toTaskId } : {},
|
|
@@ -133392,7 +134147,7 @@ __export(task_start_exports, {
|
|
|
133392
134147
|
SessionExistsError: () => SessionExistsError,
|
|
133393
134148
|
startSessionForTask: () => startSessionForTask
|
|
133394
134149
|
});
|
|
133395
|
-
import { randomUUID as
|
|
134150
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
133396
134151
|
async function startSessionForTask(params) {
|
|
133397
134152
|
const { taskIdPrefix, source } = params;
|
|
133398
134153
|
let task;
|
|
@@ -133443,7 +134198,7 @@ async function startSessionForTask(params) {
|
|
|
133443
134198
|
}
|
|
133444
134199
|
let message = params.message?.trim() || `Working on task: ${task.title}`;
|
|
133445
134200
|
const engine = normalizeEngine(params.engine);
|
|
133446
|
-
const preassignedSessionId = engineCaps(engine).idProvisioning === "provider-issued" ? void 0 :
|
|
134201
|
+
const preassignedSessionId = engineCaps(engine).idProvisioning === "provider-issued" ? void 0 : randomUUID10();
|
|
133447
134202
|
let requestId;
|
|
133448
134203
|
if (params.expectReply) {
|
|
133449
134204
|
const { resolveCaller: resolveCaller2 } = await Promise.resolve().then(() => (init_session_send_core(), session_send_core_exports));
|
|
@@ -148941,7 +149696,7 @@ async function generateLedgerDesc(task, timeoutMs = 15e3) {
|
|
|
148941
149696
|
try {
|
|
148942
149697
|
const { getConfig: getConfig2 } = await Promise.resolve().then(() => (init_config_manager(), config_manager_exports));
|
|
148943
149698
|
const config = await getConfig2();
|
|
148944
|
-
const providerName = config
|
|
149699
|
+
const providerName = resolveMainProviderName(config);
|
|
148945
149700
|
const model = cheapModelFor(providerName);
|
|
148946
149701
|
const detail = (task.description ?? "").trim().slice(0, 800);
|
|
148947
149702
|
const project = (task.project ?? "").trim();
|
|
@@ -149000,6 +149755,7 @@ var init_task_ledger_desc = __esm({
|
|
|
149000
149755
|
"use strict";
|
|
149001
149756
|
init_model();
|
|
149002
149757
|
init_model_catalog();
|
|
149758
|
+
init_default_provider();
|
|
149003
149759
|
init_logging();
|
|
149004
149760
|
init_task_manager();
|
|
149005
149761
|
MAX_DESC_LEN = 120;
|
|
@@ -149023,6 +149779,10 @@ function diffStreamedVsPersisted(streamedTextMsgIds, persistedMsgIds) {
|
|
|
149023
149779
|
}
|
|
149024
149780
|
function armStreamConvergenceCheck(sessionId, streamedTextMsgIds) {
|
|
149025
149781
|
if (streamedTextMsgIds.length === 0) return;
|
|
149782
|
+
if (consumeWarmupTurn(sessionId)) {
|
|
149783
|
+
log.obs.debug("stream-convergence: skipped hidden warm-up turn", { sessionId });
|
|
149784
|
+
return;
|
|
149785
|
+
}
|
|
149026
149786
|
const timer3 = setTimeout(() => {
|
|
149027
149787
|
void runCheck(sessionId, streamedTextMsgIds).catch((err2) => {
|
|
149028
149788
|
log.obs.warn("stream-convergence check failed", {
|
|
@@ -149088,6 +149848,7 @@ var init_stream_convergence = __esm({
|
|
|
149088
149848
|
"src/core/observability/stream-convergence.ts"() {
|
|
149089
149849
|
"use strict";
|
|
149090
149850
|
init_logging();
|
|
149851
|
+
init_side_thread_warmup();
|
|
149091
149852
|
CHECK_DELAY_MS = 15e3;
|
|
149092
149853
|
INCIDENT_DEDUPE_MS = 30 * 6e4;
|
|
149093
149854
|
lastIncidentAt = /* @__PURE__ */ new Map();
|
|
@@ -149338,12 +150099,17 @@ function getSystemHealth() {
|
|
|
149338
150099
|
}
|
|
149339
150100
|
async function refreshSystemHealth() {
|
|
149340
150101
|
systemHealth.claudeCliAvailable = checkClaudeCliAvailable();
|
|
149341
|
-
|
|
150102
|
+
applyCredentialHealth(await resolveCredentialHealth());
|
|
150103
|
+
broadcastEvent("system:health", systemHealth);
|
|
150104
|
+
return systemHealth;
|
|
150105
|
+
}
|
|
150106
|
+
function applyCredentialHealth(cred) {
|
|
149342
150107
|
systemHealth.hasReadyProvider = cred.hasReadyProvider;
|
|
149343
150108
|
systemHealth.credentialSource = cred.source;
|
|
149344
150109
|
systemHealth.credentialDetail = cred.detail;
|
|
149345
|
-
|
|
149346
|
-
|
|
150110
|
+
systemHealth.mainProvider = cred.mainProvider;
|
|
150111
|
+
systemHealth.mainProviderImplicit = cred.mainProviderImplicit;
|
|
150112
|
+
systemHealth.claudeCliAuth = cred.claudeCliAuth;
|
|
149347
150113
|
}
|
|
149348
150114
|
function checkClaudeCliAvailable() {
|
|
149349
150115
|
return isClaudeCliInstalled();
|
|
@@ -149353,31 +150119,50 @@ async function resolveCredentialHealth() {
|
|
|
149353
150119
|
const { getConfig: getConfig2 } = await Promise.resolve().then(() => (init_config_manager(), config_manager_exports));
|
|
149354
150120
|
const { resolveCredentials: resolveCredentials2 } = await Promise.resolve().then(() => (init_credential_resolver(), credential_resolver_exports));
|
|
149355
150121
|
const { buildProviderMap: buildProviderMap2 } = await Promise.resolve().then(() => (init_registry2(), registry_exports));
|
|
150122
|
+
const { resolveMainProviderName: resolveMainProviderName2, mainProviderIsImplicit: mainProviderIsImplicit2 } = await Promise.resolve().then(() => (init_default_provider(), default_provider_exports));
|
|
150123
|
+
const { detectClaudeCli: detectClaudeCli2 } = await Promise.resolve().then(() => (init_claude_cli_detect(), claude_cli_detect_exports));
|
|
149356
150124
|
const config = await getConfig2();
|
|
150125
|
+
const cli = detectClaudeCli2();
|
|
150126
|
+
const mainProvider = resolveMainProviderName2(config, cli.installed);
|
|
150127
|
+
const base = {
|
|
150128
|
+
mainProvider,
|
|
150129
|
+
mainProviderImplicit: mainProviderIsImplicit2(config),
|
|
150130
|
+
claudeCliAuth: cli.installed ? cli.auth.label : void 0
|
|
150131
|
+
};
|
|
150132
|
+
if (mainProvider === "claude_cli") {
|
|
150133
|
+
return cli.installed ? { ...base, hasReadyProvider: true, source: "config", detail: `claude-cli (${cli.auth.label})` } : { ...base, hasReadyProvider: false, source: "none" };
|
|
150134
|
+
}
|
|
149357
150135
|
const cred = resolveCredentials2(config);
|
|
149358
150136
|
if (cred.source !== "none") {
|
|
149359
|
-
return { hasReadyProvider: true, source: cred.source, detail: cred.detail };
|
|
150137
|
+
return { ...base, hasReadyProvider: true, source: cred.source, detail: cred.detail };
|
|
149360
150138
|
}
|
|
149361
150139
|
const providers = buildProviderMap2(config.providers, config);
|
|
149362
150140
|
for (const [, prov] of Object.entries(providers)) {
|
|
149363
150141
|
if (prov.api === "bedrock" || prov.api === "ollama") continue;
|
|
149364
150142
|
if (prov.api === "claude-cli") {
|
|
149365
|
-
|
|
149366
|
-
if (detectClaudeCli2().subscriptionReady) {
|
|
149367
|
-
return { hasReadyProvider: true, source: "config", detail: "claude-cli (subscription)" };
|
|
149368
|
-
}
|
|
150143
|
+
if (cli.installed) return { ...base, hasReadyProvider: true, source: "config", detail: `claude-cli (${cli.auth.label})` };
|
|
149369
150144
|
continue;
|
|
149370
150145
|
}
|
|
149371
150146
|
const implemented = prov.api === "anthropic-messages" || prov.api === "openai-chat" || prov.api === "google-generative-ai";
|
|
149372
150147
|
if (implemented && (prov.api_key || prov.bearer_token)) {
|
|
149373
|
-
return { hasReadyProvider: true, source: "config", detail: prov.api };
|
|
150148
|
+
return { ...base, hasReadyProvider: true, source: "config", detail: prov.api };
|
|
149374
150149
|
}
|
|
149375
150150
|
}
|
|
149376
|
-
return { hasReadyProvider: false, source: "none" };
|
|
150151
|
+
return { ...base, hasReadyProvider: false, source: "none" };
|
|
149377
150152
|
} catch {
|
|
149378
150153
|
return { hasReadyProvider: false, source: "none" };
|
|
149379
150154
|
}
|
|
149380
150155
|
}
|
|
150156
|
+
async function isWalnutAnsweringOn(port) {
|
|
150157
|
+
try {
|
|
150158
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/system/health`, { signal: AbortSignal.timeout(1500) });
|
|
150159
|
+
if (!res.ok) return false;
|
|
150160
|
+
const body3 = await res.json();
|
|
150161
|
+
return !!body3 && typeof body3 === "object" && "hasReadyProvider" in body3;
|
|
150162
|
+
} catch {
|
|
150163
|
+
return false;
|
|
150164
|
+
}
|
|
150165
|
+
}
|
|
149381
150166
|
async function startServer(options = {}) {
|
|
149382
150167
|
if (httpServer) throw new Error("Server already running. Call stopServer() first.");
|
|
149383
150168
|
bootCompleted = false;
|
|
@@ -149402,11 +150187,11 @@ async function startServer(options = {}) {
|
|
|
149402
150187
|
}
|
|
149403
150188
|
{
|
|
149404
150189
|
const cred = await resolveCredentialHealth();
|
|
149405
|
-
|
|
149406
|
-
systemHealth.credentialSource = cred.source;
|
|
149407
|
-
systemHealth.credentialDetail = cred.detail;
|
|
150190
|
+
applyCredentialHealth(cred);
|
|
149408
150191
|
if (!systemHealth.hasReadyProvider) {
|
|
149409
150192
|
log.web.warn("No AI provider configured \u2014 configure one in Settings");
|
|
150193
|
+
} else if (cred.mainProvider === "claude_cli") {
|
|
150194
|
+
log.web.info("AI provider: Claude Code", { auth: cred.claudeCliAuth, implicit: cred.mainProviderImplicit });
|
|
149410
150195
|
} else if (cred.source && cred.source !== "config") {
|
|
149411
150196
|
log.web.info("Auto-detected Bedrock credential", { source: cred.source, detail: cred.detail });
|
|
149412
150197
|
}
|
|
@@ -149904,6 +150689,7 @@ async function startServer(options = {}) {
|
|
|
149904
150689
|
});
|
|
149905
150690
|
if (!dev) {
|
|
149906
150691
|
const staticDir = (() => {
|
|
150692
|
+
if (process.env.WALNUT_WEB_STATIC_DIR) return process.env.WALNUT_WEB_STATIC_DIR;
|
|
149907
150693
|
let dir2 = path157.dirname(fileURLToPath13(import.meta.url));
|
|
149908
150694
|
for (let i8 = 0; i8 < 5; i8++) {
|
|
149909
150695
|
const candidate = path157.join(dir2, "web", "static", "index.html");
|
|
@@ -149920,11 +150706,65 @@ async function startServer(options = {}) {
|
|
|
149920
150706
|
}
|
|
149921
150707
|
return path157.join(process.cwd(), "dist", "web", "static");
|
|
149922
150708
|
})();
|
|
150709
|
+
const mirrorDir = process.env.WALNUT_WEB_STATIC_MIRROR || path157.join(WALNUT_HOME, "cache", "web-static");
|
|
150710
|
+
let mirrorReady = false;
|
|
150711
|
+
try {
|
|
150712
|
+
const want = fs98.readFileSync(path157.join(staticDir, "index.html"), "utf-8");
|
|
150713
|
+
let have = null;
|
|
150714
|
+
try {
|
|
150715
|
+
have = fs98.readFileSync(path157.join(mirrorDir, "index.html"), "utf-8");
|
|
150716
|
+
} catch {
|
|
150717
|
+
}
|
|
150718
|
+
if (have !== want) {
|
|
150719
|
+
fs98.rmSync(mirrorDir, { recursive: true, force: true });
|
|
150720
|
+
fs98.mkdirSync(path157.dirname(mirrorDir), { recursive: true });
|
|
150721
|
+
fs98.cpSync(staticDir, mirrorDir, { recursive: true });
|
|
150722
|
+
}
|
|
150723
|
+
} catch (err2) {
|
|
150724
|
+
log.web.warn("could not refresh the web-asset mirror", {
|
|
150725
|
+
mirrorDir,
|
|
150726
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
150727
|
+
});
|
|
150728
|
+
}
|
|
150729
|
+
try {
|
|
150730
|
+
mirrorReady = fs98.statSync(path157.join(mirrorDir, "index.html")).isFile();
|
|
150731
|
+
} catch {
|
|
150732
|
+
mirrorReady = false;
|
|
150733
|
+
}
|
|
149923
150734
|
app.use(express3.static(staticDir));
|
|
150735
|
+
if (mirrorReady) app.use(express3.static(mirrorDir));
|
|
150736
|
+
const checkStaticRoot = () => {
|
|
150737
|
+
try {
|
|
150738
|
+
return fs98.statSync(path157.join(staticDir, "index.html")).isFile();
|
|
150739
|
+
} catch {
|
|
150740
|
+
return false;
|
|
150741
|
+
}
|
|
150742
|
+
};
|
|
150743
|
+
let staticRootOk = checkStaticRoot();
|
|
150744
|
+
if (!staticRootOk) {
|
|
150745
|
+
log.web.error("web assets are NOT servable at startup", { staticDir, mirrorReady });
|
|
150746
|
+
}
|
|
150747
|
+
const staticRootTimer = setInterval(() => {
|
|
150748
|
+
const ok2 = checkStaticRoot();
|
|
150749
|
+
if (ok2 === staticRootOk) return;
|
|
150750
|
+
staticRootOk = ok2;
|
|
150751
|
+
if (ok2) log.web.info("web assets are servable again", { staticDir });
|
|
150752
|
+
else log.web.error("web assets VANISHED from under the running server", { staticDir, mirrorReady });
|
|
150753
|
+
}, 6e4);
|
|
150754
|
+
staticRootTimer.unref();
|
|
150755
|
+
setStaticRootReporter(() => ({ staticDir, ok: staticRootOk, mirrorDir, mirrorReady }));
|
|
149924
150756
|
app.use((req, res, next) => {
|
|
149925
150757
|
if (req.method !== "GET" || req.path.startsWith("/api/")) return next();
|
|
149926
150758
|
if (isStaticAssetPath(req.path)) return next();
|
|
149927
|
-
res.sendFile("index.html", { root: staticDir })
|
|
150759
|
+
res.sendFile("index.html", { root: staticDir }, (err2) => {
|
|
150760
|
+
if (!err2) return;
|
|
150761
|
+
if (mirrorReady) {
|
|
150762
|
+
res.sendFile("index.html", { root: mirrorDir }, () => {
|
|
150763
|
+
});
|
|
150764
|
+
return;
|
|
150765
|
+
}
|
|
150766
|
+
next(err2);
|
|
150767
|
+
});
|
|
149928
150768
|
});
|
|
149929
150769
|
}
|
|
149930
150770
|
app.use("/api", notFoundHandler);
|
|
@@ -149944,9 +150784,21 @@ async function startServer(options = {}) {
|
|
|
149944
150784
|
}
|
|
149945
150785
|
httpServer = createServer(app);
|
|
149946
150786
|
attachWss(httpServer);
|
|
149947
|
-
|
|
149948
|
-
|
|
149949
|
-
|
|
150787
|
+
try {
|
|
150788
|
+
await new Promise((resolve, reject) => {
|
|
150789
|
+
httpServer.once("error", reject);
|
|
150790
|
+
httpServer.listen(port, () => resolve());
|
|
150791
|
+
});
|
|
150792
|
+
} catch (err2) {
|
|
150793
|
+
if (err2?.code !== "EADDRINUSE") throw err2;
|
|
150794
|
+
const already = await isWalnutAnsweringOn(port);
|
|
150795
|
+
throw new Error(
|
|
150796
|
+
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:
|
|
150797
|
+
open-walnut web --port ${port + 1}`
|
|
150798
|
+
);
|
|
150799
|
+
}
|
|
150800
|
+
httpServer.on("error", (err2) => {
|
|
150801
|
+
log.web.error("http server error after listen", { error: err2 instanceof Error ? err2.message : String(err2) });
|
|
149950
150802
|
});
|
|
149951
150803
|
const label = dev ? "dev" : "production";
|
|
149952
150804
|
log.web.info(`server listening on http://localhost:${port}`, { mode: label, port });
|
|
@@ -149956,6 +150808,7 @@ async function startServer(options = {}) {
|
|
|
149956
150808
|
const { updateInstanceLockPort: updateInstanceLockPort2 } = await Promise.resolve().then(() => (init_instance_lock(), instance_lock_exports));
|
|
149957
150809
|
updateInstanceLockPort2(bound.port);
|
|
149958
150810
|
setPluginApiBase(`http://127.0.0.1:${bound.port}`);
|
|
150811
|
+
process.env.WALNUT_SERVER_URL = `http://localhost:${bound.port}`;
|
|
149959
150812
|
}
|
|
149960
150813
|
}
|
|
149961
150814
|
if (CLOUD_MODE) {
|
|
@@ -150618,10 +151471,9 @@ async function startServer(options = {}) {
|
|
|
150618
151471
|
const prev = systemHealth.hasReadyProvider;
|
|
150619
151472
|
const prevSource = systemHealth.credentialSource;
|
|
150620
151473
|
const cred = await resolveCredentialHealth();
|
|
150621
|
-
|
|
150622
|
-
|
|
150623
|
-
|
|
150624
|
-
if (cred.hasReadyProvider !== prev || cred.source !== prevSource) {
|
|
151474
|
+
const prevProvider = systemHealth.mainProvider;
|
|
151475
|
+
applyCredentialHealth(cred);
|
|
151476
|
+
if (cred.hasReadyProvider !== prev || cred.source !== prevSource || cred.mainProvider !== prevProvider) {
|
|
150625
151477
|
broadcastEvent("system:health", systemHealth);
|
|
150626
151478
|
}
|
|
150627
151479
|
} catch {
|
|
@@ -158048,11 +158900,12 @@ __export(session_controls_exports, {
|
|
|
158048
158900
|
applySessionModelChange: () => applySessionModelChange,
|
|
158049
158901
|
computeModelOptions: () => computeModelOptions,
|
|
158050
158902
|
controlErrorKind: () => controlErrorKind,
|
|
158903
|
+
createForkSiblingTask: () => createForkSiblingTask,
|
|
158051
158904
|
forkSessionToTask: () => forkSessionToTask,
|
|
158052
158905
|
handleSessionControlRelay: () => handleSessionControlRelay,
|
|
158053
158906
|
modelReadBackMatches: () => modelReadBackMatches
|
|
158054
158907
|
});
|
|
158055
|
-
import { randomUUID as
|
|
158908
|
+
import { randomUUID as randomUUID11 } from "crypto";
|
|
158056
158909
|
function controlErrorKind(status2) {
|
|
158057
158910
|
if (status2 === 404) return "not_found";
|
|
158058
158911
|
if (status2 === 405) return "method_not_allowed";
|
|
@@ -158225,6 +159078,124 @@ async function computeModelOptions(sessionId) {
|
|
|
158225
159078
|
currentEffort: liveEffort ?? record.effectiveEffort ?? record.effort ?? null
|
|
158226
159079
|
};
|
|
158227
159080
|
}
|
|
159081
|
+
async function createForkSiblingTask(sourceTaskId, opts) {
|
|
159082
|
+
const {
|
|
159083
|
+
getTask: getTask2,
|
|
159084
|
+
addTask: addTask2,
|
|
159085
|
+
updateTask: updateTask2,
|
|
159086
|
+
groupTasks: groupTasks2,
|
|
159087
|
+
addToGroup: addToGroup2,
|
|
159088
|
+
renameGroup: renameGroup2
|
|
159089
|
+
} = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
|
|
159090
|
+
let sourceTask;
|
|
159091
|
+
try {
|
|
159092
|
+
sourceTask = await getTask2(sourceTaskId);
|
|
159093
|
+
} catch {
|
|
159094
|
+
throw new SessionControlError(`Source task "${sourceTaskId}" not found`, 404);
|
|
159095
|
+
}
|
|
159096
|
+
const sourceBaseTitle = sourceTask.title.replace(/^Fork of\s+/i, "").replace(/\s+-\s+fork of\s+.*$/i, "").trim() || sourceTask.title;
|
|
159097
|
+
const explicitTitle = opts.childTitle || (opts.titlePrefix ? `${opts.titlePrefix} - fork of ${sourceBaseTitle}` : void 0);
|
|
159098
|
+
const autoTitle = !explicitTitle;
|
|
159099
|
+
const newTitle = explicitTitle || `Fork of ${sourceBaseTitle}`;
|
|
159100
|
+
let inheritedTier;
|
|
159101
|
+
if (sourceTask.pinned && sourceTask.focus_tier) {
|
|
159102
|
+
const { getCustomTiers: getCustomTiers2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
|
|
159103
|
+
const known = ["focus", "backlog", "wait"].includes(sourceTask.focus_tier) || (await getCustomTiers2()).some((t3) => t3.id === sourceTask.focus_tier);
|
|
159104
|
+
if (known) inheritedTier = sourceTask.focus_tier;
|
|
159105
|
+
else {
|
|
159106
|
+
log.session.warn("fork: source tier is not registered, filing the fork in Satellite", {
|
|
159107
|
+
sourceTaskId: sourceTask.id,
|
|
159108
|
+
tier: sourceTask.focus_tier
|
|
159109
|
+
});
|
|
159110
|
+
}
|
|
159111
|
+
}
|
|
159112
|
+
const { task: newFork } = await addTask2({
|
|
159113
|
+
title: newTitle,
|
|
159114
|
+
project: sourceTask.project || "",
|
|
159115
|
+
source: sourceTask.source,
|
|
159116
|
+
...opts.description ? { description: opts.description } : {},
|
|
159117
|
+
// A fork is a person splitting off live work, so it joins the board like
|
|
159118
|
+
// any other hand-made task (Satellite = pinned, no stored tier).
|
|
159119
|
+
pinned: true,
|
|
159120
|
+
...inheritedTier ? { focus_tier: inheritedTier } : {},
|
|
159121
|
+
// Forking an Ask Walnut conversation yields another Ask Walnut
|
|
159122
|
+
// conversation: without the flag the fork loses the amber marker AND the
|
|
159123
|
+
// persona drift repair (both key on task.walnut_agent, never on project).
|
|
159124
|
+
...sourceTask.walnut_agent ? { walnut_agent: true } : {}
|
|
159125
|
+
});
|
|
159126
|
+
let forkGroupId;
|
|
159127
|
+
try {
|
|
159128
|
+
if (sourceTask.group_id) {
|
|
159129
|
+
const r8 = await addToGroup2(sourceTask.group_id, [newFork.id]);
|
|
159130
|
+
forkGroupId = r8.group_id;
|
|
159131
|
+
} else {
|
|
159132
|
+
const r8 = await groupTasks2([sourceTask.id, newFork.id], sourceTask.title);
|
|
159133
|
+
forkGroupId = r8.group_id;
|
|
159134
|
+
}
|
|
159135
|
+
} catch (err2) {
|
|
159136
|
+
log.session.warn("fork: failed to group source + fork", {
|
|
159137
|
+
sourceTaskId: sourceTask.id,
|
|
159138
|
+
forkTaskId: newFork.id,
|
|
159139
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
159140
|
+
});
|
|
159141
|
+
}
|
|
159142
|
+
let task;
|
|
159143
|
+
try {
|
|
159144
|
+
task = await getTask2(newFork.id);
|
|
159145
|
+
} catch {
|
|
159146
|
+
task = newFork;
|
|
159147
|
+
}
|
|
159148
|
+
bus.emit(EventNames.TASK_CREATED, { task }, ["web-ui", "main-agent"], { source: opts.source });
|
|
159149
|
+
if (autoTitle && opts.prompt?.trim()) {
|
|
159150
|
+
const forkId = newFork.id;
|
|
159151
|
+
const sourceTitle = sourceBaseTitle;
|
|
159152
|
+
const placeholderTitle = newTitle;
|
|
159153
|
+
const prompt = opts.prompt;
|
|
159154
|
+
const titleSource = `${opts.source}-title`;
|
|
159155
|
+
void (async () => {
|
|
159156
|
+
try {
|
|
159157
|
+
const { summarizeForkPrompt: summarizeForkPrompt2 } = await Promise.resolve().then(() => (init_fork_title(), fork_title_exports));
|
|
159158
|
+
const label = await summarizeForkPrompt2(prompt);
|
|
159159
|
+
if (!label) return;
|
|
159160
|
+
const current5 = await getTask2(forkId);
|
|
159161
|
+
if (current5.title !== placeholderTitle) {
|
|
159162
|
+
log.session.info("fork title refine skipped \u2014 title changed since fork", { taskId: forkId });
|
|
159163
|
+
return;
|
|
159164
|
+
}
|
|
159165
|
+
const refinedTitle = `${label} - fork of ${sourceTitle}`;
|
|
159166
|
+
const { task: updated } = await updateTask2(forkId, { title: refinedTitle }, { source: titleSource });
|
|
159167
|
+
bus.emit(EventNames.TASK_UPDATED, { task: updated }, ["web-ui", "main-agent"], { source: titleSource });
|
|
159168
|
+
log.session.info("fork title refined", { taskId: forkId, title: refinedTitle });
|
|
159169
|
+
} catch (err2) {
|
|
159170
|
+
log.session.warn("fork title refine failed", {
|
|
159171
|
+
taskId: forkId,
|
|
159172
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
159173
|
+
});
|
|
159174
|
+
}
|
|
159175
|
+
})();
|
|
159176
|
+
}
|
|
159177
|
+
if (forkGroupId && !sourceTask.group_id) {
|
|
159178
|
+
const gid = forkGroupId;
|
|
159179
|
+
const seedTitles = [sourceTask.title, newTitle];
|
|
159180
|
+
const groupSource = opts.source;
|
|
159181
|
+
void (async () => {
|
|
159182
|
+
try {
|
|
159183
|
+
const { summarizeGroupLabel: summarizeGroupLabel2 } = await Promise.resolve().then(() => (init_fork_title(), fork_title_exports));
|
|
159184
|
+
const label = await summarizeGroupLabel2(seedTitles);
|
|
159185
|
+
if (!label) return;
|
|
159186
|
+
await renameGroup2(gid, label);
|
|
159187
|
+
bus.emit(EventNames.TASK_GROUPS_CHANGED, { group_id: gid, label }, ["web-ui", "main-agent"], { source: groupSource });
|
|
159188
|
+
log.session.info("fork group label refined", { groupId: gid, label });
|
|
159189
|
+
} catch (err2) {
|
|
159190
|
+
log.session.warn("fork group label refine failed", {
|
|
159191
|
+
groupId: gid,
|
|
159192
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
159193
|
+
});
|
|
159194
|
+
}
|
|
159195
|
+
})();
|
|
159196
|
+
}
|
|
159197
|
+
return { task, ...forkGroupId ? { groupId: forkGroupId } : {} };
|
|
159198
|
+
}
|
|
158228
159199
|
async function forkSessionToTask(sourceSessionId, input, source = "web-api") {
|
|
158229
159200
|
const { task_id, create_child_task, child_title, message, title, model, imageContext } = input;
|
|
158230
159201
|
if (!task_id && !create_child_task) {
|
|
@@ -158234,14 +159205,7 @@ async function forkSessionToTask(sourceSessionId, input, source = "web-api") {
|
|
|
158234
159205
|
throw new SessionControlError("task_id and create_child_task are mutually exclusive", 400);
|
|
158235
159206
|
}
|
|
158236
159207
|
const { getSessionByClaudeId: getSessionByClaudeId2, getSessionsForTask: getSessionsForTask2, createSessionRecord: createSessionRecord2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
|
|
158237
|
-
const {
|
|
158238
|
-
getTask: getTask2,
|
|
158239
|
-
addTask: addTask2,
|
|
158240
|
-
updateTask: updateTask2,
|
|
158241
|
-
groupTasks: groupTasks2,
|
|
158242
|
-
addToGroup: addToGroup2,
|
|
158243
|
-
renameGroup: renameGroup2
|
|
158244
|
-
} = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
|
|
159208
|
+
const { getTask: getTask2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
|
|
158245
159209
|
const sourceRecord = await getSessionByClaudeId2(sourceSessionId);
|
|
158246
159210
|
if (!sourceRecord) throw new SessionControlError("Source session not found", 404);
|
|
158247
159211
|
if (!engineCaps(sourceRecord.engine).fork) {
|
|
@@ -158258,108 +159222,13 @@ async function forkSessionToTask(sourceSessionId, input, source = "web-api") {
|
|
|
158258
159222
|
if (!sourceRecord.taskId) {
|
|
158259
159223
|
throw new SessionControlError("Source session has no task \u2014 cannot create fork task", 400);
|
|
158260
159224
|
}
|
|
158261
|
-
|
|
158262
|
-
|
|
158263
|
-
|
|
158264
|
-
|
|
158265
|
-
|
|
158266
|
-
|
|
158267
|
-
const sourceBaseTitle = sourceTask.title.replace(/^Fork of\s+/i, "").replace(/\s+-\s+fork of\s+.*$/i, "").trim() || sourceTask.title;
|
|
158268
|
-
const autoTitle = !child_title;
|
|
158269
|
-
const newTitle = child_title || `Fork of ${sourceBaseTitle}`;
|
|
158270
|
-
let inheritedTier;
|
|
158271
|
-
if (sourceTask.pinned && sourceTask.focus_tier) {
|
|
158272
|
-
const { getCustomTiers: getCustomTiers2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
|
|
158273
|
-
const known = ["focus", "backlog", "wait"].includes(sourceTask.focus_tier) || (await getCustomTiers2()).some((t3) => t3.id === sourceTask.focus_tier);
|
|
158274
|
-
if (known) inheritedTier = sourceTask.focus_tier;
|
|
158275
|
-
else {
|
|
158276
|
-
log.session.warn("fork: source tier is not registered, filing the fork in Satellite", {
|
|
158277
|
-
sourceTaskId: sourceTask.id,
|
|
158278
|
-
tier: sourceTask.focus_tier
|
|
158279
|
-
});
|
|
158280
|
-
}
|
|
158281
|
-
}
|
|
158282
|
-
const { task: newFork } = await addTask2({
|
|
158283
|
-
title: newTitle,
|
|
158284
|
-
project: sourceTask.project || "",
|
|
158285
|
-
source: sourceTask.source,
|
|
158286
|
-
// A fork is a person splitting off live work, so it joins the board like
|
|
158287
|
-
// any other hand-made task (Satellite = pinned, no stored tier).
|
|
158288
|
-
pinned: true,
|
|
158289
|
-
...inheritedTier ? { focus_tier: inheritedTier } : {},
|
|
158290
|
-
// Forking an Ask Walnut conversation yields another Ask Walnut
|
|
158291
|
-
// conversation: without the flag the fork loses the amber marker AND the
|
|
158292
|
-
// persona drift repair (both key on task.walnut_agent, never on project).
|
|
158293
|
-
...sourceTask.walnut_agent ? { walnut_agent: true } : {}
|
|
158294
|
-
});
|
|
158295
|
-
let forkGroupId;
|
|
158296
|
-
try {
|
|
158297
|
-
if (sourceTask.group_id) {
|
|
158298
|
-
const r8 = await addToGroup2(sourceTask.group_id, [newFork.id]);
|
|
158299
|
-
forkGroupId = r8.group_id;
|
|
158300
|
-
} else {
|
|
158301
|
-
const r8 = await groupTasks2([sourceTask.id, newFork.id], sourceTask.title);
|
|
158302
|
-
forkGroupId = r8.group_id;
|
|
158303
|
-
}
|
|
158304
|
-
} catch (err2) {
|
|
158305
|
-
log.session.warn("fork: failed to group source + fork", {
|
|
158306
|
-
sourceTaskId: sourceTask.id,
|
|
158307
|
-
forkTaskId: newFork.id,
|
|
158308
|
-
error: err2 instanceof Error ? err2.message : String(err2)
|
|
158309
|
-
});
|
|
158310
|
-
}
|
|
158311
|
-
try {
|
|
158312
|
-
task = await getTask2(newFork.id);
|
|
158313
|
-
} catch {
|
|
158314
|
-
task = newFork;
|
|
158315
|
-
}
|
|
158316
|
-
bus.emit(EventNames.TASK_CREATED, { task }, ["web-ui", "main-agent"], { source: "fork" });
|
|
159225
|
+
const created = await createForkSiblingTask(sourceRecord.taskId, {
|
|
159226
|
+
childTitle: child_title,
|
|
159227
|
+
prompt: message,
|
|
159228
|
+
source: "fork"
|
|
159229
|
+
});
|
|
159230
|
+
task = created.task;
|
|
158317
159231
|
childTaskCreated = true;
|
|
158318
|
-
if (autoTitle && message?.trim()) {
|
|
158319
|
-
const forkId = newFork.id;
|
|
158320
|
-
const sourceTitle = sourceBaseTitle;
|
|
158321
|
-
const placeholderTitle = newTitle;
|
|
158322
|
-
void (async () => {
|
|
158323
|
-
try {
|
|
158324
|
-
const { summarizeForkPrompt: summarizeForkPrompt2 } = await Promise.resolve().then(() => (init_fork_title(), fork_title_exports));
|
|
158325
|
-
const label = await summarizeForkPrompt2(message);
|
|
158326
|
-
if (!label) return;
|
|
158327
|
-
const current5 = await getTask2(forkId);
|
|
158328
|
-
if (current5.title !== placeholderTitle) {
|
|
158329
|
-
log.session.info("fork title refine skipped \u2014 title changed since fork", { taskId: forkId });
|
|
158330
|
-
return;
|
|
158331
|
-
}
|
|
158332
|
-
const refinedTitle = `${label} - fork of ${sourceTitle}`;
|
|
158333
|
-
const { task: updated } = await updateTask2(forkId, { title: refinedTitle }, { source: "fork-title" });
|
|
158334
|
-
bus.emit(EventNames.TASK_UPDATED, { task: updated }, ["web-ui", "main-agent"], { source: "fork-title" });
|
|
158335
|
-
log.session.info("fork title refined", { taskId: forkId, title: refinedTitle });
|
|
158336
|
-
} catch (err2) {
|
|
158337
|
-
log.session.warn("fork title refine failed", {
|
|
158338
|
-
taskId: forkId,
|
|
158339
|
-
error: err2 instanceof Error ? err2.message : String(err2)
|
|
158340
|
-
});
|
|
158341
|
-
}
|
|
158342
|
-
})();
|
|
158343
|
-
}
|
|
158344
|
-
if (forkGroupId && !sourceTask.group_id) {
|
|
158345
|
-
const gid = forkGroupId;
|
|
158346
|
-
const seedTitles = [sourceTask.title, newTitle];
|
|
158347
|
-
void (async () => {
|
|
158348
|
-
try {
|
|
158349
|
-
const { summarizeGroupLabel: summarizeGroupLabel2 } = await Promise.resolve().then(() => (init_fork_title(), fork_title_exports));
|
|
158350
|
-
const label = await summarizeGroupLabel2(seedTitles);
|
|
158351
|
-
if (!label) return;
|
|
158352
|
-
await renameGroup2(gid, label);
|
|
158353
|
-
bus.emit(EventNames.TASK_GROUPS_CHANGED, { group_id: gid, label }, ["web-ui", "main-agent"], { source: "fork" });
|
|
158354
|
-
log.session.info("fork group label refined", { groupId: gid, label });
|
|
158355
|
-
} catch (err2) {
|
|
158356
|
-
log.session.warn("fork group label refine failed", {
|
|
158357
|
-
groupId: gid,
|
|
158358
|
-
error: err2 instanceof Error ? err2.message : String(err2)
|
|
158359
|
-
});
|
|
158360
|
-
}
|
|
158361
|
-
})();
|
|
158362
|
-
}
|
|
158363
159232
|
} else {
|
|
158364
159233
|
try {
|
|
158365
159234
|
task = await getTask2(task_id);
|
|
@@ -158381,7 +159250,7 @@ async function forkSessionToTask(sourceSessionId, input, source = "web-api") {
|
|
|
158381
159250
|
const FORK_FOCUS_PREFIX = "This is a forked session. Focus on the NEW request below \u2014 treat it as your primary task. Do not resume or continue the parent session's previous work unless the user explicitly asks you to.\n\n";
|
|
158382
159251
|
const forkMessage = `${FORK_FOCUS_PREFIX}${imageContext ?? ""}New request:
|
|
158383
159252
|
${userRequest}`;
|
|
158384
|
-
const forkSessionId =
|
|
159253
|
+
const forkSessionId = randomUUID11();
|
|
158385
159254
|
const forkTitle = title ?? `Fork of ${sourceRecord.title ?? sourceSessionId.slice(0, 16)}`;
|
|
158386
159255
|
try {
|
|
158387
159256
|
await createSessionRecord2(forkSessionId, task.id, task.project ?? "", sourceRecord.cwd, {
|
|
@@ -158532,6 +159401,11 @@ async function handleSessionControlRelay(action, sessionId, params) {
|
|
|
158532
159401
|
result = await retrySession2(sessionId);
|
|
158533
159402
|
break;
|
|
158534
159403
|
}
|
|
159404
|
+
case "recheck": {
|
|
159405
|
+
const { recheckSession: recheckSession2 } = await Promise.resolve().then(() => (init_session_lifecycle(), session_lifecycle_exports));
|
|
159406
|
+
result = await recheckSession2(sessionId);
|
|
159407
|
+
break;
|
|
159408
|
+
}
|
|
158535
159409
|
case "permission": {
|
|
158536
159410
|
const { respondSessionPermission: respondSessionPermission2 } = await Promise.resolve().then(() => (init_session_lifecycle(), session_lifecycle_exports));
|
|
158537
159411
|
result = await respondSessionPermission2(sessionId, p6.requestId, p6.allow, p6.message, p6.optionId, p6.answers);
|
|
@@ -159099,7 +159973,7 @@ __export(session_rewind_exports, {
|
|
|
159099
159973
|
probeRewindViaDaemon: () => probeRewindViaDaemon,
|
|
159100
159974
|
rewindSessionToMessage: () => rewindSessionToMessage
|
|
159101
159975
|
});
|
|
159102
|
-
import { randomUUID as
|
|
159976
|
+
import { randomUUID as randomUUID12 } from "crypto";
|
|
159103
159977
|
function cutAncestorHistoryAtRewindPoint(ancestorMessages, rewoundAtMessageUuid) {
|
|
159104
159978
|
if (!rewoundAtMessageUuid) {
|
|
159105
159979
|
return { messages: ancestorMessages, found: false, dropped: 0 };
|
|
@@ -159300,7 +160174,7 @@ async function rewindSessionToMessage(sessionId, input, source = "web-api") {
|
|
|
159300
160174
|
error: err2 instanceof Error ? err2.message : String(err2)
|
|
159301
160175
|
});
|
|
159302
160176
|
}
|
|
159303
|
-
const rewoundId =
|
|
160177
|
+
const rewoundId = randomUUID12();
|
|
159304
160178
|
const baseTitle = (record.title ?? sessionId.slice(0, 16)).replace(/\s*\(rewound\)$/i, "");
|
|
159305
160179
|
const title = `${baseTitle} (rewound)`.slice(0, 200);
|
|
159306
160180
|
const { createSessionRecord: createSessionRecord2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
|
|
@@ -160020,6 +160894,7 @@ function parseSessionMessages(content, opts) {
|
|
|
160020
160894
|
}
|
|
160021
160895
|
}
|
|
160022
160896
|
let lastPlanContent = null;
|
|
160897
|
+
let dropWarmupReply = false;
|
|
160023
160898
|
const resultParentIds = [];
|
|
160024
160899
|
const result = [];
|
|
160025
160900
|
for (const [mapMsgId, msg] of messageMap) {
|
|
@@ -160097,10 +160972,18 @@ ${imgPath}` : imgPath;
|
|
|
160097
160972
|
let text = textParts.join("\n").trim();
|
|
160098
160973
|
if (!text && tools2.length === 0 && !thinking) continue;
|
|
160099
160974
|
if (msg.role === "user" && text.startsWith("<")) {
|
|
160975
|
+
if (isCacheWarmupText(text)) {
|
|
160976
|
+
dropWarmupReply = true;
|
|
160977
|
+
continue;
|
|
160978
|
+
}
|
|
160100
160979
|
const transformed = transformInjectedUserText(text);
|
|
160101
160980
|
if (transformed === null) continue;
|
|
160102
160981
|
if (transformed !== void 0) text = transformed;
|
|
160103
160982
|
}
|
|
160983
|
+
if (msg.role === "assistant" && dropWarmupReply) {
|
|
160984
|
+
dropWarmupReply = false;
|
|
160985
|
+
if (tools2.length === 0) continue;
|
|
160986
|
+
}
|
|
160104
160987
|
if (msg.role === "user") text = stripOutputModeWrappers(text);
|
|
160105
160988
|
if (msg.role === "assistant" && !text && tools2.length === 0) continue;
|
|
160106
160989
|
result.push({
|
|
@@ -161051,6 +161934,7 @@ var init_session_history = __esm({
|
|
|
161051
161934
|
init_session_io();
|
|
161052
161935
|
init_constants();
|
|
161053
161936
|
init_remote_image_mirror();
|
|
161937
|
+
init_side_thread_warmup();
|
|
161054
161938
|
init_session_file_reader();
|
|
161055
161939
|
LOCAL_HOME = os36.homedir();
|
|
161056
161940
|
TAIL_SEGMENT_ROLL_BYTES = 4 * 1024 * 1024;
|
|
@@ -161198,7 +162082,7 @@ var init_context_window = __esm({
|
|
|
161198
162082
|
});
|
|
161199
162083
|
|
|
161200
162084
|
// src/providers/turn-ledger.ts
|
|
161201
|
-
import { randomUUID as
|
|
162085
|
+
import { randomUUID as randomUUID13 } from "crypto";
|
|
161202
162086
|
function openTurn(sessionId) {
|
|
161203
162087
|
const existing = openTurns.get(sessionId);
|
|
161204
162088
|
if (existing && !existing.settled) {
|
|
@@ -161208,7 +162092,7 @@ function openTurn(sessionId) {
|
|
|
161208
162092
|
});
|
|
161209
162093
|
return { turnId: existing.turnId, promise: existing.promise };
|
|
161210
162094
|
}
|
|
161211
|
-
const turnId =
|
|
162095
|
+
const turnId = randomUUID13();
|
|
161212
162096
|
let resolve;
|
|
161213
162097
|
let reject;
|
|
161214
162098
|
const promise = new Promise((res, rej) => {
|
|
@@ -167956,11 +168840,39 @@ var init_claude_code_session = __esm({
|
|
|
167956
168840
|
session2.pendingDescription = message.slice(0, 500);
|
|
167957
168841
|
let appendSystemPrompt;
|
|
167958
168842
|
const isFork = !!data.forkedFromSessionId;
|
|
167959
|
-
|
|
167960
|
-
|
|
167961
|
-
|
|
167962
|
-
|
|
167963
|
-
|
|
168843
|
+
const { parseSideLaneKey: parseSideLaneKey2 } = await Promise.resolve().then(() => (init_side_thread_fork(), side_thread_fork_exports));
|
|
168844
|
+
const sideIds = parseSideLaneKey2(data.lane);
|
|
168845
|
+
if (sideIds) {
|
|
168846
|
+
let exact = data.appendSystemPromptExact;
|
|
168847
|
+
let source = "live-or-record";
|
|
168848
|
+
if (exact === void 0) {
|
|
168849
|
+
try {
|
|
168850
|
+
const { getSessionByClaudeId: getSessionByClaudeId2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
|
|
168851
|
+
const { spawnPrefixFromRecord: spawnPrefixFromRecord2 } = await Promise.resolve().then(() => (init_spawn_prefix(), spawn_prefix_exports));
|
|
168852
|
+
const parentRecord = await getSessionByClaudeId2(sideIds.parentSid);
|
|
168853
|
+
const prefix = parentRecord ? spawnPrefixFromRecord2(parentRecord) : null;
|
|
168854
|
+
exact = prefix?.appendSystemPrompt ?? null;
|
|
168855
|
+
source = prefix?.source ?? "no-parent-record";
|
|
168856
|
+
} catch (err2) {
|
|
168857
|
+
exact = null;
|
|
168858
|
+
source = "lookup-failed";
|
|
168859
|
+
log.session.warn("side thread: parent prompt lookup failed \u2014 spawning without an append prompt", {
|
|
168860
|
+
parentSid: sideIds.parentSid,
|
|
168861
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
168862
|
+
});
|
|
168863
|
+
}
|
|
168864
|
+
}
|
|
168865
|
+
appendSystemPrompt = exact ?? void 0;
|
|
168866
|
+
log.session.info("side thread: prefix prompt resolved", {
|
|
168867
|
+
parentSid: sideIds.parentSid,
|
|
168868
|
+
source,
|
|
168869
|
+
promptLength: appendSystemPrompt?.length ?? 0
|
|
168870
|
+
});
|
|
168871
|
+
} else {
|
|
168872
|
+
if (data.appendSystemPrompt && !isFork) {
|
|
168873
|
+
appendSystemPrompt = data.appendSystemPrompt;
|
|
168874
|
+
log.session.info("using caller-provided system prompt", { taskId: taskId2, promptLength: data.appendSystemPrompt.length });
|
|
168875
|
+
}
|
|
167964
168876
|
try {
|
|
167965
168877
|
const { buildSessionContext: buildSessionContext2 } = await Promise.resolve().then(() => (init_session_context(), session_context_exports));
|
|
167966
168878
|
const ctx = await buildSessionContext2(taskId2 ?? "", cwd, data.host);
|
|
@@ -167978,32 +168890,6 @@ ${ctx.systemPrompt}` : ctx.systemPrompt;
|
|
|
167978
168890
|
log.session.warn("failed to build session context", { taskId: taskId2, error: err2 instanceof Error ? err2.message : String(err2) });
|
|
167979
168891
|
}
|
|
167980
168892
|
}
|
|
167981
|
-
{
|
|
167982
|
-
const { parseSideLaneKey: parseSideLaneKey2 } = await Promise.resolve().then(() => (init_side_thread_fork(), side_thread_fork_exports));
|
|
167983
|
-
const sideIds = parseSideLaneKey2(data.lane);
|
|
167984
|
-
if (sideIds) {
|
|
167985
|
-
try {
|
|
167986
|
-
const { getSessionByClaudeId: getSessionByClaudeId2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
|
|
167987
|
-
const parentRecord = await getSessionByClaudeId2(sideIds.parentSid);
|
|
167988
|
-
const inherited = parentRecord?.appliedAppendSystemPrompt;
|
|
167989
|
-
if (inherited && Buffer.byteLength(inherited, "utf8") <= 65536) {
|
|
167990
|
-
appendSystemPrompt = inherited;
|
|
167991
|
-
log.session.info("side thread: inherited parent system prompt verbatim", {
|
|
167992
|
-
parentSid: sideIds.parentSid,
|
|
167993
|
-
promptLength: appendSystemPrompt.length
|
|
167994
|
-
});
|
|
167995
|
-
} else {
|
|
167996
|
-
log.session.info("side thread: parent has no stored system prompt \u2014 fresh build (cache prefix will diverge)", {
|
|
167997
|
-
parentSid: sideIds.parentSid
|
|
167998
|
-
});
|
|
167999
|
-
}
|
|
168000
|
-
} catch (err2) {
|
|
168001
|
-
log.session.warn("side thread: parent prompt lookup failed \u2014 fresh build", {
|
|
168002
|
-
error: err2 instanceof Error ? err2.message : String(err2)
|
|
168003
|
-
});
|
|
168004
|
-
}
|
|
168005
|
-
}
|
|
168006
|
-
}
|
|
168007
168893
|
if (data.preassignedSessionId && appendSystemPrompt && Buffer.byteLength(appendSystemPrompt, "utf8") <= 65536) {
|
|
168008
168894
|
const promptToStore = appendSystemPrompt;
|
|
168009
168895
|
const sidToStore = data.preassignedSessionId;
|
|
@@ -177002,7 +177888,7 @@ __export(mobile_launch_exports, {
|
|
|
177002
177888
|
performMobileLaunch: () => performMobileLaunch,
|
|
177003
177889
|
validateMobileLaunchBody: () => validateMobileLaunchBody
|
|
177004
177890
|
});
|
|
177005
|
-
import { randomUUID as
|
|
177891
|
+
import { randomUUID as randomUUID14 } from "crypto";
|
|
177006
177892
|
function launchErrorCode(status2) {
|
|
177007
177893
|
if (status2 === 404) return "not_found";
|
|
177008
177894
|
if (status2 === 409) return "conflict";
|
|
@@ -177124,7 +178010,7 @@ async function performMobileLaunch(input, source) {
|
|
|
177124
178010
|
);
|
|
177125
178011
|
}
|
|
177126
178012
|
}
|
|
177127
|
-
const preassignedSessionId = engineCaps(input.engine).idProvisioning === "provider-issued" ? void 0 :
|
|
178013
|
+
const preassignedSessionId = engineCaps(input.engine).idProvisioning === "provider-issued" ? void 0 : randomUUID14();
|
|
177128
178014
|
const task = await quickStartSession({
|
|
177129
178015
|
message: input.message,
|
|
177130
178016
|
cwd: input.cwd,
|
|
@@ -177551,6 +178437,7 @@ __export(daemon_connection_exports, {
|
|
|
177551
178437
|
isDaemonConnected: () => isDaemonConnected,
|
|
177552
178438
|
isPooledConnection: () => isPooledConnection,
|
|
177553
178439
|
probeDaemonSession: () => probeDaemonSession,
|
|
178440
|
+
probeDaemonSessionArgs: () => probeDaemonSessionArgs,
|
|
177554
178441
|
pushDaemonHooksToAllHosts: () => pushDaemonHooksToAllHosts,
|
|
177555
178442
|
setOnDaemonHostConnected: () => setOnDaemonHostConnected,
|
|
177556
178443
|
setOnDaemonStatusChange: () => setOnDaemonStatusChange,
|
|
@@ -177727,6 +178614,28 @@ async function probeDaemonSession(hostKey2, sessionId) {
|
|
|
177727
178614
|
return null;
|
|
177728
178615
|
}
|
|
177729
178616
|
}
|
|
178617
|
+
async function probeDaemonSessionArgs(hostKey2, sessionId, deadlineMs = 2e3) {
|
|
178618
|
+
const conn = getConnectedDaemonConnection(hostKey2);
|
|
178619
|
+
if (!conn) return null;
|
|
178620
|
+
try {
|
|
178621
|
+
const result = await Promise.race([
|
|
178622
|
+
conn.send("status", { sid: sessionId, includeArgs: true }),
|
|
178623
|
+
new Promise((_, reject) => setTimeout(
|
|
178624
|
+
() => reject(new Error(`status probe exceeded ${deadlineMs}ms`)),
|
|
178625
|
+
deadlineMs
|
|
178626
|
+
).unref?.())
|
|
178627
|
+
]);
|
|
178628
|
+
if (!result.ok || !result.alive || !Array.isArray(result.args)) return null;
|
|
178629
|
+
return result.args.filter((a8) => typeof a8 === "string");
|
|
178630
|
+
} catch (err2) {
|
|
178631
|
+
log.session.debug("probeDaemonSessionArgs: status probe failed", {
|
|
178632
|
+
hostKey: hostKey2,
|
|
178633
|
+
sessionId,
|
|
178634
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
178635
|
+
});
|
|
178636
|
+
return null;
|
|
178637
|
+
}
|
|
178638
|
+
}
|
|
177730
178639
|
var execFileAsync23, MOBILE_ENQUEUE_LEDGER_MAX, recentMobileEnqueues, DaemonConnection, onPoolStatusChange, onHostConnected, hostConnectedListeners, connectionPool, connectingPromises, failureCache, FAILURE_CACHE_TTL_MS;
|
|
177731
178640
|
var init_daemon_connection = __esm({
|
|
177732
178641
|
"src/providers/daemon-connection.ts"() {
|
|
@@ -182294,6 +183203,7 @@ async function createSessionRecord(claudeSessionId, taskId2, project, cwd, extra
|
|
|
182294
183203
|
...extra?.effort ? { effort: extra.effort } : {},
|
|
182295
183204
|
...extra?.profile ? { profile: extra.profile } : {},
|
|
182296
183205
|
...extra?.lane ? { lane: extra.lane } : {},
|
|
183206
|
+
...extra?.appliedAppendSystemPrompt !== void 0 ? { appliedAppendSystemPrompt: extra.appliedAppendSystemPrompt } : {},
|
|
182297
183207
|
...extra?.engine ? { engine: extra.engine } : {},
|
|
182298
183208
|
...extra?.acpRuntimeId ? { acpRuntimeId: extra.acpRuntimeId } : {},
|
|
182299
183209
|
...extra?.acpJournalPath ? { acpJournalPath: extra.acpJournalPath } : {},
|
|
@@ -182448,7 +183358,7 @@ function applyUpdateToSession(session2, updates, logLabel) {
|
|
|
182448
183358
|
};
|
|
182449
183359
|
if (count2 === 1) log.session.info("legacy status write suppressed", payload);
|
|
182450
183360
|
else log.session.debug("legacy status write suppressed", payload);
|
|
182451
|
-
if (updates.process_status === "error") {
|
|
183361
|
+
if (updates.process_status === "error" || updates.process_status === "stopped") {
|
|
182452
183362
|
noteSuppressedErrorReason(session2.claudeSessionId, {
|
|
182453
183363
|
reason: typeof reason === "string" ? reason : "unknown",
|
|
182454
183364
|
message: typeof updates.errorMessage === "string" ? updates.errorMessage : void 0,
|