@lemoncat7/dsh-partner 1.8.2 → 1.9.2

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 (134) hide show
  1. package/README.md +34 -2
  2. package/docs/architecture.md +104 -12
  3. package/lib/agent-runtime.d.ts +15 -2
  4. package/lib/agent-runtime.d.ts.map +1 -1
  5. package/lib/agent-runtime.js +164 -106
  6. package/lib/agent-runtime.js.map +1 -1
  7. package/lib/api/features/workspace-api.d.ts.map +1 -1
  8. package/lib/api/features/workspace-api.js +1 -10
  9. package/lib/api/features/workspace-api.js.map +1 -1
  10. package/lib/api.js +21 -44
  11. package/lib/api.js.map +1 -1
  12. package/lib/channels/delivery-policy.d.ts +7 -0
  13. package/lib/channels/delivery-policy.d.ts.map +1 -0
  14. package/lib/channels/delivery-policy.js +47 -0
  15. package/lib/channels/delivery-policy.js.map +1 -0
  16. package/lib/channels/manager.d.ts +1 -2
  17. package/lib/channels/manager.d.ts.map +1 -1
  18. package/lib/channels/manager.js +5 -9
  19. package/lib/channels/manager.js.map +1 -1
  20. package/lib/client-api.d.ts +1 -0
  21. package/lib/client-api.d.ts.map +1 -1
  22. package/lib/client-api.js.map +1 -1
  23. package/lib/client.d.ts.map +1 -1
  24. package/lib/client.js +1255 -1100
  25. package/lib/client.js.map +4 -4
  26. package/lib/collaboration/composition.d.ts.map +1 -1
  27. package/lib/collaboration/composition.js +37 -13
  28. package/lib/collaboration/composition.js.map +1 -1
  29. package/lib/collaboration/service.d.ts +4 -0
  30. package/lib/collaboration/service.d.ts.map +1 -1
  31. package/lib/collaboration/service.js +109 -35
  32. package/lib/collaboration/service.js.map +1 -1
  33. package/lib/collaboration/task-dispatch.d.ts +18 -0
  34. package/lib/collaboration/task-dispatch.d.ts.map +1 -0
  35. package/lib/collaboration/task-dispatch.js +44 -0
  36. package/lib/collaboration/task-dispatch.js.map +1 -0
  37. package/lib/collaboration/task-recovery.d.ts +8 -0
  38. package/lib/collaboration/task-recovery.d.ts.map +1 -0
  39. package/lib/collaboration/task-recovery.js +30 -0
  40. package/lib/collaboration/task-recovery.js.map +1 -0
  41. package/lib/companions/directory-cleanup.d.ts +4 -0
  42. package/lib/companions/directory-cleanup.d.ts.map +1 -0
  43. package/lib/companions/directory-cleanup.js +77 -0
  44. package/lib/companions/directory-cleanup.js.map +1 -0
  45. package/lib/companions/management-tool.d.ts +1 -1
  46. package/lib/companions/management-tool.d.ts.map +1 -1
  47. package/lib/companions/management-tool.js +1 -1
  48. package/lib/companions/management-tool.js.map +1 -1
  49. package/lib/companions/management.d.ts +4 -2
  50. package/lib/companions/management.d.ts.map +1 -1
  51. package/lib/companions/management.js +2 -2
  52. package/lib/companions/management.js.map +1 -1
  53. package/lib/companions/removal.d.ts +20 -0
  54. package/lib/companions/removal.d.ts.map +1 -0
  55. package/lib/companions/removal.js +84 -0
  56. package/lib/companions/removal.js.map +1 -0
  57. package/lib/companions/service.d.ts +3 -0
  58. package/lib/companions/service.d.ts.map +1 -1
  59. package/lib/companions/service.js +4 -0
  60. package/lib/companions/service.js.map +1 -1
  61. package/lib/companions/session-configuration.d.ts +19 -0
  62. package/lib/companions/session-configuration.d.ts.map +1 -0
  63. package/lib/companions/session-configuration.js +38 -0
  64. package/lib/companions/session-configuration.js.map +1 -0
  65. package/lib/companions/workspace-cleanup.d.ts +4 -0
  66. package/lib/companions/workspace-cleanup.d.ts.map +1 -0
  67. package/lib/companions/workspace-cleanup.js +37 -0
  68. package/lib/companions/workspace-cleanup.js.map +1 -0
  69. package/lib/daily-review.d.ts +1 -0
  70. package/lib/daily-review.d.ts.map +1 -1
  71. package/lib/daily-review.js +3 -0
  72. package/lib/daily-review.js.map +1 -1
  73. package/lib/execution/service.d.ts.map +1 -1
  74. package/lib/execution/service.js +2 -0
  75. package/lib/execution/service.js.map +1 -1
  76. package/lib/heartbeat.d.ts +1 -0
  77. package/lib/heartbeat.d.ts.map +1 -1
  78. package/lib/heartbeat.js +3 -0
  79. package/lib/heartbeat.js.map +1 -1
  80. package/lib/index.d.ts.map +1 -1
  81. package/lib/index.js +5 -12
  82. package/lib/index.js.map +1 -1
  83. package/lib/pendant/notice-motion.d.ts.map +1 -1
  84. package/lib/pendant/notice-motion.js +12 -6
  85. package/lib/pendant/notice-motion.js.map +1 -1
  86. package/lib/pendant/sheen.d.ts +1 -1
  87. package/lib/pendant/sheen.d.ts.map +1 -1
  88. package/lib/pendant/sheen.js +9 -8
  89. package/lib/pendant/sheen.js.map +1 -1
  90. package/lib/pendant/widget.js +1 -1
  91. package/lib/pendant/widget.js.map +1 -1
  92. package/lib/pendant-renderer.js +17 -17
  93. package/lib/skills/builtin.d.ts.map +1 -1
  94. package/lib/skills/builtin.js +3 -2
  95. package/lib/skills/builtin.js.map +1 -1
  96. package/lib/skills/service.js +1 -1
  97. package/lib/skills/service.js.map +1 -1
  98. package/lib/skills/task-planning.d.ts +3 -0
  99. package/lib/skills/task-planning.d.ts.map +1 -0
  100. package/lib/skills/task-planning.js +31 -0
  101. package/lib/skills/task-planning.js.map +1 -0
  102. package/lib/store.d.ts +6 -0
  103. package/lib/store.d.ts.map +1 -1
  104. package/lib/store.js +12 -0
  105. package/lib/store.js.map +1 -1
  106. package/lib/tasks/domain.d.ts +2 -0
  107. package/lib/tasks/domain.d.ts.map +1 -1
  108. package/lib/tasks/progress-notifier.d.ts +4 -0
  109. package/lib/tasks/progress-notifier.d.ts.map +1 -0
  110. package/lib/tasks/progress-notifier.js +20 -0
  111. package/lib/tasks/progress-notifier.js.map +1 -0
  112. package/lib/tasks/service.d.ts.map +1 -1
  113. package/lib/tasks/service.js +22 -2
  114. package/lib/tasks/service.js.map +1 -1
  115. package/lib/ui/capability-editor.d.ts +7 -0
  116. package/lib/ui/capability-editor.d.ts.map +1 -0
  117. package/lib/ui/capability-editor.js +85 -0
  118. package/lib/ui/capability-editor.js.map +1 -0
  119. package/lib/ui/companion-draft.d.ts +12 -0
  120. package/lib/ui/companion-draft.d.ts.map +1 -0
  121. package/lib/ui/companion-draft.js +2 -0
  122. package/lib/ui/companion-draft.js.map +1 -0
  123. package/lib/ui/identity-editor.d.ts +11 -0
  124. package/lib/ui/identity-editor.d.ts.map +1 -0
  125. package/lib/ui/identity-editor.js +78 -0
  126. package/lib/ui/identity-editor.js.map +1 -0
  127. package/lib/ui/task-board-panel.d.ts.map +1 -1
  128. package/lib/ui/task-board-panel.js +15 -10
  129. package/lib/ui/task-board-panel.js.map +1 -1
  130. package/lib/ui/task-board-stage.d.ts +12 -0
  131. package/lib/ui/task-board-stage.d.ts.map +1 -0
  132. package/lib/ui/task-board-stage.js +13 -0
  133. package/lib/ui/task-board-stage.js.map +1 -0
  134. package/package.json +1 -1
@@ -3,6 +3,9 @@ import { mkdir, realpath, stat, writeFile } from 'node:fs/promises';
3
3
  import { basename, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
4
  import { BlockAssembler, createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm';
5
5
  import { createScope, scopeOf, scopeParentOf } from '@deepseek-ai/dsh-scope';
6
+ import { SessionConfigurationIndex } from './companions/session-configuration.js';
7
+ import { removeOwnedWorkspace } from './companions/workspace-cleanup.js';
8
+ import { assertOwnedDirectory, removeOwnedDirectory } from './companions/directory-cleanup.js';
6
9
  import { concernObservationPrompt } from './autonomy.js';
7
10
  import { boundedConcernCheckMinutes } from './concern-domain.js';
8
11
  import { PARTNER_MEDIA_MAX_BYTES, safeMediaName } from './channel-message.js';
@@ -10,6 +13,7 @@ import { listConcernFileSources } from './concern-sources.js';
10
13
  import { CONCERN_CREATED_NOTICE, renderConcernCreatedNotice } from './concern-notification.js';
11
14
  import { HEARTBEAT_LOCAL_COMMAND, heartbeatLocalCommandTool } from './heartbeat-command.js';
12
15
  import { assistantTextAfter, renderPartnerPersona, renderToolProtocol, resolvePartnerAgentOptions as resolveAgentOptions } from './execution/agent-support.js';
16
+ import { channelReplyTextAfter } from './channels/delivery-policy.js';
13
17
  export { renderToolProtocol, resolvePartnerAgentOptions as resolveAgentOptions } from './execution/agent-support.js';
14
18
  const HEARTBEAT_TIMEOUT_MS = 115_000;
15
19
  const HEARTBEAT_DISCOVERY_BUDGET_MS = 72_000;
@@ -54,6 +58,12 @@ export class PartnerAgentRuntime {
54
58
  restoredCompositions = new Map();
55
59
  restoredCompositionJobs = new Map();
56
60
  disposeAgentListener;
61
+ disposeConfigurationListener;
62
+ configurations;
63
+ preparedTurns = new WeakMap();
64
+ observationJobs = new Map();
65
+ agentSetupJobs = new Map();
66
+ closed = false;
57
67
  questionAnswerer;
58
68
  constructor(ctx, store, defaultCwd, memory, reflection, concerns, composer) {
59
69
  this.ctx = ctx;
@@ -63,8 +73,15 @@ export class PartnerAgentRuntime {
63
73
  this.reflection = reflection;
64
74
  this.concerns = concerns;
65
75
  this.composer = composer;
76
+ this.configurations = new SessionConfigurationIndex(store);
77
+ this.disposeConfigurationListener = typeof this.ctx.on === 'function'
78
+ ? this.ctx.on('agent/pre-step', async ({ agent, turn, signal }, next) => {
79
+ await this.prepareAgentTurn(agent, turn, signal);
80
+ return next();
81
+ }) : () => { };
66
82
  this.disposeAgentListener = typeof this.ctx.on === 'function'
67
83
  ? this.ctx.on('agent/disposed', ({ agent }) => {
84
+ this.preparedTurns.delete(agent);
68
85
  const restored = this.restoredCompositions.get(agent.session.id);
69
86
  if (restored?.agent !== agent)
70
87
  return;
@@ -145,6 +162,8 @@ export class PartnerAgentRuntime {
145
162
  return route;
146
163
  }
147
164
  async ensureLocalSessionRecord(companionId) {
165
+ if (this.store.isCompanionRemoving(companionId))
166
+ throw new Error('伙伴正在删除,不能创建会话');
148
167
  const companion = this.store.snapshot().companions.find(item => item.id === companionId);
149
168
  if (companion === undefined)
150
169
  throw new Error('伙伴身份不存在');
@@ -164,6 +183,8 @@ export class PartnerAgentRuntime {
164
183
  lastMessageAt: now,
165
184
  };
166
185
  await this.store.update(state => {
186
+ if (this.store.isCompanionRemoving(companion.id) || !state.companions.some(item => item.id === companion.id))
187
+ throw new Error('伙伴正在删除或已不存在');
167
188
  state.sessions = state.sessions.filter(item => !(item.companionId === companion.id && item.kind === 'local'));
168
189
  state.sessions.push(route);
169
190
  });
@@ -191,9 +212,9 @@ export class PartnerAgentRuntime {
191
212
  const agent = await this.ensureAgent(companion, route);
192
213
  const isReviewer = task.status === 'review' && task.reviewerCompanionId === companion.id;
193
214
  const instruction = isReviewer
194
- ? '这是你创建并分配的看板任务,执行伙伴已提交结果,现在默认由你验收。请根据任务要求真实核验已有产出;通过后必须调用 partner_task_board accept,不通过则调用 reject 并写明原因。不要重新执行该任务。'
215
+ ? '这是伙伴内部验收,不是用户的新消息。执行伙伴已提交结果,现在默认由你验收。请根据任务要求真实核验已有产出;通过后必须调用 partner_task_board accept,不通过则调用 reject 并写明原因。不要重新执行该任务,不要向用户播报核验过程或 accept 操作;最终结果由系统直接投递。'
195
216
  : task.status === 'review'
196
- ? '这是你创建并分配的看板任务进度事件。执行结果已提交并等待指定伙伴验收;不要重新执行,请向用户简要说明当前进度。'
217
+ ? '这是伙伴内部进度事件。执行结果已提交并等待指定伙伴验收;不要重新执行,也不要向用户播报中间进度。只在看板中推进必要的协作,最终结果由系统直接投递。'
197
218
  : task.status === 'done'
198
219
  ? '这是你创建并分配的看板任务终态事件。不要重新执行已完成的工作。请检查依赖它的后续任务是否解锁,需要时继续在看板上分配。对用户的终态结果由系统直接投递,不要只改写成“已完成”或进度摘要。'
199
220
  : '这是你创建并分配的看板任务受阻事件。请保留已有产出、检查是否能调整分工或解锁依赖;需要用户介入时,明确说明阻塞原因与所需动作。';
@@ -364,6 +385,25 @@ export class PartnerAgentRuntime {
364
385
  }
365
386
  }
366
387
  async observeSessionEvent(session, event) {
388
+ if (event.type !== 'turn/end' || event.data.reason.kind !== 'completed')
389
+ return;
390
+ const route = this.store.snapshot().sessions.find(item => item.sessionId === session.id);
391
+ if (!route || this.store.isCompanionRemoving(route.companionId))
392
+ return;
393
+ const id = route.companionId;
394
+ this.observationJobs.set(id, (this.observationJobs.get(id) ?? 0) + 1);
395
+ try {
396
+ await this.processSessionEvent(session, event);
397
+ }
398
+ finally {
399
+ const left = (this.observationJobs.get(id) ?? 1) - 1;
400
+ if (left)
401
+ this.observationJobs.set(id, left);
402
+ else
403
+ this.observationJobs.delete(id);
404
+ }
405
+ }
406
+ async processSessionEvent(session, event) {
367
407
  if (event.type !== 'turn/end' || event.data.reason.kind !== 'completed')
368
408
  return;
369
409
  const route = this.store.snapshot().sessions.find(item => item.sessionId === session.id);
@@ -476,35 +516,65 @@ export class PartnerAgentRuntime {
476
516
  await this.releaseCompanion(companionId);
477
517
  await this.store.update(state => { state.sessions = state.sessions.filter(item => item.companionId !== companionId); });
478
518
  }
519
+ async removeCompanionWorkspace(companionId) {
520
+ const cwd = partnerCwd(this.defaultCwd, companionId);
521
+ const sharedPaths = this.store.snapshot().sessions.filter(route => route.companionId !== companionId).map(route => route.cwd ?? this.defaultCwd);
522
+ await removeOwnedWorkspace(this.ctx.workspaceRegistry, cwd, companionId, sharedPaths);
523
+ this.workspaces.delete(cwd);
524
+ }
525
+ sharedDirectoryPaths(companionId) {
526
+ const state = this.store.snapshot();
527
+ const cwd = resolve(partnerCwd(this.defaultCwd, companionId));
528
+ return [
529
+ ...state.companions.filter(item => item.id !== companionId).map(item => partnerCwd(this.defaultCwd, item.id)),
530
+ ...state.sessions.filter(route => route.companionId !== companionId).map(route => route.cwd ?? this.defaultCwd),
531
+ ...this.ctx.workspaceRegistry.list().filter(item => resolve(item.path) !== cwd).map(item => item.path),
532
+ ];
533
+ }
534
+ async validateCompanionDirectory(companionId) {
535
+ await assertOwnedDirectory(this.defaultCwd, companionId, this.sharedDirectoryPaths(companionId));
536
+ }
537
+ async removeCompanionDirectory(companionId) {
538
+ await removeOwnedDirectory(this.defaultCwd, companionId, this.sharedDirectoryPaths(companionId));
539
+ }
479
540
  async reloadCompanion(companionId) {
480
- if (this.isCompanionBusy(companionId))
481
- throw new Error('伙伴正在执行,请等待当前回复结束后再修改能力');
482
- const activeRoutes = this.store.snapshot().sessions.filter(route => route.companionId === companionId
483
- && (this.handles.has(route.sessionId) || this.ctx.agents.get(route.sessionId) !== undefined));
484
- await this.releaseCompanion(companionId);
485
- // Releasing a restored composition does not close the browser-owned Agent.
486
- // Recompose it now: staying in the same conversation must not leave it with
487
- // no partner tools until the user happens to click "open session" again.
488
- for (const route of activeRoutes) {
489
- const state = this.store.snapshot();
490
- const companion = state.companions.find(item => item.id === companionId);
491
- const currentRoute = state.sessions.find(item => item.id === route.id && item.companionId === companionId);
492
- if (companion && currentRoute)
493
- await this.ensureAgent(companion, currentRoute);
494
- }
541
+ // Compatibility for callers that saved a configuration. The index already
542
+ // observes committed writes; do not tear down or wake any Agent here.
543
+ if (!this.configurations.companion(companionId))
544
+ throw new Error('伙伴不存在');
495
545
  }
496
- async reloadManagedCompanion(companionId) {
497
- const externallyOwned = this.store.snapshot().sessions.some(route => route.companionId === companionId
498
- && !this.handles.has(route.sessionId) && this.ctx.agents.get(route.sessionId) !== undefined);
499
- await this.reloadCompanion(companionId);
500
- // Browser-owned handles cannot be disposed by this plugin. In particular,
501
- // their preset/model must not be reported as hot-reloaded successfully.
502
- if (externallyOwned) {
503
- throw new Error('目标伙伴还有由 DSH 管理的活动会话,请重新打开会话使配置生效');
546
+ /** Awaited by the host before prompt/tool assembly, never from a running tool. */
547
+ async prepareAgentTurn(agent, turn, signal) {
548
+ if (this.closed)
549
+ throw new Error('伙伴运行时已关闭');
550
+ if (this.preparedTurns.get(agent) === turn)
551
+ return;
552
+ const configuration = this.configurations.forSession(agent.session.id);
553
+ if (!configuration)
554
+ return;
555
+ if (this.store.isCompanionRemoving(configuration.companion.id))
556
+ throw new Error('伙伴正在删除,不能启动新轮次');
557
+ signal.throwIfAborted();
558
+ const installed = this.restoredCompositions.get(agent.session.id);
559
+ if (installed?.agent !== agent || installed.revision !== configuration.revision) {
560
+ const profile = await this.memory?.profileSnapshot(configuration.companion.id, memoryScope(configuration.route.channelId, configuration.route.userId)).catch(() => undefined);
561
+ signal.throwIfAborted();
562
+ if (this.closed)
563
+ throw new Error('伙伴运行时已关闭');
564
+ const job = this.composeRestoredAgent(agent, configuration.companion, configuration.route, configuration.revision, profile, signal);
565
+ this.restoredCompositionJobs.set(agent.session.id, job);
566
+ try {
567
+ await job;
568
+ }
569
+ finally {
570
+ if (this.restoredCompositionJobs.get(agent.session.id) === job)
571
+ this.restoredCompositionJobs.delete(agent.session.id);
572
+ }
504
573
  }
574
+ this.preparedTurns.set(agent, turn);
505
575
  }
506
576
  isCompanionBusy(companionId) {
507
- return this.store.snapshot().sessions.some(route => route.companionId === companionId
577
+ return this.observationJobs.has(companionId) || this.agentSetupJobs.has(companionId) || this.store.snapshot().sessions.some(route => route.companionId === companionId
508
578
  && (this.handles.get(route.sessionId)?.agent ?? this.ctx.agents.get(route.sessionId))?.status === 'running');
509
579
  }
510
580
  async releaseCompanion(companionId) {
@@ -529,7 +599,10 @@ export class PartnerAgentRuntime {
529
599
  await this.store.update(state => { state.sessions = state.sessions.filter(item => item.channelId !== channelId); });
530
600
  }
531
601
  async close() {
602
+ this.closed = true;
603
+ this.disposeConfigurationListener();
532
604
  this.disposeAgentListener();
605
+ this.configurations.close();
533
606
  await Promise.allSettled(this.restoredCompositionJobs.values());
534
607
  for (const restored of this.restoredCompositions.values())
535
608
  restored.dispose();
@@ -545,6 +618,8 @@ export class PartnerAgentRuntime {
545
618
  async releaseRestoredComposition(sessionId) {
546
619
  await this.restoredCompositionJobs.get(sessionId)?.catch(() => { });
547
620
  const restored = this.restoredCompositions.get(sessionId);
621
+ if (restored)
622
+ this.preparedTurns.delete(restored.agent);
548
623
  restored?.dispose();
549
624
  this.restoredCompositions.delete(sessionId);
550
625
  }
@@ -575,7 +650,7 @@ export class PartnerAgentRuntime {
575
650
  source: { kind: 'user' },
576
651
  }));
577
652
  await agent.whenIdle();
578
- const response = assistantTextAfter(agent, startSeq);
653
+ const response = channelReplyTextAfter(agent.session.snapshotEvents(), startSeq);
579
654
  await this.store.update(state => {
580
655
  const target = state.sessions.find(item => item.id === session.id);
581
656
  if (target)
@@ -635,6 +710,22 @@ export class PartnerAgentRuntime {
635
710
  return session;
636
711
  }
637
712
  async ensureAgent(companion, route) {
713
+ if (this.store.isCompanionRemoving(companion.id) || !this.store.snapshot().companions.some(item => item.id === companion.id))
714
+ throw new Error('伙伴正在删除或已不存在');
715
+ const id = companion.id;
716
+ this.agentSetupJobs.set(id, (this.agentSetupJobs.get(id) ?? 0) + 1);
717
+ try {
718
+ return await this.ensureAgentReady(companion, route);
719
+ }
720
+ finally {
721
+ const left = (this.agentSetupJobs.get(id) ?? 1) - 1;
722
+ if (left)
723
+ this.agentSetupJobs.set(id, left);
724
+ else
725
+ this.agentSetupJobs.delete(id);
726
+ }
727
+ }
728
+ async ensureAgentReady(companion, route) {
638
729
  const cwd = route.cwd ?? this.defaultCwd;
639
730
  await mkdir(cwd, { recursive: true, mode: 0o700 });
640
731
  const profile = await this.memory?.profileSnapshot(companion.id, memoryScope(route.channelId, route.userId)).catch(() => undefined);
@@ -647,7 +738,6 @@ export class PartnerAgentRuntime {
647
738
  }
648
739
  const live = this.ctx.agents.get(route.sessionId);
649
740
  if (live !== undefined) {
650
- await this.ensureRestoredComposition(live, companion, route, profile);
651
741
  await this.attachSession(companion, route);
652
742
  if (profile)
653
743
  this.injectProfileUpdate(live, route.sessionId, profile);
@@ -659,24 +749,8 @@ export class PartnerAgentRuntime {
659
749
  if (presets === undefined)
660
750
  throw new Error('伙伴会话缺少 Agent Presets 服务');
661
751
  await presets.mount(agentCtx, companion.presetId);
662
- agentCtx.systemPrompt.section({
663
- name: 'partner-identity',
664
- order: -10,
665
- text: renderPartnerPersona(companion, route.kind === 'local' ? 'local' : 'conversation'),
666
- });
667
- agentCtx.systemPrompt.section({
668
- name: 'partner-tool-routing',
669
- order: -9,
670
- text: renderToolProtocol(),
671
- });
672
- if (profile && profile.entries.length > 0)
673
- agentCtx.systemPrompt.section({
674
- name: 'partner-user-profile',
675
- order: -8,
676
- text: renderProfile(profile),
677
- });
678
- this.questionAnswerer?.(agentCtx, route);
679
- await this.composer?.compose(agentCtx, companion);
752
+ // Partner composition is installed at the same awaited pre-step boundary
753
+ // for factory-created and native browser-resumed Agents alike.
680
754
  };
681
755
  let handle;
682
756
  try {
@@ -703,69 +777,53 @@ export class PartnerAgentRuntime {
703
777
  await this.attachSession(companion, route);
704
778
  return handle.agent;
705
779
  }
706
- async ensureRestoredComposition(agent, companion, route, profile) {
707
- const installed = this.restoredCompositions.get(route.sessionId);
708
- if (installed?.agent === agent)
709
- return;
710
- const pending = this.restoredCompositionJobs.get(route.sessionId);
711
- if (pending !== undefined)
712
- return pending;
713
- const job = this.composeRestoredAgent(agent, companion, route, profile);
714
- this.restoredCompositionJobs.set(route.sessionId, job);
715
- try {
716
- await job;
717
- }
718
- finally {
719
- if (this.restoredCompositionJobs.get(route.sessionId) === job)
720
- this.restoredCompositionJobs.delete(route.sessionId);
780
+ async composeRestoredAgent(agent, companion, route, revision, profile, signal) {
781
+ // Already inside the driver's awaited pre-step hook. Waiting for idle or
782
+ // runMaintenance here would deadlock the very turn we are preparing.
783
+ signal.throwIfAborted();
784
+ const agentCtx = agent.ctx;
785
+ const key = scopeOf(agentCtx);
786
+ if (key === undefined)
787
+ throw new Error('伙伴会话缺少 Agent 作用域');
788
+ if (scopeParentOf(key) === undefined) {
789
+ const presets = agentCtx.get('agentPresets');
790
+ if (presets === undefined)
791
+ throw new Error('伙伴会话缺少 Agent Presets 服务');
792
+ await presets.mount(agentCtx, companion.presetId);
721
793
  }
722
- }
723
- async composeRestoredAgent(agent, companion, route, profile) {
724
- await agent.whenIdle();
725
- let dispose;
726
- await agent.runMaintenance(async (signal) => {
727
- if (signal.aborted)
728
- throw signal.reason;
729
- const agentCtx = agent.ctx;
730
- const key = scopeOf(agentCtx);
731
- if (key === undefined)
732
- throw new Error('伙伴会话缺少 Agent 作用域');
733
- if (scopeParentOf(key) === undefined) {
734
- const presets = agentCtx.get('agentPresets');
735
- if (presets === undefined)
736
- throw new Error('伙伴会话缺少 Agent Presets 服务');
737
- await presets.mount(agentCtx, companion.presetId);
738
- }
739
- const disposers = [];
740
- try {
741
- disposers.push(agentCtx.systemPrompt.section({ name: 'partner-identity', order: -10, text: renderPartnerPersona(companion, route.kind === 'local' ? 'local' : 'conversation') }));
742
- disposers.push(agentCtx.systemPrompt.section({ name: 'partner-tool-routing', order: -9, text: renderToolProtocol() }));
743
- if (profile && profile.entries.length > 0)
744
- disposers.push(agentCtx.systemPrompt.section({ name: 'partner-user-profile', order: -8, text: renderProfile(profile) }));
745
- if (this.questionAnswerer)
746
- disposers.push(this.questionAnswerer(agentCtx, route));
747
- if (this.composer)
748
- disposers.push(await this.composer.compose(agentCtx, companion));
749
- }
750
- catch (error) {
751
- for (let index = disposers.length - 1; index >= 0; index -= 1)
752
- disposers[index]?.();
753
- throw error;
754
- }
755
- let disposed = false;
756
- dispose = () => {
757
- if (disposed)
758
- return;
759
- disposed = true;
760
- for (let index = disposers.length - 1; index >= 0; index -= 1)
761
- disposers[index]?.();
762
- };
763
- });
764
- if (dispose === undefined)
765
- throw new Error('伙伴会话恢复组合未完成');
766
794
  const previous = this.restoredCompositions.get(route.sessionId);
767
795
  previous?.dispose();
768
- this.restoredCompositions.set(route.sessionId, { agent, dispose });
796
+ this.restoredCompositions.delete(route.sessionId);
797
+ const disposers = [];
798
+ try {
799
+ disposers.push(agentCtx.systemPrompt.section({ name: 'partner-identity', order: -10, text: renderPartnerPersona(companion, route.kind === 'local' ? 'local' : 'conversation') }));
800
+ disposers.push(agentCtx.systemPrompt.section({ name: 'partner-tool-routing', order: -9, text: renderToolProtocol() }));
801
+ if (profile && profile.entries.length > 0)
802
+ disposers.push(agentCtx.systemPrompt.section({ name: 'partner-user-profile', order: -8, text: renderProfile(profile) }));
803
+ if (this.questionAnswerer)
804
+ disposers.push(this.questionAnswerer(agentCtx, route));
805
+ if (this.composer)
806
+ disposers.push(await this.composer.compose(agentCtx, companion));
807
+ signal.throwIfAborted();
808
+ if (this.closed)
809
+ throw new Error('伙伴运行时已关闭');
810
+ }
811
+ catch (error) {
812
+ for (let index = disposers.length - 1; index >= 0; index -= 1)
813
+ disposers[index]?.();
814
+ throw error;
815
+ }
816
+ let disposed = false;
817
+ const dispose = () => {
818
+ if (disposed)
819
+ return;
820
+ disposed = true;
821
+ for (let index = disposers.length - 1; index >= 0; index -= 1)
822
+ disposers[index]?.();
823
+ };
824
+ // Keep the captured revision: a concurrent save must be applied next turn,
825
+ // never incorrectly recorded as already installed by this composition.
826
+ this.restoredCompositions.set(route.sessionId, { agent, dispose, revision });
769
827
  }
770
828
  injectProfileUpdate(agent, sessionId, profile) {
771
829
  const previous = this.profileVersions.get(sessionId);