mixdog 1.3.5 → 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/NOTICE.md +2 -2
- package/README.md +2 -2
- package/package.json +2 -11
- package/scripts/daemon-stub.mjs +1 -1
- package/scripts/gate-local.mjs +4 -2
- 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 +7 -11
- package/src/agents/reviewer/AGENT.md +7 -11
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +17 -1
- package/src/defaults/skills/browser-use/SKILL.md +59 -11
- 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 +22 -0
- package/src/defaults/skills/setup/references/actions.md +39 -5
- package/src/defaults/skills/setup/references/surfaces.md +11 -2
- package/src/defaults/skills/visualize/SKILL.md +2 -1
- package/src/lib/rules-builder.cjs +10 -8
- package/src/rules/agent/AGENT.md +6 -6
- 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/title-completion.mjs +3 -1
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +3 -1
- package/src/runtime/agent/orchestrator/config.mjs +17 -1
- 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 +176 -72
- package/src/runtime/agent/orchestrator/mcp/client.mjs +19 -0
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +181 -133
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +42 -6
- 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 +145 -66
- 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-transport.mjs +123 -102
- package/src/runtime/agent/orchestrator/providers/cache-warm-admission.mjs +90 -0
- package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +25 -9
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +22 -13
- package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +17 -3
- 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 +2 -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/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 +26 -0
- 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/runtime-core/ask-user-tool-defs.mjs +12 -7
- 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/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.mjs +125 -30
- package/src/runtime/agent/orchestrator/session/cache-warm.mjs +792 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +133 -22
- 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 +4 -1
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +11 -5
- package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +0 -2
- package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +36 -4
- package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +1 -1
- 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 +3 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +17 -3
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -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 +3 -2
- 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 +12 -3
- 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-execute.mjs +133 -90
- 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/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/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.mjs +36 -9
- 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-rows.mjs +8 -6
- 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/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/transcript-restore/restore.mjs +18 -1
- 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/scheduler.mjs +33 -147
- package/src/runtime/channels/lib/schedules-db.mjs +34 -11
- package/src/runtime/channels/lib/status-snapshot.mjs +1 -1
- package/src/runtime/channels/lib/tool-dispatch.mjs +1 -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 +10 -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/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/bounded-download.mjs +8 -3
- package/src/runtime/shared/config.mjs +2 -13
- package/src/runtime/shared/connector-accounts.mjs +20 -10
- package/src/runtime/shared/connector-catalog.d.mts +29 -0
- package/src/runtime/shared/connector-catalog.mjs +150 -0
- 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/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/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/usage-rollup.mjs +1 -1
- 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 +7 -1
- 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 +10 -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-result-summary.mjs +1 -14
- 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 +448 -185
- package/src/session-runtime/agent-task-sessions.mjs +329 -180
- package/src/session-runtime/boot/apis.mjs +9 -0
- 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 +6 -1
- 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/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 +55 -3
- package/src/session-runtime/mcp-oauth-login.mjs +5 -3
- package/src/session-runtime/mixy-heartbeat.mjs +153 -33
- 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 +2 -1
- package/src/session-runtime/prewarm/channel-start.mjs +3 -7
- 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-owner.mjs +40 -9
- package/src/session-runtime/services/agent-tool/close-flow.mjs +3 -0
- 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 +7 -13
- package/src/session-runtime/services/channel-session-router.mjs +4 -2
- package/src/session-runtime/services/seeds.mjs +0 -28
- package/src/session-runtime/session-compaction-sync.mjs +27 -0
- package/src/session-runtime/session-create/session-options.mjs +1 -0
- 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 +13 -1
- package/src/session-runtime/setup-tool/agent-settings-actions.mjs +65 -0
- package/src/session-runtime/setup-tool/executor.mjs +145 -22
- package/src/session-runtime/setup-tool/extended-actions.mjs +8 -3
- package/src/session-runtime/setup-tool/session-route-actions.mjs +152 -0
- package/src/session-runtime/setup-tool/settings-contract.mjs +4 -3
- package/src/session-runtime/setup-tool/tool-defs.mjs +64 -5
- package/src/session-runtime/tool-policy/agent-profile-fold.mjs +17 -13
- package/src/session-runtime/tool-policy/policy-stage.mjs +16 -2
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
- package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +4 -0
- 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 +124 -188
- package/src/standalone/daemon.mjs +26 -3
- package/src/standalone/session-client.mjs +7 -3
- package/src/standalone/session-protocol.mjs +5 -0
- 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-views.mjs +8 -0
- package/src/standalone/session-tool-output.mjs +15 -7
- package/src/tui/app/model-options.mjs +3 -28
- package/src/tui/dist/index.mjs +58 -64
- 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 +7 -1
- package/src/tui/session/queue-helpers.mjs +3 -42
- 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 +17 -1
- 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 +19 -2
- package/src/tui/session/turn.mjs +1 -0
- 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
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
import { createHash } from 'node:crypto';
|
|
10
10
|
import keychain from '../../lib/keychain-cjs.cjs';
|
|
11
11
|
import { GMAIL_APP_PASSWORD, gmailAddress } from '../shared/connector-catalog.mjs';
|
|
12
|
-
import {
|
|
12
|
+
import { connectorAccountsInstructions } from '../shared/connector-accounts.mjs';
|
|
13
13
|
|
|
14
14
|
const PROTOCOL_VERSION = '2025-06-18';
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
const GMAIL_IMAP = Object.freeze({ host: 'imap.gmail.com', port: 993, secure: true });
|
|
16
|
+
const GMAIL_SMTP = Object.freeze({ host: 'smtp.gmail.com', port: 465, secure: true });
|
|
17
17
|
const TIMEOUTS = { connectionTimeout: 20_000, greetingTimeout: 15_000, socketTimeout: 60_000 };
|
|
18
18
|
const SEARCH_DEFAULT = 10;
|
|
19
19
|
const SEARCH_MAX = 25;
|
|
@@ -48,7 +48,7 @@ const MESSAGES = {
|
|
|
48
48
|
|
|
49
49
|
/** `GMAIL_APP_PASSWORD_<CODE>: <sentence>` — the code is what Desktop shows
|
|
50
50
|
* in the user's language. */
|
|
51
|
-
|
|
51
|
+
class GmailAppPasswordError extends Error {
|
|
52
52
|
constructor(code, detail = '') {
|
|
53
53
|
super(`GMAIL_APP_PASSWORD_${code}: ${MESSAGES[code]}${detail ? ` (${detail})` : ''}`);
|
|
54
54
|
this.code = code;
|
|
@@ -101,7 +101,7 @@ function gmailError(error, password) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
// ── Clients (tests pass fakes with the same surface) ────────────────────────
|
|
104
|
-
|
|
104
|
+
const gmailClients = {
|
|
105
105
|
async imap({ email, password }) {
|
|
106
106
|
const { ImapFlow } = await import('imapflow');
|
|
107
107
|
const client = new ImapFlow({
|
|
@@ -186,6 +186,64 @@ const addresses = (list) =>
|
|
|
186
186
|
.map((entry) => (entry?.name ? `${entry.name} <${entry.address || ''}>` : entry?.address || ''))
|
|
187
187
|
.filter(Boolean)
|
|
188
188
|
.join(', ');
|
|
189
|
+
/** The From mailboxes as Gmail parsed them, one entry each; a group shows as
|
|
190
|
+
* its name with an empty address (imapflow parseEnvelope). */
|
|
191
|
+
const mailboxes = (list) =>
|
|
192
|
+
(Array.isArray(list) ? list : []).map((entry) => ({
|
|
193
|
+
name: String(entry?.name || ''),
|
|
194
|
+
address: String(entry?.address || ''),
|
|
195
|
+
}));
|
|
196
|
+
|
|
197
|
+
const stripComments = (text) => {
|
|
198
|
+
let out = text;
|
|
199
|
+
for (let previous = ''; previous !== out; ) {
|
|
200
|
+
previous = out;
|
|
201
|
+
out = out.replace(/\([^()]*\)/g, ' ');
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/** Gmail's own verdict on the sender, read from the topmost
|
|
207
|
+
* Authentication-Results only, and only when its authserv-id is Gmail's
|
|
208
|
+
* (mx.google.com):
|
|
209
|
+
* - the receiving MTA prepends the field above the trace fields it adds
|
|
210
|
+
* (RFC 8601 §4, §4.1), so the last receiver's copy is the first one;
|
|
211
|
+
* - a conforming MTA deletes any copy that arrives bearing its own
|
|
212
|
+
* authserv-id (RFC 8601 §5), so a sender cannot pass one off as Gmail's;
|
|
213
|
+
* - Gmail directs other mail clients to this field for its verdict (Gmail
|
|
214
|
+
* Help, "Check if your Gmail message is authenticated", answer 180707).
|
|
215
|
+
* A message no MTA delivered — one appended to the mailbox over IMAP — has
|
|
216
|
+
* only the headers its author wrote; that is the account owner's own mail.
|
|
217
|
+
* `{ dmarc: { result, from }, dkim: [{ result, domain }] }` (`domain` is a
|
|
218
|
+
* signature's d= as header.d reports it, RFC 8601 §2.7.1), or null. */
|
|
219
|
+
function gmailAuthentication(headers) {
|
|
220
|
+
const unfolded = String(headers || '').replace(/\r?\n[ \t]+/g, ' ');
|
|
221
|
+
const first = /^authentication-results:(.*)$/im.exec(unfolded)?.[1];
|
|
222
|
+
if (!first) return null;
|
|
223
|
+
const [server = '', ...results] = stripComments(first).split(';');
|
|
224
|
+
if (server.trim().split(/\s+/)[0].toLowerCase() !== 'mx.google.com') return null;
|
|
225
|
+
const verdict = { dmarc: null, dkim: [] };
|
|
226
|
+
for (const entry of results) {
|
|
227
|
+
const method = /^\s*([a-z0-9-]+)\s*=\s*([a-z]+)/i.exec(entry);
|
|
228
|
+
if (!method) continue;
|
|
229
|
+
const props = Object.fromEntries(
|
|
230
|
+
[...entry.matchAll(/([a-z]+\.[a-z]+)\s*=\s*("[^"]*"|[^\s;]+)/gi)].map(([, name, value]) => [
|
|
231
|
+
name.toLowerCase(),
|
|
232
|
+
value.replace(/^"|"$/g, '').toLowerCase(),
|
|
233
|
+
])
|
|
234
|
+
);
|
|
235
|
+
const result = method[2].toLowerCase();
|
|
236
|
+
if (method[1].toLowerCase() === 'dmarc' && !verdict.dmarc)
|
|
237
|
+
verdict.dmarc = { result, from: props['header.from'] || '' };
|
|
238
|
+
// Only the signing domain d= counts toward the From domain (RFC 7489
|
|
239
|
+
// §3.1.1); the agent identity i= is not an alignment identifier.
|
|
240
|
+
if (method[1].toLowerCase() === 'dkim' && props['header.d']) {
|
|
241
|
+
verdict.dkim.push({ result, domain: props['header.d'] });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return verdict;
|
|
245
|
+
}
|
|
246
|
+
|
|
189
247
|
const isoDate = (value) => {
|
|
190
248
|
const date = value ? new Date(value) : null;
|
|
191
249
|
return date && !Number.isNaN(date.getTime()) ? date.toISOString() : '';
|
|
@@ -214,25 +272,30 @@ const isAttachment = (node) =>
|
|
|
214
272
|
String(node.type || '').startsWith('message/') ||
|
|
215
273
|
Boolean(node.dispositionParameters?.filename || node.parameters?.name);
|
|
216
274
|
|
|
217
|
-
/** The text/plain
|
|
218
|
-
|
|
219
|
-
|
|
275
|
+
/** The first text/plain and the first text/html part of the message that are
|
|
276
|
+
* not attachments (part numbers), each null without one. */
|
|
277
|
+
function textParts(root) {
|
|
278
|
+
const found = { plain: null, html: null };
|
|
220
279
|
const walk = (node) => {
|
|
221
|
-
if (!node || String(node.type || '').startsWith('message/')) return
|
|
280
|
+
if (!node || String(node.type || '').startsWith('message/')) return;
|
|
222
281
|
if (node.childNodes?.length) {
|
|
223
|
-
for (const child of node.childNodes)
|
|
224
|
-
|
|
225
|
-
if (found) return found;
|
|
226
|
-
}
|
|
227
|
-
return null;
|
|
282
|
+
for (const child of node.childNodes) walk(child);
|
|
283
|
+
return;
|
|
228
284
|
}
|
|
229
|
-
if (node.disposition === 'attachment') return
|
|
285
|
+
if (node.disposition === 'attachment') return;
|
|
230
286
|
const part = node.part || '1';
|
|
231
|
-
if (node.type === 'text/plain')
|
|
232
|
-
if (node.type === 'text/html' && !html) html =
|
|
233
|
-
return null;
|
|
287
|
+
if (node.type === 'text/plain' && !found.plain) found.plain = part;
|
|
288
|
+
if (node.type === 'text/html' && !found.html) found.html = part;
|
|
234
289
|
};
|
|
235
|
-
|
|
290
|
+
walk(root);
|
|
291
|
+
return found;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** The text/plain part (else text/html) that is the message's body. */
|
|
295
|
+
function bodyPart(root) {
|
|
296
|
+
const { plain, html } = textParts(root);
|
|
297
|
+
if (plain) return { part: plain, html: false };
|
|
298
|
+
return html ? { part: html, html: true } : null;
|
|
236
299
|
}
|
|
237
300
|
|
|
238
301
|
/** Attachments by name, type and (decoded, approximate) size only. */
|
|
@@ -264,16 +327,15 @@ const htmlText = (html) =>
|
|
|
264
327
|
.replace(/ *\n\s*/g, '\n')
|
|
265
328
|
.trim();
|
|
266
329
|
|
|
267
|
-
/** Up to `maxBytes` of
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const download = await client.download(String(uid), picked.part, {
|
|
330
|
+
/** Up to `maxBytes` of part `part`, decoded to UTF-8; `complete` is false when
|
|
331
|
+
* the part may go on past them. */
|
|
332
|
+
async function partText(client, uid, part, maxBytes) {
|
|
333
|
+
const download = await client.download(String(uid), part, {
|
|
272
334
|
uid: true,
|
|
273
335
|
maxBytes,
|
|
274
336
|
chunkSize: Math.min(maxBytes, 65_536),
|
|
275
337
|
});
|
|
276
|
-
if (!download?.content) return '';
|
|
338
|
+
if (!download?.content) return { value: '', complete: true };
|
|
277
339
|
const chunks = [];
|
|
278
340
|
let size = 0;
|
|
279
341
|
for await (const chunk of download.content) {
|
|
@@ -281,10 +343,63 @@ async function bodyText(client, uid, structure, maxBytes) {
|
|
|
281
343
|
size += chunk.length;
|
|
282
344
|
if (size >= maxBytes) break;
|
|
283
345
|
}
|
|
284
|
-
|
|
346
|
+
return { value: Buffer.concat(chunks).subarray(0, maxBytes).toString('utf8'), complete: size < maxBytes };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Up to `maxBytes` of the body text, decoded to UTF-8. */
|
|
350
|
+
async function bodyText(client, uid, structure, maxBytes) {
|
|
351
|
+
const picked = bodyPart(structure);
|
|
352
|
+
if (!picked) return '';
|
|
353
|
+
const { value } = await partText(client, uid, picked.part, maxBytes);
|
|
285
354
|
return picked.html ? htmlText(value) : value.trim();
|
|
286
355
|
}
|
|
287
356
|
|
|
357
|
+
let domModule;
|
|
358
|
+
const loadDom = () => (domModule ||= import('jsdom'));
|
|
359
|
+
|
|
360
|
+
/** The targets of an HTML part's hyperlinks (`a` and `area` with href, HTML
|
|
361
|
+
* Standard §4.6 Links) as the HTML parser reads the attribute, character
|
|
362
|
+
* references decoded. A tag the download cut off is no element (HTML
|
|
363
|
+
* tokenization: the eof-in-tag error emits no tag). */
|
|
364
|
+
async function htmlLinks(html) {
|
|
365
|
+
if (!html) return [];
|
|
366
|
+
const { JSDOM } = await loadDom();
|
|
367
|
+
const dom = new JSDOM(html);
|
|
368
|
+
try {
|
|
369
|
+
return [...dom.window.document.querySelectorAll('a[href], area[href]')].map((node) => node.getAttribute('href'));
|
|
370
|
+
} finally {
|
|
371
|
+
dom.window.close();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// An absolute http(s) URI in plain text as RFC 3986 spells one: the scheme,
|
|
376
|
+
// then a run of URI characters — unreserved, reserved or percent-encoded
|
|
377
|
+
// (§2.1-§2.3). Whitespace, quotes and angle brackets are no URI characters,
|
|
378
|
+
// so they end it (Appendix C).
|
|
379
|
+
const TEXT_URI = /https?:\/\/(?:[A-Za-z0-9\-._~:/?#[\]@!$&'()*+,;=]|%[0-9A-Fa-f]{2})+/gi;
|
|
380
|
+
|
|
381
|
+
const isWebUrl = (value) => {
|
|
382
|
+
try {
|
|
383
|
+
return ['http:', 'https:'].includes(new URL(value).protocol);
|
|
384
|
+
} catch {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
/** The message's link targets, exactly as written: the hyperlinks of its
|
|
390
|
+
* HTML part, then the URIs of its text part (one the download cut off is
|
|
391
|
+
* left out). Absolute http(s) only, each once. */
|
|
392
|
+
function messageLinks(plain, hrefs) {
|
|
393
|
+
const links = [...hrefs];
|
|
394
|
+
if (plain) {
|
|
395
|
+
for (const match of plain.value.matchAll(TEXT_URI)) {
|
|
396
|
+
if (!plain.complete && match.index + match[0].length === plain.value.length) continue;
|
|
397
|
+
links.push(match[0]);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return [...new Set(links.filter((link) => typeof link === 'string' && isWebUrl(link)))];
|
|
401
|
+
}
|
|
402
|
+
|
|
288
403
|
const references = (headers) =>
|
|
289
404
|
String(headers || '')
|
|
290
405
|
.replace(/\r?\n[ \t]+/g, ' ')
|
|
@@ -317,7 +432,9 @@ const LABEL_MAX = 225;
|
|
|
317
432
|
|
|
318
433
|
/** The distinct message ids of `args.ids` (an array, or one id). */
|
|
319
434
|
function messageIds(args) {
|
|
320
|
-
|
|
435
|
+
let raw = [];
|
|
436
|
+
if (Array.isArray(args.ids)) raw = args.ids;
|
|
437
|
+
else if (args.ids != null) raw = [args.ids];
|
|
321
438
|
const ids = [...new Set(raw.map((id) => text(id)))];
|
|
322
439
|
if (!ids.length || ids.some((id) => !id)) throw new ToolInputError('ids must list ids from search_emails');
|
|
323
440
|
if (ids.length > MANAGE_MAX) throw new ToolInputError(`At most ${MANAGE_MAX} emails per call; got ${ids.length}.`);
|
|
@@ -519,7 +636,8 @@ const TOOLS = [
|
|
|
519
636
|
},
|
|
520
637
|
{
|
|
521
638
|
name: 'read_email',
|
|
522
|
-
description:
|
|
639
|
+
description:
|
|
640
|
+
'Read one email by its id from search_emails: headers, text body, link targets, and attachment names and sizes.',
|
|
523
641
|
inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
|
|
524
642
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
525
643
|
async run(ctx, args) {
|
|
@@ -530,22 +648,38 @@ const TOOLS = [
|
|
|
530
648
|
const [message] = uid
|
|
531
649
|
? await client.fetchAll(
|
|
532
650
|
String(uid),
|
|
533
|
-
{
|
|
651
|
+
{
|
|
652
|
+
uid: true,
|
|
653
|
+
envelope: true,
|
|
654
|
+
internalDate: true,
|
|
655
|
+
bodyStructure: true,
|
|
656
|
+
threadId: true,
|
|
657
|
+
headers: ['authentication-results'],
|
|
658
|
+
},
|
|
534
659
|
{ uid: true }
|
|
535
660
|
)
|
|
536
661
|
: [];
|
|
537
662
|
if (!message) throw new ToolInputError(`No email with id ${id}.`);
|
|
538
663
|
const { envelope = {} } = message;
|
|
539
|
-
const
|
|
664
|
+
const parts = textParts(message.bodyStructure);
|
|
665
|
+
const plain = parts.plain ? await partText(client, message.uid, parts.plain, READ_BYTES) : null;
|
|
666
|
+
const html = parts.html ? await partText(client, message.uid, parts.html, READ_BYTES) : null;
|
|
667
|
+
let body = '';
|
|
668
|
+
if (plain) body = plain.value.trim();
|
|
669
|
+
else if (html) body = htmlText(html.value);
|
|
670
|
+
const links = messageLinks(plain, await htmlLinks(html?.value));
|
|
540
671
|
const head = {
|
|
541
672
|
id: message.emailId || id,
|
|
542
673
|
threadId: message.threadId,
|
|
543
674
|
messageId: envelope.messageId,
|
|
544
675
|
from: addresses(envelope.from),
|
|
676
|
+
fromAddresses: mailboxes(envelope.from),
|
|
677
|
+
authentication: gmailAuthentication(message.headers),
|
|
545
678
|
to: addresses(envelope.to),
|
|
546
679
|
cc: addresses(envelope.cc) || undefined,
|
|
547
680
|
date: isoDate(envelope.date || message.internalDate),
|
|
548
681
|
subject: text(envelope.subject),
|
|
682
|
+
links,
|
|
549
683
|
attachments: attachmentsOf(message.bodyStructure),
|
|
550
684
|
};
|
|
551
685
|
const clipped = body.length > READ_CHARS ? `${body.slice(0, READ_CHARS)}\n[truncated]` : body;
|
|
@@ -588,6 +722,12 @@ const TOOLS = [
|
|
|
588
722
|
...MANAGE_TOOLS,
|
|
589
723
|
];
|
|
590
724
|
|
|
725
|
+
/** The tool definitions (name, description, schema, annotations) the server
|
|
726
|
+
* lists; with `hidden`, also the ones only the approval gate calls. */
|
|
727
|
+
export function gmailAppPasswordTools({ hidden = false } = {}) {
|
|
728
|
+
return TOOLS.filter((tool) => hidden || !tool.hidden).map(({ run, hidden: _hidden, ...tool }) => tool);
|
|
729
|
+
}
|
|
730
|
+
|
|
591
731
|
// ── The MCP server at the reserved address ──────────────────────────────────
|
|
592
732
|
const GMAIL_HOST = new URL(GMAIL_APP_PASSWORD.url).host;
|
|
593
733
|
|
|
@@ -616,18 +756,11 @@ async function rpc(session, message) {
|
|
|
616
756
|
protocolVersion: text(params?.protocolVersion) || PROTOCOL_VERSION,
|
|
617
757
|
capabilities: { tools: { listChanged: false } },
|
|
618
758
|
serverInfo: { name: 'mixdog-gmail-app-password', version: '1.0.0' },
|
|
619
|
-
instructions:
|
|
759
|
+
instructions: connectorAccountsInstructions([{ email: session.email }]),
|
|
620
760
|
});
|
|
621
761
|
}
|
|
622
762
|
if (method === 'ping') return reply({});
|
|
623
|
-
if (method === 'tools/list') {
|
|
624
|
-
return reply({
|
|
625
|
-
tools: TOOLS.filter((tool) => !tool.hidden).map(({ run, hidden, ...tool }) => ({
|
|
626
|
-
...tool,
|
|
627
|
-
description: `${tool.description} (Gmail account: ${session.email})`,
|
|
628
|
-
})),
|
|
629
|
-
});
|
|
630
|
-
}
|
|
763
|
+
if (method === 'tools/list') return reply({ tools: gmailAppPasswordTools() });
|
|
631
764
|
if (method === 'tools/call') {
|
|
632
765
|
const tool = TOOLS.find((candidate) => candidate.name === params?.name);
|
|
633
766
|
if (!tool) return { jsonrpc: '2.0', id, error: { code: -32602, message: `Unknown tool: ${params?.name}` } };
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// live in the keychain, and each MCP request carries the access token, which
|
|
10
10
|
// this server spends on the Gmail / Calendar / Drive REST APIs.
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { connectorAccountsInstructions } from '../shared/connector-accounts.mjs';
|
|
13
13
|
|
|
14
14
|
const PROTOCOL_VERSION = '2025-06-18';
|
|
15
15
|
const ORIGIN_SUFFIX = '.mixdog.invalid';
|
|
@@ -329,6 +329,12 @@ const SERVICES = {
|
|
|
329
329
|
drive: { name: 'Google Drive', tools: DRIVE_TOOLS },
|
|
330
330
|
};
|
|
331
331
|
|
|
332
|
+
/** The tool definitions (name, description, schema, annotations) of built-in
|
|
333
|
+
* Google service `service` (`gmail`, `calendar`, `drive`). */
|
|
334
|
+
export function googleMcpTools(service) {
|
|
335
|
+
return (SERVICES[service]?.tools || []).map(({ run, ...tool }) => tool);
|
|
336
|
+
}
|
|
337
|
+
|
|
332
338
|
/** The reserved MCP address of built-in Google service `service`. */
|
|
333
339
|
export function googleMcpUrl(service) {
|
|
334
340
|
return `https://${service}${ORIGIN_SUFFIX}/mcp`;
|
|
@@ -367,20 +373,12 @@ async function rpc(service, token, message) {
|
|
|
367
373
|
protocolVersion: text(params?.protocolVersion) || PROTOCOL_VERSION,
|
|
368
374
|
capabilities: { tools: { listChanged: false } },
|
|
369
375
|
serverInfo: { name: `mixdog-${service.key}`, version: '1.0.0' },
|
|
370
|
-
|
|
376
|
+
// The one account this sign-in is (the connector's accounts server reads it back).
|
|
377
|
+
instructions: connectorAccountsInstructions([{ email: info.email }]),
|
|
371
378
|
});
|
|
372
379
|
}
|
|
373
380
|
if (method === 'ping') return reply({});
|
|
374
|
-
if (method === 'tools/list') {
|
|
375
|
-
// Several accounts of one service are separate servers: each tool says whose it is.
|
|
376
|
-
const { email } = await tokenInfo(token).catch(() => ({ email: '' }));
|
|
377
|
-
return reply({
|
|
378
|
-
tools: service.tools.map(({ run, ...tool }) => ({
|
|
379
|
-
...tool,
|
|
380
|
-
...(email ? { description: `${tool.description} (${service.name} account: ${email})` } : {}),
|
|
381
|
-
})),
|
|
382
|
-
});
|
|
383
|
-
}
|
|
381
|
+
if (method === 'tools/list') return reply({ tools: googleMcpTools(service.key) });
|
|
384
382
|
if (method === 'tools/call') {
|
|
385
383
|
const tool = service.tools.find((candidate) => candidate.name === params?.name);
|
|
386
384
|
if (!tool) return { jsonrpc: '2.0', id, error: { code: -32602, message: `Unknown tool: ${params?.name}` } };
|
|
@@ -91,6 +91,7 @@ function installPoolErrorHandler(pool, label, { onConnectionLoss } = {}) {
|
|
|
91
91
|
|
|
92
92
|
import { resolve } from 'node:path';
|
|
93
93
|
import { ensurePgInstance as supervisorEnsure, stopOwnedPgForShutdown } from './supervisor.mjs';
|
|
94
|
+
import { createSharedAbortable } from '../../../shared/abort-race.mjs';
|
|
94
95
|
|
|
95
96
|
export { installOwnedPgExitStop, pgClusterExists } from './supervisor.mjs';
|
|
96
97
|
|
|
@@ -195,7 +196,7 @@ export function pendingDurablePgOps() {
|
|
|
195
196
|
* Wait, bounded, until no durable-schema operation is in flight (including
|
|
196
197
|
* ones issued while waiting). Resolves true when drained, false at the bound.
|
|
197
198
|
*/
|
|
198
|
-
|
|
199
|
+
async function drainDurablePgOps({ timeoutMs = 0 } = {}) {
|
|
199
200
|
const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
|
|
200
201
|
while (_durableInFlight.size > 0) {
|
|
201
202
|
const remaining = deadline - Date.now();
|
|
@@ -488,78 +489,84 @@ export async function ensurePgInstance(dataDir, opts = {}) {
|
|
|
488
489
|
const key = instanceKey(dataDir, schema);
|
|
489
490
|
|
|
490
491
|
if (instances.has(key)) return instances.get(key);
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
// check-then-create with a session-level advisory lock held on a single
|
|
517
|
-
// dedicated client. Concurrent first-boot workers block on the same
|
|
518
|
-
// cluster-global key here and only one issues the CREATE; the rest see
|
|
519
|
-
// the row present after the holder releases. Released in finally.
|
|
520
|
-
await withClusterAdvisoryLock(adminPool, ADVISORY_OBJID_CREATE_DB, 'create-db', async (adminClient) => {
|
|
521
|
-
const r = await adminClient.query(`SELECT 1 FROM pg_database WHERE datname = $1`, [PG_DB]);
|
|
522
|
-
if (r.rows.length === 0) {
|
|
523
|
-
await adminClient.query(`CREATE DATABASE ${PG_DB}`);
|
|
524
|
-
__mixdogMemoryLog(`[pg-adapter] created database ${PG_DB}\n`);
|
|
525
|
-
}
|
|
492
|
+
// `opts.signal` withdraws this caller; the shared open is cancelled once
|
|
493
|
+
// every caller has (createSharedAbortable).
|
|
494
|
+
if (opening.has(key)) return opening.get(key).join(opts.signal);
|
|
495
|
+
|
|
496
|
+
const shared = createSharedAbortable(
|
|
497
|
+
async (signal) => {
|
|
498
|
+
// 1. Let supervisor-pg own PG startup and health-checking.
|
|
499
|
+
// Returns { host, port, runtimeDir, pgdataDir }.
|
|
500
|
+
const { host, port, runtimeDir, pgdataDir } = await supervisorEnsure(dataDir, { signal });
|
|
501
|
+
signal.throwIfAborted();
|
|
502
|
+
|
|
503
|
+
// 2. Connect via node-postgres; auto-create the mixdog database if absent.
|
|
504
|
+
const { default: pg } = await import('pg');
|
|
505
|
+
|
|
506
|
+
const PG_USER = 'postgres';
|
|
507
|
+
const PG_DB = 'mixdog';
|
|
508
|
+
|
|
509
|
+
const adminPool = new pg.Pool({
|
|
510
|
+
host,
|
|
511
|
+
port,
|
|
512
|
+
user: PG_USER,
|
|
513
|
+
database: 'postgres',
|
|
514
|
+
password: '',
|
|
515
|
+
max: 1,
|
|
516
|
+
idleTimeoutMillis: 5_000,
|
|
526
517
|
});
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
518
|
+
installPoolErrorHandler(adminPool, 'admin-pool');
|
|
519
|
+
try {
|
|
520
|
+
// CREATE DATABASE cannot run inside a transaction, so guard the
|
|
521
|
+
// check-then-create with a session-level advisory lock held on a single
|
|
522
|
+
// dedicated client. Concurrent first-boot workers block on the same
|
|
523
|
+
// cluster-global key here and only one issues the CREATE; the rest see
|
|
524
|
+
// the row present after the holder releases. Released in finally.
|
|
525
|
+
await withClusterAdvisoryLock(adminPool, ADVISORY_OBJID_CREATE_DB, 'create-db', async (adminClient) => {
|
|
526
|
+
const r = await adminClient.query(`SELECT 1 FROM pg_database WHERE datname = $1`, [PG_DB]);
|
|
527
|
+
if (r.rows.length === 0) {
|
|
528
|
+
await adminClient.query(`CREATE DATABASE ${PG_DB}`);
|
|
529
|
+
__mixdogMemoryLog(`[pg-adapter] created database ${PG_DB}\n`);
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
} finally {
|
|
533
|
+
await adminPool.end();
|
|
534
|
+
}
|
|
530
535
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
// 4. Bootstrap extensions + schemas once (idempotent).
|
|
547
|
-
await bootstrapInstance(pgPool, resolve(dataDir));
|
|
548
|
-
|
|
549
|
-
// 5. Build the compat db shim.
|
|
550
|
-
const db = makeCompatDb(pgPool, schema, dataDir);
|
|
551
|
-
|
|
552
|
-
const result = { db, pool: pgPool, host, port, runtimeDir, pgdataDir };
|
|
553
|
-
instances.set(key, result);
|
|
554
|
-
return result;
|
|
555
|
-
})();
|
|
536
|
+
// 3. Production pool.
|
|
537
|
+
const pgPool = new pg.Pool({
|
|
538
|
+
host,
|
|
539
|
+
port,
|
|
540
|
+
user: PG_USER,
|
|
541
|
+
database: PG_DB,
|
|
542
|
+
password: '',
|
|
543
|
+
max: 5,
|
|
544
|
+
idleTimeoutMillis: 30_000,
|
|
545
|
+
connectionTimeoutMillis: 10_000,
|
|
546
|
+
});
|
|
547
|
+
installPoolErrorHandler(pgPool, `pool:${schema}`, {
|
|
548
|
+
onConnectionLoss: () => _recoverPgConnection(dataDir, schema),
|
|
549
|
+
});
|
|
556
550
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
551
|
+
// 4. Bootstrap extensions + schemas once (idempotent).
|
|
552
|
+
await bootstrapInstance(pgPool, resolve(dataDir));
|
|
553
|
+
|
|
554
|
+
// 5. Build the compat db shim.
|
|
555
|
+
const db = makeCompatDb(pgPool, schema, dataDir);
|
|
556
|
+
|
|
557
|
+
const result = { db, pool: pgPool, host, port, runtimeDir, pgdataDir };
|
|
558
|
+
instances.set(key, result);
|
|
559
|
+
return result;
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
onSettled: () => {
|
|
563
|
+
if (opening.get(key) === shared) opening.delete(key);
|
|
564
|
+
},
|
|
565
|
+
}
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
opening.set(key, shared);
|
|
569
|
+
return shared.join(opts.signal);
|
|
563
570
|
}
|
|
564
571
|
|
|
565
572
|
// ---------------------------------------------------------------------------
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
import { withFileLockSync } from '../../../shared/atomic-file.mjs';
|
|
31
31
|
import { isPidAlive } from '../../../shared/pid-liveness.mjs';
|
|
32
32
|
import { envFlag } from '../../../shared/env.mjs';
|
|
33
|
+
import { createSharedAbortable } from '../../../shared/abort-race.mjs';
|
|
33
34
|
import { ensurePrivateRuntimeRoot, resolveRuntimeRoot } from '../../../shared/runtime-root.mjs';
|
|
34
35
|
|
|
35
36
|
let _pgProc = null;
|
|
@@ -51,7 +52,7 @@ async function _getPgProc() {
|
|
|
51
52
|
/** @type {{ port: number, pgdata: string, runtimeDir: string, proc: unknown } | null} */
|
|
52
53
|
let _live = null;
|
|
53
54
|
/** Dedup: one ensure coroutine at a time. */
|
|
54
|
-
let
|
|
55
|
+
let _ensureShared = null;
|
|
55
56
|
/** Per-process flag — postgresql.conf reconcile attempted once per supervisor. */
|
|
56
57
|
let _v2ReconcileTried = false;
|
|
57
58
|
/** Set by stopOwnedPgForShutdown: this process is exiting, no further ensure. */
|
|
@@ -542,7 +543,7 @@ async function tryReusePgInstance({ pgdata, runtimeDir, healthcheckPg, source =
|
|
|
542
543
|
return null;
|
|
543
544
|
}
|
|
544
545
|
|
|
545
|
-
// ── Internal: full ensure logic (runs exclusively via
|
|
546
|
+
// ── Internal: full ensure logic (runs exclusively via _ensureShared) ──────────
|
|
546
547
|
|
|
547
548
|
function _pgdataDir(dataDir) {
|
|
548
549
|
return join(dataDir, 'pgdata');
|
|
@@ -555,13 +556,15 @@ export function pgClusterExists(dataDir) {
|
|
|
555
556
|
return existsSync(join(_pgdataDir(dataDir), 'PG_VERSION'));
|
|
556
557
|
}
|
|
557
558
|
|
|
558
|
-
async function _doEnsure(dataDir) {
|
|
559
|
+
async function _doEnsure(dataDir, signal) {
|
|
559
560
|
const { healthcheckPg } = await _getPgProc();
|
|
560
561
|
const pgdata = _pgdataDir(dataDir);
|
|
561
562
|
|
|
562
563
|
// Resolve runtimeDir via runtime-fetcher (cache-hits immediately when already downloaded).
|
|
563
564
|
const { ensureRuntime } = await import('../runtime-fetcher.mjs');
|
|
564
|
-
const { runtimeDir } = await ensureRuntime(dataDir);
|
|
565
|
+
const { runtimeDir } = await ensureRuntime(dataDir, { signal });
|
|
566
|
+
// Cancelled while installing: start no cluster for callers that are gone.
|
|
567
|
+
signal.throwIfAborted();
|
|
565
568
|
|
|
566
569
|
// One-shot v2 conf reconcile — idempotent. Covers attach paths (in-process
|
|
567
570
|
// fast path / cross-process active-instance reuse) where startPg is never
|
|
@@ -713,10 +716,15 @@ async function _doEnsure(dataDir) {
|
|
|
713
716
|
* Concurrent calls are deduplicated — exactly one spawn/recover sequence runs
|
|
714
717
|
* at a time; additional callers await the same promise.
|
|
715
718
|
*
|
|
719
|
+
* `signal` (optional) withdraws this caller: the shared ensure is cancelled
|
|
720
|
+
* (its runtime install included) only once every caller has withdrawn and
|
|
721
|
+
* none joined without a signal (createSharedAbortable).
|
|
722
|
+
*
|
|
716
723
|
* @param {string} dataDir Mixdog data directory.
|
|
724
|
+
* @param {{ signal?: AbortSignal }} [opts]
|
|
717
725
|
* @returns {Promise<{ host: string, port: number, runtimeDir: string, pgdataDir: string }>}
|
|
718
726
|
*/
|
|
719
|
-
export function ensurePgInstance(dataDir) {
|
|
727
|
+
export function ensurePgInstance(dataDir, { signal = null } = {}) {
|
|
720
728
|
// Once this process began stopping the cluster it owns, nothing may start
|
|
721
729
|
// or re-attach one: a late connection-loss recovery would otherwise bring a
|
|
722
730
|
// postmaster back up right after the stop and leave it running past exit.
|
|
@@ -727,12 +735,15 @@ export function ensurePgInstance(dataDir) {
|
|
|
727
735
|
// in-process fast path — so this is the one place the sweep cannot be
|
|
728
736
|
// skipped. Latched internally, so repeated calls schedule it only once.
|
|
729
737
|
scheduleOrphanTempPostmasterSweep();
|
|
730
|
-
if (!
|
|
731
|
-
|
|
732
|
-
|
|
738
|
+
if (!_ensureShared) {
|
|
739
|
+
const shared = createSharedAbortable((sharedSignal) => _doEnsure(dataDir, sharedSignal), {
|
|
740
|
+
onSettled: () => {
|
|
741
|
+
if (_ensureShared === shared) _ensureShared = null;
|
|
742
|
+
},
|
|
733
743
|
});
|
|
744
|
+
_ensureShared = shared;
|
|
734
745
|
}
|
|
735
|
-
return
|
|
746
|
+
return _ensureShared.join(signal);
|
|
736
747
|
}
|
|
737
748
|
|
|
738
749
|
/**
|
|
@@ -779,7 +790,7 @@ export async function stopPgForShutdown() {
|
|
|
779
790
|
return;
|
|
780
791
|
}
|
|
781
792
|
const snap = _live;
|
|
782
|
-
|
|
793
|
+
_ensureShared = null;
|
|
783
794
|
try {
|
|
784
795
|
const { stopPg } = await _getPgProc();
|
|
785
796
|
await stopPg({ runtimeDir: snap.runtimeDir, pgdataDir: snap.pgdata });
|
|
@@ -865,10 +876,11 @@ export async function stopOwnedPgForShutdown({ timeoutMs = 6_000 } = {}) {
|
|
|
865
876
|
// publishes its cluster only when it settles; stopping before that would
|
|
866
877
|
// miss the postmaster it is about to own. Bounded: a cold first start can
|
|
867
878
|
// be a download.
|
|
868
|
-
|
|
879
|
+
const inFlight = _ensureShared?.promise;
|
|
880
|
+
if (inFlight) {
|
|
869
881
|
let timer = null;
|
|
870
882
|
await Promise.race([
|
|
871
|
-
|
|
883
|
+
inFlight.catch(() => {}),
|
|
872
884
|
new Promise((resolveWait) => {
|
|
873
885
|
timer = setTimeout(resolveWait, timeoutMs);
|
|
874
886
|
timer.unref?.();
|