@task-handoff/node-agent 0.0.8 → 0.0.9-alpha.1

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.
@@ -1,2 +1,3 @@
1
1
  #!/usr/bin/env node
2
+ process.env.TASK_HANDOFF_BUNDLED_RUNTIME_DIR ||= require("node:path").join(__dirname, "..", "runtime-artifacts");
2
3
  require("../dist/cli.js");
@@ -27,11 +27,30 @@ const targetVersion = options.targetVersion;
27
27
  const service = options.service;
28
28
  const npmCommand = options.npmCommand;
29
29
  const nodeAgentManifest = path.resolve(__dirname, "..", "package.json");
30
+ const terminalStatuses = new Set(["succeeded", "degraded", "failed"]);
30
31
 
31
- function updateJob(patch) {
32
- const current = JSON.parse(fs.readFileSync(jobFile, "utf8"));
33
- const next = { ...current, ...patch, updatedAt: new Date().toISOString() };
34
- writeFileAtomic.sync(jobFile, `${JSON.stringify(next, null, 2)}\n`, { encoding: "utf8" });
32
+ function updateJob(expectedStatuses, createPatch) {
33
+ const observed = JSON.parse(fs.readFileSync(jobFile, "utf8"));
34
+ // Test-only synchronization point used to prove that the value is checked
35
+ // again after another process changes it between observation and commit.
36
+ if (process.env.TASK_HANDOFF_UPDATE_WORKER_TEST_CAS_HOOK) {
37
+ require(path.resolve(process.env.TASK_HANDOFF_UPDATE_WORKER_TEST_CAS_HOOK))({ jobFile, observed });
38
+ }
39
+
40
+ const lockPath = `${jobFile}.worker-lock`;
41
+ fs.mkdirSync(lockPath);
42
+ try {
43
+ const current = JSON.parse(fs.readFileSync(jobFile, "utf8"));
44
+ if (terminalStatuses.has(current.status) || !expectedStatuses.includes(current.status)) {
45
+ return false;
46
+ }
47
+ const patch = typeof createPatch === "function" ? createPatch(current) : createPatch;
48
+ const next = { ...current, ...patch, updatedAt: new Date().toISOString() };
49
+ writeFileAtomic.sync(jobFile, `${JSON.stringify(next, null, 2)}\n`, { encoding: "utf8" });
50
+ return true;
51
+ } finally {
52
+ fs.rmdirSync(lockPath);
53
+ }
35
54
  }
36
55
 
37
56
  function run(command, args) {
@@ -46,31 +65,63 @@ function verifyInstalledVersion() {
46
65
  }
47
66
  }
48
67
 
68
+ function verifyNpmArtifactIntegrity() {
69
+ const job = JSON.parse(fs.readFileSync(jobFile, "utf8"));
70
+ const prefix = `npm:@task-handoff/node-agent@${targetVersion}#`;
71
+ if (typeof job.artifactRef !== "string" || !job.artifactRef.startsWith(prefix) || job.artifactRef.length === prefix.length) {
72
+ throw new Error(`Update job does not pin npm integrity for @task-handoff/node-agent@${targetVersion}.`);
73
+ }
74
+ const expectedIntegrity = job.artifactRef.slice(prefix.length);
75
+ const result = spawnSync(npmCommand, ["view", `@task-handoff/node-agent@${targetVersion}`, "dist.integrity", "--json"], { encoding: "utf8" });
76
+ if (result.status !== 0) throw new Error("Could not verify the node-agent npm artifact integrity.");
77
+ let actualIntegrity;
78
+ try {
79
+ actualIntegrity = JSON.parse(result.stdout);
80
+ } catch {
81
+ throw new Error("npm returned invalid node-agent artifact integrity metadata.");
82
+ }
83
+ if (actualIntegrity !== expectedIntegrity) {
84
+ throw new Error(`Node-agent npm artifact integrity mismatch: expected ${expectedIntegrity}, found ${String(actualIntegrity || "unknown")}.`);
85
+ }
86
+ }
87
+
88
+ let restartAttempted = false;
89
+
49
90
  try {
50
- updateJob({ status: "updating", startedAt: new Date().toISOString(), error: undefined });
91
+ const claimed = updateJob(["queued"], (current) => ({
92
+ status: "updating-node",
93
+ rollout: { ...current.rollout, phase: "updating-node" },
94
+ startedAt: new Date().toISOString(),
95
+ error: undefined,
96
+ }));
97
+ if (!claimed) process.exit(0);
98
+ verifyNpmArtifactIntegrity();
51
99
  const prefixResult = spawnSync(npmCommand, ["prefix", "--global"], { encoding: "utf8" });
52
100
  if (prefixResult.status !== 0) throw new Error("Could not determine the npm global prefix.");
53
101
  const prefix = prefixResult.stdout.trim();
54
- const taskHandoff = path.join(prefix, "bin", "task-handoff");
55
- const serverPackage = path.join(prefix, "lib", "node_modules", "@task-handoff", "server", "package.json");
56
- if (fs.existsSync(taskHandoff) && fs.existsSync(serverPackage)) {
57
- run(taskHandoff, ["update", "--to", targetVersion]);
58
- } else {
59
- run(npmCommand, [
60
- "install",
61
- "--global",
62
- "--prefix",
63
- prefix,
64
- `@task-handoff/node-agent@${targetVersion}`,
65
- `@task-handoff/controlled-instance@${targetVersion}`,
66
- ]);
67
- updateJob({ status: "restarting" });
68
- run("systemctl", ["restart", service]);
69
- }
102
+ run(npmCommand, [
103
+ "install",
104
+ "--global",
105
+ "--prefix",
106
+ prefix,
107
+ `@task-handoff/node-agent@${targetVersion}`,
108
+ ]);
109
+ const handedOff = updateJob(["updating-node"], (current) => ({ status: "restarting-node", rollout: { ...current.rollout, phase: "restarting-node" } }));
110
+ if (!handedOff) process.exit(0);
111
+ // Once restart is attempted, the new node-agent exclusively owns all later
112
+ // rollout transitions. The old worker must never write this job again.
113
+ restartAttempted = true;
114
+ run("systemctl", ["restart", service]);
70
115
  verifyInstalledVersion();
71
- updateJob({ status: "succeeded", completedAt: new Date().toISOString() });
72
116
  } catch (error) {
73
- updateJob({ status: "failed", error: error instanceof Error ? error.message : String(error), completedAt: new Date().toISOString() });
117
+ if (!restartAttempted) {
118
+ updateJob(["queued", "updating-node"], (current) => ({
119
+ status: "failed",
120
+ rollout: { ...current.rollout, phase: "failed" },
121
+ error: { code: "NODE_UPDATE_FAILED", message: error instanceof Error ? error.message : String(error), retryable: false },
122
+ completedAt: new Date().toISOString(),
123
+ }));
124
+ }
74
125
  console.error(error);
75
126
  process.exit(1);
76
127
  }