@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.
Files changed (122) hide show
  1. package/README.md +20 -6
  2. package/compute-image/Dockerfile +47 -0
  3. package/compute-image/Dockerfile.dockerignore +5 -0
  4. package/dist/agent-runtime/context-build.js +26 -2
  5. package/dist/agent-runtime/contract.js +78 -28
  6. package/dist/agent-runtime/portable-history.js +19 -9
  7. package/dist/agent-runtime/registry.js +8 -2
  8. package/dist/agent-runtime/routed-session.js +120 -15
  9. package/dist/agent-runtimes/codex-native/adapter.js +12 -0
  10. package/dist/agent-runtimes/codex-native/resource-delivery.js +35 -7
  11. package/dist/agent-runtimes/omp/adapter.js +3 -7
  12. package/dist/agent-runtimes/pi/adapter.js +5 -3
  13. package/dist/agent-runtimes/pi/routed-session.js +59 -12
  14. package/dist/agent-runtimes/pi/runtime.js +6 -0
  15. package/dist/apps/chat/agent-profiles.js +31 -2
  16. package/dist/apps/chat/agent-store.js +98 -6
  17. package/dist/apps/chat/chat-request-normalizers.js +31 -0
  18. package/dist/apps/chat/data/chat-data-mappers.js +61 -11
  19. package/dist/apps/chat/data/project-service.js +119 -15
  20. package/dist/apps/chat/data/room-service.js +21 -0
  21. package/dist/apps/chat/data/session-query-service.js +33 -10
  22. package/dist/apps/chat/data/timeline-query-service.js +60 -35
  23. package/dist/apps/chat/loop-api.js +10 -2
  24. package/dist/apps/chat/output-compactor.js +144 -21
  25. package/dist/apps/chat/output-event-policy.js +179 -2
  26. package/dist/apps/chat/stream.js +16 -7
  27. package/dist/apps/chat/trace-v2.js +1 -0
  28. package/dist/apps/chat/web-app.js +317 -86
  29. package/dist/apps/chat-ui/assets/{dist-BDIATCQt.js → dist-CUQJqggN.js} +1 -1
  30. package/dist/apps/chat-ui/assets/{dist-mqCviI-c.js → dist-ClUlQWYN.js} +1 -1
  31. package/dist/apps/chat-ui/assets/{dist-JnW4v8UE.js → dist-Djul9BmZ.js} +1 -1
  32. package/dist/apps/chat-ui/assets/{dist-lRiLP9rr.js → dist-GD0JGdCi.js} +1 -1
  33. package/dist/apps/chat-ui/assets/{dist-BBDMeU5-.js → dist-W5HOqHym.js} +1 -1
  34. package/dist/apps/chat-ui/assets/index-BcjkX-iP.js +228 -0
  35. package/dist/apps/chat-ui/assets/index-CmqRSbBU.css +1 -0
  36. package/dist/apps/chat-ui/index.html +2 -2
  37. package/dist/apps/chat-vscode-web/assets/index-JvrPUGvI.js +43 -0
  38. package/dist/apps/chat-vscode-web/index.html +1 -1
  39. package/dist/apps/cli-ui/InkSessionApp.js +111 -12
  40. package/dist/apps/cli-ui/cliSessionsCommand.js +34 -8
  41. package/dist/cli-session/localSessionSource.js +153 -161
  42. package/dist/cli.js +7 -11
  43. package/dist/compute/cli.js +35 -16
  44. package/dist/compute/docker.js +341 -94
  45. package/dist/config/config.js +3 -0
  46. package/dist/core/context-build.js +7 -16
  47. package/dist/core/output-persistence-retry.js +484 -0
  48. package/dist/core/output-render-sequence.js +331 -0
  49. package/dist/core/profiles.js +24 -0
  50. package/dist/core/provider-recovery.js +7 -1
  51. package/dist/core/session-model.js +37 -1
  52. package/dist/core/session-router.js +78 -110
  53. package/dist/data/ingest-service.js +91 -9
  54. package/dist/data/navigation-store.js +5 -5
  55. package/dist/data/pibo-store.js +8 -1
  56. package/dist/data/schema.js +243 -3
  57. package/dist/data/session-store.js +3 -3
  58. package/dist/debug/agents.js +91 -89
  59. package/dist/debug/index.js +46 -2
  60. package/dist/debug/pty.js +127 -48
  61. package/dist/gateway/cli.js +4 -0
  62. package/dist/gateway/web.js +8 -8
  63. package/dist/local/client.js +3 -2
  64. package/dist/loops/channel.js +3 -1
  65. package/dist/loops/cli.js +2 -1
  66. package/dist/loops/service.js +90 -11
  67. package/dist/loops/store.js +100 -74
  68. package/dist/mcp/agent-context.js +13 -26
  69. package/dist/mcp/commands/info.js +3 -1
  70. package/dist/mcp/config-command.js +46 -2
  71. package/dist/mcp/config.js +18 -4
  72. package/dist/plugins/context-files-store.js +36 -6
  73. package/dist/plugins/context-files.js +52 -1
  74. package/dist/plugins/registry.js +2 -0
  75. package/dist/plugins/user-profile-resources.js +22 -0
  76. package/dist/previews/base-url.js +34 -0
  77. package/dist/previews/config.js +2 -32
  78. package/dist/ralph/store.js +5 -0
  79. package/dist/reliability/store.js +378 -106
  80. package/dist/session-ui/terminalRows.js +8 -4
  81. package/dist/sessions/pibo-data-store.js +145 -0
  82. package/dist/sessions/store.js +42 -0
  83. package/dist/setup/cli.js +422 -6
  84. package/dist/setup/installation-profiles.js +464 -0
  85. package/dist/shared/deterministic-digest.js +94 -0
  86. package/dist/shared/trace-engine.js +54 -0
  87. package/dist/shared/trace-event-projection.js +124 -67
  88. package/dist/shared/trace-history.js +47 -12
  89. package/dist/shared/trace-live-reducer.js +23 -4
  90. package/dist/shared/trace-nodes.js +17 -28
  91. package/dist/shared/trace-order.js +41 -5
  92. package/dist/shared/trace-page-merge.js +44 -14
  93. package/dist/shared/trace-patch-nodes.js +2 -0
  94. package/dist/shared/trace-subagent-links.js +4 -1
  95. package/dist/shared/trace-tool-identity.js +15 -8
  96. package/dist/signals/projector.js +28 -6
  97. package/dist/subagents/observation-query.js +121 -0
  98. package/dist/subagents/tool.js +7 -3
  99. package/dist/tools/agent-browser-leases.js +58 -23
  100. package/dist/tools/agent-browser-wrapper.js +1 -1
  101. package/dist/tools/browser-use-leases.js +129 -16
  102. package/dist/tools/browser-use-wrapper.js +1 -1
  103. package/dist/tools/index.js +27 -11
  104. package/dist/tools/python-runtime.js +10 -3
  105. package/dist/tools/registry.js +1 -1
  106. package/dist/tools/runtime/node-backend.js +16 -5
  107. package/dist/tools/runtime/node-worker-source.js +112 -33
  108. package/dist/tools/runtime/python-backend.js +16 -5
  109. package/dist/tools/runtime/python-worker-source.js +167 -16
  110. package/dist/tools/runtime/registry.js +23 -10
  111. package/dist/user-skills/store.js +0 -1
  112. package/docs/ops/vscode-extension-release.md +9 -1
  113. package/npm-shrinkwrap.json +9 -2
  114. package/package.json +9 -2
  115. package/scripts/docker-entrypoint.sh +46 -0
  116. package/scripts/prepare-agent-browser-wrapper.sh +70 -0
  117. package/scripts/prepare-browser-use-wrapper.sh +255 -0
  118. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +0 -228
  119. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +0 -1
  120. package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +0 -43
  121. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  122. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.1.vsix +0 -0
@@ -2,6 +2,8 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import os from "node:os";
3
3
  import { monitorEventLoopDelay } from "node:perf_hooks";
4
4
  import { PiboSteeringUnavailableError } from "../../core/events.js";
5
+ import { OutputRenderSequencer } from "../../core/output-render-sequence.js";
6
+ import { OutputPersistenceRetryQueue, } from "../../core/output-persistence-retry.js";
5
7
  import { PI_AGENT_RUNTIME_CAPABILITIES, PI_PROTOCOL_VERSION, } from "../../agent-runtimes/pi/adapter.js";
6
8
  import { AgentRuntimeBindingMissingError } from "../../agent-runtime/errors.js";
7
9
  import { buildPortableRuntimeContextSnapshot, profileWithRuntimeInstance, uniqueRuntimeDiagnostics, } from "../../agent-runtime/context-build.js";
@@ -9,7 +11,7 @@ import { PiboRuntimeResourceService } from "../../agent-runtime/resource-service
9
11
  import { summarizeSessionSignalStatus } from "../../signals/status.js";
10
12
  import { PiboWebHttpError, readJsonBody, responseJson } from "../../web/http.js";
11
13
  import { OutputCompactor } from "./output-compactor.js";
12
- import { isLiveOnlyOutputEvent, isPersistableOutputEvent } from "./output-event-policy.js";
14
+ import { isLiveOnlyOutputEvent, isPersistableOutputEvent, isPiboOutputEvent } from "./output-event-policy.js";
13
15
  import { chatRoomIdFromMetadata, isDefaultPiboRoom, isPiboRoomArchived, roomWorkspaceFromMetadata, withChatRoomId, withPiboRoomWorkspace, } from "./types/rooms.js";
14
16
  import { chatStreamFramesFromOutputEvent, createChatStreamState, nextTransientChatStreamFrameId } from "./stream.js";
15
17
  import { buildSessionNodes, buildTraceView } from "./trace.js";
@@ -17,7 +19,7 @@ import { DEFAULT_TRACE_EVENTS_PAGE_SIZE, annotateTracePage, etagForVersion, esti
17
19
  import { TRACE_V2_DEFAULT_TIMELINE_LIMIT, TRACE_V2_MAX_TIMELINE_LIMIT, TRACE_V2_PAYLOAD_DEFAULT_LIMIT_BYTES, TRACE_V2_PAYLOAD_MAX_LIMIT_BYTES, TRACE_V2_RAW_EVENTS_DEFAULT_LIMIT, TRACE_V2_RAW_EVENTS_MAX_LIMIT, TRACE_V2_TIMELINE_HARD_BYTES, TRACE_IMAGE_MAX_COUNT, parseTracePayloadRef, readTraceImagePayload, readTracePayloadChunk, traceRawEventsPageFromEvents, traceTimelinePageFromView, } from "./trace-v2.js";
18
20
  import { isChatWebSessionArchived } from "./session-metadata.js";
19
21
  import { withWorkflowSessionKind } from "../../sessions/workflow-session-kind.js";
20
- import { CustomAgentStore, createDefaultCustomAgentStore, previewCustomAgentCreate, previewCustomAgentUpdate, } from "./agent-store.js";
22
+ import { CustomAgentTargetReferenceError, CustomAgentStore, createDefaultCustomAgentStore, previewCustomAgentCreate, previewCustomAgentUpdate, } from "./agent-store.js";
21
23
  import { loadPiboModelDefaults, } from "../../core/model-defaults.js";
22
24
  import { inspectPiboContextBuild } from "../../core/context-build.js";
23
25
  import { isPiboThinkingLevel } from "../../core/thinking.js";
@@ -30,10 +32,10 @@ import { listMcpServerInfos } from "../../mcp/agent-context.js";
30
32
  import { getDefaultPiboWorkspace } from "../../core/workspace.js";
31
33
  import { findPiPackage, listPiPackages } from "../../pi-packages/store.js";
32
34
  import { ScopedUserSkillManager } from "../../user-skills/manager.js";
33
- import { ChatDataIngestService } from "../../data/ingest-service.js";
35
+ import { ChatDataIngestService, outputIdempotencyKey, outputPersistenceDeliveryKey } from "../../data/ingest-service.js";
34
36
  import { ChatEventCommandService } from "./data/event-command-service.js";
35
37
  import { ChatReadStateService } from "./data/read-state-service.js";
36
- import { ChatRoomService } from "./data/room-service.js";
38
+ import { ChatRoomService, PiboRoomHierarchyCycleError } from "./data/room-service.js";
37
39
  import { ChatSessionQueryService } from "./data/session-query-service.js";
38
40
  import { ChatTimelineQueryService } from "./data/timeline-query-service.js";
39
41
  import { ChatHistoryQueryService } from "./data/history-query-service.js";
@@ -55,7 +57,7 @@ import { chatSettingsRoute, chatSettingsRouteInvalidatesBootstrapCatalog, chatSe
55
57
  import { chatCapabilityRoute, chatCapabilityRouteRequiresSameOrigin, handleChatCapabilityRoute, } from "./chat-capability-routes.js";
56
58
  import { chatUserSkillRoute, chatUserSkillRouteRequiresSameOrigin, handleChatUserSkillRoute, syncChatUserSkills, } from "./chat-user-skill-routes.js";
57
59
  import { CHAT_WEB_API_PREFIX, agentFolderResourceId, agentResourceId, projectResourcePath, projectSessionResourceId, projectWorkflowHumanActionsResource, projectWorkflowSessionStartResource, roomResourcePath, sessionActionResource, sessionResourceId, signalResource, workflowArchiveResourceId, workflowCatalogResourceId, workflowDraftActionResource, workflowDraftManualTriggerRunResource, workflowDraftResourceId, workflowDuplicateResourceId, workflowNextDraftResourceId, workflowPickerKind, workflowPromptAssetResourceId, workflowVersionResource, } from "./chat-api-routes.js";
58
- import { assertProjectSessionPatchFields, buildStreamingFixtureSchedule, createAgentInput, createAgentUpdate, createRoomUpdate, createSessionUpdate, normalizeAgentArchived, normalizeAgentFolderName, normalizeClientTxnId, normalizeMessageDelivery, normalizeMessageText, normalizeParentRoomId, normalizeProjectArchived, normalizeProjectDescription, normalizeProjectPath, normalizeProjectSessionArchived, normalizeRoomDeleteConfirmation, normalizeRoomName, normalizeRoomTopic, normalizeRoomType, normalizeRoomWorkspace, normalizeSessionDeleteConfirmation, normalizeSessionTitle, normalizeStreamingFixtureCadenceMs, normalizeStreamingFixtureDeltas, normalizeStreamingFixtureMix, normalizeStreamingFixturePreludeMessages, normalizeStreamingFixturePreludeOnly, normalizeStreamingFixtureProfile, normalizeStreamingFixtureSuppressLiveDeltas, normalizeStreamingFixtureTraceSnapshots, resolveCreateSessionProfile, } from "./chat-request-normalizers.js";
60
+ import { assertProjectSessionPatchFields, buildStreamingFixtureSchedule, createAgentInput, createAgentUpdate, createRoomUpdate, createSessionUpdate, normalizeAgentArchived, normalizeAgentFolderName, normalizeClientTxnId, normalizeMessageDelivery, normalizeMessageText, normalizeParentRoomId, normalizeProjectArchived, normalizeProjectDescription, normalizeProjectPath, normalizeProjectSessionArchived, normalizeRoomDeleteConfirmation, normalizeRoomName, normalizeRoomTopic, normalizeRoomType, normalizeRoomWorkspace, normalizeSessionDeleteConfirmation, normalizeSessionTitle, normalizeStreamingFixtureCadenceMs, normalizeStreamingFixtureDeltas, normalizeStreamingFixtureMix, normalizeStreamingFixturePreludeMessages, normalizeStreamingFixturePreludeOnly, normalizeStreamingFixtureProfile, normalizeStreamingFixtureSuppressLiveDeltas, normalizeStreamingFixtureTraceSnapshots, normalizeStreamingFixtureToolInterleaving, resolveCreateSessionProfile, } from "./chat-request-normalizers.js";
59
61
  import { ChatWorkflowArchiveStore, ChatWorkflowDraftStore, ChatWorkflowLifecycleEventStore, ChatWorkflowPromptAssetStore, ChatWorkflowPublishedVersionStore, ChatWorkflowTombstoneStore, hashWorkflowDefinitionJson, normalizeWorkflowPromptAssetLabel, sanitizeWorkflowDiagnostics, } from "./workflow-persistence.js";
60
62
  import { normalizeProjectWorkflowHumanActionBody, projectWorkflowHumanActionDiagnosticResponse, projectWorkflowHumanActionLifecyclePayload, projectWorkflowHumanActionRuntimeDiagnostic, projectWorkflowHumanActionSubmittedLifecyclePayload, projectWorkflowPendingHumanActionFromToken, validateProjectWorkflowHumanActionRequest, } from "./project-workflow-human-actions.js";
61
63
  import { createProjectWorkflowRunCurrent, createProjectWorkflowSessionSnapshot, normalizeProjectWorkflowSessionConfiguration, workflowVersionFromSnapshot, } from "./project-workflow-sessions.js";
@@ -396,80 +398,210 @@ function ensureEventIndexing(state, context) {
396
398
  state.subscribedContext = context;
397
399
  state.unsubscribe = context.channelContext.subscribe((event) => {
398
400
  const startedAt = performance.now();
401
+ let prepared;
399
402
  try {
400
- state.activeTraceSessions.add(event.piboSessionId);
401
- const session = context.channelContext.getSession(event.piboSessionId);
403
+ if (!isPiboOutputEvent(event)) {
404
+ state.outputPersistenceRetries.quarantine("runtime_output_event_invalid");
405
+ recordPersistenceError(state.persistenceMetrics, new Error("runtime_output_event_invalid"));
406
+ return;
407
+ }
408
+ const positionedEvent = state.outputRenderSequencer.position(event);
409
+ state.activeTraceSessions.add(positionedEvent.piboSessionId);
410
+ const session = context.channelContext.getSession(positionedEvent.piboSessionId);
402
411
  const room = session ? ensureSessionRoom(state, context, session) : undefined;
403
- const result = state.outputCompactor.compact(event);
412
+ const result = state.outputCompactor.prepare(positionedEvent);
413
+ prepared = result;
404
414
  for (const liveEvent of result.liveEvents) {
405
415
  if (isPersistableOutputEvent(liveEvent))
406
416
  continue;
407
- state.sessionQuery.recordEvent(liveEvent, session);
408
417
  const transient = recordTransientReplayEvent(state, { roomId: room?.id, piboSessionId: liveEvent.piboSessionId, eventType: liveEvent.type, payload: liveEvent });
418
+ state.sessionQuery.recordEvent(liveEvent, session, undefined, new Date(transient.createdAtMs).toISOString());
409
419
  if (isLiveOnlyOutputEvent(liveEvent) && !hasLiveObserver(state, liveEvent.piboSessionId))
410
420
  continue;
411
421
  for (const listener of state.liveListeners) {
412
422
  listener(transient);
413
423
  }
414
424
  }
415
- for (const persistableEvent of result.persistedEvents) {
416
- if (!isPersistableOutputEvent(persistableEvent))
417
- continue;
418
- let stored = state.eventCommands.appendOutputEvent(persistableEvent, {
425
+ const persistableEvents = result.persistedEvents.filter(isPersistableOutputEvent);
426
+ if (persistableEvents.length === 0) {
427
+ result.ack();
428
+ }
429
+ else {
430
+ const persistenceState = {
431
+ version: 1,
432
+ piboSessionId: positionedEvent.piboSessionId,
419
433
  roomId: room?.id,
420
434
  actorId: session?.id,
421
- });
422
- if (!stored && session) {
423
- try {
424
- const createdAt = new Date().toISOString();
425
- const ingested = state.ingestService.ingestOutputEvent({
426
- session,
427
- roomId: room?.id,
428
- actorId: session.id,
429
- event: persistableEvent,
430
- createdAt,
431
- });
432
- stored = {
433
- streamId: ingested.streamId,
434
- roomId: room?.id,
435
- piboSessionId: persistableEvent.piboSessionId,
436
- eventId: "eventId" in persistableEvent && typeof persistableEvent.eventId === "string" ? persistableEvent.eventId : `pibo.output:${persistableEvent.type}:${ingested.streamId}`,
437
- eventType: persistableEvent.type,
438
- actorType: "assistant",
439
- actorId: session.id,
440
- createdAt,
441
- retentionClass: reliabilityRetentionClassForOutputEvent(persistableEvent),
442
- payload: persistableEvent,
443
- };
444
- }
445
- catch (error) {
446
- console.warn("[chat-web] failed to write output event into V2", error);
447
- }
448
- }
449
- if (!stored)
450
- continue;
451
- if (persistableEvent.type === "assistant_message" || persistableEvent.type === "message_finished" || persistableEvent.type === "session_error") {
452
- markActiveSessionRead(state, persistableEvent.piboSessionId, stored.streamId);
453
- }
454
- state.sessionQuery.recordEvent(persistableEvent, session, stored.streamId);
455
- state.reliabilityStore.append({
456
- topic: "pibo.output",
457
- key: persistableEvent.piboSessionId,
458
- eventId: `pibo.output:${persistableEvent.piboSessionId}:${persistableEvent.type}:${randomUUID()}`,
459
- retentionClass: reliabilityRetentionClassForOutputEvent(persistableEvent),
460
- payload: boundedReliabilityOutputPayload(state, persistableEvent),
461
- });
462
- for (const listener of state.liveListeners)
463
- listener(stored);
435
+ deliveries: persistableEvents.map((persistableEvent) => ({
436
+ deliveryId: outputPersistenceDeliveryKey(persistableEvent),
437
+ event: persistableEvent,
438
+ })),
439
+ };
440
+ state.outputPersistenceRetries.enqueue(createWebOutputPersistenceJob({
441
+ state,
442
+ context,
443
+ persistenceState,
444
+ onSuccess: result.ack,
445
+ onDeadLetter: (error) => {
446
+ result.rollback();
447
+ recordPersistenceError(state.persistenceMetrics, error);
448
+ },
449
+ }));
464
450
  }
465
451
  recordPersistenceDuration(state.persistenceMetrics, performance.now() - startedAt);
466
452
  }
467
453
  catch (error) {
454
+ prepared?.rollback();
468
455
  recordPersistenceError(state.persistenceMetrics, error);
469
456
  console.error("[chat-web] failed to index router event", error);
470
- throw error;
471
457
  }
472
458
  });
459
+ state.outputPersistenceRetries.recover((job) => {
460
+ const persistenceState = parseWebOutputPersistenceState(job.payload);
461
+ if (!persistenceState)
462
+ throw new Error(`Invalid durable output persistence job ${job.key}`);
463
+ return createWebOutputPersistenceJob({ state, context, persistenceState });
464
+ });
465
+ }
466
+ function createWebOutputPersistenceJob(input) {
467
+ const firstEvent = input.persistenceState.deliveries[0]?.event;
468
+ return {
469
+ key: JSON.stringify(input.persistenceState.deliveries.map((delivery) => outputPersistenceDeliveryKey(delivery.event))),
470
+ piboSessionId: input.persistenceState.piboSessionId,
471
+ eventId: firstEvent ? outputPersistenceDeliveryKey(firstEvent) : undefined,
472
+ payload: input.persistenceState,
473
+ run: (retryContext) => deliverWebOutputPersistenceState(input.state, input.context, retryContext),
474
+ onSuccess: input.onSuccess,
475
+ onDeadLetter: input.onDeadLetter,
476
+ };
477
+ }
478
+ function deliverWebOutputPersistenceState(state, context, retryContext) {
479
+ const persistenceState = parseWebOutputPersistenceState(retryContext.payload);
480
+ if (!persistenceState)
481
+ throw new Error("Invalid durable web output persistence state");
482
+ const session = context.channelContext.getSession(persistenceState.piboSessionId);
483
+ if (!session)
484
+ throw new Error(`No session available for ${persistenceState.piboSessionId}`);
485
+ for (const delivery of persistenceState.deliveries) {
486
+ if (!delivery.v2) {
487
+ const createdAt = new Date().toISOString();
488
+ const ingested = state.ingestService.ingestOutputEvent({
489
+ session,
490
+ roomId: persistenceState.roomId,
491
+ actorId: persistenceState.actorId ?? session.id,
492
+ event: delivery.event,
493
+ createdAt,
494
+ });
495
+ const storedEvent = state.dataStore.eventLog.findByIdempotencyKey(delivery.deliveryId);
496
+ if (!storedEvent || storedEvent.streamId !== ingested.streamId) {
497
+ throw new Error(`Missing V2 event ${ingested.streamId} for ${delivery.deliveryId}`);
498
+ }
499
+ delivery.v2 = {
500
+ streamId: ingested.streamId,
501
+ createdAt: storedEvent.createdAt,
502
+ eventId: eventIdentityForDelivery(delivery.event),
503
+ duplicate: ingested.duplicate,
504
+ };
505
+ retryContext.updatePayload(persistenceState);
506
+ }
507
+ if (!delivery.reliabilityDelivered) {
508
+ if (delivery.reliabilityPayload === undefined) {
509
+ delivery.reliabilityPayload = boundedReliabilityOutputPayload(state, delivery.event);
510
+ retryContext.updatePayload(persistenceState);
511
+ }
512
+ const deliveryKey = delivery.deliveryId;
513
+ state.reliabilityStore.appendOnce({
514
+ topic: "pibo.output",
515
+ key: delivery.event.piboSessionId,
516
+ eventId: deliveryKey,
517
+ idempotencyKey: deliveryKey,
518
+ retentionClass: reliabilityRetentionClassForOutputEvent(delivery.event),
519
+ payload: delivery.reliabilityPayload,
520
+ });
521
+ delivery.reliabilityDelivered = true;
522
+ retryContext.updatePayload(persistenceState);
523
+ }
524
+ if (!delivery.sideEffectsDelivered && state.reliabilityStore.hasDeliveryReceipt(delivery.deliveryId, "chat-web-observable-v1")) {
525
+ delivery.sideEffectsDelivered = true;
526
+ retryContext.updatePayload(persistenceState);
527
+ }
528
+ else if (!delivery.sideEffectsDelivered) {
529
+ const stored = storedChatEventForDelivery(persistenceState, delivery);
530
+ if (delivery.event.type === "assistant_message" || delivery.event.type === "message_finished" || delivery.event.type === "session_error") {
531
+ markActiveSessionRead(state, delivery.event.piboSessionId, stored.streamId);
532
+ }
533
+ state.sessionQuery.recordEvent(delivery.event, session, stored.streamId, stored.createdAt);
534
+ for (const listener of state.liveListeners) {
535
+ try {
536
+ listener(stored);
537
+ }
538
+ catch (error) {
539
+ console.error("[chat-web] live listener failed", error);
540
+ }
541
+ }
542
+ // This receipt is deliberately recorded after projection and live sends.
543
+ // A crash before it replays the same deliveryId/streamId at least once;
544
+ // recording before sends would trade duplicates for silent loss.
545
+ state.reliabilityStore.recordDeliveryReceipt(delivery.deliveryId, "chat-web-observable-v1");
546
+ delivery.sideEffectsDelivered = true;
547
+ retryContext.updatePayload(persistenceState);
548
+ }
549
+ }
550
+ }
551
+ function storedChatEventForDelivery(persistenceState, delivery) {
552
+ if (!delivery.v2)
553
+ throw new Error(`Missing V2 phase for ${outputPersistenceDeliveryKey(delivery.event)}`);
554
+ return {
555
+ streamId: delivery.v2.streamId,
556
+ ...(persistenceState.roomId ? { roomId: persistenceState.roomId } : {}),
557
+ piboSessionId: persistenceState.piboSessionId,
558
+ eventId: delivery.v2.eventId,
559
+ eventType: delivery.event.type,
560
+ actorType: "assistant",
561
+ ...(persistenceState.actorId ? { actorId: persistenceState.actorId } : {}),
562
+ createdAt: delivery.v2.createdAt,
563
+ retentionClass: reliabilityRetentionClassForOutputEvent(delivery.event),
564
+ payload: delivery.event,
565
+ };
566
+ }
567
+ function eventIdentityForDelivery(event) {
568
+ return "eventId" in event && typeof event.eventId === "string"
569
+ ? event.eventId
570
+ : outputPersistenceDeliveryKey(event);
571
+ }
572
+ function parseWebOutputPersistenceState(value) {
573
+ if (!value || typeof value !== "object" || Array.isArray(value))
574
+ return undefined;
575
+ const candidate = value;
576
+ if (candidate.version !== 1 || typeof candidate.piboSessionId !== "string" || !Array.isArray(candidate.deliveries))
577
+ return undefined;
578
+ const deliveries = [];
579
+ for (const rawDelivery of candidate.deliveries) {
580
+ if (!rawDelivery || typeof rawDelivery !== "object" || Array.isArray(rawDelivery))
581
+ return undefined;
582
+ const delivery = rawDelivery;
583
+ if (!isPiboOutputEvent(delivery.event) || !outputIdempotencyKey(delivery.event))
584
+ return undefined;
585
+ const deliveryId = outputPersistenceDeliveryKey(delivery.event);
586
+ if (delivery.deliveryId !== undefined && delivery.deliveryId !== deliveryId)
587
+ return undefined;
588
+ const v2 = delivery.v2;
589
+ if (v2 !== undefined && (!v2 || typeof v2 !== "object" || Array.isArray(v2)
590
+ || !Number.isSafeInteger(v2.streamId)
591
+ || typeof v2.createdAt !== "string"
592
+ || typeof v2.eventId !== "string"))
593
+ return undefined;
594
+ deliveries.push({ ...rawDelivery, deliveryId });
595
+ }
596
+ if (!deliveries.length)
597
+ return undefined;
598
+ return {
599
+ version: 1,
600
+ piboSessionId: candidate.piboSessionId,
601
+ ...(typeof candidate.roomId === "string" ? { roomId: candidate.roomId } : {}),
602
+ ...(typeof candidate.actorId === "string" ? { actorId: candidate.actorId } : {}),
603
+ deliveries,
604
+ };
473
605
  }
474
606
  function reliabilityRetentionClassForOutputEvent(event) {
475
607
  if (event.type === "assistant_delta" || event.type === "thinking_delta" || event.type === "tool_execution_updated")
@@ -943,6 +1075,7 @@ function ensureCustomAgentProfiles(state, context) {
943
1075
  function serializeCustomAgent(agent, context) {
944
1076
  return {
945
1077
  ...agent,
1078
+ brokenNativeTools: listBrokenNativeTools(agent.nativeTools, context),
946
1079
  brokenContextFiles: listBrokenContextFiles(agent.contextFiles, context),
947
1080
  };
948
1081
  }
@@ -976,6 +1109,7 @@ const RUNTIME_PROFILE_UPDATE_FIELDS = new Set([
976
1109
  "mcpServers",
977
1110
  "piPackages",
978
1111
  "mainModel",
1112
+ "mainModelFallbacks",
979
1113
  "subagentModel",
980
1114
  "thinkingLevel",
981
1115
  "mainThinkingLevel",
@@ -1040,6 +1174,13 @@ async function requireValidCustomAgentRuntime(agent, context) {
1040
1174
  return;
1041
1175
  throw new PiboWebHttpError(`Agent runtime selection is invalid: ${errors.map((diagnostic) => diagnostic.message).join(" ")}`, 400);
1042
1176
  }
1177
+ function listBrokenNativeTools(names, context) {
1178
+ const catalog = context.channelContext.getCapabilityCatalog?.();
1179
+ if (!catalog)
1180
+ return [];
1181
+ const knownNames = new Set(catalog.nativeTools.map((tool) => tool.name));
1182
+ return names.filter((name) => !knownNames.has(name));
1183
+ }
1043
1184
  function listBrokenContextFiles(keys, context) {
1044
1185
  const catalog = context.channelContext.getCapabilityCatalog?.();
1045
1186
  if (!catalog)
@@ -2211,6 +2352,17 @@ function normalizeAgentDeleteConfirmation(value) {
2211
2352
  }
2212
2353
  return value.trim();
2213
2354
  }
2355
+ function requireCustomAgentNotTargeted(state, agent, options = {}) {
2356
+ const dependents = state.agentStore.listReferencingAgents(agent.id)
2357
+ .filter((dependent) => !options.activeOnly || !dependent.archivedAt);
2358
+ if (dependents.length === 0)
2359
+ return;
2360
+ const error = new CustomAgentTargetReferenceError(agent.profileName, dependents.map((dependent) => dependent.profileName));
2361
+ const message = options.activeOnly
2362
+ ? error.message.replace("targeted by custom agents", "targeted by active custom agents")
2363
+ : error.message;
2364
+ throw new PiboWebHttpError(message, 409);
2365
+ }
2214
2366
  async function deleteSessionsForAgentProfile(state, context, webSession, profileNames) {
2215
2367
  const deleteSession = context.channelContext.deleteSession;
2216
2368
  if (!deleteSession)
@@ -2230,10 +2382,13 @@ async function deleteSessionsForAgentProfile(state, context, webSession, profile
2230
2382
  }
2231
2383
  }
2232
2384
  const orderedIds = [...ids].sort((left, right) => sessionDepth(sessionsById.get(right), sessionsById) - sessionDepth(sessionsById.get(left), sessionsById));
2385
+ for (const id of orderedIds) {
2386
+ await state.projectService.deleteProjectSessionWithCanonicalDelete(id, () => deleteSession(id));
2387
+ state.sessionQuery.deleteSessions([id]);
2388
+ state.eventCommands.deleteSessions([id]);
2389
+ }
2233
2390
  for (const id of orderedIds)
2234
- await deleteSession(id);
2235
- state.sessionQuery.deleteSessions(orderedIds);
2236
- state.eventCommands.deleteSessions(orderedIds);
2391
+ disposeSessionStreamState(state, id);
2237
2392
  return orderedIds;
2238
2393
  }
2239
2394
  async function deleteSessionSubtree(state, context, webSession, rootSession) {
@@ -2254,10 +2409,13 @@ async function deleteSessionSubtree(state, context, webSession, rootSession) {
2254
2409
  }
2255
2410
  }
2256
2411
  const orderedIds = [...ids].sort((left, right) => sessionDepth(sessionsById.get(right), sessionsById) - sessionDepth(sessionsById.get(left), sessionsById));
2412
+ for (const id of orderedIds) {
2413
+ await state.projectService.deleteProjectSessionWithCanonicalDelete(id, () => deleteSession(id));
2414
+ state.sessionQuery.deleteSessions([id]);
2415
+ state.eventCommands.deleteSessions([id]);
2416
+ }
2257
2417
  for (const id of orderedIds)
2258
- await deleteSession(id);
2259
- state.sessionQuery.deleteSessions(orderedIds);
2260
- state.eventCommands.deleteSessions(orderedIds);
2418
+ disposeSessionStreamState(state, id);
2261
2419
  return orderedIds;
2262
2420
  }
2263
2421
  async function deleteRoomTree(state, context, webSession, room, confirmName) {
@@ -2292,15 +2450,23 @@ async function deleteRoomTree(state, context, webSession, room, confirmName) {
2292
2450
  }
2293
2451
  }
2294
2452
  const orderedSessionIds = [...ids].sort((left, right) => sessionDepth(sessionsById.get(right), sessionsById) - sessionDepth(sessionsById.get(left), sessionsById));
2453
+ for (const id of orderedSessionIds) {
2454
+ await state.projectService.deleteProjectSessionWithCanonicalDelete(id, () => deleteSession(id));
2455
+ state.sessionQuery.deleteSessions([id]);
2456
+ state.eventCommands.deleteSessions([id]);
2457
+ }
2295
2458
  for (const id of orderedSessionIds)
2296
- await deleteSession(id);
2297
- state.sessionQuery.deleteSessions(orderedSessionIds);
2298
- state.eventCommands.deleteSessions(orderedSessionIds);
2459
+ disposeSessionStreamState(state, id);
2299
2460
  state.eventCommands.deleteRooms(roomIds);
2300
2461
  const orderedRoomIds = [...roomIds].reverse();
2301
2462
  state.roomService.deleteRooms(orderedRoomIds);
2302
2463
  return { deletedRoomIds: orderedRoomIds, deletedSessionIds: orderedSessionIds };
2303
2464
  }
2465
+ function disposeSessionStreamState(state, piboSessionId) {
2466
+ state.outputCompactor.disposeSession(piboSessionId);
2467
+ state.outputRenderSequencer.disposeSession(piboSessionId);
2468
+ state.activeTraceSessions.delete(piboSessionId);
2469
+ }
2304
2470
  function sessionDepth(session, sessionsById) {
2305
2471
  let depth = 0;
2306
2472
  let current = session;
@@ -2652,9 +2818,6 @@ function parseEventStreamMode(value, fallback) {
2652
2818
  return value;
2653
2819
  return fallback;
2654
2820
  }
2655
- function isPiboOutputEvent(value) {
2656
- return !!value && typeof value === "object" && !Array.isArray(value) && typeof value.type === "string";
2657
- }
2658
2821
  function liveEventMatches(event, input) {
2659
2822
  if (input.roomId && event.roomId !== input.roomId)
2660
2823
  return false;
@@ -3236,21 +3399,23 @@ function startChatStreamingFixture(input) {
3236
3399
  const deltas = normalizeStreamingFixtureDeltas(input.body.deltas, mix);
3237
3400
  const traceSnapshots = normalizeStreamingFixtureTraceSnapshots(input.body.traceSnapshots);
3238
3401
  const suppressLiveDeltas = normalizeStreamingFixtureSuppressLiveDeltas(input.body.suppressLiveDeltas);
3402
+ const toolInterleaving = normalizeStreamingFixtureToolInterleaving(input.body.toolInterleaving);
3239
3403
  const scheduleMs = buildStreamingFixtureSchedule(deltas.length, cadenceMs, profile);
3240
3404
  const reasoningDeltas = mix === "reasoning-text" ? [" think", " plan", " check", " answer"] : [];
3241
3405
  const reasoningScheduleMs = reasoningDeltas.map((_, index) => Math.max(10, Math.round(((index + 1) * cadenceMs) / 2)));
3242
3406
  const eventId = `streaming-fixture-${randomUUID()}`;
3243
3407
  const emit = (event) => {
3244
- if (traceSnapshots && (event.type === "assistant_delta" || event.type === "assistant_message" || event.type === "message_finished")) {
3245
- input.state.outputCompactor.compact(event);
3408
+ const positionedEvent = input.state.outputRenderSequencer.position(event);
3409
+ if (traceSnapshots && (positionedEvent.type === "assistant_delta" || positionedEvent.type === "assistant_message" || positionedEvent.type === "message_finished")) {
3410
+ input.state.outputCompactor.compact(positionedEvent);
3246
3411
  }
3247
- if (suppressLiveDeltas && event.type === "assistant_delta")
3412
+ if (suppressLiveDeltas && positionedEvent.type === "assistant_delta")
3248
3413
  return;
3249
3414
  const liveEvent = recordTransientReplayEvent(input.state, {
3250
3415
  roomId: room.id,
3251
3416
  piboSessionId: selectedSession.id,
3252
- eventType: event.type,
3253
- payload: event,
3417
+ eventType: positionedEvent.type,
3418
+ payload: positionedEvent,
3254
3419
  });
3255
3420
  for (const listener of input.state.liveListeners)
3256
3421
  listener(liveEvent);
@@ -3277,6 +3442,17 @@ function startChatStreamingFixture(input) {
3277
3442
  });
3278
3443
  }
3279
3444
  emit({ type: "message_started", piboSessionId: selectedSession.id, eventId, text: "Streaming benchmark fixture", source: "service" });
3445
+ const toolBase = {
3446
+ piboSessionId: selectedSession.id,
3447
+ eventId,
3448
+ toolCallId: "streaming-fixture-reused-tool",
3449
+ toolName: "read",
3450
+ };
3451
+ if (toolInterleaving) {
3452
+ emit({ ...toolBase, type: "tool_call", args: { path: "README.md" }, argsComplete: true });
3453
+ emitAt(Math.max(10, Math.round(cadenceMs / 3)), { ...toolBase, type: "tool_execution_started", args: { path: "README.md" } });
3454
+ emitAt(Math.max(20, Math.round(cadenceMs * 1.5)), { ...toolBase, type: "tool_execution_updated", args: { path: "README.md" }, partialResult: "Streaming fixture tool update" });
3455
+ }
3280
3456
  if (reasoningDeltas.length) {
3281
3457
  emit({ type: "thinking_started", piboSessionId: selectedSession.id, eventId, thinkingIndex: 0 });
3282
3458
  reasoningDeltas.forEach((delta, index) => {
@@ -3290,6 +3466,9 @@ function startChatStreamingFixture(input) {
3290
3466
  const finalText = deltas.join("");
3291
3467
  const finalReasoning = reasoningDeltas.join("");
3292
3468
  const finishDelayMs = Math.max(scheduleMs[scheduleMs.length - 1] ?? 0, reasoningScheduleMs[reasoningScheduleMs.length - 1] ?? 0) + cadenceMs;
3469
+ if (toolInterleaving) {
3470
+ emitAt(Math.max(30, finishDelayMs - Math.max(10, Math.round(cadenceMs / 2))), { ...toolBase, type: "tool_execution_finished", result: "Streaming fixture tool result", isError: false });
3471
+ }
3293
3472
  emitAt(finishDelayMs, { type: "assistant_message", piboSessionId: selectedSession.id, eventId, assistantIndex: 0, text: finalText });
3294
3473
  emitAt(finishDelayMs, { type: "message_finished", piboSessionId: selectedSession.id, eventId, source: "service" });
3295
3474
  return responseJson({
@@ -3304,6 +3483,7 @@ function startChatStreamingFixture(input) {
3304
3483
  preludeMessages,
3305
3484
  traceSnapshots,
3306
3485
  suppressLiveDeltas,
3486
+ toolInterleaving,
3307
3487
  scheduleMs,
3308
3488
  reasoningScheduleMs,
3309
3489
  reasoningDeltaCount: reasoningDeltas.length,
@@ -3428,6 +3608,7 @@ export function createChatWebApp(options = {}) {
3428
3608
  ensurePrivateChatUploadDirectory();
3429
3609
  const defaultProfile = options.defaultProfile ?? "base";
3430
3610
  const dataStore = createDataStore(options);
3611
+ const reliabilityStore = createReliabilityStore(options.reliabilityStorePath);
3431
3612
  const eventLoopDelay = monitorEventLoopDelay({ resolution: 20 });
3432
3613
  eventLoopDelay.enable();
3433
3614
  const state = {
@@ -3439,7 +3620,7 @@ export function createChatWebApp(options = {}) {
3439
3620
  roomService: new ChatRoomService(dataStore),
3440
3621
  projectService: new ChatProjectService(options.projectStorePath),
3441
3622
  agentStore: createAgentStore(options.agentStorePath),
3442
- reliabilityStore: createReliabilityStore(options.reliabilityStorePath),
3623
+ reliabilityStore,
3443
3624
  cronStore: createDefaultPiboCronStore({ path: options.cronStorePath }),
3444
3625
  loopStore: createDefaultPiboLoopStore({ path: options.ralphStorePath }),
3445
3626
  dataStore,
@@ -3447,6 +3628,8 @@ export function createChatWebApp(options = {}) {
3447
3628
  traceCache: new Map(),
3448
3629
  traceTimelinePageCache: new Map(),
3449
3630
  outputCompactor: new OutputCompactor(),
3631
+ outputPersistenceRetries: new OutputPersistenceRetryQueue({ durableStore: reliabilityStore }),
3632
+ outputRenderSequencer: new OutputRenderSequencer(),
3450
3633
  liveListeners: new Set(),
3451
3634
  transientReplaySequence: 0,
3452
3635
  transientReplayBuffer: [],
@@ -3486,11 +3669,14 @@ export function createChatWebApp(options = {}) {
3486
3669
  state.unsubscribe = undefined;
3487
3670
  state.subscribedContext = undefined;
3488
3671
  state.eventLoopDelay.disable();
3672
+ state.outputPersistenceRetries.dispose();
3489
3673
  state.projectService.close();
3490
3674
  state.agentStore.close();
3491
3675
  state.reliabilityStore.close();
3492
3676
  state.cronStore.close();
3493
3677
  state.loopStore.close();
3678
+ state.outputCompactor.disposeAll();
3679
+ state.outputRenderSequencer.disposeAll();
3494
3680
  state.dataStore.close();
3495
3681
  },
3496
3682
  async handleRequest(request, context) {
@@ -3789,7 +3975,9 @@ export function createChatWebApp(options = {}) {
3789
3975
  const webSession = await requireSession(request, context);
3790
3976
  const body = await readJsonBody(request);
3791
3977
  try {
3792
- requireSharedProject(state, webSession, projectResource.projectId, { includeArchived: true });
3978
+ const existingProject = requireSharedProject(state, webSession, projectResource.projectId, { includeArchived: true });
3979
+ if (existingProject.metadata.default === true)
3980
+ throw new PiboWebHttpError("Project Manager cannot be changed", 400);
3793
3981
  const project = state.projectService.updateProject(projectResource.projectId, {
3794
3982
  ...(body.name !== undefined ? { name: normalizeRoomName(body.name) } : {}),
3795
3983
  ...(body.description !== undefined ? { description: normalizeProjectDescription(body.description) ?? null } : {}),
@@ -3810,7 +3998,9 @@ export function createChatWebApp(options = {}) {
3810
3998
  const webSession = await requireSession(request, context);
3811
3999
  const body = await readJsonBody(request);
3812
4000
  try {
3813
- requireSharedProject(state, webSession, projectResource.projectId, { includeArchived: true });
4001
+ const project = requireSharedProject(state, webSession, projectResource.projectId, { includeArchived: true });
4002
+ if (project.metadata.default === true)
4003
+ throw new PiboWebHttpError("Project Manager cannot be deleted", 400);
3814
4004
  return responseJson(state.projectService.deleteProject(projectResource.projectId, {
3815
4005
  confirmName: normalizeRoomDeleteConfirmation(body.confirmName),
3816
4006
  deleteFiles: body.deleteFiles === true,
@@ -4100,22 +4290,43 @@ export function createChatWebApp(options = {}) {
4100
4290
  if (!rootPiboSessionId)
4101
4291
  throw new PiboWebHttpError("rootPiboSessionId is required", 400);
4102
4292
  requireSharedSession(context, rootPiboSessionId);
4103
- if (!context.channelContext.snapshotSignalTree || !context.channelContext.subscribeSignalTree) {
4293
+ const includeStatuses = parseBooleanSearchParam(url, "includeStatuses");
4294
+ if (!context.channelContext.snapshotSignalTree
4295
+ || !context.channelContext.subscribeSignalTree
4296
+ || (includeStatuses && (!context.channelContext.snapshotSignalStatuses || !context.channelContext.subscribeSignalStatuses))) {
4104
4297
  throw new PiboWebHttpError("Signal registry is not available", 503);
4105
4298
  }
4106
- let unsubscribe;
4299
+ let unsubscribeTree;
4300
+ let unsubscribeStatuses;
4107
4301
  let heartbeat;
4302
+ let closed = false;
4108
4303
  const stream = new ReadableStream({
4109
4304
  start: (controller) => {
4110
4305
  writeJsonSse(controller, "signal_snapshot", context.channelContext.snapshotSignalTree(rootPiboSessionId));
4111
- unsubscribe = context.channelContext.subscribeSignalTree(rootPiboSessionId, (patch) => {
4112
- writeJsonSse(controller, "signal_patch", patch, String(patch.toVersion));
4306
+ unsubscribeTree = context.channelContext.subscribeSignalTree(rootPiboSessionId, (patch) => {
4307
+ if (!closed)
4308
+ writeJsonSse(controller, "signal_patch", patch, String(patch.toVersion));
4113
4309
  });
4114
- heartbeat = setInterval(() => writeSseComment(controller, "heartbeat"), 25_000);
4310
+ if (includeStatuses) {
4311
+ writeJsonSse(controller, "signal_status_snapshot", context.channelContext.snapshotSignalStatuses());
4312
+ unsubscribeStatuses = context.channelContext.subscribeSignalStatuses((patch) => {
4313
+ if (closed)
4314
+ return;
4315
+ const statusPatch = compactSignalStatusPatch(patch);
4316
+ writeJsonSse(controller, "signal_status_patch", statusPatch, `${patch.rootPiboSessionId}:${patch.toVersion}`);
4317
+ });
4318
+ }
4319
+ heartbeat = setInterval(() => {
4320
+ if (!closed)
4321
+ writeSseComment(controller, "heartbeat");
4322
+ }, 25_000);
4115
4323
  },
4116
4324
  cancel: () => {
4117
- unsubscribe?.();
4118
- unsubscribe = undefined;
4325
+ closed = true;
4326
+ unsubscribeTree?.();
4327
+ unsubscribeTree = undefined;
4328
+ unsubscribeStatuses?.();
4329
+ unsubscribeStatuses = undefined;
4119
4330
  if (heartbeat)
4120
4331
  clearInterval(heartbeat);
4121
4332
  heartbeat = undefined;
@@ -4519,6 +4730,9 @@ export function createChatWebApp(options = {}) {
4519
4730
  if (customAgentUpdateAffectsRuntime(update)) {
4520
4731
  await requireValidCustomAgentRuntime(previewCustomAgentUpdate(existing, update), context);
4521
4732
  }
4733
+ if (archived === true && !existing.archivedAt) {
4734
+ requireCustomAgentNotTargeted(state, existing, { activeOnly: true });
4735
+ }
4522
4736
  const updated = Object.keys(update).length ? state.agentStore.update(patchAgentId, update) : existing;
4523
4737
  const afterUpdate = requireSharedAgent(updated);
4524
4738
  const agent = archived === undefined ? afterUpdate : state.agentStore.setArchived(patchAgentId, archived);
@@ -4545,8 +4759,16 @@ export function createChatWebApp(options = {}) {
4545
4759
  if (confirmName !== agent.profileName) {
4546
4760
  throw new PiboWebHttpError(`Type "${agent.profileName}" to permanently delete this agent and its sessions.`, 400);
4547
4761
  }
4762
+ requireCustomAgentNotTargeted(state, agent);
4548
4763
  const deletedSessionIds = await deleteSessionsForAgentProfile(state, context, webSession, [agent.profileName, ...agent.profileAliases]);
4549
- state.agentStore.delete(agent.id);
4764
+ try {
4765
+ state.agentStore.delete(agent.id);
4766
+ }
4767
+ catch (error) {
4768
+ if (error instanceof CustomAgentTargetReferenceError)
4769
+ throw new PiboWebHttpError(error.message, 409);
4770
+ throw error;
4771
+ }
4550
4772
  context.channelContext.removeProfile?.(agent.profileName);
4551
4773
  invalidateBootstrapCatalogCache(state);
4552
4774
  return responseJson({ deletedAgentId: agent.id, deletedSessionIds });
@@ -4657,7 +4879,16 @@ export function createChatWebApp(options = {}) {
4657
4879
  const update = createRoomUpdate(existingRoom, body);
4658
4880
  if (update.parentRoomId)
4659
4881
  requireRoom(state, update.parentRoomId, webSession, "admin");
4660
- const room = state.roomService.updateRoom(roomResource.roomId, update);
4882
+ let room;
4883
+ try {
4884
+ room = state.roomService.updateRoom(roomResource.roomId, update);
4885
+ }
4886
+ catch (error) {
4887
+ if (error instanceof PiboRoomHierarchyCycleError) {
4888
+ throw new PiboWebHttpError(error.message, 400);
4889
+ }
4890
+ throw error;
4891
+ }
4661
4892
  if (!room)
4662
4893
  throw new PiboWebHttpError("Room not found", 404);
4663
4894
  return responseJson({ room });