@signalridge/pi-workflows 1.7.4 → 1.8.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 +26 -0
- package/README.md +145 -10
- package/package.json +7 -6
- 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/arming.ts +264 -3
- package/src/engine.ts +156 -14
- package/src/errors.ts +26 -0
- package/src/index.ts +381 -156
- package/src/journal.ts +5 -0
- package/src/runtime.ts +40 -20
- package/src/safe-text.ts +4 -35
- package/src/ui.ts +38 -14
- package/src/vm-bootstrap.ts +218 -0
- package/src/vm-helper.ts +502 -0
package/src/index.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
4
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
import { Text } from "@earendil-works/pi-tui";
|
|
6
|
+
import { sanitizeDisplayText } from "@signalridge/pi-ui";
|
|
5
7
|
import { Type } from "@sinclair/typebox";
|
|
6
|
-
import {
|
|
7
|
-
DEFAULT_TRIGGER_WORD,
|
|
8
|
-
deduplicateWorkflowDirective,
|
|
9
|
-
hasTriggerWord,
|
|
10
|
-
WORKFLOW_ARMED_DIRECTIVE,
|
|
11
|
-
} from "./arming.js";
|
|
8
|
+
import { DEFAULT_TRIGGER_WORD, hasTriggerWord, WORKFLOW_ARMING_MESSAGE_TYPE, WorkflowTurnArming } from "./arming.js";
|
|
12
9
|
import { BUILTIN_WORKFLOWS, validateBuiltinArgs } from "./builtins.js";
|
|
13
10
|
import { type CommandResult, resolveCodeReviewScope } from "./code-review-scope.js";
|
|
14
11
|
import { type ScriptStartOptions, type ScriptStartResult, WorkflowEngine, WorkflowWaitAbortedError } from "./engine.js";
|
|
@@ -118,9 +115,10 @@ function textResult(text: string, details: unknown = {}) {
|
|
|
118
115
|
* Run one command for diff discovery. argv array, never a shell string — a
|
|
119
116
|
* branch name or path reaches this from user input.
|
|
120
117
|
*/
|
|
121
|
-
function runCommand(file: string, args: string[]): CommandResult {
|
|
118
|
+
function runCommand(file: string, args: string[], cwd: string): CommandResult {
|
|
122
119
|
try {
|
|
123
120
|
const stdout = execFileSync(file, args, {
|
|
121
|
+
cwd,
|
|
124
122
|
encoding: "utf8",
|
|
125
123
|
maxBuffer: 32 * 1024 * 1024,
|
|
126
124
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -334,6 +332,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
334
332
|
let shutdownQuiesce: Promise<{ settled: boolean; pending: string[]; diagnostic?: string }> | undefined;
|
|
335
333
|
let lifecycleGeneration = 0;
|
|
336
334
|
let deliveryGeneration = 0;
|
|
335
|
+
const deliveryObservers = new Map<string, string>();
|
|
337
336
|
let handledTreeEvent: unknown;
|
|
338
337
|
let journalBranchAnchor: string | null | undefined;
|
|
339
338
|
let protocolAbortController: AbortController | undefined;
|
|
@@ -342,9 +341,24 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
342
341
|
let effortLevel: "off" | "high" | "ultra" = workflowSettings.effort;
|
|
343
342
|
let triggerKeyword: string | undefined = workflowSettings.keywordTriggerWord;
|
|
344
343
|
let keywordTriggerEnabled = workflowSettings.keywordTriggerEnabled;
|
|
345
|
-
/**
|
|
346
|
-
let
|
|
347
|
-
|
|
344
|
+
/** Public sends are void: acceptance is checked against the owning branch. */
|
|
345
|
+
let deliveryContext: ExtensionContext | undefined;
|
|
346
|
+
let deliveryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
347
|
+
let inputPreparing = false;
|
|
348
|
+
const turnArming = new WorkflowTurnArming();
|
|
349
|
+
let compacting = false;
|
|
350
|
+
let navigating = false;
|
|
351
|
+
let deliveryBranchAnchor: string | null | undefined;
|
|
352
|
+
const DELIVERY_WAKE_PREFIX = "[pi-workflows:wake:";
|
|
353
|
+
const DELIVERY_WAKE_TEXT = "Workflow results are ready in the preceding context.";
|
|
354
|
+
const MAX_WAKE_ADMISSIONS = 256;
|
|
355
|
+
// Private, one-use admissions survive an earlier awaited input handler, not
|
|
356
|
+
// a lifecycle retirement. Missing/evicted IDs fail closed without tombstones.
|
|
357
|
+
const wakeAdmissions = new Map<
|
|
358
|
+
string,
|
|
359
|
+
{ generation: number; context: ExtensionContext; engine: WorkflowEngine; anchor: string | null | undefined }
|
|
360
|
+
>();
|
|
361
|
+
/** Pending deliveries for completed runs, including durable recovery (A10). */
|
|
348
362
|
const pendingDeliveryMarkers = new Map<string, { text: string; timestamp: number }>();
|
|
349
363
|
const PENDING_DELIVERY_TYPE = "pi-workflows:pending-delivery";
|
|
350
364
|
const PENDING_DELIVERY_ACK_TYPE = "pi-workflows:pending-delivery-ack";
|
|
@@ -381,21 +395,46 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
381
395
|
else console.warn(`[pi-workflows] ${notice}`);
|
|
382
396
|
return { cancel: true };
|
|
383
397
|
};
|
|
398
|
+
const invalidateDelivery = (): void => {
|
|
399
|
+
deliveryGeneration++;
|
|
400
|
+
deliveryObservers.clear();
|
|
401
|
+
turnArming.retire();
|
|
402
|
+
wakeAdmissions.clear();
|
|
403
|
+
if (deliveryTimer) clearTimeout(deliveryTimer);
|
|
404
|
+
deliveryTimer = undefined;
|
|
405
|
+
};
|
|
384
406
|
const beginShutdownQuiesce = (): Promise<{ settled: boolean; pending: string[]; diagnostic?: string }> => {
|
|
385
407
|
if (shutdownQuiesce) return shutdownQuiesce;
|
|
408
|
+
// Retire admission and delivery BEFORE aborting children, including when
|
|
409
|
+
// subagents initiates shutdown first. Journal cleanup retains its separate
|
|
410
|
+
// lifecycle generation until the native shutdown hook finishes quiescence.
|
|
411
|
+
active = false;
|
|
412
|
+
invalidateDelivery();
|
|
413
|
+
deliveryContext = undefined;
|
|
386
414
|
const closingEngine = engine;
|
|
387
|
-
if (!closingEngine || closingEngine.isDisposed())
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
415
|
+
if (!closingEngine || closingEngine.isDisposed()) {
|
|
416
|
+
shutdownQuiesce = Promise.resolve({ settled: true, pending: [] });
|
|
417
|
+
return shutdownQuiesce;
|
|
418
|
+
}
|
|
419
|
+
// Publish the handshake promise before abort can synchronously re-enter
|
|
420
|
+
// the bus through an owned-child cleanup callback.
|
|
421
|
+
let settle: (result: { settled: boolean; pending: string[]; diagnostic?: string }) => void = () => {};
|
|
422
|
+
shutdownQuiesce = new Promise((resolve) => {
|
|
423
|
+
settle = resolve;
|
|
424
|
+
});
|
|
425
|
+
void closingEngine.quiesceForBranchChange().then(settle, (error: unknown) => {
|
|
426
|
+
settle({
|
|
427
|
+
settled: false,
|
|
428
|
+
pending: [],
|
|
429
|
+
diagnostic: error instanceof Error ? error.message : String(error),
|
|
430
|
+
});
|
|
431
|
+
});
|
|
393
432
|
return shutdownQuiesce;
|
|
394
433
|
};
|
|
395
434
|
// Subagents starts this handshake before retiring the owned-agent RPC. When
|
|
396
435
|
// workflows shuts down first, its native hook performs the same operation.
|
|
397
436
|
pi.events.on("pi-workflows:shutdown-quiesce", (raw: unknown) => {
|
|
398
|
-
if (!
|
|
437
|
+
if (!engine) return;
|
|
399
438
|
const respond = (raw as { respond?: unknown } | null)?.respond;
|
|
400
439
|
if (typeof respond === "function") respond(beginShutdownQuiesce());
|
|
401
440
|
});
|
|
@@ -404,6 +443,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
404
443
|
const event = (raw as { event?: unknown } | null)?.event;
|
|
405
444
|
if (!event || event === handledTreeEvent) return;
|
|
406
445
|
handledTreeEvent = event;
|
|
446
|
+
invalidateDelivery();
|
|
447
|
+
pendingDeliveryMarkers.clear();
|
|
407
448
|
engine.fenceCommittedBranch();
|
|
408
449
|
});
|
|
409
450
|
|
|
@@ -488,7 +529,21 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
488
529
|
active = true;
|
|
489
530
|
const workflowEngine = currentEngine();
|
|
490
531
|
workflowEngine.onRunSettled = notifyRunChanged;
|
|
532
|
+
workflowEngine.onBackgroundExecution = (runId) => void deliverBackgroundResult(workflowEngine, runId);
|
|
491
533
|
workflowEngine.onRuntimeEvent = notifyRunChanged;
|
|
534
|
+
if (typeof pi.registerMessageRenderer === "function") {
|
|
535
|
+
pi.registerMessageRenderer("pi-workflows:delivery", (message, { expanded }, theme) => {
|
|
536
|
+
const original =
|
|
537
|
+
typeof message.content === "string"
|
|
538
|
+
? message.content
|
|
539
|
+
: message.content
|
|
540
|
+
.filter((part) => part.type === "text")
|
|
541
|
+
.map((part) => part.text)
|
|
542
|
+
.join("\n");
|
|
543
|
+
const display = sanitizeDisplayText(original, { multiline: true });
|
|
544
|
+
return new Text(theme.fg("toolOutput", display.slice(0, expanded ? 8000 : 2000)), 0, 0);
|
|
545
|
+
});
|
|
546
|
+
}
|
|
492
547
|
// `control("resume")` and the provider-limit retry start executions with no
|
|
493
548
|
// caller to pass options for them, and the strength table is deliberately
|
|
494
549
|
// never frozen onto a run. Without this they would resume with no table and
|
|
@@ -518,11 +573,13 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
518
573
|
],
|
|
519
574
|
parameters: WorkflowToolSchema,
|
|
520
575
|
renderCall(args, theme) {
|
|
521
|
-
const label = args.name ?? (args.script !== undefined ? "script" : "");
|
|
576
|
+
const label = sanitizeDisplayText(args.name ?? (args.script !== undefined ? "script" : ""));
|
|
522
577
|
return new Text(theme.fg("toolTitle", theme.bold(`Workflow ${label}`)), 0, 0);
|
|
523
578
|
},
|
|
524
579
|
renderResult(result, { expanded, isPartial }, theme) {
|
|
525
|
-
const text = result.content[0]?.type === "text" ? result.content[0].text : ""
|
|
580
|
+
const text = sanitizeDisplayText(result.content[0]?.type === "text" ? result.content[0].text : "", {
|
|
581
|
+
multiline: true,
|
|
582
|
+
});
|
|
526
583
|
if (isPartial) return new Text(theme.fg("warning", "Workflow running…"), 0, 0);
|
|
527
584
|
const preview = expanded ? text.slice(0, 8_000) : text.split("\n").slice(0, 4).join("\n");
|
|
528
585
|
return new Text(
|
|
@@ -584,7 +641,6 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
584
641
|
replacement,
|
|
585
642
|
);
|
|
586
643
|
if (!resumed) return textResult(`Workflow run not found: ${params.resumeFromRunId}`);
|
|
587
|
-
if (resumed.background) void deliverBackgroundResult(workflowEngine, resumed.runId);
|
|
588
644
|
return textResult(formatStart(resumed), resumed);
|
|
589
645
|
}
|
|
590
646
|
const result = await workflowEngine.start(script, options);
|
|
@@ -609,10 +665,16 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
609
665
|
"List, inspect, pause, resume, stop, or remove workflow runs with bounded machine-readable output.",
|
|
610
666
|
parameters: ControlSchema,
|
|
611
667
|
renderCall(args, theme) {
|
|
612
|
-
return new Text(
|
|
668
|
+
return new Text(
|
|
669
|
+
theme.fg("toolTitle", theme.bold(`Workflow control: ${sanitizeDisplayText(args.action ?? "")}`)),
|
|
670
|
+
0,
|
|
671
|
+
0,
|
|
672
|
+
);
|
|
613
673
|
},
|
|
614
674
|
renderResult(result, { expanded }, theme) {
|
|
615
|
-
const text = result.content[0]?.type === "text" ? result.content[0].text : ""
|
|
675
|
+
const text = sanitizeDisplayText(result.content[0]?.type === "text" ? result.content[0].text : "", {
|
|
676
|
+
multiline: true,
|
|
677
|
+
});
|
|
616
678
|
return new Text(theme.fg("dim", expanded ? text.slice(0, 8_000) : text.slice(0, 1_000)), 0, 0);
|
|
617
679
|
},
|
|
618
680
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
@@ -949,7 +1011,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
949
1011
|
// /workflows-trigger: keyword arming (A5). Typing the bounded word
|
|
950
1012
|
// `workflow`/`workflows` — or a configured synonym — in an ordinary message
|
|
951
1013
|
// counts as an explicit opt-in to multi-agent orchestration, and the message
|
|
952
|
-
//
|
|
1014
|
+
// receives hidden model guidance. Arming AUTHORIZES the tool; it never forces a run,
|
|
953
1015
|
// and it never opens UI. A user asking a question about workflows still gets
|
|
954
1016
|
// an ordinary answer.
|
|
955
1017
|
const TRIGGER_WORD_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/u;
|
|
@@ -1002,59 +1064,66 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1002
1064
|
},
|
|
1003
1065
|
});
|
|
1004
1066
|
|
|
1005
|
-
// Keyword arming input hook (A5).
|
|
1006
|
-
//
|
|
1067
|
+
// Keyword arming input hook (A5). Record provenance, never append model
|
|
1068
|
+
// guidance to native user text: the model may use `workflow` and may still
|
|
1007
1069
|
// decline, which is what keeps "how do workflows work?" an ordinary
|
|
1008
1070
|
// question. Nothing is swallowed and no UI opens.
|
|
1009
|
-
pi.on("input", (event) => {
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1071
|
+
pi.on("input", (event, ctx) => {
|
|
1072
|
+
const ownWake = event.source === "extension" && event.text.startsWith(DELIVERY_WAKE_PREFIX);
|
|
1073
|
+
if (ownWake) {
|
|
1074
|
+
const admission = wakeAdmissions.get(event.text);
|
|
1075
|
+
wakeAdmissions.delete(event.text); // exact, one-use ownership, even on rejection
|
|
1076
|
+
if (
|
|
1077
|
+
!admission ||
|
|
1078
|
+
admission.generation !== deliveryGeneration ||
|
|
1079
|
+
admission.context !== deliveryContext ||
|
|
1080
|
+
admission.engine !== engine ||
|
|
1081
|
+
!canDeliver()
|
|
1082
|
+
) {
|
|
1083
|
+
turnArming.retire();
|
|
1084
|
+
return { action: "handled" as const };
|
|
1085
|
+
}
|
|
1086
|
+
try {
|
|
1087
|
+
if (
|
|
1088
|
+
admission.anchor &&
|
|
1089
|
+
!admission.context.sessionManager.getBranch().some((entry) => entry.id === admission.anchor)
|
|
1090
|
+
) {
|
|
1091
|
+
turnArming.retire();
|
|
1092
|
+
return { action: "handled" as const };
|
|
1093
|
+
}
|
|
1094
|
+
} catch {
|
|
1095
|
+
turnArming.retire();
|
|
1096
|
+
return { action: "handled" as const };
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
inputPreparing = true;
|
|
1100
|
+
navigating = false; // a fresh prompt supersedes an uncommitted navigation
|
|
1101
|
+
if (deliveryTimer) clearTimeout(deliveryTimer);
|
|
1102
|
+
deliveryTimer = undefined;
|
|
1103
|
+
const text = event.text;
|
|
1104
|
+
const explicitTrigger = keywordTriggerEnabled && hasTriggerWord(text, triggerKeyword);
|
|
1105
|
+
const substantive = effortLevel !== "off" && text.trim().length >= 16 && !text.trim().startsWith("/");
|
|
1106
|
+
turnArming.observe(
|
|
1107
|
+
{ ...event, text },
|
|
1108
|
+
ctx,
|
|
1109
|
+
active && (explicitTrigger || substantive),
|
|
1110
|
+
effortLevel,
|
|
1111
|
+
explicitTrigger,
|
|
1112
|
+
);
|
|
1113
|
+
if (ownWake)
|
|
1013
1114
|
return {
|
|
1014
1115
|
action: "transform" as const,
|
|
1015
|
-
text:
|
|
1116
|
+
text: DELIVERY_WAKE_TEXT,
|
|
1016
1117
|
...(event.images ? { images: event.images } : {}),
|
|
1017
1118
|
};
|
|
1018
|
-
}
|
|
1019
|
-
const explicitTrigger = keywordTriggerEnabled && hasTriggerWord(event.text, triggerKeyword);
|
|
1020
|
-
const substantive = effortLevel !== "off" && event.text.trim().length >= 16 && !event.text.trim().startsWith("/");
|
|
1021
|
-
if (!explicitTrigger && !substantive) return { action: "continue" as const };
|
|
1022
|
-
if (event.text.includes(WORKFLOW_ARMED_DIRECTIVE)) return { action: "continue" as const };
|
|
1023
|
-
const effortDirective =
|
|
1024
|
-
effortLevel === "ultra"
|
|
1025
|
-
? 'Effort: ULTRA. Be exhaustive: use broad parallel review, verification, and completeness checks. Width is where the exhaustiveness goes: keep steps at strength: "medium" and reserve "high" for a step whose error is expensive to reverse, if the run has one at all.'
|
|
1026
|
-
: effortLevel === "high"
|
|
1027
|
-
? "Effort: HIGH. Be thorough: use several independent reviewers and an adversarial verification pass."
|
|
1028
|
-
: "";
|
|
1029
|
-
return {
|
|
1030
|
-
action: "transform" as const,
|
|
1031
|
-
text: `${event.text}\n\n${WORKFLOW_ARMED_DIRECTIVE}${effortDirective ? `\n${effortDirective}` : ""}`,
|
|
1032
|
-
...(event.images ? { images: event.images } : {}),
|
|
1033
|
-
};
|
|
1119
|
+
return { action: "continue" as const };
|
|
1034
1120
|
});
|
|
1035
1121
|
|
|
1036
|
-
//
|
|
1037
|
-
//
|
|
1038
|
-
//
|
|
1039
|
-
pi.on("
|
|
1040
|
-
|
|
1041
|
-
let lastUserIndex = -1;
|
|
1042
|
-
let lastUserMessage: UserMessage | undefined;
|
|
1043
|
-
for (let index = 0; index < event.messages.length; index += 1) {
|
|
1044
|
-
const message = event.messages[index];
|
|
1045
|
-
if (message && "role" in message && message.role === "user") {
|
|
1046
|
-
lastUserIndex = index;
|
|
1047
|
-
lastUserMessage = message;
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
const message = lastUserMessage;
|
|
1051
|
-
if (lastUserIndex < 0 || !message || !("content" in message) || typeof message.content !== "string") return;
|
|
1052
|
-
const deduplicatedText = deduplicateWorkflowDirective(message.content);
|
|
1053
|
-
if (deduplicatedText === message.content) return;
|
|
1054
|
-
const messages = event.messages.slice();
|
|
1055
|
-
messages[lastUserIndex] = { ...message, content: deduplicatedText };
|
|
1056
|
-
return { messages };
|
|
1057
|
-
});
|
|
1122
|
+
// Hidden guidance is turn-local, including on reload and after later hooks
|
|
1123
|
+
// navigate while before_agent_start is awaiting. Never edit native history.
|
|
1124
|
+
// Request-local overlays never enter native history or summarization.
|
|
1125
|
+
pi.on("message_start", (event, ctx) => turnArming.messageStarted(event, ctx));
|
|
1126
|
+
pi.on("context", (event, ctx) => turnArming.filterContext(event, ctx));
|
|
1058
1127
|
|
|
1059
1128
|
// Standing effort mode mirrors the reference: it auto-arms substantive
|
|
1060
1129
|
// interactive messages but remains guidance, not an inferred spend ceiling.
|
|
@@ -1115,7 +1184,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1115
1184
|
// takes a diff rather than something typed, so it resolves one first.
|
|
1116
1185
|
let scriptArgs: Record<string, unknown> | undefined;
|
|
1117
1186
|
if (name === "code-review") {
|
|
1118
|
-
const
|
|
1187
|
+
const cwd = ctx.cwd;
|
|
1188
|
+
const scope = resolveCodeReviewScope(text, (file, argv) => runCommand(file, argv, cwd));
|
|
1119
1189
|
for (const notice of scope.notices) ctx.ui.notify(notice, "warning");
|
|
1120
1190
|
if (!scope.diff.trim()) return;
|
|
1121
1191
|
scriptArgs = { diff: scope.diff, diffSource: scope.diffSource };
|
|
@@ -1151,8 +1221,64 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1151
1221
|
|
|
1152
1222
|
// Neither before-event is a commit point. Pi can veto in a later handler,
|
|
1153
1223
|
// or summarization may abort after every handler has run.
|
|
1154
|
-
|
|
1155
|
-
|
|
1224
|
+
const beforeNavigation = (ctx: ExtensionContext): { cancel: true } | undefined => {
|
|
1225
|
+
turnArming.retire();
|
|
1226
|
+
const veto = navigationPreflight(ctx);
|
|
1227
|
+
if (!veto) {
|
|
1228
|
+
navigating = true;
|
|
1229
|
+
if (deliveryTimer) clearTimeout(deliveryTimer);
|
|
1230
|
+
deliveryTimer = undefined;
|
|
1231
|
+
}
|
|
1232
|
+
return veto;
|
|
1233
|
+
};
|
|
1234
|
+
pi.on("session_before_switch", (_event, ctx) => beforeNavigation(ctx));
|
|
1235
|
+
pi.on("session_before_tree", (event, ctx) => {
|
|
1236
|
+
// The public preparation array is mutable; remove only legacy owned
|
|
1237
|
+
// custom guidance, never quoted user content or stored history.
|
|
1238
|
+
const entries = event.preparation?.entriesToSummarize ?? [];
|
|
1239
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
1240
|
+
const entry = entries[index];
|
|
1241
|
+
if (entry?.type === "custom_message" && entry.customType === WORKFLOW_ARMING_MESSAGE_TYPE)
|
|
1242
|
+
entries.splice(index, 1);
|
|
1243
|
+
}
|
|
1244
|
+
return beforeNavigation(ctx);
|
|
1245
|
+
});
|
|
1246
|
+
pi.on("session_before_fork", (_event, ctx) => beforeNavigation(ctx));
|
|
1247
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
1248
|
+
inputPreparing = true;
|
|
1249
|
+
if (deliveryTimer) clearTimeout(deliveryTimer);
|
|
1250
|
+
deliveryTimer = undefined;
|
|
1251
|
+
return turnArming.consume(event, ctx);
|
|
1252
|
+
});
|
|
1253
|
+
pi.on("agent_start", () => {
|
|
1254
|
+
inputPreparing = false;
|
|
1255
|
+
});
|
|
1256
|
+
pi.on("session_before_compact", (event) => {
|
|
1257
|
+
for (const messages of [
|
|
1258
|
+
event.preparation?.messagesToSummarize ?? [],
|
|
1259
|
+
event.preparation?.turnPrefixMessages ?? [],
|
|
1260
|
+
]) {
|
|
1261
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
1262
|
+
const message = messages[index];
|
|
1263
|
+
if (message?.role === "custom" && message.customType === WORKFLOW_ARMING_MESSAGE_TYPE)
|
|
1264
|
+
messages.splice(index, 1);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
compacting = true;
|
|
1268
|
+
if (deliveryTimer) clearTimeout(deliveryTimer);
|
|
1269
|
+
deliveryTimer = undefined;
|
|
1270
|
+
});
|
|
1271
|
+
const afterCompact = (): void => {
|
|
1272
|
+
compacting = false;
|
|
1273
|
+
schedulePendingDeliveries();
|
|
1274
|
+
};
|
|
1275
|
+
pi.on("session_compact", afterCompact);
|
|
1276
|
+
pi.on("session_compact_failed", afterCompact);
|
|
1277
|
+
pi.on("agent_settled", () => {
|
|
1278
|
+
turnArming.retire();
|
|
1279
|
+
inputPreparing = false;
|
|
1280
|
+
schedulePendingDeliveries();
|
|
1281
|
+
});
|
|
1156
1282
|
pi.on("session_tree", (event, ctx) => {
|
|
1157
1283
|
let workflowEngine: WorkflowEngine;
|
|
1158
1284
|
try {
|
|
@@ -1160,6 +1286,11 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1160
1286
|
} catch {
|
|
1161
1287
|
return;
|
|
1162
1288
|
}
|
|
1289
|
+
invalidateDelivery();
|
|
1290
|
+
pendingDeliveryMarkers.clear();
|
|
1291
|
+
inputPreparing = false;
|
|
1292
|
+
compacting = false;
|
|
1293
|
+
navigating = false;
|
|
1163
1294
|
if (handledTreeEvent !== event) {
|
|
1164
1295
|
// A controller abort can synchronously call stop-owned RPC. Detach the
|
|
1165
1296
|
// old subagent records first, regardless of extension load order, so its
|
|
@@ -1168,13 +1299,16 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1168
1299
|
workflowEngine.fenceCommittedBranch();
|
|
1169
1300
|
}
|
|
1170
1301
|
handledTreeEvent = event;
|
|
1171
|
-
deliveryGeneration++;
|
|
1172
1302
|
try {
|
|
1173
1303
|
const entries = ctx.sessionManager.getBranch() as unknown as SessionEntryLike[];
|
|
1174
1304
|
journalBranchAnchor =
|
|
1175
1305
|
ctx.sessionManager.getLeafId?.() ?? (entries.at(-1) as { id?: string } | undefined)?.id ?? null;
|
|
1176
1306
|
workflowEngine.restore(entries);
|
|
1177
1307
|
workflowEngine.resumeLifecycle();
|
|
1308
|
+
deliveryContext = ctx;
|
|
1309
|
+
deliveryBranchAnchor = journalBranchAnchor;
|
|
1310
|
+
hydratePendingDeliveries(entries);
|
|
1311
|
+
schedulePendingDeliveries();
|
|
1178
1312
|
} catch (error: unknown) {
|
|
1179
1313
|
active = false; // never resume an old branch after a failed restore
|
|
1180
1314
|
console.warn(
|
|
@@ -1200,12 +1334,28 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1200
1334
|
};
|
|
1201
1335
|
|
|
1202
1336
|
/** Background-run result delivery: fail-closed, session-bound (A10). */
|
|
1203
|
-
const deliverBackgroundResult = async (
|
|
1337
|
+
const deliverBackgroundResult = async (workflowEngine: WorkflowEngine, runId: string): Promise<void> => {
|
|
1204
1338
|
const generation = deliveryGeneration;
|
|
1205
|
-
const
|
|
1206
|
-
|
|
1339
|
+
const executionGeneration = workflowEngine.getState(runId)?.executionGeneration;
|
|
1340
|
+
const observerKey = `${generation}:${executionGeneration}`;
|
|
1341
|
+
if (deliveryObservers.get(runId) === observerKey) return;
|
|
1342
|
+
deliveryObservers.set(runId, observerKey);
|
|
1343
|
+
while (deliveryObservers.size > 256) {
|
|
1344
|
+
const oldest = deliveryObservers.keys().next().value;
|
|
1345
|
+
if (oldest === undefined) break;
|
|
1346
|
+
deliveryObservers.delete(oldest);
|
|
1347
|
+
}
|
|
1348
|
+
const run = await workflowEngine.waitFor(runId).catch(() => undefined);
|
|
1349
|
+
if (
|
|
1350
|
+
!run ||
|
|
1351
|
+
generation !== deliveryGeneration ||
|
|
1352
|
+
workflowEngine !== engine ||
|
|
1353
|
+
executionGeneration !== workflowEngine.getState(runId)?.executionGeneration ||
|
|
1354
|
+
!["completed", "failed", "stopped"].includes(run.status)
|
|
1355
|
+
)
|
|
1356
|
+
return;
|
|
1207
1357
|
const header = `Workflow ${run.runId} ${run.status}`;
|
|
1208
|
-
const state =
|
|
1358
|
+
const state = workflowEngine.getState(runId);
|
|
1209
1359
|
const finalResult = state?.result ?? run.finalResult;
|
|
1210
1360
|
const body =
|
|
1211
1361
|
run.error ??
|
|
@@ -1213,42 +1363,154 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1213
1363
|
? `completed with ${Object.keys(run.callResults).length} agent call(s).`
|
|
1214
1364
|
: `result:\n${typeof finalResult === "string" ? finalResult : (JSON.stringify(finalResult, null, 2) ?? String(finalResult))}`);
|
|
1215
1365
|
const text = `${header}: ${body}`;
|
|
1216
|
-
if (
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
} else {
|
|
1229
|
-
// No bound session endpoint — fail-closed: persist a pending marker and
|
|
1230
|
-
// flush it when a session binds, never fall back to shared pi.sendMessage.
|
|
1231
|
-
rememberPendingDelivery(runId, text);
|
|
1232
|
-
try {
|
|
1233
|
-
pi.appendEntry(PENDING_DELIVERY_TYPE, { runId, text, status: run.status, timestamp: Date.now() });
|
|
1234
|
-
} catch {
|
|
1235
|
-
// best-effort persistence; in-memory marker still ensures retry
|
|
1236
|
-
}
|
|
1366
|
+
if (!ownsDeliveryBranch()) return;
|
|
1367
|
+
rememberPendingDelivery(runId, text);
|
|
1368
|
+
try {
|
|
1369
|
+
pi.appendEntry(PENDING_DELIVERY_TYPE, {
|
|
1370
|
+
runId,
|
|
1371
|
+
text: text.slice(0, 8_000),
|
|
1372
|
+
status: run.status,
|
|
1373
|
+
timestamp: Date.now(),
|
|
1374
|
+
});
|
|
1375
|
+
advanceDeliveryAnchor();
|
|
1376
|
+
} catch {
|
|
1377
|
+
// Best-effort persistence; the in-memory marker remains retryable.
|
|
1237
1378
|
}
|
|
1379
|
+
schedulePendingDeliveries();
|
|
1238
1380
|
};
|
|
1239
1381
|
|
|
1382
|
+
const ownsDeliveryBranch = (): boolean => {
|
|
1383
|
+
if (!active || !deliveryContext || !engine || engine.isDisposed()) return false;
|
|
1384
|
+
try {
|
|
1385
|
+
const branch = deliveryContext.sessionManager.getBranch();
|
|
1386
|
+
return [deliveryBranchAnchor, journalBranchAnchor].every(
|
|
1387
|
+
(anchor) => !anchor || branch.some((entry) => entry.id === anchor),
|
|
1388
|
+
);
|
|
1389
|
+
} catch {
|
|
1390
|
+
return false;
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
const advanceDeliveryAnchor = (): void => {
|
|
1394
|
+
if (!deliveryContext) return;
|
|
1395
|
+
deliveryBranchAnchor =
|
|
1396
|
+
deliveryContext.sessionManager.getLeafId?.() ?? deliveryContext.sessionManager.getBranch().at(-1)?.id ?? null;
|
|
1397
|
+
};
|
|
1398
|
+
const canDeliver = (): boolean => {
|
|
1399
|
+
if (!ownsDeliveryBranch() || inputPreparing || compacting || navigating) return false;
|
|
1400
|
+
try {
|
|
1401
|
+
return deliveryContext?.isIdle() === true && deliveryContext.hasPendingMessages() === false;
|
|
1402
|
+
} catch {
|
|
1403
|
+
return false;
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
const hydratePendingDeliveries = (entries: SessionEntryLike[]): void => {
|
|
1407
|
+
pendingDeliveryMarkers.clear();
|
|
1408
|
+
for (const entry of entries) {
|
|
1409
|
+
const shaped = entry as { type?: unknown; customType?: unknown; data?: unknown };
|
|
1410
|
+
if (shaped.type !== "custom") continue;
|
|
1411
|
+
const data = shaped.data as Record<string, unknown> | undefined;
|
|
1412
|
+
if (typeof data?.runId !== "string") continue;
|
|
1413
|
+
if (shaped.customType === PENDING_DELIVERY_ACK_TYPE) pendingDeliveryMarkers.delete(data.runId);
|
|
1414
|
+
else if (shaped.customType === PENDING_DELIVERY_TYPE && typeof data.text === "string") {
|
|
1415
|
+
rememberPendingDelivery(
|
|
1416
|
+
data.runId,
|
|
1417
|
+
data.text,
|
|
1418
|
+
typeof data.timestamp === "number" ? data.timestamp : Date.now(),
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1240
1423
|
const flushPendingDeliveries = (): void => {
|
|
1241
|
-
if (!
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1424
|
+
if (!canDeliver() || !deliveryContext || pendingDeliveryMarkers.size === 0) return;
|
|
1425
|
+
if (typeof pi.sendMessage !== "function" || typeof pi.sendUserMessage !== "function") return;
|
|
1426
|
+
const ctx = deliveryContext;
|
|
1427
|
+
const generation = deliveryGeneration;
|
|
1428
|
+
// Bounded batches coalesce simultaneous completions without model polling.
|
|
1429
|
+
const batch = [...pendingDeliveryMarkers].slice(0, 8);
|
|
1430
|
+
const isCurrent = (): boolean => generation === deliveryGeneration && ctx === deliveryContext && canDeliver();
|
|
1431
|
+
const persisted = (runId: string): boolean =>
|
|
1432
|
+
ctx.sessionManager.getBranch().some((entry) => {
|
|
1433
|
+
const shaped = entry as {
|
|
1434
|
+
type?: string;
|
|
1435
|
+
customType?: string;
|
|
1436
|
+
content?: unknown;
|
|
1437
|
+
details?: { runIds?: unknown };
|
|
1438
|
+
};
|
|
1439
|
+
const ids = shaped.details?.runIds;
|
|
1440
|
+
return (
|
|
1441
|
+
shaped.type === "custom_message" &&
|
|
1442
|
+
shaped.customType === "pi-workflows:delivery" &&
|
|
1443
|
+
typeof shaped.content === "string" &&
|
|
1444
|
+
Array.isArray(ids) &&
|
|
1445
|
+
ids.length > 0 &&
|
|
1446
|
+
ids.length <= 8 &&
|
|
1447
|
+
ids.every((id) => typeof id === "string" && id.length > 0 && id.length <= 128) &&
|
|
1448
|
+
new Set(ids).size === ids.length &&
|
|
1449
|
+
ids.includes(runId)
|
|
1450
|
+
);
|
|
1451
|
+
});
|
|
1452
|
+
try {
|
|
1453
|
+
if (!isCurrent()) return;
|
|
1454
|
+
// Never use a custom-message triggerTurn: that skips input preparation
|
|
1455
|
+
// and before_agent_start. The void API is NOT an acceptance promise.
|
|
1456
|
+
const missing = batch.filter(([runId]) => !persisted(runId));
|
|
1457
|
+
if (missing.length > 0)
|
|
1458
|
+
pi.sendMessage(
|
|
1459
|
+
{
|
|
1460
|
+
content: missing.map(([, marker]) => marker.text).join("\n\n"),
|
|
1461
|
+
display: true,
|
|
1462
|
+
customType: "pi-workflows:delivery",
|
|
1463
|
+
details: { runIds: missing.map(([runId]) => runId) },
|
|
1464
|
+
},
|
|
1465
|
+
{ triggerTurn: false },
|
|
1466
|
+
);
|
|
1467
|
+
if (!isCurrent() || !batch.every(([runId]) => persisted(runId))) return;
|
|
1468
|
+
advanceDeliveryAnchor();
|
|
1469
|
+
for (const [runId] of batch) {
|
|
1470
|
+
if (!isCurrent()) return;
|
|
1471
|
+
const priorEntryIds = new Set(ctx.sessionManager.getBranch().map((entry) => entry.id));
|
|
1245
1472
|
pi.appendEntry(PENDING_DELIVERY_ACK_TYPE, { runId, timestamp: Date.now() });
|
|
1473
|
+
if (!isCurrent()) return;
|
|
1474
|
+
const acknowledged = ctx.sessionManager.getBranch().some((entry) => {
|
|
1475
|
+
const shaped = entry as { type?: string; customType?: string; data?: { runId?: unknown } };
|
|
1476
|
+
return (
|
|
1477
|
+
!priorEntryIds.has(entry.id) &&
|
|
1478
|
+
shaped.type === "custom" &&
|
|
1479
|
+
shaped.customType === PENDING_DELIVERY_ACK_TYPE &&
|
|
1480
|
+
shaped.data?.runId === runId
|
|
1481
|
+
);
|
|
1482
|
+
});
|
|
1483
|
+
if (!acknowledged) return;
|
|
1484
|
+
advanceDeliveryAnchor();
|
|
1246
1485
|
pendingDeliveryMarkers.delete(runId);
|
|
1247
|
-
} catch {
|
|
1248
|
-
// Keep pending until both delivery and its durable acknowledgement succeed.
|
|
1249
1486
|
}
|
|
1487
|
+
// An earlier input hook can await while the same manager navigates. Keep
|
|
1488
|
+
// exact owner evidence until our input hook consumes this private wake.
|
|
1489
|
+
if (isCurrent() && engine) {
|
|
1490
|
+
const wake = `${DELIVERY_WAKE_PREFIX}${randomUUID()}] ${DELIVERY_WAKE_TEXT}`;
|
|
1491
|
+
wakeAdmissions.set(wake, { generation, context: ctx, engine, anchor: deliveryBranchAnchor });
|
|
1492
|
+
while (wakeAdmissions.size > MAX_WAKE_ADMISSIONS) {
|
|
1493
|
+
const oldest = wakeAdmissions.keys().next().value;
|
|
1494
|
+
if (oldest === undefined) break;
|
|
1495
|
+
wakeAdmissions.delete(oldest);
|
|
1496
|
+
}
|
|
1497
|
+
// Disable commands/templates explicitly for internal notification input.
|
|
1498
|
+
pi.sendUserMessage(wake, { deliverAs: "followUp", expandPromptTemplates: false });
|
|
1499
|
+
}
|
|
1500
|
+
} catch {
|
|
1501
|
+
// Retain unacknowledged markers. Retry only at a later lifecycle boundary.
|
|
1250
1502
|
}
|
|
1251
1503
|
};
|
|
1504
|
+
const schedulePendingDeliveries = (): void => {
|
|
1505
|
+
if (deliveryTimer || pendingDeliveryMarkers.size === 0 || !ownsDeliveryBranch()) return;
|
|
1506
|
+
const generation = deliveryGeneration;
|
|
1507
|
+
// Defer handoff out of our notification-only agent_settled handler;
|
|
1508
|
+
// later handlers may navigate or start compaction in either load order.
|
|
1509
|
+
deliveryTimer = setTimeout(() => {
|
|
1510
|
+
deliveryTimer = undefined;
|
|
1511
|
+
if (generation === deliveryGeneration) flushPendingDeliveries();
|
|
1512
|
+
}, 0);
|
|
1513
|
+
};
|
|
1252
1514
|
|
|
1253
1515
|
/** Build a script from a natural-language prompt and run it in background. */
|
|
1254
1516
|
const runWorkflowFromPrompt = async (ctx: ExtensionCommandContext, prompt: string): Promise<string | undefined> => {
|
|
@@ -1281,6 +1543,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1281
1543
|
};
|
|
1282
1544
|
|
|
1283
1545
|
pi.on("session_start", async (_event, ctx) => {
|
|
1546
|
+
turnArming.retire();
|
|
1284
1547
|
if (active || engine) return;
|
|
1285
1548
|
const generation = ++lifecycleGeneration;
|
|
1286
1549
|
sessionCwd = ctx.cwd ?? process.cwd();
|
|
@@ -1357,6 +1620,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1357
1620
|
}
|
|
1358
1621
|
},
|
|
1359
1622
|
awaitProtocol,
|
|
1623
|
+
sessionCwd,
|
|
1360
1624
|
);
|
|
1361
1625
|
try {
|
|
1362
1626
|
const entries =
|
|
@@ -1386,56 +1650,19 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1386
1650
|
widgetUi = ctx.mode === "tui" ? ctx.ui : undefined;
|
|
1387
1651
|
registerSurface();
|
|
1388
1652
|
|
|
1389
|
-
//
|
|
1390
|
-
//
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1653
|
+
// Public API methods target the current session: generation + branch
|
|
1654
|
+
// ownership, not a captured function value, provide session affinity.
|
|
1655
|
+
deliveryContext = ctx;
|
|
1656
|
+
deliveryBranchAnchor = journalBranchAnchor;
|
|
1657
|
+
inputPreparing = false;
|
|
1658
|
+
compacting = false;
|
|
1659
|
+
navigating = false;
|
|
1394
1660
|
try {
|
|
1395
|
-
|
|
1396
|
-
if (typeof capturedSend === "function") {
|
|
1397
|
-
deliverResult = (text: string) => {
|
|
1398
|
-
// Use the captured function value for session affinity; if the
|
|
1399
|
-
// captured function is itself a shared accessor, the flush is still
|
|
1400
|
-
// fail-closed (delivery only when bound).
|
|
1401
|
-
(capturedSend as unknown as (message: unknown, options: unknown) => unknown).call(
|
|
1402
|
-
pi,
|
|
1403
|
-
{
|
|
1404
|
-
content: text,
|
|
1405
|
-
display: true,
|
|
1406
|
-
customType: "pi-workflows:delivery",
|
|
1407
|
-
},
|
|
1408
|
-
{ deliverAs: "followUp", triggerTurn: true },
|
|
1409
|
-
);
|
|
1410
|
-
};
|
|
1411
|
-
}
|
|
1661
|
+
hydratePendingDeliveries(ctx.sessionManager.getBranch() as unknown as SessionEntryLike[]);
|
|
1412
1662
|
} catch {
|
|
1413
|
-
//
|
|
1663
|
+
pendingDeliveryMarkers.clear(); // fail closed on an unreadable branch
|
|
1414
1664
|
}
|
|
1415
|
-
|
|
1416
|
-
// settled before the first session_start) — scan the branch for
|
|
1417
|
-
// PENDING_DELIVERY_TYPE entries that have no in-memory counterpart.
|
|
1418
|
-
try {
|
|
1419
|
-
const acknowledged = new Set<string>();
|
|
1420
|
-
for (const entry of initialEntries) {
|
|
1421
|
-
const shaped = entry as { type?: unknown; customType?: unknown; data?: unknown };
|
|
1422
|
-
if (shaped.type !== "custom" || typeof shaped.customType !== "string") continue;
|
|
1423
|
-
const data = shaped.data as Record<string, unknown> | undefined;
|
|
1424
|
-
const entryRunId = typeof data?.runId === "string" ? data.runId : undefined;
|
|
1425
|
-
if (!entryRunId) continue;
|
|
1426
|
-
if (shaped.customType === PENDING_DELIVERY_ACK_TYPE) {
|
|
1427
|
-
acknowledged.add(entryRunId);
|
|
1428
|
-
} else if (shaped.customType === PENDING_DELIVERY_TYPE) {
|
|
1429
|
-
const pendingText = typeof data?.text === "string" ? data.text : undefined;
|
|
1430
|
-
const timestamp = typeof data?.timestamp === "number" ? data.timestamp : Date.now();
|
|
1431
|
-
if (pendingText && !acknowledged.has(entryRunId)) rememberPendingDelivery(entryRunId, pendingText, timestamp);
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
for (const runId of acknowledged) pendingDeliveryMarkers.delete(runId);
|
|
1435
|
-
} catch {
|
|
1436
|
-
// scan is best-effort
|
|
1437
|
-
}
|
|
1438
|
-
flushPendingDeliveries();
|
|
1665
|
+
schedulePendingDeliveries();
|
|
1439
1666
|
|
|
1440
1667
|
// Saved workflow commands register at session_start (Pi cannot unregister
|
|
1441
1668
|
// or replace command metadata, so factory-time registration would freeze the
|
|
@@ -1472,10 +1699,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1472
1699
|
});
|
|
1473
1700
|
|
|
1474
1701
|
pi.on("session_shutdown", async () => {
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
// engine quiescence; the old session must not accept another run.
|
|
1478
|
-
active = false;
|
|
1702
|
+
// This synchronous boundary is shared with the bus-first shutdown path.
|
|
1703
|
+
const quiescence = beginShutdownQuiesce();
|
|
1479
1704
|
clearWidget();
|
|
1480
1705
|
widgetUi = undefined;
|
|
1481
1706
|
if (widgetTimer) {
|
|
@@ -1486,7 +1711,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1486
1711
|
if (closingEngine) {
|
|
1487
1712
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1488
1713
|
const result = await Promise.race([
|
|
1489
|
-
|
|
1714
|
+
quiescence,
|
|
1490
1715
|
new Promise<undefined>((resolve) => {
|
|
1491
1716
|
timer = setTimeout(() => resolve(undefined), 6_000);
|
|
1492
1717
|
}),
|
|
@@ -1508,7 +1733,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1508
1733
|
protocolProbe = undefined;
|
|
1509
1734
|
protocolInFlight = undefined;
|
|
1510
1735
|
protocolError = undefined;
|
|
1511
|
-
|
|
1736
|
+
deliveryContext = undefined;
|
|
1512
1737
|
});
|
|
1513
1738
|
}
|
|
1514
1739
|
|