@evandu/super-pi 0.1.145 → 0.1.147
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 +74 -2
- package/agents/super-pi-task/index.ts +7 -0
- package/dist/.super-pi-check-inputs.json +2 -2
- package/dist/client/.super-pi-client-sources.json +11 -11
- package/dist/client/assets/{CodeMirrorEditor-A9KvbIkm.js → CodeMirrorEditor-DQCjEnxG.js} +2 -2
- package/dist/client/assets/{dist-DM4Olwfx.js → dist-1Innu17E.js} +1 -1
- package/dist/client/assets/{dist-bIDce5j-.js → dist-1lynFOc5.js} +1 -1
- package/dist/client/assets/{dist-BlxwDnWr.js → dist-B1qLZVMK.js} +1 -1
- package/dist/client/assets/{dist-BTVbZigv.js → dist-B4pz_xdd.js} +1 -1
- package/dist/client/assets/{dist-UJ46aPS3.js → dist-B6KwDpeI.js} +1 -1
- package/dist/client/assets/{dist-CK2KFFtT.js → dist-BJVI1Unv.js} +1 -1
- package/dist/client/assets/{dist-CRnm1rnO.js → dist-BWvzD8Pe.js} +1 -1
- package/dist/client/assets/{dist-DSHM-PAs.js → dist-BayWssd0.js} +1 -1
- package/dist/client/assets/{dist-B9DE7sc5.js → dist-Bk_pkBVH.js} +1 -1
- package/dist/client/assets/{dist-D-o7iQ8c.js → dist-BpeClAv0.js} +1 -1
- package/dist/client/assets/{dist-CQFASxyb.js → dist-BsmJg5PX.js} +1 -1
- package/dist/client/assets/{dist-Ds5gfeAi.js → dist-Bx7s4rni.js} +1 -1
- package/dist/client/assets/{dist-UPJPMjWv.js → dist-CDBKM9tL.js} +1 -1
- package/dist/client/assets/{dist-CIgjifH-.js → dist-CGzXCzCG.js} +1 -1
- package/dist/client/assets/{dist-BAiV8I_O.js → dist-CRxCwn-Z.js} +1 -1
- package/dist/client/assets/{dist-Br-AuQUG.js → dist-CTDgUnBG.js} +1 -1
- package/dist/client/assets/{dist-4JbFTwg2.js → dist-D1VfMWmf.js} +1 -1
- package/dist/client/assets/{dist-BYOpvW02.js → dist-DTZLNEci.js} +1 -1
- package/dist/client/assets/{dist-CQV5kOCE.js → dist-DuUi29km.js} +1 -1
- package/dist/client/assets/{dist-CjszAoXk.js → dist-F3S0t1oM.js} +1 -1
- package/dist/client/assets/{dist-2boAf_pN.js → dist-QZ-FAVJV.js} +1 -1
- package/dist/client/assets/{dist-B1p4YF9C.js → dist-u0nBg0fZ.js} +1 -1
- package/dist/client/assets/index-BMpqviwU.css +1 -0
- package/dist/client/assets/index-Dzyp_7sf.js +102 -0
- package/dist/client/index.html +2 -2
- package/dist/server/active-memory.d.ts +17 -0
- package/dist/server/active-memory.js +39 -0
- 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 +12 -4
- package/dist/server/application/run-service.d.ts +32 -2
- package/dist/server/application/run-service.js +296 -40
- 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 +114 -3
- package/dist/server/application/session-orchestrator-service.js +406 -35
- package/dist/server/application/session-watchdog.d.ts +159 -0
- package/dist/server/application/session-watchdog.js +244 -0
- 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 +256 -34
- package/dist/server/index.js +3 -0
- package/dist/server/knowledge-vectors.d.ts +15 -0
- package/dist/server/knowledge-vectors.js +118 -37
- package/dist/server/knowledge.d.ts +7 -8
- package/dist/server/knowledge.js +133 -55
- package/dist/server/length-continuation.d.ts +50 -0
- package/dist/server/length-continuation.js +55 -0
- package/dist/server/model-context-window.d.ts +46 -0
- package/dist/server/model-context-window.js +121 -0
- package/dist/server/pi-session-events.js +5 -1
- package/dist/server/pi-session-pool.d.ts +2 -0
- package/dist/server/pi-session-pool.js +16 -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 +23 -7
- 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/session-tree-scope.d.ts +125 -0
- package/dist/server/session-tree-scope.js +337 -0
- package/dist/server/store.d.ts +35 -0
- package/dist/server/store.js +87 -2
- package/dist/server/task-tool.d.ts +2 -1
- package/dist/server/task-tool.js +4 -1
- package/dist/server/terminal.d.ts +1 -0
- package/dist/server/terminal.js +5 -3
- package/dist/server/turn-failure.d.ts +26 -0
- package/dist/server/turn-failure.js +67 -1
- package/dist/server/types.d.ts +33 -1
- 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 +36 -3
- package/dist/client/assets/index-BGsP8pt_.js +0 -100
- package/dist/client/assets/index-CiIc734l.css +0 -1
|
@@ -23,7 +23,9 @@ 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";
|
|
28
|
+
import { verifyDirectChildScope, verifyListParentScope, verifyReportTargetScope, verifySpawnParentScope } from "./session-tree-scope.js";
|
|
27
29
|
import { MAX_CONTENT_DEPTH, MAX_CONTENT_FILE_BYTES, MAX_CONTENT_RESULTS, searchByContent } from "./content-search.js";
|
|
28
30
|
import { getAvailableSkills, readSkill } from "./skills.js";
|
|
29
31
|
/**
|
|
@@ -34,11 +36,11 @@ import { getAvailableSkills, readSkill } from "./skills.js";
|
|
|
34
36
|
* known so far rather than refused. The scan is the server's cost, not the user's - the old
|
|
35
37
|
* implementation made the user pay it on every keystroke.
|
|
36
38
|
*/
|
|
37
|
-
function nameIndexFor(context, workspaceId, root) {
|
|
39
|
+
function nameIndexFor(context, workspaceId, root, cwd = "") {
|
|
38
40
|
const indexes = context.nameIndexes ?? new Map();
|
|
39
41
|
if (!context.nameIndexes)
|
|
40
42
|
context.nameIndexes = indexes;
|
|
41
|
-
return ensureNameIndex(indexes, workspaceId, root);
|
|
43
|
+
return ensureNameIndex(indexes, workspaceId, root, cwd);
|
|
42
44
|
}
|
|
43
45
|
const MIME_TYPES = {
|
|
44
46
|
".css": "text/css; charset=utf-8",
|
|
@@ -312,7 +314,7 @@ export function createHttpServer(options) {
|
|
|
312
314
|
// the endpoint that can spend it is exactly the one an agent's own turn can reach, and the
|
|
313
315
|
// management half (`/api/feishu/config`) stays behind the ordinary token AND the local-address
|
|
314
316
|
// 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)) {
|
|
317
|
+
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
318
|
response.writeHead(401, { "www-authenticate": "Bearer" }).end("Authentication required");
|
|
317
319
|
return;
|
|
318
320
|
}
|
|
@@ -1070,6 +1072,8 @@ export function createHttpServer(options) {
|
|
|
1070
1072
|
json(404, { error: `工作区 ${workspaceId ?? "(missing)"} 不存在`, code: "no_workspace" });
|
|
1071
1073
|
return;
|
|
1072
1074
|
}
|
|
1075
|
+
const sessionRoot = await resolveSessionCwd(workspace.path, url.searchParams.get("cwd"));
|
|
1076
|
+
const cwd = relative(resolve(workspace.path), sessionRoot).replaceAll("\\", "/");
|
|
1073
1077
|
const op = url.searchParams.get("op") ?? "status";
|
|
1074
1078
|
// Blame is a read with a GET shape: every other verb is refused HERE, before the POST branch
|
|
1075
1079
|
// could ever route the op into gitMutate and before any body is read.
|
|
@@ -1095,7 +1099,8 @@ export function createHttpServer(options) {
|
|
|
1095
1099
|
}
|
|
1096
1100
|
const raw = asked.text;
|
|
1097
1101
|
const body = raw ? JSON.parse(raw) : {};
|
|
1098
|
-
|
|
1102
|
+
const path = body.path ? await workspacePathFromSession(workspace.path, sessionRoot, body.path) : undefined;
|
|
1103
|
+
json(200, await gitMutate(workspace.path, op, path, body.message, cwd));
|
|
1099
1104
|
return;
|
|
1100
1105
|
}
|
|
1101
1106
|
if (request.method !== "GET") {
|
|
@@ -1103,16 +1108,16 @@ export function createHttpServer(options) {
|
|
|
1103
1108
|
return;
|
|
1104
1109
|
}
|
|
1105
1110
|
if (op === "status")
|
|
1106
|
-
json(200, await gitStatus(workspace.path));
|
|
1111
|
+
json(200, await gitStatus(workspace.path, cwd));
|
|
1107
1112
|
else if (op === "log")
|
|
1108
|
-
json(200, await gitLog(workspace.path, Number(url.searchParams.get("limit") ?? 20)));
|
|
1113
|
+
json(200, await gitLog(workspace.path, Number(url.searchParams.get("limit") ?? 20), cwd));
|
|
1109
1114
|
else if (op === "diff") {
|
|
1110
1115
|
const path = url.searchParams.get("path");
|
|
1111
1116
|
if (!path) {
|
|
1112
1117
|
json(400, { error: "diff 需要 path", code: "bad_path" });
|
|
1113
1118
|
return;
|
|
1114
1119
|
}
|
|
1115
|
-
json(200, await gitDiff(workspace.path, path, url.searchParams.get("staged") === "1"));
|
|
1120
|
+
json(200, await gitDiff(workspace.path, await workspacePathFromSession(workspace.path, sessionRoot, path), url.searchParams.get("staged") === "1"));
|
|
1116
1121
|
}
|
|
1117
1122
|
else if (op === "blame") {
|
|
1118
1123
|
const path = url.searchParams.get("path");
|
|
@@ -1122,7 +1127,7 @@ export function createHttpServer(options) {
|
|
|
1122
1127
|
}
|
|
1123
1128
|
const startLine = url.searchParams.get("startLine");
|
|
1124
1129
|
const endLine = url.searchParams.get("endLine");
|
|
1125
|
-
json(200, await gitBlame(workspace.path, path, {
|
|
1130
|
+
json(200, await gitBlame(workspace.path, await workspacePathFromSession(workspace.path, sessionRoot, path), {
|
|
1126
1131
|
revision: url.searchParams.get("revision") ?? undefined,
|
|
1127
1132
|
startLine: startLine === null ? undefined : Number(startLine),
|
|
1128
1133
|
endLine: endLine === null ? undefined : Number(endLine),
|
|
@@ -1136,7 +1141,7 @@ export function createHttpServer(options) {
|
|
|
1136
1141
|
const code = error?.code;
|
|
1137
1142
|
if (code === 128 || code === "not_a_repo" || /not a git repository/i.test(message))
|
|
1138
1143
|
json(404, { error: "此工作区不是 git 仓库", code: "not_a_repo" });
|
|
1139
|
-
else if (code === "path")
|
|
1144
|
+
else if (code === "path" || code === "outside_workspace")
|
|
1140
1145
|
json(400, { error: message, code: "bad_path" });
|
|
1141
1146
|
else if (code === "not_found")
|
|
1142
1147
|
json(404, { error: message, code: "not_found" });
|
|
@@ -1480,27 +1485,86 @@ export function createHttpServer(options) {
|
|
|
1480
1485
|
}
|
|
1481
1486
|
const action = (body.action || "").trim();
|
|
1482
1487
|
if (action === "spawn") {
|
|
1488
|
+
// Capability auth above proves who is calling. The target workspace is a separate, explicit
|
|
1489
|
+
// choice: a parentConversationId records a tree edge and never selects that target.
|
|
1490
|
+
const requestedWorkspaceId = body.workspaceId?.trim();
|
|
1491
|
+
if (!requestedWorkspaceId) {
|
|
1492
|
+
json(400, { error: "创建会话必须显式指定 workspaceId;父会话只记录树形关系,不能代替目标工作区。" });
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
const store = context.store;
|
|
1496
|
+
const callerWorkspaceId = store && callerConversationId
|
|
1497
|
+
? store.conversation(callerConversationId)?.workspaceId
|
|
1498
|
+
: undefined;
|
|
1499
|
+
if (requestedWorkspaceId !== callerWorkspaceId) {
|
|
1500
|
+
json(403, {
|
|
1501
|
+
error: `调用方会话 ${callerConversationId} 所在工作区为 ${callerWorkspaceId ?? "未知"},请求的目标工作区 ${requestedWorkspaceId} 不属于调用方,拒绝在该工作区创建会话。请传入调用方自己的工作区。`,
|
|
1502
|
+
});
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
// THE PARENT EDGE IS NOT THE CALLER'S TO CHOOSE. `parentConversationId` is written onto the
|
|
1506
|
+
// new conversation at birth, and the child's result callback follows that edge home. A
|
|
1507
|
+
// caller that named somebody ELSE as the parent would rewrite that session's reporting
|
|
1508
|
+
// line - the victim's result would land in the caller's inbox - so the parent must be the
|
|
1509
|
+
// caller. An empty parent means a top-level session, which is what the user's own "+"
|
|
1510
|
+
// creates and what a session with no owner legitimately is.
|
|
1511
|
+
const parentScope = verifySpawnParentScope({
|
|
1512
|
+
store: context.store,
|
|
1513
|
+
callerConversationId,
|
|
1514
|
+
requestedParentId: body.parentConversationId,
|
|
1515
|
+
});
|
|
1516
|
+
if (!parentScope.ok) {
|
|
1517
|
+
json(parentScope.status, { error: parentScope.error });
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1483
1520
|
if (!body.task)
|
|
1484
1521
|
throw new Error("spawn requires task");
|
|
1485
1522
|
const result = await service.spawn({
|
|
1486
1523
|
task: body.task,
|
|
1487
|
-
parentConversationId: body.parentConversationId,
|
|
1488
|
-
workspaceId:
|
|
1524
|
+
parentConversationId: body.parentConversationId?.trim() || undefined,
|
|
1525
|
+
workspaceId: requestedWorkspaceId,
|
|
1489
1526
|
title: body.title,
|
|
1490
1527
|
agentId: body.agentId,
|
|
1491
1528
|
model: body.model,
|
|
1492
1529
|
thinkingLevel: body.thinkingLevel,
|
|
1493
1530
|
initialPrompt: body.initialPrompt,
|
|
1494
1531
|
wait: body.wait,
|
|
1532
|
+
cwd: body.cwd,
|
|
1533
|
+
watchdogTimeoutSeconds: body.watchdogTimeoutSeconds ?? body.watchdog_timeout_seconds ?? body.watchdogSeconds,
|
|
1495
1534
|
});
|
|
1496
1535
|
json(200, { ok: true, ...result });
|
|
1497
1536
|
return;
|
|
1498
1537
|
}
|
|
1538
|
+
// THE SCOPE RULE, IN ONE PLACE, FOR EVERY ACTION THAT TOUCHES ANOTHER SESSION.
|
|
1539
|
+
//
|
|
1540
|
+
// A session may only manage its DIRECT children: the target's `parentConversationId` must be
|
|
1541
|
+
// the caller itself. Not a grandchild (that is a level away), not a peer (that is somebody
|
|
1542
|
+
// else's branch), not an ancestor (that is above), not a session in another workspace (that
|
|
1543
|
+
// is another department). A workspace is not an organisation chart.
|
|
1544
|
+
//
|
|
1545
|
+
// WHY IT IS ONE LINE. The tree edge lives on the conversation itself, so "is this my child?"
|
|
1546
|
+
// is a comparison, not a walk. The walk up the chain exists only to write a refusal that says
|
|
1547
|
+
// WHY - it never decides anything.
|
|
1548
|
+
//
|
|
1549
|
+
// WHY IT MATTERS. The result callback of a dispatched session goes back to the session that
|
|
1550
|
+
// owns it, so "who gave the order is who gets the report" holds by construction instead of by
|
|
1551
|
+
// a second mechanism that could drift.
|
|
1552
|
+
const verifyTargetScope = (targetConversationId, verb) => verifyDirectChildScope({
|
|
1553
|
+
store: context.store,
|
|
1554
|
+
callerConversationId,
|
|
1555
|
+
targetConversationId,
|
|
1556
|
+
verb,
|
|
1557
|
+
});
|
|
1499
1558
|
if (action === "dispatch") {
|
|
1500
1559
|
if (!body.conversationId)
|
|
1501
1560
|
throw new Error("dispatch requires conversationId");
|
|
1502
1561
|
if (!body.content)
|
|
1503
1562
|
throw new Error("dispatch requires content");
|
|
1563
|
+
const scope = verifyTargetScope(body.conversationId, "调度");
|
|
1564
|
+
if (!scope.ok) {
|
|
1565
|
+
json(scope.status, { error: scope.error });
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1504
1568
|
const result = await service.dispatch(body.conversationId, body.content, { wait: body.wait });
|
|
1505
1569
|
json(200, { ok: true, ...result });
|
|
1506
1570
|
return;
|
|
@@ -1508,18 +1572,44 @@ export function createHttpServer(options) {
|
|
|
1508
1572
|
if (action === "status") {
|
|
1509
1573
|
if (!body.conversationId)
|
|
1510
1574
|
throw new Error("status requires conversationId");
|
|
1575
|
+
const scope = verifyTargetScope(body.conversationId, "读取");
|
|
1576
|
+
if (!scope.ok) {
|
|
1577
|
+
json(scope.status, { error: scope.error });
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1511
1580
|
const summary = await service.status(body.conversationId, { includeOutput: body.includeOutput });
|
|
1512
1581
|
json(200, { ok: true, summary });
|
|
1513
1582
|
return;
|
|
1514
1583
|
}
|
|
1515
1584
|
if (action === "list") {
|
|
1516
|
-
|
|
1517
|
-
|
|
1585
|
+
// LISTING IS SCOPED TOO. The parent whose children are listed must be the caller itself,
|
|
1586
|
+
// so the only sessions on the list are the caller's own direct children. Listing the
|
|
1587
|
+
// children of the caller's own child would be listing grandchildren - the same cross-level
|
|
1588
|
+
// reach every other action refuses.
|
|
1589
|
+
const listScope = verifyListParentScope({
|
|
1590
|
+
store: context.store,
|
|
1591
|
+
callerConversationId,
|
|
1592
|
+
requestedParentId: body.parentConversationId,
|
|
1593
|
+
});
|
|
1594
|
+
if (!listScope.ok) {
|
|
1595
|
+
json(listScope.status, { error: listScope.error });
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
const effectiveParentId = body.parentConversationId?.trim() || callerConversationId;
|
|
1599
|
+
const sessions = await service.list(effectiveParentId);
|
|
1600
|
+
const callerWorkspaceId = context.store.conversation(callerConversationId)?.workspaceId;
|
|
1601
|
+
const allowedSessions = sessions.filter((s) => !s.workspaceId || s.workspaceId === callerWorkspaceId);
|
|
1602
|
+
json(200, { ok: true, sessions: allowedSessions });
|
|
1518
1603
|
return;
|
|
1519
1604
|
}
|
|
1520
1605
|
if (action === "stop") {
|
|
1521
1606
|
if (!body.conversationId)
|
|
1522
1607
|
throw new Error("stop requires conversationId");
|
|
1608
|
+
const scope = verifyTargetScope(body.conversationId, "停止");
|
|
1609
|
+
if (!scope.ok) {
|
|
1610
|
+
json(scope.status, { error: scope.error });
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1523
1613
|
const result = await service.stop(body.conversationId, { mode: body.mode, messageId: body.messageId });
|
|
1524
1614
|
json(200, { ok: true, ...result });
|
|
1525
1615
|
return;
|
|
@@ -1527,10 +1617,73 @@ export function createHttpServer(options) {
|
|
|
1527
1617
|
if (action === "resume") {
|
|
1528
1618
|
if (!body.conversationId)
|
|
1529
1619
|
throw new Error("resume requires conversationId");
|
|
1620
|
+
const scope = verifyTargetScope(body.conversationId, "恢复");
|
|
1621
|
+
if (!scope.ok) {
|
|
1622
|
+
json(scope.status, { error: scope.error });
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1530
1625
|
const result = await service.resume(body.conversationId, body.prompt, { wait: body.wait });
|
|
1531
1626
|
json(200, { ok: true, ...result });
|
|
1532
1627
|
return;
|
|
1533
1628
|
}
|
|
1629
|
+
if (action === "report") {
|
|
1630
|
+
if (!callerConversationId) {
|
|
1631
|
+
json(400, { error: "汇报操作缺少调用方会话标识" });
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
const callerConv = context.store.conversation(callerConversationId);
|
|
1635
|
+
if (!callerConv) {
|
|
1636
|
+
json(400, { error: `找不到会话:${callerConversationId}` });
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
if (!callerConv.parentConversationId) {
|
|
1640
|
+
json(400, { error: `会话 ${callerConversationId} 没有直属父会话,无法汇报` });
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
const targetId = body.targetConversationId ?? callerConv.parentConversationId;
|
|
1644
|
+
const scope = verifyReportTargetScope({
|
|
1645
|
+
store: context.store,
|
|
1646
|
+
callerConversationId,
|
|
1647
|
+
targetConversationId: targetId,
|
|
1648
|
+
});
|
|
1649
|
+
if (!scope.ok) {
|
|
1650
|
+
json(scope.status, { error: scope.error });
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
if (body.content === undefined || body.content === null) {
|
|
1654
|
+
json(400, { error: "report requires content" });
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
const result = await service.report(callerConversationId, {
|
|
1658
|
+
content: String(body.content),
|
|
1659
|
+
status: body.status,
|
|
1660
|
+
});
|
|
1661
|
+
json(200, { ok: true, ...result });
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
if (action === "watchdog") {
|
|
1665
|
+
if (!body.conversationId)
|
|
1666
|
+
throw new Error("watchdog requires conversationId");
|
|
1667
|
+
const scope = verifyTargetScope(body.conversationId, "看护");
|
|
1668
|
+
if (!scope.ok) {
|
|
1669
|
+
json(scope.status, { error: scope.error });
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
const subaction = body.subaction ?? "arm";
|
|
1673
|
+
if (subaction === "cancel") {
|
|
1674
|
+
const result = await service.cancelWatchdog(body.conversationId);
|
|
1675
|
+
json(200, { ...result, ok: true });
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
const timeoutSeconds = Number(body.timeoutSeconds ?? body.timeout_seconds ?? 300);
|
|
1679
|
+
const result = await service.setWatchdog({
|
|
1680
|
+
childConversationId: body.conversationId,
|
|
1681
|
+
parentConversationId: callerConversationId,
|
|
1682
|
+
timeoutSeconds,
|
|
1683
|
+
});
|
|
1684
|
+
json(200, { ...result, ok: true });
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1534
1687
|
json(400, { error: `不支持的编排操作:${action}` });
|
|
1535
1688
|
}
|
|
1536
1689
|
catch (error) {
|
|
@@ -1538,6 +1691,36 @@ export function createHttpServer(options) {
|
|
|
1538
1691
|
}
|
|
1539
1692
|
return;
|
|
1540
1693
|
}
|
|
1694
|
+
if (url.pathname === "/api/conversation/usage") {
|
|
1695
|
+
const json = (status, payload) => {
|
|
1696
|
+
response.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" }).end(JSON.stringify(payload));
|
|
1697
|
+
};
|
|
1698
|
+
if (request.method !== "GET" || !isLocalOrPrivateAddress(remote)) {
|
|
1699
|
+
json(403, { error: "Usage readout requires a local/private GET request." });
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
const callerConversationId = url.searchParams.get("callerConversationId") ?? "";
|
|
1703
|
+
const capability = request.headers["x-super-pi-capability"] ?? url.searchParams.get("capability");
|
|
1704
|
+
const verdict = runService.verifyPrimaryCapability(callerConversationId, capability);
|
|
1705
|
+
if (!verdict.ok) {
|
|
1706
|
+
json(403, { error: verdict.message });
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
const target = url.searchParams.get("conversationId") ?? callerConversationId;
|
|
1710
|
+
if (target !== callerConversationId) {
|
|
1711
|
+
json(403, { error: "Only the caller's own conversation usage may be read." });
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
const conversation = store.conversation(callerConversationId);
|
|
1715
|
+
if (!conversation) {
|
|
1716
|
+
json(403, { error: "Caller conversation is unavailable." });
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
json(200, { ok: true, conversationId: callerConversationId, usage: conversation.usage ?? null,
|
|
1720
|
+
usageCostKnown: conversation.usageCostKnown ?? null, contextUsage: conversation.contextUsage ?? null,
|
|
1721
|
+
usageUpdatedAt: conversation.usageUpdatedAt ?? null });
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1541
1724
|
if (url.pathname === "/api/orchestrator/status" && request.method === "GET") {
|
|
1542
1725
|
if (!isLocalOrPrivateAddress(remote)) {
|
|
1543
1726
|
response.writeHead(403).end();
|
|
@@ -1559,6 +1742,18 @@ export function createHttpServer(options) {
|
|
|
1559
1742
|
response.writeHead(400, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: "缺少 conversationId" }));
|
|
1560
1743
|
return;
|
|
1561
1744
|
}
|
|
1745
|
+
const store = context.store;
|
|
1746
|
+
// THE SAME SCOPE RULE AS THE POST ACTION: the target must be the caller's DIRECT child.
|
|
1747
|
+
const scope = verifyDirectChildScope({
|
|
1748
|
+
store,
|
|
1749
|
+
callerConversationId,
|
|
1750
|
+
targetConversationId: conversationId,
|
|
1751
|
+
verb: "读取",
|
|
1752
|
+
});
|
|
1753
|
+
if (!scope.ok) {
|
|
1754
|
+
response.writeHead(scope.status, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: scope.error }));
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1562
1757
|
try {
|
|
1563
1758
|
const summary = await service.status(conversationId, { includeOutput: url.searchParams.get("includeOutput") === "true" });
|
|
1564
1759
|
response.writeHead(200, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ ok: true, summary }));
|
|
@@ -1585,9 +1780,24 @@ export function createHttpServer(options) {
|
|
|
1585
1780
|
response.writeHead(403, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: verdict.message }));
|
|
1586
1781
|
return;
|
|
1587
1782
|
}
|
|
1783
|
+
const store = context.store;
|
|
1784
|
+
// THE SAME SCOPE RULE AS THE POST ACTION: the parent whose children are listed must be the
|
|
1785
|
+
// caller itself, so only the caller's own direct children can come back.
|
|
1786
|
+
const listScope = verifyListParentScope({
|
|
1787
|
+
store,
|
|
1788
|
+
callerConversationId,
|
|
1789
|
+
requestedParentId: parentId,
|
|
1790
|
+
});
|
|
1791
|
+
if (!listScope.ok) {
|
|
1792
|
+
response.writeHead(listScope.status, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: listScope.error }));
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
const callerWorkspaceId = store.conversation(callerConversationId)?.workspaceId;
|
|
1588
1796
|
try {
|
|
1589
|
-
const
|
|
1590
|
-
|
|
1797
|
+
const effectiveParentId = parentId || callerConversationId;
|
|
1798
|
+
const sessions = await service.list(effectiveParentId);
|
|
1799
|
+
const allowedSessions = sessions.filter((s) => !s.workspaceId || s.workspaceId === callerWorkspaceId);
|
|
1800
|
+
response.writeHead(200, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ ok: true, sessions: allowedSessions }));
|
|
1591
1801
|
}
|
|
1592
1802
|
catch (error) {
|
|
1593
1803
|
response.writeHead(400, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: error instanceof Error ? error.message : "list failed" }));
|
|
@@ -1644,16 +1854,20 @@ export function createHttpServer(options) {
|
|
|
1644
1854
|
json(400, { error: "需要 path 参数", code: "bad_request" });
|
|
1645
1855
|
return;
|
|
1646
1856
|
}
|
|
1857
|
+
const root = await resolveSessionCwd(workspace.path, url.searchParams.get("cwd"));
|
|
1858
|
+
const workspaceRelativePath = await workspacePathFromSession(workspace.path, root, path);
|
|
1647
1859
|
const requested = url.searchParams.get("base")?.trim();
|
|
1648
1860
|
const preferred = requested === "git" || requested === "agent" ? requested : "auto";
|
|
1649
1861
|
// The baseline is chosen by the server, but the client can name one - and when it names one, that
|
|
1650
1862
|
// is the one it gets. A silent fall-back would show the user a comparison they did not ask for.
|
|
1651
1863
|
const diff = await computeFileDiff({
|
|
1652
|
-
root
|
|
1864
|
+
root,
|
|
1653
1865
|
path,
|
|
1654
1866
|
preferred,
|
|
1655
1867
|
tracker: context.fileChanges,
|
|
1656
1868
|
workspaceId,
|
|
1869
|
+
gitRoot: workspace.path,
|
|
1870
|
+
trackerPath: workspaceRelativePath,
|
|
1657
1871
|
});
|
|
1658
1872
|
json(200, diff);
|
|
1659
1873
|
}
|
|
@@ -1677,7 +1891,9 @@ export function createHttpServer(options) {
|
|
|
1677
1891
|
json(404, { error: "找不到这个工作区——它可能已经被删除。", code: "no_workspace" });
|
|
1678
1892
|
return;
|
|
1679
1893
|
}
|
|
1680
|
-
await
|
|
1894
|
+
const root = await resolveSessionCwd(workspace.path, url.searchParams.get("cwd"));
|
|
1895
|
+
const cwd = relative(resolve(workspace.path), root).replaceAll("\\", "/");
|
|
1896
|
+
const indexKey = sessionScopeKey(workspaceId, cwd);
|
|
1681
1897
|
if (query.length > 256 || /[\r\n]/.test(query)) {
|
|
1682
1898
|
json(400, { error: "搜索词只能是一行,且不超过 256 个字符。", code: "bad_query" });
|
|
1683
1899
|
return;
|
|
@@ -1689,17 +1905,17 @@ export function createHttpServer(options) {
|
|
|
1689
1905
|
const indexes = context.fileIndexes ?? new Map();
|
|
1690
1906
|
if (!context.fileIndexes)
|
|
1691
1907
|
context.fileIndexes = indexes;
|
|
1692
|
-
let index = indexes.get(
|
|
1908
|
+
let index = indexes.get(indexKey);
|
|
1693
1909
|
if (!index) {
|
|
1694
|
-
index = new FileIndex(workspaceId, (event) => broadcaster.broadcast(event));
|
|
1695
|
-
indexes.set(
|
|
1910
|
+
index = new FileIndex(workspaceId, (event) => broadcaster.broadcast(event), cwd);
|
|
1911
|
+
indexes.set(indexKey, index);
|
|
1696
1912
|
}
|
|
1697
|
-
index.start(
|
|
1913
|
+
index.start(root);
|
|
1698
1914
|
const deadline = Date.now() + 4500;
|
|
1699
1915
|
while (!index.builtAt && Date.now() < deadline)
|
|
1700
1916
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1701
1917
|
const snapshot = index.snapshot();
|
|
1702
|
-
const result = await searchByContent(
|
|
1918
|
+
const result = await searchByContent(root, [...index.entries.values()], query, snapshot);
|
|
1703
1919
|
json(200, { mode: "content", query, ...result, maxResults: MAX_CONTENT_RESULTS, maxFileBytes: MAX_CONTENT_FILE_BYTES, maxDepth: MAX_CONTENT_DEPTH });
|
|
1704
1920
|
}
|
|
1705
1921
|
catch (error) {
|
|
@@ -1738,18 +1954,21 @@ export function createHttpServer(options) {
|
|
|
1738
1954
|
json(404, { error: `找不到工作区「${workspaceId ?? "未指定"}」——它可能已经被删除。`, code: "no_workspace" });
|
|
1739
1955
|
return;
|
|
1740
1956
|
}
|
|
1957
|
+
const root = await resolveSessionCwd(workspace.path, url.searchParams.get("cwd"));
|
|
1958
|
+
const cwd = relative(resolve(workspace.path), root).replaceAll("\\", "/");
|
|
1959
|
+
const indexKey = sessionScopeKey(workspaceId, cwd);
|
|
1741
1960
|
const indexes = context.fileIndexes ?? new Map();
|
|
1742
1961
|
if (!context.fileIndexes)
|
|
1743
1962
|
context.fileIndexes = indexes;
|
|
1744
|
-
let index = indexes.get(
|
|
1963
|
+
let index = indexes.get(indexKey);
|
|
1745
1964
|
if (!index) {
|
|
1746
|
-
index = new FileIndex(workspaceId, (event) => broadcaster.broadcast(event));
|
|
1747
|
-
indexes.set(
|
|
1965
|
+
index = new FileIndex(workspaceId, (event) => broadcaster.broadcast(event), cwd);
|
|
1966
|
+
indexes.set(indexKey, index);
|
|
1748
1967
|
}
|
|
1749
|
-
index.start(
|
|
1968
|
+
index.start(root);
|
|
1750
1969
|
// Quick Open requests the full index on demand. Name search can share its background scan;
|
|
1751
1970
|
// merely selecting a session no longer starts a multi-MB download.
|
|
1752
|
-
nameIndexFor(context, workspaceId,
|
|
1971
|
+
nameIndexFor(context, workspaceId, root, cwd);
|
|
1753
1972
|
const sinceRaw = url.searchParams.get("since");
|
|
1754
1973
|
const since = sinceRaw === null ? undefined : Number(sinceRaw);
|
|
1755
1974
|
json(200, index.snapshot(Number.isSafeInteger(since) && since >= 0 ? since : undefined));
|
|
@@ -1771,7 +1990,9 @@ export function createHttpServer(options) {
|
|
|
1771
1990
|
json(404, { error: `找不到工作区「${workspaceId ?? "未指定"}」——它可能已经被删除。`, code: "no_workspace" });
|
|
1772
1991
|
return;
|
|
1773
1992
|
}
|
|
1774
|
-
const root = workspace.path;
|
|
1993
|
+
const root = await resolveSessionCwd(workspace.path, url.searchParams.get("cwd"));
|
|
1994
|
+
const cwd = relative(resolve(workspace.path), root).replaceAll("\\", "/");
|
|
1995
|
+
const indexKey = sessionScopeKey(workspaceId, cwd);
|
|
1775
1996
|
if (url.pathname === "/api/files") {
|
|
1776
1997
|
if (request.method !== "GET") {
|
|
1777
1998
|
json(405, { error: "目录列表需要 GET 请求。", code: "method" });
|
|
@@ -1787,7 +2008,7 @@ export function createHttpServer(options) {
|
|
|
1787
2008
|
// report a limit about. `truncated` and `skipped` stay in the response so the protocol
|
|
1788
2009
|
// shape does not change under the client, but they are now constants that say the
|
|
1789
2010
|
// opposite of what they used to.
|
|
1790
|
-
const index = nameIndexFor(context, workspaceId, root);
|
|
2011
|
+
const index = nameIndexFor(context, workspaceId, root, cwd);
|
|
1791
2012
|
// THE WORKSPACE MUST EXIST, AND THE INDEX IS WHAT KNOWS.
|
|
1792
2013
|
//
|
|
1793
2014
|
// The background scan records a root it could not read rather than throwing it - a scan
|
|
@@ -1827,12 +2048,12 @@ export function createHttpServer(options) {
|
|
|
1827
2048
|
const resolved = await resolveInsideRoot(root, rel);
|
|
1828
2049
|
const meta = await fileMeta(resolved);
|
|
1829
2050
|
const path = toRelative(root, resolved);
|
|
1830
|
-
context.fileIndexes?.get(
|
|
2051
|
+
context.fileIndexes?.get(indexKey)?.recordFile(path, meta);
|
|
1831
2052
|
json(200, { path, ...meta });
|
|
1832
2053
|
return;
|
|
1833
2054
|
}
|
|
1834
2055
|
const file = await readWorkspaceFile(root, rel);
|
|
1835
|
-
context.fileIndexes?.get(
|
|
2056
|
+
context.fileIndexes?.get(indexKey)?.recordFile(file.path, file.meta);
|
|
1836
2057
|
json(200, { path: file.path, content: file.content, eol: file.eol, bom: file.bom, meta: file.meta, maxBytes: MAX_FILE_BYTES });
|
|
1837
2058
|
return;
|
|
1838
2059
|
}
|
|
@@ -1867,11 +2088,12 @@ export function createHttpServer(options) {
|
|
|
1867
2088
|
return;
|
|
1868
2089
|
}
|
|
1869
2090
|
const outcome = await writeWorkspaceFile(root, parsed.path, parsed.content, parsed.baseEtag);
|
|
1870
|
-
context.fileIndexes?.get(
|
|
2091
|
+
context.fileIndexes?.get(indexKey)?.recordFile(outcome.path, outcome.meta);
|
|
1871
2092
|
// The name index learns about the save too. A file the user just saved must be findable
|
|
1872
2093
|
// by name immediately, and the index is the thing the search reads - so this is where the
|
|
1873
2094
|
// index stays true without anybody re-scanning the tree.
|
|
1874
|
-
context.nameIndexes
|
|
2095
|
+
if (context.nameIndexes)
|
|
2096
|
+
recordWorkspacePath(context.nameIndexes, workspaceId, workspace.path, await workspacePathFromSession(workspace.path, root, outcome.path));
|
|
1875
2097
|
json(200, outcome);
|
|
1876
2098
|
return;
|
|
1877
2099
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -291,6 +291,9 @@ runService.setExecService(execService);
|
|
|
291
291
|
const scheduledTaskService = new ScheduledTaskService(store, runService, conversationService, broadcaster);
|
|
292
292
|
const sessionLedger = new SessionLedger();
|
|
293
293
|
const sessionOrchestratorService = new SessionOrchestratorService(store, conversationService, runService, sessionLedger, broadcaster);
|
|
294
|
+
void sessionOrchestratorService.restoreWatchdogs().catch((error) => {
|
|
295
|
+
console.warn("Super Pi: restoring watchdogs failed:", error instanceof Error ? error.message : String(error));
|
|
296
|
+
});
|
|
294
297
|
const serverContext = {
|
|
295
298
|
memory,
|
|
296
299
|
knowledge,
|
|
@@ -21,6 +21,8 @@ export declare const KNOWLEDGE_MODEL = "Xenova/bge-small-zh-v1.5";
|
|
|
21
21
|
* is written in.
|
|
22
22
|
*/
|
|
23
23
|
export declare const EMBED_PIECE_CHARS = 300;
|
|
24
|
+
/** Per query. Cosine similarity of unit vectors; threshold raised to 0.35 to filter out semantic noise. */
|
|
25
|
+
export declare const MIN_COSINE = 0.35;
|
|
24
26
|
export type VectorRecord = {
|
|
25
27
|
chunk: number;
|
|
26
28
|
values: number[];
|
|
@@ -32,6 +34,8 @@ export type VectorStatus = {
|
|
|
32
34
|
indexed: number;
|
|
33
35
|
error?: string;
|
|
34
36
|
bundled?: boolean;
|
|
37
|
+
health?: "ready" | "degraded" | "restarting" | "disabled";
|
|
38
|
+
available?: boolean;
|
|
35
39
|
};
|
|
36
40
|
/**
|
|
37
41
|
* Splits a retrieval chunk into pieces small enough for the model's window.
|
|
@@ -64,6 +68,11 @@ export declare class VectorIndex {
|
|
|
64
68
|
private workerReady;
|
|
65
69
|
private workerPending;
|
|
66
70
|
private failure?;
|
|
71
|
+
private consecutiveFailures;
|
|
72
|
+
private lastFailureTime;
|
|
73
|
+
private static readonly MAX_FAILURES;
|
|
74
|
+
private static readonly COOLDOWN_MS;
|
|
75
|
+
private static readonly EMBED_TIMEOUT_MS;
|
|
67
76
|
private cache;
|
|
68
77
|
private pending;
|
|
69
78
|
private queue;
|
|
@@ -92,9 +101,15 @@ export declare class VectorIndex {
|
|
|
92
101
|
* able to reach, on the first search, for every person who installed the package. Shipping the
|
|
93
102
|
* weights turns a failure mode into a file, and makes the feature work on a machine that has never
|
|
94
103
|
* been online since it was installed.
|
|
104
|
+
*
|
|
105
|
+
* RESILIENT WORKER LIFECYCLE:
|
|
106
|
+
* When the worker encounters an error, crash, or unexpected exit, it cleans up all pending requests
|
|
107
|
+
* without hanging and marks the instance for automatic, on-demand respawning on the next embed call.
|
|
108
|
+
* A bounded backoff / cooldown prevents runaway crash loops if the environment is broken.
|
|
95
109
|
*/
|
|
96
110
|
private ensureWorker;
|
|
97
111
|
private embed;
|
|
112
|
+
terminate(): Promise<void>;
|
|
98
113
|
/** Vectors already stored for one workspace, keyed by document then chunk index. */
|
|
99
114
|
private read;
|
|
100
115
|
count(workspaceId: string): Promise<number>;
|