@pasko70/pibo 3.0.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +20 -6
  2. package/compute-image/Dockerfile +47 -0
  3. package/compute-image/Dockerfile.dockerignore +5 -0
  4. package/dist/agent-runtime/context-build.js +26 -2
  5. package/dist/agent-runtime/contract.js +78 -28
  6. package/dist/agent-runtime/portable-history.js +19 -9
  7. package/dist/agent-runtime/registry.js +8 -2
  8. package/dist/agent-runtime/routed-session.js +120 -15
  9. package/dist/agent-runtimes/codex-native/adapter.js +12 -0
  10. package/dist/agent-runtimes/codex-native/resource-delivery.js +35 -7
  11. package/dist/agent-runtimes/omp/adapter.js +3 -7
  12. package/dist/agent-runtimes/pi/adapter.js +5 -3
  13. package/dist/agent-runtimes/pi/routed-session.js +59 -12
  14. package/dist/agent-runtimes/pi/runtime.js +6 -0
  15. package/dist/apps/chat/agent-profiles.js +31 -2
  16. package/dist/apps/chat/agent-store.js +98 -6
  17. package/dist/apps/chat/chat-request-normalizers.js +31 -0
  18. package/dist/apps/chat/data/chat-data-mappers.js +61 -11
  19. package/dist/apps/chat/data/project-service.js +119 -15
  20. package/dist/apps/chat/data/room-service.js +21 -0
  21. package/dist/apps/chat/data/session-query-service.js +33 -10
  22. package/dist/apps/chat/data/timeline-query-service.js +60 -35
  23. package/dist/apps/chat/loop-api.js +10 -2
  24. package/dist/apps/chat/output-compactor.js +144 -21
  25. package/dist/apps/chat/output-event-policy.js +179 -2
  26. package/dist/apps/chat/stream.js +16 -7
  27. package/dist/apps/chat/trace-v2.js +1 -0
  28. package/dist/apps/chat/web-app.js +317 -86
  29. package/dist/apps/chat-ui/assets/{dist-BDIATCQt.js → dist-CUQJqggN.js} +1 -1
  30. package/dist/apps/chat-ui/assets/{dist-mqCviI-c.js → dist-ClUlQWYN.js} +1 -1
  31. package/dist/apps/chat-ui/assets/{dist-JnW4v8UE.js → dist-Djul9BmZ.js} +1 -1
  32. package/dist/apps/chat-ui/assets/{dist-lRiLP9rr.js → dist-GD0JGdCi.js} +1 -1
  33. package/dist/apps/chat-ui/assets/{dist-BBDMeU5-.js → dist-W5HOqHym.js} +1 -1
  34. package/dist/apps/chat-ui/assets/index-BcjkX-iP.js +228 -0
  35. package/dist/apps/chat-ui/assets/index-CmqRSbBU.css +1 -0
  36. package/dist/apps/chat-ui/index.html +2 -2
  37. package/dist/apps/chat-vscode-web/assets/index-JvrPUGvI.js +43 -0
  38. package/dist/apps/chat-vscode-web/index.html +1 -1
  39. package/dist/apps/cli-ui/InkSessionApp.js +111 -12
  40. package/dist/apps/cli-ui/cliSessionsCommand.js +34 -8
  41. package/dist/cli-session/localSessionSource.js +153 -161
  42. package/dist/cli.js +7 -11
  43. package/dist/compute/cli.js +35 -16
  44. package/dist/compute/docker.js +341 -94
  45. package/dist/config/config.js +3 -0
  46. package/dist/core/context-build.js +7 -16
  47. package/dist/core/output-persistence-retry.js +484 -0
  48. package/dist/core/output-render-sequence.js +331 -0
  49. package/dist/core/profiles.js +24 -0
  50. package/dist/core/provider-recovery.js +7 -1
  51. package/dist/core/session-model.js +37 -1
  52. package/dist/core/session-router.js +78 -110
  53. package/dist/data/ingest-service.js +91 -9
  54. package/dist/data/navigation-store.js +5 -5
  55. package/dist/data/pibo-store.js +8 -1
  56. package/dist/data/schema.js +243 -3
  57. package/dist/data/session-store.js +3 -3
  58. package/dist/debug/agents.js +91 -89
  59. package/dist/debug/index.js +46 -2
  60. package/dist/debug/pty.js +127 -48
  61. package/dist/gateway/cli.js +4 -0
  62. package/dist/gateway/web.js +8 -8
  63. package/dist/local/client.js +3 -2
  64. package/dist/loops/channel.js +3 -1
  65. package/dist/loops/cli.js +2 -1
  66. package/dist/loops/service.js +90 -11
  67. package/dist/loops/store.js +100 -74
  68. package/dist/mcp/agent-context.js +13 -26
  69. package/dist/mcp/commands/info.js +3 -1
  70. package/dist/mcp/config-command.js +46 -2
  71. package/dist/mcp/config.js +18 -4
  72. package/dist/plugins/context-files-store.js +36 -6
  73. package/dist/plugins/context-files.js +52 -1
  74. package/dist/plugins/registry.js +2 -0
  75. package/dist/plugins/user-profile-resources.js +22 -0
  76. package/dist/previews/base-url.js +34 -0
  77. package/dist/previews/config.js +2 -32
  78. package/dist/ralph/store.js +5 -0
  79. package/dist/reliability/store.js +378 -106
  80. package/dist/session-ui/terminalRows.js +8 -4
  81. package/dist/sessions/pibo-data-store.js +145 -0
  82. package/dist/sessions/store.js +42 -0
  83. package/dist/setup/cli.js +422 -6
  84. package/dist/setup/installation-profiles.js +464 -0
  85. package/dist/shared/deterministic-digest.js +94 -0
  86. package/dist/shared/trace-engine.js +54 -0
  87. package/dist/shared/trace-event-projection.js +124 -67
  88. package/dist/shared/trace-history.js +47 -12
  89. package/dist/shared/trace-live-reducer.js +23 -4
  90. package/dist/shared/trace-nodes.js +17 -28
  91. package/dist/shared/trace-order.js +41 -5
  92. package/dist/shared/trace-page-merge.js +44 -14
  93. package/dist/shared/trace-patch-nodes.js +2 -0
  94. package/dist/shared/trace-subagent-links.js +4 -1
  95. package/dist/shared/trace-tool-identity.js +15 -8
  96. package/dist/signals/projector.js +28 -6
  97. package/dist/subagents/observation-query.js +121 -0
  98. package/dist/subagents/tool.js +7 -3
  99. package/dist/tools/agent-browser-leases.js +58 -23
  100. package/dist/tools/agent-browser-wrapper.js +1 -1
  101. package/dist/tools/browser-use-leases.js +129 -16
  102. package/dist/tools/browser-use-wrapper.js +1 -1
  103. package/dist/tools/index.js +27 -11
  104. package/dist/tools/python-runtime.js +10 -3
  105. package/dist/tools/registry.js +1 -1
  106. package/dist/tools/runtime/node-backend.js +16 -5
  107. package/dist/tools/runtime/node-worker-source.js +112 -33
  108. package/dist/tools/runtime/python-backend.js +16 -5
  109. package/dist/tools/runtime/python-worker-source.js +167 -16
  110. package/dist/tools/runtime/registry.js +23 -10
  111. package/dist/user-skills/store.js +0 -1
  112. package/docs/ops/vscode-extension-release.md +9 -1
  113. package/npm-shrinkwrap.json +9 -2
  114. package/package.json +9 -2
  115. package/scripts/docker-entrypoint.sh +46 -0
  116. package/scripts/prepare-agent-browser-wrapper.sh +70 -0
  117. package/scripts/prepare-browser-use-wrapper.sh +255 -0
  118. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +0 -228
  119. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +0 -1
  120. package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +0 -43
  121. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  122. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.1.vsix +0 -0
@@ -6,10 +6,10 @@ export class ChatSessionQueryService {
6
6
  constructor(store) {
7
7
  this.store = store;
8
8
  }
9
- upsertSession(session, status = "idle") {
9
+ upsertSession(session, status = "idle", lastActivityAt = session.updatedAt) {
10
10
  const roomId = chatRoomIdFromMetadata(session.metadata) ?? "room_default";
11
- this.store.sessions.upsertSession({ session, roomId, status, lastActivityAt: session.updatedAt });
12
- this.upsertNavigation(session, roomId, status, session.updatedAt);
11
+ this.store.sessions.upsertSession({ session, roomId, status, lastActivityAt });
12
+ this.upsertNavigation(session, roomId, status, lastActivityAt);
13
13
  }
14
14
  upsertSessionsIfChanged(sessions) {
15
15
  let written = 0;
@@ -24,11 +24,32 @@ export class ChatSessionQueryService {
24
24
  }
25
25
  return { checked: sessions.length, written, skipped };
26
26
  }
27
- recordEvent(event, session) {
28
- if (session)
29
- this.upsertSession(session, statusFromOutputEvent(event));
27
+ recordEvent(event, session, _streamId, createdAt = new Date().toISOString()) {
28
+ if (session) {
29
+ const status = statusFromOutputEvent(event);
30
+ if (status)
31
+ this.upsertSession(session, status, createdAt);
32
+ else
33
+ this.touchSession(session, createdAt);
34
+ }
30
35
  return undefined;
31
36
  }
37
+ touchSession(session, lastActivityAt) {
38
+ const roomId = chatRoomIdFromMetadata(session.metadata) ?? "room_default";
39
+ this.store.sessions.upsertSession({ session, roomId, lastActivityAt });
40
+ this.store.navigation.upsertSession({
41
+ roomId,
42
+ sessionId: session.id,
43
+ rootSessionId: rootSessionId(session),
44
+ parentId: session.parentId,
45
+ originId: session.originId,
46
+ title: session.title || "Untitled Session",
47
+ profile: session.profile,
48
+ lastActivityAt,
49
+ sortKey: lastActivityAt,
50
+ updatedAt: lastActivityAt,
51
+ });
52
+ }
32
53
  listSessions() {
33
54
  const rows = this.store.db.prepare(`
34
55
  SELECT s.*, b.runtime_instance_id, b.runtime_adapter_id, b.native_session_id, b.binding_state
@@ -121,12 +142,14 @@ export class ChatSessionQueryService {
121
142
  && row.status === status
122
143
  && row.metadata_json === metadataJson
123
144
  && row.created_at === session.createdAt
124
- && row.updated_at === session.updatedAt
125
- && row.last_activity_at === session.updatedAt
145
+ && row.updated_at >= session.updatedAt
146
+ && row.last_activity_at >= session.updatedAt
126
147
  && row.navigation_status === status
127
148
  && row.navigation_title === title
128
- && row.navigation_sort_key === session.updatedAt
129
- && row.navigation_updated_at === session.updatedAt;
149
+ && row.navigation_sort_key !== null
150
+ && row.navigation_sort_key >= session.updatedAt
151
+ && row.navigation_updated_at !== null
152
+ && row.navigation_updated_at >= session.updatedAt;
130
153
  }
131
154
  upsertNavigation(session, roomId, status, now) {
132
155
  this.store.navigation.upsertSession({
@@ -99,43 +99,40 @@ export class ChatTimelineQueryService {
99
99
  `).all(...values, TRACE_RECONCILIATION_ENTRY_CAP + 1);
100
100
  if (rows.length > TRACE_RECONCILIATION_ENTRY_CAP)
101
101
  return false;
102
- const lifecycles = new Map();
103
- const invocations = [];
104
- for (const row of rows) {
105
- const lifecycle = lifecycles.get(row.event_id) ?? { nextOrdinal: 0 };
106
- lifecycles.set(row.event_id, lifecycle);
107
- if (row.type === "tool_execution_started") {
108
- if (lifecycle.active)
102
+ if (nodeIdentity.qualifier) {
103
+ let hasPersistedOrdinal = false;
104
+ const explicitlyAttached = rows.some((row) => {
105
+ let attributes;
106
+ try {
107
+ const parsed = JSON.parse(row.attributes_json);
108
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
109
+ return false;
110
+ attributes = parsed;
111
+ }
112
+ catch {
109
113
  return false;
110
- lifecycle.active = {
111
- eventId: row.event_id,
112
- invocationOrdinal: lifecycle.nextOrdinal,
113
- payloadMatches: false,
114
- };
115
- lifecycle.nextOrdinal += 1;
116
- invocations.push(lifecycle.active);
117
- }
118
- else if (!lifecycle.active) {
119
- lifecycle.active = {
120
- eventId: row.event_id,
121
- invocationOrdinal: lifecycle.nextOrdinal,
122
- payloadMatches: false,
123
- };
124
- lifecycle.nextOrdinal += 1;
125
- invocations.push(lifecycle.active);
126
- }
127
- if (row.type !== "tool_execution_started"
128
- && payloadMatchesEventRow(this.store, payload.sha256, input.payloadId, row))
129
- lifecycle.active.payloadMatches = true;
130
- if (row.type === "tool_execution_finished")
131
- lifecycle.active = undefined;
132
- }
133
- if (!nodeIdentity.qualifier) {
134
- return invocations.length === 1 && invocations[0].payloadMatches;
114
+ }
115
+ const ordinal = attributes.toolInvocationOrdinal;
116
+ if (Number.isSafeInteger(ordinal))
117
+ hasPersistedOrdinal = true;
118
+ return Number.isSafeInteger(ordinal)
119
+ && Number(ordinal) === nodeIdentity.qualifier.invocationOrdinal
120
+ && row.event_id === nodeIdentity.qualifier.eventId
121
+ && row.type !== "tool_execution_started"
122
+ && payloadMatchesEventRow(this.store, payload.sha256, input.payloadId, row);
123
+ });
124
+ if (explicitlyAttached || hasPersistedOrdinal)
125
+ return explicitlyAttached;
126
+ if (nodeIdentity.qualifier.invocationOrdinal !== 0)
127
+ return false;
128
+ const inferred = inferLegacyToolInvocations(this.store, rows, payload.sha256, input.payloadId);
129
+ return !inferred.ambiguous
130
+ && inferred.invocations.length === 1
131
+ && inferred.invocations[0].eventId === nodeIdentity.qualifier.eventId
132
+ && inferred.invocations[0].payloadMatches;
135
133
  }
136
- const exact = invocations.filter((invocation) => invocation.eventId === nodeIdentity.qualifier.eventId
137
- && invocation.invocationOrdinal === nodeIdentity.qualifier.invocationOrdinal);
138
- return exact.length === 1 && exact[0].payloadMatches;
134
+ const inferred = inferLegacyToolInvocations(this.store, rows, payload.sha256, input.payloadId);
135
+ return !inferred.ambiguous && inferred.invocations.length === 1 && inferred.invocations[0].payloadMatches;
139
136
  }
140
137
  countEventsByType(input = {}) {
141
138
  const clauses = [];
@@ -199,3 +196,31 @@ function payloadMatchesEventRow(store, payloadSha256, payloadId, row) {
199
196
  return false;
200
197
  return store.payloads.findBySha256(inlineSha256)?.id === payloadId;
201
198
  }
199
+ function inferLegacyToolInvocations(store, rows, payloadSha256, payloadId) {
200
+ const activeByEvent = new Map();
201
+ const invocations = [];
202
+ let ambiguous = false;
203
+ for (const row of rows) {
204
+ let active = activeByEvent.get(row.event_id);
205
+ if (row.type === "tool_execution_started") {
206
+ if (active) {
207
+ ambiguous = true;
208
+ continue;
209
+ }
210
+ active = { eventId: row.event_id, payloadMatches: false };
211
+ activeByEvent.set(row.event_id, active);
212
+ invocations.push(active);
213
+ }
214
+ else if (!active) {
215
+ active = { eventId: row.event_id, payloadMatches: false };
216
+ activeByEvent.set(row.event_id, active);
217
+ invocations.push(active);
218
+ }
219
+ if (row.type !== "tool_execution_started" && payloadMatchesEventRow(store, payloadSha256, payloadId, row)) {
220
+ active.payloadMatches = true;
221
+ }
222
+ if (row.type === "tool_execution_finished")
223
+ activeByEvent.delete(row.event_id);
224
+ }
225
+ return { ambiguous, invocations };
226
+ }
@@ -2,7 +2,7 @@ import { PiboWebHttpError, readJsonBody, responseJson } from '../../web/http.js'
2
2
  import { getPiboLoopService } from '../../loops/channel.js';
3
3
  import { listLoopJobTemplates } from '../../loops/templates.js';
4
4
  import { isPiboThinkingLevel } from '../../core/thinking.js';
5
- import { normalizeLoopStopPolicy } from '../../loops/store.js';
5
+ import { normalizeLoopStopPolicy, PiboLoopActiveRunModeChangeError } from '../../loops/store.js';
6
6
  import { isPiboRoomArchived } from './types/rooms.js';
7
7
  const CHAT_WEB_API_PREFIX = '/api/chat';
8
8
  function requireSameOriginJsonRequest(request) { const contentType = request.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase(); if (contentType !== 'application/json')
@@ -184,7 +184,15 @@ export async function handleChatLoopApiRequest(options) {
184
184
  throw new PiboWebHttpError('Loop job not found', 404);
185
185
  if ((patch.mode ?? existing.mode) === 'ralph' && ((patch.tokenBudget !== undefined && patch.tokenBudget !== null) || (patch.tokenReserve !== undefined && patch.tokenReserve !== null)))
186
186
  throw new PiboWebHttpError('tokenBudget and tokenReserve are only available for goal mode', 400);
187
- const job = loopStore.updateJob(resource.id, patch);
187
+ let job;
188
+ try {
189
+ job = loopStore.updateJob(resource.id, patch);
190
+ }
191
+ catch (error) {
192
+ if (error instanceof PiboLoopActiveRunModeChangeError)
193
+ throw new PiboWebHttpError(error.message, 409);
194
+ throw error;
195
+ }
188
196
  if (!job)
189
197
  throw new PiboWebHttpError('Loop job not found', 404);
190
198
  return responseJson({ job: serializeJob(job) });
@@ -1,62 +1,90 @@
1
1
  import { assistantOutputKey, isLiveOnlyOutputEvent, thinkingOutputKey, toolOutputKey } from "./output-event-policy.js";
2
+ const MAX_TRACKED_COMPACTOR_SESSIONS = 1_024;
2
3
  export class OutputCompactor {
3
4
  assistantBuffers = new Map();
4
5
  thinkingBuffers = new Map();
5
6
  toolSnapshots = new Map();
7
+ sessionRecency = new Map();
8
+ sessionBufferCounts = new Map();
6
9
  compact(event) {
10
+ const prepared = this.prepare(event);
11
+ prepared.ack();
12
+ return prepared;
13
+ }
14
+ prepare(event) {
7
15
  const persistedEvents = [];
8
16
  const snapshots = [];
17
+ let liveEvents = [event];
18
+ const mutations = [];
9
19
  switch (event.type) {
10
20
  case "assistant_delta": {
11
21
  const key = assistantOutputKey(event);
12
22
  const previous = this.assistantBuffers.get(key);
13
- const text = `${previous?.text ?? ""}${event.text}`;
14
- this.assistantBuffers.set(key, { event, text });
15
- snapshots.push({ ...event, text });
23
+ const next = { event, text: `${previous?.text ?? ""}${event.text}` };
24
+ mutations.push(() => this.setBuffer(this.assistantBuffers, key, next, event.piboSessionId));
25
+ snapshots.push({ ...event, text: next.text });
16
26
  break;
17
27
  }
18
28
  case "assistant_message": {
19
29
  const key = assistantOutputKey(event);
20
30
  const buffered = this.assistantBuffers.get(key);
21
- this.assistantBuffers.delete(key);
22
- persistedEvents.push({ ...event, text: event.text || buffered?.text || "" });
31
+ const canonical = { ...event, text: event.text || buffered?.text || "" };
32
+ mutations.push(() => {
33
+ this.deleteBuffer(this.assistantBuffers, key, buffered, event.piboSessionId);
34
+ });
35
+ persistedEvents.push(canonical);
36
+ liveEvents = [canonical];
23
37
  break;
24
38
  }
25
39
  case "thinking_started": {
26
40
  const key = thinkingOutputKey(event);
27
- this.thinkingBuffers.set(key, { base: event, text: "" });
41
+ const next = { base: event, text: "" };
42
+ mutations.push(() => {
43
+ if (!this.thinkingBuffers.has(key))
44
+ this.setBuffer(this.thinkingBuffers, key, next, event.piboSessionId);
45
+ });
28
46
  persistedEvents.push(event);
29
47
  break;
30
48
  }
31
49
  case "thinking_delta": {
32
50
  const key = thinkingOutputKey(event);
33
51
  const previous = this.thinkingBuffers.get(key);
34
- const text = `${previous?.text ?? ""}${event.text}`;
35
- this.thinkingBuffers.set(key, { base: previous?.base ?? event, text });
36
- snapshots.push({ ...event, text });
52
+ const next = { base: previous?.base ?? event, text: `${previous?.text ?? ""}${event.text}` };
53
+ mutations.push(() => this.setBuffer(this.thinkingBuffers, key, next, event.piboSessionId));
54
+ snapshots.push({ ...event, text: next.text });
37
55
  break;
38
56
  }
39
57
  case "thinking_finished": {
40
58
  const key = thinkingOutputKey(event);
41
59
  const buffered = this.thinkingBuffers.get(key);
42
- this.thinkingBuffers.delete(key);
43
- persistedEvents.push({ ...event, text: event.text || buffered?.text || "" });
60
+ const canonical = { ...event, text: event.text || buffered?.text || "" };
61
+ mutations.push(() => {
62
+ this.deleteBuffer(this.thinkingBuffers, key, buffered, event.piboSessionId);
63
+ });
64
+ persistedEvents.push(canonical);
65
+ liveEvents = [canonical];
44
66
  break;
45
67
  }
46
68
  case "tool_execution_updated": {
47
- this.toolSnapshots.set(toolOutputKey(event), event);
69
+ const key = toolOutputKey(event);
70
+ mutations.push(() => this.setBuffer(this.toolSnapshots, key, event, event.piboSessionId));
48
71
  snapshots.push(event);
49
72
  break;
50
73
  }
51
74
  case "tool_execution_finished": {
52
- this.toolSnapshots.delete(toolOutputKey(event));
75
+ const key = toolOutputKey(event);
76
+ const previous = this.toolSnapshots.get(key);
77
+ mutations.push(() => {
78
+ this.deleteBuffer(this.toolSnapshots, key, previous, event.piboSessionId);
79
+ });
53
80
  persistedEvents.push(event);
54
81
  break;
55
82
  }
56
83
  case "message_finished":
57
84
  case "session_error": {
58
- persistedEvents.push(...this.flushForBoundary(event));
59
- persistedEvents.push(event);
85
+ const flushed = this.prepareBoundaryFlush(event, mutations);
86
+ persistedEvents.push(...flushed, event);
87
+ liveEvents = [...flushed, event];
60
88
  break;
61
89
  }
62
90
  default:
@@ -64,7 +92,23 @@ export class OutputCompactor {
64
92
  persistedEvents.push(event);
65
93
  break;
66
94
  }
67
- return { liveEvents: [event], persistedEvents, snapshots };
95
+ let settled = false;
96
+ return {
97
+ liveEvents,
98
+ persistedEvents,
99
+ snapshots,
100
+ ack: () => {
101
+ if (settled)
102
+ return;
103
+ settled = true;
104
+ for (const mutate of mutations)
105
+ mutate();
106
+ this.touchSession(event.piboSessionId);
107
+ },
108
+ rollback: () => {
109
+ settled = true;
110
+ },
111
+ };
68
112
  }
69
113
  snapshotsForSession(piboSessionId) {
70
114
  const snapshots = [];
@@ -86,29 +130,68 @@ export class OutputCompactor {
86
130
  }
87
131
  return snapshots;
88
132
  }
89
- flushForBoundary(event) {
133
+ disposeSession(piboSessionId) {
134
+ for (const [key, buffer] of this.assistantBuffers) {
135
+ if (buffer.event.piboSessionId === piboSessionId)
136
+ this.deleteBuffer(this.assistantBuffers, key, buffer, piboSessionId);
137
+ }
138
+ for (const [key, buffer] of this.thinkingBuffers) {
139
+ if (buffer.base.piboSessionId === piboSessionId)
140
+ this.deleteBuffer(this.thinkingBuffers, key, buffer, piboSessionId);
141
+ }
142
+ for (const [key, event] of this.toolSnapshots) {
143
+ if (event.piboSessionId === piboSessionId)
144
+ this.deleteBuffer(this.toolSnapshots, key, event, piboSessionId);
145
+ }
146
+ this.sessionBufferCounts.delete(piboSessionId);
147
+ this.sessionRecency.delete(piboSessionId);
148
+ }
149
+ disposeAll() {
150
+ this.assistantBuffers.clear();
151
+ this.thinkingBuffers.clear();
152
+ this.toolSnapshots.clear();
153
+ this.sessionRecency.clear();
154
+ this.sessionBufferCounts.clear();
155
+ }
156
+ debugState() {
157
+ return {
158
+ sessionCount: this.sessionRecency.size,
159
+ completedSessionCount: [...this.sessionRecency.keys()].filter((sessionId) => !this.sessionHasBuffers(sessionId)).length,
160
+ sessions: [...this.sessionRecency.keys()],
161
+ assistantBufferCount: this.assistantBuffers.size,
162
+ thinkingBufferCount: this.thinkingBuffers.size,
163
+ toolSnapshotCount: this.toolSnapshots.size,
164
+ };
165
+ }
166
+ prepareBoundaryFlush(event, mutations) {
90
167
  const flushed = [];
91
- for (const [key, buffer] of Array.from(this.assistantBuffers.entries())) {
168
+ for (const [key, buffer] of this.assistantBuffers) {
92
169
  if (!matchesBoundary(buffer.event, event))
93
170
  continue;
94
- this.assistantBuffers.delete(key);
171
+ mutations.push(() => {
172
+ this.deleteBuffer(this.assistantBuffers, key, buffer, buffer.event.piboSessionId);
173
+ });
95
174
  flushed.push({
96
175
  type: "assistant_message",
97
176
  piboSessionId: buffer.event.piboSessionId,
98
177
  eventId: buffer.event.eventId,
178
+ renderSequence: buffer.event.renderSequence,
99
179
  assistantIndex: buffer.event.assistantIndex,
100
180
  contentIndex: buffer.event.contentIndex,
101
181
  text: buffer.text,
102
182
  });
103
183
  }
104
- for (const [key, buffer] of Array.from(this.thinkingBuffers.entries())) {
184
+ for (const [key, buffer] of this.thinkingBuffers) {
105
185
  if (!matchesBoundary(buffer.base, event))
106
186
  continue;
107
- this.thinkingBuffers.delete(key);
187
+ mutations.push(() => {
188
+ this.deleteBuffer(this.thinkingBuffers, key, buffer, buffer.base.piboSessionId);
189
+ });
108
190
  flushed.push({
109
191
  type: "thinking_finished",
110
192
  piboSessionId: buffer.base.piboSessionId,
111
193
  eventId: buffer.base.eventId,
194
+ renderSequence: buffer.base.renderSequence,
112
195
  thinkingIndex: "thinkingIndex" in buffer.base ? buffer.base.thinkingIndex : undefined,
113
196
  contentIndex: buffer.base.contentIndex,
114
197
  text: buffer.text,
@@ -116,6 +199,46 @@ export class OutputCompactor {
116
199
  }
117
200
  return flushed;
118
201
  }
202
+ touchSession(piboSessionId) {
203
+ this.sessionRecency.delete(piboSessionId);
204
+ this.sessionRecency.set(piboSessionId, true);
205
+ let completedCount = [...this.sessionRecency.keys()].filter((sessionId) => !this.sessionHasBuffers(sessionId)).length;
206
+ while (completedCount > MAX_TRACKED_COMPACTOR_SESSIONS) {
207
+ let removed = false;
208
+ for (const sessionId of this.sessionRecency.keys()) {
209
+ if (this.sessionHasBuffers(sessionId))
210
+ continue;
211
+ this.sessionRecency.delete(sessionId);
212
+ completedCount -= 1;
213
+ removed = true;
214
+ break;
215
+ }
216
+ if (!removed)
217
+ break;
218
+ }
219
+ }
220
+ sessionHasBuffers(piboSessionId) {
221
+ return (this.sessionBufferCounts.get(piboSessionId) ?? 0) > 0;
222
+ }
223
+ setBuffer(map, key, value, piboSessionId) {
224
+ if (!map.has(key))
225
+ this.sessionBufferCounts.set(piboSessionId, (this.sessionBufferCounts.get(piboSessionId) ?? 0) + 1);
226
+ setLatest(map, key, value);
227
+ }
228
+ deleteBuffer(map, key, expected, piboSessionId) {
229
+ if (expected === undefined || map.get(key) !== expected)
230
+ return;
231
+ map.delete(key);
232
+ const next = (this.sessionBufferCounts.get(piboSessionId) ?? 1) - 1;
233
+ if (next > 0)
234
+ this.sessionBufferCounts.set(piboSessionId, next);
235
+ else
236
+ this.sessionBufferCounts.delete(piboSessionId);
237
+ }
238
+ }
239
+ function setLatest(map, key, value) {
240
+ map.delete(key);
241
+ map.set(key, value);
119
242
  }
120
243
  function matchesBoundary(bufferEvent, boundary) {
121
244
  if (bufferEvent.piboSessionId !== boundary.piboSessionId)
@@ -2,7 +2,184 @@ export function isLiveOnlyOutputEvent(event) {
2
2
  return event.type === "assistant_delta" || event.type === "thinking_delta" || event.type === "tool_execution_updated";
3
3
  }
4
4
  export function isPersistableOutputEvent(event) {
5
- return !isLiveOnlyOutputEvent(event);
5
+ return isPiboOutputEvent(event) && !isLiveOnlyOutputEvent(event);
6
+ }
7
+ export function isPiboOutputEvent(value) {
8
+ if (!isRecord(value) || typeof value.type !== "string" || typeof value.piboSessionId !== "string")
9
+ return false;
10
+ if (value.eventId !== undefined && typeof value.eventId !== "string")
11
+ return false;
12
+ if (value.renderSequence !== undefined && !isPositiveSafeInteger(value.renderSequence))
13
+ return false;
14
+ if (value.toolInvocationOrdinal !== undefined && !isNonNegativeSafeInteger(value.toolInvocationOrdinal))
15
+ return false;
16
+ if (value.provenance !== undefined && !isMessageProvenance(value.provenance))
17
+ return false;
18
+ switch (value.type) {
19
+ case "message_queued":
20
+ return typeof value.text === "string"
21
+ && isNonNegativeSafeInteger(value.queuedMessages)
22
+ && isOptionalEventSource(value.source);
23
+ case "message_steered":
24
+ return typeof value.text === "string"
25
+ && isOptionalEventSource(value.source)
26
+ && isOptionalString(value.activeEventId);
27
+ case "message_started":
28
+ return typeof value.text === "string" && isOptionalEventSource(value.source);
29
+ case "assistant_delta":
30
+ case "assistant_message":
31
+ return typeof value.text === "string"
32
+ && isOptionalNonNegativeSafeInteger(value.assistantIndex)
33
+ && isOptionalNonNegativeSafeInteger(value.contentIndex);
34
+ case "thinking_delta":
35
+ return typeof value.text === "string"
36
+ && isOptionalNonNegativeSafeInteger(value.thinkingIndex)
37
+ && isOptionalNonNegativeSafeInteger(value.contentIndex);
38
+ case "message_finished":
39
+ return isOptionalEventSource(value.source);
40
+ case "thinking_started":
41
+ return isOptionalNonNegativeSafeInteger(value.thinkingIndex) && isOptionalNonNegativeSafeInteger(value.contentIndex);
42
+ case "thinking_finished":
43
+ return isOptionalString(value.text)
44
+ && isOptionalNonNegativeSafeInteger(value.thinkingIndex)
45
+ && isOptionalNonNegativeSafeInteger(value.contentIndex);
46
+ case "tool_call":
47
+ return hasOwn(value, "args")
48
+ && typeof value.toolCallId === "string"
49
+ && typeof value.toolName === "string"
50
+ && typeof value.argsComplete === "boolean"
51
+ && isOptionalString(value.intent);
52
+ case "tool_execution_started":
53
+ return hasOwn(value, "args")
54
+ && typeof value.toolCallId === "string"
55
+ && typeof value.toolName === "string"
56
+ && isOptionalString(value.intent);
57
+ case "tool_execution_updated":
58
+ return hasOwn(value, "args")
59
+ && hasOwn(value, "partialResult")
60
+ && typeof value.toolCallId === "string"
61
+ && typeof value.toolName === "string"
62
+ && isOptionalString(value.intent);
63
+ case "tool_execution_finished":
64
+ return hasOwn(value, "result")
65
+ && typeof value.toolCallId === "string"
66
+ && typeof value.toolName === "string"
67
+ && typeof value.isError === "boolean"
68
+ && isOptionalString(value.intent);
69
+ case "subagent_session":
70
+ return typeof value.toolName === "string"
71
+ && typeof value.subagentName === "string"
72
+ && typeof value.childPiboSessionId === "string"
73
+ && isOptionalString(value.requestId)
74
+ && isOptionalString(value.toolCallId)
75
+ && isOptionalString(value.threadKey);
76
+ case "assistant_usage":
77
+ return isFiniteNumber(value.totalTokens)
78
+ && isOptionalNonNegativeSafeInteger(value.usageIndex)
79
+ && ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens", "reasoningTokens"].every((key) => isOptionalFiniteNumber(value[key]))
80
+ && isOptionalFiniteNumber(value.costUsd);
81
+ case "compaction_start":
82
+ return typeof value.reason === "string" && isOptionalNonNegativeSafeInteger(value.compactionIndex);
83
+ case "compaction_end":
84
+ return typeof value.reason === "string"
85
+ && typeof value.aborted === "boolean"
86
+ && isOptionalNonNegativeSafeInteger(value.compactionIndex)
87
+ && isOptionalString(value.errorMessage);
88
+ case "approval_requested":
89
+ return isApprovalRequest(value.request);
90
+ case "approval_resolved":
91
+ case "user_input_resolved":
92
+ return typeof value.requestId === "string" && isRuntimeRequestResolution(value.resolution);
93
+ case "user_input_requested":
94
+ return isUserInputRequest(value.request);
95
+ case "execution_result":
96
+ return typeof value.action === "string" && hasOwn(value, "result");
97
+ case "session_error":
98
+ return typeof value.error === "string" && (value.errorDetails === undefined || isRecord(value.errorDetails));
99
+ case "pi_event":
100
+ return hasOwn(value, "event");
101
+ default:
102
+ return false;
103
+ }
104
+ }
105
+ function isRecord(value) {
106
+ return !!value && typeof value === "object" && !Array.isArray(value);
107
+ }
108
+ function hasOwn(value, key) {
109
+ return Object.prototype.hasOwnProperty.call(value, key);
110
+ }
111
+ function isFiniteNumber(value) {
112
+ return typeof value === "number" && Number.isFinite(value);
113
+ }
114
+ function isOptionalFiniteNumber(value) {
115
+ return value === undefined || isFiniteNumber(value);
116
+ }
117
+ function isOptionalString(value) {
118
+ return value === undefined || typeof value === "string";
119
+ }
120
+ function isOptionalNonNegativeSafeInteger(value) {
121
+ return value === undefined || isNonNegativeSafeInteger(value);
122
+ }
123
+ function isPositiveSafeInteger(value) {
124
+ return Number.isSafeInteger(value) && value > 0;
125
+ }
126
+ function isNonNegativeSafeInteger(value) {
127
+ return Number.isSafeInteger(value) && value >= 0;
128
+ }
129
+ function isRuntimeRequestResolution(value) {
130
+ return value === "responded" || value === "cleared" || value === "aborted" || value === "expired";
131
+ }
132
+ function isOptionalEventSource(value) {
133
+ return value === undefined || value === "user" || value === "ui" || value === "service" || value === "actor";
134
+ }
135
+ function isMessageProvenance(value) {
136
+ if (!isRecord(value) || typeof value.kind !== "string")
137
+ return false;
138
+ if (value.kind === "loop-run") {
139
+ return typeof value.jobId === "string"
140
+ && typeof value.runId === "string"
141
+ && (value.cause === undefined || value.cause === "run-reminder")
142
+ && isOptionalString(value.rootEventId);
143
+ }
144
+ if (value.kind === "subagent-request") {
145
+ return typeof value.requestId === "string"
146
+ && typeof value.controllerPiboSessionId === "string"
147
+ && isOptionalString(value.loopJobId)
148
+ && isOptionalString(value.loopRunId);
149
+ }
150
+ return false;
151
+ }
152
+ function isApprovalRequest(value) {
153
+ return isRecord(value)
154
+ && typeof value.requestId === "string"
155
+ && typeof value.requestType === "string"
156
+ && isOptionalString(value.title)
157
+ && isOptionalString(value.detail)
158
+ && (value.decisions === undefined || (Array.isArray(value.decisions)
159
+ && value.decisions.every((decision) => isRecord(decision)
160
+ && typeof decision.id === "string"
161
+ && typeof decision.label === "string"
162
+ && isOptionalString(decision.description))));
163
+ }
164
+ function isUserInputRequest(value) {
165
+ return isRecord(value)
166
+ && typeof value.requestId === "string"
167
+ && Array.isArray(value.questions)
168
+ && isOptionalBoolean(value.blocking)
169
+ && value.questions.every((question) => isRecord(question)
170
+ && typeof question.id === "string"
171
+ && typeof question.question === "string"
172
+ && isOptionalString(question.header)
173
+ && isOptionalBoolean(question.multiSelect)
174
+ && isOptionalBoolean(question.allowFreeform)
175
+ && isOptionalBoolean(question.secret)
176
+ && (question.options === undefined || (Array.isArray(question.options)
177
+ && question.options.every((option) => isRecord(option)
178
+ && typeof option.label === "string"
179
+ && isOptionalString(option.description)))));
180
+ }
181
+ function isOptionalBoolean(value) {
182
+ return value === undefined || typeof value === "boolean";
6
183
  }
7
184
  export function assistantOutputKey(event) {
8
185
  const partIndex = event.assistantIndex ?? event.contentIndex ?? 0;
@@ -13,5 +190,5 @@ export function thinkingOutputKey(event) {
13
190
  return [event.piboSessionId, event.eventId ?? "", partIndex].join(":");
14
191
  }
15
192
  export function toolOutputKey(event) {
16
- return [event.piboSessionId, event.eventId ?? "", event.toolCallId].join(":");
193
+ return [event.piboSessionId, event.eventId ?? "", event.toolCallId, event.toolInvocationOrdinal ?? 0].join(":");
17
194
  }