turbollm 1.8.5 → 1.8.7
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/dist/cli.js +963 -57
- package/dist/webdist/assets/{AgentEditPage-VoVmMCf1.js → AgentEditPage-F6wQ-woa.js} +1 -1
- package/dist/webdist/assets/{ChatScreen-DHGEKPZr.js → ChatScreen-JNejy4c5.js} +1 -1
- package/dist/webdist/assets/CodeComposer-BMivBZTt.js +1 -0
- package/dist/webdist/assets/{CodeHomeScreen-BHIrDU_t.js → CodeHomeScreen-Bvv8dpoH.js} +1 -1
- package/dist/webdist/assets/CodeSessionScreen-BZ_n-Q8C.js +5 -0
- package/dist/webdist/assets/CustomizeScreen-CfQYShcO.js +4 -0
- package/dist/webdist/assets/DeveloperScreen-DlLMgh2N.js +1 -0
- package/dist/webdist/assets/EnginesScreen-CCzC8Dth.js +7 -0
- package/dist/webdist/assets/{FsBrowser-5VN4Yj8z.js → FsBrowser-d8UO9bVy.js} +1 -1
- package/dist/webdist/assets/ModelLoadMenu-BcSALweE.js +2 -0
- package/dist/webdist/assets/{ModelsScreen-GUtj_sux.js → ModelsScreen-DHqnZlFL.js} +1 -1
- package/dist/webdist/assets/{SettingsScreen-D65xd3_e.js → SettingsScreen-Byi4wqlL.js} +1 -1
- package/dist/webdist/assets/{SkillEditPage-CkKqavr8.js → SkillEditPage-CKfpniwp.js} +1 -1
- package/dist/webdist/assets/{TokensScreen-CCS_y7DZ.js → TokensScreen-CSxwCWhD.js} +1 -1
- package/dist/webdist/assets/{ToolApprovalBar-CRZPKrcR.js → ToolApprovalBar-CDRoIK9R.js} +15 -15
- package/dist/webdist/assets/WorkspaceScreen-CY5EZKSy.js +1 -0
- package/dist/webdist/assets/{agent-api-BZQuCIAd.js → agent-api-DaWwB_7n.js} +1 -1
- package/dist/webdist/assets/{arc-CG_F6y0n.js → arc-Bw5r0gQ8.js} +1 -1
- package/dist/webdist/assets/{architectureDiagram-3BPJPVTR-bK0viBCO.js → architectureDiagram-3BPJPVTR-D_qgeydj.js} +1 -1
- package/dist/webdist/assets/{arrow-left-Cxqlqe4Q.js → arrow-left-Bs4KhSqC.js} +1 -1
- package/dist/webdist/assets/{badge-D1brk9rd.js → badge-CPXRwtV2.js} +1 -1
- package/dist/webdist/assets/{blockDiagram-GPEHLZMM-DqMi7LYs.js → blockDiagram-GPEHLZMM-BbzqG8AR.js} +1 -1
- package/dist/webdist/assets/{bot-CldCBtRY.js → bot-DUgi89DH.js} +1 -1
- package/dist/webdist/assets/{c4Diagram-AAUBKEIU-D5JOtr9l.js → c4Diagram-AAUBKEIU-Bltku6hD.js} +1 -1
- package/dist/webdist/assets/channel-Cob2-fKv.js +1 -0
- package/dist/webdist/assets/{chat-api-uAJNrEFg.js → chat-api-DnoBopyh.js} +1 -1
- package/dist/webdist/assets/{chat-queries-OevUA_jH.js → chat-queries-EOqarO2W.js} +1 -1
- package/dist/webdist/assets/check-C-_wc2AD.js +1 -0
- package/dist/webdist/assets/{chevron-left-Dv-zay0l.js → chevron-left-CFIaBRPc.js} +1 -1
- package/dist/webdist/assets/{chevron-right-DvvJS6ye.js → chevron-right-DT5tldLF.js} +1 -1
- package/dist/webdist/assets/{chunk-2J33WTMH-D5FvNAKn.js → chunk-2J33WTMH-jRmwQd0-.js} +1 -1
- package/dist/webdist/assets/{chunk-4BX2VUAB-PiXdGtQT.js → chunk-4BX2VUAB-BkQR5P6X.js} +1 -1
- package/dist/webdist/assets/{chunk-55IACEB6-BcFnIoE2.js → chunk-55IACEB6-dHYKdZGl.js} +1 -1
- package/dist/webdist/assets/{chunk-727SXJPM-BigbNnpE.js → chunk-727SXJPM-Bac05mkQ.js} +1 -1
- package/dist/webdist/assets/{chunk-AQP2D5EJ-ksTbyzzH.js → chunk-AQP2D5EJ-DQSeJwV8.js} +1 -1
- package/dist/webdist/assets/{chunk-FMBD7UC4-DpDAzpmq.js → chunk-FMBD7UC4-fw71OGhl.js} +1 -1
- package/dist/webdist/assets/{chunk-ND2GUHAM-Ch7eIfE0.js → chunk-ND2GUHAM-xRyemSmx.js} +1 -1
- package/dist/webdist/assets/{chunk-QZHKN3VN-DMDG8lEf.js → chunk-QZHKN3VN-BgNNqsB9.js} +1 -1
- package/dist/webdist/assets/{circle-check-BFtGWNjh.js → circle-check-CLNNkErq.js} +1 -1
- package/dist/webdist/assets/{circle-x-BmwEPZU0.js → circle-x-DHTcnBYg.js} +1 -1
- package/dist/webdist/assets/classDiagram-4FO5ZUOK-emMvNtz-.js +1 -0
- package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-emMvNtz-.js +1 -0
- package/dist/webdist/assets/{common-BEcCBxuG.js → common-Bh_ckDae.js} +1 -1
- package/dist/webdist/assets/copy-button-B0AUQv8-.js +1 -0
- package/dist/webdist/assets/{cose-bilkent-S5V4N54A-BBGTUfOp.js → cose-bilkent-S5V4N54A-BpeA37l_.js} +1 -1
- package/dist/webdist/assets/{dagre-BM42HDAG-D8W6jcq_.js → dagre-BM42HDAG-Dhe-FxnK.js} +1 -1
- package/dist/webdist/assets/{diagram-2AECGRRQ-CvCrWf_H.js → diagram-2AECGRRQ-CsOhE15t.js} +1 -1
- package/dist/webdist/assets/{diagram-5GNKFQAL-DZS4yZU1.js → diagram-5GNKFQAL-Dj4NKMxD.js} +1 -1
- package/dist/webdist/assets/{diagram-KO2AKTUF-BlKSql5d.js → diagram-KO2AKTUF-BVeDuCrX.js} +1 -1
- package/dist/webdist/assets/{diagram-LMA3HP47-CKo-tRYQ.js → diagram-LMA3HP47-CrQt3n5J.js} +1 -1
- package/dist/webdist/assets/{diagram-OG6HWLK6-NSePJGx1.js → diagram-OG6HWLK6-CVHDvio_.js} +1 -1
- package/dist/webdist/assets/{dialog-B-GLhgc2.js → dialog-BPhXn1HL.js} +1 -1
- package/dist/webdist/assets/{erDiagram-TEJ5UH35-haPVT0D3.js → erDiagram-TEJ5UH35-DmEuGD8S.js} +1 -1
- package/dist/webdist/assets/{external-link-UbaLGQ_h.js → external-link-DgElSULb.js} +1 -1
- package/dist/webdist/assets/{flowDiagram-I6XJVG4X-BzKZFBHV.js → flowDiagram-I6XJVG4X-D3ES6R-Y.js} +1 -1
- package/dist/webdist/assets/{ganttDiagram-6RSMTGT7-CCmAecLV.js → ganttDiagram-6RSMTGT7-DzovqV_S.js} +1 -1
- package/dist/webdist/assets/{git-branch-Bj6Xfs8n.js → git-branch-DZp9oCCA.js} +1 -1
- package/dist/webdist/assets/{gitGraphDiagram-PVQCEYII-OD-Aumfw.js → gitGraphDiagram-PVQCEYII-BV0Hvb-m.js} +1 -1
- package/dist/webdist/assets/{index-D4oBHgyO.js → index-BivjMfQ5.js} +2 -2
- package/dist/webdist/assets/index-S6YWlmeh.css +1 -0
- package/dist/webdist/assets/{infoDiagram-5YYISTIA-CXiblcgy.js → infoDiagram-5YYISTIA-DHmj_pKX.js} +1 -1
- package/dist/webdist/assets/{ishikawaDiagram-YF4QCWOH-C8sCKBhs.js → ishikawaDiagram-YF4QCWOH-DjAmacek.js} +1 -1
- package/dist/webdist/assets/{journeyDiagram-JHISSGLW-B3be8ACK.js → journeyDiagram-JHISSGLW-InYx9cWN.js} +1 -1
- package/dist/webdist/assets/{kanban-definition-UN3LZRKU-BbfHizAI.js → kanban-definition-UN3LZRKU-BO9O2UDq.js} +1 -1
- package/dist/webdist/assets/{linear-C0lkBXID.js → linear-B6IwpKfz.js} +1 -1
- package/dist/webdist/assets/{mermaid.core-CW2TnRwR.js → mermaid.core-MlIRHLxX.js} +4 -4
- package/dist/webdist/assets/{mindmap-definition-RKZ34NQL-h-ro0B1A.js → mindmap-definition-RKZ34NQL-kvrr7ftb.js} +1 -1
- package/dist/webdist/assets/{pencil-BJsQ5uv5.js → pencil-k4AQARf_.js} +1 -1
- package/dist/webdist/assets/{personas-fIGMukzj.js → personas-CL0Kc_Kd.js} +5 -3
- package/dist/webdist/assets/{pieDiagram-4H26LBE5-DEGupnzV.js → pieDiagram-4H26LBE5-DaE8CAe0.js} +1 -1
- package/dist/webdist/assets/{plus-BESpOgWu.js → plus-CY4mhBkY.js} +1 -1
- package/dist/webdist/assets/{quadrantDiagram-W4KKPZXB-CMkivigQ.js → quadrantDiagram-W4KKPZXB-Duu8s5l-.js} +1 -1
- package/dist/webdist/assets/{refresh-cw-CuuCQSpX.js → refresh-cw-DCf4F0oQ.js} +1 -1
- package/dist/webdist/assets/{requirementDiagram-4Y6WPE33-DyChhWBw.js → requirementDiagram-4Y6WPE33-Cbl7MhTK.js} +1 -1
- package/dist/webdist/assets/{rotate-ccw-D99lmEA-.js → rotate-ccw-Bk6FjMT9.js} +1 -1
- package/dist/webdist/assets/{sankeyDiagram-5OEKKPKP-BDPQ7Ai5.js → sankeyDiagram-5OEKKPKP-BiK0JGus.js} +1 -1
- package/dist/webdist/assets/{sequenceDiagram-3UESZ5HK-C_jdr66e.js → sequenceDiagram-3UESZ5HK-Dr7D9A4w.js} +1 -1
- package/dist/webdist/assets/{skeleton-nGnGDZ2Z.js → skeleton-Cy7DsAmm.js} +1 -1
- package/dist/webdist/assets/{sliders-horizontal-BXU5PC3A.js → sliders-horizontal-CVf2FXz2.js} +1 -1
- package/dist/webdist/assets/{wrench-CUU3qZM0.js → sparkles-Bvrl94s5.js} +1 -1
- package/dist/webdist/assets/{star-BGgeiEjL.js → star-DLAcO0R_.js} +1 -1
- package/dist/webdist/assets/{stateDiagram-AJRCARHV-CwtPbKxE.js → stateDiagram-AJRCARHV-BthDRItX.js} +1 -1
- package/dist/webdist/assets/stateDiagram-v2-BHNVJYJU--IOHWZLa.js +1 -0
- package/dist/webdist/assets/{switch-BQT_NipX.js → switch-CI322tBg.js} +1 -1
- package/dist/webdist/assets/{terminal-CrCLNyjB.js → terminal-Bwy3mmYf.js} +1 -1
- package/dist/webdist/assets/{timeline-definition-PNZ67QCA-B1bN1YFW.js → timeline-definition-PNZ67QCA-DkR-Byrz.js} +1 -1
- package/dist/webdist/assets/{trash-2-CuUIi8kE.js → trash-2-B9m1cz2x.js} +1 -1
- package/dist/webdist/assets/{useIsDesktop-BwBLPtTS.js → useIsDesktop-xqMNa55J.js} +1 -1
- package/dist/webdist/assets/{vennDiagram-CIIHVFJN-B23LrfT3.js → vennDiagram-CIIHVFJN-aOYXm4nP.js} +1 -1
- package/dist/webdist/assets/{wardley-L42UT6IY-Bkk_DpTu.js → wardley-L42UT6IY-DptJ0PbQ.js} +1 -1
- package/dist/webdist/assets/{wardleyDiagram-YWT4CUSO-Di9GYhE4.js → wardleyDiagram-YWT4CUSO-CN66GNqR.js} +1 -1
- package/dist/webdist/assets/wrench-CLvjdoSH.js +1 -0
- package/dist/webdist/assets/{xychartDiagram-2RQKCTM6-BF1BnBYj.js → xychartDiagram-2RQKCTM6-CbOWShHZ.js} +1 -1
- package/dist/webdist/index.html +2 -2
- package/package.json +69 -69
- package/dist/webdist/assets/CodeComposer-Cxhvsus4.js +0 -1
- package/dist/webdist/assets/CodeSessionScreen-CqogdVEN.js +0 -4
- package/dist/webdist/assets/CustomizeScreen-B4XGEyxA.js +0 -4
- package/dist/webdist/assets/DeveloperScreen-jGamwaJG.js +0 -1
- package/dist/webdist/assets/EnginesScreen-BRkOTacr.js +0 -7
- package/dist/webdist/assets/ModelLoadMenu-CgM5a39X.js +0 -2
- package/dist/webdist/assets/WorkspaceScreen-D060hDIn.js +0 -1
- package/dist/webdist/assets/channel-C-X5wnqi.js +0 -1
- package/dist/webdist/assets/check-r_BeDgvE.js +0 -1
- package/dist/webdist/assets/classDiagram-4FO5ZUOK-Dw2O5mDo.js +0 -1
- package/dist/webdist/assets/classDiagram-v2-Q7XG4LA2-Dw2O5mDo.js +0 -1
- package/dist/webdist/assets/copy-button-B3sedfsF.js +0 -1
- package/dist/webdist/assets/index-CfEenPkR.css +0 -1
- package/dist/webdist/assets/layers-BqbkDoHi.js +0 -1
- package/dist/webdist/assets/stateDiagram-v2-BHNVJYJU-DlCLpW5_.js +0 -1
package/dist/cli.js
CHANGED
|
@@ -2310,6 +2310,7 @@ function trimLen(s, n) {
|
|
|
2310
2310
|
|
|
2311
2311
|
// src/engines/build-runner.ts
|
|
2312
2312
|
import { execFile as execFile9, spawn as spawn2 } from "child_process";
|
|
2313
|
+
import { createHash as createHash2 } from "crypto";
|
|
2313
2314
|
import { copyFileSync, existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync3, readdirSync as readdirSync4, rmSync as rmSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
2314
2315
|
import { delimiter as delimiter2, dirname as dirname5, join as join12 } from "path";
|
|
2315
2316
|
import { promisify as promisify7 } from "util";
|
|
@@ -2533,8 +2534,8 @@ function sameRepo(a, b) {
|
|
|
2533
2534
|
function normRepoUrl(s) {
|
|
2534
2535
|
return (s ?? "").trim().toLowerCase().replace(/^https?:\/\//, "").replace(/^github\.com\//, "").replace(/\.git$/, "").replace(/\/+$/, "");
|
|
2535
2536
|
}
|
|
2536
|
-
function sourceBuildBinary(enginesRoot, repoUrl, branch) {
|
|
2537
|
-
const root = join12(enginesRoot, "build", buildDirName(repoUrl, branch));
|
|
2537
|
+
function sourceBuildBinary(enginesRoot, repoUrl, branch, commit) {
|
|
2538
|
+
const root = join12(enginesRoot, "build", buildDirName(repoUrl, branch, commit));
|
|
2538
2539
|
return resolveServerBinary(join12(root, "build")) ?? resolveServerBinary(root);
|
|
2539
2540
|
}
|
|
2540
2541
|
function sourceBuildDirOf(binPath, enginesRoot) {
|
|
@@ -2553,6 +2554,20 @@ function notCmakeProjectError(hasCMakeLists) {
|
|
|
2553
2554
|
if (hasCMakeLists) return null;
|
|
2554
2555
|
return "This repository doesn't look like a llama.cpp-based (CMake) project \u2014 no CMakeLists.txt was found at its root. 1-click build currently only supports llama.cpp-family engines built with CMake. If this project exposes a llama-server-compatible binary some other way, add it as a custom engine by pointing TurboLLM directly at that binary instead of building from source.";
|
|
2555
2556
|
}
|
|
2557
|
+
function missingPatchShaError(patchUrl, patchSha256) {
|
|
2558
|
+
if (!(patchUrl ?? "").trim()) return null;
|
|
2559
|
+
if ((patchSha256 ?? "").trim()) return null;
|
|
2560
|
+
return "Refusing to apply a build patch with no pinned SHA-256 checksum \u2014 a patch may only be applied when its exact downloaded bytes can be verified against a known hash.";
|
|
2561
|
+
}
|
|
2562
|
+
function sha256Hex(bytes) {
|
|
2563
|
+
return createHash2("sha256").update(bytes).digest("hex");
|
|
2564
|
+
}
|
|
2565
|
+
function patchChecksumMismatchError(expectedSha256, actualSha256) {
|
|
2566
|
+
const want = expectedSha256.trim().toLowerCase();
|
|
2567
|
+
const got = actualSha256.trim().toLowerCase();
|
|
2568
|
+
if (want === got) return null;
|
|
2569
|
+
return `The downloaded patch did not match its pinned SHA-256 checksum (expected ${want}, got ${got}). TurboLLM refused to apply an unverified patch \u2014 the remote file may have changed or been tampered with. The build was stopped before any patch was applied.`;
|
|
2570
|
+
}
|
|
2556
2571
|
function vcvarsBatch(vcvars, cmakeArgs) {
|
|
2557
2572
|
const quoted = cmakeArgs.map((a) => /[\s"]/.test(a) ? `"${a}"` : a).join(" ");
|
|
2558
2573
|
return [
|
|
@@ -2830,17 +2845,44 @@ async function runBuild(req, hooks, signal) {
|
|
|
2830
2845
|
if (pinnedCommit) {
|
|
2831
2846
|
mkdirSync7(srcDir, { recursive: true });
|
|
2832
2847
|
await runStep("git", ["init"], { cwd: srcDir, env, signal, onLine: hooks.log });
|
|
2848
|
+
if (isWindows) await runStep("git", ["config", "core.longpaths", "true"], { cwd: srcDir, env, signal, onLine: hooks.log });
|
|
2833
2849
|
await runStep("git", ["remote", "add", "origin", req.repoUrl], { cwd: srcDir, env, signal, onLine: hooks.log });
|
|
2834
2850
|
await runStep("git", ["fetch", "--depth", "1", "origin", pinnedCommit], { cwd: srcDir, env, signal, onLine: hooks.log });
|
|
2835
2851
|
await runStep("git", ["checkout", "FETCH_HEAD"], { cwd: srcDir, env, signal, onLine: hooks.log });
|
|
2836
2852
|
} else {
|
|
2837
|
-
const cloneArgs = ["clone", "--depth", "1"];
|
|
2853
|
+
const cloneArgs = isWindows ? ["-c", "core.longpaths=true", "clone", "--depth", "1"] : ["clone", "--depth", "1"];
|
|
2838
2854
|
if ((req.branch ?? "").trim()) cloneArgs.push("--branch", req.branch.trim());
|
|
2839
2855
|
cloneArgs.push(req.repoUrl, srcDir);
|
|
2840
2856
|
await runStep("git", cloneArgs, { env, signal, onLine: hooks.log });
|
|
2841
2857
|
}
|
|
2842
2858
|
const commit = (await runStep("git", ["-C", srcDir, "rev-parse", "HEAD"], { env, signal, onLine: () => {
|
|
2843
2859
|
} })).trim();
|
|
2860
|
+
const patchUrl = (req.patchUrl ?? "").trim();
|
|
2861
|
+
if (patchUrl) {
|
|
2862
|
+
const expectedSha = (req.patchSha256 ?? "").trim();
|
|
2863
|
+
const guard = missingPatchShaError(patchUrl, expectedSha);
|
|
2864
|
+
if (guard) throw new Error(guard);
|
|
2865
|
+
hooks.log(`Downloading build patch from ${patchUrl} \u2026`);
|
|
2866
|
+
const res = await fetch(patchUrl, { signal });
|
|
2867
|
+
if (!res.ok) throw new Error(`Failed to download the build patch (HTTP ${res.status} ${res.statusText}) from ${patchUrl}.`);
|
|
2868
|
+
const patchBytes = Buffer.from(await res.arrayBuffer());
|
|
2869
|
+
const actualSha = sha256Hex(patchBytes);
|
|
2870
|
+
const mismatch = patchChecksumMismatchError(expectedSha, actualSha);
|
|
2871
|
+
if (mismatch) throw new Error(mismatch);
|
|
2872
|
+
const patchFile = join12(buildRoot, "_tllm_patch.diff");
|
|
2873
|
+
writeFileSync4(patchFile, patchBytes);
|
|
2874
|
+
hooks.log(`Patch verified against its pinned checksum (sha256 ${actualSha.slice(0, 12)}\u2026).`);
|
|
2875
|
+
try {
|
|
2876
|
+
await runStep("git", ["-C", srcDir, "apply", "--check", patchFile], { env, signal, onLine: hooks.log });
|
|
2877
|
+
} catch (e) {
|
|
2878
|
+
if (e?.name === "AbortError") throw e;
|
|
2879
|
+
throw new Error(
|
|
2880
|
+
"The verified patch failed to apply against the checked-out commit (most likely it has drifted from the commit it was authored against, but see the underlying error below for the exact cause). " + (e instanceof Error ? e.message : String(e))
|
|
2881
|
+
);
|
|
2882
|
+
}
|
|
2883
|
+
await runStep("git", ["-C", srcDir, "apply", patchFile], { env, signal, onLine: hooks.log });
|
|
2884
|
+
hooks.log(`Applied verified patch (sha256 ${actualSha.slice(0, 12)}\u2026) on top of ${commit.slice(0, 8)}.`);
|
|
2885
|
+
}
|
|
2844
2886
|
const notCmake = notCmakeProjectError(existsSync13(join12(srcDir, "CMakeLists.txt")));
|
|
2845
2887
|
if (notCmake) throw new Error(notCmake);
|
|
2846
2888
|
neutralizeGratuitousAsm(srcDir, hooks.log);
|
|
@@ -2931,6 +2973,7 @@ var Registry = class {
|
|
|
2931
2973
|
const sourceRepo = source?.sourceRepo?.trim() || void 0;
|
|
2932
2974
|
const sourceBranch = source?.sourceBranch?.trim() || void 0;
|
|
2933
2975
|
const sourceCommit = source?.sourceCommit?.trim() || void 0;
|
|
2976
|
+
const sourcePatchUrl = source?.sourcePatchUrl?.trim() || void 0;
|
|
2934
2977
|
const eng = {
|
|
2935
2978
|
id: randomUUID2(),
|
|
2936
2979
|
name: finalName,
|
|
@@ -2941,7 +2984,8 @@ var Registry = class {
|
|
|
2941
2984
|
addedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2942
2985
|
...sourceRepo ? { sourceRepo } : {},
|
|
2943
2986
|
...sourceBranch ? { sourceBranch } : {},
|
|
2944
|
-
...sourceCommit ? { sourceCommit } : {}
|
|
2987
|
+
...sourceCommit ? { sourceCommit } : {},
|
|
2988
|
+
...sourcePatchUrl ? { sourcePatchUrl } : {}
|
|
2945
2989
|
};
|
|
2946
2990
|
this.store.update((c) => {
|
|
2947
2991
|
if (this.nameClash(c.engines, finalName)) throw new NameTakenError();
|
|
@@ -4695,7 +4739,7 @@ function seedDefaultModelDir(store2, scanner2) {
|
|
|
4695
4739
|
}
|
|
4696
4740
|
|
|
4697
4741
|
// src/models/hashes.ts
|
|
4698
|
-
import { createHash as
|
|
4742
|
+
import { createHash as createHash3 } from "crypto";
|
|
4699
4743
|
import { createReadStream, mkdirSync as mkdirSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
4700
4744
|
import { join as join16 } from "path";
|
|
4701
4745
|
var HashStore = class {
|
|
@@ -4722,7 +4766,7 @@ var HashStore = class {
|
|
|
4722
4766
|
}
|
|
4723
4767
|
compute(path, size, mtime) {
|
|
4724
4768
|
return new Promise((resolve5) => {
|
|
4725
|
-
const hash =
|
|
4769
|
+
const hash = createHash3("sha256");
|
|
4726
4770
|
const rs = createReadStream(path);
|
|
4727
4771
|
rs.on("error", () => resolve5());
|
|
4728
4772
|
rs.on("data", (c) => hash.update(c));
|
|
@@ -6155,6 +6199,30 @@ var ConversationStore = class {
|
|
|
6155
6199
|
if (!from) return 0;
|
|
6156
6200
|
return this.db.prepare(`UPDATE messages SET is_active = 1 WHERE conv_id = $cid AND seq >= $seq`).run({ $cid: convId, $seq: from.seq }).changes;
|
|
6157
6201
|
}
|
|
6202
|
+
/** /clear (v34): deactivates `uptoMessageId` and every message BEFORE it (by `seq`) — a real,
|
|
6203
|
+
* resumable PREFIX deactivation (is_active=0), the mirror of {@link deactivateMessagesFrom}'s
|
|
6204
|
+
* suffix and the same mechanism /revert and Chat branching use. Replaces /clear's old
|
|
6205
|
+
* clearedUpToMessageId DISPLAY cursor, which left the "cleared" rows is_active=1 — so
|
|
6206
|
+
* getMessages()/getConversation() (hence session export AND the model's own history replay via
|
|
6207
|
+
* resolveEffectiveHistory) still saw them, applying the cut only client-side. /clear hides
|
|
6208
|
+
* everything up to AND INCLUDING the cut (showing only turns added AFTER it), so this is a
|
|
6209
|
+
* prefix. Returns the number of rows deactivated (0 if `uptoMessageId` doesn't exist). Only ever
|
|
6210
|
+
* called on Code conversations, which have no regenerate/branch siblings, so every prefix row is
|
|
6211
|
+
* active at clear time — same coarse assumption reactivateMessagesFrom already relies on. */
|
|
6212
|
+
deactivateMessagesUpTo(convId, uptoMessageId) {
|
|
6213
|
+
const upto = this.db.prepare(`SELECT seq FROM messages WHERE id = $id AND conv_id = $cid`).get({ $id: uptoMessageId, $cid: convId });
|
|
6214
|
+
if (!upto) return 0;
|
|
6215
|
+
return this.db.prepare(`UPDATE messages SET is_active = 0 WHERE conv_id = $cid AND seq <= $seq`).run({ $cid: convId, $seq: upto.seq }).changes;
|
|
6216
|
+
}
|
|
6217
|
+
/** Undoes {@link deactivateMessagesUpTo} — reactivates `uptoMessageId` and every message before
|
|
6218
|
+
* it (by `seq`). A NEW turn appended after a /clear has a HIGHER seq than the cut, so /resume
|
|
6219
|
+
* never touches it — only the originally-cleared prefix comes back. Safe if `uptoMessageId` no
|
|
6220
|
+
* longer exists (0 rows). */
|
|
6221
|
+
reactivateMessagesUpTo(convId, uptoMessageId) {
|
|
6222
|
+
const upto = this.db.prepare(`SELECT seq FROM messages WHERE id = $id AND conv_id = $cid`).get({ $id: uptoMessageId, $cid: convId });
|
|
6223
|
+
if (!upto) return 0;
|
|
6224
|
+
return this.db.prepare(`UPDATE messages SET is_active = 1 WHERE conv_id = $cid AND seq <= $seq`).run({ $cid: convId, $seq: upto.seq }).changes;
|
|
6225
|
+
}
|
|
6158
6226
|
/** Permanently delete a Code session: its messages, working doc, and the agent_run +
|
|
6159
6227
|
* conversation rows. There is no FK cascade on these tables (each is deleted explicitly,
|
|
6160
6228
|
* same as chat's own conversation delete never cascaded to messages) — miss one and it's
|
|
@@ -6640,7 +6708,7 @@ function dirOf(p) {
|
|
|
6640
6708
|
}
|
|
6641
6709
|
|
|
6642
6710
|
// src/downloads/downloads.ts
|
|
6643
|
-
import { createHash as
|
|
6711
|
+
import { createHash as createHash4 } from "crypto";
|
|
6644
6712
|
import {
|
|
6645
6713
|
createWriteStream as createWriteStream3,
|
|
6646
6714
|
existsSync as existsSync17,
|
|
@@ -6920,7 +6988,7 @@ var DownloadManager = class {
|
|
|
6920
6988
|
const clen = Number(res.headers.get("content-length") ?? 0);
|
|
6921
6989
|
if (clen > 0) rec.total = resuming ? startAt + clen : clen;
|
|
6922
6990
|
if (clen > 0) this.assertDisk(dirname7(part), clen);
|
|
6923
|
-
const hash = rec.sha256 ?
|
|
6991
|
+
const hash = rec.sha256 ? createHash4("sha256") : null;
|
|
6924
6992
|
const verifyHash = hash !== null && startAt === 0;
|
|
6925
6993
|
let lastTick = Date.now();
|
|
6926
6994
|
let lastBytes = startAt;
|
|
@@ -12386,11 +12454,11 @@ async function autoTitleFromConversation(d, convId) {
|
|
|
12386
12454
|
}
|
|
12387
12455
|
|
|
12388
12456
|
// src/auth.ts
|
|
12389
|
-
import { createHash as
|
|
12457
|
+
import { createHash as createHash5, randomBytes, randomUUID as randomUUID6 } from "crypto";
|
|
12390
12458
|
import { getConnInfo } from "@hono/node-server/conninfo";
|
|
12391
12459
|
var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
|
|
12392
12460
|
function hashKey(raw) {
|
|
12393
|
-
return
|
|
12461
|
+
return createHash5("sha256").update(raw).digest("hex");
|
|
12394
12462
|
}
|
|
12395
12463
|
function generateApiKey() {
|
|
12396
12464
|
const charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
@@ -12398,7 +12466,7 @@ function generateApiKey() {
|
|
|
12398
12466
|
let key = "";
|
|
12399
12467
|
for (let i = 0; i < 40; i++) key += charset[buf[i] % 62];
|
|
12400
12468
|
const full = `tllm-${key}`;
|
|
12401
|
-
const hash =
|
|
12469
|
+
const hash = createHash5("sha256").update(full).digest("hex");
|
|
12402
12470
|
return { full, hash, prefix: full.slice(0, 12) };
|
|
12403
12471
|
}
|
|
12404
12472
|
function provisionTunnelApiKey(d) {
|
|
@@ -12901,6 +12969,38 @@ var ALL = [
|
|
|
12901
12969
|
hasPrebuilt: false
|
|
12902
12970
|
}
|
|
12903
12971
|
]
|
|
12972
|
+
},
|
|
12973
|
+
{
|
|
12974
|
+
id: "solar-open2",
|
|
12975
|
+
name: "Solar Open 2 (patched llama.cpp)",
|
|
12976
|
+
kind: "llama-server",
|
|
12977
|
+
description: "Upstage's Solar Open 2 (250B-A15B MoE) isn't in mainline llama.cpp yet \u2014 this builds official llama.cpp at a pinned commit with a community patch that adds the solar_open2 architecture, then runs it on the standard llama-server path.",
|
|
12978
|
+
provision: "github-release",
|
|
12979
|
+
// Builds the OFFICIAL llama.cpp repo (like the prism/beellama build-from-source cards, no
|
|
12980
|
+
// real github-release asset is resolved) — pinned to the exact commit the patch was authored
|
|
12981
|
+
// against, then the checksum-verified patch is applied before compiling. The patch is an
|
|
12982
|
+
// INDEPENDENT community diff, NOT reviewed or endorsed by Upstage or upstream llama.cpp — same
|
|
12983
|
+
// fork-provenance honesty as the prism/beellama entries above (Honesty rule at the top).
|
|
12984
|
+
homepage: "https://github.com/ggml-org/llama.cpp",
|
|
12985
|
+
repo: "ggml-org/llama.cpp",
|
|
12986
|
+
sourceCommit: "846e991ec3c7ccec49112ff2c5b00b710e5f551d",
|
|
12987
|
+
patchUrl: "https://huggingface.co/prometheusAIR/Solar-Open2-250B-GGUF/resolve/main/solar_open2-llama.cpp.patch",
|
|
12988
|
+
patchSha256: "998a9cef479d3b01f25e473793890b05b887a8ce85563431b3b053b14bb21fa8",
|
|
12989
|
+
platforms: ["win32", "darwin", "linux"],
|
|
12990
|
+
support: "experimental",
|
|
12991
|
+
installEndpoint: "",
|
|
12992
|
+
note: "Unofficial: applies an independent community patch (not affiliated with or reviewed by Upstage or upstream llama.cpp) on top of a pinned llama.cpp commit, verified against a pinned SHA-256 before it is applied. This is a 250B-total / ~15B-active MoE \u2014 the upstream port was tested on workstation-class hardware (128GB DDR5 + a 96GB RTX PRO 6000), not a typical consumer box; GGUF quants run ~89 GiB (Q2_K) to 191 GiB (Q6_K), with IQ4_XS (~127 GiB) recommended. Two known quirks on this arch: pass -ub 512 or -ub 4096 (a batch size of -ub 2048 crashes with a CUDA illegal-memory-access), and add --no-mmap for any CPU-offloaded setup (mmap page-faulting collapses throughput ~10x on this port).",
|
|
12993
|
+
variants: [
|
|
12994
|
+
{
|
|
12995
|
+
id: "solar-open2-source",
|
|
12996
|
+
label: "Build from source (patched)",
|
|
12997
|
+
repo: "ggml-org/llama.cpp",
|
|
12998
|
+
requires: {},
|
|
12999
|
+
stability: "experimental",
|
|
13000
|
+
speed: "baseline",
|
|
13001
|
+
hasPrebuilt: false
|
|
13002
|
+
}
|
|
13003
|
+
]
|
|
12904
13004
|
}
|
|
12905
13005
|
];
|
|
12906
13006
|
function catalogForPlatform(platform = process.platform) {
|
|
@@ -13520,9 +13620,11 @@ function registerApi(app2, d) {
|
|
|
13520
13620
|
installed2 = existsSync22(llamafileBinPath(enginesRoot));
|
|
13521
13621
|
enabled = regEngines.some((x) => x.kind === "llamafile");
|
|
13522
13622
|
}
|
|
13523
|
-
const srcEng = regEngines.find(
|
|
13623
|
+
const srcEng = regEngines.find(
|
|
13624
|
+
(x) => sameRepo(x.sourceRepo, e.homepage) && (x.sourceCommit ?? "") === (e.sourceCommit ?? "") && (x.sourcePatchUrl ?? "") === (e.patchUrl ?? "")
|
|
13625
|
+
);
|
|
13524
13626
|
let sourceBinPath = srcEng?.binPath;
|
|
13525
|
-
if (!srcEng) sourceBinPath = sourceBuildBinary(enginesRoot, e.homepage) ?? void 0;
|
|
13627
|
+
if (!srcEng) sourceBinPath = sourceBuildBinary(enginesRoot, e.homepage, void 0, e.sourceCommit) ?? void 0;
|
|
13526
13628
|
const sourceBuilt = !!srcEng || !!sourceBinPath;
|
|
13527
13629
|
if (srcEng) {
|
|
13528
13630
|
installed2 = true;
|
|
@@ -13567,6 +13669,14 @@ function registerApi(app2, d) {
|
|
|
13567
13669
|
const commit = (b.commit ?? "").trim() || void 0;
|
|
13568
13670
|
if (commit && !/^[0-9a-f]{7,40}$/i.test(commit))
|
|
13569
13671
|
return err2(c, 400, "invalid_config_value", "commit must be a hex SHA (7-40 chars).");
|
|
13672
|
+
const patchUrl = (b.patchUrl ?? "").trim() || void 0;
|
|
13673
|
+
const patchSha256 = (b.patchSha256 ?? "").trim() || void 0;
|
|
13674
|
+
if (!!patchUrl !== !!patchSha256)
|
|
13675
|
+
return err2(c, 400, "invalid_config_value", "patchUrl and patchSha256 must be provided together \u2014 a patch may only be applied with a pinned checksum.");
|
|
13676
|
+
if (patchUrl && !/^https?:\/\//i.test(patchUrl))
|
|
13677
|
+
return err2(c, 400, "invalid_config_value", "patchUrl must be an http(s) URL.");
|
|
13678
|
+
if (patchSha256 && !/^[0-9a-f]{64}$/i.test(patchSha256))
|
|
13679
|
+
return err2(c, 400, "invalid_config_value", "patchSha256 must be a 64-char hex SHA-256.");
|
|
13570
13680
|
const busy = engineWorkBusy(d);
|
|
13571
13681
|
if (busy) return err2(c, 409, "engine_already_running", busy);
|
|
13572
13682
|
const name = (b.name ?? "").trim() || void 0;
|
|
@@ -13581,7 +13691,7 @@ function registerApi(app2, d) {
|
|
|
13581
13691
|
let out;
|
|
13582
13692
|
try {
|
|
13583
13693
|
out = await runBuild(
|
|
13584
|
-
{ repoUrl, branch, commit, enginesRoot, toolchainDirs },
|
|
13694
|
+
{ repoUrl, branch, commit, patchUrl, patchSha256, enginesRoot, toolchainDirs },
|
|
13585
13695
|
{ phase: (p) => d.build.phase(p), log: (line) => d.build.log(line) },
|
|
13586
13696
|
ac.signal
|
|
13587
13697
|
);
|
|
@@ -13616,11 +13726,12 @@ function registerApi(app2, d) {
|
|
|
13616
13726
|
const eng = (await d.registry.add(prior?.name ?? name ?? "", out.binPath, {
|
|
13617
13727
|
sourceRepo: repoUrl,
|
|
13618
13728
|
sourceBranch: branch,
|
|
13619
|
-
sourceCommit: commit
|
|
13729
|
+
sourceCommit: commit,
|
|
13730
|
+
sourcePatchUrl: patchUrl
|
|
13620
13731
|
})).engine;
|
|
13621
13732
|
d.registry.activate(eng.id);
|
|
13622
13733
|
if (!isCatalogRepo(repoUrl)) {
|
|
13623
|
-
d.registry.recordCustomSource({ name: eng.name, binPath: eng.binPath, kind: eng.kind, sourceRepo: repoUrl, sourceBranch: branch, sourceCommit: commit });
|
|
13734
|
+
d.registry.recordCustomSource({ name: eng.name, binPath: eng.binPath, kind: eng.kind, sourceRepo: repoUrl, sourceBranch: branch, sourceCommit: commit, sourcePatchUrl: patchUrl });
|
|
13624
13735
|
}
|
|
13625
13736
|
d.build.log(`Registered "${eng.name}" \u2014 built from ${out.commit.slice(0, 8)}.`);
|
|
13626
13737
|
d.build.done();
|
|
@@ -13893,7 +14004,8 @@ function registerApi(app2, d) {
|
|
|
13893
14004
|
kind: eng.kind,
|
|
13894
14005
|
sourceRepo: eng.sourceRepo,
|
|
13895
14006
|
sourceBranch: eng.sourceBranch,
|
|
13896
|
-
sourceCommit: eng.sourceCommit
|
|
14007
|
+
sourceCommit: eng.sourceCommit,
|
|
14008
|
+
sourcePatchUrl: eng.sourcePatchUrl
|
|
13897
14009
|
});
|
|
13898
14010
|
}
|
|
13899
14011
|
d.registry.remove(id);
|
|
@@ -14010,18 +14122,18 @@ function registerApi(app2, d) {
|
|
|
14010
14122
|
if (!existsSync22(cwd) || !statSync5(cwd).isDirectory()) {
|
|
14011
14123
|
return c.json({ isRepo: false, branch: "", branches: [] });
|
|
14012
14124
|
}
|
|
14013
|
-
const
|
|
14125
|
+
const git2 = (args) => execFileSync5("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3, windowsHide: true }).trim();
|
|
14014
14126
|
try {
|
|
14015
|
-
if (
|
|
14127
|
+
if (git2(["rev-parse", "--is-inside-work-tree"]) !== "true") return c.json({ isRepo: false, branch: "", branches: [] });
|
|
14016
14128
|
let branch = "";
|
|
14017
14129
|
try {
|
|
14018
|
-
branch =
|
|
14130
|
+
branch = git2(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
14019
14131
|
} catch {
|
|
14020
14132
|
}
|
|
14021
14133
|
if (branch === "HEAD") branch = "";
|
|
14022
14134
|
let branches = [];
|
|
14023
14135
|
try {
|
|
14024
|
-
branches =
|
|
14136
|
+
branches = git2(["branch", "--format=%(refname:short)"]).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
14025
14137
|
} catch {
|
|
14026
14138
|
}
|
|
14027
14139
|
return c.json({ isRepo: true, branch, branches });
|
|
@@ -15720,6 +15832,16 @@ function dependencyDisciplineGuidance() {
|
|
|
15720
15832
|
"training knowledge, which is frequently stale for fast-moving libraries."
|
|
15721
15833
|
].join(" ");
|
|
15722
15834
|
}
|
|
15835
|
+
function todoTrackerGuidance() {
|
|
15836
|
+
return [
|
|
15837
|
+
"For a task with several distinct steps, call update_todos to give the user a live checklist:",
|
|
15838
|
+
"send the full list of steps (each { content, status }) when you start, mark a step in_progress",
|
|
15839
|
+
"when you begin it and completed as soon as it is done, and re-send the WHOLE list on every",
|
|
15840
|
+
"update (it replaces the previous one). This shows the user the plan and real progress instead",
|
|
15841
|
+
"of an opaque run of tool calls. Do NOT use it for trivial single-step requests \u2014 a one-item",
|
|
15842
|
+
"checklist is just noise."
|
|
15843
|
+
].join(" ");
|
|
15844
|
+
}
|
|
15723
15845
|
function toolsForMode(mode) {
|
|
15724
15846
|
return mode === "plan" ? ["read", "grep", "find", "ls"] : void 0;
|
|
15725
15847
|
}
|
|
@@ -15751,6 +15873,12 @@ function readAgentsFile(path) {
|
|
|
15751
15873
|
return null;
|
|
15752
15874
|
}
|
|
15753
15875
|
}
|
|
15876
|
+
function agentsMdPresence(repoRoot, globalDir) {
|
|
15877
|
+
return {
|
|
15878
|
+
project: readAgentsFile(join27(repoRoot, "AGENTS.md")) !== null,
|
|
15879
|
+
global: readAgentsFile(join27(globalDir, "agents.md")) !== null
|
|
15880
|
+
};
|
|
15881
|
+
}
|
|
15754
15882
|
function agentsMdBlock(repoRoot, globalDir) {
|
|
15755
15883
|
const global = readAgentsFile(join27(globalDir, "agents.md"));
|
|
15756
15884
|
const project = readAgentsFile(join27(repoRoot, "AGENTS.md"));
|
|
@@ -15769,6 +15897,7 @@ function buildAppendPrompt(mode, skills = [], agentsMd, hasWebTools = false) {
|
|
|
15769
15897
|
if (mode !== "plan") {
|
|
15770
15898
|
blocks.push(editReliabilityGuidance());
|
|
15771
15899
|
blocks.push(lspGuidance());
|
|
15900
|
+
blocks.push(todoTrackerGuidance());
|
|
15772
15901
|
}
|
|
15773
15902
|
if (hasWebTools) {
|
|
15774
15903
|
blocks.push(antiFallbackGuidance());
|
|
@@ -16040,12 +16169,9 @@ var COMPACTION_SUFFIX = "\n\n(End of summary \u2014 the actual conversation cont
|
|
|
16040
16169
|
function resolveEffectiveHistory(d, convId, sessionId) {
|
|
16041
16170
|
const all = d.db.getConversation(convId, true)?.messages ?? [];
|
|
16042
16171
|
const run = d.db.getAgentRun(sessionId);
|
|
16043
|
-
|
|
16044
|
-
const clearedIdx = run?.clearedUpToMessageId ? all.findIndex((m) => m.id === run.clearedUpToMessageId) : -1;
|
|
16045
|
-
if (clearedIdx !== -1 && clearedIdx >= compactionIdx) {
|
|
16046
|
-
return { summaryText: null, messages: all.slice(clearedIdx + 1) };
|
|
16047
|
-
}
|
|
16172
|
+
if (run?.clearedUpToMessageId) return { summaryText: null, messages: all };
|
|
16048
16173
|
if (!run?.compactionUpToMessageId) return { summaryText: null, messages: all };
|
|
16174
|
+
const compactionIdx = all.findIndex((m) => m.id === run.compactionUpToMessageId);
|
|
16049
16175
|
const rest = compactionIdx === -1 ? all : all.slice(compactionIdx + 1);
|
|
16050
16176
|
return { summaryText: `${COMPACTION_PREFIX}${run.compactionSummary}${COMPACTION_SUFFIX}`, messages: rest };
|
|
16051
16177
|
}
|
|
@@ -16074,6 +16200,7 @@ function keepRecentTokensFor(messages, baseKeepRecentTokens) {
|
|
|
16074
16200
|
var MUTATING_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "bash"]);
|
|
16075
16201
|
var PATH_TOOLS = /* @__PURE__ */ new Set(["read", "edit", "write", "grep", "find", "ls"]);
|
|
16076
16202
|
var LOOP_BREAK_AFTER = 3;
|
|
16203
|
+
var LOOP_ABORT_AFTER = LOOP_BREAK_AFTER + 3;
|
|
16077
16204
|
function toolCallSignature(name, input) {
|
|
16078
16205
|
const stable = (v) => {
|
|
16079
16206
|
if (v === null || typeof v !== "object") return JSON.stringify(v) ?? "undefined";
|
|
@@ -16122,6 +16249,99 @@ function disposeLspClientsForConv(convId) {
|
|
|
16122
16249
|
for (const c of clients.values()) c.dispose();
|
|
16123
16250
|
lspClientsByConv.delete(convId);
|
|
16124
16251
|
}
|
|
16252
|
+
function extractPartialText(partialResult) {
|
|
16253
|
+
const pr = partialResult;
|
|
16254
|
+
if (!pr || !Array.isArray(pr.content)) return "";
|
|
16255
|
+
return pr.content.map((c) => c?.type === "text" ? c.text ?? "" : "").join("");
|
|
16256
|
+
}
|
|
16257
|
+
function codeEventToFrame(ev, turnCounter) {
|
|
16258
|
+
switch (ev.type) {
|
|
16259
|
+
case "message_update": {
|
|
16260
|
+
const ame = ev.assistantMessageEvent;
|
|
16261
|
+
if (ame.type === "text_delta") return { event: "delta", data: { delta: ame.delta } };
|
|
16262
|
+
if (ame.type === "thinking_delta") return { event: "reasoning", data: { delta: ame.delta } };
|
|
16263
|
+
return null;
|
|
16264
|
+
}
|
|
16265
|
+
case "compaction_start":
|
|
16266
|
+
return { event: "compaction", data: { phase: "start", reason: ev.reason } };
|
|
16267
|
+
case "compaction_end":
|
|
16268
|
+
return { event: "compaction", data: { phase: "end", reason: ev.reason, aborted: ev.aborted, tokensBefore: ev.result?.tokensBefore } };
|
|
16269
|
+
case "turn_start":
|
|
16270
|
+
return { event: "turn", data: { phase: "start", index: turnCounter.index } };
|
|
16271
|
+
case "turn_end": {
|
|
16272
|
+
const frame = { event: "turn", data: { phase: "end", index: turnCounter.index, toolResults: ev.toolResults.length } };
|
|
16273
|
+
turnCounter.index++;
|
|
16274
|
+
return frame;
|
|
16275
|
+
}
|
|
16276
|
+
case "auto_retry_start":
|
|
16277
|
+
return { event: "retry", data: { phase: "start", attempt: ev.attempt, maxAttempts: ev.maxAttempts, delayMs: ev.delayMs, message: ev.errorMessage } };
|
|
16278
|
+
case "auto_retry_end":
|
|
16279
|
+
return { event: "retry", data: { phase: "end", attempt: ev.attempt, success: ev.success, message: ev.finalError } };
|
|
16280
|
+
case "tool_execution_update":
|
|
16281
|
+
return { event: "tool_progress", data: { id: ev.toolCallId, name: ev.toolName, partial: extractPartialText(ev.partialResult) } };
|
|
16282
|
+
default:
|
|
16283
|
+
return null;
|
|
16284
|
+
}
|
|
16285
|
+
}
|
|
16286
|
+
var DELEGATE_SUBAGENT_TIMEOUT_MS = 5 * 6e4;
|
|
16287
|
+
function validateDelegateTask(raw) {
|
|
16288
|
+
const task = typeof raw === "string" ? raw.trim() : "";
|
|
16289
|
+
if (!task) return { ok: false, message: "delegate_task: `task` must be a non-empty description of the sub-task to delegate." };
|
|
16290
|
+
return { ok: true, task };
|
|
16291
|
+
}
|
|
16292
|
+
function normalizeDelegateResult(finalText, opts) {
|
|
16293
|
+
const text = finalText.trim();
|
|
16294
|
+
if (opts?.timedOut) {
|
|
16295
|
+
const mins = Math.max(1, Math.round((opts.timeoutMs ?? DELEGATE_SUBAGENT_TIMEOUT_MS) / 6e4));
|
|
16296
|
+
const partial = text ? `
|
|
16297
|
+
|
|
16298
|
+
Partial progress before it was stopped:
|
|
16299
|
+
${text}` : "";
|
|
16300
|
+
return `(the delegated sub-task did not finish within ~${mins} minute(s) and was stopped \u2014 treat it as INCOMPLETE; consider doing it yourself or delegating a smaller piece.${partial})`;
|
|
16301
|
+
}
|
|
16302
|
+
return text || "(the delegated sub-agent produced no output.)";
|
|
16303
|
+
}
|
|
16304
|
+
var DELEGATED_SUBAGENT_PREAMBLE = "You are an isolated sub-agent handling ONE delegated sub-task on behalf of a parent coding agent. You do NOT see the parent conversation or its history \u2014 everything you need is in the task message below. Do exactly that task and nothing more, using your tools against the same repository. You cannot delegate further or invoke skills. When finished, end your reply with a concise, self-contained summary of what you did, what you found, and anything the parent needs in order to continue \u2014 that summary is the ONLY thing returned to the parent.";
|
|
16305
|
+
var TODO_STATUSES = /* @__PURE__ */ new Set(["pending", "in_progress", "completed"]);
|
|
16306
|
+
var MAX_TODOS = 50;
|
|
16307
|
+
function normalizeTodos(raw) {
|
|
16308
|
+
if (!Array.isArray(raw)) return [];
|
|
16309
|
+
const out = [];
|
|
16310
|
+
for (const item of raw) {
|
|
16311
|
+
if (!item || typeof item !== "object") continue;
|
|
16312
|
+
const rec = item;
|
|
16313
|
+
const content = typeof rec.content === "string" ? rec.content.trim() : "";
|
|
16314
|
+
if (!content) continue;
|
|
16315
|
+
const status = typeof rec.status === "string" && TODO_STATUSES.has(rec.status) ? rec.status : "pending";
|
|
16316
|
+
out.push({ content, status });
|
|
16317
|
+
if (out.length >= MAX_TODOS) break;
|
|
16318
|
+
}
|
|
16319
|
+
return out;
|
|
16320
|
+
}
|
|
16321
|
+
function summarizeTodos(todos) {
|
|
16322
|
+
if (todos.length === 0) return "Todo list cleared.";
|
|
16323
|
+
const completed = todos.filter((t) => t.status === "completed").length;
|
|
16324
|
+
const inProgress = todos.filter((t) => t.status === "in_progress").length;
|
|
16325
|
+
const parts = [`${completed}/${todos.length} done`];
|
|
16326
|
+
if (inProgress > 0) parts.push(`${inProgress} in progress`);
|
|
16327
|
+
return `Tracking ${todos.length} step(s): ${parts.join(", ")}.`;
|
|
16328
|
+
}
|
|
16329
|
+
var PREFILL_POLL_MS = 300;
|
|
16330
|
+
function isProcessingSlot(s) {
|
|
16331
|
+
if (!s || typeof s !== "object") return false;
|
|
16332
|
+
const r = s;
|
|
16333
|
+
return r.is_processing === true && typeof r.n_prompt_tokens === "number" && typeof r.n_prompt_tokens_processed === "number";
|
|
16334
|
+
}
|
|
16335
|
+
function pickPrefillProgress(slots) {
|
|
16336
|
+
if (!Array.isArray(slots)) return null;
|
|
16337
|
+
const processing = slots.filter(isProcessingSlot);
|
|
16338
|
+
if (processing.length !== 1) return null;
|
|
16339
|
+
const total = processing[0].n_prompt_tokens;
|
|
16340
|
+
const processed = processing[0].n_prompt_tokens_processed;
|
|
16341
|
+
if (!(total > 0) || processed < 0) return null;
|
|
16342
|
+
const pct = Math.min(100, Math.max(0, Math.round(processed / total * 100)));
|
|
16343
|
+
return { processed, total, pct };
|
|
16344
|
+
}
|
|
16125
16345
|
async function runCodeSession(params) {
|
|
16126
16346
|
const { d, convId, sessionId, repoRoot, mode, thinkingBudget, task: rawTask, signal, sink } = params;
|
|
16127
16347
|
const ms = d.manager.status();
|
|
@@ -16321,12 +16541,192 @@ async function runCodeSession(params) {
|
|
|
16321
16541
|
skillSession.dispose();
|
|
16322
16542
|
return finalText2.trim() || "(the skill produced no output)";
|
|
16323
16543
|
}
|
|
16544
|
+
async function runDelegatedSubSession(subMode, task2) {
|
|
16545
|
+
const subAuth = AuthStorage.inMemory();
|
|
16546
|
+
const subRegistry = ModelRegistry.inMemory(subAuth);
|
|
16547
|
+
subRegistry.registerProvider("local", {
|
|
16548
|
+
baseUrl: `${target}/v1`,
|
|
16549
|
+
apiKey: "agent-key",
|
|
16550
|
+
authHeader: true,
|
|
16551
|
+
api: "openai-completions",
|
|
16552
|
+
models: [{
|
|
16553
|
+
id: modelId,
|
|
16554
|
+
name: modelDisplayName,
|
|
16555
|
+
reasoning: false,
|
|
16556
|
+
input: ["text"],
|
|
16557
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
16558
|
+
contextWindow,
|
|
16559
|
+
maxTokens: 8192,
|
|
16560
|
+
compat: { supportsDeveloperRole: false, thinkingFormat: "qwen-chat-template" }
|
|
16561
|
+
}]
|
|
16562
|
+
});
|
|
16563
|
+
const subModel = subRegistry.find("local", modelId);
|
|
16564
|
+
if (!subModel) throw new Error("Failed to register local model with pi.");
|
|
16565
|
+
const subAc = new AbortController();
|
|
16566
|
+
let timedOut = false;
|
|
16567
|
+
const onParentAbort = () => subAc.abort();
|
|
16568
|
+
if (signal.aborted) subAc.abort();
|
|
16569
|
+
else signal.addEventListener("abort", onParentAbort, { once: true });
|
|
16570
|
+
const timer = setTimeout(() => {
|
|
16571
|
+
timedOut = true;
|
|
16572
|
+
subAc.abort();
|
|
16573
|
+
}, DELEGATE_SUBAGENT_TIMEOUT_MS);
|
|
16574
|
+
let subHeldGate;
|
|
16575
|
+
const releaseSubGate = () => {
|
|
16576
|
+
const r = subHeldGate;
|
|
16577
|
+
subHeldGate = void 0;
|
|
16578
|
+
r?.();
|
|
16579
|
+
};
|
|
16580
|
+
const subExtension = (pi) => {
|
|
16581
|
+
pi.on("before_provider_request", async (event) => {
|
|
16582
|
+
releaseSubGate();
|
|
16583
|
+
if (d.gate) subHeldGate = await d.gate.acquire("bg", { signal: subAc.signal });
|
|
16584
|
+
const payload = { ...event.payload };
|
|
16585
|
+
delete payload.max_tokens;
|
|
16586
|
+
delete payload.max_completion_tokens;
|
|
16587
|
+
return payload;
|
|
16588
|
+
});
|
|
16589
|
+
pi.on("after_provider_response", () => {
|
|
16590
|
+
releaseSubGate();
|
|
16591
|
+
});
|
|
16592
|
+
pi.on("tool_call", async (event) => {
|
|
16593
|
+
const toolName = event.toolName;
|
|
16594
|
+
const toolCallId = event.toolCallId;
|
|
16595
|
+
const input = event.input;
|
|
16596
|
+
await sink({ event: "tool_call", data: { id: toolCallId, name: toolName, args: input, status: "pending" } });
|
|
16597
|
+
if (PATH_TOOLS.has(toolName)) {
|
|
16598
|
+
const p = input.path;
|
|
16599
|
+
const pathRequired = toolName === "read" || toolName === "edit" || toolName === "write";
|
|
16600
|
+
if (p !== void 0 && typeof p === "string") {
|
|
16601
|
+
if (!isContainedFromRoot(p, repoRoot)) {
|
|
16602
|
+
const reason = "path is outside the allowed repo root";
|
|
16603
|
+
await sink({ event: "tool_call", data: { id: toolCallId, name: toolName, args: input, status: "error", result: reason } });
|
|
16604
|
+
return { block: true, reason };
|
|
16605
|
+
}
|
|
16606
|
+
} else if (pathRequired) {
|
|
16607
|
+
const reason = `${toolName}: a valid path is required`;
|
|
16608
|
+
await sink({ event: "tool_call", data: { id: toolCallId, name: toolName, args: input, status: "error", result: reason } });
|
|
16609
|
+
return { block: true, reason };
|
|
16610
|
+
}
|
|
16611
|
+
}
|
|
16612
|
+
if (subMode === "ask" && MUTATING_TOOLS.has(toolName)) {
|
|
16613
|
+
await sink({ event: "tool_call", data: { id: toolCallId, name: toolName, args: input, status: "awaiting_approval" } });
|
|
16614
|
+
const decision = await waitForToolApproval(`${convId}:${toolCallId}`, subAc.signal);
|
|
16615
|
+
if (decision === "deny") {
|
|
16616
|
+
const reason = "denied by user";
|
|
16617
|
+
await sink({ event: "tool_call", data: { id: toolCallId, name: toolName, args: input, status: "error", result: reason } });
|
|
16618
|
+
return { block: true, reason };
|
|
16619
|
+
}
|
|
16620
|
+
}
|
|
16621
|
+
return;
|
|
16622
|
+
});
|
|
16623
|
+
pi.on("tool_result", async (event) => {
|
|
16624
|
+
const text = event.content.map((c) => c.type === "text" ? c.text : "").join("");
|
|
16625
|
+
const data = {
|
|
16626
|
+
id: event.toolCallId,
|
|
16627
|
+
name: event.toolName,
|
|
16628
|
+
args: event.input,
|
|
16629
|
+
status: event.isError ? "error" : "done",
|
|
16630
|
+
result: text
|
|
16631
|
+
};
|
|
16632
|
+
if (isEditToolResult(event) && event.details) {
|
|
16633
|
+
data.diff = event.details.diff;
|
|
16634
|
+
data.patch = event.details.patch;
|
|
16635
|
+
data.firstChangedLine = event.details.firstChangedLine;
|
|
16636
|
+
}
|
|
16637
|
+
await sink({ event: "tool_call", data });
|
|
16638
|
+
});
|
|
16639
|
+
};
|
|
16640
|
+
const subAgentDir = join28(d.store.dir(), "pi-agent");
|
|
16641
|
+
const subResourceLoader = new DefaultResourceLoader({
|
|
16642
|
+
cwd: repoRoot,
|
|
16643
|
+
agentDir: subAgentDir,
|
|
16644
|
+
settingsManager: SettingsManager.inMemory(),
|
|
16645
|
+
extensionFactories: [{ name: "turbollm-delegate", factory: subExtension }],
|
|
16646
|
+
// The normal mode persona (no skill catalog — a delegated sub-agent can't invoke skills),
|
|
16647
|
+
// plus the delegation preamble that frames it as a focused, summary-returning worker.
|
|
16648
|
+
appendSystemPrompt: [...buildAppendPrompt(subMode, [], { repoRoot, globalDir: d.store.dir() }, !!d.tools), DELEGATED_SUBAGENT_PREAMBLE],
|
|
16649
|
+
noSkills: true,
|
|
16650
|
+
noPromptTemplates: true,
|
|
16651
|
+
noThemes: true
|
|
16652
|
+
});
|
|
16653
|
+
await subResourceLoader.reload();
|
|
16654
|
+
const subTools = toolsForMode(subMode);
|
|
16655
|
+
const subBash = createBashTool(repoRoot, { operations: createRobustBashOperations({ sessionLabel: `${sessionId}:delegate` }) });
|
|
16656
|
+
const { session: subSession } = await createAgentSession({
|
|
16657
|
+
cwd: repoRoot,
|
|
16658
|
+
agentDir: subAgentDir,
|
|
16659
|
+
model: subModel,
|
|
16660
|
+
authStorage: subAuth,
|
|
16661
|
+
modelRegistry: subRegistry,
|
|
16662
|
+
resourceLoader: subResourceLoader,
|
|
16663
|
+
sessionManager: SessionManager.inMemory(repoRoot),
|
|
16664
|
+
settingsManager: SettingsManager.inMemory({ compaction: compactionSettingsFor(contextWindow) }),
|
|
16665
|
+
customTools: [subBash],
|
|
16666
|
+
...subTools ? { tools: subTools } : {}
|
|
16667
|
+
// No invoke_skill / delegate_task / web / mcp / lsp tools registered → depth-1, bounded set.
|
|
16668
|
+
});
|
|
16669
|
+
const onSubAbort = () => {
|
|
16670
|
+
void subSession.abort();
|
|
16671
|
+
};
|
|
16672
|
+
if (subAc.signal.aborted) onSubAbort();
|
|
16673
|
+
else subAc.signal.addEventListener("abort", onSubAbort, { once: true });
|
|
16674
|
+
try {
|
|
16675
|
+
await subSession.prompt(task2);
|
|
16676
|
+
} catch (e) {
|
|
16677
|
+
if (!timedOut && !subAc.signal.aborted) throw e;
|
|
16678
|
+
} finally {
|
|
16679
|
+
clearTimeout(timer);
|
|
16680
|
+
signal.removeEventListener("abort", onParentAbort);
|
|
16681
|
+
releaseSubGate();
|
|
16682
|
+
}
|
|
16683
|
+
const finalText2 = subSession.getLastAssistantText() ?? "";
|
|
16684
|
+
subSession.dispose();
|
|
16685
|
+
return normalizeDelegateResult(finalText2, timedOut ? { timedOut: true, timeoutMs: DELEGATE_SUBAGENT_TIMEOUT_MS } : void 0);
|
|
16686
|
+
}
|
|
16324
16687
|
const mcpToolDefs = d.tools ? (await d.tools.buildToolDefinitions()).filter((t) => t.function.name.startsWith("mcp__")) : [];
|
|
16325
16688
|
const mcpToolNames = new Set(mcpToolDefs.map((t) => t.function.name));
|
|
16689
|
+
let prefillTimer;
|
|
16690
|
+
let prefillActive = false;
|
|
16691
|
+
const stopPrefillPoll = () => {
|
|
16692
|
+
prefillActive = false;
|
|
16693
|
+
if (prefillTimer) {
|
|
16694
|
+
clearTimeout(prefillTimer);
|
|
16695
|
+
prefillTimer = void 0;
|
|
16696
|
+
}
|
|
16697
|
+
};
|
|
16698
|
+
const startPrefillPoll = () => {
|
|
16699
|
+
stopPrefillPoll();
|
|
16700
|
+
prefillActive = true;
|
|
16701
|
+
let lastPct = -1;
|
|
16702
|
+
const tick2 = async () => {
|
|
16703
|
+
if (!prefillActive) return;
|
|
16704
|
+
try {
|
|
16705
|
+
const res = await fetch(`${target}/slots`, { signal });
|
|
16706
|
+
if (res.ok) {
|
|
16707
|
+
const progress = pickPrefillProgress(await res.json());
|
|
16708
|
+
if (progress && prefillActive) {
|
|
16709
|
+
if (progress.pct !== lastPct) {
|
|
16710
|
+
lastPct = progress.pct;
|
|
16711
|
+
void sink({ event: "prefill", data: { ...progress } });
|
|
16712
|
+
}
|
|
16713
|
+
if (progress.processed >= progress.total) {
|
|
16714
|
+
stopPrefillPoll();
|
|
16715
|
+
return;
|
|
16716
|
+
}
|
|
16717
|
+
}
|
|
16718
|
+
}
|
|
16719
|
+
} catch {
|
|
16720
|
+
}
|
|
16721
|
+
if (prefillActive) prefillTimer = setTimeout(() => void tick2(), PREFILL_POLL_MS);
|
|
16722
|
+
};
|
|
16723
|
+
prefillTimer = setTimeout(() => void tick2(), PREFILL_POLL_MS);
|
|
16724
|
+
};
|
|
16326
16725
|
const extension = (pi) => {
|
|
16327
16726
|
pi.on("before_provider_request", async (event) => {
|
|
16328
16727
|
releaseGate();
|
|
16329
16728
|
if (d.gate) heldGate = await d.gate.acquire("bg", { signal });
|
|
16729
|
+
startPrefillPoll();
|
|
16330
16730
|
const payload = { ...event.payload };
|
|
16331
16731
|
delete payload.max_tokens;
|
|
16332
16732
|
delete payload.max_completion_tokens;
|
|
@@ -16343,6 +16743,7 @@ async function runCodeSession(params) {
|
|
|
16343
16743
|
return payload;
|
|
16344
16744
|
});
|
|
16345
16745
|
pi.on("after_provider_response", () => {
|
|
16746
|
+
stopPrefillPoll();
|
|
16346
16747
|
releaseGate();
|
|
16347
16748
|
});
|
|
16348
16749
|
pi.on("tool_call", async (event) => {
|
|
@@ -16350,7 +16751,14 @@ async function runCodeSession(params) {
|
|
|
16350
16751
|
const toolCallId = event.toolCallId;
|
|
16351
16752
|
const input = event.input;
|
|
16352
16753
|
await sink({ event: "tool_call", data: { id: toolCallId, name: toolName, args: input, status: "pending" } });
|
|
16353
|
-
|
|
16754
|
+
const loopCount = toolLoop.record(toolName, input);
|
|
16755
|
+
if (loopCount > LOOP_ABORT_AFTER) {
|
|
16756
|
+
const reason = `[SYSTEM: \`${toolName}\` was called with identical arguments ${loopCount} times in a row, including after being told to stop \u2014 this run has been stopped automatically rather than continue looping. Start a new turn with different instructions to try again.]`;
|
|
16757
|
+
await sink({ event: "tool_call", data: { id: toolCallId, name: toolName, args: input, status: "error", result: reason } });
|
|
16758
|
+
void session.abort();
|
|
16759
|
+
return { block: true, reason };
|
|
16760
|
+
}
|
|
16761
|
+
if (loopCount > LOOP_BREAK_AFTER) {
|
|
16354
16762
|
const reason = `[SYSTEM: you have called \`${toolName}\` with identical arguments too many times in a row and it is not making progress \u2014 this call was NOT executed. Stop repeating it: take a different action, call it with different arguments, or, if you already have what you need, give your final answer now.]`;
|
|
16355
16763
|
await sink({ event: "tool_call", data: { id: toolCallId, name: toolName, args: input, status: "error", result: reason } });
|
|
16356
16764
|
return { block: true, reason };
|
|
@@ -16473,6 +16881,50 @@ ${lines.join("\n")}${more}]`
|
|
|
16473
16881
|
}
|
|
16474
16882
|
}
|
|
16475
16883
|
});
|
|
16884
|
+
pi.registerTool({
|
|
16885
|
+
name: "delegate_task",
|
|
16886
|
+
label: "Delegate task",
|
|
16887
|
+
description: `Delegate a focused, self-contained sub-task to an isolated sub-agent that has your same repository access and tools (subject to the current mode), runs it to completion, and returns ONLY its final summary \u2014 the sub-agent's step-by-step work never enters this conversation, so your own context stays focused. Use it for a well-scoped chunk you can describe completely up front (e.g. "investigate how X works and report back", or "apply this specific refactor across these files"). The sub-agent does NOT see this conversation, so put everything it needs in the task; it cannot delegate further or invoke skills.`,
|
|
16888
|
+
promptSnippet: "delegate_task(task) - hand a focused, fully-described sub-task to an isolated sub-agent and get back its summary",
|
|
16889
|
+
parameters: Type.Object({
|
|
16890
|
+
task: Type.String({ description: "The complete, self-contained sub-task, including every bit of context the sub-agent needs (it does not see this conversation or its history)." })
|
|
16891
|
+
}),
|
|
16892
|
+
async execute(_toolCallId, params2) {
|
|
16893
|
+
const valid = validateDelegateTask(params2.task);
|
|
16894
|
+
if (!valid.ok) return { content: [{ type: "text", text: valid.message }], details: {} };
|
|
16895
|
+
const dbMode = d.db.getConversation(convId)?.agentMode ?? mode;
|
|
16896
|
+
const liveMode = dbMode === "plan" ? mode : dbMode;
|
|
16897
|
+
try {
|
|
16898
|
+
const text = await runDelegatedSubSession(liveMode, valid.task);
|
|
16899
|
+
return { content: [{ type: "text", text }], details: {} };
|
|
16900
|
+
} catch (e) {
|
|
16901
|
+
return { content: [{ type: "text", text: `delegate_task: failed (${e instanceof Error ? e.message : String(e)}) \u2014 try again, or do the task yourself.` }], details: {} };
|
|
16902
|
+
}
|
|
16903
|
+
}
|
|
16904
|
+
});
|
|
16905
|
+
pi.registerTool({
|
|
16906
|
+
name: "update_todos",
|
|
16907
|
+
label: "Update todos",
|
|
16908
|
+
description: "Maintain a visible checklist of the steps for a multi-step task so the user can see your plan and live progress. Call it when you START a task that has several distinct steps (send the full list, each step pending), and again whenever a step's state changes: mark a step in_progress when you begin it and completed as soon as it is done. ALWAYS send the ENTIRE current list each call \u2014 it REPLACES the previous one, it is not a diff. Skip it for trivial single-step requests; a one-item checklist is just noise.",
|
|
16909
|
+
promptSnippet: "update_todos(todos) - show/refresh a checklist of steps for a multi-step task (send the whole list each call)",
|
|
16910
|
+
parameters: Type.Object({
|
|
16911
|
+
todos: Type.Array(
|
|
16912
|
+
Type.Object({
|
|
16913
|
+
content: Type.String({ description: 'The step as a short imperative phrase, e.g. "Add the /export route".' }),
|
|
16914
|
+
status: Type.Union(
|
|
16915
|
+
[Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("completed")],
|
|
16916
|
+
{ description: "pending, in_progress, or completed." }
|
|
16917
|
+
)
|
|
16918
|
+
}),
|
|
16919
|
+
{ description: "The COMPLETE current step list, in order \u2014 replaces the previous list entirely." }
|
|
16920
|
+
)
|
|
16921
|
+
}),
|
|
16922
|
+
async execute(_toolCallId, params2) {
|
|
16923
|
+
const todos = normalizeTodos(params2.todos);
|
|
16924
|
+
await sink({ event: "todos", data: { todos } });
|
|
16925
|
+
return { content: [{ type: "text", text: summarizeTodos(todos) }], details: {} };
|
|
16926
|
+
}
|
|
16927
|
+
});
|
|
16476
16928
|
if (d.tools) {
|
|
16477
16929
|
const tools2 = d.tools;
|
|
16478
16930
|
pi.registerTool({
|
|
@@ -16592,24 +17044,24 @@ ${lines.join("\n")}${more}]`
|
|
|
16592
17044
|
};
|
|
16593
17045
|
if (signal.aborted) onAbort();
|
|
16594
17046
|
else signal.addEventListener("abort", onAbort, { once: true });
|
|
17047
|
+
const turnCounter = { index: 0 };
|
|
16595
17048
|
const unsubscribe = session.subscribe((ev) => {
|
|
16596
|
-
if (ev.type === "message_update")
|
|
16597
|
-
|
|
16598
|
-
|
|
16599
|
-
void sink({ event: "delta", data: { delta: ame.delta } });
|
|
16600
|
-
} else if (ame.type === "thinking_delta") {
|
|
16601
|
-
void sink({ event: "reasoning", data: { delta: ame.delta } });
|
|
16602
|
-
}
|
|
16603
|
-
} else if (ev.type === "compaction_start") {
|
|
16604
|
-
void sink({ event: "compaction", data: { phase: "start", reason: ev.reason } });
|
|
16605
|
-
} else if (ev.type === "compaction_end") {
|
|
16606
|
-
void sink({ event: "compaction", data: { phase: "end", reason: ev.reason, aborted: ev.aborted, tokensBefore: ev.result?.tokensBefore } });
|
|
16607
|
-
}
|
|
17049
|
+
if (ev.type === "message_update") stopPrefillPoll();
|
|
17050
|
+
const frame = codeEventToFrame(ev, turnCounter);
|
|
17051
|
+
if (frame) void sink(frame);
|
|
16608
17052
|
});
|
|
17053
|
+
const steerHandle = async (text) => {
|
|
17054
|
+
if (!session.isStreaming) return false;
|
|
17055
|
+
await session.steer(text);
|
|
17056
|
+
return true;
|
|
17057
|
+
};
|
|
16609
17058
|
d.manager.generationStart();
|
|
17059
|
+
params.onSteerable?.(steerHandle);
|
|
16610
17060
|
try {
|
|
16611
17061
|
await session.prompt(task);
|
|
16612
17062
|
} finally {
|
|
17063
|
+
params.onSteerable?.(null);
|
|
17064
|
+
stopPrefillPoll();
|
|
16613
17065
|
unsubscribe();
|
|
16614
17066
|
releaseGate();
|
|
16615
17067
|
d.manager.generationEnd();
|
|
@@ -16769,7 +17221,12 @@ var CodeRunManager = class {
|
|
|
16769
17221
|
* (not just index) identifies each entry so a per-chip action (sendNow) can target one
|
|
16770
17222
|
* specific queued turn even if two queued tasks have identical text. */
|
|
16771
17223
|
queued(sessionId) {
|
|
16772
|
-
return this.sessions.get(sessionId)?.queue.map((t) => ({ userMsgId: t.userMsgId, task: t.task })) ?? [];
|
|
17224
|
+
return this.sessions.get(sessionId)?.queue.map((t) => ({ userMsgId: t.userMsgId, task: t.task, kind: t.kind })) ?? [];
|
|
17225
|
+
}
|
|
17226
|
+
/** The active turn's current todo/step checklist (ADR-255), for a (re)connecting client to show
|
|
17227
|
+
* up front. Empty when no turn has emitted one this turn (or the session is unknown). */
|
|
17228
|
+
todos(sessionId) {
|
|
17229
|
+
return this.sessions.get(sessionId)?.todos ?? [];
|
|
16773
17230
|
}
|
|
16774
17231
|
/** The in-flight turn's message ids, so a reconnecting stream can synthesize a `meta` frame
|
|
16775
17232
|
* even if the real one has already aged out of the ring buffer. */
|
|
@@ -16782,7 +17239,7 @@ var CodeRunManager = class {
|
|
|
16782
17239
|
if (!s) {
|
|
16783
17240
|
const emitter = new EventEmitter();
|
|
16784
17241
|
emitter.setMaxListeners(0);
|
|
16785
|
-
s = { convId, repoRoot, buffer: new RingBuffer(), emitter, queue: [], active: null, replayFloor: 0 };
|
|
17242
|
+
s = { convId, repoRoot, buffer: new RingBuffer(), emitter, queue: [], active: null, replayFloor: 0, todos: [] };
|
|
16786
17243
|
this.sessions.set(sessionId, s);
|
|
16787
17244
|
}
|
|
16788
17245
|
if (s.cleanupTimer) {
|
|
@@ -16801,11 +17258,36 @@ var CodeRunManager = class {
|
|
|
16801
17258
|
enqueue(sessionId, params) {
|
|
16802
17259
|
const s = this.ensure(sessionId, params.convId, params.repoRoot);
|
|
16803
17260
|
const willQueue = s.active !== null;
|
|
16804
|
-
s.queue.push({ task: params.task, userMsgId: params.userMsgId, thinkingBudget: params.thinkingBudget ?? -1 });
|
|
17261
|
+
s.queue.push({ task: params.task, userMsgId: params.userMsgId, thinkingBudget: params.thinkingBudget ?? -1, kind: params.kind ?? "followUp" });
|
|
16805
17262
|
this.emitQueue(sessionId);
|
|
16806
17263
|
void this.pump(sessionId);
|
|
16807
17264
|
return { queued: willQueue };
|
|
16808
17265
|
}
|
|
17266
|
+
/**
|
|
17267
|
+
* Steer a message into the CURRENTLY ACTIVE turn (pi's session.steer, reached via the handle
|
|
17268
|
+
* runCodeSession registers) so it redirects the running turn instead of queueing a fresh one
|
|
17269
|
+
* behind it.
|
|
17270
|
+
*
|
|
17271
|
+
* Falls back to enqueue() (follow-up/queue) rather than erroring when there is nothing live to
|
|
17272
|
+
* steer — either no active turn at all, or the active turn stopped streaming in the moment
|
|
17273
|
+
* before we reached it (a race: the turn just finished). In the fallback the message is never
|
|
17274
|
+
* dropped; it simply runs as the next turn instead, and its queue entry keeps kind:'steer' so
|
|
17275
|
+
* the UI can still tell it apart from a plain follow-up.
|
|
17276
|
+
*
|
|
17277
|
+
* Returns `steered` (delivered into the live turn) and, when it wasn't, `queued` (whether the
|
|
17278
|
+
* fallback had to wait behind an active turn, mirroring enqueue's own return).
|
|
17279
|
+
*/
|
|
17280
|
+
async steer(sessionId, params) {
|
|
17281
|
+
const active = this.sessions.get(sessionId)?.active;
|
|
17282
|
+
if (active?.steer) {
|
|
17283
|
+
try {
|
|
17284
|
+
if (await active.steer(params.task)) return { steered: true, queued: false };
|
|
17285
|
+
} catch {
|
|
17286
|
+
}
|
|
17287
|
+
}
|
|
17288
|
+
const { queued } = this.enqueue(sessionId, { ...params, kind: "steer" });
|
|
17289
|
+
return { steered: false, queued };
|
|
17290
|
+
}
|
|
16809
17291
|
/** The most recent real ctxUsed/ctxMax on record for this conversation (the last assistant
|
|
16810
17292
|
* turn that actually completed and got a genuine stats.ctxUsed from runCodeSession), or
|
|
16811
17293
|
* undefined if none exists yet. Used when a turn ABORTS before runCodeSession returns a
|
|
@@ -16849,8 +17331,9 @@ var CodeRunManager = class {
|
|
|
16849
17331
|
this.emitQueue(sessionId);
|
|
16850
17332
|
const assistantMsg = this.d.db.addMessage(s.convId, "assistant", "", { stats: { aborted: false } });
|
|
16851
17333
|
const ac = new AbortController();
|
|
16852
|
-
s.active = { ac, assistantMsgId: assistantMsg.id, userMsgId: turn.userMsgId };
|
|
17334
|
+
s.active = { ac, assistantMsgId: assistantMsg.id, userMsgId: turn.userMsgId, steer: null };
|
|
16853
17335
|
s.replayFloor = s.buffer.head();
|
|
17336
|
+
s.todos = [];
|
|
16854
17337
|
const push = (event, data) => {
|
|
16855
17338
|
const ss = this.sessions.get(sessionId);
|
|
16856
17339
|
if (!ss) return;
|
|
@@ -16872,7 +17355,10 @@ var CodeRunManager = class {
|
|
|
16872
17355
|
if (last && last.type === "text") last.text += delta;
|
|
16873
17356
|
else timeline.push({ type: "text", text: delta });
|
|
16874
17357
|
} else if (ev.event === "reasoning") reasoning += String(data.delta ?? "");
|
|
16875
|
-
else if (ev.event === "
|
|
17358
|
+
else if (ev.event === "todos") {
|
|
17359
|
+
const ss = this.sessions.get(sessionId);
|
|
17360
|
+
if (ss) ss.todos = Array.isArray(data.todos) ? data.todos : [];
|
|
17361
|
+
} else if (ev.event === "tool_call" && (data.status === "done" || data.status === "error")) {
|
|
16876
17362
|
const id = String(data.id ?? "");
|
|
16877
17363
|
toolCalls.push({
|
|
16878
17364
|
id,
|
|
@@ -16904,7 +17390,14 @@ var CodeRunManager = class {
|
|
|
16904
17390
|
thinkingBudget: turn.thinkingBudget,
|
|
16905
17391
|
task: turn.task,
|
|
16906
17392
|
signal: ac.signal,
|
|
16907
|
-
sink
|
|
17393
|
+
sink,
|
|
17394
|
+
// Publish/withdraw the live turn's steer handle so steer() can inject into THIS turn.
|
|
17395
|
+
// Reads s.active fresh each call (rather than closing over the ActiveTurn captured above)
|
|
17396
|
+
// so a null clear can't accidentally resurrect a handle onto a since-replaced turn.
|
|
17397
|
+
onSteerable: (steer) => {
|
|
17398
|
+
const cur = this.sessions.get(sessionId)?.active;
|
|
17399
|
+
if (cur && cur.ac === ac) cur.steer = steer;
|
|
17400
|
+
}
|
|
16908
17401
|
});
|
|
16909
17402
|
const finalContent = content.trim() || result.finalText;
|
|
16910
17403
|
if (!content.trim() && result.finalText.trim()) timeline.push({ type: "text", text: result.finalText });
|
|
@@ -17130,6 +17623,297 @@ function revertFileEdits(messages, repoRoot) {
|
|
|
17130
17623
|
return { reverted, failed };
|
|
17131
17624
|
}
|
|
17132
17625
|
|
|
17626
|
+
// src/code/session-export.ts
|
|
17627
|
+
var EXPORT_TRUNCATE_LIMIT = 2e4;
|
|
17628
|
+
function truncateForExport(text, limit = EXPORT_TRUNCATE_LIMIT) {
|
|
17629
|
+
if (text.length <= limit) return text;
|
|
17630
|
+
const cut = text.length - limit;
|
|
17631
|
+
return `${text.slice(0, limit)}
|
|
17632
|
+
|
|
17633
|
+
\u2026 [truncated \u2014 ${cut.toLocaleString()} more character${cut === 1 ? "" : "s"}] \u2026`;
|
|
17634
|
+
}
|
|
17635
|
+
function fence(lang, body6) {
|
|
17636
|
+
return `\`\`\`${lang}
|
|
17637
|
+
${body6}
|
|
17638
|
+
\`\`\``;
|
|
17639
|
+
}
|
|
17640
|
+
function renderReasoning(reasoning) {
|
|
17641
|
+
if (!reasoning.trim()) return "";
|
|
17642
|
+
return `<details>
|
|
17643
|
+
<summary>Reasoning</summary>
|
|
17644
|
+
|
|
17645
|
+
${reasoning.trim()}
|
|
17646
|
+
|
|
17647
|
+
</details>
|
|
17648
|
+
|
|
17649
|
+
`;
|
|
17650
|
+
}
|
|
17651
|
+
function renderToolCall(call) {
|
|
17652
|
+
const parts = [`### Tool call: \`${call.name}\``];
|
|
17653
|
+
const argsJson = (() => {
|
|
17654
|
+
try {
|
|
17655
|
+
return JSON.stringify(call.args, null, 2);
|
|
17656
|
+
} catch {
|
|
17657
|
+
return String(call.args);
|
|
17658
|
+
}
|
|
17659
|
+
})();
|
|
17660
|
+
if (argsJson && argsJson !== "{}") parts.push(fence("json", truncateForExport(argsJson)));
|
|
17661
|
+
if (call.diff) {
|
|
17662
|
+
parts.push(fence("diff", truncateForExport(call.diff)));
|
|
17663
|
+
} else if (call.error) {
|
|
17664
|
+
parts.push(`**Error:**
|
|
17665
|
+
|
|
17666
|
+
${fence("text", truncateForExport(call.error))}`);
|
|
17667
|
+
} else if (call.result) {
|
|
17668
|
+
parts.push(fence("text", truncateForExport(call.result)));
|
|
17669
|
+
}
|
|
17670
|
+
return parts.join("\n\n");
|
|
17671
|
+
}
|
|
17672
|
+
function legacyTimeline(message) {
|
|
17673
|
+
const blocks = [];
|
|
17674
|
+
if (message.content) blocks.push({ type: "text", text: message.content });
|
|
17675
|
+
for (const call of message.toolCalls) blocks.push({ type: "tool", id: call.id });
|
|
17676
|
+
return blocks;
|
|
17677
|
+
}
|
|
17678
|
+
function renderAssistantMessage(message) {
|
|
17679
|
+
const toolsById = new Map(message.toolCalls.map((t) => [t.id, t]));
|
|
17680
|
+
const timeline = message.timeline?.length ? message.timeline : legacyTimeline(message);
|
|
17681
|
+
const parts = ["## Assistant"];
|
|
17682
|
+
const reasoning = renderReasoning(message.reasoning);
|
|
17683
|
+
if (reasoning) parts.push(reasoning.trimEnd());
|
|
17684
|
+
for (const block of timeline) {
|
|
17685
|
+
if (block.type === "text") {
|
|
17686
|
+
if (block.text.trim()) parts.push(block.text.trim());
|
|
17687
|
+
} else {
|
|
17688
|
+
const call = toolsById.get(block.id);
|
|
17689
|
+
if (call) parts.push(renderToolCall(call));
|
|
17690
|
+
}
|
|
17691
|
+
}
|
|
17692
|
+
if (parts.length === 1) parts.push("*(no content)*");
|
|
17693
|
+
return parts.join("\n\n");
|
|
17694
|
+
}
|
|
17695
|
+
function renderUserMessage(message) {
|
|
17696
|
+
const parts = ["## You", message.content.trim() || "*(empty message)*"];
|
|
17697
|
+
if (message.textAttachments.length) {
|
|
17698
|
+
parts.push(`**Attached:** ${message.textAttachments.map((p) => `\`${p}\``).join(", ")}`);
|
|
17699
|
+
}
|
|
17700
|
+
return parts.join("\n\n");
|
|
17701
|
+
}
|
|
17702
|
+
function renderMessage(message) {
|
|
17703
|
+
return message.role === "user" ? renderUserMessage(message) : renderAssistantMessage(message);
|
|
17704
|
+
}
|
|
17705
|
+
function serializeCodeSessionMarkdown(run, conv, opts = {}) {
|
|
17706
|
+
const exportedAt = opts.exportedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
17707
|
+
const title = run.title.trim() || "Untitled Code session";
|
|
17708
|
+
const meta = [
|
|
17709
|
+
`- **Session ID:** ${run.id}`,
|
|
17710
|
+
run.repoRoot ? `- **Repo:** ${run.repoRoot}${run.repoBranch ? ` (branch: ${run.repoBranch})` : ""}` : void 0,
|
|
17711
|
+
`- **Mode:** ${conv.agentMode ?? "auto"}`,
|
|
17712
|
+
`- **Status:** ${run.status}`,
|
|
17713
|
+
`- **Created:** ${run.createdAt}`,
|
|
17714
|
+
`- **Exported:** ${exportedAt}`
|
|
17715
|
+
].filter((line) => !!line);
|
|
17716
|
+
const messages = (conv.messages ?? []).slice().sort((a, b) => a.seq - b.seq);
|
|
17717
|
+
const body6 = messages.length ? messages.map(renderMessage).join("\n\n---\n\n") : "*(no messages in this session yet)*";
|
|
17718
|
+
return `# ${title}
|
|
17719
|
+
|
|
17720
|
+
${meta.join("\n")}
|
|
17721
|
+
|
|
17722
|
+
---
|
|
17723
|
+
|
|
17724
|
+
${body6}
|
|
17725
|
+
`;
|
|
17726
|
+
}
|
|
17727
|
+
var MAX_FILENAME_SLUG = 80;
|
|
17728
|
+
function codeSessionExportFilename(title, createdAt, ext) {
|
|
17729
|
+
const slug = title.replace(/[^a-zA-Z0-9 _-]/g, "").trim().replace(/\s+/g, "-").slice(0, MAX_FILENAME_SLUG).replace(/-+$/, "") || "code-session";
|
|
17730
|
+
const dateStr = createdAt.slice(0, 10) || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
17731
|
+
return `${slug}-${dateStr}.${ext}`;
|
|
17732
|
+
}
|
|
17733
|
+
|
|
17734
|
+
// src/code/git-actions.ts
|
|
17735
|
+
import { execFile as execFile12 } from "child_process";
|
|
17736
|
+
import { promisify as promisify9 } from "util";
|
|
17737
|
+
var execFileAsync = promisify9(execFile12);
|
|
17738
|
+
var GIT_TIMEOUT_MS = 15e3;
|
|
17739
|
+
var GIT_NETWORK_TIMEOUT_MS = 3e4;
|
|
17740
|
+
var GitError = class extends Error {
|
|
17741
|
+
stderr;
|
|
17742
|
+
exitCode;
|
|
17743
|
+
constructor(message, stderr, exitCode) {
|
|
17744
|
+
super(message);
|
|
17745
|
+
this.name = "GitError";
|
|
17746
|
+
this.stderr = stderr;
|
|
17747
|
+
this.exitCode = exitCode;
|
|
17748
|
+
}
|
|
17749
|
+
};
|
|
17750
|
+
async function git(args, cwd, timeoutMs = GIT_TIMEOUT_MS) {
|
|
17751
|
+
try {
|
|
17752
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
17753
|
+
cwd,
|
|
17754
|
+
timeout: timeoutMs,
|
|
17755
|
+
windowsHide: true,
|
|
17756
|
+
// Never block on an interactive username/password/passphrase prompt — surface the auth
|
|
17757
|
+
// failure as a normal command error instead of hanging the request.
|
|
17758
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
17759
|
+
});
|
|
17760
|
+
return stdout.trim();
|
|
17761
|
+
} catch (e) {
|
|
17762
|
+
const failure = e;
|
|
17763
|
+
if (failure.killed) throw new GitError(`git ${args[0]} timed out after ${timeoutMs}ms`, failure.stderr ?? "", null);
|
|
17764
|
+
throw new GitError((failure.stderr ?? "").trim() || failure.message, failure.stderr ?? "", failure.code ?? null);
|
|
17765
|
+
}
|
|
17766
|
+
}
|
|
17767
|
+
var EMPTY_STATUS = {
|
|
17768
|
+
isRepo: false,
|
|
17769
|
+
branch: "",
|
|
17770
|
+
detached: false,
|
|
17771
|
+
files: [],
|
|
17772
|
+
hasRemote: false,
|
|
17773
|
+
hasUpstream: false,
|
|
17774
|
+
ahead: 0,
|
|
17775
|
+
behind: 0
|
|
17776
|
+
};
|
|
17777
|
+
async function getGitStatus(repoRoot) {
|
|
17778
|
+
try {
|
|
17779
|
+
if (await git(["rev-parse", "--is-inside-work-tree"], repoRoot) !== "true") return EMPTY_STATUS;
|
|
17780
|
+
} catch {
|
|
17781
|
+
return EMPTY_STATUS;
|
|
17782
|
+
}
|
|
17783
|
+
let branch = "";
|
|
17784
|
+
let detached = false;
|
|
17785
|
+
try {
|
|
17786
|
+
branch = await git(["rev-parse", "--abbrev-ref", "HEAD"], repoRoot);
|
|
17787
|
+
if (branch === "HEAD") {
|
|
17788
|
+
detached = true;
|
|
17789
|
+
branch = "";
|
|
17790
|
+
}
|
|
17791
|
+
} catch {
|
|
17792
|
+
}
|
|
17793
|
+
let hasRemote = false;
|
|
17794
|
+
try {
|
|
17795
|
+
hasRemote = (await git(["remote"], repoRoot)).length > 0;
|
|
17796
|
+
} catch {
|
|
17797
|
+
}
|
|
17798
|
+
let hasUpstream = false;
|
|
17799
|
+
let ahead = 0;
|
|
17800
|
+
let behind = 0;
|
|
17801
|
+
if (!detached && branch) {
|
|
17802
|
+
try {
|
|
17803
|
+
await git(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], repoRoot);
|
|
17804
|
+
hasUpstream = true;
|
|
17805
|
+
const counts = await git(["rev-list", "--left-right", "--count", "HEAD...@{u}"], repoRoot);
|
|
17806
|
+
const [a, b] = counts.split(/\s+/).map((n) => parseInt(n, 10) || 0);
|
|
17807
|
+
ahead = a ?? 0;
|
|
17808
|
+
behind = b ?? 0;
|
|
17809
|
+
} catch {
|
|
17810
|
+
}
|
|
17811
|
+
}
|
|
17812
|
+
const raw = await git(["status", "--porcelain=v1"], repoRoot);
|
|
17813
|
+
const files = raw ? raw.split("\n").filter(Boolean).map((line) => ({ code: line.slice(0, 2), path: line.slice(3) })) : [];
|
|
17814
|
+
return { isRepo: true, branch, detached, files, hasRemote, hasUpstream, ahead, behind };
|
|
17815
|
+
}
|
|
17816
|
+
async function commitGitChanges(repoRoot, message, files) {
|
|
17817
|
+
const trimmedMessage = message.trim();
|
|
17818
|
+
if (!trimmedMessage) throw new GitError("A commit message is required.", "", null);
|
|
17819
|
+
const status = await getGitStatus(repoRoot);
|
|
17820
|
+
if (!status.isRepo) throw new GitError("Not a git repository.", "", null);
|
|
17821
|
+
if (status.files.length === 0) throw new GitError("Nothing to commit \u2014 the working tree is clean.", "", null);
|
|
17822
|
+
if (files && files.length > 0) {
|
|
17823
|
+
for (const f of files) {
|
|
17824
|
+
if (!isContainedFromRoot(f, repoRoot)) throw new GitError(`Refusing to stage a path outside the repo: ${f}`, "", null);
|
|
17825
|
+
}
|
|
17826
|
+
await git(["add", "--", ...files], repoRoot);
|
|
17827
|
+
} else {
|
|
17828
|
+
await git(["add", "-A"], repoRoot);
|
|
17829
|
+
}
|
|
17830
|
+
const staged = await git(["diff", "--cached", "--name-only"], repoRoot).catch(() => "");
|
|
17831
|
+
const stagedList = staged.split("\n").filter(Boolean);
|
|
17832
|
+
if (stagedList.length === 0) throw new GitError("Nothing staged \u2014 the selected file(s) have no changes.", "", null);
|
|
17833
|
+
await git(["commit", "-m", trimmedMessage], repoRoot);
|
|
17834
|
+
const hash = await git(["rev-parse", "HEAD"], repoRoot);
|
|
17835
|
+
return { hash, filesCommitted: stagedList.length };
|
|
17836
|
+
}
|
|
17837
|
+
async function pushGitBranch(repoRoot) {
|
|
17838
|
+
const status = await getGitStatus(repoRoot);
|
|
17839
|
+
if (!status.isRepo) return { ok: false, reason: "not_a_repo", message: "Not a git repository." };
|
|
17840
|
+
if (status.detached) {
|
|
17841
|
+
return { ok: false, reason: "detached_head", message: "Cannot push from a detached HEAD \u2014 check out a branch first." };
|
|
17842
|
+
}
|
|
17843
|
+
if (!status.hasRemote) return { ok: false, reason: "no_remote", message: "This repo has no configured remote." };
|
|
17844
|
+
const args = status.hasUpstream ? ["push"] : ["push", "--set-upstream", "origin", status.branch];
|
|
17845
|
+
try {
|
|
17846
|
+
await git(args, repoRoot, GIT_NETWORK_TIMEOUT_MS);
|
|
17847
|
+
return { ok: true, remote: "origin", branch: status.branch };
|
|
17848
|
+
} catch (e) {
|
|
17849
|
+
const ge = e;
|
|
17850
|
+
if (/\[rejected\]|non-fast-forward|fetch first|tip of your current branch is behind/i.test(ge.stderr)) {
|
|
17851
|
+
return {
|
|
17852
|
+
ok: false,
|
|
17853
|
+
reason: "diverged",
|
|
17854
|
+
message: "The remote has commits this branch doesn't have. Pull/rebase first \u2014 never force-pushed automatically."
|
|
17855
|
+
};
|
|
17856
|
+
}
|
|
17857
|
+
return { ok: false, reason: "push_failed", message: ge.message };
|
|
17858
|
+
}
|
|
17859
|
+
}
|
|
17860
|
+
function buildGithubCompareUrl(remoteUrl, branch) {
|
|
17861
|
+
const trimmed = remoteUrl.trim();
|
|
17862
|
+
const httpsMatch = trimmed.match(/^https?:\/\/(?:[^@/]+@)?github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
|
|
17863
|
+
const sshMatch = trimmed.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
17864
|
+
const m = httpsMatch ?? sshMatch;
|
|
17865
|
+
if (!m) return null;
|
|
17866
|
+
const [, owner, repo] = m;
|
|
17867
|
+
return `https://github.com/${owner}/${repo}/compare/${encodeURIComponent(branch)}?expand=1`;
|
|
17868
|
+
}
|
|
17869
|
+
async function getGithubCompareUrl(repoRoot, branch) {
|
|
17870
|
+
let remoteUrl;
|
|
17871
|
+
try {
|
|
17872
|
+
remoteUrl = await git(["remote", "get-url", "origin"], repoRoot);
|
|
17873
|
+
} catch {
|
|
17874
|
+
return null;
|
|
17875
|
+
}
|
|
17876
|
+
return buildGithubCompareUrl(remoteUrl, branch);
|
|
17877
|
+
}
|
|
17878
|
+
|
|
17879
|
+
// src/code/code-shell.ts
|
|
17880
|
+
var SHELL_MAX_OUTPUT = 1e5;
|
|
17881
|
+
var SHELL_TIMEOUT_SEC = 60;
|
|
17882
|
+
async function runShellCommand(command, repoRoot, sessionLabel, opts) {
|
|
17883
|
+
const maxOutput = opts?.maxOutput ?? SHELL_MAX_OUTPUT;
|
|
17884
|
+
const timeoutSec = opts?.timeoutSec ?? SHELL_TIMEOUT_SEC;
|
|
17885
|
+
const ops = createRobustBashOperations({ sessionLabel });
|
|
17886
|
+
let output = "";
|
|
17887
|
+
let truncated = false;
|
|
17888
|
+
const onData = (chunk) => {
|
|
17889
|
+
if (truncated) return;
|
|
17890
|
+
output += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
17891
|
+
if (output.length > maxOutput) {
|
|
17892
|
+
output = output.slice(0, maxOutput);
|
|
17893
|
+
truncated = true;
|
|
17894
|
+
}
|
|
17895
|
+
};
|
|
17896
|
+
const signal = opts?.signal ?? new AbortController().signal;
|
|
17897
|
+
try {
|
|
17898
|
+
const { exitCode } = await ops.exec(command, repoRoot, { onData, signal, timeout: timeoutSec, env: process.env });
|
|
17899
|
+
return { command, output, exitCode, timedOut: false, truncated };
|
|
17900
|
+
} catch (e) {
|
|
17901
|
+
const msg2 = e instanceof Error ? e.message : String(e);
|
|
17902
|
+
if (msg2.startsWith("timeout:")) return { command, output, exitCode: null, timedOut: true, truncated };
|
|
17903
|
+
throw e;
|
|
17904
|
+
}
|
|
17905
|
+
}
|
|
17906
|
+
function shellContextText(result) {
|
|
17907
|
+
const body6 = result.output.trim() || "(no output)";
|
|
17908
|
+
const status = result.timedOut ? ` (timed out after ${SHELL_TIMEOUT_SEC}s)` : result.exitCode !== null && result.exitCode !== 0 ? ` (exit code ${result.exitCode})` : "";
|
|
17909
|
+
const trunc = result.truncated ? "\n\u2026(output truncated)" : "";
|
|
17910
|
+
return `I ran a shell command in the repo${status}:
|
|
17911
|
+
|
|
17912
|
+
$ ${result.command}
|
|
17913
|
+
|
|
17914
|
+
${body6}${trunc}`;
|
|
17915
|
+
}
|
|
17916
|
+
|
|
17133
17917
|
// src/code/code-routes.ts
|
|
17134
17918
|
function err5(c, s, code, msg2) {
|
|
17135
17919
|
return c.json({ error: { code, message: msg2 } }, s);
|
|
@@ -17194,6 +17978,8 @@ function toSidebarRow(run) {
|
|
|
17194
17978
|
del: run.linesRemoved ?? 0,
|
|
17195
17979
|
mode: void 0,
|
|
17196
17980
|
// filled from conv below when available
|
|
17981
|
+
running: void 0,
|
|
17982
|
+
// filled from CodeRunManager below when available
|
|
17197
17983
|
createdAt: run.createdAt,
|
|
17198
17984
|
repoRoot: run.repoRoot ?? "",
|
|
17199
17985
|
error: run.error,
|
|
@@ -17239,16 +18025,17 @@ function registerCodeRoutes(app2, d) {
|
|
|
17239
18025
|
});
|
|
17240
18026
|
app2.get("/api/v1/code/sessions", (c) => {
|
|
17241
18027
|
const filter = c.req.query("filter") ?? "active";
|
|
17242
|
-
const
|
|
18028
|
+
const agentRuns = db2.listAgentRuns().filter((r) => {
|
|
17243
18029
|
const conv = db2.getConversation(r.convId);
|
|
17244
18030
|
if (conv?.kind !== "code") return false;
|
|
17245
18031
|
if (filter === "active") return !r.archivedAt;
|
|
17246
18032
|
if (filter === "archived") return !!r.archivedAt;
|
|
17247
18033
|
return true;
|
|
17248
18034
|
});
|
|
17249
|
-
const rows =
|
|
18035
|
+
const rows = agentRuns.map((r) => {
|
|
17250
18036
|
const row = toSidebarRow(r);
|
|
17251
18037
|
row.mode = db2.getConversation(r.convId)?.agentMode;
|
|
18038
|
+
row.running = runs.isActive(r.id);
|
|
17252
18039
|
return row;
|
|
17253
18040
|
});
|
|
17254
18041
|
return c.json({ sessions: rows });
|
|
@@ -17268,9 +18055,31 @@ function registerCodeRoutes(app2, d) {
|
|
|
17268
18055
|
running: runs.isActive(run.id),
|
|
17269
18056
|
// Tasks waiting behind the active turn — the server-side message queue, so its chips
|
|
17270
18057
|
// survive a page reload / reconnect.
|
|
17271
|
-
queued: runs.queued(run.id)
|
|
18058
|
+
queued: runs.queued(run.id),
|
|
18059
|
+
// The active turn's current step checklist (ADR-255), so a reopened session shows live
|
|
18060
|
+
// progress immediately without waiting for the next update_todos frame. [] when none.
|
|
18061
|
+
todos: runs.todos(run.id),
|
|
18062
|
+
// Whether an AGENTS.md is actually loaded for this session (ADR-262's loaded-resources
|
|
18063
|
+
// header) — project (<repoRoot>/AGENTS.md) and/or global (~/.turbollm/agents.md). Computed
|
|
18064
|
+
// with the SAME lookup persona.ts injects into the prompt, so "shown as loaded" == "actually
|
|
18065
|
+
// fed to the model". Both false when the session has no repoRoot.
|
|
18066
|
+
hasAgentsMd: run.repoRoot ? agentsMdPresence(run.repoRoot, d.store.dir()) : { project: false, global: false }
|
|
17272
18067
|
});
|
|
17273
18068
|
});
|
|
18069
|
+
app2.get("/api/v1/code/sessions/:id/export", (c) => {
|
|
18070
|
+
const id = c.req.param("id");
|
|
18071
|
+
const run = db2.getAgentRun(id);
|
|
18072
|
+
if (!run) return err5(c, 404, "not_found", "Session not found.");
|
|
18073
|
+
const conv = db2.getConversation(run.convId, true);
|
|
18074
|
+
if (!conv) return err5(c, 404, "not_found", "Session conversation not found.");
|
|
18075
|
+
const format = c.req.query("format") === "html" ? "html" : "markdown";
|
|
18076
|
+
if (format === "html") return err5(c, 400, "unsupported_format", "HTML export is not available yet \u2014 use format=markdown.");
|
|
18077
|
+
const md = serializeCodeSessionMarkdown(run, conv);
|
|
18078
|
+
const filename = codeSessionExportFilename(run.title, run.createdAt, "md");
|
|
18079
|
+
c.header("Content-Disposition", `attachment; filename="${filename}"`);
|
|
18080
|
+
c.header("Content-Type", "text/markdown; charset=utf-8");
|
|
18081
|
+
return c.body(md);
|
|
18082
|
+
});
|
|
17274
18083
|
app2.patch("/api/v1/code/sessions/:id/mode", async (c) => {
|
|
17275
18084
|
const id = c.req.param("id");
|
|
17276
18085
|
const run = db2.getAgentRun(id);
|
|
@@ -17346,6 +18155,7 @@ function registerCodeRoutes(app2, d) {
|
|
|
17346
18155
|
const lastMsg = (conv?.messages ?? []).at(-1);
|
|
17347
18156
|
if (!lastMsg) return err5(c, 400, "nothing_to_clear", "Nothing to clear yet.");
|
|
17348
18157
|
if (run.clearedUpToMessageId === lastMsg.id) return err5(c, 400, "nothing_to_clear", "Already cleared up to the latest message.");
|
|
18158
|
+
db2.deactivateMessagesUpTo(run.convId, lastMsg.id);
|
|
17349
18159
|
db2.setClearedUpToMessageId(id, lastMsg.id);
|
|
17350
18160
|
return c.json({ ok: true, clearedUpToMessageId: lastMsg.id });
|
|
17351
18161
|
});
|
|
@@ -17360,6 +18170,7 @@ function registerCodeRoutes(app2, d) {
|
|
|
17360
18170
|
return c.json({ ok: true });
|
|
17361
18171
|
}
|
|
17362
18172
|
if (!run.clearedUpToMessageId) return err5(c, 400, "not_cleared", "This session has not been cleared.");
|
|
18173
|
+
db2.reactivateMessagesUpTo(run.convId, run.clearedUpToMessageId);
|
|
17363
18174
|
db2.setClearedUpToMessageId(id, null);
|
|
17364
18175
|
return c.json({ ok: true });
|
|
17365
18176
|
});
|
|
@@ -17395,6 +18206,93 @@ function registerCodeRoutes(app2, d) {
|
|
|
17395
18206
|
db2.setRevertedFromMessageId(id, messageId);
|
|
17396
18207
|
return c.json({ ok: true, revertedFromMessageId: messageId, revertText: cut.revertText, revertedFiles, failedFiles });
|
|
17397
18208
|
});
|
|
18209
|
+
app2.get("/api/v1/code/sessions/:id/git/status", async (c) => {
|
|
18210
|
+
const run = db2.getAgentRun(c.req.param("id"));
|
|
18211
|
+
if (!run) return err5(c, 404, "not_found", "Session not found.");
|
|
18212
|
+
if (!run.repoRoot) return err5(c, 400, "invalid_input", "Session has no repo root.");
|
|
18213
|
+
const status = await getGitStatus(run.repoRoot);
|
|
18214
|
+
return c.json({ ok: true, status });
|
|
18215
|
+
});
|
|
18216
|
+
app2.post("/api/v1/code/sessions/:id/git/commit", async (c) => {
|
|
18217
|
+
const id = c.req.param("id");
|
|
18218
|
+
const run = db2.getAgentRun(id);
|
|
18219
|
+
if (!run) return err5(c, 404, "not_found", "Session not found.");
|
|
18220
|
+
if (!run.repoRoot) return err5(c, 400, "invalid_input", "Session has no repo root.");
|
|
18221
|
+
if (runs.isActive(id)) return err5(c, 409, "run_active", "Stop or wait for the current run before committing.");
|
|
18222
|
+
const b = await body5(c);
|
|
18223
|
+
try {
|
|
18224
|
+
const result = await commitGitChanges(run.repoRoot, b.message ?? "", b.files);
|
|
18225
|
+
return c.json({ ok: true, ...result });
|
|
18226
|
+
} catch (e) {
|
|
18227
|
+
const message = e instanceof Error ? e.message : "Commit failed.";
|
|
18228
|
+
if (/commit message is required/i.test(message)) return err5(c, 400, "invalid_input", message);
|
|
18229
|
+
if (/not a git repository/i.test(message)) return err5(c, 400, "not_a_git_repo", message);
|
|
18230
|
+
if (/nothing to commit|nothing staged/i.test(message)) return err5(c, 400, "nothing_to_commit", message);
|
|
18231
|
+
if (/outside the repo/i.test(message)) return err5(c, 400, "invalid_input", message);
|
|
18232
|
+
return err5(c, 500, "git_commit_failed", message);
|
|
18233
|
+
}
|
|
18234
|
+
});
|
|
18235
|
+
app2.post("/api/v1/code/sessions/:id/git/push", async (c) => {
|
|
18236
|
+
const id = c.req.param("id");
|
|
18237
|
+
const run = db2.getAgentRun(id);
|
|
18238
|
+
if (!run) return err5(c, 404, "not_found", "Session not found.");
|
|
18239
|
+
if (!run.repoRoot) return err5(c, 400, "invalid_input", "Session has no repo root.");
|
|
18240
|
+
if (runs.isActive(id)) return err5(c, 409, "run_active", "Stop or wait for the current run before pushing.");
|
|
18241
|
+
const result = await pushGitBranch(run.repoRoot);
|
|
18242
|
+
if (!result.ok) {
|
|
18243
|
+
const status = result.reason === "diverged" ? 409 : 400;
|
|
18244
|
+
return err5(c, status, result.reason, result.message);
|
|
18245
|
+
}
|
|
18246
|
+
const compareUrl = await getGithubCompareUrl(run.repoRoot, result.branch);
|
|
18247
|
+
return c.json({ ok: true, remote: result.remote, branch: result.branch, compareUrl });
|
|
18248
|
+
});
|
|
18249
|
+
app2.get("/api/v1/code/sessions/:id/git/compare-url", async (c) => {
|
|
18250
|
+
const run = db2.getAgentRun(c.req.param("id"));
|
|
18251
|
+
if (!run) return err5(c, 404, "not_found", "Session not found.");
|
|
18252
|
+
if (!run.repoRoot) return err5(c, 400, "invalid_input", "Session has no repo root.");
|
|
18253
|
+
const status = await getGitStatus(run.repoRoot);
|
|
18254
|
+
if (!status.isRepo || status.detached || !status.branch) return c.json({ ok: true, compareUrl: null });
|
|
18255
|
+
const compareUrl = await getGithubCompareUrl(run.repoRoot, status.branch);
|
|
18256
|
+
return c.json({ ok: true, compareUrl });
|
|
18257
|
+
});
|
|
18258
|
+
app2.post("/api/v1/code/sessions/:id/exec", async (c) => {
|
|
18259
|
+
const id = c.req.param("id");
|
|
18260
|
+
const run = db2.getAgentRun(id);
|
|
18261
|
+
if (!run) return err5(c, 404, "not_found", "Session not found.");
|
|
18262
|
+
if (!run.repoRoot) return err5(c, 400, "invalid_input", "Session has no repo root.");
|
|
18263
|
+
if (runs.isActive(id)) return err5(c, 409, "run_active", "Stop or wait for the current run before running a command.");
|
|
18264
|
+
const b = await body5(c);
|
|
18265
|
+
const command = (b.command ?? "").trim();
|
|
18266
|
+
if (!command) return err5(c, 400, "invalid_input", "A command is required.");
|
|
18267
|
+
const feedToModel = b.feedToModel !== false;
|
|
18268
|
+
let result;
|
|
18269
|
+
try {
|
|
18270
|
+
result = await runShellCommand(command, run.repoRoot, id);
|
|
18271
|
+
} catch (e) {
|
|
18272
|
+
return err5(c, 500, "exec_failed", e instanceof Error ? e.message : "Command failed to run.");
|
|
18273
|
+
}
|
|
18274
|
+
let messageId;
|
|
18275
|
+
if (feedToModel) {
|
|
18276
|
+
const msg2 = db2.addMessage(run.convId, "user", shellContextText(result), {
|
|
18277
|
+
toolCalls: [{
|
|
18278
|
+
id: `shell-${Date.now()}`,
|
|
18279
|
+
name: "shell",
|
|
18280
|
+
args: { command, exitCode: result.exitCode, timedOut: result.timedOut },
|
|
18281
|
+
result: result.output
|
|
18282
|
+
}]
|
|
18283
|
+
});
|
|
18284
|
+
messageId = msg2.id;
|
|
18285
|
+
}
|
|
18286
|
+
return c.json({
|
|
18287
|
+
ok: true,
|
|
18288
|
+
command,
|
|
18289
|
+
output: result.output,
|
|
18290
|
+
exitCode: result.exitCode,
|
|
18291
|
+
timedOut: result.timedOut,
|
|
18292
|
+
truncated: result.truncated,
|
|
18293
|
+
messageId
|
|
18294
|
+
}, 200);
|
|
18295
|
+
});
|
|
17398
18296
|
app2.post("/api/v1/code/sessions/:id/stop", (c) => {
|
|
17399
18297
|
const id = c.req.param("id");
|
|
17400
18298
|
runs.stop(id);
|
|
@@ -17409,6 +18307,7 @@ function registerCodeRoutes(app2, d) {
|
|
|
17409
18307
|
app2.post("/api/v1/code/sessions/:id/messages", async (c) => {
|
|
17410
18308
|
const id = c.req.param("id");
|
|
17411
18309
|
const b = await body5(c);
|
|
18310
|
+
const kind = b.kind === "steer" ? "steer" : "followUp";
|
|
17412
18311
|
const run = db2.getAgentRun(id);
|
|
17413
18312
|
if (!run) return err5(c, 404, "not_found", "Session not found.");
|
|
17414
18313
|
const conv = db2.getConversation(run.convId, true);
|
|
@@ -17417,20 +18316,25 @@ function registerCodeRoutes(app2, d) {
|
|
|
17417
18316
|
const ms = d.manager.status();
|
|
17418
18317
|
if (ms.state !== "running" || !ms.model) return err5(c, 409, "model_not_loaded", "Load a model first.");
|
|
17419
18318
|
if (!d.manager.target()) return err5(c, 409, "model_not_loaded", "Engine not running.");
|
|
17420
|
-
const
|
|
18319
|
+
const newContent = (b.content ?? "").trim();
|
|
17421
18320
|
let userMsgId;
|
|
17422
18321
|
let task;
|
|
17423
|
-
if (
|
|
17424
|
-
userMsgId = db2.addMessage(run.convId, "user",
|
|
17425
|
-
task = contextFilesBlock(b.contextFiles) + ((b.promptOverride ?? "").trim() ||
|
|
18322
|
+
if (newContent) {
|
|
18323
|
+
userMsgId = db2.addMessage(run.convId, "user", newContent, { textAttachments: b.contextFiles }).id;
|
|
18324
|
+
task = contextFilesBlock(b.contextFiles) + ((b.promptOverride ?? "").trim() || newContent);
|
|
17426
18325
|
} else {
|
|
17427
18326
|
const lastUser = (conv.messages ?? []).filter((m) => m.role === "user").at(-1);
|
|
17428
18327
|
if (!lastUser) return err5(c, 400, "no_task", "No task to run.");
|
|
17429
18328
|
userMsgId = lastUser.id;
|
|
17430
18329
|
task = contextFilesBlock(lastUser.textAttachments) + lastUser.content;
|
|
17431
18330
|
}
|
|
17432
|
-
const
|
|
17433
|
-
|
|
18331
|
+
const enqueueParams = { convId: run.convId, repoRoot: run.repoRoot, task, userMsgId, thinkingBudget: b.thinkingBudget };
|
|
18332
|
+
if (kind === "steer") {
|
|
18333
|
+
const { steered, queued: queued2 } = await runs.steer(id, enqueueParams);
|
|
18334
|
+
return c.json({ ok: true, queued: queued2, steered, userMessageId: userMsgId }, 202);
|
|
18335
|
+
}
|
|
18336
|
+
const { queued } = runs.enqueue(id, enqueueParams);
|
|
18337
|
+
return c.json({ ok: true, queued, steered: false, userMessageId: userMsgId }, 202);
|
|
17434
18338
|
});
|
|
17435
18339
|
app2.get("/api/v1/code/sessions/:id/stream", (c) => {
|
|
17436
18340
|
const id = c.req.param("id");
|
|
@@ -17446,6 +18350,8 @@ function registerCodeRoutes(app2, d) {
|
|
|
17446
18350
|
const meta = runs.activeMeta(id);
|
|
17447
18351
|
if (meta) await stream.writeSSE({ event: "meta", data: JSON.stringify(meta) });
|
|
17448
18352
|
await stream.writeSSE({ event: "queue", data: JSON.stringify({ queued: runs.queued(id) }) });
|
|
18353
|
+
const currentTodos = runs.todos(id);
|
|
18354
|
+
if (currentTodos.length > 0) await stream.writeSSE({ event: "todos", data: JSON.stringify({ todos: currentTodos }) });
|
|
17449
18355
|
for await (const ev of sub) {
|
|
17450
18356
|
await stream.writeSSE({ id: String(ev.seq), event: ev.event, data: JSON.stringify(ev.data) });
|
|
17451
18357
|
}
|
|
@@ -18131,7 +19037,7 @@ function contentType(file) {
|
|
|
18131
19037
|
}
|
|
18132
19038
|
|
|
18133
19039
|
// src/tunnel/manager.ts
|
|
18134
|
-
import { execFile as
|
|
19040
|
+
import { execFile as execFile13, spawn as spawn7, spawnSync as spawnSync2 } from "child_process";
|
|
18135
19041
|
import { mkdirSync as mkdirSync15, readFileSync as readFileSync14, readdirSync as readdirSync9, rmSync as rmSync14, writeFileSync as writeFileSync13 } from "fs";
|
|
18136
19042
|
import { join as join31 } from "path";
|
|
18137
19043
|
|
|
@@ -18211,13 +19117,13 @@ function sleep4(ms) {
|
|
|
18211
19117
|
}
|
|
18212
19118
|
function signalTerm2(child) {
|
|
18213
19119
|
if (!child.pid) return;
|
|
18214
|
-
if (process.platform === "win32")
|
|
19120
|
+
if (process.platform === "win32") execFile13("taskkill", ["/PID", String(child.pid), "/T"], () => {
|
|
18215
19121
|
});
|
|
18216
19122
|
else child.kill("SIGTERM");
|
|
18217
19123
|
}
|
|
18218
19124
|
function forceKill2(child) {
|
|
18219
19125
|
if (!child.pid) return;
|
|
18220
|
-
if (process.platform === "win32")
|
|
19126
|
+
if (process.platform === "win32") execFile13("taskkill", ["/PID", String(child.pid), "/F", "/T"], () => {
|
|
18221
19127
|
});
|
|
18222
19128
|
else child.kill("SIGKILL");
|
|
18223
19129
|
}
|