@rubytech/taskmaster 1.0.91 → 1.0.92

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.
@@ -19,9 +19,7 @@ const applyPatchSchema = Type.Object({
19
19
  description: "Patch content using the *** Begin Patch/End Patch format.",
20
20
  }),
21
21
  });
22
- export function createApplyPatchTool(options = {}
23
- // biome-ignore lint/suspicious/noExplicitAny: TypeBox schema type from pi-agent-core uses a different module instance.
24
- ) {
22
+ export function createApplyPatchTool(options = {}) {
25
23
  const cwd = options.cwd ?? process.cwd();
26
24
  const sandboxRoot = options.sandboxRoot;
27
25
  return {
@@ -513,9 +513,7 @@ async function runExecProcess(opts) {
513
513
  kill: () => killSession(session),
514
514
  };
515
515
  }
516
- export function createExecTool(defaults
517
- // biome-ignore lint/suspicious/noExplicitAny: TypeBox schema type from pi-agent-core uses a different module instance.
518
- ) {
516
+ export function createExecTool(defaults) {
519
517
  const defaultBackgroundMs = clampNumber(defaults?.backgroundMs ?? readEnvInt("PI_BASH_YIELD_MS"), 10_000, 10, 120_000);
520
518
  const allowBackground = defaults?.allowBackground ?? true;
521
519
  const defaultTimeoutSec = typeof defaults?.timeoutSec === "number" && defaults.timeoutSec > 0
@@ -15,9 +15,7 @@ const processSchema = Type.Object({
15
15
  offset: Type.Optional(Type.Number({ description: "Log offset" })),
16
16
  limit: Type.Optional(Type.Number({ description: "Log length" })),
17
17
  });
18
- export function createProcessTool(defaults
19
- // biome-ignore lint/suspicious/noExplicitAny: TypeBox schema type from pi-agent-core uses a different module instance.
20
- ) {
18
+ export function createProcessTool(defaults) {
21
19
  if (defaults?.cleanupMs !== undefined) {
22
20
  setJobTtlMs(defaults.cleanupMs);
23
21
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.91",
2
+ "version": "1.0.92",
3
3
  "commit": "d12721761c03ecc5631483c59e1696d0ddba08a3",
4
- "builtAt": "2026-02-21T00:30:04.834Z"
4
+ "builtAt": "2026-02-21T00:34:38.199Z"
5
5
  }
@@ -1,24 +1,65 @@
1
+ import { listPortListeners, forceFreePortAndWait } from "../../cli/ports.js";
1
2
  import { GatewayLockError } from "../../infra/gateway-lock.js";
3
+ import { createSubsystemLogger } from "../../logging/subsystem.js";
4
+ const log = createSubsystemLogger("http-listen");
5
+ function tryListen(httpServer, port, bindHost) {
6
+ return new Promise((resolve, reject) => {
7
+ const onError = (err) => {
8
+ httpServer.off("listening", onListening);
9
+ reject(err);
10
+ };
11
+ const onListening = () => {
12
+ httpServer.off("error", onError);
13
+ resolve();
14
+ };
15
+ httpServer.once("error", onError);
16
+ httpServer.once("listening", onListening);
17
+ httpServer.listen(port, bindHost);
18
+ });
19
+ }
20
+ /**
21
+ * Check if the process holding the port is a previous gateway instance.
22
+ * Only returns true for node/bun processes (which is what the gateway runs as).
23
+ */
24
+ function isStaleGatewayProcess(port) {
25
+ try {
26
+ const listeners = listPortListeners(port);
27
+ if (listeners.length === 0)
28
+ return false;
29
+ return listeners.every((proc) => {
30
+ const cmd = proc.command?.toLowerCase() ?? "";
31
+ return cmd.includes("node") || cmd.includes("bun") || cmd.includes("taskmaster");
32
+ });
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
2
38
  export async function listenGatewayHttpServer(params) {
3
39
  const { httpServer, bindHost, port } = params;
4
40
  try {
5
- await new Promise((resolve, reject) => {
6
- const onError = (err) => {
7
- httpServer.off("listening", onListening);
8
- reject(err);
9
- };
10
- const onListening = () => {
11
- httpServer.off("error", onError);
12
- resolve();
13
- };
14
- httpServer.once("error", onError);
15
- httpServer.once("listening", onListening);
16
- httpServer.listen(port, bindHost);
17
- });
41
+ await tryListen(httpServer, port, bindHost);
18
42
  }
19
43
  catch (err) {
20
44
  const code = err.code;
21
45
  if (code === "EADDRINUSE") {
46
+ // Self-healing: if the port is held by a stale gateway instance (e.g.
47
+ // left behind after an upgrade), kill it and retry once. This prevents
48
+ // crash-loops where systemd keeps spawning new instances that can't bind.
49
+ if (isStaleGatewayProcess(port)) {
50
+ log.warn(`port ${port} held by stale gateway process — killing and retrying`);
51
+ try {
52
+ await forceFreePortAndWait(port, {
53
+ timeoutMs: 8000,
54
+ sigtermTimeoutMs: 3000,
55
+ });
56
+ await tryListen(httpServer, port, bindHost);
57
+ return; // Retry succeeded
58
+ }
59
+ catch (retryErr) {
60
+ throw new GatewayLockError(`port ${port} still in use after killing stale process: ${String(retryErr)}`, retryErr);
61
+ }
62
+ }
22
63
  throw new GatewayLockError(`another gateway instance is already listening on ws://${bindHost}:${port}`, err);
23
64
  }
24
65
  throw new GatewayLockError(`failed to bind gateway socket on ws://${bindHost}:${port}: ${String(err)}`, err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/taskmaster",
3
- "version": "1.0.91",
3
+ "version": "1.0.92",
4
4
  "description": "AI-powered business assistant for small businesses",
5
5
  "publishConfig": {
6
6
  "access": "public"