@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.
Files changed (31) hide show
  1. package/dist/apps/chat/data/timeline-query-service.js +11 -0
  2. package/dist/apps/chat/trace.js +2 -0
  3. package/dist/apps/chat/web-app.js +70 -7
  4. package/dist/apps/chat-ui/assets/{dist-BH4dS5wk.js → dist-BCB6zezO.js} +1 -1
  5. package/dist/apps/chat-ui/assets/{dist-ZN6fdzpB.js → dist-BHa-kcGl.js} +1 -1
  6. package/dist/apps/chat-ui/assets/{dist-qwl4CNfF.js → dist-BNMu92bb.js} +1 -1
  7. package/dist/apps/chat-ui/assets/{dist-BCqTb13C.js → dist-BZ2eTC4f.js} +1 -1
  8. package/dist/apps/chat-ui/assets/{dist-Ds5VZTeC.js → dist-CnVsqwSG.js} +1 -1
  9. package/dist/apps/chat-ui/assets/{dist-BXmdj7mb.js → dist-CzE6k3F3.js} +1 -1
  10. package/dist/apps/chat-ui/assets/{dist-CdhJkLlg.js → dist-D811wJeV.js} +1 -1
  11. package/dist/apps/chat-ui/assets/{dist-CDTTlyfG.js → dist-Dq4GxJi3.js} +1 -1
  12. package/dist/apps/chat-ui/assets/{dist-D7FSg1x8.js → dist-HqTN67dc.js} +1 -1
  13. package/dist/apps/chat-ui/assets/{dist-4VdcCJgh.js → dist-WyXdYl-w.js} +1 -1
  14. package/dist/apps/chat-ui/assets/{dist--Sb99Zop.js → dist-yCYNNb5d.js} +1 -1
  15. package/dist/apps/chat-ui/assets/{index-Gf-Zb-P-.js → index-DwHJfmiF.js} +81 -81
  16. package/dist/apps/chat-ui/index.html +1 -1
  17. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  18. package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.9.13.vsix +0 -0
  19. package/dist/core/session-router.js +7 -0
  20. package/dist/data/ingest-service.js +7 -0
  21. package/dist/data/message-store.js +11 -0
  22. package/dist/data/schema.js +2 -0
  23. package/dist/gateway/server.js +2 -0
  24. package/dist/resources/lifecycle.js +4 -6
  25. package/dist/resources/reaper-state.js +30 -3
  26. package/dist/resources/reaper.js +43 -15
  27. package/dist/shared/trace-engine.js +3 -2
  28. package/dist/shared/trace-event-projection.js +26 -0
  29. package/dist/signals/registry.js +24 -1
  30. package/dist/signals/status.js +12 -0
  31. 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-Gf-Zb-P-.js"></script>
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">
@@ -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 {
@@ -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
@@ -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 planReapWorkers(options);
248
+ return await planCompute(options);
249
249
  }
250
- catch (error) {
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 CLI is unavailable in this runtime; browser and stale-file cleanup remain active."];
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
- export async function writeResourceReaperState(path, state) {
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
- await rename(temporaryPath, path);
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 });
@@ -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
- if (this.state && this.ownsTimer) {
51
- this.state = { ...this.state, status: "stopped", nextRunAt: undefined };
52
- await this.persist();
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
- const result = await this.apply(plan);
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
- this.running = false;
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().finally(() => this.arm(this.intervalMs));
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
- await writeResourceReaperState(this.statePath, this.state);
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 nodes = traceNodesFromEntries(input.session.id, entries, messageTurnTimingsFromEvents(events));
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 = [];
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "1.9.10",
3
+ "version": "1.9.13",
4
4
  "type": "module",
5
5
  "imports": {
6
6
  "vscode": "./src/apps/chat-vscode/extension/src/vscode-shim.js"