@khorsheed/dsh-ankh-guard 0.1.0 → 0.2.0
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 +28 -0
- package/README.en.md +75 -29
- package/README.i18n.yaml +2 -2
- package/README.md +74 -29
- package/lib/cli.js +2383 -209
- package/lib/client.js +257 -0
- package/lib/exit-agent.js +5 -2
- package/lib/index.js +722 -38
- package/lib/invariant.js +1 -1
- package/lib/preflight-runner.js +125 -47
- package/lib/processes-BjZgJjQr.js +344 -0
- package/lib/restart-context-D6nISh28.js +1245 -0
- package/lib/restart-context-DUyExi9O.js +1245 -0
- package/lib/{state-Dhx9VG44.js → state-4f7yny39.js} +60 -13
- package/lib/state-CZMypGkB.js +323 -0
- package/lib/test-seam-DnvLWTeO.js +119 -0
- package/lib/test-seam-cli.js +24 -0
- package/lib/test-seam-dwvaKjRp.js +459 -0
- package/lib/test-seam.js +2 -0
- package/lib/types/browser-handoff.d.ts +55 -0
- package/lib/types/browser-handoff.js +489 -0
- package/lib/types/cli.d.ts +34 -4
- package/lib/types/cli.js +1487 -225
- package/lib/types/client/index.d.ts +15 -0
- package/lib/types/client/index.js +264 -0
- package/lib/types/deployment-proof.d.ts +24 -0
- package/lib/types/deployment-proof.js +314 -0
- package/lib/types/exit-agent.js +2 -0
- package/lib/types/git.d.ts +12 -3
- package/lib/types/git.js +69 -7
- package/lib/types/index.d.ts +66 -3
- package/lib/types/index.js +157 -39
- package/lib/types/launch-spec.d.ts +263 -0
- package/lib/types/launch-spec.js +823 -0
- package/lib/types/preflight-runner.d.ts +23 -12
- package/lib/types/preflight-runner.js +152 -57
- package/lib/types/processes.d.ts +38 -6
- package/lib/types/processes.js +236 -10
- package/lib/types/restart-context.d.ts +50 -0
- package/lib/types/restart-context.js +106 -0
- package/lib/types/restart-request.d.ts +32 -0
- package/lib/types/restart-request.js +128 -0
- package/lib/types/state-files.d.ts +30 -0
- package/lib/types/state-files.js +55 -0
- package/lib/types/state.d.ts +29 -2
- package/lib/types/state.js +52 -7
- package/lib/types/temp-artifact.d.ts +15 -0
- package/lib/types/temp-artifact.js +17 -0
- package/lib/types/test-seam-cli.d.ts +3 -0
- package/lib/types/test-seam-cli.js +27 -0
- package/lib/types/test-seam.d.ts +55 -0
- package/lib/types/test-seam.js +112 -0
- package/lib/types/transition.d.ts +118 -0
- package/lib/types/transition.js +717 -0
- package/package.json +29 -9
- package/scripts/dsh-watchdog.sh +1388 -80
- package/scripts/install-launchd.sh +43 -5
- package/scripts/install-systemd.sh +43 -5
- package/scripts/on-install.js +1 -1
- package/skills/dsh-self-restart-guard/SKILL.md +38 -12
- package/lib/processes-hCAmwma-.js +0 -127
- package/lib/restart-context-DmnQXNf-.js +0 -421
|
@@ -1,421 +0,0 @@
|
|
|
1
|
-
import { c as stateFile } from "./state-Dhx9VG44.js";
|
|
2
|
-
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
4
|
-
//#region lib/types/git.js
|
|
5
|
-
/**
|
|
6
|
-
* Git helpers for the self-restart guard: the credential binds to the current
|
|
7
|
-
* HEAD, checkpoints are real commits, and rollback is a hard reset. All calls
|
|
8
|
-
* are synchronous child-process invocations scoped to the repo directory.
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* The repository's current HEAD, or null when the directory is not inside a
|
|
12
|
-
* git repository (or git itself is unavailable).
|
|
13
|
-
* @param repoDir - repository directory.
|
|
14
|
-
* @returns the full HEAD sha, or null.
|
|
15
|
-
*/
|
|
16
|
-
function currentHead(repoDir) {
|
|
17
|
-
try {
|
|
18
|
-
const out = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
19
|
-
cwd: repoDir,
|
|
20
|
-
encoding: "utf8",
|
|
21
|
-
stdio: "pipe"
|
|
22
|
-
}).trim();
|
|
23
|
-
return out.length > 0 ? out : null;
|
|
24
|
-
} catch {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Commit the whole working tree as a checkpoint snapshot (empty commits
|
|
30
|
-
* allowed — a clean tree still records a rollback point).
|
|
31
|
-
* @param repoDir - repository directory.
|
|
32
|
-
* @param message - checkpoint commit message.
|
|
33
|
-
* @param artifactPattern - staged paths matching this are reported as
|
|
34
|
-
* build-artifact-looking warnings (deployment-specific — see
|
|
35
|
-
* SRC_ARTIFACT_PATTERN in defaults.ts; omit for none).
|
|
36
|
-
* @returns the new HEAD sha, or a failure reason.
|
|
37
|
-
*/
|
|
38
|
-
function commitCheckpoint(repoDir, message, artifactPattern) {
|
|
39
|
-
try {
|
|
40
|
-
execFileSync("git", ["add", "-A"], {
|
|
41
|
-
cwd: repoDir,
|
|
42
|
-
stdio: "pipe"
|
|
43
|
-
});
|
|
44
|
-
const staged = execFileSync("git", [
|
|
45
|
-
"diff",
|
|
46
|
-
"--cached",
|
|
47
|
-
"--name-only"
|
|
48
|
-
], {
|
|
49
|
-
cwd: repoDir,
|
|
50
|
-
encoding: "utf8"
|
|
51
|
-
});
|
|
52
|
-
const artifacts = artifactPattern === void 0 ? [] : staged.split("\n").filter((file) => artifactPattern.test(file));
|
|
53
|
-
execFileSync("git", [
|
|
54
|
-
"commit",
|
|
55
|
-
"--allow-empty",
|
|
56
|
-
"-m",
|
|
57
|
-
message
|
|
58
|
-
], {
|
|
59
|
-
cwd: repoDir,
|
|
60
|
-
stdio: "pipe"
|
|
61
|
-
});
|
|
62
|
-
const sha = currentHead(repoDir);
|
|
63
|
-
if (sha === null) return {
|
|
64
|
-
ok: false,
|
|
65
|
-
error: "checkpoint commit succeeded but HEAD became unreadable"
|
|
66
|
-
};
|
|
67
|
-
return {
|
|
68
|
-
ok: true,
|
|
69
|
-
sha,
|
|
70
|
-
artifacts
|
|
71
|
-
};
|
|
72
|
-
} catch (error) {
|
|
73
|
-
return {
|
|
74
|
-
ok: false,
|
|
75
|
-
error: `git checkpoint failed: ${String(error)}`
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Roll the checkout back to a checkpoint commit WITHOUT losing work: the
|
|
81
|
-
* discarded HEAD becomes a `guard-backup-*` branch, and uncommitted tracked
|
|
82
|
-
* changes become a second `-wip` anchor commit (`git stash create` snapshots
|
|
83
|
-
* the worktree without touching it; untracked files survive `reset --hard`
|
|
84
|
-
* on their own). Every reset path — the watchdog, the CLI, the agent-facing
|
|
85
|
-
* service — funnels through here, so recovery never depends on the reflog.
|
|
86
|
-
* @param repoDir - repository directory.
|
|
87
|
-
* @param sha - the checkpoint commit to reset to.
|
|
88
|
-
* @returns success with the recovery anchor refs, or a failure reason.
|
|
89
|
-
*/
|
|
90
|
-
function resetToCheckpoint(repoDir, sha) {
|
|
91
|
-
const anchors = [];
|
|
92
|
-
try {
|
|
93
|
-
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").replace(/\..*$/, "");
|
|
94
|
-
const anchor = `guard-backup-${stamp.slice(0, 8)}-${stamp.slice(8)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
95
|
-
const head = currentHead(repoDir);
|
|
96
|
-
if (head !== null && head !== sha) try {
|
|
97
|
-
execFileSync("git", [
|
|
98
|
-
"branch",
|
|
99
|
-
anchor,
|
|
100
|
-
"HEAD"
|
|
101
|
-
], {
|
|
102
|
-
cwd: repoDir,
|
|
103
|
-
stdio: "pipe"
|
|
104
|
-
});
|
|
105
|
-
anchors.push(anchor);
|
|
106
|
-
} catch {}
|
|
107
|
-
let wip = "";
|
|
108
|
-
try {
|
|
109
|
-
wip = execFileSync("git", ["stash", "create"], {
|
|
110
|
-
cwd: repoDir,
|
|
111
|
-
encoding: "utf8"
|
|
112
|
-
}).trim();
|
|
113
|
-
} catch {}
|
|
114
|
-
if (wip !== "") try {
|
|
115
|
-
execFileSync("git", [
|
|
116
|
-
"branch",
|
|
117
|
-
`${anchor}-wip`,
|
|
118
|
-
wip
|
|
119
|
-
], {
|
|
120
|
-
cwd: repoDir,
|
|
121
|
-
stdio: "pipe"
|
|
122
|
-
});
|
|
123
|
-
anchors.push(`${anchor}-wip`);
|
|
124
|
-
} catch {}
|
|
125
|
-
execFileSync("git", [
|
|
126
|
-
"reset",
|
|
127
|
-
"--hard",
|
|
128
|
-
sha
|
|
129
|
-
], {
|
|
130
|
-
cwd: repoDir,
|
|
131
|
-
stdio: "pipe"
|
|
132
|
-
});
|
|
133
|
-
return {
|
|
134
|
-
ok: true,
|
|
135
|
-
anchors
|
|
136
|
-
};
|
|
137
|
-
} catch (error) {
|
|
138
|
-
return {
|
|
139
|
-
ok: false,
|
|
140
|
-
error: `git reset --hard ${sha} failed: ${String(error)}`,
|
|
141
|
-
anchors
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
//#endregion
|
|
146
|
-
//#region lib/types/restart-context.js
|
|
147
|
-
/**
|
|
148
|
-
* Restart-record context injection and interrupted-session continuity. After
|
|
149
|
-
* a scheduled restart, the FULL report waits for the initiating session's
|
|
150
|
-
* root agent, whenever it resumes (session restore is lazy, so no other
|
|
151
|
-
* session is ever woken for reporting); a record without an initiator is
|
|
152
|
-
* claimed by the first root agent created. Separately, a snapshot written at
|
|
153
|
-
* SIGTERM time (`interrupted-sessions.json`) records which root sessions had
|
|
154
|
-
* a live turn when the process stopped, so the next restart boot can resume
|
|
155
|
-
* those sessions and queue a "continue" turn. Pure logic reads the durable
|
|
156
|
-
* files; the plugin wires them into `agent/created` and `agent.followup`.
|
|
157
|
-
*/
|
|
158
|
-
/** Absolute path of the restart record inside a state directory. */
|
|
159
|
-
function restartRecordFile(stateDir) {
|
|
160
|
-
return stateFile(stateDir, "lastRestart");
|
|
161
|
-
}
|
|
162
|
-
/** Absolute path of the interrupted-session snapshot inside a state directory. */
|
|
163
|
-
function interruptedSnapshotFile(stateDir) {
|
|
164
|
-
return stateFile(stateDir, "interruptedSessions");
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* The pending restart record, or null when none is awaiting a report (absent,
|
|
168
|
-
* unparseable, or settled). The record stays pending until the initiator
|
|
169
|
-
* resumes or the next restart replaces it (a new exitAt) — those are the only
|
|
170
|
-
* retirement paths.
|
|
171
|
-
* @param stateDir - state directory.
|
|
172
|
-
* @returns the record without `reportedAt`, or null.
|
|
173
|
-
*/
|
|
174
|
-
function pendingRestartRecord(stateDir) {
|
|
175
|
-
const file = restartRecordFile(stateDir);
|
|
176
|
-
if (!existsSync(file)) return null;
|
|
177
|
-
try {
|
|
178
|
-
const record = JSON.parse(readFileSync(file, "utf8"));
|
|
179
|
-
return record.reportedAt !== void 0 ? null : record;
|
|
180
|
-
} catch {
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* The model-visible restart report (Chinese product copy, factual).
|
|
186
|
-
* @param record - the pending restart record.
|
|
187
|
-
* @param canaryPending - whether the restart marker is still present (the
|
|
188
|
-
* watchdog has not yet run/cleared the canary).
|
|
189
|
-
* @returns the report text, or an empty string for a malformed record.
|
|
190
|
-
*/
|
|
191
|
-
function restartContextText(record, canaryPending) {
|
|
192
|
-
if (record.exitAt === void 0 && record.error === void 0) return "";
|
|
193
|
-
const time = record.exitAt !== void 0 ? new Date(record.exitAt).toISOString() : "未知时间";
|
|
194
|
-
if (record.unexpected === true) return `[ankh-guard] 服务最近发生过一次非计划退出(崩溃或被手动停止):${time},watchdog 已自动拉起实例。请向用户简要回报这次非计划重启。`;
|
|
195
|
-
return `[ankh-guard] 服务最近重启过:${time},退出${record.error !== void 0 ? `失败(${record.error})` : "成功"},${canaryPending ? "金丝雀尚未完成" : "金丝雀已处理"}。请向用户简要回报本次重启结果。`;
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* The model-visible "continue" prompt for a session whose turn was
|
|
199
|
-
* interrupted by the restart (its log tail was closed with
|
|
200
|
-
* `reason.kind === 'interrupted'` by crash-recovery repair).
|
|
201
|
-
* @param exitAt - epoch milliseconds of the exit that interrupted the turn.
|
|
202
|
-
* @returns the prompt text.
|
|
203
|
-
*/
|
|
204
|
-
function continueInterruptedText(exitAt) {
|
|
205
|
-
return `[ankh-guard] 服务于 ${new Date(exitAt).toISOString()} 重启,你上次正在进行的回合被中断(日志已标记 interrupted)。请检查当前状态并继续未完成的任务;若任务已不再适用,简要说明原因后停止。`;
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* The combined prompt for a session that is BOTH the restart's initiator and
|
|
209
|
-
* an interrupted session: one turn continues the work and reports the
|
|
210
|
-
* restart, instead of two near-duplicate turns.
|
|
211
|
-
* @param record - the pending restart record.
|
|
212
|
-
* @param canaryPending - whether the restart marker is still present.
|
|
213
|
-
* @returns the prompt text, or an empty string for a malformed record.
|
|
214
|
-
*/
|
|
215
|
-
function continueAndReportText(record, canaryPending) {
|
|
216
|
-
if (record.exitAt === void 0 && record.error === void 0) return "";
|
|
217
|
-
const time = record.exitAt !== void 0 ? new Date(record.exitAt).toISOString() : "未知时间";
|
|
218
|
-
if (record.unexpected === true) return `[ankh-guard] 服务于 ${time} 发生非计划退出(崩溃或被手动停止),watchdog 已自动拉起实例。你上次正在进行的回合被中断(日志已标记 interrupted)。请检查当前状态并继续未完成的任务,并向用户简要回报这次非计划重启;若任务已不再适用,简要说明原因后停止。`;
|
|
219
|
-
return `[ankh-guard] 服务于 ${time} 重启,退出${record.error !== void 0 ? `失败(${record.error})` : "成功"},${canaryPending ? "金丝雀尚未完成" : "金丝雀已处理"}。你上次正在进行的回合被中断(日志已标记 interrupted)。请检查当前状态并继续未完成的任务,并向用户简要回报本次重启结果;若任务已不再适用,简要说明原因后停止。`;
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* Acknowledge a restart record so it injects exactly once.
|
|
223
|
-
* @param stateDir - state directory.
|
|
224
|
-
* @param record - the record being reported.
|
|
225
|
-
* @param now - epoch milliseconds of the acknowledgement.
|
|
226
|
-
*/
|
|
227
|
-
function acknowledgeRestartRecord(stateDir, record, now) {
|
|
228
|
-
try {
|
|
229
|
-
atomicWrite(restartRecordFile(stateDir), `${JSON.stringify({
|
|
230
|
-
...record,
|
|
231
|
-
reportedAt: now
|
|
232
|
-
})}\n`);
|
|
233
|
-
} catch {}
|
|
234
|
-
}
|
|
235
|
-
/**
|
|
236
|
-
* Record an unplanned-exit recovery (the watchdog respawned the instance with
|
|
237
|
-
* no restart marker), unless a record still awaits its report. Returns whether
|
|
238
|
-
* the record was written — the caller (CLI verb, invoked by the watchdog) logs
|
|
239
|
-
* either way, so the watchdog log must never claim a write that was skipped.
|
|
240
|
-
* @param stateDir - state directory.
|
|
241
|
-
* @param now - epoch milliseconds of the recovery.
|
|
242
|
-
* @returns true when the record was written, false when a pending record was kept.
|
|
243
|
-
*/
|
|
244
|
-
function writeUnexpectedExitRecord(stateDir, now) {
|
|
245
|
-
if (pendingRestartRecord(stateDir) !== null) return false;
|
|
246
|
-
mkdirSync(stateDir, { recursive: true });
|
|
247
|
-
atomicWrite(restartRecordFile(stateDir), `${JSON.stringify({
|
|
248
|
-
exitAt: now,
|
|
249
|
-
unexpected: true
|
|
250
|
-
})}\n`);
|
|
251
|
-
return true;
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Record the restart verb's outcome for the report machinery (the restart
|
|
255
|
-
* verb is otherwise invisible to it: it writes no marker and no record, so a
|
|
256
|
-
* restart it drove would never be reported to any session). Mirrors the exit
|
|
257
|
-
* agent's record semantics; a still-pending earlier record is replaced only
|
|
258
|
-
* by a completed newer restart.
|
|
259
|
-
* @param stateDir - state directory.
|
|
260
|
-
* @param record - the outcome fields (exitAt/pid for a stop, error on failure).
|
|
261
|
-
*/
|
|
262
|
-
function writeRestartOutcome(stateDir, record) {
|
|
263
|
-
mkdirSync(stateDir, { recursive: true });
|
|
264
|
-
atomicWrite(restartRecordFile(stateDir), `${JSON.stringify(record)}\n`);
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
|
-
* Record the watchdog's ADOPTION takeover — the first restart a deployment
|
|
268
|
-
* ever sees: `supervise` handed the port to the watchdog, which stopped the
|
|
269
|
-
* pre-existing owner and booted the supervised instance. The session that
|
|
270
|
-
* established supervision promised the user a verification report; without
|
|
271
|
-
* this record nothing wakes it after the bounce (the adoption writes no
|
|
272
|
-
* restart marker and no outcome record). Never overwrites a record that still
|
|
273
|
-
* awaits its report.
|
|
274
|
-
* @param stateDir - state directory.
|
|
275
|
-
* @param now - epoch milliseconds of the takeover boot.
|
|
276
|
-
* @param initiator - the session that established supervision, when known.
|
|
277
|
-
* @returns whether the record was written.
|
|
278
|
-
*/
|
|
279
|
-
function writeAdoptionRecord(stateDir, now, initiator) {
|
|
280
|
-
if (pendingRestartRecord(stateDir) !== null) return false;
|
|
281
|
-
mkdirSync(stateDir, { recursive: true });
|
|
282
|
-
atomicWrite(restartRecordFile(stateDir), `${JSON.stringify({
|
|
283
|
-
exitAt: now,
|
|
284
|
-
...initiator !== void 0 && initiator !== "" ? { initiator } : {}
|
|
285
|
-
})}\n`);
|
|
286
|
-
return true;
|
|
287
|
-
}
|
|
288
|
-
/**
|
|
289
|
-
* Persist the launch record (atomic). The instance-facing side of
|
|
290
|
-
* {@link writeInstanceLaunch}: only an `instance`-sourced record may be
|
|
291
|
-
* replaced by another — a `supervisor` record carries the FULL supervision
|
|
292
|
-
* chain (watchdog, launch wrapper) and the inner process must not overwrite
|
|
293
|
-
* it with its own bare argv.
|
|
294
|
-
* @param stateDir - state directory.
|
|
295
|
-
* @param launch - the launch facts.
|
|
296
|
-
* @returns whether the record was written.
|
|
297
|
-
*/
|
|
298
|
-
function writeInstanceLaunch(stateDir, launch) {
|
|
299
|
-
if (readInstanceLaunch(stateDir)?.source === "supervisor") return false;
|
|
300
|
-
mkdirSync(stateDir, { recursive: true });
|
|
301
|
-
atomicWrite(stateFile(stateDir, "instanceLaunch"), `${JSON.stringify(launch)}\n`);
|
|
302
|
-
return true;
|
|
303
|
-
}
|
|
304
|
-
/** POSIX single-quote one word for a shell command line. */
|
|
305
|
-
function shellQuote(word) {
|
|
306
|
-
return `'${word.replace(/'/g, "'\\''")}'`;
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* Render the instance's launch as a shell command: cwd, DSH_* env, and the
|
|
310
|
-
* FULL node invocation — execArgv included, because a tsx chain
|
|
311
|
-
* (`node --import tsx …`) rendered without it becomes a bare `node bin.ts`
|
|
312
|
-
* that cannot load TypeScript sources.
|
|
313
|
-
*/
|
|
314
|
-
/**
|
|
315
|
-
* Per-invocation transients, never launch configuration: the restart driver's
|
|
316
|
-
* own marker, the calling session's identity, and the shell/web hand-off
|
|
317
|
-
* vars. A recorded launch must not leak them into the next instance (a
|
|
318
|
-
* restart driver reading DSH_ANKH_RESTART_DRIVER would misbehave; a stale
|
|
319
|
-
* DSH_SESSION_ID misattributes reports).
|
|
320
|
-
*/
|
|
321
|
-
const TRANSIENT_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
322
|
-
"DSH_ANKH_RESTART_DRIVER",
|
|
323
|
-
"DSH_SESSION_ID",
|
|
324
|
-
"DSH_SESSION_JSONL",
|
|
325
|
-
"DSH_WEB_URL",
|
|
326
|
-
"DSH_SHELL"
|
|
327
|
-
]);
|
|
328
|
-
function buildLaunchCommand(execPath, execArgv, args, cwd, env) {
|
|
329
|
-
const envPart = Object.entries(env).filter(([key]) => !TRANSIENT_ENV_KEYS.has(key)).map(([key, value]) => `${key}=${shellQuote(value)}`).join(" ");
|
|
330
|
-
const argv = [
|
|
331
|
-
execPath,
|
|
332
|
-
...execArgv,
|
|
333
|
-
...args
|
|
334
|
-
].map(shellQuote).join(" ");
|
|
335
|
-
return `cd ${shellQuote(cwd)} && ${envPart !== "" ? `${envPart} ` : ""}${argv}`;
|
|
336
|
-
}
|
|
337
|
-
/** Replace any record unconditionally (the supervisor's own write path). */
|
|
338
|
-
function writeInstanceLaunchAsSupervisor(stateDir, launch) {
|
|
339
|
-
mkdirSync(stateDir, { recursive: true });
|
|
340
|
-
atomicWrite(stateFile(stateDir, "instanceLaunch"), `${JSON.stringify(launch)}\n`);
|
|
341
|
-
}
|
|
342
|
-
/**
|
|
343
|
-
* Read the launch record, or null when absent/unparseable (older deployments,
|
|
344
|
-
* or the plugin never applied in this home).
|
|
345
|
-
* @param stateDir - state directory.
|
|
346
|
-
* @returns the record, or null.
|
|
347
|
-
*/
|
|
348
|
-
function readInstanceLaunch(stateDir) {
|
|
349
|
-
try {
|
|
350
|
-
const launch = JSON.parse(readFileSync(stateFile(stateDir, "instanceLaunch"), "utf8"));
|
|
351
|
-
return typeof launch.command === "string" ? launch : null;
|
|
352
|
-
} catch {
|
|
353
|
-
return null;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
/**
|
|
357
|
-
* Read the shutdown snapshot, or null when absent/unparseable. Malformed
|
|
358
|
-
* snapshots are dropped by the caller's delete-after-read, never retried.
|
|
359
|
-
* @param stateDir - state directory.
|
|
360
|
-
* @returns the snapshot, or null.
|
|
361
|
-
*/
|
|
362
|
-
function readInterruptedSnapshot(stateDir) {
|
|
363
|
-
const file = interruptedSnapshotFile(stateDir);
|
|
364
|
-
if (!existsSync(file)) return null;
|
|
365
|
-
try {
|
|
366
|
-
const snapshot = JSON.parse(readFileSync(file, "utf8"));
|
|
367
|
-
if (!Array.isArray(snapshot.resume) || !Array.isArray(snapshot.interrupted)) return null;
|
|
368
|
-
return snapshot;
|
|
369
|
-
} catch {
|
|
370
|
-
return null;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
/**
|
|
374
|
-
* Write the shutdown snapshot (synchronous — called from a signal handler).
|
|
375
|
-
* The state directory is created on demand: a fresh deployment has no
|
|
376
|
-
* `$DSH_HOME/state` yet, and a missing directory must not silently drop the
|
|
377
|
-
* snapshot.
|
|
378
|
-
* @param stateDir - state directory.
|
|
379
|
-
* @param snapshot - the snapshot to persist.
|
|
380
|
-
*/
|
|
381
|
-
function writeInterruptedSnapshot(stateDir, snapshot) {
|
|
382
|
-
try {
|
|
383
|
-
mkdirSync(stateDir, { recursive: true });
|
|
384
|
-
atomicWrite(interruptedSnapshotFile(stateDir), `${JSON.stringify(snapshot)}\n`);
|
|
385
|
-
} catch {}
|
|
386
|
-
}
|
|
387
|
-
/** Write a small durable file atomically (tmp + rename in the same directory). */
|
|
388
|
-
function atomicWrite(file, content) {
|
|
389
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
390
|
-
writeFileSync(tmp, content);
|
|
391
|
-
renameSync(tmp, file);
|
|
392
|
-
}
|
|
393
|
-
/**
|
|
394
|
-
* Persist the skill-registration outcome (atomic, best-effort). A migration
|
|
395
|
-
* or repackaging that drops the skill is otherwise invisible until someone
|
|
396
|
-
* notices the catalog entry missing — this record lets `check-env` surface it.
|
|
397
|
-
* @param stateDir - state directory.
|
|
398
|
-
* @param record - the outcome of this boot's registration attempt.
|
|
399
|
-
*/
|
|
400
|
-
function writeSkillRegistration(stateDir, record) {
|
|
401
|
-
try {
|
|
402
|
-
mkdirSync(stateDir, { recursive: true });
|
|
403
|
-
atomicWrite(stateFile(stateDir, "skillRegistration"), `${JSON.stringify(record)}\n`);
|
|
404
|
-
} catch {}
|
|
405
|
-
}
|
|
406
|
-
/**
|
|
407
|
-
* Read the skill-registration record, or null when absent/unparseable (the
|
|
408
|
-
* plugin never applied with this state dir, or predates the record).
|
|
409
|
-
* @param stateDir - state directory.
|
|
410
|
-
* @returns the record, or null.
|
|
411
|
-
*/
|
|
412
|
-
function readSkillRegistration(stateDir) {
|
|
413
|
-
try {
|
|
414
|
-
const record = JSON.parse(readFileSync(stateFile(stateDir, "skillRegistration"), "utf8"));
|
|
415
|
-
return typeof record.registered === "boolean" && typeof record.at === "number" ? record : null;
|
|
416
|
-
} catch {
|
|
417
|
-
return null;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
//#endregion
|
|
421
|
-
export { writeUnexpectedExitRecord as _, interruptedSnapshotFile as a, resetToCheckpoint as b, readInterruptedSnapshot as c, writeAdoptionRecord as d, writeInstanceLaunch as f, writeSkillRegistration as g, writeRestartOutcome as h, continueInterruptedText as i, readSkillRegistration as l, writeInterruptedSnapshot as m, buildLaunchCommand as n, pendingRestartRecord as o, writeInstanceLaunchAsSupervisor as p, continueAndReportText as r, readInstanceLaunch as s, acknowledgeRestartRecord as t, restartContextText as u, commitCheckpoint as v, currentHead as y };
|