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
|
@@ -107,8 +107,17 @@ export function wireApis(boot) {
|
|
|
107
107
|
try {
|
|
108
108
|
boot.resourceApi.migratePluginMcpCopies();
|
|
109
109
|
} catch (error) {
|
|
110
|
+
console.warn(`[mcp] adopting plugin MCP copies failed: ${error?.message || error}`);
|
|
110
111
|
bootProfile('plugin-mcp:adopt-failed', { error: error?.message || String(error) });
|
|
111
112
|
}
|
|
113
|
+
// One server per multi-account connector: `gmail`, `gmail-2`… merge into
|
|
114
|
+
// `gmail` holding the accounts (their sign-ins and app passwords stay).
|
|
115
|
+
try {
|
|
116
|
+
boot.resourceApi.migrateConnectorAccounts();
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.warn(`[connectors] merging connector accounts failed: ${error?.message || error}`);
|
|
119
|
+
bootProfile('connector-accounts:merge-failed', { error: error?.message || String(error) });
|
|
120
|
+
}
|
|
112
121
|
}
|
|
113
122
|
boot.disposeGlobalExtensionSubscription = () => {
|
|
114
123
|
boot.disposeConfigSync?.();
|
|
@@ -20,6 +20,7 @@ import { listProjectsStrict } from '../../runtime/shared/projects.mjs';
|
|
|
20
20
|
import { isWithin } from '../../runtime/shared/worktrees/paths.mjs';
|
|
21
21
|
import { standaloneDataDir } from '../runtime-paths.mjs';
|
|
22
22
|
import { SessionAddressError, isAdmissionOpen, ownerLifecycle } from '../session-address.mjs';
|
|
23
|
+
import { syncSessionCompaction, usesInjectedConfig } from '../session-compaction-sync.mjs';
|
|
23
24
|
import { dataDirOf } from './shared.mjs';
|
|
24
25
|
|
|
25
26
|
export function wireInfrastructure(boot) {
|
|
@@ -187,6 +188,9 @@ export function sessionAdopter(boot) {
|
|
|
187
188
|
}
|
|
188
189
|
}
|
|
189
190
|
rt.session = v;
|
|
191
|
+
// A resumed or swapped-in session keeps the compaction it was saved with
|
|
192
|
+
// unless it follows the current global settings (not on an injected config).
|
|
193
|
+
if (v?.id && !usesInjectedConfig(boot.params)) syncSessionCompaction(v, rt.config);
|
|
190
194
|
// The PreToolUse/PostToolUse bridge is a non-enumerable property that
|
|
191
195
|
// neither the store nor a resumed record carries: every session installed
|
|
192
196
|
// here (create, resume, route swap, clear, the manager's copy) gets it, so
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from '../model-recency.mjs';
|
|
32
32
|
import { createNativeWebSearch } from '../native-web-search.mjs';
|
|
33
33
|
import { createConfigLifecycle } from '../config-lifecycle.mjs';
|
|
34
|
+
import { usesInjectedConfig } from '../session-compaction-sync.mjs';
|
|
34
35
|
import { createQuickModelRows } from '../quick-model-rows.mjs';
|
|
35
36
|
import { createProviderModels } from '../provider-models.mjs';
|
|
36
37
|
import { createProviderUsage } from '../provider-usage.mjs';
|
|
@@ -66,6 +67,8 @@ function wireConfigLifecycle(boot) {
|
|
|
66
67
|
rt.configHasSecrets = next;
|
|
67
68
|
},
|
|
68
69
|
getRoute: () => rt.route,
|
|
70
|
+
getSession: () => rt.session,
|
|
71
|
+
fixedSessionCompaction: usesInjectedConfig(boot.params),
|
|
69
72
|
setRouteState: (next) => {
|
|
70
73
|
rt.route = next;
|
|
71
74
|
},
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
channelNotificationSessionId,
|
|
10
10
|
} from '../../runtime/shared/channel-notification-routing.mjs';
|
|
11
11
|
import { CHANNEL_NOTIFICATION_METHOD } from '../services/channel-session-router.mjs';
|
|
12
|
+
import { PROMPT_ORIGIN_CHANNEL } from '../../runtime/agent/orchestrator/session/manager/pending-message-entry.mjs';
|
|
12
13
|
import { envFlag } from '../../runtime/shared/env.mjs';
|
|
13
14
|
// Desktop-app bridges: tiny fs/fetch clients, so a static import adds no
|
|
14
15
|
// meaningful boot cost. The tools themselves are gated per session by the
|
|
@@ -144,7 +145,9 @@ function wireChannels(boot) {
|
|
|
144
145
|
const meta = p.meta && typeof p.meta === 'object' ? p.meta : {};
|
|
145
146
|
const content = channelNotificationModelContent(p);
|
|
146
147
|
if (!content) return;
|
|
147
|
-
notifySession(channelNotificationSessionId(rt.session, rt.reservedSessionId), content, meta
|
|
148
|
+
notifySession(channelNotificationSessionId(rt.session, rt.reservedSessionId), content, meta, {
|
|
149
|
+
origin: PROMPT_ORIGIN_CHANNEL,
|
|
150
|
+
});
|
|
148
151
|
},
|
|
149
152
|
});
|
|
150
153
|
bootProfile('channels:worker-ready', { ms: (performance.now() - channelsStartedAt).toFixed(1) });
|
|
@@ -234,6 +237,8 @@ function wireInternalTools(boot) {
|
|
|
234
237
|
getCallerSession: (sessionId) =>
|
|
235
238
|
sessionId === rt.session?.id ? rt.session : boot.mgr?.getSession?.(sessionId) || null,
|
|
236
239
|
flushSettings: () => boot.flushAllConfigSavesAsync({ requireSaved: true }),
|
|
240
|
+
// Another session's route and quota actions (daemon-session-control.mjs); null without a daemon.
|
|
241
|
+
executeSessionControl: boot.params.executeSessionControl || null,
|
|
237
242
|
});
|
|
238
243
|
const disposeInternalTools = internalTools.setInternalToolsProvider({
|
|
239
244
|
scopeId: rt.mcpScopeId,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
stripStoredRouteDefaults,
|
|
18
18
|
withDefaultPresets,
|
|
19
19
|
} from '../runtime/agent/orchestrator/config-presets.mjs';
|
|
20
|
+
import { syncSessionCompaction } from './session-compaction-sync.mjs';
|
|
20
21
|
import { createConfigWriters } from './config-lifecycle/config-writers.mjs';
|
|
21
22
|
import { createLeadRouteSync } from './config-lifecycle/lead-route-sync.mjs';
|
|
22
23
|
import { createOutputStyleStatusCache } from './config-lifecycle/output-style-cache.mjs';
|
|
@@ -134,6 +135,10 @@ export function createConfigLifecycle({
|
|
|
134
135
|
getConfigHasSecrets,
|
|
135
136
|
setConfigHasSecrets,
|
|
136
137
|
getRoute,
|
|
138
|
+
// The live session follows the reloaded global compaction settings, unless
|
|
139
|
+
// the runtime runs on an injected (headless) config.
|
|
140
|
+
getSession,
|
|
141
|
+
fixedSessionCompaction = false,
|
|
137
142
|
// Main route sync on reload (both optional: a lifecycle without them never syncs)
|
|
138
143
|
setRouteState,
|
|
139
144
|
resolveRoute,
|
|
@@ -222,6 +227,7 @@ export function createConfigLifecycle({
|
|
|
222
227
|
const adopted = adoptConfig(next, { hasSecrets: true });
|
|
223
228
|
configSaveBaseline = structuredClone(adopted);
|
|
224
229
|
leadRoute.syncEmptyMainRoute(adopted);
|
|
230
|
+
if (!fixedSessionCompaction) syncSessionCompaction(getSession?.(), adopted);
|
|
225
231
|
return adopted;
|
|
226
232
|
}
|
|
227
233
|
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// After the multi-account connector merge (connector-catalog.mjs
|
|
2
|
+
// mergeConnectorAccountServers) `gmail-2`, `google-calendar-2`… are accounts of
|
|
3
|
+
// one server, no longer servers. Every store of ours that keys a setting by
|
|
4
|
+
// MCP server name must name the surviving server instead:
|
|
5
|
+
// agents/<id>/agent.json, bots/<id>/agent.json connectors.off / connectors.tools
|
|
6
|
+
// (a legacy allow list too)
|
|
7
|
+
// <data>/project-connectors.json projects.<root>.off
|
|
8
|
+
// <data>/hooks.json, <data>/hooks/hooks.json mcp_tool `server`, tool matchers
|
|
9
|
+
// (extension scopes are the config's and are dropped by migrateConnectorAccounts;
|
|
10
|
+
// an approval's "always allow" is the agent's connectors.tools; agent routes
|
|
11
|
+
// name models, not servers.) Project and plugin hook files are the user's or
|
|
12
|
+
// the plugin's own and are never rewritten here.
|
|
13
|
+
//
|
|
14
|
+
// Rule: the merged connector is OFF for an agent/project only when EVERY
|
|
15
|
+
// merged server was off (the same rule as the merged server's `enabled`: one
|
|
16
|
+
// working account keeps the connector usable). Per-tool permissions move to
|
|
17
|
+
// the surviving name; where two merged servers disagree on a tool the MORE
|
|
18
|
+
// PERMISSIVE permission wins, as "on beats off" does at server level. Entries
|
|
19
|
+
// of a server that was off are dormant and are dropped when the connector
|
|
20
|
+
// stays on, so they never become live. A setting that names no merged-away
|
|
21
|
+
// server is never touched, which makes every run after the first a no-op.
|
|
22
|
+
//
|
|
23
|
+
// The merge is saved to the config once, so the rename mapping outlives it:
|
|
24
|
+
// `<data>/connector-merge-pending.json` ({ merged: [{ name, from }] }) is
|
|
25
|
+
// written before any store is touched and deleted only when every store
|
|
26
|
+
// migrated. A store that fails (unreadable, malformed, not writable) is
|
|
27
|
+
// reported and the others still go on; the next start retries them all.
|
|
28
|
+
//
|
|
29
|
+
// Hooks: only tool identifiers are rewritten — an `mcp_tool` handler's
|
|
30
|
+
// `server`, and a matcher / tool-name field (or the tool part of an `if`
|
|
31
|
+
// before its `(`) that is an `mcp__<server>__…` identifier or an
|
|
32
|
+
// alternative of a pattern. Argument conditions such as `Bash(mcp__x__*)`
|
|
33
|
+
// and any other data are never touched.
|
|
34
|
+
import { readdirSync, readFileSync, rmSync } from 'node:fs';
|
|
35
|
+
import { join } from 'node:path';
|
|
36
|
+
import { normalizeRules, standardConfigReport } from './services/hook-bus/config.mjs';
|
|
37
|
+
import {
|
|
38
|
+
PERMISSION_RANK,
|
|
39
|
+
PROJECT_CONNECTORS_FILE,
|
|
40
|
+
connectorEnabled,
|
|
41
|
+
parseConnectorPolicy,
|
|
42
|
+
storedConnectorPolicy,
|
|
43
|
+
} from '../runtime/agent/orchestrator/mcp/agent-connectors.mjs';
|
|
44
|
+
import { parseJsonText, withFileLockSync, writeJsonAtomicSync } from '../runtime/shared/atomic-file.mjs';
|
|
45
|
+
|
|
46
|
+
const unique = (list) => [...new Set(list)].sort();
|
|
47
|
+
|
|
48
|
+
/** The policy `{ off, tools }` (or a legacy `{ allow, off, tools }`) with every
|
|
49
|
+
* merged-away server of `merged` mapped onto its surviving one; null when it
|
|
50
|
+
* names none. `merged` is mergeConnectorAccountServers' `merged`. */
|
|
51
|
+
function mapMergedConnectorPolicy(policy, merged) {
|
|
52
|
+
let current = policy;
|
|
53
|
+
for (const { name, from } of merged) {
|
|
54
|
+
const gone = from.filter((server) => server !== name);
|
|
55
|
+
const named = (server) =>
|
|
56
|
+
current.off.includes(server) || current.allow?.includes(server) || Object.hasOwn(current.tools, server);
|
|
57
|
+
if (!gone.some(named)) continue;
|
|
58
|
+
const live = from.filter((server) => connectorEnabled(current, server));
|
|
59
|
+
const on = live.length > 0;
|
|
60
|
+
const off = current.off.filter((server) => !from.includes(server));
|
|
61
|
+
const allow = current.allow?.filter((server) => !from.includes(server));
|
|
62
|
+
if (allow && on) allow.push(name);
|
|
63
|
+
if (!allow && !on) off.push(name);
|
|
64
|
+
const tools = { ...current.tools };
|
|
65
|
+
for (const server of from) delete tools[server];
|
|
66
|
+
const moved = {};
|
|
67
|
+
for (const server of on ? live : from) {
|
|
68
|
+
for (const [tool, permission] of Object.entries(current.tools[server] || {})) {
|
|
69
|
+
if (!(tool in moved) || PERMISSION_RANK[permission] > PERMISSION_RANK[moved[tool]]) moved[tool] = permission;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (Object.keys(moved).length) tools[name] = moved;
|
|
73
|
+
current = { ...(allow ? { allow: unique(allow) } : {}), off: unique(off), tools };
|
|
74
|
+
}
|
|
75
|
+
return current === policy ? null : current;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A mapped policy as the value the agent.json `connectors` field stores
|
|
79
|
+
* (undefined: the field goes). */
|
|
80
|
+
function storedValue(policy) {
|
|
81
|
+
if (!policy.allow) return storedConnectorPolicy(policy) ?? undefined;
|
|
82
|
+
const ids = unique([...policy.allow, ...Object.keys(policy.tools)]);
|
|
83
|
+
return ids.map((id) => ({
|
|
84
|
+
id,
|
|
85
|
+
enabled: policy.allow.includes(id),
|
|
86
|
+
...(policy.tools[id] ? { tools: policy.tools[id] } : {}),
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const PENDING_MERGE_FILE = 'connector-merge-pending.json';
|
|
91
|
+
|
|
92
|
+
const warn = (file, error) => process.stderr.write(`[connectors] ${file} not migrated (${error?.message || error})\n`);
|
|
93
|
+
|
|
94
|
+
/** The JSON at `file`; null when it does not exist. Any other failure throws. */
|
|
95
|
+
function readJsonFile(file) {
|
|
96
|
+
try {
|
|
97
|
+
return parseJsonText(readFileSync(file, 'utf8'));
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') return null;
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
105
|
+
|
|
106
|
+
/** Runs `step` for one store; a failure is reported and returned, never thrown. */
|
|
107
|
+
function attempt(file, step, outcome) {
|
|
108
|
+
try {
|
|
109
|
+
step();
|
|
110
|
+
} catch (error) {
|
|
111
|
+
warn(file, error);
|
|
112
|
+
outcome.failed.push(file);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function migrateAgentFiles(dataDir, merged, outcome, readdir) {
|
|
117
|
+
for (const store of ['agents', 'bots']) {
|
|
118
|
+
const dir = join(dataDir, store);
|
|
119
|
+
let entries;
|
|
120
|
+
try {
|
|
121
|
+
entries = readdir(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory());
|
|
122
|
+
} catch (error) {
|
|
123
|
+
// A store that does not exist has nothing to migrate; any other failure
|
|
124
|
+
// leaves its agents unprocessed, so the mapping must stay pending.
|
|
125
|
+
if (error?.code !== 'ENOENT') {
|
|
126
|
+
warn(dir, error);
|
|
127
|
+
outcome.failed.push(dir);
|
|
128
|
+
}
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
const file = join(dataDir, store, entry.name, 'agent.json');
|
|
133
|
+
attempt(
|
|
134
|
+
file,
|
|
135
|
+
() => {
|
|
136
|
+
const manifest = readJsonFile(file);
|
|
137
|
+
if (!isRecord(manifest) || manifest.connectors == null) return;
|
|
138
|
+
const next = mapMergedConnectorPolicy(parseConnectorPolicy(manifest.connectors), merged);
|
|
139
|
+
if (!next) return;
|
|
140
|
+
const value = storedValue(next);
|
|
141
|
+
if (value) manifest.connectors = value;
|
|
142
|
+
else delete manifest.connectors;
|
|
143
|
+
writeJsonAtomicSync(file, manifest, { backup: true });
|
|
144
|
+
outcome.agents.push(`${store}/${entry.name}`);
|
|
145
|
+
},
|
|
146
|
+
outcome
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function migrateProjectFile(dataDir, merged, outcome) {
|
|
153
|
+
const file = join(dataDir, PROJECT_CONNECTORS_FILE);
|
|
154
|
+
attempt(
|
|
155
|
+
file,
|
|
156
|
+
() => {
|
|
157
|
+
withFileLockSync(`${file}.lock`, () => {
|
|
158
|
+
const document = readJsonFile(file);
|
|
159
|
+
const projects = document?.projects;
|
|
160
|
+
if (!isRecord(projects)) return;
|
|
161
|
+
const changed = [];
|
|
162
|
+
for (const [root, entry] of Object.entries(projects)) {
|
|
163
|
+
if (!isRecord(entry) || !Array.isArray(entry.off)) continue;
|
|
164
|
+
const next = mapMergedConnectorPolicy({ off: entry.off.map(String), tools: {} }, merged);
|
|
165
|
+
if (!next) continue;
|
|
166
|
+
if (next.off.length) projects[root] = { ...entry, off: next.off };
|
|
167
|
+
else delete projects[root];
|
|
168
|
+
changed.push(root);
|
|
169
|
+
}
|
|
170
|
+
if (!changed.length) return;
|
|
171
|
+
writeJsonAtomicSync(file, document, { lock: false, backup: true });
|
|
172
|
+
outcome.projects.push(...changed);
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
outcome
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
180
|
+
|
|
181
|
+
/** A tool identifier or pattern (`mcp__a__x`, `^mcp__a__.*$`, `Bash|mcp__a__x`)
|
|
182
|
+
* with each `mcp__<merged-away>` alternative renamed. */
|
|
183
|
+
function renameToolPattern(value, renames) {
|
|
184
|
+
let next = value;
|
|
185
|
+
for (const [gone, name] of renames) {
|
|
186
|
+
const identifier = new RegExp(`(^|\\|)(\\^?)mcp__${escapeRegExp(gone)}(?=__|(?![A-Za-z0-9_-]))`, 'g');
|
|
187
|
+
next = next.replace(identifier, (_all, lead, anchor) => `${lead}${anchor}mcp__${name}`);
|
|
188
|
+
}
|
|
189
|
+
return next;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** A condition `Tool(arguments)`: only the tool part is an identifier. */
|
|
193
|
+
function renameCondition(value, renames) {
|
|
194
|
+
const open = value.indexOf('(');
|
|
195
|
+
return open < 0
|
|
196
|
+
? renameToolPattern(value, renames)
|
|
197
|
+
: renameToolPattern(value.slice(0, open), renames) + value.slice(open);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Renames in the hook structures of `document` (in place); true when any changed. */
|
|
201
|
+
function renameInHooks(document, renames) {
|
|
202
|
+
let changed = false;
|
|
203
|
+
const set = (owner, key, rename) => {
|
|
204
|
+
if (typeof owner[key] !== 'string') return;
|
|
205
|
+
const next = rename(owner[key], renames);
|
|
206
|
+
if (next === owner[key]) return;
|
|
207
|
+
owner[key] = next;
|
|
208
|
+
changed = true;
|
|
209
|
+
};
|
|
210
|
+
const handler = (hook) => {
|
|
211
|
+
if (!isRecord(hook)) return;
|
|
212
|
+
if (hook.type === 'mcp_tool') {
|
|
213
|
+
if (typeof hook.server === 'string' && renames.has(hook.server.trim())) {
|
|
214
|
+
hook.server = renames.get(hook.server.trim());
|
|
215
|
+
changed = true;
|
|
216
|
+
}
|
|
217
|
+
set(hook, 'tool', renameToolPattern);
|
|
218
|
+
}
|
|
219
|
+
set(hook, 'if', renameCondition);
|
|
220
|
+
};
|
|
221
|
+
if (standardConfigReport(document).standard) {
|
|
222
|
+
for (const groups of Object.values(document.hooks)) {
|
|
223
|
+
for (const group of Array.isArray(groups) ? groups : []) {
|
|
224
|
+
if (!isRecord(group)) continue;
|
|
225
|
+
set(group, 'matcher', renameToolPattern);
|
|
226
|
+
set(group, 'if', renameCondition);
|
|
227
|
+
for (const hook of Array.isArray(group.hooks) ? group.hooks : []) handler(hook);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
for (const rule of normalizeRules(document)) {
|
|
232
|
+
if (!isRecord(rule)) continue;
|
|
233
|
+
set(rule, 'tool', renameToolPattern);
|
|
234
|
+
set(rule, 'name', renameToolPattern);
|
|
235
|
+
set(rule, 'if', renameCondition);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return changed;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function migrateHookFiles(dataDir, merged, outcome) {
|
|
242
|
+
const renames = new Map(
|
|
243
|
+
merged.flatMap(({ name, from }) => from.filter((server) => server !== name).map((gone) => [gone, name]))
|
|
244
|
+
);
|
|
245
|
+
for (const file of [join(dataDir, 'hooks.json'), join(dataDir, 'hooks', 'hooks.json')]) {
|
|
246
|
+
attempt(
|
|
247
|
+
file,
|
|
248
|
+
() => {
|
|
249
|
+
const document = readJsonFile(file);
|
|
250
|
+
if (!isRecord(document) && !Array.isArray(document)) return;
|
|
251
|
+
if (!renameInHooks(document, renames)) return;
|
|
252
|
+
writeJsonAtomicSync(file, document, { backup: true });
|
|
253
|
+
outcome.hooks.push(file);
|
|
254
|
+
},
|
|
255
|
+
outcome
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const validGroup = (group) =>
|
|
261
|
+
isRecord(group) &&
|
|
262
|
+
typeof group.name === 'string' &&
|
|
263
|
+
Array.isArray(group.from) &&
|
|
264
|
+
group.from.every((n) => typeof n === 'string');
|
|
265
|
+
|
|
266
|
+
/** Maps every per-server setting of `dataDir` onto the surviving servers of
|
|
267
|
+
* `merged` (this start's merge, [] to only retry) plus any still pending from
|
|
268
|
+
* an earlier start. Returns what it rewrote and the files that failed:
|
|
269
|
+
* `{ agents, projects, hooks, failed, unrecorded? }`. The pending record is
|
|
270
|
+
* removed only when `failed` is empty; `unrecorded` says it could not be
|
|
271
|
+
* written, so the caller must not save the merge. `readdir` lists the agent
|
|
272
|
+
* and bot directories. */
|
|
273
|
+
export function migrateMergedConnectorServers(dataDir, merged, { readdir = readdirSync } = {}) {
|
|
274
|
+
const outcome = { agents: [], projects: [], hooks: [], failed: [] };
|
|
275
|
+
if (!dataDir) return outcome;
|
|
276
|
+
const pendingFile = join(dataDir, PENDING_MERGE_FILE);
|
|
277
|
+
let pending = [];
|
|
278
|
+
try {
|
|
279
|
+
const stored = readJsonFile(pendingFile)?.merged;
|
|
280
|
+
pending = Array.isArray(stored) ? stored.filter(validGroup).map(({ name, from }) => ({ name, from })) : [];
|
|
281
|
+
} catch (error) {
|
|
282
|
+
warn(pendingFile, error);
|
|
283
|
+
}
|
|
284
|
+
const groups = [...pending];
|
|
285
|
+
for (const { name, from } of merged) {
|
|
286
|
+
if (!groups.some((group) => group.name === name && JSON.stringify(group.from) === JSON.stringify(from))) {
|
|
287
|
+
groups.push({ name, from });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (!groups.length) return outcome;
|
|
291
|
+
if (groups.length !== pending.length) {
|
|
292
|
+
try {
|
|
293
|
+
writeJsonAtomicSync(pendingFile, { merged: groups }, { backup: false });
|
|
294
|
+
} catch (error) {
|
|
295
|
+
warn(pendingFile, error);
|
|
296
|
+
// Nothing records the mapping: a failed store must keep the merge unsaved.
|
|
297
|
+
outcome.unrecorded = true;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
migrateAgentFiles(dataDir, groups, outcome, readdir);
|
|
301
|
+
migrateProjectFile(dataDir, groups, outcome);
|
|
302
|
+
migrateHookFiles(dataDir, groups, outcome);
|
|
303
|
+
if (!outcome.failed.length) rmSync(pendingFile, { force: true });
|
|
304
|
+
return outcome;
|
|
305
|
+
}
|
|
@@ -8,29 +8,73 @@ import {
|
|
|
8
8
|
} from '../../runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs';
|
|
9
9
|
import { createCallerContextResolvers } from './caller-context.mjs';
|
|
10
10
|
import { runAbortable } from '../../runtime/shared/abort-race.mjs';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
HANDOFF_OUTCOMES as AGENT_OUTCOMES,
|
|
13
|
+
SECRET_REQUEST_TIMEOUT_MS,
|
|
14
|
+
} from '../../runtime/agent/orchestrator/session/human-gate/requests.mjs';
|
|
12
15
|
import {
|
|
13
16
|
agentIdForSession,
|
|
14
17
|
recordHumanDecision,
|
|
15
18
|
unattendedApprovalPort,
|
|
16
19
|
} from '../../runtime/agent/orchestrator/session/human-gate/gate.mjs';
|
|
17
|
-
import { browserSecretInput } from '../../runtime/browser-bridge/client.mjs';
|
|
18
|
-
import { computerSecretInput } from '../../runtime/computer-bridge/client.mjs';
|
|
20
|
+
import { browserHandoffPage, browserSecretInput } from '../../runtime/browser-bridge/client.mjs';
|
|
21
|
+
import { computerSecretBind, computerSecretInput } from '../../runtime/computer-bridge/client.mjs';
|
|
22
|
+
|
|
23
|
+
/** The settle reason of a takeover that expired (desktop takeover-gate.ts
|
|
24
|
+
* TAKEOVER_EXPIRED_REASON). */
|
|
25
|
+
const TAKEOVER_EXPIRED_REASON = 'expired';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The agent's result of a secret request, from the card's answer and the
|
|
29
|
+
* desktop's fill: one of requests.mjs HANDOFF_OUTCOMES. `fill` runs only for a provided
|
|
30
|
+
* value and answers 'entered' / 'no_field' / 'page_changed' / 'blocked' /
|
|
31
|
+
* 'expired'.
|
|
32
|
+
*/
|
|
33
|
+
export async function secretOutcome(result, fill) {
|
|
34
|
+
if (result?.timedOut === true || result?.reason === TAKEOVER_EXPIRED_REASON) return 'expired';
|
|
35
|
+
if (result?.approved !== true || result.response !== 'provide-secret') return 'user_skipped';
|
|
36
|
+
const filled = await fill();
|
|
37
|
+
return AGENT_OUTCOMES.includes(filled) && filled !== 'user_skipped' ? filled : 'blocked';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A page address as the agent may see it: origin and path, no query or
|
|
41
|
+
* fragment (a sign-in redirect can carry a code or token there). */
|
|
42
|
+
function factUrl(value) {
|
|
43
|
+
try {
|
|
44
|
+
const url = new URL(String(value || ''));
|
|
45
|
+
return url.protocol === 'http:' || url.protocol === 'https:' ? `${url.origin}${url.pathname}` : '';
|
|
46
|
+
} catch {
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The agent's result of a takeover: what the user did and where the page is
|
|
52
|
+
* now, never "signed in" (the agent checks the page). */
|
|
53
|
+
export function takeoverOutcome(result, pageUrl = '') {
|
|
54
|
+
if (result?.timedOut === true || result?.reason === TAKEOVER_EXPIRED_REASON) return 'expired';
|
|
55
|
+
if (result?.approved !== true) return 'user_skipped';
|
|
56
|
+
const url = factUrl(pageUrl);
|
|
57
|
+
const did = result.reason === 'saved_login' ? 'User chose a saved login; it was filled in' : 'User pressed Done';
|
|
58
|
+
return url ? `${did}; page now at ${url}` : did;
|
|
59
|
+
}
|
|
19
60
|
|
|
20
61
|
/**
|
|
21
|
-
* A secret or takeover handoff. Secret:
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* (
|
|
27
|
-
* main/takeover-gate.ts); the result is '
|
|
62
|
+
* A secret or takeover handoff. Secret: at request time the desktop binds the
|
|
63
|
+
* request to its page ({pageId, origin}) or, for computer use, it is bound to
|
|
64
|
+
* {window_id, ref}; the card collects the value on the desktop, which holds it
|
|
65
|
+
* for `requestId` and fills only that page, only while its origin is
|
|
66
|
+
* unchanged. Here only the outcome travels (secretOutcome). Takeover: the run
|
|
67
|
+
* waits until the user says done (that session's actions stay blocked
|
|
68
|
+
* meanwhile, desktop main/takeover-gate.ts); the result is takeoverOutcome's.
|
|
28
69
|
*/
|
|
29
70
|
export async function runAskUserHandoff(handoff, { hook, sessionId, toolCallId, signal, inputs = {} }) {
|
|
30
71
|
const secretInput = {
|
|
31
72
|
browser: inputs.browser || browserSecretInput,
|
|
32
73
|
computer: inputs.computer || computerSecretInput,
|
|
33
74
|
};
|
|
75
|
+
const page = inputs.page || browserHandoffPage;
|
|
76
|
+
const bindComputer = inputs.computerBind || computerSecretBind;
|
|
77
|
+
const identity = handoff.identity || {};
|
|
34
78
|
const gate = {
|
|
35
79
|
agentId: agentIdForSession(sessionId),
|
|
36
80
|
kind: handoff.kind,
|
|
@@ -47,32 +91,84 @@ export async function runAskUserHandoff(handoff, { hook, sessionId, toolCallId,
|
|
|
47
91
|
return UNATTENDED_TAKEOVER;
|
|
48
92
|
}
|
|
49
93
|
if (typeof hook !== 'function') {
|
|
50
|
-
// Nobody can take over here: blocked, never "proceed".
|
|
94
|
+
// Nobody can take over or type here: blocked, never "proceed".
|
|
51
95
|
if (handoff.kind === 'takeover') return NO_TAKEOVER;
|
|
52
|
-
await recordHumanDecision(gate, { response: '
|
|
53
|
-
return '
|
|
96
|
+
await recordHumanDecision(gate, { response: 'blocked' });
|
|
97
|
+
return 'blocked';
|
|
54
98
|
}
|
|
55
99
|
if (handoff.kind === 'secret') {
|
|
56
100
|
const requestId = `secret_${randomUUID()}`;
|
|
101
|
+
// Bound now: the page (and its origin) the value may go to, and what its
|
|
102
|
+
// field is. A browser page that cannot be bound, or has no such field,
|
|
103
|
+
// never asks the user for a value it could not place.
|
|
104
|
+
let bound = {};
|
|
105
|
+
if (handoff.surface === 'browser') {
|
|
106
|
+
const facts = await page(
|
|
107
|
+
sessionId,
|
|
108
|
+
'secret_bind',
|
|
109
|
+
{ request_id: requestId, ...(identity.tab ? { tab: identity.tab } : {}), field: handoff.field },
|
|
110
|
+
{ signal }
|
|
111
|
+
);
|
|
112
|
+
let outcome = '';
|
|
113
|
+
if (!facts?.origin) outcome = 'blocked';
|
|
114
|
+
else if (!facts.form) outcome = 'no_field';
|
|
115
|
+
if (outcome) {
|
|
116
|
+
await recordHumanDecision(gate, { response: outcome });
|
|
117
|
+
return outcome;
|
|
118
|
+
}
|
|
119
|
+
bound = { origin: facts.origin, form: facts.form, ...(facts.length ? { length: facts.length } : {}) };
|
|
120
|
+
} else {
|
|
121
|
+
// Computer Use: bound to the exact window and field ref the agent named
|
|
122
|
+
// (from snapshot/find). Without both there is no field to confirm, and
|
|
123
|
+
// none is made up (never the focused field or the front window).
|
|
124
|
+
const outcome =
|
|
125
|
+
identity.windowId && identity.ref
|
|
126
|
+
? await bindComputer(sessionId, requestId, { windowId: identity.windowId, ref: identity.ref, signal })
|
|
127
|
+
: 'no_field';
|
|
128
|
+
if (outcome !== 'bound') {
|
|
129
|
+
const refused = outcome === 'no_field' ? 'no_field' : 'blocked';
|
|
130
|
+
await recordHumanDecision(gate, { response: refused });
|
|
131
|
+
return refused;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
57
134
|
const result = await runAbortable(signal, () =>
|
|
58
135
|
hook({
|
|
59
136
|
name: 'ask_user',
|
|
60
137
|
kind: 'secret',
|
|
61
|
-
secret: { requestId, surface: handoff.surface, target: handoff.target, field: handoff.field },
|
|
138
|
+
secret: { requestId, surface: handoff.surface, target: handoff.target, field: handoff.field, ...bound },
|
|
62
139
|
reason: '',
|
|
63
140
|
timeoutMs: SECRET_REQUEST_TIMEOUT_MS,
|
|
64
141
|
sessionId,
|
|
65
142
|
toolCallId,
|
|
66
143
|
})
|
|
67
144
|
);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
145
|
+
const computerTarget = {
|
|
146
|
+
...(identity.ref ? { ref: identity.ref } : {}),
|
|
147
|
+
...(identity.windowId ? { windowId: identity.windowId } : {}),
|
|
148
|
+
};
|
|
149
|
+
const outcome = await secretOutcome(result, () =>
|
|
150
|
+
secretInput[handoff.surface](sessionId, requestId, {
|
|
151
|
+
signal,
|
|
152
|
+
...(handoff.surface === 'computer' ? computerTarget : {}),
|
|
153
|
+
})
|
|
154
|
+
);
|
|
155
|
+
await recordHumanDecision(gate, { response: outcome, device: result?.device });
|
|
156
|
+
return outcome;
|
|
75
157
|
}
|
|
158
|
+
// A sign-in page's saved logins (masked) let the card fill one for the
|
|
159
|
+
// user: the desktop offers them only for a page on that origin, bound now
|
|
160
|
+
// under `fillId` so the fill reaches that page alone (host-agent.ts fillBoundLogin).
|
|
161
|
+
const fillId = handoff.login ? `secret_${randomUUID()}` : '';
|
|
162
|
+
const accounts = handoff.login
|
|
163
|
+
? (
|
|
164
|
+
await page(
|
|
165
|
+
sessionId,
|
|
166
|
+
'login_accounts',
|
|
167
|
+
{ url: handoff.login.url, request_id: fillId, ...(identity.tab ? { tab: identity.tab } : {}) },
|
|
168
|
+
{ signal }
|
|
169
|
+
)
|
|
170
|
+
)?.accounts
|
|
171
|
+
: null;
|
|
76
172
|
const result = await runAbortable(signal, () =>
|
|
77
173
|
hook({
|
|
78
174
|
name: 'ask_user',
|
|
@@ -81,9 +177,10 @@ export async function runAskUserHandoff(handoff, { hook, sessionId, toolCallId,
|
|
|
81
177
|
surface: handoff.surface,
|
|
82
178
|
instructions: handoff.instructions,
|
|
83
179
|
target: handoff.target,
|
|
84
|
-
...
|
|
180
|
+
...identity,
|
|
85
181
|
// A login takeover: the desktop opens its sign-in window on this page.
|
|
86
182
|
...(handoff.login ? { purpose: 'login', url: handoff.login.url } : {}),
|
|
183
|
+
...(Array.isArray(accounts) && accounts.length ? { accounts, fillId } : {}),
|
|
87
184
|
},
|
|
88
185
|
args: { surface: handoff.surface, instructions: handoff.instructions },
|
|
89
186
|
reason: '',
|
|
@@ -92,8 +189,15 @@ export async function runAskUserHandoff(handoff, { hook, sessionId, toolCallId,
|
|
|
92
189
|
toolCallId,
|
|
93
190
|
})
|
|
94
191
|
);
|
|
95
|
-
const
|
|
96
|
-
|
|
192
|
+
const pageUrl =
|
|
193
|
+
result?.approved === true && handoff.surface === 'browser'
|
|
194
|
+
? (await page(sessionId, 'page_url', identity.tab ? { tab: identity.tab } : {}, { signal }))?.url
|
|
195
|
+
: '';
|
|
196
|
+
const answer = takeoverOutcome(result, pageUrl);
|
|
197
|
+
await recordHumanDecision(gate, {
|
|
198
|
+
response: answer.startsWith('User ') ? 'done' : answer,
|
|
199
|
+
device: result?.device,
|
|
200
|
+
});
|
|
97
201
|
return answer;
|
|
98
202
|
}
|
|
99
203
|
|
|
@@ -125,7 +229,7 @@ const DISMISSED =
|
|
|
125
229
|
'User closed the questions without answering. Proceed with your best judgment; ask again only if something new depends on it.';
|
|
126
230
|
|
|
127
231
|
/** Wire answers -> array aligned with questions; invalid entries become null. */
|
|
128
|
-
|
|
232
|
+
function normalizeAskUserAnswers(answers, questions) {
|
|
129
233
|
const list = Array.isArray(answers) ? answers.slice(0, 4) : [];
|
|
130
234
|
return questions.map((q, i) => {
|
|
131
235
|
const entry = list[i];
|
|
@@ -143,7 +247,7 @@ export function normalizeAskUserAnswers(answers, questions) {
|
|
|
143
247
|
|
|
144
248
|
// Strings are JSON-quoted so a quote inside one cannot end it early; typed
|
|
145
249
|
// text stays apart from the picked labels (`"picked" + "typed" (typed)`).
|
|
146
|
-
|
|
250
|
+
function formatAskUserAnswers(questions, answers) {
|
|
147
251
|
const quote = (text) => JSON.stringify(text);
|
|
148
252
|
const parts = questions.map((q, i) => {
|
|
149
253
|
const a = answers[i];
|
|
@@ -157,14 +261,14 @@ export function formatAskUserAnswers(questions, answers) {
|
|
|
157
261
|
|
|
158
262
|
/** A composer message sent instead of (or with) picking: kept when it is a
|
|
159
263
|
* non-empty string within the typed-answer limit, else dropped. */
|
|
160
|
-
|
|
264
|
+
function normalizeAskUserReply(reply) {
|
|
161
265
|
if (typeof reply !== 'string' || reply.length > ASK_USER_MAX_TEXT) return '';
|
|
162
266
|
return reply.trim();
|
|
163
267
|
}
|
|
164
268
|
|
|
165
269
|
// `User replied in chat: "<reply>". ` leads, then the usual picks sentence
|
|
166
270
|
// when any question was answered, else just the closing sentence.
|
|
167
|
-
|
|
271
|
+
function formatAskUserResult(questions, answers, reply) {
|
|
168
272
|
if (!reply) return formatAskUserAnswers(questions, answers);
|
|
169
273
|
const lead = `User replied in chat: ${JSON.stringify(reply)}. `;
|
|
170
274
|
if (answers.some(Boolean)) return `${lead}${formatAskUserAnswers(questions, answers)}`;
|
|
@@ -45,6 +45,8 @@ export function createBridgeToolHandlers({ rt }) {
|
|
|
45
45
|
cwd: callerCwd,
|
|
46
46
|
requestApproval: callerCtx?.toolApprovalHook,
|
|
47
47
|
toolCallId: callerCtx?.toolCallId || null,
|
|
48
|
+
// The agent turn, as for the browser: it scopes a saved-login fill's protection.
|
|
49
|
+
turnId: callerCtx?.turnId || rt.session?.usageMetricsTurnId,
|
|
48
50
|
signal: signalFor(callerCtx),
|
|
49
51
|
});
|
|
50
52
|
};
|