mixdog 1.3.4 → 1.4.0
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/LICENSES/oh-my-pi-MIT.txt +23 -0
- package/NOTICE.md +17 -2
- package/README.md +11 -0
- package/package.json +3 -11
- package/scripts/daemon-stub.mjs +1 -1
- package/scripts/embed-google-oauth-client.mjs +49 -0
- package/scripts/gate-local.mjs +4 -2
- package/scripts/lib/test-spawn-ledger.mjs +46 -1
- package/scripts/native-release-status.mjs +2 -2
- package/scripts/recall-history-cases.json +1 -1
- package/scripts/suite-health.mjs +10 -1
- package/scripts/test.mjs +12 -11
- package/src/agents/reviewer/AGENT.md +7 -11
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +25 -1
- package/src/defaults/skills/browser-use/SKILL.md +77 -17
- package/src/defaults/skills/code-tidy/SKILL.md +142 -270
- package/src/defaults/skills/code-tidy/references/agent-cleanup.md +4 -4
- package/src/defaults/skills/code-tidy/references/cleanup-report.md +31 -76
- package/src/defaults/skills/code-tidy/references/dead-code.md +6 -12
- package/src/defaults/skills/computer-use/SKILL.md +33 -9
- package/src/defaults/skills/setup/SKILL.md +29 -2
- package/src/defaults/skills/setup/references/actions.md +117 -28
- package/src/defaults/skills/setup/references/surfaces.md +77 -35
- package/src/defaults/skills/setup-guide/SKILL.md +3 -3
- package/src/defaults/skills/visualize/SKILL.md +2 -1
- package/src/lib/keychain-cjs.cjs +359 -29
- package/src/lib/rules-builder.cjs +10 -8
- package/src/rules/agent/AGENT.md +6 -6
- package/src/rules/agent-mode/AGENT_MODE.md +10 -2
- package/src/rules/agent-mode/MIXY.md +12 -0
- package/src/rules/lead/LEAD.md +18 -20
- package/src/rules/shared/05-tool-calls.md +5 -6
- package/src/rules/shared/10-tool-workflow.md +2 -0
- package/src/rules/shared/60-verification.md +7 -5
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -3
- package/src/runtime/agent/orchestrator/agent-runtime/heartbeat-completion.mjs +193 -0
- package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +3 -1
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +3 -1
- package/src/runtime/agent/orchestrator/config.mjs +34 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +3 -0
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +72 -67
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +54 -1
- package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +3 -3
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +177 -73
- package/src/runtime/agent/orchestrator/mcp/client.mjs +25 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +31 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +16 -3
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +42 -7
- package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +21 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +23 -0
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +11 -1
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +230 -137
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +64 -7
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +18 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-cache-warm.mjs +165 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/gzip-post.mjs +14 -14
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +160 -69
- package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +25 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +64 -33
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +135 -87
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +52 -8
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +10 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +22 -6
- package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +15 -4
- package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +17 -2
- package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +142 -104
- package/src/runtime/agent/orchestrator/providers/cache-warm-admission.mjs +90 -0
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +10 -4
- package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +10 -217
- package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +25 -9
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +22 -6
- package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +409 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-families.mjs +187 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-identity.mjs +342 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-models.mjs +541 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-protobuf.mjs +305 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-request.mjs +558 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-schema.mjs +880 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-session.mjs +190 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-tool-args.mjs +351 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +560 -0
- package/src/runtime/agent/orchestrator/providers/devin.mjs +491 -0
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/gemini-output-limit.mjs +46 -0
- package/src/runtime/agent/orchestrator/providers/gemini-reasoning-relay.mjs +30 -0
- package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +7 -11
- package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +61 -22
- package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -4
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +96 -5
- package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +23 -5
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +52 -10
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +8 -1
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/npm-cli-version.mjs +11 -3
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +39 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-send/send-context.mjs +22 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +43 -5
- package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/handshake-failure.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/stream-failure.mjs +6 -3
- package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/provider-availability.mjs +19 -0
- package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +65 -1
- package/src/runtime/agent/orchestrator/providers/quota-reading.mjs +38 -12
- package/src/runtime/agent/orchestrator/providers/registry.mjs +43 -3
- package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +39 -7
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/wire-protobuf-codec.mjs +368 -0
- package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +12 -7
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/compaction-config-keys.mjs +62 -0
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +37 -16
- package/src/runtime/agent/orchestrator/runtime-core/deferred-tool-delta.mjs +100 -23
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +1 -1
- package/src/runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs +133 -0
- package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +19 -4
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +125 -30
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +23 -5
- package/src/runtime/agent/orchestrator/runtime-core/tool-profile.mjs +35 -10
- package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +57 -10
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +9 -6
- package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +0 -3
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +10 -4
- package/src/runtime/agent/orchestrator/session/cache-warm.mjs +792 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +148 -25
- package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +475 -58
- package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +42 -17
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +47 -9
- package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -4
- package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +14 -2
- package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +18 -22
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +32 -3
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +51 -3
- package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +17 -3
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +10 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/after-hook.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +16 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -10
- package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +23 -5
- package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +60 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +19 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +133 -90
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-state.mjs +31 -4
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +30 -18
- package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +23 -13
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/manager/route-fallback-chain.mjs +129 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +20 -2
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -2
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +102 -15
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +18 -5
- package/src/runtime/agent/orchestrator/session/message-ids.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/message-reactions.mjs +89 -0
- package/src/runtime/agent/orchestrator/session/message-reactions.test-support.mjs +75 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +19 -8
- package/src/runtime/agent/orchestrator/session/prompt-cache-idle-gaps.mjs +113 -0
- package/src/runtime/agent/orchestrator/session/prompt-cache-touch-store.mjs +127 -0
- package/src/runtime/agent/orchestrator/session/routine-result.d.mts +0 -2
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +47 -11
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -2
- package/src/runtime/agent/orchestrator/session/sensitive-projection.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/sensitive-values.mjs +257 -0
- package/src/runtime/agent/orchestrator/session/side-question-store.mjs +20 -7
- package/src/runtime/agent/orchestrator/session/side-question.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/skill-update-notice.mjs +60 -0
- package/src/runtime/agent/orchestrator/session/store/live-state.mjs +6 -3
- package/src/runtime/agent/orchestrator/session/store/save-delta.mjs +9 -5
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +9 -8
- package/src/runtime/agent/orchestrator/session/store/session-persistence-events.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-header-cache.mjs +85 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +133 -18
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +12 -2
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -14
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +84 -3
- package/src/runtime/agent/orchestrator/session/stored-attachment-history.mjs +32 -0
- package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +11 -5
- package/src/runtime/agent/orchestrator/session/thinking-binding.mjs +108 -0
- package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/tool-batch/record-outcome.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +25 -2
- package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +9 -7
- package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-text.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/turn-failure.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/misuse-guard.mjs +240 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/result-format.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/shell-cwd.mjs +38 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +25 -5
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +65 -7
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +36 -13
- package/src/runtime/browser-bridge/action-schema.mjs +13 -2
- package/src/runtime/browser-bridge/client.mjs +73 -8
- package/src/runtime/browser-bridge/input-fields.mjs +8 -9
- package/src/runtime/browser-bridge/mail-links.mjs +107 -0
- package/src/runtime/browser-bridge/tool-defs.mjs +1 -1
- package/src/runtime/channels/lib/config.mjs +6 -61
- package/src/runtime/channels/lib/crash-log.mjs +1 -1
- package/src/runtime/channels/lib/event-pipeline.mjs +4 -19
- package/src/runtime/channels/lib/event-queue.mjs +3 -18
- package/src/runtime/channels/lib/owned-runtime/automation-services.mjs +2 -2
- package/src/runtime/channels/lib/owned-runtime/automation-start.mjs +3 -4
- package/src/runtime/channels/lib/owned-runtime/config-reload.mjs +4 -59
- package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +33 -115
- package/src/runtime/channels/lib/owned-runtime/owned-stop.mjs +5 -22
- package/src/runtime/channels/lib/owned-runtime/state.mjs +2 -8
- package/src/runtime/channels/lib/owned-runtime.mjs +6 -23
- package/src/runtime/channels/lib/parent-bridge.mjs +2 -2
- package/src/runtime/channels/lib/runtime-paths.mjs +4 -43
- package/src/runtime/channels/lib/schedule-errors.mjs +51 -0
- package/src/runtime/channels/lib/scheduler.mjs +135 -154
- package/src/runtime/channels/lib/schedules-db.mjs +83 -11
- package/src/runtime/channels/lib/status-snapshot.mjs +1 -1
- package/src/runtime/channels/lib/tool-dispatch.mjs +14 -2
- package/src/runtime/channels/lib/webhooks-db.mjs +5 -9
- package/src/runtime/channels/lib/worker-bootstrap.mjs +0 -1
- package/src/runtime/channels/lib/worker-ipc.mjs +0 -1
- package/src/runtime/channels/lib/worker-main.mjs +19 -34
- package/src/runtime/computer-bridge/action-schema.mjs +42 -1
- package/src/runtime/computer-bridge/actions.mjs +9 -1
- package/src/runtime/computer-bridge/client.mjs +180 -8
- package/src/runtime/computer-bridge/core-actions.mjs +2 -2
- package/src/runtime/computer-bridge/error-recovery.mjs +27 -2
- package/src/runtime/computer-bridge/linux-readiness.mjs +65 -23
- package/src/runtime/computer-bridge/pending-continuation.mjs +1 -1
- package/src/runtime/connectors/connector-accounts-mcp.mjs +230 -0
- package/src/runtime/connectors/gmail-app-password.mjs +172 -39
- package/src/runtime/connectors/google-mcp.mjs +10 -12
- package/src/runtime/memory/lib/pg/adapter.mjs +77 -70
- package/src/runtime/memory/lib/pg/supervisor.mjs +24 -12
- package/src/runtime/memory/lib/runtime-fetcher.mjs +23 -5
- package/src/runtime/office/com/com-adapter.mjs +17 -1
- package/src/runtime/office/com/office-com-host.ps1 +51 -1
- package/src/runtime/office/pdf/cfb-fixture.mjs +328 -0
- package/src/runtime/office/pdf/cfb-protection.mjs +311 -0
- package/src/runtime/office/pdf/document-preview.mjs +326 -8
- package/src/runtime/office/pdf/ooxml-crypto.mjs +469 -0
- package/src/runtime/office/pdf/ooxml-key-worker.mjs +14 -0
- package/src/runtime/office/portable/portable-soffice.mjs +45 -5
- package/src/runtime/session-search/session-search-db.mjs +73 -9
- package/src/runtime/session-search/session-search-ingest.mjs +110 -20
- package/src/runtime/shared/abort-race.mjs +89 -0
- package/src/runtime/shared/agent-avatar.d.mts +0 -1
- package/src/runtime/shared/agent-kind.mjs +4 -3
- package/src/runtime/shared/bot-hops.mjs +62 -2
- package/src/runtime/shared/bounded-download.mjs +8 -3
- package/src/runtime/shared/config.mjs +51 -35
- package/src/runtime/shared/connector-accounts.mjs +20 -10
- package/src/runtime/shared/connector-catalog.d.mts +33 -2
- package/src/runtime/shared/connector-catalog.mjs +166 -8
- package/src/runtime/shared/connector-triggers.mjs +22 -19
- package/src/runtime/shared/defaults-separation.mjs +2 -14
- package/src/runtime/shared/delegated-approval-host.mjs +103 -11
- package/src/runtime/shared/developer-options.mjs +32 -1
- package/src/runtime/shared/err-text.mjs +24 -15
- package/src/runtime/shared/file-permissions.d.mts +0 -8
- package/src/runtime/shared/file-permissions.mjs +0 -11
- package/src/runtime/shared/git-direct.test-support.mjs +19 -0
- package/src/runtime/shared/google-oauth-client.generated.json +4 -0
- package/src/runtime/shared/google-oauth-client.mjs +34 -9
- package/src/runtime/shared/heartbeat-interval.d.mts +4 -0
- package/src/runtime/shared/heartbeat-interval.mjs +19 -0
- package/src/runtime/shared/idle-auto-clear-gate.mjs +104 -0
- package/src/runtime/shared/llm/anthropic-betas.mjs +7 -0
- package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +12 -0
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +1 -0
- package/src/runtime/shared/llm/usage-rollup.mjs +1 -1
- package/src/runtime/shared/llm/well-formed.mjs +66 -0
- package/src/runtime/shared/mixy-presence.mjs +22 -0
- package/src/runtime/shared/orchestration.d.mts +2 -0
- package/src/runtime/shared/pg-schema-db.mjs +27 -16
- package/src/runtime/shared/profile-config.cjs +1 -1
- package/src/runtime/shared/prompt-origin.d.mts +4 -0
- package/src/runtime/shared/prompt-origin.mjs +21 -0
- package/src/runtime/shared/provider-accounts.mjs +231 -59
- package/src/runtime/shared/provider-api-key.mjs +19 -8
- package/src/runtime/shared/provider-display-name.mjs +29 -0
- package/src/runtime/shared/provider-timeout-phase.mjs +2 -0
- package/src/runtime/shared/routine-trigger.d.mts +2 -0
- package/src/runtime/shared/routine-trigger.mjs +52 -0
- package/src/runtime/shared/sensitive-path-rules.d.mts +2 -10
- package/src/runtime/shared/sensitive-path-rules.mjs +19 -41
- package/src/runtime/shared/session-activity-signal.mjs +34 -1
- package/src/runtime/shared/session-sidebar-host.d.mts +1 -1
- package/src/runtime/shared/session-sidebar-host.mjs +7 -3
- package/src/runtime/shared/session-turn-prelude.mjs +18 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -0
- package/src/runtime/shared/tool-result-summary.mjs +1 -14
- package/src/runtime/shared/transcript-metadata.mjs +38 -0
- package/src/runtime/shared/user-data-guard.mjs +30 -2
- package/src/runtime/shared/worktrees/activity-gate.mjs +12 -5
- package/src/runtime/shared/worktrees/test-support.mjs +9 -4
- package/src/runtime/terminal-bridge/client.mjs +10 -1
- package/src/runtime/visualize-bridge/client.mjs +27 -8
- package/src/runtime/visualize-bridge/tool-defs.mjs +22 -8
- package/src/runtime/visualize-bridge/tool-meta.d.mts +5 -0
- package/src/runtime/visualize-bridge/tool-meta.mjs +22 -0
- package/src/runtime/web-search/lib/fetch-pipeline.mjs +2 -2
- package/src/session-runtime/agent-home-delivery.mjs +486 -179
- package/src/session-runtime/agent-room-delivery.mjs +28 -1
- package/src/session-runtime/agent-task-sessions.mjs +561 -199
- package/src/session-runtime/boot/apis.mjs +9 -0
- package/src/session-runtime/boot/begin.mjs +3 -2
- package/src/session-runtime/boot/infrastructure.mjs +4 -0
- package/src/session-runtime/boot/providers.mjs +3 -0
- package/src/session-runtime/boot/tools.mjs +8 -1
- package/src/session-runtime/bot-chain.mjs +32 -0
- package/src/session-runtime/bot-status.mjs +7 -4
- package/src/session-runtime/channel-config-api.mjs +63 -17
- package/src/session-runtime/config-lifecycle.mjs +6 -0
- package/src/session-runtime/connector-merge-migration.mjs +305 -0
- package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +132 -28
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +2 -0
- package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +45 -0
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +72 -5
- package/src/session-runtime/internal-tool-executor.mjs +4 -0
- package/src/session-runtime/lifecycle/inheritance.mjs +16 -3
- package/src/session-runtime/lifecycle/session-catalog.mjs +3 -1
- package/src/session-runtime/lifecycle/teardown/runtime-stops.mjs +50 -4
- package/src/session-runtime/lifecycle/teardown.mjs +23 -5
- package/src/session-runtime/mcp-glue/server-catalog.mjs +61 -3
- package/src/session-runtime/mcp-oauth-login.mjs +5 -3
- package/src/session-runtime/mixy-heartbeat.mjs +927 -0
- package/src/session-runtime/model-route/route-tuning.mjs +10 -6
- package/src/session-runtime/notification-bus.mjs +31 -117
- package/src/session-runtime/notification-completion-queue.mjs +54 -0
- package/src/session-runtime/notification-listeners.mjs +76 -0
- package/src/session-runtime/owned-automation-run.mjs +35 -13
- package/src/session-runtime/prewarm/channel-start.mjs +3 -7
- package/src/session-runtime/provider-auth/login.mjs +7 -0
- package/src/session-runtime/provider-models/row-hydration.mjs +13 -2
- package/src/session-runtime/resource-mcp-api.mjs +143 -224
- package/src/session-runtime/resource-mcp-connector-accounts.mjs +163 -0
- package/src/session-runtime/resource-mcp-oauth-api.mjs +217 -0
- package/src/session-runtime/resource-mcp-secrets.mjs +82 -0
- package/src/session-runtime/resource-mcp-servers.mjs +78 -0
- package/src/session-runtime/resource-mcp-store.mjs +49 -0
- package/src/session-runtime/routine-home-turn.mjs +33 -6
- package/src/session-runtime/routine-owner.mjs +40 -9
- package/src/session-runtime/schedule-session-run.mjs +7 -3
- package/src/session-runtime/services/agent-tool/close-flow.mjs +3 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +8 -12
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +7 -2
- package/src/session-runtime/services/agent-tool/tag-registry.mjs +1 -0
- package/src/session-runtime/services/agent-tool/worker-index/awaiting-user-sync.mjs +51 -0
- package/src/session-runtime/services/agent-tool/worker-index/disposal.mjs +55 -0
- package/src/session-runtime/services/agent-tool/worker-index/released-store.mjs +123 -0
- package/src/session-runtime/services/agent-tool/worker-index/restart-settlement.mjs +112 -0
- package/src/session-runtime/services/agent-tool/worker-index/row-reads.mjs +36 -0
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -0
- package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +17 -4
- package/src/session-runtime/services/agent-tool/worker-index/row-writes.mjs +58 -0
- package/src/session-runtime/services/agent-tool/worker-index.mjs +41 -241
- package/src/session-runtime/services/agent-tool/worker-rows.mjs +3 -0
- package/src/session-runtime/services/channel-admin.mjs +73 -31
- package/src/session-runtime/services/channel-session-router.mjs +4 -2
- package/src/session-runtime/services/channel-worker.mjs +1 -1
- package/src/session-runtime/services/provider-admin.mjs +128 -25
- package/src/session-runtime/services/schedule-access.mjs +14 -0
- package/src/session-runtime/services/seeds.mjs +0 -28
- package/src/session-runtime/services/usage-dashboard-model.mjs +1 -0
- package/src/session-runtime/session-compaction-sync.mjs +27 -0
- package/src/session-runtime/session-create/session-options.mjs +8 -4
- package/src/session-runtime/settings-builtin-tools-api.mjs +11 -0
- package/src/session-runtime/settings-compaction-api.mjs +2 -40
- package/src/session-runtime/settings-system-api.mjs +28 -1
- package/src/session-runtime/setup-tool/agent-settings-actions.mjs +65 -0
- package/src/session-runtime/setup-tool/executor.mjs +187 -29
- package/src/session-runtime/setup-tool/extended-actions.mjs +33 -2
- package/src/session-runtime/setup-tool/session-route-actions.mjs +152 -0
- package/src/session-runtime/setup-tool/settings-contract.mjs +68 -6
- package/src/session-runtime/setup-tool/tool-defs.mjs +92 -6
- package/src/session-runtime/task-session-route.mjs +99 -0
- package/src/session-runtime/tool-policy/agent-profile-fold.mjs +17 -13
- package/src/session-runtime/tool-policy/policy-stage.mjs +21 -4
- package/src/session-runtime/tool-policy/surface-helpers.mjs +2 -3
- package/src/session-runtime/tool-surface.mjs +4 -0
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +98 -21
- package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +4 -0
- package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -1
- package/src/session-runtime/workspace/transitions.mjs +10 -2
- package/src/standalone/channel-transport/client-registry/client-records.mjs +65 -0
- package/src/standalone/channel-transport/client-registry/client-registration.mjs +118 -0
- package/src/standalone/channel-transport/client-registry.mjs +24 -163
- package/src/standalone/channel-transport.mjs +1 -1
- package/src/standalone/daemon-channels-loader.mjs +10 -8
- package/src/standalone/daemon-session-control/list-page.mjs +72 -0
- package/src/standalone/daemon-session-control/request.mjs +51 -0
- package/src/standalone/daemon-session-control/room-host.mjs +57 -0
- package/src/standalone/daemon-session-control/route-control.mjs +48 -0
- package/src/standalone/daemon-session-control/send-turn.mjs +60 -0
- package/src/standalone/daemon-session-control/worktree-control.mjs +40 -0
- package/src/standalone/daemon-session-control.mjs +161 -157
- package/src/standalone/daemon.mjs +33 -2
- package/src/standalone/session-client.mjs +7 -3
- package/src/standalone/session-protocol.mjs +10 -0
- package/src/standalone/session-service/agent-tree/agent-cancel.mjs +6 -2
- package/src/standalone/session-service/agent-tree/agent-child.mjs +1 -0
- package/src/standalone/session-service/quota-resume.mjs +3 -0
- package/src/standalone/session-service/retention/busy.mjs +65 -1
- package/src/standalone/session-service/session-calls/session-action.mjs +23 -2
- package/src/standalone/session-service/session-calls/session-turns.mjs +6 -0
- package/src/standalone/session-service/session-calls/session-views.mjs +8 -0
- package/src/standalone/session-service.mjs +13 -0
- package/src/standalone/session-tool-output.mjs +21 -10
- package/src/tui/app/model-options.mjs +4 -27
- package/src/tui/dist/index.mjs +81 -65
- package/src/tui/session/agent-job-feed/model-notification.mjs +3 -0
- package/src/tui/session/initial-state.mjs +3 -0
- package/src/tui/session/notice-surface.mjs +23 -1
- package/src/tui/session/queue-helpers.mjs +3 -42
- package/src/tui/session/session-api/integrations.mjs +1 -0
- package/src/tui/session/session-api/lifecycle.mjs +8 -0
- package/src/tui/session/session-api/routes-agent-definitions.mjs +14 -2
- package/src/tui/session/session-api/routes-write.mjs +5 -1
- package/src/tui/session/session-api/routes.mjs +1 -0
- package/src/tui/session/session-api/settings.mjs +30 -2
- package/src/tui/session/session-flow/auto-clear.mjs +4 -33
- package/src/tui/session/session-flow/drain-closed-queue.mjs +38 -0
- package/src/tui/session/session-flow/drain-enqueue.mjs +86 -0
- package/src/tui/session/session-flow/drain-retry.mjs +35 -0
- package/src/tui/session/session-flow/drain.mjs +118 -149
- package/src/tui/session/session-flow/queue/entry-shape.mjs +5 -0
- package/src/tui/session/session-flow/queue/take-entries.mjs +5 -2
- package/src/tui/session/tool-approval-delegation.mjs +64 -0
- package/src/tui/session/tool-approval-request.mjs +39 -0
- package/src/tui/session/tool-approval-settle.mjs +62 -0
- package/src/tui/session/tool-approval.mjs +69 -115
- package/src/tui/session/tui-steering-persist.mjs +8 -1
- package/src/tui/session/turn-stream.mjs +36 -3
- package/src/tui/session/turn.mjs +23 -2
- package/src/vendor/statusline/bin/statusline-route.mjs +1 -219
- package/src/vendor/statusline/src/gateway/route-meta.mjs +35 -13
- package/scripts/code-graph-description-contract.mjs +0 -188
- package/src/runtime/channels/lib/owned-runtime/ownership-refresh.mjs +0 -36
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
// cache-warm.mjs — keeps a session's prompt cache readable for its next turn
|
|
2
|
+
// — every session, Lead, bot or sub-agent, while warming is on
|
|
3
|
+
// (`promptCacheWarming`, default on) — one interval at a time while the
|
|
4
|
+
// session idles, as far as the session's own history of returns says it
|
|
5
|
+
// pays. A warm re-sends the session's last real request
|
|
6
|
+
// (side-question-store.mjs) through the provider's cache-warm mode
|
|
7
|
+
// (providers/anthropic-cache-warm.mjs: the provider's own request builder,
|
|
8
|
+
// max_tokens 1, one POST with no retry, fallback or account rotation, cut at
|
|
9
|
+
// the first content block), so the provider reads and refreshes the cached
|
|
10
|
+
// prefix and writes nothing. The warm never reaches the transcript, the UI,
|
|
11
|
+
// the admission queue or the session record (lastProviderSendAt included);
|
|
12
|
+
// its usage-ledger row carries the `cache-warm` source, written on success,
|
|
13
|
+
// failure and cancellation.
|
|
14
|
+
//
|
|
15
|
+
// Scope: explicit-breakpoint providers (cache-strategy.mjs) with a warm mode —
|
|
16
|
+
// a miss costs a write premium there; automatic-caching providers are left
|
|
17
|
+
// alone. A request whose thinking budget derives from max_tokens is never
|
|
18
|
+
// replayed (a different budget changes the message cache key). A session
|
|
19
|
+
// that never idles long enough to return within a refresh interval (a
|
|
20
|
+
// sub-agent, closed when its task ends) learns no return and gets no warm.
|
|
21
|
+
//
|
|
22
|
+
// Rule (planCacheWarm), one for every warm, at each refresh point (one
|
|
23
|
+
// interval I = T − 60 s after the last confirmed cache use; T is
|
|
24
|
+
// the tier the provider reported writing), e the session's idle time since
|
|
25
|
+
// its last real call: warm iff, for some j ≥ 1
|
|
26
|
+
// further warms, P(e, j) · missCost > j · warmCost — missCost a write of the
|
|
27
|
+
// prompt at that tier minus the read the turn makes anyway, warmCost a read
|
|
28
|
+
// of it plus the one output token, and P(e, j) the lower bound of the 95%
|
|
29
|
+
// Wilson interval of this session's own recorded idle gaps (idle-gap store:
|
|
30
|
+
// the last 7 days) that ended in a return within (e, e + j·I] among those
|
|
31
|
+
// still idle at e. j = 1 is the per-interval break-even; a larger j is the
|
|
32
|
+
// same break-even over the warms a return further out needs (a 3-hourly
|
|
33
|
+
// routine is idle through two refresh points before it returns). No history
|
|
34
|
+
// → no warm → plain auto-clear. Returns count up to the idle window
|
|
35
|
+
// (IDLE_RETURN_MEASURED_WINDOW_MS after the last real call). A successful
|
|
36
|
+
// warm counts as cache use for the idle auto-clear (sessionActivityTimestamp
|
|
37
|
+
// in idle-auto-clear-gate.mjs), so its deadline moves with each one.
|
|
38
|
+
// Any fact the rule needs that is not known (written tier, prices, prompt
|
|
39
|
+
// size, the account holding the entry) means no warm. The chain stops on a
|
|
40
|
+
// miss, a failure, a late timer, a turn in flight, a new real request
|
|
41
|
+
// (side-question-store.mjs cancels), a compaction, a clear, a close, or a
|
|
42
|
+
// delete; the auto-clear then runs from the last real or successful warm
|
|
43
|
+
// use.
|
|
44
|
+
//
|
|
45
|
+
// Idle gaps, across turn boundaries only and from real activity only: every
|
|
46
|
+
// settled main-loop request of a real turn (any provider) moves the session's
|
|
47
|
+
// idle anchor to that request's send; a returning turn (the user's, a
|
|
48
|
+
// scheduled fire's, a webhook's, a heartbeat's, a sub-agent dispatch) closes
|
|
49
|
+
// the gap at its start once its first request goes out (noteCacheWarmTurn,
|
|
50
|
+
// noteCacheWarmActivity). Requests inside a running turn and a retried
|
|
51
|
+
// (continued) turn are no return. System-caused requests are neither return
|
|
52
|
+
// nor activity: turns the runtime starts itself (cacheWarmTurnKind), and
|
|
53
|
+
// every call outside the main loop's capture (compaction summaries, titles,
|
|
54
|
+
// /btw side questions, memory recap, prewarms, warms). The planner counts
|
|
55
|
+
// idle time from the same anchor. Each session learns from its own gaps. A gap still open at the idle window is recorded as
|
|
56
|
+
// censored there.
|
|
57
|
+
import { ACCOUNT_PROVIDERS } from '../../../shared/provider-accounts.mjs';
|
|
58
|
+
import { setCacheWarmUseSource } from '../../../shared/idle-auto-clear-gate.mjs';
|
|
59
|
+
import {
|
|
60
|
+
ANTHROPIC_TAIL_LIFETIME_MS,
|
|
61
|
+
cacheCapabilityForProvider,
|
|
62
|
+
providerTailCacheLifetimeMs,
|
|
63
|
+
} from '../agent-runtime/cache-strategy.mjs';
|
|
64
|
+
import { anthropicEffortLevelsFor } from '../../../shared/llm/anthropic-effort.mjs';
|
|
65
|
+
import { isTurnInFlight, runtimeState } from './manager/runtime-state.mjs';
|
|
66
|
+
import { withTailReminder } from './tail-reminder-rules.mjs';
|
|
67
|
+
import { promptCacheTouchStore } from './prompt-cache-touch-store.mjs';
|
|
68
|
+
import { createIdleGapStore } from './prompt-cache-idle-gaps.mjs';
|
|
69
|
+
import { registerSessionPurgeHook } from './store/session-observers.mjs';
|
|
70
|
+
import { PROMPT_ORIGIN_AGENT_REPORT, PROMPT_ORIGIN_CHANNEL, promptOriginOf } from '../../../shared/prompt-origin.mjs';
|
|
71
|
+
|
|
72
|
+
const CACHE_WARM_SOURCE = 'cache-warm';
|
|
73
|
+
|
|
74
|
+
// The refresh goes out this long before the entry's expiry, for both tiers
|
|
75
|
+
// (5m → 240 s after the last cache use, 1h → 3540 s): in a 7-day sample of
|
|
76
|
+
// hourly routine fires, cache misses inside the 1h lifetime appeared only at
|
|
77
|
+
// 3588–3597 s after the previous call, so 60 s ahead stays clear of them
|
|
78
|
+
// (git 795bb9e36's message; the sample itself is not kept in the repo). An
|
|
79
|
+
// entry with less than this left is not worth arming either: its refresh
|
|
80
|
+
// point has passed.
|
|
81
|
+
export const CACHE_WARM_LEAD_MS = 60_000;
|
|
82
|
+
// The idle stretch returns are learned over: 12 h after the session's last
|
|
83
|
+
// real call. Origin: the 7-day ledger measurement of idle-return chances
|
|
84
|
+
// (git 39efd623e, which first stated it) covered returns within 12 h of a
|
|
85
|
+
// session's last real call, "with the auto-clear window lifted to 12 h"; the
|
|
86
|
+
// per-session learner (git d284ef970) kept that window and attributed it to a
|
|
87
|
+
// "maintainer audit" that no commit or doc records beyond that measurement.
|
|
88
|
+
// A longer gap is recorded as censored there, and no warm serves a return
|
|
89
|
+
// beyond it.
|
|
90
|
+
export const IDLE_RETURN_MEASURED_WINDOW_MS = 12 * 60 * 60 * 1000;
|
|
91
|
+
// The period a session's idle gaps are kept and learned from: the last 7
|
|
92
|
+
// days — the length of the same ledger measurement (git 39efd623e,
|
|
93
|
+
// fe0271047: "a 7-day ledger sample"); no further derivation is recorded.
|
|
94
|
+
export const IDLE_RETURN_LOOKBACK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
95
|
+
// z of a two-sided 95% normal interval (the 0.975 standard normal quantile),
|
|
96
|
+
// for the Wilson score bound.
|
|
97
|
+
const WILSON_Z_95 = 1.959963984540054;
|
|
98
|
+
|
|
99
|
+
/** Whether prompt-cache warming is on (config `promptCacheWarming`, default
|
|
100
|
+
* on; config.mjs migrates the earlier target list). */
|
|
101
|
+
export function cacheWarmingEnabled(config) {
|
|
102
|
+
return config?.promptCacheWarming !== false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The lower bound of the 95% Wilson score interval of `hits` in `n`
|
|
106
|
+
* (0 for no trials): small samples get no invented confidence. */
|
|
107
|
+
export function wilsonLowerBound(hits, n, z = WILSON_Z_95) {
|
|
108
|
+
// No trials or no hits: the bound is exactly 0 (the formula's float
|
|
109
|
+
// rounding would leave ~1e-17).
|
|
110
|
+
if (!(n > 0) || !(hits > 0)) return 0;
|
|
111
|
+
const p = hits / n;
|
|
112
|
+
const z2 = z * z;
|
|
113
|
+
const centre = p + z2 / (2 * n);
|
|
114
|
+
const spread = z * Math.sqrt((p * (1 - p)) / n + z2 / (4 * n * n));
|
|
115
|
+
return Math.max(0, (centre - spread) / (1 + z2 / n));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The chance, learned from a session's own idle `gaps` (`{ ms, censored? }`),
|
|
120
|
+
* that a session idle for `elapsedMs` returns within `withinMs` more: the
|
|
121
|
+
* Wilson lower bound of the gaps that ended in a return in
|
|
122
|
+
* (elapsedMs, elapsedMs + withinMs] among the gaps still idle at elapsedMs
|
|
123
|
+
* (a censored gap is idle up to its cut and never a return).
|
|
124
|
+
* `{ idle, returns, p }`.
|
|
125
|
+
*/
|
|
126
|
+
export function sessionReturnChance(gaps, elapsedMs, withinMs) {
|
|
127
|
+
let idle = 0;
|
|
128
|
+
let returns = 0;
|
|
129
|
+
for (const gap of gaps || []) {
|
|
130
|
+
if (!(gap.ms > elapsedMs)) continue;
|
|
131
|
+
idle += 1;
|
|
132
|
+
if (!gap.censored && gap.ms <= elapsedMs + withinMs) returns += 1;
|
|
133
|
+
}
|
|
134
|
+
return { idle, returns, p: wilsonLowerBound(returns, idle) };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** When a warm goes out after the last cache use of an entry living `ttlMs`. */
|
|
138
|
+
export function cacheWarmDelayMs(ttlMs) {
|
|
139
|
+
return Math.max(1, ttlMs - CACHE_WARM_LEAD_MS);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const TIERS = Object.freeze(['5m', '1h']);
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The lifetime of the message-tail entry a response wrote, or 0 when the
|
|
146
|
+
* usage does not prove it. Any write reaches the tail (a prefix write
|
|
147
|
+
* rewrites everything after it), so a single written tier is the tail's.
|
|
148
|
+
* With both tiers written, a tier is the tail's when no other breakpoint of
|
|
149
|
+
* `cacheStrategy` asked for it. A response that wrote nothing proves nothing.
|
|
150
|
+
*/
|
|
151
|
+
export function writtenCacheTtlMs(usage, cacheStrategy) {
|
|
152
|
+
const written = Number(usage?.cacheWriteTokens) || 0;
|
|
153
|
+
if (!(written > 0)) return 0;
|
|
154
|
+
const written1h = Math.min(Number(usage?.cacheWrite1hTokens) || 0, written);
|
|
155
|
+
const written5m = written - written1h;
|
|
156
|
+
if (written5m <= 0) return ANTHROPIC_TAIL_LIFETIME_MS['1h'];
|
|
157
|
+
if (written1h <= 0) return ANTHROPIC_TAIL_LIFETIME_MS['5m'];
|
|
158
|
+
const tail = cacheStrategy?.messages;
|
|
159
|
+
if (!TIERS.includes(tail)) return 0;
|
|
160
|
+
const others = [cacheStrategy.tools, cacheStrategy.system, cacheStrategy.tier3];
|
|
161
|
+
return others.includes(tail) ? 0 : ANTHROPIC_TAIL_LIFETIME_MS[tail];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Why `request` (a side-question-store snapshot) may not be warmed, or null. */
|
|
165
|
+
export function cacheWarmIneligibility(request) {
|
|
166
|
+
const name = request?.provider?.name;
|
|
167
|
+
if (cacheCapabilityForProvider(name) !== 'explicit-breakpoint') return 'provider caches without breakpoints';
|
|
168
|
+
if (typeof request.provider.sendCacheWarm !== 'function') return 'provider has no cache-warm mode';
|
|
169
|
+
const opts = request.opts || {};
|
|
170
|
+
if (!opts.session?.id) return 'no session';
|
|
171
|
+
if (Number(opts.thinkingBudgetTokens) > 0) return 'thinking budget derives from max_tokens';
|
|
172
|
+
if (opts.effort && anthropicEffortLevelsFor(request.model).length === 0) {
|
|
173
|
+
return 'thinking budget derives from max_tokens';
|
|
174
|
+
}
|
|
175
|
+
if (!providerTailCacheLifetimeMs(name, opts.cacheStrategy)) return 'no expiring message-tail cache';
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The send a warm makes for `request`: the same messages (with the same
|
|
180
|
+
* ephemeral tail reminder the real send added), model, tools and options,
|
|
181
|
+
* with max_tokens 1 and the warm's own signal. */
|
|
182
|
+
export function cacheWarmSend(request, { signal } = {}) {
|
|
183
|
+
const session = request.opts?.session || null;
|
|
184
|
+
return {
|
|
185
|
+
messages: withTailReminder(request.messages, { session, provider: request.provider, model: request.model }),
|
|
186
|
+
model: request.model,
|
|
187
|
+
tools: request.tools.length ? request.tools : undefined,
|
|
188
|
+
opts: { ...request.opts, maxOutputTokensOverride: 1, signal },
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** `{ warmCost, missCost }` in dollars for a prompt of `promptTokens` at
|
|
193
|
+
* per-token `prices` ({ read, write, output } of the entry's tier), or null
|
|
194
|
+
* when the prompt size or any price is unknown. */
|
|
195
|
+
export function cacheWarmEconomics(promptTokens, prices) {
|
|
196
|
+
const tokens = Number(promptTokens);
|
|
197
|
+
const read = prices?.read;
|
|
198
|
+
const write = prices?.write;
|
|
199
|
+
const output = prices?.output;
|
|
200
|
+
if (!(tokens > 0) || ![read, write, output].every((value) => Number.isFinite(value) && value >= 0)) return null;
|
|
201
|
+
return { warmCost: read * tokens + output, missCost: Math.max(0, (write - read) * tokens) };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Whether a warm sent at `now` pays off for the session: the entry was last
|
|
206
|
+
* used at `touchAt` and lives `ttlMs`; the session's last real call went out
|
|
207
|
+
* at `lastCallAt`; `gaps` are its own recorded idle gaps; `latencyMs` the
|
|
208
|
+
* measured time a warm of this chain took to reach the provider (null: not
|
|
209
|
+
* measured yet). With e = now − lastCallAt and I the refresh interval, warm
|
|
210
|
+
* iff some j ≥ 1 has P(e, j·I) · missCost > j · warmCost (P:
|
|
211
|
+
* sessionReturnChance), returns counted inside the idle window. The idle
|
|
212
|
+
* auto-clear is no bound: a successful warm is cache use, so its deadline
|
|
213
|
+
* moves with each one. `{ warm: true, warms: j, p }` or `{ stop }`.
|
|
214
|
+
*/
|
|
215
|
+
export function planCacheWarm({ now, touchAt, ttlMs, lastCallAt, gaps, promptTokens, prices, latencyMs = null }) {
|
|
216
|
+
if (!(lastCallAt > 0)) return { stop: 'last real call unknown' };
|
|
217
|
+
// The warm serves only a return after the current entry's expiry, inside
|
|
218
|
+
// the idle window returns are learned over.
|
|
219
|
+
if (!(touchAt + ttlMs < lastCallAt + IDLE_RETURN_MEASURED_WINDOW_MS)) {
|
|
220
|
+
return { stop: 'idle time reaches the learned window' };
|
|
221
|
+
}
|
|
222
|
+
// The warm must reach the provider while the entry still lives: a timer
|
|
223
|
+
// fires at or after the refresh point, TTL − lead, so the lead itself cannot
|
|
224
|
+
// be the floor; the chain's measured warm latency (none before the first
|
|
225
|
+
// warm) is.
|
|
226
|
+
if (touchAt + ttlMs - now <= (Number(latencyMs) || 0)) return { stop: 'late timer' };
|
|
227
|
+
const economics = cacheWarmEconomics(promptTokens, prices);
|
|
228
|
+
if (!economics) return { stop: 'prompt size or cache prices unknown' };
|
|
229
|
+
const interval = cacheWarmDelayMs(ttlMs);
|
|
230
|
+
const elapsed = now - lastCallAt;
|
|
231
|
+
if (!sessionReturnChance(gaps, elapsed, 0).idle) return { stop: 'no idle history this long' };
|
|
232
|
+
for (let j = 1; elapsed + j * interval <= IDLE_RETURN_MEASURED_WINDOW_MS; j += 1) {
|
|
233
|
+
const { p } = sessionReturnChance(gaps, elapsed, j * interval);
|
|
234
|
+
if (p * economics.missCost > j * economics.warmCost) return { warm: true, warms: j, p };
|
|
235
|
+
}
|
|
236
|
+
return { stop: 'warms cost more than the expected miss' };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Defaults of the injectable dependencies.
|
|
240
|
+
|
|
241
|
+
async function defaultEnabled() {
|
|
242
|
+
const { loadConfig } = await import('../config.mjs');
|
|
243
|
+
return cacheWarmingEnabled(loadConfig({ secrets: false }));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Per-token dollars of a cache read, a cache write of `ttlMs`'s tier and an
|
|
247
|
+
// output token for the request's model, from the pricing catalog (null when
|
|
248
|
+
// unpriced), at the rates of a prompt of `promptTokens`: a model priced by
|
|
249
|
+
// prompt length charges cache reads and writes at the tier that length falls
|
|
250
|
+
// in (Anthropic prompt-caching docs, Pricing: "On a model priced by prompt
|
|
251
|
+
// length, cache reads and cache writes count toward a request's prompt
|
|
252
|
+
// length"), and the warm's one output token at that tier too. The request's
|
|
253
|
+
// own service tier prices it as the real send was (fast mode: the provider's
|
|
254
|
+
// builder sends `speed: 'fast'` under exactly these conditions,
|
|
255
|
+
// providers/anthropic-api-request.mjs, anthropic-oauth.mjs).
|
|
256
|
+
export async function defaultPrices(request, ttlMs, promptTokens) {
|
|
257
|
+
const [{ priceUsage }, { supportsAnthropicFastMode }, { fastModeAvailable }] = await Promise.all([
|
|
258
|
+
import('../../../shared/llm/cost.mjs'),
|
|
259
|
+
import('../../../shared/llm/anthropic-betas.mjs'),
|
|
260
|
+
import('../providers/anthropic-fast-mode.mjs'),
|
|
261
|
+
]);
|
|
262
|
+
const MILLION = 1_000_000;
|
|
263
|
+
const fast = request.opts?.fast === true && supportsAnthropicFastMode(request.model) && fastModeAvailable();
|
|
264
|
+
const base = {
|
|
265
|
+
model: request.model,
|
|
266
|
+
provider: request.provider.name,
|
|
267
|
+
inputTokensInclusive: false,
|
|
268
|
+
...(fast ? { requestServiceTier: 'fast' } : {}),
|
|
269
|
+
};
|
|
270
|
+
// The per-million rates of a request using exactly `tokens` (null when any
|
|
271
|
+
// used slot is unpriced); a request of promptTokens prompt tokens in all.
|
|
272
|
+
const ratesOf = (tokens) => {
|
|
273
|
+
const priced = priceUsage({ ...base, ...tokens });
|
|
274
|
+
return priced.costUsd === null || priced.costUsd === undefined ? null : priced.rates;
|
|
275
|
+
};
|
|
276
|
+
const perToken = (rates, key) => (rates ? rates[key] / MILLION : null);
|
|
277
|
+
const oneHour = ttlMs >= ANTHROPIC_TAIL_LIFETIME_MS['1h'];
|
|
278
|
+
const readRates = ratesOf({ cacheReadTokens: promptTokens, outputTokens: 1 });
|
|
279
|
+
const writeRates = ratesOf({
|
|
280
|
+
cacheWriteTokens: promptTokens,
|
|
281
|
+
...(oneHour ? { cacheWrite1hTokens: promptTokens } : {}),
|
|
282
|
+
});
|
|
283
|
+
return {
|
|
284
|
+
read: perToken(readRates, 'cacheReadCostPerM'),
|
|
285
|
+
write: perToken(writeRates, oneHour ? 'cacheWrite1hCostPerM' : 'cacheWriteCostPerM'),
|
|
286
|
+
output: perToken(readRates, 'outputCostPerM'),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// The registered providers a re-armed request needs: waits for the owning
|
|
291
|
+
// runtime's own provider initialization; never initializes them itself.
|
|
292
|
+
async function defaultProvidersReady() {
|
|
293
|
+
const { providersInitialized } = await import('../providers/registry.mjs');
|
|
294
|
+
await providersInitialized();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function defaultRegisteredProvider(name) {
|
|
298
|
+
const { registeredProvider } = await import('../providers/registry.mjs');
|
|
299
|
+
return registeredProvider(name);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function defaultSend(request, chain, target, onUsage) {
|
|
303
|
+
const controller = new AbortController();
|
|
304
|
+
chain.controller = controller;
|
|
305
|
+
try {
|
|
306
|
+
const send = cacheWarmSend(request, { signal: controller.signal });
|
|
307
|
+
return await target.provider.sendCacheWarm(send.messages, send.model, send.tools, send.opts, {
|
|
308
|
+
onUsage,
|
|
309
|
+
accountId: target.accountId,
|
|
310
|
+
});
|
|
311
|
+
} finally {
|
|
312
|
+
if (chain.controller === controller) chain.controller = null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// The warm's usage-ledger row, in the shape the provider boundary writes
|
|
317
|
+
// (usage-accounting.mjs), under the `cache-warm` source; the ledger prices
|
|
318
|
+
// every reported token with the catalog.
|
|
319
|
+
async function defaultRecordUsage(request, usage, startedAt, accountId) {
|
|
320
|
+
const { getUsageLedger, makeUsageRecord } = await import('../../../shared/llm/usage-ledger.mjs');
|
|
321
|
+
const ledger = getUsageLedger();
|
|
322
|
+
if (!ledger) return;
|
|
323
|
+
const session = request.opts.session;
|
|
324
|
+
await ledger.recordQueued(
|
|
325
|
+
makeUsageRecord({
|
|
326
|
+
ts: Date.now(),
|
|
327
|
+
provider: request.provider.name,
|
|
328
|
+
model: request.model,
|
|
329
|
+
requestedModel: request.model,
|
|
330
|
+
sessionId: session.id,
|
|
331
|
+
ownerSessionId: session.ownerSessionId || session.parentSessionId,
|
|
332
|
+
sourceType: CACHE_WARM_SOURCE,
|
|
333
|
+
inputTokens: usage.inputTokens,
|
|
334
|
+
inputTokensInclusive: false,
|
|
335
|
+
outputTokens: usage.outputTokens,
|
|
336
|
+
cacheReadTokens: usage.cachedTokens,
|
|
337
|
+
cacheWriteTokens: usage.cacheWriteTokens,
|
|
338
|
+
cacheWrite1hTokens: usage.cacheWrite1hTokens,
|
|
339
|
+
account: accountId || '',
|
|
340
|
+
durationMs: Date.now() - startedAt,
|
|
341
|
+
})
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function defaultLastSentRequest(sessionId) {
|
|
346
|
+
const { lastSentMainRequest } = await import('./side-question.mjs');
|
|
347
|
+
return lastSentMainRequest(sessionId);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const DEFAULT_DEPS = Object.freeze({
|
|
351
|
+
now: () => Date.now(),
|
|
352
|
+
timers: globalThis,
|
|
353
|
+
enabled: defaultEnabled,
|
|
354
|
+
turnInFlight: (sessionId) => isTurnInFlight(runtimeState.get(sessionId)),
|
|
355
|
+
lastSentRequest: defaultLastSentRequest,
|
|
356
|
+
providersReady: defaultProvidersReady,
|
|
357
|
+
registeredProvider: defaultRegisteredProvider,
|
|
358
|
+
prices: defaultPrices,
|
|
359
|
+
send: defaultSend,
|
|
360
|
+
recordUsage: defaultRecordUsage,
|
|
361
|
+
touches: () => promptCacheTouchStore(),
|
|
362
|
+
gaps: () => promptCacheIdleGapStore(),
|
|
363
|
+
log: (line) => process.stderr.write(`[cache-warm] ${line}\n`),
|
|
364
|
+
});
|
|
365
|
+
let deps = DEFAULT_DEPS;
|
|
366
|
+
|
|
367
|
+
// The idle auto-clear measures idleness from the last cache use, a
|
|
368
|
+
// successful warm included (durable in the touch store).
|
|
369
|
+
setCacheWarmUseSource((sessionId) => (sessionId ? deps.touches().lastWarmUse(sessionId) : 0));
|
|
370
|
+
|
|
371
|
+
// A session deleted for good drops everything it learned and touched.
|
|
372
|
+
registerSessionPurgeHook((sessionId) => {
|
|
373
|
+
cancelCacheWarm(sessionId);
|
|
374
|
+
turns.delete(sessionId);
|
|
375
|
+
deps.touches().forget(sessionId);
|
|
376
|
+
deps.touches().forgetWarmUse(sessionId);
|
|
377
|
+
deps.gaps().forget(sessionId);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
let defaultGapStore = null;
|
|
381
|
+
/** The idle-gap store the warmer learns from (prompt-cache-idle-gaps.mjs). */
|
|
382
|
+
function promptCacheIdleGapStore() {
|
|
383
|
+
defaultGapStore ??= createIdleGapStore({
|
|
384
|
+
lookbackMs: IDLE_RETURN_LOOKBACK_MS,
|
|
385
|
+
windowMs: IDLE_RETURN_MEASURED_WINDOW_MS,
|
|
386
|
+
// No refresh point lies inside a gap shorter than the shortest interval.
|
|
387
|
+
minMs: cacheWarmDelayMs(ANTHROPIC_TAIL_LIFETIME_MS['5m']),
|
|
388
|
+
});
|
|
389
|
+
return defaultGapStore;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// The plan of `chain` at its refresh point `at` (planCacheWarm), from the
|
|
393
|
+
// session's own idle history. Unknown facts fail closed.
|
|
394
|
+
async function planFor(sessionId, chain, at) {
|
|
395
|
+
if (!(await deps.enabled())) return { stop: 'cache warming off' };
|
|
396
|
+
return planCacheWarm({
|
|
397
|
+
now: at,
|
|
398
|
+
touchAt: chain.touchAt,
|
|
399
|
+
ttlMs: chain.ttlMs,
|
|
400
|
+
// Idle time counts from the last real activity (the gap store's anchor),
|
|
401
|
+
// the clock the gaps were learned on; a system-caused request since then
|
|
402
|
+
// refreshed the cache (touchAt) but not the idle clock.
|
|
403
|
+
lastCallAt: deps.gaps().openAt(sessionId),
|
|
404
|
+
gaps: deps.gaps().gaps(sessionId),
|
|
405
|
+
promptTokens: chain.promptTokens,
|
|
406
|
+
prices: await deps.prices(chain.request, chain.ttlMs, chain.promptTokens),
|
|
407
|
+
latencyMs: chain.latencyMs,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Test seam: replace some dependencies; call with no argument to restore. */
|
|
412
|
+
export function _setCacheWarmDepsForTest(overrides = null) {
|
|
413
|
+
for (const sessionId of [...chains.keys()]) cancelCacheWarm(sessionId);
|
|
414
|
+
deps = overrides ? { ...DEFAULT_DEPS, ...overrides } : DEFAULT_DEPS;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// sessionId -> the armed chain: { request, ttlMs, touchAt, promptTokens,
|
|
418
|
+
// accountId, latencyMs, refreshAt, timer, controller }.
|
|
419
|
+
const chains = new Map();
|
|
420
|
+
|
|
421
|
+
/** The armed chain of `sessionId` (tests and diagnostics). */
|
|
422
|
+
export function cacheWarmChain(sessionId) {
|
|
423
|
+
return chains.get(sessionId) || null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Turns the runtime starts itself, by their prompt provenance
|
|
427
|
+
// (manager/ask-queue.mjs `promptSource`): a Goal's automatic continuation and
|
|
428
|
+
// close-out (runtime-synthetic), and a task notification (a finished
|
|
429
|
+
// background task or a pending resume reported back). Neither the user nor a
|
|
430
|
+
// routine caused them — unless the notification is an inbound channel
|
|
431
|
+
// message (`origin` PROMPT_ORIGIN_CHANNEL), which is presented the same way.
|
|
432
|
+
// The other way round, a prompt whose origin is PROMPT_ORIGIN_AGENT_REPORT (a
|
|
433
|
+
// bot-home wake, a room member's relay) is the runtime's though presented as
|
|
434
|
+
// a prompt.
|
|
435
|
+
const SYSTEM_TURN_SOURCES = new Set(['goal-continuation', 'goal-closeout', 'task-notification']);
|
|
436
|
+
|
|
437
|
+
// What a turn of the provenance `source` / `origin` is: 'system' or 'return'.
|
|
438
|
+
function provenanceKind(source, origin) {
|
|
439
|
+
if (origin === PROMPT_ORIGIN_CHANNEL) return 'return';
|
|
440
|
+
if (origin === PROMPT_ORIGIN_AGENT_REPORT) return 'system';
|
|
441
|
+
return SYSTEM_TURN_SOURCES.has(source) ? 'system' : 'return';
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// The input message of the failed turn a retry reruns, by the turn's durable
|
|
445
|
+
// identity. The failure's completion sits on the session's last message
|
|
446
|
+
// (ask-turn-settle.mjs attachFailedTurnCompletion: the prompt itself, or the
|
|
447
|
+
// turn's last reply / tool result); its owner is the user message of the same
|
|
448
|
+
// attempt (`attemptId`, else the turn start `at` the settle path matches on),
|
|
449
|
+
// searched back to the previous turn's recorded end. A retained retry
|
|
450
|
+
// (`retryTurnId`) names the failed prompt itself (retainedFailedTurnInput).
|
|
451
|
+
function failedTurnMessage(messages, input) {
|
|
452
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
453
|
+
const endIndex = list.findLastIndex((item) => item?.role !== 'system');
|
|
454
|
+
const end = list[endIndex];
|
|
455
|
+
const completion = end?.meta?.transcript?.completion;
|
|
456
|
+
if (completion?.status !== 'failed') return null;
|
|
457
|
+
if (input.retryTurnId && end.meta.transcript.messageId !== input.retryTurnId) return null;
|
|
458
|
+
if (end.role === 'user') return end;
|
|
459
|
+
const attemptId = typeof completion.attemptId === 'string' ? completion.attemptId : '';
|
|
460
|
+
const at = Number(completion.at) || 0;
|
|
461
|
+
for (let index = endIndex - 1; index >= 0; index -= 1) {
|
|
462
|
+
const transcript = list[index]?.meta?.transcript;
|
|
463
|
+
if (transcript?.completion) return null;
|
|
464
|
+
if (list[index].role !== 'user' || !transcript) continue;
|
|
465
|
+
if (transcript.attemptId ? transcript.attemptId === attemptId : at > 0 && Number(transcript.at) === at) {
|
|
466
|
+
return list[index];
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** The provenance `{ source, origin }` of the failed turn a retry input
|
|
473
|
+
* reruns (failedTurnMessage), or null when it cannot be resolved. A rebuilt
|
|
474
|
+
* retry message records it (`meta.provenance`, ask-turn-execute.mjs), so a
|
|
475
|
+
* turn retried again keeps it. */
|
|
476
|
+
export function failedTurnProvenance(messages, input) {
|
|
477
|
+
if (input?.retryFailedTurn !== true && !input?.retryTurnId) return null;
|
|
478
|
+
const meta = failedTurnMessage(messages, input)?.meta;
|
|
479
|
+
if (!meta) return null;
|
|
480
|
+
if (meta.provenance && typeof meta.provenance === 'object') return { ...meta.provenance };
|
|
481
|
+
const origin = promptOriginOf(meta.origin);
|
|
482
|
+
return {
|
|
483
|
+
...(typeof meta.source === 'string' && meta.source ? { source: meta.source } : {}),
|
|
484
|
+
...(origin ? { origin } : {}),
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* What a turn of askSession is for the idle learning, from its input (and,
|
|
490
|
+
* for a retry, the session's `messages` as the turn found them):
|
|
491
|
+
* - a retry of a failed turn (`retryFailedTurn` / `retryTurnId`: the failed
|
|
492
|
+
* turn's own user message runs again — a quota auto-resume, the Retry
|
|
493
|
+
* action) continues that turn: 'system' when the failed turn was system-
|
|
494
|
+
* caused (its stored meta.source / meta.origin / meta.provenance), else
|
|
495
|
+
* 'continuation';
|
|
496
|
+
* - 'system' for a SYSTEM_TURN_SOURCES prompt not from a channel, and for an
|
|
497
|
+
* agent report (a bot-home wake, a room relay);
|
|
498
|
+
* - 'return' for everything else (the user, a scheduled fire, a webhook, a
|
|
499
|
+
* heartbeat, a channel message, a sub-agent dispatch).
|
|
500
|
+
*/
|
|
501
|
+
export function cacheWarmTurnKind(input, messages = null) {
|
|
502
|
+
if (input?.retryFailedTurn === true || input?.retryTurnId) {
|
|
503
|
+
const failed = failedTurnProvenance(messages, input);
|
|
504
|
+
return failed && provenanceKind(failed.source, failed.origin) === 'system' ? 'system' : 'continuation';
|
|
505
|
+
}
|
|
506
|
+
const source = typeof input?.promptSource === 'string' ? input.promptSource : input?.promptSource?.source;
|
|
507
|
+
return provenanceKind(source, input?.promptOrigin);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// sessionId -> its running turn: { kind, startedAt, returning }.
|
|
511
|
+
const turns = new Map();
|
|
512
|
+
|
|
513
|
+
/** A turn of the session opened (manager/ask-session.mjs), of `kind`
|
|
514
|
+
* (cacheWarmTurnKind). A 'return' closes the session's idle gap at its
|
|
515
|
+
* start, once its first request goes out. Returns the turn's entry, which
|
|
516
|
+
* endCacheWarmTurn takes. */
|
|
517
|
+
export function noteCacheWarmTurn(sessionId, kind) {
|
|
518
|
+
if (!sessionId) return null;
|
|
519
|
+
const turn = { kind, startedAt: deps.now(), returning: kind === 'return', suspended: null };
|
|
520
|
+
turns.set(sessionId, turn);
|
|
521
|
+
// A turn entering the session owns its cache from here: a warm pending in
|
|
522
|
+
// its planning lookups or on the wire must not overlap the real request.
|
|
523
|
+
// The chain is stopped (the abort is not a miss) and its captured request
|
|
524
|
+
// and confirmed touch are kept on the turn: the turn's own settled request
|
|
525
|
+
// arms the next chain, and a turn that ends without any request gets this
|
|
526
|
+
// one back (endCacheWarmTurn).
|
|
527
|
+
const chain = chains.get(sessionId);
|
|
528
|
+
if (chain) {
|
|
529
|
+
const { request, ttlMs, touchAt, promptTokens, accountId, latencyMs } = chain;
|
|
530
|
+
turn.suspended = { request, ttlMs, touchAt, promptTokens, accountId, latencyMs };
|
|
531
|
+
stopChain(sessionId, chain, 'turn entered', { quiet: true });
|
|
532
|
+
}
|
|
533
|
+
return turn;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** The turn noteCacheWarmTurn returned ended (completed, failed or
|
|
537
|
+
* cancelled): its entry goes unless a later turn already replaced it. The
|
|
538
|
+
* durable idle anchor stays. When the turn sent no request (any request,
|
|
539
|
+
* invalidation or close dropped `suspended`), no chain replaced the one it
|
|
540
|
+
* stopped and the cache entry has not expired, that chain comes back with
|
|
541
|
+
* its original touch and schedule; the touch and idle-gap stores are left
|
|
542
|
+
* alone. */
|
|
543
|
+
export function endCacheWarmTurn(sessionId, turn) {
|
|
544
|
+
if (!turn || turns.get(sessionId) !== turn) return;
|
|
545
|
+
turns.delete(sessionId);
|
|
546
|
+
const chain = turn.suspended;
|
|
547
|
+
turn.suspended = null;
|
|
548
|
+
if (!chain || chains.has(sessionId)) return;
|
|
549
|
+
if (deps.now() < chain.touchAt + chain.ttlMs - CACHE_WARM_LEAD_MS) {
|
|
550
|
+
const restored = startChain(sessionId, chain);
|
|
551
|
+
restored.latencyMs = chain.latencyMs;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** The running turn the warmer holds for `sessionId`, or null (tests). */
|
|
556
|
+
export function cacheWarmTurn(sessionId) {
|
|
557
|
+
return turns.get(sessionId) || null;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Whether a captured request is the session's real activity: one of a turn
|
|
561
|
+
// that is not system-caused, sent by the main loop itself (no `requestKind`,
|
|
562
|
+
// or the wire's 'turn'; a compaction summary or a prewarm names its own).
|
|
563
|
+
function realActivity(sessionId, request) {
|
|
564
|
+
if (turns.get(sessionId)?.kind === 'system') return false;
|
|
565
|
+
const kind = request?.opts?.requestKind;
|
|
566
|
+
return !kind || kind === 'turn';
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** A request of the main loop is going out: the chain stops; when it is the
|
|
570
|
+
* first request of a returning turn, the open idle gap closes at the turn's
|
|
571
|
+
* start. Requests inside a running turn, of a continued turn or of a
|
|
572
|
+
* system-caused turn are no return. Warms never come through here. */
|
|
573
|
+
export function noteCacheWarmActivity(sessionId, request = null) {
|
|
574
|
+
if (!sessionId) return;
|
|
575
|
+
const turn = turns.get(sessionId);
|
|
576
|
+
if (turn?.returning && realActivity(sessionId, request)) {
|
|
577
|
+
turn.returning = false;
|
|
578
|
+
deps.gaps().close(sessionId, turn.startedAt);
|
|
579
|
+
}
|
|
580
|
+
cancelCacheWarm(sessionId);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Drop the session's chain: clear its timer and abort a warm in flight
|
|
584
|
+
* (its usage is still accounted). */
|
|
585
|
+
export function cancelCacheWarm(sessionId) {
|
|
586
|
+
// Whatever cancels (a request, an invalidation, a close) also voids the
|
|
587
|
+
// chain the running turn would restore.
|
|
588
|
+
const turn = sessionId ? turns.get(sessionId) : null;
|
|
589
|
+
if (turn) turn.suspended = null;
|
|
590
|
+
const chain = sessionId ? chains.get(sessionId) : null;
|
|
591
|
+
if (chain) stopChain(sessionId, chain, 'canceled', { quiet: true });
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** The session's cached prefix is gone (compaction, clear, rewind): drop
|
|
595
|
+
* the chain, its durable touch and its warm use. */
|
|
596
|
+
export function forgetCacheWarm(sessionId) {
|
|
597
|
+
cancelCacheWarm(sessionId);
|
|
598
|
+
if (!sessionId) return;
|
|
599
|
+
deps.touches().forget(sessionId);
|
|
600
|
+
deps.touches().forgetWarmUse(sessionId);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function recordTouch(sessionId, chain) {
|
|
604
|
+
deps.touches().record(sessionId, {
|
|
605
|
+
touchAt: chain.touchAt,
|
|
606
|
+
ttlMs: chain.ttlMs,
|
|
607
|
+
promptTokens: chain.promptTokens,
|
|
608
|
+
accountId: chain.accountId,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function stopChain(sessionId, chain, reason, { quiet = false } = {}) {
|
|
613
|
+
if (chains.get(sessionId) !== chain) return;
|
|
614
|
+
chains.delete(sessionId);
|
|
615
|
+
if (chain.timer) deps.timers.clearTimeout(chain.timer);
|
|
616
|
+
chain.timer = null;
|
|
617
|
+
chain.controller?.abort(new Error(`cache warm stopped: ${reason}`));
|
|
618
|
+
if (!quiet) deps.log(`${sessionId}: stopped (${reason})`);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function startChain(sessionId, { request, ttlMs, touchAt, promptTokens, accountId }) {
|
|
622
|
+
const chain = {
|
|
623
|
+
request,
|
|
624
|
+
ttlMs,
|
|
625
|
+
touchAt,
|
|
626
|
+
promptTokens,
|
|
627
|
+
accountId: accountId || '',
|
|
628
|
+
latencyMs: null,
|
|
629
|
+
refreshAt: 0,
|
|
630
|
+
timer: null,
|
|
631
|
+
controller: null,
|
|
632
|
+
};
|
|
633
|
+
chains.set(sessionId, chain);
|
|
634
|
+
scheduleWarm(sessionId, chain);
|
|
635
|
+
return chain;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// The provider and account a warm of `chain` goes to: the roster account
|
|
639
|
+
// that holds the entry, or the provider itself when it has no roster. null
|
|
640
|
+
// when that account is not known.
|
|
641
|
+
function warmTarget(chain) {
|
|
642
|
+
const provider = chain.request.provider;
|
|
643
|
+
if (!ACCOUNT_PROVIDERS.includes(provider.name)) return { provider, accountId: '' };
|
|
644
|
+
if (!chain.accountId || typeof provider.forAccount !== 'function') return null;
|
|
645
|
+
return { provider: provider.forAccount(chain.accountId), accountId: chain.accountId };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Arm the session's warm chain from a real request that settled with
|
|
650
|
+
* `response`, and record the confirmed touch durably; replaces any chain.
|
|
651
|
+
* Nothing is armed — and the durable touch is dropped — for an ineligible
|
|
652
|
+
* request, one without a send time, or a response that does not prove the
|
|
653
|
+
* tier it wrote or the prompt size.
|
|
654
|
+
*/
|
|
655
|
+
export function armCacheWarm(sessionId, request, response) {
|
|
656
|
+
cancelCacheWarm(sessionId);
|
|
657
|
+
// A real request settled: the session's own activity is the latest use.
|
|
658
|
+
if (sessionId) deps.touches().forgetWarmUse(sessionId);
|
|
659
|
+
// Whatever its provider: after real activity the session's idle clock runs
|
|
660
|
+
// from this request's send (the clock the planner reads). A later request
|
|
661
|
+
// of the same turn only moves it; the next returning turn closes it. A
|
|
662
|
+
// system-caused request leaves it alone.
|
|
663
|
+
if (sessionId && request && realActivity(sessionId, request)) {
|
|
664
|
+
deps.gaps().open(sessionId, Number(request.sentAt));
|
|
665
|
+
}
|
|
666
|
+
if (!sessionId || !request || cacheWarmIneligibility(request)) return;
|
|
667
|
+
const usage = response?.usage || {};
|
|
668
|
+
const touchAt = Number(request.sentAt);
|
|
669
|
+
const ttlMs = writtenCacheTtlMs(usage, request.opts.cacheStrategy);
|
|
670
|
+
const promptTokens =
|
|
671
|
+
typeof usage.promptTokens === 'number'
|
|
672
|
+
? usage.promptTokens
|
|
673
|
+
: (Number(usage.inputTokens) || 0) + (Number(usage.cachedTokens) || 0) + (Number(usage.cacheWriteTokens) || 0);
|
|
674
|
+
if (!(touchAt > 0) || !ttlMs || !(promptTokens > 0)) {
|
|
675
|
+
deps.touches().forget(sessionId);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
const chain = startChain(sessionId, {
|
|
679
|
+
request,
|
|
680
|
+
ttlMs,
|
|
681
|
+
touchAt,
|
|
682
|
+
promptTokens,
|
|
683
|
+
accountId: response?.providerMetadata?.accountRouting?.accountId,
|
|
684
|
+
});
|
|
685
|
+
recordTouch(sessionId, chain);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Daemon start: the in-memory chains died with the previous process. Re-arm
|
|
690
|
+
* every touched session while warming is on from its durable touch (prompt-cache-touch-store.mjs: the last confirmed cache
|
|
691
|
+
* use, the tier the provider wrote, the account and prompt size) while the
|
|
692
|
+
* entry still has more than the lead left, replaying the request its
|
|
693
|
+
* next turn rebuilds without the last reply. A candidate's expired touch is
|
|
694
|
+
* dropped; each chain's first refresh point applies its rule. Resolves how
|
|
695
|
+
* many chains were armed.
|
|
696
|
+
*/
|
|
697
|
+
export async function rearmCacheWarmsAfterRestart() {
|
|
698
|
+
if (!(await deps.enabled())) return 0;
|
|
699
|
+
const candidates = [];
|
|
700
|
+
for (const sessionId of deps.touches().ids()) {
|
|
701
|
+
if (chains.has(sessionId)) continue;
|
|
702
|
+
const touch = deps.touches().get(sessionId);
|
|
703
|
+
if (!touch) continue;
|
|
704
|
+
if (!(deps.now() < touch.touchAt + touch.ttlMs - CACHE_WARM_LEAD_MS)) {
|
|
705
|
+
deps.touches().forget(sessionId);
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
candidates.push([sessionId, touch]);
|
|
709
|
+
}
|
|
710
|
+
if (!candidates.length) return 0;
|
|
711
|
+
await deps.providersReady();
|
|
712
|
+
let armed = 0;
|
|
713
|
+
for (const [sessionId, touch] of candidates) {
|
|
714
|
+
let request = null;
|
|
715
|
+
try {
|
|
716
|
+
request = await deps.lastSentRequest(sessionId);
|
|
717
|
+
} catch (error) {
|
|
718
|
+
deps.log(`${sessionId}: restart re-arm skipped (${error?.message || error})`);
|
|
719
|
+
}
|
|
720
|
+
if (!request || cacheWarmIneligibility(request) || chains.has(sessionId)) continue;
|
|
721
|
+
startChain(sessionId, {
|
|
722
|
+
request,
|
|
723
|
+
ttlMs: touch.ttlMs,
|
|
724
|
+
touchAt: touch.touchAt,
|
|
725
|
+
promptTokens: touch.promptTokens,
|
|
726
|
+
accountId: touch.accountId,
|
|
727
|
+
});
|
|
728
|
+
armed += 1;
|
|
729
|
+
}
|
|
730
|
+
return armed;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function scheduleWarm(sessionId, chain) {
|
|
734
|
+
chain.refreshAt = chain.touchAt + cacheWarmDelayMs(chain.ttlMs);
|
|
735
|
+
chain.timer = deps.timers.setTimeout(
|
|
736
|
+
() => void fireWarm(sessionId, chain).catch((error) => stopChain(sessionId, chain, error?.message || error)),
|
|
737
|
+
Math.max(0, chain.refreshAt - deps.now())
|
|
738
|
+
);
|
|
739
|
+
chain.timer?.unref?.();
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
async function fireWarm(sessionId, chain) {
|
|
743
|
+
if (chains.get(sessionId) !== chain) return;
|
|
744
|
+
chain.timer = null;
|
|
745
|
+
if (deps.turnInFlight(sessionId)) return stopChain(sessionId, chain, 'turn in flight');
|
|
746
|
+
const plan = await planFor(sessionId, chain, deps.now());
|
|
747
|
+
if (chains.get(sessionId) !== chain) return;
|
|
748
|
+
if (plan.stop) return stopChain(sessionId, chain, plan.stop);
|
|
749
|
+
// A registry re-initialization replaced the instance the request was
|
|
750
|
+
// captured on: its replay would run on a retired instance, and the request
|
|
751
|
+
// is not rebuilt onto the new one.
|
|
752
|
+
if ((await deps.registeredProvider(chain.request.provider.name)) !== chain.request.provider) {
|
|
753
|
+
return stopChain(sessionId, chain, 'provider instance replaced');
|
|
754
|
+
}
|
|
755
|
+
if (chains.get(sessionId) !== chain) return;
|
|
756
|
+
const target = warmTarget(chain);
|
|
757
|
+
if (!target) return stopChain(sessionId, chain, 'account holding the cache unknown');
|
|
758
|
+
// Last check before dispatch, after the final await: a turn that began
|
|
759
|
+
// during the lookups has the cache to itself.
|
|
760
|
+
if (deps.turnInFlight(sessionId)) return stopChain(sessionId, chain, 'turn in flight');
|
|
761
|
+
const sentAt = deps.now();
|
|
762
|
+
let usage = null;
|
|
763
|
+
let reportedAt = null;
|
|
764
|
+
let failure = null;
|
|
765
|
+
try {
|
|
766
|
+
usage = await deps.send(chain.request, chain, target, (reported) => {
|
|
767
|
+
usage = reported;
|
|
768
|
+
reportedAt ??= deps.now();
|
|
769
|
+
});
|
|
770
|
+
} catch (error) {
|
|
771
|
+
failure = error;
|
|
772
|
+
usage = error?.usage || usage;
|
|
773
|
+
}
|
|
774
|
+
// Billed whatever happened next: success, failure or cancellation.
|
|
775
|
+
if (usage) {
|
|
776
|
+
void Promise.resolve()
|
|
777
|
+
.then(() => deps.recordUsage(chain.request, usage, sentAt, target.accountId))
|
|
778
|
+
.catch((error) => deps.log(`${sessionId}: warm usage not recorded (${error?.message || error})`));
|
|
779
|
+
}
|
|
780
|
+
if (chains.get(sessionId) !== chain) return;
|
|
781
|
+
if (failure) return stopChain(sessionId, chain, `warm failed: ${failure?.message || failure}`);
|
|
782
|
+
if (!(Number(usage?.cachedTokens) > 0) || Number(usage?.cacheWriteTokens) > 0) {
|
|
783
|
+
deps.touches().forget(sessionId);
|
|
784
|
+
return stopChain(sessionId, chain, 'warm missed the cache');
|
|
785
|
+
}
|
|
786
|
+
chain.touchAt = sentAt;
|
|
787
|
+
chain.latencyMs = (reportedAt ?? deps.now()) - sentAt;
|
|
788
|
+
recordTouch(sessionId, chain);
|
|
789
|
+
// A read without a write is cache use: the idle auto-clear counts from it.
|
|
790
|
+
deps.touches().recordWarmUse(sessionId, sentAt);
|
|
791
|
+
scheduleWarm(sessionId, chain);
|
|
792
|
+
}
|