@muzi3/herdr 0.3.1 → 0.3.3
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/README.md +1 -1
- package/extensions/herdr-agent-state/index.ts +52 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ extensions/
|
|
|
30
30
|
|
|
31
31
|
每个扩展目录的入口都叫 `index.ts`,与 pi 自己的目录式扩展约定一致:扩展目录内可以再放内部模块,`pi.extensions` 只指向入口,不会误加载旁支文件。
|
|
32
32
|
|
|
33
|
-
`herdr-agent-state/index.ts` 的目录名与 herdr 集成注入的文件同名,对齐上游时可以直接对比。内容分两段:官方集成正文(一直到文件末尾之前)与之后的「本包新增」段落。本包对官方正文只有**一处**改动:官方 `export default` 开头插入一行 `registerBlockedReporting(pi);` —— 该 default export 就是本扩展的唯一入口,blocked 上报只能挂在那里。重新同步上游时(`HERDR_INTEGRATION_VERSION` 从
|
|
33
|
+
`herdr-agent-state/index.ts` 的目录名与 herdr 集成注入的文件同名,对齐上游时可以直接对比。内容分两段:官方集成正文(一直到文件末尾之前)与之后的「本包新增」段落。本包对官方正文只有**一处**改动:官方 `export default` 开头插入一行 `registerBlockedReporting(pi);` —— 该 default export 就是本扩展的唯一入口,blocked 上报只能挂在那里。重新同步上游时(`HERDR_INTEGRATION_VERSION` 从 9 往上升)重取注入文件替换官方正文并补回那一行;`diff` 应只有三处新增、零删除,漏帖那一行会被 `tests/herdr-agent-state.test.ts` 拦下。
|
|
34
34
|
|
|
35
35
|
## 来源与许可
|
|
36
36
|
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
// managed by herdr; reinstalling or updating the integration overwrites this file.
|
|
3
3
|
// add custom hooks/plugins beside this file instead of editing it.
|
|
4
4
|
// HERDR_INTEGRATION_ID=pi
|
|
5
|
-
// HERDR_INTEGRATION_VERSION=
|
|
5
|
+
// HERDR_INTEGRATION_VERSION=9
|
|
6
6
|
// @ts-nocheck
|
|
7
7
|
//
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
// 以下为本包(@muzi3/herdr)的说明,不属于 herdr 官方正文:
|
|
10
10
|
//
|
|
11
11
|
// 1. 本文件上方内容是 herdr 官方 pi 集成正文的副本,保留是为了将来能直接与上游重新同步:
|
|
12
|
-
// 上游 `HERDR_INTEGRATION_VERSION` 从当前的
|
|
12
|
+
// 上游 `HERDR_INTEGRATION_VERSION` 从当前的 9 往上升时,重取 `herdr integration install pi`
|
|
13
13
|
// 注入的文件并替换本文件的官方段落即可(文件末尾的「本包新增」段落不受影响)。
|
|
14
14
|
// 本包对官方段落只有**一处**改动:`export default` 开头插入一行
|
|
15
15
|
// `registerBlockedReporting(pi);`(带「本包新增」标记)。因为官方的 default export 就是本
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
28
28
|
|
|
29
29
|
import net from "node:net";
|
|
30
|
+
import path from "node:path";
|
|
30
31
|
|
|
31
32
|
const HERDR_ENV = process.env.HERDR_ENV;
|
|
32
33
|
const socketPath = process.env.HERDR_SOCKET_PATH;
|
|
@@ -67,11 +68,11 @@ function sendRequestAttempt(request: unknown, timeoutMs: number): Promise<boolea
|
|
|
67
68
|
});
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
async function sendRequest(request: unknown): Promise<
|
|
71
|
+
async function sendRequest(request: unknown): Promise<boolean> {
|
|
71
72
|
if (await sendRequestAttempt(request, 500)) {
|
|
72
|
-
return;
|
|
73
|
+
return true;
|
|
73
74
|
}
|
|
74
|
-
|
|
75
|
+
return sendRequestAttempt(request, 1500);
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
type AgentState = "working" | "blocked" | "idle";
|
|
@@ -95,7 +96,10 @@ function updateSessionRef(ctx: any): void {
|
|
|
95
96
|
try {
|
|
96
97
|
const file = ctx?.sessionManager?.getSessionFile?.();
|
|
97
98
|
currentAgentSessionPath =
|
|
98
|
-
typeof file === "string" &&
|
|
99
|
+
typeof file === "string" &&
|
|
100
|
+
(path.posix.isAbsolute(file) || path.win32.isAbsolute(file))
|
|
101
|
+
? file
|
|
102
|
+
: undefined;
|
|
99
103
|
} catch {
|
|
100
104
|
currentAgentSessionPath = undefined;
|
|
101
105
|
}
|
|
@@ -165,6 +169,7 @@ function sendState(state: AgentState, message?: string, seq = nextReportSeq()):
|
|
|
165
169
|
|
|
166
170
|
let sendInFlight = false;
|
|
167
171
|
let queuedState: QueuedState | undefined;
|
|
172
|
+
let retryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
168
173
|
|
|
169
174
|
function queueState(state: AgentState, message?: string): void {
|
|
170
175
|
queuedState = { state, message, seq: nextReportSeq() };
|
|
@@ -173,6 +178,17 @@ function queueState(state: AgentState, message?: string): void {
|
|
|
173
178
|
}
|
|
174
179
|
}
|
|
175
180
|
|
|
181
|
+
function scheduleStateRetry(): void {
|
|
182
|
+
if (retryTimer) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
retryTimer = setTimeout(() => {
|
|
186
|
+
retryTimer = undefined;
|
|
187
|
+
void drainStateQueue();
|
|
188
|
+
}, 1000);
|
|
189
|
+
retryTimer.unref?.();
|
|
190
|
+
}
|
|
191
|
+
|
|
176
192
|
async function drainStateQueue(): Promise<void> {
|
|
177
193
|
if (sendInFlight) {
|
|
178
194
|
return;
|
|
@@ -183,11 +199,18 @@ async function drainStateQueue(): Promise<void> {
|
|
|
183
199
|
while (queuedState) {
|
|
184
200
|
const next = queuedState;
|
|
185
201
|
queuedState = undefined;
|
|
186
|
-
await sendState(next.state, next.message, next.seq)
|
|
202
|
+
if (await sendState(next.state, next.message, next.seq)) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (!queuedState || queuedState.seq < next.seq) {
|
|
206
|
+
queuedState = next;
|
|
207
|
+
}
|
|
208
|
+
scheduleStateRetry();
|
|
209
|
+
break;
|
|
187
210
|
}
|
|
188
211
|
} finally {
|
|
189
212
|
sendInFlight = false;
|
|
190
|
-
if (queuedState) {
|
|
213
|
+
if (queuedState && !retryTimer) {
|
|
191
214
|
void drainStateQueue();
|
|
192
215
|
}
|
|
193
216
|
}
|
|
@@ -358,6 +381,7 @@ export function registerBlockedReporting(pi) {
|
|
|
358
381
|
let raised = false;
|
|
359
382
|
let raisedLabel: string | undefined;
|
|
360
383
|
let askUser: { header?: string; question?: string } | undefined;
|
|
384
|
+
let turnActive = false;
|
|
361
385
|
|
|
362
386
|
function lower(): void {
|
|
363
387
|
if (!raised) return;
|
|
@@ -381,6 +405,10 @@ export function registerBlockedReporting(pi) {
|
|
|
381
405
|
|
|
382
406
|
pi.on("ui_prompt_start", (event) => {
|
|
383
407
|
if (!rootSession) return;
|
|
408
|
+
// 只有回合内的弹窗才意味着「agent 卡住等人回话」。回合外的 custom 多半是装饰性
|
|
409
|
+
// overlay(例如 prompt-nav 在每个 session_start 挂的非捕获导航条),用户不需要回答;
|
|
410
|
+
// 把它当 blocked 会让 pane 一开机就永久红点。
|
|
411
|
+
if (!turnActive) return;
|
|
384
412
|
// custom 提示(ask_user_question 走这条)的 title 是空的,用问题内容补 label。
|
|
385
413
|
raise(event.kind === "custom" ? (askUser?.header ?? askUser?.question) : event.title);
|
|
386
414
|
});
|
|
@@ -396,14 +424,28 @@ export function registerBlockedReporting(pi) {
|
|
|
396
424
|
// reload 会同时重载集成与本扩展,两侧计数一起归零。
|
|
397
425
|
if (ctx?.mode !== "tui") return;
|
|
398
426
|
rootSession = true;
|
|
399
|
-
|
|
400
|
-
|
|
427
|
+
turnActive = ctx?.isIdle?.() === false;
|
|
428
|
+
// 上一会话遗留的 raise 必须先显式降掉:官方段落只认计数,直接清 raised 会把
|
|
429
|
+
// blockedCount 永久留在 1,之后所有状态都被 blocked 压住(重启/换会话也治不好)。
|
|
430
|
+
lower();
|
|
401
431
|
askUser = undefined;
|
|
402
432
|
});
|
|
403
433
|
|
|
434
|
+
pi.on("agent_start", () => {
|
|
435
|
+
turnActive = true;
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
pi.on("agent_settled", () => {
|
|
439
|
+
turnActive = false;
|
|
440
|
+
// 等人回答时回合不会 settle,所以回合结束就不会有真问题待答;顺手清掉漏掉的 raise,
|
|
441
|
+
// 避免 pane 在回合结束后继续顶着红点。
|
|
442
|
+
lower();
|
|
443
|
+
});
|
|
444
|
+
|
|
404
445
|
pi.on("session_shutdown", () => {
|
|
405
446
|
lower();
|
|
406
447
|
rootSession = false;
|
|
448
|
+
turnActive = false;
|
|
407
449
|
askUser = undefined;
|
|
408
450
|
});
|
|
409
451
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muzi3/herdr",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Herdr for pi: three independent extensions (agent + blocked state reporting, /pi-hot-restart, and the herdr_overview / herdr_read / herdr_wait / herdr_prompt / herdr_run / herdr_create / herdr_agent_start / herdr_close manager tools aligned with dsh's herdr-tools plugin) plus the herdr-guide skill.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|