@fanchao8609/agent_brain_sync 1.9.5 → 1.9.7
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/hooks/abs.opencode.ts +15 -86
- package/hooks/abs.pi.ts +25 -165
- package/package.json +1 -1
- package/skill/abs-agent-brain-sync/SKILL.md +6 -4
- package/src/lint.js +327 -0
- package/src/query.js +139 -0
- package/src/store.js +31 -501
package/hooks/abs.opencode.ts
CHANGED
|
@@ -3,25 +3,16 @@
|
|
|
3
3
|
* 纯触发: 生命周期事件 → 技术日志一行 (~/.abs/log/hooks.log, ABS_LOG_DIR 可覆盖)。fire-and-forget。
|
|
4
4
|
* 纪律: hook 事件只进技术日志, 不进图谱 log.md (log.md 只收工作成果沉淀, 与 event.sh 同纪律)。
|
|
5
5
|
* Opencode 事件: session.created / session.idle / session.deleted (idle = 每轮结束, 对应 pi 的 agent_end)。
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* 旧设计只在 idle(轮末) 提醒收尾 —— 那时用户已想结束, AI 只想收尾不想登记。
|
|
10
|
-
* 故 tool.execute.after 检测到**项目文件**写入即注入登记提示(不等 idle):
|
|
11
|
-
* · 频繁没关系 —— 每次写都提醒(用户明确要求)
|
|
12
|
-
* · 判据 = 整个项目, 但**排除 .brain/ 自身**(写图谱是记录行为, 不是任务)
|
|
13
|
-
* · 文案明说「纯讨论可跳过」—— 不逼 AI 造任务
|
|
6
|
+
* 收尾注入已删除(2026-09-18):曾用 session.idle + promptAsync 推一条收尾提醒, 用户实报
|
|
7
|
+
* 「每次干活干一会就出来, 任务就中断了」。根因同 pi 侧: 主动往对话里插 turn = 打断,
|
|
8
|
+
* 不是频率问题(2026-09-15 删登记注入时已得出同一结论)。保留 session.idle:seen 埋点。
|
|
14
9
|
*/
|
|
15
|
-
import { appendFile, mkdir,
|
|
10
|
+
import { appendFile, mkdir, stat } from "node:fs/promises"
|
|
16
11
|
import { homedir } from "node:os"
|
|
17
|
-
import { join
|
|
18
|
-
const server = async ({
|
|
19
|
-
//
|
|
20
|
-
let wroteFiles = false
|
|
21
|
-
let nudged = false
|
|
12
|
+
import { join } from "node:path"
|
|
13
|
+
const server = async ({ directory }) => {
|
|
14
|
+
// 本会话首次 idle 是否已埋点
|
|
22
15
|
let idleSeen = false
|
|
23
|
-
// 当前 session id(由事件里拿)—— 收尾注入需要它找到会话。
|
|
24
|
-
let currentSessionID = ""
|
|
25
16
|
async function logHook(evt) {
|
|
26
17
|
try {
|
|
27
18
|
const dir = process.env.ABS_LOG_DIR || join(homedir(), ".abs", "log")
|
|
@@ -39,94 +30,32 @@ const server = async ({ client, directory }) => {
|
|
|
39
30
|
try { const st = await stat(join(d, ".brain")); return st.isDirectory() ? join(d, ".brain") : null } catch { return null }
|
|
40
31
|
}
|
|
41
32
|
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
// 三宿主(pi/CC/opencode)判据必须一致 —— 见 [[hook-throttle-alignment]]。
|
|
47
|
-
async function loggedToday(brain) {
|
|
48
|
-
try {
|
|
49
|
-
const txt = await readFile(join(brain, "log.md"), "utf8")
|
|
50
|
-
const d = new Date()
|
|
51
|
-
const pad = (n) => String(n).padStart(2, "0")
|
|
52
|
-
const today = d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate())
|
|
53
|
-
return new RegExp("^## \\[" + today + " \\d{2}:\\d{2}\\] (?:\\[[^\\]]+\\]\\] )?dev \\|", "m").test(txt)
|
|
54
|
-
} catch { return false }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const TEARDOWN_MSG =
|
|
59
|
-
"[abs] 本会话改过文件,.brain/ 今日无记录。\n" +
|
|
60
|
-
"这条是信息不是命令:该登记/沉淀就登记,没有可沉淀的直接回一句「无可沉淀」,不用凑。\n" +
|
|
61
|
-
"先看任务:本会话做完的事有没有进看板?(abs todo / abs todo done <id>;干到一半 abs todo note <id> --note 断点)\n" +
|
|
62
|
-
"再看沉淀:经验/坑 → abs note \"...\";成果流水 → abs log \"...\""
|
|
63
|
-
|
|
64
|
-
// bash 里只跑查询类命令不算改文件 (与 pi 侧 READONLY_CMD 同义, 但生成代码里要写进模板串)
|
|
65
|
-
const READONLY_CMD = /^\s*(ls|cat|grep|rg|find|head|tail|wc|git\s+(status|log|diff|show|branch)|pwd|which|echo|node\s+-v|npm\s+(ls|view)|curl)\b/
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
33
|
+
// 收尾注入已停用(2026-09-18):promptAsync 会在每轮 idle 抢一个 turn,用户实报
|
|
34
|
+
// 「每次干活干一会就出来, 任务就中断了」。wroteFiles / nudged / READONLY_CMD /
|
|
35
|
+
// tool.execute.after / loggedToday 随注入一起删除(注入没了,它们已无消费方)。
|
|
36
|
+
// 保留 session.idle:seen 埋点:区分「事件没触发」与「被守卫拦下」。
|
|
70
37
|
return {
|
|
71
|
-
// 观测真实写操作: write/edit/patch 类工具成功即标记。
|
|
72
|
-
// 坑: 曾漏掉 bash —— opencode 里很多修改是经 bash(heredoc/sed) 完成的, 纯 bash 会话
|
|
73
|
-
// 永远不置位 wroteFiles, 于是收尾提醒静默不发。与 pi 侧 WRITE_TOOLS 保持一致。
|
|
74
|
-
"tool.execute.after": async ({ tool, args }) => {
|
|
75
|
-
const t = String(tool || "").toLowerCase()
|
|
76
|
-
const a = args || {}
|
|
77
|
-
if (["write", "edit", "patch", "multiedit", "apply_patch"].includes(t)) {
|
|
78
|
-
wroteFiles = true
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
|
-
// bash 里只有非只读命令算改文件(ls/cat/git status 之类不算)
|
|
82
|
-
if (t === "bash") {
|
|
83
|
-
const cmd = String(a.command || a.cmd || "")
|
|
84
|
-
if (cmd && !READONLY_CMD.test(cmd)) wroteFiles = true
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
|
|
88
38
|
event: async ({ event }) => {
|
|
89
39
|
const type = event && event.type
|
|
90
40
|
if (!type) return
|
|
91
41
|
if (type === "session.created" || type === "session.deleted") {
|
|
92
42
|
await logHook(type)
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
if (sid) currentSessionID = String(sid)
|
|
96
|
-
// 新会话 = 重置节流状态。坑(2026-09-13 pi 侧同构实测): 状态声明在 server 工厂闭包里,
|
|
97
|
-
// 同一进程的第二个会话会继承上个会话的 true → 永久不再提醒(后半场全部静默)。
|
|
43
|
+
// 新会话 = 重置埋点状态。坑(2026-09-13 pi 侧同构实测): 状态声明在 server 工厂闭包里,
|
|
44
|
+
// 同一进程的第二个会话会继承上个会话的 true → 永久不再留痕。
|
|
98
45
|
// pi 侧在 session_start 里重置; opencode 的对应事件就是 session.created。
|
|
99
46
|
if (type === "session.created") {
|
|
100
|
-
wroteFiles = false
|
|
101
|
-
nudged = false
|
|
102
47
|
idleSeen = false
|
|
103
48
|
}
|
|
104
49
|
return
|
|
105
50
|
}
|
|
106
|
-
if (type !== "session.idle") return // idle = 每轮结束, 不记日志(太吵),
|
|
107
|
-
const sid0 = event.properties && event.properties.sessionID
|
|
108
|
-
if (sid0) currentSessionID = String(sid0)
|
|
51
|
+
if (type !== "session.idle") return // idle = 每轮结束, 不记日志(太吵), 只做可观测性埋点
|
|
109
52
|
const cwd = directory || process.cwd()
|
|
110
53
|
const brain = await findBrain(cwd)
|
|
111
|
-
// 可观测性: 首次 idle
|
|
54
|
+
// 可观测性: 首次 idle 留一行痕(否则无法区分“事件没触发”与“被守卫拦下”)
|
|
112
55
|
if (!idleSeen) {
|
|
113
56
|
idleSeen = true
|
|
114
57
|
await logHook(`session.idle:seen cwd=${cwd} brain=${brain || 'none'}`)
|
|
115
58
|
}
|
|
116
|
-
if (nudged || !wroteFiles) return
|
|
117
|
-
const sessionID = event.properties && event.properties.sessionID
|
|
118
|
-
if (!sessionID) return
|
|
119
|
-
if (!brain) return // 无图谱=不在这项目沉淀, 不打扰
|
|
120
|
-
if (await loggedToday(brain)) return // 今天已收尾过
|
|
121
|
-
nudged = true
|
|
122
|
-
await logHook("session.idle:teardown-nudge")
|
|
123
|
-
try {
|
|
124
|
-
await client.session.promptAsync({
|
|
125
|
-
path: { id: sessionID },
|
|
126
|
-
query: { directory: cwd },
|
|
127
|
-
body: { parts: [{ type: "text", text: TEARDOWN_MSG }] },
|
|
128
|
-
})
|
|
129
|
-
} catch {} // 注入失败不阻塞宿主
|
|
130
59
|
},
|
|
131
60
|
}
|
|
132
61
|
}
|
package/hooks/abs.pi.ts
CHANGED
|
@@ -4,49 +4,22 @@
|
|
|
4
4
|
* 纪律: hook 事件只进技术日志, 不进图谱 log.md (log.md 只收工作成果沉淀, 与 event.sh 同纪律)。
|
|
5
5
|
* Pi 事件: session_start / session_shutdown (对应 host hook 的 SessionStart/SessionEnd)。
|
|
6
6
|
* session_shutdown 额外触发 abs wrapup: 把当前项目未完成任务快照到 wrapup.log (跨会话收尾保险)。
|
|
7
|
-
* agent_end
|
|
8
|
-
* (被动记日志不够 —— 没人提醒就不会有人收尾)。每会话最多一次, 且已收尾后不再打扰。
|
|
7
|
+
* agent_end 只留可观测性埋点(agent_end:seen), 不再向对话注入任何东西。
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* 已删除(2026-09-15):曾加过一个「写文件即任务开始 → 每轮注入登记提醒」的功能。
|
|
17
|
-
* 已发布到 1.8.0 后用户实报「pi 里一直报 Follow-up」,判 C 整个删掉。
|
|
18
|
-
* 教训:每次改文件就往对话里插一句话,本身就是设计错误,不是频率问题 ——
|
|
19
|
-
* 改守卫(从每轮一次改成每会话一次)治不了它。别再加回来。
|
|
9
|
+
* 已删除(2026-09-15):「写文件即任务开始 → 每轮注入登记提醒」。实报「pi 里一直报 Follow-up」。
|
|
10
|
+
* 已删除(2026-09-18):收尾注入本身(agent_end + sendUserMessage deliverAs=followUp)。
|
|
11
|
+
* 实报「每次干活干一会出来, 任务就中断了」。两次同一个根因:
|
|
12
|
+
* **往对话里插一句话本身就是设计错误,不是频率问题** —— 改守卫(每轮→每会话)治不了它。
|
|
13
|
+
* 别再加回来。素材锚点(before_agent_start/turn_end)随注入一起删除, 已无消费方。
|
|
20
14
|
*/
|
|
21
15
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
22
|
-
import { appendFile, mkdir,
|
|
16
|
+
import { appendFile, mkdir, stat } from "node:fs/promises"
|
|
23
17
|
import { homedir } from "node:os"
|
|
24
|
-
import { join
|
|
18
|
+
import { join } from "node:path"
|
|
25
19
|
import { spawn } from "node:child_process"
|
|
26
20
|
|
|
27
21
|
const ABS_BIN = "@@ABS_BIN@@"
|
|
28
22
|
|
|
29
|
-
// 素材累积锚点:会话内存里累积,收尾时拼进注入提示。**不落盘、不 spawn、不然上下文**。
|
|
30
|
-
// 预算:只留最近几条(收尾回忆只需线索,不需全史)。
|
|
31
|
-
const NOTES_MAX = 12
|
|
32
|
-
const NOTE_LEN = 120
|
|
33
|
-
const sessionNotes: string[] = []
|
|
34
|
-
|
|
35
|
-
function addNote(kind: string, text: string): void {
|
|
36
|
-
const clean = String(text ?? "").replace(/\s+/g, " ").trim().slice(0, NOTE_LEN)
|
|
37
|
-
if (!clean) return
|
|
38
|
-
sessionNotes.push(`${kind}: ${clean}`)
|
|
39
|
-
if (sessionNotes.length > NOTES_MAX) sessionNotes.shift()
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** 把本会话素材拼成一段(收尾注入用)。无素材则空字符串。
|
|
43
|
-
* 用途:让 AI 据线索**归纳**话题,而不是凭记忆回想整场对话(那正是 7% 记录率的原因)。 */
|
|
44
|
-
function notesBlock(notes: string[]): string {
|
|
45
|
-
if (!notes.length) return ""
|
|
46
|
-
return " 本会话素材(hook 机械记录,供归纳)\n" +
|
|
47
|
-
notes.map((n) => " · " + n).join("\n") + "\n"
|
|
48
|
-
}
|
|
49
|
-
|
|
50
23
|
async function logHook(evt: string): Promise<void> {
|
|
51
24
|
const dir = process.env.ABS_LOG_DIR || join(homedir(), ".abs", "log")
|
|
52
25
|
const d = new Date()
|
|
@@ -68,81 +41,30 @@ function snapshotWrapup(cwd: string): void {
|
|
|
68
41
|
} catch {} // fire-and-forget
|
|
69
42
|
}
|
|
70
43
|
|
|
71
|
-
// ---- 收尾注入判定 ----
|
|
72
|
-
// 真改过文件的工具(read/grep/ls 不算, 那些不产生可沉淀的产出)
|
|
73
|
-
const WRITE_TOOLS = new Set(["write", "edit", "multi_edit", "apply_patch", "bash"])
|
|
74
|
-
// bash 里只跑查询类命令不算改文件
|
|
75
|
-
const READONLY_CMD = /^\s*(ls|cat|grep|rg|find|head|tail|wc|git\s+(status|log|diff|show|branch)|pwd|which|echo|node\s+-v|npm\s+(ls|view)|curl)\b/
|
|
76
|
-
|
|
77
|
-
function hasWriteWork(toolResults: any[]): boolean {
|
|
78
|
-
for (const r of toolResults || []) {
|
|
79
|
-
const name = String(r?.toolName || "")
|
|
80
|
-
if (!WRITE_TOOLS.has(name)) continue
|
|
81
|
-
if (r?.isError) continue
|
|
82
|
-
if (name === "bash") {
|
|
83
|
-
const cmd = String(r?.input?.command ?? r?.args?.command ?? "")
|
|
84
|
-
if (READONLY_CMD.test(cmd)) continue
|
|
85
|
-
}
|
|
86
|
-
return true
|
|
87
|
-
}
|
|
88
|
-
return false
|
|
89
|
-
}
|
|
90
|
-
|
|
91
44
|
/** 定位当前项目的 .brain/:只看 cwd 本身,不向上搜索(防爬到家目录图谱)。 */
|
|
92
45
|
async function findBrain(cwd: string): Promise<string | null> {
|
|
93
46
|
const d = cwd || process.cwd()
|
|
94
47
|
try { const st = await stat(join(d, ".brain")); return st.isDirectory() ? join(d, ".brain") : null } catch { return null }
|
|
95
48
|
}
|
|
96
49
|
|
|
97
|
-
/**
|
|
98
|
-
* .brain/log.md 今天有记录吗? 有=已收尾, 不再打扰。
|
|
99
|
-
* 必须匹配条目头 '## [YYYY-MM-DD HH:MM]' —— 裸日期 substring 会被正文里任意一处
|
|
100
|
-
* 今天的日期误命中, 导致"已收尾"误判、nudge 永不触发。
|
|
101
|
-
*/
|
|
102
|
-
async function loggedToday(brain: string): Promise<boolean> {
|
|
103
|
-
try {
|
|
104
|
-
const txt = await readFile(join(brain, "log.md"), "utf8")
|
|
105
|
-
const d = new Date()
|
|
106
|
-
const pad = (n: number) => String(n).padStart(2, "0")
|
|
107
|
-
const today = d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate())
|
|
108
|
-
// 只认**工作成果**条目(kind=dev),不能只看“今天有没有行”。
|
|
109
|
-
// 坑(2026-09-13 实测):`abs note` 也写 log.md(kind=note),
|
|
110
|
-
// 旧判据 (^## [今天 HH:MM]) 把“沉淀了一条经验”当成“今天已收尾”→
|
|
111
|
-
// 整天不再提醒 → 会话后半场全部漏登。
|
|
112
|
-
// 行格式: `## [YYYY-MM-DD HH:MM] [[name]] dev | 内容`。
|
|
113
|
-
return new RegExp("^## \\[" + today + " \\d{2}:\\d{2}\\] (?:\\[\\[[^\\]]+\\]\\] )?dev \\|", "m").test(txt)
|
|
114
|
-
} catch { return false }
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
/** 收尾注入的节流状态。
|
|
50
|
+
/** agent_end 埋点状态。
|
|
119
51
|
*
|
|
120
52
|
* 为何在**模块级**而不在 absPiHook() 里(2026-09-17 实测):扩展会被重复注册
|
|
121
|
-
* (session_start 11 分钟内触发 8 次),每次注册都新建一份闭包 → flag
|
|
122
|
-
*
|
|
53
|
+
* (session_start 11 分钟内触发 8 次),每次注册都新建一份闭包 → flag 不共享。
|
|
54
|
+
* 模块级状态在同一进程内只有一份,注册多少次都共享。
|
|
123
55
|
*
|
|
124
|
-
*
|
|
125
|
-
* teardownInFlight = 正在判定中(抢在 await 之前置位,堵住并发重入)
|
|
126
|
-
* agentEndSeen = 本会话已留过 seen 痕(可观测性,同样每会话一次)
|
|
56
|
+
* agentEndSeen = 本会话已留过 seen 痕(可观测性,每会话一次)
|
|
127
57
|
*/
|
|
128
|
-
let teardownNudged = false
|
|
129
|
-
let teardownInFlight = false
|
|
130
58
|
let agentEndSeen = false
|
|
131
59
|
|
|
132
|
-
/**
|
|
133
|
-
* 不重置 → 同进程第二个会话继承 true
|
|
60
|
+
/** 会话边界重置埋点状态。
|
|
61
|
+
* 不重置 → 同进程第二个会话继承 true 永久不留痕(2026-09-13 实测过的坑)。 */
|
|
134
62
|
function resetThrottle(): void {
|
|
135
|
-
teardownNudged = false
|
|
136
|
-
teardownInFlight = false
|
|
137
63
|
agentEndSeen = false
|
|
138
64
|
}
|
|
139
65
|
|
|
140
66
|
export default function absPiHook(pi: ExtensionAPI): void {
|
|
141
|
-
//
|
|
142
|
-
// 因为**扩展会被重复注册**:实测 session_start 在 11 分钟内触发 8 次
|
|
143
|
-
// (2026-09-17 00:07:58 一口气 3 次)。注册几份就得到几份独立闭包,
|
|
144
|
-
// 各自的 flag 彼此看不见 → “每会话一次”变成“每实例一次” → 实测同会话 5 分钟注入 6 次。
|
|
145
|
-
// 模块级状态在同一进程内共享,注册多少次都只有一份。
|
|
67
|
+
// 埋点状态同上,故意声明在**模块级**(不在被反复调用的工厂函数里)。
|
|
146
68
|
resetThrottle()
|
|
147
69
|
|
|
148
70
|
pi.on("session_start", (event: any, _ctx: any) => {
|
|
@@ -150,73 +72,19 @@ export default function absPiHook(pi: ExtensionAPI): void {
|
|
|
150
72
|
return logHook("session_start").catch(() => {})
|
|
151
73
|
})
|
|
152
74
|
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
pi.on("before_agent_start", async (event: any, _ctx: any) => {
|
|
156
|
-
addNote("user", String(event?.prompt || ""))
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
// 每轮结束 = 这轮干了什么(改了哪些文件)。机械事实,供收尾时回忆。
|
|
160
|
-
pi.on("turn_end", async (event: any, _ctx: any) => {
|
|
161
|
-
const files = new Set<string>()
|
|
162
|
-
for (const r of event?.toolResults || []) {
|
|
163
|
-
const name = String(r?.toolName || "")
|
|
164
|
-
if (!WRITE_TOOLS.has(name)) continue
|
|
165
|
-
if (r?.isError) continue
|
|
166
|
-
const f = r?.input?.file_path ?? r?.args?.file_path ?? r?.input?.path ?? r?.args?.path
|
|
167
|
-
if (f) files.add(String(f))
|
|
168
|
-
else if (name === "bash") {
|
|
169
|
-
const cmd = String(r?.input?.command ?? r?.args?.command ?? "")
|
|
170
|
-
if (!READONLY_CMD.test(cmd)) files.add("bash: " + cmd.slice(0, 60))
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
if (files.size) addNote("tool", [...files].join(", "))
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
// 收尾注入: 每个会话最多一次, 避免反复打扰。
|
|
75
|
+
// 收尾注入已停用(2026-09-18):sendUserMessage(deliverAs:"followUp") 会在每轮
|
|
76
|
+
// agent_end 抢一个 follow-up turn,用户实报「每次干活干一会出来, 任务就中断了」。
|
|
177
77
|
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (teardownNudged || teardownInFlight) return
|
|
186
|
-
teardownInFlight = true
|
|
187
|
-
// 从这里往下任何提前 return 都必须解除 in-flight(否则本次会话永久不再提醒)。
|
|
188
|
-
const release = () => { teardownInFlight = false }
|
|
78
|
+
// 为何是删除而非调参:这跟 2026-09-15 删掉「写文件即注入登记提醒」是同一个错
|
|
79
|
+
// ——「往对话里插一句话」本身就不是频率问题(见文件头)。每轮弹、每会话弹、
|
|
80
|
+
// 每项目弹,都还是在打断用户。故不再有 teardownNudged/inFlight/loggedToday 判据。
|
|
81
|
+
// 只保留 agent_end:seen 埋点:区分「事件没触发」与「被守卫拦下」。
|
|
82
|
+
pi.on("agent_end", async (_event: any, ctx: any) => {
|
|
83
|
+
if (agentEndSeen) return
|
|
84
|
+
agentEndSeen = true
|
|
189
85
|
const cwd = (ctx && ctx.cwd) || process.cwd()
|
|
190
86
|
const brain = await findBrain(cwd)
|
|
191
|
-
|
|
192
|
-
// 没有它就无法区分三种状态: ①事件没触发 ②触发了但被守卫拦下 ③真注入了。
|
|
193
|
-
if (!agentEndSeen) {
|
|
194
|
-
agentEndSeen = true
|
|
195
|
-
// 带上 cwd 与命中的 brain —— 否则事后无法解释“为何这条 nudge 会出现”
|
|
196
|
-
await logHook(`agent_end:seen cwd=${cwd} brain=${brain || 'none'}`).catch(() => {})
|
|
197
|
-
}
|
|
198
|
-
if (!brain) return release() // 无图谱=不在这项目沉淀, 不打扰
|
|
199
|
-
if (!hasWriteWork(event?.messages ? collectToolResults(event.messages) : [])) return release()
|
|
200
|
-
// 今日已收尾 → 只在**本会话还没真正干事**时才静默。
|
|
201
|
-
// 旧行为:今天 log 有一行就整天闭口 —— 于是收尾之后的产出全部没人提醒。
|
|
202
|
-
// 注意不能用 sessionNotes.length 当判据:用户每说一句话就会 push 一条,
|
|
203
|
-
// 那会让 nudge 每轮都触发(噪音)。判据保持「今天已收尾」但配合下面的笔记消费。
|
|
204
|
-
if (await loggedToday(brain)) return release()
|
|
205
|
-
teardownNudged = true
|
|
206
|
-
teardownInFlight = false
|
|
207
|
-
await logHook("agent_end:teardown-nudge").catch(() => {})
|
|
208
|
-
// 素材交给 AI 后清空:同一会话再触发时不该重复喂旧料。
|
|
209
|
-
const notes = sessionNotes.splice(0, sessionNotes.length)
|
|
210
|
-
try {
|
|
211
|
-
pi.sendUserMessage(
|
|
212
|
-
"[abs] 本会话改过文件,.brain/ 今日无记录。\n" +
|
|
213
|
-
"这条是信息不是命令:该登记/沉淀就登记,没有可沉淀的直接回一句「无可沉淀」,不用凑。\n" +
|
|
214
|
-
"先看任务:本会话做完的事有没有进看板?(abs todo / abs todo done <id>;干到一半 abs todo note <id> --note 断点)\n" +
|
|
215
|
-
"再看沉淀:经验/坑 → abs note \"...\";成果流水 → abs log \"...\"" +
|
|
216
|
-
notesBlock(notes),
|
|
217
|
-
{ deliverAs: "followUp" },
|
|
218
|
-
)
|
|
219
|
-
} catch {}
|
|
87
|
+
await logHook(`agent_end:seen cwd=${cwd} brain=${brain || 'none'}`).catch(() => {})
|
|
220
88
|
})
|
|
221
89
|
|
|
222
90
|
pi.on("session_shutdown", (_e: any, ctx: any) => {
|
|
@@ -225,12 +93,4 @@ export default function absPiHook(pi: ExtensionAPI): void {
|
|
|
225
93
|
})
|
|
226
94
|
}
|
|
227
95
|
|
|
228
|
-
/** agent_end 的 event.messages 里翻出 toolResult 消息。 */
|
|
229
|
-
function collectToolResults(messages: any[]): any[] {
|
|
230
|
-
const out: any[] = []
|
|
231
|
-
for (const m of messages || []) {
|
|
232
|
-
if (m && m.role === "toolResult") out.push(m)
|
|
233
|
-
}
|
|
234
|
-
return out
|
|
235
|
-
}
|
|
236
96
|
@@MARK@@
|
package/package.json
CHANGED
|
@@ -305,10 +305,12 @@ abs todo archive # 一条命令搞定,无需手工搬
|
|
|
305
305
|
> Stop 时 hook 把「未完成任务 + 断点」快照进 `~/.abs/log/wrapup.log`(`abs wrapup`,机械幂等),
|
|
306
306
|
> 下会话 `abs load` 会自动把滞留顶到顶部(`⏳ 上会话滞留`)—— 所以收尾不靠自觉,是开场被强制接上。
|
|
307
307
|
>
|
|
308
|
-
>
|
|
309
|
-
> `
|
|
310
|
-
>
|
|
311
|
-
>
|
|
308
|
+
> **不再主动注入**(2026-09-18):pi 扩展曾在 `agent_end` 往对话里推一条 `[abs 收尾提醒]`
|
|
309
|
+
> (Claude/Codex 靠 `Stop` 事件、opencode 靠 `session.idle` 达成同样效果)。
|
|
310
|
+
> 已全部删除:用户实报「每次干活干一会就出来, 任务就中断了」。**主动往对话里插一个 turn
|
|
311
|
+
> 就是打断,不是频率问题** —— 每轮弹、每会话弹都还是打断。收尾靠**你自己在该收尾时走**,
|
|
312
|
+
> 以及 `wrapup.log` + `abs load` 的机制兵底(开场强制接上滞留任务)。
|
|
313
|
+
> 挂钩只在 `~/.abs/log/hooks.log` 留痕,不再往对话里说话。
|
|
312
314
|
|
|
313
315
|
当用户**明确说要收尾/结束/切别的事**时,按下面走(不是每个词都触发,见开头总则):
|
|
314
316
|
|