@pasko70/pibo 1.4.5 → 1.5.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 (47) hide show
  1. package/dist/apps/chat/agent-profiles.js +4 -1
  2. package/dist/apps/chat/agent-store.js +196 -3
  3. package/dist/apps/chat/chat-settings-routes.js +24 -1
  4. package/dist/apps/chat/data/project-service.js +13 -3
  5. package/dist/apps/chat/telemetry-retention-service.js +69 -0
  6. package/dist/apps/chat/web-app.js +23 -12
  7. package/dist/apps/chat-ui/assets/{dist-oLAGkW6G.js → dist-B9sopUkn.js} +1 -1
  8. package/dist/apps/chat-ui/assets/{dist-BtF63vik.js → dist-BDQhMN_4.js} +1 -1
  9. package/dist/apps/chat-ui/assets/{dist-85Cc5Hut.js → dist-BEStK5um.js} +1 -1
  10. package/dist/apps/chat-ui/assets/{dist-D1Laodgo.js → dist-BiBY_4CK.js} +1 -1
  11. package/dist/apps/chat-ui/assets/{dist-eG89IJAV.js → dist-C2OyzisT.js} +1 -1
  12. package/dist/apps/chat-ui/assets/{dist-DE8W5WKg.js → dist-C9stINOY.js} +1 -1
  13. package/dist/apps/chat-ui/assets/{dist-ecTM1pdv.js → dist-CQKLsKIo.js} +1 -1
  14. package/dist/apps/chat-ui/assets/{dist-B8jspmzT.js → dist-CtSZyFkJ.js} +1 -1
  15. package/dist/apps/chat-ui/assets/{dist-DcME5mJj.js → dist-DPLEwPsG.js} +1 -1
  16. package/dist/apps/chat-ui/assets/{dist-CQXtvBTs.js → dist-DRTq4wrN.js} +1 -1
  17. package/dist/apps/chat-ui/assets/{dist-BGqK-7Ep.js → dist-F2W_jRom.js} +1 -1
  18. package/dist/apps/chat-ui/assets/index-D_60RTKn.css +1 -0
  19. package/dist/apps/chat-ui/assets/index-iaNLOwJ-.js +157 -0
  20. package/dist/apps/chat-ui/index.html +2 -2
  21. package/dist/apps/chat-vscode-web/assets/{index-CRUSv6iR.js → index-lA76A7Pc.js} +4 -4
  22. package/dist/apps/chat-vscode-web/index.html +1 -1
  23. package/dist/apps/cli-ui/inkMarkdown.js +8 -3
  24. package/dist/apps/cli-ui/inkSyntaxHighlighter.js +166 -0
  25. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  26. package/dist/apps/vscode-artifacts/{pibo-vscode-ext-1.4.5.vsix → pibo-vscode-ext-1.5.0.vsix} +0 -0
  27. package/dist/cli.js +20 -0
  28. package/dist/core/runtime-telemetry.js +11 -3
  29. package/dist/core/session-router.js +7 -1
  30. package/dist/core/telemetry-retention-settings.js +34 -0
  31. package/dist/core/user-settings.js +11 -0
  32. package/dist/data/telemetry.js +11 -0
  33. package/dist/mcp/agent-context.js +10 -6
  34. package/dist/mcp/commands/info.js +19 -7
  35. package/dist/mcp/config.js +98 -65
  36. package/dist/plugins/builtin.js +15 -1
  37. package/dist/session-ui/terminalRows.js +56 -2
  38. package/dist/shared/trace-nodes.js +12 -0
  39. package/dist/skills/cli.js +25 -1
  40. package/dist/tools/guides.js +71 -0
  41. package/dist/tools/index.js +7 -3
  42. package/dist/tools/python-runtime.js +2 -2
  43. package/dist/tools/registry.js +25 -1
  44. package/package.json +93 -93
  45. package/skills/builtin/graphify/SKILL.md +52 -0
  46. package/dist/apps/chat-ui/assets/index-B-qaya1G.css +0 -1
  47. package/dist/apps/chat-ui/assets/index-D4uifikB.js +0 -165
@@ -3,7 +3,7 @@ export function createCustomAgentProfileDefinition(agent, options = {}) {
3
3
  const shouldWarnMissingReferences = options.missingReferenceMode !== "silent";
4
4
  return {
5
5
  name: agent.profileName,
6
- aliases: [agent.id, `custom-agent:${agent.id}`],
6
+ aliases: uniqueAliases([agent.id, `custom-agent:${agent.id}`, ...agent.profileAliases], agent.profileName),
7
7
  description: agent.description || agent.displayName,
8
8
  create(context) {
9
9
  const builder = new InitialSessionContextBuilder(agent.profileName)
@@ -59,6 +59,9 @@ export function createCustomAgentProfileDefinition(agent, options = {}) {
59
59
  },
60
60
  };
61
61
  }
62
+ function uniqueAliases(aliases, profileName) {
63
+ return [...new Set(aliases.filter((alias) => alias && alias !== profileName))];
64
+ }
62
65
  function isUnknownContextFileError(error, contextFileKey) {
63
66
  return error instanceof Error && error.message === `Unknown context file "${contextFileKey}"`;
64
67
  }
@@ -15,6 +15,7 @@ export class CustomAgentStore {
15
15
  mkdirSync(dirname(resolvedPath), { recursive: true });
16
16
  this.db = new DatabaseSync(resolvedPath);
17
17
  this.db.exec("PRAGMA busy_timeout = 5000");
18
+ this.db.exec("PRAGMA foreign_keys = ON");
18
19
  if (resolvedPath !== ":memory:")
19
20
  this.db.exec("PRAGMA journal_mode = WAL");
20
21
  this.db.exec(`
@@ -47,6 +48,7 @@ export class CustomAgentStore {
47
48
  );
48
49
 
49
50
  `);
51
+ this.migrateProfileAliasTable();
50
52
  this.migrateArchivedAtColumn();
51
53
  this.migrateAutoContextFilesColumn();
52
54
  this.migrateMcpServersColumn();
@@ -55,6 +57,7 @@ export class CustomAgentStore {
55
57
  this.migrateThinkingLevelColumn();
56
58
  this.migrateThinkingOptionColumns();
57
59
  this.migrateBuiltinToolNamesColumn();
60
+ this.migrateAgentHistory();
58
61
  this.migrateLegacyProfileNames();
59
62
  this.migrateDuplicateProfileNames();
60
63
  }
@@ -63,12 +66,13 @@ export class CustomAgentStore {
63
66
  this.migrateDuplicateProfileNames();
64
67
  const archivedClause = options.includeArchived ? "" : " AND archived_at IS NULL";
65
68
  const rows = this.db.prepare(`SELECT * FROM chat_agents WHERE 1 = 1${archivedClause} ORDER BY updated_at DESC`).all();
66
- return rows.map(agentFromRow);
69
+ const profileAliases = this.profileAliasesByAgentId();
70
+ return rows.map((row) => agentFromRow(row, profileAliases.get(row.id) ?? []));
67
71
  }
68
72
  get(id) {
69
73
  this.migrateLegacyProfileNames();
70
74
  const row = this.db.prepare("SELECT * FROM chat_agents WHERE id = ?").get(id);
71
- return row ? agentFromRow(row) : undefined;
75
+ return row ? agentFromRow(row, this.profileAliasesByAgentId().get(row.id) ?? []) : undefined;
72
76
  }
73
77
  create(input) {
74
78
  this.migrateLegacyProfileNames();
@@ -80,6 +84,7 @@ export class CustomAgentStore {
80
84
  id,
81
85
  profileName,
82
86
  displayName: input.displayName,
87
+ profileAliases: [],
83
88
  description: input.description,
84
89
  nativeTools: [...(input.nativeTools ?? [])],
85
90
  skills: [...(input.skills ?? [])],
@@ -182,6 +187,7 @@ export class CustomAgentStore {
182
187
  return this.get(id);
183
188
  }
184
189
  delete(id) {
190
+ this.db.prepare("DELETE FROM chat_agent_profile_aliases WHERE agent_id = ?").run(id);
185
191
  const result = this.db.prepare("DELETE FROM chat_agents WHERE id = ?").run(id);
186
192
  return Number(result.changes ?? 0) > 0;
187
193
  }
@@ -225,6 +231,20 @@ export class CustomAgentStore {
225
231
  const row = this.db.prepare("SELECT id FROM chat_agents WHERE profile_name = ?").get(profileName);
226
232
  if (row && row.id !== currentId)
227
233
  throw new Error(`Agent name "${profileName}" already exists`);
234
+ const alias = this.db.prepare("SELECT agent_id FROM chat_agent_profile_aliases WHERE old_profile_name = ?").get(profileName);
235
+ if (alias && alias.agent_id !== currentId)
236
+ throw new Error(`Agent name "${profileName}" already exists`);
237
+ }
238
+ profileAliasesByAgentId() {
239
+ const rows = this.db.prepare("SELECT agent_id, old_profile_name FROM chat_agent_profile_aliases ORDER BY created_at ASC, old_profile_name ASC").all();
240
+ const aliases = new Map();
241
+ for (const row of rows) {
242
+ const values = aliases.get(row.agent_id) ?? [];
243
+ if (!values.includes(row.old_profile_name))
244
+ values.push(row.old_profile_name);
245
+ aliases.set(row.agent_id, values);
246
+ }
247
+ return aliases;
228
248
  }
229
249
  migrateLegacyProfileNames() {
230
250
  const rows = this.db.prepare("SELECT id, profile_name, display_name FROM chat_agents ORDER BY created_at ASC").all();
@@ -255,6 +275,45 @@ export class CustomAgentStore {
255
275
  .run(nextName, nextName, row.id);
256
276
  }
257
277
  }
278
+ migrateProfileAliasTable() {
279
+ this.db.exec(`
280
+ CREATE TABLE IF NOT EXISTS chat_agent_profile_aliases (
281
+ id TEXT PRIMARY KEY,
282
+ agent_id TEXT NOT NULL,
283
+ old_profile_name TEXT NOT NULL UNIQUE,
284
+ new_profile_name TEXT NOT NULL,
285
+ created_at TEXT NOT NULL,
286
+ FOREIGN KEY(agent_id) REFERENCES chat_agents(id) ON DELETE CASCADE
287
+ );
288
+
289
+ CREATE TRIGGER IF NOT EXISTS chat_agents_profile_alias_insert
290
+ AFTER UPDATE OF profile_name ON chat_agents
291
+ WHEN OLD.profile_name IS NOT NEW.profile_name
292
+ BEGIN
293
+ INSERT INTO chat_agent_profile_aliases (
294
+ id,
295
+ agent_id,
296
+ old_profile_name,
297
+ new_profile_name,
298
+ created_at
299
+ ) VALUES (
300
+ 'alias_' || lower(hex(randomblob(16))),
301
+ NEW.id,
302
+ OLD.profile_name,
303
+ NEW.profile_name,
304
+ strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
305
+ )
306
+ ON CONFLICT(old_profile_name) DO UPDATE SET
307
+ agent_id = excluded.agent_id,
308
+ new_profile_name = excluded.new_profile_name,
309
+ created_at = excluded.created_at
310
+ WHERE chat_agent_profile_aliases.agent_id = excluded.agent_id;
311
+
312
+ DELETE FROM chat_agent_profile_aliases
313
+ WHERE agent_id = NEW.id AND old_profile_name = NEW.profile_name;
314
+ END;
315
+ `);
316
+ }
258
317
  tableColumns() {
259
318
  return new Set(this.db.prepare("PRAGMA table_info(chat_agents)").all().map((column) => column.name));
260
319
  }
@@ -321,15 +380,149 @@ export class CustomAgentStore {
321
380
  this.db.prepare("ALTER TABLE chat_agents ADD COLUMN builtin_tool_names_json TEXT NOT NULL DEFAULT '[\"read\",\"bash\",\"edit\",\"write\"]'").run();
322
381
  }
323
382
  }
383
+ migrateAgentHistory() {
384
+ this.db.exec(`
385
+ CREATE TABLE IF NOT EXISTS chat_agent_events (
386
+ id TEXT PRIMARY KEY,
387
+ agent_id TEXT NOT NULL,
388
+ event_type TEXT NOT NULL CHECK (event_type IN ('updated', 'deleted')),
389
+ field_name TEXT,
390
+ old_value TEXT,
391
+ new_value TEXT,
392
+ old_profile_name TEXT,
393
+ new_profile_name TEXT,
394
+ old_display_name TEXT,
395
+ new_display_name TEXT,
396
+ recorded_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
397
+ );
398
+
399
+ CREATE INDEX IF NOT EXISTS chat_agent_events_agent_id_idx
400
+ ON chat_agent_events(agent_id, recorded_at);
401
+
402
+ CREATE INDEX IF NOT EXISTS chat_agent_events_profile_name_idx
403
+ ON chat_agent_events(old_profile_name, new_profile_name, recorded_at);
404
+
405
+ CREATE TRIGGER IF NOT EXISTS chat_agents_profile_name_history_update
406
+ AFTER UPDATE OF profile_name ON chat_agents
407
+ FOR EACH ROW
408
+ WHEN OLD.profile_name IS NOT NEW.profile_name
409
+ BEGIN
410
+ INSERT INTO chat_agent_events (
411
+ id,
412
+ agent_id,
413
+ event_type,
414
+ field_name,
415
+ old_value,
416
+ new_value,
417
+ old_profile_name,
418
+ new_profile_name,
419
+ old_display_name,
420
+ new_display_name,
421
+ recorded_at
422
+ ) VALUES (
423
+ 'agent_event_' || lower(hex(randomblob(16))),
424
+ NEW.id,
425
+ 'updated',
426
+ 'profile_name',
427
+ OLD.profile_name,
428
+ NEW.profile_name,
429
+ OLD.profile_name,
430
+ NEW.profile_name,
431
+ OLD.display_name,
432
+ NEW.display_name,
433
+ strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
434
+ );
435
+ END;
436
+
437
+ CREATE TRIGGER IF NOT EXISTS chat_agents_display_name_history_update
438
+ AFTER UPDATE OF display_name ON chat_agents
439
+ FOR EACH ROW
440
+ WHEN OLD.display_name IS NOT NEW.display_name
441
+ BEGIN
442
+ INSERT INTO chat_agent_events (
443
+ id,
444
+ agent_id,
445
+ event_type,
446
+ field_name,
447
+ old_value,
448
+ new_value,
449
+ old_profile_name,
450
+ new_profile_name,
451
+ old_display_name,
452
+ new_display_name,
453
+ recorded_at
454
+ ) VALUES (
455
+ 'agent_event_' || lower(hex(randomblob(16))),
456
+ NEW.id,
457
+ 'updated',
458
+ 'display_name',
459
+ OLD.display_name,
460
+ NEW.display_name,
461
+ OLD.profile_name,
462
+ NEW.profile_name,
463
+ OLD.display_name,
464
+ NEW.display_name,
465
+ strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
466
+ );
467
+ END;
468
+
469
+ CREATE TRIGGER IF NOT EXISTS chat_agents_history_delete
470
+ AFTER DELETE ON chat_agents
471
+ FOR EACH ROW
472
+ BEGIN
473
+ INSERT INTO chat_agent_events (
474
+ id,
475
+ agent_id,
476
+ event_type,
477
+ field_name,
478
+ old_value,
479
+ new_value,
480
+ old_profile_name,
481
+ new_profile_name,
482
+ old_display_name,
483
+ new_display_name,
484
+ recorded_at
485
+ ) VALUES (
486
+ 'agent_event_' || lower(hex(randomblob(16))),
487
+ OLD.id,
488
+ 'deleted',
489
+ NULL,
490
+ OLD.profile_name,
491
+ NULL,
492
+ OLD.profile_name,
493
+ NULL,
494
+ OLD.display_name,
495
+ NULL,
496
+ strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
497
+ );
498
+ END;
499
+
500
+ CREATE VIEW IF NOT EXISTS chat_agent_history AS
501
+ SELECT
502
+ id,
503
+ agent_id,
504
+ event_type,
505
+ field_name,
506
+ old_value,
507
+ new_value,
508
+ old_profile_name,
509
+ new_profile_name,
510
+ old_display_name,
511
+ new_display_name,
512
+ recorded_at
513
+ FROM chat_agent_events;
514
+ `);
515
+ }
324
516
  }
325
517
  export function createDefaultCustomAgentStore(_cwd) {
326
518
  return new CustomAgentStore(piboHomePath("chat-agents.sqlite"));
327
519
  }
328
- function agentFromRow(row) {
520
+ function agentFromRow(row, profileAliases) {
329
521
  return {
330
522
  id: row.id,
331
523
  profileName: row.profile_name,
332
524
  displayName: row.display_name,
525
+ profileAliases: profileAliases.filter((alias) => alias !== row.profile_name),
333
526
  description: row.description ?? undefined,
334
527
  nativeTools: parseStringArray(row.native_tools_json),
335
528
  skills: parseStringArray(row.skills_json),
@@ -1,9 +1,11 @@
1
1
  import { readPiboBasePrompt, savePiboCustomBasePrompt, setPiboBasePromptMode } from "../../core/base-prompt.js";
2
2
  import { readPiboCompactionPrompt, savePiboCustomCompactionPrompt, setPiboCompactionPromptMode } from "../../core/compaction-prompt.js";
3
- import { loadPiboUserSettings, sanitizeShortcutSettings, sanitizeTimezone, updatePiboUserSettings } from "../../core/user-settings.js";
3
+ import { sanitizeTelemetryRetentionDays, sanitizeTelemetryRetentionSettings } from "../../core/telemetry-retention-settings.js";
4
+ import { loadPiboUserSettings, sanitizeShortcutSettings, sanitizeTimezone, updatePiboUserSettings, updateTelemetryRetentionLastPrunedAt } from "../../core/user-settings.js";
4
5
  import { PiboWebHttpError, readJsonBody, responseJson } from "../../web/http.js";
5
6
  import { CHAT_WEB_API_PREFIX } from "./chat-api-routes.js";
6
7
  import { normalizeBasePromptMarkdown, normalizeBasePromptMode, normalizeCompactionPromptMarkdown, normalizeCompactionPromptMode, updateChatModelDefaults, } from "./chat-request-normalizers.js";
8
+ import { pruneTelemetryOlderThan } from "./telemetry-retention-service.js";
7
9
  export function chatSettingsRoute(pathname, method) {
8
10
  if (pathname === `${CHAT_WEB_API_PREFIX}/model-defaults` && method === "PATCH")
9
11
  return { kind: "model-defaults" };
@@ -11,6 +13,8 @@ export function chatSettingsRoute(pathname, method) {
11
13
  return { kind: "user-settings", action: "read" };
12
14
  if (pathname === `${CHAT_WEB_API_PREFIX}/user-settings` && method === "PATCH")
13
15
  return { kind: "user-settings", action: "update" };
16
+ if (pathname === `${CHAT_WEB_API_PREFIX}/telemetry-retention/prune` && method === "POST")
17
+ return { kind: "telemetry-retention", action: "prune" };
14
18
  if (pathname === `${CHAT_WEB_API_PREFIX}/base-prompt` && method === "GET")
15
19
  return { kind: "base-prompt", action: "read" };
16
20
  if (pathname === `${CHAT_WEB_API_PREFIX}/base-prompt` && method === "PATCH")
@@ -44,6 +48,18 @@ export async function handleChatSettingsRoute(input) {
44
48
  const body = await readJsonBody(request);
45
49
  return responseJson({ userSettings: updatePiboUserSettings(userSettingsPatch(body)) });
46
50
  }
51
+ if (route.kind === "telemetry-retention") {
52
+ if (!input.dataStore)
53
+ throw new PiboWebHttpError("Telemetry retention store unavailable", 503);
54
+ const body = await readJsonBody(request);
55
+ const days = sanitizeTelemetryRetentionDays(body.days);
56
+ if (!days)
57
+ throw new PiboWebHttpError("Invalid telemetry retention days", 400);
58
+ const result = pruneTelemetryOlderThan({ dataStore: input.dataStore, days, apply: body.dryRun !== true });
59
+ if (result.applied)
60
+ updateTelemetryRetentionLastPrunedAt(new Date().toISOString());
61
+ return responseJson({ telemetryRetention: result });
62
+ }
47
63
  if (route.kind === "base-prompt") {
48
64
  if (route.action === "read")
49
65
  return responseJson({ basePrompt: await readPiboBasePrompt(cwd) });
@@ -71,5 +87,12 @@ function userSettingsPatch(body) {
71
87
  }
72
88
  if (body.shortcuts !== undefined)
73
89
  patch.shortcuts = sanitizeShortcutSettings(body.shortcuts);
90
+ if (body.telemetryRetention !== undefined) {
91
+ const current = loadPiboUserSettings().telemetryRetention;
92
+ patch.telemetryRetention = {
93
+ ...sanitizeTelemetryRetentionSettings(body.telemetryRetention),
94
+ ...(current.lastPrunedAt ? { lastPrunedAt: current.lastPrunedAt } : {}),
95
+ };
96
+ }
74
97
  return patch;
75
98
  }
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { mkdirSync, rmSync } from "node:fs";
2
+ import { mkdirSync, rmSync, statSync } from "node:fs";
3
3
  import { dirname, isAbsolute, resolve } from "node:path";
4
4
  import { DatabaseSync } from "node:sqlite";
5
5
  import { piboHomePath } from "../../../core/pibo-home.js";
@@ -49,8 +49,7 @@ export class ChatProjectService {
49
49
  const projectFolder = resolve(normalizeProjectFolder(input.projectFolder));
50
50
  this.assertNameAvailable(name);
51
51
  this.assertFolderAvailable(projectFolder);
52
- if (input.createFolder)
53
- mkdirSync(projectFolder, { recursive: true });
52
+ this.ensureProjectFolderUsable(projectFolder);
54
53
  const now = new Date().toISOString();
55
54
  const id = `prj_${randomUUID()}`;
56
55
  this.db.prepare(`INSERT INTO projects (id, name, description, project_folder, configuration_status, metadata_json, created_at, updated_at)
@@ -577,6 +576,17 @@ export class ChatProjectService {
577
576
  if (existing)
578
577
  throw new Error("Project folder already exists");
579
578
  }
579
+ ensureProjectFolderUsable(folder) {
580
+ try {
581
+ mkdirSync(folder, { recursive: true });
582
+ if (!statSync(folder).isDirectory())
583
+ throw new Error("Project folder is not a directory");
584
+ }
585
+ catch (error) {
586
+ const message = error instanceof Error ? error.message : String(error);
587
+ throw new Error(`Project folder cannot be created or used: ${message}`);
588
+ }
589
+ }
580
590
  }
581
591
  function projectFromRow(row) {
582
592
  return {
@@ -0,0 +1,69 @@
1
+ import { telemetryRetentionCutoff, } from "../../core/telemetry-retention-settings.js";
2
+ export const TELEMETRY_RETENTION_CLASSES = [
3
+ "live",
4
+ "diagnostic",
5
+ "provider_event",
6
+ "payload_preview",
7
+ "incident",
8
+ ];
9
+ export const DEFAULT_TELEMETRY_RETENTION_MAINTENANCE_INTERVAL_MS = 24 * 60 * 60 * 1000;
10
+ export function pruneTelemetryOlderThan(input) {
11
+ const cutoff = telemetryRetentionCutoff(input.days, input.now);
12
+ const results = TELEMETRY_RETENTION_CLASSES.map((retentionClass) => input.dataStore.telemetry.prune({ retentionClass, before: cutoff, apply: input.apply }));
13
+ return {
14
+ cutoff,
15
+ days: input.days,
16
+ applied: input.apply === true,
17
+ results,
18
+ rowsDeleted: results.reduce((sum, result) => sum + result.rowsDeleted, 0),
19
+ bytesMatched: results.reduce((sum, result) => sum + result.bytesMatched, 0),
20
+ };
21
+ }
22
+ export function isTelemetryRetentionMaintenanceDue(input) {
23
+ if (input.state.running)
24
+ return false;
25
+ const now = input.now ?? new Date();
26
+ const intervalMs = input.intervalMs ?? DEFAULT_TELEMETRY_RETENTION_MAINTENANCE_INTERVAL_MS;
27
+ return !(input.state.lastCheckedAt && now.getTime() - input.state.lastCheckedAt < intervalMs);
28
+ }
29
+ export function maybeRunTelemetryRetentionMaintenance(input) {
30
+ if (!input.settings.enabled)
31
+ return;
32
+ if (input.state.running)
33
+ return;
34
+ const now = input.now ?? new Date();
35
+ const intervalMs = input.intervalMs ?? DEFAULT_TELEMETRY_RETENTION_MAINTENANCE_INTERVAL_MS;
36
+ if (input.state.lastCheckedAt && now.getTime() - input.state.lastCheckedAt < intervalMs)
37
+ return;
38
+ if (!isPersistentRetentionDue(input.settings.lastPrunedAt, now, intervalMs)) {
39
+ input.state.lastCheckedAt = now.getTime();
40
+ return;
41
+ }
42
+ if (hasActiveRuntimeWork(input.context))
43
+ return;
44
+ input.state.lastCheckedAt = now.getTime();
45
+ input.state.running = true;
46
+ setTimeout(() => {
47
+ try {
48
+ pruneTelemetryOlderThan({ dataStore: input.dataStore, days: input.settings.days, now, apply: true });
49
+ input.onPruned?.(now.toISOString());
50
+ }
51
+ finally {
52
+ input.state.running = false;
53
+ }
54
+ }, 0).unref?.();
55
+ }
56
+ export function isPersistentRetentionDue(lastPrunedAt, now, intervalMs) {
57
+ if (!lastPrunedAt)
58
+ return true;
59
+ const lastPrunedMs = Date.parse(lastPrunedAt);
60
+ if (!Number.isFinite(lastPrunedMs))
61
+ return true;
62
+ return now.getTime() - lastPrunedMs >= intervalMs;
63
+ }
64
+ export function hasActiveRuntimeWork(context) {
65
+ const statuses = context.channelContext.listSessionRuntimeStatuses?.()
66
+ ?? context.channelContext.listSessions?.().map((session) => context.channelContext.getSessionRuntimeStatus?.(session.id)).filter((status) => status !== undefined)
67
+ ?? [];
68
+ return statuses.some((status) => Boolean(status?.processing || status?.streaming || (status?.queuedMessages ?? 0) > 0));
69
+ }
@@ -12,7 +12,8 @@ import { withWorkflowSessionKind } from "../../sessions/workflow-session-kind.js
12
12
  import { CustomAgentStore, createDefaultCustomAgentStore, } from "./agent-store.js";
13
13
  import { loadPiboModelDefaults, } from "../../core/model-defaults.js";
14
14
  import { inspectPiboContextBuild } from "../../core/context-build.js";
15
- import { loadPiboUserSettings } from "../../core/user-settings.js";
15
+ import { loadPiboUserSettings, updateTelemetryRetentionLastPrunedAt } from "../../core/user-settings.js";
16
+ import { isTelemetryRetentionMaintenanceDue, maybeRunTelemetryRetentionMaintenance } from "./telemetry-retention-service.js";
16
17
  import { loadModelCatalog } from "./model-catalog.js";
17
18
  import { createCustomAgentProfileDefinition } from "./agent-profiles.js";
18
19
  import { createDefaultPiboReliabilityStore, PiboReliabilityStore } from "../../reliability/store.js";
@@ -707,7 +708,7 @@ function requireAgentProfileNameAvailable(state, context, profileName, currentAg
707
708
  }
708
709
  }
709
710
  const matchedProfile = context.channelContext.getProfiles?.().find((profile) => profile.name === profileName || profile.aliases.includes(profileName));
710
- if (matchedProfile)
711
+ if (matchedProfile && matchedProfile.name !== currentAgent?.profileName)
711
712
  throw new PiboWebHttpError(`Agent name "${profileName}" conflicts with an existing profile`, 400);
712
713
  }
713
714
  function requireSharedAgent(agent) {
@@ -1758,13 +1759,14 @@ function normalizeAgentDeleteConfirmation(value) {
1758
1759
  }
1759
1760
  return value.trim();
1760
1761
  }
1761
- function deleteSessionsForAgentProfile(state, context, webSession, profileName) {
1762
+ function deleteSessionsForAgentProfile(state, context, webSession, profileNames) {
1762
1763
  const deleteSession = context.channelContext.deleteSession;
1763
1764
  if (!deleteSession)
1764
1765
  throw new PiboWebHttpError("Session deletion is not available", 501);
1765
1766
  const ownedSessions = listSharedSessions(context);
1766
1767
  const sessionsById = new Map(ownedSessions.map((session) => [session.id, session]));
1767
- const ids = new Set(ownedSessions.filter((session) => session.profile === profileName).map((session) => session.id));
1768
+ const profileNameSet = new Set(profileNames);
1769
+ const ids = new Set(ownedSessions.filter((session) => profileNameSet.has(session.profile)).map((session) => session.id));
1768
1770
  let changed = true;
1769
1771
  while (changed) {
1770
1772
  changed = false;
@@ -2202,7 +2204,9 @@ function createEventStream(input) {
2202
2204
  async function buildProjectsBootstrap(input) {
2203
2205
  const sharedDefaultProject = input.state.projectService.ensureSharedDefaultProject();
2204
2206
  const selectedProject = input.projectId ? requireSharedProject(input.state, input.webSession, input.projectId, { includeArchived: true }) : sharedDefaultProject;
2205
- let storedProjectSessions = input.state.projectService.listProjectSessions(selectedProject.id, { includeArchived: input.includeArchived });
2207
+ let storedProjectSessions = input.state.projectService
2208
+ .listProjectSessions(selectedProject.id, { includeArchived: input.includeArchived })
2209
+ .filter((projectSession) => projectSession.workflowId === "simple-chat");
2206
2210
  if (selectedProject.id === sharedDefaultProject.id && storedProjectSessions.length === 0) {
2207
2211
  const session = createProjectChatSession({
2208
2212
  state: input.state,
@@ -2214,7 +2218,7 @@ async function buildProjectsBootstrap(input) {
2214
2218
  });
2215
2219
  storedProjectSessions = [input.state.projectService.getProjectSession(session.id)];
2216
2220
  }
2217
- const projectSessions = storedProjectSessions.map((projectSession) => enrichProjectSessionWorkflowWaitTokens(input.state, enrichProjectSessionWorkflowDefinitionLink(input.state, projectSession)));
2221
+ const projectSessions = storedProjectSessions;
2218
2222
  const rootSessions = projectSessions
2219
2223
  .map((projectSession) => input.context.channelContext.getSession(projectSession.piboSessionId))
2220
2224
  .filter((session) => Boolean(session));
@@ -2224,17 +2228,13 @@ async function buildProjectsBootstrap(input) {
2224
2228
  indexSharedSessions(input.state.sessionQuery, sessions);
2225
2229
  const nodes = await buildSessionNodes(sessions, input.state.sessionQuery.listSessions(), selectedProject.projectFolder, new Map(), { skipPiMetadataFallback: true });
2226
2230
  applyProjectSessionArchiveState(nodes, new Map(projectSessions.map((projectSession) => [projectSession.piboSessionId, Boolean(projectSession.archived)])));
2227
- const workflowLifecycleEvents = input.state.workflowLifecycleEventStore.listEvents({
2228
- projectId: selectedProject.id,
2229
- limit: 100,
2230
- });
2231
2231
  return {
2232
2232
  identity: input.webSession.authSession.identity,
2233
2233
  sharedDefaultProject,
2234
2234
  project: selectedProject,
2235
2235
  projects: listSharedProjects(input.state, input.webSession, { includeArchived: input.includeArchived }),
2236
2236
  projectSessions,
2237
- workflowLifecycleEvents,
2237
+ workflowLifecycleEvents: [],
2238
2238
  ...(selectedSession ? { session: selectedSession, selectedPiboSessionId: selectedSession.id } : {}),
2239
2239
  selectedProjectId: selectedProject.id,
2240
2240
  sessions: nodes,
@@ -2688,6 +2688,7 @@ export function createChatWebApp(options = {}) {
2688
2688
  workflowTombstoneStore: new ChatWorkflowTombstoneStore(dataStore),
2689
2689
  workflowLifecycleEventStore: new ChatWorkflowLifecycleEventStore(dataStore),
2690
2690
  workflowPromptAssetStore: new ChatWorkflowPromptAssetStore(dataStore),
2691
+ telemetryRetentionMaintenance: {},
2691
2692
  };
2692
2693
  const requireSession = (request, context) => context.requireSession({
2693
2694
  request,
@@ -2698,6 +2699,15 @@ export function createChatWebApp(options = {}) {
2698
2699
  apiPrefix: CHAT_WEB_API_PREFIX,
2699
2700
  async handleRequest(request, context) {
2700
2701
  const url = new URL(request.url);
2702
+ if (isTelemetryRetentionMaintenanceDue({ state: state.telemetryRetentionMaintenance })) {
2703
+ maybeRunTelemetryRetentionMaintenance({
2704
+ state: state.telemetryRetentionMaintenance,
2705
+ dataStore: state.dataStore,
2706
+ settings: loadPiboUserSettings().telemetryRetention,
2707
+ context,
2708
+ onPruned: updateTelemetryRetentionLastPrunedAt,
2709
+ });
2710
+ }
2701
2711
  ensureEventIndexing(state, context);
2702
2712
  ensureCustomAgentProfiles(state, context);
2703
2713
  syncChatUserSkills({
@@ -3417,6 +3427,7 @@ export function createChatWebApp(options = {}) {
3417
3427
  route: settingsRoute,
3418
3428
  request,
3419
3429
  cwd: process.cwd(),
3430
+ dataStore: state.dataStore,
3420
3431
  });
3421
3432
  if (chatSettingsRouteInvalidatesBootstrapCatalog(settingsRoute))
3422
3433
  invalidateBootstrapCatalogCache(state);
@@ -3504,7 +3515,7 @@ export function createChatWebApp(options = {}) {
3504
3515
  if (confirmName !== agent.profileName) {
3505
3516
  throw new PiboWebHttpError(`Type "${agent.profileName}" to permanently delete this agent and its sessions.`, 400);
3506
3517
  }
3507
- const deletedSessionIds = deleteSessionsForAgentProfile(state, context, webSession, agent.profileName);
3518
+ const deletedSessionIds = deleteSessionsForAgentProfile(state, context, webSession, [agent.profileName, ...agent.profileAliases]);
3508
3519
  state.agentStore.delete(agent.id);
3509
3520
  context.channelContext.removeProfile?.(agent.profileName);
3510
3521
  invalidateBootstrapCatalogCache(state);