@osolmaz/pi-workflows 0.15.1 → 0.15.2
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/extension/index.js +132 -140
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/session-delivery.d.ts +22 -0
- package/dist/extension/session-delivery.js +83 -0
- package/dist/extension/session-delivery.js.map +1 -0
- package/dist/host/state.js +2 -2
- package/dist/host/state.js.map +1 -1
- package/docs/WORKFLOW_HOST.md +6 -3
- package/docs/WORKFLOW_STEP_MESSAGES.md +11 -4
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/src/extension/index.ts +158 -156
- package/src/extension/session-delivery.ts +111 -0
- package/src/host/state.ts +1 -2
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export type ClaimedSessionDelivery = {
|
|
4
|
+
deliveryId: string;
|
|
5
|
+
findSessionEntryId: (entries: readonly unknown[]) => string | undefined;
|
|
6
|
+
send: () => void;
|
|
7
|
+
settle: (sessionEntryId: string) => Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type ClaimSessionDelivery = () => Promise<ClaimedSessionDelivery | undefined>;
|
|
11
|
+
|
|
12
|
+
type QueuedSessionDelivery = {
|
|
13
|
+
delivery: ClaimedSessionDelivery;
|
|
14
|
+
queuedAt: number;
|
|
15
|
+
ambiguityReported: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const SESSION_ENTRY_CONFIRMATION_MS = 10_000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Delivers at most one host-owned message into a Pi session at a time.
|
|
22
|
+
*
|
|
23
|
+
* Pi's public sendMessage API does not return the saved session entry ID. Keep
|
|
24
|
+
* the claimed delivery in memory until that entry becomes observable. Polling
|
|
25
|
+
* may reconcile the delivery, but it must never send the same claim again.
|
|
26
|
+
*/
|
|
27
|
+
export class SessionDeliveryCoordinator {
|
|
28
|
+
private readonly queued = new Map<string, QueuedSessionDelivery>();
|
|
29
|
+
private synchronizing = false;
|
|
30
|
+
|
|
31
|
+
async synchronize(
|
|
32
|
+
ctx: Pick<ExtensionContext, "hasPendingMessages" | "isIdle" | "sessionManager" | "ui">,
|
|
33
|
+
claimers: readonly ClaimSessionDelivery[],
|
|
34
|
+
): Promise<void> {
|
|
35
|
+
if (this.synchronizing) return;
|
|
36
|
+
this.synchronizing = true;
|
|
37
|
+
try {
|
|
38
|
+
if (await this.settleQueued(ctx)) return;
|
|
39
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages()) return;
|
|
40
|
+
|
|
41
|
+
for (const claim of claimers) {
|
|
42
|
+
const delivery = await claim();
|
|
43
|
+
if (delivery === undefined) continue;
|
|
44
|
+
|
|
45
|
+
const existingEntryId = delivery.findSessionEntryId(ctx.sessionManager.getBranch());
|
|
46
|
+
if (existingEntryId !== undefined) {
|
|
47
|
+
await delivery.settle(existingEntryId);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The host claim is asynchronous. Pi can start another turn while that
|
|
52
|
+
// request is in flight, so check again immediately before the
|
|
53
|
+
// synchronous send. An unused claim expires and is never sent later.
|
|
54
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages()) return;
|
|
55
|
+
|
|
56
|
+
this.queued.set(delivery.deliveryId, {
|
|
57
|
+
delivery,
|
|
58
|
+
queuedAt: Date.now(),
|
|
59
|
+
ambiguityReported: false,
|
|
60
|
+
});
|
|
61
|
+
try {
|
|
62
|
+
delivery.send();
|
|
63
|
+
} catch (error) {
|
|
64
|
+
this.queued.delete(delivery.deliveryId);
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const insertedEntryId = delivery.findSessionEntryId(ctx.sessionManager.getBranch());
|
|
69
|
+
if (insertedEntryId !== undefined) {
|
|
70
|
+
this.queued.delete(delivery.deliveryId);
|
|
71
|
+
await delivery.settle(insertedEntryId);
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
} finally {
|
|
76
|
+
this.synchronizing = false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
clear(): void {
|
|
81
|
+
this.queued.clear();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private async settleQueued(
|
|
85
|
+
ctx: Pick<ExtensionContext, "hasPendingMessages" | "isIdle" | "sessionManager" | "ui">,
|
|
86
|
+
): Promise<boolean> {
|
|
87
|
+
let hadQueuedDelivery = false;
|
|
88
|
+
for (const [deliveryId, queued] of this.queued) {
|
|
89
|
+
hadQueuedDelivery = true;
|
|
90
|
+
const sessionEntryId = queued.delivery.findSessionEntryId(ctx.sessionManager.getBranch());
|
|
91
|
+
if (sessionEntryId === undefined) {
|
|
92
|
+
if (
|
|
93
|
+
!queued.ambiguityReported &&
|
|
94
|
+
ctx.isIdle() &&
|
|
95
|
+
!ctx.hasPendingMessages() &&
|
|
96
|
+
Date.now() - queued.queuedAt >= SESSION_ENTRY_CONFIRMATION_MS
|
|
97
|
+
) {
|
|
98
|
+
queued.ambiguityReported = true;
|
|
99
|
+
ctx.ui.notify(
|
|
100
|
+
`Workflow session delivery ${deliveryId} is ambiguous: Pi did not expose a matching session entry. Do not retry it until you check the session history.`,
|
|
101
|
+
"warning",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
this.queued.delete(deliveryId);
|
|
107
|
+
await queued.delivery.settle(sessionEntryId);
|
|
108
|
+
}
|
|
109
|
+
return hadQueuedDelivery;
|
|
110
|
+
}
|
|
111
|
+
}
|
package/src/host/state.ts
CHANGED
|
@@ -676,7 +676,7 @@ export class HostStateStore {
|
|
|
676
676
|
WHERE request_id = ? AND revision = ? AND presentation_session_entry_id IS NULL
|
|
677
677
|
AND (
|
|
678
678
|
status = 'pending'
|
|
679
|
-
OR (status = 'presenting' AND
|
|
679
|
+
OR (status = 'presenting' AND presentation_claim_expires_at <= ?)
|
|
680
680
|
)`,
|
|
681
681
|
)
|
|
682
682
|
.run(
|
|
@@ -685,7 +685,6 @@ export class HostStateStore {
|
|
|
685
685
|
now,
|
|
686
686
|
options.requestId,
|
|
687
687
|
options.expectedRevision,
|
|
688
|
-
options.presenterId,
|
|
689
688
|
now,
|
|
690
689
|
);
|
|
691
690
|
if (changed.changes !== 1) throw new Error("Interactive request presentation claim conflict");
|