@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.
Files changed (93) hide show
  1. package/agents/super-pi-task/core.ts +74 -2
  2. package/agents/super-pi-task/index.ts +7 -0
  3. package/dist/.super-pi-check-inputs.json +2 -2
  4. package/dist/client/.super-pi-client-sources.json +11 -11
  5. package/dist/client/assets/{CodeMirrorEditor-A9KvbIkm.js → CodeMirrorEditor-DQCjEnxG.js} +2 -2
  6. package/dist/client/assets/{dist-DM4Olwfx.js → dist-1Innu17E.js} +1 -1
  7. package/dist/client/assets/{dist-bIDce5j-.js → dist-1lynFOc5.js} +1 -1
  8. package/dist/client/assets/{dist-BlxwDnWr.js → dist-B1qLZVMK.js} +1 -1
  9. package/dist/client/assets/{dist-BTVbZigv.js → dist-B4pz_xdd.js} +1 -1
  10. package/dist/client/assets/{dist-UJ46aPS3.js → dist-B6KwDpeI.js} +1 -1
  11. package/dist/client/assets/{dist-CK2KFFtT.js → dist-BJVI1Unv.js} +1 -1
  12. package/dist/client/assets/{dist-CRnm1rnO.js → dist-BWvzD8Pe.js} +1 -1
  13. package/dist/client/assets/{dist-DSHM-PAs.js → dist-BayWssd0.js} +1 -1
  14. package/dist/client/assets/{dist-B9DE7sc5.js → dist-Bk_pkBVH.js} +1 -1
  15. package/dist/client/assets/{dist-D-o7iQ8c.js → dist-BpeClAv0.js} +1 -1
  16. package/dist/client/assets/{dist-CQFASxyb.js → dist-BsmJg5PX.js} +1 -1
  17. package/dist/client/assets/{dist-Ds5gfeAi.js → dist-Bx7s4rni.js} +1 -1
  18. package/dist/client/assets/{dist-UPJPMjWv.js → dist-CDBKM9tL.js} +1 -1
  19. package/dist/client/assets/{dist-CIgjifH-.js → dist-CGzXCzCG.js} +1 -1
  20. package/dist/client/assets/{dist-BAiV8I_O.js → dist-CRxCwn-Z.js} +1 -1
  21. package/dist/client/assets/{dist-Br-AuQUG.js → dist-CTDgUnBG.js} +1 -1
  22. package/dist/client/assets/{dist-4JbFTwg2.js → dist-D1VfMWmf.js} +1 -1
  23. package/dist/client/assets/{dist-BYOpvW02.js → dist-DTZLNEci.js} +1 -1
  24. package/dist/client/assets/{dist-CQV5kOCE.js → dist-DuUi29km.js} +1 -1
  25. package/dist/client/assets/{dist-CjszAoXk.js → dist-F3S0t1oM.js} +1 -1
  26. package/dist/client/assets/{dist-2boAf_pN.js → dist-QZ-FAVJV.js} +1 -1
  27. package/dist/client/assets/{dist-B1p4YF9C.js → dist-u0nBg0fZ.js} +1 -1
  28. package/dist/client/assets/index-BMpqviwU.css +1 -0
  29. package/dist/client/assets/index-Dzyp_7sf.js +102 -0
  30. package/dist/client/index.html +2 -2
  31. package/dist/server/active-memory.d.ts +17 -0
  32. package/dist/server/active-memory.js +39 -0
  33. package/dist/server/agents.d.ts +1 -1
  34. package/dist/server/agents.js +3 -1
  35. package/dist/server/application/conversation-service.d.ts +1 -1
  36. package/dist/server/application/conversation-service.js +12 -4
  37. package/dist/server/application/run-service.d.ts +32 -2
  38. package/dist/server/application/run-service.js +296 -40
  39. package/dist/server/application/scheduled-task-service.d.ts +14 -1
  40. package/dist/server/application/scheduled-task-service.js +27 -2
  41. package/dist/server/application/session-orchestrator-service.d.ts +114 -3
  42. package/dist/server/application/session-orchestrator-service.js +406 -35
  43. package/dist/server/application/session-watchdog.d.ts +159 -0
  44. package/dist/server/application/session-watchdog.js +244 -0
  45. package/dist/server/command-handler.js +1 -1
  46. package/dist/server/conversation-usage.d.ts +19 -0
  47. package/dist/server/conversation-usage.js +40 -0
  48. package/dist/server/file-diff.d.ts +3 -0
  49. package/dist/server/file-diff.js +4 -4
  50. package/dist/server/file-index.d.ts +4 -2
  51. package/dist/server/file-index.js +14 -9
  52. package/dist/server/file-name-index.d.ts +5 -2
  53. package/dist/server/file-name-index.js +22 -5
  54. package/dist/server/git.d.ts +3 -3
  55. package/dist/server/git.js +10 -9
  56. package/dist/server/http-server.js +256 -34
  57. package/dist/server/index.js +3 -0
  58. package/dist/server/knowledge-vectors.d.ts +15 -0
  59. package/dist/server/knowledge-vectors.js +118 -37
  60. package/dist/server/knowledge.d.ts +7 -8
  61. package/dist/server/knowledge.js +133 -55
  62. package/dist/server/length-continuation.d.ts +50 -0
  63. package/dist/server/length-continuation.js +55 -0
  64. package/dist/server/model-context-window.d.ts +46 -0
  65. package/dist/server/model-context-window.js +121 -0
  66. package/dist/server/pi-session-events.js +5 -1
  67. package/dist/server/pi-session-pool.d.ts +2 -0
  68. package/dist/server/pi-session-pool.js +16 -0
  69. package/dist/server/pi-session.d.ts +6 -0
  70. package/dist/server/pi-session.js +15 -1
  71. package/dist/server/runtime.d.ts +3 -0
  72. package/dist/server/runtime.js +23 -7
  73. package/dist/server/session-cwd.d.ts +17 -0
  74. package/dist/server/session-cwd.js +43 -0
  75. package/dist/server/session-ledger.d.ts +7 -0
  76. package/dist/server/session-tree-scope.d.ts +125 -0
  77. package/dist/server/session-tree-scope.js +337 -0
  78. package/dist/server/store.d.ts +35 -0
  79. package/dist/server/store.js +87 -2
  80. package/dist/server/task-tool.d.ts +2 -1
  81. package/dist/server/task-tool.js +4 -1
  82. package/dist/server/terminal.d.ts +1 -0
  83. package/dist/server/terminal.js +5 -3
  84. package/dist/server/turn-failure.d.ts +26 -0
  85. package/dist/server/turn-failure.js +67 -1
  86. package/dist/server/types.d.ts +33 -1
  87. package/dist/server/version.d.ts +25 -2
  88. package/dist/server/version.js +98 -26
  89. package/dist/server/ws-server.d.ts +1 -1
  90. package/dist/server/ws-server.js +7 -5
  91. package/package.json +36 -3
  92. package/dist/client/assets/index-BGsP8pt_.js +0 -100
  93. 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
- json(200, await gitMutate(workspace.path, op, body.path, body.message));
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: body.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
- const sessions = await service.list(body.parentConversationId);
1517
- json(200, { ok: true, sessions });
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 sessions = await service.list(parentId);
1590
- response.writeHead(200, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ ok: true, sessions }));
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: workspace.path,
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 resolveInsideRoot(workspace.path, "");
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(workspaceId);
1908
+ let index = indexes.get(indexKey);
1693
1909
  if (!index) {
1694
- index = new FileIndex(workspaceId, (event) => broadcaster.broadcast(event));
1695
- indexes.set(workspaceId, index);
1910
+ index = new FileIndex(workspaceId, (event) => broadcaster.broadcast(event), cwd);
1911
+ indexes.set(indexKey, index);
1696
1912
  }
1697
- index.start(workspace.path);
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(workspace.path, [...index.entries.values()], query, snapshot);
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(workspaceId);
1963
+ let index = indexes.get(indexKey);
1745
1964
  if (!index) {
1746
- index = new FileIndex(workspaceId, (event) => broadcaster.broadcast(event));
1747
- indexes.set(workspaceId, index);
1965
+ index = new FileIndex(workspaceId, (event) => broadcaster.broadcast(event), cwd);
1966
+ indexes.set(indexKey, index);
1748
1967
  }
1749
- index.start(workspace.path);
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, workspace.path);
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(workspaceId)?.recordFile(path, meta);
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(workspaceId)?.recordFile(file.path, file.meta);
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(workspaceId)?.recordFile(outcome.path, outcome.meta);
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?.get(workspaceId)?.upsert(outcome.path);
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
  }
@@ -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>;