@lemoncat7/dsh-partner 2.21.5 → 2.21.13

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 (160) hide show
  1. package/README.md +3 -1
  2. package/docs/THIRD_PARTY_LICENSES.md +1 -1
  3. package/docs/conversation-autonomy.md +27 -0
  4. package/docs/releases/2.21.10.md +5 -0
  5. package/docs/releases/2.21.11.md +7 -0
  6. package/docs/releases/2.21.12.md +5 -0
  7. package/docs/releases/2.21.13.md +13 -0
  8. package/docs/releases/2.21.6.md +6 -0
  9. package/docs/releases/2.21.7.md +11 -0
  10. package/docs/releases/2.21.8.md +9 -0
  11. package/docs/releases/2.21.9.md +6 -0
  12. package/lib/agent-runtime.d.ts +10 -2
  13. package/lib/agent-runtime.d.ts.map +1 -1
  14. package/lib/agent-runtime.js +116 -53
  15. package/lib/agent-runtime.js.map +1 -1
  16. package/lib/api.d.ts.map +1 -1
  17. package/lib/api.js +2 -1
  18. package/lib/api.js.map +1 -1
  19. package/lib/channels/authored-progress.d.ts +16 -0
  20. package/lib/channels/authored-progress.d.ts.map +1 -0
  21. package/lib/channels/authored-progress.js +38 -0
  22. package/lib/channels/authored-progress.js.map +1 -0
  23. package/lib/channels/direct/message-format.js +127 -127
  24. package/lib/channels/direct/message-format.js.map +1 -1
  25. package/lib/channels/manager.d.ts.map +1 -1
  26. package/lib/channels/manager.js +9 -6
  27. package/lib/channels/manager.js.map +1 -1
  28. package/lib/channels/qq/actions.js +1 -1
  29. package/lib/channels/qq/actions.js.map +1 -1
  30. package/lib/channels/qq/attention-classifier.js +5 -5
  31. package/lib/channels/qq/attention-classifier.js.map +1 -1
  32. package/lib/channels/qq/authority.d.ts +2 -1
  33. package/lib/channels/qq/authority.d.ts.map +1 -1
  34. package/lib/channels/qq/authority.js +4 -4
  35. package/lib/channels/qq/authority.js.map +1 -1
  36. package/lib/channels/qq/identity-binding.d.ts +1 -1
  37. package/lib/channels/qq/identity-binding.d.ts.map +1 -1
  38. package/lib/channels/qq/identity-binding.js +5 -3
  39. package/lib/channels/qq/identity-binding.js.map +1 -1
  40. package/lib/channels/qq/reply-tool.d.ts.map +1 -1
  41. package/lib/channels/qq/reply-tool.js +15 -4
  42. package/lib/channels/qq/reply-tool.js.map +1 -1
  43. package/lib/channels/qq/transport.js +1 -1
  44. package/lib/channels/qq/transport.js.map +1 -1
  45. package/lib/character/activity-tool.d.ts +3 -0
  46. package/lib/character/activity-tool.d.ts.map +1 -1
  47. package/lib/character/activity-tool.js +8 -1
  48. package/lib/character/activity-tool.js.map +1 -1
  49. package/lib/character/affection.d.ts +2 -2
  50. package/lib/character/conversation-behavior.d.ts +5 -3
  51. package/lib/character/conversation-behavior.d.ts.map +1 -1
  52. package/lib/character/conversation-behavior.js +29 -22
  53. package/lib/character/conversation-behavior.js.map +1 -1
  54. package/lib/character/fatigue.d.ts +15 -0
  55. package/lib/character/fatigue.d.ts.map +1 -0
  56. package/lib/character/fatigue.js +37 -0
  57. package/lib/character/fatigue.js.map +1 -0
  58. package/lib/character/identity-guide.d.ts +38 -1
  59. package/lib/character/identity-guide.d.ts.map +1 -1
  60. package/lib/character/identity-guide.js +7 -1
  61. package/lib/character/identity-guide.js.map +1 -1
  62. package/lib/character/idle-contact.d.ts +1 -1
  63. package/lib/character/idle-contact.d.ts.map +1 -1
  64. package/lib/character/idle-contact.js +25 -2
  65. package/lib/character/idle-contact.js.map +1 -1
  66. package/lib/character/persona-templates.d.ts +23 -0
  67. package/lib/character/persona-templates.d.ts.map +1 -0
  68. package/lib/character/persona-templates.js +33 -0
  69. package/lib/character/persona-templates.js.map +1 -0
  70. package/lib/character/post-reply.js +2 -2
  71. package/lib/character/post-reply.js.map +1 -1
  72. package/lib/character/role-reference-data.d.ts.map +1 -0
  73. package/lib/character/role-reference-data.js.map +1 -0
  74. package/lib/character/sleep-service.d.ts +10 -1
  75. package/lib/character/sleep-service.d.ts.map +1 -1
  76. package/lib/character/sleep-service.js +156 -75
  77. package/lib/character/sleep-service.js.map +1 -1
  78. package/lib/character/sleep.d.ts +25 -3
  79. package/lib/character/sleep.d.ts.map +1 -1
  80. package/lib/character/sleep.js +15 -6
  81. package/lib/character/sleep.js.map +1 -1
  82. package/lib/client.d.ts.map +1 -1
  83. package/lib/client.js +3464 -3321
  84. package/lib/client.js.map +4 -4
  85. package/lib/collaboration/tool-discovery.d.ts +1 -1
  86. package/lib/collaboration/tool-discovery.d.ts.map +1 -1
  87. package/lib/collaboration/tool-discovery.js +14 -6
  88. package/lib/collaboration/tool-discovery.js.map +1 -1
  89. package/lib/companions/identity-tool.d.ts +1 -1
  90. package/lib/companions/identity-tool.d.ts.map +1 -1
  91. package/lib/companions/identity-tool.js +9 -7
  92. package/lib/companions/identity-tool.js.map +1 -1
  93. package/lib/companions/management-tool.d.ts +1 -1
  94. package/lib/companions/management-tool.d.ts.map +1 -1
  95. package/lib/companions/management-tool.js +3 -2
  96. package/lib/companions/management-tool.js.map +1 -1
  97. package/lib/companions/management.d.ts +28 -1
  98. package/lib/companions/management.d.ts.map +1 -1
  99. package/lib/companions/management.js +3 -2
  100. package/lib/companions/management.js.map +1 -1
  101. package/lib/core/semaphore.d.ts.map +1 -1
  102. package/lib/core/semaphore.js +6 -3
  103. package/lib/core/semaphore.js.map +1 -1
  104. package/lib/execution/reply-progress.d.ts +3 -1
  105. package/lib/execution/reply-progress.d.ts.map +1 -1
  106. package/lib/execution/reply-progress.js +16 -0
  107. package/lib/execution/reply-progress.js.map +1 -1
  108. package/lib/execution/task-preamble.d.ts +15 -0
  109. package/lib/execution/task-preamble.d.ts.map +1 -0
  110. package/lib/execution/task-preamble.js +107 -0
  111. package/lib/execution/task-preamble.js.map +1 -0
  112. package/lib/execution/tool-access-adapter.d.ts +3 -0
  113. package/lib/execution/tool-access-adapter.d.ts.map +1 -1
  114. package/lib/execution/tool-access-adapter.js.map +1 -1
  115. package/lib/index.js +2 -3
  116. package/lib/index.js.map +1 -1
  117. package/lib/memory-admission.d.ts +5 -0
  118. package/lib/memory-admission.d.ts.map +1 -0
  119. package/lib/memory-admission.js +20 -0
  120. package/lib/memory-admission.js.map +1 -0
  121. package/lib/memory-reflection.js +1 -1
  122. package/lib/ui/channels-panel.d.ts.map +1 -1
  123. package/lib/ui/channels-panel.js +2 -1
  124. package/lib/ui/channels-panel.js.map +1 -1
  125. package/lib/ui/character-editor.d.ts +1 -1
  126. package/lib/ui/character-editor.d.ts.map +1 -1
  127. package/lib/ui/character-editor.js +2 -3
  128. package/lib/ui/character-editor.js.map +1 -1
  129. package/lib/ui/companion-deletion.js +1 -1
  130. package/lib/ui/companion-deletion.js.map +1 -1
  131. package/lib/ui/companion-vitals.d.ts +6 -0
  132. package/lib/ui/companion-vitals.d.ts.map +1 -0
  133. package/lib/ui/companion-vitals.js +44 -0
  134. package/lib/ui/companion-vitals.js.map +1 -0
  135. package/lib/ui/identity-editor.d.ts.map +1 -1
  136. package/lib/ui/identity-editor.js +2 -1
  137. package/lib/ui/identity-editor.js.map +1 -1
  138. package/lib/ui/persona-template.d.ts +5 -0
  139. package/lib/ui/persona-template.d.ts.map +1 -0
  140. package/lib/ui/persona-template.js +7 -0
  141. package/lib/ui/persona-template.js.map +1 -0
  142. package/lib/ui/push-draft.d.ts +2 -2
  143. package/lib/ui/qq-channel-settings.d.ts.map +1 -1
  144. package/lib/ui/qq-channel-settings.js +2 -1
  145. package/lib/ui/qq-channel-settings.js.map +1 -1
  146. package/lib/ui/qq-connection-guide.d.ts +2 -0
  147. package/lib/ui/qq-connection-guide.d.ts.map +1 -0
  148. package/lib/ui/qq-connection-guide.js +5 -0
  149. package/lib/ui/qq-connection-guide.js.map +1 -0
  150. package/lib/ui/role-reference.d.ts.map +1 -1
  151. package/lib/ui/role-reference.js +3 -2
  152. package/lib/ui/role-reference.js.map +1 -1
  153. package/lib/ui/sleep-editor.d.ts.map +1 -1
  154. package/lib/ui/sleep-editor.js +15 -5
  155. package/lib/ui/sleep-editor.js.map +1 -1
  156. package/package.json +11 -2
  157. package/lib/ui/role-reference-data.d.ts.map +0 -1
  158. package/lib/ui/role-reference-data.js.map +0 -1
  159. /package/lib/{ui → character}/role-reference-data.d.ts +0 -0
  160. /package/lib/{ui → character}/role-reference-data.js +0 -0
@@ -1,3 +1,8 @@
1
+ import { isActivitySilenceRequested } from './character/activity-tool.js';
2
+ import { installTaskPreamble } from './execution/task-preamble.js';
3
+ import { AsyncSemaphore } from './core/semaphore.js';
4
+ import { automaticMemoryAllowed } from './memory-admission.js';
5
+ import { personaReferenceRequested, renderPersonaReference } from './character/identity-guide.js';
1
6
  import { activityPolicyCurrent, postReplyAllowed, postReplyCurrent, postReplyProbability, POST_REPLY_PROMPT } from './character/post-reply.js';
2
7
  import { PARTNER_TOOL_ENTRIES } from './collaboration/tool-discovery.js';
3
8
  import { setReplyTiming } from './channels/qq/latency.js';
@@ -7,6 +12,7 @@ import { affectionEnabled, awardAffection, renderAffection } from './character/a
7
12
  import { affectionTurnEligible, currentAffectionAudience } from './character/affection-context.js';
8
13
  import { currentQqIdentityRelation, renderQqIdentityRelation } from './channels/qq/identity-binding.js';
9
14
  import { qqSettings, qqSessionTarget } from './channels/qq/settings.js';
15
+ import { fatigueValue } from './character/fatigue.js';
10
16
  import { renderSleepContext, isSleeping } from './character/sleep.js';
11
17
  import { questionOrigin } from './channels/question-origin.js';
12
18
  import { renderCharacterContext } from './character/domain.js';
@@ -70,6 +76,7 @@ export class PartnerAgentRuntime {
70
76
  latestPostReplyInput = new Map();
71
77
  handles = new Map();
72
78
  queues = new Map();
79
+ replySlots = new AsyncSemaphore(6);
73
80
  heartbeatQueues = new Map();
74
81
  taskQueues = new Map();
75
82
  steeringQueues = new Map();
@@ -113,7 +120,7 @@ export class PartnerAgentRuntime {
113
120
  const configuration = this.configurations.forSession(context.agent.session.id);
114
121
  if (configuration?.companion.toolDiscovery)
115
122
  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() });
123
+ const access = resolveToolAccess({ agent: context.agent, state: this.store.snapshot(), ...(this.admittedInputs.get(context.agent)?.length ? { events: this.inputEvents(context.agent) } : {}) });
117
124
  return { ...assembly, tools: !access.sendTools ? [] : access.allowedTools ? assembly.tools.filter(tool => access.allowedTools.includes(tool.name)) : assembly.tools };
118
125
  };
119
126
  if (this.rebuildingAssemblies.has(context))
@@ -131,15 +138,33 @@ export class PartnerAgentRuntime {
131
138
  }
132
139
  }) : () => { };
133
140
  this.disposeConfigurationListener = typeof this.ctx.on === 'function'
134
- ? this.ctx.on('agent/pre-step', async ({ agent, turn, signal }, next) => {
135
- await this.prepareAgentTurn(agent, turn, signal);
136
- return next();
141
+ ? this.ctx.on('agent/pre-step', async ({ agent, turn, signal, messages }, next) => {
142
+ try {
143
+ await this.prepareAgentTurn(agent, turn, signal, messages);
144
+ const decision = await next();
145
+ if (decision.kind === 'reject') {
146
+ this.admittedInputs.delete(agent);
147
+ this.contextRevisions.delete(agent);
148
+ }
149
+ else if (decision.kind === 'enter') {
150
+ this.admittedInputs.set(agent, decision.messages);
151
+ await this.sleepService?.acceptInput(agent.session.id, decision.messages);
152
+ await this.prepareConversationContext(agent, signal);
153
+ }
154
+ return decision;
155
+ }
156
+ catch (error) {
157
+ this.admittedInputs.delete(agent);
158
+ this.contextRevisions.delete(agent);
159
+ throw error;
160
+ }
137
161
  }) : () => { };
138
162
  this.disposeAgentListener = typeof this.ctx.on === 'function'
139
163
  ? this.ctx.on('agent/disposed', ({ agent }) => {
140
164
  this.replyPresentations.get(agent)?.();
141
165
  this.replyPresentations.delete(agent);
142
166
  this.preparedTurns.delete(agent);
167
+ this.admittedInputs.delete(agent);
143
168
  this.characterContexts.get(agent.session.id)?.();
144
169
  this.characterContexts.delete(agent.session.id);
145
170
  this.contextRevisions.delete(agent);
@@ -151,12 +176,14 @@ export class PartnerAgentRuntime {
151
176
  })
152
177
  : () => { };
153
178
  }
179
+ sleepService;
180
+ setSleepService(service) { this.sleepService = service; }
154
181
  setQuestionAnswerer(answerer) {
155
182
  this.questionAnswerer = answerer;
156
183
  }
157
184
  /** 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
185
+ async cancelPostReply(companionId, keepUserMessageId, sessionId) {
186
+ const needsCancel = (r) => r.companionId === companionId && (!sessionId || r.sessionId === sessionId) && r.postReplyOpportunity
160
187
  && ['pending', 'claimed'].includes(r.postReplyOpportunity.status) && r.postReplyOpportunity.userMessageId !== keepUserMessageId;
161
188
  if (!this.store.snapshot().sessions.some(needsCancel))
162
189
  return;
@@ -180,21 +207,21 @@ export class PartnerAgentRuntime {
180
207
  const companion = state.companions.find(c => c.id === route?.companionId);
181
208
  if (!route || !companion || !postReplyAllowed(state, companion, route) || this.closed || this.isArchived(route) || this.store.isCompanionRemoving(companion.id))
182
209
  return;
183
- const key = `companion:${companion.id}`;
210
+ const key = route.sessionId;
184
211
  if (this.queues.has(key) || this.steeringQueues.has(key))
185
212
  return;
186
213
  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)
214
+ if (agent.status !== 'idle' || (this.latestPostReplyInput.has(route.sessionId) && this.latestPostReplyInput.get(route.sessionId) !== source.userMessageId) || lastIdleContactUser(agent.session.snapshotEvents())?.id !== source.userMessageId)
188
215
  return;
189
216
  await this.store.update(draft => {
190
217
  const current = draft.companions.find(c => c.id === companion.id), target = draft.sessions.find(r => r.id === route.id);
191
218
  if (!current || !target || target.sessionId !== source.sessionId || !postReplyAllowed(draft, current, target)
192
219
  || 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)
220
+ || this.queues.has(key) || this.steeringQueues.has(key) || (this.latestPostReplyInput.has(route.sessionId) && this.latestPostReplyInput.get(route.sessionId) !== source.userMessageId) || lastIdleContactUser(agent.session.snapshotEvents())?.id !== source.userMessageId)
194
221
  return;
195
- // At most one pending continuation per companion; different owner accounts share it.
222
+ // Shared history has one opportunity; unrelated conversations remain independent.
196
223
  for (const other of draft.sessions)
197
- if (other.companionId === companion.id && other.postReplyOpportunity?.status === 'pending')
224
+ if (other.sessionId === route.sessionId && other.postReplyOpportunity?.status === 'pending')
198
225
  other.postReplyOpportunity.status = 'cancelled';
199
226
  const now = Date.now(), probability = postReplyProbability(current);
200
227
  target.postReplyOpportunity = { userMessageId: source.userMessageId, sessionId: source.sessionId, policyRevision: current.updatedAt,
@@ -205,16 +232,17 @@ export class PartnerAgentRuntime {
205
232
  /** Use the existing timer and real conversation; never impersonate user input. */
206
233
  async idleContact(companionId, timeZone = 'Asia/Shanghai') {
207
234
  const key = `companion:${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))
235
+ if (this.closed || this.taskQueues.has(key) || this.heartbeatQueues.has(key) || this.store.isCompanionRemoving(companionId))
209
236
  return;
210
237
  const state = this.store.snapshot(), companion = state.companions.find(c => c.id === companionId);
211
- const pendingRoute = state.sessions.find(r => r.companionId === companionId && r.postReplyOpportunity?.status === 'pending');
238
+ const activity = this.companionActivity(companionId);
239
+ const pendingRoute = state.sessions.filter(r => r.companionId === companionId && r.postReplyOpportunity?.status === 'pending' && !this.queues.has(r.sessionId) && !this.steeringQueues.has(r.sessionId) && !activity.some(a => a.sessionId === r.sessionId)).sort((a, b) => a.postReplyOpportunity.dueAt - b.postReplyOpportunity.dueAt)[0];
212
240
  let route = pendingRoute ?? state.sessions.find(r => r.id === companion?.idleContact?.routeId && r.companionId === companionId);
213
241
  const continuation = !!pendingRoute;
214
242
  if (pendingRoute && companion) {
215
243
  const pending = pendingRoute.postReplyOpportunity;
216
244
  if (!postReplyCurrent(state, companion, pendingRoute) || Date.now() > pending.expiresAt) {
217
- await this.cancelPostReply(companionId);
245
+ await this.cancelPostReply(companionId, undefined, route?.sessionId);
218
246
  return;
219
247
  }
220
248
  if (Date.now() < pending.dueAt)
@@ -226,7 +254,7 @@ export class PartnerAgentRuntime {
226
254
  if (channel?.platform === 'qq' && route.qqSessionMode !== (qqSettings(channel.qq).mainSessionPeers.includes(qqSessionTarget(route.userId)) ? 'main' : 'separate'))
227
255
  route = await this.ensureSession(companion, route.channelId, route.userId);
228
256
  const agent = await this.ensureAgent(companion, route);
229
- if (agent.status !== 'idle' || this.companionActivity(companionId).some(a => a.kind !== 'memory' && a.kind !== 'observation') || this.queues.has(key) || this.taskQueues.has(key))
257
+ if (agent.status !== 'idle' || this.companionActivity(companionId).some(a => a.sessionId === route.sessionId) || this.queues.has(route.sessionId) || this.steeringQueues.has(route.sessionId) || this.taskQueues.has(key))
230
258
  return;
231
259
  const user = lastIdleContactUser(agent.session.snapshotEvents());
232
260
  const completed = [...agent.session.snapshotEvents()].reverse().find(e => e.type === 'turn/end');
@@ -234,14 +262,14 @@ export class PartnerAgentRuntime {
234
262
  user.at = Math.max(user.at, completed.time);
235
263
  if (continuation) {
236
264
  if (user?.id !== route.postReplyOpportunity?.userMessageId) {
237
- await this.cancelPostReply(companionId);
265
+ await this.cancelPostReply(companionId, undefined, route?.sessionId);
238
266
  return;
239
267
  }
240
268
  }
241
269
  else if (!idleContactDue(this.store.snapshot(), companion, user, Date.now(), timeZone))
242
270
  return;
243
271
  const stableSeq = agent.session.seq;
244
- const message = createUserMessage({ content: [{ type: 'text', text: idleActivityPrompt(this.store.snapshot(), companion) + (continuation ? '\n' + POST_REPLY_PROMPT : '')
272
+ const message = createUserMessage({ content: [{ type: 'text', text: idleActivityPrompt(this.store.snapshot(), companion, agent.session.snapshotEvents()) + (continuation ? '\n' + POST_REPLY_PROMPT : '')
245
273
  }], source: { kind: 'plugin:@lemoncat7/dsh-partner', plugin: '@lemoncat7/dsh-partner', form: 'notice', summary: IDLE_CONTACT_NOTICE } });
246
274
  let claimed = false;
247
275
  await this.store.update(draft => {
@@ -260,7 +288,7 @@ export class PartnerAgentRuntime {
260
288
  claimed = true;
261
289
  });
262
290
  // Claim before generation survives restart. Don't replay uncertain sends or steal active turns.
263
- if (!claimed || this.closed || this.store.isCompanionRemoving(companionId) || agent.status !== 'idle' || agent.session.seq !== stableSeq || this.queues.has(key) || this.taskQueues.has(key))
291
+ if (!claimed || this.closed || this.store.isCompanionRemoving(companionId) || agent.status !== 'idle' || agent.session.seq !== stableSeq || this.queues.has(route.sessionId) || this.taskQueues.has(key))
264
292
  return;
265
293
  const currentState = this.store.snapshot(), current = currentState.companions.find(c => c.id === companionId), currentRoute = currentState.sessions.find(r => r.id === route.id);
266
294
  if (!current || !currentRoute || !activityPolicyCurrent(currentState, current, currentRoute) || isSleeping(current) || current.updatedAt !== companion.updatedAt || this.isArchived(route))
@@ -276,16 +304,25 @@ export class PartnerAgentRuntime {
276
304
  if (channel?.platform === 'qq' && route.qqSessionMode !== (qqSettings(channel.qq).mainSessionPeers.includes(qqSessionTarget(route.userId)) ? 'main' : 'separate'))
277
305
  return false;
278
306
  const agent = await this.ensureAgent(companion, route);
279
- return lastIdleContactUser(agent.session.snapshotEvents())?.id === origin.userMessageId;
307
+ const events = agent.session.snapshotEvents();
308
+ const noticeIndex = events.findIndex(e => e.type === 'user/message' && e.data.id === origin.messageId);
309
+ if (noticeIndex >= 0) {
310
+ const tail = events.slice(noticeIndex + 1);
311
+ const boundary = tail.findIndex(e => e.type === 'turn/start' || e.type === 'turn/end' || e.type === 'user/message' && e.data.source.kind === 'user');
312
+ if (tail.slice(0, boundary < 0 ? tail.length : boundary).some(isActivitySilenceRequested))
313
+ return false;
314
+ }
315
+ return lastIdleContactUser(events)?.id === origin.userMessageId;
280
316
  }
281
317
  async reply(companion, channelId, userId, message, progress) {
282
- const key = `companion:${companion.id}`;
283
- await this.cancelPostReply(companion.id);
318
+ const route = await this.ensureSession(companion, channelId, userId, message.sessionTitle);
319
+ const key = route.sessionId;
320
+ await this.cancelPostReply(companion.id, undefined, route.sessionId);
284
321
  const queuedAt = Date.now();
285
322
  const previous = this.queues.get(key) ?? Promise.resolve();
286
323
  let output = { text: '', attachments: [] };
287
324
  reportProgress(progress, 'queued');
288
- const current = previous.catch(() => { }).then(async () => { output = await this.drive(companion, channelId, userId, message, progress, queuedAt); });
325
+ const current = previous.catch(() => { }).then(async () => { output = await this.replySlots.use(() => this.drive(companion, channelId, userId, message, progress, queuedAt, route)); });
289
326
  this.queues.set(key, current);
290
327
  try {
291
328
  await current;
@@ -297,8 +334,9 @@ export class PartnerAgentRuntime {
297
334
  }
298
335
  }
299
336
  async steer(companion, channelId, userId, message) {
300
- await this.cancelPostReply(companion.id);
301
- const key = `companion:${companion.id}`;
337
+ const route = await this.ensureSession(companion, channelId, userId, message.sessionTitle);
338
+ const key = route.sessionId;
339
+ await this.cancelPostReply(companion.id, undefined, route.sessionId);
302
340
  const previous = this.steeringQueues.get(key) ?? Promise.resolve();
303
341
  let accepted = false;
304
342
  const current = previous.catch(() => { }).then(async () => { accepted = await this.deliverSteering(companion, channelId, userId, message); });
@@ -700,13 +738,13 @@ export class PartnerAgentRuntime {
700
738
  }
701
739
  async runHeartbeatWhenIdle(key, companion, route, concerns) {
702
740
  for (;;) {
703
- await this.queues.get(key)?.catch(() => { });
741
+ await this.queues.get(route.sessionId)?.catch(() => { });
704
742
  const conversation = await this.ensureAgent(companion, route);
705
743
  if (conversation.status !== 'idle')
706
744
  await conversation.whenIdle();
707
745
  const stableSeq = conversation.session.seq;
708
746
  await delay(2_000);
709
- if (this.queues.has(key))
747
+ if (this.queues.has(route.sessionId))
710
748
  continue;
711
749
  if (conversation.status !== 'idle' || conversation.session.seq !== stableSeq)
712
750
  continue;
@@ -714,11 +752,16 @@ export class PartnerAgentRuntime {
714
752
  }
715
753
  }
716
754
  async observeSessionEvent(session, event) {
755
+ if (event.type === 'turn/end') {
756
+ const agent = this.ctx?.agents?.get(session.id);
757
+ if (agent)
758
+ this.admittedInputs?.delete(agent);
759
+ }
717
760
  if (event.type === 'user/message' && event.data.source.kind === 'user') {
718
761
  const owner = this.store.snapshot().sessions.find(r => r.sessionId === session.id)?.companionId;
719
762
  if (owner) {
720
- this.latestPostReplyInput.set(owner, event.data.id);
721
- await this.cancelPostReply(owner, event.data.id);
763
+ this.latestPostReplyInput.set(session.id, event.data.id);
764
+ await this.cancelPostReply(owner, event.data.id, session.id);
722
765
  }
723
766
  return;
724
767
  }
@@ -760,7 +803,7 @@ export class PartnerAgentRuntime {
760
803
  return;
761
804
  const scopeId = memoryScope(route.channelId, route.userId);
762
805
  const concernDirective = identityRelation && !identityRelation.matched ? undefined : await this.concerns?.applyUserDirective(companion.id, scopeId, userText, event.time);
763
- const created = companion.automation.memory.enabled && this.reflection !== undefined ? await this.reflection.reflect(companion, {
806
+ const created = companion.automation.memory.enabled && automaticMemoryAllowed(session.id, events, this.store.snapshot().sessions) && this.reflection !== undefined ? await this.reflection.reflect(companion, {
764
807
  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),
765
808
  characterEnabled: companion.characterCard?.enabled === true,
766
809
  ...(identityRelation ? { identityRelation } : {}),
@@ -883,12 +926,12 @@ export class PartnerAgentRuntime {
883
926
  throw new Error('伙伴不存在');
884
927
  }
885
928
  /** Awaited by the host before prompt/tool assembly, never from a running tool. */
886
- async prepareAgentTurn(agent, turn, signal) {
929
+ async prepareAgentTurn(agent, turn, signal, messages = []) {
887
930
  if (this.closed)
888
931
  throw new Error('伙伴运行时已关闭');
889
- if (this.preparedTurns.get(agent) === turn)
890
- return;
891
- await this.prepareAgentAssembly(agent, signal);
932
+ this.admittedInputs.set(agent, messages);
933
+ if (this.preparedTurns.get(agent) !== turn)
934
+ await this.prepareAgentAssembly(agent, signal);
892
935
  await this.prepareConversationContext(agent, signal);
893
936
  this.preparedTurns.set(agent, turn);
894
937
  }
@@ -908,7 +951,7 @@ export class PartnerAgentRuntime {
908
951
  if (this.store.isCompanionRemoving(configuration.companion.id))
909
952
  throw new Error('伙伴正在删除,不能启动新轮次');
910
953
  signal.throwIfAborted();
911
- const access = resolveToolAccess({ agent, state: this.store.snapshot() });
954
+ const access = resolveToolAccess({ agent, state: this.store.snapshot(), ...(this.admittedInputs.get(agent)?.length ? { events: this.inputEvents(agent) } : {}) });
912
955
  let presentationChanged = false;
913
956
  const stableNative = configuration.companion.toolDiscovery || !!access.allowedTools;
914
957
  if (stableNative && !this.replyPresentations.has(agent) && typeof agent.ctx.tools?.presentAs === 'function') {
@@ -942,6 +985,18 @@ export class PartnerAgentRuntime {
942
985
  }
943
986
  replyPresentations = new Map();
944
987
  contextRevisions = new WeakMap();
988
+ admittedInputs = new WeakMap();
989
+ /** Projection only: the host commits admitted input after prompt preparation. */
990
+ inputEvents(agent) {
991
+ const events = agent.session.snapshotEvents();
992
+ const pending = this.admittedInputs?.get(agent) ?? [];
993
+ if (!pending.length)
994
+ return events;
995
+ const committed = new Set(events.flatMap(e => e.type === 'user/message' ? [e.data.id] : []));
996
+ return [...events, ...pending.filter(m => !committed.has(m.id)).map((data, index) => ({
997
+ type: 'user/message', data, seq: (agent.session.seq + index + 1), time: Date.now(), surfaceOp: 'append',
998
+ }))];
999
+ }
945
1000
  async prepareConversationContext(agent, signal) {
946
1001
  const config = this.configurations.forSession(agent.session.id);
947
1002
  if (!config)
@@ -949,22 +1004,25 @@ export class PartnerAgentRuntime {
949
1004
  const { companion, route } = config;
950
1005
  const state = this.store.snapshot();
951
1006
  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;
1007
+ ? this.inputEvents(agent) : undefined;
953
1008
  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)) {
1009
+ const committedMessages = agent.session.deriveMessages?.() ?? [];
1010
+ const committedIds = new Set(committedMessages.map(m => m.id));
1011
+ const messages = [...committedMessages, ...(this.admittedInputs?.get(agent) ?? []).filter(m => !committedIds.has(m.id))];
1012
+ const last = [...messages].reverse().find(m => m.role === 'user' && m.source.kind === 'user');
1013
+ const query = last && last.role === 'user' ? textContent(last.content) : '';
1014
+ if (!identityRelation && !relationshipTextForIdentity(companion) && (!this.memory || !companion.automation.memory.enabled) && !companion.characterCard?.enabled && !renderSleepContext(companion, route.id) && !personaReferenceRequested(query)) {
955
1015
  this.characterContexts.get(agent.session.id)?.();
956
1016
  this.characterContexts.delete(agent.session.id);
957
1017
  this.contextRevisions.delete(agent);
958
1018
  return false;
959
1019
  }
960
- const messages = agent.session.deriveMessages();
961
- const last = [...messages].reverse().find(m => m.role === 'user' && m.source.kind === 'user');
962
- const query = last && last.role === 'user' ? textContent(last.content) : '';
963
- const events = relationEvents ?? agent.session.snapshotEvents();
1020
+ const events = relationEvents ?? this.inputEvents(agent);
964
1021
  const origin = [...events].reverse().find(e => e.type === 'user/message' && (e.data.source.kind === 'user' || isIdleContactNotice(e) || isInternalTaskNotice(e)));
965
1022
  const autonomous = Boolean(origin && isIdleContactNotice(origin));
966
1023
  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}`;
1024
+ const access = resolveToolAccess({ agent, state, events });
1025
+ const revision = `${JSON.stringify(access)}:${JSON.stringify(identityRelation)}:${config.revision}:${last?.id ?? ''}:${autonomous && origin?.type === 'user/message' ? origin.data.id : ''}:${JSON.stringify([companion.sleepState?.episode, companion.sleepState?.reception, Math.round(fatigueValue(companion))])}:${companion.sleepState?.dreams?.at(-1)?.id ?? ''}:${affectionAudience}:${companion.affection?.updatedAt ?? 0}`;
968
1026
  if (this.contextRevisions.get(agent) === revision)
969
1027
  return false;
970
1028
  const scope = conversationMemoryScope(route, this.store.snapshot().sessions);
@@ -979,16 +1037,15 @@ export class PartnerAgentRuntime {
979
1037
  }
980
1038
  signal.throwIfAborted();
981
1039
  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;不能代办操作,不承诺使用私有能力。' : '',
1040
+ const text = [origin?.type === 'user/message' && origin.data.source.kind === 'user' && access.sendTools && (!access.allowedTools || access.allowedTools.includes('partner_identity')) && personaReferenceRequested(query) ? renderPersonaReference() : '', access.allowedTools || !access.sendTools ? access.denialReason + ' 回复能力通过 partner_tools 查询和调用(未开启统一入口时使用原工具);语音能力为 partner_qq_reply;不能代办操作,不承诺使用私有能力。' : '',
984
1041
  renderIdentityRelationship(companion, identityRelation ? { channel: 'qq', matched: identityRelation.matched } : { channel: 'default' }),
985
1042
  renderAffection(companion, affectionAudience),
986
1043
  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),
987
1044
  recalled ? renderMemory(recalled.relevant, recalled.connections, recalled.scenes, recalled.history) : '',
988
1045
  recalled && relationshipAllowed ? renderProfile(recalled.profile) : '',
989
1046
  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)),
1047
+ renderQqIdentityRelation(identityRelation, autonomous && access.sendTools && !access.allowedTools),
1048
+ renderSleepContext(companion, this.store.snapshot().sessions.filter(r => r.companionId === companion.id && r.sessionId === route.sessionId).map(r => r.id), autonomous ? undefined : last?.id),
992
1049
  ].filter(Boolean).join('\n\n');
993
1050
  this.characterContexts.get(agent.session.id)?.();
994
1051
  // A changing .section() appends an entire system prompt on in-history routes.
@@ -1007,7 +1064,7 @@ export class PartnerAgentRuntime {
1007
1064
  const companion = this.configurations.companion(companionId);
1008
1065
  if (!companion?.automation.memory.enabled)
1009
1066
  return;
1010
- const routes = [...new Map(this.store.snapshot().sessions.filter(r => r.companionId === companionId).map(r => [r.sessionId, r])).values()];
1067
+ const routes = this.store.snapshot().sessions.filter(r => r.companionId === companionId && r.kind === 'local');
1011
1068
  for (const route of routes) {
1012
1069
  if (this.isArchived(route))
1013
1070
  continue;
@@ -1031,6 +1088,8 @@ export class PartnerAgentRuntime {
1031
1088
  replies.push(textContent(next.content));
1032
1089
  }
1033
1090
  const original = agent.session.snapshotEvents().find(e => e.type === 'user/message' && e.data.id === item.id);
1091
+ if (!original || !automaticMemoryAllowed(route.sessionId, [original], this.store.snapshot().sessions))
1092
+ continue;
1034
1093
  await this.memory.enqueue({ id: 'renewal-' + createHash('sha256').update(route.sessionId + ':' + item.id).digest('hex').slice(0, 32),
1035
1094
  companionId, scopeId: scope, sessionId: route.sessionId, at: original?.time ?? Date.now(), userMessageIds: [item.id],
1036
1095
  user: textContent(item.content), assistant: replies.join('\n\n') || '(本条消息没有已完成的回复,仅整理用户原话,不推测处理结果。)',
@@ -1039,17 +1098,17 @@ export class PartnerAgentRuntime {
1039
1098
  }
1040
1099
  }
1041
1100
  async renewReviewedContexts(companionId, day) {
1042
- if (!this.memory || this.isCompanionBusy(companionId) || this.queues.has(`companion:${companionId}`))
1101
+ if (!this.memory || this.isCompanionBusy(companionId) || this.store.snapshot().sessions.some(r => r.companionId === companionId && this.queues.has(r.sessionId)))
1043
1102
  return 0;
1044
1103
  const companion = this.configurations.companion(companionId);
1045
1104
  if (!companion || companion.automation.memory.contextRenewalEnabled === false)
1046
1105
  return 0;
1047
- const learning = companion.automation.memory.enabled;
1048
1106
  let renewed = 0;
1049
1107
  const routes = [...new Map(this.store.snapshot().sessions.filter(r => r.companionId === companionId).map(r => [r.sessionId, r])).values()];
1050
1108
  for (const route of routes) {
1051
1109
  if (this.ctx.workspaceRegistry.archivedSessionIds.includes(route.sessionId))
1052
1110
  continue;
1111
+ const learning = companion.automation.memory.enabled && this.store.snapshot().sessions.some(r => r.kind === 'local' && r.sessionId === route.sessionId && r.companionId === companionId);
1053
1112
  const scope = conversationMemoryScope(route, this.store.snapshot().sessions);
1054
1113
  const agent = await this.ensureAgent(companion, route);
1055
1114
  if (agent.session.surface.nodes.length <= 1) {
@@ -1068,7 +1127,7 @@ export class PartnerAgentRuntime {
1068
1127
  return false;
1069
1128
  if (!learning)
1070
1129
  return true;
1071
- return this.memory.coversContext(companionId, scope, agent.session.deriveMessages().filter(m => m.role === 'user' && m.source.kind === 'user').map(m => ({ id: m.id, text: textContent(m.content) })));
1130
+ return this.memory.coversContext(companionId, scope, agent.session.deriveMessages().filter(m => m.role === 'user' && m.source.kind === 'user' && automaticMemoryAllowed(route.sessionId, agent.session.snapshotEvents().filter(e => e.type === 'user/message' && e.data.id === m.id), this.store.snapshot().sessions)).map(m => ({ id: m.id, text: textContent(m.content) })));
1072
1131
  }, session => this.ctx.sessions.flush(session), learning);
1073
1132
  if (removed > 0 || agent.session.deriveMessages().some(m => m.role === 'user' && m.source.kind === 'plugin:@lemoncat7/dsh-partner' && m.source.form === 'notice' && m.source.summary === `上下文焕新 ${day}`)) {
1074
1133
  await this.memory.markRenewed(companionId, route.sessionId, day, removed);
@@ -1153,11 +1212,13 @@ export class PartnerAgentRuntime {
1153
1212
  restored?.dispose();
1154
1213
  this.restoredCompositions.delete(sessionId);
1155
1214
  }
1156
- async drive(companion, channelId, userId, message, progress, queuedAt = Date.now()) {
1215
+ async drive(companion, channelId, userId, message, progress, queuedAt = Date.now(), preparedRoute) {
1157
1216
  reportProgress(progress, 'working');
1158
1217
  const enteredAt = Date.now();
1159
1218
  let submittedAt = enteredAt, modelEndedAt = enteredAt, lastTurnEnd;
1160
- const session = await this.ensureSession(companion, channelId, userId, message.sessionTitle);
1219
+ const session = preparedRoute ?? await this.ensureSession(companion, channelId, userId, message.sessionTitle);
1220
+ if (!this.store.snapshot().sessions.some(r => r.id === session.id && r.sessionId === session.sessionId))
1221
+ throw new Error('会话配置已变化,请重新发送消息');
1161
1222
  const agent = await this.ensureAgent(companion, session);
1162
1223
  if (agent.status !== 'idle')
1163
1224
  await agent.whenIdle();
@@ -1179,7 +1240,7 @@ export class PartnerAgentRuntime {
1179
1240
  if (event.type === 'turn/start' || event.type === 'turn/end' || (isInternalTaskNotice(event) && !isRequirementDeliveryReceipt(event)))
1180
1241
  directProgress = false;
1181
1242
  if (event.type === 'user/message' && event.data.source.kind === 'user')
1182
- directProgress = true;
1243
+ directProgress = questionOrigin(agent.session.id, agent.session.snapshotEvents(), this.store.snapshot().sessions)?.id === session.id;
1183
1244
  const stage = replyStage(event);
1184
1245
  if (stage)
1185
1246
  reportProgress(progress, stage);
@@ -1189,6 +1250,7 @@ export class PartnerAgentRuntime {
1189
1250
  reportProgress(progress, activity);
1190
1251
  }
1191
1252
  });
1253
+ const detachPreamble = progress?.acknowledge ? installTaskPreamble(agent, progress.acknowledge, () => questionOrigin(agent.session.id, agent.session.snapshotEvents(), this.store.snapshot().sessions)?.id === session.id) : undefined;
1192
1254
  try {
1193
1255
  const userMessage = createUserMessage({
1194
1256
  content: inbound.content,
@@ -1202,6 +1264,7 @@ export class PartnerAgentRuntime {
1202
1264
  modelEndedAt = Date.now();
1203
1265
  }
1204
1266
  finally {
1267
+ detachPreamble?.();
1205
1268
  detach();
1206
1269
  }
1207
1270
  const response = channelReplyPartsAfter(agent.session.snapshotEvents(), startSeq);
@@ -1273,7 +1336,7 @@ export class PartnerAgentRuntime {
1273
1336
  return existing;
1274
1337
  const sharedScope = conversationMemoryScope(primary, [primary]);
1275
1338
  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)
1339
+ if ((!isQq || !target.startsWith('group:') && qqSettings(channel?.qq).mainSessionPeers.includes(target)) && (existing?.memoryScopeId !== sharedScope || sources.length))
1277
1340
  await this.memory?.mergeScopes(companion.id, sharedScope, [...new Set([...sources, memoryScope(channelId, userId)])], recoverLeases);
1278
1341
  let result;
1279
1342
  await this.store.update(state => {