@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
|
@@ -15,6 +15,8 @@ export const MANAGED_OWNER_GENERATION_ENV = "GJC_TMUX_OWNER_GENERATION";
|
|
|
15
15
|
export const MANAGED_OWNER_STATE_DIR_ENV = "GJC_TMUX_OWNER_STATE_DIR";
|
|
16
16
|
export const MANAGED_OWNER_RUN_ID_ENV = "GJC_MANAGED_OWNER_RUN_ID";
|
|
17
17
|
export const MANAGED_OWNER_INCARNATION_ENV = "GJC_MANAGED_OWNER_INCARNATION";
|
|
18
|
+
/** Suppresses command-derived durable artifacts for Broker-owned opaque spawn children. */
|
|
19
|
+
export const MANAGED_OWNER_REDACT_COMMAND_ENV = "GJC_MANAGED_OWNER_REDACT_COMMAND";
|
|
18
20
|
|
|
19
21
|
let bootstrapSigtermPending = false;
|
|
20
22
|
const captureBootstrapSigterm = () => {
|
|
@@ -140,26 +142,32 @@ export async function runManagedOwnerSupervisor(): Promise<void> {
|
|
|
140
142
|
if (!supervisorStartTime) throw new Error("managed_owner_supervisor_start_time_unavailable");
|
|
141
143
|
await fs.mkdir(root, { recursive: true, mode: 0o700 });
|
|
142
144
|
const childToken = crypto.randomUUID();
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
145
|
+
const redactCommand = process.env[MANAGED_OWNER_REDACT_COMMAND_ENV] === "1";
|
|
146
|
+
const binding: ManagedOwnerBinding | null = redactCommand
|
|
147
|
+
? null
|
|
148
|
+
: {
|
|
149
|
+
schema_version: 2,
|
|
150
|
+
generation,
|
|
151
|
+
session_id: sessionId,
|
|
152
|
+
run_id: runId,
|
|
153
|
+
endpoint_incarnation: incarnation,
|
|
154
|
+
child_token: childToken,
|
|
155
|
+
command,
|
|
156
|
+
command_sha256: commandDigest(command),
|
|
157
|
+
supervisor_pid: process.pid,
|
|
158
|
+
supervisor_start_time: supervisorStartTime,
|
|
159
|
+
created_at: new Date().toISOString(),
|
|
160
|
+
};
|
|
161
|
+
if (binding) await writeDurableExclusive(path.join(root, `child-${childToken}.binding.json`), binding);
|
|
162
|
+
const childEnvironment: NodeJS.ProcessEnv = { ...process.env, [MANAGED_OWNER_CHILD_TOKEN_ENV]: childToken };
|
|
163
|
+
delete childEnvironment[MANAGED_OWNER_COMMAND_ENV];
|
|
164
|
+
delete childEnvironment[MANAGED_OWNER_REDACT_COMMAND_ENV];
|
|
157
165
|
const child = Bun.spawn({
|
|
158
166
|
cmd: command,
|
|
159
167
|
stdin: "inherit",
|
|
160
168
|
stdout: "inherit",
|
|
161
169
|
stderr: "inherit",
|
|
162
|
-
env:
|
|
170
|
+
env: childEnvironment,
|
|
163
171
|
});
|
|
164
172
|
const childStartTime = await managedOwnerProcessProvenance(child.pid);
|
|
165
173
|
if (!childStartTime) throw new Error("managed_owner_child_start_time_unavailable");
|
|
@@ -167,24 +175,26 @@ export async function runManagedOwnerSupervisor(): Promise<void> {
|
|
|
167
175
|
if (!childProcess) {
|
|
168
176
|
const exitCode = await child.exited;
|
|
169
177
|
if (child.signalCode === "SIGABRT") {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
178
|
+
if (binding) {
|
|
179
|
+
const receipt: ManagedOwnerSigabrtReceipt = {
|
|
180
|
+
schema_version: 2,
|
|
181
|
+
generation,
|
|
182
|
+
session_id: sessionId,
|
|
183
|
+
run_id: runId,
|
|
184
|
+
endpoint_incarnation: incarnation,
|
|
185
|
+
child_token: childToken,
|
|
186
|
+
command_sha256: binding.command_sha256,
|
|
187
|
+
supervisor_pid: process.pid,
|
|
188
|
+
supervisor_start_time: supervisorStartTime,
|
|
189
|
+
child_pid: child.pid,
|
|
190
|
+
child_start_time: childStartTime,
|
|
191
|
+
signal: "SIGABRT",
|
|
192
|
+
signal_number: 6,
|
|
193
|
+
exit_code: exitCode,
|
|
194
|
+
received_at: new Date().toISOString(),
|
|
195
|
+
};
|
|
196
|
+
await writeDurableExclusive(path.join(root, `sigabrt-${childToken}.receipt.json`), receipt);
|
|
197
|
+
}
|
|
188
198
|
process.exitCode = 134;
|
|
189
199
|
return;
|
|
190
200
|
}
|
|
@@ -246,24 +256,26 @@ export async function runManagedOwnerSupervisor(): Promise<void> {
|
|
|
246
256
|
});
|
|
247
257
|
}
|
|
248
258
|
if (child.signalCode === "SIGABRT") {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
259
|
+
if (binding) {
|
|
260
|
+
const receipt: ManagedOwnerSigabrtReceipt = {
|
|
261
|
+
schema_version: 2,
|
|
262
|
+
generation,
|
|
263
|
+
session_id: sessionId,
|
|
264
|
+
run_id: runId,
|
|
265
|
+
endpoint_incarnation: incarnation,
|
|
266
|
+
child_token: childToken,
|
|
267
|
+
command_sha256: binding.command_sha256,
|
|
268
|
+
supervisor_pid: process.pid,
|
|
269
|
+
supervisor_start_time: supervisorStartTime,
|
|
270
|
+
child_pid: child.pid,
|
|
271
|
+
child_start_time: childStartTime,
|
|
272
|
+
signal: "SIGABRT",
|
|
273
|
+
signal_number: 6,
|
|
274
|
+
exit_code: exitCode,
|
|
275
|
+
received_at: new Date().toISOString(),
|
|
276
|
+
};
|
|
277
|
+
await writeDurableExclusive(path.join(root, `sigabrt-${childToken}.receipt.json`), receipt);
|
|
278
|
+
}
|
|
267
279
|
process.exitCode = 134;
|
|
268
280
|
return;
|
|
269
281
|
}
|
|
@@ -39,6 +39,7 @@ const LOCK_ACQUIRE_RETRY_MS = 5;
|
|
|
39
39
|
const LOCK_ACQUIRE_MAX_RETRY_MS = 100;
|
|
40
40
|
const LOCK_ACQUIRE_TIMEOUT_MS = 2_000;
|
|
41
41
|
const LOCK_STALE_MS = 30_000;
|
|
42
|
+
const RELEASED_TRANSITION_GRACE_MS = 1_000;
|
|
42
43
|
|
|
43
44
|
interface LockRetryBudget {
|
|
44
45
|
startedAt: number;
|
|
@@ -54,6 +55,10 @@ function lockRetryElapsedMs(budget: LockRetryBudget): number {
|
|
|
54
55
|
return Math.max(0, performance.now() - budget.startedAt);
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
function lockRetryExhausted(budget: LockRetryBudget): boolean {
|
|
59
|
+
return lockRetryElapsedMs(budget) >= LOCK_ACQUIRE_TIMEOUT_MS;
|
|
60
|
+
}
|
|
61
|
+
|
|
57
62
|
async function waitForLockRetry(budget: LockRetryBudget): Promise<boolean> {
|
|
58
63
|
budget.attempts++;
|
|
59
64
|
const remainingMs = LOCK_ACQUIRE_TIMEOUT_MS - lockRetryElapsedMs(budget);
|
|
@@ -77,8 +82,8 @@ async function waitForLockRetry(budget: LockRetryBudget): Promise<boolean> {
|
|
|
77
82
|
* owner record is the sibling `<file>.lock.transition.owner`. `mkdir` admits exactly one
|
|
78
83
|
* contender, while the held directory prevents a successor until release unlinks the
|
|
79
84
|
* validated sidecar and atomically `rmdir`s the claim. A crash before either step leaves a
|
|
80
|
-
*
|
|
81
|
-
*
|
|
85
|
+
* directory that a successor may reclaim only with proof: a host-qualified owner whose pid
|
|
86
|
+
* is proven dead, plus an exact directory-generation and tree match at removal time.
|
|
82
87
|
*
|
|
83
88
|
* The separate sidecar keeps the claim directory empty, which is what makes `rmdir` the
|
|
84
89
|
* identity-safe portable release primitive. The path stays distinct from `<file>.lock`
|
|
@@ -409,6 +414,8 @@ export const SessionStateLockTestHooks: {
|
|
|
409
414
|
forcedQuarantineName?: string;
|
|
410
415
|
/** @internal Observes bounded acquisition retries without changing their timing. */
|
|
411
416
|
afterAcquireContention?: (lockFile: string, attempt: number, elapsedMs: number) => void;
|
|
417
|
+
/** @internal Runs after transition mkdir contention and before stale-claim inspection. */
|
|
418
|
+
afterTransitionClaimContention?: (transitionDir: string) => void | Promise<void>;
|
|
412
419
|
} = {};
|
|
413
420
|
|
|
414
421
|
/** Raised when the lock could not be acquired; callers map it to their own refusal. */
|
|
@@ -464,6 +471,112 @@ export class SessionStateLockUnavailableError extends Error {
|
|
|
464
471
|
}
|
|
465
472
|
}
|
|
466
473
|
|
|
474
|
+
/**
|
|
475
|
+
* Log fields describing a persist failure: the error text plus, when a session state
|
|
476
|
+
* lock refusal sits anywhere in the cause chain, its typed `reason` and `lockPath`.
|
|
477
|
+
* Wrapping errors (`PreviousRuntimeStateReadError`, `AggregateError`) are searched
|
|
478
|
+
* so the actionable detail is never buried behind a generic message.
|
|
479
|
+
*/
|
|
480
|
+
export function sessionStateLockFailureFields(error: unknown): {
|
|
481
|
+
error: string;
|
|
482
|
+
reason?: SessionStateLockUnavailableReason;
|
|
483
|
+
lockPath?: string;
|
|
484
|
+
} {
|
|
485
|
+
const fields: { error: string; reason?: SessionStateLockUnavailableReason; lockPath?: string } = {
|
|
486
|
+
error: String(error),
|
|
487
|
+
};
|
|
488
|
+
const seen = new Set<unknown>();
|
|
489
|
+
const queue: unknown[] = [error];
|
|
490
|
+
while (queue.length > 0) {
|
|
491
|
+
const current = queue.shift();
|
|
492
|
+
if (current === null || typeof current !== "object" || seen.has(current)) continue;
|
|
493
|
+
seen.add(current);
|
|
494
|
+
if (current instanceof SessionStateLockUnavailableError) {
|
|
495
|
+
if (current.reason !== undefined) fields.reason = current.reason;
|
|
496
|
+
if (current.lockPath !== undefined) fields.lockPath = current.lockPath;
|
|
497
|
+
if (fields.reason !== undefined && fields.lockPath !== undefined) return fields;
|
|
498
|
+
}
|
|
499
|
+
if (current instanceof AggregateError) queue.push(...current.errors);
|
|
500
|
+
if ("cause" in current) queue.push((current as { cause?: unknown }).cause);
|
|
501
|
+
}
|
|
502
|
+
return fields;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function boundedPersistFailureClassPart(value: unknown, fallback: string): string {
|
|
506
|
+
if (typeof value !== "string" && typeof value !== "number") return fallback;
|
|
507
|
+
const normalized = String(value)
|
|
508
|
+
.replace(/[^A-Za-z0-9_.-]/gu, "_")
|
|
509
|
+
.slice(0, 64);
|
|
510
|
+
return normalized || fallback;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function persistFailureProperty(value: unknown, name: string): unknown {
|
|
514
|
+
if (value === null || (typeof value !== "object" && typeof value !== "function")) return undefined;
|
|
515
|
+
try {
|
|
516
|
+
return Reflect.get(value, name);
|
|
517
|
+
} catch {
|
|
518
|
+
return undefined;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function persistFailureConstructorName(error: unknown): string {
|
|
523
|
+
if (error === null) return "null";
|
|
524
|
+
if (typeof error !== "object" && typeof error !== "function") return typeof error;
|
|
525
|
+
const ctor = persistFailureProperty(error, "constructor");
|
|
526
|
+
const name = persistFailureProperty(ctor, "name");
|
|
527
|
+
return typeof name === "string" && name.length > 0 ? name : "Object";
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Build a bounded warn-window partition without including arbitrary error text. Typed lock
|
|
532
|
+
* refusals use their stable reason; all other failures use the root constructor and the
|
|
533
|
+
* first structured `code`/`errno` found in the error/cause chain.
|
|
534
|
+
*/
|
|
535
|
+
function persistFailureClass(error: unknown): string {
|
|
536
|
+
const lockFields = sessionStateLockFailureFields(error);
|
|
537
|
+
if (lockFields.reason !== undefined) return `reason:${lockFields.reason}`;
|
|
538
|
+
const seen = new Set<unknown>();
|
|
539
|
+
const queue: unknown[] = [error];
|
|
540
|
+
let code: unknown;
|
|
541
|
+
let errno: unknown;
|
|
542
|
+
while (queue.length > 0) {
|
|
543
|
+
const current = queue.shift();
|
|
544
|
+
if (current === null || typeof current !== "object" || seen.has(current)) continue;
|
|
545
|
+
seen.add(current);
|
|
546
|
+
code ??= persistFailureProperty(current, "code");
|
|
547
|
+
errno ??= persistFailureProperty(current, "errno");
|
|
548
|
+
if (code !== undefined && errno !== undefined) break;
|
|
549
|
+
if (current instanceof AggregateError) queue.push(...current.errors);
|
|
550
|
+
const cause = persistFailureProperty(current, "cause");
|
|
551
|
+
if (cause !== undefined) queue.push(cause);
|
|
552
|
+
}
|
|
553
|
+
return `error:${boundedPersistFailureClassPart(persistFailureConstructorName(error), "Object")}:code=${boundedPersistFailureClassPart(code, "none")}:errno=${boundedPersistFailureClassPart(errno, "none")}`;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export const PERSIST_FAILURE_WARN_WINDOW_MS = 30_000;
|
|
557
|
+
const persistFailureLastWarnedAt = new Map<string, number>();
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Whether a persist failure should be logged at warn level. The first failure for each
|
|
561
|
+
* document and stable failure class in a 30s window warns; repeats are the caller's cue
|
|
562
|
+
* to log at debug instead.
|
|
563
|
+
*/
|
|
564
|
+
export function shouldWarnPersistFailure(key: string, error: unknown, now = Date.now()): boolean {
|
|
565
|
+
for (const [otherKey, warnedAt] of persistFailureLastWarnedAt) {
|
|
566
|
+
if (now - warnedAt >= PERSIST_FAILURE_WARN_WINDOW_MS) persistFailureLastWarnedAt.delete(otherKey);
|
|
567
|
+
}
|
|
568
|
+
const scopedKey = `${key}\u0000${persistFailureClass(error)}`;
|
|
569
|
+
const last = persistFailureLastWarnedAt.get(scopedKey);
|
|
570
|
+
if (last !== undefined && now - last < PERSIST_FAILURE_WARN_WINDOW_MS) return false;
|
|
571
|
+
persistFailureLastWarnedAt.set(scopedKey, now);
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** @internal Test seam: forget every persist-failure warn window. */
|
|
576
|
+
export function resetPersistFailureWarnWindows(): void {
|
|
577
|
+
persistFailureLastWarnedAt.clear();
|
|
578
|
+
}
|
|
579
|
+
|
|
467
580
|
function lockUnavailable(
|
|
468
581
|
lockPath: string,
|
|
469
582
|
reason: SessionStateLockUnavailableReason,
|
|
@@ -1308,7 +1421,13 @@ async function acquireOwnerLock(
|
|
|
1308
1421
|
} catch {
|
|
1309
1422
|
throw error;
|
|
1310
1423
|
}
|
|
1311
|
-
if (!validLockOwner(observed)
|
|
1424
|
+
if (!validLockOwner(observed)) throw error;
|
|
1425
|
+
if (observed.released !== true) {
|
|
1426
|
+
if (await lockOwnerIsAlive(observed)) throw error;
|
|
1427
|
+
const removed = exactUnlinkOwnerRecord(file, current, quarantineName);
|
|
1428
|
+
if (removed !== "removed" && removed !== "absent") throw error;
|
|
1429
|
+
return await createOwnerLock(file, owner, quarantineName);
|
|
1430
|
+
}
|
|
1312
1431
|
try {
|
|
1313
1432
|
return await rewriteHeldOwnerRecord(file, current, owner);
|
|
1314
1433
|
} catch (error) {
|
|
@@ -1330,7 +1449,10 @@ async function acquireOwnerLock(
|
|
|
1330
1449
|
*
|
|
1331
1450
|
* Stale-owner reclaim still uses the native identity-bound detach protocol because no live
|
|
1332
1451
|
* owner or transition claim can vouch for that pathname. A mismatch here likewise leaves a
|
|
1333
|
-
* successor strictly alone.
|
|
1452
|
+
* successor strictly alone. A released tombstone is reclaimable on every platform when its
|
|
1453
|
+
* owner host is this installation and the transition directory is proven empty and unchanged:
|
|
1454
|
+
* the tombstone is the release marker, while the directory generation binds that marker to
|
|
1455
|
+
* the exact claim that release failed to remove.
|
|
1334
1456
|
*/
|
|
1335
1457
|
async function releaseOwnerLock(file: string, held: LockOwnerSnapshot): Promise<void> {
|
|
1336
1458
|
let owner: unknown;
|
|
@@ -1378,6 +1500,7 @@ async function releaseOwnerLock(file: string, held: LockOwnerSnapshot): Promise<
|
|
|
1378
1500
|
type SessionStateLockReclaimResult =
|
|
1379
1501
|
| "absent_or_changed"
|
|
1380
1502
|
| "legacy_directory_unprovenanced"
|
|
1503
|
+
| "owner_removed"
|
|
1381
1504
|
| "owner_live_or_unverifiable"
|
|
1382
1505
|
| "owner_record_fresh"
|
|
1383
1506
|
| "owner_unprovenanced"
|
|
@@ -1416,7 +1539,8 @@ async function reclaimStaleOwnerRecord(
|
|
|
1416
1539
|
// A successor that took the path in the final window keeps it; this call just loses.
|
|
1417
1540
|
if (outcome === "refused")
|
|
1418
1541
|
throw new SessionStateLockUnavailableError(new Error("Stale owner record could not be reclaimed."));
|
|
1419
|
-
|
|
1542
|
+
if (outcome === "removed") return "owner_removed";
|
|
1543
|
+
return "absent_or_changed";
|
|
1420
1544
|
}
|
|
1421
1545
|
|
|
1422
1546
|
/**
|
|
@@ -1548,14 +1672,18 @@ async function releaseTransitionClaim(
|
|
|
1548
1672
|
}
|
|
1549
1673
|
}
|
|
1550
1674
|
|
|
1551
|
-
async function reclaimStaleTransitionClaim(transitionDir: string, quarantineName: string): Promise<
|
|
1552
|
-
|
|
1553
|
-
|
|
1675
|
+
async function reclaimStaleTransitionClaim(transitionDir: string, quarantineName: string): Promise<boolean> {
|
|
1676
|
+
let stat: fsSync.BigIntStats;
|
|
1677
|
+
try {
|
|
1678
|
+
stat = await fs.lstat(transitionDir, { bigint: true });
|
|
1679
|
+
} catch {
|
|
1680
|
+
return false;
|
|
1681
|
+
}
|
|
1554
1682
|
// Regular-file claims belong to the superseded protocol. They retain the old
|
|
1555
1683
|
// exact-identity stale path; released PID-1 tombstones deliberately require
|
|
1556
1684
|
// explicit cleanup before this atomic-directory protocol can take over.
|
|
1557
1685
|
if (stat.isFile()) {
|
|
1558
|
-
await reclaimStaleOwnerRecord(
|
|
1686
|
+
const reclaimed = await reclaimStaleOwnerRecord(
|
|
1559
1687
|
transitionDir,
|
|
1560
1688
|
{
|
|
1561
1689
|
afterInspection: SessionStateLockTestHooks.afterTransitionStaleInspection,
|
|
@@ -1563,12 +1691,78 @@ async function reclaimStaleTransitionClaim(transitionDir: string, quarantineName
|
|
|
1563
1691
|
},
|
|
1564
1692
|
quarantineName,
|
|
1565
1693
|
);
|
|
1566
|
-
return;
|
|
1694
|
+
return reclaimed === "owner_removed";
|
|
1567
1695
|
}
|
|
1568
1696
|
if (!stat.isDirectory()) throw new SessionStateLockUnavailableError();
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1697
|
+
const ownerSnapshot = await captureRegularLockOwner(`${transitionDir}.owner`);
|
|
1698
|
+
if (!ownerSnapshot) return false;
|
|
1699
|
+
let owner: unknown;
|
|
1700
|
+
try {
|
|
1701
|
+
owner = JSON.parse(ownerSnapshot.bytes);
|
|
1702
|
+
} catch {
|
|
1703
|
+
return false;
|
|
1704
|
+
}
|
|
1705
|
+
if (!validLockOwner(owner)) return false;
|
|
1706
|
+
await SessionStateLockTestHooks.afterTransitionStaleInspection?.(transitionDir);
|
|
1707
|
+
if (owner.released === true) {
|
|
1708
|
+
// A released record is safe only when it is qualified by this installation.
|
|
1709
|
+
// Never let a shared-volume tombstone, an absent identity, or a legacy foreign
|
|
1710
|
+
// identity authorize removal of a claim this process cannot own.
|
|
1711
|
+
if (owner.owner_host_id === undefined) return false;
|
|
1712
|
+
const currentHost = await currentOwnerHostId();
|
|
1713
|
+
const legacyHost = await currentLegacyOwnerHostId();
|
|
1714
|
+
if (owner.owner_host_id !== currentHost && owner.owner_host_id !== legacyHost) return false;
|
|
1715
|
+
if (Date.now() - Number(ownerSnapshot.mtimeNs / 1_000_000n) < RELEASED_TRANSITION_GRACE_MS) return false;
|
|
1716
|
+
} else if (await lockOwnerIsAlive(owner)) {
|
|
1717
|
+
// Only a host-qualified owner whose pid is PROVEN dead (ESRCH, or a live pid
|
|
1718
|
+
// with a provably different incarnation) authorizes reclaim. The generation
|
|
1719
|
+
// + exact-tree checks below then bind the removal to the very directory
|
|
1720
|
+
// inspected here, on every platform; a claim stranded by a force-quit
|
|
1721
|
+
// (`postmortem.quit`) would otherwise wall the session directory forever.
|
|
1722
|
+
return false;
|
|
1723
|
+
}
|
|
1724
|
+
const generation = transitionGenerationFromStat(stat);
|
|
1725
|
+
const nativePath = await canonicalOwnedTransitionPath(transitionDir).catch(() => null);
|
|
1726
|
+
if (!nativePath) return false;
|
|
1727
|
+
const captured = nativeSessionStateLock().snapshotDirectoryTree(nativePath);
|
|
1728
|
+
if (!captured.ok || !captured.snapshot) return false;
|
|
1729
|
+
const root = captured.snapshot.entries.find(entry => entry.relativePath === "");
|
|
1730
|
+
if (
|
|
1731
|
+
!root ||
|
|
1732
|
+
root.kind !== "directory" ||
|
|
1733
|
+
root.dev !== String(generation.dev) ||
|
|
1734
|
+
root.ino !== String(generation.ino) ||
|
|
1735
|
+
root.nlink !== String(generation.nlink) ||
|
|
1736
|
+
root.mtimeNs !== String(generation.mtimeNs) ||
|
|
1737
|
+
root.ctimeNs !== String(generation.ctimeNs)
|
|
1738
|
+
)
|
|
1739
|
+
return false;
|
|
1740
|
+
if (captured.snapshot.entries.length !== 1) return false;
|
|
1741
|
+
const removed = nativeSessionStateLock().exactRemoveDirectoryTree(nativePath, captured.snapshot);
|
|
1742
|
+
if (removed.ok || removed.code === "not_found") {
|
|
1743
|
+
exactUnlinkOwnerRecord(`${transitionDir}.owner`, ownerSnapshot, quarantineName);
|
|
1744
|
+
return removed.ok;
|
|
1745
|
+
}
|
|
1746
|
+
if (
|
|
1747
|
+
removed.code === "cleanup_pending" &&
|
|
1748
|
+
removed.payloadDurable === true &&
|
|
1749
|
+
removed.detachedPath === `${nativePath}.removing` &&
|
|
1750
|
+
removed.retainedSuccessorPath === undefined &&
|
|
1751
|
+
removed.retainedUnknownPath === undefined &&
|
|
1752
|
+
removed.retainedPlaceholderPath === undefined
|
|
1753
|
+
) {
|
|
1754
|
+
// The captured claim is proven empty, so the native detach receipt leaves an
|
|
1755
|
+
// empty directory at detachedPath. Remove that exact directory directly: calling
|
|
1756
|
+
// the detach primitive again would deterministically create a second `.removing`
|
|
1757
|
+
// collision on POSIX. rmdir never follows a directory's contents and refuses if a
|
|
1758
|
+
// successor populated the detached path.
|
|
1759
|
+
await removeTransitionDir(removed.detachedPath);
|
|
1760
|
+
exactUnlinkOwnerRecord(`${transitionDir}.owner`, ownerSnapshot, quarantineName);
|
|
1761
|
+
return true;
|
|
1762
|
+
}
|
|
1763
|
+
throw new SessionStateLockUnavailableError(
|
|
1764
|
+
new Error(`Stale transition claim could not be reclaimed (${removed.code ?? "unknown"}).`),
|
|
1765
|
+
);
|
|
1572
1766
|
}
|
|
1573
1767
|
|
|
1574
1768
|
/** Run one pathname transition under an atomic `mkdir`/`rmdir` claim. */
|
|
@@ -1828,7 +2022,12 @@ async function withLockPathTransition<T>(
|
|
|
1828
2022
|
} catch (error) {
|
|
1829
2023
|
const code = (error as NodeJS.ErrnoException).code;
|
|
1830
2024
|
if (code !== "EEXIST" && !isTransientLockError(error)) throw new SessionStateLockUnavailableError(error);
|
|
1831
|
-
await
|
|
2025
|
+
await SessionStateLockTestHooks.afterTransitionClaimContention?.(transitionDir);
|
|
2026
|
+
if (await reclaimStaleTransitionClaim(transitionDir, quarantineName)) {
|
|
2027
|
+
if (lockRetryExhausted(budget))
|
|
2028
|
+
throw lockUnavailable(transitionDir, "transition_claim_timeout", budget, error);
|
|
2029
|
+
continue;
|
|
2030
|
+
}
|
|
1832
2031
|
if (!(await waitForLockRetry(budget)))
|
|
1833
2032
|
throw lockUnavailable(transitionDir, "transition_claim_timeout", budget, error);
|
|
1834
2033
|
continue;
|