@gajae-code/coding-agent 0.16.0 → 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 +41 -1
- package/dist/types/async/job-manager.d.ts +32 -1
- package/dist/types/browser-backend.d.ts +12 -0
- package/dist/types/cli/update-cli.d.ts +1 -0
- package/dist/types/commands/sdk.d.ts +2 -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/model-registry.d.ts +16 -0
- package/dist/types/config/settings-schema.d.ts +17 -7
- package/dist/types/extensibility/extensions/types.d.ts +8 -3
- package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +21 -21
- package/dist/types/modes/components/model-selector.d.ts +4 -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/interactive-mode.d.ts +16 -2
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -1
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +14 -0
- package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
- package/dist/types/sdk/broker/state-version.d.ts +17 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/index.d.ts +3 -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/rows.d.ts +8 -2
- package/dist/types/sdk/cli/session-cli.d.ts +2 -1
- package/dist/types/sdk/host/events.d.ts +1 -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 +18 -17
- package/dist/types/sdk/lifecycle-session.d.ts +1 -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 +4 -0
- package/dist/types/sdk/turn-result.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +47 -7
- package/dist/types/session/fold-coordinator.d.ts +29 -2
- package/dist/types/session/session-manager.d.ts +14 -2
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +12 -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/index.d.ts +6 -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 +7 -1
- package/src/async/job-manager.ts +107 -9
- package/src/browser-backend.ts +29 -0
- package/src/cli/plugin-cli.ts +29 -0
- package/src/cli/update-cli.ts +63 -12
- package/src/commands/sdk.ts +22 -7
- package/src/config/billing-path.ts +42 -0
- package/src/config/config-schema-version.ts +18 -1
- package/src/config/model-preset-registry.ts +177 -18
- package/src/config/model-registry.ts +268 -42
- package/src/config/settings-schema.ts +19 -7
- package/src/config/settings.ts +46 -15
- package/src/coordinator-mcp/projection-scan.ts +6 -6
- package/src/coordinator-mcp/server.ts +21 -1
- 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.ts +285 -4
- package/src/gjc-runtime/session-state-lock.ts +154 -28
- package/src/gjc-runtime/session-state-sidecar.ts +10 -17
- package/src/gjc-runtime/tmux-owner-isolation.ts +16 -1
- package/src/internal-urls/docs-index.generated.ts +10 -10
- package/src/main.ts +4 -1
- package/src/modes/acp/acp-agent.ts +5 -5
- package/src/modes/components/model-selector.ts +76 -8
- package/src/modes/components/session-selector.ts +23 -15
- package/src/modes/components/sessions-dashboard.ts +8 -5
- 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 +2 -2
- package/src/modes/controllers/selector-controller.ts +10 -3
- package/src/modes/interactive-mode.ts +36 -12
- 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/utils/ui-helpers.ts +22 -3
- package/src/prompts/agent-fragments/browser-aside.md +22 -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 +10 -1
- package/src/sdk/broker/ensure.ts +76 -3
- package/src/sdk/broker/lifecycle-ledger.ts +6 -0
- package/src/sdk/broker/lifecycle.ts +254 -34
- package/src/sdk/broker/spawn-substrate.ts +45 -2
- package/src/sdk/broker/startup-budget.ts +144 -2
- package/src/sdk/broker/state-version.ts +17 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +111 -24
- package/src/sdk/bus/discord-daemon.ts +50 -10
- package/src/sdk/bus/index.ts +33 -3
- 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/rows.ts +30 -7
- package/src/sdk/cli/session-cli.ts +33 -21
- package/src/sdk/host/control/dispatch.ts +2 -2
- package/src/sdk/host/events.ts +1 -0
- package/src/sdk/host/host.ts +2 -0
- 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 +463 -8
- package/src/sdk/host/session-runtime.ts +383 -54
- package/src/sdk/host/surface-policy.ts +3 -3
- package/src/sdk/lifecycle-session.ts +7 -2
- package/src/sdk/prompt-deadline-manager.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +54 -3
- package/src/sdk/protocol/operation-registry.ts +1 -1
- package/src/sdk/receipt-state.ts +10 -0
- package/src/sdk/router/session-router.ts +152 -59
- package/src/sdk/session.ts +60 -41
- package/src/sdk/turn-result.ts +22 -0
- package/src/session/agent-session.ts +1033 -260
- package/src/session/fold-coordinator.ts +40 -3
- package/src/session/session-manager.ts +517 -77
- package/src/slash-commands/builtin-registry.ts +41 -0
- package/src/task/executor.ts +9 -4
- 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 +156 -33
- 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/index.ts +6 -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
|
@@ -26,10 +26,13 @@ import {
|
|
|
26
26
|
import { mergeModelProfiles } from "../../config/model-profiles";
|
|
27
27
|
import { ModelsConfigFile } from "../../config/model-registry";
|
|
28
28
|
import {
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
DependencyPreparationTimeoutError,
|
|
30
|
+
ensureLaunchWorktreeCancellable,
|
|
31
|
+
ensureReusableNodeModulesCancellable,
|
|
31
32
|
type GjcLaunchWorktreePlan,
|
|
32
33
|
planLaunchWorktree,
|
|
34
|
+
removeOwnedLaunchWorktree,
|
|
35
|
+
WorktreePreparationTimeoutError,
|
|
33
36
|
} from "../../gjc-runtime/launch-worktree";
|
|
34
37
|
import { probeLinuxProcPidSync } from "../../gjc-runtime/linux-proc";
|
|
35
38
|
import {
|
|
@@ -91,8 +94,11 @@ import {
|
|
|
91
94
|
import {
|
|
92
95
|
cancellableSleep,
|
|
93
96
|
DEFAULT_READINESS_TIMEOUT_MS,
|
|
97
|
+
deriveLifecycleOuterDeadlines,
|
|
94
98
|
isValidReadinessTimeoutMs,
|
|
99
|
+
PREPARATION_TIMEOUT_INVALID_MESSAGE,
|
|
95
100
|
READINESS_TIMEOUT_INVALID_MESSAGE,
|
|
101
|
+
readPreparationTimeouts,
|
|
96
102
|
startupQueueWaitMs,
|
|
97
103
|
} from "./startup-budget";
|
|
98
104
|
import { worktreeOccupant } from "./worktree-occupancy";
|
|
@@ -201,6 +207,17 @@ const lifecycleCommandResolversForTest = new WeakMap<Broker, LifecycleCommandRes
|
|
|
201
207
|
const lifecycleCleanupHooksForTest = new WeakMap<Broker, () => void>();
|
|
202
208
|
const startupAdmittedInputs = new WeakSet<Input>();
|
|
203
209
|
const startupLaunchInputs = new WeakMap<Input, SessionLaunch>();
|
|
210
|
+
type EnsureLaunchWorktreeForTest = (
|
|
211
|
+
plan: GjcLaunchWorktreePlan,
|
|
212
|
+
opts: { signal: AbortSignal; deadlineAt: number },
|
|
213
|
+
) => Promise<SessionLifecycleWorktreeReceipt & { createdBranch: boolean }>;
|
|
214
|
+
type EnsureReusableNodeModulesForTest = (
|
|
215
|
+
sourceRoot: string,
|
|
216
|
+
worktreePath: string,
|
|
217
|
+
opts: { signal: AbortSignal; deadlineAt: number },
|
|
218
|
+
) => Promise<"symlink" | "present" | "missing">;
|
|
219
|
+
let ensureLaunchWorktreeForTest: EnsureLaunchWorktreeForTest | undefined;
|
|
220
|
+
let ensureReusableNodeModulesForTest: EnsureReusableNodeModulesForTest | undefined;
|
|
204
221
|
|
|
205
222
|
/** Test-only hook for simulating a crash immediately after one exact lifecycle detach. */
|
|
206
223
|
export function setLifecycleCleanupHookForTest(broker: Broker, hook: (() => void) | undefined): void {
|
|
@@ -220,6 +237,20 @@ export function setLifecycleTimingForTest(broker: Broker, timing: LifecycleTimin
|
|
|
220
237
|
if (timing) lifecycleTimingsForTest.set(broker, timing);
|
|
221
238
|
else lifecycleTimingsForTest.delete(broker);
|
|
222
239
|
}
|
|
240
|
+
export function setEnsureLaunchWorktreeForTest(
|
|
241
|
+
_broker: Broker | undefined,
|
|
242
|
+
fn?: EnsureLaunchWorktreeForTest | undefined,
|
|
243
|
+
): void {
|
|
244
|
+
ensureLaunchWorktreeForTest = typeof _broker === "function" ? (_broker as EnsureLaunchWorktreeForTest) : fn;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function setEnsureReusableNodeModulesForTest(
|
|
248
|
+
_broker: Broker | undefined,
|
|
249
|
+
fn?: EnsureReusableNodeModulesForTest | undefined,
|
|
250
|
+
): void {
|
|
251
|
+
ensureReusableNodeModulesForTest =
|
|
252
|
+
typeof _broker === "function" ? (_broker as EnsureReusableNodeModulesForTest) : fn;
|
|
253
|
+
}
|
|
223
254
|
|
|
224
255
|
let lifecycleHostPlatformForTest: NodeJS.Platform | undefined;
|
|
225
256
|
|
|
@@ -300,6 +331,7 @@ export interface SessionLifecycleWorktreeReceipt {
|
|
|
300
331
|
cwd: string;
|
|
301
332
|
created: boolean;
|
|
302
333
|
reused: boolean;
|
|
334
|
+
createdBranch: boolean;
|
|
303
335
|
branch?: string;
|
|
304
336
|
}
|
|
305
337
|
|
|
@@ -734,7 +766,7 @@ function isLifecycleWorktreeTarget(value: unknown): value is SessionLifecycleWor
|
|
|
734
766
|
|
|
735
767
|
function lifecycleWorktreeTarget(input: Input): SessionLifecycleWorktreeTarget | null | undefined {
|
|
736
768
|
const target = input.target as Record<string, unknown> | undefined;
|
|
737
|
-
const worktree = target?.worktree;
|
|
769
|
+
const worktree = target?.worktree ?? input.worktree;
|
|
738
770
|
if (worktree === undefined) return undefined;
|
|
739
771
|
return isLifecycleWorktreeTarget(worktree) ? worktree : null;
|
|
740
772
|
}
|
|
@@ -4150,8 +4182,22 @@ export function worktreeOccupantForTest(
|
|
|
4150
4182
|
return worktreeOccupant(sessions, worktreePath, observe);
|
|
4151
4183
|
}
|
|
4152
4184
|
|
|
4153
|
-
function preparePlannedWorktree(
|
|
4154
|
-
|
|
4185
|
+
async function preparePlannedWorktree(
|
|
4186
|
+
plan: GjcLaunchWorktreePlan,
|
|
4187
|
+
opts: { signal: AbortSignal; deadlineAt: number; now: () => number },
|
|
4188
|
+
): Promise<SessionLifecycleWorktreeReceipt> {
|
|
4189
|
+
const hooked = ensureLaunchWorktreeForTest;
|
|
4190
|
+
if (hooked) {
|
|
4191
|
+
const receipt = await hooked(plan, { signal: opts.signal, deadlineAt: opts.deadlineAt });
|
|
4192
|
+
if (!receipt.enabled || path.resolve(receipt.cwd) !== path.resolve(plan.worktreePath))
|
|
4193
|
+
throw new Error("Lifecycle worktree preparation did not preserve the durable worktree identity.");
|
|
4194
|
+
return receipt;
|
|
4195
|
+
}
|
|
4196
|
+
const prepared = await ensureLaunchWorktreeCancellable(plan, {
|
|
4197
|
+
signal: opts.signal,
|
|
4198
|
+
deadlineAt: opts.deadlineAt,
|
|
4199
|
+
now: opts.now,
|
|
4200
|
+
});
|
|
4155
4201
|
if (!prepared.enabled || path.resolve(prepared.worktreePath) !== path.resolve(plan.worktreePath))
|
|
4156
4202
|
throw new Error("Lifecycle worktree preparation did not preserve the durable worktree identity.");
|
|
4157
4203
|
return {
|
|
@@ -4159,9 +4205,67 @@ function preparePlannedWorktree(plan: GjcLaunchWorktreePlan): SessionLifecycleWo
|
|
|
4159
4205
|
cwd: path.resolve(prepared.worktreePath),
|
|
4160
4206
|
created: prepared.created,
|
|
4161
4207
|
reused: prepared.reused,
|
|
4208
|
+
createdBranch: prepared.createdBranch,
|
|
4162
4209
|
...(prepared.branchName ? { branch: prepared.branchName } : {}),
|
|
4163
4210
|
};
|
|
4164
4211
|
}
|
|
4212
|
+
|
|
4213
|
+
function mapPreparationFailure(error: unknown): BrokerResponse {
|
|
4214
|
+
const message = sanitizeSdkStartupMessage(error);
|
|
4215
|
+
if (
|
|
4216
|
+
error instanceof WorktreePreparationTimeoutError ||
|
|
4217
|
+
(error as { code?: string }).code === "worktree_preparation_timeout"
|
|
4218
|
+
) {
|
|
4219
|
+
return fail("worktree_preparation_timeout", message);
|
|
4220
|
+
}
|
|
4221
|
+
if (
|
|
4222
|
+
error instanceof DependencyPreparationTimeoutError ||
|
|
4223
|
+
(error as { code?: string }).code === "dependency_preparation_timeout"
|
|
4224
|
+
) {
|
|
4225
|
+
return fail("dependency_preparation_timeout", message);
|
|
4226
|
+
}
|
|
4227
|
+
return fail("spawn_failed", `Unable to prepare lifecycle worktree: ${message}`);
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
function abortWhenDue(
|
|
4231
|
+
controller: AbortController,
|
|
4232
|
+
deadlineAt: number,
|
|
4233
|
+
now: () => number,
|
|
4234
|
+
): ReturnType<typeof setInterval> {
|
|
4235
|
+
const tick = (): void => {
|
|
4236
|
+
if (now() >= deadlineAt && !controller.signal.aborted) controller.abort();
|
|
4237
|
+
};
|
|
4238
|
+
tick();
|
|
4239
|
+
return setInterval(tick, 10);
|
|
4240
|
+
}
|
|
4241
|
+
|
|
4242
|
+
function durableWorktreeEffects(
|
|
4243
|
+
receipt: SessionLifecycleWorktreeReceipt,
|
|
4244
|
+
timings: { worktreePreparationMs?: number; dependencyPreparationMs?: number; spawnAuthorizedAtOffsetMs?: number },
|
|
4245
|
+
): LifecycleDurableEffectsReceipt {
|
|
4246
|
+
const worktree = {
|
|
4247
|
+
cwdDigest: createHash("sha256").update(receipt.cwd, "utf8").digest("hex"),
|
|
4248
|
+
created: receipt.created,
|
|
4249
|
+
reused: receipt.reused,
|
|
4250
|
+
createdBranch: receipt.createdBranch,
|
|
4251
|
+
...(receipt.branch ? { branchDigest: createHash("sha256").update(receipt.branch, "utf8").digest("hex") } : {}),
|
|
4252
|
+
};
|
|
4253
|
+
return {
|
|
4254
|
+
worktree,
|
|
4255
|
+
timings,
|
|
4256
|
+
digest: createHash("sha256").update(canonicalJson({ worktree, timings })).digest("hex"),
|
|
4257
|
+
};
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
function deadlineFieldsPresent(input: Input): boolean {
|
|
4261
|
+
return [
|
|
4262
|
+
input.receivedAt,
|
|
4263
|
+
input.requestedReadinessTimeoutMs,
|
|
4264
|
+
input.semanticReadyDeadlineAt,
|
|
4265
|
+
input.terminationStartDeadlineAt,
|
|
4266
|
+
input.lifecycleCleanupDeadlineAt,
|
|
4267
|
+
].some(value => value !== undefined);
|
|
4268
|
+
}
|
|
4165
4269
|
async function launchInput(
|
|
4166
4270
|
broker: Broker,
|
|
4167
4271
|
operation: "session.create" | "session.fork" | "session.resume",
|
|
@@ -4923,8 +5027,16 @@ async function executeLifecycleResponse(
|
|
|
4923
5027
|
const queueWaitMs = startupQueueWaitMs(requestedReadinessTimeoutMs);
|
|
4924
5028
|
const launch = await launchInput(broker, operation, input);
|
|
4925
5029
|
if ("ok" in launch) return launch;
|
|
5030
|
+
if (launch.worktreePlan && deadlineFieldsPresent(input)) {
|
|
5031
|
+
return fail(
|
|
5032
|
+
"invalid_input",
|
|
5033
|
+
"Lifecycle worktree launches cannot carry a caller-supplied child deadline tuple.",
|
|
5034
|
+
);
|
|
5035
|
+
}
|
|
4926
5036
|
const admitted = await broker.runStartup(queueWaitMs, timing, async admittedAt => {
|
|
4927
|
-
const admittedInput =
|
|
5037
|
+
const admittedInput = launch.worktreePlan
|
|
5038
|
+
? { ...input, admittedAt, requestedReadinessTimeoutMs }
|
|
5039
|
+
: { ...input, ...deriveLifecycleDeadlines(admittedAt, requestedReadinessTimeoutMs) };
|
|
4928
5040
|
startupAdmittedInputs.add(admittedInput);
|
|
4929
5041
|
startupLaunchInputs.set(admittedInput, launch);
|
|
4930
5042
|
try {
|
|
@@ -4944,15 +5056,52 @@ async function executeLifecycleResponse(
|
|
|
4944
5056
|
return fail("startup_admission_timeout", failure.message);
|
|
4945
5057
|
}
|
|
4946
5058
|
|
|
4947
|
-
const deadlines = lifecycleDeadlines(input, input.receivedAt as number);
|
|
4948
|
-
|
|
4949
|
-
if ("ok" in deadlines) return deadlines;
|
|
4950
|
-
const lifecycleDeadline = deadlines.lifecycleCleanupDeadlineAt;
|
|
4951
|
-
const readinessDeadline = deadlines.semanticReadyDeadlineAt;
|
|
4952
|
-
const terminationStartDeadline = deadlines.terminationStartDeadlineAt;
|
|
4953
|
-
|
|
4954
5059
|
const launch = startupLaunchInputs.get(input) ?? (await launchInput(broker, operation, input));
|
|
4955
5060
|
if ("ok" in launch) return launch;
|
|
5061
|
+
if (launch.worktreePlan && deadlineFieldsPresent(input) && input.admittedAt === undefined) {
|
|
5062
|
+
return fail(
|
|
5063
|
+
"invalid_input",
|
|
5064
|
+
"Lifecycle worktree launches cannot carry a caller-supplied child deadline tuple.",
|
|
5065
|
+
);
|
|
5066
|
+
}
|
|
5067
|
+
|
|
5068
|
+
let childDeadlines: LifecycleDeadlines | undefined;
|
|
5069
|
+
let lifecycleDeadline: number;
|
|
5070
|
+
let readinessDeadline: number;
|
|
5071
|
+
let terminationStartDeadline: number;
|
|
5072
|
+
let outerCleanupDeadlineAt: number | undefined;
|
|
5073
|
+
if (launch.worktreePlan) {
|
|
5074
|
+
const prepTimeouts = readPreparationTimeouts(input);
|
|
5075
|
+
if (!prepTimeouts.ok) return fail("invalid_input", PREPARATION_TIMEOUT_INVALID_MESSAGE);
|
|
5076
|
+
const admittedAt =
|
|
5077
|
+
typeof input.admittedAt === "number" && Number.isSafeInteger(input.admittedAt)
|
|
5078
|
+
? input.admittedAt
|
|
5079
|
+
: timing.now();
|
|
5080
|
+
const requestedReadinessTimeoutMs =
|
|
5081
|
+
typeof input.requestedReadinessTimeoutMs === "number" &&
|
|
5082
|
+
isValidReadinessTimeoutMs(input.requestedReadinessTimeoutMs)
|
|
5083
|
+
? input.requestedReadinessTimeoutMs
|
|
5084
|
+
: readinessTimeout(input);
|
|
5085
|
+
if (typeof requestedReadinessTimeoutMs !== "number") return requestedReadinessTimeoutMs;
|
|
5086
|
+
const outer = deriveLifecycleOuterDeadlines({
|
|
5087
|
+
admittedAt,
|
|
5088
|
+
worktreePrepTimeoutMs: prepTimeouts.worktreePrepTimeoutMs,
|
|
5089
|
+
dependencyPrepTimeoutMs: prepTimeouts.dependencyPrepTimeoutMs,
|
|
5090
|
+
requestedReadinessTimeoutMs,
|
|
5091
|
+
});
|
|
5092
|
+
outerCleanupDeadlineAt = outer.lifecycleCleanupDeadlineAt;
|
|
5093
|
+
lifecycleDeadline = outer.lifecycleCleanupDeadlineAt;
|
|
5094
|
+
readinessDeadline = outer.lifecycleCleanupDeadlineAt;
|
|
5095
|
+
terminationStartDeadline = outer.lifecycleCleanupDeadlineAt;
|
|
5096
|
+
} else {
|
|
5097
|
+
const deadlines = lifecycleDeadlines(input, input.receivedAt as number);
|
|
5098
|
+
if ("ok" in deadlines) return deadlines;
|
|
5099
|
+
childDeadlines = deadlines;
|
|
5100
|
+
lifecycleDeadline = deadlines.lifecycleCleanupDeadlineAt;
|
|
5101
|
+
readinessDeadline = deadlines.semanticReadyDeadlineAt;
|
|
5102
|
+
terminationStartDeadline = deadlines.terminationStartDeadlineAt;
|
|
5103
|
+
}
|
|
5104
|
+
|
|
4956
5105
|
if (!hasProcessIncarnationAuthority())
|
|
4957
5106
|
return fail(
|
|
4958
5107
|
"incarnation_unavailable",
|
|
@@ -4972,7 +5121,8 @@ async function executeLifecycleResponse(
|
|
|
4972
5121
|
sessionId: launch.id,
|
|
4973
5122
|
stateRoot: launch.root,
|
|
4974
5123
|
childOwnershipEstablished: false,
|
|
4975
|
-
lifecycleCleanupDeadlineAt:
|
|
5124
|
+
lifecycleCleanupDeadlineAt:
|
|
5125
|
+
outerCleanupDeadlineAt ?? (childDeadlines as LifecycleDeadlines).lifecycleCleanupDeadlineAt,
|
|
4976
5126
|
...(plannedWorktreeIntent ? { worktree: plannedWorktreeIntent } : {}),
|
|
4977
5127
|
};
|
|
4978
5128
|
|
|
@@ -4989,39 +5139,109 @@ async function executeLifecycleResponse(
|
|
|
4989
5139
|
let worktreeReceipt: SessionLifecycleWorktreeReceipt | undefined;
|
|
4990
5140
|
try {
|
|
4991
5141
|
if (launch.worktreePlan) {
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5142
|
+
const prepTimeouts = readPreparationTimeouts(input);
|
|
5143
|
+
if (!prepTimeouts.ok) return fail("invalid_input", PREPARATION_TIMEOUT_INVALID_MESSAGE);
|
|
5144
|
+
const admittedAt =
|
|
5145
|
+
typeof input.admittedAt === "number" && Number.isSafeInteger(input.admittedAt)
|
|
5146
|
+
? input.admittedAt
|
|
5147
|
+
: timing.now();
|
|
5148
|
+
const requestedReadinessTimeoutMs =
|
|
5149
|
+
typeof input.requestedReadinessTimeoutMs === "number" &&
|
|
5150
|
+
isValidReadinessTimeoutMs(input.requestedReadinessTimeoutMs)
|
|
5151
|
+
? input.requestedReadinessTimeoutMs
|
|
5152
|
+
: readinessTimeout(input);
|
|
5153
|
+
if (typeof requestedReadinessTimeoutMs !== "number") return requestedReadinessTimeoutMs;
|
|
5154
|
+
const outer = deriveLifecycleOuterDeadlines({
|
|
5155
|
+
admittedAt,
|
|
5156
|
+
worktreePrepTimeoutMs: prepTimeouts.worktreePrepTimeoutMs,
|
|
5157
|
+
dependencyPrepTimeoutMs: prepTimeouts.dependencyPrepTimeoutMs,
|
|
5158
|
+
requestedReadinessTimeoutMs,
|
|
5159
|
+
});
|
|
5160
|
+
const controller = new AbortController();
|
|
5161
|
+
const worktreeStartedAt = timing.now();
|
|
5162
|
+
const worktreeWatch = abortWhenDue(controller, outer.worktreePreparationDeadlineAt, () => timing.now());
|
|
5163
|
+
try {
|
|
5164
|
+
worktreeReceipt = await preparePlannedWorktree(launch.worktreePlan, {
|
|
5165
|
+
signal: controller.signal,
|
|
5166
|
+
deadlineAt: outer.worktreePreparationDeadlineAt,
|
|
5167
|
+
now: () => timing.now(),
|
|
5168
|
+
});
|
|
5169
|
+
} finally {
|
|
5170
|
+
clearInterval(worktreeWatch);
|
|
5171
|
+
}
|
|
5172
|
+
const worktreeDoneAt = timing.now();
|
|
5173
|
+
const worktreePreparationMs = Math.max(0, worktreeDoneAt - worktreeStartedAt);
|
|
5174
|
+
await broker.ledger.transition(identity, "effect_started", {
|
|
5175
|
+
durableEffects: durableWorktreeEffects(worktreeReceipt, { worktreePreparationMs }),
|
|
5176
|
+
});
|
|
5177
|
+
const remainingDepBudget = Math.min(
|
|
5178
|
+
prepTimeouts.dependencyPrepTimeoutMs,
|
|
5179
|
+
Math.max(0, outer.lifecycleCleanupDeadlineAt - worktreeDoneAt - requestedReadinessTimeoutMs),
|
|
5180
|
+
);
|
|
5181
|
+
const dependencyDeadlineAt = worktreeDoneAt + remainingDepBudget;
|
|
5182
|
+
if (timing.now() >= dependencyDeadlineAt) throw new DependencyPreparationTimeoutError();
|
|
5183
|
+
const depWatch = abortWhenDue(controller, dependencyDeadlineAt, () => timing.now());
|
|
5184
|
+
const depHook = ensureReusableNodeModulesForTest;
|
|
5185
|
+
const dependencyStartedAt = timing.now();
|
|
5186
|
+
try {
|
|
5187
|
+
if (depHook) {
|
|
5188
|
+
await depHook(launch.worktreePlan.repoRoot, launch.worktreePlan.worktreePath, {
|
|
5189
|
+
signal: controller.signal,
|
|
5190
|
+
deadlineAt: dependencyDeadlineAt,
|
|
5191
|
+
});
|
|
5192
|
+
} else {
|
|
5193
|
+
await ensureReusableNodeModulesCancellable(
|
|
5194
|
+
launch.worktreePlan.repoRoot,
|
|
5195
|
+
launch.worktreePlan.worktreePath,
|
|
5196
|
+
{
|
|
5197
|
+
signal: controller.signal,
|
|
5198
|
+
deadlineAt: dependencyDeadlineAt,
|
|
5199
|
+
now: () => timing.now(),
|
|
5200
|
+
},
|
|
5201
|
+
);
|
|
5202
|
+
}
|
|
5203
|
+
} finally {
|
|
5204
|
+
clearInterval(depWatch);
|
|
5205
|
+
}
|
|
5206
|
+
const dependencyPreparationMs = Math.max(0, timing.now() - dependencyStartedAt);
|
|
5207
|
+
await broker.ledger.transition(identity, "effect_started", {
|
|
5208
|
+
durableEffects: durableWorktreeEffects(worktreeReceipt, {
|
|
5209
|
+
worktreePreparationMs,
|
|
5210
|
+
dependencyPreparationMs,
|
|
5211
|
+
spawnAuthorizedAtOffsetMs: Math.max(0, timing.now() - admittedAt),
|
|
5212
|
+
}),
|
|
5213
|
+
});
|
|
5214
|
+
const prepSucceededAt = timing.now();
|
|
5215
|
+
childDeadlines = deriveLifecycleDeadlines(prepSucceededAt, requestedReadinessTimeoutMs);
|
|
5216
|
+
readinessDeadline = childDeadlines.semanticReadyDeadlineAt;
|
|
5217
|
+
terminationStartDeadline = childDeadlines.terminationStartDeadlineAt;
|
|
5218
|
+
lifecycleDeadline = childDeadlines.lifecycleCleanupDeadlineAt;
|
|
5007
5219
|
}
|
|
5008
5220
|
await reapDeadLifecycleMarkers(launch.root);
|
|
5009
5221
|
} catch (error) {
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
);
|
|
5222
|
+
if (launch.worktreePlan && worktreeReceipt?.created && !worktreeReceipt.reused) {
|
|
5223
|
+
removeOwnedLaunchWorktree(launch.worktreePlan, worktreeReceipt);
|
|
5224
|
+
}
|
|
5225
|
+
return mapPreparationFailure(error);
|
|
5014
5226
|
}
|
|
5015
|
-
if (timing.now() >= readinessDeadline)
|
|
5227
|
+
if (!launch.worktreePlan && timing.now() >= readinessDeadline)
|
|
5016
5228
|
return fail(
|
|
5017
5229
|
"readiness_timeout",
|
|
5018
5230
|
"Lifecycle preparation exhausted the semantic readiness deadline before spawning.",
|
|
5019
5231
|
);
|
|
5232
|
+
if (launch.worktreePlan && !childDeadlines)
|
|
5233
|
+
return fail(
|
|
5234
|
+
worktreeReceipt ? "dependency_preparation_timeout" : "worktree_preparation_timeout",
|
|
5235
|
+
worktreeReceipt
|
|
5236
|
+
? "Dependency preparation exceeded its deadline before the session host was spawned."
|
|
5237
|
+
: "Worktree preparation exceeded its deadline before the session host was spawned.",
|
|
5238
|
+
);
|
|
5020
5239
|
if (!hasProcessIncarnationAuthority())
|
|
5021
5240
|
return fail(
|
|
5022
5241
|
"incarnation_unavailable",
|
|
5023
5242
|
"OS process incarnation authority is unavailable; refusing to spawn a lifecycle session.",
|
|
5024
5243
|
);
|
|
5244
|
+
const deadlines = childDeadlines as LifecycleDeadlines;
|
|
5025
5245
|
|
|
5026
5246
|
const coordinatorSidecarTarget =
|
|
5027
5247
|
typeof launch.coordinatorStateDir === "string" && typeof launch.coordinatorSidecarKeyId === "string"
|
|
@@ -90,6 +90,30 @@ function launchFailureMessage(selected: "tmux" | "psmux" | "headless", error: un
|
|
|
90
90
|
return `${selected} substrate launch failed${bounded ? `: ${bounded}` : ""}`;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
function cleanupFailureMessage(selected: "tmux" | "psmux", error: unknown): string {
|
|
94
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
95
|
+
const bounded = detail
|
|
96
|
+
.replace(/[\u0000-\u001f\u007f]/g, " ")
|
|
97
|
+
.trim()
|
|
98
|
+
.slice(0, MAX_SUBSTRATE_DIAGNOSTIC_LENGTH);
|
|
99
|
+
return `${selected} substrate cleanup failed${bounded ? `: ${bounded}` : ""}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function cleanupUncertaintyMessage(selected: "tmux" | "psmux"): string {
|
|
103
|
+
return `${selected} substrate cleanup could not be verified`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function cleanupWasVerifiedAfterManagedLaunchFailure(error: unknown): boolean {
|
|
107
|
+
if (error instanceof AggregateError) return false;
|
|
108
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
109
|
+
// The owner-isolation layer reports a failed tmux allocation before a
|
|
110
|
+
// managed session exists. That is the only managed launch failure that is
|
|
111
|
+
// safe to replace with the exact headless substrate. Every other thrown
|
|
112
|
+
// error may follow a partial create/proof path and therefore needs explicit
|
|
113
|
+
// cleanup evidence instead of optimistic fallback.
|
|
114
|
+
return detail.includes("planned_spawn_failed") && !detail.includes("cleanup_uncertain");
|
|
115
|
+
}
|
|
116
|
+
|
|
93
117
|
function hasOnlyKeys(value: Record<string, unknown>, keys: Set<string>): boolean {
|
|
94
118
|
return Object.keys(value).every(key => keys.has(key));
|
|
95
119
|
}
|
|
@@ -440,6 +464,16 @@ export function createSpawnSubstrateProvider(
|
|
|
440
464
|
managed = launchManaged(spec, launchEnv, platform);
|
|
441
465
|
} catch (error) {
|
|
442
466
|
const failure = launchFailureMessage(selected, error);
|
|
467
|
+
if (!cleanupWasVerifiedAfterManagedLaunchFailure(error))
|
|
468
|
+
return {
|
|
469
|
+
ok: false,
|
|
470
|
+
code: "substrate_proof_failed",
|
|
471
|
+
message: `${failure}; ${
|
|
472
|
+
error instanceof AggregateError
|
|
473
|
+
? cleanupFailureMessage(selected, error)
|
|
474
|
+
: cleanupUncertaintyMessage(selected)
|
|
475
|
+
}`,
|
|
476
|
+
};
|
|
443
477
|
try {
|
|
444
478
|
const fallback = await launchHeadlessSubstrate(spec, launchEnv);
|
|
445
479
|
if (fallback.ok) return fallback;
|
|
@@ -458,12 +492,21 @@ export function createSpawnSubstrateProvider(
|
|
|
458
492
|
}
|
|
459
493
|
const incarnation = readIncarnation(managed.pid);
|
|
460
494
|
if (!incarnation || verifyManaged(managed, launchEnv) !== "verified") {
|
|
495
|
+
let cleanupVerified = false;
|
|
496
|
+
let cleanupError: unknown;
|
|
461
497
|
try {
|
|
462
498
|
await closeManaged(managed, launchEnv);
|
|
463
|
-
|
|
464
|
-
|
|
499
|
+
cleanupVerified = true;
|
|
500
|
+
} catch (error) {
|
|
501
|
+
cleanupError = error;
|
|
465
502
|
}
|
|
466
503
|
const failure = launchFailureMessage(selected, new Error("exact substrate proof failed"));
|
|
504
|
+
if (!cleanupVerified)
|
|
505
|
+
return {
|
|
506
|
+
ok: false,
|
|
507
|
+
code: "substrate_proof_failed",
|
|
508
|
+
message: `${failure}; ${cleanupFailureMessage(selected, cleanupError)}`,
|
|
509
|
+
};
|
|
467
510
|
const fallback = await launchHeadlessSubstrate(spec, launchEnv);
|
|
468
511
|
if (fallback.ok) return fallback;
|
|
469
512
|
return {
|
|
@@ -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
|
+
}
|
|
@@ -8,6 +8,16 @@ export const SDK_STATE_VERSION = 1;
|
|
|
8
8
|
export const SESSION_INDEX_SNAPSHOT_VERSION = 4;
|
|
9
9
|
export const SESSION_INDEX_EVENT_VERSION = SESSION_INDEX_SNAPSHOT_VERSION;
|
|
10
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
|
+
*/
|
|
11
21
|
export class UnsupportedStateVersionError extends Error {
|
|
12
22
|
readonly code = "unsupported_state_version";
|
|
13
23
|
|
|
@@ -23,6 +33,13 @@ export class UnsupportedStateVersionError extends Error {
|
|
|
23
33
|
}
|
|
24
34
|
}
|
|
25
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
|
+
*/
|
|
26
43
|
export function assertSupportedStateVersion(file: string, value: unknown): void {
|
|
27
44
|
if (!value || typeof value !== "object") return;
|
|
28
45
|
const record = value as { version?: unknown; stateVersion?: unknown };
|
|
@@ -135,8 +135,8 @@ export type ChatDaemonAction = "stop" | "reload";
|
|
|
135
135
|
* pre-upgrade owner cannot retain the queue race fixed by #5120.
|
|
136
136
|
*/
|
|
137
137
|
export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
|
|
138
|
-
discord:
|
|
139
|
-
slack:
|
|
138
|
+
discord: 73,
|
|
139
|
+
slack: 77,
|
|
140
140
|
};
|
|
141
141
|
|
|
142
142
|
export function chatDaemonGeneration(kind: ChatDaemonKind): number {
|