@ricsam/r5d-worker 0.0.160 → 0.0.162
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/cjs/package.json +1 -1
- package/dist/mjs/internal-r5dctl.cjs +1 -1
- package/dist/mjs/main.mjs +2 -2
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/personal/publication-refusal.mjs +38 -0
- package/dist/mjs/personal/runtime.mjs +11 -12
- package/dist/mjs/runtime/workspace/authority.mjs +12 -4
- package/dist/mjs/runtime/workspace/files.mjs +18 -2
- package/dist/types/personal/publication-refusal.d.ts +31 -0
- package/dist/types/runtime/workspace/authority.d.ts +4 -0
- package/dist/types/runtime/workspace/files.d.ts +6 -0
- package/package.json +2 -2
package/dist/cjs/package.json
CHANGED
|
@@ -20605,7 +20605,7 @@ function resolveEntrypointPath(entrypoint) {
|
|
|
20605
20605
|
}
|
|
20606
20606
|
}
|
|
20607
20607
|
function getR5dctlVersion() {
|
|
20608
|
-
if (true) return "0.0.
|
|
20608
|
+
if (true) return "0.0.162";
|
|
20609
20609
|
const entrypoint = process.argv[1] ? resolveEntrypointPath(process.argv[1]) : null;
|
|
20610
20610
|
let current = entrypoint ? import_node_path2.default.dirname(entrypoint) : process.cwd();
|
|
20611
20611
|
for (let index = 0; index < 12; index += 1) {
|
package/dist/mjs/main.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { startManagerRpc } from "./runtime/releases/rpc-main.mjs";
|
|
|
7
7
|
import { ManagerRpcConfig } from "./runtime/releases/rpc-protocol.mjs";
|
|
8
8
|
const args = process.argv.slice(2);
|
|
9
9
|
if (args.includes("--version")) {
|
|
10
|
-
console.log(`r5d-worker ${true ? "0.0.
|
|
10
|
+
console.log(`r5d-worker ${true ? "0.0.162" : "development"}`);
|
|
11
11
|
} else if (!args.length || args.includes("--help")) {
|
|
12
12
|
console.log(
|
|
13
13
|
"Usage: r5d-worker start --label <label> [--root <dir>] [--base-url <url>] [--token <worker-token>]\n r5d-worker executor /absolute/private/config.json\n r5d-worker manager /absolute/private/config.json\n\nRun independently supervised durable runtime services. Provision configuration with r5dinfra."
|
|
@@ -15,7 +15,7 @@ if (args.includes("--version")) {
|
|
|
15
15
|
} else if (args[0] === "start") {
|
|
16
16
|
const runtime = await startPersonalWorker(
|
|
17
17
|
parsePersonalWorkerOptions(args.slice(1)),
|
|
18
|
-
true ? "0.0.
|
|
18
|
+
true ? "0.0.162" : "development"
|
|
19
19
|
);
|
|
20
20
|
console.log(`Worker connected: ${runtime.resourceId}`);
|
|
21
21
|
let closing = false;
|
package/dist/mjs/package.json
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const CONFLICT_PUBLICATION_CODES = /* @__PURE__ */ new Set([
|
|
2
|
+
"conflict",
|
|
3
|
+
"workbench_conflict",
|
|
4
|
+
"workbench_operation_in_progress"
|
|
5
|
+
]);
|
|
6
|
+
const BLOCKED_PUBLICATION_CODES = /* @__PURE__ */ new Set([
|
|
7
|
+
"secret_or_runtime_path",
|
|
8
|
+
"secret_content",
|
|
9
|
+
"unsafe_file",
|
|
10
|
+
"unsafe_git",
|
|
11
|
+
"unsafe_path",
|
|
12
|
+
"unsafe_tree",
|
|
13
|
+
"too_large"
|
|
14
|
+
]);
|
|
15
|
+
const PUBLICATION_OBSERVATION_THRESHOLDS = /* @__PURE__ */ new Map([["unsafe_file", 2]]);
|
|
16
|
+
const OPERATION_IN_PROGRESS_CODE = "workbench_operation_in_progress";
|
|
17
|
+
const ABANDONED_OPERATION_OBSERVATIONS = 2;
|
|
18
|
+
const STUCK_OPERATION_OBSERVATIONS = 30;
|
|
19
|
+
function publicationRefusalOutcome(code) {
|
|
20
|
+
if (CONFLICT_PUBLICATION_CODES.has(code)) return "conflict_blocked";
|
|
21
|
+
if (BLOCKED_PUBLICATION_CODES.has(code)) return "large_diff_blocked";
|
|
22
|
+
return "failed";
|
|
23
|
+
}
|
|
24
|
+
function publicationRefusalElects(code, observations, context = {}) {
|
|
25
|
+
if (code === OPERATION_IN_PROGRESS_CODE)
|
|
26
|
+
return context.workbenchHasLiveRun ? observations >= STUCK_OPERATION_OBSERVATIONS : observations >= ABANDONED_OPERATION_OBSERVATIONS;
|
|
27
|
+
return observations >= (PUBLICATION_OBSERVATION_THRESHOLDS.get(code) ?? 1);
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
ABANDONED_OPERATION_OBSERVATIONS,
|
|
31
|
+
BLOCKED_PUBLICATION_CODES,
|
|
32
|
+
CONFLICT_PUBLICATION_CODES,
|
|
33
|
+
OPERATION_IN_PROGRESS_CODE,
|
|
34
|
+
PUBLICATION_OBSERVATION_THRESHOLDS,
|
|
35
|
+
STUCK_OPERATION_OBSERVATIONS,
|
|
36
|
+
publicationRefusalElects,
|
|
37
|
+
publicationRefusalOutcome
|
|
38
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { publicationRefusalElects, publicationRefusalOutcome } from "./publication-refusal.mjs";
|
|
2
3
|
import { PersonalTcpManager } from "./tcp.mjs";
|
|
3
4
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
4
5
|
import { promises as fs } from "node:fs";
|
|
@@ -253,16 +254,6 @@ exec ${quote(executable)} ${quote(cli)} "$@"
|
|
|
253
254
|
}
|
|
254
255
|
}
|
|
255
256
|
}
|
|
256
|
-
const blockedPublicationCodes = /* @__PURE__ */ new Set([
|
|
257
|
-
"secret_or_runtime_path",
|
|
258
|
-
"secret_content",
|
|
259
|
-
"unsafe_file",
|
|
260
|
-
"unsafe_git",
|
|
261
|
-
"unsafe_path",
|
|
262
|
-
"unsafe_tree",
|
|
263
|
-
"too_large"
|
|
264
|
-
]);
|
|
265
|
-
const conflictPublicationCodes = /* @__PURE__ */ new Set(["conflict", "workbench_conflict"]);
|
|
266
257
|
const failedPublicationAttempts = /* @__PURE__ */ new Map();
|
|
267
258
|
let publication = null, closing = false;
|
|
268
259
|
const synchronize = () => {
|
|
@@ -287,13 +278,21 @@ exec ${quote(executable)} ${quote(cli)} "$@"
|
|
|
287
278
|
startingHead = status.head;
|
|
288
279
|
} catch {
|
|
289
280
|
}
|
|
290
|
-
const outcome =
|
|
281
|
+
const outcome = publicationRefusalOutcome(code);
|
|
291
282
|
const fingerprint = canonicalJson({ code, startingHead, outcome });
|
|
292
283
|
let attempt = failedPublicationAttempts.get(workbenchId);
|
|
293
284
|
if (!attempt || attempt.fingerprint !== fingerprint) {
|
|
294
|
-
attempt = { fingerprint, attemptId: randomUUID() };
|
|
285
|
+
attempt = { fingerprint, attemptId: randomUUID(), observations: 1 };
|
|
295
286
|
failedPublicationAttempts.set(workbenchId, attempt);
|
|
287
|
+
} else {
|
|
288
|
+
attempt.observations++;
|
|
296
289
|
}
|
|
290
|
+
let workbenchHasLiveRun = false;
|
|
291
|
+
try {
|
|
292
|
+
workbenchHasLiveRun = await authority.workbenchHasLiveRun({ userId: grant.userId, sessionId });
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
if (!publicationRefusalElects(code, attempt.observations, { workbenchHasLiveRun })) continue;
|
|
297
296
|
try {
|
|
298
297
|
await options.publicationReport(WorkspacePublicationReport.parse({
|
|
299
298
|
protocol: 1,
|
|
@@ -26,7 +26,8 @@ import {
|
|
|
26
26
|
selectedTree,
|
|
27
27
|
sha256,
|
|
28
28
|
snapshotTree,
|
|
29
|
-
sourceBytes
|
|
29
|
+
sourceBytes,
|
|
30
|
+
WORKBENCH_CONFLICT_CODES
|
|
30
31
|
} from "./files.mjs";
|
|
31
32
|
const GITHUB_CREDENTIAL_HELPER = '!f() { if [ "$1" = get ] && [ -n "$R5D_GIT_CREDENTIAL" ]; then git credential-store --file="$R5D_GIT_CREDENTIAL" get; else return 0; fi; }; f';
|
|
32
33
|
class WorkspaceAuthority {
|
|
@@ -417,6 +418,13 @@ class WorkspaceAuthority {
|
|
|
417
418
|
async assertAvailable(b, allowBlocked = false) {
|
|
418
419
|
if (b.state.blocked && !allowBlocked) throw new WorkspaceError(b.state.blocked.code, b.state.blocked.message);
|
|
419
420
|
}
|
|
421
|
+
/** Whether any run on this physical workbench is still live, across every
|
|
422
|
+
* session sharing it. An in-progress Git operation marker belongs to such a
|
|
423
|
+
* run, so publication defers to it instead of electing an incident over it. */
|
|
424
|
+
async workbenchHasLiveRun(identity) {
|
|
425
|
+
const b = await this.bench(identity);
|
|
426
|
+
return Object.values(b.state.runs).some((run) => !["completed", "cancelled", "rejected_capacity"].includes(run.state));
|
|
427
|
+
}
|
|
420
428
|
async status(identity) {
|
|
421
429
|
const b = await this.bench(identity);
|
|
422
430
|
return this.serial(b, async () => {
|
|
@@ -870,7 +878,7 @@ class WorkspaceAuthority {
|
|
|
870
878
|
let mode = 420;
|
|
871
879
|
let retainedBlock = null;
|
|
872
880
|
const check = async () => {
|
|
873
|
-
if (b.state.blocked && !(allowBlockedConflict && (b.state.blocked.code
|
|
881
|
+
if (b.state.blocked && !(allowBlockedConflict && WORKBENCH_CONFLICT_CODES.has(b.state.blocked.code)))
|
|
874
882
|
throw new WorkspaceError(b.state.blocked.code, b.state.blocked.message);
|
|
875
883
|
retainedBlock = b.state.blocked;
|
|
876
884
|
if (!b.state.initialized) throw new WorkspaceError("uninitialized", "Hydrate this workspace before writing files");
|
|
@@ -1154,7 +1162,7 @@ class WorkspaceAuthority {
|
|
|
1154
1162
|
const b = await this.bench(identity);
|
|
1155
1163
|
if (b.config.rootProfile === "account") return { head: "", unchanged: true };
|
|
1156
1164
|
return this.serial(b, async () => {
|
|
1157
|
-
const conflictBlocked = b.state.blocked
|
|
1165
|
+
const conflictBlocked = !!b.state.blocked && WORKBENCH_CONFLICT_CODES.has(b.state.blocked.code);
|
|
1158
1166
|
await this.assertAvailable(b, options.allowBlockedConflict === true && conflictBlocked);
|
|
1159
1167
|
return this.publishIdle(b, identity, "Destination workspace snapshot", options.allowLargeDiff === true);
|
|
1160
1168
|
});
|
|
@@ -1313,7 +1321,7 @@ class WorkspaceAuthority {
|
|
|
1313
1321
|
const b = await this.bench(identity);
|
|
1314
1322
|
const operation = OperationEnvelope.parse(JSON.parse(canonicalJson(input)));
|
|
1315
1323
|
return this.serial(b, async () => {
|
|
1316
|
-
const conflictBlocked = b.state.blocked
|
|
1324
|
+
const conflictBlocked = !!b.state.blocked && WORKBENCH_CONFLICT_CODES.has(b.state.blocked.code);
|
|
1317
1325
|
if (!b.state.initialized || b.state.blocked && !(allowBlockedConflict && conflictBlocked))
|
|
1318
1326
|
throw new WorkspaceError("workbench_blocked", b.state.blocked?.message ?? "Initialize workbench first");
|
|
1319
1327
|
if (operation.installationId !== this.config.installationId || operation.userId !== identity.userId || operation.sessionId !== identity.sessionId || operation.kind !== "host.shell")
|
|
@@ -6,6 +6,20 @@ import { createHash } from "node:crypto";
|
|
|
6
6
|
import { safeTreePath, STORAGE_LIMITS } from "./storage-wire.mjs";
|
|
7
7
|
import { WorkspaceError } from "./contracts.mjs";
|
|
8
8
|
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
9
|
+
const WORKBENCH_OPERATION_MARKERS = [
|
|
10
|
+
"MERGE_HEAD",
|
|
11
|
+
"CHERRY_PICK_HEAD",
|
|
12
|
+
"REVERT_HEAD",
|
|
13
|
+
"REBASE_HEAD",
|
|
14
|
+
"rebase-merge",
|
|
15
|
+
"rebase-apply",
|
|
16
|
+
"index.lock"
|
|
17
|
+
];
|
|
18
|
+
const WORKBENCH_CONFLICT_CODES = /* @__PURE__ */ new Set([
|
|
19
|
+
"conflict",
|
|
20
|
+
"workbench_conflict",
|
|
21
|
+
"workbench_operation_in_progress"
|
|
22
|
+
]);
|
|
9
23
|
async function privateRoot(root, installationId) {
|
|
10
24
|
if (!path.isAbsolute(root) || path.normalize(root) !== root || path.basename(root) !== installationId || root.split(path.sep).some((p) => [".r5d", "r5d-dev", "legacy", "app-data"].includes(p.toLowerCase())))
|
|
11
25
|
throw new WorkspaceError("unsafe_root", "Use an independently allocated private new-installation root, never a legacy worktree");
|
|
@@ -287,7 +301,7 @@ async function snapshotTree(repo, cwd, canonicalHead, maxBytes = 128 * 1024 * 10
|
|
|
287
301
|
if (!gitDirectory.startsWith(worktrees)) throw new WorkspaceError("unsafe_git", "Linked worktree belongs to another repository");
|
|
288
302
|
await noSymlinkAncestors(gitDirectory);
|
|
289
303
|
} else if (!metadataStat.isDirectory()) throw new WorkspaceError("unsafe_git", "Invalid worktree metadata");
|
|
290
|
-
for (const name of
|
|
304
|
+
for (const name of WORKBENCH_OPERATION_MARKERS) {
|
|
291
305
|
if (await fs.lstat(path.join(gitDirectory, name)).then(
|
|
292
306
|
() => true,
|
|
293
307
|
(e) => {
|
|
@@ -296,7 +310,7 @@ async function snapshotTree(repo, cwd, canonicalHead, maxBytes = 128 * 1024 * 10
|
|
|
296
310
|
}
|
|
297
311
|
))
|
|
298
312
|
throw new WorkspaceError(
|
|
299
|
-
"
|
|
313
|
+
"workbench_operation_in_progress",
|
|
300
314
|
"Git merge/rebase/index operation remains in progress; preserve and resolve it before publication"
|
|
301
315
|
);
|
|
302
316
|
}
|
|
@@ -406,6 +420,8 @@ async function snapshotTree(repo, cwd, canonicalHead, maxBytes = 128 * 1024 * 10
|
|
|
406
420
|
return (await git(repo, ["write-tree"])).toString().trim();
|
|
407
421
|
}
|
|
408
422
|
export {
|
|
423
|
+
WORKBENCH_CONFLICT_CODES,
|
|
424
|
+
WORKBENCH_OPERATION_MARKERS,
|
|
409
425
|
durableJson,
|
|
410
426
|
ensureAuthorityGitRepositoryLayout,
|
|
411
427
|
git,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Publication refusals, and how long one must persist before it becomes a
|
|
2
|
+
* durable remediation incident.
|
|
3
|
+
*
|
|
4
|
+
* Refusing to publish is cheap and immediate. Electing an incident is not: it
|
|
5
|
+
* interrupts whoever owns the checkout with an automatic remediation session.
|
|
6
|
+
* So a condition an ordinary command produces in passing has to outlive that
|
|
7
|
+
* command before it counts as evidence of a stuck checkout. */
|
|
8
|
+
/** Leaves the checkout holding unpublished conflict state. */
|
|
9
|
+
export declare const CONFLICT_PUBLICATION_CODES: ReadonlySet<string>;
|
|
10
|
+
/** Refused because the source candidate itself is unsafe or oversized. */
|
|
11
|
+
export declare const BLOCKED_PUBLICATION_CODES: ReadonlySet<string>;
|
|
12
|
+
/** Consecutive identical observations required before electing an incident.
|
|
13
|
+
* `unsafe_file`: a package manager or Git command can atomically replace a file
|
|
14
|
+
* between directory enumeration and the bounded single-link read. */
|
|
15
|
+
export declare const PUBLICATION_OBSERVATION_THRESHOLDS: ReadonlyMap<string, number>;
|
|
16
|
+
/** A merge, rebase, revert, cherry-pick or index lock belongs to whatever is
|
|
17
|
+
* running in that checkout. Its duration is the agent's, not ours: resolving a
|
|
18
|
+
* conflicted cherry-pick legitimately takes minutes, so no fixed number of
|
|
19
|
+
* samples is a safe bound. Publication defers while a run is live and elects
|
|
20
|
+
* only once the marker has outlived it. */
|
|
21
|
+
export declare const OPERATION_IN_PROGRESS_CODE = "workbench_operation_in_progress";
|
|
22
|
+
/** Observations required once no run is live — the marker has been abandoned. */
|
|
23
|
+
export declare const ABANDONED_OPERATION_OBSERVATIONS = 2;
|
|
24
|
+
/** Backstop for a run that never reaches a terminal state while holding a
|
|
25
|
+
* marker. At the one-minute publication interval this is roughly half an hour. */
|
|
26
|
+
export declare const STUCK_OPERATION_OBSERVATIONS = 30;
|
|
27
|
+
export declare function publicationRefusalOutcome(code: string): "conflict_blocked" | "large_diff_blocked" | "failed";
|
|
28
|
+
/** True once this refusal has proven durable enough to elect an incident. */
|
|
29
|
+
export declare function publicationRefusalElects(code: string, observations: number, context?: {
|
|
30
|
+
workbenchHasLiveRun?: boolean;
|
|
31
|
+
}): boolean;
|
|
@@ -186,6 +186,10 @@ export declare class WorkspaceAuthority {
|
|
|
186
186
|
* agents remain ordinary concurrent filesystem writers whose later changes
|
|
187
187
|
* are observed by a subsequent publication cycle. */
|
|
188
188
|
private assertAvailable;
|
|
189
|
+
/** Whether any run on this physical workbench is still live, across every
|
|
190
|
+
* session sharing it. An in-progress Git operation marker belongs to such a
|
|
191
|
+
* run, so publication defers to it instead of electing an incident over it. */
|
|
192
|
+
workbenchHasLiveRun(identity: WorkspaceIdentity): Promise<boolean>;
|
|
189
193
|
status(identity: WorkspaceIdentity): Promise<{
|
|
190
194
|
files: string[] | null;
|
|
191
195
|
initialized: boolean;
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export declare const sha256: (bytes: Buffer | string) => string;
|
|
2
|
+
/** A Git command is mid-flight. Owned by whoever is running it, and gone as soon
|
|
3
|
+
* as it finishes, so it is never on its own evidence of a stuck checkout. */
|
|
4
|
+
export declare const WORKBENCH_OPERATION_MARKERS: readonly ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD", "REBASE_HEAD", "rebase-merge", "rebase-apply", "index.lock"];
|
|
5
|
+
/** Publication refusals that leave the checkout holding unpublished conflict
|
|
6
|
+
* state. Remediation is admitted against exactly these. */
|
|
7
|
+
export declare const WORKBENCH_CONFLICT_CODES: ReadonlySet<string>;
|
|
2
8
|
export declare function privateRoot(root: string, installationId: string): Promise<void>;
|
|
3
9
|
export declare function noSymlinkAncestors(file: string): Promise<void>;
|
|
4
10
|
export declare function readRegular(file: string, limit?: number): Promise<Buffer>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ricsam/r5d-worker",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.162",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/mjs/main.mjs",
|
|
6
6
|
"module": "./dist/mjs/main.mjs",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"r5d-worker": "dist/mjs/main.mjs"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@ricsam/r5d-api": "^0.0.
|
|
24
|
+
"@ricsam/r5d-api": "^0.0.162",
|
|
25
25
|
"node-pty": "1.1.0",
|
|
26
26
|
"zod": "^4.1.13",
|
|
27
27
|
"picomatch": "^4.0.3"
|