@xopcai/xopc 0.0.92 → 0.0.94

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 (194) hide show
  1. package/dist/browser-ext/manifest.json +1 -1
  2. package/dist/extensions/telegram/xopc.extension.json +1 -1
  3. package/dist/gateway/static/root/assets/agents-OqhbJkMf.js +222 -0
  4. package/dist/gateway/static/root/assets/apps-page-OHXW9XP8.js +1 -0
  5. package/dist/gateway/static/root/assets/channels-settings-4N2R-jof.js +1 -0
  6. package/dist/gateway/static/root/assets/{channels-status-swr-XzddfJW2.js → channels-status-swr-Bv6f9kDq.js} +1 -1
  7. package/dist/gateway/static/root/assets/{cron-api--I8LJ44S.js → cron-api-BtaQaHJq.js} +1 -1
  8. package/dist/gateway/static/root/assets/cron-page-Dah32HJK.js +1 -0
  9. package/dist/gateway/static/root/assets/{dist-CYgHMQO0.js → dist-BJfD9Qvs.js} +1 -1
  10. package/dist/gateway/static/root/assets/{extension-debug-page-6cRP0nA9.js → extension-debug-page-DnYuMzmH.js} +1 -1
  11. package/dist/gateway/static/root/assets/{extension-page-DpwIkspI.js → extension-page-CJfc-6XV.js} +1 -1
  12. package/dist/gateway/static/root/assets/{extension-settings-page-DYbnQUxH.js → extension-settings-page-BxdfYQMG.js} +1 -1
  13. package/dist/gateway/static/root/assets/{fetch-DTN0w7rV.js → fetch-B0aeeY0q.js} +1 -1
  14. package/dist/gateway/static/root/assets/{field-primitives-CslW6HwD.js → field-primitives-DOLHwowi.js} +1 -1
  15. package/dist/gateway/static/root/assets/{heartbeat-config-api-2UiKevxG.js → heartbeat-config-api-Bj2INAf5.js} +1 -1
  16. package/dist/gateway/static/root/assets/index-Bj_l8QDp.css +1 -0
  17. package/dist/gateway/static/root/assets/{index-DnevRVa6.js → index-DuQ1XPoA.js} +99 -98
  18. package/dist/gateway/static/root/assets/logs-page-AsOgLNJE.js +2 -0
  19. package/dist/gateway/static/root/assets/{note-detail-page-DvW2qg4i.js → note-detail-page-24J4mVP-.js} +53 -53
  20. package/dist/gateway/static/root/assets/{note-time-BEiibLJv.js → note-time-JBszYV3s.js} +1 -1
  21. package/dist/gateway/static/root/assets/notes-page-BApAirFB.js +1 -0
  22. package/dist/gateway/static/root/assets/sessions-page-DX9huWsA.js +1 -0
  23. package/dist/gateway/static/root/assets/{settings-advanced-gate-BctKqHcf.js → settings-advanced-gate-DWvhsTuz.js} +1 -1
  24. package/dist/gateway/static/root/assets/{settings-form-section-QJh5ruel.js → settings-form-section-CxMjaMiy.js} +1 -1
  25. package/dist/gateway/static/root/assets/settings-page-4VmUTzQs.js +3 -0
  26. package/dist/gateway/static/root/assets/{share-preview-page-DBsvvbmD.js → share-preview-page-IX0TJvRd.js} +1 -1
  27. package/dist/gateway/static/root/assets/skills-page-CGKGKfwe.js +2 -0
  28. package/dist/gateway/static/root/assets/{theme-store-ht5iswWS.js → theme-store-Cg_SuBw0.js} +1 -1
  29. package/dist/gateway/static/root/assets/url-BHHmdJYc.js +3 -0
  30. package/dist/gateway/static/root/assets/{utils-DhPv9xoB.js → utils-BmlcxR2j.js} +1 -1
  31. package/dist/gateway/static/root/assets/voice-api-key-field-DaGm2N4J.js +1 -0
  32. package/dist/gateway/static/root/assets/{workflow-page.utils-CJqnPWkW.js → workflow-page.utils-D0vsIGHD.js} +1 -1
  33. package/dist/gateway/static/root/assets/workflows-page-BFCrD3nw.js +27 -0
  34. package/dist/gateway/static/root/index.html +5 -5
  35. package/dist/package.js +1 -1
  36. package/dist/src/agent/inbound/turn-dispatcher.d.ts +1 -0
  37. package/dist/src/agent/inbound/turn-dispatcher.js +3 -0
  38. package/dist/src/agent/inbound/turn-dispatcher.js.map +1 -1
  39. package/dist/src/agent/lifecycle/handlers/compaction.js +1 -1
  40. package/dist/src/agent/lifecycle/handlers/compaction.js.map +1 -1
  41. package/dist/src/agent/mcp/bundle-mcp-materialize.js +1 -1
  42. package/dist/src/agent/mcp/bundle-mcp-materialize.js.map +1 -1
  43. package/dist/src/agent/mcp/bundle-mcp-runtime.js +17 -4
  44. package/dist/src/agent/mcp/bundle-mcp-runtime.js.map +1 -1
  45. package/dist/src/agent/mcp/mcp-transport-config.js +10 -3
  46. package/dist/src/agent/mcp/mcp-transport-config.js.map +1 -1
  47. package/dist/src/agent/mcp/mcp-transport.js +1 -1
  48. package/dist/src/agent/mcp/mcp-transport.js.map +1 -1
  49. package/dist/src/agent/service/process-direct-streaming.d.ts +1 -0
  50. package/dist/src/agent/service/process-direct-streaming.js +15 -12
  51. package/dist/src/agent/service/process-direct-streaming.js.map +1 -1
  52. package/dist/src/agent/service.d.ts +4 -2
  53. package/dist/src/agent/service.js +20 -4
  54. package/dist/src/agent/service.js.map +1 -1
  55. package/dist/src/agent/service.types.d.ts +3 -1
  56. package/dist/src/agent/tools/browser/tool/browser-use-tool.js +1 -1
  57. package/dist/src/agent/tools/browser/tool/browser-use-tool.js.map +1 -1
  58. package/dist/src/agent/tools/search/registry.js +1 -1
  59. package/dist/src/agent/tools/search/registry.js.map +1 -1
  60. package/dist/src/agent/tools/session-search-tool.js +1 -1
  61. package/dist/src/agent/tools/session-search-tool.js.map +1 -1
  62. package/dist/src/agent/tools/workflow-tool.js +1 -1
  63. package/dist/src/agent/tools/workflow-tool.js.map +1 -1
  64. package/dist/src/agent/workflow/progress-broker.js +1 -1
  65. package/dist/src/agent/workflow/progress-broker.js.map +1 -1
  66. package/dist/src/agent/workflow/subagent-runner.js +1 -1
  67. package/dist/src/agent/workflow/subagent-runner.js.map +1 -1
  68. package/dist/src/channels/pipeline.js +3 -2
  69. package/dist/src/channels/pipeline.js.map +1 -1
  70. package/dist/src/cli/cli-log-level-preset.d.ts +1 -1
  71. package/dist/src/cli/cli-log-level-preset.js +2 -2
  72. package/dist/src/cli/cli-log-level-preset.js.map +1 -1
  73. package/dist/src/cli/commands/logs.js +3 -3
  74. package/dist/src/cli/commands/logs.js.map +1 -1
  75. package/dist/src/cron/executor.js +7 -4
  76. package/dist/src/cron/executor.js.map +1 -1
  77. package/dist/src/gateway/hono/app.js +4 -1
  78. package/dist/src/gateway/hono/app.js.map +1 -1
  79. package/dist/src/gateway/hono/lib/route-logger.d.ts +6 -0
  80. package/dist/src/gateway/hono/lib/route-logger.js +31 -0
  81. package/dist/src/gateway/hono/lib/route-logger.js.map +1 -0
  82. package/dist/src/gateway/hono/middleware/auth.js +16 -3
  83. package/dist/src/gateway/hono/middleware/auth.js.map +1 -1
  84. package/dist/src/gateway/hono/middleware/logger.js +1 -1
  85. package/dist/src/gateway/hono/middleware/logger.js.map +1 -1
  86. package/dist/src/gateway/hono/middleware/route-errors.d.ts +5 -0
  87. package/dist/src/gateway/hono/middleware/route-errors.js +27 -0
  88. package/dist/src/gateway/hono/middleware/route-errors.js.map +1 -0
  89. package/dist/src/gateway/hono/routes/agent-stream.js +6 -0
  90. package/dist/src/gateway/hono/routes/agent-stream.js.map +1 -1
  91. package/dist/src/gateway/hono/routes/browser-install.js +2 -4
  92. package/dist/src/gateway/hono/routes/browser-install.js.map +1 -1
  93. package/dist/src/gateway/hono/routes/config.js +25 -11
  94. package/dist/src/gateway/hono/routes/config.js.map +1 -1
  95. package/dist/src/gateway/hono/routes/cron.js +5 -0
  96. package/dist/src/gateway/hono/routes/cron.js.map +1 -1
  97. package/dist/src/gateway/hono/routes/host-fs.js +2 -4
  98. package/dist/src/gateway/hono/routes/host-fs.js.map +1 -1
  99. package/dist/src/gateway/hono/routes/lazy-bundles.js +14 -1
  100. package/dist/src/gateway/hono/routes/lazy-bundles.js.map +1 -1
  101. package/dist/src/gateway/hono/routes/lazy-fallback.js +3 -0
  102. package/dist/src/gateway/hono/routes/lazy-fallback.js.map +1 -1
  103. package/dist/src/gateway/hono/routes/logs.js +39 -7
  104. package/dist/src/gateway/hono/routes/logs.js.map +1 -1
  105. package/dist/src/gateway/hono/routes/mcp.d.ts +3 -0
  106. package/dist/src/gateway/hono/routes/mcp.js +107 -0
  107. package/dist/src/gateway/hono/routes/mcp.js.map +1 -0
  108. package/dist/src/gateway/hono/routes/notes.js +105 -1
  109. package/dist/src/gateway/hono/routes/notes.js.map +1 -1
  110. package/dist/src/gateway/hono/routes/sessions.js +6 -0
  111. package/dist/src/gateway/hono/routes/sessions.js.map +1 -1
  112. package/dist/src/gateway/hono/routes/update.js +2 -4
  113. package/dist/src/gateway/hono/routes/update.js.map +1 -1
  114. package/dist/src/gateway/hono/routes/voice.js +2 -4
  115. package/dist/src/gateway/hono/routes/voice.js.map +1 -1
  116. package/dist/src/gateway/hono/routes/workspace.js +2 -4
  117. package/dist/src/gateway/hono/routes/workspace.js.map +1 -1
  118. package/dist/src/gateway/hono/sse.js +9 -2
  119. package/dist/src/gateway/hono/sse.js.map +1 -1
  120. package/dist/src/gateway/host.d.ts +2 -0
  121. package/dist/src/gateway/host.js +6 -3
  122. package/dist/src/gateway/host.js.map +1 -1
  123. package/dist/src/gateway/service/agent-runner.js +1 -1
  124. package/dist/src/gateway/service/agent-runner.js.map +1 -1
  125. package/dist/src/gateway/service/config-coordinator.js +14 -6
  126. package/dist/src/gateway/service/config-coordinator.js.map +1 -1
  127. package/dist/src/gateway/service/marketplace-service.js +1 -1
  128. package/dist/src/gateway/service/marketplace-service.js.map +1 -1
  129. package/dist/src/gateway/service/run-gateway-agent.js +22 -5
  130. package/dist/src/gateway/service/run-gateway-agent.js.map +1 -1
  131. package/dist/src/gateway/service/sse-hub.js +1 -1
  132. package/dist/src/gateway/service/sse-hub.js.map +1 -1
  133. package/dist/src/gateway/service.js +12 -5
  134. package/dist/src/gateway/service.js.map +1 -1
  135. package/dist/src/mcp/channel-bridge.js +26 -2
  136. package/dist/src/mcp/channel-bridge.js.map +1 -1
  137. package/dist/src/mcp/gateway-http-client.js +24 -2
  138. package/dist/src/mcp/gateway-http-client.js.map +1 -1
  139. package/dist/src/notes/service.d.ts +13 -1
  140. package/dist/src/notes/service.js +237 -0
  141. package/dist/src/notes/service.js.map +1 -1
  142. package/dist/src/notes/store.d.ts +3 -0
  143. package/dist/src/notes/store.js +6 -2
  144. package/dist/src/notes/store.js.map +1 -1
  145. package/dist/src/notes/types.d.ts +31 -0
  146. package/dist/src/session/config-store.js +10 -4
  147. package/dist/src/session/config-store.js.map +1 -1
  148. package/dist/src/session/index.d.ts +1 -1
  149. package/dist/src/session/index.js +2 -2
  150. package/dist/src/session/manager.js +8 -1
  151. package/dist/src/session/manager.js.map +1 -1
  152. package/dist/src/session/session-title.d.ts +19 -3
  153. package/dist/src/session/session-title.js +82 -7
  154. package/dist/src/session/session-title.js.map +1 -1
  155. package/dist/src/utils/index.js +4 -4
  156. package/dist/src/utils/logger/config.js +2 -6
  157. package/dist/src/utils/logger/config.js.map +1 -1
  158. package/dist/src/utils/logger/context.d.ts +3 -22
  159. package/dist/src/utils/logger/context.js +4 -32
  160. package/dist/src/utils/logger/context.js.map +1 -1
  161. package/dist/src/utils/logger/index.d.ts +4 -7
  162. package/dist/src/utils/logger/index.js +9 -28
  163. package/dist/src/utils/logger/index.js.map +1 -1
  164. package/dist/src/utils/logger/log-store.d.ts +14 -32
  165. package/dist/src/utils/logger/log-store.js +67 -118
  166. package/dist/src/utils/logger/log-store.js.map +1 -1
  167. package/dist/src/utils/logger/log-stream.d.ts +5 -70
  168. package/dist/src/utils/logger/log-stream.js +67 -178
  169. package/dist/src/utils/logger/log-stream.js.map +1 -1
  170. package/dist/src/utils/logger/pino-record.d.ts +8 -0
  171. package/dist/src/utils/logger/pino-record.js +83 -0
  172. package/dist/src/utils/logger/pino-record.js.map +1 -0
  173. package/dist/src/utils/logger/stats.d.ts +1 -1
  174. package/dist/src/utils/logger/stats.js +2 -2
  175. package/dist/src/utils/logger/stats.js.map +1 -1
  176. package/dist/src/utils/logger/streams.js +18 -0
  177. package/dist/src/utils/logger/streams.js.map +1 -1
  178. package/dist/src/utils/logger/types.d.ts +0 -9
  179. package/dist/src/utils/logger/types.js.map +1 -1
  180. package/dist/src/utils/logger.js +4 -4
  181. package/package.json +6 -1
  182. package/dist/gateway/static/root/assets/agents-uwPn7ZW9.js +0 -222
  183. package/dist/gateway/static/root/assets/apps-page-CWKdhSPU.js +0 -1
  184. package/dist/gateway/static/root/assets/channels-settings-hEhW7Mbk.js +0 -1
  185. package/dist/gateway/static/root/assets/cron-page-B0kvgZGR.js +0 -1
  186. package/dist/gateway/static/root/assets/index-BUKUv7QW.css +0 -1
  187. package/dist/gateway/static/root/assets/logs-page-sOP4TXJ4.js +0 -1
  188. package/dist/gateway/static/root/assets/notes-page-BFQaquHU.js +0 -1
  189. package/dist/gateway/static/root/assets/sessions-page-CptjDKAX.js +0 -1
  190. package/dist/gateway/static/root/assets/settings-page-V3p-hISB.js +0 -2
  191. package/dist/gateway/static/root/assets/skills-page-q2zPUJAR.js +0 -2
  192. package/dist/gateway/static/root/assets/url-CWWpfkq1.js +0 -3
  193. package/dist/gateway/static/root/assets/voice-api-key-field-DLSKUipa.js +0 -1
  194. package/dist/gateway/static/root/assets/workflows-page-DRRQ1A0l.js +0 -27
@@ -1 +1 @@
1
- {"version":3,"file":"progress-broker.js","names":[],"sources":["../../../../src/agent/workflow/progress-broker.ts"],"sourcesContent":["/**\n * WorkflowProgressBroker — the single seam that turns mid-run `workflow`\n * snapshots into IM messages (Telegram today, more channels later).\n *\n * Architecture:\n *\n * pi-agent ──tool_execution_update──▶ AgentEventHandler / SessionEventBus\n * │\n * attachTo(handler) │\n * ▼\n * WorkflowProgressBroker\n * │\n * per-(sessionKey, toolCallId) state\n * │\n * ┌───────────────────┼───────────────────┐\n * ▼ ▼ ▼\n * Telegram cap Feishu cap WeChat cap\n *\n * Why broker + capability instead of \"each channel subscribes the bus\"?\n * - DRY snapshot aggregation and key-event detection.\n * - Per-channel throttling is enforced by the broker, so a slow / rate-limited\n * channel can't block a fast one.\n * - Adding a new channel = one capability + one register call. Broker code\n * never grows.\n */\n\nimport type { AgentEvent } from '@earendil-works/pi-agent-core';\n\nimport { createLogger } from '../../utils/logger.js';\nimport type { Config } from '../../config/schema.js';\n\nimport type {\n ChannelProgressCapability,\n WorkflowProgressMode,\n} from './channel-capability.js';\nimport { renderWorkflowText } from './snapshot.js';\nimport type { WorkflowAgentSnapshot, WorkflowSnapshot } from './types.js';\n\nconst log = createLogger('workflow-progress-broker');\n\nconst WORKFLOW_TOOL_NAME = 'workflow';\nconst RENDER_MAX_AGENTS_PER_PHASE = 4;\nconst RENDER_MAX_LOGS = 2;\n\n/** Per-channel resolved settings after applying config overrides on capability defaults. */\ninterface ChannelResolvedSettings {\n enabled: boolean;\n throttleMs: number;\n mode: WorkflowProgressMode;\n}\n\n/** Per-(sessionKey, toolCallId) progress state. */\ninterface RunState {\n /** Latest snapshot seen for this run. Always overwritten on update. */\n snapshot: WorkflowSnapshot;\n /** Previous snapshot used to detect \"key events\" (phase change, new errors). */\n prevSnapshot?: WorkflowSnapshot;\n /** Per-channel last-send bookkeeping. */\n perChannel: Map<string, ChannelRunState>;\n}\n\ninterface ChannelRunState {\n /** Server timestamp of the last successful postProgress. */\n lastSentAt: number;\n /** Returned messageId of the last successful postProgress — drives edit mode. */\n lastMessageId?: string;\n /** Pending timer id (Node `setTimeout`) when a throttled flush is scheduled. */\n pendingTimer?: ReturnType<typeof setTimeout>;\n /** Inflight `postProgress` promise; we serialise per-(state, channel) to avoid race. */\n inflight?: Promise<void>;\n}\n\nexport interface BrokerListenerHandle {\n /** Detach broker from the session bus and clear all in-flight state. */\n dispose(): void;\n}\n\n/**\n * Tiny façade onto the AgentEventHandler. We don't import the concrete class to\n * keep this module test-friendly — a stub listener pump is fine for unit tests.\n */\nexport interface SessionBusLike {\n registerListener(\n type: AgentEvent['type'] | 'all',\n listener: (event: AgentEvent, context: { sessionKey: string }) => void,\n ): () => void;\n}\n\nexport class WorkflowProgressBroker {\n private subscribers: ChannelProgressCapability[] = [];\n private states = new Map<string, RunState>();\n /** Now() factory — overridable in tests for deterministic time. */\n private readonly now: () => number;\n /** Cached resolved settings per (channelId), invalidated on registration. */\n private resolved = new Map<string, ChannelResolvedSettings>();\n\n constructor(\n private readonly opts: {\n getConfig?: () => Config | undefined;\n now?: () => number;\n } = {},\n ) {\n this.now = opts.now ?? (() => Date.now());\n }\n\n // ── Registration ────────────────────────────────────────────────────────────\n\n registerChannel(cap: ChannelProgressCapability): () => void {\n if (this.subscribers.some((s) => s.channelId === cap.channelId)) {\n log.warn({ channelId: cap.channelId }, 'channel capability already registered; replacing');\n this.subscribers = this.subscribers.filter((s) => s.channelId !== cap.channelId);\n }\n this.subscribers.push(cap);\n this.resolved.delete(cap.channelId);\n return () => {\n this.subscribers = this.subscribers.filter((s) => s !== cap);\n this.resolved.delete(cap.channelId);\n };\n }\n\n attachTo(bus: SessionBusLike): BrokerListenerHandle {\n const offUpdate = bus.registerListener('tool_execution_update', (event, ctx) => {\n const e = event as Extract<AgentEvent, { type: 'tool_execution_update' }>;\n if (e.toolName !== WORKFLOW_TOOL_NAME) return;\n const snap = extractWorkflowSnapshot(e.partialResult);\n if (!snap) return;\n this.onUpdate(ctx.sessionKey, e.toolCallId, snap);\n });\n\n const offEnd = bus.registerListener('tool_execution_end', (event, ctx) => {\n const e = event as Extract<AgentEvent, { type: 'tool_execution_end' }>;\n if (e.toolName !== WORKFLOW_TOOL_NAME) return;\n // tool_end ships the final envelope in `result`; reach in for the\n // authoritative snapshot (durationMs / result / final counts).\n const snap = extractWorkflowSnapshot(e.result, { fromResultEnvelope: true });\n this.onEnd(ctx.sessionKey, e.toolCallId, snap);\n });\n\n return {\n dispose: () => {\n offUpdate();\n offEnd();\n this.disposeAllPending();\n },\n };\n }\n\n // ── Core state machine ──────────────────────────────────────────────────────\n\n /** Visible for tests — direct entry path bypassing the SessionBus glue. */\n onUpdate(sessionKey: string, toolCallId: string, snapshot: WorkflowSnapshot): void {\n const key = stateKey(sessionKey, toolCallId);\n const state = this.getOrCreateState(key, snapshot);\n state.prevSnapshot = state.snapshot;\n state.snapshot = snapshot;\n\n const isKey = isKeyEvent(state.prevSnapshot, snapshot);\n for (const cap of this.subscribers) {\n this.dispatchToChannel(state, sessionKey, cap, { isFinal: false, isKey });\n }\n }\n\n /** Visible for tests — direct entry path bypassing the SessionBus glue. */\n onEnd(sessionKey: string, toolCallId: string, snapshot: WorkflowSnapshot | null): void {\n const key = stateKey(sessionKey, toolCallId);\n const state = this.states.get(key);\n if (!state) return;\n if (snapshot) state.snapshot = snapshot;\n\n for (const cap of this.subscribers) {\n // Always flush the final message — bypass throttle and any pending timer.\n this.cancelPending(state, cap.channelId);\n this.dispatchToChannel(state, sessionKey, cap, { isFinal: true, isKey: true });\n }\n // State is GC'd lazily after a small grace period so any straggler\n // `update` event arriving after `end` is silently dropped (instead of\n // resurrecting the run).\n setTimeout(() => this.states.delete(key), 2_000);\n }\n\n // ── Dispatch + throttle ─────────────────────────────────────────────────────\n\n private dispatchToChannel(\n state: RunState,\n sessionKey: string,\n cap: ChannelProgressCapability,\n flags: { isFinal: boolean; isKey: boolean },\n ): void {\n const cfg = this.resolveChannelSettings(cap);\n if (!cfg.enabled) return;\n if (cfg.mode === 'final-only' && !flags.isFinal) return;\n\n const chState = this.getOrCreateChannelState(state, cap.channelId);\n\n // Key events and the final message bypass throttle.\n if (flags.isFinal || flags.isKey) {\n this.cancelPending(state, cap.channelId);\n void this.sendNow(state, sessionKey, cap, cfg, flags.isFinal);\n return;\n }\n\n const elapsed = this.now() - chState.lastSentAt;\n const wait = Math.max(0, cfg.throttleMs - elapsed);\n if (wait === 0) {\n void this.sendNow(state, sessionKey, cap, cfg, false);\n return;\n }\n if (chState.pendingTimer) return; // already scheduled; latest snapshot will be picked up\n chState.pendingTimer = setTimeout(() => {\n chState.pendingTimer = undefined;\n void this.sendNow(state, sessionKey, cap, cfg, false);\n }, wait);\n }\n\n private async sendNow(\n state: RunState,\n sessionKey: string,\n cap: ChannelProgressCapability,\n cfg: ChannelResolvedSettings,\n isFinal: boolean,\n ): Promise<void> {\n const chState = this.getOrCreateChannelState(state, cap.channelId);\n // Serialise per-channel sends so a slow editMessage call doesn't get\n // overtaken by a faster one and leave the bubble out of order.\n if (chState.inflight) await chState.inflight.catch(() => undefined);\n\n const text = renderWorkflowText(state.snapshot, isFinal, {\n maxAgentsPerPhase: RENDER_MAX_AGENTS_PER_PHASE,\n maxLogs: RENDER_MAX_LOGS,\n showResultPreviews: isFinal,\n });\n\n const previousMessageId = cfg.mode === 'edit' ? chState.lastMessageId : undefined;\n const task = cap\n .postProgress({ sessionKey, text, previousMessageId, isFinal, mode: cfg.mode })\n .then((r) => {\n chState.lastMessageId = r.messageId;\n chState.lastSentAt = this.now();\n })\n .catch((err) => {\n const msg = err instanceof Error ? err.message : String(err);\n log.warn(\n { err, errorMessage: msg, channelId: cap.channelId, sessionKey },\n `workflow progress postProgress failed: ${msg}`,\n );\n })\n .finally(() => {\n chState.inflight = undefined;\n });\n\n chState.inflight = task;\n await task;\n }\n\n private cancelPending(state: RunState, channelId: string): void {\n const chState = state.perChannel.get(channelId);\n if (chState?.pendingTimer) {\n clearTimeout(chState.pendingTimer);\n chState.pendingTimer = undefined;\n }\n }\n\n private disposeAllPending(): void {\n for (const state of this.states.values()) {\n for (const ch of state.perChannel.values()) {\n if (ch.pendingTimer) clearTimeout(ch.pendingTimer);\n }\n }\n this.states.clear();\n }\n\n // ── State helpers ───────────────────────────────────────────────────────────\n\n private getOrCreateState(key: string, snapshot: WorkflowSnapshot): RunState {\n let state = this.states.get(key);\n if (!state) {\n state = { snapshot, perChannel: new Map() };\n this.states.set(key, state);\n }\n return state;\n }\n\n private getOrCreateChannelState(state: RunState, channelId: string): ChannelRunState {\n let ch = state.perChannel.get(channelId);\n if (!ch) {\n ch = { lastSentAt: 0 };\n state.perChannel.set(channelId, ch);\n }\n return ch;\n }\n\n /** Resolved (enabled / throttleMs / mode) for a channel, with config overrides. */\n private resolveChannelSettings(cap: ChannelProgressCapability): ChannelResolvedSettings {\n const cached = this.resolved.get(cap.channelId);\n if (cached) return cached;\n const override = readChannelConfig(this.opts.getConfig?.(), cap.channelId);\n const resolved: ChannelResolvedSettings = {\n enabled: override?.enabled ?? true,\n throttleMs: override?.throttleMs ?? cap.defaultThrottleMs,\n mode: override?.mode ?? cap.defaultMode,\n };\n this.resolved.set(cap.channelId, resolved);\n return resolved;\n }\n\n /** Drop any cached config so the next dispatch re-reads. Call after config reload. */\n invalidateConfigCache(): void {\n this.resolved.clear();\n }\n\n // ── Test introspection ──────────────────────────────────────────────────────\n\n /** @internal — for tests only. */\n _stateCount(): number {\n return this.states.size;\n }\n}\n\n// ── Singleton ───────────────────────────────────────────────────────────────\n\nlet singleton: WorkflowProgressBroker | null = null;\n\n/**\n * Process-wide broker singleton. Channels register against this one; the\n * service wires it to the session bus during startup.\n */\nexport function getWorkflowProgressBroker(): WorkflowProgressBroker {\n if (!singleton) singleton = new WorkflowProgressBroker();\n return singleton;\n}\n\n/** Test-only — reset the singleton between cases. */\nexport function _resetWorkflowProgressBrokerForTests(): void {\n singleton = null;\n}\n\n// ── Pure helpers ────────────────────────────────────────────────────────────\n\nfunction stateKey(sessionKey: string, toolCallId: string | undefined): string {\n return `${sessionKey}\u0001${toolCallId ?? ''}`;\n}\n\n/**\n * Compare two snapshots and decide whether the new one is \"key\" — i.e. worth\n * bypassing the per-channel throttle. Anything visible to the user as a\n * progress milestone qualifies; counts ticking by alone do not.\n */\nfunction isKeyEvent(prev: WorkflowSnapshot | undefined, next: WorkflowSnapshot): boolean {\n if (!prev) return true; // first update of the run\n if (prev.currentPhase !== next.currentPhase) return true;\n if (next.errorCount > prev.errorCount) return true;\n if (next.skippedCount > prev.skippedCount) return true;\n // New phase row in the rollup (declared via `phase(...)` mid-run)\n if (prev.phases.length !== next.phases.length) return true;\n if (hasNewFailedAgent(prev.agents, next.agents)) return true;\n return false;\n}\n\nfunction hasNewFailedAgent(\n prev: WorkflowAgentSnapshot[],\n next: WorkflowAgentSnapshot[],\n): boolean {\n const prevBad = new Set(\n prev.filter((a) => a.status === 'error' || a.status === 'skipped').map((a) => a.id),\n );\n for (const a of next) {\n if ((a.status === 'error' || a.status === 'skipped') && !prevBad.has(a.id)) return true;\n }\n return false;\n}\n\n/**\n * Pull a {@link WorkflowSnapshot} out of an AgentToolResult-shaped value.\n * Returns null when the payload is not snapshot-shaped (text-only updates,\n * non-workflow tools, etc.).\n *\n * `fromResultEnvelope = true` (used for `tool_end.result`) tolerates the\n * `{ content, details }` wrapper.\n */\nfunction extractWorkflowSnapshot(\n payload: unknown,\n opts: { fromResultEnvelope?: boolean } = {},\n): WorkflowSnapshot | null {\n if (!payload || typeof payload !== 'object') return null;\n const rec = payload as Record<string, unknown>;\n if (opts.fromResultEnvelope) {\n const details = rec.details;\n if (details && typeof details === 'object') return coerce(details);\n return null;\n }\n if ('details' in rec) {\n const details = rec.details;\n if (details && typeof details === 'object') return coerce(details);\n return null;\n }\n return coerce(rec);\n}\n\nfunction coerce(value: unknown): WorkflowSnapshot | null {\n if (!value || typeof value !== 'object') return null;\n const rec = value as Record<string, unknown>;\n if (typeof rec.name !== 'string') return null;\n if (!Array.isArray(rec.agents)) return null;\n return value as WorkflowSnapshot;\n}\n\nfunction readChannelConfig(\n config: Config | undefined,\n channelId: string,\n): { enabled?: boolean; throttleMs?: number; mode?: WorkflowProgressMode } | undefined {\n const channels = config?.channels as Record<string, unknown> | undefined;\n if (!channels) return undefined;\n const cfg = channels[channelId];\n if (!cfg || typeof cfg !== 'object') return undefined;\n const wf = (cfg as { workflowProgress?: unknown }).workflowProgress;\n if (!wf || typeof wf !== 'object') return undefined;\n return wf as { enabled?: boolean; throttleMs?: number; mode?: WorkflowProgressMode };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"progress-broker.js","names":[],"sources":["../../../../src/agent/workflow/progress-broker.ts"],"sourcesContent":["/**\n * WorkflowProgressBroker — the single seam that turns mid-run `workflow`\n * snapshots into IM messages (Telegram today, more channels later).\n *\n * Architecture:\n *\n * pi-agent ──tool_execution_update──▶ AgentEventHandler / SessionEventBus\n * │\n * attachTo(handler) │\n * ▼\n * WorkflowProgressBroker\n * │\n * per-(sessionKey, toolCallId) state\n * │\n * ┌───────────────────┼───────────────────┐\n * ▼ ▼ ▼\n * Telegram cap Feishu cap WeChat cap\n *\n * Why broker + capability instead of \"each channel subscribes the bus\"?\n * - DRY snapshot aggregation and key-event detection.\n * - Per-channel throttling is enforced by the broker, so a slow / rate-limited\n * channel can't block a fast one.\n * - Adding a new channel = one capability + one register call. Broker code\n * never grows.\n */\n\nimport type { AgentEvent } from '@earendil-works/pi-agent-core';\n\nimport { createLogger } from '../../utils/logger.js';\nimport type { Config } from '../../config/schema.js';\n\nimport type {\n ChannelProgressCapability,\n WorkflowProgressMode,\n} from './channel-capability.js';\nimport { renderWorkflowText } from './snapshot.js';\nimport type { WorkflowAgentSnapshot, WorkflowSnapshot } from './types.js';\n\nconst log = createLogger('Agent:WorkflowProgress');\n\nconst WORKFLOW_TOOL_NAME = 'workflow';\nconst RENDER_MAX_AGENTS_PER_PHASE = 4;\nconst RENDER_MAX_LOGS = 2;\n\n/** Per-channel resolved settings after applying config overrides on capability defaults. */\ninterface ChannelResolvedSettings {\n enabled: boolean;\n throttleMs: number;\n mode: WorkflowProgressMode;\n}\n\n/** Per-(sessionKey, toolCallId) progress state. */\ninterface RunState {\n /** Latest snapshot seen for this run. Always overwritten on update. */\n snapshot: WorkflowSnapshot;\n /** Previous snapshot used to detect \"key events\" (phase change, new errors). */\n prevSnapshot?: WorkflowSnapshot;\n /** Per-channel last-send bookkeeping. */\n perChannel: Map<string, ChannelRunState>;\n}\n\ninterface ChannelRunState {\n /** Server timestamp of the last successful postProgress. */\n lastSentAt: number;\n /** Returned messageId of the last successful postProgress — drives edit mode. */\n lastMessageId?: string;\n /** Pending timer id (Node `setTimeout`) when a throttled flush is scheduled. */\n pendingTimer?: ReturnType<typeof setTimeout>;\n /** Inflight `postProgress` promise; we serialise per-(state, channel) to avoid race. */\n inflight?: Promise<void>;\n}\n\nexport interface BrokerListenerHandle {\n /** Detach broker from the session bus and clear all in-flight state. */\n dispose(): void;\n}\n\n/**\n * Tiny façade onto the AgentEventHandler. We don't import the concrete class to\n * keep this module test-friendly — a stub listener pump is fine for unit tests.\n */\nexport interface SessionBusLike {\n registerListener(\n type: AgentEvent['type'] | 'all',\n listener: (event: AgentEvent, context: { sessionKey: string }) => void,\n ): () => void;\n}\n\nexport class WorkflowProgressBroker {\n private subscribers: ChannelProgressCapability[] = [];\n private states = new Map<string, RunState>();\n /** Now() factory — overridable in tests for deterministic time. */\n private readonly now: () => number;\n /** Cached resolved settings per (channelId), invalidated on registration. */\n private resolved = new Map<string, ChannelResolvedSettings>();\n\n constructor(\n private readonly opts: {\n getConfig?: () => Config | undefined;\n now?: () => number;\n } = {},\n ) {\n this.now = opts.now ?? (() => Date.now());\n }\n\n // ── Registration ────────────────────────────────────────────────────────────\n\n registerChannel(cap: ChannelProgressCapability): () => void {\n if (this.subscribers.some((s) => s.channelId === cap.channelId)) {\n log.warn({ channelId: cap.channelId }, 'channel capability already registered; replacing');\n this.subscribers = this.subscribers.filter((s) => s.channelId !== cap.channelId);\n }\n this.subscribers.push(cap);\n this.resolved.delete(cap.channelId);\n return () => {\n this.subscribers = this.subscribers.filter((s) => s !== cap);\n this.resolved.delete(cap.channelId);\n };\n }\n\n attachTo(bus: SessionBusLike): BrokerListenerHandle {\n const offUpdate = bus.registerListener('tool_execution_update', (event, ctx) => {\n const e = event as Extract<AgentEvent, { type: 'tool_execution_update' }>;\n if (e.toolName !== WORKFLOW_TOOL_NAME) return;\n const snap = extractWorkflowSnapshot(e.partialResult);\n if (!snap) return;\n this.onUpdate(ctx.sessionKey, e.toolCallId, snap);\n });\n\n const offEnd = bus.registerListener('tool_execution_end', (event, ctx) => {\n const e = event as Extract<AgentEvent, { type: 'tool_execution_end' }>;\n if (e.toolName !== WORKFLOW_TOOL_NAME) return;\n // tool_end ships the final envelope in `result`; reach in for the\n // authoritative snapshot (durationMs / result / final counts).\n const snap = extractWorkflowSnapshot(e.result, { fromResultEnvelope: true });\n this.onEnd(ctx.sessionKey, e.toolCallId, snap);\n });\n\n return {\n dispose: () => {\n offUpdate();\n offEnd();\n this.disposeAllPending();\n },\n };\n }\n\n // ── Core state machine ──────────────────────────────────────────────────────\n\n /** Visible for tests — direct entry path bypassing the SessionBus glue. */\n onUpdate(sessionKey: string, toolCallId: string, snapshot: WorkflowSnapshot): void {\n const key = stateKey(sessionKey, toolCallId);\n const state = this.getOrCreateState(key, snapshot);\n state.prevSnapshot = state.snapshot;\n state.snapshot = snapshot;\n\n const isKey = isKeyEvent(state.prevSnapshot, snapshot);\n for (const cap of this.subscribers) {\n this.dispatchToChannel(state, sessionKey, cap, { isFinal: false, isKey });\n }\n }\n\n /** Visible for tests — direct entry path bypassing the SessionBus glue. */\n onEnd(sessionKey: string, toolCallId: string, snapshot: WorkflowSnapshot | null): void {\n const key = stateKey(sessionKey, toolCallId);\n const state = this.states.get(key);\n if (!state) return;\n if (snapshot) state.snapshot = snapshot;\n\n for (const cap of this.subscribers) {\n // Always flush the final message — bypass throttle and any pending timer.\n this.cancelPending(state, cap.channelId);\n this.dispatchToChannel(state, sessionKey, cap, { isFinal: true, isKey: true });\n }\n // State is GC'd lazily after a small grace period so any straggler\n // `update` event arriving after `end` is silently dropped (instead of\n // resurrecting the run).\n setTimeout(() => this.states.delete(key), 2_000);\n }\n\n // ── Dispatch + throttle ─────────────────────────────────────────────────────\n\n private dispatchToChannel(\n state: RunState,\n sessionKey: string,\n cap: ChannelProgressCapability,\n flags: { isFinal: boolean; isKey: boolean },\n ): void {\n const cfg = this.resolveChannelSettings(cap);\n if (!cfg.enabled) return;\n if (cfg.mode === 'final-only' && !flags.isFinal) return;\n\n const chState = this.getOrCreateChannelState(state, cap.channelId);\n\n // Key events and the final message bypass throttle.\n if (flags.isFinal || flags.isKey) {\n this.cancelPending(state, cap.channelId);\n void this.sendNow(state, sessionKey, cap, cfg, flags.isFinal);\n return;\n }\n\n const elapsed = this.now() - chState.lastSentAt;\n const wait = Math.max(0, cfg.throttleMs - elapsed);\n if (wait === 0) {\n void this.sendNow(state, sessionKey, cap, cfg, false);\n return;\n }\n if (chState.pendingTimer) return; // already scheduled; latest snapshot will be picked up\n chState.pendingTimer = setTimeout(() => {\n chState.pendingTimer = undefined;\n void this.sendNow(state, sessionKey, cap, cfg, false);\n }, wait);\n }\n\n private async sendNow(\n state: RunState,\n sessionKey: string,\n cap: ChannelProgressCapability,\n cfg: ChannelResolvedSettings,\n isFinal: boolean,\n ): Promise<void> {\n const chState = this.getOrCreateChannelState(state, cap.channelId);\n // Serialise per-channel sends so a slow editMessage call doesn't get\n // overtaken by a faster one and leave the bubble out of order.\n if (chState.inflight) await chState.inflight.catch(() => undefined);\n\n const text = renderWorkflowText(state.snapshot, isFinal, {\n maxAgentsPerPhase: RENDER_MAX_AGENTS_PER_PHASE,\n maxLogs: RENDER_MAX_LOGS,\n showResultPreviews: isFinal,\n });\n\n const previousMessageId = cfg.mode === 'edit' ? chState.lastMessageId : undefined;\n const task = cap\n .postProgress({ sessionKey, text, previousMessageId, isFinal, mode: cfg.mode })\n .then((r) => {\n chState.lastMessageId = r.messageId;\n chState.lastSentAt = this.now();\n })\n .catch((err) => {\n const msg = err instanceof Error ? err.message : String(err);\n log.warn(\n { err, errorMessage: msg, channelId: cap.channelId, sessionKey },\n `workflow progress postProgress failed: ${msg}`,\n );\n })\n .finally(() => {\n chState.inflight = undefined;\n });\n\n chState.inflight = task;\n await task;\n }\n\n private cancelPending(state: RunState, channelId: string): void {\n const chState = state.perChannel.get(channelId);\n if (chState?.pendingTimer) {\n clearTimeout(chState.pendingTimer);\n chState.pendingTimer = undefined;\n }\n }\n\n private disposeAllPending(): void {\n for (const state of this.states.values()) {\n for (const ch of state.perChannel.values()) {\n if (ch.pendingTimer) clearTimeout(ch.pendingTimer);\n }\n }\n this.states.clear();\n }\n\n // ── State helpers ───────────────────────────────────────────────────────────\n\n private getOrCreateState(key: string, snapshot: WorkflowSnapshot): RunState {\n let state = this.states.get(key);\n if (!state) {\n state = { snapshot, perChannel: new Map() };\n this.states.set(key, state);\n }\n return state;\n }\n\n private getOrCreateChannelState(state: RunState, channelId: string): ChannelRunState {\n let ch = state.perChannel.get(channelId);\n if (!ch) {\n ch = { lastSentAt: 0 };\n state.perChannel.set(channelId, ch);\n }\n return ch;\n }\n\n /** Resolved (enabled / throttleMs / mode) for a channel, with config overrides. */\n private resolveChannelSettings(cap: ChannelProgressCapability): ChannelResolvedSettings {\n const cached = this.resolved.get(cap.channelId);\n if (cached) return cached;\n const override = readChannelConfig(this.opts.getConfig?.(), cap.channelId);\n const resolved: ChannelResolvedSettings = {\n enabled: override?.enabled ?? true,\n throttleMs: override?.throttleMs ?? cap.defaultThrottleMs,\n mode: override?.mode ?? cap.defaultMode,\n };\n this.resolved.set(cap.channelId, resolved);\n return resolved;\n }\n\n /** Drop any cached config so the next dispatch re-reads. Call after config reload. */\n invalidateConfigCache(): void {\n this.resolved.clear();\n }\n\n // ── Test introspection ──────────────────────────────────────────────────────\n\n /** @internal — for tests only. */\n _stateCount(): number {\n return this.states.size;\n }\n}\n\n// ── Singleton ───────────────────────────────────────────────────────────────\n\nlet singleton: WorkflowProgressBroker | null = null;\n\n/**\n * Process-wide broker singleton. Channels register against this one; the\n * service wires it to the session bus during startup.\n */\nexport function getWorkflowProgressBroker(): WorkflowProgressBroker {\n if (!singleton) singleton = new WorkflowProgressBroker();\n return singleton;\n}\n\n/** Test-only — reset the singleton between cases. */\nexport function _resetWorkflowProgressBrokerForTests(): void {\n singleton = null;\n}\n\n// ── Pure helpers ────────────────────────────────────────────────────────────\n\nfunction stateKey(sessionKey: string, toolCallId: string | undefined): string {\n return `${sessionKey}\u0001${toolCallId ?? ''}`;\n}\n\n/**\n * Compare two snapshots and decide whether the new one is \"key\" — i.e. worth\n * bypassing the per-channel throttle. Anything visible to the user as a\n * progress milestone qualifies; counts ticking by alone do not.\n */\nfunction isKeyEvent(prev: WorkflowSnapshot | undefined, next: WorkflowSnapshot): boolean {\n if (!prev) return true; // first update of the run\n if (prev.currentPhase !== next.currentPhase) return true;\n if (next.errorCount > prev.errorCount) return true;\n if (next.skippedCount > prev.skippedCount) return true;\n // New phase row in the rollup (declared via `phase(...)` mid-run)\n if (prev.phases.length !== next.phases.length) return true;\n if (hasNewFailedAgent(prev.agents, next.agents)) return true;\n return false;\n}\n\nfunction hasNewFailedAgent(\n prev: WorkflowAgentSnapshot[],\n next: WorkflowAgentSnapshot[],\n): boolean {\n const prevBad = new Set(\n prev.filter((a) => a.status === 'error' || a.status === 'skipped').map((a) => a.id),\n );\n for (const a of next) {\n if ((a.status === 'error' || a.status === 'skipped') && !prevBad.has(a.id)) return true;\n }\n return false;\n}\n\n/**\n * Pull a {@link WorkflowSnapshot} out of an AgentToolResult-shaped value.\n * Returns null when the payload is not snapshot-shaped (text-only updates,\n * non-workflow tools, etc.).\n *\n * `fromResultEnvelope = true` (used for `tool_end.result`) tolerates the\n * `{ content, details }` wrapper.\n */\nfunction extractWorkflowSnapshot(\n payload: unknown,\n opts: { fromResultEnvelope?: boolean } = {},\n): WorkflowSnapshot | null {\n if (!payload || typeof payload !== 'object') return null;\n const rec = payload as Record<string, unknown>;\n if (opts.fromResultEnvelope) {\n const details = rec.details;\n if (details && typeof details === 'object') return coerce(details);\n return null;\n }\n if ('details' in rec) {\n const details = rec.details;\n if (details && typeof details === 'object') return coerce(details);\n return null;\n }\n return coerce(rec);\n}\n\nfunction coerce(value: unknown): WorkflowSnapshot | null {\n if (!value || typeof value !== 'object') return null;\n const rec = value as Record<string, unknown>;\n if (typeof rec.name !== 'string') return null;\n if (!Array.isArray(rec.agents)) return null;\n return value as WorkflowSnapshot;\n}\n\nfunction readChannelConfig(\n config: Config | undefined,\n channelId: string,\n): { enabled?: boolean; throttleMs?: number; mode?: WorkflowProgressMode } | undefined {\n const channels = config?.channels as Record<string, unknown> | undefined;\n if (!channels) return undefined;\n const cfg = channels[channelId];\n if (!cfg || typeof cfg !== 'object') return undefined;\n const wf = (cfg as { workflowProgress?: unknown }).workflowProgress;\n if (!wf || typeof wf !== 'object') return undefined;\n return wf as { enabled?: boolean; throttleMs?: number; mode?: WorkflowProgressMode };\n}\n"],"mappings":"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"}
@@ -5,7 +5,7 @@ import { DEFAULT_DELEGATE_TOOLS, DELEGATE_BLOCKED_TOOLS } from "../tools/delegat
5
5
  import { STRUCTURED_OUTPUT_TOOL_NAME, createStructuredOutputTool } from "./structured-output-tool.js";
6
6
  //#region src/agent/workflow/subagent-runner.ts
7
7
  init_logger();
8
- const log = createLogger("workflow-subagent-runner");
8
+ const log = createLogger("Agent:WorkflowSubagent");
9
9
  const DEFAULT_MAX_ITERATIONS = 30;
10
10
  var DelegateSubagentRunner = class {
11
11
  constructor(deps) {
@@ -1 +1 @@
1
- {"version":3,"file":"subagent-runner.js","names":[],"sources":["../../../../src/agent/workflow/subagent-runner.ts"],"sourcesContent":["/**\n * Adapter: spawns one isolated child agent per `agent()` call from a workflow.\n *\n * Wraps the existing `createDelegateChildHandle` so the workflow runtime stays\n * decoupled from the LLM stack (it sees only the `SubagentRunner` interface).\n *\n * Key behaviour:\n * - When `opts.schema` is provided, we inject `structured_output` into the child\n * tool set and unwrap the captured value on success. If the subagent finishes\n * without ever calling `structured_output`, we treat it as failure (`null`).\n * - Failures and aborts resolve to `null`. The workflow runtime continues — this\n * matches the pi-dynamic-workflows contract and keeps fan-out pipelines robust.\n * - We do NOT mutate `createDelegateChildHandle` — we just leverage its\n * `buildChildTools` injection point.\n */\n\nimport type { AgentTool } from '@earendil-works/pi-agent-core';\nimport type { Api, Model } from '@earendil-works/pi-ai';\n\nimport type { Config } from '../../config/schema.js';\nimport type { MessageBus } from '../../infra/bus/index.js';\nimport { createLogger } from '../../utils/logger.js';\n\nimport {\n type BuildChildToolsOptions,\n createDelegateChildHandle,\n type DelegateChildHandleOptions,\n} from '../child-agent-factory.js';\nimport {\n DEFAULT_DELEGATE_TOOLS,\n DELEGATE_BLOCKED_TOOLS,\n} from '../tools/delegate-tool.js';\nimport type { ToolExecutorConfig } from '../tools/executor.js';\n\nimport {\n createStructuredOutputTool,\n STRUCTURED_OUTPUT_TOOL_NAME,\n type StructuredOutputCapture,\n} from './structured-output-tool.js';\nimport type { SubagentRunOptions, SubagentRunner, SubagentProgressEvent } from './types.js';\n\nconst log = createLogger('workflow-subagent-runner');\n\nconst DEFAULT_MAX_ITERATIONS = 30;\n\nexport interface DelegateSubagentRunnerDeps {\n workspace: string;\n bus: MessageBus;\n /** Resolves the default subagent model (typically the parent agent's primary model). */\n getDefaultModel: () => Model<Api>;\n getConfig: () => Config | undefined;\n toolExecutorConfig?: Partial<ToolExecutorConfig>;\n /**\n * Provided by the workflow tool from `AgentToolsFactory` — mirrors how\n * `delegate-tool` is wired (avoids importing `tools/factory.ts` here and\n * breaking the existing factory ↔ delegate-tool ↔ child-agent-factory\n * dependency contract).\n */\n buildChildTools: (opts: BuildChildToolsOptions) => AgentTool<any, any>[];\n}\n\nexport class DelegateSubagentRunner implements SubagentRunner {\n constructor(private readonly deps: DelegateSubagentRunnerDeps) {}\n\n async run<T = string>(prompt: string, opts: SubagentRunOptions<T>): Promise<T | null> {\n if (opts.signal?.aborted) return null;\n\n const capture: StructuredOutputCapture<T> = { called: false, value: undefined };\n const wantStructured = Boolean(opts.schema);\n\n const allowed = resolveAllowedToolNames(opts.allowedToolNames, wantStructured);\n const model = opts.model ?? safeResolveDefaultModel(this.deps.getDefaultModel);\n if (!model) {\n log.warn({ label: opts.label }, 'subagent run skipped: no primary model resolved');\n return null;\n }\n\n const fullPrompt = buildPrompt(prompt, opts, wantStructured);\n const streamMode = resolveSubagentStreamMode(this.deps.getConfig);\n\n const childOptions: DelegateChildHandleOptions = {\n workspace: this.deps.workspace,\n goal: fullPrompt,\n allowedToolNames: allowed,\n maxIterations: opts.maxIterations ?? DEFAULT_MAX_ITERATIONS,\n model,\n bus: this.deps.bus,\n getConfig: this.deps.getConfig,\n toolExecutorConfig: this.deps.toolExecutorConfig,\n buildChildTools: (childOpts) => {\n const base = this.deps.buildChildTools(childOpts);\n if (!wantStructured || !opts.schema) return base;\n // Replace any existing tool with the same name so the per-run capture wins.\n const filtered = base.filter((t) => t.name !== STRUCTURED_OUTPUT_TOOL_NAME);\n return [\n ...filtered,\n createStructuredOutputTool({ schema: opts.schema, capture }) as unknown as AgentTool<any, any>,\n ];\n },\n progressHooks:\n opts.onProgress && streamMode !== 'off'\n ? {\n mode: streamMode === 'full' ? 'full' : 'steps',\n onProgress: (event) => {\n opts.onProgress?.(mapChildProgressEvent(event));\n },\n }\n : undefined,\n };\n\n const handle = createDelegateChildHandle(childOptions);\n const onAbort = () => handle.abort();\n opts.signal?.addEventListener('abort', onAbort, { once: true });\n\n try {\n const { summary } = await handle.run();\n if (opts.signal?.aborted) return null;\n\n if (wantStructured) {\n if (!capture.called) {\n log.warn({ label: opts.label }, 'subagent finished without calling structured_output');\n return null;\n }\n return capture.value as T;\n }\n return summary as unknown as T;\n } catch (e) {\n if (opts.rethrow) throw e;\n const msg = e instanceof Error ? e.message : String(e);\n log.warn({ err: e, label: opts.label, errorMessage: msg }, `subagent run failed: ${msg}`);\n return null;\n } finally {\n opts.signal?.removeEventListener('abort', onAbort);\n }\n }\n}\n\nfunction resolveAllowedToolNames(\n requested: string[] | undefined,\n wantStructured: boolean,\n): string[] {\n const base = requested && requested.length > 0 ? requested : [...DEFAULT_DELEGATE_TOOLS];\n const filtered = base\n .map((s) => String(s).trim())\n .filter((s) => s.length > 0)\n .filter((s) => !DELEGATE_BLOCKED_TOOLS.has(s));\n if (wantStructured && !filtered.includes(STRUCTURED_OUTPUT_TOOL_NAME)) {\n filtered.push(STRUCTURED_OUTPUT_TOOL_NAME);\n }\n return [...new Set(filtered)];\n}\n\nfunction buildPrompt(prompt: string, opts: SubagentRunOptions<unknown>, structured: boolean): string {\n const parts: string[] = [];\n if (opts.instructions?.trim()) parts.push(opts.instructions.trim());\n if (opts.label) parts.push(`Task label: ${opts.label}`);\n if (opts.phase) parts.push(`Workflow phase: ${opts.phase}`);\n parts.push(prompt);\n if (structured) {\n parts.push(\n [\n 'Final output contract:',\n '- Your final action MUST be a structured_output tool call.',\n '- The structured_output arguments are the return value of this subagent.',\n '- Do not emit a prose final answer instead of structured_output.',\n '- If you need to inspect files or run commands first, do so, then call structured_output exactly once.',\n ].join('\\n'),\n );\n }\n return parts.join('\\n\\n');\n}\n\nfunction safeResolveDefaultModel(get: () => Model<Api>): Model<Api> | null {\n try {\n return get();\n } catch (e) {\n log.warn({ err: e }, 'failed to resolve default subagent model');\n return null;\n }\n}\n\nfunction resolveSubagentStreamMode(\n getConfig: () => Config | undefined,\n): 'off' | 'steps' | 'full' {\n const mode = getConfig()?.agents?.defaults?.workflow?.subagentStream;\n if (mode === 'off' || mode === 'steps' || mode === 'full') return mode;\n return 'steps';\n}\n\nfunction mapChildProgressEvent(event: {\n type: 'tool_start' | 'tool_end' | 'iteration' | 'text_delta' | 'thinking_delta';\n toolCallId?: string;\n toolName?: string;\n args?: Record<string, unknown>;\n isError?: boolean;\n resultPreview?: string;\n error?: string;\n count?: number;\n max?: number;\n delta?: string;\n}): SubagentProgressEvent {\n switch (event.type) {\n case 'tool_start':\n return {\n type: 'tool_start',\n toolCallId: event.toolCallId ?? '',\n toolName: event.toolName ?? 'tool',\n args: event.args ?? {},\n };\n case 'tool_end':\n return {\n type: 'tool_end',\n toolCallId: event.toolCallId ?? '',\n toolName: event.toolName ?? 'tool',\n isError: Boolean(event.isError),\n resultPreview: event.resultPreview,\n error: event.error,\n };\n case 'iteration':\n return {\n type: 'iteration',\n count: event.count ?? 0,\n max: event.max ?? 0,\n };\n case 'text_delta':\n return { type: 'text_delta', delta: event.delta ?? '' };\n case 'thinking_delta':\n return { type: 'thinking_delta', delta: event.delta ?? '' };\n default:\n return { type: 'text_delta', delta: '' };\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"subagent-runner.js","names":[],"sources":["../../../../src/agent/workflow/subagent-runner.ts"],"sourcesContent":["/**\n * Adapter: spawns one isolated child agent per `agent()` call from a workflow.\n *\n * Wraps the existing `createDelegateChildHandle` so the workflow runtime stays\n * decoupled from the LLM stack (it sees only the `SubagentRunner` interface).\n *\n * Key behaviour:\n * - When `opts.schema` is provided, we inject `structured_output` into the child\n * tool set and unwrap the captured value on success. If the subagent finishes\n * without ever calling `structured_output`, we treat it as failure (`null`).\n * - Failures and aborts resolve to `null`. The workflow runtime continues — this\n * matches the pi-dynamic-workflows contract and keeps fan-out pipelines robust.\n * - We do NOT mutate `createDelegateChildHandle` — we just leverage its\n * `buildChildTools` injection point.\n */\n\nimport type { AgentTool } from '@earendil-works/pi-agent-core';\nimport type { Api, Model } from '@earendil-works/pi-ai';\n\nimport type { Config } from '../../config/schema.js';\nimport type { MessageBus } from '../../infra/bus/index.js';\nimport { createLogger } from '../../utils/logger.js';\n\nimport {\n type BuildChildToolsOptions,\n createDelegateChildHandle,\n type DelegateChildHandleOptions,\n} from '../child-agent-factory.js';\nimport {\n DEFAULT_DELEGATE_TOOLS,\n DELEGATE_BLOCKED_TOOLS,\n} from '../tools/delegate-tool.js';\nimport type { ToolExecutorConfig } from '../tools/executor.js';\n\nimport {\n createStructuredOutputTool,\n STRUCTURED_OUTPUT_TOOL_NAME,\n type StructuredOutputCapture,\n} from './structured-output-tool.js';\nimport type { SubagentRunOptions, SubagentRunner, SubagentProgressEvent } from './types.js';\n\nconst log = createLogger('Agent:WorkflowSubagent');\n\nconst DEFAULT_MAX_ITERATIONS = 30;\n\nexport interface DelegateSubagentRunnerDeps {\n workspace: string;\n bus: MessageBus;\n /** Resolves the default subagent model (typically the parent agent's primary model). */\n getDefaultModel: () => Model<Api>;\n getConfig: () => Config | undefined;\n toolExecutorConfig?: Partial<ToolExecutorConfig>;\n /**\n * Provided by the workflow tool from `AgentToolsFactory` — mirrors how\n * `delegate-tool` is wired (avoids importing `tools/factory.ts` here and\n * breaking the existing factory ↔ delegate-tool ↔ child-agent-factory\n * dependency contract).\n */\n buildChildTools: (opts: BuildChildToolsOptions) => AgentTool<any, any>[];\n}\n\nexport class DelegateSubagentRunner implements SubagentRunner {\n constructor(private readonly deps: DelegateSubagentRunnerDeps) {}\n\n async run<T = string>(prompt: string, opts: SubagentRunOptions<T>): Promise<T | null> {\n if (opts.signal?.aborted) return null;\n\n const capture: StructuredOutputCapture<T> = { called: false, value: undefined };\n const wantStructured = Boolean(opts.schema);\n\n const allowed = resolveAllowedToolNames(opts.allowedToolNames, wantStructured);\n const model = opts.model ?? safeResolveDefaultModel(this.deps.getDefaultModel);\n if (!model) {\n log.warn({ label: opts.label }, 'subagent run skipped: no primary model resolved');\n return null;\n }\n\n const fullPrompt = buildPrompt(prompt, opts, wantStructured);\n const streamMode = resolveSubagentStreamMode(this.deps.getConfig);\n\n const childOptions: DelegateChildHandleOptions = {\n workspace: this.deps.workspace,\n goal: fullPrompt,\n allowedToolNames: allowed,\n maxIterations: opts.maxIterations ?? DEFAULT_MAX_ITERATIONS,\n model,\n bus: this.deps.bus,\n getConfig: this.deps.getConfig,\n toolExecutorConfig: this.deps.toolExecutorConfig,\n buildChildTools: (childOpts) => {\n const base = this.deps.buildChildTools(childOpts);\n if (!wantStructured || !opts.schema) return base;\n // Replace any existing tool with the same name so the per-run capture wins.\n const filtered = base.filter((t) => t.name !== STRUCTURED_OUTPUT_TOOL_NAME);\n return [\n ...filtered,\n createStructuredOutputTool({ schema: opts.schema, capture }) as unknown as AgentTool<any, any>,\n ];\n },\n progressHooks:\n opts.onProgress && streamMode !== 'off'\n ? {\n mode: streamMode === 'full' ? 'full' : 'steps',\n onProgress: (event) => {\n opts.onProgress?.(mapChildProgressEvent(event));\n },\n }\n : undefined,\n };\n\n const handle = createDelegateChildHandle(childOptions);\n const onAbort = () => handle.abort();\n opts.signal?.addEventListener('abort', onAbort, { once: true });\n\n try {\n const { summary } = await handle.run();\n if (opts.signal?.aborted) return null;\n\n if (wantStructured) {\n if (!capture.called) {\n log.warn({ label: opts.label }, 'subagent finished without calling structured_output');\n return null;\n }\n return capture.value as T;\n }\n return summary as unknown as T;\n } catch (e) {\n if (opts.rethrow) throw e;\n const msg = e instanceof Error ? e.message : String(e);\n log.warn({ err: e, label: opts.label, errorMessage: msg }, `subagent run failed: ${msg}`);\n return null;\n } finally {\n opts.signal?.removeEventListener('abort', onAbort);\n }\n }\n}\n\nfunction resolveAllowedToolNames(\n requested: string[] | undefined,\n wantStructured: boolean,\n): string[] {\n const base = requested && requested.length > 0 ? requested : [...DEFAULT_DELEGATE_TOOLS];\n const filtered = base\n .map((s) => String(s).trim())\n .filter((s) => s.length > 0)\n .filter((s) => !DELEGATE_BLOCKED_TOOLS.has(s));\n if (wantStructured && !filtered.includes(STRUCTURED_OUTPUT_TOOL_NAME)) {\n filtered.push(STRUCTURED_OUTPUT_TOOL_NAME);\n }\n return [...new Set(filtered)];\n}\n\nfunction buildPrompt(prompt: string, opts: SubagentRunOptions<unknown>, structured: boolean): string {\n const parts: string[] = [];\n if (opts.instructions?.trim()) parts.push(opts.instructions.trim());\n if (opts.label) parts.push(`Task label: ${opts.label}`);\n if (opts.phase) parts.push(`Workflow phase: ${opts.phase}`);\n parts.push(prompt);\n if (structured) {\n parts.push(\n [\n 'Final output contract:',\n '- Your final action MUST be a structured_output tool call.',\n '- The structured_output arguments are the return value of this subagent.',\n '- Do not emit a prose final answer instead of structured_output.',\n '- If you need to inspect files or run commands first, do so, then call structured_output exactly once.',\n ].join('\\n'),\n );\n }\n return parts.join('\\n\\n');\n}\n\nfunction safeResolveDefaultModel(get: () => Model<Api>): Model<Api> | null {\n try {\n return get();\n } catch (e) {\n log.warn({ err: e }, 'failed to resolve default subagent model');\n return null;\n }\n}\n\nfunction resolveSubagentStreamMode(\n getConfig: () => Config | undefined,\n): 'off' | 'steps' | 'full' {\n const mode = getConfig()?.agents?.defaults?.workflow?.subagentStream;\n if (mode === 'off' || mode === 'steps' || mode === 'full') return mode;\n return 'steps';\n}\n\nfunction mapChildProgressEvent(event: {\n type: 'tool_start' | 'tool_end' | 'iteration' | 'text_delta' | 'thinking_delta';\n toolCallId?: string;\n toolName?: string;\n args?: Record<string, unknown>;\n isError?: boolean;\n resultPreview?: string;\n error?: string;\n count?: number;\n max?: number;\n delta?: string;\n}): SubagentProgressEvent {\n switch (event.type) {\n case 'tool_start':\n return {\n type: 'tool_start',\n toolCallId: event.toolCallId ?? '',\n toolName: event.toolName ?? 'tool',\n args: event.args ?? {},\n };\n case 'tool_end':\n return {\n type: 'tool_end',\n toolCallId: event.toolCallId ?? '',\n toolName: event.toolName ?? 'tool',\n isError: Boolean(event.isError),\n resultPreview: event.resultPreview,\n error: event.error,\n };\n case 'iteration':\n return {\n type: 'iteration',\n count: event.count ?? 0,\n max: event.max ?? 0,\n };\n case 'text_delta':\n return { type: 'text_delta', delta: event.delta ?? '' };\n case 'thinking_delta':\n return { type: 'thinking_delta', delta: event.delta ?? '' };\n default:\n return { type: 'text_delta', delta: '' };\n }\n}\n"],"mappings":"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"}
@@ -61,8 +61,9 @@ var MessagePipeline = class {
61
61
  } catch (err) {
62
62
  log.error({
63
63
  channel,
64
- err
65
- }, "Process handler error");
64
+ err,
65
+ phase: "channel.inbound"
66
+ }, `Process handler error: ${err instanceof Error ? err.message : String(err)}`);
66
67
  this.onError?.(err, processedCtx);
67
68
  return;
68
69
  }
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.js","names":[],"sources":["../../../src/channels/pipeline.ts"],"sourcesContent":["/**\n * Message Processing Pipeline\n * \n * Three-stage pipeline:\n * - Preflight: Filter empty messages, self-messages, detect commands\n * - Process: Transform format, extract metadata\n * - Delivery: Send to Agent\n */\n\nimport { randomUUID } from 'node:crypto';\n\nimport { createLogger, runWithLogContext, updateAsyncLogContext } from '../utils/logger.js';\nimport type { AgentResponse } from './plugin-types.js';\nimport { formatEnvelopeTimestamp } from './envelope-timestamp.js';\n\n// Re-export for convenience\nexport type { AgentResponse } from './plugin-types.js';\n\nconst log = createLogger('Pipeline');\n\nfunction pipelineLogRequestId(ctx: PipelineMessageContext): string {\n const raw = ctx.metadata?.requestId;\n if (typeof raw === 'string' && raw.trim().length > 0) {\n return raw.trim();\n }\n return randomUUID();\n}\n\nfunction pipelineLogSessionId(ctx: PipelineMessageContext): string {\n const sk = ctx.metadata?.sessionKey;\n if (typeof sk === 'string' && sk.trim().length > 0) {\n return sk.trim();\n }\n return `${ctx.channel}:${ctx.chatId}`;\n}\n\n// ============================================\n// Types\n// ============================================\n\nexport interface PipelineMessageContext {\n /** Channel identifier */\n channel: string;\n /** Account ID */\n accountId: string;\n /** Chat ID */\n chatId: string;\n /** Sender ID */\n senderId: string;\n /** Message content */\n content: string;\n /** Original message metadata */\n metadata: Record<string, unknown>;\n /** Is group chat */\n isGroup: boolean;\n /** Is direct message */\n isDm: boolean;\n /** Thread ID (optional) */\n threadId?: string;\n /** Message ID (optional) */\n messageId?: string;\n}\n\nexport interface PipelineMediaRef {\n type: 'photo' | 'video' | 'audio' | 'document' | 'voice';\n fileId: string;\n mimeType?: string;\n fileName?: string;\n url?: string;\n}\n\n// ============================================\n// Handler Interfaces\n// ============================================\n\nexport interface PreflightHandler {\n /** Handler name */\n name: string;\n /** Preflight - return null to skip message */\n preflight?(ctx: PipelineMessageContext): Promise<PipelineMessageContext | null>;\n}\n\nexport interface ProcessHandler {\n /** Handler name */\n name: string;\n /** Process message - transform and extract */\n process(ctx: PipelineMessageContext): Promise<PipelineMessageContext>;\n}\n\nexport interface DeliveryHandler {\n /** Handler name */\n name: string;\n /** Deliver message to Agent */\n deliver(ctx: PipelineMessageContext, response: AgentResponse): Promise<void>;\n}\n\n// ============================================\n// Pipeline Options\n// ============================================\n\nexport interface PipelineOptions {\n /** Channel name */\n channel: string;\n /** Preflight handlers */\n preflightHandlers?: PreflightHandler[];\n /** Process handlers */\n processHandlers?: ProcessHandler[];\n /** Delivery handlers */\n deliveryHandlers?: DeliveryHandler[];\n /** Agent callback */\n agentInvoke?: (ctx: PipelineMessageContext) => Promise<AgentResponse>;\n /** Error handler */\n onError?: (err: unknown, ctx: PipelineMessageContext) => void;\n}\n\n// ============================================\n// Pipeline Implementation\n// ============================================\n\nexport class MessagePipeline {\n private channel: string;\n private preflightHandlers: PreflightHandler[];\n private processHandlers: ProcessHandler[];\n private deliveryHandlers: DeliveryHandler[];\n private agentInvoke?: PipelineOptions['agentInvoke'];\n private onError?: PipelineOptions['onError'];\n\n constructor(options: PipelineOptions) {\n this.channel = options.channel;\n this.preflightHandlers = options.preflightHandlers ?? [];\n this.processHandlers = options.processHandlers ?? [];\n this.deliveryHandlers = options.deliveryHandlers ?? [];\n this.agentInvoke = options.agentInvoke;\n this.onError = options.onError;\n }\n\n /**\n * Handle inbound message\n */\n async handleMessage(ctx: PipelineMessageContext): Promise<void> {\n const channel = this.channel;\n const requestId = pipelineLogRequestId(ctx);\n\n await runWithLogContext(\n {\n requestId,\n sessionId: pipelineLogSessionId(ctx),\n },\n async () => {\n // 1. Preflight stage\n let processedCtx = await this.runPreflight(ctx);\n if (!processedCtx) {\n log.debug({ channel, chatId: ctx.chatId }, 'Message filtered in preflight');\n return;\n }\n\n // 2. Process stage\n try {\n processedCtx = await this.runProcess(processedCtx);\n } catch (err) {\n log.error({ channel, err }, 'Process handler error');\n this.onError?.(err, processedCtx);\n return;\n }\n\n const resolvedSk = processedCtx.metadata?.sessionKey;\n if (typeof resolvedSk === 'string' && resolvedSk.trim().length > 0) {\n updateAsyncLogContext({ sessionId: resolvedSk.trim() });\n }\n\n // 3. Deliver to Agent\n if (!this.agentInvoke) {\n log.warn({ channel }, 'No agentInvoke configured');\n return;\n }\n\n let response: AgentResponse;\n try {\n response = await this.agentInvoke(processedCtx);\n } catch (err) {\n log.error({ channel, err }, 'Agent invocation error');\n this.onError?.(err, processedCtx);\n return;\n }\n\n // 4. Delivery stage\n try {\n await this.runDelivery(processedCtx, response);\n } catch (err) {\n log.error({ channel, err }, 'Delivery handler error');\n this.onError?.(err, processedCtx);\n }\n },\n );\n }\n\n private async runPreflight(ctx: PipelineMessageContext): Promise<PipelineMessageContext | null> {\n for (const handler of this.preflightHandlers) {\n try {\n const result = await handler.preflight?.(ctx);\n if (!result) {\n log.debug({ channel: this.channel, handler: handler.name }, 'Preflight filtered message');\n return null;\n }\n ctx = result;\n } catch (err) {\n log.error({ channel: this.channel, handler: handler.name, err }, 'Preflight handler error');\n return null;\n }\n }\n return ctx;\n }\n\n private async runProcess(ctx: PipelineMessageContext): Promise<PipelineMessageContext> {\n for (const handler of this.processHandlers) {\n try {\n ctx = await handler.process(ctx);\n } catch (err) {\n log.error({ channel: this.channel, handler: handler.name, err }, 'Process handler error');\n throw err;\n }\n }\n return ctx;\n }\n\n private async runDelivery(ctx: PipelineMessageContext, response: AgentResponse): Promise<void> {\n for (const handler of this.deliveryHandlers) {\n try {\n await handler.deliver(ctx, response);\n } catch (err) {\n log.error({ channel: this.channel, handler: handler.name, err }, 'Delivery handler error');\n throw err;\n }\n }\n }\n}\n\n// ============================================\n// Standard Handlers\n// ============================================\n\n/**\n * Create filter-self handler\n */\nexport function createFilterSelfHandler(currentBotId: string): PreflightHandler {\n return {\n name: 'filterSelf',\n preflight: async (ctx) => {\n if (ctx.senderId === currentBotId) {\n return null;\n }\n return ctx;\n },\n };\n}\n\n/**\n * Create filter-empty handler\n */\nexport function createFilterEmptyHandler(): PreflightHandler {\n return {\n name: 'filterEmpty',\n preflight: async (ctx) => {\n const content = ctx.content?.trim() ?? '';\n if (!content && (!ctx.metadata.media || (ctx.metadata.media as PipelineMediaRef[]).length === 0)) {\n return null;\n }\n return ctx;\n },\n };\n}\n\n/**\n * Create filter-commands handler\n */\nexport function createFilterCommandsHandler(commands: string[]): PreflightHandler {\n const commandSet = new Set(commands.map(c => c.toLowerCase()));\n return {\n name: 'filterCommands',\n preflight: async (ctx) => {\n const firstWord = ctx.content?.split(/\\s/)[0]?.toLowerCase() ?? '';\n if (firstWord.startsWith('/') && commandSet.has(firstWord.slice(1))) {\n ctx.metadata.isCommand = true;\n ctx.metadata.command = firstWord.slice(1);\n }\n return ctx;\n },\n };\n}\n\n/**\n * Create standard preflight handlers\n */\nexport function standardPreflightHandlers(botId: string): PreflightHandler[] {\n return [\n createFilterSelfHandler(botId),\n createFilterEmptyHandler(),\n createFilterCommandsHandler(['start', 'help', 'status', 'stop']),\n ];\n}\n\n/**\n * Prepends a per-turn `[YYYY-MM-DD HH:MM TZ]` prefix to inbound text so the model has\n * a stable \"now\" without changing the system prompt (prompt-cache friendly).\n */\nexport function createEnvelopeTimestampHandler(timezone?: string): ProcessHandler {\n return {\n name: 'envelopeTimestamp',\n process: async (ctx) => {\n const text = ctx.content?.trim();\n if (!text) {\n return ctx;\n }\n const timestamp = formatEnvelopeTimestamp(timezone);\n return { ...ctx, content: `[${timestamp}] ${ctx.content}` };\n },\n };\n}\n\n/**\n * Create standard process handlers\n */\nexport function standardProcessHandlers(timezone?: string): ProcessHandler[] {\n return [createEnvelopeTimestampHandler(timezone)];\n}\n\n// ============================================\n// Factory\n// ============================================\n\nexport interface CreatePipelineParams {\n channel: string;\n botId: string;\n agentInvoke: PipelineOptions['agentInvoke'];\n onError?: PipelineOptions['onError'];\n /** IANA timezone — matches userTimezone from agent config / USER.md */\n timezone?: string;\n}\n\n/**\n * Create message processing pipeline\n */\nexport function createPipeline(params: CreatePipelineParams): MessagePipeline {\n return new MessagePipeline({\n channel: params.channel,\n preflightHandlers: standardPreflightHandlers(params.botId),\n processHandlers: standardProcessHandlers(params.timezone),\n agentInvoke: params.agentInvoke,\n onError: params.onError,\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"pipeline.js","names":[],"sources":["../../../src/channels/pipeline.ts"],"sourcesContent":["/**\n * Message Processing Pipeline\n * \n * Three-stage pipeline:\n * - Preflight: Filter empty messages, self-messages, detect commands\n * - Process: Transform format, extract metadata\n * - Delivery: Send to Agent\n */\n\nimport { randomUUID } from 'node:crypto';\n\nimport { createLogger, runWithLogContext, updateAsyncLogContext } from '../utils/logger.js';\nimport type { AgentResponse } from './plugin-types.js';\nimport { formatEnvelopeTimestamp } from './envelope-timestamp.js';\n\n// Re-export for convenience\nexport type { AgentResponse } from './plugin-types.js';\n\nconst log = createLogger('Pipeline');\n\nfunction pipelineLogRequestId(ctx: PipelineMessageContext): string {\n const raw = ctx.metadata?.requestId;\n if (typeof raw === 'string' && raw.trim().length > 0) {\n return raw.trim();\n }\n return randomUUID();\n}\n\nfunction pipelineLogSessionId(ctx: PipelineMessageContext): string {\n const sk = ctx.metadata?.sessionKey;\n if (typeof sk === 'string' && sk.trim().length > 0) {\n return sk.trim();\n }\n return `${ctx.channel}:${ctx.chatId}`;\n}\n\n// ============================================\n// Types\n// ============================================\n\nexport interface PipelineMessageContext {\n /** Channel identifier */\n channel: string;\n /** Account ID */\n accountId: string;\n /** Chat ID */\n chatId: string;\n /** Sender ID */\n senderId: string;\n /** Message content */\n content: string;\n /** Original message metadata */\n metadata: Record<string, unknown>;\n /** Is group chat */\n isGroup: boolean;\n /** Is direct message */\n isDm: boolean;\n /** Thread ID (optional) */\n threadId?: string;\n /** Message ID (optional) */\n messageId?: string;\n}\n\nexport interface PipelineMediaRef {\n type: 'photo' | 'video' | 'audio' | 'document' | 'voice';\n fileId: string;\n mimeType?: string;\n fileName?: string;\n url?: string;\n}\n\n// ============================================\n// Handler Interfaces\n// ============================================\n\nexport interface PreflightHandler {\n /** Handler name */\n name: string;\n /** Preflight - return null to skip message */\n preflight?(ctx: PipelineMessageContext): Promise<PipelineMessageContext | null>;\n}\n\nexport interface ProcessHandler {\n /** Handler name */\n name: string;\n /** Process message - transform and extract */\n process(ctx: PipelineMessageContext): Promise<PipelineMessageContext>;\n}\n\nexport interface DeliveryHandler {\n /** Handler name */\n name: string;\n /** Deliver message to Agent */\n deliver(ctx: PipelineMessageContext, response: AgentResponse): Promise<void>;\n}\n\n// ============================================\n// Pipeline Options\n// ============================================\n\nexport interface PipelineOptions {\n /** Channel name */\n channel: string;\n /** Preflight handlers */\n preflightHandlers?: PreflightHandler[];\n /** Process handlers */\n processHandlers?: ProcessHandler[];\n /** Delivery handlers */\n deliveryHandlers?: DeliveryHandler[];\n /** Agent callback */\n agentInvoke?: (ctx: PipelineMessageContext) => Promise<AgentResponse>;\n /** Error handler */\n onError?: (err: unknown, ctx: PipelineMessageContext) => void;\n}\n\n// ============================================\n// Pipeline Implementation\n// ============================================\n\nexport class MessagePipeline {\n private channel: string;\n private preflightHandlers: PreflightHandler[];\n private processHandlers: ProcessHandler[];\n private deliveryHandlers: DeliveryHandler[];\n private agentInvoke?: PipelineOptions['agentInvoke'];\n private onError?: PipelineOptions['onError'];\n\n constructor(options: PipelineOptions) {\n this.channel = options.channel;\n this.preflightHandlers = options.preflightHandlers ?? [];\n this.processHandlers = options.processHandlers ?? [];\n this.deliveryHandlers = options.deliveryHandlers ?? [];\n this.agentInvoke = options.agentInvoke;\n this.onError = options.onError;\n }\n\n /**\n * Handle inbound message\n */\n async handleMessage(ctx: PipelineMessageContext): Promise<void> {\n const channel = this.channel;\n const requestId = pipelineLogRequestId(ctx);\n\n await runWithLogContext(\n {\n requestId,\n sessionId: pipelineLogSessionId(ctx),\n },\n async () => {\n // 1. Preflight stage\n let processedCtx = await this.runPreflight(ctx);\n if (!processedCtx) {\n log.debug({ channel, chatId: ctx.chatId }, 'Message filtered in preflight');\n return;\n }\n\n // 2. Process stage\n try {\n processedCtx = await this.runProcess(processedCtx);\n } catch (err) {\n log.error({ channel, err, phase: 'channel.inbound' }, `Process handler error: ${err instanceof Error ? err.message : String(err)}`);\n this.onError?.(err, processedCtx);\n return;\n }\n\n const resolvedSk = processedCtx.metadata?.sessionKey;\n if (typeof resolvedSk === 'string' && resolvedSk.trim().length > 0) {\n updateAsyncLogContext({ sessionId: resolvedSk.trim() });\n }\n\n // 3. Deliver to Agent\n if (!this.agentInvoke) {\n log.warn({ channel }, 'No agentInvoke configured');\n return;\n }\n\n let response: AgentResponse;\n try {\n response = await this.agentInvoke(processedCtx);\n } catch (err) {\n log.error({ channel, err }, 'Agent invocation error');\n this.onError?.(err, processedCtx);\n return;\n }\n\n // 4. Delivery stage\n try {\n await this.runDelivery(processedCtx, response);\n } catch (err) {\n log.error({ channel, err }, 'Delivery handler error');\n this.onError?.(err, processedCtx);\n }\n },\n );\n }\n\n private async runPreflight(ctx: PipelineMessageContext): Promise<PipelineMessageContext | null> {\n for (const handler of this.preflightHandlers) {\n try {\n const result = await handler.preflight?.(ctx);\n if (!result) {\n log.debug({ channel: this.channel, handler: handler.name }, 'Preflight filtered message');\n return null;\n }\n ctx = result;\n } catch (err) {\n log.error({ channel: this.channel, handler: handler.name, err }, 'Preflight handler error');\n return null;\n }\n }\n return ctx;\n }\n\n private async runProcess(ctx: PipelineMessageContext): Promise<PipelineMessageContext> {\n for (const handler of this.processHandlers) {\n try {\n ctx = await handler.process(ctx);\n } catch (err) {\n log.error({ channel: this.channel, handler: handler.name, err }, 'Process handler error');\n throw err;\n }\n }\n return ctx;\n }\n\n private async runDelivery(ctx: PipelineMessageContext, response: AgentResponse): Promise<void> {\n for (const handler of this.deliveryHandlers) {\n try {\n await handler.deliver(ctx, response);\n } catch (err) {\n log.error({ channel: this.channel, handler: handler.name, err }, 'Delivery handler error');\n throw err;\n }\n }\n }\n}\n\n// ============================================\n// Standard Handlers\n// ============================================\n\n/**\n * Create filter-self handler\n */\nexport function createFilterSelfHandler(currentBotId: string): PreflightHandler {\n return {\n name: 'filterSelf',\n preflight: async (ctx) => {\n if (ctx.senderId === currentBotId) {\n return null;\n }\n return ctx;\n },\n };\n}\n\n/**\n * Create filter-empty handler\n */\nexport function createFilterEmptyHandler(): PreflightHandler {\n return {\n name: 'filterEmpty',\n preflight: async (ctx) => {\n const content = ctx.content?.trim() ?? '';\n if (!content && (!ctx.metadata.media || (ctx.metadata.media as PipelineMediaRef[]).length === 0)) {\n return null;\n }\n return ctx;\n },\n };\n}\n\n/**\n * Create filter-commands handler\n */\nexport function createFilterCommandsHandler(commands: string[]): PreflightHandler {\n const commandSet = new Set(commands.map(c => c.toLowerCase()));\n return {\n name: 'filterCommands',\n preflight: async (ctx) => {\n const firstWord = ctx.content?.split(/\\s/)[0]?.toLowerCase() ?? '';\n if (firstWord.startsWith('/') && commandSet.has(firstWord.slice(1))) {\n ctx.metadata.isCommand = true;\n ctx.metadata.command = firstWord.slice(1);\n }\n return ctx;\n },\n };\n}\n\n/**\n * Create standard preflight handlers\n */\nexport function standardPreflightHandlers(botId: string): PreflightHandler[] {\n return [\n createFilterSelfHandler(botId),\n createFilterEmptyHandler(),\n createFilterCommandsHandler(['start', 'help', 'status', 'stop']),\n ];\n}\n\n/**\n * Prepends a per-turn `[YYYY-MM-DD HH:MM TZ]` prefix to inbound text so the model has\n * a stable \"now\" without changing the system prompt (prompt-cache friendly).\n */\nexport function createEnvelopeTimestampHandler(timezone?: string): ProcessHandler {\n return {\n name: 'envelopeTimestamp',\n process: async (ctx) => {\n const text = ctx.content?.trim();\n if (!text) {\n return ctx;\n }\n const timestamp = formatEnvelopeTimestamp(timezone);\n return { ...ctx, content: `[${timestamp}] ${ctx.content}` };\n },\n };\n}\n\n/**\n * Create standard process handlers\n */\nexport function standardProcessHandlers(timezone?: string): ProcessHandler[] {\n return [createEnvelopeTimestampHandler(timezone)];\n}\n\n// ============================================\n// Factory\n// ============================================\n\nexport interface CreatePipelineParams {\n channel: string;\n botId: string;\n agentInvoke: PipelineOptions['agentInvoke'];\n onError?: PipelineOptions['onError'];\n /** IANA timezone — matches userTimezone from agent config / USER.md */\n timezone?: string;\n}\n\n/**\n * Create message processing pipeline\n */\nexport function createPipeline(params: CreatePipelineParams): MessagePipeline {\n return new MessagePipeline({\n channel: params.channel,\n preflightHandlers: standardPreflightHandlers(params.botId),\n processHandlers: standardProcessHandlers(params.timezone),\n agentInvoke: params.agentInvoke,\n onError: params.onError,\n });\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Default the CLI console to `warn` when no log level is set, so routine `info`
3
3
  * output (extensions, diagnostics, etc.) does not clutter the terminal.
4
- * Opt in with `XOPC_LOG_LEVEL`, `LOG_LEVEL`, `DEBUG`, or `--verbose` / `-v`.
4
+ * Opt in with `XOPC_LOG_LEVEL` or `--verbose` / `-v`.
5
5
  * This module must load before any import of `../utils/logger.js`.
6
6
  */
7
7
  declare const env: NodeJS.ProcessEnv;
@@ -2,11 +2,11 @@
2
2
  /**
3
3
  * Default the CLI console to `warn` when no log level is set, so routine `info`
4
4
  * output (extensions, diagnostics, etc.) does not clutter the terminal.
5
- * Opt in with `XOPC_LOG_LEVEL`, `LOG_LEVEL`, `DEBUG`, or `--verbose` / `-v`.
5
+ * Opt in with `XOPC_LOG_LEVEL` or `--verbose` / `-v`.
6
6
  * This module must load before any import of `../utils/logger.js`.
7
7
  */
8
8
  const env = process.env;
9
- if (!env.VITEST && !env.TEST && !env.XOPC_LOG_LEVEL && !env.LOG_LEVEL && !env.DEBUG && !process.argv.includes("--verbose") && !process.argv.includes("-v")) env.XOPC_LOG_LEVEL = "warn";
9
+ if (!env.VITEST && !env.TEST && !env.XOPC_LOG_LEVEL && !process.argv.includes("--verbose") && !process.argv.includes("-v")) env.XOPC_LOG_LEVEL = "warn";
10
10
  //#endregion
11
11
  export {};
12
12
 
@@ -1 +1 @@
1
- {"version":3,"file":"cli-log-level-preset.js","names":[],"sources":["../../../src/cli/cli-log-level-preset.ts"],"sourcesContent":["/**\n * Default the CLI console to `warn` when no log level is set, so routine `info`\n * output (extensions, diagnostics, etc.) does not clutter the terminal.\n * Opt in with `XOPC_LOG_LEVEL`, `LOG_LEVEL`, `DEBUG`, or `--verbose` / `-v`.\n * This module must load before any import of `../utils/logger.js`.\n */\nconst env = process.env;\nif (\n !env.VITEST &&\n !env.TEST &&\n !env.XOPC_LOG_LEVEL &&\n !env.LOG_LEVEL &&\n !env.DEBUG &&\n !process.argv.includes('--verbose') &&\n !process.argv.includes('-v')\n) {\n env.XOPC_LOG_LEVEL = 'warn';\n}\n"],"mappings":";;;;;;;AAMA,MAAM,MAAM,QAAQ;AACpB,IACE,CAAC,IAAI,UACL,CAAC,IAAI,QACL,CAAC,IAAI,kBACL,CAAC,IAAI,aACL,CAAC,IAAI,SACL,CAAC,QAAQ,KAAK,SAAS,YAAY,IACnC,CAAC,QAAQ,KAAK,SAAS,KAAK,CAE5B,KAAI,iBAAiB"}
1
+ {"version":3,"file":"cli-log-level-preset.js","names":[],"sources":["../../../src/cli/cli-log-level-preset.ts"],"sourcesContent":["/**\n * Default the CLI console to `warn` when no log level is set, so routine `info`\n * output (extensions, diagnostics, etc.) does not clutter the terminal.\n * Opt in with `XOPC_LOG_LEVEL` or `--verbose` / `-v`.\n * This module must load before any import of `../utils/logger.js`.\n */\nconst env = process.env;\nif (\n !env.VITEST &&\n !env.TEST &&\n !env.XOPC_LOG_LEVEL &&\n !process.argv.includes('--verbose') &&\n !process.argv.includes('-v')\n) {\n env.XOPC_LOG_LEVEL = 'warn';\n}\n"],"mappings":";;;;;;;AAMA,MAAM,MAAM,QAAQ;AACpB,IACE,CAAC,IAAI,UACL,CAAC,IAAI,QACL,CAAC,IAAI,kBACL,CAAC,QAAQ,KAAK,SAAS,YAAY,IACnC,CAAC,QAAQ,KAAK,SAAS,KAAK,CAE5B,KAAI,iBAAiB"}
@@ -1,7 +1,7 @@
1
1
  import { getLoggerConfig, init_config } from "../../utils/logger/config.js";
2
- import { init_rotation, rotateLogs } from "../../utils/logger/rotation.js";
2
+ import { cleanOldLogs, init_rotation, rotateLogs } from "../../utils/logger/rotation.js";
3
3
  import { register } from "../registry.js";
4
- import { cleanOldLogs, getLogFiles, getLogStats, queryLogs } from "../../utils/logger/log-store.js";
4
+ import { getFileLogStats, getLogFiles, queryLogs } from "../../utils/logger/log-store.js";
5
5
  import { Command } from "commander";
6
6
  //#region src/cli/commands/logs.ts
7
7
  /**
@@ -92,7 +92,7 @@ function createLogsCommand(_ctx) {
92
92
  });
93
93
  command.command("stats").description("Show log statistics").option("--json", "Output as JSON").action(async (options) => {
94
94
  try {
95
- const stats = await getLogStats();
95
+ const stats = await getFileLogStats();
96
96
  const config = getLoggerConfig();
97
97
  if (options.json) {
98
98
  console.log(JSON.stringify({
@@ -1 +1 @@
1
- {"version":3,"file":"logs.js","names":["fetchLogStats"],"sources":["../../../../src/cli/commands/logs.ts"],"sourcesContent":["/**\n * Logs CLI Command\n * \n * Query and analyze logs: list, query, stats, tail, clean\n */\n\nimport { Command } from 'commander';\nimport { register, type CLIContext } from '../registry.js';\nimport {\n getLogFiles,\n queryLogs,\n getLogStats as fetchLogStats,\n cleanOldLogs,\n type LogQuery,\n type LogEntry,\n} from '../../utils/logger/log-store.js';\nimport { getLoggerConfig } from '../../utils/logger/config.js';\nimport { rotateLogs } from '../../utils/logger/rotation.js';\n\nfunction createLogsCommand(_ctx: CLIContext): Command {\n const command = new Command('logs');\n command.description('Manage and query logs');\n\n // List log files\n command\n .command('list')\n .description('List log files')\n .option('-t, --type <type>', 'Filter by type (app|error|audit|access)')\n .option('-s, --sort <field>', 'Sort by (name|size|date)', 'date')\n .option('--json', 'Output as JSON')\n .action(async (options) => {\n try {\n let files = getLogFiles();\n \n // Filter by type\n if (options.type) {\n files = files.filter(f => f.type === options.type);\n }\n \n // Sort\n const sortField = options.sort || 'date';\n files.sort((a, b) => {\n if (sortField === 'name') return a.name.localeCompare(b.name);\n if (sortField === 'size') return b.size - a.size;\n return new Date(b.modified).getTime() - new Date(a.modified).getTime();\n });\n \n if (options.json) {\n console.log(JSON.stringify({ files }, null, 2));\n return;\n }\n \n const config = getLoggerConfig();\n console.log(`\\n📁 Log directory: ${config.logDir}\\n`);\n \n if (files.length === 0) {\n console.log('No log files found.');\n return;\n }\n \n console.log(`Found ${files.length} log file(s):\\n`);\n \n const typeEmoji: Record<string, string> = {\n app: '📝',\n error: '❌',\n audit: '🔒',\n access: '🌐',\n };\n \n for (const file of files) {\n const emoji = typeEmoji[file.type] || '📄';\n const sizeKB = (file.size / 1024).toFixed(1);\n const date = new Date(file.modified).toLocaleString();\n console.log(`${emoji} ${file.name}`);\n console.log(` Size: ${sizeKB} KB | Modified: ${date}`);\n }\n } catch (err) {\n console.error('Failed to list logs:', err);\n process.exit(1);\n }\n });\n\n // Query logs\n command\n .command('query')\n .description('Query log entries')\n .option('-l, --level <level>', 'Filter by level (trace|debug|info|warn|error|fatal)')\n .option('-m, --module <module>', 'Filter by module')\n .option('-e, --extension <ext>', 'Filter by extension')\n .option('-q, --search <text>', 'Text search')\n .option('--session-id <id>', 'Filter by session ID')\n .option('--request-id <id>', 'Filter by request ID')\n .option('--from <date>', 'Start date (ISO 8601)')\n .option('--to <date>', 'End date (ISO 8601)')\n .option('-n, --limit <number>', 'Number of results', '50')\n .option('--json', 'Output as JSON')\n .action(async (options) => {\n try {\n const query: LogQuery = {\n levels: options.level ? [options.level as 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'] : undefined,\n module: options.module,\n extension: options.extension,\n q: options.search,\n sessionId: options.sessionId,\n requestId: options.requestId,\n from: options.from,\n to: options.to,\n limit: parseInt(options.limit, 10),\n order: 'desc',\n };\n \n const entries = await queryLogs(query);\n \n if (options.json) {\n console.log(JSON.stringify({ entries }, null, 2));\n return;\n }\n \n if (entries.length === 0) {\n console.log('No matching log entries found.');\n return;\n }\n \n console.log(`\\nFound ${entries.length} log entry(ies):\\n`);\n \n for (const entry of entries) {\n const timestamp = new Date(entry.timestamp).toLocaleTimeString();\n const levelColor = getLevelColor(entry.level);\n const module = entry.module ? `[${entry.module}]` : '';\n const session = (entry as any).sessionId ? `{session:${(entry as any).sessionId.slice(0,8)}}` : '';\n \n console.log(`${timestamp} ${levelColor(entry.level)} ${module} ${session} ${entry.message}`);\n \n // Show context if present\n if (entry.requestId) {\n console.log(` ↳ requestId: ${entry.requestId}`);\n }\n if (entry.userId) {\n console.log(` ↳ userId: ${entry.userId}`);\n }\n }\n } catch (err) {\n console.error('Failed to query logs:', err);\n process.exit(1);\n }\n });\n\n // Show statistics\n command\n .command('stats')\n .description('Show log statistics')\n .option('--json', 'Output as JSON')\n .action(async (options) => {\n try {\n const stats = await fetchLogStats();\n const config = getLoggerConfig();\n \n if (options.json) {\n console.log(JSON.stringify({ config, stats }, null, 2));\n return;\n }\n \n console.log('\\n📊 Log Statistics\\n');\n console.log(`📁 Log directory: ${config.logDir}`);\n console.log(`📝 Retention: ${config.retentionDays} days`);\n console.log(`📏 Max file size: ${config.maxFileSizeMB} MB\\n`);\n \n console.log('By Level:');\n const byLevel = stats.byLevel as Record<string, number>;\n const total = Object.values(byLevel).reduce((a, b) => a + b, 0);\n for (const [level, count] of Object.entries(byLevel)) {\n const bar = '█'.repeat(Math.min(50, total > 0 ? Math.round((count / total) * 50) : 0));\n const percent = total > 0 ? ((count / total) * 100).toFixed(1) : '0';\n console.log(` ${level.padEnd(5)}: ${count.toString().padStart(6)} (${percent}%) ${bar}`);\n }\n \n console.log(`\\n📈 Total entries: ${total}`);\n } catch (err) {\n console.error('Failed to get stats:', err);\n process.exit(1);\n }\n });\n\n // Tail logs\n command\n .command('tail')\n .description('Tail log entries in real-time')\n .option('-n, --lines <number>', 'Number of lines to show', '20')\n .option('-f, --follow', 'Follow new entries (Ctrl+C to exit)', false)\n .option('-t, --type <type>', 'Filter by type (app|error|audit|access)', 'app')\n .action(async (options) => {\n try {\n const lines = parseInt(options.lines, 10);\n \n // Initial query\n const query: LogQuery = {\n limit: lines,\n order: 'desc',\n };\n \n const entries = await queryLogs(query);\n \n // Print in chronological order\n const reversed = [...entries].reverse();\n \n for (const entry of reversed) {\n printLogEntry(entry);\n }\n \n if (options.follow) {\n console.log('\\n⏳ Watching for new entries (Ctrl+C to exit)...');\n // Note: Full follow mode would require file watching\n // For now, just show initial results\n }\n } catch (err) {\n console.error('Failed to tail logs:', err);\n process.exit(1);\n }\n });\n\n // Clean old logs\n command\n .command('clean')\n .description('Clean old log files')\n .option('-d, --days <number>', 'Keep logs for N days', '7')\n .option('--dry-run', 'Show what would be deleted without actually deleting')\n .option('--json', 'Output as JSON')\n .action(async (options) => {\n try {\n const keepDays = parseInt(options.days, 10);\n \n if (options.dryRun) {\n console.log(`\\n🔍 Dry run: would delete logs older than ${keepDays} days\\n`);\n }\n \n const result = cleanOldLogs(keepDays);\n \n if (options.json) {\n console.log(JSON.stringify(result, null, 2));\n return;\n }\n \n if (result.deleted === 0) {\n console.log('✅ No old logs to clean.');\n return;\n }\n \n console.log(`\\n🧹 Cleaned ${result.deleted} log file(s)`);\n \n if (result.errors.length > 0) {\n console.log('\\n⚠️ Errors:');\n for (const err of result.errors) {\n console.log(` - ${err}`);\n }\n }\n } catch (err) {\n console.error('Failed to clean logs:', err);\n process.exit(1);\n }\n });\n\n // Rotate logs\n command\n .command('rotate')\n .description('Rotate large log files')\n .option('--dry-run', 'Show what would be rotated without rotating')\n .option('--json', 'Output as JSON')\n .action(async (options) => {\n try {\n if (options.dryRun) {\n console.log('\\n🔍 Dry run: would rotate large log files\\n');\n }\n \n const result = await rotateLogs();\n \n if (options.json) {\n console.log(JSON.stringify(result, null, 2));\n return;\n }\n \n if (result.rotated === 0) {\n console.log('✅ No logs need rotation.');\n return;\n }\n \n console.log(`\\n🔄 Rotated ${result.rotated} log file(s)`);\n console.log(` Compressed: ${result.compressed}`);\n \n if (result.errors.length > 0) {\n console.log('\\n⚠️ Errors:');\n for (const err of result.errors) {\n console.log(` - ${err}`);\n }\n }\n } catch (err) {\n console.error('Failed to rotate logs:', err);\n process.exit(1);\n }\n });\n\n return command;\n}\n\n// Helper functions\nfunction getLevelColor(level: string): (s: string) => string {\n const colors: Record<string, (s: string) => string> = {\n trace: (s) => `🔍 ${s}`,\n debug: (s) => `🔧 ${s}`,\n info: (s) => `ℹ️ ${s}`,\n warn: (s) => `⚠️ ${s}`,\n error: (s) => `❌ ${s}`,\n fatal: (s) => `🔥 ${s}`,\n };\n return colors[level] || ((s) => s);\n}\n\nfunction printLogEntry(entry: LogEntry): void {\n const timestamp = new Date(entry.timestamp).toLocaleTimeString();\n const levelColor = getLevelColor(entry.level);\n const module = entry.module ? `[${entry.module}]` : '';\n \n console.log(`${timestamp} ${levelColor(entry.level)} ${module} ${entry.message}`);\n}\n\n// Register command\nregister({\n id: 'logs',\n name: 'logs',\n description: 'Manage and query logs',\n factory: createLogsCommand,\n metadata: { category: 'maintenance' },\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"logs.js","names":[],"sources":["../../../../src/cli/commands/logs.ts"],"sourcesContent":["/**\n * Logs CLI Command\n * \n * Query and analyze logs: list, query, stats, tail, clean\n */\n\nimport { Command } from 'commander';\nimport { register, type CLIContext } from '../registry.js';\nimport {\n getLogFiles,\n queryLogs,\n getFileLogStats,\n type LogQuery,\n type LogEntry,\n} from '../../utils/logger/log-store.js';\nimport { getLoggerConfig } from '../../utils/logger/config.js';\nimport { rotateLogs, cleanOldLogs } from '../../utils/logger/rotation.js';\n\nfunction createLogsCommand(_ctx: CLIContext): Command {\n const command = new Command('logs');\n command.description('Manage and query logs');\n\n // List log files\n command\n .command('list')\n .description('List log files')\n .option('-t, --type <type>', 'Filter by type (app|error|audit|access)')\n .option('-s, --sort <field>', 'Sort by (name|size|date)', 'date')\n .option('--json', 'Output as JSON')\n .action(async (options) => {\n try {\n let files = getLogFiles();\n \n // Filter by type\n if (options.type) {\n files = files.filter(f => f.type === options.type);\n }\n \n // Sort\n const sortField = options.sort || 'date';\n files.sort((a, b) => {\n if (sortField === 'name') return a.name.localeCompare(b.name);\n if (sortField === 'size') return b.size - a.size;\n return new Date(b.modified).getTime() - new Date(a.modified).getTime();\n });\n \n if (options.json) {\n console.log(JSON.stringify({ files }, null, 2));\n return;\n }\n \n const config = getLoggerConfig();\n console.log(`\\n📁 Log directory: ${config.logDir}\\n`);\n \n if (files.length === 0) {\n console.log('No log files found.');\n return;\n }\n \n console.log(`Found ${files.length} log file(s):\\n`);\n \n const typeEmoji: Record<string, string> = {\n app: '📝',\n error: '❌',\n audit: '🔒',\n access: '🌐',\n };\n \n for (const file of files) {\n const emoji = typeEmoji[file.type] || '📄';\n const sizeKB = (file.size / 1024).toFixed(1);\n const date = new Date(file.modified).toLocaleString();\n console.log(`${emoji} ${file.name}`);\n console.log(` Size: ${sizeKB} KB | Modified: ${date}`);\n }\n } catch (err) {\n console.error('Failed to list logs:', err);\n process.exit(1);\n }\n });\n\n // Query logs\n command\n .command('query')\n .description('Query log entries')\n .option('-l, --level <level>', 'Filter by level (trace|debug|info|warn|error|fatal)')\n .option('-m, --module <module>', 'Filter by module')\n .option('-e, --extension <ext>', 'Filter by extension')\n .option('-q, --search <text>', 'Text search')\n .option('--session-id <id>', 'Filter by session ID')\n .option('--request-id <id>', 'Filter by request ID')\n .option('--from <date>', 'Start date (ISO 8601)')\n .option('--to <date>', 'End date (ISO 8601)')\n .option('-n, --limit <number>', 'Number of results', '50')\n .option('--json', 'Output as JSON')\n .action(async (options) => {\n try {\n const query: LogQuery = {\n levels: options.level ? [options.level as 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'] : undefined,\n module: options.module,\n extension: options.extension,\n q: options.search,\n sessionId: options.sessionId,\n requestId: options.requestId,\n from: options.from,\n to: options.to,\n limit: parseInt(options.limit, 10),\n order: 'desc',\n };\n \n const entries = await queryLogs(query);\n \n if (options.json) {\n console.log(JSON.stringify({ entries }, null, 2));\n return;\n }\n \n if (entries.length === 0) {\n console.log('No matching log entries found.');\n return;\n }\n \n console.log(`\\nFound ${entries.length} log entry(ies):\\n`);\n \n for (const entry of entries) {\n const timestamp = new Date(entry.timestamp).toLocaleTimeString();\n const levelColor = getLevelColor(entry.level);\n const module = entry.module ? `[${entry.module}]` : '';\n const session = (entry as any).sessionId ? `{session:${(entry as any).sessionId.slice(0,8)}}` : '';\n \n console.log(`${timestamp} ${levelColor(entry.level)} ${module} ${session} ${entry.message}`);\n \n // Show context if present\n if (entry.requestId) {\n console.log(` ↳ requestId: ${entry.requestId}`);\n }\n if (entry.userId) {\n console.log(` ↳ userId: ${entry.userId}`);\n }\n }\n } catch (err) {\n console.error('Failed to query logs:', err);\n process.exit(1);\n }\n });\n\n // Show statistics\n command\n .command('stats')\n .description('Show log statistics')\n .option('--json', 'Output as JSON')\n .action(async (options) => {\n try {\n const stats = await getFileLogStats();\n const config = getLoggerConfig();\n \n if (options.json) {\n console.log(JSON.stringify({ config, stats }, null, 2));\n return;\n }\n \n console.log('\\n📊 Log Statistics\\n');\n console.log(`📁 Log directory: ${config.logDir}`);\n console.log(`📝 Retention: ${config.retentionDays} days`);\n console.log(`📏 Max file size: ${config.maxFileSizeMB} MB\\n`);\n \n console.log('By Level:');\n const byLevel = stats.byLevel as Record<string, number>;\n const total = Object.values(byLevel).reduce((a, b) => a + b, 0);\n for (const [level, count] of Object.entries(byLevel)) {\n const bar = '█'.repeat(Math.min(50, total > 0 ? Math.round((count / total) * 50) : 0));\n const percent = total > 0 ? ((count / total) * 100).toFixed(1) : '0';\n console.log(` ${level.padEnd(5)}: ${count.toString().padStart(6)} (${percent}%) ${bar}`);\n }\n \n console.log(`\\n📈 Total entries: ${total}`);\n } catch (err) {\n console.error('Failed to get stats:', err);\n process.exit(1);\n }\n });\n\n // Tail logs\n command\n .command('tail')\n .description('Tail log entries in real-time')\n .option('-n, --lines <number>', 'Number of lines to show', '20')\n .option('-f, --follow', 'Follow new entries (Ctrl+C to exit)', false)\n .option('-t, --type <type>', 'Filter by type (app|error|audit|access)', 'app')\n .action(async (options) => {\n try {\n const lines = parseInt(options.lines, 10);\n \n // Initial query\n const query: LogQuery = {\n limit: lines,\n order: 'desc',\n };\n \n const entries = await queryLogs(query);\n \n // Print in chronological order\n const reversed = [...entries].reverse();\n \n for (const entry of reversed) {\n printLogEntry(entry);\n }\n \n if (options.follow) {\n console.log('\\n⏳ Watching for new entries (Ctrl+C to exit)...');\n // Note: Full follow mode would require file watching\n // For now, just show initial results\n }\n } catch (err) {\n console.error('Failed to tail logs:', err);\n process.exit(1);\n }\n });\n\n // Clean old logs\n command\n .command('clean')\n .description('Clean old log files')\n .option('-d, --days <number>', 'Keep logs for N days', '7')\n .option('--dry-run', 'Show what would be deleted without actually deleting')\n .option('--json', 'Output as JSON')\n .action(async (options) => {\n try {\n const keepDays = parseInt(options.days, 10);\n \n if (options.dryRun) {\n console.log(`\\n🔍 Dry run: would delete logs older than ${keepDays} days\\n`);\n }\n \n const result = cleanOldLogs(keepDays);\n \n if (options.json) {\n console.log(JSON.stringify(result, null, 2));\n return;\n }\n \n if (result.deleted === 0) {\n console.log('✅ No old logs to clean.');\n return;\n }\n \n console.log(`\\n🧹 Cleaned ${result.deleted} log file(s)`);\n \n if (result.errors.length > 0) {\n console.log('\\n⚠️ Errors:');\n for (const err of result.errors) {\n console.log(` - ${err}`);\n }\n }\n } catch (err) {\n console.error('Failed to clean logs:', err);\n process.exit(1);\n }\n });\n\n // Rotate logs\n command\n .command('rotate')\n .description('Rotate large log files')\n .option('--dry-run', 'Show what would be rotated without rotating')\n .option('--json', 'Output as JSON')\n .action(async (options) => {\n try {\n if (options.dryRun) {\n console.log('\\n🔍 Dry run: would rotate large log files\\n');\n }\n \n const result = await rotateLogs();\n \n if (options.json) {\n console.log(JSON.stringify(result, null, 2));\n return;\n }\n \n if (result.rotated === 0) {\n console.log('✅ No logs need rotation.');\n return;\n }\n \n console.log(`\\n🔄 Rotated ${result.rotated} log file(s)`);\n console.log(` Compressed: ${result.compressed}`);\n \n if (result.errors.length > 0) {\n console.log('\\n⚠️ Errors:');\n for (const err of result.errors) {\n console.log(` - ${err}`);\n }\n }\n } catch (err) {\n console.error('Failed to rotate logs:', err);\n process.exit(1);\n }\n });\n\n return command;\n}\n\n// Helper functions\nfunction getLevelColor(level: string): (s: string) => string {\n const colors: Record<string, (s: string) => string> = {\n trace: (s) => `🔍 ${s}`,\n debug: (s) => `🔧 ${s}`,\n info: (s) => `ℹ️ ${s}`,\n warn: (s) => `⚠️ ${s}`,\n error: (s) => `❌ ${s}`,\n fatal: (s) => `🔥 ${s}`,\n };\n return colors[level] || ((s) => s);\n}\n\nfunction printLogEntry(entry: LogEntry): void {\n const timestamp = new Date(entry.timestamp).toLocaleTimeString();\n const levelColor = getLevelColor(entry.level);\n const module = entry.module ? `[${entry.module}]` : '';\n \n console.log(`${timestamp} ${levelColor(entry.level)} ${module} ${entry.message}`);\n}\n\n// Register command\nregister({\n id: 'logs',\n name: 'logs',\n description: 'Manage and query logs',\n factory: createLogsCommand,\n metadata: { category: 'maintenance' },\n});\n"],"mappings":"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"}
@@ -120,8 +120,9 @@ var DefaultJobExecutor = class {
120
120
  else log.error({
121
121
  jobId: job.id,
122
122
  executionId,
123
- error: result.error
124
- }, "Job failed");
123
+ errorMessage: result.error,
124
+ phase: "cron.execute"
125
+ }, `Job failed: ${result.error ?? "unknown"}`);
125
126
  if (result.status === "ok" && this.heartbeatService) try {
126
127
  this.heartbeatService.requestNow({ reason: `cron:${job.id}` });
127
128
  } catch (e) {
@@ -136,10 +137,12 @@ var DefaultJobExecutor = class {
136
137
  execution.duration = Date.now() - new Date(execution.startedAt).getTime();
137
138
  execution.error = error instanceof Error ? error.message : String(error);
138
139
  log.error({
140
+ err: error,
141
+ errorMessage: execution.error,
139
142
  jobId: job.id,
140
143
  executionId,
141
- error: execution.error
142
- }, "Job execution error");
144
+ phase: "cron.execute"
145
+ }, `Job execution error: ${execution.error}`);
143
146
  result = {
144
147
  status: "error",
145
148
  error: execution.error