@pasko70/pibo 3.0.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -6
- package/compute-image/Dockerfile +47 -0
- package/compute-image/Dockerfile.dockerignore +5 -0
- package/dist/agent-runtime/context-build.js +26 -2
- package/dist/agent-runtime/contract.js +78 -28
- package/dist/agent-runtime/portable-history.js +19 -9
- package/dist/agent-runtime/registry.js +8 -2
- package/dist/agent-runtime/routed-session.js +120 -15
- package/dist/agent-runtimes/codex-native/adapter.js +12 -0
- package/dist/agent-runtimes/codex-native/resource-delivery.js +35 -7
- package/dist/agent-runtimes/omp/adapter.js +3 -7
- package/dist/agent-runtimes/pi/adapter.js +5 -3
- package/dist/agent-runtimes/pi/routed-session.js +59 -12
- package/dist/agent-runtimes/pi/runtime.js +6 -0
- package/dist/apps/chat/agent-profiles.js +31 -2
- package/dist/apps/chat/agent-store.js +98 -6
- package/dist/apps/chat/chat-request-normalizers.js +31 -0
- package/dist/apps/chat/data/chat-data-mappers.js +61 -11
- package/dist/apps/chat/data/project-service.js +119 -15
- package/dist/apps/chat/data/room-service.js +21 -0
- package/dist/apps/chat/data/session-query-service.js +33 -10
- package/dist/apps/chat/data/timeline-query-service.js +60 -35
- package/dist/apps/chat/loop-api.js +10 -2
- package/dist/apps/chat/output-compactor.js +144 -21
- package/dist/apps/chat/output-event-policy.js +179 -2
- package/dist/apps/chat/stream.js +16 -7
- package/dist/apps/chat/trace-v2.js +1 -0
- package/dist/apps/chat/web-app.js +317 -86
- package/dist/apps/chat-ui/assets/{dist-BDIATCQt.js → dist-CUQJqggN.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-mqCviI-c.js → dist-ClUlQWYN.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-JnW4v8UE.js → dist-Djul9BmZ.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-lRiLP9rr.js → dist-GD0JGdCi.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BBDMeU5-.js → dist-W5HOqHym.js} +1 -1
- package/dist/apps/chat-ui/assets/index-BcjkX-iP.js +228 -0
- package/dist/apps/chat-ui/assets/index-CmqRSbBU.css +1 -0
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-JvrPUGvI.js +43 -0
- package/dist/apps/chat-vscode-web/index.html +1 -1
- package/dist/apps/cli-ui/InkSessionApp.js +111 -12
- package/dist/apps/cli-ui/cliSessionsCommand.js +34 -8
- package/dist/cli-session/localSessionSource.js +153 -161
- package/dist/cli.js +7 -11
- package/dist/compute/cli.js +35 -16
- package/dist/compute/docker.js +341 -94
- package/dist/config/config.js +3 -0
- package/dist/core/context-build.js +7 -16
- package/dist/core/output-persistence-retry.js +484 -0
- package/dist/core/output-render-sequence.js +331 -0
- package/dist/core/profiles.js +24 -0
- package/dist/core/provider-recovery.js +7 -1
- package/dist/core/session-model.js +37 -1
- package/dist/core/session-router.js +78 -110
- package/dist/data/ingest-service.js +91 -9
- package/dist/data/navigation-store.js +5 -5
- package/dist/data/pibo-store.js +8 -1
- package/dist/data/schema.js +243 -3
- package/dist/data/session-store.js +3 -3
- package/dist/debug/agents.js +91 -89
- package/dist/debug/index.js +46 -2
- package/dist/debug/pty.js +127 -48
- package/dist/gateway/cli.js +4 -0
- package/dist/gateway/web.js +8 -8
- package/dist/local/client.js +3 -2
- package/dist/loops/channel.js +3 -1
- package/dist/loops/cli.js +2 -1
- package/dist/loops/service.js +90 -11
- package/dist/loops/store.js +100 -74
- package/dist/mcp/agent-context.js +13 -26
- package/dist/mcp/commands/info.js +3 -1
- package/dist/mcp/config-command.js +46 -2
- package/dist/mcp/config.js +18 -4
- package/dist/plugins/context-files-store.js +36 -6
- package/dist/plugins/context-files.js +52 -1
- package/dist/plugins/registry.js +2 -0
- package/dist/plugins/user-profile-resources.js +22 -0
- package/dist/previews/base-url.js +34 -0
- package/dist/previews/config.js +2 -32
- package/dist/ralph/store.js +5 -0
- package/dist/reliability/store.js +378 -106
- package/dist/session-ui/terminalRows.js +8 -4
- package/dist/sessions/pibo-data-store.js +145 -0
- package/dist/sessions/store.js +42 -0
- package/dist/setup/cli.js +422 -6
- package/dist/setup/installation-profiles.js +464 -0
- package/dist/shared/deterministic-digest.js +94 -0
- package/dist/shared/trace-engine.js +54 -0
- package/dist/shared/trace-event-projection.js +124 -67
- package/dist/shared/trace-history.js +47 -12
- package/dist/shared/trace-live-reducer.js +23 -4
- package/dist/shared/trace-nodes.js +17 -28
- package/dist/shared/trace-order.js +41 -5
- package/dist/shared/trace-page-merge.js +44 -14
- package/dist/shared/trace-patch-nodes.js +2 -0
- package/dist/shared/trace-subagent-links.js +4 -1
- package/dist/shared/trace-tool-identity.js +15 -8
- package/dist/signals/projector.js +28 -6
- package/dist/subagents/observation-query.js +121 -0
- package/dist/subagents/tool.js +7 -3
- package/dist/tools/agent-browser-leases.js +58 -23
- package/dist/tools/agent-browser-wrapper.js +1 -1
- package/dist/tools/browser-use-leases.js +129 -16
- package/dist/tools/browser-use-wrapper.js +1 -1
- package/dist/tools/index.js +27 -11
- package/dist/tools/python-runtime.js +10 -3
- package/dist/tools/registry.js +1 -1
- package/dist/tools/runtime/node-backend.js +16 -5
- package/dist/tools/runtime/node-worker-source.js +112 -33
- package/dist/tools/runtime/python-backend.js +16 -5
- package/dist/tools/runtime/python-worker-source.js +167 -16
- package/dist/tools/runtime/registry.js +23 -10
- package/dist/user-skills/store.js +0 -1
- package/docs/ops/vscode-extension-release.md +9 -1
- package/npm-shrinkwrap.json +9 -2
- package/package.json +9 -2
- package/scripts/docker-entrypoint.sh +46 -0
- package/scripts/prepare-agent-browser-wrapper.sh +70 -0
- package/scripts/prepare-browser-use-wrapper.sh +255 -0
- package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +0 -228
- package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +0 -1
- package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +0 -43
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.1.vsix +0 -0
|
@@ -6,6 +6,17 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
6
6
|
import { DEFAULT_AGENT_RUNTIME_INSTANCE_ID, DEFAULT_BUILTIN_TOOL_NAMES } from "../../core/profiles.js";
|
|
7
7
|
import { isPiboThinkingLevel } from "../../core/thinking.js";
|
|
8
8
|
import { findPiPackage } from "../../pi-packages/store.js";
|
|
9
|
+
export class CustomAgentTargetReferenceError extends Error {
|
|
10
|
+
targetProfileName;
|
|
11
|
+
dependentProfileNames;
|
|
12
|
+
constructor(targetProfileName, dependentProfileNames) {
|
|
13
|
+
const sortedNames = [...dependentProfileNames].sort((left, right) => left.localeCompare(right));
|
|
14
|
+
super(`Custom agent "${targetProfileName}" is targeted by custom agents: ${sortedNames.join(", ")}`);
|
|
15
|
+
this.name = "CustomAgentTargetReferenceError";
|
|
16
|
+
this.targetProfileName = targetProfileName;
|
|
17
|
+
this.dependentProfileNames = sortedNames;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
9
20
|
const CUSTOM_AGENT_NAME_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
10
21
|
export function previewCustomAgentCreate(input, options = {}) {
|
|
11
22
|
const now = options.now ?? new Date().toISOString();
|
|
@@ -27,6 +38,7 @@ export function previewCustomAgentCreate(input, options = {}) {
|
|
|
27
38
|
mcpServers: uniqueStrings(input.mcpServers ?? []),
|
|
28
39
|
piPackages: sanitizePiPackages(input.piPackages ?? []),
|
|
29
40
|
mainModel: sanitizeModelProfile(input.mainModel),
|
|
41
|
+
mainModelFallbacks: sanitizeModelFallbacks(input.mainModelFallbacks ?? [], input.mainModel),
|
|
30
42
|
subagentModel: sanitizeModelProfile(input.subagentModel),
|
|
31
43
|
thinkingLevel: sanitizeThinkingLevel(input.thinkingLevel),
|
|
32
44
|
mainThinkingLevel: sanitizeThinkingLevel(input.mainThinkingLevel),
|
|
@@ -61,6 +73,7 @@ export function previewCustomAgentUpdate(existing, input, options = {}) {
|
|
|
61
73
|
mcpServers: input.mcpServers ? uniqueStrings(input.mcpServers) : existing.mcpServers,
|
|
62
74
|
piPackages: input.piPackages ? sanitizePiPackages(input.piPackages) : existing.piPackages,
|
|
63
75
|
mainModel: input.mainModel === undefined ? existing.mainModel : sanitizeModelProfile(input.mainModel),
|
|
76
|
+
mainModelFallbacks: sanitizeModelFallbacks(input.mainModelFallbacks ?? existing.mainModelFallbacks, input.mainModel === undefined ? existing.mainModel : input.mainModel),
|
|
64
77
|
subagentModel: input.subagentModel === undefined ? existing.subagentModel : sanitizeModelProfile(input.subagentModel),
|
|
65
78
|
thinkingLevel: input.thinkingLevel === undefined ? existing.thinkingLevel : sanitizeThinkingLevel(input.thinkingLevel),
|
|
66
79
|
mainThinkingLevel: input.mainThinkingLevel === undefined ? existing.mainThinkingLevel : sanitizeThinkingLevel(input.mainThinkingLevel),
|
|
@@ -113,6 +126,7 @@ export class CustomAgentStore {
|
|
|
113
126
|
mcp_servers_json TEXT NOT NULL DEFAULT '[]',
|
|
114
127
|
pi_packages_json TEXT NOT NULL DEFAULT '[]',
|
|
115
128
|
main_model_json TEXT,
|
|
129
|
+
main_model_fallbacks_json TEXT NOT NULL DEFAULT '[]',
|
|
116
130
|
subagent_model_json TEXT,
|
|
117
131
|
thinking_level TEXT,
|
|
118
132
|
main_thinking_level TEXT,
|
|
@@ -140,6 +154,7 @@ export class CustomAgentStore {
|
|
|
140
154
|
this.migrateRuntimeColumns();
|
|
141
155
|
this.migratePiPackagesColumn();
|
|
142
156
|
this.migrateModelColumns();
|
|
157
|
+
this.migrateModelFallbackColumns();
|
|
143
158
|
this.migrateThinkingLevelColumn();
|
|
144
159
|
this.migrateThinkingOptionColumns();
|
|
145
160
|
this.migrateBuiltinToolNamesColumn();
|
|
@@ -230,6 +245,7 @@ export class CustomAgentStore {
|
|
|
230
245
|
mcp_servers_json = ?,
|
|
231
246
|
pi_packages_json = ?,
|
|
232
247
|
main_model_json = ?,
|
|
248
|
+
main_model_fallbacks_json = ?,
|
|
233
249
|
subagent_model_json = ?,
|
|
234
250
|
thinking_level = ?,
|
|
235
251
|
main_thinking_level = ?,
|
|
@@ -245,7 +261,7 @@ export class CustomAgentStore {
|
|
|
245
261
|
updated_at = ?
|
|
246
262
|
WHERE id = ?
|
|
247
263
|
`)
|
|
248
|
-
.run(updated.profileName, updated.displayName, updated.description ?? null, updated.folderId ?? null, updated.runtimeInstanceId, JSON.stringify(updated.runtimeOptions), serializeBoolean(updated.nativeSubagents), JSON.stringify(updated.nativeTools), JSON.stringify(updated.skills), JSON.stringify(updated.contextFiles), JSON.stringify(sanitizeSubagents(updated.subagents)), JSON.stringify(updated.mcpServers), JSON.stringify(updated.piPackages), updated.mainModel ? JSON.stringify(updated.mainModel) : null, updated.subagentModel ? JSON.stringify(updated.subagentModel) : null, updated.thinkingLevel ?? null, updated.mainThinkingLevel ?? null, updated.subagentThinkingLevel ?? null, serializeBoolean(updated.fast), serializeBoolean(updated.mainFast), serializeBoolean(updated.subagentFast), updated.builtinTools, JSON.stringify(updated.builtinToolNames), updated.autoContextFiles ? 1 : 0, updated.runControl ? 1 : 0, updated.goalControl ? 1 : 0, updated.updatedAt, id);
|
|
264
|
+
.run(updated.profileName, updated.displayName, updated.description ?? null, updated.folderId ?? null, updated.runtimeInstanceId, JSON.stringify(updated.runtimeOptions), serializeBoolean(updated.nativeSubagents), JSON.stringify(updated.nativeTools), JSON.stringify(updated.skills), JSON.stringify(updated.contextFiles), JSON.stringify(sanitizeSubagents(updated.subagents)), JSON.stringify(updated.mcpServers), JSON.stringify(updated.piPackages), updated.mainModel ? JSON.stringify(updated.mainModel) : null, JSON.stringify(updated.mainModelFallbacks), updated.subagentModel ? JSON.stringify(updated.subagentModel) : null, updated.thinkingLevel ?? null, updated.mainThinkingLevel ?? null, updated.subagentThinkingLevel ?? null, serializeBoolean(updated.fast), serializeBoolean(updated.mainFast), serializeBoolean(updated.subagentFast), updated.builtinTools, JSON.stringify(updated.builtinToolNames), updated.autoContextFiles ? 1 : 0, updated.runControl ? 1 : 0, updated.goalControl ? 1 : 0, updated.updatedAt, id);
|
|
249
265
|
return this.get(id);
|
|
250
266
|
}
|
|
251
267
|
setArchived(id, archived) {
|
|
@@ -259,10 +275,42 @@ export class CustomAgentStore {
|
|
|
259
275
|
.run(archivedAt, new Date().toISOString(), id);
|
|
260
276
|
return this.get(id);
|
|
261
277
|
}
|
|
278
|
+
listReferencingAgents(id) {
|
|
279
|
+
this.migrateLegacyProfileNames();
|
|
280
|
+
const target = this.get(id);
|
|
281
|
+
if (!target)
|
|
282
|
+
return [];
|
|
283
|
+
const targetNames = new Set([
|
|
284
|
+
target.profileName,
|
|
285
|
+
target.id,
|
|
286
|
+
`custom-agent:${target.id}`,
|
|
287
|
+
...target.profileAliases,
|
|
288
|
+
]);
|
|
289
|
+
return this.list({ includeArchived: true })
|
|
290
|
+
.filter((agent) => agent.id !== id && agent.subagents.some((subagent) => targetNames.has(subagent.targetProfile)))
|
|
291
|
+
.sort((left, right) => left.profileName.localeCompare(right.profileName));
|
|
292
|
+
}
|
|
262
293
|
delete(id) {
|
|
263
|
-
this.db.
|
|
264
|
-
|
|
265
|
-
|
|
294
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
295
|
+
try {
|
|
296
|
+
const target = this.get(id);
|
|
297
|
+
if (!target) {
|
|
298
|
+
this.db.exec("COMMIT");
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
const dependents = this.listReferencingAgents(id);
|
|
302
|
+
if (dependents.length > 0) {
|
|
303
|
+
throw new CustomAgentTargetReferenceError(target.profileName, dependents.map((agent) => agent.profileName));
|
|
304
|
+
}
|
|
305
|
+
this.db.prepare("DELETE FROM chat_agent_profile_aliases WHERE agent_id = ?").run(id);
|
|
306
|
+
const result = this.db.prepare("DELETE FROM chat_agents WHERE id = ?").run(id);
|
|
307
|
+
this.db.exec("COMMIT");
|
|
308
|
+
return Number(result.changes ?? 0) > 0;
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
this.db.exec("ROLLBACK");
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
266
314
|
}
|
|
267
315
|
close() {
|
|
268
316
|
this.db.close();
|
|
@@ -286,6 +334,7 @@ export class CustomAgentStore {
|
|
|
286
334
|
mcp_servers_json,
|
|
287
335
|
pi_packages_json,
|
|
288
336
|
main_model_json,
|
|
337
|
+
main_model_fallbacks_json,
|
|
289
338
|
subagent_model_json,
|
|
290
339
|
thinking_level,
|
|
291
340
|
main_thinking_level,
|
|
@@ -301,9 +350,9 @@ export class CustomAgentStore {
|
|
|
301
350
|
created_at,
|
|
302
351
|
updated_at,
|
|
303
352
|
archived_at
|
|
304
|
-
) VALUES (${Array.from({ length:
|
|
353
|
+
) VALUES (${Array.from({ length: 31 }, () => "?").join(", ")})
|
|
305
354
|
`)
|
|
306
|
-
.run(agent.id, agent.profileName, agent.displayName, agent.description ?? null, agent.folderId ?? null, agent.runtimeInstanceId, JSON.stringify(agent.runtimeOptions), serializeBoolean(agent.nativeSubagents), JSON.stringify(agent.nativeTools), JSON.stringify(agent.skills), JSON.stringify(agent.contextFiles), JSON.stringify(sanitizeSubagents(agent.subagents)), JSON.stringify(agent.mcpServers), JSON.stringify(agent.piPackages), agent.mainModel ? JSON.stringify(agent.mainModel) : null, agent.subagentModel ? JSON.stringify(agent.subagentModel) : null, agent.thinkingLevel ?? null, agent.mainThinkingLevel ?? null, agent.subagentThinkingLevel ?? null, serializeBoolean(agent.fast), serializeBoolean(agent.mainFast), serializeBoolean(agent.subagentFast), agent.builtinTools, JSON.stringify(agent.builtinToolNames), agent.autoContextFiles ? 1 : 0, agent.runControl ? 1 : 0, agent.goalControl ? 1 : 0, agent.createdAt, agent.updatedAt, agent.archivedAt ?? null);
|
|
355
|
+
.run(agent.id, agent.profileName, agent.displayName, agent.description ?? null, agent.folderId ?? null, agent.runtimeInstanceId, JSON.stringify(agent.runtimeOptions), serializeBoolean(agent.nativeSubagents), JSON.stringify(agent.nativeTools), JSON.stringify(agent.skills), JSON.stringify(agent.contextFiles), JSON.stringify(sanitizeSubagents(agent.subagents)), JSON.stringify(agent.mcpServers), JSON.stringify(agent.piPackages), agent.mainModel ? JSON.stringify(agent.mainModel) : null, JSON.stringify(agent.mainModelFallbacks), agent.subagentModel ? JSON.stringify(agent.subagentModel) : null, agent.thinkingLevel ?? null, agent.mainThinkingLevel ?? null, agent.subagentThinkingLevel ?? null, serializeBoolean(agent.fast), serializeBoolean(agent.mainFast), serializeBoolean(agent.subagentFast), agent.builtinTools, JSON.stringify(agent.builtinToolNames), agent.autoContextFiles ? 1 : 0, agent.runControl ? 1 : 0, agent.goalControl ? 1 : 0, agent.createdAt, agent.updatedAt, agent.archivedAt ?? null);
|
|
307
356
|
}
|
|
308
357
|
requireFolderExists(folderId) {
|
|
309
358
|
if (folderId === undefined || folderId === null)
|
|
@@ -363,6 +412,15 @@ export class CustomAgentStore {
|
|
|
363
412
|
.prepare("UPDATE chat_agents SET profile_name = ?, display_name = ? WHERE id = ?")
|
|
364
413
|
.run(nextName, nextName, row.id);
|
|
365
414
|
}
|
|
415
|
+
this.db.prepare(`
|
|
416
|
+
DELETE FROM chat_agent_profile_aliases
|
|
417
|
+
WHERE EXISTS (
|
|
418
|
+
SELECT 1
|
|
419
|
+
FROM chat_agents
|
|
420
|
+
WHERE chat_agents.profile_name = chat_agent_profile_aliases.old_profile_name
|
|
421
|
+
AND chat_agents.id != chat_agent_profile_aliases.agent_id
|
|
422
|
+
)
|
|
423
|
+
`).run();
|
|
366
424
|
}
|
|
367
425
|
migrateAgentFolderColumn() {
|
|
368
426
|
if (!this.tableColumns().has("folder_id")) {
|
|
@@ -458,6 +516,12 @@ export class CustomAgentStore {
|
|
|
458
516
|
this.db.prepare("ALTER TABLE chat_agents ADD COLUMN subagent_model_json TEXT").run();
|
|
459
517
|
}
|
|
460
518
|
}
|
|
519
|
+
migrateModelFallbackColumns() {
|
|
520
|
+
const columns = new Set(this.db.prepare("PRAGMA table_info(chat_agents)").all().map((column) => column.name));
|
|
521
|
+
if (!columns.has("main_model_fallbacks_json")) {
|
|
522
|
+
this.db.prepare("ALTER TABLE chat_agents ADD COLUMN main_model_fallbacks_json TEXT NOT NULL DEFAULT '[]'").run();
|
|
523
|
+
}
|
|
524
|
+
}
|
|
461
525
|
migrateThinkingLevelColumn() {
|
|
462
526
|
const columns = new Set(this.db.prepare("PRAGMA table_info(chat_agents)").all().map((column) => column.name));
|
|
463
527
|
if (!columns.has("thinking_level")) {
|
|
@@ -649,6 +713,7 @@ function agentFromRow(row, profileAliases) {
|
|
|
649
713
|
mcpServers: parseStringArray(row.mcp_servers_json),
|
|
650
714
|
piPackages: parseStringArray(row.pi_packages_json),
|
|
651
715
|
mainModel: parseModelProfile(row.main_model_json),
|
|
716
|
+
mainModelFallbacks: parseModelFallbacks(row.main_model_fallbacks_json, row.main_model_json),
|
|
652
717
|
subagentModel: parseModelProfile(row.subagent_model_json),
|
|
653
718
|
thinkingLevel: sanitizeThinkingLevel(row.thinking_level),
|
|
654
719
|
mainThinkingLevel: sanitizeThinkingLevel(row.main_thinking_level),
|
|
@@ -774,6 +839,30 @@ function sanitizeModelProfile(value) {
|
|
|
774
839
|
return undefined;
|
|
775
840
|
return { provider, id };
|
|
776
841
|
}
|
|
842
|
+
function parseModelFallbacks(value, primaryValue) {
|
|
843
|
+
try {
|
|
844
|
+
return sanitizeModelFallbacks(JSON.parse(value), parseModelProfile(primaryValue));
|
|
845
|
+
}
|
|
846
|
+
catch {
|
|
847
|
+
return [];
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
function sanitizeModelFallbacks(value, primary) {
|
|
851
|
+
if (!Array.isArray(value))
|
|
852
|
+
return [];
|
|
853
|
+
const primaryModel = sanitizeModelProfile(primary);
|
|
854
|
+
const seen = new Set(primaryModel ? [`${primaryModel.provider}\u0000${primaryModel.id}`] : []);
|
|
855
|
+
return value.flatMap((item) => {
|
|
856
|
+
const model = sanitizeModelProfile(item);
|
|
857
|
+
if (!model)
|
|
858
|
+
return [];
|
|
859
|
+
const key = `${model.provider}\u0000${model.id}`;
|
|
860
|
+
if (seen.has(key))
|
|
861
|
+
return [];
|
|
862
|
+
seen.add(key);
|
|
863
|
+
return [model];
|
|
864
|
+
});
|
|
865
|
+
}
|
|
777
866
|
function sanitizeSubagents(value) {
|
|
778
867
|
return value.flatMap((item) => {
|
|
779
868
|
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
@@ -790,6 +879,9 @@ function sanitizeSubagents(value) {
|
|
|
790
879
|
const model = sanitizeModelProfile(candidate.model);
|
|
791
880
|
if (model)
|
|
792
881
|
subagent.model = model;
|
|
882
|
+
const modelFallbacks = sanitizeModelFallbacks(candidate.modelFallbacks, model);
|
|
883
|
+
if (modelFallbacks.length > 0)
|
|
884
|
+
subagent.modelFallbacks = modelFallbacks;
|
|
793
885
|
const thinkingLevel = sanitizeThinkingLevel(candidate.thinkingLevel);
|
|
794
886
|
if (thinkingLevel)
|
|
795
887
|
subagent.thinkingLevel = thinkingLevel;
|
|
@@ -304,6 +304,23 @@ export function normalizeModelProfile(value, fieldName) {
|
|
|
304
304
|
}
|
|
305
305
|
return { provider, id };
|
|
306
306
|
}
|
|
307
|
+
export function normalizeModelFallbacks(value, fieldName, primary) {
|
|
308
|
+
if (value === undefined || value === null)
|
|
309
|
+
return [];
|
|
310
|
+
if (!Array.isArray(value))
|
|
311
|
+
throw new PiboWebHttpError(`${fieldName} must be an array`, 400);
|
|
312
|
+
const seen = new Set(primary ? [`${primary.provider}\u0000${primary.id}`] : []);
|
|
313
|
+
return value.flatMap((item, index) => {
|
|
314
|
+
const model = normalizeModelProfile(item, `${fieldName}[${index}]`);
|
|
315
|
+
if (!model)
|
|
316
|
+
throw new PiboWebHttpError(`${fieldName}[${index}] must define a provider and model`, 400);
|
|
317
|
+
const key = `${model.provider}\u0000${model.id}`;
|
|
318
|
+
if (seen.has(key))
|
|
319
|
+
return [];
|
|
320
|
+
seen.add(key);
|
|
321
|
+
return [model];
|
|
322
|
+
});
|
|
323
|
+
}
|
|
307
324
|
export function normalizeMcpServerDescriptionBody(value) {
|
|
308
325
|
if (typeof value !== "string")
|
|
309
326
|
throw new PiboWebHttpError("MCP server description must be a string", 400);
|
|
@@ -404,6 +421,9 @@ export function normalizeAgentSubagents(value) {
|
|
|
404
421
|
const model = normalizeModelProfile(raw.model, "subagent model");
|
|
405
422
|
if (model)
|
|
406
423
|
subagent.model = model;
|
|
424
|
+
const modelFallbacks = normalizeModelFallbacks(raw.modelFallbacks, "subagent modelFallbacks", model);
|
|
425
|
+
if (modelFallbacks.length > 0)
|
|
426
|
+
subagent.modelFallbacks = modelFallbacks;
|
|
407
427
|
const thinkingLevel = normalizeThinkingLevel(raw.thinkingLevel, "subagent thinkingLevel");
|
|
408
428
|
if (thinkingLevel)
|
|
409
429
|
subagent.thinkingLevel = thinkingLevel;
|
|
@@ -529,6 +549,13 @@ export function normalizeStreamingFixtureTraceSnapshots(value) {
|
|
|
529
549
|
return value;
|
|
530
550
|
throw new PiboWebHttpError("traceSnapshots must be a boolean", 400);
|
|
531
551
|
}
|
|
552
|
+
export function normalizeStreamingFixtureToolInterleaving(value) {
|
|
553
|
+
if (value === undefined)
|
|
554
|
+
return false;
|
|
555
|
+
if (typeof value === "boolean")
|
|
556
|
+
return value;
|
|
557
|
+
throw new PiboWebHttpError("toolInterleaving must be a boolean", 400);
|
|
558
|
+
}
|
|
532
559
|
export function normalizeStreamingFixtureSuppressLiveDeltas(value) {
|
|
533
560
|
if (value === undefined)
|
|
534
561
|
return false;
|
|
@@ -702,6 +729,7 @@ export function createAgentInput(body) {
|
|
|
702
729
|
mcpServers: normalizeNameArray(body.mcpServers, "mcpServers"),
|
|
703
730
|
piPackages: normalizeRegisteredPiPackages(body.piPackages),
|
|
704
731
|
mainModel: normalizeModelProfile(body.mainModel, "mainModel"),
|
|
732
|
+
mainModelFallbacks: normalizeModelFallbacks(body.mainModelFallbacks, "mainModelFallbacks", normalizeModelProfile(body.mainModel, "mainModel")),
|
|
705
733
|
subagentModel: normalizeModelProfile(body.subagentModel, "subagentModel"),
|
|
706
734
|
thinkingLevel: normalizeThinkingLevel(body.thinkingLevel, "thinkingLevel"),
|
|
707
735
|
mainThinkingLevel: normalizeThinkingLevel(body.mainThinkingLevel, "mainThinkingLevel"),
|
|
@@ -748,6 +776,9 @@ export function createAgentUpdate(body) {
|
|
|
748
776
|
if (body.mainModel !== undefined) {
|
|
749
777
|
update.mainModel = body.mainModel === null ? null : normalizeModelProfile(body.mainModel, "mainModel");
|
|
750
778
|
}
|
|
779
|
+
if (body.mainModelFallbacks !== undefined) {
|
|
780
|
+
update.mainModelFallbacks = normalizeModelFallbacks(body.mainModelFallbacks, "mainModelFallbacks", update.mainModel ?? undefined);
|
|
781
|
+
}
|
|
751
782
|
if (body.subagentModel !== undefined) {
|
|
752
783
|
update.subagentModel = body.subagentModel === null ? null : normalizeModelProfile(body.subagentModel, "subagentModel");
|
|
753
784
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { normalizeSessionErrorDetails } from "../../../core/session-errors.js";
|
|
2
2
|
import { roomWorkspaceFromMetadata } from "../types/rooms.js";
|
|
3
3
|
import { tracePayloadRefForStoredPayload } from "../trace-v2.js";
|
|
4
|
+
import { qualifiedToolNodeId } from "../../../shared/trace-tool-identity.js";
|
|
4
5
|
const MAX_TRACE_EVENT_HYDRATION_BYTES = 1024 * 1024;
|
|
5
6
|
export function storedPiboEventFromV2Row(row, payloadStore) {
|
|
6
7
|
const attributes = parseJsonObject(row.attributes_json);
|
|
@@ -16,7 +17,8 @@ export function storedPiboEventFromV2Row(row, payloadStore) {
|
|
|
16
17
|
...payloadIdentity,
|
|
17
18
|
})
|
|
18
19
|
: undefined;
|
|
19
|
-
|
|
20
|
+
const renderSequence = numberAttribute(attributes, "renderSequence") ?? payload.renderSequence;
|
|
21
|
+
return { id: String(row.stream_id), piboSessionId: row.session_id ?? undefined, eventSequence: row.session_sequence ?? undefined, renderSequence, eventId: row.event_id ?? undefined, streamId: row.stream_id, storedPayloadRef, type: row.type, createdAt: row.created_at, payload };
|
|
20
22
|
}
|
|
21
23
|
export function storedChatEventFromV2Row(row, payloadStore) {
|
|
22
24
|
const attributes = parseJsonObject(row.attributes_json);
|
|
@@ -24,12 +26,22 @@ export function storedChatEventFromV2Row(row, payloadStore) {
|
|
|
24
26
|
}
|
|
25
27
|
function outputPayloadFromV2Row(row, attributes, persistedPayload) {
|
|
26
28
|
const inlinePayload = attributes.inlinePayload ?? persistedPayload;
|
|
27
|
-
if (inlinePayload && typeof inlinePayload === "object" && !Array.isArray(inlinePayload) && typeof inlinePayload.type === "string")
|
|
28
|
-
return
|
|
29
|
+
if (inlinePayload && typeof inlinePayload === "object" && !Array.isArray(inlinePayload) && typeof inlinePayload.type === "string") {
|
|
30
|
+
return compactObject({
|
|
31
|
+
...inlinePayload,
|
|
32
|
+
renderSequence: numberAttribute(attributes, "renderSequence") ?? inlinePayload.renderSequence,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
29
35
|
const piboSessionId = row.session_id;
|
|
30
36
|
if (!piboSessionId)
|
|
31
37
|
return undefined;
|
|
32
|
-
const
|
|
38
|
+
const eventIdentityScoped = typeof attributes.eventIdentityScoped === "boolean"
|
|
39
|
+
? attributes.eventIdentityScoped
|
|
40
|
+
: undefined;
|
|
41
|
+
const eventId = eventIdentityScoped === false
|
|
42
|
+
? undefined
|
|
43
|
+
: stringAttribute(attributes, "semanticEventId") ?? row.event_id ?? undefined;
|
|
44
|
+
const base = compactObject({ piboSessionId, eventId, renderSequence: numberAttribute(attributes, "renderSequence"), toolInvocationOrdinal: numberAttribute(attributes, "toolInvocationOrdinal") });
|
|
33
45
|
if (row.type === "assistant_message" || row.type === "assistant_delta") {
|
|
34
46
|
return compactObject({
|
|
35
47
|
...base,
|
|
@@ -44,9 +56,32 @@ function outputPayloadFromV2Row(row, attributes, persistedPayload) {
|
|
|
44
56
|
if (row.type === "message_steered")
|
|
45
57
|
return { ...base, type: "message_steered", text: stringAttribute(attributes, "inlineText") ?? row.preview_text ?? "", source: stringAttribute(attributes, "source") ?? "user", activeEventId: stringAttribute(attributes, "activeEventId") };
|
|
46
58
|
if (row.type === "message_started")
|
|
47
|
-
return {
|
|
59
|
+
return compactObject({
|
|
60
|
+
...base,
|
|
61
|
+
type: "message_started",
|
|
62
|
+
text: stringAttribute(attributes, "inlineText") ?? row.preview_text ?? "",
|
|
63
|
+
source: stringAttribute(attributes, "source"),
|
|
64
|
+
});
|
|
48
65
|
if (row.type === "message_finished")
|
|
49
|
-
return {
|
|
66
|
+
return compactObject({
|
|
67
|
+
...base,
|
|
68
|
+
type: "message_finished",
|
|
69
|
+
source: stringAttribute(attributes, "source"),
|
|
70
|
+
});
|
|
71
|
+
if (row.type === "assistant_usage") {
|
|
72
|
+
return compactObject({
|
|
73
|
+
...base,
|
|
74
|
+
type: "assistant_usage",
|
|
75
|
+
usageIndex: numberAttribute(attributes, "usageIndex"),
|
|
76
|
+
inputTokens: numberAttribute(attributes, "inputTokens"),
|
|
77
|
+
outputTokens: numberAttribute(attributes, "outputTokens"),
|
|
78
|
+
cacheReadTokens: numberAttribute(attributes, "cacheReadTokens"),
|
|
79
|
+
cacheWriteTokens: numberAttribute(attributes, "cacheWriteTokens"),
|
|
80
|
+
reasoningTokens: numberAttribute(attributes, "reasoningTokens"),
|
|
81
|
+
totalTokens: numberAttribute(attributes, "totalTokens") ?? 0,
|
|
82
|
+
costUsd: numberAttribute(attributes, "costUsd"),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
50
85
|
if (row.type === "thinking_started" || row.type === "thinking_delta" || row.type === "thinking_finished") {
|
|
51
86
|
return compactObject({
|
|
52
87
|
...base,
|
|
@@ -74,6 +109,10 @@ function outputPayloadFromV2Row(row, attributes, persistedPayload) {
|
|
|
74
109
|
}
|
|
75
110
|
if (row.type === "execution_result")
|
|
76
111
|
return { ...base, type: "execution_result", action: row.preview_text ?? stringAttribute(attributes, "action") ?? "execution", result: inlinePayload ?? null };
|
|
112
|
+
if (row.type === "compaction_start")
|
|
113
|
+
return { ...base, type: "compaction_start", compactionIndex: numberAttribute(attributes, "compactionIndex"), reason: stringAttribute(attributes, "reason") ?? row.preview_text ?? "unknown" };
|
|
114
|
+
if (row.type === "compaction_end")
|
|
115
|
+
return { ...base, type: "compaction_end", compactionIndex: numberAttribute(attributes, "compactionIndex"), reason: stringAttribute(attributes, "reason") ?? row.preview_text ?? "unknown", result: inlinePayload, aborted: booleanAttribute(attributes, "aborted") ?? false, errorMessage: stringAttribute(attributes, "errorMessage") };
|
|
77
116
|
if (row.type === "session_error") {
|
|
78
117
|
const error = stringAttribute(attributes, "error") ?? row.preview_text ?? "Error";
|
|
79
118
|
return { ...base, type: "session_error", error, errorDetails: normalizeSessionErrorDetails(error, isRecord(attributes.errorDetails) ? attributes.errorDetails : undefined) };
|
|
@@ -100,10 +139,15 @@ function tracePayloadIdentityForEvent(row, attributes) {
|
|
|
100
139
|
const toolCallId = stringAttribute(attributes, "toolCallId");
|
|
101
140
|
if (!toolCallId)
|
|
102
141
|
return undefined;
|
|
142
|
+
const eventId = stringAttribute(attributes, "semanticEventId") ?? row.event_id;
|
|
143
|
+
if (!eventId)
|
|
144
|
+
return undefined;
|
|
145
|
+
const ordinal = numberAttribute(attributes, "toolInvocationOrdinal") ?? 0;
|
|
146
|
+
const nodeId = qualifiedToolNodeId(toolCallId, eventId, ordinal);
|
|
103
147
|
if (row.type === "tool_call" || row.type === "tool_execution_started")
|
|
104
|
-
return { nodeId
|
|
148
|
+
return { nodeId, payloadKind: "input" };
|
|
105
149
|
if (row.type === "tool_execution_updated" || row.type === "tool_execution_finished")
|
|
106
|
-
return { nodeId
|
|
150
|
+
return { nodeId, payloadKind: "output" };
|
|
107
151
|
return undefined;
|
|
108
152
|
}
|
|
109
153
|
export function sessionFromRow(row) { return { piboSessionId: row.id, piSessionId: row.pi_session_id ?? "", runtimeInstanceId: row.runtime_instance_id ?? undefined, runtimeAdapterId: row.runtime_adapter_id ?? undefined, runtimeBindingState: isRuntimeBindingState(row.binding_state) ? row.binding_state : undefined, nativeSessionId: row.native_session_id ?? undefined, parentId: row.parent_id ?? undefined, profile: row.profile, channel: row.channel, kind: row.kind, createdAt: row.created_at, updatedAt: row.updated_at, lastActivityAt: row.last_activity_at, status: row.status === "running" || row.status === "error" ? row.status : "idle" }; }
|
|
@@ -124,9 +168,15 @@ export function compactObject(value) { return Object.fromEntries(Object.entries(
|
|
|
124
168
|
export function previewForPayload(payload) { if (typeof payload === "object" && payload && "text" in payload && typeof payload.text === "string")
|
|
125
169
|
return payload.text.slice(0, 512); if (typeof payload === "string")
|
|
126
170
|
return payload.slice(0, 512); return undefined; }
|
|
127
|
-
export function statusFromOutputEvent(event) {
|
|
128
|
-
|
|
129
|
-
|
|
171
|
+
export function statusFromOutputEvent(event) {
|
|
172
|
+
if (event.type === "session_error")
|
|
173
|
+
return "error";
|
|
174
|
+
if (event.type === "message_finished")
|
|
175
|
+
return "idle";
|
|
176
|
+
if (event.type === "message_queued" || event.type === "message_steered" || event.type === "message_started")
|
|
177
|
+
return "running";
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
130
180
|
function isRuntimeBindingState(value) { return value === "unbound" || value === "bound" || value === "missing" || value === "error"; }
|
|
131
181
|
function actorTypeValue(value) { return value === "user" || value === "assistant" || value === "system" || value === "agent" ? value : undefined; }
|
|
132
182
|
function retentionClassValue(value) { return value === "live_delta" || value === "chat_message" || value === "audit_event" ? value : "trace_event"; }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { mkdirSync, rmSync, statSync } from "node:fs";
|
|
3
|
-
import { dirname, isAbsolute, resolve } from "node:path";
|
|
2
|
+
import { mkdirSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { DatabaseSync } from "node:sqlite";
|
|
5
5
|
import { piboHomePath } from "../../../core/pibo-home.js";
|
|
6
6
|
export class ChatProjectService {
|
|
@@ -72,7 +72,7 @@ export class ChatProjectService {
|
|
|
72
72
|
const projectFolder = resolve(normalizeProjectFolder(input.projectFolder));
|
|
73
73
|
this.assertNameAvailable(name);
|
|
74
74
|
this.assertFolderAvailable(projectFolder);
|
|
75
|
-
this.ensureProjectFolderUsable(projectFolder);
|
|
75
|
+
this.ensureProjectFolderUsable(projectFolder, input.createFolder === true);
|
|
76
76
|
const now = new Date().toISOString();
|
|
77
77
|
const id = `prj_${randomUUID()}`;
|
|
78
78
|
this.db.prepare(`INSERT INTO projects (id, name, description, project_folder, configuration_status, metadata_json, created_at, updated_at)
|
|
@@ -93,16 +93,26 @@ export class ChatProjectService {
|
|
|
93
93
|
return this.getProject(id, { includeArchived: true });
|
|
94
94
|
}
|
|
95
95
|
deleteProject(id, options) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
96
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
97
|
+
try {
|
|
98
|
+
const project = this.requireProject(id, { includeArchived: true });
|
|
99
|
+
if (!project.archivedAt)
|
|
100
|
+
throw new Error("Archive the project before permanently deleting it.");
|
|
101
|
+
if (options.confirmName !== project.name)
|
|
102
|
+
throw new Error(`Type \"${project.name}\" to permanently delete this project.`);
|
|
103
|
+
if (options.deleteFiles)
|
|
104
|
+
this.assertFolderDoesNotOverlapAnotherProject(project);
|
|
105
|
+
this.db.prepare("DELETE FROM project_sessions WHERE project_id = ?").run(id);
|
|
106
|
+
this.db.prepare("DELETE FROM projects WHERE id = ?").run(id);
|
|
107
|
+
if (options.deleteFiles)
|
|
108
|
+
rmSync(project.projectFolder, { recursive: true, force: true });
|
|
109
|
+
this.db.exec("COMMIT");
|
|
110
|
+
return { deletedProjectId: id };
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
this.db.exec("ROLLBACK");
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
106
116
|
}
|
|
107
117
|
listProjectSessions(projectId, options = {}) {
|
|
108
118
|
const rows = this.db.prepare(`SELECT * FROM project_sessions WHERE project_id = ? ${options.includeArchived ? "" : "AND archived = 0"} ORDER BY created_at`).all(projectId);
|
|
@@ -112,6 +122,60 @@ export class ChatProjectService {
|
|
|
112
122
|
const row = this.db.prepare("SELECT * FROM project_sessions WHERE pibo_session_id = ?").get(piboSessionId);
|
|
113
123
|
return row ? projectSessionFromRow(row) : undefined;
|
|
114
124
|
}
|
|
125
|
+
deleteProjectSessions(piboSessionIds) {
|
|
126
|
+
const ids = [...new Set(piboSessionIds.filter(Boolean))];
|
|
127
|
+
if (ids.length === 0)
|
|
128
|
+
return 0;
|
|
129
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
130
|
+
try {
|
|
131
|
+
const deleted = this.deleteProjectSessionsLocked(ids, new Date().toISOString());
|
|
132
|
+
this.db.exec("COMMIT");
|
|
133
|
+
return deleted;
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
this.db.exec("ROLLBACK");
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async deleteProjectSessionWithCanonicalDelete(piboSessionId, deleteCanonicalSession) {
|
|
141
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
142
|
+
try {
|
|
143
|
+
this.deleteProjectSessionsLocked([piboSessionId], new Date().toISOString());
|
|
144
|
+
const result = await deleteCanonicalSession();
|
|
145
|
+
this.db.exec("COMMIT");
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
this.db.exec("ROLLBACK");
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
deleteProjectSessionsLocked(ids, now) {
|
|
154
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
155
|
+
const affectedProjects = this.db.prepare(`SELECT DISTINCT id FROM projects
|
|
156
|
+
WHERE current_main_session_id IN (${placeholders})
|
|
157
|
+
UNION
|
|
158
|
+
SELECT DISTINCT project_id AS id FROM project_sessions WHERE pibo_session_id IN (${placeholders})`)
|
|
159
|
+
.all(...ids, ...ids);
|
|
160
|
+
const deleted = this.db.prepare(`DELETE FROM project_sessions WHERE pibo_session_id IN (${placeholders})`).run(...ids);
|
|
161
|
+
for (const project of affectedProjects) {
|
|
162
|
+
const current = this.db.prepare("SELECT current_main_session_id FROM projects WHERE id = ?")
|
|
163
|
+
.get(project.id);
|
|
164
|
+
if (!current?.current_main_session_id)
|
|
165
|
+
continue;
|
|
166
|
+
const currentLink = this.db.prepare("SELECT 1 FROM project_sessions WHERE project_id = ? AND pibo_session_id = ?")
|
|
167
|
+
.get(project.id, current.current_main_session_id);
|
|
168
|
+
if (currentLink)
|
|
169
|
+
continue;
|
|
170
|
+
const replacement = this.db.prepare(`SELECT pibo_session_id FROM project_sessions
|
|
171
|
+
WHERE project_id = ? AND kind = 'main' AND archived = 0
|
|
172
|
+
ORDER BY updated_at DESC, created_at DESC, pibo_session_id DESC
|
|
173
|
+
LIMIT 1`).get(project.id);
|
|
174
|
+
this.db.prepare("UPDATE projects SET current_main_session_id = ?, updated_at = ? WHERE id = ?")
|
|
175
|
+
.run(replacement?.pibo_session_id ?? null, now, project.id);
|
|
176
|
+
}
|
|
177
|
+
return Number(deleted.changes ?? 0);
|
|
178
|
+
}
|
|
115
179
|
addProjectSession(input) {
|
|
116
180
|
const existing = this.getProjectSession(input.piboSessionId);
|
|
117
181
|
const now = new Date().toISOString();
|
|
@@ -599,9 +663,17 @@ export class ChatProjectService {
|
|
|
599
663
|
if (existing)
|
|
600
664
|
throw new Error("Project folder already exists");
|
|
601
665
|
}
|
|
602
|
-
|
|
666
|
+
assertFolderDoesNotOverlapAnotherProject(project) {
|
|
667
|
+
const projects = this.db.prepare("SELECT name, project_folder FROM projects WHERE id != ? ORDER BY lower(name), created_at").all(project.id);
|
|
668
|
+
const overlapping = projects.find((other) => pathsOverlap(project.projectFolder, other.project_folder));
|
|
669
|
+
if (overlapping) {
|
|
670
|
+
throw new Error(`Cannot delete project files because its folder overlaps Project \"${overlapping.name}\". Delete without deleting files or choose a non-overlapping folder.`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
ensureProjectFolderUsable(folder, createFolder) {
|
|
603
674
|
try {
|
|
604
|
-
|
|
675
|
+
if (createFolder)
|
|
676
|
+
mkdirSync(folder, { recursive: true });
|
|
605
677
|
if (!statSync(folder).isDirectory())
|
|
606
678
|
throw new Error("Project folder is not a directory");
|
|
607
679
|
}
|
|
@@ -770,6 +842,38 @@ function normalizeProjectFolder(value) {
|
|
|
770
842
|
throw new Error("Project folder must be an absolute path, e.g. ~/code/my-project or /home/me/code/my-project");
|
|
771
843
|
return folder;
|
|
772
844
|
}
|
|
845
|
+
function pathsOverlap(left, right) {
|
|
846
|
+
if (pathContains(left, right) || pathContains(right, left))
|
|
847
|
+
return true;
|
|
848
|
+
const canonicalLeft = canonicalProjectPath(left);
|
|
849
|
+
const canonicalRight = canonicalProjectPath(right);
|
|
850
|
+
if (!canonicalLeft || !canonicalRight)
|
|
851
|
+
return true;
|
|
852
|
+
return pathContains(canonicalLeft, canonicalRight) || pathContains(canonicalRight, canonicalLeft);
|
|
853
|
+
}
|
|
854
|
+
function canonicalProjectPath(path) {
|
|
855
|
+
let current = resolve(path);
|
|
856
|
+
const missingSegments = [];
|
|
857
|
+
while (true) {
|
|
858
|
+
try {
|
|
859
|
+
return resolve(realpathSync(current), ...missingSegments);
|
|
860
|
+
}
|
|
861
|
+
catch (error) {
|
|
862
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
|
|
863
|
+
if (code !== "ENOENT" && code !== "ENOTDIR")
|
|
864
|
+
return undefined;
|
|
865
|
+
const parent = dirname(current);
|
|
866
|
+
if (parent === current)
|
|
867
|
+
return undefined;
|
|
868
|
+
missingSegments.unshift(basename(current));
|
|
869
|
+
current = parent;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
function pathContains(parent, candidate) {
|
|
874
|
+
const pathFromParent = relative(resolve(parent), resolve(candidate));
|
|
875
|
+
return pathFromParent === "" || (pathFromParent !== ".." && !pathFromParent.startsWith(`..${sep}`) && !isAbsolute(pathFromParent));
|
|
876
|
+
}
|
|
773
877
|
const PROJECT_SESSION_STATES = new Set([
|
|
774
878
|
"simple_chat",
|
|
775
879
|
"workflow",
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { roomWorkspaceFromMetadata } from "../types/rooms.js";
|
|
3
3
|
import { roomFromRow } from "./chat-data-mappers.js";
|
|
4
|
+
export class PiboRoomHierarchyCycleError extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super("Room parent assignment would create a cycle.");
|
|
7
|
+
this.name = "PiboRoomHierarchyCycleError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
4
10
|
export class ChatRoomService {
|
|
5
11
|
store;
|
|
6
12
|
constructor(store) {
|
|
@@ -17,6 +23,9 @@ export class ChatRoomService {
|
|
|
17
23
|
const existing = this.getRoom(id);
|
|
18
24
|
if (!existing)
|
|
19
25
|
return undefined;
|
|
26
|
+
if (input.parentRoomId && this.parentAssignmentCreatesCycle(id, input.parentRoomId)) {
|
|
27
|
+
throw new PiboRoomHierarchyCycleError();
|
|
28
|
+
}
|
|
20
29
|
const metadata = input.metadata ?? existing.metadata;
|
|
21
30
|
const updated = { ...existing, name: input.name ?? existing.name, topic: input.topic === null ? undefined : input.topic ?? existing.topic, parentRoomId: input.parentRoomId === null ? undefined : input.parentRoomId ?? existing.parentRoomId, retentionPolicyId: input.retentionPolicyId === null ? undefined : input.retentionPolicyId ?? existing.retentionPolicyId, metadata, workspace: roomWorkspaceFromMetadata(metadata), updatedAt: new Date().toISOString() };
|
|
22
31
|
this.store.db.prepare("UPDATE rooms SET name = ?, topic = ?, parent_room_id = ?, workspace = ?, retention_policy_id = ?, metadata_json = ?, updated_at = ? WHERE id = ?").run(updated.name, updated.topic ?? null, updated.parentRoomId ?? null, updated.workspace ?? null, updated.retentionPolicyId ?? null, JSON.stringify(updated.metadata), updated.updatedAt, id);
|
|
@@ -49,4 +58,16 @@ export class ChatRoomService {
|
|
|
49
58
|
requireRoom(roomId) { const room = this.getRoom(roomId); if (!room)
|
|
50
59
|
throw new Error("Room not found"); return room; }
|
|
51
60
|
close() { }
|
|
61
|
+
parentAssignmentCreatesCycle(roomId, parentRoomId) {
|
|
62
|
+
const roomsById = new Map(this.listRooms().map((room) => [room.id, room]));
|
|
63
|
+
const visited = new Set();
|
|
64
|
+
let currentId = parentRoomId;
|
|
65
|
+
while (currentId && !visited.has(currentId)) {
|
|
66
|
+
if (currentId === roomId)
|
|
67
|
+
return true;
|
|
68
|
+
visited.add(currentId);
|
|
69
|
+
currentId = roomsById.get(currentId)?.parentRoomId;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
52
73
|
}
|