@getpipher/armory-fleet 0.16.0 → 1.1.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/README.md +98 -1
- package/package.json +1 -1
- package/src/engine/retry-fallback.ts +14 -0
- package/src/engine/spawnSubagent.ts +13 -4
- package/src/index.ts +186 -4
- package/src/panel/fleet-panel.ts +62 -9
- package/src/panel/live-timeline.ts +32 -0
- package/src/rpc/event-bus.ts +105 -0
- package/src/rpc/rpc-server.ts +284 -0
- package/src/runtime/async-runner.ts +17 -4
- package/src/runtime/run-journal.ts +11 -0
- package/src/runtime/run-log.ts +15 -0
- package/src/settings/fleet-settings.ts +103 -0
- package/src/tools/subagent.ts +8 -1
- package/src/workflows/runtime/adapters.ts +3 -0
package/README.md
CHANGED
|
@@ -254,6 +254,20 @@ Models are an ordered fallback chain (primary first; a spawn retries the next ca
|
|
|
254
254
|
|
|
255
255
|
Live cost $ and context % are tracked per run and surfaced in the Tiers view. Override per-run with `model`, or let the tier registry route based on the task class.
|
|
256
256
|
|
|
257
|
+
### Fleet settings (v1.1.0)
|
|
258
|
+
|
|
259
|
+
`~/.pi/agent/fleet/settings.json` (global) and `<project>/.pi/fleet/settings.json` (project — wins per-field) hold fleet-wide defaults that aren't env-shaped. Currently one field:
|
|
260
|
+
|
|
261
|
+
```json
|
|
262
|
+
{
|
|
263
|
+
"defaultSubagentThinking": "high"
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`defaultSubagentThinking` (#78) sets the thinking level for **every subagent whose frontmatter does not pin `thinkingLevel`** — so a controller can think at `max` while the fleet runs at `high`, without replacing builtin agents (agent-frontmatter overrides still win; per-dispatch overrides are not exposed yet). Valid values: `off | minimal | low | medium | high | xhigh | max`. On flat subscriptions (z.ai/GLM coding plans, Ollama) this is the quota lever — thinking tokens on every cheap subagent call are pure waste.
|
|
268
|
+
|
|
269
|
+
Invalid values and unknown keys produce a startup warning naming the file, the field, and the bad value — never a silent no-op. Absent files are normal.
|
|
270
|
+
|
|
257
271
|
### Operational runtime
|
|
258
272
|
|
|
259
273
|
`src/runtime/` — the async/scheduling spine:
|
|
@@ -328,7 +342,82 @@ Every workflow run is **journaled** (`workflows/journal.ts`) and **resumable**.
|
|
|
328
342
|
- **Panel Run-action:** a 3rd `cwd` input step (task → name → cwd), prefilled with the session cwd; Enter accepts, Escape cancels.
|
|
329
343
|
- **bg/scheduled + worktree cwd-isolation (#62):** the `cwd` param now scopes background and scheduled runs too — in-place bg runs pass it as the lifecycle entry cwd, and `isolation: 'worktree'`/`'auto'` resolve isolation (and create the worktree) against the **dispatch cwd's** repo, not the session's. Cross-cwd bg worktrees land in `<child-cwd>/.pi/fleet/worktrees/`.
|
|
330
344
|
|
|
331
|
-
##
|
|
345
|
+
## Event bus + RPC (v1.0)
|
|
346
|
+
|
|
347
|
+
armory-fleet publishes every run on pi's cross-extension event bus and answers an RPC verb
|
|
348
|
+
set — so your own extensions can spawn, steer, observe, and abort subagents programmatically.
|
|
349
|
+
|
|
350
|
+
### Event stream (broadcast, `pi.events`)
|
|
351
|
+
|
|
352
|
+
Two tiers, every event enveloped as `{ runId, seq, ts, ...payload }` (`seq` = per-run
|
|
353
|
+
monotonic, one space per source store):
|
|
354
|
+
|
|
355
|
+
| Channel | Payload |
|
|
356
|
+
|---|---|
|
|
357
|
+
| `fleet:run:started` | `{ agent, model?, cwd?, sessionCwd?, mode: foreground\|background\|scheduled\|workflow, task }` |
|
|
358
|
+
| `fleet:phase:started` / `completed` / `failed` | `{ phase, … }` (lifecycle runs) |
|
|
359
|
+
| `fleet:run:ended` | `{ status: completed\|failed\|aborted, result?, error?, filesTouched?, toolCallCount?, durationMs? }` |
|
|
360
|
+
| `fleet:child:message` | `{ role, text }` (journal excerpts) |
|
|
361
|
+
| `fleet:child:tool` | `{ toolName, args, result, isError }` (one per completed tool call) |
|
|
362
|
+
|
|
363
|
+
### RPC (`fleet:rpc` → `fleet:rpc:result`)
|
|
364
|
+
|
|
365
|
+
Emit `{ id, verb, params }`, get exactly one reply `{ id, ok, data }` or
|
|
366
|
+
`{ id, ok: false, error: { code, message } }`. Verbs: `spawn` (returns `{ runId }`
|
|
367
|
+
immediately; result arrives via `fleet:run:ended`), `schedule` (#83 — returns
|
|
368
|
+
`{ scheduleId, nextFire }`; schedules run lifecycles, not single delegates), `status`,
|
|
369
|
+
`observe` (replay dump — subscribe to the broadcast channels + dedupe by
|
|
370
|
+
`(channel, runId, seq)` for the live tail), `steer`, `abort`.
|
|
371
|
+
|
|
372
|
+
`spawn` params: `agent`, `task`, `background?`, `lifecycle?` (named lifecycle — with
|
|
373
|
+
`background: true` routes through the bg runner; without it runs as a detached
|
|
374
|
+
foreground-semantics lifecycle), `modelFallback?` (per-request retry-once on a retryable
|
|
375
|
+
provider failure — the retry mints a fresh runId and relinks the primary's todo), `cwd?`,
|
|
376
|
+
`isolation?`, `maxTurns?`, `model?`, `skills?`, `readOnly?`, `track?`, `todoId?`.
|
|
377
|
+
|
|
378
|
+
`schedule` params: `task`, `expression` (cron or interval), `lifecycle?`, `auto?`,
|
|
379
|
+
`isolation?`, `cwd?` — no `agent`: the scheduler runs lifecycles only. Registration emits
|
|
380
|
+
no run events; on fire, the normal `fleet:*` stream flows with `mode: "scheduled"`.
|
|
381
|
+
|
|
382
|
+
Error codes: `E-CONTROL-DISABLED`, `E-RUN-NOT-FOUND`, `E-RUN-FINISHED`, `E-BAD-VERB`,
|
|
383
|
+
`E-BAD-PARAMS`, `E-STEER-UNSUPPORTED`, `E-INTERNAL`.
|
|
384
|
+
|
|
385
|
+
### Client helper (~15 lines)
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
type FleetReply = { id: string; ok: true; data: any } | { id: string; ok: false; error: { code: string; message: string } };
|
|
389
|
+
let n = 0;
|
|
390
|
+
export function fleetRpc(pi: { events: { emit(c: string, d: unknown): void; on(c: string, h: (d: unknown) => void): () => void } }) {
|
|
391
|
+
return <T = any>(verb: string, params?: Record<string, unknown>): Promise<T> => {
|
|
392
|
+
const id = `fleet-${Date.now().toString(36)}-${++n}`;
|
|
393
|
+
return new Promise<T>((resolve, reject) => {
|
|
394
|
+
const unsub = pi.events.on("fleet:rpc:result", (raw) => {
|
|
395
|
+
const r = raw as FleetReply;
|
|
396
|
+
if (r.id !== id) return;
|
|
397
|
+
unsub();
|
|
398
|
+
r.ok ? resolve(r.data as T) : reject(new Error(`${r.error.code}: ${r.error.message}`));
|
|
399
|
+
});
|
|
400
|
+
pi.events.emit("fleet:rpc", { id, verb, params });
|
|
401
|
+
});
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
// const rpc = fleetRpc(pi); const { runId } = await rpc("spawn", { agent: "scout", task: "look" });
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Control gate
|
|
408
|
+
|
|
409
|
+
`spawn`/`steer`/`abort`/`schedule` are on by default. Set `ARMORY_FLEET_RPC_CONTROL=0` (or `false`) to
|
|
410
|
+
reject them with `E-CONTROL-DISABLED`; read-only `observe`/`status` stay available. Honest
|
|
411
|
+
threat model: in-process extensions already have full system access through pi itself — the
|
|
412
|
+
switch guards accidents, not adversaries.
|
|
413
|
+
|
|
414
|
+
### Live conversation viewer
|
|
415
|
+
|
|
416
|
+
`/fleet` → Runs → open a **running** run: the timeline overlay streams the child's
|
|
417
|
+
conversation as it happens (tail-follows; scroll up to read, back to the bottom to re-pin).
|
|
418
|
+
Finished runs replay from the journal exactly as before.
|
|
419
|
+
|
|
420
|
+
|
|
332
421
|
|
|
333
422
|
The #62 tail of SPEC-6-5: `subagent({ cwd })` now scopes **background and scheduled** runs too, not just foreground —
|
|
334
423
|
|
|
@@ -336,6 +425,14 @@ The #62 tail of SPEC-6-5: `subagent({ cwd })` now scopes **background and schedu
|
|
|
336
425
|
- **Per-dispatch worktrees** — `isolation: 'worktree'` / `'auto'` resolve against the **dispatch cwd's repo**, not the session's; cross-cwd worktrees land in `<child-cwd>/.pi/fleet/worktrees/` and are cleaned up via the same service.
|
|
337
426
|
- **bg lifecycle parity** — the bg adapter now honors the lifecycle cwd chain (`lifecycle.cwd` → dispatch `cwd`), which also fixes a pre-existing mismatch where bg isolated runs spawned phases in the session cwd but committed in the worktree.
|
|
338
427
|
|
|
428
|
+
## RPC parity + fleet settings (v1.1.0)
|
|
429
|
+
|
|
430
|
+
Post-v1.0 batch (issues #78/#83):
|
|
431
|
+
|
|
432
|
+
- **`fleet.defaultSubagentThinking` (#78)** — a fleet-dir settings home (`~/.pi/agent/fleet/settings.json` global, `<project>/.pi/fleet/settings.json` project override) whose first field sets the thinking level for every subagent that doesn't pin its own — the quota lever for flat subscriptions. See [Fleet settings](#fleet-settings-v110).
|
|
433
|
+
- **RPC spawn parity (#83)** — `spawn` gains `lifecycle` (bg routing, or a detached foreground-semantics lifecycle run) and `modelFallback` (per-request retry-once; the retry mints a fresh runId and relinks the primary's todo).
|
|
434
|
+
- **New `schedule` verb (#83)** — `{ task, expression, lifecycle?, auto?, isolation?, cwd? }` → `{ scheduleId, nextFire }`. Schedules run lifecycles only, so `spawn`'s uniform `{ runId }` reply stays unbranched; the frozen surface needed zero renames and zero new error codes.
|
|
435
|
+
|
|
339
436
|
## Provider-agnostic tiers (v0.15.0)
|
|
340
437
|
|
|
341
438
|
Small-backlog batch (issues #57/#63/#64/#65):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getpipher/armory-fleet",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The armory suite's subagent orchestrator for the pi coding agent — a cross-harness, superpowers-native fleet where every agent is armory-native from birth.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,3 +31,17 @@ export function withModelFallbackRetry(spawn: SpawnFn, fallback: string | undefi
|
|
|
31
31
|
return first;
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
/** #83 D3: the RPC foreground-semantics detached spawn's retry — mirrors the tool's direct-path
|
|
36
|
+
* contract (retry ONCE, retryable failures only, DISTINCT fallback model) but the retry runs
|
|
37
|
+
* WITHOUT the pre-minted runId: the primary already journaled run:started/ended under it, so
|
|
38
|
+
* the retry must mint a fresh id (a reused id would double-emit run:started). `todoId` links
|
|
39
|
+
* the retry to the primary's armory-todo task (same relink contract as the tool). */
|
|
40
|
+
export async function retryForegroundOnce(
|
|
41
|
+
primary: SpawnResult,
|
|
42
|
+
fallback: string | undefined,
|
|
43
|
+
spawn: (o: { model: string; todoId?: string }) => Promise<SpawnResult>,
|
|
44
|
+
): Promise<SpawnResult> {
|
|
45
|
+
if (!fallback || primary.status !== "failed" || !primary.retryable || fallback === primary.model) return primary;
|
|
46
|
+
return spawn({ model: fallback, todoId: primary.todoId ?? undefined });
|
|
47
|
+
}
|
|
@@ -132,6 +132,10 @@ export interface SpawnOptions {
|
|
|
132
132
|
visionPort?: VisionPort;
|
|
133
133
|
signal?: AbortSignal;
|
|
134
134
|
onEvent?: (e: ChildSessionEvent) => void;
|
|
135
|
+
/** SPEC-6-4: pre-minted runId (RPC spawn replies before the detached spawn resolves). Absent → mint. */
|
|
136
|
+
runId?: string;
|
|
137
|
+
/** SPEC-6-4: dispatch origin for RunMetaEvent.mode. Default "foreground". */
|
|
138
|
+
mode?: "foreground" | "background" | "scheduled" | "workflow";
|
|
135
139
|
/** SPEC-4: when set, this spawn is a lifecycle phase child. It links to this lifecycle todo
|
|
136
140
|
* (not creates a new one) and finishRun skips mark-done/revert — the lifecycle engine owns
|
|
137
141
|
* the lifecycle todo's status + progress block. */
|
|
@@ -154,6 +158,10 @@ export interface SpawnOptions {
|
|
|
154
158
|
modelRegistry?: ModelRegistryLike;
|
|
155
159
|
/** SPEC-6-3: workflow adapter tier override — replaces agent.tier before model resolution. */
|
|
156
160
|
tierOverride?: string;
|
|
161
|
+
/** #78: fleet-wide default thinking level (`fleet.defaultSubagentThinking` settings field).
|
|
162
|
+
* Applied only when the agent frontmatter does NOT pin `thinkingLevel` — precedence:
|
|
163
|
+
* agent.thinkingLevel > opts.defaultThinkingLevel > backend/session default. */
|
|
164
|
+
defaultThinkingLevel?: ThinkingLevel;
|
|
157
165
|
/** #31: when true, the caller asserts this dispatch will NOT mutate the working directory
|
|
158
166
|
* (review/audit/research). Read-only dispatches bypass the foreground single-slot lock so
|
|
159
167
|
* multiple readOnly dispatches — and/or a readOnly dispatch alongside a write dispatch — can
|
|
@@ -241,13 +249,13 @@ export async function spawnSubagent(opts: SpawnOptions): Promise<SpawnResult> {
|
|
|
241
249
|
const readOnly = opts.readOnly ?? false;
|
|
242
250
|
let runId: string;
|
|
243
251
|
if (readOnly) {
|
|
244
|
-
runId = genRunId();
|
|
252
|
+
runId = opts.runId ?? genRunId();
|
|
245
253
|
} else {
|
|
246
254
|
// #31 tail: foreground concurrency lock. cap=1 (default) is FAIL-FAST (backward-compat — a
|
|
247
255
|
// 2nd dispatch is rejected with the held runId + the cap + the env hint); cap>1 (opt-in via
|
|
248
256
|
// ARMORY_FLEET_FOREGROUND_CONCURRENCY) QUEUES — up to `cap` writes run in parallel, the rest wait.
|
|
249
257
|
// The cap is session-level (a shared lock can't be re-sized per dispatch); see concurrency-lock.ts.
|
|
250
|
-
runId = genRunId();
|
|
258
|
+
runId = opts.runId ?? genRunId();
|
|
251
259
|
const acq = await opts.lock.acquire(runId);
|
|
252
260
|
if (!acq.ok) {
|
|
253
261
|
const hint = opts.lock.cap === 1
|
|
@@ -331,7 +339,8 @@ export async function spawnSubagent(opts: SpawnOptions): Promise<SpawnResult> {
|
|
|
331
339
|
const result = await backend.factory.create({
|
|
332
340
|
cwd: childCwd,
|
|
333
341
|
model: cand,
|
|
334
|
-
|
|
342
|
+
// #78: agent frontmatter wins; the fleet-wide default fills the gap.
|
|
343
|
+
thinkingLevel: childAgent.thinkingLevel ?? opts.defaultThinkingLevel,
|
|
335
344
|
tools,
|
|
336
345
|
rolePrompt: childAgent.rolePrompt,
|
|
337
346
|
skills: childAgent.skills ?? [],
|
|
@@ -401,7 +410,7 @@ export async function spawnSubagent(opts: SpawnOptions): Promise<SpawnResult> {
|
|
|
401
410
|
if (e.type === "session_init" && e.backendSessionId) {
|
|
402
411
|
opts.runRegistry.update(runId, { backendSessionId: e.backendSessionId, sessionKey: agentDef.sessionKey });
|
|
403
412
|
try {
|
|
404
|
-
opts.runLog?.append(runId, { type: "run:meta", runId, agent: agentDef.name, model, task: opts.task, startedAt, track, todoId, backendSessionId: e.backendSessionId, sessionKey: agentDef.sessionKey, cwd: childCwd, sessionCwd: opts.parentCwd, pid: (session as { proc?: { pid?: number } }).proc?.pid });
|
|
413
|
+
opts.runLog?.append(runId, { type: "run:meta", runId, agent: agentDef.name, model, task: opts.task, startedAt, track, todoId, backendSessionId: e.backendSessionId, sessionKey: agentDef.sessionKey, cwd: childCwd, sessionCwd: opts.parentCwd, mode: opts.mode ?? "foreground", pid: (session as { proc?: { pid?: number } }).proc?.pid });
|
|
405
414
|
} catch { /* best-effort */ }
|
|
406
415
|
} else if (e.type === "turn_start") {
|
|
407
416
|
turnIdx++;
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
getAgentDir,
|
|
9
9
|
} from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import type { Model } from "@earendil-works/pi-ai";
|
|
11
|
-
import { createSubagentTool, type SubagentToolDeps } from "./tools/subagent.ts";
|
|
11
|
+
import { createSubagentTool, mergeLifecycleSkills, resolveDispatchCwd, type SubagentToolDeps } from "./tools/subagent.ts";
|
|
12
12
|
// SPEC-6-3: /fleet uses openWorkflowPanelLoop (Task 12) instead of the raw openFleetPanel factory.
|
|
13
13
|
import { discoverAgents } from "./registry/discovery.ts";
|
|
14
14
|
import { RunRegistry } from "./engine/run-registry.ts";
|
|
@@ -40,6 +40,8 @@ import { ResultsInbox } from "./runtime/results-inbox.ts";
|
|
|
40
40
|
import { runBackground, type AsyncRunnerDeps } from "./runtime/async-runner.ts";
|
|
41
41
|
import { scanResumeCandidates } from "./runtime/resume.ts";
|
|
42
42
|
import { RunLog } from "./runtime/run-log.ts";
|
|
43
|
+
import { FleetEventBus } from "./rpc/event-bus.ts";
|
|
44
|
+
import { RpcServer } from "./rpc/rpc-server.ts";
|
|
43
45
|
import { reconcileRuns } from "./runtime/reconcile.ts";
|
|
44
46
|
import { Scheduler } from "./scheduling/scheduler.ts";
|
|
45
47
|
import { createFleetResultsTool } from "./tools/fleet-results.ts";
|
|
@@ -50,6 +52,7 @@ import { FleetWidgetController } from "./panel/fleet-widget.ts";
|
|
|
50
52
|
import { TierRegistry, mergeTiers } from "./tiers/tier-registry.ts";
|
|
51
53
|
import { BUILTIN_TIERS } from "./tiers/builtin.ts";
|
|
52
54
|
import { TierStore } from "./tiers/tier-store.ts";
|
|
55
|
+
import { FleetSettingsStore } from "./settings/fleet-settings.ts";
|
|
53
56
|
import { splitModel } from "./tiers/resolve.ts";
|
|
54
57
|
import { WorkflowJournal } from "./workflows/journal.ts";
|
|
55
58
|
import { discoverWorkflows, WorkflowRegistry, type WorkflowDef } from "./workflows/registry.ts";
|
|
@@ -263,6 +266,9 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
263
266
|
// SPEC-5b-2: the live widget (above editor) controller.
|
|
264
267
|
// Display-only, independent of the /fleet panel; constructed per-session in session_start.
|
|
265
268
|
let fleetWidget: FleetWidgetController | null = null;
|
|
269
|
+
// SPEC-6-4: hoisted so session_shutdown can dispose the bus + unsubscribe fleet:rpc.
|
|
270
|
+
let fleetBus: FleetEventBus | null = null;
|
|
271
|
+
let unsubscribeRpc: (() => void) | null = null;
|
|
266
272
|
// SPEC-6-3: hoisted so /fleet command handler + session_shutdown can reach them.
|
|
267
273
|
let wfController: WorkflowController | null = null;
|
|
268
274
|
let wfStore: WorkflowRunStore | null = null;
|
|
@@ -289,6 +295,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
289
295
|
...(isolated ? { artifactDiscovery: ({ finalText, cwd, baseRef }: { finalText: string; cwd: string; baseRef: string }) => (deps.asyncRunner as AsyncRunnerDeps).diff.diffPhase(cwd, baseRef, finalText) } : {}),
|
|
290
296
|
spawn: withModelFallbackRetry(async (o) => spawnSubagent({
|
|
291
297
|
agent: o.agent, task: o.task, lifecycleTodoId: o.lifecycleTodoId, model: o.model,
|
|
298
|
+
mode: opts.fleetMode ?? "background",
|
|
292
299
|
skillsOverride: o.skills, backendOverride: o.backend,
|
|
293
300
|
registry: deps.registry, todoSync: deps.todoSync, runRegistry: deps.runRegistry, lock: bgLock,
|
|
294
301
|
backendRegistry: deps.backendRegistry, parentModel: deps.parentModel,
|
|
@@ -298,7 +305,8 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
298
305
|
// in-place runs so the cross-cwd surfacing (↗ glyph + sessionCwd audit) still fires.
|
|
299
306
|
cwd: isolated ? opts.worktreePath : o.cwd,
|
|
300
307
|
runLog: deps.runLog, tierRegistry: deps.tierRegistry, modelRegistry: deps.modelRegistry,
|
|
301
|
-
|
|
308
|
+
defaultThinkingLevel: deps.defaultSubagentThinking,
|
|
309
|
+
}), opts.modelFallback ?? deps.defaultModelFallback),
|
|
302
310
|
};
|
|
303
311
|
const res = await runLifecycle(task, lifecycleName, { deps: lifecycleFullDeps, mode: opts.mode, worktreePath: opts.worktreePath, baseRef: "HEAD", entryCwd: opts.entryCwd, onCheckpoint: async (p) => p.status === "failed" ? { action: "abort" } : { action: "continue" } });
|
|
304
312
|
return res as unknown as import("./runtime/async-runner.ts").FakeLifecycleResult;
|
|
@@ -359,6 +367,9 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
359
367
|
// SPEC-5b-1: per-session RunLog at .pi/fleet/conversations/ (separate from the
|
|
360
368
|
// SPEC-5a phase journal at .pi/fleet/runs/ — different granularity, no filename collision).
|
|
361
369
|
deps.runLog = new RunLog(join(dir, "conversations"));
|
|
370
|
+
// SPEC-6-4: ONE shared RunJournal per session — the FleetEventBus subscribes to THIS
|
|
371
|
+
// instance, so every journal append (phases AND bookends) reaches the bus exactly once.
|
|
372
|
+
const fleetJournal = new RunJournal(join(dir, "runs"));
|
|
362
373
|
// v0.10.2: pass the in-memory RunRegistry so reconcile syncs it too — otherwise orphaned
|
|
363
374
|
// (process-gone) runs keep status:"running" in memory and the live widget shows a stale ▶ forever.
|
|
364
375
|
// #22 bg-watchdog: pass todoSync so a process-gone run's linked TODO is reverted to open
|
|
@@ -378,7 +389,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
378
389
|
// service would create it from the session's repo — the #62 gap). Same-cwd → shared.
|
|
379
390
|
worktreeFor: (cwd: string) => (cwd === ctx.cwd ? sessionWorktree : new WorktreeService({ rootDir: cwd })),
|
|
380
391
|
diff: new DiffService(),
|
|
381
|
-
journal:
|
|
392
|
+
journal: fleetJournal,
|
|
382
393
|
pool: new ConcurrencyPool(3),
|
|
383
394
|
inbox: resultsInbox,
|
|
384
395
|
runLifecycle: asyncRunLifecycle,
|
|
@@ -392,7 +403,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
392
403
|
lockPath: join(dir, "schedules.lock"),
|
|
393
404
|
onFire: (spec) => {
|
|
394
405
|
if (!deps.asyncRunner) return;
|
|
395
|
-
runBackground(spec.task, { deps: deps.asyncRunner, lifecycle: spec.lifecycle ?? "default", mode: spec.auto ? "auto" : "checkpointed", isolation: spec.isolation, cwd: spec.cwd });
|
|
406
|
+
runBackground(spec.task, { deps: deps.asyncRunner, lifecycle: spec.lifecycle ?? "default", mode: spec.auto ? "auto" : "checkpointed", isolation: spec.isolation, cwd: spec.cwd, origin: "scheduled" });
|
|
396
407
|
},
|
|
397
408
|
});
|
|
398
409
|
deps.scheduler.start();
|
|
@@ -400,6 +411,159 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
400
411
|
if (cands.length > 0) {
|
|
401
412
|
ctx.ui.notify(`${cands.length} interrupted fleet run${cands.length > 1 ? "s" : ""} — open /fleet to resume`, "info");
|
|
402
413
|
}
|
|
414
|
+
// SPEC-6-4: defensive re-entrancy — a re-entered session_start must dispose the old
|
|
415
|
+
// bus/listener BEFORE constructing new ones (dispose-after-construction kills the fresh
|
|
416
|
+
// bus at birth and orphans the previous session's subscriptions).
|
|
417
|
+
fleetBus?.dispose();
|
|
418
|
+
fleetBus = null;
|
|
419
|
+
unsubscribeRpc?.();
|
|
420
|
+
unsubscribeRpc = null;
|
|
421
|
+
// SPEC-6-4: FleetEventBus — store appends → public fleet:* events on pi.events.
|
|
422
|
+
fleetBus = new FleetEventBus({
|
|
423
|
+
runLog: deps.runLog,
|
|
424
|
+
journal: fleetJournal,
|
|
425
|
+
emit: (channel, payload) => { pi.events.emit(channel, payload); },
|
|
426
|
+
});
|
|
427
|
+
// SPEC-6-4: fleet:rpc request surface — replies on fleet:rpc:result (null replies dropped).
|
|
428
|
+
const rpcServer = new RpcServer({
|
|
429
|
+
runRegistry: deps.runRegistry,
|
|
430
|
+
runLog: deps.runLog,
|
|
431
|
+
journal: fleetJournal,
|
|
432
|
+
parentCwd: ctx.cwd,
|
|
433
|
+
hasAsyncRunner: true,
|
|
434
|
+
// #83 D4: schedule registration — scheduler.register + nextFire lookup. Throws (invalid
|
|
435
|
+
// expression) propagate to the verb, which maps them to E-BAD-PARAMS.
|
|
436
|
+
schedule: (spec) => {
|
|
437
|
+
if (!deps.scheduler) throw new Error("scheduling not configured in this session (scheduler missing)");
|
|
438
|
+
const id = deps.scheduler.register({
|
|
439
|
+
task: String(spec.task),
|
|
440
|
+
expression: String(spec.expression),
|
|
441
|
+
...(typeof spec.lifecycle === "string" ? { lifecycle: spec.lifecycle } : {}),
|
|
442
|
+
...(typeof spec.auto === "boolean" ? { auto: spec.auto } : {}),
|
|
443
|
+
...(spec.isolation === "worktree" || spec.isolation === "none" || spec.isolation === "auto" ? { isolation: spec.isolation } : {}),
|
|
444
|
+
...(typeof spec.cwd === "string" ? { cwd: spec.cwd } : {}),
|
|
445
|
+
});
|
|
446
|
+
const entry = deps.scheduler.list().find((s) => s.id === id);
|
|
447
|
+
return { scheduleId: id, nextFire: entry?.nextFire?.toISOString() ?? null };
|
|
448
|
+
},
|
|
449
|
+
spawn: (params: Record<string, unknown>, runId: string) => {
|
|
450
|
+
// Detached fire-and-forget: NEVER throws — spawnSubagent journals its own fail path
|
|
451
|
+
// (run:ended + todo revert), so the caller's pre-minted runId always resolves to events.
|
|
452
|
+
void (async () => {
|
|
453
|
+
const requestedCwd = typeof params.cwd === "string" ? params.cwd : undefined;
|
|
454
|
+
const { cwd: resolvedCwd } = resolveDispatchCwd(requestedCwd, ctx.cwd);
|
|
455
|
+
// #83: lifecycle + per-request modelFallback params (validated by the verb; re-narrowed here).
|
|
456
|
+
const rpcLifecycle = typeof params.lifecycle === "string" && params.lifecycle ? params.lifecycle : "default";
|
|
457
|
+
const rpcFallback = typeof params.modelFallback === "string" && params.modelFallback ? params.modelFallback : undefined;
|
|
458
|
+
if (params.background === true) {
|
|
459
|
+
// RPC background: routed through the async runner (pool slot + isolation + origin),
|
|
460
|
+
// identical to the tool's runBackground path (spec §3.2). Ghost-runId prevention:
|
|
461
|
+
// a synchronous pre-flight failure journals run:ended failed under the caller's id.
|
|
462
|
+
const handle = runBackground(String(params.task), {
|
|
463
|
+
deps: deps.asyncRunner!,
|
|
464
|
+
lifecycle: rpcLifecycle,
|
|
465
|
+
mode: "auto",
|
|
466
|
+
isolation: params.isolation as "worktree" | "none" | "auto" | undefined,
|
|
467
|
+
cwd: resolvedCwd ?? ctx.cwd,
|
|
468
|
+
origin: "background",
|
|
469
|
+
runId,
|
|
470
|
+
...(rpcFallback ? { modelFallback: rpcFallback } : {}),
|
|
471
|
+
});
|
|
472
|
+
if (handle.status === "failed") {
|
|
473
|
+
deps.runLog?.append(runId, { type: "run:ended", runId, status: "failed", endedAt: Date.now(), tokenTotal: 0, error: handle.error });
|
|
474
|
+
}
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (params.lifecycle !== undefined) {
|
|
478
|
+
// #83 D2: lifecycle WITHOUT background = detached foreground-semantics lifecycle run
|
|
479
|
+
// (tool parity: mode "auto", failed-phase checkpoint aborts, session lock on the phase
|
|
480
|
+
// spawn, pre-minted runId via the genRunId override — same pattern as asyncRunLifecycle).
|
|
481
|
+
const { runLifecycle } = await import("./lifecycle/run-lifecycle.ts");
|
|
482
|
+
const { spawnSubagent } = await import("./engine/spawnSubagent.ts");
|
|
483
|
+
const { withModelFallbackRetry } = await import("./engine/retry-fallback.ts");
|
|
484
|
+
const lifecycleFullDeps = {
|
|
485
|
+
...deps.lifecycleDeps,
|
|
486
|
+
genRunId: () => runId,
|
|
487
|
+
spawn: withModelFallbackRetry(async (o) => spawnSubagent({
|
|
488
|
+
agent: o.agent, task: o.task, lifecycleTodoId: o.lifecycleTodoId, model: o.model,
|
|
489
|
+
skillsOverride: mergeLifecycleSkills(o.skills, Array.isArray(params.skills) ? params.skills as string[] : undefined),
|
|
490
|
+
backendOverride: o.backend,
|
|
491
|
+
registry: deps.registry, todoSync: deps.todoSync, runRegistry: deps.runRegistry, lock: deps.lock,
|
|
492
|
+
backendRegistry: deps.backendRegistry, parentModel: deps.parentModel, parentCwd: ctx.cwd,
|
|
493
|
+
runLog: deps.runLog, signal: undefined,
|
|
494
|
+
tierRegistry: deps.tierRegistry, modelRegistry: deps.modelRegistry,
|
|
495
|
+
defaultThinkingLevel: deps.defaultSubagentThinking,
|
|
496
|
+
maxTurns: typeof params.maxTurns === "number" ? params.maxTurns : undefined,
|
|
497
|
+
readOnly: params.readOnly === true,
|
|
498
|
+
cwd: o.cwd ?? resolvedCwd,
|
|
499
|
+
}), rpcFallback ?? deps.defaultModelFallback),
|
|
500
|
+
};
|
|
501
|
+
const res = await runLifecycle(String(params.task), rpcLifecycle, {
|
|
502
|
+
deps: lifecycleFullDeps, mode: "auto", entryCwd: resolvedCwd,
|
|
503
|
+
onCheckpoint: async (p) => p.status === "failed" ? { action: "abort" } : { action: "continue" },
|
|
504
|
+
});
|
|
505
|
+
deps.lifecycleRuns.set(res.runId, res); // panel Lifecycle-view visibility (workflow-lambda precedent)
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
// Foreground-semantics detached spawn (no mode — "foreground" is the truth).
|
|
509
|
+
const { spawnSubagent } = await import("./engine/spawnSubagent.ts");
|
|
510
|
+
const { retryForegroundOnce } = await import("./engine/retry-fallback.ts");
|
|
511
|
+
const primary = await spawnSubagent({
|
|
512
|
+
agent: params.agent as string,
|
|
513
|
+
task: params.task as string,
|
|
514
|
+
todoId: typeof params.todoId === "string" ? params.todoId : undefined,
|
|
515
|
+
track: params.track === true,
|
|
516
|
+
model: typeof params.model === "string" ? params.model : undefined,
|
|
517
|
+
readOnly: params.readOnly === true,
|
|
518
|
+
skillsOverride: Array.isArray(params.skills) ? params.skills as string[] : undefined,
|
|
519
|
+
registry: deps.registry,
|
|
520
|
+
todoSync: deps.todoSync,
|
|
521
|
+
runRegistry: deps.runRegistry,
|
|
522
|
+
lock: deps.lock,
|
|
523
|
+
backendRegistry: deps.backendRegistry,
|
|
524
|
+
parentModel: deps.parentModel,
|
|
525
|
+
parentCwd: ctx.cwd,
|
|
526
|
+
runLog: deps.runLog,
|
|
527
|
+
maxTurns: typeof params.maxTurns === "number" ? params.maxTurns : undefined,
|
|
528
|
+
tierRegistry: deps.tierRegistry,
|
|
529
|
+
modelRegistry: deps.modelRegistry,
|
|
530
|
+
cwd: resolvedCwd ?? ctx.cwd,
|
|
531
|
+
runId,
|
|
532
|
+
defaultThinkingLevel: deps.defaultSubagentThinking,
|
|
533
|
+
});
|
|
534
|
+
// #83 D3: per-request fallback retry — the retry mints a FRESH runId (the primary's
|
|
535
|
+
// pre-minted id stays retired; a reuse would double-emit run:started) and relinks the
|
|
536
|
+
// primary's todo, exactly like the tool's direct path.
|
|
537
|
+
await retryForegroundOnce(primary, rpcFallback, (o) => spawnSubagent({
|
|
538
|
+
agent: params.agent as string,
|
|
539
|
+
task: params.task as string,
|
|
540
|
+
todoId: o.todoId,
|
|
541
|
+
track: params.track === true,
|
|
542
|
+
model: o.model,
|
|
543
|
+
readOnly: params.readOnly === true,
|
|
544
|
+
skillsOverride: Array.isArray(params.skills) ? params.skills as string[] : undefined,
|
|
545
|
+
registry: deps.registry,
|
|
546
|
+
todoSync: deps.todoSync,
|
|
547
|
+
runRegistry: deps.runRegistry,
|
|
548
|
+
lock: deps.lock,
|
|
549
|
+
backendRegistry: deps.backendRegistry,
|
|
550
|
+
parentModel: deps.parentModel,
|
|
551
|
+
parentCwd: ctx.cwd,
|
|
552
|
+
runLog: deps.runLog,
|
|
553
|
+
maxTurns: typeof params.maxTurns === "number" ? params.maxTurns : undefined,
|
|
554
|
+
tierRegistry: deps.tierRegistry,
|
|
555
|
+
modelRegistry: deps.modelRegistry,
|
|
556
|
+
cwd: resolvedCwd ?? ctx.cwd,
|
|
557
|
+
defaultThinkingLevel: deps.defaultSubagentThinking,
|
|
558
|
+
}));
|
|
559
|
+
})().catch(() => {});
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
unsubscribeRpc = pi.events.on("fleet:rpc", (data: unknown) => {
|
|
563
|
+
void rpcServer.handle(data).then((reply) => {
|
|
564
|
+
if (reply) pi.events.emit("fleet:rpc:result", reply);
|
|
565
|
+
});
|
|
566
|
+
});
|
|
403
567
|
// SPEC-5b-2: live widget (above editor). Display-only, independent
|
|
404
568
|
// of the /fleet panel. getTheme is a live getter (EditorTheme gotcha). Disposed on session end.
|
|
405
569
|
const getModelContextWindow = (m: string): number | undefined => {
|
|
@@ -436,6 +600,16 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
436
600
|
deps.reloadTiers = reloadTiers;
|
|
437
601
|
reloadTiers(); // build the real merged registry (replaces the builtin-only placeholder)
|
|
438
602
|
|
|
603
|
+
// #78: fleet-dir settings.json (global + project, project wins) — currently carries
|
|
604
|
+
// `defaultSubagentThinking`. Warnings are surfaced, never swallowed silently.
|
|
605
|
+
const fleetSettingsStore = new FleetSettingsStore({
|
|
606
|
+
projectPath: join(dir, "settings.json"),
|
|
607
|
+
globalPath: join(process.env.HOME ?? "", ".pi", "agent", "fleet", "settings.json"),
|
|
608
|
+
});
|
|
609
|
+
const fleetSettings = fleetSettingsStore.load();
|
|
610
|
+
for (const w of fleetSettings.warnings) ctx.ui.notify(w, "warning");
|
|
611
|
+
deps.defaultSubagentThinking = fleetSettings.settings.defaultSubagentThinking;
|
|
612
|
+
|
|
439
613
|
// SPEC-6-3: workflow journal + registry + runner + fleet tool wiring.
|
|
440
614
|
const workflowJournal = new WorkflowJournal(join(dir, "workflows"));
|
|
441
615
|
wfRegistry = new WorkflowRegistry(discoverWorkflows({
|
|
@@ -458,6 +632,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
458
632
|
runLog: deps.runLog,
|
|
459
633
|
tierRegistry: deps.tierRegistry,
|
|
460
634
|
modelRegistry: deps.modelRegistry,
|
|
635
|
+
defaultThinkingLevel: deps.defaultSubagentThinking,
|
|
461
636
|
lifecycleDeps: deps.lifecycleDeps,
|
|
462
637
|
spawnSubagentFn: async (opts) => { const { spawnSubagent } = await import("./engine/spawnSubagent.ts"); return spawnSubagent(opts); },
|
|
463
638
|
runLifecycleFn: async (task, name, lcOpts) => { const { runLifecycle } = await import("./lifecycle/run-lifecycle.ts"); return runLifecycle(task, name, lcOpts); },
|
|
@@ -501,6 +676,11 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
501
676
|
});
|
|
502
677
|
|
|
503
678
|
pi.on("session_shutdown", () => {
|
|
679
|
+
// SPEC-6-4: dispose the event bus + unsubscribe fleet:rpc before the other resets.
|
|
680
|
+
fleetBus?.dispose();
|
|
681
|
+
fleetBus = null;
|
|
682
|
+
unsubscribeRpc?.();
|
|
683
|
+
unsubscribeRpc = null;
|
|
504
684
|
if (fleetWidget) { fleetWidget.dispose(); fleetWidget = null; }
|
|
505
685
|
// SPEC-6-3: abort in-flight workflow children via the session-wide adapter signal.
|
|
506
686
|
// Terminal runs are not re-journaled — only non-terminal spawns observe the abort.
|
|
@@ -582,10 +762,12 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
582
762
|
...deps.lifecycleDeps,
|
|
583
763
|
spawn: withModelFallbackRetry(async (o) => spawnSubagent({
|
|
584
764
|
agent: o.agent, task: o.task, lifecycleTodoId: o.lifecycleTodoId, model: o.model,
|
|
765
|
+
mode: "workflow",
|
|
585
766
|
skillsOverride: o.skills, backendOverride: o.backend,
|
|
586
767
|
registry: deps.registry, todoSync: deps.todoSync, runRegistry: deps.runRegistry, lock: deps.lock,
|
|
587
768
|
backendRegistry: deps.backendRegistry, parentModel: deps.parentModel, parentCwd: deps.parentCwd,
|
|
588
769
|
tierRegistry: deps.tierRegistry, modelRegistry: deps.modelRegistry, // SPEC-6-1
|
|
770
|
+
defaultThinkingLevel: deps.defaultSubagentThinking,
|
|
589
771
|
}), deps.defaultModelFallback),
|
|
590
772
|
};
|
|
591
773
|
const res = await runLifecycle(parsed.task, lcName, { deps: lifecycleFullDeps, mode: parsed.auto ? "auto" : "checkpointed", onCheckpoint });
|
package/src/panel/fleet-panel.ts
CHANGED
|
@@ -9,13 +9,14 @@ import {
|
|
|
9
9
|
matchesKey,
|
|
10
10
|
type SelectItem,
|
|
11
11
|
} from "@earendil-works/pi-tui";
|
|
12
|
-
import type { AgentDef } from "../registry/frontmatter.ts";
|
|
12
|
+
import type { AgentDef, ThinkingLevel } from "../registry/frontmatter.ts";
|
|
13
13
|
import { agentsRow, agentInfo, backendsRow, backendInfo, lifecycleRow, lifecyclePhaseTimeline, scheduleRow } from "./rows.ts";
|
|
14
14
|
import { buildFleetItems } from "./fleet-items.ts";
|
|
15
15
|
import { runsRow, runTimelineRow } from "./runs-rows.ts";
|
|
16
16
|
import { messageBody, toolBody, messageHeader, toolHeader } from "./conversation-rows.ts";
|
|
17
17
|
import { buildRunsIndex } from "./runs-index.ts";
|
|
18
18
|
import type { RunLog, RunMeta, RunLogEvent, MessageEvent, ToolEvent } from "../runtime/run-log.ts";
|
|
19
|
+
import { LiveTimelineState } from "./live-timeline.ts";
|
|
19
20
|
import type { Scheduler, Schedule } from "../scheduling/scheduler.ts";
|
|
20
21
|
import type { BgRunsStore } from "./bg-runs-store.ts";
|
|
21
22
|
import { spawnSubagent, type SpawnResult } from "../engine/spawnSubagent.ts";
|
|
@@ -45,6 +46,9 @@ export interface FleetPanelDeps {
|
|
|
45
46
|
backendRegistry: BackendRegistry; // SPEC-3: replaces childFactory
|
|
46
47
|
parentModel: { provider: string; id: string };
|
|
47
48
|
parentCwd: string;
|
|
49
|
+
/** #78: fleet-wide default thinking level (settings.json `defaultSubagentThinking`).
|
|
50
|
+
* Threads to every panel-driven spawn (Run/Resume/Fork + lifecycle-view phases). */
|
|
51
|
+
defaultSubagentThinking?: ThinkingLevel;
|
|
48
52
|
/** SPEC-4: lifecycle registry + active/recent run records + deps to drive checkpoints. */
|
|
49
53
|
lifecycleRegistry: Map<string, LifecycleDef>;
|
|
50
54
|
lifecycleRuns: Map<string, LifecycleRunRecord>;
|
|
@@ -132,6 +136,43 @@ export class FleetPanel extends Container {
|
|
|
132
136
|
* so the panel re-renders the moment a (fore- or back-ground) run mutates, without a keypress. */
|
|
133
137
|
private readonly unsubs: (() => void)[] = [];
|
|
134
138
|
private closed = false; // SPEC-5a proper-fix: guard against double-close calling onDone twice
|
|
139
|
+
// SPEC-6-4: live mode for the timeline overlay (running runs stream; finished runs replay).
|
|
140
|
+
private liveUnsub: (() => void) | null = null;
|
|
141
|
+
private liveState: LiveTimelineState | null = null;
|
|
142
|
+
|
|
143
|
+
private renderedTimelineCount(): number {
|
|
144
|
+
return (this.runTimeline ?? []).filter((e) => e.type === "message" || e.type === "tool").length;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** SPEC-6-4: release the live subscription (overlay close / different run / panel close). */
|
|
148
|
+
private releaseLiveTimeline(): void {
|
|
149
|
+
this.liveUnsub?.();
|
|
150
|
+
this.liveUnsub = null;
|
|
151
|
+
this.liveState = null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** SPEC-6-4: shared timeline-open (Runs view + gate evidence). Hydrates replay; LIVE when the
|
|
155
|
+
* run is still running — subscribes to appends and tail-follows via LiveTimelineState. */
|
|
156
|
+
private openRunTimeline(runId: string): void {
|
|
157
|
+
const runLog = this.deps.runLog;
|
|
158
|
+
if (!runLog) return;
|
|
159
|
+
const meta = buildRunsIndex(runLog.dir).find((r) => r.runId === runId) ?? null;
|
|
160
|
+
this.selectedRun = meta;
|
|
161
|
+
this.runTimeline = runLog.replay(runId);
|
|
162
|
+
this.releaseLiveTimeline();
|
|
163
|
+
if (meta?.status === "running") {
|
|
164
|
+
this.liveState = new LiveTimelineState();
|
|
165
|
+
this.liveState.index = Math.max(0, this.renderedTimelineCount() - 1);
|
|
166
|
+
this.liveUnsub = runLog.subscribe((rid, ev) => {
|
|
167
|
+
if (rid !== runId || (ev.type !== "message" && ev.type !== "tool")) return;
|
|
168
|
+
this.runTimeline = [...(this.runTimeline ?? []), ev];
|
|
169
|
+
const idx = this.liveState?.append(this.renderedTimelineCount());
|
|
170
|
+
this.selectedEventIndex = idx ?? null;
|
|
171
|
+
this.renderShell();
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
this.renderShell();
|
|
175
|
+
}
|
|
135
176
|
// SPEC-6-3: Workflows view — inline Run prompt input state
|
|
136
177
|
private wfRunMode = false;
|
|
137
178
|
private wfPromptInput: Input | null = null;
|
|
@@ -205,6 +246,8 @@ export class FleetPanel extends Container {
|
|
|
205
246
|
this.closed = true;
|
|
206
247
|
for (const u of this.unsubs) u();
|
|
207
248
|
this.unsubs.length = 0;
|
|
249
|
+
// SPEC-6-4: the live timeline subscription dies with the panel.
|
|
250
|
+
this.releaseLiveTimeline();
|
|
208
251
|
this.onDone(intent ?? null);
|
|
209
252
|
}
|
|
210
253
|
|
|
@@ -324,7 +367,7 @@ export class FleetPanel extends Container {
|
|
|
324
367
|
this.selectedEventIndex = null;
|
|
325
368
|
}
|
|
326
369
|
// esc → back to Runs list (replaces the v0.6.0 panel-level escape catch).
|
|
327
|
-
tl.onCancel = () => { this.selectedRun = null; this.runTimeline = null; this.timelineList = null; this.renderShell(); };
|
|
370
|
+
tl.onCancel = () => { this.releaseLiveTimeline(); this.selectedRun = null; this.runTimeline = null; this.timelineList = null; this.renderShell(); };
|
|
328
371
|
this.timelineList = tl;
|
|
329
372
|
this.addChild(tl);
|
|
330
373
|
}
|
|
@@ -443,6 +486,7 @@ export class FleetPanel extends Container {
|
|
|
443
486
|
backendRegistry: this.deps.backendRegistry,
|
|
444
487
|
parentModel: this.deps.parentModel, parentCwd: this.deps.parentCwd,
|
|
445
488
|
runLog: this.deps.runLog,
|
|
489
|
+
defaultThinkingLevel: this.deps.defaultSubagentThinking,
|
|
446
490
|
// live Fleet row during the run (SPEC-1 §4c) — re-render on each turn_end
|
|
447
491
|
onEvent: (e) => {
|
|
448
492
|
if (e.type === "turn_end") {
|
|
@@ -512,8 +556,7 @@ export class FleetPanel extends Container {
|
|
|
512
556
|
.flatMap((p) => p.gateResults ?? [])
|
|
513
557
|
.find((gr) => gr.runId);
|
|
514
558
|
if (agentGate?.runId && this.deps.runLog) {
|
|
515
|
-
this.
|
|
516
|
-
this.runTimeline = this.deps.runLog.replay(agentGate.runId);
|
|
559
|
+
this.openRunTimeline(agentGate.runId);
|
|
517
560
|
this.selectedLifecycle = null;
|
|
518
561
|
this.view = "runs";
|
|
519
562
|
this.renderShell();
|
|
@@ -555,8 +598,17 @@ export class FleetPanel extends Container {
|
|
|
555
598
|
return;
|
|
556
599
|
}
|
|
557
600
|
if (this.selectedRun) {
|
|
558
|
-
// SPEC-
|
|
559
|
-
//
|
|
601
|
+
// SPEC-6-4: in live mode the panel owns up/down (tail-follow cursor) — intercepted before
|
|
602
|
+
// the SelectList forward; everything else forwards as before. Finished runs: replay path.
|
|
603
|
+
if (this.liveState && (matchesKey(data, "up") || matchesKey(data, "down"))) {
|
|
604
|
+
const total = this.renderedTimelineCount();
|
|
605
|
+
const key = matchesKey(data, "up") ? "up" : "down";
|
|
606
|
+
if (this.liveState.onKey(key, total)) {
|
|
607
|
+
this.selectedEventIndex = this.liveState.index;
|
|
608
|
+
this.renderShell();
|
|
609
|
+
}
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
560
612
|
this.timelineList?.handleInput(data);
|
|
561
613
|
this.invalidate();
|
|
562
614
|
return;
|
|
@@ -645,9 +697,7 @@ export class FleetPanel extends Container {
|
|
|
645
697
|
if (matchesKey(data, "enter") || matchesKey(data, "i")) {
|
|
646
698
|
const sel = this.list.getSelectedItem();
|
|
647
699
|
if (sel) {
|
|
648
|
-
this.
|
|
649
|
-
this.runTimeline = this.deps.runLog.replay(sel.value);
|
|
650
|
-
this.renderShell();
|
|
700
|
+
this.openRunTimeline(sel.value);
|
|
651
701
|
}
|
|
652
702
|
return;
|
|
653
703
|
}
|
|
@@ -1071,6 +1121,7 @@ export class FleetPanel extends Container {
|
|
|
1071
1121
|
registry: this.deps.registry, todoSync: this.deps.todoSync, runRegistry: this.deps.runRegistry,
|
|
1072
1122
|
lock: this.deps.lock, backendRegistry: this.deps.backendRegistry,
|
|
1073
1123
|
parentModel: this.deps.parentModel, parentCwd: this.deps.parentCwd,
|
|
1124
|
+
defaultThinkingLevel: this.deps.defaultSubagentThinking,
|
|
1074
1125
|
onEvent: (e) => { if (e.type === "turn_end") { this.list = this.buildList(); this.renderShell(); } },
|
|
1075
1126
|
});
|
|
1076
1127
|
this.list = this.buildList();
|
|
@@ -1110,6 +1161,7 @@ export class FleetPanel extends Container {
|
|
|
1110
1161
|
registry: this.deps.registry, todoSync: this.deps.todoSync, runRegistry: this.deps.runRegistry,
|
|
1111
1162
|
lock: this.deps.lock, backendRegistry: this.deps.backendRegistry,
|
|
1112
1163
|
parentModel: this.deps.parentModel, parentCwd: this.deps.parentCwd,
|
|
1164
|
+
defaultThinkingLevel: this.deps.defaultSubagentThinking,
|
|
1113
1165
|
onEvent: (e) => { if (e.type === "turn_end") { this.list = this.buildList(); this.renderShell(); } },
|
|
1114
1166
|
});
|
|
1115
1167
|
this.list = this.buildList();
|
|
@@ -1182,6 +1234,7 @@ export class FleetPanel extends Container {
|
|
|
1182
1234
|
registry: this.deps.registry, todoSync: this.deps.todoSync, runRegistry: this.deps.runRegistry, lock: this.deps.lock,
|
|
1183
1235
|
backendRegistry: this.deps.backendRegistry, parentModel: this.deps.parentModel, parentCwd: this.deps.parentCwd,
|
|
1184
1236
|
runLog: this.deps.runLog,
|
|
1237
|
+
defaultThinkingLevel: this.deps.defaultSubagentThinking,
|
|
1185
1238
|
cwd: o.cwd,
|
|
1186
1239
|
});
|
|
1187
1240
|
},
|