@pasko70/pibo 3.4.1 → 3.4.3

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 (42) hide show
  1. package/dist/agent-runtime/routed-session.js +6 -1
  2. package/dist/agent-runtimes/pi/adapter.js +4 -1
  3. package/dist/agent-runtimes/pi/history.js +55 -1
  4. package/dist/apps/chat/chat-settings-routes.js +10 -0
  5. package/dist/apps/chat/data/chat-data-mappers.js +1 -1
  6. package/dist/apps/chat/stream.js +1 -1
  7. package/dist/apps/chat/trace-v2.js +1 -0
  8. package/dist/apps/chat/web-app.js +2 -1
  9. package/dist/apps/chat-ui/assets/{dist-DnZL1rRp.js → dist-B4YhOxh0.js} +1 -1
  10. package/dist/apps/chat-ui/assets/{dist-D0md0xIR.js → dist-BkUu8WPA.js} +1 -1
  11. package/dist/apps/chat-ui/assets/{dist-ERKABhp3.js → dist-C8GzUMPk.js} +1 -1
  12. package/dist/apps/chat-ui/assets/{dist-DHnaUMlq.js → dist-Cyb4rVa6.js} +1 -1
  13. package/dist/apps/chat-ui/assets/{dist-Abz605MV.js → dist-DeMKnZR8.js} +1 -1
  14. package/dist/apps/chat-ui/assets/index-BOceJ0jM.css +1 -0
  15. package/dist/apps/chat-ui/assets/index-Dk4mbXAB.js +228 -0
  16. package/dist/apps/chat-ui/index.html +2 -2
  17. package/dist/apps/chat-vscode-web/assets/index-0oTGFHni.js +43 -0
  18. package/dist/apps/chat-vscode-web/index.html +1 -1
  19. package/dist/auth/openai-codex-usage.js +34 -5
  20. package/dist/core/session-router.js +95 -11
  21. package/dist/core/user-settings.js +11 -0
  22. package/dist/data/ingest-service.js +1 -1
  23. package/dist/data/schema.js +10 -1
  24. package/dist/debug/agents.js +5 -3
  25. package/dist/gateway/server.js +1 -1
  26. package/dist/session-ui/terminalRows.js +3 -1
  27. package/dist/sessions/pibo-data-store.js +38 -1
  28. package/dist/sessions/store.js +38 -0
  29. package/dist/shared/tool-call-metrics.js +138 -0
  30. package/dist/shared/tool-call-token-settings.js +45 -0
  31. package/dist/shared/trace-event-projection.js +2 -0
  32. package/dist/shared/trace-live-reducer.js +1 -0
  33. package/dist/shared/trace-patch-nodes.js +4 -0
  34. package/dist/subagents/context.js +2 -2
  35. package/dist/subagents/observation-query.js +28 -1
  36. package/dist/subagents/observations.js +8 -0
  37. package/dist/subagents/tool.js +12 -8
  38. package/npm-shrinkwrap.json +22 -13
  39. package/package.json +2 -1
  40. package/dist/apps/chat-ui/assets/index-BWVPNFjU.js +0 -228
  41. package/dist/apps/chat-ui/assets/index-CywOD6EF.css +0 -1
  42. package/dist/apps/chat-vscode-web/assets/index-CZmxeSn3.js +0 -43
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <meta name="theme-color" content="#101d22" />
7
7
  <title>Pibo</title>
8
- <script type="module" crossorigin src="/apps/chat-vscode/assets/index-CZmxeSn3.js"></script>
8
+ <script type="module" crossorigin src="/apps/chat-vscode/assets/index-0oTGFHni.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/apps/chat-vscode/assets/index-b18ZkEo0.css">
10
10
  </head>
11
11
  <body>
@@ -1,7 +1,13 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { readPiCredential, resolvePiProviderAuth, } from "../agent-runtimes/pi/credentials.js";
2
3
  const OPENAI_CODEX_PROVIDER = "openai-codex";
3
4
  const OPENAI_CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
4
5
  const OPENAI_JWT_CLAIM_PATH = "https://api.openai.com/auth";
6
+ const USAGE_REFRESH_MS = 30_000;
7
+ const USAGE_MAX_STALE_MS = 5 * 60_000;
8
+ const USAGE_REQUEST_TIMEOUT_MS = 2_000;
9
+ // One bounded, credential-scoped snapshot shared by sessions. Never retain raw credentials.
10
+ let usageCache;
5
11
  function isRecord(value) {
6
12
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
7
13
  }
@@ -125,8 +131,31 @@ export async function getOpenAiCodexProviderUsageForActiveModel(activeModel) {
125
131
  if (activeModel?.provider !== OPENAI_CODEX_PROVIDER)
126
132
  return undefined;
127
133
  const credential = await readPiCredential(OPENAI_CODEX_PROVIDER);
128
- if (credential?.type !== "oauth")
134
+ if (credential?.type !== "oauth") {
135
+ usageCache = undefined;
129
136
  return undefined;
137
+ }
138
+ const key = createHash("sha256").update(JSON.stringify(credential)).digest("hex");
139
+ if (usageCache?.key !== key)
140
+ usageCache = { key, refreshAfter: 0 };
141
+ const cache = usageCache;
142
+ const now = Date.now();
143
+ const usable = () => cache.value && Date.now() - Date.parse(cache.value.fetchedAt) < USAGE_MAX_STALE_MS
144
+ ? cache.value
145
+ : undefined;
146
+ if (now < cache.refreshAfter)
147
+ return usable();
148
+ cache.pending ??= fetchOpenAiCodexProviderUsage(credential.accountId)
149
+ .then((value) => { cache.value = value; return value; })
150
+ .catch(() => usable())
151
+ .finally(() => {
152
+ cache.refreshAfter = Date.now() + USAGE_REFRESH_MS;
153
+ cache.pending = undefined;
154
+ });
155
+ // Quota is advisory: stale-while-revalidate keeps repeat status commands local.
156
+ return usable() ?? await cache.pending;
157
+ }
158
+ async function fetchOpenAiCodexProviderUsage(storedAccountId) {
130
159
  const resolvedAuth = await resolvePiProviderAuth(OPENAI_CODEX_PROVIDER);
131
160
  const accessToken = resolvedAuth?.auth.apiKey;
132
161
  if (!accessToken)
@@ -135,13 +164,13 @@ export async function getOpenAiCodexProviderUsageForActiveModel(activeModel) {
135
164
  Authorization: `Bearer ${accessToken}`,
136
165
  "User-Agent": "codex-cli",
137
166
  };
138
- const accountId = getOpenAiAccountId(accessToken, credential.accountId);
167
+ const accountId = getOpenAiAccountId(accessToken, storedAccountId);
139
168
  if (accountId)
140
169
  headers["ChatGPT-Account-Id"] = accountId;
141
- const response = await fetch(OPENAI_CODEX_USAGE_URL, { headers });
170
+ const response = await fetch(OPENAI_CODEX_USAGE_URL, { headers, signal: AbortSignal.timeout(USAGE_REQUEST_TIMEOUT_MS) });
142
171
  if (!response.ok) {
143
- const text = await response.text().catch(() => "");
144
- throw new Error(`OpenAI Codex usage request failed: ${response.status}${text ? ` ${text}` : ""}`);
172
+ await response.body?.cancel();
173
+ throw new Error(`OpenAI Codex usage request failed: ${response.status}`);
145
174
  }
146
175
  return normalizeUsagePayload(await response.json());
147
176
  }
@@ -7,13 +7,13 @@ import { runtimeSessionErrorDetails } from "./session-errors.js";
7
7
  import { OutputRenderSequencer, outputRenderHighWaterStore } from "./output-render-sequence.js";
8
8
  import { normalizePiboAgentSessionName, } from "../subagents/tool.js";
9
9
  import { piboAgentObservationDetails, piboAgentObservationKind, piboAgentObservationRole, piboAgentObservationSourceFromEvent, piboAgentObservationText, } from "../subagents/observations.js";
10
- import { preparePiboAgentObservationQuery, selectPiboAgentObservationPage, } from "../subagents/observation-query.js";
10
+ import { piboAgentObservationCursorScopeKey, preparePiboAgentObservationQuery, selectPiboAgentObservationPage, } from "../subagents/observation-query.js";
11
11
  import { PiboRunRegistry } from "../runs/registry.js";
12
12
  import { PiboRunCancellationError, PiboRunCancelledError, PiboRunExecutionTimeoutError, waitForRunCancellationSettlement } from "../runs/lifecycle.js";
13
13
  import { PiboRunResourceLimitError } from "../runs/resource-isolation.js";
14
14
  import { createPiboSignalRegistry } from "../signals/registry.js";
15
15
  import { createDefaultPiboReliabilityStore } from "../reliability/store.js";
16
- import { InMemoryPiboSessionStore, createPiboSessionId, } from "../sessions/store.js";
16
+ import { PIBO_AGENT_OBSERVATION_AUTO_CURSOR_MAX_SCOPES, InMemoryPiboSessionStore, createPiboSessionId, } from "../sessions/store.js";
17
17
  import { createAgentRuntimeBindingPersistence } from "../sessions/runtime-binding-persistence.js";
18
18
  import { createLegacyPiRuntimeSessionBinding, RuntimeSessionBindingConflictError, } from "../sessions/runtime-binding.js";
19
19
  import { AgentRuntimeBindingMissingError, AgentRuntimeUnavailableError } from "../agent-runtime/errors.js";
@@ -349,7 +349,9 @@ export class PiboSessionRouter {
349
349
  activeSubagentRequests = new Map();
350
350
  subagentRequestIdsByEvent = new Map();
351
351
  agentObservations = [];
352
+ agentObservationHighWaterByParent = new Map();
352
353
  agentObservationEvictedThroughByParent = new Map();
354
+ agentObservationAutoCursorFallback = new Map();
353
355
  nextAgentObservationSequence = 1;
354
356
  scheduledRunReminders = new Map();
355
357
  runReminderDeliveries = new Map();
@@ -945,16 +947,41 @@ export class PiboSessionRouter {
945
947
  const status = this.sessions.get(piboSessionId)?.getStatus();
946
948
  return status ? this.withPersistedRuntimeBinding(status) : undefined;
947
949
  }
948
- async getSessionStatusSnapshot(piboSessionId) {
949
- const session = await this.getOrCreateSession(piboSessionId);
950
+ async getSessionStatusSnapshot(piboSessionId, options) {
951
+ const session = options?.activate === false ? this.sessions.get(piboSessionId) : await this.getOrCreateSession(piboSessionId);
952
+ if (!session) {
953
+ this.resolvePiboSession(piboSessionId);
954
+ return undefined;
955
+ }
950
956
  try {
951
957
  return this.withPersistedRuntimeBinding(await session.getStatusSnapshot());
952
958
  }
953
959
  finally {
954
- this.scheduleIdleSessionEvictionIfIdle(piboSessionId);
960
+ // Passive header polling must neither create nor indefinitely retain a runtime.
961
+ if (options?.activate !== false)
962
+ this.scheduleIdleSessionEvictionIfIdle(piboSessionId);
955
963
  }
956
964
  }
957
965
  async getSessionForkCandidates(piboSessionId) {
966
+ const canReadPersisted = () => !this.closing
967
+ && !this.quiescingSessions.has(piboSessionId)
968
+ && !this.disposingSessions.has(piboSessionId)
969
+ && !this.sessions.has(piboSessionId)
970
+ && !this.pendingSessions.has(piboSessionId);
971
+ if (canReadPersisted()) {
972
+ const record = this.resolvePiboSession(piboSessionId);
973
+ const binding = this.resolveSessionRuntimeBinding(record);
974
+ const adapter = this.resolveAgentRuntimeRegistry(binding.runtimeInstanceId).requireAgentRuntimeAdapter(binding.runtimeInstanceId);
975
+ if (binding.state === "bound" && adapter.descriptor.id === binding.adapterId
976
+ && adapter.descriptor.capabilities.lifecycle.fork && adapter.readForkCandidates) {
977
+ const workspace = record.workspace ?? this.options.cwd ?? getDefaultPiboWorkspace();
978
+ const candidates = await adapter.readForkCandidates({ binding, workspace });
979
+ const current = this.resolvePiboSession(piboSessionId);
980
+ if (candidates !== undefined && canReadPersisted() && current.workspace === record.workspace
981
+ && runtimeBindingsEqual(binding, this.resolveSessionRuntimeBinding(current)))
982
+ return candidates;
983
+ }
984
+ }
958
985
  const session = await this.getOrCreateSession(piboSessionId);
959
986
  try {
960
987
  return await session.getForkCandidates();
@@ -1146,7 +1173,9 @@ export class PiboSessionRouter {
1146
1173
  this.subagentRequestIdsByEvent.clear();
1147
1174
  this.outputRenderSequencer.disposeAll();
1148
1175
  this.agentObservations.length = 0;
1176
+ this.agentObservationHighWaterByParent.clear();
1149
1177
  this.agentObservationEvictedThroughByParent.clear();
1178
+ this.agentObservationAutoCursorFallback.clear();
1150
1179
  await this.telemetryWriter?.dispose();
1151
1180
  const lifecycleFailures = [...authDisposals, ...webAppDisposals]
1152
1181
  .flatMap((result) => result.status === "rejected" ? [result.reason] : []);
@@ -1523,6 +1552,7 @@ export class PiboSessionRouter {
1523
1552
  return result;
1524
1553
  },
1525
1554
  statusResources,
1555
+ getToolMetricTokenCalculation: () => loadPiboUserSettings().toolMetrics.tokenCalculation,
1526
1556
  });
1527
1557
  this.sessions.set(piboSession.id, session);
1528
1558
  return session;
@@ -2098,8 +2128,18 @@ export class PiboSessionRouter {
2098
2128
  observeManagedAgents(parentPiboSessionId, input) {
2099
2129
  for (const agentId of input.agentIds ?? [])
2100
2130
  this.requireManagedAgent(parentPiboSessionId, agentId);
2101
- const query = preparePiboAgentObservationQuery(input);
2131
+ const baseQuery = preparePiboAgentObservationQuery(input);
2132
+ const cursorScope = piboAgentObservationCursorScopeKey(baseQuery.filters);
2133
+ const explicitAfterSequence = input.afterSequence !== undefined;
2134
+ const savedAfterSequence = baseQuery.cursorMode === "auto" && !explicitAfterSequence
2135
+ ? this.getAgentObservationAutoCursor(parentPiboSessionId, cursorScope)
2136
+ : undefined;
2137
+ const query = savedAfterSequence === undefined
2138
+ ? baseQuery
2139
+ : preparePiboAgentObservationQuery({ ...input, afterSequence: savedAfterSequence });
2102
2140
  const observations = this.agentObservations;
2141
+ const evictedThrough = this.agentObservationEvictedThroughByParent.get(parentPiboSessionId) ?? 0;
2142
+ const sourceHighWater = Math.max(evictedThrough, this.agentObservationHighWaterByParent.get(parentPiboSessionId) ?? 0);
2103
2143
  function* ordered() {
2104
2144
  const start = query.scanOrder === "asc" ? 0 : observations.length - 1;
2105
2145
  const end = query.scanOrder === "asc" ? observations.length : -1;
@@ -2110,7 +2150,48 @@ export class PiboSessionRouter {
2110
2150
  yield observation;
2111
2151
  }
2112
2152
  }
2113
- return selectPiboAgentObservationPage(ordered(), query, { evictedThrough: this.agentObservationEvictedThroughByParent.get(parentPiboSessionId) ?? 0 });
2153
+ const page = selectPiboAgentObservationPage(ordered(), query, { evictedThrough });
2154
+ if (query.cursorMode === "history")
2155
+ return page;
2156
+ const initialSnapshot = !explicitAfterSequence && savedAfterSequence === undefined;
2157
+ const nextAfterSequence = initialSnapshot || !page.truncated
2158
+ ? Math.max(page.nextAfterSequence, sourceHighWater)
2159
+ : page.nextAfterSequence;
2160
+ const advancedAfterSequence = this.advanceAgentObservationAutoCursor(parentPiboSessionId, cursorScope, nextAfterSequence);
2161
+ return {
2162
+ ...page,
2163
+ autoCursorSequence: advancedAfterSequence,
2164
+ };
2165
+ }
2166
+ getAgentObservationAutoCursor(parentPiboSessionId, cursorScope) {
2167
+ if (this.sessionStore.getAgentObservationAutoCursor) {
2168
+ return this.sessionStore.getAgentObservationAutoCursor(parentPiboSessionId, cursorScope);
2169
+ }
2170
+ return this.agentObservationAutoCursorFallback.get(JSON.stringify([parentPiboSessionId, cursorScope]));
2171
+ }
2172
+ advanceAgentObservationAutoCursor(parentPiboSessionId, cursorScope, sequence) {
2173
+ if (this.sessionStore.advanceAgentObservationAutoCursor) {
2174
+ return this.sessionStore.advanceAgentObservationAutoCursor(parentPiboSessionId, cursorScope, sequence);
2175
+ }
2176
+ const key = JSON.stringify([parentPiboSessionId, cursorScope]);
2177
+ const advanced = Math.max(this.agentObservationAutoCursorFallback.get(key) ?? 0, sequence);
2178
+ this.agentObservationAutoCursorFallback.delete(key);
2179
+ this.agentObservationAutoCursorFallback.set(key, advanced);
2180
+ const prefix = `${JSON.stringify([parentPiboSessionId]).slice(0, -1)},`;
2181
+ let scopeCount = 0;
2182
+ for (const existingKey of this.agentObservationAutoCursorFallback.keys()) {
2183
+ if (existingKey.startsWith(prefix))
2184
+ scopeCount += 1;
2185
+ }
2186
+ for (const existingKey of this.agentObservationAutoCursorFallback.keys()) {
2187
+ if (scopeCount <= PIBO_AGENT_OBSERVATION_AUTO_CURSOR_MAX_SCOPES)
2188
+ break;
2189
+ if (!existingKey.startsWith(prefix))
2190
+ continue;
2191
+ this.agentObservationAutoCursorFallback.delete(existingKey);
2192
+ scopeCount -= 1;
2193
+ }
2194
+ return advanced;
2114
2195
  }
2115
2196
  async killManagedAgent(parentPiboSessionId, agentId) {
2116
2197
  const child = this.requireManagedAgent(parentPiboSessionId, agentId);
@@ -2300,16 +2381,16 @@ export class PiboSessionRouter {
2300
2381
  throw new Error(`Subagent "${subagent.name}" exceeded max depth ${maxDepth} from Pibo session "${parentPiboSessionId}"`);
2301
2382
  }
2302
2383
  }
2303
- getSubagentDepth(piboSessionId) {
2384
+ getSubagentDepth(piboSessionId, sessionsById) {
2304
2385
  let depth = 0;
2305
- let current = this.sessionStore.get(piboSessionId);
2386
+ let current = sessionsById ? sessionsById.get(piboSessionId) : this.sessionStore.get(piboSessionId);
2306
2387
  const seen = new Set();
2307
2388
  while (current?.parentId) {
2308
2389
  if (seen.has(current.parentId))
2309
2390
  break;
2310
2391
  seen.add(current.parentId);
2311
2392
  depth += 1;
2312
- current = this.sessionStore.get(current.parentId);
2393
+ current = sessionsById ? sessionsById.get(current.parentId) : this.sessionStore.get(current.parentId);
2313
2394
  }
2314
2395
  return depth;
2315
2396
  }
@@ -2412,6 +2493,7 @@ export class PiboSessionRouter {
2412
2493
  details: piboAgentObservationDetails(event),
2413
2494
  };
2414
2495
  this.agentObservations.push(observation);
2496
+ this.agentObservationHighWaterByParent.set(session.parentId, Math.max(this.agentObservationHighWaterByParent.get(session.parentId) ?? 0, sequence));
2415
2497
  if (this.agentObservations.length > MAX_AGENT_OBSERVATIONS) {
2416
2498
  const evicted = this.agentObservations.splice(0, this.agentObservations.length - MAX_AGENT_OBSERVATIONS);
2417
2499
  for (const item of evicted) {
@@ -2567,7 +2649,9 @@ export class PiboSessionRouter {
2567
2649
  }
2568
2650
  projectKnownSessionSignals() {
2569
2651
  const sessions = this.sessionStore.list?.() ?? [];
2570
- const depthBySessionId = new Map(sessions.map((session) => [session.id, this.getSubagentDepth(session.id)]));
2652
+ // The complete list is already loaded; avoid an additional store query for every ancestor.
2653
+ const sessionsById = new Map(sessions.map((session) => [session.id, session]));
2654
+ const depthBySessionId = new Map(sessions.map((session) => [session.id, this.getSubagentDepth(session.id, sessionsById)]));
2571
2655
  sessions.sort((left, right) => (depthBySessionId.get(left.id) ?? 0) - (depthBySessionId.get(right.id) ?? 0));
2572
2656
  for (const session of sessions) {
2573
2657
  this.signalRegistry.project({ type: "session_created", session });
@@ -4,6 +4,7 @@ import { piboHomePath } from "./pibo-home.js";
4
4
  import { sanitizePreviewServerSettings } from "./preview-server-settings.js";
5
5
  import { sanitizeTelemetryRetentionSettings } from "./telemetry-retention-settings.js";
6
6
  import { sanitizeTelemetryStaleThresholdSettings } from "./telemetry-staleness.js";
7
+ import { DEFAULT_TOOL_METRIC_TOKEN_CALCULATION, parseToolMetricTokenCalculation, } from "../shared/tool-call-token-settings.js";
7
8
  export const DEFAULT_USER_TIMEZONE = "UTC";
8
9
  export const DEFAULT_WEB_ANNOTATIONS_TOGGLE_SHORTCUT = "Alt+Shift+A";
9
10
  export const DEFAULT_TRANSCRIPTION_PROVIDER_ID = "openai-chatgpt";
@@ -64,6 +65,15 @@ export function sanitizeSpeechSettings(value) {
64
65
  : {};
65
66
  return { providerId: sanitizeTranscriptionProviderId(raw.providerId) ?? DEFAULT_SPEECH_PROVIDER_ID };
66
67
  }
68
+ export function sanitizeToolMetricSettings(value) {
69
+ const raw = value && typeof value === "object" && !Array.isArray(value)
70
+ ? value
71
+ : {};
72
+ return {
73
+ tokenCalculation: parseToolMetricTokenCalculation(raw.tokenCalculation)
74
+ ?? DEFAULT_TOOL_METRIC_TOKEN_CALCULATION,
75
+ };
76
+ }
67
77
  export function sanitizeTimezone(value) {
68
78
  if (typeof value !== "string")
69
79
  return undefined;
@@ -87,6 +97,7 @@ function sanitizeUserSettings(value) {
87
97
  shortcuts: sanitizeShortcutSettings(raw.shortcuts),
88
98
  transcription: sanitizeTranscriptionSettings(raw.transcription),
89
99
  speech: sanitizeSpeechSettings(raw.speech),
100
+ toolMetrics: sanitizeToolMetricSettings(raw.toolMetrics),
90
101
  previewServers: sanitizePreviewServerSettings(raw.previewServers),
91
102
  telemetryStaleThresholds: sanitizeTelemetryStaleThresholdSettings(raw.telemetryStaleThresholds),
92
103
  telemetryRetention: sanitizeTelemetryRetentionSettings(raw.telemetryRetention),
@@ -427,7 +427,7 @@ function specificAttributesForOutputEvent(event) {
427
427
  if (event.type === "tool_call")
428
428
  return { toolCallId: event.toolCallId, toolName: event.toolName, argsComplete: event.argsComplete, intent: event.intent };
429
429
  if (event.type === "tool_execution_started" || event.type === "tool_execution_updated" || event.type === "tool_execution_finished")
430
- return { toolCallId: event.toolCallId, toolName: event.toolName, isError: "isError" in event ? event.isError : undefined, intent: event.intent };
430
+ return { toolCallId: event.toolCallId, toolName: event.toolName, isError: "isError" in event ? event.isError : undefined, intent: event.intent, toolMetrics: event.type === "tool_execution_finished" ? event.toolMetrics : undefined };
431
431
  if (event.type === "subagent_session")
432
432
  return { toolCallId: event.toolCallId, toolName: event.toolName, subagentName: event.subagentName, childPiboSessionId: event.childPiboSessionId, threadKey: event.threadKey };
433
433
  if (event.type === "execution_result")
@@ -1,4 +1,4 @@
1
- export const PIBO_DATA_SCHEMA_VERSION = 8;
1
+ export const PIBO_DATA_SCHEMA_VERSION = 9;
2
2
  const NATIVE_HISTORY_FALLBACK_SCHEMA_VERSION = 5;
3
3
  const retiredScopeColumn = ["owner", "scope"].join("_");
4
4
  const payloadTableDefinition = `
@@ -254,6 +254,15 @@ function applyPiboDataSchemaInTransaction(db, hooks, previousVersion, tablesToRe
254
254
  FOREIGN KEY (parent_pibo_session_id) REFERENCES sessions(id) ON DELETE CASCADE
255
255
  );
256
256
 
257
+ CREATE TABLE IF NOT EXISTS session_agent_observation_auto_cursors (
258
+ parent_pibo_session_id TEXT NOT NULL,
259
+ cursor_scope TEXT NOT NULL,
260
+ sequence INTEGER NOT NULL CHECK(sequence >= 0),
261
+ updated_at TEXT NOT NULL,
262
+ PRIMARY KEY (parent_pibo_session_id, cursor_scope),
263
+ FOREIGN KEY (parent_pibo_session_id) REFERENCES sessions(id) ON DELETE CASCADE
264
+ );
265
+
257
266
  CREATE TABLE IF NOT EXISTS session_output_part_counters (
258
267
  pibo_session_id TEXT NOT NULL,
259
268
  event_id TEXT NOT NULL,
@@ -81,7 +81,7 @@ export function inspectDebugAgentObservations(parentPiboSessionId, store, input
81
81
  throw new Error(`Debug store "pibo-data" not found at ${store.path}`);
82
82
  const db = openReadOnlyDebugDatabase(store);
83
83
  try {
84
- const query = preparePiboAgentObservationQuery(input);
84
+ const query = preparePiboAgentObservationQuery({ ...input, cursorMode: "history" });
85
85
  const owned = readOwnedAgents(db, parentPiboSessionId);
86
86
  const ownedById = new Map(owned.map((agent) => [agent.agentId, agent]));
87
87
  for (const agentId of input.agentIds ?? []) {
@@ -287,8 +287,10 @@ Usage:
287
287
  [--order asc|desc] [--limit 1..200] [--include-tools]
288
288
  [--tool-detail summary|full] [--details] [--json]
289
289
 
290
- Default: the newest 20 completed assistant messages, with streaming deltas and tools hidden.
291
- Use --include-tools for compact tool calls and terminal results. Explicit --event-type or --kind
290
+ Default: the newest 20 completed assistant messages; streaming deltas and tools are hidden.
291
+ The CLI is stateless history inspection. Use --after-sequence for explicit incremental polling.
292
+ Use --include-tools only for stalls, errors, or targeted diagnosis; prefer --tool-call-id when known.
293
+ Explicit --event-type or --kind
292
294
  filters retain access to progress events. Repeat plural filters for OR within that field.
293
295
  Different fields combine with AND. With --after-sequence, pages always consume the oldest unseen rows;
294
296
  --order desc reverses only the returned page, so nextAfterSequence remains safe for polling.
@@ -421,7 +421,7 @@ export class PiboGatewayServer {
421
421
  },
422
422
  rebindSessionRuntime: (piboSessionId, input) => this.requireRouter().rebindSessionRuntime(piboSessionId, input),
423
423
  getSessionRuntimeStatus: (piboSessionId) => this.requireRouter().getSessionRuntimeStatus(piboSessionId),
424
- getSessionStatusSnapshot: (piboSessionId) => this.requireRouter().getSessionStatusSnapshot(piboSessionId),
424
+ getSessionStatusSnapshot: (piboSessionId, options) => this.requireRouter().getSessionStatusSnapshot(piboSessionId, options),
425
425
  getSessionForkCandidates: (piboSessionId) => this.requireRouter().getSessionForkCandidates(piboSessionId),
426
426
  listSessionRuntimeStatuses: () => this.requireRouter().listSessionRuntimeStatuses(),
427
427
  listRuns: (options) => this.requireRouter().listRuns(options),
@@ -22,7 +22,7 @@ export function buildCompactTerminalRows(traceView, options) {
22
22
  const candidates = syncThinkingToolRows(flatNodes.map((item) => createRowCandidate(item.node, item.turnId)));
23
23
  applyCompletedTurnTiming(candidates, turnById);
24
24
  const reconciled = reconcileConceptualRowCandidates(candidates);
25
- const rows = (options.toolDisplayMode ?? "default") === "default"
25
+ const rows = !options.debugMode && (options.toolDisplayMode ?? "default") === "default"
26
26
  ? groupRelatedToolCandidates(reconciled).map((candidate) => candidate.row)
27
27
  : reconciled.map((candidate) => candidate.row);
28
28
  return applyToolDisplayMode(rows, options.toolDisplayMode ?? "default");
@@ -165,6 +165,8 @@ function createRowCandidate(node, turnId) {
165
165
  ...candidate.row,
166
166
  id: compactTerminalRowIdentity(node),
167
167
  intent: node.intent,
168
+ isToolCall: node.type === "tool.call" || node.type === "tool.result" || (node.type === "agent.delegation" && Boolean(node.toolCallId)),
169
+ toolMetrics: node.toolMetrics,
168
170
  ...debugFields(node),
169
171
  },
170
172
  };
@@ -1,6 +1,6 @@
1
1
  import { ChatDataIngestService } from "../data/ingest-service.js";
2
2
  import { PiboDataStore } from "../data/pibo-store.js";
3
- import { createPiboSession, matchesFindInput, } from "./store.js";
3
+ import { PIBO_AGENT_OBSERVATION_AUTO_CURSOR_MAX_SCOPES, createPiboSession, matchesFindInput, } from "./store.js";
4
4
  import { createLegacyPiRuntimeSessionBinding, nextRuntimeSessionBinding, RuntimeSessionBindingConflictError, } from "./runtime-binding.js";
5
5
  const SESSION_SELECT = `
6
6
  SELECT
@@ -165,6 +165,43 @@ export class PiboDataSessionStore {
165
165
  return sequence;
166
166
  });
167
167
  }
168
+ getAgentObservationAutoCursor(parentPiboSessionId, cursorScope) {
169
+ const row = this.db.prepare(`
170
+ SELECT sequence
171
+ FROM session_agent_observation_auto_cursors
172
+ WHERE parent_pibo_session_id = ? AND cursor_scope = ?
173
+ `).get(parentPiboSessionId, cursorScope);
174
+ return row?.sequence;
175
+ }
176
+ advanceAgentObservationAutoCursor(parentPiboSessionId, cursorScope, sequence) {
177
+ return this.dataStore.transaction(() => {
178
+ this.db.prepare(`
179
+ INSERT INTO session_agent_observation_auto_cursors (
180
+ parent_pibo_session_id, cursor_scope, sequence, updated_at
181
+ )
182
+ SELECT id, ?, ?, ? FROM sessions WHERE id = ? AND deleted_at IS NULL
183
+ ON CONFLICT(parent_pibo_session_id, cursor_scope) DO UPDATE SET
184
+ sequence = MAX(session_agent_observation_auto_cursors.sequence, excluded.sequence),
185
+ updated_at = CASE
186
+ WHEN excluded.sequence > session_agent_observation_auto_cursors.sequence THEN excluded.updated_at
187
+ ELSE session_agent_observation_auto_cursors.updated_at
188
+ END
189
+ `).run(cursorScope, sequence, new Date().toISOString(), parentPiboSessionId);
190
+ this.db.prepare(`
191
+ DELETE FROM session_agent_observation_auto_cursors
192
+ WHERE parent_pibo_session_id = ?
193
+ AND cursor_scope <> ?
194
+ AND cursor_scope NOT IN (
195
+ SELECT cursor_scope
196
+ FROM session_agent_observation_auto_cursors
197
+ WHERE parent_pibo_session_id = ? AND cursor_scope <> ?
198
+ ORDER BY updated_at DESC, cursor_scope DESC
199
+ LIMIT ?
200
+ )
201
+ `).run(parentPiboSessionId, cursorScope, parentPiboSessionId, cursorScope, PIBO_AGENT_OBSERVATION_AUTO_CURSOR_MAX_SCOPES - 1);
202
+ return this.getAgentObservationAutoCursor(parentPiboSessionId, cursorScope) ?? sequence;
203
+ });
204
+ }
168
205
  claimOrAttachOutputPart(input) {
169
206
  return this.dataStore.transaction(() => {
170
207
  const indexAttribute = outputPartIndexAttribute(input.kind);
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { createInitialRuntimeSessionBinding, createLegacyPiRuntimeSessionBinding, nextRuntimeSessionBinding, } from "./runtime-binding.js";
3
+ export const PIBO_AGENT_OBSERVATION_AUTO_CURSOR_MAX_SCOPES = 128;
3
4
  export function createPiboSessionId() {
4
5
  return `ps_${randomUUID()}`;
5
6
  }
@@ -50,6 +51,7 @@ export class InMemoryPiboSessionStore {
50
51
  byNativeSession = new Map();
51
52
  outputRenderHighWater = new Map();
52
53
  agentObservationNextSequence = new Map();
54
+ agentObservationAutoCursors = new Map();
53
55
  outputToolInvocationNextOrdinal = new Map();
54
56
  get(id) {
55
57
  return this.byId.get(id);
@@ -128,6 +130,34 @@ export class InMemoryPiboSessionStore {
128
130
  this.agentObservationNextSequence.set(parentPiboSessionId, sequence + 1);
129
131
  return sequence;
130
132
  }
133
+ getAgentObservationAutoCursor(parentPiboSessionId, cursorScope) {
134
+ if (!this.byId.has(parentPiboSessionId))
135
+ return undefined;
136
+ return this.agentObservationAutoCursors.get(agentObservationAutoCursorKey(parentPiboSessionId, cursorScope));
137
+ }
138
+ advanceAgentObservationAutoCursor(parentPiboSessionId, cursorScope, sequence) {
139
+ if (!this.byId.has(parentPiboSessionId))
140
+ return sequence;
141
+ const key = agentObservationAutoCursorKey(parentPiboSessionId, cursorScope);
142
+ const advanced = Math.max(this.agentObservationAutoCursors.get(key) ?? 0, sequence);
143
+ this.agentObservationAutoCursors.delete(key);
144
+ this.agentObservationAutoCursors.set(key, advanced);
145
+ const prefix = `${JSON.stringify([parentPiboSessionId]).slice(0, -1)},`;
146
+ let scopeCount = 0;
147
+ for (const existingKey of this.agentObservationAutoCursors.keys()) {
148
+ if (existingKey.startsWith(prefix))
149
+ scopeCount += 1;
150
+ }
151
+ for (const existingKey of this.agentObservationAutoCursors.keys()) {
152
+ if (scopeCount <= PIBO_AGENT_OBSERVATION_AUTO_CURSOR_MAX_SCOPES)
153
+ break;
154
+ if (!existingKey.startsWith(prefix))
155
+ continue;
156
+ this.agentObservationAutoCursors.delete(existingKey);
157
+ scopeCount -= 1;
158
+ }
159
+ return advanced;
160
+ }
131
161
  claimOutputToolInvocationOrdinal(piboSessionId, eventId, toolCallId) {
132
162
  const key = outputToolInvocationCounterKey(piboSessionId, eventId, toolCallId);
133
163
  const ordinal = this.outputToolInvocationNextOrdinal.get(key) ?? 0;
@@ -172,6 +202,11 @@ export class InMemoryPiboSessionStore {
172
202
  this.byNativeSession.delete(nativeKey);
173
203
  this.outputRenderHighWater.delete(id);
174
204
  this.agentObservationNextSequence.delete(id);
205
+ const observationCursorPrefix = `${JSON.stringify([id]).slice(0, -1)},`;
206
+ for (const key of this.agentObservationAutoCursors.keys()) {
207
+ if (key.startsWith(observationCursorPrefix))
208
+ this.agentObservationAutoCursors.delete(key);
209
+ }
175
210
  const counterPrefix = `${JSON.stringify([id]).slice(0, -1)},`;
176
211
  for (const key of this.outputToolInvocationNextOrdinal.keys()) {
177
212
  if (key.startsWith(counterPrefix))
@@ -213,6 +248,9 @@ function outputRenderSequenceHighWater(metadata) {
213
248
  const value = metadata?.outputRenderSequenceHighWater;
214
249
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
215
250
  }
251
+ function agentObservationAutoCursorKey(parentPiboSessionId, cursorScope) {
252
+ return JSON.stringify([parentPiboSessionId, cursorScope]);
253
+ }
216
254
  function outputToolInvocationCounterKey(piboSessionId, eventId, toolCallId) {
217
255
  return JSON.stringify([piboSessionId, eventId, toolCallId]);
218
256
  }
@@ -0,0 +1,138 @@
1
+ import { createRequire } from "node:module";
2
+ import { DEFAULT_TOOL_METRIC_TOKEN_CALCULATION, sanitizeToolMetricTokenCalculation, toolMetricTokenBasis, } from "./tool-call-token-settings.js";
3
+ export const MAX_TIKTOKEN_PAYLOAD_CHARACTERS = 4_000_000;
4
+ const require = createRequire(import.meta.url);
5
+ let activeTokenizer;
6
+ /** Bounded structural walk; character mode never tokenizes, copies, or scans large strings. */
7
+ export function measureToolPayloadCharacters(payload) {
8
+ let budget = 10_000;
9
+ const seen = new WeakSet();
10
+ function size(value, depth) {
11
+ if (--budget < 0 || depth > 64)
12
+ return NaN;
13
+ if (typeof value === "string")
14
+ return value.length;
15
+ if (value === null)
16
+ return 4;
17
+ if (typeof value === "boolean")
18
+ return value ? 4 : 5;
19
+ if (typeof value === "number")
20
+ return String(value).length;
21
+ if (typeof value !== "object" || seen.has(value))
22
+ return NaN;
23
+ seen.add(value);
24
+ let chars = 2;
25
+ if (Array.isArray(value)) {
26
+ for (const item of value) {
27
+ chars += size(item, depth + 1) + 1;
28
+ if (!Number.isFinite(chars))
29
+ return NaN;
30
+ }
31
+ }
32
+ else {
33
+ const record = value;
34
+ if (["image", "audio", "document", "resource", "image_url"].includes(String(record.type)))
35
+ return NaN;
36
+ for (const key in record) {
37
+ if (--budget < 0)
38
+ return NaN;
39
+ if (!Object.hasOwn(record, key) || record[key] === undefined)
40
+ continue;
41
+ chars += key.length + 3 + size(record[key], depth + 1) + 1;
42
+ if (!Number.isFinite(chars))
43
+ return NaN;
44
+ }
45
+ }
46
+ seen.delete(value);
47
+ return chars;
48
+ }
49
+ try {
50
+ const chars = size(payload, 0);
51
+ return Number.isFinite(chars) ? chars : undefined;
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ }
57
+ export function estimateToolPayloadTokens(payload, calculation = DEFAULT_TOOL_METRIC_TOKEN_CALCULATION) {
58
+ try {
59
+ const normalized = sanitizeToolMetricTokenCalculation(calculation);
60
+ if (normalized.method === "characters") {
61
+ const chars = measureToolPayloadCharacters(payload);
62
+ if (chars === undefined)
63
+ return undefined;
64
+ const tokens = Math.ceil(chars / normalized.factor);
65
+ return Number.isSafeInteger(tokens) ? tokens : undefined;
66
+ }
67
+ const text = serializableToolPayloadText(payload);
68
+ if (text === undefined)
69
+ return undefined;
70
+ return tiktokenPayloadTokens(text, normalized.encoding);
71
+ }
72
+ catch {
73
+ // Diagnostics must not turn an otherwise successful tool into a failure.
74
+ return undefined;
75
+ }
76
+ }
77
+ export class ToolCallMetricsCollector {
78
+ getCalculation;
79
+ active = new Map();
80
+ constructor(getCalculation = () => DEFAULT_TOOL_METRIC_TOKEN_CALCULATION) {
81
+ this.getCalculation = getCalculation;
82
+ }
83
+ start(id, args, now = performance.now()) {
84
+ if (this.active.has(id))
85
+ return;
86
+ const calculation = this.currentCalculation();
87
+ this.active.set(id, {
88
+ startedAt: now,
89
+ inputTokens: estimateToolPayloadTokens(args, calculation),
90
+ calculation,
91
+ });
92
+ }
93
+ finish(id, result, now = performance.now()) {
94
+ const started = this.active.get(id);
95
+ this.active.delete(id);
96
+ const calculation = started?.calculation ?? this.currentCalculation();
97
+ // Harness result metadata is not model-visible tool output.
98
+ const output = result && typeof result === "object" && "content" in result
99
+ ? result.content : result;
100
+ return {
101
+ tokenBasis: toolMetricTokenBasis(calculation),
102
+ durationMs: started ? Math.max(0, now - started.startedAt) : undefined,
103
+ inputTokens: started?.inputTokens,
104
+ outputTokens: estimateToolPayloadTokens(output, calculation),
105
+ };
106
+ }
107
+ clear() {
108
+ this.active.clear();
109
+ }
110
+ currentCalculation() {
111
+ try {
112
+ return sanitizeToolMetricTokenCalculation(this.getCalculation());
113
+ }
114
+ catch {
115
+ return DEFAULT_TOOL_METRIC_TOKEN_CALCULATION;
116
+ }
117
+ }
118
+ }
119
+ function serializableToolPayloadText(payload) {
120
+ const measuredCharacters = measureToolPayloadCharacters(payload);
121
+ if (measuredCharacters === undefined || measuredCharacters > MAX_TIKTOKEN_PAYLOAD_CHARACTERS)
122
+ return undefined;
123
+ if (typeof payload === "string")
124
+ return payload;
125
+ const serialized = JSON.stringify(payload);
126
+ return typeof serialized === "string" && serialized.length <= MAX_TIKTOKEN_PAYLOAD_CHARACTERS
127
+ ? serialized
128
+ : undefined;
129
+ }
130
+ function tiktokenPayloadTokens(text, encoding) {
131
+ if (activeTokenizer?.encoding !== encoding) {
132
+ activeTokenizer?.tokenizer.free();
133
+ const tiktoken = require("tiktoken");
134
+ activeTokenizer = { encoding, tokenizer: tiktoken.get_encoding(encoding) };
135
+ }
136
+ const tokens = activeTokenizer.tokenizer.encode_ordinary(text).length;
137
+ return Number.isSafeInteger(tokens) ? tokens : undefined;
138
+ }