pi-pier 0.1.4 → 0.2.0
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 +1 -1
- package/schemas/role-manifest.schema.json +1 -2
- package/src/bootstrap.ts +18 -22
- package/src/core/grid-shape.ts +13 -14
- package/src/core/subagent.ts +325 -857
- package/src/core/terminal.ts +25 -28
- package/src/core/todo.ts +47 -53
- package/src/gc-core.ts +15 -15
- package/src/git-adapter.ts +104 -0
- package/src/herdr-client.ts +76 -68
- package/src/history-store.ts +70 -36
- package/src/index-locks.ts +105 -0
- package/src/index-master.ts +113 -0
- package/src/index-notices.ts +43 -0
- package/src/index-pipe.ts +73 -0
- package/src/index-runtime.ts +23 -0
- package/src/index-worker.ts +39 -0
- package/src/index.ts +134 -386
- package/src/ledger.ts +15 -17
- package/src/lock-core.ts +15 -29
- package/src/manifest-compose.ts +9 -23
- package/src/notice-buffer.ts +9 -14
- package/src/pane-title.ts +24 -24
- package/src/pi-surface.ts +22 -29
- package/src/pipe-channel.ts +67 -34
- package/src/platform-paths.ts +72 -0
- package/src/progress-core.ts +26 -24
- package/src/reconcile-core.ts +25 -24
- package/src/role-loader.ts +32 -35
- package/src/role-manifest.ts +24 -28
- package/src/roles/master.json +2 -2
- package/src/roles/worker-default.json +3 -3
- package/src/runtime-policy.ts +66 -0
- package/src/session-tail.ts +46 -45
- package/src/settle-wake-core.ts +19 -18
- package/src/slim-frame.ts +45 -44
- package/src/stale-core.ts +23 -21
- package/src/storage-layout.ts +80 -0
- package/src/subagent-core.ts +151 -94
- package/src/subagent-gc.ts +237 -0
- package/src/subagent-git-io.ts +80 -0
- package/src/subagent-launch.ts +110 -0
- package/src/subagent-poll-loop.ts +285 -0
- package/src/subagent-poller.ts +90 -0
- package/src/subagent-port.ts +27 -0
- package/src/subagent-scope.ts +4 -4
- package/src/subagent-session-io.ts +143 -0
- package/src/subagent-spawn.ts +156 -0
- package/src/terminal-core.ts +56 -54
- package/src/todo-core.ts +27 -64
- package/src/todo-read-hook.ts +28 -24
- package/src/todo-reminder-core.ts +89 -0
- package/src/todos-service.ts +36 -25
- package/src/tool-gate.ts +7 -28
- package/src/vocab.ts +18 -19
package/src/index.ts
CHANGED
|
@@ -1,39 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pi-herdr
|
|
2
|
+
* pi-herdr extension entry point (v1.1: closed-loop todos + interactive subagents,
|
|
3
|
+
* DESIGN.md §12 option C).
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
+
* Installation: `pi package` (package.json's pi.extensions) or ~/.pi/agent/extensions/.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* -
|
|
13
|
-
* -
|
|
7
|
+
* Capabilities:
|
|
8
|
+
* - `todo_write`: replace the complete todo list; the current pi session JSONL is authoritative,
|
|
9
|
+
* and branches automatically roll back in DSH-aligned semantics.
|
|
10
|
+
* - `subagent`: foreground/background delegation. Each child pane is an interactive pi TUI
|
|
11
|
+
* with an independent session/context that humans can enter directly; the controller uses
|
|
12
|
+
* herdr for state-gated injection (idle only), waiting (agent.wait), and JSONL result reads.
|
|
13
|
+
* - `list_agents` / `send_message` (followUp queue semantics) / `interrupt_agent` (esc).
|
|
14
|
+
* - `/todos`, TUI widget, and herdr title projection with graceful degradation without herdr.
|
|
14
15
|
*
|
|
15
|
-
*
|
|
16
|
-
* - onUpdate
|
|
17
|
-
* -
|
|
16
|
+
* pi 0.84.2 contract (validated):
|
|
17
|
+
* - onUpdate must have AgentToolResult shape; a string causes the TUI to crash and exit pi;
|
|
18
|
+
* - tool-result details persist in session JSONL, and getBranch() replay implements branch rollback.
|
|
18
19
|
*/
|
|
19
20
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
20
21
|
import { Type } from 'typebox';
|
|
21
|
-
import {
|
|
22
|
-
formatSettlementNotice,
|
|
23
|
-
} from './vocab.ts';
|
|
24
|
-
import { collapseNotices } from './notice-buffer.ts';
|
|
22
|
+
import { pipeNameFor, pipeRequest, startPipeServer } from './pipe-channel.ts';
|
|
25
23
|
import {
|
|
26
24
|
TODO_EDIT_CUSTOM_TYPE,
|
|
27
25
|
currentActivity,
|
|
28
26
|
} from './todo-core.ts';
|
|
29
|
-
import { pipeNameFor, pipeRequest, startPipeServer } from './pipe-channel.ts';
|
|
30
27
|
|
|
31
|
-
/**
|
|
28
|
+
/** Tier 2: record the synthesized manifest that governed a worker session (D38-compatible and branch-replayable). */
|
|
32
29
|
const ROLE_MANIFEST_CUSTOM_TYPE = 'pi-herdr.role-manifest';
|
|
33
30
|
|
|
34
|
-
import { createHerdrClient
|
|
35
|
-
// subagent
|
|
36
|
-
// session-tail
|
|
31
|
+
import { createHerdrClient } from './herdr-client.ts';
|
|
32
|
+
// The subagent family moved to core/subagent.ts (loader entry, D78/D81); its history-store,
|
|
33
|
+
// session-tail, and gc-core imports moved with it, leaving index.ts only the common readers.
|
|
37
34
|
import { lastAssistantText, readSessionFile } from './session-tail.ts';
|
|
38
35
|
import { fileURLToPath } from 'node:url';
|
|
39
36
|
import { TodosService } from './todos-service.ts';
|
|
@@ -44,32 +41,29 @@ import {
|
|
|
44
41
|
planToolBadge,
|
|
45
42
|
progressOf,
|
|
46
43
|
} from './progress-core.ts';
|
|
47
|
-
import {
|
|
48
|
-
WRITE_LOCK_ENV,
|
|
49
|
-
acquireTokensFor,
|
|
50
|
-
isLockTokenKey,
|
|
51
|
-
parseLockTokenValue,
|
|
52
|
-
planWriteGuard,
|
|
53
|
-
releaseTokensFor,
|
|
54
|
-
type LockAgentView,
|
|
55
|
-
} from './lock-core.ts';
|
|
44
|
+
import { WRITE_LOCK_ENV } from './lock-core.ts';
|
|
56
45
|
import { ABORT_STOP_REASON, planSettleWake } from './settle-wake-core.ts';
|
|
57
46
|
import { composeForRole } from './manifest-compose.ts';
|
|
58
47
|
import { parseRuntimeManifest, planActiveTools, planToolGate, type RuntimeRoleManifest } from './tool-gate.ts';
|
|
59
|
-
|
|
48
|
+
|
|
60
49
|
import { formatPaneTitle } from './pane-title.ts';
|
|
61
50
|
import { registerSlimFrame, updateSlimFrame } from './slim-frame.ts';
|
|
51
|
+
import { planIndexMode } from './index-runtime.ts';
|
|
52
|
+
import { emptySubagentPortBox } from './subagent-port.ts';
|
|
53
|
+
import { createNoticeBuffer } from './index-notices.ts';
|
|
54
|
+
import { handlePipeRequest } from './index-pipe.ts';
|
|
55
|
+
import { installWriteLocks } from './index-locks.ts';
|
|
62
56
|
|
|
63
57
|
/**
|
|
64
|
-
* WS-D7
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* master.json
|
|
58
|
+
* WS-D7: apply the master-pane manifest to itself through the same mandatory chain as subagents
|
|
59
|
+
* (gate, visible layer, badge, and session record); the baseline includes our core plus inspect/execute tools.
|
|
60
|
+
* Match only a herdr pane (HERDR_ENV=1) that is not a subagent and has no explicitly supplied manifest.
|
|
61
|
+
* Standalone pi sessions without herdr remain full-featured for backward compatibility.
|
|
62
|
+
* A malformed master.json fails open to no-role state, matching malformed env semantics.
|
|
69
63
|
*/
|
|
70
64
|
function composeMasterRuntime(): RuntimeRoleManifest | null {
|
|
71
65
|
try {
|
|
72
|
-
// v1.1
|
|
66
|
+
// v1.1: read the built-in name directly so a workspace master.json decoy cannot affect self-application; the reserved-name check remains for loud spawn errors.
|
|
73
67
|
const { role, manifest } = composeForRole('master', [], { loadRoleOpts: { builtinDirect: true } });
|
|
74
68
|
return {
|
|
75
69
|
role: role.role,
|
|
@@ -84,24 +78,24 @@ function composeMasterRuntime(): RuntimeRoleManifest | null {
|
|
|
84
78
|
return null;
|
|
85
79
|
}
|
|
86
80
|
}
|
|
87
|
-
// C3
|
|
88
|
-
//
|
|
89
|
-
//
|
|
81
|
+
// C3: cordis belongs only in the master process; subagent-scope includes @deepseek-ai/cordis.
|
|
82
|
+
// Dynamically import it on the master branch so worker processes never load the module.
|
|
83
|
+
// Subagent constants/helpers moved with the family to core/subagent.ts.
|
|
90
84
|
|
|
91
85
|
export default async function (pi: ExtensionAPI) {
|
|
92
|
-
|
|
93
|
-
|
|
86
|
+
const mode = planIndexMode();
|
|
87
|
+
const isSubagent = mode.isSubagent;
|
|
94
88
|
const runtimeManifest =
|
|
95
89
|
parseRuntimeManifest(process.env.PI_HERDR_ROLE_MANIFEST) ??
|
|
96
|
-
(
|
|
97
|
-
const todos = new TodosService(TodosService.configFromRuntime(runtimeManifest,
|
|
90
|
+
(mode.composeMaster ? composeMasterRuntime() : null);
|
|
91
|
+
const todos = new TodosService(TodosService.configFromRuntime(runtimeManifest, isSubagent));
|
|
98
92
|
const { client, env } = createHerdrClient();
|
|
99
93
|
|
|
100
94
|
let sessionId: string = process.env.PI_SESSION_FILE ?? process.env.PI_SESSION_ID ?? '';
|
|
101
|
-
/** M16
|
|
95
|
+
/** M16: completion timestamps used as rate-estimation input, appended with todo.completed events. */
|
|
102
96
|
const completedStamps: number[] = [];
|
|
103
97
|
|
|
104
|
-
/* ──
|
|
98
|
+
/* ── State reconstruction (branch correctness comes from the last todo_write snapshot on the branch) ── */
|
|
105
99
|
|
|
106
100
|
function rebuildFromBranch(ctx: unknown): void {
|
|
107
101
|
try {
|
|
@@ -109,13 +103,13 @@ export default async function (pi: ExtensionAPI) {
|
|
|
109
103
|
?.sessionManager?.getBranch?.() ?? [];
|
|
110
104
|
todos.rebuild(entries);
|
|
111
105
|
} catch {
|
|
112
|
-
//
|
|
106
|
+
// A reconstruction failure must not disrupt the main flow; the next todo_write re-anchors state.
|
|
113
107
|
}
|
|
114
108
|
}
|
|
115
109
|
|
|
116
|
-
/* ── herdr
|
|
110
|
+
/* ── herdr reporting (zero cost for Noop; silent failures never affect pi's main flow) ── */
|
|
117
111
|
|
|
118
|
-
/**
|
|
112
|
+
/** Tier 2: worker role badge parsed from the env manifest and kept visible while idle. */
|
|
119
113
|
let roleBadge: string | null = null;
|
|
120
114
|
|
|
121
115
|
function reportAgent(state: 'working' | 'idle' | 'blocked', activity: string | null): void {
|
|
@@ -129,7 +123,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
129
123
|
client.reportAgentSession(path).catch(() => {});
|
|
130
124
|
}
|
|
131
125
|
|
|
132
|
-
/**
|
|
126
|
+
/** Session identity: sessionManager is authoritative (env may be unset in print/RPC mode), with env as fallback. */
|
|
133
127
|
function resolveSessionId(ctx: unknown): string {
|
|
134
128
|
try {
|
|
135
129
|
const sm = (ctx as {
|
|
@@ -153,16 +147,16 @@ export default async function (pi: ExtensionAPI) {
|
|
|
153
147
|
function mirrorTodos(): void {
|
|
154
148
|
if (!client.available) return;
|
|
155
149
|
const label = sessionId || (env ? `pane:${env.paneId}` : '');
|
|
156
|
-
// M16
|
|
150
|
+
// M16: progress badge (conservative N/M plus confidence ETA; fall back to a plain count when estimates are unreliable).
|
|
157
151
|
const p = progressOf(todos.items);
|
|
158
152
|
const eta = estimateEta({ completedAt: completedStamps, total: p.total, now: Date.now() });
|
|
159
153
|
const suffix = formatProgressSuffix({ completed: p.completed, total: p.total, eta });
|
|
160
|
-
//
|
|
154
|
+
// Unfreeze by carrying lastWriteAt into the title-side archived check (pane-title).
|
|
161
155
|
const title = formatPaneTitle(todos.items, null, {
|
|
162
156
|
progressSuffix: suffix,
|
|
163
157
|
lastWriteAt: todos.lastWriteAt,
|
|
164
158
|
});
|
|
165
|
-
// D97
|
|
159
|
+
// D97: narrow-frame static state shares inputs with the pane title; thinking tokens cannot reach it, so a static frame would freeze.
|
|
166
160
|
updateSlimFrame(title);
|
|
167
161
|
client.reportMetadata({
|
|
168
162
|
session: label,
|
|
@@ -172,7 +166,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
172
166
|
}).catch(() => {});
|
|
173
167
|
}
|
|
174
168
|
|
|
175
|
-
/* ── M17
|
|
169
|
+
/* ── M17: automatic settlement reconciliation (pure planner + D38 authority; idempotent and safe to run through both paths) ── */
|
|
176
170
|
|
|
177
171
|
function reconcileOnSettlement(description: string, outcome: 'settled' | 'failed'): string[] {
|
|
178
172
|
if (!description) return [];
|
|
@@ -185,7 +179,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
185
179
|
{ version: 1, edits: plan.edits, ts: Date.now() },
|
|
186
180
|
);
|
|
187
181
|
} catch {
|
|
188
|
-
/*
|
|
182
|
+
/* Best effort persistence; in-memory state still advances. */
|
|
189
183
|
}
|
|
190
184
|
todos.applyEdits(plan.edits);
|
|
191
185
|
mirrorTodos();
|
|
@@ -196,24 +190,24 @@ export default async function (pi: ExtensionAPI) {
|
|
|
196
190
|
}
|
|
197
191
|
}
|
|
198
192
|
|
|
199
|
-
/** M17
|
|
193
|
+
/** M17: append reconciliation note lines to settlement notices; an empty list returns the original text. */
|
|
200
194
|
function withReconcileNotes(base: string, notes: readonly string[]): string {
|
|
201
195
|
return notes.length ? `${base}\n${notes.join('\n')}` : base;
|
|
202
196
|
}
|
|
203
197
|
|
|
204
|
-
/* ── M16
|
|
198
|
+
/* ── M16: progress and tool badges (title suffix + report_agent.message; no new protocol) ── */
|
|
205
199
|
|
|
206
200
|
todos.on('todo.completed', (e: { count: number; at: number }) => {
|
|
207
201
|
for (let i = 0; i < e.count; i++) completedStamps.push(e.at);
|
|
208
202
|
mirrorTodos();
|
|
209
203
|
});
|
|
210
204
|
|
|
211
|
-
//
|
|
205
|
+
// Tool badge: parallel mode starts in source order and ends in completion order (pi docs contract); track in-flight calls in a Map.
|
|
212
206
|
const runningTools = new Map<string, string>(); // toolCallId → toolName
|
|
213
207
|
let lastToolBadge: string | null = null;
|
|
214
208
|
function reportToolBadge(): void {
|
|
215
209
|
const badge = planToolBadge([...new Set(runningTools.values())]);
|
|
216
|
-
if (badge === lastToolBadge) return; //
|
|
210
|
+
if (badge === lastToolBadge) return; // Idempotent: report only changes (M13 rendering discipline).
|
|
217
211
|
lastToolBadge = badge;
|
|
218
212
|
if (badge) reportAgent('working', badge);
|
|
219
213
|
else reportAgent(agentActive ? 'working' : 'idle', agentActive ? currentActivity(todos.items) : null);
|
|
@@ -232,125 +226,40 @@ export default async function (pi: ExtensionAPI) {
|
|
|
232
226
|
}
|
|
233
227
|
});
|
|
234
228
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
* v1 范围:write/edit(bash 文件目标提取不可靠,声明为 seam)。 */
|
|
240
|
-
|
|
241
|
-
const writeLockHard = process.env[WRITE_LOCK_ENV] === '1';
|
|
242
|
-
/** 本 pane 当前持有的锁(归一路径;settled 时统一释放)。 */
|
|
243
|
-
const heldLocks = new Set<string>();
|
|
244
|
-
/** 软 veto 待附警告的 toolCall → 警告文本。 */
|
|
245
|
-
const lockWarnByToolCall = new Map<string, string>();
|
|
246
|
-
|
|
247
|
-
async function acquireLocks(paths: readonly string[]): Promise<void> {
|
|
248
|
-
for (const p of paths) heldLocks.add(p);
|
|
249
|
-
await client.reportLockTokens(acquireTokensFor(paths, env?.paneId ?? ''));
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
pi.on('tool_call', async (event: { toolName?: string; toolCallId?: string; input?: unknown }, ctx: { cwd?: string }) => {
|
|
253
|
-
if (!client.available || !env) return;
|
|
254
|
-
const cwd = (ctx as { cwd?: string }).cwd ?? process.cwd();
|
|
255
|
-
let agents: LockAgentView[];
|
|
256
|
-
try {
|
|
257
|
-
agents = (await client.listAgents()).map((a) => ({ paneId: a.paneId, tokens: a.tokens }));
|
|
258
|
-
} catch {
|
|
259
|
-
return; // 查询失败放行(fail-open:锁是协作性保护,不作单点故障)
|
|
260
|
-
}
|
|
261
|
-
const plan = planWriteGuard({
|
|
262
|
-
toolName: event.toolName ?? '',
|
|
263
|
-
input: event.input,
|
|
264
|
-
agents,
|
|
265
|
-
ownPaneId: env.paneId,
|
|
266
|
-
cwd,
|
|
267
|
-
hard: writeLockHard,
|
|
268
|
-
});
|
|
269
|
-
if (plan.kind === 'skip') return;
|
|
270
|
-
if (plan.kind === 'block') {
|
|
271
|
-
return { block: true, reason: plan.reason };
|
|
272
|
-
}
|
|
273
|
-
if (plan.kind === 'warn' && typeof event.toolCallId === 'string') {
|
|
274
|
-
lockWarnByToolCall.set(event.toolCallId, plan.warning);
|
|
275
|
-
// 软模式:写入照常进行,但本 pane 也成为编辑者 → 接管锁(对称可见)
|
|
276
|
-
}
|
|
277
|
-
await acquireLocks(plan.paths);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
pi.on('tool_result', async (event: { toolCallId?: string; content?: Array<{ type: string; text: string }> }) => {
|
|
281
|
-
if (typeof event?.toolCallId !== 'string') return;
|
|
282
|
-
const warning = lockWarnByToolCall.get(event.toolCallId);
|
|
283
|
-
if (!warning) return;
|
|
284
|
-
lockWarnByToolCall.delete(event.toolCallId);
|
|
285
|
-
const content = Array.isArray(event.content) ? event.content : [];
|
|
286
|
-
return { content: [...content, { type: 'text', text: warning }] };
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
// 锁释放:settled(一轮结束)置 null;pane 关闭由 agent 条目消失兜底
|
|
290
|
-
pi.on('agent_settled', async () => {
|
|
291
|
-
if (heldLocks.size === 0) return;
|
|
292
|
-
const paths = [...heldLocks];
|
|
293
|
-
heldLocks.clear();
|
|
294
|
-
await client.reportLockTokens(releaseTokensFor(paths));
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
// 人类可读:/locks 查看本 pane 持有 + 全局锁视图
|
|
298
|
-
pi.registerCommand('locks', {
|
|
299
|
-
description: 'Show write locks held by this pane and all live panes (M18)',
|
|
300
|
-
handler: async (_args, ctx) => {
|
|
301
|
-
const ui = (ctx as { ui?: { notify?: (text: string, level?: string) => void } }).ui;
|
|
302
|
-
const mine = [...heldLocks];
|
|
303
|
-
const lines = [`held by this pane (${mine.length}):`];
|
|
304
|
-
lines.push(...(mine.length ? mine.map((p) => ` ${p}`) : [' (none)']));
|
|
305
|
-
lines.push('all live locks:');
|
|
306
|
-
let any = false;
|
|
307
|
-
try {
|
|
308
|
-
for (const a of await client.listAgents()) {
|
|
309
|
-
for (const [k, v] of Object.entries(a.tokens)) {
|
|
310
|
-
if (!isLockTokenKey(k) || typeof v !== 'string' || !v) continue;
|
|
311
|
-
const parsed = parseLockTokenValue(v);
|
|
312
|
-
if (!parsed) continue;
|
|
313
|
-
any = true;
|
|
314
|
-
lines.push(` ${parsed.path} → pane ${parsed.holderPaneId}`);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
} catch {
|
|
318
|
-
lines.push(' (agent.list failed)');
|
|
319
|
-
}
|
|
320
|
-
if (!any && lines[lines.length - 1] !== ' (agent.list failed)') lines.push(' (none)');
|
|
321
|
-
ui?.notify?.(lines.join('\n'), 'info');
|
|
322
|
-
},
|
|
229
|
+
installWriteLocks(pi, {
|
|
230
|
+
client,
|
|
231
|
+
env,
|
|
232
|
+
hard: process.env[WRITE_LOCK_ENV] === '1',
|
|
323
233
|
});
|
|
324
234
|
|
|
325
|
-
/* ── todo
|
|
235
|
+
/* ── todo family slot (core/todo.ts fills the plugin hook; widget rendering moved with the family) ── */
|
|
326
236
|
const todoUi: { renderWidget: (ctx: unknown) => void } = {
|
|
327
|
-
renderWidget: () => { /*
|
|
237
|
+
renderWidget: () => { /* No-op before plugin mounting; filled after mounting. */ },
|
|
328
238
|
};
|
|
329
239
|
|
|
330
|
-
/* ──
|
|
240
|
+
/* ── Lifecycle ────────────────────────────────────────────────────── */
|
|
331
241
|
|
|
332
242
|
pi.on('session_start', async (event, ctx) => {
|
|
333
243
|
sessionId = resolveSessionId(ctx);
|
|
334
244
|
rebuildFromBranch(ctx);
|
|
335
|
-
// v1.3 M9
|
|
336
|
-
//
|
|
245
|
+
// v1.3 M9 fix (observed): on resume pi restores widget state from the session;
|
|
246
|
+
// calling setWidget again breaks the TUI '/' command-panel route ('/' would be sent to the model as message text).
|
|
337
247
|
const reason = (event as { reason?: string } | undefined)?.reason;
|
|
338
248
|
if (reason !== 'resume') todoUi.renderWidget(ctx);
|
|
339
|
-
// D97
|
|
340
|
-
//
|
|
341
|
-
// 切换会 resetExtensionUI 拆掉 overlay)。
|
|
249
|
+
// D97: narrow-frame overlay is meaningful only inside herdr; covering an interactive narrow terminal is unsafe,
|
|
250
|
+
// and the heatmap amplification provides the exit path. Re-register on resume because session switching resets the overlay.
|
|
342
251
|
if (env) registerSlimFrame(ctx);
|
|
343
252
|
mirrorTodos();
|
|
344
253
|
reportSession(sessionId);
|
|
345
|
-
// D93
|
|
346
|
-
// master → 'master'
|
|
347
|
-
//
|
|
254
|
+
// D93: sidebar identity is the role name (display_agent takes precedence over detected agent, actions.rs:563 in 0.8.2).
|
|
255
|
+
// master → 'master'; worker → manifest.role (prettify worker-default as worker).
|
|
256
|
+
// A bare pi without a manifest does not report, so ordinary pi sessions remain undisturbed.
|
|
348
257
|
if (runtimeManifest) {
|
|
349
258
|
const roleDisplay = runtimeManifest.role === 'worker-default' ? 'worker' : runtimeManifest.role;
|
|
350
259
|
void client.reportDisplayAgent(roleDisplay);
|
|
351
260
|
}
|
|
352
|
-
//
|
|
353
|
-
//
|
|
261
|
+
// Tier 2: worker role manifest parsed at process start supplies the badge and authoritative session record.
|
|
262
|
+
// The custom entry mirrors D38 todo-edit and anchors execution-time replay.
|
|
354
263
|
if (runtimeManifest) {
|
|
355
264
|
roleBadge = `role ${runtimeManifest.role} v${runtimeManifest.version ?? '?'} (${runtimeManifest.tools.length} tools)`;
|
|
356
265
|
try {
|
|
@@ -359,10 +268,10 @@ export default async function (pi: ExtensionAPI) {
|
|
|
359
268
|
{ version: 1, role: runtimeManifest.role, manifestVersion: runtimeManifest.version, tools: runtimeManifest.tools, permissions: runtimeManifest.permissions, unknownTools: runtimeManifest.unknownTools ?? 'deny', ts: Date.now() },
|
|
360
269
|
);
|
|
361
270
|
} catch {
|
|
362
|
-
/*
|
|
271
|
+
/* Best effort recording. */
|
|
363
272
|
}
|
|
364
|
-
// D77
|
|
365
|
-
//
|
|
273
|
+
// D77 visible layer: remove tools outside the manifest from the model's view after all plugins load at session_start.
|
|
274
|
+
// Intersection semantics prevent clearing everything; master without a manifest stays full. Missing APIs are skipped for old pi compatibility.
|
|
366
275
|
const piTools = pi as { getActiveTools?: () => string[]; setActiveTools?: (names: string[]) => void };
|
|
367
276
|
if (typeof piTools.getActiveTools === 'function' && typeof piTools.setActiveTools === 'function') {
|
|
368
277
|
try {
|
|
@@ -376,17 +285,17 @@ export default async function (pi: ExtensionAPI) {
|
|
|
376
285
|
console.error(`[pi-herdr] D77 visible-layer: role ${runtimeManifest.role} tools ${active.length} → ${vis.next.length}`);
|
|
377
286
|
}
|
|
378
287
|
} catch {
|
|
379
|
-
/*
|
|
288
|
+
/* Best effort visible layer; the mandatory layer remains active. */
|
|
380
289
|
}
|
|
381
290
|
}
|
|
382
291
|
}
|
|
383
292
|
reportAgent('idle', null);
|
|
384
293
|
});
|
|
385
294
|
|
|
386
|
-
/* ──
|
|
387
|
-
* deny /
|
|
388
|
-
*
|
|
389
|
-
*
|
|
295
|
+
/* ── Tier 2 Weeks 4–5: worker execution enforcement (the manifest's final gate) ──
|
|
296
|
+
* deny / outside manifest → block (reason is given to the model); ask → v1 allows with stderr logging
|
|
297
|
+
* (V56 acceptance anchor). Master and label-only workers without a manifest remain open.
|
|
298
|
+
* Rate limiting was removed (WS-D6): we own the permission boundary; plugin integrators own resource quotas. */
|
|
390
299
|
pi.on('tool_call', async (event: { toolName?: string }) => {
|
|
391
300
|
if (!runtimeManifest) return;
|
|
392
301
|
const tool = typeof event?.toolName === 'string' ? event.toolName : '';
|
|
@@ -396,20 +305,20 @@ export default async function (pi: ExtensionAPI) {
|
|
|
396
305
|
}
|
|
397
306
|
if (gate.kind === 'ask') {
|
|
398
307
|
console.error(`${gate.notice} (v1 soft-approval: allowed, hard gate lands in v2)`);
|
|
399
|
-
//
|
|
308
|
+
// Durable trace (V56 anchor): TUI redraw erases stderr, so the session custom entry is authoritative.
|
|
400
309
|
try {
|
|
401
310
|
(pi as { appendEntry?: (customType: string, data: unknown) => void }).appendEntry?.(
|
|
402
311
|
'pi-herdr.approval-needed',
|
|
403
312
|
{ role: runtimeManifest.role, tool, ts: Date.now() },
|
|
404
313
|
);
|
|
405
314
|
} catch {
|
|
406
|
-
/*
|
|
315
|
+
/* Best effort. */
|
|
407
316
|
}
|
|
408
317
|
}
|
|
409
318
|
});
|
|
410
319
|
|
|
411
320
|
|
|
412
|
-
//
|
|
321
|
+
// Session-tree navigation (/tree, /fork): reconstruction preserves branch correctness.
|
|
413
322
|
pi.on('session_tree', async (_event, ctx) => {
|
|
414
323
|
sessionId = resolveSessionId(ctx);
|
|
415
324
|
rebuildFromBranch(ctx);
|
|
@@ -418,11 +327,12 @@ export default async function (pi: ExtensionAPI) {
|
|
|
418
327
|
});
|
|
419
328
|
|
|
420
329
|
let agentActive = false;
|
|
421
|
-
|
|
330
|
+
const subagentPort = emptySubagentPortBox();
|
|
331
|
+
// D96 state, anchoring settle-wake-core deduplication and cooldown.
|
|
422
332
|
let d96NoticeKey: string | null = null;
|
|
423
333
|
let d96NoticeAt = 0;
|
|
424
|
-
//
|
|
425
|
-
// 'aborted'
|
|
334
|
+
// Prevent wake-up storms by tracking the last assistant turn's stopReason;
|
|
335
|
+
// 'aborted' means the user explicitly pressed ESC, so settlement must inject no wake-up message.
|
|
426
336
|
let lastStopReason: string | null = null;
|
|
427
337
|
pi.on('turn_start', async () => {
|
|
428
338
|
agentActive = true;
|
|
@@ -430,7 +340,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
430
340
|
});
|
|
431
341
|
pi.on('turn_end', async (event: unknown) => {
|
|
432
342
|
if (event === null || typeof event !== 'object' || !('message' in event)) return;
|
|
433
|
-
const msg = (event as { message: unknown }).message; // 'message' in
|
|
343
|
+
const msg = (event as { message: unknown }).message; // 'message' in has been guarded.
|
|
434
344
|
if (msg === null || typeof msg !== 'object') return;
|
|
435
345
|
const { role, stopReason } = msg as { role?: unknown; stopReason?: unknown };
|
|
436
346
|
if (role === 'assistant' && typeof stopReason === 'string') {
|
|
@@ -443,7 +353,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
443
353
|
reportAgent('idle', null);
|
|
444
354
|
const plan = planSettleWake({
|
|
445
355
|
lastStopReason,
|
|
446
|
-
running:
|
|
356
|
+
running: subagentPort.current?.listRunningSubs() ?? [],
|
|
447
357
|
lastNoticeKey: d96NoticeKey,
|
|
448
358
|
lastNoticeAt: d96NoticeAt,
|
|
449
359
|
now: Date.now(),
|
|
@@ -451,35 +361,34 @@ export default async function (pi: ExtensionAPI) {
|
|
|
451
361
|
d96NoticeKey = plan.noticeKey;
|
|
452
362
|
d96NoticeAt = plan.noticeAt;
|
|
453
363
|
if (!plan.wake) {
|
|
454
|
-
//
|
|
455
|
-
//
|
|
364
|
+
// Stay silent after a user abort while retaining the settlement buffer (do not clear pendingSettleNotices);
|
|
365
|
+
// deliver it on the next natural run's turn_end steer or natural settlement.
|
|
456
366
|
return;
|
|
457
367
|
}
|
|
458
|
-
// D96
|
|
459
|
-
// worker 侧 subagentSlots.listRunningSubs 为 null(不注入)。
|
|
368
|
+
// D96: master settled while background subagents still run → remind (worker port is unbound).
|
|
460
369
|
if (plan.notice && !isSubagent) {
|
|
461
|
-
const running =
|
|
370
|
+
const running = subagentPort.current?.listRunningSubs() ?? [];
|
|
462
371
|
const brief = running.map((s) => `${s.paneId} (${s.description})`).join('、');
|
|
463
372
|
const notice = `注意:仍有 ${running.length} 个后台 subagent 在运行:${brief}。若你的任务依赖它们,请等待其结算(list_agents 查看状态);若不等待,请说明放弃原因。`;
|
|
464
373
|
void sendUserMessageIn(notice);
|
|
465
374
|
}
|
|
466
375
|
});
|
|
467
376
|
|
|
468
|
-
// v1.3 M8
|
|
469
|
-
//
|
|
470
|
-
//
|
|
377
|
+
// v1.3 M8: report blocked state ourselves (D29's exemption relies on blocked visibility).
|
|
378
|
+
// Observed: pi 0.84.2 core emits no "herdr:blocked" (the official integration listener is dead code),
|
|
379
|
+
// so this extension manages enterBlocked/exitBlocked while ask_user_question waits for human input.
|
|
471
380
|
let blockedDepth = 0;
|
|
472
381
|
function enterBlocked(label: string | null): void {
|
|
473
382
|
blockedDepth += 1;
|
|
474
383
|
reportAgent('blocked', label);
|
|
475
|
-
// D95
|
|
384
|
+
// D95: human-gate marker lets the workbench heatmap distinguish ask from block.
|
|
476
385
|
if (label) void client.reportAskFlag(label).catch(() => {});
|
|
477
386
|
}
|
|
478
387
|
function exitBlocked(): void {
|
|
479
388
|
blockedDepth = Math.max(0, blockedDepth - 1);
|
|
480
389
|
if (blockedDepth === 0) {
|
|
481
390
|
reportAgent(agentActive ? 'working' : 'idle', agentActive ? currentActivity(todos.items) : null);
|
|
482
|
-
// D95
|
|
391
|
+
// D95: gate released → clear marker.
|
|
483
392
|
void client.reportAskFlag(null).catch(() => {});
|
|
484
393
|
}
|
|
485
394
|
}
|
|
@@ -492,7 +401,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
492
401
|
},
|
|
493
402
|
);
|
|
494
403
|
|
|
495
|
-
/* ──
|
|
404
|
+
/* ── Tool: ask_user_question (v1.3 M8 human gate, available to master and subagents) ── */
|
|
496
405
|
|
|
497
406
|
pi.registerTool({
|
|
498
407
|
name: 'ask_user_question',
|
|
@@ -515,8 +424,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
515
424
|
ui?: { input?: (title: string, placeholder?: string) => Promise<string | undefined> };
|
|
516
425
|
}).ui;
|
|
517
426
|
enterBlocked(question);
|
|
518
|
-
// v1.3 M8
|
|
519
|
-
//
|
|
427
|
+
// v1.3 M8 observed: herdr keeps detecting working while waiting for a human, overriding a one-shot blocked report;
|
|
428
|
+
// repeat it every 5s so the pane stays blocked and remains visible/GC-exempt.
|
|
520
429
|
const hb = setInterval(() => { reportAgent('blocked', question); }, 5000);
|
|
521
430
|
try {
|
|
522
431
|
const answer = await ui?.input?.(question, 'your answer');
|
|
@@ -535,31 +444,20 @@ export default async function (pi: ExtensionAPI) {
|
|
|
535
444
|
client.close();
|
|
536
445
|
});
|
|
537
446
|
|
|
538
|
-
/* ── M12
|
|
447
|
+
/* ── M12: bidirectional message channel (D49/D50/D48; every pane listens on its own name, with no controller/child assumption) ── */
|
|
539
448
|
|
|
540
449
|
const settleNoticeLatch = new Set<string>();
|
|
541
|
-
/**
|
|
450
|
+
/** Deduplicate settlement notices: choose push fast path or pollLoop fallback; one notice per paneId+request ID per round. */
|
|
542
451
|
function claimSettleNotice(key: string): boolean {
|
|
543
452
|
if (settleNoticeLatch.has(key)) return false;
|
|
544
453
|
settleNoticeLatch.add(key);
|
|
545
454
|
return true;
|
|
546
455
|
}
|
|
547
|
-
/** O6:master 收到 worker reply 时回写 sessionFile(core/subagent 插件挂载时回填槽)。 */
|
|
548
|
-
const subagentSlots: {
|
|
549
|
-
applyReplySession: ((paneId: string, sessionFile: string | null) => void) | null;
|
|
550
|
-
reconcileOnReply: ((paneId: string) => string[]) | null;
|
|
551
|
-
/** D96:处于 running 状态的后台 subagent 描述列表(master settled 时检查用)。 */
|
|
552
|
-
listRunningSubs: (() => Array<{ paneId: string; description: string }>) | null;
|
|
553
|
-
} = { applyReplySession: null, reconcileOnReply: null, listRunningSubs: null };
|
|
554
|
-
/** pipe server 盒(common 持有创建/关闭;core/subagent 的 root effect 也经盒关)。 */
|
|
555
456
|
const pipeServerBox: { current: ReturnType<typeof startPipeServer> | null } = { current: null };
|
|
556
|
-
/** 最近一次机器请求(结算时按它决定是否 push、push 给谁)。 */
|
|
557
457
|
let pendingMachineRequest: { id: string; from: string | null; push: boolean; sinceTs: number } | null = null;
|
|
558
|
-
/** 缓存的扩展上下文(interrupt 用 ctx.abort(),D48)。 */
|
|
559
458
|
let latestCtx: { abort?: () => void } | null = null;
|
|
560
459
|
|
|
561
|
-
//
|
|
562
|
-
// 用户实机:subagent 结算后 idle master 不醒来处理,任务丢失;extensions.md:1410)。
|
|
460
|
+
// triggerTurn:true — idle followUp must start a new turn or settlement is lost.
|
|
563
461
|
const sendUserMessageIn = (content: string): Promise<void> =>
|
|
564
462
|
(pi as { sendUserMessage?: (content: string, opts?: { deliverAs?: string; triggerTurn?: boolean }) => Promise<void> })
|
|
565
463
|
.sendUserMessage?.(content, { deliverAs: 'followUp', triggerTurn: true }) ?? Promise.resolve();
|
|
@@ -567,43 +465,17 @@ export default async function (pi: ExtensionAPI) {
|
|
|
567
465
|
(pi as { sendUserMessage?: (content: string, opts?: { deliverAs?: string; triggerTurn?: boolean }) => Promise<void> })
|
|
568
466
|
.sendUserMessage?.(content, { deliverAs: mode, triggerTurn: true }) ?? Promise.resolve();
|
|
569
467
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
* - turn_end(LLM 迭代边界)折叠成一条以 steer 注入——pi 文档:steer =
|
|
576
|
-
* 「当前 assistant 的 tool call 执行完、下一次 LLM 调用前投递」,即 turn 间隙;
|
|
577
|
-
* - agent_settled 兜底:run 真正结束后残余缓冲直投(触发新 run)。
|
|
578
|
-
* 折叠规则(notice-buffer.ts):≤3 条原文逐条,>3 条前 3 + 尾行计数/全量指引。 */
|
|
579
|
-
const pendingSettleNotices: string[] = [];
|
|
580
|
-
/** B2:缓冲中尚未送达的结算通知所属 pane(GC 豁免判据——01a03c0d 实证:
|
|
581
|
-
* pD 结算 3s 后 pane 被 GC 关掉,通知 100s 后才经 turn 间隙送达,master 只好 revive)。 */
|
|
582
|
-
const pendingNoticePaneIds = new Set<string>();
|
|
583
|
-
const deliverNotice = async (content: string, paneId?: string): Promise<void> => {
|
|
584
|
-
// 反唤醒风暴:abort 后不直投(会触发新 run)——入缓冲,待下次自然 turn 投递。
|
|
585
|
-
// 结算内容本身持久在台账(list_agents / resume_subagent 可查),推送让位于用户意图。
|
|
586
|
-
if (content !== '' && paneId !== undefined) pendingNoticePaneIds.add(paneId);
|
|
587
|
-
if (agentActive || lastStopReason === ABORT_STOP_REASON) {
|
|
588
|
-
pendingSettleNotices.push(content);
|
|
589
|
-
return;
|
|
590
|
-
}
|
|
591
|
-
await sendUserMessageIn(content);
|
|
592
|
-
if (content !== '' && paneId !== undefined) pendingNoticePaneIds.delete(paneId);
|
|
593
|
-
};
|
|
594
|
-
const flushSettleNotices = async (mode: 'steer' | 'followUp'): Promise<void> => {
|
|
595
|
-
if (pendingSettleNotices.length === 0) return;
|
|
596
|
-
const collapsed = collapseNotices(pendingSettleNotices.splice(0));
|
|
597
|
-
pendingNoticePaneIds.clear(); // B2:折叠批次视为已送达(GC 恢复正常回收)
|
|
598
|
-
if (collapsed) await sendUserMessageAs(collapsed, mode);
|
|
599
|
-
};
|
|
468
|
+
const notices = createNoticeBuffer({
|
|
469
|
+
isBusy: () => agentActive || lastStopReason === ABORT_STOP_REASON,
|
|
470
|
+
send: sendUserMessageAs,
|
|
471
|
+
});
|
|
472
|
+
const deliverNotice = notices.deliverNotice;
|
|
600
473
|
pi.on('turn_end', () => {
|
|
601
|
-
void
|
|
474
|
+
void notices.flush('steer');
|
|
602
475
|
});
|
|
603
476
|
pi.on('agent_settled', () => {
|
|
604
|
-
// abort 后不投缓冲(会触发新 run;缓冲保留待下次自然 turn)——见 settle-wake-core。
|
|
605
477
|
if (lastStopReason === ABORT_STOP_REASON) return;
|
|
606
|
-
void
|
|
478
|
+
void notices.flush('followUp');
|
|
607
479
|
});
|
|
608
480
|
|
|
609
481
|
pi.on('session_start', async (_event, ctx) => {
|
|
@@ -613,61 +485,32 @@ export default async function (pi: ExtensionAPI) {
|
|
|
613
485
|
if (!paneId) return;
|
|
614
486
|
const name = pipeNameFor(cwd, paneId);
|
|
615
487
|
if (pipeServerBox.current) {
|
|
616
|
-
try { pipeServerBox.current.close(); } catch { /*
|
|
488
|
+
try { pipeServerBox.current.close(); } catch { /* Previous instance. */ }
|
|
617
489
|
pipeServerBox.current = null;
|
|
618
490
|
}
|
|
619
491
|
try {
|
|
620
|
-
pipeServerBox.current = startPipeServer(name, async (req) => {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
// 下次 LLM 调用前到达)——长 run 中途的补充契约秒级生效,不再排队整个 run
|
|
631
|
-
// (01a03c0d:契约 20min 后才到,worker 按旧契约实现被迫返工)。
|
|
632
|
-
// 初始 prompt 不 steer:空闲 worker 上的 followUp 才触发新 run(D96 语义)。
|
|
633
|
-
if (req.steer === true) await sendUserMessageAs(req.text, 'steer');
|
|
634
|
-
else await sendUserMessageIn(req.text);
|
|
635
|
-
return { type: 'ok', id: req.id };
|
|
636
|
-
}
|
|
637
|
-
if (req.type === 'interrupt') {
|
|
638
|
-
// D48:进程内中止(ctx.abort(),pi 0.84.2 实测存在);排队机器消息保持排队
|
|
639
|
-
latestCtx?.abort?.();
|
|
640
|
-
pendingMachineRequest = null; // 被中断的轮次不 push(请求方已知情)
|
|
641
|
-
return { type: 'ok', id: req.id };
|
|
642
|
-
}
|
|
643
|
-
if (req.type === 'reply') {
|
|
644
|
-
// D50 / O6:回写 sessionFile 与通知去重解耦——账本必须先纠正
|
|
645
|
-
subagentSlots.applyReplySession?.(req.paneId, req.sessionFile);
|
|
646
|
-
if (claimSettleNotice(`${req.paneId}:${req.id}`)) {
|
|
647
|
-
// M17:快路径结算 → 自动对账(幂等;与 pollLoop 兜底双跑无害)
|
|
648
|
-
const notes = subagentSlots.reconcileOnReply?.(req.paneId) ?? [];
|
|
649
|
-
const head = formatSettlementNotice(`${req.paneId}`, req.text);
|
|
650
|
-
const body = (req.sessionFile ? `${head}\nSession: ${req.sessionFile}` : head)
|
|
651
|
-
+ (notes.length ? `\n${notes.join('\n')}` : '');
|
|
652
|
-
// D92:经缓冲注入(忙时 turn_end 折叠 steer,闲时直投)
|
|
653
|
-
await deliverNotice(body, req.paneId);
|
|
654
|
-
}
|
|
655
|
-
return { type: 'ok', id: req.id };
|
|
656
|
-
}
|
|
657
|
-
return { type: 'error', id: req.id, message: `unknown type ${req.type}` };
|
|
658
|
-
});
|
|
492
|
+
pipeServerBox.current = startPipeServer(name, async (req) => handlePipeRequest(req, {
|
|
493
|
+
paneId,
|
|
494
|
+
port: subagentPort,
|
|
495
|
+
claimSettleNotice,
|
|
496
|
+
deliverNotice,
|
|
497
|
+
sendUserMessageIn,
|
|
498
|
+
sendUserMessageAs,
|
|
499
|
+
abort: () => { latestCtx?.abort?.(); },
|
|
500
|
+
setPendingMachineRequest: (next) => { pendingMachineRequest = next; },
|
|
501
|
+
}));
|
|
659
502
|
} catch {
|
|
660
|
-
/*
|
|
503
|
+
/* Pipe name collision (rare): this session has no channel; callers report an error after ping times out. */
|
|
661
504
|
}
|
|
662
505
|
});
|
|
663
506
|
pi.on('session_shutdown', () => {
|
|
664
507
|
if (pipeServerBox.current) {
|
|
665
|
-
try { pipeServerBox.current.close(); } catch { /*
|
|
508
|
+
try { pipeServerBox.current.close(); } catch { /* Already closed. */ }
|
|
666
509
|
pipeServerBox.current = null;
|
|
667
510
|
}
|
|
668
511
|
});
|
|
669
512
|
|
|
670
|
-
// D50
|
|
513
|
+
// D50: when this pane settles, push a summary and session path to any machine request awaiting a reply.
|
|
671
514
|
pi.on('agent_settled', async () => {
|
|
672
515
|
const req = pendingMachineRequest;
|
|
673
516
|
if (!req || !req.push || !req.from) return;
|
|
@@ -686,128 +529,33 @@ export default async function (pi: ExtensionAPI) {
|
|
|
686
529
|
sessionFile: sessionId || null,
|
|
687
530
|
}, 5000);
|
|
688
531
|
} catch {
|
|
689
|
-
/*
|
|
532
|
+
/* Push failed silently; the requester's pollLoop is the fallback. */
|
|
690
533
|
}
|
|
691
534
|
});
|
|
692
535
|
|
|
693
|
-
/*
|
|
694
|
-
|
|
695
|
-
const isSubagent = process.env.PI_HERDR_SUBAGENT === '1';
|
|
536
|
+
/* subagent tools are master-only (depth 1). C3: worker never loads bootstrap. */
|
|
696
537
|
if (!isSubagent) {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
const { createCordisApp } = await import('./bootstrap.ts');
|
|
701
|
-
const { PiSurface } = await import('./pi-surface.ts');
|
|
702
|
-
const cordisApp = await createCordisApp();
|
|
703
|
-
const sessionRoot = cordisApp.root;
|
|
704
|
-
|
|
705
|
-
// pipe server 关闭挂在树根(common 段创建、树根 LIFO 拆除)——不进任何 core
|
|
706
|
-
// 插件:hmr 重载插件不能误杀 index 拥有的跨属资源(d87 修)。
|
|
707
|
-
sessionRoot.effect(() => () => {
|
|
708
|
-
if (pipeServerBox.current) {
|
|
709
|
-
try { pipeServerBox.current.close(); } catch { /* 已关 */ }
|
|
710
|
-
pipeServerBox.current = null;
|
|
711
|
-
}
|
|
712
|
-
}, 'pipe-server');
|
|
713
|
-
|
|
714
|
-
// D79 pi 注册面:core 模块经 surface 注册(tombstone + ledger 咬合);
|
|
715
|
-
// index 自身的注册仍走 pi 原面(逐模块迁移,本批 = terminal 族)。
|
|
716
|
-
const surface = new PiSurface(pi as unknown as object, cordisApp.ledger);
|
|
717
|
-
sessionRoot.provide('pi-herdr.surface', surface);
|
|
718
|
-
// terminal 依赖槽:client/env 直传 + state 槽回填(GC 豁免查询)
|
|
719
|
-
const terminalDeps = {
|
|
538
|
+
const { mountMasterPlugins } = await import('./index-master.ts');
|
|
539
|
+
await mountMasterPlugins({
|
|
540
|
+
pi,
|
|
720
541
|
client,
|
|
721
542
|
env,
|
|
722
|
-
state: { activePaneIds: (): Set<string> => new Set() },
|
|
723
|
-
};
|
|
724
|
-
sessionRoot.provide('pi-herdr.terminal-deps', terminalDeps);
|
|
725
|
-
|
|
726
|
-
// M14 terminal 族迁 loader entry(热换面 = core/terminal.ts;loader 降级 → 直接 root.plugin)
|
|
727
|
-
const terminalMod = await import('./core/terminal.ts');
|
|
728
|
-
if (cordisApp.loaderReady) {
|
|
729
|
-
const withLoader = sessionRoot as typeof sessionRoot & {
|
|
730
|
-
loader?: { create: (o: { name: string }) => Promise<unknown> };
|
|
731
|
-
};
|
|
732
|
-
await withLoader.loader?.create({ name: './core/terminal.ts' });
|
|
733
|
-
} else {
|
|
734
|
-
await sessionRoot.plugin(terminalMod.default);
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
// todo 族迁 loader entry(协调工具:master/worker 都挂;本分支 = master 走 loader)
|
|
738
|
-
sessionRoot.provide('pi-herdr.todo-deps', {
|
|
739
543
|
todos,
|
|
740
|
-
|
|
741
|
-
maxItems: 15,
|
|
544
|
+
todoUi,
|
|
742
545
|
mirrorTodos,
|
|
743
|
-
appendEntry: (customType: string, data: unknown) => {
|
|
744
|
-
(pi as { appendEntry?: (t: string, d: unknown) => void }).appendEntry?.(customType, data);
|
|
745
|
-
},
|
|
746
|
-
state: todoUi,
|
|
747
|
-
});
|
|
748
|
-
const todoMod = await import('./core/todo.ts');
|
|
749
|
-
if (cordisApp.loaderReady) {
|
|
750
|
-
const withLoader = sessionRoot as typeof sessionRoot & {
|
|
751
|
-
loader?: { create: (o: { name: string }) => Promise<unknown> };
|
|
752
|
-
};
|
|
753
|
-
await withLoader.loader?.create({ name: './core/todo.ts' });
|
|
754
|
-
} else {
|
|
755
|
-
await sessionRoot.plugin(todoMod.default);
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
// subagent 族迁 loader entry(master-only:五工具 + 注册表 + poller + 历史 + GC)
|
|
759
|
-
sessionRoot.provide('pi-herdr.subagent-deps', {
|
|
760
|
-
client,
|
|
761
|
-
env,
|
|
762
546
|
extPath: fileURLToPath(import.meta.url),
|
|
763
|
-
|
|
547
|
+
port: subagentPort,
|
|
548
|
+
pipeServerBox,
|
|
764
549
|
deliverNotice,
|
|
765
|
-
noticePending:
|
|
550
|
+
noticePending: notices.noticePending,
|
|
766
551
|
getSessionId: () => sessionId,
|
|
767
552
|
getBlockedDepth: () => blockedDepth,
|
|
768
553
|
reconcileOnSettlement,
|
|
769
554
|
withReconcileNotes,
|
|
770
555
|
claimSettleNotice,
|
|
771
|
-
deliverNotice,
|
|
772
|
-
terminalState: terminalDeps.state,
|
|
773
|
-
todos,
|
|
774
|
-
});
|
|
775
|
-
const subagentMod = await import('./core/subagent.ts');
|
|
776
|
-
if (cordisApp.loaderReady) {
|
|
777
|
-
const withLoader = sessionRoot as typeof sessionRoot & {
|
|
778
|
-
loader?: { create: (o: { name: string }) => Promise<unknown> };
|
|
779
|
-
};
|
|
780
|
-
await withLoader.loader?.create({ name: './core/subagent.ts' });
|
|
781
|
-
} else {
|
|
782
|
-
await sessionRoot.plugin(subagentMod.default);
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// 档0 起的拆除链:session_shutdown → dispose 整树(scope/effect LIFO)
|
|
786
|
-
pi.on('session_shutdown', () => {
|
|
787
|
-
void disposeSessionRoot(sessionRoot);
|
|
788
556
|
});
|
|
789
557
|
} else {
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
const { Context } = await import('@deepseek-ai/cordis');
|
|
793
|
-
const { PiSurface } = await import('./pi-surface.ts');
|
|
794
|
-
const workerRoot = new Context();
|
|
795
|
-
const workerSurface = new PiSurface(pi as unknown as object);
|
|
796
|
-
workerRoot.provide('pi-herdr.surface', workerSurface);
|
|
797
|
-
workerRoot.provide('pi-herdr.todo-deps', {
|
|
798
|
-
todos,
|
|
799
|
-
allowParallelInProgress: todos.config.allowParallelInProgress,
|
|
800
|
-
maxItems: 15,
|
|
801
|
-
mirrorTodos,
|
|
802
|
-
appendEntry: (customType: string, data: unknown) => {
|
|
803
|
-
(pi as { appendEntry?: (t: string, d: unknown) => void }).appendEntry?.(customType, data);
|
|
804
|
-
},
|
|
805
|
-
state: todoUi,
|
|
806
|
-
});
|
|
807
|
-
const todoMod = await import('./core/todo.ts');
|
|
808
|
-
await workerRoot.plugin(todoMod.default);
|
|
809
|
-
pi.on('session_shutdown', () => {
|
|
810
|
-
void workerRoot.fiber.dispose();
|
|
811
|
-
});
|
|
558
|
+
const { mountWorkerTodo } = await import('./index-worker.ts');
|
|
559
|
+
await mountWorkerTodo({ pi, todos, todoUi, mirrorTodos });
|
|
812
560
|
}
|
|
813
561
|
}
|