openmausbot 0.1.87 → 0.1.88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-BTiuL79F.js → abnfDiagram-O67JEVCF-qd9VD3GV.js} +1 -1
- package/dist/assets/{arc-C88Blc2v.js → arc-3RwkUjRe.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-BZsJIZrb.js → architectureDiagram-NJMV4G6O-CFKSJS7a.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CVeMe9Ah.js → blockDiagram-BEXU5L5S-CG1rEfWD.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-Cedk3Bhx.js → c4Diagram-YGBWAQC7-CBW9N664.js} +1 -1
- package/dist/assets/channel-CnRnO3tF.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-D-VRqzTB.js → chunk-2Q5K7J3B-2mRfTksB.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BChov01Y.js → chunk-3FUC2YCW-Cv1dpErj.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-C-TfwvHy.js → chunk-5VM5RSS4-B7IACJp-.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Db7CGj5Y.js → chunk-GTNCS2PH-An0hZtYa.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-CNvIfqKT.js → chunk-GWA4HPMP-BcJB2nEm.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-C_bEp5tq.js → chunk-JWPE2WC7-CV_W7TN7.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-Coi4BOls.js → chunk-NETBCI7D-Bcs_kwnA.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CzylEuL3.js → chunk-WEXAMYUT-lrx_DxFP.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-m7p7ztaD.js → chunk-XXDRQBXY-DabHlLGM.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BMy6o9x2.js → classDiagram-v2-NBCMYWYE-BnHj6P7d.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-BDQ__0_J.js → cose-bilkent-JH36ORCC-CfLp6UDi.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-Djv4ghUb.js → cynefin-OW5HDTMX-Dm4dbVA1.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-BULrlGYH.js → cynefinDiagram-VND7K2PF-B9mH-PID.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-D-jrT_hU.js → dagre-6A5THRUB-DDc_NVQJ.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-p2UhQmfL.js → diagram-22UHCM2B-lwr8Ip8-.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-DHvGWZ7q.js → diagram-3UASUU5V-BLLe6dmI.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-ChhKDot-.js → diagram-ATOU4E4O-6eUIq4lz.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-B-T7A43R.js → diagram-CDSNMT55-O-pRN7VQ.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-CJm8ENNY.js → diagram-MLGK6HIB-BietvGAI.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-510tlza8.js → diagram-MPIPVDR6-Bd2Cl62X.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-PkvRPHel.js → ebnfDiagram-ZINNZB2B-B6iDy69W.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-BVBVPoX3.js → elk-276RUBZZ-DQVHfKe0.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BgYMVm7L.js → erDiagram-OPXOYQCR-BWdVp2tQ.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-BGqsnc_3.js → flowDiagram-KWPJA3E3-WLE6rKB4.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-D_-UeReH.js → ganttDiagram-FUAMR5RP-BbjS-4gn.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-D0uooONx.js → gitGraphDiagram-X574FWY7-Dpy2QSGq.js} +1 -1
- package/dist/assets/index-BIMdIbaw.css +1 -0
- package/dist/assets/index-CHnODCr2.js +583 -0
- package/dist/assets/{index-zRsJERGJ.js → index-CIaHn5NS.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-B9e1z77f.js → infoDiagram-VRGFBTTK-D_jKWlKe.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BB4vNwvD.js → ishikawaDiagram-OU5B5YK6-DLJohSuK.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DpkosOTB.js → journeyDiagram-ZHPQQLJL-DPOFj0qN.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DbDP4kUu.js → kanban-definition-PNTS6WVX-BI7v-xpK.js} +1 -1
- package/dist/assets/{linear-DBJGPmjT.js → linear-C2pWPSxO.js} +1 -1
- package/dist/assets/{mermaid.core-DWPRi-ue.js → mermaid.core-CR-j5WQU.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-nszBbJmx.js → mindmap-definition-NLK3R4M7-lz9GvXRh.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CWykJWu_.js → pegDiagram-GJSIUBJH-Dl4_ia61.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-g3Da-Vnv.js → pieDiagram-5QR66LMP-DWuT3n88.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-Wzrlpzgh.js → quadrantDiagram-O4NWA36T-DXWe9Jx7.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-moHTFMC0.js → railroadDiagram-XR7U4H2S-COY1IBVY.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CwfXdFH8.js → requirementDiagram-PLB6GJNP-DnyVuhsQ.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-3ZROYvnq.js → sankeyDiagram-IPEJSGJF-BSc4NdGK.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-E1TBebP8.js → sequenceDiagram-PO4LG4MO-kqEU2EGq.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-DHKEU5pu.js → sizeCapture-INFHLROL-BvB4bkWD.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-BguEj09D.js → stateDiagram-v2-GCMORJYK-BIrqNv7y.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-C7IDYnhd.js → swimlanes-2SLR337P-CFnalQUE.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-DCW-F5PW.js → timeline-definition-EJHVYXUP-CsSWJnSQ.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-xBMFtWYV.js → usecaseDiagram-POWQR4AR-DkAmJ1EE.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-B5nIkuH8.js → vennDiagram-UO4OBE2U-C3vd9drL.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DYFAhdbr.js → wardleyDiagram-VNRHLVJA-j26CM-pi.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-DeXwX6Cz.js → xychartDiagram-PMCCYNJV-BYu1Q_at.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/companion/src/routes.js +8 -1
- package/dist-server/connector-proxy.js +105 -1
- package/dist-server/container-mcp.js +3 -1
- package/dist-server/drivers/agents-proxy.js +277 -22
- package/dist-server/index.js +23150 -19947
- package/dist-server/local-computer-proxy.js +2 -0
- package/dist-server/mcp-gate.js +2 -0
- package/dist-server/mcp-server.js +5 -3
- package/dist-server/message-search.worker.js +52 -0
- package/dist-server/openmausbot.js +540 -298
- package/dist-server/pair-cli.js +540 -298
- package/dist-server/server/admission.js +81 -0
- package/dist-server/server/aside-queue.js +294 -0
- package/dist-server/server/attachments.js +43 -1
- package/dist-server/server/bot-attachment.js +107 -0
- package/dist-server/server/bot-overview.js +19 -0
- package/dist-server/server/cli-api-setup.js +1 -0
- package/dist-server/server/composio.js +297 -1
- package/dist-server/server/config.js +25 -3
- package/dist-server/server/connector-advertisement.js +165 -0
- package/dist-server/server/connector-proxy.js +14 -2
- package/dist-server/server/connector-verdict.js +31 -4
- package/dist-server/server/container-computer.js +60 -0
- package/dist-server/server/contracts.js +22 -0
- package/dist-server/server/drivers/acp/core.js +24 -6
- package/dist-server/server/drivers/acp/cursor.js +3 -7
- package/dist-server/server/drivers/acp/droid.js +4 -7
- package/dist-server/server/drivers/acp/gemini.js +4 -4
- package/dist-server/server/drivers/acp/grok.js +4 -9
- package/dist-server/server/drivers/acp/hermes.js +3 -5
- package/dist-server/server/drivers/acp/opencode-go.js +2 -5
- package/dist-server/server/drivers/acp/qwen.js +9 -15
- package/dist-server/server/drivers/agents-call.js +137 -10
- package/dist-server/server/drivers/agents-catalog.js +100 -6
- package/dist-server/server/drivers/boxagent.js +2 -16
- package/dist-server/server/drivers/claude.js +31 -17
- package/dist-server/server/drivers/codex-catalog.js +14 -6
- package/dist-server/server/drivers/codex-identity.js +2 -1
- package/dist-server/server/drivers/codex-release.js +48 -0
- package/dist-server/server/drivers/codex.js +7 -25
- package/dist-server/server/drivers/openai-chat.js +26 -2
- package/dist-server/server/drivers/pi.js +237 -30
- package/dist-server/server/drivers/prompt-split.js +32 -9
- package/dist-server/server/drivers/session-idle.js +29 -0
- package/dist-server/server/env-path.js +15 -0
- package/dist-server/server/guarded-requests.js +2 -2
- package/dist-server/server/harness-capabilities.js +46 -0
- package/dist-server/server/index.js +1037 -209
- package/dist-server/server/message-db.js +22 -42
- package/dist-server/server/message-file.js +9 -1
- package/dist-server/server/message-search-query.js +43 -0
- package/dist-server/server/message-search-worker.js +80 -0
- package/dist-server/server/message-search.worker.js +21 -0
- package/dist-server/server/model-requests.js +248 -0
- package/dist-server/server/options-card.js +21 -0
- package/dist-server/server/org-library.js +15 -1
- package/dist-server/server/peer-delivery.js +60 -0
- package/dist-server/server/peer-roster.js +57 -0
- package/dist-server/server/presets.js +32 -32
- package/dist-server/server/profile-requests.js +66 -39
- package/dist-server/server/profile-revision.js +9 -1
- package/dist-server/server/profile-versions.js +6 -3
- package/dist-server/server/routine-requests.js +29 -7
- package/dist-server/server/skill-fetch.js +72 -14
- package/dist-server/server/skills.js +1 -0
- package/dist-server/server/store.js +32 -2
- package/dist-server/server/system-prompt.js +26 -2
- package/dist-server/server/team-setup-requests.js +124 -23
- package/dist-server/server/thread-events.js +3 -1
- package/dist-server/server/tightening-requests.js +405 -0
- package/dist-server/server/tts/chatterbox.js +27 -5
- package/dist-server/server/usage-ledger.js +7 -1
- package/dist-server/server/workspace-backup.js +40 -3
- package/dist-server/server/workspace-backup.worker.js +10 -0
- package/dist-server/shared/ask-question.js +19 -0
- package/dist-server/shared/credential-request.js +5 -1
- package/dist-server/shared/line-diff.js +23 -0
- package/dist-server/shared/model-request.js +1 -0
- package/dist-server/shared/options-card.js +51 -0
- package/dist-server/shared/profile-request.js +5 -4
- package/dist-server/shared/tightening-request.js +87 -0
- package/dist-server/src/lib/composer-attachments.js +25 -3
- package/dist-server/vps-container-mcp.js +3 -1
- package/dist-server/workspace-backup.worker.js +19474 -0
- package/package.json +1 -1
- package/dist/assets/channel-taCNRlUU.js +0 -1
- package/dist/assets/index-BwdMAQ6p.css +0 -1
- package/dist/assets/index-ByJennr7.js +0 -581
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.js +0 -8
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// old file's email. Only the protocol's display email leaves this helper.
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { userHome } from "../env-path.js";
|
|
6
7
|
import { killCliTree, spawnCli } from "../procs.js";
|
|
7
8
|
const MAX_OUTPUT = 16_384;
|
|
8
9
|
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -11,7 +12,7 @@ const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
11
12
|
export function codexHome(env) {
|
|
12
13
|
if (env.CODEX_HOME)
|
|
13
14
|
return isAbsolute(env.CODEX_HOME) ? resolve(env.CODEX_HOME) : null;
|
|
14
|
-
const home = env
|
|
15
|
+
const home = userHome(env);
|
|
15
16
|
return isAbsolute(home) ? join(home, ".codex") : null;
|
|
16
17
|
}
|
|
17
18
|
/** Read-only account metadata. Unsupported CLIs and uncertain responses stay
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Release freshness is independent of account-specific model availability.
|
|
2
|
+
function parseCodexReleaseVersion(value) {
|
|
3
|
+
const match = /^(?:.*\bcodex-cli\s+)?v?(\d+)\.(\d+)\.(\d+)(-[0-9a-z.-]+)?(?:\+[0-9a-z.-]+)?$/i.exec(value.trim());
|
|
4
|
+
return match ? { parts: match.slice(1, 4).map(Number), prerelease: Boolean(match[4]) } : null;
|
|
5
|
+
}
|
|
6
|
+
export function codexVersionBehind(installed, latest) {
|
|
7
|
+
const a = parseCodexReleaseVersion(installed);
|
|
8
|
+
const b = parseCodexReleaseVersion(latest);
|
|
9
|
+
if (!a || !b || b.prerelease)
|
|
10
|
+
return false;
|
|
11
|
+
for (let i = 0; i < 3; i++) {
|
|
12
|
+
if (a.parts[i] !== b.parts[i])
|
|
13
|
+
return a.parts[i] < b.parts[i];
|
|
14
|
+
}
|
|
15
|
+
return a.prerelease;
|
|
16
|
+
}
|
|
17
|
+
export function createCodexReleaseReader(fetchImpl = fetch, now = Date.now) {
|
|
18
|
+
let cached;
|
|
19
|
+
let expires = 0;
|
|
20
|
+
return () => {
|
|
21
|
+
if (cached && now() < expires)
|
|
22
|
+
return cached;
|
|
23
|
+
expires = now() + 60 * 60 * 1000;
|
|
24
|
+
cached = (async () => {
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetchImpl("https://registry.npmjs.org/@openai/codex/latest", {
|
|
27
|
+
signal: AbortSignal.timeout(3000),
|
|
28
|
+
headers: { Accept: "application/json" },
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok)
|
|
31
|
+
throw new Error("Release lookup failed");
|
|
32
|
+
const payload = await response.json();
|
|
33
|
+
const version = typeof payload.version === "string" ? payload.version : "";
|
|
34
|
+
const parsed = parseCodexReleaseVersion(version);
|
|
35
|
+
if (!parsed || parsed.prerelease)
|
|
36
|
+
throw new Error("Invalid stable release");
|
|
37
|
+
return version;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Offline checks must not make an otherwise usable engine unavailable.
|
|
41
|
+
expires = now() + 5 * 60 * 1000;
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
})();
|
|
45
|
+
return cached;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export const readLatestCodexRelease = createCodexReleaseReader();
|
|
@@ -31,10 +31,9 @@ import { codexAccountEmail } from "./codex-identity.js";
|
|
|
31
31
|
import { classifyResumeFailure, mayReplay, recoveryPromptFor } from "../resume-recovery.js";
|
|
32
32
|
import { extractMcpImages } from "../mcp-tool-images.js";
|
|
33
33
|
import { parseProtocolAskQuestions, questionAnswersById, questionChoices } from "../../shared/ask-question.js";
|
|
34
|
+
import { codexVersionBehind, readLatestCodexRelease } from "./codex-release.js";
|
|
34
35
|
export { decodeCodexSelection, readCodexModelCatalog, STATIC_CODEX_MODELS } from "./codex-catalog.js";
|
|
35
36
|
const DRIVER_KIND = "codex";
|
|
36
|
-
const ASTRA_MODEL_ID = "gpt-6-astra";
|
|
37
|
-
const ASTRA_MIN_CODEX_VERSION = [0, 153, 1];
|
|
38
37
|
class CodexRpcError extends Error {
|
|
39
38
|
code;
|
|
40
39
|
constructor(error) {
|
|
@@ -49,23 +48,6 @@ function missingNativeCodexThread(error, cursor) {
|
|
|
49
48
|
return error instanceof CodexRpcError && error.code === -32600 &&
|
|
50
49
|
error.message === `no rollout found for thread id ${cursor}`;
|
|
51
50
|
}
|
|
52
|
-
/** Whether an installed Codex predates the release that exposes GPT-6 Astra
|
|
53
|
-
* through app-server. Unknown version formats stay quiet: a bad guess should
|
|
54
|
-
* never nag someone whose custom build may already support the model. */
|
|
55
|
-
export function codexPredatesAstra(version) {
|
|
56
|
-
const value = version.trim();
|
|
57
|
-
const match = /\bcodex-cli\s+v?(\d+)\.(\d+)\.(\d+)(?:[-+][0-9a-z.-]+)?(?![\d.])\b/i.exec(value)
|
|
58
|
-
?? /^v?(\d+)\.(\d+)\.(\d+)(?:[-+][0-9a-z.-]+)?$/i.exec(value);
|
|
59
|
-
if (!match)
|
|
60
|
-
return false;
|
|
61
|
-
const installed = match.slice(1, 4).map(Number);
|
|
62
|
-
for (let i = 0; i < ASTRA_MIN_CODEX_VERSION.length; i += 1) {
|
|
63
|
-
if (installed[i] !== ASTRA_MIN_CODEX_VERSION[i]) {
|
|
64
|
-
return installed[i] < ASTRA_MIN_CODEX_VERSION[i];
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
51
|
/** Ask the configured executable to update itself. This matters when the user
|
|
70
52
|
* selected a non-PATH Codex: installing a second global copy would leave
|
|
71
53
|
* OpenMausBot pointing at the old binary. */
|
|
@@ -85,13 +67,13 @@ export function codexUpdateCommand(cli, platform = process.platform) {
|
|
|
85
67
|
const command = (tokens.length > 0 ? tokens : [trimmed]).map(quote).join(" ");
|
|
86
68
|
return platform === "win32" ? `& ${command} update` : `${command} update`;
|
|
87
69
|
}
|
|
88
|
-
function
|
|
89
|
-
|
|
70
|
+
async function codexReleaseUpdate(version, cli) {
|
|
71
|
+
const latest = await readLatestCodexRelease();
|
|
72
|
+
if (!latest || !codexVersionBehind(version, latest))
|
|
90
73
|
return undefined;
|
|
91
|
-
}
|
|
92
74
|
return {
|
|
93
|
-
title:
|
|
94
|
-
message:
|
|
75
|
+
title: `Update Codex to ${latest}`,
|
|
76
|
+
message: `A newer stable Codex CLI is available (installed: ${version}). Update it, then refresh models. Model availability also depends on your signed-in account.`,
|
|
95
77
|
command: codexUpdateCommand(cli),
|
|
96
78
|
};
|
|
97
79
|
}
|
|
@@ -1579,7 +1561,7 @@ export const CodexDriver = {
|
|
|
1579
1561
|
version,
|
|
1580
1562
|
authenticated,
|
|
1581
1563
|
...(email ? { account: { email } } : {}),
|
|
1582
|
-
update:
|
|
1564
|
+
update: await codexReleaseUpdate(version, config.cli),
|
|
1583
1565
|
billing: "subscription",
|
|
1584
1566
|
};
|
|
1585
1567
|
};
|
|
@@ -332,7 +332,8 @@ export function createOpenAIChatRuntime(options) {
|
|
|
332
332
|
});
|
|
333
333
|
let resolveDone;
|
|
334
334
|
const done = new Promise((resolve) => { resolveDone = resolve; });
|
|
335
|
-
|
|
335
|
+
const turnEntry = { abort, turnId, done, approval, asides: [] };
|
|
336
|
+
active.set(turn.threadId, turnEntry);
|
|
336
337
|
emit({ ...base(turn.threadId, turnId), type: "turn.started" });
|
|
337
338
|
emit({ ...base(turn.threadId, turnId), type: "session.started", sessionId: null, model });
|
|
338
339
|
void (async () => {
|
|
@@ -358,6 +359,13 @@ export function createOpenAIChatRuntime(options) {
|
|
|
358
359
|
}
|
|
359
360
|
for (let round = 0; round < 16; round++) {
|
|
360
361
|
abort.signal.throwIfAborted();
|
|
362
|
+
// Drain parked mid-turn input here, before the next completion
|
|
363
|
+
// request: the previous round's tool results are complete, so a
|
|
364
|
+
// user message lands on a consistent array (never inside a tool
|
|
365
|
+
// batch) and the provider sees it as the newest input.
|
|
366
|
+
const parked = turnEntry.asides.splice(0);
|
|
367
|
+
if (parked.length)
|
|
368
|
+
messages.push({ role: "user", content: parked.join("\n\n") });
|
|
361
369
|
native("out", options.nativeLog.outgoing(turn, messages, model));
|
|
362
370
|
let attempt = 0;
|
|
363
371
|
let completion;
|
|
@@ -611,7 +619,10 @@ export function createOpenAIChatRuntime(options) {
|
|
|
611
619
|
provider: options.driverKind,
|
|
612
620
|
capabilities: { ...(options.computerUse ? { computerMcp: options.tools !== false, cloudComputerMcp: options.tools !== false, localComputerMcp: options.tools !== false,
|
|
613
621
|
browserMcp: options.tools !== false, nativeImageInput: true, images: true } : {}),
|
|
614
|
-
sessionModelSwitch: "in-session", customMcp: options.tools !== false, agentsMcp: options.tools !== false, composioMcp: options.tools !== false
|
|
622
|
+
sessionModelSwitch: "in-session", customMcp: options.tools !== false, agentsMcp: options.tools !== false, composioMcp: options.tools !== false,
|
|
623
|
+
// The runtime owns the whole tool loop, so it can always take a
|
|
624
|
+
// user message mid-turn: park it, deliver before the next completion.
|
|
625
|
+
queueing: true },
|
|
615
626
|
sendTurn,
|
|
616
627
|
interruptTurn: async (threadId, turnId) => {
|
|
617
628
|
const turn = active.get(threadId);
|
|
@@ -621,6 +632,19 @@ export function createOpenAIChatRuntime(options) {
|
|
|
621
632
|
await turn.done;
|
|
622
633
|
},
|
|
623
634
|
respondToRequest: async (threadId, requestId, decision) => active.get(threadId)?.approval.answer(requestId, decision.behavior, decision.message) ?? "unavailable",
|
|
635
|
+
steer: async (threadId, text) => {
|
|
636
|
+
// This engine has no external session to respect — parking the
|
|
637
|
+
// words in the live turn's entry IS delivery into the loop, so a
|
|
638
|
+
// successful park is "steered" and a missing turn is the only
|
|
639
|
+
// refusal. The words ride the array at the next round boundary;
|
|
640
|
+
// if the turn settles first, the harness's transcript record
|
|
641
|
+
// keeps them for the next turn (they are never re-sent here).
|
|
642
|
+
const running = active.get(threadId);
|
|
643
|
+
if (!running)
|
|
644
|
+
return "refused";
|
|
645
|
+
running.asides.push(text);
|
|
646
|
+
return "steered";
|
|
647
|
+
},
|
|
624
648
|
hasSession: (threadId) => active.has(threadId),
|
|
625
649
|
stopAll: async () => {
|
|
626
650
|
const turns = [...active.values()];
|
|
@@ -26,6 +26,7 @@ import { join } from "node:path";
|
|
|
26
26
|
import { PROVIDER_CREDENTIAL_ENV, stripWorkspaceCredentialEnv } from "../config.js";
|
|
27
27
|
import { augmentedPath } from "../env-path.js";
|
|
28
28
|
import { describeSpawnFailure, execCli, killCliTree, spawnCli } from "../procs.js";
|
|
29
|
+
import { deletePromptSplitReceipt, promptHalves, readPromptSplitReceipt, splitSessionPrompt, writePromptSplitReceipt, } from "./prompt-split.js";
|
|
29
30
|
import { SPAWNED_PROXIES } from "../proxy-paths.js";
|
|
30
31
|
import { commandSummary, toolDetailPreview } from "../tool-summary.js";
|
|
31
32
|
import { EFFORT_LEVELS } from "../../shared/wire.js";
|
|
@@ -37,6 +38,21 @@ const DRIVER_KIND = "piAgent";
|
|
|
37
38
|
const PI_ARGS = ["--mode", "rpc", "--no-session"];
|
|
38
39
|
const PI_MODEL_UPDATE_ARGS = ["update", "--models", "--no-approve"];
|
|
39
40
|
const NODE_ENV_FLAG = { ELECTRON_RUN_AS_NODE: "1" };
|
|
41
|
+
/** After this many bare turns on one pi session the full prompt rides
|
|
42
|
+
* again even without a compaction event: any history rewrite the events
|
|
43
|
+
* miss (an older pi, a missed line) still loses at most this many turns
|
|
44
|
+
* of standing instructions instead of the rest of the session. */
|
|
45
|
+
const PI_PROMPT_RE_ANCHOR_TURNS = 8;
|
|
46
|
+
/** How long the driver keeps listening after a finished turn_end for the
|
|
47
|
+
* run's terminal agent_end before settling anyway: a runtime that never
|
|
48
|
+
* emits agent_end still completes, while one that does cancels the wait. */
|
|
49
|
+
const PI_AGENT_END_GRACE_MS = 60_000;
|
|
50
|
+
/** pi answered a command with success:false — an explicit refusal from a
|
|
51
|
+
* live runtime, not a transport failure. Only steer branches on the
|
|
52
|
+
* difference today ("refused": provably not delivered, safe to re-queue);
|
|
53
|
+
* the handshake commands keep their catch-all ignore. */
|
|
54
|
+
class PiRpcRefusalError extends Error {
|
|
55
|
+
}
|
|
40
56
|
function readPiPromptImages(turn) {
|
|
41
57
|
return (turn.images ?? []).map((image) => ({
|
|
42
58
|
type: "image",
|
|
@@ -431,6 +447,13 @@ export const PiDriver = {
|
|
|
431
447
|
// closure (send, child) alive until it fires.
|
|
432
448
|
const askTimers = new Set();
|
|
433
449
|
let settled = false;
|
|
450
|
+
// the last turn_end's outcome, replayed when the run's terminal
|
|
451
|
+
// agent_end (or the grace fallback) settles the turn
|
|
452
|
+
let endStopReason;
|
|
453
|
+
let endUsage;
|
|
454
|
+
let endErrorMessage;
|
|
455
|
+
// hang insurance for runtimes with no agent_end frame at all
|
|
456
|
+
let agentEndTimer = null;
|
|
434
457
|
// pi's RPC surface accepts image content directly. Read before spawning
|
|
435
458
|
// so an attachment that disappeared produces one clear dispatch error
|
|
436
459
|
// instead of starting a child that can never receive its prompt.
|
|
@@ -493,8 +516,17 @@ export const PiDriver = {
|
|
|
493
516
|
})();
|
|
494
517
|
let buf = "";
|
|
495
518
|
let assistantText = "";
|
|
519
|
+
// set when a compaction event arrives this turn; gates the receipt
|
|
520
|
+
// write below so a compacted-around delivery is never claimed
|
|
521
|
+
let compactionObserved = false;
|
|
496
522
|
// resolve one-shot RPC responses (new_session / switch_session / set_model)
|
|
497
523
|
const responseWaiters = new Map();
|
|
524
|
+
// Steer frames correlate by a unique per-request id, not the shared
|
|
525
|
+
// "steer" command name: two concurrent steers would otherwise overwrite
|
|
526
|
+
// each other's waiter, and a refusal response could reject the wrong
|
|
527
|
+
// caller — requeueing words pi already accepted. Every other command
|
|
528
|
+
// keeps its command-name key.
|
|
529
|
+
let steerSeq = 0;
|
|
498
530
|
const rejectWaiters = (err) => {
|
|
499
531
|
for (const waiter of responseWaiters.values()) {
|
|
500
532
|
clearTimeout(waiter.timer);
|
|
@@ -502,13 +534,13 @@ export const PiDriver = {
|
|
|
502
534
|
}
|
|
503
535
|
responseWaiters.clear();
|
|
504
536
|
};
|
|
505
|
-
const awaitResponse = (command, timeoutMs = 20_000) => new Promise((resolve, reject) => {
|
|
537
|
+
const awaitResponse = (command, timeoutMs = 20_000, key = command) => new Promise((resolve, reject) => {
|
|
506
538
|
const timer = setTimeout(() => {
|
|
507
|
-
responseWaiters.delete(
|
|
539
|
+
responseWaiters.delete(key);
|
|
508
540
|
reject(new Error(`pi ${command} timed out`));
|
|
509
541
|
}, timeoutMs);
|
|
510
542
|
timer.unref?.();
|
|
511
|
-
responseWaiters.set(
|
|
543
|
+
responseWaiters.set(key, { resolve, reject, timer });
|
|
512
544
|
});
|
|
513
545
|
child.stdin.on("error", () => rejectWaiters(new Error("pi stdin closed")));
|
|
514
546
|
const send = (obj) => {
|
|
@@ -527,6 +559,10 @@ export const PiDriver = {
|
|
|
527
559
|
if (settled)
|
|
528
560
|
return;
|
|
529
561
|
settled = true;
|
|
562
|
+
if (agentEndTimer) {
|
|
563
|
+
clearTimeout(agentEndTimer);
|
|
564
|
+
agentEndTimer = null;
|
|
565
|
+
}
|
|
530
566
|
// The turn is over: drop unanswered asks so their 15-minute
|
|
531
567
|
// fail-safe timers are cancelled outright instead of no-oping on a
|
|
532
568
|
// dead child while holding the ask closure alive.
|
|
@@ -564,6 +600,23 @@ export const PiDriver = {
|
|
|
564
600
|
}
|
|
565
601
|
active.delete(threadId);
|
|
566
602
|
};
|
|
603
|
+
/** Settle from the last turn_end's recorded outcome. Errors surface
|
|
604
|
+
* here, at run completion, so a failed run reports exactly once and
|
|
605
|
+
* only after any scheduled maintenance events had their chance to
|
|
606
|
+
* arrive. */
|
|
607
|
+
const finishRun = () => {
|
|
608
|
+
const sr = endStopReason;
|
|
609
|
+
if (sr === "error" || sr === "failed") {
|
|
610
|
+
emit({
|
|
611
|
+
...base(threadId, turnId),
|
|
612
|
+
type: "runtime.error",
|
|
613
|
+
message: String(endErrorMessage ?? "pi turn failed").slice(0, 2_000),
|
|
614
|
+
});
|
|
615
|
+
settle(false, "failed", endUsage);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
settle(true, sr === "cancelled" || sr === "aborted" ? "cancelled" : "end_turn", endUsage);
|
|
619
|
+
};
|
|
567
620
|
const stop = () => {
|
|
568
621
|
try {
|
|
569
622
|
send({ type: "abort" });
|
|
@@ -579,19 +632,53 @@ export const PiDriver = {
|
|
|
579
632
|
}
|
|
580
633
|
settle(true, "cancelled");
|
|
581
634
|
};
|
|
582
|
-
|
|
635
|
+
// Mid-turn input rides pi's native steer frame — the runtime queues
|
|
636
|
+
// it into the running agent without aborting the in-flight step, and
|
|
637
|
+
// answers success only after session.steer() accepted it. A success:
|
|
638
|
+
// false response is an explicit refusal (nothing consumed the words:
|
|
639
|
+
// safe to re-queue), while a death or timeout after the frame was
|
|
640
|
+
// written leaves the outcome unknowable — those are "indeterminate"
|
|
641
|
+
// so the caller can never run the words twice. Each frame carries a
|
|
642
|
+
// unique id that pi echoes on its response, so concurrent steers on
|
|
643
|
+
// the same turn never share or overwrite a waiter.
|
|
644
|
+
const steerActiveTurn = async (text) => {
|
|
645
|
+
if (settled || child.exitCode !== null || child.signalCode !== null)
|
|
646
|
+
return "refused";
|
|
647
|
+
let ack;
|
|
648
|
+
try {
|
|
649
|
+
const id = `steer:${++steerSeq}`;
|
|
650
|
+
ack = awaitResponse("steer", 20_000, id);
|
|
651
|
+
send({ type: "steer", id, message: text });
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
// stdin closed before the frame was written: provably undelivered.
|
|
655
|
+
return "refused";
|
|
656
|
+
}
|
|
657
|
+
try {
|
|
658
|
+
await ack;
|
|
659
|
+
return "steered";
|
|
660
|
+
}
|
|
661
|
+
catch (error) {
|
|
662
|
+
return error instanceof PiRpcRefusalError ? "refused" : "indeterminate";
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
active.set(threadId, { stop, turnId, pending, child, steer: steerActiveTurn });
|
|
583
666
|
const onEvent = (evt) => {
|
|
584
667
|
appendNative(threadId, { dir: "in", source: "pi.rpc", msg: evt });
|
|
585
668
|
switch (evt.type) {
|
|
586
669
|
case "response": {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
670
|
+
// Resolve the echoed request id first (unique per steer);
|
|
671
|
+
// fall back to the command name for the handshake-style call
|
|
672
|
+
// sites, whose responses carry no id.
|
|
673
|
+
const key = typeof evt.id === "string" && responseWaiters.has(evt.id) ? evt.id : evt.command;
|
|
674
|
+
if (key && responseWaiters.has(key)) {
|
|
675
|
+
const waiter = responseWaiters.get(key);
|
|
676
|
+
responseWaiters.delete(key);
|
|
590
677
|
clearTimeout(waiter.timer);
|
|
591
678
|
if (evt.success)
|
|
592
679
|
waiter.resolve(evt.data);
|
|
593
680
|
else
|
|
594
|
-
waiter.reject(new
|
|
681
|
+
waiter.reject(new PiRpcRefusalError(`pi ${evt.command} failed`));
|
|
595
682
|
}
|
|
596
683
|
return;
|
|
597
684
|
}
|
|
@@ -715,24 +802,81 @@ export const PiDriver = {
|
|
|
715
802
|
}
|
|
716
803
|
return;
|
|
717
804
|
}
|
|
805
|
+
case "compaction_start":
|
|
806
|
+
case "compaction_end":
|
|
807
|
+
case "auto_compaction_start":
|
|
808
|
+
case "auto_compaction_end": {
|
|
809
|
+
// pi compaction (manual or automatic) summarizes older
|
|
810
|
+
// messages, which can absorb the turn that carried this
|
|
811
|
+
// session's standing prompt. Both events ride the RPC surface
|
|
812
|
+
// for every compaction kind, so the moment either arrives the
|
|
813
|
+
// receipt is dropped and the next turn re-establishes the full
|
|
814
|
+
// prompt. The rest of the compacting turn may still answer
|
|
815
|
+
// without its standing instructions; that single turn is the
|
|
816
|
+
// unavoidable cost of summarizing history mid-session.
|
|
817
|
+
if (sessionReady && sessionFile) {
|
|
818
|
+
compactionObserved = true;
|
|
819
|
+
deletePromptSplitReceipt("pi", JSON.stringify([threadId, sessionFile]));
|
|
820
|
+
}
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
718
823
|
case "turn_end":
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
824
|
+
{
|
|
825
|
+
// a killed child can still flush one buffered frame after the
|
|
826
|
+
// turn settled (the abort path's cancelled turn_end is the
|
|
827
|
+
// common one); arming the grace timer then would leak it past
|
|
828
|
+
// a turn that is already complete.
|
|
829
|
+
if (settled)
|
|
830
|
+
return;
|
|
831
|
+
const sr = evt.message?.stopReason;
|
|
832
|
+
// toolUse means pi ran a tool and auto-continues next turn to
|
|
833
|
+
// answer — the run is not over, and the next turn_end carries
|
|
834
|
+
// the real outcome.
|
|
835
|
+
if (sr === "toolUse" || sr === "tool_use" || sr === "tool_calls")
|
|
836
|
+
return;
|
|
837
|
+
endStopReason = sr;
|
|
838
|
+
endUsage = evt.usage ?? evt.message?.usage;
|
|
839
|
+
endErrorMessage = evt.message?.errorMessage;
|
|
840
|
+
// agent_end, not turn_end, closes the run: pi can schedule
|
|
841
|
+
// post-run maintenance (overflow-recovery compaction) after
|
|
842
|
+
// the final turn_end, and only the terminal agent_end proves
|
|
843
|
+
// the session is done rewriting itself. The grace timer
|
|
844
|
+
// settles a runtime that never sends agent_end at all.
|
|
845
|
+
if (agentEndTimer)
|
|
846
|
+
clearTimeout(agentEndTimer);
|
|
847
|
+
agentEndTimer = setTimeout(finishRun, PI_AGENT_END_GRACE_MS);
|
|
848
|
+
agentEndTimer.unref?.();
|
|
724
849
|
return;
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
850
|
+
}
|
|
851
|
+
case "agent_end": {
|
|
852
|
+
// Two dialects mark a non-terminal agent_end: upstream pi sets
|
|
853
|
+
// willRetry: true when an automatic retry follows (overflow
|
|
854
|
+
// recovery runs compaction and retries as a fresh run), and the
|
|
855
|
+
// omp fork sets isTerminal: false while maintenance or async
|
|
856
|
+
// delivery has more work scheduled. Either marker means the
|
|
857
|
+
// session may still rewrite the turn that carried this
|
|
858
|
+
// session's standing prompt — keep listening so those events
|
|
859
|
+
// can drop the prompt-split receipt.
|
|
860
|
+
if (evt.isTerminal === false || evt.willRetry === true)
|
|
733
861
|
return;
|
|
862
|
+
if (agentEndTimer) {
|
|
863
|
+
clearTimeout(agentEndTimer);
|
|
864
|
+
agentEndTimer = null;
|
|
865
|
+
}
|
|
866
|
+
finishRun();
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
case "agent_settled": {
|
|
870
|
+
// upstream pi's explicit fully-settled guarantee: no automatic
|
|
871
|
+
// retry, compaction retry, or queued continuation remains.
|
|
872
|
+
// Usually settles one frame after the final agent_end already
|
|
873
|
+
// did; kept as its own terminal signal so the driver never
|
|
874
|
+
// depends on which dialect the configured cli speaks.
|
|
875
|
+
if (agentEndTimer) {
|
|
876
|
+
clearTimeout(agentEndTimer);
|
|
877
|
+
agentEndTimer = null;
|
|
734
878
|
}
|
|
735
|
-
|
|
879
|
+
finishRun();
|
|
736
880
|
return;
|
|
737
881
|
}
|
|
738
882
|
default:
|
|
@@ -774,6 +918,7 @@ export const PiDriver = {
|
|
|
774
918
|
// sessionFile, which switch_session expects as `sessionPath`.
|
|
775
919
|
const sessionPath = typeof turn.resumeCursor === "string" ? turn.resumeCursor : null;
|
|
776
920
|
let sessionFile = sessionPath;
|
|
921
|
+
let sessionReady = false;
|
|
777
922
|
try {
|
|
778
923
|
const command = sessionPath ? "switch_session" : "new_session";
|
|
779
924
|
const hsPromise = awaitResponse(command);
|
|
@@ -781,6 +926,7 @@ export const PiDriver = {
|
|
|
781
926
|
const hs = (await hsPromise);
|
|
782
927
|
if (hs?.sessionFile)
|
|
783
928
|
sessionFile = hs.sessionFile;
|
|
929
|
+
sessionReady = true;
|
|
784
930
|
emit({
|
|
785
931
|
...base(threadId, turnId),
|
|
786
932
|
type: "session.started",
|
|
@@ -816,17 +962,71 @@ export const PiDriver = {
|
|
|
816
962
|
/* keep going on the engine default */
|
|
817
963
|
}
|
|
818
964
|
}
|
|
819
|
-
//
|
|
820
|
-
//
|
|
821
|
-
//
|
|
822
|
-
//
|
|
823
|
-
//
|
|
824
|
-
//
|
|
825
|
-
|
|
965
|
+
// The stable/volatile split: the full prompt rides only the turn that
|
|
966
|
+
// establishes - or re-instructs, after a soul edit - this pi session,
|
|
967
|
+
// which previously re-sent the whole system prompt on every turn and
|
|
968
|
+
// let it accumulate in the session file once per turn. pi compacts
|
|
969
|
+
// long sessions by summarizing older messages, which can absorb the
|
|
970
|
+
// turn that carried the prompt, so the driver watches pi's
|
|
971
|
+
// compaction events and drops the receipt the moment one arrives,
|
|
972
|
+
// and re-anchors the full prompt every PI_PROMPT_RE_ANCHOR_TURNS
|
|
973
|
+
// bare turns as a backstop for anything the events miss. Receipts
|
|
974
|
+
// are durable because the session file outlives both the per-turn
|
|
975
|
+
// child and this process. Without a session the prompt is the
|
|
976
|
+
// model's only context, so that turn keeps the full block and writes
|
|
977
|
+
// no receipt.
|
|
978
|
+
const halves = promptHalves(turn);
|
|
979
|
+
let message;
|
|
980
|
+
let pendingReceipt = null;
|
|
981
|
+
if (halves.stable !== null && sessionReady && sessionFile) {
|
|
982
|
+
const receiptKey = JSON.stringify([threadId, sessionFile]);
|
|
983
|
+
const composed = splitSessionPrompt(halves.stable, halves.volatile, readPromptSplitReceipt("pi", receiptKey), turn.system, turn.text, Boolean(turn.mentionTurn), PI_PROMPT_RE_ANCHOR_TURNS);
|
|
984
|
+
message = composed.text;
|
|
985
|
+
pendingReceipt = { key: receiptKey, receipt: composed.receipt };
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
message = turn.system ? `${turn.system}\n\n${turn.text}` : turn.text;
|
|
989
|
+
}
|
|
990
|
+
// events cannot interleave with this synchronous stretch, so any
|
|
991
|
+
// compaction recorded above already invalidated what compose read
|
|
826
992
|
try {
|
|
993
|
+
// pi answers the prompt with an authoritative response only after
|
|
994
|
+
// preflight accepts it - and rejects it outright while a
|
|
995
|
+
// compaction runs - so the receipt commits only after that
|
|
996
|
+
// acceptance: a rejected or compacted-around send redelivers on
|
|
997
|
+
// the next turn. The wait is long enough to survive a compaction
|
|
998
|
+
// queue; a dead child rejects it immediately.
|
|
999
|
+
// sendTurn still resolves once the prompt is written: a child that
|
|
1000
|
+
// dies before replying settles the turn through the close handler,
|
|
1001
|
+
// whose waiter sweep can precede this registration.
|
|
1002
|
+
const accepted = awaitResponse("prompt", 120_000);
|
|
827
1003
|
send({ type: "prompt", message, ...(images.length ? { images } : {}) });
|
|
1004
|
+
void accepted.then(() => {
|
|
1005
|
+
if (!pendingReceipt || compactionObserved)
|
|
1006
|
+
return;
|
|
1007
|
+
try {
|
|
1008
|
+
writePromptSplitReceipt("pi", pendingReceipt.key, pendingReceipt.receipt);
|
|
1009
|
+
}
|
|
1010
|
+
catch {
|
|
1011
|
+
/* an unwritten receipt only re-delivers the full prompt next turn */
|
|
1012
|
+
}
|
|
1013
|
+
}, (err) => {
|
|
1014
|
+
if (settled)
|
|
1015
|
+
return;
|
|
1016
|
+
emit({
|
|
1017
|
+
...base(threadId, turnId),
|
|
1018
|
+
type: "runtime.error",
|
|
1019
|
+
message: String(err?.message ?? err).slice(0, 2_000),
|
|
1020
|
+
});
|
|
1021
|
+
settle(false);
|
|
1022
|
+
});
|
|
828
1023
|
}
|
|
829
|
-
catch {
|
|
1024
|
+
catch (err) {
|
|
1025
|
+
emit({
|
|
1026
|
+
...base(threadId, turnId),
|
|
1027
|
+
type: "runtime.error",
|
|
1028
|
+
message: String(err?.message ?? err).slice(0, 2_000),
|
|
1029
|
+
});
|
|
830
1030
|
settle(false);
|
|
831
1031
|
}
|
|
832
1032
|
return { turnId };
|
|
@@ -899,6 +1099,9 @@ export const PiDriver = {
|
|
|
899
1099
|
// xhigh/max only land on models that expose them; pi rejects an
|
|
900
1100
|
// unsupported level and the turn keeps the engine default.
|
|
901
1101
|
effortLevels: EFFORT_LEVELS,
|
|
1102
|
+
// pi's RPC mode takes a mid-turn steer frame the runtime queues
|
|
1103
|
+
// into the running agent without aborting the current step.
|
|
1104
|
+
queueing: true,
|
|
902
1105
|
},
|
|
903
1106
|
sendTurn,
|
|
904
1107
|
interruptTurn: async (threadId) => active.get(threadId)?.stop(),
|
|
@@ -918,6 +1121,10 @@ export const PiDriver = {
|
|
|
918
1121
|
});
|
|
919
1122
|
return decision.behavior === "allow" ? "allowed-once" : decision.behavior === "answer" ? "answered" : "rejected";
|
|
920
1123
|
},
|
|
1124
|
+
steer: async (threadId, text) => {
|
|
1125
|
+
const entry = active.get(threadId);
|
|
1126
|
+
return entry?.steer ? await entry.steer(text) : "refused";
|
|
1127
|
+
},
|
|
921
1128
|
hasSession: (threadId) => active.has(threadId),
|
|
922
1129
|
stopAll: async () => {
|
|
923
1130
|
for (const { stop } of active.values())
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// work, recent work) legitimately changes mid-conversation and reaches
|
|
6
6
|
// the model inside the turn that changed it, after the cacheable prefix.
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
|
-
import { mkdirSync, readFileSync } from "node:fs";
|
|
8
|
+
import { mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { DATA_DIR } from "../config.js";
|
|
11
11
|
import { writeFileAtomic } from "../atomic.js";
|
|
@@ -53,7 +53,11 @@ export function readPromptSplitReceipt(scope, key) {
|
|
|
53
53
|
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
54
54
|
const record = raw;
|
|
55
55
|
if (typeof record.stable === "string" && typeof record.volatile === "string") {
|
|
56
|
-
return {
|
|
56
|
+
return {
|
|
57
|
+
stable: record.stable,
|
|
58
|
+
volatile: record.volatile,
|
|
59
|
+
...(typeof record.turnsSinceFull === "number" ? { turnsSinceFull: record.turnsSinceFull } : {}),
|
|
60
|
+
};
|
|
57
61
|
}
|
|
58
62
|
}
|
|
59
63
|
}
|
|
@@ -66,6 +70,18 @@ export function writePromptSplitReceipt(scope, key, receipt) {
|
|
|
66
70
|
mkdirSync(join(DATA_DIR, "prompt-split"), { recursive: true });
|
|
67
71
|
writeFileAtomic(receiptPath(scope, key), JSON.stringify(receipt), { mode: 0o600 });
|
|
68
72
|
}
|
|
73
|
+
/** Drop a session's receipt: its history was rewritten (pi compaction
|
|
74
|
+
* summarizes older messages), so what it last carried is unknown and the
|
|
75
|
+
* next turn re-delivers the full prompt. A missing receipt already reads
|
|
76
|
+
* as unknown, so deleting one is always safe. */
|
|
77
|
+
export function deletePromptSplitReceipt(scope, key) {
|
|
78
|
+
try {
|
|
79
|
+
rmSync(receiptPath(scope, key));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
/* a missing receipt is already unknown */
|
|
83
|
+
}
|
|
84
|
+
}
|
|
69
85
|
/** Compose the prompt for a session that persists its own history (ACP
|
|
70
86
|
* agents, pi): the full system block rides only when this native session
|
|
71
87
|
* has not carried it - or carried a different one, which re-delivers the
|
|
@@ -74,14 +90,21 @@ export function writePromptSplitReceipt(scope, key, receipt) {
|
|
|
74
90
|
* through bare, so an ordinary memory write neither appends a second copy
|
|
75
91
|
* of the prompt to the session nor re-prices its cached prefix.
|
|
76
92
|
* perTurnVolatile marks a turn whose volatile half describes this very
|
|
77
|
-
* turn (a mention): its note is delivered even when the text is unchanged.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
93
|
+
* turn (a mention): its note is delivered even when the text is unchanged.
|
|
94
|
+
* reAnchorAfterTurns, when positive, re-sends the full prompt after that
|
|
95
|
+
* many consecutive bare turns: a session that rewrites its own history
|
|
96
|
+
* without an observable event (an older pi, a missed compaction) still
|
|
97
|
+
* gets its standing instructions back within a bounded number of turns. */
|
|
98
|
+
export function splitSessionPrompt(stable, volatile, previous, fullSystem, text, perTurnVolatile = false, reAnchorAfterTurns = 0) {
|
|
99
|
+
const fingerprints = promptSplitFingerprints(stable, volatile);
|
|
100
|
+
// Legacy receipts predate the turn counter; count them from zero.
|
|
101
|
+
const turnsSinceFull = typeof previous?.turnsSinceFull === "number" ? previous.turnsSinceFull : 0;
|
|
102
|
+
const reAnchor = reAnchorAfterTurns > 0 && previous !== null && turnsSinceFull >= reAnchorAfterTurns;
|
|
103
|
+
if (previous === null || previous.stable !== fingerprints.stable || reAnchor) {
|
|
104
|
+
return { text: fullSystem ? fullSystem + "\n\n" + text : text, receipt: { ...fingerprints, turnsSinceFull: 0 } };
|
|
82
105
|
}
|
|
83
|
-
const note = previous.volatile ===
|
|
106
|
+
const note = previous.volatile === fingerprints.volatile && !perTurnVolatile
|
|
84
107
|
? ""
|
|
85
108
|
: volatileContextNote(volatile, previous.volatile !== EMPTY_FINGERPRINT);
|
|
86
|
-
return { text: withContextNote(note, text), receipt };
|
|
109
|
+
return { text: withContextNote(note, text), receipt: { ...fingerprints, turnsSinceFull: turnsSinceFull + 1 } };
|
|
87
110
|
}
|