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
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Child-session JSONL I/O: path resolution, settlement text, liveness probes.
|
|
3
|
-
*
|
|
4
|
-
* Why: pollLoop, foreground wait, and revive all shared the same candidate
|
|
5
|
-
* order (reported path/id before recent-file fallback, excluding the parent
|
|
6
|
-
* session). Keeping it in one adapter prevents settlement text from crossing sessions.
|
|
7
|
-
*/
|
|
8
|
-
import { accessSync, constants, statSync } from 'node:fs';
|
|
9
|
-
import type { HerdrClientLike } from './herdr-client.ts';
|
|
10
|
-
import {
|
|
11
|
-
deriveSubSessionState,
|
|
12
|
-
lastAssistantText,
|
|
13
|
-
listSessionFiles,
|
|
14
|
-
readSessionFile,
|
|
15
|
-
sessionFileById,
|
|
16
|
-
type SessionEntryLike,
|
|
17
|
-
type SubSessionState,
|
|
18
|
-
} from './session-tail.ts';
|
|
19
|
-
import type { AliveProbe } from './subagent-core.ts';
|
|
20
|
-
|
|
21
|
-
export interface SessionIoHost {
|
|
22
|
-
client: HerdrClientLike;
|
|
23
|
-
getSessionId: () => string;
|
|
24
|
-
sessionsDir: () => string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface SessionIo {
|
|
28
|
-
resolveSessionFileCandidates(paneId: string, cwd: string): Promise<string[]>;
|
|
29
|
-
resolveSessionFile(paneId: string, cwd: string): Promise<string | null>;
|
|
30
|
-
collectFinalText(paneId: string, cwd: string, sinceTs: number, attempts?: number): Promise<string | null>;
|
|
31
|
-
readAskFlag(paneId: string): Promise<string | null>;
|
|
32
|
-
probeAlive(paneId: string, cwd: string): Promise<AliveProbe>;
|
|
33
|
-
subSessionState(paneId: string, cwd: string, sinceTs: number): Promise<SubSessionState>;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Cheap change fingerprint of a session file; null when it does not exist. */
|
|
37
|
-
interface FileStamp {
|
|
38
|
-
size: number;
|
|
39
|
-
mtimeMs: number;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function stampOf(file: string): FileStamp | null {
|
|
43
|
-
try {
|
|
44
|
-
const s = statSync(file);
|
|
45
|
-
return { size: s.size, mtimeMs: s.mtimeMs };
|
|
46
|
-
} catch {
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Readable regular file check: parsing the file would read it in full (up to MBs). */
|
|
52
|
-
function isReadableFile(file: string): boolean {
|
|
53
|
-
try {
|
|
54
|
-
if (!statSync(file).isFile()) return false;
|
|
55
|
-
accessSync(file, constants.R_OK);
|
|
56
|
-
return true;
|
|
57
|
-
} catch {
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Memo of values derived from a session file, keyed by path and invalidated by (size, mtime).
|
|
64
|
-
*
|
|
65
|
-
* Why: worker sessions grow to tens of MB, and the poll loop re-derives the same idle state every
|
|
66
|
-
* tick (collectFinalText re-derives up to `attempts` times). Measured on a 31.6MB session:
|
|
67
|
-
* file read + JSON.parse + derive ≈ 227ms, so an unchanged file must not be parsed twice.
|
|
68
|
-
* An append-only JSONL changes size on every write, so (size, mtime) is a sound fingerprint.
|
|
69
|
-
*/
|
|
70
|
-
class DerivedCache<T> {
|
|
71
|
-
private readonly entries = new Map<string, { stamp: FileStamp; sinceTs: number; value: T }>();
|
|
72
|
-
private readonly limit: number;
|
|
73
|
-
|
|
74
|
-
constructor(limit = 32) {
|
|
75
|
-
this.limit = limit;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
get(file: string, stamp: FileStamp, sinceTs: number): T | undefined {
|
|
79
|
-
const hit = this.entries.get(file);
|
|
80
|
-
if (!hit || hit.stamp.size !== stamp.size || hit.stamp.mtimeMs !== stamp.mtimeMs || hit.sinceTs !== sinceTs) {
|
|
81
|
-
return undefined;
|
|
82
|
-
}
|
|
83
|
-
return hit.value;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
set(file: string, stamp: FileStamp, sinceTs: number, value: T): void {
|
|
87
|
-
this.entries.delete(file);
|
|
88
|
-
this.entries.set(file, { stamp, sinceTs, value });
|
|
89
|
-
while (this.entries.size > this.limit) {
|
|
90
|
-
const oldest = this.entries.keys().next().value;
|
|
91
|
-
if (oldest === undefined) break;
|
|
92
|
-
this.entries.delete(oldest);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function createSessionIo(h: SessionIoHost): SessionIo {
|
|
98
|
-
const stateCache = new DerivedCache<SubSessionState>();
|
|
99
|
-
const finalTextCache = new DerivedCache<string | null>();
|
|
100
|
-
|
|
101
|
-
async function resolveSessionFileCandidates(paneId: string, cwd: string): Promise<string[]> {
|
|
102
|
-
const out: string[] = [];
|
|
103
|
-
try {
|
|
104
|
-
const reported = await h.client.getAgentSessionPath(paneId);
|
|
105
|
-
if (reported) {
|
|
106
|
-
if (/\.jsonl$/.test(reported)) out.push(reported);
|
|
107
|
-
else {
|
|
108
|
-
const byId = sessionFileById(cwd, h.sessionsDir(), reported);
|
|
109
|
-
if (byId) out.push(byId);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
} catch {
|
|
113
|
-
/* Reports may be unavailable during startup. */
|
|
114
|
-
}
|
|
115
|
-
const ownSession = h.getSessionId();
|
|
116
|
-
for (const f of listSessionFiles(cwd, h.sessionsDir(), 4)) {
|
|
117
|
-
if (f !== ownSession && !out.includes(f)) out.push(f);
|
|
118
|
-
}
|
|
119
|
-
return out;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async function resolveSessionFile(paneId: string, cwd: string): Promise<string | null> {
|
|
123
|
-
for (const file of await resolveSessionFileCandidates(paneId, cwd)) {
|
|
124
|
-
if (isReadableFile(file)) return file;
|
|
125
|
-
}
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
async function collectFinalText(
|
|
130
|
-
paneId: string,
|
|
131
|
-
cwd: string,
|
|
132
|
-
sinceTs: number,
|
|
133
|
-
attempts = 12,
|
|
134
|
-
): Promise<string | null> {
|
|
135
|
-
for (let i = 0; i < attempts; i++) {
|
|
136
|
-
for (const file of await resolveSessionFileCandidates(paneId, cwd)) {
|
|
137
|
-
const stamp = stampOf(file);
|
|
138
|
-
if (!stamp) continue;
|
|
139
|
-
const cached = finalTextCache.get(file, stamp, sinceTs);
|
|
140
|
-
if (cached !== undefined) {
|
|
141
|
-
if (cached) return cached;
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
const entries: SessionEntryLike[] | null = readSessionFile(file);
|
|
145
|
-
if (!entries) continue;
|
|
146
|
-
const text = lastAssistantText(entries, { sinceTs })?.text ?? null;
|
|
147
|
-
finalTextCache.set(file, stamp, sinceTs, text);
|
|
148
|
-
if (text) return text;
|
|
149
|
-
}
|
|
150
|
-
const wait = Promise.withResolvers<void>();
|
|
151
|
-
setTimeout(wait.resolve, 500);
|
|
152
|
-
await wait.promise;
|
|
153
|
-
}
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function readAskFlag(paneId: string): Promise<string | null> {
|
|
158
|
-
try {
|
|
159
|
-
const a = (await h.client.listAgents()).find((x) => x.paneId === paneId);
|
|
160
|
-
const v = a?.tokens?.['pi-ask'];
|
|
161
|
-
return typeof v === 'string' && v ? v : null;
|
|
162
|
-
} catch {
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
async function probeAlive(paneId: string, cwd: string): Promise<AliveProbe> {
|
|
168
|
-
const probe: AliveProbe = { paneExists: false, agentStatus: null, lastActivityMs: null };
|
|
169
|
-
let listed = false;
|
|
170
|
-
try {
|
|
171
|
-
const pane = (await h.client.listPanes()).find((p) => p.paneId === paneId);
|
|
172
|
-
listed = true;
|
|
173
|
-
probe.paneExists = pane != null;
|
|
174
|
-
probe.agentStatus = pane?.agentStatus ?? null;
|
|
175
|
-
if (pane?.foregroundCwd) probe.foregroundCwd = pane.foregroundCwd;
|
|
176
|
-
} catch {
|
|
177
|
-
/* pane.list unavailable — agent.list is a degraded fallback, not a death signal */
|
|
178
|
-
}
|
|
179
|
-
try {
|
|
180
|
-
const a = (await h.client.listAgents()).find((x) => x.paneId === paneId);
|
|
181
|
-
if (a) {
|
|
182
|
-
if (!listed) probe.paneExists = true;
|
|
183
|
-
probe.agentStatus = a.status ?? probe.agentStatus;
|
|
184
|
-
if (a.foregroundCwd) probe.foregroundCwd = a.foregroundCwd;
|
|
185
|
-
}
|
|
186
|
-
} catch {
|
|
187
|
-
/* agent.list is status-only; absence is not death */
|
|
188
|
-
}
|
|
189
|
-
for (const file of await resolveSessionFileCandidates(paneId, cwd)) {
|
|
190
|
-
const stamp = stampOf(file);
|
|
191
|
-
// Session files can disappear during candidate scanning; a vanished file has no activity.
|
|
192
|
-
if (!stamp) continue;
|
|
193
|
-
if (probe.lastActivityMs == null || stamp.mtimeMs > probe.lastActivityMs) probe.lastActivityMs = stamp.mtimeMs;
|
|
194
|
-
}
|
|
195
|
-
return probe;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
async function subSessionState(
|
|
199
|
-
paneId: string,
|
|
200
|
-
cwd: string,
|
|
201
|
-
sinceTs: number,
|
|
202
|
-
): Promise<SubSessionState> {
|
|
203
|
-
for (const file of await resolveSessionFileCandidates(paneId, cwd)) {
|
|
204
|
-
const stamp = stampOf(file);
|
|
205
|
-
// A missing or vanished path is skipped: herdr often reports a .jsonl path before the worker
|
|
206
|
-
// creates the file, and waitAgent(idle) returns immediately, which used to throw
|
|
207
|
-
// `Cannot read properties of null (reading 'length')` (session 01a055c5).
|
|
208
|
-
if (!stamp) continue;
|
|
209
|
-
const cached = stateCache.get(file, stamp, sinceTs);
|
|
210
|
-
if (cached) return cached;
|
|
211
|
-
const entries = readSessionFile(file);
|
|
212
|
-
// An empty/unparsable file has no state to cache; the next candidate may still have one.
|
|
213
|
-
if (!entries?.length) continue;
|
|
214
|
-
const state = deriveSubSessionState(entries, sinceTs);
|
|
215
|
-
stateCache.set(file, stamp, sinceTs, state);
|
|
216
|
-
return state;
|
|
217
|
-
}
|
|
218
|
-
return { text: null, pendingTool: false, activity: false, turnEnded: false };
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return {
|
|
222
|
-
resolveSessionFileCandidates,
|
|
223
|
-
resolveSessionFile,
|
|
224
|
-
collectFinalText,
|
|
225
|
-
readAskFlag,
|
|
226
|
-
probeAlive,
|
|
227
|
-
subSessionState,
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|