@evandu/super-pi 0.1.144 → 0.1.146
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/agents/super-pi-task/core.ts +15 -2
- package/agents/super-pi-task/index.ts +3 -0
- package/dist/.super-pi-check-inputs.json +2 -2
- package/dist/client/.super-pi-client-sources.json +9 -9
- package/dist/client/assets/{CodeMirrorEditor-DfQHX1Hy.js → CodeMirrorEditor-CEwBCVM6.js} +2 -2
- package/dist/client/assets/{dist-Cu3B0JQX.js → dist-76ArZ5M4.js} +1 -1
- package/dist/client/assets/{dist-BEbEv2KM.js → dist-9F1wL-T5.js} +1 -1
- package/dist/client/assets/{dist-BRjyQs_L.js → dist-B0RXZeXQ.js} +1 -1
- package/dist/client/assets/{dist-SzZEAk24.js → dist-BCNaDgNW.js} +1 -1
- package/dist/client/assets/{dist-qAUj_2Ue.js → dist-BXBSIuTD.js} +1 -1
- package/dist/client/assets/{dist-BGBinEie.js → dist-Bh8Q7Y_n.js} +1 -1
- package/dist/client/assets/{dist-DKkMk3XX.js → dist-BoyICDlL.js} +1 -1
- package/dist/client/assets/{dist-ISoxXlmW.js → dist-BuR5Iagd.js} +1 -1
- package/dist/client/assets/{dist-LRNzDUVb.js → dist-Bx6ZWYUW.js} +1 -1
- package/dist/client/assets/{dist-DOvzruAQ.js → dist-CAOcjKAQ.js} +1 -1
- package/dist/client/assets/{dist-CDtFD-PW.js → dist-CHt8PX8R.js} +1 -1
- package/dist/client/assets/{dist-BuPimjY-.js → dist-CSCUT1PQ.js} +1 -1
- package/dist/client/assets/{dist-BsMikSTM.js → dist-CUB_Chdh.js} +1 -1
- package/dist/client/assets/{dist-KQJCVwxy.js → dist-CbVf2Cjn.js} +1 -1
- package/dist/client/assets/{dist-Dnru8_uX.js → dist-DBUqwiWG.js} +1 -1
- package/dist/client/assets/{dist-B8oH6dsc.js → dist-DC1IocrS.js} +1 -1
- package/dist/client/assets/{dist-D7aAMRTb.js → dist-DFMtP3wG.js} +1 -1
- package/dist/client/assets/{dist-Cw_KCDH9.js → dist-DN0GlnnZ.js} +1 -1
- package/dist/client/assets/{dist-BCLhQXYY.js → dist-DftD4ibj.js} +1 -1
- package/dist/client/assets/{dist-CgQ7zaWY.js → dist-Do52MRwm.js} +1 -1
- package/dist/client/assets/{dist-BS-FBUKW.js → dist-JvQIIdqB.js} +1 -1
- package/dist/client/assets/{dist-C5BQsmB7.js → dist-oLq6HXLG.js} +1 -1
- package/dist/client/assets/{index-Xaui0uUI.css → index-D-OxlUxN.css} +1 -1
- package/dist/client/assets/index-E7yaLa2C.js +102 -0
- package/dist/client/index.html +2 -2
- package/dist/server/agents.d.ts +1 -1
- package/dist/server/agents.js +3 -1
- package/dist/server/application/conversation-service.d.ts +1 -1
- package/dist/server/application/conversation-service.js +6 -4
- package/dist/server/application/run-service.d.ts +16 -2
- package/dist/server/application/run-service.js +121 -14
- package/dist/server/application/scheduled-task-service.d.ts +14 -1
- package/dist/server/application/scheduled-task-service.js +27 -2
- package/dist/server/application/session-orchestrator-service.d.ts +11 -2
- package/dist/server/application/session-orchestrator-service.js +103 -27
- package/dist/server/command-handler.js +1 -1
- package/dist/server/conversation-usage.d.ts +19 -0
- package/dist/server/conversation-usage.js +40 -0
- package/dist/server/file-diff.d.ts +3 -0
- package/dist/server/file-diff.js +4 -4
- package/dist/server/file-index.d.ts +4 -2
- package/dist/server/file-index.js +14 -9
- package/dist/server/file-name-index.d.ts +5 -2
- package/dist/server/file-name-index.js +22 -5
- package/dist/server/git.d.ts +3 -3
- package/dist/server/git.js +10 -9
- package/dist/server/http-server.js +195 -33
- package/dist/server/model-context-window.d.ts +46 -0
- package/dist/server/model-context-window.js +121 -0
- package/dist/server/pi-session.d.ts +6 -0
- package/dist/server/pi-session.js +15 -1
- package/dist/server/runtime.d.ts +3 -0
- package/dist/server/runtime.js +21 -6
- package/dist/server/session-cwd.d.ts +17 -0
- package/dist/server/session-cwd.js +43 -0
- package/dist/server/session-ledger.d.ts +7 -0
- package/dist/server/store.d.ts +22 -0
- package/dist/server/store.js +52 -0
- package/dist/server/task-tool.d.ts +2 -1
- package/dist/server/task-tool.js +3 -1
- package/dist/server/terminal.d.ts +1 -0
- package/dist/server/terminal.js +5 -3
- package/dist/server/turn-failure.js +16 -1
- package/dist/server/types.d.ts +18 -0
- package/dist/server/version.d.ts +25 -2
- package/dist/server/version.js +98 -26
- package/dist/server/ws-server.d.ts +1 -1
- package/dist/server/ws-server.js +7 -5
- package/package.json +20 -3
- package/dist/client/assets/index-BLWr70ej.js +0 -100
|
@@ -23,7 +23,8 @@ import { MAX_WEB_REQUEST_BYTES } from "./web.js";
|
|
|
23
23
|
import { MAX_TOOL_BODY_BYTES } from "./tool-registry.js";
|
|
24
24
|
import { packageUpdateArgs } from "./update.js";
|
|
25
25
|
import { FileIndex } from "./file-index.js";
|
|
26
|
-
import { ensureNameIndex } from "./file-name-index.js";
|
|
26
|
+
import { ensureNameIndex, recordWorkspacePath } from "./file-name-index.js";
|
|
27
|
+
import { resolveSessionCwd, sessionScopeKey, workspacePathFromSession } from "./session-cwd.js";
|
|
27
28
|
import { MAX_CONTENT_DEPTH, MAX_CONTENT_FILE_BYTES, MAX_CONTENT_RESULTS, searchByContent } from "./content-search.js";
|
|
28
29
|
import { getAvailableSkills, readSkill } from "./skills.js";
|
|
29
30
|
/**
|
|
@@ -34,11 +35,11 @@ import { getAvailableSkills, readSkill } from "./skills.js";
|
|
|
34
35
|
* known so far rather than refused. The scan is the server's cost, not the user's - the old
|
|
35
36
|
* implementation made the user pay it on every keystroke.
|
|
36
37
|
*/
|
|
37
|
-
function nameIndexFor(context, workspaceId, root) {
|
|
38
|
+
function nameIndexFor(context, workspaceId, root, cwd = "") {
|
|
38
39
|
const indexes = context.nameIndexes ?? new Map();
|
|
39
40
|
if (!context.nameIndexes)
|
|
40
41
|
context.nameIndexes = indexes;
|
|
41
|
-
return ensureNameIndex(indexes, workspaceId, root);
|
|
42
|
+
return ensureNameIndex(indexes, workspaceId, root, cwd);
|
|
42
43
|
}
|
|
43
44
|
const MIME_TYPES = {
|
|
44
45
|
".css": "text/css; charset=utf-8",
|
|
@@ -312,7 +313,7 @@ export function createHttpServer(options) {
|
|
|
312
313
|
// the endpoint that can spend it is exactly the one an agent's own turn can reach, and the
|
|
313
314
|
// management half (`/api/feishu/config`) stays behind the ordinary token AND the local-address
|
|
314
315
|
// predicate.
|
|
315
|
-
if (requestPath !== "/api/memory/tool" && requestPath !== "/api/knowledge/tool" && requestPath !== "/api/documents/tool" && requestPath !== "/api/web/tool" && requestPath !== "/api/feishu/tool" && requestPath !== "/api/todos" && requestPath !== "/api/scheduled-tasks" && requestPath !== "/api/question" && (requestPath !== "/api/tools" || request.method !== "POST") && !requestPath.startsWith("/tools/") && !isAuthorized(request, accessToken)) {
|
|
316
|
+
if (requestPath !== "/api/conversation/usage" && requestPath !== "/api/memory/tool" && requestPath !== "/api/knowledge/tool" && requestPath !== "/api/documents/tool" && requestPath !== "/api/web/tool" && requestPath !== "/api/feishu/tool" && requestPath !== "/api/todos" && requestPath !== "/api/scheduled-tasks" && requestPath !== "/api/question" && (requestPath !== "/api/tools" || request.method !== "POST") && !requestPath.startsWith("/tools/") && !isAuthorized(request, accessToken)) {
|
|
316
317
|
response.writeHead(401, { "www-authenticate": "Bearer" }).end("Authentication required");
|
|
317
318
|
return;
|
|
318
319
|
}
|
|
@@ -1070,6 +1071,8 @@ export function createHttpServer(options) {
|
|
|
1070
1071
|
json(404, { error: `工作区 ${workspaceId ?? "(missing)"} 不存在`, code: "no_workspace" });
|
|
1071
1072
|
return;
|
|
1072
1073
|
}
|
|
1074
|
+
const sessionRoot = await resolveSessionCwd(workspace.path, url.searchParams.get("cwd"));
|
|
1075
|
+
const cwd = relative(resolve(workspace.path), sessionRoot).replaceAll("\\", "/");
|
|
1073
1076
|
const op = url.searchParams.get("op") ?? "status";
|
|
1074
1077
|
// Blame is a read with a GET shape: every other verb is refused HERE, before the POST branch
|
|
1075
1078
|
// could ever route the op into gitMutate and before any body is read.
|
|
@@ -1095,7 +1098,8 @@ export function createHttpServer(options) {
|
|
|
1095
1098
|
}
|
|
1096
1099
|
const raw = asked.text;
|
|
1097
1100
|
const body = raw ? JSON.parse(raw) : {};
|
|
1098
|
-
|
|
1101
|
+
const path = body.path ? await workspacePathFromSession(workspace.path, sessionRoot, body.path) : undefined;
|
|
1102
|
+
json(200, await gitMutate(workspace.path, op, path, body.message, cwd));
|
|
1099
1103
|
return;
|
|
1100
1104
|
}
|
|
1101
1105
|
if (request.method !== "GET") {
|
|
@@ -1103,16 +1107,16 @@ export function createHttpServer(options) {
|
|
|
1103
1107
|
return;
|
|
1104
1108
|
}
|
|
1105
1109
|
if (op === "status")
|
|
1106
|
-
json(200, await gitStatus(workspace.path));
|
|
1110
|
+
json(200, await gitStatus(workspace.path, cwd));
|
|
1107
1111
|
else if (op === "log")
|
|
1108
|
-
json(200, await gitLog(workspace.path, Number(url.searchParams.get("limit") ?? 20)));
|
|
1112
|
+
json(200, await gitLog(workspace.path, Number(url.searchParams.get("limit") ?? 20), cwd));
|
|
1109
1113
|
else if (op === "diff") {
|
|
1110
1114
|
const path = url.searchParams.get("path");
|
|
1111
1115
|
if (!path) {
|
|
1112
1116
|
json(400, { error: "diff 需要 path", code: "bad_path" });
|
|
1113
1117
|
return;
|
|
1114
1118
|
}
|
|
1115
|
-
json(200, await gitDiff(workspace.path, path, url.searchParams.get("staged") === "1"));
|
|
1119
|
+
json(200, await gitDiff(workspace.path, await workspacePathFromSession(workspace.path, sessionRoot, path), url.searchParams.get("staged") === "1"));
|
|
1116
1120
|
}
|
|
1117
1121
|
else if (op === "blame") {
|
|
1118
1122
|
const path = url.searchParams.get("path");
|
|
@@ -1122,7 +1126,7 @@ export function createHttpServer(options) {
|
|
|
1122
1126
|
}
|
|
1123
1127
|
const startLine = url.searchParams.get("startLine");
|
|
1124
1128
|
const endLine = url.searchParams.get("endLine");
|
|
1125
|
-
json(200, await gitBlame(workspace.path, path, {
|
|
1129
|
+
json(200, await gitBlame(workspace.path, await workspacePathFromSession(workspace.path, sessionRoot, path), {
|
|
1126
1130
|
revision: url.searchParams.get("revision") ?? undefined,
|
|
1127
1131
|
startLine: startLine === null ? undefined : Number(startLine),
|
|
1128
1132
|
endLine: endLine === null ? undefined : Number(endLine),
|
|
@@ -1136,7 +1140,7 @@ export function createHttpServer(options) {
|
|
|
1136
1140
|
const code = error?.code;
|
|
1137
1141
|
if (code === 128 || code === "not_a_repo" || /not a git repository/i.test(message))
|
|
1138
1142
|
json(404, { error: "此工作区不是 git 仓库", code: "not_a_repo" });
|
|
1139
|
-
else if (code === "path")
|
|
1143
|
+
else if (code === "path" || code === "outside_workspace")
|
|
1140
1144
|
json(400, { error: message, code: "bad_path" });
|
|
1141
1145
|
else if (code === "not_found")
|
|
1142
1146
|
json(404, { error: message, code: "not_found" });
|
|
@@ -1480,27 +1484,70 @@ export function createHttpServer(options) {
|
|
|
1480
1484
|
}
|
|
1481
1485
|
const action = (body.action || "").trim();
|
|
1482
1486
|
if (action === "spawn") {
|
|
1487
|
+
// Capability auth above proves who is calling. The target workspace is a separate, explicit
|
|
1488
|
+
// choice: a parentConversationId records a tree edge and never selects that target.
|
|
1489
|
+
const requestedWorkspaceId = body.workspaceId?.trim();
|
|
1490
|
+
if (!requestedWorkspaceId) {
|
|
1491
|
+
json(400, { error: "创建会话必须显式指定 workspaceId;父会话只记录树形关系,不能代替目标工作区。" });
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
const store = context.store;
|
|
1495
|
+
const callerWorkspaceId = store && callerConversationId
|
|
1496
|
+
? store.conversation(callerConversationId)?.workspaceId
|
|
1497
|
+
: undefined;
|
|
1498
|
+
if (requestedWorkspaceId !== callerWorkspaceId) {
|
|
1499
|
+
json(403, {
|
|
1500
|
+
error: `调用方会话 ${callerConversationId} 所在工作区为 ${callerWorkspaceId ?? "未知"},请求的目标工作区 ${requestedWorkspaceId} 不属于调用方,拒绝在该工作区创建会话。请传入调用方自己的工作区。`,
|
|
1501
|
+
});
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1483
1504
|
if (!body.task)
|
|
1484
1505
|
throw new Error("spawn requires task");
|
|
1485
1506
|
const result = await service.spawn({
|
|
1486
1507
|
task: body.task,
|
|
1487
1508
|
parentConversationId: body.parentConversationId,
|
|
1488
|
-
workspaceId:
|
|
1509
|
+
workspaceId: requestedWorkspaceId,
|
|
1489
1510
|
title: body.title,
|
|
1490
1511
|
agentId: body.agentId,
|
|
1491
1512
|
model: body.model,
|
|
1492
1513
|
thinkingLevel: body.thinkingLevel,
|
|
1493
1514
|
initialPrompt: body.initialPrompt,
|
|
1494
1515
|
wait: body.wait,
|
|
1516
|
+
cwd: body.cwd,
|
|
1495
1517
|
});
|
|
1496
1518
|
json(200, { ok: true, ...result });
|
|
1497
1519
|
return;
|
|
1498
1520
|
}
|
|
1521
|
+
const verifyTargetScope = (targetConversationId) => {
|
|
1522
|
+
const store = context.store;
|
|
1523
|
+
const callerWorkspaceId = store && callerConversationId
|
|
1524
|
+
? store.conversation(callerConversationId)?.workspaceId
|
|
1525
|
+
: undefined;
|
|
1526
|
+
if (!callerWorkspaceId) {
|
|
1527
|
+
return {
|
|
1528
|
+
ok: false,
|
|
1529
|
+
error: `调用方会话 ${callerConversationId} 不存在或未关联有效工作区,拒绝编排操作。`,
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
const target = store?.conversation(targetConversationId);
|
|
1533
|
+
if (target && target.workspaceId !== callerWorkspaceId) {
|
|
1534
|
+
return {
|
|
1535
|
+
ok: false,
|
|
1536
|
+
error: `调用方会话 ${callerConversationId} 所在工作区为 ${callerWorkspaceId},目标会话 ${targetConversationId} 所在工作区为 ${target.workspaceId},拒绝跨工作区操作。目标会话必须属于调用方工作区。`,
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
return { ok: true };
|
|
1540
|
+
};
|
|
1499
1541
|
if (action === "dispatch") {
|
|
1500
1542
|
if (!body.conversationId)
|
|
1501
1543
|
throw new Error("dispatch requires conversationId");
|
|
1502
1544
|
if (!body.content)
|
|
1503
1545
|
throw new Error("dispatch requires content");
|
|
1546
|
+
const scope = verifyTargetScope(body.conversationId);
|
|
1547
|
+
if (!scope.ok) {
|
|
1548
|
+
json(403, { error: scope.error });
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1504
1551
|
const result = await service.dispatch(body.conversationId, body.content, { wait: body.wait });
|
|
1505
1552
|
json(200, { ok: true, ...result });
|
|
1506
1553
|
return;
|
|
@@ -1508,18 +1555,48 @@ export function createHttpServer(options) {
|
|
|
1508
1555
|
if (action === "status") {
|
|
1509
1556
|
if (!body.conversationId)
|
|
1510
1557
|
throw new Error("status requires conversationId");
|
|
1558
|
+
const scope = verifyTargetScope(body.conversationId);
|
|
1559
|
+
if (!scope.ok) {
|
|
1560
|
+
json(403, { error: scope.error });
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1511
1563
|
const summary = await service.status(body.conversationId, { includeOutput: body.includeOutput });
|
|
1512
1564
|
json(200, { ok: true, summary });
|
|
1513
1565
|
return;
|
|
1514
1566
|
}
|
|
1515
1567
|
if (action === "list") {
|
|
1516
|
-
const
|
|
1517
|
-
|
|
1568
|
+
const store = context.store;
|
|
1569
|
+
const callerWorkspaceId = store && callerConversationId
|
|
1570
|
+
? store.conversation(callerConversationId)?.workspaceId
|
|
1571
|
+
: undefined;
|
|
1572
|
+
if (!callerWorkspaceId) {
|
|
1573
|
+
json(403, { error: `调用方会话 ${callerConversationId} 不存在或未关联有效工作区,拒绝编排操作。` });
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
const requestedParentId = body.parentConversationId?.trim();
|
|
1577
|
+
if (requestedParentId) {
|
|
1578
|
+
const targetParent = store?.conversation(requestedParentId);
|
|
1579
|
+
if (targetParent && targetParent.workspaceId !== callerWorkspaceId) {
|
|
1580
|
+
json(403, {
|
|
1581
|
+
error: `调用方会话 ${callerConversationId} 所在工作区为 ${callerWorkspaceId},请求的父会话 ${requestedParentId} 所在工作区为 ${targetParent.workspaceId},拒绝跨工作区查询。父会话必须属于调用方工作区。`,
|
|
1582
|
+
});
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
const effectiveParentId = requestedParentId || callerConversationId;
|
|
1587
|
+
const sessions = await service.list(effectiveParentId);
|
|
1588
|
+
const allowedSessions = sessions.filter((s) => !s.workspaceId || s.workspaceId === callerWorkspaceId);
|
|
1589
|
+
json(200, { ok: true, sessions: allowedSessions });
|
|
1518
1590
|
return;
|
|
1519
1591
|
}
|
|
1520
1592
|
if (action === "stop") {
|
|
1521
1593
|
if (!body.conversationId)
|
|
1522
1594
|
throw new Error("stop requires conversationId");
|
|
1595
|
+
const scope = verifyTargetScope(body.conversationId);
|
|
1596
|
+
if (!scope.ok) {
|
|
1597
|
+
json(403, { error: scope.error });
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1523
1600
|
const result = await service.stop(body.conversationId, { mode: body.mode, messageId: body.messageId });
|
|
1524
1601
|
json(200, { ok: true, ...result });
|
|
1525
1602
|
return;
|
|
@@ -1527,6 +1604,11 @@ export function createHttpServer(options) {
|
|
|
1527
1604
|
if (action === "resume") {
|
|
1528
1605
|
if (!body.conversationId)
|
|
1529
1606
|
throw new Error("resume requires conversationId");
|
|
1607
|
+
const scope = verifyTargetScope(body.conversationId);
|
|
1608
|
+
if (!scope.ok) {
|
|
1609
|
+
json(403, { error: scope.error });
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1530
1612
|
const result = await service.resume(body.conversationId, body.prompt, { wait: body.wait });
|
|
1531
1613
|
json(200, { ok: true, ...result });
|
|
1532
1614
|
return;
|
|
@@ -1538,6 +1620,36 @@ export function createHttpServer(options) {
|
|
|
1538
1620
|
}
|
|
1539
1621
|
return;
|
|
1540
1622
|
}
|
|
1623
|
+
if (url.pathname === "/api/conversation/usage") {
|
|
1624
|
+
const json = (status, payload) => {
|
|
1625
|
+
response.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" }).end(JSON.stringify(payload));
|
|
1626
|
+
};
|
|
1627
|
+
if (request.method !== "GET" || !isLocalOrPrivateAddress(remote)) {
|
|
1628
|
+
json(403, { error: "Usage readout requires a local/private GET request." });
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
const callerConversationId = url.searchParams.get("callerConversationId") ?? "";
|
|
1632
|
+
const capability = request.headers["x-super-pi-capability"] ?? url.searchParams.get("capability");
|
|
1633
|
+
const verdict = runService.verifyPrimaryCapability(callerConversationId, capability);
|
|
1634
|
+
if (!verdict.ok) {
|
|
1635
|
+
json(403, { error: verdict.message });
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
const target = url.searchParams.get("conversationId") ?? callerConversationId;
|
|
1639
|
+
if (target !== callerConversationId) {
|
|
1640
|
+
json(403, { error: "Only the caller's own conversation usage may be read." });
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
const conversation = store.conversation(callerConversationId);
|
|
1644
|
+
if (!conversation) {
|
|
1645
|
+
json(403, { error: "Caller conversation is unavailable." });
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
json(200, { ok: true, conversationId: callerConversationId, usage: conversation.usage ?? null,
|
|
1649
|
+
usageCostKnown: conversation.usageCostKnown ?? null, contextUsage: conversation.contextUsage ?? null,
|
|
1650
|
+
usageUpdatedAt: conversation.usageUpdatedAt ?? null });
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1541
1653
|
if (url.pathname === "/api/orchestrator/status" && request.method === "GET") {
|
|
1542
1654
|
if (!isLocalOrPrivateAddress(remote)) {
|
|
1543
1655
|
response.writeHead(403).end();
|
|
@@ -1559,6 +1671,23 @@ export function createHttpServer(options) {
|
|
|
1559
1671
|
response.writeHead(400, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "缺少 conversationId" }));
|
|
1560
1672
|
return;
|
|
1561
1673
|
}
|
|
1674
|
+
const store = context.store;
|
|
1675
|
+
const callerWorkspaceId = store && callerConversationId
|
|
1676
|
+
? store.conversation(callerConversationId)?.workspaceId
|
|
1677
|
+
: undefined;
|
|
1678
|
+
if (!callerWorkspaceId) {
|
|
1679
|
+
response.writeHead(403, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({
|
|
1680
|
+
error: `调用方会话 ${callerConversationId} 不存在或未关联有效工作区,拒绝编排操作。`,
|
|
1681
|
+
}));
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
const target = store?.conversation(conversationId);
|
|
1685
|
+
if (target && target.workspaceId !== callerWorkspaceId) {
|
|
1686
|
+
response.writeHead(403, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({
|
|
1687
|
+
error: `调用方会话 ${callerConversationId} 所在工作区为 ${callerWorkspaceId},目标会话 ${conversationId} 所在工作区为 ${target.workspaceId},拒绝跨工作区操作。目标会话必须属于调用方工作区。`,
|
|
1688
|
+
}));
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1562
1691
|
try {
|
|
1563
1692
|
const summary = await service.status(conversationId, { includeOutput: url.searchParams.get("includeOutput") === "true" });
|
|
1564
1693
|
response.writeHead(200, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ ok: true, summary }));
|
|
@@ -1585,9 +1714,30 @@ export function createHttpServer(options) {
|
|
|
1585
1714
|
response.writeHead(403, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: verdict.message }));
|
|
1586
1715
|
return;
|
|
1587
1716
|
}
|
|
1717
|
+
const store = context.store;
|
|
1718
|
+
const callerWorkspaceId = store && callerConversationId
|
|
1719
|
+
? store.conversation(callerConversationId)?.workspaceId
|
|
1720
|
+
: undefined;
|
|
1721
|
+
if (!callerWorkspaceId) {
|
|
1722
|
+
response.writeHead(403, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({
|
|
1723
|
+
error: `调用方会话 ${callerConversationId} 不存在或未关联有效工作区,拒绝编排操作。`,
|
|
1724
|
+
}));
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
if (parentId) {
|
|
1728
|
+
const targetParent = store?.conversation(parentId);
|
|
1729
|
+
if (targetParent && targetParent.workspaceId !== callerWorkspaceId) {
|
|
1730
|
+
response.writeHead(403, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({
|
|
1731
|
+
error: `调用方会话 ${callerConversationId} 所在工作区为 ${callerWorkspaceId},请求的父会话 ${parentId} 所在工作区为 ${targetParent.workspaceId},拒绝跨工作区查询。父会话必须属于调用方工作区。`,
|
|
1732
|
+
}));
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1588
1736
|
try {
|
|
1589
|
-
const
|
|
1590
|
-
|
|
1737
|
+
const effectiveParentId = parentId || callerConversationId;
|
|
1738
|
+
const sessions = await service.list(effectiveParentId);
|
|
1739
|
+
const allowedSessions = sessions.filter((s) => !s.workspaceId || s.workspaceId === callerWorkspaceId);
|
|
1740
|
+
response.writeHead(200, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ ok: true, sessions: allowedSessions }));
|
|
1591
1741
|
}
|
|
1592
1742
|
catch (error) {
|
|
1593
1743
|
response.writeHead(400, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: error instanceof Error ? error.message : "list failed" }));
|
|
@@ -1644,16 +1794,20 @@ export function createHttpServer(options) {
|
|
|
1644
1794
|
json(400, { error: "需要 path 参数", code: "bad_request" });
|
|
1645
1795
|
return;
|
|
1646
1796
|
}
|
|
1797
|
+
const root = await resolveSessionCwd(workspace.path, url.searchParams.get("cwd"));
|
|
1798
|
+
const workspaceRelativePath = await workspacePathFromSession(workspace.path, root, path);
|
|
1647
1799
|
const requested = url.searchParams.get("base")?.trim();
|
|
1648
1800
|
const preferred = requested === "git" || requested === "agent" ? requested : "auto";
|
|
1649
1801
|
// The baseline is chosen by the server, but the client can name one - and when it names one, that
|
|
1650
1802
|
// is the one it gets. A silent fall-back would show the user a comparison they did not ask for.
|
|
1651
1803
|
const diff = await computeFileDiff({
|
|
1652
|
-
root
|
|
1804
|
+
root,
|
|
1653
1805
|
path,
|
|
1654
1806
|
preferred,
|
|
1655
1807
|
tracker: context.fileChanges,
|
|
1656
1808
|
workspaceId,
|
|
1809
|
+
gitRoot: workspace.path,
|
|
1810
|
+
trackerPath: workspaceRelativePath,
|
|
1657
1811
|
});
|
|
1658
1812
|
json(200, diff);
|
|
1659
1813
|
}
|
|
@@ -1677,7 +1831,9 @@ export function createHttpServer(options) {
|
|
|
1677
1831
|
json(404, { error: "找不到这个工作区——它可能已经被删除。", code: "no_workspace" });
|
|
1678
1832
|
return;
|
|
1679
1833
|
}
|
|
1680
|
-
await
|
|
1834
|
+
const root = await resolveSessionCwd(workspace.path, url.searchParams.get("cwd"));
|
|
1835
|
+
const cwd = relative(resolve(workspace.path), root).replaceAll("\\", "/");
|
|
1836
|
+
const indexKey = sessionScopeKey(workspaceId, cwd);
|
|
1681
1837
|
if (query.length > 256 || /[\r\n]/.test(query)) {
|
|
1682
1838
|
json(400, { error: "搜索词只能是一行,且不超过 256 个字符。", code: "bad_query" });
|
|
1683
1839
|
return;
|
|
@@ -1689,17 +1845,17 @@ export function createHttpServer(options) {
|
|
|
1689
1845
|
const indexes = context.fileIndexes ?? new Map();
|
|
1690
1846
|
if (!context.fileIndexes)
|
|
1691
1847
|
context.fileIndexes = indexes;
|
|
1692
|
-
let index = indexes.get(
|
|
1848
|
+
let index = indexes.get(indexKey);
|
|
1693
1849
|
if (!index) {
|
|
1694
|
-
index = new FileIndex(workspaceId, (event) => broadcaster.broadcast(event));
|
|
1695
|
-
indexes.set(
|
|
1850
|
+
index = new FileIndex(workspaceId, (event) => broadcaster.broadcast(event), cwd);
|
|
1851
|
+
indexes.set(indexKey, index);
|
|
1696
1852
|
}
|
|
1697
|
-
index.start(
|
|
1853
|
+
index.start(root);
|
|
1698
1854
|
const deadline = Date.now() + 4500;
|
|
1699
1855
|
while (!index.builtAt && Date.now() < deadline)
|
|
1700
1856
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1701
1857
|
const snapshot = index.snapshot();
|
|
1702
|
-
const result = await searchByContent(
|
|
1858
|
+
const result = await searchByContent(root, [...index.entries.values()], query, snapshot);
|
|
1703
1859
|
json(200, { mode: "content", query, ...result, maxResults: MAX_CONTENT_RESULTS, maxFileBytes: MAX_CONTENT_FILE_BYTES, maxDepth: MAX_CONTENT_DEPTH });
|
|
1704
1860
|
}
|
|
1705
1861
|
catch (error) {
|
|
@@ -1738,18 +1894,21 @@ export function createHttpServer(options) {
|
|
|
1738
1894
|
json(404, { error: `找不到工作区「${workspaceId ?? "未指定"}」——它可能已经被删除。`, code: "no_workspace" });
|
|
1739
1895
|
return;
|
|
1740
1896
|
}
|
|
1897
|
+
const root = await resolveSessionCwd(workspace.path, url.searchParams.get("cwd"));
|
|
1898
|
+
const cwd = relative(resolve(workspace.path), root).replaceAll("\\", "/");
|
|
1899
|
+
const indexKey = sessionScopeKey(workspaceId, cwd);
|
|
1741
1900
|
const indexes = context.fileIndexes ?? new Map();
|
|
1742
1901
|
if (!context.fileIndexes)
|
|
1743
1902
|
context.fileIndexes = indexes;
|
|
1744
|
-
let index = indexes.get(
|
|
1903
|
+
let index = indexes.get(indexKey);
|
|
1745
1904
|
if (!index) {
|
|
1746
|
-
index = new FileIndex(workspaceId, (event) => broadcaster.broadcast(event));
|
|
1747
|
-
indexes.set(
|
|
1905
|
+
index = new FileIndex(workspaceId, (event) => broadcaster.broadcast(event), cwd);
|
|
1906
|
+
indexes.set(indexKey, index);
|
|
1748
1907
|
}
|
|
1749
|
-
index.start(
|
|
1908
|
+
index.start(root);
|
|
1750
1909
|
// Quick Open requests the full index on demand. Name search can share its background scan;
|
|
1751
1910
|
// merely selecting a session no longer starts a multi-MB download.
|
|
1752
|
-
nameIndexFor(context, workspaceId,
|
|
1911
|
+
nameIndexFor(context, workspaceId, root, cwd);
|
|
1753
1912
|
const sinceRaw = url.searchParams.get("since");
|
|
1754
1913
|
const since = sinceRaw === null ? undefined : Number(sinceRaw);
|
|
1755
1914
|
json(200, index.snapshot(Number.isSafeInteger(since) && since >= 0 ? since : undefined));
|
|
@@ -1771,7 +1930,9 @@ export function createHttpServer(options) {
|
|
|
1771
1930
|
json(404, { error: `找不到工作区「${workspaceId ?? "未指定"}」——它可能已经被删除。`, code: "no_workspace" });
|
|
1772
1931
|
return;
|
|
1773
1932
|
}
|
|
1774
|
-
const root = workspace.path;
|
|
1933
|
+
const root = await resolveSessionCwd(workspace.path, url.searchParams.get("cwd"));
|
|
1934
|
+
const cwd = relative(resolve(workspace.path), root).replaceAll("\\", "/");
|
|
1935
|
+
const indexKey = sessionScopeKey(workspaceId, cwd);
|
|
1775
1936
|
if (url.pathname === "/api/files") {
|
|
1776
1937
|
if (request.method !== "GET") {
|
|
1777
1938
|
json(405, { error: "目录列表需要 GET 请求。", code: "method" });
|
|
@@ -1787,7 +1948,7 @@ export function createHttpServer(options) {
|
|
|
1787
1948
|
// report a limit about. `truncated` and `skipped` stay in the response so the protocol
|
|
1788
1949
|
// shape does not change under the client, but they are now constants that say the
|
|
1789
1950
|
// opposite of what they used to.
|
|
1790
|
-
const index = nameIndexFor(context, workspaceId, root);
|
|
1951
|
+
const index = nameIndexFor(context, workspaceId, root, cwd);
|
|
1791
1952
|
// THE WORKSPACE MUST EXIST, AND THE INDEX IS WHAT KNOWS.
|
|
1792
1953
|
//
|
|
1793
1954
|
// The background scan records a root it could not read rather than throwing it - a scan
|
|
@@ -1827,12 +1988,12 @@ export function createHttpServer(options) {
|
|
|
1827
1988
|
const resolved = await resolveInsideRoot(root, rel);
|
|
1828
1989
|
const meta = await fileMeta(resolved);
|
|
1829
1990
|
const path = toRelative(root, resolved);
|
|
1830
|
-
context.fileIndexes?.get(
|
|
1991
|
+
context.fileIndexes?.get(indexKey)?.recordFile(path, meta);
|
|
1831
1992
|
json(200, { path, ...meta });
|
|
1832
1993
|
return;
|
|
1833
1994
|
}
|
|
1834
1995
|
const file = await readWorkspaceFile(root, rel);
|
|
1835
|
-
context.fileIndexes?.get(
|
|
1996
|
+
context.fileIndexes?.get(indexKey)?.recordFile(file.path, file.meta);
|
|
1836
1997
|
json(200, { path: file.path, content: file.content, eol: file.eol, bom: file.bom, meta: file.meta, maxBytes: MAX_FILE_BYTES });
|
|
1837
1998
|
return;
|
|
1838
1999
|
}
|
|
@@ -1867,11 +2028,12 @@ export function createHttpServer(options) {
|
|
|
1867
2028
|
return;
|
|
1868
2029
|
}
|
|
1869
2030
|
const outcome = await writeWorkspaceFile(root, parsed.path, parsed.content, parsed.baseEtag);
|
|
1870
|
-
context.fileIndexes?.get(
|
|
2031
|
+
context.fileIndexes?.get(indexKey)?.recordFile(outcome.path, outcome.meta);
|
|
1871
2032
|
// The name index learns about the save too. A file the user just saved must be findable
|
|
1872
2033
|
// by name immediately, and the index is the thing the search reads - so this is where the
|
|
1873
2034
|
// index stays true without anybody re-scanning the tree.
|
|
1874
|
-
context.nameIndexes
|
|
2035
|
+
if (context.nameIndexes)
|
|
2036
|
+
recordWorkspacePath(context.nameIndexes, workspaceId, workspace.path, await workspacePathFromSession(workspace.path, root, outcome.path));
|
|
1875
2037
|
json(200, outcome);
|
|
1876
2038
|
return;
|
|
1877
2039
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** One model's declared window, as the person's file states it. */
|
|
2
|
+
export type DeclaredWindow = {
|
|
3
|
+
provider: string;
|
|
4
|
+
modelId: string;
|
|
5
|
+
contextWindow: number;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* The windows the person's `models.json` declares, keyed by `provider/modelId`.
|
|
9
|
+
*
|
|
10
|
+
* Pi's file shape is `{ providers: { <providerId>: { models: [{ id, contextWindow, ... }] } } }`,
|
|
11
|
+
* and a model without a `contextWindow` is simply not declared - it does not become a zero, because
|
|
12
|
+
* a zero would be a claim ("this model has no window") rather than an absence.
|
|
13
|
+
*/
|
|
14
|
+
export declare function declaredWindows(modelsJson: unknown): Map<string, number>;
|
|
15
|
+
/**
|
|
16
|
+
* The window to USE for a model, given what the person's file says.
|
|
17
|
+
*
|
|
18
|
+
* `undefined` means "say nothing" - the caller keeps whatever Pi believes. That is the honest
|
|
19
|
+
* answer for a model the person's file does not mention: Super Pi has no opinion about a model
|
|
20
|
+
* somebody else configured, and inventing one would be the same mistake in the other direction.
|
|
21
|
+
*
|
|
22
|
+
* TWO LOOKUPS, AND THE SECOND ONE IS THE ONE THAT MATTERS:
|
|
23
|
+
*
|
|
24
|
+
* 1. `provider/modelId` - the exact pair. This is the common case.
|
|
25
|
+
* 2. `modelId` ALONE, across every provider the person's file declares.
|
|
26
|
+
*
|
|
27
|
+
* The second lookup exists because a CONTEXT WINDOW IS A PROPERTY OF THE MODEL, NOT OF THE
|
|
28
|
+
* ENDPOINT. Measured on this machine: the person's `models.json` declares the model `MiniMax-M3`
|
|
29
|
+
* under the provider id `minimax` (the OpenAI-compatible endpoint) with `contextWindow: 128000`,
|
|
30
|
+
* while Pi's remote catalogue declares the SAME model under a DIFFERENT provider id,
|
|
31
|
+
* `minimax-cn` (the Anthropic-compatible endpoint), with `contextWindow: 1000000`. The session runs
|
|
32
|
+
* on `minimax-cn`, so the exact-pair lookup finds nothing and the bogus number survives.
|
|
33
|
+
*
|
|
34
|
+
* When several providers declare the same model with DIFFERENT windows, the SMALLEST wins: a model
|
|
35
|
+
* can never really have more context than its own limit, and a window that is too large is exactly
|
|
36
|
+
* the failure this exists to prevent - it pushes compaction past the point where the model refuses.
|
|
37
|
+
*/
|
|
38
|
+
export declare function effectiveContextWindow(declared: Map<string, number>, provider: string, modelId: string, reported?: number): number | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Reads the person's `models.json` and returns its declared windows.
|
|
41
|
+
*
|
|
42
|
+
* A missing or unreadable file is an empty map, not a throw: Super Pi must not refuse to start
|
|
43
|
+
* because a person has not configured a custom provider, and the consequence of an empty map is
|
|
44
|
+
* simply "Super Pi has no opinion about this model's window" - which is where it started.
|
|
45
|
+
*/
|
|
46
|
+
export declare function readDeclaredWindows(agentDir: string): Map<string, number>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE CONTEXT WINDOW A MODEL REALLY HAS.
|
|
3
|
+
*
|
|
4
|
+
* Pi reads its model list from two places and one of them wins:
|
|
5
|
+
*
|
|
6
|
+
* - `~/.pi/agent/models.json` - the file a PERSON writes, or that Super Pi writes from its own
|
|
7
|
+
* registry. Its `contextWindow` is a deliberate statement about the model.
|
|
8
|
+
* - `~/.pi/agent/models-store.json` - the REMOTE CATALOGUE Pi fetches from the provider, with
|
|
9
|
+
* an etag and a timestamp. Its `contextWindow` is the PROVIDER'S SELF-REPORT.
|
|
10
|
+
*
|
|
11
|
+
* Pi's own merge lets the remote catalogue overwrite the person's file (`merged[index] = model`),
|
|
12
|
+
* so a provider that reports a generous window wins over the one the person configured.
|
|
13
|
+
*
|
|
14
|
+
* WHY THAT MATTERS, AND HOW IT BREAKS A SESSION:
|
|
15
|
+
*
|
|
16
|
+
* Pi's auto-compaction is `shouldCompact(contextTokens, contextWindow, settings)`, i.e.
|
|
17
|
+
* `contextTokens > contextWindow - reserveTokens`. With `reserveTokens = 16384` and a reported
|
|
18
|
+
* window of 1,000,000, compaction waits for 983,616 tokens. The real window of that model is
|
|
19
|
+
* 128,000. So the model refuses the request at 128K, and the OVERFLOW RECOVERY - which asks the
|
|
20
|
+
* SAME model to summarise the same too-long conversation - fails the same way. The session is
|
|
21
|
+
* then a session that cannot answer anything, and the reader sees a dead conversation.
|
|
22
|
+
*
|
|
23
|
+
* Measured on this machine: `models.json` says `MiniMax-M3` has `contextWindow: 128000`; the
|
|
24
|
+
* remote catalogue says `1000000`; Pi ends up believing 1,000,000.
|
|
25
|
+
*
|
|
26
|
+
* SO: THE PERSON'S FILE WINS. A number somebody typed is a decision; a number a catalogue fetched
|
|
27
|
+
* is a claim, and a claim that is too generous does not fail loudly - it fails by never compacting.
|
|
28
|
+
*
|
|
29
|
+
* This module is deliberately narrow: it reads the person's file, and it says what the model
|
|
30
|
+
* window SHOULD be. It does not touch Pi, does not decide anything else, and is a pure function of
|
|
31
|
+
* two plain objects so it can be tested without a server, a socket, a browser or a model.
|
|
32
|
+
*/
|
|
33
|
+
import { readFileSync } from "node:fs";
|
|
34
|
+
import { join } from "node:path";
|
|
35
|
+
/**
|
|
36
|
+
* The windows the person's `models.json` declares, keyed by `provider/modelId`.
|
|
37
|
+
*
|
|
38
|
+
* Pi's file shape is `{ providers: { <providerId>: { models: [{ id, contextWindow, ... }] } } }`,
|
|
39
|
+
* and a model without a `contextWindow` is simply not declared - it does not become a zero, because
|
|
40
|
+
* a zero would be a claim ("this model has no window") rather than an absence.
|
|
41
|
+
*/
|
|
42
|
+
export function declaredWindows(modelsJson) {
|
|
43
|
+
const out = new Map();
|
|
44
|
+
if (!modelsJson || typeof modelsJson !== "object")
|
|
45
|
+
return out;
|
|
46
|
+
const providers = modelsJson.providers;
|
|
47
|
+
if (!providers || typeof providers !== "object")
|
|
48
|
+
return out;
|
|
49
|
+
for (const [providerId, provider] of Object.entries(providers)) {
|
|
50
|
+
if (!provider || typeof provider !== "object")
|
|
51
|
+
continue;
|
|
52
|
+
const models = provider.models;
|
|
53
|
+
if (!Array.isArray(models))
|
|
54
|
+
continue;
|
|
55
|
+
for (const model of models) {
|
|
56
|
+
if (!model || typeof model !== "object")
|
|
57
|
+
continue;
|
|
58
|
+
const id = model.id;
|
|
59
|
+
const window = model.contextWindow;
|
|
60
|
+
if (typeof id !== "string" || !id.trim())
|
|
61
|
+
continue;
|
|
62
|
+
if (typeof window !== "number" || !Number.isSafeInteger(window) || window <= 0)
|
|
63
|
+
continue;
|
|
64
|
+
out.set(`${providerId}/${id}`, window);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The window to USE for a model, given what the person's file says.
|
|
71
|
+
*
|
|
72
|
+
* `undefined` means "say nothing" - the caller keeps whatever Pi believes. That is the honest
|
|
73
|
+
* answer for a model the person's file does not mention: Super Pi has no opinion about a model
|
|
74
|
+
* somebody else configured, and inventing one would be the same mistake in the other direction.
|
|
75
|
+
*
|
|
76
|
+
* TWO LOOKUPS, AND THE SECOND ONE IS THE ONE THAT MATTERS:
|
|
77
|
+
*
|
|
78
|
+
* 1. `provider/modelId` - the exact pair. This is the common case.
|
|
79
|
+
* 2. `modelId` ALONE, across every provider the person's file declares.
|
|
80
|
+
*
|
|
81
|
+
* The second lookup exists because a CONTEXT WINDOW IS A PROPERTY OF THE MODEL, NOT OF THE
|
|
82
|
+
* ENDPOINT. Measured on this machine: the person's `models.json` declares the model `MiniMax-M3`
|
|
83
|
+
* under the provider id `minimax` (the OpenAI-compatible endpoint) with `contextWindow: 128000`,
|
|
84
|
+
* while Pi's remote catalogue declares the SAME model under a DIFFERENT provider id,
|
|
85
|
+
* `minimax-cn` (the Anthropic-compatible endpoint), with `contextWindow: 1000000`. The session runs
|
|
86
|
+
* on `minimax-cn`, so the exact-pair lookup finds nothing and the bogus number survives.
|
|
87
|
+
*
|
|
88
|
+
* When several providers declare the same model with DIFFERENT windows, the SMALLEST wins: a model
|
|
89
|
+
* can never really have more context than its own limit, and a window that is too large is exactly
|
|
90
|
+
* the failure this exists to prevent - it pushes compaction past the point where the model refuses.
|
|
91
|
+
*/
|
|
92
|
+
export function effectiveContextWindow(declared, provider, modelId, reported) {
|
|
93
|
+
const exact = declared.get(`${provider}/${modelId}`);
|
|
94
|
+
if (exact !== undefined)
|
|
95
|
+
return exact;
|
|
96
|
+
let smallest;
|
|
97
|
+
for (const [key, window] of declared) {
|
|
98
|
+
const slash = key.indexOf("/");
|
|
99
|
+
if (slash < 0 || key.slice(slash + 1) !== modelId)
|
|
100
|
+
continue;
|
|
101
|
+
if (smallest === undefined || window < smallest)
|
|
102
|
+
smallest = window;
|
|
103
|
+
}
|
|
104
|
+
return smallest;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Reads the person's `models.json` and returns its declared windows.
|
|
108
|
+
*
|
|
109
|
+
* A missing or unreadable file is an empty map, not a throw: Super Pi must not refuse to start
|
|
110
|
+
* because a person has not configured a custom provider, and the consequence of an empty map is
|
|
111
|
+
* simply "Super Pi has no opinion about this model's window" - which is where it started.
|
|
112
|
+
*/
|
|
113
|
+
export function readDeclaredWindows(agentDir) {
|
|
114
|
+
try {
|
|
115
|
+
const raw = readFileSync(join(agentDir, "models.json"), "utf8");
|
|
116
|
+
return declaredWindows(JSON.parse(raw));
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return new Map();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -349,6 +349,12 @@ export interface PiSessionLike {
|
|
|
349
349
|
provider?: string;
|
|
350
350
|
id?: string;
|
|
351
351
|
} | undefined;
|
|
352
|
+
/** Pi's estimate for the current context, including its post-compaction unknown state. */
|
|
353
|
+
getContextUsage?(): {
|
|
354
|
+
tokens: number | null;
|
|
355
|
+
contextWindow: number;
|
|
356
|
+
percent: number | null;
|
|
357
|
+
} | undefined;
|
|
352
358
|
/**
|
|
353
359
|
* Switches the model on a LIVE session.
|
|
354
360
|
*
|