@bolloon/bolloon-agent 0.5.5 → 0.6.0

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 (109) hide show
  1. package/dist/agents/parse-tool-call.js +6 -1
  2. package/dist/agents/pi-sdk-session-factory.js +36 -3
  3. package/dist/agents/pi-sdk.js +349 -199
  4. package/dist/agents/run-context.js +20 -0
  5. package/dist/agents/runner-resolver.js +2 -1
  6. package/dist/electron-build/agents/parse-tool-call.js +6 -1
  7. package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
  8. package/dist/electron-build/agents/pi-sdk-session-factory.js +36 -3
  9. package/dist/electron-build/agents/pi-sdk-session-factory.js.map +1 -1
  10. package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
  11. package/dist/electron-build/agents/pi-sdk.js +349 -199
  12. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  13. package/dist/electron-build/agents/run-context.js +24 -0
  14. package/dist/electron-build/agents/run-context.js.map +1 -0
  15. package/dist/electron-build/agents/runner-resolver.js +2 -1
  16. package/dist/electron-build/agents/runner-resolver.js.map +1 -1
  17. package/dist/electron-build/kernel/channel-actor.js +314 -0
  18. package/dist/electron-build/kernel/channel-actor.js.map +1 -0
  19. package/dist/index.js +24 -19
  20. package/dist/kernel/channel-actor.js +297 -0
  21. package/dist/kernel/control.js +95 -0
  22. package/dist/kernel/gate-scan.js +1036 -0
  23. package/dist/kernel/model-runtime.js +472 -0
  24. package/dist/kernel/plan-channel-actor.js +298 -0
  25. package/dist/kernel/plan-constraint.js +165 -0
  26. package/dist/kernel/plan-deletion.js +32 -0
  27. package/dist/kernel/plan-modelruntime.js +60 -0
  28. package/dist/kernel/plan-runcontext.js +163 -0
  29. package/dist/kernel/plan.js +153 -0
  30. package/dist/kernel/roster.js +242 -0
  31. package/dist/web/bolloon-web.json +3 -3
  32. package/dist/web/client.js +3 -2
  33. package/dist/web/routes-tasks.js +5 -1
  34. package/dist/web/server.js +58 -18
  35. package/dist/web/sw.js +1 -1
  36. package/dist/web/ui/message-renderer.js +473 -29
  37. package/dist/web/ui/step-timeline.js +13 -12
  38. package/package.json +1 -1
  39. package/scripts/build-web.ts +74 -1
  40. package/dist/constraint-runtime/_archive_helper.d.ts +0 -1
  41. package/dist/constraint-runtime/_archive_helper.js +0 -9
  42. package/dist/constraint-runtime/assistant/index.d.ts +0 -11
  43. package/dist/constraint-runtime/assistant/index.js +0 -12
  44. package/dist/constraint-runtime/bootstrap/index.d.ts +0 -11
  45. package/dist/constraint-runtime/bootstrap/index.js +0 -12
  46. package/dist/constraint-runtime/bridge/index.d.ts +0 -11
  47. package/dist/constraint-runtime/bridge/index.js +0 -12
  48. package/dist/constraint-runtime/buddy/index.d.ts +0 -11
  49. package/dist/constraint-runtime/buddy/index.js +0 -12
  50. package/dist/constraint-runtime/cli/index.d.ts +0 -11
  51. package/dist/constraint-runtime/cli/index.js +0 -12
  52. package/dist/constraint-runtime/components/index.d.ts +0 -11
  53. package/dist/constraint-runtime/components/index.js +0 -12
  54. package/dist/constraint-runtime/constants/index.d.ts +0 -11
  55. package/dist/constraint-runtime/constants/index.js +0 -12
  56. package/dist/constraint-runtime/coordinator/index.d.ts +0 -11
  57. package/dist/constraint-runtime/coordinator/index.js +0 -12
  58. package/dist/constraint-runtime/cost_hook.d.ts +0 -2
  59. package/dist/constraint-runtime/cost_hook.js +0 -4
  60. package/dist/constraint-runtime/entrypoints/index.d.ts +0 -11
  61. package/dist/constraint-runtime/entrypoints/index.js +0 -12
  62. package/dist/constraint-runtime/execution_registry.d.ts +0 -20
  63. package/dist/constraint-runtime/execution_registry.js +0 -44
  64. package/dist/constraint-runtime/hooks/index.d.ts +0 -11
  65. package/dist/constraint-runtime/hooks/index.js +0 -12
  66. package/dist/constraint-runtime/ink.d.ts +0 -1
  67. package/dist/constraint-runtime/ink.js +0 -4
  68. package/dist/constraint-runtime/keybindings/index.d.ts +0 -11
  69. package/dist/constraint-runtime/keybindings/index.js +0 -12
  70. package/dist/constraint-runtime/memdir/index.d.ts +0 -11
  71. package/dist/constraint-runtime/memdir/index.js +0 -12
  72. package/dist/constraint-runtime/migrations/index.d.ts +0 -11
  73. package/dist/constraint-runtime/migrations/index.js +0 -12
  74. package/dist/constraint-runtime/moreright/index.d.ts +0 -11
  75. package/dist/constraint-runtime/moreright/index.js +0 -12
  76. package/dist/constraint-runtime/native_ts/index.d.ts +0 -11
  77. package/dist/constraint-runtime/native_ts/index.js +0 -12
  78. package/dist/constraint-runtime/output_styles/index.d.ts +0 -11
  79. package/dist/constraint-runtime/output_styles/index.js +0 -12
  80. package/dist/constraint-runtime/plugins/index.d.ts +0 -11
  81. package/dist/constraint-runtime/plugins/index.js +0 -12
  82. package/dist/constraint-runtime/port_manifest.d.ts +0 -7
  83. package/dist/constraint-runtime/port_manifest.js +0 -11
  84. package/dist/constraint-runtime/query.d.ts +0 -6
  85. package/dist/constraint-runtime/query.js +0 -1
  86. package/dist/constraint-runtime/remote/index.d.ts +0 -11
  87. package/dist/constraint-runtime/remote/index.js +0 -12
  88. package/dist/constraint-runtime/schemas/index.d.ts +0 -11
  89. package/dist/constraint-runtime/schemas/index.js +0 -12
  90. package/dist/constraint-runtime/screens/index.d.ts +0 -11
  91. package/dist/constraint-runtime/screens/index.js +0 -12
  92. package/dist/constraint-runtime/server/index.d.ts +0 -11
  93. package/dist/constraint-runtime/server/index.js +0 -12
  94. package/dist/constraint-runtime/services/index.d.ts +0 -11
  95. package/dist/constraint-runtime/services/index.js +0 -12
  96. package/dist/constraint-runtime/state/index.d.ts +0 -11
  97. package/dist/constraint-runtime/state/index.js +0 -12
  98. package/dist/constraint-runtime/system_init.d.ts +0 -1
  99. package/dist/constraint-runtime/system_init.js +0 -20
  100. package/dist/constraint-runtime/types/index.d.ts +0 -11
  101. package/dist/constraint-runtime/types/index.js +0 -12
  102. package/dist/constraint-runtime/upstream_proxy/index.d.ts +0 -11
  103. package/dist/constraint-runtime/upstream_proxy/index.js +0 -12
  104. package/dist/constraint-runtime/utils/index.d.ts +0 -11
  105. package/dist/constraint-runtime/utils/index.js +0 -12
  106. package/dist/constraint-runtime/vim/index.d.ts +0 -11
  107. package/dist/constraint-runtime/vim/index.js +0 -12
  108. package/dist/constraint-runtime/voice/index.d.ts +0 -11
  109. package/dist/constraint-runtime/voice/index.js +0 -12
@@ -52,6 +52,8 @@ const fs = __importStar(require("fs/promises"));
52
52
  const fsSync = __importStar(require("fs"));
53
53
  const os = __importStar(require("os"));
54
54
  const path = __importStar(require("path"));
55
+ const run_context_js_1 = require("./run-context.js");
56
+ const channel_actor_js_1 = require("../kernel/channel-actor.js");
55
57
  const tool_path_args_js_1 = require("./tool-path-args.js");
56
58
  const background_notices_js_1 = require("./background-notices.js");
57
59
  const skill_ledger_js_1 = require("./skill-ledger.js");
@@ -260,7 +262,90 @@ class PiAgentSession {
260
262
  sessionManager;
261
263
  agentsManager;
262
264
  socialHeartbeat = null;
263
- messageHistory = [];
265
+ /**
266
+ * **K5 第 4 步 (所有权转移)**: history 的**本体**。
267
+ * · 未绑定 actor ⇒ 本体在这里 (绑定前的暂存);
268
+ * · 绑定 actor 后 ⇒ 本体是 `actor.state.messageHistory`, 本地那份被**收养**并清空。
269
+ * 绑定由 `attachActor()` 做, 且**只在会话身份已知时**发生 (见 session factory) —— 没有身份就不归属。
270
+ */
271
+ /**
272
+ * **K5 第 4 步 — history 写入的唯一漏斗 (push)**。
273
+ * 为什么必须同步: 调用点遍布 ReAct 循环 / 工具回灌 / 压缩, 而且**写完立刻读** (length / 索引 / slice),
274
+ * 改成 await 会改变同拍可见性。所以这里有意识地**不排队** —— 归属收敛 + 可数 (门能断言"零处直写"),
275
+ * 并发安全由入口投递进 mailbox 负责 (K5 第 5 步)。
276
+ */
277
+ pushHistory(...msgs) {
278
+ for (const m of msgs) {
279
+ // 步骤⑧ 起**只有一条路径**: 写 actor (实例侧暂存字段已删除) —— 也彻底消除了"自递归"的可能
280
+ this.actor.appendMessageSync(m);
281
+ }
282
+ }
283
+ /** **K5 第 4 步 — history 写入漏斗 (pop)** —— 同 pushHistory 的性质 */
284
+ popHistory() {
285
+ return this.actor.popMessageSync();
286
+ }
287
+ /** **K5 第 4 步 — history 写入漏斗 (整体替换)**: hydrate 回灌 / 压缩后的整块赋值都走这里 */
288
+ /**
289
+ * **整体替换**历史 —— 唯一漏斗之一, 步骤⑧ 起是**唯一**的"种历史"入口 (公开)。
290
+ * 公开的理由: 上层/测试要种一份历史时必须走这里; 直接给对象赋一个新数组会**换掉数组身份**
291
+ * (actor 里那份仍是空的) —— 那是静默丢数据 (实测: 删掉 setter 后 `session.messageHistory = [...]`
292
+ * 变成写在一个凭空出现的自有属性上, 测试于是看到 0 条)。
293
+ */
294
+ replaceHistory(next) {
295
+ this.actor.replaceHistory(next);
296
+ }
297
+ /**
298
+ * **K5 步骤⑦ — 把一次性请求里的绑定落到位** (入口不再散着设字段: 只交一个请求)。
299
+ * 写入都走访问器 ⇒ 本体落进 `actor.state` (步骤③ 起就是这样); 没给的不覆盖 (与环境变量"只覆盖显式给出的"同秩)。
300
+ */
301
+ applyExecutionRequest(req) {
302
+ if (req.channelId)
303
+ this.actor.state.channelId = req.channelId;
304
+ if (req.agentId)
305
+ this.actor.state.agentId = req.agentId;
306
+ if (req.goalId)
307
+ this.actor.state.goalBinding = req.goalId;
308
+ if (req.resumeRunId)
309
+ this.resumeRunId = req.resumeRunId;
310
+ }
311
+ /**
312
+ * **K5 步骤⑦ — Pi 的唯一执行入口**: 只接收一次性 `ExecutionRequest`。
313
+ * 有 `onStream` ⇒ 走 `promptStream` (流式); 否则走 `prompt`。绑定由 `applyExecutionRequest` 落位。
314
+ * 入口侧只需: `deliverThroughActor(session, () => session.runExecution(req))`。
315
+ */
316
+ async runExecution(req) {
317
+ this.applyExecutionRequest(req);
318
+ if (req.onStream) {
319
+ return this.promptStream(req.input, req.onStream, req.signal, req.channelId);
320
+ }
321
+ return this.prompt(req.input, { signal: req.signal, channelId: req.channelId });
322
+ }
323
+ /**
324
+ * **K5 第 4 步**: 绑定 actor 并**收养**绑定前已存在的本地历史。
325
+ * 收养规则: actor 侧为空 且 本地非空 ⇒ 搬过去 (任何绑定时序都不丢历史); 收养后清空本地。
326
+ * 约束 (全量回归打出来的): 调用方必须用**会话身份**当注册键; 没有身份就**不要调**这个方法。
327
+ */
328
+ attachActor(actor) {
329
+ const prev = this.actor;
330
+ this.actor = actor;
331
+ if (!prev || prev === actor)
332
+ return;
333
+ // **收养**: 把上一个 actor (通常是"出生时的私有 actor") 的状态搬进新家。
334
+ // 新家已有值时不覆盖 —— 同一会话身份以先到者为准 (禁止"后到的调用把先建的状态冲掉")。
335
+ const from = prev.state;
336
+ const to = actor.state;
337
+ if (from.messageHistory.length > 0 && to.messageHistory.length === 0) {
338
+ to.messageHistory.push(...from.messageHistory);
339
+ }
340
+ if (from.channelId && !to.channelId)
341
+ to.channelId = from.channelId;
342
+ if (from.agentId && !to.agentId)
343
+ to.agentId = from.agentId;
344
+ if (from.goalBinding && !to.goalBinding)
345
+ to.goalBinding = from.goalBinding;
346
+ if (from.activeRun && !to.activeRun)
347
+ to.activeRun = from.activeRun;
348
+ }
264
349
  tools = new Map();
265
350
  /** 2026-06-30: tool registry 模块 — 独立 alias resolve, 测试可消融. */
266
351
  _toolRegistry = new tool_registry_js_1.ToolRegistry();
@@ -423,11 +508,29 @@ class PiAgentSession {
423
508
  // 拼到 systemPrompt 末尾, 别再混进 user message
424
509
  contextHintAddition = '';
425
510
  /**
426
- * 当前 onStream 引用 + abort signal (computeJudgmentGate 需要 onStream 广播 phase)
427
- * 每次 prompt / promptStream / promptWithPivotLoop 入口设置, 用完即清
511
+ * **K2 收尾 (leo 2026-10-02 口径)**: **唯一一处**把"已有的恢复身份"播种进 RunContext。
512
+ *
513
+ * · `currentRunId` 属 **run-boundary**, 不是循环内部的可变 RunContext 状态 ⇒ 只允许这一次显式播种读取;
514
+ * · 冻结 `CURRENT_RUN_ID_SEED_READS = 1` (见 src/kernel/plan-runcontext.ts), 由门强制;
515
+ * · 只发生在**进入一次 Run 时**; 不得在循环中新增读取点; 不得成为新的写入口;
516
+ * · K5 Channel Actor 完成后改为 `createRunContext({ runId: request.resumeRunId })`, 届时删除 `currentRunId` 字段本体。
517
+ */
518
+ seedRunContext(extra = {}) {
519
+ return (0, run_context_js_1.createRunContext)({ runId: this.actor.state.activeRun, ...extra });
520
+ }
521
+ /**
522
+ * **K5 迁移中**: 本会话所属 channel 的 Actor (由 session factory 在创建时绑定)。
523
+ * 现在只做**归属** —— 会话状态 (messageHistory / channelId / agentId / goalId) **仍然**在实例字段上,
524
+ * 逐项迁入 Actor 见 `src/kernel/plan-channel-actor.ts` 的 8 步。
525
+ */
526
+ actor;
527
+ /**
528
+ * **K2 迁移中**: 一次 Run 的显式状态载体 (见 src/agents/run-context.ts)。
529
+ * 入口 (prompt / promptStream / promptWithPivotLoop) 用 createRunContext() 快照一次, 用完即清。
530
+ * **已外置**: `eventSink` (原 currentOnStream) · `abortSignal` (原 currentSignal)。
531
+ * 其余 6 个字段仍以本类实例字段为准, 逐格迁移。
428
532
  */
429
- currentOnStream = null;
430
- currentSignal = null;
533
+ runCtx = (0, run_context_js_1.createRunContext)();
431
534
  /** Bootstrap SessionStart 拼的 system prompt 片段 (用完即清) */
432
535
  bootstrapAddition = '';
433
536
  /** 2026-08-12 (Task3 认知卸载): 工具选择与认知卸载指南 — 注入 system prompt, 降低"模型不触发 write/edit/read"率. */
@@ -441,12 +544,17 @@ class PiAgentSession {
441
544
  - 每步只选一个最合适的工具, 先读后写, 写完验证 (跑 tsc/vitest 或读回文件).`;
442
545
  /** 当前 prompt 开始时间 (供 Stop hook 计算 durationMs) */
443
546
  promptStartTime = 0;
444
- /** 当前 channel id (由 getAgentForChannel / prompt 4 参注入, 供 hook / log 使用) */
445
- currentChannelId = '';
446
- /** 2026-07-04: 当前 agentId (server.ts 通过 createAgentSession 选项注入), 供 onSessionStart 加载 persona docs */
447
- currentAgentId = '';
448
- /** M2.2 (2026-06-17): 当前轮的用户请求 intent, runReActLoop 拼 systemPrompt 时会读这个 */
449
- currentIntent = 'chitchat';
547
+ /**
548
+ * **K5 步骤③ (会话绑定迁入 Actor)**: 三处绑定 (channelId / agentId / goalBinding) 的本体住进
549
+ * `actor.state`, 实例上只留**绑定前的暂存** (`_channelId` 等), 由 `attachActor()` 收养后清空。
550
+ * 用访问器而不是改调用点: 写入点 (prompt 入口 / Goal 绑定 / snapshot) 分散且语义各异, 逐个改风险大;
551
+ * 访问器让所有读写自动落到 actor (同一个值对象), 调用点零改动。
552
+ * 注意: 这三个字段的**访问数不变** (台账按 "self.currentX" 形态计数) —— 迁的是**所有权**, 不是删访问。
553
+ * 本注释刻意不写出那种带 `this.` 前缀的字面形态: 计数口径只剥 `//` 行注释, 块注释里的同形串会被算进去
554
+ * (曾因此把 currentChannelId 的计数虚增 1, 被门照出)。
555
+ */
556
+ // **K5 步骤⑧ 批次4**: channelId 的访问器已删 —— 本体 `actor.state.channelId`, 直接影响本体。
557
+ // M2.2 intent 已外置到 runCtx.intent (K2); 拼 systemPrompt 时读 this.runCtx.intent
450
558
  /** 2026-08-10: 本轮用户原始输入 (loop-review 任务动词兜底检测用) */
451
559
  currentUserInput = '';
452
560
  /**
@@ -454,7 +562,16 @@ class PiAgentSession {
454
562
  * 之前只有"跑完才写"的轨迹 (trajectory-store, fire-and-forget, 崩了就没有), 没有跨重载可见的
455
563
  * 运行事实, 也没有预算闸门/失速巡检; 这里补的就是那一层。
456
564
  */
457
- currentRunId = '';
565
+ /**
566
+ * **K5 步骤⑥ (Run 身份归属 Actor)**: 本体是 `actor.state.activeRun`。
567
+ * · 未绑定 actor ⇒ 住在这里 (`_runId`, 绑定前的暂存);
568
+ * · 绑定后 ⇒ 读写全落 actor (同一个值), 由 `attachActor()` 收养绑定前的值。
569
+ * 语义: "本会话当前活跃的 Run" —— 一个会话身份同时只有一个活跃 Run ✓ 与 Actor 的粒度一致。
570
+ * 注意: 访问数**不变** —— 迁的是**所有权**不是删访问 (与步骤③同理)。
571
+ * 本注释刻意不写出"带 self. 前缀 + 该字段名"的那种字面形态: K2 的计数只剥 `//` 行注释,
572
+ * 块注释里的同形串会被算进去 (同类踩过三次, 每次都被门照出)。
573
+ */
574
+ // **K5 步骤⑧ 批次5 (收尾)**: currentRunId 访问器已删 —— Run 身份的本体是 `actor.state.activeRun`, 直接读写本体。
458
575
  /** 上一次运行的 runId (收尾不清空; 见 getLastRunId) */
459
576
  lastRunId = '';
460
577
  runSurface = 'cli';
@@ -471,14 +588,14 @@ class PiAgentSession {
471
588
  currentIntentHint = '';
472
589
  /**
473
590
  * 算 judgment 注入门: 失败静默, 不阻塞主对话
474
- * 期间通过 currentOnStream 广播 phase 事件, 前端可显示 "正在检索判断力..." 状态
591
+ * 期间通过 runCtx.eventSink 广播 phase 事件, 前端可显示 "正在检索判断力..." 状态
475
592
  * 调用方负责用完即清 (judgmentGateAddition='')
476
593
  */
477
594
  async computeJudgmentGate(input) {
478
595
  const safePhase = (phase, extra = {}) => {
479
596
  try {
480
- if (this.currentOnStream) {
481
- this.currentOnStream({ type: 'phase', phase, ...extra, content: '' });
597
+ if (this.runCtx.eventSink) {
598
+ this.runCtx.eventSink({ type: 'phase', phase, ...extra, content: '' });
482
599
  }
483
600
  }
484
601
  catch { /* 静默 */ }
@@ -520,19 +637,23 @@ class PiAgentSession {
520
637
  this.judgmentGateNegativeUsedIds = [];
521
638
  }
522
639
  constructor(config) {
640
+ // **K5 步骤⑧**: actor 从出生就在 —— 工厂通常在**构造前**就把身份 actor 注进来 (`config.actor`);
641
+ // 没有身份时用一份**私有 actor** (不注册 ⇒ 谁也拿不到, 隔离性优先)。
642
+ // ⇒ 实例侧不再需要"绑定前暂存"字段 (那些字段本步删除)。
643
+ this.actor = config.actor ?? (0, channel_actor_js_1.createPrivateActor)();
523
644
  this.cwd = config.cwd;
524
645
  this.peerId = config.peerId || 'local';
525
646
  this.identity = config.identityDoc || this.createDefaultIdentity();
526
647
  this.minimaxAvailable = this.checkMinimax();
527
648
  // 2026-07-04: 透传 agentId (server.ts 通过 createAgentSession 选项注入)
528
- this.currentAgentId = config.agentId || '';
649
+ this.actor.state.agentId = config.agentId || '';
529
650
  // 2026-10-01 **身份自愈** (用户实测: get_identity 返回 "DID: " 空 + 各 channel 名字串台):
530
651
  // 工具上下文里的身份来自 this.identity, 而它可能是 config.identityDoc 与 session 实例身份的
531
652
  // 合并结果 —— **空 did 会盖掉真 did** ✗; 名字也可能缺失或被别处覆盖。
532
653
  // 规矩: 缺 did / 名字时, 按 **currentAgentId** 从真身份生成器补齐 (did:key + 落盘密钥),
533
654
  // 名字优先取该 agent 自己的 persona.json。绝不回落到全局/用户身份名。
534
655
  try {
535
- if (this.currentAgentId) {
656
+ if (this.actor.state.agentId) {
536
657
  const cur = this.identity || {};
537
658
  const curDid = String(cur.did || '');
538
659
  const badDid = !curDid || /^did:(local|pi):/i.test(curDid);
@@ -540,17 +661,17 @@ class PiAgentSession {
540
661
  // 2026-10-01 加固: **did 与本人密钥不符** 也算坏 —— 用户实测 get_identity 返回一个
541
662
  // 数据里根本不存在的 did (按 peerId 当 scope 新造的), 而它非空、非假值 ⇒ 上一版判据漏过 ✗。
542
663
  // 规矩: 只要 currentAgentId 有值, 身份的 did 就必须等于该 agent 自己的密钥 did。
543
- const mine = (0, agent_identity_js_1.loadOrCreateAgentIdentity)(this.currentAgentId);
664
+ const mine = (0, agent_identity_js_1.loadOrCreateAgentIdentity)(this.actor.state.agentId);
544
665
  const mismatch = !!mine?.did && !!curDid && curDid !== mine.did;
545
666
  if (badDid || missingName || mismatch) {
546
667
  if (badDid || mismatch) {
547
- console.warn(`[identity] 身份纠正: ${this.currentAgentId} 的 did ${curDid ? curDid.slice(0, 26) : '(空)'} ⇒ ${mine.did.slice(0, 26)} (以该 agent 自己的密钥为准)`);
668
+ console.warn(`[identity] 身份纠正: ${this.actor.state.agentId} 的 did ${curDid ? curDid.slice(0, 26) : '(空)'} ⇒ ${mine.did.slice(0, 26)} (以该 agent 自己的密钥为准)`);
548
669
  }
549
670
  // 2026-10-01: 同样必须**原地改** —— 这段在 registerTools() **之后**跑, 工具上下文已经按引用
550
671
  // 捕获了 this.identity; 换对象 ⇒ 工具看不到纠正结果 ✗ (同类 bug, 一并修)
551
672
  Object.assign(this.identity, {
552
673
  ...(mine?.did ? { did: mine.did, publicKey: mine.publicKey || cur.publicKey } : {}),
553
- ...(missingName ? { name: (0, channel_identity_js_1.agentPersonaName)(this.currentAgentId) || this.currentAgentId } : {}),
674
+ ...(missingName ? { name: (0, channel_identity_js_1.agentPersonaName)(this.actor.state.agentId) || this.actor.state.agentId } : {}),
554
675
  });
555
676
  }
556
677
  }
@@ -560,7 +681,7 @@ class PiAgentSession {
560
681
  this._sessionStore = config.sessionStore ?? session_store_js_1.sessionStore;
561
682
  this.constraintLayer = new constraint_layer_js_1.ConstraintLayer();
562
683
  this.workflowEngine = new workflow_engine_js_1.WorkflowEngine(this.constraintLayer);
563
- this.sessionManager = new pi_sdk_session_manager_js_2.PiSessionManager(this.identity.did, this.cwd, this.currentAgentId);
684
+ this.sessionManager = new pi_sdk_session_manager_js_2.PiSessionManager(this.identity.did, this.cwd, this.actor.state.agentId);
564
685
  this.agentsManager = new heartbeat_js_1.DiscoveredAgentsManager();
565
686
  this.usePivotLoop = config.usePivotLoop ?? false;
566
687
  this.pivotLoopConfig = config.pivotLoopConfig;
@@ -646,6 +767,18 @@ class PiAgentSession {
646
767
  */
647
768
  async hydrateMessageHistory(sessionKey, maxMessages) {
648
769
  try {
770
+ // **K5 第 4 步**: 实现体 (load → filter → 截断 → 替换) 已搬进 Actor —— 走邮箱 ⇒ 与 append/persist 串行。
771
+ // 业务侧只交两个**纯回调** (内核因此不必 import 业务模块); 未绑定 actor 的会话走下面原路径 (行为不变)。
772
+ if (this.actor) {
773
+ const n = await this.actor.hydrateHistory({
774
+ load: () => this._sessionStore.loadMessages(sessionKey),
775
+ filter: (loaded) => this._filterToMessage(loaded),
776
+ maxMessages,
777
+ });
778
+ if (n > 0)
779
+ console.log(`[PiAgent] 从 ${sessionKey} 回灌 ${n} 条历史 (经 Channel Actor)`);
780
+ return;
781
+ }
649
782
  const loaded = await this._sessionStore.loadMessages(sessionKey);
650
783
  if (!loaded) {
651
784
  console.log(`[PiAgent] hydrate: 没有 ${sessionKey} 的历史`);
@@ -653,7 +786,7 @@ class PiAgentSession {
653
786
  }
654
787
  const hydrated = this._filterToMessage(loaded).slice(-maxMessages);
655
788
  if (hydrated.length > 0) {
656
- this.messageHistory = hydrated;
789
+ this.replaceHistory(hydrated);
657
790
  console.log(`[PiAgent] 从 ${sessionKey} 回灌 ${hydrated.length} 条历史`);
658
791
  }
659
792
  }
@@ -673,7 +806,10 @@ class PiAgentSession {
673
806
  * 即可获得"重启 / 跨进程接续"的语义.
674
807
  */
675
808
  async saveCurrentSession(key) {
676
- const persisted = this.messageHistory.map((m) => ({
809
+ // **K5 第 4 步**: 取数拍走 Actor 的快照 (走邮箱 ⇒ 与 append 串行, 不会抓到"边写边读"的半截状态);
810
+ // 未绑定 actor 的会话走原路径 (行为不变)。
811
+ const source = this.actor ? await this.actor.historySnapshot() : this.actor.state.messageHistory;
812
+ const persisted = source.map((m) => ({
677
813
  role: m.role,
678
814
  content: m.content,
679
815
  toolCall: m.toolCall,
@@ -690,9 +826,9 @@ class PiAgentSession {
690
826
  * 与 loadSessionKey (构造时读) 不同: 这个是 session 已建好后再读.
691
827
  */
692
828
  async resumeSession(key, maxMessages = 30) {
693
- const before = this.messageHistory.length;
829
+ const before = this.actor.state.messageHistory.length;
694
830
  await this.hydrateMessageHistory(key, maxMessages);
695
- return this.messageHistory.length - before;
831
+ return this.actor.state.messageHistory.length - before;
696
832
  }
697
833
  /**
698
834
  * 2026-06-30: 读历史不修改 messageHistory.
@@ -864,7 +1000,7 @@ class PiAgentSession {
864
1000
  const { CHANNELS_PATH } = await Promise.resolve().then(() => __importStar(require('../web/server-types.js')));
865
1001
  const { loadChannels } = await Promise.resolve().then(() => __importStar(require('../web/server-storage.js')));
866
1002
  const channels = await loadChannels();
867
- const ch = channels.find((c) => c.id === this.currentChannelId);
1003
+ const ch = channels.find((c) => c.id === this.actor.state.channelId);
868
1004
  if (ch && ch.encryptedPrivateKey && ch.encryptedPrivateKeyIv && ch.walletAddress) {
869
1005
  return {
870
1006
  encryptedPrivateKey: ch.encryptedPrivateKey,
@@ -939,7 +1075,7 @@ class PiAgentSession {
939
1075
  // 2026-10-01: 原先自造 `did:pi:<peerId>` —— **不是有效 DID** (仓里 server.ts 自己都把 did:pi:
940
1076
  // 当"待升级"占位; 用户实测 get_identity 报 did:pi:ch_1785668060213)。
941
1077
  // 改用仓里既有的真身份生成器 (agent-identity.loadOrCreateAgentIdentity, 同步, 产 did:key + 落盘密钥)。
942
- const scope = this.currentAgentId || this.peerId || 'default';
1078
+ const scope = this.actor.state.agentId || this.peerId || 'default';
943
1079
  try {
944
1080
  const real = (0, agent_identity_js_1.loadOrCreateAgentIdentity)(scope);
945
1081
  return {
@@ -998,9 +1134,9 @@ class PiAgentSession {
998
1134
  const did = await resolveUserDid();
999
1135
  const model = this.llmConfig?.model || this.model || '';
1000
1136
  return new TrajectoryRecorder({
1001
- agentId: this.currentAgentId || 'default',
1137
+ agentId: this.actor.state.agentId || 'default',
1002
1138
  input,
1003
- channelId: channelId || this.currentChannelId,
1139
+ channelId: channelId || this.actor.state.channelId,
1004
1140
  did: did || undefined,
1005
1141
  model: typeof model === 'string' && model ? model : undefined,
1006
1142
  });
@@ -1042,8 +1178,8 @@ class PiAgentSession {
1042
1178
  if (gate !== 'ready') {
1043
1179
  const why = `初始化未就绪 (${gate}, 当前阶段 ${state.stage})${state.lastError ? ` — ${state.lastError.message}` : ''}`;
1044
1180
  const hint = (state.actions && state.actions[0]) || '运行 `bolloon setup` 完成初始化';
1045
- this.messageHistory.push({ role: 'user', content: input });
1046
- this.messageHistory.push({ role: 'assistant', content: `[初始化未就绪] ${why}\n${hint}` });
1181
+ this.pushHistory({ role: 'user', content: input });
1182
+ this.pushHistory({ role: 'assistant', content: `[初始化未就绪] ${why}\n${hint}` });
1047
1183
  console.warn(`[PiAgent] 拒绝执行: ${why}`);
1048
1184
  return `[初始化未就绪] ${why}\n${hint}`;
1049
1185
  }
@@ -1051,13 +1187,13 @@ class PiAgentSession {
1051
1187
  catch { /* 门禁自身异常 → 按"不能判定"处理会阻塞一切; 这里只在能读到状态时才拦 */ }
1052
1188
  }
1053
1189
  this.minimaxAvailable = this.checkMinimax();
1054
- this.currentChannelId = options?.channelId ?? this.currentChannelId;
1190
+ this.actor.state.channelId = options?.channelId ?? this.actor.state.channelId;
1055
1191
  // 2026-08-08: 运行轨迹采集 (落盘 + OrbitDB, 失败静默) — 包裹 onStream 收集步骤事件
1056
1192
  const trajRec = await this.createTrajectoryRecorder(input, options?.channelId);
1057
1193
  if (trajRec && options?.onStream) {
1058
1194
  options = { ...options, onStream: this.wrapTrajectoryStream(options.onStream, trajRec) };
1059
1195
  }
1060
- this.messageHistory.push({
1196
+ this.pushHistory({
1061
1197
  role: 'user',
1062
1198
  content: input
1063
1199
  });
@@ -1067,23 +1203,23 @@ class PiAgentSession {
1067
1203
  // 于是把"什么都没跑"当成一次正常执行 (这正是"agent 跑了但没记录"的老毛病在 fallback 路径的翻版)。
1068
1204
  const response = await this.handleFallback(input);
1069
1205
  try {
1070
- let boundGoalId = this.currentGoalId;
1206
+ let boundGoalId = this.actor.state.goalBinding;
1071
1207
  if (!boundGoalId) {
1072
1208
  const g = await (0, goal_store_js_1.createGoal)({
1073
1209
  objective: this.currentUserInput || input.slice(0, 200),
1074
- channelId: this.currentChannelId || undefined,
1075
- agentId: this.currentAgentId || undefined,
1210
+ channelId: this.actor.state.channelId || undefined,
1211
+ agentId: this.actor.state.agentId || undefined,
1076
1212
  createdBy: this.runSurface,
1077
1213
  });
1078
1214
  boundGoalId = g.goalId;
1079
1215
  }
1080
- this.currentGoalId = boundGoalId;
1216
+ this.actor.state.goalBinding = boundGoalId;
1081
1217
  const rec = await (0, run_store_js_1.startRun)({
1082
1218
  surface: this.runSurface,
1083
1219
  goal: this.currentUserInput || input.slice(0, 200),
1084
1220
  goalId: boundGoalId,
1085
- channelId: this.currentChannelId || undefined,
1086
- agentId: this.currentAgentId || undefined,
1221
+ channelId: this.actor.state.channelId || undefined,
1222
+ agentId: this.actor.state.agentId || undefined,
1087
1223
  modelConfig: await this.runModelSnapshot(),
1088
1224
  });
1089
1225
  this.lastRunId = rec.runId;
@@ -1122,25 +1258,27 @@ class PiAgentSession {
1122
1258
  catch (err) {
1123
1259
  await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.fallbackRun', runId: this.lastRunId || '', message: String(err?.message || err).slice(0, 160) }).catch(() => { });
1124
1260
  }
1125
- this.messageHistory.push({ role: 'assistant', content: response });
1261
+ this.pushHistory({ role: 'assistant', content: response });
1126
1262
  this.reportUsageToContextManager();
1127
1263
  this.finishTrajectory(trajRec, response);
1128
1264
  return response;
1129
1265
  }
1130
1266
  // P0 注入门
1131
- this.currentSignal = options?.signal ?? null;
1132
- this.currentOnStream = options?.onStream ?? null;
1267
+ // K2: eventSink + abortSignal 已外置, 入口一次建好本轮 Context。
1268
+ // 其余字段**不在这里抄一份** —— 抄写会新增对旧实例字段的读, 违反方向判据
1269
+ // 「新层出现后旧写口调用点数只许不变或减少」。逐字段迁移时再各自搬进来。
1270
+ this.runCtx = this.seedRunContext({ eventSink: options?.onStream ?? null, abortSignal: options?.signal ?? null });
1133
1271
  await this.computeJudgmentGate(input);
1134
1272
  // M2.2 (2026-06-17): intent 分类 — prompt() 路径也跑 (跟 promptStream 对齐)
1135
1273
  try {
1136
1274
  const { classifyIntent, intentHint } = await Promise.resolve().then(() => __importStar(require('./intent-classifier.js')));
1137
- this.currentIntent = classifyIntent(input);
1138
- this.currentIntentHint = intentHint(this.currentIntent);
1275
+ this.runCtx.intent = classifyIntent(input);
1276
+ this.currentIntentHint = intentHint(this.runCtx.intent);
1139
1277
  this.currentUserInput = input;
1140
1278
  }
1141
1279
  catch (err) {
1142
1280
  console.warn('[PiAgent] classifyIntent in prompt() failed:', err);
1143
- this.currentIntent = 'chitchat';
1281
+ this.runCtx.intent = 'chitchat';
1144
1282
  this.currentIntentHint = '';
1145
1283
  }
1146
1284
  // P2: 解析当前 permission mode
@@ -1165,14 +1303,13 @@ class PiAgentSession {
1165
1303
  (0, injection_gate_js_1.recordJudgmentUsage)(this.judgmentGateUsedIds, { userInput: input }).catch((err) => console.warn('[PiAgent] recordJudgmentUsage failed:', err));
1166
1304
  }
1167
1305
  this.clearJudgmentGate();
1168
- this.currentSignal = null;
1169
- this.currentOnStream = null;
1306
+ this.runCtx = (0, run_context_js_1.createRunContext)(); // K2: 用完即清 (换新 Context, 不继承残留);
1170
1307
  this.reportUsageToContextManager();
1171
1308
  }
1172
1309
  }
1173
1310
  try {
1174
1311
  // 2026-06-16: runReActLoop 现在返回 { reply, aiFailed, aiFailureReason } — 这里只需 reply 字符串
1175
- const loopResult = await this.runReActLoop(this.currentOnStream ?? undefined, options?.signal);
1312
+ const loopResult = await this.runReActLoop(this.runCtx.eventSink ?? undefined, options?.signal);
1176
1313
  // 2026-10-01: **本回合改过 TS ⇒ 收尾自动类型检查** (一轮一次) —— 复用已有的 tsc_check 工具
1177
1314
  if ((0, code_write_gate_js_1.decideTypecheck)(this.tsTouchedThisTurn, this.typecheckRanThisTurn)) {
1178
1315
  this.typecheckRanThisTurn = true;
@@ -1181,11 +1318,11 @@ class PiAgentSession {
1181
1318
  if (tscTool?.execute) {
1182
1319
  const r = await tscTool.execute({});
1183
1320
  const line = (0, code_write_gate_js_1.formatTypecheckResult)(r?.success !== false && !/error TS\d+/.test(String(r?.output || '')), String(r?.output || ''));
1184
- this.currentOnStream?.({ type: 'status', content: `🔎 ${line}`, tool: 'system' });
1321
+ this.runCtx.eventSink?.({ type: 'status', content: `🔎 ${line}`, tool: 'system' });
1185
1322
  }
1186
1323
  }
1187
1324
  catch (e) {
1188
- this.currentOnStream?.({ type: 'status', content: `🔎 类型检查没能跑起来: ${String(e?.message || e).slice(0, 100)} (改动已落盘, 记得自己跑一次)`, tool: 'system' });
1325
+ this.runCtx.eventSink?.({ type: 'status', content: `🔎 类型检查没能跑起来: ${String(e?.message || e).slice(0, 100)} (改动已落盘, 记得自己跑一次)`, tool: 'system' });
1189
1326
  }
1190
1327
  this.tsTouchedThisTurn = [];
1191
1328
  }
@@ -1217,7 +1354,7 @@ class PiAgentSession {
1217
1354
  }
1218
1355
  catch { /* 落日志失败不影响主流程 */ }
1219
1356
  try {
1220
- this.currentOnStream?.({ type: 'status', content: `📚 复盘: ${m}`, tool: 'system' });
1357
+ this.runCtx.eventSink?.({ type: 'status', content: `📚 复盘: ${m}`, tool: 'system' });
1221
1358
  }
1222
1359
  catch { /* 上屏失败不打断 */ }
1223
1360
  };
@@ -1284,8 +1421,7 @@ class PiAgentSession {
1284
1421
  (0, injection_gate_js_1.recordJudgmentUsage)(this.judgmentGateNegativeUsedIds, { userInput: input, polarity: 'negative' }).catch((err) => console.warn('[PiAgent] recordJudgmentUsage (negative) failed:', err));
1285
1422
  }
1286
1423
  this.clearJudgmentGate();
1287
- this.currentSignal = null;
1288
- this.currentOnStream = null;
1424
+ this.runCtx = (0, run_context_js_1.createRunContext)(); // K2: 用完即清 (换新 Context, 不继承残留);
1289
1425
  this.reportUsageToContextManager();
1290
1426
  }
1291
1427
  }
@@ -1293,7 +1429,7 @@ class PiAgentSession {
1293
1429
  console.log(`[PiAgent.promptStream] ENTRY, channelId=${channelId}, input chars=${input.length}`);
1294
1430
  this.minimaxAvailable = this.checkMinimax();
1295
1431
  console.log(`[PiAgent.promptStream] minimaxAvailable=${this.minimaxAvailable}`);
1296
- this.currentChannelId = channelId ?? this.currentChannelId;
1432
+ this.actor.state.channelId = channelId ?? this.actor.state.channelId;
1297
1433
  // 2026-08-08: 运行轨迹采集 (落盘 + OrbitDB, 失败静默) — 包裹 onStream 收集步骤事件
1298
1434
  const trajRec = await this.createTrajectoryRecorder(input, channelId);
1299
1435
  if (trajRec)
@@ -1306,7 +1442,7 @@ class PiAgentSession {
1306
1442
  const userText = markerMatch ? markerMatch[1].trim() : input;
1307
1443
  const contextHint = markerMatch ? input.replace(markerMatch[0], '').trim() : '';
1308
1444
  console.log(`[PiAgent.promptStream] marker matched=${!!markerMatch}, userText chars=${userText.length}, contextHint chars=${contextHint.length}`);
1309
- this.messageHistory.push({
1445
+ this.pushHistory({
1310
1446
  role: 'user',
1311
1447
  content: userText
1312
1448
  });
@@ -1316,7 +1452,7 @@ class PiAgentSession {
1316
1452
  // 让 agent 能"回忆起"之前 session 的记忆 (自动获取之前 session), 而非只靠启动时批量压缩.
1317
1453
  try {
1318
1454
  const { recallMemory } = await Promise.resolve().then(() => __importStar(require('./memory-recall.js')));
1319
- const recalled = await recallMemory({ query: userText, agentId: this.currentAgentId || this.peerId || '' });
1455
+ const recalled = await recallMemory({ query: userText, agentId: this.actor.state.agentId || this.peerId || '' });
1320
1456
  if (recalled) {
1321
1457
  this.contextHintAddition = [this.contextHintAddition, recalled].filter(Boolean).join('\n\n');
1322
1458
  }
@@ -1325,29 +1461,29 @@ class PiAgentSession {
1325
1461
  onStream({ type: 'thinking', content: '🤔 开始思考...' });
1326
1462
  if (!this.minimaxAvailable) {
1327
1463
  const response = await this.handleFallback(userText);
1328
- this.messageHistory.push({ role: 'assistant', content: response });
1464
+ this.pushHistory({ role: 'assistant', content: response });
1329
1465
  onStream({ type: 'done', content: '' });
1330
1466
  this.reportUsageToContextManager();
1331
1467
  this.finishTrajectory(trajRec, response);
1332
1468
  return response;
1333
1469
  }
1334
- // P0 注入门: 缓存 onStream + signal, computeJudgmentGate 用 currentOnStream 广播 phase
1335
- this.currentOnStream = onStream;
1336
- this.currentSignal = signal ?? null;
1470
+ // P0 注入门: 缓存 onStream + signal, computeJudgmentGate 用 runCtx.eventSink 广播 phase
1471
+ // K2: eventSink + abortSignal 一起建进本轮 Context (只搬已外置的字段, 不抄未迁移的)
1472
+ this.runCtx = this.seedRunContext({ eventSink: onStream, abortSignal: signal ?? null });
1337
1473
  await this.computeJudgmentGate(userText);
1338
1474
  // M2.2 (2026-06-17): intent 分类 — 0 LLM 成本, 5 行 keyword 匹配
1339
1475
  try {
1340
1476
  const { classifyIntent, intentHint } = await Promise.resolve().then(() => __importStar(require('./intent-classifier.js')));
1341
- this.currentIntent = classifyIntent(userText);
1342
- this.currentIntentHint = intentHint(this.currentIntent);
1477
+ this.runCtx.intent = classifyIntent(userText);
1478
+ this.currentIntentHint = intentHint(this.runCtx.intent);
1343
1479
  this.currentUserInput = userText;
1344
- if (this.currentIntent !== 'chitchat') {
1345
- onStream({ type: 'phase', phase: 'intent_classified', detail: this.currentIntent, content: '' });
1480
+ if (this.runCtx.intent !== 'chitchat') {
1481
+ onStream({ type: 'phase', phase: 'intent_classified', detail: this.runCtx.intent, content: '' });
1346
1482
  }
1347
1483
  }
1348
1484
  catch (err) {
1349
1485
  console.warn('[PiAgent] classifyIntent failed (non-fatal):', err);
1350
- this.currentIntent = 'chitchat';
1486
+ this.runCtx.intent = 'chitchat';
1351
1487
  this.currentIntentHint = '';
1352
1488
  }
1353
1489
  // P1.1: 异步跑 Auto-Compact (LLM 摘要, 仅在 budget 超限时触发, 失败静默)
@@ -1364,8 +1500,8 @@ class PiAgentSession {
1364
1500
  let bootstrapAddition = '';
1365
1501
  try {
1366
1502
  const ss = await (0, human_value_pipeline_js_1.onSessionStart)({
1367
- channelId: this.currentChannelId || undefined,
1368
- agentId: this.currentAgentId || undefined,
1503
+ channelId: this.actor.state.channelId || undefined,
1504
+ agentId: this.actor.state.agentId || undefined,
1369
1505
  });
1370
1506
  bootstrapAddition = ss.systemAddition || '';
1371
1507
  }
@@ -1374,10 +1510,10 @@ class PiAgentSession {
1374
1510
  }
1375
1511
  // 2026-07-07 P1-B: 注入最近 5 条项目事件日志 (L2) — 让 LLM 知道项目状态/feature 变化
1376
1512
  // 失败静默, append 到 bootstrapAddition 末尾 (超 800 字截断)
1377
- if (this.currentChannelId) {
1513
+ if (this.actor.state.channelId) {
1378
1514
  try {
1379
1515
  const { getRecentEvents } = await Promise.resolve().then(() => __importStar(require('../bootstrap/event-log.js')));
1380
- const events = await getRecentEvents(this.currentChannelId, 5);
1516
+ const events = await getRecentEvents(this.actor.state.channelId, 5);
1381
1517
  if (events.length > 0) {
1382
1518
  const eventBlock = [
1383
1519
  '## 最近项目事件 (最近 5 条, 倒序)',
@@ -1392,7 +1528,7 @@ class PiAgentSession {
1392
1528
  // 2026-07-07 P2-C: 注入项目当前状态 (L3) — 目标/约束/待办/已完成
1393
1529
  try {
1394
1530
  const { readState, formatStateForPrompt } = await Promise.resolve().then(() => __importStar(require('../bootstrap/project-state.js')));
1395
- const state = await readState({ channelId: this.currentChannelId });
1531
+ const state = await readState({ channelId: this.actor.state.channelId });
1396
1532
  const stateText = formatStateForPrompt(state);
1397
1533
  if (stateText) {
1398
1534
  bootstrapAddition = (bootstrapAddition + '\n\n' + stateText).slice(-2500);
@@ -1404,7 +1540,7 @@ class PiAgentSession {
1404
1540
  // 2026-07-07 P2-C: 向量检索 top-3 (L4) — 按当前 channelId + userText 找历史相关片段
1405
1541
  try {
1406
1542
  const { searchIndex } = await Promise.resolve().then(() => __importStar(require('../bootstrap/vector-index.js')));
1407
- const indexName = `channel-${this.currentChannelId}`;
1543
+ const indexName = `channel-${this.actor.state.channelId}`;
1408
1544
  const results = await searchIndex({
1409
1545
  indexName,
1410
1546
  query: userText,
@@ -1465,13 +1601,12 @@ class PiAgentSession {
1465
1601
  (0, monitor_gate_js_1.monitorAfterReply)(userText, pivotResult);
1466
1602
  const stopStartTime = this.promptStartTime || Date.now();
1467
1603
  (0, human_value_pipeline_js_1.onStop)({
1468
- channelId: this.currentChannelId || 'unknown',
1604
+ channelId: this.actor.state.channelId || 'unknown',
1469
1605
  durationMs: Date.now() - stopStartTime,
1470
1606
  usedJudgmentIds: [...this.judgmentGateUsedIds],
1471
1607
  }).catch((err) => console.warn('[PiAgent] onStop failed:', err));
1472
1608
  this.clearJudgmentGate();
1473
- this.currentOnStream = null;
1474
- this.currentSignal = null;
1609
+ this.runCtx = (0, run_context_js_1.createRunContext)(); // K2: 用完即清 (换新 Context, 不继承残留);
1475
1610
  this.bootstrapAddition = '';
1476
1611
  this.contextHintAddition = '';
1477
1612
  this.promptStartTime = 0;
@@ -1501,8 +1636,7 @@ class PiAgentSession {
1501
1636
  }
1502
1637
  catch (err) {
1503
1638
  // abort 失败: 视作"已中断", 抛错让上层用 partial 兜底
1504
- this.currentOnStream = null;
1505
- this.currentSignal = null;
1639
+ this.runCtx = (0, run_context_js_1.createRunContext)(); // K2: 用完即清 (换新 Context, 不继承残留);
1506
1640
  throw err;
1507
1641
  }
1508
1642
  attempt++;
@@ -1540,8 +1674,8 @@ class PiAgentSession {
1540
1674
  // 因为 messageHistory.push({role:'user'}) 在 promptStream 顶部已经做过, 重跑 runReActLoop 不会重复 push,
1541
1675
  // 但 assistant 失败那条也别留 (留了会污染下一轮 LLM context).
1542
1676
  // 简化: 重试前 pop 一次 assistant (如果最后一条是 assistant)
1543
- if (this.messageHistory.length > 0 && this.messageHistory[this.messageHistory.length - 1].role === 'assistant') {
1544
- this.messageHistory.pop();
1677
+ if (this.actor.state.messageHistory.length > 0 && this.actor.state.messageHistory[this.actor.state.messageHistory.length - 1].role === 'assistant') {
1678
+ this.popHistory();
1545
1679
  }
1546
1680
  }
1547
1681
  onStream({ type: 'done', content: '' });
@@ -1559,15 +1693,14 @@ class PiAgentSession {
1559
1693
  // Bootstrap Stop hook: fire-and-forget 写本次 session 摘要
1560
1694
  const stopStartTime = this.promptStartTime || Date.now();
1561
1695
  (0, human_value_pipeline_js_1.onStop)({
1562
- channelId: this.currentChannelId || 'unknown',
1696
+ channelId: this.actor.state.channelId || 'unknown',
1563
1697
  durationMs: Date.now() - stopStartTime,
1564
1698
  usedJudgmentIds: [...this.judgmentGateUsedIds],
1565
1699
  }).catch((err) => console.warn('[PiAgent] onStop failed:', err));
1566
1700
  // 用完即清, 避免污染下一轮
1567
1701
  this.clearJudgmentGate();
1568
- this.currentOnStream = null;
1702
+ this.runCtx = (0, run_context_js_1.createRunContext)(); // K2: 用完即清 (换新 Context, 不继承残留);
1569
1703
  this.reportUsageToContextManager();
1570
- this.currentSignal = null;
1571
1704
  this.bootstrapAddition = '';
1572
1705
  this.promptStartTime = 0;
1573
1706
  // 2026-08-08: 轨迹收尾 — 落盘 + OrbitDB (失败静默, 不阻塞回复)
@@ -1575,7 +1708,7 @@ class PiAgentSession {
1575
1708
  return result;
1576
1709
  }
1577
1710
  async promptWithPivotLoop(input, config, channelId) {
1578
- this.currentChannelId = channelId ?? this.currentChannelId;
1711
+ this.actor.state.channelId = channelId ?? this.actor.state.channelId;
1579
1712
  if (!this.minimaxAvailable) {
1580
1713
  const response = await this.handleFallback(input);
1581
1714
  return {
@@ -1607,8 +1740,8 @@ class PiAgentSession {
1607
1740
  // M2.2 (2026-06-17): intent 分类 — pivot loop 也要拿到 hint
1608
1741
  try {
1609
1742
  const { classifyIntent, intentHint } = await Promise.resolve().then(() => __importStar(require('./intent-classifier.js')));
1610
- this.currentIntent = classifyIntent(input);
1611
- this.currentIntentHint = intentHint(this.currentIntent);
1743
+ this.runCtx.intent = classifyIntent(input);
1744
+ this.currentIntentHint = intentHint(this.runCtx.intent);
1612
1745
  this.currentUserInput = input;
1613
1746
  }
1614
1747
  catch (err) {
@@ -1655,21 +1788,21 @@ ${await this.renderActivePlansSection()}
1655
1788
  - 工具调用前可以简短思考 (1-2 句话), 但**不要写长篇 thinking** (会撞 max_tokens)
1656
1789
  - 工具调用后必须等结果, 不要在同一个回复里继续输出
1657
1790
  - <final gen> 只在**真完成所有任务**时输出, 不要在工具调用前/中输出${this.judgmentGateAddition}${this.contextHintAddition}`;
1658
- // 2026-06-15: 把 currentOnStream 传给 loop, 让 step-timeline 在 pivot 循环里也能 emit step_start/done
1791
+ // 2026-06-15: 把 runCtx.eventSink 传给 loop, 让 step-timeline 在 pivot 循环里也能 emit step_start/done
1659
1792
  // 之前 loop.execute() 不接 streamCallback, 导致 step-timeline 只能看到老 runReActLoop 路径
1660
1793
  // promptWithPivotLoop 路径 0 step events — UI 显示 timeline 但永远是空
1661
1794
  // 2026-06-17: 透传 signal 让 abort 工作 — loop.execute() 当前不接 signal 参数,
1662
1795
  // 所以 abort 行为通过 this.currentSignal 共享给 loop 内部读 (后续 M3.2 接 task plan 时一起加)
1663
1796
  // 2026-07-06: pivot 内 token 累计到 ~70% 时回调 (workflow-pivot-loop.ts line 270+).
1664
- // pivot 的 messageHistory 是 process-local, 不和 pi-sdk 的 this.messageHistory 同步.
1797
+ // pivot 的 messageHistory 是 process-local, 不和 pi-sdk 的 actor 的 messageHistory 同步.
1665
1798
  // 真正折叠需要把 pi-sdk 历史灌回 pivot 的 history 数组 — 侵入较大.
1666
1799
  // 当前 priority: 临时传空实现, 让 budget 公式放够 (workflow-pivot-loop.ts line 220)
1667
1800
  // 不再撞预算. 这条路径留作技术债.
1668
1801
  // 2026-07-17 Bug 1 修: 注入 messageHistory (hydrateMessageHistory 从 session JSON 回灌的) 到 system prompt
1669
- // pivot loop execute() 内部自己维护 messageHistory, 跟 pi-sdk 的 this.messageHistory 隔离,
1802
+ // pivot loop execute() 内部自己维护 messageHistory, 跟 pi-sdk 的 actor 的 messageHistory 隔离,
1670
1803
  // 不注入的话 LLM 看不到历史对话, 每次都是新对话.
1671
1804
  const historyLines = [];
1672
- const historyToInject = this.messageHistory.slice(-20, -1);
1805
+ const historyToInject = this.actor.state.messageHistory.slice(-20, -1);
1673
1806
  for (const m of historyToInject) {
1674
1807
  const roleLabel = m.role === 'user' ? '用户' : m.role === 'assistant' ? '你' : m.role === 'tool' ? '工具结果' : m.role;
1675
1808
  const text = (m.content || '').slice(0, 2000);
@@ -1682,9 +1815,9 @@ ${await this.renderActivePlansSection()}
1682
1815
  const onCompact = async () => {
1683
1816
  // no-op (best-effort hook for future pi-sdk/pivot history sync)
1684
1817
  };
1685
- const result = await loop.execute(input, llm, systemPrompt + historyBlock, this.currentOnStream ?? undefined, this.currentSignal ?? undefined, onCompact);
1818
+ const result = await loop.execute(input, llm, systemPrompt + historyBlock, this.runCtx.eventSink ?? undefined, this.runCtx.abortSignal ?? undefined, onCompact);
1686
1819
  if (result.response) {
1687
- this.messageHistory.push({ role: 'assistant', content: result.response });
1820
+ this.pushHistory({ role: 'assistant', content: result.response });
1688
1821
  }
1689
1822
  // 回溯 + 清场
1690
1823
  if (this.judgmentGateUsedIds.length > 0) {
@@ -1700,7 +1833,9 @@ ${await this.renderActivePlansSection()}
1700
1833
  */
1701
1834
  _harness = null;
1702
1835
  /** M2 绑定 GoalStore 后填真值; 在此之前为空 (事件里 goalId 字段已就位) */
1703
- currentGoalId = '';
1836
+ // **K5 步骤③/⑧**: 会话的 Goal 默认绑定本体在 `actor.state.goalBinding` (访问器已删, 直接读本体)。
1837
+ // leo 口径不变: 这只是默认值; 每次执行开始必须把最终绑定写进 RunContext/Run 记录,
1838
+ // 运行中重绑必须走显式 Goal Binding 操作, 不许靠裸字段隐式生效。
1704
1839
  /** 2026-09-16 (M2): 本次执行是"从 checkpoint 恢复"的 runId (非空 = 恢复模式, 不再新建 run) */
1705
1840
  resumeRunId = '';
1706
1841
  resumePlan = null;
@@ -1723,7 +1858,7 @@ ${await this.renderActivePlansSection()}
1723
1858
  }
1724
1859
  /** CLI / Web 注入"当前目标" (有 goalId 就在该 Goal 下执行) */
1725
1860
  setGoalId(goalId) {
1726
- this.currentGoalId = String(goalId || '');
1861
+ this.actor.state.goalBinding = String(goalId || '');
1727
1862
  }
1728
1863
  /** 核心状态迁移的兜底: 失败 → 记降级 + 交给循环顶部的持久化硬闸 (不吞错) */
1729
1864
  async safeSetRunStatus(runId, to) {
@@ -1742,13 +1877,13 @@ ${await this.renderActivePlansSection()}
1742
1877
  * 这是"错误恢复闭环"的运行时接线点 (此前 recordRecovery / repeatedFailureCount 只是数据结构)。
1743
1878
  */
1744
1879
  async wireToolFailure(tool, errorText, args) {
1745
- if (!this.currentRunId)
1880
+ if (!this.actor.state.activeRun)
1746
1881
  return;
1747
1882
  const cls = (0, run_store_js_1.classifyError)(errorText);
1748
1883
  const digest = (0, run_store_js_1.argsDigestOf)(args);
1749
1884
  let repeats = 1;
1750
1885
  try {
1751
- const rec = await (0, run_store_js_1.readRun)(this.currentRunId);
1886
+ const rec = await (0, run_store_js_1.readRun)(this.actor.state.activeRun);
1752
1887
  // recordStep 已经把**这次**失败记进去了, 所以"连续失败次数"直接就是当前值 (不再 +1)
1753
1888
  const seen = rec ? (0, run_store_js_1.repeatedFailureCount)(rec, tool, digest) : 0;
1754
1889
  repeats = Math.max(seen, 1);
@@ -1759,7 +1894,7 @@ ${await this.renderActivePlansSection()}
1759
1894
  : cls === 'external_no_reply' ? 'pause'
1760
1895
  : 'retry';
1761
1896
  try {
1762
- await (0, run_store_js_1.recordRecovery)(this.currentRunId, {
1897
+ await (0, run_store_js_1.recordRecovery)(this.actor.state.activeRun, {
1763
1898
  errorClass: cls,
1764
1899
  message: `${tool}: ${errorText}`.slice(0, 200),
1765
1900
  action,
@@ -1768,20 +1903,20 @@ ${await this.renderActivePlansSection()}
1768
1903
  });
1769
1904
  }
1770
1905
  catch (err) {
1771
- await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.recordRecovery', runId: this.currentRunId, message: String(err?.message || err) }).catch(() => { });
1906
+ await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.recordRecovery', runId: this.actor.state.activeRun, message: String(err?.message || err) }).catch(() => { });
1772
1907
  }
1773
1908
  // 外部无响应 → awaiting_external (不算失败; 等回话后回到 running)
1774
1909
  if (cls === 'external_no_reply') {
1775
1910
  this.awaitingExternal = true;
1776
- await this.safeSetRunStatus(this.currentRunId, 'awaiting_external');
1911
+ await this.safeSetRunStatus(this.actor.state.activeRun, 'awaiting_external');
1777
1912
  // 2026-09-16 (2-C.4): 把"在等什么"写成持久化事实 (来源/关联/过期), 否则真实回包到了也不知道该唤醒谁。
1778
1913
  try {
1779
- if (this.currentGoalId) {
1914
+ if (this.actor.state.goalBinding) {
1780
1915
  const { bindExternalWait, newContinuationId, defaultWaitExpiry } = await Promise.resolve().then(() => __importStar(require('./external-events.js')));
1781
1916
  const isDelegate = /delegate/i.test(String(tool || ''));
1782
- await bindExternalWait(this.currentGoalId, {
1783
- requestId: this.pendingExternalRequestId || `${this.currentRunId}:${Date.now().toString(36)}`,
1784
- continuationId: newContinuationId(this.currentGoalId),
1917
+ await bindExternalWait(this.actor.state.goalBinding, {
1918
+ requestId: this.pendingExternalRequestId || `${this.actor.state.activeRun}:${Date.now().toString(36)}`,
1919
+ continuationId: newContinuationId(this.actor.state.goalBinding),
1785
1920
  expectedSource: isDelegate ? 'delegate' : 'p2p',
1786
1921
  expectedEvent: 'result',
1787
1922
  createdAt: new Date().toISOString(),
@@ -1791,31 +1926,31 @@ ${await this.renderActivePlansSection()}
1791
1926
  }
1792
1927
  }
1793
1928
  catch (e) {
1794
- await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.bindExternalWait', runId: this.currentRunId, message: String(e?.message || e) }).catch(() => { });
1929
+ await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.bindExternalWait', runId: this.actor.state.activeRun, message: String(e?.message || e) }).catch(() => { });
1795
1930
  }
1796
1931
  return;
1797
1932
  }
1798
1933
  // 鉴权类: 不重试, 直接交人
1799
1934
  if (cls === 'auth') {
1800
1935
  this.breakerReason = `鉴权类错误不重试 (${tool}): ${errorText.slice(0, 120)}`;
1801
- await this.safeSetRunStatus(this.currentRunId, 'needs_human');
1936
+ await this.safeSetRunStatus(this.actor.state.activeRun, 'needs_human');
1802
1937
  return;
1803
1938
  }
1804
1939
  // 重复失败熔断: 同一工具 + 同一组参数连续失败达 3 次
1805
1940
  if (repeats >= MAX_SAME_TOOL_FAILURES) {
1806
1941
  this.breakerReason = `同一工具 ${tool} 连续失败 ${repeats} 次 (${cls}) → 熔断, 不再重试`;
1807
- await this.safeSetRunStatus(this.currentRunId, 'needs_human');
1942
+ await this.safeSetRunStatus(this.actor.state.activeRun, 'needs_human');
1808
1943
  }
1809
1944
  }
1810
1945
  getRunId() {
1811
- return this.currentRunId;
1946
+ return this.actor.state.activeRun;
1812
1947
  }
1813
1948
  /**
1814
1949
  * 2026-09-16 (M2-B): **上一次**运行的 runId —— prompt 收尾时 `currentRunId` 会被清空,
1815
1950
  * 而 Supervisor / 控制面要在运行结束后才知道"刚才跑的是哪条 run" (否则会拿旧 run 做决策)。
1816
1951
  */
1817
1952
  getLastRunId() {
1818
- return this.lastRunId || this.currentRunId;
1953
+ return this.lastRunId || this.actor.state.activeRun;
1819
1954
  }
1820
1955
  /**
1821
1956
  * 2026-09-16 (M2): 从 checkpoint 恢复一次运行 —— **不是**重发原 prompt。
@@ -1862,7 +1997,7 @@ ${await this.renderActivePlansSection()}
1862
1997
  }
1863
1998
  this.resumeRunId = runId;
1864
1999
  this.resumePlan = prep.plan;
1865
- this.currentGoalId = prep.plan.goalId || this.currentGoalId;
2000
+ this.actor.state.goalBinding = prep.plan.goalId || this.actor.state.goalBinding;
1866
2001
  try {
1867
2002
  const reply = await this.prompt((0, run_store_js_1.buildResumeInstruction)(prep.plan), {});
1868
2003
  return { ok: true, reply, ...(modelDrift ? { modelDrift } : {}), ...(modelApplied ? { modelApplied } : {}) };
@@ -1881,8 +2016,8 @@ ${await this.renderActivePlansSection()}
1881
2016
  // pre-tool-validator 4 步链 (modeGate/blacklist/shell-guard/schema), 经 human-value-pipeline 包装
1882
2017
  preToolUse: async (o) => (0, human_value_pipeline_js_1.onPreToolUse)({ tool: o.tool, args: o.args, permissionMode: o.permissionMode }),
1883
2018
  // 事件写 Run: 观测级 (记账失败不改变已做出的决策)
1884
- events: (e) => { if (this.currentRunId)
1885
- void (0, run_store_js_1.recordHarnessEvent)(this.currentRunId, e); },
2019
+ events: (e) => { if (this.actor.state.activeRun)
2020
+ void (0, run_store_js_1.recordHarnessEvent)(this.actor.state.activeRun, e); },
1886
2021
  });
1887
2022
  }
1888
2023
  return this._harness;
@@ -1890,10 +2025,10 @@ ${await this.renderActivePlansSection()}
1890
2025
  /** 每个生命周期事件都带上的运行身份 (runId / goalId / agentId / channelId / surface) */
1891
2026
  harnessCtx() {
1892
2027
  return {
1893
- runId: this.currentRunId || undefined,
1894
- goalId: this.currentGoalId || undefined,
1895
- agentId: this.currentAgentId || undefined,
1896
- channelId: this.currentChannelId || undefined,
2028
+ runId: this.actor.state.activeRun || undefined,
2029
+ goalId: this.actor.state.goalBinding || undefined,
2030
+ agentId: this.actor.state.agentId || undefined,
2031
+ channelId: this.actor.state.channelId || undefined,
1897
2032
  surface: this.runSurface,
1898
2033
  };
1899
2034
  }
@@ -1960,26 +2095,26 @@ ${await this.renderActivePlansSection()}
1960
2095
  this.awaitingExternal = false;
1961
2096
  if (this.resumeRunId) {
1962
2097
  // ── 恢复模式: 复用原来的 runId, 不新建 run (历史保留) ──
1963
- this.currentRunId = this.resumeRunId;
2098
+ this.actor.state.activeRun = this.resumeRunId;
1964
2099
  this.lastRunId = this.resumeRunId;
1965
2100
  try {
1966
- await (0, run_store_js_1.markRunRunning)(this.currentRunId);
2101
+ await (0, run_store_js_1.markRunRunning)(this.actor.state.activeRun);
1967
2102
  }
1968
2103
  catch (err) {
1969
2104
  runPersistenceFailure = `恢复时状态迁移失败 (recovering → running): ${String(err?.message || err).slice(0, 180)}`;
1970
2105
  }
1971
2106
  const doneN = this.resumePlan?.completedSteps.length ?? 0;
1972
2107
  const guards = this.resumePlan?.replayGuards.length ?? 0;
1973
- onStream?.({ type: 'status', internal: true, content: `♻️ 从 checkpoint 恢复运行 ${this.currentRunId} (已完成 ${doneN} 步, 非幂等重放守卫 ${guards} 条)`, tool: 'harness' });
2108
+ onStream?.({ type: 'status', internal: true, content: `♻️ 从 checkpoint 恢复运行 ${this.actor.state.activeRun} (已完成 ${doneN} 步, 非幂等重放守卫 ${guards} 条)`, tool: 'harness' });
1974
2109
  }
1975
2110
  else {
1976
2111
  // 2026-09-16 (M2): 目标绑定 —— 有 goalId 就在该 Goal 下执行; 没有就建 Goal 再建 Run。
1977
2112
  // 延续规则 (确定性, 不靠猜): 该 channel/agent 上已有 open/active Goal, 且它的上一次执行**没收尾**
1978
2113
  // (interrupted/stalled/needs_human/paused/awaiting_external/recovering) → 继续该 Goal; 否则新建。
1979
- let boundGoalId = this.currentGoalId;
2114
+ let boundGoalId = this.actor.state.goalBinding;
1980
2115
  if (!boundGoalId) {
1981
2116
  try {
1982
- const active = await (0, goal_store_js_1.findActiveGoal)({ channelId: this.currentChannelId || undefined, agentId: this.currentAgentId || undefined });
2117
+ const active = await (0, goal_store_js_1.findActiveGoal)({ channelId: this.actor.state.channelId || undefined, agentId: this.actor.state.agentId || undefined });
1983
2118
  if (active?.currentRunId) {
1984
2119
  const prev = await (0, run_store_js_1.readRun)(active.currentRunId);
1985
2120
  const unfinished = prev && ['interrupted', 'stalled', 'needs_human', 'paused', 'awaiting_external', 'recovering'].includes(prev.status);
@@ -1994,8 +2129,8 @@ ${await this.renderActivePlansSection()}
1994
2129
  try {
1995
2130
  const g = await (0, goal_store_js_1.createGoal)({
1996
2131
  objective: this.currentUserInput || '(未记录目标)',
1997
- channelId: this.currentChannelId || undefined,
1998
- agentId: this.currentAgentId || undefined,
2132
+ channelId: this.actor.state.channelId || undefined,
2133
+ agentId: this.actor.state.agentId || undefined,
1999
2134
  createdBy: this.runSurface,
2000
2135
  });
2001
2136
  boundGoalId = g.goalId;
@@ -2004,25 +2139,25 @@ ${await this.renderActivePlansSection()}
2004
2139
  console.warn('[PiAgent] 创建 Goal 失败 (无目标也要有运行记录):', err?.message);
2005
2140
  }
2006
2141
  }
2007
- this.currentGoalId = boundGoalId;
2142
+ this.actor.state.goalBinding = boundGoalId;
2008
2143
  }
2009
2144
  try {
2010
2145
  const rec = await (0, run_store_js_1.startRun)({
2011
2146
  surface: this.runSurface,
2012
2147
  goal: this.currentUserInput || '(未记录目标)',
2013
2148
  goalId: boundGoalId || undefined,
2014
- channelId: this.currentChannelId || undefined,
2015
- agentId: this.currentAgentId || undefined,
2149
+ channelId: this.actor.state.channelId || undefined,
2150
+ agentId: this.actor.state.agentId || undefined,
2016
2151
  modelConfig: await this.runModelSnapshot(),
2017
2152
  });
2018
- this.currentRunId = rec.runId;
2153
+ this.actor.state.activeRun = rec.runId;
2019
2154
  this.lastRunId = rec.runId;
2020
- this.currentGoalId = rec.goalId || this.currentGoalId;
2021
- if (this.currentGoalId) {
2155
+ this.actor.state.goalBinding = rec.goalId || this.actor.state.goalBinding;
2156
+ if (this.actor.state.goalBinding) {
2022
2157
  // Run → Goal 反查链: runId → goalId → objective / success criteria
2023
- await (0, goal_store_js_1.attachRun)(this.currentGoalId, rec.runId).catch((err) => console.warn('[PiAgent] attachRun 失败:', err?.message));
2158
+ await (0, goal_store_js_1.attachRun)(this.actor.state.goalBinding, rec.runId).catch((err) => console.warn('[PiAgent] attachRun 失败:', err?.message));
2024
2159
  }
2025
- onStream?.({ type: 'status', internal: true, content: `🧷 运行已登记 (run=${rec.runId}${this.currentGoalId ? `, goal=${this.currentGoalId}` : ''}, 预算 ${rec.budget.maxSteps} 步 / ${Math.round(rec.budget.deadlineMs / 60000)} 分钟)`, tool: 'harness' });
2160
+ onStream?.({ type: 'status', internal: true, content: `🧷 运行已登记 (run=${rec.runId}${this.actor.state.goalBinding ? `, goal=${this.actor.state.goalBinding}` : ''}, 预算 ${rec.budget.maxSteps} 步 / ${Math.round(rec.budget.deadlineMs / 60000)} 分钟)`, tool: 'harness' });
2026
2161
  }
2027
2162
  catch (err) {
2028
2163
  // 核心写失败: 不再 warn 后继续 —— 没有运行记录就不执行 (strict 模式默认如此)
@@ -2052,18 +2187,18 @@ ${await this.renderActivePlansSection()}
2052
2187
  break;
2053
2188
  }
2054
2189
  // 2026-09-16: 预算闸门 (持久化 harness 的约束面) —— 到点必须**如实**终止, 不许静默算完成
2055
- if (this.currentRunId) {
2190
+ if (this.actor.state.activeRun) {
2056
2191
  try {
2057
- const rec = await (0, run_store_js_1.readRun)(this.currentRunId);
2192
+ const rec = await (0, run_store_js_1.readRun)(this.actor.state.activeRun);
2058
2193
  if (!rec)
2059
- throw new Error(`运行记录读不到: ${this.currentRunId}`);
2194
+ throw new Error(`运行记录读不到: ${this.actor.state.activeRun}`);
2060
2195
  // 2026-09-16 (M5): 外部控制面 (CLI /pause /abort, Web API) 改过状态 → 如实停在那儿。
2061
2196
  // 不覆盖成 done/failed: 人按下暂停就是暂停, 人按下中止就是中止。
2062
2197
  if (rec.status === 'paused' || rec.status === 'aborted') {
2063
2198
  runExternallyPaused = rec.status === 'paused';
2064
2199
  runExternallyAborted = rec.status === 'aborted';
2065
2200
  runStopReason = `外部请求: ${rec.status}`;
2066
- onStream?.({ type: 'error', content: `⏹️ 运行被外部${rec.status === 'paused' ? '暂停' : '中止'} (run=${this.currentRunId})`, tool: 'harness' });
2201
+ onStream?.({ type: 'error', content: `⏹️ 运行被外部${rec.status === 'paused' ? '暂停' : '中止'} (run=${this.actor.state.activeRun})`, tool: 'harness' });
2067
2202
  finalResponse = finalResponse || `(运行已${rec.status === 'paused' ? '暂停' : '中止'})`;
2068
2203
  break;
2069
2204
  }
@@ -2115,14 +2250,14 @@ ${await this.renderActivePlansSection()}
2115
2250
  if (totalToolCallsThisLoop >= MAX_TOOL_CALLS_PER_LOOP) {
2116
2251
  console.warn(`[PiAgent] 单轮工具调用已达 ${MAX_TOOL_CALLS_PER_LOOP}, 注入 hint 让 LLM 总结`);
2117
2252
  onStream?.({ type: 'error', content: `⏹️ 工具调用已达上限 (${MAX_TOOL_CALLS_PER_LOOP}), 请基于已有结果回答`, tool: 'loop' });
2118
- this.messageHistory.push({ role: 'system', content: `[注意] 你已连续调用 ${MAX_TOOL_CALLS_PER_LOOP} 次工具。请基于已有结果直接回答用户, 不要再次调用任何工具。在回答末尾加 <final gen> 标记结束。` });
2253
+ this.pushHistory({ role: 'system', content: `[注意] 你已连续调用 ${MAX_TOOL_CALLS_PER_LOOP} 次工具。请基于已有结果直接回答用户, 不要再次调用任何工具。在回答末尾加 <final gen> 标记结束。` });
2119
2254
  totalToolCallsThisLoop = 0; // 重置计数器, 只防连续死循环
2120
2255
  }
2121
2256
  if (lastNTools.length >= MAX_IDEMPOTENT_TOOL && new Set(lastNTools).size === 1) {
2122
2257
  const repeatedTool = lastNTools[0];
2123
2258
  console.warn(`[PiAgent] 同工具 ${repeatedTool} 连续成功调 ${MAX_IDEMPOTENT_TOOL} 次, 注入 hint 让 LLM 总结`);
2124
2259
  onStream?.({ type: 'error', content: `⏹️ 工具 ${repeatedTool} 重复调用 ${MAX_IDEMPOTENT_TOOL} 次, 请基于已有结果回答`, tool: 'loop' });
2125
- this.messageHistory.push({ role: 'system', content: `[注意] 你已连续 ${MAX_IDEMPOTENT_TOOL} 次调用 ${repeatedTool}。请基于已有结果直接回答用户, 不要再次调用任何工具。在回答末尾加 <final gen> 标记结束。` });
2260
+ this.pushHistory({ role: 'system', content: `[注意] 你已连续 ${MAX_IDEMPOTENT_TOOL} 次调用 ${repeatedTool}。请基于已有结果直接回答用户, 不要再次调用任何工具。在回答末尾加 <final gen> 标记结束。` });
2126
2261
  lastNTools.length = 0; // 重置计数器
2127
2262
  // 不 break — 让 LLM 在下一轮用已有信息回答
2128
2263
  }
@@ -2325,7 +2460,7 @@ ${await this.renderActivePlansSection()}
2325
2460
  break;
2326
2461
  }
2327
2462
  // 把错误当成 tool 结果 push 进 history, 这样下一轮 LLM 看到错误能调整
2328
- this.messageHistory.push({
2463
+ this.pushHistory({
2329
2464
  role: 'system',
2330
2465
  content: `[Loop 错误恢复 ${totalErrors}/${this.MAX_TOTAL_ERRORS}] ${aiFailureReason}\n\n请基于上轮工具结果继续完成任务, 不要重复调用同一失败操作. 如果工具已成功执行, 请基于 result.output 给用户总结; 如果工具失败, 请换其他方式或重试.`
2331
2466
  });
@@ -2398,7 +2533,7 @@ ${await this.renderActivePlansSection()}
2398
2533
  }
2399
2534
  if (toolCalls.length > 0) {
2400
2535
  // 把原始 LLM 回复 push 进 history (仅一次)
2401
- this.messageHistory.push({
2536
+ this.pushHistory({
2402
2537
  role: 'assistant',
2403
2538
  content: reply,
2404
2539
  toolCalls: toolCalls.length > 1 ? toolCalls : [toolCalls[0]],
@@ -2428,12 +2563,12 @@ ${await this.renderActivePlansSection()}
2428
2563
  consecutiveErrors++;
2429
2564
  totalErrors++;
2430
2565
  const errorResult = { success: false, error: `未知工具: ${toolCall.name}` };
2431
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
2566
+ this.pushHistory({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
2432
2567
  this.logToHarness(toolCall.name, toolCall.args, errorResult);
2433
2568
  // 2026-07-28: 注入 Reflection 帮助 LLM 理解错误
2434
2569
  const obs = (0, error_classifier_js_1.buildObservation)(toolCall.name, toolCall.args, errorResult);
2435
2570
  const ref = (0, error_classifier_js_1.buildReflection)(toolCall.name, errorResult.error, totalErrors, lastFailedToolCount);
2436
- this.messageHistory.push({ role: 'system', content: (0, error_classifier_js_1.formatObservationWithReflection)(obs, ref) });
2571
+ this.pushHistory({ role: 'system', content: (0, error_classifier_js_1.formatObservationWithReflection)(obs, ref) });
2437
2572
  if (onStream)
2438
2573
  onStream({ type: 'status', content: `💡 Reflection: ${obs.summary}`, tool: 'system' });
2439
2574
  console.warn(`[PiAgent] 未知工具: ${toolCall.name} (累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}),跳过并继续`);
@@ -2467,7 +2602,7 @@ ${await this.renderActivePlansSection()}
2467
2602
  consecutiveErrors++;
2468
2603
  totalErrors++;
2469
2604
  const denyResultMsg = { success: false, error: `拒绝: [${toolDecision.rejectedBy || 'deny-pipeline'}] ${toolDecision.reason}` };
2470
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(denyResultMsg), toolResult: denyResultMsg });
2605
+ this.pushHistory({ role: 'tool', content: JSON.stringify(denyResultMsg), toolResult: denyResultMsg });
2471
2606
  this.logToHarness(toolCall.name, toolCall.args, denyResultMsg);
2472
2607
  return;
2473
2608
  }
@@ -2479,7 +2614,7 @@ ${await this.renderActivePlansSection()}
2479
2614
  ? `Harness gate 拒绝 (${toolDecision.rejectedBy}): ${toolDecision.reason || '未通过安全校验'}`
2480
2615
  : `PreToolUse 拒绝: ${toolDecision.reason || '未通过安全校验'}`,
2481
2616
  };
2482
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(deniedResult), toolResult: deniedResult });
2617
+ this.pushHistory({ role: 'tool', content: JSON.stringify(deniedResult), toolResult: deniedResult });
2483
2618
  this.logToHarness(toolCall.name, toolCall.args, deniedResult);
2484
2619
  if (onStream) {
2485
2620
  const gateName = isGateDeny ? `Harness ${toolDecision.rejectedBy}` : (isHarnessError ? '核心约束层' : 'PreToolUse');
@@ -2508,13 +2643,13 @@ ${await this.renderActivePlansSection()}
2508
2643
  ? `[注意] 连续 ${consecutiveErrors} 次工具调用因约束层失效被阻止. 请换其他工具或直接回答用户, 末尾加 <final gen>.`
2509
2644
  : `[注意] 连续 ${consecutiveErrors} 次工具调用被系统拒绝. 请换其他工具或直接回答用户, 末尾加 <final gen>.`;
2510
2645
  if (lastFailedToolCount >= MAX_SAME_TOOL_FAILURES) {
2511
- this.messageHistory.push({ role: 'system', content: systemMaxMsg });
2646
+ this.pushHistory({ role: 'system', content: systemMaxMsg });
2512
2647
  lastFailedTool = '';
2513
2648
  lastFailedToolCount = 0;
2514
2649
  consecutiveErrors = 0;
2515
2650
  }
2516
2651
  else if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
2517
- this.messageHistory.push({ role: 'system', content: systemConsecMsg });
2652
+ this.pushHistory({ role: 'system', content: systemConsecMsg });
2518
2653
  consecutiveErrors = 0;
2519
2654
  }
2520
2655
  return;
@@ -2548,18 +2683,18 @@ ${await this.renderActivePlansSection()}
2548
2683
  }
2549
2684
  console.log(`[PiAgent] 工具 ${toolCall.name} 执行完成: success=${result.success} (${toolDurationMs}ms)`);
2550
2685
  // 2026-09-16 (M3): 失败接线 —— 分类 + recovery 留痕 + 熔断 / 外部等待
2551
- if (!result.success && this.currentRunId && !replaySkip) {
2686
+ if (!result.success && this.actor.state.activeRun && !replaySkip) {
2552
2687
  await this.wireToolFailure(toolCall.name, String(result.error || ''), toolCall.args);
2553
2688
  }
2554
- else if (result.success && this.currentRunId && this.awaitingExternal) {
2689
+ else if (result.success && this.actor.state.activeRun && this.awaitingExternal) {
2555
2690
  // 外部回话了: awaiting_external → running (不是"恢复完成", 只是等待结束)
2556
2691
  this.awaitingExternal = false;
2557
- await this.safeSetRunStatus(this.currentRunId, 'running');
2692
+ await this.safeSetRunStatus(this.actor.state.activeRun, 'running');
2558
2693
  }
2559
2694
  // 2026-09-16: 持久化 run harness — 每步工具调用立即落盘 (崩在这里也能看到做到哪步)
2560
- if (this.currentRunId) {
2695
+ if (this.actor.state.activeRun) {
2561
2696
  try {
2562
- await (0, run_store_js_1.recordStep)(this.currentRunId, {
2697
+ await (0, run_store_js_1.recordStep)(this.actor.state.activeRun, {
2563
2698
  tool: toolCall.name,
2564
2699
  ok: !!result.success,
2565
2700
  ms: toolDurationMs,
@@ -2590,7 +2725,7 @@ ${await this.renderActivePlansSection()}
2590
2725
  ok: !!result.success,
2591
2726
  });
2592
2727
  if (after.routeHint?.systemAddition) {
2593
- this.messageHistory.push({ role: 'system', content: `[Harness Router Hint: ${after.routeHint.reason}]\n${after.routeHint.systemAddition}` });
2728
+ this.pushHistory({ role: 'system', content: `[Harness Router Hint: ${after.routeHint.reason}]\n${after.routeHint.systemAddition}` });
2594
2729
  }
2595
2730
  if (after.outputBlocked) {
2596
2731
  if (onStream) {
@@ -2623,14 +2758,14 @@ ${await this.renderActivePlansSection()}
2623
2758
  }
2624
2759
  }
2625
2760
  catch { /* 观测失败绝不影响主路径 */ }
2626
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(result), toolResult: result, toolCallId: toolCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` });
2761
+ this.pushHistory({ role: 'tool', content: JSON.stringify(result), toolResult: result, toolCallId: toolCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` });
2627
2762
  // 2026-10-01 (用户: 「为什么这么慢」): 量到的事实 —— 每次 LLM 往返平均 3338ms,
2628
2763
  // 而"一轮发多个工具"(toolCalls>1) 只占 12/63 ✗ ⇒ 回合时长 ≈ 往返次数 × 3.3 秒 ✓。
2629
2764
  // 本回合**第一次**且**只发一个**工具时, 附一句带代价的提醒(不是口号 ✓), 只提一次免得刷屏。
2630
2765
  if (toolCalls.length === 1 && this.successfulToolResults.length <= 1) { // 本回合头一次(免得每轮刷屏)
2631
2766
  const __batchHint = (0, tool_loop_guard_js_1.batchHint)(toolCall.name, 1);
2632
2767
  if (__batchHint)
2633
- this.messageHistory.push({ role: 'system', content: __batchHint.trim() });
2768
+ this.pushHistory({ role: 'system', content: __batchHint.trim() });
2634
2769
  }
2635
2770
  this.logToHarness(toolCall.name, toolCall.args, result);
2636
2771
  // 2026-08-09: 记录到本轮行动日志 (循环进度 + final 前目标核查用)
@@ -2742,18 +2877,18 @@ ${await this.renderActivePlansSection()}
2742
2877
  // 2026-07-28: 注入 Observation + Reflection 替代旧 hardcode 提示
2743
2878
  const obs = (0, error_classifier_js_1.buildObservation)(toolCall.name, toolCall.args, { success: false, error: result.error });
2744
2879
  const ref = (0, error_classifier_js_1.buildReflection)(toolCall.name, result.error, totalErrors, lastFailedToolCount);
2745
- this.messageHistory.push({ role: 'system', content: (0, error_classifier_js_1.formatObservationWithReflection)(obs, ref) + SHELL_ESCAPE_HINT });
2880
+ this.pushHistory({ role: 'system', content: (0, error_classifier_js_1.formatObservationWithReflection)(obs, ref) + SHELL_ESCAPE_HINT });
2746
2881
  if (onStream)
2747
2882
  onStream({ type: 'status', content: `💡 Reflection: ${obs.summary} → ${ref[0]?.action || '放弃'}`, tool: 'system' });
2748
2883
  if (lastFailedToolCount >= MAX_SAME_TOOL_FAILURES) {
2749
- this.messageHistory.push({ role: 'system', content: `[注意] 工具 ${toolCall.name} 在这个上下文中不可用 (连续 ${MAX_SAME_TOOL_FAILURES} 次失败: ${result.error}). 请不要再次调用它, 直接用你已知的信息回答用户, 并在回答开头标记 <final gen>.` });
2884
+ this.pushHistory({ role: 'system', content: `[注意] 工具 ${toolCall.name} 在这个上下文中不可用 (连续 ${MAX_SAME_TOOL_FAILURES} 次失败: ${result.error}). 请不要再次调用它, 直接用你已知的信息回答用户, 并在回答开头标记 <final gen>.` });
2750
2885
  lastFailedTool = '';
2751
2886
  lastFailedToolCount = 0;
2752
2887
  consecutiveErrors = 0;
2753
2888
  return;
2754
2889
  }
2755
2890
  if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
2756
- this.messageHistory.push({ role: 'system', content: `[注意] 前面的工具调用连续失败。请尝试其他工具或换一种方式完成用户请求, 或用 <final gen> 给出最终回答.` });
2891
+ this.pushHistory({ role: 'system', content: `[注意] 前面的工具调用连续失败。请尝试其他工具或换一种方式完成用户请求, 或用 <final gen> 给出最终回答.` });
2757
2892
  consecutiveErrors = 0;
2758
2893
  }
2759
2894
  }
@@ -2762,11 +2897,11 @@ ${await this.renderActivePlansSection()}
2762
2897
  consecutiveErrors++;
2763
2898
  totalErrors++;
2764
2899
  const errorResult = { success: false, error: String(execError) };
2765
- this.messageHistory.push({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
2900
+ this.pushHistory({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
2766
2901
  this.logToHarness(toolCall.name, toolCall.args, errorResult);
2767
2902
  const obs = (0, error_classifier_js_1.buildObservation)(toolCall.name, toolCall.args, errorResult);
2768
2903
  const ref = (0, error_classifier_js_1.buildReflection)(toolCall.name, errorResult.error, totalErrors, lastFailedToolCount);
2769
- this.messageHistory.push({ role: 'system', content: (0, error_classifier_js_1.formatObservationWithReflection)(obs, ref) + SHELL_ESCAPE_HINT });
2904
+ this.pushHistory({ role: 'system', content: (0, error_classifier_js_1.formatObservationWithReflection)(obs, ref) + SHELL_ESCAPE_HINT });
2770
2905
  if (onStream)
2771
2906
  onStream({ type: 'status', content: `💡 Reflection: ${obs.summary}`, tool: 'system' });
2772
2907
  console.error(`[PiAgent] 工具执行异常 (累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}): ${execError}`);
@@ -2777,7 +2912,7 @@ ${await this.renderActivePlansSection()}
2777
2912
  }
2778
2913
  else {
2779
2914
  // LLM 返回的不是 tool call 格式
2780
- this.messageHistory.push({
2915
+ this.pushHistory({
2781
2916
  role: 'assistant',
2782
2917
  content: reply,
2783
2918
  reasoningContent: response?.reasoningContent,
@@ -2806,7 +2941,7 @@ ${await this.renderActivePlansSection()}
2806
2941
  unreportedRetries++;
2807
2942
  const unreported = this.successfulToolResults.length;
2808
2943
  console.log(`[PiAgent] LLM 想 final_gen 但还有 ${unreported} 个工具结果未汇报 (${unreportedRetries}/${MAX_UNREPORTED_RETRIES}), push hint 让其继续`);
2809
- this.messageHistory.push({
2944
+ this.pushHistory({
2810
2945
  role: 'system',
2811
2946
  content: `[dive-into stop condition] 你之前已成功执行了 ${unreported} 个工具, 但当前回复里没把它们的结果告诉用户. 请基于已有的工具结果 (在 history 里) 写一个完整总结回复给用户, 用 <final gen> 结尾. 不要再调工具.`
2812
2947
  });
@@ -2819,7 +2954,7 @@ ${await this.renderActivePlansSection()}
2819
2954
  // 反复提示仍未汇报超过上限 → 清空积压强制 final, 不再死循环
2820
2955
  console.log(`[PiAgent] unreported 循环超限 (${unreportedRetries} 次), 清空积压强制 final`);
2821
2956
  this.successfulToolResults = [];
2822
- this.messageHistory.push({
2957
+ this.pushHistory({
2823
2958
  role: 'system',
2824
2959
  content: `[dive-into stop condition] 已多次提示汇报工具结果仍未完成 (超过 ${MAX_UNREPORTED_RETRIES} 次). 现在直接基于你已知的信息写最终回复给用户, 用 <final gen> 结尾, 不要再调任何工具.`
2825
2960
  });
@@ -2831,7 +2966,7 @@ ${await this.renderActivePlansSection()}
2831
2966
  // 2026-07-29: 质量门 — 即使 LLM 声称完成, 质量太低也继续
2832
2967
  if (lastQualityScore < this.QUALITY_THRESHOLD && refineAttempts < this.MAX_REFINE_ATTEMPTS) {
2833
2968
  console.log(`[PiAgent] final gen 质量 ${lastQualityScore.toFixed(2)} < ${this.QUALITY_THRESHOLD}, 注入 refine hint`);
2834
- this.messageHistory.push({ role: 'system', content: `[质量检查] 你的回答质量评分为 ${(lastQualityScore * 10).toFixed(1)}/10, 低于 ${(this.QUALITY_THRESHOLD * 10).toFixed(1)}/10 阈值。请提供更完整、详细的回答, 包含工具调用获取到的具体信息, 末尾加 <final gen>。` });
2969
+ this.pushHistory({ role: 'system', content: `[质量检查] 你的回答质量评分为 ${(lastQualityScore * 10).toFixed(1)}/10, 低于 ${(this.QUALITY_THRESHOLD * 10).toFixed(1)}/10 阈值。请提供更完整、详细的回答, 包含工具调用获取到的具体信息, 末尾加 <final gen>。` });
2835
2970
  refineAttempts++;
2836
2971
  continue;
2837
2972
  }
@@ -2846,13 +2981,13 @@ ${await this.renderActivePlansSection()}
2846
2981
  userIntent: this.currentUserInput,
2847
2982
  completedTools: Array.from(loopReviewCompletedTools),
2848
2983
  actionLog: loopActionLog,
2849
- runId: this.currentRunId || undefined,
2850
- goalId: this.currentGoalId || undefined,
2984
+ runId: this.actor.state.activeRun || undefined,
2985
+ goalId: this.actor.state.goalBinding || undefined,
2851
2986
  }, loop_review_js_1.DEFAULT_MAX_REVIEWS);
2852
2987
  if (reviewDecision.kind === 'continue-review') {
2853
2988
  loopReviewCount++;
2854
2989
  console.log(`[PiAgent] review ${loopReviewCount}/${loop_review_js_1.DEFAULT_MAX_REVIEWS}: LLM 想 final 但先对齐需求深挖一次`);
2855
- this.messageHistory.push({ role: 'system', content: reviewDecision.hint });
2990
+ this.pushHistory({ role: 'system', content: reviewDecision.hint });
2856
2991
  if (onStream) {
2857
2992
  onStream({ type: 'status', internal: true, content: `🔄 目标对齐 review ${loopReviewCount}/${loop_review_js_1.DEFAULT_MAX_REVIEWS}: 深挖续跑`, tool: 'system' });
2858
2993
  }
@@ -2905,16 +3040,16 @@ ${await this.renderActivePlansSection()}
2905
3040
  if (onStream) {
2906
3041
  onStream({ type: 'status', internal: true, content: `✅ 处理完成,共 ${iteration - 1} 次循环`, tool: 'system' });
2907
3042
  }
2908
- this.messageHistory.push({ role: 'assistant', content: finalResponse });
3043
+ this.pushHistory({ role: 'assistant', content: finalResponse });
2909
3044
  // React Harness: 循环结束
2910
3045
  // 2026-09-16 (Milestone 1-B): 会话收尾走唯一门面
2911
3046
  await this.piHarness().sessionEnd(this.harnessCtx());
2912
3047
  // 2026-09-16: 收尾落盘 — done / failed / aborted / needs_human 如实写回 (不留幽灵 running)
2913
- if (this.currentRunId && !runExternallyPaused && !runExternallyAborted) {
3048
+ if (this.actor.state.activeRun && !runExternallyPaused && !runExternallyAborted) {
2914
3049
  try {
2915
3050
  // 2026-09-16 (M4): 完成门 —— "模型说完成"不等于"系统确认完成"。
2916
3051
  // 证据 = 成功步骤的事实摘要; 末尾还有失败步骤没被后续成功覆盖 → 不许 done。
2917
- const recBefore = await (0, run_store_js_1.readRun)(this.currentRunId).catch(() => null);
3052
+ const recBefore = await (0, run_store_js_1.readRun)(this.actor.state.activeRun).catch(() => null);
2918
3053
  const steps = recBefore?.steps || [];
2919
3054
  const evidence = steps.filter((s) => s.ok).map((s) => `${s.tool}: ${(s.summary || '').slice(0, 120)}`).slice(-10);
2920
3055
  const lastStep = steps[steps.length - 1];
@@ -2935,7 +3070,7 @@ ${await this.renderActivePlansSection()}
2935
3070
  : (trailingFailure || noEvidence)
2936
3071
  ? 'failed'
2937
3072
  : 'done';
2938
- await (0, run_store_js_1.finishRun)(this.currentRunId, {
3073
+ await (0, run_store_js_1.finishRun)(this.actor.state.activeRun, {
2939
3074
  status,
2940
3075
  summary: finalResponse ? String(finalResponse).slice(0, 400) : undefined,
2941
3076
  error: errText
@@ -2948,7 +3083,7 @@ ${await this.renderActivePlansSection()}
2948
3083
  // 终态也写不下去: 这是最坏情况 —— 除了留痕, 没有别的自愈手段, 所以必须显眼
2949
3084
  const message = `收尾落盘失败 (状态无法写回, 记录会停在 running): ${String(err?.message || err).slice(0, 200)}`;
2950
3085
  console.error('[PiAgent] run-store finishRun 失败 (核心持久化):', message);
2951
- await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.finishRun', runId: this.currentRunId, message }).catch(() => { });
3086
+ await (0, run_store_js_1.recordDegradation)({ kind: 'core', op: 'pi-sdk.finishRun', runId: this.actor.state.activeRun, message }).catch(() => { });
2952
3087
  onStream?.({ type: 'error', content: `⚠️ ${message}`, tool: 'harness' });
2953
3088
  }
2954
3089
  // ★ 2026-09-25 (M0 接线冻结, 规则 ③ + ④): Run 结束**必过收尾漏斗**。
@@ -2960,14 +3095,14 @@ ${await this.renderActivePlansSection()}
2960
3095
  //
2961
3096
  // 现在: `closeRunOnce` (幂等) → 9 步收尾 → 产物落盘; 再由 Goal reducer 把"下一步是什么"
2962
3097
  // 落进 Goal (没有 Supervisor 宿主时 Runner 也要留下 continuation, 否则下一次还是从零开始)。
2963
- if (this.currentGoalId) {
3098
+ if (this.actor.state.goalBinding) {
2964
3099
  try {
2965
3100
  const wiring = await Promise.resolve().then(() => __importStar(require('./goal-flywheel-wiring.js')));
2966
3101
  const { reduceGoalState } = await Promise.resolve().then(() => __importStar(require('./goal-state-reducer.js')));
2967
3102
  const nowIso = new Date().toISOString();
2968
3103
  const closed = await wiring.closeRunOnce({
2969
- goalId: this.currentGoalId,
2970
- runId: this.currentRunId,
3104
+ goalId: this.actor.state.goalBinding,
3105
+ runId: this.actor.state.activeRun,
2971
3106
  caller: 'runner',
2972
3107
  now: nowIso,
2973
3108
  finalReview: finalResponse ? String(finalResponse).slice(0, 2000) : '',
@@ -2978,7 +3113,7 @@ ${await this.renderActivePlansSection()}
2978
3113
  else if (closed.outcome) {
2979
3114
  const view = closed.outcome;
2980
3115
  const applied = await reduceGoalState({
2981
- goalId: this.currentGoalId,
3116
+ goalId: this.actor.state.goalBinding,
2982
3117
  intent: 'closure_outcome',
2983
3118
  now: nowIso,
2984
3119
  by: 'runner',
@@ -2995,7 +3130,7 @@ ${await this.renderActivePlansSection()}
2995
3130
  onStream?.({
2996
3131
  type: 'status', internal: true,
2997
3132
  content: `${line}${applied.gateRejected ? ` [完成门拒绝: ${applied.gateRejected}]` : ''} `
2998
- + `(收尾 ${view.steps} 步 · memory ${view.memories} · skill 候选 ${view.candidates} · goal=${this.currentGoalId})`,
3133
+ + `(收尾 ${view.steps} 步 · memory ${view.memories} · skill 候选 ${view.candidates} · goal=${this.actor.state.goalBinding})`,
2999
3134
  tool: 'harness',
3000
3135
  });
3001
3136
  }
@@ -3005,15 +3140,15 @@ ${await this.renderActivePlansSection()}
3005
3140
  }
3006
3141
  }
3007
3142
  catch (err) {
3008
- await (0, run_store_js_1.recordDegradation)({ kind: 'observational', op: 'pi-sdk.runClosure', runId: this.currentRunId, message: String(err?.message || err).slice(0, 160) }).catch(() => { });
3143
+ await (0, run_store_js_1.recordDegradation)({ kind: 'observational', op: 'pi-sdk.runClosure', runId: this.actor.state.activeRun, message: String(err?.message || err).slice(0, 160) }).catch(() => { });
3009
3144
  }
3010
3145
  }
3011
- this.currentRunId = '';
3146
+ this.actor.state.activeRun = '';
3012
3147
  }
3013
- else if (this.currentRunId) {
3148
+ else if (this.actor.state.activeRun) {
3014
3149
  // 外部暂停/中止: 状态是人定的, 不覆盖 (paused 等 /resume; aborted 是终态)
3015
3150
  onStream?.({ type: 'status', content: `⏹️ 运行状态保持为 ${runExternallyPaused ? 'paused' : 'aborted'} (由外部控制面决定)`, tool: 'harness' });
3016
- this.currentRunId = '';
3151
+ this.actor.state.activeRun = '';
3017
3152
  }
3018
3153
  // 2026-06-16: 暴露 aiFailed 标志 — promptStream 据此决定是否自动重试整个 loop
3019
3154
  return { reply: finalResponse, aiFailed, aiFailureReason: aiFailureReason || undefined };
@@ -3067,7 +3202,7 @@ ${await this.renderActivePlansSection()}
3067
3202
  // 失败静默: 任何 stage 抛错 → 走老 slice(-10) 逻辑
3068
3203
  //
3069
3204
  // P1.2: 如果 maybeAutoCompact 算过 Context Collapse 投影, 用 this.projectedHistory (读时投影, 非破坏)
3070
- const source = this.projectedHistory ?? this.messageHistory;
3205
+ const source = this.projectedHistory ?? this.actor.state.messageHistory;
3071
3206
  const recentMessages = this.compressHistorySync(source).slice(-10);
3072
3207
  return recentMessages.map(m => {
3073
3208
  if (m.role === 'user')
@@ -3094,7 +3229,7 @@ ${await this.renderActivePlansSection()}
3094
3229
  // 2026-08-06: 来源优先用 projectedHistory (Context Collapse 投影, 非破坏) —
3095
3230
  // 与 buildContext 一致; 之前只让字符串路径用投影, messages 数组路径被跳过,
3096
3231
  // 导致 LLM 实际看到的还是未压缩的历史.
3097
- const source = this.projectedHistory ?? this.messageHistory;
3232
+ const source = this.projectedHistory ?? this.actor.state.messageHistory;
3098
3233
  const WINDOW = 15;
3099
3234
  const out = [];
3100
3235
  // 2026-09-28 (前缀 KV 可命中): 带 CURRENT TURN 注入的条目要**原样回带**.
@@ -3173,11 +3308,11 @@ ${await this.renderActivePlansSection()}
3173
3308
  /**
3174
3309
  * 2026-09-28 (前缀 KV 可命中): 把 chat() 回带的当前轮写回自己的 messageHistory.
3175
3310
  * 逻辑在模块级 `writeBackCurrentTurnInto` (纯函数, 门可以直接驱它断言);
3176
- * 这里只负责接到 `this.messageHistory` 上, 并保证任何异常都静默 (写回失败不影响对话).
3311
+ * 这里只负责接到 `actor 的 messageHistory` 上, 并保证任何异常都静默 (写回失败不影响对话).
3177
3312
  */
3178
3313
  writeBackCurrentTurn(wire) {
3179
3314
  try {
3180
- return writeBackCurrentTurnInto(this.messageHistory, wire);
3315
+ return writeBackCurrentTurnInto(this.actor.state.messageHistory, wire);
3181
3316
  }
3182
3317
  catch (err) {
3183
3318
  console.warn('[PiAgent] writeBackCurrentTurn failed (silent):', err);
@@ -3191,7 +3326,7 @@ ${await this.renderActivePlansSection()}
3191
3326
  estimateHistoryTokens() {
3192
3327
  try {
3193
3328
  const { estimateTokens } = _piRequire('../context-compaction/index.js');
3194
- return estimateTokens(this.messageHistory);
3329
+ return estimateTokens(this.actor.state.messageHistory);
3195
3330
  }
3196
3331
  catch {
3197
3332
  return 0;
@@ -3214,8 +3349,10 @@ ${await this.renderActivePlansSection()}
3214
3349
  console.warn(`[PiAgent] reactive compaction pre-check (${estimated} tokens > 80% threshold)`);
3215
3350
  onStream?.({ type: 'status', internal: true, content: '⚠️ reactive compaction 预检触发', tool: 'recovery' });
3216
3351
  try {
3217
- const compacted = this.compressHistorySync(this.messageHistory);
3218
- this.messageHistory = compacted;
3352
+ // 同步压缩: 取快照与替换在**同一拍相邻两行** (中间没有 await) ⇒ 不存在"变换期间被追加"的窗口。
3353
+ // 若哪天这里插入 await, 必须改成 `actor.rebaseHistory(…, snapshotLen)` (见 channel-actor.ts)。
3354
+ const compacted = this.compressHistorySync(this.actor.state.messageHistory);
3355
+ this.replaceHistory(compacted);
3219
3356
  if (this.estimateHistoryTokens() > this.maxContextTokens() * 0.8) {
3220
3357
  await this.maybeAutoCompact(onStream, signal);
3221
3358
  }
@@ -3271,7 +3408,7 @@ ${await this.renderActivePlansSection()}
3271
3408
  try {
3272
3409
  const rc = String(response?.reasoningContent || '').trim();
3273
3410
  if (rc)
3274
- this.currentOnStream?.({ type: 'reasoning', content: rc });
3411
+ this.runCtx.eventSink?.({ type: 'reasoning', content: rc });
3275
3412
  }
3276
3413
  catch { /* 送不出去不影响主流程 */ } // 2026-06-30: 透传 toolCalls (OpenAI 协议 native) 给上层, 让 assistant message 能 emit 真 id
3277
3414
  return { reply: response.reply || '', toolCalls: response.toolCalls, messages: response.messages };
@@ -3311,8 +3448,8 @@ ${await this.renderActivePlansSection()}
3311
3448
  if (Array.isArray(contextOrMessages)) {
3312
3449
  contextOrMessages = this.buildContext();
3313
3450
  // 也清除最近一轮的 toolCalls, 防止再触发
3314
- if (this.messageHistory.length > 1) {
3315
- const last = this.messageHistory[this.messageHistory.length - 1];
3451
+ if (this.actor.state.messageHistory.length > 1) {
3452
+ const last = this.actor.state.messageHistory[this.actor.state.messageHistory.length - 1];
3316
3453
  if (last.role === 'assistant' && last.toolCalls) {
3317
3454
  delete last.toolCalls;
3318
3455
  }
@@ -3375,7 +3512,7 @@ ${await this.renderActivePlansSection()}
3375
3512
  * onStream 广播: 跟 computeJudgmentGate 风格一致 (phase 事件供 UI timeline 显示)
3376
3513
  */
3377
3514
  async maybeAutoCompact(onStream, signal) {
3378
- if (this.messageHistory.length < 10)
3515
+ if (this.actor.state.messageHistory.length < 10)
3379
3516
  return; // 历史太短, 不值得压
3380
3517
  onStream?.({ type: 'status', internal: true, content: '🗜️ 评估是否需要压缩上下文...', tool: 'compactor' });
3381
3518
  // 注入 LLM (用 getMinimax().chat, 与 judgment 注入门 / ReAct 循环同一来源)
@@ -3394,14 +3531,17 @@ ${await this.renderActivePlansSection()}
3394
3531
  const maxTokens = Math.max(4000, Math.round(cfg.maxTokens * cfg.compressionThreshold));
3395
3532
  const beforeTokens = this.estimateHistoryTokens();
3396
3533
  const { compactPipeline, isContextCollapseEnabled } = await Promise.resolve().then(() => __importStar(require('../context-compaction/index.js')));
3397
- const result = await compactPipeline(this.messageHistory, {
3534
+ // **K5 第 4 步**: 记下取快照时的长度 —— 压缩是 async, 从这一拍到 "落地" 之间是 await 窗口;
3535
+ // 期间 append 进来的消息必须由 `actor.rebaseHistory(…, snapshotLen)` 接回去, 否则会被整块替换丢掉。
3536
+ const snapshotLen = this.actor.state.messageHistory.length;
3537
+ const result = await compactPipeline(this.actor.state.messageHistory, {
3398
3538
  maxTokens,
3399
3539
  llmChat,
3400
3540
  collapseLlmChat: llmChat, // P1.2: Context Collapse 投影也用同一 LLM
3401
- cacheScope: this.currentChannelId || 'default',
3541
+ cacheScope: this.actor.state.channelId || 'default',
3402
3542
  });
3403
- if (result.compacted && result.history.length < this.messageHistory.length) {
3404
- const saved = this.messageHistory.length - result.history.length;
3543
+ if (result.compacted && result.history.length < this.actor.state.messageHistory.length) {
3544
+ const saved = this.actor.state.messageHistory.length - result.history.length;
3405
3545
  const stagesApplied = result.stages.filter((s) => s.applied).map((s) => s.stage).join(' → ');
3406
3546
  const afterTokens = this.estimateHistoryTokens();
3407
3547
  const savedTokens = Math.max(0, beforeTokens - afterTokens);
@@ -3419,7 +3559,17 @@ ${await this.renderActivePlansSection()}
3419
3559
  // messageHistory 不变 (非破坏)
3420
3560
  }
3421
3561
  else {
3422
- this.messageHistory = result.history; // 真破坏性更新
3562
+ // 真破坏性更新 —— **K5 第 4 步**: 绑定了 actor 就交它落地 (rebase: 保住变换期间的追加);
3563
+ // 未绑定的会话走原路径 (行为不变)。
3564
+ if (this.actor) {
3565
+ const { keptTail } = await this.actor.rebaseHistory(result.history, snapshotLen);
3566
+ if (keptTail > 0) {
3567
+ console.warn(`[PiAgent] 压缩落地: 保住变换期间新追加的 ${keptTail} 条消息 (快照后到达)`);
3568
+ }
3569
+ }
3570
+ else {
3571
+ this.replaceHistory(result.history);
3572
+ }
3423
3573
  this.projectedHistory = null;
3424
3574
  }
3425
3575
  // 2026-08-06: snapshot 记录 before/after + 摘要 (供恢复/调试/UI), 事件广播
@@ -3430,10 +3580,10 @@ ${await this.renderActivePlansSection()}
3430
3580
  afterTokens,
3431
3581
  summary: `压缩管道: ${summaryLine}; 节省 ${savedTokens} tokens / ${saved} 条消息`,
3432
3582
  preservedMemory: [
3433
- ...this.messageHistory.filter(m => m.role === 'user').slice(-3).map(m => (m.content || '').slice(0, 80)),
3583
+ ...this.actor.state.messageHistory.filter(m => m.role === 'user').slice(-3).map(m => (m.content || '').slice(0, 80)),
3434
3584
  ],
3435
- agentId: this.currentAgentId,
3436
- channelId: this.currentChannelId,
3585
+ agentId: this.actor.state.agentId,
3586
+ channelId: this.actor.state.channelId,
3437
3587
  });
3438
3588
  cm.markCompressComplete(snap);
3439
3589
  }
@@ -3828,7 +3978,7 @@ ${this.extractOperationsFromRef(operationsRef)}
3828
3978
  Object.assign(this.identity, updates);
3829
3979
  }
3830
3980
  setCurrentChannelId(channelId) {
3831
- this.currentChannelId = channelId;
3981
+ this.actor.state.channelId = channelId;
3832
3982
  }
3833
3983
  getSessionState() {
3834
3984
  return this.sessionManager.getState();
@@ -3843,12 +3993,12 @@ ${this.extractOperationsFromRef(operationsRef)}
3843
3993
  await this.sessionManager.savePersona(persona);
3844
3994
  // 2026-10-01: **写透** —— 有身份文档的 agent 不套 persona.json, 只写 JSON 等于没改
3845
3995
  // (用户报"每次让智能体改都是同一个") ⇒ 同步落进它自己的身份文档 (标记区内)
3846
- if (this.currentAgentId) {
3996
+ if (this.actor.state.agentId) {
3847
3997
  try {
3848
3998
  const { applyPersonaToDocs } = await Promise.resolve().then(() => __importStar(require('../bootstrap/persona-init.js')));
3849
- const wrote = await applyPersonaToDocs(this.currentAgentId, persona);
3999
+ const wrote = await applyPersonaToDocs(this.actor.state.agentId, persona);
3850
4000
  if (wrote.length)
3851
- console.warn(`[persona] 已把 ${this.currentAgentId} 的 persona 写进身份文档: ${wrote.join(' · ')}`);
4001
+ console.warn(`[persona] 已把 ${this.actor.state.agentId} 的 persona 写进身份文档: ${wrote.join(' · ')}`);
3852
4002
  }
3853
4003
  catch { /* 非致命 */ }
3854
4004
  }