@osolmaz/pi-workflows 0.10.0 → 0.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/README.md +23 -12
- package/dist/builtins/autodoc.workflow.d.ts +58 -0
- package/dist/builtins/autodoc.workflow.js +266 -0
- package/dist/builtins/autodoc.workflow.js.map +1 -0
- package/dist/builtins/autoimplement.workflow.d.ts +210 -11
- package/dist/builtins/autoimplement.workflow.js +245 -13
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/{autodevise.workflow.d.ts → autoplan.workflow.d.ts} +9 -9
- package/dist/builtins/{autodevise.workflow.js → autoplan.workflow.js} +20 -20
- package/dist/builtins/autoplan.workflow.js.map +1 -0
- package/dist/builtins/catalog.js +8 -4
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +6 -3
- package/dist/builtins/index.js +5 -2
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/monitor.workflow.d.ts +4 -0
- package/dist/builtins/monitor.workflow.js +134 -9
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/plan-approval.workflow.d.ts +83 -0
- package/dist/builtins/plan-approval.workflow.js +148 -0
- package/dist/builtins/plan-approval.workflow.js.map +1 -0
- package/dist/builtins/plan-presentation.d.ts +7 -0
- package/dist/builtins/plan-presentation.js +44 -0
- package/dist/builtins/plan-presentation.js.map +1 -0
- package/dist/extension/decision-channels.d.ts +132 -0
- package/dist/extension/decision-channels.js +1082 -0
- package/dist/extension/decision-channels.js.map +1 -0
- package/dist/extension/index.d.ts +1 -0
- package/dist/extension/index.js +426 -11
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/widget.js +26 -2
- package/dist/extension/widget.js.map +1 -1
- package/dist/extension/workflow-tool.d.ts +1 -37
- package/dist/extension/workflow-tool.js +1 -42
- package/dist/extension/workflow-tool.js.map +1 -1
- package/dist/host/rpc-bridge.js +4 -21
- package/dist/host/rpc-bridge.js.map +1 -1
- package/dist/render/graph-render.js +33 -8
- package/dist/render/graph-render.js.map +1 -1
- package/dist/viewer/render.js +36 -2
- package/dist/viewer/render.js.map +1 -1
- package/dist/workflows/composition.js +15 -1
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/decision-presentation.d.ts +18 -0
- package/dist/workflows/decision-presentation.js +417 -0
- package/dist/workflows/decision-presentation.js.map +1 -0
- package/dist/workflows/engine.d.ts +2 -1
- package/dist/workflows/engine.js +86 -4
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +87 -0
- package/dist/workflows/human-decision.js +583 -0
- package/dist/workflows/human-decision.js.map +1 -0
- package/dist/workflows/index.d.ts +3 -1
- package/dist/workflows/index.js +2 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/schema.js +15 -0
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.js +7 -0
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +72 -0
- package/dist/workflows/tool-input.js +141 -0
- package/dist/workflows/tool-input.js.map +1 -0
- package/dist/workflows/types.d.ts +223 -0
- package/docs/HUMAN_DECISIONS.md +371 -0
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +323 -0
- package/docs/MONITOR.md +17 -6
- package/docs/WORKFLOW_COMPOSITION.md +13 -6
- package/docs/plans/2026-08-19-human-decision-gates-plan.md +296 -0
- package/docs/plans/2026-08-19-human-decision-presentations-plan.md +172 -0
- package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +45 -0
- package/docs/plans/2026-08-19-workflow-composition-plan.md +25 -17
- package/docs/run-bundles.md +23 -2
- package/docs/workflows.md +43 -5
- package/examples/workflows/approved-plan.workflow.ts +58 -0
- package/examples/workflows/autoplan.workflow.ts +1 -0
- package/examples/workflows/human-decision.workflow.ts +62 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +2 -1
- package/schemas/decision-presentation-v1.schema.json +83 -0
- package/schemas/human-decision-accepted-v1.schema.json +44 -0
- package/schemas/human-decision-accepted-v2.schema.json +50 -0
- package/schemas/human-decision-answer-attempt-v1.schema.json +37 -0
- package/schemas/human-decision-cancellation-v1.schema.json +15 -0
- package/schemas/human-decision-continuation-v1.schema.json +16 -0
- package/schemas/human-decision-delivery-v1.schema.json +28 -0
- package/schemas/human-decision-delivery-v2.schema.json +36 -0
- package/schemas/human-decision-receipt-v1.schema.json +33 -0
- package/schemas/human-decision-receipt-v2.schema.json +39 -0
- package/schemas/human-decision-request-v1.schema.json +59 -0
- package/schemas/human-decision-request-v2.schema.json +69 -0
- package/schemas/human-decision-resolution-v1.schema.json +27 -0
- package/schemas/human-decision-resolution-v2.schema.json +27 -0
- package/schemas/human-decision-settlement-v1.schema.json +28 -0
- package/skills/autodoc/SKILL.md +43 -0
- package/skills/autoimplement/SKILL.md +58 -0
- package/skills/autoimplement/agents/openai.yaml +4 -0
- package/skills/autoplan/SKILL.md +25 -0
- package/skills/monitor/SKILL.md +3 -1
- package/skills/pi-workflows/SKILL.md +3 -3
- package/src/builtins/autodoc.workflow.ts +325 -0
- package/src/builtins/autoimplement.workflow.ts +281 -14
- package/src/builtins/{autodevise.workflow.ts → autoplan.workflow.ts} +32 -32
- package/src/builtins/catalog.ts +8 -4
- package/src/builtins/index.ts +25 -6
- package/src/builtins/monitor.workflow.ts +162 -8
- package/src/builtins/plan-approval.workflow.ts +190 -0
- package/src/builtins/plan-presentation.ts +57 -0
- package/src/extension/decision-channels.ts +1533 -0
- package/src/extension/index.ts +499 -9
- package/src/extension/widget.ts +39 -2
- package/src/extension/workflow-tool.ts +5 -81
- package/src/host/rpc-bridge.ts +7 -30
- package/src/render/graph-render.ts +40 -3
- package/src/viewer/render.ts +56 -3
- package/src/workflows/composition.ts +16 -1
- package/src/workflows/decision-presentation.ts +501 -0
- package/src/workflows/engine.ts +108 -4
- package/src/workflows/human-decision.ts +819 -0
- package/src/workflows/index.ts +69 -0
- package/src/workflows/schema.ts +17 -0
- package/src/workflows/store.ts +8 -0
- package/src/workflows/tool-input.ts +202 -0
- package/src/workflows/types.ts +265 -0
- package/dist/builtins/autodevise.workflow.js.map +0 -1
- package/examples/workflows/autodevise.workflow.ts +0 -1
package/src/extension/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { builtinWorkflowCatalog } from "../builtins/catalog.js";
|
|
|
6
6
|
import { projectControllerStorePath, SqliteControllerStore } from "../controllers/index.js";
|
|
7
7
|
import type { JsonObject } from "../controllers/types.js";
|
|
8
8
|
import type { WorkflowSchedulerResult } from "../controllers/workflows.js";
|
|
9
|
+
import { humanDecisionChannelRequest } from "../workflows/decision-presentation.js";
|
|
9
10
|
import { WorkflowEngine } from "../workflows/engine.js";
|
|
10
11
|
import {
|
|
11
12
|
ClaimLostError,
|
|
@@ -13,6 +14,11 @@ import {
|
|
|
13
14
|
isClaimLostError,
|
|
14
15
|
TimeoutError,
|
|
15
16
|
} from "../workflows/errors.js";
|
|
17
|
+
import {
|
|
18
|
+
HumanDecisionStore,
|
|
19
|
+
createHumanDecisionAttemptId,
|
|
20
|
+
validateHumanDecisionResponse,
|
|
21
|
+
} from "../workflows/human-decision.js";
|
|
16
22
|
import { discoverWorkflows, resolveWorkflowRef } from "../workflows/loader.js";
|
|
17
23
|
import { migrateLegacyWorkflowSources } from "../workflows/migrate-sources.js";
|
|
18
24
|
import { appendProgressHistory, progressRecordsFromTrace } from "../workflows/progress.js";
|
|
@@ -25,7 +31,10 @@ import {
|
|
|
25
31
|
createDefinitionSnapshot,
|
|
26
32
|
} from "../workflows/store.js";
|
|
27
33
|
import type {
|
|
34
|
+
AcceptedHumanDecision,
|
|
28
35
|
AgentStepContract,
|
|
36
|
+
HumanDecisionRequest,
|
|
37
|
+
HumanDecisionSubmission,
|
|
29
38
|
WorkflowDefinition,
|
|
30
39
|
WorkflowDefinitionSnapshot,
|
|
31
40
|
WorkflowRunResult,
|
|
@@ -37,6 +46,18 @@ import {
|
|
|
37
46
|
parseControllerArgs,
|
|
38
47
|
type PiChildWorkflowStarter,
|
|
39
48
|
} from "./controller-host.js";
|
|
49
|
+
import {
|
|
50
|
+
PiDecisionChannel,
|
|
51
|
+
TelegramDecisionChannel,
|
|
52
|
+
audienceChannels,
|
|
53
|
+
createTelegramChannels,
|
|
54
|
+
loadDecisionChannelConfig,
|
|
55
|
+
verifyTelegramTokenFile,
|
|
56
|
+
writeDecisionChannelProfile,
|
|
57
|
+
type DecisionChannelConfig,
|
|
58
|
+
type HumanDecisionChannelAnswer,
|
|
59
|
+
type SettledHumanDecision,
|
|
60
|
+
} from "./decision-channels.js";
|
|
40
61
|
import { ConversationStepExecutor } from "./executor.js";
|
|
41
62
|
import {
|
|
42
63
|
HerdrWorkflowViewer,
|
|
@@ -56,7 +77,27 @@ import {
|
|
|
56
77
|
type WorkflowAgentStepMessageDetails,
|
|
57
78
|
} from "./step-message.js";
|
|
58
79
|
import { buildWidgetView } from "./widget.js";
|
|
59
|
-
import {
|
|
80
|
+
import { parseWorkflowToolInput, WorkflowToolParameters } from "./workflow-tool.js";
|
|
81
|
+
|
|
82
|
+
export {
|
|
83
|
+
PiDecisionChannel,
|
|
84
|
+
TelegramDecisionChannel,
|
|
85
|
+
audienceChannels,
|
|
86
|
+
createTelegramChannels,
|
|
87
|
+
decisionConfigDir,
|
|
88
|
+
loadDecisionChannelConfig,
|
|
89
|
+
verifyTelegramTokenFile,
|
|
90
|
+
writeDecisionChannelProfile,
|
|
91
|
+
type DecisionChannelConfig,
|
|
92
|
+
type DecisionCredentialConfig,
|
|
93
|
+
type HumanDecisionChannel,
|
|
94
|
+
type HumanDecisionChannelAnswer,
|
|
95
|
+
type HumanDecisionDeliveryResult,
|
|
96
|
+
type LoadedDecisionChannelConfig,
|
|
97
|
+
type PiDecisionUi,
|
|
98
|
+
type SettledHumanDecision,
|
|
99
|
+
type TelegramFetch,
|
|
100
|
+
} from "./decision-channels.js";
|
|
60
101
|
|
|
61
102
|
const RUN_CLAIM_LEASE_MS = 30_000;
|
|
62
103
|
const RUN_CLAIM_RENEW_MS = 10_000;
|
|
@@ -115,6 +156,20 @@ type ActiveRun = {
|
|
|
115
156
|
parentRunId?: string | undefined;
|
|
116
157
|
};
|
|
117
158
|
|
|
159
|
+
function humanDecisionRequest(value: unknown): HumanDecisionRequest | null {
|
|
160
|
+
const schema =
|
|
161
|
+
value !== null && typeof value === "object"
|
|
162
|
+
? (value as { schema?: unknown }).schema
|
|
163
|
+
: undefined;
|
|
164
|
+
if (
|
|
165
|
+
schema !== "pi-workflows.human-decision-request.v1" &&
|
|
166
|
+
schema !== "pi-workflows.human-decision-request.v2"
|
|
167
|
+
) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
return value as HumanDecisionRequest;
|
|
171
|
+
}
|
|
172
|
+
|
|
118
173
|
type StartRunOptions = {
|
|
119
174
|
runId?: string;
|
|
120
175
|
childKey?: string;
|
|
@@ -122,8 +177,10 @@ type StartRunOptions = {
|
|
|
122
177
|
presentation?: boolean;
|
|
123
178
|
quiet?: boolean;
|
|
124
179
|
signal?: AbortSignal;
|
|
125
|
-
/** Continue a checkpointed run: input becomes the answer payload. */
|
|
180
|
+
/** Continue a checkpointed run: input becomes the legacy answer payload. */
|
|
126
181
|
parentRunId?: string;
|
|
182
|
+
/** Accepted verified-human answer for a protected decision continuation. */
|
|
183
|
+
humanDecision?: AcceptedHumanDecision;
|
|
127
184
|
/** Resume a parked run at its stopped node instead of starting fresh. */
|
|
128
185
|
resume?: boolean;
|
|
129
186
|
/** An existing queue claim token, when the caller already claimed the run. */
|
|
@@ -266,6 +323,16 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
266
323
|
// One runner identity per session; it names this session in run claims.
|
|
267
324
|
const runnerId = randomUUID();
|
|
268
325
|
let runQueueStore: SqliteControllerStore | null = null;
|
|
326
|
+
let decisionChannelConfig: DecisionChannelConfig | null = null;
|
|
327
|
+
let telegramDecisionChannels = new Map<string, TelegramDecisionChannel>();
|
|
328
|
+
const activePiDecisionChannels = new Map<string, PiDecisionChannel>();
|
|
329
|
+
const settleHumanDecisionChannels = async (decision: SettledHumanDecision): Promise<void> => {
|
|
330
|
+
const piChannel = activePiDecisionChannels.get(decision.decisionId);
|
|
331
|
+
await Promise.allSettled([
|
|
332
|
+
...(piChannel === undefined ? [] : [piChannel.settle(decision)]),
|
|
333
|
+
...[...telegramDecisionChannels.values()].map(async (channel) => channel.settle(decision)),
|
|
334
|
+
]);
|
|
335
|
+
};
|
|
269
336
|
const migrationBlockedRuns = new Set<string>();
|
|
270
337
|
const ensureRunQueueStore = (cwd: string): SqliteControllerStore => {
|
|
271
338
|
runQueueStore ??= new SqliteControllerStore(projectControllerStorePath(cwd));
|
|
@@ -276,6 +343,8 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
276
343
|
// Run events remain an audit feed and never enter a conversation.
|
|
277
344
|
let syncArmed = false;
|
|
278
345
|
let runSyncTimer: ReturnType<typeof setInterval> | null = null;
|
|
346
|
+
let decisionRecoveryTimer: ReturnType<typeof setInterval> | null = null;
|
|
347
|
+
let decisionRecoveryActive = false;
|
|
279
348
|
|
|
280
349
|
const recordRunEvent = (event: {
|
|
281
350
|
runId: string;
|
|
@@ -734,9 +803,12 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
734
803
|
if (state.status === "waiting" && run.childKey === undefined) {
|
|
735
804
|
lastWaitingRunId = run.runId;
|
|
736
805
|
}
|
|
806
|
+
const pendingDecision = humanDecisionRequest(state.finalOutput);
|
|
737
807
|
const summary =
|
|
738
808
|
state.status === "waiting" && state.waitingOn
|
|
739
|
-
?
|
|
809
|
+
? pendingDecision === null
|
|
810
|
+
? `Workflow ${state.workflowName} parked at checkpoint ${state.waitingOn} — answer with /workflow answer <json> (run ${state.runId})`
|
|
811
|
+
: `Workflow ${state.workflowName} is waiting for a verified human decision: ${pendingDecision.title} (run ${state.runId})`
|
|
740
812
|
: `Workflow ${state.workflowName} ${state.status} (run ${state.runId})${
|
|
741
813
|
state.error !== undefined ? `: ${state.error.slice(0, MAX_STATUS_ERROR_CHARS)}` : ""
|
|
742
814
|
}`;
|
|
@@ -757,6 +829,28 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
757
829
|
notify(ctx, `Could not record child workflow completion: ${errorMessage(error)}`, "warning");
|
|
758
830
|
}
|
|
759
831
|
void presentRun(ctx, run, state);
|
|
832
|
+
if (pendingDecision !== null && state.status === "waiting") {
|
|
833
|
+
const channels = audienceChannels(decisionChannelConfig, pendingDecision.audience);
|
|
834
|
+
let available = false;
|
|
835
|
+
if (ctx.mode === "tui" && channels.includes("pi")) {
|
|
836
|
+
available = true;
|
|
837
|
+
queueMicrotask(() => void promptHumanDecision(ctx, pendingDecision));
|
|
838
|
+
}
|
|
839
|
+
for (const channelId of channels) {
|
|
840
|
+
const channel = telegramDecisionChannels.get(channelId);
|
|
841
|
+
if (channel !== undefined) {
|
|
842
|
+
available = true;
|
|
843
|
+
queueMicrotask(() => void channel.deliver(humanDecisionChannelRequest(pendingDecision)));
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (!available) {
|
|
847
|
+
notify(
|
|
848
|
+
ctx,
|
|
849
|
+
`Human decision ${pendingDecision.title} remains waiting because its audience has no available channel.`,
|
|
850
|
+
"warning",
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
760
854
|
};
|
|
761
855
|
|
|
762
856
|
const startRun = async (
|
|
@@ -1004,6 +1098,9 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1004
1098
|
: engine.continueRun(workflow, options.parentRunId, input, {
|
|
1005
1099
|
workflowSource,
|
|
1006
1100
|
runId,
|
|
1101
|
+
...(options.humanDecision !== undefined
|
|
1102
|
+
? { humanDecision: options.humanDecision }
|
|
1103
|
+
: {}),
|
|
1007
1104
|
})
|
|
1008
1105
|
)
|
|
1009
1106
|
.then((result) => finishRun(ctx, run, result))
|
|
@@ -1279,7 +1376,43 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1279
1376
|
notify(ctx, result.message, result.level);
|
|
1280
1377
|
};
|
|
1281
1378
|
|
|
1282
|
-
const
|
|
1379
|
+
const cancelHumanDecision = async (request: HumanDecisionRequest): Promise<void> => {
|
|
1380
|
+
const store = new HumanDecisionStore(new WorkflowRunStore().outputRoot);
|
|
1381
|
+
await store.cancel(request, "cancelled");
|
|
1382
|
+
const cancellation = await store.readCancellation(request.decisionId);
|
|
1383
|
+
if (cancellation !== null) await settleHumanDecisionChannels(cancellation);
|
|
1384
|
+
lastWaitingRunId = null;
|
|
1385
|
+
};
|
|
1386
|
+
|
|
1387
|
+
const findOwnedWaitingHumanDecision = async (
|
|
1388
|
+
ctx: ExtensionContext,
|
|
1389
|
+
): Promise<{ state: WorkflowRunState; request: HumanDecisionRequest } | null> => {
|
|
1390
|
+
const rows = ensureRunQueueStore(ctx.cwd).listWorkflowRuns();
|
|
1391
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
1392
|
+
const owned = new Set(
|
|
1393
|
+
rows
|
|
1394
|
+
.filter((row) => row.originSessionId === null || row.originSessionId === sessionId)
|
|
1395
|
+
.map((row) => row.runId),
|
|
1396
|
+
);
|
|
1397
|
+
const continued = new Set(
|
|
1398
|
+
rows.map((row) => row.parentRunId).filter((parent): parent is string => parent !== null),
|
|
1399
|
+
);
|
|
1400
|
+
const bundles = await listRunBundles(new WorkflowRunStore().outputRoot);
|
|
1401
|
+
for (const bundle of bundles) {
|
|
1402
|
+
if (
|
|
1403
|
+
bundle.state.status !== "waiting" ||
|
|
1404
|
+
!owned.has(bundle.state.runId) ||
|
|
1405
|
+
continued.has(bundle.state.runId)
|
|
1406
|
+
) {
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
const request = humanDecisionRequest(bundle.state.finalOutput);
|
|
1410
|
+
if (request !== null) return { state: bundle.state, request };
|
|
1411
|
+
}
|
|
1412
|
+
return null;
|
|
1413
|
+
};
|
|
1414
|
+
|
|
1415
|
+
const cancelWorkflowControl = async (ctx: ExtensionContext): Promise<WorkflowControlResult> => {
|
|
1283
1416
|
if (activeRun) {
|
|
1284
1417
|
const workflowName = activeRun.workflowName;
|
|
1285
1418
|
const runId = activeRun.runId;
|
|
@@ -1299,6 +1432,16 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1299
1432
|
}
|
|
1300
1433
|
if (widgetSource) {
|
|
1301
1434
|
const { state } = widgetSource;
|
|
1435
|
+
const request = humanDecisionRequest(state.finalOutput);
|
|
1436
|
+
if (state.status === "waiting" && request !== null) {
|
|
1437
|
+
await cancelHumanDecision(request);
|
|
1438
|
+
clearWidgetTimer();
|
|
1439
|
+
clearWidget(ctx);
|
|
1440
|
+
return {
|
|
1441
|
+
message: `Cancelled the pending human decision for workflow ${state.workflowName}.`,
|
|
1442
|
+
details: { action: "cancel", workflowName: state.workflowName, runId: state.runId },
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1302
1445
|
clearWidgetTimer();
|
|
1303
1446
|
clearWidget(ctx);
|
|
1304
1447
|
const detail =
|
|
@@ -1310,6 +1453,18 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1310
1453
|
details: { action: "clear", workflowName: state.workflowName, runId: state.runId },
|
|
1311
1454
|
};
|
|
1312
1455
|
}
|
|
1456
|
+
const recovered = await findOwnedWaitingHumanDecision(ctx);
|
|
1457
|
+
if (recovered !== null) {
|
|
1458
|
+
await cancelHumanDecision(recovered.request);
|
|
1459
|
+
return {
|
|
1460
|
+
message: `Cancelled the pending human decision for workflow ${recovered.state.workflowName}.`,
|
|
1461
|
+
details: {
|
|
1462
|
+
action: "cancel",
|
|
1463
|
+
workflowName: recovered.state.workflowName,
|
|
1464
|
+
runId: recovered.state.runId,
|
|
1465
|
+
},
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1313
1468
|
return {
|
|
1314
1469
|
message: "No workflow is running.",
|
|
1315
1470
|
details: { action: "cancel", active: false },
|
|
@@ -1421,6 +1576,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1421
1576
|
const resolveWaitingWorkflow = async (
|
|
1422
1577
|
ctx: ExtensionContext,
|
|
1423
1578
|
requestedRunId?: string,
|
|
1579
|
+
allowOtherSession = false,
|
|
1424
1580
|
): Promise<{ parentRunId: string; workflowRef: string }> => {
|
|
1425
1581
|
let parentRunId = requestedRunId ?? lastWaitingRunId;
|
|
1426
1582
|
if (parentRunId === null) {
|
|
@@ -1448,6 +1604,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1448
1604
|
}
|
|
1449
1605
|
const queueRecord = ensureRunQueueStore(ctx.cwd).getWorkflowRun(parentRunId);
|
|
1450
1606
|
if (
|
|
1607
|
+
!allowOtherSession &&
|
|
1451
1608
|
queueRecord?.originSessionId !== null &&
|
|
1452
1609
|
queueRecord?.originSessionId !== undefined &&
|
|
1453
1610
|
queueRecord.originSessionId !== ctx.sessionManager.getSessionId()
|
|
@@ -1478,14 +1635,103 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1478
1635
|
ctx: ExtensionContext,
|
|
1479
1636
|
input: unknown,
|
|
1480
1637
|
requestedRunId?: string,
|
|
1638
|
+
verified?: HumanDecisionChannelAnswer,
|
|
1481
1639
|
): Promise<WorkflowControlResult> => {
|
|
1482
|
-
const waiting = await resolveWaitingWorkflow(ctx, requestedRunId);
|
|
1483
|
-
const
|
|
1640
|
+
const waiting = await resolveWaitingWorkflow(ctx, requestedRunId, verified !== undefined);
|
|
1641
|
+
const parent = await readRunBundle(new WorkflowRunStore().runDirFor(waiting.parentRunId));
|
|
1642
|
+
if (parent === null) throw new Error(`Workflow run ${waiting.parentRunId} is unreadable.`);
|
|
1643
|
+
const request = humanDecisionRequest(parent.state.finalOutput);
|
|
1644
|
+
let accepted: AcceptedHumanDecision | undefined;
|
|
1645
|
+
let continuationInput = input;
|
|
1646
|
+
let continuationRunId: string | undefined;
|
|
1647
|
+
if (request !== null) {
|
|
1648
|
+
if (
|
|
1649
|
+
verified !== undefined &&
|
|
1650
|
+
(verified.request.decisionId !== request.decisionId ||
|
|
1651
|
+
verified.request.requestDigest !== request.requestDigest)
|
|
1652
|
+
) {
|
|
1653
|
+
throw new Error("Verified human answer does not match the waiting decision.");
|
|
1654
|
+
}
|
|
1655
|
+
const response = validateHumanDecisionResponse(request, verified?.response ?? input);
|
|
1656
|
+
const submission: HumanDecisionSubmission = {
|
|
1657
|
+
decisionId: request.decisionId,
|
|
1658
|
+
requestDigest: request.requestDigest,
|
|
1659
|
+
...response,
|
|
1660
|
+
source: verified?.source ?? {
|
|
1661
|
+
channel: "pi",
|
|
1662
|
+
actorId: ctx.sessionManager.getSessionId(),
|
|
1663
|
+
eventId: createHumanDecisionAttemptId(),
|
|
1664
|
+
},
|
|
1665
|
+
idempotencyKey: verified?.idempotencyKey ?? createHumanDecisionAttemptId(),
|
|
1666
|
+
};
|
|
1667
|
+
const store = new HumanDecisionStore(new WorkflowRunStore().outputRoot);
|
|
1668
|
+
const acceptance = await store.accept(request, submission);
|
|
1669
|
+
if (acceptance.status === "conflict") {
|
|
1670
|
+
throw new Error("That human decision was already answered differently.");
|
|
1671
|
+
}
|
|
1672
|
+
accepted = acceptance.decision;
|
|
1673
|
+
continuationInput = parent.state.input;
|
|
1674
|
+
continuationRunId = `continuation-${request.decisionId.slice("decision-".length)}`;
|
|
1675
|
+
}
|
|
1676
|
+
if (request !== null && accepted !== undefined && verified !== undefined) {
|
|
1677
|
+
const queueRecord = ensureRunQueueStore(ctx.cwd).getWorkflowRun(waiting.parentRunId);
|
|
1678
|
+
const currentSessionId = ctx.sessionManager.getSessionId();
|
|
1679
|
+
if (
|
|
1680
|
+
queueRecord === undefined ||
|
|
1681
|
+
(queueRecord.originSessionId !== null && queueRecord.originSessionId !== currentSessionId)
|
|
1682
|
+
) {
|
|
1683
|
+
await settleHumanDecisionChannels(accepted);
|
|
1684
|
+
return {
|
|
1685
|
+
message: "Human decision accepted; its owning Pi session will continue the workflow.",
|
|
1686
|
+
details: {
|
|
1687
|
+
action: "answer",
|
|
1688
|
+
parentRunId: waiting.parentRunId,
|
|
1689
|
+
accepted: true,
|
|
1690
|
+
continuationPending: true,
|
|
1691
|
+
},
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
const decisionStore = new HumanDecisionStore(new WorkflowRunStore().outputRoot);
|
|
1696
|
+
if (request !== null && continuationRunId !== undefined && accepted !== undefined) {
|
|
1697
|
+
await decisionStore.recordContinuation(request.decisionId, {
|
|
1698
|
+
schema: "pi-workflows.human-decision-continuation.v1",
|
|
1699
|
+
decisionId: request.decisionId,
|
|
1700
|
+
requestDigest: request.requestDigest,
|
|
1701
|
+
parentRunId: waiting.parentRunId,
|
|
1702
|
+
runId: continuationRunId,
|
|
1703
|
+
createdAt: accepted.acceptedAt,
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
if (continuationRunId !== undefined) {
|
|
1707
|
+
const existingContinuation = await readRunBundle(
|
|
1708
|
+
new WorkflowRunStore().runDirFor(continuationRunId),
|
|
1709
|
+
);
|
|
1710
|
+
if (existingContinuation !== null) {
|
|
1711
|
+
if (accepted !== undefined) await settleHumanDecisionChannels(accepted);
|
|
1712
|
+
lastWaitingRunId = null;
|
|
1713
|
+
return {
|
|
1714
|
+
message: `Human decision already continued as ${continuationRunId}.`,
|
|
1715
|
+
details: {
|
|
1716
|
+
action: "answer",
|
|
1717
|
+
parentRunId: waiting.parentRunId,
|
|
1718
|
+
runId: continuationRunId,
|
|
1719
|
+
adopted: true,
|
|
1720
|
+
},
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
const continued = await startRun(ctx, waiting.workflowRef, continuationInput, {
|
|
1484
1725
|
parentRunId: waiting.parentRunId,
|
|
1726
|
+
...(accepted !== undefined ? { humanDecision: accepted } : {}),
|
|
1727
|
+
...(continuationRunId !== undefined ? { runId: continuationRunId } : {}),
|
|
1485
1728
|
});
|
|
1486
1729
|
if (continued === undefined) {
|
|
1487
1730
|
throw new Error("Could not start the checkpoint continuation.");
|
|
1488
1731
|
}
|
|
1732
|
+
if (request !== null && accepted !== undefined) {
|
|
1733
|
+
await settleHumanDecisionChannels(accepted);
|
|
1734
|
+
}
|
|
1489
1735
|
lastWaitingRunId = null;
|
|
1490
1736
|
return {
|
|
1491
1737
|
message: `Answered checkpoint ${waiting.parentRunId}; continuation ${continued} started.`,
|
|
@@ -1497,6 +1743,158 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1497
1743
|
};
|
|
1498
1744
|
};
|
|
1499
1745
|
|
|
1746
|
+
async function promptHumanDecision(
|
|
1747
|
+
ctx: ExtensionContext,
|
|
1748
|
+
request: HumanDecisionRequest,
|
|
1749
|
+
): Promise<void> {
|
|
1750
|
+
if (activePiDecisionChannels.has(request.decisionId)) return;
|
|
1751
|
+
const channel = new PiDecisionChannel({
|
|
1752
|
+
actorId: ctx.sessionManager.getSessionId(),
|
|
1753
|
+
ui: ctx.ui,
|
|
1754
|
+
store: new HumanDecisionStore(new WorkflowRunStore().outputRoot),
|
|
1755
|
+
onAnswer: async (answer) => {
|
|
1756
|
+
const result = await answerWorkflowControl(ctx, answer.response, request.runId, answer);
|
|
1757
|
+
notify(ctx, result.message, result.level);
|
|
1758
|
+
},
|
|
1759
|
+
});
|
|
1760
|
+
activePiDecisionChannels.set(request.decisionId, channel);
|
|
1761
|
+
try {
|
|
1762
|
+
await channel.deliver(humanDecisionChannelRequest(request));
|
|
1763
|
+
} catch (error) {
|
|
1764
|
+
notify(ctx, `Could not answer human decision: ${errorMessage(error)}`, "error");
|
|
1765
|
+
} finally {
|
|
1766
|
+
if (activePiDecisionChannels.get(request.decisionId) === channel) {
|
|
1767
|
+
activePiDecisionChannels.delete(request.decisionId);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
const stopDecisionChannels = async (): Promise<void> => {
|
|
1773
|
+
await Promise.allSettled([
|
|
1774
|
+
...[...activePiDecisionChannels.values()].map(async (channel) => channel.stop()),
|
|
1775
|
+
...[...telegramDecisionChannels.values()].map(async (channel) => channel.stop()),
|
|
1776
|
+
]);
|
|
1777
|
+
activePiDecisionChannels.clear();
|
|
1778
|
+
telegramDecisionChannels.clear();
|
|
1779
|
+
decisionChannelConfig = null;
|
|
1780
|
+
};
|
|
1781
|
+
|
|
1782
|
+
const reloadDecisionChannels = async (ctx: ExtensionContext): Promise<void> => {
|
|
1783
|
+
await stopDecisionChannels();
|
|
1784
|
+
const loaded = await loadDecisionChannelConfig();
|
|
1785
|
+
if (loaded === null) return;
|
|
1786
|
+
decisionChannelConfig = loaded.channels;
|
|
1787
|
+
telegramDecisionChannels = createTelegramChannels({
|
|
1788
|
+
config: loaded.channels,
|
|
1789
|
+
credentials: loaded.credentials,
|
|
1790
|
+
configDir: loaded.configDir,
|
|
1791
|
+
store: new HumanDecisionStore(new WorkflowRunStore().outputRoot),
|
|
1792
|
+
onAnswer: async (answer) => {
|
|
1793
|
+
await answerWorkflowControl(ctx, answer.response, answer.request.runId, answer);
|
|
1794
|
+
},
|
|
1795
|
+
});
|
|
1796
|
+
await Promise.all(
|
|
1797
|
+
[...telegramDecisionChannels.values()].map(async (channel) => channel.start()),
|
|
1798
|
+
);
|
|
1799
|
+
};
|
|
1800
|
+
|
|
1801
|
+
const recoverHumanDecisions = async (
|
|
1802
|
+
ctx: ExtensionContext,
|
|
1803
|
+
deliverPending = true,
|
|
1804
|
+
): Promise<void> => {
|
|
1805
|
+
const runStore = new WorkflowRunStore();
|
|
1806
|
+
const store = new HumanDecisionStore(runStore.outputRoot);
|
|
1807
|
+
const requests = (await store.listRequests()).sort((left, right) =>
|
|
1808
|
+
left.createdAt.localeCompare(right.createdAt),
|
|
1809
|
+
);
|
|
1810
|
+
for (const request of requests) {
|
|
1811
|
+
const parent = await readRunBundle(runStore.runDirFor(request.runId));
|
|
1812
|
+
if (parent === null || parent.state.status !== "waiting") continue;
|
|
1813
|
+
const currentRequest = humanDecisionRequest(parent.state.finalOutput);
|
|
1814
|
+
if (
|
|
1815
|
+
currentRequest === null ||
|
|
1816
|
+
currentRequest.decisionId !== request.decisionId ||
|
|
1817
|
+
currentRequest.requestDigest !== request.requestDigest
|
|
1818
|
+
) {
|
|
1819
|
+
continue;
|
|
1820
|
+
}
|
|
1821
|
+
const queueRecord = ensureRunQueueStore(ctx.cwd).getWorkflowRun(request.runId);
|
|
1822
|
+
const ownedBySession =
|
|
1823
|
+
queueRecord !== undefined &&
|
|
1824
|
+
(queueRecord.originSessionId === null ||
|
|
1825
|
+
queueRecord.originSessionId === ctx.sessionManager.getSessionId());
|
|
1826
|
+
const accepted = await store.readAccepted(request.decisionId);
|
|
1827
|
+
if (accepted === null) {
|
|
1828
|
+
let cancellation = await store.readCancellation(request.decisionId);
|
|
1829
|
+
if (
|
|
1830
|
+
cancellation === null &&
|
|
1831
|
+
request.expiresAt !== undefined &&
|
|
1832
|
+
Date.parse(request.expiresAt) <= Date.now()
|
|
1833
|
+
) {
|
|
1834
|
+
await store.cancel(request, "expired");
|
|
1835
|
+
cancellation = await store.readCancellation(request.decisionId);
|
|
1836
|
+
}
|
|
1837
|
+
if (cancellation !== null) {
|
|
1838
|
+
await settleHumanDecisionChannels(cancellation);
|
|
1839
|
+
continue;
|
|
1840
|
+
}
|
|
1841
|
+
if (!deliverPending || !ownedBySession) continue;
|
|
1842
|
+
const channels = audienceChannels(decisionChannelConfig, request.audience);
|
|
1843
|
+
for (const channelId of channels) {
|
|
1844
|
+
const channel = telegramDecisionChannels.get(channelId);
|
|
1845
|
+
if (channel !== undefined) await channel.deliver(humanDecisionChannelRequest(request));
|
|
1846
|
+
}
|
|
1847
|
+
if (ctx.mode === "tui" && channels.includes("pi") && lastWaitingRunId === null) {
|
|
1848
|
+
lastWaitingRunId = request.runId;
|
|
1849
|
+
queueMicrotask(() => void promptHumanDecision(ctx, request));
|
|
1850
|
+
}
|
|
1851
|
+
continue;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
if (!ownedBySession) continue;
|
|
1855
|
+
const runId = `continuation-${request.decisionId.slice("decision-".length)}`;
|
|
1856
|
+
const continuation = (await store.readContinuation(request.decisionId)) ?? {
|
|
1857
|
+
schema: "pi-workflows.human-decision-continuation.v1" as const,
|
|
1858
|
+
decisionId: request.decisionId,
|
|
1859
|
+
requestDigest: request.requestDigest,
|
|
1860
|
+
parentRunId: request.runId,
|
|
1861
|
+
runId,
|
|
1862
|
+
createdAt: accepted.acceptedAt,
|
|
1863
|
+
};
|
|
1864
|
+
await store.recordContinuation(request.decisionId, continuation);
|
|
1865
|
+
const existing = await readRunBundle(runStore.runDirFor(continuation.runId));
|
|
1866
|
+
if (existing === null && activeRun === null) {
|
|
1867
|
+
if (parent.state.workflowSource === undefined) continue;
|
|
1868
|
+
const workflowRef =
|
|
1869
|
+
parent.state.workflowSource.kind === "builtin"
|
|
1870
|
+
? `builtin:${parent.state.workflowSource.id}`
|
|
1871
|
+
: parent.state.workflowSource.path;
|
|
1872
|
+
await startRun(ctx, workflowRef, parent.state.input, {
|
|
1873
|
+
parentRunId: request.runId,
|
|
1874
|
+
humanDecision: accepted,
|
|
1875
|
+
runId: continuation.runId,
|
|
1876
|
+
quiet: true,
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
await settleHumanDecisionChannels(accepted);
|
|
1880
|
+
if (activeRun !== null) break;
|
|
1881
|
+
}
|
|
1882
|
+
};
|
|
1883
|
+
|
|
1884
|
+
const startDecisionRecovery = (ctx: ExtensionContext): void => {
|
|
1885
|
+
if (decisionRecoveryTimer !== null) return;
|
|
1886
|
+
decisionRecoveryTimer = setInterval(() => {
|
|
1887
|
+
if (decisionRecoveryActive || sessionClosed) return;
|
|
1888
|
+
decisionRecoveryActive = true;
|
|
1889
|
+
void recoverHumanDecisions(ctx, false)
|
|
1890
|
+
.catch(() => undefined)
|
|
1891
|
+
.finally(() => {
|
|
1892
|
+
decisionRecoveryActive = false;
|
|
1893
|
+
});
|
|
1894
|
+
}, 1_000);
|
|
1895
|
+
decisionRecoveryTimer.unref?.();
|
|
1896
|
+
};
|
|
1897
|
+
|
|
1500
1898
|
const startWorkflowControl = async (
|
|
1501
1899
|
ctx: ExtensionContext,
|
|
1502
1900
|
ref: string,
|
|
@@ -1610,7 +2008,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1610
2008
|
return;
|
|
1611
2009
|
}
|
|
1612
2010
|
if (parsed.kind === "cancel") {
|
|
1613
|
-
const result = cancelWorkflowControl(ctx);
|
|
2011
|
+
const result = await cancelWorkflowControl(ctx);
|
|
1614
2012
|
notify(ctx, result.message, result.level);
|
|
1615
2013
|
return;
|
|
1616
2014
|
}
|
|
@@ -1658,6 +2056,69 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1658
2056
|
},
|
|
1659
2057
|
});
|
|
1660
2058
|
|
|
2059
|
+
pi.registerCommand("workflow-channel", {
|
|
2060
|
+
description: "Configure or reload private human decision channels",
|
|
2061
|
+
handler: async (args, ctx) => {
|
|
2062
|
+
const action = args.trim() || "status";
|
|
2063
|
+
try {
|
|
2064
|
+
if (action === "reload") {
|
|
2065
|
+
await reloadDecisionChannels(ctx);
|
|
2066
|
+
notify(ctx, "Human decision channels reloaded.");
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
if (action === "status") {
|
|
2070
|
+
notify(
|
|
2071
|
+
ctx,
|
|
2072
|
+
decisionChannelConfig === null
|
|
2073
|
+
? "Human decisions use the Pi channel only."
|
|
2074
|
+
: `Human decision channels are configured for ${Object.keys(decisionChannelConfig.audiences).length} audience(s).`,
|
|
2075
|
+
);
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
if (action !== "setup") {
|
|
2079
|
+
throw new Error("Use /workflow-channel status, setup, or reload.");
|
|
2080
|
+
}
|
|
2081
|
+
if (!ctx.hasUI || ctx.mode !== "tui") {
|
|
2082
|
+
throw new Error("Channel setup requires interactive Pi TUI mode.");
|
|
2083
|
+
}
|
|
2084
|
+
const tokenFile = await ctx.ui.input(
|
|
2085
|
+
"Absolute path to the mode-0600 Telegram token file",
|
|
2086
|
+
"",
|
|
2087
|
+
);
|
|
2088
|
+
if (tokenFile === undefined) return;
|
|
2089
|
+
const audience = await ctx.ui.input("Logical audience", "operator");
|
|
2090
|
+
if (audience === undefined) return;
|
|
2091
|
+
const profile = await ctx.ui.input("Private Telegram profile name", "default");
|
|
2092
|
+
if (profile === undefined) return;
|
|
2093
|
+
const credential = await ctx.ui.input("Private credential reference name", "telegram");
|
|
2094
|
+
if (credential === undefined) return;
|
|
2095
|
+
const users = await ctx.ui.input("Allowed numeric Telegram user IDs, comma separated", "");
|
|
2096
|
+
if (users === undefined) return;
|
|
2097
|
+
const chats = await ctx.ui.input("Allowed numeric Telegram chat IDs, comma separated", "");
|
|
2098
|
+
if (chats === undefined) return;
|
|
2099
|
+
await verifyTelegramTokenFile(tokenFile.trim());
|
|
2100
|
+
await writeDecisionChannelProfile({
|
|
2101
|
+
audience: audience.trim(),
|
|
2102
|
+
profile: profile.trim(),
|
|
2103
|
+
credential: credential.trim(),
|
|
2104
|
+
tokenFile: tokenFile.trim(),
|
|
2105
|
+
allowedUserIds: users
|
|
2106
|
+
.split(",")
|
|
2107
|
+
.map((value) => value.trim())
|
|
2108
|
+
.filter(Boolean),
|
|
2109
|
+
allowedChatIds: chats
|
|
2110
|
+
.split(",")
|
|
2111
|
+
.map((value) => value.trim())
|
|
2112
|
+
.filter(Boolean),
|
|
2113
|
+
});
|
|
2114
|
+
await reloadDecisionChannels(ctx);
|
|
2115
|
+
notify(ctx, "The private human decision channel profile was verified and installed.");
|
|
2116
|
+
} catch (error) {
|
|
2117
|
+
notify(ctx, `Human decision channel setup failed: ${errorMessage(error)}`, "error");
|
|
2118
|
+
}
|
|
2119
|
+
},
|
|
2120
|
+
});
|
|
2121
|
+
|
|
1661
2122
|
pi.registerCommand("controller", {
|
|
1662
2123
|
description: "Manage durable controllers: list, get, apply, reconcile, delete, start, or stop",
|
|
1663
2124
|
getArgumentCompletions: async (prefix: string) => {
|
|
@@ -1737,7 +2198,8 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1737
2198
|
"Do not start repeated work without the user's request, and keep monitoring observation-only unless the user authorizes mutations.",
|
|
1738
2199
|
].join(" "),
|
|
1739
2200
|
parameters: WorkflowToolParameters,
|
|
1740
|
-
async execute(toolCallId,
|
|
2201
|
+
async execute(toolCallId, rawParams, _signal, _onUpdate, ctx) {
|
|
2202
|
+
const params = parseWorkflowToolInput(rawParams);
|
|
1741
2203
|
let control: WorkflowControlResult;
|
|
1742
2204
|
switch (params.action) {
|
|
1743
2205
|
case "list":
|
|
@@ -1756,10 +2218,16 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1756
2218
|
control = resumeWorkflowControl(ctx);
|
|
1757
2219
|
break;
|
|
1758
2220
|
case "cancel":
|
|
1759
|
-
control = cancelWorkflowControl(ctx);
|
|
2221
|
+
control = await cancelWorkflowControl(ctx);
|
|
1760
2222
|
break;
|
|
1761
2223
|
case "answer": {
|
|
1762
2224
|
const waiting = await resolveWaitingWorkflow(ctx, params.runId);
|
|
2225
|
+
const parent = await readRunBundle(new WorkflowRunStore().runDirFor(waiting.parentRunId));
|
|
2226
|
+
if (parent !== null && humanDecisionRequest(parent.state.finalOutput) !== null) {
|
|
2227
|
+
throw new Error(
|
|
2228
|
+
"This checkpoint requires a verified human answer from Pi UI or a configured decision channel.",
|
|
2229
|
+
);
|
|
2230
|
+
}
|
|
1763
2231
|
control = await queueToolLaunch(ctx, waiting.workflowRef, params.input, {
|
|
1764
2232
|
parentRunId: waiting.parentRunId,
|
|
1765
2233
|
});
|
|
@@ -1857,12 +2325,28 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
1857
2325
|
} catch (error) {
|
|
1858
2326
|
notify(ctx, `Could not migrate legacy workflow sources: ${errorMessage(error)}`, "warning");
|
|
1859
2327
|
}
|
|
2328
|
+
try {
|
|
2329
|
+
await reloadDecisionChannels(ctx);
|
|
2330
|
+
} catch {
|
|
2331
|
+
await stopDecisionChannels();
|
|
2332
|
+
notify(
|
|
2333
|
+
ctx,
|
|
2334
|
+
"Could not start human decision channels because the private channel configuration is invalid or unavailable.",
|
|
2335
|
+
"warning",
|
|
2336
|
+
);
|
|
2337
|
+
}
|
|
1860
2338
|
try {
|
|
1861
2339
|
syncArmed = true;
|
|
1862
2340
|
startRunSync(ctx);
|
|
1863
2341
|
} catch {
|
|
1864
2342
|
// Session sync is best-effort; runs themselves never depend on it.
|
|
1865
2343
|
}
|
|
2344
|
+
try {
|
|
2345
|
+
await recoverHumanDecisions(ctx);
|
|
2346
|
+
startDecisionRecovery(ctx);
|
|
2347
|
+
} catch (error) {
|
|
2348
|
+
notify(ctx, `Could not recover human decisions: ${errorMessage(error)}`, "warning");
|
|
2349
|
+
}
|
|
1866
2350
|
try {
|
|
1867
2351
|
await resumeParkedRun(ctx);
|
|
1868
2352
|
} catch (error) {
|
|
@@ -2008,7 +2492,13 @@ export default function piWorkflows(pi: ExtensionAPI) {
|
|
|
2008
2492
|
clearInterval(runSyncTimer);
|
|
2009
2493
|
runSyncTimer = null;
|
|
2010
2494
|
}
|
|
2495
|
+
if (decisionRecoveryTimer !== null) {
|
|
2496
|
+
clearInterval(decisionRecoveryTimer);
|
|
2497
|
+
decisionRecoveryTimer = null;
|
|
2498
|
+
}
|
|
2499
|
+
decisionRecoveryActive = false;
|
|
2011
2500
|
syncArmed = false;
|
|
2501
|
+
await stopDecisionChannels();
|
|
2012
2502
|
await controllerHost?.close().catch(() => undefined);
|
|
2013
2503
|
controllerHost = undefined;
|
|
2014
2504
|
controllerContext = null;
|