@bastani/atomic 0.9.44-alpha.5 → 0.9.44-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/dist/builtin/intercom/CHANGELOG.md +6 -0
- package/dist/builtin/intercom/broker/broker.ts +13 -1
- package/dist/builtin/intercom/index.bundle.mjs +41 -5
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/intercom/types.ts +7 -0
- package/dist/builtin/intercom/workflow-delivery-states.ts +48 -0
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +20 -0
- package/dist/builtin/workflows/builtin/{chunk-zjecnjxx.js → chunk-6901nsd6.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-k8d7ehjx.js → chunk-jvna28mq.js} +381 -130
- package/dist/builtin/workflows/builtin/{chunk-qxpymnaf.js → chunk-vwx023ge.js} +1 -1
- package/dist/builtin/workflows/builtin/goal.js +2 -2
- package/dist/builtin/workflows/builtin/index.js +3 -3
- package/dist/builtin/workflows/builtin/ralph.js +2 -2
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/durable/journal/internal.d.ts +15 -1
- package/dist/builtin/workflows/src/durable/journal/sql-run-journal.d.ts +11 -0
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +2514 -741
- package/dist/builtin/workflows/src/extension/local-worker-entry.mjs +2384 -727
- package/dist/builtin/workflows/src/index.js +2383 -727
- package/dist/builtin/workflows/src/local-worker-entry.mjs +2384 -727
- package/dist/core/agent-session-custom-message-commit.d.ts.map +1 -1
- package/dist/core/agent-session-custom-message-commit.js +16 -0
- package/dist/core/agent-session-custom-message-commit.js.map +1 -1
- package/dist/core/storage/node-sqlite.d.ts.map +1 -1
- package/dist/core/storage/node-sqlite.js +130 -13
- package/dist/core/storage/node-sqlite.js.map +1 -1
- package/docs/intercom/reference.md +2 -0
- package/docs/workflows/operations.md +1 -1
- package/npm-shrinkwrap.json +32 -32
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.9.44-alpha.7] - 2026-10-25
|
|
6
|
+
|
|
7
|
+
Workflow stages always get their queued Intercom messages and never wait forever to start, even around pauses, worker replacement and reconnects.
|
|
8
|
+
|
|
9
|
+
- **No stuck stages:** a stage no longer waits forever before its first turn for a pattern message it receives live, and never waits more than 60 seconds without delivery progress; any message still queued is delivered afterwards ([#3958](https://github.com/bastani-inc/atomic/pull/3958))
|
|
10
|
+
- **No lost messages:** a direct send to a paused, blocked or not-yet-started stage is queued for that stage and survives a pooled worker replacement, and messages missed while a stage was disconnected arrive after it reconnects, with their delivery mode and reply target intact ([#3958](https://github.com/bastani-inc/atomic/pull/3958), [#3959](https://github.com/bastani-inc/atomic/pull/3959))
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- An Intercom message sent to one specific workflow stage while that stage is paused, blocked or awaiting input and has not started yet is now queued for that stage and reaches it when it starts. Previously the send was accepted as a message for a possible future stage (`notInKnownSet`) and matched only the stage's old id, so it was lost when the stage started under a new id, as it does when a pooled worker is replaced. Messages sent to the whole run (`workflow:<rootRunId>/**`) were not affected ([#3869](https://github.com/bastani-inc/atomic/issues/3869))
|
|
15
|
+
- A workflow stage no longer waits forever before its first turn when an Intercom message to a pattern target such as `workflow:<run>/child-a/**` is queued just after the stage's queued messages were delivered at startup. The stage now starts once that startup delivery has finished, and later messages reach it as ordinary live messages. Before, the stage waited for a second delivery that never ran, so its prompt was never sent and its pooled work stayed leased with its lease still renewed. In a 640-child pooled load test this left 7 children (about 1%) stuck for 20–45 minutes.
|
|
16
|
+
- A workflow stage never waits more than 60 seconds without delivery progress for its queued Intercom messages before it starts. If a startup delivery stops making progress, the stage logs one warning that names the undelivered messages and whether the delivery is still running. It then starts, and the messages stay queued: they arrive live or on the stage's next delivery.
|
|
17
|
+
- A sticky Intercom message that could not reach a workflow stage while the stage was disconnected from the broker is now delivered when the stage reconnects, exactly as it would have arrived live: an `interrupt` still cancels the stage's running operation, and an ask becomes the stage's reply target. Messages the stage already has are not delivered again.
|
|
18
|
+
|
|
19
|
+
## [0.9.44-alpha.6] - 2026-10-25
|
|
20
|
+
|
|
21
|
+
Large workflow fan-outs stay fast for the whole run and recover cleanly from a crash, even with hundreds of pooled child workflows messaging each other.
|
|
22
|
+
|
|
23
|
+
- **Journal writes that keep up:** transactions queued behind a commit share it, delivery receipts are batched, and pending-message changes for a run are written once per batch, so Intercom sends no longer time out behind journal traffic ([#3942](https://github.com/bastani-inc/atomic/pull/3942), [#3943](https://github.com/bastani-inc/atomic/pull/3943), [#3947](https://github.com/bastani-inc/atomic/pull/3947))
|
|
24
|
+
- **Costs that stay flat as a run grows:** pool commits, delivery transitions, route announcements and receipt bookkeeping now cost what changed rather than the run's whole history, and each sticky delivery receipt is written once instead of repeatedly ([#3944](https://github.com/bastani-inc/atomic/pull/3944), [#3945](https://github.com/bastani-inc/atomic/pull/3945), [#3948](https://github.com/bastani-inc/atomic/pull/3948), [#3951](https://github.com/bastani-inc/atomic/pull/3951), [#3952](https://github.com/bastani-inc/atomic/pull/3952))
|
|
25
|
+
- **Fast, reliable recovery:** `/workflow status` and resume load large journals in seconds, resume no longer rebuilds every child's history, and resumed pooled children no longer fail with `Pool checkpoint identity conflict` or a spurious `Storage unavailable` ([#3950](https://github.com/bastani-inc/atomic/pull/3950), [#3951](https://github.com/bastani-inc/atomic/pull/3951), [#3954](https://github.com/bastani-inc/atomic/pull/3954), [#3955](https://github.com/bastani-inc/atomic/pull/3955))
|
|
26
|
+
- **Bounded delivery states:** route announcements, `intercom list`, `/workflow status` and inspect show at most 5 sticky delivery states per row plus totals ([#3951](https://github.com/bastani-inc/atomic/pull/3951))
|
|
27
|
+
- **Paused stages resume their work:** a background completion that arrives while a paused stage resumes now starts its next turn ([#3953](https://github.com/bastani-inc/atomic/pull/3953))
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Under heavy write load a local SQLite store (`node-sqlite`, including the workflow journal) commits transactions in groups: transactions requested while another is committing share one `BEGIN IMMEDIATE … COMMIT`, each in its own savepoint and in request order. A transaction that fails rolls back only its own writes, and every caller still settles only after the shared commit. In a 480-child pooled workflow the pool coordinator was committing about 60 transcript transactions a second, each taking the write lock and syncing on its own, which starved the workflow host's writes until Intercom sends timed out and worker leases expired.
|
|
32
|
+
- A background task completion that arrives just as a paused workflow stage is resumed now starts the stage's next turn. Pausing a stage cancels its background shells, and their completion notices are written to the session before the stage acts on them. If the stage was resumed during that write, the notice was queued after the resume had already released the stage's held messages, so the stage sat idle with the notice unanswered until something else started a turn.
|
|
33
|
+
|
|
5
34
|
## [0.9.44-alpha.5] - 2026-10-25
|
|
6
35
|
|
|
7
36
|
Large workflow fan-outs keep their Intercom routes and journal writes responsive under load.
|
|
@@ -4,6 +4,12 @@ All notable changes to the `pi-intercom` extension will be documented in this fi
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.44-alpha.6] - 2026-10-25
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `intercom list` lists at most 5 sticky delivery states per workflow row: states that still need action first, then the most recent deliveries. A final line reports how many were omitted and the total in each state, and the row details carry the same totals as `deliveryStateCounts` and `omittedDeliveryStates`. In a 640-child workflow, each list result had stored about 3 MB of delivery states in the session transcript. A test with 8,000 queued messages in 163 rows shrinks the list result from about 4.6 MB to about 0.2 MB. The broker applies the same bound to rows announced by older workflow hosts.
|
|
12
|
+
|
|
7
13
|
## [0.9.44-alpha.5] - 2026-10-25
|
|
8
14
|
|
|
9
15
|
### Fixed
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
writeMessageIfOpen,
|
|
16
16
|
writeMessageWithOutcome,
|
|
17
17
|
} from "./socket-writes.js";
|
|
18
|
+
import { boundWorkflowPossibleStageRow } from "../workflow-delivery-states.js";
|
|
18
19
|
import { isSocketAnswering } from "./socket-liveness.js";
|
|
19
20
|
import { acquireSocketReplacementLock } from "./socket-replacement-lock.js";
|
|
20
21
|
import { startBrokerWatchdog } from "./watchdog.js";
|
|
@@ -303,6 +304,14 @@ function isWorkflowPossibleStageAnnouncements(
|
|
|
303
304
|
Number.isInteger(announcement.queuedCount) &&
|
|
304
305
|
announcement.queuedCount >= 0 &&
|
|
305
306
|
(announcement.deliveryOnly === undefined || announcement.deliveryOnly === true) &&
|
|
307
|
+
(announcement.omittedDeliveryStates === undefined ||
|
|
308
|
+
(Number.isInteger(announcement.omittedDeliveryStates) && announcement.omittedDeliveryStates >= 0)) &&
|
|
309
|
+
(announcement.deliveryStateCounts === undefined ||
|
|
310
|
+
(typeof announcement.deliveryStateCounts === "object" &&
|
|
311
|
+
announcement.deliveryStateCounts !== null &&
|
|
312
|
+
Object.values(announcement.deliveryStateCounts).every(
|
|
313
|
+
(count) => Number.isInteger(count) && count >= 0,
|
|
314
|
+
))) &&
|
|
306
315
|
(announcement.deliveryStates === undefined || (Array.isArray(announcement.deliveryStates) && announcement.deliveryStates.every((delivery) =>
|
|
307
316
|
delivery !== null && typeof delivery === "object" &&
|
|
308
317
|
typeof delivery.messageId === "string" && typeof delivery.runId === "string" &&
|
|
@@ -724,6 +733,8 @@ class IntercomBroker {
|
|
|
724
733
|
target: row.target,
|
|
725
734
|
queuedCount: row.queuedCount,
|
|
726
735
|
...(row.deliveryStates === undefined ? {} : { deliveryStates: row.deliveryStates }),
|
|
736
|
+
...(row.deliveryStateCounts === undefined ? {} : { deliveryStateCounts: row.deliveryStateCounts }),
|
|
737
|
+
...(row.omittedDeliveryStates === undefined ? {} : { omittedDeliveryStates: row.omittedDeliveryStates }),
|
|
727
738
|
...(row.deliveryOnly === undefined ? {} : { deliveryOnly: row.deliveryOnly }),
|
|
728
739
|
group: roster.group,
|
|
729
740
|
});
|
|
@@ -1913,9 +1924,10 @@ class IntercomBroker {
|
|
|
1913
1924
|
// D7 (slice 4): presence replaces, absence keeps — a terminal root publishes `[]`
|
|
1914
1925
|
// so the broker drops every future row, while nested runs' re-announcements (which
|
|
1915
1926
|
// never carry the field) preserve the root's rows.
|
|
1927
|
+
// Rows from hosts that announce every delivery state are bounded like current hosts' rows.
|
|
1916
1928
|
const possibleStages =
|
|
1917
1929
|
clientMessage.possibleStages !== undefined
|
|
1918
|
-
? clientMessage.possibleStages
|
|
1930
|
+
? clientMessage.possibleStages.map((row) => boundWorkflowPossibleStageRow(row))
|
|
1919
1931
|
: this.workflowRosters.get(clientMessage.runId)?.possibleStages;
|
|
1920
1932
|
// Optional path metadata is advisory for old hosts; omission retains a
|
|
1921
1933
|
// previously announced edge during two-session route replay.
|
|
@@ -4082,7 +4082,15 @@ function formatWorkflowStageRow(stage) {
|
|
|
4082
4082
|
}
|
|
4083
4083
|
function formatWorkflowFutureStageRow(stage) {
|
|
4084
4084
|
return `- \`${stage.target}\` [${stage.deliveryOnly === true ? "delivery" : "future"}] ${stage.queuedCount} queued message${stage.queuedCount === 1 ? "" : "s"}${(stage.deliveryStates ?? []).map((delivery) => `
|
|
4085
|
-
delivery ${delivery.messageId} stage ${delivery.runId}/${delivery.stageId}${delivery.sessionId === undefined ? "" : ` session ${delivery.sessionId}`} ${delivery.state} — ${delivery.reason}`).join("")}`;
|
|
4085
|
+
delivery ${delivery.messageId} stage ${delivery.runId}/${delivery.stageId}${delivery.sessionId === undefined ? "" : ` session ${delivery.sessionId}`} ${delivery.state} — ${delivery.reason}`).join("")}${formatDeliveryStateSummary(stage)}`;
|
|
4086
|
+
}
|
|
4087
|
+
function formatDeliveryStateSummary(stage) {
|
|
4088
|
+
const omitted = stage.omittedDeliveryStates ?? 0;
|
|
4089
|
+
if (omitted === 0 || stage.deliveryStateCounts === undefined)
|
|
4090
|
+
return "";
|
|
4091
|
+
const counts = Object.entries(stage.deliveryStateCounts).map(([state, count]) => `${count} ${state}`).join(", ");
|
|
4092
|
+
return `
|
|
4093
|
+
… ${omitted} more delivery state${omitted === 1 ? "" : "s"} omitted (all: ${counts})`;
|
|
4086
4094
|
}
|
|
4087
4095
|
function registerIntercomTool(pi, deps) {
|
|
4088
4096
|
const retryIdentities = new RetryIdentityReservations({ maxEntries: deps.retryIdentityMaxEntries });
|
|
@@ -7915,11 +7923,12 @@ function piIntercomExtension(pi, testOverrides = {}) {
|
|
|
7915
7923
|
scheduleInboundFlush();
|
|
7916
7924
|
return plan.notices ?? [];
|
|
7917
7925
|
}
|
|
7918
|
-
|
|
7926
|
+
let redrainPendingStage = null;
|
|
7927
|
+
function handleIncomingMessage(ctx, from, message, channel, receivedBeforeStageStart = false, recoveredAfterStageStart = false) {
|
|
7919
7928
|
const messageGeneration = runtimeGeneration;
|
|
7920
7929
|
const liveContext = getLiveContext(ctx, messageGeneration);
|
|
7921
7930
|
if (!liveContext) {
|
|
7922
|
-
if (receivedBeforeStageStart)
|
|
7931
|
+
if (receivedBeforeStageStart || recoveredAfterStageStart)
|
|
7923
7932
|
throw new Error("Intercom session retired before pending-stage delivery; message remains queued");
|
|
7924
7933
|
return;
|
|
7925
7934
|
}
|
|
@@ -7980,6 +7989,13 @@ function piIntercomExtension(pi, testOverrides = {}) {
|
|
|
7980
7989
|
});
|
|
7981
7990
|
}, () => foregroundDetachHandoff.claim(from, message, messageGeneration, () => Boolean(getLiveContext(liveContext, messageGeneration))), release);
|
|
7982
7991
|
if (activeDelivery !== false) {
|
|
7992
|
+
if (recoveredAfterStageStart) {
|
|
7993
|
+
return activeDelivery.then(commit, (error) => {
|
|
7994
|
+
replyTracker.forgetIncomingMessage(replyContext);
|
|
7995
|
+
inboundDeliveries.release(reservation, toError3(error));
|
|
7996
|
+
throw error;
|
|
7997
|
+
});
|
|
7998
|
+
}
|
|
7983
7999
|
activeDelivery.then(commit, release);
|
|
7984
8000
|
return;
|
|
7985
8001
|
}
|
|
@@ -8545,6 +8561,8 @@ function piIntercomExtension(pi, testOverrides = {}) {
|
|
|
8545
8561
|
if (nextClient.sessionId && nextClient.sessionId !== preferredSessionId) {
|
|
8546
8562
|
await recordIntercomSessionIdentity(sessionIdAtStart, nextClient.sessionId);
|
|
8547
8563
|
}
|
|
8564
|
+
if (reason !== "startup")
|
|
8565
|
+
redrainPendingStage?.();
|
|
8548
8566
|
return nextClient;
|
|
8549
8567
|
} catch (error) {
|
|
8550
8568
|
if (client === nextClient) {
|
|
@@ -8780,18 +8798,36 @@ function piIntercomExtension(pi, testOverrides = {}) {
|
|
|
8780
8798
|
await ensureConnected("startup");
|
|
8781
8799
|
return;
|
|
8782
8800
|
}
|
|
8783
|
-
const
|
|
8801
|
+
const generation = runtimeGeneration;
|
|
8802
|
+
const deliverPending = async (started = false) => {
|
|
8784
8803
|
const receivedMessageIds = ctx.sessionManager.getBranch().flatMap((entry) => {
|
|
8785
8804
|
if (entry.type !== "custom_message" || entry.customType !== "intercom_message")
|
|
8786
8805
|
return [];
|
|
8787
8806
|
const id = entry.details?.message?.id;
|
|
8788
8807
|
return typeof id === "string" ? [id] : [];
|
|
8789
8808
|
});
|
|
8790
|
-
await pendingStageDelivery.deliverPending((from, message) => handleIncomingMessage(ctx, from, message, undefined, true), { sessionId: ctx.sessionManager.getSessionId(), receivedMessageIds });
|
|
8809
|
+
await pendingStageDelivery.deliverPending((from, message) => started ? handleIncomingMessage(ctx, from, message, undefined, false, true) : handleIncomingMessage(ctx, from, message, undefined, true), { sessionId: ctx.sessionManager.getSessionId(), receivedMessageIds });
|
|
8791
8810
|
};
|
|
8811
|
+
let startupDrained = false;
|
|
8812
|
+
let redrainRequested = false;
|
|
8813
|
+
const redrain = () => {
|
|
8814
|
+
if (!getLiveContext(ctx, generation))
|
|
8815
|
+
return;
|
|
8816
|
+
if (!startupDrained) {
|
|
8817
|
+
redrainRequested = true;
|
|
8818
|
+
return;
|
|
8819
|
+
}
|
|
8820
|
+
deliverPending(true).catch(() => {});
|
|
8821
|
+
};
|
|
8822
|
+
redrainPendingStage = redrain;
|
|
8792
8823
|
await deliverPending();
|
|
8793
8824
|
await ensureConnected("startup");
|
|
8794
8825
|
await deliverPending();
|
|
8826
|
+
startupDrained = true;
|
|
8827
|
+
if (redrainRequested) {
|
|
8828
|
+
redrainRequested = false;
|
|
8829
|
+
redrain();
|
|
8830
|
+
}
|
|
8795
8831
|
});
|
|
8796
8832
|
pi.registerMessageRenderer("intercom_message", (message, _options, theme) => {
|
|
8797
8833
|
const details = message.details;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/intercom",
|
|
3
|
-
"version": "0.9.44-alpha.
|
|
3
|
+
"version": "0.9.44-alpha.7",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Atomic extension providing a private coordination channel between parent and child agent sessions. Fork of: https://github.com/nicobailon/pi-intercom",
|
|
6
6
|
"contributors": [
|
|
@@ -52,7 +52,12 @@ export interface WorkflowPossibleStageAnnouncement {
|
|
|
52
52
|
readonly target: string;
|
|
53
53
|
/** Current number of queued sticky entries matching this target. */
|
|
54
54
|
readonly queuedCount: number;
|
|
55
|
+
/** At most a fixed number of exemplar states: unresolved states first, then the latest delivered. */
|
|
55
56
|
readonly deliveryStates?: readonly WorkflowStickyDeliveryState[];
|
|
57
|
+
/** Delivery states per state, over every state of this row, including omitted ones. */
|
|
58
|
+
readonly deliveryStateCounts?: Readonly<Record<string, number>>;
|
|
59
|
+
/** Delivery states left out of `deliveryStates`. */
|
|
60
|
+
readonly omittedDeliveryStates?: number;
|
|
56
61
|
readonly deliveryOnly?: true;
|
|
57
62
|
}
|
|
58
63
|
|
|
@@ -63,6 +68,8 @@ export interface WorkflowFutureStageRosterEntry {
|
|
|
63
68
|
readonly queuedCount: number;
|
|
64
69
|
readonly group: string;
|
|
65
70
|
readonly deliveryStates?: readonly WorkflowStickyDeliveryState[];
|
|
71
|
+
readonly deliveryStateCounts?: Readonly<Record<string, number>>;
|
|
72
|
+
readonly omittedDeliveryStates?: number;
|
|
66
73
|
readonly deliveryOnly?: true;
|
|
67
74
|
}
|
|
68
75
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { WorkflowPossibleStageAnnouncement, WorkflowStickyDeliveryState } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/** Most delivery states one possible-stage row carries; the rest are counted, not listed. */
|
|
4
|
+
export const WORKFLOW_DELIVERY_STATE_EXEMPLAR_LIMIT = 5;
|
|
5
|
+
|
|
6
|
+
/** States that still need action, in the order exemplars are chosen; `delivered` follows, most recent first. */
|
|
7
|
+
const UNRESOLVED_STATES = [
|
|
8
|
+
"queued",
|
|
9
|
+
"receipt-unverified",
|
|
10
|
+
"session-replaced",
|
|
11
|
+
"delivery-unavailable",
|
|
12
|
+
"root-unresolved",
|
|
13
|
+
"stage-terminal",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Bound a possible-stage row's delivery states as workflow hosts do, for rows from hosts that announce
|
|
18
|
+
* every state. Rows already within the limit pass through unchanged.
|
|
19
|
+
*/
|
|
20
|
+
export function boundWorkflowPossibleStageRow(
|
|
21
|
+
row: WorkflowPossibleStageAnnouncement,
|
|
22
|
+
limit = WORKFLOW_DELIVERY_STATE_EXEMPLAR_LIMIT,
|
|
23
|
+
): WorkflowPossibleStageAnnouncement {
|
|
24
|
+
const states = row.deliveryStates;
|
|
25
|
+
if (states === undefined || states.length <= limit) return row;
|
|
26
|
+
const exemplars: WorkflowStickyDeliveryState[] = [];
|
|
27
|
+
for (const state of UNRESOLVED_STATES) {
|
|
28
|
+
for (const delivery of states) {
|
|
29
|
+
if (exemplars.length === limit) break;
|
|
30
|
+
if (delivery.state === state) exemplars.push(delivery);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for (let index = states.length - 1; index >= 0 && exemplars.length < limit; index--) {
|
|
34
|
+
if (states[index]!.state === "delivered") exemplars.push(states[index]!);
|
|
35
|
+
}
|
|
36
|
+
for (const delivery of states) {
|
|
37
|
+
if (exemplars.length === limit) break;
|
|
38
|
+
if (delivery.state !== "delivered" && !UNRESOLVED_STATES.includes(delivery.state)) exemplars.push(delivery);
|
|
39
|
+
}
|
|
40
|
+
const counts = new Map<string, number>();
|
|
41
|
+
for (const delivery of states) counts.set(delivery.state, (counts.get(delivery.state) ?? 0) + 1);
|
|
42
|
+
return {
|
|
43
|
+
...row,
|
|
44
|
+
deliveryStates: exemplars,
|
|
45
|
+
deliveryStateCounts: row.deliveryStateCounts ?? Object.fromEntries(counts),
|
|
46
|
+
omittedDeliveryStates: (row.omittedDeliveryStates ?? 0) + states.length - exemplars.length,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/subagents",
|
|
3
|
-
"version": "0.9.44-alpha.
|
|
3
|
+
"version": "0.9.44-alpha.7",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Atomic extension for delegating tasks to subagents with parallel execution. Fork of: https://github.com/nicobailon/pi-subagents",
|
|
6
6
|
"contributors": [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/web-access",
|
|
3
|
-
"version": "0.9.44-alpha.
|
|
3
|
+
"version": "0.9.44-alpha.7",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
|
|
6
6
|
"contributors": [
|
|
@@ -6,6 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.44-alpha.6] - 2026-10-25
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- After a pooled root was killed and resumed, some child workflows no longer fail with `Extension startup failed: …intercom…: Storage unavailable: custom at custom journal`. While a resumed root and its workers were busy, a worker's lease could run out a moment before the pool extended it. When the root then recorded that a queued Intercom message had reached the worker's stage, it refused the write as if the attempt had been replaced and reported the refusal as unavailable storage, so the stage failed at startup. In a 640-child run this failed 21 children. The pool already keeps a live worker's child when its lease runs out (#3888), and the root now does the same when recording deliveries. Only a reassigned attempt or a new root epoch is refused, with `Stale work lease` or the session reason, and the replaced attempt's child is replayed. A failure of a custom journal now also names the cause's error class and code, though not its message, which may contain credentials.
|
|
14
|
+
- A long-running workflow fan-out whose stages keep sending Intercom messages to stage patterns (for example `workflow:<rootRunId>/child-*/**`) no longer slows down with every message it has sent. Pattern messages stay queued until the root run ends, and each delivery receipt made the workflow host re-check every queued message and every recorded delivery against every run. In a 480-child load test this saturated the host, Intercom sends timed out after 10 s, and about half of them were never delivered. A delivery now re-checks only the messages it changed and the messages delivered to stages that have since ended: with 3,000 queued messages, recording one delivery takes about 1.2 ms instead of 6.7 ms. Delivery results are unchanged.
|
|
15
|
+
- An Intercom send to a workflow stage no longer times out when a wave of stages starts at the same time. Every pending-message change for a run used to wait for the previous one's journal write, so a send that arrived while about 160 starting stages recorded their deliveries waited behind about 160 writes. Changes that arrive while a write is in progress are now applied together and saved in one write. In a test with a 40 ms write, a send issued in the middle of a 160-stage burst returned after 2 writes (about 90 ms) instead of 81 (about 3.3 s).
|
|
16
|
+
- Large workflow fan-outs no longer saturate the local worker pool's coordinator. Every pool commit, including each batch of session transcript entries, was checked for JSON-serializability with a recursive schema whose `$ref` evaluation cost far more than the write itself, so with 160 workers the coordinator spent about 90% of its time validating and stages stopped starting. Plain JSON values now pass a direct structural check; anything unusual still goes through the schema, with the same errors.
|
|
17
|
+
- Intercom sends to stage patterns no longer time out while a large workflow fan-out records delivery receipts. The workflow host wrote each receipt in its own journal transaction, one after another, and a pattern message waited behind every receipt queued before it. When another process also held the journal's write lock, that wait exceeded the 10-second send acknowledgement, and in a 480-child load test delivery fell to 0–2%. Receipts and settlements queued back to back for a run are now written together in one transaction, in the same order and with the same durability. In a test that queues a pattern message halfway through 2,000 receipts from 160 callers while a second process holds the write lock, the message persisted in under 30 ms instead of more than 4 seconds.
|
|
18
|
+
- Resuming a large workflow with many completed child workflows no longer takes minutes before any work restarts. Resume rebuilt every nested run from the whole checkpoint journal once for each cached child, and each rebuild itself rescanned the journal for every child boundary. Resume now rebuilds the nested runs once, reuses them while replay records only its own bookkeeping, and rebuilds each in a single pass. Resuming a pooled root with 640 completed children went from 156 s to 1.3 s (64 children: 271 ms to 37 ms), and resumed runs reach the same state as before.
|
|
19
|
+
- The local worker pool coordinator no longer slows down as a long pooled run grows. Each checkpoint a worker committed was validated against every checkpoint the root had ever recorded, and each commit re-added every usage report, so after about 15 minutes of a 640-child run the coordinator sat near full CPU and Intercom sends began to fail. Validation now reads only the checkpoints recorded since the previous commit, and the root's usage total is kept up to date as usage is written. Validation results and totals are unchanged. With 5,000 recorded checkpoints, a commit takes about 1.2 ms instead of 16 ms, and reading the usage total for 40,000 usage rows takes 0.02 ms instead of 8 ms.
|
|
20
|
+
- Late in a long workflow fan-out, Intercom sends no longer time out while the host announces workflow routes or records delivery receipts. Each root route announcement recomputed every queued pattern message's delivery state on every matching stage, and every recorded receipt re-read the delivery count of every queued message. Both grow for the whole run (about 7,700 queued messages after 21 minutes in a 640-child load test), so one announcement could block the host for seconds. Announcements and receipts now recompute only what changed. With 8,000 queued messages, an announcement after one delivery makes 2 receipt checks instead of 16,000, and recording a receipt reads 1 delivery count instead of 8,000. Delivery counts are unchanged.
|
|
21
|
+
- `/workflow status` from a new session, and resuming a workflow, no longer stall on a journal that holds many delivery receipts. Loading the journal re-read the delivery count of every queued message for every receipt. After a 640-child run with about 800,000 receipts, status had not rendered after 3 minutes. In a test with 200,000 receipts over 8,000 queued messages, opening and loading the run now takes about 3.4 s instead of 168 s.
|
|
22
|
+
- Each delivery of a pattern (sticky) Intercom message to a workflow stage is now written to the run's journal once. When a delivered stage ended, the workflow host archived that delivery's receipt together with every earlier receipt still kept for the message. Those earlier receipts stayed kept, so they were written again each time a later stage ended. In pooled load-test journals about 93% of delivery receipts were exact copies, with one receipt written up to 423 times, and each copy took a slot in the run's journal writes. Receipts that are already archived are no longer written again. Delivery counts are unchanged: the copies never raised the recorded count.
|
|
23
|
+
- A pooled child workflow resumed more than once no longer fails with `Pool checkpoint identity conflict`. Replaying a completed stage records a small replay-metadata checkpoint under an id derived from the stage, stamped with the time of that replay. The first resume recorded it. When a later resume, or a replacement worker, replayed the stage again, the pool coordinator compared the new record byte for byte with the stored one, saw a different time and failed the child. In a 640-child load test, 31 children failed this way after one SIGKILL and resume. A re-recorded checkpoint that differs only in its record time now keeps the first write. A checkpoint with any other difference is still rejected.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Route announcements carry at most 5 sticky delivery states per possible-stage row, with per-state totals and the number omitted. Each state leaves out its target, which the row already names. Workflow status and run inspection list at most 5 delivery states per run in the same way, with the same omitted-states line. Previously each announcement, `intercom list` result and status result carried every delivery state. With 8,000 queued messages across 163 rows, an announcement shrinks from about 4.6 MB to about 0.19 MB.
|
|
28
|
+
|
|
9
29
|
## [0.9.44-alpha.5] - 2026-10-25
|
|
10
30
|
|
|
11
31
|
### Fixed
|