@henryqw/pi-subagent 27.5.3 → 27.5.4
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/dist/runner.d.ts +1 -0
- package/dist/runner.js +96 -72
- package/docs/orchestration.md +2 -0
- package/package.json +1 -1
package/dist/runner.d.ts
CHANGED
|
@@ -350,6 +350,7 @@ export declare class IsolatedRunner {
|
|
|
350
350
|
private releaseRejected;
|
|
351
351
|
private releaseSuperseded;
|
|
352
352
|
private cleanupPromoted;
|
|
353
|
+
private cleanupPromotedStage;
|
|
353
354
|
private requireExactIntegration;
|
|
354
355
|
listOrphanRequests(cwd: string): Promise<{
|
|
355
356
|
invalidIds: string[];
|
package/dist/runner.js
CHANGED
|
@@ -592,6 +592,8 @@ export function nextIntegrationAction(state) {
|
|
|
592
592
|
return `Call subagent_integrate refresh with generation ${generation.number + 1}, expectedTip ${JSON.stringify(generation.combinedTip)}, and identical expectedMain and newMain; then restage the candidates and validate again.`;
|
|
593
593
|
}
|
|
594
594
|
return undefined;
|
|
595
|
+
case "promoted":
|
|
596
|
+
return `Promotion is proven, but cleanup retained resources; resolve the recorded cleanup failures without discarding work, then call subagent_integrate cleanup with ${exact}.`;
|
|
595
597
|
default:
|
|
596
598
|
return undefined;
|
|
597
599
|
}
|
|
@@ -1062,6 +1064,8 @@ export class IsolatedRunner {
|
|
|
1062
1064
|
if (action.action === "reject" && allocationByKind(attempt, "agent")) {
|
|
1063
1065
|
if (await this.terminateWithSafety(handle, task, attempt, action.candidate, scope.signal))
|
|
1064
1066
|
task.status = "rejected";
|
|
1067
|
+
else
|
|
1068
|
+
this.attention(task, `Worker termination is unproved: ${attempt.termination.failure}`);
|
|
1065
1069
|
await this.saveProductive(handle);
|
|
1066
1070
|
}
|
|
1067
1071
|
if (action.action === "revise") {
|
|
@@ -1605,101 +1609,123 @@ export class IsolatedRunner {
|
|
|
1605
1609
|
// Promotion proves selection independently of worker or checkout cleanup.
|
|
1606
1610
|
for (const stage of generation.stages)
|
|
1607
1611
|
changesetTaskState(state, stage.taskId).status = "completed";
|
|
1612
|
+
const generationCleanup = generation.cleanup ??= [{ kind: "worktree", status: "pending" }, { kind: "branch", status: "pending" }];
|
|
1613
|
+
const stepLists = [...generation.stages.map((stage) => changesetTaskState(state, stage.taskId).attempts[stage.attempt - 1].cleanup),
|
|
1614
|
+
generationCleanup];
|
|
1615
|
+
// Record each failure on the step it blocks; other stages and the integration checkout continue.
|
|
1616
|
+
const blocked = (steps, error) => {
|
|
1617
|
+
const step = steps.find((item) => item.status === "running") ?? steps.find((item) => item.status !== "completed");
|
|
1618
|
+
if (step)
|
|
1619
|
+
step.failure = bounded(`Cleanup requires attention: ${errorText(error)}`);
|
|
1620
|
+
};
|
|
1608
1621
|
await this.saveProductive(handle);
|
|
1622
|
+
let main;
|
|
1609
1623
|
try {
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1624
|
+
main = await this.callProductive(handle, scope, (context) => this.gitRuntime.inspectMain({ root: state.root }, context));
|
|
1625
|
+
}
|
|
1626
|
+
catch (error) {
|
|
1627
|
+
for (const steps of stepLists)
|
|
1628
|
+
blocked(steps, error);
|
|
1629
|
+
}
|
|
1630
|
+
if (main && !sameIdentity(main, promoted)) {
|
|
1631
|
+
for (const steps of stepLists)
|
|
1632
|
+
blocked(steps, new Error("Main changed or became dirty after promotion; cleanup is blocked."));
|
|
1633
|
+
}
|
|
1634
|
+
else if (main) {
|
|
1613
1635
|
for (const stage of generation.stages) {
|
|
1614
1636
|
const task = changesetTaskState(state, stage.taskId);
|
|
1615
1637
|
const attempt = task.attempts[stage.attempt - 1];
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
const worker = allocationByKind(attempt, "worktree")?.worktree;
|
|
1619
|
-
if (!worker)
|
|
1620
|
-
throw new Error("Promoted worker lacks a proven checkout.");
|
|
1621
|
-
candidate.worker = "release_pending";
|
|
1622
|
-
await this.saveProductive(handle);
|
|
1623
|
-
if (attempt.termination?.status !== "terminated") {
|
|
1624
|
-
if (attempt.termination) {
|
|
1625
|
-
const result = await this.callProductive(handle, scope, (context) => this.hostRuntime.reconcileWorkerTermination({
|
|
1626
|
-
task: changesetTaskRequest(state, task.taskId), attempt,
|
|
1627
|
-
workerId: attempt.termination.workerId, candidate: stage.source,
|
|
1628
|
-
}, context));
|
|
1629
|
-
if (result.outcome !== "terminated")
|
|
1630
|
-
throw new Error("Worker termination cannot be proved; never replay termination.");
|
|
1631
|
-
attempt.termination = { status: "terminated", workerId: attempt.termination.workerId,
|
|
1632
|
-
candidate: stage.source, at: this.coordinatorRuntime.now() };
|
|
1633
|
-
await this.saveProductive(handle);
|
|
1634
|
-
}
|
|
1635
|
-
else if (!await this.terminateWithSafety(handle, task, attempt, stage.source, scope.signal)) {
|
|
1636
|
-
throw new Error("Worker termination was not proved.");
|
|
1637
|
-
}
|
|
1638
|
+
try {
|
|
1639
|
+
await this.cleanupPromotedStage(handle, generation, receipts, promoted, stage, task, attempt, scope);
|
|
1638
1640
|
}
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
+
catch (error) {
|
|
1642
|
+
blocked(attempt.cleanup, error);
|
|
1643
|
+
}
|
|
1644
|
+
await this.saveProductive(handle);
|
|
1645
|
+
}
|
|
1646
|
+
try {
|
|
1647
|
+
for (const step of generationCleanup) {
|
|
1641
1648
|
if (step.status === "completed")
|
|
1642
1649
|
continue;
|
|
1643
1650
|
step.status = "running";
|
|
1644
1651
|
await this.saveProductive(handle);
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
}, context));
|
|
1649
|
-
if (result.outcome === "blocked")
|
|
1650
|
-
throw new Error(result.failure);
|
|
1651
|
-
}
|
|
1652
|
-
else {
|
|
1653
|
-
const result = await this.callProductive(handle, scope, (context) => this.integrationGit.cleanup(state.root, generation.worktree, worker, generation.expectedMain, receipts, promoted, kind, context.signal, generation.correction));
|
|
1654
|
-
if (result.outcome !== "ready")
|
|
1655
|
-
throw new Error(result.failure);
|
|
1656
|
-
}
|
|
1652
|
+
const result = await this.callProductive(handle, scope, (context) => this.integrationGit.cleanup(state.root, generation.worktree, generation.worktree, generation.expectedMain, receipts, promoted, step.kind, context.signal, generation.correction));
|
|
1653
|
+
if (result.outcome !== "ready")
|
|
1654
|
+
throw new Error(result.failure);
|
|
1657
1655
|
step.status = "completed";
|
|
1658
1656
|
delete step.failure;
|
|
1659
1657
|
await this.saveProductive(handle);
|
|
1660
1658
|
}
|
|
1661
|
-
candidate.worker = "released";
|
|
1662
|
-
// Same-attempt revisions share this exact worker checkout; release historical
|
|
1663
|
-
// rejected tips only after the physical checkout has been proved removed.
|
|
1664
|
-
for (const old of state.integration.candidates.filter((item) => item.decision === "rejected"
|
|
1665
|
-
&& item.taskId === stage.taskId && item.attempt === stage.attempt))
|
|
1666
|
-
old.worker = "released";
|
|
1667
|
-
await this.saveProductive(handle);
|
|
1668
1659
|
}
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
for (const kind of ["worktree", "branch"]) {
|
|
1672
|
-
const step = generation.cleanup.find((item) => item.kind === kind);
|
|
1673
|
-
if (step.status === "completed")
|
|
1674
|
-
continue;
|
|
1675
|
-
step.status = "running";
|
|
1676
|
-
await this.saveProductive(handle);
|
|
1677
|
-
const result = await this.callProductive(handle, scope, (context) => this.integrationGit.cleanup(state.root, generation.worktree, generation.worktree, generation.expectedMain, receipts, promoted, kind, context.signal, generation.correction));
|
|
1678
|
-
if (result.outcome !== "ready")
|
|
1679
|
-
throw new Error(result.failure);
|
|
1680
|
-
step.status = "completed";
|
|
1681
|
-
delete step.failure;
|
|
1682
|
-
await this.saveProductive(handle);
|
|
1660
|
+
catch (error) {
|
|
1661
|
+
blocked(generationCleanup, error);
|
|
1683
1662
|
}
|
|
1684
1663
|
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
?? generation.stages.flatMap((stage) => changesetTaskState(state, stage.taskId).attempts[stage.attempt - 1].cleanup)
|
|
1689
|
-
.find((step) => step.status === "running");
|
|
1690
|
-
if (pending)
|
|
1691
|
-
pending.failure = bounded(`Cleanup requires attention: ${errorText(error)}`);
|
|
1692
|
-
await this.saveProductive(handle);
|
|
1693
|
-
}
|
|
1694
|
-
if (state.tasks.every((task) => task.status === "completed" || task.status === "rejected")
|
|
1664
|
+
// Promotion remains proven; the request stays visible until every promoted resource is released.
|
|
1665
|
+
if (stepLists.every((steps) => steps.every((step) => step.status === "completed"))
|
|
1666
|
+
&& state.tasks.every((task) => task.status === "completed" || task.status === "rejected")
|
|
1695
1667
|
&& state.integration.candidates.every((candidate) => candidate.decision || generation.stages.some((stage) => stage.taskId === candidate.taskId && stage.attempt === candidate.attempt && sameIdentity(stage.source, candidate.tip)))) {
|
|
1696
1668
|
state.final = { status: "passed", identity: promoted, checks: generation.checks,
|
|
1697
1669
|
...(generation.review ? { review: generation.review } : {}) };
|
|
1698
1670
|
state.status = "completed";
|
|
1699
1671
|
state.accepted = true;
|
|
1700
1672
|
state.acceptedAt = this.coordinatorRuntime.now();
|
|
1673
|
+
}
|
|
1674
|
+
await this.saveProductive(handle);
|
|
1675
|
+
}
|
|
1676
|
+
async cleanupPromotedStage(handle, generation, receipts, promoted, stage, task, attempt, scope) {
|
|
1677
|
+
const state = handle.state;
|
|
1678
|
+
const candidate = state.integration.candidates.find((item) => item.taskId === stage.taskId && item.attempt === stage.attempt
|
|
1679
|
+
&& sameIdentity(item.tip, stage.source));
|
|
1680
|
+
const worker = allocationByKind(attempt, "worktree")?.worktree;
|
|
1681
|
+
if (!worker)
|
|
1682
|
+
throw new Error("Promoted worker lacks a proven checkout.");
|
|
1683
|
+
candidate.worker = "release_pending";
|
|
1684
|
+
await this.saveProductive(handle);
|
|
1685
|
+
if (attempt.termination?.status !== "terminated") {
|
|
1686
|
+
if (attempt.termination) {
|
|
1687
|
+
const result = await this.callProductive(handle, scope, (context) => this.hostRuntime.reconcileWorkerTermination({
|
|
1688
|
+
task: changesetTaskRequest(state, task.taskId), attempt,
|
|
1689
|
+
workerId: attempt.termination.workerId, candidate: stage.source,
|
|
1690
|
+
}, context));
|
|
1691
|
+
if (result.outcome !== "terminated")
|
|
1692
|
+
throw new Error("Worker termination cannot be proved; never replay termination.");
|
|
1693
|
+
attempt.termination = { status: "terminated", workerId: attempt.termination.workerId,
|
|
1694
|
+
candidate: stage.source, at: this.coordinatorRuntime.now() };
|
|
1695
|
+
await this.saveProductive(handle);
|
|
1696
|
+
}
|
|
1697
|
+
else if (!await this.terminateWithSafety(handle, task, attempt, stage.source, scope.signal)) {
|
|
1698
|
+
throw new Error(`Worker termination was not proved: ${attempt.termination.failure}`);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
for (const kind of CLEANUP_KINDS) {
|
|
1702
|
+
const step = attempt.cleanup.find((item) => item.kind === kind);
|
|
1703
|
+
if (step.status === "completed")
|
|
1704
|
+
continue;
|
|
1705
|
+
step.status = "running";
|
|
1706
|
+
await this.saveProductive(handle);
|
|
1707
|
+
if (kind === "worker_tab" || kind === "workspace") {
|
|
1708
|
+
const result = await this.callProductive(handle, scope, (context) => this.hostRuntime.cleanupHost({
|
|
1709
|
+
requestId: state.request.id, kind, task: changesetTaskRequest(state, task.taskId), attempt,
|
|
1710
|
+
}, context));
|
|
1711
|
+
if (result.outcome === "blocked")
|
|
1712
|
+
throw new Error(result.failure);
|
|
1713
|
+
}
|
|
1714
|
+
else {
|
|
1715
|
+
const result = await this.callProductive(handle, scope, (context) => this.integrationGit.cleanup(state.root, generation.worktree, worker, generation.expectedMain, receipts, promoted, kind, context.signal, generation.correction));
|
|
1716
|
+
if (result.outcome !== "ready")
|
|
1717
|
+
throw new Error(result.failure);
|
|
1718
|
+
}
|
|
1719
|
+
step.status = "completed";
|
|
1720
|
+
delete step.failure;
|
|
1701
1721
|
await this.saveProductive(handle);
|
|
1702
1722
|
}
|
|
1723
|
+
candidate.worker = "released";
|
|
1724
|
+
// Same-attempt revisions share this exact worker checkout; release historical
|
|
1725
|
+
// rejected tips only after the physical checkout has been proved removed.
|
|
1726
|
+
for (const old of state.integration.candidates.filter((item) => item.decision === "rejected"
|
|
1727
|
+
&& item.taskId === stage.taskId && item.attempt === stage.attempt))
|
|
1728
|
+
old.worker = "released";
|
|
1703
1729
|
}
|
|
1704
1730
|
async requireExactIntegration(handle, generation, receipts, scope, allowDirtyMain = false) {
|
|
1705
1731
|
const root = handle.state.root;
|
|
@@ -2736,7 +2762,6 @@ export class IsolatedRunner {
|
|
|
2736
2762
|
if (result.outcome !== "terminated") {
|
|
2737
2763
|
const failure = boundedRuntimeText(result.failure, "Worker termination failure");
|
|
2738
2764
|
attempt.termination = { status: "unknown", workerId, candidate, failure };
|
|
2739
|
-
this.attention(task, `Worker termination is unproved: ${failure}`);
|
|
2740
2765
|
return false;
|
|
2741
2766
|
}
|
|
2742
2767
|
attempt.termination = { status: "terminated", workerId, candidate, at: this.coordinatorRuntime.now() };
|
|
@@ -2745,7 +2770,6 @@ export class IsolatedRunner {
|
|
|
2745
2770
|
catch (error) {
|
|
2746
2771
|
this.rethrowStopped(error);
|
|
2747
2772
|
attempt.termination = { status: "unknown", workerId, candidate, failure: errorText(error) };
|
|
2748
|
-
this.attention(task, `Worker termination is unproved: ${errorText(error)}`);
|
|
2749
2773
|
return false;
|
|
2750
2774
|
}
|
|
2751
2775
|
finally {
|
package/docs/orchestration.md
CHANGED
|
@@ -295,6 +295,8 @@ Rejection invalidates dependent work. Rejection and revision ignore dependent wo
|
|
|
295
295
|
|
|
296
296
|
Use the generation's exact `combinedTip` as `expectedTip`. `advance` dispatches ready dependents from that snapshot. `correct` records one clean committed correction after a definitive failed check or review. `reconcile` inspects interrupted validation or promotion without replaying uncertain promotion. `cleanup` finishes proved post-promotion cleanup.
|
|
297
297
|
|
|
298
|
+
Post-promotion cleanup handles each promoted stage separately. A blocked termination or cleanup step records its failure on that step, and cleanup continues with the other stages and then the promoted integration checkout. Clean Main at the promoted identity is a shared precondition; if Main changed, every pending step records the blocker. Dirty, untracked, or ignored files block worktree removal and stay preserved. Until every promoted termination and cleanup step completes, the request stays `needs_attention` with promotion proven, selected tasks `completed`, and not accepted, so `/subagent` lists it. Resolve the recorded blockers, then call `cleanup` again; the request becomes `completed` and accepted only after cleanup completes.
|
|
299
|
+
|
|
298
300
|
Validation acknowledges only after saving the exact generation's validation intent. Checks and final judgment then run in the background. Later turn cancellation does not cancel acknowledged validation; session replacement or shutdown does. Acknowledgement is not passing evidence. Inspect status if the completion follow-up fails.
|
|
299
301
|
|
|
300
302
|
#### Refresh
|