@evandu/super-pi 0.1.146 → 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 (62) hide show
  1. package/agents/super-pi-task/core.ts +60 -1
  2. package/agents/super-pi-task/index.ts +4 -0
  3. package/dist/.super-pi-check-inputs.json +2 -2
  4. package/dist/client/.super-pi-client-sources.json +8 -8
  5. package/dist/client/assets/{CodeMirrorEditor-CEwBCVM6.js → CodeMirrorEditor-DQCjEnxG.js} +2 -2
  6. package/dist/client/assets/{dist-DC1IocrS.js → dist-1Innu17E.js} +1 -1
  7. package/dist/client/assets/{dist-CSCUT1PQ.js → dist-1lynFOc5.js} +1 -1
  8. package/dist/client/assets/{dist-Do52MRwm.js → dist-B1qLZVMK.js} +1 -1
  9. package/dist/client/assets/{dist-CAOcjKAQ.js → dist-B4pz_xdd.js} +1 -1
  10. package/dist/client/assets/{dist-B0RXZeXQ.js → dist-B6KwDpeI.js} +1 -1
  11. package/dist/client/assets/{dist-DFMtP3wG.js → dist-BJVI1Unv.js} +1 -1
  12. package/dist/client/assets/{dist-oLq6HXLG.js → dist-BWvzD8Pe.js} +1 -1
  13. package/dist/client/assets/{dist-9F1wL-T5.js → dist-BayWssd0.js} +1 -1
  14. package/dist/client/assets/{dist-DBUqwiWG.js → dist-Bk_pkBVH.js} +1 -1
  15. package/dist/client/assets/{dist-CbVf2Cjn.js → dist-BpeClAv0.js} +1 -1
  16. package/dist/client/assets/{dist-DftD4ibj.js → dist-BsmJg5PX.js} +1 -1
  17. package/dist/client/assets/{dist-CUB_Chdh.js → dist-Bx7s4rni.js} +1 -1
  18. package/dist/client/assets/{dist-Bh8Q7Y_n.js → dist-CDBKM9tL.js} +1 -1
  19. package/dist/client/assets/{dist-BoyICDlL.js → dist-CGzXCzCG.js} +1 -1
  20. package/dist/client/assets/{dist-BCNaDgNW.js → dist-CRxCwn-Z.js} +1 -1
  21. package/dist/client/assets/{dist-76ArZ5M4.js → dist-CTDgUnBG.js} +1 -1
  22. package/dist/client/assets/{dist-BXBSIuTD.js → dist-D1VfMWmf.js} +1 -1
  23. package/dist/client/assets/{dist-DN0GlnnZ.js → dist-DTZLNEci.js} +1 -1
  24. package/dist/client/assets/{dist-JvQIIdqB.js → dist-DuUi29km.js} +1 -1
  25. package/dist/client/assets/{dist-CHt8PX8R.js → dist-F3S0t1oM.js} +1 -1
  26. package/dist/client/assets/{dist-BuR5Iagd.js → dist-QZ-FAVJV.js} +1 -1
  27. package/dist/client/assets/{dist-Bx6ZWYUW.js → dist-u0nBg0fZ.js} +1 -1
  28. package/dist/client/assets/index-BMpqviwU.css +1 -0
  29. package/dist/client/assets/{index-E7yaLa2C.js → index-Dzyp_7sf.js} +43 -43
  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/application/conversation-service.js +6 -0
  34. package/dist/server/application/run-service.d.ts +16 -0
  35. package/dist/server/application/run-service.js +175 -26
  36. package/dist/server/application/session-orchestrator-service.d.ts +103 -1
  37. package/dist/server/application/session-orchestrator-service.js +303 -8
  38. package/dist/server/application/session-watchdog.d.ts +159 -0
  39. package/dist/server/application/session-watchdog.js +244 -0
  40. package/dist/server/http-server.js +136 -76
  41. package/dist/server/index.js +3 -0
  42. package/dist/server/knowledge-vectors.d.ts +15 -0
  43. package/dist/server/knowledge-vectors.js +118 -37
  44. package/dist/server/knowledge.d.ts +7 -8
  45. package/dist/server/knowledge.js +133 -55
  46. package/dist/server/length-continuation.d.ts +50 -0
  47. package/dist/server/length-continuation.js +55 -0
  48. package/dist/server/pi-session-events.js +5 -1
  49. package/dist/server/pi-session-pool.d.ts +2 -0
  50. package/dist/server/pi-session-pool.js +16 -0
  51. package/dist/server/runtime.js +2 -1
  52. package/dist/server/session-tree-scope.d.ts +125 -0
  53. package/dist/server/session-tree-scope.js +337 -0
  54. package/dist/server/store.d.ts +13 -0
  55. package/dist/server/store.js +37 -4
  56. package/dist/server/task-tool.d.ts +1 -1
  57. package/dist/server/task-tool.js +2 -1
  58. package/dist/server/turn-failure.d.ts +26 -0
  59. package/dist/server/turn-failure.js +51 -0
  60. package/dist/server/types.d.ts +15 -1
  61. package/package.json +19 -3
  62. package/dist/client/assets/index-D-OxlUxN.css +0 -1
@@ -0,0 +1,244 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ /** The file name the state lives under, inside the workspace's `.super-pi` directory. */
4
+ export const WATCHDOG_STATE_FILE = "watchdogs.json";
5
+ /** The longest silence a parent may declare: a day. Beyond that it is not a watchdog, it is a plan. */
6
+ export const WATCHDOG_MAX_TIMEOUT_SECONDS = 86_400;
7
+ /** The shortest silence a parent may declare. One second is a test's value, not a parent's. */
8
+ export const WATCHDOG_MIN_TIMEOUT_SECONDS = 1;
9
+ /**
10
+ * The watchdog manager: the timers, the state, and the state file.
11
+ *
12
+ * ONE WATCHDOG PER CHILD. The parent declares a duration for a child, and the duration belongs to that
13
+ * child - not to the pair of sessions in general - because "how long may this child be quiet" is a
14
+ * property of that child, and the parent may well declare different expectations for different children.
15
+ */
16
+ export class SessionWatchdog {
17
+ host;
18
+ entries = new Map();
19
+ timers = new Map();
20
+ /** One write chain per workspace, so two watchdogs in one workspace cannot interleave a write. */
21
+ writes = new Map();
22
+ constructor(host) {
23
+ this.host = host;
24
+ }
25
+ /**
26
+ * Declares (or re-declares) the watchdog for a child.
27
+ *
28
+ * `timeoutSeconds` of 0 CANCELS the watchdog: "I am no longer waiting for a report from this one".
29
+ * That is the only way a parent takes the expectation back, and it is deliberately explicit - a
30
+ * watchdog that quietly switched itself off would leave the parent believing it was still watched.
31
+ */
32
+ async arm(input) {
33
+ const { childConversationId } = input;
34
+ if (!childConversationId.trim())
35
+ return;
36
+ this.clearTimer(childConversationId);
37
+ if (!Number.isFinite(input.timeoutSeconds) || input.timeoutSeconds <= 0) {
38
+ // CANCEL. The entry is removed and the file rewritten, so a cancelled watchdog stays cancelled
39
+ // across a restart.
40
+ if (this.entries.delete(childConversationId))
41
+ await this.persist(input.workspaceId);
42
+ return;
43
+ }
44
+ const timeoutSeconds = Math.min(WATCHDOG_MAX_TIMEOUT_SECONDS, Math.max(WATCHDOG_MIN_TIMEOUT_SECONDS, Math.floor(input.timeoutSeconds)));
45
+ const entry = {
46
+ childConversationId,
47
+ parentConversationId: input.parentConversationId,
48
+ workspaceId: input.workspaceId,
49
+ timeoutSeconds,
50
+ lastSignalAt: Date.now(),
51
+ endedReminded: false,
52
+ };
53
+ this.entries.set(childConversationId, entry);
54
+ await this.persist(input.workspaceId);
55
+ this.schedule(entry);
56
+ }
57
+ /**
58
+ * Records a sign of life: the child reported, so the clock starts again.
59
+ *
60
+ * A REPORT RESETS THE CLOCK, IT DOES NOT CANCEL THE WATCHDOG. The parent asked to hear from this
61
+ * child while the child was working; a report that says "the first stage is done, I am continuing"
62
+ * must not be read as "the work is over". Cancelling on the first report would turn the watchdog into
63
+ * a one-shot device, and the whole point of the parent's declaration is that it holds for as long as
64
+ * the child is out of sight.
65
+ */
66
+ async noteSignal(childConversationId) {
67
+ const entry = this.entries.get(childConversationId);
68
+ if (!entry)
69
+ return;
70
+ entry.lastSignalAt = Date.now();
71
+ entry.endedReminded = false;
72
+ this.clearTimer(childConversationId);
73
+ this.schedule(entry);
74
+ await this.persist(entry.workspaceId);
75
+ }
76
+ /** Takes the watchdog away. The parent is no longer waiting. */
77
+ async cancel(childConversationId) {
78
+ const entry = this.entries.get(childConversationId);
79
+ this.clearTimer(childConversationId);
80
+ if (!entry)
81
+ return;
82
+ this.entries.delete(childConversationId);
83
+ await this.persist(entry.workspaceId);
84
+ }
85
+ /** The watchdogs that are currently armed. */
86
+ list() {
87
+ return [...this.entries.values()].map((entry) => ({ ...entry }));
88
+ }
89
+ /** The watchdog on one child, if there is one. */
90
+ get(childConversationId) {
91
+ const entry = this.entries.get(childConversationId);
92
+ return entry ? { ...entry } : undefined;
93
+ }
94
+ /**
95
+ * Re-arms the watchdogs that were on disk when the server stopped.
96
+ *
97
+ * A watchdog whose silence had ALREADY run out while the server was down fires as soon as it comes
98
+ * back: the parent asked to be told after N minutes of silence, and the silence did happen. The
99
+ * remaining time is measured from the last sign of life, not from the restart.
100
+ */
101
+ async restore() {
102
+ for (const workspaceId of this.host.workspaceIds()) {
103
+ const path = this.statePath(workspaceId);
104
+ if (!path)
105
+ continue;
106
+ let parsed;
107
+ try {
108
+ parsed = JSON.parse(await readFile(path, "utf8"));
109
+ }
110
+ catch {
111
+ // A missing file is the normal case (no watchdog was ever declared). A corrupt one is not
112
+ // fatal either: the worst case of losing it is that the parent stops being reminded, which
113
+ // is the state it was in before this feature existed.
114
+ continue;
115
+ }
116
+ const records = parsed?.watchdogs;
117
+ if (!records || typeof records !== "object")
118
+ continue;
119
+ for (const [childConversationId, record] of Object.entries(records)) {
120
+ if (!record || typeof record.timeoutSeconds !== "number" || !record.parentConversationId)
121
+ continue;
122
+ const entry = {
123
+ childConversationId,
124
+ parentConversationId: record.parentConversationId,
125
+ workspaceId: record.workspaceId ?? workspaceId,
126
+ timeoutSeconds: record.timeoutSeconds,
127
+ lastSignalAt: typeof record.lastSignalAt === "number" ? record.lastSignalAt : Date.now(),
128
+ endedReminded: Boolean(record.endedReminded),
129
+ };
130
+ this.entries.set(childConversationId, entry);
131
+ this.schedule(entry);
132
+ }
133
+ }
134
+ }
135
+ /** Stops every timer. The process is going away; the state stays on disk. */
136
+ dispose() {
137
+ for (const timer of this.timers.values())
138
+ clearTimeout(timer);
139
+ this.timers.clear();
140
+ }
141
+ /**
142
+ * Arms the timer for one entry.
143
+ *
144
+ * THE TIMER IS UNREF'D. A pending reminder must not keep the Node process alive: the server is
145
+ * allowed to shut down with a watchdog in flight, and the state is on disk anyway.
146
+ */
147
+ schedule(entry) {
148
+ this.clearTimer(entry.childConversationId);
149
+ const elapsed = Date.now() - entry.lastSignalAt;
150
+ const remaining = Math.max(0, entry.timeoutSeconds * 1000 - elapsed);
151
+ const timer = setTimeout(() => {
152
+ void this.fire(entry.childConversationId);
153
+ }, remaining);
154
+ timer.unref?.();
155
+ this.timers.set(entry.childConversationId, timer);
156
+ }
157
+ clearTimer(childConversationId) {
158
+ const timer = this.timers.get(childConversationId);
159
+ if (timer)
160
+ clearTimeout(timer);
161
+ this.timers.delete(childConversationId);
162
+ }
163
+ /**
164
+ * The clock ran out.
165
+ *
166
+ * THE CHILD'S STATE DECIDES WHETHER THIS IS A REPEAT, AND THE REPEAT IS NOT LOUDER - IT IS THE
167
+ * SAME. Every interval the parent gets the same reminder, because the parent's need is the same:
168
+ * "I still have no news". An interval that grew would make the watchdog quietest exactly when the
169
+ * child has been silent longest, which is backwards.
170
+ *
171
+ * THE ONE EXCEPTION IS A CHILD THAT IS NO LONGER WORKING. A child whose turn ended without reporting
172
+ * is not going to report: the turn is over. The parent is told once that the child has gone quiet
173
+ * AND stopped, and the watchdog then takes itself off - a reminder that repeats for a child that
174
+ * cannot speak any more is noise, not information.
175
+ */
176
+ async fire(childConversationId) {
177
+ const entry = this.entries.get(childConversationId);
178
+ if (!entry)
179
+ return;
180
+ const childWorking = this.host.isChildWorking(childConversationId);
181
+ if (!childWorking && entry.endedReminded) {
182
+ await this.cancel(childConversationId);
183
+ return;
184
+ }
185
+ const elapsedSeconds = Math.max(0, Math.round((Date.now() - entry.lastSignalAt) / 1000));
186
+ try {
187
+ await this.host.onTimeout({
188
+ childConversationId,
189
+ parentConversationId: entry.parentConversationId,
190
+ timeoutSeconds: entry.timeoutSeconds,
191
+ elapsedSeconds,
192
+ childWorking,
193
+ });
194
+ }
195
+ catch (error) {
196
+ this.host.warn?.(`Super Pi: watchdog reminder for ${childConversationId} failed: ${error instanceof Error ? error.message : String(error)}`);
197
+ }
198
+ entry.endedReminded = !childWorking;
199
+ entry.lastSignalAt = Date.now();
200
+ await this.persist(entry.workspaceId);
201
+ this.schedule(entry);
202
+ }
203
+ /** The path of one workspace's state file, or undefined when the workspace is unknown. */
204
+ statePath(workspaceId) {
205
+ const workspacePath = this.host.workspacePath(workspaceId);
206
+ if (!workspacePath)
207
+ return undefined;
208
+ return join(workspacePath, ".super-pi", WATCHDOG_STATE_FILE);
209
+ }
210
+ /**
211
+ * Writes one workspace's state file.
212
+ *
213
+ * THE WRITES ARE SERIALIZED PER WORKSPACE. Two watchdogs in one workspace each rewrite the same
214
+ * file, and an interleaved pair of writes would leave a file that describes one watchdog twice and
215
+ * loses the other.
216
+ */
217
+ async persist(workspaceId) {
218
+ const path = this.statePath(workspaceId);
219
+ if (!path)
220
+ return;
221
+ const previous = this.writes.get(workspaceId) ?? Promise.resolve();
222
+ const next = previous.then(() => this.writeStateFile(path, workspaceId)).catch((error) => {
223
+ this.host.warn?.(`Super Pi: watchdog state could not be written: ${error instanceof Error ? error.message : String(error)}`);
224
+ });
225
+ this.writes.set(workspaceId, next);
226
+ await next;
227
+ }
228
+ async writeStateFile(path, workspaceId) {
229
+ const watchdogs = {};
230
+ for (const entry of this.entries.values()) {
231
+ if (entry.workspaceId !== workspaceId)
232
+ continue;
233
+ watchdogs[entry.childConversationId] = {
234
+ parentConversationId: entry.parentConversationId,
235
+ workspaceId: entry.workspaceId,
236
+ timeoutSeconds: entry.timeoutSeconds,
237
+ lastSignalAt: entry.lastSignalAt,
238
+ endedReminded: entry.endedReminded,
239
+ };
240
+ }
241
+ await mkdir(join(path, ".."), { recursive: true });
242
+ await writeFile(path, `${JSON.stringify({ version: 1, watchdogs }, null, 2)}\n`, "utf8");
243
+ }
244
+ }
@@ -25,6 +25,7 @@ import { packageUpdateArgs } from "./update.js";
25
25
  import { FileIndex } from "./file-index.js";
26
26
  import { ensureNameIndex, recordWorkspacePath } from "./file-name-index.js";
27
27
  import { resolveSessionCwd, sessionScopeKey, workspacePathFromSession } from "./session-cwd.js";
28
+ import { verifyDirectChildScope, verifyListParentScope, verifyReportTargetScope, verifySpawnParentScope } from "./session-tree-scope.js";
28
29
  import { MAX_CONTENT_DEPTH, MAX_CONTENT_FILE_BYTES, MAX_CONTENT_RESULTS, searchByContent } from "./content-search.js";
29
30
  import { getAvailableSkills, readSkill } from "./skills.js";
30
31
  /**
@@ -1501,11 +1502,26 @@ export function createHttpServer(options) {
1501
1502
  });
1502
1503
  return;
1503
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
+ }
1504
1520
  if (!body.task)
1505
1521
  throw new Error("spawn requires task");
1506
1522
  const result = await service.spawn({
1507
1523
  task: body.task,
1508
- parentConversationId: body.parentConversationId,
1524
+ parentConversationId: body.parentConversationId?.trim() || undefined,
1509
1525
  workspaceId: requestedWorkspaceId,
1510
1526
  title: body.title,
1511
1527
  agentId: body.agentId,
@@ -1514,38 +1530,39 @@ export function createHttpServer(options) {
1514
1530
  initialPrompt: body.initialPrompt,
1515
1531
  wait: body.wait,
1516
1532
  cwd: body.cwd,
1533
+ watchdogTimeoutSeconds: body.watchdogTimeoutSeconds ?? body.watchdog_timeout_seconds ?? body.watchdogSeconds,
1517
1534
  });
1518
1535
  json(200, { ok: true, ...result });
1519
1536
  return;
1520
1537
  }
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
- };
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
+ });
1541
1558
  if (action === "dispatch") {
1542
1559
  if (!body.conversationId)
1543
1560
  throw new Error("dispatch requires conversationId");
1544
1561
  if (!body.content)
1545
1562
  throw new Error("dispatch requires content");
1546
- const scope = verifyTargetScope(body.conversationId);
1563
+ const scope = verifyTargetScope(body.conversationId, "调度");
1547
1564
  if (!scope.ok) {
1548
- json(403, { error: scope.error });
1565
+ json(scope.status, { error: scope.error });
1549
1566
  return;
1550
1567
  }
1551
1568
  const result = await service.dispatch(body.conversationId, body.content, { wait: body.wait });
@@ -1555,9 +1572,9 @@ export function createHttpServer(options) {
1555
1572
  if (action === "status") {
1556
1573
  if (!body.conversationId)
1557
1574
  throw new Error("status requires conversationId");
1558
- const scope = verifyTargetScope(body.conversationId);
1575
+ const scope = verifyTargetScope(body.conversationId, "读取");
1559
1576
  if (!scope.ok) {
1560
- json(403, { error: scope.error });
1577
+ json(scope.status, { error: scope.error });
1561
1578
  return;
1562
1579
  }
1563
1580
  const summary = await service.status(body.conversationId, { includeOutput: body.includeOutput });
@@ -1565,26 +1582,22 @@ export function createHttpServer(options) {
1565
1582
  return;
1566
1583
  }
1567
1584
  if (action === "list") {
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} 不存在或未关联有效工作区,拒绝编排操作。` });
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 });
1574
1596
  return;
1575
1597
  }
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;
1598
+ const effectiveParentId = body.parentConversationId?.trim() || callerConversationId;
1587
1599
  const sessions = await service.list(effectiveParentId);
1600
+ const callerWorkspaceId = context.store.conversation(callerConversationId)?.workspaceId;
1588
1601
  const allowedSessions = sessions.filter((s) => !s.workspaceId || s.workspaceId === callerWorkspaceId);
1589
1602
  json(200, { ok: true, sessions: allowedSessions });
1590
1603
  return;
@@ -1592,9 +1605,9 @@ export function createHttpServer(options) {
1592
1605
  if (action === "stop") {
1593
1606
  if (!body.conversationId)
1594
1607
  throw new Error("stop requires conversationId");
1595
- const scope = verifyTargetScope(body.conversationId);
1608
+ const scope = verifyTargetScope(body.conversationId, "停止");
1596
1609
  if (!scope.ok) {
1597
- json(403, { error: scope.error });
1610
+ json(scope.status, { error: scope.error });
1598
1611
  return;
1599
1612
  }
1600
1613
  const result = await service.stop(body.conversationId, { mode: body.mode, messageId: body.messageId });
@@ -1604,15 +1617,73 @@ export function createHttpServer(options) {
1604
1617
  if (action === "resume") {
1605
1618
  if (!body.conversationId)
1606
1619
  throw new Error("resume requires conversationId");
1607
- const scope = verifyTargetScope(body.conversationId);
1620
+ const scope = verifyTargetScope(body.conversationId, "恢复");
1608
1621
  if (!scope.ok) {
1609
- json(403, { error: scope.error });
1622
+ json(scope.status, { error: scope.error });
1610
1623
  return;
1611
1624
  }
1612
1625
  const result = await service.resume(body.conversationId, body.prompt, { wait: body.wait });
1613
1626
  json(200, { ok: true, ...result });
1614
1627
  return;
1615
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
+ }
1616
1687
  json(400, { error: `不支持的编排操作:${action}` });
1617
1688
  }
1618
1689
  catch (error) {
@@ -1672,20 +1743,15 @@ export function createHttpServer(options) {
1672
1743
  return;
1673
1744
  }
1674
1745
  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
- }));
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 }));
1689
1755
  return;
1690
1756
  }
1691
1757
  try {
@@ -1715,24 +1781,18 @@ export function createHttpServer(options) {
1715
1781
  return;
1716
1782
  }
1717
1783
  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
- }));
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 }));
1725
1793
  return;
1726
1794
  }
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
- }
1795
+ const callerWorkspaceId = store.conversation(callerConversationId)?.workspaceId;
1736
1796
  try {
1737
1797
  const effectiveParentId = parentId || callerConversationId;
1738
1798
  const sessions = await service.list(effectiveParentId);
@@ -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>;