visionclaw 0.1.193 → 0.1.194-dev.feat-backup-progress-reporting.1
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/dist/agent/backup-uploader.d.ts +44 -0
- package/dist/agent/backup-uploader.d.ts.map +1 -0
- package/dist/agent/backup-uploader.js +83 -0
- package/dist/agent/backup-uploader.js.map +1 -0
- package/dist/agent/backup.d.ts +7 -1
- package/dist/agent/backup.d.ts.map +1 -1
- package/dist/agent/backup.js +337 -52
- package/dist/agent/backup.js.map +1 -1
- package/dist/agent/command-handlers.d.ts +2 -0
- package/dist/agent/command-handlers.d.ts.map +1 -1
- package/dist/agent/command-handlers.js +29 -3
- package/dist/agent/command-handlers.js.map +1 -1
- package/dist/agent/context.d.ts +2 -0
- package/dist/agent/context.d.ts.map +1 -1
- package/dist/agent/context.js +11 -1
- package/dist/agent/context.js.map +1 -1
- package/dist/agent/data-collector.d.ts.map +1 -1
- package/dist/agent/data-collector.js +23 -38
- package/dist/agent/data-collector.js.map +1 -1
- package/dist/agent/loop.d.ts +3 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +48 -0
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/message-format.d.ts.map +1 -1
- package/dist/agent/message-format.js +3 -0
- package/dist/agent/message-format.js.map +1 -1
- package/dist/agent/status.d.ts.map +1 -1
- package/dist/agent/status.js +2 -1
- package/dist/agent/status.js.map +1 -1
- package/dist/backup.d.ts +4 -1
- package/dist/backup.d.ts.map +1 -1
- package/dist/backup.js +86 -3
- package/dist/backup.js.map +1 -1
- package/dist/calendar/google-calendar.d.ts +2 -0
- package/dist/calendar/google-calendar.d.ts.map +1 -1
- package/dist/calendar/google-calendar.js +3 -0
- package/dist/calendar/google-calendar.js.map +1 -1
- package/dist/channels/interface.d.ts +9 -0
- package/dist/channels/interface.d.ts.map +1 -1
- package/dist/channels/manager.d.ts +7 -1
- package/dist/channels/manager.d.ts.map +1 -1
- package/dist/channels/manager.js +12 -0
- package/dist/channels/manager.js.map +1 -1
- package/dist/channels/telegram.d.ts.map +1 -1
- package/dist/channels/telegram.js +5 -0
- package/dist/channels/telegram.js.map +1 -1
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +42 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/config/types.d.ts +9 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +6 -0
- package/dist/config/types.js.map +1 -1
- package/dist/drive/google-drive.d.ts +1 -0
- package/dist/drive/google-drive.d.ts.map +1 -1
- package/dist/drive/google-drive.js +5 -0
- package/dist/drive/google-drive.js.map +1 -1
- package/dist/i18n/messages.d.ts +4 -0
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/i18n/messages.js +7 -2
- package/dist/i18n/messages.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/obs/server.d.ts +7 -0
- package/dist/obs/server.d.ts.map +1 -1
- package/dist/obs/server.js +22 -0
- package/dist/obs/server.js.map +1 -1
- package/dist/onboarding/bot-profile.d.ts.map +1 -1
- package/dist/onboarding/bot-profile.js +1 -0
- package/dist/onboarding/bot-profile.js.map +1 -1
- package/dist/onboarding/index.d.ts.map +1 -1
- package/dist/onboarding/index.js +5 -0
- package/dist/onboarding/index.js.map +1 -1
- package/dist/onboarding/prepare-mac.d.ts +1 -0
- package/dist/onboarding/prepare-mac.d.ts.map +1 -1
- package/dist/onboarding/prepare-mac.js +32 -15
- package/dist/onboarding/prepare-mac.js.map +1 -1
- package/dist/onboarding/setup-shared.d.ts.map +1 -1
- package/dist/onboarding/setup-shared.js +6 -0
- package/dist/onboarding/setup-shared.js.map +1 -1
- package/dist/onboarding/setup-steps.d.ts.map +1 -1
- package/dist/onboarding/setup-steps.js +8 -0
- package/dist/onboarding/setup-steps.js.map +1 -1
- package/dist/realtime/agent-bridge.d.ts +7 -0
- package/dist/realtime/agent-bridge.d.ts.map +1 -0
- package/dist/realtime/agent-bridge.js +31 -0
- package/dist/realtime/agent-bridge.js.map +1 -0
- package/dist/realtime/assets/index.html +1058 -0
- package/dist/realtime/assets/samples/alloy.mp3 +0 -0
- package/dist/realtime/assets/samples/ash.mp3 +0 -0
- package/dist/realtime/assets/samples/ballad.mp3 +0 -0
- package/dist/realtime/assets/samples/cedar.mp3 +0 -0
- package/dist/realtime/assets/samples/coral.mp3 +0 -0
- package/dist/realtime/assets/samples/echo.mp3 +0 -0
- package/dist/realtime/assets/samples/marin.mp3 +0 -0
- package/dist/realtime/assets/samples/sage.mp3 +0 -0
- package/dist/realtime/assets/samples/shimmer.mp3 +0 -0
- package/dist/realtime/assets/samples/verse.mp3 +0 -0
- package/dist/realtime/context.d.ts +14 -0
- package/dist/realtime/context.d.ts.map +1 -0
- package/dist/realtime/context.js +153 -0
- package/dist/realtime/context.js.map +1 -0
- package/dist/realtime/http-helpers.d.ts +5 -0
- package/dist/realtime/http-helpers.d.ts.map +1 -0
- package/dist/realtime/http-helpers.js +29 -0
- package/dist/realtime/http-helpers.js.map +1 -0
- package/dist/realtime/index.d.ts +62 -0
- package/dist/realtime/index.d.ts.map +1 -0
- package/dist/realtime/index.js +94 -0
- package/dist/realtime/index.js.map +1 -0
- package/dist/realtime/server.d.ts +6 -0
- package/dist/realtime/server.d.ts.map +1 -0
- package/dist/realtime/server.js +476 -0
- package/dist/realtime/server.js.map +1 -0
- package/dist/realtime/telegram-auth.d.ts +2 -0
- package/dist/realtime/telegram-auth.d.ts.map +1 -0
- package/dist/realtime/telegram-auth.js +24 -0
- package/dist/realtime/telegram-auth.js.map +1 -0
- package/dist/realtime/tools.d.ts +829 -0
- package/dist/realtime/tools.d.ts.map +1 -0
- package/dist/realtime/tools.js +630 -0
- package/dist/realtime/tools.js.map +1 -0
- package/dist/realtime/types.d.ts +62 -0
- package/dist/realtime/types.d.ts.map +1 -0
- package/dist/realtime/types.js +3 -0
- package/dist/realtime/types.js.map +1 -0
- package/dist/realtime/voice-summarizer.d.ts +4 -0
- package/dist/realtime/voice-summarizer.d.ts.map +1 -0
- package/dist/realtime/voice-summarizer.js +129 -0
- package/dist/realtime/voice-summarizer.js.map +1 -0
- package/dist/restore.d.ts +5 -0
- package/dist/restore.d.ts.map +1 -1
- package/dist/restore.js +132 -16
- package/dist/restore.js.map +1 -1
- package/dist/tools/email.d.ts +1 -1
- package/dist/tools/memory.d.ts +13 -0
- package/dist/tools/memory.d.ts.map +1 -1
- package/dist/tools/memory.js +43 -43
- package/dist/tools/memory.js.map +1 -1
- package/dist/tools/stock-data.d.ts +16 -0
- package/dist/tools/stock-data.d.ts.map +1 -1
- package/dist/tools/stock-data.js +36 -38
- package/dist/tools/stock-data.js.map +1 -1
- package/dist/tools/web-fetch.d.ts +4 -0
- package/dist/tools/web-fetch.d.ts.map +1 -1
- package/dist/tools/web-fetch.js +95 -23
- package/dist/tools/web-fetch.js.map +1 -1
- package/dist/tos-storage.d.ts +45 -0
- package/dist/tos-storage.d.ts.map +1 -0
- package/dist/tos-storage.js +134 -0
- package/dist/tos-storage.js.map +1 -0
- package/dist-agent/bundle.cjs +170934 -167547
- package/package.json +3 -2
- package/dist/agent/applied-credential-signature.d.ts +0 -53
- package/dist/agent/applied-credential-signature.d.ts.map +0 -1
- package/dist/agent/applied-credential-signature.js +0 -137
- package/dist/agent/applied-credential-signature.js.map +0 -1
- package/dist/agent/engines/claude/cli-resolver.d.ts +0 -16
- package/dist/agent/engines/claude/cli-resolver.d.ts.map +0 -1
- package/dist/agent/engines/claude/cli-resolver.js +0 -83
- package/dist/agent/engines/claude/cli-resolver.js.map +0 -1
- package/dist/agent/engines/claude/session-browser-policy.d.ts +0 -9
- package/dist/agent/engines/claude/session-browser-policy.d.ts.map +0 -1
- package/dist/agent/engines/claude/session-browser-policy.js +0 -49
- package/dist/agent/engines/claude/session-browser-policy.js.map +0 -1
- package/dist/agent/engines/claude/session.d.ts +0 -291
- package/dist/agent/engines/claude/session.d.ts.map +0 -1
- package/dist/agent/engines/claude/session.js +0 -1177
- package/dist/agent/engines/claude/session.js.map +0 -1
- package/dist/agent/engines/client-factory.d.ts +0 -63
- package/dist/agent/engines/client-factory.d.ts.map +0 -1
- package/dist/agent/engines/client-factory.js +0 -382
- package/dist/agent/engines/client-factory.js.map +0 -1
- package/dist/agent/engines/engine.d.ts +0 -8
- package/dist/agent/engines/engine.d.ts.map +0 -1
- package/dist/agent/engines/engine.js +0 -15
- package/dist/agent/engines/engine.js.map +0 -1
- package/dist/agent/engines/openai/file-session.d.ts +0 -49
- package/dist/agent/engines/openai/file-session.d.ts.map +0 -1
- package/dist/agent/engines/openai/file-session.js +0 -108
- package/dist/agent/engines/openai/file-session.js.map +0 -1
- package/dist/agent/engines/openai/file-tools.d.ts +0 -35
- package/dist/agent/engines/openai/file-tools.d.ts.map +0 -1
- package/dist/agent/engines/openai/file-tools.js +0 -194
- package/dist/agent/engines/openai/file-tools.js.map +0 -1
- package/dist/agent/engines/openai/session.d.ts +0 -190
- package/dist/agent/engines/openai/session.d.ts.map +0 -1
- package/dist/agent/engines/openai/session.js +0 -1066
- package/dist/agent/engines/openai/session.js.map +0 -1
- package/dist/agent/engines/openai/tools.d.ts +0 -13
- package/dist/agent/engines/openai/tools.d.ts.map +0 -1
- package/dist/agent/engines/openai/tools.js +0 -248
- package/dist/agent/engines/openai/tools.js.map +0 -1
- package/dist/agent/engines/session-types.d.ts +0 -146
- package/dist/agent/engines/session-types.d.ts.map +0 -1
- package/dist/agent/engines/session-types.js +0 -2
- package/dist/agent/engines/session-types.js.map +0 -1
- package/dist/agent/engines/system-prompt-log.d.ts +0 -9
- package/dist/agent/engines/system-prompt-log.d.ts.map +0 -1
- package/dist/agent/engines/system-prompt-log.js +0 -46
- package/dist/agent/engines/system-prompt-log.js.map +0 -1
- package/dist/agent/transcript/transcript-backfill.d.ts +0 -54
- package/dist/agent/transcript/transcript-backfill.d.ts.map +0 -1
- package/dist/agent/transcript/transcript-backfill.js +0 -604
- package/dist/agent/transcript/transcript-backfill.js.map +0 -1
- package/dist/agent/transcript/transcript-indexer.d.ts +0 -273
- package/dist/agent/transcript/transcript-indexer.d.ts.map +0 -1
- package/dist/agent/transcript/transcript-indexer.js +0 -1217
- package/dist/agent/transcript/transcript-indexer.js.map +0 -1
- package/dist/agent/transcript/transcript-memory-migrations.d.ts +0 -25
- package/dist/agent/transcript/transcript-memory-migrations.d.ts.map +0 -1
- package/dist/agent/transcript/transcript-memory-migrations.js +0 -87
- package/dist/agent/transcript/transcript-memory-migrations.js.map +0 -1
- package/dist/agent/transcript-memory-migrations.d.ts +0 -25
- package/dist/agent/transcript-memory-migrations.d.ts.map +0 -1
- package/dist/agent/transcript-memory-migrations.js +0 -87
- package/dist/agent/transcript-memory-migrations.js.map +0 -1
- package/dist/agent/tunnel-credential-handler.d.ts +0 -90
- package/dist/agent/tunnel-credential-handler.d.ts.map +0 -1
- package/dist/agent/tunnel-credential-handler.js +0 -162
- package/dist/agent/tunnel-credential-handler.js.map +0 -1
- package/dist/agent/usage/usage-backfill-handler.d.ts +0 -18
- package/dist/agent/usage/usage-backfill-handler.d.ts.map +0 -1
- package/dist/agent/usage/usage-backfill-handler.js +0 -69
- package/dist/agent/usage/usage-backfill-handler.js.map +0 -1
- package/dist/agent/usage/usage-gate.d.ts +0 -25
- package/dist/agent/usage/usage-gate.d.ts.map +0 -1
- package/dist/agent/usage/usage-gate.js +0 -83
- package/dist/agent/usage/usage-gate.js.map +0 -1
- package/dist/agent/usage/usage-handler.d.ts +0 -7
- package/dist/agent/usage/usage-handler.d.ts.map +0 -1
- package/dist/agent/usage/usage-handler.js +0 -28
- package/dist/agent/usage/usage-handler.js.map +0 -1
- package/dist/agent/usage/usage-report-builder.d.ts +0 -26
- package/dist/agent/usage/usage-report-builder.d.ts.map +0 -1
- package/dist/agent/usage/usage-report-builder.js +0 -80
- package/dist/agent/usage/usage-report-builder.js.map +0 -1
- package/dist/agent/usage/usage-report-queue.d.ts +0 -26
- package/dist/agent/usage/usage-report-queue.d.ts.map +0 -1
- package/dist/agent/usage/usage-report-queue.js +0 -199
- package/dist/agent/usage/usage-report-queue.js.map +0 -1
- package/dist/agent/usage/usage-report-types.d.ts +0 -41
- package/dist/agent/usage/usage-report-types.d.ts.map +0 -1
- package/dist/agent/usage/usage-report-types.js +0 -2
- package/dist/agent/usage/usage-report-types.js.map +0 -1
- package/dist/agent/usage/usage-reporter.d.ts +0 -31
- package/dist/agent/usage/usage-reporter.d.ts.map +0 -1
- package/dist/agent/usage/usage-reporter.js +0 -102
- package/dist/agent/usage/usage-reporter.js.map +0 -1
- package/dist/agent/usage-backfill-handler.d.ts +0 -18
- package/dist/agent/usage-backfill-handler.d.ts.map +0 -1
- package/dist/agent/usage-backfill-handler.js +0 -69
- package/dist/agent/usage-backfill-handler.js.map +0 -1
- package/dist/agent/usage-gate.d.ts +0 -25
- package/dist/agent/usage-gate.d.ts.map +0 -1
- package/dist/agent/usage-gate.js +0 -83
- package/dist/agent/usage-gate.js.map +0 -1
- package/dist/agent/usage-report-builder.d.ts +0 -26
- package/dist/agent/usage-report-builder.d.ts.map +0 -1
- package/dist/agent/usage-report-builder.js +0 -80
- package/dist/agent/usage-report-builder.js.map +0 -1
- package/dist/agent/usage-report-queue.d.ts +0 -26
- package/dist/agent/usage-report-queue.d.ts.map +0 -1
- package/dist/agent/usage-report-queue.js +0 -199
- package/dist/agent/usage-report-queue.js.map +0 -1
- package/dist/agent/usage-report-types.d.ts +0 -41
- package/dist/agent/usage-report-types.d.ts.map +0 -1
- package/dist/agent/usage-report-types.js +0 -2
- package/dist/agent/usage-report-types.js.map +0 -1
- package/dist/agent/usage-reporter.d.ts +0 -31
- package/dist/agent/usage-reporter.d.ts.map +0 -1
- package/dist/agent/usage-reporter.js +0 -102
- package/dist/agent/usage-reporter.js.map +0 -1
- package/dist/agent/wake-cycle-tool-tracker.d.ts +0 -39
- package/dist/agent/wake-cycle-tool-tracker.d.ts.map +0 -1
- package/dist/agent/wake-cycle-tool-tracker.js +0 -72
- package/dist/agent/wake-cycle-tool-tracker.js.map +0 -1
- package/dist/billing/payg-handler.d.ts +0 -29
- package/dist/billing/payg-handler.d.ts.map +0 -1
- package/dist/billing/payg-handler.js +0 -92
- package/dist/billing/payg-handler.js.map +0 -1
- package/dist/billing/payment-handler.d.ts +0 -24
- package/dist/billing/payment-handler.d.ts.map +0 -1
- package/dist/billing/payment-handler.js +0 -101
- package/dist/billing/payment-handler.js.map +0 -1
- package/dist/builtin-skills/catalog/phone-adb-automation/SKILL.md +0 -412
- package/dist/builtin-skills/catalog/phone-adb-automation/phone_input.sh +0 -132
- package/dist/builtin-skills/catalog/phone-adb-automation/phone_launch.sh +0 -166
- package/dist/builtin-skills/catalog/phone-adb-automation/phone_screenshot.sh +0 -87
- package/dist/builtin-skills/catalog/phone-adb-automation/phone_security_kbd.py +0 -174
- package/dist/builtin-skills/catalog/phone-adb-automation/phone_setup.sh +0 -274
- package/dist/builtin-skills/catalog/phone-adb-automation/phone_swipe.sh +0 -111
- package/dist/builtin-skills/catalog/phone-adb-automation/phone_tap.sh +0 -87
- package/dist/builtin-skills/catalog/phone-adb-automation/phone_ui_parse.py +0 -176
- package/dist/builtin-skills/catalog/phone-adb-automation/phone_wake_unlock.sh +0 -67
- package/dist/builtin-skills/transcribe-audio/SKILL.md +0 -122
- package/dist/data-processing/convert-demo-cli.d.ts +0 -7
- package/dist/data-processing/convert-demo-cli.d.ts.map +0 -1
- package/dist/data-processing/convert-demo-cli.js +0 -30
- package/dist/data-processing/convert-demo-cli.js.map +0 -1
- package/dist/data-processing/convert-demo.d.ts +0 -26
- package/dist/data-processing/convert-demo.d.ts.map +0 -1
- package/dist/data-processing/convert-demo.js +0 -233
- package/dist/data-processing/convert-demo.js.map +0 -1
- package/dist/obs/rdp/icons/icons/app_windows.svg +0 -4
- package/dist/obs/rdp/icons/icons/clip_get.svg +0 -4
- package/dist/obs/rdp/icons/icons/clip_send.svg +0 -4
- package/dist/obs/rdp/icons/icons/clip_shared.svg +0 -4
- package/dist/obs/rdp/icons/icons/clipboard.svg +0 -4
- package/dist/obs/rdp/icons/icons/clipboard_shared.svg +0 -4
- package/dist/obs/rdp/icons/icons/control.svg +0 -4
- package/dist/obs/rdp/icons/icons/desktop.svg +0 -4
- package/dist/obs/rdp/icons/icons/display.svg +0 -4
- package/dist/obs/rdp/icons/icons/launchpad.svg +0 -4
- package/dist/obs/rdp/icons/icons/mission_control.svg +0 -4
- package/dist/obs/rdp/icons/icons/screenshot.svg +0 -4
- package/dist/obs/rdp/icons/icons/zoom_actual.svg +0 -4
- package/dist/obs/rdp/icons/icons/zoom_fit.svg +0 -4
- package/dist/obs/rdp/icons/icons/zoom_in.svg +0 -4
- package/dist/obs/rdp/icons/icons/zoom_out.svg +0 -4
- package/dist/obs/tunnel-telemetry.d.ts +0 -46
- package/dist/obs/tunnel-telemetry.d.ts.map +0 -1
- package/dist/obs/tunnel-telemetry.js +0 -70
- package/dist/obs/tunnel-telemetry.js.map +0 -1
- package/dist/onboarding/cloudflared-cert.d.ts +0 -15
- package/dist/onboarding/cloudflared-cert.d.ts.map +0 -1
- package/dist/onboarding/cloudflared-cert.js +0 -57
- package/dist/onboarding/cloudflared-cert.js.map +0 -1
- package/dist/onboarding/playwriter-extension.d.ts +0 -19
- package/dist/onboarding/playwriter-extension.d.ts.map +0 -1
- package/dist/onboarding/playwriter-extension.js +0 -246
- package/dist/onboarding/playwriter-extension.js.map +0 -1
- package/dist/service/gbox-tun.d.ts +0 -14
- package/dist/service/gbox-tun.d.ts.map +0 -1
- package/dist/service/gbox-tun.js +0 -315
- package/dist/service/gbox-tun.js.map +0 -1
- package/dist/skills/installed.d.ts +0 -11
- package/dist/skills/installed.d.ts.map +0 -1
- package/dist/skills/installed.js +0 -35
- package/dist/skills/installed.js.map +0 -1
- package/dist/tools/coordinate-resolver.d.ts +0 -30
- package/dist/tools/coordinate-resolver.d.ts.map +0 -1
- package/dist/tools/coordinate-resolver.js +0 -104
- package/dist/tools/coordinate-resolver.js.map +0 -1
- package/dist/utils/playwriter-relay.d.ts +0 -9
- package/dist/utils/playwriter-relay.d.ts.map +0 -1
- package/dist/utils/playwriter-relay.js +0 -77
- package/dist/utils/playwriter-relay.js.map +0 -1
- package/dist/utils/wechat-monitor.d.ts +0 -21
- package/dist/utils/wechat-monitor.d.ts.map +0 -1
- package/dist/utils/wechat-monitor.js +0 -88
- package/dist/utils/wechat-monitor.js.map +0 -1
|
@@ -1,1177 +0,0 @@
|
|
|
1
|
-
import * as claudeAgentSdk from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
-
import { query, } from "@anthropic-ai/claude-agent-sdk";
|
|
3
|
-
import { loadSessionId, saveSessionId, loadUsageSnapshot, saveUsageSnapshot, getConfigDir } from "../../../config/index.js";
|
|
4
|
-
import { seedClaudeAutoMemorySetting } from "../../../config/claude-settings.js";
|
|
5
|
-
import { logger } from "../../../logger.js";
|
|
6
|
-
import { createToolServer } from "../../../tools/index.js";
|
|
7
|
-
import { sanitizeJsonString, sanitizeJsonValueInPlace } from "../../../utils/json-sanitize.js";
|
|
8
|
-
import { buildAgentEnv, getModelId } from "../client-factory.js";
|
|
9
|
-
import { logSystemPrompt } from "../system-prompt-log.js";
|
|
10
|
-
import { getAgentState } from "../../state.js";
|
|
11
|
-
import { resolveClaudeCliExecutable } from "./cli-resolver.js";
|
|
12
|
-
import { ensurePlaywriterChrome } from "./session-browser-policy.js";
|
|
13
|
-
import path from "path";
|
|
14
|
-
import { getClaudeTranscriptDirForProfileDir } from "../../../utils/claude-transcripts.js";
|
|
15
|
-
import { ensureBrowser } from "../../browser-launcher.js";
|
|
16
|
-
import * as imagePrunerUnTyped from "../../image-pruner.js";
|
|
17
|
-
import * as sessionTrimmerUnTyped from "../../session-trimmer.js";
|
|
18
|
-
const imagePruner = imagePrunerUnTyped;
|
|
19
|
-
const sessionTrimmer = sessionTrimmerUnTyped;
|
|
20
|
-
/**
|
|
21
|
-
* Used-token threshold (fraction of context window) at which we trigger
|
|
22
|
-
* compaction for **large** context windows (>= {@link COMPACT_MAX_CONTEXT_WINDOW}).
|
|
23
|
-
*
|
|
24
|
-
* Smaller windows scale up to {@link COMPACT_SMALL_WINDOW_USED_PCT_THRESHOLD}
|
|
25
|
-
* via {@link getCompactUsedPctThreshold}.
|
|
26
|
-
*/
|
|
27
|
-
export const COMPACT_LARGE_WINDOW_USED_PCT_THRESHOLD = 0.7;
|
|
28
|
-
const COMPACT_MIN_CONTEXT_WINDOW = 200_000;
|
|
29
|
-
const COMPACT_MAX_CONTEXT_WINDOW = 1_000_000;
|
|
30
|
-
/**
|
|
31
|
-
* Used-token threshold (fraction of context window) at which we trigger
|
|
32
|
-
* compaction for **small** context windows (<= {@link COMPACT_MIN_CONTEXT_WINDOW}).
|
|
33
|
-
* Set above 1.0 to effectively disable token-based compaction for tiny
|
|
34
|
-
* windows where the SDK already manages context internally.
|
|
35
|
-
*/
|
|
36
|
-
const COMPACT_SMALL_WINDOW_USED_PCT_THRESHOLD = 4.0;
|
|
37
|
-
export const MAX_BASE64_IMAGE_BLOCKS = 10;
|
|
38
|
-
/** When total image blocks (real + placeholder) exceed this, trigger compaction
|
|
39
|
-
* to avoid hitting Claude API's 100-image limit. */
|
|
40
|
-
export const IMAGE_COMPACT_THRESHOLD = 80;
|
|
41
|
-
//const CONTEXT_1M_BETAS = ["context-1m-2025-08-07"] as const;
|
|
42
|
-
/**
|
|
43
|
-
* Cap the SDK's `startup()` initialization wait. The SDK's default is 60s,
|
|
44
|
-
* which can effectively block an entire wake cycle if the CLI hangs on
|
|
45
|
-
* startup. We prefer to fall back to a normal cold `query()` quickly and
|
|
46
|
-
* let the live path surface the real error.
|
|
47
|
-
*/
|
|
48
|
-
const WARM_INITIALIZE_TIMEOUT_MS = 15_000;
|
|
49
|
-
/**
|
|
50
|
-
* How long to keep a pre-warmed subprocess alive before proactively
|
|
51
|
-
* refreshing it. Bounded staleness protects us from silent degradation:
|
|
52
|
-
* - Anthropic API sessions have server-side idle limits; a long-idle
|
|
53
|
-
* warmed handle may get GC'd on the server, causing the first real
|
|
54
|
-
* turn to fail and forcing a cold recovery.
|
|
55
|
-
* - Long-lived Node subprocesses can accumulate state (fds, memory,
|
|
56
|
-
* half-closed sockets). Periodic replacement keeps each consumed
|
|
57
|
-
* handle young.
|
|
58
|
-
*
|
|
59
|
-
* Ten minutes is a comfortable trade-off: frequent enough to avoid
|
|
60
|
-
* most server-side reaping thresholds, infrequent enough that the
|
|
61
|
-
* extra cold-starts during refresh are negligible (~6/hour).
|
|
62
|
-
*/
|
|
63
|
-
const WARM_REFRESH_TTL_MS = 10 * 60_000;
|
|
64
|
-
/**
|
|
65
|
-
* When a proactive refresh fails (startup errored), retry at this
|
|
66
|
-
* shorter cadence instead of waiting a full TTL. This keeps us from
|
|
67
|
-
* sitting cold for 10 minutes if the failure was transient.
|
|
68
|
-
*/
|
|
69
|
-
const WARM_REFRESH_RETRY_MS = 30_000;
|
|
70
|
-
const sdkStartup = claudeAgentSdk.startup;
|
|
71
|
-
/**
|
|
72
|
-
* Compute the used-pct compaction threshold for a given context window.
|
|
73
|
-
*
|
|
74
|
-
* Behavior:
|
|
75
|
-
* - `contextWindow <= 0` (unknown): return `fallbackThreshold` (caller's choice).
|
|
76
|
-
* - `contextWindow <= 200K`: {@link COMPACT_SMALL_WINDOW_USED_PCT_THRESHOLD} (effectively disabled).
|
|
77
|
-
* - `contextWindow >= 1M`: {@link COMPACT_LARGE_WINDOW_USED_PCT_THRESHOLD} (default 50%).
|
|
78
|
-
* - In between: linear interpolation between the two.
|
|
79
|
-
*
|
|
80
|
-
* Tiny windows accumulate tokens slowly and the SDK manages context internally,
|
|
81
|
-
* so we want a much higher trigger; very large windows benefit from compacting
|
|
82
|
-
* earlier to keep latency / cache hit-rate reasonable.
|
|
83
|
-
*/
|
|
84
|
-
function getCompactUsedPctThreshold(contextWindow, fallbackThreshold) {
|
|
85
|
-
if (contextWindow <= 0)
|
|
86
|
-
return fallbackThreshold;
|
|
87
|
-
if (contextWindow <= COMPACT_MIN_CONTEXT_WINDOW)
|
|
88
|
-
return COMPACT_SMALL_WINDOW_USED_PCT_THRESHOLD;
|
|
89
|
-
if (contextWindow >= COMPACT_MAX_CONTEXT_WINDOW)
|
|
90
|
-
return COMPACT_LARGE_WINDOW_USED_PCT_THRESHOLD;
|
|
91
|
-
const progress = (contextWindow - COMPACT_MIN_CONTEXT_WINDOW) / (COMPACT_MAX_CONTEXT_WINDOW - COMPACT_MIN_CONTEXT_WINDOW);
|
|
92
|
-
return COMPACT_SMALL_WINDOW_USED_PCT_THRESHOLD + progress * (COMPACT_LARGE_WINDOW_USED_PCT_THRESHOLD - COMPACT_SMALL_WINDOW_USED_PCT_THRESHOLD);
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Wraps the V1 query() API with a long-lived async generator for streaming
|
|
96
|
-
* input. This allows injecting additional user messages while the agent is
|
|
97
|
-
* processing (interrupt messages), instead of waiting for the query to
|
|
98
|
-
* finish before sending the next message.
|
|
99
|
-
*/
|
|
100
|
-
export class ClaudeAgentSession {
|
|
101
|
-
config;
|
|
102
|
-
buildSystemPrompt;
|
|
103
|
-
runtimeSurface;
|
|
104
|
-
getDualSessionEnabled;
|
|
105
|
-
getAndroidUseEnabled;
|
|
106
|
-
/** Working directory for the SDK subprocess. Defaults to profile config dir. */
|
|
107
|
-
sessionCwd;
|
|
108
|
-
nativeTools;
|
|
109
|
-
/** Resolved external MCP servers (playwright, serpapi, etc.) for this runtime. */
|
|
110
|
-
externalMcpServers;
|
|
111
|
-
/** Dynamic MCP servers added at runtime via manage_mcp_servers tool. */
|
|
112
|
-
dynamicMcpServers;
|
|
113
|
-
currentQuery = null;
|
|
114
|
-
warmQuery = null;
|
|
115
|
-
warmUpPromise = null;
|
|
116
|
-
warmReadyAtMs = null;
|
|
117
|
-
sessionId;
|
|
118
|
-
transcriptPath = null;
|
|
119
|
-
mode;
|
|
120
|
-
/**
|
|
121
|
-
* Pending message queue for the long-lived generator.
|
|
122
|
-
* When a message is injected, it's pushed here and the waiting
|
|
123
|
-
* generator is woken up via the resolver.
|
|
124
|
-
*/
|
|
125
|
-
pendingMessages = [];
|
|
126
|
-
messageResolver = null;
|
|
127
|
-
generatorClosed = true;
|
|
128
|
-
_orphanedInjections = 0;
|
|
129
|
-
lastCompactRequestAtMs = 0;
|
|
130
|
-
compactInFlight = false;
|
|
131
|
-
stopRequested = false;
|
|
132
|
-
/**
|
|
133
|
-
* Tracks whether we've seeded `<profile>/.claude/settings.local.json`
|
|
134
|
-
* with our auto-memory directory override this process-lifetime. The seed
|
|
135
|
-
* helper is itself idempotent, but this flag keeps hot paths (every
|
|
136
|
-
* `consume()` / `warmUp()`) from hitting the filesystem repeatedly.
|
|
137
|
-
*/
|
|
138
|
-
autoMemorySeedDone = false;
|
|
139
|
-
/** Optional: auto-injects screenshots after Playwright tool calls. */
|
|
140
|
-
_screenshotInjector = null;
|
|
141
|
-
/** AbortController passed to the SDK; aborting it cleanly stops the running query. */
|
|
142
|
-
abortController = null;
|
|
143
|
-
warmAbortController = null;
|
|
144
|
-
/**
|
|
145
|
-
* Timer that proactively replaces the pre-warmed subprocess at
|
|
146
|
-
* `WARM_REFRESH_TTL_MS` intervals. Cleared when the warm is consumed,
|
|
147
|
-
* on stop, or before arming a new one.
|
|
148
|
-
*/
|
|
149
|
-
warmRefreshTimer = null;
|
|
150
|
-
lastUsageSnapshot = null;
|
|
151
|
-
playwriterConnectionsResetThisTurn = false;
|
|
152
|
-
constructor({ config, buildSystemPrompt, runtimeSurface, sessionContext, }) {
|
|
153
|
-
this.config = config;
|
|
154
|
-
this.buildSystemPrompt = buildSystemPrompt;
|
|
155
|
-
this.runtimeSurface = runtimeSurface;
|
|
156
|
-
this.getDualSessionEnabled = sessionContext.getDualSessionEnabled;
|
|
157
|
-
this.getAndroidUseEnabled = sessionContext.getAndroidUseEnabled;
|
|
158
|
-
// Always use the config dir as cwd so the SDK stores transcripts in a
|
|
159
|
-
// consistent directory regardless of which project the coding session
|
|
160
|
-
// is working on.
|
|
161
|
-
this.sessionCwd = getConfigDir();
|
|
162
|
-
this.nativeTools = runtimeSurface.nativeTools;
|
|
163
|
-
this.externalMcpServers = runtimeSurface.externalMcpServers;
|
|
164
|
-
this.dynamicMcpServers = {};
|
|
165
|
-
this.mode = sessionContext.mode;
|
|
166
|
-
this.sessionId = loadSessionId(this.config, this.mode);
|
|
167
|
-
const persisted = loadUsageSnapshot(this.config, this.mode);
|
|
168
|
-
if (persisted && this.sessionId) {
|
|
169
|
-
this.lastUsageSnapshot = { ...persisted, capturedAtMs: Date.now() };
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
set screenshotInjector(injector) {
|
|
173
|
-
this._screenshotInjector = injector;
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* Seed `<profile>/.claude/settings.local.json` once per process so the
|
|
177
|
-
* CLI's auto-memory lands inside the active profile instead of the
|
|
178
|
-
* default `~/.claude/projects/...` tree. Called from every CLI-spawn
|
|
179
|
-
* path (warm + cold); the underlying helper is itself idempotent.
|
|
180
|
-
*
|
|
181
|
-
* Gated on the `autoMemorySeedDone` flag so the happy path doesn't
|
|
182
|
-
* re-read/re-write the file every turn. The flag is set **only on
|
|
183
|
-
* success** — if the first attempt fails (permissions, disk full, etc.)
|
|
184
|
-
* the next warm/cold spawn will retry until it lands, instead of
|
|
185
|
-
* silently leaving the CLI on its default memory path for the rest of
|
|
186
|
-
* the session.
|
|
187
|
-
*/
|
|
188
|
-
ensureAutoMemorySeeded() {
|
|
189
|
-
if (this.autoMemorySeedDone)
|
|
190
|
-
return;
|
|
191
|
-
try {
|
|
192
|
-
this.autoMemorySeedDone = seedClaudeAutoMemorySetting();
|
|
193
|
-
}
|
|
194
|
-
catch (err) {
|
|
195
|
-
// Never let a settings-seeding failure block the agent. Worst case
|
|
196
|
-
// the CLI uses its default memory path and /obs reports that path
|
|
197
|
-
// correctly via the system/init event — degraded but functional.
|
|
198
|
-
// We deliberately do NOT set autoMemorySeedDone here so the next
|
|
199
|
-
// spawn retries.
|
|
200
|
-
logger.warn(`[auto-memory] Seeding failed; proceeding with CLI defaults: ${err instanceof Error ? err.message : String(err)}`);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* Build a fresh mcpServers record with a new visionclaw tool server instance.
|
|
205
|
-
* This avoids the "Already connected to a transport" error when the same
|
|
206
|
-
* McpServer instance is reused across concurrent/sequential query() calls.
|
|
207
|
-
*/
|
|
208
|
-
buildMcpServers() {
|
|
209
|
-
if (this.runtimeSurface.visionClawToolTransport.transport !== "claude-mcp") {
|
|
210
|
-
throw new Error("Claude session requires claude-mcp VisionClaw tool transport");
|
|
211
|
-
}
|
|
212
|
-
return {
|
|
213
|
-
...this.externalMcpServers,
|
|
214
|
-
...this.dynamicMcpServers,
|
|
215
|
-
visionclaw: createToolServer({
|
|
216
|
-
dualSession: this.getDualSessionEnabled()
|
|
217
|
-
&& this.runtimeSurface.visionClawToolTransport.supportsSwitchSessionTool,
|
|
218
|
-
androidUse: this.getAndroidUseEnabled(),
|
|
219
|
-
}),
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
buildNativeToolOptions() {
|
|
223
|
-
if (this.nativeTools.kind === "claude-default") {
|
|
224
|
-
return {
|
|
225
|
-
tools: { type: "preset", preset: "claude_code" },
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
return {};
|
|
229
|
-
}
|
|
230
|
-
buildHooks() {
|
|
231
|
-
return {
|
|
232
|
-
UserPromptSubmit: [
|
|
233
|
-
{
|
|
234
|
-
hooks: [
|
|
235
|
-
(async (input, _toolUseID, { signal }) => {
|
|
236
|
-
const up = input;
|
|
237
|
-
try {
|
|
238
|
-
this.captureTranscriptPath(up.transcript_path);
|
|
239
|
-
if (signal.aborted)
|
|
240
|
-
return {};
|
|
241
|
-
const res = await imagePruner.pruneSessionImages({
|
|
242
|
-
transcriptPath: up.transcript_path,
|
|
243
|
-
keepLastNBase64Images: MAX_BASE64_IMAGE_BLOCKS,
|
|
244
|
-
});
|
|
245
|
-
if (this.lastUsageSnapshot) {
|
|
246
|
-
this.captureUsageSnapshot({
|
|
247
|
-
...this.lastUsageSnapshot,
|
|
248
|
-
allImageBlocks: res.allImageBlocks,
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
catch (err) {
|
|
253
|
-
logger.warn(`UserPromptSubmit image pruning failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
254
|
-
}
|
|
255
|
-
return {};
|
|
256
|
-
}),
|
|
257
|
-
],
|
|
258
|
-
timeout: 120,
|
|
259
|
-
},
|
|
260
|
-
],
|
|
261
|
-
PreCompact: [
|
|
262
|
-
{
|
|
263
|
-
hooks: [
|
|
264
|
-
(async (input, _toolUseID, { signal }) => {
|
|
265
|
-
const pre = input;
|
|
266
|
-
try {
|
|
267
|
-
this.captureTranscriptPath(pre.transcript_path);
|
|
268
|
-
if (signal.aborted)
|
|
269
|
-
return {};
|
|
270
|
-
const res = await imagePruner.pruneSessionImages({
|
|
271
|
-
transcriptPath: pre.transcript_path,
|
|
272
|
-
keepLastNBase64Images: MAX_BASE64_IMAGE_BLOCKS,
|
|
273
|
-
});
|
|
274
|
-
if (res.fileChanged) {
|
|
275
|
-
logger.system(`Pruned transcript images before compaction: pruned=${res.prunedImageBlocks} resized=${res.resizedImageBlocks} kept=${res.keptImageBlocks} total=${res.totalImageBlocks}`);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
catch (err) {
|
|
279
|
-
logger.warn(`PreCompact image pruning failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
280
|
-
}
|
|
281
|
-
return {};
|
|
282
|
-
}),
|
|
283
|
-
],
|
|
284
|
-
timeout: 120,
|
|
285
|
-
},
|
|
286
|
-
],
|
|
287
|
-
PreToolUse: [
|
|
288
|
-
{
|
|
289
|
-
matcher: this.config.browserBackend === "playwriter"
|
|
290
|
-
? "mcp__playwriter__.*"
|
|
291
|
-
: "mcp__playwright__.*",
|
|
292
|
-
hooks: [
|
|
293
|
-
(async (_input) => {
|
|
294
|
-
const pre = _input;
|
|
295
|
-
logger.debug(`[PreToolUse] ensuring browser for ${pre.tool_name}`);
|
|
296
|
-
if (this.config.browserBackend === "playwriter") {
|
|
297
|
-
const didReset = ensurePlaywriterChrome(this.playwriterConnectionsResetThisTurn);
|
|
298
|
-
if (didReset) {
|
|
299
|
-
this.playwriterConnectionsResetThisTurn = true;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
await ensureBrowser();
|
|
304
|
-
}
|
|
305
|
-
return {};
|
|
306
|
-
}),
|
|
307
|
-
],
|
|
308
|
-
timeout: 30,
|
|
309
|
-
},
|
|
310
|
-
],
|
|
311
|
-
PostToolUse: [
|
|
312
|
-
{
|
|
313
|
-
hooks: [
|
|
314
|
-
(async (input, _toolUseID) => {
|
|
315
|
-
const post = input;
|
|
316
|
-
// Track the (possibly mutated) tool response separately from
|
|
317
|
-
// post.tool_response so the top-level-string case can swap in
|
|
318
|
-
// a sanitized replacement without aliasing the original.
|
|
319
|
-
let toolResponse = post.tool_response;
|
|
320
|
-
let mutated = false;
|
|
321
|
-
try {
|
|
322
|
-
const { resizedCount } = await imagePruner.resizeOversizedImagesInToolOutput(toolResponse);
|
|
323
|
-
if (resizedCount > 0) {
|
|
324
|
-
logger.debug(`[PostToolUse] resized ${resizedCount} oversized image(s) in ${post.tool_name} output`);
|
|
325
|
-
mutated = true;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
catch (err) {
|
|
329
|
-
logger.warn(`PostToolUse image resize failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
330
|
-
}
|
|
331
|
-
// Strip lone UTF-16 surrogates from the tool response so they
|
|
332
|
-
// never reach the model API's JSON validator (which rejects
|
|
333
|
-
// them, unlike Node's permissive JSON.stringify).
|
|
334
|
-
//
|
|
335
|
-
// MCP tools return objects (CallToolResult), but
|
|
336
|
-
// tool_response is typed as `unknown` and non-MCP tools may
|
|
337
|
-
// surface raw strings. Handle both shapes:
|
|
338
|
-
// - string: sanitize and replace `toolResponse`
|
|
339
|
-
// - object/array: sanitize in place
|
|
340
|
-
try {
|
|
341
|
-
if (typeof toolResponse === "string") {
|
|
342
|
-
const sanitized = sanitizeJsonString(toolResponse);
|
|
343
|
-
if (sanitized !== toolResponse) {
|
|
344
|
-
toolResponse = sanitized;
|
|
345
|
-
logger.debug(`[PostToolUse] sanitized lone surrogates in ${post.tool_name} string output`);
|
|
346
|
-
mutated = true;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
else if (sanitizeJsonValueInPlace(toolResponse)) {
|
|
350
|
-
logger.debug(`[PostToolUse] sanitized lone surrogates in ${post.tool_name} output`);
|
|
351
|
-
mutated = true;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
catch (err) {
|
|
355
|
-
logger.warn(`PostToolUse surrogate sanitize failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
356
|
-
}
|
|
357
|
-
if (this._screenshotInjector) {
|
|
358
|
-
try {
|
|
359
|
-
await this._screenshotInjector.onToolComplete(post.tool_name, this);
|
|
360
|
-
}
|
|
361
|
-
catch (err) {
|
|
362
|
-
logger.warn(`PostToolUse screenshot injection failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
if (mutated) {
|
|
366
|
-
return {
|
|
367
|
-
hookSpecificOutput: {
|
|
368
|
-
hookEventName: "PostToolUse",
|
|
369
|
-
updatedMCPToolOutput: toolResponse,
|
|
370
|
-
},
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
return {};
|
|
374
|
-
}),
|
|
375
|
-
],
|
|
376
|
-
timeout: 120,
|
|
377
|
-
},
|
|
378
|
-
],
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
/**
|
|
382
|
-
* Build the shared Claude query options used for both pre-warm and live runs.
|
|
383
|
-
*/
|
|
384
|
-
buildQueryOptions(systemPrompt, savedSessionId, abortController, effort) {
|
|
385
|
-
const cliResolution = resolveClaudeCliExecutable();
|
|
386
|
-
const options = {
|
|
387
|
-
model: getModelId(this.config),
|
|
388
|
-
...(cliResolution.kind !== "sdk-default"
|
|
389
|
-
? { pathToClaudeCodeExecutable: cliResolution.path }
|
|
390
|
-
: {}),
|
|
391
|
-
systemPrompt,
|
|
392
|
-
...this.buildNativeToolOptions(),
|
|
393
|
-
disallowedTools: this.runtimeSurface.disallowedTools,
|
|
394
|
-
permissionMode: "bypassPermissions",
|
|
395
|
-
allowDangerouslySkipPermissions: true,
|
|
396
|
-
cwd: this.sessionCwd,
|
|
397
|
-
settingSources: ["project"],
|
|
398
|
-
mcpServers: this.buildMcpServers(),
|
|
399
|
-
hooks: this.buildHooks(),
|
|
400
|
-
env: buildAgentEnv(this.config),
|
|
401
|
-
...(savedSessionId ? { resume: savedSessionId } : {}),
|
|
402
|
-
...(abortController ? { abortController } : {}),
|
|
403
|
-
//betas: [...CONTEXT_1M_BETAS],
|
|
404
|
-
};
|
|
405
|
-
options.effort = effort ?? (this.mode === "coding" ? "high" : "medium");
|
|
406
|
-
return options;
|
|
407
|
-
}
|
|
408
|
-
/**
|
|
409
|
-
* Cancel any pending proactive-refresh timer. Idempotent.
|
|
410
|
-
*/
|
|
411
|
-
clearWarmRefreshTimer() {
|
|
412
|
-
if (this.warmRefreshTimer) {
|
|
413
|
-
clearTimeout(this.warmRefreshTimer);
|
|
414
|
-
this.warmRefreshTimer = null;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
/**
|
|
418
|
-
* Arm the proactive-refresh timer. Caller must ensure any previous timer
|
|
419
|
-
* is cleared. Uses `.unref()` so the process can exit while a warm
|
|
420
|
-
* subprocess is idle — the refresh is purely a background optimization
|
|
421
|
-
* and should never hold the event loop alive on its own.
|
|
422
|
-
*/
|
|
423
|
-
scheduleWarmRefresh(delayMs) {
|
|
424
|
-
this.clearWarmRefreshTimer();
|
|
425
|
-
const timer = setTimeout(() => {
|
|
426
|
-
this.warmRefreshTimer = null;
|
|
427
|
-
void this.performWarmRefresh();
|
|
428
|
-
}, delayMs);
|
|
429
|
-
if (typeof timer.unref === "function") {
|
|
430
|
-
timer.unref();
|
|
431
|
-
}
|
|
432
|
-
this.warmRefreshTimer = timer;
|
|
433
|
-
}
|
|
434
|
-
/**
|
|
435
|
-
* Handler for the refresh timer. Covers two cases:
|
|
436
|
-
* 1. Normal TTL refresh: `warmQuery` is non-null → close the stale
|
|
437
|
-
* subprocess and start a fresh one.
|
|
438
|
-
* 2. Retry after a previous failure: `warmQuery` is null → just
|
|
439
|
-
* attempt warmUp again (no teardown needed).
|
|
440
|
-
*
|
|
441
|
-
* If the warm was already consumed between timer arming and firing,
|
|
442
|
-
* the consume path would have cleared the timer, so we shouldn't
|
|
443
|
-
* reach here in that case.
|
|
444
|
-
*/
|
|
445
|
-
async performWarmRefresh() {
|
|
446
|
-
if (this.stopRequested) {
|
|
447
|
-
// stopRequested is briefly true between requestStop() and the next
|
|
448
|
-
// loop iteration's clearStop(). If we just return here without
|
|
449
|
-
// rescheduling, the refresh loop silently dies until someone
|
|
450
|
-
// explicitly re-arms it. Defer to a shorter retry so the refresh
|
|
451
|
-
// resumes once the stop window passes.
|
|
452
|
-
logger.debug(`Claude warm refresh deferred: stop requested; retrying in ${WARM_REFRESH_RETRY_MS}ms`);
|
|
453
|
-
this.scheduleWarmRefresh(WARM_REFRESH_RETRY_MS);
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
if (this.warmQuery) {
|
|
457
|
-
const oldWarm = this.warmQuery;
|
|
458
|
-
const oldAbort = this.warmAbortController;
|
|
459
|
-
const oldAgeMs = this.warmReadyAtMs === null ? null : Date.now() - this.warmReadyAtMs;
|
|
460
|
-
this.warmQuery = null;
|
|
461
|
-
this.warmAbortController = null;
|
|
462
|
-
this.warmReadyAtMs = null;
|
|
463
|
-
// Tear down the stale warm subprocess before starting a fresh one.
|
|
464
|
-
// We abort first so any in-flight SDK I/O short-circuits, then close
|
|
465
|
-
// to release the child process and open transports.
|
|
466
|
-
try {
|
|
467
|
-
oldAbort?.abort();
|
|
468
|
-
}
|
|
469
|
-
catch { /* best-effort */ }
|
|
470
|
-
try {
|
|
471
|
-
oldWarm.close();
|
|
472
|
-
}
|
|
473
|
-
catch { /* best-effort */ }
|
|
474
|
-
logger.debug(`Claude warm refresh: replacing ${oldAgeMs === null ? "warm" : `${oldAgeMs}ms-old warm`} subprocess`);
|
|
475
|
-
}
|
|
476
|
-
else {
|
|
477
|
-
// Retry path after a prior failure — nothing to tear down.
|
|
478
|
-
logger.debug("Claude warm refresh: retrying after previous failure");
|
|
479
|
-
}
|
|
480
|
-
// `warmUp()` swallows its own errors (to keep its contract simple for
|
|
481
|
-
// fire-and-forget callers), so we detect failure by inspecting warm
|
|
482
|
-
// state after it resolves rather than relying on a thrown error. We
|
|
483
|
-
// route the post-warmUp checks through a helper so TypeScript doesn't
|
|
484
|
-
// narrow `this.stopRequested`/`this.warmQuery` based on the synchronous
|
|
485
|
-
// assignments above.
|
|
486
|
-
await this.warmUp();
|
|
487
|
-
this.handleWarmRefreshOutcome();
|
|
488
|
-
}
|
|
489
|
-
/**
|
|
490
|
-
* Post-warmUp follow-up for performWarmRefresh. Kept in its own method so
|
|
491
|
-
* TS's control-flow narrowing in performWarmRefresh doesn't treat these
|
|
492
|
-
* fields as having their pre-await values.
|
|
493
|
-
*/
|
|
494
|
-
handleWarmRefreshOutcome() {
|
|
495
|
-
if (this.stopRequested) {
|
|
496
|
-
// Stop arrived while we were refreshing — requestStop already cleaned
|
|
497
|
-
// up; don't re-arm a refresh timer.
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
if (!this.warmQuery) {
|
|
501
|
-
// warmUp() failed (or was a no-op because startup is unavailable).
|
|
502
|
-
// Schedule a shorter retry so we don't sit cold for a full TTL after
|
|
503
|
-
// a transient failure. If startup is permanently unavailable, these
|
|
504
|
-
// retries are cheap no-ops.
|
|
505
|
-
logger.debug(`Claude warm refresh did not produce a new warm; retrying in ${WARM_REFRESH_RETRY_MS}ms`);
|
|
506
|
-
this.scheduleWarmRefresh(WARM_REFRESH_RETRY_MS);
|
|
507
|
-
}
|
|
508
|
-
// On success, warmUp() already scheduled the next refresh at TTL.
|
|
509
|
-
}
|
|
510
|
-
/**
|
|
511
|
-
* Send the initial user message and return the query generator to stream
|
|
512
|
-
* responses. The underlying async generator stays open so that additional
|
|
513
|
-
* messages can be injected via injectMessage().
|
|
514
|
-
*/
|
|
515
|
-
async warmUp() {
|
|
516
|
-
if (!sdkStartup) {
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
519
|
-
// Seed `<profile>/.claude/settings.local.json` once per process. The
|
|
520
|
-
// CLI reads `autoMemoryDirectory` from this file at subprocess
|
|
521
|
-
// startup, so we must write it before spawning the warm child.
|
|
522
|
-
this.ensureAutoMemorySeeded();
|
|
523
|
-
// Already warm — nothing to do, but defensively re-arm the refresh
|
|
524
|
-
// timer if it happens to be missing. This can occur if a TTL refresh
|
|
525
|
-
// fired during a brief stopRequested window and chose to defer rather
|
|
526
|
-
// than close the warm.
|
|
527
|
-
if (this.warmQuery) {
|
|
528
|
-
if (!this.warmRefreshTimer && !this.stopRequested) {
|
|
529
|
-
this.scheduleWarmRefresh(WARM_REFRESH_TTL_MS);
|
|
530
|
-
}
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
// A warm-up is already in flight — await that one instead of racing.
|
|
534
|
-
if (this.warmUpPromise) {
|
|
535
|
-
await this.warmUpPromise;
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
// Honor an in-progress stop request — don't spin up a subprocess we're
|
|
539
|
-
// just going to abort on the next line.
|
|
540
|
-
if (this.stopRequested) {
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
const savedSessionId = this.sessionId;
|
|
544
|
-
const systemPrompt = this.buildSystemPrompt();
|
|
545
|
-
const warmAbortController = new AbortController();
|
|
546
|
-
const warmStartMs = Date.now();
|
|
547
|
-
const promise = (async () => {
|
|
548
|
-
try {
|
|
549
|
-
const warmed = await sdkStartup({
|
|
550
|
-
options: this.buildQueryOptions(systemPrompt, savedSessionId, warmAbortController),
|
|
551
|
-
initializeTimeoutMs: WARM_INITIALIZE_TIMEOUT_MS,
|
|
552
|
-
});
|
|
553
|
-
// Note: we intentionally do NOT discard the freshly-ready warm when
|
|
554
|
-
// `this.stopRequested` is true. requestStop() is typically a "stop
|
|
555
|
-
// the current task" signal, and the user usually starts a new task
|
|
556
|
-
// shortly after. Throwing the warm away here would force the next
|
|
557
|
-
// task to pay the cold-start cost we just paid. On actual process
|
|
558
|
-
// shutdown, the warm child process is reaped with the parent.
|
|
559
|
-
this.warmQuery = warmed;
|
|
560
|
-
this.warmAbortController = warmAbortController;
|
|
561
|
-
this.warmReadyAtMs = Date.now();
|
|
562
|
-
logger.debug(`Claude query pre-warm ready in ${this.warmReadyAtMs - warmStartMs}ms (next refresh in ${WARM_REFRESH_TTL_MS}ms)`);
|
|
563
|
-
// Arm the proactive-refresh timer. When it fires we'll close this
|
|
564
|
-
// subprocess and spin up a fresh one, keeping staleness bounded.
|
|
565
|
-
this.scheduleWarmRefresh(WARM_REFRESH_TTL_MS);
|
|
566
|
-
}
|
|
567
|
-
catch (err) {
|
|
568
|
-
logger.debug(`Claude query pre-warm skipped after ${Date.now() - warmStartMs}ms: ${err instanceof Error ? err.message : String(err)}`);
|
|
569
|
-
this.warmQuery = null;
|
|
570
|
-
this.warmAbortController = null;
|
|
571
|
-
this.warmReadyAtMs = null;
|
|
572
|
-
}
|
|
573
|
-
finally {
|
|
574
|
-
this.warmUpPromise = null;
|
|
575
|
-
}
|
|
576
|
-
})();
|
|
577
|
-
this.warmUpPromise = promise;
|
|
578
|
-
await promise;
|
|
579
|
-
}
|
|
580
|
-
/**
|
|
581
|
-
* Tear down any idle pre-warmed subprocess and cancel its refresh timer.
|
|
582
|
-
*
|
|
583
|
-
* Called when something in `config` that was baked into the warm options
|
|
584
|
-
* changed (provider, env, model). The warm subprocess inlines provider
|
|
585
|
-
* env via `buildAgentEnv(config)` at warm time and `sendAndStream()`
|
|
586
|
-
* deliberately reuses those baked options on the warm path — so without
|
|
587
|
-
* this, a provider switch can be silently undone by consuming a stale
|
|
588
|
-
* warm child for one more turn.
|
|
589
|
-
*
|
|
590
|
-
* Idempotent and best-effort: safe to call when no warm exists, and
|
|
591
|
-
* does not throw on close failures (the child will be reaped on
|
|
592
|
-
* process exit if abort/close misfires).
|
|
593
|
-
*/
|
|
594
|
-
discardWarmQuery() {
|
|
595
|
-
this.clearWarmRefreshTimer();
|
|
596
|
-
const oldWarm = this.warmQuery;
|
|
597
|
-
const oldAbort = this.warmAbortController;
|
|
598
|
-
if (!oldWarm && !oldAbort)
|
|
599
|
-
return;
|
|
600
|
-
this.warmQuery = null;
|
|
601
|
-
this.warmAbortController = null;
|
|
602
|
-
this.warmReadyAtMs = null;
|
|
603
|
-
try {
|
|
604
|
-
oldAbort?.abort();
|
|
605
|
-
}
|
|
606
|
-
catch { /* best-effort */ }
|
|
607
|
-
try {
|
|
608
|
-
oldWarm?.close();
|
|
609
|
-
}
|
|
610
|
-
catch { /* best-effort */ }
|
|
611
|
-
logger.debug("Claude warm subprocess discarded (provider/env changed)");
|
|
612
|
-
}
|
|
613
|
-
sendAndStream(content, options) {
|
|
614
|
-
this.playwriterConnectionsResetThisTurn = false;
|
|
615
|
-
const savedSessionId = this.sessionId;
|
|
616
|
-
const effort = options?.effort;
|
|
617
|
-
// Resolve the Claude Code executable — prefers the native binary shipped
|
|
618
|
-
// by @anthropic-ai/claude-agent-sdk >= 0.2.113, falls back to the legacy
|
|
619
|
-
// cli.js, and finally lets the SDK resolve it internally.
|
|
620
|
-
const cliResolution = resolveClaudeCliExecutable();
|
|
621
|
-
logger.debug(`Claude CLI resolution: kind=${cliResolution.kind}` +
|
|
622
|
-
(cliResolution.kind === "sdk-default" ? "" : ` path=${cliResolution.path}`));
|
|
623
|
-
const systemPrompt = this.buildSystemPrompt();
|
|
624
|
-
// Log the resolved system prompt for training data capture. The console
|
|
625
|
-
// gets a compact head/tail preview; the full prompt goes into `data.prompt`
|
|
626
|
-
// of the JSONL entry so the log file remains complete.
|
|
627
|
-
logSystemPrompt(systemPrompt);
|
|
628
|
-
// Seed the generator with the initial message
|
|
629
|
-
const initialMessage = {
|
|
630
|
-
type: "user",
|
|
631
|
-
session_id: savedSessionId ?? "",
|
|
632
|
-
message: { role: "user", content },
|
|
633
|
-
parent_tool_use_id: null,
|
|
634
|
-
};
|
|
635
|
-
this.pendingMessages = [initialMessage];
|
|
636
|
-
this.generatorClosed = false;
|
|
637
|
-
this._orphanedInjections = 0;
|
|
638
|
-
// Create a long-lived async generator that yields the initial message
|
|
639
|
-
// and then waits for additional messages injected via injectMessage().
|
|
640
|
-
// We capture references to the session's pending queue and resolver
|
|
641
|
-
// so the generator can read from them without aliasing `this`.
|
|
642
|
-
const pending = this.pendingMessages;
|
|
643
|
-
const setResolver = (r) => {
|
|
644
|
-
this.messageResolver = r;
|
|
645
|
-
};
|
|
646
|
-
const isClosed = () => this.generatorClosed;
|
|
647
|
-
const shiftPending = () => pending.shift();
|
|
648
|
-
async function* messageStream() {
|
|
649
|
-
while (!isClosed()) {
|
|
650
|
-
// Yield all pending messages
|
|
651
|
-
let next = shiftPending();
|
|
652
|
-
while (next) {
|
|
653
|
-
yield next;
|
|
654
|
-
next = shiftPending();
|
|
655
|
-
}
|
|
656
|
-
// If generator is still open, wait for the next message
|
|
657
|
-
if (!isClosed()) {
|
|
658
|
-
await new Promise((resolve) => {
|
|
659
|
-
setResolver(resolve);
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
if (savedSessionId) {
|
|
665
|
-
logger.system(`Resuming session: ${savedSessionId}`);
|
|
666
|
-
}
|
|
667
|
-
else {
|
|
668
|
-
logger.system("Creating new session...");
|
|
669
|
-
}
|
|
670
|
-
const prompt = messageStream();
|
|
671
|
-
if (this.warmQuery) {
|
|
672
|
-
// Warm path: the subprocess was already initialized with a full set of
|
|
673
|
-
// query options (system prompt, MCP servers, hooks, CLI path, betas,
|
|
674
|
-
// env, resume, etc.) by warmUp(). We deliberately do NOT call
|
|
675
|
-
// buildQueryOptions() here — doing so would spin up a throwaway
|
|
676
|
-
// McpServer instance and allocate a fresh hook closure set that get
|
|
677
|
-
// immediately discarded, defeating the whole point of pre-warming.
|
|
678
|
-
//
|
|
679
|
-
// Cancel the proactive-refresh timer first: we're about to consume
|
|
680
|
-
// this warm, and we don't want the timer to fire mid-turn and try
|
|
681
|
-
// to close a subprocess that's now actively serving a query.
|
|
682
|
-
this.clearWarmRefreshTimer();
|
|
683
|
-
const warmQuery = this.warmQuery;
|
|
684
|
-
this.warmQuery = null;
|
|
685
|
-
// Adopt the warm abort controller so requestStop() cleanly cancels
|
|
686
|
-
// the running query via the same AbortSignal the SDK is watching.
|
|
687
|
-
this.abortController = this.warmAbortController;
|
|
688
|
-
this.warmAbortController = null;
|
|
689
|
-
const warmAgeMs = this.warmReadyAtMs === null ? null : Date.now() - this.warmReadyAtMs;
|
|
690
|
-
if (effort) {
|
|
691
|
-
logger.debug(`Claude query using pre-warmed subprocess${warmAgeMs === null ? "" : ` (ready ${warmAgeMs}ms ago)`} (effort=${effort} ignored — warm subprocess uses pre-built options)`);
|
|
692
|
-
}
|
|
693
|
-
else {
|
|
694
|
-
logger.debug(`Claude query using pre-warmed subprocess${warmAgeMs === null ? "" : ` (ready ${warmAgeMs}ms ago)`}`);
|
|
695
|
-
}
|
|
696
|
-
this.warmReadyAtMs = null;
|
|
697
|
-
this.currentQuery = warmQuery.query(prompt);
|
|
698
|
-
}
|
|
699
|
-
else {
|
|
700
|
-
// Cold path: build full options and let query() spawn a fresh CLI.
|
|
701
|
-
// Same rationale as warmUp() — seed before the subprocess starts so
|
|
702
|
-
// it picks up the overridden `autoMemoryDirectory`.
|
|
703
|
-
this.ensureAutoMemorySeeded();
|
|
704
|
-
const abortController = new AbortController();
|
|
705
|
-
this.abortController = abortController;
|
|
706
|
-
const options = this.buildQueryOptions(systemPrompt, savedSessionId, abortController, effort);
|
|
707
|
-
if (effort) {
|
|
708
|
-
logger.debug(`Claude query using cold start (effort=${effort})`);
|
|
709
|
-
}
|
|
710
|
-
else {
|
|
711
|
-
logger.debug("Claude query using cold start");
|
|
712
|
-
}
|
|
713
|
-
this.currentQuery = query({ prompt, options });
|
|
714
|
-
}
|
|
715
|
-
return this.currentQuery;
|
|
716
|
-
}
|
|
717
|
-
/**
|
|
718
|
-
* Inject a new user message into the running query stream.
|
|
719
|
-
* The agent will see this as a follow-up user message in the conversation.
|
|
720
|
-
* Only works while a query is active (between sendAndStream and closeInput).
|
|
721
|
-
*
|
|
722
|
-
* @returns true if the message was injected, false if the generator is closed.
|
|
723
|
-
*/
|
|
724
|
-
injectMessage(content, options) {
|
|
725
|
-
const msg = {
|
|
726
|
-
type: "user",
|
|
727
|
-
session_id: this.sessionId ?? "",
|
|
728
|
-
message: { role: "user", content },
|
|
729
|
-
parent_tool_use_id: null,
|
|
730
|
-
...(options?.shouldQuery === false ? { shouldQuery: false } : {}),
|
|
731
|
-
};
|
|
732
|
-
if (this.generatorClosed) {
|
|
733
|
-
logger.warn("Cannot inject message: generator is closed");
|
|
734
|
-
return false;
|
|
735
|
-
}
|
|
736
|
-
this.pendingMessages.push(msg);
|
|
737
|
-
logger.info("Injected interrupt message into active session");
|
|
738
|
-
// Wake the generator if it's waiting
|
|
739
|
-
if (this.messageResolver) {
|
|
740
|
-
this.messageResolver();
|
|
741
|
-
this.messageResolver = null;
|
|
742
|
-
}
|
|
743
|
-
return true;
|
|
744
|
-
}
|
|
745
|
-
/**
|
|
746
|
-
* Signal the input generator to close.
|
|
747
|
-
* Any injected messages still in the pending queue are counted as
|
|
748
|
-
* orphaned — they were accepted by injectMessage but never delivered
|
|
749
|
-
* to the SDK.
|
|
750
|
-
*/
|
|
751
|
-
closeInput() {
|
|
752
|
-
this._orphanedInjections = this.pendingMessages.length;
|
|
753
|
-
if (this._orphanedInjections > 0) {
|
|
754
|
-
logger.warn(`closeInput: ${this._orphanedInjections} injected message(s) orphaned`);
|
|
755
|
-
}
|
|
756
|
-
this.pendingMessages = [];
|
|
757
|
-
this.generatorClosed = true;
|
|
758
|
-
if (this.messageResolver) {
|
|
759
|
-
this.messageResolver();
|
|
760
|
-
this.messageResolver = null;
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
/**
|
|
764
|
-
* Returns true if injected messages were lost when the generator closed.
|
|
765
|
-
*/
|
|
766
|
-
get hasOrphanedInjections() {
|
|
767
|
-
return this._orphanedInjections > 0;
|
|
768
|
-
}
|
|
769
|
-
get isInputClosed() {
|
|
770
|
-
return this.generatorClosed;
|
|
771
|
-
}
|
|
772
|
-
/**
|
|
773
|
-
* Request that the current agent turn be stopped.
|
|
774
|
-
* Uses the SDK's AbortController to cleanly signal the running query to
|
|
775
|
-
* stop, then closes the input stream so the loop returns to idle.
|
|
776
|
-
*/
|
|
777
|
-
requestStop() {
|
|
778
|
-
this.stopRequested = true;
|
|
779
|
-
if (this.abortController) {
|
|
780
|
-
this.abortController.abort();
|
|
781
|
-
logger.system("Stop requested — aborting query via AbortController");
|
|
782
|
-
}
|
|
783
|
-
// IMPORTANT: Do NOT tear down the idle pre-warmed subprocess here.
|
|
784
|
-
// requestStop() is typically called to end the *current* turn (user
|
|
785
|
-
// /stop, watchdog timeout, stream silence, obs UI button); the user
|
|
786
|
-
// will usually start a new task shortly after. Preserving the idle
|
|
787
|
-
// warm lets that next task skip cold-start (~2-3s).
|
|
788
|
-
//
|
|
789
|
-
// The warm is a fully separate subprocess with its own AbortController
|
|
790
|
-
// — aborting `this.abortController` above does not touch it.
|
|
791
|
-
//
|
|
792
|
-
// For graceful shutdown: the warm is a child process of ours and will
|
|
793
|
-
// be reaped when we exit; the refresh timer uses `.unref()` so it
|
|
794
|
-
// doesn't block process exit.
|
|
795
|
-
this.closeInput();
|
|
796
|
-
}
|
|
797
|
-
isStopRequested() {
|
|
798
|
-
return this.stopRequested;
|
|
799
|
-
}
|
|
800
|
-
signalFinish() {
|
|
801
|
-
// Claude SDK stream ends naturally after the finish tool result —
|
|
802
|
-
// no early abort needed.
|
|
803
|
-
}
|
|
804
|
-
clearStop() {
|
|
805
|
-
this.stopRequested = false;
|
|
806
|
-
// Only null the *live query's* abort controller. The warm subprocess
|
|
807
|
-
// has its own abort controller that must survive the stop→clearStop
|
|
808
|
-
// cycle — sendAndStream() adopts it when consuming the warm.
|
|
809
|
-
this.abortController = null;
|
|
810
|
-
}
|
|
811
|
-
/**
|
|
812
|
-
* Update the persisted session ID.
|
|
813
|
-
*
|
|
814
|
-
* Only saves on first capture (null → id). Once set, a different ID from
|
|
815
|
-
* the SDK is ignored — it means the resume failed and the SDK created a
|
|
816
|
-
* throwaway conversation. Persisting it would corrupt session.json and
|
|
817
|
-
* orphan the real transcript.
|
|
818
|
-
*/
|
|
819
|
-
captureSessionId(id) {
|
|
820
|
-
if (!id || this.sessionId)
|
|
821
|
-
return;
|
|
822
|
-
this.sessionId = id;
|
|
823
|
-
saveSessionId(this.config, id, this.mode);
|
|
824
|
-
}
|
|
825
|
-
captureTranscriptPath(p) {
|
|
826
|
-
if (p && typeof p === "string") {
|
|
827
|
-
this.transcriptPath = p;
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
getSessionId() {
|
|
831
|
-
return this.sessionId;
|
|
832
|
-
}
|
|
833
|
-
/**
|
|
834
|
-
* Proactively request compaction by running the /compact slash command as a
|
|
835
|
-
* separate one-turn query resumed from the current session.
|
|
836
|
-
*
|
|
837
|
-
* This is more reliable than injecting "/compact" into the streaming prompt
|
|
838
|
-
* because the running query may treat it as ordinary text.
|
|
839
|
-
*/
|
|
840
|
-
async requestCompaction() {
|
|
841
|
-
if (this.compactInFlight)
|
|
842
|
-
return;
|
|
843
|
-
this.compactInFlight = true;
|
|
844
|
-
try {
|
|
845
|
-
getAgentState().compacting = true;
|
|
846
|
-
}
|
|
847
|
-
catch {
|
|
848
|
-
// Agent state not yet initialized
|
|
849
|
-
}
|
|
850
|
-
// TODO: maybe should use reply map.
|
|
851
|
-
const sendCompactNotice = (text) => {
|
|
852
|
-
try {
|
|
853
|
-
const state = getAgentState();
|
|
854
|
-
const { ownerConfig, channelManager } = state;
|
|
855
|
-
const channel = ownerConfig.telegramChatId ? "telegram" : ownerConfig.ownerEmail ? "gmail" : undefined;
|
|
856
|
-
const recipient = ownerConfig.telegramChatId ? String(ownerConfig.telegramChatId) : ownerConfig.ownerEmail;
|
|
857
|
-
if (channel && recipient) {
|
|
858
|
-
channelManager
|
|
859
|
-
.sendMessage(channel, recipient, text)
|
|
860
|
-
.catch((err) => {
|
|
861
|
-
logger.warn(`Failed to send compaction notification: ${err instanceof Error ? err.message : String(err)}`);
|
|
862
|
-
});
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
catch {
|
|
866
|
-
// Agent state not yet initialized — skip notification
|
|
867
|
-
}
|
|
868
|
-
};
|
|
869
|
-
try {
|
|
870
|
-
const savedSessionId = this.sessionId;
|
|
871
|
-
if (!savedSessionId) {
|
|
872
|
-
logger.warn("Cannot request /compact: session_id not available yet");
|
|
873
|
-
return;
|
|
874
|
-
}
|
|
875
|
-
// Resolve the Claude Code executable — same lookup as sendAndStream().
|
|
876
|
-
const cliResolution = resolveClaudeCliExecutable();
|
|
877
|
-
const options = {
|
|
878
|
-
model: getModelId(this.config),
|
|
879
|
-
...(cliResolution.kind !== "sdk-default"
|
|
880
|
-
? { pathToClaudeCodeExecutable: cliResolution.path }
|
|
881
|
-
: {}),
|
|
882
|
-
systemPrompt: this.buildSystemPrompt(),
|
|
883
|
-
permissionMode: "bypassPermissions",
|
|
884
|
-
allowDangerouslySkipPermissions: true,
|
|
885
|
-
cwd: this.sessionCwd,
|
|
886
|
-
settingSources: ["project"],
|
|
887
|
-
mcpServers: this.buildMcpServers(),
|
|
888
|
-
env: buildAgentEnv(this.config),
|
|
889
|
-
resume: savedSessionId,
|
|
890
|
-
maxTurns: 1,
|
|
891
|
-
};
|
|
892
|
-
logger.system("Requesting /compact...");
|
|
893
|
-
const isSilent = getAgentState().ownerConfig.silentCompaction;
|
|
894
|
-
if (!isSilent) {
|
|
895
|
-
sendCompactNotice("My brain is getting full... Let me tidy up my thoughts. This may take a minute — I might forget some older details.");
|
|
896
|
-
}
|
|
897
|
-
const cliLabel = cliResolution.kind === "sdk-default"
|
|
898
|
-
? "sdk-default"
|
|
899
|
-
: `${cliResolution.kind}:${cliResolution.path}`;
|
|
900
|
-
logger.debug(`Compaction query options: cli=${cliLabel} cwd=${this.sessionCwd} resume=${savedSessionId}`);
|
|
901
|
-
// Sanitize the transcript before compaction — the compact subprocess
|
|
902
|
-
// loads the file on startup, so any corrupt entries must be fixed first.
|
|
903
|
-
const tp = this.transcriptPath ?? (savedSessionId ? this.deriveTranscriptPath(savedSessionId) : null);
|
|
904
|
-
if (tp) {
|
|
905
|
-
try {
|
|
906
|
-
await imagePruner.sanitizeTranscript(tp);
|
|
907
|
-
}
|
|
908
|
-
catch (sanitizeErr) {
|
|
909
|
-
logger.warn(`Pre-compact sanitization failed: ${sanitizeErr instanceof Error ? sanitizeErr.message : String(sanitizeErr)}`);
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
// Guard against compaction hanging forever — abort after 5 minutes.
|
|
913
|
-
const COMPACTION_TIMEOUT_MS = 5 * 60_000;
|
|
914
|
-
const compactAbort = new AbortController();
|
|
915
|
-
const compactTimer = setTimeout(() => {
|
|
916
|
-
logger.warn(`[compact] Compaction timed out after ${COMPACTION_TIMEOUT_MS / 1000}s — aborting`);
|
|
917
|
-
compactAbort.abort();
|
|
918
|
-
}, COMPACTION_TIMEOUT_MS);
|
|
919
|
-
try {
|
|
920
|
-
for await (const msg of query({
|
|
921
|
-
prompt: "/compact",
|
|
922
|
-
options: { ...options, abortController: compactAbort },
|
|
923
|
-
})) {
|
|
924
|
-
if (msg.type === "system" && msg.subtype === "compact_boundary") {
|
|
925
|
-
// Capture session id (should remain the same) and log compaction metadata.
|
|
926
|
-
if ("session_id" in msg && msg.session_id) {
|
|
927
|
-
this.captureSessionId(msg.session_id);
|
|
928
|
-
}
|
|
929
|
-
const meta = msg.compact_metadata;
|
|
930
|
-
logger.system(`Compaction completed (trigger=${meta?.trigger ?? "unknown"}, pre_tokens=${meta?.pre_tokens ?? "?"})`);
|
|
931
|
-
if (typeof meta?.pre_tokens === "number") {
|
|
932
|
-
this.capturePostCompactionSnapshot(meta.pre_tokens);
|
|
933
|
-
}
|
|
934
|
-
else if (this.lastUsageSnapshot) {
|
|
935
|
-
// No token data available, but still reset the image count
|
|
936
|
-
// so maybeCompact() doesn't re-trigger on the stale value.
|
|
937
|
-
this.captureUsageSnapshot({
|
|
938
|
-
...this.lastUsageSnapshot,
|
|
939
|
-
allImageBlocks: 0,
|
|
940
|
-
});
|
|
941
|
-
}
|
|
942
|
-
if (!isSilent) {
|
|
943
|
-
sendCompactNotice("Brain cleanup done! Ready to continue.");
|
|
944
|
-
}
|
|
945
|
-
// Trim the JSONL transcript to remove pre-compaction content.
|
|
946
|
-
// This keeps the live file small for faster resumes and pruning.
|
|
947
|
-
const tp = this.transcriptPath;
|
|
948
|
-
if (tp) {
|
|
949
|
-
try {
|
|
950
|
-
const archivePath = sessionTrimmer.deriveArchivePath(tp);
|
|
951
|
-
await sessionTrimmer.trimTranscriptAfterCompaction({
|
|
952
|
-
transcriptPath: tp,
|
|
953
|
-
archivePath,
|
|
954
|
-
});
|
|
955
|
-
}
|
|
956
|
-
catch (trimErr) {
|
|
957
|
-
logger.warn(`Post-compaction JSONL trim failed: ${trimErr instanceof Error ? trimErr.message : String(trimErr)}`);
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
finally {
|
|
964
|
-
clearTimeout(compactTimer);
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
catch (err) {
|
|
968
|
-
logger.warn(`Request /compact failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
969
|
-
}
|
|
970
|
-
finally {
|
|
971
|
-
this.compactInFlight = false;
|
|
972
|
-
try {
|
|
973
|
-
getAgentState().compacting = false;
|
|
974
|
-
}
|
|
975
|
-
catch {
|
|
976
|
-
// Agent state not yet initialized
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
captureUsageSnapshot(snapshot) {
|
|
981
|
-
// Merge with previous snapshot so callers that only update token data
|
|
982
|
-
// don't clobber allImageBlocks, and vice-versa.
|
|
983
|
-
this.lastUsageSnapshot = {
|
|
984
|
-
...this.lastUsageSnapshot,
|
|
985
|
-
...snapshot,
|
|
986
|
-
capturedAtMs: snapshot.capturedAtMs ?? Date.now(),
|
|
987
|
-
};
|
|
988
|
-
saveUsageSnapshot(this.config, this.lastUsageSnapshot, this.mode);
|
|
989
|
-
}
|
|
990
|
-
/**
|
|
991
|
-
* Update the usage snapshot after the SDK performs mid-query auto-compaction.
|
|
992
|
-
* Uses `pre_tokens` (the post-compaction token count) and the last known
|
|
993
|
-
* context window to compute an accurate `usedPct`, preventing the post-query
|
|
994
|
-
* `maybeCompact` from triggering a redundant compaction.
|
|
995
|
-
*/
|
|
996
|
-
capturePostCompactionSnapshot(postCompactionTokens) {
|
|
997
|
-
const contextWindow = this.lastUsageSnapshot?.contextWindow ?? 0;
|
|
998
|
-
if (contextWindow <= 0 || postCompactionTokens < 0) {
|
|
999
|
-
this.lastUsageSnapshot = null;
|
|
1000
|
-
return;
|
|
1001
|
-
}
|
|
1002
|
-
this.captureUsageSnapshot({
|
|
1003
|
-
usedInputTokens: postCompactionTokens,
|
|
1004
|
-
contextWindow,
|
|
1005
|
-
usedPct: postCompactionTokens / contextWindow,
|
|
1006
|
-
allImageBlocks: 0,
|
|
1007
|
-
});
|
|
1008
|
-
}
|
|
1009
|
-
/**
|
|
1010
|
-
* Unified between-turn compaction gate. Triggers compaction when **either**:
|
|
1011
|
-
* 1. Token usage exceeds the context-window threshold, OR
|
|
1012
|
-
* 2. Total image blocks (real + placeholder) in the transcript reach
|
|
1013
|
-
* `imageThreshold`, approaching Claude API's 100-image limit.
|
|
1014
|
-
*
|
|
1015
|
-
* Both triggers share the same cooldown and in-flight guard so we never
|
|
1016
|
-
* run two back-to-back compactions in the same between-turn pass.
|
|
1017
|
-
*/
|
|
1018
|
-
async maybeCompact(options) {
|
|
1019
|
-
const explicitUsedPctThreshold = options?.usedPctThreshold;
|
|
1020
|
-
const cooldownMs = options?.cooldownMs ?? 10 * 60 * 1000;
|
|
1021
|
-
const imageThreshold = options?.imageThreshold ?? IMAGE_COMPACT_THRESHOLD;
|
|
1022
|
-
// --- guard: in-flight ---
|
|
1023
|
-
if (this.compactInFlight) {
|
|
1024
|
-
logger.debug("[compact] skip: compaction already in flight");
|
|
1025
|
-
return;
|
|
1026
|
-
}
|
|
1027
|
-
// --- guard: cooldown ---
|
|
1028
|
-
const nowMs = Date.now();
|
|
1029
|
-
const sinceLastCompactMs = nowMs - this.lastCompactRequestAtMs;
|
|
1030
|
-
if (sinceLastCompactMs <= cooldownMs) {
|
|
1031
|
-
logger.debug(`[compact] skip: cooldown active sinceLast=${sinceLastCompactMs}ms cooldown=${cooldownMs}ms`);
|
|
1032
|
-
return;
|
|
1033
|
-
}
|
|
1034
|
-
// --- trigger 1: token usage ---
|
|
1035
|
-
const snap = this.lastUsageSnapshot;
|
|
1036
|
-
// Caller-supplied override always wins. Otherwise auto-scale by
|
|
1037
|
-
// context window, falling back to the large-window default when no
|
|
1038
|
-
// snapshot is available yet.
|
|
1039
|
-
const effectiveUsedPctThreshold = explicitUsedPctThreshold ??
|
|
1040
|
-
(snap
|
|
1041
|
-
? getCompactUsedPctThreshold(snap.contextWindow, COMPACT_LARGE_WINDOW_USED_PCT_THRESHOLD)
|
|
1042
|
-
: COMPACT_LARGE_WINDOW_USED_PCT_THRESHOLD);
|
|
1043
|
-
let tokenTrigger = false;
|
|
1044
|
-
if (snap) {
|
|
1045
|
-
const snapshotAgeMs = nowMs - (snap.capturedAtMs ?? 0);
|
|
1046
|
-
logger.debug(`[compact] check: usedPct=${(snap.usedPct * 100).toFixed(1)}% threshold=${(effectiveUsedPctThreshold * 100).toFixed(1)}% used=${snap.usedInputTokens} window=${snap.contextWindow} snapshotAge=${snapshotAgeMs}ms`);
|
|
1047
|
-
tokenTrigger = snap.contextWindow > 0 && snap.usedPct >= effectiveUsedPctThreshold;
|
|
1048
|
-
}
|
|
1049
|
-
// --- trigger 2: image count ---
|
|
1050
|
-
const allImages = snap?.allImageBlocks ?? 0;
|
|
1051
|
-
const imageTrigger = allImages >= imageThreshold;
|
|
1052
|
-
if (!tokenTrigger && !imageTrigger) {
|
|
1053
|
-
if (snap) {
|
|
1054
|
-
logger.debug(`[compact] skip: under threshold (usedPct=${(snap.usedPct * 100).toFixed(1)}% threshold=${(effectiveUsedPctThreshold * 100).toFixed(1)}%, images=${allImages}/${imageThreshold})`);
|
|
1055
|
-
}
|
|
1056
|
-
else {
|
|
1057
|
-
logger.debug(`[compact] skip: no usage snapshot yet (images=${allImages}/${imageThreshold})`);
|
|
1058
|
-
}
|
|
1059
|
-
return;
|
|
1060
|
-
}
|
|
1061
|
-
const reason = [tokenTrigger && "tokens", imageTrigger && `images(${allImages}>=${imageThreshold})`]
|
|
1062
|
-
.filter(Boolean)
|
|
1063
|
-
.join("+");
|
|
1064
|
-
logger.system(`[compact] requesting compaction (reason=${reason})`);
|
|
1065
|
-
this.lastCompactRequestAtMs = nowMs;
|
|
1066
|
-
await this.requestCompaction();
|
|
1067
|
-
}
|
|
1068
|
-
getTranscriptPath() {
|
|
1069
|
-
return this.transcriptPath;
|
|
1070
|
-
}
|
|
1071
|
-
/**
|
|
1072
|
-
* Derive the transcript path from the session ID and config dir, even before
|
|
1073
|
-
* the SDK subprocess has started and reported it via hooks.
|
|
1074
|
-
*/
|
|
1075
|
-
deriveTranscriptPath(sessionId) {
|
|
1076
|
-
const dir = getClaudeTranscriptDirForProfileDir(getConfigDir());
|
|
1077
|
-
return path.join(dir, `${sessionId}.jsonl`);
|
|
1078
|
-
}
|
|
1079
|
-
getUsageSnapshot() {
|
|
1080
|
-
return this.lastUsageSnapshot ? { ...this.lastUsageSnapshot } : null;
|
|
1081
|
-
}
|
|
1082
|
-
/** Returns the live Query object, or null if no query is active. */
|
|
1083
|
-
getCurrentQuery() {
|
|
1084
|
-
return this.currentQuery;
|
|
1085
|
-
}
|
|
1086
|
-
/**
|
|
1087
|
-
* Validation adapter around `Query.getContextUsage()` (SDK 0.2.114+).
|
|
1088
|
-
*
|
|
1089
|
-
* The SDK's `Query` interface exposes `getContextUsage()` as a control
|
|
1090
|
-
* request over the streaming bridge. It returns a rich breakdown:
|
|
1091
|
-
*
|
|
1092
|
-
* ```ts
|
|
1093
|
-
* {
|
|
1094
|
-
* categories: { name: string; tokens: number; color: string; isDeferred?: boolean }[];
|
|
1095
|
-
* totalTokens: number;
|
|
1096
|
-
* maxTokens: number; // context window for the *active* model
|
|
1097
|
-
* rawMaxTokens: number; // nominal max before any reductions
|
|
1098
|
-
* percentage: number; // UI percentage (0..100)
|
|
1099
|
-
* gridRows: ...[]; // TUI-only
|
|
1100
|
-
* model: string;
|
|
1101
|
-
* memoryFiles: { path: string; type: string; tokens: number }[];
|
|
1102
|
-
* mcpTools: { name: string; serverName: string; tokens: number }[];
|
|
1103
|
-
* }
|
|
1104
|
-
* ```
|
|
1105
|
-
*
|
|
1106
|
-
* We map it onto our existing {@link UsageSnapshot} shape:
|
|
1107
|
-
* - `usedInputTokens` ← `totalTokens`
|
|
1108
|
-
* - `contextWindow` ← `maxTokens` (post-reduction; matches what the
|
|
1109
|
-
* model actually has to work with right now)
|
|
1110
|
-
* - `usedPct` ← computed from the two above, NOT from the
|
|
1111
|
-
* SDK's `percentage` field. The SDK's value is
|
|
1112
|
-
* UI-scale (0..100); our consumers expect 0..1.
|
|
1113
|
-
* Computing locally keeps both fields internally
|
|
1114
|
-
* consistent.
|
|
1115
|
-
* - `allImageBlocks` — intentionally preserved from any existing
|
|
1116
|
-
* snapshot; `getContextUsage` doesn't know about
|
|
1117
|
-
* raw image-block counts in the transcript, so
|
|
1118
|
-
* we don't clobber the value that
|
|
1119
|
-
* {@link captureUsageSnapshot} maintains.
|
|
1120
|
-
*
|
|
1121
|
-
* This is purely a read adapter — no callers in compaction or the
|
|
1122
|
-
* wake loop should depend on it yet. It exists to let us sanity-check
|
|
1123
|
-
* the live SDK response before wiring any behavior changes around it.
|
|
1124
|
-
* Returns null when no live query is active, when the method is absent
|
|
1125
|
-
* on the active Query instance (e.g. an older CLI), or when the call
|
|
1126
|
-
* errors transiently.
|
|
1127
|
-
*
|
|
1128
|
-
* TODO: we should update snapshot based on this.
|
|
1129
|
-
*/
|
|
1130
|
-
async getLiveContextUsage() {
|
|
1131
|
-
const q = this.currentQuery;
|
|
1132
|
-
if (!q) {
|
|
1133
|
-
logger.debug("[getLiveContextUsage] no active query");
|
|
1134
|
-
return null;
|
|
1135
|
-
}
|
|
1136
|
-
// The typed interface declares the method, but runtime code may
|
|
1137
|
-
// predate it if the user pins an older CLI via
|
|
1138
|
-
// `pathToClaudeCodeExecutable`. Probe defensively.
|
|
1139
|
-
const candidate = q.getContextUsage;
|
|
1140
|
-
if (typeof candidate !== "function") {
|
|
1141
|
-
logger.debug("[getLiveContextUsage] query.getContextUsage unavailable");
|
|
1142
|
-
return null;
|
|
1143
|
-
}
|
|
1144
|
-
try {
|
|
1145
|
-
const raw = (await candidate.call(q));
|
|
1146
|
-
logger.debug(`[getContextUsage] raw: totalTokens=${String(raw.totalTokens)} maxTokens=${String(raw.maxTokens)}`);
|
|
1147
|
-
const totalTokens = typeof raw.totalTokens === "number" ? raw.totalTokens : NaN;
|
|
1148
|
-
const maxTokens = typeof raw.maxTokens === "number" ? raw.maxTokens : NaN;
|
|
1149
|
-
if (!Number.isFinite(totalTokens) || !Number.isFinite(maxTokens) || maxTokens <= 0) {
|
|
1150
|
-
logger.debug(`[getLiveContextUsage] unexpected shape: totalTokens=${String(raw.totalTokens)} maxTokens=${String(raw.maxTokens)}`);
|
|
1151
|
-
return null;
|
|
1152
|
-
}
|
|
1153
|
-
const usage = {
|
|
1154
|
-
usedInputTokens: totalTokens,
|
|
1155
|
-
contextWindow: maxTokens,
|
|
1156
|
-
usedPct: totalTokens / maxTokens,
|
|
1157
|
-
allImageBlocks: this.lastUsageSnapshot?.allImageBlocks,
|
|
1158
|
-
capturedAtMs: Date.now(),
|
|
1159
|
-
};
|
|
1160
|
-
logger.debug(`[getLiveContextUsage] snapshot: usedPct=${(usage.usedPct * 100).toFixed(1)}% used=${usage.usedInputTokens} window=${usage.contextWindow}`);
|
|
1161
|
-
return usage;
|
|
1162
|
-
}
|
|
1163
|
-
catch (err) {
|
|
1164
|
-
logger.debug(`[getLiveContextUsage] call failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1165
|
-
return null;
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
/** Replace the full set of dynamic MCP servers (used when loading from disk). */
|
|
1169
|
-
setDynamicMcpServers(servers) {
|
|
1170
|
-
this.dynamicMcpServers = { ...servers };
|
|
1171
|
-
}
|
|
1172
|
-
/** Get the current dynamic MCP servers record. */
|
|
1173
|
-
getDynamicMcpServers() {
|
|
1174
|
-
return { ...this.dynamicMcpServers };
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
//# sourceMappingURL=session.js.map
|