@cotal-ai/cli 0.50.1 → 0.51.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/dist/command.d.ts.map +1 -1
- package/dist/command.js +24 -1
- package/dist/command.js.map +1 -1
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +18 -1
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +114 -5
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/endpoints.d.ts.map +1 -1
- package/dist/commands/endpoints.js +2 -1
- package/dist/commands/endpoints.js.map +1 -1
- package/dist/commands/join.d.ts.map +1 -1
- package/dist/commands/join.js +25 -0
- package/dist/commands/join.js.map +1 -1
- package/dist/commands/meshes-add.d.ts +2 -54
- package/dist/commands/meshes-add.d.ts.map +1 -1
- package/dist/commands/meshes-add.js +20 -105
- package/dist/commands/meshes-add.js.map +1 -1
- package/dist/commands/meshes.d.ts.map +1 -1
- package/dist/commands/meshes.js +5 -2
- package/dist/commands/meshes.js.map +1 -1
- package/dist/commands/service.d.ts +17 -0
- package/dist/commands/service.d.ts.map +1 -0
- package/dist/commands/service.js +541 -0
- package/dist/commands/service.js.map +1 -0
- package/dist/commands/spawn.d.ts +44 -2
- package/dist/commands/spawn.d.ts.map +1 -1
- package/dist/commands/spawn.js +292 -45
- package/dist/commands/spawn.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +11 -4
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/sync.d.ts +50 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +284 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/up.d.ts.map +1 -1
- package/dist/commands/up.js +71 -13
- package/dist/commands/up.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +26 -5
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/use.js +1 -1
- package/dist/commands/use.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/manifest/apply.d.ts.map +1 -1
- package/dist/lib/manifest/apply.js +2 -0
- package/dist/lib/manifest/apply.js.map +1 -1
- package/dist/lib/manifest/model.d.ts +2 -0
- package/dist/lib/manifest/model.d.ts.map +1 -1
- package/dist/lib/manifest/prepare.d.ts +2 -0
- package/dist/lib/manifest/prepare.d.ts.map +1 -1
- package/dist/lib/manifest/prepare.js +1 -0
- package/dist/lib/manifest/prepare.js.map +1 -1
- package/dist/lib/manifest/resolve.js +1 -0
- package/dist/lib/manifest/resolve.js.map +1 -1
- package/dist/lib/manifest/schema.d.ts +2 -0
- package/dist/lib/manifest/schema.d.ts.map +1 -1
- package/dist/lib/manifest/schema.js +2 -0
- package/dist/lib/manifest/schema.js.map +1 -1
- package/dist/lib/restore.d.ts +3 -0
- package/dist/lib/restore.d.ts.map +1 -1
- package/dist/lib/restore.js +17 -0
- package/dist/lib/restore.js.map +1 -1
- package/dist/lib/seat-admission.d.ts +51 -0
- package/dist/lib/seat-admission.d.ts.map +1 -0
- package/dist/lib/seat-admission.js +81 -0
- package/dist/lib/seat-admission.js.map +1 -0
- package/dist/lib/seat-capture.d.ts +47 -0
- package/dist/lib/seat-capture.d.ts.map +1 -0
- package/dist/lib/seat-capture.js +195 -0
- package/dist/lib/seat-capture.js.map +1 -0
- package/dist/lib/seat-restore.d.ts +39 -0
- package/dist/lib/seat-restore.d.ts.map +1 -0
- package/dist/lib/seat-restore.js +249 -0
- package/dist/lib/seat-restore.js.map +1 -0
- package/dist/lib/seat-resume.d.ts +24 -0
- package/dist/lib/seat-resume.d.ts.map +1 -0
- package/dist/lib/seat-resume.js +127 -0
- package/dist/lib/seat-resume.js.map +1 -0
- package/package.json +5 -5
- package/dist/lib/join-target.d.ts +0 -173
- package/dist/lib/join-target.d.ts.map +0 -1
- package/dist/lib/join-target.js +0 -335
- package/dist/lib/join-target.js.map +0 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type SeatCheckpoint } from "@cotal-ai/workspace";
|
|
2
|
+
/** One admitted seat this restore is about to place, with the destination paths it lands on. */
|
|
3
|
+
export interface SeatRestoreRequest {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
/** The directory the checkpoint was read from, re-verified here over the files as they are now. */
|
|
6
|
+
readonly directory: string;
|
|
7
|
+
readonly checkpoint: SeatCheckpoint;
|
|
8
|
+
/** The working tree the manager will launch this seat in, from the retained inventory. */
|
|
9
|
+
readonly cwd: string;
|
|
10
|
+
/** The session id the retained inventory says this seat reopens, when it has one. A pointer that
|
|
11
|
+
* names a different session is not this seat's pointer. */
|
|
12
|
+
readonly inventorySessionId?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface SeatRestoreOptions {
|
|
15
|
+
/** The destination workspace root, the anchor a `workspace-root` destination resolves against. */
|
|
16
|
+
readonly root: string;
|
|
17
|
+
readonly seats: readonly SeatRestoreRequest[];
|
|
18
|
+
/** Reports each promoted seat, so the command can print what it placed. */
|
|
19
|
+
readonly onPromoted?: (seat: {
|
|
20
|
+
name: string;
|
|
21
|
+
cwd: string;
|
|
22
|
+
superseded?: string;
|
|
23
|
+
}) => void;
|
|
24
|
+
}
|
|
25
|
+
/** Every refusal below. The message names the seat, the step, and what git or tar actually said. */
|
|
26
|
+
export declare class SeatRestoreError extends Error {
|
|
27
|
+
constructor(message: string);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Restore every admitted seat, or leave every live `cwd` as it was.
|
|
31
|
+
*
|
|
32
|
+
* Two phases. Every seat is staged first, so a refusal on the last seat costs nothing on the first.
|
|
33
|
+
* Only once every staging directory is complete does any promotion happen, and a failure during the
|
|
34
|
+
* promotion phase puts back what this call moved: the renames it made are undone in reverse and the
|
|
35
|
+
* session files it exclusively created are removed, so the destination is where it started and the
|
|
36
|
+
* whole set can be retried once the refusal is understood.
|
|
37
|
+
*/
|
|
38
|
+
export declare function restoreSeatCheckpoints(options: SeatRestoreOptions): void;
|
|
39
|
+
//# sourceMappingURL=seat-restore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seat-restore.d.ts","sourceRoot":"","sources":["../../src/lib/seat-restore.ts"],"names":[],"mappings":"AAoCA,OAAO,EAEL,KAAK,cAAc,EAGpB,MAAM,qBAAqB,CAAC;AAE7B,gGAAgG;AAChG,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,mGAAmG;IACnG,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,cAAc,CAAC;IACpC,0FAA0F;IAC1F,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;gEAC4D;IAC5D,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACtC;AAED,MAAM,WAAW,kBAAkB;IACjC,kGAAkG;IAClG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAC9C,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CAC1F;AAED,oGAAoG;AACpG,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AA4JD;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,CA+BxE"}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Restore: the destination consumes an admitted seat checkpoint before the seat is alive.
|
|
3
|
+
*
|
|
4
|
+
* This is section 6 step 7 of `docs/design/session-recovery.md`, which that record gives as a shell
|
|
5
|
+
* sequence and calls manual only because no command carried it. Every rule stated there is a rule
|
|
6
|
+
* here, and the reasons are the same ones measured there:
|
|
7
|
+
*
|
|
8
|
+
* - The sequence stages. A clone lands in `<cwd>.incoming`, every verification and both applies run
|
|
9
|
+
* inside it, and the two renames that promote it are the only steps that touch the path the seat
|
|
10
|
+
* will use. A failed restore leaves the live `cwd` as it was.
|
|
11
|
+
* - An existing `<cwd>.incoming` refuses. A staging directory from a failed run is the only record
|
|
12
|
+
* of what failed, so nothing here removes one: an operator inspects it and removes it by hand.
|
|
13
|
+
* The shell version's `set -e` exists because a bare test reported and continued, and the final
|
|
14
|
+
* `mv` then promoted a leftover directory that was never restored. Here every step throws.
|
|
15
|
+
* - `git clone` rather than a fetch into an existing repository, because the refspec form that
|
|
16
|
+
* would populate it fails against the branch HEAD is on, and clone avoids the question.
|
|
17
|
+
* - The recorded base is verified in the clone. A bundle that does not contain it does not apply to
|
|
18
|
+
* the tree this checkpoint describes.
|
|
19
|
+
* - Two applies, `--index` first and the worktree diff second, both `--binary --allow-empty`. The
|
|
20
|
+
* order is what puts staged content back in the index rather than only in the worktree, and
|
|
21
|
+
* `--allow-empty` is why the cleanest possible seat is still restorable.
|
|
22
|
+
* - A pre-existing `cwd` is moved aside under a timestamped name rather than deleted, so a wrong
|
|
23
|
+
* checkpoint costs a rename instead of a tree.
|
|
24
|
+
*
|
|
25
|
+
* Two things this adds over the manual sequence. The session files travel with the tree, to the
|
|
26
|
+
* paths the destination's own connector will read. And the promoted tree is re-read with
|
|
27
|
+
* `git status --porcelain` and compared to what the checkpoint recorded, so a restore that applied
|
|
28
|
+
* without error and still produced a different index is a refusal rather than a silent difference.
|
|
29
|
+
*
|
|
30
|
+
* git and tar run as child processes through `execFileSync`, never a shell string.
|
|
31
|
+
*/
|
|
32
|
+
import { execFileSync } from "node:child_process";
|
|
33
|
+
import { createHash } from "node:crypto";
|
|
34
|
+
import { closeSync, constants, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeSync } from "node:fs";
|
|
35
|
+
import { homedir } from "node:os";
|
|
36
|
+
import { dirname, join, resolve } from "node:path";
|
|
37
|
+
import { assertSeatCheckpointIntegrity, } from "@cotal-ai/workspace";
|
|
38
|
+
/** Every refusal below. The message names the seat, the step, and what git or tar actually said. */
|
|
39
|
+
export class SeatRestoreError extends Error {
|
|
40
|
+
constructor(message) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = "SeatRestoreError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function run(command, args, cwd, seat, step) {
|
|
46
|
+
try {
|
|
47
|
+
// Both streams are captured. git writes progress and advice to stderr even when it succeeds,
|
|
48
|
+
// and the default here would forward that to the operator's terminal as if the resume had
|
|
49
|
+
// failed; on a failure the same bytes are what the refusal has to name.
|
|
50
|
+
return execFileSync(command, args, {
|
|
51
|
+
cwd,
|
|
52
|
+
encoding: "utf8",
|
|
53
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
54
|
+
maxBuffer: 1024 * 1024 * 1024,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
const failure = error;
|
|
59
|
+
const output = `${failure.stderr ?? ""}${failure.stdout ?? ""}`.trim() || failure.message || "no output";
|
|
60
|
+
throw new SeatRestoreError(`seat ${seat}: ${step} failed (${command}): ${output}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Where one captured session file lands on THIS host.
|
|
65
|
+
*
|
|
66
|
+
* The anchors are the destination's own: its workspace root, the account home it runs under, and
|
|
67
|
+
* the seat's working tree. The record carries a relative path and the reader already refused an
|
|
68
|
+
* absolute one or a `..` segment, so the result cannot leave the anchor it names.
|
|
69
|
+
*/
|
|
70
|
+
function destinationPath(destination, root, cwd) {
|
|
71
|
+
const anchor = destination.anchor === "workspace-root" ? root : destination.anchor === "home" ? homedir() : cwd;
|
|
72
|
+
return join(anchor, destination.path);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Write one captured session file where the connector will read it.
|
|
76
|
+
*
|
|
77
|
+
* Durable, private and exclusive. A file already at that path is judged by CONTENT, which is the
|
|
78
|
+
* same rule the rest of the checkpoint is judged by: a destination whose file already hashes to the
|
|
79
|
+
* recorded digest holds the bytes this step exists to place, and that is the ordinary same-host
|
|
80
|
+
* resume, where nothing removed the transcript the seat was writing. Different bytes under the path
|
|
81
|
+
* the connector is about to read are a different session, and that is refused rather than
|
|
82
|
+
* clobbered: the harness transcript is the one thing here Cotal cannot reconstruct.
|
|
83
|
+
*/
|
|
84
|
+
function placeSessionFile(seat, file, root, created) {
|
|
85
|
+
const path = destinationPath(file.restore, root, seat.cwd);
|
|
86
|
+
const bytes = readFileSync(join(seat.directory, file.path));
|
|
87
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
88
|
+
let fd;
|
|
89
|
+
try {
|
|
90
|
+
fd = openSync(path, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error.code === "EEXIST") {
|
|
94
|
+
const present = createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
95
|
+
if (present === file.sha256)
|
|
96
|
+
return;
|
|
97
|
+
throw new SeatRestoreError(`seat ${seat.name}: ${path} already holds different bytes than the checkpoint recorded (${present} against ${file.sha256}); inspect it and move it aside before resuming`);
|
|
98
|
+
}
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
created.push(path);
|
|
102
|
+
try {
|
|
103
|
+
writeSync(fd, bytes);
|
|
104
|
+
fsyncSync(fd);
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
closeSync(fd);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** The sha256-verified pointer's own session id, refused when it is not the one this seat reopens. */
|
|
111
|
+
function assertPointerSession(seat, pointer) {
|
|
112
|
+
const recorded = seat.checkpoint.session.sessionId ?? seat.inventorySessionId;
|
|
113
|
+
if (!recorded)
|
|
114
|
+
return;
|
|
115
|
+
let parsed;
|
|
116
|
+
try {
|
|
117
|
+
parsed = JSON.parse(readFileSync(join(seat.directory, pointer.path), "utf8"));
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
throw new SeatRestoreError(`seat ${seat.name}: captured session pointer is not readable JSON (${error.message})`);
|
|
121
|
+
}
|
|
122
|
+
if (typeof parsed.sessionId !== "string" || parsed.sessionId !== recorded)
|
|
123
|
+
throw new SeatRestoreError(`seat ${seat.name}: captured session pointer names session ${JSON.stringify(parsed.sessionId)}, this resume reopens ${JSON.stringify(recorded)}`);
|
|
124
|
+
if (seat.inventorySessionId && seat.checkpoint.session.sessionId && seat.inventorySessionId !== seat.checkpoint.session.sessionId)
|
|
125
|
+
throw new SeatRestoreError(`seat ${seat.name}: checkpoint records session ${seat.checkpoint.session.sessionId}, the retained inventory says ${seat.inventorySessionId}`);
|
|
126
|
+
}
|
|
127
|
+
/** Stage one seat: everything up to and including the extraction, inside `<cwd>.incoming` alone. */
|
|
128
|
+
function stageSeat(seat, staging) {
|
|
129
|
+
// THE ONE TREE A PROMOTION MAY NOT REPLACE.
|
|
130
|
+
//
|
|
131
|
+
// A checkpoint deliberately excludes the control directory (see `UNTRACKED_SELECTION`): it holds
|
|
132
|
+
// the broker trust material, the space account, the seat's own credentials and the maintenance
|
|
133
|
+
// journal this very resume is reading, and a checkpoint carries credential references rather than
|
|
134
|
+
// values. Promotion replaces the whole `cwd`, so when the seat's `cwd` IS a workspace root, which
|
|
135
|
+
// is the ordinary layout an operator gets by running `cotal up` and `cotal spawn` in one
|
|
136
|
+
// directory, promoting a clone that cannot contain `.cotal/` moves the live control directory
|
|
137
|
+
// aside and leaves the destination with none.
|
|
138
|
+
//
|
|
139
|
+
// Moving the control directory across the promotion is not the answer: its contents are live
|
|
140
|
+
// while this command runs. So this refuses, and names the remedy rather than degrading.
|
|
141
|
+
const control = join(resolve(seat.cwd), ".cotal");
|
|
142
|
+
if (existsSync(control))
|
|
143
|
+
throw new SeatRestoreError(`seat ${seat.name}: ${resolve(seat.cwd)} holds a control directory at ${control}, and a checkpoint never carries one, so promoting the restored tree over it would move this destination's live trust material and maintenance state aside; give the seat a working tree that is not a workspace root, then resume`);
|
|
144
|
+
// The digests again, over the files as they are NOW. Gate 1 ran before custody moved; a
|
|
145
|
+
// checkpoint that changed between the gate and the bytes being applied is not the admitted one.
|
|
146
|
+
try {
|
|
147
|
+
assertSeatCheckpointIntegrity(seat.directory, seat.checkpoint);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
throw new SeatRestoreError(`seat ${seat.name}: re-verifying the checkpoint before restore failed: ${error.message}`);
|
|
151
|
+
}
|
|
152
|
+
// Before the clone, because this decides nothing about the tree and therefore should cost
|
|
153
|
+
// nothing: a pointer that names another session leaves no staging directory to inspect for a
|
|
154
|
+
// step that never ran.
|
|
155
|
+
if (seat.checkpoint.session.pointer)
|
|
156
|
+
assertPointerSession(seat, seat.checkpoint.session.pointer);
|
|
157
|
+
// A leftover staging directory is evidence of a failed run, never a tree to promote, and nothing
|
|
158
|
+
// here removes it.
|
|
159
|
+
if (existsSync(staging))
|
|
160
|
+
throw new SeatRestoreError(`seat ${seat.name}: ${staging} already exists; a staging directory from a failed restore is evidence - inspect it and remove it by hand, then resume`);
|
|
161
|
+
const parent = dirname(staging);
|
|
162
|
+
mkdirSync(parent, { recursive: true });
|
|
163
|
+
const repository = seat.checkpoint.repository;
|
|
164
|
+
run("git", ["clone", "--quiet", join(seat.directory, repository.bundle.path), staging], parent, seat.name, "cloning the bundle");
|
|
165
|
+
run("git", ["rev-parse", "--verify", `${repository.base}^{commit}`], staging, seat.name, `verifying the recorded base ${repository.base}`);
|
|
166
|
+
run("git", ["checkout", "--quiet", "--detach", repository.base], staging, seat.name, "checking out the recorded base");
|
|
167
|
+
run("git", ["apply", "--binary", "--allow-empty", "--index", join(seat.directory, repository.indexDiff.path)], staging, seat.name, "applying the index diff");
|
|
168
|
+
run("git", ["apply", "--binary", "--allow-empty", join(seat.directory, repository.worktreeDiff.path)], staging, seat.name, "applying the worktree diff");
|
|
169
|
+
run("tar", ["-xf", join(seat.directory, repository.untracked.path)], staging, seat.name, "extracting the untracked archive");
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Move an existing `cwd` aside under a timestamped name, and return the name it went to.
|
|
173
|
+
*
|
|
174
|
+
* The name has to be one this call can prove it owns. A whole-second timestamp is not that: two
|
|
175
|
+
* promotions of the same seat inside one second compute the same path, and the second `rename`
|
|
176
|
+
* lands on the non-empty directory the first one just created. A refusal naming a rename instead
|
|
177
|
+
* of the real cause is not acceptable for the one step that exists to keep a superseded tree.
|
|
178
|
+
*
|
|
179
|
+
* So the rename IS the claim. Nothing creates the timestamped name ahead of the move: an
|
|
180
|
+
* exclusive `mkdir` first and a rename onto that empty directory is the replace POSIX allows, but
|
|
181
|
+
* Windows refuses to rename onto any existing directory (EPERM), so a claim made that way can never
|
|
182
|
+
* be redeemed there. Renaming onto a name that already holds a tree fails on every platform
|
|
183
|
+
* (ENOTEMPTY or EEXIST on POSIX, EPERM on Windows), and that failure is read as "taken" and retried
|
|
184
|
+
* with a counter. A rename that fails for any other reason is the error it is.
|
|
185
|
+
*/
|
|
186
|
+
function supersede(cwd, seat) {
|
|
187
|
+
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
188
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
189
|
+
const candidate = `${cwd}.superseded.${stamp}${attempt ? `.${attempt}` : ""}`;
|
|
190
|
+
try {
|
|
191
|
+
renameSync(cwd, candidate);
|
|
192
|
+
return candidate;
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
const code = error.code;
|
|
196
|
+
if ((code === "ENOTEMPTY" || code === "EEXIST" || code === "EPERM") && existsSync(candidate))
|
|
197
|
+
continue;
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
throw new SeatRestoreError(`seat ${seat}: could not claim a free superseded name beside ${cwd}; ${cwd}.superseded.${stamp}[.0-99] are all taken, so an earlier tree would be overwritten`);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Restore every admitted seat, or leave every live `cwd` as it was.
|
|
205
|
+
*
|
|
206
|
+
* Two phases. Every seat is staged first, so a refusal on the last seat costs nothing on the first.
|
|
207
|
+
* Only once every staging directory is complete does any promotion happen, and a failure during the
|
|
208
|
+
* promotion phase puts back what this call moved: the renames it made are undone in reverse and the
|
|
209
|
+
* session files it exclusively created are removed, so the destination is where it started and the
|
|
210
|
+
* whole set can be retried once the refusal is understood.
|
|
211
|
+
*/
|
|
212
|
+
export function restoreSeatCheckpoints(options) {
|
|
213
|
+
const staged = options.seats.map((seat) => ({ seat, staging: `${resolve(seat.cwd)}.incoming` }));
|
|
214
|
+
for (const { seat, staging } of staged)
|
|
215
|
+
stageSeat(seat, staging);
|
|
216
|
+
const promoted = [];
|
|
217
|
+
const created = [];
|
|
218
|
+
try {
|
|
219
|
+
for (const { seat, staging } of staged) {
|
|
220
|
+
const cwd = resolve(seat.cwd);
|
|
221
|
+
const superseded = existsSync(cwd) ? supersede(cwd, seat.name) : undefined;
|
|
222
|
+
renameSync(staging, cwd);
|
|
223
|
+
promoted.push({ cwd, staging, ...(superseded ? { superseded } : {}) });
|
|
224
|
+
// Before the status is read, for the same reason the capture read it with the store in place:
|
|
225
|
+
// a store file inside the tree is part of the state the checkpoint recorded.
|
|
226
|
+
if (seat.checkpoint.session.pointer)
|
|
227
|
+
placeSessionFile(seat, seat.checkpoint.session.pointer, options.root, created);
|
|
228
|
+
for (const file of seat.checkpoint.session.store)
|
|
229
|
+
placeSessionFile(seat, file, options.root, created);
|
|
230
|
+
// The bytes landed and git accepted them; this is the only check that sees a restore which
|
|
231
|
+
// applied cleanly and still produced a different index.
|
|
232
|
+
const status = run("git", ["status", "--porcelain"], cwd, seat.name, "re-reading the restored working tree status");
|
|
233
|
+
if (status !== seat.checkpoint.repository.status)
|
|
234
|
+
throw new SeatRestoreError(`seat ${seat.name}: the restored tree reports a different status than the checkpoint recorded\n recorded: ${JSON.stringify(seat.checkpoint.repository.status)}\n restored: ${JSON.stringify(status)}`);
|
|
235
|
+
options.onPromoted?.({ name: seat.name, cwd, ...(superseded ? { superseded } : {}) });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
for (const path of created.reverse())
|
|
240
|
+
rmSync(path, { force: true });
|
|
241
|
+
for (const entry of promoted.reverse()) {
|
|
242
|
+
renameSync(entry.cwd, entry.staging);
|
|
243
|
+
if (entry.superseded)
|
|
244
|
+
renameSync(entry.superseded, entry.cwd);
|
|
245
|
+
}
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
//# sourceMappingURL=seat-restore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seat-restore.js","sourceRoot":"","sources":["../../src/lib/seat-restore.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** One stale checkpoint the operator deliberately admitted, for the resume journal. */
|
|
2
|
+
export interface StaleCheckpointConsent {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly capturedAt: string;
|
|
5
|
+
readonly ageMs: number;
|
|
6
|
+
readonly horizonMs: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Run section 2.2's gates over every seat checkpoint the cut wrote, restore the captured bytes,
|
|
10
|
+
* then take custody of each seat.
|
|
11
|
+
*
|
|
12
|
+
* Gate 2 is told which lifecycle uids are live. On these paths that set is empty and the reason is
|
|
13
|
+
* structural rather than an omission: a preserved resume is only reached from a `ready` journal,
|
|
14
|
+
* which `down --preserve-state` publishes after it has proven every recorded process stopped and
|
|
15
|
+
* the exact recorded endpoint unreachable. There is no roster to consult because there is no
|
|
16
|
+
* broker. A destination that resumes against a live space supplies the roster instead.
|
|
17
|
+
*
|
|
18
|
+
* The restore runs inside the admission, after every gate and before any generation is claimed, so
|
|
19
|
+
* a refusal costs nothing: no tree has been promoted, no claim has been made, nothing has started.
|
|
20
|
+
*/
|
|
21
|
+
export declare function admitAndRestoreSeatCheckpoints(root: string, space: string, attemptId: string, values: {
|
|
22
|
+
"accept-stale-checkpoint"?: boolean;
|
|
23
|
+
}, inventory: unknown): StaleCheckpointConsent[];
|
|
24
|
+
//# sourceMappingURL=seat-resume.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seat-resume.d.ts","sourceRoot":"","sources":["../../src/lib/seat-resume.ts"],"names":[],"mappings":"AAyBA,uFAAuF;AACvF,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AASD;;;;;;;;;;;;GAYG;AACH,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE;IAAE,yBAAyB,CAAC,EAAE,OAAO,CAAA;CAAE,EAC/C,SAAS,EAAE,OAAO,GACjB,sBAAsB,EAAE,CAuF1B"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admission plus restore for one preserved cut, as both destination paths run it.
|
|
3
|
+
*
|
|
4
|
+
* `cotal up` from a `ready` journal and `cotal up --restore <dir>` reach a preserved resume by
|
|
5
|
+
* different routes and hand the same retained inventory to the same manager operation. They
|
|
6
|
+
* therefore owe the seats the same thing: section 2.2's gates, the restore of section 6 step 7, and
|
|
7
|
+
* the writer generation, in that order, before a manager launches. Keeping one implementation is
|
|
8
|
+
* what stops the two routes from drifting into one gated path and one ungated one.
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { seatCheckpointDir } from "@cotal-ai/workspace";
|
|
13
|
+
import { admitSeatCheckpoints } from "./seat-admission.js";
|
|
14
|
+
import { restoreSeatCheckpoints } from "./seat-restore.js";
|
|
15
|
+
import { c } from "../ui.js";
|
|
16
|
+
/** This host's current revision for a seat's launch config, or undefined when the file is gone.
|
|
17
|
+
* A missing profile is not a mismatch: the manager's own relaunch check reports that separately,
|
|
18
|
+
* and inventing a digest here would turn an absent file into a false profile refusal. */
|
|
19
|
+
function digestFileOrUndefined(path) {
|
|
20
|
+
try {
|
|
21
|
+
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Run section 2.2's gates over every seat checkpoint the cut wrote, restore the captured bytes,
|
|
29
|
+
* then take custody of each seat.
|
|
30
|
+
*
|
|
31
|
+
* Gate 2 is told which lifecycle uids are live. On these paths that set is empty and the reason is
|
|
32
|
+
* structural rather than an omission: a preserved resume is only reached from a `ready` journal,
|
|
33
|
+
* which `down --preserve-state` publishes after it has proven every recorded process stopped and
|
|
34
|
+
* the exact recorded endpoint unreachable. There is no roster to consult because there is no
|
|
35
|
+
* broker. A destination that resumes against a live space supplies the roster instead.
|
|
36
|
+
*
|
|
37
|
+
* The restore runs inside the admission, after every gate and before any generation is claimed, so
|
|
38
|
+
* a refusal costs nothing: no tree has been promoted, no claim has been made, nothing has started.
|
|
39
|
+
*/
|
|
40
|
+
export function admitAndRestoreSeatCheckpoints(root, space, attemptId, values, inventory) {
|
|
41
|
+
// The checkpoints of the CUT this resume is consuming, named by that cut's attempt id. A resume
|
|
42
|
+
// must not admit an older cut's artifacts: they describe a tree and a transcript this inventory
|
|
43
|
+
// is not about to restore.
|
|
44
|
+
const checkpointDir = seatCheckpointDir(root, attemptId);
|
|
45
|
+
// The seats this resume is about to hand the manager. Admission is reconciled against this set,
|
|
46
|
+
// because "no checkpoint" and "no seat" look identical to a reader of the checkpoint directory
|
|
47
|
+
// alone, and only one of them is safe.
|
|
48
|
+
const retainedAgents = (inventory.agents ?? []);
|
|
49
|
+
const retained = new Map(retainedAgents.map((agent) => [agent.name ?? "", agent]));
|
|
50
|
+
const admitted = admitSeatCheckpoints({
|
|
51
|
+
root,
|
|
52
|
+
checkpointDir,
|
|
53
|
+
space,
|
|
54
|
+
// This host's current revision for each seat's launch source, digested from the same file the
|
|
55
|
+
// manager re-digests before it relaunches. Passing it is what makes gate 2's profile half a
|
|
56
|
+
// decision: without it the gate has nothing to compare and the override decides nothing.
|
|
57
|
+
currentProfileConfigSha256: (name) => {
|
|
58
|
+
const source = retained.get(name)?.launch?.source;
|
|
59
|
+
return source ? digestFileOrUndefined(source.configPath) : undefined;
|
|
60
|
+
},
|
|
61
|
+
liveLifecycleUids: new Set(),
|
|
62
|
+
// Reconcile inside the admission, after the gates and BEFORE any generation is claimed. A
|
|
63
|
+
// retained seat with no checkpoint would otherwise resume with no gate run and no custody
|
|
64
|
+
// claimed, and checking after the claims had landed would leave the earlier seats' exclusive
|
|
65
|
+
// creates behind, so the retry over a repaired checkpoint set could never make them again.
|
|
66
|
+
requireCovered: (admittedNames) => {
|
|
67
|
+
const covered = new Set(admittedNames);
|
|
68
|
+
const uncovered = retainedAgents.map((agent) => agent.name ?? "").filter((name) => !covered.has(name));
|
|
69
|
+
if (uncovered.length)
|
|
70
|
+
throw new Error(`preserved resume is refused: retained seat(s) ${uncovered.map((name) => JSON.stringify(name)).join(", ")} have no admitted checkpoint under ${checkpointDir}; a seat cannot resume without passing the admission gates and claiming its writer generation`);
|
|
71
|
+
},
|
|
72
|
+
...(values["accept-stale-checkpoint"] ? { acceptStale: true } : {}),
|
|
73
|
+
// The restore, between the gates and custody. Every admitted seat's captured bytes land in a
|
|
74
|
+
// staging directory beside its cwd and are promoted only once every seat has staged; a refusal
|
|
75
|
+
// here leaves every live cwd as it was and claims no generation, like a gate failure.
|
|
76
|
+
beforeCustody: (staged) => {
|
|
77
|
+
// THE INCARNATION RECONCILIATION, AND IT RUNS BEFORE THE RESTORE TOUCHES ANYTHING.
|
|
78
|
+
//
|
|
79
|
+
// This compares the checkpoint's recovered `lifecycleUid` against the one the inventory this
|
|
80
|
+
// resume is about to hand the manager carries, and it used to run after the whole admission
|
|
81
|
+
// returned. By then the restore had already moved each live `cwd` aside and promoted another
|
|
82
|
+
// seat's tree over it, and the generation had been claimed under the checkpoint's uid, so the
|
|
83
|
+
// refusal arrived after the two irreversible steps it exists to prevent. A mismatch means the
|
|
84
|
+
// checkpoint describes a different incarnation than the seat being resumed, which is the last
|
|
85
|
+
// moment where refusing is still free.
|
|
86
|
+
for (const seat of staged) {
|
|
87
|
+
const expected = retained.get(seat.checkpoint.name)?.identity?.lifecycleUid ?? "";
|
|
88
|
+
if (expected && expected !== seat.checkpoint.lifecycleUid)
|
|
89
|
+
throw new Error(`seat ${seat.checkpoint.name}: checkpoint records lifecycle ${seat.checkpoint.lifecycleUid}, the retained inventory says ${expected}`);
|
|
90
|
+
}
|
|
91
|
+
restoreSeatCheckpoints({
|
|
92
|
+
root,
|
|
93
|
+
seats: staged.map((seat) => {
|
|
94
|
+
const entry = retained.get(seat.checkpoint.name);
|
|
95
|
+
const cwd = entry?.launch?.cwd;
|
|
96
|
+
if (!cwd)
|
|
97
|
+
throw new Error(`preserved resume is refused: retained seat ${JSON.stringify(seat.checkpoint.name)} records no launch cwd, so its checkpoint has no working tree to restore into`);
|
|
98
|
+
const sessionId = entry?.launch?.sessionId;
|
|
99
|
+
return {
|
|
100
|
+
name: seat.checkpoint.name,
|
|
101
|
+
directory: seat.directory,
|
|
102
|
+
checkpoint: seat.checkpoint,
|
|
103
|
+
cwd,
|
|
104
|
+
...(sessionId ? { inventorySessionId: sessionId } : {}),
|
|
105
|
+
};
|
|
106
|
+
}),
|
|
107
|
+
onPromoted: (seat) => {
|
|
108
|
+
console.log(c.dim(` restored working tree: ${seat.name} -> ${seat.cwd}${seat.superseded ? ` (previous tree kept at ${seat.superseded})` : ""}`));
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
for (const seat of admitted) {
|
|
114
|
+
console.log(c.dim(` admitted checkpoint: ${seat.checkpoint.name} (${seat.checkpoint.session.continuity}) at generation ${seat.generation}`));
|
|
115
|
+
if (seat.staleOverrideAgeMs !== undefined)
|
|
116
|
+
console.log(c.yellow(` stale checkpoint admitted by --accept-stale-checkpoint: ${seat.checkpoint.name} is ${seat.staleOverrideAgeMs}ms past capture, horizon ${seat.checkpoint.recencyHorizonMs}ms`));
|
|
117
|
+
}
|
|
118
|
+
return admitted
|
|
119
|
+
.filter((seat) => seat.staleOverrideAgeMs !== undefined)
|
|
120
|
+
.map((seat) => ({
|
|
121
|
+
name: seat.checkpoint.name,
|
|
122
|
+
capturedAt: seat.checkpoint.capturedAt,
|
|
123
|
+
ageMs: seat.staleOverrideAgeMs,
|
|
124
|
+
horizonMs: seat.checkpoint.recencyHorizonMs,
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=seat-resume.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seat-resume.js","sourceRoot":"","sources":["../../src/lib/seat-resume.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cotal-ai/cli",
|
|
3
3
|
"description": "Cotal mesh CLI: up, join, watch, console, spawn, mint, channels, history, ext.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.51.0",
|
|
5
5
|
"homepage": "https://cotal.ai",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"yaml": "^2.9.0",
|
|
30
30
|
"zod": "^4.4.3",
|
|
31
31
|
"ws": "^8.21.0",
|
|
32
|
-
"@cotal-ai/core": "0.
|
|
33
|
-
"@cotal-ai/workspace": "0.
|
|
32
|
+
"@cotal-ai/core": "0.51.0",
|
|
33
|
+
"@cotal-ai/workspace": "0.51.0"
|
|
34
34
|
},
|
|
35
35
|
"optionalDependencies": {
|
|
36
36
|
"@eplightning/nats-server-darwin-arm64": "^2.14.0",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/react": "^19.0.0",
|
|
45
45
|
"@types/ws": "^8.18.1",
|
|
46
|
-
"@cotal-ai/
|
|
47
|
-
"@cotal-ai/
|
|
46
|
+
"@cotal-ai/smoke-kit": "0.0.0",
|
|
47
|
+
"@cotal-ai/auth": "0.51.0"
|
|
48
48
|
},
|
|
49
49
|
"files": [
|
|
50
50
|
"dist",
|