@signalridge/pi-workflows 1.11.1 → 1.12.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/CHANGELOG.md +16 -0
- package/README.md +7 -1
- package/package.json +2 -2
- package/skills/workflow-authoring/SKILL.md +1 -1
- package/skills/workflow-authoring/references/capabilities.md +1 -1
- package/skills/workflow-authoring/references/capability-details.md +1 -1
- package/skills/workflow-authoring/references/runtime.md +3 -1
- package/skills/workflow-patterns/SKILL.md +1 -1
- package/skills/workflow-review/SKILL.md +1 -1
- package/src/engine.ts +63 -9
- package/src/errors.ts +14 -0
- package/src/index.ts +21 -16
- package/src/journal.ts +13 -4
- package/src/rpc-client.ts +33 -20
- package/src/runtime.ts +58 -29
- package/src/ui-overview.ts +97 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.12.0
|
|
4
|
+
### Minor Changes
|
|
5
|
+
|
|
6
|
+
- 3096585: Give Workflows one rich below-editor overview and keep Subagents Fleet limited to independent agents. Preserve workflow navigation, completed-call token semantics, periodic projection/footer updates, and exact retained-child access through the owner navigator and explicit `/agents` retained-child paths, including when Fleet is disabled. No execution/model policy or managed/UI protocol changes.
|
|
7
|
+
|
|
8
|
+
Update both packages together for the new layout: older Subagents can still duplicate the new overview; newer Subagents paired with older Workflows leaves the older upper progress panel. Existing running sessions do not acquire these UI changes until a later normal activation.
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- 3096585: Do not charge managed-request preflight rejections as submissions, and finalize schema-invalid logical calls with their exact settled attempt metadata. Count all owner-side workflows in the passive overview without expanding the bounded peer snapshot.
|
|
13
|
+
- 3096585: Validate complete managed requests before allocating dispatch leases, preserving local validation diagnostics and completed resume prefixes. Apply structured-output constraints conjunctively and extract balanced JSON from large malformed replies in linear time without changing bounded repair behavior.
|
|
14
|
+
- 3096585: Validate persisted transcript pointers with the managed protocol contract and settle logical calls when a token budget prevents a repair attempt, preserving the completed child's usage and ownership on restore.
|
|
15
|
+
- Updated dependencies [3096585]
|
|
16
|
+
- Updated dependencies [3096585]
|
|
17
|
+
- @signalridge/pi-ui@1.5.0
|
|
18
|
+
|
|
3
19
|
## 1.11.1
|
|
4
20
|
### Patch Changes
|
|
5
21
|
|
package/README.md
CHANGED
|
@@ -265,7 +265,13 @@ Phase entry, task attempts and call attribution come from fenced runtime observa
|
|
|
265
265
|
|
|
266
266
|
A selected root agent call can open its retained conversation through the subagent owner's optional exact-attempt UI endpoint. Unknown or reused old attempts are not redirected to a newer child. Workflow controls remain here; steering, child stop, sessions, model policy and foreground handoff remain in `pi-subagents`. The optional UI protocol does not change the required managed v4 handshake.
|
|
267
267
|
|
|
268
|
-
|
|
268
|
+
Workflows owns one rich passive overview **below the editor**; there is no second upper workflow list. Rows put the workflow name first, followed by semantic status, the observed current phase, running/total call counts, done/failed counts, elapsed time and available **settled-call tokens**. Tokens are completed call-result totals, not live in-flight usage; unknown is omitted and an explicit zero stays zero. Paused, stopping and childless runs remain visible; recent terminal runs linger for four seconds, while all retained history stays in `/workflows`.
|
|
269
|
+
|
|
270
|
+
`/workflows-progress compact|detailed|status|max <N>` retains its settings: `status` reports them, `detailed` adds bounded call rows, and `max` caps shown runs/calls. The panel uses a terminal-height row budget, prioritizes live calls and reports overflow rather than growing as runs × calls. Names/results are sanitized only in display copies; journals and stored source remain unchanged. The panel takes no empty-prompt arrow keys: `/workflows` is the authoritative navigator. Closing or disposing presentation never stops execution. Fullscreen, regular terminal, RPC and print paths retain their distinct public API boundaries.
|
|
271
|
+
|
|
272
|
+
Subagents Fleet lists independent agents only. For retained children, use a selected root call in `/workflows`, or `/agents → Retained workflow children` to open the owner inspector or directly inspect the bounded child list. The latter also works when the workflow UI peer is absent or Fleet is disabled. `/agents → Agent runs` retains individual sessions and child steering/stop controls; eligible independent foreground agents keep handoff. Workflow controls and model/execution policy do not move across owners. The optional validated UI query/change/open bridge remains available to external observers and is not a usage RPC.
|
|
273
|
+
|
|
274
|
+
**Mixed-version UI caveat:** update both packages together for this ownership layout. An older Subagents may still render its own workflow overview alongside the new panel. A newer Subagents with older Workflows hides owned Fleet rows but leaves the older upper progress list in place. Managed protocol v4 and exact-attempt conversation admission are unchanged; no untyped event guessing or automatic legacy-peer suppression is used.
|
|
269
275
|
|
|
270
276
|
### Background result notifications
|
|
271
277
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signalridge/pi-workflows",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "Deterministic JavaScript orchestration workflows backed by pi-subagents managed spawning.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@signalridge/pi-subagents-protocol": "^1.6.0",
|
|
26
|
-
"@signalridge/pi-ui": "^1.
|
|
26
|
+
"@signalridge/pi-ui": "^1.5.0",
|
|
27
27
|
"acorn": "^8.16.0"
|
|
28
28
|
},
|
|
29
29
|
"pi": {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: workflow-authoring
|
|
3
3
|
description: Design, write, review, and debug Pi JavaScript workflows that coordinate multiple subagents. Use when a request needs named dependencies, bounded fan-out, per-item pipelines, quality gates, saved or nested workflows, pause/resume, or workflow progress; do not use it merely to run an existing named workflow.
|
|
4
4
|
metadata:
|
|
5
|
-
version: "1.
|
|
5
|
+
version: "1.12.0"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Workflow authoring
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# Workflow capability index
|
|
3
3
|
|
|
4
4
|
Contract format: `2.0.0`<br>
|
|
5
|
-
Contract content / skill / extension: `1.
|
|
5
|
+
Contract content / skill / extension: `1.12.0`
|
|
6
6
|
|
|
7
7
|
This compact generated index covers supported runtime globals and workflow-tool inputs. For constraints, compatibility behavior, internal boundaries, and dynamic-reference ownership, follow the [exhaustive generated facts](capability-details.md).
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# Exhaustive workflow capability facts
|
|
3
3
|
|
|
4
4
|
Contract format: `2.0.0`<br>
|
|
5
|
-
Contract content / skill / extension: `1.
|
|
5
|
+
Contract content / skill / extension: `1.12.0`
|
|
6
6
|
|
|
7
7
|
Every exact fact below is projected from the installed extension's capability contract. Explanatory judgment belongs in the hand-written references next to this file.
|
|
8
8
|
|
|
@@ -21,7 +21,9 @@ Call `agent(prompt, { label, schema?, strength? })`; it returns text, a schema-v
|
|
|
21
21
|
|
|
22
22
|
Use `agent(prompt, { thread: "implementer" })` when the same managed subagent must receive a later follow-up with its complete conversation intact. Reuse a thread name sequentially; never put same-thread calls in one `parallel()` batch. Threads are scoped to one workflow run/session and cannot use worktree isolation. A pause or process restart may require a fresh thread session.
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
Managed requests are validated locally before dispatch. The complete effective prompt must be nonblank and at most 100,000 characters after trimming, including schema instructions and any repair feedback. Leave room for both when sizing a prompt; aggregate compact summaries or bounded excerpts instead of concatenating full worker outputs. An oversized request throws a validation error without allocating a child or invalidating the completed journal prefix. A submitted request with a lost/ambiguous reply still requires positive exact-owner cleanup; a missing reconciliation record is not proof of cleanup.
|
|
25
|
+
|
|
26
|
+
When JavaScript reads fields, pass a small plain JSON Schema. Supported constraints are conjunctive: `const` and `enum` do not bypass type, required/properties/items, bounds, or `additionalProperties: false`. Unsupported keywords are rejected, not silently ignored. JSON extraction accepts prose/fences and balanced nested values, handles escaped strings, and can recover a later balanced candidate after a malformed prefix in linear time; balanced but invalid JSON still enters bounded repair. Schema noncompliance after repair throws and bypasses agent retries. Catch it only to return an explicit incomplete outcome without reading missing fields. Return objects, arrays, strings, numbers, booleans, and `null`—not functions, promises, cycles, `BigInt`, or runtime handles.
|
|
25
27
|
|
|
26
28
|
## Routing and support
|
|
27
29
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: workflow-patterns
|
|
3
3
|
description: Choose and run one of Pi's built-in multi-agent workflows for research, adversarial review, code review, multi-perspective analysis, or codebase auditing. Use when the request matches a named pattern; use workflow-authoring instead for a new script or custom topology.
|
|
4
4
|
metadata:
|
|
5
|
-
version: "1.
|
|
5
|
+
version: "1.12.0"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Built-in workflow patterns
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: workflow-review
|
|
3
3
|
description: Audit an existing Pi JavaScript workflow for topology, prompt/data flow, limits, failure handling, cancellation, resume safety, and publication compatibility. Use when reviewing or debugging a workflow script or saved run; do not use to run a standard named workflow.
|
|
4
4
|
metadata:
|
|
5
|
-
version: "1.
|
|
5
|
+
version: "1.12.0"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Workflow review
|
package/src/engine.ts
CHANGED
|
@@ -20,7 +20,13 @@
|
|
|
20
20
|
import { createHash, randomUUID } from "node:crypto";
|
|
21
21
|
import type { ManagedSpawnResponse } from "@signalridge/pi-subagents-protocol";
|
|
22
22
|
import { isManagedAgentTier } from "@signalridge/pi-subagents-protocol";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
AllocationFreeDispatchError,
|
|
25
|
+
describeWorkflowError,
|
|
26
|
+
WorkflowError,
|
|
27
|
+
WorkflowErrorCode,
|
|
28
|
+
wrapError,
|
|
29
|
+
} from "./errors.js";
|
|
24
30
|
import { type WorkflowInspection, type WorkflowInspectionHeader, WorkflowPresentation } from "./inspection.js";
|
|
25
31
|
import {
|
|
26
32
|
applyRecoveryEvent,
|
|
@@ -46,6 +52,7 @@ import {
|
|
|
46
52
|
type DispatchTask,
|
|
47
53
|
type ManagedProtocolCheck,
|
|
48
54
|
type ManagedSpawnClient,
|
|
55
|
+
prepareManagedSpawnRequest,
|
|
49
56
|
type WorkflowEventBus,
|
|
50
57
|
} from "./rpc-client.js";
|
|
51
58
|
import {
|
|
@@ -179,6 +186,8 @@ interface ManagedCallFacts {
|
|
|
179
186
|
executionGeneration: number;
|
|
180
187
|
attemptId: string;
|
|
181
188
|
agentId: string;
|
|
189
|
+
/** Exact-owner terminal admission, not merely a queued/running identity. */
|
|
190
|
+
settled?: boolean;
|
|
182
191
|
tokenCount?: number;
|
|
183
192
|
outputFile?: string;
|
|
184
193
|
compactionCount: number;
|
|
@@ -788,6 +797,7 @@ export class WorkflowEngine {
|
|
|
788
797
|
facts.agentId !== agentId
|
|
789
798
|
)
|
|
790
799
|
return;
|
|
800
|
+
facts.settled = true;
|
|
791
801
|
facts.tokenCount = terminal.tokenCount;
|
|
792
802
|
facts.outputFile = terminal.outputFile;
|
|
793
803
|
facts.compactionCount = Math.max(facts.compactionCount, terminal.compactionCount);
|
|
@@ -1564,6 +1574,12 @@ export class WorkflowEngine {
|
|
|
1564
1574
|
onAgentEnd: (event) => {
|
|
1565
1575
|
if (!isPresentationCurrent()) return;
|
|
1566
1576
|
const nodeId = event.id.startsWith(`${runId}:`) ? event.id.slice(runId.length + 1) : undefined;
|
|
1577
|
+
// A completed child is not a successful logical call until runtime
|
|
1578
|
+
// validation accepts it. Exhausted schema/repair failures must settle
|
|
1579
|
+
// the current journal attempt too, not just its volatile presentation.
|
|
1580
|
+
if (nodeId !== undefined && event.error !== undefined)
|
|
1581
|
+
this.finalizeLogicalCallFailure(state, nodeId, event.error);
|
|
1582
|
+
if (!isPresentationCurrent()) return;
|
|
1567
1583
|
const persisted = nodeId === undefined ? undefined : state.run.callResults[nodeId];
|
|
1568
1584
|
const persistedTokens =
|
|
1569
1585
|
nodeId !== undefined && persisted?.attemptId === state.run.attemptIds[nodeId]
|
|
@@ -1712,7 +1728,6 @@ export class WorkflowEngine {
|
|
|
1712
1728
|
const spawnNodeId = `call-${nodeId}`;
|
|
1713
1729
|
const base = state.generations.get(nodeId) ?? 0;
|
|
1714
1730
|
const generation = base + (runOptions.attempt ?? 1);
|
|
1715
|
-
state.generations.set(nodeId, generation);
|
|
1716
1731
|
const spawnAttemptId = `attempt-${generation}`;
|
|
1717
1732
|
const attemptId = `${runId}/${nodeId}/attempt-${generation}`;
|
|
1718
1733
|
|
|
@@ -1725,13 +1740,6 @@ export class WorkflowEngine {
|
|
|
1725
1740
|
{ recoverable: false },
|
|
1726
1741
|
);
|
|
1727
1742
|
}
|
|
1728
|
-
// This dispatch is authoritative for the current script revision. Do not
|
|
1729
|
-
// carry an identity from the previous script: deleting `tier` must clear
|
|
1730
|
-
// the recorded one instead of silently preserving the old route.
|
|
1731
|
-
const tierIdentity: CallTierIdentity = tier === undefined ? {} : { tier };
|
|
1732
|
-
state.callTiers.set(nodeId, tierIdentity);
|
|
1733
|
-
run.callTiers[nodeId] = tierIdentity;
|
|
1734
|
-
state.managedCallFacts.delete(nodeId);
|
|
1735
1743
|
const task: DispatchTask = {
|
|
1736
1744
|
subagent_type: type,
|
|
1737
1745
|
prompt,
|
|
@@ -1743,6 +1751,25 @@ export class WorkflowEngine {
|
|
|
1743
1751
|
...(runOptions.isolation === undefined ? {} : { isolation: runOptions.isolation }),
|
|
1744
1752
|
};
|
|
1745
1753
|
|
|
1754
|
+
// Validate the complete effective wire request (schema/repair text included)
|
|
1755
|
+
// before any attempt fact or submitted lease. A deterministic caller error
|
|
1756
|
+
// owns no remote allocation; cleanup must not mask its original diagnostic.
|
|
1757
|
+
try {
|
|
1758
|
+
prepareManagedSpawnRequest(task, runId, spawnNodeId, spawnAttemptId);
|
|
1759
|
+
} catch (error: unknown) {
|
|
1760
|
+
// This proof is minted only on the allocation-free side of dispatch;
|
|
1761
|
+
// errors from client.spawn below remain conservatively chargeable.
|
|
1762
|
+
throw new AllocationFreeDispatchError(error);
|
|
1763
|
+
}
|
|
1764
|
+
state.generations.set(nodeId, generation);
|
|
1765
|
+
// This dispatch is authoritative for the current script revision. Do not
|
|
1766
|
+
// carry an identity from the previous script: deleting `tier` must clear
|
|
1767
|
+
// the recorded one instead of silently preserving the old route.
|
|
1768
|
+
const tierIdentity: CallTierIdentity = tier === undefined ? {} : { tier };
|
|
1769
|
+
state.callTiers.set(nodeId, tierIdentity);
|
|
1770
|
+
run.callTiers[nodeId] = tierIdentity;
|
|
1771
|
+
state.managedCallFacts.delete(nodeId);
|
|
1772
|
+
|
|
1746
1773
|
// Journal the attempt rotation only for top-level calls. A nested frame is
|
|
1747
1774
|
// represented by its parent workflow_result replay boundary instead of
|
|
1748
1775
|
// leaking child call facts into the parent's numeric call namespace.
|
|
@@ -2235,6 +2262,33 @@ export class WorkflowEngine {
|
|
|
2235
2262
|
state.run.updatedAt = Date.now();
|
|
2236
2263
|
}
|
|
2237
2264
|
|
|
2265
|
+
private finalizeLogicalCallFailure(state: ScriptRunState, nodeId: string, error: string): void {
|
|
2266
|
+
const run = state.run;
|
|
2267
|
+
const facts = state.managedCallFacts.get(nodeId);
|
|
2268
|
+
// No guesses about unknown remote admissions, stale attempts, nested calls,
|
|
2269
|
+
// or failures already committed by dispatch's lifecycle terminal path.
|
|
2270
|
+
if (
|
|
2271
|
+
run.callStatus[nodeId] !== "running" ||
|
|
2272
|
+
!facts?.settled ||
|
|
2273
|
+
facts.executionGeneration !== state.executionGeneration ||
|
|
2274
|
+
facts.attemptId !== run.attemptIds[nodeId]
|
|
2275
|
+
)
|
|
2276
|
+
return;
|
|
2277
|
+
const result = resultFromLifecycle(
|
|
2278
|
+
"failed",
|
|
2279
|
+
facts.agentId,
|
|
2280
|
+
undefined,
|
|
2281
|
+
error,
|
|
2282
|
+
facts.compactionCount,
|
|
2283
|
+
facts.tokenCount,
|
|
2284
|
+
state.callTiers.get(nodeId)?.tier,
|
|
2285
|
+
);
|
|
2286
|
+
result.attemptId = facts.attemptId;
|
|
2287
|
+
if (facts.outputFile !== undefined) result.outputFile = facts.outputFile;
|
|
2288
|
+
// No call hash/store delta: invalid output can never enter the replay cache.
|
|
2289
|
+
this.commitCallTerminal(run.runId, nodeId, result);
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2238
2292
|
private commitCallTerminal(runId: string, nodeId: string, result: CallResult, required = false): void {
|
|
2239
2293
|
const state = this.runs.get(runId);
|
|
2240
2294
|
if (!state) return;
|
package/src/errors.ts
CHANGED
|
@@ -67,6 +67,20 @@ export class WorkflowError extends Error {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Host-only proof that effective managed-request validation rejected before any
|
|
72
|
+
* attempt rotation or remote allocation. Never infer this from a code/message,
|
|
73
|
+
* or attach it to an error received after submitting a spawn.
|
|
74
|
+
*/
|
|
75
|
+
export class AllocationFreeDispatchError extends WorkflowError {
|
|
76
|
+
constructor(cause: unknown) {
|
|
77
|
+
super(cause instanceof Error ? cause.message : String(cause), WorkflowErrorCode.SCRIPT_VALIDATION_ERROR, {
|
|
78
|
+
recoverable: false,
|
|
79
|
+
details: cause,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
70
84
|
// Transported VM throws must not acquire provider/abort classification merely
|
|
71
85
|
// because IPC reconstructed them in the host Error realm. Keep provenance and
|
|
72
86
|
// helper-computed text outside user data (including non-Error object throws).
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { Text
|
|
5
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
6
6
|
import { sanitizeDisplayText } from "@signalridge/pi-ui";
|
|
7
7
|
import { Type } from "@sinclair/typebox";
|
|
8
8
|
import { DEFAULT_TRIGGER_WORD, hasTriggerWord, WORKFLOW_ARMING_MESSAGE_TYPE, WorkflowTurnArming } from "./arming.js";
|
|
@@ -26,8 +26,9 @@ import {
|
|
|
26
26
|
WORKFLOW_STRENGTHS,
|
|
27
27
|
type WorkflowStrengthTable,
|
|
28
28
|
} from "./strengths.js";
|
|
29
|
-
import {
|
|
29
|
+
import { showWorkflowNavigator } from "./ui.js";
|
|
30
30
|
import { buildWorkflowUiSnapshot, openOwnedConversation, WorkflowUiBridge, type WorkflowUiSurface } from "./ui-bus.js";
|
|
31
|
+
import { overviewRuns, workflowOverviewLines } from "./ui-overview.js";
|
|
31
32
|
import {
|
|
32
33
|
loadProjectWorkflowSettings,
|
|
33
34
|
loadWorkflowSettings,
|
|
@@ -509,11 +510,12 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
509
510
|
.map((summary) => workflowEngine.getRun(String(summary.runId)))
|
|
510
511
|
.filter((run): run is NonNullable<typeof run> => run !== undefined);
|
|
511
512
|
if (statusSource?.()) tui.setStatus("pi-workflows", workflowStatusSummary(runs));
|
|
512
|
-
const
|
|
513
|
+
const hasProgress = overviewRuns(runs).length > 0;
|
|
513
514
|
const surfaceId = workflowUiSurfaceId;
|
|
515
|
+
const snapshot = buildWorkflowUiSnapshot(workflowEngine, surfaceId);
|
|
514
516
|
tui.setWidget(
|
|
515
517
|
"pi-workflows",
|
|
516
|
-
|
|
518
|
+
hasProgress
|
|
517
519
|
? (host, theme) => ({
|
|
518
520
|
render(width) {
|
|
519
521
|
if (
|
|
@@ -521,29 +523,33 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
521
523
|
engine !== workflowEngine ||
|
|
522
524
|
workflowEngine.isDisposed() ||
|
|
523
525
|
workflowUiSurfaceId !== surfaceId ||
|
|
526
|
+
!ownsDeliveryBranch() ||
|
|
524
527
|
!Number.isFinite(width) ||
|
|
525
528
|
width < 1
|
|
526
529
|
)
|
|
527
530
|
return [];
|
|
528
531
|
const height = Number.isFinite(host.terminal.rows) ? Math.max(0, host.terminal.rows) : 24;
|
|
529
|
-
const rowBudget = Math.max(
|
|
530
|
-
return
|
|
532
|
+
const rowBudget = Math.max(0, Math.min(12, Math.floor(height / 4)));
|
|
533
|
+
return workflowOverviewLines(
|
|
531
534
|
runs,
|
|
535
|
+
snapshot.runs,
|
|
536
|
+
theme,
|
|
537
|
+
width,
|
|
538
|
+
rowBudget,
|
|
532
539
|
workflowSettings.progressMode,
|
|
533
540
|
workflowSettings.maxAgentsShown,
|
|
534
|
-
rowBudget,
|
|
535
|
-
).map((line, index) =>
|
|
536
|
-
truncateToWidth(theme.fg(index === 0 ? "accent" : "muted", line), Math.floor(width)),
|
|
537
541
|
);
|
|
538
542
|
},
|
|
539
543
|
invalidate() {},
|
|
540
544
|
})
|
|
541
545
|
: undefined,
|
|
546
|
+
{ placement: "belowEditor" },
|
|
542
547
|
);
|
|
548
|
+
// Publication/footer cadence is independent of passive row visibility.
|
|
543
549
|
workflowUiBridge?.publish();
|
|
544
|
-
if (
|
|
550
|
+
if (hasProgress && !widgetTimer) {
|
|
545
551
|
widgetTimer = setInterval(refreshWidget, 2_000);
|
|
546
|
-
} else if (!
|
|
552
|
+
} else if (!hasProgress && widgetTimer) {
|
|
547
553
|
clearInterval(widgetTimer);
|
|
548
554
|
widgetTimer = undefined;
|
|
549
555
|
}
|
|
@@ -765,9 +771,6 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
765
771
|
// re-run every finished call untabled.
|
|
766
772
|
workflowEngine.strengths = () => workflowSettings.strengths;
|
|
767
773
|
workflowEngine.loadSavedWorkflow = resolveNestedWorkflow;
|
|
768
|
-
// Command/tool registrations persist across session replacement, but the
|
|
769
|
-
// widget belongs to the newly restored engine and must be refreshed too.
|
|
770
|
-
refreshWidget();
|
|
771
774
|
if (surfaceRegistered) return;
|
|
772
775
|
surfaceRegistered = true;
|
|
773
776
|
|
|
@@ -2008,7 +2011,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
2008
2011
|
return;
|
|
2009
2012
|
}
|
|
2010
2013
|
engine = recoveredEngine;
|
|
2011
|
-
widgetUi = ctx.mode === "tui" ? ctx.ui : undefined;
|
|
2014
|
+
widgetUi = ctx.mode === "tui" && ctx.hasUI ? ctx.ui : undefined;
|
|
2012
2015
|
registerSurface();
|
|
2013
2016
|
|
|
2014
2017
|
// Public API methods target the current session: generation + branch
|
|
@@ -2017,7 +2020,9 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
2017
2020
|
deliveryBranchAnchor = journalBranchAnchor;
|
|
2018
2021
|
registerStatusActions();
|
|
2019
2022
|
workflowUiBridge ??= new WorkflowUiBridge(pi.events, workflowUiSurface);
|
|
2020
|
-
|
|
2023
|
+
// Bind the presentation only after its session/branch source is installed.
|
|
2024
|
+
refreshWidget();
|
|
2025
|
+
if (!widgetUi) workflowUiBridge.publish();
|
|
2021
2026
|
inputPreparing = false;
|
|
2022
2027
|
compacting = false;
|
|
2023
2028
|
navigating = false;
|
package/src/journal.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import { createHash } from "node:crypto";
|
|
26
26
|
import type { ManagedOwner } from "@signalridge/pi-subagents-protocol";
|
|
27
|
-
import { isManagedAgentTier } from "@signalridge/pi-subagents-protocol";
|
|
27
|
+
import { isManagedAgentTier, parseManagedTerminalSnapshot } from "@signalridge/pi-subagents-protocol";
|
|
28
28
|
import {
|
|
29
29
|
MAX_CALLS_PER_WORKFLOW,
|
|
30
30
|
normalizeAgentRetries,
|
|
@@ -402,6 +402,17 @@ function parseResult(raw: unknown, label: string): CallResult {
|
|
|
402
402
|
}
|
|
403
403
|
const updatedAt = validTimestamp(raw.updatedAt, label);
|
|
404
404
|
const tierIdentity = parseCallTierIdentity(raw, label);
|
|
405
|
+
// Transcript pointers have their own managed protocol contract (including
|
|
406
|
+
// normalization and the 2000-character bound), not the journal identity cap.
|
|
407
|
+
const outputFile =
|
|
408
|
+
raw.outputFile === undefined
|
|
409
|
+
? undefined
|
|
410
|
+
: parseManagedTerminalSnapshot({
|
|
411
|
+
status,
|
|
412
|
+
outputFile: raw.outputFile,
|
|
413
|
+
compactionCount,
|
|
414
|
+
completedAt: updatedAt,
|
|
415
|
+
}).outputFile;
|
|
405
416
|
const result: CallResult = {
|
|
406
417
|
status,
|
|
407
418
|
compactionCount,
|
|
@@ -413,9 +424,7 @@ function parseResult(raw: unknown, label: string): CallResult {
|
|
|
413
424
|
: { attemptId: boundedString(raw.attemptId, `${label}.attemptId`, JOURNAL_ID_LIMIT) }),
|
|
414
425
|
...(raw.result === undefined ? {} : { result: raw.result }),
|
|
415
426
|
...(raw.error === undefined ? {} : { error: boundedString(raw.error, `${label}.error`, JOURNAL_ERROR_LIMIT) }),
|
|
416
|
-
...(
|
|
417
|
-
? {}
|
|
418
|
-
: { outputFile: boundedString(raw.outputFile, `${label}.outputFile`, JOURNAL_ID_LIMIT) }),
|
|
427
|
+
...(outputFile === undefined ? {} : { outputFile }),
|
|
419
428
|
...(raw.tokenCount === undefined
|
|
420
429
|
? {}
|
|
421
430
|
: {
|
package/src/rpc-client.ts
CHANGED
|
@@ -6,10 +6,12 @@ import type {
|
|
|
6
6
|
} from "@signalridge/pi-subagents-protocol";
|
|
7
7
|
import {
|
|
8
8
|
PROTOCOL_VERSION,
|
|
9
|
+
parseManagedSpawnRequest,
|
|
9
10
|
parseManagedSpawnResponse,
|
|
10
11
|
parseProtocolPing,
|
|
11
12
|
requiredCapabilitiesMatch,
|
|
12
13
|
} from "@signalridge/pi-subagents-protocol";
|
|
14
|
+
import { WorkflowError, WorkflowErrorCode } from "./errors.js";
|
|
13
15
|
import type { WorkflowOwner } from "./journal.js";
|
|
14
16
|
|
|
15
17
|
/** Minimal dispatch task for a script-run agent() call. */
|
|
@@ -298,6 +300,31 @@ export interface ManagedSpawnClient {
|
|
|
298
300
|
checkProtocol?: () => Promise<ManagedProtocolCheck>;
|
|
299
301
|
}
|
|
300
302
|
|
|
303
|
+
/** Shared wire preflight, also used before the engine submits an allocation lease. */
|
|
304
|
+
export function prepareManagedSpawnRequest(task: DispatchTask, runId: string, nodeId: string, attemptId = "attempt-1") {
|
|
305
|
+
try {
|
|
306
|
+
return parseManagedSpawnRequest({
|
|
307
|
+
requestId: requestId(),
|
|
308
|
+
spawnKey: `${runId}/${nodeId}/${attemptId}`,
|
|
309
|
+
type: task.subagent_type,
|
|
310
|
+
prompt: task.prompt,
|
|
311
|
+
description: task.description,
|
|
312
|
+
...(task.tier === undefined ? {} : { tier: task.tier }),
|
|
313
|
+
...(task.toolset === undefined ? {} : { toolset: task.toolset }),
|
|
314
|
+
...(task.thread === undefined ? {} : { thread: task.thread }),
|
|
315
|
+
...(task.excludeTools === undefined ? {} : { excludeTools: task.excludeTools }),
|
|
316
|
+
...(task.isolation === undefined ? {} : { isolation: task.isolation }),
|
|
317
|
+
owner: { extension: "pi-workflows", runId, nodeId, attemptId },
|
|
318
|
+
});
|
|
319
|
+
} catch (error: unknown) {
|
|
320
|
+
throw new WorkflowError(
|
|
321
|
+
error instanceof Error ? error.message : String(error),
|
|
322
|
+
WorkflowErrorCode.SCRIPT_VALIDATION_ERROR,
|
|
323
|
+
{ recoverable: false, details: error },
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
301
328
|
/** Event-bus-only client for the additive pi-subagents managed protocol. */
|
|
302
329
|
export function createManagedSpawnClient(
|
|
303
330
|
events: WorkflowEventBus,
|
|
@@ -325,29 +352,15 @@ export function createManagedSpawnClient(
|
|
|
325
352
|
* fingerprint still reuses the persisted tombstone (per-call cache).
|
|
326
353
|
*/
|
|
327
354
|
async spawn(task, runId, nodeId, attemptId, signal) {
|
|
328
|
-
|
|
329
|
-
|
|
355
|
+
// Outside the transport catch: a local rejection never emitted a request,
|
|
356
|
+
// so it must not create reconciliation or owned-cleanup uncertainty.
|
|
357
|
+
const request = prepareManagedSpawnRequest(task, runId, nodeId, attemptId);
|
|
358
|
+
const { spawnKey, owner } = request;
|
|
330
359
|
const requestSignal =
|
|
331
360
|
signal && sessionSignal ? AbortSignal.any([signal, sessionSignal]) : (signal ?? sessionSignal);
|
|
332
361
|
let data: unknown;
|
|
333
362
|
try {
|
|
334
|
-
data = await call<unknown>(
|
|
335
|
-
"subagents:rpc:spawn-managed",
|
|
336
|
-
{
|
|
337
|
-
requestId: requestId(),
|
|
338
|
-
spawnKey,
|
|
339
|
-
type: task.subagent_type,
|
|
340
|
-
prompt: task.prompt,
|
|
341
|
-
description: task.description,
|
|
342
|
-
...(task.tier === undefined ? {} : { tier: task.tier }),
|
|
343
|
-
...(task.toolset === undefined ? {} : { toolset: task.toolset }),
|
|
344
|
-
...(task.thread === undefined ? {} : { thread: task.thread }),
|
|
345
|
-
...(task.excludeTools === undefined ? {} : { excludeTools: task.excludeTools }),
|
|
346
|
-
...(task.isolation === undefined ? {} : { isolation: task.isolation }),
|
|
347
|
-
owner: { extension: "pi-workflows", runId, nodeId, attemptId: effectiveAttemptId },
|
|
348
|
-
},
|
|
349
|
-
requestSignal,
|
|
350
|
-
);
|
|
363
|
+
data = await call<unknown>("subagents:rpc:spawn-managed", { ...request }, requestSignal);
|
|
351
364
|
} catch (error: unknown) {
|
|
352
365
|
// Reconciliation must not inherit the signal that caused the original
|
|
353
366
|
// request to fail: shutdown/abort can happen after allocation but before
|
|
@@ -357,7 +370,7 @@ export function createManagedSpawnClient(
|
|
|
357
370
|
{
|
|
358
371
|
requestId: requestId(),
|
|
359
372
|
spawnKey,
|
|
360
|
-
owner
|
|
373
|
+
owner,
|
|
361
374
|
},
|
|
362
375
|
undefined,
|
|
363
376
|
5_000,
|
package/src/runtime.ts
CHANGED
|
@@ -25,7 +25,7 @@ import type { ManagedRoutingPolicy } from "@signalridge/pi-subagents-protocol";
|
|
|
25
25
|
import { agentTierPolicyIdentity, isManagedAgentTier } from "@signalridge/pi-subagents-protocol";
|
|
26
26
|
import { parse } from "acorn";
|
|
27
27
|
import { checkRuntimeBindings } from "./capabilities.js";
|
|
28
|
-
import { WorkflowError, WorkflowErrorCode, wrapError } from "./errors.js";
|
|
28
|
+
import { AllocationFreeDispatchError, WorkflowError, WorkflowErrorCode, wrapError } from "./errors.js";
|
|
29
29
|
import {
|
|
30
30
|
executeOrchestration,
|
|
31
31
|
normalizeOrchestration,
|
|
@@ -1038,31 +1038,34 @@ async function withTimeout<T>(
|
|
|
1038
1038
|
|
|
1039
1039
|
/** Extract the first balanced JSON value ({...} or [...]) from text. */
|
|
1040
1040
|
export function findJsonBlock(text: string): string | undefined {
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1041
|
+
// Suffix summaries avoid rescanning after every malformed opening bracket.
|
|
1042
|
+
// stringEnd[i] is the position after a closing quote when already in a string
|
|
1043
|
+
// at i; contentEnd[i] is the first unconsumed closing bracket after skipping
|
|
1044
|
+
// balanced children/strings, or -1 for an incomplete/mismatched suffix.
|
|
1045
|
+
// Compute candidates even inside strings: an unmatched/malformed outer prefix
|
|
1046
|
+
// must still allow a later, independently balanced candidate, as before.
|
|
1047
|
+
const stringEnd = new Int32Array(text.length + 2);
|
|
1048
|
+
const contentEnd = new Int32Array(text.length + 1);
|
|
1049
|
+
stringEnd[text.length] = stringEnd[text.length + 1] = contentEnd[text.length] = -1;
|
|
1050
|
+
let start = -1;
|
|
1051
|
+
let end = -1;
|
|
1052
|
+
for (let i = text.length - 1; i >= 0; i--) {
|
|
1053
|
+
const char = text[i];
|
|
1054
|
+
stringEnd[i] = char === '"' ? i + 1 : stringEnd[i + (char === "\\" ? 2 : 1)];
|
|
1055
|
+
if (char === "}" || char === "]") contentEnd[i] = i;
|
|
1056
|
+
else if (char === '"') {
|
|
1057
|
+
const after = stringEnd[i + 1];
|
|
1058
|
+
contentEnd[i] = after < 0 ? -1 : contentEnd[after];
|
|
1059
|
+
} else if (char === "{" || char === "[") {
|
|
1060
|
+
const close = contentEnd[i + 1];
|
|
1061
|
+
if (close >= 0 && text[close] === (char === "{" ? "}" : "]")) {
|
|
1062
|
+
start = i;
|
|
1063
|
+
end = close + 1;
|
|
1064
|
+
contentEnd[i] = contentEnd[end];
|
|
1065
|
+
} else contentEnd[i] = -1;
|
|
1066
|
+
} else contentEnd[i] = contentEnd[i + 1];
|
|
1064
1067
|
}
|
|
1065
|
-
return undefined;
|
|
1068
|
+
return start < 0 ? undefined : text.slice(start, end);
|
|
1066
1069
|
}
|
|
1067
1070
|
|
|
1068
1071
|
// ── Minimal plain-JSON-Schema validator ──────────────────────────────────────
|
|
@@ -1078,13 +1081,11 @@ function validateValue(value: unknown, schema: SchemaNode, path: string): string
|
|
|
1078
1081
|
if (schema.const !== undefined) {
|
|
1079
1082
|
if (JSON.stringify(value) !== JSON.stringify(schema.const))
|
|
1080
1083
|
errors.push(`${path}: expected const ${JSON.stringify(schema.const)}`);
|
|
1081
|
-
return errors;
|
|
1082
1084
|
}
|
|
1083
1085
|
if (schema.enum !== undefined) {
|
|
1084
1086
|
if (!(schema.enum as unknown[]).some((e) => JSON.stringify(e) === JSON.stringify(value))) {
|
|
1085
1087
|
errors.push(`${path}: value not in enum`);
|
|
1086
1088
|
}
|
|
1087
|
-
return errors;
|
|
1088
1089
|
}
|
|
1089
1090
|
const type = schema.type;
|
|
1090
1091
|
if (type !== undefined) {
|
|
@@ -1790,12 +1791,34 @@ export async function runWorkflow<T = unknown>(
|
|
|
1790
1791
|
? `\n\nReply with a single raw JSON value (object or array) matching exactly this JSON Schema:\n${JSON.stringify(schema)}\nDo not wrap it in prose or markdown fences.`
|
|
1791
1792
|
: "";
|
|
1792
1793
|
let repairFeedback = "";
|
|
1794
|
+
let previousAttempt: { tokens: number; tokenUsage?: AgentUsage; tier?: string } | undefined;
|
|
1793
1795
|
|
|
1794
1796
|
try {
|
|
1795
1797
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1796
1798
|
// Outside the attempt catch: a rejected admission is not a provider
|
|
1797
1799
|
// attempt and must not be charged an estimated failure or retried.
|
|
1798
|
-
|
|
1800
|
+
try {
|
|
1801
|
+
checkBudget();
|
|
1802
|
+
} catch (error) {
|
|
1803
|
+
const workflowError = wrapError(error, { agentLabel: label });
|
|
1804
|
+
// A between-attempt denial ends the logical call, but it neither
|
|
1805
|
+
// starts nor charges another physical child. Preserve the previous
|
|
1806
|
+
// attempt's observations; the engine independently requires exact
|
|
1807
|
+
// settled-generation facts before making a durable terminal result.
|
|
1808
|
+
if (previousAttempt && !isAborted() && workflowError.code === WorkflowErrorCode.TOKEN_BUDGET_EXHAUSTED) {
|
|
1809
|
+
options.onAgentEnd?.({
|
|
1810
|
+
id: deltaKey,
|
|
1811
|
+
label,
|
|
1812
|
+
...phaseObservation(assignedPhase),
|
|
1813
|
+
result: null,
|
|
1814
|
+
...previousAttempt,
|
|
1815
|
+
error: workflowError.message,
|
|
1816
|
+
errorCode: workflowError.code,
|
|
1817
|
+
recoverable: workflowError.recoverable,
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
throw error;
|
|
1821
|
+
}
|
|
1799
1822
|
usage = undefined;
|
|
1800
1823
|
const externalSignal = options.signal;
|
|
1801
1824
|
let onExternalAbort: (() => void) | undefined;
|
|
@@ -1964,7 +1987,12 @@ export async function runWorkflow<T = unknown>(
|
|
|
1964
1987
|
if (isAborted()) throw error;
|
|
1965
1988
|
|
|
1966
1989
|
const workflowError = wrapError(error, { agentLabel: label });
|
|
1967
|
-
const
|
|
1990
|
+
const allocationFree = error instanceof AllocationFreeDispatchError;
|
|
1991
|
+
// Only trusted local preflight proves this attempt never submitted.
|
|
1992
|
+
// A rejected repair still owns the logical call's earlier dispatch;
|
|
1993
|
+
// neither transport text nor a lost reply can refund that lease.
|
|
1994
|
+
if (allocationFree && attempt === 1) shared.physicalAgentCount--;
|
|
1995
|
+
const tokens = allocationFree ? 0 : recordTokens(null);
|
|
1968
1996
|
// This attempt's store writes must not survive it — a failed attempt
|
|
1969
1997
|
// shares this call's deltaKey with every other attempt, so without
|
|
1970
1998
|
// rolling back here its writes would stay live in the store and merge
|
|
@@ -1977,6 +2005,7 @@ export async function runWorkflow<T = unknown>(
|
|
|
1977
2005
|
`agent "${label}" attempt ${attempt}/${maxAttempts} failed: ${workflowError.code} ${workflowError.message}; retrying`,
|
|
1978
2006
|
);
|
|
1979
2007
|
options.onRetrySpend?.(tokens);
|
|
2008
|
+
previousAttempt = { tokens, tokenUsage: usage, tier: resolvedTier };
|
|
1980
2009
|
continue;
|
|
1981
2010
|
}
|
|
1982
2011
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { WorkflowUiRunSummary } from "@signalridge/pi-subagents-protocol";
|
|
4
|
+
import type { ScriptRun } from "./journal.js";
|
|
5
|
+
import { sanitizeDisplayText } from "./safe-text.js";
|
|
6
|
+
import { callIndexes, isTerminal, statusMark } from "./ui-presentation.js";
|
|
7
|
+
|
|
8
|
+
export const OVERVIEW_LINGER_MS = 4_000;
|
|
9
|
+
|
|
10
|
+
/** Terminal history remains in /workflows, not an indefinitely growing passive list. */
|
|
11
|
+
export function overviewRuns(runs: readonly ScriptRun[], now = Date.now()): ScriptRun[] {
|
|
12
|
+
return runs.filter((run) => !isTerminal(run.status) || now - run.updatedAt < OVERVIEW_LINGER_MS);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function statusColor(status: string): "success" | "error" | "warning" | "accent" | "muted" {
|
|
16
|
+
if (status === "completed") return "success";
|
|
17
|
+
if (status === "failed") return "error";
|
|
18
|
+
if (["paused", "pausing", "interrupted", "stopping", "stopped"].includes(status)) return "warning";
|
|
19
|
+
return status === "running" ? "accent" : "muted";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function tokens(count: number): string {
|
|
23
|
+
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
24
|
+
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k`;
|
|
25
|
+
return String(count);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The workflow owner's passive overview. Authority/phase/settled usage come from
|
|
30
|
+
* its existing projection, never from child records or in-flight usage polling.
|
|
31
|
+
* No keyboard listener: /workflows is the sole workflow navigator.
|
|
32
|
+
*/
|
|
33
|
+
export function workflowOverviewLines(
|
|
34
|
+
runs: readonly ScriptRun[],
|
|
35
|
+
summaries: readonly WorkflowUiRunSummary[],
|
|
36
|
+
theme: Pick<Theme, "fg" | "bold">,
|
|
37
|
+
width: number,
|
|
38
|
+
maxRows: number,
|
|
39
|
+
mode: "compact" | "detailed" = "compact",
|
|
40
|
+
maxAgentsShown = 6,
|
|
41
|
+
now = Date.now(),
|
|
42
|
+
): string[] {
|
|
43
|
+
if (!Number.isFinite(width) || width < 1 || !Number.isFinite(maxRows) || maxRows < 1) return [];
|
|
44
|
+
const columns = Math.floor(width);
|
|
45
|
+
const rows = Math.min(32, Math.floor(maxRows));
|
|
46
|
+
const max = Number.isFinite(maxAgentsShown) ? Math.max(1, Math.min(32, Math.floor(maxAgentsShown))) : 6;
|
|
47
|
+
const available = new Map(overviewRuns(runs, now).map((run) => [run.runId, run]));
|
|
48
|
+
const entries = summaries.filter((summary) => available.has(summary.runId));
|
|
49
|
+
if (!available.size) return [];
|
|
50
|
+
// The peer bridge caps publication at 256 summaries. It supplies bounded row
|
|
51
|
+
// metadata, never authority over how many owner-side runs exist or are live.
|
|
52
|
+
const active = [...available.values()].filter((run) => !isTerminal(run.status)).length;
|
|
53
|
+
const lines = [theme.fg("muted", `Workflows · ${active} active · /workflows`)];
|
|
54
|
+
if (rows === 1) return lines.map((line) => truncateToWidth(line, columns));
|
|
55
|
+
let shown = Math.min(max, entries.length, rows - 1);
|
|
56
|
+
if (available.size > shown && shown + 2 > rows) shown = Math.max(0, shown - 1);
|
|
57
|
+
const hidden = available.size - shown;
|
|
58
|
+
for (const [index, summary] of entries.slice(0, shown).entries()) {
|
|
59
|
+
const run = available.get(summary.runId);
|
|
60
|
+
if (!run) continue;
|
|
61
|
+
const color = statusColor(summary.status);
|
|
62
|
+
const phase = summary.currentPhase ? ` · ${sanitizeDisplayText(summary.currentPhase, 80)}` : "";
|
|
63
|
+
const stats = `${summary.runningCalls}/${summary.callCount} in flight · ${summary.completedCalls} done · ${summary.failedCalls} failed`;
|
|
64
|
+
// These are settled-call totals (including explicit zero), NOT live usage.
|
|
65
|
+
const usage = summary.tokenCount === undefined ? "" : ` · ${tokens(summary.tokenCount)} settled tokens`;
|
|
66
|
+
const metadata = `${phase} · ${stats}${summary.restored ? " · restored" : ""}`;
|
|
67
|
+
const left = ` ${theme.fg(color, "▸")} ${theme.bold(sanitizeDisplayText(summary.name, 80))} · ${theme.fg(color, summary.status)}${theme.fg("muted", metadata)}`;
|
|
68
|
+
const right = theme.fg("muted", `${Math.floor(summary.elapsedMs / 1_000)}s${usage}`);
|
|
69
|
+
const runLine = lines.length;
|
|
70
|
+
if (columns >= 48) {
|
|
71
|
+
// Preserve elapsed/settled usage on ordinary terminals, but never let
|
|
72
|
+
// secondary stats consume the primary name on a narrow viewport.
|
|
73
|
+
const boundedRight = truncateToWidth(right, Math.floor(columns / 3));
|
|
74
|
+
const boundedLeft = truncateToWidth(left, columns - visibleWidth(boundedRight) - 1);
|
|
75
|
+
const gap = Math.max(1, columns - visibleWidth(boundedLeft) - visibleWidth(boundedRight));
|
|
76
|
+
lines.push(boundedLeft + " ".repeat(gap) + boundedRight);
|
|
77
|
+
} else lines.push(`${left} ${right}`);
|
|
78
|
+
if (mode !== "detailed") continue;
|
|
79
|
+
const calls = callIndexes(run);
|
|
80
|
+
const live = calls.filter((id) => run.callStatus[id] === "running");
|
|
81
|
+
const liveIds = new Set(live);
|
|
82
|
+
const prioritized = [...live, ...calls.filter((id) => !liveIds.has(id))];
|
|
83
|
+
const reserved = shown - index - 1 + (hidden > 0 ? 1 : 0);
|
|
84
|
+
const callRows = Math.max(0, Math.min(max, rows - lines.length - reserved));
|
|
85
|
+
const visible = prioritized.slice(0, callRows);
|
|
86
|
+
for (const id of visible) {
|
|
87
|
+
const status = run.callStatus[id] ?? run.callResults[id]?.status ?? "unknown";
|
|
88
|
+
lines.push(` ${theme.fg(statusColor(status), statusMark(status))} ${theme.fg("muted", `${id}: ${status}`)}`);
|
|
89
|
+
}
|
|
90
|
+
const omitted = calls.length - visible.length;
|
|
91
|
+
if (omitted > 0)
|
|
92
|
+
lines[visible.length ? lines.length - 1 : runLine] += theme.fg("muted", ` · ${omitted} more calls`);
|
|
93
|
+
}
|
|
94
|
+
if (hidden > 0) lines.push(theme.fg("muted", ` ${hidden} more workflows · /workflows`));
|
|
95
|
+
if (lines.length < rows) lines.push(""); // One separator before the next dock/footer surface.
|
|
96
|
+
return lines.map((line) => truncateToWidth(line, columns));
|
|
97
|
+
}
|