@evandu/super-pi 0.1.163 → 0.1.165

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 (38) hide show
  1. package/agents/super-pi-task/core.ts +1 -1
  2. package/dist/client/.super-pi-client-sources.json +4 -4
  3. package/dist/client/assets/{CodeMirrorEditor-DEkvKQts.js → CodeMirrorEditor-B3-4Qpd1.js} +2 -2
  4. package/dist/client/assets/{dist-CNrwgb43.js → dist-2Ax3GOhT.js} +1 -1
  5. package/dist/client/assets/{dist-DYahm0uS.js → dist-BD4w6y25.js} +1 -1
  6. package/dist/client/assets/{dist-D5nqZFe3.js → dist-BIqjsoet.js} +1 -1
  7. package/dist/client/assets/{dist-a40vvieq.js → dist-BWsed8P8.js} +1 -1
  8. package/dist/client/assets/{dist-D5doZ1C8.js → dist-Bh9jVMEA.js} +1 -1
  9. package/dist/client/assets/{dist-BtNbcR8w.js → dist-BhiW2HYK.js} +1 -1
  10. package/dist/client/assets/{dist-Dgkmmss0.js → dist-C2mhG_5m.js} +1 -1
  11. package/dist/client/assets/{dist-CbFClRtF.js → dist-CL9_Ff9R.js} +1 -1
  12. package/dist/client/assets/{dist-B-8UFI-6.js → dist-CN6rdrJA.js} +1 -1
  13. package/dist/client/assets/{dist-DwUj9On7.js → dist-Cn4Q7-eG.js} +1 -1
  14. package/dist/client/assets/{dist-GFhYndkm.js → dist-D45Y5G5v.js} +1 -1
  15. package/dist/client/assets/{dist-CVljZ_cT.js → dist-DF05sEDO.js} +1 -1
  16. package/dist/client/assets/{dist-DpVnCvxg.js → dist-DGOgW2hA.js} +1 -1
  17. package/dist/client/assets/{dist-C2Uv2gLb.js → dist-DQnuHIqA.js} +1 -1
  18. package/dist/client/assets/{dist-DC-1StQ4.js → dist-DRgOr5sP.js} +1 -1
  19. package/dist/client/assets/{dist-CSiW1PcF.js → dist-Di8I1Dsi.js} +1 -1
  20. package/dist/client/assets/{dist-C2mmiivD.js → dist-Do72hsvz.js} +1 -1
  21. package/dist/client/assets/{dist-OOixZci6.js → dist-DtNwDyK1.js} +1 -1
  22. package/dist/client/assets/{dist-FPDQhYJa.js → dist-QTqWnNGE.js} +1 -1
  23. package/dist/client/assets/{dist-DywKNYXP.js → dist-hUAwe2Kn.js} +1 -1
  24. package/dist/client/assets/{dist-CipOPceB.js → dist-rFG_iRW3.js} +1 -1
  25. package/dist/client/assets/{dist-ChSDUOFD.js → dist-wW-2h0X5.js} +1 -1
  26. package/dist/client/assets/{index-CLV7t20O.js → index-BUTxkA3D.js} +11 -11
  27. package/dist/client/index.html +1 -1
  28. package/dist/server/application/project-service.d.ts +95 -4
  29. package/dist/server/application/project-service.js +341 -72
  30. package/dist/server/application/run-service.d.ts +3 -0
  31. package/dist/server/application/run-service.js +27 -7
  32. package/dist/server/application/session-watchdog.d.ts +14 -2
  33. package/dist/server/application/session-watchdog.js +42 -2
  34. package/dist/server/http-server.js +7 -0
  35. package/dist/server/index.js +8 -1
  36. package/dist/server/tool-runner.js +11 -2
  37. package/dist/server/types.d.ts +4 -0
  38. package/package.json +3 -1
@@ -4,13 +4,22 @@ const stamp = () => new Date().toISOString();
4
4
  * WHAT THE PROJECT LEADER IS TOLD WHEN IT STARTS.
5
5
  *
6
6
  * The two prompts are kept apart on purpose. The standard one is the shape the project flow has
7
- * always had - decompose, delegate, review - and it is not touched. The lightweight one is a
8
- * different set of instructions about a different shape: one session, no decomposition, no
9
- * reviewer. Writing one prompt with holes in it would mean every sentence had to carry a
10
- * condition, and the model would have to work out which shape it was in.
7
+ * always had - decompose, delegate, review. The lightweight one is a different set of
8
+ * instructions about a different shape: one session, no decomposition, no reviewer. Writing one
9
+ * prompt with holes in it would mean every sentence had to carry a condition, and the model would
10
+ * have to work out which shape it was in.
11
+ *
12
+ * THE ESCALATION RULE IS SAID ONCE, IN ONE VOICE. The standard leader used to be told two
13
+ * opposite things about the same event - "your subordinates' escalations come to you first, you
14
+ * decide whether to handle them yourself or report them up" AND "you must not resolve escalations
15
+ * yourself" - and a model reading both has no way to pick, so it hesitates between them. The
16
+ * rule is now one sentence with three parts: handle it at your own level, escalate only what
17
+ * really needs the Boss, and never decide for the Boss or drop an escalation. The lightweight
18
+ * leader has no subordinates at all, so it is told the second half only - it must not decide
19
+ * alone either - and is never told about subordinates it does not have.
11
20
  */
12
- const STANDARD_LEADER_PROMPT = "你是直接向 Boss 负责的项目 Leader,兼任秘书。先调用 project(action=get, projectId) 了解最新状态。小项目只拆最少必要任务,大项目按交付边界分工。用 add_task 登记任务(明确目标、交付物、验收标准、依赖);add_task / update_task 时可以带 executorProfile 与 reviewerProfile(支持指定 agentId、cwd、model、thinkingLevel、watchdogTimeoutSeconds),按任务性质将执行与复核委派给最合适的员工,不指定则继承项目默认;需要继续拆解的工作设 role=leader,叶子执行任务设 role=executor,子 Leader 用 parentTaskId 登记自己的子任务,dispatch 启动执行子会话;不要用 session_spawn/session_dispatch 或 task/workflow 绕过项目资源限制。收到交付后用 request_review 启动独立复核。子会话结束会自动回报给你,届时继续派发 ready 任务、复核待验收任务或按 review.summary 返工。不要轮询等待,不要反复无进展重试,并发满时结束当前回复等待回报。用 report 登记关键进展,用 escalate 将重大偏差、权限或预算问题交给 Boss,不得自行解决升级事项。所有任务验收后,用 complete 提交带证据的最终交付总结。上下文有限,明确假设,主动寻找反证。只在本次触发范围内工作。";
13
- const LIGHTWEIGHT_LEADER_PROMPT = "你是直接向 Boss 负责的项目 Leader,这是一个轻量项目:这个项目由你亲自完成,不要拆子任务、不要派发给别人、也没有独立复核。先调用 project(action=get, projectId) 了解最新状态。用 add_task 登记任务(明确目标、交付物、验收标准、依赖),任务一律登记为执行任务:role=leader 或带 parentTaskId 都会被拒绝。dispatch 会把任务交回给你自己,你亲自做完;做完后调用 project(action=submit, projectId, taskId, output, evidenceRefs) 提交交付,提交即完成,不会有人回来复核,证据必须附上。不要用 session_spawn/session_dispatch 或 task/workflow 绕过项目资源限制。不要轮询等待,不要反复无进展重试。用 report 登记关键进展,用 escalate 将重大偏差、权限或预算问题交给 Boss,不得自行解决升级事项。所有任务交付后,用 complete 提交带证据的最终交付总结。上下文有限,明确假设,主动寻找反证。只在本次触发范围内工作。";
21
+ export const STANDARD_LEADER_PROMPT = "你是直接向 Boss 负责的项目 Leader,兼任秘书。先调用 project(action=get, projectId) 了解最新状态。小项目只拆最少必要任务,大项目按交付边界分工。用 add_task 登记任务(明确目标、交付物、验收标准、依赖);add_task / update_task 时可以带 executorProfile 与 reviewerProfile(支持指定 agentId、cwd、model、thinkingLevel、watchdogTimeoutSeconds),按任务性质将执行与复核委派给最合适的员工,不指定则继承项目默认;需要继续拆解的工作设 role=leader,叶子执行任务设 role=executor,子 Leader 用 parentTaskId 登记自己的子任务,dispatch 启动执行子会话;不要用 session_spawn/session_dispatch 或 task/workflow 绕过项目资源限制。收到交付后用 request_review 启动独立复核。子会话结束会自动回报给你,届时继续派发 ready 任务、复核待验收任务或按 review.summary 返工。不要轮询等待,不要反复无进展重试,并发满时结束当前回复等待回报。用 report 登记关键进展。你的下属的升级先到你这里:你在自己这一层处理,能自己解决的自己解决;只有确实需要 Boss 决策的(重大偏差、权限或预算问题)才用 escalate 交给 Boss。你不能替 Boss 做决定,也不要把没处理的升级压下去不理。所有任务验收后,用 complete 提交带证据的最终交付总结。上下文有限,明确假设,主动寻找反证。只在本次触发范围内工作。";
22
+ export const LIGHTWEIGHT_LEADER_PROMPT = "你是直接向 Boss 负责的项目 Leader,这是一个轻量项目:这个项目由你亲自完成,不要拆子任务、不要派发给别人、也没有独立复核。先调用 project(action=get, projectId) 了解最新状态。用 add_task 登记任务(明确目标、交付物、验收标准、依赖),任务一律登记为执行任务:role=leader 或带 parentTaskId 都会被拒绝。dispatch 会把任务交回给你自己,你亲自做完;做完后调用 project(action=submit, projectId, taskId, output, evidenceRefs) 提交交付,提交即完成,不会有人回来复核,证据必须附上。不要用 session_spawn/session_dispatch 或 task/workflow 绕过项目资源限制。不要轮询等待,不要反复无进展重试。用 report 登记关键进展,用 escalate 将重大偏差、权限或预算问题交给 Boss,不得自行解决升级事项。所有任务交付后,用 complete 提交带证据的最终交付总结。上下文有限,明确假设,主动寻找反证。只在本次触发范围内工作。";
14
23
  const text = (value, label) => {
15
24
  if (typeof value !== "string" || !value.trim())
16
25
  throw new Error(`${label} 不能为空`);
@@ -122,6 +131,30 @@ const CACHE_WRITE_WEIGHT = 1.25;
122
131
  const tokens = (usage) => usage ? Math.round(usage.input + usage.output + usage.cacheWrite * CACHE_WRITE_WEIGHT + usage.cacheRead * CACHE_READ_WEIGHT) : 0;
123
132
  /** The budget a project starts with, and the floor the one-click raise falls back to. */
124
133
  const DEFAULT_MAX_TOKENS = 100_000;
134
+ /**
135
+ * HOW MANY PROJECTS ONE LIST CALL MAY CARRY.
136
+ *
137
+ * The list goes into an agent's context, and a workspace that has been alive for a year has
138
+ * hundreds of finished projects. Twenty is enough to answer "how are my projects doing" - the
139
+ * running ones and the ones that just finished are all in there - and small enough that the
140
+ * answer does not arrive wrapped in a year of history. The count that was cut is NOT hidden:
141
+ * the list carries the real total next to the truncated one.
142
+ */
143
+ const PROJECT_LIST_LIMIT = 20;
144
+ /**
145
+ * HOW MANY EVENTS THE REPORT'S TIMELINE PRINTS.
146
+ *
147
+ * A project that ran for weeks has thousands of events, and a timeline that lists all of them
148
+ * is not a document - it is a wall of text nobody reads, and it makes the report too big to
149
+ * paste into a weekly note. So the timeline prints the MOST RECENT 200 events and says, in
150
+ * front of the list, how many there were in total: a reader then knows that the list is the
151
+ * tail of a longer story rather than the whole of it.
152
+ *
153
+ * ONLY THE EVENT STREAM IS CONVERGED. The tasks, the deliveries, the acceptances, the
154
+ * exceptions and the recent reports are the report itself - they are all still printed, one by
155
+ * one, because dropping any of them would drop the answer to "what did this project do".
156
+ */
157
+ const REPORT_EVENT_LIMIT = 200;
125
158
  // THE WORDS THE REPORT SPEAKS IN. A reader of the report is a person, not a console, so every
126
159
  // state and every role is written the way a person would say it out loud.
127
160
  const PROJECT_STATUS_TEXT = {
@@ -136,7 +169,7 @@ const EVENT_TEXT = {
136
169
  created: "创建项目", started: "启动执行", task_created: "登记任务", dispatched: "派发执行",
137
170
  submitted: "提交交付", reviewed: "复核", review_requested: "启动复核", task_updated: "任务更新", status: "状态变更",
138
171
  budget: "预算变更", report: "进展汇报", escalated: "例外升级", decision: "决定",
139
- completed: "项目完成", ready: "依赖就绪",
172
+ completed: "项目完成", ready: "依赖就绪", unreachable: "转述对象不可达",
140
173
  };
141
174
  const ESCALATION_STATUS_TEXT = { open: "未决", resolved: "已解决", dismissed: "已忽略" };
142
175
  const SEVERITY_TEXT = { low: "低", medium: "中", high: "高" };
@@ -188,13 +221,24 @@ export class ProjectService {
188
221
  delete t.effectiveReviewerProfile;
189
222
  return t;
190
223
  }
191
- projectView(p) {
224
+ filterEscalations(escalations, actor) {
225
+ return escalations.filter((e) => {
226
+ const audience = e.audience ?? "boss";
227
+ if (audience === "boss")
228
+ return true;
229
+ if (!actor)
230
+ return false;
231
+ return actor === e.targetConversationId || actor === e.requestedByConversationId;
232
+ });
233
+ }
234
+ projectView(p, actor) {
192
235
  const resources = this.resources(p);
193
236
  return {
194
237
  ...p,
195
238
  resources,
196
239
  suggestedMaxTokens: this.suggestedMaxTokens(p, resources.tokens),
197
240
  tasks: p.tasks.map((t) => ProjectService.taskView(t, p)),
241
+ escalations: this.filterEscalations(p.escalations ?? [], actor),
198
242
  };
199
243
  }
200
244
  /**
@@ -227,7 +271,7 @@ export class ProjectService {
227
271
  p.updatedAt = stamp();
228
272
  (p.events ??= []).push({ id: randomUUID(), kind, summary, taskId, actorConversationId, createdAt: p.updatedAt });
229
273
  }
230
- async change(id, workspaceId, work) {
274
+ async change(id, workspaceId, work, actor) {
231
275
  this.workspace(workspaceId);
232
276
  const previous = this.queues.get(id) ?? Promise.resolve();
233
277
  const pending = previous.catch(() => { }).then(async () => {
@@ -239,7 +283,7 @@ export class ProjectService {
239
283
  }
240
284
  await this.store.setProject(p);
241
285
  if (result && typeof result === "object" && "tasks" in result && Array.isArray(result.tasks)) {
242
- return structuredClone(this.projectView(result));
286
+ return structuredClone(this.projectView(result, actor));
243
287
  }
244
288
  return structuredClone(result);
245
289
  });
@@ -254,6 +298,45 @@ export class ProjectService {
254
298
  }
255
299
  live(p) { if (closed(p))
256
300
  throw new Error("项目已结束,不能继续修改或派发"); }
301
+ /**
302
+ * WAKING THE OTHER SIDE MUST NOT TAKE THE REPORT DOWN WITH IT.
303
+ *
304
+ * The report, the escalation or the decision is already written to the store by the time the
305
+ * other side is woken, so a dispatch that fails - a session that was archived, stopped or never
306
+ * existed - must NOT fail the request. It would: the caller would be told the whole thing
307
+ * failed although it succeeded, it would try again, and the same report would be written twice.
308
+ *
309
+ * So the wake is wrapped, the failure is written down, and the record is returned as the
310
+ * success it is.
311
+ *
312
+ * AND THE FAILURE IS NOT SILENT. A report nobody was told about is a report nobody will ever
313
+ * handle, so the project's own timeline gets an event saying exactly that, and the log line
314
+ * names the project and the escalation so the case can be found again later.
315
+ */
316
+ async wake(target, prompt, what, projectId, workspaceId, actor) {
317
+ if (!this.orchestrator)
318
+ return;
319
+ try {
320
+ await this.orchestrator.dispatch(target, prompt, { wait: false });
321
+ }
322
+ catch (error) {
323
+ const message = error instanceof Error ? error.message : String(error);
324
+ console.warn(`Super Pi: 项目 ${projectId} 的${what}已保存,但叫不醒会话 ${target}:${message}`);
325
+ await this.noteUnreachable(projectId, workspaceId, `${what}已保存,但转述对象会话不可达(${target}):${message}`, actor);
326
+ }
327
+ }
328
+ /** THE TIMELINE SAYS THAT NOBODY COULD BE TOLD. Written after the fact, so it never loses the
329
+ * record that was already saved. */
330
+ async noteUnreachable(projectId, workspaceId, summary, actor) {
331
+ if (workspaceId === undefined)
332
+ return;
333
+ try {
334
+ await this.change(projectId, workspaceId, (p) => { this.event(p, "unreachable", summary, undefined, actor); });
335
+ }
336
+ catch (error) {
337
+ console.warn(`Super Pi: 项目 ${projectId} 记录「转述对象不可达」事件失败:${error instanceof Error ? error.message : String(error)}`);
338
+ }
339
+ }
257
340
  leader(p, actor) { if (actor && actor !== p.ownerConversationId)
258
341
  throw new Error("仅项目负责人可以执行此操作"); }
259
342
  topLevelAuthor(p, actor) {
@@ -306,6 +389,78 @@ export class ProjectService {
306
389
  if (actor && actor !== p.ownerConversationId && actor !== p.initiatorConversationId && !p.tasks.some((t) => t.ownerConversationId === actor || t.reviewerConversationId === actor))
307
390
  throw new Error("调用会话不属于项目执行团队");
308
391
  }
392
+ isMember(p, actor) {
393
+ if (!actor)
394
+ return false;
395
+ if (actor === p.initiatorConversationId)
396
+ return true;
397
+ if (actor === p.ownerConversationId)
398
+ return true;
399
+ if (p.tasks?.some((t) => t.ownerConversationId === actor || t.reviewerConversationId === actor))
400
+ return true;
401
+ if (this.projectForSession(actor)?.id === p.id)
402
+ return true;
403
+ return false;
404
+ }
405
+ directSupervisor(p, actor, taskId) {
406
+ if (!actor || actor === p.ownerConversationId || actor === p.initiatorConversationId)
407
+ return undefined;
408
+ const session = this.store.conversation(actor);
409
+ const parentId = session?.parentConversationId;
410
+ if (parentId && this.isMember(p, parentId))
411
+ return parentId;
412
+ const t = taskId
413
+ ? p.tasks.find((item) => item.id === taskId)
414
+ : p.tasks.find((item) => item.ownerConversationId === actor || item.reviewerConversationId === actor);
415
+ const manager = t ? this.manager(p, t) : undefined;
416
+ if (manager && this.isMember(p, manager))
417
+ return manager;
418
+ return undefined;
419
+ }
420
+ /**
421
+ * THE PROJECT A SESSION BELONGS TO RIGHT NOW.
422
+ *
423
+ * `projectForSession` answers a different question - "which project is this session part of" -
424
+ * and it counts the finished projects too, because the history of a project has to stay visible
425
+ * to the people who worked on it. This method answers the narrower question the QUESTION path
426
+ * needs: "is this session inside a project that is still running".
427
+ *
428
+ * WHY THAT DISTINCTION MATTERS. A conversation that took part in a project which has since been
429
+ * completed is not a project member any more - it is a conversation that happens to have a
430
+ * finished project in its history. The old lookup mixed the two, so asking the Boss a question
431
+ * from such a conversation was refused with "a project member may not ask the Boss directly",
432
+ * which is not what was true: the project was over, and nobody was being bypassed. The finished
433
+ * project is still found and still visible in the history; it just does not block the question.
434
+ *
435
+ * A session that is in a running project AND in a finished one belongs to the running one - it
436
+ * is the one that is still in charge of it.
437
+ */
438
+ liveProjectForSession(conversationId) {
439
+ let c = this.store.conversation(conversationId);
440
+ const seen = new Set();
441
+ while (c && !seen.has(c.id)) {
442
+ seen.add(c.id);
443
+ const projects = this.store.projects().filter((p) => p.workspaceId === c.workspaceId
444
+ && (p.ownerConversationId === c.id || p.tasks.some((t) => t.ownerConversationId === c.id || t.reviewerConversationId === c.id)));
445
+ const live = projects.find((p) => !closed(p));
446
+ if (live)
447
+ return live;
448
+ c = c.parentConversationId ? this.store.conversation(c.parentConversationId) : undefined;
449
+ }
450
+ return undefined;
451
+ }
452
+ canAskQuestion(conversationId) {
453
+ const project = this.liveProjectForSession(conversationId);
454
+ if (!project)
455
+ return { ok: true };
456
+ if (conversationId === project.ownerConversationId || conversationId === project.initiatorConversationId) {
457
+ return { ok: true };
458
+ }
459
+ return {
460
+ ok: false,
461
+ reason: "项目成员不能直接向 Boss 提问:请通过直接上级上报,由负责人代为转述",
462
+ };
463
+ }
309
464
  reconcile(p) {
310
465
  if (closed(p))
311
466
  return;
@@ -315,9 +470,15 @@ export class ProjectService {
315
470
  t.updatedAt = stamp();
316
471
  this.event(p, "ready", `前置交付已验收:${t.title}`, t.id);
317
472
  }
318
- const open = p.escalations.filter((e) => e.status === "open");
319
- p.status = open.some((e) => e.severity === "high") ? "awaiting_approval" : open.length || p.tasks.some((t) => t.status === "blocked") ? "blocked"
320
- : p.tasks.some((t) => t.status === "awaiting_review") ? "awaiting_review" : p.tasks.length ? "active" : "planning";
473
+ const isLeader = (convId) => !convId || convId === p.ownerConversationId || convId === p.initiatorConversationId;
474
+ const openBoss = p.escalations.filter((e) => e.status === "open" && (e.audience === "boss" || !e.audience));
475
+ const hasLeaderBlockedTask = p.tasks.some((t) => t.status === "blocked" && isLeader(t.blockedReportedByConversationId));
476
+ const lastLeaderProjectReport = [...p.reports].reverse().find((r) => !r.taskId && isLeader(r.authorConversationId));
477
+ const leaderProjectBlocked = lastLeaderProjectReport?.kind === "blocked";
478
+ p.status = openBoss.some((e) => e.severity === "high") ? "awaiting_approval"
479
+ : openBoss.length || hasLeaderBlockedTask || leaderProjectBlocked ? "blocked"
480
+ : p.tasks.some((t) => t.status === "awaiting_review") ? "awaiting_review"
481
+ : p.tasks.length ? "active" : "planning";
321
482
  }
322
483
  resources(p) {
323
484
  const state = this.store.snapshot();
@@ -371,7 +532,7 @@ export class ProjectService {
371
532
  const scope = this.workspace(workspaceId);
372
533
  return this.store.projects().filter((p) => p.workspaceId === scope).map((p) => this.projectView(p));
373
534
  }
374
- headline(p) {
535
+ headline(p, actor) {
375
536
  const total = p.tasks.length;
376
537
  const completed = p.tasks.filter((t) => t.status === "completed").length;
377
538
  if (p.status === "completed") {
@@ -381,9 +542,10 @@ export class ProjectService {
381
542
  return "项目已取消";
382
543
  if (p.status === "failed")
383
544
  return "项目失败";
384
- const openEscalation = (p.escalations ?? []).find((e) => e.status === "open");
545
+ const visibleEscalations = this.filterEscalations(p.escalations ?? [], actor);
546
+ const openEscalation = visibleEscalations.find((e) => e.status === "open");
385
547
  if (openEscalation) {
386
- return p.status === "awaiting_approval"
548
+ return (openEscalation.audience === "boss" || !openEscalation.audience) && p.status === "awaiting_approval"
387
549
  ? `等待审批:${openEscalation.reason}`
388
550
  : `项目已阻塞:${openEscalation.reason}`;
389
551
  }
@@ -410,26 +572,16 @@ export class ProjectService {
410
572
  return "项目规划中,尚未拆解任务";
411
573
  }
412
574
  isActorRelated(p, actor) {
413
- if (!actor)
414
- return false;
415
- if (actor === p.initiatorConversationId)
416
- return true;
417
- if (actor === p.ownerConversationId)
418
- return true;
419
- if (p.tasks?.some((t) => t.ownerConversationId === actor || t.reviewerConversationId === actor))
420
- return true;
421
- if (this.projectForSession(actor)?.id === p.id)
422
- return true;
423
- return false;
575
+ return this.isMember(p, actor);
424
576
  }
425
- projectSummary(p) {
577
+ projectSummary(p, actor) {
426
578
  const resources = this.resources(p);
427
579
  return {
428
580
  id: p.id,
429
581
  name: p.title,
430
582
  status: p.status,
431
583
  mode: p.mode ?? "standard",
432
- headline: this.headline(p),
584
+ headline: this.headline(p, actor),
433
585
  tasks: {
434
586
  total: p.tasks.length,
435
587
  completed: p.tasks.filter((t) => t.status === "completed").length,
@@ -441,15 +593,32 @@ export class ProjectService {
441
593
  updatedAt: p.updatedAt,
442
594
  };
443
595
  }
444
- listForActor(workspaceId, actor) {
596
+ /**
597
+ * THE PROJECTS AN ACTOR IS PART OF, AND HOW MANY OF THEM THERE ARE.
598
+ *
599
+ * The list is capped, because the list is what goes into an agent's context: a workspace that
600
+ * has been alive for a year has hundreds of finished projects, and handing all of them over
601
+ * answers the question "how are my projects doing" with a wall of history. The cap is on the
602
+ * RECENT ones - the same order the list has always had, newest update first - so what the
603
+ * agent reads first is what actually changed.
604
+ *
605
+ * AND THE COUNT IS TOLD, NOT IMPLIED. `total` is how many projects the session is really
606
+ * related to, counted BEFORE the cut, and `returned` is how many came back. An agent that sees
607
+ * total 57 and returned 20 knows the workspace has more history than it was shown, and says so
608
+ * instead of telling the Boss the workspace only has 20 projects. A silently truncated list
609
+ * would be a lie the agent could not see through.
610
+ */
611
+ listForActor(workspaceId, actor, limit = PROJECT_LIST_LIMIT) {
445
612
  const scope = this.workspace(workspaceId);
446
- return this.store.projects()
613
+ const related = this.store.projects()
447
614
  .filter((p) => p.workspaceId === scope && this.isActorRelated(p, actor))
448
- .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
449
- .map((p) => this.projectSummary(p));
615
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
616
+ const total = related.length;
617
+ const projects = related.slice(0, limit).map((p) => this.projectSummary(p, actor));
618
+ return { projects, total, returned: projects.length };
450
619
  }
451
- get(id, workspaceId) {
452
- return this.projectView(this.raw(id, workspaceId));
620
+ get(id, workspaceId, actor) {
621
+ return this.projectView(this.raw(id, workspaceId), actor);
453
622
  }
454
623
  /**
455
624
  * THE PROJECT AS A REPORT A HUMAN CAN READ.
@@ -544,8 +713,17 @@ export class ProjectService {
544
713
  const events = [...(p.events ?? [])].sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt));
545
714
  if (!events.length)
546
715
  lines.push("无");
547
- for (const e of events)
548
- lines.push(`- ${moment(e.createdAt)} · ${EVENT_TEXT[e.kind] ?? e.kind} · ${oneLine(e.summary)} · ${session(e.actorConversationId)}`);
716
+ else {
717
+ /* THE TIMELINE IS CONVERGED, AND SAYS SO. The list is the tail of the project's story -
718
+ the most recent events, still in time order - and the number in front of it is how
719
+ long the whole story was, so a reader never mistakes the tail for the whole. */
720
+ const shown = events.length > REPORT_EVENT_LIMIT ? events.slice(-REPORT_EVENT_LIMIT) : events;
721
+ lines.push(events.length > REPORT_EVENT_LIMIT
722
+ ? `共 ${events.length} 条事件,以下是最近 ${REPORT_EVENT_LIMIT} 条(更早的省略)`
723
+ : `共 ${events.length} 条事件`);
724
+ for (const e of shown)
725
+ lines.push(`- ${moment(e.createdAt)} · ${EVENT_TEXT[e.kind] ?? e.kind} · ${oneLine(e.summary)} · ${session(e.actorConversationId)}`);
726
+ }
549
727
  lines.push("", "## 最近汇报", "");
550
728
  if (!p.reports.length)
551
729
  lines.push("无");
@@ -611,7 +789,7 @@ export class ProjectService {
611
789
  }
612
790
  admission(p, checkConcurrency = true, coordinator = false) {
613
791
  this.live(p);
614
- if (p.escalations.some((e) => e.status === "open" && e.severity === "high"))
792
+ if (p.escalations.some((e) => e.status === "open" && e.severity === "high" && (e.audience === "boss" || !e.audience)))
615
793
  throw new Error("等待 Boss 处理升级决策后才能继续派发");
616
794
  const r = this.resources(p);
617
795
  if (p.budget?.maxTokens && r.tokens >= p.budget.maxTokens)
@@ -787,15 +965,16 @@ export class ProjectService {
787
965
  t.status = "in_progress";
788
966
  t.updatedAt = stamp();
789
967
  delete t.blockedReason;
968
+ delete t.blockedReportedByConversationId;
790
969
  this.event(p, "dispatched", `派发执行:${t.title}`, t.id, actor);
791
970
  this.reconcile(p);
792
971
  await this.store.setProject(p);
793
- const delegation = t.role === "leader" ? `你是分支 Leader,负责这项任务的拆解、委派、集成交付。用 project add_task 时必须带 parentTaskId="${t.id}";可按必要性指定 role="leader" 或 "executor";add_task / update_task 时可以带 executorProfile 与 reviewerProfile(支持指定 agentId、cwd、model、thinkingLevel、watchdogTimeoutSeconds),按任务性质将执行与复核委派给最合适的员工,不指定则继承项目默认。用 project dispatch / request_review 管理直属子任务,子会话结束自动回报给你,不要轮询;并发满时结束本轮等待回报。所有必需子任务独立验收后才能 submit 聚合交付,上级还会独立复核你的集成结果。最多 3 层 Leader、8 个分支 Leader;小任务不要继续拆层。` : "你是执行者,不得自行拆解子任务。";
972
+ const delegation = t.role === "leader" ? `你是分支 Leader,负责这项任务的拆解、委派、集成交付。用 project add_task 时必须带 parentTaskId="${t.id}";可按必要性指定 role="leader" 或 "executor";add_task / update_task 时可以带 executorProfile 与 reviewerProfile(支持指定 agentId、cwd、model、thinkingLevel、watchdogTimeoutSeconds),按任务性质将执行与复核委派给最合适的员工,不指定则继承项目默认。用 project dispatch / request_review 管理直属子任务,子会话结束自动回报给你,不要轮询;并发满时结束本轮等待回报。所有必需子任务独立验收后才能 submit 聚合交付,上级还会独立复核你的集成结果。最多 3 层 Leader、8 个分支 Leader;小任务不要继续拆层。你的下属的升级先到你这里;你决定是自己解决还是再向上级转述。` : "你是执行者,不得自行拆解子任务。";
794
973
  /* THE LIGHTWEIGHT TASK SAYS WHAT IS DIFFERENT, because the difference is the point: there is
795
974
  no reviewer coming back, so the evidence that the reviewer would have demanded is the only
796
975
  thing that stands behind the delivery. */
797
976
  const modeNote = lightweight(p) ? "本项目是轻量项目:你就是唯一的执行会话,不会有独立复核人回来,完成后调用 project(action=submit, …) 提交即完成,交付证据必须附上。" : "";
798
- await this.orchestrator.dispatch(t.ownerConversationId, `${this.context(p)}\n${delegation}\n你负责执行任务 ${t.id}:${t.title}\n目标:${t.objective}\n交付物:${t.deliverables.join(";")}\n验收:${t.acceptanceCriteria.join(";")}\n上次复核意见与验证证据:${JSON.stringify(t.review ?? null)}\n已验收前置产物:${JSON.stringify(p.tasks.filter((d) => t.dependencies.includes(d.id)).map((d) => ({ title: d.title, output: d.output, evidenceRefs: d.evidenceRefs })))}\n你不是唯一执行者,遵守分工边界。${modeNote}完成后必须调用 project(action=submit, projectId="${p.id}", taskId="${t.id}", output="交付总结", evidenceRefs=["产物路径或验证记录"]),不能自行标记已完成。复核退回时修复后重新 submit。遇到阻塞用 report 或 escalate,保留证据、假设和未解决问题。不要自行创建其他执行会话或 task/workflow。`, { wait: false });
977
+ await this.orchestrator.dispatch(t.ownerConversationId, `${this.context(p)}\n${delegation}\n你负责执行任务 ${t.id}:${t.title}\n目标:${t.objective}\n交付物:${t.deliverables.join(";")}\n验收:${t.acceptanceCriteria.join(";")}\n上次复核意见与验证证据:${JSON.stringify(t.review ?? null)}\n已验收前置产物:${JSON.stringify(p.tasks.filter((d) => t.dependencies.includes(d.id)).map((d) => ({ title: d.title, output: d.output, evidenceRefs: d.evidenceRefs })))}\n你不是唯一执行者,遵守分工边界。${modeNote}完成后必须调用 project(action=submit, projectId="${p.id}", taskId="${t.id}", output="交付总结", evidenceRefs=["产物路径或验证记录"]),不能自行标记已完成。复核退回时修复后重新 submit。遇到阻塞用 report(阻塞) 或 escalate 上报给你的直接上级,不要直接找 Boss,保留证据、假设和未解决问题。不要自行创建其他执行会话或 task/workflow。`, { wait: false });
799
978
  return ProjectService.taskView(t, p);
800
979
  });
801
980
  }
@@ -915,6 +1094,12 @@ export class ProjectService {
915
1094
  if (patch.status === "ready" && actor && actor !== manager)
916
1095
  throw new Error("仅直属 Leader 可以重新派发任务");
917
1096
  t.status = patch.status;
1097
+ if (t.status === "blocked") {
1098
+ t.blockedReportedByConversationId = actor;
1099
+ }
1100
+ else {
1101
+ delete t.blockedReportedByConversationId;
1102
+ }
918
1103
  }
919
1104
  if (patch.executorProfile !== undefined) {
920
1105
  if (t.ownerConversationId)
@@ -1114,7 +1299,10 @@ export class ProjectService {
1114
1299
  return { ...raised, project };
1115
1300
  }
1116
1301
  async addReport(id, input, workspaceId, actor) {
1117
- return this.change(id, workspaceId, (p) => {
1302
+ let supervisorToWake;
1303
+ let taskForWake;
1304
+ let memberBlockedReport = false;
1305
+ const report = await this.change(id, workspaceId, (p) => {
1118
1306
  this.live(p);
1119
1307
  this.member(p, actor);
1120
1308
  if (!["progress", "blocked", "delivery", "decision_request", "review"].includes(input.kind))
@@ -1128,31 +1316,85 @@ export class ProjectService {
1128
1316
  progress: text(input.progress, "汇报进展"), variance: optional(input.variance, "偏差"), nextStep: optional(input.nextStep, "下一步"), risks: strings(input.risks, "风险"),
1129
1317
  decisionNeeded: optional(input.decisionNeeded, "所需决策"), evidenceRefs: strings(input.evidenceRefs, "证据"), createdAt: stamp() };
1130
1318
  p.reports.push(report);
1131
- if (report.kind === "blocked" && t) {
1132
- t.status = "blocked";
1133
- t.blockedReason = report.progress;
1134
- t.updatedAt = stamp();
1319
+ if (report.kind === "blocked") {
1320
+ if (t) {
1321
+ t.status = "blocked";
1322
+ t.blockedReason = report.progress;
1323
+ t.blockedReportedByConversationId = actor;
1324
+ t.updatedAt = stamp();
1325
+ }
1326
+ if (actor && actor !== p.ownerConversationId && actor !== p.initiatorConversationId) {
1327
+ memberBlockedReport = true;
1328
+ supervisorToWake = this.directSupervisor(p, actor, t?.id);
1329
+ taskForWake = t;
1330
+ }
1135
1331
  }
1136
1332
  this.event(p, "report", report.progress, t?.id, actor);
1137
1333
  this.reconcile(p);
1138
1334
  return report;
1139
- });
1335
+ }, actor);
1336
+ if (supervisorToWake) {
1337
+ const taskInfo = taskForWake ? `任务 ${taskForWake.id}(${taskForWake.title})` : "所属任务";
1338
+ await this.wake(supervisorToWake, `下级成员(会话:${actor})汇报阻塞:${taskInfo}\n原因:${report.progress}\n请评估阻塞并跟进解决,必要时向 Boss 转述或升级。`, "阻塞上报", id, workspaceId, actor);
1339
+ }
1340
+ else if (memberBlockedReport) {
1341
+ /* A MEMBER REPORTED A BLOCK AND NO SUPERVISOR COULD BE FOUND. The report is saved either
1342
+ * way, but nobody has been told, so the case is written down instead of vanishing. */
1343
+ const summary = "阻塞上报已保存,但找不到直接上级会话,上报没有被转述";
1344
+ console.warn(`Super Pi: 项目 ${id} 的${summary}`);
1345
+ await this.noteUnreachable(id, workspaceId, summary, actor);
1346
+ }
1347
+ return report;
1140
1348
  }
1141
1349
  async addEscalation(id, input, workspaceId, actor) {
1142
- return this.change(id, workspaceId, (p) => {
1350
+ let supervisorToWake;
1351
+ let wakeMemberRole;
1352
+ const result = await this.change(id, workspaceId, (p) => {
1143
1353
  this.live(p);
1144
1354
  this.member(p, actor);
1145
1355
  if (!["low", "medium", "high"].includes(input.severity))
1146
1356
  throw new Error("升级风险等级不合法");
1147
- const t = input.taskId ? this.task(p, text(input.taskId, "任务 id")) : undefined;
1148
- const e = { id: randomUUID(), projectId: id, taskId: t?.id, reason: text(input.reason, "升级原因"), severity: input.severity, status: "open", requestedByConversationId: actor, createdAt: stamp() };
1357
+ const t = input.taskId
1358
+ ? this.task(p, text(input.taskId, "任务 id"))
1359
+ : p.tasks.find((task) => task.ownerConversationId === actor || task.reviewerConversationId === actor);
1360
+ const isBossAudience = !actor || actor === p.ownerConversationId || actor === p.initiatorConversationId;
1361
+ const audience = isBossAudience ? "boss" : "supervisor";
1362
+ let targetConversationId;
1363
+ if (!isBossAudience) {
1364
+ targetConversationId = this.directSupervisor(p, actor, t?.id);
1365
+ if (!targetConversationId) {
1366
+ throw new Error("无法确定直接上级会话:上级不在项目执行团队内且无法定位所属任务 Leader,升级已拒绝。");
1367
+ }
1368
+ supervisorToWake = targetConversationId;
1369
+ wakeMemberRole = p.tasks.some((task) => task.role === "leader" && task.ownerConversationId === actor)
1370
+ ? "分支 Leader"
1371
+ : p.tasks.some((task) => task.reviewerConversationId === actor)
1372
+ ? "独立复核"
1373
+ : "执行者";
1374
+ }
1375
+ const e = {
1376
+ id: randomUUID(),
1377
+ projectId: id,
1378
+ taskId: t?.id,
1379
+ reason: text(input.reason, "升级原因"),
1380
+ severity: input.severity,
1381
+ status: "open",
1382
+ requestedByConversationId: actor,
1383
+ audience,
1384
+ ...(targetConversationId ? { targetConversationId } : {}),
1385
+ createdAt: stamp(),
1386
+ };
1149
1387
  p.escalations.push(e);
1150
1388
  this.event(p, "escalated", e.reason, t?.id, actor);
1151
1389
  this.reconcile(p);
1152
1390
  return e;
1153
- });
1391
+ }, actor);
1392
+ if (supervisorToWake) {
1393
+ await this.wake(supervisorToWake, `下级成员(会话:${actor},身份:${wakeMemberRole ?? "执行者"})发起例外升级(ID:${result.id}):\n原因:${result.reason}\n严重级别:${result.severity}\n请及时处理该例外(调用 project action="resolve_escalation" 记录处理意见),或按需向上级转述升级。`, `例外升级(ID:${result.id})`, id, workspaceId, actor);
1394
+ }
1395
+ return result;
1154
1396
  }
1155
- async resolveEscalation(id, escalationId, decision, status, workspaceId) {
1397
+ async resolveEscalation(id, escalationId, decision, status, workspaceId, actor) {
1156
1398
  const result = await this.change(id, workspaceId, (p) => {
1157
1399
  this.live(p);
1158
1400
  if (status !== "resolved" && status !== "dismissed")
@@ -1162,33 +1404,54 @@ export class ProjectService {
1162
1404
  throw new Error("升级事项不存在");
1163
1405
  if (e.status !== "open")
1164
1406
  throw new Error("升级事项已处理");
1407
+ const audience = e.audience ?? "boss";
1408
+ if (audience === "boss") {
1409
+ if (actor)
1410
+ throw new Error("此例外需由 Boss 决策,团队成员无权处理");
1411
+ }
1412
+ else if (audience === "supervisor") {
1413
+ if (!actor)
1414
+ throw new Error("该例外属于直接上级处理范围,Boss 不能直接处理 supervisor 级别的例外");
1415
+ if (actor !== e.targetConversationId)
1416
+ throw new Error("只有指定的直接上级可以处理此例外");
1417
+ e.resolvedByConversationId = actor;
1418
+ }
1165
1419
  e.status = status;
1166
1420
  e.decision = text(decision, "处理意见");
1167
1421
  e.resolvedAt = stamp();
1168
- this.event(p, "decision", `Boss 决策:${e.decision}`, e.taskId);
1422
+ const decider = audience === "boss" ? "Boss" : "直接上级";
1423
+ this.event(p, "decision", `${decider}决策:${e.decision}`, e.taskId, actor);
1169
1424
  this.reconcile(p);
1170
1425
  return e;
1171
- });
1172
- const p = this.get(id, workspaceId);
1173
- const targetTask = result.taskId
1174
- ? p.tasks.find((t) => t.id === result.taskId)
1175
- : p.tasks.find((t) => t.ownerConversationId === result.requestedByConversationId || t.reviewerConversationId === result.requestedByConversationId);
1176
- let directLeaderId;
1177
- if (targetTask) {
1178
- if (targetTask.role === "leader" && targetTask.ownerConversationId) {
1179
- directLeaderId = targetTask.ownerConversationId;
1426
+ }, actor);
1427
+ const p = this.raw(id, workspaceId);
1428
+ if (result.audience === "supervisor") {
1429
+ if (result.requestedByConversationId) {
1430
+ await this.wake(result.requestedByConversationId, `直接上级已处理例外(ID:${result.id}):${result.reason}\n处理意见:${result.decision}\n请按处理意见继续工作。`, `例外处理结果(ID:${result.id})`, id, workspaceId, actor);
1180
1431
  }
1181
- else if (targetTask.parentTaskId) {
1182
- const parent = p.tasks.find((t) => t.id === targetTask.parentTaskId);
1183
- directLeaderId = parent?.ownerConversationId ?? targetTask.accountableConversationId;
1432
+ }
1433
+ else {
1434
+ const targetTask = result.taskId
1435
+ ? p.tasks.find((t) => t.id === result.taskId)
1436
+ : p.tasks.find((t) => t.ownerConversationId === result.requestedByConversationId || t.reviewerConversationId === result.requestedByConversationId);
1437
+ let directLeaderId;
1438
+ if (targetTask) {
1439
+ if (targetTask.role === "leader" && targetTask.ownerConversationId) {
1440
+ directLeaderId = targetTask.ownerConversationId;
1441
+ }
1442
+ else if (targetTask.parentTaskId) {
1443
+ const parent = p.tasks.find((t) => t.id === targetTask.parentTaskId);
1444
+ directLeaderId = parent?.ownerConversationId ?? targetTask.accountableConversationId;
1445
+ }
1446
+ else {
1447
+ directLeaderId = targetTask.accountableConversationId ?? p.ownerConversationId;
1448
+ }
1184
1449
  }
1185
- else {
1186
- directLeaderId = targetTask.accountableConversationId ?? p.ownerConversationId;
1450
+ const recipientId = directLeaderId || p.ownerConversationId;
1451
+ if (recipientId) {
1452
+ await this.wake(recipientId, `Boss 已处理项目 ${p.id} 的例外:${result.reason}\n决策:${result.decision}\n先读取 project 最新状态,按决策继续范围内的工作。`, `例外处理结果(ID:${result.id})`, id, workspaceId, actor);
1187
1453
  }
1188
1454
  }
1189
- const recipientId = directLeaderId || p.ownerConversationId;
1190
- if (recipientId && this.orchestrator)
1191
- await this.orchestrator.dispatch(recipientId, `Boss 已处理项目 ${p.id} 的例外:${result.reason}\n决策:${result.decision}\n先读取 project 最新状态,按决策继续范围内的工作。`, { wait: false });
1192
1455
  return result;
1193
1456
  }
1194
1457
  async complete(id, summary, evidenceRefs, workspaceId, actor) {
@@ -1314,13 +1577,16 @@ export class ProjectService {
1314
1577
  return this.get(project.id, workspaceId);
1315
1578
  }
1316
1579
  if (body.action === "list") {
1580
+ // THE LIST IS CAPPED, AND IT SAYS SO. `total` and `returned` travel with it, so an agent
1581
+ // that sees a short list can tell the difference between "the workspace has three projects"
1582
+ // and "the workspace has fifty and I was shown twenty".
1317
1583
  return this.listForActor(workspaceId, actor);
1318
1584
  }
1319
1585
  const id = text(body.projectId, "projectId");
1320
- const p = this.get(id, workspaceId);
1321
- this.member(p, actor);
1586
+ const rawProject = this.raw(id, workspaceId);
1587
+ this.member(rawProject, actor);
1322
1588
  switch (body.action) {
1323
- case "get": return p;
1589
+ case "get": return this.get(id, workspaceId, actor);
1324
1590
  case "export": return this.exportReport(id, workspaceId);
1325
1591
  case "start": return this.start(id, workspaceId, actor);
1326
1592
  case "add_task": return this.addTask(id, body, workspaceId, actor);
@@ -1331,6 +1597,9 @@ export class ProjectService {
1331
1597
  case "update_task": return this.updateTask(id, text(body.taskId, "taskId"), { ...body, workspaceId }, actor);
1332
1598
  case "report": return this.addReport(id, body, workspaceId, actor);
1333
1599
  case "escalate": return this.addEscalation(id, body, workspaceId, actor);
1600
+ case "resolve_escalation":
1601
+ case "resolveEscalation":
1602
+ return this.resolveEscalation(id, text(body.escalationId, "escalationId"), text(body.decision, "decision"), body.status ?? "resolved", workspaceId, actor);
1334
1603
  case "complete": return this.complete(id, body.summary, body.evidenceRefs, workspaceId, actor);
1335
1604
  case "cancel": return this.cancel(id, body, workspaceId, actor);
1336
1605
  default: throw new Error("未知的项目工具操作");
@@ -102,6 +102,9 @@ export declare class RunService {
102
102
  private readonly runCompletions;
103
103
  /** A selected interrupt owns the next free run slot, even while the old child exits. */
104
104
  private readonly interruptTargets;
105
+ private shuttingDown;
106
+ /** Preserve incoming background results as waiting messages while the server drains its producers. */
107
+ beginShutdown(): void;
105
108
  /** Claims the run slot and arms its completion promise. Call sites release with `releaseRunSlot`. */
106
109
  private claimRunSlot;
107
110
  /** Releases the run slot and settles its completion promise, so a waiter wakes on the SAME instruction. */