pi-pier 0.3.1 → 0.4.2
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 +7 -5
- package/package.json +3 -3
- package/schemas/efficiency-config.schema.json +6 -0
- package/src/ask-dialog.ts +9 -2
- package/src/ask-user.ts +142 -74
- package/src/bootstrap.ts +2 -1
- package/src/compact-coordinator.ts +207 -35
- package/src/compact-economics-core.ts +62 -29
- package/src/config-catalog-core.ts +1 -0
- package/src/cordis-services.ts +64 -0
- package/src/dashboard-command.ts +40 -15
- package/src/delegation-core.ts +115 -0
- package/src/efficiency-config-core.ts +17 -6
- package/src/efficiency-store.ts +43 -6
- package/src/guards.ts +6 -0
- package/src/herdr-client.ts +141 -36
- package/src/history-store.ts +2 -0
- package/src/index-gates.ts +6 -12
- package/src/index-master.ts +12 -16
- package/src/index-pipe.ts +6 -0
- package/src/index-runtime.ts +14 -0
- package/src/index-worker.ts +4 -10
- package/src/index.ts +310 -76
- package/src/jev-client.ts +32 -7
- package/src/jev-core.ts +71 -36
- package/src/observation-core.ts +13 -1
- package/src/panel-open.ts +47 -0
- package/src/pier-options.ts +1 -2
- package/src/pipe-channel.ts +9 -0
- package/src/plugins/heat-plan.ts +18 -3
- package/src/plugins/observation.ts +118 -46
- package/src/plugins/subagent.ts +119 -68
- package/src/plugins/terminal.ts +221 -57
- package/src/plugins/todo.ts +63 -63
- package/src/reducer-core.ts +3 -5
- package/src/reducer-invoker.ts +27 -13
- package/src/renderers.ts +25 -2
- package/src/role-manifest.ts +2 -3
- package/src/roles/worker-default.json +2 -2
- package/src/settle-wake-core.ts +17 -3
- package/src/subagent-core.ts +209 -3
- package/src/subagent-poller.ts +69 -19
- package/src/subagent-session.ts +67 -4
- package/src/subagent-spawn.ts +55 -7
- package/src/terminal-core.ts +178 -50
- package/src/todo-core.ts +47 -2
- package/src/todo-read-hook.ts +54 -17
- package/src/todo-reminder-core.ts +9 -5
- package/src/todo-window.ts +8 -4
- package/src/todos-service.ts +12 -0
- package/src/tree-view.ts +162 -0
- package/src/vocab.ts +7 -1
- package/src/worker-marker.ts +76 -0
- package/src/focus-poller.ts +0 -223
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
hasAskUi,
|
|
20
20
|
noUiResult,
|
|
21
21
|
prepareAsk,
|
|
22
|
+
prepareAskArguments,
|
|
22
23
|
runAsk,
|
|
23
24
|
} from './ask-user.ts';
|
|
24
25
|
import { pipeNameFor, pipeRequest, startPipeServer } from './pipe-channel.ts';
|
|
@@ -28,6 +29,8 @@ import { createHerdrClient } from './herdr-client.ts';
|
|
|
28
29
|
import { bareSessionId, lastAssistantText, readSessionFile, resolveSessionFileValue } from './session-tail.ts';
|
|
29
30
|
import { fileURLToPath } from 'node:url';
|
|
30
31
|
import { join } from 'node:path';
|
|
32
|
+
import { mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
33
|
+
import { tmpdir } from 'node:os';
|
|
31
34
|
import { platformPaths } from './platform-paths.ts';
|
|
32
35
|
import { TodosService, type TodoCompletionSource } from './todos-service.ts';
|
|
33
36
|
import { reconcileTodos } from './reconcile-core.ts';
|
|
@@ -38,49 +41,48 @@ import { ABORT_STOP_REASON, planSettleWake, shouldPushSettleReply } from './sett
|
|
|
38
41
|
import { parseRuntimeManifest } from './tool-gate.ts';
|
|
39
42
|
import { formatPaneTitle } from './pane-title.ts';
|
|
40
43
|
import { registerSlimFrame, updateSlimFrame } from './slim-frame.ts';
|
|
41
|
-
import { planIndexMode } from './index-runtime.ts';
|
|
42
|
-
import { emptySubagentPortBox } from './subagent-core.ts';
|
|
44
|
+
import { applyRestoredWorkerMode, planIndexMode } from './index-runtime.ts';
|
|
45
|
+
import { buildResumeArgv, emptySubagentPortBox, sleepWhileSubsRunningNote } from './subagent-core.ts';
|
|
46
|
+
import { hasWorkerMarker, writeWorkerMarker, readWorkerMarker } from './worker-marker.ts';
|
|
47
|
+
import {
|
|
48
|
+
DELEGATION_CUSTOM_TYPE,
|
|
49
|
+
applyWorkerTag,
|
|
50
|
+
buildDelegationSection,
|
|
51
|
+
foldDelegation,
|
|
52
|
+
parseDelegationContext,
|
|
53
|
+
type DelegationContext,
|
|
54
|
+
} from './delegation-core.ts';
|
|
55
|
+
import { formatDelegationTree, type WorkerTodoSnapshot, type WorkerWidgetRow } from './tree-view.ts';
|
|
43
56
|
import { createNoticeBuffer } from './index-notices.ts';
|
|
44
57
|
import { handlePipeRequest, type MachineRequest } from './index-pipe.ts';
|
|
45
58
|
import { installWriteLocks } from './index-locks.ts';
|
|
46
59
|
import { installDashboardCommand } from './dashboard-command.ts';
|
|
47
|
-
import {
|
|
60
|
+
import { openPierPanel } from './panel-open.ts';
|
|
61
|
+
import { registerObservationPack, createCompactionBatchPackHook, type PickMiddleExcerpts } from './plugins/observation.ts';
|
|
48
62
|
import { createJevRuntime } from './jev-client.ts';
|
|
49
63
|
import {
|
|
50
64
|
NOTICE_RANK_MIN_CONFIDENCE,
|
|
51
|
-
buildExcerptWindows,
|
|
52
65
|
composeNoticeRanking,
|
|
53
66
|
evaluateExcerptPick,
|
|
54
|
-
excerptAskIsSafe,
|
|
55
67
|
excerptPickRequest,
|
|
56
68
|
noticeRankRequest,
|
|
69
|
+
planExcerptAsks,
|
|
57
70
|
} from './jev-core.ts';
|
|
58
|
-
import { completeLineExcerpt } from './observation-core.ts';
|
|
59
71
|
import { installConfigCommand } from './config-command.ts';
|
|
60
72
|
import { CompactCoordinator } from './compact-coordinator.ts';
|
|
61
73
|
import { handleReducerToolResult, type ToolResultEventLike } from './reducer-invoker.ts';
|
|
62
74
|
import { appendEfficiencyLog, efficiencyLogPath, pruneSessionObjects, resolveSessionRoot } from './efficiency-store.ts';
|
|
63
75
|
import type { RoutingTelemetryRecord, RoutingTelemetryRow } from './routing-telemetry.ts';
|
|
64
76
|
import { loadEfficiencyConfigFromDisk, resolveEfficiencyConfig, type EfficiencyConfig } from './efficiency-config-core.ts';
|
|
65
|
-
import { resolveDefaultFocusPollMs, parseFocusSample, spawnReflow, startFocusPoller, type FocusPoller } from './focus-poller.ts';
|
|
66
77
|
import { composeMasterRuntime, createRoleRuntime } from './index-roles.ts';
|
|
67
78
|
import { createGateRuntime } from './index-gates.ts';
|
|
68
79
|
|
|
69
|
-
/** D-4: PIER_FOCUS_POLL_MS=0 disables the poller; invalid values fall back to the version default. */
|
|
70
|
-
function focusPollIntervalMs(env: NodeJS.ProcessEnv = process.env, herdrVersion?: string | null): number {
|
|
71
|
-
const defaultMs = resolveDefaultFocusPollMs(herdrVersion);
|
|
72
|
-
const ms = Number(env.PIER_FOCUS_POLL_MS);
|
|
73
|
-
return env.PIER_FOCUS_POLL_MS !== undefined && env.PIER_FOCUS_POLL_MS.trim() !== '' && Number.isFinite(ms) && ms >= 0
|
|
74
|
-
? ms
|
|
75
|
-
: defaultMs;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
80
|
function agentSessionsDir(): string {
|
|
79
81
|
return process.env.PI_CODING_AGENT_DIR ? join(process.env.PI_CODING_AGENT_DIR, 'sessions') : platformPaths.sessionsDir;
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
export default async function (pi: ExtensionAPI) {
|
|
83
|
-
const mode = planIndexMode();
|
|
85
|
+
const mode = applyRestoredWorkerMode(planIndexMode(), process.env.PI_SESSION_FILE ?? null, hasWorkerMarker);
|
|
84
86
|
const isSubagent = mode.isSubagent;
|
|
85
87
|
const roleBase = process.env.PI_HERDR_ROLE_BASE || process.cwd();
|
|
86
88
|
|
|
@@ -89,7 +91,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
89
91
|
let effConfig: EfficiencyConfig = resolveEfficiencyConfig();
|
|
90
92
|
const coordinator = new CompactCoordinator();
|
|
91
93
|
const { client, env } = createHerdrClient();
|
|
92
|
-
|
|
94
|
+
/** RFC rfc-delegation-identity §2.1: resolved env → marker → branch fold; worker panes only. */
|
|
95
|
+
let delegation: DelegationContext | null = parseDelegationContext(process.env.PI_HERDR_DELEGATION);
|
|
93
96
|
const initialManifest =
|
|
94
97
|
parseRuntimeManifest(process.env.PI_HERDR_ROLE_MANIFEST) ??
|
|
95
98
|
(mode.composeMaster ? composeMasterRuntime() : null);
|
|
@@ -108,30 +111,37 @@ export default async function (pi: ExtensionAPI) {
|
|
|
108
111
|
/* ── Jev decision layer (RFC docs/rfc-jev-integration.md): direct HTTP, total-budget
|
|
109
112
|
* abort, fail-open at every site. Disabled or keyless → behavior identical to before. ── */
|
|
110
113
|
const jevRuntime = createJevRuntime(() => effConfig.jev, { getSessionRoot: () => sessionRoot, getSessionId: () => sessionId || null });
|
|
111
|
-
/** P0-3: candidate windows are generated in code; jev only picks (rerank pattern). */
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const tailExcerpt = completeLineExcerpt(text, excerptBudgetBytes - halfBudget, true);
|
|
119
|
-
// Credential-shaped text never leaves the process (§8 privacy boundary).
|
|
120
|
-
if (!excerptAskIsSafe(windows, headExcerpt, tailExcerpt)) return null;
|
|
121
|
-
const request = excerptPickRequest(windows, headExcerpt, tailExcerpt);
|
|
122
|
-
if (!request) return null;
|
|
114
|
+
/** P0-3: candidate windows are generated in code; jev only picks (rerank pattern), one request per batch. */
|
|
115
|
+
const pickMiddleExcerpts: PickMiddleExcerpts = async (items, excerptBudgetBytes) => {
|
|
116
|
+
const picks: Array<{ text: string; label: string } | null> = items.map(() => null);
|
|
117
|
+
if (!jevRuntime.available) return picks;
|
|
118
|
+
const asked = planExcerptAsks(items, excerptBudgetBytes);
|
|
119
|
+
const request = excerptPickRequest(asked);
|
|
120
|
+
if (!request) return picks;
|
|
123
121
|
const result = await jevRuntime.ask(request, {
|
|
124
122
|
questionId: 'obs-excerpt-window',
|
|
125
|
-
extra: {
|
|
123
|
+
extra: {
|
|
124
|
+
site: 'obs-excerpt',
|
|
125
|
+
items: asked.map((a) => ({ obsId: a.obsId, windows: a.windows.map((w) => ({ id: w.id, startLine: w.startLine })) })),
|
|
126
|
+
},
|
|
126
127
|
enrich: ({ answers }) => {
|
|
127
|
-
if (!answers
|
|
128
|
-
return {
|
|
128
|
+
if (!answers) return {};
|
|
129
|
+
return {
|
|
130
|
+
picked: asked.map((_, i) => {
|
|
131
|
+
const answer = answers[`excerpt_${i}_window`];
|
|
132
|
+
return answer?.type === 'choice' ? { choice: answer.choice, confidence: answer.confidence } : null;
|
|
133
|
+
}),
|
|
134
|
+
};
|
|
129
135
|
},
|
|
130
136
|
});
|
|
131
|
-
if (!result.ok) return
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
137
|
+
if (!result.ok) return picks;
|
|
138
|
+
for (let i = 0; i < asked.length; i++) {
|
|
139
|
+
const { index, windows } = asked[i]!;
|
|
140
|
+
const picked = evaluateExcerptPick(result.answers, i, effConfig.jev.minConfidence);
|
|
141
|
+
const window = picked === null ? undefined : windows.find((w) => w.id === picked);
|
|
142
|
+
if (window) picks[index] = { text: window.text, label: window.label };
|
|
143
|
+
}
|
|
144
|
+
return picks;
|
|
135
145
|
};
|
|
136
146
|
|
|
137
147
|
const rendererTypes = installRenderers(pi);
|
|
@@ -189,16 +199,87 @@ export default async function (pi: ExtensionAPI) {
|
|
|
189
199
|
const p = progressOf(todos.items);
|
|
190
200
|
const eta = estimateEta({ completedAt: completedStamps, total: p.total, now: Date.now() });
|
|
191
201
|
const suffix = formatProgressSuffix({ completed: p.completed, total: p.total, eta });
|
|
192
|
-
const title =
|
|
202
|
+
const title = applyWorkerTag(
|
|
203
|
+
formatPaneTitle(todos.items, null, { progressSuffix: suffix, lastWriteAt: todos.lastWriteAt }),
|
|
204
|
+
delegation?.description ?? null,
|
|
205
|
+
);
|
|
193
206
|
updateSlimFrame({ title, items: todos.items, lastWriteAt: todos.lastWriteAt });
|
|
194
207
|
client.reportMetadata({
|
|
195
208
|
session: label,
|
|
196
209
|
items: todos.items,
|
|
197
210
|
progressSuffix: suffix,
|
|
198
211
|
lastWriteAt: todos.lastWriteAt,
|
|
212
|
+
workerTag: delegation?.description ?? null,
|
|
199
213
|
}).catch(() => {});
|
|
214
|
+
// D113: delegated workers also push the full list to the master's pipe so its resident widget
|
|
215
|
+
// and /pier-tree stay live without polling. Best effort — a lost push self-heals on the next write.
|
|
216
|
+
if (isSubagent && delegation?.parentPipe) {
|
|
217
|
+
void pipeRequest(delegation.parentPipe, {
|
|
218
|
+
type: 'todo_sync',
|
|
219
|
+
id: `tsync-${Date.now()}`,
|
|
220
|
+
paneId: env?.paneId ?? '',
|
|
221
|
+
items: todos.items.map((it) => ({ content: it.content, status: it.status, ...(it.blocker ? { blocker: it.blocker } : {}) })),
|
|
222
|
+
summary: title ?? delegation.description,
|
|
223
|
+
}, 3000).catch(() => {});
|
|
224
|
+
}
|
|
200
225
|
}
|
|
201
226
|
|
|
227
|
+
/** D113: per-worker todo cache (pipe pushes; summary fallback after a master resume). */
|
|
228
|
+
const workerTodos = new Map<string, WorkerTodoSnapshot>();
|
|
229
|
+
function applyWorkerTodos(workerPaneId: string, items: WorkerTodoSnapshot['items'], summary: string): void {
|
|
230
|
+
if (!workerPaneId) return;
|
|
231
|
+
workerTodos.set(workerPaneId, { items, summary, ts: Date.now() });
|
|
232
|
+
todoUi.rerenderWidget?.();
|
|
233
|
+
refreshTreeSnapshot();
|
|
234
|
+
}
|
|
235
|
+
/** Running rows joined with their cached todos — feeds the unified widget and /pier-tree. */
|
|
236
|
+
function widgetWorkers(): WorkerWidgetRow[] {
|
|
237
|
+
return (subagentPort.current?.listRunningSubs() ?? []).map((s) => ({
|
|
238
|
+
...s,
|
|
239
|
+
todos: workerTodos.get(s.paneId) ?? null,
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* ── /pier-tree panel: the pi process owns the data, the popup/overlay owns the surface. ── */
|
|
244
|
+
/** Set while a panel is open; todo/worker updates republish the snapshot it polls. */
|
|
245
|
+
let treeSnapshot: { path: string } | null = null;
|
|
246
|
+
|
|
247
|
+
function renderTreeLines(): string[] {
|
|
248
|
+
const rows = subagentPort.current?.listRows() ?? [];
|
|
249
|
+
return formatDelegationTree({
|
|
250
|
+
selfItems: todos.items,
|
|
251
|
+
workers: widgetWorkers(),
|
|
252
|
+
settled: rows
|
|
253
|
+
.filter((r) => r.status === 'settled' || r.status === 'consumed')
|
|
254
|
+
.map((r) => ({ paneId: r.paneId, slug: r.slug, description: r.description, settledAt: r.settledAt })),
|
|
255
|
+
now: Date.now(),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function treeSnapshotPath(): string {
|
|
260
|
+
const id = (sessionId || (env ? `pane-${env.paneId}` : 'session')).replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 80);
|
|
261
|
+
return join(tmpdir(), 'pier-tree', `${id}.txt`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function publishTreeSnapshot(): string {
|
|
265
|
+
const path = treeSnapshotPath();
|
|
266
|
+
mkdirSync(join(tmpdir(), 'pier-tree'), { recursive: true });
|
|
267
|
+
writeFileSync(path, `${renderTreeLines().join('\n')}\n`);
|
|
268
|
+
treeSnapshot = { path };
|
|
269
|
+
return path;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function refreshTreeSnapshot(): void {
|
|
273
|
+
if (!treeSnapshot) return;
|
|
274
|
+
try {
|
|
275
|
+
writeFileSync(treeSnapshot.path, `${renderTreeLines().join('\n')}\n`);
|
|
276
|
+
} catch {
|
|
277
|
+
/* The panel may have been closed and its temp dir swept; the next command republishes. */
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
todos.on('todo.updated', () => refreshTreeSnapshot());
|
|
282
|
+
|
|
202
283
|
/** M17 settlement reconciliation: auto-complete settled todos / unblock on failure notes (idempotent). */
|
|
203
284
|
function reconcileOnSettlement(description: string, outcome: 'settled' | 'failed'): string[] {
|
|
204
285
|
if (!description) return [];
|
|
@@ -214,6 +295,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
214
295
|
}
|
|
215
296
|
todos.applyEdits(plan.edits, { source: 'reconcile' });
|
|
216
297
|
mirrorTodos();
|
|
298
|
+
// Reconcile is not a tool call, so nothing else refreshes the resident widget after an edit.
|
|
299
|
+
todoUi.rerenderWidget?.();
|
|
217
300
|
}
|
|
218
301
|
return plan.noteLines;
|
|
219
302
|
} catch {
|
|
@@ -246,10 +329,24 @@ export default async function (pi: ExtensionAPI) {
|
|
|
246
329
|
reportToolBadge();
|
|
247
330
|
}
|
|
248
331
|
});
|
|
249
|
-
pi.on('tool_execution_end', async (event: { toolCallId?: string }) => {
|
|
250
|
-
if (typeof event?.toolCallId
|
|
251
|
-
|
|
252
|
-
|
|
332
|
+
pi.on('tool_execution_end', async (event: { toolCallId?: string; toolName?: string; durationMs?: number }) => {
|
|
333
|
+
if (typeof event?.toolCallId !== 'string') return;
|
|
334
|
+
const tool = typeof event.toolName === 'string' ? event.toolName : runningTools.get(event.toolCallId) ?? null;
|
|
335
|
+
runningTools.delete(event.toolCallId);
|
|
336
|
+
reportToolBadge();
|
|
337
|
+
// pi ≥ 1.1 records the real execution time on the event; the tools ledger replaces wall-clock
|
|
338
|
+
// estimates in the efficiency trial with measured numbers. Absent on older pi — nothing logged.
|
|
339
|
+
if (typeof event.durationMs === 'number' && sessionRoot) {
|
|
340
|
+
try {
|
|
341
|
+
await appendEfficiencyLog(efficiencyLogPath(sessionRoot, 'tools'), {
|
|
342
|
+
sessionId: sessionId || 'unknown',
|
|
343
|
+
ts: Date.now(),
|
|
344
|
+
tool,
|
|
345
|
+
durationMs: event.durationMs,
|
|
346
|
+
});
|
|
347
|
+
} catch {
|
|
348
|
+
/* ledger is observability only */
|
|
349
|
+
}
|
|
253
350
|
}
|
|
254
351
|
});
|
|
255
352
|
|
|
@@ -263,6 +360,18 @@ export default async function (pi: ExtensionAPI) {
|
|
|
263
360
|
});
|
|
264
361
|
});
|
|
265
362
|
|
|
363
|
+
/* ── Masters sleep-polled background workers (sleep 120…840) instead of trusting the settlement notice. ── */
|
|
364
|
+
if (!isSubagent) {
|
|
365
|
+
pi.on('tool_result', async (event: unknown) => {
|
|
366
|
+
const rec = (event ?? {}) as { toolName?: unknown; input?: { command?: unknown }; content?: unknown };
|
|
367
|
+
if (rec.toolName !== 'bash' || typeof rec.input?.command !== 'string') return;
|
|
368
|
+
const note = sleepWhileSubsRunningNote(rec.input.command, subagentPort.current?.listRunningSubs() ?? []);
|
|
369
|
+
if (!note) return;
|
|
370
|
+
const content = Array.isArray(rec.content) ? rec.content as Array<{ type: 'text'; text: string }> : [];
|
|
371
|
+
return { content: [...content, { type: 'text' as const, text: note }] };
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
266
375
|
let locksHandle = { getHeldLocks: () => [] as readonly string[] };
|
|
267
376
|
if (env) {
|
|
268
377
|
locksHandle = installWriteLocks(pi, { client, env, hard: process.env[WRITE_LOCK_ENV] === '1' });
|
|
@@ -274,8 +383,12 @@ export default async function (pi: ExtensionAPI) {
|
|
|
274
383
|
.sendUserMessage?.(content, { deliverAs: mode, triggerTurn: true });
|
|
275
384
|
return Promise.resolve();
|
|
276
385
|
};
|
|
386
|
+
|
|
277
387
|
const notices = createNoticeBuffer({
|
|
278
|
-
|
|
388
|
+
// pi core rejects prompts while it is compacting (and drops fire-and-forget sends), so a
|
|
389
|
+
// settlement landing inside the compaction window must stay buffered until the continuation
|
|
390
|
+
// run settles — otherwise the result is lost with nobody left to retry it.
|
|
391
|
+
isBusy: () => agentActive || lastStopReason === ABORT_STOP_REASON || coordinator.compactionInFlight,
|
|
279
392
|
send: sendUserMessageAs,
|
|
280
393
|
// P0-2: rank collapsed batches by relevance to the master's in-progress work.
|
|
281
394
|
rank: async (contents) => {
|
|
@@ -341,13 +454,14 @@ export default async function (pi: ExtensionAPI) {
|
|
|
341
454
|
abort: () => { latestCtx?.abort?.(); },
|
|
342
455
|
setPendingMachineRequest: (next) => { pendingMachineRequest = next; },
|
|
343
456
|
applyRoleSwitch: roles.applyRoleSwitch,
|
|
457
|
+
applyWorkerTodos,
|
|
344
458
|
}));
|
|
345
459
|
} catch {
|
|
346
460
|
/* Pipe name collision (rare): this session has no channel; callers report after ping timeout. */
|
|
347
461
|
}
|
|
348
462
|
}
|
|
349
463
|
|
|
350
|
-
async function pushSettleReply(): Promise<void> {
|
|
464
|
+
async function pushSettleReply(aborted = false): Promise<void> {
|
|
351
465
|
const req = pendingMachineRequest;
|
|
352
466
|
if (!req || !req.push || !req.from) return;
|
|
353
467
|
// An aborted settle is not a settlement: OCC's abort means the continuation turn is coming, and
|
|
@@ -356,6 +470,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
356
470
|
// request armed; the post-compaction settle pushes it (settle-wake-core.ts).
|
|
357
471
|
if (!shouldPushSettleReply({
|
|
358
472
|
intentionalAbort: coordinator.intentionalAbort,
|
|
473
|
+
aborted,
|
|
359
474
|
compactionInFlight: coordinator.compactionInFlight,
|
|
360
475
|
lastStopReason,
|
|
361
476
|
})) {
|
|
@@ -383,8 +498,6 @@ export default async function (pi: ExtensionAPI) {
|
|
|
383
498
|
}
|
|
384
499
|
}
|
|
385
500
|
|
|
386
|
-
const sessionFocusPoller: { current: FocusPoller | null } = { current: null };
|
|
387
|
-
|
|
388
501
|
pi.on('session_start', async (event, ctx) => {
|
|
389
502
|
sessionId = resolveSessionId(ctx);
|
|
390
503
|
latestCtx = ctx as { abort?: () => void } | null;
|
|
@@ -412,31 +525,57 @@ export default async function (pi: ExtensionAPI) {
|
|
|
412
525
|
// Self-healing: the ask marker lives in herdr with a 24h TTL; a session killed
|
|
413
526
|
// while a dialog was open must not keep the pane looking blocked forever.
|
|
414
527
|
void client.reportAskFlag(null).catch(() => {});
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
528
|
+
if (isSubagent) {
|
|
529
|
+
// herdr 0.9.2 restore identity: the marker re-derives worker mode for the resumed boot
|
|
530
|
+
// (resume_argv cannot carry spawn env), and the self-reported command replays this exact
|
|
531
|
+
// session — extension, role model, approve, fullscreen — instead of a bare `pi --resume`.
|
|
532
|
+
const sm = typeof ctx === 'object' && ctx !== null && 'sessionManager' in ctx ? ctx.sessionManager : undefined;
|
|
533
|
+
const sessionFile = (typeof sm?.getSessionFile === 'function' ? sm.getSessionFile() : undefined)
|
|
534
|
+
?? process.env.PI_SESSION_FILE ?? null;
|
|
535
|
+
// RFC rfc-delegation-identity §2.1: env (spawn) > marker (herdr restart) > branch fold (/resume).
|
|
536
|
+
if (!delegation) delegation = readWorkerMarker(sessionFile)?.delegation ?? null;
|
|
537
|
+
const branchEntries = (ctx as { sessionManager?: { getBranch?: () => readonly unknown[] } })
|
|
538
|
+
?.sessionManager?.getBranch?.() ?? [];
|
|
539
|
+
const folded = foldDelegation(branchEntries as Parameters<typeof foldDelegation>[0]);
|
|
540
|
+
if (!delegation && folded) delegation = folded;
|
|
541
|
+
if (delegation && !folded) {
|
|
542
|
+
// First-frame card: write once — the entry doubles as the /resume replay source.
|
|
543
|
+
try {
|
|
544
|
+
pi.appendEntry(DELEGATION_CUSTOM_TYPE, { version: 1, ...delegation, ts: Date.now() });
|
|
545
|
+
} catch {
|
|
546
|
+
/* Best effort; the marker already carries the identity. */
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (delegation) void client.reportDelegationTokens(delegation).catch(() => {});
|
|
550
|
+
if (sessionRoot) {
|
|
551
|
+
writeWorkerMarker(sessionFile, roles.state.manifest?.role, delegation ?? undefined);
|
|
552
|
+
const resumeArgv = buildResumeArgv(process.argv);
|
|
553
|
+
if (resumeArgv && client.available) {
|
|
554
|
+
void client.reportAgentSessionResume(sessionId, resumeArgv).catch(() => {});
|
|
555
|
+
}
|
|
556
|
+
}
|
|
436
557
|
}
|
|
558
|
+
|
|
559
|
+
// Focus heat is event-driven on herdr 0.9.5+: the workbench receives pane.focused directly,
|
|
560
|
+
// so no per-pane layout sampler is started here.
|
|
437
561
|
startPipeServerFor(cwd);
|
|
438
562
|
});
|
|
439
563
|
|
|
564
|
+
/* RFC rfc-delegation-identity §2.2: worker identity + delivery contract ride the prompt
|
|
565
|
+
* sections each turn (same diff discipline as pier-role; removal cleans up). */
|
|
566
|
+
if (isSubagent) {
|
|
567
|
+
pi.on('before_agent_start', async (event) => {
|
|
568
|
+
const sections = event.systemPromptOptions?.sections;
|
|
569
|
+
if (!sections) return;
|
|
570
|
+
if (delegation) {
|
|
571
|
+
const text = buildDelegationSection(delegation, env?.paneId ?? '');
|
|
572
|
+
if (sections['pier-delegation'] !== text) sections['pier-delegation'] = text;
|
|
573
|
+
} else {
|
|
574
|
+
delete sections['pier-delegation'];
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
440
579
|
pi.on('session_tree', async (_event, ctx) => {
|
|
441
580
|
sessionId = resolveSessionId(ctx);
|
|
442
581
|
rebuildFromBranch(ctx);
|
|
@@ -447,6 +586,12 @@ export default async function (pi: ExtensionAPI) {
|
|
|
447
586
|
mirrorTodos();
|
|
448
587
|
});
|
|
449
588
|
|
|
589
|
+
/* pi 1.2 counts the run as busy from `before_agent_start`, not `turn_start`: a settlement
|
|
590
|
+
* notice arriving in that window must enter the D92 buffer instead of being sent immediately. */
|
|
591
|
+
pi.on('before_agent_start', async () => {
|
|
592
|
+
agentActive = true;
|
|
593
|
+
});
|
|
594
|
+
|
|
450
595
|
pi.on('turn_start', async () => {
|
|
451
596
|
agentActive = true;
|
|
452
597
|
// A11: reset so a previous turn's abort cannot taint this one.
|
|
@@ -454,6 +599,9 @@ export default async function (pi: ExtensionAPI) {
|
|
|
454
599
|
// Safety fallback: pi core prohibits prompts while compaction is active.
|
|
455
600
|
coordinator.compactionInFlight = false;
|
|
456
601
|
coordinator.intentionalAbort = false;
|
|
602
|
+
// A selection that never became startable must not latch every later decision (and the
|
|
603
|
+
// settle-wake hold) until the next human input.
|
|
604
|
+
coordinator.selectedCompaction = null;
|
|
457
605
|
gates.report('working', currentActivity(todos.items));
|
|
458
606
|
});
|
|
459
607
|
|
|
@@ -477,15 +625,20 @@ export default async function (pi: ExtensionAPI) {
|
|
|
477
625
|
// flush ran regardless. The merged handler keeps both semantics.
|
|
478
626
|
const msg = event !== null && typeof event === 'object' && 'message' in event ? (event as { message: unknown }).message : null;
|
|
479
627
|
if (msg !== null && typeof msg === 'object') {
|
|
480
|
-
const { role, stopReason } = msg as { role?: unknown; stopReason?: unknown };
|
|
628
|
+
const { role, stopReason, usage } = msg as { role?: unknown; stopReason?: unknown; usage?: unknown };
|
|
481
629
|
if (role === 'assistant' && typeof stopReason === 'string') {
|
|
482
630
|
lastStopReason = stopReason;
|
|
483
631
|
}
|
|
632
|
+
if (role === 'assistant' && usage !== null && typeof usage === 'object') {
|
|
633
|
+
coordinator.onAssistantUsage(usage as { input?: number; cacheRead?: number; cacheWrite?: number });
|
|
634
|
+
}
|
|
484
635
|
if (ctx && typeof ctx === 'object') {
|
|
485
636
|
coordinator.onTurnEnd({
|
|
486
637
|
ctx,
|
|
487
638
|
todos: todos.items,
|
|
488
639
|
config: effConfig.onlineContextCompact,
|
|
640
|
+
pi,
|
|
641
|
+
lastStopReason: typeof stopReason === 'string' ? stopReason : null,
|
|
489
642
|
cancelReminder: todoUi.cancelReminder,
|
|
490
643
|
});
|
|
491
644
|
}
|
|
@@ -493,12 +646,16 @@ export default async function (pi: ExtensionAPI) {
|
|
|
493
646
|
void notices.flush('steer');
|
|
494
647
|
});
|
|
495
648
|
|
|
496
|
-
pi.on('agent_settled', async (
|
|
649
|
+
pi.on('agent_settled', async (event: unknown, ctx) => {
|
|
497
650
|
agentActive = false;
|
|
498
651
|
gates.report('idle', null);
|
|
652
|
+
// pi ≥ 1.1 stamps cancelled runs on the event itself; stopReason tracking misses ESC mid-tool
|
|
653
|
+
// (no final assistant message), so the event field is authoritative when present.
|
|
654
|
+
const aborted = event !== null && typeof event === 'object' && 'aborted' in event && event.aborted === true;
|
|
499
655
|
const plan = planSettleWake({
|
|
500
656
|
lastStopReason,
|
|
501
657
|
intentionalAbort: coordinator.intentionalAbort,
|
|
658
|
+
aborted,
|
|
502
659
|
compactionPending: coordinator.selectedCompaction !== null || coordinator.compactionInFlight,
|
|
503
660
|
running: subagentPort.current?.listRunningSubs() ?? [],
|
|
504
661
|
lastNoticeKey: d96NoticeKey,
|
|
@@ -515,6 +672,13 @@ export default async function (pi: ExtensionAPI) {
|
|
|
515
672
|
void sendUserMessageAs(notice, 'followUp');
|
|
516
673
|
}
|
|
517
674
|
if (plan.compact && ctx && typeof ctx === 'object') {
|
|
675
|
+
// Settlement notices that land inside the compaction window stay buffered (isBusy includes
|
|
676
|
+
// compactionInFlight). The continuation run flushes them at its own agent_settled; a cancelled
|
|
677
|
+
// or rejected compaction sends no continuation, so release the buffer once the coordinator is
|
|
678
|
+
// done and no run has started.
|
|
679
|
+
const flushAfterCompaction = (): void => {
|
|
680
|
+
if (!coordinator.compactionInFlight && !agentActive) void notices.flush('followUp');
|
|
681
|
+
};
|
|
518
682
|
void coordinator.onAgentSettled({
|
|
519
683
|
ctx,
|
|
520
684
|
todos: todos.items,
|
|
@@ -525,12 +689,12 @@ export default async function (pi: ExtensionAPI) {
|
|
|
525
689
|
getObsConfig: () => effConfig.observationPack,
|
|
526
690
|
getManifest: () => roles.state.manifest,
|
|
527
691
|
getSessionId: () => sessionId,
|
|
528
|
-
|
|
692
|
+
pickMiddleExcerpts,
|
|
529
693
|
}),
|
|
530
|
-
});
|
|
694
|
+
}).then(flushAfterCompaction, flushAfterCompaction);
|
|
531
695
|
}
|
|
532
|
-
if (lastStopReason !== ABORT_STOP_REASON) void notices.flush('followUp');
|
|
533
|
-
void pushSettleReply();
|
|
696
|
+
if (lastStopReason !== ABORT_STOP_REASON && !aborted) void notices.flush('followUp');
|
|
697
|
+
void pushSettleReply(aborted);
|
|
534
698
|
});
|
|
535
699
|
|
|
536
700
|
/* ── D101 ObservationPack (obs_recall tool & context projector) ── */
|
|
@@ -538,7 +702,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
538
702
|
pi,
|
|
539
703
|
getConfig: () => effConfig,
|
|
540
704
|
getRuntimeManifest: () => roles.state.manifest,
|
|
541
|
-
|
|
705
|
+
pickMiddleExcerpts,
|
|
542
706
|
getRemainingHorizon: () => {
|
|
543
707
|
const usage = (latestCtx as { getContextUsage?: () => { contextWindow?: number } })?.getContextUsage?.();
|
|
544
708
|
return coordinator.getRemainingHorizon(3, usage?.contextWindow ?? null);
|
|
@@ -552,6 +716,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
552
716
|
env,
|
|
553
717
|
getTodoItems: () => todos.items,
|
|
554
718
|
getHeldLocks: () => locksHandle.getHeldLocks(),
|
|
719
|
+
prepareTree: () => publishTreeSnapshot(),
|
|
555
720
|
});
|
|
556
721
|
|
|
557
722
|
/* ── ask_user_question (v1.3 M8 human gate; master and subagents) ── */
|
|
@@ -561,6 +726,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
561
726
|
description: ASK_TOOL_DESCRIPTION,
|
|
562
727
|
promptGuidelines: ASK_PROMPT_GUIDELINES,
|
|
563
728
|
parameters: ASK_PARAMETERS,
|
|
729
|
+
// Models sometimes send options/questions as a JSON string; repair them before pi validates.
|
|
730
|
+
prepareArguments: prepareAskArguments,
|
|
564
731
|
async execute(_tc, params, signal, _upd, ctx) {
|
|
565
732
|
const prepared = prepareAsk(params);
|
|
566
733
|
if (!prepared.ok) return prepared.result;
|
|
@@ -580,8 +747,6 @@ export default async function (pi: ExtensionAPI) {
|
|
|
580
747
|
pi.on('session_shutdown', async () => {
|
|
581
748
|
coordinator.compactionInFlight = false;
|
|
582
749
|
coordinator.intentionalAbort = false;
|
|
583
|
-
sessionFocusPoller.current?.stop();
|
|
584
|
-
sessionFocusPoller.current = null;
|
|
585
750
|
if (pipeServerBox.current) {
|
|
586
751
|
try { pipeServerBox.current.close(); } catch { /* Already closed. */ }
|
|
587
752
|
pipeServerBox.current = null;
|
|
@@ -592,6 +757,14 @@ export default async function (pi: ExtensionAPI) {
|
|
|
592
757
|
} catch {
|
|
593
758
|
}
|
|
594
759
|
}
|
|
760
|
+
if (treeSnapshot) {
|
|
761
|
+
try {
|
|
762
|
+
unlinkSync(treeSnapshot.path);
|
|
763
|
+
} catch {
|
|
764
|
+
/* Already swept. */
|
|
765
|
+
}
|
|
766
|
+
treeSnapshot = null;
|
|
767
|
+
}
|
|
595
768
|
client.close();
|
|
596
769
|
});
|
|
597
770
|
|
|
@@ -612,6 +785,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
612
785
|
noticePending: notices.noticePending,
|
|
613
786
|
getSessionId: () => sessionId,
|
|
614
787
|
getBlockedDepth: () => gates.depth(),
|
|
788
|
+
getWorkers: widgetWorkers,
|
|
615
789
|
reconcileOnSettlement,
|
|
616
790
|
withReconcileNotes,
|
|
617
791
|
claimSettleNotice,
|
|
@@ -620,6 +794,66 @@ export default async function (pi: ExtensionAPI) {
|
|
|
620
794
|
jev: { ask: (request, meta) => jevRuntime.ask(request, meta), getMinConfidence: () => effConfig.jev.minConfidence },
|
|
621
795
|
appendRoutingLog,
|
|
622
796
|
});
|
|
797
|
+
|
|
798
|
+
/* ── /pier-tree (D113): full todo × subagent delegation tree; the resident widget is minimal ── */
|
|
799
|
+
pi.registerCommand('pier-tree', {
|
|
800
|
+
description: 'Show the full delegation tree: your todos plus every subagent\'s todos and settled nodes',
|
|
801
|
+
handler: async (args: unknown, ctx: unknown) => {
|
|
802
|
+
const ui = (ctx as { ui?: { notify?: (text: string, level?: string) => void } }).ui;
|
|
803
|
+
const lines = renderTreeLines();
|
|
804
|
+
const overlay = typeof args === 'string' && args.trim().toLowerCase() === 'overlay';
|
|
805
|
+
|
|
806
|
+
if (client.available && env) {
|
|
807
|
+
const treeFile = publishTreeSnapshot();
|
|
808
|
+
if (overlay) {
|
|
809
|
+
try {
|
|
810
|
+
const res = await client.openPluginPane({
|
|
811
|
+
pluginId: 'pier.workbench',
|
|
812
|
+
entrypoint: 'tree',
|
|
813
|
+
placement: 'overlay',
|
|
814
|
+
focus: true,
|
|
815
|
+
env: { PIER_PANEL: 'tree', PIER_TREE_FILE: treeFile },
|
|
816
|
+
});
|
|
817
|
+
if (res.mode === 'pane') return;
|
|
818
|
+
} catch {
|
|
819
|
+
// Plugin missing / older herdr: fall back to the in-pane notify view below.
|
|
820
|
+
}
|
|
821
|
+
} else {
|
|
822
|
+
const res = await openPierPanel({ client, env }, { panel: 'tree', treeFile });
|
|
823
|
+
if (res === 'popup') return;
|
|
824
|
+
if (res === 'tab') {
|
|
825
|
+
ui?.notify?.('Herdr < 0.9.1: Pier Tree opened in new tab', 'info');
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
if (res === 'busy') {
|
|
829
|
+
ui?.notify?.('Another popup is already open — close it and run /pier-tree again.', 'warning');
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
// 'unavailable' / failed overlay → the old in-pane view.
|
|
834
|
+
}
|
|
835
|
+
treeSnapshot = null;
|
|
836
|
+
ui?.notify?.(lines.join('\n'), 'info');
|
|
837
|
+
},
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
/** D113 resync: a resumed master rebuilds worker summaries from herdr pi-todo tokens until the
|
|
841
|
+
* next live push replaces them with full lists. */
|
|
842
|
+
const resyncWorkerTodos = async (): Promise<void> => {
|
|
843
|
+
try {
|
|
844
|
+
const agents = await client.listAgents();
|
|
845
|
+
for (const a of agents) {
|
|
846
|
+
const summary = a.tokens?.['pi-todo'];
|
|
847
|
+
if (typeof summary === 'string' && summary && !workerTodos.has(a.paneId)) {
|
|
848
|
+
workerTodos.set(a.paneId, { summary, ts: Date.now() });
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
todoUi.rerenderWidget?.();
|
|
852
|
+
} catch {
|
|
853
|
+
/* herdr absent or unreachable: live pushes remain the source */
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
pi.on('session_start', () => { void resyncWorkerTodos(); });
|
|
623
857
|
} else {
|
|
624
858
|
const { mountTodoOnly } = await import('./index-worker.ts');
|
|
625
859
|
await mountTodoOnly({
|