@gajae-code/coding-agent 0.16.7 → 0.17.1
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 +180 -1
- package/dist/types/capability/fs.d.ts +37 -4
- package/dist/types/capability/index.d.ts +19 -0
- package/dist/types/capability/types.d.ts +9 -0
- package/dist/types/cli/doctor/args.d.ts +36 -0
- package/dist/types/cli/doctor/checks.d.ts +18 -0
- package/dist/types/cli/doctor/config-repairs.d.ts +38 -0
- package/dist/types/cli/doctor/context.d.ts +89 -0
- package/dist/types/cli/doctor/files.d.ts +27 -0
- package/dist/types/cli/doctor/ids.d.ts +20 -0
- package/dist/types/cli/doctor/install-repairs.d.ts +94 -0
- package/dist/types/cli/doctor/journal.d.ts +43 -0
- package/dist/types/cli/doctor/managed-link.d.ts +39 -0
- package/dist/types/cli/doctor/native-probe.d.ts +2 -0
- package/dist/types/cli/doctor/permission-repairs.d.ts +25 -0
- package/dist/types/cli/doctor/plugin-quarantine.d.ts +77 -0
- package/dist/types/cli/doctor/plugin-restore.d.ts +40 -0
- package/dist/types/cli/doctor/plugin-targets.d.ts +11 -0
- package/dist/types/cli/doctor/probe-client.d.ts +9 -0
- package/dist/types/cli/doctor/probe-types.d.ts +18 -0
- package/dist/types/cli/doctor/probe.d.ts +3 -0
- package/dist/types/cli/doctor/projection-probe.d.ts +2 -0
- package/dist/types/cli/doctor/repairs.d.ts +28 -0
- package/dist/types/cli/doctor/report.d.ts +27 -0
- package/dist/types/cli/doctor/runner.d.ts +4 -0
- package/dist/types/cli/doctor/safety.d.ts +10 -0
- package/dist/types/cli/doctor/service-repairs.d.ts +8 -0
- package/dist/types/cli/doctor/service-targets.d.ts +41 -0
- package/dist/types/cli/doctor/types.d.ts +145 -0
- package/dist/types/cli/doctor-cli.d.ts +7 -0
- package/dist/types/cli/doctor-supervisor.d.ts +121 -0
- package/dist/types/cli/doctor-worker.d.ts +29 -0
- package/dist/types/cli/install-activation.d.ts +158 -0
- package/dist/types/cli/macos-community-app.d.ts +41 -0
- package/dist/types/cli/mcp-cli.d.ts +0 -11
- package/dist/types/cli/update-cli.d.ts +41 -4
- package/dist/types/cli-main.d.ts +174 -0
- package/dist/types/cli.d.ts +1 -171
- package/dist/types/commands/doctor.d.ts +7 -0
- package/dist/types/commands/sdk.d.ts +17 -8
- package/dist/types/config/autorouting-tier-map.d.ts +19 -0
- package/dist/types/config/file-lock.d.ts +27 -3
- package/dist/types/config/model-preset-registry.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +12 -5
- package/dist/types/config/model-resolver.d.ts +16 -3
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-gate-codec.d.ts +1 -1
- package/dist/types/coordinator-mcp/session-reaper.d.ts +15 -5
- package/dist/types/cursor.d.ts +9 -9
- package/dist/types/discovery/agents-md.d.ts +6 -1
- package/dist/types/discovery/helpers.d.ts +36 -7
- package/dist/types/discovery/index.d.ts +1 -1
- package/dist/types/discovery/mcp-json.d.ts +2 -0
- package/dist/types/extensibility/extensions/loader.d.ts +22 -1
- package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +14 -1
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -1
- package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
- package/dist/types/extensibility/plugins/manager.d.ts +33 -4
- package/dist/types/extensibility/plugins/marketplace/cache.d.ts +6 -0
- package/dist/types/extensibility/plugins/marketplace/manager.d.ts +186 -0
- package/dist/types/extensibility/plugins/marketplace/registry.d.ts +27 -0
- package/dist/types/extensibility/plugins/marketplace/source-resolver.d.ts +16 -1
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +16 -3
- package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +6 -0
- package/dist/types/gjc-runtime/launch-worktree.d.ts +53 -0
- package/dist/types/gjc-runtime/session-layout.d.ts +8 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +15 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +6 -0
- package/dist/types/main.d.ts +19 -1
- package/dist/types/modes/acp/acp-agent.d.ts +40 -0
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/runtime-mcp/config-writer.d.ts +47 -0
- package/dist/types/runtime-mcp/policy-json-edit.d.ts +60 -0
- package/dist/types/runtime-mcp/startup-policy.d.ts +4 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -2
- package/dist/types/sdk/acp/adapter.d.ts +5 -1
- package/dist/types/sdk/broker/broker.d.ts +51 -0
- package/dist/types/sdk/broker/daemon-entry.d.ts +39 -0
- package/dist/types/sdk/broker/discovery.d.ts +38 -0
- package/dist/types/sdk/broker/doctor-restart.d.ts +79 -0
- package/dist/types/sdk/broker/endpoint-authority.d.ts +7 -0
- package/dist/types/sdk/broker/ensure.d.ts +14 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
- package/dist/types/sdk/broker/process-incarnation.d.ts +50 -0
- package/dist/types/sdk/broker/runtime.d.ts +26 -0
- package/dist/types/sdk/broker/session-index.d.ts +13 -4
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +14 -0
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +16 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +101 -6
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +16 -0
- package/dist/types/sdk/bus/daemon-startup-exclusion.d.ts +43 -0
- package/dist/types/sdk/bus/doctor-daemon-restart-client.d.ts +51 -0
- package/dist/types/sdk/bus/doctor-daemon-restart.d.ts +39 -0
- package/dist/types/sdk/bus/index.d.ts +2 -0
- package/dist/types/sdk/bus/notification-service.d.ts +3 -0
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +11 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -0
- package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +1 -0
- package/dist/types/sdk/bus/telegram-daemon.d.ts +36 -5
- package/dist/types/sdk/cli/session-cli.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +4 -0
- package/dist/types/sdk/host/surface-policy.d.ts +3 -1
- package/dist/types/sdk/lifecycle/service.d.ts +44 -10
- package/dist/types/sdk/prompt-deadline-lease.d.ts +6 -3
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -2
- package/dist/types/sdk/prompt-failure.d.ts +47 -0
- package/dist/types/sdk/prompt-status.d.ts +19 -0
- package/dist/types/sdk/router/session-router.d.ts +3 -4
- package/dist/types/sdk/service-artifact-paths.d.ts +16 -0
- package/dist/types/sdk/session-authority.d.ts +9 -0
- package/dist/types/sdk/session.d.ts +3 -1
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/blob-store.d.ts +1 -1
- package/dist/types/session/fallback-chain-controller.d.ts +3 -6
- package/dist/types/session/internal/managed-session-scope.d.ts +44 -0
- package/dist/types/session/primary-control-surface.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +6 -0
- package/dist/types/tools/ask.d.ts +25 -0
- package/dist/types/tools/image-gen.d.ts +0 -4
- package/dist/types/tools/skill-discovery.d.ts +7 -5
- package/dist/types/tools/ultragoal-ask-guard.d.ts +7 -4
- package/dist/types/utils/changelog.d.ts +0 -9
- package/dist/types/utils/mupdf-wasm.d.ts +1 -0
- package/package.json +9 -8
- package/scripts/compile-args.ts +8 -4
- package/src/capability/fs.ts +395 -17
- package/src/capability/index.ts +273 -10
- package/src/capability/types.ts +9 -0
- package/src/cli/customize-doctor.ts +18 -12
- package/src/cli/doctor/args.ts +236 -0
- package/src/cli/doctor/checks.ts +431 -0
- package/src/cli/doctor/config-repairs.ts +517 -0
- package/src/cli/doctor/context.ts +149 -0
- package/src/cli/doctor/files.ts +149 -0
- package/src/cli/doctor/ids.ts +115 -0
- package/src/cli/doctor/install-repairs.ts +335 -0
- package/src/cli/doctor/journal.ts +423 -0
- package/src/cli/doctor/managed-link.ts +514 -0
- package/src/cli/doctor/native-probe.ts +13 -0
- package/src/cli/doctor/permission-repairs.ts +242 -0
- package/src/cli/doctor/plugin-quarantine.ts +377 -0
- package/src/cli/doctor/plugin-restore.ts +59 -0
- package/src/cli/doctor/plugin-targets.ts +373 -0
- package/src/cli/doctor/probe-client.ts +195 -0
- package/src/cli/doctor/probe-types.ts +21 -0
- package/src/cli/doctor/probe.ts +42 -0
- package/src/cli/doctor/projection-probe.ts +249 -0
- package/src/cli/doctor/repairs.ts +1157 -0
- package/src/cli/doctor/report.ts +176 -0
- package/src/cli/doctor/runner.ts +342 -0
- package/src/cli/doctor/safety.ts +40 -0
- package/src/cli/doctor/service-repairs.ts +452 -0
- package/src/cli/doctor/service-targets.ts +274 -0
- package/src/cli/doctor/types.ts +164 -0
- package/src/cli/doctor-cli.ts +62 -0
- package/src/cli/doctor-supervisor.ts +571 -0
- package/src/cli/doctor-worker.ts +181 -0
- package/src/cli/install-activation.ts +666 -0
- package/src/cli/macos-community-app.ts +1449 -0
- package/src/cli/mcp-cli.ts +11 -21
- package/src/cli/nofile-limit.ts +5 -1
- package/src/cli/plugin-cli.ts +6 -1
- package/src/cli/update-cli.ts +362 -264
- package/src/cli-main.ts +517 -0
- package/src/cli.ts +64 -433
- package/src/commands/autoresearch.ts +11 -5
- package/src/commands/completion.ts +1 -1
- package/src/commands/doctor.ts +13 -0
- package/src/commands/sdk.ts +58 -10
- package/src/commands/ultragoal.ts +11 -5
- package/src/config/autorouting-tier-map.ts +7 -0
- package/src/config/file-lock.ts +736 -71
- package/src/config/mcp-schema.json +3 -3
- package/src/config/model-preset-registry.ts +1 -0
- package/src/config/model-registry.ts +80 -27
- package/src/config/model-resolver.ts +33 -4
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +6 -2
- package/src/coordinator-mcp/question-gate-codec.ts +1 -0
- package/src/coordinator-mcp/server.ts +534 -51
- package/src/coordinator-mcp/session-reaper.ts +64 -8
- package/src/cursor.ts +223 -53
- package/src/discovery/agents-md.ts +74 -8
- package/src/discovery/agents.ts +32 -7
- package/src/discovery/builtin.ts +184 -75
- package/src/discovery/claude-plugins.ts +85 -17
- package/src/discovery/cline.ts +38 -10
- package/src/discovery/cursor.ts +19 -6
- package/src/discovery/gemini.ts +62 -22
- package/src/discovery/github.ts +13 -3
- package/src/discovery/helpers.ts +483 -89
- package/src/discovery/index.ts +1 -0
- package/src/discovery/mcp-json.ts +36 -10
- package/src/discovery/opencode.ts +36 -19
- package/src/discovery/ssh.ts +11 -8
- package/src/discovery/windsurf.ts +12 -3
- package/src/exec/bash-executor.ts +10 -3
- package/src/exec/isolated-shell.ts +15 -2
- package/src/extensibility/extensions/loader.ts +88 -26
- package/src/extensibility/gjc-plugins/lifecycle.ts +189 -0
- package/src/extensibility/gjc-plugins/paths.ts +4 -1
- package/src/extensibility/gjc-plugins/types.ts +27 -0
- package/src/extensibility/plugins/manager.ts +200 -22
- package/src/extensibility/plugins/marketplace/cache.ts +55 -2
- package/src/extensibility/plugins/marketplace/manager.ts +1262 -162
- package/src/extensibility/plugins/marketplace/registry.ts +153 -0
- package/src/extensibility/plugins/marketplace/source-resolver.ts +116 -10
- package/src/extensibility/runtime-skill-discovery.ts +61 -9
- package/src/extensibility/skill-management.ts +7 -1
- package/src/gjc-runtime/autoresearch-runtime.ts +10 -1
- package/src/gjc-runtime/launch-worktree.test.ts +248 -0
- package/src/gjc-runtime/launch-worktree.ts +275 -38
- package/src/gjc-runtime/repository-binding.ts +26 -1
- package/src/gjc-runtime/session-layout.ts +10 -0
- package/src/gjc-runtime/session-state-lock.ts +330 -23
- package/src/gjc-runtime/session-state-sidecar.ts +328 -80
- package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
- package/src/harness-control-plane/session-lease.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +28 -26
- package/src/main.ts +150 -29
- package/src/memories/index.ts +24 -2
- package/src/modes/acp/acp-agent.ts +522 -68
- package/src/modes/components/agent-dashboard.ts +3 -1
- package/src/modes/components/model-selector.ts +28 -19
- package/src/modes/components/plugin-settings.ts +2 -1
- package/src/modes/controllers/selector-controller.ts +15 -9
- package/src/modes/interactive-mode.ts +21 -3
- package/src/prompts/system/system-prompt.md +5 -1
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/move-session.md +4 -0
- package/src/prompts/tools/read.md +2 -1
- package/src/prompts/tools/skill-discovery.md +6 -5
- package/src/prompts/tools/task.md +1 -0
- package/src/runtime-mcp/config-writer.ts +228 -0
- package/src/runtime-mcp/oauth-discovery.ts +114 -14
- package/src/runtime-mcp/policy-json-edit.ts +129 -0
- package/src/runtime-mcp/startup-policy.ts +14 -0
- package/src/runtime-mcp/types.ts +2 -2
- package/src/sdk/acp/adapter.ts +27 -12
- package/src/sdk/broker/broker.ts +273 -44
- package/src/sdk/broker/daemon-entry.ts +119 -0
- package/src/sdk/broker/discovery.ts +173 -20
- package/src/sdk/broker/doctor-restart.ts +206 -0
- package/src/sdk/broker/endpoint-authority.ts +36 -0
- package/src/sdk/broker/ensure.ts +49 -14
- package/src/sdk/broker/lifecycle.ts +174 -42
- package/src/sdk/broker/process-incarnation.ts +86 -2
- package/src/sdk/broker/runtime.ts +145 -21
- package/src/sdk/broker/session-index.ts +136 -22
- package/src/sdk/broker/spawn-substrate.ts +4 -1
- package/src/sdk/broker/transport.ts +5 -0
- package/src/sdk/broker/worktree-occupancy.ts +21 -1
- package/src/sdk/bus/chat-daemon-cli.ts +63 -0
- package/src/sdk/bus/chat-daemon-control.ts +260 -64
- package/src/sdk/bus/chat-daemon-runtime.ts +141 -25
- package/src/sdk/bus/daemon-startup-exclusion.ts +119 -0
- package/src/sdk/bus/doctor-daemon-restart-client.ts +291 -0
- package/src/sdk/bus/doctor-daemon-restart.ts +92 -0
- package/src/sdk/bus/index.ts +252 -51
- package/src/sdk/bus/kind-aware-reconciliation.ts +76 -41
- package/src/sdk/bus/notification-service.ts +21 -3
- package/src/sdk/bus/reconciliation-store.ts +17 -11
- package/src/sdk/bus/telegram-daemon-cli.ts +91 -5
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/bus/telegram-daemon-control.ts +47 -0
- package/src/sdk/bus/telegram-daemon-owner-registry.ts +1 -1
- package/src/sdk/bus/telegram-daemon.ts +178 -13
- package/src/sdk/cli/session-cli.ts +100 -12
- package/src/sdk/host/host.ts +2 -7
- package/src/sdk/host/session-runtime.test.ts +329 -8
- package/src/sdk/host/session-runtime.ts +202 -50
- package/src/sdk/host/surface-policy.ts +8 -1
- package/src/sdk/lifecycle/service.ts +199 -22
- package/src/sdk/mcp/server.ts +1 -1
- package/src/sdk/prompt-deadline-lease.ts +12 -4
- package/src/sdk/prompt-deadline-manager.ts +5 -7
- package/src/sdk/prompt-failure.ts +148 -0
- package/src/sdk/prompt-status.ts +30 -1
- package/src/sdk/prompt-watchdog.ts +11 -3
- package/src/sdk/protocol/operation-inventory.generated.json +18 -0
- package/src/sdk/protocol/operation-registry.ts +20 -8
- package/src/sdk/router/session-router.ts +8 -3
- package/src/sdk/service-artifact-paths.ts +26 -0
- package/src/sdk/session-authority.ts +10 -0
- package/src/sdk/session.ts +361 -194
- package/src/session/agent-session.ts +398 -93
- package/src/session/blob-store.ts +9 -9
- package/src/session/fallback-chain-controller.ts +3 -7
- package/src/session/internal/managed-session-scope.ts +445 -29
- package/src/session/internal/managed-session-storage.ts +38 -6
- package/src/session/primary-control-surface.ts +12 -0
- package/src/session/session-manager.ts +11 -3
- package/src/session/session-storage.ts +1 -1
- package/src/session-import/codex.ts +6 -1
- package/src/session-import/redact.ts +7 -0
- package/src/skill-state/workflow-mutation-guard.ts +645 -39
- package/src/slash-commands/builtin-registry.ts +7 -3
- package/src/system-prompt.ts +8 -0
- package/src/task/executor.ts +36 -3
- package/src/task/types.ts +27 -5
- package/src/tools/ask.ts +114 -34
- package/src/tools/browser/actions.ts +2 -2
- package/src/tools/browser/tab-worker.ts +11 -8
- package/src/tools/descriptors.ts +6 -1
- package/src/tools/fetch.ts +7 -2
- package/src/tools/image-gen.ts +33 -34
- package/src/tools/read.ts +78 -32
- package/src/tools/skill-discovery.ts +17 -7
- package/src/tools/tool-catalog.generated.ts +8 -8
- package/src/tools/ultragoal-ask-guard.ts +16 -26
- package/src/utils/changelog.ts +7 -1
- package/src/utils/git.ts +3 -1
- package/src/utils/markit.ts +24 -2
- package/src/utils/mupdf-wasm.ts +26 -0
- package/src/web/search/providers/openai-compatible.ts +191 -14
|
@@ -14,6 +14,7 @@ function nativeChatDaemon(): NativeChatDaemonBindings {
|
|
|
14
14
|
return nativeChatDaemonBindings;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
import { isEnoent } from "@gajae-code/utils/fs-error";
|
|
17
18
|
import type { Settings } from "../../config/settings";
|
|
18
19
|
import type {
|
|
19
20
|
BuiltInDaemonController,
|
|
@@ -25,7 +26,10 @@ import type {
|
|
|
25
26
|
} from "../../daemon/control-types";
|
|
26
27
|
import { resolveGjcRuntimeSpawnInfo } from "../../daemon/runtime";
|
|
27
28
|
import { isProcessIncarnation, processIncarnation } from "../broker/process-incarnation";
|
|
29
|
+
import { CHAT_DAEMON_DIRECTORY, CHAT_DAEMON_FILES, canonicalServiceRootDigest } from "../service-artifact-paths";
|
|
28
30
|
import { getNotificationConfig, isDiscordComplete, isProviderEffectivelyEnabled, isSlackComplete } from "./config";
|
|
31
|
+
import { withDaemonStartupExclusion } from "./daemon-startup-exclusion";
|
|
32
|
+
import { type DoctorDaemonControlRequest, isDoctorDaemonControlRequest } from "./doctor-daemon-restart";
|
|
29
33
|
|
|
30
34
|
export type ChatDaemonKind = "discord" | "slack";
|
|
31
35
|
export type ChatDaemonAction = "stop" | "reload";
|
|
@@ -137,8 +141,8 @@ export type ChatDaemonAction = "stop" | "reload";
|
|
|
137
141
|
* crashes, recovery, and attachment retirement cannot replay ambiguous work.
|
|
138
142
|
*/
|
|
139
143
|
export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
|
|
140
|
-
discord:
|
|
141
|
-
slack:
|
|
144
|
+
discord: 76,
|
|
145
|
+
slack: 83,
|
|
142
146
|
};
|
|
143
147
|
|
|
144
148
|
export function chatDaemonGeneration(kind: ChatDaemonKind): number {
|
|
@@ -157,6 +161,14 @@ export interface ChatDaemonState {
|
|
|
157
161
|
transportHealthy: boolean;
|
|
158
162
|
generation: number;
|
|
159
163
|
stoppedAt?: number;
|
|
164
|
+
/**
|
|
165
|
+
* Digest of the canonical (symlink-resolved) agent root this record was
|
|
166
|
+
* published for. Legacy records omit it and remain non-authorizing for the
|
|
167
|
+
* D9 cross-bind check: a state record without a matching rootDigest can
|
|
168
|
+
* never be treated as this root's live owner for that check, even though it
|
|
169
|
+
* is still valid for every other pre-existing owner check in this module.
|
|
170
|
+
*/
|
|
171
|
+
rootDigest?: string;
|
|
160
172
|
}
|
|
161
173
|
|
|
162
174
|
/**
|
|
@@ -326,13 +338,23 @@ const DEFAULT_KILL_TIMEOUT_MS = 3_000;
|
|
|
326
338
|
/** Covers Discord READY plus its first 5-second heartbeat; tests inject a smaller timeout. */
|
|
327
339
|
const DEFAULT_SPAWN_READY_TIMEOUT_MS = 8_000;
|
|
328
340
|
|
|
329
|
-
|
|
341
|
+
/**
|
|
342
|
+
* `version`/`ownerId`/`rootDigest` are the D9 cross-bind receipt delta: a
|
|
343
|
+
* legacy lock predating this generation omits them. Legacy records remain
|
|
344
|
+
* valid, signalable owner locks for every existing check in this module —
|
|
345
|
+
* they are simply non-authorizing for a D9 cross-bind check that requires a
|
|
346
|
+
* matching `ownerId`/`rootDigest` against the sibling state record.
|
|
347
|
+
*/
|
|
348
|
+
export interface ChatDaemonOwnerLock {
|
|
349
|
+
version?: 1;
|
|
330
350
|
pid: number;
|
|
331
351
|
incarnation: string;
|
|
332
352
|
createdAt: number;
|
|
353
|
+
ownerId?: string;
|
|
354
|
+
rootDigest?: string;
|
|
333
355
|
}
|
|
334
356
|
|
|
335
|
-
interface ChatDaemonOwnerLockLease {
|
|
357
|
+
export interface ChatDaemonOwnerLockLease {
|
|
336
358
|
content: string;
|
|
337
359
|
dev: bigint;
|
|
338
360
|
ino: bigint;
|
|
@@ -344,6 +366,19 @@ interface ChatDaemonOwnerLockLease {
|
|
|
344
366
|
sha256: string;
|
|
345
367
|
}
|
|
346
368
|
|
|
369
|
+
/** Result of a bounded, no-follow owner-lock lease capture. Only `"absent"` (ENOENT) means the lock currently does not exist; every other failure is `"unreadable"` and must never be treated as absence. */
|
|
370
|
+
export type ChatDaemonOwnerLockLeaseResult =
|
|
371
|
+
| { status: "present"; lease: ChatDaemonOwnerLockLease }
|
|
372
|
+
| { status: "absent" }
|
|
373
|
+
| { status: "unreadable" };
|
|
374
|
+
|
|
375
|
+
const CHAT_DAEMON_OWNER_LOCK_LEASE_MAX_BYTES = 64 * 1024;
|
|
376
|
+
|
|
377
|
+
/** Matches the canonical no-follow, non-blocking lock-info open flags used by config/file-lock.ts. */
|
|
378
|
+
const CHAT_DAEMON_OWNER_LOCK_OPEN_FLAGS =
|
|
379
|
+
fs.constants.O_RDONLY |
|
|
380
|
+
(process.platform === "win32" ? 0 : (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0));
|
|
381
|
+
|
|
347
382
|
interface ChatDaemonOwnershipProbe {
|
|
348
383
|
pidAlive(pid: number): boolean;
|
|
349
384
|
pidIncarnation(pid: number): string | undefined;
|
|
@@ -353,15 +388,56 @@ export function chatDaemonPaths(
|
|
|
353
388
|
agentDir: string,
|
|
354
389
|
kind: ChatDaemonKind,
|
|
355
390
|
): { dir: string; lock: string; state: string; control: string } {
|
|
356
|
-
const dir = path.join(agentDir,
|
|
391
|
+
const dir = path.join(agentDir, CHAT_DAEMON_DIRECTORY, kind);
|
|
357
392
|
return {
|
|
358
393
|
dir,
|
|
359
|
-
lock: path.join(dir,
|
|
360
|
-
state: path.join(dir,
|
|
361
|
-
control: path.join(dir,
|
|
394
|
+
lock: path.join(dir, CHAT_DAEMON_FILES.ownerLock),
|
|
395
|
+
state: path.join(dir, CHAT_DAEMON_FILES.state),
|
|
396
|
+
control: path.join(dir, CHAT_DAEMON_FILES.control),
|
|
362
397
|
};
|
|
363
398
|
}
|
|
364
399
|
|
|
400
|
+
export function chatDoctorControlRequestPath(agentDir: string, kind: ChatDaemonKind): string {
|
|
401
|
+
return path.join(chatDaemonPaths(agentDir, kind).dir, "doctor-restart.control.json");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export async function readChatDoctorControlRequest(
|
|
405
|
+
agentDir: string,
|
|
406
|
+
kind: ChatDaemonKind,
|
|
407
|
+
): Promise<DoctorDaemonControlRequest | undefined> {
|
|
408
|
+
try {
|
|
409
|
+
const parsed = JSON.parse(
|
|
410
|
+
await fs.promises.readFile(chatDoctorControlRequestPath(agentDir, kind), "utf8"),
|
|
411
|
+
) as unknown;
|
|
412
|
+
return isDoctorDaemonControlRequest(parsed) && parsed.owner === kind ? parsed : undefined;
|
|
413
|
+
} catch {
|
|
414
|
+
return undefined;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export async function writeChatDoctorControlRequest(
|
|
419
|
+
agentDir: string,
|
|
420
|
+
kind: ChatDaemonKind,
|
|
421
|
+
request: DoctorDaemonControlRequest,
|
|
422
|
+
): Promise<void> {
|
|
423
|
+
if (request.owner !== kind) throw new Error(`${kind} doctor request owner mismatch`);
|
|
424
|
+
const file = chatDoctorControlRequestPath(agentDir, kind);
|
|
425
|
+
await fs.promises.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
426
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
427
|
+
await fs.promises.writeFile(tmp, `${JSON.stringify(request)}\n`, { mode: 0o600 });
|
|
428
|
+
await fs.promises.rename(tmp, file);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export async function clearChatDoctorControlRequest(
|
|
432
|
+
agentDir: string,
|
|
433
|
+
kind: ChatDaemonKind,
|
|
434
|
+
requestId?: string,
|
|
435
|
+
): Promise<void> {
|
|
436
|
+
const file = chatDoctorControlRequestPath(agentDir, kind);
|
|
437
|
+
if (requestId && (await readChatDoctorControlRequest(agentDir, kind))?.requestId !== requestId) return;
|
|
438
|
+
await fs.promises.unlink(file).catch(() => undefined);
|
|
439
|
+
}
|
|
440
|
+
|
|
365
441
|
/**
|
|
366
442
|
* Configuration fingerprint that identifies which settings a daemon owner was
|
|
367
443
|
* started for. `undefined` means the current settings cannot configure that
|
|
@@ -951,7 +1027,7 @@ export async function ensureSlackDaemon(
|
|
|
951
1027
|
return await ensureChatDaemon("slack", settings, deps);
|
|
952
1028
|
}
|
|
953
1029
|
|
|
954
|
-
export
|
|
1030
|
+
export interface AcquireChatDaemonOwnershipInput {
|
|
955
1031
|
agentDir: string;
|
|
956
1032
|
kind: ChatDaemonKind;
|
|
957
1033
|
ownerId: string;
|
|
@@ -960,7 +1036,16 @@ export async function acquireChatDaemonOwnership(input: {
|
|
|
960
1036
|
incarnation?: string;
|
|
961
1037
|
pidAlive?: (pid: number) => boolean;
|
|
962
1038
|
pidIncarnation?: (pid: number) => string | undefined;
|
|
963
|
-
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* Actual publication body. Runs ONLY inside {@link withDaemonStartupExclusion}
|
|
1043
|
+
* (see {@link acquireChatDaemonOwnership}); never call this directly, and
|
|
1044
|
+
* never call it from inside another already-held startup-exclusion guard for
|
|
1045
|
+
* the same `(agentDir, kind)` — that would be a self-deadlocking reacquisition
|
|
1046
|
+
* of a non-reentrant lock.
|
|
1047
|
+
*/
|
|
1048
|
+
async function acquireChatDaemonOwnershipLocked(input: AcquireChatDaemonOwnershipInput): Promise<boolean> {
|
|
964
1049
|
const paths = chatDaemonPaths(input.agentDir, input.kind);
|
|
965
1050
|
const pid = input.pid ?? process.pid;
|
|
966
1051
|
const probe: ChatDaemonOwnershipProbe = {
|
|
@@ -983,7 +1068,17 @@ export async function acquireChatDaemonOwnership(input: {
|
|
|
983
1068
|
)
|
|
984
1069
|
return false;
|
|
985
1070
|
}
|
|
986
|
-
|
|
1071
|
+
// Existing root: withDaemonStartupExclusion has already acquired its guard
|
|
1072
|
+
// under this same agentDir, so the directory chain up to it exists.
|
|
1073
|
+
const rootDigest = await canonicalServiceRootDigest(input.agentDir);
|
|
1074
|
+
const owner: ChatDaemonOwnerLock = {
|
|
1075
|
+
version: 1,
|
|
1076
|
+
pid,
|
|
1077
|
+
incarnation,
|
|
1078
|
+
createdAt: Date.now(),
|
|
1079
|
+
ownerId: input.ownerId,
|
|
1080
|
+
rootDigest,
|
|
1081
|
+
};
|
|
987
1082
|
let lock = await createChatDaemonOwnerLock(paths.lock, owner);
|
|
988
1083
|
if (!lock) {
|
|
989
1084
|
if (!(await reclaimChatDaemonOwnerLock(paths.lock, paths.state, probe))) return false;
|
|
@@ -1003,11 +1098,26 @@ export async function acquireChatDaemonOwnership(input: {
|
|
|
1003
1098
|
heartbeatAt: Date.now(),
|
|
1004
1099
|
transportHealthy: false,
|
|
1005
1100
|
generation: chatDaemonGeneration(input.kind),
|
|
1101
|
+
rootDigest,
|
|
1006
1102
|
} satisfies ChatDaemonState);
|
|
1007
1103
|
return true;
|
|
1008
1104
|
});
|
|
1009
1105
|
}
|
|
1010
1106
|
|
|
1107
|
+
/**
|
|
1108
|
+
* Publishes this process as the `(agentDir, kind)` chat daemon owner
|
|
1109
|
+
* (owner.lock + state.json). Runs behind the shared cross-process startup
|
|
1110
|
+
* exclusion so it can never race a doctor startup/maintenance repair that
|
|
1111
|
+
* holds the same guard (e.g. detaching a stale owner-lock quarantine) — the
|
|
1112
|
+
* guard is acquired here and released before returning; the actual
|
|
1113
|
+
* lock-creation/state-publication logic lives in the unlocked
|
|
1114
|
+
* {@link acquireChatDaemonOwnershipLocked} so the guard is never held across
|
|
1115
|
+
* a nested reacquisition of itself.
|
|
1116
|
+
*/
|
|
1117
|
+
export async function acquireChatDaemonOwnership(input: AcquireChatDaemonOwnershipInput): Promise<boolean> {
|
|
1118
|
+
return await withDaemonStartupExclusion(input.agentDir, input.kind, () => acquireChatDaemonOwnershipLocked(input));
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1011
1121
|
async function createChatDaemonOwnerLock(
|
|
1012
1122
|
lock: string,
|
|
1013
1123
|
owner: ChatDaemonOwnerLock,
|
|
@@ -1029,44 +1139,79 @@ async function createChatDaemonOwnerLock(
|
|
|
1029
1139
|
throw error;
|
|
1030
1140
|
}
|
|
1031
1141
|
await fs.promises.unlink(temporary);
|
|
1032
|
-
return await
|
|
1142
|
+
return await captureChatDaemonOwnerLockLeaseOrUndefined(lock);
|
|
1033
1143
|
} finally {
|
|
1034
1144
|
await fs.promises.unlink(temporary).catch(() => undefined);
|
|
1035
1145
|
}
|
|
1036
1146
|
}
|
|
1037
1147
|
|
|
1038
|
-
|
|
1148
|
+
/**
|
|
1149
|
+
* Bounded, no-follow, non-blocking owner-lock lease capture. Opens with the
|
|
1150
|
+
* same `O_NOFOLLOW|O_NONBLOCK` posture as the canonical lock-info reader in
|
|
1151
|
+
* config/file-lock.ts, reads at most {@link CHAT_DAEMON_OWNER_LOCK_LEASE_MAX_BYTES}
|
|
1152
|
+
* bytes through the open descriptor (never an unbounded `readFile`), and
|
|
1153
|
+
* revalidates identity/parent-directory metadata before and after the read so a
|
|
1154
|
+
* TOCTOU replacement during the read is detected rather than silently trusted.
|
|
1155
|
+
*
|
|
1156
|
+
* Only `ENOENT` on the initial open means the lock is currently absent.
|
|
1157
|
+
* SYMLINK, FIFO/special-file, oversize, and any other I/O failure are all
|
|
1158
|
+
* `"unreadable"` — distinct from absence, and callers must not treat them as
|
|
1159
|
+
* "no lock exists".
|
|
1160
|
+
*/
|
|
1161
|
+
export async function captureChatDaemonOwnerLockLease(lock: string): Promise<ChatDaemonOwnerLockLeaseResult> {
|
|
1162
|
+
let handle: fs.promises.FileHandle | undefined;
|
|
1039
1163
|
try {
|
|
1040
|
-
const handle = await fs.promises.open(lock, "r");
|
|
1041
1164
|
try {
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
if (
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1165
|
+
handle = await fs.promises.open(lock, CHAT_DAEMON_OWNER_LOCK_OPEN_FLAGS);
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
if (isEnoent(error)) return { status: "absent" };
|
|
1168
|
+
return { status: "unreadable" };
|
|
1169
|
+
}
|
|
1170
|
+
const before = await handle.stat({ bigint: true });
|
|
1171
|
+
const parentBefore = await fs.promises.lstat(path.dirname(lock), { bigint: true });
|
|
1172
|
+
if (!before.isFile() || before.size > BigInt(CHAT_DAEMON_OWNER_LOCK_LEASE_MAX_BYTES))
|
|
1173
|
+
return { status: "unreadable" };
|
|
1174
|
+
const buffer = Buffer.alloc(CHAT_DAEMON_OWNER_LOCK_LEASE_MAX_BYTES);
|
|
1175
|
+
let length = 0;
|
|
1176
|
+
while (length < buffer.length) {
|
|
1177
|
+
const read = await handle.read(buffer, length, buffer.length - length, length);
|
|
1178
|
+
if (read.bytesRead === 0) break;
|
|
1179
|
+
length += read.bytesRead;
|
|
1180
|
+
}
|
|
1181
|
+
const content = buffer.subarray(0, length).toString("utf8");
|
|
1182
|
+
const after = await handle.stat({ bigint: true });
|
|
1183
|
+
let pathname: fs.BigIntStats;
|
|
1184
|
+
let parentAfter: fs.BigIntStats;
|
|
1185
|
+
try {
|
|
1186
|
+
pathname = await fs.promises.lstat(lock, { bigint: true });
|
|
1187
|
+
parentAfter = await fs.promises.lstat(path.dirname(lock), { bigint: true });
|
|
1188
|
+
} catch {
|
|
1189
|
+
return { status: "unreadable" };
|
|
1190
|
+
}
|
|
1191
|
+
if (
|
|
1192
|
+
!after.isFile() ||
|
|
1193
|
+
!pathname.isFile() ||
|
|
1194
|
+
pathname.isSymbolicLink() ||
|
|
1195
|
+
before.nlink !== 1n ||
|
|
1196
|
+
pathname.nlink !== 1n ||
|
|
1197
|
+
!parentBefore.isDirectory() ||
|
|
1198
|
+
parentBefore.isSymbolicLink() ||
|
|
1199
|
+
parentBefore.dev !== parentAfter.dev ||
|
|
1200
|
+
parentBefore.ino !== parentAfter.ino ||
|
|
1201
|
+
before.dev !== after.dev ||
|
|
1202
|
+
before.ino !== after.ino ||
|
|
1203
|
+
before.size !== after.size ||
|
|
1204
|
+
before.mtimeNs !== after.mtimeNs ||
|
|
1205
|
+
before.dev !== pathname.dev ||
|
|
1206
|
+
before.ino !== pathname.ino ||
|
|
1207
|
+
before.size !== pathname.size ||
|
|
1208
|
+
before.mtimeNs !== pathname.mtimeNs ||
|
|
1209
|
+
after.size > BigInt(CHAT_DAEMON_OWNER_LOCK_LEASE_MAX_BYTES)
|
|
1210
|
+
)
|
|
1211
|
+
return { status: "unreadable" };
|
|
1212
|
+
return {
|
|
1213
|
+
status: "present",
|
|
1214
|
+
lease: {
|
|
1070
1215
|
content,
|
|
1071
1216
|
dev: before.dev,
|
|
1072
1217
|
ino: before.ino,
|
|
@@ -1076,17 +1221,23 @@ async function captureChatDaemonOwnerLockLease(lock: string): Promise<ChatDaemon
|
|
|
1076
1221
|
parentDev: parentBefore.dev,
|
|
1077
1222
|
parentIno: parentBefore.ino,
|
|
1078
1223
|
sha256: crypto.createHash("sha256").update(content).digest("hex"),
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
await handle.close();
|
|
1082
|
-
}
|
|
1224
|
+
},
|
|
1225
|
+
};
|
|
1083
1226
|
} catch {
|
|
1084
|
-
return
|
|
1227
|
+
return { status: "unreadable" };
|
|
1228
|
+
} finally {
|
|
1229
|
+
await handle?.close();
|
|
1085
1230
|
}
|
|
1086
1231
|
}
|
|
1087
1232
|
|
|
1233
|
+
/** Convenience wrapper for existing call sites that only need the lease or `undefined` (absent or unreadable alike). */
|
|
1234
|
+
async function captureChatDaemonOwnerLockLeaseOrUndefined(lock: string): Promise<ChatDaemonOwnerLockLease | undefined> {
|
|
1235
|
+
const result = await captureChatDaemonOwnerLockLease(lock);
|
|
1236
|
+
return result.status === "present" ? result.lease : undefined;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1088
1239
|
async function ownsChatDaemonOwnerLock(lock: string, lease: ChatDaemonOwnerLockLease): Promise<boolean> {
|
|
1089
|
-
const current = await
|
|
1240
|
+
const current = await captureChatDaemonOwnerLockLeaseOrUndefined(lock);
|
|
1090
1241
|
return (
|
|
1091
1242
|
current?.dev === lease.dev &&
|
|
1092
1243
|
current.ino === lease.ino &&
|
|
@@ -1162,7 +1313,7 @@ async function staleChatDaemonLockLease(
|
|
|
1162
1313
|
lock: string,
|
|
1163
1314
|
probe: ChatDaemonOwnershipProbe,
|
|
1164
1315
|
): Promise<ChatDaemonOwnerLockLease | undefined> {
|
|
1165
|
-
const lease = await
|
|
1316
|
+
const lease = await captureChatDaemonOwnerLockLeaseOrUndefined(lock);
|
|
1166
1317
|
if (!lease) return undefined;
|
|
1167
1318
|
let owner: unknown;
|
|
1168
1319
|
try {
|
|
@@ -1197,22 +1348,39 @@ async function canReclaimChatDaemonOwnerLock(
|
|
|
1197
1348
|
return await staleChatDaemonLockLease(lock, probe);
|
|
1198
1349
|
}
|
|
1199
1350
|
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1351
|
+
/**
|
|
1352
|
+
* Strict: `pid`/`incarnation`/`createdAt` are always required. `version`,
|
|
1353
|
+
* `ownerId`, and `rootDigest` are individually optional (legacy locks omit
|
|
1354
|
+
* them), but any one that IS present must be well-formed — a malformed
|
|
1355
|
+
* optional field is never silently ignored.
|
|
1356
|
+
*/
|
|
1357
|
+
export function isChatDaemonOwnerLock(value: unknown): value is ChatDaemonOwnerLock {
|
|
1358
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1359
|
+
const candidate = value as Record<string, unknown>;
|
|
1360
|
+
if (
|
|
1361
|
+
typeof candidate.pid !== "number" ||
|
|
1362
|
+
!Number.isSafeInteger(candidate.pid) ||
|
|
1363
|
+
candidate.pid <= 0 ||
|
|
1364
|
+
typeof candidate.incarnation !== "string" ||
|
|
1365
|
+
candidate.incarnation.length === 0 ||
|
|
1366
|
+
typeof candidate.createdAt !== "number" ||
|
|
1367
|
+
!Number.isFinite(candidate.createdAt)
|
|
1368
|
+
)
|
|
1369
|
+
return false;
|
|
1370
|
+
if (candidate.version !== undefined && candidate.version !== 1) return false;
|
|
1371
|
+
if (
|
|
1372
|
+
candidate.ownerId !== undefined &&
|
|
1373
|
+
(typeof candidate.ownerId !== "string" || candidate.ownerId.length === 0 || candidate.ownerId.length > 256)
|
|
1374
|
+
)
|
|
1375
|
+
return false;
|
|
1376
|
+
if (candidate.rootDigest !== undefined && !/^[0-9a-f]{64}$/.test(candidate.rootDigest as string)) return false;
|
|
1377
|
+
return true;
|
|
1210
1378
|
}
|
|
1211
1379
|
|
|
1212
1380
|
function isAlreadyExists(error: unknown): error is NodeJS.ErrnoException {
|
|
1213
1381
|
return typeof error === "object" && error !== null && (error as NodeJS.ErrnoException).code === "EEXIST";
|
|
1214
1382
|
}
|
|
1215
|
-
export
|
|
1383
|
+
export interface RenewChatDaemonHeartbeatInput {
|
|
1216
1384
|
agentDir: string;
|
|
1217
1385
|
kind: ChatDaemonKind;
|
|
1218
1386
|
ownerId: string;
|
|
@@ -1221,7 +1389,10 @@ export async function renewChatDaemonHeartbeat(input: {
|
|
|
1221
1389
|
transportHealthy: boolean;
|
|
1222
1390
|
pidAlive?: (pid: number) => boolean;
|
|
1223
1391
|
pidIncarnation?: (pid: number) => string | undefined;
|
|
1224
|
-
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
/** Actual body; runs ONLY inside {@link withDaemonStartupExclusion} (see {@link renewChatDaemonHeartbeat}). */
|
|
1395
|
+
async function renewChatDaemonHeartbeatLocked(input: RenewChatDaemonHeartbeatInput): Promise<boolean> {
|
|
1225
1396
|
const paths = chatDaemonPaths(input.agentDir, input.kind);
|
|
1226
1397
|
const pidAlive = input.pidAlive ?? defaultPidAlive;
|
|
1227
1398
|
const pidIncarnation = input.pidIncarnation ?? defaultPidIncarnation;
|
|
@@ -1244,7 +1415,19 @@ export async function renewChatDaemonHeartbeat(input: {
|
|
|
1244
1415
|
return true;
|
|
1245
1416
|
});
|
|
1246
1417
|
}
|
|
1247
|
-
|
|
1418
|
+
|
|
1419
|
+
/**
|
|
1420
|
+
* Renews the exact-owner heartbeat for `(agentDir, kind)`. Runs behind the
|
|
1421
|
+
* shared cross-process startup exclusion so a heartbeat can never publish in
|
|
1422
|
+
* the middle of a doctor startup/maintenance repair holding the same guard;
|
|
1423
|
+
* the actual re-check-and-write logic lives in the unlocked
|
|
1424
|
+
* {@link renewChatDaemonHeartbeatLocked}.
|
|
1425
|
+
*/
|
|
1426
|
+
export async function renewChatDaemonHeartbeat(input: RenewChatDaemonHeartbeatInput): Promise<boolean> {
|
|
1427
|
+
return await withDaemonStartupExclusion(input.agentDir, input.kind, () => renewChatDaemonHeartbeatLocked(input));
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
export interface ReleaseChatDaemonOwnershipInput {
|
|
1248
1431
|
agentDir: string;
|
|
1249
1432
|
kind: ChatDaemonKind;
|
|
1250
1433
|
ownerId: string;
|
|
@@ -1252,7 +1435,10 @@ export async function releaseChatDaemonOwnership(input: {
|
|
|
1252
1435
|
incarnation: string;
|
|
1253
1436
|
pidAlive?: (pid: number) => boolean;
|
|
1254
1437
|
pidIncarnation?: (pid: number) => string | undefined;
|
|
1255
|
-
}
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
/** Actual body; runs ONLY inside {@link withDaemonStartupExclusion} (see {@link releaseChatDaemonOwnership}). */
|
|
1441
|
+
async function releaseChatDaemonOwnershipLocked(input: ReleaseChatDaemonOwnershipInput): Promise<void> {
|
|
1256
1442
|
const paths = chatDaemonPaths(input.agentDir, input.kind);
|
|
1257
1443
|
const pidAlive = input.pidAlive ?? defaultPidAlive;
|
|
1258
1444
|
const pidIncarnation = input.pidIncarnation ?? defaultPidIncarnation;
|
|
@@ -1271,7 +1457,7 @@ export async function releaseChatDaemonOwnership(input: {
|
|
|
1271
1457
|
)
|
|
1272
1458
|
return;
|
|
1273
1459
|
await writeJson(paths.state, { ...state, stoppedAt: Date.now(), transportHealthy: false });
|
|
1274
|
-
const lock = await
|
|
1460
|
+
const lock = await captureChatDaemonOwnerLockLeaseOrUndefined(paths.lock);
|
|
1275
1461
|
let owner: unknown;
|
|
1276
1462
|
try {
|
|
1277
1463
|
owner = lock && JSON.parse(lock.content);
|
|
@@ -1280,3 +1466,13 @@ export async function releaseChatDaemonOwnership(input: {
|
|
|
1280
1466
|
unlinkExactChatDaemonOwnerLock(paths.lock, lock);
|
|
1281
1467
|
});
|
|
1282
1468
|
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* Releases this process's `(agentDir, kind)` chat daemon ownership. Runs
|
|
1472
|
+
* behind the shared cross-process startup exclusion for the same reason as
|
|
1473
|
+
* {@link acquireChatDaemonOwnership}; the actual re-check-and-unpublish logic
|
|
1474
|
+
* lives in the unlocked {@link releaseChatDaemonOwnershipLocked}.
|
|
1475
|
+
*/
|
|
1476
|
+
export async function releaseChatDaemonOwnership(input: ReleaseChatDaemonOwnershipInput): Promise<void> {
|
|
1477
|
+
await withDaemonStartupExclusion(input.agentDir, input.kind, () => releaseChatDaemonOwnershipLocked(input));
|
|
1478
|
+
}
|