@ricsam/r5d-worker 0.0.168 → 0.0.170
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 +10 -11
- package/dist/mjs/main.mjs +2 -2
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/personal/runtime.mjs +2 -0
- package/dist/mjs/runtime/executor.mjs +2 -1
- package/dist/mjs/runtime/host-environment.mjs +10 -0
- package/dist/mjs/runtime/pty.mjs +2 -1
- package/dist/mjs/runtime/releases/process.mjs +2 -1
- package/dist/mjs/runtime/workspace/authority.mjs +82 -69
- package/dist/mjs/runtime/workspace/outer.mjs +8 -2
- package/dist/types/runtime/host-environment.d.ts +12 -0
- package/dist/types/runtime/workspace/outer.d.ts +10 -1
- package/package.json +2 -2
package/dist/cjs/package.json
CHANGED
|
@@ -20296,7 +20296,7 @@ var SESSION_RUN_HELP_TEXT = [
|
|
|
20296
20296
|
' r5dctl -p <project> session start --worker <label> --worktree <branch> --model <tier> [-f|--follow|--watch] [--tools] [--parent-session <session-id>] "<prompt>"',
|
|
20297
20297
|
' r5dctl -p <project> session start --worker <label> --new-worktree <branch> --source worktree:<branch> [--working-tree carry|clean] --model <tier> [-f|--follow|--watch] [--tools] [--parent-session <session-id>] "<prompt>"',
|
|
20298
20298
|
" r5dctl session status <session-id>",
|
|
20299
|
-
' r5dctl session prompt --worker <label> --mode <
|
|
20299
|
+
' r5dctl session prompt --worker <label> --mode <plan|agent> --model <tier> [--delivery queue|steer|force] [--sender <name>] <session-id> "<prompt>"',
|
|
20300
20300
|
" r5dctl session stop <session-id>",
|
|
20301
20301
|
" r5dctl session set-worker <session-id> --worker <label>",
|
|
20302
20302
|
" r5dctl session process-log <run-id> [--tail <n>] [--grep <pattern>]",
|
|
@@ -20321,7 +20321,7 @@ var SESSION_RUN_HELP_TEXT = [
|
|
|
20321
20321
|
"The reserved topic `checkout` declares tracked-file writes in that checkout: claim it before running the command, hold it until the",
|
|
20322
20322
|
"write is done, and release it. A running session native to that branch holds `checkout` implicitly, so the call names who to message.",
|
|
20323
20323
|
"Commands started while a cross-scope `checkout` claim is held also fence the claimed checkout on the bound worker (the fence line says",
|
|
20324
|
-
"whether the worker supports it). checkout path prints
|
|
20324
|
+
"whether the worker supports it). checkout path prints the absolute checkout path of a project branch on the session's bound worker, one bare line (use --json for the checkout and worker too).",
|
|
20325
20325
|
"Agent shells act as their own session; user credentials select the acting session with -s or R5D_SESSION_ID.",
|
|
20326
20326
|
"A new worktree starts with the source checkout's uncommitted changes (--working-tree carry, the default);",
|
|
20327
20327
|
"--working-tree clean checks out the source branch's current commit only. Start and status print the mode, never a commit hash.",
|
|
@@ -20465,7 +20465,7 @@ var SHARED_HELP_ENTRIES = [
|
|
|
20465
20465
|
},
|
|
20466
20466
|
{
|
|
20467
20467
|
section: "sessions",
|
|
20468
|
-
usage: 'session prompt --worker <label> --mode <
|
|
20468
|
+
usage: 'session prompt --worker <label> --mode <plan|agent> --model <tier> [--delivery queue|steer|force] [--sender <name>] <session-id> "<prompt>"',
|
|
20469
20469
|
description: "Prompt a session: steer after the next tool result (default), queue a deferred follow-up, or force in place of the current tool round. An idle unfinished session may require explicit continuation or confirmation of interrupted tools. Blocked delivery reports the saved message/queue ID and reason; inspect the existing queue in the session UI instead of sending duplicates. The recipient sees the verified sender session id; --sender only adds a display title."
|
|
20470
20470
|
},
|
|
20471
20471
|
{
|
|
@@ -20500,12 +20500,12 @@ var SHARED_HELP_ENTRIES = [
|
|
|
20500
20500
|
},
|
|
20501
20501
|
{
|
|
20502
20502
|
section: "sessions",
|
|
20503
|
-
usage: '-s <session-id> answer-questions --worker <label> --mode <
|
|
20503
|
+
usage: '-s <session-id> answer-questions --worker <label> --mode <plan|agent> --model <tier> -a1 "<answer>" ...',
|
|
20504
20504
|
description: "Answer pending questions in order."
|
|
20505
20505
|
},
|
|
20506
20506
|
{
|
|
20507
20507
|
section: "sessions",
|
|
20508
|
-
usage: "-s <session-id> answer-env-request --worker <label> --mode <
|
|
20508
|
+
usage: "-s <session-id> answer-env-request --worker <label> --mode <plan|agent> --model <tier> [-e KEY=value] [--context <text>]",
|
|
20509
20509
|
description: "Answer a pending environment variable request with values, context, or both."
|
|
20510
20510
|
},
|
|
20511
20511
|
{
|
|
@@ -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.170";
|
|
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) {
|
|
@@ -20762,7 +20762,7 @@ function requireModel(value) {
|
|
|
20762
20762
|
return model;
|
|
20763
20763
|
}
|
|
20764
20764
|
function requireSessionMode(value) {
|
|
20765
|
-
const mode = requireValue(value?.trim(), "--mode <
|
|
20765
|
+
const mode = requireValue(value?.trim(), "--mode <plan|agent> is required");
|
|
20766
20766
|
if (!R5DCTL_SESSION_MODES.has(mode)) {
|
|
20767
20767
|
throw new Error("Invalid --mode. Expected one of: plan, agent");
|
|
20768
20768
|
}
|
|
@@ -21263,9 +21263,8 @@ ${rows.join("\n")}
|
|
|
21263
21263
|
`;
|
|
21264
21264
|
}
|
|
21265
21265
|
function renderCheckoutPath(response) {
|
|
21266
|
-
return
|
|
21267
|
-
|
|
21268
|
-
);
|
|
21266
|
+
return `${response.path}
|
|
21267
|
+
`;
|
|
21269
21268
|
}
|
|
21270
21269
|
function assertAuthLoginArgs(args) {
|
|
21271
21270
|
const valueFlags = /* @__PURE__ */ new Set(["--device-name", "--worker-label"]);
|
|
@@ -21504,7 +21503,7 @@ function parseAnswerFlags(args) {
|
|
|
21504
21503
|
return ordered.map(([, value]) => value);
|
|
21505
21504
|
}
|
|
21506
21505
|
function requireChatMode(value) {
|
|
21507
|
-
const mode = requireValue(value?.trim(), "--mode <
|
|
21506
|
+
const mode = requireValue(value?.trim(), "--mode <plan|agent> is required");
|
|
21508
21507
|
if (!CHAT_MODES.has(mode)) throw new Error(`Invalid --mode: ${mode}`);
|
|
21509
21508
|
return mode;
|
|
21510
21509
|
}
|
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.170" : "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.170" : "development"
|
|
19
19
|
);
|
|
20
20
|
console.log(`Worker connected: ${runtime.resourceId}`);
|
|
21
21
|
let closing = false;
|
package/dist/mjs/package.json
CHANGED
|
@@ -407,6 +407,8 @@ exec ${quote(executable)} ${quote(cli)} "$@"
|
|
|
407
407
|
if (command.method === "resize") return authority.resize(identity, ref, command.id, command.cols, command.rows);
|
|
408
408
|
if (command.method === "cancel" || command.method === "processCancel") return authority.cancel(identity, ref, command.id);
|
|
409
409
|
}
|
|
410
|
+
if (pausedForIncident && request.remediation !== true && ["hydrate", "publish", "reconcilePublication"].includes(String(command.method)))
|
|
411
|
+
throw new WorkspaceError("remediation_paused", "This worker's workspace is under remediation; synchronization resumes when it is confirmed", true);
|
|
410
412
|
if (request.kind === "product") {
|
|
411
413
|
if (command.method === "fileWrite") {
|
|
412
414
|
await authority.ensureHydrated(identity);
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from "./protocol.mjs";
|
|
21
21
|
import { ExecutorStore, privateDirectory } from "./storage.mjs";
|
|
22
22
|
import { AdapterGrantRegistry, digest } from "./releases/grants.mjs";
|
|
23
|
+
import { hostEnvironment } from "./host-environment.mjs";
|
|
23
24
|
const tokenHash = (token) => createHash("sha256").update(token).digest("hex");
|
|
24
25
|
class HostExecutor {
|
|
25
26
|
constructor(config, nativePty) {
|
|
@@ -219,7 +220,7 @@ class HostExecutor {
|
|
|
219
220
|
const secrets = join(this.config.stateDir, "secrets", tokenHash(operation.operationId));
|
|
220
221
|
try {
|
|
221
222
|
privateDirectory(secrets);
|
|
222
|
-
const env = {
|
|
223
|
+
const env = { ...hostEnvironment(), ...payload.env };
|
|
223
224
|
for (const credential of credentials) {
|
|
224
225
|
Object.assign(env, credential.env);
|
|
225
226
|
for (const [name, content] of Object.entries(credential.files)) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
function hostEnvironment() {
|
|
2
|
+
const env = {};
|
|
3
|
+
for (const [name, value] of Object.entries(process.env)) if (typeof value === "string") env[name] = value;
|
|
4
|
+
if (!env.PATH) env.PATH = "/usr/local/bin:/usr/bin:/bin";
|
|
5
|
+
if (!env.LANG && !env.LC_ALL) env.LANG = "C.UTF-8";
|
|
6
|
+
return env;
|
|
7
|
+
}
|
|
8
|
+
export {
|
|
9
|
+
hostEnvironment
|
|
10
|
+
};
|
package/dist/mjs/runtime/pty.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { ExecutorError } from "./protocol.mjs";
|
|
4
|
+
import { hostEnvironment } from "./host-environment.mjs";
|
|
4
5
|
const nodeExecutable = typeof R5D_PACKAGED_NODE !== "undefined" && R5D_PACKAGED_NODE ? fileURLToPath(new URL("./bin/node", import.meta.url)) : Bun.which("node") ?? "node";
|
|
5
6
|
function preflightNativePty() {
|
|
6
7
|
if (process.platform !== "darwin") return;
|
|
@@ -19,7 +20,7 @@ class PtySession {
|
|
|
19
20
|
ready;
|
|
20
21
|
constructor(input) {
|
|
21
22
|
this.child = spawn(nodeExecutable, [fileURLToPath(new URL("./pty-host.mjs", import.meta.url))], {
|
|
22
|
-
env:
|
|
23
|
+
env: hostEnvironment(),
|
|
23
24
|
stdio: "pipe"
|
|
24
25
|
});
|
|
25
26
|
this.ready = new Promise((resolve, reject) => {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { ExecutorError, isStartNonAdmissionCode, MAX_FRAME_BYTES } from "../protocol.mjs";
|
|
4
|
+
import { hostEnvironment } from "../host-environment.mjs";
|
|
4
5
|
class AdapterChild {
|
|
5
6
|
constructor(runtimePath, entrypoint, configPath, home, timeoutMs = 1e4) {
|
|
6
7
|
this.timeoutMs = timeoutMs;
|
|
7
8
|
this.process = spawn(runtimePath, [entrypoint, configPath], {
|
|
8
9
|
cwd: home,
|
|
9
|
-
env:
|
|
10
|
+
env: hostEnvironment(),
|
|
10
11
|
stdio: "pipe"
|
|
11
12
|
});
|
|
12
13
|
let buffer = Buffer.alloc(0);
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
WorkspaceConfig,
|
|
12
12
|
WorkspaceError
|
|
13
13
|
} from "./contracts.mjs";
|
|
14
|
-
import { OuterRepository, OuterSnapshotRefusal, OUTER_BRANCH, OUTER_CONFLICT_CODE } from "./outer.mjs";
|
|
14
|
+
import { OuterRepository, OuterSnapshotRefusal, OUTER_BRANCH, OUTER_CONFLICT_CODE, OUTER_PUBLISH_ATTEMPTS } from "./outer.mjs";
|
|
15
15
|
import { SessionArtifactStore, SessionArtifactChunk, RESERVED_ARTIFACT_ENV } from "./artifacts.mjs";
|
|
16
16
|
import { WorkspaceFileWrite, WorkspaceFileWriteLookup } from "./file-write.mjs";
|
|
17
17
|
import { walkDirectoryTree } from "./walk.mjs";
|
|
@@ -1379,8 +1379,8 @@ class WorkspaceAuthority {
|
|
|
1379
1379
|
const storage = await this.options.accountStorage(userId);
|
|
1380
1380
|
const repositoryId = this.accountRepositoryId(userId);
|
|
1381
1381
|
if (state.blocked?.code === "publication_unknown") {
|
|
1382
|
-
const { operationId
|
|
1383
|
-
const receipt = await storage.read({ method: "operation.get", lookupId:
|
|
1382
|
+
const { operationId, commit } = state.blocked;
|
|
1383
|
+
const receipt = await storage.read({ method: "operation.get", lookupId: operationId }).catch((error) => {
|
|
1384
1384
|
if (error instanceof WorkspaceError && error.code === "not_found") return null;
|
|
1385
1385
|
throw error;
|
|
1386
1386
|
});
|
|
@@ -1395,20 +1395,20 @@ class WorkspaceAuthority {
|
|
|
1395
1395
|
await this.saveOuter(state);
|
|
1396
1396
|
}
|
|
1397
1397
|
if (!state.initialized) {
|
|
1398
|
-
const
|
|
1399
|
-
if (
|
|
1400
|
-
if (!await repo.has(
|
|
1401
|
-
if (!await repo.isLegacyBootstrap(
|
|
1402
|
-
await repo.setHead(
|
|
1398
|
+
const remote = await this.remoteHead(storage, repositoryId, OUTER_BRANCH, true);
|
|
1399
|
+
if (remote) {
|
|
1400
|
+
if (!await repo.has(remote)) await repo.unbundle(await this.downloadBundle(storage, repositoryId));
|
|
1401
|
+
if (!await repo.isLegacyBootstrap(remote)) await repo.checkout(remote);
|
|
1402
|
+
await repo.setHead(remote);
|
|
1403
1403
|
}
|
|
1404
1404
|
state.initialized = true;
|
|
1405
|
-
state.publishedHead =
|
|
1406
|
-
state.head =
|
|
1405
|
+
state.publishedHead = remote;
|
|
1406
|
+
state.head = remote;
|
|
1407
1407
|
await this.saveOuter(state);
|
|
1408
|
-
if (options.bootstrapOnly) return { head:
|
|
1408
|
+
if (options.bootstrapOnly) return { head: remote, unchanged: true };
|
|
1409
1409
|
} else if (options.bootstrapOnly) return { head: state.head, unchanged: true };
|
|
1410
1410
|
if (state.conflict && !options.resolveConflict)
|
|
1411
|
-
throw new OuterSnapshotRefusal(OUTER_CONFLICT_CODE, "Workspace synchronization conflict awaits resolution", state.conflict.paths);
|
|
1411
|
+
throw new OuterSnapshotRefusal(OUTER_CONFLICT_CODE, "Workspace synchronization conflict awaits resolution", state.conflict.paths, true);
|
|
1412
1412
|
const inners = await this.innerCheckouts(userId);
|
|
1413
1413
|
let snapshot;
|
|
1414
1414
|
try {
|
|
@@ -1423,26 +1423,34 @@ class WorkspaceAuthority {
|
|
|
1423
1423
|
if (state.conflict) {
|
|
1424
1424
|
const marker = /^(<{7}|={7}|>{7})( |$)/m;
|
|
1425
1425
|
for (const file of state.conflict.paths) {
|
|
1426
|
-
const
|
|
1426
|
+
const bytes = await readRegular(path.join(repo.workTree, file)).catch((error) => {
|
|
1427
1427
|
if (error.code === "ENOENT" || error.code === "ENOTDIR") return null;
|
|
1428
1428
|
throw error;
|
|
1429
1429
|
});
|
|
1430
|
-
if (
|
|
1431
|
-
throw new OuterSnapshotRefusal(OUTER_CONFLICT_CODE, "Conflict markers remain; resolve them before confirming", [file]);
|
|
1430
|
+
if (bytes && marker.test(bytes.toString("utf8")))
|
|
1431
|
+
throw new OuterSnapshotRefusal(OUTER_CONFLICT_CODE, "Conflict markers remain; resolve them before confirming", [file], true);
|
|
1432
1432
|
}
|
|
1433
1433
|
} else if (state.blocked && state.blocked.code !== "publication_unknown") {
|
|
1434
1434
|
state.blocked = null;
|
|
1435
1435
|
await this.saveOuter(state);
|
|
1436
1436
|
}
|
|
1437
1437
|
const base = state.publishedHead;
|
|
1438
|
-
const
|
|
1438
|
+
const carried = state.head && state.head !== base ? (await repo.parents(state.head)).slice(1).filter((parent) => parent !== base) : [];
|
|
1439
1439
|
let local;
|
|
1440
|
+
let integratedRemote = null;
|
|
1440
1441
|
if (state.conflict) {
|
|
1441
1442
|
local = await repo.commit(snapshot.tree, [state.conflict.ours, state.conflict.theirs], "Resolve workspace synchronization conflict");
|
|
1443
|
+
integratedRemote = state.conflict.theirs;
|
|
1442
1444
|
state.conflict = null;
|
|
1443
1445
|
state.blocked = null;
|
|
1444
|
-
} else if (base && snapshot.tree ===
|
|
1445
|
-
|
|
1446
|
+
} else if (state.head && state.head !== base && snapshot.tree === await repo.treeOf(state.head)) {
|
|
1447
|
+
local = state.head;
|
|
1448
|
+
integratedRemote = carried.at(-1) ?? null;
|
|
1449
|
+
} else if (base && !carried.length && snapshot.tree === await repo.treeOf(base)) local = base;
|
|
1450
|
+
else {
|
|
1451
|
+
local = await repo.commit(snapshot.tree, [...base ? [base] : [], ...carried], carried.length ? "Merge workspace" : "Workspace snapshot");
|
|
1452
|
+
integratedRemote = carried.at(-1) ?? null;
|
|
1453
|
+
}
|
|
1446
1454
|
if (state.head !== local) {
|
|
1447
1455
|
state.head = local;
|
|
1448
1456
|
await repo.setHead(local);
|
|
@@ -1453,66 +1461,71 @@ class WorkspaceAuthority {
|
|
|
1453
1461
|
throw new WorkspaceError("conflict", "The required workspace head is not integrated; retry the transfer");
|
|
1454
1462
|
return options.requireHead ? { integrated: options.requireHead } : {};
|
|
1455
1463
|
};
|
|
1456
|
-
const remote = await this.remoteHead(storage, repositoryId, OUTER_BRANCH, true);
|
|
1457
1464
|
let currentTree = snapshot.tree;
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
if (local
|
|
1461
|
-
await repo.
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
+
for (let attempt = 1; ; attempt++) {
|
|
1466
|
+
const remote = await this.remoteHead(storage, repositoryId, OUTER_BRANCH, true);
|
|
1467
|
+
if (remote && remote !== local && remote !== integratedRemote) {
|
|
1468
|
+
if (!await repo.has(remote)) await repo.unbundle(await this.downloadBundle(storage, repositoryId));
|
|
1469
|
+
if (local === base) {
|
|
1470
|
+
await repo.apply(currentTree, await repo.treeOf(remote));
|
|
1471
|
+
state.publishedHead = remote;
|
|
1472
|
+
state.head = remote;
|
|
1473
|
+
await repo.setHead(remote);
|
|
1474
|
+
await this.saveOuter(state);
|
|
1475
|
+
return { head: remote, unchanged: false, ...await integrated(remote) };
|
|
1476
|
+
}
|
|
1477
|
+
const merge = await repo.merge(local, remote);
|
|
1478
|
+
if (merge.conflicts.length) {
|
|
1479
|
+
await repo.apply(currentTree, merge.tree);
|
|
1480
|
+
state.conflict = { ours: local, theirs: remote, tree: merge.tree, paths: merge.conflicts };
|
|
1481
|
+
state.blocked = { code: OUTER_CONFLICT_CODE, message: "The workspace diverged from another worker; conflict markers were written to the paths named", paths: merge.conflicts };
|
|
1482
|
+
await this.saveOuter(state);
|
|
1483
|
+
throw new OuterSnapshotRefusal(OUTER_CONFLICT_CODE, state.blocked.message, merge.conflicts, true);
|
|
1484
|
+
}
|
|
1485
|
+
await repo.apply(currentTree, merge.tree);
|
|
1486
|
+
currentTree = merge.tree;
|
|
1487
|
+
local = await repo.commit(merge.tree, [local, remote], "Merge workspace");
|
|
1488
|
+
integratedRemote = remote;
|
|
1489
|
+
state.head = local;
|
|
1490
|
+
await repo.setHead(local);
|
|
1465
1491
|
await this.saveOuter(state);
|
|
1466
|
-
return { head: remote, unchanged: false, ...await integrated(remote) };
|
|
1467
1492
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
await repo.apply(currentTree, merge.tree);
|
|
1471
|
-
state.conflict = { ours: local, theirs: remote, tree: merge.tree, paths: merge.conflicts };
|
|
1472
|
-
state.blocked = { code: OUTER_CONFLICT_CODE, message: "The workspace diverged from another worker; conflict markers were written to the paths named", paths: merge.conflicts };
|
|
1493
|
+
if (local === remote) {
|
|
1494
|
+
state.publishedHead = local;
|
|
1473
1495
|
await this.saveOuter(state);
|
|
1474
|
-
|
|
1496
|
+
return { head: local, unchanged: true, ...await integrated(local) };
|
|
1475
1497
|
}
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
state.head = local;
|
|
1480
|
-
await repo.setHead(local);
|
|
1498
|
+
const operationId = `ws-${randomUUID()}`, blobId = `ws-${randomUUID()}`;
|
|
1499
|
+
const bytes = await repo.bundle(remote && await repo.has(remote) ? remote : null);
|
|
1500
|
+
state.blocked = { code: "publication_unknown", operationId, commit: local, message: "Workspace publication is in progress/unknown. Inspect the original storage operation; never retry under a new ID." };
|
|
1481
1501
|
await this.saveOuter(state);
|
|
1482
|
-
|
|
1483
|
-
|
|
1502
|
+
try {
|
|
1503
|
+
await this.uploadBundle(storage, bytes, blobId, operationId);
|
|
1504
|
+
const result = await storage.mutate({
|
|
1505
|
+
method: "repository.publish",
|
|
1506
|
+
operationId,
|
|
1507
|
+
repositoryId,
|
|
1508
|
+
branch: OUTER_BRANCH,
|
|
1509
|
+
expectedHead: remote,
|
|
1510
|
+
commit: local,
|
|
1511
|
+
bundleId: blobId
|
|
1512
|
+
});
|
|
1513
|
+
if (result.head !== local) throw new WorkspaceError("invalid_response", "Workspace publication receipt head mismatch; inspect original operation");
|
|
1514
|
+
} catch (error) {
|
|
1515
|
+
if (error instanceof WorkspaceError && error.rejectedBeforeAdmission) {
|
|
1516
|
+
state.blocked = null;
|
|
1517
|
+
await this.saveOuter(state);
|
|
1518
|
+
if (error.code !== "conflict") throw error;
|
|
1519
|
+
if (attempt < OUTER_PUBLISH_ATTEMPTS) continue;
|
|
1520
|
+
throw new WorkspaceError("mirror_advanced", "The workspace mirror advanced during publication; retried next cycle", true);
|
|
1521
|
+
}
|
|
1522
|
+
throw error;
|
|
1523
|
+
}
|
|
1484
1524
|
state.publishedHead = local;
|
|
1525
|
+
state.blocked = null;
|
|
1485
1526
|
await this.saveOuter(state);
|
|
1486
|
-
return { head: local, unchanged:
|
|
1487
|
-
}
|
|
1488
|
-
const operationId = `ws-${randomUUID()}`, blobId = `ws-${randomUUID()}`;
|
|
1489
|
-
const bytes = await repo.bundle(remote && await repo.has(remote) ? remote : null);
|
|
1490
|
-
state.blocked = { code: "publication_unknown", operationId, commit: local, message: "Workspace publication is in progress/unknown. Inspect the original storage operation; never retry under a new ID." };
|
|
1491
|
-
await this.saveOuter(state);
|
|
1492
|
-
try {
|
|
1493
|
-
await this.uploadBundle(storage, bytes, blobId, operationId);
|
|
1494
|
-
const result = await storage.mutate({
|
|
1495
|
-
method: "repository.publish",
|
|
1496
|
-
operationId,
|
|
1497
|
-
repositoryId,
|
|
1498
|
-
branch: OUTER_BRANCH,
|
|
1499
|
-
expectedHead: remote,
|
|
1500
|
-
commit: local,
|
|
1501
|
-
bundleId: blobId
|
|
1502
|
-
});
|
|
1503
|
-
if (result.head !== local) throw new WorkspaceError("invalid_response", "Workspace publication receipt head mismatch; inspect original operation");
|
|
1504
|
-
} catch (error) {
|
|
1505
|
-
if (error instanceof WorkspaceError && error.rejectedBeforeAdmission) {
|
|
1506
|
-
state.blocked = null;
|
|
1507
|
-
await this.saveOuter(state);
|
|
1508
|
-
throw error.code === "conflict" ? new WorkspaceError("mirror_advanced", "The workspace mirror advanced during publication; retried next cycle", true) : error;
|
|
1509
|
-
}
|
|
1510
|
-
throw error;
|
|
1527
|
+
return { head: local, unchanged: false, ...await integrated(local) };
|
|
1511
1528
|
}
|
|
1512
|
-
state.publishedHead = local;
|
|
1513
|
-
state.blocked = null;
|
|
1514
|
-
await this.saveOuter(state);
|
|
1515
|
-
return { head: local, unchanged: false, ...await integrated(local) };
|
|
1516
1529
|
}
|
|
1517
1530
|
/** Push a project checkout's own HEAD to its mirror. Last push wins: no merge
|
|
1518
1531
|
* is attempted, and a rewind or rewrite replaces the mirror as readily as a
|
|
@@ -9,6 +9,7 @@ const OUTER_REF = `refs/heads/${OUTER_BRANCH}`;
|
|
|
9
9
|
const OUTER_SNAPSHOT_LIMIT_BYTES = 5 * 1024 * 1024;
|
|
10
10
|
const OUTER_GIT_TIMEOUT_MS = 10 * 6e4;
|
|
11
11
|
const OUTER_CONFLICT_CODE = "sync_conflict";
|
|
12
|
+
const OUTER_PUBLISH_ATTEMPTS = 3;
|
|
12
13
|
const LEGACY_ACCOUNT_README = "# Account workspace\n";
|
|
13
14
|
const OUTER_EXCLUDES = [
|
|
14
15
|
"/artifacts/",
|
|
@@ -33,8 +34,8 @@ const OUTER_EXCLUDES = [
|
|
|
33
34
|
".pypirc"
|
|
34
35
|
];
|
|
35
36
|
class OuterSnapshotRefusal extends WorkspaceError {
|
|
36
|
-
constructor(code, message, paths) {
|
|
37
|
-
super(code, message);
|
|
37
|
+
constructor(code, message, paths, definite = false) {
|
|
38
|
+
super(code, message, definite);
|
|
38
39
|
this.paths = paths;
|
|
39
40
|
this.name = "OuterSnapshotRefusal";
|
|
40
41
|
}
|
|
@@ -100,6 +101,10 @@ class OuterRepository {
|
|
|
100
101
|
async emptyTree() {
|
|
101
102
|
return GitOid.parse((await this.run(["hash-object", "-t", "tree", "--stdin"], "")).toString("utf8").trim());
|
|
102
103
|
}
|
|
104
|
+
async parents(commit) {
|
|
105
|
+
const line = (await this.run(["rev-list", "--parents", "-n", "1", GitOid.parse(commit)])).toString("utf8").trim();
|
|
106
|
+
return line.split(/\s+/).slice(1).map((oid) => GitOid.parse(oid));
|
|
107
|
+
}
|
|
103
108
|
async isAncestor(ancestor, descendant) {
|
|
104
109
|
const { code } = await this.result(["merge-base", "--is-ancestor", GitOid.parse(ancestor), GitOid.parse(descendant)]);
|
|
105
110
|
if (code !== 0 && code !== 1) throw new WorkspaceError("git_failed", "Outer repository ancestry is unreadable");
|
|
@@ -308,6 +313,7 @@ export {
|
|
|
308
313
|
OUTER_CONFLICT_CODE,
|
|
309
314
|
OUTER_EXCLUDES,
|
|
310
315
|
OUTER_GIT_TIMEOUT_MS,
|
|
316
|
+
OUTER_PUBLISH_ATTEMPTS,
|
|
311
317
|
OUTER_REF,
|
|
312
318
|
OUTER_SNAPSHOT_LIMIT_BYTES,
|
|
313
319
|
OuterRepository,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** The environment every process the worker spawns for the user runs with.
|
|
2
|
+
*
|
|
3
|
+
* It is the host account's own environment, as the worker itself was started
|
|
4
|
+
* with — HOME, PATH, agent sockets, configs — with only a UTF-8 locale added
|
|
5
|
+
* when the host set none. The worker is a machine boundary, not a sandbox:
|
|
6
|
+
* shells and agents behave exactly like the host user, and isolation is the
|
|
7
|
+
* operator's decision (a dedicated account or machine), never an environment
|
|
8
|
+
* variable. Re-pointing HOME at the checkout was tried on 2026-09-16/17 and
|
|
9
|
+
* abandoned: tools that ignore HOME confused more than it isolated, and the
|
|
10
|
+
* caches that honoured it landed inside checkouts, where the publication guard
|
|
11
|
+
* measured them and two workers conflicted on them. */
|
|
12
|
+
export declare function hostEnvironment(): Record<string, string>;
|
|
@@ -11,6 +11,9 @@ export declare const OUTER_SNAPSHOT_LIMIT_BYTES: number;
|
|
|
11
11
|
* is sized for one project. */
|
|
12
12
|
export declare const OUTER_GIT_TIMEOUT_MS: number;
|
|
13
13
|
export declare const OUTER_CONFLICT_CODE = "sync_conflict";
|
|
14
|
+
/** Publications one cycle attempts while the mirror keeps moving underneath
|
|
15
|
+
* them; each attempt integrates what the mirror gained before trying again. */
|
|
16
|
+
export declare const OUTER_PUBLISH_ATTEMPTS = 3;
|
|
14
17
|
/** The bootstrap tree a legacy account repository carries. It is never
|
|
15
18
|
* materialized: the workspace root is the user's, not a README's. */
|
|
16
19
|
export declare const LEGACY_ACCOUNT_README = "# Account workspace\n";
|
|
@@ -42,9 +45,14 @@ export type OuterTreeEntry = {
|
|
|
42
45
|
oid: string;
|
|
43
46
|
};
|
|
44
47
|
/** A refusal that names the paths responsible, for the incident evidence. */
|
|
48
|
+
/** A refusal whose outcome is definite: either nothing happened, or what
|
|
49
|
+
* happened (conflict markers written, state saved) is fully recorded in the
|
|
50
|
+
* durable outer state and re-running the same command refuses the same way.
|
|
51
|
+
* `definite` travels as the relay's `rejectedBeforeAdmission`, so a caller
|
|
52
|
+
* receives a typed error instead of an uncertain outcome that blocks its run. */
|
|
45
53
|
export declare class OuterSnapshotRefusal extends WorkspaceError {
|
|
46
54
|
readonly paths: string[];
|
|
47
|
-
constructor(code: string, message: string, paths: string[]);
|
|
55
|
+
constructor(code: string, message: string, paths: string[], definite?: boolean);
|
|
48
56
|
}
|
|
49
57
|
export declare class OuterRepository {
|
|
50
58
|
readonly gitDir: string;
|
|
@@ -58,6 +66,7 @@ export declare class OuterRepository {
|
|
|
58
66
|
has(object: string): Promise<boolean>;
|
|
59
67
|
treeOf(commit: string): Promise<string>;
|
|
60
68
|
emptyTree(): Promise<string>;
|
|
69
|
+
parents(commit: string): Promise<string[]>;
|
|
61
70
|
isAncestor(ancestor: string, descendant: string): Promise<boolean>;
|
|
62
71
|
listTree(tree: string): Promise<OuterTreeEntry[]>;
|
|
63
72
|
/** Git refuses to walk into a nested repository until the outer index holds
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ricsam/r5d-worker",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.170",
|
|
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.170",
|
|
25
25
|
"node-pty": "1.1.0",
|
|
26
26
|
"zod": "^4.1.13",
|
|
27
27
|
"picomatch": "^4.0.3"
|