@gajae-code/coding-agent 0.16.4 → 0.16.6
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 +54 -0
- package/dist/types/capability/types.d.ts +4 -0
- package/dist/types/cli/customize-doctor.d.ts +22 -0
- package/dist/types/config/file-lock.d.ts +31 -3
- package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
- package/dist/types/coordinator-mcp/server.d.ts +7 -0
- package/dist/types/discovery/helpers.d.ts +33 -0
- package/dist/types/exec/bash-executor.d.ts +17 -1
- package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
- package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-signals.d.ts +2 -0
- package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
- package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
- package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
- package/dist/types/exec/bash-shell-worker.d.ts +1 -0
- package/dist/types/exec/isolated-shell.d.ts +19 -0
- package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
- package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/runner.d.ts +46 -6
- package/dist/types/extensibility/extensions/types.d.ts +16 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
- package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
- package/dist/types/extensibility/hooks/types.d.ts +4 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +4 -1
- package/dist/types/extensibility/skill-management.d.ts +9 -4
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
- package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +12 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
- package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
- package/dist/types/modes/components/bash-execution.d.ts +1 -0
- package/dist/types/modes/components/execution-shared.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/sdk/broker/ensure.d.ts +32 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
- package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
- package/dist/types/sdk/client/client.d.ts +6 -0
- package/dist/types/sdk/host/host.d.ts +3 -1
- package/dist/types/sdk/host/session-runtime.d.ts +4 -2
- package/dist/types/sdk/lifecycle/service.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +10 -2
- package/dist/types/sdk/session.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/fallback-chain-controller.d.ts +15 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/setup/paseo/announce.d.ts +4 -3
- package/dist/types/skill-state/canonical-skills.d.ts +4 -0
- package/dist/types/system-prompt.d.ts +8 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/github-cache.d.ts +2 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/package.json +9 -7
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/src/capability/index.ts +14 -2
- package/src/capability/types.ts +4 -0
- package/src/cli/customize-doctor.ts +155 -12
- package/src/cli.ts +22 -0
- package/src/commands/sdk.ts +74 -31
- package/src/config/file-lock-gc.ts +40 -2
- package/src/config/file-lock.ts +198 -14
- package/src/config/model-preset-registry.ts +115 -16
- package/src/config/model-registry.ts +16 -12
- package/src/coordinator-mcp/question-state.ts +33 -2
- package/src/coordinator-mcp/server.ts +477 -101
- package/src/crash/sanitize.ts +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
- package/src/discovery/builtin.ts +50 -62
- package/src/discovery/helpers.ts +297 -32
- package/src/exec/bash-executor.ts +330 -50
- package/src/exec/bash-shell-guardian.ts +388 -0
- package/src/exec/bash-shell-runtime-entry.ts +5 -0
- package/src/exec/bash-shell-signals.ts +29 -0
- package/src/exec/bash-shell-supervisor-entry.ts +3 -0
- package/src/exec/bash-shell-supervisor.ts +359 -0
- package/src/exec/bash-shell-worker-entry.ts +5 -0
- package/src/exec/bash-shell-worker-protocol.ts +36 -0
- package/src/exec/bash-shell-worker.ts +116 -0
- package/src/exec/isolated-shell.ts +393 -0
- package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
- package/src/extensibility/extensions/function-hooks.ts +959 -0
- package/src/extensibility/extensions/index.ts +22 -0
- package/src/extensibility/extensions/loader.ts +53 -2
- package/src/extensibility/extensions/runner.ts +895 -264
- package/src/extensibility/extensions/types.ts +42 -0
- package/src/extensibility/extensions/wrapper.ts +56 -15
- package/src/extensibility/gjc-plugins/compiler.ts +20 -2
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
- package/src/extensibility/gjc-plugins/registry.ts +113 -1
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
- package/src/extensibility/gjc-plugins/schema.ts +121 -4
- package/src/extensibility/gjc-plugins/types.ts +25 -0
- package/src/extensibility/hooks/loader.ts +133 -16
- package/src/extensibility/hooks/types.ts +28 -0
- package/src/extensibility/runtime-skill-discovery.ts +80 -47
- package/src/extensibility/skill-management.ts +200 -52
- package/src/extensibility/skills.ts +29 -9
- package/src/gjc-runtime/empty-delete-gc.ts +94 -4
- package/src/gjc-runtime/gc-runtime.ts +29 -0
- package/src/gjc-runtime/linux-proc.ts +14 -0
- package/src/gjc-runtime/session-state-lock.ts +51 -8
- package/src/gjc-runtime/session-state-sidecar.ts +27 -11
- package/src/gjc-runtime/state-writer.ts +30 -29
- package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
- package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -0
- package/src/harness-control-plane/session-lease.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/lsp/config.ts +10 -3
- package/src/lsp/index.ts +1 -1
- package/src/lsp/path-trust.ts +69 -14
- package/src/main.ts +6 -1
- package/src/modes/acp/acp-agent.ts +16 -3
- package/src/modes/components/bash-execution.ts +4 -1
- package/src/modes/components/execution-shared.ts +4 -1
- package/src/modes/components/session-observer-overlay.ts +1 -1
- package/src/modes/components/tool-status-header.ts +3 -1
- package/src/modes/controllers/command-controller.ts +1 -0
- package/src/modes/controllers/event-controller.ts +3 -2
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +6 -3
- package/src/modes/interactive-mode.ts +6 -5
- package/src/modes/print-mode.ts +5 -1
- package/src/modes/prompt-action-autocomplete.ts +54 -27
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/types.ts +16 -3
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/tools/subagent.md +1 -0
- package/src/runtime-mcp/manager.ts +20 -2
- package/src/runtime-mcp/pool.test.ts +20 -11
- package/src/sdk/broker/broker.ts +119 -28
- package/src/sdk/broker/ensure.ts +248 -41
- package/src/sdk/broker/lifecycle-ledger.ts +7 -17
- package/src/sdk/broker/lifecycle.ts +56 -6
- package/src/sdk/broker/startup-failure.ts +15 -6
- package/src/sdk/bus/chat-daemon-control.ts +3 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +35 -5
- package/src/sdk/bus/chat-effect-journal.ts +132 -7
- package/src/sdk/bus/conversation-store.ts +29 -0
- package/src/sdk/bus/lifecycle-commands.ts +17 -11
- package/src/sdk/bus/slack-conversation.ts +1 -0
- package/src/sdk/bus/slack-daemon.ts +387 -48
- package/src/sdk/bus/telegram-daemon.ts +16 -2
- package/src/sdk/bus/topic-registry.ts +19 -0
- package/src/sdk/cli/master-cli.ts +1 -1
- package/src/sdk/cli/session-cli.ts +2 -2
- package/src/sdk/client/client.ts +66 -17
- package/src/sdk/host/host.ts +29 -9
- package/src/sdk/host/query/revision-store.ts +19 -13
- package/src/sdk/host/reverse-leases.ts +7 -4
- package/src/sdk/host/session-runtime.test.ts +50 -8
- package/src/sdk/host/session-runtime.ts +158 -51
- package/src/sdk/lifecycle/service.ts +4 -0
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/router/session-router.ts +89 -38
- package/src/sdk/session.ts +155 -26
- package/src/sdk/transport/relay.ts +35 -13
- package/src/session/agent-session.ts +117 -52
- package/src/session/fallback-chain-controller.ts +38 -0
- package/src/session/internal/managed-session-storage.ts +19 -5
- package/src/session/messages.ts +5 -0
- package/src/session/session-manager.ts +29 -5
- package/src/session-import/redact.ts +5 -1
- package/src/setup/paseo/announce.ts +49 -43
- package/src/setup/paseo/setup-deps.ts +1 -1
- package/src/skill-state/canonical-skills.ts +12 -0
- package/src/skills/skills.test.ts +41 -21
- package/src/system-prompt.ts +25 -3
- package/src/task/executor.ts +5 -0
- package/src/task/index.ts +4 -0
- package/src/tools/bash.ts +28 -11
- package/src/tools/gh.ts +26 -3
- package/src/tools/github-cache.ts +68 -16
- package/src/tools/index.ts +2 -0
- package/src/tools/job.ts +13 -2
- package/src/tools/read.ts +14 -2
- package/src/tools/render-utils.ts +1 -3
- package/src/tools/resource-gc.ts +28 -8
- package/src/tools/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +15 -0
- package/src/tools/subagent.ts +3 -2
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/tools/ultragoal-ask-guard.ts +2 -2
package/src/commands/sdk.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { initTheme } from "../modes/theme/theme";
|
|
|
13
13
|
import { ACP_MCP_REQUEST_TIMEOUT_MS, ACP_MCP_STARTUP_HEADROOM_MS } from "../sdk/acp/mcp";
|
|
14
14
|
import { Broker } from "../sdk/broker/broker";
|
|
15
15
|
import { readBrokerDiscovery } from "../sdk/broker/discovery";
|
|
16
|
+
import { reconcileBrokerGenerationForStartup, withBrokerStartupLock } from "../sdk/broker/ensure";
|
|
16
17
|
import { completeBrokerProcess } from "../sdk/broker/internal";
|
|
17
18
|
import {
|
|
18
19
|
type LifecycleTranscriptEvidence,
|
|
@@ -1086,59 +1087,101 @@ export default class Sdk extends Command {
|
|
|
1086
1087
|
await runSessionHost();
|
|
1087
1088
|
return;
|
|
1088
1089
|
}
|
|
1089
|
-
const agentDir = internal.agentDir;
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1090
|
+
const agentDir = path.resolve(internal.agentDir);
|
|
1091
|
+
let broker: Broker | undefined;
|
|
1092
|
+
try {
|
|
1093
|
+
broker = await withBrokerStartupLock(agentDir, async deadline => {
|
|
1094
|
+
const remainingMs = Math.max(1, deadline - Date.now());
|
|
1095
|
+
const testWatchdogMs = Number(process.env.GJC_SDK_TEST_BROKER_STARTUP_WATCHDOG_MS ?? 0);
|
|
1096
|
+
const watchdogMs =
|
|
1097
|
+
Number.isSafeInteger(testWatchdogMs) && testWatchdogMs > 0 && testWatchdogMs <= remainingMs
|
|
1098
|
+
? testWatchdogMs
|
|
1099
|
+
: remainingMs;
|
|
1100
|
+
const startupWatchdog = setTimeout(() => {
|
|
1101
|
+
process.stderr.write(`SDK broker startup exceeded its ${watchdogMs}ms fence deadline.\n`);
|
|
1102
|
+
process.exit(1);
|
|
1103
|
+
}, watchdogMs);
|
|
1097
1104
|
try {
|
|
1098
|
-
|
|
1099
|
-
|
|
1105
|
+
if (await reconcileBrokerGenerationForStartup({ agentDir }, deadline)) return undefined;
|
|
1106
|
+
if (process.env.GJC_SDK_TEST_BROKER_STARTUP_STALL === "1") {
|
|
1107
|
+
const stalled = Promise.withResolvers<void>();
|
|
1108
|
+
await stalled.promise;
|
|
1109
|
+
}
|
|
1110
|
+
const startupDelayMs = Number(process.env.GJC_SDK_TEST_BROKER_STARTUP_DELAY_MS ?? 0);
|
|
1111
|
+
if (Number.isSafeInteger(startupDelayMs) && startupDelayMs > 0 && startupDelayMs <= 10_000)
|
|
1112
|
+
await Bun.sleep(startupDelayMs);
|
|
1113
|
+
const candidate = new Broker({
|
|
1114
|
+
agentDir,
|
|
1115
|
+
masterOrphanGraceMs: (await Settings.loadForScope({ cwd: process.cwd(), agentDir })).get(
|
|
1116
|
+
"sdk.masterOrphanGraceMs",
|
|
1117
|
+
),
|
|
1118
|
+
resolveDirectoryMigration: async cwd => {
|
|
1119
|
+
const settings = await Settings.loadForScope({ cwd, agentDir });
|
|
1120
|
+
try {
|
|
1121
|
+
const policy = settings.get("session.directoryMigration");
|
|
1122
|
+
return policy === "disabled" ? "disabled" : "copy-retain";
|
|
1123
|
+
} finally {
|
|
1124
|
+
await settings.close();
|
|
1125
|
+
}
|
|
1126
|
+
},
|
|
1127
|
+
});
|
|
1128
|
+
broker = candidate;
|
|
1129
|
+
await candidate.start();
|
|
1130
|
+
return candidate;
|
|
1100
1131
|
} finally {
|
|
1101
|
-
|
|
1132
|
+
clearTimeout(startupWatchdog);
|
|
1102
1133
|
}
|
|
1103
|
-
}
|
|
1104
|
-
});
|
|
1105
|
-
try {
|
|
1106
|
-
await broker.start();
|
|
1134
|
+
});
|
|
1107
1135
|
} catch (error) {
|
|
1136
|
+
if (broker) {
|
|
1137
|
+
try {
|
|
1138
|
+
await broker.stop();
|
|
1139
|
+
} catch {
|
|
1140
|
+
// Preserve the startup/fence failure that made this bootstrap unusable.
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1108
1143
|
// This process spawns detached with stdio ignored (see ensure.ts), so the
|
|
1109
1144
|
// durable marker is the only channel the caller has to see why start()
|
|
1110
1145
|
// failed instead of a bare exit code (#3963).
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1146
|
+
const incarnation = processIncarnation(process.pid);
|
|
1147
|
+
if (incarnation)
|
|
1148
|
+
await writeBrokerStartupFailureMarker(agentDir, {
|
|
1149
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1150
|
+
exitCode: 1,
|
|
1151
|
+
signal: null,
|
|
1152
|
+
pid: process.pid,
|
|
1153
|
+
incarnation,
|
|
1154
|
+
});
|
|
1117
1155
|
throw error;
|
|
1118
1156
|
}
|
|
1119
|
-
if (!broker
|
|
1157
|
+
if (!broker) return;
|
|
1158
|
+
const runningBroker = broker;
|
|
1159
|
+
if (!runningBroker.ownsDiscovery) {
|
|
1120
1160
|
// Another broker owns discovery; this process exits cleanly (code 0) as
|
|
1121
1161
|
// the race loser. Record why so a caller polling for a winner that never
|
|
1122
1162
|
// appears can diagnose the loss instead of seeing only a bare exit 0.
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1163
|
+
const incarnation = processIncarnation(process.pid);
|
|
1164
|
+
if (incarnation)
|
|
1165
|
+
await writeBrokerStartupFailureMarker(agentDir, {
|
|
1166
|
+
reason: "Another broker owns the lock/discovery; this broker exited as the race loser.",
|
|
1167
|
+
exitCode: 0,
|
|
1168
|
+
signal: null,
|
|
1169
|
+
pid: process.pid,
|
|
1170
|
+
incarnation,
|
|
1171
|
+
});
|
|
1129
1172
|
return;
|
|
1130
1173
|
}
|
|
1131
1174
|
// A live broker must not keep advertising sessions whose host process is
|
|
1132
1175
|
// gone; the sweep is the broker-side half of the host reaping bound.
|
|
1133
|
-
const stopSweep = startBrokerDeadRegistrationSweep(
|
|
1176
|
+
const stopSweep = startBrokerDeadRegistrationSweep(runningBroker);
|
|
1134
1177
|
const stop = () => {
|
|
1135
1178
|
stopSweep();
|
|
1136
|
-
void
|
|
1179
|
+
void runningBroker.stop();
|
|
1137
1180
|
};
|
|
1138
1181
|
process.once("SIGTERM", stop);
|
|
1139
1182
|
process.once("SIGINT", stop);
|
|
1140
1183
|
try {
|
|
1141
|
-
await completeBrokerProcess(
|
|
1184
|
+
await completeBrokerProcess(runningBroker);
|
|
1142
1185
|
} finally {
|
|
1143
1186
|
stopSweep();
|
|
1144
1187
|
}
|
|
@@ -17,7 +17,12 @@ import type {
|
|
|
17
17
|
} from "../gjc-runtime/gc-runtime";
|
|
18
18
|
import { gcPidStatusLabel } from "../gjc-runtime/gc-runtime";
|
|
19
19
|
import { resolveReceiptSpoolDir } from "../harness-control-plane/receipt-spool";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
fileLockStagingOwnerPid,
|
|
22
|
+
inspectFileLockStagingDir,
|
|
23
|
+
readFileLockObservationForGc,
|
|
24
|
+
removeFileLockDirForGc,
|
|
25
|
+
} from "./file-lock";
|
|
21
26
|
|
|
22
27
|
const MAX_WALK_DEPTH = 6;
|
|
23
28
|
/** Default per-root walk budget. Truncation is a warning, not a hard error. */
|
|
@@ -67,7 +72,27 @@ function keptMalformedRecord(lockDir: string): GcRecord {
|
|
|
67
72
|
};
|
|
68
73
|
}
|
|
69
74
|
|
|
75
|
+
function stagingOwnerStatus(ctx: GcContext, pid: number): "alive" | "dead" | "unknown" {
|
|
76
|
+
const status = gcPidStatusLabel(ctx.probe(pid));
|
|
77
|
+
return status === "eperm" ? "unknown" : status;
|
|
78
|
+
}
|
|
79
|
+
|
|
70
80
|
async function collectLockRecord(lockDir: string, ctx: GcContext): Promise<GcRecord> {
|
|
81
|
+
if (fileLockStagingOwnerPid(path.basename(lockDir)) !== null) {
|
|
82
|
+
const staging = await inspectFileLockStagingDir(lockDir, pid => stagingOwnerStatus(ctx, pid));
|
|
83
|
+
return {
|
|
84
|
+
store: "file_locks",
|
|
85
|
+
id: lockDir,
|
|
86
|
+
path: lockDir,
|
|
87
|
+
pid: staging.pid,
|
|
88
|
+
pid_status: staging.status,
|
|
89
|
+
status: "file_lock_staging_orphan",
|
|
90
|
+
stale: staging.status === "dead",
|
|
91
|
+
removable: staging.status === "dead",
|
|
92
|
+
action: "none",
|
|
93
|
+
reason: staging.reason,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
71
96
|
const observation = await readFileLockObservationForGc(lockDir);
|
|
72
97
|
const info = observation?.info;
|
|
73
98
|
if (!info) return keptMalformedRecord(lockDir);
|
|
@@ -131,7 +156,7 @@ async function walkForLockDirs(
|
|
|
131
156
|
state.entries++;
|
|
132
157
|
if (!stat.isDirectory() || stat.isSymbolicLink()) return;
|
|
133
158
|
|
|
134
|
-
if (path.basename(dir).endsWith(".lock")) {
|
|
159
|
+
if (path.basename(dir).endsWith(".lock") || fileLockStagingOwnerPid(path.basename(dir)) !== null) {
|
|
135
160
|
lockDirs.add(dir);
|
|
136
161
|
return;
|
|
137
162
|
}
|
|
@@ -152,6 +177,11 @@ async function walkForLockDirs(
|
|
|
152
177
|
state.truncated = true;
|
|
153
178
|
return;
|
|
154
179
|
}
|
|
180
|
+
// Session index locks live directly in sessions/, but session payload trees remain pruned.
|
|
181
|
+
if (entry === "sessions") {
|
|
182
|
+
await walkForLockDirs(path.join(dir, entry), MAX_WALK_DEPTH - 1, state, lockDirs, errors, maxWalkEntries);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
155
185
|
if (PRUNED_DIR_NAMES.has(entry)) continue;
|
|
156
186
|
await walkForLockDirs(path.join(dir, entry), depth + 1, state, lockDirs, errors, maxWalkEntries);
|
|
157
187
|
}
|
|
@@ -204,6 +234,14 @@ export const fileLocksGcAdapter: GcStoreAdapter = {
|
|
|
204
234
|
},
|
|
205
235
|
async prune(record: GcRecord, ctx: GcContext): Promise<GcPruneOutcome> {
|
|
206
236
|
const lockDir = record.path ?? record.id;
|
|
237
|
+
if (fileLockStagingOwnerPid(path.basename(lockDir)) !== null) {
|
|
238
|
+
try {
|
|
239
|
+
const staging = await inspectFileLockStagingDir(lockDir, pid => stagingOwnerStatus(ctx, pid), true);
|
|
240
|
+
return staging.removed ? { removed: true } : { removed: false, skipped: staging.reason };
|
|
241
|
+
} catch (error) {
|
|
242
|
+
return { removed: false, error: errorMessage(error) };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
207
245
|
const observation = await readFileLockObservationForGc(lockDir);
|
|
208
246
|
const info = observation?.info;
|
|
209
247
|
if (!info) return { removed: false, skipped: "lock_no_longer_dead_or_missing" };
|
package/src/config/file-lock.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
renameNoReplacePathAsync,
|
|
16
16
|
snapshotDirectoryTree,
|
|
17
17
|
} from "@gajae-code/natives";
|
|
18
|
+
import { logger } from "@gajae-code/utils";
|
|
18
19
|
import { isEnoent } from "@gajae-code/utils/fs-error";
|
|
19
20
|
import { nativeProcessBindings } from "@gajae-code/utils/native-process";
|
|
20
21
|
|
|
@@ -30,6 +31,23 @@ export interface FileLockOptions {
|
|
|
30
31
|
previousOwnerHostIds?: readonly string[];
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
export class FileLockAcquireError extends Error {
|
|
35
|
+
readonly code = "acquire_timeout";
|
|
36
|
+
|
|
37
|
+
constructor(
|
|
38
|
+
readonly filePath: string,
|
|
39
|
+
readonly lockPath: string,
|
|
40
|
+
readonly attempts: number,
|
|
41
|
+
readonly holder: string,
|
|
42
|
+
) {
|
|
43
|
+
super(
|
|
44
|
+
`Failed to acquire lock for ${filePath} after ${attempts} attempts: ${holder} (${lockPath}); ` +
|
|
45
|
+
`a live owner is never displaced — if this is an SDK broker (gjc sdk status), it must finish or be stopped before retrying`,
|
|
46
|
+
);
|
|
47
|
+
this.name = "FileLockAcquireError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
33
51
|
const DEFAULT_OPTIONS: Required<
|
|
34
52
|
Omit<FileLockOptions, "ownerHostId" | "previousOwnerHostIds" | "signal" | "onAcquired">
|
|
35
53
|
> = {
|
|
@@ -38,9 +56,9 @@ const DEFAULT_OPTIONS: Required<
|
|
|
38
56
|
retryDelayMs: 100,
|
|
39
57
|
};
|
|
40
58
|
|
|
41
|
-
/** Release retries cover transient
|
|
42
|
-
export const FILE_LOCK_RELEASE_RETRY_ATTEMPTS =
|
|
43
|
-
export const FILE_LOCK_RELEASE_RETRY_DELAY_MS =
|
|
59
|
+
/** Release retries cover transient handle denial and a competing exact-removal quarantine cleanup. */
|
|
60
|
+
export const FILE_LOCK_RELEASE_RETRY_ATTEMPTS = 20;
|
|
61
|
+
export const FILE_LOCK_RELEASE_RETRY_DELAY_MS = 25;
|
|
44
62
|
const PROCESS_START_TIME_FORMAT = "utc-v1";
|
|
45
63
|
|
|
46
64
|
type LocalLockState = {
|
|
@@ -101,12 +119,27 @@ export function processStartTime(pid: number): string | null {
|
|
|
101
119
|
}
|
|
102
120
|
|
|
103
121
|
let ownProcessStartTime: string | undefined;
|
|
122
|
+
let ownProcessIncarnation: string | null | undefined;
|
|
104
123
|
|
|
105
124
|
function currentProcessStartTime(): string {
|
|
106
125
|
if (ownProcessStartTime === undefined) ownProcessStartTime = processStartTime(process.pid) ?? "unknown";
|
|
107
126
|
return ownProcessStartTime;
|
|
108
127
|
}
|
|
109
128
|
|
|
129
|
+
function processIncarnation(pid: number): string | null {
|
|
130
|
+
try {
|
|
131
|
+
const incarnation = nativeProcessBindings().Process.fromPid(pid)?.incarnation;
|
|
132
|
+
return typeof incarnation === "string" && incarnation.length > 0 ? incarnation : null;
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function currentProcessIncarnation(): string | null {
|
|
139
|
+
if (ownProcessIncarnation === undefined) ownProcessIncarnation = processIncarnation(process.pid);
|
|
140
|
+
return ownProcessIncarnation;
|
|
141
|
+
}
|
|
142
|
+
|
|
110
143
|
function cachedProcessStartTime(owner: FileLockOwnerToken, cache?: Map<string, string | null>): string | null {
|
|
111
144
|
if (!cache) return processStartTime(owner.pid);
|
|
112
145
|
const key = `${owner.pid}:${owner.start_time ?? ""}`;
|
|
@@ -119,6 +152,10 @@ function cachedProcessStartTime(owner: FileLockOwnerToken, cache?: Map<string, s
|
|
|
119
152
|
|
|
120
153
|
function ownerIsAlive(owner: FileLockOwnerToken, startTimeCache?: Map<string, string | null>): boolean {
|
|
121
154
|
if (ownerLiveness(owner.pid) !== "alive") return false;
|
|
155
|
+
if (owner.process_incarnation) {
|
|
156
|
+
const currentIncarnation = processIncarnation(owner.pid);
|
|
157
|
+
return currentIncarnation === null || currentIncarnation === owner.process_incarnation;
|
|
158
|
+
}
|
|
122
159
|
if (!owner.start_time || owner.start_time === "unknown") return true;
|
|
123
160
|
const currentStartTime = cachedProcessStartTime(owner, startTimeCache);
|
|
124
161
|
if (currentStartTime === null || currentStartTime === owner.start_time) return true;
|
|
@@ -130,10 +167,12 @@ function ownerIsAlive(owner: FileLockOwnerToken, startTimeCache?: Map<string, st
|
|
|
130
167
|
}
|
|
131
168
|
|
|
132
169
|
function lockInfo(ownerHostId: string | undefined, ownerToken: string): LockInfo {
|
|
170
|
+
const incarnation = currentProcessIncarnation();
|
|
133
171
|
return {
|
|
134
172
|
pid: process.pid,
|
|
135
173
|
start_time: currentProcessStartTime(),
|
|
136
174
|
start_time_format: PROCESS_START_TIME_FORMAT,
|
|
175
|
+
...(incarnation === null ? {} : { process_incarnation: incarnation }),
|
|
137
176
|
timestamp: Date.now(),
|
|
138
177
|
owner_token: ownerToken,
|
|
139
178
|
...(ownerHostId === undefined ? {} : { owner_host_id: ownerHostId }),
|
|
@@ -327,7 +366,8 @@ async function readLockInfo(lockPath: string): Promise<LockInfo | null> {
|
|
|
327
366
|
}
|
|
328
367
|
|
|
329
368
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
330
|
-
const { pid, start_time, start_time_format, timestamp, owner_host_id, owner_token } =
|
|
369
|
+
const { pid, start_time, start_time_format, process_incarnation, timestamp, owner_host_id, owner_token } =
|
|
370
|
+
parsed as Partial<LockInfo>;
|
|
331
371
|
if (
|
|
332
372
|
typeof pid !== "number" ||
|
|
333
373
|
!Number.isInteger(pid) ||
|
|
@@ -336,11 +376,12 @@ async function readLockInfo(lockPath: string): Promise<LockInfo | null> {
|
|
|
336
376
|
!Number.isFinite(timestamp) ||
|
|
337
377
|
(start_time !== undefined && (typeof start_time !== "string" || !start_time)) ||
|
|
338
378
|
(start_time_format !== undefined && (typeof start_time_format !== "string" || !start_time_format)) ||
|
|
379
|
+
(process_incarnation !== undefined && (typeof process_incarnation !== "string" || !process_incarnation)) ||
|
|
339
380
|
(owner_host_id !== undefined && (typeof owner_host_id !== "string" || !owner_host_id)) ||
|
|
340
381
|
(owner_token !== undefined && (typeof owner_token !== "string" || !owner_token))
|
|
341
382
|
)
|
|
342
383
|
return null;
|
|
343
|
-
return { pid, start_time, start_time_format, timestamp, owner_host_id, owner_token };
|
|
384
|
+
return { pid, start_time, start_time_format, process_incarnation, timestamp, owner_host_id, owner_token };
|
|
344
385
|
}
|
|
345
386
|
|
|
346
387
|
function parseLockInfoBytes(bytes: string): LockInfo | null {
|
|
@@ -351,7 +392,8 @@ function parseLockInfoBytes(bytes: string): LockInfo | null {
|
|
|
351
392
|
return null;
|
|
352
393
|
}
|
|
353
394
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
354
|
-
const { pid, start_time, start_time_format, timestamp, owner_host_id, owner_token } =
|
|
395
|
+
const { pid, start_time, start_time_format, process_incarnation, timestamp, owner_host_id, owner_token } =
|
|
396
|
+
parsed as Partial<LockInfo>;
|
|
355
397
|
if (
|
|
356
398
|
typeof pid !== "number" ||
|
|
357
399
|
!Number.isInteger(pid) ||
|
|
@@ -360,11 +402,12 @@ function parseLockInfoBytes(bytes: string): LockInfo | null {
|
|
|
360
402
|
!Number.isFinite(timestamp) ||
|
|
361
403
|
(start_time !== undefined && (typeof start_time !== "string" || !start_time)) ||
|
|
362
404
|
(start_time_format !== undefined && (typeof start_time_format !== "string" || !start_time_format)) ||
|
|
405
|
+
(process_incarnation !== undefined && (typeof process_incarnation !== "string" || !process_incarnation)) ||
|
|
363
406
|
(owner_host_id !== undefined && (typeof owner_host_id !== "string" || !owner_host_id)) ||
|
|
364
407
|
(owner_token !== undefined && (typeof owner_token !== "string" || !owner_token))
|
|
365
408
|
)
|
|
366
409
|
return null;
|
|
367
|
-
return { pid, start_time, start_time_format, timestamp, owner_host_id, owner_token };
|
|
410
|
+
return { pid, start_time, start_time_format, process_incarnation, timestamp, owner_host_id, owner_token };
|
|
368
411
|
}
|
|
369
412
|
|
|
370
413
|
/** @internal */
|
|
@@ -393,6 +436,8 @@ export async function readFileLockInfoForGc(lockDir: string): Promise<FileLockOw
|
|
|
393
436
|
/** Owner identity stamped into a `<file>.lock/info` record. */
|
|
394
437
|
export interface FileLockOwnerToken {
|
|
395
438
|
pid: number;
|
|
439
|
+
/** Kernel-derived identity for the exact process generation owning `pid`. */
|
|
440
|
+
process_incarnation?: string;
|
|
396
441
|
start_time?: string;
|
|
397
442
|
/** Encoding marker for the canonical UTC process-start identity. */
|
|
398
443
|
start_time_format?: string;
|
|
@@ -672,6 +717,11 @@ async function localLockKey(lockPath: string): Promise<string> {
|
|
|
672
717
|
}
|
|
673
718
|
|
|
674
719
|
function ownerIncarnationChanged(owner: FileLockOwnerToken, startTimeCache?: Map<string, string | null>): boolean {
|
|
720
|
+
if (owner.process_incarnation) {
|
|
721
|
+
if (ownerLiveness(owner.pid) !== "alive") return false;
|
|
722
|
+
const currentIncarnation = processIncarnation(owner.pid);
|
|
723
|
+
return currentIncarnation !== null && currentIncarnation !== owner.process_incarnation;
|
|
724
|
+
}
|
|
675
725
|
if (owner.start_time_format !== PROCESS_START_TIME_FORMAT || !owner.start_time || owner.start_time === "unknown")
|
|
676
726
|
return false;
|
|
677
727
|
if (ownerLiveness(owner.pid) !== "alive") return false;
|
|
@@ -753,6 +803,7 @@ export async function removeFileLockDirForGc(
|
|
|
753
803
|
if (!expectedIdentity) return "owner_changed";
|
|
754
804
|
if (
|
|
755
805
|
current.pid !== expected.pid ||
|
|
806
|
+
(expected.process_incarnation !== undefined && current.process_incarnation !== expected.process_incarnation) ||
|
|
756
807
|
(expected.start_time !== undefined && current.start_time !== expected.start_time) ||
|
|
757
808
|
current.owner_host_id !== expected.owner_host_id ||
|
|
758
809
|
(expected.owner_token !== undefined && current.owner_token !== expected.owner_token) ||
|
|
@@ -931,7 +982,15 @@ async function staleLockSnapshot(
|
|
|
931
982
|
|
|
932
983
|
async function removeStaleLockForAcquire(lockPath: string, snapshot: LockStaleSnapshot): Promise<boolean> {
|
|
933
984
|
if (!snapshot.stale) return false;
|
|
934
|
-
|
|
985
|
+
try {
|
|
986
|
+
return (await removeFileLockDirForGc(lockPath, snapshot.owner, snapshot.identity)) === "removed";
|
|
987
|
+
} catch {
|
|
988
|
+
// Exact removal refusal is not authority to fail or mutate by another path.
|
|
989
|
+
// Keep contending: a concurrent reclaimer may already be completing the same
|
|
990
|
+
// dead generation, while a persistent refusal remains fail-closed until the
|
|
991
|
+
// normal retry budget reports the still-held lock.
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
935
994
|
}
|
|
936
995
|
|
|
937
996
|
/**
|
|
@@ -1332,7 +1391,7 @@ async function lockHolderDescription(lockPath: string): Promise<string> {
|
|
|
1332
1391
|
}
|
|
1333
1392
|
}
|
|
1334
1393
|
|
|
1335
|
-
async function
|
|
1394
|
+
export async function acquireFileLock(filePath: string, options: FileLockOptions = {}): Promise<() => Promise<void>> {
|
|
1336
1395
|
const requestedFilePath: unknown = filePath;
|
|
1337
1396
|
if (typeof requestedFilePath !== "string" || requestedFilePath.length === 0 || !path.isAbsolute(requestedFilePath))
|
|
1338
1397
|
throw new TypeError("filePath must be a non-empty absolute path");
|
|
@@ -1344,6 +1403,11 @@ async function acquireLock(filePath: string, options: FileLockOptions = {}): Pro
|
|
|
1344
1403
|
if (opts.signal?.aborted) throw opts.signal.reason ?? new Error("File lock acquisition aborted");
|
|
1345
1404
|
const lockPath = getLockPath(filePath);
|
|
1346
1405
|
await ensureLockParent(path.dirname(lockPath));
|
|
1406
|
+
try {
|
|
1407
|
+
await reapOrphanedLockStagingDirs(lockPath);
|
|
1408
|
+
} catch (error) {
|
|
1409
|
+
logger.debug("Failed to reap orphaned file-lock staging directories", { lockPath, error: String(error) });
|
|
1410
|
+
}
|
|
1347
1411
|
const ownerToken = crypto.randomUUID();
|
|
1348
1412
|
const contentionStartTimes = new Map<string, string | null>();
|
|
1349
1413
|
for (let attempt = 0; attempt < opts.retries; attempt++) {
|
|
@@ -1392,10 +1456,7 @@ async function acquireLock(filePath: string, options: FileLockOptions = {}): Pro
|
|
|
1392
1456
|
opts.signal.removeEventListener("abort", onAbort);
|
|
1393
1457
|
}
|
|
1394
1458
|
}
|
|
1395
|
-
throw new
|
|
1396
|
-
`Failed to acquire lock for ${filePath} after ${opts.retries} attempts: ${await lockHolderDescription(lockPath)} (${lockPath}); ` +
|
|
1397
|
-
`a live owner is never displaced — if this is an SDK broker (gjc sdk status), it must finish or be stopped before retrying`,
|
|
1398
|
-
);
|
|
1459
|
+
throw new FileLockAcquireError(filePath, lockPath, opts.retries, await lockHolderDescription(lockPath));
|
|
1399
1460
|
}
|
|
1400
1461
|
|
|
1401
1462
|
/**
|
|
@@ -1409,7 +1470,7 @@ export async function withFileLock<T>(
|
|
|
1409
1470
|
fn: () => Promise<T>,
|
|
1410
1471
|
options: FileLockOptions = {},
|
|
1411
1472
|
): Promise<T> {
|
|
1412
|
-
const release = await
|
|
1473
|
+
const release = await acquireFileLock(filePath, options);
|
|
1413
1474
|
let result: T;
|
|
1414
1475
|
try {
|
|
1415
1476
|
result = await fn();
|
|
@@ -1424,3 +1485,126 @@ export async function withFileLock<T>(
|
|
|
1424
1485
|
await release();
|
|
1425
1486
|
return result;
|
|
1426
1487
|
}
|
|
1488
|
+
|
|
1489
|
+
/** Strictly recognize the staging names emitted by tryAcquireLock. */
|
|
1490
|
+
export function fileLockStagingOwnerPid(name: string): number | null {
|
|
1491
|
+
const match = /^.+\.lock\.pending\.([1-9]\d*)\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.exec(
|
|
1492
|
+
name,
|
|
1493
|
+
);
|
|
1494
|
+
if (!match) return null;
|
|
1495
|
+
const pid = Number(match[1]);
|
|
1496
|
+
return Number.isSafeInteger(pid) ? pid : null;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
export interface FileLockStagingResult {
|
|
1500
|
+
path: string;
|
|
1501
|
+
pid?: number;
|
|
1502
|
+
status: OwnerLiveness;
|
|
1503
|
+
removed: boolean;
|
|
1504
|
+
reason: string;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/** Observe identity before probing; neither a replacement nor a symlink inherits the verdict. */
|
|
1508
|
+
export async function inspectFileLockStagingDir(
|
|
1509
|
+
stagingPath: string,
|
|
1510
|
+
probe: (pid: number) => OwnerLiveness = ownerLiveness,
|
|
1511
|
+
remove = false,
|
|
1512
|
+
): Promise<FileLockStagingResult> {
|
|
1513
|
+
const kept: FileLockStagingResult = {
|
|
1514
|
+
path: stagingPath,
|
|
1515
|
+
status: "unknown",
|
|
1516
|
+
removed: false,
|
|
1517
|
+
reason: "unverified_staging_directory",
|
|
1518
|
+
};
|
|
1519
|
+
const namePid = fileLockStagingOwnerPid(path.basename(stagingPath));
|
|
1520
|
+
if (namePid === null) return kept;
|
|
1521
|
+
const canonical = await canonicalLockPathPreservingFinal(stagingPath);
|
|
1522
|
+
const root = await fs.lstat(canonical, { bigint: true });
|
|
1523
|
+
if (!root.isDirectory() || root.isSymbolicLink()) return kept;
|
|
1524
|
+
const captured = nativeFileLockBindings().snapshotDirectoryTree(canonical);
|
|
1525
|
+
if (
|
|
1526
|
+
!captured.ok ||
|
|
1527
|
+
!captured.snapshot ||
|
|
1528
|
+
captured.snapshot.rootDev !== root.dev.toString() ||
|
|
1529
|
+
captured.snapshot.rootIno !== root.ino.toString()
|
|
1530
|
+
)
|
|
1531
|
+
return kept;
|
|
1532
|
+
const observation = await readFileLockObservationForGc(canonical);
|
|
1533
|
+
let pid: number;
|
|
1534
|
+
if (observation) {
|
|
1535
|
+
if (
|
|
1536
|
+
observation.identity.rootDev !== captured.snapshot.rootDev ||
|
|
1537
|
+
observation.identity.rootIno !== captured.snapshot.rootIno
|
|
1538
|
+
)
|
|
1539
|
+
return kept;
|
|
1540
|
+
pid = observation.info.pid;
|
|
1541
|
+
if (observation.info.owner_host_id !== undefined) return { ...kept, pid, reason: "host_qualified_staging_owner" };
|
|
1542
|
+
} else {
|
|
1543
|
+
// Missing info is the only case where the filename is ownership evidence.
|
|
1544
|
+
try {
|
|
1545
|
+
await fs.lstat(path.join(canonical, "info"));
|
|
1546
|
+
return kept;
|
|
1547
|
+
} catch (error) {
|
|
1548
|
+
if (!isEnoent(error)) throw error;
|
|
1549
|
+
}
|
|
1550
|
+
if (captured.snapshot.entries.some(entry => entry.relativePath !== "")) return kept;
|
|
1551
|
+
pid = namePid;
|
|
1552
|
+
}
|
|
1553
|
+
if (pid === process.pid || namePid === process.pid) {
|
|
1554
|
+
return { ...kept, pid, status: "alive", reason: "in_process_staging_owner" };
|
|
1555
|
+
}
|
|
1556
|
+
let status = probe(pid);
|
|
1557
|
+
if (status === "alive" && observation && ownerIncarnationChanged(observation.info)) status = "dead";
|
|
1558
|
+
const result: FileLockStagingResult = {
|
|
1559
|
+
path: stagingPath,
|
|
1560
|
+
pid,
|
|
1561
|
+
status,
|
|
1562
|
+
removed: false,
|
|
1563
|
+
reason: `file_lock_staging_owner_${status}`,
|
|
1564
|
+
};
|
|
1565
|
+
if (status !== "dead" || !remove) return result;
|
|
1566
|
+
if (observation) {
|
|
1567
|
+
const removal = await removeFileLockDirForGc(canonical, observation.info, observation.identity);
|
|
1568
|
+
return { ...result, removed: removal === "removed", reason: removal };
|
|
1569
|
+
}
|
|
1570
|
+
const removal = nativeFileLockBindings().exactRemoveDirectoryTree(canonical, captured.snapshot);
|
|
1571
|
+
if (
|
|
1572
|
+
removal.detachedPath &&
|
|
1573
|
+
path.resolve(removal.detachedPath) !== path.resolve(canonical) &&
|
|
1574
|
+
!removal.retainedSuccessorPath &&
|
|
1575
|
+
!removal.retainedPlaceholderPath &&
|
|
1576
|
+
!removal.retainedUnknownPath
|
|
1577
|
+
) {
|
|
1578
|
+
const detached = await fs.lstat(removal.detachedPath, { bigint: true });
|
|
1579
|
+
if (
|
|
1580
|
+
detached.isDirectory() &&
|
|
1581
|
+
!detached.isSymbolicLink() &&
|
|
1582
|
+
detached.dev.toString() === captured.snapshot.rootDev &&
|
|
1583
|
+
detached.ino.toString() === captured.snapshot.rootIno
|
|
1584
|
+
) {
|
|
1585
|
+
await fs.rmdir(removal.detachedPath);
|
|
1586
|
+
return { ...result, removed: true, reason: "removed" };
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
return { ...result, removed: removal.ok, reason: removal.ok ? "removed" : (removal.code ?? "cleanup_failed") };
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/** Bounded opportunistic cleanup; the acquisition caller treats failures as diagnostic only. */
|
|
1593
|
+
export async function reapOrphanedLockStagingDirs(lockPath: string): Promise<{
|
|
1594
|
+
removed: string[];
|
|
1595
|
+
retained: FileLockStagingResult[];
|
|
1596
|
+
}> {
|
|
1597
|
+
const summary: { removed: string[]; retained: FileLockStagingResult[] } = { removed: [], retained: [] };
|
|
1598
|
+
const parent = path.dirname(lockPath);
|
|
1599
|
+
const prefix = `${path.basename(lockPath)}.pending.`;
|
|
1600
|
+
const entries = await fs.readdir(parent);
|
|
1601
|
+
let candidates = 0;
|
|
1602
|
+
for (const entry of entries) {
|
|
1603
|
+
if (!entry.startsWith(prefix) || fileLockStagingOwnerPid(entry) === null) continue;
|
|
1604
|
+
if (++candidates > 64) break;
|
|
1605
|
+
const result = await inspectFileLockStagingDir(path.join(parent, entry), ownerLiveness, true);
|
|
1606
|
+
if (result.removed) summary.removed.push(result.path);
|
|
1607
|
+
else summary.retained.push(result);
|
|
1608
|
+
}
|
|
1609
|
+
return summary;
|
|
1610
|
+
}
|