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
@@ -17,10 +17,31 @@ import type { EmbeddingProvider } from './embedding-provider'
17
17
  const W_RELEVANCE = 0.5
18
18
  const W_RECENCY = 0.3
19
19
  const W_IMPORTANCE = 0.2
20
- const RECENCY_DAYS = 7
21
20
  const RECENT_FALLBACK = 5
22
- const WEAK_HIT_KEEP = 3
23
21
  const MAX_TERMS = 12
22
+ /** CJK 单字词项上限(单字区分度低,12 个易主题漂移;ascii 词项仍取 MAX_TERMS) */
23
+ const MAX_CJK_TERMS = 24
24
+ /** FTS 候选池深度(RRF 融合需要比最终引子更深的榜) */
25
+ const FTS_POOL = 20
26
+ /** Reciprocal Rank Fusion 常数(业界标准 k=60) */
27
+ const RRF_K = 60
28
+ /** MMR 多样性权重(λ×相关性 − (1−λ)×与已选集的最大相似) */
29
+ const MMR_LAMBDA = 0.7
30
+ /** 任务关联加权:候选行 taskId ∈ 兄弟/父任务集时终分加成 */
31
+ const RELATED_TASK_BOOST = 0.3
32
+ /** 策展层 kind(确定性 L0/按 key 直取;不进 L1 排名防双份注入,search_memory 仍可命中) */
33
+ const CURATED_KINDS = new Set(['brief', 'chronicle', 'reflection'])
34
+ /** kind 感知时近衰减半衰期(天;0 = 不衰减) */
35
+ const RECENCY_HALF_LIFE_DAYS: Record<string, number> = {
36
+ 'episodic-peer': 3,
37
+ 'episodic-task': 14,
38
+ 'episodic-session': 14,
39
+ 'episodic-team-task': 14,
40
+ 'semantic': 0,
41
+ 'brief': 0,
42
+ 'chronicle': 0,
43
+ 'reflection': 0,
44
+ }
24
45
  const CONTENT_STORE_LIMIT = 800
25
46
 
26
47
  /** 环境变量安全数值(非有限/非正数 → fallback;防 NaN/0 破坏预算与定时器) */
@@ -45,6 +66,8 @@ export interface RecallOptions {
45
66
  budgetTokens?: number
46
67
  /** 检索域:auto=私有+公共(默认) / private=仅私有 / shared=仅 Channel 公共 */
47
68
  scope?: MemoryScope
69
+ /** 任务关联集(自身+父+兄弟 ≤20):命中行终分 +RELATED_TASK_BOOST(任务需求驱动的相关性) */
70
+ relatedTaskIds?: string[]
48
71
  }
49
72
 
50
73
  export type MemoryScope = 'auto' | 'private' | 'shared'
@@ -68,12 +91,18 @@ export function segmentCJK(text: string): string {
68
91
 
69
92
  export function buildMatchQuery(text: string): string | null {
70
93
  const cleaned = text.replace(/["'*().:+-]/g, ' ').toLowerCase()
71
- const terms = segmentCJK(cleaned)
72
- .split(/\s+/)
73
- .filter(Boolean)
74
- .filter(t => /[\u4e00-\u9fff]/.test(t) || t.length >= 2)
75
- .filter(t => !['and', 'or', 'not', 'near'].includes(t))
76
- .slice(0, MAX_TERMS)
94
+ const words = segmentCJK(cleaned).split(/\s+/).filter(Boolean)
95
+ const cjkTerms: string[] = []
96
+ const otherTerms: string[] = []
97
+ for (const t of words) {
98
+ const isCjk = /[\u4e00-\u9fff]/.test(t)
99
+ if (!isCjk && (t.length < 2 || ['and', 'or', 'not', 'near'].includes(t))) continue
100
+ const pool = isCjk ? cjkTerms : otherTerms
101
+ const cap = isCjk ? MAX_CJK_TERMS : MAX_TERMS
102
+ if (pool.length >= cap) continue
103
+ if (!pool.includes(t)) pool.push(t)
104
+ }
105
+ const terms = [...otherTerms, ...cjkTerms]
77
106
  // 词项一律引号包裹:残留特殊字符(FTS5 列过滤/短语语法)全部惰性化
78
107
  return terms.length > 0 ? terms.map(t => `"${t}"`).join(' OR ') : null
79
108
  }
@@ -137,27 +166,26 @@ export class AgentMemory {
137
166
  : row.agentId === this.opts.agentId
138
167
  }
139
168
 
140
- /** 混合检索原始命中(FTS+向量融合;不排序不 touch) */
169
+ /**
170
+ * 混合检索原始命中(RRF 融合;不排序不 touch)。
171
+ * FTS 榜与向量榜各按名次贡献 1/(k+rank) 累加,再按榜内最高分归一化到 0-1——
172
+ * 相比 v1 的 max 距离归一化,弱首命中/两榜分纲不一致时排序依然稳定。
173
+ */
141
174
  private async collectHits(query: string, scope: MemoryScope): Promise<Map<string, { row: MemoryRow, relevance: number }>> {
142
- const hits = new Map<string, { row: MemoryRow, relevance: number }>()
175
+ const rrf = new Map<string, { row: MemoryRow, score: number }>()
176
+ const addRank = (row: MemoryRow, rank: number): void => {
177
+ if (!this.inScope(row, scope)) return
178
+ const inc = 1 / (RRF_K + rank + 1)
179
+ const cur = rrf.get(row.id)
180
+ if (cur) cur.score += inc
181
+ else rrf.set(row.id, { row, score: inc })
182
+ }
143
183
  const match = buildMatchQuery(query)
144
184
  if (match) {
145
- const found = this.repo.search(this.opts.agentId, match, MAX_TERMS)
146
- const first = found[0]
147
- const best = first ? Math.max(-first.bm25, 0.001) : 1
148
- // 弱命中保留按"过滤后保留数"计数(非原始下标):
149
- // 跨 channel 的 team 残留行会占据排序下标,若按下标保留会把本 channel 有效弱命中挤出门外
150
- let kept = 0
151
- for (const row of found) {
152
- if (!this.inScope(row, scope)) continue
153
- const rel = Math.min(1, -row.bm25 / best)
154
- if (rel >= 0.1 || kept < WEAK_HIT_KEEP) {
155
- hits.set(row.id, { row, relevance: rel })
156
- kept += 1
157
- }
158
- }
185
+ const found = this.repo.search(this.opts.agentId, match, FTS_POOL)
186
+ found.forEach((row, i) => addRank(row, i))
159
187
  }
160
- // 向量分支:查询向量 + 本人/team 双域 kNN,与 FTS 按 id 融合(rel 取 max;失败退化纯 FTS)
188
+ // 向量分支:查询向量 + 本人/team 双域 kNN,按距离名次参与 RRF(失败退化纯 FTS)
161
189
  if (this.embedder) {
162
190
  try {
163
191
  const [qv] = await this.embedder.embed([query])
@@ -171,85 +199,157 @@ export class AgentMemory {
171
199
  : [this.opts.agentId, TEAM_AGENT_ID]
172
200
  const vAll = ownDomains.flatMap(d =>
173
201
  this.repo.vecSearch(d, qv, d === TEAM_AGENT_ID ? 5 : 10))
174
- // rowid → 最小距离(agent/team 分区不相交;防御性取 min)
202
+ // rowid → 最小距离(agent/team 分区不相交;防御性取 min),保持距离序作名次
175
203
  const distByRowid = new Map<number, number>()
176
204
  for (const { memRowid, distance } of vAll) {
177
205
  const prev = distByRowid.get(memRowid)
178
206
  if (prev === undefined || distance < prev) distByRowid.set(memRowid, distance)
179
207
  }
180
- for (const row of this.repo.listByRowids([...distByRowid.keys()])) {
181
- // 所有权守卫 + channel 隔离 + scope 过滤(脏 rowid/rowid 复用反查到他人行一律丢弃)
182
- if (!this.inScope(row, scope)) continue
183
- if (row.agentId !== this.opts.agentId && row.agentId !== TEAM_AGENT_ID) continue
184
- const sim = Math.min(1, Math.max(0, 1 - (distByRowid.get(row.rowid) ?? 1)))
185
- const prev = hits.get(row.id)
186
- hits.set(row.id, { row, relevance: prev ? Math.max(prev.relevance, sim) : sim })
187
- }
208
+ const rowids = [...distByRowid.entries()].sort((a, b) => a[1] - b[1]).map(([rid]) => rid)
209
+ this.repo.listByRowids(rowids).forEach((row, i) => {
210
+ // 所有权守卫 + channel 隔离(脏 rowid/rowid 复用反查到他人行一律丢弃)
211
+ if (row.agentId !== this.opts.agentId && row.agentId !== TEAM_AGENT_ID) return
212
+ addRank(row, i)
213
+ })
188
214
  }
189
215
  }
190
216
  catch { /* 向量不可用退化为 FTS */ }
191
217
  }
192
- // 时近兜底仅对含私有域的 scope 生效(shared 域无"本人最近"语义)
218
+ // 时近兜底仅对含私有域的 scope 生效(shared 域无"本人最近"语义);
219
+ // 兜底行不参与 RRF,固定弱相关度(策展层行排除——brief 恒 L0 注入,不占引子)
220
+ const hits = new Map<string, { row: MemoryRow, relevance: number }>()
221
+ let best = 0
222
+ for (const { score } of rrf.values()) {
223
+ if (score > best) best = score
224
+ }
225
+ for (const { row, score } of rrf.values()) {
226
+ hits.set(row.id, { row, relevance: best > 0 ? score / best : 0 })
227
+ }
193
228
  if (scope !== 'shared') {
194
229
  for (const row of this.repo.listRecent(this.opts.agentId, RECENT_FALLBACK)) {
230
+ if (CURATED_KINDS.has(row.kind)) continue
195
231
  if (!hits.has(row.id)) hits.set(row.id, { row, relevance: 0.15 })
196
232
  }
197
233
  }
198
234
  return hits
199
235
  }
200
236
 
201
- /** 排序(综合分降序;不含预算/touch 副作用) */
202
- private async rank(query: string, scope: MemoryScope): Promise<Array<{ row: MemoryRow, relevance: number, score: number }>> {
237
+ /** 排序(综合分降序;策展层排除/任务关联加权;不含预算/touch 副作用) */
238
+ private async rank(
239
+ query: string,
240
+ scope: MemoryScope,
241
+ opts: { excludeCurated?: boolean, relatedTaskIds?: string[] } = {},
242
+ ): Promise<Array<{ row: MemoryRow, relevance: number, score: number }>> {
203
243
  const hits = await this.collectHits(query, scope)
244
+ const related = opts.relatedTaskIds && opts.relatedTaskIds.length > 0 ? new Set(opts.relatedTaskIds) : null
204
245
  return [...hits.values()]
205
- .map(h => ({ ...h, score: this.score(h.row, h.relevance) }))
246
+ .filter(h => !opts.excludeCurated || !CURATED_KINDS.has(h.row.kind))
247
+ .map((h) => {
248
+ let score = this.score(h.row, h.relevance)
249
+ if (related && h.row.taskId && related.has(h.row.taskId)) {
250
+ score = Math.min(1, score + RELATED_TASK_BOOST)
251
+ }
252
+ return { ...h, score }
253
+ })
206
254
  .sort((a, b) => b.score - a.score)
207
255
  }
208
256
 
209
257
  /**
210
- * run/supervise 前的"记忆引子"注入:混合检索+排序+小预算装配 → 记忆块。
211
- * 默认 AW_MEMORY_PRIMER_TOKENS(300)小预算,只给最相关的少量线索;
212
- * 完整内容由 Agent 运行时经 search_memory 工具按需抓取(防全量注入污染上下文)。
258
+ * run/supervise 前的记忆注入:三层装配(硬顶 AW_MEMORY_INJECT_TOTAL)。
259
+ * - L0 会话简报(brief 行,确定性置顶,不占检索);
260
+ * - L1 相关性引子(RRF 混合检索 + MMR 多样性 + 任务关联加权,小预算);
261
+ * - 完整内容由 Agent 经 search_memory 工具按需抓取(L2)。
262
+ * 预算内装配;使用提示计费(防提示挤占正文预算)。
213
263
  */
214
264
  async recall(query: string, recallOpts: RecallOptions = {}): Promise<string | null> {
215
265
  const doTouch = recallOpts.touch !== false
216
- const budget = recallOpts.budgetTokens ?? this.opts.budgetTokens ?? envNum('AW_MEMORY_PRIMER_TOKENS', 300)
266
+ // 显式 budgetTokens(实例/调用级)= 整块记忆硬顶(L0 简报+L1 引子+提示全计入);
267
+ // 未显式设置 = 双预算:L1 引子 ≤ AW_MEMORY_PRIMER_TOKENS(300),整块 ≤ AW_MEMORY_INJECT_TOTAL(500)
268
+ const explicit = recallOpts.budgetTokens ?? this.opts.budgetTokens
269
+ const budget = explicit ?? envNum('AW_MEMORY_PRIMER_TOKENS', 300)
270
+ const totalBudget = explicit ?? envNum('AW_MEMORY_INJECT_TOTAL', 500)
217
271
  const scope = recallOpts.scope ?? 'auto'
218
- const scored = await this.rank(query, scope)
219
- if (scored.length === 0) return null
272
+ const scored = await this.rank(query, scope, { excludeCurated: true, relatedTaskIds: recallOpts.relatedTaskIds })
220
273
 
221
274
  const lines: string[] = []
222
275
  const touched: string[] = []
223
- let used = 0
224
- for (const s of scored) {
225
- const line = this.formatLine(s.row)
276
+ let usedTotal = 0
277
+ // L0 会话简报置顶(幂等单行;确定性注入,不依赖检索命中;计费走总预算,不挤占 L1 引子预算)
278
+ const briefRef = this.repo.findByAgentDedup(this.opts.channelId, this.opts.agentId, `brief:${this.opts.agentId}`)
279
+ const briefRow = briefRef ? this.repo.getById(briefRef.id) : null
280
+ if (briefRow) {
281
+ const briefText = `## 会话简报(自动维护)\n${unsegmentCJK(briefRow.content)}`
282
+ const briefCost = estimateTokens(briefText)
283
+ if (usedTotal + briefCost <= totalBudget) {
284
+ usedTotal += briefCost
285
+ lines.push(briefText)
286
+ }
287
+ }
288
+ // 使用提示预留计费:正文与提示共享预算,提示永不超卖
289
+ const tipsText = [
290
+ `记忆使用提示:`,
291
+ `- 以上只是线索级摘要。开工前先判断是否需要更多上下文:历史结论/团队约定/相似任务经验 → 调 search_memory(query, scope) 主动检索(覆盖私有+共享域)。`,
292
+ `- 作业中发现可复用的结论、约定、教训 → 立即 save_memory 沉淀:scope=private 存个人经验,scope=shared 发布到 Channel 公共记忆供全员复用。`,
293
+ ].join('\n')
294
+ const tipsCost = estimateTokens(tipsText)
295
+ // MMR 装配:逐条贪心选 λ×相关性 − (1−λ)×与已选集最大相似 的最大者(多样性,防近重复挤占)
296
+ const candidates = scored.slice()
297
+ const tokenSet = (row: MemoryRow): Set<string> =>
298
+ new Set(unsegmentCJK(`${row.title} ${row.content.slice(0, 200)}`).toLowerCase().split(/\s+/).filter(Boolean))
299
+ const selectedTokens: Set<string>[] = []
300
+ let usedPrimer = 0
301
+ while (candidates.length > 0 && lines.length < 12) {
302
+ let bestIdx = -1
303
+ let bestGain = -Infinity
304
+ let bestTokens: Set<string> | null = null
305
+ for (let i = 0; i < candidates.length; i++) {
306
+ const c = candidates[i]!
307
+ const toks = tokenSet(c.row)
308
+ let maxSim = 0
309
+ for (const st of selectedTokens) {
310
+ if (st.size === 0 || toks.size === 0) continue
311
+ let inter = 0
312
+ for (const t of toks) if (st.has(t)) inter++
313
+ const sim = inter / (st.size + toks.size - inter)
314
+ if (sim > maxSim) maxSim = sim
315
+ }
316
+ const gain = MMR_LAMBDA * c.score - (1 - MMR_LAMBDA) * maxSim
317
+ if (gain > bestGain) {
318
+ bestGain = gain
319
+ bestIdx = i
320
+ bestTokens = toks
321
+ }
322
+ }
323
+ if (bestIdx < 0) break
324
+ const [c] = candidates.splice(bestIdx, 1)
325
+ const line = this.formatLine(c!.row)
226
326
  const cost = estimateTokens(line)
227
- if (used + cost > budget) continue
228
- used += cost
327
+ // 双预算:L1 引子预算(budget)+ 注入总预算(total,含 brief);超预算跳过但继续找更短行
328
+ if (usedPrimer + tipsCost + cost > budget || usedTotal + cost + tipsCost > totalBudget) continue
329
+ usedPrimer += cost
330
+ usedTotal += cost
229
331
  lines.push(line)
230
- if (doTouch) touched.push(s.row.id)
332
+ selectedTokens.push(bestTokens!)
333
+ if (doTouch) touched.push(c!.row.id)
231
334
  }
232
335
  if (lines.length === 0) return null
233
336
  for (const id of touched) this.repo.touch(id)
234
337
  return [
235
338
  `## 相关记忆(自动召回的高相关/最近线索摘要;与当前任务冲突时,以当前任务为准)`,
236
339
  ...lines,
237
- [
238
- `记忆使用提示:`,
239
- `- 以上只是线索级摘要。开工前先判断是否需要更多上下文:历史结论/团队约定/相似任务经验 → 调 search_memory(query, scope) 主动检索(覆盖私有+共享域)。`,
240
- `- 作业中发现可复用的结论、约定、教训 → 立即 save_memory 沉淀:scope=private 存个人经验,scope=shared 发布到 Channel 公共记忆供全员复用。`,
241
- ].join('\n'),
340
+ tipsText,
242
341
  ].join('\n')
243
342
  }
244
343
 
245
344
  /**
246
345
  * 工具按需抓取(search_memory):结构化片段返回,content 还原为未切分原文。
247
- * 触发 touch(access_count 强化后续召回排序);limit 上限防御。
346
+ * 触发 touch(access_count 强化后续召回排序);limit 上限防御;
347
+ * relatedTaskIds 透传任务关联加权(与 recall 同源排序)。
248
348
  */
249
- async recallRows(query: string, opts: { scope?: MemoryScope, limit?: number } = {}): Promise<MemorySnippet[]> {
349
+ async recallRows(query: string, opts: { scope?: MemoryScope, limit?: number, relatedTaskIds?: string[] } = {}): Promise<MemorySnippet[]> {
250
350
  const scope = opts.scope ?? 'auto'
251
351
  const limit = Math.min(Math.max(opts.limit ?? 5, 1), 20)
252
- const scored = (await this.rank(query, scope)).slice(0, limit)
352
+ const scored = (await this.rank(query, scope, { relatedTaskIds: opts.relatedTaskIds })).slice(0, limit)
253
353
  for (const s of scored) this.repo.touch(s.row.id)
254
354
  return scored.map(s => ({
255
355
  id: s.row.id,
@@ -286,10 +386,12 @@ export class AgentMemory {
286
386
  dedupKey,
287
387
  })
288
388
  await vectorizeMemory(this.repo, this.embedder, this.opts.channelId, owner, dedupKey, input.content)
389
+ // 共享约定进入简报"团队共享约定"行
390
+ if (shared) await this.updateBrief()
289
391
  return { scope: input.scope, dedupKey }
290
392
  }
291
393
 
292
- /** run 后(任务路径;仅终态调用):harvest TaskEngine 终态 + deliverable */
394
+ /** run 后(任务路径;仅终态调用):harvest TaskEngine 终态 + deliverable,并刷新 L0 简报 */
293
395
  async recordTaskOutcome(task: WorkspaceTask): Promise<void> {
294
396
  // 优先 deliverable/summary 命名成果;harness 任意命名(如 mock 的 result)兜底取全部成果
295
397
  const preferred = task.artifacts.filter(a => a.name === 'deliverable' || a.name === 'summary')
@@ -312,6 +414,7 @@ export class AgentMemory {
312
414
  dedupKey: `task:${task.id}`,
313
415
  })
314
416
  await this.vectorize(content, `task:${task.id}`)
417
+ await this.updateBrief()
315
418
  }
316
419
 
317
420
  /** run 后(点对点路径):请求 + 我方回复摘要 */
@@ -339,19 +442,141 @@ export class AgentMemory {
339
442
  await vectorizeMemory(this.repo, this.embedder, this.opts.channelId, this.opts.agentId, dedupKey, plainContent)
340
443
  }
341
444
 
445
+ // ===== 上下文治理/团队历史(P0-3 新增写入面;全部幂等 upsert)=====
446
+
447
+ /** 会话压缩摘要序号(每实例自增;dedupKey 防同幂键覆盖) */
448
+ private sessionSeq = 0
449
+
450
+ /**
451
+ * 压缩摘要入库(compaction harvest):kind=episodic-session,14d 保鲜,向量化一次。
452
+ * 三路统一入口(平台主动 compact / omp 阈值自动 / overflow),dedupKey 含序号防互踩。
453
+ */
454
+ async recordSessionCompaction(input: { summary: string, tokensBefore?: number, tokensAfter?: number, reason?: string }): Promise<{ dedupKey: string }> {
455
+ this.sessionSeq += 1
456
+ const dedupKey = `session:${this.opts.agentId}:c${this.sessionSeq}`
457
+ const meta: string[] = []
458
+ if (typeof input.tokensBefore === 'number') meta.push(`压缩前 ${input.tokensBefore} tok`)
459
+ if (typeof input.tokensAfter === 'number') meta.push(`压缩后约 ${input.tokensAfter} tok`)
460
+ if (input.reason) meta.push(`触发=${input.reason}`)
461
+ const head = meta.length > 0 ? `[会话压缩摘要 · ${meta.join(' · ')}]\n` : '[会话压缩摘要]\n'
462
+ this.repo.upsert({
463
+ channelId: this.opts.channelId,
464
+ agentId: this.opts.agentId,
465
+ kind: 'episodic-session',
466
+ title: `会话压缩摘要 #${this.sessionSeq}`,
467
+ titleFts: segmentCJK('会话压缩摘要'),
468
+ content: segmentCJK(`${head}${input.summary}`).slice(0, CONTENT_STORE_LIMIT),
469
+ importance: 0.75,
470
+ taskId: null,
471
+ dedupKey,
472
+ })
473
+ await this.vectorize(input.summary, dedupKey)
474
+ return { dedupKey }
475
+ }
476
+
477
+ /**
478
+ * L0 会话简报刷新(零 LLM 模板拼装;brief:<agentId> 幂等单行;免向量化——
479
+ * 确定性层恒注入,无需被检索;FTS 仍可命中)。数据源=本人已有记忆行,零额外查询依赖。
480
+ */
481
+ async updateBrief(): Promise<void> {
482
+ const own = this.repo.listByAgentChannel(this.opts.channelId, this.opts.agentId, 40)
483
+ .filter(r => r.kind === 'episodic-task')
484
+ const shared = this.repo.listByAgentChannel(this.opts.channelId, TEAM_AGENT_ID, 40)
485
+ .filter(r => r.kind === 'semantic')
486
+ const lines: string[] = []
487
+ const done = own.filter(r => r.importance >= 0.75).slice(0, 3)
488
+ if (done.length > 0) {
489
+ lines.push(`最近完成:`)
490
+ done.forEach((r, i) => {
491
+ lines.push(`${i + 1}) ${r.title} — ${unsegmentCJK(r.content).slice(0, 30)}`)
492
+ })
493
+ }
494
+ if (shared.length > 0) {
495
+ lines.push(`团队共享约定:${shared.slice(0, 2).map(r => r.title).join(';')}`)
496
+ }
497
+ if (lines.length === 0) return
498
+ this.repo.upsert({
499
+ channelId: this.opts.channelId,
500
+ agentId: this.opts.agentId,
501
+ kind: 'brief',
502
+ title: '会话简报',
503
+ titleFts: segmentCJK('会话简报'),
504
+ content: segmentCJK(lines.join('\n')),
505
+ importance: 0.6,
506
+ taskId: null,
507
+ dedupKey: `brief:${this.opts.agentId}`,
508
+ })
509
+ }
510
+
511
+ /**
512
+ * 团队任务成果入共享域(挂 channel task 终态事件;team-task:<taskId> 幂等;向量化)。
513
+ * 全员 search_memory(scope=shared) 即可命中"谁做完了什么/为什么失败"。
514
+ */
515
+ async appendTeamTaskRecord(input: { taskId: string, title: string, content: string, importance: number }): Promise<void> {
516
+ const dedupKey = `team-task:${input.taskId}`
517
+ this.repo.upsert({
518
+ channelId: this.opts.channelId,
519
+ agentId: TEAM_AGENT_ID,
520
+ kind: 'episodic-team-task',
521
+ title: input.title,
522
+ titleFts: segmentCJK(input.title),
523
+ content: segmentCJK(input.content).slice(0, CONTENT_STORE_LIMIT),
524
+ importance: input.importance,
525
+ taskId: input.taskId,
526
+ dedupKey,
527
+ })
528
+ await vectorizeMemory(this.repo, this.embedder, this.opts.channelId, TEAM_AGENT_ID, dedupKey, input.content)
529
+ }
530
+
531
+ /** 团队编年史滚动重写(chronicle:<channelId> 幂等单行;免向量化;策展层豁免维护) */
532
+ upsertChronicle(entriesText: string): void {
533
+ this.repo.upsert({
534
+ channelId: this.opts.channelId,
535
+ agentId: TEAM_AGENT_ID,
536
+ kind: 'chronicle',
537
+ title: '团队编年史',
538
+ titleFts: segmentCJK('团队编年史'),
539
+ content: segmentCJK(entriesText).slice(0, 1500),
540
+ importance: 0.9,
541
+ taskId: null,
542
+ dedupKey: `chronicle:${this.opts.channelId}`,
543
+ })
544
+ }
545
+
546
+ /** 空闲反思行(reflection:<agentId>:<month> 幂等单行;免向量化;策展层豁免维护) */
547
+ upsertReflection(input: { month: string, content: string }): void {
548
+ this.repo.upsert({
549
+ channelId: this.opts.channelId,
550
+ agentId: this.opts.agentId,
551
+ kind: 'reflection',
552
+ title: `作业反思 ${input.month}`,
553
+ titleFts: segmentCJK(`作业反思 ${input.month}`),
554
+ content: segmentCJK(input.content).slice(0, CONTENT_STORE_LIMIT),
555
+ importance: 0.7,
556
+ taskId: null,
557
+ dedupKey: `reflection:${this.opts.agentId}:${input.month}`,
558
+ })
559
+ }
560
+
561
+ /** kind 感知综合分:时近衰减按 kind 半衰期(知识/策展层不随时间贬值) */
342
562
  private score(row: MemoryRow, relevance: number): number {
563
+ const halfLife = RECENCY_HALF_LIFE_DAYS[row.kind] ?? 7
343
564
  const ageDays = (Date.now() - Date.parse(row.createdAt)) / 86_400_000
344
- const recency = Math.exp(-ageDays / RECENCY_DAYS)
565
+ const recency = halfLife <= 0 ? 1 : Math.exp(-ageDays / halfLife)
345
566
  const importance = Math.min(1, row.importance + row.accessCount * 0.05)
346
567
  return W_RELEVANCE * relevance + W_RECENCY * recency + W_IMPORTANCE * importance
347
568
  }
348
569
 
349
570
  private formatLine(row: MemoryRow): string {
350
- const tag = row.kind === 'episodic-task'
351
- ? '任务'
352
- : row.kind === 'episodic-peer'
353
- ? '协作'
354
- : '共享'
571
+ const tag = ({
572
+ 'episodic-task': '任务',
573
+ 'episodic-team-task': '团队任务',
574
+ 'episodic-peer': '协作',
575
+ 'episodic-session': '会话',
576
+ 'brief': '简报',
577
+ 'chronicle': '编年史',
578
+ 'reflection': '反思',
579
+ } as Record<string, string>)[row.kind] ?? '共享'
355
580
  const content = unsegmentCJK(row.content)
356
581
  const short = content.length > 240 ? `${content.slice(0, 240)}…` : content
357
582
  return `- [${humanAgo(row.createdAt)}·${tag}] ${row.title}:${short}`
@@ -389,36 +614,37 @@ export interface MaintenanceResult {
389
614
  }
390
615
 
391
616
  /**
392
- * 记忆维护:① episodic 过期删除(now - (lastAccessedAt ?? createdAt) > expireDays)
393
- * ② 每 agent 容量淘汰(仅 episodic;effectiveScore = importance + accessCount×0.05 降序保留)
394
- * ③ 孤儿 vec 清理。semantic 与 team 哨兵行(人工策展)全程豁免。
617
+ * 记忆维护:① episodic 过期删除(now - (lastAccessedAt ?? createdAt) > 过期天数;
618
+ * kind 分离:episodic-session 14d / 其余 episodic 180d)
619
+ * ② 每 agent 容量淘汰(仅 episodic 族;effectiveScore = importance + accessCount×0.05 降序保留)
620
+ * ③ team 域行:episodic-team-task 同样过期+淘汰(防共享域膨胀);chronicle/semantic 策展行豁免
621
+ * ④ 孤儿 vec 清理。semantic / brief / chronicle / reflection 全程豁免(单行幂等自限)。
395
622
  */
396
623
  export function runMemoryMaintenance(
397
624
  repo: MemoryRepo,
398
625
  opts: { expireDays?: number, cap?: number } = {},
399
626
  ): MaintenanceResult {
400
627
  const expireDays = opts.expireDays ?? envNum('AW_MEMORY_EXPIRE_DAYS', 180)
628
+ const sessionExpireDays = envNum('AW_MEMORY_EXPIRE_SESSION_DAYS', 14)
401
629
  const cap = opts.cap ?? envNum('AW_MEMORY_CAP', 500)
402
- const expireMs = expireDays * 86_400_000
403
630
  const now = Date.now()
404
631
  let deletedExpired = 0
405
632
  let evicted = 0
406
633
 
407
- for (const agentId of repo.listMemoryAgentIds()) {
408
- if (agentId === TEAM_AGENT_ID) continue // 双保险:team 行人工策展,永不衰减
409
- const rows = repo.listByAgentWithRowid(agentId, 1_000_000)
410
- // ① 过期删除(仅 episodic)
634
+ /** 过期天数按 kind 分离(session 摘要短保鲜) */
635
+ const expireMsFor = (kind: string): number =>
636
+ (kind === 'episodic-session' ? sessionExpireDays : expireDays) * 86_400_000
637
+
638
+ /** 对一组行执行 ①过期 ②容量淘汰(仅传入选定行;调用方负责 kind 圈定) */
639
+ const expireAndEvict = (rows: Array<MemoryRow & { rowid: number }>): void => {
411
640
  for (const r of rows) {
412
- if (!r.kind.startsWith('episodic')) continue
413
- if (now - Date.parse(r.lastAccessedAt ?? r.createdAt) > expireMs) {
641
+ if (now - Date.parse(r.lastAccessedAt ?? r.createdAt) > expireMsFor(r.kind)) {
414
642
  repo.vecDelete(r.rowid)
415
643
  repo.delete(r.id)
416
644
  deletedExpired++
417
645
  }
418
646
  }
419
- // ② 容量淘汰(仅 episodic;vec 已在上面过期路径删,此处剩余重查)
420
- const remaining = repo.listByAgentWithRowid(agentId, 1_000_000)
421
- .filter(r => r.kind.startsWith('episodic'))
647
+ const remaining = rows.filter(r => repo.getById(r.id) !== null)
422
648
  if (remaining.length > cap) {
423
649
  const sorted = remaining
424
650
  .map(r => ({ r, s: r.importance + r.accessCount * 0.05 }))
@@ -430,6 +656,15 @@ export function runMemoryMaintenance(
430
656
  }
431
657
  }
432
658
  }
659
+
660
+ // 成员私有域(排除 team)
661
+ for (const agentId of repo.listMemoryAgentIds()) {
662
+ expireAndEvict(repo.listByAgentWithRowid(agentId, 1_000_000)
663
+ .filter(r => r.kind.startsWith('episodic')))
664
+ }
665
+ // team 共享域:仅 episodic-team-task 参与过期/淘汰(chronicle/semantic 策展行豁免)
666
+ expireAndEvict(repo.listByAgentWithRowid(TEAM_AGENT_ID, 1_000_000)
667
+ .filter(r => r.kind === 'episodic-team-task'))
433
668
  let cleanedVec = 0
434
669
  try {
435
670
  cleanedVec = repo.vecCleanOrphans()
@@ -65,6 +65,18 @@ export interface AgentTaskQueueView {
65
65
  completed: WorkspaceTask[]
66
66
  }
67
67
 
68
+ /** harness 上下文用量快照(omp harness 有;进程内 harness 无 → 字段缺省) */
69
+ export interface AgentContextStats {
70
+ /** 最近已知上下文 tokens(≈ prompt 规模) */
71
+ usedTokens: number
72
+ /** 模型上下文窗口(未知为 null,percent 同为 null) */
73
+ contextWindow: number | null
74
+ /** 占窗口比例 0-1(窗口未知为 null) */
75
+ percent: number | null
76
+ /** harness 正在压缩会话 */
77
+ compacting: boolean
78
+ }
79
+
68
80
  /** Agent 实时状态视图(状态管理机制:idle/busy/stopped + 队列上下文) */
69
81
  export interface AgentStatusView {
70
82
  agentId: string
@@ -82,4 +94,6 @@ export interface AgentStatusView {
82
94
  queuedCount: number
83
95
  /** 已完成任务数 */
84
96
  completedCount: number
97
+ /** harness 上下文用量(omp 有;进程内 harness/未知时缺省) */
98
+ context?: AgentContextStats
85
99
  }
@@ -1 +0,0 @@
1
- {"id":"f2f649c5-fd3b-4507-a3a1-8b4a194de72d","timestamp":1788345881592,"prerendered":[]}