@pasko70/pibo 1.9.10 → 1.9.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apps/chat/data/timeline-query-service.js +11 -0
- package/dist/apps/chat/trace.js +2 -0
- package/dist/apps/chat/web-app.js +70 -7
- package/dist/apps/chat-ui/assets/{dist-BH4dS5wk.js → dist-BCB6zezO.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-ZN6fdzpB.js → dist-BHa-kcGl.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-qwl4CNfF.js → dist-BNMu92bb.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BCqTb13C.js → dist-BZ2eTC4f.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Ds5VZTeC.js → dist-CnVsqwSG.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BXmdj7mb.js → dist-CzE6k3F3.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CdhJkLlg.js → dist-D811wJeV.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CDTTlyfG.js → dist-Dq4GxJi3.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-D7FSg1x8.js → dist-HqTN67dc.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-4VdcCJgh.js → dist-WyXdYl-w.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist--Sb99Zop.js → dist-yCYNNb5d.js} +1 -1
- package/dist/apps/chat-ui/assets/{index-Gf-Zb-P-.js → index-DwHJfmiF.js} +81 -81
- package/dist/apps/chat-ui/index.html +1 -1
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.9.13.vsix +0 -0
- package/dist/core/session-router.js +7 -0
- package/dist/data/ingest-service.js +7 -0
- package/dist/data/message-store.js +11 -0
- package/dist/data/schema.js +2 -0
- package/dist/gateway/server.js +2 -0
- package/dist/resources/lifecycle.js +4 -6
- package/dist/resources/reaper-state.js +30 -3
- package/dist/resources/reaper.js +43 -15
- package/dist/shared/trace-engine.js +3 -2
- package/dist/shared/trace-event-projection.js +26 -0
- package/dist/signals/registry.js +24 -1
- package/dist/signals/status.js +12 -0
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<link rel="manifest" href="/apps/chat/manifest.webmanifest" />
|
|
10
10
|
<link rel="apple-touch-icon" href="/apps/chat/assets/pwa-images/ios/180.png" />
|
|
11
11
|
<title>Pibo Web Chat</title>
|
|
12
|
-
<script type="module" crossorigin src="/apps/chat/assets/index-
|
|
12
|
+
<script type="module" crossorigin src="/apps/chat/assets/index-DwHJfmiF.js"></script>
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/apps/chat/assets/rolldown-runtime-S-ySWqyJ.js">
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/apps/chat/assets/dist-SVPsM5Oi.js">
|
|
15
15
|
<link rel="stylesheet" crossorigin href="/apps/chat/assets/index-C0x9nEcf.css">
|
|
Binary file
|
|
Binary file
|
|
@@ -353,9 +353,16 @@ export class PiboSessionRouter {
|
|
|
353
353
|
this.projectKnownSessionSignals();
|
|
354
354
|
return this.signalRegistry.snapshotTree(rootPiboSessionId);
|
|
355
355
|
}
|
|
356
|
+
snapshotSignalStatuses() {
|
|
357
|
+
this.projectKnownSessionSignals();
|
|
358
|
+
return this.signalRegistry.snapshotStatuses();
|
|
359
|
+
}
|
|
356
360
|
subscribeSignalTree(rootPiboSessionId, listener) {
|
|
357
361
|
return this.signalRegistry.subscribe(rootPiboSessionId, listener);
|
|
358
362
|
}
|
|
363
|
+
subscribeSignalStatuses(listener) {
|
|
364
|
+
return this.signalRegistry.subscribeAll(listener);
|
|
365
|
+
}
|
|
359
366
|
async emitMessageAndWaitForReply(event, timeoutMs = 120000) {
|
|
360
367
|
const eventWithId = { ...event, id: event.id ?? randomUUID() };
|
|
361
368
|
return await new Promise((resolve, reject) => {
|
|
@@ -120,6 +120,13 @@ export class ChatDataIngestService {
|
|
|
120
120
|
indexedAt: now,
|
|
121
121
|
});
|
|
122
122
|
let messageId;
|
|
123
|
+
if (event.type === "message_finished" && event.eventId) {
|
|
124
|
+
this.store.messages.completeAssistantMessagesForTurn({
|
|
125
|
+
sessionId: input.session.id,
|
|
126
|
+
turnId: event.eventId,
|
|
127
|
+
completedAt: now,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
123
130
|
if (event.type === "assistant_message") {
|
|
124
131
|
messageId = messageIdForOutputEvent(event);
|
|
125
132
|
if (messageId && !this.store.messages.getMessage(messageId)) {
|
|
@@ -38,6 +38,17 @@ export class MessageStore {
|
|
|
38
38
|
const rows = this.db.prepare("SELECT * FROM chat_messages WHERE session_id = ? ORDER BY sequence ASC").all(sessionId);
|
|
39
39
|
return rows.map(messageFromRow);
|
|
40
40
|
}
|
|
41
|
+
completeAssistantMessagesForTurn(input) {
|
|
42
|
+
const result = this.db.prepare(`
|
|
43
|
+
UPDATE chat_messages
|
|
44
|
+
SET completed_at = ?,
|
|
45
|
+
status = ?
|
|
46
|
+
WHERE session_id = ?
|
|
47
|
+
AND turn_id = ?
|
|
48
|
+
AND role = 'assistant'
|
|
49
|
+
`).run(input.completedAt, input.status ?? "complete", input.sessionId, input.turnId);
|
|
50
|
+
return Number(result.changes ?? 0);
|
|
51
|
+
}
|
|
41
52
|
}
|
|
42
53
|
function messageFromRow(row) {
|
|
43
54
|
return {
|
package/dist/data/schema.js
CHANGED
|
@@ -356,6 +356,8 @@ export function applyPiboDataSchema(db) {
|
|
|
356
356
|
ON event_log(session_id, stream_id);
|
|
357
357
|
CREATE INDEX IF NOT EXISTS idx_event_log_session_sequence_stream
|
|
358
358
|
ON event_log(session_id, session_sequence DESC, stream_id DESC);
|
|
359
|
+
CREATE INDEX IF NOT EXISTS idx_event_log_session_type_sequence_stream
|
|
360
|
+
ON event_log(session_id, type, session_sequence ASC, stream_id ASC);
|
|
359
361
|
CREATE INDEX IF NOT EXISTS idx_event_log_room_stream
|
|
360
362
|
ON event_log(room_id, stream_id);
|
|
361
363
|
CREATE INDEX IF NOT EXISTS idx_event_log_topic_stream
|
package/dist/gateway/server.js
CHANGED
|
@@ -319,7 +319,9 @@ export class PiboGatewayServer {
|
|
|
319
319
|
listRuns: (options) => this.requireRouter().listRuns(options),
|
|
320
320
|
snapshotSignalSession: (piboSessionId) => this.requireRouter().snapshotSignalSession(piboSessionId),
|
|
321
321
|
snapshotSignalTree: (rootPiboSessionId) => this.requireRouter().snapshotSignalTree(rootPiboSessionId),
|
|
322
|
+
snapshotSignalStatuses: () => this.requireRouter().snapshotSignalStatuses(),
|
|
322
323
|
subscribeSignalTree: (rootPiboSessionId, listener) => this.requireRouter().subscribeSignalTree(rootPiboSessionId, listener),
|
|
324
|
+
subscribeSignalStatuses: (listener) => this.requireRouter().subscribeSignalStatuses(listener),
|
|
323
325
|
getGatewayActions: () => this.pluginRegistry.getGatewayActionInfos(),
|
|
324
326
|
getProfiles: () => this.pluginRegistry.getProfileInfos(),
|
|
325
327
|
createProfile: (name) => this.pluginRegistry.createProfile(name),
|
|
@@ -243,15 +243,13 @@ function buildBrowserReapPlanItem(record, now, idleTimeoutMinutes) {
|
|
|
243
243
|
preservesWorktree: true,
|
|
244
244
|
};
|
|
245
245
|
}
|
|
246
|
-
async function planComputeReapSafely(options) {
|
|
246
|
+
export async function planComputeReapSafely(options, planCompute = planReapWorkers) {
|
|
247
247
|
try {
|
|
248
|
-
return await
|
|
248
|
+
return await planCompute(options);
|
|
249
249
|
}
|
|
250
|
-
catch
|
|
251
|
-
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
252
|
-
throw error;
|
|
250
|
+
catch {
|
|
253
251
|
const plan = buildComputeWorkerReapPlan([], options);
|
|
254
|
-
plan.nextCommands = ["Docker
|
|
252
|
+
plan.nextCommands = ["Docker compute cleanup is unavailable; browser and stale-file cleanup remain active."];
|
|
255
253
|
return plan;
|
|
256
254
|
}
|
|
257
255
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
3
|
import { mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import { dirname } from "node:path";
|
|
@@ -5,11 +6,37 @@ import { piboHomePath } from "../core/pibo-home.js";
|
|
|
5
6
|
export function defaultResourceReaperStatePath() {
|
|
6
7
|
return process.env.PIBO_RESOURCE_REAPER_STATE_PATH || piboHomePath("resource-reaper-state.json");
|
|
7
8
|
}
|
|
8
|
-
|
|
9
|
+
const DEFAULT_RENAME_RETRY_DELAYS_MS = [10, 25, 50, 100, 200];
|
|
10
|
+
const TRANSIENT_RENAME_ERROR_CODES = new Set(["EACCES", "EBUSY", "ENOTEMPTY", "EPERM"]);
|
|
11
|
+
export async function writeResourceReaperState(path, state, options = {}) {
|
|
9
12
|
await mkdir(dirname(path), { recursive: true });
|
|
10
|
-
const temporaryPath = `${path}.${process.pid}.tmp`;
|
|
13
|
+
const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
14
|
+
const renameFile = options.rename ?? rename;
|
|
15
|
+
const wait = options.wait ?? defaultWait;
|
|
16
|
+
const retryDelaysMs = options.retryDelaysMs ?? DEFAULT_RENAME_RETRY_DELAYS_MS;
|
|
11
17
|
await writeFile(temporaryPath, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
12
|
-
|
|
18
|
+
try {
|
|
19
|
+
for (let attempt = 0;; attempt += 1) {
|
|
20
|
+
try {
|
|
21
|
+
await renameFile(temporaryPath, path);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (!isTransientRenameError(error) || attempt >= retryDelaysMs.length)
|
|
26
|
+
throw error;
|
|
27
|
+
await wait(retryDelaysMs[attempt]);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function isTransientRenameError(error) {
|
|
36
|
+
return error instanceof Error && "code" in error && TRANSIENT_RENAME_ERROR_CODES.has(String(error.code));
|
|
37
|
+
}
|
|
38
|
+
async function defaultWait(delayMs) {
|
|
39
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
13
40
|
}
|
|
14
41
|
export async function claimResourceReaperOwnership(lockPath, pid = process.pid, isPidAlive = defaultIsPidAlive) {
|
|
15
42
|
await mkdir(dirname(lockPath), { recursive: true });
|
package/dist/resources/reaper.js
CHANGED
|
@@ -9,6 +9,7 @@ export class ResourceReaperService {
|
|
|
9
9
|
plan;
|
|
10
10
|
apply;
|
|
11
11
|
now;
|
|
12
|
+
writeState;
|
|
12
13
|
timer;
|
|
13
14
|
running = false;
|
|
14
15
|
stopped = true;
|
|
@@ -23,6 +24,7 @@ export class ResourceReaperService {
|
|
|
23
24
|
this.plan = options.plan ?? planResourceReap;
|
|
24
25
|
this.apply = options.apply ?? applyResourceReapPlan;
|
|
25
26
|
this.now = options.clock ?? (() => new Date());
|
|
27
|
+
this.writeState = options.writeState ?? writeResourceReaperState;
|
|
26
28
|
}
|
|
27
29
|
async start() {
|
|
28
30
|
if (!this.stopped)
|
|
@@ -47,19 +49,24 @@ export class ResourceReaperService {
|
|
|
47
49
|
if (this.timer)
|
|
48
50
|
clearTimeout(this.timer);
|
|
49
51
|
this.timer = undefined;
|
|
50
|
-
|
|
51
|
-
this.state
|
|
52
|
-
|
|
52
|
+
try {
|
|
53
|
+
if (this.state && this.ownsTimer) {
|
|
54
|
+
this.state = { ...this.state, status: "stopped", nextRunAt: undefined };
|
|
55
|
+
await this.persist();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
if (this.ownsTimer)
|
|
60
|
+
await releaseResourceReaperOwnership(this.lockPath);
|
|
61
|
+
this.ownsTimer = false;
|
|
53
62
|
}
|
|
54
|
-
if (this.ownsTimer)
|
|
55
|
-
await releaseResourceReaperOwnership(this.lockPath);
|
|
56
|
-
this.ownsTimer = false;
|
|
57
63
|
}
|
|
58
64
|
async runNow() {
|
|
59
65
|
if (!this.ownsTimer || this.running)
|
|
60
66
|
return undefined;
|
|
61
67
|
this.running = true;
|
|
62
68
|
const runAt = this.now();
|
|
69
|
+
let result;
|
|
63
70
|
try {
|
|
64
71
|
const plan = await this.plan({
|
|
65
72
|
includeDev: this.options.includeDev,
|
|
@@ -71,7 +78,7 @@ export class ResourceReaperService {
|
|
|
71
78
|
exemptBrowserPids: this.options.exemptBrowserPids,
|
|
72
79
|
now: runAt,
|
|
73
80
|
});
|
|
74
|
-
|
|
81
|
+
result = await this.apply(plan);
|
|
75
82
|
this.state = {
|
|
76
83
|
...(this.state ?? {
|
|
77
84
|
status: "running",
|
|
@@ -91,8 +98,6 @@ export class ResourceReaperService {
|
|
|
91
98
|
lastError: undefined,
|
|
92
99
|
};
|
|
93
100
|
console.error(JSON.stringify({ event: "resource_reaper_finished", at: runAt.toISOString(), ...this.state.lastResult }));
|
|
94
|
-
await this.persist();
|
|
95
|
-
return result;
|
|
96
101
|
}
|
|
97
102
|
catch (error) {
|
|
98
103
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -109,12 +114,16 @@ export class ResourceReaperService {
|
|
|
109
114
|
lastError: message,
|
|
110
115
|
};
|
|
111
116
|
console.error(JSON.stringify({ event: "resource_reaper_failed", at: runAt.toISOString(), error: message }));
|
|
112
|
-
await this.persist();
|
|
113
|
-
return undefined;
|
|
114
117
|
}
|
|
115
118
|
finally {
|
|
116
|
-
|
|
119
|
+
try {
|
|
120
|
+
await this.persist();
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
this.running = false;
|
|
124
|
+
}
|
|
117
125
|
}
|
|
126
|
+
return result;
|
|
118
127
|
}
|
|
119
128
|
arm(delayMs) {
|
|
120
129
|
if (this.stopped)
|
|
@@ -122,13 +131,32 @@ export class ResourceReaperService {
|
|
|
122
131
|
if (this.timer)
|
|
123
132
|
clearTimeout(this.timer);
|
|
124
133
|
this.timer = setTimeout(() => {
|
|
125
|
-
void this.runNow()
|
|
134
|
+
void this.runNow()
|
|
135
|
+
.catch((error) => {
|
|
136
|
+
console.error(JSON.stringify({
|
|
137
|
+
event: "resource_reaper_timer_failed",
|
|
138
|
+
at: new Date().toISOString(),
|
|
139
|
+
error: error instanceof Error ? error.message : String(error),
|
|
140
|
+
}));
|
|
141
|
+
})
|
|
142
|
+
.finally(() => this.arm(this.intervalMs));
|
|
126
143
|
}, delayMs);
|
|
127
144
|
this.timer.unref?.();
|
|
128
145
|
}
|
|
129
146
|
async persist() {
|
|
130
|
-
if (this.state)
|
|
131
|
-
|
|
147
|
+
if (!this.state)
|
|
148
|
+
return;
|
|
149
|
+
try {
|
|
150
|
+
await this.writeState(this.statePath, this.state);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
console.error(JSON.stringify({
|
|
154
|
+
event: "resource_reaper_state_persist_failed",
|
|
155
|
+
at: new Date().toISOString(),
|
|
156
|
+
path: this.statePath,
|
|
157
|
+
error: error instanceof Error ? error.message : String(error),
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
132
160
|
}
|
|
133
161
|
}
|
|
134
162
|
function readPositiveInteger(value) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { reconcileAsyncAgentRunStatuses } from "./trace-async-agent-runs.js";
|
|
2
|
-
import { applySingleEventToNodes, contentDeltaPatchNodeId, dedupeTraceEvents, eventsCanAffectAsyncAgentRunStatus, findOpenTranscriptEventIds, isConfirmedUserMessageEcho, latestTraceStreamId, messageTurnTimingsFromEvents, reconcileTranscriptUserMessageTimestamps, traceEventDedupeKey, } from "./trace-event-projection.js";
|
|
2
|
+
import { applySingleEventToNodes, contentDeltaPatchNodeId, dedupeTraceEvents, eventsCanAffectAsyncAgentRunStatus, findOpenTranscriptEventIds, isConfirmedUserMessageEcho, latestTraceStreamId, mergeMessageTurnTimings, messageTurnTimingsFromEvents, reconcileTranscriptUserMessageTimestamps, traceEventDedupeKey, } from "./trace-event-projection.js";
|
|
3
3
|
import { flattenTraceNodes, mapTraceNodesById, nestTraceNodes } from "./trace-nodes.js";
|
|
4
4
|
import { nestMutableCopiedTraceNodes, shareUnchangedTraceNodes } from "./trace-patch-nodes.js";
|
|
5
5
|
import { mapTraceChildSessionsByParent, mapTraceSubagentSessionLinks, } from "./trace-subagent-links.js";
|
|
@@ -14,7 +14,8 @@ export function buildTraceViewFromEvents(input) {
|
|
|
14
14
|
const allEntries = input.transcriptEntries ?? [];
|
|
15
15
|
const openTranscriptEventIds = findOpenTranscriptEventIds(events, sessionStatus);
|
|
16
16
|
const entries = projectTranscriptEntries(allEntries, sessionStatus, openTranscriptEventIds);
|
|
17
|
-
const
|
|
17
|
+
const turnTimings = mergeMessageTurnTimings(input.turnTimings ?? [], messageTurnTimingsFromEvents(events));
|
|
18
|
+
const nodes = traceNodesFromEntries(input.session.id, entries, turnTimings);
|
|
18
19
|
reconcileTranscriptUserMessageTimestamps(nodes, events);
|
|
19
20
|
const byId = mapTraceNodesById(nodes);
|
|
20
21
|
const childByParent = mapTraceChildSessionsByParent(input.sessions ?? []);
|
|
@@ -531,6 +531,32 @@ function shouldKeepTranscriptEchoEvent(event, openTranscriptEventIds) {
|
|
|
531
531
|
function isStaleToolCallEchoEvent(event, sessionStatus) {
|
|
532
532
|
return sessionStatus !== "running" && event.type === "tool_call";
|
|
533
533
|
}
|
|
534
|
+
export function mergeMessageTurnTimings(...groups) {
|
|
535
|
+
const byEventId = new Map();
|
|
536
|
+
for (const timing of groups.flat()) {
|
|
537
|
+
const existing = byEventId.get(timing.eventId);
|
|
538
|
+
if (!existing) {
|
|
539
|
+
byEventId.set(timing.eventId, timing);
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
const merged = {
|
|
543
|
+
eventId: timing.eventId,
|
|
544
|
+
userText: timing.userText ?? existing.userText,
|
|
545
|
+
startedAt: timing.startedAt ?? existing.startedAt,
|
|
546
|
+
completedAt: timing.completedAt ?? existing.completedAt,
|
|
547
|
+
durationMs: timing.durationMs ?? existing.durationMs,
|
|
548
|
+
};
|
|
549
|
+
if (merged.durationMs === undefined) {
|
|
550
|
+
const startedAtMs = parseTimestamp(merged.startedAt);
|
|
551
|
+
const completedAtMs = parseTimestamp(merged.completedAt);
|
|
552
|
+
if (startedAtMs !== undefined && completedAtMs !== undefined) {
|
|
553
|
+
merged.durationMs = Math.max(0, completedAtMs - startedAtMs);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
byEventId.set(timing.eventId, merged);
|
|
557
|
+
}
|
|
558
|
+
return [...byEventId.values()];
|
|
559
|
+
}
|
|
534
560
|
export function messageTurnTimingsFromEvents(events) {
|
|
535
561
|
const timings = new Map();
|
|
536
562
|
const completedEventIds = [];
|
package/dist/signals/registry.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DEFAULT_TELEMETRY_STALE_THRESHOLD_MS } from "../core/telemetry-staleness.js";
|
|
2
2
|
import { errorFromNode, isActiveSignalStatus, isTerminalSignalStatus, phaseForStatus, strongestStatus } from "./aggregate.js";
|
|
3
3
|
import { createDefaultSignalProducers } from "./projector.js";
|
|
4
|
+
import { summarizeSessionSignalStatus } from "./status.js";
|
|
4
5
|
function now() { return new Date().toISOString(); }
|
|
5
6
|
const DEFAULT_TERMINAL_SUCCESS_TTL_MS = 60_000;
|
|
6
7
|
const DEFAULT_TERMINAL_ERROR_TTL_MS = 10 * 60_000;
|
|
@@ -226,6 +227,7 @@ export class InMemoryPiboSignalRegistry {
|
|
|
226
227
|
sessionSnapshotById = new Map();
|
|
227
228
|
queuedMessagesBySessionId = new Map();
|
|
228
229
|
subscribersByRootId = new Map();
|
|
230
|
+
globalSubscribers = new Set();
|
|
229
231
|
producers = createDefaultSignalProducers();
|
|
230
232
|
constructor(options = {}) {
|
|
231
233
|
this.options = options;
|
|
@@ -260,7 +262,7 @@ export class InMemoryPiboSignalRegistry {
|
|
|
260
262
|
nodeCount: this.nodesById.size,
|
|
261
263
|
sessionCount: this.rootSessionIdBySessionId.size,
|
|
262
264
|
rootCount: this.versionByRootId.size,
|
|
263
|
-
subscriberCount: [...this.subscribersByRootId.values()].reduce((sum, listeners) => sum + listeners.size, 0),
|
|
265
|
+
subscriberCount: this.globalSubscribers.size + [...this.subscribersByRootId.values()].reduce((sum, listeners) => sum + listeners.size, 0),
|
|
264
266
|
subscribersByRootId,
|
|
265
267
|
stuckActiveNodes: [...this.nodesById.values()].filter((node) => isActiveSignalStatus(node.status) && nowMs - Date.parse(node.startedAt ?? node.createdAt) >= thresholdMs),
|
|
266
268
|
};
|
|
@@ -322,6 +324,21 @@ export class InMemoryPiboSignalRegistry {
|
|
|
322
324
|
}
|
|
323
325
|
return { rootPiboSessionId: rootId, version, generatedAt: now(), sessions, nodes };
|
|
324
326
|
}
|
|
327
|
+
snapshotStatuses() {
|
|
328
|
+
const sessions = {};
|
|
329
|
+
for (const piboSessionId of this.rootSessionIdBySessionId.keys()) {
|
|
330
|
+
const rootId = this.getSessionRoot(piboSessionId);
|
|
331
|
+
const version = this.versionByRootId.get(rootId) ?? 0;
|
|
332
|
+
const snapshot = this.sessionSnapshotById.get(piboSessionId) ?? this.computeSessionSnapshot(piboSessionId, version);
|
|
333
|
+
sessions[piboSessionId] = summarizeSessionSignalStatus(snapshot);
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
type: "signal_status_snapshot",
|
|
337
|
+
generatedAt: now(),
|
|
338
|
+
rootVersions: Object.fromEntries(this.versionByRootId),
|
|
339
|
+
sessions,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
325
342
|
subscribe(rootPiboSessionId, listener) {
|
|
326
343
|
const rootId = this.getSessionRoot(rootPiboSessionId);
|
|
327
344
|
const listeners = this.subscribersByRootId.get(rootId) ?? new Set();
|
|
@@ -333,6 +350,10 @@ export class InMemoryPiboSignalRegistry {
|
|
|
333
350
|
this.subscribersByRootId.delete(rootId);
|
|
334
351
|
};
|
|
335
352
|
}
|
|
353
|
+
subscribeAll(listener) {
|
|
354
|
+
this.globalSubscribers.add(listener);
|
|
355
|
+
return () => this.globalSubscribers.delete(listener);
|
|
356
|
+
}
|
|
336
357
|
context() {
|
|
337
358
|
return {
|
|
338
359
|
now,
|
|
@@ -547,6 +568,8 @@ export class InMemoryPiboSignalRegistry {
|
|
|
547
568
|
notify(rootId, patch) {
|
|
548
569
|
for (const listener of this.subscribersByRootId.get(rootId) ?? [])
|
|
549
570
|
queueMicrotask(() => listener(patch));
|
|
571
|
+
for (const listener of this.globalSubscribers)
|
|
572
|
+
queueMicrotask(() => listener(patch));
|
|
550
573
|
}
|
|
551
574
|
}
|
|
552
575
|
export function createPiboSignalRegistry(options) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function summarizeSessionSignalStatus(snapshot) {
|
|
2
|
+
const isTurnActive = snapshot.latestTurn?.state === "running";
|
|
3
|
+
const hasError = snapshot.hasError || snapshot.hasErrorDescendant || snapshot.aggregateStatus === "error";
|
|
4
|
+
const isTreeActive = snapshot.isTreeActive || isTurnActive;
|
|
5
|
+
return {
|
|
6
|
+
piboSessionId: snapshot.piboSessionId,
|
|
7
|
+
rootPiboSessionId: snapshot.rootPiboSessionId,
|
|
8
|
+
updatedAt: snapshot.updatedAt,
|
|
9
|
+
status: isTreeActive ? "running" : hasError ? "error" : "idle",
|
|
10
|
+
isTreeActive,
|
|
11
|
+
};
|
|
12
|
+
}
|