pi-pier 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -11
- package/package.json +2 -2
- package/schemas/role-manifest.schema.json +7 -1
- package/skills/pi-herdr-workflow/SKILL.md +4 -5
- package/src/ansi-text.ts +62 -11
- package/src/ask-dialog.ts +308 -0
- package/src/ask-user.ts +113 -112
- package/src/bootstrap.ts +8 -12
- package/src/compact-coordinator.ts +278 -261
- package/src/compact-economics-core.ts +77 -145
- package/src/config-catalog-core.ts +77 -197
- package/src/config-command.ts +271 -64
- package/src/dashboard-command.ts +38 -19
- package/src/efficiency-config-core.ts +287 -484
- package/src/efficiency-store.ts +16 -29
- package/src/focus-poller.ts +14 -35
- package/src/gc-core.ts +10 -36
- package/src/herdr-client.ts +82 -208
- package/src/history-store.ts +36 -23
- package/src/index-gates.ts +124 -0
- package/src/index-locks.ts +2 -7
- package/src/index-master.ts +24 -18
- package/src/index-notices.ts +28 -15
- package/src/index-pipe.ts +17 -12
- package/src/index-roles.ts +306 -0
- package/src/index-runtime.ts +2 -6
- package/src/index-worker.ts +4 -8
- package/src/index.ts +305 -564
- package/src/jev-client.ts +12 -17
- package/src/jev-core.ts +137 -87
- package/src/ledger.ts +13 -21
- package/src/lock-core.ts +28 -56
- package/src/manifest-compose.ts +35 -20
- package/src/observation-core.ts +52 -100
- package/src/pane-title.ts +19 -38
- package/src/pi-surface.ts +34 -22
- package/src/pier-options.ts +54 -50
- package/src/pipe-channel.ts +27 -59
- package/src/platform-paths.ts +19 -52
- package/src/{core → plugins}/grid-shape.ts +8 -10
- package/src/{core → plugins}/heat-plan.ts +20 -22
- package/src/plugins/observation.ts +475 -0
- package/src/plugins/subagent.ts +728 -0
- package/src/{core → plugins}/terminal.ts +167 -127
- package/src/{core → plugins}/todo.ts +61 -100
- package/src/progress-core.ts +16 -53
- package/src/reconcile-core.ts +24 -33
- package/src/reducer-core.ts +24 -60
- package/src/reducer-invoker.ts +266 -239
- package/src/renderers.ts +76 -87
- package/src/role-loader.ts +33 -28
- package/src/role-manifest.ts +23 -28
- package/src/role-state.ts +119 -0
- package/src/routing-telemetry.ts +136 -0
- package/src/runtime-policy.ts +25 -40
- package/src/session-tail.ts +65 -41
- package/src/settle-wake-core.ts +52 -21
- package/src/slim-frame.ts +25 -122
- package/src/stale-core.ts +17 -24
- package/src/storage-layout.ts +14 -43
- package/src/subagent-core.ts +253 -118
- package/src/subagent-gc.ts +109 -67
- package/src/subagent-poller.ts +367 -24
- package/src/subagent-session.ts +516 -0
- package/src/subagent-spawn.ts +530 -52
- package/src/swallow.ts +7 -17
- package/src/terminal-core.ts +125 -106
- package/src/todo-core.ts +57 -109
- package/src/todo-read-hook.ts +23 -39
- package/src/todo-reminder-core.ts +26 -51
- package/src/todo-window.ts +11 -12
- package/src/todos-service.ts +15 -35
- package/src/tool-error.ts +6 -13
- package/src/tool-gate.ts +38 -21
- package/src/vocab.ts +52 -25
- package/src/ask-multi.ts +0 -260
- package/src/config-guide.ts +0 -361
- package/src/core/observation.ts +0 -555
- package/src/core/subagent.ts +0 -633
- package/src/efficiency-config-io.ts +0 -53
- package/src/git-adapter.ts +0 -113
- package/src/notice-buffer.ts +0 -23
- package/src/subagent-execute.ts +0 -328
- package/src/subagent-git-io.ts +0 -80
- package/src/subagent-launch.ts +0 -114
- package/src/subagent-list-action.ts +0 -69
- package/src/subagent-output-action.ts +0 -104
- package/src/subagent-output-adapter.ts +0 -27
- package/src/subagent-output-core.ts +0 -263
- package/src/subagent-poll-loop.ts +0 -305
- package/src/subagent-port.ts +0 -27
- package/src/subagent-ready.ts +0 -89
- package/src/subagent-registry.ts +0 -70
- package/src/subagent-resolution.ts +0 -41
- package/src/subagent-scope.ts +0 -41
- package/src/subagent-session-io.ts +0 -230
package/src/index.ts
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pi-herdr extension entry point
|
|
3
|
-
* DESIGN.md §12 option C).
|
|
2
|
+
* pi-herdr extension entry point and composition root.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
4
|
+
* Layout: index-runtime.ts picks the process mode (worker / bare pi / herdr master);
|
|
5
|
+
* index-roles.ts the role runtime, index-gates.ts human-gate reporting, index-pipe.ts the
|
|
6
|
+
* pipe protocol handler, index-notices.ts the notice buffer, index-locks.ts write locks;
|
|
7
|
+
* index-master.ts / index-worker.ts are the dynamic master / todo-only mounts.
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* and branches automatically roll back in DSH-aligned semantics.
|
|
10
|
-
* - `subagent` / `terminal`: herdr-master only. Bare pi does not register them.
|
|
11
|
-
* - `/todos`, TUI widget, and herdr title projection with graceful degradation without herdr.
|
|
12
|
-
*
|
|
13
|
-
* pi 0.84.2 contract (validated):
|
|
14
|
-
* - onUpdate must have AgentToolResult shape; a string causes the TUI to crash and exit pi;
|
|
15
|
-
* - tool-result details persist in session JSONL, and getBranch() replay implements branch rollback.
|
|
9
|
+
* pi contract: onUpdate must have AgentToolResult shape (a string crashes the TUI); tool-result
|
|
10
|
+
* details persist in session JSONL and getBranch() replay implements branch rollback.
|
|
16
11
|
*/
|
|
17
12
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
18
13
|
import {
|
|
@@ -27,46 +22,36 @@ import {
|
|
|
27
22
|
runAsk,
|
|
28
23
|
} from './ask-user.ts';
|
|
29
24
|
import { pipeNameFor, pipeRequest, startPipeServer } from './pipe-channel.ts';
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
currentActivity,
|
|
33
|
-
} from './todo-core.ts';
|
|
34
|
-
|
|
35
|
-
import { APPROVAL_NEEDED_CUSTOM_TYPE, ROLE_MANIFEST_CUSTOM_TYPE, installRenderers } from './renderers.ts';
|
|
25
|
+
import { TODO_EDIT_CUSTOM_TYPE, currentActivity } from './todo-core.ts';
|
|
26
|
+
import { installRenderers } from './renderers.ts';
|
|
36
27
|
import { createHerdrClient } from './herdr-client.ts';
|
|
37
|
-
|
|
38
|
-
// session-tail, and gc-core imports moved with it, leaving index.ts only the common readers.
|
|
39
|
-
import { lastAssistantText, readSessionFile } from './session-tail.ts';
|
|
28
|
+
import { bareSessionId, lastAssistantText, readSessionFile, resolveSessionFileValue } from './session-tail.ts';
|
|
40
29
|
import { fileURLToPath } from 'node:url';
|
|
41
|
-
import {
|
|
30
|
+
import { join } from 'node:path';
|
|
31
|
+
import { platformPaths } from './platform-paths.ts';
|
|
32
|
+
import { TodosService, type TodoCompletionSource } from './todos-service.ts';
|
|
42
33
|
import { reconcileTodos } from './reconcile-core.ts';
|
|
43
|
-
import type { TodoUiSlot } from './
|
|
44
|
-
import {
|
|
45
|
-
estimateEta,
|
|
46
|
-
formatProgressSuffix,
|
|
47
|
-
planToolBadge,
|
|
48
|
-
progressOf,
|
|
49
|
-
} from './progress-core.ts';
|
|
34
|
+
import type { TodoUiSlot } from './plugins/todo.ts';
|
|
35
|
+
import { estimateEta, formatProgressSuffix, planToolBadge, progressOf } from './progress-core.ts';
|
|
50
36
|
import { WRITE_LOCK_ENV } from './lock-core.ts';
|
|
51
|
-
import { ABORT_STOP_REASON, planSettleWake } from './settle-wake-core.ts';
|
|
52
|
-
import {
|
|
53
|
-
import { parseRuntimeManifest, planActiveTools, planToolGate, type RuntimeRoleManifest } from './tool-gate.ts';
|
|
54
|
-
|
|
37
|
+
import { ABORT_STOP_REASON, planSettleWake, shouldPushSettleReply } from './settle-wake-core.ts';
|
|
38
|
+
import { parseRuntimeManifest } from './tool-gate.ts';
|
|
55
39
|
import { formatPaneTitle } from './pane-title.ts';
|
|
56
40
|
import { registerSlimFrame, updateSlimFrame } from './slim-frame.ts';
|
|
57
41
|
import { planIndexMode } from './index-runtime.ts';
|
|
58
|
-
import { emptySubagentPortBox } from './subagent-
|
|
42
|
+
import { emptySubagentPortBox } from './subagent-core.ts';
|
|
59
43
|
import { createNoticeBuffer } from './index-notices.ts';
|
|
60
|
-
import { handlePipeRequest } from './index-pipe.ts';
|
|
44
|
+
import { handlePipeRequest, type MachineRequest } from './index-pipe.ts';
|
|
61
45
|
import { installWriteLocks } from './index-locks.ts';
|
|
62
46
|
import { installDashboardCommand } from './dashboard-command.ts';
|
|
63
|
-
import { registerObservationPack, createCompactionBatchPackHook, type PickMiddleExcerpt } from './
|
|
47
|
+
import { registerObservationPack, createCompactionBatchPackHook, type PickMiddleExcerpt } from './plugins/observation.ts';
|
|
64
48
|
import { createJevRuntime } from './jev-client.ts';
|
|
65
49
|
import {
|
|
66
50
|
NOTICE_RANK_MIN_CONFIDENCE,
|
|
67
51
|
buildExcerptWindows,
|
|
68
52
|
composeNoticeRanking,
|
|
69
53
|
evaluateExcerptPick,
|
|
54
|
+
excerptAskIsSafe,
|
|
70
55
|
excerptPickRequest,
|
|
71
56
|
noticeRankRequest,
|
|
72
57
|
} from './jev-core.ts';
|
|
@@ -74,93 +59,66 @@ import { completeLineExcerpt } from './observation-core.ts';
|
|
|
74
59
|
import { installConfigCommand } from './config-command.ts';
|
|
75
60
|
import { CompactCoordinator } from './compact-coordinator.ts';
|
|
76
61
|
import { handleReducerToolResult, type ToolResultEventLike } from './reducer-invoker.ts';
|
|
77
|
-
import {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
} from './
|
|
81
|
-
import {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
} from './efficiency-config-core.ts';
|
|
86
|
-
import {
|
|
87
|
-
resolveDefaultFocusPollMs,
|
|
88
|
-
parseFocusSample,
|
|
89
|
-
spawnReflow,
|
|
90
|
-
startFocusPoller,
|
|
91
|
-
type FocusPoller,
|
|
92
|
-
} from './focus-poller.ts';
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* D-4: focus sampling cadence. `PIER_FOCUS_POLL_MS=0` disables the poller (heat layout then only
|
|
96
|
-
* reacts to herdr events). Invalid values fall back to the version default: 1500ms on Herdr <0.9.1,
|
|
97
|
-
* 0 (event-first) on 0.9.1+ where native pane.focused is authoritative.
|
|
98
|
-
*/
|
|
62
|
+
import { appendEfficiencyLog, efficiencyLogPath, pruneSessionObjects, resolveSessionRoot } from './efficiency-store.ts';
|
|
63
|
+
import type { RoutingTelemetryRecord, RoutingTelemetryRow } from './routing-telemetry.ts';
|
|
64
|
+
import { loadEfficiencyConfigFromDisk, resolveEfficiencyConfig, type EfficiencyConfig } from './efficiency-config-core.ts';
|
|
65
|
+
import { resolveDefaultFocusPollMs, parseFocusSample, spawnReflow, startFocusPoller, type FocusPoller } from './focus-poller.ts';
|
|
66
|
+
import { composeMasterRuntime, createRoleRuntime } from './index-roles.ts';
|
|
67
|
+
import { createGateRuntime } from './index-gates.ts';
|
|
68
|
+
|
|
69
|
+
/** D-4: PIER_FOCUS_POLL_MS=0 disables the poller; invalid values fall back to the version default. */
|
|
99
70
|
function focusPollIntervalMs(env: NodeJS.ProcessEnv = process.env, herdrVersion?: string | null): number {
|
|
100
71
|
const defaultMs = resolveDefaultFocusPollMs(herdrVersion);
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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;
|
|
105
76
|
}
|
|
106
77
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
* (gate, visible layer, badge, and session record); the baseline includes our core plus inspect/execute tools.
|
|
110
|
-
* Match only a herdr pane (HERDR_ENV=1) that is not a subagent and has no explicitly supplied manifest.
|
|
111
|
-
* Standalone pi sessions without herdr remain full-featured for backward compatibility.
|
|
112
|
-
* A malformed master.json fails open to no-role state, matching malformed env semantics.
|
|
113
|
-
*/
|
|
114
|
-
function composeMasterRuntime(): RuntimeRoleManifest | null {
|
|
115
|
-
try {
|
|
116
|
-
// 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.
|
|
117
|
-
const { role, manifest } = composeForRole('master', [], { loadRoleOpts: { builtinDirect: true } });
|
|
118
|
-
return {
|
|
119
|
-
role: role.role,
|
|
120
|
-
version: role.version,
|
|
121
|
-
tools: manifest.tools,
|
|
122
|
-
permissions: manifest.permissions,
|
|
123
|
-
unknownTools: manifest.unknownTools,
|
|
124
|
-
services: role.services ?? {},
|
|
125
|
-
};
|
|
126
|
-
} catch (err) {
|
|
127
|
-
console.error(`[pi-herdr] master manifest invalid (fail-open): ${err instanceof Error ? err.message : String(err)}`);
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
78
|
+
function agentSessionsDir(): string {
|
|
79
|
+
return process.env.PI_CODING_AGENT_DIR ? join(process.env.PI_CODING_AGENT_DIR, 'sessions') : platformPaths.sessionsDir;
|
|
130
80
|
}
|
|
131
|
-
// C3: cordis belongs only in the master process; subagent-scope includes @deepseek-ai/cordis.
|
|
132
|
-
// Dynamically import it on the master branch so worker processes never load the module.
|
|
133
|
-
// Subagent constants/helpers moved with the family to core/subagent.ts.
|
|
134
81
|
|
|
135
82
|
export default async function (pi: ExtensionAPI) {
|
|
136
83
|
const mode = planIndexMode();
|
|
137
84
|
const isSubagent = mode.isSubagent;
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const { client, env } = createHerdrClient();
|
|
85
|
+
const roleBase = process.env.PI_HERDR_ROLE_BASE || process.cwd();
|
|
86
|
+
|
|
87
|
+
let sessionId: string = process.env.PI_SESSION_FILE ?? process.env.PI_SESSION_ID ?? '';
|
|
88
|
+
let sessionRoot: string | null = null;
|
|
143
89
|
let effConfig: EfficiencyConfig = resolveEfficiencyConfig();
|
|
144
90
|
const coordinator = new CompactCoordinator();
|
|
145
|
-
|
|
91
|
+
const { client, env } = createHerdrClient();
|
|
146
92
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
93
|
+
const initialManifest =
|
|
94
|
+
parseRuntimeManifest(process.env.PI_HERDR_ROLE_MANIFEST) ??
|
|
95
|
+
(mode.composeMaster ? composeMasterRuntime() : null);
|
|
96
|
+
const todos = new TodosService(TodosService.configFromRuntime(initialManifest, isSubagent));
|
|
97
|
+
|
|
98
|
+
/** Phase 0 routing telemetry (RFC docs/rfc-jev-role-routing.md §8): best-effort append; never gates anything. */
|
|
99
|
+
const appendRoutingLog = (row: RoutingTelemetryRecord): void => {
|
|
100
|
+
const root = sessionRoot;
|
|
101
|
+
if (!root) return;
|
|
102
|
+
const stamped: RoutingTelemetryRow = { ...row, sessionId: sessionId || 'unknown' };
|
|
103
|
+
void appendEfficiencyLog(efficiencyLogPath(root, 'routing'), stamped).catch(() => {});
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const roles = createRoleRuntime({ pi, client, roleBase, initialManifest, isSubagent, todos, appendRoutingLog });
|
|
107
|
+
|
|
108
|
+
/* ── Jev decision layer (RFC docs/rfc-jev-integration.md): direct HTTP, total-budget
|
|
109
|
+
* abort, fail-open at every site. Disabled or keyless → behavior identical to before. ── */
|
|
110
|
+
const jevRuntime = createJevRuntime(() => effConfig.jev, { getSessionRoot: () => sessionRoot, getSessionId: () => sessionId || null });
|
|
153
111
|
/** P0-3: candidate windows are generated in code; jev only picks (rerank pattern). */
|
|
154
112
|
const pickMiddleExcerpt: PickMiddleExcerpt = async (text, excerptBudgetBytes) => {
|
|
155
113
|
if (!jevRuntime.available) return null;
|
|
156
114
|
const halfBudget = Math.floor(excerptBudgetBytes / 2);
|
|
157
115
|
const windows = buildExcerptWindows(text, halfBudget);
|
|
158
|
-
if (windows.length === 0) return null; //
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
);
|
|
116
|
+
if (windows.length === 0) return null; // degenerate budget only
|
|
117
|
+
const headExcerpt = completeLineExcerpt(text, halfBudget, false);
|
|
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);
|
|
164
122
|
if (!request) return null;
|
|
165
123
|
const result = await jevRuntime.ask(request, {
|
|
166
124
|
questionId: 'obs-excerpt-window',
|
|
@@ -171,84 +129,67 @@ export default async function (pi: ExtensionAPI) {
|
|
|
171
129
|
},
|
|
172
130
|
});
|
|
173
131
|
if (!result.ok) return null;
|
|
174
|
-
const picked = evaluateExcerptPick(result.answers,
|
|
132
|
+
const picked = evaluateExcerptPick(result.answers, effConfig.jev.minConfidence);
|
|
175
133
|
const window = picked === null ? undefined : windows.find((w) => w.id === picked);
|
|
176
134
|
return window ? { text: window.text, label: window.label } : null;
|
|
177
135
|
};
|
|
178
|
-
|
|
179
|
-
// best-effort (older pi builds without the renderer API simply keep the raw rows).
|
|
136
|
+
|
|
180
137
|
const rendererTypes = installRenderers(pi);
|
|
181
138
|
if (process.env.PI_HERDR_TRACE) {
|
|
182
139
|
console.error(`[pi-herdr] transcript renderers: ${rendererTypes.length ? rendererTypes.join(', ') : 'none'}`);
|
|
183
140
|
}
|
|
184
141
|
|
|
185
|
-
let
|
|
186
|
-
|
|
142
|
+
let agentActive = false;
|
|
143
|
+
let lastStopReason: string | null = null;
|
|
144
|
+
let latestCtx: { abort?: () => void } | null = null;
|
|
187
145
|
const completedStamps: number[] = [];
|
|
146
|
+
const subagentPort = emptySubagentPortBox();
|
|
147
|
+
let d96NoticeKey: string | null = null;
|
|
148
|
+
let d96NoticeAt = 0;
|
|
188
149
|
|
|
189
|
-
|
|
150
|
+
const todoUi: TodoUiSlot = { renderWidget: () => { /* filled after plugin mounting */ } };
|
|
151
|
+
const gates = createGateRuntime({
|
|
152
|
+
pi,
|
|
153
|
+
client,
|
|
154
|
+
rerenderWidget: () => todoUi.rerenderWidget?.(),
|
|
155
|
+
isAgentActive: () => agentActive,
|
|
156
|
+
idleMessage: () => roles.idleBadge(),
|
|
157
|
+
workingMessage: () => currentActivity(todos.items),
|
|
158
|
+
});
|
|
190
159
|
|
|
191
|
-
|
|
160
|
+
/** Session identity: sessionManager is authoritative (env may be unset in print/RPC mode). */
|
|
161
|
+
function resolveSessionId(ctx: unknown): string {
|
|
192
162
|
try {
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
163
|
+
const sm = (ctx as {
|
|
164
|
+
sessionManager?: { getSessionFile?: () => string | undefined; getSessionId?: () => string };
|
|
165
|
+
}).sessionManager;
|
|
166
|
+
return bareSessionId(
|
|
167
|
+
sm?.getSessionFile?.() ?? sm?.getSessionId?.() ?? process.env.PI_SESSION_FILE ?? process.env.PI_SESSION_ID ?? '',
|
|
168
|
+
);
|
|
197
169
|
} catch {
|
|
198
|
-
|
|
170
|
+
return bareSessionId(process.env.PI_SESSION_FILE ?? '');
|
|
199
171
|
}
|
|
200
172
|
}
|
|
201
173
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
/** Tier 2: worker role badge parsed from the env manifest and kept visible while idle. */
|
|
205
|
-
let roleBadge: string | null = null;
|
|
206
|
-
/** Human-gate depth; ask_user_question and external herdr:blocked events share it. */
|
|
207
|
-
let blockedDepth = 0;
|
|
208
|
-
|
|
209
|
-
function reportAgent(state: 'working' | 'idle' | 'blocked', activity: string | null): void {
|
|
210
|
-
if (!client.available) return;
|
|
211
|
-
// While a human gate is open, never let tool-badge / turn_start / settle overwrite blocked.
|
|
212
|
-
// Needed when herdr:pi is absent (pi-herdr is the authority); no-op under full-lifecycle herdr:pi.
|
|
213
|
-
if (blockedDepth > 0 && state !== 'blocked') return;
|
|
214
|
-
const message = activity ?? (state === 'idle' ? roleBadge : null);
|
|
215
|
-
client.reportAgent(state, message).catch(() => {});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/** Session identity: sessionManager is authoritative (env may be unset in print/RPC mode), with env as fallback. */
|
|
219
|
-
function resolveSessionId(ctx: unknown): string {
|
|
174
|
+
/** Branch reconstruction: todos + OCC debt state (branch correctness from the last snapshot on the branch). */
|
|
175
|
+
function rebuildFromBranch(ctx: unknown): void {
|
|
220
176
|
try {
|
|
221
|
-
const
|
|
222
|
-
sessionManager
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
};
|
|
226
|
-
}).sessionManager;
|
|
227
|
-
return (
|
|
228
|
-
sm?.getSessionFile?.() ??
|
|
229
|
-
sm?.getSessionId?.() ??
|
|
230
|
-
process.env.PI_SESSION_FILE ??
|
|
231
|
-
process.env.PI_SESSION_ID ??
|
|
232
|
-
''
|
|
233
|
-
);
|
|
177
|
+
const entries = (ctx as { sessionManager?: { getBranch?: () => readonly unknown[] } })
|
|
178
|
+
?.sessionManager?.getBranch?.() ?? [];
|
|
179
|
+
todos.rebuild(entries);
|
|
180
|
+
coordinator.rebuildFromBranch(entries);
|
|
234
181
|
} catch {
|
|
235
|
-
|
|
182
|
+
/* A reconstruction failure must not disrupt the main flow; the next todo_write re-anchors. */
|
|
236
183
|
}
|
|
237
184
|
}
|
|
238
185
|
|
|
239
186
|
function mirrorTodos(): void {
|
|
240
187
|
if (!client.available) return;
|
|
241
188
|
const label = sessionId || (env ? `pane:${env.paneId}` : '');
|
|
242
|
-
// M16: progress badge (conservative N/M plus confidence ETA; fall back to a plain count when estimates are unreliable).
|
|
243
189
|
const p = progressOf(todos.items);
|
|
244
190
|
const eta = estimateEta({ completedAt: completedStamps, total: p.total, now: Date.now() });
|
|
245
191
|
const suffix = formatProgressSuffix({ completed: p.completed, total: p.total, eta });
|
|
246
|
-
|
|
247
|
-
const title = formatPaneTitle(todos.items, null, {
|
|
248
|
-
progressSuffix: suffix,
|
|
249
|
-
lastWriteAt: todos.lastWriteAt,
|
|
250
|
-
});
|
|
251
|
-
// D97: overlay content shares the todo snapshot; thinking tokens cannot reach it.
|
|
192
|
+
const title = formatPaneTitle(todos.items, null, { progressSuffix: suffix, lastWriteAt: todos.lastWriteAt });
|
|
252
193
|
updateSlimFrame({ title, items: todos.items, lastWriteAt: todos.lastWriteAt });
|
|
253
194
|
client.reportMetadata({
|
|
254
195
|
session: label,
|
|
@@ -258,8 +199,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
258
199
|
}).catch(() => {});
|
|
259
200
|
}
|
|
260
201
|
|
|
261
|
-
|
|
262
|
-
|
|
202
|
+
/** M17 settlement reconciliation: auto-complete settled todos / unblock on failure notes (idempotent). */
|
|
263
203
|
function reconcileOnSettlement(description: string, outcome: 'settled' | 'failed'): string[] {
|
|
264
204
|
if (!description) return [];
|
|
265
205
|
try {
|
|
@@ -271,7 +211,6 @@ export default async function (pi: ExtensionAPI) {
|
|
|
271
211
|
{ version: 1, edits: plan.edits, ts: Date.now() },
|
|
272
212
|
);
|
|
273
213
|
} catch {
|
|
274
|
-
/* Best effort persistence; in-memory state still advances. */
|
|
275
214
|
}
|
|
276
215
|
todos.applyEdits(plan.edits, { source: 'reconcile' });
|
|
277
216
|
mirrorTodos();
|
|
@@ -282,30 +221,25 @@ export default async function (pi: ExtensionAPI) {
|
|
|
282
221
|
}
|
|
283
222
|
}
|
|
284
223
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
return notes.length ? `${base}\n${notes.join('\n')}` : base;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/* ── M16: progress and tool badges (title suffix + report_agent.message; no new protocol) ── */
|
|
224
|
+
const withReconcileNotes = (base: string, notes: readonly string[]): string =>
|
|
225
|
+
notes.length ? `${base}\n${notes.join('\n')}` : base;
|
|
291
226
|
|
|
292
|
-
todos.on('todo.completed', (e: { count: number; at: number; source?:
|
|
227
|
+
todos.on('todo.completed', (e: { count: number; at: number; source?: TodoCompletionSource }) => {
|
|
293
228
|
for (let i = 0; i < e.count; i++) completedStamps.push(e.at);
|
|
294
229
|
coordinator.recordBoundaryCompleted(e.count, e.source);
|
|
295
230
|
mirrorTodos();
|
|
296
231
|
});
|
|
297
232
|
|
|
298
|
-
|
|
233
|
+
/* ── tool badges: parallel calls start in source order, end in completion order ── */
|
|
299
234
|
const runningTools = new Map<string, string>(); // toolCallId → toolName
|
|
300
235
|
let lastToolBadge: string | null = null;
|
|
301
236
|
function reportToolBadge(): void {
|
|
302
237
|
const badge = planToolBadge([...new Set(runningTools.values())]);
|
|
303
|
-
if (badge === lastToolBadge) return; //
|
|
238
|
+
if (badge === lastToolBadge) return; // report only changes (M13 rendering discipline)
|
|
304
239
|
lastToolBadge = badge;
|
|
305
|
-
if (badge)
|
|
306
|
-
else
|
|
240
|
+
if (badge) gates.report('working', badge);
|
|
241
|
+
else gates.report(agentActive ? 'working' : 'idle', agentActive ? currentActivity(todos.items) : null);
|
|
307
242
|
}
|
|
308
|
-
|
|
309
243
|
pi.on('tool_execution_start', async (event: { toolCallId?: string; toolName?: string }) => {
|
|
310
244
|
if (typeof event?.toolCallId === 'string' && typeof event?.toolName === 'string') {
|
|
311
245
|
runningTools.set(event.toolCallId, event.toolName);
|
|
@@ -319,34 +253,141 @@ export default async function (pi: ExtensionAPI) {
|
|
|
319
253
|
}
|
|
320
254
|
});
|
|
321
255
|
|
|
322
|
-
/* ── D102
|
|
256
|
+
/* ── D102 evidence-preserving reducer (registered before append-handlers like write-locks) ── */
|
|
323
257
|
pi.on('tool_result', async (event, ctx) => {
|
|
324
258
|
if (!effConfig.evidencePreservingReducer.enabled) return;
|
|
325
259
|
if (!event || typeof event !== 'object') return;
|
|
326
260
|
return handleReducerToolResult(event as unknown as ToolResultEventLike, ctx, effConfig.evidencePreservingReducer, {
|
|
327
261
|
epoch: coordinator.state.epoch,
|
|
328
|
-
jev: { ask: jevRuntime.ask, getMinConfidence:
|
|
262
|
+
jev: { ask: jevRuntime.ask, getMinConfidence: () => effConfig.jev.minConfidence },
|
|
329
263
|
});
|
|
330
264
|
});
|
|
331
265
|
|
|
332
266
|
let locksHandle = { getHeldLocks: () => [] as readonly string[] };
|
|
333
267
|
if (env) {
|
|
334
|
-
locksHandle = installWriteLocks(pi, {
|
|
335
|
-
client,
|
|
336
|
-
env,
|
|
337
|
-
hard: process.env[WRITE_LOCK_ENV] === '1',
|
|
338
|
-
});
|
|
268
|
+
locksHandle = installWriteLocks(pi, { client, env, hard: process.env[WRITE_LOCK_ENV] === '1' });
|
|
339
269
|
}
|
|
340
270
|
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
|
|
271
|
+
/** pi's sendUserMessage is fire-and-forget (void); callers of this wrapper still get a promise. */
|
|
272
|
+
const sendUserMessageAs = (content: string, mode: 'steer' | 'followUp'): Promise<void> => {
|
|
273
|
+
(pi as unknown as { sendUserMessage?: (content: string, opts?: { deliverAs?: string; triggerTurn?: boolean }) => void })
|
|
274
|
+
.sendUserMessage?.(content, { deliverAs: mode, triggerTurn: true });
|
|
275
|
+
return Promise.resolve();
|
|
344
276
|
};
|
|
277
|
+
const notices = createNoticeBuffer({
|
|
278
|
+
isBusy: () => agentActive || lastStopReason === ABORT_STOP_REASON,
|
|
279
|
+
send: sendUserMessageAs,
|
|
280
|
+
// P0-2: rank collapsed batches by relevance to the master's in-progress work.
|
|
281
|
+
rank: async (contents) => {
|
|
282
|
+
if (!jevRuntime.available) return null;
|
|
283
|
+
const inProgressTodos = todos.items.filter((item) => item.status === 'in_progress').map((item) => item.content);
|
|
284
|
+
let rankedOrder: number[] | null | undefined;
|
|
285
|
+
const result = await jevRuntime.ask(
|
|
286
|
+
noticeRankRequest({ inProgressTodos, notices: [...contents] }),
|
|
287
|
+
{
|
|
288
|
+
questionId: 'notice-rank',
|
|
289
|
+
sessionId,
|
|
290
|
+
extra: { site: 'notice-rank', noticeCount: contents.length },
|
|
291
|
+
enrich: ({ answers }) => {
|
|
292
|
+
if (!answers) return {};
|
|
293
|
+
rankedOrder = composeNoticeRanking(
|
|
294
|
+
contents.length,
|
|
295
|
+
answers,
|
|
296
|
+
Math.max(effConfig.jev.minConfidence, NOTICE_RANK_MIN_CONFIDENCE),
|
|
297
|
+
);
|
|
298
|
+
const values = Array.from({ length: contents.length }, (_, i) => {
|
|
299
|
+
const rank = answers[`notice_${i}_rank`];
|
|
300
|
+
const fail = answers[`notice_${i}_fail`];
|
|
301
|
+
return rank !== undefined && fail !== undefined && rank.type === 'score' && fail.type === 'noul'
|
|
302
|
+
? `${rank.score.toFixed(2)}:${rank.confidence.toFixed(2)}:${fail.noul.toFixed(2)}`
|
|
303
|
+
: '?';
|
|
304
|
+
});
|
|
305
|
+
return { order: rankedOrder === null ? 'fallback' : rankedOrder.join(','), values };
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
);
|
|
309
|
+
if (!result.ok) return null;
|
|
310
|
+
return rankedOrder === undefined || rankedOrder === null ? null : rankedOrder.map((index) => contents[index]!);
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
const deliverNotice = notices.deliverNotice;
|
|
314
|
+
|
|
315
|
+
/* ── pipe channel: this pane's NDJSON server + the settle fast-path push (D48/D49/D50) ── */
|
|
316
|
+
const pipeServerBox: { current: ReturnType<typeof startPipeServer> | null } = { current: null };
|
|
317
|
+
let pendingMachineRequest: MachineRequest | null = null;
|
|
318
|
+
/** Dedup latch for settle notices: one per paneId+request id per round. */
|
|
319
|
+
const settleNoticeLatch = new Set<string>();
|
|
320
|
+
function claimSettleNotice(key: string): boolean {
|
|
321
|
+
if (settleNoticeLatch.has(key)) return false;
|
|
322
|
+
settleNoticeLatch.add(key);
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function startPipeServerFor(cwd: string): void {
|
|
327
|
+
const paneId = env?.paneId ?? '';
|
|
328
|
+
if (!paneId) return;
|
|
329
|
+
if (pipeServerBox.current) {
|
|
330
|
+
try { pipeServerBox.current.close(); } catch { /* Previous instance. */ }
|
|
331
|
+
pipeServerBox.current = null;
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
pipeServerBox.current = startPipeServer(pipeNameFor(cwd, paneId), async (req) => handlePipeRequest(req, {
|
|
335
|
+
paneId,
|
|
336
|
+
port: subagentPort,
|
|
337
|
+
claimSettleNotice,
|
|
338
|
+
deliverNotice,
|
|
339
|
+
sendUserMessageIn: (content) => sendUserMessageAs(content, 'followUp'),
|
|
340
|
+
sendUserMessageAs,
|
|
341
|
+
abort: () => { latestCtx?.abort?.(); },
|
|
342
|
+
setPendingMachineRequest: (next) => { pendingMachineRequest = next; },
|
|
343
|
+
applyRoleSwitch: roles.applyRoleSwitch,
|
|
344
|
+
}));
|
|
345
|
+
} catch {
|
|
346
|
+
/* Pipe name collision (rare): this session has no channel; callers report after ping timeout. */
|
|
347
|
+
}
|
|
348
|
+
}
|
|
345
349
|
|
|
346
|
-
|
|
350
|
+
async function pushSettleReply(): Promise<void> {
|
|
351
|
+
const req = pendingMachineRequest;
|
|
352
|
+
if (!req || !req.push || !req.from) return;
|
|
353
|
+
// An aborted settle is not a settlement: OCC's abort means the continuation turn is coming, and
|
|
354
|
+
// answering now would burn the one-shot request + the parent's claim latch on a null-text reply
|
|
355
|
+
// ("left no closing message") — the real closing message then never gets delivered. Keep the
|
|
356
|
+
// request armed; the post-compaction settle pushes it (settle-wake-core.ts).
|
|
357
|
+
if (!shouldPushSettleReply({
|
|
358
|
+
intentionalAbort: coordinator.intentionalAbort,
|
|
359
|
+
compactionInFlight: coordinator.compactionInFlight,
|
|
360
|
+
lastStopReason,
|
|
361
|
+
})) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
pendingMachineRequest = null;
|
|
365
|
+
try {
|
|
366
|
+
const ownFile = sessionId ? resolveSessionFileValue(process.cwd(), agentSessionsDir(), sessionId) : null;
|
|
367
|
+
let text: string | null = null;
|
|
368
|
+
if (ownFile) {
|
|
369
|
+
const entries = readSessionFile(ownFile);
|
|
370
|
+
if (entries) text = lastAssistantText(entries, { sinceTs: req.sinceTs })?.text ?? null;
|
|
371
|
+
}
|
|
372
|
+
await pipeRequest(req.from, {
|
|
373
|
+
type: 'reply',
|
|
374
|
+
id: req.id,
|
|
375
|
+
paneId: env?.paneId ?? '',
|
|
376
|
+
text,
|
|
377
|
+
// Push the resolved PATH when readable; fall back to the bare id for the master-side mapping.
|
|
378
|
+
sessionFile: ownFile ?? (sessionId || null),
|
|
379
|
+
}, 5000);
|
|
380
|
+
} catch (err) {
|
|
381
|
+
/* The requester's pollLoop is the fallback; surface the dead letter for triage. */
|
|
382
|
+
console.error(`pier: settle reply push to ${req.from} failed: ${(err as Error)?.message ?? err}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const sessionFocusPoller: { current: FocusPoller | null } = { current: null };
|
|
347
387
|
|
|
348
388
|
pi.on('session_start', async (event, ctx) => {
|
|
349
389
|
sessionId = resolveSessionId(ctx);
|
|
390
|
+
latestCtx = ctx as { abort?: () => void } | null;
|
|
350
391
|
const cwd = (ctx as { cwd?: string }).cwd ?? process.cwd();
|
|
351
392
|
const isTrusted =
|
|
352
393
|
typeof (ctx as { isProjectTrusted?: () => boolean }).isProjectTrusted === 'function'
|
|
@@ -357,117 +398,63 @@ export default async function (pi: ExtensionAPI) {
|
|
|
357
398
|
(ctx as { sessionManager?: { getSessionDir?: () => string | undefined } })?.sessionManager?.getSessionDir?.(),
|
|
358
399
|
sessionId,
|
|
359
400
|
);
|
|
360
|
-
|
|
361
|
-
//
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if (reason !== 'resume') todoUi.renderWidget(ctx);
|
|
365
|
-
// D97: narrow-frame overlay is meaningful only inside herdr; covering an interactive narrow terminal is unsafe,
|
|
366
|
-
// and the heatmap amplification provides the exit path. Re-register on resume because session switching resets the overlay.
|
|
401
|
+
// On resume pi restores widget state from the session; calling setWidget again
|
|
402
|
+
// breaks the TUI '/' command-panel route (v1.3 M9).
|
|
403
|
+
if ((event as { reason?: string } | undefined)?.reason !== 'resume') todoUi.renderWidget(ctx);
|
|
404
|
+
// D97: narrow-frame overlay is meaningful only inside herdr; re-register (session switching resets it).
|
|
367
405
|
if (env) registerSlimFrame(ctx);
|
|
406
|
+
// P2-5: resume folds todos/compaction state from the branch like session_tree; a fresh session folds nothing.
|
|
407
|
+
rebuildFromBranch(ctx);
|
|
368
408
|
mirrorTodos();
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
// A bare pi without a manifest does not report, so ordinary pi sessions remain undisturbed.
|
|
375
|
-
if (runtimeManifest) {
|
|
376
|
-
const roleDisplay = runtimeManifest.role === 'worker-default' ? 'worker' : runtimeManifest.role;
|
|
377
|
-
void client.reportDisplayAgent(roleDisplay);
|
|
378
|
-
}
|
|
379
|
-
// Tier 2: worker role manifest parsed at process start supplies the badge and authoritative session record.
|
|
380
|
-
// The custom entry mirrors D38 todo-edit and anchors execution-time replay.
|
|
381
|
-
if (runtimeManifest) {
|
|
382
|
-
roleBadge = `role ${runtimeManifest.role} v${runtimeManifest.version ?? '?'} (${runtimeManifest.tools.length} tools)`;
|
|
383
|
-
try {
|
|
384
|
-
(pi as { appendEntry?: (customType: string, data: unknown) => void }).appendEntry?.(
|
|
385
|
-
ROLE_MANIFEST_CUSTOM_TYPE,
|
|
386
|
-
{ version: 1, role: runtimeManifest.role, manifestVersion: runtimeManifest.version, tools: runtimeManifest.tools, permissions: runtimeManifest.permissions, unknownTools: runtimeManifest.unknownTools ?? 'deny', ts: Date.now() },
|
|
387
|
-
);
|
|
388
|
-
} catch {
|
|
389
|
-
/* Best effort recording. */
|
|
390
|
-
}
|
|
391
|
-
// D77 visible layer: remove tools outside the manifest from the model's view after all plugins load at session_start.
|
|
392
|
-
// Intersection semantics prevent clearing everything; master without a manifest stays full. Missing APIs are skipped for old pi compatibility.
|
|
393
|
-
const piTools = pi as { getActiveTools?: () => string[]; setActiveTools?: (names: string[]) => void };
|
|
394
|
-
if (typeof piTools.getActiveTools === 'function' && typeof piTools.setActiveTools === 'function') {
|
|
395
|
-
try {
|
|
396
|
-
const active = piTools.getActiveTools();
|
|
397
|
-
const vis = planActiveTools(runtimeManifest.tools, active, {
|
|
398
|
-
unknownTools: runtimeManifest.unknownTools,
|
|
399
|
-
permissions: runtimeManifest.permissions,
|
|
400
|
-
});
|
|
401
|
-
if (vis?.changed) {
|
|
402
|
-
piTools.setActiveTools?.(vis.next);
|
|
403
|
-
console.error(`[pi-herdr] D77 visible-layer: role ${runtimeManifest.role} tools ${active.length} → ${vis.next.length}`);
|
|
404
|
-
}
|
|
405
|
-
} catch {
|
|
406
|
-
/* Best effort visible layer; the mandatory layer remains active. */
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
reportAgent('idle', null);
|
|
411
|
-
// Self-healing: a gate does not survive a process restart, but the ask marker lives in herdr
|
|
412
|
-
// with a 24h TTL, so a session killed while a dialog was open would otherwise keep the pane
|
|
413
|
-
// looking blocked forever. Depth is 0 on session_start, so this cannot race a live gate.
|
|
409
|
+
roles.syncFromBranch(ctx);
|
|
410
|
+
if (mode.composeMaster && sessionRoot) roles.scanRoleUsage();
|
|
411
|
+
gates.report('idle', null);
|
|
412
|
+
// Self-healing: the ask marker lives in herdr with a 24h TTL; a session killed
|
|
413
|
+
// while a dialog was open must not keep the pane looking blocked forever.
|
|
414
414
|
void client.reportAskFlag(null).catch(() => {});
|
|
415
|
-
});
|
|
416
415
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
);
|
|
438
|
-
} catch {
|
|
439
|
-
/* Best effort. */
|
|
440
|
-
}
|
|
416
|
+
// D-4 focus sampling: herdr <0.9.1 never delivered pane.focused to plugins;
|
|
417
|
+
// 0.9.1+ is event-first (pollMs 0). One poller per pane, self-scoped.
|
|
418
|
+
if (sessionFocusPoller.current) sessionFocusPoller.current.stop();
|
|
419
|
+
sessionFocusPoller.current = null;
|
|
420
|
+
const paneId = env?.paneId ?? '';
|
|
421
|
+
const serverVer = await client.getServerVersion();
|
|
422
|
+
const pollMs = focusPollIntervalMs(process.env, serverVer);
|
|
423
|
+
if (paneId && client.available && pollMs > 0) {
|
|
424
|
+
sessionFocusPoller.current = startFocusPoller({
|
|
425
|
+
myPaneId: paneId,
|
|
426
|
+
intervalMs: pollMs,
|
|
427
|
+
sample: async () => {
|
|
428
|
+
// layout.export reports the focused pane of our own tab; without the
|
|
429
|
+
// field (older herdr) the tick stays silent instead of guessing.
|
|
430
|
+
const layout = await client.exportLayout({ paneId });
|
|
431
|
+
const sample = parseFocusSample(layout);
|
|
432
|
+
return sample && sample.focusedPaneId !== null ? sample : null;
|
|
433
|
+
},
|
|
434
|
+
fire: (focusedPaneId, cause) => spawnReflow({ paneId: focusedPaneId, cause }),
|
|
435
|
+
});
|
|
441
436
|
}
|
|
437
|
+
startPipeServerFor(cwd);
|
|
442
438
|
});
|
|
443
439
|
|
|
444
|
-
|
|
445
|
-
// Session-tree navigation (/tree, /fork): reconstruction preserves branch correctness.
|
|
446
440
|
pi.on('session_tree', async (_event, ctx) => {
|
|
447
441
|
sessionId = resolveSessionId(ctx);
|
|
448
442
|
rebuildFromBranch(ctx);
|
|
443
|
+
// pi reverts the transcript loadout at the navigated branch point; the role
|
|
444
|
+
// runtime follows the same point (same-origin, RFC §4.3).
|
|
445
|
+
roles.syncFromBranch(ctx);
|
|
449
446
|
todoUi.renderWidget(ctx);
|
|
450
447
|
mirrorTodos();
|
|
451
448
|
});
|
|
452
449
|
|
|
453
|
-
let agentActive = false;
|
|
454
|
-
const subagentPort = emptySubagentPortBox();
|
|
455
|
-
// D96 state, anchoring settle-wake-core deduplication and cooldown.
|
|
456
|
-
let d96NoticeKey: string | null = null;
|
|
457
|
-
let d96NoticeAt = 0;
|
|
458
|
-
// Prevent wake-up storms by tracking the last assistant turn's stopReason;
|
|
459
|
-
// 'aborted' means the user explicitly pressed ESC, so settlement must inject no wake-up message.
|
|
460
|
-
let lastStopReason: string | null = null;
|
|
461
450
|
pi.on('turn_start', async () => {
|
|
462
451
|
agentActive = true;
|
|
463
|
-
//
|
|
464
|
-
// does not taint subsequent turns or suppress settlement wake / notice flush.
|
|
452
|
+
// A11: reset so a previous turn's abort cannot taint this one.
|
|
465
453
|
lastStopReason = null;
|
|
466
|
-
// Safety fallback:
|
|
467
|
-
// ensure flags are reset. Pi core prohibits prompts while compaction is active, so this is safe.
|
|
454
|
+
// Safety fallback: pi core prohibits prompts while compaction is active.
|
|
468
455
|
coordinator.compactionInFlight = false;
|
|
469
456
|
coordinator.intentionalAbort = false;
|
|
470
|
-
|
|
457
|
+
gates.report('working', currentActivity(todos.items));
|
|
471
458
|
});
|
|
472
459
|
|
|
473
460
|
pi.on('before_provider_request', (_event: unknown, ctx: unknown) => {
|
|
@@ -486,29 +473,33 @@ export default async function (pi: ExtensionAPI) {
|
|
|
486
473
|
pi.on('session_before_tree', () => (coordinator.compactionInFlight ? { cancel: true } : undefined));
|
|
487
474
|
|
|
488
475
|
pi.on('turn_end', async (event: unknown, ctx) => {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
476
|
+
// Master registered this twice: the lifecycle handler skipped malformed events, the notice
|
|
477
|
+
// flush ran regardless. The merged handler keeps both semantics.
|
|
478
|
+
const msg = event !== null && typeof event === 'object' && 'message' in event ? (event as { message: unknown }).message : null;
|
|
479
|
+
if (msg !== null && typeof msg === 'object') {
|
|
480
|
+
const { role, stopReason } = msg as { role?: unknown; stopReason?: unknown };
|
|
481
|
+
if (role === 'assistant' && typeof stopReason === 'string') {
|
|
482
|
+
lastStopReason = stopReason;
|
|
483
|
+
}
|
|
484
|
+
if (ctx && typeof ctx === 'object') {
|
|
485
|
+
coordinator.onTurnEnd({
|
|
486
|
+
ctx,
|
|
487
|
+
todos: todos.items,
|
|
488
|
+
config: effConfig.onlineContextCompact,
|
|
489
|
+
cancelReminder: todoUi.cancelReminder,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
503
492
|
}
|
|
493
|
+
void notices.flush('steer');
|
|
504
494
|
});
|
|
505
495
|
|
|
506
496
|
pi.on('agent_settled', async (_event: unknown, ctx) => {
|
|
507
497
|
agentActive = false;
|
|
508
|
-
|
|
498
|
+
gates.report('idle', null);
|
|
509
499
|
const plan = planSettleWake({
|
|
510
500
|
lastStopReason,
|
|
511
501
|
intentionalAbort: coordinator.intentionalAbort,
|
|
502
|
+
compactionPending: coordinator.selectedCompaction !== null || coordinator.compactionInFlight,
|
|
512
503
|
running: subagentPort.current?.listRunningSubs() ?? [],
|
|
513
504
|
lastNoticeKey: d96NoticeKey,
|
|
514
505
|
lastNoticeAt: d96NoticeAt,
|
|
@@ -516,19 +507,14 @@ export default async function (pi: ExtensionAPI) {
|
|
|
516
507
|
});
|
|
517
508
|
d96NoticeKey = plan.noticeKey;
|
|
518
509
|
d96NoticeAt = plan.noticeAt;
|
|
519
|
-
if (!plan.wake) {
|
|
520
|
-
// Stay silent after a user abort while retaining the settlement buffer (do not clear pendingSettleNotices);
|
|
521
|
-
// deliver it on the next natural run's turn_end steer or natural settlement.
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
510
|
// D96: master settled while background subagents still run → remind (worker port is unbound).
|
|
525
|
-
if (plan.notice && !isSubagent) {
|
|
511
|
+
if (plan.wake && plan.notice && !isSubagent) {
|
|
526
512
|
const running = subagentPort.current?.listRunningSubs() ?? [];
|
|
527
513
|
const brief = running.map((s) => `${s.paneId} (${s.description})`).join('、');
|
|
528
514
|
const notice = `注意:仍有 ${running.length} 个后台 subagent 在运行:${brief}。若你的任务依赖它们,请等待其结算(subagent list 查看状态);若不等待,请说明放弃原因。`;
|
|
529
|
-
void
|
|
515
|
+
void sendUserMessageAs(notice, 'followUp');
|
|
530
516
|
}
|
|
531
|
-
if (ctx && typeof ctx === 'object') {
|
|
517
|
+
if (plan.compact && ctx && typeof ctx === 'object') {
|
|
532
518
|
void coordinator.onAgentSettled({
|
|
533
519
|
ctx,
|
|
534
520
|
todos: todos.items,
|
|
@@ -537,117 +523,21 @@ export default async function (pi: ExtensionAPI) {
|
|
|
537
523
|
cancelReminder: todoUi.cancelReminder,
|
|
538
524
|
onBeforeCompact: createCompactionBatchPackHook({
|
|
539
525
|
getObsConfig: () => effConfig.observationPack,
|
|
540
|
-
getManifest: () =>
|
|
526
|
+
getManifest: () => roles.state.manifest,
|
|
541
527
|
getSessionId: () => sessionId,
|
|
542
528
|
pickMiddleExcerpt,
|
|
543
529
|
}),
|
|
544
530
|
});
|
|
545
531
|
}
|
|
532
|
+
if (lastStopReason !== ABORT_STOP_REASON) void notices.flush('followUp');
|
|
533
|
+
void pushSettleReply();
|
|
546
534
|
});
|
|
547
535
|
|
|
548
|
-
|
|
549
|
-
// only on the herdr:blocked event (pi core never emits it). Custom source=pi-herdr
|
|
550
|
-
// pane.report_agent is ignored while that authority is live
|
|
551
|
-
// (screen_detection_skip_reason=full_lifecycle_hook_authority). Emit the event so
|
|
552
|
-
// herdr:pi publishes blocked; keep report_agent as fallback when it is absent.
|
|
553
|
-
/**
|
|
554
|
-
* Open one human gate. Nested gates (pier's ask tool plus pi's own ctx.ui prompt, or an
|
|
555
|
-
* external herdr:blocked producer) are coalesced: only the 0→1 transition reports blocked,
|
|
556
|
-
* marks the ask flag and folds the widget. Returns true when this call opened the outer gate.
|
|
557
|
-
*/
|
|
558
|
-
function enterBlocked(label: string | null): boolean {
|
|
559
|
-
blockedDepth += 1;
|
|
560
|
-
if (blockedDepth > 1) return false;
|
|
561
|
-
reportAgent('blocked', label);
|
|
562
|
-
// D95: human-gate marker lets the workbench heatmap distinguish ask from block.
|
|
563
|
-
if (label) void client.reportAskFlag(label).catch(() => {});
|
|
564
|
-
// Gate open collapses the todo widget to one line — the question owns the fixed area.
|
|
565
|
-
todoUi.rerenderWidget?.();
|
|
566
|
-
return true;
|
|
567
|
-
}
|
|
568
|
-
/** Close one gate; only the 1→0 transition restores the pre-gate state. Returns true when closed.
|
|
569
|
-
* An unmatched release (no gate open) is ignored rather than reported, so a stray
|
|
570
|
-
* ui_prompt_end cannot emit a false "no longer blocked" edge. */
|
|
571
|
-
function exitBlocked(): boolean {
|
|
572
|
-
if (blockedDepth === 0) return false;
|
|
573
|
-
blockedDepth -= 1;
|
|
574
|
-
// Gate closed → restore the full widget window.
|
|
575
|
-
todoUi.rerenderWidget?.();
|
|
576
|
-
if (blockedDepth > 0) return false;
|
|
577
|
-
reportAgent(agentActive ? 'working' : 'idle', agentActive ? currentActivity(todos.items) : null);
|
|
578
|
-
// D95: gate released → clear marker.
|
|
579
|
-
void client.reportAskFlag(null).catch(() => {});
|
|
580
|
-
return true;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// emit() is synchronous; this flag stops our own listener from double-counting depth.
|
|
584
|
-
let publishingHerdrBlocked = false;
|
|
585
|
-
/** Publish the herdr:blocked edge for pi-herdr's own gates; no-op re-entry is prevented by the flag. */
|
|
586
|
-
function emitHerdrBlocked(active: boolean, label: string | null): void {
|
|
587
|
-
publishingHerdrBlocked = true;
|
|
588
|
-
try {
|
|
589
|
-
pi.events.emit(
|
|
590
|
-
'herdr:blocked',
|
|
591
|
-
active ? { active: true, ...(label ? { label } : {}) } : { active: false },
|
|
592
|
-
);
|
|
593
|
-
} finally {
|
|
594
|
-
publishingHerdrBlocked = false;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
function publishHerdrBlocked(active: boolean, label: string | null): void {
|
|
598
|
-
if (active) enterBlocked(label);
|
|
599
|
-
else exitBlocked();
|
|
600
|
-
emitHerdrBlocked(active, label);
|
|
601
|
-
}
|
|
602
|
-
pi.events.on('herdr:blocked', (data) => {
|
|
603
|
-
if (publishingHerdrBlocked) return;
|
|
604
|
-
if (!data || typeof data !== 'object') {
|
|
605
|
-
exitBlocked();
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
if (!('active' in data) || data.active !== true) {
|
|
609
|
-
exitBlocked();
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
|
-
enterBlocked('label' in data && typeof data.label === 'string' ? data.label : null);
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
/* ── Generic human-gate reporting (pi 0.84.4+) ─────────────────────────
|
|
616
|
-
* Why: `ui_prompt_start` / `ui_prompt_end` fire around blocking ctx.ui dialogs.
|
|
617
|
-
* Scope: only the inherently human-blocking kinds (select / confirm / input / editor).
|
|
618
|
-
* `custom` is deliberately excluded: pi routes both modals and *persistent overlays*
|
|
619
|
-
* through ctx.ui.custom, and pier itself registers a resident slim-frame overlay that
|
|
620
|
-
* never calls done() — treating it as a gate left the pane blocked (and the ask marker
|
|
621
|
-
* set) for the entire session while the agent kept working. Note that a resident overlay
|
|
622
|
-
* also holds pi's own prompt span open, so these events generally stop firing in herdr
|
|
623
|
-
* panes anyway; pier's ask tool opens its gate explicitly and is the reliable path there.
|
|
624
|
-
* Probed through a structural type because the events postdate our pinned pi devDependency. */
|
|
625
|
-
const uiPromptEvents = pi as unknown as {
|
|
626
|
-
on?: (name: 'ui_prompt_start' | 'ui_prompt_end', handler: (event: unknown) => void) => void;
|
|
627
|
-
};
|
|
628
|
-
const GATING_PROMPT_KINDS = new Set(['select', 'confirm', 'input', 'editor']);
|
|
629
|
-
uiPromptEvents.on?.('ui_prompt_start', (event) => {
|
|
630
|
-
const rec = (event ?? {}) as { kind?: unknown; title?: unknown };
|
|
631
|
-
const title = typeof rec.title === 'string' && rec.title.trim() ? rec.title.trim() : null;
|
|
632
|
-
const kind = typeof rec.kind === 'string' ? rec.kind : 'prompt';
|
|
633
|
-
if (!GATING_PROMPT_KINDS.has(kind)) return;
|
|
634
|
-
// Normalize once so the gate report and the published edge carry the same label.
|
|
635
|
-
const label = title ?? kind;
|
|
636
|
-
// The ask tool opens its own gate first, so this is usually a nested (no-op) transition;
|
|
637
|
-
// prompts from other extensions or pi core open the gate here.
|
|
638
|
-
if (enterBlocked(label)) emitHerdrBlocked(true, label);
|
|
639
|
-
});
|
|
640
|
-
uiPromptEvents.on?.('ui_prompt_end', () => {
|
|
641
|
-
if (exitBlocked()) emitHerdrBlocked(false, null);
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
/* ── Tool: ask_user_question (v1.3 M8 human gate, available to master and subagents) ── */
|
|
645
|
-
|
|
646
|
-
/* ── D101: ObservationPack (obs_recall tool & context projector) ── */
|
|
536
|
+
/* ── D101 ObservationPack (obs_recall tool & context projector) ── */
|
|
647
537
|
registerObservationPack({
|
|
648
538
|
pi,
|
|
649
539
|
getConfig: () => effConfig,
|
|
650
|
-
getRuntimeManifest: () =>
|
|
540
|
+
getRuntimeManifest: () => roles.state.manifest,
|
|
651
541
|
pickMiddleExcerpt,
|
|
652
542
|
getRemainingHorizon: () => {
|
|
653
543
|
const usage = (latestCtx as { getContextUsage?: () => { contextWindow?: number } })?.getContextUsage?.();
|
|
@@ -655,10 +545,6 @@ export default async function (pi: ExtensionAPI) {
|
|
|
655
545
|
},
|
|
656
546
|
});
|
|
657
547
|
|
|
658
|
-
/* ── D104: /pier-config (read-only config guide + guided-change handoff) ──
|
|
659
|
-
* Supersedes the former /efficiency command: the index line reports OCC/OBS/EPR state and
|
|
660
|
-
* `show efficiency` lists every D100-D103 knob with its effective value and source. */
|
|
661
|
-
|
|
662
548
|
installConfigCommand({ pi });
|
|
663
549
|
installDashboardCommand({
|
|
664
550
|
pi,
|
|
@@ -668,6 +554,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
668
554
|
getHeldLocks: () => locksHandle.getHeldLocks(),
|
|
669
555
|
});
|
|
670
556
|
|
|
557
|
+
/* ── ask_user_question (v1.3 M8 human gate; master and subagents) ── */
|
|
671
558
|
pi.registerTool({
|
|
672
559
|
name: ASK_TOOL_NAME,
|
|
673
560
|
label: 'Ask User',
|
|
@@ -679,15 +566,13 @@ export default async function (pi: ExtensionAPI) {
|
|
|
679
566
|
if (!prepared.ok) return prepared.result;
|
|
680
567
|
const ui = ctx && typeof ctx === 'object' && 'ui' in ctx ? ctx.ui : undefined;
|
|
681
568
|
if (!hasAskUi(ui)) return noUiResult();
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
//
|
|
685
|
-
// blockedDepth > 0, and pi's ui_prompt_start/end events keep the depth honest for prompts
|
|
686
|
-
// that do not come from this tool. (The old 5s refresh interval is obsolete.)
|
|
569
|
+
gates.publish(true, gateLabel(prepared.spec));
|
|
570
|
+
// The gate stays reported for the whole wait: gates.report drops
|
|
571
|
+
// working/idle while blocked, and pi's ui_prompt events keep the depth honest.
|
|
687
572
|
try {
|
|
688
573
|
return await runAsk(prepared.spec, ui, signal instanceof AbortSignal ? signal : undefined);
|
|
689
574
|
} finally {
|
|
690
|
-
|
|
575
|
+
gates.publish(false, null);
|
|
691
576
|
}
|
|
692
577
|
},
|
|
693
578
|
});
|
|
@@ -695,168 +580,22 @@ export default async function (pi: ExtensionAPI) {
|
|
|
695
580
|
pi.on('session_shutdown', async () => {
|
|
696
581
|
coordinator.compactionInFlight = false;
|
|
697
582
|
coordinator.intentionalAbort = false;
|
|
698
|
-
if (sessionRoot) {
|
|
699
|
-
try {
|
|
700
|
-
await pruneSessionObjects(sessionRoot);
|
|
701
|
-
} catch {
|
|
702
|
-
/* ignore shutdown pruning errors */
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
client.close();
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
/* ── M12: bidirectional message channel (D49/D50/D48; every pane listens on its own name, with no controller/child assumption) ── */
|
|
709
|
-
|
|
710
|
-
const settleNoticeLatch = new Set<string>();
|
|
711
|
-
/** Deduplicate settlement notices: choose push fast path or pollLoop fallback; one notice per paneId+request ID per round. */
|
|
712
|
-
function claimSettleNotice(key: string): boolean {
|
|
713
|
-
if (settleNoticeLatch.has(key)) return false;
|
|
714
|
-
settleNoticeLatch.add(key);
|
|
715
|
-
return true;
|
|
716
|
-
}
|
|
717
|
-
const pipeServerBox: { current: ReturnType<typeof startPipeServer> | null } = { current: null };
|
|
718
|
-
/** D-4: focus sampler for this pane (started on session_start, stopped on session_shutdown). */
|
|
719
|
-
const sessionFocusPoller: { current: FocusPoller | null } = { current: null };
|
|
720
|
-
let pendingMachineRequest: { id: string; from: string | null; push: boolean; sinceTs: number } | null = null;
|
|
721
|
-
let latestCtx: { abort?: () => void } | null = null;
|
|
722
|
-
|
|
723
|
-
// triggerTurn:true — idle followUp must start a new turn or settlement is lost.
|
|
724
|
-
const sendUserMessageIn = (content: string): Promise<void> =>
|
|
725
|
-
(pi as unknown as { sendUserMessage?: (content: string, opts?: { deliverAs?: string; triggerTurn?: boolean }) => Promise<void> })
|
|
726
|
-
.sendUserMessage?.(content, { deliverAs: 'followUp', triggerTurn: true }) ?? Promise.resolve();
|
|
727
|
-
const sendUserMessageAs = (content: string, mode: 'steer' | 'followUp'): Promise<void> =>
|
|
728
|
-
(pi as unknown as { sendUserMessage?: (content: string, opts?: { deliverAs?: string; triggerTurn?: boolean }) => Promise<void> })
|
|
729
|
-
.sendUserMessage?.(content, { deliverAs: mode, triggerTurn: true }) ?? Promise.resolve();
|
|
730
|
-
|
|
731
|
-
const notices = createNoticeBuffer({
|
|
732
|
-
isBusy: () => agentActive || lastStopReason === ABORT_STOP_REASON,
|
|
733
|
-
send: sendUserMessageAs,
|
|
734
|
-
// P0-2: rank collapsed batches by relevance to the master's in-progress work
|
|
735
|
-
rank: async (contents) => {
|
|
736
|
-
if (!jevRuntime.available) return null;
|
|
737
|
-
const inProgressTodos = todos.items
|
|
738
|
-
.filter((item) => item.status === 'in_progress')
|
|
739
|
-
.map((item) => item.content);
|
|
740
|
-
let rankedOrder: number[] | null | undefined;
|
|
741
|
-
const result = await jevRuntime.ask(
|
|
742
|
-
noticeRankRequest({ inProgressTodos, notices: [...contents] }),
|
|
743
|
-
{
|
|
744
|
-
questionId: 'notice-rank',
|
|
745
|
-
sessionId,
|
|
746
|
-
extra: { site: 'notice-rank', noticeCount: contents.length },
|
|
747
|
-
enrich: ({ answers }) => {
|
|
748
|
-
if (!answers) return {};
|
|
749
|
-
rankedOrder = composeNoticeRanking(
|
|
750
|
-
contents.length,
|
|
751
|
-
answers,
|
|
752
|
-
Math.max(effConfig.jev.minConfidence, NOTICE_RANK_MIN_CONFIDENCE),
|
|
753
|
-
);
|
|
754
|
-
const values = Array.from({ length: contents.length }, (_, i) => {
|
|
755
|
-
const rank = answers[`notice_${i}_rank`];
|
|
756
|
-
const fail = answers[`notice_${i}_fail`];
|
|
757
|
-
return rank !== undefined && fail !== undefined && rank.type === 'score' && fail.type === 'noul'
|
|
758
|
-
? `${rank.score.toFixed(2)}:${rank.confidence.toFixed(2)}:${fail.noul.toFixed(2)}`
|
|
759
|
-
: '?';
|
|
760
|
-
});
|
|
761
|
-
return { order: rankedOrder === null ? 'fallback' : rankedOrder.join(','), values };
|
|
762
|
-
},
|
|
763
|
-
},
|
|
764
|
-
);
|
|
765
|
-
if (!result.ok) return null;
|
|
766
|
-
return rankedOrder === undefined || rankedOrder === null ? null : rankedOrder.map((index) => contents[index]!);
|
|
767
|
-
},
|
|
768
|
-
});
|
|
769
|
-
const deliverNotice = notices.deliverNotice;
|
|
770
|
-
pi.on('turn_end', () => {
|
|
771
|
-
void notices.flush('steer');
|
|
772
|
-
});
|
|
773
|
-
pi.on('agent_settled', () => {
|
|
774
|
-
if (lastStopReason === ABORT_STOP_REASON) return;
|
|
775
|
-
void notices.flush('followUp');
|
|
776
|
-
});
|
|
777
|
-
|
|
778
|
-
pi.on('session_start', async (_event, ctx) => {
|
|
779
|
-
latestCtx = ctx as { abort?: () => void } | null;
|
|
780
|
-
const cwd = (ctx as { cwd?: string }).cwd ?? process.cwd();
|
|
781
|
-
const paneId = env?.paneId ?? '';
|
|
782
|
-
// D-4: herdr 0.9.0 never delivered pane.focused to plugins; 0.9.1+ does. Poller is a
|
|
783
|
-
// fallback for old servers (see resolveDefaultFocusPollMs). One poller per pane, self-scoped.
|
|
784
|
-
if (sessionFocusPoller.current) {
|
|
785
|
-
sessionFocusPoller.current.stop();
|
|
786
|
-
sessionFocusPoller.current = null;
|
|
787
|
-
}
|
|
788
|
-
const serverVer = await client.getServerVersion();
|
|
789
|
-
const pollMs = focusPollIntervalMs(process.env, serverVer);
|
|
790
|
-
if (paneId && client.available && pollMs > 0) {
|
|
791
|
-
sessionFocusPoller.current = startFocusPoller({
|
|
792
|
-
myPaneId: paneId,
|
|
793
|
-
intervalMs: pollMs,
|
|
794
|
-
sample: async () => {
|
|
795
|
-
// layout.export reports the focused pane of our own tab. Without that field (older herdr)
|
|
796
|
-
// the tick stays silent instead of guessing focus from the tree.
|
|
797
|
-
const layout = await client.exportLayout({ paneId });
|
|
798
|
-
const sample = parseFocusSample(layout);
|
|
799
|
-
return sample && sample.focusedPaneId !== null ? sample : null;
|
|
800
|
-
},
|
|
801
|
-
fire: (focusedPaneId, cause) => spawnReflow({ paneId: focusedPaneId, cause }),
|
|
802
|
-
});
|
|
803
|
-
}
|
|
804
|
-
if (!paneId) return;
|
|
805
|
-
const name = pipeNameFor(cwd, paneId);
|
|
806
|
-
if (pipeServerBox.current) {
|
|
807
|
-
try { pipeServerBox.current.close(); } catch { /* Previous instance. */ }
|
|
808
|
-
pipeServerBox.current = null;
|
|
809
|
-
}
|
|
810
|
-
try {
|
|
811
|
-
pipeServerBox.current = startPipeServer(name, async (req) => handlePipeRequest(req, {
|
|
812
|
-
paneId,
|
|
813
|
-
port: subagentPort,
|
|
814
|
-
claimSettleNotice,
|
|
815
|
-
deliverNotice,
|
|
816
|
-
sendUserMessageIn,
|
|
817
|
-
sendUserMessageAs,
|
|
818
|
-
abort: () => { latestCtx?.abort?.(); },
|
|
819
|
-
setPendingMachineRequest: (next) => { pendingMachineRequest = next; },
|
|
820
|
-
}));
|
|
821
|
-
} catch {
|
|
822
|
-
/* Pipe name collision (rare): this session has no channel; callers report an error after ping times out. */
|
|
823
|
-
}
|
|
824
|
-
});
|
|
825
|
-
pi.on('session_shutdown', () => {
|
|
826
583
|
sessionFocusPoller.current?.stop();
|
|
827
584
|
sessionFocusPoller.current = null;
|
|
828
585
|
if (pipeServerBox.current) {
|
|
829
586
|
try { pipeServerBox.current.close(); } catch { /* Already closed. */ }
|
|
830
587
|
pipeServerBox.current = null;
|
|
831
588
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
const req = pendingMachineRequest;
|
|
837
|
-
if (!req || !req.push || !req.from) return;
|
|
838
|
-
pendingMachineRequest = null;
|
|
839
|
-
try {
|
|
840
|
-
let text: string | null = null;
|
|
841
|
-
if (sessionId && /\.jsonl$/.test(sessionId)) {
|
|
842
|
-
const entries = readSessionFile(sessionId);
|
|
843
|
-
if (entries) text = lastAssistantText(entries, { sinceTs: req.sinceTs })?.text ?? null;
|
|
589
|
+
if (sessionRoot) {
|
|
590
|
+
try {
|
|
591
|
+
await pruneSessionObjects(sessionRoot);
|
|
592
|
+
} catch {
|
|
844
593
|
}
|
|
845
|
-
await pipeRequest(req.from, {
|
|
846
|
-
type: 'reply',
|
|
847
|
-
id: req.id,
|
|
848
|
-
paneId: env?.paneId ?? '',
|
|
849
|
-
text,
|
|
850
|
-
sessionFile: sessionId || null,
|
|
851
|
-
}, 5000);
|
|
852
|
-
} catch {
|
|
853
|
-
/* Push failed silently; the requester's pollLoop is the fallback. */
|
|
854
594
|
}
|
|
595
|
+
client.close();
|
|
855
596
|
});
|
|
856
597
|
|
|
857
|
-
/*
|
|
858
|
-
* Bare pi and worker panes mount todo only so unused tools never appear.
|
|
859
|
-
* Dynamic import: worker/bare-pi must never load bootstrap/subagent-scope (C3). */
|
|
598
|
+
/* ── master vs worker mount (dynamic: workers never load the cordis tree, C3) ── */
|
|
860
599
|
if (mode.composeMaster) {
|
|
861
600
|
const { mountMasterPlugins } = await import('./index-master.ts');
|
|
862
601
|
await mountMasterPlugins({
|
|
@@ -872,12 +611,14 @@ export default async function (pi: ExtensionAPI) {
|
|
|
872
611
|
deliverNotice,
|
|
873
612
|
noticePending: notices.noticePending,
|
|
874
613
|
getSessionId: () => sessionId,
|
|
875
|
-
getBlockedDepth: () =>
|
|
614
|
+
getBlockedDepth: () => gates.depth(),
|
|
876
615
|
reconcileOnSettlement,
|
|
877
616
|
withReconcileNotes,
|
|
878
617
|
claimSettleNotice,
|
|
879
618
|
isCompactionInFlight: () => coordinator.compactionInFlight,
|
|
880
619
|
isIntentionalAbort: () => coordinator.intentionalAbort,
|
|
620
|
+
jev: { ask: (request, meta) => jevRuntime.ask(request, meta), getMinConfidence: () => effConfig.jev.minConfidence },
|
|
621
|
+
appendRoutingLog,
|
|
881
622
|
});
|
|
882
623
|
} else {
|
|
883
624
|
const { mountTodoOnly } = await import('./index-worker.ts');
|
|
@@ -886,12 +627,12 @@ export default async function (pi: ExtensionAPI) {
|
|
|
886
627
|
todos,
|
|
887
628
|
todoUi,
|
|
888
629
|
mirrorTodos,
|
|
889
|
-
getBlockedDepth: () =>
|
|
630
|
+
getBlockedDepth: () => gates.depth(),
|
|
890
631
|
...(isSubagent
|
|
891
632
|
? {}
|
|
892
633
|
: {
|
|
893
634
|
stopReminder: {
|
|
894
|
-
getBlockedDepth: () =>
|
|
635
|
+
getBlockedDepth: () => gates.depth(),
|
|
895
636
|
getRunningSubs: () => subagentPort.current?.listRunningSubs().length ?? 0,
|
|
896
637
|
isCompactionInFlight: () => coordinator.compactionInFlight,
|
|
897
638
|
isIntentionalAbort: () => coordinator.intentionalAbort,
|