@fitchmultz/pi-subagents 0.46.0 → 0.46.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/agents/agent-management.js +8 -2
- package/dist/agents/agent-selection.js +5 -17
- package/dist/agents/agents.js +10 -10
- package/dist/extension/build-info.js +1 -1
- package/dist/extension/index.js +11 -5
- package/dist/extension/result-snapshot.js +0 -18
- package/dist/history/ingest.js +40 -31
- package/dist/history/prune.js +1 -1
- package/dist/history/run-projection.js +1 -2
- package/dist/history/search-queries.js +8 -2
- package/dist/history/store.js +20 -4
- package/dist/history/text-chunks.js +1 -2
- package/dist/history/transcript-queries.js +12 -6
- package/dist/intercom/result-intercom.js +0 -74
- package/dist/pi-intercom/broker/broker.js +33 -16
- package/dist/pi-intercom/broker/framing.js +67 -17
- package/dist/pi-intercom/broker/snapshot-reader.js +29 -8
- package/dist/pi-intercom/broker/snapshots.js +144 -41
- package/dist/pi-intercom/session-targets.js +0 -1
- package/dist/runs/background/async-run-discovery.js +4 -6
- package/dist/runs/background/completion-dedupe.js +4 -55
- package/dist/runs/background/notify.js +1 -9
- package/dist/runs/background/result-watcher.js +1 -1
- package/dist/runs/background/runner-child-observer.js +12 -7
- package/dist/runs/background/runner-monitor.js +2 -0
- package/dist/runs/background/runner-parallel.js +2 -3
- package/dist/runs/background/runner-step-output.js +1 -3
- package/dist/runs/shared/acceptance-finalization.js +2 -2
- package/dist/runs/shared/acceptance.js +1 -1
- package/dist/runs/shared/child-observation-spool.js +8 -3
- package/dist/runs/shared/model-fallback.js +3 -0
- package/dist/runs/shared/parallel-utils.js +1 -21
- package/dist/runs/shared/run-restoration.js +4 -5
- package/dist/runs/shared/single-output.js +1 -4
- package/dist/runs/shared/subagent-prompt-runtime.js +2 -2
- package/dist/runs/shared/supervisor-questions.js +7 -4
- package/dist/runs/shared/workflow-policy.js +2 -1
- package/dist/shared/journal-frames.js +1 -1
- package/dist/shared/settings.js +1 -4
- package/dist/slash/command-discovery.js +13 -12
- package/dist/tui/agent-history.js +0 -14
- package/dist/tui/agent-view.js +0 -1
- package/dist/tui/conversation-history.js +10 -1
- package/dist/tui/history-cards.js +8 -22
- package/docs/intercom.md +5 -1
- package/docs/performance.md +72 -3
- package/docs/reference.md +1 -1
- package/package.json +1 -1
- package/src/agents/agent-management.ts +8 -4
- package/src/agents/agent-selection.ts +8 -19
- package/src/agents/agents.ts +19 -14
- package/src/extension/index.ts +12 -5
- package/src/extension/result-snapshot.ts +1 -27
- package/src/history/ingest.ts +57 -49
- package/src/history/prune.ts +1 -1
- package/src/history/run-projection.ts +2 -4
- package/src/history/search-queries.ts +9 -2
- package/src/history/store.ts +21 -5
- package/src/history/text-chunks.ts +7 -2
- package/src/history/transcript-queries.ts +16 -10
- package/src/intercom/result-intercom.ts +0 -103
- package/src/pi-intercom/broker/broker.ts +34 -20
- package/src/pi-intercom/broker/framing.ts +71 -20
- package/src/pi-intercom/broker/snapshot-reader.ts +47 -9
- package/src/pi-intercom/broker/snapshots.ts +184 -51
- package/src/pi-intercom/session-targets.ts +0 -2
- package/src/runs/background/async-run-discovery.ts +4 -10
- package/src/runs/background/completion-dedupe.ts +6 -69
- package/src/runs/background/notify.ts +2 -12
- package/src/runs/background/result-watcher.ts +1 -1
- package/src/runs/background/runner-child-observer.ts +19 -11
- package/src/runs/background/runner-monitor.ts +2 -0
- package/src/runs/background/runner-parallel.ts +2 -3
- package/src/runs/background/runner-step-output.ts +1 -3
- package/src/runs/shared/acceptance-finalization.ts +2 -2
- package/src/runs/shared/acceptance.ts +0 -2
- package/src/runs/shared/child-observation-spool.ts +8 -3
- package/src/runs/shared/model-fallback.ts +3 -0
- package/src/runs/shared/parallel-utils.ts +1 -25
- package/src/runs/shared/run-restoration.ts +4 -4
- package/src/runs/shared/single-output.ts +0 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +2 -2
- package/src/runs/shared/supervisor-questions.ts +10 -10
- package/src/runs/shared/workflow-policy.ts +1 -1
- package/src/shared/journal-frames.ts +1 -1
- package/src/shared/settings.ts +1 -4
- package/src/slash/command-discovery.ts +13 -12
- package/src/tui/agent-history.ts +1 -18
- package/src/tui/agent-view.ts +0 -1
- package/src/tui/conversation-history.ts +15 -1
- package/src/tui/history-cards.ts +8 -26
|
@@ -23,7 +23,7 @@ import { errorMessage, isRecord, type UnknownRecord } from "../../shared/unknown
|
|
|
23
23
|
import {
|
|
24
24
|
messageSender,
|
|
25
25
|
snapshotFrames,
|
|
26
|
-
|
|
26
|
+
TopicSnapshot,
|
|
27
27
|
publicationMessage,
|
|
28
28
|
deliveryAcknowledgement,
|
|
29
29
|
shouldReplaceDelivery,
|
|
@@ -36,6 +36,7 @@ const REPLACE_DELIVERY_DELAY_MS = 1500;
|
|
|
36
36
|
interface ConnectedSession {
|
|
37
37
|
readonly socket: net.Socket;
|
|
38
38
|
info: SessionInfo;
|
|
39
|
+
topics: TopicSnapshot;
|
|
39
40
|
readonly topicFrames: boolean;
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -178,7 +179,12 @@ class IntercomBroker {
|
|
|
178
179
|
const info: SessionInfo = { ...msg.session, id, lastSeen: msg.session.lastSeen ?? Date.now() };
|
|
179
180
|
snapshotFrames(randomUUID(), [info], true);
|
|
180
181
|
const topicFrames = msg.topicFrames === true;
|
|
181
|
-
this.sessions.set(id, {
|
|
182
|
+
this.sessions.set(id, {
|
|
183
|
+
socket,
|
|
184
|
+
info: messageSender(info),
|
|
185
|
+
topics: new TopicSnapshot(info),
|
|
186
|
+
topicFrames,
|
|
187
|
+
});
|
|
182
188
|
setId(id);
|
|
183
189
|
if (this.shutdownTimer) {
|
|
184
190
|
clearTimeout(this.shutdownTimer);
|
|
@@ -208,9 +214,7 @@ class IntercomBroker {
|
|
|
208
214
|
return deliveries;
|
|
209
215
|
}
|
|
210
216
|
for (const target of this.sessions.values()) {
|
|
211
|
-
const subscription = target.
|
|
212
|
-
(entry) => entry.topic === publication.topic,
|
|
213
|
-
);
|
|
217
|
+
const subscription = target.topics.subscription(publication.topic);
|
|
214
218
|
if (target.info.id === fromId || !subscription) {
|
|
215
219
|
continue;
|
|
216
220
|
}
|
|
@@ -234,24 +238,28 @@ class IntercomBroker {
|
|
|
234
238
|
if (!session) {
|
|
235
239
|
throw new Error("Sender session not found");
|
|
236
240
|
}
|
|
237
|
-
const prepared =
|
|
238
|
-
const nextInfo = prepared.
|
|
241
|
+
const prepared = session.topics.prepare(msg.change, session.topicFrames);
|
|
242
|
+
const nextInfo = prepared.restore
|
|
243
|
+
? session.info
|
|
244
|
+
: session.topics.snapshot(session.info, prepared);
|
|
239
245
|
const from = messageSender(nextInfo);
|
|
240
246
|
const deliveries = this.planPublications(currentId, from, prepared.publication);
|
|
241
|
-
const restore = prepared.restore;
|
|
242
|
-
const infos = [...this.sessions.values()].map((entry) =>
|
|
243
|
-
entry === session ? nextInfo : entry.info,
|
|
244
|
-
);
|
|
245
247
|
// Validate every frame before committing state or delivering publications.
|
|
246
|
-
if (restore) {
|
|
247
|
-
|
|
248
|
+
if (prepared.restore) {
|
|
249
|
+
session.topics.validateRestore(session.info, prepared, requestId);
|
|
248
250
|
}
|
|
249
|
-
const
|
|
251
|
+
const infos = prepared.restore
|
|
252
|
+
? []
|
|
253
|
+
: [...this.sessions.values()].map((entry) => {
|
|
254
|
+
if (entry === session) {
|
|
255
|
+
return nextInfo;
|
|
256
|
+
}
|
|
257
|
+
return msg.stream === true ? entry.topics.snapshot(entry.info) : entry.info;
|
|
258
|
+
});
|
|
259
|
+
const frames = snapshotFrames(requestId, infos, msg.stream === true);
|
|
250
260
|
const last = frames.pop();
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
session.info = nextInfo;
|
|
261
|
+
session.topics.commit(prepared);
|
|
262
|
+
session.info = messageSender(nextInfo);
|
|
255
263
|
const receipts: Array<SessionSnapshot["receipts"][number]> = [];
|
|
256
264
|
for (const { to, message } of deliveries) {
|
|
257
265
|
receipts.push({ to, ...this.sendToSession(currentId, to, from, message) });
|
|
@@ -326,7 +334,10 @@ class IntercomBroker {
|
|
|
326
334
|
if (!session) {
|
|
327
335
|
return;
|
|
328
336
|
}
|
|
329
|
-
const update: Record<string, unknown> = {
|
|
337
|
+
const update: Record<string, unknown> = {
|
|
338
|
+
...session.topics.snapshot(session.info),
|
|
339
|
+
lastSeen: Date.now(),
|
|
340
|
+
};
|
|
330
341
|
for (const key of [
|
|
331
342
|
"name",
|
|
332
343
|
"status",
|
|
@@ -346,7 +357,10 @@ class IntercomBroker {
|
|
|
346
357
|
throw new Error("Invalid presence update");
|
|
347
358
|
}
|
|
348
359
|
snapshotFrames(randomUUID(), [nextInfo], true);
|
|
349
|
-
|
|
360
|
+
if (msg.topics !== undefined || msg.subscriptions !== undefined) {
|
|
361
|
+
session.topics = new TopicSnapshot(nextInfo);
|
|
362
|
+
}
|
|
363
|
+
session.info = messageSender(nextInfo);
|
|
350
364
|
}
|
|
351
365
|
|
|
352
366
|
private sendToSession(
|
|
@@ -19,17 +19,67 @@ export function validateIntercomMessageSize(msg: unknown): Error | null {
|
|
|
19
19
|
* Format: 4-byte big-endian length + JSON payload
|
|
20
20
|
*/
|
|
21
21
|
export function writeMessage(socket: Socket, msg: unknown): void {
|
|
22
|
-
const tooLarge = validateIntercomMessageSize(msg);
|
|
23
|
-
if (tooLarge) {
|
|
24
|
-
throw tooLarge;
|
|
25
|
-
}
|
|
26
22
|
const json = JSON.stringify(msg);
|
|
23
|
+
const size = Buffer.byteLength(json, "utf-8");
|
|
24
|
+
if (size > MAX_FRAME_SIZE_BYTES) {
|
|
25
|
+
throw new Error(`Intercom message too large (${size} bytes; max ${MAX_FRAME_SIZE_BYTES})`);
|
|
26
|
+
}
|
|
27
27
|
const payload = Buffer.from(json, "utf-8");
|
|
28
28
|
const header = Buffer.alloc(4);
|
|
29
29
|
header.writeUInt32BE(payload.length, 0);
|
|
30
30
|
socket.write(Buffer.concat([header, payload]));
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** Retain fragments until a header or payload is complete; copy each fragmented value once. */
|
|
34
|
+
class MessageBuffer {
|
|
35
|
+
private chunks: Buffer[] = [];
|
|
36
|
+
private index = 0;
|
|
37
|
+
length = 0;
|
|
38
|
+
|
|
39
|
+
append(data: Buffer): void {
|
|
40
|
+
if (data.length > 0) {
|
|
41
|
+
this.chunks.push(data);
|
|
42
|
+
this.length += data.length;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
clear(): void {
|
|
47
|
+
this.chunks = [];
|
|
48
|
+
this.index = 0;
|
|
49
|
+
this.length = 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
read(size: number): Buffer {
|
|
53
|
+
const first = this.chunks[this.index];
|
|
54
|
+
if (size === 0) {
|
|
55
|
+
return Buffer.alloc(0);
|
|
56
|
+
}
|
|
57
|
+
const result = first.length >= size ? first.subarray(0, size) : Buffer.allocUnsafe(size);
|
|
58
|
+
let written = 0;
|
|
59
|
+
while (written < size) {
|
|
60
|
+
const chunk = this.chunks[this.index];
|
|
61
|
+
const count = Math.min(chunk.length, size - written);
|
|
62
|
+
if (first.length < size) {
|
|
63
|
+
chunk.copy(result, written, 0, count);
|
|
64
|
+
}
|
|
65
|
+
written += count;
|
|
66
|
+
this.length -= count;
|
|
67
|
+
if (count === chunk.length) {
|
|
68
|
+
this.index++;
|
|
69
|
+
} else {
|
|
70
|
+
this.chunks[this.index] = chunk.subarray(count);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (this.index === this.chunks.length) {
|
|
74
|
+
this.clear();
|
|
75
|
+
} else if (this.index * 2 >= this.chunks.length) {
|
|
76
|
+
this.chunks = this.chunks.slice(this.index);
|
|
77
|
+
this.index = 0;
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
33
83
|
/**
|
|
34
84
|
* Create a message reader that handles partial reads.
|
|
35
85
|
* Calls onMessage for each complete message received.
|
|
@@ -39,27 +89,28 @@ export function createMessageReader(
|
|
|
39
89
|
onMessage: (msg: unknown) => void,
|
|
40
90
|
onError: (error: Readonly<Error>) => void,
|
|
41
91
|
): (data: Buffer) => void {
|
|
42
|
-
|
|
92
|
+
const buffer = new MessageBuffer();
|
|
93
|
+
let length: number | undefined;
|
|
43
94
|
|
|
44
95
|
return (data: Buffer) => {
|
|
45
|
-
buffer
|
|
46
|
-
|
|
47
|
-
while (buffer.length >= 4) {
|
|
48
|
-
const length = buffer.readUInt32BE(0);
|
|
49
|
-
if (length > MAX_FRAME_SIZE_BYTES) {
|
|
50
|
-
onError(
|
|
51
|
-
new Error(`Intercom frame too large (${length} bytes; max ${MAX_FRAME_SIZE_BYTES})`),
|
|
52
|
-
);
|
|
53
|
-
buffer = Buffer.alloc(0);
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
96
|
+
buffer.append(data);
|
|
56
97
|
|
|
57
|
-
|
|
58
|
-
|
|
98
|
+
while (buffer.length >= (length ?? 4)) {
|
|
99
|
+
if (length === undefined) {
|
|
100
|
+
length = buffer.read(4).readUInt32BE(0);
|
|
101
|
+
if (length > MAX_FRAME_SIZE_BYTES) {
|
|
102
|
+
onError(
|
|
103
|
+
new Error(`Intercom frame too large (${length} bytes; max ${MAX_FRAME_SIZE_BYTES})`),
|
|
104
|
+
);
|
|
105
|
+
buffer.clear();
|
|
106
|
+
length = undefined;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
continue;
|
|
59
110
|
}
|
|
60
111
|
|
|
61
|
-
const payload = buffer.
|
|
62
|
-
|
|
112
|
+
const payload = buffer.read(length);
|
|
113
|
+
length = undefined;
|
|
63
114
|
|
|
64
115
|
let msg: unknown;
|
|
65
116
|
try {
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { isRecord, isUnknownArray, type UnknownRecord } from "../../shared/unknown.ts";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
normalizeSessionInfo,
|
|
4
|
+
type SessionInfo,
|
|
5
|
+
type SessionSnapshot,
|
|
6
|
+
type TopicSubscription,
|
|
7
|
+
type TopicUpdate,
|
|
8
|
+
} from "../types.ts";
|
|
3
9
|
|
|
4
10
|
type Receipt = SessionSnapshot["receipts"][number];
|
|
5
11
|
|
|
@@ -46,9 +52,30 @@ function receipt(value: unknown): Receipt {
|
|
|
46
52
|
};
|
|
47
53
|
}
|
|
48
54
|
|
|
55
|
+
interface SessionParts {
|
|
56
|
+
readonly info: SessionInfo;
|
|
57
|
+
readonly topics: TopicUpdate[] | undefined;
|
|
58
|
+
readonly subscriptions: TopicSubscription[] | undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function appendRows<T>(
|
|
62
|
+
previous: T[] | undefined,
|
|
63
|
+
rows: readonly T[] | undefined,
|
|
64
|
+
supplied: boolean,
|
|
65
|
+
): T[] | undefined {
|
|
66
|
+
if (rows === undefined) {
|
|
67
|
+
return supplied ? undefined : previous;
|
|
68
|
+
}
|
|
69
|
+
const result = previous ?? [];
|
|
70
|
+
for (const row of rows) {
|
|
71
|
+
result.push(row);
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
49
76
|
/** Multipart frames build a private cache; only a complete validated snapshot escapes. */
|
|
50
77
|
export class SnapshotReader {
|
|
51
|
-
private readonly sessions = new Map<string,
|
|
78
|
+
private readonly sessions = new Map<string, SessionParts>();
|
|
52
79
|
private readonly receipts: Receipt[] = [];
|
|
53
80
|
|
|
54
81
|
private addSession(row: unknown): void {
|
|
@@ -59,17 +86,20 @@ export class SnapshotReader {
|
|
|
59
86
|
const subscriptions = optionalArray(row.subscriptions);
|
|
60
87
|
const previous = this.sessions.get(row.id);
|
|
61
88
|
const info = normalizeSessionInfo({
|
|
62
|
-
...previous,
|
|
89
|
+
...previous?.info,
|
|
63
90
|
...row,
|
|
64
|
-
...(topics !== undefined ? { topics
|
|
65
|
-
...(subscriptions !== undefined
|
|
66
|
-
? { subscriptions: [...(previous?.subscriptions ?? []), ...subscriptions] }
|
|
67
|
-
: {}),
|
|
91
|
+
...(topics !== undefined ? { topics } : {}),
|
|
92
|
+
...(subscriptions !== undefined ? { subscriptions } : {}),
|
|
68
93
|
});
|
|
69
94
|
if (!info) {
|
|
70
95
|
throw new Error("Invalid session snapshot");
|
|
71
96
|
}
|
|
72
|
-
|
|
97
|
+
const { topics: newTopics, subscriptions: newSubscriptions, ...identity } = info;
|
|
98
|
+
this.sessions.set(info.id, {
|
|
99
|
+
info: identity,
|
|
100
|
+
topics: appendRows(previous?.topics, newTopics, "topics" in row),
|
|
101
|
+
subscriptions: appendRows(previous?.subscriptions, newSubscriptions, "subscriptions" in row),
|
|
102
|
+
});
|
|
73
103
|
}
|
|
74
104
|
|
|
75
105
|
append(sessions: readonly unknown[], receipts: unknown): void {
|
|
@@ -86,6 +116,14 @@ export class SnapshotReader {
|
|
|
86
116
|
}
|
|
87
117
|
|
|
88
118
|
finish(): SessionSnapshot {
|
|
89
|
-
|
|
119
|
+
const sessions: SessionInfo[] = [];
|
|
120
|
+
for (const { info, topics, subscriptions } of this.sessions.values()) {
|
|
121
|
+
sessions.push({
|
|
122
|
+
...info,
|
|
123
|
+
...(topics !== undefined ? { topics } : {}),
|
|
124
|
+
...(subscriptions !== undefined ? { subscriptions } : {}),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return { sessions, receipts: this.receipts };
|
|
90
128
|
}
|
|
91
129
|
}
|
|
@@ -10,7 +10,11 @@ import {
|
|
|
10
10
|
type TopicSubscription,
|
|
11
11
|
type SendResult,
|
|
12
12
|
} from "../types.ts";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
intercomMessageSizeBytes,
|
|
15
|
+
MAX_FRAME_SIZE_BYTES,
|
|
16
|
+
validateIntercomMessageSize,
|
|
17
|
+
} from "./framing.ts";
|
|
14
18
|
|
|
15
19
|
/** Direct messages and old clients receive identity, not potentially large topic state. */
|
|
16
20
|
export function messageSender(info: SessionInfo): SessionInfo {
|
|
@@ -124,72 +128,201 @@ function publicationUpdate(
|
|
|
124
128
|
return { ...topic, revision };
|
|
125
129
|
}
|
|
126
130
|
|
|
127
|
-
class
|
|
128
|
-
private readonly
|
|
129
|
-
private
|
|
130
|
-
private readonly
|
|
131
|
+
class SnapshotRecords<T extends { readonly topic: string }> {
|
|
132
|
+
private readonly records = new Map<string, { readonly value: T; readonly bytes: number }>();
|
|
133
|
+
private largest = 0;
|
|
134
|
+
private initial: readonly T[] | undefined;
|
|
135
|
+
private present: boolean;
|
|
131
136
|
|
|
132
|
-
constructor(
|
|
133
|
-
this.
|
|
134
|
-
this.
|
|
135
|
-
|
|
137
|
+
constructor(values: readonly T[] | undefined) {
|
|
138
|
+
this.initial = values;
|
|
139
|
+
this.present = values !== undefined;
|
|
140
|
+
for (const value of values ?? []) {
|
|
141
|
+
this.set(value, intercomMessageSizeBytes(value));
|
|
142
|
+
}
|
|
136
143
|
}
|
|
137
144
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
get(topic: string, first = false): T | undefined {
|
|
146
|
+
if (first && this.initial !== undefined) {
|
|
147
|
+
return this.initial.find((record) => record.topic === topic);
|
|
148
|
+
}
|
|
149
|
+
return this.records.get(topic)?.value;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
maximumAfter(topic?: string, bytes = 0): number {
|
|
153
|
+
const previous = topic === undefined ? undefined : this.records.get(topic);
|
|
154
|
+
if (previous?.bytes !== this.largest || bytes >= this.largest) {
|
|
155
|
+
return Math.max(this.largest, bytes);
|
|
156
|
+
}
|
|
157
|
+
// ponytail: shrinking the largest record scans cached sizes; index sizes if churn warrants it.
|
|
158
|
+
let largest = bytes;
|
|
159
|
+
for (const [key, record] of this.records) {
|
|
160
|
+
if (key !== topic) {
|
|
161
|
+
largest = Math.max(largest, record.bytes);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return largest;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
set(value: T, bytes: number): void {
|
|
168
|
+
this.largest = this.maximumAfter(value.topic, bytes);
|
|
169
|
+
this.records.set(value.topic, { value, bytes });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
delete(topic: string): void {
|
|
173
|
+
this.largest = this.maximumAfter(topic);
|
|
174
|
+
this.records.delete(topic);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
accept(): void {
|
|
178
|
+
this.initial = undefined;
|
|
179
|
+
this.present = true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
valuesAfter(topic?: string, value?: T, changed = false): readonly T[] | undefined {
|
|
183
|
+
if (!changed) {
|
|
184
|
+
if (this.initial !== undefined) {
|
|
185
|
+
return this.initial;
|
|
186
|
+
}
|
|
187
|
+
if (!this.present) {
|
|
149
188
|
return;
|
|
150
189
|
}
|
|
190
|
+
}
|
|
191
|
+
const values: T[] = [];
|
|
192
|
+
for (const [key, record] of this.records) {
|
|
193
|
+
const next = key === topic ? value : record.value;
|
|
194
|
+
if (next !== undefined) {
|
|
195
|
+
values.push(next);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (value !== undefined && !this.records.has(value.topic)) {
|
|
199
|
+
values.push(value);
|
|
200
|
+
}
|
|
201
|
+
return values;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
interface PreparedTopicChange {
|
|
206
|
+
readonly topic?: TopicUpdate;
|
|
207
|
+
readonly subscription?: TopicSubscription;
|
|
208
|
+
readonly unsubscribe?: string;
|
|
209
|
+
readonly publication?: TopicUpdate;
|
|
210
|
+
readonly restore: boolean;
|
|
211
|
+
readonly changed: boolean;
|
|
212
|
+
readonly bytes: number;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const UNCHANGED: PreparedTopicChange = { restore: false, changed: false, bytes: 0 };
|
|
216
|
+
|
|
217
|
+
/** Keep committed records indexed; restores validate only new bytes and cached envelope maxima. */
|
|
218
|
+
export class TopicSnapshot {
|
|
219
|
+
private readonly topics: SnapshotRecords<TopicUpdate>;
|
|
220
|
+
private readonly subscriptions: SnapshotRecords<TopicSubscription>;
|
|
221
|
+
|
|
222
|
+
constructor(info: SessionInfo) {
|
|
223
|
+
this.topics = new SnapshotRecords(info.topics);
|
|
224
|
+
this.subscriptions = new SnapshotRecords(info.subscriptions);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
subscription(topic: string): TopicSubscription | undefined {
|
|
228
|
+
return this.subscriptions.get(topic, true);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
prepare(change: unknown, supported: boolean): PreparedTopicChange {
|
|
232
|
+
if (change === undefined) {
|
|
233
|
+
return UNCHANGED;
|
|
234
|
+
}
|
|
235
|
+
if (!supported || !isRecord(change)) {
|
|
236
|
+
throw new Error("Topic snapshots unavailable for this client");
|
|
237
|
+
}
|
|
238
|
+
const mutation = topicMutation(change);
|
|
239
|
+
const base = { restore: change.action === "restore", changed: true, bytes: 0 };
|
|
240
|
+
switch (mutation.kind) {
|
|
241
|
+
case "publication":
|
|
242
|
+
return this.preparePublication(mutation, base);
|
|
151
243
|
case "subscription":
|
|
152
|
-
|
|
153
|
-
|
|
244
|
+
return {
|
|
245
|
+
...base,
|
|
246
|
+
subscription: mutation.subscription,
|
|
247
|
+
bytes: intercomMessageSizeBytes(mutation.subscription),
|
|
248
|
+
};
|
|
154
249
|
case "unsubscribe":
|
|
155
|
-
|
|
156
|
-
|
|
250
|
+
return { ...base, unsubscribe: mutation.topic };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private preparePublication(
|
|
255
|
+
mutation: Extract<TopicMutation, { readonly kind: "publication" }>,
|
|
256
|
+
base: PreparedTopicChange,
|
|
257
|
+
): PreparedTopicChange {
|
|
258
|
+
const previous = this.topics.get(mutation.topic.topic);
|
|
259
|
+
const update = publicationUpdate(mutation.topic, previous, mutation.publish);
|
|
260
|
+
if (!mutation.publish && (previous?.revision ?? 0) >= update.revision) {
|
|
261
|
+
return base;
|
|
157
262
|
}
|
|
263
|
+
return {
|
|
264
|
+
...base,
|
|
265
|
+
topic: update,
|
|
266
|
+
bytes: intercomMessageSizeBytes(update),
|
|
267
|
+
...(mutation.publish ? { publication: update } : {}),
|
|
268
|
+
};
|
|
158
269
|
}
|
|
159
270
|
|
|
160
|
-
snapshot(
|
|
271
|
+
snapshot(info: SessionInfo, prepared = UNCHANGED): SessionInfo {
|
|
272
|
+
const topics = this.topics.valuesAfter(prepared.topic?.topic, prepared.topic, prepared.changed);
|
|
273
|
+
const subscriptions = this.subscriptions.valuesAfter(
|
|
274
|
+
prepared.subscription?.topic ?? prepared.unsubscribe,
|
|
275
|
+
prepared.subscription,
|
|
276
|
+
prepared.changed,
|
|
277
|
+
);
|
|
161
278
|
return {
|
|
162
|
-
...
|
|
163
|
-
topics:
|
|
164
|
-
subscriptions:
|
|
165
|
-
...(publication ? { lastIntercomActivity: Date.now() } : {}),
|
|
279
|
+
...info,
|
|
280
|
+
...(topics !== undefined ? { topics } : {}),
|
|
281
|
+
...(subscriptions !== undefined ? { subscriptions } : {}),
|
|
282
|
+
...(prepared.publication ? { lastIntercomActivity: Date.now() } : {}),
|
|
166
283
|
};
|
|
167
284
|
}
|
|
168
|
-
}
|
|
169
285
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
286
|
+
validateRestore(info: SessionInfo, prepared: PreparedTopicChange, requestId: string): void {
|
|
287
|
+
snapshotFrames(requestId, [{ ...info, topics: [], subscriptions: [] }], true);
|
|
288
|
+
const topicBytes = this.topics.maximumAfter(
|
|
289
|
+
prepared.topic?.topic,
|
|
290
|
+
prepared.topic ? prepared.bytes : 0,
|
|
291
|
+
);
|
|
292
|
+
const subscriptionBytes = this.subscriptions.maximumAfter(
|
|
293
|
+
prepared.subscription?.topic,
|
|
294
|
+
prepared.subscription ? prepared.bytes : 0,
|
|
295
|
+
);
|
|
296
|
+
for (const [field, bytes] of [
|
|
297
|
+
["topics", topicBytes],
|
|
298
|
+
["subscriptions", subscriptionBytes],
|
|
299
|
+
] as const) {
|
|
300
|
+
const size =
|
|
301
|
+
intercomMessageSizeBytes({
|
|
302
|
+
type: "sessions",
|
|
303
|
+
requestId,
|
|
304
|
+
sessions: [{ id: info.id, [field]: [] }],
|
|
305
|
+
more: true,
|
|
306
|
+
}) + bytes;
|
|
307
|
+
if (size > MAX_FRAME_SIZE_BYTES) {
|
|
308
|
+
throw new Error(`Intercom message too large (${size} bytes; max ${MAX_FRAME_SIZE_BYTES})`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
182
311
|
}
|
|
183
|
-
|
|
184
|
-
|
|
312
|
+
|
|
313
|
+
commit(prepared: PreparedTopicChange): void {
|
|
314
|
+
if (prepared.changed) {
|
|
315
|
+
this.topics.accept();
|
|
316
|
+
this.subscriptions.accept();
|
|
317
|
+
}
|
|
318
|
+
if (prepared.topic) {
|
|
319
|
+
this.topics.set(prepared.topic, prepared.bytes);
|
|
320
|
+
} else if (prepared.subscription) {
|
|
321
|
+
this.subscriptions.set(prepared.subscription, prepared.bytes);
|
|
322
|
+
} else if (prepared.unsubscribe !== undefined) {
|
|
323
|
+
this.subscriptions.delete(prepared.unsubscribe);
|
|
324
|
+
}
|
|
185
325
|
}
|
|
186
|
-
const pending = new TopicSnapshot(info);
|
|
187
|
-
const publication = pending.apply(topicMutation(change));
|
|
188
|
-
return {
|
|
189
|
-
info: pending.snapshot(publication),
|
|
190
|
-
restore: change.action === "restore",
|
|
191
|
-
...(publication ? { publication } : {}),
|
|
192
|
-
};
|
|
193
326
|
}
|
|
194
327
|
|
|
195
328
|
export function deliveryAcknowledgement(receipt: SendResult): BrokerMessage {
|
|
@@ -18,7 +18,6 @@ export interface TargetResolution<T extends TargetIdentity> {
|
|
|
18
18
|
readonly status: "none" | "found" | "ambiguous" | "prefix_too_short";
|
|
19
19
|
readonly target?: T;
|
|
20
20
|
readonly matches: readonly T[];
|
|
21
|
-
readonly minLength?: number;
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
export function shortSessionId(sessionId: string): string {
|
|
@@ -180,7 +179,6 @@ function shortTargetResolution<T extends TargetIdentity>(
|
|
|
180
179
|
return {
|
|
181
180
|
status: "prefix_too_short",
|
|
182
181
|
matches: prefixes,
|
|
183
|
-
minLength: MIN_SESSION_TARGET_PREFIX_LENGTH,
|
|
184
182
|
};
|
|
185
183
|
}
|
|
186
184
|
const byId = new Map<string, T>();
|
|
@@ -6,12 +6,12 @@ import { runSynchronously } from "../../shared/cooperative.ts";
|
|
|
6
6
|
import { asyncRunRoots, readAsyncRunRecord } from "./async-resume.ts";
|
|
7
7
|
import type { AsyncRunRecord } from "./async-run-record.ts";
|
|
8
8
|
import type { AsyncRunSummary } from "./async-run-summary.ts";
|
|
9
|
-
import {
|
|
9
|
+
import { readQuestionOwner } from "../shared/supervisor-questions.ts";
|
|
10
10
|
import { findNestedRouteForRootId } from "../shared/nested-events.ts";
|
|
11
11
|
import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
|
|
12
12
|
import { validateStatusForSummary } from "./async-status-validation.ts";
|
|
13
13
|
import { asyncStatusToSummary } from "./async-status-projection.ts";
|
|
14
|
-
import { errorCode, errorMessage, hasText
|
|
14
|
+
import { errorCode, errorMessage, hasText } from "./async-value.ts";
|
|
15
15
|
|
|
16
16
|
interface AsyncRunListOptions {
|
|
17
17
|
readonly states?: readonly AsyncRunSummary["state"][];
|
|
@@ -112,14 +112,8 @@ class AsyncRunDiscovery {
|
|
|
112
112
|
let owner = this.owners.get(entry);
|
|
113
113
|
if (!hasText(owner) && hasText(this.ownerSessionId)) {
|
|
114
114
|
try {
|
|
115
|
-
const saved
|
|
116
|
-
|
|
117
|
-
);
|
|
118
|
-
if (
|
|
119
|
-
isRecord(saved) &&
|
|
120
|
-
typeof saved.sessionId === "string" &&
|
|
121
|
-
saved.sessionId.trim().length > 0
|
|
122
|
-
) {
|
|
115
|
+
const saved = readQuestionOwner(entry);
|
|
116
|
+
if (saved?.sessionId !== undefined && saved.sessionId.trim().length > 0) {
|
|
123
117
|
owner = saved.sessionId;
|
|
124
118
|
this.owners.set(entry, owner);
|
|
125
119
|
}
|