@lemoncat7/dsh-partner 1.12.0 → 1.14.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 (142) hide show
  1. package/README.md +22 -2
  2. package/docs/architecture.md +4 -0
  3. package/docs/releases/1.14.0.md +24 -0
  4. package/lib/agent-runtime.d.ts +23 -2
  5. package/lib/agent-runtime.d.ts.map +1 -1
  6. package/lib/agent-runtime.js +45 -282
  7. package/lib/agent-runtime.js.map +1 -1
  8. package/lib/api/features/workspace-api.d.ts.map +1 -1
  9. package/lib/api/features/workspace-api.js +7 -0
  10. package/lib/api/features/workspace-api.js.map +1 -1
  11. package/lib/api/http.d.ts +1 -1
  12. package/lib/api/http.d.ts.map +1 -1
  13. package/lib/api/http.js +2 -2
  14. package/lib/api/http.js.map +1 -1
  15. package/lib/api.js +34 -2
  16. package/lib/api.js.map +1 -1
  17. package/lib/autonomy.d.ts.map +1 -1
  18. package/lib/autonomy.js +13 -5
  19. package/lib/autonomy.js.map +1 -1
  20. package/lib/client-api.d.ts +8 -1
  21. package/lib/client-api.d.ts.map +1 -1
  22. package/lib/client-api.js.map +1 -1
  23. package/lib/client.js +1875 -1379
  24. package/lib/client.js.map +4 -4
  25. package/lib/collaboration/composition.js +7 -3
  26. package/lib/collaboration/composition.js.map +1 -1
  27. package/lib/companions/session-recovery.d.ts +7 -0
  28. package/lib/companions/session-recovery.d.ts.map +1 -0
  29. package/lib/companions/session-recovery.js +22 -0
  30. package/lib/companions/session-recovery.js.map +1 -0
  31. package/lib/concern-domain.d.ts +9 -0
  32. package/lib/concern-domain.d.ts.map +1 -1
  33. package/lib/concern-domain.js.map +1 -1
  34. package/lib/concern-recording.d.ts +20 -0
  35. package/lib/concern-recording.d.ts.map +1 -0
  36. package/lib/concern-recording.js +37 -0
  37. package/lib/concern-recording.js.map +1 -0
  38. package/lib/concern-store.d.ts +20 -1
  39. package/lib/concern-store.d.ts.map +1 -1
  40. package/lib/concern-store.js +170 -1
  41. package/lib/concern-store.js.map +1 -1
  42. package/lib/heartbeat.d.ts +16 -0
  43. package/lib/heartbeat.d.ts.map +1 -1
  44. package/lib/heartbeat.js +70 -5
  45. package/lib/heartbeat.js.map +1 -1
  46. package/lib/memory-errors.d.ts +6 -0
  47. package/lib/memory-errors.d.ts.map +1 -0
  48. package/lib/memory-errors.js +12 -0
  49. package/lib/memory-errors.js.map +1 -0
  50. package/lib/memory-journal.js +4 -4
  51. package/lib/memory-journal.js.map +1 -1
  52. package/lib/memory-reflection.d.ts.map +1 -1
  53. package/lib/memory-reflection.js +11 -3
  54. package/lib/memory-reflection.js.map +1 -1
  55. package/lib/memory-store.d.ts +4 -0
  56. package/lib/memory-store.d.ts.map +1 -1
  57. package/lib/memory-store.js +11 -3
  58. package/lib/memory-store.js.map +1 -1
  59. package/lib/observation-budget.d.ts +10 -0
  60. package/lib/observation-budget.d.ts.map +1 -0
  61. package/lib/observation-budget.js +11 -0
  62. package/lib/observation-budget.js.map +1 -0
  63. package/lib/observation-errors.d.ts +22 -0
  64. package/lib/observation-errors.d.ts.map +1 -0
  65. package/lib/observation-errors.js +37 -0
  66. package/lib/observation-errors.js.map +1 -0
  67. package/lib/observation-executor.d.ts +7 -0
  68. package/lib/observation-executor.d.ts.map +1 -0
  69. package/lib/observation-executor.js +23 -0
  70. package/lib/observation-executor.js.map +1 -0
  71. package/lib/observation-loop.d.ts +17 -0
  72. package/lib/observation-loop.d.ts.map +1 -0
  73. package/lib/observation-loop.js +195 -0
  74. package/lib/observation-loop.js.map +1 -0
  75. package/lib/recording-snapshot.d.ts +11 -0
  76. package/lib/recording-snapshot.d.ts.map +1 -0
  77. package/lib/recording-snapshot.js +19 -0
  78. package/lib/recording-snapshot.js.map +1 -0
  79. package/lib/skills/import-request.d.ts +3 -0
  80. package/lib/skills/import-request.d.ts.map +1 -0
  81. package/lib/skills/import-request.js +26 -0
  82. package/lib/skills/import-request.js.map +1 -0
  83. package/lib/skills/package-zip.d.ts +5 -0
  84. package/lib/skills/package-zip.d.ts.map +1 -0
  85. package/lib/skills/package-zip.js +75 -0
  86. package/lib/skills/package-zip.js.map +1 -0
  87. package/lib/skills/package.d.ts +15 -0
  88. package/lib/skills/package.d.ts.map +1 -0
  89. package/lib/skills/package.js +48 -0
  90. package/lib/skills/package.js.map +1 -0
  91. package/lib/skills/repository.d.ts +2 -0
  92. package/lib/skills/repository.d.ts.map +1 -1
  93. package/lib/skills/repository.js +7 -0
  94. package/lib/skills/repository.js.map +1 -1
  95. package/lib/skills/service.d.ts +2 -0
  96. package/lib/skills/service.d.ts.map +1 -1
  97. package/lib/skills/service.js +10 -2
  98. package/lib/skills/service.js.map +1 -1
  99. package/lib/skills/task-planning.d.ts +2 -2
  100. package/lib/skills/task-planning.d.ts.map +1 -1
  101. package/lib/skills/task-planning.js +8 -1
  102. package/lib/skills/task-planning.js.map +1 -1
  103. package/lib/ui/memory/concern-board.d.ts +4 -2
  104. package/lib/ui/memory/concern-board.d.ts.map +1 -1
  105. package/lib/ui/memory/concern-board.js +17 -6
  106. package/lib/ui/memory/concern-board.js.map +1 -1
  107. package/lib/ui/memory/concern-deletion.d.ts +12 -0
  108. package/lib/ui/memory/concern-deletion.d.ts.map +1 -0
  109. package/lib/ui/memory/concern-deletion.js +43 -0
  110. package/lib/ui/memory/concern-deletion.js.map +1 -0
  111. package/lib/ui/memory/concern-editor.d.ts +8 -0
  112. package/lib/ui/memory/concern-editor.d.ts.map +1 -0
  113. package/lib/ui/memory/concern-editor.js +37 -0
  114. package/lib/ui/memory/concern-editor.js.map +1 -0
  115. package/lib/ui/memory/concern-panel.d.ts.map +1 -1
  116. package/lib/ui/memory/concern-panel.js +51 -12
  117. package/lib/ui/memory/concern-panel.js.map +1 -1
  118. package/lib/ui/memory/concern-record-fields.d.ts +10 -0
  119. package/lib/ui/memory/concern-record-fields.d.ts.map +1 -0
  120. package/lib/ui/memory/concern-record-fields.js +7 -0
  121. package/lib/ui/memory/concern-record-fields.js.map +1 -0
  122. package/lib/ui/memory/job-feedback.d.ts +5 -0
  123. package/lib/ui/memory/job-feedback.d.ts.map +1 -0
  124. package/lib/ui/memory/job-feedback.js +7 -0
  125. package/lib/ui/memory/job-feedback.js.map +1 -0
  126. package/lib/ui/memory/memory-panel.d.ts.map +1 -1
  127. package/lib/ui/memory/memory-panel.js +4 -2
  128. package/lib/ui/memory/memory-panel.js.map +1 -1
  129. package/lib/ui/memory/record-target-picker.d.ts +7 -0
  130. package/lib/ui/memory/record-target-picker.d.ts.map +1 -0
  131. package/lib/ui/memory/record-target-picker.js +33 -0
  132. package/lib/ui/memory/record-target-picker.js.map +1 -0
  133. package/lib/ui/memory/types.d.ts +4 -0
  134. package/lib/ui/memory/types.d.ts.map +1 -1
  135. package/lib/ui/skill-import-dialog.d.ts +5 -0
  136. package/lib/ui/skill-import-dialog.d.ts.map +1 -0
  137. package/lib/ui/skill-import-dialog.js +72 -0
  138. package/lib/ui/skill-import-dialog.js.map +1 -0
  139. package/lib/ui/skills-panel.d.ts.map +1 -1
  140. package/lib/ui/skills-panel.js +3 -1
  141. package/lib/ui/skills-panel.js.map +1 -1
  142. package/package.json +2 -1
@@ -1,33 +1,23 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { mkdir, realpath, stat, writeFile } from 'node:fs/promises';
3
3
  import { isAbsolute, join, relative, resolve, sep } from 'node:path';
4
- import { BlockAssembler, createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm';
5
- import { createScope, scopeOf, scopeParentOf } from '@deepseek-ai/dsh-scope';
4
+ import { createUserMessage } from '@deepseek-ai/dsh-llm';
5
+ import { scopeOf, scopeParentOf } from '@deepseek-ai/dsh-scope';
6
6
  import { SessionConfigurationIndex } from './companions/session-configuration.js';
7
+ import { isMissingSession, SessionRecoveryJobs } from './companions/session-recovery.js';
7
8
  import { removeOwnedWorkspace } from './companions/workspace-cleanup.js';
8
9
  import { assertOwnedDirectory, removeOwnedDirectory } from './companions/directory-cleanup.js';
9
- import { concernObservationPrompt } from './autonomy.js';
10
10
  import { boundedConcernCheckMinutes } from './concern-domain.js';
11
11
  import { taskWorkContext } from './tasks/context.js';
12
12
  import { PARTNER_MEDIA_MAX_BYTES, safeMediaName } from './channel-message.js';
13
13
  import { listConcernFileSources } from './concern-sources.js';
14
14
  import { CONCERN_CREATED_NOTICE, renderConcernCreatedNotice } from './concern-notification.js';
15
- import { HEARTBEAT_LOCAL_COMMAND, heartbeatLocalCommandTool } from './heartbeat-command.js';
15
+ import { executeObservationLoop } from './observation-loop.js';
16
16
  import { assistantTextAfter, renderPartnerPersona, renderToolProtocol, resolvePartnerAgentOptions as resolveAgentOptions } from './execution/agent-support.js';
17
17
  import { channelReplyPartsAfter } from './channels/delivery-policy.js';
18
18
  import { prepareChannelReply } from './channels/outbound-media.js';
19
19
  export { extractOutboundAttachments } from './channels/outbound-media.js';
20
20
  export { renderToolProtocol, resolvePartnerAgentOptions as resolveAgentOptions } from './execution/agent-support.js';
21
- const HEARTBEAT_TIMEOUT_MS = 115_000;
22
- const HEARTBEAT_DISCOVERY_BUDGET_MS = 72_000;
23
- const HEARTBEAT_TOOL_TIMEOUT_MS = 18_000;
24
- const HEARTBEAT_WEB_TOOL_TIMEOUT_MS = 30_000;
25
- const HEARTBEAT_TOOL_RESULT_LIMIT = 5_000;
26
- const HEARTBEAT_KNOWLEDGE_RESULT_LIMIT = 8_000;
27
- const HEARTBEAT_WEB_FETCH_RESULT_LIMIT = 40_000;
28
- const HEARTBEAT_WEB_SOURCE_RESULT_LIMIT = 125_000;
29
- const HEARTBEAT_AUDIT_INPUT_LIMIT = 320;
30
- const HEARTBEAT_AUDIT_OUTPUT_LIMIT = 520;
31
21
  const PARTNER_TASK_TIMEOUT_MS = 30 * 60_000;
32
22
  const HEARTBEAT_READ_ONLY_TOOLS = new Set([
33
23
  'knowledge_base_search',
@@ -41,8 +31,6 @@ const HEARTBEAT_READ_ONLY_TOOLS = new Set([
41
31
  'read',
42
32
  ]);
43
33
  const HEARTBEAT_FILE_UPDATE_TOOLS = new Set(['write', 'edit', 'str_replace_editor']);
44
- const HEARTBEAT_TOOLS = new Set([...HEARTBEAT_READ_ONLY_TOOLS, ...HEARTBEAT_FILE_UPDATE_TOOLS]);
45
- HEARTBEAT_TOOLS.add(HEARTBEAT_LOCAL_COMMAND);
46
34
  export class PartnerAgentRuntime {
47
35
  ctx;
48
36
  store;
@@ -66,6 +54,7 @@ export class PartnerAgentRuntime {
66
54
  preparedTurns = new WeakMap();
67
55
  observationJobs = new Map();
68
56
  agentSetupJobs = new Map();
57
+ sessionRecoveryJobs = new SessionRecoveryJobs();
69
58
  closed = false;
70
59
  questionAnswerer;
71
60
  constructor(ctx, store, defaultCwd, memory, reflection, concerns, composer) {
@@ -328,44 +317,6 @@ export class PartnerAgentRuntime {
328
317
  this.heartbeatQueues.delete(key);
329
318
  }
330
319
  }
331
- async persistKnowledgeObservations(companion, route, concerns, observations) {
332
- const service = this.ctx.get('dshKnowledgeTracking');
333
- if (service === undefined || observations.length === 0)
334
- return;
335
- const agent = await this.ensureAgent(companion, route);
336
- const byId = new Map(concerns.map(item => [item.id, item]));
337
- const controller = new AbortController();
338
- const timeout = setTimeout(() => controller.abort(new Error('知识库观察记录超时')), 30_000);
339
- timeout.unref?.();
340
- try {
341
- for (const observation of observations) {
342
- if (observation.decision === 'drop')
343
- continue;
344
- const concern = byId.get(observation.concernId);
345
- if (concern === undefined)
346
- continue;
347
- const references = concern.resources.filter(item => item.kind === 'knowledge').map(item => item.locator).slice(0, 4);
348
- if (references.length === 0 && concern.watchKind !== 'knowledge')
349
- continue;
350
- for (const reference of references.length > 0 ? references : [undefined]) {
351
- await service.record(agent, {
352
- id: observation.id,
353
- subject: concern.subject,
354
- event: observation.event,
355
- evidence: observation.evidence,
356
- source: observation.source,
357
- ...(reference === undefined ? {} : { reference }),
358
- at: observation.createdAt,
359
- }, controller.signal).catch(error => {
360
- this.ctx.logger.warn(`dsh-partner: knowledge observation fell back to local storage: ${errorMessage(error)}`);
361
- });
362
- }
363
- }
364
- }
365
- finally {
366
- clearTimeout(timeout);
367
- }
368
- }
369
320
  async concernSources(companion, query = '') {
370
321
  const routes = this.store.snapshot().sessions
371
322
  .filter(item => item.companionId === companion.id)
@@ -390,6 +341,24 @@ export class PartnerAgentRuntime {
390
341
  clearTimeout(timeout);
391
342
  }
392
343
  }
344
+ async recordingSources(companion, query = '') {
345
+ const route = this.store.snapshot().sessions.filter(item => item.companionId === companion.id).sort((a, b) => b.lastMessageAt - a.lastMessageAt)[0];
346
+ const files = await listConcernFileSources(route?.cwd ?? partnerCwd(this.defaultCwd, companion.id), query, 24);
347
+ const bridge = this.ctx.get('dshKnowledgeNoteRecording');
348
+ const notes = bridge?.version === 1 ? await bridge.list(query, AbortSignal.timeout(8000)) : [];
349
+ return [...files.map(file => ({ kind: 'file', locator: file.label, label: file.label })), ...notes.map(note => ({ kind: 'note', locator: note.id, label: note.name }))];
350
+ }
351
+ async validateRecordingTarget(companion, raw) {
352
+ if (raw === undefined || raw === null)
353
+ return undefined;
354
+ if (!isRecord(raw) || typeof raw.locator !== 'string' || typeof raw.label !== 'string')
355
+ throw new Error('记录位置无效,请重新选择');
356
+ const choices = await this.recordingSources(companion, raw.kind === 'file' ? raw.locator : raw.label);
357
+ const selected = choices.find(item => item.kind === raw.kind && item.locator === raw.locator);
358
+ if (!selected)
359
+ throw new Error('记录位置已不存在或不可写,请重新选择');
360
+ return selected;
361
+ }
393
362
  async runHeartbeatWhenIdle(key, companion, route, concerns) {
394
363
  for (;;) {
395
364
  await this.queues.get(key)?.catch(() => { });
@@ -486,38 +455,9 @@ export class PartnerAgentRuntime {
486
455
  return true;
487
456
  }
488
457
  async driveScopedHeartbeat(conversation, companion, concerns) {
489
- const scope = await createHeartbeatScope(this.ctx, conversation, concerns);
490
- try {
491
- return await this.driveEphemeralHeartbeat(scope.agent, companion, concerns);
492
- }
493
- finally {
494
- await scope.dispose();
495
- }
496
- }
497
- async driveEphemeralHeartbeat(agent, companion, concerns) {
498
- const controller = new AbortController();
499
- const timeout = setTimeout(() => controller.abort(new Error('伙伴心跳超过安全时限')), HEARTBEAT_TIMEOUT_MS);
500
- timeout.unref?.();
501
- try {
502
- const execution = await runHeartbeatInference(this.ctx, agent, companion, concerns, controller.signal);
503
- return controller.signal.aborted
504
- ? { ...execution, error: '伙伴心跳超过安全时限,已收束并终止' }
505
- : execution;
506
- }
507
- catch (error) {
508
- const now = Date.now();
509
- return {
510
- concerns,
511
- candidates: [],
512
- startedAt: now,
513
- completedAt: now,
514
- tools: [],
515
- error: controller.signal.aborted ? '伙伴心跳超过安全时限,已收束并终止' : errorMessage(error),
516
- };
517
- }
518
- finally {
519
- clearTimeout(timeout);
520
- }
458
+ const access = await resolveHeartbeatFileAccess(conversation.session.header.cwd, concerns);
459
+ return executeObservationLoop({ ctx: this.ctx, conversation, companion, concerns, guard: (name, args) => heartbeatToolDenial(name, args, access), parse: parseConcernObservations, checkpoint: async (item, snapshot) => { if (!this.concerns)
460
+ throw new Error('关注存储不可用'); await this.concerns.checkpointRecording(item, snapshot); } });
521
461
  }
522
462
  async recordHeartbeatActivity(companion, route, execution, outcome, deliveryError) {
523
463
  const conversation = await this.ensureAgent(companion, route);
@@ -734,7 +674,7 @@ export class PartnerAgentRuntime {
734
674
  const id = companion.id;
735
675
  this.agentSetupJobs.set(id, (this.agentSetupJobs.get(id) ?? 0) + 1);
736
676
  try {
737
- return await this.ensureAgentReady(companion, route);
677
+ return await this.sessionRecoveryJobs.run(route.sessionId, () => this.ensureAgentReady(companion, route));
738
678
  }
739
679
  finally {
740
680
  const left = (this.agentSetupJobs.get(id) ?? 1) - 1;
@@ -779,7 +719,10 @@ export class PartnerAgentRuntime {
779
719
  setup,
780
720
  });
781
721
  }
782
- catch {
722
+ catch (error) {
723
+ // Never turn a preset/loading/permission failure into a duplicate create.
724
+ if (!isMissingSession(error, route.sessionId))
725
+ throw error;
783
726
  handle = await this.ctx.agents.create({
784
727
  sessionId: route.sessionId,
785
728
  meta: {
@@ -960,123 +903,13 @@ export function renewedSession(previous, now = Date.now()) {
960
903
  export function partnerCwd(root, companionId) {
961
904
  return join(root, 'partners', companionId);
962
905
  }
963
- async function createHeartbeatScope(ctx, conversation, concerns) {
964
- const parent = scopeOf(conversation.ctx);
965
- if (parent === undefined)
966
- throw new Error('伙伴心跳无法解析当前 Agent 作用域');
967
- const availableTools = [...HEARTBEAT_TOOLS].filter(name => ctx.tools.get(name, conversation) !== undefined);
968
- const policy = heartbeatToolPolicy(concerns, availableTools);
969
- const allowedTools = [...policy.allowed];
970
- const cwd = conversation.session.header.cwd;
971
- if (!cwd)
972
- throw new Error('伙伴心跳缺少当前伙伴工作目录');
973
- const fileAccess = await resolveHeartbeatFileAccess(cwd, concerns);
974
- const agent = {};
975
- const scope = createScope(ctx, agent, { parent });
976
- const agentCtx = scope.ctx.extend({ agent });
977
- Object.assign(agent, {
978
- id: conversation.id,
979
- options: conversation.options,
980
- session: conversation.session,
981
- inbox: conversation.inbox,
982
- status: 'idle',
983
- ctx: agentCtx,
984
- cancel: () => { },
985
- whenIdle: async () => { },
986
- runMaintenance: async (job) => job(new AbortController().signal),
987
- send: () => { throw new Error('心跳作用域不接受会话消息'); },
988
- followup: () => { throw new Error('心跳作用域不接受会话消息'); },
989
- steer: () => { throw new Error('心跳作用域不接受会话消息'); },
990
- inject: () => { throw new Error('心跳作用域不接受会话消息'); },
991
- });
992
- agentCtx.tools.presentAs('native');
993
- agentCtx.tools.register(heartbeatLocalCommandTool(cwd));
994
- if (allowedTools.length > 0)
995
- agentCtx.tools.restrict({ allow: allowedTools });
996
- agentCtx.tools.guard(exec => {
997
- if (exec.name !== HEARTBEAT_LOCAL_COMMAND && !policy.allowed.has(exec.name))
998
- return `当前挂念的来源策略不允许调用 ${exec.name}`;
999
- return heartbeatToolDenial(exec.name, exec.arguments, fileAccess);
1000
- });
1001
- return { agent, dispose: () => scope.dispose() };
1002
- }
1003
906
  export function heartbeatToolPolicy(concerns, available) {
1004
907
  const installed = new Set(available);
1005
- const hasLinkedFile = concerns.some(item => item.resources?.some(resource => resource.kind === 'file') === true);
908
+ const hasLinkedFile = concerns.some(item => item.recordTarget?.kind === 'file');
1006
909
  return {
1007
910
  allowed: new Set([...installed].filter(name => HEARTBEAT_READ_ONLY_TOOLS.has(name) || (hasLinkedFile && HEARTBEAT_FILE_UPDATE_TOOLS.has(name)))),
1008
911
  };
1009
912
  }
1010
- async function runHeartbeatInference(ctx, agent, companion, concerns, signal) {
1011
- const startedAt = Date.now();
1012
- const traces = [];
1013
- const discoveryDeadline = startedAt + HEARTBEAT_DISCOVERY_BUDGET_MS;
1014
- try {
1015
- const selection = resolveAgentOptions(ctx.agentDefaultModel, companion);
1016
- if (!selection.provider || !selection.model)
1017
- throw new Error('伙伴心跳没有可用的模型路由');
1018
- const tools = ctx.tools.schemas(agent).filter(tool => HEARTBEAT_TOOLS.has(tool.name));
1019
- if (tools.length === 0)
1020
- throw new Error('伙伴心跳没有可用的只读发现工具');
1021
- const messages = [createUserMessage({
1022
- content: [{ type: 'text', text: concernObservationPrompt(concerns, agent.session.header.cwd) }],
1023
- source: { kind: 'plugin', plugin: '@lemoncat7/dsh-partner', form: 'notice', summary: '伙伴观察挂念变化' },
1024
- })];
1025
- for (;;) {
1026
- const discoveryOpen = Date.now() < discoveryDeadline;
1027
- const prepared = await ctx.llm.prepareCall(selection, signal);
1028
- const assembler = new BlockAssembler();
1029
- for await (const chunk of prepared.stream({
1030
- ...prepared.config,
1031
- messages,
1032
- system: renderPartnerPersona(companion, 'heartbeat'),
1033
- tools: discoveryOpen ? tools : [],
1034
- signal,
1035
- }))
1036
- assembler.push(chunk);
1037
- const finish = assembler.finish;
1038
- if (finish.kind === 'error' || finish.kind === 'aborted') {
1039
- throw new Error(`伙伴心跳模型调用失败:${finish.failure.message}`);
1040
- }
1041
- const response = assembler.message({ kind: 'model', provider: prepared.config.provider, model: prepared.config.model });
1042
- messages.push(response);
1043
- const pending = response.content.filter((block) => block.type === 'tool-call');
1044
- if (pending.length === 0) {
1045
- const output = textContent(response.content);
1046
- return {
1047
- concerns,
1048
- candidates: parseConcernObservations(output, new Set(concerns.map(item => item.id))),
1049
- startedAt,
1050
- completedAt: Date.now(),
1051
- output,
1052
- tools: traces,
1053
- };
1054
- }
1055
- for (const call of pending) {
1056
- const argumentsValue = boundHeartbeatToolArguments(call.name, parseHeartbeatToolArguments(call));
1057
- const toolStartedAt = Date.now();
1058
- const remainingDiscoveryMs = discoveryDeadline - toolStartedAt;
1059
- const result = remainingDiscoveryMs <= 0
1060
- ? { isError: true, content: [{ type: 'text', text: '本轮发现阶段的时间预算已用完,请根据已有结果立即输出最终 Observation JSON。' }] }
1061
- : await executeHeartbeatTool(ctx, agent, call, argumentsValue, signal, remainingDiscoveryMs);
1062
- const toolCompletedAt = Date.now();
1063
- const visibleContent = heartbeatToolContent(call.name, result.content);
1064
- traces.push({
1065
- name: call.name,
1066
- input: auditValue(argumentsValue, HEARTBEAT_AUDIT_INPUT_LIMIT),
1067
- output: auditText(textContent(visibleContent) || '工具返回了非文本结果', HEARTBEAT_AUDIT_OUTPUT_LIMIT),
1068
- startedAt: toolStartedAt,
1069
- completedAt: toolCompletedAt,
1070
- status: result.isError ? 'failed' : 'completed',
1071
- });
1072
- messages.push(createToolResultMessage({ callId: call.id, content: visibleContent, isError: result.isError }));
1073
- }
1074
- }
1075
- }
1076
- catch (error) {
1077
- return { concerns, candidates: [], startedAt, completedAt: Date.now(), tools: traces, error: errorMessage(error) };
1078
- }
1079
- }
1080
913
  export function parseConcernObservations(raw, allowedIds) {
1081
914
  const match = raw.trim().match(/\{[\s\S]*\}/);
1082
915
  if (!match)
@@ -1115,18 +948,10 @@ export function parseConcernObservations(raw, allowedIds) {
1115
948
  function boundedScore(value) {
1116
949
  return typeof value === 'number' && Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : .5;
1117
950
  }
1118
- function parseHeartbeatToolArguments(call) {
1119
- try {
1120
- return JSON.parse(call.arguments);
1121
- }
1122
- catch {
1123
- return { __invalidArguments: call.arguments };
1124
- }
1125
- }
1126
951
  async function resolveHeartbeatFileAccess(cwd, concerns) {
1127
952
  const root = await realpath(cwd);
1128
953
  const writable = new Set();
1129
- const locators = concerns.flatMap(item => item.resources ?? []).filter(item => item.kind === 'file').map(item => item.locator);
954
+ const locators = concerns.flatMap(item => item.recordTarget?.kind === 'file' ? [item.recordTarget.locator] : []);
1130
955
  for (const locator of new Set(locators)) {
1131
956
  const lexical = resolve(root, locator);
1132
957
  if (!pathInside(root, lexical) || isPartnerMemoryPath(relative(root, lexical)))
@@ -1144,6 +969,8 @@ async function resolveHeartbeatFileAccess(cwd, concerns) {
1144
969
  return { root, writable };
1145
970
  }
1146
971
  export function heartbeatToolDenial(name, argumentsValue, access) {
972
+ if (['knowledge_note_references', 'knowledge_note_read', 'knowledge_note_update'].includes(name))
973
+ return '关注不处理依据中的笔记引用;记录仅使用手动配置的 recordTarget 和专用记录工具。';
1147
974
  if (!isRecord(argumentsValue))
1148
975
  return undefined;
1149
976
  const isRead = ['glob', 'grep', 'read'].includes(name);
@@ -1165,70 +992,10 @@ export function heartbeatToolDenial(name, argumentsValue, access) {
1165
992
  }
1166
993
  return undefined;
1167
994
  }
1168
- async function executeHeartbeatTool(ctx, agent, call, argumentsValue, signal, remainingDiscoveryMs) {
1169
- const configuredTimeoutMs = call.name === 'web_fetch' || call.name === 'web_source' ? HEARTBEAT_WEB_TOOL_TIMEOUT_MS : HEARTBEAT_TOOL_TIMEOUT_MS;
1170
- const timeoutMs = Math.max(1, Math.min(configuredTimeoutMs, remainingDiscoveryMs));
1171
- const timeoutSignal = AbortSignal.timeout(timeoutMs);
1172
- const toolSignal = AbortSignal.any([signal, timeoutSignal]);
1173
- try {
1174
- return await ctx.tools.execute({ callId: call.id, name: call.name, arguments: argumentsValue, agent, signal: toolSignal });
1175
- }
1176
- catch (error) {
1177
- const message = timeoutSignal.aborted && !signal.aborted
1178
- ? `工具 ${call.name} 超过 ${timeoutMs / 1_000} 秒,已跳过;请按既有规则判断来源不可用,或根据已有结果收束。`
1179
- : `工具 ${call.name} 调用失败:${errorMessage(error)}`;
1180
- return { isError: true, content: [{ type: 'text', text: message }] };
1181
- }
1182
- }
1183
995
  function pathInside(root, target) {
1184
996
  const value = relative(root, target);
1185
997
  return value === '' || (value !== '..' && !value.startsWith(`..${sep}`) && !isAbsolute(value));
1186
998
  }
1187
- function boundHeartbeatToolArguments(name, value) {
1188
- if (!isRecord(value))
1189
- return value;
1190
- const bounded = { ...value };
1191
- if (name === 'knowledge_base_search' || name === 'knowledge_search')
1192
- bounded.limit = boundedInteger(value.limit, 4);
1193
- if (name === 'knowledge_read')
1194
- bounded.maxChars = boundedInteger(value.maxChars, HEARTBEAT_KNOWLEDGE_RESULT_LIMIT);
1195
- if (name === 'web_source')
1196
- bounded.max_chars = boundedInteger(value.max_chars, 120_000);
1197
- if (name === 'read')
1198
- bounded.limit = boundedInteger(value.limit, 80);
1199
- return bounded;
1200
- }
1201
- function heartbeatToolContent(name, content) {
1202
- let remaining = heartbeatToolResultLimit(name);
1203
- const bounded = [];
1204
- for (const block of content) {
1205
- if (remaining <= 0)
1206
- break;
1207
- if (block.type !== 'text') {
1208
- bounded.push(block);
1209
- continue;
1210
- }
1211
- const sanitized = name === 'glob'
1212
- ? block.text.split('\n').filter(line => !isPartnerMemoryPath(line.trim())).join('\n')
1213
- : block.text;
1214
- const text = sanitized.length <= remaining ? sanitized : `${sanitized.slice(0, Math.max(0, remaining - 1))}…`;
1215
- if (text)
1216
- bounded.push({ ...block, text });
1217
- remaining -= text.length;
1218
- }
1219
- if (bounded.length === 0 && name === 'glob')
1220
- return [{ type: 'text', text: '未发现可供心跳检查的普通工作文件;伙伴记忆与会话归档已从结果中排除。' }];
1221
- return bounded;
1222
- }
1223
- function heartbeatToolResultLimit(name) {
1224
- if (name === 'knowledge_read')
1225
- return HEARTBEAT_KNOWLEDGE_RESULT_LIMIT;
1226
- if (name === 'web_fetch')
1227
- return HEARTBEAT_WEB_FETCH_RESULT_LIMIT;
1228
- if (name === 'web_source')
1229
- return HEARTBEAT_WEB_SOURCE_RESULT_LIMIT;
1230
- return HEARTBEAT_TOOL_RESULT_LIMIT;
1231
- }
1232
999
  function isPartnerMemoryPath(value) {
1233
1000
  const normalized = value.replaceAll('\\', '/').replace(/^\.\//, '/');
1234
1001
  return /(?:^|\/)(?:memory(?:\/|-backup(?:-|\/)|$)|concerns(?:\/|$))/i.test(normalized);
@@ -1240,45 +1007,41 @@ function isRecord(value) {
1240
1007
  return typeof value === 'object' && value !== null && !Array.isArray(value);
1241
1008
  }
1242
1009
  export function renderHeartbeatActivity(execution, outcome, deliveryError) {
1243
- const status = outcome === 'notified' ? '已通过微信提醒' : outcome === 'quiet' ? '无需主动提醒' : '执行失败';
1010
+ const status = execution.blocked?.length ? '检查受阻' : outcome === 'notified' ? '已通过微信提醒' : outcome === 'quiet' ? '检查完成,无需主动提醒' : '执行失败';
1244
1011
  const lines = [
1245
1012
  `状态:${status}`,
1246
1013
  `本轮挂念:${execution.concerns.map(item => item.subject).join(';')}`,
1247
1014
  `用时:${formatDuration(execution.completedAt - execution.startedAt)}`,
1248
1015
  `工具:${execution.tools.length > 0 ? `${execution.tools.length} 次调用` : '未调用'}`,
1016
+ ...(execution.recording ?? []).map(item => `记录同步:${execution.concerns.find(concern => concern.id === item.concernId)?.subject ?? item.concernId} · ${item.state === 'synced' ? '已写入手动指定位置' : '待同步(本轮未完成写入,下轮继续处理)'}`),
1017
+ ...(execution.rounds?.length ? [`模型轮次:${execution.rounds.map(item => `${item.round}${item.phase === 'final' ? '(收尾)' : ''} / ${(item.modelMs / 1000).toFixed(1)}秒 / ${item.outputChars}字`).join(';')}`] : []),
1249
1018
  ];
1250
1019
  execution.tools.forEach((trace, index) => {
1251
1020
  lines.push('', `${index + 1}. ${trace.name} · ${trace.status === 'completed' ? '完成' : '失败'} · ${formatDuration(trace.completedAt - trace.startedAt)}`, `输入:${trace.input}`, `结果:${trace.output}`);
1252
1021
  });
1022
+ for (const id of execution.blocked ?? [])
1023
+ lines.push(`受阻事项:${execution.concerns.find(item => item.id === id)?.subject ?? id};${execution.blockedReasons?.[id] ?? '目标数据尚未核实'}`);
1253
1024
  const changed = execution.observations ?? [];
1254
1025
  const conclusion = execution.error || deliveryError
1255
1026
  ? `失败原因:${deliveryError ?? execution.error}`
1256
1027
  : changed.length > 0
1257
1028
  ? `新变化:\n${changed.map(item => `- ${item.event}(${decisionLabel(item.decision)} · ${Math.round(item.interruptScore * 100)})`).join('\n')}`
1258
- : '最终结论:本轮没有发现经过校验的新变化。';
1029
+ : execution.concerns.some(item => item.recordingSnapshot) ? '最终结论:本轮仅执行记录同步,未抓取来源或发送变化提醒。' : '最终结论:本轮没有发现经过校验的新变化。';
1259
1030
  lines.push('', conclusion);
1260
1031
  return lines.join('\n');
1261
1032
  }
1262
1033
  function decisionLabel(value) {
1263
1034
  return value === 'notify' ? '已提醒' : value === 'feed' ? '伙伴动态' : value === 'defer' ? '顺手一提' : value === 'remember' ? '静默记下' : '忽略';
1264
1035
  }
1265
- function auditValue(value, limit) {
1266
- try {
1267
- return auditText(JSON.stringify(value), limit);
1268
- }
1269
- catch {
1270
- return auditText(String(value), limit);
1271
- }
1272
- }
1273
- function auditText(value, limit) {
1274
- const text = value.trim().replace(/\n{3,}/g, '\n\n');
1275
- return text.length <= limit ? text : `${text.slice(0, limit - 1)}…`;
1276
- }
1277
1036
  function formatDuration(milliseconds) {
1278
1037
  if (milliseconds < 1_000)
1279
1038
  return `${Math.max(0, milliseconds)} ms`;
1280
1039
  return `${(milliseconds / 1_000).toFixed(milliseconds < 10_000 ? 1 : 0)} 秒`;
1281
1040
  }
1041
+ function auditText(value, limit) {
1042
+ const text = value.trim().replace(/\n{3,}/g, '\n\n');
1043
+ return text.length <= limit ? text : `${text.slice(0, limit - 1)}…`;
1044
+ }
1282
1045
  function errorMessage(error) { return error instanceof Error ? error.message : String(error); }
1283
1046
  function textContent(content) {
1284
1047
  return content.filter((block) => block.type === 'text' && typeof block.text === 'string')