@a9i5k4/dsh-auto-memory 2.2.2 → 2.2.4
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.
- package/docs/A3-RISK-ASSESSMENT-20260830.md +61 -0
- package/docs/COT-WATCH-RFC.md +88 -0
- package/docs/DUAL-TIER-RATIFICATION-PROMPT.md +71 -0
- package/docs/HANDOFF-HARNESS.md +78 -0
- package/docs/HANDOFF-M8-M9-M10.md +203 -0
- package/docs/HY4-TOUR-LOGO-HANDOFF-2.md +60 -0
- package/docs/HY4-TOUR-LOGO-HANDOFF.md +100 -0
- package/docs/ISSUE-REPLY-UNATTENDED.md +31 -0
- package/docs/K3-LANDING-HANDOFF.md +76 -0
- package/docs/LANDING-OUTLINE.md +85 -0
- package/docs/M-CM-PLAN.md +166 -0
- package/docs/M-CM-STATE.md +64 -0
- package/docs/M3B-CONTRACT.md +461 -0
- package/docs/M4-CONTRACT.md +1207 -0
- package/docs/M5-CONTRACT.md +383 -0
- package/docs/M6-CONTRACT.md +342 -0
- package/docs/M7-ACTIVATION-ALGO-REFERENCES.md +104 -0
- package/docs/M7-ACTIVATION-CALIBRATION.md +144 -0
- package/docs/M7-ACTIVATION-FEATURE-AGENT-PROMPT.md +79 -0
- package/docs/M7-ACTIVATION-FEATURE-CALIBRATION.md +58 -0
- package/docs/M7-ACTIVATION-FEATURE-DESIGN.md +124 -0
- package/docs/M7-ACTIVATION-V2-CONTROLLED-SHADOW.md +127 -0
- package/docs/M7-ACTIVATION-V2-HANDOFF.md +132 -0
- package/docs/M7-ACTIVATION-V2-HOLDEDOUT-EVAL.md +94 -0
- package/docs/M7-ACTIVATION-V2-HOLDEDOUT-SHADOW.md +60 -0
- package/docs/M7-ACTIVATION-V2-LIVE-SHADOW-PLAN.md +85 -0
- package/docs/M7-ACTIVATION-V2-PAPER.md +303 -0
- package/docs/M7-AGENT-HANDOFF-PROMPT.md +63 -0
- package/docs/M7-ALGORITHM-DECISION.md +174 -0
- package/docs/M7-AUTONOMOUS-STATE.md +252 -0
- package/docs/M7-BENCHMARK-PLAN.md +78 -0
- package/docs/M7-CLOSED-LOOP-WIRING.md +110 -0
- package/docs/M7-EMBEDDING-BENCHMARK.md +164 -0
- package/docs/M7-INTERFACE-DIGEST.md +144 -0
- package/docs/M7-LABEL-REVIEW-REPORT.md +107 -0
- package/docs/M7-LEXICAL-TUNING.md +58 -0
- package/docs/M7-LIVE-SHADOW-SCRIPT.md +30 -0
- package/docs/M7-PYTHON-IMPLEMENTATION-REPORT.md +92 -0
- package/docs/M7-RESEARCH-PAPER.md +442 -0
- package/docs/M7-TASKSET-DISPATCH.md +213 -0
- package/docs/M8-MEMORY-HUB.md +105 -0
- package/docs/MEMORY-SYSTEMS-SURVEY-2026-09.md +166 -0
- package/docs/NEXT-MAJOR-PROMO.md +363 -0
- package/docs/NEXT-MAJOR-README-DRAFT.zh.md +205 -0
- package/docs/NEXT-MAJOR-VISION.md +112 -0
- package/docs/PREVIEW-NEXT-STEPS.md +229 -0
- package/docs/PROJECT-FREEZE-AND-ROADMAP.md +200 -0
- package/docs/PROMO-STYLE-GUIDE.md +84 -0
- package/docs/PYTHON-SIDECAR-CONTRACT.md +539 -0
- package/docs/R2-POLICY-PLUMBING-BLUEPRINT.md +99 -0
- package/docs/RELEASE-READINESS-PLAN.md +91 -0
- package/docs/RELEASE-SEMANTIC-OPTION.md +181 -0
- package/docs/S1-SCIENTIFIC-RIGOR.md +269 -0
- package/docs/S2-DEEP-ABSORPTION.md +259 -0
- package/docs/S3-TARGET-ARCHITECTURE.md +172 -0
- package/docs/USER-GUIDE.zh-CN.md +283 -0
- package/docs/banner.jpg +0 -0
- package/docs/implementation-handoff-context.zh-CN.md +429 -0
- package/docs/landing/index.html +1745 -0
- package/docs/paper-figures/fig1_model_quality.png +0 -0
- package/docs/paper-figures/fig2_chunk_reversal.png +0 -0
- package/docs/paper-figures/fig3_latency.png +0 -0
- package/docs/paper-figures/fig4_hybrid.png +0 -0
- package/docs/paper-figures/fig5_rerank_tradeoff.png +0 -0
- package/docs/paper-figures/fig6_cluster_sweep.png +0 -0
- package/docs/paper-figures/fig7_resource.png +0 -0
- package/docs/paper-figures-v2/fig1_echo_trap.png +0 -0
- package/docs/paper-figures-v2/fig2_pr_paths.png +0 -0
- package/docs/paper-figures-v2/fig3_coefficients.png +0 -0
- package/docs/paper-figures-v2/fig4_calibration.png +0 -0
- package/docs/paper-figures-v2/fig5_order_ablation.png +0 -0
- package/docs/paper-figures-v2/fig6_containment.png +0 -0
- package/docs/proactive-associative-memory-architecture.html +659 -0
- package/docs/proactive-associative-memory-meta-code.html +1058 -0
- package/docs/proactive-associative-memory-research-report.zh-CN.md +685 -0
- package/docs/proactive-associative-memory-system-map.html +1580 -0
- package/docs/promo/first-run-guide.html +397 -0
- package/docs/promo/homepage.html +384 -0
- package/docs/screenshots/calendar-en.png +0 -0
- package/docs/screenshots/calendar-zh.png +0 -0
- package/docs/screenshots/connect-en.png +0 -0
- package/docs/screenshots/connect-zh.png +0 -0
- package/docs/screenshots/main-connect-en.png +0 -0
- package/docs/screenshots/main-connect-zh.png +0 -0
- package/docs/screenshots/overview-en.png +0 -0
- package/docs/screenshots/overview-zh.png +0 -0
- package/docs/screenshots/panel-hub.png +0 -0
- package/docs/screenshots/panel-overview.png +0 -0
- package/docs/screenshots/panel-refine.png +0 -0
- package/docs/screenshots/promo/promo-0-banner-v2.png +0 -0
- package/docs/screenshots/promo/promo-1-hero.png +0 -0
- package/docs/screenshots/promo/promo-2-tour.png +0 -0
- package/docs/screenshots/promo/promo-3-recall.png +0 -0
- package/docs/screenshots/promo/promo-4-unattended.png +0 -0
- package/docs/screenshots/promo/promo-5-external.png +0 -0
- package/docs/screenshots/promo/promo-6-greeting.png +0 -0
- package/docs/screenshots/reflections-en.png +0 -0
- package/docs/screenshots/search-zh.png +0 -0
- package/docs/screenshots/settings-2-zh.png +0 -0
- package/docs/screenshots/settings-debug-zh.png +0 -0
- package/docs/screenshots/settings-en.png +0 -0
- package/docs/screenshots/settings-zh.png +0 -0
- package/docs/screenshots/tour-core.png +0 -0
- package/docs/screenshots/tour-external.png +0 -0
- package/docs/screenshots/tour-toggles.png +0 -0
- package/docs/screenshots/tour-welcome.png +0 -0
- package/docs/screenshots/workspace-map-zh.png +0 -0
- package/docs/social-preview.png +0 -0
- package/lib/client.js +277 -118
- package/lib/index.js +329 -60
- package/lib/subagent-gc.js +239 -0
- package/lib/water-window.js +88 -0
- package/package.json +2 -1
package/lib/index.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* - 路由:/api/dsh-auto-memory/{state,list,file,recall,config,reflect}(loopback-only)
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { readFile, writeFile, mkdir, readdir, stat, rm, copyFile, appendFile } from 'node:fs/promises'
|
|
20
|
+
import { readFile, writeFile, mkdir, readdir, stat, rm, copyFile, appendFile, rename } from 'node:fs/promises'
|
|
21
21
|
import { createReadStream, existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, rmSync, statSync } from 'node:fs'
|
|
22
22
|
import { exec as cpExec } from 'node:child_process'
|
|
23
23
|
import { promisify } from 'node:util'
|
|
@@ -43,6 +43,8 @@ import { createMemoryHubPre } from './memory-hub.js'
|
|
|
43
43
|
import { pickConsolidationTextPre } from './intent-clean.js'
|
|
44
44
|
import { createStorageManagerPre } from './storage-manage.js'
|
|
45
45
|
import { rankWorkspacesByMemoryRecencyPre } from './ws-overview-rank.js'
|
|
46
|
+
import { scanPluginSubagentSessions, recycleSessions, PLUGIN_LABEL_PREFIX } from './subagent-gc.js'
|
|
47
|
+
import { parseModelWindowsPre, pickWindowPre, findOfficialContextWindowPre } from './water-window.js'
|
|
46
48
|
import { buildSourceCatalog, CorpusRegistry, canonicalize } from './m4-corpus.js'
|
|
47
49
|
import * as nodeZlib from 'node:zlib'
|
|
48
50
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
@@ -159,6 +161,7 @@ export const API = {
|
|
|
159
161
|
'semantic-deep-detect': '/api/dsh-auto-memory/semantic-deep-detect',
|
|
160
162
|
'handoff-state': '/api/dsh-auto-memory/handoff-state',
|
|
161
163
|
'handoff-continue': '/api/dsh-auto-memory/handoff-continue',
|
|
164
|
+
'subagent-gc': '/api/dsh-auto-memory/subagent-gc',
|
|
162
165
|
'semantic-download': '/api/dsh-auto-memory/semantic-download',
|
|
163
166
|
'semantic-emit': '/api/dsh-auto-memory/semantic-emit',
|
|
164
167
|
'shadow-recent': '/api/dsh-auto-memory/shadow-recent',
|
|
@@ -224,6 +227,23 @@ const DEFAULT_CONFIG = {
|
|
|
224
227
|
waterLevelAdvisory: true,
|
|
225
228
|
/** 水位越阈时自动写一篇系统骨架账本(每会话一次;模型仍应自己写正式交接)。 */
|
|
226
229
|
waterLevelAutoHandoff: true,
|
|
230
|
+
/** 自动接续(默认开):水位≥阈值 且 harness 权威 running==false(轮次边界,取代旧"两轮水位持平")时弹确认卡。 */
|
|
231
|
+
autoContinueEnabled: true,
|
|
232
|
+
/** 自动接续水位阈值(0.5-0.95)。 */
|
|
233
|
+
autoContinueThreshold: 0.8,
|
|
234
|
+
/** 确认卡倒计时秒数(30-40s 无操作 = 挂机 → 自动接续兜底)。 */
|
|
235
|
+
autoContinueConfirmSeconds: 35,
|
|
236
|
+
/** 接续前刷新仪式(默认开):先让旧 Agent 刷新 PLAN.md + 交接账本,host 再用最新材料组装交接。 */
|
|
237
|
+
autoContinueRefreshRitual: true,
|
|
238
|
+
/** 刷新仪式等待上限(秒);超时用现有材料 fail-soft 继续接续。 */
|
|
239
|
+
autoContinueRefreshTimeoutSeconds: 90,
|
|
240
|
+
/** 自动接续冷却(分钟):一次接续后多久内不再触发。 */
|
|
241
|
+
autoContinueCooldownMinutes: 30,
|
|
242
|
+
/** 子代理痕迹回收(2026-09-08,默认开):本插件 spawn 的一次性子代理(auto-memory-*)在任务结束后立即把其会话目录与投影缓存移入备份目录。
|
|
243
|
+
* 背景:实测全机 686 个子代理会话中 638 个来自本插件,数量上千后拖慢会话列表加载。 */
|
|
244
|
+
subagentGcEnabled: true,
|
|
245
|
+
/** 子代理痕迹兜底 GC 保留天数:超过该天数仍残留的本插件一次性痕迹会在每日巡检时回收(0=不按时间、只靠任务结束即删)。 */
|
|
246
|
+
subagentGcKeepDays: 3,
|
|
227
247
|
/** 暂离阈值(分钟):距上次活动超过该值视为暂离,回归时自动弹出记忆窗口并欢迎。默认 60。 */
|
|
228
248
|
awayMinutes: 60,
|
|
229
249
|
/** 无人值守/托管模式(默认关):面向无人值守批量任务。开启后剥离所有"会话性/行为性"注入——
|
|
@@ -1630,7 +1650,9 @@ class MemoryEngine {
|
|
|
1630
1650
|
const now = Date.now()
|
|
1631
1651
|
if (this._waterWindowCache && now - this._waterWindowCache.at < 60000) return this._waterWindowCache.value
|
|
1632
1652
|
const text = await this.readTextSafe(path.join(dshHome(), 'settings.yaml'))
|
|
1633
|
-
let value =
|
|
1653
|
+
let value = 0
|
|
1654
|
+
let model = ''
|
|
1655
|
+
let provider = ''
|
|
1634
1656
|
if (text) {
|
|
1635
1657
|
const lines = text.split(/\r?\n/)
|
|
1636
1658
|
const ai = lines.findIndex((l) => /^agent-default-model:/.test(l))
|
|
@@ -1642,23 +1664,11 @@ class MemoryEngine {
|
|
|
1642
1664
|
if (m[1] === 'model') { model = m[2].trim(); break }
|
|
1643
1665
|
}
|
|
1644
1666
|
}
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
let currentId = null
|
|
1649
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1650
|
-
const pm = lines[i].match(/^ {4}([^ #][^:]*): *$/)
|
|
1651
|
-
if (pm) { currentProvider = pm[1]; currentId = null; continue }
|
|
1652
|
-
const im = lines[i].match(/^ *- id: *(.+?) *$/)
|
|
1653
|
-
if (im) { currentId = im[1]; continue }
|
|
1654
|
-
const cm = lines[i].match(/^ +contextWindow: *([0-9]+) *$/)
|
|
1655
|
-
if (cm && currentId) { const route = currentProvider + '/' + currentId; if (windowsByRoute[route] === undefined) windowsByRoute[route] = Number(cm[1]) }
|
|
1656
|
-
}
|
|
1657
|
-
if (windowsByRoute[(provider || '') + '/' + model] !== undefined) value = windowsByRoute[provider + '/' + model]
|
|
1658
|
-
else { for (const route of Object.keys(windowsByRoute)) if (route.endsWith('/' + model)) { value = windowsByRoute[route]; break } }
|
|
1659
|
-
}
|
|
1667
|
+
// 2.2.4 修复:settings.yaml 的 llm-deepseek 段是 flow 风格 YAML(`{ models: [ { id: x, contextWindow: 1000000, … } ] }`),
|
|
1668
|
+
// 旧解析器只认 block 风格 → 整段解析失败、窗口退化为 fallback。改为纯函数解析(block + flow 双支持)。
|
|
1669
|
+
if (model) value = pickWindowPre(parseModelWindowsPre(text), provider, model)
|
|
1660
1670
|
}
|
|
1661
|
-
const result = value
|
|
1671
|
+
const result = value > 0
|
|
1662
1672
|
? { window: value, source: 'auto' + (provider ? ':' + provider : '') + (model ? '/' + model : ''), model: model || '' }
|
|
1663
1673
|
: { window: 131072, source: 'fallback', model: model || '' }
|
|
1664
1674
|
this._waterWindowCache = { at: now, value: result }
|
|
@@ -1675,17 +1685,21 @@ class MemoryEngine {
|
|
|
1675
1685
|
let win = Number(winInfo && winInfo.window) || 0
|
|
1676
1686
|
let winSource = (winInfo && winInfo.source) || ''
|
|
1677
1687
|
if (win > 0 && !/^manual/.test(winSource)) {
|
|
1678
|
-
//
|
|
1688
|
+
// 官方容量:request/context 事件的 contextWindow(路由权威值,last-wins)。
|
|
1689
|
+
// 2.2.4 修复:该事件只在请求头部变化时追加(实测 2999 条事件里仅 2 条,且位于会话开头),
|
|
1690
|
+
// 旧实现只扫最近 256 条 → 长会话永远扫不到,窗口退化为 fallback 131072(水位虚高到 150%+)。
|
|
1691
|
+
// 现在全量向前扫 + 按会话 id 缓存 5 分钟,避免每轮重复遍历。
|
|
1679
1692
|
try {
|
|
1680
|
-
const
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
}
|
|
1693
|
+
const sid = agent && agent.session && agent.session.id ? String(agent.session.id) : ''
|
|
1694
|
+
const cached = this._officialWindowCache
|
|
1695
|
+
let cw = 0
|
|
1696
|
+
if (cached && sid && cached.sid === sid && Date.now() - cached.at < 300000) {
|
|
1697
|
+
cw = cached.window
|
|
1698
|
+
} else {
|
|
1699
|
+
cw = findOfficialContextWindowPre(sessionEventsOf(agent && agent.session))
|
|
1700
|
+
this._officialWindowCache = { sid, at: Date.now(), window: cw }
|
|
1688
1701
|
}
|
|
1702
|
+
if (cw > 0) { win = cw; winSource = 'official-context' }
|
|
1689
1703
|
} catch (eWin) {}
|
|
1690
1704
|
}
|
|
1691
1705
|
if (win <= 0) return
|
|
@@ -1774,10 +1788,44 @@ class MemoryEngine {
|
|
|
1774
1788
|
return tokens
|
|
1775
1789
|
}
|
|
1776
1790
|
|
|
1791
|
+
/** 当前(最近活跃)会话 id;无 agent 时返回 ''。 */
|
|
1792
|
+
currentSessionId() {
|
|
1793
|
+
try {
|
|
1794
|
+
const agent = this._lastAgent
|
|
1795
|
+
return agent && agent.session && agent.session.id ? String(agent.session.id) : ''
|
|
1796
|
+
} catch (e) { return '' }
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
/** 修A(2026-09-08):解析会话所属 workspaceId。官方 api-session-controller create() 只在传 workspaceId 时
|
|
1800
|
+
* 调 workspace.attachSession()(index.js:566-588),只传 cwd 仅设工作目录 → 新会话永远落「未分组工作区」。
|
|
1801
|
+
* 归属判定走 workspaceRegistry(与官方 forkWorkspace index.js:874 同款),服务缺失/无归属返回 ''(client 回退 cwd)。 */
|
|
1802
|
+
resolveWorkspaceIdForSession(sessionId, cwd) {
|
|
1803
|
+
try {
|
|
1804
|
+
// 惰性解析:apply 时服务可能尚未就绪(strict get 会返回 undefined),每次调用再取一次兜底
|
|
1805
|
+
let reg = this._workspaceRegistry
|
|
1806
|
+
if (!reg && this._ctxRef && typeof this._ctxRef.get === 'function') {
|
|
1807
|
+
try { reg = this._ctxRef.get('workspaceRegistry') } catch (e) {}
|
|
1808
|
+
}
|
|
1809
|
+
if (!reg || typeof reg.list !== 'function') return ''
|
|
1810
|
+
return workspaceIdForSession(reg.list(), sessionId, cwd)
|
|
1811
|
+
} catch (e) { return '' }
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
/** 接续前刷新仪式指令(③,2026-09-08):交给 client 用 remote.session.prompt 发回旧会话,
|
|
1815
|
+
* 让旧 Agent 在材料组装前把 PLAN.md 与交接账本刷到最新。 */
|
|
1816
|
+
refreshRitualPrompt() {
|
|
1817
|
+
return [
|
|
1818
|
+
'[接续前刷新仪式] 即将把本会话交接给新会话。请在本轮内只做下面两件事,不要执行其他操作、不要推进任务:',
|
|
1819
|
+
'1. 调用 memory_note(kind=plan, content=<项目白板的最新完整全貌:项目全貌/当前目标/下一步,旧版会自动归档>) 重写 PLAN.md;',
|
|
1820
|
+
'2. 调用 memory_note(kind=handoff, content=<四段式交接账本:## 任务状态 / ## 目标 / ## 已试方案与失败原因 / ## 进度与下一步,每段≤5行,下一步必须是可以直接执行的第一步,带文件路径或命令>) 写一篇新账本。',
|
|
1821
|
+
'完成后只回复一句「已刷新」,不要再调用其他工具。',
|
|
1822
|
+
].join(String.fromCharCode(10))
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1777
1825
|
/** M-CM6-B v2·旧会话上下文包(2026-09-08):定位当前(旧)会话持久化文件(~/.dsh/sessions/<ws>/<sid>/session.jsonl[.zstd]),
|
|
1778
|
-
* zstd 解压抽取 header(cwd/agentPreset)、最新 request/
|
|
1779
|
-
*
|
|
1780
|
-
* 返回 {sessionId, transcriptPath, provider, model, agentPreset, cwd, tailText} 或部分为空(
|
|
1826
|
+
* zstd 解压抽取 header(cwd/agentPreset)、最新 request/header 的 data.header.config(provider/model/reasoningEffort)、
|
|
1827
|
+
* user+assistant+工具 消息全量转写;转写落盘 handoff/prev-session-*.md → 新会话 AI 用 read 工具即可随时读取旧会话全部内容。
|
|
1828
|
+
* 返回 {sessionId, transcriptPath, provider, model, reasoningEffort, agentPreset, cwd, tailText, msgCount} 或部分为空(fail-soft)。 */
|
|
1781
1829
|
async buildPrevSessionPack() {
|
|
1782
1830
|
try {
|
|
1783
1831
|
const agent = this._lastAgent
|
|
@@ -1802,25 +1850,14 @@ class MemoryEngine {
|
|
|
1802
1850
|
text = buf.toString('utf8')
|
|
1803
1851
|
}
|
|
1804
1852
|
const lines = text.split('\n').filter((l) => l.trim())
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
const
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
if (t === 'request/context') {
|
|
1814
|
-
const d = (ev && ev.data) || {}
|
|
1815
|
-
if (d.model) { provider = String(d.provider || provider); model = String(d.model) }
|
|
1816
|
-
} else if (t === 'user/message' || t === 'assistant/message') {
|
|
1817
|
-
const m = messageOfEvent(ev)
|
|
1818
|
-
if (m && m.role && Array.isArray(m.content)) {
|
|
1819
|
-
const txt = textOfContent(m.content)
|
|
1820
|
-
if (txt) msgs.push({ role: m.role, text: txt })
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1853
|
+
// 修B(2026-09-08):模型/思考档位取自 request/header 的 data.header.config(官方投影同源),request/context 仅回退。
|
|
1854
|
+
const folded = foldSessionLogEvents(lines)
|
|
1855
|
+
const preset = folded.preset
|
|
1856
|
+
const cwd = folded.cwd
|
|
1857
|
+
const provider = folded.provider
|
|
1858
|
+
const model = folded.model
|
|
1859
|
+
const reasoningEffort = folded.reasoningEffort
|
|
1860
|
+
const msgs = folded.msgs
|
|
1824
1861
|
const p = await this.resolvePaths(undefined)
|
|
1825
1862
|
const NL = String.fromCharCode(10)
|
|
1826
1863
|
const stamp = new Date().toISOString().slice(11, 19).replace(/:/g, '')
|
|
@@ -1843,9 +1880,9 @@ class MemoryEngine {
|
|
|
1843
1880
|
}
|
|
1844
1881
|
await mkdir(p.handoffDir, { recursive: true })
|
|
1845
1882
|
await writeFile(outPath, body.join(NL), 'utf8')
|
|
1846
|
-
const tail = msgs.slice(-
|
|
1847
|
-
diag('prev-session pack built: sid=' + sid + ' msgs=' + msgs.length + ' -> ' + outPath)
|
|
1848
|
-
return { sessionId: sid, transcriptPath: outPath, provider, model, agentPreset: preset, cwd: cwd || p.ws, tailText: tail }
|
|
1883
|
+
const tail = msgs.slice(-20).map((m) => m.role + ': ' + m.text.slice(0, 700)).join(NL + '---' + NL)
|
|
1884
|
+
diag('prev-session pack built: sid=' + sid + ' msgs=' + msgs.length + ' model=' + (model || '-') + '@' + (provider || '-') + ' effort=' + (reasoningEffort || '-') + ' -> ' + outPath)
|
|
1885
|
+
return { sessionId: sid, transcriptPath: outPath, provider, model, reasoningEffort, agentPreset: preset, cwd: cwd || p.ws, tailText: tail, msgCount: msgs.length }
|
|
1849
1886
|
} catch (e) { diag('buildPrevSessionPack error: ' + (e && e.message)); return null }
|
|
1850
1887
|
}
|
|
1851
1888
|
|
|
@@ -1867,13 +1904,32 @@ class MemoryEngine {
|
|
|
1867
1904
|
}
|
|
1868
1905
|
const pack = await this.buildPrevSessionPack()
|
|
1869
1906
|
if (!plan && !ledger && !pack) return { ok: false, error: 'no handoff material (PLAN/账本/旧会话均为空)' }
|
|
1907
|
+
// 材料时间戳与新鲜度(分层压缩:过期提示)
|
|
1908
|
+
const fmtMt = (ms) => { try { return ms ? new Date(Number(ms)).toISOString().replace('T', ' ').slice(0, 19) + 'Z' : '(未知)' } catch (e) { return '(未知)' } }
|
|
1909
|
+
const planMt = await stat(p.planPath).then((s) => Number(s.mtimeMs)).catch(() => 0)
|
|
1910
|
+
let ledgerName = '', ledgerMt = 0
|
|
1911
|
+
try {
|
|
1912
|
+
const names = (await readdir(p.handoffDir).catch(() => [])).filter((n) => /^handoff-\d{8}-\d{6}(-[a-z])?\.md$/.test(n))
|
|
1913
|
+
for (const n of names) {
|
|
1914
|
+
const st = await stat(path.join(p.handoffDir, n)).catch(() => null)
|
|
1915
|
+
const mt = st ? Number(st.mtimeMs) : 0
|
|
1916
|
+
if (mt >= ledgerMt) { ledgerMt = mt; ledgerName = n }
|
|
1917
|
+
}
|
|
1918
|
+
} catch (eL) {}
|
|
1919
|
+
const staleNote = planMt && ledgerMt && planMt < ledgerMt ? '(白板比账本旧——以账本为准)' : ''
|
|
1920
|
+
// 修A(2026-09-08):解析旧会话所属工作区,交给 client 用 workspaceId 建新会话(官方只有 workspaceId 分支会 attachSession)。
|
|
1921
|
+
const prevSid = (pack && pack.sessionId) || this.currentSessionId()
|
|
1922
|
+
const wsForSession = (pack && pack.cwd) || p.ws
|
|
1923
|
+
const workspaceId = this.resolveWorkspaceIdForSession(prevSid, wsForSession)
|
|
1870
1924
|
const parts = [
|
|
1871
1925
|
'接续上一会话的任务。请先读完下面的交接材料恢复上下文,然后直接继续推进未完成事项,不要重新开始。',
|
|
1926
|
+
'材料分层:第0层=指令+白板(全局图景);第1层=交接账本(四段式,最新);第2层=近期线程(最近 20 条,单条上限 700 字,保留角色与工具标记);第3层=完整转写(按需 read)。',
|
|
1872
1927
|
]
|
|
1873
|
-
if (plan) parts.push('
|
|
1874
|
-
if (ledger) parts.push('
|
|
1928
|
+
if (plan) parts.push('【第0层 · 白板 PLAN.md(节选)】更新于 ' + fmtMt(planMt) + staleNote + NL + plan.slice(0, 3000))
|
|
1929
|
+
if (ledger) parts.push('【第1层 · 交接账本 ' + (ledgerName || ledgerFrom) + '】写于 ' + fmtMt(ledgerMt) + ' (' + ledgerFrom + ')' + NL + ledger.slice(0, 8000))
|
|
1875
1930
|
if (pack) {
|
|
1876
|
-
|
|
1931
|
+
if (pack.tailText) parts.push('【第2层 · 近期线程(最近 ' + Math.min(20, Number(pack.msgCount) || 20) + ' 条 / 共 ' + (pack.msgCount || 0) + ' 条)】' + NL + pack.tailText)
|
|
1932
|
+
const guide = ['【第3层 · 完整转写与检索(按需)】']
|
|
1877
1933
|
if (pack.transcriptPath) {
|
|
1878
1934
|
guide.push('- 旧会话(' + pack.sessionId + ')的完整对话转写已写入: ' + pack.transcriptPath)
|
|
1879
1935
|
guide.push('- **接续前先用 read 工具读取该转写文件**,以完全理解旧会话的讨论、结论与未竟事项;任务推进中也可随时回读。')
|
|
@@ -1881,20 +1937,32 @@ class MemoryEngine {
|
|
|
1881
1937
|
guide.push('- 旧会话 ID: ' + pack.sessionId + '(转写文件生成失败,可用 memory_recall 检索)')
|
|
1882
1938
|
}
|
|
1883
1939
|
guide.push('- 历史会话全文检索: memory_recall(scope=\'sessions\', query=\'关键词\') 可跨全部历史会话查证细节。')
|
|
1884
|
-
if (pack.
|
|
1885
|
-
if (pack.
|
|
1886
|
-
if (pack.agentPreset) guide.push('- 旧会话 agentPreset: ' + pack.agentPreset + '(preset 不随创建传递,如需同款预设请手动选择;注意 code→ptc 改名的历史会话需用户级兼容预设)。')
|
|
1940
|
+
if (pack.model) guide.push('- 状态沿用: 模型 ' + pack.model + (pack.provider ? ' @ ' + pack.provider : '') + (pack.reasoningEffort ? '(思考档位 ' + pack.reasoningEffort + ')' : '') + ' 已在新会话自动选回(失败则回退路由默认);工作区 ' + (workspaceId ? '按 workspaceId 绑定 ' + workspaceId : '按 cwd 绑定 ' + wsForSession) + ';插件配置(无人值守/水位/交接等)为全局配置,自动继承。')
|
|
1941
|
+
if (pack.agentPreset) guide.push('- 旧会话 agentPreset: ' + pack.agentPreset + '(已随创建传递;注意 code→ptc 改名的历史会话需用户级兼容预设)。')
|
|
1887
1942
|
parts.push(guide.join(NL))
|
|
1888
1943
|
}
|
|
1944
|
+
// 接续序号:统计 handoff 目录里已有的 prev-session 包数量 +1(每次一键/自动接续产出一个)
|
|
1945
|
+
let contSeq = 1
|
|
1946
|
+
try { const prevs = (await readdir(p.handoffDir).catch(() => [])).filter((f) => /^prev-session-.*\.md$/.test(f)); contSeq = prevs.length + 1 } catch (eS) {}
|
|
1947
|
+
const wsBase = String((pack && pack.cwd) || p.ws || '').split(/[\\/]/).filter(Boolean).pop() || ''
|
|
1889
1948
|
return {
|
|
1890
1949
|
ok: true,
|
|
1891
1950
|
carryText: parts.join(NL + NL).slice(0, 18000),
|
|
1892
1951
|
planPath: p.planPath,
|
|
1893
1952
|
ws: (pack && pack.cwd) || p.ws,
|
|
1953
|
+
workspaceId: workspaceId,
|
|
1894
1954
|
provider: (pack && pack.provider) || '',
|
|
1895
1955
|
model: (pack && pack.model) || '',
|
|
1956
|
+
reasoningEffort: (pack && pack.reasoningEffort) || '',
|
|
1957
|
+
agentPreset: (pack && pack.agentPreset) || '',
|
|
1958
|
+
wsBase: wsBase,
|
|
1959
|
+
contSeq: contSeq,
|
|
1896
1960
|
prevSessionId: (pack && pack.sessionId) || '',
|
|
1897
1961
|
transcriptPath: (pack && pack.transcriptPath) || '',
|
|
1962
|
+
planMtime: planMt,
|
|
1963
|
+
ledgerName: ledgerName,
|
|
1964
|
+
ledgerMtime: ledgerMt,
|
|
1965
|
+
msgCount: (pack && pack.msgCount) || 0,
|
|
1898
1966
|
}
|
|
1899
1967
|
} catch (e) { return { ok: false, error: String(e && e.message ? e.message : e) } }
|
|
1900
1968
|
}
|
|
@@ -1934,11 +2002,18 @@ class MemoryEngine {
|
|
|
1934
2002
|
this.state.waterLevelModel = (wi && wi.model) || ''
|
|
1935
2003
|
} catch (e) {}
|
|
1936
2004
|
}
|
|
2005
|
+
const planMt = await stat(planPath).then((s) => Number(s.mtimeMs)).catch(() => 0)
|
|
2006
|
+
const sid = this.currentSessionId()
|
|
1937
2007
|
return {
|
|
1938
2008
|
enabled: true, planPath,
|
|
1939
2009
|
plan: await this.readTextSafe(planPath),
|
|
2010
|
+
planMtime: planMt,
|
|
1940
2011
|
planVersions: versions.slice(0, 30),
|
|
1941
2012
|
ledgers: ledgers.slice(0, 60),
|
|
2013
|
+
// ③接续前刷新仪式(2026-09-08):client 拿到 sessionId+prompt 后先让旧 Agent 刷新白板/账本,再取接续材料。
|
|
2014
|
+
refresh: this.config.autoContinueRefreshRitual === false || !sid
|
|
2015
|
+
? { sessionId: '', prompt: '' }
|
|
2016
|
+
: { sessionId: sid, prompt: this.refreshRitualPrompt() },
|
|
1942
2017
|
waterLevel: {
|
|
1943
2018
|
ratio: Number(this.state.waterLevelRatio) || 0,
|
|
1944
2019
|
tokens: Number(this.state.waterLevelTokens) || 0,
|
|
@@ -3843,6 +3918,93 @@ class MemoryEngine {
|
|
|
3843
3918
|
if (controller.signal.aborted && run && typeof run.dispose === 'function') {
|
|
3844
3919
|
try { await run.dispose() } catch (e2) {}
|
|
3845
3920
|
}
|
|
3921
|
+
// 子代理痕迹回收(2026-09-08):本插件的一次性子代理(auto-memory-*)结束即把会话目录移入备份,
|
|
3922
|
+
// 避免 ~/.dsh/sessions 下插件痕迹堆积拖慢宿主会话列表。失败静默,由每日兜底巡检补收。
|
|
3923
|
+
if (run && run.agent) { try { await this.recycleSubagentSession(run.agent) } catch (e3) {} }
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3927
|
+
/** 子代理痕迹回收(2026-09-08):本插件 spawn 的一次性子代理结束后,把其会话目录与投影缓存移入备份目录。
|
|
3928
|
+
* 只动 origin=subagent 的会话;任何失败静默(留给每日兜底巡检),绝不影响子代理结果返回。 */
|
|
3929
|
+
async recycleSubagentSession(agent) {
|
|
3930
|
+
try {
|
|
3931
|
+
if (this.config.subagentGcEnabled === false) return { ok: false, reason: 'disabled' }
|
|
3932
|
+
const session = agent && agent.session
|
|
3933
|
+
const header = session && session.header
|
|
3934
|
+
const sid = session && (session.id || (header && header.id)) ? String(session.id || header.id) : ''
|
|
3935
|
+
if (!sid) return { ok: false, reason: 'no-id' }
|
|
3936
|
+
if (!header || header.origin !== 'subagent') return { ok: false, reason: 'not-subagent' }
|
|
3937
|
+
const dir = await this.locateSessionDir(sid, header)
|
|
3938
|
+
if (!dir) return { ok: false, reason: 'not-located' }
|
|
3939
|
+
const wsDir = path.basename(path.dirname(dir))
|
|
3940
|
+
const backupRoot = path.join(dshHome(), 'subagent-gc-backup')
|
|
3941
|
+
const dest = path.join(backupRoot, wsDir, sid)
|
|
3942
|
+
await mkdir(path.dirname(dest), { recursive: true })
|
|
3943
|
+
await rename(dir, dest)
|
|
3944
|
+
// 投影缓存同步移走,避免孤儿条目长期占用索引
|
|
3945
|
+
try {
|
|
3946
|
+
const cacheFile = path.join(dshHome(), 'storages', 'session_projcache', 'sessions', sid)
|
|
3947
|
+
if (existsSync(cacheFile)) {
|
|
3948
|
+
const cacheDest = path.join(backupRoot, '_projcache', sid)
|
|
3949
|
+
await mkdir(path.dirname(cacheDest), { recursive: true })
|
|
3950
|
+
await rename(cacheFile, cacheDest)
|
|
3951
|
+
}
|
|
3952
|
+
} catch (e2) {}
|
|
3953
|
+
diag('subagent gc: recycled session ' + sid)
|
|
3954
|
+
return { ok: true, sid }
|
|
3955
|
+
} catch (e) {
|
|
3956
|
+
diag('subagent gc failed: ' + (e && e.message ? e.message : String(e)))
|
|
3957
|
+
return { ok: false, reason: e && e.message ? e.message : String(e) }
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3960
|
+
|
|
3961
|
+
/** 定位会话持久化目录:优先官方 sessionPersistence.locate,回退按会话 id 扫描 sessions 根。 */
|
|
3962
|
+
async locateSessionDir(sid, header) {
|
|
3963
|
+
try {
|
|
3964
|
+
const sp = this.ctx && typeof this.ctx.get === 'function' ? this.ctx.get('sessionPersistence') : null
|
|
3965
|
+
const loc = sp && typeof sp.locate === 'function' ? sp.locate(header) : null
|
|
3966
|
+
if (loc && loc.path) {
|
|
3967
|
+
const dir = path.dirname(String(loc.path))
|
|
3968
|
+
if (existsSync(dir)) return dir
|
|
3969
|
+
}
|
|
3970
|
+
} catch (e) {}
|
|
3971
|
+
try {
|
|
3972
|
+
const root = path.join(dshHome(), 'sessions')
|
|
3973
|
+
const wss = await readdir(root).catch(() => [])
|
|
3974
|
+
for (const ws of wss) {
|
|
3975
|
+
const dir = path.join(root, ws, sid)
|
|
3976
|
+
if (existsSync(dir)) return dir
|
|
3977
|
+
}
|
|
3978
|
+
} catch (e) {}
|
|
3979
|
+
return ''
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3982
|
+
/** 子代理痕迹兜底巡检(每日一次):回收超过保留期仍残留的本插件一次性痕迹。 */
|
|
3983
|
+
async subagentGcSweep(force) {
|
|
3984
|
+
try {
|
|
3985
|
+
if (this.config.subagentGcEnabled === false) return { ok: false, reason: 'disabled' }
|
|
3986
|
+
const today = this.memToday()
|
|
3987
|
+
if (!force && this._subagentGcDay === today) return { ok: true, skipped: 'done-today' }
|
|
3988
|
+
this._subagentGcDay = today
|
|
3989
|
+
const sessionsRoot = path.join(dshHome(), 'sessions')
|
|
3990
|
+
const keepDays = Math.max(0, Number(this.config.subagentGcKeepDays) || 0)
|
|
3991
|
+
const scan = await scanPluginSubagentSessions({ sessionsRoot, labelPrefix: PLUGIN_LABEL_PREFIX, keepMs: 0 })
|
|
3992
|
+
const now = Date.now()
|
|
3993
|
+
// 无论保留期多长,最近 10 分钟的一律不动(可能与正在运行的子代理竞争)
|
|
3994
|
+
const floorMs = Math.max(keepDays * 24 * 3600 * 1000, 10 * 60 * 1000)
|
|
3995
|
+
const eligible = scan.candidates.filter((c) => now - c.mtimeMs >= floorMs)
|
|
3996
|
+
if (!eligible.length) return { ok: true, scanned: scan.scanned, recycled: 0 }
|
|
3997
|
+
const res = await recycleSessions({
|
|
3998
|
+
candidates: eligible,
|
|
3999
|
+
backupRoot: path.join(dshHome(), 'subagent-gc-backup'),
|
|
4000
|
+
projcacheRoot: path.join(dshHome(), 'storages', 'session_projcache', 'sessions'),
|
|
4001
|
+
apply: true,
|
|
4002
|
+
})
|
|
4003
|
+
diag('subagent gc sweep: recycled=' + res.moved.length + ' failed=' + res.failed.length + ' bytes=' + res.bytes)
|
|
4004
|
+
return { ok: true, scanned: scan.scanned, recycled: res.moved.length, failed: res.failed.length, bytes: res.bytes }
|
|
4005
|
+
} catch (e) {
|
|
4006
|
+
diag('subagent gc sweep failed: ' + (e && e.message ? e.message : String(e)))
|
|
4007
|
+
return { ok: false, reason: e && e.message ? e.message : String(e) }
|
|
3846
4008
|
}
|
|
3847
4009
|
}
|
|
3848
4010
|
|
|
@@ -4940,6 +5102,76 @@ function hasStutter(text) {
|
|
|
4940
5102
|
if (/([\u4e00-\u9fff\u3040-\u30ff])(?:[^\u4e00-\u9fff\u3040-\u30ff]{0,2}\1){4,}/.test(t)) return true
|
|
4941
5103
|
return false
|
|
4942
5104
|
}
|
|
5105
|
+
/** 折叠会话日志事件流(纯函数,供 buildPrevSessionPack 与 smoke 共用)。
|
|
5106
|
+
* 2026-09-08 修B:模型/思考档位在 request/header 的 data.header.config.{provider,model,reasoningEffort}
|
|
5107
|
+
* (官方 api-session-controller/lib/index.js:2008-2012 applyModelSelectionProjection 同源),取最后一条;
|
|
5108
|
+
* request/context 只是旧日志的回退来源,单查它必然取不到(2.2.4 实测:新会话仍用默认模型)。
|
|
5109
|
+
* 消息序列保留角色标记与工具标记(user/assistant/tool_call/tool_result),供第2层近期线程还原结构。 */
|
|
5110
|
+
function foldSessionLogEvents(lines) {
|
|
5111
|
+
const out = { preset: '', cwd: '', provider: '', model: '', reasoningEffort: '', msgs: [] }
|
|
5112
|
+
const list = Array.isArray(lines) ? lines : []
|
|
5113
|
+
try {
|
|
5114
|
+
const h = JSON.parse(list[0] || '')
|
|
5115
|
+
if (h && typeof h === 'object') { out.preset = String(h.agentPreset || ''); out.cwd = String(h.cwd || '') }
|
|
5116
|
+
} catch (e) {}
|
|
5117
|
+
let hProvider = '', hModel = '', hEffort = '', cProvider = '', cModel = '', cEffort = ''
|
|
5118
|
+
for (const l of list) {
|
|
5119
|
+
let ev
|
|
5120
|
+
try { ev = JSON.parse(l) } catch (e) { continue }
|
|
5121
|
+
const t = ev && ev.type
|
|
5122
|
+
if (t === 'request/header') {
|
|
5123
|
+
const cfg = (ev.data && ev.data.header && ev.data.header.config) || {}
|
|
5124
|
+
if (cfg.model) {
|
|
5125
|
+
hProvider = String(cfg.provider || '')
|
|
5126
|
+
hModel = String(cfg.model)
|
|
5127
|
+
hEffort = cfg.reasoningEffort === undefined || cfg.reasoningEffort === null ? '' : String(cfg.reasoningEffort)
|
|
5128
|
+
}
|
|
5129
|
+
} else if (t === 'request/context') {
|
|
5130
|
+
const d = (ev && ev.data) || {}
|
|
5131
|
+
if (d.model) {
|
|
5132
|
+
cProvider = String(d.provider || '')
|
|
5133
|
+
cModel = String(d.model)
|
|
5134
|
+
const eff = d.reasoningEffort || d.reasoning || d.reasoningEffortLevel
|
|
5135
|
+
cEffort = eff ? String(eff) : ''
|
|
5136
|
+
}
|
|
5137
|
+
} else if (t === 'user/message' || t === 'assistant/message') {
|
|
5138
|
+
const m = messageOfEvent(ev)
|
|
5139
|
+
if (m && m.role && Array.isArray(m.content)) {
|
|
5140
|
+
const txt = textOfContent(m.content)
|
|
5141
|
+
if (txt) out.msgs.push({ role: m.role, text: txt })
|
|
5142
|
+
}
|
|
5143
|
+
} else if (t === 'tool/call') {
|
|
5144
|
+
const d = (ev && ev.data) || {}
|
|
5145
|
+
const nm = String(d.name || 'tool')
|
|
5146
|
+
const argsRaw = typeof d.arguments === 'string' ? d.arguments : JSON.stringify(d.arguments == null ? null : d.arguments)
|
|
5147
|
+
out.msgs.push({ role: 'tool_call', text: nm + '(' + String(argsRaw || '').slice(0, 400) + ')' })
|
|
5148
|
+
} else if (t === 'tool/result') {
|
|
5149
|
+
const m = messageOfEvent(ev)
|
|
5150
|
+
const txt = m && Array.isArray(m.content) ? textOfContent(m.content) : ''
|
|
5151
|
+
if (txt) out.msgs.push({ role: 'tool_result', text: txt.slice(0, 400) })
|
|
5152
|
+
}
|
|
5153
|
+
}
|
|
5154
|
+
if (hModel) { out.provider = hProvider; out.model = hModel; out.reasoningEffort = hEffort }
|
|
5155
|
+
else { out.provider = cProvider; out.model = cModel; out.reasoningEffort = cEffort }
|
|
5156
|
+
return out
|
|
5157
|
+
}
|
|
5158
|
+
|
|
5159
|
+
/** 从 workspaceRegistry.list() 的结果解析某会话所属 workspaceId(纯函数,供 smoke 驱动)。
|
|
5160
|
+
* 2026-09-08 修A:官方 create 只在传 workspaceId 时调 workspace.attachSession(),只传 cwd 仅设工作目录
|
|
5161
|
+
* → 新会话永远落「未分组」。归属判定与官方 forkWorkspace(index.js:874)同款:workspace.sessionIds 含该会话;
|
|
5162
|
+
* 再按目录兜底(该路径已被某工作区认领,官方 indexLiveSessions 也按路径补账)。返回 '' = 解析不到,调用方回退 cwd。 */
|
|
5163
|
+
function workspaceIdForSession(workspaces, sessionId, cwd) {
|
|
5164
|
+
if (!Array.isArray(workspaces) || !sessionId) return ''
|
|
5165
|
+
const direct = workspaces.find((w) => w && Array.isArray(w.sessionIds) && w.sessionIds.includes(sessionId))
|
|
5166
|
+
if (direct && direct.id) return String(direct.id)
|
|
5167
|
+
if (cwd) {
|
|
5168
|
+
const norm = (v) => String(v || '').replace(/[\\/]+$/, '').toLowerCase()
|
|
5169
|
+
const byPath = workspaces.find((w) => w && w.path && norm(w.path) === norm(cwd))
|
|
5170
|
+
if (byPath && byPath.id) return String(byPath.id)
|
|
5171
|
+
}
|
|
5172
|
+
return ''
|
|
5173
|
+
}
|
|
5174
|
+
|
|
4943
5175
|
/** 从 session 提取消息。surface 不是完整可靠的 user 来源,因此失败时回退完整事件日志。 */
|
|
4944
5176
|
function messageOfEvent(ev) {
|
|
4945
5177
|
if (!ev) return null
|
|
@@ -5476,6 +5708,9 @@ export function apply(ctx, config) {
|
|
|
5476
5708
|
engine._agentSvc = ctx.get('agent') || undefined
|
|
5477
5709
|
// 模型目录(设置页「总结/问候默认模型」抽屉数据源);软获取,缺失时抽屉回退手动输入
|
|
5478
5710
|
engine._llm = ctx.get('llm') || undefined
|
|
5711
|
+
// 工作区注册表(修A,2026-09-08):接续时解析旧会话所属 workspaceId——官方 create 只在传 workspaceId 时绑定工作区
|
|
5712
|
+
engine._workspaceRegistry = ctx.get('workspaceRegistry') || undefined
|
|
5713
|
+
engine._ctxRef = ctx // 惰性重取服务用(apply 时可能尚未就绪)
|
|
5479
5714
|
|
|
5480
5715
|
// 生命周期刷新
|
|
5481
5716
|
const refreshAll = (agent) => {
|
|
@@ -6026,6 +6261,40 @@ export function apply(ctx, config) {
|
|
|
6026
6261
|
} catch (e) { return writeJson(res, 500, { error: String(e && e.message ? e.message : e) }) }
|
|
6027
6262
|
},
|
|
6028
6263
|
},
|
|
6264
|
+
{
|
|
6265
|
+
kind: 'exact',
|
|
6266
|
+
path: API['subagent-gc'],
|
|
6267
|
+
handler: async (req, res) => {
|
|
6268
|
+
// 子代理痕迹回收(loopback):GET=预览(只扫描,不移动文件),POST=执行回收(移入备份目录,可回滚)。
|
|
6269
|
+
if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
6270
|
+
try {
|
|
6271
|
+
const method = req.method || 'GET'
|
|
6272
|
+
if (method === 'GET') {
|
|
6273
|
+
const scan = await scanPluginSubagentSessions({
|
|
6274
|
+
sessionsRoot: path.join(dshHome(), 'sessions'),
|
|
6275
|
+
labelPrefix: PLUGIN_LABEL_PREFIX,
|
|
6276
|
+
keepMs: 0,
|
|
6277
|
+
})
|
|
6278
|
+
const byLabel = {}
|
|
6279
|
+
for (const c of scan.candidates) byLabel[c.label] = (byLabel[c.label] || 0) + 1
|
|
6280
|
+
return writeJson(res, 200, {
|
|
6281
|
+
ok: true,
|
|
6282
|
+
mode: 'preview',
|
|
6283
|
+
enabled: engine.config.subagentGcEnabled !== false,
|
|
6284
|
+
keepDays: Number(engine.config.subagentGcKeepDays) || 0,
|
|
6285
|
+
scanned: scan.scanned,
|
|
6286
|
+
subagentSessions: scan.subagents,
|
|
6287
|
+
candidates: scan.candidates.length,
|
|
6288
|
+
bytes: scan.bytes,
|
|
6289
|
+
byLabel,
|
|
6290
|
+
})
|
|
6291
|
+
}
|
|
6292
|
+
if (method !== 'POST') return writeJson(res, 405, { error: 'method not allowed' })
|
|
6293
|
+
const r = await engine.subagentGcSweep(true)
|
|
6294
|
+
return writeJson(res, r.ok === false ? 400 : 200, r)
|
|
6295
|
+
} catch (e) { return writeJson(res, 500, { error: String(e && e.message ? e.message : e) }) }
|
|
6296
|
+
},
|
|
6297
|
+
},
|
|
6029
6298
|
{
|
|
6030
6299
|
kind: 'exact',
|
|
6031
6300
|
path: API['semantic-download'],
|
|
@@ -6792,7 +7061,7 @@ export function apply(ctx, config) {
|
|
|
6792
7061
|
})()
|
|
6793
7062
|
}, 5 * 60 * 1000)
|
|
6794
7063
|
// 心跳独立 15 秒一次(对齐 polling-lease 心跳节奏),证明轮询机制活着
|
|
6795
|
-
const heartbeatTimer = setInterval(() => { void writeHeartbeat(); try { engine.tickTime() } catch (e) {} }, 15000)
|
|
7064
|
+
const heartbeatTimer = setInterval(() => { void writeHeartbeat(); try { engine.tickTime() } catch (e) {}; try { void engine.subagentGcSweep() } catch (e) {} }, 15000)
|
|
6796
7065
|
void writeHeartbeat() // 立即心跳一次:重启后马上可见轮询存活
|
|
6797
7066
|
|
|
6798
7067
|
// ---------- 注册与清理 ----------
|
|
@@ -6831,4 +7100,4 @@ export function apply(ctx, config) {
|
|
|
6831
7100
|
}
|
|
6832
7101
|
|
|
6833
7102
|
/** 导出卫生守卫与脏 token 检查器(供 smoke-test / 回归测试直接调用)。 */
|
|
6834
|
-
export { sanitizeForWrite, dirtyScanForFiles, mojibakeDensity, tailHas, hasStutter, WRITE_GATE_REASON }
|
|
7103
|
+
export { sanitizeForWrite, dirtyScanForFiles, mojibakeDensity, tailHas, hasStutter, WRITE_GATE_REASON, foldSessionLogEvents, workspaceIdForSession }
|