pi-orchestrator-config 4.3.12 → 4.3.13

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/README.md CHANGED
@@ -10,36 +10,36 @@ A [pi package](https://github.com/badlogic/pi-mono) that implements an **orchest
10
10
 
11
11
  Single extension that provides:
12
12
 
13
- | Feature | Description |
14
- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
15
- | **Subagent tool** | Delegate tasks to specialist agents (single, parallel, chain, async modes); optional `model` param for model override |
16
- | **`list_models`** | List available models and providers for subagent model override |
17
- | **Async background agents** | Spawn agents in background with `async: true` — results surface automatically when complete. Fullscreen overlay dashboard with live output, keyboard nav, and kill support (`/async-status`, `/async-kill`). On **acpx** parents, optional async is coerced to sync; dream/cron need `internal_operations_provider` + `internal_operations_model`. `cli-*` providers support async natively (see `dev-docs/cli-provider.md`) |
18
- | **CLI / ACPX providers** | Optional `cli_agents` / `acpx_agents` register `cli-*` / `acpx-*` via native `createProvider` (pi ≥ 0.84): `/login cli-<agent>` or `/login acpx-<agent>`, model refresh, filter when unavailable. Missing context window / maxTokens / cost filled from models.dev (`~/.pi/pi-config/models.dev.json`, 1-day refresh). Cold-start default restore (#753): gates `startup`\|`new`, empty `enabledModels`, trusted project merge — see `dev-docs/cli-provider.md`. Headless Cursor `--approve-mcps` when `CLI_APPROVE_MCPS` is set (`false` opts out) or the process is a sidecar (`SIDECAR_PORT`; `startSidecar()` stamps it while running and restores it on `close()`). Executable sidecar consumers should call `bindSidecarListenExit()` so bind/`stopped` fatals exit 1. Headless Gemini defaults `GEMINI_CLI_TRUST_WORKSPACE=true` and preserves an explicit parent value (including `false`). |
19
- | **`/btw` command** | Quick side questions without polluting conversation history — ephemeral overlay |
20
- | **`/async-status` command** | Show status of background agents — select one for live output streaming |
21
- | **`/async-kill` command** | Kill async agents (overlay picker or by name/id) |
22
- | **`ask_user` tool** | Structured user input with options and free-text — used by workflows |
23
- | **Python/pip enforcement** | Auto-fixes `python`/`python3` → `uv run python`; blocks `pip`/`pip3` — requires `uv` |
24
- | **Git protection** | Blocks commits/pushes to main/master, merged branches, `--no-verify`, `git add .` |
25
- | **Remote script exec block** | Blocks `curl \| bash`, `eval $(curl)`, etc. Allows safe `VAR=$(curl ...)` variable assignments |
26
- | **Dangerous command gate** | Confirms `rm -rf`, `sudo`, `mkfs`, etc. |
27
- | **Rule injection** | Injects orchestrator routing rules into system prompt |
28
- | **Git status** | Live git status in status line with colored icons — updates after every tool call. Shows clickable `#N` when the branch has an open PR. Last-activity clock `⏱ HH:MM (Xm/Xh ago)` shows time since last response |
29
- | **Desktop notifications** | Notifies via `notify-send` on task completion, waiting for input, and action required |
30
- | **File preview** | Serves generated HTML/frontend files via HTTP for browser preview from container |
31
- | **Pidash dashboard** | Live web dashboard — multi-session monitoring, browser messaging, model switching, live session name updates, reasoning token display |
32
- | **Pidiff viewer** | Per-project diff viewer with review comments — branch diffs, file tree, inline comments, git-based ignore rules |
33
- | **Dreaming** | Background memory consolidation — extracts memories from sessions, deduplicates, maintains topic-based memory |
34
- | **Memory enforcement** | Code-enforced memory entries — triggers on bash/tool/file events, actions: block, run_after, warn. LLM cannot ignore enforced rules. Dreaming-safe via _(enforced)_ marker |
35
- | **Upgrade changelog** | Shows release notes on session start after pi-config version upgrade |
36
- | **Task tracking** | Structured task lists for multi-step workflows — live widget, progress tracking, reminder nudges (owned, based on [@tintinweb/pi-tasks](https://github.com/tintinweb/pi-tasks) MIT) |
37
- | **Neovim integration** | Send changed files and review findings to nvim's quickfix list — only active when running inside nvim |
38
- | **Inter-agent communication** | P2P (`/coms`) agent communication — on-demand activation via slash command |
39
- | **Slash commands** | `/pr-review`, `/issue-review`, `/release`, `/review-local`, `/review-status`, `/query-db`, `/btw`, `/async-status`, `/async-kill`, `/status`, `/dream`, `/remember`, `/coms`, `/pi-config-settings`, `/mcpc connect` — with autocomplete argument hints |
40
- | **GitHub autocomplete** | Type `#` in the editor to get issue/PR suggestions from the current repo — lazy-loaded, 5min cache |
41
- | **Command arg completions** | Tab-complete arguments for slash commands — providers and models for `/external-ai`, branches for `/review-local`, PR numbers for `/pr-review`, and more |
42
- | **Discord bot** | Control pi sessions from your phone via Discord DMs — send prompts, answer ask_user dialogs, switch sessions |
13
+ | Feature | Description |
14
+ | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
15
+ | **Subagent tool** | Delegate tasks to specialist agents (single, parallel, chain, async modes); optional `model` param for model override |
16
+ | **`list_models`** | List available models and providers for subagent model override |
17
+ | **Async background agents** | Spawn agents in background with `async: true` — results surface automatically when complete. Fullscreen overlay dashboard with live output, keyboard nav, and kill support (`/async-status`, `/async-kill`). On **acpx** parents, optional async is coerced to sync; dream/cron need `internal_operations_provider` + `internal_operations_model`. `cli-*` providers support async natively (see `dev-docs/cli-provider.md`) |
18
+ | **CLI / ACPX providers** | Optional `cli_agents` / `acpx_agents` register `cli-*` / `acpx-*` via native `createProvider` (pi ≥ 0.84): `/login cli-<agent>` or `/login acpx-<agent>`, model refresh, filter when unavailable. Missing context window / maxTokens / cost filled from models.dev (`~/.pi/pi-config/models.dev.json`, 1-day refresh). Cold-start default restore (#753): gates `startup`\|`new`, empty `enabledModels`, trusted project merge — see `dev-docs/cli-provider.md`. Headless Cursor `--approve-mcps` when `CLI_APPROVE_MCPS` is set (`false` opts out) or the process is a sidecar (`SIDECAR_PORT`; `startSidecar()` stamps it while running and restores it on `close()`). Executable sidecar consumers should call `bindSidecarListenExit()` so bind/`stopped` fatals exit 1. Headless Gemini defaults `GEMINI_CLI_TRUST_WORKSPACE=true` and preserves an explicit parent value (including `false`). |
19
+ | **`/btw` command** | Quick side questions without polluting conversation history — ephemeral overlay |
20
+ | **`/async-status` command** | Show status of background agents — select one for live output streaming |
21
+ | **`/async-kill` command** | Kill async agents (overlay picker or by name/id) |
22
+ | **`ask_user` tool** | Structured user input with options and free-text — used by workflows |
23
+ | **Python/pip enforcement** | Auto-fixes `python`/`python3` → `uv run python`; blocks `pip`/`pip3` — requires `uv` |
24
+ | **Git protection** | Blocks commits/pushes to main/master, merged branches, `--no-verify`, `git add .` |
25
+ | **Remote script exec block** | Blocks `curl \| bash`, `eval $(curl)`, etc. Allows safe `VAR=$(curl ...)` variable assignments |
26
+ | **Dangerous command gate** | Confirms `rm -rf`, `sudo`, `mkfs`, etc. |
27
+ | **Rule injection** | Injects orchestrator routing rules into system prompt |
28
+ | **Git status** | Live git status in status line with colored icons — updates after every tool call. Shows clickable `#N` when the branch has an open PR. Last-activity clock `⏱ HH:MM (Xm/Xh ago)` shows time since last response |
29
+ | **Desktop notifications** | Notifies via `notify-send` on task completion, waiting for input, and action required |
30
+ | **File preview** | Serves generated HTML/frontend files via HTTP for browser preview from container |
31
+ | **Pidash dashboard** | Live web dashboard — multi-session monitoring, browser messaging, model switching, live session name updates, reasoning token display |
32
+ | **Pidiff viewer** | Per-project diff viewer with review comments — branch diffs, file tree, inline comments, git-based ignore rules |
33
+ | **Dreaming** | Background memory consolidation — extracts memories from sessions, deduplicates, maintains topic-based memory |
34
+ | **Memory enforcement** | Code-enforced memory entries — triggers on bash/tool/file events, actions: block, run_after, warn. LLM cannot ignore enforced rules. Dreaming-safe via _(enforced)_ marker |
35
+ | **Upgrade changelog** | Shows release notes on session start after pi-config version upgrade |
36
+ | **Task tracking** | Structured task lists for multi-step workflows — live widget, progress tracking, reminder nudges (owned, based on [@tintinweb/pi-tasks](https://github.com/tintinweb/pi-tasks) MIT) |
37
+ | **Neovim integration** | Send changed files and review findings to nvim's quickfix list — only active when running inside nvim |
38
+ | **Inter-agent communication** | P2P (`/coms`) agent communication — on-demand activation via slash command |
39
+ | **Slash commands** | `/pr-review`, `/issue-review`, `/release`, `/review-local`, `/review-status`, `/query-db`, `/btw`, `/async-status`, `/async-kill`, `/status`, `/dream`, `/remember`, `/coms`, `/pi-config-settings`, `/mcpc connect` — with autocomplete argument hints |
40
+ | **GitHub autocomplete** | Type `#` in the editor to get issue/PR suggestions from the current repo — lazy-loaded, 5min cache |
41
+ | **Command arg completions** | Tab-complete arguments for slash commands — providers and models for `/external-ai`, branches for `/review-local`, PR numbers for `/pr-review`, and more |
42
+ | **Discord bot** | Control pi sessions from your phone via Discord DMs — send prompts, answer ask_user dialogs, switch sessions |
43
43
 
44
44
  ### Agents (26)
45
45
 
@@ -55,34 +55,34 @@ Single extension that provides:
55
55
 
56
56
  ### Prompt Templates
57
57
 
58
- | Prompt | Description |
59
- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
60
- | `/implement <task>` | scout → planner → worker |
61
- | `/scout-and-plan <task>` | scout → planner |
62
- | `/implement-and-review <task>` | worker → 5 reviewers → worker |
63
- | `/pr-review [number\|url]` | Fetch PR diff, check past review comments, review with guidelines, post and track comments |
64
- | `/issue-review` | Review a GitHub issue spec and fix it |
65
- | `/release [flags]` | Create GitHub release with changelog and version bumping |
66
- | `/review-local [branch]` | Review local uncommitted or branch changes |
67
- | `/review-handler [url] [--autorabbit] [--autoqodo]` | Process PR review comments, fix approved items |
68
- | `/domain-model [focus area]` | Scan codebase and build/update a CONTEXT.md domain glossary for consistent AI vocabulary |
69
- | `/refine-review <url>` | Refine and improve existing PR review comments |
70
- | `/coderabbit-rate-limit [number\|url]` | Handle CodeRabbit rate limiting on PRs |
71
- | `/create-coms-feature-manager` | Generate a coms feature-manager prompt customized for the current project (source template: `templates/coms-feature-manager-prompt.md`) |
72
- | `/query-db <command>` | Query the review comments database |
73
- | `/external-ai <agent> [--model <model>] [--session-id <id>] [--fix\|--peer\|--resume] <prompt>` | Run prompts via AI CLIs directly (cursor, claude, gemini) — full model access |
74
- | `/external-ai-models-refresh` | Refresh cached AI CLI models for autocomplete |
75
- | `/dream` | Run memory consolidation — extract, deduplicate, maintain topic-based memory |
76
- | `/remember <what>` | Save a memory for future sessions |
77
- | `/dream-auto` | Toggle automatic memory dreaming (every 3h + session end) |
78
- | `/cron add\|list\|list-all\|remove` | Schedule recurring tasks within the pi session (e.g., `/cron add every 2h check for new issues`, `/cron add at 12:00 /review-handler`). `/cron list` and `/cron list-all` open overlay UI (view / remove; list-all = all sessions). Tasks run while pi is active, survive `/reload`, and stop on exit |
79
- | `/async-kill [name\|id\|all]` | Kill async agents (overlay picker or by name/id) |
80
- | `/pi-config-settings [project\|global]` | Interactive TUI settings editor — browse all pi-config settings with scope indicators, smart pickers for providers/models, and immediate save |
81
- | `/status` | Unified session snapshot — async agents, cron tasks, git branch, context usage |
82
- | `/nvim-changed-files` | Send git changed files to nvim's quickfix list (only inside nvim) |
83
- | `/pidiff start\|stop\|restart\|status` | Manage the pidiff diff viewer server (per-project) |
84
- | `/coms start\|stop\|status` | P2P local agent communication (Unix socket) |
85
- | `/mcpc connect` | Connect MCP servers from `~/.pi/pi-config/mcp.json` (`mcpc connect --stdio`). Run after editing that file. |
58
+ | Prompt | Description |
59
+ | ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
60
+ | `/implement <task>` | scout → planner → worker |
61
+ | `/scout-and-plan <task>` | scout → planner |
62
+ | `/implement-and-review <task>` | worker → 5 reviewers → worker |
63
+ | `/pr-review [number\|url]` | Fetch PR diff, check past review comments, review with guidelines, post and track comments |
64
+ | `/issue-review` | Review a GitHub issue spec and fix it |
65
+ | `/release [flags]` | Create GitHub release with changelog and version bumping |
66
+ | `/review-local [branch]` | Review local uncommitted or branch changes |
67
+ | `/review-handler [url] [--autorabbit] [--autoqodo]` | Process PR review comments, fix approved items |
68
+ | `/domain-model [focus area]` | Scan codebase and build/update a CONTEXT.md domain glossary for consistent AI vocabulary |
69
+ | `/refine-review <url>` | Refine and improve existing PR review comments |
70
+ | `/coderabbit-rate-limit [number\|url]` | Handle CodeRabbit rate limiting on PRs |
71
+ | `/create-coms-feature-manager` | Generate a coms feature-manager prompt customized for the current project (source template: `templates/coms-feature-manager-prompt.md`) |
72
+ | `/query-db <command>` | Query the review comments database |
73
+ | `/external-ai <agent> [--model <model>] [--session-id <id>] [--fix\|--peer\|--resume] <prompt>` | Run prompts via AI CLIs directly (cursor, claude, gemini) — full model access |
74
+ | `/external-ai-models-refresh` | Refresh cached AI CLI models for autocomplete |
75
+ | `/dream` | Run memory consolidation — extract, deduplicate, maintain topic-based memory |
76
+ | `/remember <what>` | Save a memory for future sessions |
77
+ | `/dream-auto` | Toggle automatic memory dreaming (every 3h + session end) |
78
+ | `/cron add\|list\|list-all\|remove [--persist]` | Schedule recurring tasks. Session jobs end with the session; `--persist` keeps a job across Pi sessions in this project. Add/remove mutations still work when leader election fails closed on an unsupported platform. For example: `/cron add every 2h check for new issues --persist` or `/cron remove persist:<uuid>`. `/cron list` opens an overlay for the selected jobs (or both when omitted). Persistent delivery is local at-least-once: a crash or leader recovery can repeat a run. |
79
+ | `/async-kill [name\|id\|all]` | Kill async agents (overlay picker or by name/id) |
80
+ | `/pi-config-settings [project\|global]` | Interactive TUI settings editor — browse all pi-config settings with scope indicators, smart pickers for providers/models, and immediate save |
81
+ | `/status` | Unified session snapshot — async agents, cron tasks, git branch, context usage |
82
+ | `/nvim-changed-files` | Send git changed files to nvim's quickfix list (only inside nvim) |
83
+ | `/pidiff start\|stop\|restart\|status` | Manage the pidiff diff viewer server (per-project) |
84
+ | `/coms start\|stop\|status` | P2P local agent communication (Unix socket) |
85
+ | `/mcpc connect` | Connect MCP servers from `~/.pi/pi-config/mcp.json` (`mcpc connect --stdio`). Run after editing that file. |
86
86
 
87
87
  ### Inter-Agent Communication (coms)
88
88
 
@@ -98,14 +98,14 @@ Two systems for Pi agents to communicate with each other, activated on-demand vi
98
98
 
99
99
  **Tools available once activated:**
100
100
 
101
- | Tool | Description |
102
- | --------- | --------------------------------------------------------------- |
103
- | `*_list` | List peer agents with names, models, context usage, queue depth |
104
- | `*_send` | Send a prompt to a peer agent |
105
- | `*_get` | Non-blocking poll for a response |
106
- | `*_await` | Block until response arrives |
107
- | `coms_queue_inspect` | Preview owned queued P2P messages before recovery |
108
- | `coms_queue_clear` | Irreversibly clear only an inspect preview |
101
+ | Tool | Description |
102
+ | -------------------- | --------------------------------------------------------------- |
103
+ | `*_list` | List peer agents with names, models, context usage, queue depth |
104
+ | `*_send` | Send a prompt to a peer agent |
105
+ | `*_get` | Non-blocking poll for a response |
106
+ | `*_await` | Block until response arrives |
107
+ | `coms_queue_inspect` | Preview owned queued P2P messages before recovery |
108
+ | `coms_queue_clear` | Irreversibly clear only an inspect preview |
109
109
 
110
110
  Queue recovery is explicit. Run `coms_queue_inspect` first and review its
111
111
  body-free preview. Then pass its `preview_id` to `coms_queue_clear`.
@@ -154,13 +154,15 @@ interface RegistryEntry {
154
154
  }
155
155
 
156
156
  interface PendingReply {
157
- resolve: (value: any) => void;
158
- reject: (err: Error) => void;
159
- timer: NodeJS.Timeout | null;
160
- promise: Promise<{ response?: any; error?: string | null; queued_msg_ids?: string[] }>;
157
+ resolve?: (value: any) => void;
158
+ reject?: (err: Error) => void;
159
+ timer?: NodeJS.Timeout | null;
160
+ promise?: Promise<{ response?: any; error?: string | null; queued_msg_ids?: string[] }>;
161
161
  result?: { response?: any; error?: string | null; queued_msg_ids?: string[] };
162
+ target_session?: string;
162
163
  target_name?: string;
163
- created_at: string;
164
+ sent_at?: number;
165
+ created_at?: string;
164
166
  /** Creation time drives body-free recovery previews. */
165
167
  queued_at?: string;
166
168
  }
@@ -549,7 +551,10 @@ function sendEnvelope(endpoint: string, envelope: Envelope | Pong | { type: stri
549
551
  sock.write(JSON.stringify(envelope) + "\n");
550
552
  const line = await readOneLine(sock);
551
553
  const parsed = JSON.parse(line);
552
- try { sock.end(); } catch { /* ignore */ }
554
+ // The response is complete. Destroy the outbound client now instead of
555
+ // waiting for the peer's half-close, which can outlive a test session.
556
+ try { sock.destroy(); } catch { /* ignore */ }
557
+ log.debug("coms_envelope_sent", { type: envelope.type, has_msg_id: !!envelope.msg_id });
553
558
  if (settled) return;
554
559
  settled = true;
555
560
  if (parsed && parsed.type === "nack") {
@@ -642,6 +647,7 @@ export default function (pi: ExtensionAPI) {
642
647
  log.debug("recovery_preview_retained", { session: targetSession, count: itemIds.length, active: queueRecoveryPreviews.size });
643
648
  }
644
649
  let server: net.Server | null = null;
650
+ const connections = new Set<net.Socket>();
645
651
  let fsWatcher: fs.FSWatcher | null = null;
646
652
  let pingWorker: Worker | null = null;
647
653
  let pingWorkerReady = false;
@@ -795,7 +801,7 @@ export default function (pi: ExtensionAPI) {
795
801
  const queuedMsgIds: string[] = Array.isArray(env.queued_msg_ids) ? env.queued_msg_ids.filter((id: unknown) => typeof id === "string") : [];
796
802
  pending.result = { response: env.response, error: env.error ?? null, queued_msg_ids: queuedMsgIds };
797
803
  try {
798
- pending.resolve(pending.result);
804
+ pending.resolve?.(pending.result);
799
805
  } catch (e: any) {
800
806
  log.error("resolve_failed", env.msg_id, e?.message);
801
807
  }
@@ -892,6 +898,8 @@ export default function (pi: ExtensionAPI) {
892
898
  }
893
899
 
894
900
  function connHandler(socket: net.Socket): void {
901
+ connections.add(socket);
902
+ socket.once("close", () => connections.delete(socket));
895
903
  let buf = "";
896
904
  let handled = false;
897
905
  const onData = (chunk: Buffer) => {
@@ -1030,7 +1038,7 @@ export default function (pi: ExtensionAPI) {
1030
1038
  const storedPreview = queueRecoveryPreviews.get(env.preview_id);
1031
1039
  if (!storedPreview || storedPreview.targetSession !== env.sender_session) {
1032
1040
  error = "invalid_preview";
1033
- try { socket.write(JSON.stringify({ type: "ack", msg_id: env.msg_id, affected, error }) + "\n"); } catch {}
1041
+ try { socket.write(JSON.stringify({ type: "ack", msg_id: env.msg_id, affected, error }) + "\n"); socket.end(); } catch {}
1034
1042
  log.warn("recovery_clear_denied", { sender: env.sender_name, reason: error });
1035
1043
  return;
1036
1044
  }
@@ -1056,7 +1064,7 @@ export default function (pi: ExtensionAPI) {
1056
1064
  }));
1057
1065
  const preview = buildQueuePreview("local", items, Date.now(), ulid());
1058
1066
  retainQueueRecoveryPreview(preview.previewId, env.sender_session, preview.items.map(item => item.id));
1059
- try { socket.write(JSON.stringify({ type: "ack", msg_id: env.msg_id, affected: 0, error: null, preview }) + "\n"); } catch {}
1067
+ try { socket.write(JSON.stringify({ type: "ack", msg_id: env.msg_id, affected: 0, error: null, preview }) + "\n"); socket.end(); } catch {}
1060
1068
  log.info("recovery_preview", { provider: "local", sender: env.sender_name, count: preview.items.length });
1061
1069
  return;
1062
1070
  } else if (action === "prioritize" && env.target_msg_id) {
@@ -1078,6 +1086,7 @@ export default function (pi: ExtensionAPI) {
1078
1086
  // Send ack with result — includes error and affected count
1079
1087
  try {
1080
1088
  socket.write(JSON.stringify({ type: "ack", msg_id: env.msg_id, affected, error }) + "\n");
1089
+ socket.end();
1081
1090
  } catch { /* ignore */ }
1082
1091
 
1083
1092
  maybeRefreshWidget();
@@ -3217,11 +3226,22 @@ Do not respond to this message.`;
3217
3226
 
3218
3227
  // ━━ Clean shutdown ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3219
3228
  let shuttingDown = false;
3229
+ async function stopPingWorker(worker: Worker): Promise<void> {
3230
+ let timeout: NodeJS.Timeout | undefined;
3231
+ const exited = new Promise<boolean>((resolve) => worker.once("exit", () => resolve(true)));
3232
+ log.info("ping_worker_stop", { outcome: "shutdown_requested" });
3233
+ try { worker.postMessage({ type: "shutdown" }); } catch {}
3234
+ const graceful = await Promise.race([exited, new Promise<boolean>((resolve) => { timeout = setTimeout(() => resolve(false), 1_000); })]);
3235
+ if (timeout) clearTimeout(timeout);
3236
+ if (!graceful) try { await worker.terminate(); } catch {}
3237
+ log.info("ping_worker_stop", { outcome: graceful ? "graceful" : "terminated" });
3238
+ }
3220
3239
  async function cleanShutdown(): Promise<void> {
3221
3240
  if (shuttingDown) return;
3222
3241
  shuttingDown = true;
3223
3242
  try { pi.events.emit("pidash:coms-identity", { name: null, purpose: null }); } catch {}
3224
3243
  const ident = identity; // snapshot before nulling
3244
+ const shutdownCtx = currentCtx;
3225
3245
  identity = null; // null identity so old handlers from previous reload cycles exit via !identity check
3226
3246
  if (fsWatcher) { try { fsWatcher.close(); } catch { /* ignore */ } fsWatcher = null; }
3227
3247
  if (keepaliveTimer) { try { clearInterval(keepaliveTimer); } catch { /* ignore */ } keepaliveTimer = null; }
@@ -3257,13 +3277,20 @@ Do not respond to this message.`;
3257
3277
  }
3258
3278
  }
3259
3279
  if (server) {
3260
- try { server.close(); } catch { /* ignore */ }
3280
+ const activeServer = server;
3261
3281
  server = null;
3282
+ try {
3283
+ for (const connection of connections) connection.destroy();
3284
+ connections.clear();
3285
+ activeServer.closeAllConnections?.(); activeServer.close(); activeServer.unref();
3286
+ } catch { /* ignore */ }
3262
3287
  }
3263
3288
  if (pingWorker) {
3264
- try { pingWorker.postMessage({ type: "shutdown" }); } catch {}
3289
+ const worker = pingWorker;
3265
3290
  pingWorker = null;
3266
3291
  pingWorkerReady = false;
3292
+ await stopPingWorker(worker);
3293
+ log.info("ping_worker_shutdown", { outcome: "graceful_or_terminated" });
3267
3294
  }
3268
3295
  if (ident) {
3269
3296
  if (process.platform !== "win32") {
@@ -3272,15 +3299,25 @@ Do not respond to this message.`;
3272
3299
  try { removeRegistryEntry(ident.project, ident.coms_session_id); } catch { /* ignore */ }
3273
3300
  log.info("shutdown", ident.coms_session_id);
3274
3301
  }
3302
+ for (const pending of pendingReplies.values()) {
3303
+ if (pending.timer) clearTimeout(pending.timer);
3304
+ if (pending.resolve) pending.resolve({ error: "session_closed" });
3305
+ }
3306
+ pendingReplies.clear();
3307
+ pendingOutbound.clear();
3275
3308
  queueRecoveryPreviews.clear();
3309
+ process.off("SIGINT", handleSigint);
3310
+ process.off("SIGTERM", handleSigterm);
3276
3311
  log.debug("recovery_previews_cleared", { reason: "shutdown" });
3277
- if (currentCtx?.hasUI) {
3278
- try { currentCtx.ui.setWidget("coms-pool", undefined); } catch { /* ignore */ }
3279
- try { currentCtx.ui.setStatus("coms", undefined); } catch { /* ignore */ }
3312
+ if (shutdownCtx?.hasUI) {
3313
+ try { shutdownCtx.ui.setWidget("coms-pool", undefined); } catch { /* ignore */ }
3314
+ try { shutdownCtx.ui.setStatus("coms", undefined); } catch { /* ignore */ }
3280
3315
  }
3281
3316
  }
3282
3317
 
3318
+ const handleSigint = () => { log.info("signal_received", { signal: "SIGINT" }); void cleanShutdown(); };
3319
+ const handleSigterm = () => { log.info("signal_received", { signal: "SIGTERM" }); void cleanShutdown(); };
3283
3320
  pi.on("session_shutdown", async () => { await cleanShutdown(); });
3284
- process.on("SIGINT", () => { void cleanShutdown(); });
3285
- process.on("SIGTERM", () => { void cleanShutdown(); });
3321
+ process.on("SIGINT", handleSigint);
3322
+ process.on("SIGTERM", handleSigterm);
3286
3323
  }