pi-pier 0.3.0 → 0.3.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/README.md +12 -11
- package/package.json +2 -2
- package/schemas/role-manifest.schema.json +7 -1
- package/skills/pi-herdr-workflow/SKILL.md +4 -5
- package/src/ansi-text.ts +62 -11
- package/src/ask-dialog.ts +308 -0
- package/src/ask-user.ts +113 -112
- package/src/bootstrap.ts +8 -12
- package/src/compact-coordinator.ts +278 -261
- package/src/compact-economics-core.ts +77 -145
- package/src/config-catalog-core.ts +77 -197
- package/src/config-command.ts +271 -64
- package/src/dashboard-command.ts +38 -19
- package/src/efficiency-config-core.ts +287 -484
- package/src/efficiency-store.ts +16 -29
- package/src/focus-poller.ts +14 -35
- package/src/gc-core.ts +10 -36
- package/src/herdr-client.ts +82 -208
- package/src/history-store.ts +36 -23
- package/src/index-gates.ts +124 -0
- package/src/index-locks.ts +2 -7
- package/src/index-master.ts +24 -18
- package/src/index-notices.ts +28 -15
- package/src/index-pipe.ts +17 -12
- package/src/index-roles.ts +306 -0
- package/src/index-runtime.ts +2 -6
- package/src/index-worker.ts +4 -8
- package/src/index.ts +305 -564
- package/src/jev-client.ts +12 -17
- package/src/jev-core.ts +137 -87
- package/src/ledger.ts +13 -21
- package/src/lock-core.ts +28 -56
- package/src/manifest-compose.ts +35 -20
- package/src/observation-core.ts +52 -100
- package/src/pane-title.ts +19 -38
- package/src/pi-surface.ts +34 -22
- package/src/pier-options.ts +54 -50
- package/src/pipe-channel.ts +27 -59
- package/src/platform-paths.ts +19 -52
- package/src/{core → plugins}/grid-shape.ts +8 -10
- package/src/{core → plugins}/heat-plan.ts +20 -22
- package/src/plugins/observation.ts +475 -0
- package/src/plugins/subagent.ts +728 -0
- package/src/{core → plugins}/terminal.ts +167 -127
- package/src/{core → plugins}/todo.ts +61 -100
- package/src/progress-core.ts +16 -53
- package/src/reconcile-core.ts +24 -33
- package/src/reducer-core.ts +24 -60
- package/src/reducer-invoker.ts +266 -239
- package/src/renderers.ts +76 -87
- package/src/role-loader.ts +33 -28
- package/src/role-manifest.ts +23 -28
- package/src/role-state.ts +119 -0
- package/src/routing-telemetry.ts +136 -0
- package/src/runtime-policy.ts +25 -40
- package/src/session-tail.ts +65 -41
- package/src/settle-wake-core.ts +52 -21
- package/src/slim-frame.ts +25 -122
- package/src/stale-core.ts +17 -24
- package/src/storage-layout.ts +14 -43
- package/src/subagent-core.ts +253 -118
- package/src/subagent-gc.ts +109 -67
- package/src/subagent-poller.ts +367 -24
- package/src/subagent-session.ts +516 -0
- package/src/subagent-spawn.ts +530 -52
- package/src/swallow.ts +7 -17
- package/src/terminal-core.ts +125 -106
- package/src/todo-core.ts +57 -109
- package/src/todo-read-hook.ts +23 -39
- package/src/todo-reminder-core.ts +26 -51
- package/src/todo-window.ts +11 -12
- package/src/todos-service.ts +15 -35
- package/src/tool-error.ts +6 -13
- package/src/tool-gate.ts +38 -21
- package/src/vocab.ts +52 -25
- package/src/ask-multi.ts +0 -260
- package/src/config-guide.ts +0 -361
- package/src/core/observation.ts +0 -555
- package/src/core/subagent.ts +0 -633
- package/src/efficiency-config-io.ts +0 -53
- package/src/git-adapter.ts +0 -113
- package/src/notice-buffer.ts +0 -23
- package/src/subagent-execute.ts +0 -328
- package/src/subagent-git-io.ts +0 -80
- package/src/subagent-launch.ts +0 -114
- package/src/subagent-list-action.ts +0 -69
- package/src/subagent-output-action.ts +0 -104
- package/src/subagent-output-adapter.ts +0 -27
- package/src/subagent-output-core.ts +0 -263
- package/src/subagent-poll-loop.ts +0 -305
- package/src/subagent-port.ts +0 -27
- package/src/subagent-ready.ts +0 -89
- package/src/subagent-registry.ts +0 -70
- package/src/subagent-resolution.ts +0 -41
- package/src/subagent-scope.ts +0 -41
- package/src/subagent-session-io.ts +0 -230
package/src/herdr-client.ts
CHANGED
|
@@ -1,28 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Minimal herdr socket API client used inside the pi extension.
|
|
3
3
|
*
|
|
4
|
-
* Validated against herdr 0.9.1 (protocol 22
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Validated against herdr 0.9.1 (protocol 22; see WIRE.md). The request shapes pier builds are pinned
|
|
5
|
+
* to herdr's own `api schema` by test/herdr-client.test.ts — regenerate test/fixtures/herdr-contract.json
|
|
6
|
+
* and bump HERDR_PROTOCOL_EXPECTED when the server moves on:
|
|
7
7
|
* - Transport is NDJSON: request {id, method, params} → response {id, result} | {id, error:{code,message}};
|
|
8
8
|
* - **Control requests use one connection per request**; the server closes it after replying;
|
|
9
9
|
* - Windows targets are named pipes whose name is the complete socket_path with a \\.\pipe\ prefix;
|
|
10
|
-
* - pane.report_agent reports {pane_id, source, agent, state, message
|
|
11
|
-
*
|
|
12
|
-
* - pane.report_metadata reports {pane_id, source, title?, state_labels?,
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* - agent.list {} → {type:'agent_list', agents: AgentInfo[]};
|
|
16
|
-
* - agent.wait {target, until[], timeout_ms?} → the matching agent, or an error on timeout;
|
|
17
|
-
* - agent.send_keys {target, keys[]} / pane.send_text {pane_id, text}.
|
|
10
|
+
* - pane.report_agent reports {pane_id, source, agent, state, message?} where state ∈
|
|
11
|
+
* idle|working|blocked|unknown ('done' is server-derived and is not reported by the client);
|
|
12
|
+
* - pane.report_metadata reports {pane_id, source, title?, state_labels?, clear_*, tokens?, ttl_ms?}
|
|
13
|
+
* (ttl_ms is capped at 24 h by the server; the first report of a session also clears legacy
|
|
14
|
+
* pi-herdr chunk tokens to null).
|
|
18
15
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* (waitSettled marker polling, readPaneText parsing, onAgentState long-lived subscriptions)
|
|
22
|
-
* are replaced by session JSONL results (session-tail.ts) and server-side agent.wait.
|
|
23
|
-
*
|
|
24
|
-
* Design (DESIGN.md §4.1): without herdr, fall back to Noop so pi remains independent;
|
|
25
|
-
* failures in reporting calls stay silent and never affect the main pi flow.
|
|
16
|
+
* Reporting failures stay silent and never affect the main pi flow; without herdr the Noop client
|
|
17
|
+
* keeps pi independent (DESIGN.md §4.1).
|
|
26
18
|
*/
|
|
27
19
|
import { REPORT_AGENT_SOURCE, type TodoItem } from './vocab.ts';
|
|
28
20
|
import { BLOCKED_LABEL_KEY, SIDEBAR_ASK_TOKEN, formatBlockedLabel, formatPaneTitle, sidebarTodoTokens, staleTokenClearance } from './pane-title.ts';
|
|
@@ -31,7 +23,6 @@ import * as net from 'node:net';
|
|
|
31
23
|
|
|
32
24
|
/* ── Types ──────────────────────────────────────────────────────────── */
|
|
33
25
|
|
|
34
|
-
/** Herdr agent semantic state; schema has five observed states and 'done' is server-derived. */
|
|
35
26
|
export type HerdrAgentState = 'idle' | 'working' | 'blocked' | 'done' | 'unknown';
|
|
36
27
|
|
|
37
28
|
export interface HerdrEnv {
|
|
@@ -100,7 +91,6 @@ export type OpenPluginPaneResult =
|
|
|
100
91
|
| { mode: 'pane'; paneId: string }
|
|
101
92
|
| { mode: 'fallback_tab'; tabId?: string; paneId?: string };
|
|
102
93
|
|
|
103
|
-
/** TabInfo projection; fields mirror the observed tab schema. */
|
|
104
94
|
export interface TabInfo {
|
|
105
95
|
tabId: string;
|
|
106
96
|
workspaceId: string;
|
|
@@ -122,17 +112,13 @@ export function detectHerdrEnv(env: NodeJS.ProcessEnv = process.env): HerdrEnv |
|
|
|
122
112
|
};
|
|
123
113
|
}
|
|
124
114
|
|
|
125
|
-
/** Resolve herdr socket target (Windows named-pipe prefix vs Unix path). */
|
|
126
115
|
export function herdrSocketTarget(socketPath: string, platform: NodeJS.Platform = process.platform): string {
|
|
127
116
|
if (platform !== 'win32') return socketPath;
|
|
128
117
|
return socketPath.startsWith('\\\\.\\pipe\\') ? socketPath : `\\\\.\\pipe\\${socketPath}`;
|
|
129
118
|
}
|
|
130
119
|
|
|
131
|
-
/**
|
|
132
|
-
*
|
|
133
|
-
* A raw errno ("connect ENOENT /Users/…/herdr.sock") tells the model nothing to act on, so tool
|
|
134
|
-
* layers append this one sentence. Returns null when the failure is not a transport failure.
|
|
135
|
-
*/
|
|
120
|
+
/** B5: herdr configured but unreachable (stopped server, stale HERDR_SOCKET_PATH) — tool layers append
|
|
121
|
+
* this sentence because a raw errno tells the model nothing. Null when the failure is not transport. */
|
|
136
122
|
export function herdrUnavailableHint(err: unknown): string | null {
|
|
137
123
|
const msg = String((err as { message?: unknown })?.message ?? err);
|
|
138
124
|
if (!/ENOENT|ECONNREFUSED|ECONNRESET|EPIPE|ENOTCONN|not connected|socket|connect |timeout/i.test(msg)) {
|
|
@@ -141,7 +127,6 @@ export function herdrUnavailableHint(err: unknown): string | null {
|
|
|
141
127
|
return `herdr unreachable (${msg}) — check that the herdr server is running and HERDR_SOCKET_PATH matches its socket`;
|
|
142
128
|
}
|
|
143
129
|
|
|
144
|
-
|
|
145
130
|
/** Wire protocol this client is written against (herdr 0.9.1). See test/fixtures/herdr-contract.json. */
|
|
146
131
|
export const HERDR_PROTOCOL_EXPECTED = 22;
|
|
147
132
|
|
|
@@ -149,74 +134,60 @@ export type WaitForOutputResult =
|
|
|
149
134
|
| { matched: true }
|
|
150
135
|
| { matched: false; reason: 'timeout' | 'unavailable' };
|
|
151
136
|
|
|
137
|
+
/** pane.read / agent.read selection: `recent` keeps ANSI so T6 fullscreen detection still sees it. */
|
|
138
|
+
export interface ReadOptions {
|
|
139
|
+
source?: 'visible' | 'recent' | 'recent_unwrapped';
|
|
140
|
+
lines?: number;
|
|
141
|
+
stripAnsi?: boolean;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Decoded read payload; `revision` is the server's buffer revision (0 when unreadable). */
|
|
145
|
+
export interface ReadBuffer {
|
|
146
|
+
text: string;
|
|
147
|
+
revision: number;
|
|
148
|
+
truncated: boolean;
|
|
149
|
+
}
|
|
150
|
+
|
|
152
151
|
export interface HerdrClientLike {
|
|
153
152
|
readonly available: boolean;
|
|
154
153
|
reportAgent(state: PaneAgentState, message: string | null): Promise<void>;
|
|
155
154
|
reportMetadata(meta: { session: string; items: readonly TodoItem[]; progressSuffix?: string | null; lastWriteAt?: number | null }): Promise<void>;
|
|
156
155
|
/** M18: report write-lock tokens (lock-<hash> → paneId|path); null releases, and batches stay within 16 keys. */
|
|
157
156
|
reportLockTokens(tokens: Record<string, string | null>): Promise<void>;
|
|
158
|
-
/**
|
|
159
|
-
* D93: sidebar agent display name (display_agent is the role name).
|
|
160
|
-
* The report persists without a TTL; null clears it back to the detected value. Best effort.
|
|
161
|
-
*/
|
|
157
|
+
/** D93: sidebar agent display name (display_agent is the role name); null clears it. Persists, no TTL. */
|
|
162
158
|
reportDisplayAgent(name: string | null): Promise<void>;
|
|
163
159
|
/**
|
|
164
|
-
* D95: ask_user_question waiting marker (tokens['pi-ask']; null clears it).
|
|
165
|
-
*
|
|
160
|
+
* D95: ask_user_question waiting marker (tokens['pi-ask']; null clears it). The sidebar/heatmap
|
|
161
|
+
* grade blocked + pi-ask as the ask level, distinct from plain block.
|
|
166
162
|
*/
|
|
167
163
|
reportAskFlag(text: string | null): Promise<void>;
|
|
168
164
|
listAgents(): Promise<AgentInfo[]>;
|
|
169
|
-
/**
|
|
170
|
-
spawnSubPane(opts: { label: string; command: string[]; cwd: string; env?: Record<string, string> }): Promise<{ tabId: string; paneId: string }>;
|
|
171
|
-
/** Inject text into a pane terminal (pi editor input + Enter); send_text+CR reaches it directly and is used only to start launchLine. */
|
|
165
|
+
/** Inject text into a pane terminal (pi editor input + Enter); the PTY channel is used only to start launchLine. */
|
|
172
166
|
sendPaneText(paneId: string, text: string): Promise<void>;
|
|
173
|
-
/**
|
|
167
|
+
/** Server-side wait; null on timeout, throws on any other error. */
|
|
174
168
|
waitAgent(paneId: string, until: HerdrAgentState[], timeoutMs: number): Promise<HerdrAgentState | null>;
|
|
175
169
|
/** Query the child-agent session path (agent_session.value, kind=path); return null when absent. */
|
|
176
170
|
getAgentSessionPath(paneId: string): Promise<string | null>;
|
|
177
|
-
/** v1.2: Focus a pane before adding it to a group tab. */
|
|
178
|
-
focusPane(paneId: string): Promise<void>;
|
|
179
|
-
/** v1.2: Split a new shell pane in the current tab after focus, placing it in the group tab; return its pane ID. */
|
|
180
171
|
splitPane(opts: { direction?: 'left' | 'right' | 'up' | 'down'; cwd?: string; env?: Record<string, string>; targetPaneId?: string; focus?: boolean; ratio?: number }): Promise<string>;
|
|
181
|
-
/**
|
|
172
|
+
/** Close a pane; herdr kills its process tree and closes an empty tab. */
|
|
182
173
|
closePane(paneId: string): Promise<void>;
|
|
183
|
-
/** v1.2: Create a tab with a root shell pane, returning tabId/paneId for group-tab infrastructure. */
|
|
184
174
|
createTab(opts: { workspaceId: string; label?: string; cwd?: string; env?: Record<string, string> }): Promise<{ tabId: string; paneId: string }>;
|
|
185
|
-
/** v1.2: List all panes with tab ownership for group-tab additions. */
|
|
186
175
|
listPanes(): Promise<PaneListItem[]>;
|
|
187
|
-
/** D91:
|
|
176
|
+
/** D91: layout tree by paneId or tabId; null on failure. */
|
|
188
177
|
exportLayout(opts?: { paneId?: string; tabId?: string }): Promise<{ tabId: string | null; zoomed: boolean; root: unknown; focusedPaneId: string | null } | null>;
|
|
189
178
|
/** 0.9.1: live pane rects for the tab containing paneId. Best effort returns null. */
|
|
190
179
|
paneLayout(opts?: { paneId?: string }): Promise<PaneLayoutSnapshot | null>;
|
|
191
|
-
/** v1.3: List tabs (tab.list); fields follow the observed schema. */
|
|
192
180
|
tabList(): Promise<TabInfo[]>;
|
|
193
|
-
/** v1.3:
|
|
194
|
-
tabGet(tabId: string): Promise<TabInfo | null>;
|
|
195
|
-
/** v1.3: Close a tab (tab.close), cascading to its panes. */
|
|
181
|
+
/** v1.3: tab.close cascades to the tab's panes. */
|
|
196
182
|
tabClose(tabId: string): Promise<void>;
|
|
197
183
|
/** 0.9.1: Launch a manifest pane entrypoint with placement (popup/tab/split/overlay/zoomed). */
|
|
198
184
|
openPluginPane(opts: OpenPluginPaneOptions): Promise<OpenPluginPaneResult>;
|
|
199
|
-
/** 0.9.1: Close active popup if open; returns false if no popup was open. */
|
|
200
|
-
closePopup(): Promise<boolean>;
|
|
201
|
-
/** 0.9.1: Query agent explain diagnostics. */
|
|
202
185
|
agentExplain(target: string): Promise<Record<string, unknown> | null>;
|
|
203
|
-
/** 0.9.1: Query server version via ping. Cached in memory. */
|
|
204
186
|
getServerVersion(): Promise<string | null>;
|
|
205
|
-
/**
|
|
187
|
+
/** pane.send_keys; herdr key combos include ctrl+c, enter, and esc. */
|
|
206
188
|
sendPaneKeys(paneId: string, keys: string[]): Promise<void>;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
source?: 'visible' | 'recent' | 'recent_unwrapped';
|
|
210
|
-
lines?: number;
|
|
211
|
-
stripAnsi?: boolean;
|
|
212
|
-
}): Promise<{ text: string; revision: number; truncated: boolean }>;
|
|
213
|
-
/** Read the agent output buffer via herdr 0.9 agent.read RPC. */
|
|
214
|
-
readAgent(target: string, opts?: {
|
|
215
|
-
source?: 'visible' | 'recent' | 'recent_unwrapped';
|
|
216
|
-
lines?: number;
|
|
217
|
-
stripAnsi?: boolean;
|
|
218
|
-
}): Promise<{ text: string; revision: number; truncated: boolean }>;
|
|
219
|
-
/** M14: Wait for output to match a substring or regex; return discriminated result on match/timeout/unavailable. */
|
|
189
|
+
readPane(paneId: string, opts?: ReadOptions): Promise<ReadBuffer>;
|
|
190
|
+
readAgent(target: string, opts?: ReadOptions): Promise<ReadBuffer>;
|
|
220
191
|
waitForOutput(paneId: string, match: { type: 'substring' | 'regex'; value: string }, timeoutMs: number): Promise<WaitForOutputResult>;
|
|
221
192
|
close(): void;
|
|
222
193
|
}
|
|
@@ -237,6 +208,7 @@ function findIdIn(obj: unknown, key: string, depth = 0): string | null {
|
|
|
237
208
|
|
|
238
209
|
/* ── Noop ──────────────────────────────────────────────────────────── */
|
|
239
210
|
|
|
211
|
+
/** Stand-in outside a herdr pane: queries answer empty, pane creation throws, reporting is a no-op. */
|
|
240
212
|
export class NoopHerdrClient implements HerdrClientLike {
|
|
241
213
|
readonly available = false;
|
|
242
214
|
async reportAgent(): Promise<void> {}
|
|
@@ -244,66 +216,26 @@ export class NoopHerdrClient implements HerdrClientLike {
|
|
|
244
216
|
async reportLockTokens(): Promise<void> {}
|
|
245
217
|
async reportDisplayAgent(): Promise<void> {}
|
|
246
218
|
async reportAskFlag(): Promise<void> {}
|
|
247
|
-
async listAgents(): Promise<AgentInfo[]> {
|
|
248
|
-
return [];
|
|
249
|
-
}
|
|
250
|
-
async spawnSubPane(): Promise<{ tabId: string; paneId: string }> {
|
|
251
|
-
throw new Error('subagent requires a herdr-managed pane');
|
|
252
|
-
}
|
|
253
219
|
async sendPaneText(): Promise<void> {}
|
|
254
|
-
async waitAgent(): Promise<null> {
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
257
|
-
async getAgentSessionPath(): Promise<null> {
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
async focusPane(): Promise<void> {}
|
|
261
|
-
async splitPane(): Promise<string> {
|
|
262
|
-
throw new Error('subagent requires a herdr-managed pane');
|
|
263
|
-
}
|
|
264
220
|
async closePane(): Promise<void> {}
|
|
265
|
-
async createTab(): Promise<{ tabId: string; paneId: string }> {
|
|
266
|
-
throw new Error('subagent requires a herdr-managed pane');
|
|
267
|
-
}
|
|
268
|
-
async listPanes(): Promise<PaneListItem[]> {
|
|
269
|
-
return [];
|
|
270
|
-
}
|
|
271
|
-
async exportLayout(): Promise<null> {
|
|
272
|
-
return null;
|
|
273
|
-
}
|
|
274
|
-
async paneLayout(): Promise<null> {
|
|
275
|
-
return null;
|
|
276
|
-
}
|
|
277
|
-
async tabList(): Promise<TabInfo[]> {
|
|
278
|
-
return [];
|
|
279
|
-
}
|
|
280
|
-
async tabGet(): Promise<TabInfo | null> {
|
|
281
|
-
return null;
|
|
282
|
-
}
|
|
283
221
|
async tabClose(): Promise<void> {}
|
|
284
|
-
async openPluginPane(): Promise<OpenPluginPaneResult> {
|
|
285
|
-
throw new Error('plugin pane requires a herdr-managed pane');
|
|
286
|
-
}
|
|
287
|
-
async closePopup(): Promise<boolean> {
|
|
288
|
-
return false;
|
|
289
|
-
}
|
|
290
|
-
async agentExplain(): Promise<null> {
|
|
291
|
-
return null;
|
|
292
|
-
}
|
|
293
|
-
async getServerVersion(): Promise<null> {
|
|
294
|
-
return null;
|
|
295
|
-
}
|
|
296
222
|
async sendPaneKeys(): Promise<void> {}
|
|
297
|
-
async readPane(): Promise<{ text: string; revision: number; truncated: boolean }> {
|
|
298
|
-
return { text: '', revision: 0, truncated: false };
|
|
299
|
-
}
|
|
300
|
-
async readAgent(): Promise<{ text: string; revision: number; truncated: boolean }> {
|
|
301
|
-
return { text: '', revision: 0, truncated: false };
|
|
302
|
-
}
|
|
303
|
-
async waitForOutput(): Promise<WaitForOutputResult> {
|
|
304
|
-
return { matched: false, reason: 'unavailable' };
|
|
305
|
-
}
|
|
306
223
|
close(): void {}
|
|
224
|
+
async listAgents(): Promise<AgentInfo[]> { return []; }
|
|
225
|
+
async listPanes(): Promise<PaneListItem[]> { return []; }
|
|
226
|
+
async tabList(): Promise<TabInfo[]> { return []; }
|
|
227
|
+
async waitAgent(): Promise<null> { return null; }
|
|
228
|
+
async getAgentSessionPath(): Promise<null> { return null; }
|
|
229
|
+
async exportLayout(): Promise<null> { return null; }
|
|
230
|
+
async paneLayout(): Promise<null> { return null; }
|
|
231
|
+
async agentExplain(): Promise<null> { return null; }
|
|
232
|
+
async getServerVersion(): Promise<null> { return null; }
|
|
233
|
+
async readPane(): Promise<ReadBuffer> { return { text: '', revision: 0, truncated: false }; }
|
|
234
|
+
async readAgent(): Promise<ReadBuffer> { return { text: '', revision: 0, truncated: false }; }
|
|
235
|
+
async waitForOutput(): Promise<WaitForOutputResult> { return { matched: false, reason: 'unavailable' }; }
|
|
236
|
+
async splitPane(): Promise<string> { throw new Error('subagent requires a herdr-managed pane'); }
|
|
237
|
+
async createTab(): Promise<{ tabId: string; paneId: string }> { throw new Error('subagent requires a herdr-managed pane'); }
|
|
238
|
+
async openPluginPane(): Promise<OpenPluginPaneResult> { throw new Error('plugin pane requires a herdr-managed pane'); }
|
|
307
239
|
}
|
|
308
240
|
|
|
309
241
|
/* ── Implementation ────────────────────────────────────────────────── */
|
|
@@ -321,10 +253,7 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
321
253
|
return herdrSocketTarget(this.env.socketPath);
|
|
322
254
|
}
|
|
323
255
|
|
|
324
|
-
/**
|
|
325
|
-
* Control requests use one connection per request; the server closes it after replying, as observed in the protocol.
|
|
326
|
-
* Reading a complete response line finishes the request; responses are {id, result} or {id, error}.
|
|
327
|
-
*/
|
|
256
|
+
/** One connection per control request: the reply is a single {id,result}|{id,error} line. */
|
|
328
257
|
private request(method: string, params: Record<string, unknown>, timeoutMs = 15000): Promise<unknown> {
|
|
329
258
|
return new Promise((resolve, reject) => {
|
|
330
259
|
const sock = net.createConnection(this.target());
|
|
@@ -393,13 +322,11 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
393
322
|
...(message ? { message } : {}),
|
|
394
323
|
});
|
|
395
324
|
} catch {
|
|
396
|
-
/* Silent best effort. */
|
|
397
325
|
}
|
|
398
326
|
}
|
|
399
327
|
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
// activity badge, because `state` is a required field there and pier's todo/role text is pier-only data.
|
|
328
|
+
// Single writer: herdr's native pi integration owns the session path, so pier never sends
|
|
329
|
+
// pane.report_agent_session; the activity badge stays on report_agent.
|
|
403
330
|
|
|
404
331
|
async reportMetadata(meta: { session: string; items: readonly TodoItem[]; progressSuffix?: string | null; lastWriteAt?: number | null }): Promise<void> {
|
|
405
332
|
try {
|
|
@@ -409,8 +336,8 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
409
336
|
lastWriteAt: meta.lastWriteAt,
|
|
410
337
|
});
|
|
411
338
|
const blocked = formatBlockedLabel(meta.items);
|
|
412
|
-
// D96:
|
|
413
|
-
//
|
|
339
|
+
// D96: stale cleanup needs its own batch — stale(16) + pi-todo(1) exceeds herdr's 16-token
|
|
340
|
+
// limit, and the rejected request would drop both the title and the tokens.
|
|
414
341
|
if (!this.clearedStaleTokens) {
|
|
415
342
|
await this.request('pane.report_metadata', {
|
|
416
343
|
pane_id: this.env.paneId,
|
|
@@ -427,12 +354,11 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
427
354
|
...(blocked
|
|
428
355
|
? { state_labels: { [BLOCKED_LABEL_KEY]: blocked } }
|
|
429
356
|
: { clear_state_labels: true }),
|
|
430
|
-
// D93:
|
|
357
|
+
// D93: the todo summary is a custom token; an empty string clears the key instead of leaving a stale summary.
|
|
431
358
|
tokens: sidebarTodoTokens(title),
|
|
432
359
|
ttl_ms: 86400000,
|
|
433
360
|
});
|
|
434
361
|
} catch {
|
|
435
|
-
/* Silent best effort: the title projection must never affect pi's main flow. */
|
|
436
362
|
}
|
|
437
363
|
}
|
|
438
364
|
|
|
@@ -440,7 +366,6 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
440
366
|
const keys = Object.keys(tokens);
|
|
441
367
|
if (keys.length === 0) return;
|
|
442
368
|
try {
|
|
443
|
-
// schema maxProperties=16, so split into independent best-effort requests.
|
|
444
369
|
for (let i = 0; i < keys.length; i += LOCK_BATCH_LIMIT) {
|
|
445
370
|
const batch: Record<string, string | null> = {};
|
|
446
371
|
for (const k of keys.slice(i, i + LOCK_BATCH_LIMIT)) batch[k] = tokens[k];
|
|
@@ -464,7 +389,6 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
464
389
|
...(name ? { display_agent: name } : { clear_display_agent: true }),
|
|
465
390
|
});
|
|
466
391
|
} catch {
|
|
467
|
-
/* Silent best effort: sidebar identity is supplementary. */
|
|
468
392
|
}
|
|
469
393
|
}
|
|
470
394
|
|
|
@@ -477,7 +401,6 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
477
401
|
ttl_ms: 86400000,
|
|
478
402
|
});
|
|
479
403
|
} catch {
|
|
480
|
-
/* Silent best effort: the human-gate marker is supplementary. */
|
|
481
404
|
}
|
|
482
405
|
}
|
|
483
406
|
|
|
@@ -503,16 +426,6 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
503
426
|
});
|
|
504
427
|
}
|
|
505
428
|
|
|
506
|
-
async spawnSubPane(opts: { label: string; command: string[]; cwd: string; env?: Record<string, string> }): Promise<{ tabId: string; paneId: string }> {
|
|
507
|
-
const result = (await this.request('layout.apply', {
|
|
508
|
-
tab_label: opts.label,
|
|
509
|
-
root: { type: 'pane', command: opts.command, cwd: opts.cwd, ...(opts.env ? { env: opts.env } : {}) },
|
|
510
|
-
})) as Record<string, unknown>;
|
|
511
|
-
const paneId = findIdIn(result, 'pane_id');
|
|
512
|
-
if (!paneId) throw new Error('layout.apply: no pane_id in response');
|
|
513
|
-
return { tabId: findIdIn(result, 'tab_id') ?? '', paneId };
|
|
514
|
-
}
|
|
515
|
-
|
|
516
429
|
async sendPaneText(paneId: string, text: string): Promise<void> {
|
|
517
430
|
// A trailing \r acts as Enter; observed behavior sends text to the pi editor and submits it.
|
|
518
431
|
await this.request('pane.send_text', { pane_id: paneId, text: text + '\r' });
|
|
@@ -531,8 +444,7 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
531
444
|
const msg = String((err as Error)?.message ?? err);
|
|
532
445
|
if (/timeout/i.test(msg)) return null;
|
|
533
446
|
// A14: herdr reports agent_not_found while a freshly spawned pane's agent is still registering;
|
|
534
|
-
// callers poll
|
|
535
|
-
// failing the whole spawn with a raw server error.
|
|
447
|
+
// callers poll, so map it to the same "nothing yet" result as a timeout.
|
|
536
448
|
if (/agent_not_found|agent target .* not found/i.test(msg)) return null;
|
|
537
449
|
throw err;
|
|
538
450
|
}
|
|
@@ -544,10 +456,6 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
544
456
|
return a?.session ?? null;
|
|
545
457
|
}
|
|
546
458
|
|
|
547
|
-
async focusPane(paneId: string): Promise<void> {
|
|
548
|
-
await this.request('pane.focus', { pane_id: paneId });
|
|
549
|
-
}
|
|
550
|
-
|
|
551
459
|
async splitPane(opts: { direction?: 'left' | 'right' | 'up' | 'down'; cwd?: string; env?: Record<string, string>; targetPaneId?: string; focus?: boolean; ratio?: number } = {}): Promise<string> {
|
|
552
460
|
const params: Record<string, unknown> = {
|
|
553
461
|
direction: opts.direction ?? 'right',
|
|
@@ -676,11 +584,6 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
676
584
|
return (result?.tabs ?? []).map((t) => this.mapTabInfo(t)).filter((t): t is TabInfo => t !== null);
|
|
677
585
|
}
|
|
678
586
|
|
|
679
|
-
async tabGet(tabId: string): Promise<TabInfo | null> {
|
|
680
|
-
const result = (await this.request('tab.get', { tab_id: tabId })) as { tab?: Record<string, unknown> | null } | null;
|
|
681
|
-
return this.mapTabInfo(result?.tab ?? null);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
587
|
async tabClose(tabId: string): Promise<void> {
|
|
685
588
|
await this.request('tab.close', { tab_id: tabId });
|
|
686
589
|
}
|
|
@@ -708,15 +611,8 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
708
611
|
// 0.9.0 serde rejects unknown `popup` as "unknown variant" / invalid_params — not a dedicated
|
|
709
612
|
// invalid_placement code. Any popup failure retries as tab; tab failure surfaces to Level 3.
|
|
710
613
|
if (opts.placement !== 'popup') throw err;
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
entrypoint: opts.entrypoint,
|
|
714
|
-
placement: 'tab',
|
|
715
|
-
...(opts.focus !== undefined ? { focus: opts.focus } : {}),
|
|
716
|
-
...(opts.cwd ? { cwd: opts.cwd } : {}),
|
|
717
|
-
...(opts.env ? { env: opts.env } : {}),
|
|
718
|
-
...(opts.workspaceId ? { workspace_id: opts.workspaceId } : {}),
|
|
719
|
-
})) as Record<string, unknown> | null;
|
|
614
|
+
const { width: _width, height: _height, target_pane_id: _target, ...base } = params;
|
|
615
|
+
const fallbackRes = (await this.request('plugin.pane.open', { ...base, placement: 'tab' })) as Record<string, unknown> | null;
|
|
720
616
|
return {
|
|
721
617
|
mode: 'fallback_tab',
|
|
722
618
|
paneId: findIdIn(fallbackRes, 'pane_id') ?? undefined,
|
|
@@ -725,19 +621,6 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
725
621
|
}
|
|
726
622
|
}
|
|
727
623
|
|
|
728
|
-
async closePopup(): Promise<boolean> {
|
|
729
|
-
try {
|
|
730
|
-
await this.request('popup.close', {});
|
|
731
|
-
return true;
|
|
732
|
-
} catch (err) {
|
|
733
|
-
const msg = String((err as Error)?.message ?? err);
|
|
734
|
-
if (/popup_not_open|not found|not supported|unknown method/i.test(msg)) {
|
|
735
|
-
return false;
|
|
736
|
-
}
|
|
737
|
-
throw err;
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
624
|
async agentExplain(target: string): Promise<Record<string, unknown> | null> {
|
|
742
625
|
try {
|
|
743
626
|
const result = (await this.request('agent.explain', { target })) as Record<string, unknown> | null;
|
|
@@ -763,7 +646,6 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
763
646
|
return result.version;
|
|
764
647
|
}
|
|
765
648
|
} catch {
|
|
766
|
-
/* ignore */
|
|
767
649
|
}
|
|
768
650
|
return null;
|
|
769
651
|
}
|
|
@@ -772,19 +654,23 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
772
654
|
await this.request('pane.send_keys', { pane_id: paneId, keys });
|
|
773
655
|
}
|
|
774
656
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
657
|
+
/** Shared pane.read/agent.read decode: payload nested under `read` (older servers answer it flat). */
|
|
658
|
+
private async readBuffer(
|
|
659
|
+
method: 'pane.read' | 'agent.read',
|
|
660
|
+
target: { pane_id: string } | { target: string },
|
|
661
|
+
opts: ReadOptions,
|
|
662
|
+
stripAnsiDefault: boolean,
|
|
663
|
+
): Promise<ReadBuffer> {
|
|
664
|
+
const result = (await this.request(method, {
|
|
665
|
+
...target,
|
|
783
666
|
source: opts.source ?? 'recent',
|
|
784
667
|
format: 'text',
|
|
785
|
-
strip_ansi: opts.stripAnsi ??
|
|
668
|
+
strip_ansi: opts.stripAnsi ?? stripAnsiDefault,
|
|
786
669
|
...(opts.lines != null ? { lines: opts.lines } : {}),
|
|
787
|
-
})) as {
|
|
670
|
+
})) as {
|
|
671
|
+
read?: { text?: unknown; revision?: unknown; truncated?: unknown };
|
|
672
|
+
text?: unknown; revision?: unknown; truncated?: unknown;
|
|
673
|
+
} | null;
|
|
788
674
|
const payload = result?.read ?? result ?? {};
|
|
789
675
|
return {
|
|
790
676
|
text: typeof payload.text === 'string' ? payload.text : '',
|
|
@@ -793,24 +679,12 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
793
679
|
};
|
|
794
680
|
}
|
|
795
681
|
|
|
796
|
-
async
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
target,
|
|
803
|
-
source: opts.source ?? 'recent',
|
|
804
|
-
format: 'text',
|
|
805
|
-
strip_ansi: opts.stripAnsi ?? true,
|
|
806
|
-
...(opts.lines != null ? { lines: opts.lines } : {}),
|
|
807
|
-
})) as { read?: { text?: unknown; revision?: unknown; truncated?: unknown }; text?: unknown; revision?: unknown; truncated?: unknown } | null;
|
|
808
|
-
const payload = result?.read ?? result ?? {};
|
|
809
|
-
return {
|
|
810
|
-
text: typeof payload.text === 'string' ? payload.text : '',
|
|
811
|
-
revision: typeof payload.revision === 'number' ? payload.revision : 0,
|
|
812
|
-
truncated: payload.truncated === true,
|
|
813
|
-
};
|
|
682
|
+
async readPane(paneId: string, opts: ReadOptions = {}): Promise<ReadBuffer> {
|
|
683
|
+
return this.readBuffer('pane.read', { pane_id: paneId }, opts, false);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async readAgent(target: string, opts: ReadOptions = {}): Promise<ReadBuffer> {
|
|
687
|
+
return this.readBuffer('agent.read', { target }, opts, true);
|
|
814
688
|
}
|
|
815
689
|
|
|
816
690
|
async waitForOutput(paneId: string, match: { type: 'substring' | 'regex'; value: string }, timeoutMs: number): Promise<WaitForOutputResult> {
|
package/src/history-store.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* <agentRoot>/herdr-pi/history/--<cwd>--/history.jsonl
|
|
6
|
-
* Records are immutable so GC can remove panes without deleting history. Recovery keeps
|
|
7
|
-
* both the sessionFile and launchCommand snapshots as fallbacks.
|
|
2
|
+
* Delegation ledger: append-only JSONL partitioned by cwd, mirroring pi's session partitioning
|
|
3
|
+
* (<agentRoot>/herdr-pi/history/--<cwd>--/history.jsonl). Rows are immutable, so GC can drop panes
|
|
4
|
+
* without losing history; recovery keeps both the sessionFile and launchCommand snapshots.
|
|
8
5
|
*/
|
|
9
6
|
import * as fs from 'node:fs';
|
|
10
7
|
import * as path from 'node:path';
|
|
@@ -12,13 +9,13 @@ import { historyFilePath, preferredHistoryFile } from './storage-layout.ts';
|
|
|
12
9
|
|
|
13
10
|
export { historyFilePath, preferredHistoryFile };
|
|
14
11
|
|
|
15
|
-
/** Legacy short/resident entries normalize to task
|
|
12
|
+
/** Legacy short/resident entries normalize to task, as do missing values; role names pass through. */
|
|
16
13
|
export function normalizeEntryKind(kind: string | null | undefined): string {
|
|
17
14
|
if (!kind || kind === 'short' || kind === 'resident') return 'task';
|
|
18
15
|
return kind;
|
|
19
16
|
}
|
|
20
17
|
|
|
21
|
-
/**
|
|
18
|
+
/** Only a .jsonl path is accepted, so an invalid report cannot overwrite a valid one. */
|
|
22
19
|
export function applyReportedSessionFile(
|
|
23
20
|
current: string | null,
|
|
24
21
|
reported: string | null | undefined,
|
|
@@ -29,11 +26,9 @@ export function applyReportedSessionFile(
|
|
|
29
26
|
|
|
30
27
|
export interface HistoryEntry {
|
|
31
28
|
taskId: string;
|
|
32
|
-
/** 'task' or a role name; disk reads normalize short/resident to task for compatibility. */
|
|
33
29
|
kind: string;
|
|
34
30
|
paneId: string;
|
|
35
31
|
tabId: string;
|
|
36
|
-
/** v1.3: task tab name retained for history review and revival; omitted by older entries. */
|
|
37
32
|
tabName?: string;
|
|
38
33
|
workspaceId: string;
|
|
39
34
|
cwd: string;
|
|
@@ -45,19 +40,17 @@ export interface HistoryEntry {
|
|
|
45
40
|
status: 'running' | 'settled' | 'consumed' | 'closed';
|
|
46
41
|
outcome?: string | null;
|
|
47
42
|
createdAt: number;
|
|
48
|
-
/** M13c: durable consumption time, which provides the GC TTL basis. */
|
|
49
43
|
consumedAt?: number | null;
|
|
50
44
|
closedAt?: number | null;
|
|
51
|
-
/** Previous-generation pane ID, retained when work is redone or revived. */
|
|
52
45
|
revivedFrom?: string | null;
|
|
53
|
-
/**
|
|
54
|
-
* multiple entries in one second remain auditable (ledger row#23/24 proves both writes). */
|
|
46
|
+
/** Writer marker (spawn / poll-settle / poll-timeout / gc / zombie-sweep …) for auditability. */
|
|
55
47
|
via?: string;
|
|
56
48
|
}
|
|
57
49
|
|
|
58
|
-
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
50
|
+
/**
|
|
51
|
+
* When a patch omits `outcome`, inherit the latest non-empty value for the taskId: GC bookkeeping
|
|
52
|
+
* must not erase a settlement result under the "latest row is state" rule.
|
|
53
|
+
*/
|
|
61
54
|
export function inheritOutcome(lastOutcome: string | null | undefined, patchOutcome: string | null | undefined): string | null {
|
|
62
55
|
if (patchOutcome !== undefined) return patchOutcome;
|
|
63
56
|
return lastOutcome ?? null;
|
|
@@ -82,7 +75,6 @@ function coerceHistoryEntry(obj: unknown): HistoryEntry | null {
|
|
|
82
75
|
return { ...raw, kind: normalizeEntryKind(raw.kind) };
|
|
83
76
|
}
|
|
84
77
|
|
|
85
|
-
/** Parse line by line; skip junk JSON and records missing taskId/status. */
|
|
86
78
|
export function parseHistoryEntries(text: string): HistoryEntry[] {
|
|
87
79
|
const entries: HistoryEntry[] = [];
|
|
88
80
|
for (const line of (text ?? '').split(/\r?\n/)) {
|
|
@@ -107,7 +99,7 @@ export type HistoryReadResult =
|
|
|
107
99
|
| { status: 'missing' }
|
|
108
100
|
| { status: 'unreadable'; error: Error };
|
|
109
101
|
|
|
110
|
-
/** Distinguish missing vs permission/IO failure
|
|
102
|
+
/** Distinguish missing vs permission/IO failure; readHistory still returns [] for both. */
|
|
111
103
|
export function inspectHistory(file: string): HistoryReadResult {
|
|
112
104
|
try {
|
|
113
105
|
if (!fs.existsSync(file)) return { status: 'missing' };
|
|
@@ -118,15 +110,37 @@ export function inspectHistory(file: string): HistoryReadResult {
|
|
|
118
110
|
}
|
|
119
111
|
}
|
|
120
112
|
|
|
121
|
-
/** Read a history file; missing or unreadable yields []. */
|
|
122
113
|
export function readHistory(file: string): HistoryEntry[] {
|
|
123
114
|
const r = inspectHistory(file);
|
|
124
115
|
return r.status === 'ok' ? r.entries : [];
|
|
125
116
|
}
|
|
126
117
|
|
|
118
|
+
/** Every ledger under `<agentRoot>/herdr-pi/history`. Resume must see tasks spawned into another checkout. */
|
|
119
|
+
export function readHistoryTree(agentRoot: string): HistoryEntry[] {
|
|
120
|
+
const root = path.join(agentRoot, 'herdr-pi', 'history');
|
|
121
|
+
let names: string[];
|
|
122
|
+
try {
|
|
123
|
+
names = fs.readdirSync(root);
|
|
124
|
+
} catch {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
const all: HistoryEntry[] = [];
|
|
128
|
+
for (const name of names) {
|
|
129
|
+
let isDir = false;
|
|
130
|
+
try {
|
|
131
|
+
isDir = fs.statSync(path.join(root, name)).isDirectory();
|
|
132
|
+
} catch {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (!isDir) continue;
|
|
136
|
+
all.push(...readHistory(path.join(root, name, 'history.jsonl')));
|
|
137
|
+
}
|
|
138
|
+
return all;
|
|
139
|
+
}
|
|
140
|
+
|
|
127
141
|
export type HistoryWriteResult = { ok: true } | { ok: false; error: Error };
|
|
128
142
|
|
|
129
|
-
/**
|
|
143
|
+
/** Failures are logged and returned so the ledger is not silently lost. */
|
|
130
144
|
export function appendHistory(file: string, entry: HistoryEntry): HistoryWriteResult {
|
|
131
145
|
try {
|
|
132
146
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -139,7 +153,6 @@ export function appendHistory(file: string, entry: HistoryEntry): HistoryWriteRe
|
|
|
139
153
|
}
|
|
140
154
|
}
|
|
141
155
|
|
|
142
|
-
/** Group all generations of each taskId in ascending createdAt order for review. */
|
|
143
156
|
export function generationsByTask(entries: readonly HistoryEntry[]): Map<string, HistoryEntry[]> {
|
|
144
157
|
const map = new Map<string, HistoryEntry[]>();
|
|
145
158
|
for (const e of entries) {
|
|
@@ -151,7 +164,7 @@ export function generationsByTask(entries: readonly HistoryEntry[]): Map<string,
|
|
|
151
164
|
return map;
|
|
152
165
|
}
|
|
153
166
|
|
|
154
|
-
/**
|
|
167
|
+
/** Newest generation for taskId — the one used for resume. */
|
|
155
168
|
export function latestGeneration(entries: readonly HistoryEntry[], taskId: string): HistoryEntry | null {
|
|
156
169
|
const gens = generationsByTask(entries).get(taskId);
|
|
157
170
|
if (!gens || gens.length === 0) return null;
|