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
|
@@ -6,10 +6,17 @@ import {
|
|
|
6
6
|
} from './settings-contract.mjs';
|
|
7
7
|
import { MEDIA_KINDS } from '../../runtime/media/tool-defs.mjs';
|
|
8
8
|
import { INSTRUCTION_FILES_MODES } from '../../runtime/shared/instruction-files-mode.mjs';
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_HEARTBEAT_INTERVAL_MINUTES,
|
|
11
|
+
HEARTBEAT_INTERVAL_CHOICES,
|
|
12
|
+
} from '../../runtime/shared/heartbeat-interval.mjs';
|
|
9
13
|
import { AUTO_EFFORT_BASES } from '../../runtime/agent/orchestrator/providers/auto-effort.mjs';
|
|
10
14
|
import { WORKTREE_NEW_TASK_DEFAULTS } from '../../runtime/shared/worktrees/settings.mjs';
|
|
11
15
|
import { CONNECTOR_IDS } from '../../runtime/shared/connector-catalog.mjs';
|
|
12
16
|
import { COMPACTION_SUMMARY_MODES } from '../../runtime/agent/orchestrator/session/compact/summary-mode.mjs';
|
|
17
|
+
import { ACTION_KINDS } from '../../runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs';
|
|
18
|
+
import { PERMISSION_MODES } from '../../runtime/agent/orchestrator/session/human-gate/store.mjs';
|
|
19
|
+
import { CONNECTOR_PERMISSIONS } from '../../runtime/agent/orchestrator/mcp/agent-connectors.mjs';
|
|
13
20
|
|
|
14
21
|
/** Built-in `setup` tool: the model's handle on a Mixdog user's persisted
|
|
15
22
|
* configuration. The description is a contract only; how to use each action
|
|
@@ -104,6 +111,7 @@ export const SETUP_ACTION_FIELDS = Object.freeze({
|
|
|
104
111
|
set_quota_auto_resume: 'enabled',
|
|
105
112
|
set_model_preferences: 'provider model hidden? favorite?',
|
|
106
113
|
set_heartbeat_enabled: 'enabled',
|
|
114
|
+
set_heartbeat_interval: 'minutes',
|
|
107
115
|
set_reactions_enabled: 'enabled',
|
|
108
116
|
forget_provider_auth: 'name accountId?',
|
|
109
117
|
set_provider_account: 'name providerAccount',
|
|
@@ -121,6 +129,19 @@ export const SETUP_ACTION_FIELDS = Object.freeze({
|
|
|
121
129
|
enable_plugin_mcp: 'name',
|
|
122
130
|
connect_connector: 'connector',
|
|
123
131
|
set_worktree_settings: 'worktree',
|
|
132
|
+
reset_route_fallback: 'sessionId? agent?',
|
|
133
|
+
continue_in_model: 'sessionId route',
|
|
134
|
+
schedule_quota_resume: 'sessionId',
|
|
135
|
+
cancel_quota_resume: 'sessionId',
|
|
136
|
+
set_cache_warming_enabled: 'enabled',
|
|
137
|
+
read_agent_settings: 'agent',
|
|
138
|
+
read_project_connectors: 'projectPath',
|
|
139
|
+
set_agent_permission: 'agent actionKind permissionMode',
|
|
140
|
+
set_agent_connector: 'agent name enabled',
|
|
141
|
+
set_agent_connector_tool: 'agent name tool permission',
|
|
142
|
+
set_project_connector: 'projectPath name enabled',
|
|
143
|
+
restore_definition: 'definitionKind name',
|
|
144
|
+
set_bot_order: 'botOrder',
|
|
124
145
|
...SETUP_EXTENDED_ACTION_FIELDS,
|
|
125
146
|
});
|
|
126
147
|
|
|
@@ -170,7 +191,7 @@ const ROUTE_SCHEMA = {
|
|
|
170
191
|
ROUTE_SCHEMA.properties.fallbacks = {
|
|
171
192
|
type: 'array',
|
|
172
193
|
description:
|
|
173
|
-
'Lead (set_route), agent (bots included) and Web Search routes only: ordered fallback models tried when the model hits its usage limit or is unavailable; [] clears the list.',
|
|
194
|
+
'Lead (set_route), agent (bots included) and Web Search routes only: ordered fallback models tried when the model hits its usage limit or is unavailable, each on another provider; [] clears the list. Running sessions on that route read it at their next hop.',
|
|
174
195
|
items: {
|
|
175
196
|
type: 'object',
|
|
176
197
|
additionalProperties: false,
|
|
@@ -219,7 +240,11 @@ export const SETUP_TOOL_DEFS = Object.freeze([
|
|
|
219
240
|
description:
|
|
220
241
|
'Partial route update; omitted fields are preserved. Agent or Web Search provider "" restores Main inheritance. contextPercent and contextTokens apply to Main and agent routes only.',
|
|
221
242
|
},
|
|
222
|
-
agent: {
|
|
243
|
+
agent: {
|
|
244
|
+
type: 'string',
|
|
245
|
+
description:
|
|
246
|
+
'Agent or bot id (lead = Mixy): set_agent_route, read_agent_settings, set_agent_permission, set_agent_connector(_tool); reset_route_fallback: every chat of that bot.',
|
|
247
|
+
},
|
|
223
248
|
workflow: { type: 'string', description: 'set_workflow: pack id.' },
|
|
224
249
|
style: { type: 'string', description: 'set_output_style: style id.' },
|
|
225
250
|
profile: {
|
|
@@ -272,7 +297,7 @@ export const SETUP_TOOL_DEFS = Object.freeze([
|
|
|
272
297
|
enabled: {
|
|
273
298
|
type: 'boolean',
|
|
274
299
|
description:
|
|
275
|
-
'Boolean setters (set_*_enabled, set_compaction, set_auto_update, set_ai_session_titles, set_quota_auto_resume, set_heartbeat_enabled, set_mcp_enabled, set_plugin_enabled).',
|
|
300
|
+
'Boolean setters (set_*_enabled, set_compaction, set_auto_update, set_ai_session_titles, set_quota_auto_resume, set_heartbeat_enabled, set_mcp_enabled, set_plugin_enabled, set_agent_connector, set_project_connector).',
|
|
276
301
|
},
|
|
277
302
|
provider: { type: 'string', description: 'set_model_preferences: provider id.' },
|
|
278
303
|
hidden: {
|
|
@@ -297,12 +322,12 @@ export const SETUP_TOOL_DEFS = Object.freeze([
|
|
|
297
322
|
},
|
|
298
323
|
name: {
|
|
299
324
|
type: 'string',
|
|
300
|
-
description: `set_builtin_enabled: ${SETUP_BUILTIN_TOGGLE_FEATURES.join('|')}; install_builtin also accepts memory. Memory toggles use set_memory_enabled. revoke_linked_device: client id from status connection, not its display name; set_developer_option: option id from status developer; read_definition/delete_definition: the definition id; reconnect_mcp: optional MCP server name (omit for all); else MCP/plugin/provider id.`,
|
|
325
|
+
description: `set_builtin_enabled: ${SETUP_BUILTIN_TOGGLE_FEATURES.join('|')}; install_builtin also accepts memory. Memory toggles use set_memory_enabled. revoke_linked_device: client id from status connection, not its display name; set_developer_option: option id from status developer; read_definition/delete_definition: the definition id; reconnect_mcp: optional MCP server name (omit for all); set_agent_connector(_tool)/set_project_connector: connector (MCP server) id from read_agent_settings; restore_definition: archived agent id; else MCP/plugin/provider id.`,
|
|
301
326
|
},
|
|
302
327
|
accountId: {
|
|
303
328
|
type: 'string',
|
|
304
329
|
description:
|
|
305
|
-
|
|
330
|
+
"forget_provider_auth: OAuth account id from status providers with detail:true; required when the provider has several accounts. set_media_default: optional account of the OAuth lane to pin; omitted = the lane provider's selected account.",
|
|
306
331
|
},
|
|
307
332
|
providerAccount: {
|
|
308
333
|
type: 'object',
|
|
@@ -350,6 +375,11 @@ export const SETUP_TOOL_DEFS = Object.freeze([
|
|
|
350
375
|
maximum: 86400,
|
|
351
376
|
description: 'set_local_idle_ttl: unload after this many idle seconds, 0 disables; default 3600.',
|
|
352
377
|
},
|
|
378
|
+
minutes: {
|
|
379
|
+
type: 'integer',
|
|
380
|
+
enum: [...HEARTBEAT_INTERVAL_CHOICES],
|
|
381
|
+
description: `set_heartbeat_interval: minutes between Mixy's heartbeat checks; default ${DEFAULT_HEARTBEAT_INTERVAL_MINUTES}.`,
|
|
382
|
+
},
|
|
353
383
|
query: { type: 'string', description: 'search_local_models: Hugging Face GGUF search query.' },
|
|
354
384
|
repository: { type: 'string', description: 'inspect_hf_model: public Hugging Face owner/model repository.' },
|
|
355
385
|
filename: {
|
|
@@ -427,6 +457,35 @@ export const SETUP_TOOL_DEFS = Object.freeze([
|
|
|
427
457
|
},
|
|
428
458
|
description: 'set_worktree_settings: only the given fields change.',
|
|
429
459
|
},
|
|
460
|
+
sessionId: {
|
|
461
|
+
type: 'string',
|
|
462
|
+
description:
|
|
463
|
+
'reset_route_fallback (default: this session), continue_in_model, schedule_quota_resume, cancel_quota_resume: session id from status model/agents or the session tool. The last three take another session.',
|
|
464
|
+
},
|
|
465
|
+
actionKind: {
|
|
466
|
+
type: 'string',
|
|
467
|
+
enum: [...ACTION_KINDS],
|
|
468
|
+
description: "set_agent_permission: the action kind row of the agent's permission table.",
|
|
469
|
+
},
|
|
470
|
+
permissionMode: {
|
|
471
|
+
type: 'string',
|
|
472
|
+
enum: [...PERMISSION_MODES],
|
|
473
|
+
description:
|
|
474
|
+
'set_agent_permission: block or ask; allow (and any change that lets an agent do more without asking) is made by the person in the desktop app.',
|
|
475
|
+
},
|
|
476
|
+
tool: { type: 'string', description: 'set_agent_connector_tool: tool name within the connector `name`.' },
|
|
477
|
+
permission: {
|
|
478
|
+
type: 'string',
|
|
479
|
+
enum: [...CONNECTOR_PERMISSIONS],
|
|
480
|
+
description: 'set_agent_connector_tool: needs_approval or disabled; auto is made in the desktop app.',
|
|
481
|
+
},
|
|
482
|
+
botOrder: {
|
|
483
|
+
type: 'array',
|
|
484
|
+
items: { type: 'string', minLength: 1, pattern: '\\S' },
|
|
485
|
+
maxItems: 500,
|
|
486
|
+
description:
|
|
487
|
+
'set_bot_order: bot ids in display order (Mixy is always first and not listed); [] = default order.',
|
|
488
|
+
},
|
|
430
489
|
...SETUP_EXTENDED_PROPERTIES,
|
|
431
490
|
},
|
|
432
491
|
},
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
} from '../../runtime/agent/orchestrator/mcp/agent-connectors.mjs';
|
|
20
20
|
import { agentListEntries } from '../../runtime/agent/orchestrator/session/manager/prompt-utils.mjs';
|
|
21
21
|
import { markToolSurfaceMigrated } from '../../runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs';
|
|
22
|
+
import { resetBp2StableContext } from '../../runtime/agent/orchestrator/context/collect.mjs';
|
|
23
|
+
import { requestPrefixIdentity } from '../../runtime/agent/orchestrator/session/thinking-binding.mjs';
|
|
22
24
|
import {
|
|
23
25
|
agentProfileRevision,
|
|
24
26
|
leadRosterRevision,
|
|
@@ -40,15 +42,6 @@ const PROMPT_FOLD_INTENT = 'prompt_fold';
|
|
|
40
42
|
// Cache-break intents that already rebuild the conversation's cached prefix.
|
|
41
43
|
const COLD_INTENTS = new Set(['post_turn_compaction', 'manual_compaction']);
|
|
42
44
|
|
|
43
|
-
// What the next request sends ahead of the conversation: the system blocks and tools.
|
|
44
|
-
const promptPrefix = (session) =>
|
|
45
|
-
JSON.stringify([
|
|
46
|
-
(Array.isArray(session.messages) ? session.messages : [])
|
|
47
|
-
.filter((message) => message?.role === 'system')
|
|
48
|
-
.map((message) => message.content),
|
|
49
|
-
session.tools,
|
|
50
|
-
]);
|
|
51
|
-
|
|
52
45
|
// A bot thread built from other shipped rules than these (a release changed one).
|
|
53
46
|
const rulesStale = (session) =>
|
|
54
47
|
sessionModeOf(session) === AGENT_MODE && session.rulesRevision !== promptRulesRevision();
|
|
@@ -208,7 +201,10 @@ export function createAgentProfileFold({
|
|
|
208
201
|
const config = configForSession(getConfig(), session.workflow?.id, currentMode(session));
|
|
209
202
|
const staged = await stagePolicy(session, identity, config, { conversation: true });
|
|
210
203
|
if (getSession?.() !== session) return false;
|
|
211
|
-
const before =
|
|
204
|
+
const before = requestPrefixIdentity(session);
|
|
205
|
+
// BP2 (profile meta, skill manifest) from current sources too; applyPolicy
|
|
206
|
+
// re-appends its deferred manifest.
|
|
207
|
+
if (typeof staged.stableContext === 'string') resetBp2StableContext(session, staged.stableContext);
|
|
212
208
|
applyPolicy(session, staged);
|
|
213
209
|
rewriteConversationBp3Core(session, staged.core);
|
|
214
210
|
if (session.agentProfileRevision !== undefined) {
|
|
@@ -218,17 +214,25 @@ export function createAgentProfileFold({
|
|
|
218
214
|
}
|
|
219
215
|
if (agentMode) session.rulesRevision = promptRulesRevision();
|
|
220
216
|
session.updatedAt = Date.now();
|
|
221
|
-
if (
|
|
217
|
+
if (requestPrefixIdentity(session) === before) return false;
|
|
222
218
|
markToolSurfaceMigrated(session, session.pendingCacheBreakIntent || intent);
|
|
223
219
|
return true;
|
|
224
220
|
}
|
|
225
221
|
|
|
226
222
|
/** Fold pending prompt changes (a switched workflow, a bot's moved tool
|
|
227
|
-
* gates, changed shipped rules) when the cache is cold anyway.
|
|
223
|
+
* gates, changed shipped rules) when the cache is cold anyway. A
|
|
224
|
+
* compaction rebuilds the prompt from current sources whether or not
|
|
225
|
+
* anything is known pending (an update can change shipped rules, skills
|
|
226
|
+
* and manifests no stamp tracks); an unchanged prompt stays byte-identical
|
|
227
|
+
* and adds no break. */
|
|
228
228
|
async function foldPromptIfCold() {
|
|
229
229
|
const session = getSession?.();
|
|
230
|
+
// Scope exception: delegated worker sessions (`owner: 'agent'`) keep the
|
|
231
|
+
// prompt they were created with; they are task-scoped, and their skill
|
|
232
|
+
// bodies are still restored from current sources at Compact.
|
|
230
233
|
if (!session?.id || !session.promptIdentity || isAgentOwner(session)) return false;
|
|
231
|
-
|
|
234
|
+
const compacted = COLD_INTENTS.has(session.pendingCacheBreakIntent);
|
|
235
|
+
if (!compacted && !(foldPending(session) && cacheCold(session))) return false;
|
|
232
236
|
return foldPrompt(session, PROMPT_FOLD_INTENT);
|
|
233
237
|
}
|
|
234
238
|
|
|
@@ -11,9 +11,14 @@ import {
|
|
|
11
11
|
import { deferredSurfaceModeForLead } from '../../runtime/agent/orchestrator/runtime-core/effort.mjs';
|
|
12
12
|
import {
|
|
13
13
|
applyInitialDeferredToolManifestToBp2,
|
|
14
|
+
buildSkillManifest,
|
|
15
|
+
collectPromptSkillsCached,
|
|
14
16
|
composeSystemPrompt,
|
|
15
17
|
} from '../../runtime/agent/orchestrator/context/collect.mjs';
|
|
16
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
_buildBaseRules,
|
|
20
|
+
_buildLeadMetaContext,
|
|
21
|
+
} from '../../runtime/agent/orchestrator/session/manager/rules-cache.mjs';
|
|
17
22
|
import { sessionRolePromptFields } from '../../runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs';
|
|
18
23
|
import {
|
|
19
24
|
delegationDisabled,
|
|
@@ -134,7 +139,12 @@ export function createPolicyStage({
|
|
|
134
139
|
model: getRoute()?.model,
|
|
135
140
|
});
|
|
136
141
|
const coreMemoryContext = conversation ? '' : await leadCoreMemoryContext();
|
|
137
|
-
|
|
142
|
+
// BP2's stable part as session creation composes it
|
|
143
|
+
// (session-prompt-composition.mjs): the Lead meta context and the skill
|
|
144
|
+
// manifest of the skills in scope now.
|
|
145
|
+
const skipRules = identity.skipAgentRules || toolFreeRole;
|
|
146
|
+
const skills = schemaAllowedTools?.length === 0 ? [] : collectPromptSkillsCached(session.cwd);
|
|
147
|
+
const { sessionMarkerCore, stableSystemContext } = composeSystemPrompt({
|
|
138
148
|
...sessionRolePromptFields(identity, {
|
|
139
149
|
ownerIsAgent,
|
|
140
150
|
toolFreeRole,
|
|
@@ -149,6 +159,8 @@ export function createPolicyStage({
|
|
|
149
159
|
// setting; agent mode carries none.
|
|
150
160
|
instructionFilesContext:
|
|
151
161
|
conversation || toolFreeRole || agentMode ? null : loadInstructionFilesContext?.(session) || null,
|
|
162
|
+
metaContext: skipRules || ownerIsAgent ? undefined : _buildLeadMetaContext({ includeOutputStyle: !agentMode }),
|
|
163
|
+
skillManifest: buildSkillManifest(skills),
|
|
152
164
|
});
|
|
153
165
|
return {
|
|
154
166
|
workflow,
|
|
@@ -159,6 +171,8 @@ export function createPolicyStage({
|
|
|
159
171
|
ownerIsAgent,
|
|
160
172
|
baseRules,
|
|
161
173
|
core: conversation ? withPresentedWorkspaceSlices(session, sessionMarkerCore) : sessionMarkerCore,
|
|
174
|
+
// Without BP1 the BP2 block cannot be located: null leaves it as it is.
|
|
175
|
+
stableContext: skipRules ? null : stableSystemContext,
|
|
162
176
|
};
|
|
163
177
|
}
|
|
164
178
|
|
|
@@ -80,6 +80,7 @@ export function createAskCallbacks({ transcript, notifyFnForSession, scheduleSea
|
|
|
80
80
|
beforeToolExecution: () => turn.snapshot.promise || Promise.resolve(),
|
|
81
81
|
transcriptMeta: stampCheckpoint(options),
|
|
82
82
|
promptSource: options.promptSource,
|
|
83
|
+
...(options.promptOrigin ? { promptOrigin: options.promptOrigin } : {}),
|
|
83
84
|
// A routine turn's trusted selection text (its instructions only).
|
|
84
85
|
...(typeof options.skillSelection === 'string' ? { skillSelection: options.skillSelection } : {}),
|
|
85
86
|
retryFailedTurn: options.retryFailedTurn === true,
|
|
@@ -64,6 +64,10 @@ export function createStyleAndModeApi(deps) {
|
|
|
64
64
|
listOutputStyles: () => getOutputStyleStatusCached(),
|
|
65
65
|
setOutputStyle,
|
|
66
66
|
getOrchestrationMode: () => configuredOrchestrationMode(displayConfig()),
|
|
67
|
+
// The default new sessions inherit. The runtime facade's own
|
|
68
|
+
// getOrchestrationMode answers for the ACTIVE session when one exists, so a
|
|
69
|
+
// New Task composer reads this one: it never depends on which session is open.
|
|
70
|
+
getConfiguredOrchestrationMode: () => configuredOrchestrationMode(displayConfig()),
|
|
67
71
|
setOrchestrationMode,
|
|
68
72
|
};
|
|
69
73
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// The live client records of the channel transport: pid-liveness reads and
|
|
2
|
+
// pruning, the one removal primitive every path uses, and the UI control
|
|
3
|
+
// pointer lookup. `replays` and `grace` are the registry's replay records and
|
|
4
|
+
// self-shutdown grace timer.
|
|
5
|
+
import { isPidAlive } from '../../../runtime/shared/pid-liveness.mjs';
|
|
6
|
+
|
|
7
|
+
export function createClientRecords({ state, log, publishRemoteState, replays, grace }) {
|
|
8
|
+
const { clients } = state;
|
|
9
|
+
|
|
10
|
+
// pid death is the authoritative signal for client liveness: a client whose
|
|
11
|
+
// SSE stream closed may still reconnect, one whose lead pid is gone cannot.
|
|
12
|
+
function pruneDeadClients() {
|
|
13
|
+
for (const [token, c] of clients) {
|
|
14
|
+
if (!isPidAlive(c.leadPid)) dropClient(token, 'pid dead');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function liveClients() {
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const [token, c] of clients) {
|
|
21
|
+
if (isPidAlive(c.leadPid)) out.push([token, c]);
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Every removal path uses this primitive, including replacement's deliberate
|
|
27
|
+
// no-failover retirement, so replay records/timers never target absent tokens.
|
|
28
|
+
function removeClientRecord(token) {
|
|
29
|
+
const c = clients.get(token);
|
|
30
|
+
if (!c) return null;
|
|
31
|
+
clients.delete(token);
|
|
32
|
+
replays.forgetFor(token);
|
|
33
|
+
try {
|
|
34
|
+
c.sse?.end?.();
|
|
35
|
+
} catch {
|
|
36
|
+
// The stream may already be closed by the peer.
|
|
37
|
+
}
|
|
38
|
+
return c;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function dropClient(token, reason) {
|
|
42
|
+
const c = removeClientRecord(token);
|
|
43
|
+
if (!c) return;
|
|
44
|
+
if (state.pointerToken === token) state.pointerToken = null;
|
|
45
|
+
log(`client ${token} (lead=${c.leadPid}) removed: ${reason}`);
|
|
46
|
+
// Client presence is not channel authority. The durable session pin stays
|
|
47
|
+
// active until that same session explicitly turns Remote OFF.
|
|
48
|
+
publishRemoteState();
|
|
49
|
+
grace.maybeArm('client removed');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Resolve the active UI pointer for state delivery only. Session routing
|
|
53
|
+
// never depends on this client.
|
|
54
|
+
function resolveTarget() {
|
|
55
|
+
if (state.pointerToken) {
|
|
56
|
+
const c = clients.get(state.pointerToken);
|
|
57
|
+
if (c && isPidAlive(c.leadPid)) return c;
|
|
58
|
+
if (c) dropClient(state.pointerToken, 'pid dead (notify-time)');
|
|
59
|
+
else state.pointerToken = null;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { pruneDeadClients, liveClients, removeClientRecord, dropClient, resolveTarget };
|
|
65
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Client registration for the channel transport: a fresh record per attach,
|
|
2
|
+
// a passive reconnect's response-loss replay, the replaced token's retirement
|
|
3
|
+
// with its token-scoped state carried over, and the replacement's
|
|
4
|
+
// cancellation. `records` is the registry's client records (client-records).
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { parsePid } from '../../../runtime/shared/pid-liveness.mjs';
|
|
7
|
+
import { replayMatchesCancellation, replayMatchesRegistration } from './registration-replays.mjs';
|
|
8
|
+
|
|
9
|
+
export function createClientRegistration({ state, log, onClientRegistered, replays, grace, records }) {
|
|
10
|
+
const { clients } = state;
|
|
11
|
+
|
|
12
|
+
function cancelReplacementRegistration(request) {
|
|
13
|
+
const replay = replays.lookup(request.registrationId);
|
|
14
|
+
if (!replay) return 'missing';
|
|
15
|
+
if (!replayMatchesCancellation(replay, request)) return 'forbidden';
|
|
16
|
+
records.dropClient(replay.token, 'replacement deregister');
|
|
17
|
+
return 'cancelled';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A passive reconnect retrying under a known registration id gets the fresh
|
|
21
|
+
* token it already created; a different identity under that id is a 409. */
|
|
22
|
+
function replayedToken(replayId, identity) {
|
|
23
|
+
const existing = replays.lookup(replayId);
|
|
24
|
+
if (!existing) return null;
|
|
25
|
+
if (replayMatchesRegistration(existing, identity) && clients.has(existing.token)) {
|
|
26
|
+
replays.arm(replayId, existing);
|
|
27
|
+
log(`client reconnect replay token=${existing.token} lead=${identity.pid}`);
|
|
28
|
+
return existing.token;
|
|
29
|
+
}
|
|
30
|
+
if (clients.has(existing.token)) {
|
|
31
|
+
const err = new Error('registration replay identity mismatch');
|
|
32
|
+
err.statusCode = 409;
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
replays.remove(replayId, existing);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A reconnect names the exact token it replaces. Token-scoped state belongs
|
|
40
|
+
* to the logical client, so the fresh record inherits the buffered frame,
|
|
41
|
+
* remote session and pointer; the old token is retired even when it is not
|
|
42
|
+
* the pointer — retaining it would let it later call or accumulate a
|
|
43
|
+
* buffered frame after the fresh client has gone away. */
|
|
44
|
+
function adoptReplacedClient(fresh, replaced) {
|
|
45
|
+
const replacedWasPointer = state.pointerToken === replaced.token;
|
|
46
|
+
if (replaced.pendingRemoteStateFrame) {
|
|
47
|
+
fresh.pendingRemoteStateFrame = replaced.pendingRemoteStateFrame;
|
|
48
|
+
replaced.pendingRemoteStateFrame = null;
|
|
49
|
+
}
|
|
50
|
+
if (replaced.remoteSessionId) fresh.remoteSessionId = replaced.remoteSessionId;
|
|
51
|
+
if (replaced.restoreSessionId) fresh.restoreSessionId = replaced.restoreSessionId;
|
|
52
|
+
if (replacedWasPointer) state.pointerToken = fresh.token;
|
|
53
|
+
records.removeClientRecord(replaced.token);
|
|
54
|
+
log(`client reconnect replaced token=${replaced.token} -> ${fresh.token} lead=${fresh.leadPid}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function registerClient({
|
|
58
|
+
leadPid,
|
|
59
|
+
cwd,
|
|
60
|
+
passive = false,
|
|
61
|
+
replaceToken = null,
|
|
62
|
+
registrationId = null,
|
|
63
|
+
restoreSessionId = null,
|
|
64
|
+
}) {
|
|
65
|
+
const pid = parsePid(leadPid) ?? 0;
|
|
66
|
+
const restoreId = /^[A-Za-z0-9_-]+$/.test(String(restoreSessionId || '')) ? String(restoreSessionId) : null;
|
|
67
|
+
const replacementToken = replaceToken ? String(replaceToken) : null;
|
|
68
|
+
const replayId = passive && registrationId ? replays.replayIdOf(registrationId) : null;
|
|
69
|
+
const replayed = replayedToken(replayId, { pid, cwd, replacementToken, restoreId });
|
|
70
|
+
if (replayed) return replayed;
|
|
71
|
+
const token = randomUUID();
|
|
72
|
+
const fresh = {
|
|
73
|
+
token,
|
|
74
|
+
leadPid: pid,
|
|
75
|
+
cwd: cwd || null,
|
|
76
|
+
sse: null,
|
|
77
|
+
pendingRemoteStateFrame: null,
|
|
78
|
+
lastSeen: Date.now(),
|
|
79
|
+
registeredAt: Date.now(),
|
|
80
|
+
restoreSessionId: restoreId,
|
|
81
|
+
};
|
|
82
|
+
clients.set(token, fresh);
|
|
83
|
+
if (state.remoteAcquired && restoreId && restoreId === state.pinnedSessionId) fresh.remoteSessionId = restoreId;
|
|
84
|
+
state.everHadClient = true;
|
|
85
|
+
grace.cancel();
|
|
86
|
+
grace.resetBackoff();
|
|
87
|
+
grace.startSweep();
|
|
88
|
+
log(`client registered token=${token} lead=${pid} cwd=${cwd || '-'}`);
|
|
89
|
+
// The unified daemon starts the channels runtime (automation, webhooks)
|
|
90
|
+
// only once a CHANNELS client is actually present — a
|
|
91
|
+
// session runtime-only daemon must not run tunnels nobody asked for.
|
|
92
|
+
if (typeof onClientRegistered === 'function') {
|
|
93
|
+
try {
|
|
94
|
+
onClientRegistered({ token, leadPid: pid, cwd: cwd || null });
|
|
95
|
+
} catch {
|
|
96
|
+
// The registered-client hook must not fail the registration itself.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Token replacement never crosses leadPid boundaries. Registration is
|
|
100
|
+
// otherwise transport-only, including same-pid fresh attaches.
|
|
101
|
+
const replaced = passive && replacementToken ? clients.get(replacementToken) : null;
|
|
102
|
+
if (replaced && replaced.leadPid === pid) adoptReplacedClient(fresh, replaced);
|
|
103
|
+
if (replayId) {
|
|
104
|
+
replays.remember(replayId, {
|
|
105
|
+
token,
|
|
106
|
+
leadPid: pid,
|
|
107
|
+
cwd: cwd || null,
|
|
108
|
+
replaceToken: replacementToken,
|
|
109
|
+
restoreSessionId: restoreId,
|
|
110
|
+
responseFinished: false,
|
|
111
|
+
timer: null,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return token;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { registerClient, cancelReplacementRegistration };
|
|
118
|
+
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* transport: the live client records, registration with reconnect
|
|
4
4
|
* replacement, pid-liveness pruning, and the UI control pointer lookup. The
|
|
5
5
|
* supporting state machines live under ./client-registry/:
|
|
6
|
+
* client-records — live records, removal, pruning, pointer lookup
|
|
7
|
+
* client-registration — registration, replay and reconnect replacement
|
|
6
8
|
* registration-replays — response-loss replay records and their TTL
|
|
7
9
|
* client-grace — self-shutdown grace timer + dead-pid sweep
|
|
8
10
|
* sse-attach — binding a client's notify stream
|
|
@@ -11,14 +13,10 @@
|
|
|
11
13
|
* pointerToken, everHadClient, closed, remoteAcquired, pinnedSessionId,
|
|
12
14
|
* stickyRemoteFrame.
|
|
13
15
|
*/
|
|
14
|
-
import { randomUUID } from 'node:crypto';
|
|
15
|
-
import { isPidAlive, parsePid } from '../../runtime/shared/pid-liveness.mjs';
|
|
16
16
|
import { createClientGrace } from './client-registry/client-grace.mjs';
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
replayMatchesRegistration,
|
|
21
|
-
} from './client-registry/registration-replays.mjs';
|
|
17
|
+
import { createClientRecords } from './client-registry/client-records.mjs';
|
|
18
|
+
import { createClientRegistration } from './client-registry/client-registration.mjs';
|
|
19
|
+
import { createRegistrationReplays } from './client-registry/registration-replays.mjs';
|
|
22
20
|
import { attachClientStream } from './client-registry/sse-attach.mjs';
|
|
23
21
|
|
|
24
22
|
export function createClientRegistry({
|
|
@@ -37,162 +35,25 @@ export function createClientRegistry({
|
|
|
37
35
|
ttlMs: registrationReplayTtlMs,
|
|
38
36
|
onExpiredUnflushed: (token) => dropClient(token, 'unflushed registration replay expired'),
|
|
39
37
|
});
|
|
40
|
-
const grace = createClientGrace({
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
// no-failover retirement, so replay records/timers never target absent tokens.
|
|
60
|
-
function removeClientRecord(token) {
|
|
61
|
-
const c = clients.get(token);
|
|
62
|
-
if (!c) return null;
|
|
63
|
-
clients.delete(token);
|
|
64
|
-
replays.forgetFor(token);
|
|
65
|
-
try {
|
|
66
|
-
c.sse?.end?.();
|
|
67
|
-
} catch {}
|
|
68
|
-
return c;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function dropClient(token, reason) {
|
|
72
|
-
const c = removeClientRecord(token);
|
|
73
|
-
if (!c) return;
|
|
74
|
-
if (state.pointerToken === token) state.pointerToken = null;
|
|
75
|
-
log(`client ${token} (lead=${c.leadPid}) removed: ${reason}`);
|
|
76
|
-
// Client presence is not channel authority. The durable session pin stays
|
|
77
|
-
// active until that same session explicitly turns Remote OFF.
|
|
78
|
-
publishRemoteState();
|
|
79
|
-
grace.maybeArm('client removed');
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function cancelReplacementRegistration(request) {
|
|
83
|
-
const replay = replays.lookup(request.registrationId);
|
|
84
|
-
if (!replay) return 'missing';
|
|
85
|
-
if (!replayMatchesCancellation(replay, request)) return 'forbidden';
|
|
86
|
-
dropClient(replay.token, 'replacement deregister');
|
|
87
|
-
return 'cancelled';
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Resolve the active UI pointer for state delivery only. Session routing
|
|
91
|
-
// never depends on this client.
|
|
92
|
-
function resolveTarget() {
|
|
93
|
-
if (state.pointerToken) {
|
|
94
|
-
const c = clients.get(state.pointerToken);
|
|
95
|
-
if (c && isPidAlive(c.leadPid)) return c;
|
|
96
|
-
if (c) dropClient(state.pointerToken, 'pid dead (notify-time)');
|
|
97
|
-
else state.pointerToken = null;
|
|
98
|
-
}
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** A passive reconnect retrying under a known registration id gets the fresh
|
|
103
|
-
* token it already created; a different identity under that id is a 409. */
|
|
104
|
-
function replayedToken(replayId, identity) {
|
|
105
|
-
const existing = replays.lookup(replayId);
|
|
106
|
-
if (!existing) return null;
|
|
107
|
-
if (replayMatchesRegistration(existing, identity) && clients.has(existing.token)) {
|
|
108
|
-
replays.arm(replayId, existing);
|
|
109
|
-
log(`client reconnect replay token=${existing.token} lead=${identity.pid}`);
|
|
110
|
-
return existing.token;
|
|
111
|
-
}
|
|
112
|
-
if (clients.has(existing.token)) {
|
|
113
|
-
const err = new Error('registration replay identity mismatch');
|
|
114
|
-
err.statusCode = 409;
|
|
115
|
-
throw err;
|
|
116
|
-
}
|
|
117
|
-
replays.remove(replayId, existing);
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** A reconnect names the exact token it replaces. Token-scoped state belongs
|
|
122
|
-
* to the logical client, so the fresh record inherits the buffered frame,
|
|
123
|
-
* remote session and pointer; the old token is retired even when it is not
|
|
124
|
-
* the pointer — retaining it would let it later call or accumulate a
|
|
125
|
-
* buffered frame after the fresh client has gone away. */
|
|
126
|
-
function adoptReplacedClient(fresh, replaced) {
|
|
127
|
-
const replacedWasPointer = state.pointerToken === replaced.token;
|
|
128
|
-
if (replaced.pendingRemoteStateFrame) {
|
|
129
|
-
fresh.pendingRemoteStateFrame = replaced.pendingRemoteStateFrame;
|
|
130
|
-
replaced.pendingRemoteStateFrame = null;
|
|
131
|
-
}
|
|
132
|
-
if (replaced.remoteSessionId) fresh.remoteSessionId = replaced.remoteSessionId;
|
|
133
|
-
if (replaced.restoreSessionId) fresh.restoreSessionId = replaced.restoreSessionId;
|
|
134
|
-
if (replacedWasPointer) state.pointerToken = fresh.token;
|
|
135
|
-
removeClientRecord(replaced.token);
|
|
136
|
-
log(`client reconnect replaced token=${replaced.token} -> ${fresh.token} lead=${fresh.leadPid}`);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function registerClient({
|
|
140
|
-
leadPid,
|
|
141
|
-
cwd,
|
|
142
|
-
passive = false,
|
|
143
|
-
replaceToken = null,
|
|
144
|
-
registrationId = null,
|
|
145
|
-
restoreSessionId = null,
|
|
146
|
-
}) {
|
|
147
|
-
const pid = parsePid(leadPid) ?? 0;
|
|
148
|
-
const restoreId = /^[A-Za-z0-9_-]+$/.test(String(restoreSessionId || '')) ? String(restoreSessionId) : null;
|
|
149
|
-
const replacementToken = replaceToken ? String(replaceToken) : null;
|
|
150
|
-
const replayId = passive && registrationId ? replays.replayIdOf(registrationId) : null;
|
|
151
|
-
const replayed = replayedToken(replayId, { pid, cwd, replacementToken, restoreId });
|
|
152
|
-
if (replayed) return replayed;
|
|
153
|
-
const token = randomUUID();
|
|
154
|
-
const fresh = {
|
|
155
|
-
token,
|
|
156
|
-
leadPid: pid,
|
|
157
|
-
cwd: cwd || null,
|
|
158
|
-
sse: null,
|
|
159
|
-
pendingRemoteStateFrame: null,
|
|
160
|
-
lastSeen: Date.now(),
|
|
161
|
-
registeredAt: Date.now(),
|
|
162
|
-
restoreSessionId: restoreId,
|
|
163
|
-
};
|
|
164
|
-
clients.set(token, fresh);
|
|
165
|
-
if (state.remoteAcquired && restoreId && restoreId === state.pinnedSessionId) fresh.remoteSessionId = restoreId;
|
|
166
|
-
state.everHadClient = true;
|
|
167
|
-
grace.cancel();
|
|
168
|
-
grace.resetBackoff();
|
|
169
|
-
grace.startSweep();
|
|
170
|
-
log(`client registered token=${token} lead=${pid} cwd=${cwd || '-'}`);
|
|
171
|
-
// The unified daemon starts the channels runtime (automation, webhooks,
|
|
172
|
-
// messaging provider) only once a CHANNELS client is actually present — a
|
|
173
|
-
// session runtime-only daemon must not run tunnels nobody asked for.
|
|
174
|
-
if (typeof onClientRegistered === 'function') {
|
|
175
|
-
try {
|
|
176
|
-
onClientRegistered({ token, leadPid: pid, cwd: cwd || null });
|
|
177
|
-
} catch {}
|
|
178
|
-
}
|
|
179
|
-
// Token replacement never crosses leadPid boundaries. Registration is
|
|
180
|
-
// otherwise transport-only, including same-pid fresh attaches.
|
|
181
|
-
const replaced = passive && replacementToken ? clients.get(replacementToken) : null;
|
|
182
|
-
if (replaced && replaced.leadPid === pid) adoptReplacedClient(fresh, replaced);
|
|
183
|
-
if (replayId) {
|
|
184
|
-
replays.remember(replayId, {
|
|
185
|
-
token,
|
|
186
|
-
leadPid: pid,
|
|
187
|
-
cwd: cwd || null,
|
|
188
|
-
replaceToken: replacementToken,
|
|
189
|
-
restoreSessionId: restoreId,
|
|
190
|
-
responseFinished: false,
|
|
191
|
-
timer: null,
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
return token;
|
|
195
|
-
}
|
|
38
|
+
const grace = createClientGrace({
|
|
39
|
+
state,
|
|
40
|
+
clients,
|
|
41
|
+
log,
|
|
42
|
+
clientGraceMs,
|
|
43
|
+
sweepMs,
|
|
44
|
+
onClientsEmpty,
|
|
45
|
+
pruneDeadClients: () => records.pruneDeadClients(),
|
|
46
|
+
});
|
|
47
|
+
const records = createClientRecords({ state, log, publishRemoteState, replays, grace });
|
|
48
|
+
const { liveClients, dropClient, resolveTarget } = records;
|
|
49
|
+
const { registerClient, cancelReplacementRegistration } = createClientRegistration({
|
|
50
|
+
state,
|
|
51
|
+
log,
|
|
52
|
+
onClientRegistered,
|
|
53
|
+
replays,
|
|
54
|
+
grace,
|
|
55
|
+
records,
|
|
56
|
+
});
|
|
196
57
|
|
|
197
58
|
function attachSse(token, res) {
|
|
198
59
|
const c = clients.get(token);
|