@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.
- package/dist/apps/chat/agent-profiles.js +4 -1
- package/dist/apps/chat/agent-store.js +196 -3
- package/dist/apps/chat/chat-settings-routes.js +24 -1
- package/dist/apps/chat/data/project-service.js +13 -3
- package/dist/apps/chat/telemetry-retention-service.js +69 -0
- package/dist/apps/chat/web-app.js +23 -12
- package/dist/apps/chat-ui/assets/{dist-oLAGkW6G.js → dist-B9sopUkn.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BtF63vik.js → dist-BDQhMN_4.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-85Cc5Hut.js → dist-BEStK5um.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-D1Laodgo.js → dist-BiBY_4CK.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-eG89IJAV.js → dist-C2OyzisT.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DE8W5WKg.js → dist-C9stINOY.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-ecTM1pdv.js → dist-CQKLsKIo.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-B8jspmzT.js → dist-CtSZyFkJ.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DcME5mJj.js → dist-DPLEwPsG.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CQXtvBTs.js → dist-DRTq4wrN.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BGqK-7Ep.js → dist-F2W_jRom.js} +1 -1
- package/dist/apps/chat-ui/assets/index-D_60RTKn.css +1 -0
- package/dist/apps/chat-ui/assets/index-iaNLOwJ-.js +157 -0
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/{index-CRUSv6iR.js → index-lA76A7Pc.js} +4 -4
- package/dist/apps/chat-vscode-web/index.html +1 -1
- package/dist/apps/cli-ui/inkMarkdown.js +8 -3
- package/dist/apps/cli-ui/inkSyntaxHighlighter.js +166 -0
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/{pibo-vscode-ext-1.4.5.vsix → pibo-vscode-ext-1.5.0.vsix} +0 -0
- package/dist/cli.js +20 -0
- package/dist/core/runtime-telemetry.js +11 -3
- package/dist/core/session-router.js +7 -1
- package/dist/core/telemetry-retention-settings.js +34 -0
- package/dist/core/user-settings.js +11 -0
- package/dist/data/telemetry.js +11 -0
- package/dist/mcp/agent-context.js +10 -6
- package/dist/mcp/commands/info.js +19 -7
- package/dist/mcp/config.js +98 -65
- package/dist/plugins/builtin.js +15 -1
- package/dist/session-ui/terminalRows.js +56 -2
- package/dist/shared/trace-nodes.js +12 -0
- package/dist/skills/cli.js +25 -1
- package/dist/tools/guides.js +71 -0
- package/dist/tools/index.js +7 -3
- package/dist/tools/python-runtime.js +2 -2
- package/dist/tools/registry.js +25 -1
- package/package.json +93 -93
- package/skills/builtin/graphify/SKILL.md +52 -0
- package/dist/apps/chat-ui/assets/index-B-qaya1G.css +0 -1
- 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
|
-
|
|
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 {
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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);
|