agentworkshop 0.5.5 → 0.5.6

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 (46) hide show
  1. package/.AgentWorkShop/prompts/compaction-hints.md +9 -0
  2. package/.AgentWorkShop/prompts/host-tools.json +664 -630
  3. package/.AgentWorkShop/prompts/peer-reply-cross-channel.md +10 -0
  4. package/.AgentWorkShop/prompts/system-manual.md +6 -1
  5. package/.output/.AgentWorkShop/prompts/compaction-hints.md +9 -0
  6. package/.output/.AgentWorkShop/prompts/host-tools.json +664 -630
  7. package/.output/.AgentWorkShop/prompts/peer-reply-cross-channel.md +10 -0
  8. package/.output/.AgentWorkShop/prompts/system-manual.md +6 -1
  9. package/.output/nitro.json +1 -1
  10. package/.output/public/_nuxt/builds/latest.json +1 -1
  11. package/.output/public/_nuxt/builds/meta/d50c6ec2-801c-4ba2-97d8-a4db401b702c.json +1 -0
  12. package/.output/server/chunks/_/nitro.mjs +1920 -1211
  13. package/.output/server/chunks/_/nitro.mjs.map +1 -1
  14. package/.output/server/chunks/routes/api/workshop/channels/_id/index3.post.mjs +25 -3
  15. package/.output/server/chunks/routes/api/workshop/channels/_id/index3.post.mjs.map +1 -1
  16. package/.output/server/package.json +1 -1
  17. package/package.json +1 -1
  18. package/scripts/_dbg-cross-channel-e2e.mjs +157 -0
  19. package/scripts/test-context-governor.ts +176 -0
  20. package/scripts/test-memory-brief-chronicle.ts +180 -0
  21. package/scripts/test-memory-dynamic.ts +3 -1
  22. package/scripts/test-memory-rrf-mmr.ts +114 -0
  23. package/server/api/workshop/channels/[id]/messages/index.post.ts +26 -1
  24. package/server/api/workshop/ws.ts +1 -0
  25. package/server/data/daqs.json +35 -13
  26. package/server/data/dcw-lines.json +14 -0
  27. package/server/data/dcw-products.json +14 -0
  28. package/server/data/dcw-recipes.json +28 -0
  29. package/server/data/dcw-rollback.json +122 -1
  30. package/server/data/dcw-runs.json +54 -36
  31. package/server/data/dcw-writes.json +60 -0
  32. package/server/data/dcws.json +42 -0
  33. package/server/data/line-runs.json +32 -22
  34. package/server/services/workshop/agents/adapters/omp-rpc-client.ts +65 -0
  35. package/server/services/workshop/agents/agent-interface.ts +26 -1
  36. package/server/services/workshop/agents/mock-agent.ts +12 -0
  37. package/server/services/workshop/agents/omp-agent.ts +277 -8
  38. package/server/services/workshop/db/database.ts +16 -1
  39. package/server/services/workshop/db/memory.repo.ts +22 -2
  40. package/server/services/workshop/prompts/loader.ts +6 -5
  41. package/server/services/workshop/runtime/agent-runtime.ts +52 -6
  42. package/server/services/workshop/runtime/channel-runtime.ts +14 -6
  43. package/server/services/workshop/runtime/manager.ts +192 -3
  44. package/server/services/workshop/runtime/memory.ts +313 -78
  45. package/server/services/workshop/types/task.ts +14 -0
  46. package/.output/public/_nuxt/builds/meta/f2f649c5-fd3b-4507-a3a1-8b4a194de72d.json +0 -1
@@ -5,7 +5,7 @@
5
5
  * 权威契约见 docs/superpowers/plans/2026-08-13-agent-workshop-multi-agent.md 核心契约块。
6
6
  */
7
7
  import type { A2AMessage, A2AArtifact, A2AError, ChannelMail, Part } from '../types/a2a'
8
- import type { WorkspaceTask, AgentTaskQueueView, AgentStatusView } from '../types/task'
8
+ import type { WorkspaceTask, AgentTaskQueueView, AgentStatusView, AgentContextStats } from '../types/task'
9
9
 
10
10
  /** Agent 元信息:Channel 内成员声明(hook harness 类型与配置) */
11
11
  export interface AgentInfo {
@@ -59,6 +59,16 @@ export interface AgentWorkspace {
59
59
  cancelTask(taskId: string): Promise<WorkspaceTask>
60
60
  /** 点对点发消息给同事 */
61
61
  sendMessage(input: { toAgentId: string, parts: Part[], metadata?: Record<string, unknown> }): Promise<A2AMessage>
62
+ /**
63
+ * (仅 lead)跨 Channel 点对点:直投目标 channel 的 lead mailbox(沿用 mailbox/route 机制)。
64
+ * 权限:发送方必须是本 channel 的 lead;目标 channel 与本 channel 须同主且启用、有 lead。
65
+ */
66
+ sendCrossChannelMessage(input: { toChannelId: string, parts: Part[], requireReply?: boolean, inReplyTo?: string }): Promise<{
67
+ messageId: string
68
+ toChannelId: string
69
+ toChannelName: string
70
+ toLeadAgentId: string
71
+ }>
62
72
  /**
63
73
  * 拒绝指派给自己的任务(能力/范畴不匹配):任务置 FAILED(调度器改派他人),
64
74
  * 并向派发方(任务创建者,缺省 channel lead)回执拒绝原因。
@@ -91,6 +101,11 @@ export interface AgentWorkspace {
91
101
  * scope='private' → 本人记忆库;scope='shared' → Channel 公共记忆域(全员可检索)。
92
102
  */
93
103
  saveMemory(input: { title: string, content: string, importance?: number, scope: 'private' | 'shared', dedupKey?: string }): Promise<{ scope: 'private' | 'shared', dedupKey: string }>
104
+ /**
105
+ * 会话压缩摘要入库(harvest 桥;omp compaction 产出的会话摘要 → 本成员持久记忆)。
106
+ * 平台 70% 主动压缩与 omp 原生 auto-compaction 双路统一经此落库(episodic-session)。
107
+ */
108
+ recordSessionMemory?(input: { summary: string, tokensBefore?: number, tokensAfter?: number, reason?: string }): Promise<void>
94
109
  /** 订阅同事产出 */
95
110
  subscribe(input: { agentIds?: string[] }): Promise<void>
96
111
  /** (lead)在本 channel 内新建团队成员(worker):按需建模板并克隆为独立实例 */
@@ -216,4 +231,14 @@ export interface AgentInterface {
216
231
  * 探到进程已死时收敛为已退出,让在途回合归位、下一回合自动重生子进程。
217
232
  */
218
233
  reconcileProcess?(): void
234
+ /**
235
+ * 可选:harness 上下文用量快照(omp;被动缓存值,无探测 RPC)。
236
+ * 进程内 harness(mock/claude)不实现 → getStatus 不透出 context 字段。
237
+ */
238
+ getContextStats?(): AgentContextStats | null
239
+ /**
240
+ * 可选:回合落定后的后台钩子(post-settle 上下文压缩检查)。
241
+ * 实现方自守卫(仅回合间隙发起、异常不抛出);调用方 AgentRuntime 在信箱无排队消息时调用。
242
+ */
243
+ onTurnSettled?(): Promise<void>
219
244
  }
@@ -352,6 +352,18 @@ export class MockAgentImpl implements AgentInterface {
352
352
  const text = request.message.parts
353
353
  .map(p => ('text' in p ? p.text : 'data' in p ? JSON.stringify(p.data) : ''))
354
354
  .join('\n')
355
+ // 跨 Channel 来信:发送方不是本 channel 成员,回复须走 lead 级跨 Channel 发送(回源 channel)
356
+ const crossChannel = request.message.metadata?.['x-aw-cross-channel'] === 'true'
357
+ const fromChannel = request.message.metadata?.['x-aw-from-channel']
358
+ if (crossChannel && typeof fromChannel === 'string') {
359
+ await ctx.workspace.sendCrossChannelMessage({
360
+ toChannelId: fromChannel,
361
+ parts: [{ text: `mock 跨Channel回复(${ctx.agentId}):已处理「${text.slice(0, 60)}」。执行结果:完成;你所需的内容已包含在本回复中。本回复不需要再响应。` }],
362
+ inReplyTo: request.message.messageId,
363
+ })
364
+ yield { kind: 'done' }
365
+ return
366
+ }
355
367
  await ctx.workspace.sendMessage({
356
368
  toAgentId: fromId,
357
369
  parts: [{ text: `mock 回复(${ctx.agentId}):已处理「${text.slice(0, 60)}」。执行结果:完成;你所需的内容已包含在本回复中。本回复不需要再响应。` }],
@@ -28,10 +28,11 @@ import type {
28
28
  SupervisionSnapshot,
29
29
  } from './agent-interface'
30
30
  import type { A2AArtifact, A2AMessage, Part } from '../types/a2a'
31
- import type { WorkspaceTask } from '../types/task'
31
+ import type { AgentContextStats, WorkspaceTask } from '../types/task'
32
32
  import {
33
33
  OmpRpcClient,
34
34
  type AgentSessionEvent,
35
+ type CompactionResult,
35
36
  type RpcHostToolDefinition,
36
37
  type HostToolCallRequest,
37
38
  } from './adapters/omp-rpc-client'
@@ -52,6 +53,7 @@ import {
52
53
  import { toolDaqQuery, toolDcwControl, toolDcwJudge, toolDcwJournal, toolDcwRollback, toolMyIndustrialNodes } from './industrial-tools'
53
54
  import { buildIndustrialContext, industrialLoopGuide } from './industrial-context'
54
55
  import { extractTaskMode } from '../runtime/execution-mode'
56
+ import { envNum } from '../runtime/memory'
55
57
 
56
58
  const log = createLogger('workshop.omp')
57
59
 
@@ -217,6 +219,194 @@ export class OmpRpcAgentImpl implements AgentInterface {
217
219
  */
218
220
  private replyContext: { fromId: string, messageId: string } | null = null
219
221
 
222
+ // ===== 上下文治理(70% 无中断压缩环)=====
223
+ /** 压缩进行中(平台 gate 与 omp 原生压缩共用互斥位) */
224
+ private compacting = false
225
+ private lastCompactAt = 0
226
+ /** omp 不支持 compact 命令(旧版)→ 永久停用平台压缩;原生 auto-compaction 兜底,harvest 照常 */
227
+ private compactLegacy = false
228
+ /** 模型上下文窗口(get_state/get_session_stats 探测;null = 未知,percent 不可算) */
229
+ private contextWindow: number | null = null
230
+ private sessionId: string | null = null
231
+ /** harvest 双路去重(compact 响应与 compaction_end 事件可能双达) */
232
+ private lastHarvestKey = ''
233
+ private lastHarvestAt = 0
234
+
235
+ /** 总开关(AW_OMP_COMPACT_ENABLED=0 显式关闭;envNum 拒 0,故单独解析) */
236
+ private compactEnabled(): boolean {
237
+ const v = Number(process.env.AW_OMP_COMPACT_ENABLED)
238
+ return !(Number.isFinite(v) && v === 0)
239
+ }
240
+
241
+ private compactThreshold(): number {
242
+ return Math.min(1, envNum('AW_OMP_COMPACT_THRESHOLD', 0.7))
243
+ }
244
+
245
+ private compactMinIntervalMs(): number {
246
+ return envNum('AW_OMP_COMPACT_MIN_INTERVAL_MS', 300_000)
247
+ }
248
+
249
+ private compactWaitMs(): number {
250
+ return envNum('AW_OMP_COMPACT_WAIT_MS', 120_000)
251
+ }
252
+
253
+ /** 被动上下文用量快照(无探测 RPC;getStatus 透出用) */
254
+ getContextStats(): AgentContextStats | null {
255
+ const usage = this.client?.getContextUsage() ?? null
256
+ const window = this.contextWindow ?? usage?.contextWindow ?? null
257
+ if (!usage && window === null) return null
258
+ const usedTokens = usage?.tokens ?? 0
259
+ return {
260
+ usedTokens,
261
+ contextWindow: window,
262
+ percent: window && window > 0 ? Math.min(1, usedTokens / window) : null,
263
+ compacting: this.compacting,
264
+ }
265
+ }
266
+
267
+ /**
268
+ * 上下文用量探测:get_session_stats 权威值;不可用退化为被动 usage + 已探测窗口。
269
+ * percent 归一化到 0-1(omp 可能返回 0-100)。
270
+ */
271
+ private async probeUsage(client: OmpRpcClient): Promise<{ tokens: number, contextWindow: number | null, percent: number | null } | null> {
272
+ try {
273
+ const resp = await client.send({ type: 'get_session_stats' })
274
+ const cu = ((resp.data ?? {}) as Record<string, unknown>).contextUsage as
275
+ | { tokens?: number, contextWindow?: number, percent?: number }
276
+ | undefined
277
+ if (cu && typeof cu.tokens === 'number' && cu.tokens > 0) {
278
+ if (typeof cu.contextWindow === 'number' && cu.contextWindow > 0) {
279
+ this.contextWindow = cu.contextWindow
280
+ client.setContextWindow(cu.contextWindow)
281
+ }
282
+ const window = this.contextWindow ?? cu.contextWindow ?? null
283
+ const rawPercent = typeof cu.percent === 'number' ? (cu.percent > 1 ? cu.percent / 100 : cu.percent) : null
284
+ const percent = rawPercent ?? (window && window > 0 ? Math.min(1, cu.tokens / window) : null)
285
+ return { tokens: cu.tokens, contextWindow: window, percent }
286
+ }
287
+ }
288
+ catch { /* get_session_stats 不可用 → 被动跟踪 */ }
289
+ return client.getContextUsage()
290
+ }
291
+
292
+ /**
293
+ * 上下文治理门控:三条 prompt 路径(worker/peer/supervise)在回合间隙统一调用;
294
+ * post-settle 经 onTurnSettled 复用同一守卫。任何失败路径都放行——压缩晚一轮
295
+ * 永远好过中断作业。三重防线:仅回合间隙发起 → get_state 复查 isStreaming/
296
+ * isCompacting → compacting 互斥位 + 最小间隔防振荡。
297
+ */
298
+ private async contextGate(reason: string): Promise<void> {
299
+ if (!this.compactEnabled() || this.compactLegacy || this.compacting) return
300
+ const client = this.client
301
+ if (!client || !client.alive) return
302
+ try {
303
+ const usage = await this.probeUsage(client)
304
+ if (!usage || usage.percent === null || usage.percent < this.compactThreshold()) return
305
+ if (Date.now() - this.lastCompactAt < this.compactMinIntervalMs()) return
306
+ // 回合间隙硬校验:流式中/压缩中绝不发起(不中断执行的硬约束)
307
+ const st = await client.send({ type: 'get_state' })
308
+ const s = (st.data ?? {}) as Record<string, unknown>
309
+ if (s.isStreaming === true || s.isCompacting === true) return
310
+ if (typeof s.sessionId === 'string' && s.sessionId) this.sessionId = s.sessionId
311
+ this.compacting = true
312
+ this.lastCompactAt = Date.now()
313
+ try {
314
+ const result = await this.runCompaction(client)
315
+ if (result?.summary) {
316
+ await this.harvestCompaction(
317
+ result.summary,
318
+ result.tokensBefore,
319
+ result.tokensAfter ?? result.estimatedTokensAfter,
320
+ reason,
321
+ )
322
+ }
323
+ }
324
+ finally {
325
+ this.compacting = false
326
+ }
327
+ }
328
+ catch (err) {
329
+ const msg = err instanceof Error ? err.message : String(err)
330
+ if (/compact/i.test(msg) && /unknown|unsupported|invalid|无效|未知|不支持/i.test(msg)) this.compactLegacy = true
331
+ log.warn(`[OmpRpcAgent:${this.selfAgentId}] contextGate(${reason}) 失败,放行: ${msg}`)
332
+ }
333
+ }
334
+
335
+ /**
336
+ * 发起 compact 并等待 compaction_end(响应内含 result 则立即收口;
337
+ * 超时放行——压缩在 harness 内后台继续,isCompacting 防后续双发)。
338
+ */
339
+ private runCompaction(client: OmpRpcClient): Promise<CompactionResult | null> {
340
+ return new Promise<CompactionResult | null>((resolve) => {
341
+ let settled = false
342
+ const finish = (r: CompactionResult | null): void => {
343
+ if (settled) return
344
+ settled = true
345
+ off()
346
+ clearTimeout(timer)
347
+ resolve(r)
348
+ }
349
+ const off = client.onEvent((event) => {
350
+ if (event.type === 'compaction_end') {
351
+ finish((event as { result?: CompactionResult }).result ?? null)
352
+ }
353
+ else if (event.type === '__process_exit__') {
354
+ finish(null)
355
+ }
356
+ })
357
+ const timer = setTimeout(() => {
358
+ log.warn(`[OmpRpcAgent:${this.selfAgentId}] compact 等待超时(${this.compactWaitMs()}ms)→ 放行(压缩后台继续)`)
359
+ finish(null)
360
+ }, this.compactWaitMs())
361
+ void client.send({ type: 'compact', customInstructions: this.compactionHints() })
362
+ .then((resp) => {
363
+ const result = (resp.data as { result?: CompactionResult } | undefined)?.result
364
+ if (result?.summary) finish(result)
365
+ // 响应无 result:等 compaction_end 事件(协议权威语义)
366
+ })
367
+ .catch((err) => {
368
+ const msg = err instanceof Error ? err.message : String(err)
369
+ if (/compact/i.test(msg)) this.compactLegacy = true
370
+ log.warn(`[OmpRpcAgent:${this.selfAgentId}] compact 发起失败: ${msg}`)
371
+ finish(null)
372
+ })
373
+ })
374
+ }
375
+
376
+ /** 压缩摘要指令(外置 prompts;缺失时省略,omp 用默认策略) */
377
+ private compactionHints(): string | undefined {
378
+ try {
379
+ return renderPrompt('compaction-hints')
380
+ }
381
+ catch {
382
+ return undefined
383
+ }
384
+ }
385
+
386
+ /**
387
+ * 压缩摘要 harvest(三路统一:平台主动 compact / omp 阈值自动 / overflow):
388
+ * 经 workspace.recordSessionMemory 落库为本人 episodic-session 记忆。
389
+ * 双路去重:同摘要 60s 内只入库一次。
390
+ */
391
+ private async harvestCompaction(summary: string, tokensBefore?: number, tokensAfter?: number, reason?: string): Promise<void> {
392
+ const key = summary.slice(0, 120)
393
+ if (key === this.lastHarvestKey && Date.now() - this.lastHarvestAt < 60_000) return
394
+ this.lastHarvestKey = key
395
+ this.lastHarvestAt = Date.now()
396
+ try {
397
+ await this.workspace?.recordSessionMemory?.({ summary, tokensBefore, tokensAfter, reason })
398
+ log.info(`[OmpRpcAgent:${this.selfAgentId}] 压缩摘要已入记忆(${summary.length} 字,reason=${reason ?? 'auto'})`)
399
+ }
400
+ catch (err) {
401
+ log.warn(`[OmpRpcAgent:${this.selfAgentId}] 压缩摘要入库失败(不影响会话):`, err instanceof Error ? err.message : err)
402
+ }
403
+ }
404
+
405
+ /** 回合落定钩子(AgentRuntime 在信箱空时调用):后台压缩检查,自守卫不抛错 */
406
+ async onTurnSettled(): Promise<void> {
407
+ await this.contextGate('post-settle')
408
+ }
409
+
220
410
  constructor(config: Record<string, unknown> = {}) {
221
411
  this.config = config as OmpAgentConfig
222
412
  // factory 注入的 agent 身份(无需等待 init())
@@ -449,6 +639,9 @@ export class OmpRpcAgentImpl implements AgentInterface {
449
639
  if (!this.client) return []
450
640
  if (this.supervising) return [] // 上一轮 supervise 未收口:跳过本拍(节流即正确)
451
641
 
642
+ // 上下文门控(≥70% 先压缩再调度;回合间隙发起,失败放行)
643
+ await this.contextGate('supervise')
644
+
452
645
  const prompt = await this.buildSupervisePrompt(snapshot, ctx.memory)
453
646
  // 150s 上界:supervise 持 lead.execLock 期间信箱消费停顿(更久会拖垮 worker 回执处理);
454
647
  // supervise 是一次真实 LLM 回合:omp 冷启动(插件/MCP 加载 30~90s)+ 慢 provider
@@ -589,9 +782,14 @@ export class OmpRpcAgentImpl implements AgentInterface {
589
782
  const msgText = partsToText(msg.parts)
590
783
  const requireReply = msg.metadata?.['x-aw-require-reply'] === 'true'
591
784
  const isReply = typeof msg.metadata?.['x-aw-in-reply-to'] === 'string'
785
+ // 跨 Channel 来信:发送方是其他 channel 的 lead,回执必须走 send_cross_channel_message
786
+ const crossChannel = msg.metadata?.['x-aw-cross-channel'] === 'true'
787
+ const fromChannel = typeof msg.metadata?.['x-aw-from-channel'] === 'string'
788
+ ? String(msg.metadata['x-aw-from-channel'])
789
+ : ''
592
790
  // 回执自动关联仅对真实 agent 发送者生效(人类无 agentId 可回投;
593
791
  // 对人类的回复经事件流/时间线可见)
594
- this.replyContext = requireReply && request.fromAgentId
792
+ this.replyContext = requireReply && request.fromAgentId && !crossChannel
595
793
  ? { fromId: request.fromAgentId, messageId: msg.messageId }
596
794
  : null
597
795
 
@@ -599,11 +797,13 @@ export class OmpRpcAgentImpl implements AgentInterface {
599
797
  ? `You are "${this.agentName}", the LEAD coordinator of a multi-agent team (Channel: ${this.channelId}). A team member sent you a direct message.`
600
798
  : `You are "${this.agentName}", a worker agent in a multi-agent team (Channel: ${this.channelId}).`
601
799
 
602
- // 消息应答指令:必复/可选两态 + lead 名册提示(全部外置 prompts/)
603
- const respondBlock = renderPrompt(requireReply ? 'peer-reply-required' : 'peer-reply-optional', {
604
- fromId,
605
- messageId: msg.messageId,
606
- })
800
+ // 消息应答指令:必复/可选/跨 Channel 三态 + lead 名册提示(全部外置 prompts/)
801
+ const respondBlock = crossChannel
802
+ ? renderPrompt('peer-reply-cross-channel', { fromId, messageId: msg.messageId, fromChannel })
803
+ : renderPrompt(requireReply ? 'peer-reply-required' : 'peer-reply-optional', {
804
+ fromId,
805
+ messageId: msg.messageId,
806
+ })
607
807
  const peerBody = renderPrompt('peer-message', {
608
808
  fromId,
609
809
  messageId: msg.messageId,
@@ -647,6 +847,8 @@ export class OmpRpcAgentImpl implements AgentInterface {
647
847
  if (!client) {
648
848
  throw new Error('omp 客户端未就绪')
649
849
  }
850
+ // 上下文门控(≥70% 时在回合间隙先压缩再发 prompt;等待有界,失败放行)
851
+ await this.contextGate('pre-prompt')
650
852
  const queue: AgentEvent[] = []
651
853
  let resolveWait: (() => void) | null = null
652
854
  let isDone = false
@@ -929,6 +1131,29 @@ export class OmpRpcAgentImpl implements AgentInterface {
929
1131
  error: { code: 'OMP_PROCESS_EXIT', message: 'omp 子进程意外退出' },
930
1132
  }]
931
1133
 
1134
+ // ===== 上下文治理事件(回合中到达的原生阈值/overflow 压缩;不污染事件流)=====
1135
+ case 'compaction_start':
1136
+ this.compacting = true
1137
+ return []
1138
+
1139
+ case 'compaction_end': {
1140
+ this.compacting = false
1141
+ const result = (event as { result?: CompactionResult }).result
1142
+ if (result?.summary) {
1143
+ void this.harvestCompaction(
1144
+ result.summary,
1145
+ result.tokensBefore,
1146
+ result.tokensAfter ?? result.estimatedTokensAfter,
1147
+ (event as { reason?: string }).reason,
1148
+ )
1149
+ }
1150
+ return []
1151
+ }
1152
+
1153
+ // 回合完全落定(无自动重试/压缩重试/排队续跑):后续压缩检查由 AgentRuntime 驱动
1154
+ case 'agent_settled':
1155
+ return []
1156
+
932
1157
  case '__error__':
933
1158
  return [{
934
1159
  kind: 'error',
@@ -1176,7 +1401,12 @@ export class OmpRpcAgentImpl implements AgentInterface {
1176
1401
  // 只配其一时也显式发送,避免 respawn 后静默回落默认模型)
1177
1402
  if (this.config.provider || this.config.model) {
1178
1403
  try {
1179
- await client.send({ type: 'set_model', provider: this.config.provider, modelId: this.config.model })
1404
+ // 未配置一侧保持 undefined(JSON 序列化丢键)→ omp 以当前已设值补全
1405
+ await client.send({
1406
+ type: 'set_model',
1407
+ provider: this.config.provider as string,
1408
+ modelId: this.config.model as string,
1409
+ })
1180
1410
  }
1181
1411
  catch {
1182
1412
  // 模型设置失败不致命(用 omp 默认模型)
@@ -1187,11 +1417,30 @@ export class OmpRpcAgentImpl implements AgentInterface {
1187
1417
  client.onHostToolCall(req => this.handleHostTool(req))
1188
1418
  await client.send({ type: 'set_host_tools', tools: hostToolsForRole(this.agentRole) })
1189
1419
 
1420
+ // 上下文治理探测(feature-detect 一次;失败退化被动 usage 跟踪)+ 原生压缩兜底保持开启
1421
+ void this.probeContext(client).catch(() => {})
1422
+ void client.send({ type: 'set_auto_compaction', enabled: true }).catch(() => {})
1423
+
1190
1424
  this.client = client
1191
1425
  this.hostToolsRegistered = true
1192
1426
  }
1193
1427
  }
1194
1428
 
1429
+ /** 上下文状态探测:get_state 取 contextWindow/sessionId(一次;失败仅损失 percent 精度) */
1430
+ private async probeContext(client: OmpRpcClient): Promise<void> {
1431
+ try {
1432
+ const resp = await client.send({ type: 'get_state' })
1433
+ const data = (resp.data ?? {}) as Record<string, unknown>
1434
+ const model = data.model as { contextWindow?: number } | undefined
1435
+ if (typeof model?.contextWindow === 'number' && model.contextWindow > 0) {
1436
+ this.contextWindow = model.contextWindow
1437
+ client.setContextWindow(model.contextWindow)
1438
+ }
1439
+ if (typeof data.sessionId === 'string' && data.sessionId) this.sessionId = data.sessionId
1440
+ }
1441
+ catch { /* get_state 不可用:仅被动 usage */ }
1442
+ }
1443
+
1195
1444
  // ===== 内部:host tool handler(workspace 桥接) =====
1196
1445
 
1197
1446
  private async handleHostTool(req: HostToolCallRequest): Promise<{ text: string, isError?: boolean }> {
@@ -1309,6 +1558,26 @@ export class OmpRpcAgentImpl implements AgentInterface {
1309
1558
  return { text: `消息 ${sent.messageId.slice(0, 8)}… 已发送给 ${toAgentId}(priority=${priority})${triggerNote}` }
1310
1559
  }
1311
1560
 
1561
+ case 'send_cross_channel_message': {
1562
+ // 跨 Channel 通信(仅 lead):直投目标 channel 的 lead mailbox
1563
+ const toChannelId = req.arguments.to_channel_id as string
1564
+ const message = req.arguments.message as string
1565
+ if (!toChannelId || !message) return { text: '缺少 to_channel_id 或 message', isError: true }
1566
+ try {
1567
+ const r = await ws.sendCrossChannelMessage({
1568
+ toChannelId,
1569
+ parts: [{ text: message }],
1570
+ requireReply: req.arguments.require_reply === true,
1571
+ inReplyTo: req.arguments.in_reply_to as string | undefined,
1572
+ })
1573
+ const note = req.arguments.require_reply === true ? '(已要求对方 Leader 回复)' : ''
1574
+ return { text: `跨 Channel 消息 ${r.messageId.slice(0, 8)}… 已送达 channel「${r.toChannelName}」的 Leader(${r.toLeadAgentId.slice(0, 8)}…)${note}。对方将按你的信息需求处理;其回复会经 mailbox 到达你这里。` }
1575
+ }
1576
+ catch (err) {
1577
+ return { text: `跨 Channel 发送失败: ${err instanceof Error ? err.message : String(err)}(仅 Leader 可跨 Channel 通信)`, isError: true }
1578
+ }
1579
+ }
1580
+
1312
1581
  case 'refuse_task': {
1313
1582
  const taskId = req.arguments.task_id as string
1314
1583
  const reason = req.arguments.reason as string
@@ -549,12 +549,27 @@ export interface TeamMemberRow {
549
549
  createdAt: string
550
550
  }
551
551
 
552
+ /**
553
+ * 记忆类别:episodic-task/episodic-peer/episodic-session/episodic-team-task(harvest 族,
554
+ * 参与过期+淘汰)/ semantic(知识,免衰减)/ brief/chronicle/reflection(策展层,
555
+ * 免向量化免维护)。TEXT 自由列(无 CHECK),联合类型仅约束调用面。
556
+ */
557
+ export type MemoryKind
558
+ = | 'episodic-task'
559
+ | 'episodic-peer'
560
+ | 'episodic-session'
561
+ | 'episodic-team-task'
562
+ | 'semantic'
563
+ | 'brief'
564
+ | 'chronicle'
565
+ | 'reflection'
566
+
552
567
  /** agent_memories 表行(content 为已 CJK 切分存储文本;agentId='__team__' 为团队共享行) */
553
568
  export interface MemoryRow {
554
569
  id: string
555
570
  channelId: string
556
571
  agentId: string
557
- kind: 'episodic-task' | 'episodic-peer' | 'semantic'
572
+ kind: MemoryKind
558
573
  title: string
559
574
  content: string
560
575
  importance: number
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { randomUUID } from 'node:crypto'
8
8
  import type { DatabaseSync, StatementSync } from 'node:sqlite'
9
- import type { MemoryRow } from './database'
9
+ import type { MemoryKind, MemoryRow } from './database'
10
10
 
11
11
  /** 团队共享记忆域 sentinel(单 channel 内全员可读) */
12
12
  export const TEAM_AGENT_ID = '__team__'
@@ -14,7 +14,8 @@ export const TEAM_AGENT_ID = '__team__'
14
14
  export interface MemoryUpsertInput {
15
15
  channelId: string
16
16
  agentId: string
17
- kind: 'episodic-task' | 'episodic-peer' | 'semantic'
17
+ /** 语义见 database.ts MemoryKind(harvest 族参与维护;semantic 知识;策展层双免) */
18
+ kind: MemoryKind
18
19
  title: string
19
20
  /** title 的 CJK 切分副本(FTS 索引用;调用方经 AgentMemory.segmentCJK 处理,V8) */
20
21
  titleFts: string
@@ -75,6 +76,15 @@ export function createMemoryRepo(db: DatabaseSync) {
75
76
  const agentIdsStmt = db.prepare(
76
77
  `SELECT DISTINCT agent_id FROM agent_memories WHERE agent_id != ?`,
77
78
  )
79
+ const channelIdsStmt = db.prepare(
80
+ `SELECT DISTINCT channel_id AS channelId FROM agent_memories`,
81
+ )
82
+ const getByIdStmt = db.prepare(
83
+ `SELECT id, channel_id AS channelId, agent_id AS agentId, kind, title, content,
84
+ importance, task_id AS taskId, access_count AS accessCount,
85
+ last_accessed_at AS lastAccessedAt, created_at AS createdAt
86
+ FROM agent_memories WHERE id = ?`,
87
+ )
78
88
  // ===== 向量方法(P1;vec0 分区表 + mem_rowid 辅助列映射)=====
79
89
  // 已实测事实:
80
90
  // - vec0 分区表不支持显式 rowid 插入 → 以 +mem_rowid 辅助列映射 agent_memories.rowid
@@ -188,6 +198,16 @@ export function createMemoryRepo(db: DatabaseSync) {
188
198
  return (agentIdsStmt.all(TEAM_AGENT_ID) as Array<{ agent_id: string }>).map(r => r.agent_id)
189
199
  },
190
200
 
201
+ /** 有记忆的 channel 清单(team 域行按 channel 迭代维护用) */
202
+ listChannelIds(): string[] {
203
+ return (channelIdsStmt.all() as Array<{ channelId: string }>).map(r => r.channelId)
204
+ },
205
+
206
+ /** 按 id 取单行(brief 置顶注入等直取场景;不存在返回 null) */
207
+ getById(id: string): MemoryRow | null {
208
+ return (getByIdStmt.get(id) as MemoryRow | undefined) ?? null
209
+ },
210
+
191
211
  // ===== vec:延迟建表(vecInit)/ 刷新写入(vecSet)/ 删除(vecDelete)/ 分区 kNN(vecSearch)=====
192
212
 
193
213
  /** 首次调用按 dims 建表;同 dims 幂等 true;不同 dims 拒绝 false(禁用向量,绝不重建) */
@@ -18,7 +18,7 @@
18
18
  import { copyFileSync, existsSync, readdirSync, readFileSync, statSync, mkdirSync } from 'node:fs'
19
19
  import { dirname, join, resolve } from 'node:path'
20
20
  import { fileURLToPath } from 'node:url'
21
- import { awHome, findLocalConfigRoot } from '../../../../shared/config/home.mjs'
21
+ import { awHome, findLocalConfigRoot } from '@/shared/config/home.mjs'
22
22
  import { AppError } from '../../../utils/errors'
23
23
 
24
24
  /** 模板变量值(字符串化注入) */
@@ -40,7 +40,8 @@ function packagedPromptsDir(): string | null {
40
40
  return existsSync(candidate) ? candidate : null
41
41
  }
42
42
 
43
- /** 首次解析时把包内初始 prompts 播种到 ~/.AgentWorkShop/prompts(只补缺,不覆盖) */
43
+ /** 首次/每次解析把包内初始 prompts 补种到 ~/.AgentWorkShop/prompts(逐文件只补缺,不覆盖)。
44
+ * 每次解析都执行:包升级新增的提示词文件能进入已存在的 Home(用户已定制文件不回写)。 */
44
45
  function seedHomePrompts(homePrompts: string): void {
45
46
  const src = packagedPromptsDir()
46
47
  if (!src || resolve(src) === resolve(homePrompts)) return
@@ -55,7 +56,7 @@ function seedHomePrompts(homePrompts: string): void {
55
56
  }
56
57
  catch { /* 单文件失败不阻断播种 */ }
57
58
  }
58
- if (seeded > 0) console.log(`[prompts] 已向 ${homePrompts} 播种 ${seeded} 个初始提示词文件`)
59
+ if (seeded > 0) console.log(`[prompts] 已向 ${homePrompts} 补种 ${seeded} 个提示词文件`)
59
60
  }
60
61
 
61
62
  /**
@@ -72,9 +73,9 @@ export function promptsDir(): string {
72
73
  return base
73
74
  }
74
75
 
75
- // ~/.AgentWorkShop/prompts:默认兜底目录,首次解析自动播种初始文件(只补缺,不覆盖用户定制)
76
+ // ~/.AgentWorkShop/prompts:默认兜底目录,解析时自动补种初始文件(只补缺,不覆盖用户定制)
76
77
  const homePrompts = join(awHome(process.env), 'prompts')
77
- if (!existsSync(homePrompts)) seedHomePrompts(homePrompts)
78
+ seedHomePrompts(homePrompts)
78
79
 
79
80
  // ② 项目内 ./.AgentWorkShop/prompts 真实存在 → 项目内优先
80
81
  const localConfigRoot = findLocalConfigRoot(process.cwd())