open-agents-ai 0.187.515 → 0.187.516

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/index.js CHANGED
@@ -595998,6 +595998,159 @@ function checkConcurrentJobLimit(auth) {
595998
595998
  }
595999
595999
  return null;
596000
596000
  }
596001
+ function adoptHandoffRuns() {
596002
+ if (process.env["OA_DISABLE_RUN_HANDOFF"] === "1") return;
596003
+ const fs7 = require3("node:fs");
596004
+ const path8 = require3("node:path");
596005
+ const home = require3("node:os").homedir();
596006
+ const handoffPath2 = path8.join(home, ".open-agents", "runs-handoff.json");
596007
+ if (!fs7.existsSync(handoffPath2)) return;
596008
+ let handoff = {};
596009
+ try {
596010
+ const raw = fs7.readFileSync(handoffPath2, "utf-8");
596011
+ handoff = JSON.parse(raw);
596012
+ } catch (e2) {
596013
+ process.stderr.write(` WARN: handoff file unreadable (${e2.message}) — discarding.
596014
+ `);
596015
+ try {
596016
+ fs7.unlinkSync(handoffPath2);
596017
+ } catch {
596018
+ }
596019
+ return;
596020
+ }
596021
+ try {
596022
+ fs7.unlinkSync(handoffPath2);
596023
+ } catch {
596024
+ }
596025
+ const runs = handoff.runs ?? [];
596026
+ if (runs.length === 0) return;
596027
+ const adopted = [];
596028
+ const orphaned = [];
596029
+ for (const r2 of runs) {
596030
+ if (!r2.pid || r2.pid <= 0) continue;
596031
+ let alive = false;
596032
+ try {
596033
+ process.kill(r2.pid, 0);
596034
+ alive = true;
596035
+ } catch {
596036
+ alive = false;
596037
+ }
596038
+ const jobsDir3 = path8.join(home, ".open-agents", "jobs");
596039
+ const jobFile = path8.join(jobsDir3, `${r2.jobId}.json`);
596040
+ let job = null;
596041
+ try {
596042
+ if (fs7.existsSync(jobFile)) job = JSON.parse(fs7.readFileSync(jobFile, "utf-8"));
596043
+ } catch {
596044
+ }
596045
+ if (!alive) {
596046
+ orphaned.push(r2.jobId);
596047
+ if (job) {
596048
+ let finalJson = "";
596049
+ try {
596050
+ if (r2.outputFile && fs7.existsSync(r2.outputFile)) {
596051
+ const raw = fs7.readFileSync(r2.outputFile, "utf-8");
596052
+ const lines = raw.trim().split("\n");
596053
+ for (let i2 = lines.length - 1; i2 >= 0; i2--) {
596054
+ if (lines[i2].trimEnd() === "{" || lines[i2].startsWith("{") && !lines[i2].startsWith('{"type":"tool_call"')) {
596055
+ finalJson = lines.slice(i2).join("\n");
596056
+ break;
596057
+ }
596058
+ }
596059
+ }
596060
+ } catch {
596061
+ }
596062
+ try {
596063
+ const parsed = finalJson ? JSON.parse(finalJson) : null;
596064
+ if (parsed && parsed.status) job.status = parsed.status;
596065
+ else job.status = "failed";
596066
+ job.completedAt = (/* @__PURE__ */ new Date()).toISOString();
596067
+ if (parsed?.summary) job.summary = parsed.summary;
596068
+ if (parsed?.error) job.error = parsed.error;
596069
+ job.adoptedAs = "orphaned";
596070
+ } catch {
596071
+ job.status = "failed";
596072
+ job.completedAt = (/* @__PURE__ */ new Date()).toISOString();
596073
+ job.adoptedAs = "orphaned";
596074
+ }
596075
+ try {
596076
+ fs7.writeFileSync(jobFile, JSON.stringify(job, null, 2), "utf-8");
596077
+ } catch {
596078
+ }
596079
+ }
596080
+ continue;
596081
+ }
596082
+ adopted.push(r2.jobId);
596083
+ const adoptedShim = {
596084
+ pid: r2.pid,
596085
+ // Stub stdio (we never read from it again — the child writes directly
596086
+ // to the on-disk output file which /v1/runs/{id}/output already reads).
596087
+ stdout: { on: () => {
596088
+ } },
596089
+ stderr: { on: () => {
596090
+ } },
596091
+ __adopted: true
596092
+ };
596093
+ runningProcesses.set(r2.jobId, adoptedShim);
596094
+ if (job) {
596095
+ job.adoptedAs = "live";
596096
+ job.adoptedAt = (/* @__PURE__ */ new Date()).toISOString();
596097
+ try {
596098
+ fs7.writeFileSync(jobFile, JSON.stringify(job, null, 2), "utf-8");
596099
+ } catch {
596100
+ }
596101
+ }
596102
+ const pollHandle = setInterval(() => {
596103
+ let stillAlive = false;
596104
+ try {
596105
+ process.kill(r2.pid, 0);
596106
+ stillAlive = true;
596107
+ } catch {
596108
+ stillAlive = false;
596109
+ }
596110
+ if (stillAlive) return;
596111
+ clearInterval(pollHandle);
596112
+ runningProcesses.delete(r2.jobId);
596113
+ try {
596114
+ const jf = jobFile;
596115
+ if (!fs7.existsSync(jf)) return;
596116
+ const j = JSON.parse(fs7.readFileSync(jf, "utf-8"));
596117
+ if (j.status !== "running") return;
596118
+ let finalJson = "";
596119
+ try {
596120
+ if (r2.outputFile && fs7.existsSync(r2.outputFile)) {
596121
+ const raw = fs7.readFileSync(r2.outputFile, "utf-8");
596122
+ const lines = raw.trim().split("\n");
596123
+ for (let i2 = lines.length - 1; i2 >= 0; i2--) {
596124
+ if (lines[i2].trimEnd() === "{" || lines[i2].startsWith("{") && !lines[i2].startsWith('{"type":"tool_call"')) {
596125
+ finalJson = lines.slice(i2).join("\n");
596126
+ break;
596127
+ }
596128
+ }
596129
+ }
596130
+ } catch {
596131
+ }
596132
+ try {
596133
+ const parsed = finalJson ? JSON.parse(finalJson) : null;
596134
+ if (parsed && parsed.status) j.status = parsed.status;
596135
+ else j.status = "completed";
596136
+ j.completedAt = (/* @__PURE__ */ new Date()).toISOString();
596137
+ if (parsed?.summary) j.summary = parsed.summary;
596138
+ if (parsed?.error) j.error = parsed.error;
596139
+ fs7.writeFileSync(jf, JSON.stringify(j, null, 2), "utf-8");
596140
+ } catch {
596141
+ }
596142
+ } catch {
596143
+ }
596144
+ }, 3e3);
596145
+ pollHandle.unref();
596146
+ }
596147
+ process.stderr.write(` Adopted ${adopted.length} live run(s) from previous daemon, ${orphaned.length} orphaned.
596148
+ `);
596149
+ if (adopted.length > 0) process.stderr.write(` live: ${adopted.join(", ")}
596150
+ `);
596151
+ if (orphaned.length > 0) process.stderr.write(` orphaned: ${orphaned.join(", ")}
596152
+ `);
596153
+ }
596001
596154
  function incrementActiveJobs(user) {
596002
596155
  if (!user) return;
596003
596156
  getKeyUsage(user).activeJobs++;
@@ -601614,6 +601767,12 @@ function startApiServer(options2 = {}) {
601614
601767
  `);
601615
601768
  log22(` Primary: ${config.backendUrl} (${config.backendType || "ollama"})
601616
601769
  `);
601770
+ try {
601771
+ adoptHandoffRuns();
601772
+ } catch (e2) {
601773
+ log22(` WARN: handoff adoption crashed: ${e2.message}
601774
+ `);
601775
+ }
601617
601776
  const _retCheck = process.env["OA_RUN_RETENTION_H"];
601618
601777
  const _retOff = _retCheck === "0";
601619
601778
  if (!_retOff) {
@@ -601660,8 +601819,96 @@ function startApiServer(options2 = {}) {
601660
601819
  }).catch(() => {
601661
601820
  });
601662
601821
  });
601663
- const shutdown = () => {
601664
- log22("\n Shutting down API server...\n");
601822
+ const shutdown = (signal) => {
601823
+ const hardKill = signal === "SIGINT" || process.env["OA_DAEMON_HARD_SHUTDOWN"] === "1";
601824
+ log22(`
601825
+ Shutting down API server (signal=${signal}, mode=${hardKill ? "hard-kill" : "graceful-handoff"}) ...
601826
+ `);
601827
+ if (!hardKill && runningProcesses.size > 0) {
601828
+ try {
601829
+ const handoffPath2 = (() => {
601830
+ try {
601831
+ const home = require3("node:os").homedir();
601832
+ return require3("node:path").join(home, ".open-agents", "runs-handoff.json");
601833
+ } catch {
601834
+ return "";
601835
+ }
601836
+ })();
601837
+ if (handoffPath2) {
601838
+ const handoff = [];
601839
+ for (const [id, child] of runningProcesses) {
601840
+ const pid = child.pid;
601841
+ if (!pid || pid <= 0) continue;
601842
+ let outputFile = null;
601843
+ let startedAt2 = null;
601844
+ let user = null;
601845
+ let scope = null;
601846
+ let cwd5 = null;
601847
+ try {
601848
+ const fs7 = require3("node:fs");
601849
+ const path8 = require3("node:path");
601850
+ const home = require3("node:os").homedir();
601851
+ const jobsDir3 = path8.join(home, ".open-agents", "jobs");
601852
+ const jobFile = path8.join(jobsDir3, `${id}.json`);
601853
+ if (fs7.existsSync(jobFile)) {
601854
+ const j = JSON.parse(fs7.readFileSync(jobFile, "utf-8"));
601855
+ outputFile = j.outputFile ?? path8.join(jobsDir3, `${id}.output`);
601856
+ startedAt2 = j.startedAt ?? null;
601857
+ user = j.user ?? null;
601858
+ scope = j.scope ?? null;
601859
+ cwd5 = j.cwd ?? null;
601860
+ }
601861
+ } catch {
601862
+ }
601863
+ handoff.push({ jobId: id, pid, outputFile, startedAt: startedAt2, user, scope, cwd: cwd5, handoffAt: (/* @__PURE__ */ new Date()).toISOString() });
601864
+ }
601865
+ if (handoff.length > 0) {
601866
+ try {
601867
+ const fs7 = require3("node:fs");
601868
+ const path8 = require3("node:path");
601869
+ const home = require3("node:os").homedir();
601870
+ const dir = path8.join(home, ".open-agents");
601871
+ fs7.mkdirSync(dir, { recursive: true });
601872
+ fs7.writeFileSync(handoffPath2, JSON.stringify({
601873
+ writtenAt: (/* @__PURE__ */ new Date()).toISOString(),
601874
+ fromPid: process.pid,
601875
+ runs: handoff
601876
+ }, null, 2), "utf-8");
601877
+ log22(` Wrote handoff for ${handoff.length} in-flight run(s) → ${handoffPath2}
601878
+ `);
601879
+ log22(` Children will continue running; next daemon will adopt them.
601880
+ `);
601881
+ } catch (e2) {
601882
+ log22(` WARN: failed to write handoff (${e2.message}) — falling back to hard-kill.
601883
+ `);
601884
+ for (const [id, child] of runningProcesses) {
601885
+ const pid = child.pid;
601886
+ if (pid && pid > 0) {
601887
+ try {
601888
+ process.kill(-pid, "SIGTERM");
601889
+ } catch {
601890
+ }
601891
+ try {
601892
+ process.kill(pid, "SIGTERM");
601893
+ } catch {
601894
+ }
601895
+ }
601896
+ runningProcesses.delete(id);
601897
+ }
601898
+ }
601899
+ }
601900
+ }
601901
+ } catch (e2) {
601902
+ log22(` WARN: handoff block crashed (${e2.message}) — proceeding to server.close()
601903
+ `);
601904
+ }
601905
+ server2.close(() => {
601906
+ log22(" Server stopped (children preserved for adoption).\n");
601907
+ process.exit(0);
601908
+ });
601909
+ setTimeout(() => process.exit(0), 8e3).unref();
601910
+ return;
601911
+ }
601665
601912
  for (const [id, child] of runningProcesses) {
601666
601913
  const pid = child.pid;
601667
601914
  if (pid && pid > 0) {
@@ -601697,8 +601944,8 @@ function startApiServer(options2 = {}) {
601697
601944
  });
601698
601945
  setTimeout(() => process.exit(1), 5e3).unref();
601699
601946
  };
601700
- process.on("SIGINT", shutdown);
601701
- process.on("SIGTERM", shutdown);
601947
+ process.on("SIGINT", () => shutdown("SIGINT"));
601948
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
601702
601949
  return server2;
601703
601950
  }
601704
601951
  async function handleVisionEmbed(req2, res) {
@@ -145,6 +145,105 @@ function effectiveUser() {
145
145
  return process.env.USER || process.env.LOGNAME || os.userInfo().username;
146
146
  }
147
147
 
148
+ // ─── Force-kill port holder (regardless of how it was launched) ────────────
149
+ //
150
+ // Critical for upgrade correctness: when an OLD daemon (started before this
151
+ // install) is running, it holds port 11435 with stale in-memory code. The
152
+ // systemctl/launchctl `restart` calls below CANNOT reach it because the
153
+ // service manager doesn't own that process — it was started detached by
154
+ // `oa serve --daemon` from a previous TUI session.
155
+ //
156
+ // Result without this kill: postinstall tries to start a NEW systemd
157
+ // daemon, port-bind fails, old daemon stays alive serving stale code,
158
+ // and the user's runs continue with broken patches.
159
+ //
160
+ // Strategy (matches packages/cli/src/daemon.ts:forceKillDaemon):
161
+ // 1. SIGTERM via known PID files (~/.open-agents/daemon.pid + .oa/nexus/daemon.pid)
162
+ // 2. lsof / fuser port probe — find ANY other holder
163
+ // 3. 2s graceful grace, then SIGKILL stragglers
164
+ // 4. Poll /health up to 5s for confirmation
165
+ //
166
+ // Skips when OA_DISABLE_FORCE_KILL_DAEMON=1 (escape valve).
167
+ function forceKillPortHolder(port, cb) {
168
+ if (process.env.OA_DISABLE_FORCE_KILL_DAEMON === "1") {
169
+ log("OA_DISABLE_FORCE_KILL_DAEMON=1 — skipping port-holder kill (upgrades may not pick up new code).");
170
+ return cb(0);
171
+ }
172
+
173
+ var killed = 0;
174
+
175
+ // Step 1: SIGTERM via PID files (graceful)
176
+ var pidFiles = [
177
+ path.join(HOME, ".open-agents", "daemon.pid"),
178
+ path.join(process.cwd(), ".oa", "nexus", "daemon.pid"),
179
+ ];
180
+ pidFiles.forEach(function (pidFile) {
181
+ try {
182
+ if (!fs.existsSync(pidFile)) return;
183
+ var n = parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10);
184
+ if (!n || n <= 0) return;
185
+ try { process.kill(n, "SIGTERM"); killed++; log("SIGTERM old daemon (pid " + n + ", from " + pidFile + ")"); } catch (e) { /* dead */ }
186
+ } catch (e) { /* */ }
187
+ });
188
+
189
+ // Step 2: port probe — lsof/fuser to find ANY pid holding the port
190
+ try {
191
+ var out = "";
192
+ try {
193
+ out = cp.execSync("lsof -ti :" + port + " 2>/dev/null || fuser " + port + "/tcp 2>/dev/null || true", {
194
+ encoding: "utf8", timeout: 3000,
195
+ }).trim();
196
+ } catch (e) { /* tools unavailable */ }
197
+ if (out) {
198
+ var pids = out.split(/[\s\n]+/).map(function (s) { return parseInt(s, 10); }).filter(function (n) {
199
+ return Number.isFinite(n) && n > 0 && n !== process.pid;
200
+ });
201
+ pids.forEach(function (otherPid) {
202
+ try { process.kill(otherPid, "SIGTERM"); killed++; log("SIGTERM port-holder (pid " + otherPid + ")"); } catch (e) { /* */ }
203
+ });
204
+ }
205
+ } catch (e) { /* */ }
206
+
207
+ if (killed === 0) {
208
+ // Nothing to kill — port was already free.
209
+ return cb(0);
210
+ }
211
+
212
+ // Step 3: 2s grace, then SIGKILL stragglers
213
+ setTimeout(function () {
214
+ try {
215
+ var out = cp.execSync("lsof -ti :" + port + " 2>/dev/null || fuser " + port + "/tcp 2>/dev/null || true", {
216
+ encoding: "utf8", timeout: 3000,
217
+ }).trim();
218
+ if (out) {
219
+ var pids = out.split(/[\s\n]+/).map(function (s) { return parseInt(s, 10); }).filter(function (n) {
220
+ return Number.isFinite(n) && n > 0 && n !== process.pid;
221
+ });
222
+ pids.forEach(function (otherPid) {
223
+ try { process.kill(otherPid, "SIGKILL"); log("SIGKILL straggler (pid " + otherPid + ")"); } catch (e) { /* */ }
224
+ });
225
+ }
226
+ } catch (e) { /* */ }
227
+
228
+ // Step 4: poll until port is free (up to 5s) — required so the next
229
+ // service-manager restart binds without conflict.
230
+ var pollStart = Date.now();
231
+ function pollFree() {
232
+ if (Date.now() - pollStart > 5000) return cb(killed);
233
+ tryHealth(port, function (alive) {
234
+ if (!alive) return cb(killed);
235
+ setTimeout(pollFree, 200);
236
+ });
237
+ }
238
+ pollFree();
239
+
240
+ // Clean up PID files now that the processes are dead
241
+ pidFiles.forEach(function (pidFile) {
242
+ try { if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile); } catch (e) { /* */ }
243
+ });
244
+ }, 2000);
245
+ }
246
+
148
247
  // ─── Nexus cleanup (preserve prior postinstall behaviour) ──────────────────
149
248
 
150
249
  function cleanNexus() {
@@ -572,7 +671,25 @@ function main() {
572
671
  return safeExit(0);
573
672
  }
574
673
 
674
+ // Force-kill any process holding the daemon port BEFORE we try to install
675
+ // a service. This handles the orphan-detached-daemon case (started by
676
+ // `oa serve --daemon` from a previous TUI session) — systemctl/launchctl
677
+ // restart can't reach it, so we have to clean up explicitly. Without this,
678
+ // the new service-managed daemon fails to bind port 11435 and the user
679
+ // ends up running stale in-memory code from the previous version.
680
+ forceKillPortHolder(PORT, function (killedCount) {
681
+ if (killedCount > 0) {
682
+ log("Killed " + killedCount + " stale daemon process(es) holding port " + PORT + ".");
683
+ }
684
+ runMainAfterKill();
685
+ });
686
+ }
687
+
688
+ function runMainAfterKill() {
575
689
  // Fast path: daemon already answering /health on the target port.
690
+ // After forceKillPortHolder, this should be false unless a service manager
691
+ // already auto-restarted (rare race). We still check so the rest of the
692
+ // flow stays idempotent.
576
693
  tryHealth(PORT, function (alreadyUp) {
577
694
  var user = effectiveUser();
578
695
  var nodeBin = resolveNodeBinary();
@@ -584,10 +701,10 @@ function main() {
584
701
  }
585
702
 
586
703
  if (alreadyUp) {
587
- log("OA daemon already responding on port " + PORT + " — reinstalling service definition so it picks up the new build.");
588
- // Fall through to (re)install service files; the serve command's
589
- // daemon-mode /health pre-check will no-op if the existing daemon
590
- // is still healthy.
704
+ log("OA daemon answering /health on port " + PORT + " — re-checking version drift before service (re)install.");
705
+ // Note: we already force-killed any stale process anything answering
706
+ // now is a fresh service-managed daemon. Service (re)install below is
707
+ // idempotent and ensures the unit file matches the current bundle.
591
708
  }
592
709
 
593
710
  log("Installing OA API daemon service for port " + PORT + " ...");
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.187.515",
3
+ "version": "0.187.516",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "open-agents-ai",
9
- "version": "0.187.515",
9
+ "version": "0.187.516",
10
10
  "hasInstallScript": true,
11
11
  "license": "CC-BY-NC-4.0",
12
12
  "dependencies": {
@@ -2081,9 +2081,9 @@
2081
2081
  }
2082
2082
  },
2083
2083
  "node_modules/b4a": {
2084
- "version": "1.8.0",
2085
- "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz",
2086
- "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==",
2084
+ "version": "1.8.1",
2085
+ "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz",
2086
+ "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==",
2087
2087
  "license": "Apache-2.0",
2088
2088
  "optional": true,
2089
2089
  "peerDependencies": {
@@ -4789,9 +4789,9 @@
4789
4789
  "license": "Apache-2.0 OR MIT"
4790
4790
  },
4791
4791
  "node_modules/nanoid": {
4792
- "version": "5.1.9",
4793
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.9.tgz",
4794
- "integrity": "sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw==",
4792
+ "version": "5.1.11",
4793
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz",
4794
+ "integrity": "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==",
4795
4795
  "funding": [
4796
4796
  {
4797
4797
  "type": "github",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.187.515",
3
+ "version": "0.187.516",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",