@shgroup/dsh-serenity-hooks 1.29.0 → 1.29.1
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/dsh.plugin.json +1 -1
- package/lib/index.js +215 -26
- package/lib/seams/context.d.ts +2 -0
- package/lib/session-bound.d.ts +80 -0
- package/lib/session-ops.d.ts +13 -0
- package/package.json +1 -1
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-serenity-hooks",
|
|
3
|
-
"version": "1.29.
|
|
3
|
+
"version": "1.29.1",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"description": "宁静号 ACC harness(Native Cordis 插件):真实 DSH 工具 cc_fs/session/acc_msm/eap/neat/cce/handyman/session_rebuild/skiff_admin + 拦截缝机械约束(safe-mode/路径守卫)+ 高级设定面板(双端口网关/账号管理)+ Skiff 认知子集角色(实验性)。适配 DSH 公开版(0.1.0-rc,deepseek-ai/deepseek-harness)。",
|
|
6
6
|
"engines": {
|
package/lib/index.js
CHANGED
|
@@ -2919,6 +2919,43 @@ function isSessionDirName(dirName) {
|
|
|
2919
2919
|
return /^\d{4}-\d{2}-\d{2}--/.test(dirName) && dirName.length > 11;
|
|
2920
2920
|
}
|
|
2921
2921
|
/**
|
|
2922
|
+
* 从 dsh 会话标题解析 SESSION 目录(编码无关 best-match,U3/U4)——
|
|
2923
|
+
* 标题不假设 S### 前缀(CCC 可自定义编码:apaas-xxx / P### / 完整目录名等)。
|
|
2924
|
+
* 匹配优先级(全部对 AGENT_SESSIONS 现有目录 best-match,不猜):
|
|
2925
|
+
* ① 标题即完整目录名(含日期前缀 `YYYY-MM-DD--`)→ 精确命中
|
|
2926
|
+
* ② 标题含 `--<code>--` 段(如完整目录名被截断为 `<code>-日期-概括` 前段)→
|
|
2927
|
+
* 按 code 段匹配:取标题首 token(`-` 前),与各目录 `--<code>--`/`--<code>` 尾段比对
|
|
2928
|
+
* ③ 唯一模糊子串匹配(多个则返回 null 防误猜)
|
|
2929
|
+
* @param title dsh 会话标题(如 `S142-2026-08-24-概括` / `apaas-26116-…` / 完整目录名)
|
|
2930
|
+
* @param sessionsDir AGENT_SESSIONS 绝对路径
|
|
2931
|
+
* @returns 命中目录的绝对路径(SESSION.md);无/歧义返回 null
|
|
2932
|
+
*/
|
|
2933
|
+
function resolveSessionByTitle(title, sessionsDir) {
|
|
2934
|
+
const t = (title ?? "").trim();
|
|
2935
|
+
if (!t) return null;
|
|
2936
|
+
const all = readAllSessions(sessionsDir);
|
|
2937
|
+
if (all.length === 0) return null;
|
|
2938
|
+
if (isSessionDirName(t)) {
|
|
2939
|
+
const exact = all.find((s) => s.dirName === t);
|
|
2940
|
+
if (exact) return join(exact.path, SESSION_MD);
|
|
2941
|
+
}
|
|
2942
|
+
const codeToken = t.split("-")[0]?.trim() ?? "";
|
|
2943
|
+
if (codeToken) {
|
|
2944
|
+
const byCode = all.filter((s) => {
|
|
2945
|
+
const m = s.dirName.match(/--([^--]+)--/);
|
|
2946
|
+
const code = m ? m[1] : null;
|
|
2947
|
+
const tailMatch = s.dirName.match(/--([^--]+)$/);
|
|
2948
|
+
const tailCode = tailMatch && !s.dirName.includes("--", s.dirName.lastIndexOf("--") + 3) ? tailMatch[1] : null;
|
|
2949
|
+
return code === codeToken || tailCode === codeToken || s.dirName === codeToken || s.dirName.includes(`--${codeToken}`);
|
|
2950
|
+
});
|
|
2951
|
+
if (byCode.length === 1) return join(byCode[0].path, SESSION_MD);
|
|
2952
|
+
if (byCode.length > 1) return null;
|
|
2953
|
+
}
|
|
2954
|
+
const fuzzy = all.filter((s) => s.dirName.toLowerCase().includes(t.toLowerCase()));
|
|
2955
|
+
if (fuzzy.length === 1) return join(fuzzy[0].path, SESSION_MD);
|
|
2956
|
+
return null;
|
|
2957
|
+
}
|
|
2958
|
+
/**
|
|
2922
2959
|
* 约定回退(v1.24.11):AGENT_SESSIONS 下最新修改的**未完成**会话的 SESSION.md。
|
|
2923
2960
|
* readAllSessions 已按「未完成优先 + mtime 降序」排序 → 首个未完成且含 SESSION.md 即最新活动。
|
|
2924
2961
|
* 只作最后手段(内存/events/锚点全缺时),保证重建锚点至少指向一个真实存在的轨迹。
|
|
@@ -3179,6 +3216,56 @@ function qaCheck(root, key) {
|
|
|
3179
3216
|
return lines.join("\n");
|
|
3180
3217
|
}
|
|
3181
3218
|
//#endregion
|
|
3219
|
+
//#region src/session-bound.ts
|
|
3220
|
+
/** 事件 type 常量(声明 + 读取共用) */
|
|
3221
|
+
const SESSION_BOUND_EVENT = "serenity/bound";
|
|
3222
|
+
/** 事件形状归一(运行时数据经 session.append 深冻结,此处仅类型收窄) */
|
|
3223
|
+
function asBoundEvent(e) {
|
|
3224
|
+
const ev = e;
|
|
3225
|
+
if (!ev || ev.type !== "serenity/bound") return null;
|
|
3226
|
+
const d = ev.data;
|
|
3227
|
+
if (!d || typeof d.dirName !== "string" || typeof d.mdPath !== "string") return null;
|
|
3228
|
+
return d;
|
|
3229
|
+
}
|
|
3230
|
+
/**
|
|
3231
|
+
* 读取会话日志中**最后一条** `serenity/bound`(权威绑定,latest-wins)。
|
|
3232
|
+
* 尾到头扫描(时间序最新在后);无 bound 事件返回 null。
|
|
3233
|
+
* 纯逻辑零 DSH 依赖(经 sessionEvents helper 读 snapshotEvents/events 兜底)。
|
|
3234
|
+
*/
|
|
3235
|
+
function readLastBound(session) {
|
|
3236
|
+
const events = sessionEvents(session);
|
|
3237
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
3238
|
+
const b = asBoundEvent(events[i]);
|
|
3239
|
+
if (b) return b;
|
|
3240
|
+
}
|
|
3241
|
+
return null;
|
|
3242
|
+
}
|
|
3243
|
+
/**
|
|
3244
|
+
* append 一条 `serenity/bound` 绑定事件(log-only,无 surfaceOp——元数据不进模型可见面)。
|
|
3245
|
+
* @param session 目标 dsh 会话(真实 Session;append 泛型经内部断言兼容——类型由
|
|
3246
|
+
* `serenity/bound` 声明面保证,append 接受已声明事件)
|
|
3247
|
+
* @param action 绑定动作
|
|
3248
|
+
* @param rec 绑定记录(dirName + mdPath 必填;sessionId 可选展示码)
|
|
3249
|
+
* @returns 是否成功(append 抛错/会话不可用时 false——绑定失败不阻断主流程)
|
|
3250
|
+
*/
|
|
3251
|
+
function appendBound(session, action, rec) {
|
|
3252
|
+
if (!session || typeof session.append !== "function") return false;
|
|
3253
|
+
try {
|
|
3254
|
+
const append = session.append;
|
|
3255
|
+
append(SESSION_BOUND_EVENT, {
|
|
3256
|
+
dirName: rec.dirName,
|
|
3257
|
+
mdPath: rec.mdPath,
|
|
3258
|
+
...rec.sessionId ? { sessionId: rec.sessionId } : {},
|
|
3259
|
+
action,
|
|
3260
|
+
at: Date.now(),
|
|
3261
|
+
...rec.note ? { note: rec.note } : {}
|
|
3262
|
+
});
|
|
3263
|
+
return true;
|
|
3264
|
+
} catch {
|
|
3265
|
+
return false;
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
//#endregion
|
|
3182
3269
|
//#region src/tools/session.ts
|
|
3183
3270
|
/**
|
|
3184
3271
|
* session.ts — session 真实 DSH 工具定义(defineTool)
|
|
@@ -3203,6 +3290,21 @@ function agentCwd$5(exec) {
|
|
|
3203
3290
|
function agentScope$2(exec) {
|
|
3204
3291
|
return exec.agent?.session?.id ?? "default";
|
|
3205
3292
|
}
|
|
3293
|
+
/** 当前 dsh 会话对象(append bound 用);无 → null */
|
|
3294
|
+
function agentDshSession(exec) {
|
|
3295
|
+
const s = exec.agent?.session;
|
|
3296
|
+
return s && typeof s.append === "function" ? { append: s.append } : null;
|
|
3297
|
+
}
|
|
3298
|
+
/**
|
|
3299
|
+
* 当前绑定的权威 dirName(优先持久化 bound——防内存被误 use 污染;无 bound 回退内存 active)。
|
|
3300
|
+
* 供 G1 守卫对比目标会话是否切换。
|
|
3301
|
+
*/
|
|
3302
|
+
function currentBoundDirName(exec, scope) {
|
|
3303
|
+
const dsh = agentDshSession(exec);
|
|
3304
|
+
const bound = dsh ? readLastBound(dsh) : null;
|
|
3305
|
+
if (bound) return bound.dirName;
|
|
3306
|
+
return getActiveSessionInfo(scope)?.dirName ?? null;
|
|
3307
|
+
}
|
|
3206
3308
|
/**
|
|
3207
3309
|
* 清洗 + 截断会话概括(需求② S142 用户拍板:编号日期后加 ≤20 字内容概括)。
|
|
3208
3310
|
* 规则(服务端统一,不信任 LLM 输入):
|
|
@@ -3275,18 +3377,6 @@ function renameDshSessionOnUse(deps, session, titles, active, summary) {
|
|
|
3275
3377
|
}
|
|
3276
3378
|
}
|
|
3277
3379
|
/**
|
|
3278
|
-
* 从 create 结果构造命名用 ActiveSessionInfo(v1.25.11,S142 用户:create 也要命名):
|
|
3279
|
-
* createSession 返回的 result 已含 sessionId(S###/issue)与 dirName——无需等待 use 激活,
|
|
3280
|
-
* 直接构造(mdPath = sessionPath/SESSION.md,与 use 时一致)即可驱动 renameDshSessionOnUse。
|
|
3281
|
-
*/
|
|
3282
|
-
function activeInfoFromCreate(result) {
|
|
3283
|
-
return {
|
|
3284
|
-
sessionId: result.sessionId,
|
|
3285
|
-
dirName: result.dirName,
|
|
3286
|
-
mdPath: join(result.sessionPath, "SESSION.md")
|
|
3287
|
-
};
|
|
3288
|
-
}
|
|
3289
|
-
/**
|
|
3290
3380
|
* 把当前 dsh 会话重命名为指定 SESSION 的命名标题(use/create 共用;v1.25.11)。
|
|
3291
3381
|
* 需求②:summary 参数(≤20 字概括)透传——标题带概括(编号日期固定派生)。
|
|
3292
3382
|
* 门控/失败可见性与 renameDshSessionOnUse 一致(不静默:成功 log / 失败 warn)。
|
|
@@ -3440,6 +3530,10 @@ function createSessionTool(ctx) {
|
|
|
3440
3530
|
type: "boolean",
|
|
3441
3531
|
description: "close must be true (prevents accidental close)"
|
|
3442
3532
|
},
|
|
3533
|
+
force: {
|
|
3534
|
+
type: "boolean",
|
|
3535
|
+
description: "use: allow switching away from the currently-bound session (binding guard override)"
|
|
3536
|
+
},
|
|
3443
3537
|
dryRun: {
|
|
3444
3538
|
type: "boolean",
|
|
3445
3539
|
description: "create/archive preview mode (no actual changes)"
|
|
@@ -3474,7 +3568,12 @@ function createSessionTool(ctx) {
|
|
|
3474
3568
|
dryRun: isDryRun
|
|
3475
3569
|
});
|
|
3476
3570
|
let message = result.message;
|
|
3477
|
-
if (!isDryRun)
|
|
3571
|
+
if (!isDryRun) appendBound(agentDshSession(exec), "create", {
|
|
3572
|
+
dirName: result.dirName,
|
|
3573
|
+
mdPath: join(result.sessionPath, "SESSION.md"),
|
|
3574
|
+
sessionId: result.sessionId,
|
|
3575
|
+
note: "created (binding unchanged until explicit use)"
|
|
3576
|
+
});
|
|
3478
3577
|
if (!isDryRun && cccHooks.includes("create-transform")) try {
|
|
3479
3578
|
const hookResult = await runMsmAsync(root, {
|
|
3480
3579
|
action: "exec",
|
|
@@ -3492,14 +3591,47 @@ function createSessionTool(ctx) {
|
|
|
3492
3591
|
if (!args.name) throw new Error("use requires name (S### or directory name)");
|
|
3493
3592
|
if (!args.summary || args.summary.trim() === "") throw new Error("use requires --summary <content summary ≤20 chars> (appended to the dsh session title as S###-YYYY-MM-DD-<summary>; id and date stay server-derived)");
|
|
3494
3593
|
const scope = agentScope$2(exec);
|
|
3594
|
+
const targetEntry = findSession(join(root, "AGENT_SESSIONS"), args.name);
|
|
3595
|
+
if (!targetEntry) throw new Error(`Session not found: "${args.name}". Use "list" to see available sessions.`);
|
|
3596
|
+
const targetDirName = targetEntry.dirName;
|
|
3597
|
+
const currentDir = currentBoundDirName(exec, scope);
|
|
3598
|
+
const switching = currentDir !== null && targetDirName !== currentDir;
|
|
3599
|
+
const force = args.force === true;
|
|
3600
|
+
if (switching && !force) throw new Error(`Session is bound to ${currentDir}. Switching to ${targetDirName} would orphan the current trajectory. Re-run with --force to switch (or close the current session first).`);
|
|
3495
3601
|
const active = useSession(root, args.name, scope);
|
|
3496
3602
|
const info = getActiveSessionInfo(scope);
|
|
3497
3603
|
if (info) renameDshSessionForActive(ctx, exec, info, args.summary);
|
|
3604
|
+
const dsh = agentDshSession(exec);
|
|
3605
|
+
if (dsh && info) {
|
|
3606
|
+
const prevBound = readLastBound(dsh);
|
|
3607
|
+
appendBound(dsh, switching && prevBound ? "switch" : "activate", {
|
|
3608
|
+
dirName: info.dirName,
|
|
3609
|
+
mdPath: info.mdPath,
|
|
3610
|
+
sessionId: info.sessionId,
|
|
3611
|
+
...switching ? { note: "forced switch" } : {}
|
|
3612
|
+
});
|
|
3613
|
+
}
|
|
3498
3614
|
return active;
|
|
3499
3615
|
}
|
|
3500
|
-
case "close":
|
|
3501
|
-
|
|
3502
|
-
|
|
3616
|
+
case "close": {
|
|
3617
|
+
const scope = agentScope$2(exec);
|
|
3618
|
+
const requested = args.name?.trim() ?? "";
|
|
3619
|
+
const boundDir = currentBoundDirName(exec, scope);
|
|
3620
|
+
if (boundDir && requested) {
|
|
3621
|
+
if ((findSession(join(root, "AGENT_SESSIONS"), requested)?.dirName ?? requested) !== boundDir) throw new Error(`Session is bound to ${boundDir}. close targets the bound session — pass the bound session (or omit name) to close it; closing ${requested} while bound to another session is not allowed.`);
|
|
3622
|
+
}
|
|
3623
|
+
if (!boundDir && !requested) throw new Error("close requires name (no active binding to close) — pass the session to close.");
|
|
3624
|
+
const closed = closeSession(root, boundDir ? boundDir : requested, args.confirm ?? false, scope);
|
|
3625
|
+
const dsh = agentDshSession(exec);
|
|
3626
|
+
const bound = dsh ? readLastBound(dsh) : null;
|
|
3627
|
+
if (bound) appendBound(dsh, "release", {
|
|
3628
|
+
dirName: bound.dirName,
|
|
3629
|
+
mdPath: bound.mdPath,
|
|
3630
|
+
sessionId: bound.sessionId,
|
|
3631
|
+
note: "session closed"
|
|
3632
|
+
});
|
|
3633
|
+
return closed;
|
|
3634
|
+
}
|
|
3503
3635
|
case "archive": return archiveSessions(root, {
|
|
3504
3636
|
name: args.name,
|
|
3505
3637
|
dryRun: args.dryRun ?? false
|
|
@@ -3988,7 +4120,14 @@ async function queueRebuild(ctx, opts) {
|
|
|
3988
4120
|
if (!session) throw new Error(`Unable to locate dsh session ${dshSessionId} (session may be closed)`);
|
|
3989
4121
|
const mdPath = resolveSessionMdPath(root, dshSessionId, session);
|
|
3990
4122
|
if (!mdPath) throw new Error("Unable to determine the active SESSION.md — no session context found in this conversation. Run \"session use <S###> --summary <内容概括 ≤20 字>\" first to activate the trajectory to resume, then retry session_rebuild.");
|
|
3991
|
-
const
|
|
4123
|
+
const sessionName = getActiveSessionInfo(dshSessionId)?.sessionId ?? sessionNameFromMdPath(mdPath);
|
|
4124
|
+
const anchor = buildRebuildAnchor(root, sessionName, mdPath);
|
|
4125
|
+
appendBound(session, "rebuild", {
|
|
4126
|
+
dirName: basename(dirname(mdPath)),
|
|
4127
|
+
mdPath,
|
|
4128
|
+
sessionId: sessionName.startsWith("S") ? sessionName : void 0,
|
|
4129
|
+
note: "rebuild queued"
|
|
4130
|
+
});
|
|
3992
4131
|
pendingRebuilds.set(dshSessionId, {
|
|
3993
4132
|
anchor,
|
|
3994
4133
|
summary,
|
|
@@ -6281,6 +6420,17 @@ function registerEntrySkillSection(agent, root) {
|
|
|
6281
6420
|
//#endregion
|
|
6282
6421
|
//#region src/seams/context.ts
|
|
6283
6422
|
const DEFAULT_ENTRY_SKILL_MAX_CHARS = 3e4;
|
|
6423
|
+
/** 从 dsh 会话日志读标题(latest-wins `session/title` 事件;无返回 null)——供标题 reconcile(U3) */
|
|
6424
|
+
function readDshSessionTitle(session) {
|
|
6425
|
+
try {
|
|
6426
|
+
const events = sessionEvents(session);
|
|
6427
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
6428
|
+
const e = events[i];
|
|
6429
|
+
if (e?.type === "session/title" && typeof e.data?.title === "string" && e.data.title.trim() !== "") return e.data.title.trim();
|
|
6430
|
+
}
|
|
6431
|
+
} catch {}
|
|
6432
|
+
return null;
|
|
6433
|
+
}
|
|
6284
6434
|
function accIdentityText(root, configPaths = DEFAULT_SERENITY_CONFIG_PATHS, entrySkillMaxChars = DEFAULT_ENTRY_SKILL_MAX_CHARS) {
|
|
6285
6435
|
loadSerenityConfig(root, configPaths);
|
|
6286
6436
|
const defaultModel = readHandymanConfig(root, configPaths)?.defaultModel;
|
|
@@ -6368,17 +6518,56 @@ function registerContext(ctx, opts = {}) {
|
|
|
6368
6518
|
const scope = agentScope(agent);
|
|
6369
6519
|
if (shouldRestoreActive(agent) && getActiveSessionInfo(scope) === null) try {
|
|
6370
6520
|
if (loadSerenityConfig(root, configPaths).hooks?.autoRestoreSession ?? true) {
|
|
6371
|
-
const
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6521
|
+
const dshSession = agent.session;
|
|
6522
|
+
let restored = null;
|
|
6523
|
+
const bound = readLastBound(dshSession);
|
|
6524
|
+
if (bound) {
|
|
6525
|
+
const abs = bound.mdPath.startsWith(root) ? bound.mdPath : resolve(root, bound.mdPath);
|
|
6526
|
+
if (existsSync(abs)) restored = {
|
|
6527
|
+
dirName: bound.dirName,
|
|
6528
|
+
mdPath: abs,
|
|
6529
|
+
sessionId: bound.sessionId
|
|
6530
|
+
};
|
|
6531
|
+
}
|
|
6532
|
+
if (!restored) {
|
|
6533
|
+
const info = parseSessionContextFromEvents(sessionEvents(dshSession));
|
|
6534
|
+
if (info) {
|
|
6535
|
+
const abs = info.mdPath.startsWith(root) ? info.mdPath : resolve(root, info.mdPath);
|
|
6536
|
+
if (existsSync(abs)) restored = {
|
|
6537
|
+
dirName: info.dirName,
|
|
6538
|
+
mdPath: abs,
|
|
6539
|
+
sessionId: info.sessionId
|
|
6540
|
+
};
|
|
6380
6541
|
}
|
|
6381
6542
|
}
|
|
6543
|
+
if (!restored) {
|
|
6544
|
+
const title = readDshSessionTitle(dshSession);
|
|
6545
|
+
if (title) {
|
|
6546
|
+
const md = resolveSessionByTitle(title, sessionsRoot(root));
|
|
6547
|
+
if (md && existsSync(md)) {
|
|
6548
|
+
const dirName = basename(dirname(md));
|
|
6549
|
+
restored = {
|
|
6550
|
+
dirName,
|
|
6551
|
+
mdPath: md,
|
|
6552
|
+
sessionId: void 0
|
|
6553
|
+
};
|
|
6554
|
+
appendBound(dshSession, "reconcile", {
|
|
6555
|
+
dirName,
|
|
6556
|
+
mdPath: md,
|
|
6557
|
+
note: "auto from title"
|
|
6558
|
+
});
|
|
6559
|
+
console.log(`[serenity-hooks] ↻ 标题兼容持久化绑定: ${title} → ${dirName}`);
|
|
6560
|
+
}
|
|
6561
|
+
}
|
|
6562
|
+
}
|
|
6563
|
+
if (restored) {
|
|
6564
|
+
const derived = restored.sessionId ?? (restored.dirName.match(/--([^--]+)--/) ? restored.dirName.match(/--([^--]+)--/)[1] : void 0) ?? restored.dirName;
|
|
6565
|
+
setActiveSessionInfo(scope, {
|
|
6566
|
+
...restored,
|
|
6567
|
+
sessionId: derived
|
|
6568
|
+
});
|
|
6569
|
+
console.log(`[serenity-hooks] ↻ 从历史恢复激活会话: ${restored.dirName}`);
|
|
6570
|
+
}
|
|
6382
6571
|
}
|
|
6383
6572
|
} catch {}
|
|
6384
6573
|
if (injected.has(key)) return;
|
package/lib/seams/context.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ import type { Context } from 'cordis';
|
|
|
13
13
|
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
14
14
|
import type { UserMessage } from '@deepseek-ai/dsh-session';
|
|
15
15
|
export declare const DEFAULT_ENTRY_SKILL_MAX_CHARS = 30000;
|
|
16
|
+
/** 从 dsh 会话日志读标题(latest-wins `session/title` 事件;无返回 null)——供标题 reconcile(U3) */
|
|
17
|
+
export declare function readDshSessionTitle(session: unknown): string | null;
|
|
16
18
|
export declare function accIdentityText(root: string, configPaths?: string[], entrySkillMaxChars?: number): string;
|
|
17
19
|
/**
|
|
18
20
|
* ACC 注入消息(S134 去重):**只含简短身份锚点**([ACC] 已激活 + CCC 根 + 约束 + handyman 模型 + Phase 2)。
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-bound.ts — SESSION 绑定持久化(serenity/bound 会话事件)
|
|
3
|
+
*
|
|
4
|
+
* 目标(S142 用户拍板,方案 v1.0):让「dsh 会话(载体)↔ SESSION(宁静号轨迹)」
|
|
5
|
+
* 的绑定坚固——LLM 不能因智力因素(幻觉/理解偏差)在过程中或 session_rebuild 后
|
|
6
|
+
* 自行更换 SESSION。
|
|
7
|
+
*
|
|
8
|
+
* 机制(DSH 原生,零改 harness):
|
|
9
|
+
* `SessionEventMap` 是 merge-extensible(先例:dsh-compaction 扩展 compaction/*;
|
|
10
|
+
* dsp 已在 append compaction/prune)——插件可 declare module 自定义事件,
|
|
11
|
+
* `Session.append(type, data)` 将其写入会话 append-only 日志,随 session.jsonl
|
|
12
|
+
* 持久化落盘,进程重启后 snapshotEvents() 可完整读取。
|
|
13
|
+
*
|
|
14
|
+
* 权威绑定 = 会话日志中**最后一条** `serenity/bound`(append-only latest-wins)。
|
|
15
|
+
* 本事件是 log-only 元数据(无 surfaceOp——永不上模型可见面,不进系统提示词)。
|
|
16
|
+
*
|
|
17
|
+
* 编码无关(U4):绑定锚 = **完整 AGENT_SESSIONS 目录名**(磁盘唯一存在),
|
|
18
|
+
* sessionId(S###/apaas-xxx/自定义)仅是派生展示字段——绝不假设 S 前缀。
|
|
19
|
+
*/
|
|
20
|
+
import type { Session } from '@deepseek-ai/dsh-session';
|
|
21
|
+
import type { SessionEventMap } from '@deepseek-ai/dsh-session/types';
|
|
22
|
+
declare module '@deepseek-ai/dsh-session/types' {
|
|
23
|
+
interface SessionEventMap {
|
|
24
|
+
/**
|
|
25
|
+
* Serenity SESSION binding — authoritative; appended on every binding change.
|
|
26
|
+
* Code-agnostic (keyed by full dir name), log-only (no surfaceOp).
|
|
27
|
+
*/
|
|
28
|
+
'serenity/bound': {
|
|
29
|
+
/** Full AGENT_SESSIONS dir name — the ONLY hard identity. */
|
|
30
|
+
dirName: string;
|
|
31
|
+
/** SESSION.md absolute path. */
|
|
32
|
+
mdPath: string;
|
|
33
|
+
/** Display code when parseable (S142 / apaas-26116 / custom); informational. */
|
|
34
|
+
sessionId?: string;
|
|
35
|
+
/** Why this binding record was written. */
|
|
36
|
+
action: 'activate' | 'switch' | 'create' | 'rebuild' | 'reconcile' | 'release';
|
|
37
|
+
/** Epoch ms. */
|
|
38
|
+
at: number;
|
|
39
|
+
/** Optional reason (e.g. 'auto from title'). */
|
|
40
|
+
note?: string;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export type SessionBoundAction = SessionEventMap['serenity/bound']['action'];
|
|
45
|
+
export interface SessionBoundRecord {
|
|
46
|
+
dirName: string;
|
|
47
|
+
mdPath: string;
|
|
48
|
+
sessionId?: string;
|
|
49
|
+
action: SessionBoundAction;
|
|
50
|
+
at: number;
|
|
51
|
+
note?: string;
|
|
52
|
+
}
|
|
53
|
+
/** 事件 type 常量(声明 + 读取共用) */
|
|
54
|
+
export declare const SESSION_BOUND_EVENT: "serenity/bound";
|
|
55
|
+
/**
|
|
56
|
+
* 读取会话日志中**最后一条** `serenity/bound`(权威绑定,latest-wins)。
|
|
57
|
+
* 尾到头扫描(时间序最新在后);无 bound 事件返回 null。
|
|
58
|
+
* 纯逻辑零 DSH 依赖(经 sessionEvents helper 读 snapshotEvents/events 兜底)。
|
|
59
|
+
*/
|
|
60
|
+
export declare function readLastBound(session: unknown): SessionBoundRecord | null;
|
|
61
|
+
/**
|
|
62
|
+
* 判定会话日志是否已有任何 bound 事件(供 reconcile 判定:无绑定才标题升级)。
|
|
63
|
+
*/
|
|
64
|
+
export declare function hasAnyBound(session: unknown): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* append 一条 `serenity/bound` 绑定事件(log-only,无 surfaceOp——元数据不进模型可见面)。
|
|
67
|
+
* @param session 目标 dsh 会话(真实 Session;append 泛型经内部断言兼容——类型由
|
|
68
|
+
* `serenity/bound` 声明面保证,append 接受已声明事件)
|
|
69
|
+
* @param action 绑定动作
|
|
70
|
+
* @param rec 绑定记录(dirName + mdPath 必填;sessionId 可选展示码)
|
|
71
|
+
* @returns 是否成功(append 抛错/会话不可用时 false——绑定失败不阻断主流程)
|
|
72
|
+
*/
|
|
73
|
+
export declare function appendBound(session: Session | {
|
|
74
|
+
append: (type: string, data: unknown) => unknown;
|
|
75
|
+
} | null | undefined, action: SessionBoundAction, rec: {
|
|
76
|
+
dirName: string;
|
|
77
|
+
mdPath: string;
|
|
78
|
+
sessionId?: string;
|
|
79
|
+
note?: string;
|
|
80
|
+
}): boolean;
|
package/lib/session-ops.d.ts
CHANGED
|
@@ -117,6 +117,19 @@ export declare function parseSessionContextFromEvents(events: readonly unknown[]
|
|
|
117
117
|
/** 从文本提取 SESSION.md 路径(use 上下文 / 重建锚点规范行通用;无匹配返回 null)。
|
|
118
118
|
* 同行已知尾注(如系统提示词 Session 块的 persistent-body 注释)剥除——规范行只取路径本体。 */
|
|
119
119
|
export declare function extractSessionMdPathFromText(text: string): string | null;
|
|
120
|
+
/**
|
|
121
|
+
* 从 dsh 会话标题解析 SESSION 目录(编码无关 best-match,U3/U4)——
|
|
122
|
+
* 标题不假设 S### 前缀(CCC 可自定义编码:apaas-xxx / P### / 完整目录名等)。
|
|
123
|
+
* 匹配优先级(全部对 AGENT_SESSIONS 现有目录 best-match,不猜):
|
|
124
|
+
* ① 标题即完整目录名(含日期前缀 `YYYY-MM-DD--`)→ 精确命中
|
|
125
|
+
* ② 标题含 `--<code>--` 段(如完整目录名被截断为 `<code>-日期-概括` 前段)→
|
|
126
|
+
* 按 code 段匹配:取标题首 token(`-` 前),与各目录 `--<code>--`/`--<code>` 尾段比对
|
|
127
|
+
* ③ 唯一模糊子串匹配(多个则返回 null 防误猜)
|
|
128
|
+
* @param title dsh 会话标题(如 `S142-2026-08-24-概括` / `apaas-26116-…` / 完整目录名)
|
|
129
|
+
* @param sessionsDir AGENT_SESSIONS 绝对路径
|
|
130
|
+
* @returns 命中目录的绝对路径(SESSION.md);无/歧义返回 null
|
|
131
|
+
*/
|
|
132
|
+
export declare function resolveSessionByTitle(title: string, sessionsDir: string): string | null;
|
|
120
133
|
/**
|
|
121
134
|
* 约定回退(v1.24.11):AGENT_SESSIONS 下最新修改的**未完成**会话的 SESSION.md。
|
|
122
135
|
* readAllSessions 已按「未完成优先 + mtime 降序」排序 → 首个未完成且含 SESSION.md 即最新活动。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.29.
|
|
3
|
+
"version": "1.29.1",
|
|
4
4
|
"description": "宁静号 ACC harness — Native Cordis 插件(DeepSeek Harness 运行时)。真实 DSH 工具注册(cc_fs/session/acc_msm 等 9 工具)+ 拦截缝机械约束(safe-mode/路径守卫/会话落盘)+ 系统提示词注入(ACC/CCE/Constraints/SKILL/Session 五块)。适配 DSH 公开版(deepseek-ai/deepseek-harness 0.1.0-rc)。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|