@dougbots/avenor-pi 0.16.1 → 0.17.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 +57 -2
- package/dist/index.d.ts +37 -2
- package/dist/index.js +410 -101
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -14,6 +14,10 @@ which avenor
|
|
|
14
14
|
export AVENOR_BIN=~/.botfiles/bin/avenor
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
You also need Pi 1.4.0 or newer (`pi --version`). Busy-session delivery depends on
|
|
18
|
+
the `agent_before_settle` hook, so a notice raised while you were busy needs that
|
|
19
|
+
version to reach you. A missing notification is not a lost run.
|
|
20
|
+
|
|
17
21
|
## Installation
|
|
18
22
|
|
|
19
23
|
### From npm (once published)
|
|
@@ -144,8 +148,9 @@ Each roster entry must contain `backend` and at least one of `agent` or `model`.
|
|
|
144
148
|
Beyond the tools, the extension integrates with Pi's TUI and event system:
|
|
145
149
|
|
|
146
150
|
- **Status widget** — persistent widget showing all active runs with status, phase, and permission state
|
|
147
|
-
- **Footer status** — active runs
|
|
148
|
-
- **
|
|
151
|
+
- **Footer status** — the `avenor` status key carries active runs, the live agent-tree total, the unread notice count, and the current poll-failure count (see [Completion notices](#completion-notices))
|
|
152
|
+
- **Completion notices** — one notice per finished run, delivered immediately when your session is idle and batched into a single continuation when it was busy
|
|
153
|
+
- **Polling diagnostics** — bounded polling errors are emitted on `avenor:poll:error` for companion extensions; a completion-notice delivery that exhausts its retries is emitted separately on `avenor:delivery:error`
|
|
149
154
|
- **Live progress** — blocking `avenor_spawn` calls stream progress updates via `onUpdate`
|
|
150
155
|
- **Context enrichment** — active sub-agents are automatically surfaced in the system prompt via `before_agent_start`
|
|
151
156
|
- **Custom rendering** — The Pi extension renders all Avenor tool calls and results as themed, bounded summaries. Results have collapsed and expanded views.
|
|
@@ -155,6 +160,56 @@ Beyond the tools, the extension integrates with Pi's TUI and event system:
|
|
|
155
160
|
- `/avenor-watch <run_id>` — open a live event feed overlay
|
|
156
161
|
- `/avenor-cancel <run_id>` — cancel a running sub-agent
|
|
157
162
|
|
|
163
|
+
### Completion notices
|
|
164
|
+
|
|
165
|
+
A fire-and-forget spawn (`wait: false`) promises one thing: you hear about the
|
|
166
|
+
run when it ends. Each finished run produces one notice — label, terminal
|
|
167
|
+
status, bounded output preview — and where it lands depends on your session at
|
|
168
|
+
that moment.
|
|
169
|
+
|
|
170
|
+
- **Idle.** The notice is sent as a user message and starts its own turn. It
|
|
171
|
+
counts as unread until Pi confirms the message landed, and while compaction is
|
|
172
|
+
running the send waits for it to finish. A send that is accepted but never
|
|
173
|
+
confirmed is sent at most three times; once that budget is spent the notice
|
|
174
|
+
stays unread for the next settlement and the failure is emitted on
|
|
175
|
+
`avenor:delivery:error`.
|
|
176
|
+
- **Busy.** The notice is queued and the footer shows `unread:N`. The next
|
|
177
|
+
settlement that completes normally appends each queued notice as an
|
|
178
|
+
`avenor-completion` custom message under a single continuation, so three runs
|
|
179
|
+
finishing while you work mean three messages and one wake-up. A settlement
|
|
180
|
+
that ends in abort or error delivers nothing; the notices wait.
|
|
181
|
+
|
|
182
|
+
Every terminal status queues a notice — `done`, `failed`, `timeout`, `killed`.
|
|
183
|
+
|
|
184
|
+
`avenor_result` and a blocking `avenor_spawn` (`wait: true`) own delivery while
|
|
185
|
+
they run; `avenor_result` takes the label you spawned with, so you need not keep
|
|
186
|
+
the id. A `ready` result consumes the run: the queued notice is dropped,
|
|
187
|
+
`unread:N` falls by one, and nothing arrives for it later. A result that is not
|
|
188
|
+
`ready` — interrupted by a sibling's stopping point, timed out, or reporting a
|
|
189
|
+
pending permission — consumes nothing, and its notice arrives on the normal
|
|
190
|
+
schedule. A blocking spawn that reaches completion returns the result inline and
|
|
191
|
+
removes the notice; the footer can show a transient `unread:N` for that run while
|
|
192
|
+
the wait is in flight.
|
|
193
|
+
|
|
194
|
+
#### Footer status
|
|
195
|
+
|
|
196
|
+
One string under the status key `avenor`, in this order:
|
|
197
|
+
|
|
198
|
+
| Token | Meaning |
|
|
199
|
+
|---|---|
|
|
200
|
+
| `🟢 label — running [phase] 🔒 +2` | one per non-terminal run: status emoji, label, status, phase label, `🔒` for a pending permission, `+N` for that run's active descendants |
|
|
201
|
+
| `total:N` | direct active runs plus discovered active descendants; present only when active descendants exist |
|
|
202
|
+
| `unread:N` | notices not yet delivered: queued for a settlement, or in flight for an idle session |
|
|
203
|
+
| `errors:N` | recent poll failures; cleared by a cycle that adds no new error |
|
|
204
|
+
|
|
205
|
+
Missing `unread:N` or `errors:N` means zero. Without `total:N`, count the active run lines. `unread:N` counts notices,
|
|
206
|
+
not runs, and can stay set after the last run exits — an otherwise idle session
|
|
207
|
+
can still advertise `unread:1` until you consume or receive that notice.
|
|
208
|
+
`total:N` tracks live runs and disappears with them. `errors:N` outlives them
|
|
209
|
+
too: it clears when a later poll cycle adds no new error, so it counts recent
|
|
210
|
+
failures rather than a consecutive streak. The bounded history behind
|
|
211
|
+
`/avenor-errors` is longer than `errors:N`; running that command clears both.
|
|
212
|
+
|
|
158
213
|
### Tool-result channels
|
|
159
214
|
|
|
160
215
|
Pi returns JSON model content in `content[0].text` and structured `details` for
|
package/dist/index.d.ts
CHANGED
|
@@ -37,7 +37,11 @@ declare const CHANNEL_POLL_COMPLETED: "avenor:poll:completed";
|
|
|
37
37
|
declare const CHANNEL_RUN_TERMINAL: "avenor:run:terminal";
|
|
38
38
|
/** Emitted when a best-effort polling operation fails. */
|
|
39
39
|
declare const CHANNEL_POLL_ERROR: "avenor:poll:error";
|
|
40
|
+
/** Emitted when a completion-notice delivery fails permanently. */
|
|
41
|
+
declare const CHANNEL_DELIVERY_ERROR: "avenor:delivery:error";
|
|
40
42
|
type PollErrorSource = 'singleton-list' | 'run-status' | 'spawn-status';
|
|
43
|
+
/** Delivery paths whose retries can be exhausted. */
|
|
44
|
+
type DeliveryErrorSource = 'idle-send';
|
|
41
45
|
interface PollErrorPayload {
|
|
42
46
|
/** Polling operation that failed. */
|
|
43
47
|
readonly source: PollErrorSource;
|
|
@@ -60,6 +64,31 @@ interface PollErrorPayloadInput {
|
|
|
60
64
|
readonly count: number;
|
|
61
65
|
readonly timestamp: number;
|
|
62
66
|
}
|
|
67
|
+
interface DeliveryErrorPayload {
|
|
68
|
+
/** Delivery path that failed. */
|
|
69
|
+
readonly source: DeliveryErrorSource;
|
|
70
|
+
/** Run whose completion notice is stuck unread. */
|
|
71
|
+
readonly runId: string;
|
|
72
|
+
/** Label of the run, as shown in completion notices. */
|
|
73
|
+
readonly label: string;
|
|
74
|
+
/** Human-readable failure description. */
|
|
75
|
+
readonly message: string;
|
|
76
|
+
/** Bounded, sanitized error text suitable for debug consumers. */
|
|
77
|
+
readonly error: string;
|
|
78
|
+
/** Delivery attempts made for this notice. */
|
|
79
|
+
readonly attempts: number;
|
|
80
|
+
/** Wall-clock timestamp in epoch milliseconds. */
|
|
81
|
+
readonly timestamp: number;
|
|
82
|
+
}
|
|
83
|
+
interface DeliveryErrorPayloadInput {
|
|
84
|
+
readonly source: DeliveryErrorSource;
|
|
85
|
+
readonly runId: string;
|
|
86
|
+
readonly label: string;
|
|
87
|
+
readonly message: string;
|
|
88
|
+
readonly error: unknown;
|
|
89
|
+
readonly attempts: number;
|
|
90
|
+
readonly timestamp: number;
|
|
91
|
+
}
|
|
63
92
|
type AvenorRunStatus = Readonly<Omit<RunStatusEntry, 'supervisorId' | 'pid' | 'permissionDescription'> & {
|
|
64
93
|
supervisorKey: string;
|
|
65
94
|
}>;
|
|
@@ -79,7 +108,7 @@ interface RunTerminalPayload {
|
|
|
79
108
|
readonly label: string;
|
|
80
109
|
readonly agent: string;
|
|
81
110
|
readonly status: string;
|
|
82
|
-
/** Number of active descendants
|
|
111
|
+
/** Number of active descendants last seen for the run, if available. */
|
|
83
112
|
readonly nestedCount?: number;
|
|
84
113
|
readonly backend?: string;
|
|
85
114
|
}
|
|
@@ -98,6 +127,7 @@ interface AvenorEventMap {
|
|
|
98
127
|
[CHANNEL_POLL_COMPLETED]: PollCompletedPayload;
|
|
99
128
|
[CHANNEL_RUN_TERMINAL]: RunTerminalPayload;
|
|
100
129
|
[CHANNEL_POLL_ERROR]: PollErrorPayload;
|
|
130
|
+
[CHANNEL_DELIVERY_ERROR]: DeliveryErrorPayload;
|
|
101
131
|
}
|
|
102
132
|
type AvenorEventChannel = keyof AvenorEventMap;
|
|
103
133
|
type AvenorEventBus = Pick<EventBus, 'emit' | 'on'>;
|
|
@@ -113,6 +143,8 @@ declare function onAvenorEvent<K extends AvenorEventChannel>(bus: Pick<EventBus,
|
|
|
113
143
|
declare function createPollCompletedPayload(entries: readonly RunStatusEntry[], timestamp: number, generation: number): PollCompletedPayload;
|
|
114
144
|
/** Copy and freeze a terminal payload before publishing it. */
|
|
115
145
|
declare function createRunTerminalPayload(payload: RunTerminalPayloadInput): RunTerminalPayload;
|
|
146
|
+
/** Copy, bound, sanitize, and freeze a delivery failure before publishing it. */
|
|
147
|
+
declare function createDeliveryErrorPayload(payload: DeliveryErrorPayloadInput): DeliveryErrorPayload;
|
|
116
148
|
/** Copy, bound, sanitize, and freeze a polling error before publishing it. */
|
|
117
149
|
declare function createPollErrorPayload(payload: PollErrorPayloadInput): PollErrorPayload;
|
|
118
150
|
//#endregion
|
|
@@ -167,10 +199,13 @@ declare function buildInspectPayload(result: InspectResult): Record<string, unkn
|
|
|
167
199
|
interface ExtensionOptions {
|
|
168
200
|
/** Polling cadence; injectable so tests can drive deterministic ticks. */
|
|
169
201
|
pollIntervalMs?: number;
|
|
202
|
+
/** How long an unconfirmed idle completion send waits before re-arming;
|
|
203
|
+
* injectable so tests can exercise the timeout without fixed sleeps. */
|
|
204
|
+
idleSendTimeoutMs?: number;
|
|
170
205
|
/** Overrides broker-credential autodetection for sub-agent broker mode (tests). */
|
|
171
206
|
subAgentEnv?: SubAgentEnv | null;
|
|
172
207
|
}
|
|
173
208
|
declare function createExtension(deps?: ExtensionDeps, options?: ExtensionOptions): (pi: ExtensionAPI) => Promise<void>;
|
|
174
209
|
declare const _default: (pi: ExtensionAPI) => Promise<void>;
|
|
175
210
|
//#endregion
|
|
176
|
-
export { type AvenorEventBus, type AvenorEventChannel, type AvenorEventMap, type AvenorRunStatus, CHANNEL_POLL_COMPLETED, CHANNEL_POLL_ERROR, CHANNEL_RUN_TERMINAL, ExtensionDeps, ExtensionOptions, type PollCompletedPayload, type PollErrorPayload, type PollErrorPayloadInput, type PollErrorSource, type RunTerminalPayload, type RunTerminalPayloadInput, buildCompletionText, buildInspectPayload, compactWhitespace, createExtension, createPollCompletedPayload, createPollErrorPayload, createRunTerminalPayload, _default as default, emitAvenorEvent, isTerminalStatus, onAvenorEvent, renderStatusLines, spawnIdentityMetadata, statusSupervisorId };
|
|
211
|
+
export { type AvenorEventBus, type AvenorEventChannel, type AvenorEventMap, type AvenorRunStatus, CHANNEL_DELIVERY_ERROR, CHANNEL_POLL_COMPLETED, CHANNEL_POLL_ERROR, CHANNEL_RUN_TERMINAL, type DeliveryErrorPayload, type DeliveryErrorPayloadInput, type DeliveryErrorSource, ExtensionDeps, ExtensionOptions, type PollCompletedPayload, type PollErrorPayload, type PollErrorPayloadInput, type PollErrorSource, type RunTerminalPayload, type RunTerminalPayloadInput, buildCompletionText, buildInspectPayload, compactWhitespace, createDeliveryErrorPayload, createExtension, createPollCompletedPayload, createPollErrorPayload, createRunTerminalPayload, _default as default, emitAvenorEvent, isTerminalStatus, onAvenorEvent, renderStatusLines, spawnIdentityMetadata, statusSupervisorId };
|
package/dist/index.js
CHANGED
|
@@ -106,6 +106,14 @@ async function postBroker(env, path, body, fetchImpl = fetch, signal) {
|
|
|
106
106
|
if (!res.ok) throw await codedError(path, res);
|
|
107
107
|
return res.json();
|
|
108
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Publish the harness's own user-defined session name for this run, or clear it
|
|
111
|
+
* with an empty name. The broker stores it apart from any model-set rename and
|
|
112
|
+
* prefers it, so peers see the name the human gave the session.
|
|
113
|
+
*/
|
|
114
|
+
async function publishHarnessName(env, name, fetchImpl = fetch) {
|
|
115
|
+
await postBroker(env, "/session_name", { name: name ?? "" }, fetchImpl);
|
|
116
|
+
}
|
|
109
117
|
/** Authenticated GET; broker credentials travel in headers, never the URL. */
|
|
110
118
|
async function getBroker(env, path, fetchImpl = fetch, signal) {
|
|
111
119
|
const res = await fetchImpl(`${env.brokerUrl}${path}`, {
|
|
@@ -536,7 +544,13 @@ function createBrokerChildBroker(env, fetchImpl = fetch) {
|
|
|
536
544
|
}
|
|
537
545
|
},
|
|
538
546
|
async peers() {
|
|
539
|
-
|
|
547
|
+
const body = await getBroker(env, "/sessions", fetchImpl);
|
|
548
|
+
const peers = peerRecords(body);
|
|
549
|
+
const detail = body !== null && typeof body === "object" ? body.peers_error : void 0;
|
|
550
|
+
return typeof detail === "string" && detail ? {
|
|
551
|
+
peers,
|
|
552
|
+
detail
|
|
553
|
+
} : { peers };
|
|
540
554
|
},
|
|
541
555
|
async rename(req) {
|
|
542
556
|
try {
|
|
@@ -1136,21 +1150,6 @@ function formatRunLine(entry) {
|
|
|
1136
1150
|
const phase = entry.phaseLabel ? ` (${entry.phaseLabel})` : "";
|
|
1137
1151
|
return `${emoji} ${entry.label} (${entry.agent}) — ${entry.status}${phase}${perm}`;
|
|
1138
1152
|
}
|
|
1139
|
-
/**
|
|
1140
|
-
* decideCompletion determines whether to skip or send the completion
|
|
1141
|
-
* steering message for a tracked run that has reached terminal status.
|
|
1142
|
-
*
|
|
1143
|
-
* The message is delivered on the same terminal tick — never deferred to a
|
|
1144
|
-
* later cycle — so a run that is the last live one still gets its completion.
|
|
1145
|
-
* `blocking` suppresses it while avenor_result is awaiting (it will deliver
|
|
1146
|
-
* the result itself and set `consumed`), and `consumed` suppresses it once
|
|
1147
|
-
* avenor_result has already returned the result successfully.
|
|
1148
|
-
*/
|
|
1149
|
-
function decideCompletion(run) {
|
|
1150
|
-
if (run.blocking) return "skip";
|
|
1151
|
-
if (run.consumed) return "skip";
|
|
1152
|
-
return "send";
|
|
1153
|
-
}
|
|
1154
1153
|
//#endregion
|
|
1155
1154
|
//#region src/nested-count.ts
|
|
1156
1155
|
const NESTED_DIAL_TIMEOUT_MS = 2e3;
|
|
@@ -1303,6 +1302,8 @@ const CHANNEL_POLL_COMPLETED = "avenor:poll:completed";
|
|
|
1303
1302
|
const CHANNEL_RUN_TERMINAL = "avenor:run:terminal";
|
|
1304
1303
|
/** Emitted when a best-effort polling operation fails. */
|
|
1305
1304
|
const CHANNEL_POLL_ERROR = "avenor:poll:error";
|
|
1305
|
+
/** Emitted when a completion-notice delivery fails permanently. */
|
|
1306
|
+
const CHANNEL_DELIVERY_ERROR = "avenor:delivery:error";
|
|
1306
1307
|
/** Maximum sanitized error text exposed to companion extensions. */
|
|
1307
1308
|
const POLL_ERROR_TEXT_CHARS = 600;
|
|
1308
1309
|
function sanitizeAndBoundText(value) {
|
|
@@ -1360,6 +1361,18 @@ function createRunTerminalPayload(payload) {
|
|
|
1360
1361
|
...payload.backend !== void 0 && { backend: payload.backend }
|
|
1361
1362
|
});
|
|
1362
1363
|
}
|
|
1364
|
+
/** Copy, bound, sanitize, and freeze a delivery failure before publishing it. */
|
|
1365
|
+
function createDeliveryErrorPayload(payload) {
|
|
1366
|
+
return Object.freeze({
|
|
1367
|
+
source: payload.source,
|
|
1368
|
+
runId: payload.runId,
|
|
1369
|
+
label: payload.label,
|
|
1370
|
+
message: sanitizeAndBoundText(payload.message),
|
|
1371
|
+
error: sanitizeAndBoundText(payload.error),
|
|
1372
|
+
attempts: payload.attempts,
|
|
1373
|
+
timestamp: payload.timestamp
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1363
1376
|
/** Copy, bound, sanitize, and freeze a polling error before publishing it. */
|
|
1364
1377
|
function createPollErrorPayload(payload) {
|
|
1365
1378
|
return Object.freeze({
|
|
@@ -1920,6 +1933,8 @@ function asStatusResult(raw) {
|
|
|
1920
1933
|
return raw;
|
|
1921
1934
|
}
|
|
1922
1935
|
const POLL_INTERVAL_MS = 3e3;
|
|
1936
|
+
const IDLE_SEND_TIMEOUT_MS = 1e4;
|
|
1937
|
+
const MAX_IDLE_SEND_ATTEMPTS = 3;
|
|
1923
1938
|
const INSPECT_LIMIT = 128;
|
|
1924
1939
|
const COMPLETION_OUTPUT_CHARS = 600;
|
|
1925
1940
|
const INSPECT_JSON_STRING_CHARS = 600;
|
|
@@ -2092,6 +2107,7 @@ function trackedRunKey(supervisorId, runId) {
|
|
|
2092
2107
|
function createExtension(deps = defaultDeps, options = {}) {
|
|
2093
2108
|
return async function(pi) {
|
|
2094
2109
|
const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
2110
|
+
const idleSendTimeoutMs = options.idleSendTimeoutMs ?? IDLE_SEND_TIMEOUT_MS;
|
|
2095
2111
|
let telemetryGeneration = 0;
|
|
2096
2112
|
const trackedRuns = /* @__PURE__ */ new Map();
|
|
2097
2113
|
let pollingActive = false;
|
|
@@ -2101,6 +2117,8 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2101
2117
|
let pollingErrorCount = 0;
|
|
2102
2118
|
const pollingErrors = [];
|
|
2103
2119
|
let lastStatusEntries = [];
|
|
2120
|
+
let sessionGeneration = 0;
|
|
2121
|
+
let compacting = false;
|
|
2104
2122
|
let lastTurnSocketClosed = false;
|
|
2105
2123
|
const pendingWaits = /* @__PURE__ */ new Set();
|
|
2106
2124
|
/** The agent-facing prose clause for a stopping point. A synthetic status
|
|
@@ -2174,12 +2192,16 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2174
2192
|
};
|
|
2175
2193
|
}
|
|
2176
2194
|
/** Build the early return for a wait interrupted by a sibling stopping
|
|
2177
|
-
* point. When the awaited run itself has since reached a stopping point
|
|
2178
|
-
*
|
|
2179
|
-
*
|
|
2195
|
+
* point. When the awaited run itself has since reached a stopping point,
|
|
2196
|
+
* deliver its real state instead of an interruption notice. The probe and
|
|
2197
|
+
* redelivery use the caller's original run reference, matching the non-
|
|
2198
|
+
* interrupted path: a label may resolve canonically to a newer run than
|
|
2199
|
+
* the one this call first bound to, and the reconciliation below handles
|
|
2200
|
+
* that divergence the same way. A caller that omitted supervisor_id still
|
|
2201
|
+
* probes through the bound run's supervisor. */
|
|
2180
2202
|
async function resultAfterInterrupt(params, stop, tracked) {
|
|
2181
|
-
const runId =
|
|
2182
|
-
const supervisorId = tracked?.supervisorId
|
|
2203
|
+
const runId = params.run_id;
|
|
2204
|
+
const supervisorId = params.supervisor_id ?? tracked?.supervisorId;
|
|
2183
2205
|
let current;
|
|
2184
2206
|
try {
|
|
2185
2207
|
current = asStatusResult(await deps.statusTool({
|
|
@@ -2229,6 +2251,35 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2229
2251
|
subAgentPollTimer = null;
|
|
2230
2252
|
}
|
|
2231
2253
|
}
|
|
2254
|
+
let publishedSessionName = null;
|
|
2255
|
+
let publishChain = Promise.resolve();
|
|
2256
|
+
let refusedSessionName = null;
|
|
2257
|
+
function publishSessionName(ctx) {
|
|
2258
|
+
if (!subAgentEnv) return publishChain;
|
|
2259
|
+
const env = subAgentEnv;
|
|
2260
|
+
const manager = ctx.sessionManager;
|
|
2261
|
+
if (!manager || typeof manager.getSessionName !== "function") return publishChain;
|
|
2262
|
+
const name = manager.getSessionName();
|
|
2263
|
+
if (publishedSessionName && publishedSessionName.name === name) return publishChain;
|
|
2264
|
+
if (refusedSessionName === name) return publishChain;
|
|
2265
|
+
const attempt = { name };
|
|
2266
|
+
publishedSessionName = attempt;
|
|
2267
|
+
publishChain = publishChain.then(async () => {
|
|
2268
|
+
try {
|
|
2269
|
+
await publishHarnessName(env, name);
|
|
2270
|
+
refusedSessionName = null;
|
|
2271
|
+
} catch (error) {
|
|
2272
|
+
if (error instanceof BrokerHttpError && error.status === 400 && error.code === "invalid-name") {
|
|
2273
|
+
refusedSessionName = name;
|
|
2274
|
+
console.error("[avenor] session_name_set refused:", error.message);
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
if (publishedSessionName === attempt) publishedSessionName = null;
|
|
2278
|
+
console.error("[avenor] session_name_set failed:", error instanceof Error ? error.message : error);
|
|
2279
|
+
}
|
|
2280
|
+
});
|
|
2281
|
+
return publishChain;
|
|
2282
|
+
}
|
|
2232
2283
|
async function startSubAgentPoll() {
|
|
2233
2284
|
if (!subAgentEnv) return;
|
|
2234
2285
|
stopSubAgentPoll();
|
|
@@ -2313,6 +2364,40 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2313
2364
|
if (pollingErrors.length > MAX_POLL_ERRORS) pollingErrors.shift();
|
|
2314
2365
|
emitAvenorEvent(pi.events, CHANNEL_POLL_ERROR, payload);
|
|
2315
2366
|
}
|
|
2367
|
+
/** A delivery that exhausted its retries is not a polling failure: emit
|
|
2368
|
+
* it on its own channel so `avenor:poll:error` consumers keep a pure
|
|
2369
|
+
* polling contract. The unread notice is the persistent user-facing
|
|
2370
|
+
* surface; this event is the companion-extension signal. */
|
|
2371
|
+
function recordDeliveryError(run, attempts) {
|
|
2372
|
+
emitAvenorEvent(pi.events, CHANNEL_DELIVERY_ERROR, createDeliveryErrorPayload({
|
|
2373
|
+
source: "idle-send",
|
|
2374
|
+
runId: run.runId,
|
|
2375
|
+
label: run.label,
|
|
2376
|
+
message: "idle completion send was never confirmed",
|
|
2377
|
+
error: /* @__PURE__ */ new Error(`no message_end after ${attempts} attempts`),
|
|
2378
|
+
attempts,
|
|
2379
|
+
timestamp: Date.now()
|
|
2380
|
+
}));
|
|
2381
|
+
console.error(`[avenor] idle completion send for run "${run.label}" was never confirmed after ${attempts} attempts; the notice stays unread`);
|
|
2382
|
+
}
|
|
2383
|
+
/** Latch the per-notice retry budget as spent and report the exhausted
|
|
2384
|
+
* delivery. Idempotent, so the timeout re-arm and the compaction
|
|
2385
|
+
* hand-back cannot double-report the same notice. */
|
|
2386
|
+
function capIdleSend(run) {
|
|
2387
|
+
if (!run.idleSend || run.idleSend.capped) return;
|
|
2388
|
+
run.idleSend.capped = true;
|
|
2389
|
+
recordDeliveryError(run, run.idleSend.attempts);
|
|
2390
|
+
}
|
|
2391
|
+
/** Hand an unconfirmed idle send back to the poll: clear the in-flight
|
|
2392
|
+
* mark and latch the budget when it is spent. Shared by the poll-tick
|
|
2393
|
+
* timeout and the compaction hand-back, which must stay in lockstep. */
|
|
2394
|
+
function rearmIdleSend(run) {
|
|
2395
|
+
const idle = run.idleSend;
|
|
2396
|
+
if (!idle) return;
|
|
2397
|
+
idle.inFlight = false;
|
|
2398
|
+
idle.sentAt = void 0;
|
|
2399
|
+
if (idle.attempts >= MAX_IDLE_SEND_ATTEMPTS) capIdleSend(run);
|
|
2400
|
+
}
|
|
2316
2401
|
function clearPollingErrors() {
|
|
2317
2402
|
pollingErrorCount = 0;
|
|
2318
2403
|
pollingErrors.length = 0;
|
|
@@ -2321,38 +2406,75 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2321
2406
|
function upsertTrackedRun(run, status, blocking = false) {
|
|
2322
2407
|
const key = trackedRunKey(run.supervisorId, run.runId);
|
|
2323
2408
|
const current = trackedRuns.get(key);
|
|
2324
|
-
|
|
2409
|
+
const tracked = current ?? {
|
|
2325
2410
|
runId: run.runId,
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2411
|
+
startTime: Date.now(),
|
|
2412
|
+
agent: "unknown",
|
|
2413
|
+
label: run.label ?? run.runId
|
|
2414
|
+
};
|
|
2415
|
+
tracked.runId = run.runId;
|
|
2416
|
+
tracked.agent = run.agent ?? status?.agent ?? current?.agent ?? tracked.agent;
|
|
2417
|
+
tracked.label = run.label ?? status?.label ?? current?.label ?? tracked.label;
|
|
2418
|
+
tracked.supervisorId = run.supervisorId ?? current?.supervisorId;
|
|
2419
|
+
tracked.runtimeId = run.runtimeId ?? status?.runtime_id ?? current?.runtimeId;
|
|
2420
|
+
if (!current) tracked.startTime = Date.now();
|
|
2421
|
+
tracked.lastStatus = status ?? current?.lastStatus;
|
|
2422
|
+
tracked.blocking = current?.blocking ?? blocking;
|
|
2423
|
+
trackedRuns.set(key, tracked);
|
|
2424
|
+
}
|
|
2425
|
+
function findTrackedByLabel(label, supervisorId) {
|
|
2426
|
+
let match;
|
|
2427
|
+
for (const run of trackedRuns.values()) {
|
|
2428
|
+
if (supervisorId !== void 0 && run.supervisorId !== supervisorId) continue;
|
|
2429
|
+
if (run.label !== label) continue;
|
|
2430
|
+
if (match) return void 0;
|
|
2431
|
+
match = run;
|
|
2432
|
+
}
|
|
2433
|
+
return match;
|
|
2336
2434
|
}
|
|
2337
2435
|
/**
|
|
2338
|
-
*
|
|
2339
|
-
* supervisor is supplied, resolve exactly. Without one, prefer the
|
|
2436
|
+
* Resolve a canonical run id in the per-supervisor tracking namespace.
|
|
2437
|
+
* When a supervisor is supplied, resolve exactly. Without one, prefer the
|
|
2340
2438
|
* singleton namespace and only fall back to a run id walk when it is
|
|
2341
2439
|
* unambiguous across supervisors. Duplicate run ids across supervisors
|
|
2342
2440
|
* without an explicit supervisor return undefined so callers never bind
|
|
2343
2441
|
* flags or telemetry to the wrong supervisor's run.
|
|
2344
2442
|
*/
|
|
2345
|
-
function
|
|
2346
|
-
if (supervisorId !== void 0)
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
let match;
|
|
2350
|
-
for (const run of trackedRuns.values()) {
|
|
2351
|
-
if (run.runId !== runId) continue;
|
|
2352
|
-
if (match) return void 0;
|
|
2353
|
-
match = run;
|
|
2443
|
+
function trackedRunsById(runId, supervisorId) {
|
|
2444
|
+
if (supervisorId !== void 0) {
|
|
2445
|
+
const exact = trackedRuns.get(trackedRunKey(supervisorId, runId));
|
|
2446
|
+
return exact ? [exact] : [];
|
|
2354
2447
|
}
|
|
2355
|
-
|
|
2448
|
+
const singleton = trackedRuns.get(trackedRunKey(void 0, runId));
|
|
2449
|
+
if (singleton) return [singleton];
|
|
2450
|
+
const matches = [];
|
|
2451
|
+
for (const run of trackedRuns.values()) if (run.runId === runId) matches.push(run);
|
|
2452
|
+
return matches;
|
|
2453
|
+
}
|
|
2454
|
+
function getTrackedRunById(runId, supervisorId) {
|
|
2455
|
+
const matches = trackedRunsById(runId, supervisorId);
|
|
2456
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
2457
|
+
}
|
|
2458
|
+
/**
|
|
2459
|
+
* Resolve a user-supplied run reference. A canonical identity always wins;
|
|
2460
|
+
* a label is only a fallback, and an ambiguous canonical id never falls
|
|
2461
|
+
* through to a label.
|
|
2462
|
+
*/
|
|
2463
|
+
function getTrackedRun(runId, supervisorId) {
|
|
2464
|
+
const exact = getTrackedRunById(runId, supervisorId);
|
|
2465
|
+
if (exact) return exact;
|
|
2466
|
+
if (trackedRunsById(runId, supervisorId).length > 1) return void 0;
|
|
2467
|
+
return findTrackedByLabel(runId, supervisorId);
|
|
2468
|
+
}
|
|
2469
|
+
/** Resolve the tracked run a successful result belongs to from the
|
|
2470
|
+
* canonical run id the result tool reports, accepting a runtime-id alias
|
|
2471
|
+
* for the label-resolved run. */
|
|
2472
|
+
function reconcileResultRun(tracked, result, supervisorId) {
|
|
2473
|
+
const runId = result.run_id;
|
|
2474
|
+
if (!runId) return tracked;
|
|
2475
|
+
if (tracked && (tracked.runId === runId || tracked.runtimeId === runId)) return tracked;
|
|
2476
|
+
const namespace = tracked?.supervisorId ?? supervisorId;
|
|
2477
|
+
return trackedRuns.get(trackedRunKey(namespace, runId));
|
|
2356
2478
|
}
|
|
2357
2479
|
async function resolveFinalOutput(run, status) {
|
|
2358
2480
|
const preview = finalOutputPreview(status);
|
|
@@ -2368,6 +2490,67 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2368
2490
|
return;
|
|
2369
2491
|
}
|
|
2370
2492
|
}
|
|
2493
|
+
/**
|
|
2494
|
+
* Fire-and-forget delivery. Pi's extension-facing `sendUserMessage` returns
|
|
2495
|
+
* void and the runtime swallows async rejections, so a send is only known to
|
|
2496
|
+
* have landed when the user message comes back through `message_end`. Only a
|
|
2497
|
+
* synchronous throw is observable here.
|
|
2498
|
+
*/
|
|
2499
|
+
function sendUserMessageLogged(text, failureLog) {
|
|
2500
|
+
try {
|
|
2501
|
+
pi.sendUserMessage(text, { deliverAs: "followUp" });
|
|
2502
|
+
} catch (err) {
|
|
2503
|
+
console.error(failureLog, err);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
function clearPendingNotices() {
|
|
2507
|
+
for (const run of trackedRuns.values()) {
|
|
2508
|
+
run.pendingNotice = void 0;
|
|
2509
|
+
run.idleSend = void 0;
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
/**
|
|
2513
|
+
* Emit the single terminal event for a tracked run. Every terminal path
|
|
2514
|
+
* (poll, blocking spawn, avenor_result) goes through here so the event-once
|
|
2515
|
+
* guard and payload shape stay identical. A tracked run emits at most once;
|
|
2516
|
+
* an untracked run emits with the canonical result identity, unguarded.
|
|
2517
|
+
*
|
|
2518
|
+
* @returns whether the event was emitted, so callers can gate one-time
|
|
2519
|
+
* side effects.
|
|
2520
|
+
*/
|
|
2521
|
+
function emitRunTerminal(input) {
|
|
2522
|
+
const { run } = input;
|
|
2523
|
+
if (!run) {
|
|
2524
|
+
if (!input.runId) return false;
|
|
2525
|
+
emitAvenorEvent(pi.events, CHANNEL_RUN_TERMINAL, createRunTerminalPayload({
|
|
2526
|
+
runId: input.runId,
|
|
2527
|
+
supervisorId: input.supervisorId,
|
|
2528
|
+
runtimeId: input.runtimeId,
|
|
2529
|
+
label: input.label,
|
|
2530
|
+
agent: input.agent,
|
|
2531
|
+
status: input.status,
|
|
2532
|
+
nestedCount: input.nestedCount,
|
|
2533
|
+
backend: input.backend
|
|
2534
|
+
}));
|
|
2535
|
+
return true;
|
|
2536
|
+
}
|
|
2537
|
+
if (run.terminalEventEmitted) return false;
|
|
2538
|
+
run.terminalEventEmitted = true;
|
|
2539
|
+
emitAvenorEvent(pi.events, CHANNEL_RUN_TERMINAL, createRunTerminalPayload({
|
|
2540
|
+
runId: run.runId,
|
|
2541
|
+
supervisorId: run.supervisorId,
|
|
2542
|
+
runtimeId: input.runtimeId,
|
|
2543
|
+
label: input.label,
|
|
2544
|
+
agent: input.agent,
|
|
2545
|
+
status: input.status,
|
|
2546
|
+
nestedCount: input.nestedCount,
|
|
2547
|
+
backend: input.backend
|
|
2548
|
+
}));
|
|
2549
|
+
return true;
|
|
2550
|
+
}
|
|
2551
|
+
function sendCompletionNotice(notice) {
|
|
2552
|
+
sendUserMessageLogged(notice.text, "avenor tick: sendUserMessage failed");
|
|
2553
|
+
}
|
|
2371
2554
|
async function pollRuns() {
|
|
2372
2555
|
const errorCountAtPollStart = pollingErrorCount;
|
|
2373
2556
|
const liveMap = /* @__PURE__ */ new Map();
|
|
@@ -2460,7 +2643,11 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2460
2643
|
if (nestedPids.length > 0) {
|
|
2461
2644
|
const nestedCounts = await Promise.all(nestedPids.map((pid) => countNestedRuns(pid, deps.dial)));
|
|
2462
2645
|
let idx = 0;
|
|
2463
|
-
for (const entry of entries) if (entry.backend === "pi" && entry.pid && !isTerminalStatus(entry.status))
|
|
2646
|
+
for (const entry of entries) if (entry.backend === "pi" && entry.pid && !isTerminalStatus(entry.status)) {
|
|
2647
|
+
entry.nestedCount = nestedCounts[idx++] ?? 0;
|
|
2648
|
+
const tracked = trackedRuns.get(trackedRunKey(entry.supervisorId, entry.runId));
|
|
2649
|
+
if (tracked) tracked.nestedCount = entry.nestedCount;
|
|
2650
|
+
}
|
|
2464
2651
|
}
|
|
2465
2652
|
if (pollingErrorCount === errorCountAtPollStart) pollingErrorCount = 0;
|
|
2466
2653
|
return entries;
|
|
@@ -2469,17 +2656,24 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2469
2656
|
lastStatusEntries = entries;
|
|
2470
2657
|
if (!sessionCtx) return;
|
|
2471
2658
|
const lines = renderStatusLines(entries);
|
|
2659
|
+
let unread = 0;
|
|
2660
|
+
for (const run of trackedRuns.values()) if (run.pendingNotice) unread++;
|
|
2472
2661
|
const errorSuffix = pollingErrorCount > 0 ? `errors:${pollingErrorCount}` : "";
|
|
2662
|
+
const unreadSuffix = unread > 0 ? `unread:${unread}` : "";
|
|
2473
2663
|
if (lines.length > 0) {
|
|
2474
2664
|
sessionCtx.ui.setWidget("avenor-status", lines);
|
|
2475
2665
|
const statusStr = lines.map((line) => line.slice(0, 60)).join(" ");
|
|
2476
2666
|
const nestedTotal = entries.filter((e) => !isTerminalStatus(e.status)).reduce((sum, e) => sum + (e.nestedCount ?? 0), 0);
|
|
2477
2667
|
const total = lines.length + nestedTotal;
|
|
2478
|
-
const statusWithTotal = [
|
|
2668
|
+
const statusWithTotal = [
|
|
2669
|
+
nestedTotal > 0 ? `${statusStr} total:${total}` : statusStr,
|
|
2670
|
+
unreadSuffix,
|
|
2671
|
+
errorSuffix
|
|
2672
|
+
].filter(Boolean).join(" ");
|
|
2479
2673
|
sessionCtx.ui.setStatus("avenor", statusWithTotal);
|
|
2480
2674
|
} else {
|
|
2481
2675
|
sessionCtx.ui.setWidget("avenor-status", void 0);
|
|
2482
|
-
sessionCtx.ui.setStatus("avenor", errorSuffix);
|
|
2676
|
+
sessionCtx.ui.setStatus("avenor", [unreadSuffix, errorSuffix].filter(Boolean).join(" "));
|
|
2483
2677
|
}
|
|
2484
2678
|
}
|
|
2485
2679
|
function startPolling() {
|
|
@@ -2500,12 +2694,20 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2500
2694
|
});
|
|
2501
2695
|
if (!pollingActive || generation !== pollingGeneration) return;
|
|
2502
2696
|
updateWidget(entries);
|
|
2697
|
+
let widgetDirty = false;
|
|
2698
|
+
const sessionAtPollStart = sessionGeneration;
|
|
2503
2699
|
emitAvenorEvent(pi.events, CHANNEL_POLL_COMPLETED, createPollCompletedPayload(entries, Date.now(), ++telemetryGeneration));
|
|
2504
2700
|
for (const entry of entries) {
|
|
2505
2701
|
const entryKey = trackedRunKey(entry.supervisorId, entry.runId);
|
|
2506
2702
|
const run = trackedRuns.get(entryKey);
|
|
2507
2703
|
if (!run) continue;
|
|
2508
2704
|
const prevStatus = prevStatuses.get(entryKey);
|
|
2705
|
+
const idle = run.idleSend;
|
|
2706
|
+
if (idle?.inFlight && idle.sentAt !== void 0 && Date.now() - idle.sentAt >= idleSendTimeoutMs) {
|
|
2707
|
+
rearmIdleSend(run);
|
|
2708
|
+
widgetDirty = true;
|
|
2709
|
+
}
|
|
2710
|
+
if (run.idleSend?.inFlight && isTerminalStatus(entry.status)) continue;
|
|
2509
2711
|
const wasStopped = prevStatus?.status !== void 0 && isTerminalStatus(prevStatus.status) || !!prevStatus?.pendingPermission;
|
|
2510
2712
|
if ((entry.pendingPermission || isTerminalStatus(entry.status)) && !wasStopped) interruptPendingWaits({
|
|
2511
2713
|
runId: entry.runId,
|
|
@@ -2513,45 +2715,61 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2513
2715
|
status: isTerminalStatus(entry.status) ? entry.status : "waiting"
|
|
2514
2716
|
}, entryKey);
|
|
2515
2717
|
if (isTerminalStatus(entry.status)) {
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
if (
|
|
2521
|
-
|
|
2522
|
-
try {
|
|
2523
|
-
pi.sendUserMessage(buildCompletionText({
|
|
2524
|
-
runId: run.runId,
|
|
2525
|
-
label: run.label
|
|
2526
|
-
}, run.lastStatus, finalOutput), { deliverAs: "followUp" });
|
|
2527
|
-
} catch (err) {
|
|
2528
|
-
console.error("avenor tick: sendUserMessage failed", err);
|
|
2529
|
-
}
|
|
2530
|
-
emitAvenorEvent(pi.events, CHANNEL_RUN_TERMINAL, createRunTerminalPayload({
|
|
2531
|
-
runId: entry.runId,
|
|
2532
|
-
supervisorId: entry.supervisorId,
|
|
2718
|
+
const finalOutput = run.pendingNotice ? void 0 : await resolveFinalOutput(run, run.lastStatus);
|
|
2719
|
+
if (sessionAtPollStart !== sessionGeneration) continue;
|
|
2720
|
+
if (trackedRuns.get(entryKey) !== run) continue;
|
|
2721
|
+
const wasTerminalBefore = prevStatus?.status !== void 0 && isTerminalStatus(prevStatus.status);
|
|
2722
|
+
if (emitRunTerminal({
|
|
2723
|
+
run,
|
|
2533
2724
|
runtimeId: entry.runtimeId,
|
|
2534
2725
|
label: entry.label,
|
|
2535
2726
|
agent: entry.agent,
|
|
2536
2727
|
status: entry.status,
|
|
2537
|
-
nestedCount: entry.nestedCount,
|
|
2728
|
+
nestedCount: entry.nestedCount ?? run.nestedCount,
|
|
2538
2729
|
backend: entry.backend
|
|
2539
|
-
}))
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
run.
|
|
2543
|
-
|
|
2544
|
-
|
|
2730
|
+
})) {
|
|
2731
|
+
if (!wasTerminalBefore && !run.blocking) sessionCtx?.ui.notify(`Sub-agent "${entry.label}" finished: ${entry.status}`, entry.status === "done" ? "info" : "warning");
|
|
2732
|
+
}
|
|
2733
|
+
const notice = run.pendingNotice ?? {
|
|
2734
|
+
status: entry.status,
|
|
2735
|
+
text: buildCompletionText({
|
|
2545
2736
|
runId: run.runId,
|
|
2546
|
-
label: run.label
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2737
|
+
label: run.label
|
|
2738
|
+
}, run.lastStatus, finalOutput)
|
|
2739
|
+
};
|
|
2740
|
+
if (run.blocking) {
|
|
2741
|
+
if (!run.pendingNotice) {
|
|
2742
|
+
run.pendingNotice = notice;
|
|
2743
|
+
widgetDirty = true;
|
|
2744
|
+
}
|
|
2745
|
+
continue;
|
|
2746
|
+
}
|
|
2747
|
+
if (sessionCtx?.isIdle() === true && !compacting && !run.idleSend?.capped) {
|
|
2748
|
+
run.pendingNotice = notice;
|
|
2749
|
+
run.idleSend = {
|
|
2750
|
+
inFlight: true,
|
|
2751
|
+
sentAt: Date.now(),
|
|
2752
|
+
attempts: (run.idleSend?.attempts ?? 0) + 1,
|
|
2753
|
+
capped: false
|
|
2754
|
+
};
|
|
2755
|
+
widgetDirty = true;
|
|
2756
|
+
sendCompletionNotice(notice);
|
|
2757
|
+
continue;
|
|
2758
|
+
} else if (!run.pendingNotice) {
|
|
2759
|
+
run.pendingNotice = notice;
|
|
2760
|
+
widgetDirty = true;
|
|
2551
2761
|
}
|
|
2762
|
+
} else if (entry.pendingPermission && !run.blocking && !run.permissionNotified) {
|
|
2763
|
+
run.permissionNotified = true;
|
|
2764
|
+
if (run.lastStatus) sendUserMessageLogged(buildWaitingText({
|
|
2765
|
+
runId: run.runId,
|
|
2766
|
+
label: run.label,
|
|
2767
|
+
agent: run.agent
|
|
2768
|
+
}, run.lastStatus), "avenor tick: sendUserMessage for permission failed");
|
|
2552
2769
|
} else if (!entry.pendingPermission && run.permissionNotified) run.permissionNotified = false;
|
|
2553
2770
|
}
|
|
2554
|
-
if (
|
|
2771
|
+
if (widgetDirty) updateWidget(entries);
|
|
2772
|
+
if ((entries.some((entry) => !isTerminalStatus(entry.status)) || Array.from(trackedRuns.values()).some((run) => run.idleSend?.inFlight)) && pollingActive && generation === pollingGeneration) setTimeout(() => {
|
|
2555
2773
|
tick().catch((err) => {
|
|
2556
2774
|
pollingActive = false;
|
|
2557
2775
|
console.error("avenor polling loop failed", err);
|
|
@@ -2846,10 +3064,16 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2846
3064
|
});
|
|
2847
3065
|
pi.on("session_start", async (_event, ctx) => {
|
|
2848
3066
|
sessionCtx = ctx;
|
|
3067
|
+
sessionGeneration++;
|
|
3068
|
+
compacting = false;
|
|
3069
|
+
clearPendingNotices();
|
|
3070
|
+
if (trackedRuns.size > 0) startPolling();
|
|
2849
3071
|
lastStatusEntries = [];
|
|
2850
3072
|
lastTurnSocketClosed = false;
|
|
2851
3073
|
pollingErrorCount = 0;
|
|
2852
3074
|
pollingErrors.length = 0;
|
|
3075
|
+
publishedSessionName = null;
|
|
3076
|
+
publishSessionName(ctx);
|
|
2853
3077
|
if (!isSubAgentBrokerMode) startHostAskPoll();
|
|
2854
3078
|
startSubAgentPoll();
|
|
2855
3079
|
});
|
|
@@ -2859,6 +3083,8 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2859
3083
|
disposeHostProviders();
|
|
2860
3084
|
hostPendingAsks.clear();
|
|
2861
3085
|
subAgentPendingAsks.clear();
|
|
3086
|
+
sessionGeneration++;
|
|
3087
|
+
clearPendingNotices();
|
|
2862
3088
|
await stopPolling();
|
|
2863
3089
|
for (const wait of [...pendingWaits]) {
|
|
2864
3090
|
pendingWaits.delete(wait);
|
|
@@ -2870,7 +3096,36 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2870
3096
|
}
|
|
2871
3097
|
sessionCtx = null;
|
|
2872
3098
|
});
|
|
2873
|
-
pi.on("
|
|
3099
|
+
pi.on("message_end", (event) => {
|
|
3100
|
+
if (event.message?.role !== "user") return;
|
|
3101
|
+
const text = extractTurnText([event.message]).text;
|
|
3102
|
+
if (text === "") return;
|
|
3103
|
+
const matches = [...trackedRuns].filter(([, run]) => run.pendingNotice?.text === text);
|
|
3104
|
+
let targets = matches.filter(([, run]) => run.idleSend?.inFlight);
|
|
3105
|
+
if (targets.length === 0) targets = matches.filter(([, run]) => run.idleSend !== void 0);
|
|
3106
|
+
for (const [key, run] of targets) {
|
|
3107
|
+
run.pendingNotice = void 0;
|
|
3108
|
+
run.idleSend = void 0;
|
|
3109
|
+
trackedRuns.delete(key);
|
|
3110
|
+
}
|
|
3111
|
+
if (targets.length > 0) updateWidget(lastStatusEntries);
|
|
3112
|
+
});
|
|
3113
|
+
pi.on("session_before_compact", () => {
|
|
3114
|
+
compacting = true;
|
|
3115
|
+
});
|
|
3116
|
+
const resumeAfterCompaction = () => {
|
|
3117
|
+
compacting = false;
|
|
3118
|
+
for (const run of trackedRuns.values()) {
|
|
3119
|
+
if (run.permissionNotified && run.lastStatus?.pending_permission) run.permissionNotified = false;
|
|
3120
|
+
if (!run.idleSend) continue;
|
|
3121
|
+
rearmIdleSend(run);
|
|
3122
|
+
}
|
|
3123
|
+
if (Array.from(trackedRuns.values()).some((run) => run.pendingNotice || !run.permissionNotified && !!run.lastStatus?.pending_permission)) startPolling();
|
|
3124
|
+
};
|
|
3125
|
+
pi.on("session_compact", resumeAfterCompaction);
|
|
3126
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
3127
|
+
publishSessionName(ctx);
|
|
3128
|
+
compacting = false;
|
|
2874
3129
|
if (trackedRuns.size === 0) return;
|
|
2875
3130
|
const active = (await pollRuns()).filter((entry) => !isTerminalStatus(entry.status));
|
|
2876
3131
|
if (active.length === 0) return;
|
|
@@ -2884,6 +3139,38 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2884
3139
|
display: true
|
|
2885
3140
|
} };
|
|
2886
3141
|
});
|
|
3142
|
+
pi.on("agent_before_settle", async (event) => {
|
|
3143
|
+
if (event.outcome !== "completed") return;
|
|
3144
|
+
const deliverable = [];
|
|
3145
|
+
for (const run of trackedRuns.values()) {
|
|
3146
|
+
if (!run.pendingNotice) continue;
|
|
3147
|
+
if (run.idleSend?.inFlight) continue;
|
|
3148
|
+
deliverable.push({
|
|
3149
|
+
run,
|
|
3150
|
+
notice: run.pendingNotice
|
|
3151
|
+
});
|
|
3152
|
+
}
|
|
3153
|
+
if (deliverable.length === 0) return;
|
|
3154
|
+
for (const { run } of deliverable) {
|
|
3155
|
+
run.pendingNotice = void 0;
|
|
3156
|
+
trackedRuns.delete(trackedRunKey(run.supervisorId, run.runId));
|
|
3157
|
+
}
|
|
3158
|
+
updateWidget(lastStatusEntries);
|
|
3159
|
+
return {
|
|
3160
|
+
entries: [...event.entries, ...deliverable.map(({ run, notice }) => ({
|
|
3161
|
+
type: "custom_message",
|
|
3162
|
+
customType: "avenor-completion",
|
|
3163
|
+
content: notice.text,
|
|
3164
|
+
display: true,
|
|
3165
|
+
details: {
|
|
3166
|
+
run_id: run.runId,
|
|
3167
|
+
supervisor_id: run.supervisorId,
|
|
3168
|
+
status: notice.status
|
|
3169
|
+
}
|
|
3170
|
+
}))],
|
|
3171
|
+
continue: true
|
|
3172
|
+
};
|
|
3173
|
+
});
|
|
2887
3174
|
pi.registerMessageRenderer("avenor-active-runs", (message, _options, theme) => {
|
|
2888
3175
|
const lines = message.content.split("\n");
|
|
2889
3176
|
let text = "";
|
|
@@ -3022,7 +3309,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
3022
3309
|
}).catch(() => void 0));
|
|
3023
3310
|
if (probed && (isTerminalStatus(probed.status) || probed.status === "waiting")) waitStatus = probed;
|
|
3024
3311
|
else {
|
|
3025
|
-
const tracked =
|
|
3312
|
+
const tracked = getTrackedRunById(result.run_id, supervisorId);
|
|
3026
3313
|
if (tracked) tracked.blocking = false;
|
|
3027
3314
|
return {
|
|
3028
3315
|
content: [{
|
|
@@ -3041,7 +3328,8 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
3041
3328
|
};
|
|
3042
3329
|
}
|
|
3043
3330
|
} else {
|
|
3044
|
-
|
|
3331
|
+
const runKey = trackedRunKey(supervisorId, result.run_id);
|
|
3332
|
+
trackedRuns.delete(runKey);
|
|
3045
3333
|
return {
|
|
3046
3334
|
content: [{
|
|
3047
3335
|
type: "text",
|
|
@@ -3066,10 +3354,13 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
3066
3354
|
...spawnIdentityMetadata(hostParams)
|
|
3067
3355
|
}
|
|
3068
3356
|
};
|
|
3069
|
-
upsertTrackedRun(runRef, status, wait);
|
|
3357
|
+
if (trackedRuns.has(trackedRunKey(supervisorId, result.run_id))) upsertTrackedRun(runRef, status, wait);
|
|
3070
3358
|
if (status.status === "waiting") {
|
|
3071
|
-
const tracked =
|
|
3072
|
-
if (tracked)
|
|
3359
|
+
const tracked = getTrackedRunById(result.run_id, supervisorId);
|
|
3360
|
+
if (tracked) {
|
|
3361
|
+
tracked.permissionNotified = true;
|
|
3362
|
+
tracked.blocking = false;
|
|
3363
|
+
}
|
|
3073
3364
|
return {
|
|
3074
3365
|
content: [{
|
|
3075
3366
|
type: "text",
|
|
@@ -3093,16 +3384,19 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
3093
3384
|
runId: result.run_id,
|
|
3094
3385
|
supervisorId
|
|
3095
3386
|
}, status);
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3387
|
+
const runKey = trackedRunKey(supervisorId, result.run_id);
|
|
3388
|
+
const tracked = trackedRuns.get(runKey);
|
|
3389
|
+
emitRunTerminal({
|
|
3390
|
+
run: tracked,
|
|
3099
3391
|
runtimeId: status.runtime_id ?? runRef.runtimeId,
|
|
3100
3392
|
label,
|
|
3101
3393
|
agent: params.agent,
|
|
3102
3394
|
status: status.status,
|
|
3395
|
+
nestedCount: tracked?.nestedCount,
|
|
3103
3396
|
backend: status.backend
|
|
3104
|
-
})
|
|
3105
|
-
trackedRuns.delete(
|
|
3397
|
+
});
|
|
3398
|
+
trackedRuns.delete(runKey);
|
|
3399
|
+
updateWidget(lastStatusEntries);
|
|
3106
3400
|
return {
|
|
3107
3401
|
content: [{
|
|
3108
3402
|
type: "text",
|
|
@@ -3120,6 +3414,8 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
3120
3414
|
}
|
|
3121
3415
|
};
|
|
3122
3416
|
}
|
|
3417
|
+
const fallthrough = getTrackedRunById(result.run_id, supervisorId);
|
|
3418
|
+
if (fallthrough) fallthrough.blocking = false;
|
|
3123
3419
|
return {
|
|
3124
3420
|
content: [{
|
|
3125
3421
|
type: "text",
|
|
@@ -3205,6 +3501,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
3205
3501
|
const previousBlocking = tracked?.blocking;
|
|
3206
3502
|
if (tracked) tracked.blocking = true;
|
|
3207
3503
|
let completed = false;
|
|
3504
|
+
let resultRun = tracked;
|
|
3208
3505
|
const blockingWait = params.wait ?? true ? createInterruptibleWait(trackedRunKey(tracked?.supervisorId ?? params.supervisor_id, tracked?.runId ?? params.run_id), signal) : void 0;
|
|
3209
3506
|
try {
|
|
3210
3507
|
let result;
|
|
@@ -3221,18 +3518,30 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
3221
3518
|
result = await resultAfterInterrupt(params, blockingWait.stopped, tracked);
|
|
3222
3519
|
}
|
|
3223
3520
|
completed = result.ready;
|
|
3521
|
+
resultRun = reconcileResultRun(tracked, result, params.supervisor_id);
|
|
3224
3522
|
if (completed) {
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3523
|
+
const idMatches = trackedRunsById(params.run_id, params.supervisor_id).length;
|
|
3524
|
+
const labelMatches = [...trackedRuns.values()].filter((run) => (params.supervisor_id === void 0 || run.supervisorId === params.supervisor_id) && run.label === params.run_id).length;
|
|
3525
|
+
emitRunTerminal({
|
|
3526
|
+
run: resultRun,
|
|
3527
|
+
runId: idMatches > 1 || idMatches === 0 && labelMatches > 1 ? void 0 : result.run_id ?? params.run_id,
|
|
3528
|
+
supervisorId: params.supervisor_id,
|
|
3529
|
+
runtimeId: resultRun?.runtimeId ?? result.runtime_id,
|
|
3530
|
+
label: resultRun?.label ?? result.label ?? params.run_id,
|
|
3531
|
+
agent: resultRun?.agent ?? "unknown",
|
|
3532
|
+
status: result.status ?? "done",
|
|
3533
|
+
nestedCount: resultRun?.nestedCount,
|
|
3534
|
+
backend: resultRun?.lastStatus?.backend
|
|
3535
|
+
});
|
|
3536
|
+
if (resultRun) {
|
|
3537
|
+
trackedRuns.delete(trackedRunKey(resultRun.supervisorId, resultRun.runId));
|
|
3538
|
+
updateWidget(lastStatusEntries);
|
|
3539
|
+
if (tracked && resultRun !== tracked) {
|
|
3540
|
+
const trackedKey = trackedRunKey(tracked.supervisorId, tracked.runId);
|
|
3541
|
+
if (trackedRuns.get(trackedKey) === tracked) tracked.blocking = previousBlocking ?? false;
|
|
3542
|
+
}
|
|
3543
|
+
} else if (tracked && trackedRuns.get(trackedRunKey(tracked.supervisorId, tracked.runId)) === tracked) tracked.blocking = previousBlocking ?? false;
|
|
3544
|
+
} else if (resultRun && result.status === "waiting") resultRun.permissionNotified = true;
|
|
3236
3545
|
return {
|
|
3237
3546
|
content: [{
|
|
3238
3547
|
type: "text",
|
|
@@ -3242,7 +3551,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
3242
3551
|
};
|
|
3243
3552
|
} finally {
|
|
3244
3553
|
blockingWait?.dispose();
|
|
3245
|
-
if (!completed && tracked &&
|
|
3554
|
+
if (!completed && tracked && trackedRuns.get(trackedRunKey(tracked.supervisorId, tracked.runId)) === tracked) tracked.blocking = previousBlocking ?? false;
|
|
3246
3555
|
}
|
|
3247
3556
|
},
|
|
3248
3557
|
renderCall(args, theme, context) {
|
|
@@ -3832,4 +4141,4 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
3832
4141
|
}
|
|
3833
4142
|
var src_default = createExtension();
|
|
3834
4143
|
//#endregion
|
|
3835
|
-
export { CHANNEL_POLL_COMPLETED, CHANNEL_POLL_ERROR, CHANNEL_RUN_TERMINAL, buildCompletionText, buildInspectPayload, compactWhitespace, createExtension, createPollCompletedPayload, createPollErrorPayload, createRunTerminalPayload, src_default as default, emitAvenorEvent, isTerminalStatus, onAvenorEvent, renderStatusLines, spawnIdentityMetadata, statusSupervisorId };
|
|
4144
|
+
export { CHANNEL_DELIVERY_ERROR, CHANNEL_POLL_COMPLETED, CHANNEL_POLL_ERROR, CHANNEL_RUN_TERMINAL, buildCompletionText, buildInspectPayload, compactWhitespace, createDeliveryErrorPayload, createExtension, createPollCompletedPayload, createPollErrorPayload, createRunTerminalPayload, src_default as default, emitAvenorEvent, isTerminalStatus, onAvenorEvent, renderStatusLines, spawnIdentityMetadata, statusSupervisorId };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dougbots/avenor-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -35,15 +35,15 @@
|
|
|
35
35
|
"dev": "tsdown --watch"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@dougbots/avenor-core": "^0.
|
|
38
|
+
"@dougbots/avenor-core": "^0.17.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@earendil-works/pi-coding-agent": ">=
|
|
41
|
+
"@earendil-works/pi-coding-agent": ">=1.4.0",
|
|
42
42
|
"@earendil-works/pi-tui": "*"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@earendil-works/pi-coding-agent": "
|
|
46
|
-
"@earendil-works/pi-tui": "
|
|
45
|
+
"@earendil-works/pi-coding-agent": "^1.4.0",
|
|
46
|
+
"@earendil-works/pi-tui": "^1.4.0",
|
|
47
47
|
"@types/bun": "^1.3.12",
|
|
48
48
|
"@types/node": "^25.8.0",
|
|
49
49
|
"tsdown": "^0.22.0",
|