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/web/server.js
CHANGED
|
@@ -10199,7 +10199,26 @@ function reapSession(sid, code, reason) {
|
|
|
10199
10199
|
let jsonlAgeMs = null;
|
|
10200
10200
|
try { jsonlAgeMs = Date.now() - fs.statSync(session.jsonlPath).mtimeMs; } catch {}
|
|
10201
10201
|
const cleanExit = isTurnCompleteExit(session.jsonlPath);
|
|
10202
|
-
|
|
10202
|
+
// A DELIBERATE stop is not a failure, and the JSONL tail cannot tell us it
|
|
10203
|
+
// happened. Both writers (our idle scanner, and any stop RPC via cmdStop)
|
|
10204
|
+
// stamp intentionalStopAt before signalling; the death then lands here through
|
|
10205
|
+
// whichever path notices it (usually orphan-poll ESRCH \u2192 code -1). Verified
|
|
10206
|
+
// production case: the last line was a control_response (the reply to a
|
|
10207
|
+
// Walnut-issued control request \u2014 a routine shape), so isTurnCompleteExit said
|
|
10208
|
+
// false, exitCode stayed -1, and the projection painted a red "Error" over a
|
|
10209
|
+
// healthy, fully resumable session that was stopped on purpose. Checked FIRST
|
|
10210
|
+
// and unconditionally: the flag carries information the tail does not contain,
|
|
10211
|
+
// which is why isTurnCompleteExit is left alone.
|
|
10212
|
+
// Keep in sync with daemon-core.ts reapSession.
|
|
10213
|
+
const intentionalStop = session.intentionalStopAt != null;
|
|
10214
|
+
if (intentionalStop && code !== 0) {
|
|
10215
|
+
logMsg('info', 'reapSession: intentional stop, normalizing exit code', {
|
|
10216
|
+
sid, pid: session.pid, originalCode: code, originalReason: reason,
|
|
10217
|
+
jsonlAgeMs, cleanExit, intentionalStopAt: session.intentionalStopAt,
|
|
10218
|
+
});
|
|
10219
|
+
code = 0;
|
|
10220
|
+
reason = reason + '+intentional-stop';
|
|
10221
|
+
} else if (code !== 0 && cleanExit) {
|
|
10203
10222
|
logMsg('info', 'reapSession: turn-complete detected, normalizing exit code', {
|
|
10204
10223
|
sid, pid: session.pid, originalCode: code, originalReason: reason, jsonlAgeMs,
|
|
10205
10224
|
});
|
|
@@ -13852,6 +13871,15 @@ function cmdStop(ws, id, cmd) {
|
|
|
13852
13871
|
const pid = session.pid;
|
|
13853
13872
|
logMsg('info', 'cmdStop: stopping session (process group kill)', { sid, pid });
|
|
13854
13873
|
|
|
13874
|
+
// Nobody sends stop by accident, so record the intent BEFORE the first signal:
|
|
13875
|
+
// the death lands asynchronously (orphan poll \u2192 ESRCH \u2192 code -1) and by then
|
|
13876
|
+
// nothing remembers this was asked for. This path is how the SERVER's idle
|
|
13877
|
+
// reaper kills (mgr.kill() \u2192 stop RPC), not the daemon's own scan, so it needs
|
|
13878
|
+
// its own stamp: a LOCAL session auto-stopped after 2h idle surfaced as
|
|
13879
|
+
// "Session ended unexpectedly and no cause was recorded" (2026-09-03).
|
|
13880
|
+
// Keep in sync with daemon-standalone.ts cmdStop.
|
|
13881
|
+
session.intentionalStopAt = Date.now();
|
|
13882
|
+
|
|
13855
13883
|
// An explicit stop is a human decision \u2014 never undone by a pending auto-retry
|
|
13856
13884
|
// respawning the CLI minutes later. Cancel the timer AND clear the streak so
|
|
13857
13885
|
// an in-flight result line can't re-arm one either.
|
|
@@ -13888,7 +13916,7 @@ function cmdStop(ws, id, cmd) {
|
|
|
13888
13916
|
|
|
13889
13917
|
// \u2500\u2500 Status \u2500\u2500
|
|
13890
13918
|
function cmdStatus(ws, id, cmd) {
|
|
13891
|
-
const { sid } = cmd;
|
|
13919
|
+
const { sid, includeArgs } = cmd;
|
|
13892
13920
|
if (!sid) return sendError(ws, id, 'status: missing sid');
|
|
13893
13921
|
|
|
13894
13922
|
const session = sessions.get(sid);
|
|
@@ -13919,6 +13947,11 @@ function cmdStatus(ws, id, cmd) {
|
|
|
13919
13947
|
exitCode: session.exitCode,
|
|
13920
13948
|
exitReason: session.exitReason,
|
|
13921
13949
|
pendingCtrl: session.pendingCtrl,
|
|
13950
|
+
// Opt-in: the argv the live process was launched with (a fork copies it to
|
|
13951
|
+
// reproduce the parent's prompt-cache prefix). Off by default because the
|
|
13952
|
+
// health monitor polls status for every session and the argv carries the
|
|
13953
|
+
// multi-KB --append-system-prompt.
|
|
13954
|
+
...(includeArgs && session.args && session.args.length > 0 ? { args: session.args } : {}),
|
|
13922
13955
|
});
|
|
13923
13956
|
}
|
|
13924
13957
|
|
|
@@ -15964,6 +15997,14 @@ function scanIdleSessions() {
|
|
|
15964
15997
|
}
|
|
15965
15998
|
const idleMinutes = Math.round(idleMs / 60000);
|
|
15966
15999
|
logMsg('warn', 'idle scan: killing idle session (no subscribers, no output)', { sid, pid, idleMinutes, protectedBy: prot.source, thresholdMs: prot.killMs, detail: prot.detail });
|
|
16000
|
+
// Record the intent BEFORE signalling: the reap arrives asynchronously
|
|
16001
|
+
// (the orphan poll sees ESRCH and reports 'orphan-poll-dead', code -1), so
|
|
16002
|
+
// by then nothing remembers that WE asked for this. Without the stamp
|
|
16003
|
+
// reapSession can only consult the JSONL tail, which says "not a clean turn
|
|
16004
|
+
// end" for any session whose last line isn't a type:result \u2014 and the
|
|
16005
|
+
// projection then shows a red Error for our own routine reclamation.
|
|
16006
|
+
// Keep in sync with daemon-standalone.ts scanIdleSessions.
|
|
16007
|
+
session.intentionalStopAt = now;
|
|
15967
16008
|
killSessionProcessGroup(pid, sid);
|
|
15968
16009
|
}
|
|
15969
16010
|
}
|
|
@@ -16647,7 +16688,20 @@ function createDaemonCore(deps) {
|
|
|
16647
16688
|
jsonlAgeMs = clock() - fs114.statSync(session2.jsonlPath).mtimeMs;
|
|
16648
16689
|
} catch {
|
|
16649
16690
|
}
|
|
16650
|
-
|
|
16691
|
+
const intentionalStop = session2.intentionalStopAt != null;
|
|
16692
|
+
if (intentionalStop && code !== 0) {
|
|
16693
|
+
logger2("info", "reapSession: intentional stop, normalizing exit code", {
|
|
16694
|
+
sid,
|
|
16695
|
+
pid: session2.pid,
|
|
16696
|
+
originalCode: code,
|
|
16697
|
+
originalReason: reason,
|
|
16698
|
+
jsonlAgeMs,
|
|
16699
|
+
cleanExit,
|
|
16700
|
+
intentionalStopAt: session2.intentionalStopAt
|
|
16701
|
+
});
|
|
16702
|
+
code = 0;
|
|
16703
|
+
reason = reason + "+intentional-stop";
|
|
16704
|
+
} else if (cleanExit && code !== 0) {
|
|
16651
16705
|
logger2("info", "reapSession: turn-complete detected, normalizing exit code", {
|
|
16652
16706
|
sid,
|
|
16653
16707
|
pid: session2.pid,
|
|
@@ -17437,6 +17491,7 @@ var init_daemon_core = __esm({
|
|
|
17437
17491
|
var claude_cli_detect_exports = {};
|
|
17438
17492
|
__export(claude_cli_detect_exports, {
|
|
17439
17493
|
_CLAUDE_CREDENTIALS_HOME: () => _CLAUDE_CREDENTIALS_HOME,
|
|
17494
|
+
describeClaudeCliAuth: () => describeClaudeCliAuth,
|
|
17440
17495
|
detectClaudeCli: () => detectClaudeCli,
|
|
17441
17496
|
hasClaudeSubscriptionAuth: () => hasClaudeSubscriptionAuth,
|
|
17442
17497
|
isClaudeCliInstalled: () => isClaudeCliInstalled,
|
|
@@ -17497,9 +17552,32 @@ function detectClaudeCli() {
|
|
|
17497
17552
|
return {
|
|
17498
17553
|
installed: installed2,
|
|
17499
17554
|
subscriptionAuth,
|
|
17500
|
-
subscriptionReady: installed2 && subscriptionAuth
|
|
17555
|
+
subscriptionReady: installed2 && subscriptionAuth,
|
|
17556
|
+
ready: installed2,
|
|
17557
|
+
auth: installed2 ? describeClaudeCliAuth() : { mode: "unknown", label: "not installed" }
|
|
17501
17558
|
};
|
|
17502
17559
|
}
|
|
17560
|
+
function describeClaudeCliAuth(env3 = process.env, settingsEnv = readClaudeSettingsEnv()) {
|
|
17561
|
+
const merged = { ...env3, ...settingsEnv };
|
|
17562
|
+
const truthy = (v3) => !!v3 && v3 !== "0" && v3.toLowerCase() !== "false";
|
|
17563
|
+
if (truthy(merged.CLAUDE_CODE_USE_BEDROCK)) {
|
|
17564
|
+
const region = merged.AWS_REGION || merged.AWS_DEFAULT_REGION;
|
|
17565
|
+
return { mode: "bedrock", label: region ? `Bedrock (${region})` : "Bedrock" };
|
|
17566
|
+
}
|
|
17567
|
+
if (truthy(merged.CLAUDE_CODE_USE_VERTEX)) return { mode: "vertex", label: "Vertex AI" };
|
|
17568
|
+
if (merged.ANTHROPIC_API_KEY || merged.ANTHROPIC_AUTH_TOKEN) return { mode: "api-key", label: "an Anthropic API key" };
|
|
17569
|
+
if (hasClaudeSubscriptionAuth()) return { mode: "subscription", label: "your Claude subscription" };
|
|
17570
|
+
return { mode: "unknown", label: "its own login" };
|
|
17571
|
+
}
|
|
17572
|
+
function readClaudeSettingsEnv() {
|
|
17573
|
+
try {
|
|
17574
|
+
const raw = fs17.readFileSync(path18.join(os4.homedir(), ".claude", "settings.json"), "utf8");
|
|
17575
|
+
const env3 = JSON.parse(raw).env ?? {};
|
|
17576
|
+
return Object.fromEntries(Object.entries(env3).filter(([, v3]) => typeof v3 === "string"));
|
|
17577
|
+
} catch {
|
|
17578
|
+
return {};
|
|
17579
|
+
}
|
|
17580
|
+
}
|
|
17503
17581
|
var KEYCHAIN_OAUTH_SERVICE, _CLAUDE_CREDENTIALS_HOME;
|
|
17504
17582
|
var init_claude_cli_detect = __esm({
|
|
17505
17583
|
"src/core/claude-cli-detect.ts"() {
|
|
@@ -18118,6 +18196,22 @@ var init_local_daemon = __esm({
|
|
|
18118
18196
|
});
|
|
18119
18197
|
|
|
18120
18198
|
// src/core/session-error-kind.ts
|
|
18199
|
+
var session_error_kind_exports = {};
|
|
18200
|
+
__export(session_error_kind_exports, {
|
|
18201
|
+
INFRA_STATUS_REASONS: () => INFRA_STATUS_REASONS,
|
|
18202
|
+
RESCUABLE_STOPPED_WINDOW_MS: () => RESCUABLE_STOPPED_WINDOW_MS,
|
|
18203
|
+
TERMINAL_STATUS_REASONS: () => TERMINAL_STATUS_REASONS,
|
|
18204
|
+
assertsHostUnreachable: () => assertsHostUnreachable,
|
|
18205
|
+
classifySessionError: () => classifySessionError,
|
|
18206
|
+
classifyStatusReasonKind: () => classifyStatusReasonKind,
|
|
18207
|
+
isInfraSessionError: () => isInfraSessionError,
|
|
18208
|
+
isRecoverableSessionError: () => isRecoverableSessionError,
|
|
18209
|
+
isRescuableStoppedRecord: () => isRescuableStoppedRecord
|
|
18210
|
+
});
|
|
18211
|
+
function assertsHostUnreachable(record) {
|
|
18212
|
+
if (!record.errorMessage) return false;
|
|
18213
|
+
return typeof record.status_reason === "string" && REACHABILITY_CLAIM_REASONS.has(record.status_reason);
|
|
18214
|
+
}
|
|
18121
18215
|
function classifyStatusReasonKind(reason) {
|
|
18122
18216
|
if (typeof reason !== "string") return void 0;
|
|
18123
18217
|
if (INFRA_STATUS_REASONS.has(reason)) return "infra";
|
|
@@ -18146,7 +18240,7 @@ function isRescuableStoppedRecord(record, nowMs = Date.now()) {
|
|
|
18146
18240
|
if (!Number.isFinite(changedAt)) return false;
|
|
18147
18241
|
return nowMs - changedAt <= RESCUABLE_STOPPED_WINDOW_MS;
|
|
18148
18242
|
}
|
|
18149
|
-
var INFRA_STATUS_REASONS, TERMINAL_STATUS_REASONS, INFRA_TEXT_SIGNATURES, RESCUABLE_STOPPED_WINDOW_MS;
|
|
18243
|
+
var INFRA_STATUS_REASONS, TERMINAL_STATUS_REASONS, INFRA_TEXT_SIGNATURES, REACHABILITY_CLAIM_REASONS, RESCUABLE_STOPPED_WINDOW_MS;
|
|
18150
18244
|
var init_session_error_kind = __esm({
|
|
18151
18245
|
"src/core/session-error-kind.ts"() {
|
|
18152
18246
|
"use strict";
|
|
@@ -18190,6 +18284,9 @@ var init_session_error_kind = __esm({
|
|
|
18190
18284
|
"ssh tunnel",
|
|
18191
18285
|
"broken pipe"
|
|
18192
18286
|
];
|
|
18287
|
+
REACHABILITY_CLAIM_REASONS = /* @__PURE__ */ new Set([
|
|
18288
|
+
"remote_unreachable"
|
|
18289
|
+
]);
|
|
18193
18290
|
RESCUABLE_STOPPED_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
18194
18291
|
}
|
|
18195
18292
|
});
|
|
@@ -24670,7 +24767,7 @@ __export(credential_resolver_exports, {
|
|
|
24670
24767
|
DEFAULT_REGION: () => DEFAULT_REGION,
|
|
24671
24768
|
hasResolvableCredential: () => hasResolvableCredential,
|
|
24672
24769
|
readClaudeCredentialExport: () => readClaudeCredentialExport,
|
|
24673
|
-
readClaudeSettingsEnv: () =>
|
|
24770
|
+
readClaudeSettingsEnv: () => readClaudeSettingsEnv2,
|
|
24674
24771
|
resolveCredentialFrom: () => resolveCredentialFrom,
|
|
24675
24772
|
resolveCredentials: () => resolveCredentials,
|
|
24676
24773
|
traceCredentialResolution: () => traceCredentialResolution
|
|
@@ -24825,7 +24922,7 @@ function traceCredentialResolution(inputs) {
|
|
|
24825
24922
|
region: { value: winner.region ?? DEFAULT_REGION, source: regionSource }
|
|
24826
24923
|
};
|
|
24827
24924
|
}
|
|
24828
|
-
function
|
|
24925
|
+
function readClaudeSettingsEnv2(settingsFile = CLAUDE_SETTINGS_FILE) {
|
|
24829
24926
|
try {
|
|
24830
24927
|
const raw = fs31.readFileSync(settingsFile, "utf-8");
|
|
24831
24928
|
const parsed = JSON.parse(raw);
|
|
@@ -24865,7 +24962,7 @@ function probeAwsFiles() {
|
|
|
24865
24962
|
function resolveCredentials(config) {
|
|
24866
24963
|
const result = resolveCredentialFrom({
|
|
24867
24964
|
config,
|
|
24868
|
-
claudeEnv:
|
|
24965
|
+
claudeEnv: readClaudeSettingsEnv2(),
|
|
24869
24966
|
processEnv: process.env,
|
|
24870
24967
|
awsFiles: probeAwsFiles(),
|
|
24871
24968
|
claudeCredentialExport: readClaudeCredentialExport()
|
|
@@ -67647,10 +67744,9 @@ function buildArgs(opts) {
|
|
|
67647
67744
|
// Input stays default (text): we write the whole prompt to stdin and close.
|
|
67648
67745
|
// (stream-json INPUT would force us into a JSON envelope for no gain here.)
|
|
67649
67746
|
"--tools",
|
|
67650
|
-
""
|
|
67747
|
+
""
|
|
67651
67748
|
// disable ALL built-in tools (deny-all)
|
|
67652
|
-
|
|
67653
|
-
FORCE_SUBSCRIPTION_SETTINGS
|
|
67749
|
+
// No --settings override: the user's settings.json (and so their auth) applies as-is.
|
|
67654
67750
|
];
|
|
67655
67751
|
let systemText = systemToText(opts.system).trim();
|
|
67656
67752
|
if (opts.tools && opts.tools.length > 0) {
|
|
@@ -67679,7 +67775,7 @@ function systemToText(system) {
|
|
|
67679
67775
|
}
|
|
67680
67776
|
function buildSpawnEnv(base = process.env) {
|
|
67681
67777
|
const env3 = { ...base };
|
|
67682
|
-
for (const key2 of
|
|
67778
|
+
for (const key2 of ENV_TO_STRIP) {
|
|
67683
67779
|
delete env3[key2];
|
|
67684
67780
|
}
|
|
67685
67781
|
env3.CLAUDE_CODE_ENTRYPOINT = WALNUT_UTILITY_ENTRYPOINT;
|
|
@@ -67741,7 +67837,7 @@ function normalizeCliModel(model) {
|
|
|
67741
67837
|
}
|
|
67742
67838
|
return void 0;
|
|
67743
67839
|
}
|
|
67744
|
-
var
|
|
67840
|
+
var ENV_TO_STRIP, MAX_CONCURRENT_CLI, CLI_TURN_TIMEOUT_MS, SESSION_REUSE_TTL_MS, MAX_TRACKED_SESSIONS, ClaudeCliAdapter;
|
|
67745
67841
|
var init_adapter_claude_cli = __esm({
|
|
67746
67842
|
"src/agent/providers/adapter-claude-cli.ts"() {
|
|
67747
67843
|
"use strict";
|
|
@@ -67751,24 +67847,11 @@ var init_adapter_claude_cli = __esm({
|
|
|
67751
67847
|
init_logging();
|
|
67752
67848
|
init_claude_cli_detect();
|
|
67753
67849
|
init_claude_cli_protocol();
|
|
67754
|
-
|
|
67755
|
-
|
|
67756
|
-
|
|
67757
|
-
|
|
67758
|
-
|
|
67759
|
-
"AWS_ACCESS_KEY_ID",
|
|
67760
|
-
"AWS_SECRET_ACCESS_KEY",
|
|
67761
|
-
"AWS_SESSION_TOKEN",
|
|
67762
|
-
"AWS_PROFILE",
|
|
67763
|
-
"CLAUDE_CODE_USE_BEDROCK",
|
|
67764
|
-
"CLAUDE_CODE_USE_VERTEX",
|
|
67765
|
-
// Nested-session marker — inline-subagent strips this too so the child doesn't
|
|
67766
|
-
// think it's running inside another Claude session.
|
|
67767
|
-
"CLAUDECODE"
|
|
67768
|
-
];
|
|
67769
|
-
FORCE_SUBSCRIPTION_SETTINGS = JSON.stringify({
|
|
67770
|
-
env: { CLAUDE_CODE_USE_BEDROCK: "", CLAUDE_CODE_USE_VERTEX: "" }
|
|
67771
|
-
});
|
|
67850
|
+
ENV_TO_STRIP = ["CLAUDECODE"];
|
|
67851
|
+
MAX_CONCURRENT_CLI = (() => {
|
|
67852
|
+
const n6 = Number(process.env.WALNUT_CLAUDE_CLI_CONCURRENCY);
|
|
67853
|
+
return Number.isFinite(n6) && n6 > 0 ? Math.floor(n6) : 3;
|
|
67854
|
+
})();
|
|
67772
67855
|
CLI_TURN_TIMEOUT_MS = 12e4;
|
|
67773
67856
|
SESSION_REUSE_TTL_MS = 30 * 60 * 1e3;
|
|
67774
67857
|
MAX_TRACKED_SESSIONS = 50;
|
|
@@ -67776,6 +67859,25 @@ var init_adapter_claude_cli = __esm({
|
|
|
67776
67859
|
protocol = "claude-cli";
|
|
67777
67860
|
/** conversationKey → CLI session for --resume chaining. In-process only. */
|
|
67778
67861
|
sessions = /* @__PURE__ */ new Map();
|
|
67862
|
+
/** Process gate: MAX_CONCURRENT_CLI turns at once, the rest queue in order. */
|
|
67863
|
+
inFlight = 0;
|
|
67864
|
+
waiters = [];
|
|
67865
|
+
async withSlot(fn) {
|
|
67866
|
+
if (this.inFlight >= MAX_CONCURRENT_CLI) {
|
|
67867
|
+
await new Promise((resolve) => this.waiters.push(resolve));
|
|
67868
|
+
}
|
|
67869
|
+
this.inFlight++;
|
|
67870
|
+
try {
|
|
67871
|
+
return await fn();
|
|
67872
|
+
} finally {
|
|
67873
|
+
this.inFlight--;
|
|
67874
|
+
this.waiters.shift()?.();
|
|
67875
|
+
}
|
|
67876
|
+
}
|
|
67877
|
+
/** Test hook: how many turns are running / waiting right now. */
|
|
67878
|
+
_gateStateForTesting() {
|
|
67879
|
+
return { inFlight: this.inFlight, waiting: this.waiters.length };
|
|
67880
|
+
}
|
|
67779
67881
|
resetClient() {
|
|
67780
67882
|
this.sessions.clear();
|
|
67781
67883
|
}
|
|
@@ -67853,7 +67955,7 @@ User: ${overridePrompt}` : "");
|
|
|
67853
67955
|
}
|
|
67854
67956
|
if (!prompt.trim()) prompt = "(continue)";
|
|
67855
67957
|
const args = [...buildArgs(opts), session2.flag, session2.id];
|
|
67856
|
-
return this.execCli(command6, args, env3, prompt, opts.signal, onTextDelta).then((result) => {
|
|
67958
|
+
return this.withSlot(() => this.execCli(command6, args, env3, prompt, opts.signal, onTextDelta)).then((result) => {
|
|
67857
67959
|
this.sessions.set(key2, {
|
|
67858
67960
|
sessionId: session2.id,
|
|
67859
67961
|
seenMessages: opts.messages.length,
|
|
@@ -67872,7 +67974,7 @@ User: ${overridePrompt}` : "");
|
|
|
67872
67974
|
User: ${overridePrompt}` : "");
|
|
67873
67975
|
const freshId = randomUUID3();
|
|
67874
67976
|
const freshArgs = [...buildArgs(opts), "--session-id", freshId];
|
|
67875
|
-
return this.execCli(command6, freshArgs, env3, freshPrompt || "(continue)", opts.signal, onTextDelta).then((result) => {
|
|
67977
|
+
return this.withSlot(() => this.execCli(command6, freshArgs, env3, freshPrompt || "(continue)", opts.signal, onTextDelta)).then((result) => {
|
|
67876
67978
|
this.sessions.set(key2, {
|
|
67877
67979
|
sessionId: freshId,
|
|
67878
67980
|
seenMessages: opts.messages.length,
|
|
@@ -67956,7 +68058,7 @@ User: ${overridePrompt}` : "");
|
|
|
67956
68058
|
}
|
|
67957
68059
|
const resultText = finalResult?.result || accumulatedText;
|
|
67958
68060
|
if (resultText && resultText.length < 300 && /^\s*failed to authenticate|^\s*api error: 4\d\d/i.test(resultText)) {
|
|
67959
|
-
reject(new Error(`claude-cli
|
|
68061
|
+
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)}`));
|
|
67960
68062
|
return;
|
|
67961
68063
|
}
|
|
67962
68064
|
if (code !== 0 && !accumulatedText && !finalResult) {
|
|
@@ -68156,6 +68258,50 @@ var init_registry = __esm({
|
|
|
68156
68258
|
}
|
|
68157
68259
|
});
|
|
68158
68260
|
|
|
68261
|
+
// src/agent/providers/default-provider.ts
|
|
68262
|
+
var default_provider_exports = {};
|
|
68263
|
+
__export(default_provider_exports, {
|
|
68264
|
+
CLAUDE_CLI_PROVIDER: () => CLAUDE_CLI_PROVIDER,
|
|
68265
|
+
LEGACY_DEFAULT_PROVIDER: () => LEGACY_DEFAULT_PROVIDER,
|
|
68266
|
+
_resetDefaultProviderCacheForTesting: () => _resetDefaultProviderCacheForTesting,
|
|
68267
|
+
hasExplicitBedrockConfig: () => hasExplicitBedrockConfig,
|
|
68268
|
+
mainProviderIsImplicit: () => mainProviderIsImplicit,
|
|
68269
|
+
resolveMainProviderName: () => resolveMainProviderName
|
|
68270
|
+
});
|
|
68271
|
+
function claudeCliInstalledCached() {
|
|
68272
|
+
const now = Date.now();
|
|
68273
|
+
if (!cliProbe || now - cliProbe.at > CLI_PROBE_TTL_MS) {
|
|
68274
|
+
cliProbe = { at: now, installed: isClaudeCliInstalled() };
|
|
68275
|
+
}
|
|
68276
|
+
return cliProbe.installed;
|
|
68277
|
+
}
|
|
68278
|
+
function _resetDefaultProviderCacheForTesting() {
|
|
68279
|
+
cliProbe = void 0;
|
|
68280
|
+
}
|
|
68281
|
+
function hasExplicitBedrockConfig(config) {
|
|
68282
|
+
const b8 = config.providers?.bedrock;
|
|
68283
|
+
if (b8 && (b8.bearer_token || b8.aws_access_key_id || b8.aws_profile || b8.aws_credential_export)) return true;
|
|
68284
|
+
return !!config.provider?.bedrock_bearer_token;
|
|
68285
|
+
}
|
|
68286
|
+
function resolveMainProviderName(config, claudeInstalled = claudeCliInstalledCached()) {
|
|
68287
|
+
if (config.agent?.main_provider) return config.agent.main_provider;
|
|
68288
|
+
if (claudeInstalled) return CLAUDE_CLI_PROVIDER;
|
|
68289
|
+
return LEGACY_DEFAULT_PROVIDER;
|
|
68290
|
+
}
|
|
68291
|
+
function mainProviderIsImplicit(config) {
|
|
68292
|
+
return !config.agent?.main_provider;
|
|
68293
|
+
}
|
|
68294
|
+
var LEGACY_DEFAULT_PROVIDER, CLAUDE_CLI_PROVIDER, CLI_PROBE_TTL_MS, cliProbe;
|
|
68295
|
+
var init_default_provider = __esm({
|
|
68296
|
+
"src/agent/providers/default-provider.ts"() {
|
|
68297
|
+
"use strict";
|
|
68298
|
+
init_claude_cli_detect();
|
|
68299
|
+
LEGACY_DEFAULT_PROVIDER = "bedrock";
|
|
68300
|
+
CLAUDE_CLI_PROVIDER = "claude_cli";
|
|
68301
|
+
CLI_PROBE_TTL_MS = 6e4;
|
|
68302
|
+
}
|
|
68303
|
+
});
|
|
68304
|
+
|
|
68159
68305
|
// src/agent/model.ts
|
|
68160
68306
|
var model_exports = {};
|
|
68161
68307
|
__export(model_exports, {
|
|
@@ -68190,7 +68336,7 @@ function getContextThreshold(model, percent) {
|
|
|
68190
68336
|
}
|
|
68191
68337
|
async function resolveForCall(config) {
|
|
68192
68338
|
const fullConfig = await getConfig();
|
|
68193
|
-
const providerName = config?.provider ?? fullConfig
|
|
68339
|
+
const providerName = config?.provider ?? resolveMainProviderName(fullConfig);
|
|
68194
68340
|
const hasExplicitProviders = fullConfig.providers && Object.keys(fullConfig.providers).length > 0;
|
|
68195
68341
|
let providers = hasExplicitProviders ? buildProviderMap(fullConfig.providers, fullConfig) : synthesizeFromLegacy(fullConfig);
|
|
68196
68342
|
const resolved = resolveProvider(providerName, providers);
|
|
@@ -68260,6 +68406,7 @@ var init_model = __esm({
|
|
|
68260
68406
|
"use strict";
|
|
68261
68407
|
init_config_manager();
|
|
68262
68408
|
init_registry();
|
|
68409
|
+
init_default_provider();
|
|
68263
68410
|
init_defaults();
|
|
68264
68411
|
init_model_catalog();
|
|
68265
68412
|
}
|
|
@@ -68462,8 +68609,22 @@ var init_token_truth = __esm({
|
|
|
68462
68609
|
});
|
|
68463
68610
|
|
|
68464
68611
|
// src/utils/image-compress.ts
|
|
68465
|
-
|
|
68612
|
+
async function loadSharp() {
|
|
68613
|
+
sharpLoad ??= import("sharp").then(
|
|
68614
|
+
(m6) => m6.default ?? m6,
|
|
68615
|
+
(err2) => {
|
|
68616
|
+
log.agent.warn("sharp is not installed; images are sent uncompressed", {
|
|
68617
|
+
error: err2 instanceof Error ? err2.message : String(err2),
|
|
68618
|
+
fix: "a machine with glibc 2.28+ installs sharp prebuilt; otherwise install libvips and re-run npm install"
|
|
68619
|
+
});
|
|
68620
|
+
return null;
|
|
68621
|
+
}
|
|
68622
|
+
);
|
|
68623
|
+
return sharpLoad;
|
|
68624
|
+
}
|
|
68466
68625
|
async function clampImageDimensions(buffer2, mimeType) {
|
|
68626
|
+
const sharp = await loadSharp();
|
|
68627
|
+
if (!sharp) return { buffer: buffer2, mimeType };
|
|
68467
68628
|
try {
|
|
68468
68629
|
const meta = await sharp(buffer2).metadata();
|
|
68469
68630
|
const longest = Math.max(meta.width ?? 0, meta.height ?? 0);
|
|
@@ -68485,6 +68646,8 @@ async function compressForApi(inputBuffer, inputMimeType) {
|
|
|
68485
68646
|
if (buffer2.toString("base64").length <= MAX_BASE64_BYTES) {
|
|
68486
68647
|
return { buffer: buffer2, mimeType };
|
|
68487
68648
|
}
|
|
68649
|
+
const sharp = await loadSharp();
|
|
68650
|
+
if (!sharp) return { buffer: buffer2, mimeType };
|
|
68488
68651
|
try {
|
|
68489
68652
|
if (mimeType === "image/gif") {
|
|
68490
68653
|
const webp = await sharp(buffer2, { animated: true }).webp({ quality: 80 }).toBuffer();
|
|
@@ -68522,10 +68685,11 @@ async function compressForApi(inputBuffer, inputMimeType) {
|
|
|
68522
68685
|
return { buffer: buffer2, mimeType };
|
|
68523
68686
|
}
|
|
68524
68687
|
}
|
|
68525
|
-
var MAX_BASE64_BYTES, MAX_IMAGE_DIMENSION;
|
|
68688
|
+
var sharpLoad, MAX_BASE64_BYTES, MAX_IMAGE_DIMENSION;
|
|
68526
68689
|
var init_image_compress = __esm({
|
|
68527
68690
|
"src/utils/image-compress.ts"() {
|
|
68528
68691
|
"use strict";
|
|
68692
|
+
init_logging();
|
|
68529
68693
|
MAX_BASE64_BYTES = 5e6;
|
|
68530
68694
|
MAX_IMAGE_DIMENSION = 1568;
|
|
68531
68695
|
}
|
|
@@ -69683,7 +69847,7 @@ __export(cheap_model_exports, {
|
|
|
69683
69847
|
});
|
|
69684
69848
|
function fastModelFor(config) {
|
|
69685
69849
|
if (config.agent?.fast_model) return config.agent.fast_model;
|
|
69686
|
-
const providerName = config
|
|
69850
|
+
const providerName = resolveMainProviderName(config);
|
|
69687
69851
|
return MODEL_CATALOG[providerName]?.find(
|
|
69688
69852
|
(model) => model.id.toLowerCase().includes("haiku")
|
|
69689
69853
|
)?.id;
|
|
@@ -69695,6 +69859,7 @@ var init_cheap_model = __esm({
|
|
|
69695
69859
|
"src/core/cheap-model.ts"() {
|
|
69696
69860
|
"use strict";
|
|
69697
69861
|
init_model_catalog();
|
|
69862
|
+
init_default_provider();
|
|
69698
69863
|
}
|
|
69699
69864
|
});
|
|
69700
69865
|
|
|
@@ -71514,11 +71679,35 @@ async function applySnapshot(sessionId, snapshot2, source) {
|
|
|
71514
71679
|
if (projected !== "error") {
|
|
71515
71680
|
updates.errorMessage = void 0;
|
|
71516
71681
|
updates.errorKind = void 0;
|
|
71517
|
-
|
|
71682
|
+
const stashedStop = projected === "stopped" ? takeSuppressedErrorReason(sessionId) : null;
|
|
71683
|
+
if (stashedStop?.message) {
|
|
71684
|
+
updates.errorMessage = stashedStop.message;
|
|
71685
|
+
if (stashedStop.kind) updates.errorKind = stashedStop.kind;
|
|
71686
|
+
log.session.info("snapshot projection adopted suppressed stop reason", {
|
|
71687
|
+
sessionId,
|
|
71688
|
+
suppressedReason: stashedStop.reason,
|
|
71689
|
+
kind: stashedStop.kind ?? null,
|
|
71690
|
+
projected,
|
|
71691
|
+
source
|
|
71692
|
+
});
|
|
71693
|
+
} else {
|
|
71694
|
+
clearSuppressedErrorReason(sessionId);
|
|
71695
|
+
}
|
|
71518
71696
|
} else {
|
|
71697
|
+
let droppedDisproven = false;
|
|
71698
|
+
if (assertsHostUnreachable(record)) {
|
|
71699
|
+
updates.errorMessage = void 0;
|
|
71700
|
+
droppedDisproven = true;
|
|
71701
|
+
log.session.info("snapshot projection dropped disproven unreachability diagnosis", {
|
|
71702
|
+
sessionId,
|
|
71703
|
+
priorReason: record.status_reason ?? null,
|
|
71704
|
+
projected,
|
|
71705
|
+
source
|
|
71706
|
+
});
|
|
71707
|
+
}
|
|
71519
71708
|
const stashed = takeSuppressedErrorReason(sessionId);
|
|
71520
71709
|
if (stashed) {
|
|
71521
|
-
if (!record.errorMessage && stashed.message) updates.errorMessage = stashed.message;
|
|
71710
|
+
if ((!record.errorMessage || droppedDisproven) && stashed.message) updates.errorMessage = stashed.message;
|
|
71522
71711
|
if (!record.errorKind && stashed.kind) updates.errorKind = stashed.kind;
|
|
71523
71712
|
log.session.info("snapshot projection adopted suppressed error reason", {
|
|
71524
71713
|
sessionId,
|
|
@@ -71571,6 +71760,7 @@ var init_session_snapshot_apply = __esm({
|
|
|
71571
71760
|
"use strict";
|
|
71572
71761
|
init_logging();
|
|
71573
71762
|
init_session_snapshot_gate();
|
|
71763
|
+
init_session_error_kind();
|
|
71574
71764
|
init_engine_registry();
|
|
71575
71765
|
init_session_snapshot_gate();
|
|
71576
71766
|
TERMINAL = /* @__PURE__ */ new Set(["stopped", "error"]);
|
|
@@ -74185,9 +74375,15 @@ var init_acp_session = __esm({
|
|
|
74185
74375
|
}
|
|
74186
74376
|
async resolveArtifacts() {
|
|
74187
74377
|
if (this.cfg.artifacts) return this.cfg.artifacts;
|
|
74188
|
-
|
|
74378
|
+
const artifacts = resolveAcpArtifacts(this.engineId, {
|
|
74189
74379
|
configuredAdapterCmd: await this.resolveConfiguredAdapterCmd()
|
|
74190
74380
|
});
|
|
74381
|
+
if (!fsModule.existsSync(artifacts.workerCmd[1])) {
|
|
74382
|
+
throw new Error(
|
|
74383
|
+
`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.`
|
|
74384
|
+
);
|
|
74385
|
+
}
|
|
74386
|
+
return artifacts;
|
|
74191
74387
|
}
|
|
74192
74388
|
async establishProviderSession() {
|
|
74193
74389
|
await this.seedReplayCursor();
|
|
@@ -75570,6 +75766,26 @@ var init_transcript_chain = __esm({
|
|
|
75570
75766
|
}
|
|
75571
75767
|
});
|
|
75572
75768
|
|
|
75769
|
+
// src/core/sessions/side-thread-warmup.ts
|
|
75770
|
+
function isCacheWarmupText(text) {
|
|
75771
|
+
return text.startsWith(CACHE_WARMUP_TAG);
|
|
75772
|
+
}
|
|
75773
|
+
function markWarmupTurnPending(sessionId) {
|
|
75774
|
+
pendingWarmupTurns.add(sessionId);
|
|
75775
|
+
}
|
|
75776
|
+
function consumeWarmupTurn(sessionId) {
|
|
75777
|
+
return pendingWarmupTurns.delete(sessionId);
|
|
75778
|
+
}
|
|
75779
|
+
var CACHE_WARMUP_TAG, CACHE_WARMUP_MESSAGE, pendingWarmupTurns;
|
|
75780
|
+
var init_side_thread_warmup = __esm({
|
|
75781
|
+
"src/core/sessions/side-thread-warmup.ts"() {
|
|
75782
|
+
"use strict";
|
|
75783
|
+
CACHE_WARMUP_TAG = "<walnut-cache-warmup>";
|
|
75784
|
+
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>`;
|
|
75785
|
+
pendingWarmupTurns = /* @__PURE__ */ new Set();
|
|
75786
|
+
}
|
|
75787
|
+
});
|
|
75788
|
+
|
|
75573
75789
|
// src/core/history-disk-cache.ts
|
|
75574
75790
|
var history_disk_cache_exports = {};
|
|
75575
75791
|
__export(history_disk_cache_exports, {
|
|
@@ -77724,11 +77940,13 @@ __export(session_lifecycle_exports, {
|
|
|
77724
77940
|
persistSessionModeChange: () => persistSessionModeChange,
|
|
77725
77941
|
readProviderSessionHistory: () => readProviderSessionHistory,
|
|
77726
77942
|
readSessionRichHistory: () => readSessionRichHistory,
|
|
77943
|
+
recheckSession: () => recheckSession,
|
|
77727
77944
|
respondSessionPermission: () => respondSessionPermission,
|
|
77728
77945
|
restartSession: () => restartSession,
|
|
77729
77946
|
retrySession: () => retrySession,
|
|
77730
77947
|
terminateSession: () => terminateSession
|
|
77731
77948
|
});
|
|
77949
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
77732
77950
|
async function readProviderSessionHistory(sessionId, record, nativeHost, skipSubagents = true, opts) {
|
|
77733
77951
|
if (record && engineCaps(record.engine).historySource === "acp-journal") {
|
|
77734
77952
|
const { readAcpSessionHistoryState: readAcpSessionHistoryState2 } = await Promise.resolve().then(() => (init_acp_session_history(), acp_session_history_exports));
|
|
@@ -77839,6 +78057,33 @@ async function changeSessionMode(existingRecord, sessionId, mode, updates = {})
|
|
|
77839
78057
|
});
|
|
77840
78058
|
return updated;
|
|
77841
78059
|
}
|
|
78060
|
+
function normalizePinQuote(value) {
|
|
78061
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
78062
|
+
throw new SessionControlError("pinned_messages[].quote must be an object", 400);
|
|
78063
|
+
}
|
|
78064
|
+
const quote = value;
|
|
78065
|
+
const exact = quote.exact;
|
|
78066
|
+
if (typeof exact !== "string" || !exact.trim() || exact.length > MAX_PIN_QUOTE_CHARS) {
|
|
78067
|
+
throw new SessionControlError(
|
|
78068
|
+
`pinned_messages[].quote.exact must be a non-empty string (max ${MAX_PIN_QUOTE_CHARS} chars)`,
|
|
78069
|
+
400
|
|
78070
|
+
);
|
|
78071
|
+
}
|
|
78072
|
+
for (const side of ["prefix", "suffix"]) {
|
|
78073
|
+
const context = quote[side];
|
|
78074
|
+
if (context !== void 0 && context !== null && (typeof context !== "string" || context.length > MAX_PIN_QUOTE_CONTEXT_CHARS)) {
|
|
78075
|
+
throw new SessionControlError(
|
|
78076
|
+
`pinned_messages[].quote.${side} must be a string (max ${MAX_PIN_QUOTE_CONTEXT_CHARS} chars)`,
|
|
78077
|
+
400
|
|
78078
|
+
);
|
|
78079
|
+
}
|
|
78080
|
+
}
|
|
78081
|
+
return {
|
|
78082
|
+
exact,
|
|
78083
|
+
...typeof quote.prefix === "string" && quote.prefix ? { prefix: quote.prefix } : {},
|
|
78084
|
+
...typeof quote.suffix === "string" && quote.suffix ? { suffix: quote.suffix } : {}
|
|
78085
|
+
};
|
|
78086
|
+
}
|
|
77842
78087
|
function normalizePinnedMessages(value) {
|
|
77843
78088
|
if (!Array.isArray(value)) {
|
|
77844
78089
|
throw new SessionControlError("pinned_messages must be an array", 400);
|
|
@@ -77847,7 +78092,9 @@ function normalizePinnedMessages(value) {
|
|
|
77847
78092
|
throw new SessionControlError(`pinned_messages holds at most ${MAX_PINNED_MESSAGES} entries`, 400);
|
|
77848
78093
|
}
|
|
77849
78094
|
const out = [];
|
|
77850
|
-
const
|
|
78095
|
+
const seenWholeMessage = /* @__PURE__ */ new Set();
|
|
78096
|
+
const seenQuoteId = /* @__PURE__ */ new Set();
|
|
78097
|
+
const seenQuoteShape = /* @__PURE__ */ new Set();
|
|
77851
78098
|
for (const raw of value) {
|
|
77852
78099
|
if (!raw || typeof raw !== "object") {
|
|
77853
78100
|
throw new SessionControlError("each pinned_messages entry must be an object", 400);
|
|
@@ -77857,13 +78104,40 @@ function normalizePinnedMessages(value) {
|
|
|
77857
78104
|
if (typeof msgId !== "string" || !msgId.trim() || msgId.length > MAX_PIN_ID_CHARS) {
|
|
77858
78105
|
throw new SessionControlError("pinned_messages[].msgId must be a non-empty string", 400);
|
|
77859
78106
|
}
|
|
77860
|
-
|
|
77861
|
-
|
|
78107
|
+
const rawId = entry.id;
|
|
78108
|
+
if (rawId !== void 0 && rawId !== null && (typeof rawId !== "string" || !rawId.trim() || rawId.length > MAX_PIN_ENTRY_ID_CHARS)) {
|
|
78109
|
+
throw new SessionControlError(
|
|
78110
|
+
`pinned_messages[].id must be a non-empty string (max ${MAX_PIN_ENTRY_ID_CHARS} chars)`,
|
|
78111
|
+
400
|
|
78112
|
+
);
|
|
78113
|
+
}
|
|
78114
|
+
const id = typeof rawId === "string" ? rawId : void 0;
|
|
78115
|
+
const quote = entry.quote === void 0 || entry.quote === null ? void 0 : normalizePinQuote(entry.quote);
|
|
78116
|
+
let pinId = id;
|
|
78117
|
+
if (quote) {
|
|
78118
|
+
if (id && seenQuoteId.has(id)) continue;
|
|
78119
|
+
const shape = [msgId, quote.exact, quote.prefix ?? "", quote.suffix ?? ""].join("\0");
|
|
78120
|
+
if (seenQuoteShape.has(shape)) continue;
|
|
78121
|
+
pinId = id ?? randomUUID4();
|
|
78122
|
+
seenQuoteId.add(pinId);
|
|
78123
|
+
seenQuoteShape.add(shape);
|
|
78124
|
+
} else {
|
|
78125
|
+
if (seenWholeMessage.has(msgId)) continue;
|
|
78126
|
+
seenWholeMessage.add(msgId);
|
|
78127
|
+
}
|
|
77862
78128
|
const role = entry.role === "user" || entry.role === "assistant" || entry.role === "system" ? entry.role : "assistant";
|
|
77863
78129
|
const label = typeof entry.label === "string" ? entry.label.trim().slice(0, MAX_PIN_LABEL_CHARS) : "";
|
|
77864
78130
|
const timestamp = typeof entry.timestamp === "string" ? entry.timestamp : void 0;
|
|
77865
78131
|
const pinnedAt = typeof entry.pinnedAt === "string" ? entry.pinnedAt : (/* @__PURE__ */ new Date()).toISOString();
|
|
77866
|
-
out.push({
|
|
78132
|
+
out.push({
|
|
78133
|
+
msgId,
|
|
78134
|
+
label,
|
|
78135
|
+
role,
|
|
78136
|
+
...timestamp ? { timestamp } : {},
|
|
78137
|
+
pinnedAt,
|
|
78138
|
+
...pinId ? { id: pinId } : {},
|
|
78139
|
+
...quote ? { quote } : {}
|
|
78140
|
+
});
|
|
77867
78141
|
}
|
|
77868
78142
|
return out;
|
|
77869
78143
|
}
|
|
@@ -78061,7 +78335,7 @@ async function retrySession(sessionId) {
|
|
|
78061
78335
|
return { status: "reconnected", sessionId };
|
|
78062
78336
|
}
|
|
78063
78337
|
if (await resumeConversationExists(record)) {
|
|
78064
|
-
const {
|
|
78338
|
+
const { getQueue: getQueue2, unparkMessage: unparkMessage2 } = await Promise.resolve().then(() => (init_session_message_queue(), session_message_queue_exports));
|
|
78065
78339
|
const pendingMsgs = await getQueue2(sessionId);
|
|
78066
78340
|
for (const parked of pendingMsgs.filter((m6) => m6.status === "parked")) {
|
|
78067
78341
|
await unparkMessage2(sessionId, parked.id);
|
|
@@ -78071,9 +78345,22 @@ async function retrySession(sessionId) {
|
|
|
78071
78345
|
log.session.info("session retry: re-processing pending queue messages", { sessionId, taskId: record.taskId, count: pendingMsgs.length });
|
|
78072
78346
|
return { status: "resuming", sessionId, restoredMessages: pendingMsgs.length };
|
|
78073
78347
|
}
|
|
78074
|
-
await
|
|
78075
|
-
|
|
78076
|
-
|
|
78348
|
+
const relabelled = await updateSessionRecord2(sessionId, {
|
|
78349
|
+
process_status: "stopped",
|
|
78350
|
+
errorMessage: void 0,
|
|
78351
|
+
errorKind: void 0,
|
|
78352
|
+
activity: void 0,
|
|
78353
|
+
last_status_change: (/* @__PURE__ */ new Date()).toISOString(),
|
|
78354
|
+
status_reason: "retry_reconnect",
|
|
78355
|
+
status_changed_by: "user"
|
|
78356
|
+
});
|
|
78357
|
+
emitSessionStatusChanged2(relabelled, {}, ["*"]);
|
|
78358
|
+
log.session.info("session retry: reconnected \u2014 conversation preserved, no message sent", {
|
|
78359
|
+
sessionId,
|
|
78360
|
+
taskId: record.taskId,
|
|
78361
|
+
host: record.host ?? "__local__"
|
|
78362
|
+
});
|
|
78363
|
+
return { status: "resumable", sessionId };
|
|
78077
78364
|
}
|
|
78078
78365
|
log.session.warn("session retry: conversation never persisted \u2014 starting fresh instead of --resume", {
|
|
78079
78366
|
sessionId,
|
|
@@ -78131,6 +78418,91 @@ async function retrySession(sessionId) {
|
|
|
78131
78418
|
});
|
|
78132
78419
|
return { status: "pending", taskId: task.id, oldSessionId: sessionId };
|
|
78133
78420
|
}
|
|
78421
|
+
async function withDeadline(p6, ms) {
|
|
78422
|
+
let timer3;
|
|
78423
|
+
try {
|
|
78424
|
+
return await Promise.race([
|
|
78425
|
+
p6,
|
|
78426
|
+
new Promise((resolve) => {
|
|
78427
|
+
timer3 = setTimeout(() => resolve(RECHECK_TIMED_OUT), ms);
|
|
78428
|
+
if (timer3 && typeof timer3 === "object" && "unref" in timer3) timer3.unref();
|
|
78429
|
+
})
|
|
78430
|
+
]);
|
|
78431
|
+
} finally {
|
|
78432
|
+
if (timer3) clearTimeout(timer3);
|
|
78433
|
+
}
|
|
78434
|
+
}
|
|
78435
|
+
async function recheckSession(sessionId) {
|
|
78436
|
+
const { getSessionByClaudeId: getSessionByClaudeId2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
|
|
78437
|
+
const record = await getSessionByClaudeId2(sessionId);
|
|
78438
|
+
if (!record) throw new SessionControlError("Session not found", 404);
|
|
78439
|
+
const hostKey2 = record.host ?? "__local__";
|
|
78440
|
+
const { classifySessionError: classifySessionError2 } = await Promise.resolve().then(() => (init_session_error_kind(), session_error_kind_exports));
|
|
78441
|
+
const kind = classifySessionError2(record);
|
|
78442
|
+
const base = { sessionId, processStatus: record.process_status, infraClaim: kind === "infra" };
|
|
78443
|
+
const { isDaemonConnected: isDaemonConnected2, getPooledSnapshotConnection: getPooledSnapshotConnection2 } = await Promise.resolve().then(() => (init_daemon_connection(), daemon_connection_exports));
|
|
78444
|
+
const reachable = isDaemonConnected2(hostKey2);
|
|
78445
|
+
if (kind === "terminal") return { ...base, checked: false, reachable, reason: "terminal_error" };
|
|
78446
|
+
if (record.archived) return { ...base, checked: false, reachable, reason: "archived" };
|
|
78447
|
+
const conn = getPooledSnapshotConnection2(record.host);
|
|
78448
|
+
if (!conn) {
|
|
78449
|
+
log.session.info("session recheck: no pooled snapshot connection \u2014 nothing to ask", {
|
|
78450
|
+
sessionId,
|
|
78451
|
+
host: hostKey2,
|
|
78452
|
+
reachable
|
|
78453
|
+
});
|
|
78454
|
+
return { ...base, checked: false, reachable, reason: "no_pooled_connection" };
|
|
78455
|
+
}
|
|
78456
|
+
let raced;
|
|
78457
|
+
try {
|
|
78458
|
+
raced = await withDeadline(
|
|
78459
|
+
conn.send("getState", { sid: sessionId }, RECHECK_RPC_TIMEOUT_MS),
|
|
78460
|
+
RECHECK_RPC_TIMEOUT_MS
|
|
78461
|
+
);
|
|
78462
|
+
} catch (err2) {
|
|
78463
|
+
log.session.info("session recheck: getState failed", {
|
|
78464
|
+
sessionId,
|
|
78465
|
+
host: hostKey2,
|
|
78466
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
78467
|
+
});
|
|
78468
|
+
return { ...base, checked: false, reachable, reason: "rpc_failed" };
|
|
78469
|
+
}
|
|
78470
|
+
if (raced === RECHECK_TIMED_OUT) {
|
|
78471
|
+
log.session.warn("session recheck: getState timed out", {
|
|
78472
|
+
sessionId,
|
|
78473
|
+
host: hostKey2,
|
|
78474
|
+
timeoutMs: RECHECK_RPC_TIMEOUT_MS
|
|
78475
|
+
});
|
|
78476
|
+
return { ...base, checked: false, reachable, reason: "timeout" };
|
|
78477
|
+
}
|
|
78478
|
+
const snapshot2 = raced?.ok ? raced.snapshot : void 0;
|
|
78479
|
+
if (!snapshot2) {
|
|
78480
|
+
return { ...base, checked: false, reachable: true, reason: "no_snapshot" };
|
|
78481
|
+
}
|
|
78482
|
+
const { applySnapshot: applySnapshot2 } = await Promise.resolve().then(() => (init_session_snapshot_apply(), session_snapshot_apply_exports));
|
|
78483
|
+
const outcome = await applySnapshot2(sessionId, snapshot2, "recheck-open");
|
|
78484
|
+
const after = await getSessionByClaudeId2(sessionId);
|
|
78485
|
+
const processStatus = after?.process_status ?? record.process_status;
|
|
78486
|
+
log.session.info("session recheck: reconciled against host", {
|
|
78487
|
+
sessionId,
|
|
78488
|
+
host: hostKey2,
|
|
78489
|
+
previous: record.process_status,
|
|
78490
|
+
processStatus,
|
|
78491
|
+
cliState: snapshot2.cliState,
|
|
78492
|
+
applyOutcome: outcome.outcome
|
|
78493
|
+
});
|
|
78494
|
+
return {
|
|
78495
|
+
sessionId,
|
|
78496
|
+
checked: true,
|
|
78497
|
+
reachable: true,
|
|
78498
|
+
alive: snapshot2.cliState !== "dead",
|
|
78499
|
+
processStatus,
|
|
78500
|
+
// Re-classify against the CURRENT record: a converged row may no longer
|
|
78501
|
+
// carry the claim, and the client uses this to decide whether the sentence
|
|
78502
|
+
// on screen is still true.
|
|
78503
|
+
infraClaim: classifySessionError2(after ?? record) === "infra"
|
|
78504
|
+
};
|
|
78505
|
+
}
|
|
78134
78506
|
async function respondSessionPermission(sessionId, requestId, allow, denyMessage, optionId, answers) {
|
|
78135
78507
|
if (typeof requestId !== "string" || !requestId || typeof allow !== "boolean") {
|
|
78136
78508
|
throw new SessionControlError("requestId (string) and allow (boolean) are required", 400);
|
|
@@ -78424,7 +78796,7 @@ async function readSessionRichHistory(sessionId, tail) {
|
|
|
78424
78796
|
...historyWindowed ? { windowed: true } : {}
|
|
78425
78797
|
};
|
|
78426
78798
|
}
|
|
78427
|
-
var CLAUDE_SESSION_MODES, MAX_PINNED_MESSAGES, MAX_PIN_LABEL_CHARS, MAX_PIN_ID_CHARS, GIT_BASES, MAX_FORK_DEPTH, HISTORY_STARTUP_GRACE_MS;
|
|
78799
|
+
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;
|
|
78428
78800
|
var init_session_lifecycle = __esm({
|
|
78429
78801
|
"src/core/sessions/session-lifecycle.ts"() {
|
|
78430
78802
|
"use strict";
|
|
@@ -78438,6 +78810,11 @@ var init_session_lifecycle = __esm({
|
|
|
78438
78810
|
MAX_PINNED_MESSAGES = 200;
|
|
78439
78811
|
MAX_PIN_LABEL_CHARS = 300;
|
|
78440
78812
|
MAX_PIN_ID_CHARS = 200;
|
|
78813
|
+
MAX_PIN_ENTRY_ID_CHARS = 64;
|
|
78814
|
+
MAX_PIN_QUOTE_CHARS = 2e3;
|
|
78815
|
+
MAX_PIN_QUOTE_CONTEXT_CHARS = 64;
|
|
78816
|
+
RECHECK_RPC_TIMEOUT_MS = 5e3;
|
|
78817
|
+
RECHECK_TIMED_OUT = /* @__PURE__ */ Symbol("recheck-timeout");
|
|
78441
78818
|
GIT_BASES = /* @__PURE__ */ new Set(["uncommitted", "previous", "remote"]);
|
|
78442
78819
|
MAX_FORK_DEPTH = 5;
|
|
78443
78820
|
HISTORY_STARTUP_GRACE_MS = 2 * 60 * 1e3;
|
|
@@ -78738,7 +79115,7 @@ __export(compact_inject_exports, {
|
|
|
78738
79115
|
extractSessionMeta: () => extractSessionMeta,
|
|
78739
79116
|
injectCompactBoundary: () => injectCompactBoundary
|
|
78740
79117
|
});
|
|
78741
|
-
import { randomUUID as
|
|
79118
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
78742
79119
|
import fsp26 from "fs/promises";
|
|
78743
79120
|
async function extractSessionMeta(jsonlPath) {
|
|
78744
79121
|
const content = await fsp26.readFile(jsonlPath, "utf-8");
|
|
@@ -78790,8 +79167,8 @@ async function injectCompactBoundary(jsonlPath, summary, meta) {
|
|
|
78790
79167
|
log.session.error("Could not find last message UUID in JSONL", { jsonlPath });
|
|
78791
79168
|
return null;
|
|
78792
79169
|
}
|
|
78793
|
-
const boundaryUuid =
|
|
78794
|
-
const summaryUuid =
|
|
79170
|
+
const boundaryUuid = randomUUID5();
|
|
79171
|
+
const summaryUuid = randomUUID5();
|
|
78795
79172
|
const now = /* @__PURE__ */ new Date();
|
|
78796
79173
|
const boundaryTs = now.toISOString();
|
|
78797
79174
|
const summaryTs = new Date(now.getTime() + 1e3).toISOString();
|
|
@@ -87820,7 +88197,7 @@ function isRevealSecretPath(resolved) {
|
|
|
87820
88197
|
];
|
|
87821
88198
|
return denied.some((d8) => resolved === d8 || resolved.startsWith(d8 + path70.sep)) || /(^|\/)config\.ya?ml$/.test(resolved);
|
|
87822
88199
|
}
|
|
87823
|
-
var filesRouter, MAX_ENTRIES2, REMOTE_TIMEOUT_MS3, REMOTE_RESOLVE_BUDGET_MS, FilesOpError, MAX_UPWARD_LEVELS2, MAX_DOWNWARD_DEPTH, REFERENCE_SEARCH_BUDGET_MS, SYMBOL_RE2, REVEAL_APP_DENY_EXTS;
|
|
88200
|
+
var filesRouter, MAX_ENTRIES2, REMOTE_TIMEOUT_MS3, REMOTE_RESOLVE_BUDGET_MS, FilesOpError, MAX_UPWARD_LEVELS2, MAX_DOWNWARD_DEPTH, REFERENCE_SEARCH_BUDGET_MS, SYMBOL_RE2, LIST_MANY_MAX_PATHS, LIST_MANY_CONCURRENCY, LIST_MANY_DEADLINE_MS, REVEAL_APP_DENY_EXTS;
|
|
87824
88201
|
var init_files2 = __esm({
|
|
87825
88202
|
"src/web/routes/files.ts"() {
|
|
87826
88203
|
"use strict";
|
|
@@ -87953,6 +88330,54 @@ var init_files2 = __esm({
|
|
|
87953
88330
|
next(err2);
|
|
87954
88331
|
}
|
|
87955
88332
|
});
|
|
88333
|
+
LIST_MANY_MAX_PATHS = 64;
|
|
88334
|
+
LIST_MANY_CONCURRENCY = 8;
|
|
88335
|
+
LIST_MANY_DEADLINE_MS = 8e3;
|
|
88336
|
+
filesRouter.post("/list-many", async (req, res, next) => {
|
|
88337
|
+
try {
|
|
88338
|
+
const body3 = req.body ?? {};
|
|
88339
|
+
const raw = Array.isArray(body3.paths) ? body3.paths : null;
|
|
88340
|
+
if (!raw) {
|
|
88341
|
+
res.status(400).json({ error: "paths (string[]) is required" });
|
|
88342
|
+
return;
|
|
88343
|
+
}
|
|
88344
|
+
const host = typeof body3.host === "string" && body3.host ? body3.host : void 0;
|
|
88345
|
+
const showHidden = body3.showHidden === true || body3.showHidden === "1";
|
|
88346
|
+
const paths = [...new Set(
|
|
88347
|
+
raw.filter((p6) => typeof p6 === "string" && !!p6)
|
|
88348
|
+
)];
|
|
88349
|
+
const truncated = paths.length > LIST_MANY_MAX_PATHS;
|
|
88350
|
+
const wanted = paths.slice(0, LIST_MANY_MAX_PATHS);
|
|
88351
|
+
const deadline4 = Date.now() + LIST_MANY_DEADLINE_MS;
|
|
88352
|
+
const listings = [];
|
|
88353
|
+
let timedOut = false;
|
|
88354
|
+
let next_ = 0;
|
|
88355
|
+
const worker = async () => {
|
|
88356
|
+
for (; ; ) {
|
|
88357
|
+
const i8 = next_++;
|
|
88358
|
+
if (i8 >= wanted.length) return;
|
|
88359
|
+
if (Date.now() >= deadline4) {
|
|
88360
|
+
timedOut = true;
|
|
88361
|
+
return;
|
|
88362
|
+
}
|
|
88363
|
+
const dirPath = wanted[i8];
|
|
88364
|
+
try {
|
|
88365
|
+
listings.push(await listSessionFiles(dirPath, host, showHidden));
|
|
88366
|
+
} catch (err2) {
|
|
88367
|
+
listings.push({ path: dirPath, error: err2 instanceof Error ? err2.message : String(err2) });
|
|
88368
|
+
}
|
|
88369
|
+
}
|
|
88370
|
+
};
|
|
88371
|
+
await Promise.all(Array.from({ length: Math.min(LIST_MANY_CONCURRENCY, wanted.length) }, worker));
|
|
88372
|
+
res.json({ listings, truncated, timedOut });
|
|
88373
|
+
} catch (err2) {
|
|
88374
|
+
if (err2 instanceof FilesOpError) {
|
|
88375
|
+
res.status(err2.statusCode).json({ error: err2.message });
|
|
88376
|
+
return;
|
|
88377
|
+
}
|
|
88378
|
+
next(err2);
|
|
88379
|
+
}
|
|
88380
|
+
});
|
|
87956
88381
|
REVEAL_APP_DENY_EXTS = /* @__PURE__ */ new Set([
|
|
87957
88382
|
"command",
|
|
87958
88383
|
"sh",
|
|
@@ -89527,6 +89952,82 @@ var init_task = __esm({
|
|
|
89527
89952
|
}
|
|
89528
89953
|
});
|
|
89529
89954
|
|
|
89955
|
+
// src/core/sessions/spawn-prefix.ts
|
|
89956
|
+
var spawn_prefix_exports = {};
|
|
89957
|
+
__export(spawn_prefix_exports, {
|
|
89958
|
+
MAX_SPAWN_PROMPT_BYTES: () => MAX_SPAWN_PROMPT_BYTES,
|
|
89959
|
+
parseSpawnPrefixFromArgs: () => parseSpawnPrefixFromArgs,
|
|
89960
|
+
readParentSpawnPrefix: () => readParentSpawnPrefix,
|
|
89961
|
+
spawnPrefixFromRecord: () => spawnPrefixFromRecord
|
|
89962
|
+
});
|
|
89963
|
+
function flagValue(args, flag) {
|
|
89964
|
+
const i8 = args.indexOf(flag);
|
|
89965
|
+
if (i8 < 0 || i8 + 1 >= args.length) return void 0;
|
|
89966
|
+
const v3 = args[i8 + 1];
|
|
89967
|
+
return v3 !== void 0 && !v3.startsWith("--") ? v3 : void 0;
|
|
89968
|
+
}
|
|
89969
|
+
function parseSpawnPrefixFromArgs(args) {
|
|
89970
|
+
const prompt = flagValue(args, "--append-system-prompt");
|
|
89971
|
+
return {
|
|
89972
|
+
appendSystemPrompt: prompt !== void 0 && Buffer.byteLength(prompt, "utf8") <= MAX_SPAWN_PROMPT_BYTES ? prompt : null,
|
|
89973
|
+
...flagValue(args, "--model") ? { model: flagValue(args, "--model") } : {},
|
|
89974
|
+
...EFFORTS.includes(flagValue(args, "--effort") ?? "") ? { effort: flagValue(args, "--effort") } : {},
|
|
89975
|
+
...flagValue(args, "--permission-mode") ? { permissionMode: flagValue(args, "--permission-mode") } : {},
|
|
89976
|
+
source: "live-process"
|
|
89977
|
+
};
|
|
89978
|
+
}
|
|
89979
|
+
function spawnPrefixFromRecord(record) {
|
|
89980
|
+
const stored = record.appliedAppendSystemPrompt;
|
|
89981
|
+
if (stored === void 0 || stored === null) return { appendSystemPrompt: null, source: "unknown" };
|
|
89982
|
+
const usable = stored.length > 0 && Buffer.byteLength(stored, "utf8") <= MAX_SPAWN_PROMPT_BYTES;
|
|
89983
|
+
return { appendSystemPrompt: usable ? stored : null, source: "record" };
|
|
89984
|
+
}
|
|
89985
|
+
async function defaultLiveArgsReader(hostKey2, sessionId) {
|
|
89986
|
+
const { probeDaemonSessionArgs: probeDaemonSessionArgs2 } = await Promise.resolve().then(() => (init_daemon_connection(), daemon_connection_exports));
|
|
89987
|
+
return probeDaemonSessionArgs2(hostKey2, sessionId);
|
|
89988
|
+
}
|
|
89989
|
+
async function readParentSpawnPrefix(parent, deps = {}) {
|
|
89990
|
+
const readLiveArgs = deps.readLiveArgs ?? defaultLiveArgsReader;
|
|
89991
|
+
const hostKey2 = parent.host || "__local__";
|
|
89992
|
+
let live = null;
|
|
89993
|
+
try {
|
|
89994
|
+
live = await readLiveArgs(hostKey2, parent.claudeSessionId);
|
|
89995
|
+
} catch (err2) {
|
|
89996
|
+
log.session.debug("spawn prefix: live argv read threw", {
|
|
89997
|
+
sessionId: parent.claudeSessionId,
|
|
89998
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
89999
|
+
});
|
|
90000
|
+
}
|
|
90001
|
+
if (live && live.length > 0) {
|
|
90002
|
+
const prefix = parseSpawnPrefixFromArgs(live);
|
|
90003
|
+
const remembered = parent.appliedAppendSystemPrompt ?? void 0;
|
|
90004
|
+
const actual = prefix.appendSystemPrompt ?? "";
|
|
90005
|
+
if (remembered !== actual) {
|
|
90006
|
+
const persist4 = deps.persist ?? (async (sid, prompt) => {
|
|
90007
|
+
const { updateSessionRecord: updateSessionRecord2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
|
|
90008
|
+
return updateSessionRecord2(sid, { appliedAppendSystemPrompt: prompt });
|
|
90009
|
+
});
|
|
90010
|
+
void persist4(parent.claudeSessionId, actual).catch((err2) => {
|
|
90011
|
+
log.session.warn("spawn prefix: backfill of applied prompt failed", {
|
|
90012
|
+
sessionId: parent.claudeSessionId,
|
|
90013
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
90014
|
+
});
|
|
90015
|
+
});
|
|
90016
|
+
}
|
|
90017
|
+
return prefix;
|
|
90018
|
+
}
|
|
90019
|
+
return spawnPrefixFromRecord(parent);
|
|
90020
|
+
}
|
|
90021
|
+
var EFFORTS, MAX_SPAWN_PROMPT_BYTES;
|
|
90022
|
+
var init_spawn_prefix = __esm({
|
|
90023
|
+
"src/core/sessions/spawn-prefix.ts"() {
|
|
90024
|
+
"use strict";
|
|
90025
|
+
init_logging();
|
|
90026
|
+
EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
90027
|
+
MAX_SPAWN_PROMPT_BYTES = 65536;
|
|
90028
|
+
}
|
|
90029
|
+
});
|
|
90030
|
+
|
|
89530
90031
|
// src/core/sessions/side-thread-fork.ts
|
|
89531
90032
|
var side_thread_fork_exports = {};
|
|
89532
90033
|
__export(side_thread_fork_exports, {
|
|
@@ -89583,6 +90084,10 @@ async function forkSideThreadSession(parentSid, threadId, opts) {
|
|
|
89583
90084
|
const sessionId = crypto18.randomUUID();
|
|
89584
90085
|
const lane = sideThreadLaneKey(parentSid, threadId);
|
|
89585
90086
|
const mode = parent.mode !== "default" ? parent.mode : void 0;
|
|
90087
|
+
const { readParentSpawnPrefix: readParentSpawnPrefix2 } = await Promise.resolve().then(() => (init_spawn_prefix(), spawn_prefix_exports));
|
|
90088
|
+
const prefix = await readParentSpawnPrefix2(parent);
|
|
90089
|
+
const cliModel = prefix.model ?? parent.cliModel;
|
|
90090
|
+
const effort = prefix.effort ?? parent.effort;
|
|
89586
90091
|
await createSessionRecord(sessionId, "", parent.project ?? "", parent.cwd, {
|
|
89587
90092
|
title,
|
|
89588
90093
|
...mode ? { mode } : {},
|
|
@@ -89590,8 +90095,10 @@ async function forkSideThreadSession(parentSid, threadId, opts) {
|
|
|
89590
90095
|
// The thread inherits the parent's launch bundle verbatim, re-applied from
|
|
89591
90096
|
// the record on every cold resume.
|
|
89592
90097
|
...parent.profile ? { profile: parent.profile } : {},
|
|
89593
|
-
...
|
|
89594
|
-
...
|
|
90098
|
+
...effort ? { effort } : {},
|
|
90099
|
+
...cliModel ? { cliModel } : {},
|
|
90100
|
+
// '' = the parent runs without an append prompt, and so does the thread.
|
|
90101
|
+
appliedAppendSystemPrompt: prefix.appendSystemPrompt ?? "",
|
|
89595
90102
|
lane,
|
|
89596
90103
|
forkedFromSessionId: resumeFrom,
|
|
89597
90104
|
initialProcessStatus: "idle",
|
|
@@ -89607,11 +90114,12 @@ async function forkSideThreadSession(parentSid, threadId, opts) {
|
|
|
89607
90114
|
...mode ? { mode } : {},
|
|
89608
90115
|
...parent.host ? { host: parent.host } : {},
|
|
89609
90116
|
...parent.profile ? { profile: parent.profile } : {},
|
|
89610
|
-
...
|
|
90117
|
+
...effort ? { effort } : {},
|
|
89611
90118
|
// Must be `cliModel` (the parent's verbatim --model arg, [1m] marker
|
|
89612
90119
|
// included): the CLI does NOT inherit a model across --fork-session, and
|
|
89613
90120
|
// the reported `model` has lost the marker.
|
|
89614
|
-
...
|
|
90121
|
+
...cliModel ? { model: cliModel } : {},
|
|
90122
|
+
appendSystemPromptExact: prefix.appendSystemPrompt,
|
|
89615
90123
|
lane,
|
|
89616
90124
|
forkedFromSessionId: resumeFrom
|
|
89617
90125
|
}, ["session-runner"], { source: "side-thread-fork" });
|
|
@@ -89620,7 +90128,9 @@ async function forkSideThreadSession(parentSid, threadId, opts) {
|
|
|
89620
90128
|
threadId,
|
|
89621
90129
|
sessionId,
|
|
89622
90130
|
resumeFrom,
|
|
89623
|
-
initOnly: !opts?.message
|
|
90131
|
+
initOnly: !opts?.message,
|
|
90132
|
+
prefixSource: prefix.source,
|
|
90133
|
+
prefixPromptLength: prefix.appendSystemPrompt?.length ?? 0
|
|
89624
90134
|
});
|
|
89625
90135
|
return { sessionId, resumeFromSessionId: resumeFrom };
|
|
89626
90136
|
}
|
|
@@ -115492,11 +116002,12 @@ __export(session_controls_exports, {
|
|
|
115492
116002
|
applySessionModelChange: () => applySessionModelChange,
|
|
115493
116003
|
computeModelOptions: () => computeModelOptions,
|
|
115494
116004
|
controlErrorKind: () => controlErrorKind,
|
|
116005
|
+
createForkSiblingTask: () => createForkSiblingTask,
|
|
115495
116006
|
forkSessionToTask: () => forkSessionToTask,
|
|
115496
116007
|
handleSessionControlRelay: () => handleSessionControlRelay,
|
|
115497
116008
|
modelReadBackMatches: () => modelReadBackMatches
|
|
115498
116009
|
});
|
|
115499
|
-
import { randomUUID as
|
|
116010
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
115500
116011
|
function controlErrorKind(status2) {
|
|
115501
116012
|
if (status2 === 404) return "not_found";
|
|
115502
116013
|
if (status2 === 405) return "method_not_allowed";
|
|
@@ -115669,6 +116180,124 @@ async function computeModelOptions(sessionId) {
|
|
|
115669
116180
|
currentEffort: liveEffort ?? record.effectiveEffort ?? record.effort ?? null
|
|
115670
116181
|
};
|
|
115671
116182
|
}
|
|
116183
|
+
async function createForkSiblingTask(sourceTaskId, opts) {
|
|
116184
|
+
const {
|
|
116185
|
+
getTask: getTask2,
|
|
116186
|
+
addTask: addTask2,
|
|
116187
|
+
updateTask: updateTask2,
|
|
116188
|
+
groupTasks: groupTasks2,
|
|
116189
|
+
addToGroup: addToGroup2,
|
|
116190
|
+
renameGroup: renameGroup2
|
|
116191
|
+
} = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
|
|
116192
|
+
let sourceTask;
|
|
116193
|
+
try {
|
|
116194
|
+
sourceTask = await getTask2(sourceTaskId);
|
|
116195
|
+
} catch {
|
|
116196
|
+
throw new SessionControlError(`Source task "${sourceTaskId}" not found`, 404);
|
|
116197
|
+
}
|
|
116198
|
+
const sourceBaseTitle = sourceTask.title.replace(/^Fork of\s+/i, "").replace(/\s+-\s+fork of\s+.*$/i, "").trim() || sourceTask.title;
|
|
116199
|
+
const explicitTitle = opts.childTitle || (opts.titlePrefix ? `${opts.titlePrefix} - fork of ${sourceBaseTitle}` : void 0);
|
|
116200
|
+
const autoTitle = !explicitTitle;
|
|
116201
|
+
const newTitle = explicitTitle || `Fork of ${sourceBaseTitle}`;
|
|
116202
|
+
let inheritedTier;
|
|
116203
|
+
if (sourceTask.pinned && sourceTask.focus_tier) {
|
|
116204
|
+
const { getCustomTiers: getCustomTiers2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
|
|
116205
|
+
const known = ["focus", "backlog", "wait"].includes(sourceTask.focus_tier) || (await getCustomTiers2()).some((t3) => t3.id === sourceTask.focus_tier);
|
|
116206
|
+
if (known) inheritedTier = sourceTask.focus_tier;
|
|
116207
|
+
else {
|
|
116208
|
+
log.session.warn("fork: source tier is not registered, filing the fork in Satellite", {
|
|
116209
|
+
sourceTaskId: sourceTask.id,
|
|
116210
|
+
tier: sourceTask.focus_tier
|
|
116211
|
+
});
|
|
116212
|
+
}
|
|
116213
|
+
}
|
|
116214
|
+
const { task: newFork } = await addTask2({
|
|
116215
|
+
title: newTitle,
|
|
116216
|
+
project: sourceTask.project || "",
|
|
116217
|
+
source: sourceTask.source,
|
|
116218
|
+
...opts.description ? { description: opts.description } : {},
|
|
116219
|
+
// A fork is a person splitting off live work, so it joins the board like
|
|
116220
|
+
// any other hand-made task (Satellite = pinned, no stored tier).
|
|
116221
|
+
pinned: true,
|
|
116222
|
+
...inheritedTier ? { focus_tier: inheritedTier } : {},
|
|
116223
|
+
// Forking an Ask Walnut conversation yields another Ask Walnut
|
|
116224
|
+
// conversation: without the flag the fork loses the amber marker AND the
|
|
116225
|
+
// persona drift repair (both key on task.walnut_agent, never on project).
|
|
116226
|
+
...sourceTask.walnut_agent ? { walnut_agent: true } : {}
|
|
116227
|
+
});
|
|
116228
|
+
let forkGroupId;
|
|
116229
|
+
try {
|
|
116230
|
+
if (sourceTask.group_id) {
|
|
116231
|
+
const r8 = await addToGroup2(sourceTask.group_id, [newFork.id]);
|
|
116232
|
+
forkGroupId = r8.group_id;
|
|
116233
|
+
} else {
|
|
116234
|
+
const r8 = await groupTasks2([sourceTask.id, newFork.id], sourceTask.title);
|
|
116235
|
+
forkGroupId = r8.group_id;
|
|
116236
|
+
}
|
|
116237
|
+
} catch (err2) {
|
|
116238
|
+
log.session.warn("fork: failed to group source + fork", {
|
|
116239
|
+
sourceTaskId: sourceTask.id,
|
|
116240
|
+
forkTaskId: newFork.id,
|
|
116241
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
116242
|
+
});
|
|
116243
|
+
}
|
|
116244
|
+
let task;
|
|
116245
|
+
try {
|
|
116246
|
+
task = await getTask2(newFork.id);
|
|
116247
|
+
} catch {
|
|
116248
|
+
task = newFork;
|
|
116249
|
+
}
|
|
116250
|
+
bus.emit(EventNames.TASK_CREATED, { task }, ["web-ui", "main-agent"], { source: opts.source });
|
|
116251
|
+
if (autoTitle && opts.prompt?.trim()) {
|
|
116252
|
+
const forkId = newFork.id;
|
|
116253
|
+
const sourceTitle = sourceBaseTitle;
|
|
116254
|
+
const placeholderTitle = newTitle;
|
|
116255
|
+
const prompt = opts.prompt;
|
|
116256
|
+
const titleSource = `${opts.source}-title`;
|
|
116257
|
+
void (async () => {
|
|
116258
|
+
try {
|
|
116259
|
+
const { summarizeForkPrompt: summarizeForkPrompt2 } = await Promise.resolve().then(() => (init_fork_title(), fork_title_exports));
|
|
116260
|
+
const label = await summarizeForkPrompt2(prompt);
|
|
116261
|
+
if (!label) return;
|
|
116262
|
+
const current5 = await getTask2(forkId);
|
|
116263
|
+
if (current5.title !== placeholderTitle) {
|
|
116264
|
+
log.session.info("fork title refine skipped \u2014 title changed since fork", { taskId: forkId });
|
|
116265
|
+
return;
|
|
116266
|
+
}
|
|
116267
|
+
const refinedTitle = `${label} - fork of ${sourceTitle}`;
|
|
116268
|
+
const { task: updated } = await updateTask2(forkId, { title: refinedTitle }, { source: titleSource });
|
|
116269
|
+
bus.emit(EventNames.TASK_UPDATED, { task: updated }, ["web-ui", "main-agent"], { source: titleSource });
|
|
116270
|
+
log.session.info("fork title refined", { taskId: forkId, title: refinedTitle });
|
|
116271
|
+
} catch (err2) {
|
|
116272
|
+
log.session.warn("fork title refine failed", {
|
|
116273
|
+
taskId: forkId,
|
|
116274
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
116275
|
+
});
|
|
116276
|
+
}
|
|
116277
|
+
})();
|
|
116278
|
+
}
|
|
116279
|
+
if (forkGroupId && !sourceTask.group_id) {
|
|
116280
|
+
const gid = forkGroupId;
|
|
116281
|
+
const seedTitles = [sourceTask.title, newTitle];
|
|
116282
|
+
const groupSource = opts.source;
|
|
116283
|
+
void (async () => {
|
|
116284
|
+
try {
|
|
116285
|
+
const { summarizeGroupLabel: summarizeGroupLabel2 } = await Promise.resolve().then(() => (init_fork_title(), fork_title_exports));
|
|
116286
|
+
const label = await summarizeGroupLabel2(seedTitles);
|
|
116287
|
+
if (!label) return;
|
|
116288
|
+
await renameGroup2(gid, label);
|
|
116289
|
+
bus.emit(EventNames.TASK_GROUPS_CHANGED, { group_id: gid, label }, ["web-ui", "main-agent"], { source: groupSource });
|
|
116290
|
+
log.session.info("fork group label refined", { groupId: gid, label });
|
|
116291
|
+
} catch (err2) {
|
|
116292
|
+
log.session.warn("fork group label refine failed", {
|
|
116293
|
+
groupId: gid,
|
|
116294
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
116295
|
+
});
|
|
116296
|
+
}
|
|
116297
|
+
})();
|
|
116298
|
+
}
|
|
116299
|
+
return { task, ...forkGroupId ? { groupId: forkGroupId } : {} };
|
|
116300
|
+
}
|
|
115672
116301
|
async function forkSessionToTask(sourceSessionId, input, source = "web-api") {
|
|
115673
116302
|
const { task_id, create_child_task, child_title, message, title, model, imageContext } = input;
|
|
115674
116303
|
if (!task_id && !create_child_task) {
|
|
@@ -115678,14 +116307,7 @@ async function forkSessionToTask(sourceSessionId, input, source = "web-api") {
|
|
|
115678
116307
|
throw new SessionControlError("task_id and create_child_task are mutually exclusive", 400);
|
|
115679
116308
|
}
|
|
115680
116309
|
const { getSessionByClaudeId: getSessionByClaudeId2, getSessionsForTask: getSessionsForTask2, createSessionRecord: createSessionRecord2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
|
|
115681
|
-
const {
|
|
115682
|
-
getTask: getTask2,
|
|
115683
|
-
addTask: addTask2,
|
|
115684
|
-
updateTask: updateTask2,
|
|
115685
|
-
groupTasks: groupTasks2,
|
|
115686
|
-
addToGroup: addToGroup2,
|
|
115687
|
-
renameGroup: renameGroup2
|
|
115688
|
-
} = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
|
|
116310
|
+
const { getTask: getTask2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
|
|
115689
116311
|
const sourceRecord = await getSessionByClaudeId2(sourceSessionId);
|
|
115690
116312
|
if (!sourceRecord) throw new SessionControlError("Source session not found", 404);
|
|
115691
116313
|
if (!engineCaps(sourceRecord.engine).fork) {
|
|
@@ -115702,108 +116324,13 @@ async function forkSessionToTask(sourceSessionId, input, source = "web-api") {
|
|
|
115702
116324
|
if (!sourceRecord.taskId) {
|
|
115703
116325
|
throw new SessionControlError("Source session has no task \u2014 cannot create fork task", 400);
|
|
115704
116326
|
}
|
|
115705
|
-
|
|
115706
|
-
|
|
115707
|
-
|
|
115708
|
-
|
|
115709
|
-
|
|
115710
|
-
|
|
115711
|
-
const sourceBaseTitle = sourceTask.title.replace(/^Fork of\s+/i, "").replace(/\s+-\s+fork of\s+.*$/i, "").trim() || sourceTask.title;
|
|
115712
|
-
const autoTitle = !child_title;
|
|
115713
|
-
const newTitle = child_title || `Fork of ${sourceBaseTitle}`;
|
|
115714
|
-
let inheritedTier;
|
|
115715
|
-
if (sourceTask.pinned && sourceTask.focus_tier) {
|
|
115716
|
-
const { getCustomTiers: getCustomTiers2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
|
|
115717
|
-
const known = ["focus", "backlog", "wait"].includes(sourceTask.focus_tier) || (await getCustomTiers2()).some((t3) => t3.id === sourceTask.focus_tier);
|
|
115718
|
-
if (known) inheritedTier = sourceTask.focus_tier;
|
|
115719
|
-
else {
|
|
115720
|
-
log.session.warn("fork: source tier is not registered, filing the fork in Satellite", {
|
|
115721
|
-
sourceTaskId: sourceTask.id,
|
|
115722
|
-
tier: sourceTask.focus_tier
|
|
115723
|
-
});
|
|
115724
|
-
}
|
|
115725
|
-
}
|
|
115726
|
-
const { task: newFork } = await addTask2({
|
|
115727
|
-
title: newTitle,
|
|
115728
|
-
project: sourceTask.project || "",
|
|
115729
|
-
source: sourceTask.source,
|
|
115730
|
-
// A fork is a person splitting off live work, so it joins the board like
|
|
115731
|
-
// any other hand-made task (Satellite = pinned, no stored tier).
|
|
115732
|
-
pinned: true,
|
|
115733
|
-
...inheritedTier ? { focus_tier: inheritedTier } : {},
|
|
115734
|
-
// Forking an Ask Walnut conversation yields another Ask Walnut
|
|
115735
|
-
// conversation: without the flag the fork loses the amber marker AND the
|
|
115736
|
-
// persona drift repair (both key on task.walnut_agent, never on project).
|
|
115737
|
-
...sourceTask.walnut_agent ? { walnut_agent: true } : {}
|
|
115738
|
-
});
|
|
115739
|
-
let forkGroupId;
|
|
115740
|
-
try {
|
|
115741
|
-
if (sourceTask.group_id) {
|
|
115742
|
-
const r8 = await addToGroup2(sourceTask.group_id, [newFork.id]);
|
|
115743
|
-
forkGroupId = r8.group_id;
|
|
115744
|
-
} else {
|
|
115745
|
-
const r8 = await groupTasks2([sourceTask.id, newFork.id], sourceTask.title);
|
|
115746
|
-
forkGroupId = r8.group_id;
|
|
115747
|
-
}
|
|
115748
|
-
} catch (err2) {
|
|
115749
|
-
log.session.warn("fork: failed to group source + fork", {
|
|
115750
|
-
sourceTaskId: sourceTask.id,
|
|
115751
|
-
forkTaskId: newFork.id,
|
|
115752
|
-
error: err2 instanceof Error ? err2.message : String(err2)
|
|
115753
|
-
});
|
|
115754
|
-
}
|
|
115755
|
-
try {
|
|
115756
|
-
task = await getTask2(newFork.id);
|
|
115757
|
-
} catch {
|
|
115758
|
-
task = newFork;
|
|
115759
|
-
}
|
|
115760
|
-
bus.emit(EventNames.TASK_CREATED, { task }, ["web-ui", "main-agent"], { source: "fork" });
|
|
116327
|
+
const created = await createForkSiblingTask(sourceRecord.taskId, {
|
|
116328
|
+
childTitle: child_title,
|
|
116329
|
+
prompt: message,
|
|
116330
|
+
source: "fork"
|
|
116331
|
+
});
|
|
116332
|
+
task = created.task;
|
|
115761
116333
|
childTaskCreated = true;
|
|
115762
|
-
if (autoTitle && message?.trim()) {
|
|
115763
|
-
const forkId = newFork.id;
|
|
115764
|
-
const sourceTitle = sourceBaseTitle;
|
|
115765
|
-
const placeholderTitle = newTitle;
|
|
115766
|
-
void (async () => {
|
|
115767
|
-
try {
|
|
115768
|
-
const { summarizeForkPrompt: summarizeForkPrompt2 } = await Promise.resolve().then(() => (init_fork_title(), fork_title_exports));
|
|
115769
|
-
const label = await summarizeForkPrompt2(message);
|
|
115770
|
-
if (!label) return;
|
|
115771
|
-
const current5 = await getTask2(forkId);
|
|
115772
|
-
if (current5.title !== placeholderTitle) {
|
|
115773
|
-
log.session.info("fork title refine skipped \u2014 title changed since fork", { taskId: forkId });
|
|
115774
|
-
return;
|
|
115775
|
-
}
|
|
115776
|
-
const refinedTitle = `${label} - fork of ${sourceTitle}`;
|
|
115777
|
-
const { task: updated } = await updateTask2(forkId, { title: refinedTitle }, { source: "fork-title" });
|
|
115778
|
-
bus.emit(EventNames.TASK_UPDATED, { task: updated }, ["web-ui", "main-agent"], { source: "fork-title" });
|
|
115779
|
-
log.session.info("fork title refined", { taskId: forkId, title: refinedTitle });
|
|
115780
|
-
} catch (err2) {
|
|
115781
|
-
log.session.warn("fork title refine failed", {
|
|
115782
|
-
taskId: forkId,
|
|
115783
|
-
error: err2 instanceof Error ? err2.message : String(err2)
|
|
115784
|
-
});
|
|
115785
|
-
}
|
|
115786
|
-
})();
|
|
115787
|
-
}
|
|
115788
|
-
if (forkGroupId && !sourceTask.group_id) {
|
|
115789
|
-
const gid = forkGroupId;
|
|
115790
|
-
const seedTitles = [sourceTask.title, newTitle];
|
|
115791
|
-
void (async () => {
|
|
115792
|
-
try {
|
|
115793
|
-
const { summarizeGroupLabel: summarizeGroupLabel2 } = await Promise.resolve().then(() => (init_fork_title(), fork_title_exports));
|
|
115794
|
-
const label = await summarizeGroupLabel2(seedTitles);
|
|
115795
|
-
if (!label) return;
|
|
115796
|
-
await renameGroup2(gid, label);
|
|
115797
|
-
bus.emit(EventNames.TASK_GROUPS_CHANGED, { group_id: gid, label }, ["web-ui", "main-agent"], { source: "fork" });
|
|
115798
|
-
log.session.info("fork group label refined", { groupId: gid, label });
|
|
115799
|
-
} catch (err2) {
|
|
115800
|
-
log.session.warn("fork group label refine failed", {
|
|
115801
|
-
groupId: gid,
|
|
115802
|
-
error: err2 instanceof Error ? err2.message : String(err2)
|
|
115803
|
-
});
|
|
115804
|
-
}
|
|
115805
|
-
})();
|
|
115806
|
-
}
|
|
115807
116334
|
} else {
|
|
115808
116335
|
try {
|
|
115809
116336
|
task = await getTask2(task_id);
|
|
@@ -115825,7 +116352,7 @@ async function forkSessionToTask(sourceSessionId, input, source = "web-api") {
|
|
|
115825
116352
|
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";
|
|
115826
116353
|
const forkMessage = `${FORK_FOCUS_PREFIX}${imageContext ?? ""}New request:
|
|
115827
116354
|
${userRequest}`;
|
|
115828
|
-
const forkSessionId =
|
|
116355
|
+
const forkSessionId = randomUUID6();
|
|
115829
116356
|
const forkTitle = title ?? `Fork of ${sourceRecord.title ?? sourceSessionId.slice(0, 16)}`;
|
|
115830
116357
|
try {
|
|
115831
116358
|
await createSessionRecord2(forkSessionId, task.id, task.project ?? "", sourceRecord.cwd, {
|
|
@@ -115976,6 +116503,11 @@ async function handleSessionControlRelay(action, sessionId, params) {
|
|
|
115976
116503
|
result = await retrySession2(sessionId);
|
|
115977
116504
|
break;
|
|
115978
116505
|
}
|
|
116506
|
+
case "recheck": {
|
|
116507
|
+
const { recheckSession: recheckSession2 } = await Promise.resolve().then(() => (init_session_lifecycle(), session_lifecycle_exports));
|
|
116508
|
+
result = await recheckSession2(sessionId);
|
|
116509
|
+
break;
|
|
116510
|
+
}
|
|
115979
116511
|
case "permission": {
|
|
115980
116512
|
const { respondSessionPermission: respondSessionPermission2 } = await Promise.resolve().then(() => (init_session_lifecycle(), session_lifecycle_exports));
|
|
115981
116513
|
result = await respondSessionPermission2(sessionId, p6.requestId, p6.allow, p6.message, p6.optionId, p6.answers);
|
|
@@ -116543,7 +117075,7 @@ __export(session_rewind_exports, {
|
|
|
116543
117075
|
probeRewindViaDaemon: () => probeRewindViaDaemon,
|
|
116544
117076
|
rewindSessionToMessage: () => rewindSessionToMessage
|
|
116545
117077
|
});
|
|
116546
|
-
import { randomUUID as
|
|
117078
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
116547
117079
|
function cutAncestorHistoryAtRewindPoint(ancestorMessages, rewoundAtMessageUuid) {
|
|
116548
117080
|
if (!rewoundAtMessageUuid) {
|
|
116549
117081
|
return { messages: ancestorMessages, found: false, dropped: 0 };
|
|
@@ -116744,7 +117276,7 @@ async function rewindSessionToMessage(sessionId, input, source = "web-api") {
|
|
|
116744
117276
|
error: err2 instanceof Error ? err2.message : String(err2)
|
|
116745
117277
|
});
|
|
116746
117278
|
}
|
|
116747
|
-
const rewoundId =
|
|
117279
|
+
const rewoundId = randomUUID7();
|
|
116748
117280
|
const baseTitle = (record.title ?? sessionId.slice(0, 16)).replace(/\s*\(rewound\)$/i, "");
|
|
116749
117281
|
const title = `${baseTitle} (rewound)`.slice(0, 200);
|
|
116750
117282
|
const { createSessionRecord: createSessionRecord2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
|
|
@@ -117464,6 +117996,7 @@ function parseSessionMessages(content, opts) {
|
|
|
117464
117996
|
}
|
|
117465
117997
|
}
|
|
117466
117998
|
let lastPlanContent = null;
|
|
117999
|
+
let dropWarmupReply = false;
|
|
117467
118000
|
const resultParentIds = [];
|
|
117468
118001
|
const result = [];
|
|
117469
118002
|
for (const [mapMsgId, msg] of messageMap) {
|
|
@@ -117541,10 +118074,18 @@ ${imgPath}` : imgPath;
|
|
|
117541
118074
|
let text = textParts.join("\n").trim();
|
|
117542
118075
|
if (!text && tools2.length === 0 && !thinking) continue;
|
|
117543
118076
|
if (msg.role === "user" && text.startsWith("<")) {
|
|
118077
|
+
if (isCacheWarmupText(text)) {
|
|
118078
|
+
dropWarmupReply = true;
|
|
118079
|
+
continue;
|
|
118080
|
+
}
|
|
117544
118081
|
const transformed = transformInjectedUserText(text);
|
|
117545
118082
|
if (transformed === null) continue;
|
|
117546
118083
|
if (transformed !== void 0) text = transformed;
|
|
117547
118084
|
}
|
|
118085
|
+
if (msg.role === "assistant" && dropWarmupReply) {
|
|
118086
|
+
dropWarmupReply = false;
|
|
118087
|
+
if (tools2.length === 0) continue;
|
|
118088
|
+
}
|
|
117548
118089
|
if (msg.role === "user") text = stripOutputModeWrappers(text);
|
|
117549
118090
|
if (msg.role === "assistant" && !text && tools2.length === 0) continue;
|
|
117550
118091
|
result.push({
|
|
@@ -118495,6 +119036,7 @@ var init_session_history = __esm({
|
|
|
118495
119036
|
init_session_io();
|
|
118496
119037
|
init_constants();
|
|
118497
119038
|
init_remote_image_mirror();
|
|
119039
|
+
init_side_thread_warmup();
|
|
118498
119040
|
init_session_file_reader();
|
|
118499
119041
|
LOCAL_HOME = os19.homedir();
|
|
118500
119042
|
TAIL_SEGMENT_ROLL_BYTES = 4 * 1024 * 1024;
|
|
@@ -118787,7 +119329,7 @@ var init_context_window = __esm({
|
|
|
118787
119329
|
});
|
|
118788
119330
|
|
|
118789
119331
|
// src/providers/turn-ledger.ts
|
|
118790
|
-
import { randomUUID as
|
|
119332
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
118791
119333
|
function openTurn(sessionId) {
|
|
118792
119334
|
const existing = openTurns.get(sessionId);
|
|
118793
119335
|
if (existing && !existing.settled) {
|
|
@@ -118797,7 +119339,7 @@ function openTurn(sessionId) {
|
|
|
118797
119339
|
});
|
|
118798
119340
|
return { turnId: existing.turnId, promise: existing.promise };
|
|
118799
119341
|
}
|
|
118800
|
-
const turnId =
|
|
119342
|
+
const turnId = randomUUID8();
|
|
118801
119343
|
let resolve;
|
|
118802
119344
|
let reject;
|
|
118803
119345
|
const promise = new Promise((res, rej) => {
|
|
@@ -126791,11 +127333,39 @@ var init_claude_code_session = __esm({
|
|
|
126791
127333
|
session2.pendingDescription = message.slice(0, 500);
|
|
126792
127334
|
let appendSystemPrompt;
|
|
126793
127335
|
const isFork = !!data.forkedFromSessionId;
|
|
126794
|
-
|
|
126795
|
-
|
|
126796
|
-
|
|
126797
|
-
|
|
126798
|
-
|
|
127336
|
+
const { parseSideLaneKey: parseSideLaneKey2 } = await Promise.resolve().then(() => (init_side_thread_fork(), side_thread_fork_exports));
|
|
127337
|
+
const sideIds = parseSideLaneKey2(data.lane);
|
|
127338
|
+
if (sideIds) {
|
|
127339
|
+
let exact = data.appendSystemPromptExact;
|
|
127340
|
+
let source = "live-or-record";
|
|
127341
|
+
if (exact === void 0) {
|
|
127342
|
+
try {
|
|
127343
|
+
const { getSessionByClaudeId: getSessionByClaudeId2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
|
|
127344
|
+
const { spawnPrefixFromRecord: spawnPrefixFromRecord2 } = await Promise.resolve().then(() => (init_spawn_prefix(), spawn_prefix_exports));
|
|
127345
|
+
const parentRecord = await getSessionByClaudeId2(sideIds.parentSid);
|
|
127346
|
+
const prefix = parentRecord ? spawnPrefixFromRecord2(parentRecord) : null;
|
|
127347
|
+
exact = prefix?.appendSystemPrompt ?? null;
|
|
127348
|
+
source = prefix?.source ?? "no-parent-record";
|
|
127349
|
+
} catch (err2) {
|
|
127350
|
+
exact = null;
|
|
127351
|
+
source = "lookup-failed";
|
|
127352
|
+
log.session.warn("side thread: parent prompt lookup failed \u2014 spawning without an append prompt", {
|
|
127353
|
+
parentSid: sideIds.parentSid,
|
|
127354
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
127355
|
+
});
|
|
127356
|
+
}
|
|
127357
|
+
}
|
|
127358
|
+
appendSystemPrompt = exact ?? void 0;
|
|
127359
|
+
log.session.info("side thread: prefix prompt resolved", {
|
|
127360
|
+
parentSid: sideIds.parentSid,
|
|
127361
|
+
source,
|
|
127362
|
+
promptLength: appendSystemPrompt?.length ?? 0
|
|
127363
|
+
});
|
|
127364
|
+
} else {
|
|
127365
|
+
if (data.appendSystemPrompt && !isFork) {
|
|
127366
|
+
appendSystemPrompt = data.appendSystemPrompt;
|
|
127367
|
+
log.session.info("using caller-provided system prompt", { taskId: taskId2, promptLength: data.appendSystemPrompt.length });
|
|
127368
|
+
}
|
|
126799
127369
|
try {
|
|
126800
127370
|
const { buildSessionContext: buildSessionContext2 } = await Promise.resolve().then(() => (init_session_context(), session_context_exports));
|
|
126801
127371
|
const ctx = await buildSessionContext2(taskId2 ?? "", cwd, data.host);
|
|
@@ -126813,32 +127383,6 @@ ${ctx.systemPrompt}` : ctx.systemPrompt;
|
|
|
126813
127383
|
log.session.warn("failed to build session context", { taskId: taskId2, error: err2 instanceof Error ? err2.message : String(err2) });
|
|
126814
127384
|
}
|
|
126815
127385
|
}
|
|
126816
|
-
{
|
|
126817
|
-
const { parseSideLaneKey: parseSideLaneKey2 } = await Promise.resolve().then(() => (init_side_thread_fork(), side_thread_fork_exports));
|
|
126818
|
-
const sideIds = parseSideLaneKey2(data.lane);
|
|
126819
|
-
if (sideIds) {
|
|
126820
|
-
try {
|
|
126821
|
-
const { getSessionByClaudeId: getSessionByClaudeId2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
|
|
126822
|
-
const parentRecord = await getSessionByClaudeId2(sideIds.parentSid);
|
|
126823
|
-
const inherited = parentRecord?.appliedAppendSystemPrompt;
|
|
126824
|
-
if (inherited && Buffer.byteLength(inherited, "utf8") <= 65536) {
|
|
126825
|
-
appendSystemPrompt = inherited;
|
|
126826
|
-
log.session.info("side thread: inherited parent system prompt verbatim", {
|
|
126827
|
-
parentSid: sideIds.parentSid,
|
|
126828
|
-
promptLength: appendSystemPrompt.length
|
|
126829
|
-
});
|
|
126830
|
-
} else {
|
|
126831
|
-
log.session.info("side thread: parent has no stored system prompt \u2014 fresh build (cache prefix will diverge)", {
|
|
126832
|
-
parentSid: sideIds.parentSid
|
|
126833
|
-
});
|
|
126834
|
-
}
|
|
126835
|
-
} catch (err2) {
|
|
126836
|
-
log.session.warn("side thread: parent prompt lookup failed \u2014 fresh build", {
|
|
126837
|
-
error: err2 instanceof Error ? err2.message : String(err2)
|
|
126838
|
-
});
|
|
126839
|
-
}
|
|
126840
|
-
}
|
|
126841
|
-
}
|
|
126842
127386
|
if (data.preassignedSessionId && appendSystemPrompt && Buffer.byteLength(appendSystemPrompt, "utf8") <= 65536) {
|
|
126843
127387
|
const promptToStore = appendSystemPrompt;
|
|
126844
127388
|
const sidToStore = data.preassignedSessionId;
|
|
@@ -128038,7 +128582,7 @@ __export(session_requests_exports, {
|
|
|
128038
128582
|
settleNotified: () => settleNotified,
|
|
128039
128583
|
settleReplied: () => settleReplied
|
|
128040
128584
|
});
|
|
128041
|
-
import { createHash as createHash14, randomUUID as
|
|
128585
|
+
import { createHash as createHash14, randomUUID as randomUUID9 } from "crypto";
|
|
128042
128586
|
import path119 from "path";
|
|
128043
128587
|
function oneLine2(text, max = 160) {
|
|
128044
128588
|
const flat = text.replace(/\s+/g, " ").trim();
|
|
@@ -128062,7 +128606,7 @@ function clampReplyTimeoutSecs(secs) {
|
|
|
128062
128606
|
}
|
|
128063
128607
|
async function createSessionRequest(input) {
|
|
128064
128608
|
const request2 = {
|
|
128065
|
-
id: `rq-${
|
|
128609
|
+
id: `rq-${randomUUID9().replace(/-/g, "").slice(0, 12)}`,
|
|
128066
128610
|
fromSessionId: input.fromSessionId,
|
|
128067
128611
|
...input.toSessionId ? { toSessionId: input.toSessionId } : {},
|
|
128068
128612
|
...input.toTaskId ? { toTaskId: input.toTaskId } : {},
|
|
@@ -137021,7 +137565,7 @@ __export(mobile_launch_exports, {
|
|
|
137021
137565
|
performMobileLaunch: () => performMobileLaunch,
|
|
137022
137566
|
validateMobileLaunchBody: () => validateMobileLaunchBody
|
|
137023
137567
|
});
|
|
137024
|
-
import { randomUUID as
|
|
137568
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
137025
137569
|
function launchErrorCode(status2) {
|
|
137026
137570
|
if (status2 === 404) return "not_found";
|
|
137027
137571
|
if (status2 === 409) return "conflict";
|
|
@@ -137143,7 +137687,7 @@ async function performMobileLaunch(input, source) {
|
|
|
137143
137687
|
);
|
|
137144
137688
|
}
|
|
137145
137689
|
}
|
|
137146
|
-
const preassignedSessionId = engineCaps(input.engine).idProvisioning === "provider-issued" ? void 0 :
|
|
137690
|
+
const preassignedSessionId = engineCaps(input.engine).idProvisioning === "provider-issued" ? void 0 : randomUUID10();
|
|
137147
137691
|
const task = await quickStartSession({
|
|
137148
137692
|
message: input.message,
|
|
137149
137693
|
cwd: input.cwd,
|
|
@@ -137942,6 +138486,7 @@ __export(daemon_connection_exports, {
|
|
|
137942
138486
|
isDaemonConnected: () => isDaemonConnected,
|
|
137943
138487
|
isPooledConnection: () => isPooledConnection,
|
|
137944
138488
|
probeDaemonSession: () => probeDaemonSession,
|
|
138489
|
+
probeDaemonSessionArgs: () => probeDaemonSessionArgs,
|
|
137945
138490
|
pushDaemonHooksToAllHosts: () => pushDaemonHooksToAllHosts,
|
|
137946
138491
|
setOnDaemonHostConnected: () => setOnDaemonHostConnected,
|
|
137947
138492
|
setOnDaemonStatusChange: () => setOnDaemonStatusChange,
|
|
@@ -138118,6 +138663,28 @@ async function probeDaemonSession(hostKey2, sessionId) {
|
|
|
138118
138663
|
return null;
|
|
138119
138664
|
}
|
|
138120
138665
|
}
|
|
138666
|
+
async function probeDaemonSessionArgs(hostKey2, sessionId, deadlineMs = 2e3) {
|
|
138667
|
+
const conn = getConnectedDaemonConnection(hostKey2);
|
|
138668
|
+
if (!conn) return null;
|
|
138669
|
+
try {
|
|
138670
|
+
const result = await Promise.race([
|
|
138671
|
+
conn.send("status", { sid: sessionId, includeArgs: true }),
|
|
138672
|
+
new Promise((_, reject) => setTimeout(
|
|
138673
|
+
() => reject(new Error(`status probe exceeded ${deadlineMs}ms`)),
|
|
138674
|
+
deadlineMs
|
|
138675
|
+
).unref?.())
|
|
138676
|
+
]);
|
|
138677
|
+
if (!result.ok || !result.alive || !Array.isArray(result.args)) return null;
|
|
138678
|
+
return result.args.filter((a8) => typeof a8 === "string");
|
|
138679
|
+
} catch (err2) {
|
|
138680
|
+
log.session.debug("probeDaemonSessionArgs: status probe failed", {
|
|
138681
|
+
hostKey: hostKey2,
|
|
138682
|
+
sessionId,
|
|
138683
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
138684
|
+
});
|
|
138685
|
+
return null;
|
|
138686
|
+
}
|
|
138687
|
+
}
|
|
138121
138688
|
var execFileAsync11, MOBILE_ENQUEUE_LEDGER_MAX, recentMobileEnqueues, DaemonConnection, onPoolStatusChange, onHostConnected, hostConnectedListeners, connectionPool, connectingPromises, failureCache, FAILURE_CACHE_TTL_MS;
|
|
138122
138689
|
var init_daemon_connection = __esm({
|
|
138123
138690
|
"src/providers/daemon-connection.ts"() {
|
|
@@ -143463,6 +144030,7 @@ async function createSessionRecord(claudeSessionId, taskId2, project, cwd, extra
|
|
|
143463
144030
|
...extra?.effort ? { effort: extra.effort } : {},
|
|
143464
144031
|
...extra?.profile ? { profile: extra.profile } : {},
|
|
143465
144032
|
...extra?.lane ? { lane: extra.lane } : {},
|
|
144033
|
+
...extra?.appliedAppendSystemPrompt !== void 0 ? { appliedAppendSystemPrompt: extra.appliedAppendSystemPrompt } : {},
|
|
143466
144034
|
...extra?.engine ? { engine: extra.engine } : {},
|
|
143467
144035
|
...extra?.acpRuntimeId ? { acpRuntimeId: extra.acpRuntimeId } : {},
|
|
143468
144036
|
...extra?.acpJournalPath ? { acpJournalPath: extra.acpJournalPath } : {},
|
|
@@ -143617,7 +144185,7 @@ function applyUpdateToSession(session2, updates, logLabel) {
|
|
|
143617
144185
|
};
|
|
143618
144186
|
if (count2 === 1) log.session.info("legacy status write suppressed", payload);
|
|
143619
144187
|
else log.session.debug("legacy status write suppressed", payload);
|
|
143620
|
-
if (updates.process_status === "error") {
|
|
144188
|
+
if (updates.process_status === "error" || updates.process_status === "stopped") {
|
|
143621
144189
|
noteSuppressedErrorReason(session2.claudeSessionId, {
|
|
143622
144190
|
reason: typeof reason === "string" ? reason : "unknown",
|
|
143623
144191
|
message: typeof updates.errorMessage === "string" ? updates.errorMessage : void 0,
|
|
@@ -151566,14 +152134,16 @@ function isLive(r8) {
|
|
|
151566
152134
|
function isRevivable(r8) {
|
|
151567
152135
|
return !!r8.outputFile || r8.consumedOffset !== void 0;
|
|
151568
152136
|
}
|
|
151569
|
-
var STANDBY_TTL_MS, MAX_LIVE_THREADS, IDLE_SWEEP_INTERVAL_MS, THREAD_IDLE_MS, SideThreadManager, sideThreadManager;
|
|
152137
|
+
var STANDBY_TTL_MS, WARMED_STANDBY_TTL_MS, MAX_LIVE_THREADS, IDLE_SWEEP_INTERVAL_MS, THREAD_IDLE_MS, SideThreadManager, sideThreadManager;
|
|
151570
152138
|
var init_side_thread_manager = __esm({
|
|
151571
152139
|
"src/core/sessions/side-thread-manager.ts"() {
|
|
151572
152140
|
"use strict";
|
|
151573
152141
|
init_logging();
|
|
151574
152142
|
init_side_thread_fork();
|
|
151575
152143
|
init_session_controls();
|
|
152144
|
+
init_side_thread_warmup();
|
|
151576
152145
|
STANDBY_TTL_MS = 12e4;
|
|
152146
|
+
WARMED_STANDBY_TTL_MS = 15 * 6e4;
|
|
151577
152147
|
MAX_LIVE_THREADS = 3;
|
|
151578
152148
|
IDLE_SWEEP_INTERVAL_MS = 5 * 6e4;
|
|
151579
152149
|
THREAD_IDLE_MS = 30 * 6e4;
|
|
@@ -151584,6 +152154,8 @@ var init_side_thread_manager = __esm({
|
|
|
151584
152154
|
* In-memory only: a server restart loses it, but the boot sweep archives
|
|
151585
152155
|
* every standby anyway, so a record can never outlive its map entry. */
|
|
151586
152156
|
standbyParentOffsets = /* @__PURE__ */ new Map();
|
|
152157
|
+
/** parentSid → standby sid that already ran its cache warm-up turn. */
|
|
152158
|
+
warmedStandbys = /* @__PURE__ */ new Map();
|
|
151587
152159
|
/** One in-flight standby/create op per parent — two concurrent asks must not
|
|
151588
152160
|
* both consume the same standby (they would collide on its lane rename). */
|
|
151589
152161
|
inFlight = /* @__PURE__ */ new Map();
|
|
@@ -151615,6 +152187,7 @@ var init_side_thread_manager = __esm({
|
|
|
151615
152187
|
for (const timer3 of this.standbyTimers.values()) clearTimeout(timer3);
|
|
151616
152188
|
this.standbyTimers.clear();
|
|
151617
152189
|
this.standbyParentOffsets.clear();
|
|
152190
|
+
this.warmedStandbys.clear();
|
|
151618
152191
|
this.inFlight.clear();
|
|
151619
152192
|
}
|
|
151620
152193
|
// ── Standby ────────────────────────────────────────────────────────────────
|
|
@@ -151630,10 +152203,12 @@ var init_side_thread_manager = __esm({
|
|
|
151630
152203
|
const existing = await this.findStandby(parentSid);
|
|
151631
152204
|
if (existing) {
|
|
151632
152205
|
if (await this.isStandbyUsable(parentSid, existing)) {
|
|
151633
|
-
this.
|
|
152206
|
+
const warmed = this.warmedStandbys.get(parentSid) === existing.claudeSessionId;
|
|
152207
|
+
this.armStandbyTtl(parentSid, existing.claudeSessionId, warmed ? WARMED_STANDBY_TTL_MS : STANDBY_TTL_MS);
|
|
151634
152208
|
return existing.claudeSessionId;
|
|
151635
152209
|
}
|
|
151636
152210
|
void this.retireSession(existing.claudeSessionId, "side_thread_standby_stale");
|
|
152211
|
+
this.warmedStandbys.delete(parentSid);
|
|
151637
152212
|
}
|
|
151638
152213
|
const parentOffset = parent.consumedOffset;
|
|
151639
152214
|
const { sessionId } = await forkSideThreadSession(parentSid, STANDBY_THREAD_ID);
|
|
@@ -151659,7 +152234,7 @@ var init_side_thread_manager = __esm({
|
|
|
151659
152234
|
if (!parent) return false;
|
|
151660
152235
|
return parent.consumedOffset === this.standbyParentOffsets.get(parentSid);
|
|
151661
152236
|
}
|
|
151662
|
-
armStandbyTtl(parentSid, sessionId) {
|
|
152237
|
+
armStandbyTtl(parentSid, sessionId, ttlMs = STANDBY_TTL_MS) {
|
|
151663
152238
|
const prev = this.standbyTimers.get(parentSid);
|
|
151664
152239
|
if (prev) clearTimeout(prev);
|
|
151665
152240
|
const timer3 = setTimeout(() => {
|
|
@@ -151669,13 +152244,41 @@ var init_side_thread_manager = __esm({
|
|
|
151669
152244
|
const record = await getSessionByClaudeId2(sessionId).catch(() => null);
|
|
151670
152245
|
if (!record || parseSideLaneKey(record.lane)?.threadId !== STANDBY_THREAD_ID) return;
|
|
151671
152246
|
this.standbyParentOffsets.delete(parentSid);
|
|
152247
|
+
this.warmedStandbys.delete(parentSid);
|
|
151672
152248
|
await this.retireSession(sessionId, "side_thread_standby_ttl");
|
|
151673
152249
|
}).catch(() => {
|
|
151674
152250
|
});
|
|
151675
|
-
},
|
|
152251
|
+
}, ttlMs);
|
|
151676
152252
|
timer3.unref?.();
|
|
151677
152253
|
this.standbyTimers.set(parentSid, timer3);
|
|
151678
152254
|
}
|
|
152255
|
+
/**
|
|
152256
|
+
* Run the standby's cache warm-up turn (see side-thread-warmup.ts). Called
|
|
152257
|
+
* when the user starts TYPING a new question: the fork's first API call pays
|
|
152258
|
+
* the full prefix write no matter what, so paying it while they type turns
|
|
152259
|
+
* the actual question into a cached follow-up. Idempotent per standby; a
|
|
152260
|
+
* missing or stale standby is a no-op (the ask path will fork fresh anyway).
|
|
152261
|
+
*/
|
|
152262
|
+
async warmStandby(parentSid) {
|
|
152263
|
+
return this.serialize(parentSid, async () => {
|
|
152264
|
+
const standby = await this.findStandby(parentSid);
|
|
152265
|
+
if (!standby) return { warmed: false, reason: "no_standby" };
|
|
152266
|
+
if (!await this.isStandbyUsable(parentSid, standby)) return { warmed: false, reason: "stale" };
|
|
152267
|
+
if (this.warmedStandbys.get(parentSid) === standby.claudeSessionId) {
|
|
152268
|
+
return { warmed: true, reason: "already_warm" };
|
|
152269
|
+
}
|
|
152270
|
+
const { sendMessageToSession: sendMessageToSession2 } = await Promise.resolve().then(() => (init_session_message_queue(), session_message_queue_exports));
|
|
152271
|
+
markWarmupTurnPending(standby.claudeSessionId);
|
|
152272
|
+
await sendMessageToSession2(standby.claudeSessionId, CACHE_WARMUP_MESSAGE, { source: "side-thread-warmup" });
|
|
152273
|
+
this.warmedStandbys.set(parentSid, standby.claudeSessionId);
|
|
152274
|
+
this.armStandbyTtl(parentSid, standby.claudeSessionId, WARMED_STANDBY_TTL_MS);
|
|
152275
|
+
log.session.info("side thread: standby cache warm-up sent", {
|
|
152276
|
+
parentSid,
|
|
152277
|
+
standbySid: standby.claudeSessionId
|
|
152278
|
+
});
|
|
152279
|
+
return { warmed: true };
|
|
152280
|
+
});
|
|
152281
|
+
}
|
|
151679
152282
|
// ── Threads ────────────────────────────────────────────────────────────────
|
|
151680
152283
|
/**
|
|
151681
152284
|
* Open a side thread on `parentSid` and ask `question` in it.
|
|
@@ -151686,6 +152289,10 @@ var init_side_thread_manager = __esm({
|
|
|
151686
152289
|
* question as its spawn's first turn instead — a send issued right after
|
|
151687
152290
|
* SESSION_START can lose that race and cold-`--resume` an id the CLI has never
|
|
151688
152291
|
* seen (see personal-ai-lane.ts LaneSession.created).
|
|
152292
|
+
*
|
|
152293
|
+
* `imageContext` (attachment paths for the CLI to Read) rides the MESSAGE only.
|
|
152294
|
+
* The stored entry keeps the plain question, so the chip label, the injected
|
|
152295
|
+
* transcript and a promoted task never carry the machine preamble.
|
|
151689
152296
|
*/
|
|
151690
152297
|
async createThread(parentSid, input) {
|
|
151691
152298
|
const question = input.question?.trim();
|
|
@@ -151693,8 +152300,11 @@ var init_side_thread_manager = __esm({
|
|
|
151693
152300
|
return this.serialize(parentSid, async () => {
|
|
151694
152301
|
const threadId = mintSideThreadId();
|
|
151695
152302
|
const title = input.title?.trim() || void 0;
|
|
152303
|
+
const message = input.imageContext ? `${input.imageContext}
|
|
152304
|
+
|
|
152305
|
+
${question}` : question;
|
|
151696
152306
|
const consumed = await this.consumeStandby(parentSid, threadId, title);
|
|
151697
|
-
const threadSessionId = consumed ?? (await forkSideThreadSession(parentSid, threadId, { message
|
|
152307
|
+
const threadSessionId = consumed ?? (await forkSideThreadSession(parentSid, threadId, { message, ...title ? { title } : {} })).sessionId;
|
|
151698
152308
|
let entry;
|
|
151699
152309
|
try {
|
|
151700
152310
|
const { addSideThread: addSideThread2 } = await Promise.resolve().then(() => (init_side_questions(), side_questions_exports));
|
|
@@ -151706,7 +152316,7 @@ var init_side_thread_manager = __esm({
|
|
|
151706
152316
|
});
|
|
151707
152317
|
if (consumed) {
|
|
151708
152318
|
const { sendMessageToSession: sendMessageToSession2 } = await Promise.resolve().then(() => (init_session_message_queue(), session_message_queue_exports));
|
|
151709
|
-
await sendMessageToSession2(threadSessionId,
|
|
152319
|
+
await sendMessageToSession2(threadSessionId, message, { source: "side-thread" });
|
|
151710
152320
|
}
|
|
151711
152321
|
} catch (err2) {
|
|
151712
152322
|
void this.retireSession(threadSessionId, "side_thread_create_failed");
|
|
@@ -151741,6 +152351,7 @@ var init_side_thread_manager = __esm({
|
|
|
151741
152351
|
if (!await this.isStandbyUsable(parentSid, standby)) {
|
|
151742
152352
|
void this.retireSession(standby.claudeSessionId, "side_thread_standby_stale");
|
|
151743
152353
|
this.standbyParentOffsets.delete(parentSid);
|
|
152354
|
+
this.warmedStandbys.delete(parentSid);
|
|
151744
152355
|
return null;
|
|
151745
152356
|
}
|
|
151746
152357
|
const timer3 = this.standbyTimers.get(parentSid);
|
|
@@ -151749,6 +152360,7 @@ var init_side_thread_manager = __esm({
|
|
|
151749
152360
|
this.standbyTimers.delete(parentSid);
|
|
151750
152361
|
}
|
|
151751
152362
|
this.standbyParentOffsets.delete(parentSid);
|
|
152363
|
+
this.warmedStandbys.delete(parentSid);
|
|
151752
152364
|
const lane = sideThreadLaneKey(parentSid, threadId);
|
|
151753
152365
|
const { updateSessionRecord: updateSessionRecord2 } = await Promise.resolve().then(() => (init_session_tracker(), session_tracker_exports));
|
|
151754
152366
|
await updateSessionRecord2(standby.claudeSessionId, {
|
|
@@ -151935,16 +152547,30 @@ async function promoteSideThread(parentSid, threadId, opts) {
|
|
|
151935
152547
|
const threadRecord = await getSessionByClaudeId2(sessionId);
|
|
151936
152548
|
if (!threadRecord) throw new SessionControlError("Side thread session not found", 404);
|
|
151937
152549
|
const parentRecord = await getSessionByClaudeId2(parentSid);
|
|
151938
|
-
const
|
|
151939
|
-
const
|
|
152550
|
+
const siblingOfTaskId = parentRecord?.taskId?.trim() || void 0;
|
|
152551
|
+
const threadLabel = opts?.title?.trim() || entry.title?.trim() || void 0;
|
|
151940
152552
|
const { addTask: addTask2, linkSession: linkSession2, linkSessionSlot: linkSessionSlot2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
|
|
151941
|
-
|
|
151942
|
-
|
|
151943
|
-
|
|
151944
|
-
|
|
151945
|
-
|
|
151946
|
-
|
|
151947
|
-
|
|
152553
|
+
let groupId;
|
|
152554
|
+
let task;
|
|
152555
|
+
if (siblingOfTaskId) {
|
|
152556
|
+
const created = await createForkSiblingTask(siblingOfTaskId, {
|
|
152557
|
+
// A known label short-circuits the background refine; without one the
|
|
152558
|
+
// thread's first question is what gets summarized (same as a fork's message).
|
|
152559
|
+
titlePrefix: threadLabel,
|
|
152560
|
+
prompt: entry.question,
|
|
152561
|
+
description: entry.question,
|
|
152562
|
+
source: "side-thread-promote"
|
|
152563
|
+
});
|
|
152564
|
+
task = created.task;
|
|
152565
|
+
groupId = created.groupId;
|
|
152566
|
+
} else {
|
|
152567
|
+
({ task } = await addTask2({
|
|
152568
|
+
title: threadLabel || entry.question,
|
|
152569
|
+
description: entry.question,
|
|
152570
|
+
// A person clicked "promote" — same board default as any hand-made task.
|
|
152571
|
+
pinned: true
|
|
152572
|
+
}));
|
|
152573
|
+
}
|
|
151948
152574
|
const stamped = await markThreadPromoted2(parentSid, threadId, task.id);
|
|
151949
152575
|
if (!stamped) {
|
|
151950
152576
|
log.session.warn("side thread promote: entry vanished before stamp \u2014 promote proceeds, 409 guard is unarmed", {
|
|
@@ -151958,7 +152584,7 @@ async function promoteSideThread(parentSid, threadId, opts) {
|
|
|
151958
152584
|
project: task.project ?? "",
|
|
151959
152585
|
// Clearing the lane un-hides the session (see the file header).
|
|
151960
152586
|
lane: void 0,
|
|
151961
|
-
title
|
|
152587
|
+
title: task.title
|
|
151962
152588
|
});
|
|
151963
152589
|
try {
|
|
151964
152590
|
const { sessionRunner: sessionRunner2 } = await Promise.resolve().then(() => (init_claude_code_session(), claude_code_session_exports));
|
|
@@ -151977,10 +152603,18 @@ async function promoteSideThread(parentSid, threadId, opts) {
|
|
|
151977
152603
|
error: err2 instanceof Error ? err2.message : String(err2)
|
|
151978
152604
|
});
|
|
151979
152605
|
}
|
|
151980
|
-
log.session.info("side thread: promoted to task", {
|
|
152606
|
+
log.session.info("side thread: promoted to task", {
|
|
152607
|
+
parentSid,
|
|
152608
|
+
threadId,
|
|
152609
|
+
sessionId,
|
|
152610
|
+
taskId: task.id,
|
|
152611
|
+
siblingOfTaskId,
|
|
152612
|
+
groupId
|
|
152613
|
+
});
|
|
151981
152614
|
return {
|
|
151982
152615
|
taskId: task.id,
|
|
151983
|
-
...
|
|
152616
|
+
...siblingOfTaskId ? { siblingOfTaskId } : {},
|
|
152617
|
+
...groupId ? { groupId } : {},
|
|
151984
152618
|
sessionId
|
|
151985
152619
|
};
|
|
151986
152620
|
}
|
|
@@ -151995,7 +152629,7 @@ var init_side_thread_promote = __esm({
|
|
|
151995
152629
|
|
|
151996
152630
|
// src/web/routes/sessions.ts
|
|
151997
152631
|
import { Router as Router16 } from "express";
|
|
151998
|
-
import { randomUUID as
|
|
152632
|
+
import { randomUUID as randomUUID11 } from "crypto";
|
|
151999
152633
|
import os27 from "os";
|
|
152000
152634
|
import path142 from "path";
|
|
152001
152635
|
function condenseStaleReason(msg) {
|
|
@@ -152341,7 +152975,7 @@ var init_sessions = __esm({
|
|
|
152341
152975
|
}
|
|
152342
152976
|
const isNativeEngine = !isAcpEngine(engine);
|
|
152343
152977
|
const clientSessionId = typeof req.body.sessionId === "string" && UUID_RE3.test(req.body.sessionId) ? req.body.sessionId : void 0;
|
|
152344
|
-
const preassignedSessionId = isNativeEngine ? clientSessionId ??
|
|
152978
|
+
const preassignedSessionId = isNativeEngine ? clientSessionId ?? randomUUID11() : void 0;
|
|
152345
152979
|
const updatedTask = await quickStartSession({
|
|
152346
152980
|
message: sessionMessage,
|
|
152347
152981
|
messagePrefix,
|
|
@@ -153316,18 +153950,38 @@ var init_sessions = __esm({
|
|
|
153316
153950
|
});
|
|
153317
153951
|
}
|
|
153318
153952
|
});
|
|
153953
|
+
sessionsRouter.post("/:sessionId/side-threads/standby/warm", async (req, res) => {
|
|
153954
|
+
if (refuseSideThreadsOnReplica(res)) return;
|
|
153955
|
+
const sessionId = String(req.params.sessionId);
|
|
153956
|
+
try {
|
|
153957
|
+
const { sideThreadManager: sideThreadManager2 } = await Promise.resolve().then(() => (init_side_thread_manager(), side_thread_manager_exports));
|
|
153958
|
+
res.json(await sideThreadManager2.warmStandby(sessionId));
|
|
153959
|
+
} catch (err2) {
|
|
153960
|
+
log.web.warn("side thread standby warm-up failed", {
|
|
153961
|
+
sessionId,
|
|
153962
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
153963
|
+
});
|
|
153964
|
+
res.json({ warmed: false, reason: "error" });
|
|
153965
|
+
}
|
|
153966
|
+
});
|
|
153319
153967
|
sessionsRouter.post("/:sessionId/side-threads", async (req, res, next) => {
|
|
153320
153968
|
if (refuseSideThreadsOnReplica(res)) return;
|
|
153321
153969
|
try {
|
|
153322
|
-
const { question, title } = req.body ?? {};
|
|
153970
|
+
const { question, title, images } = req.body ?? {};
|
|
153323
153971
|
if (!question || typeof question !== "string" || !question.trim()) {
|
|
153324
153972
|
res.status(400).json({ error: "question (non-empty string) is required" });
|
|
153325
153973
|
return;
|
|
153326
153974
|
}
|
|
153975
|
+
let imageContext = "";
|
|
153976
|
+
if (images && images.length > 0) {
|
|
153977
|
+
const processed = await processAndSaveImages(images);
|
|
153978
|
+
if (processed) imageContext = buildSessionImageContext(processed.savedImages);
|
|
153979
|
+
}
|
|
153327
153980
|
const { sideThreadManager: sideThreadManager2 } = await Promise.resolve().then(() => (init_side_thread_manager(), side_thread_manager_exports));
|
|
153328
153981
|
const thread = await sideThreadManager2.createThread(String(req.params.sessionId), {
|
|
153329
153982
|
question,
|
|
153330
|
-
...typeof title === "string" && title.trim() ? { title } : {}
|
|
153983
|
+
...typeof title === "string" && title.trim() ? { title } : {},
|
|
153984
|
+
...imageContext ? { imageContext } : {}
|
|
153331
153985
|
});
|
|
153332
153986
|
res.json({ thread });
|
|
153333
153987
|
} catch (err2) {
|
|
@@ -153346,7 +154000,8 @@ var init_sessions = __esm({
|
|
|
153346
154000
|
);
|
|
153347
154001
|
res.json({
|
|
153348
154002
|
taskId: result.taskId,
|
|
153349
|
-
...result.
|
|
154003
|
+
...result.siblingOfTaskId ? { siblingOfTaskId: result.siblingOfTaskId } : {},
|
|
154004
|
+
...result.groupId ? { groupId: result.groupId } : {},
|
|
153350
154005
|
sessionId: result.sessionId
|
|
153351
154006
|
});
|
|
153352
154007
|
} catch (err2) {
|
|
@@ -153537,6 +154192,19 @@ var init_sessions = __esm({
|
|
|
153537
154192
|
next(err2);
|
|
153538
154193
|
}
|
|
153539
154194
|
});
|
|
154195
|
+
sessionsRouter.post("/:sessionId/recheck", async (req, res, next) => {
|
|
154196
|
+
try {
|
|
154197
|
+
const sessionId = req.params.sessionId;
|
|
154198
|
+
const result = await recheckSession(sessionId);
|
|
154199
|
+
res.json(result);
|
|
154200
|
+
} catch (err2) {
|
|
154201
|
+
if (err2 instanceof SessionControlError) {
|
|
154202
|
+
res.status(err2.statusCode).json({ error: err2.message });
|
|
154203
|
+
return;
|
|
154204
|
+
}
|
|
154205
|
+
next(err2);
|
|
154206
|
+
}
|
|
154207
|
+
});
|
|
153540
154208
|
sessionsRouter.post("/:sessionId/restart", async (req, res, next) => {
|
|
153541
154209
|
const sessionId = req.params.sessionId;
|
|
153542
154210
|
const startedAt = Date.now();
|
|
@@ -154028,7 +154696,7 @@ var micro_claude_exports = {};
|
|
|
154028
154696
|
__export(micro_claude_exports, {
|
|
154029
154697
|
runMicroClaude: () => runMicroClaude
|
|
154030
154698
|
});
|
|
154031
|
-
import { randomUUID as
|
|
154699
|
+
import { randomUUID as randomUUID12 } from "crypto";
|
|
154032
154700
|
async function runMicroClaude(opts) {
|
|
154033
154701
|
if (opts.warm) {
|
|
154034
154702
|
const run10 = await runWarmMicroClaude({
|
|
@@ -154037,7 +154705,7 @@ async function runMicroClaude(opts) {
|
|
|
154037
154705
|
tools: opts.tools ?? [],
|
|
154038
154706
|
prompt: opts.prompt,
|
|
154039
154707
|
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
|
|
154040
|
-
toolUseId: opts.toolUseId ?? `micro-claude-${
|
|
154708
|
+
toolUseId: opts.toolUseId ?? `micro-claude-${randomUUID12()}`,
|
|
154041
154709
|
...opts.onBlock ? { onBlock: opts.onBlock } : {}
|
|
154042
154710
|
});
|
|
154043
154711
|
return { response: run10.response, costUsd: run10.costUsd, durationMs: run10.durationMs };
|
|
@@ -154047,7 +154715,7 @@ async function runMicroClaude(opts) {
|
|
|
154047
154715
|
systemPrompt: opts.system,
|
|
154048
154716
|
model: opts.model ?? DEFAULT_MODEL2,
|
|
154049
154717
|
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
|
|
154050
|
-
toolUseId: opts.toolUseId ?? `micro-claude-${
|
|
154718
|
+
toolUseId: opts.toolUseId ?? `micro-claude-${randomUUID12()}`,
|
|
154051
154719
|
slim: true,
|
|
154052
154720
|
thinking: opts.thinking ?? false,
|
|
154053
154721
|
...opts.tools && opts.tools.length > 0 ? { tools: opts.tools } : {},
|
|
@@ -154077,7 +154745,7 @@ __export(micro_agent_exports, {
|
|
|
154077
154745
|
async function resolveTierModel(tier) {
|
|
154078
154746
|
const { getConfig: getConfig2 } = await Promise.resolve().then(() => (init_config_manager(), config_manager_exports));
|
|
154079
154747
|
const config = await getConfig2();
|
|
154080
|
-
const provider = config
|
|
154748
|
+
const provider = resolveMainProviderName(config);
|
|
154081
154749
|
const { MODEL_CATALOG: MODEL_CATALOG3 } = await Promise.resolve().then(() => (init_model_catalog(), model_catalog_exports));
|
|
154082
154750
|
const entry = MODEL_CATALOG3[provider]?.find((m6) => {
|
|
154083
154751
|
const id = m6.id.toLowerCase();
|
|
@@ -154146,6 +154814,7 @@ var DEFAULT_TIER, DEFAULT_MAX_TOKENS2, DEFAULT_MAX_ROUNDS, DEFAULT_TIMEOUT_MS3;
|
|
|
154146
154814
|
var init_micro_agent = __esm({
|
|
154147
154815
|
"src/agent/micro-agent.ts"() {
|
|
154148
154816
|
"use strict";
|
|
154817
|
+
init_default_provider();
|
|
154149
154818
|
DEFAULT_TIER = "sonnet";
|
|
154150
154819
|
DEFAULT_MAX_TOKENS2 = 2e3;
|
|
154151
154820
|
DEFAULT_MAX_ROUNDS = 3;
|
|
@@ -154161,7 +154830,7 @@ __export(task_search_agent_exports, {
|
|
|
154161
154830
|
prewarmAgentSearchChild: () => prewarmAgentSearchChild,
|
|
154162
154831
|
runTaskSearchAgent: () => runTaskSearchAgent
|
|
154163
154832
|
});
|
|
154164
|
-
import { randomUUID as
|
|
154833
|
+
import { randomUUID as randomUUID13 } from "crypto";
|
|
154165
154834
|
function useCliEngine() {
|
|
154166
154835
|
return process.env.WALNUT_AGENT_SEARCH_ENGINE !== "inprocess";
|
|
154167
154836
|
}
|
|
@@ -154241,7 +154910,7 @@ async function claudeCliEngine(userPrompt, options) {
|
|
|
154241
154910
|
model: options.model,
|
|
154242
154911
|
timeoutMs: options.timeoutMs,
|
|
154243
154912
|
tools: ["Bash"],
|
|
154244
|
-
toolUseId: `task-search-${
|
|
154913
|
+
toolUseId: `task-search-${randomUUID13()}`,
|
|
154245
154914
|
warm: true,
|
|
154246
154915
|
onBlock
|
|
154247
154916
|
});
|
|
@@ -155260,9 +155929,13 @@ var init_credential_verify = __esm({
|
|
|
155260
155929
|
var config_exports = {};
|
|
155261
155930
|
__export(config_exports, {
|
|
155262
155931
|
buildProvidersPayload: () => buildProvidersPayload,
|
|
155263
|
-
configRouter: () => configRouter
|
|
155932
|
+
configRouter: () => configRouter,
|
|
155933
|
+
setStaticRootReporter: () => setStaticRootReporter
|
|
155264
155934
|
});
|
|
155265
155935
|
import { Router as Router20 } from "express";
|
|
155936
|
+
function setStaticRootReporter(fn) {
|
|
155937
|
+
staticRootReporter = fn;
|
|
155938
|
+
}
|
|
155266
155939
|
async function fetchOllamaModels(baseUrl) {
|
|
155267
155940
|
if (Date.now() - _ollamaCache.ts < OLLAMA_CACHE_TTL) return _ollamaCache.models;
|
|
155268
155941
|
const url = (baseUrl ?? DEFAULT_BASE_URLS["ollama"] ?? "http://localhost:11434") + "/api/tags";
|
|
@@ -155315,10 +155988,11 @@ async function buildProvidersPayload() {
|
|
|
155315
155988
|
providers[name] = {
|
|
155316
155989
|
api: prov.api,
|
|
155317
155990
|
base_url: prov.base_url,
|
|
155318
|
-
status: caps.
|
|
155991
|
+
status: caps.installed ? "ready" : "no_key",
|
|
155319
155992
|
auto_detected: !explicitNames.has(name),
|
|
155320
155993
|
models: getModelsForProvider(name, prov.models),
|
|
155321
|
-
credential_source: caps.
|
|
155994
|
+
credential_source: caps.installed ? `cli_${caps.auth.mode}` : "cli_not_installed",
|
|
155995
|
+
credential_detail: caps.installed ? caps.auth.label : void 0
|
|
155322
155996
|
};
|
|
155323
155997
|
continue;
|
|
155324
155998
|
}
|
|
@@ -155377,10 +156051,11 @@ async function buildProvidersPayload() {
|
|
|
155377
156051
|
const caps = detectClaudeCli2();
|
|
155378
156052
|
providers[name] = {
|
|
155379
156053
|
api: template.api,
|
|
155380
|
-
status: caps.
|
|
156054
|
+
status: caps.installed ? "ready" : "no_key",
|
|
155381
156055
|
auto_detected: false,
|
|
155382
156056
|
models: getModelsForProvider(name),
|
|
155383
|
-
credential_source: caps.
|
|
156057
|
+
credential_source: caps.installed ? `cli_${caps.auth.mode}` : "cli_not_installed",
|
|
156058
|
+
credential_detail: caps.installed ? caps.auth.label : void 0
|
|
155384
156059
|
};
|
|
155385
156060
|
continue;
|
|
155386
156061
|
}
|
|
@@ -155395,7 +156070,7 @@ async function buildProvidersPayload() {
|
|
|
155395
156070
|
}
|
|
155396
156071
|
return { providers };
|
|
155397
156072
|
}
|
|
155398
|
-
var configRouter, _ollamaCache, OLLAMA_CACHE_TTL;
|
|
156073
|
+
var configRouter, staticRootReporter, _ollamaCache, OLLAMA_CACHE_TTL;
|
|
155399
156074
|
var init_config11 = __esm({
|
|
155400
156075
|
"src/web/routes/config.ts"() {
|
|
155401
156076
|
"use strict";
|
|
@@ -155410,6 +156085,7 @@ var init_config11 = __esm({
|
|
|
155410
156085
|
init_defaults();
|
|
155411
156086
|
init_config_redact();
|
|
155412
156087
|
configRouter = Router20();
|
|
156088
|
+
staticRootReporter = null;
|
|
155413
156089
|
configRouter.get("/", async (_req, res, next) => {
|
|
155414
156090
|
try {
|
|
155415
156091
|
const config = await getConfig();
|
|
@@ -155436,7 +156112,8 @@ var init_config11 = __esm({
|
|
|
155436
156112
|
remoteIdUniquenessGaps = getExtIndexUniquenessGaps2();
|
|
155437
156113
|
} catch {
|
|
155438
156114
|
}
|
|
155439
|
-
|
|
156115
|
+
const webAssets = staticRootReporter?.() ?? null;
|
|
156116
|
+
res.json({ config: CLOUD_MODE ? redactConfig(config) : config, envTokenHint, installDir: CLOUD_MODE ? null : WALNUT_INSTALL_DIR, notesDir: CLOUD_MODE ? null : NOTES_DIR, processNice, memory, canRevealLocalFiles, remoteIdUniquenessGaps, cloud: CLOUD_MODE, webAssets });
|
|
155440
156117
|
} catch (err2) {
|
|
155441
156118
|
next(err2);
|
|
155442
156119
|
}
|
|
@@ -155534,7 +156211,7 @@ var init_config11 = __esm({
|
|
|
155534
156211
|
const {
|
|
155535
156212
|
traceCredentialResolution: traceCredentialResolution2,
|
|
155536
156213
|
resolveCredentials: resolveCredentials2,
|
|
155537
|
-
readClaudeSettingsEnv:
|
|
156214
|
+
readClaudeSettingsEnv: readClaudeSettingsEnv3,
|
|
155538
156215
|
readClaudeCredentialExport: readClaudeCredentialExport2
|
|
155539
156216
|
} = await Promise.resolve().then(() => (init_credential_resolver(), credential_resolver_exports));
|
|
155540
156217
|
const fs114 = await import("fs");
|
|
@@ -155550,7 +156227,7 @@ var init_config11 = __esm({
|
|
|
155550
156227
|
};
|
|
155551
156228
|
const trace = traceCredentialResolution2({
|
|
155552
156229
|
config,
|
|
155553
|
-
claudeEnv:
|
|
156230
|
+
claudeEnv: readClaudeSettingsEnv3(),
|
|
155554
156231
|
processEnv: process.env,
|
|
155555
156232
|
awsFiles: {
|
|
155556
156233
|
credentials: exists3(path185.join(home, ".aws", "credentials")),
|
|
@@ -159226,6 +159903,9 @@ var init_session_chat = __esm({
|
|
|
159226
159903
|
|
|
159227
159904
|
// src/web/routes/browser-logs.ts
|
|
159228
159905
|
import { Router as Router31 } from "express";
|
|
159906
|
+
function entrySubsystem(entry) {
|
|
159907
|
+
return entry.subsystem && ENTRY_SUBSYSTEMS.has(entry.subsystem) ? entry.subsystem : "browser";
|
|
159908
|
+
}
|
|
159229
159909
|
function checkRate(client, entryCount) {
|
|
159230
159910
|
const now = Date.now();
|
|
159231
159911
|
let state9 = clientRates.get(client);
|
|
@@ -159267,10 +159947,11 @@ function writeEntries(entries) {
|
|
|
159267
159947
|
if (entry.args) meta.args = truncate(entry.args, 1e3);
|
|
159268
159948
|
if (entry.url) meta.url = entry.url;
|
|
159269
159949
|
if (entry.count && entry.count > 1) meta.count = entry.count;
|
|
159950
|
+
if (entry.client === "mac-app") meta.client = entry.client;
|
|
159270
159951
|
writeLogEntry({
|
|
159271
159952
|
time: entry.time,
|
|
159272
159953
|
level: mapLevel(entry.level),
|
|
159273
|
-
subsystem:
|
|
159954
|
+
subsystem: entrySubsystem(entry),
|
|
159274
159955
|
message: truncate(entry.message, 2e3),
|
|
159275
159956
|
browserLevel: entry.level,
|
|
159276
159957
|
// preserve original level (e.g. 'log' vs 'info')
|
|
@@ -159293,13 +159974,14 @@ function registerBrowserLogsRpc() {
|
|
|
159293
159974
|
writeEntries(entries);
|
|
159294
159975
|
});
|
|
159295
159976
|
}
|
|
159296
|
-
var RATE_WINDOW_MS, RATE_LIMIT, clientRates, REST_MAX_ENTRIES, browserLogsRouter;
|
|
159977
|
+
var ENTRY_SUBSYSTEMS, RATE_WINDOW_MS, RATE_LIMIT, clientRates, REST_MAX_ENTRIES, browserLogsRouter;
|
|
159297
159978
|
var init_browser_logs = __esm({
|
|
159298
159979
|
"src/web/routes/browser-logs.ts"() {
|
|
159299
159980
|
"use strict";
|
|
159300
159981
|
init_handler();
|
|
159301
159982
|
init_logging();
|
|
159302
159983
|
init_logger();
|
|
159984
|
+
ENTRY_SUBSYSTEMS = /* @__PURE__ */ new Set(["browser", "desktop"]);
|
|
159303
159985
|
RATE_WINDOW_MS = 1e4;
|
|
159304
159986
|
RATE_LIMIT = 500;
|
|
159305
159987
|
clientRates = /* @__PURE__ */ new WeakMap();
|
|
@@ -163643,6 +164325,7 @@ var init_session_health_monitor = __esm({
|
|
|
163643
164325
|
log.session.info("health monitor: drained orphan dead-pool", { orphanCount: orphanIds.length, written: written.length });
|
|
163644
164326
|
}
|
|
163645
164327
|
if (sessions3.length === 0) {
|
|
164328
|
+
if (!ctx.overBudget()) await this.checkSnapshotPull(sessions3, ctx, allSessions);
|
|
163646
164329
|
const total = Date.now() - checkT0;
|
|
163647
164330
|
if (total > 500) log.session.warn("health monitor: check() slow (no active sessions)", { totalMs: total, orphanMs: tOrphan - checkT0, recoverMs: tRecover - tOrphan });
|
|
163648
164331
|
return;
|
|
@@ -163672,7 +164355,7 @@ var init_session_health_monitor = __esm({
|
|
|
163672
164355
|
await this.reconcilePendingBackgroundTasks(sessions3);
|
|
163673
164356
|
endPhase("pendingBgTasks");
|
|
163674
164357
|
if (ctx.overBudget()) return;
|
|
163675
|
-
await this.checkSnapshotPull(sessions3, ctx);
|
|
164358
|
+
await this.checkSnapshotPull(sessions3, ctx, allSessions);
|
|
163676
164359
|
endPhase("snapshotPull");
|
|
163677
164360
|
if (ctx.overBudget()) return;
|
|
163678
164361
|
const reconciledIds = await this.reconcileStuckRunningSessions(sessions3, taskMap);
|
|
@@ -164166,15 +164849,39 @@ var init_session_health_monitor = __esm({
|
|
|
164166
164849
|
bus.emit(EventNames.SESSION_WILL_REAP, payload, ["*"], { source: "health-monitor" });
|
|
164167
164850
|
}
|
|
164168
164851
|
/**
|
|
164169
|
-
* C2 snapshot pull channel (contract §5): every 30s tick,
|
|
164170
|
-
*
|
|
164171
|
-
*
|
|
164172
|
-
*
|
|
164173
|
-
*
|
|
164852
|
+
* C2 snapshot pull channel (contract §5): every 30s tick, PULL getState from
|
|
164853
|
+
* the host's POOLED CONNECTED DaemonConnection advertising 'snapshot-v1' and
|
|
164854
|
+
* feed the answer to applySnapshot. NEVER dials a new connection, always
|
|
164855
|
+
* sequential. TWO candidate classes with SEPARATE budgets and cadences:
|
|
164856
|
+
*
|
|
164857
|
+
* - LIVE — {running, idle}: 10 sids/tick, 25s per-sid spacing. The steady
|
|
164858
|
+
* state channel.
|
|
164859
|
+
* - RE-EXAMINATION — {error, stopped} whose cause is NOT positively terminal
|
|
164860
|
+
* (classifySessionError) and whose last_status_change is inside
|
|
164861
|
+
* RESCUABLE_STOPPED_WINDOW_MS: 5 sids/tick, 5 min per-sid spacing. Same
|
|
164862
|
+
* engine/provider/awaiting_spawn/archived exclusions as the live class.
|
|
164863
|
+
*
|
|
164864
|
+
* WHY RE-EXAMINATION EXISTS — do not "optimize" the error class back out. A
|
|
164865
|
+
* wrongly-'error' record was a two-sided deadlock: this lane is the ONE writer
|
|
164866
|
+
* the enforce-mode gate sanctions for a snapshot-covered session, and while it
|
|
164867
|
+
* looked at live records only it never re-derived status for an 'error' one.
|
|
164868
|
+
* recoverInfraFailedSessions DOES look, but the fix it writes
|
|
164869
|
+
* (('health-monitor','auto_recovered_dead')) is itself category-①, so the gate
|
|
164870
|
+
* drops the whole patch — the lane allowed to write refused to look, and the
|
|
164871
|
+
* lane that looked was not allowed to write. Records froze in 'error' with
|
|
164872
|
+
* "Connection lost — unable to reach remote host" for hours after their host
|
|
164873
|
+
* came back (2026-09-03, a remote dev host whose tunnel flapped: prose still on
|
|
164874
|
+
* the record 2h after the reconnect), burning a 30s log-churn loop the whole
|
|
164875
|
+
* time. Terminality is decided STRUCTURALLY, never by matching errorMessage
|
|
164876
|
+
* prose — see the header of session-error-kind.ts for why that ban exists.
|
|
164877
|
+
*
|
|
164878
|
+
* Re-examination candidates come from a WIDER pool than the live ones: the
|
|
164879
|
+
* tick's own working set drops every 'error' row (isTerminalSession), so the
|
|
164880
|
+
* caller passes the unfiltered health-scan set for this class only.
|
|
164174
164881
|
*
|
|
164175
|
-
*
|
|
164882
|
+
* THREE fairness/budget properties both classes must keep (C8/C9/C12/C29):
|
|
164176
164883
|
*
|
|
164177
|
-
* -
|
|
164884
|
+
* - The TickContext budget is honored INSIDE the loop (same discipline as
|
|
164178
164885
|
* checkHungSessions). Each iteration is a real daemon RPC with a
|
|
164179
164886
|
* probe-timeout ceiling, so 10 slow hosts could burn 10 × the probe
|
|
164180
164887
|
* timeout sequentially BEFORE the authoritative reconcile phases that run
|
|
@@ -164183,70 +164890,101 @@ var init_session_health_monitor = __esm({
|
|
|
164183
164890
|
* next tick (see the rotation below).
|
|
164184
164891
|
*
|
|
164185
164892
|
* - Candidates are ordered by lastPullAt ASCENDING (never-pulled first),
|
|
164186
|
-
* which turns
|
|
164187
|
-
*
|
|
164188
|
-
*
|
|
164189
|
-
*
|
|
164893
|
+
* which turns each cap into a round-robin. In list order the SAME first ten
|
|
164894
|
+
* sids were pulled every tick and — because the 25s spacing is shorter than
|
|
164895
|
+
* the 30s cadence — sids 11+ were never pulled at all: the pull channel
|
|
164896
|
+
* silently did not exist for them. Each class rotates on its OWN timestamps.
|
|
164897
|
+
*
|
|
164898
|
+
* - The classes cannot spend each other's budget. Re-examination is
|
|
164899
|
+
* speculative ("is this record still true?") and a settled-death cohort
|
|
164900
|
+
* dwarfs the live set (the live DB measured 100 stopped candidates against
|
|
164901
|
+
* 4 error rows), so it must cost a handful of cheap RPCs per tick and
|
|
164902
|
+
* nothing more — the same reasoning as MAX_STOPPED_PROBES_PER_TICK below.
|
|
164190
164903
|
*/
|
|
164191
164904
|
snapshotPullAt = /* @__PURE__ */ new Map();
|
|
164192
|
-
|
|
164905
|
+
/** Separate from snapshotPullAt on purpose: the two classes rotate
|
|
164906
|
+
* independently, so a live pull can never reset the slow re-examination
|
|
164907
|
+
* cadence (or vice versa) when a record moves between them. */
|
|
164908
|
+
snapshotReexamPullAt = /* @__PURE__ */ new Map();
|
|
164909
|
+
async checkSnapshotPull(sessions3, ctx, reexamPool = sessions3) {
|
|
164193
164910
|
const PULL_MIN_GAP_MS = 25e3;
|
|
164194
164911
|
const MAX_PULLS_PER_TICK = 10;
|
|
164912
|
+
const REEXAM_MIN_GAP_MS = 5 * 6e4;
|
|
164913
|
+
const MAX_REEXAM_PULLS_PER_TICK = 5;
|
|
164195
164914
|
try {
|
|
164196
164915
|
const { getSnapshotStatusMode: getSnapshotStatusMode2 } = await Promise.resolve().then(() => (init_session_snapshot_gate(), session_snapshot_gate_exports));
|
|
164197
164916
|
if (getSnapshotStatusMode2() === "off") return;
|
|
164198
164917
|
const { getPooledSnapshotConnection: getPooledSnapshotConnection2 } = await Promise.resolve().then(() => (init_daemon_connection(), daemon_connection_exports));
|
|
164199
164918
|
const { applySnapshot: applySnapshot2 } = await Promise.resolve().then(() => (init_session_snapshot_apply(), session_snapshot_apply_exports));
|
|
164200
164919
|
const now = Date.now();
|
|
164201
|
-
const
|
|
164202
|
-
if (s4.
|
|
164920
|
+
const pullable = (s4) => {
|
|
164921
|
+
if (s4.archived) return false;
|
|
164203
164922
|
if (!engineCaps(s4.engine).snapshotPull) return false;
|
|
164204
164923
|
if (s4.provider === "embedded" || s4.provider === "sdk") return false;
|
|
164205
|
-
|
|
164206
|
-
|
|
164207
|
-
|
|
164208
|
-
|
|
164209
|
-
|
|
164210
|
-
|
|
164211
|
-
|
|
164212
|
-
|
|
164213
|
-
|
|
164214
|
-
|
|
164215
|
-
|
|
164216
|
-
|
|
164217
|
-
|
|
164218
|
-
|
|
164219
|
-
|
|
164220
|
-
|
|
164221
|
-
|
|
164222
|
-
|
|
164223
|
-
|
|
164224
|
-
|
|
164225
|
-
|
|
164226
|
-
|
|
164227
|
-
|
|
164228
|
-
|
|
164229
|
-
|
|
164230
|
-
|
|
164231
|
-
|
|
164232
|
-
|
|
164233
|
-
|
|
164234
|
-
|
|
164235
|
-
|
|
164236
|
-
|
|
164237
|
-
|
|
164238
|
-
|
|
164239
|
-
|
|
164240
|
-
|
|
164241
|
-
|
|
164924
|
+
return s4.status_reason !== "awaiting_spawn";
|
|
164925
|
+
};
|
|
164926
|
+
const byPullAge = (at) => (a8, b8) => (at.get(a8.claudeSessionId) ?? 0) - (at.get(b8.claudeSessionId) ?? 0);
|
|
164927
|
+
const live = sessions3.filter((s4) => {
|
|
164928
|
+
if (s4.process_status !== "running" && s4.process_status !== "idle") return false;
|
|
164929
|
+
if (!pullable(s4)) return false;
|
|
164930
|
+
return now - (this.snapshotPullAt.get(s4.claudeSessionId) ?? 0) >= PULL_MIN_GAP_MS;
|
|
164931
|
+
}).sort(byPullAge(this.snapshotPullAt));
|
|
164932
|
+
const reexam = reexamPool.filter((s4) => {
|
|
164933
|
+
if (s4.process_status !== "error" && s4.process_status !== "stopped") return false;
|
|
164934
|
+
if (!pullable(s4)) return false;
|
|
164935
|
+
if (classifySessionError(s4) === "terminal") return false;
|
|
164936
|
+
const changedAt = s4.last_status_change ? Date.parse(s4.last_status_change) : NaN;
|
|
164937
|
+
if (!Number.isFinite(changedAt)) return false;
|
|
164938
|
+
if (now - changedAt > RESCUABLE_STOPPED_WINDOW_MS) return false;
|
|
164939
|
+
return now - (this.snapshotReexamPullAt.get(s4.claudeSessionId) ?? 0) >= REEXAM_MIN_GAP_MS;
|
|
164940
|
+
}).sort(byPullAge(this.snapshotReexamPullAt));
|
|
164941
|
+
let abandoned = false;
|
|
164942
|
+
const runClass = async (candidates, cap2, at, candidateClass) => {
|
|
164943
|
+
let pulled = 0;
|
|
164944
|
+
for (const s4 of candidates) {
|
|
164945
|
+
if (ctx?.overBudget()) {
|
|
164946
|
+
abandoned = true;
|
|
164947
|
+
log.session.warn("health monitor: checkSnapshotPull abandoned mid-loop (over budget)", {
|
|
164948
|
+
pulled,
|
|
164949
|
+
candidateCount: candidates.length,
|
|
164950
|
+
candidateClass
|
|
164951
|
+
});
|
|
164952
|
+
break;
|
|
164953
|
+
}
|
|
164954
|
+
const conn = getPooledSnapshotConnection2(s4.host);
|
|
164955
|
+
if (!conn) continue;
|
|
164956
|
+
if (pulled >= cap2) {
|
|
164957
|
+
log.session.info("health monitor: snapshot pull capped this tick", {
|
|
164958
|
+
cap: cap2,
|
|
164959
|
+
sessionCount: sessions3.length,
|
|
164960
|
+
candidateCount: candidates.length,
|
|
164961
|
+
candidateClass
|
|
164962
|
+
});
|
|
164963
|
+
break;
|
|
164964
|
+
}
|
|
164965
|
+
pulled++;
|
|
164966
|
+
at.set(s4.claudeSessionId, now);
|
|
164967
|
+
try {
|
|
164968
|
+
const resp = await probeWithTimeout(
|
|
164969
|
+
conn.send("getState", { sid: s4.claudeSessionId }),
|
|
164970
|
+
null,
|
|
164971
|
+
"snapshot-pull-getState",
|
|
164972
|
+
s4.claudeSessionId
|
|
164973
|
+
);
|
|
164974
|
+
const snapshot2 = resp?.ok ? resp.snapshot : void 0;
|
|
164975
|
+
if (snapshot2) await applySnapshot2(s4.claudeSessionId, snapshot2, "pull-30s");
|
|
164976
|
+
} catch (err2) {
|
|
164977
|
+
log.session.debug("health monitor: snapshot pull failed", {
|
|
164978
|
+
sessionId: s4.claudeSessionId,
|
|
164979
|
+
host: s4.host ?? "__local__",
|
|
164980
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
164981
|
+
});
|
|
164982
|
+
}
|
|
164242
164983
|
}
|
|
164243
|
-
}
|
|
164244
|
-
|
|
164245
|
-
|
|
164246
|
-
|
|
164247
|
-
sessionCount: sessions3.length,
|
|
164248
|
-
candidateCount: candidates.length
|
|
164249
|
-
});
|
|
164984
|
+
};
|
|
164985
|
+
await runClass(live, MAX_PULLS_PER_TICK, this.snapshotPullAt, "live");
|
|
164986
|
+
if (!abandoned) {
|
|
164987
|
+
await runClass(reexam, MAX_REEXAM_PULLS_PER_TICK, this.snapshotReexamPullAt, "reexam");
|
|
164250
164988
|
}
|
|
164251
164989
|
if (this.snapshotPullAt.size > 200) {
|
|
164252
164990
|
const liveIds = new Set(sessions3.map((s4) => s4.claudeSessionId));
|
|
@@ -164254,6 +164992,12 @@ var init_session_health_monitor = __esm({
|
|
|
164254
164992
|
if (!liveIds.has(sid)) this.snapshotPullAt.delete(sid);
|
|
164255
164993
|
}
|
|
164256
164994
|
}
|
|
164995
|
+
if (this.snapshotReexamPullAt.size > 200) {
|
|
164996
|
+
const poolIds = new Set(reexamPool.map((s4) => s4.claudeSessionId));
|
|
164997
|
+
for (const sid of this.snapshotReexamPullAt.keys()) {
|
|
164998
|
+
if (!poolIds.has(sid)) this.snapshotReexamPullAt.delete(sid);
|
|
164999
|
+
}
|
|
165000
|
+
}
|
|
164257
165001
|
} catch (err2) {
|
|
164258
165002
|
log.session.warn("health monitor: checkSnapshotPull failed", {
|
|
164259
165003
|
error: err2 instanceof Error ? err2.message : String(err2)
|
|
@@ -164416,6 +165160,31 @@ var init_session_health_monitor = __esm({
|
|
|
164416
165160
|
});
|
|
164417
165161
|
}
|
|
164418
165162
|
}
|
|
165163
|
+
/**
|
|
165164
|
+
* Can a scheduled auto-recovery actually FIRE for this record?
|
|
165165
|
+
*
|
|
165166
|
+
* fire() (session-auto-recover) hard-requires the linked task to still be
|
|
165167
|
+
* IN_PROGRESS — AGENT_COMPLETE means the work was already handed back and
|
|
165168
|
+
* resuming would talk over the human. Arming a recovery that cannot pass that
|
|
165169
|
+
* check is not free: the 30s tick re-armed it forever and each arming logged
|
|
165170
|
+
* an abort 20s later (measured running for 2+ hours on one session), while
|
|
165171
|
+
* `if (armed) continue` also skipped the phase sync that IS appropriate there.
|
|
165172
|
+
*
|
|
165173
|
+
* A lookup that cannot answer (no task id, deleted task, DB error) returns
|
|
165174
|
+
* true: only a KNOWN non-IN_PROGRESS phase blocks arming, so a transient read
|
|
165175
|
+
* failure can never disable recovery. schedule()'s own guards still apply.
|
|
165176
|
+
*/
|
|
165177
|
+
async autoRecoverCanFire(s4) {
|
|
165178
|
+
if (!s4.taskId) return true;
|
|
165179
|
+
try {
|
|
165180
|
+
const { getTask: getTask2 } = await Promise.resolve().then(() => (init_task_manager(), task_manager_exports));
|
|
165181
|
+
const task = await getTask2(s4.taskId);
|
|
165182
|
+
if (!task) return true;
|
|
165183
|
+
return task.phase === "IN_PROGRESS";
|
|
165184
|
+
} catch {
|
|
165185
|
+
return true;
|
|
165186
|
+
}
|
|
165187
|
+
}
|
|
164419
165188
|
/**
|
|
164420
165189
|
* Auto-recover remote sessions stuck in 'error' with "Connection lost" message.
|
|
164421
165190
|
*
|
|
@@ -164502,28 +165271,31 @@ var init_session_health_monitor = __esm({
|
|
|
164502
165271
|
} else if (wasStopped) {
|
|
164503
165272
|
continue;
|
|
164504
165273
|
} else {
|
|
164505
|
-
const
|
|
164506
|
-
|
|
164507
|
-
|
|
164508
|
-
|
|
164509
|
-
|
|
164510
|
-
|
|
164511
|
-
|
|
164512
|
-
|
|
164513
|
-
|
|
164514
|
-
|
|
164515
|
-
|
|
164516
|
-
|
|
164517
|
-
|
|
164518
|
-
|
|
164519
|
-
|
|
164520
|
-
|
|
164521
|
-
|
|
164522
|
-
|
|
164523
|
-
|
|
164524
|
-
|
|
165274
|
+
const { isSnapshotCovered: isSnapshotCovered2 } = await Promise.resolve().then(() => (init_session_snapshot_gate(), session_snapshot_gate_exports));
|
|
165275
|
+
if (!isSnapshotCovered2(s4.claudeSessionId)) {
|
|
165276
|
+
const updated = await updateSessionRecord2(s4.claudeSessionId, {
|
|
165277
|
+
process_status: "stopped",
|
|
165278
|
+
errorMessage: void 0,
|
|
165279
|
+
errorKind: "infra",
|
|
165280
|
+
activity: void 0,
|
|
165281
|
+
last_status_change: now,
|
|
165282
|
+
status_reason: "auto_recovered_dead",
|
|
165283
|
+
status_changed_by: "health-monitor"
|
|
165284
|
+
});
|
|
165285
|
+
emitSessionStatusChanged(
|
|
165286
|
+
updated,
|
|
165287
|
+
{},
|
|
165288
|
+
["*"],
|
|
165289
|
+
{ source: "health-monitor", urgency: "urgent" }
|
|
165290
|
+
);
|
|
165291
|
+
log.session.info("health monitor: auto-recovered connection-lost session (process dead)", {
|
|
165292
|
+
sessionId: s4.claudeSessionId,
|
|
165293
|
+
host: s4.host,
|
|
165294
|
+
alive: false
|
|
165295
|
+
});
|
|
165296
|
+
}
|
|
164525
165297
|
const { scheduleSessionAutoRecover: scheduleSessionAutoRecover2 } = await Promise.resolve().then(() => (init_session_auto_recover(), session_auto_recover_exports));
|
|
164526
|
-
const armed = scheduleSessionAutoRecover2(s4, s4.status_reason ?? "daemon_reported_exit");
|
|
165298
|
+
const armed = await this.autoRecoverCanFire(s4) && scheduleSessionAutoRecover2(s4, s4.status_reason ?? "daemon_reported_exit");
|
|
164527
165299
|
if (armed) continue;
|
|
164528
165300
|
if (s4.taskId) {
|
|
164529
165301
|
try {
|
|
@@ -172673,6 +173445,20 @@ var init_session_lifecycle_v1 = __esm({
|
|
|
172673
173445
|
next(err2);
|
|
172674
173446
|
}
|
|
172675
173447
|
});
|
|
173448
|
+
sessionLifecycleV1Router.post("/sessions/:id/recheck", async (req, res, next) => {
|
|
173449
|
+
try {
|
|
173450
|
+
const sessionId = validSid2(req, res);
|
|
173451
|
+
if (!sessionId) return;
|
|
173452
|
+
if (CLOUD_MODE) {
|
|
173453
|
+
await relayControlAction(res, "recheck", sessionId, void 0, 200);
|
|
173454
|
+
return;
|
|
173455
|
+
}
|
|
173456
|
+
const { recheckSession: recheckSession2 } = await Promise.resolve().then(() => (init_session_lifecycle(), session_lifecycle_exports));
|
|
173457
|
+
await runLocal(res, next, 200, () => recheckSession2(sessionId));
|
|
173458
|
+
} catch (err2) {
|
|
173459
|
+
next(err2);
|
|
173460
|
+
}
|
|
173461
|
+
});
|
|
172676
173462
|
sessionLifecycleV1Router.post("/sessions/:id/permission", async (req, res, next) => {
|
|
172677
173463
|
try {
|
|
172678
173464
|
const sessionId = validSid2(req, res);
|
|
@@ -172771,7 +173557,7 @@ __export(task_start_exports, {
|
|
|
172771
173557
|
SessionExistsError: () => SessionExistsError,
|
|
172772
173558
|
startSessionForTask: () => startSessionForTask
|
|
172773
173559
|
});
|
|
172774
|
-
import { randomUUID as
|
|
173560
|
+
import { randomUUID as randomUUID14 } from "crypto";
|
|
172775
173561
|
async function startSessionForTask(params) {
|
|
172776
173562
|
const { taskIdPrefix, source } = params;
|
|
172777
173563
|
let task;
|
|
@@ -172822,7 +173608,7 @@ async function startSessionForTask(params) {
|
|
|
172822
173608
|
}
|
|
172823
173609
|
let message = params.message?.trim() || `Working on task: ${task.title}`;
|
|
172824
173610
|
const engine = normalizeEngine(params.engine);
|
|
172825
|
-
const preassignedSessionId = engineCaps(engine).idProvisioning === "provider-issued" ? void 0 :
|
|
173611
|
+
const preassignedSessionId = engineCaps(engine).idProvisioning === "provider-issued" ? void 0 : randomUUID14();
|
|
172826
173612
|
let requestId;
|
|
172827
173613
|
if (params.expectReply) {
|
|
172828
173614
|
const { resolveCaller: resolveCaller2 } = await Promise.resolve().then(() => (init_session_send_core(), session_send_core_exports));
|
|
@@ -184150,7 +184936,7 @@ async function generateLedgerDesc(task, timeoutMs = 15e3) {
|
|
|
184150
184936
|
try {
|
|
184151
184937
|
const { getConfig: getConfig2 } = await Promise.resolve().then(() => (init_config_manager(), config_manager_exports));
|
|
184152
184938
|
const config = await getConfig2();
|
|
184153
|
-
const providerName = config
|
|
184939
|
+
const providerName = resolveMainProviderName(config);
|
|
184154
184940
|
const model = cheapModelFor(providerName);
|
|
184155
184941
|
const detail = (task.description ?? "").trim().slice(0, 800);
|
|
184156
184942
|
const project = (task.project ?? "").trim();
|
|
@@ -184209,6 +184995,7 @@ var init_task_ledger_desc = __esm({
|
|
|
184209
184995
|
"use strict";
|
|
184210
184996
|
init_model();
|
|
184211
184997
|
init_model_catalog();
|
|
184998
|
+
init_default_provider();
|
|
184212
184999
|
init_logging();
|
|
184213
185000
|
init_task_manager();
|
|
184214
185001
|
MAX_DESC_LEN = 120;
|
|
@@ -184232,6 +185019,10 @@ function diffStreamedVsPersisted(streamedTextMsgIds, persistedMsgIds) {
|
|
|
184232
185019
|
}
|
|
184233
185020
|
function armStreamConvergenceCheck(sessionId, streamedTextMsgIds) {
|
|
184234
185021
|
if (streamedTextMsgIds.length === 0) return;
|
|
185022
|
+
if (consumeWarmupTurn(sessionId)) {
|
|
185023
|
+
log.obs.debug("stream-convergence: skipped hidden warm-up turn", { sessionId });
|
|
185024
|
+
return;
|
|
185025
|
+
}
|
|
184235
185026
|
const timer3 = setTimeout(() => {
|
|
184236
185027
|
void runCheck(sessionId, streamedTextMsgIds).catch((err2) => {
|
|
184237
185028
|
log.obs.warn("stream-convergence check failed", {
|
|
@@ -184297,6 +185088,7 @@ var init_stream_convergence = __esm({
|
|
|
184297
185088
|
"src/core/observability/stream-convergence.ts"() {
|
|
184298
185089
|
"use strict";
|
|
184299
185090
|
init_logging();
|
|
185091
|
+
init_side_thread_warmup();
|
|
184300
185092
|
CHECK_DELAY_MS = 15e3;
|
|
184301
185093
|
INCIDENT_DEDUPE_MS = 30 * 6e4;
|
|
184302
185094
|
lastIncidentAt = /* @__PURE__ */ new Map();
|
|
@@ -184547,12 +185339,17 @@ function getSystemHealth() {
|
|
|
184547
185339
|
}
|
|
184548
185340
|
async function refreshSystemHealth() {
|
|
184549
185341
|
systemHealth.claudeCliAvailable = checkClaudeCliAvailable();
|
|
184550
|
-
|
|
185342
|
+
applyCredentialHealth(await resolveCredentialHealth());
|
|
185343
|
+
broadcastEvent("system:health", systemHealth);
|
|
185344
|
+
return systemHealth;
|
|
185345
|
+
}
|
|
185346
|
+
function applyCredentialHealth(cred) {
|
|
184551
185347
|
systemHealth.hasReadyProvider = cred.hasReadyProvider;
|
|
184552
185348
|
systemHealth.credentialSource = cred.source;
|
|
184553
185349
|
systemHealth.credentialDetail = cred.detail;
|
|
184554
|
-
|
|
184555
|
-
|
|
185350
|
+
systemHealth.mainProvider = cred.mainProvider;
|
|
185351
|
+
systemHealth.mainProviderImplicit = cred.mainProviderImplicit;
|
|
185352
|
+
systemHealth.claudeCliAuth = cred.claudeCliAuth;
|
|
184556
185353
|
}
|
|
184557
185354
|
function checkClaudeCliAvailable() {
|
|
184558
185355
|
return isClaudeCliInstalled();
|
|
@@ -184562,31 +185359,50 @@ async function resolveCredentialHealth() {
|
|
|
184562
185359
|
const { getConfig: getConfig2 } = await Promise.resolve().then(() => (init_config_manager(), config_manager_exports));
|
|
184563
185360
|
const { resolveCredentials: resolveCredentials2 } = await Promise.resolve().then(() => (init_credential_resolver(), credential_resolver_exports));
|
|
184564
185361
|
const { buildProviderMap: buildProviderMap2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
|
|
185362
|
+
const { resolveMainProviderName: resolveMainProviderName2, mainProviderIsImplicit: mainProviderIsImplicit2 } = await Promise.resolve().then(() => (init_default_provider(), default_provider_exports));
|
|
185363
|
+
const { detectClaudeCli: detectClaudeCli2 } = await Promise.resolve().then(() => (init_claude_cli_detect(), claude_cli_detect_exports));
|
|
184565
185364
|
const config = await getConfig2();
|
|
185365
|
+
const cli = detectClaudeCli2();
|
|
185366
|
+
const mainProvider = resolveMainProviderName2(config, cli.installed);
|
|
185367
|
+
const base = {
|
|
185368
|
+
mainProvider,
|
|
185369
|
+
mainProviderImplicit: mainProviderIsImplicit2(config),
|
|
185370
|
+
claudeCliAuth: cli.installed ? cli.auth.label : void 0
|
|
185371
|
+
};
|
|
185372
|
+
if (mainProvider === "claude_cli") {
|
|
185373
|
+
return cli.installed ? { ...base, hasReadyProvider: true, source: "config", detail: `claude-cli (${cli.auth.label})` } : { ...base, hasReadyProvider: false, source: "none" };
|
|
185374
|
+
}
|
|
184566
185375
|
const cred = resolveCredentials2(config);
|
|
184567
185376
|
if (cred.source !== "none") {
|
|
184568
|
-
return { hasReadyProvider: true, source: cred.source, detail: cred.detail };
|
|
185377
|
+
return { ...base, hasReadyProvider: true, source: cred.source, detail: cred.detail };
|
|
184569
185378
|
}
|
|
184570
185379
|
const providers = buildProviderMap2(config.providers, config);
|
|
184571
185380
|
for (const [, prov] of Object.entries(providers)) {
|
|
184572
185381
|
if (prov.api === "bedrock" || prov.api === "ollama") continue;
|
|
184573
185382
|
if (prov.api === "claude-cli") {
|
|
184574
|
-
|
|
184575
|
-
if (detectClaudeCli2().subscriptionReady) {
|
|
184576
|
-
return { hasReadyProvider: true, source: "config", detail: "claude-cli (subscription)" };
|
|
184577
|
-
}
|
|
185383
|
+
if (cli.installed) return { ...base, hasReadyProvider: true, source: "config", detail: `claude-cli (${cli.auth.label})` };
|
|
184578
185384
|
continue;
|
|
184579
185385
|
}
|
|
184580
185386
|
const implemented = prov.api === "anthropic-messages" || prov.api === "openai-chat" || prov.api === "google-generative-ai";
|
|
184581
185387
|
if (implemented && (prov.api_key || prov.bearer_token)) {
|
|
184582
|
-
return { hasReadyProvider: true, source: "config", detail: prov.api };
|
|
185388
|
+
return { ...base, hasReadyProvider: true, source: "config", detail: prov.api };
|
|
184583
185389
|
}
|
|
184584
185390
|
}
|
|
184585
|
-
return { hasReadyProvider: false, source: "none" };
|
|
185391
|
+
return { ...base, hasReadyProvider: false, source: "none" };
|
|
184586
185392
|
} catch {
|
|
184587
185393
|
return { hasReadyProvider: false, source: "none" };
|
|
184588
185394
|
}
|
|
184589
185395
|
}
|
|
185396
|
+
async function isWalnutAnsweringOn(port) {
|
|
185397
|
+
try {
|
|
185398
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/system/health`, { signal: AbortSignal.timeout(1500) });
|
|
185399
|
+
if (!res.ok) return false;
|
|
185400
|
+
const body3 = await res.json();
|
|
185401
|
+
return !!body3 && typeof body3 === "object" && "hasReadyProvider" in body3;
|
|
185402
|
+
} catch {
|
|
185403
|
+
return false;
|
|
185404
|
+
}
|
|
185405
|
+
}
|
|
184590
185406
|
async function startServer(options = {}) {
|
|
184591
185407
|
if (httpServer) throw new Error("Server already running. Call stopServer() first.");
|
|
184592
185408
|
bootCompleted = false;
|
|
@@ -184611,11 +185427,11 @@ async function startServer(options = {}) {
|
|
|
184611
185427
|
}
|
|
184612
185428
|
{
|
|
184613
185429
|
const cred = await resolveCredentialHealth();
|
|
184614
|
-
|
|
184615
|
-
systemHealth.credentialSource = cred.source;
|
|
184616
|
-
systemHealth.credentialDetail = cred.detail;
|
|
185430
|
+
applyCredentialHealth(cred);
|
|
184617
185431
|
if (!systemHealth.hasReadyProvider) {
|
|
184618
185432
|
log.web.warn("No AI provider configured \u2014 configure one in Settings");
|
|
185433
|
+
} else if (cred.mainProvider === "claude_cli") {
|
|
185434
|
+
log.web.info("AI provider: Claude Code", { auth: cred.claudeCliAuth, implicit: cred.mainProviderImplicit });
|
|
184619
185435
|
} else if (cred.source && cred.source !== "config") {
|
|
184620
185436
|
log.web.info("Auto-detected Bedrock credential", { source: cred.source, detail: cred.detail });
|
|
184621
185437
|
}
|
|
@@ -185113,6 +185929,7 @@ async function startServer(options = {}) {
|
|
|
185113
185929
|
});
|
|
185114
185930
|
if (!dev) {
|
|
185115
185931
|
const staticDir = (() => {
|
|
185932
|
+
if (process.env.WALNUT_WEB_STATIC_DIR) return process.env.WALNUT_WEB_STATIC_DIR;
|
|
185116
185933
|
let dir2 = path184.dirname(fileURLToPath13(import.meta.url));
|
|
185117
185934
|
for (let i8 = 0; i8 < 5; i8++) {
|
|
185118
185935
|
const candidate = path184.join(dir2, "web", "static", "index.html");
|
|
@@ -185129,11 +185946,65 @@ async function startServer(options = {}) {
|
|
|
185129
185946
|
}
|
|
185130
185947
|
return path184.join(process.cwd(), "dist", "web", "static");
|
|
185131
185948
|
})();
|
|
185949
|
+
const mirrorDir = process.env.WALNUT_WEB_STATIC_MIRROR || path184.join(WALNUT_HOME, "cache", "web-static");
|
|
185950
|
+
let mirrorReady = false;
|
|
185951
|
+
try {
|
|
185952
|
+
const want = fs113.readFileSync(path184.join(staticDir, "index.html"), "utf-8");
|
|
185953
|
+
let have = null;
|
|
185954
|
+
try {
|
|
185955
|
+
have = fs113.readFileSync(path184.join(mirrorDir, "index.html"), "utf-8");
|
|
185956
|
+
} catch {
|
|
185957
|
+
}
|
|
185958
|
+
if (have !== want) {
|
|
185959
|
+
fs113.rmSync(mirrorDir, { recursive: true, force: true });
|
|
185960
|
+
fs113.mkdirSync(path184.dirname(mirrorDir), { recursive: true });
|
|
185961
|
+
fs113.cpSync(staticDir, mirrorDir, { recursive: true });
|
|
185962
|
+
}
|
|
185963
|
+
} catch (err2) {
|
|
185964
|
+
log.web.warn("could not refresh the web-asset mirror", {
|
|
185965
|
+
mirrorDir,
|
|
185966
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
185967
|
+
});
|
|
185968
|
+
}
|
|
185969
|
+
try {
|
|
185970
|
+
mirrorReady = fs113.statSync(path184.join(mirrorDir, "index.html")).isFile();
|
|
185971
|
+
} catch {
|
|
185972
|
+
mirrorReady = false;
|
|
185973
|
+
}
|
|
185132
185974
|
app.use(express3.static(staticDir));
|
|
185975
|
+
if (mirrorReady) app.use(express3.static(mirrorDir));
|
|
185976
|
+
const checkStaticRoot = () => {
|
|
185977
|
+
try {
|
|
185978
|
+
return fs113.statSync(path184.join(staticDir, "index.html")).isFile();
|
|
185979
|
+
} catch {
|
|
185980
|
+
return false;
|
|
185981
|
+
}
|
|
185982
|
+
};
|
|
185983
|
+
let staticRootOk = checkStaticRoot();
|
|
185984
|
+
if (!staticRootOk) {
|
|
185985
|
+
log.web.error("web assets are NOT servable at startup", { staticDir, mirrorReady });
|
|
185986
|
+
}
|
|
185987
|
+
const staticRootTimer = setInterval(() => {
|
|
185988
|
+
const ok = checkStaticRoot();
|
|
185989
|
+
if (ok === staticRootOk) return;
|
|
185990
|
+
staticRootOk = ok;
|
|
185991
|
+
if (ok) log.web.info("web assets are servable again", { staticDir });
|
|
185992
|
+
else log.web.error("web assets VANISHED from under the running server", { staticDir, mirrorReady });
|
|
185993
|
+
}, 6e4);
|
|
185994
|
+
staticRootTimer.unref();
|
|
185995
|
+
setStaticRootReporter(() => ({ staticDir, ok: staticRootOk, mirrorDir, mirrorReady }));
|
|
185133
185996
|
app.use((req, res, next) => {
|
|
185134
185997
|
if (req.method !== "GET" || req.path.startsWith("/api/")) return next();
|
|
185135
185998
|
if (isStaticAssetPath(req.path)) return next();
|
|
185136
|
-
res.sendFile("index.html", { root: staticDir })
|
|
185999
|
+
res.sendFile("index.html", { root: staticDir }, (err2) => {
|
|
186000
|
+
if (!err2) return;
|
|
186001
|
+
if (mirrorReady) {
|
|
186002
|
+
res.sendFile("index.html", { root: mirrorDir }, () => {
|
|
186003
|
+
});
|
|
186004
|
+
return;
|
|
186005
|
+
}
|
|
186006
|
+
next(err2);
|
|
186007
|
+
});
|
|
185137
186008
|
});
|
|
185138
186009
|
}
|
|
185139
186010
|
app.use("/api", notFoundHandler);
|
|
@@ -185153,9 +186024,21 @@ async function startServer(options = {}) {
|
|
|
185153
186024
|
}
|
|
185154
186025
|
httpServer = createServer(app);
|
|
185155
186026
|
attachWss(httpServer);
|
|
185156
|
-
|
|
185157
|
-
|
|
185158
|
-
|
|
186027
|
+
try {
|
|
186028
|
+
await new Promise((resolve, reject) => {
|
|
186029
|
+
httpServer.once("error", reject);
|
|
186030
|
+
httpServer.listen(port, () => resolve());
|
|
186031
|
+
});
|
|
186032
|
+
} catch (err2) {
|
|
186033
|
+
if (err2?.code !== "EADDRINUSE") throw err2;
|
|
186034
|
+
const already = await isWalnutAnsweringOn(port);
|
|
186035
|
+
throw new Error(
|
|
186036
|
+
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:
|
|
186037
|
+
open-walnut web --port ${port + 1}`
|
|
186038
|
+
);
|
|
186039
|
+
}
|
|
186040
|
+
httpServer.on("error", (err2) => {
|
|
186041
|
+
log.web.error("http server error after listen", { error: err2 instanceof Error ? err2.message : String(err2) });
|
|
185159
186042
|
});
|
|
185160
186043
|
const label = dev ? "dev" : "production";
|
|
185161
186044
|
log.web.info(`server listening on http://localhost:${port}`, { mode: label, port });
|
|
@@ -185165,6 +186048,7 @@ async function startServer(options = {}) {
|
|
|
185165
186048
|
const { updateInstanceLockPort: updateInstanceLockPort2 } = await Promise.resolve().then(() => (init_instance_lock(), instance_lock_exports));
|
|
185166
186049
|
updateInstanceLockPort2(bound.port);
|
|
185167
186050
|
setPluginApiBase(`http://127.0.0.1:${bound.port}`);
|
|
186051
|
+
process.env.WALNUT_SERVER_URL = `http://localhost:${bound.port}`;
|
|
185168
186052
|
}
|
|
185169
186053
|
}
|
|
185170
186054
|
if (CLOUD_MODE) {
|
|
@@ -185827,10 +186711,9 @@ async function startServer(options = {}) {
|
|
|
185827
186711
|
const prev = systemHealth.hasReadyProvider;
|
|
185828
186712
|
const prevSource = systemHealth.credentialSource;
|
|
185829
186713
|
const cred = await resolveCredentialHealth();
|
|
185830
|
-
|
|
185831
|
-
|
|
185832
|
-
|
|
185833
|
-
if (cred.hasReadyProvider !== prev || cred.source !== prevSource) {
|
|
186714
|
+
const prevProvider = systemHealth.mainProvider;
|
|
186715
|
+
applyCredentialHealth(cred);
|
|
186716
|
+
if (cred.hasReadyProvider !== prev || cred.source !== prevSource || cred.mainProvider !== prevProvider) {
|
|
185834
186717
|
broadcastEvent("system:health", systemHealth);
|
|
185835
186718
|
}
|
|
185836
186719
|
} catch {
|