@pasko70/pibo 3.0.1 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +20 -6
  2. package/dist/agent-runtime/context-build.js +26 -2
  3. package/dist/agent-runtime/routed-session.js +108 -9
  4. package/dist/agent-runtimes/codex-native/adapter.js +12 -0
  5. package/dist/agent-runtimes/codex-native/resource-delivery.js +35 -7
  6. package/dist/agent-runtimes/omp/adapter.js +3 -7
  7. package/dist/agent-runtimes/pi/adapter.js +5 -3
  8. package/dist/agent-runtimes/pi/routed-session.js +49 -8
  9. package/dist/agent-runtimes/pi/runtime.js +6 -0
  10. package/dist/apps/chat/agent-profiles.js +31 -2
  11. package/dist/apps/chat/agent-store.js +43 -3
  12. package/dist/apps/chat/chat-request-normalizers.js +31 -0
  13. package/dist/apps/chat/data/chat-data-mappers.js +61 -11
  14. package/dist/apps/chat/data/session-query-service.js +33 -10
  15. package/dist/apps/chat/data/timeline-query-service.js +60 -35
  16. package/dist/apps/chat/output-compactor.js +144 -21
  17. package/dist/apps/chat/output-event-policy.js +179 -2
  18. package/dist/apps/chat/stream.js +16 -7
  19. package/dist/apps/chat/trace-v2.js +1 -0
  20. package/dist/apps/chat/web-app.js +236 -64
  21. package/dist/apps/chat-ui/assets/{dist-BBDMeU5-.js → dist-B6GZgWSj.js} +1 -1
  22. package/dist/apps/chat-ui/assets/{dist-mqCviI-c.js → dist-BPotHecb.js} +1 -1
  23. package/dist/apps/chat-ui/assets/{dist-JnW4v8UE.js → dist-BzqQKeVO.js} +1 -1
  24. package/dist/apps/chat-ui/assets/{dist-BDIATCQt.js → dist-CvaPTBTN.js} +1 -1
  25. package/dist/apps/chat-ui/assets/{dist-lRiLP9rr.js → dist-paagejNj.js} +1 -1
  26. package/dist/apps/chat-ui/assets/index-6JJV-eic.js +228 -0
  27. package/dist/apps/chat-ui/assets/index-CeL9JPP5.css +1 -0
  28. package/dist/apps/chat-ui/index.html +2 -2
  29. package/dist/apps/chat-vscode-web/assets/{index-Dtw2Z7jz.js → index-U-MSErGa.js} +4 -4
  30. package/dist/apps/chat-vscode-web/index.html +1 -1
  31. package/dist/apps/cli-ui/cliSessionsCommand.js +34 -8
  32. package/dist/cli-session/localSessionSource.js +153 -161
  33. package/dist/cli.js +7 -11
  34. package/dist/core/context-build.js +7 -16
  35. package/dist/core/output-persistence-retry.js +484 -0
  36. package/dist/core/output-render-sequence.js +331 -0
  37. package/dist/core/profiles.js +24 -0
  38. package/dist/core/provider-recovery.js +7 -1
  39. package/dist/core/session-model.js +37 -1
  40. package/dist/core/session-router.js +53 -13
  41. package/dist/data/ingest-service.js +91 -9
  42. package/dist/data/navigation-store.js +5 -5
  43. package/dist/data/schema.js +122 -2
  44. package/dist/data/session-store.js +3 -3
  45. package/dist/debug/index.js +44 -2
  46. package/dist/gateway/web.js +8 -8
  47. package/dist/local/client.js +3 -2
  48. package/dist/plugins/context-files-store.js +30 -0
  49. package/dist/plugins/context-files.js +52 -1
  50. package/dist/plugins/registry.js +2 -0
  51. package/dist/plugins/user-profile-resources.js +22 -0
  52. package/dist/reliability/store.js +246 -37
  53. package/dist/session-ui/terminalRows.js +6 -3
  54. package/dist/sessions/pibo-data-store.js +145 -0
  55. package/dist/sessions/store.js +42 -0
  56. package/dist/setup/cli.js +422 -6
  57. package/dist/setup/installation-profiles.js +464 -0
  58. package/dist/shared/deterministic-digest.js +94 -0
  59. package/dist/shared/trace-engine.js +54 -0
  60. package/dist/shared/trace-event-projection.js +124 -67
  61. package/dist/shared/trace-history.js +47 -12
  62. package/dist/shared/trace-live-reducer.js +23 -4
  63. package/dist/shared/trace-nodes.js +17 -28
  64. package/dist/shared/trace-order.js +41 -5
  65. package/dist/shared/trace-page-merge.js +44 -14
  66. package/dist/shared/trace-patch-nodes.js +2 -0
  67. package/dist/shared/trace-subagent-links.js +4 -1
  68. package/dist/shared/trace-tool-identity.js +15 -8
  69. package/dist/signals/projector.js +28 -6
  70. package/dist/subagents/tool.js +7 -3
  71. package/docs/ops/vscode-extension-release.md +9 -1
  72. package/npm-shrinkwrap.json +2 -2
  73. package/package.json +2 -1
  74. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +0 -228
  75. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +0 -1
  76. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  77. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.1.vsix +0 -0
@@ -1,11 +1,9 @@
1
1
  import { CHAT_WEB_APP_NAME } from "../apps/chat/web-app.js";
2
2
  import { createDefaultPiboPlugins } from "../plugins/builtin.js";
3
3
  import { createPiboBetterAuthPlugin } from "../plugins/better-auth.js";
4
- import { createPiboChatCustomAgentProfilesPlugin } from "../plugins/chat-custom-agents.js";
5
- import { createPiboChatUserSkillsPlugin } from "../plugins/chat-user-skills.js";
6
4
  import { createPiboChatWebPlugin } from "../plugins/chat-web.js";
7
5
  import { createPiboChatVscodeWebPlugin } from "../plugins/chat-vscode-web.js";
8
- import { createPiboContextFilesPlugin } from "../plugins/context-files.js";
6
+ import { createPiboUserProfileResourcePlugins } from "../plugins/user-profile-resources.js";
9
7
  import { createPiboPreviewPlugin } from "../previews/plugin.js";
10
8
  import { createPiboCronPlugin } from "../cron/plugin.js";
11
9
  import { createPiboLoopPlugin } from "../loops/plugin.js";
@@ -154,13 +152,15 @@ export function createWebPiboPluginRegistry(options = {}) {
154
152
  landingAppName: CHAT_WEB_APP_NAME,
155
153
  }),
156
154
  createPiboCronPlugin({ cronStorePath: resolvedOptions.chat?.cronStorePath, dataStorePath: resolvedOptions.chat?.dataStorePath, dataPayloadRootDir: resolvedOptions.chat?.dataPayloadRootDir }),
157
- createPiboChatUserSkillsPlugin({
158
- globalRoot: resolvedOptions.chat?.userSkillGlobalRoot,
159
- workspaceRoot: resolvedOptions.chat?.userSkillWorkspaceRoot,
155
+ ...createPiboUserProfileResourcePlugins({
156
+ userSkills: {
157
+ globalRoot: resolvedOptions.chat?.userSkillGlobalRoot,
158
+ workspaceRoot: resolvedOptions.chat?.userSkillWorkspaceRoot,
159
+ },
160
+ contextFiles: resolvedOptions.contextFiles,
161
+ customAgents: { agentStorePath: resolvedOptions.chat?.agentStorePath },
160
162
  }),
161
- createPiboChatCustomAgentProfilesPlugin({ agentStorePath: resolvedOptions.chat?.agentStorePath }),
162
163
  createPiboLoopPlugin({ loopStorePath: resolvedOptions.chat?.ralphStorePath, dataStorePath: resolvedOptions.chat?.dataStorePath, dataPayloadRootDir: resolvedOptions.chat?.dataPayloadRootDir }),
163
- createPiboContextFilesPlugin(resolvedOptions.contextFiles),
164
164
  createPiboPreviewPlugin(),
165
165
  createPiboChatWebPlugin(resolvedOptions.chat),
166
166
  createPiboChatVscodeWebPlugin(),
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { PiboSessionRouter } from "../core/session-router.js";
3
- import { createDefaultPiboPluginRegistry, createPiboProfileFromRegistryOrDefault, resolvePiboProfileNameFromRegistryOrDefault } from "../plugins/builtin.js";
3
+ import { createPiboProfileFromRegistryOrDefault, resolvePiboProfileNameFromRegistryOrDefault } from "../plugins/builtin.js";
4
+ import { createDefaultPiboUserProfileRegistry } from "../plugins/user-profile-resources.js";
4
5
  import { getDefaultPiboWorkspace } from "../core/workspace.js";
5
6
  import { InMemoryPiboSessionStore, } from "../sessions/store.js";
6
7
  export const LOCAL_TUI_CHANNEL_NAME = "local-tui";
@@ -52,7 +53,7 @@ export class LocalRoutedTuiClient {
52
53
  }
53
54
  }
54
55
  export function createLocalRoutedTuiClient(options = {}) {
55
- const registry = options.pluginRegistry ?? createDefaultPiboPluginRegistry();
56
+ const registry = options.pluginRegistry ?? createDefaultPiboUserProfileRegistry();
56
57
  const profileName = resolvePiboProfileNameFromRegistryOrDefault(registry, options.profile);
57
58
  const profile = createPiboProfileFromRegistryOrDefault(registry, profileName);
58
59
  const sessionName = options.sessionName ?? "default";
@@ -101,6 +101,36 @@ function revisionRowToRecord(row) {
101
101
  ...(row.actor_id ? { actorId: row.actor_id } : {}),
102
102
  };
103
103
  }
104
+ export function readContextFileCatalog(path) {
105
+ const resolvedPath = resolve(path);
106
+ if (!existsSync(resolvedPath))
107
+ return [];
108
+ const db = new DatabaseSync(resolvedPath, { readOnly: true });
109
+ try {
110
+ const table = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'context_files'").get();
111
+ if (!table)
112
+ return [];
113
+ const columns = new Set(db.prepare("PRAGMA table_info(context_files)").all().map((column) => column.name));
114
+ const required = ["key", "label", "managed_path", "scope", "agent_profile_name"];
115
+ if (!required.every((column) => columns.has(column)))
116
+ return [];
117
+ const rows = db.prepare(`
118
+ SELECT key, label, managed_path, scope, agent_profile_name
119
+ FROM context_files
120
+ ORDER BY updated_at DESC
121
+ `).all();
122
+ return rows.map((row) => ({
123
+ key: row.key,
124
+ label: row.label,
125
+ managedPath: row.managed_path,
126
+ scope: row.scope,
127
+ ...(row.agent_profile_name ? { agentProfileName: row.agent_profile_name } : {}),
128
+ }));
129
+ }
130
+ finally {
131
+ db.close();
132
+ }
133
+ }
104
134
  export class ContextFileMetadataStore {
105
135
  legacyStorePath;
106
136
  db;
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
  import { readFallbackPidFile, readPidFile } from "../gateway/pidfile.js";
7
7
  import { PiboWebHttpError, readJsonBody, responseHtml, responseJson } from "../web/http.js";
8
8
  import { definePiboPlugin } from "./registry.js";
9
- import { buildContextFileDiff, ContextFileMetadataStore, hashContextFileContent, } from "./context-files-store.js";
9
+ import { buildContextFileDiff, ContextFileMetadataStore, hashContextFileContent, readContextFileCatalog, } from "./context-files-store.js";
10
10
  export const CONTEXT_FILES_APP_NAME = "pibo.context-files";
11
11
  export const CONTEXT_FILES_MOUNT_PATH = "/apps/context-files";
12
12
  export const CONTEXT_FILES_API_PREFIX = "/api/context-files";
@@ -1028,6 +1028,57 @@ function createContextFilesWebApp(service) {
1028
1028
  },
1029
1029
  };
1030
1030
  }
1031
+ export function createPiboContextFilesCatalogPlugin(options = {}) {
1032
+ const paths = resolveContextFilesPaths(options);
1033
+ return definePiboPlugin({
1034
+ id: "pibo.context-files-catalog",
1035
+ name: "Pibo Context Files Catalog",
1036
+ register(api) {
1037
+ const registeredPaths = new Set();
1038
+ const registeredKeys = new Set();
1039
+ try {
1040
+ for (const file of readContextFileCatalog(paths.metadataPath)) {
1041
+ api.upsertContextFile(profileForManaged(file));
1042
+ registeredPaths.add(resolve(file.managedPath));
1043
+ registeredKeys.add(file.key);
1044
+ }
1045
+ }
1046
+ catch (error) {
1047
+ console.warn(`[pibo] Skipping managed context-file catalog registration: ${error instanceof Error ? error.message : String(error)}`);
1048
+ }
1049
+ let entries = [];
1050
+ try {
1051
+ entries = readdirSync(paths.globalDir, { withFileTypes: true });
1052
+ }
1053
+ catch (error) {
1054
+ const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
1055
+ if (code !== "ENOENT") {
1056
+ console.warn(`[pibo] Skipping global context-file discovery: ${error instanceof Error ? error.message : String(error)}`);
1057
+ }
1058
+ }
1059
+ const usedKeys = new Set(registeredKeys);
1060
+ for (const entry of entries) {
1061
+ if (!entry.isFile())
1062
+ continue;
1063
+ const extension = extname(entry.name).toLowerCase();
1064
+ if (extension !== ".md" && extension !== ".markdown")
1065
+ continue;
1066
+ const path = resolve(paths.globalDir, entry.name);
1067
+ if (registeredPaths.has(path))
1068
+ continue;
1069
+ const key = uniqueKey(`ctx:${slugSegment(entry.name.slice(0, -extension.length) || entry.name)}`, usedKeys);
1070
+ usedKeys.add(key);
1071
+ api.upsertContextFile({
1072
+ key,
1073
+ label: labelFromManagedPath(entry.name),
1074
+ path,
1075
+ source: "managed",
1076
+ scope: "global",
1077
+ });
1078
+ }
1079
+ },
1080
+ });
1081
+ }
1031
1082
  export function createPiboContextFilesPlugin(options = {}) {
1032
1083
  const paths = resolveContextFilesPaths(options);
1033
1084
  return definePiboPlugin({
@@ -521,11 +521,13 @@ export class PiboPluginRegistry {
521
521
  nativeTools: sessionContext.tools.filter((tool) => tool.enabled !== false).map((tool) => tool.name),
522
522
  skills: sessionContext.skills.filter((skill) => skill.enabled !== false).map((skill) => skill.name),
523
523
  contextFiles: sessionContext.contextFiles.filter((contextFile) => contextFile.enabled !== false).map(contextFileKey),
524
+ diagnostics: sessionContext.diagnostics.map((diagnostic) => ({ ...diagnostic })),
524
525
  subagents: sessionContext.subagents.filter((subagent) => subagent.enabled !== false),
525
526
  mcpServers: [...sessionContext.mcpServers],
526
527
  piPackages: sessionContext.piPackages.filter((pkg) => pkg.enabled !== false).map((pkg) => pkg.id),
527
528
  model: sessionContext.model ? { ...sessionContext.model } : undefined,
528
529
  mainModel: sessionContext.mainModel ? { ...sessionContext.mainModel } : undefined,
530
+ mainModelFallbacks: sessionContext.mainModelFallbacks.map((model) => ({ ...model })),
529
531
  subagentModel: sessionContext.subagentModel ? { ...sessionContext.subagentModel } : undefined,
530
532
  thinkingLevel: sessionContext.thinkingLevel,
531
533
  mainThinkingLevel: sessionContext.mainThinkingLevel,
@@ -0,0 +1,22 @@
1
+ import { createPiboChatCustomAgentProfilesPlugin, } from "./chat-custom-agents.js";
2
+ import { createPiboChatUserSkillsPlugin } from "./chat-user-skills.js";
3
+ import { createPiboContextFilesCatalogPlugin, createPiboContextFilesPlugin, } from "./context-files.js";
4
+ import { createDefaultPiboPluginRegistry } from "./builtin.js";
5
+ export function createPiboUserProfileResourcePlugins(options = {}) {
6
+ return [
7
+ createPiboChatUserSkillsPlugin(options.userSkills),
8
+ options.contextFilesMode === "catalog"
9
+ ? createPiboContextFilesCatalogPlugin(options.contextFiles)
10
+ : createPiboContextFilesPlugin(options.contextFiles),
11
+ createPiboChatCustomAgentProfilesPlugin(options.customAgents),
12
+ ];
13
+ }
14
+ export function createDefaultPiboUserProfileRegistry(options = {}) {
15
+ const registry = createDefaultPiboPluginRegistry();
16
+ registerPiboUserProfileResources(registry, { contextFilesMode: "catalog", ...options });
17
+ return registry;
18
+ }
19
+ export function registerPiboUserProfileResources(registry, options = {}) {
20
+ for (const plugin of createPiboUserProfileResourcePlugins(options))
21
+ registry.registerPlugin(plugin);
22
+ }
@@ -36,6 +36,11 @@ function ensurePiboRunColumn(db, name, definition) {
36
36
  if (!columns.has(name))
37
37
  db.exec(`ALTER TABLE pibo_runs ADD COLUMN ${name} ${definition}`);
38
38
  }
39
+ function ensurePiboJobColumn(db, name, definition) {
40
+ const columns = sqliteTableColumns(db, "pibo_jobs");
41
+ if (!columns.has(name))
42
+ db.exec(`ALTER TABLE pibo_jobs ADD COLUMN ${name} ${definition}`);
43
+ }
39
44
  export class PiboReliabilityStore {
40
45
  db;
41
46
  appendEventStatement;
@@ -74,6 +79,13 @@ export class PiboReliabilityStore {
74
79
  CREATE INDEX IF NOT EXISTS idx_pibo_event_stream_topic_stream
75
80
  ON pibo_event_stream(topic, stream_id);
76
81
 
82
+ CREATE TABLE IF NOT EXISTS pibo_delivery_receipts (
83
+ delivery_id TEXT NOT NULL,
84
+ projection TEXT NOT NULL,
85
+ delivered_at TEXT NOT NULL,
86
+ PRIMARY KEY(delivery_id, projection)
87
+ );
88
+
77
89
  CREATE TABLE IF NOT EXISTS pibo_event_consumers (
78
90
  consumer TEXT NOT NULL,
79
91
  topic TEXT NOT NULL,
@@ -91,6 +103,7 @@ export class PiboReliabilityStore {
91
103
  priority INTEGER NOT NULL DEFAULT 0,
92
104
  worker_id TEXT,
93
105
  claim_expires_at TEXT,
106
+ claim_token INTEGER NOT NULL DEFAULT 0,
94
107
  attempts INTEGER NOT NULL DEFAULT 0,
95
108
  max_attempts INTEGER NOT NULL DEFAULT 1,
96
109
  idempotency_key TEXT,
@@ -157,6 +170,7 @@ export class PiboReliabilityStore {
157
170
  CREATE INDEX IF NOT EXISTS idx_pibo_runs_status
158
171
  ON pibo_runs(status);
159
172
  `);
173
+ ensurePiboJobColumn(this.db, "claim_token", "INTEGER NOT NULL DEFAULT 0");
160
174
  ensurePiboRunColumn(this.db, "timeout_ms", "INTEGER");
161
175
  ensurePiboRunColumn(this.db, "timeout_at", "TEXT");
162
176
  ensurePiboRunColumn(this.db, "timeout_phase", "TEXT");
@@ -189,6 +203,21 @@ export class PiboReliabilityStore {
189
203
  throw error;
190
204
  }
191
205
  }
206
+ hasDeliveryReceipt(deliveryId, projection) {
207
+ return this.db
208
+ .prepare("SELECT 1 AS found FROM pibo_delivery_receipts WHERE delivery_id = ? AND projection = ?")
209
+ .get(deliveryId, projection) !== undefined;
210
+ }
211
+ recordDeliveryReceipt(deliveryId, projection, deliveredAt = now()) {
212
+ this.db.prepare(`
213
+ INSERT OR IGNORE INTO pibo_delivery_receipts (delivery_id, projection, delivered_at)
214
+ VALUES (?, ?, ?)
215
+ `).run(deliveryId, projection, deliveredAt);
216
+ const row = this.db
217
+ .prepare("SELECT delivery_id, projection, delivered_at FROM pibo_delivery_receipts WHERE delivery_id = ? AND projection = ?")
218
+ .get(deliveryId, projection);
219
+ return { deliveryId: row.delivery_id, projection: row.projection, deliveredAt: row.delivered_at };
220
+ }
192
221
  list(input = {}) {
193
222
  const clauses = [];
194
223
  const values = [];
@@ -339,6 +368,7 @@ export class PiboReliabilityStore {
339
368
  SET state = 'running',
340
369
  worker_id = ?,
341
370
  claim_expires_at = ?,
371
+ claim_token = claim_token + 1,
342
372
  attempts = attempts + 1,
343
373
  updated_at = ?
344
374
  WHERE job_id IN (
@@ -361,6 +391,7 @@ export class PiboReliabilityStore {
361
391
  SET state = 'running',
362
392
  worker_id = ?,
363
393
  claim_expires_at = ?,
394
+ claim_token = claim_token + 1,
364
395
  attempts = attempts + 1,
365
396
  updated_at = ?
366
397
  WHERE job_id = ?
@@ -372,7 +403,50 @@ export class PiboReliabilityStore {
372
403
  .get(workerId, claimExpiresAt, timestamp, jobId, timestamp, timestamp);
373
404
  return row ? jobFromRow(row) : undefined;
374
405
  }
375
- ack(jobId, workerId) {
406
+ claimRecoverableJob(jobId, workerId, visibilityTimeoutMs = 30000, forcePending = false) {
407
+ const timestamp = now();
408
+ const claimExpiresAt = new Date(Date.now() + visibilityTimeoutMs).toISOString();
409
+ const row = this.db.prepare(`
410
+ UPDATE pibo_jobs
411
+ SET state = 'running',
412
+ worker_id = ?,
413
+ claim_expires_at = ?,
414
+ claim_token = claim_token + 1,
415
+ attempts = attempts + 1,
416
+ updated_at = ?
417
+ WHERE job_id = ?
418
+ AND (
419
+ (state = 'pending' AND (? = 1 OR run_at <= ?))
420
+ OR (state = 'running' AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?)
421
+ )
422
+ AND (expires_at IS NULL OR expires_at > ?)
423
+ RETURNING *
424
+ `).get(workerId, claimExpiresAt, timestamp, jobId, forcePending ? 1 : 0, timestamp, timestamp, timestamp);
425
+ return row ? jobFromRow(row) : undefined;
426
+ }
427
+ updateJobPayload(jobId, workerId, payload, claimToken) {
428
+ const result = this.db.prepare(`
429
+ UPDATE pibo_jobs
430
+ SET payload_json = ?, updated_at = ?
431
+ WHERE job_id = ? AND state = 'running' AND worker_id = ?
432
+ AND (? IS NULL OR claim_token = ?)
433
+ AND claim_expires_at IS NOT NULL AND claim_expires_at > ?
434
+ `).run(json(payload), now(), jobId, workerId, claimToken ?? null, claimToken ?? null, now());
435
+ return Number(result.changes ?? 0) > 0;
436
+ }
437
+ releaseJob(jobId, workerId, delayMs = 0, claimToken) {
438
+ const timestamp = now();
439
+ const runAt = new Date(Date.parse(timestamp) + Math.max(0, delayMs)).toISOString();
440
+ const result = this.db.prepare(`
441
+ UPDATE pibo_jobs
442
+ SET state = 'pending', worker_id = NULL, claim_expires_at = NULL, run_at = ?, updated_at = ?
443
+ WHERE job_id = ? AND state = 'running' AND worker_id = ?
444
+ AND (? IS NULL OR claim_token = ?)
445
+ AND claim_expires_at IS NOT NULL AND claim_expires_at > ?
446
+ `).run(runAt, timestamp, jobId, workerId, claimToken ?? null, claimToken ?? null, timestamp);
447
+ return Number(result.changes ?? 0) > 0;
448
+ }
449
+ ack(jobId, workerId, claimToken) {
376
450
  const timestamp = now();
377
451
  const result = this.db
378
452
  .prepare(`
@@ -380,47 +454,53 @@ export class PiboReliabilityStore {
380
454
  WHERE job_id = ?
381
455
  AND state = 'running'
382
456
  AND worker_id = ?
457
+ AND (? IS NULL OR claim_token = ?)
383
458
  AND claim_expires_at IS NOT NULL
384
459
  AND claim_expires_at > ?
385
460
  `)
386
- .run(jobId, workerId, timestamp);
461
+ .run(jobId, workerId, claimToken ?? null, claimToken ?? null, timestamp);
387
462
  return Number(result.changes ?? 0) > 0;
388
463
  }
389
464
  retry(jobId, workerId, input = {}) {
390
465
  const timestamp = now();
391
- const row = this.getLiveWorkerJob(jobId, workerId, timestamp);
392
- if (!row)
393
- return false;
394
- const job = jobFromRow(row);
395
- if (job.attempts >= job.maxAttempts) {
396
- this.moveJobToDead(row, input.error ?? "Job exhausted retry attempts.", "max_attempts", timestamp);
397
- return true;
398
- }
399
- const delayMs = input.delayMs ?? retryDelayMs(job.attempts, input);
400
- const runAt = new Date(asDate(input.now).getTime() + delayMs).toISOString();
401
- this.db
402
- .prepare(`
403
- UPDATE pibo_jobs
404
- SET state = 'pending',
405
- worker_id = NULL,
406
- claim_expires_at = NULL,
407
- run_at = ?,
408
- updated_at = ?,
409
- last_error = ?
410
- WHERE job_id = ?
411
- `)
412
- .run(runAt, timestamp, input.error ?? null, jobId);
413
- return true;
466
+ return this.inImmediateTransaction(() => {
467
+ const row = this.getLiveWorkerJob(jobId, workerId, timestamp, input.claimToken);
468
+ if (!row)
469
+ return false;
470
+ const job = jobFromRow(row);
471
+ if (job.attempts >= job.maxAttempts) {
472
+ this.moveJobToDead(row, input.error ?? "Job exhausted retry attempts.", "max_attempts", timestamp);
473
+ return true;
474
+ }
475
+ const delayMs = input.delayMs ?? retryDelayMs(job.attempts, input);
476
+ const runAt = new Date(asDate(input.now).getTime() + delayMs).toISOString();
477
+ const result = this.db
478
+ .prepare(`
479
+ UPDATE pibo_jobs
480
+ SET state = 'pending',
481
+ worker_id = NULL,
482
+ claim_expires_at = NULL,
483
+ run_at = ?,
484
+ updated_at = ?,
485
+ last_error = ?
486
+ WHERE job_id = ? AND state = 'running' AND worker_id = ?
487
+ AND (? IS NULL OR claim_token = ?)
488
+ `)
489
+ .run(runAt, timestamp, input.error ?? null, jobId, workerId, input.claimToken ?? null, input.claimToken ?? null);
490
+ return Number(result.changes ?? 0) > 0;
491
+ });
414
492
  }
415
- fail(jobId, workerId, error) {
493
+ fail(jobId, workerId, error, claimToken) {
416
494
  const timestamp = now();
417
- const row = this.getLiveWorkerJob(jobId, workerId, timestamp);
418
- if (!row)
419
- return false;
420
- this.moveJobToDead(row, error, "failed", timestamp);
421
- return true;
495
+ return this.inImmediateTransaction(() => {
496
+ const row = this.getLiveWorkerJob(jobId, workerId, timestamp, claimToken);
497
+ if (!row)
498
+ return false;
499
+ this.moveJobToDead(row, error, "failed", timestamp);
500
+ return true;
501
+ });
422
502
  }
423
- heartbeat(jobId, workerId, extendMs) {
503
+ heartbeat(jobId, workerId, extendMs, claimToken) {
424
504
  const timestamp = now();
425
505
  const claimExpiresAt = new Date(Date.now() + Math.max(1, extendMs)).toISOString();
426
506
  const result = this.db
@@ -430,10 +510,11 @@ export class PiboReliabilityStore {
430
510
  WHERE job_id = ?
431
511
  AND state = 'running'
432
512
  AND worker_id = ?
513
+ AND (? IS NULL OR claim_token = ?)
433
514
  AND claim_expires_at IS NOT NULL
434
515
  AND claim_expires_at > ?
435
516
  `)
436
- .run(claimExpiresAt, timestamp, jobId, workerId, timestamp);
517
+ .run(claimExpiresAt, timestamp, jobId, workerId, claimToken ?? null, claimToken ?? null, timestamp);
437
518
  return Number(result.changes ?? 0) > 0;
438
519
  }
439
520
  listJobs(input = {}) {
@@ -447,11 +528,56 @@ export class PiboReliabilityStore {
447
528
  clauses.push("state = ?");
448
529
  values.push(input.state);
449
530
  }
531
+ if (input.excludeJobIds?.length) {
532
+ clauses.push(`job_id NOT IN (${input.excludeJobIds.map(() => "?").join(", ")})`);
533
+ values.push(...input.excludeJobIds);
534
+ }
450
535
  const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
451
536
  const rows = this.db
452
537
  .prepare(`SELECT * FROM pibo_jobs ${where} ORDER BY priority DESC, run_at ASC, created_at ASC LIMIT ?`)
453
538
  .all(...values, clampLimit(input.limit, 100));
454
- return rows.map(jobFromRow);
539
+ const jobs = [];
540
+ for (const row of rows) {
541
+ try {
542
+ jobs.push(jobFromRow(row));
543
+ }
544
+ catch {
545
+ this.quarantineJobRow(row, "payload_malformed");
546
+ }
547
+ }
548
+ return jobs;
549
+ }
550
+ listRecoverableJobs(input = {}) {
551
+ const timestamp = now();
552
+ const clauses = [
553
+ "((state = 'pending' AND (? = 1 OR run_at <= ?)) OR (state = 'running' AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?))",
554
+ "(expires_at IS NULL OR expires_at > ?)",
555
+ ];
556
+ const values = [input.includeDeferredPending ? 1 : 0, timestamp, timestamp, timestamp];
557
+ if (input.queue) {
558
+ clauses.push("queue = ?");
559
+ values.push(input.queue);
560
+ }
561
+ if (input.excludeJobIds?.length) {
562
+ clauses.push(`job_id NOT IN (${input.excludeJobIds.map(() => "?").join(", ")})`);
563
+ values.push(...input.excludeJobIds);
564
+ }
565
+ const rows = this.db.prepare(`
566
+ SELECT * FROM pibo_jobs
567
+ WHERE ${clauses.join(" AND ")}
568
+ ORDER BY priority DESC, run_at ASC, created_at ASC
569
+ LIMIT ?
570
+ `).all(...values, clampLimit(input.limit, 100));
571
+ const jobs = [];
572
+ for (const row of rows) {
573
+ try {
574
+ jobs.push(jobFromRow(row));
575
+ }
576
+ catch {
577
+ this.quarantineJobRow(row, "payload_malformed");
578
+ }
579
+ }
580
+ return jobs;
455
581
  }
456
582
  listDead(input = {}) {
457
583
  const clauses = [];
@@ -466,6 +592,46 @@ export class PiboReliabilityStore {
466
592
  .all(...values, clampLimit(input.limit, 100));
467
593
  return rows.map(deadJobFromRow);
468
594
  }
595
+ hasLiveJob(jobId) {
596
+ return this.db.prepare("SELECT 1 AS found FROM pibo_jobs WHERE job_id = ?").get(jobId) !== undefined;
597
+ }
598
+ hasJobs(queue) {
599
+ return queue
600
+ ? this.db.prepare("SELECT 1 AS found FROM pibo_jobs WHERE queue = ? LIMIT 1").get(queue) !== undefined
601
+ : this.db.prepare("SELECT 1 AS found FROM pibo_jobs LIMIT 1").get() !== undefined;
602
+ }
603
+ hasRecoverableJobs(queue, includeDeferredPending = false) {
604
+ const timestamp = now();
605
+ return this.db.prepare(`
606
+ SELECT 1 AS found FROM pibo_jobs
607
+ WHERE queue = ?
608
+ AND (
609
+ (state = 'pending' AND (? = 1 OR run_at <= ?))
610
+ OR (state = 'running' AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?)
611
+ )
612
+ AND (expires_at IS NULL OR expires_at > ?)
613
+ LIMIT 1
614
+ `).get(queue, includeDeferredPending ? 1 : 0, timestamp, timestamp, timestamp) !== undefined;
615
+ }
616
+ quarantineJob(jobId, reason, correlation = {}) {
617
+ const row = this.db.prepare("SELECT * FROM pibo_jobs WHERE job_id = ?").get(jobId);
618
+ if (!row)
619
+ return false;
620
+ this.quarantineJobRow(row, reason, correlation);
621
+ return true;
622
+ }
623
+ deadLetter(input) {
624
+ const timestamp = now();
625
+ const jobId = input.jobId ?? `job_${randomUUID()}`;
626
+ this.db.prepare(`
627
+ INSERT OR REPLACE INTO pibo_dead_jobs (
628
+ job_id, queue, payload_json, attempts, max_attempts, idempotency_key, created_at,
629
+ updated_at, expires_at, last_error, dead_at, dead_reason
630
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?)
631
+ `).run(jobId, input.queue, json(input.payload ?? null), Math.max(0, input.attempts ?? 0), Math.max(1, input.maxAttempts ?? 1), input.idempotencyKey ?? null, timestamp, timestamp, input.error ?? input.reason, timestamp, input.reason);
632
+ const row = this.db.prepare("SELECT * FROM pibo_dead_jobs WHERE job_id = ?").get(jobId);
633
+ return deadJobFromRow(row);
634
+ }
469
635
  requeueDead(jobId, input = {}) {
470
636
  const dead = this.db.prepare("SELECT * FROM pibo_dead_jobs WHERE job_id = ?").get(jobId);
471
637
  if (!dead)
@@ -659,17 +825,18 @@ export class PiboReliabilityStore {
659
825
  .get(queue, idempotencyKey);
660
826
  return row ? jobFromRow(row) : undefined;
661
827
  }
662
- getLiveWorkerJob(jobId, workerId, timestamp) {
828
+ getLiveWorkerJob(jobId, workerId, timestamp, claimToken) {
663
829
  return this.db
664
830
  .prepare(`
665
831
  SELECT * FROM pibo_jobs
666
832
  WHERE job_id = ?
667
833
  AND state = 'running'
668
834
  AND worker_id = ?
835
+ AND (? IS NULL OR claim_token = ?)
669
836
  AND claim_expires_at IS NOT NULL
670
837
  AND claim_expires_at > ?
671
838
  `)
672
- .get(jobId, workerId, timestamp);
839
+ .get(jobId, workerId, claimToken ?? null, claimToken ?? null, timestamp);
673
840
  }
674
841
  moveExpiredJobs(timestamp) {
675
842
  const expired = this.db.prepare("SELECT * FROM pibo_jobs WHERE expires_at IS NOT NULL AND expires_at <= ?").all(timestamp);
@@ -718,6 +885,39 @@ export class PiboReliabilityStore {
718
885
  .run(row.job_id, row.queue, row.payload_json, row.attempts, row.max_attempts, row.idempotency_key, row.created_at, timestamp, row.expires_at, error, timestamp, reason);
719
886
  this.db.prepare("DELETE FROM pibo_jobs WHERE job_id = ?").run(row.job_id);
720
887
  }
888
+ quarantineJobRow(row, reason, correlation = {}) {
889
+ this.inImmediateTransaction(() => {
890
+ const current = this.db.prepare("SELECT * FROM pibo_jobs WHERE job_id = ?").get(row.job_id);
891
+ if (!current)
892
+ return;
893
+ const payload = {
894
+ version: 1,
895
+ key: correlation.key ?? current.job_id,
896
+ ...(correlation.piboSessionId ? { piboSessionId: correlation.piboSessionId } : {}),
897
+ ...(correlation.eventId ? { eventId: correlation.eventId } : {}),
898
+ state: { phase: "quarantined", reasonCode: reason },
899
+ };
900
+ const sanitized = {
901
+ ...current,
902
+ payload_json: json(payload),
903
+ idempotency_key: correlation.key ?? null,
904
+ last_error: reason,
905
+ };
906
+ this.moveJobToDead(sanitized, reason, reason, now());
907
+ });
908
+ }
909
+ inImmediateTransaction(operation) {
910
+ this.db.exec("BEGIN IMMEDIATE");
911
+ try {
912
+ const result = operation();
913
+ this.db.exec("COMMIT");
914
+ return result;
915
+ }
916
+ catch (error) {
917
+ this.db.exec("ROLLBACK");
918
+ throw error;
919
+ }
920
+ }
721
921
  requireRun(runId) {
722
922
  const run = this.getRun(runId);
723
923
  if (!run)
@@ -750,6 +950,7 @@ function jobFromRow(row) {
750
950
  priority: row.priority,
751
951
  workerId: row.worker_id ?? undefined,
752
952
  claimExpiresAt: row.claim_expires_at ?? undefined,
953
+ claimToken: row.claim_token,
753
954
  attempts: row.attempts,
754
955
  maxAttempts: row.max_attempts,
755
956
  idempotencyKey: row.idempotency_key ?? undefined,
@@ -763,7 +964,7 @@ function deadJobFromRow(row) {
763
964
  return {
764
965
  jobId: row.job_id,
765
966
  queue: row.queue,
766
- payload: parseJson(row.payload_json),
967
+ payload: safeDeadPayload(row.payload_json, row.dead_reason, row.job_id),
767
968
  attempts: row.attempts,
768
969
  maxAttempts: row.max_attempts,
769
970
  idempotencyKey: row.idempotency_key ?? undefined,
@@ -775,6 +976,14 @@ function deadJobFromRow(row) {
775
976
  deadReason: row.dead_reason,
776
977
  };
777
978
  }
979
+ function safeDeadPayload(value, reason, jobId) {
980
+ try {
981
+ return parseJson(value);
982
+ }
983
+ catch {
984
+ return { version: 1, key: jobId, state: { phase: "quarantined", reasonCode: reason } };
985
+ }
986
+ }
778
987
  function runFromRow(row) {
779
988
  const output = {
780
989
  runId: row.run_id,
@@ -1,4 +1,5 @@
1
1
  import { compareTraceNodes } from "../shared/trace-nodes.js";
2
+ import { parseTraceToolNodeIdentity } from "../shared/trace-tool-identity.js";
2
3
  import { terminalTextValue } from "./terminalValue.js";
3
4
  export const MAX_COMPACT_TERMINAL_IMAGE_PREVIEWS = 20;
4
5
  export const COMPACT_TERMINAL_OUTPUT_PREVIEW_LINES = 5;
@@ -168,9 +169,11 @@ function createRowCandidate(node, turnId) {
168
169
  }
169
170
  export function compactTerminalRowIdentity(node) {
170
171
  if (node.type === "tool.call" || node.type === "tool.result" || node.type === "agent.delegation") {
171
- const toolCallId = node.toolCallId ?? stableKeySuffix(node.stableKey, "tool:");
172
- if (toolCallId)
173
- return `terminal:tool:${toolCallId}`;
172
+ if (node.toolCallId) {
173
+ return parseTraceToolNodeIdentity(node.id)?.qualifier
174
+ ? `terminal:${node.id}`
175
+ : `terminal:tool:${node.toolCallId}`;
176
+ }
174
177
  if (node.type === "agent.delegation" && node.linkedPiboSessionId) {
175
178
  return `terminal:delegation:${node.linkedPiboSessionId}`;
176
179
  }