@lemoncat7/dsh-partner 2.11.6 → 2.21.5

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 (355) hide show
  1. package/README.md +3 -0
  2. package/docs/character-cards-and-renewal.md +1 -1
  3. package/docs/qq-attention.md +51 -0
  4. package/docs/qq-channel.md +80 -0
  5. package/docs/releases/2.11.7.md +9 -0
  6. package/docs/releases/2.12.0.md +14 -0
  7. package/docs/releases/2.13.0.md +9 -0
  8. package/docs/releases/2.14.0.md +9 -0
  9. package/docs/releases/2.15.0.md +9 -0
  10. package/docs/releases/2.15.1.md +9 -0
  11. package/docs/releases/2.16.0.md +18 -0
  12. package/docs/releases/2.17.0.md +23 -0
  13. package/docs/releases/2.17.1.md +9 -0
  14. package/docs/releases/2.18.0.md +9 -0
  15. package/docs/releases/2.18.1.md +5 -0
  16. package/docs/releases/2.19.0.md +14 -0
  17. package/docs/releases/2.19.1.md +7 -0
  18. package/docs/releases/2.19.2.md +9 -0
  19. package/docs/releases/2.19.3.md +5 -0
  20. package/docs/releases/2.20.0.md +10 -0
  21. package/docs/releases/2.20.1.md +8 -0
  22. package/docs/releases/2.21.0.md +7 -0
  23. package/docs/releases/2.21.1.md +8 -0
  24. package/docs/releases/2.21.2.md +10 -0
  25. package/docs/releases/2.21.3.md +7 -0
  26. package/docs/releases/2.21.4.md +8 -0
  27. package/docs/releases/2.21.5.md +15 -0
  28. package/docs/tool-entry-compatibility.md +11 -0
  29. package/lib/agent-runtime.d.ts +17 -0
  30. package/lib/agent-runtime.d.ts.map +1 -1
  31. package/lib/agent-runtime.js +313 -52
  32. package/lib/agent-runtime.js.map +1 -1
  33. package/lib/api.d.ts +1 -0
  34. package/lib/api.d.ts.map +1 -1
  35. package/lib/api.js +93 -27
  36. package/lib/api.js.map +1 -1
  37. package/lib/channel-message.d.ts +14 -0
  38. package/lib/channel-message.d.ts.map +1 -1
  39. package/lib/channel-message.js.map +1 -1
  40. package/lib/channels/delivery-policy.js +1 -1
  41. package/lib/channels/delivery-policy.js.map +1 -1
  42. package/lib/channels/direct/dispatcher.d.ts +2 -2
  43. package/lib/channels/direct/dispatcher.d.ts.map +1 -1
  44. package/lib/channels/direct/dispatcher.js +13 -4
  45. package/lib/channels/direct/dispatcher.js.map +1 -1
  46. package/lib/channels/direct/login.d.ts.map +1 -1
  47. package/lib/channels/direct/login.js +4 -0
  48. package/lib/channels/direct/login.js.map +1 -1
  49. package/lib/channels/direct/media.d.ts +2 -0
  50. package/lib/channels/direct/media.d.ts.map +1 -1
  51. package/lib/channels/direct/media.js +27 -23
  52. package/lib/channels/direct/media.js.map +1 -1
  53. package/lib/channels/direct/message-format.js +127 -127
  54. package/lib/channels/direct/message-format.js.map +1 -1
  55. package/lib/channels/direct/transport.d.ts +26 -3
  56. package/lib/channels/direct/transport.d.ts.map +1 -1
  57. package/lib/channels/direct/transport.js +31 -5
  58. package/lib/channels/direct/transport.js.map +1 -1
  59. package/lib/channels/manager.d.ts +26 -0
  60. package/lib/channels/manager.d.ts.map +1 -1
  61. package/lib/channels/manager.js +538 -148
  62. package/lib/channels/manager.js.map +1 -1
  63. package/lib/channels/notification-delivery.d.ts +4 -0
  64. package/lib/channels/notification-delivery.d.ts.map +1 -1
  65. package/lib/channels/notification-delivery.js +15 -5
  66. package/lib/channels/notification-delivery.js.map +1 -1
  67. package/lib/channels/notification-route.d.ts.map +1 -1
  68. package/lib/channels/notification-route.js +10 -4
  69. package/lib/channels/notification-route.js.map +1 -1
  70. package/lib/channels/notification-timeout.d.ts.map +1 -1
  71. package/lib/channels/notification-timeout.js +2 -0
  72. package/lib/channels/notification-timeout.js.map +1 -1
  73. package/lib/channels/outbound-media.d.ts.map +1 -1
  74. package/lib/channels/outbound-media.js +2 -0
  75. package/lib/channels/outbound-media.js.map +1 -1
  76. package/lib/channels/qq/action-catalog.d.ts +38 -0
  77. package/lib/channels/qq/action-catalog.d.ts.map +1 -0
  78. package/lib/channels/qq/action-catalog.js +2731 -0
  79. package/lib/channels/qq/action-catalog.js.map +1 -0
  80. package/lib/channels/qq/actions.d.ts +21 -0
  81. package/lib/channels/qq/actions.d.ts.map +1 -0
  82. package/lib/channels/qq/actions.js +226 -0
  83. package/lib/channels/qq/actions.js.map +1 -0
  84. package/lib/channels/qq/affection-adapter.d.ts +3 -0
  85. package/lib/channels/qq/affection-adapter.d.ts.map +1 -0
  86. package/lib/channels/qq/affection-adapter.js +16 -0
  87. package/lib/channels/qq/affection-adapter.js.map +1 -0
  88. package/lib/channels/qq/attention-classifier.d.ts +10 -0
  89. package/lib/channels/qq/attention-classifier.d.ts.map +1 -0
  90. package/lib/channels/qq/attention-classifier.js +62 -0
  91. package/lib/channels/qq/attention-classifier.js.map +1 -0
  92. package/lib/channels/qq/authority.d.ts +10 -0
  93. package/lib/channels/qq/authority.d.ts.map +1 -0
  94. package/lib/channels/qq/authority.js +50 -0
  95. package/lib/channels/qq/authority.js.map +1 -0
  96. package/lib/channels/qq/autonomy.d.ts +30 -0
  97. package/lib/channels/qq/autonomy.d.ts.map +1 -0
  98. package/lib/channels/qq/autonomy.js +88 -0
  99. package/lib/channels/qq/autonomy.js.map +1 -0
  100. package/lib/channels/qq/catalog.d.ts +14 -0
  101. package/lib/channels/qq/catalog.d.ts.map +1 -0
  102. package/lib/channels/qq/catalog.js +34 -0
  103. package/lib/channels/qq/catalog.js.map +1 -0
  104. package/lib/channels/qq/connection.d.ts +28 -0
  105. package/lib/channels/qq/connection.d.ts.map +1 -0
  106. package/lib/channels/qq/connection.js +200 -0
  107. package/lib/channels/qq/connection.js.map +1 -0
  108. package/lib/channels/qq/identity-binding.d.ts +14 -0
  109. package/lib/channels/qq/identity-binding.d.ts.map +1 -0
  110. package/lib/channels/qq/identity-binding.js +58 -0
  111. package/lib/channels/qq/identity-binding.js.map +1 -0
  112. package/lib/channels/qq/latency.d.ts +22 -0
  113. package/lib/channels/qq/latency.d.ts.map +1 -0
  114. package/lib/channels/qq/latency.js +21 -0
  115. package/lib/channels/qq/latency.js.map +1 -0
  116. package/lib/channels/qq/message.d.ts +6 -0
  117. package/lib/channels/qq/message.d.ts.map +1 -0
  118. package/lib/channels/qq/message.js +49 -0
  119. package/lib/channels/qq/message.js.map +1 -0
  120. package/lib/channels/qq/reply-rules.d.ts +30 -0
  121. package/lib/channels/qq/reply-rules.d.ts.map +1 -0
  122. package/lib/channels/qq/reply-rules.js +72 -0
  123. package/lib/channels/qq/reply-rules.js.map +1 -0
  124. package/lib/channels/qq/reply-tool.d.ts +6 -0
  125. package/lib/channels/qq/reply-tool.d.ts.map +1 -0
  126. package/lib/channels/qq/reply-tool.js +113 -0
  127. package/lib/channels/qq/reply-tool.js.map +1 -0
  128. package/lib/channels/qq/settings.d.ts +42 -0
  129. package/lib/channels/qq/settings.d.ts.map +1 -0
  130. package/lib/channels/qq/settings.js +68 -0
  131. package/lib/channels/qq/settings.js.map +1 -0
  132. package/lib/channels/qq/sticker-adapter.d.ts +5 -0
  133. package/lib/channels/qq/sticker-adapter.d.ts.map +1 -0
  134. package/lib/channels/qq/sticker-adapter.js +43 -0
  135. package/lib/channels/qq/sticker-adapter.js.map +1 -0
  136. package/lib/channels/qq/transport.d.ts +29 -0
  137. package/lib/channels/qq/transport.d.ts.map +1 -0
  138. package/lib/channels/qq/transport.js +187 -0
  139. package/lib/channels/qq/transport.js.map +1 -0
  140. package/lib/channels/reply-access.d.ts +25 -0
  141. package/lib/channels/reply-access.d.ts.map +1 -0
  142. package/lib/channels/reply-access.js +29 -0
  143. package/lib/channels/reply-access.js.map +1 -0
  144. package/lib/channels/weixin/connect.d.ts +3 -1
  145. package/lib/channels/weixin/connect.d.ts.map +1 -1
  146. package/lib/character/activity-tool.d.ts +7 -0
  147. package/lib/character/activity-tool.d.ts.map +1 -0
  148. package/lib/character/activity-tool.js +38 -0
  149. package/lib/character/activity-tool.js.map +1 -0
  150. package/lib/character/affection-adapter.d.ts +13 -0
  151. package/lib/character/affection-adapter.d.ts.map +1 -0
  152. package/lib/character/affection-adapter.js +13 -0
  153. package/lib/character/affection-adapter.js.map +1 -0
  154. package/lib/character/affection-context.d.ts +7 -0
  155. package/lib/character/affection-context.d.ts.map +1 -0
  156. package/lib/character/affection-context.js +33 -0
  157. package/lib/character/affection-context.js.map +1 -0
  158. package/lib/character/affection.d.ts +47 -0
  159. package/lib/character/affection.d.ts.map +1 -0
  160. package/lib/character/affection.js +41 -0
  161. package/lib/character/affection.js.map +1 -0
  162. package/lib/character/conversation-behavior.d.ts +1 -1
  163. package/lib/character/conversation-behavior.d.ts.map +1 -1
  164. package/lib/character/conversation-behavior.js +5 -3
  165. package/lib/character/conversation-behavior.js.map +1 -1
  166. package/lib/character/domain.d.ts +5 -0
  167. package/lib/character/domain.d.ts.map +1 -1
  168. package/lib/character/domain.js +21 -4
  169. package/lib/character/domain.js.map +1 -1
  170. package/lib/character/identity-guide.d.ts +4 -0
  171. package/lib/character/identity-guide.d.ts.map +1 -0
  172. package/lib/character/identity-guide.js +14 -0
  173. package/lib/character/identity-guide.js.map +1 -0
  174. package/lib/character/idle-contact.d.ts +12 -5
  175. package/lib/character/idle-contact.d.ts.map +1 -1
  176. package/lib/character/idle-contact.js +50 -21
  177. package/lib/character/idle-contact.js.map +1 -1
  178. package/lib/character/post-reply.d.ts +19 -0
  179. package/lib/character/post-reply.d.ts.map +1 -0
  180. package/lib/character/post-reply.js +33 -0
  181. package/lib/character/post-reply.js.map +1 -0
  182. package/lib/character/preview.d.ts.map +1 -1
  183. package/lib/character/preview.js +9 -1
  184. package/lib/character/preview.js.map +1 -1
  185. package/lib/character/relationship-adapter.d.ts +18 -0
  186. package/lib/character/relationship-adapter.d.ts.map +1 -0
  187. package/lib/character/relationship-adapter.js +18 -0
  188. package/lib/character/relationship-adapter.js.map +1 -0
  189. package/lib/character/sleep-service.d.ts +35 -0
  190. package/lib/character/sleep-service.d.ts.map +1 -0
  191. package/lib/character/sleep-service.js +278 -0
  192. package/lib/character/sleep-service.js.map +1 -0
  193. package/lib/character/sleep.d.ts +43 -0
  194. package/lib/character/sleep.d.ts.map +1 -0
  195. package/lib/character/sleep.js +36 -0
  196. package/lib/character/sleep.js.map +1 -0
  197. package/lib/character/state-policy.d.ts +4 -0
  198. package/lib/character/state-policy.d.ts.map +1 -0
  199. package/lib/character/state-policy.js +14 -0
  200. package/lib/character/state-policy.js.map +1 -0
  201. package/lib/character/store.d.ts.map +1 -1
  202. package/lib/character/store.js +3 -3
  203. package/lib/character/store.js.map +1 -1
  204. package/lib/chat/conversation.js +1 -1
  205. package/lib/chat/conversation.js.map +1 -1
  206. package/lib/chat/protocol.d.ts +8 -0
  207. package/lib/chat/protocol.d.ts.map +1 -1
  208. package/lib/chat/protocol.js +24 -5
  209. package/lib/chat/protocol.js.map +1 -1
  210. package/lib/chat/sticker-adapter.d.ts +34 -0
  211. package/lib/chat/sticker-adapter.d.ts.map +1 -0
  212. package/lib/chat/sticker-adapter.js +30 -0
  213. package/lib/chat/sticker-adapter.js.map +1 -0
  214. package/lib/chat/tools.d.ts +2 -1
  215. package/lib/chat/tools.d.ts.map +1 -1
  216. package/lib/chat/tools.js +59 -13
  217. package/lib/chat/tools.js.map +1 -1
  218. package/lib/client-api.d.ts +21 -1
  219. package/lib/client-api.d.ts.map +1 -1
  220. package/lib/client-api.js.map +1 -1
  221. package/lib/client.d.ts.map +1 -1
  222. package/lib/client.js +6388 -2685
  223. package/lib/client.js.map +4 -4
  224. package/lib/collaboration/composition.d.ts +2 -0
  225. package/lib/collaboration/composition.d.ts.map +1 -1
  226. package/lib/collaboration/composition.js +32 -10
  227. package/lib/collaboration/composition.js.map +1 -1
  228. package/lib/collaboration/tool-discovery.d.ts +12 -0
  229. package/lib/collaboration/tool-discovery.d.ts.map +1 -0
  230. package/lib/collaboration/tool-discovery.js +148 -0
  231. package/lib/collaboration/tool-discovery.js.map +1 -0
  232. package/lib/companions/identity-tool.d.ts +176 -0
  233. package/lib/companions/identity-tool.d.ts.map +1 -0
  234. package/lib/companions/identity-tool.js +67 -0
  235. package/lib/companions/identity-tool.js.map +1 -0
  236. package/lib/companions/identity.d.ts +24 -0
  237. package/lib/companions/identity.d.ts.map +1 -0
  238. package/lib/companions/identity.js +49 -0
  239. package/lib/companions/identity.js.map +1 -0
  240. package/lib/companions/management-tool.d.ts +1 -1
  241. package/lib/companions/management-tool.d.ts.map +1 -1
  242. package/lib/companions/management-tool.js +4 -2
  243. package/lib/companions/management-tool.js.map +1 -1
  244. package/lib/companions/management.d.ts +21 -9
  245. package/lib/companions/management.d.ts.map +1 -1
  246. package/lib/companions/management.js +18 -4
  247. package/lib/companions/management.js.map +1 -1
  248. package/lib/companions/service.js +1 -0
  249. package/lib/companions/service.js.map +1 -1
  250. package/lib/companions/session-configuration.js +2 -2
  251. package/lib/companions/session-configuration.js.map +1 -1
  252. package/lib/domain.d.ts +37 -1
  253. package/lib/domain.d.ts.map +1 -1
  254. package/lib/domain.js +6 -0
  255. package/lib/domain.js.map +1 -1
  256. package/lib/execution/agent-support.d.ts +1 -1
  257. package/lib/execution/agent-support.d.ts.map +1 -1
  258. package/lib/execution/agent-support.js +7 -2
  259. package/lib/execution/agent-support.js.map +1 -1
  260. package/lib/execution/operation-tool-access.d.ts +13 -0
  261. package/lib/execution/operation-tool-access.d.ts.map +1 -0
  262. package/lib/execution/operation-tool-access.js +49 -0
  263. package/lib/execution/operation-tool-access.js.map +1 -0
  264. package/lib/execution/service.d.ts.map +1 -1
  265. package/lib/execution/service.js +2 -10
  266. package/lib/execution/service.js.map +1 -1
  267. package/lib/execution/tool-access-adapter.d.ts +23 -0
  268. package/lib/execution/tool-access-adapter.d.ts.map +1 -0
  269. package/lib/execution/tool-access-adapter.js +25 -0
  270. package/lib/execution/tool-access-adapter.js.map +1 -0
  271. package/lib/heartbeat.js +1 -1
  272. package/lib/index.d.ts.map +1 -1
  273. package/lib/index.js +32 -2
  274. package/lib/index.js.map +1 -1
  275. package/lib/memory-domain.d.ts +1 -0
  276. package/lib/memory-domain.d.ts.map +1 -1
  277. package/lib/memory-prompt-context.js +1 -0
  278. package/lib/memory-prompt-context.js.map +1 -1
  279. package/lib/memory-reflection.d.ts.map +1 -1
  280. package/lib/memory-reflection.js +16 -5
  281. package/lib/memory-reflection.js.map +1 -1
  282. package/lib/memory-scope.d.ts +2 -1
  283. package/lib/memory-scope.d.ts.map +1 -1
  284. package/lib/memory-scope.js +3 -1
  285. package/lib/memory-scope.js.map +1 -1
  286. package/lib/memory-store.d.ts +1 -1
  287. package/lib/observation-knowledge-entry.d.ts +2 -0
  288. package/lib/observation-knowledge-entry.d.ts.map +1 -0
  289. package/lib/observation-knowledge-entry.js +13 -0
  290. package/lib/observation-knowledge-entry.js.map +1 -0
  291. package/lib/observation-loop.d.ts.map +1 -1
  292. package/lib/observation-loop.js +3 -0
  293. package/lib/observation-loop.js.map +1 -1
  294. package/lib/storage/split-state.d.ts +1 -0
  295. package/lib/storage/split-state.d.ts.map +1 -1
  296. package/lib/storage/split-state.js +9 -2
  297. package/lib/storage/split-state.js.map +1 -1
  298. package/lib/store.d.ts.map +1 -1
  299. package/lib/store.js +7 -0
  300. package/lib/store.js.map +1 -1
  301. package/lib/ui/affection-panel.d.ts +10 -0
  302. package/lib/ui/affection-panel.d.ts.map +1 -0
  303. package/lib/ui/affection-panel.js +44 -0
  304. package/lib/ui/affection-panel.js.map +1 -0
  305. package/lib/ui/capability-editor.d.ts.map +1 -1
  306. package/lib/ui/capability-editor.js +1 -1
  307. package/lib/ui/capability-editor.js.map +1 -1
  308. package/lib/ui/channels-panel.d.ts.map +1 -1
  309. package/lib/ui/channels-panel.js +13 -9
  310. package/lib/ui/channels-panel.js.map +1 -1
  311. package/lib/ui/character-editor.d.ts +3 -1
  312. package/lib/ui/character-editor.d.ts.map +1 -1
  313. package/lib/ui/character-editor.js +20 -18
  314. package/lib/ui/character-editor.js.map +1 -1
  315. package/lib/ui/character-memory-editor.d.ts +6 -1
  316. package/lib/ui/character-memory-editor.d.ts.map +1 -1
  317. package/lib/ui/character-memory-editor.js +17 -11
  318. package/lib/ui/character-memory-editor.js.map +1 -1
  319. package/lib/ui/companion-draft.d.ts +3 -0
  320. package/lib/ui/companion-draft.d.ts.map +1 -1
  321. package/lib/ui/companion-draft.js +1 -1
  322. package/lib/ui/companion-draft.js.map +1 -1
  323. package/lib/ui/identity-editor.d.ts.map +1 -1
  324. package/lib/ui/identity-editor.js +12 -6
  325. package/lib/ui/identity-editor.js.map +1 -1
  326. package/lib/ui/idle-contact-editor.d.ts.map +1 -1
  327. package/lib/ui/idle-contact-editor.js +3 -2
  328. package/lib/ui/idle-contact-editor.js.map +1 -1
  329. package/lib/ui/memory/memory-panel.d.ts.map +1 -1
  330. package/lib/ui/memory/memory-panel.js +5 -3
  331. package/lib/ui/memory/memory-panel.js.map +1 -1
  332. package/lib/ui/push-draft.d.ts +5 -5
  333. package/lib/ui/qq-capability-settings.d.ts +9 -0
  334. package/lib/ui/qq-capability-settings.d.ts.map +1 -0
  335. package/lib/ui/qq-capability-settings.js +24 -0
  336. package/lib/ui/qq-capability-settings.js.map +1 -0
  337. package/lib/ui/qq-channel-settings.d.ts +13 -0
  338. package/lib/ui/qq-channel-settings.d.ts.map +1 -0
  339. package/lib/ui/qq-channel-settings.js +51 -0
  340. package/lib/ui/qq-channel-settings.js.map +1 -0
  341. package/lib/ui/qq-identity-binding.d.ts +9 -0
  342. package/lib/ui/qq-identity-binding.d.ts.map +1 -0
  343. package/lib/ui/qq-identity-binding.js +6 -0
  344. package/lib/ui/qq-identity-binding.js.map +1 -0
  345. package/lib/ui/qq-voice-reply-settings.d.ts +9 -0
  346. package/lib/ui/qq-voice-reply-settings.d.ts.map +1 -0
  347. package/lib/ui/qq-voice-reply-settings.js +23 -0
  348. package/lib/ui/qq-voice-reply-settings.js.map +1 -0
  349. package/lib/ui/role-reference.js +1 -1
  350. package/lib/ui/role-reference.js.map +1 -1
  351. package/lib/ui/sleep-editor.d.ts +10 -0
  352. package/lib/ui/sleep-editor.d.ts.map +1 -0
  353. package/lib/ui/sleep-editor.js +45 -0
  354. package/lib/ui/sleep-editor.js.map +1 -0
  355. package/package.json +33 -5
@@ -1,3 +1,13 @@
1
+ import { activityPolicyCurrent, postReplyAllowed, postReplyCurrent, postReplyProbability, POST_REPLY_PROMPT } from './character/post-reply.js';
2
+ import { PARTNER_TOOL_ENTRIES } from './collaboration/tool-discovery.js';
3
+ import { setReplyTiming } from './channels/qq/latency.js';
4
+ import { resolveToolAccess, installToolAccessGuard } from './execution/tool-access-adapter.js';
5
+ import { renderIdentityRelationship, relationshipTextForIdentity } from './character/relationship-adapter.js';
6
+ import { affectionEnabled, awardAffection, renderAffection } from './character/affection.js';
7
+ import { affectionTurnEligible, currentAffectionAudience } from './character/affection-context.js';
8
+ import { currentQqIdentityRelation, renderQqIdentityRelation } from './channels/qq/identity-binding.js';
9
+ import { qqSettings, qqSessionTarget } from './channels/qq/settings.js';
10
+ import { renderSleepContext, isSleeping } from './character/sleep.js';
1
11
  import { questionOrigin } from './channels/question-origin.js';
2
12
  import { renderCharacterContext } from './character/domain.js';
3
13
  import { renderConversationTurn } from './character/conversation-behavior.js';
@@ -12,7 +22,7 @@ import { conversationMemoryScope } from './memory-scope.js';
12
22
  import { mkdir, realpath, stat, writeFile } from 'node:fs/promises';
13
23
  import { isAbsolute, join, relative, resolve, sep } from 'node:path';
14
24
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
15
- import { IDLE_CONTACT_NOTICE, idleContactDue, idleContactDay, lastIdleContactUser } from './character/idle-contact.js';
25
+ import { IDLE_CONTACT_NOTICE, idleActivityPrompt, isIdleContactNotice, idleActivityProbability, idleContactDue, idleContactDay, idleContactRouteAllowed, lastIdleContactUser } from './character/idle-contact.js';
16
26
  import { scopeOf, scopeParentOf } from '@deepseek-ai/dsh-scope';
17
27
  import { SessionConfigurationIndex } from './companions/session-configuration.js';
18
28
  import { isMissingSession, SessionRecoveryJobs } from './companions/session-recovery.js';
@@ -27,6 +37,7 @@ import { concernExecutionNotes } from './concern-execution-notes.js';
27
37
  import { bindTaskSshAccess } from './collaboration/ssh-task-access.js';
28
38
  import { hasConcernReply } from './concern-execution-receipt.js';
29
39
  import { executeObservationLoop } from './observation-loop.js';
40
+ import { observationKnowledgeEntryDenial } from './observation-knowledge-entry.js';
30
41
  import { waitForBoardTurn } from './tasks/continuation-yield.js';
31
42
  import { assistantTextAfter, renderPartnerPersona, renderToolProtocol, resolvePartnerAgentOptions as resolveAgentOptions } from './execution/agent-support.js';
32
43
  import { channelReplyPartsAfter, isInternalTaskNotice, isRequirementDeliveryReceipt } from './channels/delivery-policy.js';
@@ -36,6 +47,7 @@ export { extractOutboundAttachments } from './channels/outbound-media.js';
36
47
  export { renderToolProtocol, resolvePartnerAgentOptions as resolveAgentOptions } from './execution/agent-support.js';
37
48
  const PARTNER_TASK_TIMEOUT_MS = 30 * 60_000;
38
49
  const HEARTBEAT_READ_ONLY_TOOLS = new Set([
50
+ 'knowledge',
39
51
  'knowledge_base_search',
40
52
  'knowledge_search',
41
53
  'knowledge_read',
@@ -55,6 +67,7 @@ export class PartnerAgentRuntime {
55
67
  reflection;
56
68
  concerns;
57
69
  composer;
70
+ latestPostReplyInput = new Map();
58
71
  handles = new Map();
59
72
  queues = new Map();
60
73
  heartbeatQueues = new Map();
@@ -65,7 +78,9 @@ export class PartnerAgentRuntime {
65
78
  restoredCompositionJobs = new Map();
66
79
  disposeAgentListener;
67
80
  disposeConfigurationListener;
81
+ disposeToolAuthority;
68
82
  disposeAssemblyListener;
83
+ autonomousAssemblyContexts = new WeakSet();
69
84
  rebuildingAssemblies = new WeakSet();
70
85
  configurations;
71
86
  characterContexts = new Map();
@@ -84,20 +99,32 @@ export class PartnerAgentRuntime {
84
99
  this.concerns = concerns;
85
100
  this.composer = composer;
86
101
  this.configurations = new SessionConfigurationIndex(store);
102
+ this.disposeToolAuthority = typeof this.ctx.tools?.guard === 'function' ? installToolAccessGuard(this.ctx.tools, () => this.store.snapshot()) : (() => { });
87
103
  // Current hosts snapshot providers BEFORE agent/pre-step. Rebuild the first
88
104
  // assembly after installing scoped contributions; returning the old snapshot
89
105
  // would still omit tools for one request even though registration succeeded.
90
106
  this.disposeAssemblyListener = typeof this.ctx.on === 'function'
91
107
  ? this.ctx.on('system-prompt/assemble', async (_assembly, context, next) => {
92
- if (!context.agent || !context.signal || this.rebuildingAssemblies.has(context))
108
+ if (!context.agent || !context.signal)
93
109
  return next();
110
+ const enforce = (assembly) => {
111
+ if (this.autonomousAssemblyContexts.has(context))
112
+ return assembly;
113
+ const configuration = this.configurations.forSession(context.agent.session.id);
114
+ if (configuration?.companion.toolDiscovery)
115
+ return { ...assembly, tools: assembly.tools.filter(tool => PARTNER_TOOL_ENTRIES.some(name => name === tool.name)) };
116
+ const access = resolveToolAccess({ agent: context.agent, state: this.store.snapshot() });
117
+ return { ...assembly, tools: !access.sendTools ? [] : access.allowedTools ? assembly.tools.filter(tool => access.allowedTools.includes(tool.name)) : assembly.tools };
118
+ };
119
+ if (this.rebuildingAssemblies.has(context))
120
+ return enforce(await next());
94
121
  const changed = await this.prepareAgentAssembly(context.agent, context.signal);
95
122
  const contextual = await this.prepareConversationContext(context.agent, context.signal);
96
123
  if (!changed && !contextual)
97
- return next();
124
+ return enforce(await next());
98
125
  this.rebuildingAssemblies.add(context);
99
126
  try {
100
- return await this.ctx.systemPrompt.assemble(context);
127
+ return enforce(await this.ctx.systemPrompt.assemble(context));
101
128
  }
102
129
  finally {
103
130
  this.rebuildingAssemblies.delete(context);
@@ -110,6 +137,8 @@ export class PartnerAgentRuntime {
110
137
  }) : () => { };
111
138
  this.disposeAgentListener = typeof this.ctx.on === 'function'
112
139
  ? this.ctx.on('agent/disposed', ({ agent }) => {
140
+ this.replyPresentations.get(agent)?.();
141
+ this.replyPresentations.delete(agent);
113
142
  this.preparedTurns.delete(agent);
114
143
  this.characterContexts.get(agent.session.id)?.();
115
144
  this.characterContexts.delete(agent.session.id);
@@ -125,48 +154,138 @@ export class PartnerAgentRuntime {
125
154
  setQuestionAnswerer(answerer) {
126
155
  this.questionAnswerer = answerer;
127
156
  }
157
+ /** Cancel pending/claimed continuations as soon as another real input is admitted. */
158
+ async cancelPostReply(companionId, keepUserMessageId) {
159
+ const needsCancel = (r) => r.companionId === companionId && r.postReplyOpportunity
160
+ && ['pending', 'claimed'].includes(r.postReplyOpportunity.status) && r.postReplyOpportunity.userMessageId !== keepUserMessageId;
161
+ if (!this.store.snapshot().sessions.some(needsCancel))
162
+ return;
163
+ await this.store.update(state => { for (const route of state.sessions)
164
+ if (needsCancel(route))
165
+ route.postReplyOpportunity.status = 'cancelled'; });
166
+ }
167
+ async localReplyDisplayed(source) {
168
+ const state = this.store.snapshot(), route = state.sessions.find(r => r.id === source.routeId && r.sessionId === source.sessionId && r.kind === 'local');
169
+ const companion = state.companions.find(c => c.id === route?.companionId);
170
+ if (!route || !companion || !postReplyAllowed(state, companion, route))
171
+ return;
172
+ const agent = await this.ensureAgent(companion, route);
173
+ if (agent.status !== 'idle')
174
+ await agent.whenIdle();
175
+ await this.replyDelivered(source);
176
+ }
177
+ /** Called only after delivery succeeds; autonomous notices never carry this provenance. */
178
+ async replyDelivered(source) {
179
+ const state = this.store.snapshot(), route = state.sessions.find(r => r.id === source.routeId && r.sessionId === source.sessionId);
180
+ const companion = state.companions.find(c => c.id === route?.companionId);
181
+ if (!route || !companion || !postReplyAllowed(state, companion, route) || this.closed || this.isArchived(route) || this.store.isCompanionRemoving(companion.id))
182
+ return;
183
+ const key = `companion:${companion.id}`;
184
+ if (this.queues.has(key) || this.steeringQueues.has(key))
185
+ return;
186
+ const agent = await this.ensureAgent(companion, route);
187
+ if (agent.status !== 'idle' || (this.latestPostReplyInput.has(companion.id) && this.latestPostReplyInput.get(companion.id) !== source.userMessageId) || lastIdleContactUser(agent.session.snapshotEvents())?.id !== source.userMessageId)
188
+ return;
189
+ await this.store.update(draft => {
190
+ const current = draft.companions.find(c => c.id === companion.id), target = draft.sessions.find(r => r.id === route.id);
191
+ if (!current || !target || target.sessionId !== source.sessionId || !postReplyAllowed(draft, current, target)
192
+ || target.postReplyOpportunity?.userMessageId === source.userMessageId || agent.status !== 'idle'
193
+ || this.queues.has(key) || this.steeringQueues.has(key) || (this.latestPostReplyInput.has(companion.id) && this.latestPostReplyInput.get(companion.id) !== source.userMessageId) || lastIdleContactUser(agent.session.snapshotEvents())?.id !== source.userMessageId)
194
+ return;
195
+ // At most one pending continuation per companion; different owner accounts share it.
196
+ for (const other of draft.sessions)
197
+ if (other.companionId === companion.id && other.postReplyOpportunity?.status === 'pending')
198
+ other.postReplyOpportunity.status = 'cancelled';
199
+ const now = Date.now(), probability = postReplyProbability(current);
200
+ target.postReplyOpportunity = { userMessageId: source.userMessageId, sessionId: source.sessionId, policyRevision: current.updatedAt,
201
+ deliveredAt: now, dueAt: now + 20_000 + Math.floor(Math.random() * 70_000), expiresAt: now + 180_000,
202
+ probability, status: Math.random() < probability ? 'pending' : 'missed' };
203
+ });
204
+ }
128
205
  /** Use the existing timer and real conversation; never impersonate user input. */
129
206
  async idleContact(companionId, timeZone = 'Asia/Shanghai') {
130
207
  const key = `companion:${companionId}`;
131
- if (this.closed || this.isCompanionBusy(companionId) || this.queues.has(key) || this.taskQueues.has(key) || this.heartbeatQueues.has(key) || this.steeringQueues.has(key) || this.store.isCompanionRemoving(companionId))
208
+ if (this.closed || this.companionActivity(companionId).some(a => a.kind !== 'memory' && a.kind !== 'observation') || this.queues.has(key) || this.taskQueues.has(key) || this.heartbeatQueues.has(key) || this.steeringQueues.has(key) || this.store.isCompanionRemoving(companionId))
132
209
  return;
133
210
  const state = this.store.snapshot(), companion = state.companions.find(c => c.id === companionId);
134
- const route = state.sessions.find(r => r.id === companion?.idleContact?.routeId && r.companionId === companionId);
211
+ const pendingRoute = state.sessions.find(r => r.companionId === companionId && r.postReplyOpportunity?.status === 'pending');
212
+ let route = pendingRoute ?? state.sessions.find(r => r.id === companion?.idleContact?.routeId && r.companionId === companionId);
213
+ const continuation = !!pendingRoute;
214
+ if (pendingRoute && companion) {
215
+ const pending = pendingRoute.postReplyOpportunity;
216
+ if (!postReplyCurrent(state, companion, pendingRoute) || Date.now() > pending.expiresAt) {
217
+ await this.cancelPostReply(companionId);
218
+ return;
219
+ }
220
+ if (Date.now() < pending.dueAt)
221
+ return;
222
+ }
135
223
  if (!companion?.idleContact?.enabled || !route || this.isArchived(route))
136
224
  return;
225
+ const channel = state.channels.find(c => c.id === route.channelId);
226
+ if (channel?.platform === 'qq' && route.qqSessionMode !== (qqSettings(channel.qq).mainSessionPeers.includes(qqSessionTarget(route.userId)) ? 'main' : 'separate'))
227
+ route = await this.ensureSession(companion, route.channelId, route.userId);
137
228
  const agent = await this.ensureAgent(companion, route);
138
- if (agent.status !== 'idle' || this.isCompanionBusy(companionId) || this.queues.has(key) || this.taskQueues.has(key))
229
+ if (agent.status !== 'idle' || this.companionActivity(companionId).some(a => a.kind !== 'memory' && a.kind !== 'observation') || this.queues.has(key) || this.taskQueues.has(key))
139
230
  return;
140
231
  const user = lastIdleContactUser(agent.session.snapshotEvents());
141
- if (!idleContactDue(this.store.snapshot(), companion, user, Date.now(), timeZone))
232
+ const completed = [...agent.session.snapshotEvents()].reverse().find(e => e.type === 'turn/end');
233
+ if (user && completed)
234
+ user.at = Math.max(user.at, completed.time);
235
+ if (continuation) {
236
+ if (user?.id !== route.postReplyOpportunity?.userMessageId) {
237
+ await this.cancelPostReply(companionId);
238
+ return;
239
+ }
240
+ }
241
+ else if (!idleContactDue(this.store.snapshot(), companion, user, Date.now(), timeZone))
142
242
  return;
143
243
  const stableSeq = agent.session.seq;
144
- const message = createUserMessage({ content: [{ type: 'text', text: '这是已开启的闲时主动联系,不是用户新发言。沿用当前角色和真实聊天记录,自然主动说一句,有具体话题才展开;不固定问候、不凑两段、不说“检测到你空闲”、不催促或内疚施压,不假装已做过事情。不要重复回答上轮问题。只生成这次联系的最终正文,不调用渠道发送工具、不设置下一次提醒;系统会将最终正文发送到指定会话。其他工具权限保持原样。'
244
+ const message = createUserMessage({ content: [{ type: 'text', text: idleActivityPrompt(this.store.snapshot(), companion) + (continuation ? '\n' + POST_REPLY_PROMPT : '')
145
245
  }], source: { kind: 'plugin:@lemoncat7/dsh-partner', plugin: '@lemoncat7/dsh-partner', form: 'notice', summary: IDLE_CONTACT_NOTICE } });
146
246
  let claimed = false;
147
247
  await this.store.update(draft => {
148
248
  const current = draft.companions.find(c => c.id === companionId), target = draft.sessions.find(r => r.id === route.id);
149
249
  const now = Date.now();
150
- if (!current || current.updatedAt !== companion.updatedAt || !target || target.sessionId !== route.sessionId || !idleContactDue(draft, current, user, now, timeZone))
250
+ if (!current || current.updatedAt !== companion.updatedAt || !target || target.sessionId !== route.sessionId || (continuation ? !postReplyCurrent(draft, current, target, now) || target.postReplyOpportunity?.status !== 'pending' || target.postReplyOpportunity.userMessageId !== user?.id || now > target.postReplyOpportunity.expiresAt : !idleContactDue(draft, current, user, now, timeZone)))
151
251
  return;
252
+ const probability = continuation ? target.postReplyOpportunity.probability : idleActivityProbability(draft, current, user, now);
253
+ target.idleActivityCheck = { at: now, probability };
254
+ if (!continuation && Math.random() >= probability)
255
+ return;
256
+ if (continuation)
257
+ target.postReplyOpportunity.status = 'claimed';
152
258
  const day = idleContactDay(now, timeZone);
153
- target.idleContactState = { userMessageId: user.id, messageId: message.id, attemptedAt: now, day, count: (target.idleContactState?.day === day ? target.idleContactState.count : 0) + 1, timeZone, policyRevision: current.updatedAt };
259
+ target.idleContactState = { ...(continuation ? { kind: 'post-reply' } : {}), userMessageId: user.id, messageId: message.id, attemptedAt: now, day, count: (target.idleContactState?.day === day ? target.idleContactState.count : 0) + 1, timeZone, policyRevision: current.updatedAt };
154
260
  claimed = true;
155
261
  });
156
262
  // Claim before generation survives restart. Don't replay uncertain sends or steal active turns.
157
263
  if (!claimed || this.closed || this.store.isCompanionRemoving(companionId) || agent.status !== 'idle' || agent.session.seq !== stableSeq || this.queues.has(key) || this.taskQueues.has(key))
158
264
  return;
159
- const current = this.store.snapshot().companions.find(c => c.id === companionId);
160
- if (!current?.idleContact?.enabled || current.updatedAt !== companion.updatedAt || this.isArchived(route))
265
+ const currentState = this.store.snapshot(), current = currentState.companions.find(c => c.id === companionId), currentRoute = currentState.sessions.find(r => r.id === route.id);
266
+ if (!current || !currentRoute || !activityPolicyCurrent(currentState, current, currentRoute) || isSleeping(current) || current.updatedAt !== companion.updatedAt || this.isArchived(route))
161
267
  return;
162
268
  agent.followup(message);
163
269
  }
270
+ async idleContactDeliveryCurrent(origin) {
271
+ const state = this.store.snapshot(), route = state.sessions.find(r => r.id === origin.routeId);
272
+ const companion = state.companions.find(c => c.id === route?.companionId);
273
+ if (!route || !companion || route.sessionId !== origin.sessionId || !activityPolicyCurrent(state, companion, route) || companion.updatedAt !== origin.policyRevision || route.idleContactState?.messageId !== origin.messageId || !idleContactRouteAllowed(state, companion, route.id) || isSleeping(companion) || this.isArchived(route) || this.store.isCompanionRemoving(companion.id))
274
+ return false;
275
+ const channel = state.channels.find(c => c.id === route.channelId);
276
+ if (channel?.platform === 'qq' && route.qqSessionMode !== (qqSettings(channel.qq).mainSessionPeers.includes(qqSessionTarget(route.userId)) ? 'main' : 'separate'))
277
+ return false;
278
+ const agent = await this.ensureAgent(companion, route);
279
+ return lastIdleContactUser(agent.session.snapshotEvents())?.id === origin.userMessageId;
280
+ }
164
281
  async reply(companion, channelId, userId, message, progress) {
165
282
  const key = `companion:${companion.id}`;
283
+ await this.cancelPostReply(companion.id);
284
+ const queuedAt = Date.now();
166
285
  const previous = this.queues.get(key) ?? Promise.resolve();
167
286
  let output = { text: '', attachments: [] };
168
287
  reportProgress(progress, 'queued');
169
- const current = previous.catch(() => { }).then(async () => { output = await this.drive(companion, channelId, userId, message, progress); });
288
+ const current = previous.catch(() => { }).then(async () => { output = await this.drive(companion, channelId, userId, message, progress, queuedAt); });
170
289
  this.queues.set(key, current);
171
290
  try {
172
291
  await current;
@@ -178,6 +297,7 @@ export class PartnerAgentRuntime {
178
297
  }
179
298
  }
180
299
  async steer(companion, channelId, userId, message) {
300
+ await this.cancelPostReply(companion.id);
181
301
  const key = `companion:${companion.id}`;
182
302
  const previous = this.steeringQueues.get(key) ?? Promise.resolve();
183
303
  let accepted = false;
@@ -196,6 +316,9 @@ export class PartnerAgentRuntime {
196
316
  const route = this.store.snapshot().sessions.find(item => item.channelId === channelId && item.userId === userId && item.companionId === companion.id);
197
317
  if (route === undefined)
198
318
  return false;
319
+ const channel = this.store.snapshot().channels.find(c => c.id === channelId);
320
+ if (channel?.platform === 'qq' && route.qqSessionMode !== (qqSettings(channel.qq).mainSessionPeers.includes(qqSessionTarget(userId)) ? 'main' : 'separate'))
321
+ return false;
199
322
  const agent = this.handles.get(route.sessionId)?.agent ?? this.ctx.agents.get(route.sessionId);
200
323
  if (agent === undefined || agent.status !== 'running')
201
324
  return false;
@@ -258,7 +381,7 @@ export class PartnerAgentRuntime {
258
381
  // If there was no local conversation, continue the newest existing channel
259
382
  // conversation. Do not concatenate unrelated historical transcripts.
260
383
  if (!existing) {
261
- const previous = state.sessions.filter(item => item.companionId === companion.id && canReuseSession(item, companion.id, this.ctx.workspaceRegistry.archivedSessionIds)).sort((a, b) => b.lastMessageAt - a.lastMessageAt)[0];
384
+ const previous = state.sessions.filter(item => item.companionId === companion.id && !state.channels.some(c => c.id === item.channelId && c.platform === 'qq') && canReuseSession(item, companion.id, this.ctx.workspaceRegistry.archivedSessionIds)).sort((a, b) => b.lastMessageAt - a.lastMessageAt)[0];
262
385
  if (previous) {
263
386
  route.sessionId = previous.sessionId;
264
387
  if (previous.cwd)
@@ -338,9 +461,16 @@ export class PartnerAgentRuntime {
338
461
  const agent = await this.ensureAgent(companion, route);
339
462
  if (!this.store.snapshot().sessions.some(s => s.id === route.id && s.sessionId === wake.originSessionId) || this.isArchived(route))
340
463
  throw new Error('原续接会话已变更');
341
- const assembly = await agent.ctx.systemPrompt.assemble(assembleContextFor(agent, signal));
342
- signal.throwIfAborted();
343
- assertWakeTools(entry, assembly.tools.map(tool => tool.name));
464
+ const preflight = assembleContextFor(agent, signal);
465
+ this.autonomousAssemblyContexts.add(preflight);
466
+ try {
467
+ const assembly = await agent.ctx.systemPrompt.assemble(preflight);
468
+ signal.throwIfAborted();
469
+ assertWakeTools(entry, assembly.tools.map(tool => tool.name));
470
+ }
471
+ finally {
472
+ this.autonomousAssemblyContexts.delete(preflight);
473
+ }
344
474
  const message = createUserMessage({ content: [{ type: 'text', text: continuationPrompt(entry) }],
345
475
  source: { kind: 'plugin:@lemoncat7/dsh-partner', plugin: '@lemoncat7/dsh-partner', form: 'notice', summary: '伙伴长任务续接' } });
346
476
  await this.store.update(state => {
@@ -584,6 +714,14 @@ export class PartnerAgentRuntime {
584
714
  }
585
715
  }
586
716
  async observeSessionEvent(session, event) {
717
+ if (event.type === 'user/message' && event.data.source.kind === 'user') {
718
+ const owner = this.store.snapshot().sessions.find(r => r.sessionId === session.id)?.companionId;
719
+ if (owner) {
720
+ this.latestPostReplyInput.set(owner, event.data.id);
721
+ await this.cancelPostReply(owner, event.data.id);
722
+ }
723
+ return;
724
+ }
587
725
  if (event.type !== 'turn/end' || event.data.reason.kind !== 'completed')
588
726
  return;
589
727
  const route = this.store.snapshot().sessions.find(item => item.sessionId === session.id);
@@ -612,6 +750,7 @@ export class PartnerAgentRuntime {
612
750
  if (companion === undefined)
613
751
  return;
614
752
  const events = completedTurnEvents(session.snapshotEvents(), event);
753
+ const identityRelation = currentQqIdentityRelation(session.id, events, this.store.snapshot().sessions, this.store.snapshot().channels ?? []);
615
754
  const prompt = events.find(item => item.type === 'user/message' && item.data.source.kind === 'user');
616
755
  const replies = events.filter(item => item.type === 'assistant/message' && !item.data.interrupted);
617
756
  const userText = events.filter(e => e.type === 'user/message' && e.data.source.kind === 'user').map(e => e.type === 'user/message' ? textContent(e.data.content) : '').filter(Boolean).join('\n\n');
@@ -620,10 +759,11 @@ export class PartnerAgentRuntime {
620
759
  if (!userText || !assistantText)
621
760
  return;
622
761
  const scopeId = memoryScope(route.channelId, route.userId);
623
- const concernDirective = await this.concerns?.applyUserDirective(companion.id, scopeId, userText, event.time);
762
+ const concernDirective = identityRelation && !identityRelation.matched ? undefined : await this.concerns?.applyUserDirective(companion.id, scopeId, userText, event.time);
624
763
  const created = companion.automation.memory.enabled && this.reflection !== undefined ? await this.reflection.reflect(companion, {
625
764
  id: `turn-${createHash('sha256').update(JSON.stringify([session.id, event.time, userText])).digest('hex').slice(0, 32)}`, companionId: companion.id, scopeId: conversationMemoryScope(route, this.store.snapshot().sessions),
626
765
  characterEnabled: companion.characterCard?.enabled === true,
766
+ ...(identityRelation ? { identityRelation } : {}),
627
767
  userMessageIds: events.flatMap(e => e.type === 'user/message' && e.data.source.kind === 'user' ? [e.data.id] : []),
628
768
  sessionId: session.id, at: event.time, user: userText, assistant: assistantText, concernScopeId: scopeId,
629
769
  ...(concernDirective === undefined ? {} : { concernDirective }),
@@ -645,6 +785,9 @@ export class PartnerAgentRuntime {
645
785
  const target = state.sessions.find(item => item.id === route.id);
646
786
  if (target)
647
787
  target.lastMessageAt = Date.now();
788
+ const current = state.companions.find(c => c.id === companion.id);
789
+ if (current && affectionEnabled(current) && affectionTurnEligible(state, current.id, session.id, events))
790
+ awardAffection(current, `${session.id}:${event.seq}`, event.time);
648
791
  });
649
792
  }
650
793
  async recordConcernCreatedNotice(companion, scopeId, concerns) {
@@ -765,6 +908,18 @@ export class PartnerAgentRuntime {
765
908
  if (this.store.isCompanionRemoving(configuration.companion.id))
766
909
  throw new Error('伙伴正在删除,不能启动新轮次');
767
910
  signal.throwIfAborted();
911
+ const access = resolveToolAccess({ agent, state: this.store.snapshot() });
912
+ let presentationChanged = false;
913
+ const stableNative = configuration.companion.toolDiscovery || !!access.allowedTools;
914
+ if (stableNative && !this.replyPresentations.has(agent) && typeof agent.ctx.tools?.presentAs === 'function') {
915
+ this.replyPresentations.set(agent, agent.ctx.tools.presentAs('native'));
916
+ presentationChanged = true;
917
+ }
918
+ else if (!stableNative && this.replyPresentations.has(agent)) {
919
+ this.replyPresentations.get(agent)();
920
+ this.replyPresentations.delete(agent);
921
+ presentationChanged = true;
922
+ }
768
923
  const installed = this.restoredCompositions.get(agent.session.id);
769
924
  if (installed?.agent !== agent || installed.revision !== configuration.revision) {
770
925
  const job = (async () => {
@@ -783,15 +938,20 @@ export class PartnerAgentRuntime {
783
938
  }
784
939
  return true;
785
940
  }
786
- return false;
941
+ return presentationChanged;
787
942
  }
943
+ replyPresentations = new Map();
788
944
  contextRevisions = new WeakMap();
789
945
  async prepareConversationContext(agent, signal) {
790
946
  const config = this.configurations.forSession(agent.session.id);
791
947
  if (!config)
792
948
  return false;
793
949
  const { companion, route } = config;
794
- if ((!this.memory || !companion.automation.memory.enabled) && !companion.characterCard?.enabled) {
950
+ const state = this.store.snapshot();
951
+ const relationEvents = state.channels?.some(c => c.platform === 'qq' && state.sessions.some(r => r.sessionId === agent.session.id && r.channelId === c.id))
952
+ ? agent.session.snapshotEvents() : undefined;
953
+ const identityRelation = relationEvents ? currentQqIdentityRelation(agent.session.id, relationEvents, state.sessions, state.channels) : undefined;
954
+ if (!identityRelation && !relationshipTextForIdentity(companion) && (!this.memory || !companion.automation.memory.enabled) && !companion.characterCard?.enabled && !renderSleepContext(companion, route.id)) {
795
955
  this.characterContexts.get(agent.session.id)?.();
796
956
  this.characterContexts.delete(agent.session.id);
797
957
  this.contextRevisions.delete(agent);
@@ -800,7 +960,11 @@ export class PartnerAgentRuntime {
800
960
  const messages = agent.session.deriveMessages();
801
961
  const last = [...messages].reverse().find(m => m.role === 'user' && m.source.kind === 'user');
802
962
  const query = last && last.role === 'user' ? textContent(last.content) : '';
803
- const revision = `${config.revision}:${last?.id ?? ''}`;
963
+ const events = relationEvents ?? agent.session.snapshotEvents();
964
+ const origin = [...events].reverse().find(e => e.type === 'user/message' && (e.data.source.kind === 'user' || isIdleContactNotice(e) || isInternalTaskNotice(e)));
965
+ const autonomous = Boolean(origin && isIdleContactNotice(origin));
966
+ const affectionAudience = currentAffectionAudience(state, companion.id, agent.session.id, events);
967
+ const revision = `${JSON.stringify(identityRelation)}:${config.revision}:${last?.id ?? ''}:${autonomous && origin?.type === 'user/message' ? origin.data.id : ''}:${companion.sleepState?.dreams?.at(-1)?.id ?? ''}:${affectionAudience}:${companion.affection?.updatedAt ?? 0}`;
804
968
  if (this.contextRevisions.get(agent) === revision)
805
969
  return false;
806
970
  const scope = conversationMemoryScope(route, this.store.snapshot().sessions);
@@ -814,10 +978,17 @@ export class PartnerAgentRuntime {
814
978
  consecutiveUserMessages++;
815
979
  }
816
980
  signal.throwIfAborted();
817
- const text = [renderCharacterContext(companion.characterCard, messages.slice(-6).filter(m => m.role === 'user' || m.role === 'assistant').map(m => textContent(m.content)).join('\n').slice(-12000), story),
981
+ const relationshipAllowed = !identityRelation || identityRelation.matched;
982
+ const access = resolveToolAccess({ agent, state });
983
+ const text = [access.allowedTools || !access.sendTools ? access.denialReason + ' 回复能力通过 partner_tools 查询和调用(未开启统一入口时使用原工具);语音能力为 partner_qq_reply;不能代办操作,不承诺使用私有能力。' : '',
984
+ renderIdentityRelationship(companion, identityRelation ? { channel: 'qq', matched: identityRelation.matched } : { channel: 'default' }),
985
+ renderAffection(companion, affectionAudience),
986
+ renderCharacterContext(companion.characterCard, messages.slice(-6).filter(m => m.role === 'user' || m.role === 'assistant').map(m => textContent(m.content)).join('\n').slice(-12000), relationshipAllowed ? story : undefined),
818
987
  recalled ? renderMemory(recalled.relevant, recalled.connections, recalled.scenes, recalled.history) : '',
819
- recalled ? renderProfile(recalled.profile) : '',
820
- renderConversationTurn(companion.characterCard, consecutiveUserMessages),
988
+ recalled && relationshipAllowed ? renderProfile(recalled.profile) : '',
989
+ renderConversationTurn(companion.characterCard, consecutiveUserMessages, autonomous),
990
+ renderQqIdentityRelation(identityRelation),
991
+ renderSleepContext(companion, this.store.snapshot().sessions.filter(r => r.companionId === companion.id && r.sessionId === route.sessionId).map(r => r.id)),
821
992
  ].filter(Boolean).join('\n\n');
822
993
  this.characterContexts.get(agent.session.id)?.();
823
994
  // A changing .section() appends an entire system prompt on in-history routes.
@@ -907,10 +1078,25 @@ export class PartnerAgentRuntime {
907
1078
  }
908
1079
  return renewed;
909
1080
  }
910
- isCompanionBusy(companionId) {
911
- return this.reflection?.isRunning(companionId) === true || this.observationJobs.has(companionId) || this.agentSetupJobs.has(companionId) || this.store.snapshot().sessions.some(route => route.companionId === companionId
912
- && (this.handles.get(route.sessionId)?.agent ?? this.ctx.agents.get(route.sessionId))?.status === 'running');
1081
+ companionActivity(companionId) {
1082
+ const activity = [];
1083
+ if (this.reflection?.isRunning(companionId))
1084
+ activity.push({ kind: 'memory', label: '后台记忆整理(不阻止群聊判断)' });
1085
+ if (this.observationJobs.has(companionId))
1086
+ activity.push({ kind: 'observation', label: '归档已完成的聊天' });
1087
+ if (this.agentSetupJobs.has(companionId))
1088
+ activity.push({ kind: 'setup', label: '准备伙伴会话' });
1089
+ const seen = new Set();
1090
+ for (const route of this.store.snapshot().sessions.filter(r => r.companionId === companionId)) {
1091
+ if (seen.has(route.sessionId))
1092
+ continue;
1093
+ seen.add(route.sessionId);
1094
+ if ((this.handles.get(route.sessionId)?.agent ?? this.ctx.agents.get(route.sessionId))?.status === 'running')
1095
+ activity.push({ kind: 'conversation', label: route.qqSessionTitle || (route.kind === 'local' ? '主会话正在运行' : '渠道会话正在运行'), sessionId: route.sessionId });
1096
+ }
1097
+ return activity;
913
1098
  }
1099
+ isCompanionBusy(companionId) { return this.companionActivity(companionId).length > 0; }
914
1100
  async releaseCompanion(companionId) {
915
1101
  const sessions = this.store.snapshot().sessions.filter(item => item.companionId === companionId);
916
1102
  await Promise.all(sessions.map(async (item) => {
@@ -936,7 +1122,11 @@ export class PartnerAgentRuntime {
936
1122
  this.closed = true;
937
1123
  this.disposeConfigurationListener();
938
1124
  this.disposeAssemblyListener();
1125
+ this.disposeToolAuthority();
939
1126
  this.disposeAgentListener();
1127
+ for (const dispose of this.replyPresentations.values())
1128
+ dispose();
1129
+ this.replyPresentations.clear();
940
1130
  this.configurations.close();
941
1131
  await Promise.allSettled(this.restoredCompositionJobs.values());
942
1132
  for (const restored of this.restoredCompositions.values())
@@ -948,6 +1138,7 @@ export class PartnerAgentRuntime {
948
1138
  this.restoredCompositionJobs.clear();
949
1139
  await Promise.all([...this.handles.values()].map(handle => handle.dispose().catch(() => { })));
950
1140
  this.handles.clear();
1141
+ this.latestPostReplyInput.clear();
951
1142
  this.heartbeatQueues.clear();
952
1143
  this.taskQueues.clear();
953
1144
  this.steeringQueues.clear();
@@ -962,9 +1153,11 @@ export class PartnerAgentRuntime {
962
1153
  restored?.dispose();
963
1154
  this.restoredCompositions.delete(sessionId);
964
1155
  }
965
- async drive(companion, channelId, userId, message, progress) {
1156
+ async drive(companion, channelId, userId, message, progress, queuedAt = Date.now()) {
966
1157
  reportProgress(progress, 'working');
967
- const session = await this.ensureSession(companion, channelId, userId);
1158
+ const enteredAt = Date.now();
1159
+ let submittedAt = enteredAt, modelEndedAt = enteredAt, lastTurnEnd;
1160
+ const session = await this.ensureSession(companion, channelId, userId, message.sessionTitle);
968
1161
  const agent = await this.ensureAgent(companion, session);
969
1162
  if (agent.status !== 'idle')
970
1163
  await agent.whenIdle();
@@ -976,10 +1169,13 @@ export class PartnerAgentRuntime {
976
1169
  source: { kind: 'plugin:@lemoncat7/dsh-partner', plugin: '@lemoncat7/dsh-partner', form: 'notice', summary: '伙伴想起了与当前消息相关的挂念' },
977
1170
  }));
978
1171
  const startSeq = agent.session.seq;
1172
+ let deliveredUserMessageId = '';
979
1173
  let directProgress = false;
980
- const detach = progress ? this.ctx.on('session/event', (current, event) => {
1174
+ const detach = this.ctx.on('session/event', (current, event) => {
981
1175
  if (current.id !== agent.session.id || event.seq < startSeq)
982
1176
  return;
1177
+ if (event.type === 'turn/end')
1178
+ lastTurnEnd = event.time;
983
1179
  if (event.type === 'turn/start' || event.type === 'turn/end' || (isInternalTaskNotice(event) && !isRequirementDeliveryReceipt(event)))
984
1180
  directProgress = false;
985
1181
  if (event.type === 'user/message' && event.data.source.kind === 'user')
@@ -992,28 +1188,38 @@ export class PartnerAgentRuntime {
992
1188
  if (activity)
993
1189
  reportProgress(progress, activity);
994
1190
  }
995
- }) : () => { };
1191
+ });
996
1192
  try {
997
1193
  const userMessage = createUserMessage({
998
1194
  content: inbound.content,
999
1195
  source: { kind: 'user' },
1000
1196
  });
1197
+ deliveredUserMessageId = userMessage.id;
1001
1198
  await this.rememberMessageOrigin(session, userMessage.id);
1199
+ submittedAt = Date.now();
1002
1200
  agent.followup(userMessage);
1003
1201
  await agent.whenIdle();
1202
+ modelEndedAt = Date.now();
1004
1203
  }
1005
1204
  finally {
1006
1205
  detach();
1007
1206
  }
1008
1207
  const response = channelReplyPartsAfter(agent.session.snapshotEvents(), startSeq);
1208
+ const stateStarted = Date.now();
1009
1209
  await this.store.update(state => {
1010
1210
  for (const target of state.sessions)
1011
1211
  if (target.sessionId === session.sessionId)
1012
1212
  target.lastMessageAt = Date.now();
1013
1213
  });
1214
+ const stateEnded = Date.now();
1014
1215
  if (deferred && deferred.length > 0)
1015
1216
  await this.concerns?.markMentioned(companion.id, deferred.map(item => item.id)).catch(() => { });
1016
- return prepareChannelReply(response, session.cwd ?? this.defaultCwd);
1217
+ const reply = await prepareChannelReply(response, session.cwd ?? this.defaultCwd);
1218
+ reply.postReplySource = { routeId: session.id, sessionId: session.sessionId, userMessageId: deliveredUserMessageId };
1219
+ if (this.store.snapshot().channels.some(c => c.id === channelId && c.platform === 'qq'))
1220
+ this.ctx.logger?.info?.(`[qq-latency] ${JSON.stringify({ stage: 'agent', sessionId: session.sessionId, inputPrepMs: submittedAt - enteredAt, modelMs: modelEndedAt - submittedAt, afterModelMs: Date.now() - modelEndedAt })}`);
1221
+ setReplyTiming(reply, { queueMs: enteredAt - queuedAt, inputPrepMs: submittedAt - enteredAt, modelAndIdleMs: modelEndedAt - submittedAt, idleAfterTurnEndMs: lastTurnEnd === undefined ? 0 : Math.max(0, modelEndedAt - lastTurnEnd), stateCommitMs: stateEnded - stateStarted, outboundPrepMs: Date.now() - stateEnded });
1222
+ return reply;
1017
1223
  }
1018
1224
  async rememberMessageOrigin(route, messageId) {
1019
1225
  await this.store.update(state => {
@@ -1049,28 +1255,78 @@ export class PartnerAgentRuntime {
1049
1255
  content.push({ type: 'text', text: '[渠道附件消息]' });
1050
1256
  return { content, query: [message.text, ...message.attachments.map(item => item.name)].filter(Boolean).join(' ') };
1051
1257
  }
1052
- async ensureSession(companion, channelId, userId) {
1258
+ async reconcileQqSessions() {
1259
+ const state = this.store.snapshot();
1260
+ // Startup only: workers have not started, so stale leases can be recovered.
1261
+ for (const route of state.sessions.filter(r => r.kind === 'channel' && state.channels.some(c => c.id === r.channelId && c.platform === 'qq'))) {
1262
+ const companion = state.companions.find(c => c.id === route.companionId);
1263
+ if (companion)
1264
+ await this.ensureSession(companion, route.channelId, route.userId, undefined, true);
1265
+ }
1266
+ }
1267
+ async ensureSession(companion, channelId, userId, title, recoverLeases = false) {
1053
1268
  const primary = await this.ensureLocalSessionRecord(companion.id);
1054
- const existing = this.store.snapshot().sessions.find(item => item.channelId === channelId && item.userId === userId);
1055
- if (existing !== undefined && existing.sessionId === primary.sessionId && canReuseSession(existing, companion.id, this.ctx.workspaceRegistry.archivedSessionIds))
1269
+ const snapshot = this.store.snapshot(), channel = snapshot.channels.find(c => c.id === channelId);
1270
+ const existing = snapshot.sessions.find(item => item.channelId === channelId && item.userId === userId && item.companionId === companion.id);
1271
+ const isQq = channel?.platform === 'qq', target = qqSessionTarget(userId);
1272
+ if (!isQq && existing?.sessionId === primary.sessionId && canReuseSession(existing, companion.id, this.ctx.workspaceRegistry.archivedSessionIds))
1056
1273
  return existing;
1057
- const now = Date.now();
1058
- const session = {
1059
- id: `route-${randomUUID()}`,
1060
- kind: 'channel',
1061
- channelId,
1062
- userId,
1063
- companionId: companion.id,
1064
- sessionId: primary.sessionId,
1065
- cwd: primary.cwd ?? partnerCwd(this.defaultCwd, companion.id),
1066
- lastMessageAt: now,
1067
- };
1068
- await this.memory?.mergeScopes(companion.id, conversationMemoryScope(primary, [primary]), [memoryScope(channelId, userId)]);
1274
+ const sharedScope = conversationMemoryScope(primary, [primary]);
1275
+ const sources = isQq ? snapshot.sessions.filter(r => r.channelId === channelId && r.companionId === companion.id && qqSessionTarget(r.userId) === target && r.memoryScopeId !== sharedScope).map(r => memoryScope(r.channelId, r.userId)) : [];
1276
+ if (existing?.memoryScopeId !== sharedScope || sources.length)
1277
+ await this.memory?.mergeScopes(companion.id, sharedScope, [...new Set([...sources, memoryScope(channelId, userId)])], recoverLeases);
1278
+ let result;
1069
1279
  await this.store.update(state => {
1070
- state.sessions = state.sessions.filter(item => !(item.channelId === channelId && item.userId === userId));
1071
- state.sessions.push(session);
1280
+ const currentChannel = state.channels.find(c => c.id === channelId);
1281
+ if (this.store.isCompanionRemoving(companion.id) || !state.companions.some(c => c.id === companion.id))
1282
+ throw Error('伙伴已移除');
1283
+ const main = !isQq || qqSettings(currentChannel?.qq).mainSessionPeers.includes(target);
1284
+ const current = state.sessions.find(r => r.channelId === channelId && r.userId === userId && r.companionId === companion.id);
1285
+ const peers = state.sessions.filter(r => r.channelId === channelId && r.companionId === companion.id && qqSessionTarget(r.userId) === target).sort((a, b) => b.lastMessageAt - a.lastMessageAt);
1286
+ const archived = this.ctx.workspaceRegistry.archivedSessionIds;
1287
+ // Prefer an active shared group route, then its saved separate conversation.
1288
+ const separate = peers.find(r => r.qqSessionMode !== 'main' && r.sessionId !== primary.sessionId && !archived.includes(r.sessionId));
1289
+ const saved = peers.find(r => r.qqSeparateSessionId && !archived.includes(r.qqSeparateSessionId));
1290
+ const separateId = separate?.sessionId ?? saved?.qqSeparateSessionId ?? `session-${randomUUID()}`;
1291
+ const separateCwd = separate?.cwd ?? saved?.qqSeparateCwd ?? partnerCwd(this.defaultCwd, companion.id);
1292
+ const sessionId = main ? primary.sessionId : separateId;
1293
+ const name = title?.replace(/[\x00-\x1f\x7f]/g, ' ').trim().slice(0, 140) || peers.find(r => r.qqSessionTitle)?.qqSessionTitle || `${target.startsWith('group:') ? '群聊' : '私聊'} · ${target.split(':')[1]}`;
1294
+ const route = { ...current, id: current?.id ?? `route-${randomUUID()}`, kind: 'channel', channelId, userId, companionId: companion.id, sessionId, cwd: main ? (primary.cwd ?? partnerCwd(this.defaultCwd, companion.id)) : separateCwd, lastMessageAt: current?.lastMessageAt ?? Date.now(),
1295
+ ...(isQq ? { memoryScopeId: sharedScope, qqSessionTarget: target, qqSessionMode: main ? 'main' : 'separate', qqSeparateSessionId: separateId, qqSeparateCwd: separateCwd, qqSessionTitle: name } : {}) };
1296
+ const applied = peers.find(r => r.sessionId === sessionId && r.qqAppliedTitle)?.qqAppliedTitle;
1297
+ if (applied)
1298
+ route.qqAppliedTitle = applied;
1299
+ if (current?.sessionId !== sessionId)
1300
+ delete route.inboundMessageIds;
1301
+ // One group context for all members, with separate delivery/grant identities.
1302
+ if (isQq)
1303
+ for (const peer of peers) {
1304
+ if (peer.sessionId !== sessionId)
1305
+ delete peer.inboundMessageIds;
1306
+ Object.assign(peer, { sessionId, cwd: route.cwd, memoryScopeId: sharedScope, qqSessionTarget: target, qqSessionMode: route.qqSessionMode, qqSeparateSessionId: separateId, qqSeparateCwd: separateCwd, qqSessionTitle: name });
1307
+ }
1308
+ state.sessions = state.sessions.filter(r => !(r.channelId === channelId && r.userId === userId));
1309
+ state.sessions.push(route);
1310
+ result = route;
1072
1311
  });
1073
- return session;
1312
+ return result;
1313
+ }
1314
+ async nameQqSession(agent, route) {
1315
+ if (route.qqSessionMode !== 'separate' || !route.qqSessionTitle)
1316
+ return;
1317
+ const titles = this.ctx.get('sessionTitle');
1318
+ if (!titles)
1319
+ return;
1320
+ const current = titles.get(agent.session);
1321
+ // Respect a user-edited title; only refresh our own generated names.
1322
+ if (current?.source.kind === 'user' && current.title !== route.qqAppliedTitle)
1323
+ return;
1324
+ if (current?.title === route.qqSessionTitle)
1325
+ return;
1326
+ titles.rename(agent.session, route.qqSessionTitle);
1327
+ await this.store.update(state => { for (const peer of state.sessions)
1328
+ if (peer.sessionId === route.sessionId)
1329
+ peer.qqAppliedTitle = route.qqSessionTitle; });
1074
1330
  }
1075
1331
  async ensureAgent(companion, route) {
1076
1332
  if (this.store.isCompanionRemoving(companion.id) || !this.store.snapshot().companions.some(item => item.id === companion.id))
@@ -1094,11 +1350,13 @@ export class PartnerAgentRuntime {
1094
1350
  const held = this.handles.get(route.sessionId);
1095
1351
  if (held !== undefined) {
1096
1352
  await this.attachSession(companion, route);
1353
+ await this.nameQqSession(held.agent, route);
1097
1354
  return held.agent;
1098
1355
  }
1099
1356
  const live = this.ctx.agents.get(route.sessionId);
1100
1357
  if (live !== undefined) {
1101
1358
  await this.attachSession(companion, route);
1359
+ await this.nameQqSession(live, route);
1102
1360
  return live;
1103
1361
  }
1104
1362
  const options = resolveAgentOptions(this.ctx.agentDefaultModel, companion);
@@ -1134,6 +1392,7 @@ export class PartnerAgentRuntime {
1134
1392
  }
1135
1393
  this.handles.set(route.sessionId, handle);
1136
1394
  await this.attachSession(companion, route);
1395
+ await this.nameQqSession(handle.agent, route);
1137
1396
  return handle.agent;
1138
1397
  }
1139
1398
  async composeRestoredAgent(agent, companion, route, revision, signal) {
@@ -1157,7 +1416,7 @@ export class PartnerAgentRuntime {
1157
1416
  try {
1158
1417
  // Native DEPLOYMENT_PERSONA_SUFFIX: delivery style follows the general work preset,
1159
1418
  // while all tool definitions and execution/permission policies remain present.
1160
- disposers.push(agentCtx.systemPrompt.section({ name: 'partner-identity', order: companion.characterCard?.enabled ? 10200 : -10, interpolate: false, text: renderPartnerPersona(companion, route.kind === 'local' ? 'local' : 'conversation') }));
1419
+ disposers.push(agentCtx.systemPrompt.section({ name: 'partner-identity', order: companion.characterCard?.enabled ? 10200 : -10, interpolate: false, text: renderPartnerPersona(companion, route.kind === 'local' ? 'local' : 'conversation', false) }));
1161
1420
  disposers.push(agentCtx.systemPrompt.section({ name: 'partner-tool-routing', order: -9, text: renderToolProtocol() }));
1162
1421
  if (this.memory)
1163
1422
  disposers.push(agentCtx.tools.register({
@@ -1371,6 +1630,8 @@ async function resolveHeartbeatFileAccess(cwd, concerns) {
1371
1630
  return { root, writable };
1372
1631
  }
1373
1632
  export function heartbeatToolDenial(name, argumentsValue, access) {
1633
+ if (name === 'knowledge')
1634
+ return observationKnowledgeEntryDenial(argumentsValue);
1374
1635
  if (['knowledge_note_references', 'knowledge_note_read', 'knowledge_note_update'].includes(name))
1375
1636
  return '关注不处理依据中的笔记引用;记录仅使用手动配置的 recordTarget 和专用记录工具。';
1376
1637
  if (!isRecord(argumentsValue))