@dorokuma/herdsman-pi 0.14.9 → 0.15.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorokuma/herdsman-pi",
3
- "version": "0.14.9",
3
+ "version": "0.15.1",
4
4
  "description": "Pi extension bridge for Herdsman agent history.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -21,6 +21,9 @@
21
21
  "url": "git+https://github.com/dorokuma/herdsman.git",
22
22
  "directory": "packages/herdsman-pi"
23
23
  },
24
+ "scripts": {
25
+ "typecheck": "tsc --noEmit -p tsconfig.json"
26
+ },
24
27
  "pi": {
25
28
  "extensions": [
26
29
  "./src/index.ts"
@@ -34,8 +37,5 @@
34
37
  "bugs": {
35
38
  "url": "https://github.com/dorokuma/herdsman/issues"
36
39
  },
37
- "homepage": "https://github.com/dorokuma/herdsman#readme",
38
- "scripts": {
39
- "typecheck": "tsc --noEmit -p tsconfig.json"
40
- }
41
- }
40
+ "homepage": "https://github.com/dorokuma/herdsman#readme"
41
+ }
package/src/index.ts CHANGED
@@ -97,6 +97,7 @@ type DeliveredBatch = {
97
97
  };
98
98
 
99
99
  type HerdsmanState = {
100
+ autoClaimAttempted: boolean;
100
101
  client: HerdsmanDaemonClient | undefined;
101
102
  connected: boolean;
102
103
  currentScope: CurrentScope | undefined;
@@ -450,6 +451,7 @@ export function createHerdsmanPiExtension(options: ExtensionOptions = {}) {
450
451
  const wakeFilter = options.wakeFilter ?? loadWakeFilterConfig();
451
452
 
452
453
  const state: HerdsmanState = {
454
+ autoClaimAttempted: false,
453
455
  client: undefined,
454
456
  connected: false,
455
457
  currentScope: undefined,
@@ -2021,10 +2023,32 @@ export function createHerdsmanPiExtension(options: ExtensionOptions = {}) {
2021
2023
  subscriberKind: "pi",
2022
2024
  workspaceId: state.currentScope?.workspaceId ?? launchIdentity.workspaceId,
2023
2025
  })
2024
- .then((response) => {
2026
+ .then(async (rawResponse) => {
2025
2027
  state.connected = true;
2026
2028
  startKeepalive(client);
2027
- applyConnectionStateResponse(response as ConnectionStateResponse, ctx, {
2029
+ let response = rawResponse as ConnectionStateResponse;
2030
+ const currentPaneId = state.currentScope?.paneId ?? launchIdentity.paneId;
2031
+ if (
2032
+ process.env.HERDSMAN_AUTO_ON === "1" &&
2033
+ !state.autoClaimAttempted &&
2034
+ isMainOrchestratorPane(currentPaneId) &&
2035
+ !isDispatchedRoleSession(sessionRef.value, process.env)
2036
+ ) {
2037
+ state.autoClaimAttempted = true;
2038
+ if (!isLocalOwner(response) && !response.state?.owner) {
2039
+ try {
2040
+ state.roleMutationInFlight = true;
2041
+ response = (await client.request("agent.orchestrator.set", {
2042
+ enabled: true,
2043
+ })) as ConnectionStateResponse;
2044
+ } catch {
2045
+ // Ignore race if another terminal claimed the workspace concurrently.
2046
+ } finally {
2047
+ state.roleMutationInFlight = false;
2048
+ }
2049
+ }
2050
+ }
2051
+ applyConnectionStateResponse(response, ctx, {
2028
2052
  notifyReconnectLoss: true,
2029
2053
  });
2030
2054
  })
@@ -2095,6 +2119,7 @@ export function createHerdsmanPiExtension(options: ExtensionOptions = {}) {
2095
2119
 
2096
2120
  pi.on("session_start", (_event: unknown, ctx: PiContext) => {
2097
2121
  activeContext = ctx;
2122
+ state.autoClaimAttempted = false;
2098
2123
  state.subscriberId = ctx.sessionManager.getSessionId();
2099
2124
  state.sessionRef = {
2100
2125
  agent: "pi",
@@ -2438,6 +2463,17 @@ function herdrLaunchIdentity(environment: NodeJS.ProcessEnv): LaunchIdentity | u
2438
2463
  return { herdrSocketPath, paneId, workspaceId };
2439
2464
  }
2440
2465
 
2466
+ function isMainOrchestratorPane(paneId: string): boolean {
2467
+ return paneId.trim().endsWith(":p1");
2468
+ }
2469
+
2470
+ function isDispatchedRoleSession(sessionPath: string, environment: NodeJS.ProcessEnv): boolean {
2471
+ if (typeof environment.CTXMODE_ROLE === "string" && environment.CTXMODE_ROLE.trim().length > 0) {
2472
+ return true;
2473
+ }
2474
+ return sessionPath.startsWith("/tmp/herdr-role-sessions/");
2475
+ }
2476
+
2441
2477
 
2442
2478
  function stringValue(value: unknown): string | null {
2443
2479
  return typeof value === "string" && value.length > 0 ? value : null;
package/src/wake.ts CHANGED
@@ -7,15 +7,19 @@ import { DEFAULT_WAKE_FILTER_CONFIG, isUpstreamModelError, type WakeFilterConfig
7
7
  // next microtask (0ms timer) instead of waiting out a settle window. Delivery
8
8
  // latency is owned by the bounded deferral in the extension, not by this delay.
9
9
  export const WAKE_SETTLE_MS = 0;
10
- // Mirrors `PLAN_WAITING_HISTORY` in `src/observability/contracts.ts` (the string
11
- // value is the contract; duplicated here because `@dorokuma/herdsman-pi` ships
12
- // standalone `src/` without root `src/`).
10
+ // Mirrors the `STALLED_PLAN_ERRORS` set in `src/observability/contracts.ts`
11
+ // (`PLAN_WAITING_HISTORY` / `PLAN_STALLED` / `DEGRADED_PLAN_ERROR`, the last one
12
+ // spelled `"degraded"` inline below) and the copy in `src/agy/hook.ts`: the
13
+ // string value is the contract, and the reason is duplicated here because
14
+ // `@dorokuma/herdsman-pi` ships standalone `src/` without root `src/`. All three
15
+ // places must change together.
13
16
  export const PLAN_WAITING_HISTORY = "PLAN_WAITING_HISTORY";
17
+ export const PLAN_STALLED = "PLAN_STALLED";
14
18
 
15
19
  export type AgentOutcome = {
16
20
  agent: string;
17
21
  eventId: number;
18
- kind: "blocked" | "completed" | "failed";
22
+ kind: "blocked" | "completed" | "failed" | "stalled";
19
23
  name?: string | null;
20
24
  paneId: string | null;
21
25
  reason?: string;
@@ -93,8 +97,31 @@ function outcomeKind(event: AgentEventWireRecord): AgentOutcome["kind"] | undefi
93
97
  return "failed";
94
98
  }
95
99
  if (event.type === "agent.discarded") {
96
- // 观察者放弃等待也是终态失败:结果永远不会到达,必须能在编排者对话里被唤醒。
97
- // 压制/死信语义保持既有实现(上游错误抑制、pane 级 fallback 压制、seen 去重)。
100
+ const payload = asRecord(event.payload);
101
+ const agent = stringValue(payload.agent);
102
+ const isAgyOrEmpty = agent === undefined || agent === "agy";
103
+ if (isAgyOrEmpty) {
104
+ const reason = stringValue(payload.reason);
105
+ // A stalled plan has *settled*, which makes it unlike PLAN_WAITING_HISTORY
106
+ // / degraded ("still waiting, nothing happened yet"): the round's outcome is
107
+ // gone and the orchestrator has to see that. With the fallback marker it
108
+ // projects as the self-readable `stalled`; a legacy row written before the
109
+ // marker existed still wakes as a plain `failed` rather than vanishing.
110
+ // A `pi` discard never enters this branch (`agent === "pi"` fails
111
+ // `isAgyOrEmpty`), so pi's own PLAN_STALLED handling is unchanged.
112
+ if (reason === PLAN_STALLED) {
113
+ if (payload.fallbackOutcome === true) {
114
+ return "stalled";
115
+ }
116
+ return "failed";
117
+ }
118
+ if (reason === PLAN_WAITING_HISTORY || reason === "degraded") {
119
+ if (payload.fallbackOutcome === true) {
120
+ return "stalled";
121
+ }
122
+ return undefined;
123
+ }
124
+ }
98
125
  return "failed";
99
126
  }
100
127
  const payload = asRecord(event.payload);
@@ -169,7 +196,8 @@ function project(
169
196
  }
170
197
 
171
198
  const text = normalizeExcerpt(event.compactHistory?.lastAssistantMessage?.text);
172
- const reason = kind === "failed" ? normalizeExcerpt(payload.reason) : undefined;
199
+ const reason =
200
+ kind === "failed" || kind === "stalled" ? normalizeExcerpt(payload.reason) : undefined;
173
201
  // Upstream model errors are transient provider failures, not agent results:
174
202
  // they are dropped from the wake projection without an outcome (and without
175
203
  // being consumed into `seen`, so they stay visible as raw evidence).
@@ -243,6 +271,12 @@ export function formatAgentOutcomeUpdates(outcomes: AgentOutcome[]): string {
243
271
  outcome.text.length > 0 ? `\n last assistant (pre-round): ${outcome.text}` : "";
244
272
  return `- failed ${identity} ${pane}\n reason: ${reason}${body}`;
245
273
  }
274
+ if (outcome.kind === "stalled") {
275
+ const reason = outcome.reason && outcome.reason.length > 0 ? outcome.reason : "(unknown)";
276
+ const body =
277
+ outcome.text.length > 0 ? `\n last assistant (pre-round): ${outcome.text}` : "";
278
+ return `- stalled ${identity} ${pane}\n reason: ${reason} (结果未取到,需自读 pane)${body}`;
279
+ }
246
280
  const excerpt = outcome.text.length > 0 ? outcome.text : "(no assistant message)";
247
281
  return `- ${outcome.kind} ${identity} ${pane}\n last assistant: ${excerpt}\n event: ${outcome.eventId}`;
248
282
  }).join("\n");