@gajae-code/coding-agent 0.15.6 → 0.16.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 +99 -0
- package/dist/types/async/job-manager.d.ts +32 -1
- package/dist/types/browser-backend.d.ts +12 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +14 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/commands/sdk.d.ts +2 -0
- package/dist/types/config/autorouting-tier-map.d.ts +24 -0
- package/dist/types/config/billing-path.d.ts +25 -0
- package/dist/types/config/config-schema-version.d.ts +13 -1
- package/dist/types/config/file-lock.d.ts +5 -0
- package/dist/types/config/model-registry.d.ts +22 -0
- package/dist/types/config/settings-schema.d.ts +27 -8
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/extensibility/extensions/types.d.ts +8 -3
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/master-mode/context.d.ts +21 -0
- package/dist/types/master-mode/first-request.d.ts +34 -0
- package/dist/types/master-mode/snapshot.d.ts +32 -0
- package/dist/types/modes/acp/acp-agent.d.ts +21 -21
- package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
- package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
- package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
- package/dist/types/modes/components/status-line/segments.d.ts +10 -0
- package/dist/types/modes/interactive-mode.d.ts +16 -2
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +79 -2
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +40 -0
- package/dist/types/sdk/broker/master-capability.d.ts +10 -0
- package/dist/types/sdk/broker/session-index.d.ts +40 -11
- package/dist/types/sdk/broker/session-scope.d.ts +72 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
- package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
- package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
- package/dist/types/sdk/broker/state-version.d.ts +21 -1
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/index.d.ts +9 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/index.d.ts +1 -0
- package/dist/types/sdk/cli/master-cli.d.ts +63 -0
- package/dist/types/sdk/cli/rows.d.ts +12 -5
- package/dist/types/sdk/cli/session-cli.d.ts +20 -3
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +2 -0
- package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
- package/dist/types/sdk/host/events.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +10 -0
- package/dist/types/sdk/host/query/cursor.d.ts +4 -2
- package/dist/types/sdk/host/query/handlers.d.ts +1 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +70 -17
- package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
- package/dist/types/sdk/lifecycle/service.d.ts +15 -3
- package/dist/types/sdk/lifecycle-session.d.ts +3 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
- package/dist/types/sdk/receipt-state.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +6 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/sdk/turn-result.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +52 -7
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/fold-coordinator.d.ts +29 -2
- package/dist/types/session/session-manager.d.ts +79 -2
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +16 -0
- package/dist/types/tools/browser/launch.d.ts +10 -0
- package/dist/types/tools/browser/registry.d.ts +8 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +10 -10
- package/dist/types/tools/job.d.ts +3 -1
- package/dist/types/utils/external-editor.d.ts +7 -0
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/pirate.ts +7 -4
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +13 -1
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/async/job-manager.ts +107 -9
- package/src/browser-backend.ts +29 -0
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/plugin-cli.ts +29 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +140 -23
- package/src/commands/launch.ts +86 -18
- package/src/commands/sdk.ts +135 -28
- package/src/config/autorouting-tier-map.ts +12 -0
- package/src/config/billing-path.ts +42 -0
- package/src/config/config-schema-version.ts +18 -1
- package/src/config/file-lock.ts +158 -83
- package/src/config/model-preset-registry.ts +178 -23
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +291 -42
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +27 -8
- package/src/config/settings.ts +51 -15
- package/src/coordinator-mcp/projection-scan.ts +27 -6
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +115 -21
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +27 -0
- package/src/extensibility/extensions/types.ts +8 -3
- package/src/extensibility/gjc-plugins/installer.ts +51 -11
- package/src/extensibility/runtime-skill-discovery.ts +110 -3
- package/src/extensibility/skills.ts +22 -12
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/launch-worktree.ts +285 -4
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +213 -14
- package/src/gjc-runtime/session-state-sidecar.ts +938 -39
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +18 -3
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/internal-urls/docs-index.generated.ts +15 -15
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/main.ts +61 -3
- package/src/master-mode/context.ts +50 -0
- package/src/master-mode/first-request.ts +65 -0
- package/src/master-mode/snapshot.ts +205 -0
- package/src/modes/acp/acp-agent.ts +610 -161
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/model-selector.ts +76 -8
- package/src/modes/components/oauth-selector.ts +6 -1
- package/src/modes/components/session-selector.ts +23 -15
- package/src/modes/components/sessions-dashboard.ts +8 -5
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/model-name.ts +53 -0
- package/src/modes/components/status-line/priority-row.ts +107 -0
- package/src/modes/components/status-line/segments.ts +23 -9
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +30 -4
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +30 -2
- package/src/modes/controllers/selector-controller.ts +200 -98
- package/src/modes/interactive-mode.ts +36 -12
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/prompt-action-autocomplete.ts +30 -4
- package/src/modes/runtime-init.ts +12 -9
- package/src/modes/theme/theme.ts +5 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/ui-language.ts +49 -2
- package/src/modes/utils/ui-helpers.ts +22 -3
- package/src/prompts/agent-fragments/browser-aside.md +22 -0
- package/src/prompts/system/master-mode.md +28 -0
- package/src/prompts/tools/ask.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +5 -0
- package/src/sdk/broker/broker.ts +1646 -27
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +81 -3
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle-ledger.ts +6 -0
- package/src/sdk/broker/lifecycle.ts +440 -75
- package/src/sdk/broker/master-capability.ts +232 -0
- package/src/sdk/broker/session-index.ts +121 -17
- package/src/sdk/broker/session-scope.ts +272 -0
- package/src/sdk/broker/spawn-authority.ts +1031 -0
- package/src/sdk/broker/spawn-substrate.ts +591 -0
- package/src/sdk/broker/startup-budget.ts +144 -2
- package/src/sdk/broker/state-version.ts +43 -14
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/broker/worktree-occupancy.ts +90 -0
- package/src/sdk/bus/chat-daemon-control.ts +5 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +136 -18
- package/src/sdk/bus/discord-daemon.ts +50 -10
- package/src/sdk/bus/index.ts +84 -7
- package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
- package/src/sdk/bus/lifecycle-commands.ts +4 -0
- package/src/sdk/bus/notification-service.ts +22 -5
- package/src/sdk/bus/prompt-reconciliation.ts +7 -2
- package/src/sdk/bus/reconciliation-store.ts +55 -31
- package/src/sdk/bus/slack-conversation.ts +1 -1
- package/src/sdk/bus/slack-daemon.ts +72 -38
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/master-cli.ts +259 -0
- package/src/sdk/cli/rows.ts +41 -15
- package/src/sdk/cli/session-cli.ts +297 -26
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/host/control/dispatch.ts +101 -40
- package/src/sdk/host/control/index.ts +2 -0
- package/src/sdk/host/control/operations.ts +2 -0
- package/src/sdk/host/control/runtime-gate.ts +203 -11
- package/src/sdk/host/events.ts +1 -0
- package/src/sdk/host/host.ts +29 -2
- package/src/sdk/host/query/cursor.ts +52 -7
- package/src/sdk/host/query/handlers.ts +33 -12
- package/src/sdk/host/query/revision-store.ts +12 -0
- package/src/sdk/host/session-runtime.test.ts +673 -14
- package/src/sdk/host/session-runtime.ts +674 -103
- package/src/sdk/host/surface-policy.ts +3 -3
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +17 -4
- package/src/sdk/prompt-deadline-manager.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +105 -3
- package/src/sdk/protocol/operation-registry.ts +14 -4
- package/src/sdk/receipt-state.ts +10 -0
- package/src/sdk/router/session-router.ts +219 -118
- package/src/sdk/session.ts +416 -195
- package/src/sdk/startup-capability.ts +1 -0
- package/src/sdk/turn-result.ts +22 -0
- package/src/session/agent-session.ts +1628 -318
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/fold-coordinator.ts +40 -3
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/session-manager.ts +850 -97
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/slash-commands/builtin-registry.ts +41 -0
- package/src/task/executor.ts +9 -4
- package/src/telemetry/events.ts +1138 -12
- package/src/tools/ask-contract.test.ts +64 -0
- package/src/tools/ask-contract.ts +36 -1
- package/src/tools/ask.ts +10 -8
- package/src/tools/bash-interactive.ts +11 -2
- package/src/tools/bash.ts +412 -75
- package/src/tools/browser/launch.ts +37 -0
- package/src/tools/browser/registry.ts +99 -9
- package/src/tools/cron.ts +6 -4
- package/src/tools/descriptors.test.ts +15 -0
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +10 -6
- package/src/tools/job.ts +4 -1
- package/src/tools/monitor.ts +12 -9
- package/src/tools/resolve.ts +30 -10
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/utils/external-editor.ts +72 -1
|
@@ -3,6 +3,14 @@ export const DEFAULT_READINESS_TIMEOUT_MS = 10_000;
|
|
|
3
3
|
export const MIN_READINESS_TIMEOUT_MS = 4_000;
|
|
4
4
|
export const MAX_READINESS_TIMEOUT_MS = 60_000;
|
|
5
5
|
|
|
6
|
+
/** Bounded git worktree add/reuse window; independent of child semantic readiness. */
|
|
7
|
+
export const DEFAULT_WORKTREE_PREPARATION_TIMEOUT_MS = 30_000;
|
|
8
|
+
export const MIN_PREPARATION_TIMEOUT_MS = 1_000;
|
|
9
|
+
export const MAX_PREPARATION_TIMEOUT_MS = 120_000;
|
|
10
|
+
|
|
11
|
+
/** Bounded workspace install window; independent of child semantic readiness. */
|
|
12
|
+
export const DEFAULT_DEPENDENCY_PREPARATION_TIMEOUT_MS = 30_000;
|
|
13
|
+
|
|
6
14
|
export function isValidReadinessTimeoutMs(value: unknown): value is number {
|
|
7
15
|
return (
|
|
8
16
|
typeof value === "number" &&
|
|
@@ -12,7 +20,17 @@ export function isValidReadinessTimeoutMs(value: unknown): value is number {
|
|
|
12
20
|
);
|
|
13
21
|
}
|
|
14
22
|
|
|
23
|
+
export function isValidPreparationTimeoutMs(value: unknown): value is number {
|
|
24
|
+
return (
|
|
25
|
+
typeof value === "number" &&
|
|
26
|
+
Number.isSafeInteger(value) &&
|
|
27
|
+
value >= MIN_PREPARATION_TIMEOUT_MS &&
|
|
28
|
+
value <= MAX_PREPARATION_TIMEOUT_MS
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
15
32
|
export const READINESS_TIMEOUT_INVALID_MESSAGE = `readinessTimeoutMs must be an integer between ${MIN_READINESS_TIMEOUT_MS} and ${MAX_READINESS_TIMEOUT_MS}.`;
|
|
33
|
+
export const PREPARATION_TIMEOUT_INVALID_MESSAGE = `worktreePreparationTimeoutMs and dependencyPreparationTimeoutMs must be integers between ${MIN_PREPARATION_TIMEOUT_MS} and ${MAX_PREPARATION_TIMEOUT_MS}.`;
|
|
16
34
|
/**
|
|
17
35
|
* Sleep until the duration elapses or the caller cancels the wait. Queue admission
|
|
18
36
|
* uses this so a granted or refused waiter does not retain its cutoff timer for the
|
|
@@ -77,6 +95,10 @@ export function lifecycleStartupBudgetMs(requestedReadinessTimeoutMs: number): n
|
|
|
77
95
|
* rather than left unextended: the broker queues it for exactly as long as one that
|
|
78
96
|
* asked, so the common path would otherwise fail client-side while the broker runs
|
|
79
97
|
* the startup to a durably persisted terminal result.
|
|
98
|
+
*
|
|
99
|
+
* Worktree launches add independent preparation budgets so git add / install cannot
|
|
100
|
+
* cut the caller while the broker still owns the request. One-arg
|
|
101
|
+
* {@link lifecycleStartupBudgetMs} stays queue+readiness for no-worktree callers.
|
|
80
102
|
*/
|
|
81
103
|
export function lifecycleRequestTimeoutMs(operation: string, input: Record<string, unknown>): number | undefined {
|
|
82
104
|
const deadlineFields = [
|
|
@@ -97,7 +119,127 @@ export function lifecycleRequestTimeoutMs(operation: string, input: Record<strin
|
|
|
97
119
|
if (requested !== undefined && !isValidReadinessTimeoutMs(requested)) return undefined;
|
|
98
120
|
supplied = requested as number | undefined;
|
|
99
121
|
}
|
|
100
|
-
if (isStartupLifecycleOperation(operation))
|
|
101
|
-
|
|
122
|
+
if (isStartupLifecycleOperation(operation)) {
|
|
123
|
+
const readiness = supplied ?? DEFAULT_READINESS_TIMEOUT_MS;
|
|
124
|
+
const preparation = preparationBudgetMs(input);
|
|
125
|
+
if (preparation === undefined) return undefined;
|
|
126
|
+
return lifecycleStartupBudgetMs(readiness) + preparation + CALLER_DEADLINE_SLACK_MS;
|
|
127
|
+
}
|
|
102
128
|
return supplied === undefined ? undefined : supplied + CALLER_DEADLINE_SLACK_MS;
|
|
103
129
|
}
|
|
130
|
+
|
|
131
|
+
function recordEnabled(value: unknown): boolean {
|
|
132
|
+
return (
|
|
133
|
+
typeof value === "object" &&
|
|
134
|
+
value !== null &&
|
|
135
|
+
!Array.isArray(value) &&
|
|
136
|
+
(value as Record<string, unknown>).enabled === true
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function worktreeNamePresent(value: unknown): boolean {
|
|
141
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
142
|
+
const name = (value as Record<string, unknown>).name;
|
|
143
|
+
return typeof name === "string" && name.length > 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* True when this request will run named or detached worktree preparation.
|
|
148
|
+
* Coordinator nests the selector at `target.worktree`; Direct/lifecycle service
|
|
149
|
+
* put `worktree` on the mutation target itself; broker serialization also
|
|
150
|
+
* accepts `{ worktree: { name } }` without `enabled`.
|
|
151
|
+
*/
|
|
152
|
+
export function lifecycleRequestHasWorktree(input: Record<string, unknown>): boolean {
|
|
153
|
+
const nested = input.target;
|
|
154
|
+
if (typeof nested === "object" && nested !== null && !Array.isArray(nested)) {
|
|
155
|
+
const nestedWorktree = (nested as Record<string, unknown>).worktree;
|
|
156
|
+
if (recordEnabled(nestedWorktree) || worktreeNamePresent(nestedWorktree)) return true;
|
|
157
|
+
}
|
|
158
|
+
const worktree = input.worktree;
|
|
159
|
+
if (worktree === true) return true;
|
|
160
|
+
return recordEnabled(worktree) || worktreeNamePresent(worktree);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function readPreparationTimeout(
|
|
164
|
+
input: Record<string, unknown>,
|
|
165
|
+
key: "worktreePreparationTimeoutMs" | "dependencyPreparationTimeoutMs",
|
|
166
|
+
): number | undefined | "invalid" {
|
|
167
|
+
const value = input[key];
|
|
168
|
+
if (value === undefined) return undefined;
|
|
169
|
+
return isValidPreparationTimeoutMs(value) ? value : "invalid";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Sum of worktree and dependency preparation budgets, or 0 when the request
|
|
174
|
+
* has no worktree. Returns `undefined` when an explicit prep field is present
|
|
175
|
+
* but invalid so callers fail closed instead of silently dropping prep time.
|
|
176
|
+
*/
|
|
177
|
+
export function preparationBudgetMs(input: Record<string, unknown>): number | undefined {
|
|
178
|
+
const worktreeTimeout = readPreparationTimeout(input, "worktreePreparationTimeoutMs");
|
|
179
|
+
const dependencyTimeout = readPreparationTimeout(input, "dependencyPreparationTimeoutMs");
|
|
180
|
+
if (worktreeTimeout === "invalid" || dependencyTimeout === "invalid") return undefined;
|
|
181
|
+
if (!lifecycleRequestHasWorktree(input)) return 0;
|
|
182
|
+
return (
|
|
183
|
+
(worktreeTimeout ?? DEFAULT_WORKTREE_PREPARATION_TIMEOUT_MS) +
|
|
184
|
+
(dependencyTimeout ?? DEFAULT_DEPENDENCY_PREPARATION_TIMEOUT_MS)
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface LifecycleOuterDeadlines {
|
|
189
|
+
admittedAt: number;
|
|
190
|
+
worktreePrepTimeoutMs: number;
|
|
191
|
+
dependencyPrepTimeoutMs: number;
|
|
192
|
+
requestedReadinessTimeoutMs: number;
|
|
193
|
+
worktreePreparationDeadlineAt: number;
|
|
194
|
+
lifecycleCleanupDeadlineAt: number;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Whole-request outer deadlines for a worktree launch. Child semantic readiness
|
|
199
|
+
* is still derived later at prepSucceededAt. Dependency start is sequential:
|
|
200
|
+
* callers clip remaining dep budget against `lifecycleCleanupDeadlineAt`
|
|
201
|
+
* after worktreeDoneAt.
|
|
202
|
+
*/
|
|
203
|
+
export function deriveLifecycleOuterDeadlines(input: {
|
|
204
|
+
admittedAt: number;
|
|
205
|
+
worktreePrepTimeoutMs: number;
|
|
206
|
+
dependencyPrepTimeoutMs: number;
|
|
207
|
+
requestedReadinessTimeoutMs: number;
|
|
208
|
+
}): LifecycleOuterDeadlines {
|
|
209
|
+
const { admittedAt, worktreePrepTimeoutMs, dependencyPrepTimeoutMs, requestedReadinessTimeoutMs } = input;
|
|
210
|
+
if (
|
|
211
|
+
!Number.isSafeInteger(admittedAt) ||
|
|
212
|
+
!isValidPreparationTimeoutMs(worktreePrepTimeoutMs) ||
|
|
213
|
+
!isValidPreparationTimeoutMs(dependencyPrepTimeoutMs) ||
|
|
214
|
+
!isValidReadinessTimeoutMs(requestedReadinessTimeoutMs)
|
|
215
|
+
) {
|
|
216
|
+
throw new Error("Lifecycle outer timing values must be safe integers in the approved ranges.");
|
|
217
|
+
}
|
|
218
|
+
const worktreePreparationDeadlineAt = admittedAt + worktreePrepTimeoutMs;
|
|
219
|
+
const lifecycleCleanupDeadlineAt =
|
|
220
|
+
admittedAt + worktreePrepTimeoutMs + dependencyPrepTimeoutMs + requestedReadinessTimeoutMs;
|
|
221
|
+
if (!Number.isSafeInteger(worktreePreparationDeadlineAt) || !Number.isSafeInteger(lifecycleCleanupDeadlineAt)) {
|
|
222
|
+
throw new Error("Lifecycle outer timing values overflow the safe integer range.");
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
admittedAt,
|
|
226
|
+
worktreePrepTimeoutMs,
|
|
227
|
+
dependencyPrepTimeoutMs,
|
|
228
|
+
requestedReadinessTimeoutMs,
|
|
229
|
+
worktreePreparationDeadlineAt,
|
|
230
|
+
lifecycleCleanupDeadlineAt,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function readPreparationTimeouts(
|
|
235
|
+
input: Record<string, unknown>,
|
|
236
|
+
): { ok: true; worktreePrepTimeoutMs: number; dependencyPrepTimeoutMs: number } | { ok: false } {
|
|
237
|
+
const worktreeTimeout = readPreparationTimeout(input, "worktreePreparationTimeoutMs");
|
|
238
|
+
const dependencyTimeout = readPreparationTimeout(input, "dependencyPreparationTimeoutMs");
|
|
239
|
+
if (worktreeTimeout === "invalid" || dependencyTimeout === "invalid") return { ok: false };
|
|
240
|
+
return {
|
|
241
|
+
ok: true,
|
|
242
|
+
worktreePrepTimeoutMs: worktreeTimeout ?? DEFAULT_WORKTREE_PREPARATION_TIMEOUT_MS,
|
|
243
|
+
dependencyPrepTimeoutMs: dependencyTimeout ?? DEFAULT_DEPENDENCY_PREPARATION_TIMEOUT_MS,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
export const SDK_STATE_VERSION = 1;
|
|
2
2
|
|
|
3
|
-
// The session-index snapshot
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
// remain readable. Bumping only this constant keeps the broker protocol untouched.
|
|
11
|
-
export const SESSION_INDEX_SNAPSHOT_VERSION = 3;
|
|
3
|
+
// The session-index snapshot and event log share one format version. Version 4
|
|
4
|
+
// requires SessionLocatorV2 (`cwd`, `worktreeRoot`, `stateRoot`) on every
|
|
5
|
+
// retained row. New v4 events force older brokers to fail closed before they
|
|
6
|
+
// misread `cwd` as the former `repo`; pre-v4 snapshots remain readable only to
|
|
7
|
+
// quarantine legacy rows.
|
|
8
|
+
export const SESSION_INDEX_SNAPSHOT_VERSION = 4;
|
|
9
|
+
export const SESSION_INDEX_EVENT_VERSION = SESSION_INDEX_SNAPSHOT_VERSION;
|
|
12
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Thrown when a persisted SDK state file carries a version this build does not
|
|
13
|
+
* support. `assertSupportedStateVersion` fences only generic SDK state (broker
|
|
14
|
+
* discovery, lifecycle-ledger rows, guide cache meta) whose current format is
|
|
15
|
+
* exactly `SDK_STATE_VERSION`. Session-index files carry their own versions
|
|
16
|
+
* (see `assertSupportedSessionIndexEventVersion` and
|
|
17
|
+
* `assertSupportedSnapshotVersion`) and MUST NOT be validated with the generic
|
|
18
|
+
* guard — a supported v4 index row would crash with "maximum supported version
|
|
19
|
+
* is 1" (#5181).
|
|
20
|
+
*/
|
|
13
21
|
export class UnsupportedStateVersionError extends Error {
|
|
14
22
|
readonly code = "unsupported_state_version";
|
|
15
23
|
|
|
@@ -25,6 +33,13 @@ export class UnsupportedStateVersionError extends Error {
|
|
|
25
33
|
}
|
|
26
34
|
}
|
|
27
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Generic SDK-state fence: rejects any version above `SDK_STATE_VERSION`.
|
|
38
|
+
* ONLY for generic state whose current format is exactly `SDK_STATE_VERSION`
|
|
39
|
+
* (broker discovery `broker.json`, lifecycle-ledger rows, guide cache meta).
|
|
40
|
+
* Never validate session-index rows or snapshots with this guard — they have
|
|
41
|
+
* dedicated version fences and currently accept v4 (#5181).
|
|
42
|
+
*/
|
|
28
43
|
export function assertSupportedStateVersion(file: string, value: unknown): void {
|
|
29
44
|
if (!value || typeof value !== "object") return;
|
|
30
45
|
const record = value as { version?: unknown; stateVersion?: unknown };
|
|
@@ -35,11 +50,25 @@ export function assertSupportedStateVersion(file: string, value: unknown): void
|
|
|
35
50
|
}
|
|
36
51
|
}
|
|
37
52
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
/** Rejects session-index events outside the known legacy and current formats. */
|
|
54
|
+
export function assertSupportedSessionIndexEventVersion(file: string, value: unknown): void {
|
|
55
|
+
if (!value || typeof value !== "object") return;
|
|
56
|
+
const record = value as { version?: unknown; stateVersion?: unknown };
|
|
57
|
+
for (const version of [record.version, record.stateVersion]) {
|
|
58
|
+
if (
|
|
59
|
+
typeof version === "number" &&
|
|
60
|
+
Number.isFinite(version) &&
|
|
61
|
+
version !== SDK_STATE_VERSION &&
|
|
62
|
+
version !== SESSION_INDEX_EVENT_VERSION
|
|
63
|
+
) {
|
|
64
|
+
throw new UnsupportedStateVersionError(file, version, SESSION_INDEX_EVENT_VERSION);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Fences future session-index snapshot formats. Locator-v2 row validation below
|
|
70
|
+
// rejects legacy `repo` rows without translating them, while snapshots predating
|
|
71
|
+
// v4 remain readable solely to quarantine those rows with a re-register diagnostic.
|
|
43
72
|
export function assertSupportedSnapshotVersion(file: string, value: unknown): void {
|
|
44
73
|
if (!value || typeof value !== "object") return;
|
|
45
74
|
const record = value as { version?: unknown; stateVersion?: unknown };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which session currently holds a GJC-managed worktree.
|
|
3
|
+
*
|
|
4
|
+
* Both launch paths need this answer and must not disagree about it: the broker
|
|
5
|
+
* refuses a lifecycle launch into an occupied worktree, and the interactive
|
|
6
|
+
* `gjc --worktree` launch refuses for the same reason. Keeping one predicate —
|
|
7
|
+
* and one definition of "still running" — here prevents the two from drifting
|
|
8
|
+
* into different notions of liveness.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type * as NativeBindings from "@gajae-code/natives";
|
|
12
|
+
import { resolveEquivalentPath } from "@gajae-code/utils";
|
|
13
|
+
import { probeLinuxProcPidSync } from "../../gjc-runtime/linux-proc";
|
|
14
|
+
import { processIncarnation } from "./process-incarnation";
|
|
15
|
+
import type { IndexedSession } from "./session-index";
|
|
16
|
+
|
|
17
|
+
function nativeLifecycle(): typeof NativeBindings {
|
|
18
|
+
return require("@gajae-code/natives") as typeof NativeBindings;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ProcessObservation = "alive" | "exited" | "uncertain";
|
|
22
|
+
|
|
23
|
+
/** Only ESRCH or a changed, readable incarnation proves the owned process exited. */
|
|
24
|
+
export function observeProcess(
|
|
25
|
+
pid: number,
|
|
26
|
+
expectedIncarnation: string | undefined,
|
|
27
|
+
readIncarnation: (pid: number) => string | undefined = processIncarnation,
|
|
28
|
+
): ProcessObservation {
|
|
29
|
+
if (process.platform === "win32") {
|
|
30
|
+
try {
|
|
31
|
+
const reference = nativeLifecycle().Process.fromPid(pid);
|
|
32
|
+
if (reference?.status() !== "running") return "exited";
|
|
33
|
+
} catch {
|
|
34
|
+
return "uncertain";
|
|
35
|
+
}
|
|
36
|
+
if (!expectedIncarnation) return "uncertain";
|
|
37
|
+
const actualIncarnation = readIncarnation(pid);
|
|
38
|
+
if (!actualIncarnation) return "uncertain";
|
|
39
|
+
return actualIncarnation === expectedIncarnation ? "alive" : "exited";
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
process.kill(pid, 0);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return (error as NodeJS.ErrnoException).code === "ESRCH" ? "exited" : "uncertain";
|
|
45
|
+
}
|
|
46
|
+
if (process.platform === "linux") {
|
|
47
|
+
const probe = probeLinuxProcPidSync(pid);
|
|
48
|
+
if (probe.kind === "live" && probe.state === "Z" && expectedIncarnation === `linux:${probe.startTime}`)
|
|
49
|
+
return "exited";
|
|
50
|
+
}
|
|
51
|
+
if (!expectedIncarnation) return "uncertain";
|
|
52
|
+
const actualIncarnation = readIncarnation(pid);
|
|
53
|
+
if (!actualIncarnation) return "uncertain";
|
|
54
|
+
return actualIncarnation === expectedIncarnation ? "alive" : "exited";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The id of a session still occupying `worktreePath`, or null when it is free.
|
|
59
|
+
*
|
|
60
|
+
* `ensureLaunchWorktree` reuses an existing worktree without asking whether
|
|
61
|
+
* anyone is in it, and an unnamed launch derives its directory deterministically
|
|
62
|
+
* from the repository's current branch. Two concurrent sessions in one
|
|
63
|
+
* repository therefore land in the same checkout and overwrite each other's
|
|
64
|
+
* files with no error.
|
|
65
|
+
*
|
|
66
|
+
* Only a process observed as definitively exited releases the worktree.
|
|
67
|
+
* `uncertain` counts as occupied: refusing a launch is recoverable by picking
|
|
68
|
+
* another worktree name, whereas two live sessions sharing a checkout corrupts
|
|
69
|
+
* work already done.
|
|
70
|
+
*/
|
|
71
|
+
export function worktreeOccupant(
|
|
72
|
+
sessions: readonly IndexedSession[],
|
|
73
|
+
worktreePath: string,
|
|
74
|
+
observe: (pid: number, expectedIncarnation: string | undefined) => ProcessObservation = observeProcess,
|
|
75
|
+
): string | null {
|
|
76
|
+
// Session locators retain the canonical Git worktree root separately from the
|
|
77
|
+
// session cwd. A host may start below that root, and a launch plan may arrive
|
|
78
|
+
// through a symlink, so compare the root identity rather than the cwd spelling.
|
|
79
|
+
const target = resolveEquivalentPath(worktreePath);
|
|
80
|
+
for (const session of sessions) {
|
|
81
|
+
const sessionWorktreeRoot = session.locator.worktreeRoot;
|
|
82
|
+
if (session.terminal || typeof sessionWorktreeRoot !== "string" || sessionWorktreeRoot.length === 0) continue;
|
|
83
|
+
if (resolveEquivalentPath(sessionWorktreeRoot) !== target) continue;
|
|
84
|
+
// `live` is heartbeat-derived and can be stale. Only positive process-exit
|
|
85
|
+
// evidence releases a matching retained session's worktree.
|
|
86
|
+
if (observe(session.pid, session.hostIncarnation ?? session.processIncarnation) === "exited") continue;
|
|
87
|
+
return session.sessionId;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
@@ -130,10 +130,13 @@ export type ChatDaemonAction = "stop" | "reload";
|
|
|
130
130
|
* session-index lock every 2s; staleness retirement moved to a 30s sweep and
|
|
131
131
|
* lease heartbeats no longer force an authority reconcile. A pre-upgrade owner
|
|
132
132
|
* would retain the old hot polling loop, so replacement is required.
|
|
133
|
+
* Discord generation 72 / Slack generation 75 serialize same-generation
|
|
134
|
+
* successor attachment admission behind predecessor provider retirement, so a
|
|
135
|
+
* pre-upgrade owner cannot retain the queue race fixed by #5120.
|
|
133
136
|
*/
|
|
134
137
|
export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
|
|
135
|
-
discord:
|
|
136
|
-
slack:
|
|
138
|
+
discord: 73,
|
|
139
|
+
slack: 77,
|
|
137
140
|
};
|
|
138
141
|
|
|
139
142
|
export function chatDaemonGeneration(kind: ChatDaemonKind): number {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { logger } from "@gajae-code/utils";
|
|
2
3
|
import { SdkClientError } from "../client/client";
|
|
3
4
|
import { SESSION_PREPARED_EVENT } from "../host/host";
|
|
4
5
|
|
|
@@ -258,7 +259,11 @@ export class ChatDaemonRuntime {
|
|
|
258
259
|
readonly #router: SessionRouter;
|
|
259
260
|
readonly #attachments = new Map<string, SessionAttachment>();
|
|
260
261
|
readonly #cleanupWork = new Map<string, Promise<void>>();
|
|
262
|
+
readonly #retirementWork = new Map<string, Promise<void>>();
|
|
261
263
|
readonly #attachmentBarriers = new Map<string, Promise<void>>();
|
|
264
|
+
readonly #cleanupFailures = new Map<string, unknown>();
|
|
265
|
+
#replayReady: Promise<void> = Promise.resolve();
|
|
266
|
+
#stopping = false;
|
|
262
267
|
#discord: DiscordNotificationDaemon | undefined;
|
|
263
268
|
#slack: SlackNotificationDaemon | undefined;
|
|
264
269
|
#presentation: NotificationPresentationEngine | undefined;
|
|
@@ -275,18 +280,40 @@ export class ChatDaemonRuntime {
|
|
|
275
280
|
...deps.routerDeps,
|
|
276
281
|
onFrame: async (attachment, frame) => await this.#handleFrame(attachment, frame),
|
|
277
282
|
onAttachment: async attachment => this.#onAttachment(attachment),
|
|
283
|
+
onReplayReady: async () => await this.#replayReady,
|
|
278
284
|
onSessionRemoved: async (attachment, reason) => await this.#onSessionRemoved(attachment, reason),
|
|
279
285
|
},
|
|
280
286
|
});
|
|
281
287
|
}
|
|
282
288
|
|
|
283
289
|
async start(): Promise<void> {
|
|
284
|
-
|
|
290
|
+
const replayReady = Promise.withResolvers<void>();
|
|
291
|
+
this.#replayReady = replayReady.promise;
|
|
292
|
+
// Startup can fail before any attachment observes the replay barrier. Mark
|
|
293
|
+
// the rejection handled without changing what actual waiters observe.
|
|
294
|
+
void this.#replayReady.catch(() => undefined);
|
|
295
|
+
if (this.#cleanupWork.size > 0 || this.#retirementWork.size > 0) {
|
|
296
|
+
const retirements = [...this.#retirementWork.entries()];
|
|
297
|
+
const cleanup = [...this.#cleanupWork.values()];
|
|
298
|
+
const retirement = retirements.map(([, work]) => work);
|
|
285
299
|
const settled = await Promise.race([
|
|
286
|
-
Promise.
|
|
300
|
+
Promise.all([Promise.allSettled(cleanup), Promise.allSettled(retirement)]).then(() => true),
|
|
287
301
|
Bun.sleep(5_000).then(() => false),
|
|
288
302
|
]);
|
|
289
303
|
if (!settled) throw new Error("Prior provider cleanup did not settle before chat daemon restart.");
|
|
304
|
+
const failures = await Promise.all([Promise.allSettled(cleanup), Promise.allSettled(retirement)]);
|
|
305
|
+
const rejected = failures
|
|
306
|
+
.flat()
|
|
307
|
+
.filter((result): result is PromiseRejectedResult => result.status === "rejected");
|
|
308
|
+
if (rejected.length > 0) {
|
|
309
|
+
logger.warn(`SDK chat daemon retained ${rejected.length} prior cleanup failure(s) for recovery.`);
|
|
310
|
+
}
|
|
311
|
+
for (const [sessionId, work] of retirements)
|
|
312
|
+
if (this.#retirementWork.get(sessionId) === work && !rejected.some(result => result.status === "rejected"))
|
|
313
|
+
this.#retirementWork.delete(sessionId);
|
|
314
|
+
for (const [sessionId, work] of [...this.#cleanupWork.entries()])
|
|
315
|
+
if (work !== undefined && !rejected.some(result => result.status === "rejected"))
|
|
316
|
+
this.#cleanupWork.delete(sessionId);
|
|
290
317
|
}
|
|
291
318
|
this.#attachments.clear();
|
|
292
319
|
const retainedProvider =
|
|
@@ -301,8 +328,10 @@ export class ChatDaemonRuntime {
|
|
|
301
328
|
try {
|
|
302
329
|
await this.#router.start();
|
|
303
330
|
await retainedProvider.start();
|
|
331
|
+
replayReady.resolve();
|
|
304
332
|
return;
|
|
305
333
|
} catch (error) {
|
|
334
|
+
replayReady.reject(error);
|
|
306
335
|
await this.#router.stop().catch(() => undefined);
|
|
307
336
|
throw error;
|
|
308
337
|
}
|
|
@@ -363,7 +392,9 @@ export class ChatDaemonRuntime {
|
|
|
363
392
|
await this.#router.start();
|
|
364
393
|
if (this.#discord) await this.#discord.start();
|
|
365
394
|
if (this.#slack) await this.#slack.start();
|
|
395
|
+
replayReady.resolve();
|
|
366
396
|
} catch (error) {
|
|
397
|
+
replayReady.reject(error);
|
|
367
398
|
await this.stop();
|
|
368
399
|
throw error;
|
|
369
400
|
}
|
|
@@ -379,6 +410,20 @@ export class ChatDaemonRuntime {
|
|
|
379
410
|
}
|
|
380
411
|
|
|
381
412
|
async stop(): Promise<void> {
|
|
413
|
+
this.#stopping = true;
|
|
414
|
+
let routerError: unknown;
|
|
415
|
+
try {
|
|
416
|
+
await this.#router.stop();
|
|
417
|
+
} catch (error) {
|
|
418
|
+
routerError = error;
|
|
419
|
+
}
|
|
420
|
+
const cleanupDrain = Promise.allSettled([...this.#cleanupWork.values(), ...this.#retirementWork.values()]);
|
|
421
|
+
const cleanupSettled = await Promise.race([cleanupDrain.then(() => true), Bun.sleep(5_000).then(() => false)]);
|
|
422
|
+
const cleanupResults = cleanupSettled
|
|
423
|
+
? await cleanupDrain
|
|
424
|
+
: [{ status: "rejected" as const, reason: new Error("Provider cleanup exceeded shutdown drain.") }];
|
|
425
|
+
if (!cleanupSettled)
|
|
426
|
+
logger.warn("SDK chat daemon cleanup exceeded the shutdown drain; provider stop will invalidate it.");
|
|
382
427
|
const providerResults = await Promise.allSettled([this.#discord?.stop(), this.#slack?.stop()]);
|
|
383
428
|
const discordRestartBlocked = this.#discord?.restartBlocked() ?? false;
|
|
384
429
|
const slackRestartBlocked = this.#slack?.restartBlocked() ?? false;
|
|
@@ -388,10 +433,12 @@ export class ChatDaemonRuntime {
|
|
|
388
433
|
this.#presentation = undefined;
|
|
389
434
|
this.#transportHealthy = undefined;
|
|
390
435
|
}
|
|
391
|
-
|
|
392
|
-
const failures =
|
|
393
|
-
(
|
|
394
|
-
|
|
436
|
+
this.#stopping = false;
|
|
437
|
+
const failures = [
|
|
438
|
+
...(routerError === undefined ? [] : [{ status: "rejected" as const, reason: routerError }]),
|
|
439
|
+
...cleanupResults,
|
|
440
|
+
...providerResults,
|
|
441
|
+
].filter((result): result is PromiseRejectedResult => result.status === "rejected");
|
|
395
442
|
if (failures.length > 0)
|
|
396
443
|
throw new AggregateError(
|
|
397
444
|
failures.map(result => result.reason),
|
|
@@ -428,9 +475,34 @@ export class ChatDaemonRuntime {
|
|
|
428
475
|
const discord = this.#discord;
|
|
429
476
|
const slack = this.#slack;
|
|
430
477
|
const barrier = (async () => {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
478
|
+
const retirement = this.#retirementWork.get(attachment.sessionId);
|
|
479
|
+
let retirementFailure: unknown;
|
|
480
|
+
if (retirement) {
|
|
481
|
+
try {
|
|
482
|
+
await retirement;
|
|
483
|
+
} catch (error) {
|
|
484
|
+
retirementFailure = error;
|
|
485
|
+
logger.warn(`SDK chat daemon retirement cleanup failed before successor attachment: ${String(error)}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const cleanup = this.#cleanupWork.get(attachment.sessionId);
|
|
489
|
+
if (cleanup) await cleanup.catch(error => logger.warn(`SDK cleanup retry pending: ${String(error)}`));
|
|
490
|
+
const [discordRecovered, slackRecovered] = await Promise.all([
|
|
491
|
+
discord?.recoverCleanup(attachment.sessionId, attachment.generation, attachment.authorityId) ?? true,
|
|
492
|
+
slack?.recoverCleanup(attachment.sessionId, attachment.generation, attachment.authorityId) ?? true,
|
|
493
|
+
]);
|
|
494
|
+
if (!discordRecovered || !slackRecovered)
|
|
495
|
+
throw new Error("successor_attachment_blocked_until_retirement_is_verified");
|
|
496
|
+
if (cleanup !== undefined && this.#cleanupWork.get(attachment.sessionId) === cleanup) {
|
|
497
|
+
this.#cleanupWork.delete(attachment.sessionId);
|
|
498
|
+
this.#cleanupFailures.delete(attachment.sessionId);
|
|
499
|
+
}
|
|
500
|
+
if (
|
|
501
|
+
retirementFailure !== undefined &&
|
|
502
|
+
retirement &&
|
|
503
|
+
this.#retirementWork.get(attachment.sessionId) === retirement
|
|
504
|
+
)
|
|
505
|
+
this.#retirementWork.delete(attachment.sessionId);
|
|
434
506
|
})();
|
|
435
507
|
this.#attachmentBarriers.set(attachment.sessionId, barrier);
|
|
436
508
|
try {
|
|
@@ -451,11 +523,41 @@ export class ChatDaemonRuntime {
|
|
|
451
523
|
): Promise<void> {
|
|
452
524
|
if (this.#attachments.get(attachment.sessionId) !== attachment) return;
|
|
453
525
|
if (reason === "replaced_same_generation") {
|
|
454
|
-
|
|
455
|
-
|
|
526
|
+
const retirement = this.#trackRetirement(attachment);
|
|
527
|
+
this.#attachments.delete(attachment.sessionId);
|
|
528
|
+
await retirement;
|
|
529
|
+
return;
|
|
456
530
|
}
|
|
457
531
|
this.#attachments.delete(attachment.sessionId);
|
|
458
|
-
if (reason === "removed") await this.#trackCleanup(attachment);
|
|
532
|
+
if (reason === "removed" && !this.#stopping) await this.#trackCleanup(attachment);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
#trackRetirement(attachment: SessionAttachment): Promise<void> {
|
|
536
|
+
const sessionId = attachment.sessionId;
|
|
537
|
+
const discord = this.#discord;
|
|
538
|
+
const slack = this.#slack;
|
|
539
|
+
const previous = this.#retirementWork.get(sessionId);
|
|
540
|
+
const work = (
|
|
541
|
+
previous
|
|
542
|
+
? previous.catch(error => {
|
|
543
|
+
this.#cleanupFailures.set(sessionId, error);
|
|
544
|
+
})
|
|
545
|
+
: Promise.resolve()
|
|
546
|
+
).then(async () => {
|
|
547
|
+
// Capture the predecessor providers before stop() can clear the runtime
|
|
548
|
+
// fields. The successor barrier owns this promise until every retirement
|
|
549
|
+
// waiter settles, even when takeover overlaps worker shutdown.
|
|
550
|
+
await discord?.retireAttachment(sessionId, attachment.generation);
|
|
551
|
+
await slack?.retireAttachment(sessionId, attachment.generation);
|
|
552
|
+
});
|
|
553
|
+
this.#retirementWork.set(sessionId, work);
|
|
554
|
+
void work.then(
|
|
555
|
+
() => {
|
|
556
|
+
if (this.#retirementWork.get(sessionId) === work) this.#retirementWork.delete(sessionId);
|
|
557
|
+
},
|
|
558
|
+
() => undefined,
|
|
559
|
+
);
|
|
560
|
+
return work;
|
|
459
561
|
}
|
|
460
562
|
|
|
461
563
|
async #handleFrame(attachment: SessionAttachment, correlated: CorrelatedFrame): Promise<void> {
|
|
@@ -476,7 +578,10 @@ export class ChatDaemonRuntime {
|
|
|
476
578
|
}
|
|
477
579
|
if (name === SESSION_PREPARED_EVENT || bodyType === SESSION_PREPARED_EVENT) return;
|
|
478
580
|
if (name === "session_ready") {
|
|
479
|
-
|
|
581
|
+
// The attachment itself is the endpoint-generation authority. An
|
|
582
|
+
// unpositioned session_ready frame deliberately carries no ring
|
|
583
|
+
// generation; reject only an explicit, conflicting Router-owned value.
|
|
584
|
+
if (correlated.generation !== undefined && correlated.generation !== attachment.generation) return;
|
|
480
585
|
await this.#resume(
|
|
481
586
|
sessionId,
|
|
482
587
|
attachment.generation,
|
|
@@ -519,7 +624,7 @@ export class ChatDaemonRuntime {
|
|
|
519
624
|
await this.#slack.notify(
|
|
520
625
|
sessionId,
|
|
521
626
|
content,
|
|
522
|
-
notification.type === "action_needed" ? notification.id : undefined,
|
|
627
|
+
notification.type === "action_needed" && notification.kind === "ask" ? notification.id : undefined,
|
|
523
628
|
attachment.generation,
|
|
524
629
|
publicationId,
|
|
525
630
|
);
|
|
@@ -528,13 +633,26 @@ export class ChatDaemonRuntime {
|
|
|
528
633
|
#trackCleanup(attachment: SessionAttachment): Promise<void> {
|
|
529
634
|
const sessionId = attachment.sessionId;
|
|
530
635
|
const previous = this.#cleanupWork.get(sessionId);
|
|
531
|
-
const work = (
|
|
636
|
+
const work = (
|
|
637
|
+
previous
|
|
638
|
+
? previous.catch(error => {
|
|
639
|
+
this.#cleanupFailures.set(sessionId, error);
|
|
640
|
+
})
|
|
641
|
+
: Promise.resolve()
|
|
642
|
+
).then(async () => {
|
|
532
643
|
await this.#closeProviders(attachment);
|
|
533
644
|
});
|
|
534
645
|
this.#cleanupWork.set(sessionId, work);
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
646
|
+
void work.then(
|
|
647
|
+
() => {
|
|
648
|
+
if (this.#cleanupWork.get(sessionId) === work) {
|
|
649
|
+
this.#cleanupWork.delete(sessionId);
|
|
650
|
+
this.#cleanupFailures.delete(sessionId);
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
() => undefined,
|
|
654
|
+
);
|
|
655
|
+
return work;
|
|
538
656
|
}
|
|
539
657
|
async #closeProviders(attachment: SessionAttachment): Promise<void> {
|
|
540
658
|
const discord = this.#discord;
|