@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 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` 从 8 往上升)重取注入文件替换官方正文并补回那一行;`diff` 应只有三处新增、零删除,漏帖那一行会被 `tests/herdr-agent-state.test.ts` 拦下。
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=8
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` 从当前的 8 往上升时,重取 `herdr integration install pi`
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<void> {
71
+ async function sendRequest(request: unknown): Promise<boolean> {
71
72
  if (await sendRequestAttempt(request, 500)) {
72
- return;
73
+ return true;
73
74
  }
74
- await sendRequestAttempt(request, 1500);
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" && file.startsWith("/") ? file : undefined;
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
- raised = false;
400
- raisedLabel = undefined;
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.1",
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",