@vellumai/assistant 0.8.7 → 0.8.8
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 +20 -4
- package/docker-entrypoint.sh +4 -2
- package/docker-init-apt-root.sh +3 -1
- package/docker-kata-apt-env.sh +3 -1
- package/docker-kata-runtime-family.sh +12 -0
- package/docs/architecture/memory.md +1 -1
- package/docs/plugins.md +75 -79
- package/examples/plugins/echo/README.md +6 -12
- package/examples/plugins/echo/register.ts +0 -41
- package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +3 -3
- package/openapi.yaml +3381 -348
- package/package.json +1 -1
- package/scripts/generate-openapi.ts +68 -41
- package/src/__tests__/agent-loop-exit-reason.test.ts +34 -39
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +1 -1
- package/src/__tests__/agent-loop.test.ts +37 -87
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
- package/src/__tests__/annotate-activity-metadata.test.ts +262 -0
- package/src/__tests__/annotate-risk-options.test.ts +2 -3
- package/src/__tests__/anthropic-provider.test.ts +95 -2
- package/src/__tests__/assistant-event-hub.test.ts +25 -0
- package/src/__tests__/assistant-events-sse-shed.test.ts +8 -0
- package/src/__tests__/{conversation-stream-state.test.ts → assistant-stream-state.test.ts} +252 -91
- package/src/__tests__/auth-fallback-events-store.test.ts +116 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +6 -0
- package/src/__tests__/btw-routes.test.ts +62 -3
- package/src/__tests__/build-persisted-content.test.ts +184 -0
- package/src/__tests__/catalog-files.test.ts +1 -1
- package/src/__tests__/clawhub-files.test.ts +1 -1
- package/src/__tests__/compaction-pipeline.test.ts +1 -1
- package/src/__tests__/compaction.benchmark.test.ts +0 -30
- package/src/__tests__/config-watcher.test.ts +1 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +57 -19
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +6 -2
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +10 -4
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +313 -1136
- package/src/__tests__/conversation-agent-loop.test.ts +596 -1616
- package/src/__tests__/conversation-analysis-routes.test.ts +6 -0
- package/src/__tests__/conversation-history-web-search.test.ts +11 -1
- package/src/__tests__/conversation-pairing.test.ts +4 -31
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +6 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +26 -5
- package/src/__tests__/conversation-queue.test.ts +2 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +3 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +6 -5
- package/src/__tests__/conversation-runtime-assembly.test.ts +170 -229
- package/src/__tests__/conversation-runtime-workspace.test.ts +3 -24
- package/src/__tests__/conversation-slash-commands.test.ts +8 -42
- package/src/__tests__/conversation-slash-queue.test.ts +6 -1
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +84 -0
- package/src/__tests__/conversation-sync-tags.test.ts +27 -15
- package/src/__tests__/conversation-title-service.test.ts +135 -2
- package/src/__tests__/conversation-workspace-injection.test.ts +6 -1
- package/src/__tests__/cross-provider-web-search.test.ts +214 -1
- package/src/__tests__/db-schedule-syntax-migration.test.ts +5 -0
- package/src/__tests__/dm-persistence.test.ts +5 -1
- package/src/__tests__/empty-response-hook.test.ts +304 -0
- package/src/__tests__/feature-flag-test-helpers.ts +2 -2
- package/src/__tests__/gemini-image-service.test.ts +13 -0
- package/src/__tests__/helpers/mock-provider.ts +110 -0
- package/src/__tests__/helpers/native-web-search-harness.ts +129 -0
- package/src/__tests__/history-repair-hook.test.ts +1 -0
- package/src/__tests__/identity-intro-cache.test.ts +12 -100
- package/src/__tests__/identity-routes.test.ts +248 -7
- package/src/__tests__/inbound-slack-persistence.test.ts +5 -1
- package/src/__tests__/injector-background-turn.test.ts +2 -8
- package/src/__tests__/injector-chain.test.ts +106 -270
- package/src/__tests__/injector-disk-pressure.test.ts +3 -12
- package/src/__tests__/injector-document-comments.test.ts +2 -2
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +30 -22
- package/src/__tests__/injector-v3-suppression.test.ts +31 -37
- package/src/__tests__/internal-telemetry-routes.test.ts +109 -0
- package/src/__tests__/list-messages-page-latest.test.ts +60 -0
- package/src/__tests__/list-messages-tool-merge.test.ts +20 -0
- package/src/__tests__/llm-usage-store.test.ts +223 -1
- package/src/__tests__/memory-retrieval-hook.test.ts +297 -0
- package/src/__tests__/memory-v2-static-injector.test.ts +103 -35
- package/src/__tests__/native-web-search.test.ts +191 -0
- package/src/__tests__/onboarding-template-contract.test.ts +2 -0
- package/src/__tests__/openai-image-service.test.ts +17 -0
- package/src/__tests__/openai-provider.test.ts +31 -1
- package/src/__tests__/persist-unsendable-image.test.ts +215 -0
- package/src/__tests__/persistence-secret-redaction.test.ts +1 -0
- package/src/__tests__/pipeline-runner.test.ts +29 -39
- package/src/__tests__/pkb-autoinject.test.ts +2 -5
- package/src/__tests__/plugin-bootstrap.test.ts +13 -28
- package/src/__tests__/plugin-registry.test.ts +0 -27
- package/src/__tests__/plugin-types.test.ts +2 -125
- package/src/__tests__/process-message-display-content.test.ts +6 -2
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +5 -1
- package/src/__tests__/resolve-trust-class.test.ts +4 -4
- package/src/__tests__/runtime-events-sse-reconnect.test.ts +60 -23
- package/src/__tests__/schedule-routes.test.ts +603 -2
- package/src/__tests__/schedule-store.test.ts +41 -0
- package/src/__tests__/schedule-tools.test.ts +35 -0
- package/src/__tests__/server-history-render.test.ts +314 -1
- package/src/__tests__/skillssh-files.test.ts +1 -1
- package/src/__tests__/system-prompt.test.ts +20 -0
- package/src/__tests__/task-scheduler.test.ts +162 -1
- package/src/__tests__/terminal-tools.test.ts +6 -1
- package/src/__tests__/title-generate-hook.test.ts +319 -0
- package/src/__tests__/tool-error-hook.test.ts +278 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +468 -5
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +127 -0
- package/src/__tests__/tool-result-truncation.test.ts +0 -2
- package/src/__tests__/ui-choice-copy-surfaces.test.ts +254 -0
- package/src/__tests__/ui-work-result-surface.test.ts +159 -0
- package/src/__tests__/usage-routes.test.ts +285 -1
- package/src/__tests__/user-plugin-loader.test.ts +2 -2
- package/src/__tests__/voice-session-bridge.test.ts +6 -3
- package/src/__tests__/web-search-backend-failure.test.ts +166 -0
- package/src/agent/loop.ts +346 -442
- package/src/api/events/assistant-thinking-delta.ts +33 -0
- package/src/api/events/tool-output-chunk.ts +45 -0
- package/src/api/events/tool-use-preview-start.ts +32 -0
- package/src/api/events/trace-event.ts +69 -0
- package/src/api/index.ts +48 -13
- package/src/api/responses/conversation-message.ts +368 -0
- package/src/avatar/__tests__/avatar-store.test.ts +34 -29
- package/src/cli/commands/__tests__/notifications.test.ts +58 -14
- package/src/cli/commands/notifications.ts +112 -60
- package/src/config/assistant-feature-flags.ts +22 -11
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -20
- package/src/config/bundled-skills/app-builder/references/examples/README.md +17 -0
- package/src/config/bundled-skills/app-builder/references/examples/expense-tracker.md +515 -0
- package/src/config/bundled-skills/app-builder/references/examples/focus-timer.md +342 -0
- package/src/config/bundled-skills/app-builder/references/examples/habit-tracker.md +490 -0
- package/src/config/bundled-skills/document-editor/SKILL.md +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +0 -7
- package/src/config/feature-flag-cache.ts +3 -3
- package/src/config/feature-flag-registry.json +35 -3
- package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +25 -0
- package/src/config/schemas/llm.ts +1 -0
- package/src/config/schemas/memory-v2.ts +8 -0
- package/src/config/schemas/memory-v3.ts +8 -0
- package/src/config/schemas/platform.ts +8 -0
- package/src/config/seed-inference-profiles.ts +2 -2
- package/src/config/skills.ts +13 -0
- package/src/context/compactor.ts +1 -1
- package/src/context/strip-injections.ts +122 -0
- package/src/context/token-estimator.ts +23 -0
- package/src/context/tool-result-truncation.ts +0 -23
- package/src/context/window-manager.ts +3 -6
- package/src/credential-execution/executable-discovery.ts +16 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +6 -0
- package/src/daemon/__tests__/inference-profile-notification.test.ts +153 -0
- package/src/daemon/__tests__/native-web-search-metadata.test.ts +10 -8
- package/src/daemon/assistant-attachments.ts +1 -1
- package/src/daemon/config-watcher.ts +2 -2
- package/src/daemon/context-overflow-reducer.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +605 -153
- package/src/daemon/conversation-agent-loop.ts +281 -760
- package/src/daemon/conversation-history.ts +5 -4
- package/src/daemon/conversation-lifecycle.ts +3 -4
- package/src/daemon/conversation-messaging.ts +7 -6
- package/src/daemon/conversation-process.ts +11 -16
- package/src/daemon/conversation-runtime-assembly.ts +130 -347
- package/src/daemon/conversation-slash.ts +6 -25
- package/src/daemon/conversation-surfaces.ts +222 -4
- package/src/daemon/conversation-tool-setup.ts +2 -29
- package/src/daemon/conversation.ts +32 -14
- package/src/daemon/external-plugins-bootstrap.ts +9 -10
- package/src/daemon/handlers/config-a2a.ts +51 -36
- package/src/daemon/handlers/config-slack-channel.ts +20 -14
- package/src/daemon/handlers/config-telegram.ts +16 -2
- package/src/daemon/handlers/shared.ts +156 -84
- package/src/daemon/handlers/skills.ts +39 -10
- package/src/daemon/lifecycle.ts +4 -0
- package/src/daemon/message-types/apps.ts +1 -29
- package/src/daemon/message-types/messages.ts +9 -57
- package/src/daemon/message-types/skills.ts +2 -0
- package/src/daemon/message-types/surfaces.ts +136 -3
- package/src/daemon/now-scratchpad.ts +21 -0
- package/src/daemon/orphan-reaper.test.ts +210 -0
- package/src/daemon/orphan-reaper.ts +240 -0
- package/src/daemon/persist-unsendable-image.ts +117 -0
- package/src/daemon/process-message.ts +1 -3
- package/src/daemon/trace-emitter.ts +6 -4
- package/src/daemon/trust-context.ts +19 -0
- package/src/daemon/wake-target-adapter.ts +3 -1
- package/src/home/home-greeting-cache.ts +24 -1
- package/src/ipc/gateway-client.test.ts +2 -2
- package/src/ipc/gateway-client.ts +3 -3
- package/src/media/gemini-image-service.ts +15 -0
- package/src/media/openai-image-service.ts +14 -0
- package/src/media/types.ts +34 -0
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +56 -0
- package/src/memory/auth-fallback-events-store.ts +94 -0
- package/src/memory/conversation-title-service.ts +65 -41
- package/src/memory/db-init.ts +4 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-registry.test.ts +119 -0
- package/src/memory/graph/conversation-graph-memory.ts +65 -0
- package/src/memory/jobs-store.ts +33 -0
- package/src/memory/jobs-worker.ts +31 -4
- package/src/memory/llm-usage-store.ts +224 -50
- package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +6 -5
- package/src/memory/migrations/270-schedule-source-conversation.ts +13 -0
- package/src/memory/migrations/271-create-auth-fallback-events.ts +21 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/pkb/autoinject.ts +61 -0
- package/src/memory/pkb/context.ts +50 -0
- package/src/memory/pkb/types.ts +14 -0
- package/src/memory/schedule-attribution-sql.ts +104 -0
- package/src/memory/schema/infrastructure.ts +16 -0
- package/src/memory/usage-grouped-buckets.ts +6 -1
- package/src/memory/v2/__tests__/consolidation-job.test.ts +1 -1
- package/src/memory/v2/consolidation-job.ts +1 -1
- package/src/memory/v3/__tests__/health.test.ts +16 -0
- package/src/memory/v3/__tests__/orchestrate.test.ts +45 -9
- package/src/memory/v3/__tests__/provider-blocks.test.ts +13 -0
- package/src/memory/v3/__tests__/router.test.ts +101 -29
- package/src/memory/v3/__tests__/selector.test.ts +93 -27
- package/src/memory/v3/__tests__/shadow-plugin.test.ts +23 -5
- package/src/memory/v3/health.ts +0 -0
- package/src/memory/v3/llm-retry.ts +32 -0
- package/src/memory/v3/orchestrate.ts +26 -14
- package/src/memory/v3/provider-blocks.ts +15 -5
- package/src/memory/v3/router.ts +48 -42
- package/src/memory/v3/selector.ts +57 -42
- package/src/memory/v3/shadow-plugin.ts +47 -15
- package/src/memory/v3/types.ts +8 -0
- package/src/notifications/conversation-pairing.ts +8 -15
- package/src/notifications/decision-engine.ts +6 -3
- package/src/notifications/home-feed-side-effect.ts +12 -1
- package/src/permissions/prompter.ts +4 -0
- package/src/plugin-api/constants.ts +4 -0
- package/src/plugin-api/index.ts +8 -1
- package/src/plugin-api/types.ts +151 -1
- package/src/plugins/defaults/empty-response/hooks/stop.ts +126 -0
- package/src/plugins/defaults/empty-response/register.ts +8 -13
- package/src/plugins/defaults/index.ts +1 -15
- package/src/plugins/defaults/injectors/register.ts +243 -74
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +91 -0
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +216 -0
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +35 -0
- package/src/plugins/defaults/title-generate/hooks/stop.ts +75 -0
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +35 -0
- package/src/plugins/defaults/title-generate/package.json +1 -1
- package/src/plugins/defaults/title-generate/register.ts +18 -18
- package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +118 -0
- package/src/plugins/defaults/tool-error/package.json +1 -1
- package/src/plugins/defaults/tool-error/register.ts +9 -21
- package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +32 -0
- package/src/plugins/defaults/tool-result-truncate/register.ts +10 -21
- package/src/plugins/defaults/tool-result-truncate/terminal.ts +37 -18
- package/src/plugins/pipeline.ts +6 -18
- package/src/plugins/registry.ts +8 -25
- package/src/plugins/types.ts +43 -474
- package/src/proactive-artifact/aux-message-injector.ts +3 -3
- package/src/proactive-artifact/job.test.ts +7 -12
- package/src/prompts/__tests__/system-prompt.test.ts +36 -0
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +62 -0
- package/src/prompts/templates/BOOTSTRAP.md +2 -2
- package/src/prompts/templates/system-sections.ts +15 -0
- package/src/providers/anthropic/client.ts +37 -29
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +112 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -0
- package/src/providers/openrouter/client.ts +1 -0
- package/src/providers/placeholder-sentinels.ts +35 -0
- package/src/runtime/__tests__/agent-wake.test.ts +5 -1
- package/src/runtime/agent-wake.ts +2 -2
- package/src/runtime/assistant-event-hub.ts +36 -6
- package/src/runtime/{conversation-stream-state.ts → assistant-stream-state.ts} +132 -58
- package/src/runtime/http-router.ts +16 -21
- package/src/runtime/http-types.ts +16 -70
- package/src/runtime/pending-interactions.ts +1 -0
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +265 -2
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +31 -1
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +6 -2
- package/src/runtime/routes/__tests__/tts-routes.test.ts +6 -2
- package/src/runtime/routes/app-management-routes.ts +6 -117
- package/src/runtime/routes/app-routes.ts +13 -15
- package/src/runtime/routes/attachment-routes.ts +26 -15
- package/src/runtime/routes/avatar-routes.ts +26 -0
- package/src/runtime/routes/btw-routes.ts +29 -23
- package/src/runtime/routes/consolidation-routes.ts +120 -20
- package/src/runtime/routes/conversation-query-routes.ts +2 -0
- package/src/runtime/routes/conversation-routes.ts +358 -184
- package/src/runtime/routes/documents-routes.ts +4 -0
- package/src/runtime/routes/domain-routes.ts +51 -37
- package/src/runtime/routes/epoch-millis-range.ts +34 -0
- package/src/runtime/routes/events-routes.ts +28 -34
- package/src/runtime/routes/gateway-log-routes.ts +26 -4
- package/src/runtime/routes/heartbeat-routes.ts +32 -12
- package/src/runtime/routes/identity-intro-cache.ts +11 -34
- package/src/runtime/routes/identity-routes.ts +208 -17
- package/src/runtime/routes/image-generation-routes.ts +40 -2
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/integrations/a2a.ts +12 -10
- package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +16 -0
- package/src/runtime/routes/integrations/slack/channel.ts +4 -0
- package/src/runtime/routes/integrations/slack/share.ts +27 -6
- package/src/runtime/routes/integrations/telegram.ts +6 -0
- package/src/runtime/routes/integrations/twilio.ts +42 -0
- package/src/runtime/routes/internal-telemetry-routes.ts +88 -0
- package/src/runtime/routes/log-export-routes.ts +8 -0
- package/src/runtime/routes/memory-v2-routes.ts +15 -8
- package/src/runtime/routes/memory-v3-routes.ts +50 -28
- package/src/runtime/routes/oauth-apps.ts +66 -12
- package/src/runtime/routes/oauth-providers.ts +44 -5
- package/src/runtime/routes/platform-routes.ts +81 -5
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +6 -4
- package/src/runtime/routes/playground/force-compact.ts +1 -1
- package/src/runtime/routes/rename-conversation-routes.ts +5 -0
- package/src/runtime/routes/schedule-routes.ts +152 -42
- package/src/runtime/routes/secret-routes.ts +14 -2
- package/src/runtime/routes/skills-routes.ts +43 -14
- package/src/runtime/routes/tool-call-confirmation-enrichment.test.ts +161 -0
- package/src/runtime/routes/tool-call-confirmation-enrichment.ts +107 -0
- package/src/runtime/routes/trust-rules-routes.ts +26 -2
- package/src/runtime/routes/tts-routes.ts +35 -0
- package/src/runtime/routes/types.ts +66 -8
- package/src/runtime/routes/usage-routes.ts +47 -39
- package/src/runtime/routes/webhook-routes.ts +41 -2
- package/src/runtime/routes/workspace-routes.ts +4 -0
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +6 -0
- package/src/runtime/services/analyze-conversation.ts +2 -2
- package/src/schedule/schedule-store.ts +20 -1
- package/src/schedule/schedule-usage-store.ts +83 -0
- package/src/schedule/scheduler.ts +12 -5
- package/src/skills/catalog-files.ts +2 -2
- package/src/skills/catalog-install.ts +3 -0
- package/src/skills/categories-cache.ts +118 -0
- package/src/skills/clawhub-files.ts +1 -2
- package/src/skills/skillssh-files.ts +1 -2
- package/src/telemetry/types.ts +29 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +112 -3
- package/src/telemetry/usage-telemetry-reporter.ts +57 -2
- package/src/tools/executor.ts +1 -53
- package/src/tools/network/__tests__/web-search-metadata.test.ts +7 -1
- package/src/tools/network/__tests__/web-search.test.ts +11 -3
- package/src/tools/network/web-search-error.test.ts +248 -0
- package/src/tools/network/web-search-error.ts +267 -0
- package/src/tools/network/web-search.ts +207 -48
- package/src/tools/schedule/create.ts +2 -0
- package/src/tools/terminal/safe-env.ts +10 -1
- package/src/tools/ui-surface/definitions.ts +9 -1
- package/src/tts/__tests__/provider-catalog-consistency.test.ts +85 -1
- package/src/tts/provider-catalog.ts +76 -1
- package/src/util/mutex.ts +47 -0
- package/src/workspace/git-service.ts +1 -42
- package/src/workspace/migrations/095-bump-heartbeat-interval-30m-to-60m.ts +51 -0
- package/src/workspace/migrations/096-reduce-quality-profile-effort.ts +72 -0
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +93 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/__tests__/bootstrap-turn-cleanup.test.ts +0 -44
- package/src/__tests__/empty-response-pipeline.test.ts +0 -423
- package/src/__tests__/llm-call-pipeline.test.ts +0 -287
- package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -418
- package/src/__tests__/persistence-pipeline.test.ts +0 -503
- package/src/__tests__/title-generate-pipeline.test.ts +0 -211
- package/src/__tests__/token-estimate-pipeline.test.ts +0 -479
- package/src/__tests__/tool-error-pipeline.test.ts +0 -241
- package/src/__tests__/tool-execute-pipeline.test.ts +0 -417
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -341
- package/src/daemon/bootstrap-turn-cleanup.ts +0 -45
- package/src/gallery/default-gallery.ts +0 -1359
- package/src/gallery/gallery-manifest.ts +0 -28
- package/src/home/feature-gate.ts +0 -22
- package/src/plugins/defaults/empty-response/middlewares/emptyResponse.ts +0 -22
- package/src/plugins/defaults/empty-response/terminal.ts +0 -106
- package/src/plugins/defaults/injectors/package.json +0 -15
- package/src/plugins/defaults/llm-call/middlewares/llmCall.ts +0 -17
- package/src/plugins/defaults/llm-call/package.json +0 -15
- package/src/plugins/defaults/llm-call/register.ts +0 -45
- package/src/plugins/defaults/memory-retrieval/middlewares/memoryRetrieval.ts +0 -17
- package/src/plugins/defaults/memory-retrieval/package.json +0 -15
- package/src/plugins/defaults/memory-retrieval/register.ts +0 -181
- package/src/plugins/defaults/persistence/middlewares/persistence.ts +0 -19
- package/src/plugins/defaults/persistence/package.json +0 -15
- package/src/plugins/defaults/persistence/register.ts +0 -38
- package/src/plugins/defaults/persistence/terminal.ts +0 -83
- package/src/plugins/defaults/title-generate/terminal.ts +0 -31
- package/src/plugins/defaults/token-estimate/middlewares/tokenEstimate.ts +0 -23
- package/src/plugins/defaults/token-estimate/package.json +0 -15
- package/src/plugins/defaults/token-estimate/register.ts +0 -34
- package/src/plugins/defaults/token-estimate/terminal.ts +0 -40
- package/src/plugins/defaults/tool-error/middlewares/toolError.ts +0 -21
- package/src/plugins/defaults/tool-error/terminal.ts +0 -47
- package/src/plugins/defaults/tool-execute/middlewares/toolExecute.ts +0 -23
- package/src/plugins/defaults/tool-execute/package.json +0 -15
- package/src/plugins/defaults/tool-execute/register.ts +0 -49
- package/src/plugins/defaults/tool-result-truncate/middlewares/toolResultTruncate.ts +0 -23
- package/src/plugins/defaults/tool-result-truncate/types.ts +0 -22
- package/src/skills/category-inference.ts +0 -111
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Periodic reaper for orphaned subprocesses that reparent to the daemon.
|
|
3
|
+
*
|
|
4
|
+
* Tools run commands in their own process group (`detached: true`) and, on
|
|
5
|
+
* timeout/abort, group-kill with `process.kill(-pgid, SIGKILL)` (the bash and
|
|
6
|
+
* host_bash tools, the skill sandbox runner, and the debug-bash route). The
|
|
7
|
+
* immediate child is reaped by Bun/libuv, but its descendants — e.g. git's
|
|
8
|
+
* transport helpers or a skill runner's `bun` process — were never spawned by
|
|
9
|
+
* the daemon, so when the group dies they reparent to PID 1. When the daemon
|
|
10
|
+
* runs as PID 1 in a container, Bun is not an init: it never calls `waitpid()`
|
|
11
|
+
* on those reparented orphans, so they accumulate as `<defunct>` entries that
|
|
12
|
+
* consume PID slots until the container is recycled.
|
|
13
|
+
*
|
|
14
|
+
* This reaper scans `/proc` for zombie children of the daemon and reaps each
|
|
15
|
+
* by **specific PID** with `WNOHANG`. It deliberately does NOT use
|
|
16
|
+
* `waitpid(-1)`: libuv reaps the children it spawned by specific PID on
|
|
17
|
+
* `SIGCHLD`, and a blanket `waitpid(-1)` would race libuv and could swallow a
|
|
18
|
+
* tracked child's exit status — libuv's own source handles the lost race by
|
|
19
|
+
* dropping the exit callback ("someone else stole the waitpid from us. Handle
|
|
20
|
+
* this by not handling it at all."). To stay clear of that race we only reap a
|
|
21
|
+
* zombie after it has survived at least one scan interval: libuv reaps its own
|
|
22
|
+
* within milliseconds of `SIGCHLD`, so anything still defunct a full interval
|
|
23
|
+
* later is a genuine orphan libuv is not tracking.
|
|
24
|
+
*
|
|
25
|
+
* The reaper is a no-op unless the daemon is PID 1 on Linux. Off PID 1 (local
|
|
26
|
+
* macOS dev, or if an init such as tini is ever placed above the daemon),
|
|
27
|
+
* orphans reparent to that init and are reaped there, so there is nothing for
|
|
28
|
+
* this to do. Because the daemon is PID 1, orphans already reparent to it and
|
|
29
|
+
* `PR_SET_CHILD_SUBREAPER` is unnecessary. It is additionally gated behind the
|
|
30
|
+
* `daemon.reapOrphanedSubprocesses` config flag (default off) so the behavior
|
|
31
|
+
* can be enabled per workspace for validation before becoming the default.
|
|
32
|
+
*
|
|
33
|
+
* References:
|
|
34
|
+
* - libuv reaps its own children by pid on SIGCHLD (`uv__wait_children`):
|
|
35
|
+
* https://github.com/nodejs/node/blob/main/deps/uv/src/unix/process.c
|
|
36
|
+
* - Subreaper reaping pattern for runtimes embedding libuv (specific-pid +
|
|
37
|
+
* WNOHANG, never `waitpid(-1)`, grace window for libuv co-existence):
|
|
38
|
+
* https://github.com/coopergwrenn/prctl-subreaper
|
|
39
|
+
* - waitpid(2): https://man7.org/linux/man-pages/man2/waitpid.2.html
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
43
|
+
import { dlopen, FFIType, ptr } from "bun:ffi";
|
|
44
|
+
|
|
45
|
+
import { getConfigReadOnly } from "../config/loader.js";
|
|
46
|
+
import { getLogger } from "../util/logger.js";
|
|
47
|
+
|
|
48
|
+
const log = getLogger("orphan-reaper");
|
|
49
|
+
|
|
50
|
+
/** Linux `WNOHANG` — return immediately if no child has changed state. */
|
|
51
|
+
const WNOHANG = 1;
|
|
52
|
+
|
|
53
|
+
const SCAN_INTERVAL_MS = 60_000;
|
|
54
|
+
|
|
55
|
+
let scanTimer: ReturnType<typeof setInterval> | null = null;
|
|
56
|
+
|
|
57
|
+
/** Zombie child PIDs observed on the previous scan (the grace set). */
|
|
58
|
+
let seenLastScan: Set<number> = new Set();
|
|
59
|
+
|
|
60
|
+
type WaitpidFn = (pid: number, statusPtr: unknown, options: number) => number;
|
|
61
|
+
|
|
62
|
+
let waitpid: WaitpidFn | null = null;
|
|
63
|
+
// Held at module scope so the backing buffer is not GC'd while `waitStatusPtr`
|
|
64
|
+
// keeps only a raw pointer into it (waitpid writes the exit status here).
|
|
65
|
+
let waitStatusBuf: Int32Array | null = null;
|
|
66
|
+
let waitStatusPtr: unknown = null;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Bind libc `waitpid` via FFI. Returns false (and disables the reaper) if FFI
|
|
70
|
+
* is unavailable so daemon startup never fails on this subsystem.
|
|
71
|
+
*/
|
|
72
|
+
function initWaitpid(): boolean {
|
|
73
|
+
if (waitpid) return true;
|
|
74
|
+
try {
|
|
75
|
+
const lib = dlopen("libc.so.6", {
|
|
76
|
+
waitpid: {
|
|
77
|
+
args: [FFIType.i32, FFIType.ptr, FFIType.i32],
|
|
78
|
+
returns: FFIType.i32,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
// Reusable out-param buffer for the wstatus we don't inspect.
|
|
82
|
+
waitStatusBuf = new Int32Array(1);
|
|
83
|
+
waitStatusPtr = ptr(waitStatusBuf);
|
|
84
|
+
waitpid = lib.symbols.waitpid as unknown as WaitpidFn;
|
|
85
|
+
return true;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
log.warn(
|
|
88
|
+
{ err },
|
|
89
|
+
"Orphan reaper unavailable: failed to bind libc waitpid via FFI",
|
|
90
|
+
);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface ZombieChild {
|
|
96
|
+
pid: number;
|
|
97
|
+
comm: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Parse a `/proc/<pid>/stat` line into its leading fields. `comm` (the
|
|
102
|
+
* executable name) may itself contain spaces and parentheses, so the fixed
|
|
103
|
+
* fields are read relative to the final `)` rather than by naive splitting.
|
|
104
|
+
* Returns null if the line is malformed.
|
|
105
|
+
*/
|
|
106
|
+
export function parseProcStat(
|
|
107
|
+
content: string,
|
|
108
|
+
): { comm: string; state: string; ppid: number } | null {
|
|
109
|
+
const lparen = content.indexOf("(");
|
|
110
|
+
const rparen = content.lastIndexOf(")");
|
|
111
|
+
if (lparen === -1 || rparen === -1 || rparen < lparen) return null;
|
|
112
|
+
const comm = content.slice(lparen + 1, rparen);
|
|
113
|
+
const rest = content.slice(rparen + 2).split(" ");
|
|
114
|
+
const state = rest[0];
|
|
115
|
+
const ppid = Number(rest[1]);
|
|
116
|
+
if (!state || !Number.isInteger(ppid)) return null;
|
|
117
|
+
return { comm, state, ppid };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Given the zombie child PIDs seen this scan and those seen on the previous
|
|
122
|
+
* scan, decide which to reap now. A zombie is only reaped once it has
|
|
123
|
+
* survived a full interval (present in `seenLast`), leaving newly-defunct
|
|
124
|
+
* children for libuv to reap first. Returns the PIDs to reap and the set to
|
|
125
|
+
* carry into the next scan.
|
|
126
|
+
*/
|
|
127
|
+
export function selectReapable(
|
|
128
|
+
current: number[],
|
|
129
|
+
seenLast: Set<number>,
|
|
130
|
+
): { reap: number[]; nextSeen: Set<number> } {
|
|
131
|
+
const reap = current.filter((pid) => seenLast.has(pid));
|
|
132
|
+
return { reap, nextSeen: new Set(current) };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Scan `/proc` for zombie (`Z`) processes whose parent is this daemon.
|
|
137
|
+
* Reparented orphans keep their original process group but their parent
|
|
138
|
+
* becomes PID 1 (the daemon), so they appear here once defunct.
|
|
139
|
+
*/
|
|
140
|
+
function findZombieChildren(): ZombieChild[] {
|
|
141
|
+
const selfPid = process.pid;
|
|
142
|
+
const zombies: ZombieChild[] = [];
|
|
143
|
+
let entries: string[];
|
|
144
|
+
try {
|
|
145
|
+
entries = readdirSync("/proc");
|
|
146
|
+
} catch {
|
|
147
|
+
return zombies;
|
|
148
|
+
}
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
const pid = Number(entry);
|
|
151
|
+
if (!Number.isInteger(pid) || pid <= 1) continue;
|
|
152
|
+
let stat: string;
|
|
153
|
+
try {
|
|
154
|
+
stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
155
|
+
} catch {
|
|
156
|
+
// Process exited between readdir and read — skip.
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const parsed = parseProcStat(stat);
|
|
160
|
+
if (parsed && parsed.state === "Z" && parsed.ppid === selfPid) {
|
|
161
|
+
zombies.push({ pid, comm: parsed.comm });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return zombies;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Reap zombie children that have persisted for at least one scan interval,
|
|
169
|
+
* leaving newly-defunct children for libuv to reap first.
|
|
170
|
+
*/
|
|
171
|
+
function reapScan(): void {
|
|
172
|
+
if (!waitpid) return;
|
|
173
|
+
const zombies = findZombieChildren();
|
|
174
|
+
const byPid = new Map(zombies.map((z) => [z.pid, z]));
|
|
175
|
+
const { reap, nextSeen } = selectReapable([...byPid.keys()], seenLastScan);
|
|
176
|
+
const reaped: ZombieChild[] = [];
|
|
177
|
+
for (const pid of reap) {
|
|
178
|
+
const rc = waitpid(pid, waitStatusPtr, WNOHANG);
|
|
179
|
+
// rc > 0: reaped. rc <= 0 (0 = not yet, -1 = ECHILD/raced): leave it.
|
|
180
|
+
if (rc > 0) reaped.push(byPid.get(pid)!);
|
|
181
|
+
}
|
|
182
|
+
seenLastScan = nextSeen;
|
|
183
|
+
if (reaped.length > 0) {
|
|
184
|
+
log.info(
|
|
185
|
+
{
|
|
186
|
+
count: reaped.length,
|
|
187
|
+
pids: reaped.map((z) => z.pid),
|
|
188
|
+
comms: reaped.map((z) => z.comm),
|
|
189
|
+
},
|
|
190
|
+
"Reaped orphaned subprocesses reparented to the daemon (PID 1)",
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Read the opt-in gate from workspace config (`daemon.reapOrphanedSubprocesses`),
|
|
197
|
+
* tolerating a missing or malformed config so startup never fails on it.
|
|
198
|
+
*/
|
|
199
|
+
function isReaperEnabled(): boolean {
|
|
200
|
+
try {
|
|
201
|
+
return getConfigReadOnly().daemon.reapOrphanedSubprocesses;
|
|
202
|
+
} catch {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Start the periodic orphan reaper. No-op unless the daemon is PID 1 on Linux
|
|
209
|
+
* (otherwise reparented orphans are reaped by the real init) and the
|
|
210
|
+
* `daemon.reapOrphanedSubprocesses` config gate is enabled.
|
|
211
|
+
*/
|
|
212
|
+
export function startOrphanReaper(): void {
|
|
213
|
+
if (scanTimer) return;
|
|
214
|
+
if (process.platform !== "linux" || process.pid !== 1) {
|
|
215
|
+
log.info(
|
|
216
|
+
{ platform: process.platform, pid: process.pid },
|
|
217
|
+
"Orphan reaper not started: daemon is not PID 1 on Linux",
|
|
218
|
+
);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (!isReaperEnabled()) {
|
|
222
|
+
log.info(
|
|
223
|
+
"Orphan reaper not started: daemon.reapOrphanedSubprocesses is disabled",
|
|
224
|
+
);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (!initWaitpid()) return;
|
|
228
|
+
seenLastScan = new Set();
|
|
229
|
+
scanTimer = setInterval(reapScan, SCAN_INTERVAL_MS);
|
|
230
|
+
scanTimer.unref?.();
|
|
231
|
+
log.info({ intervalMs: SCAN_INTERVAL_MS }, "Orphan reaper started");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function stopOrphanReaper(): void {
|
|
235
|
+
if (scanTimer) {
|
|
236
|
+
clearInterval(scanTimer);
|
|
237
|
+
scanTimer = null;
|
|
238
|
+
}
|
|
239
|
+
seenLastScan = new Set();
|
|
240
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence for the image-too-large recovery path.
|
|
3
|
+
*
|
|
4
|
+
* When the provider rejects a turn because an attached image exceeds its
|
|
5
|
+
* limits, the agent loop downgrades the offending image blocks in memory and
|
|
6
|
+
* retries. That transformation is transient — the stored message row keeps the
|
|
7
|
+
* full-size image block, so the rejected image is rehydrated on every later
|
|
8
|
+
* turn and keeps re-entering the model's context. This module makes the
|
|
9
|
+
* downgrade durable for images that can *never* be transmitted, so a rejected
|
|
10
|
+
* upload cannot resurface after the user re-uploads a smaller version.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { optimizeImageForTransport } from "../agent/image-optimize.js";
|
|
14
|
+
import { parseImageDimensions } from "../context/image-dimensions.js";
|
|
15
|
+
import {
|
|
16
|
+
getMessages,
|
|
17
|
+
updateMessageContent,
|
|
18
|
+
} from "../memory/conversation-crud.js";
|
|
19
|
+
import type { ContentBlock } from "../providers/types.js";
|
|
20
|
+
import { getLogger } from "../util/logger.js";
|
|
21
|
+
|
|
22
|
+
const log = getLogger("persist-unsendable-image");
|
|
23
|
+
|
|
24
|
+
// Anthropic rejects any image whose longest side exceeds this many pixels,
|
|
25
|
+
// regardless of payload size. Mirrors the user-facing message surfaced by
|
|
26
|
+
// `classifyConversationError` for the IMAGE_TOO_LARGE code.
|
|
27
|
+
// https://docs.anthropic.com/en/docs/build-with-claude/vision#image-size
|
|
28
|
+
const PROVIDER_MAX_IMAGE_DIMENSION = 8000;
|
|
29
|
+
|
|
30
|
+
// Anthropic rejects any single image whose base64 payload exceeds 5 MB.
|
|
31
|
+
// https://docs.anthropic.com/en/docs/build-with-claude/vision#image-size
|
|
32
|
+
const PROVIDER_MAX_IMAGE_PAYLOAD_BYTES = 5 * 1024 * 1024;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Note left in place of an image that cannot be sent to the provider. Shared
|
|
36
|
+
* with the in-memory recovery path so the persisted history matches what the
|
|
37
|
+
* model saw on the turn the image was rejected.
|
|
38
|
+
*/
|
|
39
|
+
export const UNSENDABLE_IMAGE_NOTE =
|
|
40
|
+
"(An image was attached but could not be sent — its dimensions exceed the provider limit and automatic resize was not available. Please resize the image and try again.)";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* True when a stored image block can never be transmitted to the provider: it
|
|
44
|
+
* violates a provider hard limit (per-side pixel cap or payload size) and
|
|
45
|
+
* cannot be shrunk to fit (re-optimization is a no-op).
|
|
46
|
+
*
|
|
47
|
+
* Stored blocks are already post-optimization, so a block that is still
|
|
48
|
+
* oversized here only stays oversized because resizing is unavailable on this
|
|
49
|
+
* host (e.g. `sips` is absent off macOS, or the format is unsupported). A
|
|
50
|
+
* downscalable image would have been reduced at upload time and would not reach
|
|
51
|
+
* this predicate, so it is left untouched.
|
|
52
|
+
*/
|
|
53
|
+
function isImagePermanentlyUnsendable(
|
|
54
|
+
block: Extract<ContentBlock, { type: "image" }>,
|
|
55
|
+
): boolean {
|
|
56
|
+
const payloadBytes = block.source.data.length;
|
|
57
|
+
const dims = parseImageDimensions(block.source.data, block.source.media_type);
|
|
58
|
+
const exceedsDimensionCap =
|
|
59
|
+
dims != null &&
|
|
60
|
+
(dims.width > PROVIDER_MAX_IMAGE_DIMENSION ||
|
|
61
|
+
dims.height > PROVIDER_MAX_IMAGE_DIMENSION);
|
|
62
|
+
const exceedsPayloadCap = payloadBytes > PROVIDER_MAX_IMAGE_PAYLOAD_BYTES;
|
|
63
|
+
if (!exceedsDimensionCap && !exceedsPayloadCap) return false;
|
|
64
|
+
|
|
65
|
+
const optimized = optimizeImageForTransport(
|
|
66
|
+
block.source.data,
|
|
67
|
+
block.source.media_type,
|
|
68
|
+
);
|
|
69
|
+
return optimized.data === block.source.data;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Rewrite every stored message in a conversation that holds a permanently
|
|
74
|
+
* unsendable image, replacing those image blocks with {@link
|
|
75
|
+
* UNSENDABLE_IMAGE_NOTE}. Reads stored content directly (not the in-memory,
|
|
76
|
+
* injection-enriched copy) so injected prefixes and hydrated source paths are
|
|
77
|
+
* never written back.
|
|
78
|
+
*
|
|
79
|
+
* Idempotent: once an image is replaced by the note there is no image block
|
|
80
|
+
* left to match, so re-running is a no-op. Returns the number of rewritten
|
|
81
|
+
* messages.
|
|
82
|
+
*/
|
|
83
|
+
export function persistUnsendableImageDowngrades(
|
|
84
|
+
conversationId: string,
|
|
85
|
+
): number {
|
|
86
|
+
let rewritten = 0;
|
|
87
|
+
for (const row of getMessages(conversationId)) {
|
|
88
|
+
// Cheap prefilter — JSON.stringify emits no spaces, so an image block
|
|
89
|
+
// always serializes with this exact substring.
|
|
90
|
+
if (!row.content.includes('"type":"image"')) continue;
|
|
91
|
+
|
|
92
|
+
let parsed: unknown;
|
|
93
|
+
try {
|
|
94
|
+
parsed = JSON.parse(row.content);
|
|
95
|
+
} catch {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (!Array.isArray(parsed)) continue;
|
|
99
|
+
|
|
100
|
+
let changed = false;
|
|
101
|
+
const next = (parsed as ContentBlock[]).map((block): ContentBlock => {
|
|
102
|
+
if (block.type !== "image") return block;
|
|
103
|
+
if (!isImagePermanentlyUnsendable(block)) return block;
|
|
104
|
+
changed = true;
|
|
105
|
+
return { type: "text", text: UNSENDABLE_IMAGE_NOTE };
|
|
106
|
+
});
|
|
107
|
+
if (!changed) continue;
|
|
108
|
+
|
|
109
|
+
updateMessageContent(row.id, JSON.stringify(next));
|
|
110
|
+
rewritten++;
|
|
111
|
+
log.info(
|
|
112
|
+
{ conversationId, messageId: row.id },
|
|
113
|
+
"Persisted unsendable-image downgrade so it cannot resurface on later turns",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return rewritten;
|
|
117
|
+
}
|
|
@@ -437,9 +437,7 @@ export async function processMessage(
|
|
|
437
437
|
conversation.getMessages().push(cleanMsg);
|
|
438
438
|
|
|
439
439
|
conversation.emitActivityState("thinking", "context_compacting");
|
|
440
|
-
const result = await conversation.forceCompact(
|
|
441
|
-
targetInputTokensOverride: slashResult.targetInputTokensOverride,
|
|
442
|
-
});
|
|
440
|
+
const result = await conversation.forceCompact();
|
|
443
441
|
const responseText = formatCompactResult(result);
|
|
444
442
|
const assistantMsg = createAssistantMessage(responseText);
|
|
445
443
|
const persistedAssistant = await addMessage(
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { v4 as uuid } from "uuid";
|
|
2
2
|
|
|
3
|
+
import type {
|
|
4
|
+
TraceEvent,
|
|
5
|
+
TraceEventKind,
|
|
6
|
+
TraceEventStatus,
|
|
7
|
+
} from "../api/events/trace-event.js";
|
|
3
8
|
import {
|
|
4
9
|
getMaxSequence,
|
|
5
10
|
persistTraceEvent,
|
|
6
11
|
} from "../memory/trace-event-store.js";
|
|
7
12
|
import { getLogger } from "../util/logger.js";
|
|
8
|
-
import type { ServerMessage
|
|
9
|
-
import type { TraceEvent } from "./message-types/messages.js";
|
|
10
|
-
|
|
11
|
-
export type TraceEventStatus = "info" | "success" | "warning" | "error";
|
|
13
|
+
import type { ServerMessage } from "./message-protocol.js";
|
|
12
14
|
|
|
13
15
|
const log = getLogger("trace-emitter");
|
|
14
16
|
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* imports (memory/conversation-crud → daemon/conversation-runtime-assembly).
|
|
6
6
|
*/
|
|
7
7
|
import type { ChannelId } from "../channels/types.js";
|
|
8
|
+
import { isHttpAuthDisabled } from "../config/env.js";
|
|
9
|
+
import type { TrustClass } from "../runtime/actor-trust-resolver.js";
|
|
8
10
|
|
|
9
11
|
export interface TrustContext {
|
|
10
12
|
/** Channel through which the inbound message arrived. */
|
|
@@ -62,3 +64,20 @@ export const FALLBACK_TURN_TRUST: TrustContext = {
|
|
|
62
64
|
sourceChannel: "vellum",
|
|
63
65
|
trustClass: "unknown",
|
|
64
66
|
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the effective trust class for an actor.
|
|
70
|
+
*
|
|
71
|
+
* When HTTP auth is disabled (dev bypass), always returns `'guardian'`
|
|
72
|
+
* so that control-plane gates don't block local development.
|
|
73
|
+
*
|
|
74
|
+
* When no trust context is available (e.g. desktop-only conversations that
|
|
75
|
+
* don't go through channel trust resolution), defaults to `'unknown'`
|
|
76
|
+
* to fail-closed.
|
|
77
|
+
*/
|
|
78
|
+
export function resolveTrustClass(
|
|
79
|
+
trustContext: TrustContext | undefined,
|
|
80
|
+
): TrustClass {
|
|
81
|
+
if (isHttpAuthDisabled()) return "guardian";
|
|
82
|
+
return trustContext?.trustClass ?? "unknown";
|
|
83
|
+
}
|
|
@@ -140,6 +140,8 @@ function translateAgentEventToServerMessage(
|
|
|
140
140
|
case "context_compacting":
|
|
141
141
|
case "compaction_circuit_open":
|
|
142
142
|
case "compaction_circuit_closed":
|
|
143
|
+
case "compaction_completed":
|
|
144
|
+
case "history_stripped":
|
|
143
145
|
case "agent_loop_exit":
|
|
144
146
|
return null;
|
|
145
147
|
case "llm_call_started":
|
|
@@ -192,7 +194,7 @@ export function conversationToWakeTarget(
|
|
|
192
194
|
},
|
|
193
195
|
isProcessing: () => conversation.isProcessing(),
|
|
194
196
|
markProcessing: (on) => {
|
|
195
|
-
conversation.
|
|
197
|
+
conversation.setProcessing(on);
|
|
196
198
|
},
|
|
197
199
|
setTrustContext: (ctx) => conversation.setTrustContext(ctx),
|
|
198
200
|
setWakeAllowedTools: (tools) => {
|
|
@@ -10,11 +10,15 @@
|
|
|
10
10
|
* Storage uses the existing `memory_checkpoints` table (simple key-value store).
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
15
|
+
|
|
13
16
|
import {
|
|
14
17
|
getMemoryCheckpoint,
|
|
15
18
|
setMemoryCheckpoint,
|
|
16
19
|
} from "../memory/checkpoints.js";
|
|
17
|
-
import {
|
|
20
|
+
import { resolveGuardianPersona } from "../prompts/persona-resolver.js";
|
|
21
|
+
import { getWorkspacePromptPath } from "../util/platform.js";
|
|
18
22
|
|
|
19
23
|
// ---------------------------------------------------------------------------
|
|
20
24
|
// Constants
|
|
@@ -26,6 +30,25 @@ const CHECKPOINT_KEY_TEXT = "home:greeting:text";
|
|
|
26
30
|
const CHECKPOINT_KEY_HASH = "home:greeting:content_hash";
|
|
27
31
|
const CHECKPOINT_KEY_TIMESTAMP = "home:greeting:cached_at";
|
|
28
32
|
|
|
33
|
+
const IDENTITY_FILES = ["IDENTITY.md", "SOUL.md"] as const;
|
|
34
|
+
|
|
35
|
+
function readWorkspaceFile(name: string): string {
|
|
36
|
+
try {
|
|
37
|
+
const path = getWorkspacePromptPath(name);
|
|
38
|
+
if (!existsSync(path)) return "";
|
|
39
|
+
return readFileSync(path, "utf-8");
|
|
40
|
+
} catch {
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function computeIdentityContentHash(): string {
|
|
46
|
+
const staticFiles = IDENTITY_FILES.map(readWorkspaceFile).join("\n---\n");
|
|
47
|
+
const guardianPersona = resolveGuardianPersona() ?? "";
|
|
48
|
+
const combined = staticFiles + "\n---\n" + guardianPersona;
|
|
49
|
+
return createHash("sha256").update(combined).digest("hex");
|
|
50
|
+
}
|
|
51
|
+
|
|
29
52
|
// ---------------------------------------------------------------------------
|
|
30
53
|
// Public API
|
|
31
54
|
// ---------------------------------------------------------------------------
|
|
@@ -91,7 +91,7 @@ describe("ipcGetFeatureFlags", () => {
|
|
|
91
91
|
expect(flags).toEqual({ "flag-a": true, "flag-b": false });
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
-
test("
|
|
94
|
+
test("accepts boolean and string values, filters other types from response", async () => {
|
|
95
95
|
mockGatewayIpc(null, {
|
|
96
96
|
results: {
|
|
97
97
|
get_feature_flags: {
|
|
@@ -104,7 +104,7 @@ describe("ipcGetFeatureFlags", () => {
|
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
const flags = await ipcGetFeatureFlags();
|
|
107
|
-
expect(flags).toEqual({ valid: true });
|
|
107
|
+
expect(flags).toEqual({ valid: true, string: "yes" });
|
|
108
108
|
});
|
|
109
109
|
|
|
110
110
|
test("returns empty record when IPC returns undefined", async () => {
|
|
@@ -98,12 +98,12 @@ export function resetPersistentClient(): void {
|
|
|
98
98
|
*/
|
|
99
99
|
export async function ipcGetFeatureFlags(
|
|
100
100
|
timeoutMs?: number,
|
|
101
|
-
): Promise<Record<string, boolean>> {
|
|
101
|
+
): Promise<Record<string, boolean | string>> {
|
|
102
102
|
const result = await ipcCall("get_feature_flags", undefined, timeoutMs);
|
|
103
103
|
if (result && typeof result === "object" && !Array.isArray(result)) {
|
|
104
|
-
const filtered: Record<string, boolean> = {};
|
|
104
|
+
const filtered: Record<string, boolean | string> = {};
|
|
105
105
|
for (const [k, v] of Object.entries(result as Record<string, unknown>)) {
|
|
106
|
-
if (typeof v === "boolean") filtered[k] = v;
|
|
106
|
+
if (typeof v === "boolean" || typeof v === "string") filtered[k] = v;
|
|
107
107
|
}
|
|
108
108
|
return filtered;
|
|
109
109
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type ImageGenCredentials,
|
|
6
6
|
type ImageGenerationRequest,
|
|
7
7
|
type ImageGenerationResult,
|
|
8
|
+
isImageProviderBillingError,
|
|
8
9
|
type ManagedProxyCredentials,
|
|
9
10
|
MAX_VARIANTS,
|
|
10
11
|
} from "./types.js";
|
|
@@ -19,9 +20,18 @@ const ALLOWED_MODELS = new Set([
|
|
|
19
20
|
|
|
20
21
|
// --- Error mapping ---
|
|
21
22
|
|
|
23
|
+
const GEMINI_BILLING_MESSAGE =
|
|
24
|
+
"Image generation is unavailable because the Gemini account or API key is out of credits. " +
|
|
25
|
+
"Add funds with the provider or update the key in Settings — retrying won't help until credits are added.";
|
|
26
|
+
|
|
22
27
|
export function mapGeminiError(error: unknown): string {
|
|
23
28
|
if (error instanceof ApiError) {
|
|
24
29
|
const status = error.status;
|
|
30
|
+
// Billing failures are non-retryable, so check them before the rate-limit
|
|
31
|
+
// branch to avoid telling the user to "wait and try again".
|
|
32
|
+
if (isImageProviderBillingError({ status, message: error.message })) {
|
|
33
|
+
return GEMINI_BILLING_MESSAGE;
|
|
34
|
+
}
|
|
25
35
|
if (status === 400) {
|
|
26
36
|
return "The image request was invalid. Please check your prompt and try again.";
|
|
27
37
|
}
|
|
@@ -37,6 +47,11 @@ export function mapGeminiError(error: unknown): string {
|
|
|
37
47
|
return `Gemini API error (status ${status}). Please try again.`;
|
|
38
48
|
}
|
|
39
49
|
if (error instanceof Error) {
|
|
50
|
+
// The managed proxy surfaces failures as plain Errors whose message embeds
|
|
51
|
+
// the upstream status (e.g. "Managed proxy request failed (402): ...").
|
|
52
|
+
if (isImageProviderBillingError({ message: error.message })) {
|
|
53
|
+
return GEMINI_BILLING_MESSAGE;
|
|
54
|
+
}
|
|
40
55
|
return `Image generation failed: ${error.message}`;
|
|
41
56
|
}
|
|
42
57
|
return "An unexpected error occurred during image generation.";
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type ImageGenCredentials,
|
|
7
7
|
type ImageGenerationRequest,
|
|
8
8
|
type ImageGenerationResult,
|
|
9
|
+
isImageProviderBillingError,
|
|
9
10
|
MAX_VARIANTS,
|
|
10
11
|
} from "./types.js";
|
|
11
12
|
|
|
@@ -16,6 +17,10 @@ const ALLOWED_MODELS = new Set(["gpt-image-2"]);
|
|
|
16
17
|
|
|
17
18
|
// --- Error mapping ---
|
|
18
19
|
|
|
20
|
+
const OPENAI_BILLING_MESSAGE =
|
|
21
|
+
"Image generation is unavailable because the OpenAI account or API key is out of credits. " +
|
|
22
|
+
"Add funds with the provider or update the key in Settings — retrying won't help until credits are added.";
|
|
23
|
+
|
|
19
24
|
/**
|
|
20
25
|
* Map an error raised by the OpenAI Images API to a user-friendly string.
|
|
21
26
|
* Mirrors the status-code branches of `mapGeminiError` in
|
|
@@ -24,6 +29,12 @@ const ALLOWED_MODELS = new Set(["gpt-image-2"]);
|
|
|
24
29
|
export function mapOpenAIError(error: unknown): string {
|
|
25
30
|
if (error instanceof OpenAI.APIError) {
|
|
26
31
|
const status = error.status;
|
|
32
|
+
// Billing failures are non-retryable and can surface as a 402 or as a 429
|
|
33
|
+
// with an `insufficient_quota` body, so check them before the rate-limit
|
|
34
|
+
// branch to avoid telling the user to "wait and try again".
|
|
35
|
+
if (isImageProviderBillingError({ status, message: error.message })) {
|
|
36
|
+
return OPENAI_BILLING_MESSAGE;
|
|
37
|
+
}
|
|
27
38
|
if (status === 400) {
|
|
28
39
|
return "The image request was invalid. Please check your prompt and try again.";
|
|
29
40
|
}
|
|
@@ -39,6 +50,9 @@ export function mapOpenAIError(error: unknown): string {
|
|
|
39
50
|
return `OpenAI API error (status ${status}). Please try again.`;
|
|
40
51
|
}
|
|
41
52
|
if (error instanceof Error) {
|
|
53
|
+
if (isImageProviderBillingError({ message: error.message })) {
|
|
54
|
+
return OPENAI_BILLING_MESSAGE;
|
|
55
|
+
}
|
|
42
56
|
return `Image generation failed: ${error.message}`;
|
|
43
57
|
}
|
|
44
58
|
return "An unexpected error occurred during image generation.";
|
package/src/media/types.ts
CHANGED
|
@@ -44,3 +44,37 @@ export function providerForImageModelPrefix(model: string): ImageGenProvider {
|
|
|
44
44
|
}
|
|
45
45
|
return "gemini";
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Message fragments that indicate a provider billing / insufficient-credits
|
|
50
|
+
* failure. Used by the per-provider error mappers to detect a non-retryable
|
|
51
|
+
* out-of-credits condition that no number of retries will resolve.
|
|
52
|
+
*/
|
|
53
|
+
const BILLING_MESSAGE_PATTERNS: readonly RegExp[] = [
|
|
54
|
+
/credit balance is too low/i,
|
|
55
|
+
/insufficient[\s_-]*credits?/i,
|
|
56
|
+
/insufficient_quota/i,
|
|
57
|
+
/exceeded your current quota/i,
|
|
58
|
+
/out of credits/i,
|
|
59
|
+
/requires more credits/i,
|
|
60
|
+
/billing/i,
|
|
61
|
+
/request failed \(402\)/i,
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Detect a provider billing / insufficient-credits failure from an HTTP status
|
|
66
|
+
* and/or error message. A 402 status is billing by definition; otherwise the
|
|
67
|
+
* message is matched against known billing phrasings (OpenAI's
|
|
68
|
+
* `insufficient_quota` is reported as a 429, so status alone is insufficient).
|
|
69
|
+
*
|
|
70
|
+
* Billing failures are non-retryable: the user must add funds or update the API
|
|
71
|
+
* key. Callers surface a distinct message instead of a generic "try again".
|
|
72
|
+
*/
|
|
73
|
+
export function isImageProviderBillingError(args: {
|
|
74
|
+
status?: number;
|
|
75
|
+
message?: string;
|
|
76
|
+
}): boolean {
|
|
77
|
+
if (args.status === 402) return true;
|
|
78
|
+
const message = args.message ?? "";
|
|
79
|
+
return BILLING_MESSAGE_PATTERNS.some((pattern) => pattern.test(message));
|
|
80
|
+
}
|