@signalridge/pi-workflows 1.10.1 → 1.11.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 +11 -0
- package/README.md +11 -0
- package/package.json +1 -1
- 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-patterns/SKILL.md +1 -1
- package/skills/workflow-review/SKILL.md +1 -1
- package/src/engine.ts +3 -1
- package/src/index.ts +136 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.11.0
|
|
4
|
+
### Minor Changes
|
|
5
|
+
|
|
6
|
+
- c639660: Keep pending completion reminders cancellable until parent boundaries and consume deliberately read terminal results instead of replaying stale notifications.
|
|
7
|
+
|
|
8
|
+
Subagents selectively adapt tintinweb/pi-subagents #388's completion outbox, merge eligible batches into one steering message, and retain explicit groups, attempt ownership, and Claude Code notification XML. Successful terminal result reads suppress that attempt's pending reminder; unread failures remain eligible.
|
|
9
|
+
|
|
10
|
+
Background terminal log or explicit-ID status reads consume only exit notices, preserving unread monitor matches and future completion after running reads. Workflow get/status adds explicit `consume: true` with a branch-visible acknowledgement receipt, without consuming ordinary inspections or suppressing resumed executions. Already accepted transcript messages and historical results remain unchanged.
|
|
11
|
+
|
|
12
|
+
Stopped workflow state retains the accepted terminal event timestamp across slow appends and restore. Background reads prepare job data before committing exit consumption, and post-commit status-rendering failures cannot turn a successful read into an error. Early smart-group completions retain finalized membership separately from immutable results, with partial-group labels rechecked at handoff.
|
|
13
|
+
|
|
3
14
|
## 1.10.1
|
|
4
15
|
### Patch Changes
|
|
5
16
|
|
package/README.md
CHANGED
|
@@ -269,6 +269,17 @@ The passive progress panel uses a terminal-height row budget, prioritizes live c
|
|
|
269
269
|
|
|
270
270
|
### Background result notifications
|
|
271
271
|
|
|
272
|
+
To explicitly read and acknowledge a terminal result, call
|
|
273
|
+
`workflow_control({ action: "get", run_id: "...", consume: true })` (`status`
|
|
274
|
+
is an alias). A `notificationConsumed: true` receipt means an acknowledgement
|
|
275
|
+
was observed on the active branch and the pending reminder was consumed.
|
|
276
|
+
Ordinary `get`/`status`, `list`, running results, and cancelled/stale reads do
|
|
277
|
+
not consume future completion. Consumption is scoped to that execution; a
|
|
278
|
+
later resumed execution remains eligible. Identity comes from the durable terminal
|
|
279
|
+
journal entry, not a runtime counter that restarts on restore. Older markers
|
|
280
|
+
without that evidence are not retroactively consumed. Receipts do not delete run history
|
|
281
|
+
or retract messages already handed to Pi.
|
|
282
|
+
|
|
272
283
|
Completed background runs are buffered in branch-local pending-delivery markers
|
|
273
284
|
(up to 256, expiring after seven days). Notifications wait for an owned, idle
|
|
274
285
|
session with no queued input, input preparation, compaction, or navigation in
|
package/package.json
CHANGED
|
@@ -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.11.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.11.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.11.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
|
|
|
@@ -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.11.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.11.0"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Workflow review
|
package/src/engine.ts
CHANGED
|
@@ -2614,7 +2614,9 @@ export class WorkflowEngine {
|
|
|
2614
2614
|
run.callStatus[terminal.nodeId] = "stopped";
|
|
2615
2615
|
}
|
|
2616
2616
|
run.status = "stopped";
|
|
2617
|
-
|
|
2617
|
+
// Publish the accepted event's identity, just like other terminal paths.
|
|
2618
|
+
// A slow append must not detach the live state from its durable receipt.
|
|
2619
|
+
run.updatedAt = event.timestamp;
|
|
2618
2620
|
state.presentation.settle(run.status);
|
|
2619
2621
|
this.notifyInspection(state);
|
|
2620
2622
|
if (current()) this.notifyTerminal(state);
|
package/src/index.ts
CHANGED
|
@@ -94,6 +94,12 @@ const ControlSchema = Type.Object(
|
|
|
94
94
|
{ description: "Control action" },
|
|
95
95
|
),
|
|
96
96
|
run_id: Type.Optional(Type.String({ minLength: 1, maxLength: 128 })),
|
|
97
|
+
consume: Type.Optional(
|
|
98
|
+
Type.Boolean({
|
|
99
|
+
description:
|
|
100
|
+
"With get/status only: explicitly acknowledge a reviewed terminal result and suppress its pending completion reminder. Running results and ordinary inspections are not consumed.",
|
|
101
|
+
}),
|
|
102
|
+
),
|
|
97
103
|
// Accept camelCase and legacy workflow_run_id for interop with pi-dynamic-workflows docs
|
|
98
104
|
runId: Type.Optional(Type.String({ minLength: 1, maxLength: 128 })),
|
|
99
105
|
workflow_run_id: Type.Optional(Type.String({ minLength: 1, maxLength: 128 })),
|
|
@@ -344,6 +350,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
344
350
|
let lifecycleGeneration = 0;
|
|
345
351
|
let deliveryGeneration = 0;
|
|
346
352
|
const deliveryObservers = new Map<string, string>();
|
|
353
|
+
const observedExecutions = new Map<string, string>();
|
|
347
354
|
let handledTreeEvent: unknown;
|
|
348
355
|
let journalBranchAnchor: string | null | undefined;
|
|
349
356
|
let protocolAbortController: AbortController | undefined;
|
|
@@ -370,7 +377,10 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
370
377
|
{ generation: number; context: ExtensionContext; engine: WorkflowEngine; anchor: string | null | undefined }
|
|
371
378
|
>();
|
|
372
379
|
/** Pending deliveries for completed runs, including durable recovery (A10). */
|
|
373
|
-
const pendingDeliveryMarkers = new Map<
|
|
380
|
+
const pendingDeliveryMarkers = new Map<
|
|
381
|
+
string,
|
|
382
|
+
{ text: string; timestamp: number; executionGeneration?: number; completionId?: string }
|
|
383
|
+
>();
|
|
374
384
|
const PENDING_DELIVERY_TYPE = "pi-workflows:pending-delivery";
|
|
375
385
|
const PENDING_DELIVERY_ACK_TYPE = "pi-workflows:pending-delivery-ack";
|
|
376
386
|
const MAX_PENDING_DELIVERIES = 256;
|
|
@@ -426,6 +436,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
426
436
|
workflowUiSurfaceId = randomUUID();
|
|
427
437
|
deliveryGeneration++;
|
|
428
438
|
deliveryObservers.clear();
|
|
439
|
+
observedExecutions.clear();
|
|
429
440
|
turnArming.retire();
|
|
430
441
|
wakeAdmissions.clear();
|
|
431
442
|
if (deliveryTimer) clearTimeout(deliveryTimer);
|
|
@@ -880,7 +891,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
880
891
|
});
|
|
881
892
|
return new Text(theme.fg("dim", expanded ? text.slice(0, 8_000) : text.slice(0, 1_000)), 0, 0);
|
|
882
893
|
},
|
|
883
|
-
async execute(_toolCallId, params,
|
|
894
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
884
895
|
try {
|
|
885
896
|
const rawAction = params.action as string;
|
|
886
897
|
const action = (rawAction === "status" ? "get" : rawAction) as
|
|
@@ -895,7 +906,22 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
895
906
|
}
|
|
896
907
|
const runId = normalizeControlRunId(params as unknown as Record<string, unknown>);
|
|
897
908
|
const workflowEngine = currentEngine();
|
|
909
|
+
if (params.consume && action !== "get") throw new Error("consume is supported only with get/status.");
|
|
910
|
+
const execution = runId ? workflowEngine.getState(runId)?.executionGeneration : undefined;
|
|
911
|
+
const ownerGeneration = deliveryGeneration;
|
|
912
|
+
const completionId =
|
|
913
|
+
params.consume && action === "get" && runId ? completionReceipt(workflowEngine, runId) : undefined;
|
|
898
914
|
const result = await workflowEngine.control(action, runId);
|
|
915
|
+
signal?.throwIfAborted();
|
|
916
|
+
if (params.consume) {
|
|
917
|
+
const notificationConsumed =
|
|
918
|
+
!!runId &&
|
|
919
|
+
execution !== undefined &&
|
|
920
|
+
completionId !== undefined &&
|
|
921
|
+
consumeReviewedResult(workflowEngine, runId, execution, completionId, ownerGeneration, ctx);
|
|
922
|
+
const receipt = { ...result, notificationConsumed };
|
|
923
|
+
return textResult(JSON.stringify(receipt), receipt);
|
|
924
|
+
}
|
|
899
925
|
return textResult(JSON.stringify(result), result);
|
|
900
926
|
} catch (error: unknown) {
|
|
901
927
|
return textResult(error instanceof Error ? error.message : String(error));
|
|
@@ -1548,9 +1574,92 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1548
1574
|
});
|
|
1549
1575
|
};
|
|
1550
1576
|
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1577
|
+
/** Native journal identity survives restore; runtime generation counters do not. */
|
|
1578
|
+
function completionReceipt(workflowEngine: WorkflowEngine, runId: string): string | undefined {
|
|
1579
|
+
const run = workflowEngine.getState(runId)?.run;
|
|
1580
|
+
if (!run || !["completed", "failed", "stopped"].includes(run.status)) return;
|
|
1581
|
+
try {
|
|
1582
|
+
const branch = deliveryContext?.sessionManager.getBranch() ?? [];
|
|
1583
|
+
for (let index = branch.length - 1; index >= 0; index--) {
|
|
1584
|
+
const entry = branch[index];
|
|
1585
|
+
if (entry.type !== "custom" || entry.customType !== JOURNAL_ENTRY_TYPE) continue;
|
|
1586
|
+
const data = entry.data as Record<string, unknown> | undefined;
|
|
1587
|
+
if (
|
|
1588
|
+
data?.runId === runId &&
|
|
1589
|
+
data.status === run.status &&
|
|
1590
|
+
data.timestamp === run.updatedAt &&
|
|
1591
|
+
(data.kind === "workflow_transition" || data.kind === "terminal_recovery")
|
|
1592
|
+
)
|
|
1593
|
+
return entry.id;
|
|
1594
|
+
}
|
|
1595
|
+
} catch {
|
|
1596
|
+
// Unavailable branch evidence cannot acknowledge a result.
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function consumeReviewedResult(
|
|
1601
|
+
workflowEngine: WorkflowEngine,
|
|
1602
|
+
runId: string,
|
|
1603
|
+
execution: number,
|
|
1604
|
+
completionId: string,
|
|
1605
|
+
generation: number,
|
|
1606
|
+
ctx: ExtensionContext,
|
|
1607
|
+
): boolean {
|
|
1608
|
+
const current = () =>
|
|
1609
|
+
generation === deliveryGeneration &&
|
|
1610
|
+
workflowEngine === engine &&
|
|
1611
|
+
ctx.sessionManager === deliveryContext?.sessionManager &&
|
|
1612
|
+
ownsDeliveryBranch() &&
|
|
1613
|
+
workflowEngine.getState(runId)?.executionGeneration === execution &&
|
|
1614
|
+
completionReceipt(workflowEngine, runId) === completionId;
|
|
1615
|
+
if (!current()) return false;
|
|
1616
|
+
// An older marker without a durable receipt cannot be attributed to the
|
|
1617
|
+
// reviewed execution. Preserve it rather than silently migrating history.
|
|
1618
|
+
if (pendingDeliveryMarkers.has(runId) && pendingDeliveryMarkers.get(runId)?.completionId === undefined)
|
|
1619
|
+
return false;
|
|
1620
|
+
if (observedExecutions.get(runId) === completionId) return true;
|
|
1621
|
+
try {
|
|
1622
|
+
const previous = new Set(ctx.sessionManager.getBranch().map((entry) => entry.id));
|
|
1623
|
+
pi.appendEntry(PENDING_DELIVERY_ACK_TYPE, {
|
|
1624
|
+
runId,
|
|
1625
|
+
executionGeneration: execution,
|
|
1626
|
+
completionId,
|
|
1627
|
+
observed: true,
|
|
1628
|
+
timestamp: Date.now(),
|
|
1629
|
+
});
|
|
1630
|
+
if (!current()) return false;
|
|
1631
|
+
const accepted = ctx.sessionManager.getBranch().some((entry) => {
|
|
1632
|
+
if (previous.has(entry.id) || entry.type !== "custom" || entry.customType !== PENDING_DELIVERY_ACK_TYPE)
|
|
1633
|
+
return false;
|
|
1634
|
+
const data = entry.data as Record<string, unknown> | undefined;
|
|
1635
|
+
return data?.runId === runId && data.completionId === completionId && data.observed === true;
|
|
1636
|
+
});
|
|
1637
|
+
if (!accepted) return false;
|
|
1638
|
+
advanceDeliveryAnchor();
|
|
1639
|
+
observedExecutions.set(runId, completionId);
|
|
1640
|
+
while (observedExecutions.size > MAX_PENDING_DELIVERIES)
|
|
1641
|
+
observedExecutions.delete(observedExecutions.keys().next().value as string);
|
|
1642
|
+
if (pendingDeliveryMarkers.get(runId)?.completionId === completionId) pendingDeliveryMarkers.delete(runId);
|
|
1643
|
+
return true;
|
|
1644
|
+
} catch {
|
|
1645
|
+
return false;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
const rememberPendingDelivery = (
|
|
1650
|
+
runId: string,
|
|
1651
|
+
text: string,
|
|
1652
|
+
timestamp = Date.now(),
|
|
1653
|
+
executionGeneration?: number,
|
|
1654
|
+
completionId?: string,
|
|
1655
|
+
): void => {
|
|
1656
|
+
if (
|
|
1657
|
+
!runId ||
|
|
1658
|
+
timestamp + PENDING_DELIVERY_TTL_MS < Date.now() ||
|
|
1659
|
+
(completionId !== undefined && observedExecutions.get(runId) === completionId)
|
|
1660
|
+
)
|
|
1661
|
+
return;
|
|
1662
|
+
pendingDeliveryMarkers.set(runId, { text: text.slice(0, 8_000), timestamp, executionGeneration, completionId });
|
|
1554
1663
|
for (const [id, marker] of pendingDeliveryMarkers) {
|
|
1555
1664
|
if (marker.timestamp + PENDING_DELIVERY_TTL_MS < Date.now()) pendingDeliveryMarkers.delete(id);
|
|
1556
1665
|
}
|
|
@@ -1582,6 +1691,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1582
1691
|
!["completed", "failed", "stopped"].includes(run.status)
|
|
1583
1692
|
)
|
|
1584
1693
|
return;
|
|
1694
|
+
const completionId = completionReceipt(workflowEngine, runId);
|
|
1695
|
+
if (completionId !== undefined && observedExecutions.get(runId) === completionId) return;
|
|
1585
1696
|
const header = `Workflow ${run.runId} ${run.status}`;
|
|
1586
1697
|
const state = workflowEngine.getState(runId);
|
|
1587
1698
|
const finalResult = state?.result ?? run.finalResult;
|
|
@@ -1592,10 +1703,12 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1592
1703
|
: `result:\n${typeof finalResult === "string" ? finalResult : (JSON.stringify(finalResult, null, 2) ?? String(finalResult))}`);
|
|
1593
1704
|
const text = `${header}: ${body}`;
|
|
1594
1705
|
if (!ownsDeliveryBranch()) return;
|
|
1595
|
-
rememberPendingDelivery(runId, text);
|
|
1706
|
+
rememberPendingDelivery(runId, text, Date.now(), executionGeneration, completionId);
|
|
1596
1707
|
try {
|
|
1597
1708
|
pi.appendEntry(PENDING_DELIVERY_TYPE, {
|
|
1598
1709
|
runId,
|
|
1710
|
+
executionGeneration,
|
|
1711
|
+
completionId,
|
|
1599
1712
|
text: text.slice(0, 8_000),
|
|
1600
1713
|
status: run.status,
|
|
1601
1714
|
timestamp: Date.now(),
|
|
@@ -1633,22 +1746,37 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1633
1746
|
};
|
|
1634
1747
|
const hydratePendingDeliveries = (entries: SessionEntryLike[]): void => {
|
|
1635
1748
|
pendingDeliveryMarkers.clear();
|
|
1749
|
+
observedExecutions.clear();
|
|
1636
1750
|
for (const entry of entries) {
|
|
1637
1751
|
const shaped = entry as { type?: unknown; customType?: unknown; data?: unknown };
|
|
1638
1752
|
if (shaped.type !== "custom") continue;
|
|
1639
1753
|
const data = shaped.data as Record<string, unknown> | undefined;
|
|
1640
1754
|
if (typeof data?.runId !== "string") continue;
|
|
1641
|
-
if (shaped.customType === PENDING_DELIVERY_ACK_TYPE)
|
|
1642
|
-
|
|
1755
|
+
if (shaped.customType === PENDING_DELIVERY_ACK_TYPE) {
|
|
1756
|
+
// Explicit reads acknowledge exactly one durable terminal receipt.
|
|
1757
|
+
// Ordinary delivery ACKs still acknowledge the actual handed-off marker.
|
|
1758
|
+
if (
|
|
1759
|
+
data.observed !== true ||
|
|
1760
|
+
(typeof data.completionId === "string" &&
|
|
1761
|
+
pendingDeliveryMarkers.get(data.runId)?.completionId === data.completionId)
|
|
1762
|
+
)
|
|
1763
|
+
pendingDeliveryMarkers.delete(data.runId);
|
|
1764
|
+
} else if (shaped.customType === PENDING_DELIVERY_TYPE && typeof data.text === "string") {
|
|
1643
1765
|
rememberPendingDelivery(
|
|
1644
1766
|
data.runId,
|
|
1645
1767
|
data.text,
|
|
1646
1768
|
typeof data.timestamp === "number" ? data.timestamp : Date.now(),
|
|
1769
|
+
typeof data.executionGeneration === "number" ? data.executionGeneration : undefined,
|
|
1770
|
+
typeof data.completionId === "string" ? data.completionId : undefined,
|
|
1647
1771
|
);
|
|
1648
1772
|
}
|
|
1649
1773
|
}
|
|
1650
1774
|
};
|
|
1651
1775
|
const flushPendingDeliveries = (): void => {
|
|
1776
|
+
for (const [runId, marker] of pendingDeliveryMarkers) {
|
|
1777
|
+
if (marker.completionId !== undefined && observedExecutions.get(runId) === marker.completionId)
|
|
1778
|
+
pendingDeliveryMarkers.delete(runId);
|
|
1779
|
+
}
|
|
1652
1780
|
if (!canDeliver() || !deliveryContext || pendingDeliveryMarkers.size === 0) return;
|
|
1653
1781
|
if (typeof pi.sendMessage !== "function" || typeof pi.sendUserMessage !== "function") return;
|
|
1654
1782
|
const ctx = deliveryContext;
|