@tekmidian/pai 0.59.0 → 0.60.0

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.
Files changed (129) hide show
  1. package/.claude-plugin/plugin.json +8 -33
  2. package/PLUGIN-ARCHITECTURE.md +4 -6
  3. package/dist/{aibroker-client-B8Wtasez.mjs → aibroker-client-BYG-GtAg.mjs} +1 -1
  4. package/dist/{chain-Cltx0NjY.mjs → chain-DFlzAfDE.mjs} +4 -4
  5. package/dist/{chain-Cltx0NjY.mjs.map → chain-DFlzAfDE.mjs.map} +1 -1
  6. package/dist/cli/index.mjs +12 -12
  7. package/dist/cli/program.mjs +12 -12
  8. package/dist/{config-DCYWFaEi.mjs → config-GceCMUxa.mjs} +1 -1
  9. package/dist/{config-D1omG_UU.mjs → config-msER8xj_.mjs} +16 -4
  10. package/dist/config-msER8xj_.mjs.map +1 -0
  11. package/dist/{context-handover-cache-Dt4HW1Ro.mjs → context-handover-cache-DF0xWVB0.mjs} +4 -4
  12. package/dist/{context-handover-cache-Dt4HW1Ro.mjs.map → context-handover-cache-DF0xWVB0.mjs.map} +1 -1
  13. package/dist/daemon/index.mjs +12 -12
  14. package/dist/daemon-BppwJtrH.mjs +19 -0
  15. package/dist/{daemon-CmqJfeb9.mjs → daemon-DArRKBlk.mjs} +97 -23
  16. package/dist/daemon-DArRKBlk.mjs.map +1 -0
  17. package/dist/daemon-mcp/index.mjs +9 -9
  18. package/dist/daemon-mcp/index.mjs.map +1 -1
  19. package/dist/{factory-DiuntvwG.mjs → factory-ChfAvCzO.mjs} +7 -7
  20. package/dist/{factory-DiuntvwG.mjs.map → factory-ChfAvCzO.mjs.map} +1 -1
  21. package/dist/{factory-C9wGUe30.mjs → factory-D6I2jmjQ.mjs} +1 -1
  22. package/dist/{fallback-B_2ozQHw.mjs → fallback-B7VEMMRn.mjs} +80 -9
  23. package/dist/fallback-B7VEMMRn.mjs.map +1 -0
  24. package/dist/hooks/block-sleep-poll.mjs +7590 -0
  25. package/dist/hooks/block-sleep-poll.mjs.map +7 -0
  26. package/dist/hooks/capture-all-events.mjs +8 -4
  27. package/dist/hooks/capture-all-events.mjs.map +2 -2
  28. package/dist/hooks/capture-session-summary.mjs.map +1 -1
  29. package/dist/hooks/capture-tool-output.mjs.map +1 -1
  30. package/dist/hooks/cleanup-session-files.mjs.map +1 -1
  31. package/dist/hooks/context-compression-hook.mjs +6 -6
  32. package/dist/hooks/context-compression-hook.mjs.map +3 -3
  33. package/dist/hooks/initialize-session.mjs.map +1 -1
  34. package/dist/hooks/load-core-context.mjs.map +1 -1
  35. package/dist/hooks/load-project-context.mjs.map +2 -2
  36. package/dist/hooks/post-compact-inject.mjs +1 -1
  37. package/dist/hooks/post-compact-inject.mjs.map +2 -2
  38. package/dist/hooks/route-agents-to-worker.mjs +4 -3
  39. package/dist/hooks/route-agents-to-worker.mjs.map +3 -3
  40. package/dist/hooks/security-validator.mjs +4 -3
  41. package/dist/hooks/security-validator.mjs.map +3 -3
  42. package/dist/hooks/stop-hook.mjs.map +1 -1
  43. package/dist/hooks/sync-todo-to-md.mjs.map +1 -1
  44. package/dist/hooks/whisper-rules.mjs +4 -3
  45. package/dist/hooks/whisper-rules.mjs.map +3 -3
  46. package/dist/hooks/worker-proxy.mjs +4 -3
  47. package/dist/hooks/worker-proxy.mjs.map +3 -3
  48. package/dist/hooks/worker-status-line.mjs +9 -6
  49. package/dist/hooks/worker-status-line.mjs.map +3 -3
  50. package/dist/hooks/worker-supervision.mjs +4 -3
  51. package/dist/hooks/worker-supervision.mjs.map +3 -3
  52. package/dist/{main-resolver-DYNxxNwJ.mjs → main-resolver-BqTjt-GL.mjs} +100 -21
  53. package/dist/main-resolver-BqTjt-GL.mjs.map +1 -0
  54. package/dist/main-resolver-CCojcYr2.mjs +7 -0
  55. package/dist/{planner-DsZZEZCA.mjs → planner-DbpsMZqY.mjs} +83 -30
  56. package/dist/planner-DbpsMZqY.mjs.map +1 -0
  57. package/dist/{program-B5miXqQa.mjs → program-QSuci3RH.mjs} +278 -92
  58. package/dist/program-QSuci3RH.mjs.map +1 -0
  59. package/dist/router-C5hPk9YP.mjs +3 -0
  60. package/dist/{router-BdxdsAvs.mjs → router-DjFZIoDl.mjs} +2 -1
  61. package/dist/router-DjFZIoDl.mjs.map +1 -0
  62. package/dist/{run-XaA-UPdW.mjs → run-BFxuNnIJ.mjs} +100 -17
  63. package/dist/run-BFxuNnIJ.mjs.map +1 -0
  64. package/dist/{run-env-DyTWgZC9.mjs → run-env-kbWVwUvU.mjs} +2 -2
  65. package/dist/{run-env-DyTWgZC9.mjs.map → run-env-kbWVwUvU.mjs.map} +1 -1
  66. package/dist/{server-CzqHGbKt.mjs → server-B0vBmfJt.mjs} +2 -2
  67. package/dist/{server-CzqHGbKt.mjs.map → server-B0vBmfJt.mjs.map} +1 -1
  68. package/dist/{session-keepalive-DDsEo81Q.mjs → session-keepalive-CUcFa8ld.mjs} +5 -5
  69. package/dist/{session-keepalive-DDsEo81Q.mjs.map → session-keepalive-CUcFa8ld.mjs.map} +1 -1
  70. package/dist/{tools-Diro2s8J.mjs → tools-CaTZnIau.mjs} +2 -2
  71. package/dist/{tools-Diro2s8J.mjs.map → tools-CaTZnIau.mjs.map} +1 -1
  72. package/dist/{tools-KgL1rkPD.mjs → tools-_7MF8Ebb.mjs} +3 -3
  73. package/dist/{wakeup-D0uvqLsE.mjs → wakeup-CTX3rlz-.mjs} +2 -2
  74. package/dist/wakeup-CTX3rlz-.mjs.map +1 -0
  75. package/dist/{work-queue-worker-DL9lu8sN.mjs → work-queue-worker-B4eZGy5n.mjs} +2 -2
  76. package/dist/{work-queue-worker-DL9lu8sN.mjs.map → work-queue-worker-B4eZGy5n.mjs.map} +1 -1
  77. package/dist/work-queue-worker-DuhZmfDB.mjs +11 -0
  78. package/docker/init.sql +1 -1
  79. package/docs/commands/README.md +4 -4
  80. package/docs/commands/daemon.md +6 -6
  81. package/docs/commands/session.md +2 -2
  82. package/docs/commands/worker.md +5 -2
  83. package/docs/workers-config.md +5 -1
  84. package/package.json +1 -1
  85. package/plugins/context-preservation/hooks/hooks.json +38 -31
  86. package/plugins/context-preservation/plugin.json +1 -4
  87. package/plugins/core/hooks/hooks.json +51 -46
  88. package/plugins/core/plugin.json +1 -4
  89. package/plugins/creative/plugin.json +1 -5
  90. package/plugins/observability/hooks/hooks.json +87 -72
  91. package/plugins/observability/plugin.json +1 -5
  92. package/plugins/productivity/hooks/hooks.json +22 -19
  93. package/plugins/productivity/plugin.json +2 -5
  94. package/plugins/semantic-search/plugin.json +1 -16
  95. package/plugins/ui/hooks/hooks.json +17 -14
  96. package/plugins/ui/plugin.json +1 -5
  97. package/plugins/zettelkasten/plugin.json +1 -14
  98. package/src/hooks/session-autosave.sh +4 -2
  99. package/src/hooks/session-stop.sh +2 -2
  100. package/src/hooks/ts/capture-all-events.ts +3 -3
  101. package/src/hooks/ts/lib/metadata-extraction.ts +3 -2
  102. package/src/hooks/ts/lib/pai-paths.test.ts +1 -1
  103. package/src/hooks/ts/lib/sleep-poll-gate.test.ts +62 -0
  104. package/src/hooks/ts/lib/sleep-poll-gate.ts +77 -0
  105. package/src/hooks/ts/post-tool-use/capture-tool-output.ts +1 -1
  106. package/src/hooks/ts/post-tool-use/sync-todo-to-md.ts +1 -1
  107. package/src/hooks/ts/pre-compact/context-compression-hook.ts +1 -1
  108. package/src/hooks/ts/pre-tool-use/block-sleep-poll.test.ts +44 -0
  109. package/src/hooks/ts/pre-tool-use/block-sleep-poll.ts +50 -0
  110. package/src/hooks/ts/session-end/capture-session-summary.ts +1 -1
  111. package/src/hooks/ts/session-start/initialize-session.ts +2 -2
  112. package/src/hooks/ts/session-start/load-core-context.ts +2 -2
  113. package/src/hooks/ts/session-start/load-project-context.ts +1 -1
  114. package/src/hooks/ts/stop/stop-hook.ts +1 -1
  115. package/src/hooks/ts/user-prompt/cleanup-session-files.ts +1 -1
  116. package/statusline-command.sh +26 -11
  117. package/dist/config-D1omG_UU.mjs.map +0 -1
  118. package/dist/daemon-B6hx1Ysz.mjs +0 -19
  119. package/dist/daemon-CmqJfeb9.mjs.map +0 -1
  120. package/dist/fallback-B_2ozQHw.mjs.map +0 -1
  121. package/dist/main-resolver-DYNxxNwJ.mjs.map +0 -1
  122. package/dist/main-resolver-Df65bPAi.mjs +0 -7
  123. package/dist/planner-DsZZEZCA.mjs.map +0 -1
  124. package/dist/program-B5miXqQa.mjs.map +0 -1
  125. package/dist/router-BXOf_e9e.mjs +0 -3
  126. package/dist/router-BdxdsAvs.mjs.map +0 -1
  127. package/dist/run-XaA-UPdW.mjs.map +0 -1
  128. package/dist/wakeup-D0uvqLsE.mjs.map +0 -1
  129. package/dist/work-queue-worker-BY7xn4pw.mjs +0 -11
@@ -0,0 +1,3 @@
1
+ import { t as routeNotification } from "./router-DjFZIoDl.mjs";
2
+
3
+ export { routeNotification };
@@ -131,6 +131,7 @@ var MacOsProvider = class {
131
131
  channelId = "macos";
132
132
  async send(payload, config) {
133
133
  if (!config.channels.macos.enabled) return false;
134
+ if (process.platform !== "darwin") return false;
134
135
  try {
135
136
  const safeTitle = (payload.title ?? "PAI").replace(/'/g, "\\'");
136
137
  const script = `display notification "${payload.message.replace(/'/g, "\\'")}" with title "${safeTitle}"`;
@@ -246,4 +247,4 @@ async function routeNotification(payload, config) {
246
247
 
247
248
  //#endregion
248
249
  export { routeNotification as t };
249
- //# sourceMappingURL=router-BdxdsAvs.mjs.map
250
+ //# sourceMappingURL=router-DjFZIoDl.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router-DjFZIoDl.mjs","names":[],"sources":["../src/notifications/providers/ntfy.ts","../src/notifications/providers/whatsapp.ts","../src/notifications/providers/macos.ts","../src/notifications/providers/cli.ts","../src/notifications/router.ts"],"sourcesContent":["/**\n * ntfy.ts — ntfy.sh notification provider\n *\n * Sends notifications to a configured ntfy.sh topic via HTTP.\n */\n\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\nexport class NtfyProvider implements NotificationProvider {\n readonly channelId = \"ntfy\" as const;\n\n async send(\n payload: NotificationPayload,\n config: NotificationConfig\n ): Promise<boolean> {\n const cfg = config.channels.ntfy;\n if (!cfg.enabled || !cfg.url) return false;\n\n try {\n const headers: Record<string, string> = {\n \"Content-Type\": \"text/plain; charset=utf-8\",\n };\n\n if (payload.title) {\n headers[\"Title\"] = payload.title;\n }\n\n if (cfg.priority && cfg.priority !== \"default\") {\n headers[\"Priority\"] = cfg.priority;\n }\n\n const response = await fetch(cfg.url, {\n method: \"POST\",\n headers,\n body: payload.message,\n });\n\n return response.ok;\n } catch {\n return false;\n }\n }\n}\n","/**\n * whatsapp.ts — WhatsApp notification provider (via the AIBroker hub)\n *\n * Sends through AIBroker's hub socket, which proxies to the whazaa adapter.\n *\n * It used to dial /tmp/whazaa.sock and call a method named `whatsapp_send`.\n * Both were wrong, and had been since AIBroker became the runtime hub and\n * adapters became thin transports: Whazaa no longer owns an IPC socket of its\n * own (it registers with the hub, currently at /tmp/whazaa-watcher.sock, which\n * is the hub's business and not ours), and `whatsapp_send` is an MCP TOOL name\n * — the adapter itself takes `send` and `tts`.\n *\n * So every WhatsApp notification failed. Silently, because a failed channel\n * writes one line to stderr and the router has no fallback: on 2026-08-04 four\n * task-bus escalations about a job that had not run for nine hours reached\n * /tmp/pai-scheduler.log and nowhere else, while the user had no idea.\n *\n * Routing through the hub rather than at the adapter directly is deliberate:\n * the hub knows where its adapters are, and that is exactly the knowledge whose\n * absence broke this. Connect-per-call, no persistent state.\n */\n\nimport { connect } from \"node:net\";\nimport { randomUUID } from \"node:crypto\";\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\n/**\n * AIBroker's hub socket.\n *\n * Hardcoded rather than imported: PAI must not depend on AIBroker. This is a\n * protocol constant between them, like the agent mark in tasks/poller.ts.\n */\nconst HUB_SOCKET = \"/tmp/aibroker.sock\";\nconst HUB_TIMEOUT_MS = 10_000;\n\n/**\n * Send a single IPC call to the hub.\n * Returns true on success, false if the hub is not available or errors.\n */\nfunction callHub(\n method: string,\n params: Record<string, unknown>\n): Promise<boolean> {\n return new Promise((resolve) => {\n let done = false;\n let buffer = \"\";\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n function finish(ok: boolean): void {\n if (done) return;\n done = true;\n if (timer) { clearTimeout(timer); timer = null; }\n try { socket?.destroy(); } catch { /* ignore */ }\n resolve(ok);\n }\n\n const socket = connect(HUB_SOCKET, () => {\n const request = {\n jsonrpc: \"2.0\",\n id: randomUUID(),\n method,\n params,\n };\n socket.write(JSON.stringify(request) + \"\\n\");\n });\n\n socket.on(\"data\", (chunk: Buffer) => {\n buffer += chunk.toString();\n const nl = buffer.indexOf(\"\\n\");\n if (nl === -1) return;\n try {\n const resp = JSON.parse(buffer.slice(0, nl)) as { error?: unknown; ok?: boolean };\n // The hub reports failure as {ok:false,error}; JSON-RPC reports {error}.\n // Checking only `error` counted an {ok:false} reply as a delivered\n // notification, which is the one mistake this file must never make.\n finish(!resp.error && resp.ok !== false);\n } catch {\n finish(false);\n }\n });\n\n socket.on(\"error\", () => finish(false));\n socket.on(\"end\", () => finish(false));\n\n timer = setTimeout(() => finish(false), HUB_TIMEOUT_MS);\n });\n}\n\nexport class WhatsAppProvider implements NotificationProvider {\n readonly channelId = \"whatsapp\" as const;\n\n async send(\n payload: NotificationPayload,\n config: NotificationConfig\n ): Promise<boolean> {\n const cfg = config.channels.whatsapp;\n if (!cfg.enabled) return false;\n\n const isVoiceMode = config.mode === \"voice\" || config.channels.voice.enabled;\n const asVoice = isVoiceMode && config.mode === \"voice\";\n\n // The adapter's own vocabulary is `send` and `tts`, reached through the\n // hub's `adapter_call`. `whatsapp_send` — what this used to ask for — is the\n // name of the MCP TOOL that wraps it, and the adapter has never answered to\n // it. The two vocabularies are easy to confuse because the MCP tool exists\n // and works; it just is not this interface.\n const inner: Record<string, unknown> = asVoice\n ? { text: payload.message, voice: config.channels.voice.voiceName ?? \"bm_george\" }\n : { message: payload.message };\n\n if (cfg.recipient) {\n // `send` addresses by `recipient`, `tts` by `jid` — same destination,\n // different key, per the adapter's interface.\n inner[asVoice ? \"jid\" : \"recipient\"] = cfg.recipient;\n }\n\n return callHub(\"adapter_call\", {\n adapter: \"whazaa\",\n method: asVoice ? \"tts\" : \"send\",\n params: inner,\n });\n }\n}\n","/**\n * macos.ts — macOS notification provider\n *\n * Uses the `osascript` command to display a macOS system notification.\n * Non-blocking: spawns the process and returns success without waiting.\n */\n\nimport { spawn } from \"node:child_process\";\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\nexport class MacOsProvider implements NotificationProvider {\n readonly channelId = \"macos\" as const;\n\n async send(\n payload: NotificationPayload,\n config: NotificationConfig\n ): Promise<boolean> {\n const cfg = config.channels.macos;\n if (!cfg.enabled) return false;\n if (process.platform !== \"darwin\") return false;\n\n try {\n const title = payload.title ?? \"PAI\";\n // Escape single quotes in title and message for AppleScript\n const safeTitle = title.replace(/'/g, \"\\\\'\");\n const safeMessage = payload.message.replace(/'/g, \"\\\\'\");\n\n const script = `display notification \"${safeMessage}\" with title \"${safeTitle}\"`;\n\n return new Promise((resolve) => {\n const child = spawn(\"osascript\", [\"-e\", script], {\n detached: true,\n stdio: \"ignore\",\n });\n child.unref();\n\n // Give the process a moment to start, then assume success.\n // osascript is always present on macOS.\n child.on(\"error\", () => resolve(false));\n\n // Resolve after a short timeout — osascript exits quickly\n setTimeout(() => resolve(true), 200);\n });\n } catch {\n return false;\n }\n }\n}\n","/**\n * cli.ts — CLI notification provider\n *\n * Writes notifications to the PAI daemon log (stderr).\n * Always succeeds — it's the fallback channel.\n */\n\nimport type {\n NotificationProvider,\n NotificationPayload,\n NotificationConfig,\n} from \"../types.js\";\n\nexport class CliProvider implements NotificationProvider {\n readonly channelId = \"cli\" as const;\n\n async send(\n payload: NotificationPayload,\n _config: NotificationConfig\n ): Promise<boolean> {\n const prefix = `[pai-notify:${payload.event}]`;\n const title = payload.title ? ` ${payload.title}:` : \"\";\n process.stderr.write(`${prefix}${title} ${payload.message}\\n`);\n return true;\n }\n}\n","/**\n * router.ts — Notification router\n *\n * Routes notification events to the appropriate channels based on the\n * current mode and per-event routing config.\n *\n * Channel providers are instantiated lazily and cached.\n */\n\nimport type {\n NotificationPayload,\n NotificationConfig,\n NotificationProvider,\n ChannelId,\n SendResult,\n NotificationMode,\n} from \"./types.js\";\nimport { NtfyProvider } from \"./providers/ntfy.js\";\nimport { WhatsAppProvider } from \"./providers/whatsapp.js\";\nimport { MacOsProvider } from \"./providers/macos.js\";\nimport { CliProvider } from \"./providers/cli.js\";\n\n// ---------------------------------------------------------------------------\n// Provider registry (singletons — stateless, safe to reuse)\n// ---------------------------------------------------------------------------\n\nconst PROVIDERS: Record<ChannelId, NotificationProvider> = {\n ntfy: new NtfyProvider(),\n whatsapp: new WhatsAppProvider(),\n macos: new MacOsProvider(),\n voice: new WhatsAppProvider(), // Voice uses WhatsApp TTS; handled in WhatsAppProvider\n cli: new CliProvider(),\n};\n\n// ---------------------------------------------------------------------------\n// Channel resolution\n// ---------------------------------------------------------------------------\n\n/**\n * Given the current config, resolve which channels should receive a\n * notification for the given event type.\n *\n * Mode overrides:\n * \"off\" → no channels\n * \"auto\" → use routing table, filtered by enabled channels\n * \"voice\" → whatsapp (TTS enabled in provider)\n * \"whatsapp\" → whatsapp\n * \"ntfy\" → ntfy\n * \"macos\" → macos\n * \"cli\" → cli\n */\nfunction resolveChannels(\n config: NotificationConfig,\n event: NotificationPayload[\"event\"]\n): ChannelId[] {\n const { mode, channels, routing } = config;\n\n if (mode === \"off\") return [];\n\n // Non-auto modes: force a single channel\n const modeToChannel: Partial<Record<NotificationMode, ChannelId>> = {\n voice: \"whatsapp\", // WhatsAppProvider checks mode === \"voice\" for TTS\n whatsapp: \"whatsapp\",\n ntfy: \"ntfy\",\n macos: \"macos\",\n cli: \"cli\",\n };\n\n if (mode !== \"auto\") {\n const ch = modeToChannel[mode];\n if (!ch) return [];\n // Check the channel is enabled\n const cfg = channels[ch];\n if (cfg && !cfg.enabled) return [ch]; // Still send — mode override bypasses enabled check\n return [ch];\n }\n\n // Auto mode: use routing table, filter to enabled channels\n const candidates = routing[event] ?? [];\n return candidates.filter((ch) => {\n const cfg = channels[ch];\n // \"voice\" channel is virtual — it overlaps with whatsapp.\n // Skip \"voice\" as an independent channel; voice is handled by checking config.mode.\n if (ch === \"voice\") return false;\n return cfg?.enabled === true;\n });\n}\n\n// ---------------------------------------------------------------------------\n// Router\n// ---------------------------------------------------------------------------\n\n/**\n * Route a notification to the appropriate channels.\n *\n * Sends to all resolved channels in parallel.\n * Individual channel failures are non-fatal and logged to stderr.\n *\n * @param payload The notification to send\n * @param config The current notification config (from daemon state)\n */\nexport async function routeNotification(\n payload: NotificationPayload,\n config: NotificationConfig\n): Promise<SendResult> {\n const channels = resolveChannels(config, payload.event);\n\n if (channels.length === 0) {\n return {\n channelsAttempted: [],\n channelsSucceeded: [],\n channelsFailed: [],\n mode: config.mode,\n };\n }\n\n const results = await Promise.allSettled(\n channels.map(async (ch) => {\n const provider = PROVIDERS[ch];\n const ok = await provider.send(payload, config);\n if (!ok) {\n process.stderr.write(\n `[pai-notify] Channel ${ch} failed for event ${payload.event}\\n`\n );\n }\n return { ch, ok };\n })\n );\n\n const succeeded: ChannelId[] = [];\n const failed: ChannelId[] = [];\n\n for (const r of results) {\n if (r.status === \"fulfilled\") {\n if (r.value.ok) {\n succeeded.push(r.value.ch);\n } else {\n failed.push(r.value.ch);\n }\n } else {\n // Provider threw — treat as failure\n failed.push(channels[results.indexOf(r)]);\n }\n }\n\n return {\n channelsAttempted: channels,\n channelsSucceeded: succeeded,\n channelsFailed: failed,\n mode: config.mode,\n };\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
- import { i as getRegistryBackend } from "./factory-DiuntvwG.mjs";
2
- import { D as resolveModelCapability, O as resolveProviderKey, T as readWorkersSection, h as classModelCapability, m as assertProviderRunnable, n as claudeCommand, p as WorkersConfigError, t as buildRunEnv, w as providerKeyPath, y as isModelCapability } from "./run-env-DyTWgZC9.mjs";
3
- import { _ as statusPath, d as resolveTarget, f as setCooldown, g as panesDir, h as ledgerPath, i as capabilityForRun, m as eventsPath, n as ensureProxyRunning, o as isQuotaFailure, p as ensureNoMcpConfig, s as nextAutoProvider, t as DEFAULT_PROXY_PORT, u as resolveCapabilityRun, v as workersLogDir } from "./server-CzqHGbKt.mjs";
1
+ import { i as getRegistryBackend } from "./factory-ChfAvCzO.mjs";
2
+ import { D as resolveModelCapability, O as resolveProviderKey, T as readWorkersSection, h as classModelCapability, m as assertProviderRunnable, n as claudeCommand, p as WorkersConfigError, t as buildRunEnv, w as providerKeyPath, y as isModelCapability } from "./run-env-kbWVwUvU.mjs";
3
+ import { _ as statusPath, d as resolveTarget, f as setCooldown, g as panesDir, h as ledgerPath, i as capabilityForRun, m as eventsPath, n as ensureProxyRunning, o as isQuotaFailure, p as ensureNoMcpConfig, s as nextAutoProvider, t as DEFAULT_PROXY_PORT, u as resolveCapabilityRun, v as workersLogDir } from "./server-B0vBmfJt.mjs";
4
4
  import { appendFileSync, closeSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
5
5
  import { homedir, tmpdir } from "node:os";
6
6
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
@@ -469,6 +469,10 @@ function elapsedOf(ts, now = /* @__PURE__ */ new Date()) {
469
469
  /** Shorter sleeps are legitimate polling — not worth a supervision event. */
470
470
  const SLEEP_FLOOR_SECS = 60;
471
471
  const SLEEP_RE = /^\s*sleep\s+([0-9]+(?:\.[0-9]+)?)([smhd]?)\s*(?:$|[;&|])/;
472
+ /** s/m/h/d suffix to seconds multiplier, shared by every `sleep N<unit>` parse below. */
473
+ function sleepUnitMultiplier(unit) {
474
+ return unit === "m" ? 60 : unit === "h" ? 3600 : unit === "d" ? 86400 : 1;
475
+ }
472
476
  /**
473
477
  * Seconds of a `sleep N` (N plain or with an s/m/h/d suffix) that stands alone
474
478
  * or leads a compound command (`sleep 150; cat f`, `sleep 2m && tail f`,
@@ -480,8 +484,7 @@ const SLEEP_RE = /^\s*sleep\s+([0-9]+(?:\.[0-9]+)?)([smhd]?)\s*(?:$|[;&|])/;
480
484
  function parseSleepSecs(command) {
481
485
  const m = command.match(SLEEP_RE);
482
486
  if (!m) return null;
483
- const mult = m[2] === "m" ? 60 : m[2] === "h" ? 3600 : m[2] === "d" ? 86400 : 1;
484
- const secs = Number(m[1]) * mult;
487
+ const secs = Number(m[1]) * sleepUnitMultiplier(m[2]);
485
488
  return Number.isFinite(secs) && secs >= SLEEP_FLOOR_SECS ? secs : null;
486
489
  }
487
490
  /** One-line description of a tool call, e.g. "Bash: npm test". */
@@ -1294,6 +1297,75 @@ function itermForClaudeSession(logDir, claudeSession, now = Date.now()) {
1294
1297
  return term ? itermUuid(term) : null;
1295
1298
  }
1296
1299
 
1300
+ //#endregion
1301
+ //#region src/workers/controls.ts
1302
+ /**
1303
+ * controls.ts — the clickr argv for handing desktop control to a worker or
1304
+ * taking it back, shared by `pai worker controls` and the follow pane's
1305
+ * "your controls" / "my controls" chat action, so both go through clickr the
1306
+ * same way.
1307
+ *
1308
+ * The grant is exclusive to one worker at a time: clickr's MCP server checks
1309
+ * PAI_WORKER_ID/pid against the `--agent <id> --pid <pid>` it was handed
1310
+ * control for. Crash paths are covered by clickr's own pid check; a clean
1311
+ * exit is covered by returnControlsIfHeld below, called from every exit path
1312
+ * of run.ts's runWorker.
1313
+ */
1314
+ /** clickr argv for `who`: `you` binds the grant to `id` (and its runner pid, when known); `me` has no target. */
1315
+ function clickrControlsArgv(id, who, pid) {
1316
+ if (who === "me") return ["controls", "me"];
1317
+ const argv = [
1318
+ "controls",
1319
+ "you",
1320
+ "--agent",
1321
+ id
1322
+ ];
1323
+ if (pid !== void 0) argv.push("--pid", String(pid));
1324
+ return argv;
1325
+ }
1326
+ /** clickr argv to hand a worker's grant back on its exit. */
1327
+ function clickrControlsReturnArgv(id) {
1328
+ return [
1329
+ "controls",
1330
+ "return",
1331
+ "--agent",
1332
+ id
1333
+ ];
1334
+ }
1335
+ /** Record that `id` was granted clickr desktop controls (`who: "you"` succeeded). */
1336
+ function markControlsHeld(logDir, id) {
1337
+ const status = loadStatus(logDir, id);
1338
+ if (!status) return;
1339
+ status.controlsHeld = true;
1340
+ saveStatus(logDir, status);
1341
+ }
1342
+ /**
1343
+ * Best-effort hand desktop controls back on a worker's exit, when it ever
1344
+ * held them. Never throws and never hangs past clickr's own exit or a 3s
1345
+ * cap — a missing clickr binary or a stuck process must not hold up the
1346
+ * worker's own exit.
1347
+ */
1348
+ async function returnControlsIfHeld(logDir, id) {
1349
+ if (!id) return;
1350
+ if (!loadStatus(logDir, id)?.controlsHeld) return;
1351
+ await new Promise((resolve) => {
1352
+ let done = false;
1353
+ const finish = () => {
1354
+ if (done) return;
1355
+ done = true;
1356
+ resolve();
1357
+ };
1358
+ try {
1359
+ const proc = spawn("clickr", clickrControlsReturnArgv(id), { stdio: "ignore" });
1360
+ proc.on("close", finish);
1361
+ proc.on("error", finish);
1362
+ } catch {
1363
+ finish();
1364
+ }
1365
+ setTimeout(finish, 3e3).unref();
1366
+ });
1367
+ }
1368
+
1297
1369
  //#endregion
1298
1370
  //#region src/workers/engines/image.ts
1299
1371
  /**
@@ -2428,9 +2500,11 @@ function fullContractPrompt(format) {
2428
2500
  "will pick something up later, never leave a background command as your final action. Finish, then",
2429
2501
  "report.",
2430
2502
  "No narration, no timestamps, no greetings, no summaries of what you read; act, verify, then stop.",
2431
- "Never sleep-poll: no `sleep N` over 60 seconds, standalone or leading a compound command.",
2432
- "Long builds and tests run in background with no arbitrary `timeout` — the task notification",
2433
- "wakes you; read the output then. Wait on a bounded timeout or event instead.",
2503
+ "Never sleep-poll: no `sleep N` over 60 seconds, anywhere in a command — standalone, after `&&`/`||`/",
2504
+ "`;`/`|`, or inside a loop or subshell. Ending your turn ends this process, so a background job gets",
2505
+ "no later wake-up here: run the long command in the FOREGROUND with a Bash timeout up to 600000 ms;",
2506
+ "if it can run longer than that, start it in the background and wait with a foreground loop that ends",
2507
+ "when the job ends: `while kill -0 <pid> 2>/dev/null; do sleep 15; done`.",
2434
2508
  ...fileNotInlineBlock(),
2435
2509
  "",
2436
2510
  `Operator messages: a user turn starting with ${OPERATOR_MARK} was typed by the operator while you`,
@@ -3041,7 +3115,7 @@ async function runWorker(opts) {
3041
3115
  const logDir = workersLogDir(config);
3042
3116
  mkdirSync(logDir, { recursive: true });
3043
3117
  if (opts.className === "plan" && !opts._planner) {
3044
- const { runPlanner } = await import("./planner-DsZZEZCA.mjs");
3118
+ const { runPlanner } = await import("./planner-DbpsMZqY.mjs");
3045
3119
  return runPlanner(opts);
3046
3120
  }
3047
3121
  const parent = launchParent(opts.parent);
@@ -3066,8 +3140,14 @@ async function runWorker(opts) {
3066
3140
  const label = opts.label ?? shortText(parsed.prompt ?? UNLABELED, 70);
3067
3141
  const model = capabilityResolution ? capabilityResolution.model : resolveRunModel(target, opts.className, opts.modelFlag);
3068
3142
  const reportFormat = resolveReportFormat(opts.reportFormatFlag);
3143
+ let workerId = opts.id;
3144
+ const trackStart = (wid) => {
3145
+ workerId = wid;
3146
+ opts.onWorkerStart?.(wid);
3147
+ };
3069
3148
  try {
3070
- if (target.provider.engine === "image") return await executeImageRun({
3149
+ let rc;
3150
+ if (target.provider.engine === "image") rc = await executeImageRun({
3071
3151
  config,
3072
3152
  logDir,
3073
3153
  target,
@@ -3083,10 +3163,10 @@ async function runWorker(opts) {
3083
3163
  parent: parent ?? void 0,
3084
3164
  stage: opts.stage,
3085
3165
  quiet: opts.quiet,
3086
- onWorkerStart: opts.onWorkerStart,
3166
+ onWorkerStart: trackStart,
3087
3167
  id: opts.id
3088
3168
  });
3089
- if (target.provider.engine === "codex") return await executeCodexRun({
3169
+ else if (target.provider.engine === "codex") rc = await executeCodexRun({
3090
3170
  config,
3091
3171
  logDir,
3092
3172
  target,
@@ -3100,14 +3180,14 @@ async function runWorker(opts) {
3100
3180
  parent: parent ?? void 0,
3101
3181
  stage: opts.stage,
3102
3182
  quiet: opts.quiet,
3103
- onWorkerStart: opts.onWorkerStart,
3183
+ onWorkerStart: trackStart,
3104
3184
  id: opts.id,
3105
3185
  worktreeFlag: opts.worktreeFlag,
3106
3186
  className: opts.className,
3107
3187
  reportFormat,
3108
3188
  capability: capability ?? void 0
3109
3189
  });
3110
- return await executeRun({
3190
+ else rc = await executeRun({
3111
3191
  config,
3112
3192
  logDir,
3113
3193
  target,
@@ -3122,7 +3202,7 @@ async function runWorker(opts) {
3122
3202
  parent: parent ?? void 0,
3123
3203
  stage: opts.stage,
3124
3204
  quiet: opts.quiet,
3125
- onWorkerStart: opts.onWorkerStart,
3205
+ onWorkerStart: trackStart,
3126
3206
  id: opts.id,
3127
3207
  capability: capability ?? void 0,
3128
3208
  worktreeFlag: opts.worktreeFlag,
@@ -3132,7 +3212,10 @@ async function runWorker(opts) {
3132
3212
  reportFormat,
3133
3213
  noReportRetry: opts.noReportRetry ?? false
3134
3214
  });
3215
+ await returnControlsIfHeld(logDir, workerId);
3216
+ return rc;
3135
3217
  } catch (e) {
3218
+ await returnControlsIfHeld(logDir, workerId);
3136
3219
  if (e instanceof Error && e.message.startsWith("key file")) throw new Error(`provider "${target.providerName}": ${e.message}\nPut the token in that file (chmod 600) or point keyFile elsewhere.`);
3137
3220
  throw e;
3138
3221
  }
@@ -4129,5 +4212,5 @@ function resultFromOutput(out) {
4129
4212
  }
4130
4213
 
4131
4214
  //#endregion
4132
- export { contextLabel as A, saveStatus as B, mergeWorker as C, UNLABELED as D, ledgerSummary as E, loadStatus as F, shortText as G, waitForTerminalStatus as H, loadStatuses as I, describeMcp as K, newWorkerId as L, elapsedOf as M, isChatPane as N, ageOf as O, isLive as P, nowStamp as R, discardWorker as S, appendLedger as T, longInlinePromptHint as U, setWorkerLabel as V, parseRunnerArgs as W, itermUuid as _, projectLaunchConfig as a, sessionTag as b, handoffFromInside as c, workerDepth as d, checkPaneForWorker as f, itermForClaudeSession as g, currentTabKey as h, usageContextTokens as i, contextPercent as j, alive as k, readInbox as l, openPaneForWorker as m, runWorker as n, parseWorkerReport as o, openFollowPane as p, expandMcpNames as q, testProvider as r, renderReport as s, printResult as t, sayToWorker as u, resolveSession as v, worktreesDir as w, workerInScope as x, sessionMapPath as y, ownsPid as z };
4133
- //# sourceMappingURL=run-XaA-UPdW.mjs.map
4215
+ export { UNLABELED as A, newWorkerId as B, workerInScope as C, worktreesDir as D, mergeWorker as E, elapsedOf as F, waitForTerminalStatus as G, ownsPid as H, isChatPane as I, shortText as J, longInlinePromptHint as K, isLive as L, alive as M, contextLabel as N, appendLedger as O, contextPercent as P, loadStatus as R, sessionTag as S, git as T, saveStatus as U, nowStamp as V, setWorkerLabel as W, expandMcpNames as X, describeMcp as Y, currentTabKey as _, projectLaunchConfig as a, resolveSession as b, handoffFromInside as c, workerDepth as d, checkPaneForWorker as f, markControlsHeld as g, clickrControlsArgv as h, usageContextTokens as i, ageOf as j, ledgerSummary as k, readInbox as l, openPaneForWorker as m, runWorker as n, parseWorkerReport as o, openFollowPane as p, parseRunnerArgs as q, testProvider as r, renderReport as s, printResult as t, sayToWorker as u, itermForClaudeSession as v, discardWorker as w, sessionMapPath as x, itermUuid as y, loadStatuses as z };
4216
+ //# sourceMappingURL=run-BFxuNnIJ.mjs.map