@gajae-code/coding-agent 0.13.3 → 0.14.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/CHANGELOG.md +184 -2
- package/README.md +2 -0
- package/bin/gajaessi.js +4 -0
- package/dist/types/async/job-manager.d.ts +55 -2
- package/dist/types/async/support.d.ts +14 -0
- package/dist/types/capability/mcp.d.ts +6 -0
- package/dist/types/capability/types.d.ts +6 -0
- package/dist/types/cli/accounts-cli.d.ts +19 -0
- package/dist/types/cli/args.d.ts +3 -0
- package/dist/types/cli/config-cli.d.ts +6 -0
- package/dist/types/cli/crash-cli.d.ts +10 -0
- package/dist/types/cli/customize-doctor.d.ts +145 -0
- package/dist/types/cli/daemon-cli.d.ts +3 -1
- package/dist/types/cli/mcp-cli.d.ts +11 -0
- package/dist/types/cli/notify-cli.d.ts +1 -1
- package/dist/types/cli/root-flags.d.ts +12 -0
- package/dist/types/cli/setup-cli.d.ts +3 -1
- package/dist/types/cli/skills-cli.d.ts +2 -1
- package/dist/types/cli/update-cli.d.ts +19 -1
- package/dist/types/cli.d.ts +6 -0
- package/dist/types/commands/accounts.d.ts +40 -0
- package/dist/types/commands/crash.d.ts +24 -0
- package/dist/types/commands/customize.d.ts +22 -0
- package/dist/types/commands/daemon.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/commands/setup.d.ts +6 -0
- package/dist/types/commands/skills.d.ts +6 -1
- package/dist/types/commands/update.d.ts +7 -0
- package/dist/types/config/atomic-yaml-patch-worker.d.ts +25 -0
- package/dist/types/config/atomic-yaml-patch.d.ts +62 -2
- package/dist/types/config/config-schema-version.d.ts +10 -0
- package/dist/types/config/file-lock.d.ts +18 -0
- package/dist/types/config/model-profile-activation.d.ts +3 -1
- package/dist/types/config/model-registry.d.ts +9 -1
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +70 -6
- package/dist/types/config/settings.d.ts +63 -0
- package/dist/types/config/skill-settings-defaults.d.ts +26 -0
- package/dist/types/config/task-delegation.d.ts +39 -0
- package/dist/types/config/update-channel.d.ts +10 -0
- package/dist/types/coordinator-mcp/server.d.ts +5 -1
- package/dist/types/crash/index-store.d.ts +87 -0
- package/dist/types/crash/nudge.d.ts +50 -0
- package/dist/types/crash/record-loader.d.ts +17 -0
- package/dist/types/crash/report.d.ts +134 -0
- package/dist/types/crash/sanitize.d.ts +25 -0
- package/dist/types/cursor.d.ts +13 -2
- package/dist/types/customization/import.d.ts +37 -0
- package/dist/types/customization/index.d.ts +4 -0
- package/dist/types/customization/inventory.d.ts +25 -0
- package/dist/types/customization/mutations.d.ts +32 -0
- package/dist/types/customization/types.d.ts +115 -0
- package/dist/types/daemon/control-types.d.ts +8 -1
- package/dist/types/daemon/operator-contract.d.ts +4 -4
- package/dist/types/discovery/claude.d.ts +39 -1
- package/dist/types/discovery/codex.d.ts +39 -1
- package/dist/types/discovery/index.d.ts +2 -0
- package/dist/types/discovery/mcp-compat.d.ts +30 -0
- package/dist/types/edit/renderer.d.ts +23 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +21 -4
- package/dist/types/extensibility/gjc-plugins/observability.d.ts +6 -1
- package/dist/types/extensibility/gjc-plugins/registry.d.ts +8 -1
- package/dist/types/extensibility/hooks/loader.d.ts +19 -5
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +22 -1
- package/dist/types/extensibility/skill-management.d.ts +107 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +3 -1
- package/dist/types/gjc-runtime/launch-worktree.d.ts +8 -0
- package/dist/types/gjc-runtime/linux-proc.d.ts +3 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +10 -6
- package/dist/types/gjc-runtime/session-state-lock.d.ts +104 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +120 -2
- package/dist/types/gjc-runtime/state-writer.d.ts +10 -1
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -2
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -6
- package/dist/types/gjc-runtime/workflow-settings.d.ts +109 -0
- package/dist/types/hooks/events.d.ts +60 -0
- package/dist/types/hooks/normalize.d.ts +83 -0
- package/dist/types/hooks/skill-state.d.ts +9 -2
- package/dist/types/main.d.ts +1 -1
- package/dist/types/modes/acp/abort-scope.d.ts +13 -0
- package/dist/types/modes/acp/acp-agent.d.ts +7 -4
- package/dist/types/modes/components/customization/customization-dashboard.d.ts +44 -0
- package/dist/types/modes/components/customization/import-wizard.d.ts +21 -0
- package/dist/types/modes/components/customization/index.d.ts +1 -0
- package/dist/types/modes/components/gajae-pet-widget.d.ts +8 -30
- package/dist/types/modes/components/irc-sidebar.d.ts +1 -0
- package/dist/types/modes/components/iterm-pet-transport.d.ts +122 -0
- package/dist/types/modes/components/oauth-selector.d.ts +10 -4
- package/dist/types/modes/components/pet-capability.d.ts +11 -25
- package/dist/types/modes/components/status-line/gh.d.ts +6 -2
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +7 -8
- package/dist/types/modes/controllers/stt-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/utils/injected-user-submission.d.ts +10 -0
- package/dist/types/runtime-mcp/config.d.ts +9 -0
- package/dist/types/runtime-mcp/manager.d.ts +26 -1
- package/dist/types/runtime-mcp/oauth-discovery.d.ts +4 -0
- package/dist/types/runtime-mcp/oauth-flow.d.ts +24 -1
- package/dist/types/runtime-mcp/pool-key.d.ts +2 -0
- package/dist/types/runtime-mcp/protocol.d.ts +278 -0
- package/dist/types/runtime-mcp/scope-config.d.ts +29 -0
- package/dist/types/runtime-mcp/tool-bridge.d.ts +15 -13
- package/dist/types/runtime-mcp/transports/http.d.ts +9 -0
- package/dist/types/runtime-mcp/types.d.ts +102 -10
- package/dist/types/sdk/acp/adapter.d.ts +26 -5
- package/dist/types/sdk/broker/broker.d.ts +3 -55
- package/dist/types/sdk/broker/lifecycle.d.ts +18 -12
- package/dist/types/sdk/broker/session-index.d.ts +14 -5
- package/dist/types/sdk/broker/startup-failure.d.ts +16 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +51 -1
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -41
- package/dist/types/sdk/bus/config.d.ts +11 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +0 -1
- package/dist/types/sdk/bus/daemon-paths.d.ts +6 -0
- package/dist/types/sdk/bus/discord-conversation.d.ts +2 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +17 -12
- package/dist/types/sdk/bus/existing-thread-readiness.d.ts +4 -34
- package/dist/types/sdk/bus/inbound-reaction-ordering.d.ts +61 -0
- package/dist/types/sdk/bus/index.d.ts +85 -99
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +29 -7
- package/dist/types/sdk/bus/managed-daemon.d.ts +2 -32
- package/dist/types/sdk/bus/notification-service.d.ts +52 -0
- package/dist/types/sdk/bus/operator-runtime.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +16 -5
- package/dist/types/sdk/bus/reconciliation-store.d.ts +113 -5
- package/dist/types/sdk/bus/session-control.d.ts +25 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +4 -0
- package/dist/types/sdk/bus/slack-daemon.d.ts +9 -17
- package/dist/types/sdk/bus/slack-live-provider.d.ts +3 -1
- package/dist/types/sdk/bus/slack-provider.d.ts +4 -0
- package/dist/types/sdk/bus/slack-thread-binding.d.ts +6 -40
- package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +30 -104
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +10 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +262 -19
- package/dist/types/sdk/bus/telegram-daemon-orphan-reap.d.ts +125 -0
- package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +22 -0
- package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -310
- package/dist/types/sdk/bus/telegram-reference.d.ts +1 -47
- package/dist/types/sdk/bus/threaded-render.d.ts +1 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +33 -17
- package/dist/types/sdk/cli/rows.d.ts +2 -11
- package/dist/types/sdk/cli/session-cli.d.ts +15 -25
- package/dist/types/sdk/client/client.d.ts +97 -1
- package/dist/types/sdk/host/control/dispatch.d.ts +0 -7
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +18 -5
- package/dist/types/sdk/host/control/runtime-gate.d.ts +19 -0
- package/dist/types/sdk/host/host.d.ts +15 -1
- package/dist/types/sdk/host/query/handlers.d.ts +5 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
- package/dist/types/sdk/host/session-runtime.d.ts +127 -7
- package/dist/types/sdk/host/types.d.ts +1 -1
- package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
- package/dist/types/sdk/index.d.ts +2 -1
- package/dist/types/sdk/lifecycle/broker-client.d.ts +8 -0
- package/dist/types/sdk/lifecycle/client.d.ts +59 -0
- package/dist/types/sdk/lifecycle/index.d.ts +3 -0
- package/dist/types/sdk/lifecycle/service.d.ts +232 -0
- package/dist/types/sdk/mcp/server.d.ts +9 -6
- package/dist/types/sdk/prompt-deadline-lease.d.ts +43 -0
- package/dist/types/sdk/prompt-deadline-manager.d.ts +28 -0
- package/dist/types/sdk/prompt-status.d.ts +10 -3
- package/dist/types/sdk/protocol/operation-registry.d.ts +5 -0
- package/dist/types/sdk/receipt-state.d.ts +17 -0
- package/dist/types/sdk/reconciliation-extensions.d.ts +2 -0
- package/dist/types/sdk/router/index.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +152 -0
- package/dist/types/sdk/session-list.d.ts +22 -0
- package/dist/types/sdk/session-reconnect.d.ts +43 -1
- package/dist/types/sdk/session.d.ts +34 -1
- package/dist/types/sdk/transport/serve-cli.d.ts +1 -4
- package/dist/types/session/account-inventory.d.ts +86 -0
- package/dist/types/session/agent-session.d.ts +145 -6
- package/dist/types/session/agent-storage.d.ts +10 -0
- package/dist/types/session/coordinator-tool-label.d.ts +28 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +23 -4
- package/dist/types/session/messages.d.ts +1 -1
- package/dist/types/session/secure-token-file.d.ts +3 -0
- package/dist/types/session/session-manager.d.ts +40 -1
- package/dist/types/session/startup-auth-config.d.ts +44 -0
- package/dist/types/session/terminal-abort.d.ts +378 -0
- package/dist/types/session/yield-queue.d.ts +7 -0
- package/dist/types/session-import/claude.d.ts +26 -0
- package/dist/types/session-import/codex.d.ts +88 -0
- package/dist/types/session-import/command.d.ts +35 -0
- package/dist/types/session-import/detect.d.ts +22 -0
- package/dist/types/session-import/index.d.ts +7 -0
- package/dist/types/session-import/provider-codex.d.ts +22 -0
- package/dist/types/session-import/provider-service.d.ts +38 -0
- package/dist/types/session-import/redact.d.ts +23 -0
- package/dist/types/session-import/service.d.ts +47 -0
- package/dist/types/session-import/types.d.ts +120 -0
- package/dist/types/setup/paseo/check.d.ts +16 -0
- package/dist/types/setup/paseo/install-saga.d.ts +115 -0
- package/dist/types/setup/paseo/json-publisher.d.ts +76 -0
- package/dist/types/setup/paseo/orchestration-preferences.d.ts +24 -0
- package/dist/types/setup/paseo/paseo-ownership.d.ts +87 -0
- package/dist/types/setup/paseo/paseo-setup.d.ts +29 -0
- package/dist/types/setup/paseo/provider-config.d.ts +35 -0
- package/dist/types/setup/paseo/remove.d.ts +14 -0
- package/dist/types/setup/paseo/result-types.d.ts +59 -0
- package/dist/types/setup/paseo/setup-deps.d.ts +73 -0
- package/dist/types/setup/paseo/skills-bridge.d.ts +44 -0
- package/dist/types/skill-state/active-state.d.ts +5 -1
- package/dist/types/slash-commands/acp-builtins.d.ts +5 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +2 -0
- package/dist/types/slash-commands/helpers/credential-switch.d.ts +12 -0
- package/dist/types/slash-commands/helpers/usage-report.d.ts +5 -6
- package/dist/types/slash-commands/types.d.ts +7 -1
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/tools/ask-answer-registry.d.ts +6 -3
- package/dist/types/tools/bash-sleep-advisory.d.ts +14 -0
- package/dist/types/tools/bash.d.ts +2 -6
- package/dist/types/tools/browser/attach.d.ts +11 -0
- package/dist/types/tools/browser/launch.d.ts +20 -0
- package/dist/types/tools/browser/profile-discovery.d.ts +6 -0
- package/dist/types/tools/browser/render.d.ts +1 -0
- package/dist/types/tools/browser.d.ts +3 -1
- package/dist/types/tools/image-gen.d.ts +9 -2
- package/dist/types/tools/index.d.ts +8 -0
- package/dist/types/tools/monitor.d.ts +1 -1
- package/dist/types/tools/skill-discovery.d.ts +8 -1
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +2 -2
- package/dist/types/utils/herdr-pane.d.ts +76 -0
- package/package.json +19 -9
- package/scripts/build-binary.ts +2 -1
- package/scripts/build-sdk-package-smoke.ts +2 -13
- package/scripts/capture-sticky-viewport-showcase.ts +0 -1
- package/scripts/compile-args.ts +2 -0
- package/scripts/generate-sdk-adapter-parity-manifest.ts +31 -16
- package/scripts/generate-sdk-operation-inventory.ts +28 -0
- package/scripts/generate-telegram-baseline-manifest.ts +1 -0
- package/scripts/generate-tool-catalog.ts +6 -4
- package/scripts/verify-gjc-sdk-canonicalization.ts +5 -39
- package/src/async/job-manager.ts +352 -17
- package/src/async/support.ts +25 -0
- package/src/autoresearch/dashboard.ts +28 -13
- package/src/capability/index.ts +3 -1
- package/src/capability/mcp.ts +6 -0
- package/src/capability/types.ts +6 -0
- package/src/cli/accounts-cli.ts +395 -0
- package/src/cli/args.ts +23 -0
- package/src/cli/auth-broker-cli.ts +136 -66
- package/src/cli/auth-gateway-cli.ts +94 -70
- package/src/cli/config-cli.ts +27 -2
- package/src/cli/crash-cli.ts +78 -0
- package/src/cli/customize-doctor.ts +1605 -0
- package/src/cli/daemon-cli.ts +12 -3
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/mcp-cli.ts +128 -60
- package/src/cli/notify-cli.ts +8 -14
- package/src/cli/root-flags.ts +8 -0
- package/src/cli/setup-cli.ts +145 -0
- package/src/cli/skills-cli.ts +38 -2
- package/src/cli/update-cli.ts +167 -12
- package/src/cli.ts +4 -0
- package/src/commands/accounts.ts +71 -0
- package/src/commands/crash.ts +35 -0
- package/src/commands/customize.ts +84 -0
- package/src/commands/daemon.ts +2 -2
- package/src/commands/deep-interview.ts +2 -0
- package/src/commands/launch.ts +1 -0
- package/src/commands/mcp.ts +12 -7
- package/src/commands/ralplan.ts +2 -0
- package/src/commands/sdk.ts +65 -32
- package/src/commands/setup.ts +5 -0
- package/src/commands/skills.ts +14 -4
- package/src/commands/ultragoal.ts +10 -1
- package/src/commands/update.ts +26 -15
- package/src/config/atomic-yaml-patch-worker.ts +57 -0
- package/src/config/atomic-yaml-patch.ts +769 -29
- package/src/config/config-schema-version.ts +10 -0
- package/src/config/file-lock.ts +70 -1
- package/src/config/mcp-schema.json +52 -14
- package/src/config/model-profile-activation.ts +20 -1
- package/src/config/model-profiles.ts +29 -19
- package/src/config/model-registry.ts +132 -21
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +76 -6
- package/src/config/settings.ts +4038 -199
- package/src/config/skill-settings-defaults.ts +41 -3
- package/src/config/task-delegation.ts +87 -0
- package/src/config/update-channel.ts +23 -0
- package/src/coordinator-mcp/server.ts +377 -308
- package/src/crash/index-store.ts +723 -0
- package/src/crash/nudge.ts +89 -0
- package/src/crash/record-loader.ts +149 -0
- package/src/crash/report.ts +551 -0
- package/src/crash/sanitize.ts +129 -0
- package/src/cursor.ts +36 -10
- package/src/customization/import.ts +946 -0
- package/src/customization/index.ts +4 -0
- package/src/customization/inventory.ts +335 -0
- package/src/customization/mutations.ts +141 -0
- package/src/customization/types.ts +223 -0
- package/src/daemon/control-types.ts +8 -1
- package/src/daemon/operator-contract.ts +6 -6
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -22
- package/src/defaults/gjc/skills/ralplan/SKILL.md +36 -23
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
- package/src/discovery/claude-hooks.ts +38 -0
- package/src/discovery/claude.ts +142 -209
- package/src/discovery/codex-hooks.ts +38 -0
- package/src/discovery/codex.ts +132 -198
- package/src/discovery/helpers.ts +8 -1
- package/src/discovery/index.ts +2 -0
- package/src/discovery/mcp-compat.ts +234 -0
- package/src/discovery/mcp-json.ts +6 -0
- package/src/edit/renderer.ts +40 -0
- package/src/edit/streaming.ts +4 -2
- package/src/exec/bash-executor.ts +24 -20
- package/src/export/html/index.ts +51 -2
- package/src/extensibility/extensions/loader.ts +9 -1
- package/src/extensibility/extensions/runner.ts +17 -4
- package/src/extensibility/extensions/types.ts +31 -6
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +33 -0
- package/src/extensibility/gjc-plugins/installer.ts +9 -5
- package/src/extensibility/gjc-plugins/observability.ts +8 -2
- package/src/extensibility/gjc-plugins/registry.ts +13 -2
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +3 -1
- package/src/extensibility/gjc-plugins/schema.ts +222 -39
- package/src/extensibility/hooks/loader.ts +154 -10
- package/src/extensibility/runtime-skill-discovery.ts +257 -60
- package/src/extensibility/skill-management.ts +368 -0
- package/src/extensibility/skills.ts +52 -16
- package/src/gjc-runtime/deep-interview-runtime.ts +45 -61
- package/src/gjc-runtime/launch-worktree.ts +56 -2
- package/src/gjc-runtime/linux-proc.ts +7 -1
- package/src/gjc-runtime/ralplan-runtime.ts +100 -164
- package/src/gjc-runtime/session-state-lock.ts +992 -0
- package/src/gjc-runtime/session-state-sidecar.ts +644 -142
- package/src/gjc-runtime/state-writer.ts +39 -14
- package/src/gjc-runtime/tmux-owner-isolation.ts +5 -4
- package/src/gjc-runtime/ultragoal-guard.ts +11 -4
- package/src/gjc-runtime/ultragoal-runtime.ts +44 -41
- package/src/gjc-runtime/workflow-settings.ts +596 -0
- package/src/goals/tools/goal-tool.ts +6 -1
- package/src/hooks/events.ts +291 -0
- package/src/hooks/native-skill-hook.ts +4 -0
- package/src/hooks/normalize.ts +369 -0
- package/src/hooks/skill-state.ts +165 -46
- package/src/internal-urls/docs-index.generated.ts +40 -32
- package/src/main.ts +32 -5
- package/src/migrate/mcp-mapper.ts +25 -2
- package/src/modes/acp/abort-scope.ts +29 -0
- package/src/modes/acp/acp-agent.ts +423 -98
- package/src/modes/components/assistant-message.ts +6 -1
- package/src/modes/components/customization/customization-dashboard.ts +443 -0
- package/src/modes/components/customization/import-wizard.ts +372 -0
- package/src/modes/components/customization/index.ts +1 -0
- package/src/modes/components/gajae-pet-widget.ts +539 -213
- package/src/modes/components/irc-sidebar.ts +11 -0
- package/src/modes/components/iterm-pet-transport.ts +585 -0
- package/src/modes/components/oauth-selector.ts +277 -86
- package/src/modes/components/pet-capability.ts +39 -40
- package/src/modes/components/settings-selector.ts +9 -0
- package/src/modes/components/status-line/gh.ts +45 -1
- package/src/modes/components/status-line/segments.ts +16 -1
- package/src/modes/components/tool-status-header.ts +13 -3
- package/src/modes/controllers/command-controller.ts +20 -8
- package/src/modes/controllers/extension-ui-controller.ts +56 -257
- package/src/modes/controllers/input-controller.ts +10 -0
- package/src/modes/controllers/runtime-mcp-command-controller.ts +16 -3
- package/src/modes/controllers/selector-controller.ts +177 -72
- package/src/modes/controllers/stt-controller.ts +3 -3
- package/src/modes/interactive-mode.ts +88 -16
- package/src/modes/print-mode.ts +35 -6
- package/src/modes/runtime-init.ts +21 -68
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/injected-user-submission.ts +25 -0
- package/src/modes/utils/terminal-bell.ts +15 -1
- package/src/prompts/agent-fragments/ralplan-persistence.md +1 -1
- package/src/prompts/agent-fragments/restricted-bash.md +1 -1
- package/src/prompts/agents/critic.md +1 -1
- package/src/prompts/system/rlm-research.md +1 -1
- package/src/prompts/system/system-prompt.md +0 -2
- package/src/prompts/tools/bash.md +7 -1
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/cron.md +6 -6
- package/src/prompts/tools/read.md +0 -2
- package/src/prompts/tools/skill-discovery.md +4 -2
- package/src/runtime-mcp/client.ts +427 -23
- package/src/runtime-mcp/config.ts +37 -5
- package/src/runtime-mcp/manager-pool.test.ts +56 -0
- package/src/runtime-mcp/manager.ts +52 -2
- package/src/runtime-mcp/oauth-discovery.ts +13 -0
- package/src/runtime-mcp/oauth-flow.ts +46 -1
- package/src/runtime-mcp/pool-key.ts +5 -1
- package/src/runtime-mcp/pool.test.ts +9 -0
- package/src/runtime-mcp/protocol.test.ts +353 -0
- package/src/runtime-mcp/protocol.ts +624 -0
- package/src/runtime-mcp/scope-config.ts +78 -0
- package/src/runtime-mcp/tool-bridge.ts +44 -10
- package/src/runtime-mcp/transports/http.ts +84 -13
- package/src/runtime-mcp/types.ts +123 -12
- package/src/sdk/acp/adapter.ts +328 -97
- package/src/sdk/broker/broker.ts +160 -741
- package/src/sdk/broker/ensure.ts +9 -1
- package/src/sdk/broker/lifecycle.ts +510 -116
- package/src/sdk/broker/operation-receipt-ledger.ts +11 -2
- package/src/sdk/broker/process-incarnation.ts +19 -0
- package/src/sdk/broker/session-index.ts +516 -117
- package/src/sdk/broker/startup-failure.ts +28 -5
- package/src/sdk/broker/transport.ts +1 -13
- package/src/sdk/bus/chat-daemon-cli.ts +4 -2
- package/src/sdk/bus/chat-daemon-control.ts +67 -6
- package/src/sdk/bus/chat-daemon-runtime.ts +183 -859
- package/src/sdk/bus/config.ts +13 -0
- package/src/sdk/bus/conversation-store.ts +59 -171
- package/src/sdk/bus/daemon-paths.ts +20 -0
- package/src/sdk/bus/discord-conversation.ts +2 -0
- package/src/sdk/bus/discord-daemon.ts +524 -126
- package/src/sdk/bus/discord-live-provider.ts +206 -79
- package/src/sdk/bus/existing-thread-readiness.ts +4 -97
- package/src/sdk/bus/inbound-reaction-ordering.ts +122 -0
- package/src/sdk/bus/index.ts +2480 -456
- package/src/sdk/bus/kind-aware-reconciliation.ts +204 -34
- package/src/sdk/bus/lifecycle-commands.ts +2 -2
- package/src/sdk/bus/managed-daemon.ts +2 -145
- package/src/sdk/bus/notification-service.ts +196 -0
- package/src/sdk/bus/operator-runtime.ts +1 -1
- package/src/sdk/bus/prompt-reconciliation.ts +24 -15
- package/src/sdk/bus/reconciliation-store.ts +553 -24
- package/src/sdk/bus/session-control.ts +49 -3
- package/src/sdk/bus/slack-conversation.ts +4 -0
- package/src/sdk/bus/slack-daemon.ts +733 -134
- package/src/sdk/bus/slack-live-provider.ts +65 -15
- package/src/sdk/bus/slack-provider.ts +4 -0
- package/src/sdk/bus/slack-thread-binding.ts +9 -104
- package/src/sdk/bus/telegram-adoption-intent.ts +299 -209
- package/src/sdk/bus/telegram-daemon-cli.ts +194 -7
- package/src/sdk/bus/telegram-daemon-contract.ts +265 -19
- package/src/sdk/bus/telegram-daemon-control.ts +3 -13
- package/src/sdk/bus/telegram-daemon-orphan-reap.ts +498 -0
- package/src/sdk/bus/telegram-daemon-owner-registry.ts +176 -0
- package/src/sdk/bus/telegram-daemon.ts +2552 -2280
- package/src/sdk/bus/telegram-reference.ts +1 -226
- package/src/sdk/bus/telegram-setup.ts +2 -2
- package/src/sdk/bus/threaded-render.ts +1 -0
- package/src/sdk/bus/topic-registry.ts +279 -146
- package/src/sdk/cli/rows.ts +2 -35
- package/src/sdk/cli/session-cli.ts +776 -700
- package/src/sdk/client/client.ts +892 -1
- package/src/sdk/host/control/dispatch.ts +93 -58
- package/src/sdk/host/control/index.ts +1 -1
- package/src/sdk/host/control/operations.ts +18 -10
- package/src/sdk/host/control/runtime-gate.ts +40 -0
- package/src/sdk/host/host.ts +63 -12
- package/src/sdk/host/query/handlers.ts +66 -7
- package/src/sdk/host/reverse-leases.ts +2 -2
- package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
- package/src/sdk/host/session-runtime.test.ts +2787 -34
- package/src/sdk/host/session-runtime.ts +1489 -101
- package/src/sdk/host/surface-policy.ts +12 -8
- package/src/sdk/host/types.ts +1 -1
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +31 -59
- package/src/sdk/host/websocket-transport.ts +85 -50
- package/src/sdk/index.ts +2 -1
- package/src/sdk/lifecycle/broker-client.ts +53 -0
- package/src/sdk/lifecycle/client.ts +234 -0
- package/src/sdk/lifecycle/index.ts +3 -0
- package/src/sdk/{bus/recent-activity.ts → lifecycle/recent-sessions.ts} +5 -2
- package/src/sdk/lifecycle/service.ts +720 -0
- package/src/sdk/mcp/server.ts +226 -188
- package/src/sdk/prompt-deadline-lease.ts +55 -0
- package/src/sdk/prompt-deadline-manager.ts +148 -0
- package/src/sdk/prompt-status.ts +11 -3
- package/src/sdk/protocol/operation-inventory.generated.json +272 -129
- package/src/sdk/protocol/operation-registry.ts +71 -8
- package/src/sdk/receipt-state.ts +43 -0
- package/src/sdk/reconciliation-extensions.ts +14 -0
- package/src/sdk/router/index.ts +1 -0
- package/src/sdk/router/session-router.ts +1658 -0
- package/src/sdk/session-list.ts +93 -0
- package/src/sdk/session-reconnect.ts +45 -1
- package/src/sdk/session.ts +716 -120
- package/src/sdk/transport/serve-cli.ts +18 -34
- package/src/session/account-inventory.ts +603 -0
- package/src/session/agent-session.ts +2857 -582
- package/src/session/agent-storage.ts +35 -0
- package/src/session/blob-store.ts +32 -1
- package/src/session/coordinator-tool-label.ts +40 -0
- package/src/session/internal/managed-session-scope.ts +4 -0
- package/src/session/internal/managed-session-storage.ts +173 -12
- package/src/session/messages.ts +1 -1
- package/src/session/secure-token-file.ts +80 -0
- package/src/session/session-manager.ts +730 -58
- package/src/session/session-storage.ts +25 -16
- package/src/session/startup-auth-config.ts +297 -0
- package/src/session/terminal-abort.ts +1253 -0
- package/src/session/yield-queue.ts +57 -15
- package/src/session-import/claude.ts +334 -0
- package/src/session-import/codex.ts +1033 -0
- package/src/session-import/command.ts +77 -0
- package/src/session-import/detect.ts +133 -0
- package/src/session-import/index.ts +7 -0
- package/src/session-import/provider-codex.ts +327 -0
- package/src/session-import/provider-service.ts +457 -0
- package/src/session-import/redact.ts +128 -0
- package/src/session-import/service.ts +1264 -0
- package/src/session-import/types.ts +148 -0
- package/src/setup/paseo/check.ts +213 -0
- package/src/setup/paseo/install-saga.ts +293 -0
- package/src/setup/paseo/json-publisher.ts +263 -0
- package/src/setup/paseo/orchestration-preferences.ts +86 -0
- package/src/setup/paseo/paseo-ownership.ts +224 -0
- package/src/setup/paseo/paseo-setup.ts +253 -0
- package/src/setup/paseo/provider-config.ts +139 -0
- package/src/setup/paseo/remove.ts +183 -0
- package/src/setup/paseo/result-types.ts +79 -0
- package/src/setup/paseo/setup-deps.ts +175 -0
- package/src/setup/paseo/skills-bridge.ts +260 -0
- package/src/skill-state/active-state.ts +21 -11
- package/src/slash-commands/acp-builtins.ts +9 -1
- package/src/slash-commands/builtin-registry.ts +98 -16
- package/src/slash-commands/helpers/credential-switch.ts +143 -0
- package/src/slash-commands/helpers/mcp.ts +9 -3
- package/src/slash-commands/helpers/usage-report.ts +61 -56
- package/src/slash-commands/types.ts +7 -2
- package/src/task/executor.ts +30 -11
- package/src/task/index.ts +66 -7
- package/src/task/render.ts +23 -9
- package/src/tools/ask-answer-registry.ts +23 -7
- package/src/tools/ask.ts +8 -4
- package/src/tools/bash-sleep-advisory.ts +115 -0
- package/src/tools/bash.ts +98 -18
- package/src/tools/browser/attach.ts +71 -3
- package/src/tools/browser/launch.ts +86 -13
- package/src/tools/browser/profile-discovery.ts +57 -7
- package/src/tools/browser/render.ts +8 -1
- package/src/tools/browser.ts +130 -21
- package/src/tools/computer.enforcement.test.ts +1 -1
- package/src/tools/computer.ts +3 -2
- package/src/tools/cron.ts +12 -1
- package/src/tools/image-gen.ts +27 -16
- package/src/tools/index.ts +8 -0
- package/src/tools/job.ts +33 -6
- package/src/tools/monitor.ts +41 -2
- package/src/tools/skill-discovery.ts +21 -29
- package/src/tools/subagent.ts +142 -24
- package/src/tools/tool-catalog.generated.ts +6 -6
- package/src/tools/ultragoal-ask-guard.ts +10 -3
- package/src/utils/herdr-pane.ts +345 -0
- package/src/utils/title-generator.ts +2 -0
- package/src/web/scrapers/index.ts +6 -4
- package/dist/types/cli/malloc-env-guard.test.d.ts +0 -1
- package/dist/types/defaults/gjc-defaults.test.d.ts +0 -1
- package/dist/types/memory-backend/lazy-loading.test.d.ts +0 -1
- package/dist/types/runtime/boot-ordering.test.d.ts +0 -1
- package/dist/types/runtime/lazy-service.test.d.ts +0 -1
- package/dist/types/runtime/network-prewarm-service.test.d.ts +0 -1
- package/dist/types/runtime/workspace-tree-service.test.d.ts +0 -1
- package/dist/types/runtime-mcp/config.test.d.ts +0 -1
- package/dist/types/runtime-mcp/http.test.d.ts +0 -1
- package/dist/types/runtime-mcp/manager-pool.test.d.ts +0 -1
- package/dist/types/runtime-mcp/pool-key.test.d.ts +0 -1
- package/dist/types/runtime-mcp/pool.test.d.ts +0 -1
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +0 -117
- package/dist/types/sdk/bus/lifecycle-orchestrator.d.ts +0 -141
- package/dist/types/sdk/bus/telegram-cli.d.ts +0 -31
- package/dist/types/sdk/elevation/allowlist.d.ts +0 -20
- package/dist/types/sdk/elevation/audit.d.ts +0 -93
- package/dist/types/sdk/elevation/capability.d.ts +0 -3
- package/dist/types/sdk/elevation/digest.d.ts +0 -72
- package/dist/types/sdk/elevation/dispatch-receipt.d.ts +0 -85
- package/dist/types/sdk/elevation/grant-ledger.d.ts +0 -163
- package/dist/types/sdk/elevation/owner.d.ts +0 -48
- package/dist/types/sdk/host/import-graph.test.d.ts +0 -1
- package/dist/types/sdk/host/sdk-surface-parity.test.d.ts +0 -1
- package/dist/types/sdk/host/session-runtime.test.d.ts +0 -1
- package/dist/types/sdk/host/websocket-transport.lifecycle.test.d.ts +0 -1
- package/dist/types/session/auth-broker-config.d.ts +0 -13
- package/dist/types/session/heap-eviction-artifacts.test.d.ts +0 -1
- package/dist/types/skills/skills.test.d.ts +0 -1
- package/dist/types/slash-commands/notify-command.test.d.ts +0 -1
- package/dist/types/task/token-log.test.d.ts +0 -1
- package/dist/types/tools/computer.enforcement.test.d.ts +0 -1
- package/dist/types/tools/descriptors.test.d.ts +0 -1
- package/scripts/g005-daemon-smoke.ts +0 -180
- package/scripts/g011-daemon-path-smoke.ts +0 -153
- package/src/sdk/bus/lifecycle-control-runtime.ts +0 -1733
- package/src/sdk/bus/lifecycle-orchestrator.ts +0 -489
- package/src/sdk/bus/telegram-cli.ts +0 -212
- package/src/sdk/elevation/allowlist.ts +0 -54
- package/src/sdk/elevation/audit.ts +0 -208
- package/src/sdk/elevation/capability.ts +0 -41
- package/src/sdk/elevation/digest.ts +0 -129
- package/src/sdk/elevation/dispatch-receipt.ts +0 -184
- package/src/sdk/elevation/grant-ledger.ts +0 -1142
- package/src/sdk/elevation/owner.ts +0 -109
- package/src/session/auth-broker-config.ts +0 -109
- /package/dist/types/{ai-core-import-gate.test.d.ts → discovery/claude-hooks.d.ts} +0 -0
- /package/dist/types/{cli/fixture-report.test.d.ts → discovery/codex-hooks.d.ts} +0 -0
- /package/dist/types/sdk/{bus/recent-activity.d.ts → lifecycle/recent-sessions.d.ts} +0 -0
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as nodeCrypto from "node:crypto";
|
|
2
|
+
import * as fsSync from "node:fs";
|
|
2
3
|
import * as fs from "node:fs/promises";
|
|
3
4
|
import * as path from "node:path";
|
|
5
|
+
import type { NativeExactFileIdentity, NativeExactUnlinkResult, NativeNoReplaceResult } from "@gajae-code/natives";
|
|
6
|
+
import { exactReplacePath, exactUnlink } from "@gajae-code/natives";
|
|
7
|
+
import { isCompiledBinary } from "@gajae-code/utils/env";
|
|
4
8
|
import { YAML } from "bun";
|
|
9
|
+
import type { AtomicYamlNativeWorkerRequest, AtomicYamlNativeWorkerResponse } from "./atomic-yaml-patch-worker";
|
|
5
10
|
import { withFileLock } from "./file-lock";
|
|
6
11
|
|
|
7
12
|
export interface AtomicYamlExpectedPrecondition {
|
|
@@ -49,7 +54,8 @@ export interface AtomicYamlPatchRevision {
|
|
|
49
54
|
export type CasRestoreResult =
|
|
50
55
|
| { status: "restored"; receipt: CasReceipt }
|
|
51
56
|
| { status: "conflict"; paths: readonly string[] }
|
|
52
|
-
| { status: "discarded" }
|
|
57
|
+
| { status: "discarded" }
|
|
58
|
+
| { status: "not-restorable" };
|
|
53
59
|
|
|
54
60
|
/**
|
|
55
61
|
* A receipt intentionally exposes only path-level hashes and opaque revisions.
|
|
@@ -70,6 +76,15 @@ export interface AtomicYamlUpdate<T> {
|
|
|
70
76
|
export interface AtomicYamlPatchOptions {
|
|
71
77
|
/** Test seam for deterministic pre-rename and Windows sharing-violation failures. */
|
|
72
78
|
rename?: (from: string, to: string) => Promise<void>;
|
|
79
|
+
/** Test seam for an identity-checked atomic replacement. */
|
|
80
|
+
exactReplace?: (
|
|
81
|
+
sourcePath: string,
|
|
82
|
+
destinationPath: string,
|
|
83
|
+
expectedSource: NativeExactFileIdentity,
|
|
84
|
+
expectedDestination: NativeExactFileIdentity,
|
|
85
|
+
) => NativeExactUnlinkResult | Promise<NativeExactUnlinkResult>;
|
|
86
|
+
/** Test seam for an atomic no-replace publication. */
|
|
87
|
+
noReplace?: (sourcePath: string, destinationPath: string) => NativeNoReplaceResult | Promise<NativeNoReplaceResult>;
|
|
73
88
|
/** Test seam for bounded retry timing. */
|
|
74
89
|
sleep?: (ms: number) => Promise<void>;
|
|
75
90
|
/** Test seam for Windows rename retry behavior. */
|
|
@@ -88,12 +103,36 @@ export class AtomicYamlReplaceError extends Error {
|
|
|
88
103
|
readonly configPath: string,
|
|
89
104
|
readonly attempts: number,
|
|
90
105
|
readonly cause: unknown,
|
|
106
|
+
readonly preserveTempPath = false,
|
|
91
107
|
) {
|
|
92
108
|
super(`Failed to atomically replace ${configPath} after ${attempts} rename attempts.`);
|
|
93
109
|
this.name = "AtomicYamlReplaceError";
|
|
94
110
|
}
|
|
95
111
|
}
|
|
96
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Raised when the LEXICAL config path no longer resolves to the canonical
|
|
115
|
+
* target an operation is about to write: e.g. a `config.yml` symlink was
|
|
116
|
+
* repointed while the operation waited in the per-file queue or ran. The
|
|
117
|
+
* native identity check protects only the previously resolved destination, so
|
|
118
|
+
* without this guard the write would modify an unrelated prior target and
|
|
119
|
+
* report success while the now-active configuration is left unchanged.
|
|
120
|
+
*/
|
|
121
|
+
export class AtomicYamlRetargetError extends Error {
|
|
122
|
+
readonly code = "ATOMIC_YAML_RETARGETED";
|
|
123
|
+
|
|
124
|
+
constructor(
|
|
125
|
+
readonly lexicalPath: string,
|
|
126
|
+
readonly expectedCanonicalPath: string,
|
|
127
|
+
readonly actualCanonicalPath: string,
|
|
128
|
+
) {
|
|
129
|
+
super(
|
|
130
|
+
`Atomic YAML target retargeted: ${lexicalPath} resolves to ${actualCanonicalPath}, expected ${expectedCanonicalPath}.`,
|
|
131
|
+
);
|
|
132
|
+
this.name = "AtomicYamlRetargetError";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
97
136
|
type PathState = { exists: boolean; value: unknown };
|
|
98
137
|
type ReceiptChange = {
|
|
99
138
|
path: string;
|
|
@@ -109,7 +148,109 @@ const WINDOWS_RENAME_BACKOFF_MS = [10, 25, 50, 100, 200] as const;
|
|
|
109
148
|
const WINDOWS_SHARING_VIOLATION_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
110
149
|
|
|
111
150
|
function canonicalConfigPath(configPath: string): string {
|
|
112
|
-
|
|
151
|
+
const resolved = path.normalize(path.resolve(configPath));
|
|
152
|
+
try {
|
|
153
|
+
// Follow a symlinked config (e.g. `.gjc/config.yml` -> `../shared/config.yml`)
|
|
154
|
+
// so read, identity capture, and the native exact exchange all operate on the
|
|
155
|
+
// real target consistently: the native replacement opens the destination with
|
|
156
|
+
// no-follow semantics and would reject the symlink entry itself. An absent or
|
|
157
|
+
// dangling path falls back to the normalized path (first-time creation still
|
|
158
|
+
// publishes through the no-replace path).
|
|
159
|
+
return fsSync.realpathSync(resolved);
|
|
160
|
+
} catch {
|
|
161
|
+
return resolveDanglingSymlink(resolved);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Reject a symlink retarget right before publication: the lexical config path
|
|
167
|
+
* (e.g. `.gjc/config.yml`) must still resolve to the canonical target this
|
|
168
|
+
* operation read, or the write would modify an unrelated prior target and
|
|
169
|
+
* report success while the now-active configuration is unchanged. The native
|
|
170
|
+
* identity check protects only the previously resolved destination, so this
|
|
171
|
+
* lexical re-resolution is the retarget guard.
|
|
172
|
+
*/
|
|
173
|
+
function assertLexicalTargetUnchanged(lexicalPath: string, canonicalPath: string): void {
|
|
174
|
+
const current = canonicalConfigPath(lexicalPath);
|
|
175
|
+
if (current !== canonicalPath) {
|
|
176
|
+
throw new AtomicYamlRetargetError(lexicalPath, canonicalPath, current);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* realpathSync fails when the final path component is absent or a DANGLING
|
|
182
|
+
* symlink (its target has not been created yet). Treating the symlink pathname
|
|
183
|
+
* itself as the config path would read it as absent while the native no-replace
|
|
184
|
+
* publication rejects the EXISTING symlink directory entry as
|
|
185
|
+
* `destination_exists`, so the first publication must target the link's lexical
|
|
186
|
+
* target instead. An ABSENT final component still resolves its deepest existing
|
|
187
|
+
* ancestor (symlinked parents included), so a parent symlink repointed while
|
|
188
|
+
* the operation waits is caught by the lexical retarget check instead of
|
|
189
|
+
* comparing two unresolved lexical strings. Resolves a chain of dangling links
|
|
190
|
+
* without requiring the target to exist; genuinely absent paths are resolved
|
|
191
|
+
* through their existing ancestors.
|
|
192
|
+
*/
|
|
193
|
+
function resolveDanglingSymlink(configPath: string): string {
|
|
194
|
+
const ancestorResolved = resolveAbsentThroughAncestors(configPath);
|
|
195
|
+
if (ancestorResolved !== null) return ancestorResolved;
|
|
196
|
+
// The path itself exists: it may be a dangling final-component symlink
|
|
197
|
+
// (link target absent) or an unreadable entry; walk the link chain.
|
|
198
|
+
let current = configPath;
|
|
199
|
+
for (let depth = 0; depth < 40; depth++) {
|
|
200
|
+
let stat: fsSync.Stats;
|
|
201
|
+
try {
|
|
202
|
+
stat = fsSync.lstatSync(current);
|
|
203
|
+
} catch {
|
|
204
|
+
// The link chain walked into an ABSENT path: resolve the deepest
|
|
205
|
+
// existing ancestor (symlinked parents realpath'd) and re-append
|
|
206
|
+
// the missing suffix, exactly like the absent-top-level case. The
|
|
207
|
+
// unresolved lexical string would not capture a symlinked parent's
|
|
208
|
+
// identity, so repointing that parent while the operation waits
|
|
209
|
+
// must be detectable by the retarget check instead of comparing
|
|
210
|
+
// two identical unresolved strings.
|
|
211
|
+
return resolveAbsentThroughAncestors(current) ?? current;
|
|
212
|
+
}
|
|
213
|
+
if (!stat.isSymbolicLink()) return current;
|
|
214
|
+
let target: string;
|
|
215
|
+
try {
|
|
216
|
+
target = fsSync.readlinkSync(current);
|
|
217
|
+
} catch {
|
|
218
|
+
return current;
|
|
219
|
+
}
|
|
220
|
+
current = path.isAbsolute(target) ? path.normalize(target) : path.resolve(path.dirname(current), target);
|
|
221
|
+
}
|
|
222
|
+
return current;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Find the deepest EXISTING ancestor of `target`, realpath-resolve it (its
|
|
227
|
+
* symlinks included), and re-append the missing suffix. Returns null when no
|
|
228
|
+
* existing ancestor exists (the filesystem root is reached without an lstat
|
|
229
|
+
* success), leaving the caller free to fall back to the lexical path.
|
|
230
|
+
*/
|
|
231
|
+
function resolveAbsentThroughAncestors(target: string): string | null {
|
|
232
|
+
const missing: string[] = [];
|
|
233
|
+
let existing = target;
|
|
234
|
+
for (;;) {
|
|
235
|
+
try {
|
|
236
|
+
fsSync.lstatSync(existing);
|
|
237
|
+
break;
|
|
238
|
+
} catch {
|
|
239
|
+
const parent = path.dirname(existing);
|
|
240
|
+
if (parent === existing) return null; // filesystem root reached
|
|
241
|
+
missing.unshift(path.basename(existing));
|
|
242
|
+
existing = parent;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// The path itself exists (e.g. a dangling symlink whose chain the caller
|
|
246
|
+
// walks): only a MISSING component is resolved through ancestors, so the
|
|
247
|
+
// caller's link-chain walk owns the existing-path case.
|
|
248
|
+
if (missing.length === 0) return null;
|
|
249
|
+
try {
|
|
250
|
+
return path.join(fsSync.realpathSync(existing), ...missing);
|
|
251
|
+
} catch {
|
|
252
|
+
return path.join(existing, ...missing);
|
|
253
|
+
}
|
|
113
254
|
}
|
|
114
255
|
|
|
115
256
|
function assertPatch(patch: AtomicYamlPatch): void {
|
|
@@ -197,7 +338,8 @@ function stableValue(value: unknown): string {
|
|
|
197
338
|
}
|
|
198
339
|
|
|
199
340
|
function stateHash(state: PathState): string {
|
|
200
|
-
return
|
|
341
|
+
return nodeCrypto
|
|
342
|
+
.createHash("sha256")
|
|
201
343
|
.update(state.exists ? `present:${stableValue(state.value)}` : "absent")
|
|
202
344
|
.digest("hex");
|
|
203
345
|
}
|
|
@@ -211,21 +353,31 @@ export function atomicYamlPathHash(value: Record<string, unknown>, path: string)
|
|
|
211
353
|
return stateHash(stateAtPath(value, path.split(".")));
|
|
212
354
|
}
|
|
213
355
|
|
|
214
|
-
type
|
|
356
|
+
type YamlFileState = {
|
|
357
|
+
exists: boolean;
|
|
358
|
+
raw: string;
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
type YamlReadResult = YamlFileState & {
|
|
215
362
|
current: Record<string, unknown>;
|
|
216
363
|
root: unknown;
|
|
217
364
|
};
|
|
218
365
|
|
|
219
|
-
async function
|
|
366
|
+
async function readYamlFileState(configPath: string): Promise<YamlFileState> {
|
|
220
367
|
try {
|
|
221
|
-
|
|
222
|
-
return { current: record(root) ?? {}, root };
|
|
368
|
+
return { exists: true, raw: await Bun.file(configPath).text() };
|
|
223
369
|
} catch (error) {
|
|
224
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return {
|
|
370
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return { exists: false, raw: "" };
|
|
225
371
|
throw error;
|
|
226
372
|
}
|
|
227
373
|
}
|
|
228
374
|
|
|
375
|
+
async function readYaml(configPath: string): Promise<YamlReadResult> {
|
|
376
|
+
const file = await readYamlFileState(configPath);
|
|
377
|
+
const root = file.exists ? YAML.parse(file.raw) : undefined;
|
|
378
|
+
return { ...file, current: record(root) ?? {}, root };
|
|
379
|
+
}
|
|
380
|
+
|
|
229
381
|
async function syncParentDirectory(directory: string): Promise<void> {
|
|
230
382
|
try {
|
|
231
383
|
const directoryHandle = await fs.open(directory, "r");
|
|
@@ -240,12 +392,247 @@ async function syncParentDirectory(directory: string): Promise<void> {
|
|
|
240
392
|
}
|
|
241
393
|
}
|
|
242
394
|
|
|
243
|
-
async function
|
|
395
|
+
async function captureExactFileIdentity(file: string): Promise<NativeExactFileIdentity | null> {
|
|
396
|
+
try {
|
|
397
|
+
const [bytes, stat, parent] = await Promise.all([
|
|
398
|
+
Bun.file(file).arrayBuffer(),
|
|
399
|
+
fs.stat(file, { bigint: true }),
|
|
400
|
+
fs.stat(path.dirname(file), { bigint: true }),
|
|
401
|
+
]);
|
|
402
|
+
return {
|
|
403
|
+
dev: stat.dev,
|
|
404
|
+
ino: stat.ino,
|
|
405
|
+
nlink: stat.nlink,
|
|
406
|
+
parentDev: parent.dev,
|
|
407
|
+
parentIno: parent.ino,
|
|
408
|
+
size: stat.size,
|
|
409
|
+
mtimeNs: stat.mtimeNs,
|
|
410
|
+
sha256: nodeCrypto.createHash("sha256").update(Buffer.from(bytes)).digest("hex"),
|
|
411
|
+
};
|
|
412
|
+
} catch (error) {
|
|
413
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function currentYamlFileState(configPath: string): Promise<YamlFileState> {
|
|
419
|
+
return await readYamlFileState(configPath);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function runNativeAtomicYamlWorker(
|
|
423
|
+
request: AtomicYamlNativeWorkerRequest,
|
|
424
|
+
): Promise<NativeExactUnlinkResult | NativeNoReplaceResult> {
|
|
425
|
+
const worker = isCompiledBinary()
|
|
426
|
+
? new Worker("./packages/coding-agent/src/config/atomic-yaml-patch-worker.ts", { type: "module" })
|
|
427
|
+
: new Worker(new URL("./atomic-yaml-patch-worker.ts", import.meta.url).href, { type: "module" });
|
|
428
|
+
const { promise, resolve, reject } = Promise.withResolvers<NativeExactUnlinkResult | NativeNoReplaceResult>();
|
|
429
|
+
let settled = false;
|
|
430
|
+
let cleanedUp = false;
|
|
431
|
+
const cleanup = (): void => {
|
|
432
|
+
if (cleanedUp) return;
|
|
433
|
+
cleanedUp = true;
|
|
434
|
+
worker.removeEventListener("message", onMessage);
|
|
435
|
+
worker.removeEventListener("error", onError);
|
|
436
|
+
worker.removeEventListener("messageerror", onMessageError);
|
|
437
|
+
worker.removeEventListener("close", onClose);
|
|
438
|
+
worker.removeEventListener("exit", onExit);
|
|
439
|
+
};
|
|
440
|
+
const succeed = (result: NativeExactUnlinkResult | NativeNoReplaceResult): void => {
|
|
441
|
+
if (settled) return;
|
|
442
|
+
settled = true;
|
|
443
|
+
cleanup();
|
|
444
|
+
resolve(result);
|
|
445
|
+
};
|
|
446
|
+
const fail = (error: Error): void => {
|
|
447
|
+
if (settled) return;
|
|
448
|
+
settled = true;
|
|
449
|
+
cleanup();
|
|
450
|
+
reject(error);
|
|
451
|
+
};
|
|
452
|
+
const onMessage: EventListener = event => {
|
|
453
|
+
const response = (event as MessageEvent<AtomicYamlNativeWorkerResponse>).data;
|
|
454
|
+
if (response?.type === "result") {
|
|
455
|
+
succeed(response.result);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (response?.type === "error") {
|
|
459
|
+
const failure = new Error(response.message);
|
|
460
|
+
if (response.name) failure.name = response.name;
|
|
461
|
+
if (response.stack) failure.stack = response.stack;
|
|
462
|
+
fail(failure);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
fail(new Error("Atomic YAML native worker returned an invalid response."));
|
|
466
|
+
};
|
|
467
|
+
const onError: EventListener = () => fail(new Error("Atomic YAML native worker failed."));
|
|
468
|
+
const onMessageError: EventListener = () => fail(new Error("Atomic YAML native worker message failed."));
|
|
469
|
+
// Bun emits `close`; Node-compatible worker hosts may emit `exit` instead.
|
|
470
|
+
const onClose: EventListener = () => fail(new Error("Atomic YAML native worker closed before responding."));
|
|
471
|
+
const onExit: EventListener = () => fail(new Error("Atomic YAML native worker exited before responding."));
|
|
472
|
+
try {
|
|
473
|
+
worker.addEventListener("message", onMessage);
|
|
474
|
+
worker.addEventListener("error", onError);
|
|
475
|
+
worker.addEventListener("messageerror", onMessageError);
|
|
476
|
+
worker.addEventListener("close", onClose);
|
|
477
|
+
worker.addEventListener("exit", onExit);
|
|
478
|
+
worker.postMessage(request);
|
|
479
|
+
return await promise;
|
|
480
|
+
} catch (error) {
|
|
481
|
+
// The native op was already dispatched; a termination without a response
|
|
482
|
+
// leaves the exchange outcome unknown (it may have committed), so the
|
|
483
|
+
// staged path must be retained for recovery instead of unlinked.
|
|
484
|
+
throw new AtomicYamlReplaceError(request.destinationPath, 1, error, true);
|
|
485
|
+
} finally {
|
|
486
|
+
cleanup();
|
|
487
|
+
worker.terminate();
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function replaceWithExpectedIdentity(
|
|
492
|
+
tempPath: string,
|
|
493
|
+
configPath: string,
|
|
494
|
+
expectedState: YamlFileState,
|
|
495
|
+
options: AtomicYamlPatchOptions,
|
|
496
|
+
): Promise<void> {
|
|
497
|
+
const { exists: expectedExists, raw: expectedRaw } = expectedState;
|
|
498
|
+
const expectedHash = nodeCrypto.createHash("sha256").update(expectedRaw).digest("hex");
|
|
499
|
+
const [source, destination] = await Promise.all([
|
|
500
|
+
captureExactFileIdentity(tempPath),
|
|
501
|
+
captureExactFileIdentity(configPath),
|
|
502
|
+
]);
|
|
503
|
+
if (!source)
|
|
504
|
+
throw new AtomicYamlReplaceError(configPath, 1, new Error("staged YAML disappeared before replacement"));
|
|
505
|
+
if (!destination) {
|
|
506
|
+
if (expectedExists)
|
|
507
|
+
throw new AtomicYamlConflictError(
|
|
508
|
+
configPath,
|
|
509
|
+
expectedHash,
|
|
510
|
+
nodeCrypto.createHash("sha256").update("").digest("hex"),
|
|
511
|
+
);
|
|
512
|
+
const publishOnce = async (): Promise<NativeNoReplaceResult> =>
|
|
513
|
+
options.noReplace
|
|
514
|
+
? await options.noReplace(tempPath, configPath)
|
|
515
|
+
: ((await runNativeAtomicYamlWorker({
|
|
516
|
+
operation: "no-replace",
|
|
517
|
+
sourcePath: tempPath,
|
|
518
|
+
destinationPath: configPath,
|
|
519
|
+
})) as NativeNoReplaceResult);
|
|
520
|
+
const publishOnceWithLink = async (): Promise<NativeNoReplaceResult> =>
|
|
521
|
+
options.noReplace
|
|
522
|
+
? await options.noReplace(tempPath, configPath)
|
|
523
|
+
: ((await runNativeAtomicYamlWorker({
|
|
524
|
+
operation: "no-replace-link",
|
|
525
|
+
sourcePath: tempPath,
|
|
526
|
+
destinationPath: configPath,
|
|
527
|
+
})) as NativeNoReplaceResult);
|
|
528
|
+
let published = await publishOnce();
|
|
529
|
+
if (
|
|
530
|
+
!published.ok &&
|
|
531
|
+
(published.reason === "invalid_request" || published.reason === "atomic_unavailable") &&
|
|
532
|
+
published.mutationState === "not_committed" &&
|
|
533
|
+
published.durabilityState === "not_attempted"
|
|
534
|
+
) {
|
|
535
|
+
// RENAME_NOREPLACE is unsupported (NFS, kernels before 3.15): the native
|
|
536
|
+
// linkat fallback keeps the no-overwrite guarantee (EEXIST on an
|
|
537
|
+
// occupied destination) and is safe because the rename reported a
|
|
538
|
+
// pre-mutation refusal. The staged name survives a link, so the caller
|
|
539
|
+
// still unlinks it after publication.
|
|
540
|
+
published = await publishOnceWithLink();
|
|
541
|
+
}
|
|
542
|
+
if (published.ok) return;
|
|
543
|
+
if (published.reason === "destination_exists") {
|
|
544
|
+
const actual = await currentYamlFileState(configPath);
|
|
545
|
+
throw new AtomicYamlConflictError(
|
|
546
|
+
configPath,
|
|
547
|
+
expectedHash,
|
|
548
|
+
nodeCrypto.createHash("sha256").update(actual.raw).digest("hex"),
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
throw new AtomicYamlReplaceError(
|
|
552
|
+
configPath,
|
|
553
|
+
1,
|
|
554
|
+
new Error(`native no-replace publish failed: ${published.code ?? "unknown"}`),
|
|
555
|
+
published.mutationState === "unknown",
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
if (!expectedExists || destination.sha256 !== expectedHash) {
|
|
559
|
+
throw new AtomicYamlConflictError(configPath, expectedHash, destination.sha256 ?? expectedHash);
|
|
560
|
+
}
|
|
561
|
+
// `exactReplacePath` validates both identities inside one native atomic
|
|
562
|
+
// namespace exchange. A save after the snapshots therefore rejects the
|
|
563
|
+
// exchange without replacing the editor's newer destination.
|
|
564
|
+
const replaced: NativeExactUnlinkResult = options.exactReplace
|
|
565
|
+
? await options.exactReplace(tempPath, configPath, source, destination)
|
|
566
|
+
: ((await runNativeAtomicYamlWorker({
|
|
567
|
+
operation: "exact-replace",
|
|
568
|
+
sourcePath: tempPath,
|
|
569
|
+
destinationPath: configPath,
|
|
570
|
+
expectedSource: source,
|
|
571
|
+
expectedDestination: destination,
|
|
572
|
+
})) as NativeExactUnlinkResult);
|
|
573
|
+
if (replaced.ok) return;
|
|
574
|
+
// RENAME_EXCHANGE is unsupported (NFS, kernels before 3.15): the native
|
|
575
|
+
// exact-replace reports atomic_unavailable. A replacement fallback that
|
|
576
|
+
// briefly detaches the destination would expose an ABSENT config.yml to
|
|
577
|
+
// concurrent readers (workflow resolvers do not take the config lock) that
|
|
578
|
+
// could then fall back to legacy/default settings - so the write FAILS
|
|
579
|
+
// CLOSED instead of trading a transient absence window for availability.
|
|
580
|
+
if (replaced.code === "atomic_unavailable") {
|
|
581
|
+
throw new AtomicYamlReplaceError(
|
|
582
|
+
configPath,
|
|
583
|
+
1,
|
|
584
|
+
new Error(
|
|
585
|
+
`native exact replacement is unavailable on this filesystem (atomic_unavailable); refusing a fallback that would expose an absent config.yml to concurrent readers`,
|
|
586
|
+
),
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
// A retained successor proves the namespace exchange published the staged
|
|
590
|
+
// document. It may still report a post-exchange verification or durability
|
|
591
|
+
// failure, so this is not a CAS rejection and the detached temp-path object
|
|
592
|
+
// must be retained for native recovery rather than unlinked by the caller.
|
|
593
|
+
if (
|
|
594
|
+
replaced.detachedPath ||
|
|
595
|
+
replaced.retainedSuccessorPath ||
|
|
596
|
+
replaced.retainedPlaceholderPath ||
|
|
597
|
+
replaced.retainedUnknownPath
|
|
598
|
+
) {
|
|
599
|
+
throw new AtomicYamlReplaceError(
|
|
600
|
+
configPath,
|
|
601
|
+
1,
|
|
602
|
+
new Error(`native exact replacement completed with retained recovery paths: ${replaced.code ?? "unknown"}`),
|
|
603
|
+
true,
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
if (replaced.code === "identity_mismatch") {
|
|
607
|
+
const actual = await currentYamlFileState(configPath);
|
|
608
|
+
throw new AtomicYamlConflictError(
|
|
609
|
+
configPath,
|
|
610
|
+
expectedHash,
|
|
611
|
+
nodeCrypto.createHash("sha256").update(actual.raw).digest("hex"),
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
throw new AtomicYamlReplaceError(
|
|
615
|
+
configPath,
|
|
616
|
+
1,
|
|
617
|
+
new Error(`native exact replacement failed: ${replaced.code ?? "unknown"}`),
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
async function replaceWithRetry(
|
|
622
|
+
tempPath: string,
|
|
623
|
+
configPath: string,
|
|
624
|
+
options: AtomicYamlPatchOptions,
|
|
625
|
+
/** Expected current file state; native exact replacement validates it with the destination identity. */
|
|
626
|
+
expectedState?: YamlFileState,
|
|
627
|
+
): Promise<void> {
|
|
628
|
+
if (expectedState !== undefined) {
|
|
629
|
+
await replaceWithExpectedIdentity(tempPath, configPath, expectedState, options);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
244
632
|
const rename = options.rename ?? fs.rename;
|
|
245
633
|
const sleep = options.sleep ?? (async (delay: number): Promise<void> => await Bun.sleep(delay));
|
|
246
634
|
const isWindows = (options.platform ?? process.platform) === "win32";
|
|
247
635
|
let attempts = 0;
|
|
248
|
-
|
|
249
636
|
for (;;) {
|
|
250
637
|
attempts++;
|
|
251
638
|
try {
|
|
@@ -269,22 +656,168 @@ async function writeAtomicYaml(
|
|
|
269
656
|
configPath: string,
|
|
270
657
|
value: Record<string, unknown>,
|
|
271
658
|
options: AtomicYamlPatchOptions,
|
|
272
|
-
|
|
659
|
+
/** Expected current file state; re-verified immediately before the rename. */
|
|
660
|
+
expectedState?: YamlFileState,
|
|
661
|
+
/** Lexical (unresolved) config path; re-verified against the canonical target before the rename. */
|
|
662
|
+
lexicalConfigPath?: string,
|
|
663
|
+
): Promise<YamlFileState> {
|
|
273
664
|
const directory = path.dirname(configPath);
|
|
274
|
-
const tempPath = path.join(directory, `.${path.basename(configPath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
665
|
+
const tempPath = path.join(directory, `.${path.basename(configPath)}.${process.pid}.${nodeCrypto.randomUUID()}.tmp`);
|
|
666
|
+
const nextRaw = YAML.stringify(value, null, 2);
|
|
667
|
+
let preserveTempPath = false;
|
|
275
668
|
try {
|
|
276
669
|
const tempHandle = await fs.open(tempPath, "wx", 0o600);
|
|
277
670
|
try {
|
|
278
|
-
await tempHandle.writeFile(
|
|
671
|
+
await tempHandle.writeFile(nextRaw, "utf8");
|
|
279
672
|
await tempHandle.sync();
|
|
280
673
|
} finally {
|
|
281
674
|
await tempHandle.close();
|
|
282
675
|
}
|
|
283
|
-
|
|
676
|
+
// Reject a lexical retarget (e.g. a repointed config.yml symlink) before
|
|
677
|
+
// the rename, whether it happened while the operation waited in the queue
|
|
678
|
+
// or while it ran; then re-verify the CAS state right before the rename so
|
|
679
|
+
// an external save during the temp write cannot be silently overwritten.
|
|
680
|
+
if (lexicalConfigPath !== undefined) assertLexicalTargetUnchanged(lexicalConfigPath, configPath);
|
|
681
|
+
if (expectedState !== undefined) {
|
|
682
|
+
// The transaction's CAS guard ran before this write; re-verify right
|
|
683
|
+
// before the rename so an external save during the temp write cannot be
|
|
684
|
+
// silently overwritten by the replacement.
|
|
685
|
+
const currentState = await currentYamlFileState(configPath);
|
|
686
|
+
if (currentState.exists !== expectedState.exists || currentState.raw !== expectedState.raw) {
|
|
687
|
+
throw new AtomicYamlConflictError(
|
|
688
|
+
configPath,
|
|
689
|
+
nodeCrypto.createHash("sha256").update(expectedState.raw).digest("hex"),
|
|
690
|
+
nodeCrypto.createHash("sha256").update(currentState.raw).digest("hex"),
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
// Capture whether the canonical target existed BEFORE the publication:
|
|
695
|
+
// the retarget rollback must distinguish a first-write (restore absence)
|
|
696
|
+
// from an exchange (restore the pre-publication content). For
|
|
697
|
+
// nontransactional writers expectedState is undefined, so probe the
|
|
698
|
+
// filesystem here - after this point the publication changes the answer.
|
|
699
|
+
const targetExistedBefore =
|
|
700
|
+
expectedState?.exists ??
|
|
701
|
+
(await fs
|
|
702
|
+
.lstat(configPath)
|
|
703
|
+
.then(() => true)
|
|
704
|
+
.catch(() => false));
|
|
705
|
+
let publishedIdentity: NativeExactFileIdentity | null = null;
|
|
706
|
+
publishedIdentity = await captureExactFileIdentity(tempPath);
|
|
707
|
+
try {
|
|
708
|
+
await replaceWithRetry(tempPath, configPath, options, expectedState);
|
|
709
|
+
} catch (error) {
|
|
710
|
+
preserveTempPath = error instanceof AtomicYamlReplaceError && error.preserveTempPath;
|
|
711
|
+
throw error;
|
|
712
|
+
}
|
|
713
|
+
// Revalidate the lexical target AFTER the publication: the native
|
|
714
|
+
// exchange validated only the previously resolved canonical
|
|
715
|
+
// destination, so a config.yml symlink repointed between the
|
|
716
|
+
// pre-publication retarget check and the exchange would update the
|
|
717
|
+
// now-inactive target and report success while the lexical config.yml
|
|
718
|
+
// points at an unchanged file. The old canonical state is restored
|
|
719
|
+
// before failing: retrying must update the new target without leaving
|
|
720
|
+
// the now-inactive profile modified as a side effect.
|
|
721
|
+
if (lexicalConfigPath !== undefined) {
|
|
722
|
+
try {
|
|
723
|
+
assertLexicalTargetUnchanged(lexicalConfigPath, configPath);
|
|
724
|
+
} catch (error) {
|
|
725
|
+
let rollbackFailed: Error | null = null;
|
|
726
|
+
try {
|
|
727
|
+
if (!targetExistedBefore) {
|
|
728
|
+
// Restore absence with an IDENTITY-ATOMIC unlink: the native
|
|
729
|
+
// validates the destination identity in the same syscall
|
|
730
|
+
// sequence as the unlink, so a successor appearing after
|
|
731
|
+
// our earlier verification is retained as a conflict.
|
|
732
|
+
if (publishedIdentity === null) {
|
|
733
|
+
throw new Error("no published identity available to remove");
|
|
734
|
+
}
|
|
735
|
+
// The native exact unlink requires a caller-persisted,
|
|
736
|
+
// single-component quarantine destination (authority survives
|
|
737
|
+
// a post-detach crash); the placeholder is exchanged into it
|
|
738
|
+
// and cleaned up within the same protocol.
|
|
739
|
+
const removed = exactUnlink(configPath, {
|
|
740
|
+
...publishedIdentity,
|
|
741
|
+
quarantineName: `.${path.basename(configPath)}.${process.pid}.${nodeCrypto.randomUUID()}.quarantine`,
|
|
742
|
+
});
|
|
743
|
+
// cleanup_pending with a detached path + the canonical pathname
|
|
744
|
+
// proven absent counts as removed (the native retirement
|
|
745
|
+
// protocol retains the placeholder for crash recovery); the
|
|
746
|
+
// retained artifacts are purged best-effort below.
|
|
747
|
+
const detached =
|
|
748
|
+
removed.ok || (removed.code === "cleanup_pending" && typeof removed.detachedPath === "string");
|
|
749
|
+
if (!detached) {
|
|
750
|
+
throw new Error(
|
|
751
|
+
"the canonical target was replaced by another writer; the first-write publication was not removed",
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
if (removed.retainedPlaceholderPath) {
|
|
755
|
+
await fs.rm(removed.retainedPlaceholderPath, { force: true }).catch(() => undefined);
|
|
756
|
+
}
|
|
757
|
+
if (removed.detachedPath) {
|
|
758
|
+
await fs.rm(removed.detachedPath, { force: true }).catch(() => undefined);
|
|
759
|
+
}
|
|
760
|
+
} else {
|
|
761
|
+
// EXISTING target: restore the pre-publication content
|
|
762
|
+
// captured by the CAS state through an identity-checked
|
|
763
|
+
// EXCHANGE. The native validates the destination identity
|
|
764
|
+
// atomically with the replacement, so a concurrent update
|
|
765
|
+
// is a rollback conflict, never clobbered with the stale
|
|
766
|
+
// bytes. The exchange unlinks both paths, so the original
|
|
767
|
+
// temp path is NOT a recovery source.
|
|
768
|
+
if (expectedState?.raw === undefined) {
|
|
769
|
+
throw new Error("no pre-publication content available to restore");
|
|
770
|
+
}
|
|
771
|
+
if (publishedIdentity === null) {
|
|
772
|
+
throw new Error("no published identity available to restore over");
|
|
773
|
+
}
|
|
774
|
+
const restoreTemp = path.join(
|
|
775
|
+
directory,
|
|
776
|
+
`.${path.basename(configPath)}.${process.pid}.${nodeCrypto.randomUUID()}.restore.tmp`,
|
|
777
|
+
);
|
|
778
|
+
try {
|
|
779
|
+
// Create the restore staging file with PRIVATE permissions from
|
|
780
|
+
// the outset (same wx + 0o600 pattern as the main temp path):
|
|
781
|
+
// a directory watcher must never observe the previous
|
|
782
|
+
// configuration at mode 0644 before a post-write chmod.
|
|
783
|
+
const restoreHandle = await fs.open(restoreTemp, "wx", 0o600);
|
|
784
|
+
try {
|
|
785
|
+
await restoreHandle.writeFile(expectedState.raw, "utf8");
|
|
786
|
+
} finally {
|
|
787
|
+
await restoreHandle.close();
|
|
788
|
+
}
|
|
789
|
+
const restoreIdentity = await captureExactFileIdentity(restoreTemp);
|
|
790
|
+
if (restoreIdentity === null) {
|
|
791
|
+
throw new Error("staged restore disappeared");
|
|
792
|
+
}
|
|
793
|
+
const replaced = exactReplacePath(restoreTemp, configPath, restoreIdentity, publishedIdentity);
|
|
794
|
+
if (!replaced.ok) {
|
|
795
|
+
throw new Error(
|
|
796
|
+
"the canonical target was replaced by another writer; the old content was not restored",
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
} finally {
|
|
800
|
+
await fs.rm(restoreTemp, { force: true }).catch(() => undefined);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
} catch (rollbackError) {
|
|
804
|
+
rollbackFailed = rollbackError instanceof Error ? rollbackError : new Error(String(rollbackError));
|
|
805
|
+
}
|
|
806
|
+
if (rollbackFailed !== null) {
|
|
807
|
+
throw new AtomicYamlReplaceError(
|
|
808
|
+
configPath,
|
|
809
|
+
1,
|
|
810
|
+
new Error(`Atomic YAML target retargeted and rollback failed: ${rollbackFailed.message}`),
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
throw error;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
284
816
|
await syncParentDirectory(directory);
|
|
285
817
|
} finally {
|
|
286
|
-
await fs.rm(tempPath, { force: true }).catch(() => undefined);
|
|
818
|
+
if (!preserveTempPath) await fs.rm(tempPath, { force: true }).catch(() => undefined);
|
|
287
819
|
}
|
|
820
|
+
return { exists: true, raw: nextRaw };
|
|
288
821
|
}
|
|
289
822
|
|
|
290
823
|
function createReceipt(
|
|
@@ -302,7 +835,7 @@ function createReceipt(
|
|
|
302
835
|
},
|
|
303
836
|
async restore(): Promise<CasRestoreResult> {
|
|
304
837
|
if (discarded) return { status: "discarded" };
|
|
305
|
-
return await enqueueAtomicYamlOperation(configPath, async canonicalPath => {
|
|
838
|
+
return await enqueueAtomicYamlOperation(configPath, async (canonicalPath, lexicalPath) => {
|
|
306
839
|
if (discarded) return { status: "discarded" };
|
|
307
840
|
return await withFileLock(canonicalPath, async () => {
|
|
308
841
|
const { current, root } = await readYaml(canonicalPath);
|
|
@@ -320,7 +853,15 @@ function createReceipt(
|
|
|
320
853
|
: { path: change.path, op: "unset" },
|
|
321
854
|
);
|
|
322
855
|
await options.validateRoot?.(root, restorePatches);
|
|
323
|
-
const receipt = await applyPatchesUnderLock(
|
|
856
|
+
const receipt = await applyPatchesUnderLock(
|
|
857
|
+
canonicalPath,
|
|
858
|
+
current,
|
|
859
|
+
restorePatches,
|
|
860
|
+
options,
|
|
861
|
+
false,
|
|
862
|
+
undefined,
|
|
863
|
+
lexicalPath,
|
|
864
|
+
);
|
|
324
865
|
await options.onRestored?.(restorePatches);
|
|
325
866
|
return { status: "restored", receipt };
|
|
326
867
|
});
|
|
@@ -334,6 +875,9 @@ async function applyPatchesUnderLock(
|
|
|
334
875
|
current: Record<string, unknown>,
|
|
335
876
|
patches: readonly AtomicYamlPatch[],
|
|
336
877
|
options: AtomicYamlPatchOptions,
|
|
878
|
+
skipWrite = false,
|
|
879
|
+
expectedState?: YamlFileState,
|
|
880
|
+
lexicalConfigPath?: string,
|
|
337
881
|
): Promise<CasReceipt> {
|
|
338
882
|
if (patches.length === 0) return createReceipt(configPath, [], options);
|
|
339
883
|
|
|
@@ -373,7 +917,7 @@ async function applyPatchesUnderLock(
|
|
|
373
917
|
}
|
|
374
918
|
const changes = [...changesByPath.values()];
|
|
375
919
|
|
|
376
|
-
await writeAtomicYaml(configPath, current, options);
|
|
920
|
+
if (!skipWrite) await writeAtomicYaml(configPath, current, options, expectedState, lexicalConfigPath);
|
|
377
921
|
return createReceipt(configPath, changes, options);
|
|
378
922
|
}
|
|
379
923
|
|
|
@@ -385,14 +929,187 @@ export function applyAtomicYamlPatchesWithCurrent(
|
|
|
385
929
|
) => Promise<readonly AtomicYamlPatch[]> | readonly AtomicYamlPatch[],
|
|
386
930
|
options: AtomicYamlPatchOptions = {},
|
|
387
931
|
): Promise<CasReceipt> {
|
|
388
|
-
return enqueueAtomicYamlOperation(configPath, async canonicalPath => {
|
|
932
|
+
return enqueueAtomicYamlOperation(configPath, async (canonicalPath, lexicalPath) => {
|
|
389
933
|
await fs.mkdir(path.dirname(canonicalPath), { recursive: true, mode: 0o700 });
|
|
390
934
|
return await withFileLock(canonicalPath, async () => {
|
|
391
935
|
const { current, root } = await readYaml(canonicalPath);
|
|
392
936
|
const patches = await buildPatches(current);
|
|
393
937
|
for (const patch of patches) assertPatch(patch);
|
|
394
938
|
await options.validateRoot?.(root, patches);
|
|
395
|
-
return await applyPatchesUnderLock(canonicalPath, current, patches, options);
|
|
939
|
+
return await applyPatchesUnderLock(canonicalPath, current, patches, options, false, undefined, lexicalPath);
|
|
940
|
+
});
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
export interface AtomicYamlConfigTransaction {
|
|
944
|
+
configPath: string;
|
|
945
|
+
root: unknown;
|
|
946
|
+
current: Readonly<Record<string, unknown>>;
|
|
947
|
+
/** True once any write op has durably committed (a later CAS rejection then
|
|
948
|
+
* leaves the target with partial writes, so recovery artifacts must stay). */
|
|
949
|
+
written: boolean;
|
|
950
|
+
applyPatches(patches: readonly AtomicYamlPatch[], options?: AtomicYamlPatchOptions): Promise<CasReceipt>;
|
|
951
|
+
/**
|
|
952
|
+
* Delete top-level keys verbatim, including dotted key names (e.g. a flat
|
|
953
|
+
* `"gjc.ralplan.maxIterations"` key that the patch grammar would otherwise
|
|
954
|
+
* interpret as a nested path). Writes atomically under the same lock.
|
|
955
|
+
*/
|
|
956
|
+
removeTopLevelKeys(keys: readonly string[], options?: AtomicYamlPatchOptions): Promise<CasReceipt>;
|
|
957
|
+
/**
|
|
958
|
+
* Apply patches AND delete top-level keys verbatim in a SINGLE atomic
|
|
959
|
+
* write, so an external editor's change cannot land between the two
|
|
960
|
+
* operations (external editors do not participate in the file lock). The
|
|
961
|
+
* returned receipt is not restorable (the deletions are not journaled).
|
|
962
|
+
*/
|
|
963
|
+
applyPatchesAndRemoveTopLevelKeys(
|
|
964
|
+
patches: readonly AtomicYamlPatch[],
|
|
965
|
+
topLevelKeys: readonly string[],
|
|
966
|
+
options?: AtomicYamlPatchOptions,
|
|
967
|
+
): Promise<CasReceipt>;
|
|
968
|
+
/**
|
|
969
|
+
* Replace the whole document (used to revert the target when a later
|
|
970
|
+
* verification fails). Writes atomically under the same lock; the returned
|
|
971
|
+
* receipt is not restorable.
|
|
972
|
+
*/
|
|
973
|
+
replaceCurrent(next: Readonly<Record<string, unknown>>, options?: AtomicYamlPatchOptions): Promise<CasReceipt>;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Run a caller-owned multi-step mutation under the config file's per-file queue
|
|
978
|
+
* and cross-process lock. The current YAML is read once and exposed as
|
|
979
|
+
* `root`/`current`; the callback may inspect it, decide patches, and apply them
|
|
980
|
+
* (or perform adjacent durable actions such as marker/source transitions)
|
|
981
|
+
* without re-acquiring the lock. A YAML parse failure surfaces before the
|
|
982
|
+
* callback runs, so no migration action can execute against a malformed target.
|
|
983
|
+
*/
|
|
984
|
+
export function withAtomicYamlConfigTransaction<T>(
|
|
985
|
+
configPath: string,
|
|
986
|
+
operation: (transaction: AtomicYamlConfigTransaction) => Promise<T>,
|
|
987
|
+
): Promise<T> {
|
|
988
|
+
return enqueueAtomicYamlOperation(configPath, async (canonicalPath, lexicalPath) => {
|
|
989
|
+
await fs.mkdir(path.dirname(canonicalPath), { recursive: true, mode: 0o700 });
|
|
990
|
+
return await withFileLock(canonicalPath, async () => {
|
|
991
|
+
const { current, root, raw, exists } = await readYaml(canonicalPath);
|
|
992
|
+
// External editors do not participate in the file lock, so a save
|
|
993
|
+
// between the initial read and a write would be silently overwritten.
|
|
994
|
+
// Guard every write with a compare-and-swap against the last file state
|
|
995
|
+
// this transaction read/wrote; on mismatch, fail closed.
|
|
996
|
+
const initialState: YamlFileState = { exists, raw };
|
|
997
|
+
let lastKnownState: YamlFileState | null = null;
|
|
998
|
+
const casGuard = async (): Promise<void> => {
|
|
999
|
+
const expected = lastKnownState ?? initialState;
|
|
1000
|
+
const currentState = await currentYamlFileState(canonicalPath);
|
|
1001
|
+
if (currentState.exists !== expected.exists || currentState.raw !== expected.raw) {
|
|
1002
|
+
throw new AtomicYamlConflictError(
|
|
1003
|
+
canonicalPath,
|
|
1004
|
+
nodeCrypto.createHash("sha256").update(expected.raw).digest("hex"),
|
|
1005
|
+
nodeCrypto.createHash("sha256").update(currentState.raw).digest("hex"),
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
let written = false;
|
|
1010
|
+
const markWritten = (): void => {
|
|
1011
|
+
written = true;
|
|
1012
|
+
};
|
|
1013
|
+
return await operation({
|
|
1014
|
+
configPath: canonicalPath,
|
|
1015
|
+
root,
|
|
1016
|
+
current,
|
|
1017
|
+
get written(): boolean {
|
|
1018
|
+
return written;
|
|
1019
|
+
},
|
|
1020
|
+
applyPatches: async (patches, options = {}) => {
|
|
1021
|
+
for (const patch of patches) assertPatch(patch);
|
|
1022
|
+
await options.validateRoot?.(root, patches);
|
|
1023
|
+
await casGuard();
|
|
1024
|
+
const receipt = await applyPatchesUnderLock(
|
|
1025
|
+
canonicalPath,
|
|
1026
|
+
current,
|
|
1027
|
+
patches,
|
|
1028
|
+
options,
|
|
1029
|
+
false,
|
|
1030
|
+
lastKnownState ?? initialState,
|
|
1031
|
+
lexicalPath,
|
|
1032
|
+
);
|
|
1033
|
+
if (patches.length > 0) {
|
|
1034
|
+
lastKnownState = { exists: true, raw: YAML.stringify(current, null, 2) };
|
|
1035
|
+
markWritten();
|
|
1036
|
+
}
|
|
1037
|
+
return receipt;
|
|
1038
|
+
},
|
|
1039
|
+
removeTopLevelKeys: async (keys, options = {}) => {
|
|
1040
|
+
for (const key of keys) delete current[key];
|
|
1041
|
+
await casGuard();
|
|
1042
|
+
lastKnownState = await writeAtomicYaml(
|
|
1043
|
+
canonicalPath,
|
|
1044
|
+
current,
|
|
1045
|
+
options,
|
|
1046
|
+
lastKnownState ?? initialState,
|
|
1047
|
+
lexicalPath,
|
|
1048
|
+
);
|
|
1049
|
+
markWritten();
|
|
1050
|
+
// The deleted top-level key values are not journaled, so a
|
|
1051
|
+
// restore() would vacuously claim success; report honestly that
|
|
1052
|
+
// the receipt is not restorable.
|
|
1053
|
+
let discarded = false;
|
|
1054
|
+
return {
|
|
1055
|
+
revisions: [],
|
|
1056
|
+
discard(): void {
|
|
1057
|
+
discarded = true;
|
|
1058
|
+
},
|
|
1059
|
+
async restore(): Promise<CasRestoreResult> {
|
|
1060
|
+
return discarded ? { status: "discarded" } : { status: "not-restorable" };
|
|
1061
|
+
},
|
|
1062
|
+
};
|
|
1063
|
+
},
|
|
1064
|
+
applyPatchesAndRemoveTopLevelKeys: async (patches, topLevelKeys, options = {}) => {
|
|
1065
|
+
for (const patch of patches) assertPatch(patch);
|
|
1066
|
+
await options.validateRoot?.(root, patches);
|
|
1067
|
+
await casGuard();
|
|
1068
|
+
await applyPatchesUnderLock(canonicalPath, current, patches, options, true, undefined, lexicalPath);
|
|
1069
|
+
for (const key of topLevelKeys) delete current[key];
|
|
1070
|
+
lastKnownState = await writeAtomicYaml(
|
|
1071
|
+
canonicalPath,
|
|
1072
|
+
current,
|
|
1073
|
+
options,
|
|
1074
|
+
lastKnownState ?? initialState,
|
|
1075
|
+
lexicalPath,
|
|
1076
|
+
);
|
|
1077
|
+
markWritten();
|
|
1078
|
+
let discarded = false;
|
|
1079
|
+
return {
|
|
1080
|
+
revisions: [],
|
|
1081
|
+
discard(): void {
|
|
1082
|
+
discarded = true;
|
|
1083
|
+
},
|
|
1084
|
+
async restore(): Promise<CasRestoreResult> {
|
|
1085
|
+
return discarded ? { status: "discarded" } : { status: "not-restorable" };
|
|
1086
|
+
},
|
|
1087
|
+
};
|
|
1088
|
+
},
|
|
1089
|
+
replaceCurrent: async (next, options = {}) => {
|
|
1090
|
+
for (const key of Object.keys(current)) delete current[key];
|
|
1091
|
+
Object.assign(current, next);
|
|
1092
|
+
await casGuard();
|
|
1093
|
+
lastKnownState = await writeAtomicYaml(
|
|
1094
|
+
canonicalPath,
|
|
1095
|
+
current,
|
|
1096
|
+
options,
|
|
1097
|
+
lastKnownState ?? initialState,
|
|
1098
|
+
lexicalPath,
|
|
1099
|
+
);
|
|
1100
|
+
markWritten();
|
|
1101
|
+
let discarded = false;
|
|
1102
|
+
return {
|
|
1103
|
+
revisions: [],
|
|
1104
|
+
discard(): void {
|
|
1105
|
+
discarded = true;
|
|
1106
|
+
},
|
|
1107
|
+
async restore(): Promise<CasRestoreResult> {
|
|
1108
|
+
return discarded ? { status: "discarded" } : { status: "not-restorable" };
|
|
1109
|
+
},
|
|
1110
|
+
};
|
|
1111
|
+
},
|
|
1112
|
+
});
|
|
396
1113
|
});
|
|
397
1114
|
});
|
|
398
1115
|
}
|
|
@@ -404,11 +1121,11 @@ export function applyAtomicYamlPatchesWithCurrent(
|
|
|
404
1121
|
*/
|
|
405
1122
|
export function enqueueAtomicYamlOperation<T>(
|
|
406
1123
|
configPath: string,
|
|
407
|
-
operation: (canonicalConfigPath: string) => Promise<T>,
|
|
1124
|
+
operation: (canonicalConfigPath: string, lexicalConfigPath: string) => Promise<T>,
|
|
408
1125
|
): Promise<T> {
|
|
409
1126
|
const canonicalPath = canonicalConfigPath(configPath);
|
|
410
1127
|
const prior = queues.get(canonicalPath) ?? Promise.resolve();
|
|
411
|
-
const result = prior.catch(() => undefined).then(() => operation(canonicalPath));
|
|
1128
|
+
const result = prior.catch(() => undefined).then(() => operation(canonicalPath, configPath));
|
|
412
1129
|
const completion = result.then(
|
|
413
1130
|
() => undefined,
|
|
414
1131
|
() => undefined,
|
|
@@ -429,14 +1146,27 @@ export function reserveAtomicYamlPatchSlot(
|
|
|
429
1146
|
patches: () => Promise<readonly AtomicYamlPatch[]> | readonly AtomicYamlPatch[],
|
|
430
1147
|
options: AtomicYamlPatchOptions = {},
|
|
431
1148
|
): Promise<CasReceipt> {
|
|
432
|
-
return enqueueAtomicYamlOperation(configPath, async canonicalPath => {
|
|
1149
|
+
return enqueueAtomicYamlOperation(configPath, async (canonicalPath, lexicalPath) => {
|
|
433
1150
|
const nextPatches = await patches();
|
|
434
1151
|
for (const patch of nextPatches) assertPatch(patch);
|
|
435
1152
|
await fs.mkdir(path.dirname(canonicalPath), { recursive: true, mode: 0o700 });
|
|
436
1153
|
return await withFileLock(canonicalPath, async () => {
|
|
437
|
-
const
|
|
1154
|
+
const state = await readYaml(canonicalPath);
|
|
1155
|
+
const { current, root } = state;
|
|
438
1156
|
await options.validateRoot?.(root, nextPatches);
|
|
439
|
-
return await applyPatchesUnderLock(
|
|
1157
|
+
return await applyPatchesUnderLock(
|
|
1158
|
+
canonicalPath,
|
|
1159
|
+
current,
|
|
1160
|
+
nextPatches,
|
|
1161
|
+
options,
|
|
1162
|
+
false,
|
|
1163
|
+
// Pass the read state as the CAS baseline (like the transaction
|
|
1164
|
+
// path): a symlink retarget after the publication can then restore
|
|
1165
|
+
// the old canonical content, and an external edit during the queue
|
|
1166
|
+
// wait is a CAS conflict instead of a clobber.
|
|
1167
|
+
state,
|
|
1168
|
+
lexicalPath,
|
|
1169
|
+
);
|
|
440
1170
|
});
|
|
441
1171
|
});
|
|
442
1172
|
}
|
|
@@ -450,14 +1180,24 @@ export function reserveAtomicYamlUpdateSlot<T>(
|
|
|
450
1180
|
update: () => Promise<AtomicYamlUpdate<T>> | AtomicYamlUpdate<T>,
|
|
451
1181
|
options: AtomicYamlPatchOptions = {},
|
|
452
1182
|
): Promise<T> {
|
|
453
|
-
return enqueueAtomicYamlOperation(configPath, async canonicalPath => {
|
|
1183
|
+
return enqueueAtomicYamlOperation(configPath, async (canonicalPath, lexicalPath) => {
|
|
454
1184
|
const atomicUpdate = await update();
|
|
455
1185
|
await fs.mkdir(path.dirname(canonicalPath), { recursive: true, mode: 0o700 });
|
|
456
1186
|
return await withFileLock(canonicalPath, async () => {
|
|
457
|
-
const
|
|
1187
|
+
const state = await readYaml(canonicalPath);
|
|
1188
|
+
const { current } = state;
|
|
458
1189
|
const result = await atomicUpdate.apply(current);
|
|
459
1190
|
if (atomicUpdate.shouldWrite?.(result) !== false) {
|
|
460
|
-
await writeAtomicYaml(
|
|
1191
|
+
await writeAtomicYaml(
|
|
1192
|
+
canonicalPath,
|
|
1193
|
+
current,
|
|
1194
|
+
options,
|
|
1195
|
+
// The read state as the CAS baseline: a symlink retarget after
|
|
1196
|
+
// the publication can restore the old canonical content, and
|
|
1197
|
+
// an external edit during the queue wait is a conflict.
|
|
1198
|
+
state,
|
|
1199
|
+
lexicalPath,
|
|
1200
|
+
);
|
|
461
1201
|
}
|
|
462
1202
|
await atomicUpdate.committed?.(current, result);
|
|
463
1203
|
return result;
|