@llblab/pi-kit 0.35.5 → 0.36.1
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/AGENTS.md +2 -1
- package/BACKLOG.md +0 -5
- package/CHANGELOG.md +12 -0
- package/README.md +33 -31
- package/node_modules/@llblab/pi-actors/AGENTS.md +3 -1
- package/node_modules/@llblab/pi-actors/CHANGELOG.md +4 -0
- package/node_modules/@llblab/pi-actors/README.md +2 -2
- package/node_modules/@llblab/pi-actors/dist/index.js +1 -0
- package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.d.ts +1 -0
- package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +3 -0
- package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +7 -2
- package/node_modules/@llblab/pi-actors/dist/lib/pi.js +20 -6
- package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.d.ts +1 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +74 -74
- package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +1 -1
- package/node_modules/@llblab/pi-actors/dist/skills/actors/references/runs.md +1 -1
- package/node_modules/@llblab/pi-actors/docs/README.md +1 -1
- package/node_modules/@llblab/pi-actors/docs/async-runs.md +3 -1
- package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +22 -20
- package/node_modules/@llblab/pi-actors/index.ts +1 -0
- package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +4 -0
- package/node_modules/@llblab/pi-actors/lib/pi.ts +33 -9
- package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +72 -69
- package/node_modules/@llblab/pi-actors/package.json +1 -1
- package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +1 -1
- package/node_modules/@llblab/pi-actors/skills/actors/references/runs.md +1 -1
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +4 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +5 -4
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +1 -0
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +5 -4
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/package.json +3 -3
|
@@ -7,7 +7,7 @@ Living index of all documentation in the `/docs` directory.
|
|
|
7
7
|
- [command-templates.md](./command-templates.md) — Portable synchronous command execution standard
|
|
8
8
|
- [template-recipes.md](./template-recipes.md) — Saved JSON/Markdown recipe standard, imports, and reusable command-template graph composition
|
|
9
9
|
- [async-runs.md](./async-runs.md) — Run lifecycle, state, Control, Trace, cancellation, and terminal reconciliation
|
|
10
|
-
- [coordinator-delivery.md](./coordinator-delivery.md) —
|
|
10
|
+
- [coordinator-delivery.md](./coordinator-delivery.md) — Active model-step completion overlays, idle batching, durable acceptance, and explicit urgent Pi steering
|
|
11
11
|
- [actor-inspector.md](./actor-inspector.md) — Owner-filtered actor-instance navigation through Recipe, Trace, and Control
|
|
12
12
|
- [inspection.md](./inspection.md) — Complete `inspect` target/view matrix, authorization boundaries, and diagnostic routes
|
|
13
13
|
- [tool-registry.md](./tool-registry.md) — Local `pi-actors` registry storage and `register_tool` adaptation
|
|
@@ -110,7 +110,9 @@ Statuses include `running`, `done`, `failed`, `exited`, `cancelled`, and `killed
|
|
|
110
110
|
|
|
111
111
|
Ambient observation detects root terminal transitions and explicit retained Trace attention. Terminal transitions reconcile before semantic attention. Canonical attention is an in-memory wake hint, not a durable queue: observers prime retained ids at startup, deliver each later retained unseen id once, and bound memory to the current retained set across compaction. Persist durable recovery state or an artifact before emitting attention; compaction may discard older hints and its marker makes that history loss explicit. Terminal follow-up delivery persists handled/failure evidence so reloads retry unhandled transitions without duplicating completed notifications.
|
|
112
112
|
|
|
113
|
-
Ordinary finite Runs project terminal results through one root-coordinator completion scheduler. Every detached runner passes its delivery-root owner and exact parent Run generation to nested actor processes. A descendant terminal remains authoritative but ineligible until its containing top-level
|
|
113
|
+
Ordinary finite Runs project terminal results through one root-coordinator completion scheduler. Every detached runner passes its delivery-root owner and exact parent Run generation to nested actor processes. A descendant terminal remains authoritative but ineligible until its complete containing top-level tree is terminal; descendant Pi sessions never schedule completion delivery. At an active completed `turn_end`, the root scheduler snapshots all ready trees into one owner-fenced immutable batch of at most 256 exact generations, preserves preceding boundary entries, and returns one visible `custom_message` plus `continue: true`. The full tool batch finishes before insertion, and the next model request receives fresh results within the same agent run without using native steering or follow-up queues. Aborted/error boundaries and pending native input defer insertion. Already-idle Pi still wakes through `agent_settled`, session recovery, or a short idle debounce. One batch renders one gray operator card and exposes at most 64 bounded parent-child rows with semantic outputs.
|
|
114
|
+
|
|
115
|
+
Boundary proposals remain pending until exact model context or bounded active-session evidence proves the entry was committed; returning a draft never marks member terminals handled. An idle send finalizes immediately on Pi acceptance, and durable queued phase also proves acceptance. Recovery retries a pending batch only after proving absence from the bounded active session chain; incomplete or conflicting evidence stays pending with diagnostics. Exact acceptance marks the member generations handled and releases the next epoch without depending on one later context callback. Pending send failures retain bounded retry evidence. Duplicate exact context envelopes collapse before presentation. See [Coordinator Delivery](./coordinator-delivery.md) for scheduling and recovery details.
|
|
114
116
|
|
|
115
117
|
Sequence, parallel, repeat, and imported branches are internal execution topology and never own branch-level turns. Each separately launched Run owns its own generation and terminal lifecycle; compatible singleton reuse is not a new launch. Explicit semantic attention may intentionally add a checkpoint turn; Runs marked silent and synchronously acknowledged stop outcomes suppress automatic projection. Large semantic results stay outside compact completion rows and remain available in structured details, execution captures, or artifacts.
|
|
116
118
|
|
|
@@ -8,10 +8,10 @@ Separate durable Run completion truth from the scheduling of model turns:
|
|
|
8
8
|
|
|
9
9
|
```text
|
|
10
10
|
one Run generation -> one terminal record with inherited tree lineage
|
|
11
|
-
one ready completion forest per bounded epoch -> one root-coordinator
|
|
11
|
+
one ready completion forest per bounded epoch -> one root-coordinator model step
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Ordinary terminals accumulate across nested actor sessions
|
|
14
|
+
Ordinary terminals accumulate across nested actor sessions. Descendants wait for their complete containing top-level tree, then all ready trees and concurrently completed top-level Runs reach only the root coordinator in one bounded batch. During active work, a public `turn_end` overlay supplies fresh results to the next model request after the full tool batch, without waiting for idle or using native steering. Already-idle Pi retains debounced follow-up wakeups. Explicit actor-authored urgent semantic checkpoints remain a separate native-steer delivery class.
|
|
15
15
|
|
|
16
16
|
## Non-Goals
|
|
17
17
|
|
|
@@ -27,7 +27,7 @@ Ordinary terminals accumulate across nested actor sessions while Pi is active. D
|
|
|
27
27
|
- `attention: "notify"` remains visible UI status without a model turn.
|
|
28
28
|
- `attention: "followup"` retains its existing explicit semantic follow-up behavior.
|
|
29
29
|
- New `attention: "steer"` requests urgent semantic delivery at Pi's next safe assistant/tool boundary.
|
|
30
|
-
- Terminal transitions inherit exact parent-generation and root-owner lineage; only the root session batches ready completion trees
|
|
30
|
+
- Terminal transitions inherit exact parent-generation and root-owner lineage; only the root session batches ready completion trees into a model-step overlay or an already-idle follow-up, never one turn per Run or descendant session.
|
|
31
31
|
|
|
32
32
|
`command.done` remains non-projectable even if malformed or legacy Trace attaches any attention value.
|
|
33
33
|
|
|
@@ -37,7 +37,7 @@ Ordinary terminals accumulate across nested actor sessions while Pi is active. D
|
|
|
37
37
|
- `runs-trace.ts` owns admission of the new `steer` attention value.
|
|
38
38
|
- `observability.ts` discovers terminal candidates and explicit semantic attention without deciding Pi delivery timing.
|
|
39
39
|
- A new `run-delivery.ts` domain owns the owner-scoped delivery journal, batch construction, bounds, phases, formatting, recovery, and acknowledgments.
|
|
40
|
-
- `run-ui-runtime.ts` owns idle detection, debounce, reconciliation, flushing, and stale-context containment.
|
|
40
|
+
- `run-ui-runtime.ts` owns active model-step boundaries, idle detection, debounce, reconciliation, flushing, and stale-context containment.
|
|
41
41
|
- `extension-runtime.ts` orders completion flushes before automatic Recipe review.
|
|
42
42
|
- `index.ts` remains a thin registration root and adds only the required Pi lifecycle adapter.
|
|
43
43
|
- `pi.ts` exposes narrow ports for batched follow-up and urgent steer delivery.
|
|
@@ -75,13 +75,13 @@ Completion batch shape:
|
|
|
75
75
|
|
|
76
76
|
Phases are monotonic:
|
|
77
77
|
|
|
78
|
-
1. `pending`: The exact member snapshot is durable but
|
|
78
|
+
1. `pending`: The exact member snapshot is durable but Pi acceptance is unproven. Returning a boundary draft does not advance this phase.
|
|
79
79
|
2. `queued`: Pi accepted a custom message carrying the exact batch or steer ID.
|
|
80
|
-
3. `presented`:
|
|
80
|
+
3. `presented`: Exact completion acceptance is proven, or a Pi `context` event observed the urgent-steer ID in messages being supplied to an LLM call.
|
|
81
81
|
|
|
82
82
|
Every journal mutation uses the canonical token-owned lock, expected-phase fencing, owner and generation validation, and atomic replacement. Repeated transitions are idempotent. Corrupt, oversized, foreign-owner, or stale-generation state fails closed with bounded diagnostics.
|
|
83
83
|
|
|
84
|
-
Completion and steer acknowledgments intentionally differ. Successful `sendMessage()` acceptance finalizes an ordinary completion batch immediately
|
|
84
|
+
Completion and steer acknowledgments intentionally differ. An active completion overlay remains pending until exact model context or bounded active-session evidence proves its committed entry. Successful idle `sendMessage()` acceptance finalizes an ordinary completion batch immediately; durable queued phase also proves acceptance. Both paths mark members through the existing terminal-handled authority, so a missed context callback cannot block every later epoch. Urgent steer still requires exact model-context presentation. If a completion member was synchronously archived or pruned after acceptance, the bounded delivery snapshot remains sufficient and the missing state write becomes a diagnostic rather than invalidating acceptance.
|
|
85
85
|
|
|
86
86
|
## Completion Collection
|
|
87
87
|
|
|
@@ -97,17 +97,20 @@ It excludes:
|
|
|
97
97
|
|
|
98
98
|
Candidates sort by terminal timestamp, then stable Run identity, then `run_instance_id`. Replacement generations with the same logical Run id remain distinct internal members.
|
|
99
99
|
|
|
100
|
-
While
|
|
100
|
+
While Pi is active, candidates remain durable until the next completed `turn_end`; watchers do not enqueue ordinary terminal follow-ups into the native message queue. The boundary itself reads current Run state, so a missed watcher cannot make the next step stale. A flush snapshots eligible candidates into one immutable batch. While acceptance is pending, newer terminals stay unhandled for the next bounded completion epoch.
|
|
101
101
|
|
|
102
102
|
## Batch Flush
|
|
103
103
|
|
|
104
104
|
Flush one batch when:
|
|
105
105
|
|
|
106
|
-
1. `
|
|
107
|
-
2.
|
|
108
|
-
3.
|
|
106
|
+
1. A completed active `turn_end` reaches the exact root owner without an aborted signal or native pending input;
|
|
107
|
+
2. `agent_settled` fires for the still-active owner and `ctx.isIdle()` remains true;
|
|
108
|
+
3. Terminals arrive while Pi is already idle and survive the 100 ms default debounce window;
|
|
109
|
+
4. Session restoration discovers unhandled terminal generations or recoverable delivery state.
|
|
109
110
|
|
|
110
|
-
|
|
111
|
+
Active delivery returns `{ entries: [...event.entries, entry], continue: true }`, where `entry` is one `custom_message` with `customType: "pi-actors-run-batch"` and `display: true`. It preserves previous extensions' boundary entries and requests one next model step in the same agent run after the entire tool batch. Aborted/error boundaries, queued native input, and outstanding urgent steer defer the overlay. It never uses `sendMessage(..., { deliverAs: "steer" })` or a native follow-up queue for active completion delivery.
|
|
112
|
+
|
|
113
|
+
Already-idle delivery uses the same custom message with `deliverAs: "followUp"` and `triggerTurn: true` to wake Pi. Both paths render one gray operator card and supply the same tree-compressed prompt to the model. The bounded content retains the exact batch identity for session evidence and deduplication even though Pi omits private message details from model context. It includes:
|
|
111
114
|
|
|
112
115
|
- batch ID and completion window;
|
|
113
116
|
- counts by terminal status;
|
|
@@ -120,14 +123,14 @@ Completion member details remain redacted through existing terminal projection r
|
|
|
120
123
|
|
|
121
124
|
## Acceptance, Presentation, And Recovery
|
|
122
125
|
|
|
123
|
-
|
|
126
|
+
Returning boundary drafts is a proposal, not an SDK receipt. An active completion batch advances through queued acceptance to finalization only when exact model-bound `context` or a bounded active-session parent-chain read proves the committed entry. If another extension replaces or invalidates the proposed draft chain, the absent batch remains pending and can retry with the same identity. Idle delivery advances immediately after `sendMessage()` returns. Acceptance marks every still-present member generation terminal-handled and retains a bounded owner receipt. The `context` lifecycle adapter also deduplicates completion envelopes and handles exact urgent-steer presentation; presented steer writes `delivery.steer_presented` in the exact Run generation.
|
|
124
127
|
|
|
125
128
|
The generation-fenced Trace marker prevents a retained historical steer from replaying after bounded owner receipts rotate: suffix compaction cannot retain the older steer while discarding its newer presentation marker. Missing, archived, pruned, or replaced Run state needs no marker because it can no longer replay that original generation.
|
|
126
129
|
|
|
127
130
|
Recovery rules:
|
|
128
131
|
|
|
129
132
|
- Send failure: keep `pending`, record failure evidence, and retry.
|
|
130
|
-
-
|
|
133
|
+
- Pending batch after a boundary attempt or restart: inspect the bounded active Pi session chain for the exact batch ID and content. Proven presence finalizes; proven absence permits retry; incomplete or conflicting evidence stays pending with a diagnostic.
|
|
131
134
|
- Completion send accepted: finalize immediately and release the next epoch.
|
|
132
135
|
- Crash leaves a queued completion: queued itself proves `sendMessage()` acceptance, so finalize without resend or content reformatting.
|
|
133
136
|
- Presented urgent steer: never resend.
|
|
@@ -179,12 +182,12 @@ Another extension may start work during `agent_settled`; recheck `ctx.isIdle()`
|
|
|
179
182
|
|
|
180
183
|
Implementation is complete only when source and packed-extension tests prove:
|
|
181
184
|
|
|
182
|
-
1. Multiple Runs finishing during one
|
|
185
|
+
1. Multiple Runs finishing during active work produce one boundary entry and one next model request before idle, after all tools in the current batch finish; no native steering/follow-up enqueue occurs.
|
|
183
186
|
2. Idle completions inside the debounce window form one batch.
|
|
184
187
|
3. Completion/settled races project every generation exactly once.
|
|
185
188
|
4. Send failure and restart before queueing retry without a handled marker.
|
|
186
189
|
5. Restart in the narrow queue-acceptance/finalization boundary neither loses nor duplicates the batch.
|
|
187
|
-
6.
|
|
190
|
+
6. Boundary proposals do not acknowledge terminals early. Exact committed-entry evidence or idle send acceptance acknowledges members atomically and idempotently; urgent steer still requires exact `context` presentation.
|
|
188
191
|
7. Replacement generations sharing a Run id remain distinct.
|
|
189
192
|
8. Silent, stopped, cancelled, handled, and foreign-owner Runs remain excluded.
|
|
190
193
|
9. Legacy or malformed `command.done` attention, including `steer`, remains non-projectable.
|
|
@@ -192,11 +195,10 @@ Implementation is complete only when source and packed-extension tests prove:
|
|
|
192
195
|
11. Overflow, corruption, journal backpressure, archive/prune races, and stale contexts fail safely.
|
|
193
196
|
12. Completion flushing precedes automatic Recipe review.
|
|
194
197
|
13. Pi 1.0.0 is the minimum source and packed lifecycle baseline; coding-agent and TUI peers share that floor.
|
|
198
|
+
14. The packed extension runs through the public SDK with a synthetic provider, proving one batch reaches the next request before settlement for both a full tool batch and a tool-free final answer, while preserving preceding boundary entries.
|
|
195
199
|
|
|
196
200
|
Focused observability and delivery tests precede TypeScript/build/import checks. The acceptance checkpoint then runs full product validation, dependency audit, package dry-run, and ABCd context validation.
|
|
197
201
|
|
|
198
|
-
##
|
|
199
|
-
|
|
200
|
-
This is one minor release because durable batching, acceptance acknowledgment, lifecycle ordering, and explicit steer share one model-delivery invariant. Completion acceptance must release later epochs without depending on a context callback; steer must not ship before its durable presentation-deduplication path exists.
|
|
202
|
+
## Product Boundaries
|
|
201
203
|
|
|
202
|
-
|
|
204
|
+
Active overlays and idle follow-ups share the same batch identity, bounds, lineage, terminal authority, and acceptance journal. They differ only in the Pi scheduling port and its acceptance evidence. Urgent semantic steer remains independent; completion status never implies urgency. The extension has no dependency on Telegram and does not modify Telegram reply or preview ownership.
|
|
@@ -17,6 +17,7 @@ export default function toolRegistryExtension(pi: Pi.ExtensionAPI) {
|
|
|
17
17
|
);
|
|
18
18
|
}
|
|
19
19
|
pi.on("session_start", async (_event, ctx) => runtime.onSessionStart(ctx));
|
|
20
|
+
pi.on("turn_end", (event, ctx) => runtime.onTurnEnd(event, ctx));
|
|
20
21
|
pi.on("agent_settled", async (_event, ctx) => runtime.onAgentSettled(ctx));
|
|
21
22
|
pi.on("context", async (event, ctx) => ({
|
|
22
23
|
messages: runtime.onContext(event.messages, ctx) as typeof event.messages,
|
|
@@ -26,6 +26,7 @@ export interface ActorExtensionRuntime {
|
|
|
26
26
|
discoverResources(metaUrl: string): { skillPaths: string[] } | undefined;
|
|
27
27
|
getRunOwnerId(ctx: Pi.ExtensionContext): string;
|
|
28
28
|
onAgentSettled(ctx: Pi.ExtensionContext): void;
|
|
29
|
+
onTurnEnd(event: Pi.TurnEndEvent, ctx: Pi.ExtensionContext): Pi.TurnEndEventResult | undefined;
|
|
29
30
|
onContext(messages: unknown[], ctx: Pi.ExtensionContext): unknown[];
|
|
30
31
|
onSessionShutdown(reason: string, ctx: Pi.ExtensionContext): void;
|
|
31
32
|
onSessionStart(ctx: Pi.ExtensionContext): Promise<void>;
|
|
@@ -146,6 +147,9 @@ export function createActorExtensionRuntime(
|
|
|
146
147
|
if (!activeRunOwnerId || getRunOwnerId(ctx) !== activeRunOwnerId) return;
|
|
147
148
|
if (!runUiRuntime.flushCompletionBatch(ctx)) automaticReview.schedule();
|
|
148
149
|
},
|
|
150
|
+
onTurnEnd(event, ctx) {
|
|
151
|
+
return runUiRuntime.onTurnEnd(event, ctx);
|
|
152
|
+
},
|
|
149
153
|
onContext(messages, ctx) {
|
|
150
154
|
return runUiRuntime.projectContext(messages, ctx);
|
|
151
155
|
},
|
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
import type {
|
|
8
8
|
ExtensionAPI,
|
|
9
9
|
ExtensionContext,
|
|
10
|
+
CustomMessageEntryDraft,
|
|
11
|
+
TurnEndEvent,
|
|
12
|
+
TurnEndEventResult,
|
|
10
13
|
} from "@earendil-works/pi-coding-agent";
|
|
11
14
|
|
|
12
15
|
import * as SessionEvidence from "./session-evidence.ts";
|
|
13
16
|
|
|
14
|
-
export type { ExtensionAPI, ExtensionContext };
|
|
17
|
+
export type { ExtensionAPI, ExtensionContext, TurnEndEvent, TurnEndEventResult };
|
|
15
18
|
|
|
16
19
|
export const RUN_COMPLETION_BATCH_CUSTOM_TYPE = "pi-actors-run-batch";
|
|
17
20
|
export const RUN_STEER_CUSTOM_TYPE = "pi-actors-run-steer";
|
|
@@ -44,12 +47,12 @@ export function createNotificationSink(
|
|
|
44
47
|
};
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
export function
|
|
48
|
-
pi: ExtensionAPI,
|
|
50
|
+
export function createRunCompletionBatchEntry(
|
|
49
51
|
batchId: string,
|
|
50
52
|
content: string,
|
|
51
|
-
):
|
|
52
|
-
|
|
53
|
+
): CustomMessageEntryDraft {
|
|
54
|
+
return {
|
|
55
|
+
type: "custom_message",
|
|
53
56
|
customType: RUN_COMPLETION_BATCH_CUSTOM_TYPE,
|
|
54
57
|
content,
|
|
55
58
|
display: true,
|
|
@@ -59,10 +62,16 @@ export function sendRunCompletionBatch(
|
|
|
59
62
|
kind: "completion_batch",
|
|
60
63
|
},
|
|
61
64
|
},
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function sendRunCompletionBatch(
|
|
69
|
+
pi: ExtensionAPI,
|
|
70
|
+
batchId: string,
|
|
71
|
+
content: string,
|
|
72
|
+
): void {
|
|
73
|
+
const { type: _type, ...message } = createRunCompletionBatchEntry(batchId, content);
|
|
74
|
+
pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
export function sendRunSteer(
|
|
@@ -219,6 +228,21 @@ export function removeRunSteerFromContext(
|
|
|
219
228
|
return messages.filter((message) => steerMessage(message)?.steerId !== steerId);
|
|
220
229
|
}
|
|
221
230
|
|
|
231
|
+
export function inspectRunCompletionBatchSessionEvidence(
|
|
232
|
+
ctx: ExtensionContext,
|
|
233
|
+
input: { batchId: string; content: string },
|
|
234
|
+
): SessionEvidence.ActiveSessionEntryEvidence {
|
|
235
|
+
return SessionEvidence.inspectBoundedActiveSessionEntries({
|
|
236
|
+
getEntry: (id) => ctx.sessionManager.getEntry(id),
|
|
237
|
+
leaf: ctx.sessionManager.getLeafEntry(),
|
|
238
|
+
match: (entry) => {
|
|
239
|
+
const batch = completionBatchMessage(entry);
|
|
240
|
+
if (!batch || batch.batchId !== input.batchId) return undefined;
|
|
241
|
+
return batch.content === input.content ? "present" : "conflict";
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
222
246
|
export function inspectRunSteerSessionEvidence(
|
|
223
247
|
ctx: ExtensionContext,
|
|
224
248
|
input: { content: string; eventId: string; steerId: string },
|
|
@@ -15,6 +15,7 @@ import * as RunDeliveryLineage from "./run-delivery-lineage.ts";
|
|
|
15
15
|
export interface RunUiRuntime {
|
|
16
16
|
close(): void;
|
|
17
17
|
flushCompletionBatch(ctx: Pi.ExtensionContext): boolean;
|
|
18
|
+
onTurnEnd(event: Pi.TurnEndEvent, ctx: Pi.ExtensionContext): Pi.TurnEndEventResult | undefined;
|
|
18
19
|
projectContext(messages: unknown[], ctx: Pi.ExtensionContext): unknown[];
|
|
19
20
|
shutdown(
|
|
20
21
|
eventReason: string,
|
|
@@ -51,6 +52,7 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
51
52
|
const deliveryObservation = Observability.createRunUiObservationState();
|
|
52
53
|
const retirementAttempts = new Set<string>();
|
|
53
54
|
const steerDiagnostics = new Set<string>();
|
|
55
|
+
const completionDiagnostics = new Set<string>();
|
|
54
56
|
|
|
55
57
|
const close = (): void => {
|
|
56
58
|
running = false;
|
|
@@ -72,6 +74,7 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
72
74
|
deliveryTimeout = undefined;
|
|
73
75
|
recoverQueuedSteers = false;
|
|
74
76
|
steerDiagnostics.clear();
|
|
77
|
+
completionDiagnostics.clear();
|
|
75
78
|
if (animationInterval) clearInterval(animationInterval);
|
|
76
79
|
animationInterval = undefined;
|
|
77
80
|
};
|
|
@@ -242,6 +245,53 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
242
245
|
});
|
|
243
246
|
return true;
|
|
244
247
|
};
|
|
248
|
+
const acceptCompletionBatch = (ownerId: string, batchId: string): boolean => {
|
|
249
|
+
const input = { batchId, ownerId, tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir };
|
|
250
|
+
if (!RunDelivery.markRunCompletionBatchQueued(input)) return false;
|
|
251
|
+
if (!RunDelivery.markRunCompletionBatchPresented(input)) return false;
|
|
252
|
+
const accepted = journal(ownerId).completion_batch;
|
|
253
|
+
return accepted ? finishPresentedBatch(ownerId, accepted) : false;
|
|
254
|
+
};
|
|
255
|
+
const reconcileCompletionBatch = (ctx: Pi.ExtensionContext, ownerId: string): boolean => {
|
|
256
|
+
const batch = journal(ownerId).completion_batch;
|
|
257
|
+
if (!batch) return false;
|
|
258
|
+
if (batch.phase === "pending") {
|
|
259
|
+
// Returning boundary drafts is not acceptance: prove the exact entry before acknowledging it.
|
|
260
|
+
const evidence = Pi.inspectRunCompletionBatchSessionEvidence(ctx, {
|
|
261
|
+
batchId: batch.batch_id,
|
|
262
|
+
content: RunDelivery.formatRunCompletionBatchMessage(batch),
|
|
263
|
+
});
|
|
264
|
+
if (evidence.status === "absent") return false;
|
|
265
|
+
if (evidence.status !== "present") {
|
|
266
|
+
const key = `${batch.batch_id}:${evidence.status}:${evidence.reason ?? ""}`;
|
|
267
|
+
if (!completionDiagnostics.has(key)) {
|
|
268
|
+
completionDiagnostics.add(key);
|
|
269
|
+
ctx.ui.notify(
|
|
270
|
+
`Actor completion recovery is ${evidence.status}: ${evidence.reason ?? "conflicting session evidence"}. Batch ${batch.batch_id} remains pending.`,
|
|
271
|
+
"warning",
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return !acceptCompletionBatch(ownerId, batch.batch_id);
|
|
278
|
+
};
|
|
279
|
+
const pendingCompletionBatch = (ownerId: string): RunDelivery.RunCompletionBatch | undefined => {
|
|
280
|
+
const existing = journal(ownerId).completion_batch;
|
|
281
|
+
if (existing) return existing;
|
|
282
|
+
const snapshot = Observability.readRunDeliverySnapshot(deliveryObservation, ownerId);
|
|
283
|
+
admitCompletionTransitions(ownerId, snapshot);
|
|
284
|
+
Observability.pruneRunUiObservationState(deliveryObservation, snapshot);
|
|
285
|
+
return journal(ownerId).completion_batch;
|
|
286
|
+
};
|
|
287
|
+
const isCurrentOwner = (ctx: Pi.ExtensionContext): boolean => {
|
|
288
|
+
if (!running || !activeOwnerId) return false;
|
|
289
|
+
try {
|
|
290
|
+
return Pi.getSessionId(ctx) === activeOwnerId;
|
|
291
|
+
} catch {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
245
295
|
const isIdle = (ctx: Pi.ExtensionContext): boolean =>
|
|
246
296
|
typeof ctx.isIdle !== "function" || ctx.isIdle();
|
|
247
297
|
const isDeliveryRoot = (ownerId: string): boolean => {
|
|
@@ -422,48 +472,14 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
422
472
|
|
|
423
473
|
flushCompletionBatch = (ctx: Pi.ExtensionContext): boolean => {
|
|
424
474
|
const ownerId = activeOwnerId;
|
|
425
|
-
if (!
|
|
426
|
-
try {
|
|
427
|
-
if (Pi.getSessionId(ctx) !== ownerId) return false;
|
|
428
|
-
} catch {
|
|
429
|
-
return false;
|
|
430
|
-
}
|
|
475
|
+
if (!ownerId || !isCurrentOwner(ctx)) return false;
|
|
431
476
|
if (flushSteers(ctx, ownerId)) return true;
|
|
432
477
|
if (!isDeliveryRoot(ownerId)) return false;
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const snapshot = Observability.readRunDeliverySnapshot(
|
|
436
|
-
deliveryObservation,
|
|
437
|
-
ownerId,
|
|
438
|
-
);
|
|
439
|
-
admitCompletionTransitions(ownerId, snapshot);
|
|
440
|
-
Observability.pruneRunUiObservationState(deliveryObservation, snapshot);
|
|
441
|
-
batch = journal(ownerId).completion_batch;
|
|
442
|
-
}
|
|
478
|
+
if (reconcileCompletionBatch(ctx, ownerId)) return true;
|
|
479
|
+
const batch = pendingCompletionBatch(ownerId);
|
|
443
480
|
if (!batch) return false;
|
|
444
|
-
if (batch.phase === "presented") {
|
|
445
|
-
finishPresentedBatch(ownerId, batch);
|
|
446
|
-
const snapshot = Observability.readRunDeliverySnapshot(
|
|
447
|
-
deliveryObservation,
|
|
448
|
-
ownerId,
|
|
449
|
-
);
|
|
450
|
-
admitCompletionTransitions(ownerId, snapshot);
|
|
451
|
-
Observability.pruneRunUiObservationState(deliveryObservation, snapshot);
|
|
452
|
-
batch = journal(ownerId).completion_batch;
|
|
453
|
-
if (!batch) return false;
|
|
454
|
-
}
|
|
455
481
|
if (!isIdle(ctx)) return true;
|
|
456
482
|
const content = RunDelivery.formatRunCompletionBatchMessage(batch);
|
|
457
|
-
if (batch.phase === "queued") {
|
|
458
|
-
if (!RunDelivery.markRunCompletionBatchPresented({
|
|
459
|
-
batchId: batch.batch_id,
|
|
460
|
-
ownerId,
|
|
461
|
-
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
462
|
-
})) return true;
|
|
463
|
-
const accepted = journal(ownerId).completion_batch;
|
|
464
|
-
if (accepted) finishPresentedBatch(ownerId, accepted);
|
|
465
|
-
return flushCompletionBatch(ctx);
|
|
466
|
-
}
|
|
467
483
|
if (!isIdle(ctx)) return true;
|
|
468
484
|
try {
|
|
469
485
|
Pi.sendRunCompletionBatch(deps.pi, batch.batch_id, content);
|
|
@@ -476,22 +492,9 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
476
492
|
});
|
|
477
493
|
throw error;
|
|
478
494
|
}
|
|
479
|
-
if (!
|
|
480
|
-
batchId: batch.batch_id,
|
|
481
|
-
ownerId,
|
|
482
|
-
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
483
|
-
})) {
|
|
484
|
-
throw new Error("Completion batch changed before queue acknowledgment");
|
|
485
|
-
}
|
|
486
|
-
if (!RunDelivery.markRunCompletionBatchPresented({
|
|
487
|
-
batchId: batch.batch_id,
|
|
488
|
-
ownerId,
|
|
489
|
-
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
490
|
-
})) {
|
|
495
|
+
if (!acceptCompletionBatch(ownerId, batch.batch_id)) {
|
|
491
496
|
throw new Error("Completion batch changed before durable acceptance");
|
|
492
497
|
}
|
|
493
|
-
const accepted = journal(ownerId).completion_batch;
|
|
494
|
-
if (accepted) finishPresentedBatch(ownerId, accepted);
|
|
495
498
|
ctx.ui.notify(
|
|
496
499
|
`Actor completions ready: ${batch.members.length}`,
|
|
497
500
|
"info",
|
|
@@ -502,14 +505,25 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
502
505
|
return {
|
|
503
506
|
close,
|
|
504
507
|
flushCompletionBatch,
|
|
508
|
+
onTurnEnd(event, ctx) {
|
|
509
|
+
const ownerId = activeOwnerId;
|
|
510
|
+
if (
|
|
511
|
+
!ownerId || !isCurrentOwner(ctx) || !isDeliveryRoot(ownerId) ||
|
|
512
|
+
event.outcome !== "completed" || isIdle(ctx) || ctx.signal?.aborted ||
|
|
513
|
+
ctx.hasPendingMessages?.() || event.context.pendingMessages.length > 0
|
|
514
|
+
) return undefined;
|
|
515
|
+
if (flushSteers(ctx, ownerId) || reconcileCompletionBatch(ctx, ownerId)) return undefined;
|
|
516
|
+
const batch = pendingCompletionBatch(ownerId);
|
|
517
|
+
if (!batch) return undefined;
|
|
518
|
+
const entry = Pi.createRunCompletionBatchEntry(
|
|
519
|
+
batch.batch_id,
|
|
520
|
+
RunDelivery.formatRunCompletionBatchMessage(batch),
|
|
521
|
+
);
|
|
522
|
+
return { entries: [...event.entries, entry], continue: true };
|
|
523
|
+
},
|
|
505
524
|
projectContext(messages, ctx) {
|
|
506
525
|
const ownerId = activeOwnerId;
|
|
507
|
-
if (!
|
|
508
|
-
try {
|
|
509
|
-
if (Pi.getSessionId(ctx) !== ownerId) return messages;
|
|
510
|
-
} catch {
|
|
511
|
-
return messages;
|
|
512
|
-
}
|
|
526
|
+
if (!ownerId || !isCurrentOwner(ctx)) return messages;
|
|
513
527
|
const steerContext = Pi.dedupeRunSteerContext(messages);
|
|
514
528
|
let contextMessages = steerContext.messages;
|
|
515
529
|
for (const steer of journal(ownerId).steers) {
|
|
@@ -549,18 +563,7 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
549
563
|
batch.batch_id,
|
|
550
564
|
);
|
|
551
565
|
}
|
|
552
|
-
|
|
553
|
-
batchId: batch.batch_id,
|
|
554
|
-
ownerId,
|
|
555
|
-
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
556
|
-
})) return projected.messages;
|
|
557
|
-
if (!RunDelivery.markRunCompletionBatchPresented({
|
|
558
|
-
batchId: batch.batch_id,
|
|
559
|
-
ownerId,
|
|
560
|
-
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
561
|
-
})) return projected.messages;
|
|
562
|
-
const presented = journal(ownerId).completion_batch;
|
|
563
|
-
if (presented) finishPresentedBatch(ownerId, presented);
|
|
566
|
+
acceptCompletionBatch(ownerId, batch.batch_id);
|
|
564
567
|
return projected.messages;
|
|
565
568
|
},
|
|
566
569
|
shutdown(eventReason, ownerId, ctx) {
|
|
@@ -107,7 +107,7 @@ Run = Recipe + Trace + Control
|
|
|
107
107
|
```
|
|
108
108
|
|
|
109
109
|
1. Spawn with the exact logical Recipe identity and caller-owned values.
|
|
110
|
-
2. Retain the returned `run:<id>` and normally wait for the root coordinator's
|
|
110
|
+
2. Retain the returned `run:<id>` and normally wait for the root coordinator's completion batch instead of polling. While Pi is active, ready results arrive together at the next safe model-step boundary after the full tool batch; already-idle Pi wakes through a debounced follow-up. Nested actor completions wait for their complete containing top-level tree and arrive in that one compressed batch; descendant sessions do not need notification options.
|
|
111
111
|
3. Inspect `view=trace` when retained observations or attention matter.
|
|
112
112
|
4. Inspect `view=control` before diagnosing service readiness, stale work, or saturation.
|
|
113
113
|
5. Send `message` only for an action declared and consumed by that controlled Recipe.
|
|
@@ -14,7 +14,7 @@ A rare Skill Recipe may declare `singleton: true`. Do not pass `as`: the runtime
|
|
|
14
14
|
|
|
15
15
|
## Observe
|
|
16
16
|
|
|
17
|
-
Normally wait for the
|
|
17
|
+
Normally wait for the completion batch: ready trees reach an active root coordinator at the next safe model-step boundary after the full tool batch, or wake an already-idle coordinator through a debounced follow-up. Inspect only when requested, when meaningful attention arrives, or when the Run is overdue or blocked:
|
|
18
18
|
|
|
19
19
|
```text
|
|
20
20
|
inspect target=run:<id> view=recipe
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
> Each release keeps at most 8 outcome records of at most 512 characters.
|
|
4
4
|
|
|
5
|
+
## 0.26.5: Telegram Mode Receipt Hotfix
|
|
6
|
+
|
|
7
|
+
- `Telegram mode controls`: Passive now refreshes the selected button on the first accepted click after Off, including existing-memory restoration and write-fence recovery during an active run. Accepted controls retain their renewed presentation receipt instead of being mistaken for canceled work; later mode choices, branch changes and shutdown still suppress obsolete updates. Memory storage and mode semantics are unchanged; no migration is required.
|
|
8
|
+
|
|
5
9
|
## 0.26.4: Passive Compaction Continuity
|
|
6
10
|
|
|
7
11
|
- `Passive continuation`: After idle Stop, native compaction can replace the later user anchor with a summary. Passive now retains that summary, the available tool calls/results and later refinements instead of reducing requests to a frozen handoff. Reload/resume preserve the same continuation; discarded raw history is not reconstructed. Native compaction, memory scopes and stored state are unchanged; no migration or configuration change is required.
|
|
@@ -1214,11 +1214,11 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
1214
1214
|
});
|
|
1215
1215
|
if (!modePersistenceError) {
|
|
1216
1216
|
appendCheckpoint();
|
|
1217
|
-
return { ok: true, message: `State Flow ${selectedMode}
|
|
1217
|
+
return { ok: true, message: `State Flow ${selectedMode}`, signal: sharedInspectionLifetime.signal };
|
|
1218
1218
|
}
|
|
1219
1219
|
const message = conciseDiagnostic(`State Flow ${selectedMode}; memory writes paused: ${modePersistenceError}`);
|
|
1220
1220
|
notifyProblem(ctx, message, "warning");
|
|
1221
|
-
return { ok: true, message };
|
|
1221
|
+
return { ok: true, message, signal: sharedInspectionLifetime.signal };
|
|
1222
1222
|
};
|
|
1223
1223
|
let accepted = false;
|
|
1224
1224
|
try {
|
|
@@ -1291,10 +1291,11 @@ export default function stateFlowExtension(pi, options = {}) {
|
|
|
1291
1291
|
const operation = selectMode(activeContext, mode);
|
|
1292
1292
|
if (mode === "active")
|
|
1293
1293
|
return operation;
|
|
1294
|
-
//
|
|
1294
|
+
// Native-only outcomes use the selected lifetime; async acceptance may renew and return its own receipt.
|
|
1295
1295
|
const signal = sharedInspectionLifetime.signal;
|
|
1296
1296
|
try {
|
|
1297
|
-
|
|
1297
|
+
const result = await operation;
|
|
1298
|
+
return { ...result, signal: result.signal ?? signal };
|
|
1298
1299
|
}
|
|
1299
1300
|
catch (error) {
|
|
1300
1301
|
return { ok: false, message: diagnosticText(error), signal };
|
|
@@ -138,7 +138,7 @@ The optional adapter presents one Off | Passive | Active row and uses the same m
|
|
|
138
138
|
- Inspection returns coherent state plus matching revisions, without publication.
|
|
139
139
|
- Controls and inspections acknowledge callbacks before waiting, suppress revoked results and escape late failures in the current menu.
|
|
140
140
|
- Synchronous mode-selection ports are supported.
|
|
141
|
-
-
|
|
141
|
+
- Passive acceptance returns its current presentation receipt after any internal restoration or lifetime renewal, including existing-memory reacquisition after Off and write-fence recovery. The adapter preserves that accepted receipt, so the first click refreshes the button in both busy and idle sessions. Pre-runtime shared-cache loading also retains its receipt. Later controls, branch changes and shutdown still revoke obsolete presentation, including a change away and back to the same mode while callback acknowledgement waits.
|
|
142
142
|
|
|
143
143
|
Adapter tests establish these contracts with isolated transport fixtures; they are not a live Telegram smoke test. Missing or unready transport remains fail-open and cannot change core memory behavior.
|
|
144
144
|
|
|
@@ -211,6 +211,7 @@ When `pi-telegram` is available, its main-menu section shows `State Flow: active
|
|
|
211
211
|
|
|
212
212
|
- A horizontal radio row presents `Off | Passive | Active`. The selected option uses 🟣, 🟡 or 🟢 respectively; each inactive option uses ⚫️.
|
|
213
213
|
- Telegram Active requested during a run waits for settlement; Passive and Off apply immediately.
|
|
214
|
+
- The first accepted Passive click refreshes the selected button after memory restoration or write-fence recovery, including while the agent is busy. A later mode or branch selection still suppresses an obsolete menu update.
|
|
214
215
|
- Ordinary successful controls add no redundant mode receipt; diagnostic outcomes stay visible.
|
|
215
216
|
- Off exposes neither tool nor State Flow model context.
|
|
216
217
|
|
|
@@ -1200,11 +1200,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1200
1200
|
});
|
|
1201
1201
|
if (!modePersistenceError) {
|
|
1202
1202
|
appendCheckpoint();
|
|
1203
|
-
return { ok: true, message: `State Flow ${selectedMode}
|
|
1203
|
+
return { ok: true, message: `State Flow ${selectedMode}`, signal: sharedInspectionLifetime.signal };
|
|
1204
1204
|
}
|
|
1205
1205
|
const message = conciseDiagnostic(`State Flow ${selectedMode}; memory writes paused: ${modePersistenceError}`);
|
|
1206
1206
|
notifyProblem(ctx, message, "warning");
|
|
1207
|
-
return { ok: true, message };
|
|
1207
|
+
return { ok: true, message, signal: sharedInspectionLifetime.signal };
|
|
1208
1208
|
};
|
|
1209
1209
|
let accepted = false;
|
|
1210
1210
|
try {
|
|
@@ -1272,10 +1272,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
|
|
|
1272
1272
|
if (!activeContext) throw new Error("State Flow is not attached to an active session yet");
|
|
1273
1273
|
const operation = selectMode(activeContext, mode);
|
|
1274
1274
|
if (mode === "active") return operation;
|
|
1275
|
-
//
|
|
1275
|
+
// Native-only outcomes use the selected lifetime; async acceptance may renew and return its own receipt.
|
|
1276
1276
|
const signal = sharedInspectionLifetime.signal;
|
|
1277
1277
|
try {
|
|
1278
|
-
|
|
1278
|
+
const result = await operation;
|
|
1279
|
+
return { ...result, signal: result.signal ?? signal };
|
|
1279
1280
|
} catch (error) {
|
|
1280
1281
|
return { ok: false, message: diagnosticText(error), signal };
|
|
1281
1282
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -44,12 +44,12 @@
|
|
|
44
44
|
"banner.jpg"
|
|
45
45
|
],
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@llblab/pi-actors": "0.
|
|
47
|
+
"@llblab/pi-actors": "0.55.0",
|
|
48
48
|
"@llblab/pi-claude-usage": "0.2.2",
|
|
49
49
|
"@llblab/pi-clean-room": "0.3.0",
|
|
50
50
|
"@llblab/pi-codex-usage": "0.13.1",
|
|
51
51
|
"@llblab/pi-grow-loop": "0.9.0",
|
|
52
|
-
"@llblab/pi-state-flow": "0.26.
|
|
52
|
+
"@llblab/pi-state-flow": "0.26.5",
|
|
53
53
|
"@llblab/pi-telegram": "0.57.3",
|
|
54
54
|
"@llblab/skills": "1.15.0"
|
|
55
55
|
},
|