@vellumai/assistant 0.11.1 → 0.11.2-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +1 -3
- package/README.md +1 -1
- package/eslint.config.mjs +28 -8
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
- package/openapi.yaml +292 -5
- package/package.json +1 -1
- package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
- package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
- package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
- package/src/__tests__/compaction-events.test.ts +139 -2
- package/src/__tests__/config-loader-backfill.test.ts +35 -3
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
- package/src/__tests__/conversation-agent-loop.test.ts +180 -7
- package/src/__tests__/conversation-error.test.ts +211 -4
- package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
- package/src/__tests__/conversation-queue.test.ts +126 -0
- package/src/__tests__/conversation-retry-route.test.ts +46 -7
- package/src/__tests__/conversation-slash.test.ts +4 -2
- package/src/__tests__/conversation-summarize-route.test.ts +7 -1
- package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
- package/src/__tests__/document-create-dedupe.test.ts +2 -1
- package/src/__tests__/document-find-replace.test.ts +2 -1
- package/src/__tests__/document-tool-security.test.ts +2 -1
- package/src/__tests__/document-update-default-surface.test.ts +2 -1
- package/src/__tests__/document-workspace-file.test.ts +467 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
- package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
- package/src/__tests__/http-user-message-parity.test.ts +66 -0
- package/src/__tests__/list-messages-attachments.test.ts +87 -0
- package/src/__tests__/list-messages-provider-error.test.ts +143 -0
- package/src/__tests__/list-messages-system-card.test.ts +100 -0
- package/src/__tests__/llm-resolver.test.ts +19 -9
- package/src/__tests__/managed-profile-guard.test.ts +23 -0
- package/src/__tests__/notification-platform-adapter.test.ts +130 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
- package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/provider-error-scenarios.test.ts +140 -0
- package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
- package/src/__tests__/skills.test.ts +27 -0
- package/src/__tests__/slack-channels-routes.test.ts +0 -2
- package/src/__tests__/slack-share-routes.test.ts +0 -3
- package/src/__tests__/slack-users-routes.test.ts +0 -2
- package/src/__tests__/subagent-tools.test.ts +11 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
- package/src/__tests__/tool-result-spool.test.ts +5 -4
- package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
- package/src/__tests__/turn-events-store.test.ts +26 -0
- package/src/__tests__/ui-visual-surface.test.ts +695 -0
- package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
- package/src/__tests__/visible-app-context.test.ts +189 -0
- package/src/__tests__/workspace-git-service.test.ts +173 -33
- package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
- package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
- package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
- package/src/agent/loop.ts +36 -7
- package/src/api/events/context-window-usage.ts +31 -0
- package/src/api/events/notification-intent.ts +8 -0
- package/src/api/events/ui-surface-pending.ts +35 -0
- package/src/api/index.ts +14 -0
- package/src/api/responses/conversation-message.ts +25 -4
- package/src/api/surfaces.ts +90 -1
- package/src/approvals/guardian-card-withdrawal.ts +66 -31
- package/src/approvals/guardian-decision-primitive.ts +4 -0
- package/src/calls/__tests__/call-setup-router.test.ts +156 -26
- package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
- package/src/calls/call-setup-router.ts +80 -36
- package/src/calls/voice-session-bridge.ts +173 -26
- package/src/cli/commands/inference.help.ts +3 -3
- package/src/cli/commands/notifications.help.ts +3 -2
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
- package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
- package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
- package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
- package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
- package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
- package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
- package/src/cli/commands/platform/index.help.ts +92 -0
- package/src/cli/commands/platform/index.ts +7 -0
- package/src/cli/commands/platform/invoices.ts +132 -0
- package/src/cli/commands/usage.help.ts +1 -1
- package/src/cli/lib/list-installed-plugins.ts +2 -1
- package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
- package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
- package/src/config/assistant-feature-flags.ts +7 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
- package/src/config/bundled-skills/subagent/SKILL.md +3 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
- package/src/config/bundled-skills/visualize/SKILL.md +163 -0
- package/src/config/call-site-defaults.ts +3 -2
- package/src/config/default-profile-catalog.ts +75 -70
- package/src/config/default-profile-names.ts +13 -28
- package/src/config/env-registry.ts +1 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/llm-resolver.ts +21 -5
- package/src/config/loader.ts +18 -11
- package/src/config/schemas/llm.ts +1 -9
- package/src/config/schemas/memory-retrospective.ts +9 -0
- package/src/config/schemas/monitoring.ts +28 -2
- package/src/config/schemas/workspace-git.ts +15 -0
- package/src/config/seed-inference-profiles.ts +19 -5
- package/src/context/post-turn-tool-result-truncation.ts +2 -2
- package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
- package/src/conversations/message-consolidation.ts +17 -15
- package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
- package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
- package/src/daemon/conversation-agent-loop.ts +107 -22
- package/src/daemon/conversation-error.ts +169 -48
- package/src/daemon/conversation-messaging.ts +87 -4
- package/src/daemon/conversation-process.ts +57 -45
- package/src/daemon/conversation-runtime-assembly.ts +57 -0
- package/src/daemon/conversation-store.ts +36 -1
- package/src/daemon/conversation-surfaces.ts +68 -6
- package/src/daemon/conversation-turn-finalize.ts +76 -18
- package/src/daemon/conversation.ts +122 -27
- package/src/daemon/lifecycle.ts +9 -0
- package/src/daemon/message-types/conversations.ts +8 -0
- package/src/daemon/message-types/surfaces.ts +3 -0
- package/src/documents/document-store.ts +247 -10
- package/src/home/__tests__/feed-types.test.ts +43 -8
- package/src/home/__tests__/feed-writer.test.ts +59 -0
- package/src/home/feed-types.ts +34 -7
- package/src/home/feed-writer.ts +15 -6
- package/src/live-voice/__tests__/activity-label.test.ts +95 -0
- package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
- package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
- package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
- package/src/live-voice/activity-label.ts +169 -0
- package/src/live-voice/live-activity-reporter.ts +35 -6
- package/src/live-voice/live-voice-session.ts +282 -41
- package/src/live-voice/protocol.ts +35 -0
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
- package/src/messaging/providers/slack/adapter.ts +86 -19
- package/src/messaging/providers/slack/api.test.ts +85 -1
- package/src/messaging/providers/slack/api.ts +121 -288
- package/src/messaging/providers/slack/client.ts +20 -251
- package/src/messaging/providers/slack/send.test.ts +4 -9
- package/src/messaging/providers/slack/send.ts +1 -1
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
- package/src/messaging/providers/slack/web-api-transport.ts +367 -0
- package/src/messaging/providers/slack/withdraw.ts +6 -14
- package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
- package/src/messaging/providers/telegram-bot/send.ts +23 -2
- package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
- package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
- package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
- package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
- package/src/monitoring/file-descriptors.ts +262 -0
- package/src/monitoring/process-memory.ts +4 -11
- package/src/monitoring/resource-sampler.ts +5 -0
- package/src/monitoring/worker.ts +12 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
- package/src/notifications/__tests__/broadcaster.test.ts +347 -7
- package/src/notifications/__tests__/copy-composer.test.ts +53 -3
- package/src/notifications/__tests__/decision-engine.test.ts +349 -29
- package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
- package/src/notifications/__tests__/edit-notification.test.ts +330 -0
- package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
- package/src/notifications/adapters/macos.ts +3 -0
- package/src/notifications/adapters/platform.ts +90 -14
- package/src/notifications/adapters/telegram.ts +6 -4
- package/src/notifications/assistant-reply-producer.ts +219 -0
- package/src/notifications/broadcaster.ts +523 -317
- package/src/notifications/copy-composer.ts +27 -6
- package/src/notifications/decision-engine.ts +154 -77
- package/src/notifications/deterministic-checks.ts +6 -7
- package/src/notifications/edit-notification.ts +8 -4
- package/src/notifications/emit-signal.ts +49 -16
- package/src/notifications/guardian-delivery-recorder.ts +8 -2
- package/src/notifications/home-feed-side-effect.ts +43 -6
- package/src/notifications/notification-utils.ts +39 -4
- package/src/notifications/signal.ts +10 -0
- package/src/notifications/types.ts +17 -0
- package/src/persistence/bookmark-crud.ts +18 -9
- package/src/persistence/conversation-attention-store.ts +26 -0
- package/src/persistence/conversation-crud.ts +79 -32
- package/src/persistence/conversation-queries.ts +30 -15
- package/src/persistence/conversation-title-service.ts +5 -170
- package/src/persistence/conversation-types.ts +192 -1
- package/src/persistence/db-init.ts +14 -1
- package/src/persistence/db-maintenance.ts +5 -4
- package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
- package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
- package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
- package/src/persistence/embeddings/embedding-local.ts +2 -1
- package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
- package/src/persistence/embeddings/plugin-index.ts +84 -12
- package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
- package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
- package/src/persistence/planner-statistics.ts +14 -0
- package/src/persistence/schema/documents.ts +26 -11
- package/src/persistence/steps.ts +2 -0
- package/src/platform/client.test.ts +172 -2
- package/src/platform/client.ts +178 -55
- package/src/plugin-api/conversation-turn.ts +13 -0
- package/src/plugin-api/model-profiles.test.ts +6 -2
- package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
- package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
- package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
- package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
- package/src/plugins/defaults/memory/host-utils.ts +5 -0
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
- package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
- package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
- package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
- package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
- package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
- package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
- package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
- package/src/plugins/defaults/memory/v2/injection.ts +48 -3
- package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
- package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
- package/src/plugins/defaults/turn-context/injectors.ts +1 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
- package/src/plugins/types.ts +17 -0
- package/src/prompts/templates/system-sections.ts +2 -2
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
- package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
- package/src/providers/call-site-routing.ts +77 -14
- package/src/providers/connection-resolution.ts +42 -2
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
- package/src/providers/inference/adapter-factory.ts +70 -12
- package/src/providers/model-catalog.ts +0 -11
- package/src/providers/model-intents.ts +11 -0
- package/src/providers/registry.ts +8 -0
- package/src/providers/retry.ts +85 -17
- package/src/providers/types.ts +8 -1
- package/src/runtime/__tests__/agent-wake.test.ts +181 -0
- package/src/runtime/agent-wake.ts +132 -9
- package/src/runtime/channel-approval-types.ts +25 -0
- package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
- package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
- package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
- package/src/runtime/routes/consolidation-routes.ts +1 -1
- package/src/runtime/routes/conversation-management-routes.ts +18 -6
- package/src/runtime/routes/conversation-query-routes.ts +14 -23
- package/src/runtime/routes/conversation-routes.ts +113 -8
- package/src/runtime/routes/credential-routes.ts +1 -5
- package/src/runtime/routes/documents-routes.ts +207 -12
- package/src/runtime/routes/identity-routes.ts +3 -93
- package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
- package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
- package/src/runtime/routes/notification-routes.ts +4 -1
- package/src/runtime/routes/platform-routes.ts +238 -3
- package/src/runtime/routes/playground/guard.ts +1 -2
- package/src/runtime/routes/slack-channel-routes.ts +2 -4
- package/src/runtime/routes/workspace-commit-routes.ts +49 -2
- package/src/runtime/routes/workspace-routes.ts +11 -12
- package/src/runtime/routes/workspace-utils.ts +49 -2
- package/src/runtime/services/conversation-serializer.ts +2 -4
- package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
- package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
- package/src/subagent/__tests__/consult-context.test.ts +84 -0
- package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
- package/src/subagent/consult-context.ts +410 -0
- package/src/subagent/consult-prompt.ts +38 -5
- package/src/subagent/manager.ts +9 -4
- package/src/subagent/types.ts +8 -0
- package/src/telemetry/telemetry-event-sources.ts +7 -0
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire.generated.ts +1 -0
- package/src/telemetry/turn-events-store.ts +30 -1
- package/src/telemetry/types.ts +24 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
- package/src/tools/browser/pinned-tabs.ts +3 -1
- package/src/tools/subagent/spawn.ts +23 -1
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/tools/ui-surface/definitions.ts +39 -1
- package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
- package/src/tools/ui-surface/visual-validation.ts +787 -0
- package/src/tools/workflows/run-workflow.test.ts +1 -0
- package/src/util/__tests__/short-title.test.ts +229 -0
- package/src/util/__tests__/worker-compute.test.ts +65 -0
- package/src/util/cgroup-cpu.ts +93 -0
- package/src/util/errors.ts +27 -0
- package/src/util/process-tree.ts +19 -0
- package/src/util/short-title.ts +189 -0
- package/src/util/worker-compute.ts +83 -0
- package/src/workspace/byok-default-profile-ensure.ts +25 -15
- package/src/workspace/git-service.ts +79 -30
- package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
- package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
- package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
|
@@ -29,11 +29,7 @@ import {
|
|
|
29
29
|
discardLastAssistantDisplayTurn,
|
|
30
30
|
extractUserPromptText,
|
|
31
31
|
} from "../../daemon/conversation-history.js";
|
|
32
|
-
import {
|
|
33
|
-
formatSummarizeUpToResult,
|
|
34
|
-
isBackgroundEventMetadata,
|
|
35
|
-
isEchoSuppressedUserMessage,
|
|
36
|
-
} from "../../daemon/conversation-process.js";
|
|
32
|
+
import { formatSummarizeUpToResult } from "../../daemon/conversation-process.js";
|
|
37
33
|
import { findConversation } from "../../daemon/conversation-registry.js";
|
|
38
34
|
import {
|
|
39
35
|
destroyActiveConversation,
|
|
@@ -55,6 +51,8 @@ import {
|
|
|
55
51
|
deleteConversation,
|
|
56
52
|
forkConversation as forkConversationInStore,
|
|
57
53
|
getConversation,
|
|
54
|
+
isBackgroundEventMetadata,
|
|
55
|
+
isEchoSuppressedUserMessage,
|
|
58
56
|
setConversationEnabledPlugins,
|
|
59
57
|
setConversationSurfaced,
|
|
60
58
|
unarchiveConversation,
|
|
@@ -280,7 +278,14 @@ async function handleSummarizeConversation({ body = {} }: RouteHandlerArgs) {
|
|
|
280
278
|
conversation.emitActivityState("thinking", "context_compacting", {
|
|
281
279
|
statusText: "Summarizing conversation",
|
|
282
280
|
});
|
|
283
|
-
|
|
281
|
+
// The context-window usage push goes out on the same broadcast path as
|
|
282
|
+
// the result card below: this route resolves its conversation outside
|
|
283
|
+
// the send path, so the instance may still hold the store's no-op
|
|
284
|
+
// sender and a `sendToClient` emit would reach nobody.
|
|
285
|
+
const result = await conversation.summarizeUpToMessage(
|
|
286
|
+
beforeMessageId,
|
|
287
|
+
broadcastMessage,
|
|
288
|
+
);
|
|
284
289
|
// Stop aborted the in-flight summary: the compactor reports the aborted
|
|
285
290
|
// provider call as a non-compacted result (it swallows the abort rather
|
|
286
291
|
// than throwing), so detect cancellation via the signal. A cancelled
|
|
@@ -745,6 +750,13 @@ async function handleRetryLastAssistantTurn({
|
|
|
745
750
|
onEvent: broadcastMessage,
|
|
746
751
|
isUserMessage: true,
|
|
747
752
|
isInteractive,
|
|
753
|
+
// The re-run carries none of the anchor's original delivery
|
|
754
|
+
// orchestration: a channel anchor's reply is not posted back to
|
|
755
|
+
// Slack/Telegram (that is owned by the inbound event's
|
|
756
|
+
// `finalizeEventDelivery`) and a voice anchor's is not spoken over a
|
|
757
|
+
// session. The regenerated reply reaches SSE subscribers only, so the
|
|
758
|
+
// push is the user's only copy once they leave.
|
|
759
|
+
replyDeliveredInAppOnly: true,
|
|
748
760
|
...(isHiddenPrompt ? { isHiddenPrompt: true } : {}),
|
|
749
761
|
});
|
|
750
762
|
} catch (err) {
|
|
@@ -87,12 +87,15 @@ import {
|
|
|
87
87
|
getMessageById,
|
|
88
88
|
} from "../../persistence/conversation-crud.js";
|
|
89
89
|
import { getConversationByKey } from "../../persistence/conversation-key-store.js";
|
|
90
|
+
import {
|
|
91
|
+
type ConversationKind,
|
|
92
|
+
resolveConversationKind,
|
|
93
|
+
} from "../../persistence/conversation-types.js";
|
|
90
94
|
import { getDb } from "../../persistence/db-connection.js";
|
|
91
95
|
import { clearEmbeddingBackendCache } from "../../persistence/embeddings/embedding-backend.js";
|
|
92
96
|
import { getLlmRequestLogSource } from "../../persistence/llm-request-log-source.js";
|
|
93
97
|
import { type LogRow } from "../../persistence/llm-request-log-store.js";
|
|
94
98
|
import { getMemoryRecallLogByMessageIds } from "../../plugins/defaults/memory/memory-recall-log-store.js";
|
|
95
|
-
import { MEMORY_V2_CONSOLIDATION_SOURCE } from "../../plugins/defaults/memory/substrate/constants.js";
|
|
96
99
|
import { getMemoryV2ActivationLogByMessageIds } from "../../plugins/defaults/memory/v2/activation-log-store.js";
|
|
97
100
|
import { getMemoryV3SelectionForInspectorByMessageIds } from "../../plugins/defaults/memory/v3/selection-log-store.js";
|
|
98
101
|
import { ROUTING_IDENTITY_PROVIDERS } from "../../providers/inference/auth.js";
|
|
@@ -146,6 +149,7 @@ type LlmContextRouteResult = Omit<LlmContextNormalizationResult, "summary"> & {
|
|
|
146
149
|
};
|
|
147
150
|
|
|
148
151
|
import {
|
|
152
|
+
CODE_OWNED_PROFILE_NAMES,
|
|
149
153
|
getEffectiveProfilesForProvider,
|
|
150
154
|
INVARIANT_PROFILE_NAMES,
|
|
151
155
|
MANAGED_PROFILE_NAMES,
|
|
@@ -1743,6 +1747,15 @@ async function handleReplaceInferenceProfile({
|
|
|
1743
1747
|
`Profile "${name}" is not currently available and cannot be edited.`,
|
|
1744
1748
|
);
|
|
1745
1749
|
}
|
|
1750
|
+
if (CODE_OWNED_PROFILE_NAMES.has(name)) {
|
|
1751
|
+
// A code-owned profile resolves from the catalog whatever the workspace
|
|
1752
|
+
// holds, so even a status re-enable would persist a stub that never
|
|
1753
|
+
// governs anything. Reject the write rather than accept a silent no-op.
|
|
1754
|
+
throw new BadRequestError(
|
|
1755
|
+
`Profile "${name}" is code-owned and cannot be edited. ` +
|
|
1756
|
+
`Duplicate it to a custom profile to customize.`,
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1746
1759
|
if (isManaged) {
|
|
1747
1760
|
// Managed profiles are daemon-seeded and read-only — the commit guard
|
|
1748
1761
|
// (`assertInvariantProfilesPreserved`) rejects every write to them
|
|
@@ -1966,28 +1979,6 @@ function handleGetMessageContent({
|
|
|
1966
1979
|
return result;
|
|
1967
1980
|
}
|
|
1968
1981
|
|
|
1969
|
-
type ConversationKind =
|
|
1970
|
-
| "user"
|
|
1971
|
-
| "background"
|
|
1972
|
-
| "background_memory_consolidation"
|
|
1973
|
-
| "scheduled";
|
|
1974
|
-
|
|
1975
|
-
function resolveConversationKind(
|
|
1976
|
-
source: string,
|
|
1977
|
-
conversationType: string,
|
|
1978
|
-
): ConversationKind {
|
|
1979
|
-
if (source === MEMORY_V2_CONSOLIDATION_SOURCE) {
|
|
1980
|
-
return "background_memory_consolidation";
|
|
1981
|
-
}
|
|
1982
|
-
if (conversationType === "background") {
|
|
1983
|
-
return "background";
|
|
1984
|
-
}
|
|
1985
|
-
if (conversationType === "scheduled") {
|
|
1986
|
-
return "scheduled";
|
|
1987
|
-
}
|
|
1988
|
-
return "user";
|
|
1989
|
-
}
|
|
1990
|
-
|
|
1991
1982
|
async function handleGetLlmContext({
|
|
1992
1983
|
pathParams = {},
|
|
1993
1984
|
queryParams = {},
|
|
@@ -53,7 +53,6 @@ import {
|
|
|
53
53
|
buildModelInfoEvent,
|
|
54
54
|
formatCleanResult,
|
|
55
55
|
formatCompactResult,
|
|
56
|
-
isBackgroundEventMetadata,
|
|
57
56
|
isModelSlashCommand,
|
|
58
57
|
} from "../../daemon/conversation-process.js";
|
|
59
58
|
import { findConversation } from "../../daemon/conversation-registry.js";
|
|
@@ -113,8 +112,10 @@ import {
|
|
|
113
112
|
getMessages,
|
|
114
113
|
getMessagesPaginated,
|
|
115
114
|
hasMessages,
|
|
115
|
+
isBackgroundEventMetadata,
|
|
116
116
|
isConversationProcessing,
|
|
117
117
|
isHiddenMessageMetadata,
|
|
118
|
+
isProviderErrorMetadata,
|
|
118
119
|
isSystemCardMetadata,
|
|
119
120
|
type MessageRow,
|
|
120
121
|
recordConversationPersistedSeq,
|
|
@@ -212,6 +213,21 @@ interface AlignedAttachments {
|
|
|
212
213
|
rewriteContentOrder: ContentOrderRewrite;
|
|
213
214
|
}
|
|
214
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Metadata-only projection of an attachment for inline `contentBlocks`
|
|
218
|
+
* placement. The flat `attachments` array is the payload carrier: it keeps
|
|
219
|
+
* `data`/`thumbnailData`, and `/v1/assistants/:id/attachments/:id/content`
|
|
220
|
+
* serves stored bytes on demand. Attachment blocks are positional references
|
|
221
|
+
* the renderer resolves against that array by id, so inlining the base64 here
|
|
222
|
+
* would ship every image twice in the same response.
|
|
223
|
+
*/
|
|
224
|
+
function toAttachmentBlockRef(
|
|
225
|
+
a: RuntimeAttachmentMetadata,
|
|
226
|
+
): RuntimeAttachmentMetadata {
|
|
227
|
+
const { data: _data, thumbnailData: _thumbnailData, ...meta } = a;
|
|
228
|
+
return meta;
|
|
229
|
+
}
|
|
230
|
+
|
|
215
231
|
/**
|
|
216
232
|
* Align DB-hydrated attachment rows with the file-block refs `renderHistoryContent`
|
|
217
233
|
* captured. When a file block carries an attachment id (user-message uploads —
|
|
@@ -339,6 +355,33 @@ function isValidRiskThreshold(value: unknown): value is RiskThreshold {
|
|
|
339
355
|
);
|
|
340
356
|
}
|
|
341
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Upper bound on the reported visible-app id. Sized so it can never clip an id
|
|
360
|
+
* the viewer can actually open: a plugin app id is `plugins~<plugin>~<app>`,
|
|
361
|
+
* and each of those two segments is a filesystem directory name bounded at 255
|
|
362
|
+
* bytes, so the longest openable id runs to ~519 characters. The cap exists
|
|
363
|
+
* only to bound what an arbitrary client can park on the conversation, not to
|
|
364
|
+
* validate the id — `resolveAppSource` decides what resolves.
|
|
365
|
+
*/
|
|
366
|
+
const VISIBLE_APP_ID_MAX_LENGTH = 640;
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* True when the client-reported visible-app id is safe to carry as view state:
|
|
370
|
+
* non-empty, trimmed, bounded, and free of path separators or traversal.
|
|
371
|
+
* Mirrors the app store's own id validation so a malformed id is dropped at
|
|
372
|
+
* ingress instead of reaching a filesystem lookup.
|
|
373
|
+
*/
|
|
374
|
+
function isSafeVisibleAppId(value: string): boolean {
|
|
375
|
+
return (
|
|
376
|
+
value.length > 0 &&
|
|
377
|
+
value.length <= VISIBLE_APP_ID_MAX_LENGTH &&
|
|
378
|
+
value === value.trim() &&
|
|
379
|
+
!value.includes("/") &&
|
|
380
|
+
!value.includes("\\") &&
|
|
381
|
+
!value.includes("..")
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
342
385
|
/**
|
|
343
386
|
* True when a message's persisted metadata explicitly flags it as hidden.
|
|
344
387
|
* Used to suppress internal scaffolding messages from UI history while
|
|
@@ -890,6 +933,7 @@ export function handleListMessages({
|
|
|
890
933
|
let backgroundEventNotification: boolean | undefined;
|
|
891
934
|
let backgroundToolCompletion: ConversationMessage["backgroundToolCompletion"];
|
|
892
935
|
let systemCard: boolean | undefined;
|
|
936
|
+
let providerError: ConversationMessage["providerError"];
|
|
893
937
|
if (msg.metadata) {
|
|
894
938
|
try {
|
|
895
939
|
const meta = JSON.parse(msg.metadata);
|
|
@@ -901,6 +945,19 @@ export function handleListMessages({
|
|
|
901
945
|
if (isSystemCardMetadata(meta)) {
|
|
902
946
|
systemCard = true;
|
|
903
947
|
}
|
|
948
|
+
// Daemon-persisted provider-failure notices carry the classified
|
|
949
|
+
// error code/category so clients can render a themed card instead
|
|
950
|
+
// of a persona bubble.
|
|
951
|
+
if (isProviderErrorMetadata(meta)) {
|
|
952
|
+
providerError = {
|
|
953
|
+
...(typeof meta.providerErrorCode === "string"
|
|
954
|
+
? { code: meta.providerErrorCode }
|
|
955
|
+
: {}),
|
|
956
|
+
...(typeof meta.providerErrorCategory === "string"
|
|
957
|
+
? { category: meta.providerErrorCategory }
|
|
958
|
+
: {}),
|
|
959
|
+
};
|
|
960
|
+
}
|
|
904
961
|
// Every wake persists a `<background_event source="...">` trigger row
|
|
905
962
|
// (see `persistWakeTriggerMessage`) that the LLM reads. Flag any such
|
|
906
963
|
// row so clients hide it from the transcript like a subagent/ACP
|
|
@@ -973,6 +1030,7 @@ export function handleListMessages({
|
|
|
973
1030
|
backgroundEventNotification,
|
|
974
1031
|
backgroundToolCompletion,
|
|
975
1032
|
systemCard,
|
|
1033
|
+
providerError,
|
|
976
1034
|
slackMessage,
|
|
977
1035
|
clientMessageId: msg.clientMessageId ?? undefined,
|
|
978
1036
|
};
|
|
@@ -1050,9 +1108,10 @@ export function handleListMessages({
|
|
|
1050
1108
|
const attachmentRefs = collectAttachmentRefs(m.content);
|
|
1051
1109
|
const aligned = alignAttachments(attachmentRefs, msgAttachments);
|
|
1052
1110
|
msgAttachments = aligned.attachments;
|
|
1053
|
-
const attachmentBlocks = attachmentRefs.map(
|
|
1054
|
-
|
|
1055
|
-
|
|
1111
|
+
const attachmentBlocks = attachmentRefs.map((_ref, refIdx) => {
|
|
1112
|
+
const att = aligned.refIndexToAttachment.get(refIdx);
|
|
1113
|
+
return att ? toAttachmentBlockRef(att) : null;
|
|
1114
|
+
});
|
|
1056
1115
|
const rendered = renderHistoryContent(
|
|
1057
1116
|
m.content,
|
|
1058
1117
|
attachmentBlocks,
|
|
@@ -1129,7 +1188,10 @@ export function handleListMessages({
|
|
|
1129
1188
|
);
|
|
1130
1189
|
for (const att of msgAttachments) {
|
|
1131
1190
|
if (!existingAttachmentIds.has(att.id)) {
|
|
1132
|
-
contentBlocks.push({
|
|
1191
|
+
contentBlocks.push({
|
|
1192
|
+
type: "attachment",
|
|
1193
|
+
attachment: toAttachmentBlockRef(att),
|
|
1194
|
+
});
|
|
1133
1195
|
}
|
|
1134
1196
|
}
|
|
1135
1197
|
|
|
@@ -1169,6 +1231,8 @@ export function handleListMessages({
|
|
|
1169
1231
|
...(m.backgroundToolCompletion
|
|
1170
1232
|
? { backgroundToolCompletion: m.backgroundToolCompletion }
|
|
1171
1233
|
: {}),
|
|
1234
|
+
...(m.systemCard ? { systemCard: true } : {}),
|
|
1235
|
+
...(m.providerError ? { providerError: m.providerError } : {}),
|
|
1172
1236
|
...(m.slackMessage ? { slackMessage: m.slackMessage } : {}),
|
|
1173
1237
|
};
|
|
1174
1238
|
});
|
|
@@ -1387,11 +1451,16 @@ export async function handleSendMessage(
|
|
|
1387
1451
|
// handoff to prime a proactive assistant greeting without showing the
|
|
1388
1452
|
// triggering user message. Honored on the standard send path only.
|
|
1389
1453
|
hidden?: boolean;
|
|
1454
|
+
// True when the turn was auto-sent on the user's behalf rather than typed.
|
|
1455
|
+
// Independent of `hidden`: the research prompt is visible AND scripted,
|
|
1456
|
+
// the kickoff greeting is hidden AND scripted. Absent means UNKNOWN.
|
|
1457
|
+
scripted?: boolean;
|
|
1390
1458
|
bypassSecretCheck?: boolean;
|
|
1391
1459
|
hostHomeDir?: string;
|
|
1392
1460
|
hostUsername?: string;
|
|
1393
1461
|
clientTimezone?: unknown;
|
|
1394
1462
|
clientOs?: unknown;
|
|
1463
|
+
visibleAppId?: unknown;
|
|
1395
1464
|
clientId?: string;
|
|
1396
1465
|
clientMessageId?: string;
|
|
1397
1466
|
inferenceProfile?: string | null;
|
|
@@ -1519,6 +1588,16 @@ export async function handleSendMessage(
|
|
|
1519
1588
|
typeof body.clientOs === "string"
|
|
1520
1589
|
? (parseClientOs(body.clientOs) ?? undefined)
|
|
1521
1590
|
: undefined;
|
|
1591
|
+
// App the client has open on screen. Purely view state: it drives the
|
|
1592
|
+
// per-turn `visible_app:` context line and nothing else, so an id that no
|
|
1593
|
+
// longer resolves (deleted app) is dropped silently during assembly rather
|
|
1594
|
+
// than failing the send. Traversal-shaped ids are rejected here so nothing
|
|
1595
|
+
// downstream has to treat the value as a path segment.
|
|
1596
|
+
const visibleAppId =
|
|
1597
|
+
typeof body.visibleAppId === "string" &&
|
|
1598
|
+
isSafeVisibleAppId(body.visibleAppId)
|
|
1599
|
+
? body.visibleAppId
|
|
1600
|
+
: undefined;
|
|
1522
1601
|
|
|
1523
1602
|
// Reject non-string content values (numbers, objects, etc.)
|
|
1524
1603
|
if (content != null && typeof content !== "string") {
|
|
@@ -1679,12 +1758,14 @@ export async function handleSendMessage(
|
|
|
1679
1758
|
hostUsername: body.hostUsername,
|
|
1680
1759
|
...(clientTimezone ? { clientTimezone } : {}),
|
|
1681
1760
|
...(clientOs ? { clientOs } : {}),
|
|
1761
|
+
...(visibleAppId ? { visibleAppId } : {}),
|
|
1682
1762
|
} satisfies HostProxyTransportMetadata)
|
|
1683
1763
|
: ({
|
|
1684
1764
|
channelId: sourceChannel,
|
|
1685
1765
|
interfaceId: sourceInterface,
|
|
1686
1766
|
...(clientTimezone ? { clientTimezone } : {}),
|
|
1687
1767
|
...(clientOs ? { clientOs } : {}),
|
|
1768
|
+
...(visibleAppId ? { visibleAppId } : {}),
|
|
1688
1769
|
} satisfies NonHostProxyTransportMetadata);
|
|
1689
1770
|
|
|
1690
1771
|
const conversation = await smDeps.getOrCreateConversation(
|
|
@@ -1878,8 +1959,7 @@ export async function handleSendMessage(
|
|
|
1878
1959
|
// real first response. Gated behind the `self-intro-greeting` flag (default
|
|
1879
1960
|
// off); `undefined` (flag off or no names) falls back to the canned path.
|
|
1880
1961
|
const selfIntroGreetingEnabled =
|
|
1881
|
-
isWakeUp &&
|
|
1882
|
-
isAssistantFeatureFlagEnabled(SELF_INTRO_GREETING_FLAG, getConfig());
|
|
1962
|
+
isWakeUp && isAssistantFeatureFlagEnabled(SELF_INTRO_GREETING_FLAG);
|
|
1883
1963
|
const selfIntro = selfIntroGreetingEnabled
|
|
1884
1964
|
? buildSelfIntroMessage(body.onboarding ?? undefined)
|
|
1885
1965
|
: undefined;
|
|
@@ -2090,6 +2170,13 @@ export async function handleSendMessage(
|
|
|
2090
2170
|
// hidden send that lands mid-turn stays hidden when drained —
|
|
2091
2171
|
// the drain path persists this metadata and skips the echo.
|
|
2092
2172
|
...(body.hidden === true ? { hidden: true } : {}),
|
|
2173
|
+
// Same reason: the queue round-trips metadata, not persist options,
|
|
2174
|
+
// so a scripted send that lands mid-turn can only keep its marker
|
|
2175
|
+
// this way. Both booleans forwarded, since false is a real assertion
|
|
2176
|
+
// ("the user typed this"), not an absence.
|
|
2177
|
+
...(typeof body.scripted === "boolean"
|
|
2178
|
+
? { scripted: body.scripted }
|
|
2179
|
+
: {}),
|
|
2093
2180
|
},
|
|
2094
2181
|
clientMetadata,
|
|
2095
2182
|
),
|
|
@@ -2232,6 +2319,9 @@ export async function handleSendMessage(
|
|
|
2232
2319
|
userMessageInterface: sourceInterface,
|
|
2233
2320
|
assistantMessageInterface: sourceInterface,
|
|
2234
2321
|
...(body.automated === true ? { automated: true } : {}),
|
|
2322
|
+
...(typeof body.scripted === "boolean"
|
|
2323
|
+
? { scripted: body.scripted }
|
|
2324
|
+
: {}),
|
|
2235
2325
|
};
|
|
2236
2326
|
const persisted = await persistQueuedMessageBody(conversation, {
|
|
2237
2327
|
content: rawContent,
|
|
@@ -2376,7 +2466,9 @@ export async function handleSendMessage(
|
|
|
2376
2466
|
});
|
|
2377
2467
|
publishConversationMessagesChanged(conversationId, originClientId);
|
|
2378
2468
|
conversation.emitActivityState("thinking", "context_compacting");
|
|
2379
|
-
|
|
2469
|
+
// Same sink the result card below goes out on, so the indicator and
|
|
2470
|
+
// the card can never be delivered to different places.
|
|
2471
|
+
const result = await conversation.forceCompact(broadcastMessage);
|
|
2380
2472
|
const cardId = await persistCannedAssistantCard({
|
|
2381
2473
|
conversation,
|
|
2382
2474
|
conversationId,
|
|
@@ -2497,6 +2589,7 @@ export async function handleSendMessage(
|
|
|
2497
2589
|
: undefined,
|
|
2498
2590
|
clientMetadata,
|
|
2499
2591
|
),
|
|
2592
|
+
scripted: body.scripted,
|
|
2500
2593
|
clientMessageId,
|
|
2501
2594
|
});
|
|
2502
2595
|
|
|
@@ -3004,6 +3097,12 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
3004
3097
|
.describe(
|
|
3005
3098
|
'Client OS surface ("web" | "ios" | "macos" | "android"), reported separately from `interface`. Drives the per-turn `client_os` context only; does not affect transport/host-proxy capabilities.',
|
|
3006
3099
|
),
|
|
3100
|
+
visibleAppId: z
|
|
3101
|
+
.string()
|
|
3102
|
+
.optional()
|
|
3103
|
+
.describe(
|
|
3104
|
+
'Id of the app the client currently has open on screen (app viewer or the app-editing split). Drives the per-turn `visible_app:` context line so the assistant can resolve "the app" to what the user is looking at. View state only: it never affects transport, routing, or tool gating, and is omitted whenever no app is in view.',
|
|
3105
|
+
),
|
|
3007
3106
|
clientMessageId: z
|
|
3008
3107
|
.string()
|
|
3009
3108
|
.describe(
|
|
@@ -3031,6 +3130,12 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
3031
3130
|
.describe(
|
|
3032
3131
|
"When true, persist the user message but suppress it from the UI transcript (it stays in LLM-side history and still drives the turn). Used for machine signals the user never typed (proactive-greeting priming, channel-setup wizard close). Suppression covers the queued path too: a hidden send that lands mid-turn returns { queued: true, requestId } but never appears in list-messages queued snapshots, emits no echo, and does not supersede pending interactions. Honored on the standard send path only — slash-command content bypasses it.",
|
|
3033
3132
|
),
|
|
3133
|
+
scripted: z
|
|
3134
|
+
.boolean()
|
|
3135
|
+
.optional()
|
|
3136
|
+
.describe(
|
|
3137
|
+
"When true, this turn was auto-sent on the user's behalf rather than typed by them: onboarding research prompts, the personality rewrite message, research corrections, hidden kickoff greetings, the legacy pre-chat bootstrap. Stamped onto the persisted message and forwarded to turn telemetry, where activation metrics exclude it. Send false for a genuine typed message; OMIT the field only if the client genuinely cannot tell, since absent means UNKNOWN and a wrong false is trusted downstream. Independent of `hidden`: a turn can be visible and scripted (the research prompt) or hidden and scripted (the kickoff greeting).",
|
|
3138
|
+
),
|
|
3034
3139
|
onboarding: z
|
|
3035
3140
|
.object({
|
|
3036
3141
|
tools: z.array(z.string()),
|
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
assertAcpCredentialFormat,
|
|
23
23
|
} from "../../acp/acp-credentials.js";
|
|
24
24
|
import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
|
|
25
|
-
import { getConfig } from "../../config/loader.js";
|
|
26
25
|
import {
|
|
27
26
|
fetchManagedCatalog,
|
|
28
27
|
type ManagedCredentialDescriptor,
|
|
@@ -319,10 +318,7 @@ async function handleCredentialsReveal({ body, headers }: RouteHandlerArgs) {
|
|
|
319
318
|
}
|
|
320
319
|
|
|
321
320
|
const forChat = (body as { forChat?: unknown }).forChat === true;
|
|
322
|
-
if (
|
|
323
|
-
forChat &&
|
|
324
|
-
!isAssistantFeatureFlagEnabled("chat-credential-reveal", getConfig())
|
|
325
|
-
) {
|
|
321
|
+
if (forChat && !isAssistantFeatureFlagEnabled("chat-credential-reveal")) {
|
|
326
322
|
throw new BadRequestError(
|
|
327
323
|
"--for-chat requires the chat-credential-reveal feature flag",
|
|
328
324
|
);
|
|
@@ -4,21 +4,37 @@
|
|
|
4
4
|
* Exposes document CRUD over HTTP, sharing business logic with the
|
|
5
5
|
* handlers in `daemon/handlers/documents.ts`.
|
|
6
6
|
*/
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { readFileSync, statSync } from "node:fs";
|
|
9
|
+
import { basename, extname } from "node:path";
|
|
10
|
+
|
|
7
11
|
import { z } from "zod";
|
|
8
12
|
|
|
9
13
|
import {
|
|
10
14
|
addDocumentConversation,
|
|
15
|
+
createFileBackedDocument,
|
|
11
16
|
getDocumentById,
|
|
17
|
+
getDocumentByWorkspacePath,
|
|
12
18
|
getDocumentsForConversation,
|
|
19
|
+
refreshDocumentContentFromFile,
|
|
13
20
|
saveDocument,
|
|
14
21
|
} from "../../documents/document-store.js";
|
|
15
22
|
import { rawAll } from "../../persistence/raw-query.js";
|
|
16
23
|
import { getLogger } from "../../util/logger.js";
|
|
17
24
|
import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
|
|
18
25
|
import { renderMarkdownToPDF } from "./document-pdf-renderer.js";
|
|
19
|
-
import {
|
|
20
|
-
|
|
26
|
+
import {
|
|
27
|
+
BadRequestError,
|
|
28
|
+
InternalError,
|
|
29
|
+
NotFoundError,
|
|
30
|
+
UnprocessableEntityError,
|
|
31
|
+
} from "./errors.js";
|
|
32
|
+
import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
|
|
21
33
|
import { RouteResponse } from "./types.js";
|
|
34
|
+
import {
|
|
35
|
+
resolveWorkspacePath,
|
|
36
|
+
toWorkspaceRelativePath,
|
|
37
|
+
} from "./workspace-utils.js";
|
|
22
38
|
|
|
23
39
|
const log = getLogger("documents-routes");
|
|
24
40
|
|
|
@@ -64,6 +80,164 @@ function listAllDocuments(): Array<{
|
|
|
64
80
|
}
|
|
65
81
|
}
|
|
66
82
|
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// File-backed documents
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
/** Extensions the document editor round-trips faithfully as markdown. */
|
|
88
|
+
const MARKDOWN_EXTENSIONS = new Set([".md", ".markdown"]);
|
|
89
|
+
|
|
90
|
+
/** The document payload shape shared by `GET documents/{id}` and its siblings. */
|
|
91
|
+
const documentPayloadSchema = z.object({
|
|
92
|
+
success: z.boolean(),
|
|
93
|
+
surfaceId: z.string(),
|
|
94
|
+
conversationId: z.string(),
|
|
95
|
+
title: z.string(),
|
|
96
|
+
content: z.string(),
|
|
97
|
+
wordCount: z.number(),
|
|
98
|
+
createdAt: z.number(),
|
|
99
|
+
updatedAt: z.number(),
|
|
100
|
+
workspacePath: z
|
|
101
|
+
.string()
|
|
102
|
+
.nullable()
|
|
103
|
+
.describe("Workspace-relative file this document is bound to, or null"),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve a client-supplied workspace path to the markdown file it names.
|
|
108
|
+
*
|
|
109
|
+
* Uses the same resolution as the workspace file routes, so a path that
|
|
110
|
+
* escapes the workspace (traversal, absolute, symlinked out, dot-segment) is
|
|
111
|
+
* rejected here exactly as it would be there.
|
|
112
|
+
*/
|
|
113
|
+
function resolveWorkspaceMarkdownFile(path: string): {
|
|
114
|
+
absolutePath: string;
|
|
115
|
+
relativePath: string;
|
|
116
|
+
} {
|
|
117
|
+
const resolved = resolveWorkspacePath(path);
|
|
118
|
+
if (resolved === undefined) {
|
|
119
|
+
throw new BadRequestError("Invalid path");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!MARKDOWN_EXTENSIONS.has(extname(resolved).toLowerCase())) {
|
|
123
|
+
throw new UnprocessableEntityError(
|
|
124
|
+
"Only markdown files (.md, .markdown) can back a document",
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
absolutePath: resolved,
|
|
130
|
+
relativePath: toWorkspaceRelativePath(resolved),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Current text of a workspace file, or `null` when it is gone or not a file. */
|
|
135
|
+
function readWorkspaceFileText(absolutePath: string): string | null {
|
|
136
|
+
try {
|
|
137
|
+
if (!statSync(absolutePath).isFile()) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
return readFileSync(absolutePath, "utf-8");
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Load a document by surface ID for a response, or fail loudly if it vanished. */
|
|
147
|
+
function loadDocumentPayload(surfaceId: string): Record<string, unknown> {
|
|
148
|
+
const doc = getDocumentById(surfaceId);
|
|
149
|
+
if (!doc) {
|
|
150
|
+
throw new InternalError("Document could not be loaded after persisting");
|
|
151
|
+
}
|
|
152
|
+
return { success: true, ...doc };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Find or create the document bound to a workspace markdown file.
|
|
157
|
+
*
|
|
158
|
+
* The file is the source of truth at open time: when a bound document's stored
|
|
159
|
+
* content has drifted from the bytes on disk (the file was edited outside the
|
|
160
|
+
* editor), the row is refreshed from the file before it is returned. Writes go
|
|
161
|
+
* the other way — the document store writes through to the file.
|
|
162
|
+
*/
|
|
163
|
+
function handleDocumentForWorkspaceFile({
|
|
164
|
+
body,
|
|
165
|
+
}: RouteHandlerArgs): Record<string, unknown> {
|
|
166
|
+
const { path, conversationId } = (body ?? {}) as {
|
|
167
|
+
path?: string;
|
|
168
|
+
conversationId?: string;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
if (!path || typeof path !== "string") {
|
|
172
|
+
throw new BadRequestError("path is required");
|
|
173
|
+
}
|
|
174
|
+
if (!conversationId || typeof conversationId !== "string") {
|
|
175
|
+
throw new BadRequestError("conversationId is required");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const { absolutePath, relativePath } = resolveWorkspaceMarkdownFile(path);
|
|
179
|
+
const existing = getDocumentByWorkspacePath(relativePath);
|
|
180
|
+
const fileText = readWorkspaceFileText(absolutePath);
|
|
181
|
+
|
|
182
|
+
if (fileText === null) {
|
|
183
|
+
// A bound document whose file is gone is reported distinctly: the client
|
|
184
|
+
// needs to tell "this file was deleted" apart from "no such file", and the
|
|
185
|
+
// document must never be used to resurrect the file.
|
|
186
|
+
if (existing) {
|
|
187
|
+
throw new NotFoundError(
|
|
188
|
+
`The file backing this document no longer exists: ${relativePath}`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
throw new NotFoundError("File not found");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (existing) {
|
|
195
|
+
// Opening the file from another conversation grants that conversation
|
|
196
|
+
// access, the same association the link route writes.
|
|
197
|
+
addDocumentConversation(existing.surfaceId, conversationId);
|
|
198
|
+
|
|
199
|
+
if (existing.content !== fileText) {
|
|
200
|
+
const refresh = refreshDocumentContentFromFile(
|
|
201
|
+
existing.surfaceId,
|
|
202
|
+
fileText,
|
|
203
|
+
);
|
|
204
|
+
if (!refresh.success) {
|
|
205
|
+
throw new InternalError(refresh.error);
|
|
206
|
+
}
|
|
207
|
+
log.info(
|
|
208
|
+
{ surfaceId: existing.surfaceId, workspacePath: relativePath },
|
|
209
|
+
"Refreshed file-backed document from disk",
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return loadDocumentPayload(existing.surfaceId);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const surfaceId = `doc-${randomUUID()}`;
|
|
216
|
+
const created = createFileBackedDocument({
|
|
217
|
+
surfaceId,
|
|
218
|
+
conversationId,
|
|
219
|
+
title: basename(relativePath),
|
|
220
|
+
content: fileText,
|
|
221
|
+
workspacePath: relativePath,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
if (!created.success) {
|
|
225
|
+
// A concurrent request may have won the unique index on workspace_path.
|
|
226
|
+
const raced = getDocumentByWorkspacePath(relativePath);
|
|
227
|
+
if (!raced) {
|
|
228
|
+
throw new InternalError(created.error);
|
|
229
|
+
}
|
|
230
|
+
addDocumentConversation(raced.surfaceId, conversationId);
|
|
231
|
+
return loadDocumentPayload(raced.surfaceId);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
log.info(
|
|
235
|
+
{ surfaceId, workspacePath: relativePath, conversationId },
|
|
236
|
+
"Bound workspace file to a new document",
|
|
237
|
+
);
|
|
238
|
+
return loadDocumentPayload(surfaceId);
|
|
239
|
+
}
|
|
240
|
+
|
|
67
241
|
// ---------------------------------------------------------------------------
|
|
68
242
|
// Route definitions
|
|
69
243
|
// ---------------------------------------------------------------------------
|
|
@@ -119,16 +293,7 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
119
293
|
summary: "Get a document",
|
|
120
294
|
description: "Return a single document by surface ID.",
|
|
121
295
|
tags: ["documents"],
|
|
122
|
-
responseBody:
|
|
123
|
-
success: z.boolean(),
|
|
124
|
-
surfaceId: z.string(),
|
|
125
|
-
conversationId: z.string(),
|
|
126
|
-
title: z.string(),
|
|
127
|
-
content: z.string(),
|
|
128
|
-
wordCount: z.number(),
|
|
129
|
-
createdAt: z.number(),
|
|
130
|
-
updatedAt: z.number(),
|
|
131
|
-
}),
|
|
296
|
+
responseBody: documentPayloadSchema,
|
|
132
297
|
handler: ({ pathParams }) => {
|
|
133
298
|
const doc = getDocumentById(pathParams!.id);
|
|
134
299
|
if (!doc) {
|
|
@@ -201,6 +366,36 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
201
366
|
},
|
|
202
367
|
},
|
|
203
368
|
|
|
369
|
+
{
|
|
370
|
+
operationId: "documentForWorkspaceFile",
|
|
371
|
+
endpoint: "documents/for-workspace-file",
|
|
372
|
+
method: "POST",
|
|
373
|
+
policy: {
|
|
374
|
+
requiredScopes: ["settings.write"],
|
|
375
|
+
allowedPrincipalTypes: ACTOR_PRINCIPALS,
|
|
376
|
+
},
|
|
377
|
+
summary: "Get or create the document for a workspace markdown file",
|
|
378
|
+
description:
|
|
379
|
+
"Return the document bound to a workspace markdown file, creating it on " +
|
|
380
|
+
"first open. The file is the source of truth at open time: stored " +
|
|
381
|
+
"content that has drifted from the file is refreshed from disk before " +
|
|
382
|
+
"the document is returned.",
|
|
383
|
+
tags: ["documents"],
|
|
384
|
+
requestBody: z.object({
|
|
385
|
+
path: z
|
|
386
|
+
.string()
|
|
387
|
+
.describe("Workspace-relative path to a .md/.markdown file"),
|
|
388
|
+
conversationId: z
|
|
389
|
+
.string()
|
|
390
|
+
.describe("Conversation the document is opened from"),
|
|
391
|
+
}),
|
|
392
|
+
responseBody: documentPayloadSchema,
|
|
393
|
+
additionalResponses: {
|
|
394
|
+
"422": { description: "Path is not a markdown file" },
|
|
395
|
+
},
|
|
396
|
+
handler: handleDocumentForWorkspaceFile,
|
|
397
|
+
},
|
|
398
|
+
|
|
204
399
|
{
|
|
205
400
|
operationId: "linkDocumentConversation",
|
|
206
401
|
endpoint: "documents/:id/conversations",
|