vellum 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -2
- package/bun.lock +5 -2
- package/package.json +4 -2
- 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 +299 -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-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 +62 -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-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-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-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/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-provider.ts +10 -6
- package/src/calls/twilio-routes.ts +90 -76
- 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 +246 -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 +33 -0
- package/src/config/loader.ts +4 -1
- package/src/config/schema.ts +161 -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/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 +163 -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 +28 -4
- package/src/daemon/ipc-contract.ts +133 -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 +74 -10
- package/src/daemon/server.ts +143 -26
- package/src/daemon/session-agent-loop.ts +887 -0
- package/src/daemon/session-attachments.ts +28 -5
- 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/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/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/http-server.ts +108 -20
- 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 +5 -10
- 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 +220 -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 +77 -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 +242 -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
package/src/daemon/session.ts
CHANGED
|
@@ -1,90 +1,55 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Session — thin coordinator that delegates to extracted modules.
|
|
3
|
+
*
|
|
4
|
+
* Each concern lives in its own file:
|
|
5
|
+
* - session-lifecycle.ts — loadFromDb, abort, dispose
|
|
6
|
+
* - session-messaging.ts — enqueueMessage, persistUserMessage, redirectToSecurePrompt
|
|
7
|
+
* - session-agent-loop.ts — runAgentLoop, generateTitle
|
|
8
|
+
* - session-notifiers.ts — watch/call notifier registration
|
|
9
|
+
* - session-tool-setup.ts — tool definitions, executor, resolveTools callback
|
|
10
|
+
* - session-media-retry.ts — media trimming + raceWithTimeout
|
|
11
|
+
* - session-process.ts — drainQueue, processMessage
|
|
12
|
+
* - session-history.ts — undo, regenerate, consolidateAssistantMessages
|
|
13
|
+
* - session-surfaces.ts — handleSurfaceAction, handleSurfaceUndo
|
|
14
|
+
* - session-workspace.ts — refreshWorkspaceTopLevelContext
|
|
15
|
+
* - session-usage.ts — recordUsage
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { Message } from '../providers/types.js';
|
|
19
|
+
import type { ServerMessage, UsageStats, UserMessageAttachment, SurfaceType, SurfaceData } from './ipc-protocol.js';
|
|
5
20
|
import { AgentLoop } from '../agent/loop.js';
|
|
6
|
-
import type { CheckpointDecision } from '../agent/loop.js';
|
|
7
21
|
import type { Provider } from '../providers/types.js';
|
|
8
|
-
import { createUserMessage, createAssistantMessage } from '../agent/message-types.js';
|
|
9
|
-
import * as conversationStore from '../memory/conversation-store.js';
|
|
10
22
|
import { PermissionPrompter } from '../permissions/prompter.js';
|
|
11
23
|
import { SecretPrompter } from '../permissions/secret-prompter.js';
|
|
12
24
|
import { ToolExecutor } from '../tools/executor.js';
|
|
13
25
|
import type { UserDecision } from '../permissions/types.js';
|
|
14
26
|
import { getConfig } from '../config/loader.js';
|
|
15
|
-
import { getLogger } from '../util/logger.js';
|
|
16
27
|
import { TraceEmitter } from './trace-emitter.js';
|
|
17
|
-
import { classifySessionError, isUserCancellation, isContextTooLarge, buildSessionErrorMessage } from './session-error.js';
|
|
18
28
|
import { EventBus } from '../events/bus.js';
|
|
19
29
|
import type { AssistantDomainEvents } from '../events/domain-events.js';
|
|
20
|
-
import {
|
|
21
|
-
registerWatchStartNotifier,
|
|
22
|
-
unregisterWatchStartNotifier,
|
|
23
|
-
registerWatchCommentaryNotifier,
|
|
24
|
-
unregisterWatchCommentaryNotifier,
|
|
25
|
-
registerWatchCompletionNotifier,
|
|
26
|
-
unregisterWatchCompletionNotifier,
|
|
27
|
-
pruneWatchSessions,
|
|
28
|
-
} from '../tools/watch/watch-state.js';
|
|
29
|
-
import type { WatchSession } from '../tools/watch/watch-state.js';
|
|
30
|
-
import { lastCommentaryBySession, lastSummaryBySession } from './watch-handler.js';
|
|
31
30
|
import { createToolDomainEventPublisher } from '../events/tool-domain-event-publisher.js';
|
|
32
31
|
import { registerToolMetricsLoggingListener } from '../events/tool-metrics-listener.js';
|
|
33
32
|
import { registerToolNotificationListener } from '../events/tool-notification-listener.js';
|
|
34
33
|
import { registerToolTraceListener } from '../events/tool-trace-listener.js';
|
|
35
34
|
import { createToolAuditListener } from '../events/tool-audit-listener.js';
|
|
36
35
|
import { ToolProfiler, registerToolProfilingListener } from '../events/tool-profiling-listener.js';
|
|
37
|
-
import {
|
|
38
|
-
ContextWindowManager,
|
|
39
|
-
createContextSummaryMessage,
|
|
40
|
-
getSummaryFromContextMessage,
|
|
41
|
-
} from '../context/window-manager.js';
|
|
36
|
+
import { ContextWindowManager } from '../context/window-manager.js';
|
|
42
37
|
import { getHookManager } from '../hooks/manager.js';
|
|
43
|
-
import {
|
|
44
|
-
stripMemoryRecallMessages,
|
|
45
|
-
} from '../memory/retriever.js';
|
|
46
|
-
import { getApp, listAppFiles } from '../memory/app-store.js';
|
|
47
38
|
import { ConflictGate } from './session-conflict-gate.js';
|
|
48
|
-
import { stripDynamicProfileMessages } from './session-dynamic-profile.js';
|
|
49
39
|
import { MessageQueue } from './session-queue-manager.js';
|
|
50
40
|
import type { QueueDrainReason } from './session-queue-manager.js';
|
|
51
|
-
import {
|
|
52
|
-
|
|
53
|
-
stripActiveSurfaceContext,
|
|
54
|
-
stripWorkspaceTopLevelContext,
|
|
55
|
-
stripChannelCapabilityContext,
|
|
56
|
-
} from './session-runtime-assembly.js';
|
|
57
|
-
import type {
|
|
58
|
-
ActiveSurfaceContext,
|
|
59
|
-
ChannelCapabilities,
|
|
60
|
-
} from './session-runtime-assembly.js';
|
|
61
|
-
import {
|
|
62
|
-
cleanAssistantContent,
|
|
63
|
-
drainDirectiveDisplayBuffer,
|
|
64
|
-
type DirectiveRequest,
|
|
65
|
-
type AssistantAttachmentDraft,
|
|
66
|
-
} from './assistant-attachments.js';
|
|
41
|
+
import type { ChannelCapabilities } from './session-runtime-assembly.js';
|
|
42
|
+
import type { AssistantAttachmentDraft } from './assistant-attachments.js';
|
|
67
43
|
import {
|
|
68
44
|
handleSurfaceAction as handleSurfaceActionImpl,
|
|
69
45
|
handleSurfaceUndo as handleSurfaceUndoImpl,
|
|
70
46
|
} from './session-surfaces.js';
|
|
71
|
-
import { prepareMemoryContext } from './session-memory.js';
|
|
72
47
|
import {
|
|
73
|
-
approveHostAttachmentRead,
|
|
74
|
-
formatAttachmentWarnings,
|
|
75
|
-
resolveAssistantAttachments,
|
|
76
|
-
} from './session-attachments.js';
|
|
77
|
-
import {
|
|
78
|
-
consolidateAssistantMessages,
|
|
79
48
|
undo as undoImpl,
|
|
80
49
|
regenerate as regenerateImpl,
|
|
81
50
|
type HistorySessionContext,
|
|
82
51
|
} from './session-history.js';
|
|
83
|
-
import { recordUsage } from './session-usage.js';
|
|
84
|
-
import { recordRequestLog } from '../memory/llm-request-log-store.js';
|
|
85
|
-
import { isProviderOrderingError } from './session-slash.js';
|
|
86
52
|
import { refreshWorkspaceTopLevelContextIfNeeded as refreshWorkspaceImpl } from './session-workspace.js';
|
|
87
|
-
import type { UsageActor } from '../usage/actors.js';
|
|
88
53
|
import {
|
|
89
54
|
drainQueue as drainQueueImpl,
|
|
90
55
|
processMessage as processMessageImpl,
|
|
@@ -93,12 +58,24 @@ import {
|
|
|
93
58
|
import {
|
|
94
59
|
buildToolDefinitions,
|
|
95
60
|
createToolExecutor,
|
|
61
|
+
createResolveToolsCallback,
|
|
96
62
|
type ToolSetupContext,
|
|
97
63
|
} from './session-tool-setup.js';
|
|
98
|
-
import {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
import {
|
|
64
|
+
import type { SkillProjectionCache } from './session-skill-tools.js';
|
|
65
|
+
|
|
66
|
+
// Extracted modules
|
|
67
|
+
import { registerSessionNotifiers } from './session-notifiers.js';
|
|
68
|
+
import {
|
|
69
|
+
loadFromDb as loadFromDbImpl,
|
|
70
|
+
abortSession,
|
|
71
|
+
disposeSession,
|
|
72
|
+
} from './session-lifecycle.js';
|
|
73
|
+
import {
|
|
74
|
+
enqueueMessage as enqueueMessageImpl,
|
|
75
|
+
persistUserMessage as persistUserMessageImpl,
|
|
76
|
+
redirectToSecurePrompt as redirectToSecurePromptImpl,
|
|
77
|
+
} from './session-messaging.js';
|
|
78
|
+
import { runAgentLoopImpl } from './session-agent-loop.js';
|
|
102
79
|
|
|
103
80
|
export interface SessionMemoryPolicy {
|
|
104
81
|
scopeId: string;
|
|
@@ -112,88 +89,56 @@ export const DEFAULT_MEMORY_POLICY: Readonly<SessionMemoryPolicy> = Object.freez
|
|
|
112
89
|
strictSideEffects: false,
|
|
113
90
|
});
|
|
114
91
|
|
|
115
|
-
const log = getLogger('session');
|
|
116
|
-
const RETRY_KEEP_LATEST_MEDIA_BLOCKS = 3;
|
|
117
|
-
const MAX_MEDIA_STUB_TEXT = 2_000;
|
|
118
|
-
|
|
119
92
|
export { MAX_QUEUE_DEPTH, type QueueDrainReason, type QueuePolicy } from './session-queue-manager.js';
|
|
120
93
|
export { findLastUndoableUserMessageIndex } from './session-history.js';
|
|
121
94
|
|
|
122
95
|
export class Session {
|
|
123
96
|
public readonly conversationId: string;
|
|
124
|
-
|
|
125
|
-
/** @internal
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
/** @internal — exposed for session-history.ts module functions. */
|
|
129
|
-
processing = false;
|
|
97
|
+
/** @internal */ provider: Provider;
|
|
98
|
+
/** @internal */ messages: Message[] = [];
|
|
99
|
+
/** @internal */ agentLoop: AgentLoop;
|
|
100
|
+
/** @internal */ processing = false;
|
|
130
101
|
private stale = false;
|
|
131
|
-
/** @internal
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
private secretPrompter: SecretPrompter;
|
|
102
|
+
/** @internal */ abortController: AbortController | null = null;
|
|
103
|
+
/** @internal */ prompter: PermissionPrompter;
|
|
104
|
+
/** @internal */ secretPrompter: SecretPrompter;
|
|
135
105
|
private executor: ToolExecutor;
|
|
136
|
-
|
|
137
|
-
/** @internal
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
/** @internal
|
|
143
|
-
|
|
144
|
-
/** @internal
|
|
145
|
-
|
|
146
|
-
/** @internal
|
|
147
|
-
|
|
148
|
-
/** @internal
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
|
|
154
|
-
/** @internal
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
/** @internal
|
|
161
|
-
currentRequestId?: string;
|
|
162
|
-
/** @internal — exposed for session-usage.ts module functions. */
|
|
163
|
-
assistantId: string | null = null;
|
|
164
|
-
private conflictGate = new ConflictGate();
|
|
165
|
-
/** @internal — exposed for session-tool-setup.ts to propagate into ToolContext. */
|
|
166
|
-
hasNoClient = false;
|
|
167
|
-
/** @internal — exposed for session-process.ts module functions. */
|
|
168
|
-
readonly queue = new MessageQueue();
|
|
169
|
-
/** @internal — exposed for session-process.ts module functions. */
|
|
170
|
-
currentActiveSurfaceId?: string;
|
|
171
|
-
/** @internal — exposed for session-process.ts module functions. */
|
|
172
|
-
currentPage?: string;
|
|
173
|
-
private channelCapabilities?: ChannelCapabilities;
|
|
174
|
-
/** @internal — exposed for session-surfaces.ts module functions. */
|
|
175
|
-
pendingSurfaceActions = new Map<string, {
|
|
176
|
-
surfaceType: SurfaceType;
|
|
177
|
-
}>();
|
|
106
|
+
/** @internal */ profiler: ToolProfiler;
|
|
107
|
+
/** @internal */ sendToClient: (msg: ServerMessage) => void;
|
|
108
|
+
/** @internal */ eventBus = new EventBus<AssistantDomainEvents>();
|
|
109
|
+
/** @internal */ workingDir: string;
|
|
110
|
+
/** @internal */ sandboxOverride?: boolean;
|
|
111
|
+
/** @internal */ allowedToolNames?: Set<string>;
|
|
112
|
+
/** @internal */ preactivatedSkillIds?: string[];
|
|
113
|
+
/** @internal */ coreToolNames: Set<string>;
|
|
114
|
+
/** @internal */ readonly skillProjectionState = new Map<string, string>();
|
|
115
|
+
/** @internal */ readonly skillProjectionCache: SkillProjectionCache = {};
|
|
116
|
+
/** @internal */ usageStats: UsageStats = { inputTokens: 0, outputTokens: 0, estimatedCost: 0 };
|
|
117
|
+
/** @internal */ readonly systemPrompt: string;
|
|
118
|
+
/** @internal */ contextWindowManager: ContextWindowManager;
|
|
119
|
+
/** @internal */ contextCompactedMessageCount = 0;
|
|
120
|
+
/** @internal */ contextCompactedAt: number | null = null;
|
|
121
|
+
/** @internal */ currentRequestId?: string;
|
|
122
|
+
/** @internal */ conflictGate = new ConflictGate();
|
|
123
|
+
/** @internal */ hasNoClient = false;
|
|
124
|
+
/** @internal */ headlessLock = false;
|
|
125
|
+
/** @internal */ taskRunId?: string;
|
|
126
|
+
/** @internal */ readonly queue = new MessageQueue();
|
|
127
|
+
/** @internal */ currentActiveSurfaceId?: string;
|
|
128
|
+
/** @internal */ currentPage?: string;
|
|
129
|
+
/** @internal */ channelCapabilities?: ChannelCapabilities;
|
|
130
|
+
/** @internal */ pendingSurfaceActions = new Map<string, { surfaceType: SurfaceType }>();
|
|
178
131
|
/** @internal */ lastSurfaceAction = new Map<string, { actionId: string; data?: Record<string, unknown> }>();
|
|
179
132
|
/** @internal */ surfaceState = new Map<string, { surfaceType: SurfaceType; data: SurfaceData }>();
|
|
180
|
-
/** @internal
|
|
181
|
-
|
|
182
|
-
/** @internal Surfaces created during the current agent loop turn, to be persisted with the message. */
|
|
183
|
-
currentTurnSurfaces: Array<{ surfaceId: string; surfaceType: SurfaceType; title?: string; data: SurfaceData; actions?: Array<{ id: string; label: string; style?: string }>; display?: string }> = [];
|
|
133
|
+
/** @internal */ surfaceUndoStacks = new Map<string, string[]>();
|
|
134
|
+
/** @internal */ currentTurnSurfaces: Array<{ surfaceId: string; surfaceType: SurfaceType; title?: string; data: SurfaceData; actions?: Array<{ id: string; label: string; style?: string }>; display?: string }> = [];
|
|
184
135
|
/** @internal */ onEscalateToComputerUse?: (task: string, sourceSessionId: string) => boolean;
|
|
185
|
-
/** @internal
|
|
186
|
-
|
|
187
|
-
/** @internal — exposed for session-workspace.ts module functions. */
|
|
188
|
-
workspaceTopLevelDirty = true;
|
|
136
|
+
/** @internal */ workspaceTopLevelContext: string | null = null;
|
|
137
|
+
/** @internal */ workspaceTopLevelDirty = true;
|
|
189
138
|
public readonly traceEmitter: TraceEmitter;
|
|
190
139
|
public memoryPolicy: SessionMemoryPolicy;
|
|
191
|
-
/**
|
|
192
|
-
private turnCount = 0;
|
|
193
|
-
|
|
194
|
-
/** Resolved assistant attachment drafts from the most recent exchange. */
|
|
140
|
+
/** @internal */ turnCount = 0;
|
|
195
141
|
public lastAssistantAttachments: AssistantAttachmentDraft[] = [];
|
|
196
|
-
/** Warnings from directive parsing/resolution for the most recent exchange. */
|
|
197
142
|
public lastAttachmentWarnings: string[] = [];
|
|
198
143
|
|
|
199
144
|
constructor(
|
|
@@ -211,54 +156,15 @@ export class Session {
|
|
|
211
156
|
this.provider = provider;
|
|
212
157
|
this.workingDir = workingDir;
|
|
213
158
|
this.sendToClient = sendToClient;
|
|
214
|
-
this.broadcastToAllClients = broadcastToAllClients;
|
|
215
159
|
this.memoryPolicy = memoryPolicy ? { ...memoryPolicy } : { ...DEFAULT_MEMORY_POLICY };
|
|
216
160
|
this.traceEmitter = new TraceEmitter(conversationId, sendToClient);
|
|
217
161
|
this.prompter = new PermissionPrompter(sendToClient);
|
|
218
162
|
this.secretPrompter = new SecretPrompter(sendToClient);
|
|
219
163
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
type: 'watch_started',
|
|
223
|
-
sessionId: conversationId,
|
|
224
|
-
watchId: session.watchId,
|
|
225
|
-
durationSeconds: session.durationSeconds,
|
|
226
|
-
intervalSeconds: session.intervalSeconds,
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
registerWatchCommentaryNotifier(conversationId, (_session: WatchSession) => {
|
|
231
|
-
const commentary = lastCommentaryBySession.get(conversationId);
|
|
232
|
-
if (commentary) {
|
|
233
|
-
lastCommentaryBySession.delete(conversationId);
|
|
234
|
-
this.sendToClient({
|
|
235
|
-
type: 'assistant_text_delta',
|
|
236
|
-
text: commentary,
|
|
237
|
-
sessionId: conversationId,
|
|
238
|
-
});
|
|
239
|
-
this.sendToClient({
|
|
240
|
-
type: 'message_complete',
|
|
241
|
-
sessionId: conversationId,
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
registerWatchCompletionNotifier(conversationId, (_session: WatchSession) => {
|
|
247
|
-
const summary = lastSummaryBySession.get(conversationId);
|
|
248
|
-
if (summary) {
|
|
249
|
-
lastSummaryBySession.delete(conversationId);
|
|
250
|
-
this.sendToClient({
|
|
251
|
-
type: 'assistant_text_delta',
|
|
252
|
-
text: summary,
|
|
253
|
-
sessionId: conversationId,
|
|
254
|
-
});
|
|
255
|
-
this.sendToClient({
|
|
256
|
-
type: 'message_complete',
|
|
257
|
-
sessionId: conversationId,
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
});
|
|
164
|
+
// Register watch/call notifiers (reads ctx properties lazily)
|
|
165
|
+
registerSessionNotifiers(conversationId, this);
|
|
261
166
|
|
|
167
|
+
// Tool infrastructure
|
|
262
168
|
this.executor = new ToolExecutor(this.prompter);
|
|
263
169
|
this.profiler = new ToolProfiler();
|
|
264
170
|
registerToolMetricsLoggingListener(this.eventBus);
|
|
@@ -284,24 +190,7 @@ export class Session {
|
|
|
284
190
|
);
|
|
285
191
|
|
|
286
192
|
const config = getConfig();
|
|
287
|
-
|
|
288
|
-
// dynamically projected skill tools on each agent turn. Also updates
|
|
289
|
-
// allowedToolNames so newly-activated skill tools aren't blocked by
|
|
290
|
-
// the executor's stale gate.
|
|
291
|
-
const resolveTools = toolDefs.length > 0
|
|
292
|
-
? (history: Message[]) => {
|
|
293
|
-
const projection = projectSkillTools(history, {
|
|
294
|
-
preactivatedSkillIds: this.preactivatedSkillIds,
|
|
295
|
-
previouslyActiveSkillIds: this.skillProjectionState,
|
|
296
|
-
});
|
|
297
|
-
const turnAllowed = new Set(this.coreToolNames);
|
|
298
|
-
for (const name of projection.allowedToolNames) {
|
|
299
|
-
turnAllowed.add(name);
|
|
300
|
-
}
|
|
301
|
-
this.allowedToolNames = turnAllowed;
|
|
302
|
-
return [...toolDefs, ...projection.toolDefinitions];
|
|
303
|
-
}
|
|
304
|
-
: undefined;
|
|
193
|
+
const resolveTools = createResolveToolsCallback(toolDefs, this);
|
|
305
194
|
|
|
306
195
|
this.agentLoop = new AgentLoop(
|
|
307
196
|
provider,
|
|
@@ -323,51 +212,10 @@ export class Session {
|
|
|
323
212
|
});
|
|
324
213
|
}
|
|
325
214
|
|
|
326
|
-
|
|
327
|
-
const dbMessages = conversationStore.getMessages(this.conversationId);
|
|
215
|
+
// ── Lifecycle ────────────────────────────────────────────────────
|
|
328
216
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
this.contextCompactedMessageCount = Math.max(
|
|
332
|
-
0,
|
|
333
|
-
Math.min(conv?.contextCompactedMessageCount ?? 0, dbMessages.length),
|
|
334
|
-
);
|
|
335
|
-
this.contextCompactedAt = conv?.contextCompactedAt ?? null;
|
|
336
|
-
|
|
337
|
-
const parsedMessages: Message[] = dbMessages
|
|
338
|
-
.slice(this.contextCompactedMessageCount)
|
|
339
|
-
.map((m) => {
|
|
340
|
-
const role = m.role as 'user' | 'assistant';
|
|
341
|
-
let content: ContentBlock[];
|
|
342
|
-
try {
|
|
343
|
-
const parsed = JSON.parse(m.content);
|
|
344
|
-
content = Array.isArray(parsed) ? parsed : [{ type: 'text', text: m.content }];
|
|
345
|
-
} catch {
|
|
346
|
-
log.warn({ conversationId: this.conversationId, messageId: m.id }, 'Invalid JSON in persisted message content, replacing with safe text block');
|
|
347
|
-
content = [{ type: 'text', text: m.content }];
|
|
348
|
-
}
|
|
349
|
-
return { role, content };
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
const { messages: repairedMessages, stats } = repairHistory(parsedMessages);
|
|
353
|
-
if (stats.assistantToolResultsMigrated > 0 || stats.missingToolResultsInserted > 0 || stats.orphanToolResultsDowngraded > 0 || stats.consecutiveSameRoleMerged > 0) {
|
|
354
|
-
log.warn({ conversationId: this.conversationId, phase: 'load', ...stats }, 'Repaired persisted history');
|
|
355
|
-
}
|
|
356
|
-
this.messages = repairedMessages;
|
|
357
|
-
|
|
358
|
-
if (contextSummary) {
|
|
359
|
-
this.messages.unshift(createContextSummaryMessage(contextSummary));
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (conv) {
|
|
363
|
-
this.usageStats = {
|
|
364
|
-
inputTokens: conv.totalInputTokens,
|
|
365
|
-
outputTokens: conv.totalOutputTokens,
|
|
366
|
-
estimatedCost: conv.totalEstimatedCost,
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
log.info({ conversationId: this.conversationId, count: this.messages.length }, 'Loaded messages from DB');
|
|
217
|
+
async loadFromDb(): Promise<void> {
|
|
218
|
+
return loadFromDbImpl(this);
|
|
371
219
|
}
|
|
372
220
|
|
|
373
221
|
updateClient(sendToClient: (msg: ServerMessage) => void, hasNoClient = false): void {
|
|
@@ -382,10 +230,6 @@ export class Session {
|
|
|
382
230
|
this.sandboxOverride = enabled;
|
|
383
231
|
}
|
|
384
232
|
|
|
385
|
-
/**
|
|
386
|
-
* Set a callback for when a text_qa session escalates to computer use
|
|
387
|
-
* via the `computer_use_request_control` tool.
|
|
388
|
-
*/
|
|
389
233
|
setEscalationHandler(handler: (task: string, sourceSessionId: string) => boolean): void {
|
|
390
234
|
this.onEscalateToComputerUse = handler;
|
|
391
235
|
}
|
|
@@ -394,55 +238,15 @@ export class Session {
|
|
|
394
238
|
return this.onEscalateToComputerUse !== undefined;
|
|
395
239
|
}
|
|
396
240
|
|
|
397
|
-
/**
|
|
398
|
-
* Redirect the user to the secure credential prompt after an ingress block.
|
|
399
|
-
* If the user enters a value, it is stored in the vault (or injected as
|
|
400
|
-
* transient) so the credential is available for later tool use.
|
|
401
|
-
*
|
|
402
|
-
* @param onComplete Called after the prompt resolves (success, cancel, or
|
|
403
|
-
* timeout) so the caller can clean up ephemeral resources like placeholder
|
|
404
|
-
* conversations.
|
|
405
|
-
*/
|
|
406
|
-
redirectToSecurePrompt(detectedTypes: string[], onComplete?: () => void): void {
|
|
407
|
-
const service = 'detected';
|
|
408
|
-
const field = detectedTypes.join(',');
|
|
409
|
-
this.secretPrompter.prompt(
|
|
410
|
-
service, field,
|
|
411
|
-
'Secure Credential Entry',
|
|
412
|
-
'Your message contained a secret. Please enter it here instead — it will be stored securely and never sent to the AI.',
|
|
413
|
-
undefined, this.conversationId,
|
|
414
|
-
).then(async (result) => {
|
|
415
|
-
if (!result.value) return; // user cancelled or timed out
|
|
416
|
-
|
|
417
|
-
const { setSecureKey } = await import('../security/secure-keys.js');
|
|
418
|
-
const { upsertCredentialMetadata } = await import('../tools/credentials/metadata-store.js');
|
|
419
|
-
|
|
420
|
-
if (result.delivery === 'transient_send') {
|
|
421
|
-
const { credentialBroker } = await import('../tools/credentials/broker.js');
|
|
422
|
-
credentialBroker.injectTransient(service, field, result.value);
|
|
423
|
-
try { upsertCredentialMetadata(service, field, {}); } catch {}
|
|
424
|
-
log.info({ service, field, delivery: 'transient_send' }, 'Ingress redirect: transient credential injected');
|
|
425
|
-
} else {
|
|
426
|
-
const key = `credential:${service}:${field}`;
|
|
427
|
-
const stored = setSecureKey(key, result.value);
|
|
428
|
-
if (stored) {
|
|
429
|
-
try { upsertCredentialMetadata(service, field, {}); } catch {}
|
|
430
|
-
log.info({ service, field }, 'Ingress redirect: credential stored');
|
|
431
|
-
} else {
|
|
432
|
-
log.warn({ service, field }, 'Ingress redirect: secure storage write failed');
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}).catch(() => { /* prompt timeout or cancel is fine */ }).finally(() => {
|
|
436
|
-
onComplete?.();
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
|
|
440
241
|
isProcessing(): boolean {
|
|
441
242
|
return this.processing;
|
|
442
243
|
}
|
|
443
244
|
|
|
444
245
|
markStale(): void {
|
|
445
246
|
this.stale = true;
|
|
247
|
+
// Invalidate the cached skill catalog so the next projection picks up
|
|
248
|
+
// filesystem changes (e.g. a skill created during this run).
|
|
249
|
+
this.skillProjectionCache.catalog = undefined;
|
|
446
250
|
}
|
|
447
251
|
|
|
448
252
|
isStale(): boolean {
|
|
@@ -450,57 +254,19 @@ export class Session {
|
|
|
450
254
|
}
|
|
451
255
|
|
|
452
256
|
abort(): void {
|
|
453
|
-
|
|
454
|
-
log.info({ conversationId: this.conversationId }, 'Aborting in-flight processing');
|
|
455
|
-
this.abortController?.abort();
|
|
456
|
-
this.prompter.dispose();
|
|
457
|
-
this.secretPrompter.dispose();
|
|
458
|
-
this.pendingSurfaceActions.clear();
|
|
459
|
-
this.surfaceState.clear();
|
|
460
|
-
unregisterWatchStartNotifier(this.conversationId);
|
|
461
|
-
unregisterWatchCommentaryNotifier(this.conversationId);
|
|
462
|
-
unregisterWatchCompletionNotifier(this.conversationId);
|
|
463
|
-
pruneWatchSessions(this.conversationId);
|
|
464
|
-
|
|
465
|
-
// Clear queued messages and notify each caller with a session-scoped
|
|
466
|
-
// cancel event so other sessions do not receive cross-thread errors.
|
|
467
|
-
for (const queued of this.queue) {
|
|
468
|
-
queued.onEvent({ type: 'generation_cancelled', sessionId: this.conversationId });
|
|
469
|
-
}
|
|
470
|
-
this.queue.clear();
|
|
471
|
-
}
|
|
257
|
+
abortSession(this);
|
|
472
258
|
}
|
|
473
259
|
|
|
474
|
-
/** Abort and permanently tear down this session. Call when removing from the sessions map. */
|
|
475
260
|
dispose(): void {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
});
|
|
479
|
-
this.abort();
|
|
480
|
-
unregisterSessionSender(this.conversationId);
|
|
481
|
-
resetSkillToolProjection(this.skillProjectionState);
|
|
482
|
-
this.eventBus.dispose();
|
|
261
|
+
disposeSession(this);
|
|
262
|
+
}
|
|
483
263
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
this.
|
|
488
|
-
this.profiler.clear();
|
|
489
|
-
this.surfaceUndoStacks.clear();
|
|
490
|
-
this.currentTurnSurfaces = [];
|
|
491
|
-
this.pendingSurfaceActions.clear();
|
|
492
|
-
this.surfaceState.clear();
|
|
493
|
-
this.lastSurfaceAction.clear();
|
|
494
|
-
this.workspaceTopLevelContext = null;
|
|
264
|
+
// ── Messaging ────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
redirectToSecurePrompt(detectedTypes: string[], onComplete?: () => void): void {
|
|
267
|
+
redirectToSecurePromptImpl(this.conversationId, this.secretPrompter, detectedTypes, onComplete);
|
|
495
268
|
}
|
|
496
269
|
|
|
497
|
-
/**
|
|
498
|
-
* Enqueue a message if the session is busy, or indicate it should be
|
|
499
|
-
* processed immediately. Returns `{ queued: true }` if the message was
|
|
500
|
-
* added to the queue, `{ queued: false, rejected: true }` if the queue
|
|
501
|
-
* is full, or `{ queued: false }` if the caller should invoke
|
|
502
|
-
* `processMessage` directly.
|
|
503
|
-
*/
|
|
504
270
|
enqueueMessage(
|
|
505
271
|
content: string,
|
|
506
272
|
attachments: UserMessageAttachment[],
|
|
@@ -508,42 +274,23 @@ export class Session {
|
|
|
508
274
|
requestId: string,
|
|
509
275
|
activeSurfaceId?: string,
|
|
510
276
|
currentPage?: string,
|
|
277
|
+
metadata?: Record<string, unknown>,
|
|
511
278
|
): { queued: boolean; rejected?: boolean; requestId: string } {
|
|
512
|
-
|
|
513
|
-
return { queued: false, requestId };
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
const pushed = this.queue.push({ content, attachments, requestId, onEvent, activeSurfaceId, currentPage });
|
|
517
|
-
if (!pushed) {
|
|
518
|
-
return { queued: false, rejected: true, requestId };
|
|
519
|
-
}
|
|
520
|
-
return { queued: true, requestId };
|
|
279
|
+
return enqueueMessageImpl(this, content, attachments, onEvent, requestId, activeSurfaceId, currentPage, metadata);
|
|
521
280
|
}
|
|
522
281
|
|
|
523
282
|
getQueueDepth(): number {
|
|
524
283
|
return this.queue.length;
|
|
525
284
|
}
|
|
526
285
|
|
|
527
|
-
/**
|
|
528
|
-
* Returns true if there are messages waiting in the queue.
|
|
529
|
-
*/
|
|
530
286
|
hasQueuedMessages(): boolean {
|
|
531
287
|
return !this.queue.isEmpty;
|
|
532
288
|
}
|
|
533
289
|
|
|
534
|
-
/**
|
|
535
|
-
* Remove a queued message by requestId. Returns true if the message was found
|
|
536
|
-
* and removed, false if the requestId was not in the queue.
|
|
537
|
-
*/
|
|
538
290
|
removeQueuedMessage(requestId: string): boolean {
|
|
539
291
|
return this.queue.removeByRequestId(requestId) !== undefined;
|
|
540
292
|
}
|
|
541
293
|
|
|
542
|
-
/**
|
|
543
|
-
* Returns true if the session is currently processing and there are queued
|
|
544
|
-
* messages waiting. This is the predicate used to decide whether to yield
|
|
545
|
-
* at a turn boundary (checkpoint handoff).
|
|
546
|
-
*/
|
|
547
294
|
canHandoffAtCheckpoint(): boolean {
|
|
548
295
|
return this.processing && this.hasQueuedMessages();
|
|
549
296
|
}
|
|
@@ -569,900 +316,32 @@ export class Session {
|
|
|
569
316
|
this.secretPrompter.resolveSecret(requestId, value, delivery);
|
|
570
317
|
}
|
|
571
318
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
* IPC-only desktop sessions can leave this unset and use a local scope.
|
|
575
|
-
*/
|
|
576
|
-
setAssistantId(assistantId: string): void {
|
|
577
|
-
this.assistantId = assistantId;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
setChannelCapabilities(caps: ChannelCapabilities): void {
|
|
581
|
-
this.channelCapabilities = caps;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
private async approveHostAttachmentReadImpl(filePath: string): Promise<boolean> {
|
|
585
|
-
return approveHostAttachmentRead(filePath, this.workingDir, this.prompter, this.conversationId, this.hasNoClient);
|
|
319
|
+
setChannelCapabilities(caps: ChannelCapabilities | null): void {
|
|
320
|
+
this.channelCapabilities = caps ?? undefined;
|
|
586
321
|
}
|
|
587
322
|
|
|
588
|
-
/**
|
|
589
|
-
* Persist a user message and mark the session as processing.
|
|
590
|
-
* Returns the messageId immediately without running the agent loop.
|
|
591
|
-
* After calling this, call `runAgentLoop` to continue processing.
|
|
592
|
-
*/
|
|
593
323
|
persistUserMessage(
|
|
594
324
|
content: string,
|
|
595
325
|
attachments: UserMessageAttachment[],
|
|
596
326
|
requestId?: string,
|
|
327
|
+
metadata?: Record<string, unknown>,
|
|
597
328
|
): string {
|
|
598
|
-
|
|
599
|
-
throw new Error('Session is already processing a message');
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
if (!content.trim() && attachments.length === 0) {
|
|
603
|
-
throw new Error('Message content or attachments are required');
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
const reqId = requestId ?? uuid();
|
|
607
|
-
this.currentRequestId = reqId;
|
|
608
|
-
this.processing = true;
|
|
609
|
-
this.abortController = new AbortController();
|
|
610
|
-
|
|
611
|
-
const userMessage = createUserMessage(content, attachments.map((attachment) => ({
|
|
612
|
-
id: attachment.id,
|
|
613
|
-
filename: attachment.filename,
|
|
614
|
-
mimeType: attachment.mimeType,
|
|
615
|
-
data: attachment.data,
|
|
616
|
-
extractedText: attachment.extractedText,
|
|
617
|
-
})));
|
|
618
|
-
this.messages.push(userMessage);
|
|
619
|
-
|
|
620
|
-
try {
|
|
621
|
-
const persistedUserMessage = conversationStore.addMessage(
|
|
622
|
-
this.conversationId,
|
|
623
|
-
'user',
|
|
624
|
-
JSON.stringify(userMessage.content),
|
|
625
|
-
);
|
|
626
|
-
|
|
627
|
-
if (!persistedUserMessage.id) {
|
|
628
|
-
throw new Error('Failed to persist user message');
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
return persistedUserMessage.id;
|
|
632
|
-
} catch (err) {
|
|
633
|
-
this.messages.pop();
|
|
634
|
-
this.processing = false;
|
|
635
|
-
this.abortController = null;
|
|
636
|
-
this.currentRequestId = undefined;
|
|
637
|
-
throw err;
|
|
638
|
-
}
|
|
329
|
+
return persistUserMessageImpl(this, content, attachments, requestId, metadata);
|
|
639
330
|
}
|
|
640
331
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
* `persistUserMessage`. Clears the `processing` flag when done.
|
|
644
|
-
*
|
|
645
|
-
* @param options.skipPreMessageRollback - When true, the pre-message hook
|
|
646
|
-
* blocked path will NOT delete the user message from in-memory history or
|
|
647
|
-
* the DB. Used by `regenerate()` where the user message is the original
|
|
648
|
-
* (not freshly persisted) and must be preserved.
|
|
649
|
-
*/
|
|
332
|
+
// ── Agent Loop ───────────────────────────────────────────────────
|
|
333
|
+
|
|
650
334
|
async runAgentLoop(
|
|
651
335
|
content: string,
|
|
652
336
|
userMessageId: string,
|
|
653
337
|
onEvent: (msg: ServerMessage) => void,
|
|
654
338
|
options?: { skipPreMessageRollback?: boolean },
|
|
655
339
|
): Promise<void> {
|
|
656
|
-
|
|
657
|
-
throw new Error('runAgentLoop called without prior persistUserMessage');
|
|
658
|
-
}
|
|
659
|
-
const abortController = this.abortController;
|
|
660
|
-
const reqId = this.currentRequestId ?? uuid();
|
|
661
|
-
const rlog = log.child({ conversationId: this.conversationId, requestId: reqId });
|
|
662
|
-
let yieldedForHandoff = false;
|
|
663
|
-
|
|
664
|
-
// Reset attachment state so a failed exchange never retains stale data
|
|
665
|
-
// from a prior successful run.
|
|
666
|
-
this.lastAssistantAttachments = [];
|
|
667
|
-
this.lastAttachmentWarnings = [];
|
|
668
|
-
|
|
669
|
-
// Ensure the workspace git repo is initialized before any tools run.
|
|
670
|
-
// This must happen before the first turn so the initial commit captures
|
|
671
|
-
// the pre-turn workspace state; otherwise ensureInitialized() would be
|
|
672
|
-
// triggered lazily by getStatus() inside commitTurnChanges(), absorbing
|
|
673
|
-
// the first turn's file changes into the initial commit.
|
|
674
|
-
try {
|
|
675
|
-
const gitService = getWorkspaceGitService(this.workingDir);
|
|
676
|
-
await gitService.ensureInitialized();
|
|
677
|
-
} catch (err) {
|
|
678
|
-
rlog.warn({ err }, 'Failed to initialize workspace git repo (non-fatal)');
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
this.profiler.startRequest();
|
|
682
|
-
|
|
683
|
-
// Tracks whether the agent loop started — once true, we guarantee a
|
|
684
|
-
// turn-boundary commit even if post-processing throws.
|
|
685
|
-
let turnStarted = false;
|
|
686
|
-
|
|
687
|
-
try {
|
|
688
|
-
const preMessageResult = await getHookManager().trigger('pre-message', {
|
|
689
|
-
sessionId: this.conversationId,
|
|
690
|
-
messagePreview: content.slice(0, 200),
|
|
691
|
-
});
|
|
692
|
-
|
|
693
|
-
if (preMessageResult.blocked) {
|
|
694
|
-
if (!options?.skipPreMessageRollback) {
|
|
695
|
-
// Roll back the user message from both in-memory history and the DB.
|
|
696
|
-
// We use deleteMessageById (not deleteLastExchange) because it NULLs
|
|
697
|
-
// nullable FK references (message_runs, channel_inbound_events) before
|
|
698
|
-
// deleting the message row, so the run record survives.
|
|
699
|
-
this.messages.pop();
|
|
700
|
-
conversationStore.deleteMessageById(userMessageId);
|
|
701
|
-
}
|
|
702
|
-
onEvent({ type: 'error', message: `Message blocked by hook "${preMessageResult.blockedBy}"` });
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
const isFirstMessage = this.messages.length === 1;
|
|
707
|
-
|
|
708
|
-
const compacted = await this.contextWindowManager.maybeCompact(
|
|
709
|
-
this.messages,
|
|
710
|
-
abortController.signal,
|
|
711
|
-
{ lastCompactedAt: this.contextCompactedAt ?? undefined },
|
|
712
|
-
);
|
|
713
|
-
if (compacted.compacted) {
|
|
714
|
-
this.messages = compacted.messages;
|
|
715
|
-
this.contextCompactedMessageCount += compacted.compactedPersistedMessages;
|
|
716
|
-
this.contextCompactedAt = Date.now();
|
|
717
|
-
conversationStore.updateConversationContextWindow(
|
|
718
|
-
this.conversationId,
|
|
719
|
-
compacted.summaryText,
|
|
720
|
-
this.contextCompactedMessageCount,
|
|
721
|
-
);
|
|
722
|
-
onEvent({
|
|
723
|
-
type: 'context_compacted',
|
|
724
|
-
previousEstimatedInputTokens: compacted.previousEstimatedInputTokens,
|
|
725
|
-
estimatedInputTokens: compacted.estimatedInputTokens,
|
|
726
|
-
maxInputTokens: compacted.maxInputTokens,
|
|
727
|
-
thresholdTokens: compacted.thresholdTokens,
|
|
728
|
-
compactedMessages: compacted.compactedMessages,
|
|
729
|
-
summaryCalls: compacted.summaryCalls,
|
|
730
|
-
summaryInputTokens: compacted.summaryInputTokens,
|
|
731
|
-
summaryOutputTokens: compacted.summaryOutputTokens,
|
|
732
|
-
summaryModel: compacted.summaryModel,
|
|
733
|
-
});
|
|
734
|
-
this.recordUsage(
|
|
735
|
-
compacted.summaryInputTokens,
|
|
736
|
-
compacted.summaryOutputTokens,
|
|
737
|
-
compacted.summaryModel,
|
|
738
|
-
onEvent,
|
|
739
|
-
'context_compactor',
|
|
740
|
-
reqId,
|
|
741
|
-
);
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// Run agent loop
|
|
745
|
-
let firstAssistantText = '';
|
|
746
|
-
let exchangeInputTokens = 0;
|
|
747
|
-
let exchangeOutputTokens = 0;
|
|
748
|
-
let model = '';
|
|
749
|
-
let runMessages = this.messages;
|
|
750
|
-
const pendingToolResults = new Map<string, { content: string; isError: boolean; contentBlocks?: ContentBlock[] }>();
|
|
751
|
-
const persistedToolUseIds = new Set<string>();
|
|
752
|
-
const accumulatedDirectives: DirectiveRequest[] = [];
|
|
753
|
-
const accumulatedToolContentBlocks: ContentBlock[] = [];
|
|
754
|
-
const directiveWarnings: string[] = [];
|
|
755
|
-
let pendingDirectiveDisplayBuffer = '';
|
|
756
|
-
let lastAssistantMessageId: string | undefined;
|
|
757
|
-
let providerErrorUserMessage: string | null = null;
|
|
758
|
-
const memoryResult = await prepareMemoryContext(
|
|
759
|
-
{
|
|
760
|
-
conversationId: this.conversationId,
|
|
761
|
-
messages: this.messages,
|
|
762
|
-
systemPrompt: this.systemPrompt,
|
|
763
|
-
provider: this.provider,
|
|
764
|
-
conflictGate: this.conflictGate,
|
|
765
|
-
scopeId: this.memoryPolicy.scopeId,
|
|
766
|
-
includeDefaultFallback: this.memoryPolicy.includeDefaultFallback,
|
|
767
|
-
},
|
|
768
|
-
content,
|
|
769
|
-
userMessageId,
|
|
770
|
-
abortController.signal,
|
|
771
|
-
onEvent,
|
|
772
|
-
);
|
|
773
|
-
|
|
774
|
-
if (memoryResult.conflictClarification) {
|
|
775
|
-
const assistantMessage = createAssistantMessage(memoryResult.conflictClarification);
|
|
776
|
-
conversationStore.addMessage(
|
|
777
|
-
this.conversationId,
|
|
778
|
-
'assistant',
|
|
779
|
-
JSON.stringify(assistantMessage.content),
|
|
780
|
-
);
|
|
781
|
-
this.messages.push(assistantMessage);
|
|
782
|
-
onEvent({
|
|
783
|
-
type: 'assistant_text_delta',
|
|
784
|
-
text: memoryResult.conflictClarification,
|
|
785
|
-
sessionId: this.conversationId,
|
|
786
|
-
});
|
|
787
|
-
this.traceEmitter.emit('message_complete', 'Conflict clarification requested (relevant)', {
|
|
788
|
-
requestId: reqId,
|
|
789
|
-
status: 'info',
|
|
790
|
-
attributes: { conflictGate: 'relevant' },
|
|
791
|
-
});
|
|
792
|
-
onEvent({ type: 'message_complete', sessionId: this.conversationId });
|
|
793
|
-
return;
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
const { recall, dynamicProfile, softConflictInstruction, recallInjectionStrategy } = memoryResult;
|
|
797
|
-
runMessages = memoryResult.runMessages;
|
|
798
|
-
|
|
799
|
-
// Inject soft-conflict instruction and active surface context
|
|
800
|
-
let activeSurface: ActiveSurfaceContext | null = null;
|
|
801
|
-
if (this.currentActiveSurfaceId) {
|
|
802
|
-
const stored = this.surfaceState.get(this.currentActiveSurfaceId);
|
|
803
|
-
if (stored && stored.surfaceType === 'dynamic_page') {
|
|
804
|
-
const data = stored.data as DynamicPageSurfaceData;
|
|
805
|
-
activeSurface = {
|
|
806
|
-
surfaceId: this.currentActiveSurfaceId,
|
|
807
|
-
html: data.html,
|
|
808
|
-
currentPage: this.currentPage,
|
|
809
|
-
};
|
|
810
|
-
// Enrich with app context when the surface is backed by a persisted app
|
|
811
|
-
if (data.appId) {
|
|
812
|
-
const app = getApp(data.appId);
|
|
813
|
-
if (app) {
|
|
814
|
-
activeSurface.appId = app.id;
|
|
815
|
-
activeSurface.appName = app.name;
|
|
816
|
-
activeSurface.appSchemaJson = app.schemaJson;
|
|
817
|
-
activeSurface.appFiles = listAppFiles(app.id);
|
|
818
|
-
if (app.pages && Object.keys(app.pages).length > 0) {
|
|
819
|
-
activeSurface.appPages = app.pages;
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
// Refresh workspace top-level context before injection
|
|
826
|
-
this.refreshWorkspaceTopLevelContextIfNeeded();
|
|
827
|
-
|
|
828
|
-
runMessages = applyRuntimeInjections(runMessages, {
|
|
829
|
-
softConflictInstruction,
|
|
830
|
-
activeSurface,
|
|
831
|
-
workspaceTopLevelContext: this.workspaceTopLevelContext,
|
|
832
|
-
channelCapabilities: this.channelCapabilities ?? null,
|
|
833
|
-
});
|
|
834
|
-
|
|
835
|
-
// Pre-run repair: fix any message ordering issues before sending to provider.
|
|
836
|
-
// Keep a reference to the original (un-repaired) messages so we can
|
|
837
|
-
// reconstruct this.messages after the agent loop without leaking synthetic
|
|
838
|
-
// tool_result blocks that repair may inject. Leaking those blocks would
|
|
839
|
-
// break undo semantics (isUndoableUserMessage skips user messages
|
|
840
|
-
// containing only tool_result blocks).
|
|
841
|
-
let preRepairMessages = runMessages;
|
|
842
|
-
const preRunRepair = repairHistory(runMessages);
|
|
843
|
-
if (preRunRepair.stats.assistantToolResultsMigrated > 0 || preRunRepair.stats.missingToolResultsInserted > 0 || preRunRepair.stats.orphanToolResultsDowngraded > 0 || preRunRepair.stats.consecutiveSameRoleMerged > 0) {
|
|
844
|
-
rlog.warn({ phase: 'pre_run', ...preRunRepair.stats }, 'Repaired runtime history before provider call');
|
|
845
|
-
runMessages = preRunRepair.messages;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
let orderingErrorDetected = false;
|
|
849
|
-
let deferredOrderingError: string | null = null;
|
|
850
|
-
let contextTooLargeDetected = false;
|
|
851
|
-
let preRunHistoryLength = runMessages.length;
|
|
852
|
-
|
|
853
|
-
// Track whether llm_call_started has been emitted for the current provider turn.
|
|
854
|
-
// Reset on each usage event (which marks the end of a provider call).
|
|
855
|
-
let llmCallStartedEmitted = false;
|
|
856
|
-
|
|
857
|
-
// Map tool_use_id → toolName so tool_result processing can identify the originating tool.
|
|
858
|
-
const toolUseIdToName = new Map<string, string>();
|
|
859
|
-
|
|
860
|
-
// Track tool names used in the current agent turn for checkpoint decisions.
|
|
861
|
-
let currentTurnToolNames: string[] = [];
|
|
862
|
-
|
|
863
|
-
const buildEventHandler = () => (event: import('../agent/loop.js').AgentEvent) => {
|
|
864
|
-
// Emit llm_call_started once per provider call. Called on first streaming
|
|
865
|
-
// token (text or thinking) or, for tool-only turns, right before the
|
|
866
|
-
// usage event so every llm_call_finished has a matching start.
|
|
867
|
-
const emitLlmCallStartedIfNeeded = () => {
|
|
868
|
-
if (llmCallStartedEmitted) return;
|
|
869
|
-
llmCallStartedEmitted = true;
|
|
870
|
-
this.traceEmitter.emit('llm_call_started', `LLM call to ${this.provider.name}`, {
|
|
871
|
-
requestId: reqId,
|
|
872
|
-
status: 'info',
|
|
873
|
-
attributes: { provider: this.provider.name, model: model || 'unknown' },
|
|
874
|
-
});
|
|
875
|
-
};
|
|
876
|
-
|
|
877
|
-
switch (event.type) {
|
|
878
|
-
case 'text_delta': {
|
|
879
|
-
emitLlmCallStartedIfNeeded();
|
|
880
|
-
pendingDirectiveDisplayBuffer += event.text;
|
|
881
|
-
const drained = drainDirectiveDisplayBuffer(pendingDirectiveDisplayBuffer);
|
|
882
|
-
pendingDirectiveDisplayBuffer = drained.bufferedRemainder;
|
|
883
|
-
if (drained.emitText.length > 0) {
|
|
884
|
-
onEvent({ type: 'assistant_text_delta', text: drained.emitText, sessionId: this.conversationId });
|
|
885
|
-
if (isFirstMessage) firstAssistantText += drained.emitText;
|
|
886
|
-
}
|
|
887
|
-
break;
|
|
888
|
-
}
|
|
889
|
-
case 'thinking_delta':
|
|
890
|
-
// Thinking content itself is NOT included in traces to avoid leaking
|
|
891
|
-
// extended-thinking data.
|
|
892
|
-
emitLlmCallStartedIfNeeded();
|
|
893
|
-
onEvent({ type: 'assistant_thinking_delta', thinking: event.thinking });
|
|
894
|
-
break;
|
|
895
|
-
case 'tool_use':
|
|
896
|
-
toolUseIdToName.set(event.id, event.name);
|
|
897
|
-
currentTurnToolNames.push(event.name);
|
|
898
|
-
onEvent({ type: 'tool_use_start', toolName: event.name, input: event.input, sessionId: this.conversationId });
|
|
899
|
-
break;
|
|
900
|
-
case 'tool_output_chunk':
|
|
901
|
-
onEvent({ type: 'tool_output_chunk', chunk: event.chunk });
|
|
902
|
-
break;
|
|
903
|
-
case 'input_json_delta':
|
|
904
|
-
onEvent({ type: 'tool_input_delta', toolName: event.toolName, content: event.accumulatedJson, sessionId: this.conversationId });
|
|
905
|
-
break;
|
|
906
|
-
case 'tool_result': {
|
|
907
|
-
const imageBlock = event.contentBlocks?.find((b): b is ImageContent => b.type === 'image');
|
|
908
|
-
onEvent({ type: 'tool_result', toolName: '', result: event.content, isError: event.isError, diff: event.diff, status: event.status, sessionId: this.conversationId, imageData: imageBlock?.source.data });
|
|
909
|
-
pendingToolResults.set(event.toolUseId, { content: event.content, isError: event.isError, contentBlocks: event.contentBlocks });
|
|
910
|
-
// Mark workspace context dirty for mutation tools.
|
|
911
|
-
// file_write and bash are always dirty regardless of isError —
|
|
912
|
-
// file_write may physically write before a post-write error, and
|
|
913
|
-
// bash commands can modify the filesystem even when exiting
|
|
914
|
-
// non-zero (e.g. `mkdir foo && false`, `npm install` with audit
|
|
915
|
-
// warnings, compound commands where early parts succeed).
|
|
916
|
-
// file_edit is only dirty on success — a failed edit provably
|
|
917
|
-
// never touches the filesystem.
|
|
918
|
-
{
|
|
919
|
-
const toolName = toolUseIdToName.get(event.toolUseId);
|
|
920
|
-
if (toolName === 'file_write' || toolName === 'bash') {
|
|
921
|
-
this.markWorkspaceTopLevelDirty();
|
|
922
|
-
} else if (toolName === 'file_edit' && !event.isError) {
|
|
923
|
-
this.markWorkspaceTopLevelDirty();
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
// Collect image/file content blocks for assistant attachment conversion
|
|
927
|
-
if (event.contentBlocks) {
|
|
928
|
-
for (const cb of event.contentBlocks) {
|
|
929
|
-
if (cb.type === 'image' || cb.type === 'file') {
|
|
930
|
-
accumulatedToolContentBlocks.push(cb);
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
break;
|
|
935
|
-
}
|
|
936
|
-
case 'error':
|
|
937
|
-
if (isProviderOrderingError(event.error.message)) {
|
|
938
|
-
orderingErrorDetected = true;
|
|
939
|
-
// Defer the error event — only forward if retry also fails
|
|
940
|
-
deferredOrderingError = event.error.message;
|
|
941
|
-
} else if (isContextTooLarge(event.error.message)) {
|
|
942
|
-
contextTooLargeDetected = true;
|
|
943
|
-
// Defer — attempt compaction + retry before surfacing to user
|
|
944
|
-
} else {
|
|
945
|
-
const classified = classifySessionError(event.error, { phase: 'agent_loop' });
|
|
946
|
-
onEvent(buildSessionErrorMessage(this.conversationId, classified));
|
|
947
|
-
providerErrorUserMessage = classified.userMessage;
|
|
948
|
-
}
|
|
949
|
-
break;
|
|
950
|
-
case 'message_complete': {
|
|
951
|
-
if (pendingDirectiveDisplayBuffer.length > 0) {
|
|
952
|
-
onEvent({
|
|
953
|
-
type: 'assistant_text_delta',
|
|
954
|
-
text: pendingDirectiveDisplayBuffer,
|
|
955
|
-
sessionId: this.conversationId,
|
|
956
|
-
});
|
|
957
|
-
if (isFirstMessage) firstAssistantText += pendingDirectiveDisplayBuffer;
|
|
958
|
-
pendingDirectiveDisplayBuffer = '';
|
|
959
|
-
}
|
|
960
|
-
// Save pending tool results as a user message before the next assistant message.
|
|
961
|
-
// tool_result blocks belong in user messages per the Anthropic API spec.
|
|
962
|
-
if (pendingToolResults.size > 0) {
|
|
963
|
-
const toolResultBlocks = Array.from(pendingToolResults.entries()).map(
|
|
964
|
-
([toolUseId, result]) => ({
|
|
965
|
-
type: 'tool_result',
|
|
966
|
-
tool_use_id: toolUseId,
|
|
967
|
-
content: result.content,
|
|
968
|
-
is_error: result.isError,
|
|
969
|
-
...(result.contentBlocks ? { contentBlocks: result.contentBlocks } : {}),
|
|
970
|
-
}),
|
|
971
|
-
);
|
|
972
|
-
conversationStore.addMessage(
|
|
973
|
-
this.conversationId,
|
|
974
|
-
'user',
|
|
975
|
-
JSON.stringify(toolResultBlocks),
|
|
976
|
-
);
|
|
977
|
-
for (const id of pendingToolResults.keys()) {
|
|
978
|
-
persistedToolUseIds.add(id);
|
|
979
|
-
}
|
|
980
|
-
pendingToolResults.clear();
|
|
981
|
-
}
|
|
982
|
-
// Parse and strip attachment directives from assistant text
|
|
983
|
-
const { cleanedContent, directives: msgDirectives, warnings: msgWarnings } =
|
|
984
|
-
cleanAssistantContent(event.message.content);
|
|
985
|
-
accumulatedDirectives.push(...msgDirectives);
|
|
986
|
-
directiveWarnings.push(...msgWarnings);
|
|
987
|
-
if (msgDirectives.length > 0) {
|
|
988
|
-
rlog.info(
|
|
989
|
-
{ parsedDirectives: msgDirectives.map(d => ({ source: d.source, path: d.path, mimeType: d.mimeType })), totalAccumulated: accumulatedDirectives.length },
|
|
990
|
-
'Parsed attachment directives from assistant message',
|
|
991
|
-
);
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
// Add surface blocks to content for persistence
|
|
995
|
-
const contentWithSurfaces: ContentBlock[] = [...cleanedContent as ContentBlock[]];
|
|
996
|
-
for (const surface of this.currentTurnSurfaces) {
|
|
997
|
-
contentWithSurfaces.push({
|
|
998
|
-
type: 'ui_surface',
|
|
999
|
-
surfaceId: surface.surfaceId,
|
|
1000
|
-
surfaceType: surface.surfaceType,
|
|
1001
|
-
title: surface.title,
|
|
1002
|
-
data: surface.data,
|
|
1003
|
-
actions: surface.actions,
|
|
1004
|
-
display: surface.display,
|
|
1005
|
-
} as unknown as ContentBlock);
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
// Save assistant message with cleaned content (tags stripped) plus surfaces
|
|
1009
|
-
const assistantMsg = conversationStore.addMessage(
|
|
1010
|
-
this.conversationId,
|
|
1011
|
-
'assistant',
|
|
1012
|
-
JSON.stringify(contentWithSurfaces),
|
|
1013
|
-
);
|
|
1014
|
-
lastAssistantMessageId = assistantMsg.id;
|
|
1015
|
-
|
|
1016
|
-
// Clear surfaces for next turn
|
|
1017
|
-
this.currentTurnSurfaces = [];
|
|
1018
|
-
|
|
1019
|
-
// Emit assistant_message trace with content metrics.
|
|
1020
|
-
// Char count only includes text blocks; thinking blocks are
|
|
1021
|
-
// explicitly excluded from traces.
|
|
1022
|
-
const charCount = cleanedContent
|
|
1023
|
-
.filter((b) => (b as Record<string, unknown>).type === 'text')
|
|
1024
|
-
.reduce((sum: number, b) => sum + ((b as { text?: string }).text?.length ?? 0), 0);
|
|
1025
|
-
const toolUseCount = event.message.content
|
|
1026
|
-
.filter((b) => b.type === 'tool_use')
|
|
1027
|
-
.length;
|
|
1028
|
-
this.traceEmitter.emit('assistant_message', 'Assistant message complete', {
|
|
1029
|
-
requestId: reqId,
|
|
1030
|
-
status: 'success',
|
|
1031
|
-
attributes: { charCount, toolUseCount },
|
|
1032
|
-
});
|
|
1033
|
-
break;
|
|
1034
|
-
}
|
|
1035
|
-
case 'usage':
|
|
1036
|
-
exchangeInputTokens += event.inputTokens;
|
|
1037
|
-
exchangeOutputTokens += event.outputTokens;
|
|
1038
|
-
model = event.model;
|
|
1039
|
-
|
|
1040
|
-
// Persist raw LLM request/response payloads for diagnostics export
|
|
1041
|
-
if (event.rawRequest && event.rawResponse) {
|
|
1042
|
-
try {
|
|
1043
|
-
recordRequestLog(
|
|
1044
|
-
this.conversationId,
|
|
1045
|
-
JSON.stringify(event.rawRequest),
|
|
1046
|
-
JSON.stringify(event.rawResponse),
|
|
1047
|
-
);
|
|
1048
|
-
} catch (err) {
|
|
1049
|
-
rlog.warn({ err }, 'Failed to persist LLM request log (non-fatal)');
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
// Ensure llm_call_started is emitted even for tool-only turns
|
|
1054
|
-
// (where no text_delta or thinking_delta events fire)
|
|
1055
|
-
emitLlmCallStartedIfNeeded();
|
|
1056
|
-
|
|
1057
|
-
// Emit llm_call_finished trace with token and latency metrics
|
|
1058
|
-
this.traceEmitter.emit('llm_call_finished', `LLM call to ${this.provider.name} finished`, {
|
|
1059
|
-
requestId: reqId,
|
|
1060
|
-
status: 'success',
|
|
1061
|
-
attributes: {
|
|
1062
|
-
provider: this.provider.name,
|
|
1063
|
-
model: event.model,
|
|
1064
|
-
inputTokens: event.inputTokens,
|
|
1065
|
-
outputTokens: event.outputTokens,
|
|
1066
|
-
latencyMs: event.providerDurationMs,
|
|
1067
|
-
},
|
|
1068
|
-
});
|
|
1069
|
-
// Reset flag so the next provider call in this agent loop run
|
|
1070
|
-
// gets its own llm_call_started trace
|
|
1071
|
-
llmCallStartedEmitted = false;
|
|
1072
|
-
break;
|
|
1073
|
-
}
|
|
1074
|
-
};
|
|
1075
|
-
|
|
1076
|
-
const onCheckpoint = (): CheckpointDecision => {
|
|
1077
|
-
// Capture and reset tool names for this turn
|
|
1078
|
-
const turnTools = currentTurnToolNames;
|
|
1079
|
-
currentTurnToolNames = [];
|
|
1080
|
-
|
|
1081
|
-
if (this.canHandoffAtCheckpoint()) {
|
|
1082
|
-
// Don't interrupt active browser interaction flows — the agent
|
|
1083
|
-
// needs multiple consecutive turns (snapshot → click → snapshot)
|
|
1084
|
-
// and yielding mid-flow leaves the task incomplete.
|
|
1085
|
-
const inBrowserFlow = turnTools.length > 0
|
|
1086
|
-
&& turnTools.every(n => n.startsWith('browser_'));
|
|
1087
|
-
if (!inBrowserFlow) {
|
|
1088
|
-
yieldedForHandoff = true;
|
|
1089
|
-
return 'yield';
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
return 'continue';
|
|
1093
|
-
};
|
|
1094
|
-
|
|
1095
|
-
// Mark that the agent loop is about to run — workspace files may be
|
|
1096
|
-
// modified from this point onward, so we must commit at the turn boundary
|
|
1097
|
-
// even if post-processing (e.g. resolveAssistantAttachments) throws.
|
|
1098
|
-
turnStarted = true;
|
|
1099
|
-
|
|
1100
|
-
let updatedHistory = await this.agentLoop.run(
|
|
1101
|
-
runMessages,
|
|
1102
|
-
buildEventHandler(),
|
|
1103
|
-
abortController.signal,
|
|
1104
|
-
reqId,
|
|
1105
|
-
onCheckpoint,
|
|
1106
|
-
);
|
|
1107
|
-
|
|
1108
|
-
// One-shot self-heal retry: if the provider returned a strict ordering
|
|
1109
|
-
// error and no messages were appended (error on first call), apply a
|
|
1110
|
-
// deep repair (handles additional edge cases like consecutive same-role
|
|
1111
|
-
// messages) and retry exactly once.
|
|
1112
|
-
if (orderingErrorDetected && updatedHistory.length === preRunHistoryLength) {
|
|
1113
|
-
rlog.warn({ phase: 'retry' }, 'Provider ordering error detected, attempting one-shot deep-repair retry');
|
|
1114
|
-
const retryRepair = deepRepairHistory(runMessages);
|
|
1115
|
-
runMessages = retryRepair.messages;
|
|
1116
|
-
// Update preRepairMessages so that structural fixes from deep repair
|
|
1117
|
-
// (e.g., stripping leading assistant messages, merging same-role runs)
|
|
1118
|
-
// persist in this.messages after the run. Without this, the original
|
|
1119
|
-
// malformed prefix would be restored and trigger the same error next turn.
|
|
1120
|
-
preRepairMessages = retryRepair.messages;
|
|
1121
|
-
preRunHistoryLength = runMessages.length;
|
|
1122
|
-
orderingErrorDetected = false;
|
|
1123
|
-
deferredOrderingError = null;
|
|
1124
|
-
|
|
1125
|
-
updatedHistory = await this.agentLoop.run(
|
|
1126
|
-
runMessages,
|
|
1127
|
-
buildEventHandler(),
|
|
1128
|
-
abortController.signal,
|
|
1129
|
-
reqId,
|
|
1130
|
-
onCheckpoint,
|
|
1131
|
-
);
|
|
1132
|
-
|
|
1133
|
-
if (orderingErrorDetected) {
|
|
1134
|
-
rlog.error({ phase: 'retry' }, 'Deep-repair retry also failed with ordering error. Consider starting a new conversation if this persists.');
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
// One-shot context-too-large recovery: force compaction and retry once.
|
|
1139
|
-
if (contextTooLargeDetected && updatedHistory.length === preRunHistoryLength) {
|
|
1140
|
-
rlog.warn({ phase: 'retry' }, 'Context too large — attempting forced compaction and retry');
|
|
1141
|
-
const emergencyCompact = await this.contextWindowManager.maybeCompact(
|
|
1142
|
-
this.messages,
|
|
1143
|
-
abortController.signal,
|
|
1144
|
-
{ lastCompactedAt: this.contextCompactedAt ?? undefined, force: true },
|
|
1145
|
-
);
|
|
1146
|
-
if (emergencyCompact.compacted) {
|
|
1147
|
-
this.messages = emergencyCompact.messages;
|
|
1148
|
-
this.contextCompactedMessageCount += emergencyCompact.compactedPersistedMessages;
|
|
1149
|
-
this.contextCompactedAt = Date.now();
|
|
1150
|
-
conversationStore.updateConversationContextWindow(
|
|
1151
|
-
this.conversationId,
|
|
1152
|
-
emergencyCompact.summaryText,
|
|
1153
|
-
this.contextCompactedMessageCount,
|
|
1154
|
-
);
|
|
1155
|
-
onEvent({
|
|
1156
|
-
type: 'context_compacted',
|
|
1157
|
-
previousEstimatedInputTokens: emergencyCompact.previousEstimatedInputTokens,
|
|
1158
|
-
estimatedInputTokens: emergencyCompact.estimatedInputTokens,
|
|
1159
|
-
maxInputTokens: emergencyCompact.maxInputTokens,
|
|
1160
|
-
thresholdTokens: emergencyCompact.thresholdTokens,
|
|
1161
|
-
compactedMessages: emergencyCompact.compactedMessages,
|
|
1162
|
-
summaryCalls: emergencyCompact.summaryCalls,
|
|
1163
|
-
summaryInputTokens: emergencyCompact.summaryInputTokens,
|
|
1164
|
-
summaryOutputTokens: emergencyCompact.summaryOutputTokens,
|
|
1165
|
-
summaryModel: emergencyCompact.summaryModel,
|
|
1166
|
-
});
|
|
1167
|
-
this.recordUsage(
|
|
1168
|
-
emergencyCompact.summaryInputTokens,
|
|
1169
|
-
emergencyCompact.summaryOutputTokens,
|
|
1170
|
-
emergencyCompact.summaryModel,
|
|
1171
|
-
onEvent,
|
|
1172
|
-
'context_compactor',
|
|
1173
|
-
reqId,
|
|
1174
|
-
);
|
|
1175
|
-
|
|
1176
|
-
// Retry with compacted context
|
|
1177
|
-
runMessages = applyRuntimeInjections(this.messages, {
|
|
1178
|
-
softConflictInstruction,
|
|
1179
|
-
activeSurface,
|
|
1180
|
-
workspaceTopLevelContext: this.workspaceTopLevelContext,
|
|
1181
|
-
});
|
|
1182
|
-
preRepairMessages = runMessages;
|
|
1183
|
-
preRunHistoryLength = runMessages.length;
|
|
1184
|
-
contextTooLargeDetected = false;
|
|
1185
|
-
|
|
1186
|
-
updatedHistory = await this.agentLoop.run(
|
|
1187
|
-
runMessages,
|
|
1188
|
-
buildEventHandler(),
|
|
1189
|
-
abortController.signal,
|
|
1190
|
-
reqId,
|
|
1191
|
-
onCheckpoint,
|
|
1192
|
-
);
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
if (contextTooLargeDetected) {
|
|
1196
|
-
const mediaTrimmed = stripMediaPayloadsForRetry(this.messages);
|
|
1197
|
-
if (mediaTrimmed.modified) {
|
|
1198
|
-
rlog.warn(
|
|
1199
|
-
{
|
|
1200
|
-
phase: 'retry',
|
|
1201
|
-
replacedBlocks: mediaTrimmed.replacedBlocks,
|
|
1202
|
-
latestUserIndex: mediaTrimmed.latestUserIndex,
|
|
1203
|
-
},
|
|
1204
|
-
'Context still too large — retrying with older media payloads trimmed',
|
|
1205
|
-
);
|
|
1206
|
-
this.messages = mediaTrimmed.messages;
|
|
1207
|
-
runMessages = applyRuntimeInjections(this.messages, {
|
|
1208
|
-
softConflictInstruction,
|
|
1209
|
-
activeSurface,
|
|
1210
|
-
workspaceTopLevelContext: this.workspaceTopLevelContext,
|
|
1211
|
-
});
|
|
1212
|
-
preRepairMessages = runMessages;
|
|
1213
|
-
preRunHistoryLength = runMessages.length;
|
|
1214
|
-
contextTooLargeDetected = false;
|
|
1215
|
-
|
|
1216
|
-
updatedHistory = await this.agentLoop.run(
|
|
1217
|
-
runMessages,
|
|
1218
|
-
buildEventHandler(),
|
|
1219
|
-
abortController.signal,
|
|
1220
|
-
reqId,
|
|
1221
|
-
onCheckpoint,
|
|
1222
|
-
);
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
// Surface the error if compaction didn't help or wasn't possible
|
|
1227
|
-
if (contextTooLargeDetected) {
|
|
1228
|
-
const classified = classifySessionError(
|
|
1229
|
-
new Error('context_length_exceeded'),
|
|
1230
|
-
{ phase: 'agent_loop' },
|
|
1231
|
-
);
|
|
1232
|
-
onEvent(buildSessionErrorMessage(this.conversationId, classified));
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
// Forward the deferred ordering error to the client if retry failed or was not attempted
|
|
1237
|
-
if (deferredOrderingError) {
|
|
1238
|
-
const classified = classifySessionError(new Error(deferredOrderingError), { phase: 'agent_loop' });
|
|
1239
|
-
onEvent(buildSessionErrorMessage(this.conversationId, classified));
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
// Reconcile synthesized cancellation tool_results from history tail.
|
|
1243
|
-
// When abort happens, the agent loop synthesizes "Cancelled by user"
|
|
1244
|
-
// results directly into the history without firing tool_result events,
|
|
1245
|
-
// so they're missing from pendingToolResults and would not be persisted.
|
|
1246
|
-
for (let i = preRunHistoryLength; i < updatedHistory.length; i++) {
|
|
1247
|
-
const msg = updatedHistory[i];
|
|
1248
|
-
if (msg.role === 'user') {
|
|
1249
|
-
for (const block of msg.content) {
|
|
1250
|
-
if (block.type === 'tool_result' && !pendingToolResults.has(block.tool_use_id) && !persistedToolUseIds.has(block.tool_use_id)) {
|
|
1251
|
-
pendingToolResults.set(block.tool_use_id, {
|
|
1252
|
-
content: block.content,
|
|
1253
|
-
isError: block.is_error ?? false,
|
|
1254
|
-
});
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
// Flush any remaining tool results as a user message
|
|
1261
|
-
if (pendingToolResults.size > 0) {
|
|
1262
|
-
const toolResultBlocks = Array.from(pendingToolResults.entries()).map(
|
|
1263
|
-
([toolUseId, result]) => ({
|
|
1264
|
-
type: 'tool_result',
|
|
1265
|
-
tool_use_id: toolUseId,
|
|
1266
|
-
content: result.content,
|
|
1267
|
-
is_error: result.isError,
|
|
1268
|
-
...(result.contentBlocks ? { contentBlocks: result.contentBlocks } : {}),
|
|
1269
|
-
}),
|
|
1270
|
-
);
|
|
1271
|
-
conversationStore.addMessage(
|
|
1272
|
-
this.conversationId,
|
|
1273
|
-
'user',
|
|
1274
|
-
JSON.stringify(toolResultBlocks),
|
|
1275
|
-
);
|
|
1276
|
-
pendingToolResults.clear();
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
// Reconstruct history: use the original (un-repaired) prefix so that
|
|
1280
|
-
// synthetic tool_result blocks from pre-run repair don't leak into
|
|
1281
|
-
// this.messages. Only the new messages appended by the agent loop
|
|
1282
|
-
// (beyond the repaired prefix) are carried forward.
|
|
1283
|
-
//
|
|
1284
|
-
// Strip directive tags from assistant messages so in-memory history
|
|
1285
|
-
// matches the cleaned content persisted to the DB. Without this,
|
|
1286
|
-
// subsequent turns would send raw <vellum-attachment /> tags to the
|
|
1287
|
-
// LLM, wasting tokens and encouraging hallucinated directives.
|
|
1288
|
-
const newMessages = updatedHistory.slice(preRunHistoryLength).map((msg) => {
|
|
1289
|
-
if (msg.role !== 'assistant') return msg;
|
|
1290
|
-
const { cleanedContent } = cleanAssistantContent(msg.content);
|
|
1291
|
-
return { ...msg, content: cleanedContent as ContentBlock[] };
|
|
1292
|
-
});
|
|
1293
|
-
|
|
1294
|
-
// If no assistant response was produced (e.g. provider 500 error),
|
|
1295
|
-
// synthesize an assistant message so the error is visible in the conversation.
|
|
1296
|
-
const hasAssistantResponse = newMessages.some((msg) => msg.role === 'assistant');
|
|
1297
|
-
if (!hasAssistantResponse && providerErrorUserMessage && !abortController.signal.aborted && !yieldedForHandoff) {
|
|
1298
|
-
const errorAssistantMessage = createAssistantMessage(providerErrorUserMessage);
|
|
1299
|
-
conversationStore.addMessage(
|
|
1300
|
-
this.conversationId,
|
|
1301
|
-
'assistant',
|
|
1302
|
-
JSON.stringify(errorAssistantMessage.content),
|
|
1303
|
-
);
|
|
1304
|
-
newMessages.push(errorAssistantMessage);
|
|
1305
|
-
onEvent({
|
|
1306
|
-
type: 'assistant_text_delta',
|
|
1307
|
-
text: providerErrorUserMessage,
|
|
1308
|
-
sessionId: this.conversationId,
|
|
1309
|
-
});
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
const restoredHistory = [...preRepairMessages, ...newMessages];
|
|
1313
|
-
const recallStripped = stripMemoryRecallMessages(restoredHistory, recall.injectedText, recallInjectionStrategy);
|
|
1314
|
-
this.messages = stripChannelCapabilityContext(
|
|
1315
|
-
stripWorkspaceTopLevelContext(
|
|
1316
|
-
stripActiveSurfaceContext(
|
|
1317
|
-
stripDynamicProfileMessages(recallStripped, dynamicProfile.text),
|
|
1318
|
-
),
|
|
1319
|
-
),
|
|
1320
|
-
);
|
|
1321
|
-
|
|
1322
|
-
this.recordUsage(exchangeInputTokens, exchangeOutputTokens, model, onEvent, 'main_agent', reqId);
|
|
1323
|
-
|
|
1324
|
-
void getHookManager().trigger('post-message', {
|
|
1325
|
-
sessionId: this.conversationId,
|
|
1326
|
-
});
|
|
1327
|
-
|
|
1328
|
-
// Resolve accumulated attachment directives and tool content blocks
|
|
1329
|
-
// BEFORE emitting the completion event so attachments are included.
|
|
1330
|
-
const attachmentResult = await resolveAssistantAttachments(
|
|
1331
|
-
accumulatedDirectives,
|
|
1332
|
-
accumulatedToolContentBlocks,
|
|
1333
|
-
directiveWarnings,
|
|
1334
|
-
this.workingDir,
|
|
1335
|
-
async (filePath) => this.approveHostAttachmentReadImpl(filePath),
|
|
1336
|
-
lastAssistantMessageId,
|
|
1337
|
-
this.assistantId ?? 'local-assistant',
|
|
1338
|
-
);
|
|
1339
|
-
const { assistantAttachments, emittedAttachments } = attachmentResult;
|
|
1340
|
-
|
|
1341
|
-
this.lastAssistantAttachments = assistantAttachments;
|
|
1342
|
-
this.lastAttachmentWarnings = attachmentResult.directiveWarnings;
|
|
1343
|
-
|
|
1344
|
-
const warningText = formatAttachmentWarnings(attachmentResult.directiveWarnings);
|
|
1345
|
-
if (warningText) {
|
|
1346
|
-
onEvent({ type: 'assistant_text_delta', text: warningText, sessionId: this.conversationId });
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
// Emit the completion event here in the try block; the turn-boundary
|
|
1350
|
-
// commit runs in `finally` (after this), so the client's
|
|
1351
|
-
// thinking/streaming indicators clear immediately without waiting
|
|
1352
|
-
// for the git commit (which can take 0.5–2 s on large workspaces).
|
|
1353
|
-
if (yieldedForHandoff) {
|
|
1354
|
-
this.traceEmitter.emit('generation_handoff', 'Handing off to next queued message', {
|
|
1355
|
-
requestId: reqId,
|
|
1356
|
-
status: 'info',
|
|
1357
|
-
attributes: { queuedCount: this.getQueueDepth() },
|
|
1358
|
-
});
|
|
1359
|
-
onEvent({
|
|
1360
|
-
type: 'generation_handoff',
|
|
1361
|
-
sessionId: this.conversationId,
|
|
1362
|
-
requestId: reqId,
|
|
1363
|
-
queuedCount: this.getQueueDepth(),
|
|
1364
|
-
...(emittedAttachments.length > 0 ? { attachments: emittedAttachments } : {}),
|
|
1365
|
-
});
|
|
1366
|
-
} else if (abortController.signal.aborted) {
|
|
1367
|
-
this.traceEmitter.emit('generation_cancelled', 'Generation cancelled by user', {
|
|
1368
|
-
requestId: reqId,
|
|
1369
|
-
status: 'warning',
|
|
1370
|
-
});
|
|
1371
|
-
onEvent({ type: 'generation_cancelled', sessionId: this.conversationId });
|
|
1372
|
-
} else {
|
|
1373
|
-
this.traceEmitter.emit('message_complete', 'Message processing complete', {
|
|
1374
|
-
requestId: reqId,
|
|
1375
|
-
status: 'success',
|
|
1376
|
-
});
|
|
1377
|
-
onEvent({
|
|
1378
|
-
type: 'message_complete',
|
|
1379
|
-
sessionId: this.conversationId,
|
|
1380
|
-
...(emittedAttachments.length > 0 ? { attachments: emittedAttachments } : {}),
|
|
1381
|
-
});
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
// Auto-generate conversation title after first exchange
|
|
1385
|
-
if (isFirstMessage) {
|
|
1386
|
-
this.generateTitle(content, firstAssistantText).catch((err) => {
|
|
1387
|
-
log.warn({ err, conversationId: this.conversationId }, 'Failed to generate conversation title (non-fatal, using default title)');
|
|
1388
|
-
});
|
|
1389
|
-
}
|
|
1390
|
-
} catch (err) {
|
|
1391
|
-
const errorCtx = { phase: 'agent_loop' as const, aborted: abortController.signal.aborted };
|
|
1392
|
-
// AbortError is expected when user cancels — don't treat as an error
|
|
1393
|
-
if (isUserCancellation(err, errorCtx)) {
|
|
1394
|
-
rlog.info('Generation cancelled by user');
|
|
1395
|
-
this.traceEmitter.emit('generation_cancelled', 'Generation cancelled by user', {
|
|
1396
|
-
requestId: reqId,
|
|
1397
|
-
status: 'warning',
|
|
1398
|
-
});
|
|
1399
|
-
onEvent({ type: 'generation_cancelled', sessionId: this.conversationId });
|
|
1400
|
-
} else {
|
|
1401
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1402
|
-
const errorClass = err instanceof Error ? err.constructor.name : 'Error';
|
|
1403
|
-
rlog.error({ err }, 'Session processing error');
|
|
1404
|
-
this.traceEmitter.emit('request_error', message.slice(0, 200), {
|
|
1405
|
-
requestId: reqId,
|
|
1406
|
-
status: 'error',
|
|
1407
|
-
attributes: { errorClass, message: message.slice(0, 500) },
|
|
1408
|
-
});
|
|
1409
|
-
onEvent({ type: 'error', message: `Failed to process message: ${message}` });
|
|
1410
|
-
const classified = classifySessionError(err, errorCtx);
|
|
1411
|
-
onEvent(buildSessionErrorMessage(this.conversationId, classified));
|
|
1412
|
-
void getHookManager().trigger('on-error', {
|
|
1413
|
-
error: err instanceof Error ? err.name : 'Error',
|
|
1414
|
-
message,
|
|
1415
|
-
stack: err instanceof Error ? err.stack : undefined,
|
|
1416
|
-
sessionId: this.conversationId,
|
|
1417
|
-
});
|
|
1418
|
-
}
|
|
1419
|
-
} finally {
|
|
1420
|
-
// Turn-boundary commit: runs after completion/error events (try or
|
|
1421
|
-
// catch) but before drainQueue. Guarantees a commit attempt whenever
|
|
1422
|
-
// the agent loop started, even if post-processing threw.
|
|
1423
|
-
if (turnStarted) {
|
|
1424
|
-
this.turnCount++;
|
|
1425
|
-
const config = getConfig();
|
|
1426
|
-
const maxWait = config.workspaceGit?.turnCommitMaxWaitMs ?? 4000;
|
|
1427
|
-
const deadlineMs = Date.now() + maxWait;
|
|
1428
|
-
const commitPromise = commitTurnChanges(
|
|
1429
|
-
this.workingDir, this.conversationId, this.turnCount,
|
|
1430
|
-
undefined, // use default commit message provider
|
|
1431
|
-
deadlineMs,
|
|
1432
|
-
);
|
|
1433
|
-
const outcome = await raceWithTimeout(commitPromise, maxWait);
|
|
1434
|
-
if (outcome === 'timed_out') {
|
|
1435
|
-
rlog.warn(
|
|
1436
|
-
{ turnNumber: this.turnCount, maxWaitMs: maxWait, conversationId: this.conversationId },
|
|
1437
|
-
'Turn-boundary commit timed out — continuing without waiting (commit still runs in background)',
|
|
1438
|
-
);
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
this.profiler.emitSummary(this.traceEmitter, reqId);
|
|
1443
|
-
|
|
1444
|
-
this.abortController = null;
|
|
1445
|
-
this.processing = false;
|
|
1446
|
-
this.currentRequestId = undefined;
|
|
1447
|
-
this.currentActiveSurfaceId = undefined;
|
|
1448
|
-
this.allowedToolNames = undefined;
|
|
1449
|
-
this.preactivatedSkillIds = undefined;
|
|
1450
|
-
|
|
1451
|
-
// Consolidate consecutive assistant messages from this agent loop run
|
|
1452
|
-
if (userMessageId) {
|
|
1453
|
-
this.consolidateAssistantMessages(userMessageId);
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
// Drain the next queued message, if any
|
|
1457
|
-
this.drainQueue(yieldedForHandoff ? 'checkpoint_handoff' : 'loop_complete');
|
|
1458
|
-
}
|
|
340
|
+
return runAgentLoopImpl(this, content, userMessageId, onEvent, options);
|
|
1459
341
|
}
|
|
1460
342
|
|
|
1461
|
-
private consolidateAssistantMessages(userMessageId: string): void {
|
|
1462
|
-
consolidateAssistantMessages(this.conversationId, userMessageId);
|
|
1463
|
-
}
|
|
1464
343
|
|
|
1465
|
-
|
|
344
|
+
drainQueue(reason: QueueDrainReason = 'loop_complete'): void {
|
|
1466
345
|
drainQueueImpl(this as ProcessSessionContext, reason);
|
|
1467
346
|
}
|
|
1468
347
|
|
|
@@ -1477,9 +356,7 @@ export class Session {
|
|
|
1477
356
|
return processMessageImpl(this as ProcessSessionContext, content, attachments, onEvent, requestId, activeSurfaceId, currentPage);
|
|
1478
357
|
}
|
|
1479
358
|
|
|
1480
|
-
|
|
1481
|
-
handleSurfaceActionImpl(this, surfaceId, actionId, data);
|
|
1482
|
-
}
|
|
359
|
+
// ── History ──────────────────────────────────────────────────────
|
|
1483
360
|
|
|
1484
361
|
getMessages(): Message[] {
|
|
1485
362
|
return this.messages;
|
|
@@ -1493,7 +370,17 @@ export class Session {
|
|
|
1493
370
|
return regenerateImpl(this as HistorySessionContext, onEvent, requestId);
|
|
1494
371
|
}
|
|
1495
372
|
|
|
1496
|
-
// ──
|
|
373
|
+
// ── Surfaces ─────────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
handleSurfaceAction(surfaceId: string, actionId: string, data?: Record<string, unknown>): void {
|
|
376
|
+
handleSurfaceActionImpl(this, surfaceId, actionId, data);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
handleSurfaceUndo(surfaceId: string): void {
|
|
380
|
+
handleSurfaceUndoImpl(this, surfaceId);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ── Workspace ────────────────────────────────────────────────────
|
|
1497
384
|
|
|
1498
385
|
refreshWorkspaceTopLevelContextIfNeeded(): void {
|
|
1499
386
|
refreshWorkspaceImpl(this);
|
|
@@ -1510,181 +397,4 @@ export class Session {
|
|
|
1510
397
|
isWorkspaceTopLevelDirty(): boolean {
|
|
1511
398
|
return this.workspaceTopLevelDirty;
|
|
1512
399
|
}
|
|
1513
|
-
|
|
1514
|
-
/**
|
|
1515
|
-
* After an app_update, refresh any active surface that displays the updated app.
|
|
1516
|
-
* This makes app_update a single call that both persists AND displays changes.
|
|
1517
|
-
*/
|
|
1518
|
-
handleSurfaceUndo(surfaceId: string): void {
|
|
1519
|
-
handleSurfaceUndoImpl(this, surfaceId);
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
private recordUsage(
|
|
1523
|
-
inputTokens: number,
|
|
1524
|
-
outputTokens: number,
|
|
1525
|
-
model: string,
|
|
1526
|
-
onEvent: (msg: ServerMessage) => void,
|
|
1527
|
-
actor: UsageActor,
|
|
1528
|
-
requestId: string | null = null,
|
|
1529
|
-
): void {
|
|
1530
|
-
recordUsage(
|
|
1531
|
-
{ conversationId: this.conversationId, providerName: this.provider.name, assistantId: this.assistantId, usageStats: this.usageStats },
|
|
1532
|
-
inputTokens, outputTokens, model, onEvent, actor, requestId,
|
|
1533
|
-
);
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
private async generateTitle(userMessage: string, assistantResponse: string): Promise<void> {
|
|
1537
|
-
const prompt = `Generate a very short title for this conversation. Rules: at most 5 words, at most 40 characters, no quotes.\n\nUser: ${userMessage.slice(0, 200)}\nAssistant: ${assistantResponse.slice(0, 200)}`;
|
|
1538
|
-
const response = await this.provider.sendMessage(
|
|
1539
|
-
[{ role: 'user', content: [{ type: 'text', text: prompt }] }],
|
|
1540
|
-
[], // no tools
|
|
1541
|
-
undefined, // no system prompt
|
|
1542
|
-
{ config: { max_tokens: 30 } },
|
|
1543
|
-
);
|
|
1544
|
-
|
|
1545
|
-
const textBlock = response.content.find((b) => b.type === 'text');
|
|
1546
|
-
if (textBlock && textBlock.type === 'text') {
|
|
1547
|
-
let title = textBlock.text.trim().replace(/^["']|["']$/g, '');
|
|
1548
|
-
const words = title.split(/\s+/);
|
|
1549
|
-
if (words.length > 5) title = words.slice(0, 5).join(' ');
|
|
1550
|
-
if (title.length > 40) title = title.slice(0, 40).trimEnd();
|
|
1551
|
-
conversationStore.updateConversationTitle(this.conversationId, title);
|
|
1552
|
-
log.info({ conversationId: this.conversationId, title }, 'Auto-generated conversation title');
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
function stripMediaPayloadsForRetry(messages: Message[]): { messages: Message[]; modified: boolean; replacedBlocks: number; latestUserIndex: number | null } {
|
|
1559
|
-
let latestUserIndex: number | null = null;
|
|
1560
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1561
|
-
const msg = messages[i];
|
|
1562
|
-
if (msg.role !== 'user') continue;
|
|
1563
|
-
if (getSummaryFromContextMessage(msg) !== null) continue;
|
|
1564
|
-
if (isToolResultOnlyMessage(msg)) continue;
|
|
1565
|
-
latestUserIndex = i;
|
|
1566
|
-
break;
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
let modified = false;
|
|
1570
|
-
let replacedBlocks = 0;
|
|
1571
|
-
let keptLatestMediaBlocks = 0;
|
|
1572
|
-
|
|
1573
|
-
const nextMessages = messages.map((msg, msgIndex) => {
|
|
1574
|
-
const nextContent: ContentBlock[] = [];
|
|
1575
|
-
for (const block of msg.content) {
|
|
1576
|
-
if (block.type === 'image') {
|
|
1577
|
-
const keep = latestUserIndex === msgIndex && keptLatestMediaBlocks < RETRY_KEEP_LATEST_MEDIA_BLOCKS;
|
|
1578
|
-
if (keep) {
|
|
1579
|
-
keptLatestMediaBlocks += 1;
|
|
1580
|
-
nextContent.push(block);
|
|
1581
|
-
} else {
|
|
1582
|
-
replacedBlocks += 1;
|
|
1583
|
-
modified = true;
|
|
1584
|
-
nextContent.push(imageBlockToStub(block));
|
|
1585
|
-
}
|
|
1586
|
-
continue;
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
if (block.type === 'file') {
|
|
1590
|
-
const keep = latestUserIndex === msgIndex && keptLatestMediaBlocks < RETRY_KEEP_LATEST_MEDIA_BLOCKS;
|
|
1591
|
-
if (keep) {
|
|
1592
|
-
keptLatestMediaBlocks += 1;
|
|
1593
|
-
nextContent.push(block);
|
|
1594
|
-
} else {
|
|
1595
|
-
replacedBlocks += 1;
|
|
1596
|
-
modified = true;
|
|
1597
|
-
nextContent.push(fileBlockToStub(block));
|
|
1598
|
-
}
|
|
1599
|
-
continue;
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
if (block.type === 'tool_result' && block.contentBlocks && block.contentBlocks.length > 0) {
|
|
1603
|
-
let toolResultChanged = false;
|
|
1604
|
-
const nextToolContentBlocks: ContentBlock[] = block.contentBlocks.map((cb) => {
|
|
1605
|
-
if (cb.type === 'image') {
|
|
1606
|
-
replacedBlocks += 1;
|
|
1607
|
-
modified = true;
|
|
1608
|
-
toolResultChanged = true;
|
|
1609
|
-
return imageBlockToStub(cb);
|
|
1610
|
-
}
|
|
1611
|
-
if (cb.type === 'file') {
|
|
1612
|
-
replacedBlocks += 1;
|
|
1613
|
-
modified = true;
|
|
1614
|
-
toolResultChanged = true;
|
|
1615
|
-
return fileBlockToStub(cb);
|
|
1616
|
-
}
|
|
1617
|
-
return cb;
|
|
1618
|
-
});
|
|
1619
|
-
if (toolResultChanged) {
|
|
1620
|
-
nextContent.push({ ...block, contentBlocks: nextToolContentBlocks });
|
|
1621
|
-
} else {
|
|
1622
|
-
nextContent.push(block);
|
|
1623
|
-
}
|
|
1624
|
-
continue;
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
nextContent.push(block);
|
|
1628
|
-
}
|
|
1629
|
-
return { ...msg, content: nextContent };
|
|
1630
|
-
});
|
|
1631
|
-
|
|
1632
|
-
return {
|
|
1633
|
-
messages: modified ? nextMessages : messages,
|
|
1634
|
-
modified,
|
|
1635
|
-
replacedBlocks,
|
|
1636
|
-
latestUserIndex,
|
|
1637
|
-
};
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
function imageBlockToStub(block: Extract<ContentBlock, { type: 'image' }>): Extract<ContentBlock, { type: 'text' }> {
|
|
1641
|
-
const sizeBytes = Math.ceil(block.source.data.length / 4) * 3;
|
|
1642
|
-
return {
|
|
1643
|
-
type: 'text',
|
|
1644
|
-
text: `[Image omitted from retry context: ${block.source.media_type}, ${sizeBytes} bytes]`,
|
|
1645
|
-
};
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
function fileBlockToStub(block: Extract<ContentBlock, { type: 'file' }>): Extract<ContentBlock, { type: 'text' }> {
|
|
1649
|
-
const sizeBytes = Math.ceil(block.source.data.length / 4) * 3;
|
|
1650
|
-
const extracted = (block.extracted_text ?? '').trim();
|
|
1651
|
-
const preview = extracted.length > MAX_MEDIA_STUB_TEXT
|
|
1652
|
-
? `${extracted.slice(0, MAX_MEDIA_STUB_TEXT)}...`
|
|
1653
|
-
: extracted;
|
|
1654
|
-
return {
|
|
1655
|
-
type: 'text',
|
|
1656
|
-
text: preview.length > 0
|
|
1657
|
-
? `[File omitted from retry context: ${block.source.filename} (${block.source.media_type}, ${sizeBytes} bytes)]\n${preview}`
|
|
1658
|
-
: `[File omitted from retry context: ${block.source.filename} (${block.source.media_type}, ${sizeBytes} bytes)]`,
|
|
1659
|
-
};
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
function isToolResultOnlyMessage(message: Message): boolean {
|
|
1663
|
-
return message.content.length > 0
|
|
1664
|
-
&& message.content.every((block) => block.type === 'tool_result');
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
/**
|
|
1668
|
-
* Race a promise against a timeout. Returns 'completed' if the promise
|
|
1669
|
-
* resolves/rejects within the budget, or 'timed_out' if the timeout fires
|
|
1670
|
-
* first. The timer is always cleared in `finally` to prevent handle leaks.
|
|
1671
|
-
*/
|
|
1672
|
-
async function raceWithTimeout<T>(
|
|
1673
|
-
promise: Promise<T>,
|
|
1674
|
-
timeoutMs: number,
|
|
1675
|
-
): Promise<'completed' | 'timed_out'> {
|
|
1676
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1677
|
-
try {
|
|
1678
|
-
const result = await Promise.race([
|
|
1679
|
-
promise.then(() => 'completed' as const),
|
|
1680
|
-
new Promise<'timed_out'>((resolve) => {
|
|
1681
|
-
timer = setTimeout(() => resolve('timed_out'), timeoutMs);
|
|
1682
|
-
}),
|
|
1683
|
-
]);
|
|
1684
|
-
return result;
|
|
1685
|
-
} finally {
|
|
1686
|
-
if (timer !== undefined) {
|
|
1687
|
-
clearTimeout(timer);
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
400
|
}
|