vellum 0.2.1 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -2
- package/bun.lock +71 -100
- package/package.json +5 -3
- package/scripts/capture-x-graphql.ts +562 -0
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -1
- package/scripts/test.sh +5 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +133 -34
- package/src/__tests__/account-registry.test.ts +2 -1
- package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
- package/src/__tests__/asset-materialize-tool.test.ts +16 -15
- package/src/__tests__/asset-search-tool.test.ts +23 -22
- package/src/__tests__/attachments-store.test.ts +56 -127
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
- package/src/__tests__/browser-skill-endstate.test.ts +4 -3
- package/src/__tests__/call-bridge.test.ts +385 -0
- package/src/__tests__/call-constants.test.ts +40 -0
- package/src/__tests__/call-orchestrator.test.ts +130 -4
- package/src/__tests__/call-recovery.test.ts +518 -0
- package/src/__tests__/call-routes-http.test.ts +459 -0
- package/src/__tests__/call-state-machine.test.ts +143 -0
- package/src/__tests__/call-store.test.ts +216 -1
- package/src/__tests__/cli-discover.test.ts +1 -1
- package/src/__tests__/commit-message-enrichment-service.test.ts +148 -7
- package/src/__tests__/compaction.benchmark.test.ts +176 -0
- package/src/__tests__/computer-use-tools.test.ts +250 -0
- package/src/__tests__/config-schema.test.ts +305 -3
- package/src/__tests__/conflict-store.test.ts +2 -1
- package/src/__tests__/contacts-tools.test.ts +331 -0
- package/src/__tests__/conversation-store.test.ts +30 -32
- package/src/__tests__/credential-security-invariants.test.ts +4 -0
- package/src/__tests__/date-context.test.ts +373 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
- package/src/__tests__/followup-tools.test.ts +303 -0
- package/src/__tests__/handlers-twilio-config.test.ts +221 -0
- package/src/__tests__/handlers-twitter-config.test.ts +718 -0
- package/src/__tests__/intent-routing.test.ts +64 -57
- package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
- package/src/__tests__/ipc-snapshot.test.ts +71 -28
- package/src/__tests__/llm-usage-store.test.ts +3 -8
- package/src/__tests__/media-generate-image.test.ts +1 -1
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
- package/src/__tests__/memory-regressions.test.ts +100 -2
- package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
- package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
- package/src/__tests__/playbook-tools.test.ts +342 -0
- package/src/__tests__/profile-compiler.test.ts +2 -1
- package/src/__tests__/provider-commit-message-generator.test.ts +303 -0
- package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
- package/src/__tests__/recurrence-engine.test.ts +69 -0
- package/src/__tests__/recurrence-types.test.ts +71 -0
- package/src/__tests__/registry.test.ts +5 -3
- package/src/__tests__/relay-server.test.ts +633 -0
- package/src/__tests__/reminder-store.test.ts +6 -3
- package/src/__tests__/reminder.test.ts +43 -77
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +8 -4
- package/src/__tests__/run-orchestrator.test.ts +4 -4
- package/src/__tests__/runtime-attachment-metadata.test.ts +7 -6
- package/src/__tests__/runtime-runs-http.test.ts +4 -4
- package/src/__tests__/runtime-runs.test.ts +4 -4
- package/src/__tests__/schedule-store.test.ts +482 -0
- package/src/__tests__/schedule-tools.test.ts +700 -0
- package/src/__tests__/scheduler-recurrence.test.ts +329 -0
- package/src/__tests__/server-history-render.test.ts +14 -13
- package/src/__tests__/session-conflict-gate.test.ts +28 -25
- package/src/__tests__/session-error.test.ts +28 -0
- package/src/__tests__/session-init.benchmark.test.ts +462 -0
- package/src/__tests__/session-queue.test.ts +71 -48
- package/src/__tests__/session-runtime-assembly.test.ts +161 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
- package/src/__tests__/signup-e2e.test.ts +2 -1
- package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
- package/src/__tests__/skill-script-runner.test.ts +159 -0
- package/src/__tests__/speaker-identification.test.ts +52 -0
- package/src/__tests__/subagent-manager-notify.test.ts +42 -10
- package/src/__tests__/subagent-tools.test.ts +141 -41
- package/src/__tests__/task-compiler.test.ts +2 -1
- package/src/__tests__/task-runner.test.ts +2 -1
- package/src/__tests__/task-scheduler.test.ts +2 -1
- package/src/__tests__/task-tools.test.ts +49 -56
- package/src/__tests__/tool-audit-listener.test.ts +1 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
- package/src/__tests__/tool-executor.test.ts +13 -17
- package/src/__tests__/turn-commit.test.ts +218 -3
- package/src/__tests__/twilio-provider.test.ts +143 -0
- package/src/__tests__/twilio-routes.test.ts +789 -0
- package/src/__tests__/twitter-auth-handler.test.ts +581 -0
- package/src/__tests__/view-image-tool.test.ts +217 -0
- package/src/__tests__/workspace-git-service.test.ts +186 -0
- package/src/__tests__/workspace-heartbeat-service.test.ts +13 -3
- package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
- package/src/bundler/app-bundler.ts +12 -8
- package/src/calls/__tests__/twilio-webhook-urls.test.ts +162 -0
- package/src/calls/call-bridge.ts +95 -0
- package/src/calls/call-constants.ts +43 -5
- package/src/calls/call-domain.ts +276 -0
- package/src/calls/call-orchestrator.ts +43 -17
- package/src/calls/call-recovery.ts +207 -0
- package/src/calls/call-state-machine.ts +68 -0
- package/src/calls/call-store.ts +192 -5
- package/src/calls/relay-server.ts +41 -4
- package/src/calls/speaker-identification.ts +213 -0
- package/src/calls/twilio-config.ts +8 -8
- package/src/calls/twilio-provider.ts +13 -9
- package/src/calls/twilio-routes.ts +90 -76
- package/src/calls/twilio-webhook-urls.ts +50 -0
- package/src/calls/types.ts +1 -1
- package/src/cli/config-commands.ts +334 -0
- package/src/cli/core-commands.ts +776 -0
- package/src/cli/doordash.ts +251 -1
- package/src/cli/ipc-client.ts +82 -0
- package/src/cli/map.ts +270 -0
- package/src/cli/twitter.ts +575 -0
- package/src/cli.ts +7 -5
- package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
- package/src/commands/cc-command-registry.ts +209 -0
- package/src/config/bundled-skills/contacts/SKILL.md +39 -0
- package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
- package/src/config/bundled-skills/document/SKILL.md +18 -0
- package/src/config/bundled-skills/document/TOOLS.json +53 -0
- package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
- package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
- package/src/config/bundled-skills/doordash/SKILL.md +82 -23
- package/src/config/bundled-skills/followups/SKILL.md +32 -0
- package/src/config/bundled-skills/followups/TOOLS.json +100 -0
- package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +1 -23
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
- package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
- package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
- package/src/config/bundled-skills/reminder/SKILL.md +20 -0
- package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
- package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
- package/src/config/bundled-skills/schedule/SKILL.md +74 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
- package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
- package/src/config/bundled-skills/subagent/SKILL.md +25 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
- package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
- package/src/config/bundled-skills/tasks/SKILL.md +28 -0
- package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
- package/src/config/bundled-skills/twitter/SKILL.md +134 -0
- package/src/config/bundled-skills/watcher/SKILL.md +27 -0
- package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
- package/src/config/defaults.ts +34 -0
- package/src/config/loader.ts +4 -1
- package/src/config/schema.ts +165 -1
- package/src/config/system-prompt.ts +61 -16
- package/src/config/templates/IDENTITY.md +7 -0
- package/src/config/types.ts +4 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +1 -5
- package/src/contacts/contact-store.ts +4 -4
- package/src/daemon/assistant-attachments.ts +10 -0
- package/src/daemon/classifier.ts +3 -1
- package/src/daemon/computer-use-session.ts +3 -1
- package/src/daemon/date-context.ts +136 -0
- package/src/daemon/handlers/apps.ts +16 -1
- package/src/daemon/handlers/browser.ts +54 -0
- package/src/daemon/handlers/computer-use.ts +7 -1
- package/src/daemon/handlers/config.ts +205 -5
- package/src/daemon/handlers/diagnostics.ts +5 -1
- package/src/daemon/handlers/documents.ts +18 -29
- package/src/daemon/handlers/home-base.ts +5 -1
- package/src/daemon/handlers/index.ts +40 -277
- package/src/daemon/handlers/misc.ts +9 -1
- package/src/daemon/handlers/publish.ts +6 -1
- package/src/daemon/handlers/sessions.ts +65 -12
- package/src/daemon/handlers/shared.ts +36 -1
- package/src/daemon/handlers/signing.ts +37 -0
- package/src/daemon/handlers/skills.ts +20 -6
- package/src/daemon/handlers/subagents.ts +8 -3
- package/src/daemon/handlers/twitter-auth.ts +169 -0
- package/src/daemon/handlers/work-items.ts +384 -68
- package/src/daemon/ipc-contract-inventory.json +32 -4
- package/src/daemon/ipc-contract.ts +156 -37
- package/src/daemon/ipc-protocol.ts +7 -2
- package/src/daemon/lifecycle.ts +21 -0
- package/src/daemon/main.ts +10 -4
- package/src/daemon/ride-shotgun-handler.ts +75 -10
- package/src/daemon/server.ts +143 -26
- package/src/daemon/session-agent-loop.ts +922 -0
- package/src/daemon/session-attachments.ts +28 -5
- package/src/daemon/session-conflict-gate.ts +18 -109
- package/src/daemon/session-error.ts +24 -3
- package/src/daemon/session-lifecycle.ts +147 -0
- package/src/daemon/session-media-retry.ts +147 -0
- package/src/daemon/session-messaging.ts +145 -0
- package/src/daemon/session-notifiers.ts +164 -0
- package/src/daemon/session-process.ts +2 -2
- package/src/daemon/session-queue-manager.ts +1 -0
- package/src/daemon/session-runtime-assembly.ts +52 -0
- package/src/daemon/session-skill-tools.ts +124 -5
- package/src/daemon/session-slash.ts +3 -0
- package/src/daemon/session-surfaces.ts +77 -2
- package/src/daemon/session-tool-setup.ts +216 -2
- package/src/daemon/session-usage.ts +0 -2
- package/src/daemon/session.ts +114 -1404
- package/src/daemon/video-thumbnail.ts +60 -0
- package/src/doordash/client.ts +121 -27
- package/src/doordash/queries.ts +1 -2
- package/src/export/formatter.ts +3 -1
- package/src/followups/followup-store.ts +4 -2
- package/src/followups/types.ts +6 -0
- package/src/hooks/templates.ts +1 -1
- package/src/index.ts +32 -1153
- package/src/memory/attachments-store.ts +28 -83
- package/src/memory/channel-delivery-store.ts +7 -21
- package/src/memory/clarification-resolver.ts +6 -5
- package/src/memory/conflict-intent.ts +114 -0
- package/src/memory/contradiction-checker.ts +3 -2
- package/src/memory/conversation-key-store.ts +10 -29
- package/src/memory/conversation-store.ts +2 -1
- package/src/memory/db.ts +96 -2
- package/src/memory/entity-extractor.ts +6 -3
- package/src/memory/items-extractor.ts +5 -4
- package/src/memory/job-handlers/conflict.ts +23 -1
- package/src/memory/jobs-store.ts +3 -2
- package/src/memory/llm-usage-store.ts +1 -2
- package/src/memory/runs-store.ts +1 -2
- package/src/memory/schema.ts +23 -2
- package/src/messaging/style-analyzer.ts +3 -2
- package/src/messaging/thread-summarizer.ts +8 -12
- package/src/messaging/triage-engine.ts +4 -2
- package/src/providers/openrouter/client.ts +20 -0
- package/src/providers/registry.ts +8 -0
- package/src/runtime/gateway-client.ts +36 -0
- package/src/runtime/http-server.ts +166 -22
- package/src/runtime/routes/attachment-routes.ts +2 -3
- package/src/runtime/routes/call-routes.ts +140 -0
- package/src/runtime/routes/channel-routes.ts +125 -88
- package/src/runtime/routes/conversation-routes.ts +5 -5
- package/src/runtime/routes/run-routes.ts +2 -2
- package/src/runtime/run-orchestrator.ts +9 -3
- package/src/schedule/recurrence-engine.ts +138 -0
- package/src/schedule/recurrence-types.ts +67 -0
- package/src/schedule/schedule-store.ts +102 -57
- package/src/schedule/scheduler.ts +9 -6
- package/src/security/oauth2.ts +29 -4
- package/src/security/secret-allowlist.ts +46 -0
- package/src/skills/clawhub.ts +1 -1
- package/src/subagent/manager.ts +40 -8
- package/src/swarm/backend-claude-code.ts +64 -9
- package/src/swarm/worker-prompts.ts +2 -1
- package/src/tasks/SPEC.md +34 -28
- package/src/tasks/ephemeral-permissions.ts +16 -7
- package/src/tasks/task-compiler.ts +5 -4
- package/src/tasks/task-runner.ts +10 -5
- package/src/tasks/task-scheduler.ts +1 -1
- package/src/tasks/tool-sanitizer.ts +36 -0
- package/src/tools/assets/search.ts +4 -4
- package/src/tools/browser/api-map.ts +293 -0
- package/src/tools/browser/auto-navigate.ts +270 -0
- package/src/tools/browser/browser-execution.ts +2 -1
- package/src/tools/browser/browser-manager.ts +2 -2
- package/src/tools/browser/network-recorder.ts +5 -4
- package/src/tools/browser/x-auto-navigate.ts +207 -0
- package/src/tools/calls/call-end.ts +17 -67
- package/src/tools/calls/call-start.ts +24 -85
- package/src/tools/calls/call-status.ts +35 -51
- package/src/tools/claude-code/claude-code.ts +207 -11
- package/src/tools/contacts/contact-merge.ts +46 -78
- package/src/tools/contacts/contact-search.ts +35 -79
- package/src/tools/contacts/contact-upsert.ts +35 -108
- package/src/tools/credentials/vault.ts +20 -4
- package/src/tools/document/document-tool.ts +71 -144
- package/src/tools/executor.ts +129 -10
- package/src/tools/followups/followup_create.ts +46 -88
- package/src/tools/followups/followup_list.ts +34 -74
- package/src/tools/followups/followup_resolve.ts +31 -66
- package/src/tools/host-terminal/cli-discover.ts +2 -1
- package/src/tools/host-terminal/host-shell.ts +10 -0
- package/src/tools/memory/handlers.ts +5 -4
- package/src/tools/network/__tests__/web-search.test.ts +427 -0
- package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
- package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
- package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
- package/src/tools/network/web-fetch.ts +18 -6
- package/src/tools/playbooks/index.ts +4 -5
- package/src/tools/playbooks/playbook-create.ts +3 -47
- package/src/tools/playbooks/playbook-delete.ts +1 -25
- package/src/tools/playbooks/playbook-list.ts +1 -28
- package/src/tools/playbooks/playbook-update.ts +3 -51
- package/src/tools/reminder/reminder.ts +5 -78
- package/src/tools/schedule/create.ts +69 -74
- package/src/tools/schedule/delete.ts +21 -47
- package/src/tools/schedule/list.ts +55 -74
- package/src/tools/schedule/update.ts +77 -84
- package/src/tools/subagent/abort.ts +29 -58
- package/src/tools/subagent/message.ts +30 -63
- package/src/tools/subagent/read.ts +53 -84
- package/src/tools/subagent/spawn.ts +43 -82
- package/src/tools/subagent/status.ts +42 -71
- package/src/tools/swarm/delegate.ts +2 -1
- package/src/tools/tasks/index.ts +8 -8
- package/src/tools/tasks/task-delete.ts +60 -88
- package/src/tools/tasks/task-list.ts +31 -52
- package/src/tools/tasks/task-run.ts +72 -108
- package/src/tools/tasks/task-save.ts +33 -65
- package/src/tools/tasks/work-item-enqueue.ts +183 -215
- package/src/tools/tasks/work-item-list.ts +33 -63
- package/src/tools/tasks/work-item-remove.ts +45 -97
- package/src/tools/tasks/work-item-update.ts +91 -163
- package/src/tools/terminal/backends/native.ts +3 -1
- package/src/tools/tool-manifest.ts +0 -62
- package/src/tools/types.ts +6 -0
- package/src/tools/ui-surface/definitions.ts +3 -1
- package/src/tools/watch/screen-watch.ts +3 -1
- package/src/tools/watcher/create.ts +52 -98
- package/src/tools/watcher/delete.ts +20 -46
- package/src/tools/watcher/digest.ts +36 -70
- package/src/tools/watcher/list.ts +49 -79
- package/src/tools/watcher/update.ts +45 -91
- package/src/twitter/client.ts +690 -0
- package/src/twitter/session.ts +91 -0
- package/src/usage/types.ts +0 -1
- package/src/util/truncate.ts +6 -0
- package/src/watcher/providers/slack.ts +2 -1
- package/src/watcher/watcher-store.ts +3 -2
- package/src/work-items/work-item-store.ts +27 -2
- package/src/workspace/commit-message-enrichment-service.ts +31 -7
- package/src/workspace/git-service.ts +87 -22
- package/src/workspace/provider-commit-message-generator.ts +269 -0
- package/src/workspace/turn-commit.ts +62 -3
- package/src/tools/contacts/index.ts +0 -4
- package/src/tools/document/index.ts +0 -5
- package/src/tools/followups/index.ts +0 -3
- package/src/tools/subagent/index.ts +0 -5
- /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
|
@@ -3,7 +3,8 @@ import type { UserMessageAttachment } from './ipc-protocol.js';
|
|
|
3
3
|
import { check, classifyRisk, generateAllowlistOptions, generateScopeOptions } from '../permissions/checker.js';
|
|
4
4
|
import { addRule } from '../permissions/trust-store.js';
|
|
5
5
|
import type { PermissionPrompter } from '../permissions/prompter.js';
|
|
6
|
-
import { uploadAttachment, linkAttachmentToMessage, AttachmentUploadError } from '../memory/attachments-store.js';
|
|
6
|
+
import { uploadAttachment, linkAttachmentToMessage, setAttachmentThumbnail, AttachmentUploadError } from '../memory/attachments-store.js';
|
|
7
|
+
import { generateVideoThumbnail } from './video-thumbnail.js';
|
|
7
8
|
import {
|
|
8
9
|
resolveDirectives,
|
|
9
10
|
contentBlocksToDrafts,
|
|
@@ -92,7 +93,6 @@ export async function resolveAssistantAttachments(
|
|
|
92
93
|
workingDir: string,
|
|
93
94
|
approveHostRead: ApproveHostRead,
|
|
94
95
|
lastAssistantMessageId: string | undefined,
|
|
95
|
-
assistantScope: string,
|
|
96
96
|
): Promise<AttachmentResolutionResult> {
|
|
97
97
|
let assistantAttachments: AssistantAttachmentDraft[] = [];
|
|
98
98
|
const emittedAttachments: UserMessageAttachment[] = [];
|
|
@@ -131,14 +131,17 @@ export async function resolveAssistantAttachments(
|
|
|
131
131
|
log.info('No directives or tool content blocks to resolve');
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
// Persist resolved attachments and link to the last assistant message
|
|
134
|
+
// Persist resolved attachments and link to the last assistant message.
|
|
135
|
+
// Large video attachments are omitted from the IPC payload and lazy-loaded
|
|
136
|
+
// by the client via the HTTP endpoint (same pattern as history_response).
|
|
137
|
+
const MAX_INLINE_B64_SIZE = 512 * 1024;
|
|
138
|
+
|
|
135
139
|
if (assistantAttachments.length > 0 && lastAssistantMessageId) {
|
|
136
140
|
for (let i = 0; i < assistantAttachments.length; i++) {
|
|
137
141
|
const draft = assistantAttachments[i];
|
|
138
142
|
let stored;
|
|
139
143
|
try {
|
|
140
144
|
stored = uploadAttachment(
|
|
141
|
-
assistantScope,
|
|
142
145
|
draft.filename,
|
|
143
146
|
draft.mimeType,
|
|
144
147
|
draft.dataBase64,
|
|
@@ -152,11 +155,31 @@ export async function resolveAssistantAttachments(
|
|
|
152
155
|
throw err;
|
|
153
156
|
}
|
|
154
157
|
linkAttachmentToMessage(lastAssistantMessageId, stored.id, i);
|
|
158
|
+
const isVideo = draft.mimeType.startsWith('video/');
|
|
159
|
+
const omitData = isVideo && draft.dataBase64.length > MAX_INLINE_B64_SIZE;
|
|
160
|
+
|
|
161
|
+
// Generate and persist a thumbnail for video attachments.
|
|
162
|
+
let thumbnailData: string | undefined;
|
|
163
|
+
if (isVideo) {
|
|
164
|
+
const existing = stored.thumbnailBase64;
|
|
165
|
+
if (existing) {
|
|
166
|
+
thumbnailData = existing;
|
|
167
|
+
} else {
|
|
168
|
+
const generated = await generateVideoThumbnail(draft.dataBase64);
|
|
169
|
+
if (generated) {
|
|
170
|
+
setAttachmentThumbnail(stored.id, generated);
|
|
171
|
+
thumbnailData = generated;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
155
176
|
emittedAttachments.push({
|
|
156
177
|
id: stored.id,
|
|
157
178
|
filename: draft.filename,
|
|
158
179
|
mimeType: draft.mimeType,
|
|
159
|
-
data: draft.dataBase64,
|
|
180
|
+
data: omitData ? '' : draft.dataBase64,
|
|
181
|
+
...(omitData ? { sizeBytes: draft.sizeBytes } : {}),
|
|
182
|
+
...(thumbnailData ? { thumbnailData } : {}),
|
|
160
183
|
});
|
|
161
184
|
}
|
|
162
185
|
} else if (assistantAttachments.length > 0) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Conflict-gate logic extracted from Session.
|
|
3
3
|
*
|
|
4
|
-
* Decides whether to ask the user about a pending memory conflict (relevant gate)
|
|
5
|
-
*
|
|
4
|
+
* Decides whether to ask the user about a pending memory conflict (relevant gate)
|
|
5
|
+
* or skip entirely.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {
|
|
@@ -12,6 +12,11 @@ import {
|
|
|
12
12
|
} from '../memory/conflict-store.js';
|
|
13
13
|
import type { PendingConflictDetail } from '../memory/conflict-store.js';
|
|
14
14
|
import { resolveConflictClarification } from '../memory/clarification-resolver.js';
|
|
15
|
+
import {
|
|
16
|
+
computeConflictRelevance,
|
|
17
|
+
looksLikeClarificationReply,
|
|
18
|
+
shouldAttemptConflictResolution,
|
|
19
|
+
} from '../memory/conflict-intent.js';
|
|
15
20
|
|
|
16
21
|
export interface ConflictGateDecision {
|
|
17
22
|
question: string;
|
|
@@ -39,14 +44,14 @@ export class ConflictGate {
|
|
|
39
44
|
const threshold = conflictConfig.relevanceThreshold;
|
|
40
45
|
const cooldownTurns = Math.max(1, conflictConfig.reaskCooldownTurns);
|
|
41
46
|
const pendingBeforeResolve = listPendingConflictDetails(scopeId, 50);
|
|
47
|
+
const clarificationReply = looksLikeClarificationReply(userMessage);
|
|
42
48
|
const candidatesBeforeResolve = pendingBeforeResolve.filter((conflict) => {
|
|
43
49
|
const relevance = computeConflictRelevance(userMessage, conflict);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
&& looksLikeClarificationReply(userMessage);
|
|
50
|
+
return shouldAttemptConflictResolution({
|
|
51
|
+
clarificationReply,
|
|
52
|
+
relevance,
|
|
53
|
+
wasRecentlyAsked: this.wasRecentlyAsked(conflict.id, cooldownTurns),
|
|
54
|
+
});
|
|
50
55
|
});
|
|
51
56
|
await this.resolvePendingConflicts(
|
|
52
57
|
userMessage,
|
|
@@ -61,18 +66,16 @@ export class ConflictGate {
|
|
|
61
66
|
conflict,
|
|
62
67
|
relevance: computeConflictRelevance(userMessage, conflict),
|
|
63
68
|
}));
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const askable = ordered.find((entry) => this.shouldAsk(entry.conflict.id, cooldownTurns));
|
|
69
|
+
const askable = scored
|
|
70
|
+
.filter((entry) => entry.relevance >= threshold)
|
|
71
|
+
.find((entry) => this.shouldAsk(entry.conflict.id, cooldownTurns));
|
|
69
72
|
if (!askable) return null;
|
|
70
73
|
|
|
71
74
|
this.lastAskedTurn.set(askable.conflict.id, this.turnCounter);
|
|
72
75
|
markConflictAsked(askable.conflict.id);
|
|
73
76
|
return {
|
|
74
77
|
question: askable.conflict.clarificationQuestion ?? buildFallbackConflictQuestion(askable.conflict),
|
|
75
|
-
relevant:
|
|
78
|
+
relevant: true,
|
|
76
79
|
};
|
|
77
80
|
}
|
|
78
81
|
|
|
@@ -122,98 +125,4 @@ export function buildFallbackConflictQuestion(conflict: PendingConflictDetail):
|
|
|
122
125
|
'Which one should I keep?',
|
|
123
126
|
].join('\n');
|
|
124
127
|
}
|
|
125
|
-
|
|
126
|
-
export function computeConflictRelevance(
|
|
127
|
-
userMessage: string,
|
|
128
|
-
conflict: Pick<PendingConflictDetail, 'existingStatement' | 'candidateStatement'>,
|
|
129
|
-
): number {
|
|
130
|
-
const queryTokens = tokenizeForConflictRelevance(userMessage);
|
|
131
|
-
if (queryTokens.size === 0) return 0;
|
|
132
|
-
const existingTokens = tokenizeForConflictRelevance(conflict.existingStatement);
|
|
133
|
-
const candidateTokens = tokenizeForConflictRelevance(conflict.candidateStatement);
|
|
134
|
-
return Math.max(
|
|
135
|
-
overlapRatio(queryTokens, existingTokens),
|
|
136
|
-
overlapRatio(queryTokens, candidateTokens),
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function tokenizeForConflictRelevance(input: string): Set<string> {
|
|
141
|
-
const tokens = input
|
|
142
|
-
.toLowerCase()
|
|
143
|
-
.split(/[^a-z0-9]+/g)
|
|
144
|
-
.map((token) => token.trim())
|
|
145
|
-
.filter((token) => token.length >= 4);
|
|
146
|
-
return new Set(tokens);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function overlapRatio(left: Set<string>, right: Set<string>): number {
|
|
150
|
-
if (left.size === 0 || right.size === 0) return 0;
|
|
151
|
-
let overlap = 0;
|
|
152
|
-
for (const token of left) {
|
|
153
|
-
if (right.has(token)) overlap += 1;
|
|
154
|
-
}
|
|
155
|
-
return overlap / Math.max(left.size, right.size);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Action verbs that signal the user is making a deliberate choice.
|
|
159
|
-
const ACTION_CUES = new Set([
|
|
160
|
-
'keep', 'use', 'prefer', 'go', 'pick', 'choose', 'take', 'want', 'select',
|
|
161
|
-
]);
|
|
162
|
-
|
|
163
|
-
// Directional/merge cue words mirrored from clarification-resolver.ts heuristics.
|
|
164
|
-
const DIRECTIONAL_CUES = new Set([
|
|
165
|
-
'existing', 'old', 'previous', 'first', 'earlier', 'original',
|
|
166
|
-
'candidate', 'new', 'latest', 'second', 'updated', 'instead', 'replace',
|
|
167
|
-
'both', 'merge', 'combine', 'together', 'either', 'mix',
|
|
168
|
-
'option', 'former', 'latter',
|
|
169
|
-
]);
|
|
170
|
-
|
|
171
|
-
const MAX_REPLY_WORD_COUNT = 12;
|
|
172
|
-
|
|
173
|
-
// Direction-only matches (no action verb) must be very short to avoid
|
|
174
|
-
// false positives from unrelated statements that happen to contain
|
|
175
|
-
// common words like "new", "old", "option", etc.
|
|
176
|
-
const MAX_DIRECTION_ONLY_WORD_COUNT = 4;
|
|
177
|
-
|
|
178
|
-
// Messages starting with a question word are unlikely to be clarification
|
|
179
|
-
// replies even when they lack a trailing question mark.
|
|
180
|
-
const QUESTION_WORD_PREFIXES = new Set([
|
|
181
|
-
'what', 'how', 'why', 'where', 'when', 'which', 'who', 'whom', 'whose',
|
|
182
|
-
]);
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Determines whether a user message looks like a deliberate reply to a
|
|
186
|
-
* recently asked conflict clarification (e.g. "keep the new one", "both",
|
|
187
|
-
* "option B", "new one"). Requires at least one action or directional cue,
|
|
188
|
-
* and the message must be short. Questions and longer statements are excluded
|
|
189
|
-
* to prevent accidental resolution from unrelated messages.
|
|
190
|
-
*
|
|
191
|
-
* When only directional cues are present (no action verb), the message must
|
|
192
|
-
* be very short (<= 4 words) to avoid false positives from unrelated messages
|
|
193
|
-
* like "What's new in Bun" or "try the old approach".
|
|
194
|
-
*/
|
|
195
|
-
export function looksLikeClarificationReply(userMessage: string): boolean {
|
|
196
|
-
const trimmed = userMessage.trim();
|
|
197
|
-
if (trimmed.endsWith('?')) return false;
|
|
198
|
-
|
|
199
|
-
const words = trimmed.toLowerCase().split(/\s+/).filter(Boolean);
|
|
200
|
-
if (words.length === 0 || words.length > MAX_REPLY_WORD_COUNT) return false;
|
|
201
|
-
|
|
202
|
-
const normalized = words.map((w) => w.replace(/[^a-z]/g, ''));
|
|
203
|
-
|
|
204
|
-
// Reject messages that start with a question word (even without '?').
|
|
205
|
-
// Match exact question words or contractions (e.g. "what's", "where's"),
|
|
206
|
-
// but not words that merely share a prefix (e.g. "whichever", "however").
|
|
207
|
-
const firstWord = words[0];
|
|
208
|
-
const firstNorm = normalized[0];
|
|
209
|
-
for (const qw of QUESTION_WORD_PREFIXES) {
|
|
210
|
-
if (firstNorm === qw || (firstWord.startsWith(qw) && "'\u2018\u2019".includes(firstWord[qw.length]))) return false;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const hasAction = normalized.some((w) => ACTION_CUES.has(w));
|
|
214
|
-
const hasDirection = normalized.some((w) => DIRECTIONAL_CUES.has(w));
|
|
215
|
-
|
|
216
|
-
if (hasAction) return true;
|
|
217
|
-
if (hasDirection) return words.length <= MAX_DIRECTION_ONLY_WORD_COUNT;
|
|
218
|
-
return false;
|
|
219
|
-
}
|
|
128
|
+
export { computeConflictRelevance, looksLikeClarificationReply };
|
|
@@ -41,7 +41,16 @@ const CONTEXT_TOO_LARGE_PATTERNS = [
|
|
|
41
41
|
/prompt.?is.?too.?long/i,
|
|
42
42
|
/request too large/i,
|
|
43
43
|
/too many.*input.*tokens/i,
|
|
44
|
-
/max_tokens/i,
|
|
44
|
+
/max_tokens.*exceeded/i,
|
|
45
|
+
/exceeded.*max_tokens/i,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
// Generic timeout patterns — checked after NETWORK_PATTERNS and PROVIDER_API_PATTERNS
|
|
49
|
+
// so that "connection timeout" → PROVIDER_NETWORK and "gateway timeout" → PROVIDER_API
|
|
50
|
+
const TIMEOUT_PATTERNS = [
|
|
51
|
+
/\btimeout\b/i,
|
|
52
|
+
/deadline.?exceeded/i,
|
|
53
|
+
/request.?timed?.?out/i,
|
|
45
54
|
];
|
|
46
55
|
|
|
47
56
|
// Provider API error patterns (5xx, server error, etc.)
|
|
@@ -210,7 +219,7 @@ function classifyByMessage(message: string): Omit<ClassifiedSessionError, 'debug
|
|
|
210
219
|
}
|
|
211
220
|
}
|
|
212
221
|
|
|
213
|
-
// Network errors
|
|
222
|
+
// Network errors (before timeout so "connection timeout" is classified as network)
|
|
214
223
|
for (const pattern of NETWORK_PATTERNS) {
|
|
215
224
|
if (pattern.test(message)) {
|
|
216
225
|
return {
|
|
@@ -221,7 +230,7 @@ function classifyByMessage(message: string): Omit<ClassifiedSessionError, 'debug
|
|
|
221
230
|
}
|
|
222
231
|
}
|
|
223
232
|
|
|
224
|
-
// Provider API errors (
|
|
233
|
+
// Provider API errors (before timeout so "gateway timeout" keeps its specific message)
|
|
225
234
|
for (const pattern of PROVIDER_API_PATTERNS) {
|
|
226
235
|
if (pattern.test(message)) {
|
|
227
236
|
return {
|
|
@@ -232,6 +241,18 @@ function classifyByMessage(message: string): Omit<ClassifiedSessionError, 'debug
|
|
|
232
241
|
}
|
|
233
242
|
}
|
|
234
243
|
|
|
244
|
+
// Generic timeout errors (checked after network and provider API patterns so
|
|
245
|
+
// specific timeouts like "connection timeout" and "gateway timeout" aren't misclassified)
|
|
246
|
+
for (const pattern of TIMEOUT_PATTERNS) {
|
|
247
|
+
if (pattern.test(message)) {
|
|
248
|
+
return {
|
|
249
|
+
code: 'PROVIDER_API',
|
|
250
|
+
userMessage: 'The request took too long. This is usually temporary — try again shortly.',
|
|
251
|
+
retryable: true,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
235
256
|
// Non-user abort/failure (e.g. AbortError from internal logic, not user cancel)
|
|
236
257
|
for (const pattern of CANCEL_PATTERNS) {
|
|
237
258
|
if (pattern.test(message)) {
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session lifecycle methods extracted from Session: loadFromDb, abort,
|
|
3
|
+
* and dispose. Each operates on a context interface so the Session class
|
|
4
|
+
* can delegate without exposing its full surface.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Message, ContentBlock } from '../providers/types.js';
|
|
8
|
+
import type { UsageStats, SurfaceType, SurfaceData } from './ipc-protocol.js';
|
|
9
|
+
import { repairHistory } from './history-repair.js';
|
|
10
|
+
import { createContextSummaryMessage } from '../context/window-manager.js';
|
|
11
|
+
import * as conversationStore from '../memory/conversation-store.js';
|
|
12
|
+
import type { PermissionPrompter } from '../permissions/prompter.js';
|
|
13
|
+
import type { SecretPrompter } from '../permissions/secret-prompter.js';
|
|
14
|
+
import type { ToolProfiler } from '../events/tool-profiling-listener.js';
|
|
15
|
+
import type { EventBus } from '../events/bus.js';
|
|
16
|
+
import type { AssistantDomainEvents } from '../events/domain-events.js';
|
|
17
|
+
import type { MessageQueue } from './session-queue-manager.js';
|
|
18
|
+
import { getHookManager } from '../hooks/manager.js';
|
|
19
|
+
import { getLogger } from '../util/logger.js';
|
|
20
|
+
import { unregisterWatchNotifiers, unregisterCallNotifiers } from './session-notifiers.js';
|
|
21
|
+
import { unregisterSessionSender } from '../tools/browser/browser-screencast.js';
|
|
22
|
+
import { resetSkillToolProjection } from './session-skill-tools.js';
|
|
23
|
+
|
|
24
|
+
const log = getLogger('session-lifecycle');
|
|
25
|
+
|
|
26
|
+
// ── Context Interfaces ───────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export interface LoadFromDbContext {
|
|
29
|
+
readonly conversationId: string;
|
|
30
|
+
messages: Message[];
|
|
31
|
+
usageStats: UsageStats;
|
|
32
|
+
contextCompactedMessageCount: number;
|
|
33
|
+
contextCompactedAt: number | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AbortContext {
|
|
37
|
+
readonly conversationId: string;
|
|
38
|
+
processing: boolean;
|
|
39
|
+
abortController: AbortController | null;
|
|
40
|
+
prompter: PermissionPrompter;
|
|
41
|
+
secretPrompter: SecretPrompter;
|
|
42
|
+
pendingSurfaceActions: Map<string, { surfaceType: SurfaceType }>;
|
|
43
|
+
surfaceState: Map<string, { surfaceType: SurfaceType; data: SurfaceData }>;
|
|
44
|
+
readonly queue: MessageQueue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DisposeContext extends AbortContext {
|
|
48
|
+
eventBus: EventBus<AssistantDomainEvents>;
|
|
49
|
+
readonly skillProjectionState: Map<string, string>;
|
|
50
|
+
profiler: ToolProfiler;
|
|
51
|
+
messages: Message[];
|
|
52
|
+
surfaceUndoStacks: Map<string, string[]>;
|
|
53
|
+
currentTurnSurfaces: Array<unknown>;
|
|
54
|
+
lastSurfaceAction: Map<string, unknown>;
|
|
55
|
+
workspaceTopLevelContext: string | null;
|
|
56
|
+
abort(): void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── loadFromDb ───────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
export async function loadFromDb(ctx: LoadFromDbContext): Promise<void> {
|
|
62
|
+
const dbMessages = conversationStore.getMessages(ctx.conversationId);
|
|
63
|
+
|
|
64
|
+
const conv = conversationStore.getConversation(ctx.conversationId);
|
|
65
|
+
const contextSummary = conv?.contextSummary?.trim() || null;
|
|
66
|
+
ctx.contextCompactedMessageCount = Math.max(
|
|
67
|
+
0,
|
|
68
|
+
Math.min(conv?.contextCompactedMessageCount ?? 0, dbMessages.length),
|
|
69
|
+
);
|
|
70
|
+
ctx.contextCompactedAt = conv?.contextCompactedAt ?? null;
|
|
71
|
+
|
|
72
|
+
const parsedMessages: Message[] = dbMessages
|
|
73
|
+
.slice(ctx.contextCompactedMessageCount)
|
|
74
|
+
.map((m) => {
|
|
75
|
+
const role = m.role as 'user' | 'assistant';
|
|
76
|
+
let content: ContentBlock[];
|
|
77
|
+
try {
|
|
78
|
+
const parsed = JSON.parse(m.content);
|
|
79
|
+
content = Array.isArray(parsed) ? parsed : [{ type: 'text', text: m.content }];
|
|
80
|
+
} catch {
|
|
81
|
+
log.warn({ conversationId: ctx.conversationId, messageId: m.id }, 'Invalid JSON in persisted message content, replacing with safe text block');
|
|
82
|
+
content = [{ type: 'text', text: m.content }];
|
|
83
|
+
}
|
|
84
|
+
return { role, content };
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const { messages: repairedMessages, stats } = repairHistory(parsedMessages);
|
|
88
|
+
if (stats.assistantToolResultsMigrated > 0 || stats.missingToolResultsInserted > 0 || stats.orphanToolResultsDowngraded > 0 || stats.consecutiveSameRoleMerged > 0) {
|
|
89
|
+
log.warn({ conversationId: ctx.conversationId, phase: 'load', ...stats }, 'Repaired persisted history');
|
|
90
|
+
}
|
|
91
|
+
ctx.messages = repairedMessages;
|
|
92
|
+
|
|
93
|
+
if (contextSummary) {
|
|
94
|
+
ctx.messages.unshift(createContextSummaryMessage(contextSummary));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (conv) {
|
|
98
|
+
ctx.usageStats = {
|
|
99
|
+
inputTokens: conv.totalInputTokens,
|
|
100
|
+
outputTokens: conv.totalOutputTokens,
|
|
101
|
+
estimatedCost: conv.totalEstimatedCost,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
log.info({ conversationId: ctx.conversationId, count: ctx.messages.length }, 'Loaded messages from DB');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── abort ─────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
export function abortSession(ctx: AbortContext): void {
|
|
111
|
+
if (ctx.processing) {
|
|
112
|
+
log.info({ conversationId: ctx.conversationId }, 'Aborting in-flight processing');
|
|
113
|
+
ctx.abortController?.abort();
|
|
114
|
+
ctx.prompter.dispose();
|
|
115
|
+
ctx.secretPrompter.dispose();
|
|
116
|
+
ctx.pendingSurfaceActions.clear();
|
|
117
|
+
ctx.surfaceState.clear();
|
|
118
|
+
unregisterWatchNotifiers(ctx.conversationId);
|
|
119
|
+
for (const queued of ctx.queue) {
|
|
120
|
+
queued.onEvent({ type: 'generation_cancelled', sessionId: ctx.conversationId });
|
|
121
|
+
}
|
|
122
|
+
ctx.queue.clear();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── dispose ──────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
export function disposeSession(ctx: DisposeContext): void {
|
|
129
|
+
void getHookManager().trigger('session-end', {
|
|
130
|
+
sessionId: ctx.conversationId,
|
|
131
|
+
});
|
|
132
|
+
ctx.abort();
|
|
133
|
+
unregisterCallNotifiers(ctx.conversationId);
|
|
134
|
+
unregisterSessionSender(ctx.conversationId);
|
|
135
|
+
resetSkillToolProjection(ctx.skillProjectionState);
|
|
136
|
+
ctx.eventBus.dispose();
|
|
137
|
+
|
|
138
|
+
// Release heavy in-memory data so GC can reclaim it
|
|
139
|
+
ctx.messages = [];
|
|
140
|
+
ctx.profiler.clear();
|
|
141
|
+
ctx.surfaceUndoStacks.clear();
|
|
142
|
+
ctx.currentTurnSurfaces = [];
|
|
143
|
+
ctx.pendingSurfaceActions.clear();
|
|
144
|
+
ctx.surfaceState.clear();
|
|
145
|
+
ctx.lastSurfaceAction.clear();
|
|
146
|
+
ctx.workspaceTopLevelContext = null;
|
|
147
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media payload trimming for context-too-large retry scenarios.
|
|
3
|
+
*
|
|
4
|
+
* When the provider rejects a request because the context is too large,
|
|
5
|
+
* this module replaces older image and file content blocks with lightweight
|
|
6
|
+
* text stubs to shrink the payload before retrying.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Message, ContentBlock } from '../providers/types.js';
|
|
10
|
+
import { getSummaryFromContextMessage } from '../context/window-manager.js';
|
|
11
|
+
|
|
12
|
+
const RETRY_KEEP_LATEST_MEDIA_BLOCKS = 3;
|
|
13
|
+
const MAX_MEDIA_STUB_TEXT = 2_000;
|
|
14
|
+
|
|
15
|
+
export function stripMediaPayloadsForRetry(messages: Message[]): { messages: Message[]; modified: boolean; replacedBlocks: number; latestUserIndex: number | null } {
|
|
16
|
+
let latestUserIndex: number | null = null;
|
|
17
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
18
|
+
const msg = messages[i];
|
|
19
|
+
if (msg.role !== 'user') continue;
|
|
20
|
+
if (getSummaryFromContextMessage(msg) !== null) continue;
|
|
21
|
+
if (isToolResultOnlyMessage(msg)) continue;
|
|
22
|
+
latestUserIndex = i;
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let modified = false;
|
|
27
|
+
let replacedBlocks = 0;
|
|
28
|
+
let keptLatestMediaBlocks = 0;
|
|
29
|
+
|
|
30
|
+
const nextMessages = messages.map((msg, msgIndex) => {
|
|
31
|
+
const nextContent: ContentBlock[] = [];
|
|
32
|
+
for (const block of msg.content) {
|
|
33
|
+
if (block.type === 'image') {
|
|
34
|
+
const keep = latestUserIndex === msgIndex && keptLatestMediaBlocks < RETRY_KEEP_LATEST_MEDIA_BLOCKS;
|
|
35
|
+
if (keep) {
|
|
36
|
+
keptLatestMediaBlocks += 1;
|
|
37
|
+
nextContent.push(block);
|
|
38
|
+
} else {
|
|
39
|
+
replacedBlocks += 1;
|
|
40
|
+
modified = true;
|
|
41
|
+
nextContent.push(imageBlockToStub(block));
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (block.type === 'file') {
|
|
47
|
+
const keep = latestUserIndex === msgIndex && keptLatestMediaBlocks < RETRY_KEEP_LATEST_MEDIA_BLOCKS;
|
|
48
|
+
if (keep) {
|
|
49
|
+
keptLatestMediaBlocks += 1;
|
|
50
|
+
nextContent.push(block);
|
|
51
|
+
} else {
|
|
52
|
+
replacedBlocks += 1;
|
|
53
|
+
modified = true;
|
|
54
|
+
nextContent.push(fileBlockToStub(block));
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (block.type === 'tool_result' && block.contentBlocks && block.contentBlocks.length > 0) {
|
|
60
|
+
let toolResultChanged = false;
|
|
61
|
+
const nextToolContentBlocks: ContentBlock[] = block.contentBlocks.map((cb) => {
|
|
62
|
+
if (cb.type === 'image') {
|
|
63
|
+
replacedBlocks += 1;
|
|
64
|
+
modified = true;
|
|
65
|
+
toolResultChanged = true;
|
|
66
|
+
return imageBlockToStub(cb);
|
|
67
|
+
}
|
|
68
|
+
if (cb.type === 'file') {
|
|
69
|
+
replacedBlocks += 1;
|
|
70
|
+
modified = true;
|
|
71
|
+
toolResultChanged = true;
|
|
72
|
+
return fileBlockToStub(cb);
|
|
73
|
+
}
|
|
74
|
+
return cb;
|
|
75
|
+
});
|
|
76
|
+
if (toolResultChanged) {
|
|
77
|
+
nextContent.push({ ...block, contentBlocks: nextToolContentBlocks });
|
|
78
|
+
} else {
|
|
79
|
+
nextContent.push(block);
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
nextContent.push(block);
|
|
85
|
+
}
|
|
86
|
+
return { ...msg, content: nextContent };
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
messages: modified ? nextMessages : messages,
|
|
91
|
+
modified,
|
|
92
|
+
replacedBlocks,
|
|
93
|
+
latestUserIndex,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function imageBlockToStub(block: Extract<ContentBlock, { type: 'image' }>): Extract<ContentBlock, { type: 'text' }> {
|
|
98
|
+
const sizeBytes = Math.ceil(block.source.data.length / 4) * 3;
|
|
99
|
+
return {
|
|
100
|
+
type: 'text',
|
|
101
|
+
text: `[Image omitted from retry context: ${block.source.media_type}, ${sizeBytes} bytes]`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function fileBlockToStub(block: Extract<ContentBlock, { type: 'file' }>): Extract<ContentBlock, { type: 'text' }> {
|
|
106
|
+
const sizeBytes = Math.ceil(block.source.data.length / 4) * 3;
|
|
107
|
+
const extracted = (block.extracted_text ?? '').trim();
|
|
108
|
+
const preview = extracted.length > MAX_MEDIA_STUB_TEXT
|
|
109
|
+
? `${extracted.slice(0, MAX_MEDIA_STUB_TEXT)}...`
|
|
110
|
+
: extracted;
|
|
111
|
+
return {
|
|
112
|
+
type: 'text',
|
|
113
|
+
text: preview.length > 0
|
|
114
|
+
? `[File omitted from retry context: ${block.source.filename} (${block.source.media_type}, ${sizeBytes} bytes)]\n${preview}`
|
|
115
|
+
: `[File omitted from retry context: ${block.source.filename} (${block.source.media_type}, ${sizeBytes} bytes)]`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isToolResultOnlyMessage(message: Message): boolean {
|
|
120
|
+
return message.content.length > 0
|
|
121
|
+
&& message.content.every((block) => block.type === 'tool_result');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Race a promise against a timeout. Returns 'completed' if the promise
|
|
126
|
+
* resolves/rejects within the budget, or 'timed_out' if the timeout fires
|
|
127
|
+
* first. The timer is always cleared in `finally` to prevent handle leaks.
|
|
128
|
+
*/
|
|
129
|
+
export async function raceWithTimeout<T>(
|
|
130
|
+
promise: Promise<T>,
|
|
131
|
+
timeoutMs: number,
|
|
132
|
+
): Promise<'completed' | 'timed_out'> {
|
|
133
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
134
|
+
try {
|
|
135
|
+
const result = await Promise.race([
|
|
136
|
+
promise.then(() => 'completed' as const, () => 'completed' as const),
|
|
137
|
+
new Promise<'timed_out'>((resolve) => {
|
|
138
|
+
timer = setTimeout(() => resolve('timed_out'), timeoutMs);
|
|
139
|
+
}),
|
|
140
|
+
]);
|
|
141
|
+
return result;
|
|
142
|
+
} finally {
|
|
143
|
+
if (timer !== undefined) {
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|