@kolisachint/hoocode-agent 0.4.110 → 0.4.112

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 (108) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/dist/cli/args.d.ts +1 -0
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +8 -3
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/extensions/loader.d.ts +10 -1
  7. package/dist/core/extensions/loader.d.ts.map +1 -1
  8. package/dist/core/extensions/loader.js +17 -2
  9. package/dist/core/extensions/loader.js.map +1 -1
  10. package/dist/core/extensions/plugins/marketplace.d.ts +36 -5
  11. package/dist/core/extensions/plugins/marketplace.d.ts.map +1 -1
  12. package/dist/core/extensions/plugins/marketplace.js +63 -17
  13. package/dist/core/extensions/plugins/marketplace.js.map +1 -1
  14. package/dist/core/format-duration.d.ts +7 -0
  15. package/dist/core/format-duration.d.ts.map +1 -0
  16. package/dist/core/format-duration.js +16 -0
  17. package/dist/core/format-duration.js.map +1 -0
  18. package/dist/core/keybindings.d.ts +10 -0
  19. package/dist/core/keybindings.d.ts.map +1 -1
  20. package/dist/core/keybindings.js +10 -0
  21. package/dist/core/keybindings.js.map +1 -1
  22. package/dist/core/lifeguard.d.ts +15 -0
  23. package/dist/core/lifeguard.d.ts.map +1 -1
  24. package/dist/core/lifeguard.js +48 -18
  25. package/dist/core/lifeguard.js.map +1 -1
  26. package/dist/core/scheduler.d.ts +8 -2
  27. package/dist/core/scheduler.d.ts.map +1 -1
  28. package/dist/core/scheduler.js +9 -3
  29. package/dist/core/scheduler.js.map +1 -1
  30. package/dist/core/settings-defaults.d.ts +1 -1
  31. package/dist/core/settings-defaults.d.ts.map +1 -1
  32. package/dist/core/settings-defaults.js +2 -2
  33. package/dist/core/settings-defaults.js.map +1 -1
  34. package/dist/core/settings-types.d.ts.map +1 -1
  35. package/dist/core/settings-types.js.map +1 -1
  36. package/dist/core/subagent-inbox.d.ts +1 -1
  37. package/dist/core/subagent-inbox.d.ts.map +1 -1
  38. package/dist/core/subagent-inbox.js +2 -0
  39. package/dist/core/subagent-inbox.js.map +1 -1
  40. package/dist/core/subagent-pool-instance.d.ts.map +1 -1
  41. package/dist/core/subagent-pool-instance.js +18 -11
  42. package/dist/core/subagent-pool-instance.js.map +1 -1
  43. package/dist/core/subagent-pool.d.ts +25 -3
  44. package/dist/core/subagent-pool.d.ts.map +1 -1
  45. package/dist/core/subagent-pool.js +120 -25
  46. package/dist/core/subagent-pool.js.map +1 -1
  47. package/dist/core/subagent-result.d.ts.map +1 -1
  48. package/dist/core/subagent-result.js +6 -4
  49. package/dist/core/subagent-result.js.map +1 -1
  50. package/dist/core/task-store.d.ts +45 -3
  51. package/dist/core/task-store.d.ts.map +1 -1
  52. package/dist/core/task-store.js +51 -5
  53. package/dist/core/task-store.js.map +1 -1
  54. package/dist/core/team-view.d.ts.map +1 -1
  55. package/dist/core/team-view.js +2 -0
  56. package/dist/core/team-view.js.map +1 -1
  57. package/dist/core/token-budget.d.ts +13 -1
  58. package/dist/core/token-budget.d.ts.map +1 -1
  59. package/dist/core/token-budget.js +18 -1
  60. package/dist/core/token-budget.js.map +1 -1
  61. package/dist/core/tools/subagent.d.ts +16 -4
  62. package/dist/core/tools/subagent.d.ts.map +1 -1
  63. package/dist/core/tools/subagent.js +211 -67
  64. package/dist/core/tools/subagent.js.map +1 -1
  65. package/dist/core/tools/todo.d.ts.map +1 -1
  66. package/dist/core/tools/todo.js +39 -7
  67. package/dist/core/tools/todo.js.map +1 -1
  68. package/dist/extensions/core/loop.d.ts.map +1 -1
  69. package/dist/extensions/core/loop.js +4 -1
  70. package/dist/extensions/core/loop.js.map +1 -1
  71. package/dist/extensions/core/marketplace.d.ts +5 -3
  72. package/dist/extensions/core/marketplace.d.ts.map +1 -1
  73. package/dist/extensions/core/marketplace.js +37 -15
  74. package/dist/extensions/core/marketplace.js.map +1 -1
  75. package/dist/modes/interactive/components/keybinding-hints.d.ts +10 -0
  76. package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
  77. package/dist/modes/interactive/components/keybinding-hints.js +27 -1
  78. package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
  79. package/dist/modes/interactive/components/task-panel.d.ts +22 -1
  80. package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
  81. package/dist/modes/interactive/components/task-panel.js +302 -80
  82. package/dist/modes/interactive/components/task-panel.js.map +1 -1
  83. package/dist/modes/interactive/components/team-attach-panel.d.ts.map +1 -1
  84. package/dist/modes/interactive/components/team-attach-panel.js +15 -5
  85. package/dist/modes/interactive/components/team-attach-panel.js.map +1 -1
  86. package/dist/modes/interactive/theme/dark.json +8 -1
  87. package/dist/modes/interactive/theme/light.json +8 -1
  88. package/dist/modes/interactive/theme/theme-schema.json +28 -1
  89. package/dist/modes/interactive/theme/theme.d.ts +12 -1
  90. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  91. package/dist/modes/interactive/theme/theme.js +52 -0
  92. package/dist/modes/interactive/theme/theme.js.map +1 -1
  93. package/dist/modes/interactive/voice/voice-panel.d.ts.map +1 -1
  94. package/dist/modes/interactive/voice/voice-panel.js +5 -2
  95. package/dist/modes/interactive/voice/voice-panel.js.map +1 -1
  96. package/dist/modes/rpc/jsonl.d.ts +16 -1
  97. package/dist/modes/rpc/jsonl.d.ts.map +1 -1
  98. package/dist/modes/rpc/jsonl.js +53 -8
  99. package/dist/modes/rpc/jsonl.js.map +1 -1
  100. package/dist/utils/atomic-file.d.ts +13 -0
  101. package/dist/utils/atomic-file.d.ts.map +1 -0
  102. package/dist/utils/atomic-file.js +26 -0
  103. package/dist/utils/atomic-file.js.map +1 -0
  104. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  105. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  106. package/examples/extensions/sandbox/package.json +1 -1
  107. package/examples/extensions/with-deps/package.json +1 -1
  108. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"lifeguard.js","sourceRoot":"","sources":["../../src/core/lifeguard.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ChildProcess } from \"node:child_process\";\nimport { EventEmitter } from \"node:events\";\nimport { existsSync, readdirSync, readFileSync, rmdirSync, statSync, unlinkSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { getDispatchRoot } from \"../config.js\";\n\nconst TIMEOUTS_MS: Record<string, number> = {\n\texplore: 5 * 60 * 1000,\n\tedit: 10 * 60 * 1000,\n\ttest: 10 * 60 * 1000,\n\treview: 8 * 60 * 1000,\n\tdoc: 5 * 60 * 1000,\n};\n\nconst HEARTBEAT_MISS_THRESHOLD_MS = 60000;\nconst HEARTBEAT_CHECK_INTERVAL_MS = 5000;\nconst PARENT_SHUTDOWN_GRACE_MS = 5000;\n\n/**\n * Concurrency tolerance. Each *additional* concurrently-monitored subagent adds\n * this fraction to the heartbeat-miss and hard-timeout budgets.\n *\n * When several subagents run at once (plus background MCP tools), they saturate\n * the CPU and starve the parent's event loop: its `setInterval` heartbeat check\n * fires late, and it cannot read a child's `{\"ping\":true}` line in time even\n * though the child is healthy and still working. Scaling the budgets by load\n * stops that contention from false-positive SIGKILLing healthy subagents — the\n * failure the demo hit when running many agents + MCP tools in the background.\n */\nconst LOAD_TOLERANCE_PER_PROCESS = 0.5;\n\n/**\n * Hard ceiling on the load tolerance multiplier, so a genuinely stuck subagent\n * is still eventually reaped no matter how busy the pool is.\n */\nconst MAX_LOAD_MULTIPLIER = 4;\n\nexport interface LifeguardProcess {\n\tpid: number;\n\ttask_id: string;\n\tagent_type: string;\n\tprocess: ChildProcess;\n}\n\n/**\n * Monitors running subagent processes for heartbeats, hard timeouts,\n * and parent-exit cleanup. Emits \"stalled\" and \"timeout\" events when\n * processes are terminated.\n */\nexport class SubagentLifeguard extends EventEmitter {\n\tprivate processes = new Map<string, LifeguardProcess>();\n\tprivate lastHeartbeat = new Map<string, number>();\n\tprivate timeouts = new Map<string, NodeJS.Timeout>();\n\t/** When each task started, used to compute the load-scaled hard timeout. */\n\tprivate startedAt = new Map<string, number>();\n\t/** Per-agent base hard timeout (before load scaling), captured at monitor(). */\n\tprivate baseTimeoutMs = new Map<string, number>();\n\tprivate checkInterval: NodeJS.Timeout | null = null;\n\t/** Wall-clock time the heartbeat check last ran, to measure event-loop lag. */\n\tprivate lastCheckAt = Date.now();\n\t/**\n\t * Count of external in-process tasks (e.g. background MCP tools) running in the\n\t * parent alongside the monitored subagents. These don't show up in `processes`\n\t * but still saturate the parent's CPU/event loop, so they're folded into the\n\t * load multiplier. Updated by the pool via setExternalLoad().\n\t */\n\tprivate externalLoad = 0;\n\tprivate disposed = false;\n\tprivate readonly cwd: string;\n\tprivate parentShutdownHandler?: () => void;\n\n\tconstructor(cwd: string) {\n\t\tsuper();\n\t\tthis.cwd = cwd;\n\t\tthis.setupParentExitHandlers();\n\t\tthis.sweepOldAgents();\n\t\tthis.lastCheckAt = Date.now();\n\t\tthis.checkInterval = setInterval(() => this.checkHeartbeats(), HEARTBEAT_CHECK_INTERVAL_MS);\n\t}\n\n\t/**\n\t * Set the count of external in-process tasks (background MCP tools) sharing the\n\t * parent's CPU/event loop. Folded into loadMultiplier() so concurrent MCP work\n\t * widens the heartbeat/timeout budgets just like extra monitored subagents do.\n\t * Negative values are clamped to 0.\n\t */\n\tsetExternalLoad(count: number): void {\n\t\tthis.externalLoad = Math.max(0, Math.floor(count));\n\t}\n\n\t/**\n\t * Tolerance multiplier for the current load. 1 concurrent task → 1x; each extra\n\t * concurrent task adds LOAD_TOLERANCE_PER_PROCESS, capped at MAX_LOAD_MULTIPLIER.\n\t * Concurrency counts both monitored subagents and external load (background MCP\n\t * tools), since both contend for the same parent event loop.\n\t */\n\tprivate loadMultiplier(): number {\n\t\tconst concurrent = this.processes.size + this.externalLoad;\n\t\tconst mult = 1 + Math.max(0, concurrent - 1) * LOAD_TOLERANCE_PER_PROCESS;\n\t\treturn Math.min(mult, MAX_LOAD_MULTIPLIER);\n\t}\n\n\t/**\n\t * Begin monitoring a child process. The process must emit a\n\t * `{\"ping\":true}` JSON line on stdout every 30 seconds.\n\t */\n\tmonitor(task_id: string, agent_type: string, proc: ChildProcess): void {\n\t\tif (this.disposed) return;\n\n\t\tconst pid = proc.pid ?? 0;\n\t\tthis.processes.set(task_id, { pid, task_id, agent_type, process: proc });\n\t\tthis.lastHeartbeat.set(task_id, Date.now());\n\n\t\tconst timeoutMs = TIMEOUTS_MS[agent_type] ?? TIMEOUTS_MS.explore;\n\t\tthis.startedAt.set(task_id, Date.now());\n\t\tthis.baseTimeoutMs.set(task_id, timeoutMs);\n\t\t// Arm the hard timeout scaled by current load. When it fires, handleTimeout\n\t\t// re-checks load and re-arms (up to MAX_LOAD_MULTIPLIER) if the pool is still\n\t\t// busy, so a slow-but-progressing subagent isn't killed for CPU contention.\n\t\tconst timeout = setTimeout(\n\t\t\t() => {\n\t\t\t\tthis.handleTimeout(task_id);\n\t\t\t},\n\t\t\tMath.round(timeoutMs * this.loadMultiplier()),\n\t\t);\n\t\tthis.timeouts.set(task_id, timeout);\n\n\t\tproc.once(\"exit\", () => {\n\t\t\tthis.untrack(task_id);\n\t\t});\n\t}\n\n\t/** Record a heartbeat for a monitored task. */\n\trecordHeartbeat(task_id: string): void {\n\t\tif (this.processes.has(task_id)) {\n\t\t\tthis.lastHeartbeat.set(task_id, Date.now());\n\t\t}\n\t}\n\n\t/** Get the last recorded heartbeat timestamp, or null. */\n\tlastHeartbeatAt(task_id: string): number | null {\n\t\treturn this.lastHeartbeat.get(task_id) ?? null;\n\t}\n\n\t/** True if the task is currently being monitored. */\n\tisMonitoring(task_id: string): boolean {\n\t\treturn this.processes.has(task_id);\n\t}\n\n\t/** Kill all monitored processes and clean up. */\n\tdispose(): void {\n\t\tif (this.disposed) return;\n\t\tthis.disposed = true;\n\n\t\tif (this.checkInterval) {\n\t\t\tclearInterval(this.checkInterval);\n\t\t\tthis.checkInterval = null;\n\t\t}\n\n\t\tfor (const timeout of this.timeouts.values()) {\n\t\t\tclearTimeout(timeout);\n\t\t}\n\t\tthis.timeouts.clear();\n\n\t\tfor (const monitored of this.processes.values()) {\n\t\t\tif (!monitored.process.killed) {\n\t\t\t\tmonitored.process.kill(\"SIGKILL\");\n\t\t\t}\n\t\t}\n\t\tthis.processes.clear();\n\t\tthis.lastHeartbeat.clear();\n\t\tthis.startedAt.clear();\n\t\tthis.baseTimeoutMs.clear();\n\t\tthis.removeAllListeners();\n\n\t\tif (this.parentShutdownHandler) {\n\t\t\tprocess.removeListener(\"SIGINT\", this.parentShutdownHandler);\n\t\t\tprocess.removeListener(\"SIGTERM\", this.parentShutdownHandler);\n\t\t}\n\t}\n\n\tprivate checkHeartbeats(): void {\n\t\tconst now = Date.now();\n\t\t// Event-loop lag: how much later than scheduled this check actually ran.\n\t\t// Heavy CPU load (many concurrent subagents + background MCP tools) starves\n\t\t// the loop, delaying both this check *and* our reading of child heartbeats.\n\t\t// Forgive that gap so the parent's own starvation isn't charged against the\n\t\t// children as a missed heartbeat.\n\t\tconst loopLag = Math.max(0, now - this.lastCheckAt - HEARTBEAT_CHECK_INTERVAL_MS);\n\t\tthis.lastCheckAt = now;\n\n\t\tconst threshold = HEARTBEAT_MISS_THRESHOLD_MS * this.loadMultiplier() + loopLag;\n\t\tfor (const [task_id] of this.processes) {\n\t\t\tconst last = this.lastHeartbeat.get(task_id);\n\t\t\tif (last === undefined) continue;\n\t\t\tif (now - last > threshold) {\n\t\t\t\tthis.handleStalled(task_id);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate handleStalled(task_id: string): void {\n\t\tconst monitored = this.processes.get(task_id);\n\t\tif (!monitored) return;\n\n\t\t// Record why we reaped this child so a recurrence is diagnosable rather than\n\t\t// just \"stalled\": how long since the last heartbeat, and the load factors\n\t\t// (concurrent monitored subagents, threshold) that fed the decision.\n\t\tconst last = this.lastHeartbeat.get(task_id);\n\t\tconst silentMs = last === undefined ? -1 : Date.now() - last;\n\t\tconsole.error(\n\t\t\t`[LIFEGUARD] stalled task_id=${task_id} agent=${monitored.agent_type} ` +\n\t\t\t\t`silent_ms=${silentMs} concurrent=${this.processes.size} ` +\n\t\t\t\t`load_mult=${this.loadMultiplier().toFixed(2)} base_threshold_ms=${HEARTBEAT_MISS_THRESHOLD_MS}`,\n\t\t);\n\n\t\tif (!monitored.process.killed) {\n\t\t\tmonitored.process.kill(\"SIGKILL\");\n\t\t}\n\n\t\tthis.emit(\"stalled\", { task_id, pid: monitored.pid });\n\t\t// Process exit handler will call untrack()\n\t}\n\n\tprivate handleTimeout(task_id: string): void {\n\t\tconst monitored = this.processes.get(task_id);\n\t\tif (!monitored) return;\n\n\t\t// Under load the wall-clock timer can fire while the subagent is still doing\n\t\t// real work — its turns are just slow because the CPU is shared. Re-arm rather\n\t\t// than kill, up to a hard ceiling (base * MAX_LOAD_MULTIPLIER) so a genuinely\n\t\t// stuck agent still terminates.\n\t\tconst started = this.startedAt.get(task_id) ?? Date.now();\n\t\tconst base = this.baseTimeoutMs.get(task_id) ?? TIMEOUTS_MS[monitored.agent_type] ?? TIMEOUTS_MS.explore;\n\t\tconst elapsed = Date.now() - started;\n\t\tconst ceiling = base * MAX_LOAD_MULTIPLIER;\n\t\tif (this.loadMultiplier() > 1 && elapsed < ceiling) {\n\t\t\tconst remaining = ceiling - elapsed;\n\t\t\tconst next = Math.min(\n\t\t\t\tMath.round(base * this.loadMultiplier()),\n\t\t\t\tMath.max(HEARTBEAT_CHECK_INTERVAL_MS, remaining),\n\t\t\t);\n\t\t\tthis.timeouts.set(\n\t\t\t\ttask_id,\n\t\t\t\tsetTimeout(() => this.handleTimeout(task_id), next),\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tif (!monitored.process.killed) {\n\t\t\tmonitored.process.kill(\"SIGKILL\");\n\t\t}\n\n\t\tthis.emit(\"timeout\", { task_id, pid: monitored.pid });\n\t\tthis.timeouts.delete(task_id);\n\t\t// Process exit handler will call untrack()\n\t}\n\n\tprivate untrack(task_id: string): void {\n\t\tconst timeout = this.timeouts.get(task_id);\n\t\tif (timeout) {\n\t\t\tclearTimeout(timeout);\n\t\t\tthis.timeouts.delete(task_id);\n\t\t}\n\t\tthis.processes.delete(task_id);\n\t\tthis.lastHeartbeat.delete(task_id);\n\t\tthis.startedAt.delete(task_id);\n\t\tthis.baseTimeoutMs.delete(task_id);\n\t}\n\n\tprivate setupParentExitHandlers(): void {\n\t\tconst shutdown = () => this.gracefulShutdown();\n\t\tthis.parentShutdownHandler = shutdown;\n\t\tprocess.setMaxListeners(Math.max(process.getMaxListeners(), 20));\n\t\tprocess.once(\"SIGINT\", shutdown);\n\t\tprocess.once(\"SIGTERM\", shutdown);\n\t}\n\n\tprivate gracefulShutdown(): void {\n\t\t// SIGTERM all children\n\t\tfor (const monitored of this.processes.values()) {\n\t\t\tif (!monitored.process.killed) {\n\t\t\t\tmonitored.process.kill(\"SIGTERM\");\n\t\t\t}\n\t\t}\n\n\t\t// SIGKILL after grace period\n\t\tsetTimeout(() => {\n\t\t\tfor (const monitored of this.processes.values()) {\n\t\t\t\tif (!monitored.process.killed) {\n\t\t\t\t\tmonitored.process.kill(\"SIGKILL\");\n\t\t\t\t}\n\t\t\t}\n\t\t}, PARENT_SHUTDOWN_GRACE_MS).unref();\n\t}\n\n\tprivate sweepOldAgents(): void {\n\t\tconst dispatchDir = getDispatchRoot(this.cwd);\n\t\tif (!existsSync(dispatchDir)) return;\n\n\t\tconst now = Date.now();\n\t\tconst cutoff = 24 * 60 * 60 * 1000; // 24 hours\n\n\t\tfor (const entry of readdirSync(dispatchDir)) {\n\t\t\tconst entryPath = join(dispatchDir, entry);\n\t\t\ttry {\n\t\t\t\tconst stats = statSync(entryPath);\n\t\t\t\tif (!stats.isDirectory()) continue;\n\n\t\t\t\tif (now - stats.mtimeMs > cutoff) {\n\t\t\t\t\tconst hasRunningPid = this.hasRunningPid(entryPath);\n\t\t\t\t\tif (!hasRunningPid) {\n\t\t\t\t\t\tthis.rmrf(entryPath);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Ignore errors for individual entries\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate hasRunningPid(dir: string): boolean {\n\t\tconst pidFile = join(dir, \"pid\");\n\t\tif (!existsSync(pidFile)) return false;\n\n\t\ttry {\n\t\t\tconst pid = Number.parseInt(readFileSync(pidFile, \"utf-8\"), 10);\n\t\t\tif (Number.isNaN(pid)) return false;\n\t\t\tprocess.kill(pid, 0); // Check if process exists\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate rmrf(dir: string): void {\n\t\tfor (const entry of readdirSync(dir)) {\n\t\t\tconst entryPath = join(dir, entry);\n\t\t\ttry {\n\t\t\t\tconst stats = statSync(entryPath);\n\t\t\t\tif (stats.isDirectory()) {\n\t\t\t\t\tthis.rmrf(entryPath);\n\t\t\t\t} else {\n\t\t\t\t\tunlinkSync(entryPath);\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Ignore\n\t\t\t}\n\t\t}\n\t\ttry {\n\t\t\trmdirSync(dir);\n\t\t} catch {\n\t\t\t// Ignore\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"lifeguard.js","sourceRoot":"","sources":["../../src/core/lifeguard.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ChildProcess } from \"node:child_process\";\nimport { EventEmitter } from \"node:events\";\nimport { existsSync, readdirSync, readFileSync, rmdirSync, statSync, unlinkSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { getDispatchRoot } from \"../config.js\";\nimport { killProcessTree } from \"../utils/shell.js\";\n\nconst TIMEOUTS_MS: Record<string, number> = {\n\texplore: 5 * 60 * 1000,\n\tedit: 10 * 60 * 1000,\n\ttest: 10 * 60 * 1000,\n\treview: 8 * 60 * 1000,\n\tdoc: 5 * 60 * 1000,\n};\n\nconst HEARTBEAT_MISS_THRESHOLD_MS = 60000;\nconst HEARTBEAT_CHECK_INTERVAL_MS = 5000;\nconst PARENT_SHUTDOWN_GRACE_MS = 5000;\n\n/**\n * Concurrency tolerance. Each *additional* concurrently-monitored subagent adds\n * this fraction to the heartbeat-miss and hard-timeout budgets.\n *\n * When several subagents run at once (plus background MCP tools), they saturate\n * the CPU and starve the parent's event loop: its `setInterval` heartbeat check\n * fires late, and it cannot read a child's `{\"ping\":true}` line in time even\n * though the child is healthy and still working. Scaling the budgets by load\n * stops that contention from false-positive SIGKILLing healthy subagents — the\n * failure the demo hit when running many agents + MCP tools in the background.\n */\nconst LOAD_TOLERANCE_PER_PROCESS = 0.5;\n\n/**\n * Hard ceiling on the load tolerance multiplier, so a genuinely stuck subagent\n * is still eventually reaped no matter how busy the pool is.\n */\nconst MAX_LOAD_MULTIPLIER = 4;\n\nexport interface LifeguardProcess {\n\tpid: number;\n\ttask_id: string;\n\tagent_type: string;\n\tprocess: ChildProcess;\n}\n\n/**\n * Monitors running subagent processes for heartbeats, hard timeouts,\n * and parent-exit cleanup. Emits \"stalled\" and \"timeout\" events when\n * processes are terminated.\n */\nexport class SubagentLifeguard extends EventEmitter {\n\tprivate processes = new Map<string, LifeguardProcess>();\n\tprivate lastHeartbeat = new Map<string, number>();\n\tprivate timeouts = new Map<string, NodeJS.Timeout>();\n\t/** When each task started, used to compute the load-scaled hard timeout. */\n\tprivate startedAt = new Map<string, number>();\n\t/** Per-agent base hard timeout (before load scaling), captured at monitor(). */\n\tprivate baseTimeoutMs = new Map<string, number>();\n\tprivate checkInterval: NodeJS.Timeout | null = null;\n\t/** Wall-clock time the heartbeat check last ran, to measure event-loop lag. */\n\tprivate lastCheckAt = Date.now();\n\t/**\n\t * Count of external in-process tasks (e.g. background MCP tools) running in the\n\t * parent alongside the monitored subagents. These don't show up in `processes`\n\t * but still saturate the parent's CPU/event loop, so they're folded into the\n\t * load multiplier. Updated by the pool via setExternalLoad().\n\t */\n\tprivate externalLoad = 0;\n\t/**\n\t * Tasks already reaped (stalled/timeout kill sent) but whose `exit` has not\n\t * fired yet. The heartbeat check runs every 5s, so without this a stalled task\n\t * would re-emit \"stalled\" (and re-kill) on every tick until the process exits.\n\t */\n\tprivate reaping = new Set<string>();\n\tprivate disposed = false;\n\tprivate readonly cwd: string;\n\tprivate parentShutdownHandler?: () => void;\n\n\tconstructor(cwd: string) {\n\t\tsuper();\n\t\tthis.cwd = cwd;\n\t\tthis.setupParentExitHandlers();\n\t\tthis.sweepOldAgents();\n\t\tthis.lastCheckAt = Date.now();\n\t\tthis.checkInterval = setInterval(() => this.checkHeartbeats(), HEARTBEAT_CHECK_INTERVAL_MS);\n\t}\n\n\t/**\n\t * Set the count of external in-process tasks (background MCP tools) sharing the\n\t * parent's CPU/event loop. Folded into loadMultiplier() so concurrent MCP work\n\t * widens the heartbeat/timeout budgets just like extra monitored subagents do.\n\t * Negative values are clamped to 0.\n\t */\n\tsetExternalLoad(count: number): void {\n\t\tthis.externalLoad = Math.max(0, Math.floor(count));\n\t}\n\n\t/**\n\t * Tolerance multiplier for the current load. 1 concurrent task → 1x; each extra\n\t * concurrent task adds LOAD_TOLERANCE_PER_PROCESS, capped at MAX_LOAD_MULTIPLIER.\n\t * Concurrency counts both monitored subagents and external load (background MCP\n\t * tools), since both contend for the same parent event loop.\n\t */\n\tprivate loadMultiplier(): number {\n\t\tconst concurrent = this.processes.size + this.externalLoad;\n\t\tconst mult = 1 + Math.max(0, concurrent - 1) * LOAD_TOLERANCE_PER_PROCESS;\n\t\treturn Math.min(mult, MAX_LOAD_MULTIPLIER);\n\t}\n\n\t/**\n\t * Begin monitoring a child process. The process must emit a\n\t * `{\"ping\":true}` JSON line on stdout every 30 seconds.\n\t */\n\tmonitor(task_id: string, agent_type: string, proc: ChildProcess): void {\n\t\tif (this.disposed) return;\n\n\t\tconst pid = proc.pid ?? 0;\n\t\tthis.processes.set(task_id, { pid, task_id, agent_type, process: proc });\n\t\tthis.lastHeartbeat.set(task_id, Date.now());\n\n\t\tconst timeoutMs = TIMEOUTS_MS[agent_type] ?? TIMEOUTS_MS.explore;\n\t\tthis.startedAt.set(task_id, Date.now());\n\t\tthis.baseTimeoutMs.set(task_id, timeoutMs);\n\t\t// Arm the hard timeout scaled by current load. When it fires, handleTimeout\n\t\t// re-checks load and re-arms (up to MAX_LOAD_MULTIPLIER) if the pool is still\n\t\t// busy, so a slow-but-progressing subagent isn't killed for CPU contention.\n\t\tconst timeout = setTimeout(\n\t\t\t() => {\n\t\t\t\tthis.handleTimeout(task_id);\n\t\t\t},\n\t\t\tMath.round(timeoutMs * this.loadMultiplier()),\n\t\t);\n\t\tthis.timeouts.set(task_id, timeout);\n\n\t\tproc.once(\"exit\", () => {\n\t\t\tthis.untrack(task_id);\n\t\t});\n\t}\n\n\t/** Record a heartbeat for a monitored task. */\n\trecordHeartbeat(task_id: string): void {\n\t\tif (this.processes.has(task_id)) {\n\t\t\tthis.lastHeartbeat.set(task_id, Date.now());\n\t\t}\n\t}\n\n\t/** Get the last recorded heartbeat timestamp, or null. */\n\tlastHeartbeatAt(task_id: string): number | null {\n\t\treturn this.lastHeartbeat.get(task_id) ?? null;\n\t}\n\n\t/** True if the task is currently being monitored. */\n\tisMonitoring(task_id: string): boolean {\n\t\treturn this.processes.has(task_id);\n\t}\n\n\t/** Kill all monitored processes and clean up. */\n\tdispose(): void {\n\t\tif (this.disposed) return;\n\t\tthis.disposed = true;\n\n\t\tif (this.checkInterval) {\n\t\t\tclearInterval(this.checkInterval);\n\t\t\tthis.checkInterval = null;\n\t\t}\n\n\t\tfor (const timeout of this.timeouts.values()) {\n\t\t\tclearTimeout(timeout);\n\t\t}\n\t\tthis.timeouts.clear();\n\n\t\tfor (const monitored of this.processes.values()) {\n\t\t\tthis.killTree(monitored);\n\t\t}\n\t\tthis.processes.clear();\n\t\tthis.lastHeartbeat.clear();\n\t\tthis.startedAt.clear();\n\t\tthis.baseTimeoutMs.clear();\n\t\tthis.reaping.clear();\n\t\tthis.removeAllListeners();\n\n\t\tif (this.parentShutdownHandler) {\n\t\t\tprocess.removeListener(\"SIGINT\", this.parentShutdownHandler);\n\t\t\tprocess.removeListener(\"SIGTERM\", this.parentShutdownHandler);\n\t\t}\n\t}\n\n\tprivate checkHeartbeats(): void {\n\t\tconst now = Date.now();\n\t\t// Event-loop lag: how much later than scheduled this check actually ran.\n\t\t// Heavy CPU load (many concurrent subagents + background MCP tools) starves\n\t\t// the loop, delaying both this check *and* our reading of child heartbeats.\n\t\t// Forgive that gap so the parent's own starvation isn't charged against the\n\t\t// children as a missed heartbeat.\n\t\tconst loopLag = Math.max(0, now - this.lastCheckAt - HEARTBEAT_CHECK_INTERVAL_MS);\n\t\tthis.lastCheckAt = now;\n\n\t\tconst threshold = HEARTBEAT_MISS_THRESHOLD_MS * this.loadMultiplier() + loopLag;\n\t\tfor (const [task_id] of this.processes) {\n\t\t\tif (this.reaping.has(task_id)) continue;\n\t\t\tconst last = this.lastHeartbeat.get(task_id);\n\t\t\tif (last === undefined) continue;\n\t\t\tif (now - last > threshold) {\n\t\t\t\tthis.handleStalled(task_id);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate handleStalled(task_id: string): void {\n\t\tconst monitored = this.processes.get(task_id);\n\t\tif (!monitored || this.reaping.has(task_id)) return;\n\t\tthis.reaping.add(task_id);\n\n\t\t// Record why we reaped this child so a recurrence is diagnosable rather than\n\t\t// just \"stalled\": how long since the last heartbeat, and the load factors\n\t\t// (concurrent monitored subagents, threshold) that fed the decision.\n\t\tconst last = this.lastHeartbeat.get(task_id);\n\t\tconst silentMs = last === undefined ? -1 : Date.now() - last;\n\t\tconsole.error(\n\t\t\t`[LIFEGUARD] stalled task_id=${task_id} agent=${monitored.agent_type} ` +\n\t\t\t\t`silent_ms=${silentMs} concurrent=${this.processes.size} ` +\n\t\t\t\t`load_mult=${this.loadMultiplier().toFixed(2)} base_threshold_ms=${HEARTBEAT_MISS_THRESHOLD_MS}`,\n\t\t);\n\n\t\tthis.killTree(monitored);\n\n\t\tthis.emit(\"stalled\", { task_id, pid: monitored.pid });\n\t\t// Process exit handler will call untrack()\n\t}\n\n\tprivate handleTimeout(task_id: string): void {\n\t\tconst monitored = this.processes.get(task_id);\n\t\tif (!monitored || this.reaping.has(task_id)) return;\n\n\t\t// Under load the wall-clock timer can fire while the subagent is still doing\n\t\t// real work — its turns are just slow because the CPU is shared. Re-arm rather\n\t\t// than kill, up to a hard ceiling (base * MAX_LOAD_MULTIPLIER) so a genuinely\n\t\t// stuck agent still terminates.\n\t\tconst started = this.startedAt.get(task_id) ?? Date.now();\n\t\tconst base = this.baseTimeoutMs.get(task_id) ?? TIMEOUTS_MS[monitored.agent_type] ?? TIMEOUTS_MS.explore;\n\t\tconst elapsed = Date.now() - started;\n\t\tconst ceiling = base * MAX_LOAD_MULTIPLIER;\n\t\tif (this.loadMultiplier() > 1 && elapsed < ceiling) {\n\t\t\tconst remaining = ceiling - elapsed;\n\t\t\tconst next = Math.min(\n\t\t\t\tMath.round(base * this.loadMultiplier()),\n\t\t\t\tMath.max(HEARTBEAT_CHECK_INTERVAL_MS, remaining),\n\t\t\t);\n\t\t\tthis.timeouts.set(\n\t\t\t\ttask_id,\n\t\t\t\tsetTimeout(() => this.handleTimeout(task_id), next),\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.reaping.add(task_id);\n\t\tthis.killTree(monitored);\n\n\t\tthis.emit(\"timeout\", { task_id, pid: monitored.pid });\n\t\tthis.timeouts.delete(task_id);\n\t\t// Process exit handler will call untrack()\n\t}\n\n\t/**\n\t * Kill a monitored subagent and everything it spawned. The pool spawns\n\t * children detached on POSIX (each leads its own process group), so the\n\t * group/tree kill reaches nested grandchildren (the subagent's bash commands,\n\t * its own subagents) that a single-PID SIGKILL would orphan. A pid of 0 means\n\t * the spawn never produced a process — nothing to kill (and `kill(-0)` would\n\t * signal our own process group).\n\t */\n\tprivate killTree(monitored: LifeguardProcess): void {\n\t\tif (monitored.pid > 0) {\n\t\t\tkillProcessTree(monitored.pid);\n\t\t} else if (!monitored.process.killed) {\n\t\t\tmonitored.process.kill(\"SIGKILL\");\n\t\t}\n\t}\n\n\tprivate untrack(task_id: string): void {\n\t\tconst timeout = this.timeouts.get(task_id);\n\t\tif (timeout) {\n\t\t\tclearTimeout(timeout);\n\t\t\tthis.timeouts.delete(task_id);\n\t\t}\n\t\tthis.processes.delete(task_id);\n\t\tthis.lastHeartbeat.delete(task_id);\n\t\tthis.startedAt.delete(task_id);\n\t\tthis.baseTimeoutMs.delete(task_id);\n\t\tthis.reaping.delete(task_id);\n\t}\n\n\tprivate setupParentExitHandlers(): void {\n\t\tconst shutdown = () => this.gracefulShutdown();\n\t\tthis.parentShutdownHandler = shutdown;\n\t\tprocess.setMaxListeners(Math.max(process.getMaxListeners(), 20));\n\t\tprocess.once(\"SIGINT\", shutdown);\n\t\tprocess.once(\"SIGTERM\", shutdown);\n\t}\n\n\tprivate gracefulShutdown(): void {\n\t\t// SIGTERM each child's process group (POSIX; the pool spawns them detached\n\t\t// as group leaders) so their own descendants get the graceful signal too.\n\t\tfor (const monitored of this.processes.values()) {\n\t\t\ttry {\n\t\t\t\tif (process.platform !== \"win32\" && monitored.pid > 0) {\n\t\t\t\t\tprocess.kill(-monitored.pid, \"SIGTERM\");\n\t\t\t\t} else if (!monitored.process.killed) {\n\t\t\t\t\tmonitored.process.kill(\"SIGTERM\");\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Already gone.\n\t\t\t}\n\t\t}\n\n\t\t// Kill whole trees after the grace period\n\t\tsetTimeout(() => {\n\t\t\tfor (const monitored of this.processes.values()) {\n\t\t\t\tthis.killTree(monitored);\n\t\t\t}\n\t\t}, PARENT_SHUTDOWN_GRACE_MS).unref();\n\t}\n\n\tprivate sweepOldAgents(): void {\n\t\tconst dispatchDir = getDispatchRoot(this.cwd);\n\t\tif (!existsSync(dispatchDir)) return;\n\n\t\tconst now = Date.now();\n\t\tconst cutoff = 24 * 60 * 60 * 1000; // 24 hours\n\n\t\tfor (const entry of readdirSync(dispatchDir)) {\n\t\t\tconst entryPath = join(dispatchDir, entry);\n\t\t\ttry {\n\t\t\t\tconst stats = statSync(entryPath);\n\t\t\t\tif (!stats.isDirectory()) continue;\n\n\t\t\t\tif (now - stats.mtimeMs > cutoff) {\n\t\t\t\t\tconst hasRunningPid = this.hasRunningPid(entryPath);\n\t\t\t\t\tif (!hasRunningPid) {\n\t\t\t\t\t\tthis.rmrf(entryPath);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Ignore errors for individual entries\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate hasRunningPid(dir: string): boolean {\n\t\tconst pidFile = join(dir, \"pid\");\n\t\tif (!existsSync(pidFile)) return false;\n\n\t\ttry {\n\t\t\tconst pid = Number.parseInt(readFileSync(pidFile, \"utf-8\"), 10);\n\t\t\tif (Number.isNaN(pid)) return false;\n\t\t\tprocess.kill(pid, 0); // Check if process exists\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate rmrf(dir: string): void {\n\t\tfor (const entry of readdirSync(dir)) {\n\t\t\tconst entryPath = join(dir, entry);\n\t\t\ttry {\n\t\t\t\tconst stats = statSync(entryPath);\n\t\t\t\tif (stats.isDirectory()) {\n\t\t\t\t\tthis.rmrf(entryPath);\n\t\t\t\t} else {\n\t\t\t\t\tunlinkSync(entryPath);\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Ignore\n\t\t\t}\n\t\t}\n\t\ttry {\n\t\t\trmdirSync(dir);\n\t\t} catch {\n\t\t\t// Ignore\n\t\t}\n\t}\n}\n"]}
@@ -24,8 +24,13 @@ export interface ScheduledTask {
24
24
  /** True when `date` matches the 5-field cron expression. Invalid expressions never match. */
25
25
  export declare function matchesCron(expr: string, date: Date): boolean;
26
26
  export interface TaskSchedulerOptions {
27
- /** Path to the durable JSON store. */
27
+ /** Path to the durable JSON store (written on every mutation). */
28
28
  storePath: string;
29
+ /**
30
+ * Legacy store path read only when {@link storePath} does not yet exist, so
31
+ * tasks scheduled under an older location migrate forward on first persist.
32
+ */
33
+ legacyStorePath?: string;
29
34
  /** Submit a due task's prompt (e.g. via sendUserMessage). */
30
35
  fire: (prompt: string) => void;
31
36
  /** Whether the agent is idle; tasks only fire when true. Defaults to always-idle. */
@@ -38,7 +43,8 @@ export declare class TaskScheduler {
38
43
  private timer;
39
44
  private readonly opts;
40
45
  constructor(opts: TaskSchedulerOptions);
41
- /** Load persisted tasks from disk (best-effort). */
46
+ /** Load persisted tasks from disk (best-effort). Falls back to the legacy
47
+ * store path when the primary one does not exist yet. */
42
48
  load(): void;
43
49
  private persist;
44
50
  /** Begin the tick loop. Safe to call once. */
@@ -1 +1 @@
1
- {"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../src/core/scheduler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,MAAM,WAAW,aAAa;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AA4BD,6FAA6F;AAC7F,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAsB7D;AAQD,MAAM,WAAW,oBAAoB;IACpC,sCAAsC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,qFAAqF;IACrF,MAAM,CAAC,EAAE,MAAM,OAAO,CAAC;IACvB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,aAAa;IACzB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,KAAK,CAA6C;IAC1D,OAAO,CAAC,QAAQ,CAAC,IAAI,CACgC;IAErD,YAAY,IAAI,EAAE,oBAAoB,EAGrC;IAED,oDAAoD;IACpD,IAAI,IAAI,IAAI,CASX;IAED,OAAO,CAAC,OAAO;IASf,8CAA8C;IAC9C,KAAK,IAAI,IAAI,CAIZ;IAED,IAAI,IAAI,IAAI,CAKX;IAED,MAAM,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,aAAa,CAWlF;IAED,IAAI,IAAI,aAAa,EAAE,CAEtB;IAED,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAM1B;IAED,KAAK,IAAI,IAAI,CAIZ;IAED,gFAAgF;IAChF,IAAI,CAAC,GAAG,EAAE,IAAI,GAAG,IAAI,CAsBpB;CACD","sourcesContent":["/**\n * Task scheduler — cron-driven recurring/one-shot prompts.\n *\n * Backs the `/loop` command and the Cron* tools. Tasks are matched against a\n * standard 5-field cron expression (minute hour day-of-month month day-of-week)\n * in local time and fired by re-submitting their prompt as a user message.\n * Firing is idle-gated so a task never interrupts an in-flight turn; a task due\n * while the agent is busy is picked up on a later tick within the same minute.\n *\n * Recurring tasks persist; one-shot tasks delete themselves after firing.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\n\nexport interface ScheduledTask {\n\tid: string;\n\t/** 5-field cron expression in local time. */\n\tcron: string;\n\t/** Prompt re-submitted on each fire. */\n\tprompt: string;\n\t/** Recurring (fire on every match) vs one-shot (fire once, then delete). */\n\trecurring: boolean;\n\tcreatedAt: number;\n\t/** Minute-bucket key of the last fire, to avoid double-firing within a minute. */\n\tlastRunMinute?: number;\n}\n\n/** Parse one cron field into a predicate over its numeric value. */\nfunction parseField(field: string, min: number, max: number): (value: number) => boolean {\n\tif (field === \"*\") return () => true;\n\n\tconst allowed = new Set<number>();\n\tfor (const part of field.split(\",\")) {\n\t\t// step: */n or a-b/n or a/n\n\t\tconst [range, stepStr] = part.split(\"/\");\n\t\tconst step = stepStr ? Number.parseInt(stepStr, 10) : 1;\n\t\tif (!Number.isFinite(step) || step < 1) continue;\n\n\t\tlet lo = min;\n\t\tlet hi = max;\n\t\tif (range && range !== \"*\") {\n\t\t\tconst [a, b] = range.split(\"-\");\n\t\t\tlo = Number.parseInt(a, 10);\n\t\t\thi = b !== undefined ? Number.parseInt(b, 10) : lo;\n\t\t\tif (!Number.isFinite(lo) || !Number.isFinite(hi)) continue;\n\t\t}\n\t\tfor (let v = lo; v <= hi; v += step) {\n\t\t\tif (v >= min && v <= max) allowed.add(v);\n\t\t}\n\t}\n\treturn (value: number) => allowed.has(value);\n}\n\n/** True when `date` matches the 5-field cron expression. Invalid expressions never match. */\nexport function matchesCron(expr: string, date: Date): boolean {\n\tconst fields = expr.trim().split(/\\s+/);\n\tif (fields.length !== 5) return false;\n\n\tconst [m, h, dom, mon, dow] = fields;\n\tconst minute = parseField(m, 0, 59)(date.getMinutes());\n\tconst hour = parseField(h, 0, 23)(date.getHours());\n\tconst month = parseField(mon, 1, 12)(date.getMonth() + 1);\n\t// day-of-week: cron allows 0 or 7 for Sunday; normalize 7→0.\n\tconst dowVal = date.getDay();\n\tconst domField = parseField(dom, 1, 31);\n\tconst dowField = parseField(dow.replace(/7/g, \"0\"), 0, 6);\n\n\tif (!(minute && hour && month)) return false;\n\n\t// Standard cron semantics: when both DOM and DOW are restricted, match either.\n\tconst domRestricted = dom !== \"*\";\n\tconst dowRestricted = dow !== \"*\";\n\tif (domRestricted && dowRestricted) {\n\t\treturn domField(date.getDate()) || dowField(dowVal);\n\t}\n\treturn domField(date.getDate()) && dowField(dowVal);\n}\n\nlet idCounter = 0;\nfunction newId(): string {\n\tidCounter += 1;\n\treturn `task_${Date.now().toString(36)}${idCounter.toString(36)}`;\n}\n\nexport interface TaskSchedulerOptions {\n\t/** Path to the durable JSON store. */\n\tstorePath: string;\n\t/** Submit a due task's prompt (e.g. via sendUserMessage). */\n\tfire: (prompt: string) => void;\n\t/** Whether the agent is idle; tasks only fire when true. Defaults to always-idle. */\n\tisIdle?: () => boolean;\n\t/** Tick interval in ms. Defaults to 30s. */\n\tintervalMs?: number;\n}\n\nexport class TaskScheduler {\n\tprivate tasks: ScheduledTask[] = [];\n\tprivate timer: ReturnType<typeof setInterval> | undefined;\n\tprivate readonly opts: Required<Pick<TaskSchedulerOptions, \"storePath\" | \"fire\">> &\n\t\tPick<TaskSchedulerOptions, \"isIdle\" | \"intervalMs\">;\n\n\tconstructor(opts: TaskSchedulerOptions) {\n\t\tthis.opts = opts;\n\t\tthis.load();\n\t}\n\n\t/** Load persisted tasks from disk (best-effort). */\n\tload(): void {\n\t\ttry {\n\t\t\tif (existsSync(this.opts.storePath)) {\n\t\t\t\tconst parsed = JSON.parse(readFileSync(this.opts.storePath, \"utf8\")) as { tasks?: ScheduledTask[] };\n\t\t\t\tthis.tasks = Array.isArray(parsed.tasks) ? parsed.tasks : [];\n\t\t\t}\n\t\t} catch {\n\t\t\tthis.tasks = [];\n\t\t}\n\t}\n\n\tprivate persist(): void {\n\t\ttry {\n\t\t\tmkdirSync(dirname(this.opts.storePath), { recursive: true });\n\t\t\twriteFileSync(this.opts.storePath, `${JSON.stringify({ tasks: this.tasks }, null, 2)}\\n`, \"utf8\");\n\t\t} catch {\n\t\t\t// best-effort persistence\n\t\t}\n\t}\n\n\t/** Begin the tick loop. Safe to call once. */\n\tstart(): void {\n\t\tif (this.timer) return;\n\t\tthis.timer = setInterval(() => this.tick(new Date()), this.opts.intervalMs ?? 30_000);\n\t\tthis.timer.unref?.();\n\t}\n\n\tstop(): void {\n\t\tif (this.timer) {\n\t\t\tclearInterval(this.timer);\n\t\t\tthis.timer = undefined;\n\t\t}\n\t}\n\n\tcreate(input: { cron: string; prompt: string; recurring?: boolean }): ScheduledTask {\n\t\tconst task: ScheduledTask = {\n\t\t\tid: newId(),\n\t\t\tcron: input.cron,\n\t\t\tprompt: input.prompt,\n\t\t\trecurring: input.recurring ?? true,\n\t\t\tcreatedAt: Date.now(),\n\t\t};\n\t\tthis.tasks.push(task);\n\t\tthis.persist();\n\t\treturn task;\n\t}\n\n\tlist(): ScheduledTask[] {\n\t\treturn [...this.tasks];\n\t}\n\n\tdelete(id: string): boolean {\n\t\tconst before = this.tasks.length;\n\t\tthis.tasks = this.tasks.filter((t) => t.id !== id);\n\t\tconst removed = this.tasks.length < before;\n\t\tif (removed) this.persist();\n\t\treturn removed;\n\t}\n\n\tclear(): void {\n\t\tif (this.tasks.length === 0) return;\n\t\tthis.tasks = [];\n\t\tthis.persist();\n\t}\n\n\t/** Evaluate all tasks against `now` and fire those that are due (when idle). */\n\ttick(now: Date): void {\n\t\tconst idle = this.opts.isIdle ? this.opts.isIdle() : true;\n\t\tif (!idle) return;\n\n\t\tconst minuteKey = Math.floor(now.getTime() / 60_000);\n\t\tlet mutated = false;\n\t\tconst toDelete: string[] = [];\n\n\t\tfor (const task of this.tasks) {\n\t\t\tif (task.lastRunMinute === minuteKey) continue;\n\t\t\tif (!matchesCron(task.cron, now)) continue;\n\n\t\t\ttask.lastRunMinute = minuteKey;\n\t\t\tmutated = true;\n\t\t\tthis.opts.fire(task.prompt);\n\t\t\tif (!task.recurring) toDelete.push(task.id);\n\t\t}\n\n\t\tif (toDelete.length > 0) {\n\t\t\tthis.tasks = this.tasks.filter((t) => !toDelete.includes(t.id));\n\t\t}\n\t\tif (mutated || toDelete.length > 0) this.persist();\n\t}\n}\n"]}
1
+ {"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../src/core/scheduler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,MAAM,WAAW,aAAa;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AA4BD,6FAA6F;AAC7F,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAsB7D;AAQD,MAAM,WAAW,oBAAoB;IACpC,kEAAkE;IAClE,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,6DAA6D;IAC7D,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,qFAAqF;IACrF,MAAM,CAAC,EAAE,MAAM,OAAO,CAAC;IACvB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,aAAa;IACzB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,KAAK,CAA6C;IAC1D,OAAO,CAAC,QAAQ,CAAC,IAAI,CACoD;IAEzE,YAAY,IAAI,EAAE,oBAAoB,EAGrC;IAED;8DAC0D;IAC1D,IAAI,IAAI,IAAI,CAeX;IAED,OAAO,CAAC,OAAO;IASf,8CAA8C;IAC9C,KAAK,IAAI,IAAI,CAIZ;IAED,IAAI,IAAI,IAAI,CAKX;IAED,MAAM,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,aAAa,CAWlF;IAED,IAAI,IAAI,aAAa,EAAE,CAEtB;IAED,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAM1B;IAED,KAAK,IAAI,IAAI,CAIZ;IAED,gFAAgF;IAChF,IAAI,CAAC,GAAG,EAAE,IAAI,GAAG,IAAI,CAsBpB;CACD","sourcesContent":["/**\n * Task scheduler — cron-driven recurring/one-shot prompts.\n *\n * Backs the `/loop` command and the Cron* tools. Tasks are matched against a\n * standard 5-field cron expression (minute hour day-of-month month day-of-week)\n * in local time and fired by re-submitting their prompt as a user message.\n * Firing is idle-gated so a task never interrupts an in-flight turn; a task due\n * while the agent is busy is picked up on a later tick within the same minute.\n *\n * Recurring tasks persist; one-shot tasks delete themselves after firing.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\n\nexport interface ScheduledTask {\n\tid: string;\n\t/** 5-field cron expression in local time. */\n\tcron: string;\n\t/** Prompt re-submitted on each fire. */\n\tprompt: string;\n\t/** Recurring (fire on every match) vs one-shot (fire once, then delete). */\n\trecurring: boolean;\n\tcreatedAt: number;\n\t/** Minute-bucket key of the last fire, to avoid double-firing within a minute. */\n\tlastRunMinute?: number;\n}\n\n/** Parse one cron field into a predicate over its numeric value. */\nfunction parseField(field: string, min: number, max: number): (value: number) => boolean {\n\tif (field === \"*\") return () => true;\n\n\tconst allowed = new Set<number>();\n\tfor (const part of field.split(\",\")) {\n\t\t// step: */n or a-b/n or a/n\n\t\tconst [range, stepStr] = part.split(\"/\");\n\t\tconst step = stepStr ? Number.parseInt(stepStr, 10) : 1;\n\t\tif (!Number.isFinite(step) || step < 1) continue;\n\n\t\tlet lo = min;\n\t\tlet hi = max;\n\t\tif (range && range !== \"*\") {\n\t\t\tconst [a, b] = range.split(\"-\");\n\t\t\tlo = Number.parseInt(a, 10);\n\t\t\thi = b !== undefined ? Number.parseInt(b, 10) : lo;\n\t\t\tif (!Number.isFinite(lo) || !Number.isFinite(hi)) continue;\n\t\t}\n\t\tfor (let v = lo; v <= hi; v += step) {\n\t\t\tif (v >= min && v <= max) allowed.add(v);\n\t\t}\n\t}\n\treturn (value: number) => allowed.has(value);\n}\n\n/** True when `date` matches the 5-field cron expression. Invalid expressions never match. */\nexport function matchesCron(expr: string, date: Date): boolean {\n\tconst fields = expr.trim().split(/\\s+/);\n\tif (fields.length !== 5) return false;\n\n\tconst [m, h, dom, mon, dow] = fields;\n\tconst minute = parseField(m, 0, 59)(date.getMinutes());\n\tconst hour = parseField(h, 0, 23)(date.getHours());\n\tconst month = parseField(mon, 1, 12)(date.getMonth() + 1);\n\t// day-of-week: cron allows 0 or 7 for Sunday; normalize 7→0.\n\tconst dowVal = date.getDay();\n\tconst domField = parseField(dom, 1, 31);\n\tconst dowField = parseField(dow.replace(/7/g, \"0\"), 0, 6);\n\n\tif (!(minute && hour && month)) return false;\n\n\t// Standard cron semantics: when both DOM and DOW are restricted, match either.\n\tconst domRestricted = dom !== \"*\";\n\tconst dowRestricted = dow !== \"*\";\n\tif (domRestricted && dowRestricted) {\n\t\treturn domField(date.getDate()) || dowField(dowVal);\n\t}\n\treturn domField(date.getDate()) && dowField(dowVal);\n}\n\nlet idCounter = 0;\nfunction newId(): string {\n\tidCounter += 1;\n\treturn `task_${Date.now().toString(36)}${idCounter.toString(36)}`;\n}\n\nexport interface TaskSchedulerOptions {\n\t/** Path to the durable JSON store (written on every mutation). */\n\tstorePath: string;\n\t/**\n\t * Legacy store path read only when {@link storePath} does not yet exist, so\n\t * tasks scheduled under an older location migrate forward on first persist.\n\t */\n\tlegacyStorePath?: string;\n\t/** Submit a due task's prompt (e.g. via sendUserMessage). */\n\tfire: (prompt: string) => void;\n\t/** Whether the agent is idle; tasks only fire when true. Defaults to always-idle. */\n\tisIdle?: () => boolean;\n\t/** Tick interval in ms. Defaults to 30s. */\n\tintervalMs?: number;\n}\n\nexport class TaskScheduler {\n\tprivate tasks: ScheduledTask[] = [];\n\tprivate timer: ReturnType<typeof setInterval> | undefined;\n\tprivate readonly opts: Required<Pick<TaskSchedulerOptions, \"storePath\" | \"fire\">> &\n\t\tPick<TaskSchedulerOptions, \"isIdle\" | \"intervalMs\" | \"legacyStorePath\">;\n\n\tconstructor(opts: TaskSchedulerOptions) {\n\t\tthis.opts = opts;\n\t\tthis.load();\n\t}\n\n\t/** Load persisted tasks from disk (best-effort). Falls back to the legacy\n\t * store path when the primary one does not exist yet. */\n\tload(): void {\n\t\tconst source =\n\t\t\texistsSync(this.opts.storePath) || !this.opts.legacyStorePath\n\t\t\t\t? this.opts.storePath\n\t\t\t\t: existsSync(this.opts.legacyStorePath)\n\t\t\t\t\t? this.opts.legacyStorePath\n\t\t\t\t\t: this.opts.storePath;\n\t\ttry {\n\t\t\tif (existsSync(source)) {\n\t\t\t\tconst parsed = JSON.parse(readFileSync(source, \"utf8\")) as { tasks?: ScheduledTask[] };\n\t\t\t\tthis.tasks = Array.isArray(parsed.tasks) ? parsed.tasks : [];\n\t\t\t}\n\t\t} catch {\n\t\t\tthis.tasks = [];\n\t\t}\n\t}\n\n\tprivate persist(): void {\n\t\ttry {\n\t\t\tmkdirSync(dirname(this.opts.storePath), { recursive: true });\n\t\t\twriteFileSync(this.opts.storePath, `${JSON.stringify({ tasks: this.tasks }, null, 2)}\\n`, \"utf8\");\n\t\t} catch {\n\t\t\t// best-effort persistence\n\t\t}\n\t}\n\n\t/** Begin the tick loop. Safe to call once. */\n\tstart(): void {\n\t\tif (this.timer) return;\n\t\tthis.timer = setInterval(() => this.tick(new Date()), this.opts.intervalMs ?? 30_000);\n\t\tthis.timer.unref?.();\n\t}\n\n\tstop(): void {\n\t\tif (this.timer) {\n\t\t\tclearInterval(this.timer);\n\t\t\tthis.timer = undefined;\n\t\t}\n\t}\n\n\tcreate(input: { cron: string; prompt: string; recurring?: boolean }): ScheduledTask {\n\t\tconst task: ScheduledTask = {\n\t\t\tid: newId(),\n\t\t\tcron: input.cron,\n\t\t\tprompt: input.prompt,\n\t\t\trecurring: input.recurring ?? true,\n\t\t\tcreatedAt: Date.now(),\n\t\t};\n\t\tthis.tasks.push(task);\n\t\tthis.persist();\n\t\treturn task;\n\t}\n\n\tlist(): ScheduledTask[] {\n\t\treturn [...this.tasks];\n\t}\n\n\tdelete(id: string): boolean {\n\t\tconst before = this.tasks.length;\n\t\tthis.tasks = this.tasks.filter((t) => t.id !== id);\n\t\tconst removed = this.tasks.length < before;\n\t\tif (removed) this.persist();\n\t\treturn removed;\n\t}\n\n\tclear(): void {\n\t\tif (this.tasks.length === 0) return;\n\t\tthis.tasks = [];\n\t\tthis.persist();\n\t}\n\n\t/** Evaluate all tasks against `now` and fire those that are due (when idle). */\n\ttick(now: Date): void {\n\t\tconst idle = this.opts.isIdle ? this.opts.isIdle() : true;\n\t\tif (!idle) return;\n\n\t\tconst minuteKey = Math.floor(now.getTime() / 60_000);\n\t\tlet mutated = false;\n\t\tconst toDelete: string[] = [];\n\n\t\tfor (const task of this.tasks) {\n\t\t\tif (task.lastRunMinute === minuteKey) continue;\n\t\t\tif (!matchesCron(task.cron, now)) continue;\n\n\t\t\ttask.lastRunMinute = minuteKey;\n\t\t\tmutated = true;\n\t\t\tthis.opts.fire(task.prompt);\n\t\t\tif (!task.recurring) toDelete.push(task.id);\n\t\t}\n\n\t\tif (toDelete.length > 0) {\n\t\t\tthis.tasks = this.tasks.filter((t) => !toDelete.includes(t.id));\n\t\t}\n\t\tif (mutated || toDelete.length > 0) this.persist();\n\t}\n}\n"]}
@@ -74,11 +74,17 @@ export class TaskScheduler {
74
74
  this.opts = opts;
75
75
  this.load();
76
76
  }
77
- /** Load persisted tasks from disk (best-effort). */
77
+ /** Load persisted tasks from disk (best-effort). Falls back to the legacy
78
+ * store path when the primary one does not exist yet. */
78
79
  load() {
80
+ const source = existsSync(this.opts.storePath) || !this.opts.legacyStorePath
81
+ ? this.opts.storePath
82
+ : existsSync(this.opts.legacyStorePath)
83
+ ? this.opts.legacyStorePath
84
+ : this.opts.storePath;
79
85
  try {
80
- if (existsSync(this.opts.storePath)) {
81
- const parsed = JSON.parse(readFileSync(this.opts.storePath, "utf8"));
86
+ if (existsSync(source)) {
87
+ const parsed = JSON.parse(readFileSync(source, "utf8"));
82
88
  this.tasks = Array.isArray(parsed.tasks) ? parsed.tasks : [];
83
89
  }
84
90
  }
@@ -1 +1 @@
1
- {"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../../src/core/scheduler.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Task scheduler — cron-driven recurring/one-shot prompts.\n *\n * Backs the `/loop` command and the Cron* tools. Tasks are matched against a\n * standard 5-field cron expression (minute hour day-of-month month day-of-week)\n * in local time and fired by re-submitting their prompt as a user message.\n * Firing is idle-gated so a task never interrupts an in-flight turn; a task due\n * while the agent is busy is picked up on a later tick within the same minute.\n *\n * Recurring tasks persist; one-shot tasks delete themselves after firing.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\n\nexport interface ScheduledTask {\n\tid: string;\n\t/** 5-field cron expression in local time. */\n\tcron: string;\n\t/** Prompt re-submitted on each fire. */\n\tprompt: string;\n\t/** Recurring (fire on every match) vs one-shot (fire once, then delete). */\n\trecurring: boolean;\n\tcreatedAt: number;\n\t/** Minute-bucket key of the last fire, to avoid double-firing within a minute. */\n\tlastRunMinute?: number;\n}\n\n/** Parse one cron field into a predicate over its numeric value. */\nfunction parseField(field: string, min: number, max: number): (value: number) => boolean {\n\tif (field === \"*\") return () => true;\n\n\tconst allowed = new Set<number>();\n\tfor (const part of field.split(\",\")) {\n\t\t// step: */n or a-b/n or a/n\n\t\tconst [range, stepStr] = part.split(\"/\");\n\t\tconst step = stepStr ? Number.parseInt(stepStr, 10) : 1;\n\t\tif (!Number.isFinite(step) || step < 1) continue;\n\n\t\tlet lo = min;\n\t\tlet hi = max;\n\t\tif (range && range !== \"*\") {\n\t\t\tconst [a, b] = range.split(\"-\");\n\t\t\tlo = Number.parseInt(a, 10);\n\t\t\thi = b !== undefined ? Number.parseInt(b, 10) : lo;\n\t\t\tif (!Number.isFinite(lo) || !Number.isFinite(hi)) continue;\n\t\t}\n\t\tfor (let v = lo; v <= hi; v += step) {\n\t\t\tif (v >= min && v <= max) allowed.add(v);\n\t\t}\n\t}\n\treturn (value: number) => allowed.has(value);\n}\n\n/** True when `date` matches the 5-field cron expression. Invalid expressions never match. */\nexport function matchesCron(expr: string, date: Date): boolean {\n\tconst fields = expr.trim().split(/\\s+/);\n\tif (fields.length !== 5) return false;\n\n\tconst [m, h, dom, mon, dow] = fields;\n\tconst minute = parseField(m, 0, 59)(date.getMinutes());\n\tconst hour = parseField(h, 0, 23)(date.getHours());\n\tconst month = parseField(mon, 1, 12)(date.getMonth() + 1);\n\t// day-of-week: cron allows 0 or 7 for Sunday; normalize 7→0.\n\tconst dowVal = date.getDay();\n\tconst domField = parseField(dom, 1, 31);\n\tconst dowField = parseField(dow.replace(/7/g, \"0\"), 0, 6);\n\n\tif (!(minute && hour && month)) return false;\n\n\t// Standard cron semantics: when both DOM and DOW are restricted, match either.\n\tconst domRestricted = dom !== \"*\";\n\tconst dowRestricted = dow !== \"*\";\n\tif (domRestricted && dowRestricted) {\n\t\treturn domField(date.getDate()) || dowField(dowVal);\n\t}\n\treturn domField(date.getDate()) && dowField(dowVal);\n}\n\nlet idCounter = 0;\nfunction newId(): string {\n\tidCounter += 1;\n\treturn `task_${Date.now().toString(36)}${idCounter.toString(36)}`;\n}\n\nexport interface TaskSchedulerOptions {\n\t/** Path to the durable JSON store. */\n\tstorePath: string;\n\t/** Submit a due task's prompt (e.g. via sendUserMessage). */\n\tfire: (prompt: string) => void;\n\t/** Whether the agent is idle; tasks only fire when true. Defaults to always-idle. */\n\tisIdle?: () => boolean;\n\t/** Tick interval in ms. Defaults to 30s. */\n\tintervalMs?: number;\n}\n\nexport class TaskScheduler {\n\tprivate tasks: ScheduledTask[] = [];\n\tprivate timer: ReturnType<typeof setInterval> | undefined;\n\tprivate readonly opts: Required<Pick<TaskSchedulerOptions, \"storePath\" | \"fire\">> &\n\t\tPick<TaskSchedulerOptions, \"isIdle\" | \"intervalMs\">;\n\n\tconstructor(opts: TaskSchedulerOptions) {\n\t\tthis.opts = opts;\n\t\tthis.load();\n\t}\n\n\t/** Load persisted tasks from disk (best-effort). */\n\tload(): void {\n\t\ttry {\n\t\t\tif (existsSync(this.opts.storePath)) {\n\t\t\t\tconst parsed = JSON.parse(readFileSync(this.opts.storePath, \"utf8\")) as { tasks?: ScheduledTask[] };\n\t\t\t\tthis.tasks = Array.isArray(parsed.tasks) ? parsed.tasks : [];\n\t\t\t}\n\t\t} catch {\n\t\t\tthis.tasks = [];\n\t\t}\n\t}\n\n\tprivate persist(): void {\n\t\ttry {\n\t\t\tmkdirSync(dirname(this.opts.storePath), { recursive: true });\n\t\t\twriteFileSync(this.opts.storePath, `${JSON.stringify({ tasks: this.tasks }, null, 2)}\\n`, \"utf8\");\n\t\t} catch {\n\t\t\t// best-effort persistence\n\t\t}\n\t}\n\n\t/** Begin the tick loop. Safe to call once. */\n\tstart(): void {\n\t\tif (this.timer) return;\n\t\tthis.timer = setInterval(() => this.tick(new Date()), this.opts.intervalMs ?? 30_000);\n\t\tthis.timer.unref?.();\n\t}\n\n\tstop(): void {\n\t\tif (this.timer) {\n\t\t\tclearInterval(this.timer);\n\t\t\tthis.timer = undefined;\n\t\t}\n\t}\n\n\tcreate(input: { cron: string; prompt: string; recurring?: boolean }): ScheduledTask {\n\t\tconst task: ScheduledTask = {\n\t\t\tid: newId(),\n\t\t\tcron: input.cron,\n\t\t\tprompt: input.prompt,\n\t\t\trecurring: input.recurring ?? true,\n\t\t\tcreatedAt: Date.now(),\n\t\t};\n\t\tthis.tasks.push(task);\n\t\tthis.persist();\n\t\treturn task;\n\t}\n\n\tlist(): ScheduledTask[] {\n\t\treturn [...this.tasks];\n\t}\n\n\tdelete(id: string): boolean {\n\t\tconst before = this.tasks.length;\n\t\tthis.tasks = this.tasks.filter((t) => t.id !== id);\n\t\tconst removed = this.tasks.length < before;\n\t\tif (removed) this.persist();\n\t\treturn removed;\n\t}\n\n\tclear(): void {\n\t\tif (this.tasks.length === 0) return;\n\t\tthis.tasks = [];\n\t\tthis.persist();\n\t}\n\n\t/** Evaluate all tasks against `now` and fire those that are due (when idle). */\n\ttick(now: Date): void {\n\t\tconst idle = this.opts.isIdle ? this.opts.isIdle() : true;\n\t\tif (!idle) return;\n\n\t\tconst minuteKey = Math.floor(now.getTime() / 60_000);\n\t\tlet mutated = false;\n\t\tconst toDelete: string[] = [];\n\n\t\tfor (const task of this.tasks) {\n\t\t\tif (task.lastRunMinute === minuteKey) continue;\n\t\t\tif (!matchesCron(task.cron, now)) continue;\n\n\t\t\ttask.lastRunMinute = minuteKey;\n\t\t\tmutated = true;\n\t\t\tthis.opts.fire(task.prompt);\n\t\t\tif (!task.recurring) toDelete.push(task.id);\n\t\t}\n\n\t\tif (toDelete.length > 0) {\n\t\t\tthis.tasks = this.tasks.filter((t) => !toDelete.includes(t.id));\n\t\t}\n\t\tif (mutated || toDelete.length > 0) this.persist();\n\t}\n}\n"]}
1
+ {"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../../src/core/scheduler.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Task scheduler — cron-driven recurring/one-shot prompts.\n *\n * Backs the `/loop` command and the Cron* tools. Tasks are matched against a\n * standard 5-field cron expression (minute hour day-of-month month day-of-week)\n * in local time and fired by re-submitting their prompt as a user message.\n * Firing is idle-gated so a task never interrupts an in-flight turn; a task due\n * while the agent is busy is picked up on a later tick within the same minute.\n *\n * Recurring tasks persist; one-shot tasks delete themselves after firing.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\n\nexport interface ScheduledTask {\n\tid: string;\n\t/** 5-field cron expression in local time. */\n\tcron: string;\n\t/** Prompt re-submitted on each fire. */\n\tprompt: string;\n\t/** Recurring (fire on every match) vs one-shot (fire once, then delete). */\n\trecurring: boolean;\n\tcreatedAt: number;\n\t/** Minute-bucket key of the last fire, to avoid double-firing within a minute. */\n\tlastRunMinute?: number;\n}\n\n/** Parse one cron field into a predicate over its numeric value. */\nfunction parseField(field: string, min: number, max: number): (value: number) => boolean {\n\tif (field === \"*\") return () => true;\n\n\tconst allowed = new Set<number>();\n\tfor (const part of field.split(\",\")) {\n\t\t// step: */n or a-b/n or a/n\n\t\tconst [range, stepStr] = part.split(\"/\");\n\t\tconst step = stepStr ? Number.parseInt(stepStr, 10) : 1;\n\t\tif (!Number.isFinite(step) || step < 1) continue;\n\n\t\tlet lo = min;\n\t\tlet hi = max;\n\t\tif (range && range !== \"*\") {\n\t\t\tconst [a, b] = range.split(\"-\");\n\t\t\tlo = Number.parseInt(a, 10);\n\t\t\thi = b !== undefined ? Number.parseInt(b, 10) : lo;\n\t\t\tif (!Number.isFinite(lo) || !Number.isFinite(hi)) continue;\n\t\t}\n\t\tfor (let v = lo; v <= hi; v += step) {\n\t\t\tif (v >= min && v <= max) allowed.add(v);\n\t\t}\n\t}\n\treturn (value: number) => allowed.has(value);\n}\n\n/** True when `date` matches the 5-field cron expression. Invalid expressions never match. */\nexport function matchesCron(expr: string, date: Date): boolean {\n\tconst fields = expr.trim().split(/\\s+/);\n\tif (fields.length !== 5) return false;\n\n\tconst [m, h, dom, mon, dow] = fields;\n\tconst minute = parseField(m, 0, 59)(date.getMinutes());\n\tconst hour = parseField(h, 0, 23)(date.getHours());\n\tconst month = parseField(mon, 1, 12)(date.getMonth() + 1);\n\t// day-of-week: cron allows 0 or 7 for Sunday; normalize 7→0.\n\tconst dowVal = date.getDay();\n\tconst domField = parseField(dom, 1, 31);\n\tconst dowField = parseField(dow.replace(/7/g, \"0\"), 0, 6);\n\n\tif (!(minute && hour && month)) return false;\n\n\t// Standard cron semantics: when both DOM and DOW are restricted, match either.\n\tconst domRestricted = dom !== \"*\";\n\tconst dowRestricted = dow !== \"*\";\n\tif (domRestricted && dowRestricted) {\n\t\treturn domField(date.getDate()) || dowField(dowVal);\n\t}\n\treturn domField(date.getDate()) && dowField(dowVal);\n}\n\nlet idCounter = 0;\nfunction newId(): string {\n\tidCounter += 1;\n\treturn `task_${Date.now().toString(36)}${idCounter.toString(36)}`;\n}\n\nexport interface TaskSchedulerOptions {\n\t/** Path to the durable JSON store (written on every mutation). */\n\tstorePath: string;\n\t/**\n\t * Legacy store path read only when {@link storePath} does not yet exist, so\n\t * tasks scheduled under an older location migrate forward on first persist.\n\t */\n\tlegacyStorePath?: string;\n\t/** Submit a due task's prompt (e.g. via sendUserMessage). */\n\tfire: (prompt: string) => void;\n\t/** Whether the agent is idle; tasks only fire when true. Defaults to always-idle. */\n\tisIdle?: () => boolean;\n\t/** Tick interval in ms. Defaults to 30s. */\n\tintervalMs?: number;\n}\n\nexport class TaskScheduler {\n\tprivate tasks: ScheduledTask[] = [];\n\tprivate timer: ReturnType<typeof setInterval> | undefined;\n\tprivate readonly opts: Required<Pick<TaskSchedulerOptions, \"storePath\" | \"fire\">> &\n\t\tPick<TaskSchedulerOptions, \"isIdle\" | \"intervalMs\" | \"legacyStorePath\">;\n\n\tconstructor(opts: TaskSchedulerOptions) {\n\t\tthis.opts = opts;\n\t\tthis.load();\n\t}\n\n\t/** Load persisted tasks from disk (best-effort). Falls back to the legacy\n\t * store path when the primary one does not exist yet. */\n\tload(): void {\n\t\tconst source =\n\t\t\texistsSync(this.opts.storePath) || !this.opts.legacyStorePath\n\t\t\t\t? this.opts.storePath\n\t\t\t\t: existsSync(this.opts.legacyStorePath)\n\t\t\t\t\t? this.opts.legacyStorePath\n\t\t\t\t\t: this.opts.storePath;\n\t\ttry {\n\t\t\tif (existsSync(source)) {\n\t\t\t\tconst parsed = JSON.parse(readFileSync(source, \"utf8\")) as { tasks?: ScheduledTask[] };\n\t\t\t\tthis.tasks = Array.isArray(parsed.tasks) ? parsed.tasks : [];\n\t\t\t}\n\t\t} catch {\n\t\t\tthis.tasks = [];\n\t\t}\n\t}\n\n\tprivate persist(): void {\n\t\ttry {\n\t\t\tmkdirSync(dirname(this.opts.storePath), { recursive: true });\n\t\t\twriteFileSync(this.opts.storePath, `${JSON.stringify({ tasks: this.tasks }, null, 2)}\\n`, \"utf8\");\n\t\t} catch {\n\t\t\t// best-effort persistence\n\t\t}\n\t}\n\n\t/** Begin the tick loop. Safe to call once. */\n\tstart(): void {\n\t\tif (this.timer) return;\n\t\tthis.timer = setInterval(() => this.tick(new Date()), this.opts.intervalMs ?? 30_000);\n\t\tthis.timer.unref?.();\n\t}\n\n\tstop(): void {\n\t\tif (this.timer) {\n\t\t\tclearInterval(this.timer);\n\t\t\tthis.timer = undefined;\n\t\t}\n\t}\n\n\tcreate(input: { cron: string; prompt: string; recurring?: boolean }): ScheduledTask {\n\t\tconst task: ScheduledTask = {\n\t\t\tid: newId(),\n\t\t\tcron: input.cron,\n\t\t\tprompt: input.prompt,\n\t\t\trecurring: input.recurring ?? true,\n\t\t\tcreatedAt: Date.now(),\n\t\t};\n\t\tthis.tasks.push(task);\n\t\tthis.persist();\n\t\treturn task;\n\t}\n\n\tlist(): ScheduledTask[] {\n\t\treturn [...this.tasks];\n\t}\n\n\tdelete(id: string): boolean {\n\t\tconst before = this.tasks.length;\n\t\tthis.tasks = this.tasks.filter((t) => t.id !== id);\n\t\tconst removed = this.tasks.length < before;\n\t\tif (removed) this.persist();\n\t\treturn removed;\n\t}\n\n\tclear(): void {\n\t\tif (this.tasks.length === 0) return;\n\t\tthis.tasks = [];\n\t\tthis.persist();\n\t}\n\n\t/** Evaluate all tasks against `now` and fire those that are due (when idle). */\n\ttick(now: Date): void {\n\t\tconst idle = this.opts.isIdle ? this.opts.isIdle() : true;\n\t\tif (!idle) return;\n\n\t\tconst minuteKey = Math.floor(now.getTime() / 60_000);\n\t\tlet mutated = false;\n\t\tconst toDelete: string[] = [];\n\n\t\tfor (const task of this.tasks) {\n\t\t\tif (task.lastRunMinute === minuteKey) continue;\n\t\t\tif (!matchesCron(task.cron, now)) continue;\n\n\t\t\ttask.lastRunMinute = minuteKey;\n\t\t\tmutated = true;\n\t\t\tthis.opts.fire(task.prompt);\n\t\t\tif (!task.recurring) toDelete.push(task.id);\n\t\t}\n\n\t\tif (toDelete.length > 0) {\n\t\t\tthis.tasks = this.tasks.filter((t) => !toDelete.includes(t.id));\n\t\t}\n\t\tif (mutated || toDelete.length > 0) this.persist();\n\t}\n}\n"]}
@@ -24,7 +24,7 @@ export declare const DEFAULT_SETTINGS: {
24
24
  collapseChangelog: false;
25
25
  enableInstallTelemetry: true;
26
26
  enableSkillCommands: true;
27
- enableSubagent: false;
27
+ enableSubagent: true;
28
28
  warmSubagents: false;
29
29
  maxSubagentDepth: number;
30
30
  nestedSubagentConcurrency: number;
@@ -1 +1 @@
1
- {"version":3,"file":"settings-defaults.d.ts","sourceRoot":"","sources":["../../src/core/settings-defaults.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6DT,CAAC","sourcesContent":["import type { Settings } from \"./settings-manager.js\";\n\nexport const DEFAULT_SETTINGS = {\n\ttransport: \"auto\",\n\tsteeringMode: \"one-at-a-time\",\n\tfollowUpMode: \"one-at-a-time\",\n\tcompaction: {\n\t\tenabled: true,\n\t\treserveTokens: 16384,\n\t\tkeepRecentTokens: 20000,\n\t},\n\tbranchSummary: {\n\t\treserveTokens: 16384,\n\t\tskipPrompt: false,\n\t},\n\tretry: {\n\t\tenabled: true,\n\t\tmaxRetries: 3,\n\t\tbaseDelayMs: 2000,\n\t\tprovider: {\n\t\t\tmaxRetryDelayMs: 60000,\n\t\t},\n\t},\n\thideThinkingBlock: false,\n\tquietStartup: false,\n\tcollapseChangelog: false,\n\tenableInstallTelemetry: true,\n\tenableSkillCommands: true,\n\tenableSubagent: false,\n\twarmSubagents: false,\n\tmaxSubagentDepth: 1,\n\tnestedSubagentConcurrency: 2,\n\tenableTodoWrite: true,\n\tenableWebTools: false,\n\tenableBrowserTools: false,\n\tenableBrowserLivePreview: false,\n\tenableFileTools: false,\n\tterminal: {\n\t\tshowImages: true,\n\t\timageWidthCells: 60,\n\t\tclearOnShrink: false,\n\t\tshowTerminalProgress: false,\n\t},\n\timages: {\n\t\tautoResize: true,\n\t\tblockImages: false,\n\t},\n\tdoubleEscapeAction: \"tree\",\n\ttreeFilterMode: \"default\",\n\teditorPaddingX: 0,\n\tautocompleteMaxVisible: 5,\n\tmarkdown: {\n\t\tcodeBlockIndent: \" \",\n\t},\n\twarnings: {\n\t\tanthropicExtraUsage: true,\n\t},\n\tpackages: [],\n\textensions: [],\n\tskills: [],\n\tprompts: [],\n\tslashCommands: [],\n\tthemes: [],\n} satisfies Settings;\n"]}
1
+ {"version":3,"file":"settings-defaults.d.ts","sourceRoot":"","sources":["../../src/core/settings-defaults.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6DT,CAAC","sourcesContent":["import type { Settings } from \"./settings-manager.js\";\n\nexport const DEFAULT_SETTINGS = {\n\ttransport: \"auto\",\n\tsteeringMode: \"one-at-a-time\",\n\tfollowUpMode: \"one-at-a-time\",\n\tcompaction: {\n\t\tenabled: true,\n\t\treserveTokens: 16384,\n\t\tkeepRecentTokens: 20000,\n\t},\n\tbranchSummary: {\n\t\treserveTokens: 16384,\n\t\tskipPrompt: false,\n\t},\n\tretry: {\n\t\tenabled: true,\n\t\tmaxRetries: 3,\n\t\tbaseDelayMs: 2000,\n\t\tprovider: {\n\t\t\tmaxRetryDelayMs: 60000,\n\t\t},\n\t},\n\thideThinkingBlock: false,\n\tquietStartup: false,\n\tcollapseChangelog: false,\n\tenableInstallTelemetry: true,\n\tenableSkillCommands: true,\n\tenableSubagent: true,\n\twarmSubagents: false,\n\tmaxSubagentDepth: 2,\n\tnestedSubagentConcurrency: 2,\n\tenableTodoWrite: true,\n\tenableWebTools: false,\n\tenableBrowserTools: false,\n\tenableBrowserLivePreview: false,\n\tenableFileTools: false,\n\tterminal: {\n\t\tshowImages: true,\n\t\timageWidthCells: 60,\n\t\tclearOnShrink: false,\n\t\tshowTerminalProgress: false,\n\t},\n\timages: {\n\t\tautoResize: true,\n\t\tblockImages: false,\n\t},\n\tdoubleEscapeAction: \"tree\",\n\ttreeFilterMode: \"default\",\n\teditorPaddingX: 0,\n\tautocompleteMaxVisible: 5,\n\tmarkdown: {\n\t\tcodeBlockIndent: \" \",\n\t},\n\twarnings: {\n\t\tanthropicExtraUsage: true,\n\t},\n\tpackages: [],\n\textensions: [],\n\tskills: [],\n\tprompts: [],\n\tslashCommands: [],\n\tthemes: [],\n} satisfies Settings;\n"]}
@@ -24,9 +24,9 @@ export const DEFAULT_SETTINGS = {
24
24
  collapseChangelog: false,
25
25
  enableInstallTelemetry: true,
26
26
  enableSkillCommands: true,
27
- enableSubagent: false,
27
+ enableSubagent: true,
28
28
  warmSubagents: false,
29
- maxSubagentDepth: 1,
29
+ maxSubagentDepth: 2,
30
30
  nestedSubagentConcurrency: 2,
31
31
  enableTodoWrite: true,
32
32
  enableWebTools: false,
@@ -1 +1 @@
1
- {"version":3,"file":"settings-defaults.js","sourceRoot":"","sources":["../../src/core/settings-defaults.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC/B,SAAS,EAAE,MAAM;IACjB,YAAY,EAAE,eAAe;IAC7B,YAAY,EAAE,eAAe;IAC7B,UAAU,EAAE;QACX,OAAO,EAAE,IAAI;QACb,aAAa,EAAE,KAAK;QACpB,gBAAgB,EAAE,KAAK;KACvB;IACD,aAAa,EAAE;QACd,aAAa,EAAE,KAAK;QACpB,UAAU,EAAE,KAAK;KACjB;IACD,KAAK,EAAE;QACN,OAAO,EAAE,IAAI;QACb,UAAU,EAAE,CAAC;QACb,WAAW,EAAE,IAAI;QACjB,QAAQ,EAAE;YACT,eAAe,EAAE,KAAK;SACtB;KACD;IACD,iBAAiB,EAAE,KAAK;IACxB,YAAY,EAAE,KAAK;IACnB,iBAAiB,EAAE,KAAK;IACxB,sBAAsB,EAAE,IAAI;IAC5B,mBAAmB,EAAE,IAAI;IACzB,cAAc,EAAE,KAAK;IACrB,aAAa,EAAE,KAAK;IACpB,gBAAgB,EAAE,CAAC;IACnB,yBAAyB,EAAE,CAAC;IAC5B,eAAe,EAAE,IAAI;IACrB,cAAc,EAAE,KAAK;IACrB,kBAAkB,EAAE,KAAK;IACzB,wBAAwB,EAAE,KAAK;IAC/B,eAAe,EAAE,KAAK;IACtB,QAAQ,EAAE;QACT,UAAU,EAAE,IAAI;QAChB,eAAe,EAAE,EAAE;QACnB,aAAa,EAAE,KAAK;QACpB,oBAAoB,EAAE,KAAK;KAC3B;IACD,MAAM,EAAE;QACP,UAAU,EAAE,IAAI;QAChB,WAAW,EAAE,KAAK;KAClB;IACD,kBAAkB,EAAE,MAAM;IAC1B,cAAc,EAAE,SAAS;IACzB,cAAc,EAAE,CAAC;IACjB,sBAAsB,EAAE,CAAC;IACzB,QAAQ,EAAE;QACT,eAAe,EAAE,IAAI;KACrB;IACD,QAAQ,EAAE;QACT,mBAAmB,EAAE,IAAI;KACzB;IACD,QAAQ,EAAE,EAAE;IACZ,UAAU,EAAE,EAAE;IACd,MAAM,EAAE,EAAE;IACV,OAAO,EAAE,EAAE;IACX,aAAa,EAAE,EAAE;IACjB,MAAM,EAAE,EAAE;CACS,CAAC","sourcesContent":["import type { Settings } from \"./settings-manager.js\";\n\nexport const DEFAULT_SETTINGS = {\n\ttransport: \"auto\",\n\tsteeringMode: \"one-at-a-time\",\n\tfollowUpMode: \"one-at-a-time\",\n\tcompaction: {\n\t\tenabled: true,\n\t\treserveTokens: 16384,\n\t\tkeepRecentTokens: 20000,\n\t},\n\tbranchSummary: {\n\t\treserveTokens: 16384,\n\t\tskipPrompt: false,\n\t},\n\tretry: {\n\t\tenabled: true,\n\t\tmaxRetries: 3,\n\t\tbaseDelayMs: 2000,\n\t\tprovider: {\n\t\t\tmaxRetryDelayMs: 60000,\n\t\t},\n\t},\n\thideThinkingBlock: false,\n\tquietStartup: false,\n\tcollapseChangelog: false,\n\tenableInstallTelemetry: true,\n\tenableSkillCommands: true,\n\tenableSubagent: false,\n\twarmSubagents: false,\n\tmaxSubagentDepth: 1,\n\tnestedSubagentConcurrency: 2,\n\tenableTodoWrite: true,\n\tenableWebTools: false,\n\tenableBrowserTools: false,\n\tenableBrowserLivePreview: false,\n\tenableFileTools: false,\n\tterminal: {\n\t\tshowImages: true,\n\t\timageWidthCells: 60,\n\t\tclearOnShrink: false,\n\t\tshowTerminalProgress: false,\n\t},\n\timages: {\n\t\tautoResize: true,\n\t\tblockImages: false,\n\t},\n\tdoubleEscapeAction: \"tree\",\n\ttreeFilterMode: \"default\",\n\teditorPaddingX: 0,\n\tautocompleteMaxVisible: 5,\n\tmarkdown: {\n\t\tcodeBlockIndent: \" \",\n\t},\n\twarnings: {\n\t\tanthropicExtraUsage: true,\n\t},\n\tpackages: [],\n\textensions: [],\n\tskills: [],\n\tprompts: [],\n\tslashCommands: [],\n\tthemes: [],\n} satisfies Settings;\n"]}
1
+ {"version":3,"file":"settings-defaults.js","sourceRoot":"","sources":["../../src/core/settings-defaults.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC/B,SAAS,EAAE,MAAM;IACjB,YAAY,EAAE,eAAe;IAC7B,YAAY,EAAE,eAAe;IAC7B,UAAU,EAAE;QACX,OAAO,EAAE,IAAI;QACb,aAAa,EAAE,KAAK;QACpB,gBAAgB,EAAE,KAAK;KACvB;IACD,aAAa,EAAE;QACd,aAAa,EAAE,KAAK;QACpB,UAAU,EAAE,KAAK;KACjB;IACD,KAAK,EAAE;QACN,OAAO,EAAE,IAAI;QACb,UAAU,EAAE,CAAC;QACb,WAAW,EAAE,IAAI;QACjB,QAAQ,EAAE;YACT,eAAe,EAAE,KAAK;SACtB;KACD;IACD,iBAAiB,EAAE,KAAK;IACxB,YAAY,EAAE,KAAK;IACnB,iBAAiB,EAAE,KAAK;IACxB,sBAAsB,EAAE,IAAI;IAC5B,mBAAmB,EAAE,IAAI;IACzB,cAAc,EAAE,IAAI;IACpB,aAAa,EAAE,KAAK;IACpB,gBAAgB,EAAE,CAAC;IACnB,yBAAyB,EAAE,CAAC;IAC5B,eAAe,EAAE,IAAI;IACrB,cAAc,EAAE,KAAK;IACrB,kBAAkB,EAAE,KAAK;IACzB,wBAAwB,EAAE,KAAK;IAC/B,eAAe,EAAE,KAAK;IACtB,QAAQ,EAAE;QACT,UAAU,EAAE,IAAI;QAChB,eAAe,EAAE,EAAE;QACnB,aAAa,EAAE,KAAK;QACpB,oBAAoB,EAAE,KAAK;KAC3B;IACD,MAAM,EAAE;QACP,UAAU,EAAE,IAAI;QAChB,WAAW,EAAE,KAAK;KAClB;IACD,kBAAkB,EAAE,MAAM;IAC1B,cAAc,EAAE,SAAS;IACzB,cAAc,EAAE,CAAC;IACjB,sBAAsB,EAAE,CAAC;IACzB,QAAQ,EAAE;QACT,eAAe,EAAE,IAAI;KACrB;IACD,QAAQ,EAAE;QACT,mBAAmB,EAAE,IAAI;KACzB;IACD,QAAQ,EAAE,EAAE;IACZ,UAAU,EAAE,EAAE;IACd,MAAM,EAAE,EAAE;IACV,OAAO,EAAE,EAAE;IACX,aAAa,EAAE,EAAE;IACjB,MAAM,EAAE,EAAE;CACS,CAAC","sourcesContent":["import type { Settings } from \"./settings-manager.js\";\n\nexport const DEFAULT_SETTINGS = {\n\ttransport: \"auto\",\n\tsteeringMode: \"one-at-a-time\",\n\tfollowUpMode: \"one-at-a-time\",\n\tcompaction: {\n\t\tenabled: true,\n\t\treserveTokens: 16384,\n\t\tkeepRecentTokens: 20000,\n\t},\n\tbranchSummary: {\n\t\treserveTokens: 16384,\n\t\tskipPrompt: false,\n\t},\n\tretry: {\n\t\tenabled: true,\n\t\tmaxRetries: 3,\n\t\tbaseDelayMs: 2000,\n\t\tprovider: {\n\t\t\tmaxRetryDelayMs: 60000,\n\t\t},\n\t},\n\thideThinkingBlock: false,\n\tquietStartup: false,\n\tcollapseChangelog: false,\n\tenableInstallTelemetry: true,\n\tenableSkillCommands: true,\n\tenableSubagent: true,\n\twarmSubagents: false,\n\tmaxSubagentDepth: 2,\n\tnestedSubagentConcurrency: 2,\n\tenableTodoWrite: true,\n\tenableWebTools: false,\n\tenableBrowserTools: false,\n\tenableBrowserLivePreview: false,\n\tenableFileTools: false,\n\tterminal: {\n\t\tshowImages: true,\n\t\timageWidthCells: 60,\n\t\tclearOnShrink: false,\n\t\tshowTerminalProgress: false,\n\t},\n\timages: {\n\t\tautoResize: true,\n\t\tblockImages: false,\n\t},\n\tdoubleEscapeAction: \"tree\",\n\ttreeFilterMode: \"default\",\n\teditorPaddingX: 0,\n\tautocompleteMaxVisible: 5,\n\tmarkdown: {\n\t\tcodeBlockIndent: \" \",\n\t},\n\twarnings: {\n\t\tanthropicExtraUsage: true,\n\t},\n\tpackages: [],\n\textensions: [],\n\tskills: [],\n\tprompts: [],\n\tslashCommands: [],\n\tthemes: [],\n} satisfies Settings;\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"settings-types.d.ts","sourceRoot":"","sources":["../../src/core/settings-types.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Settings schema shared across the app.\n *\n * The `Settings` interface and its nested option groups describe the on-disk\n * global/project settings.json shape. Extracted from settings-manager.ts so the\n * schema can be imported without pulling in the manager implementation.\n */\n\nimport type { Transport } from \"@kolisachint/hoocode-ai\";\n\nexport interface CompactionSettings {\n\tenabled?: boolean; // default: true\n\treserveTokens?: number; // default: 16384\n\tkeepRecentTokens?: number; // default: 20000\n}\n\nexport interface BranchSummarySettings {\n\treserveTokens?: number; // default: 16384 (tokens reserved for prompt + LLM response)\n\tskipPrompt?: boolean; // default: false - when true, skips \"Summarize branch?\" prompt and defaults to no summary\n}\n\nexport interface ProviderRetrySettings {\n\ttimeoutMs?: number; // SDK/provider request timeout in milliseconds\n\tmaxRetries?: number; // SDK/provider retry attempts\n\tmaxRetryDelayMs?: number; // default: 60000 (max server-requested delay before failing)\n}\n\nexport interface RetrySettings {\n\tenabled?: boolean; // default: true\n\tmaxRetries?: number; // default: 3\n\tbaseDelayMs?: number; // default: 2000 (exponential backoff: 2s, 4s, 8s)\n\tprovider?: ProviderRetrySettings;\n}\n\nexport interface TerminalSettings {\n\tshowImages?: boolean; // default: true (only relevant if terminal supports images)\n\timageWidthCells?: number; // default: 60 (preferred inline image width in terminal cells)\n\tclearOnShrink?: boolean; // default: false (clear empty rows when content shrinks)\n\tshowTerminalProgress?: boolean; // default: false (OSC 9;4 terminal progress indicators)\n}\n\nexport interface ImageSettings {\n\tautoResize?: boolean; // default: true (resize images to 2000x2000 max for better model compatibility)\n\tblockImages?: boolean; // default: false - when true, prevents all images from being sent to LLM providers\n}\n\nexport interface ThinkingBudgetsSettings {\n\tminimal?: number;\n\tlow?: number;\n\tmedium?: number;\n\thigh?: number;\n}\n\nexport interface MarkdownSettings {\n\tcodeBlockIndent?: string; // default: \" \"\n}\n\nexport interface WarningSettings {\n\tanthropicExtraUsage?: boolean; // default: true\n}\n\n/**\n * Model categories for subagent model selection.\n * Categories map to explicit model IDs (e.g., \"<provider>/<model-id>\").\n * When a category is not configured, no override is applied and the agent's or\n * parent's default model is used.\n */\nexport interface ModelCategories {\n\t/** Quick, cheap models for read-only exploration (grep, find, file discovery) */\n\tfast?: string;\n\t/** Balanced models for general work (planning, moderate complexity) */\n\tstandard?: string;\n\t/** Most capable models for complex reasoning (multi-file refactors) */\n\tcapable?: string;\n}\n\nexport type TransportSetting = Transport;\n\n/**\n * Package source for npm/git packages.\n * - String form: load all resources from the package\n * - Object form: filter which resources to load\n */\nexport type PackageSource =\n\t| string\n\t| {\n\t\t\tsource: string;\n\t\t\textensions?: string[];\n\t\t\tskills?: string[];\n\t\t\tprompts?: string[];\n\t\t\tthemes?: string[];\n\t };\n\nexport interface Settings {\n\tlastChangelogVersion?: string;\n\tdefaultProvider?: string;\n\tdefaultModel?: string;\n\tdefaultThinkingLevel?: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\";\n\tmodelCategories?: ModelCategories; // Model categories for subagent model selection (fast, standard, capable)\n\ttransport?: TransportSetting; // default: \"auto\"\n\tsteeringMode?: \"all\" | \"one-at-a-time\";\n\tfollowUpMode?: \"all\" | \"one-at-a-time\";\n\ttheme?: string;\n\tcompaction?: CompactionSettings;\n\tbranchSummary?: BranchSummarySettings;\n\tretry?: RetrySettings;\n\thideThinkingBlock?: boolean;\n\tshellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows)\n\tquietStartup?: boolean;\n\tshellCommandPrefix?: string; // Prefix prepended to every bash command (e.g., \"shopt -s expand_aliases\" for alias support)\n\tnpmCommand?: string[]; // Command used for npm package lookup/install operations, argv-style (e.g., [\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"])\n\tcollapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full)\n\tenableInstallTelemetry?: boolean; // default: true - anonymous version/update ping after changelog-detected updates\n\tpackages?: PackageSource[]; // Array of npm/git package sources (string or object with filtering)\n\textensions?: string[]; // Array of local extension file paths or directories\n\tskills?: string[]; // Array of local skill file paths or directories\n\tprompts?: string[]; // Array of local prompt template paths or directories\n\tslashCommands?: string[]; // Array of local slash-command paths or directories\n\tthemes?: string[]; // Array of local theme file paths or directories\n\tenableSkillCommands?: boolean; // default: true - register skills as /skill:name commands\n\tenableSubagent?: boolean; // default: false - enable the subagent tool (delegate tasks to isolated agent loops)\n\twarmSubagents?: boolean; // default: false - dispatch eligible subagents on reused warm RPC workers (experimental)\n\tmaxSubagentDepth?: number; // default: 1 - tree-wide subagent nesting cap (1 = subagents cannot spawn subagents)\n\tnestedSubagentConcurrency?: number; // default: 2 - max concurrent subagents per pool at nesting depth >= 1\n\tenableTodoWrite?: boolean; // default: true - enable the TodoWrite tool (maintain a live todo list in the task panel)\n\tenableWebTools?: boolean; // default: false - enable the webfetch + websearch tools (network access)\n\tenableBrowserTools?: boolean; // default: false - enable the browser_run + browser_continue tools (browsertools engine)\n\tenableBrowserLivePreview?: boolean; // default: false - default the live viewer on for browser_run runs and auto-open it\n\tenableFileTools?: boolean; // default: false - enable the document tools: DocRead/DocEdit/DocWrite + DocScan/DocGrep/DocPeek (filetools binary)\n\tterminal?: TerminalSettings;\n\timages?: ImageSettings;\n\tenabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)\n\tdoubleEscapeAction?: \"fork\" | \"tree\" | \"none\"; // Action for double-escape with empty editor (default: \"tree\")\n\ttreeFilterMode?: \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\"; // Default filter when opening /tree\n\tthinkingBudgets?: ThinkingBudgetsSettings; // Custom token budgets for thinking levels\n\tthinkingDisplay?: \"summarized\" | \"omitted\"; // How adaptive-thinking models return thinking content. Opus 4.8 defaults to \"omitted\" (faster tool use); set \"summarized\" to surface thinking text.\n\teditorPaddingX?: number; // Horizontal padding for input editor (default: 0)\n\tautocompleteMaxVisible?: number; // Max visible items in autocomplete dropdown (default: 5)\n\tshowHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME\n\tmarkdown?: MarkdownSettings;\n\twarnings?: WarningSettings;\n\tsessionDir?: string; // Custom session storage directory (same format as --session-dir CLI flag)\n}\n"]}
1
+ {"version":3,"file":"settings-types.d.ts","sourceRoot":"","sources":["../../src/core/settings-types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEzD,MAAM,WAAW,kBAAkB;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,qBAAqB;IACrC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,qBAAqB,CAAC;CACjC;AAED,MAAM,WAAW,gBAAgB;IAChC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa;IAC7B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,uBAAuB;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gBAAgB;IAChC,eAAe,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC/B,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC/B,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,gBAAgB,GAAG,SAAS,CAAC;AAEzC;;;;GAIG;AACH,MAAM,MAAM,aAAa,GACtB,MAAM,GACN;IACA,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAEL,MAAM,WAAW,QAAQ;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oBAAoB,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;IAC/E,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,YAAY,CAAC,EAAE,KAAK,GAAG,eAAe,CAAC;IACvC,YAAY,CAAC,EAAE,KAAK,GAAG,eAAe,CAAC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,kBAAkB,CAAC;IAChC,aAAa,CAAC,EAAE,qBAAqB,CAAC;IACtC,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IAC9C,cAAc,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,WAAW,GAAG,cAAc,GAAG,KAAK,CAAC;IAC/E,eAAe,CAAC,EAAE,uBAAuB,CAAC;IAC1C,eAAe,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;IAC3C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB","sourcesContent":["/**\n * Settings schema shared across the app.\n *\n * The `Settings` interface and its nested option groups describe the on-disk\n * global/project settings.json shape. Extracted from settings-manager.ts so the\n * schema can be imported without pulling in the manager implementation.\n */\n\nimport type { Transport } from \"@kolisachint/hoocode-ai\";\n\nexport interface CompactionSettings {\n\tenabled?: boolean; // default: true\n\treserveTokens?: number; // default: 16384\n\tkeepRecentTokens?: number; // default: 20000\n}\n\nexport interface BranchSummarySettings {\n\treserveTokens?: number; // default: 16384 (tokens reserved for prompt + LLM response)\n\tskipPrompt?: boolean; // default: false - when true, skips \"Summarize branch?\" prompt and defaults to no summary\n}\n\nexport interface ProviderRetrySettings {\n\ttimeoutMs?: number; // SDK/provider request timeout in milliseconds\n\tmaxRetries?: number; // SDK/provider retry attempts\n\tmaxRetryDelayMs?: number; // default: 60000 (max server-requested delay before failing)\n}\n\nexport interface RetrySettings {\n\tenabled?: boolean; // default: true\n\tmaxRetries?: number; // default: 3\n\tbaseDelayMs?: number; // default: 2000 (exponential backoff: 2s, 4s, 8s)\n\tprovider?: ProviderRetrySettings;\n}\n\nexport interface TerminalSettings {\n\tshowImages?: boolean; // default: true (only relevant if terminal supports images)\n\timageWidthCells?: number; // default: 60 (preferred inline image width in terminal cells)\n\tclearOnShrink?: boolean; // default: false (clear empty rows when content shrinks)\n\tshowTerminalProgress?: boolean; // default: false (OSC 9;4 terminal progress indicators)\n}\n\nexport interface ImageSettings {\n\tautoResize?: boolean; // default: true (resize images to 2000x2000 max for better model compatibility)\n\tblockImages?: boolean; // default: false - when true, prevents all images from being sent to LLM providers\n}\n\nexport interface ThinkingBudgetsSettings {\n\tminimal?: number;\n\tlow?: number;\n\tmedium?: number;\n\thigh?: number;\n}\n\nexport interface MarkdownSettings {\n\tcodeBlockIndent?: string; // default: \" \"\n}\n\nexport interface WarningSettings {\n\tanthropicExtraUsage?: boolean; // default: true\n}\n\n/**\n * Model categories for subagent model selection.\n * Categories map to explicit model IDs (e.g., \"<provider>/<model-id>\").\n * When a category is not configured, no override is applied and the agent's or\n * parent's default model is used.\n */\nexport interface ModelCategories {\n\t/** Quick, cheap models for read-only exploration (grep, find, file discovery) */\n\tfast?: string;\n\t/** Balanced models for general work (planning, moderate complexity) */\n\tstandard?: string;\n\t/** Most capable models for complex reasoning (multi-file refactors) */\n\tcapable?: string;\n}\n\nexport type TransportSetting = Transport;\n\n/**\n * Package source for npm/git packages.\n * - String form: load all resources from the package\n * - Object form: filter which resources to load\n */\nexport type PackageSource =\n\t| string\n\t| {\n\t\t\tsource: string;\n\t\t\textensions?: string[];\n\t\t\tskills?: string[];\n\t\t\tprompts?: string[];\n\t\t\tthemes?: string[];\n\t };\n\nexport interface Settings {\n\tlastChangelogVersion?: string;\n\tdefaultProvider?: string;\n\tdefaultModel?: string;\n\tdefaultThinkingLevel?: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\";\n\tmodelCategories?: ModelCategories; // Model categories for subagent model selection (fast, standard, capable)\n\ttransport?: TransportSetting; // default: \"auto\"\n\tsteeringMode?: \"all\" | \"one-at-a-time\";\n\tfollowUpMode?: \"all\" | \"one-at-a-time\";\n\ttheme?: string;\n\tcompaction?: CompactionSettings;\n\tbranchSummary?: BranchSummarySettings;\n\tretry?: RetrySettings;\n\thideThinkingBlock?: boolean;\n\tshellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows)\n\tquietStartup?: boolean;\n\tshellCommandPrefix?: string; // Prefix prepended to every bash command (e.g., \"shopt -s expand_aliases\" for alias support)\n\tnpmCommand?: string[]; // Command used for npm package lookup/install operations, argv-style (e.g., [\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"])\n\tcollapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full)\n\tenableInstallTelemetry?: boolean; // default: true - anonymous version/update ping after changelog-detected updates\n\tpackages?: PackageSource[]; // Array of npm/git package sources (string or object with filtering)\n\textensions?: string[]; // Array of local extension file paths or directories\n\tskills?: string[]; // Array of local skill file paths or directories\n\tprompts?: string[]; // Array of local prompt template paths or directories\n\tslashCommands?: string[]; // Array of local slash-command paths or directories\n\tthemes?: string[]; // Array of local theme file paths or directories\n\tenableSkillCommands?: boolean; // default: true - register skills as /skill:name commands\n\tenableSubagent?: boolean; // default: true - enable the subagent tool (delegate tasks to isolated agent loops); set false to disable\n\twarmSubagents?: boolean; // default: false - dispatch eligible subagents on reused warm RPC workers (experimental)\n\tmaxSubagentDepth?: number; // default: 2 - tree-wide subagent nesting cap (2 = a subagent may spawn one more level; 1 = no nesting)\n\tnestedSubagentConcurrency?: number; // default: 2 - max concurrent subagents per pool at nesting depth >= 1\n\tenableTodoWrite?: boolean; // default: true - enable the TodoWrite tool (maintain a live todo list in the task panel)\n\tenableWebTools?: boolean; // default: false - enable the webfetch + websearch tools (network access)\n\tenableBrowserTools?: boolean; // default: false - enable the browser_run + browser_continue tools (browsertools engine)\n\tenableBrowserLivePreview?: boolean; // default: false - default the live viewer on for browser_run runs and auto-open it\n\tenableFileTools?: boolean; // default: false - enable the document tools: DocRead/DocEdit/DocWrite + DocScan/DocGrep/DocPeek (filetools binary)\n\tterminal?: TerminalSettings;\n\timages?: ImageSettings;\n\tenabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)\n\tdoubleEscapeAction?: \"fork\" | \"tree\" | \"none\"; // Action for double-escape with empty editor (default: \"tree\")\n\ttreeFilterMode?: \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\"; // Default filter when opening /tree\n\tthinkingBudgets?: ThinkingBudgetsSettings; // Custom token budgets for thinking levels\n\tthinkingDisplay?: \"summarized\" | \"omitted\"; // How adaptive-thinking models return thinking content. Opus 4.8 defaults to \"omitted\" (faster tool use); set \"summarized\" to surface thinking text.\n\teditorPaddingX?: number; // Horizontal padding for input editor (default: 0)\n\tautocompleteMaxVisible?: number; // Max visible items in autocomplete dropdown (default: 5)\n\tshowHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME\n\tmarkdown?: MarkdownSettings;\n\twarnings?: WarningSettings;\n\tsessionDir?: string; // Custom session storage directory (same format as --session-dir CLI flag)\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"settings-types.js","sourceRoot":"","sources":["../../src/core/settings-types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG","sourcesContent":["/**\n * Settings schema shared across the app.\n *\n * The `Settings` interface and its nested option groups describe the on-disk\n * global/project settings.json shape. Extracted from settings-manager.ts so the\n * schema can be imported without pulling in the manager implementation.\n */\n\nimport type { Transport } from \"@kolisachint/hoocode-ai\";\n\nexport interface CompactionSettings {\n\tenabled?: boolean; // default: true\n\treserveTokens?: number; // default: 16384\n\tkeepRecentTokens?: number; // default: 20000\n}\n\nexport interface BranchSummarySettings {\n\treserveTokens?: number; // default: 16384 (tokens reserved for prompt + LLM response)\n\tskipPrompt?: boolean; // default: false - when true, skips \"Summarize branch?\" prompt and defaults to no summary\n}\n\nexport interface ProviderRetrySettings {\n\ttimeoutMs?: number; // SDK/provider request timeout in milliseconds\n\tmaxRetries?: number; // SDK/provider retry attempts\n\tmaxRetryDelayMs?: number; // default: 60000 (max server-requested delay before failing)\n}\n\nexport interface RetrySettings {\n\tenabled?: boolean; // default: true\n\tmaxRetries?: number; // default: 3\n\tbaseDelayMs?: number; // default: 2000 (exponential backoff: 2s, 4s, 8s)\n\tprovider?: ProviderRetrySettings;\n}\n\nexport interface TerminalSettings {\n\tshowImages?: boolean; // default: true (only relevant if terminal supports images)\n\timageWidthCells?: number; // default: 60 (preferred inline image width in terminal cells)\n\tclearOnShrink?: boolean; // default: false (clear empty rows when content shrinks)\n\tshowTerminalProgress?: boolean; // default: false (OSC 9;4 terminal progress indicators)\n}\n\nexport interface ImageSettings {\n\tautoResize?: boolean; // default: true (resize images to 2000x2000 max for better model compatibility)\n\tblockImages?: boolean; // default: false - when true, prevents all images from being sent to LLM providers\n}\n\nexport interface ThinkingBudgetsSettings {\n\tminimal?: number;\n\tlow?: number;\n\tmedium?: number;\n\thigh?: number;\n}\n\nexport interface MarkdownSettings {\n\tcodeBlockIndent?: string; // default: \" \"\n}\n\nexport interface WarningSettings {\n\tanthropicExtraUsage?: boolean; // default: true\n}\n\n/**\n * Model categories for subagent model selection.\n * Categories map to explicit model IDs (e.g., \"<provider>/<model-id>\").\n * When a category is not configured, no override is applied and the agent's or\n * parent's default model is used.\n */\nexport interface ModelCategories {\n\t/** Quick, cheap models for read-only exploration (grep, find, file discovery) */\n\tfast?: string;\n\t/** Balanced models for general work (planning, moderate complexity) */\n\tstandard?: string;\n\t/** Most capable models for complex reasoning (multi-file refactors) */\n\tcapable?: string;\n}\n\nexport type TransportSetting = Transport;\n\n/**\n * Package source for npm/git packages.\n * - String form: load all resources from the package\n * - Object form: filter which resources to load\n */\nexport type PackageSource =\n\t| string\n\t| {\n\t\t\tsource: string;\n\t\t\textensions?: string[];\n\t\t\tskills?: string[];\n\t\t\tprompts?: string[];\n\t\t\tthemes?: string[];\n\t };\n\nexport interface Settings {\n\tlastChangelogVersion?: string;\n\tdefaultProvider?: string;\n\tdefaultModel?: string;\n\tdefaultThinkingLevel?: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\";\n\tmodelCategories?: ModelCategories; // Model categories for subagent model selection (fast, standard, capable)\n\ttransport?: TransportSetting; // default: \"auto\"\n\tsteeringMode?: \"all\" | \"one-at-a-time\";\n\tfollowUpMode?: \"all\" | \"one-at-a-time\";\n\ttheme?: string;\n\tcompaction?: CompactionSettings;\n\tbranchSummary?: BranchSummarySettings;\n\tretry?: RetrySettings;\n\thideThinkingBlock?: boolean;\n\tshellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows)\n\tquietStartup?: boolean;\n\tshellCommandPrefix?: string; // Prefix prepended to every bash command (e.g., \"shopt -s expand_aliases\" for alias support)\n\tnpmCommand?: string[]; // Command used for npm package lookup/install operations, argv-style (e.g., [\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"])\n\tcollapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full)\n\tenableInstallTelemetry?: boolean; // default: true - anonymous version/update ping after changelog-detected updates\n\tpackages?: PackageSource[]; // Array of npm/git package sources (string or object with filtering)\n\textensions?: string[]; // Array of local extension file paths or directories\n\tskills?: string[]; // Array of local skill file paths or directories\n\tprompts?: string[]; // Array of local prompt template paths or directories\n\tslashCommands?: string[]; // Array of local slash-command paths or directories\n\tthemes?: string[]; // Array of local theme file paths or directories\n\tenableSkillCommands?: boolean; // default: true - register skills as /skill:name commands\n\tenableSubagent?: boolean; // default: false - enable the subagent tool (delegate tasks to isolated agent loops)\n\twarmSubagents?: boolean; // default: false - dispatch eligible subagents on reused warm RPC workers (experimental)\n\tmaxSubagentDepth?: number; // default: 1 - tree-wide subagent nesting cap (1 = subagents cannot spawn subagents)\n\tnestedSubagentConcurrency?: number; // default: 2 - max concurrent subagents per pool at nesting depth >= 1\n\tenableTodoWrite?: boolean; // default: true - enable the TodoWrite tool (maintain a live todo list in the task panel)\n\tenableWebTools?: boolean; // default: false - enable the webfetch + websearch tools (network access)\n\tenableBrowserTools?: boolean; // default: false - enable the browser_run + browser_continue tools (browsertools engine)\n\tenableBrowserLivePreview?: boolean; // default: false - default the live viewer on for browser_run runs and auto-open it\n\tenableFileTools?: boolean; // default: false - enable the document tools: DocRead/DocEdit/DocWrite + DocScan/DocGrep/DocPeek (filetools binary)\n\tterminal?: TerminalSettings;\n\timages?: ImageSettings;\n\tenabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)\n\tdoubleEscapeAction?: \"fork\" | \"tree\" | \"none\"; // Action for double-escape with empty editor (default: \"tree\")\n\ttreeFilterMode?: \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\"; // Default filter when opening /tree\n\tthinkingBudgets?: ThinkingBudgetsSettings; // Custom token budgets for thinking levels\n\tthinkingDisplay?: \"summarized\" | \"omitted\"; // How adaptive-thinking models return thinking content. Opus 4.8 defaults to \"omitted\" (faster tool use); set \"summarized\" to surface thinking text.\n\teditorPaddingX?: number; // Horizontal padding for input editor (default: 0)\n\tautocompleteMaxVisible?: number; // Max visible items in autocomplete dropdown (default: 5)\n\tshowHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME\n\tmarkdown?: MarkdownSettings;\n\twarnings?: WarningSettings;\n\tsessionDir?: string; // Custom session storage directory (same format as --session-dir CLI flag)\n}\n"]}
1
+ {"version":3,"file":"settings-types.js","sourceRoot":"","sources":["../../src/core/settings-types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG","sourcesContent":["/**\n * Settings schema shared across the app.\n *\n * The `Settings` interface and its nested option groups describe the on-disk\n * global/project settings.json shape. Extracted from settings-manager.ts so the\n * schema can be imported without pulling in the manager implementation.\n */\n\nimport type { Transport } from \"@kolisachint/hoocode-ai\";\n\nexport interface CompactionSettings {\n\tenabled?: boolean; // default: true\n\treserveTokens?: number; // default: 16384\n\tkeepRecentTokens?: number; // default: 20000\n}\n\nexport interface BranchSummarySettings {\n\treserveTokens?: number; // default: 16384 (tokens reserved for prompt + LLM response)\n\tskipPrompt?: boolean; // default: false - when true, skips \"Summarize branch?\" prompt and defaults to no summary\n}\n\nexport interface ProviderRetrySettings {\n\ttimeoutMs?: number; // SDK/provider request timeout in milliseconds\n\tmaxRetries?: number; // SDK/provider retry attempts\n\tmaxRetryDelayMs?: number; // default: 60000 (max server-requested delay before failing)\n}\n\nexport interface RetrySettings {\n\tenabled?: boolean; // default: true\n\tmaxRetries?: number; // default: 3\n\tbaseDelayMs?: number; // default: 2000 (exponential backoff: 2s, 4s, 8s)\n\tprovider?: ProviderRetrySettings;\n}\n\nexport interface TerminalSettings {\n\tshowImages?: boolean; // default: true (only relevant if terminal supports images)\n\timageWidthCells?: number; // default: 60 (preferred inline image width in terminal cells)\n\tclearOnShrink?: boolean; // default: false (clear empty rows when content shrinks)\n\tshowTerminalProgress?: boolean; // default: false (OSC 9;4 terminal progress indicators)\n}\n\nexport interface ImageSettings {\n\tautoResize?: boolean; // default: true (resize images to 2000x2000 max for better model compatibility)\n\tblockImages?: boolean; // default: false - when true, prevents all images from being sent to LLM providers\n}\n\nexport interface ThinkingBudgetsSettings {\n\tminimal?: number;\n\tlow?: number;\n\tmedium?: number;\n\thigh?: number;\n}\n\nexport interface MarkdownSettings {\n\tcodeBlockIndent?: string; // default: \" \"\n}\n\nexport interface WarningSettings {\n\tanthropicExtraUsage?: boolean; // default: true\n}\n\n/**\n * Model categories for subagent model selection.\n * Categories map to explicit model IDs (e.g., \"<provider>/<model-id>\").\n * When a category is not configured, no override is applied and the agent's or\n * parent's default model is used.\n */\nexport interface ModelCategories {\n\t/** Quick, cheap models for read-only exploration (grep, find, file discovery) */\n\tfast?: string;\n\t/** Balanced models for general work (planning, moderate complexity) */\n\tstandard?: string;\n\t/** Most capable models for complex reasoning (multi-file refactors) */\n\tcapable?: string;\n}\n\nexport type TransportSetting = Transport;\n\n/**\n * Package source for npm/git packages.\n * - String form: load all resources from the package\n * - Object form: filter which resources to load\n */\nexport type PackageSource =\n\t| string\n\t| {\n\t\t\tsource: string;\n\t\t\textensions?: string[];\n\t\t\tskills?: string[];\n\t\t\tprompts?: string[];\n\t\t\tthemes?: string[];\n\t };\n\nexport interface Settings {\n\tlastChangelogVersion?: string;\n\tdefaultProvider?: string;\n\tdefaultModel?: string;\n\tdefaultThinkingLevel?: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\";\n\tmodelCategories?: ModelCategories; // Model categories for subagent model selection (fast, standard, capable)\n\ttransport?: TransportSetting; // default: \"auto\"\n\tsteeringMode?: \"all\" | \"one-at-a-time\";\n\tfollowUpMode?: \"all\" | \"one-at-a-time\";\n\ttheme?: string;\n\tcompaction?: CompactionSettings;\n\tbranchSummary?: BranchSummarySettings;\n\tretry?: RetrySettings;\n\thideThinkingBlock?: boolean;\n\tshellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows)\n\tquietStartup?: boolean;\n\tshellCommandPrefix?: string; // Prefix prepended to every bash command (e.g., \"shopt -s expand_aliases\" for alias support)\n\tnpmCommand?: string[]; // Command used for npm package lookup/install operations, argv-style (e.g., [\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"])\n\tcollapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full)\n\tenableInstallTelemetry?: boolean; // default: true - anonymous version/update ping after changelog-detected updates\n\tpackages?: PackageSource[]; // Array of npm/git package sources (string or object with filtering)\n\textensions?: string[]; // Array of local extension file paths or directories\n\tskills?: string[]; // Array of local skill file paths or directories\n\tprompts?: string[]; // Array of local prompt template paths or directories\n\tslashCommands?: string[]; // Array of local slash-command paths or directories\n\tthemes?: string[]; // Array of local theme file paths or directories\n\tenableSkillCommands?: boolean; // default: true - register skills as /skill:name commands\n\tenableSubagent?: boolean; // default: true - enable the subagent tool (delegate tasks to isolated agent loops); set false to disable\n\twarmSubagents?: boolean; // default: false - dispatch eligible subagents on reused warm RPC workers (experimental)\n\tmaxSubagentDepth?: number; // default: 2 - tree-wide subagent nesting cap (2 = a subagent may spawn one more level; 1 = no nesting)\n\tnestedSubagentConcurrency?: number; // default: 2 - max concurrent subagents per pool at nesting depth >= 1\n\tenableTodoWrite?: boolean; // default: true - enable the TodoWrite tool (maintain a live todo list in the task panel)\n\tenableWebTools?: boolean; // default: false - enable the webfetch + websearch tools (network access)\n\tenableBrowserTools?: boolean; // default: false - enable the browser_run + browser_continue tools (browsertools engine)\n\tenableBrowserLivePreview?: boolean; // default: false - default the live viewer on for browser_run runs and auto-open it\n\tenableFileTools?: boolean; // default: false - enable the document tools: DocRead/DocEdit/DocWrite + DocScan/DocGrep/DocPeek (filetools binary)\n\tterminal?: TerminalSettings;\n\timages?: ImageSettings;\n\tenabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)\n\tdoubleEscapeAction?: \"fork\" | \"tree\" | \"none\"; // Action for double-escape with empty editor (default: \"tree\")\n\ttreeFilterMode?: \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\"; // Default filter when opening /tree\n\tthinkingBudgets?: ThinkingBudgetsSettings; // Custom token budgets for thinking levels\n\tthinkingDisplay?: \"summarized\" | \"omitted\"; // How adaptive-thinking models return thinking content. Opus 4.8 defaults to \"omitted\" (faster tool use); set \"summarized\" to surface thinking text.\n\teditorPaddingX?: number; // Horizontal padding for input editor (default: 0)\n\tautocompleteMaxVisible?: number; // Max visible items in autocomplete dropdown (default: 5)\n\tshowHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME\n\tmarkdown?: MarkdownSettings;\n\twarnings?: WarningSettings;\n\tsessionDir?: string; // Custom session storage directory (same format as --session-dir CLI flag)\n}\n"]}
@@ -17,7 +17,7 @@
17
17
  * └─▶ timeout
18
18
  */
19
19
  import type { SubagentPool, TaskResult } from "./subagent-pool.js";
20
- export type TaskLifecycle = "running" | "done" | "failed" | "stalled" | "timeout" | "collected";
20
+ export type TaskLifecycle = "running" | "done" | "failed" | "stalled" | "timeout" | "cancelled" | "collected";
21
21
  /** A non-terminal task is still doing work; the rest have settled. */
22
22
  export declare function isOutstanding(lifecycle: TaskLifecycle): boolean;
23
23
  export interface InboxRecord {
@@ -1 +1 @@
1
- {"version":3,"file":"subagent-inbox.d.ts","sourceRoot":"","sources":["../../src/core/subagent-inbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGnE,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,CAAC;AAEhG,sEAAsE;AACtE,wBAAgB,aAAa,CAAC,SAAS,EAAE,aAAa,GAAG,OAAO,CAE/D;AAED,MAAM,WAAW,WAAW;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,aAAa,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAgCD,cAAM,aAAa;IAClB,OAAO,CAAC,OAAO,CAAkC;IACjD,mEAAmE;IACnE,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,aAAa,CAA+B;IACpD,+EAA+E;IAC/E,OAAO,CAAC,eAAe,CAAyB;IAEhD,OAAO,CAAC,YAAY;IAIpB;;;OAGG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAsB3E;IAED,uFAAuF;IACvF,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3C;IAED,0FAAwF;IACxF,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAInC;IAED;;;OAGG;IACH,OAAO,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAYhC;IAED,gEAAgE;IAChE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,CAMnE;IAED,iGAAiG;IACjG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,WAAW,GAAG,SAAS,CAkBlE;IAED,qFAAqF;IACrF,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAE,aAAwB,GAAG,WAAW,GAAG,SAAS,CAUjG;IAED,wDAAwD;IACxD,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAO3C;IAED;;;;OAIG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG;QAAE,MAAM,EAAE,WAAW,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAOzE;IAED,iCAAiC;IACjC,IAAI,IAAI,WAAW,EAAE,CAEpB;IAED,gCAAgC;IAChC,WAAW,IAAI,WAAW,EAAE,CAE3B;IAED,4BAA4B;IAC5B,KAAK,IAAI,IAAI,CAIZ;IAED,mFAAmF;IACnF,OAAO,CAAC,KAAK;CAkBb;AAED,2EAA2E;AAC3E,eAAO,MAAM,aAAa,eAAsB,CAAC","sourcesContent":["/**\n * Subagent inbox: the notify-and-pull bookkeeping behind background `Task`\n * dispatch and the `TaskOutput` tool.\n *\n * Background dispatches don't push their full result into the parent's context.\n * Instead the parent gets a compact notification (\"explore#1 finished\") and the\n * body is retained here, keyed by task id, until the model explicitly pulls it\n * with TaskOutput. This keeps a wide swarm of subagents from flooding the\n * context with N full summaries, and gives the model a queryable liveness view\n * (running / done / failed + last activity) so it can tell a working subagent\n * from a rogue one. The same surface works at every delegation depth.\n *\n * Lifecycle per task:\n * running ──▶ done (body retained) ──collect──▶ collected (body dropped)\n * ├─▶ failed\n * ├─▶ stalled\n * └─▶ timeout\n */\n\nimport type { SubagentPool, TaskResult } from \"./subagent-pool.js\";\nimport type { SubagentResultFile } from \"./subagent-result.js\";\n\nexport type TaskLifecycle = \"running\" | \"done\" | \"failed\" | \"stalled\" | \"timeout\" | \"collected\";\n\n/** A non-terminal task is still doing work; the rest have settled. */\nexport function isOutstanding(lifecycle: TaskLifecycle): boolean {\n\treturn lifecycle === \"running\";\n}\n\nexport interface InboxRecord {\n\ttaskId: string;\n\t/** Friendly handle shown to the model, e.g. \"explore#1\". */\n\tlabel: string;\n\tagentType: string;\n\tlifecycle: TaskLifecycle;\n\tstartedAt: number;\n\tendedAt?: number;\n\t/** Tool the subagent is currently running, from the pool's progress stream. */\n\tlastActivity?: string;\n\t/** First line of the result/summary, kept even after the body is collected. */\n\tsummaryLine?: string;\n\t/** Full subagent summary, retained only while `done` and uncollected. */\n\tbody?: string;\n\t/** Failure reason when the task did not succeed. */\n\terror?: string;\n}\n\n/** Cap on settled (terminal) records kept for late pulls; running records are never pruned. */\nconst MAX_SETTLED = 50;\n\n/** First non-empty line of a block of text, length-capped. */\nfunction firstLine(text: string, max = 120): string {\n\tconst line = (\n\t\ttext\n\t\t\t.trim()\n\t\t\t.split(\"\\n\")\n\t\t\t.find((l) => l.trim()) ?? \"\"\n\t).trim();\n\treturn line.length > max ? `${line.slice(0, max - 1)}…` : line;\n}\n\n/** Map a subagent's terminal `status` onto an inbox lifecycle. */\nfunction failLifecycle(status: string | undefined): TaskLifecycle {\n\tif (status === \"stalled\") return \"stalled\";\n\tif (status === \"timeout\") return \"timeout\";\n\treturn \"failed\";\n}\n\n/** Derive the \"currently running X\" activity label from a forwarded progress event. */\nfunction activityFromEvent(event: Record<string, unknown>): string | undefined {\n\tif (event.type === \"tool_execution_start\" && typeof event.toolName === \"string\") {\n\t\treturn event.toolName;\n\t}\n\tif (event.type === \"turn_end\") return \"thinking\";\n\treturn undefined;\n}\n\nclass SubagentInbox {\n\tprivate records = new Map<string, InboxRecord>();\n\t/** Insertion order of task ids, for stable listing and pruning. */\n\tprivate order: string[] = [];\n\tprivate labelCounters = new Map<string, number>();\n\tprivate observedPools = new WeakSet<SubagentPool>();\n\t/** One-shot callbacks fired after any record settles, for the wait helpers. */\n\tprivate settleListeners = new Set<() => void>();\n\n\tprivate notifySettle(): void {\n\t\tfor (const listener of [...this.settleListeners]) listener();\n\t}\n\n\t/**\n\t * Resolve once the given task settles (or immediately if already settled /\n\t * unknown), bounded by `timeoutMs`. Backs TaskOutput's per-task wait.\n\t */\n\twaitFor(handle: string, timeoutMs: number): Promise<InboxRecord | undefined> {\n\t\tconst current = this.get(handle);\n\t\tif (!current || !isOutstanding(current.lifecycle)) return Promise.resolve(current);\n\t\treturn new Promise((resolve) => {\n\t\t\tconst cleanup = () => {\n\t\t\t\tthis.settleListeners.delete(check);\n\t\t\t\tclearTimeout(timer);\n\t\t\t};\n\t\t\tconst check = () => {\n\t\t\t\tconst rec = this.get(handle);\n\t\t\t\tif (!rec || !isOutstanding(rec.lifecycle)) {\n\t\t\t\t\tcleanup();\n\t\t\t\t\tresolve(rec);\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tcleanup();\n\t\t\t\tresolve(this.get(handle));\n\t\t\t}, timeoutMs);\n\t\t\ttimer.unref?.();\n\t\t\tthis.settleListeners.add(check);\n\t\t});\n\t}\n\n\t/** Resolve once nothing is outstanding (the swarm barrier), bounded by `timeoutMs`. */\n\twaitForAll(timeoutMs: number): Promise<void> {\n\t\tif (this.outstanding().length === 0) return Promise.resolve();\n\t\treturn new Promise((resolve) => {\n\t\t\tconst cleanup = () => {\n\t\t\t\tthis.settleListeners.delete(check);\n\t\t\t\tclearTimeout(timer);\n\t\t\t};\n\t\t\tconst check = () => {\n\t\t\t\tif (this.outstanding().length === 0) {\n\t\t\t\t\tcleanup();\n\t\t\t\t\tresolve();\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tcleanup();\n\t\t\t\tresolve();\n\t\t\t}, timeoutMs);\n\t\t\ttimer.unref?.();\n\t\t\tthis.settleListeners.add(check);\n\t\t});\n\t}\n\n\t/** Allocate the next friendly label for an agent type (`explore#1`, `explore#2`, …). */\n\tnextLabel(agentType: string): string {\n\t\tconst n = (this.labelCounters.get(agentType) ?? 0) + 1;\n\t\tthis.labelCounters.set(agentType, n);\n\t\treturn `${agentType}#${n}`;\n\t}\n\n\t/**\n\t * Track a pool's `task_progress` events so running records carry a live\n\t * `lastActivity`. Idempotent per pool, so callers can wire it on every dispatch.\n\t */\n\tobserve(pool: SubagentPool): void {\n\t\t// Tolerate a non-EventEmitter stand-in (test fakes): without progress events\n\t\t// records simply carry no live activity.\n\t\tif (typeof (pool as { on?: unknown }).on !== \"function\") return;\n\t\tif (this.observedPools.has(pool)) return;\n\t\tthis.observedPools.add(pool);\n\t\tpool.on(\"task_progress\", (data: { task_id: string; event: Record<string, unknown> }) => {\n\t\t\tconst rec = this.records.get(data.task_id);\n\t\t\tif (!rec || rec.lifecycle !== \"running\") return;\n\t\t\tconst activity = activityFromEvent(data.event);\n\t\t\tif (activity) rec.lastActivity = activity;\n\t\t});\n\t}\n\n\t/** Register a freshly dispatched background task as running. */\n\tstart(taskId: string, label: string, agentType: string): InboxRecord {\n\t\tconst rec: InboxRecord = { taskId, label, agentType, lifecycle: \"running\", startedAt: Date.now() };\n\t\tthis.records.set(taskId, rec);\n\t\tthis.order.push(taskId);\n\t\tthis.prune();\n\t\treturn rec;\n\t}\n\n\t/** Settle a task from its dispatch result: retain the body on success, the reason on failure. */\n\tfinish(taskId: string, result: TaskResult): InboxRecord | undefined {\n\t\tconst rec = this.records.get(taskId);\n\t\tif (!rec) return undefined;\n\t\trec.endedAt = Date.now();\n\t\trec.lastActivity = undefined;\n\t\tconst r = result.result;\n\t\tconst data = r?.result_data as SubagentResultFile | undefined;\n\t\tif (r?.ok) {\n\t\t\trec.lifecycle = \"done\";\n\t\t\trec.body = data?.summary?.trim() || \"(subagent returned no output)\";\n\t\t\trec.summaryLine = firstLine(rec.body);\n\t\t} else {\n\t\t\trec.lifecycle = failLifecycle(r?.status);\n\t\t\trec.error = r?.error ?? (r?.status ? `subagent ${r.status}` : \"unknown error\");\n\t\t\trec.summaryLine = rec.error;\n\t\t}\n\t\tthis.notifySettle();\n\t\treturn rec;\n\t}\n\n\t/** Settle a task that never produced a TaskResult (e.g. a thrown dispatch error). */\n\tfail(taskId: string, reason: string, lifecycle: TaskLifecycle = \"failed\"): InboxRecord | undefined {\n\t\tconst rec = this.records.get(taskId);\n\t\tif (!rec) return undefined;\n\t\trec.endedAt = Date.now();\n\t\trec.lastActivity = undefined;\n\t\trec.lifecycle = lifecycle;\n\t\trec.error = reason;\n\t\trec.summaryLine = reason;\n\t\tthis.notifySettle();\n\t\treturn rec;\n\t}\n\n\t/** Look up a record by task id or by friendly label. */\n\tget(handle: string): InboxRecord | undefined {\n\t\tconst byId = this.records.get(handle);\n\t\tif (byId) return byId;\n\t\tfor (const rec of this.records.values()) {\n\t\t\tif (rec.label === handle) return rec;\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Read a done task's body and mark it collected, dropping the body so it is not\n\t * re-fed to the model. Returns the record (with `body` still populated for this\n\t * one read) or undefined for an unknown handle.\n\t */\n\tcollect(handle: string): { record: InboxRecord; body: string } | undefined {\n\t\tconst rec = this.get(handle);\n\t\tif (!rec || rec.lifecycle !== \"done\" || rec.body === undefined) return undefined;\n\t\tconst body = rec.body;\n\t\trec.lifecycle = \"collected\";\n\t\trec.body = undefined;\n\t\treturn { record: rec, body };\n\t}\n\n\t/** All records, oldest first. */\n\tlist(): InboxRecord[] {\n\t\treturn this.order.map((id) => this.records.get(id)).filter((r): r is InboxRecord => r !== undefined);\n\t}\n\n\t/** Records still doing work. */\n\toutstanding(): InboxRecord[] {\n\t\treturn this.list().filter((r) => isOutstanding(r.lifecycle));\n\t}\n\n\t/** Test/teardown helper. */\n\tclear(): void {\n\t\tthis.records.clear();\n\t\tthis.order = [];\n\t\tthis.labelCounters.clear();\n\t}\n\n\t/** Drop the oldest settled records once past the cap; running records are kept. */\n\tprivate prune(): void {\n\t\tlet settled = this.order.filter((id) => {\n\t\t\tconst r = this.records.get(id);\n\t\t\treturn r !== undefined && !isOutstanding(r.lifecycle);\n\t\t}).length;\n\t\tif (settled <= MAX_SETTLED) return;\n\t\tconst kept: string[] = [];\n\t\tfor (const id of this.order) {\n\t\t\tconst r = this.records.get(id);\n\t\t\tif (r && !isOutstanding(r.lifecycle) && settled > MAX_SETTLED) {\n\t\t\t\tthis.records.delete(id);\n\t\t\t\tsettled--;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tkept.push(id);\n\t\t}\n\t\tthis.order = kept;\n\t}\n}\n\n/** Process-wide subagent inbox shared by the Task and TaskOutput tools. */\nexport const subagentInbox = new SubagentInbox();\n"]}
1
+ {"version":3,"file":"subagent-inbox.d.ts","sourceRoot":"","sources":["../../src/core/subagent-inbox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGnE,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,CAAC;AAE9G,sEAAsE;AACtE,wBAAgB,aAAa,CAAC,SAAS,EAAE,aAAa,GAAG,OAAO,CAE/D;AAED,MAAM,WAAW,WAAW;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,aAAa,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAiCD,cAAM,aAAa;IAClB,OAAO,CAAC,OAAO,CAAkC;IACjD,mEAAmE;IACnE,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,aAAa,CAA+B;IACpD,+EAA+E;IAC/E,OAAO,CAAC,eAAe,CAAyB;IAEhD,OAAO,CAAC,YAAY;IAIpB;;;OAGG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAsB3E;IAED,uFAAuF;IACvF,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3C;IAED,0FAAwF;IACxF,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAInC;IAED;;;OAGG;IACH,OAAO,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAYhC;IAED,gEAAgE;IAChE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,CAMnE;IAED,iGAAiG;IACjG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,WAAW,GAAG,SAAS,CAkBlE;IAED,qFAAqF;IACrF,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAE,aAAwB,GAAG,WAAW,GAAG,SAAS,CAUjG;IAED,wDAAwD;IACxD,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAO3C;IAED;;;;OAIG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG;QAAE,MAAM,EAAE,WAAW,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAOzE;IAED,iCAAiC;IACjC,IAAI,IAAI,WAAW,EAAE,CAEpB;IAED,gCAAgC;IAChC,WAAW,IAAI,WAAW,EAAE,CAE3B;IAED,4BAA4B;IAC5B,KAAK,IAAI,IAAI,CAIZ;IAED,mFAAmF;IACnF,OAAO,CAAC,KAAK;CAkBb;AAED,2EAA2E;AAC3E,eAAO,MAAM,aAAa,eAAsB,CAAC","sourcesContent":["/**\n * Subagent inbox: the notify-and-pull bookkeeping behind background `Task`\n * dispatch and the `TaskOutput` tool.\n *\n * Background dispatches don't push their full result into the parent's context.\n * Instead the parent gets a compact notification (\"explore#1 finished\") and the\n * body is retained here, keyed by task id, until the model explicitly pulls it\n * with TaskOutput. This keeps a wide swarm of subagents from flooding the\n * context with N full summaries, and gives the model a queryable liveness view\n * (running / done / failed + last activity) so it can tell a working subagent\n * from a rogue one. The same surface works at every delegation depth.\n *\n * Lifecycle per task:\n * running ──▶ done (body retained) ──collect──▶ collected (body dropped)\n * ├─▶ failed\n * ├─▶ stalled\n * └─▶ timeout\n */\n\nimport type { SubagentPool, TaskResult } from \"./subagent-pool.js\";\nimport type { SubagentResultFile } from \"./subagent-result.js\";\n\nexport type TaskLifecycle = \"running\" | \"done\" | \"failed\" | \"stalled\" | \"timeout\" | \"cancelled\" | \"collected\";\n\n/** A non-terminal task is still doing work; the rest have settled. */\nexport function isOutstanding(lifecycle: TaskLifecycle): boolean {\n\treturn lifecycle === \"running\";\n}\n\nexport interface InboxRecord {\n\ttaskId: string;\n\t/** Friendly handle shown to the model, e.g. \"explore#1\". */\n\tlabel: string;\n\tagentType: string;\n\tlifecycle: TaskLifecycle;\n\tstartedAt: number;\n\tendedAt?: number;\n\t/** Tool the subagent is currently running, from the pool's progress stream. */\n\tlastActivity?: string;\n\t/** First line of the result/summary, kept even after the body is collected. */\n\tsummaryLine?: string;\n\t/** Full subagent summary, retained only while `done` and uncollected. */\n\tbody?: string;\n\t/** Failure reason when the task did not succeed. */\n\terror?: string;\n}\n\n/** Cap on settled (terminal) records kept for late pulls; running records are never pruned. */\nconst MAX_SETTLED = 50;\n\n/** First non-empty line of a block of text, length-capped. */\nfunction firstLine(text: string, max = 120): string {\n\tconst line = (\n\t\ttext\n\t\t\t.trim()\n\t\t\t.split(\"\\n\")\n\t\t\t.find((l) => l.trim()) ?? \"\"\n\t).trim();\n\treturn line.length > max ? `${line.slice(0, max - 1)}…` : line;\n}\n\n/** Map a subagent's terminal `status` onto an inbox lifecycle. */\nfunction failLifecycle(status: string | undefined): TaskLifecycle {\n\tif (status === \"stalled\") return \"stalled\";\n\tif (status === \"timeout\") return \"timeout\";\n\tif (status === \"cancelled\") return \"cancelled\";\n\treturn \"failed\";\n}\n\n/** Derive the \"currently running X\" activity label from a forwarded progress event. */\nfunction activityFromEvent(event: Record<string, unknown>): string | undefined {\n\tif (event.type === \"tool_execution_start\" && typeof event.toolName === \"string\") {\n\t\treturn event.toolName;\n\t}\n\tif (event.type === \"turn_end\") return \"thinking\";\n\treturn undefined;\n}\n\nclass SubagentInbox {\n\tprivate records = new Map<string, InboxRecord>();\n\t/** Insertion order of task ids, for stable listing and pruning. */\n\tprivate order: string[] = [];\n\tprivate labelCounters = new Map<string, number>();\n\tprivate observedPools = new WeakSet<SubagentPool>();\n\t/** One-shot callbacks fired after any record settles, for the wait helpers. */\n\tprivate settleListeners = new Set<() => void>();\n\n\tprivate notifySettle(): void {\n\t\tfor (const listener of [...this.settleListeners]) listener();\n\t}\n\n\t/**\n\t * Resolve once the given task settles (or immediately if already settled /\n\t * unknown), bounded by `timeoutMs`. Backs TaskOutput's per-task wait.\n\t */\n\twaitFor(handle: string, timeoutMs: number): Promise<InboxRecord | undefined> {\n\t\tconst current = this.get(handle);\n\t\tif (!current || !isOutstanding(current.lifecycle)) return Promise.resolve(current);\n\t\treturn new Promise((resolve) => {\n\t\t\tconst cleanup = () => {\n\t\t\t\tthis.settleListeners.delete(check);\n\t\t\t\tclearTimeout(timer);\n\t\t\t};\n\t\t\tconst check = () => {\n\t\t\t\tconst rec = this.get(handle);\n\t\t\t\tif (!rec || !isOutstanding(rec.lifecycle)) {\n\t\t\t\t\tcleanup();\n\t\t\t\t\tresolve(rec);\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tcleanup();\n\t\t\t\tresolve(this.get(handle));\n\t\t\t}, timeoutMs);\n\t\t\ttimer.unref?.();\n\t\t\tthis.settleListeners.add(check);\n\t\t});\n\t}\n\n\t/** Resolve once nothing is outstanding (the swarm barrier), bounded by `timeoutMs`. */\n\twaitForAll(timeoutMs: number): Promise<void> {\n\t\tif (this.outstanding().length === 0) return Promise.resolve();\n\t\treturn new Promise((resolve) => {\n\t\t\tconst cleanup = () => {\n\t\t\t\tthis.settleListeners.delete(check);\n\t\t\t\tclearTimeout(timer);\n\t\t\t};\n\t\t\tconst check = () => {\n\t\t\t\tif (this.outstanding().length === 0) {\n\t\t\t\t\tcleanup();\n\t\t\t\t\tresolve();\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tcleanup();\n\t\t\t\tresolve();\n\t\t\t}, timeoutMs);\n\t\t\ttimer.unref?.();\n\t\t\tthis.settleListeners.add(check);\n\t\t});\n\t}\n\n\t/** Allocate the next friendly label for an agent type (`explore#1`, `explore#2`, …). */\n\tnextLabel(agentType: string): string {\n\t\tconst n = (this.labelCounters.get(agentType) ?? 0) + 1;\n\t\tthis.labelCounters.set(agentType, n);\n\t\treturn `${agentType}#${n}`;\n\t}\n\n\t/**\n\t * Track a pool's `task_progress` events so running records carry a live\n\t * `lastActivity`. Idempotent per pool, so callers can wire it on every dispatch.\n\t */\n\tobserve(pool: SubagentPool): void {\n\t\t// Tolerate a non-EventEmitter stand-in (test fakes): without progress events\n\t\t// records simply carry no live activity.\n\t\tif (typeof (pool as { on?: unknown }).on !== \"function\") return;\n\t\tif (this.observedPools.has(pool)) return;\n\t\tthis.observedPools.add(pool);\n\t\tpool.on(\"task_progress\", (data: { task_id: string; event: Record<string, unknown> }) => {\n\t\t\tconst rec = this.records.get(data.task_id);\n\t\t\tif (!rec || rec.lifecycle !== \"running\") return;\n\t\t\tconst activity = activityFromEvent(data.event);\n\t\t\tif (activity) rec.lastActivity = activity;\n\t\t});\n\t}\n\n\t/** Register a freshly dispatched background task as running. */\n\tstart(taskId: string, label: string, agentType: string): InboxRecord {\n\t\tconst rec: InboxRecord = { taskId, label, agentType, lifecycle: \"running\", startedAt: Date.now() };\n\t\tthis.records.set(taskId, rec);\n\t\tthis.order.push(taskId);\n\t\tthis.prune();\n\t\treturn rec;\n\t}\n\n\t/** Settle a task from its dispatch result: retain the body on success, the reason on failure. */\n\tfinish(taskId: string, result: TaskResult): InboxRecord | undefined {\n\t\tconst rec = this.records.get(taskId);\n\t\tif (!rec) return undefined;\n\t\trec.endedAt = Date.now();\n\t\trec.lastActivity = undefined;\n\t\tconst r = result.result;\n\t\tconst data = r?.result_data as SubagentResultFile | undefined;\n\t\tif (r?.ok) {\n\t\t\trec.lifecycle = \"done\";\n\t\t\trec.body = data?.summary?.trim() || \"(subagent returned no output)\";\n\t\t\trec.summaryLine = firstLine(rec.body);\n\t\t} else {\n\t\t\trec.lifecycle = failLifecycle(r?.status);\n\t\t\trec.error = r?.error ?? (r?.status ? `subagent ${r.status}` : \"unknown error\");\n\t\t\trec.summaryLine = rec.error;\n\t\t}\n\t\tthis.notifySettle();\n\t\treturn rec;\n\t}\n\n\t/** Settle a task that never produced a TaskResult (e.g. a thrown dispatch error). */\n\tfail(taskId: string, reason: string, lifecycle: TaskLifecycle = \"failed\"): InboxRecord | undefined {\n\t\tconst rec = this.records.get(taskId);\n\t\tif (!rec) return undefined;\n\t\trec.endedAt = Date.now();\n\t\trec.lastActivity = undefined;\n\t\trec.lifecycle = lifecycle;\n\t\trec.error = reason;\n\t\trec.summaryLine = reason;\n\t\tthis.notifySettle();\n\t\treturn rec;\n\t}\n\n\t/** Look up a record by task id or by friendly label. */\n\tget(handle: string): InboxRecord | undefined {\n\t\tconst byId = this.records.get(handle);\n\t\tif (byId) return byId;\n\t\tfor (const rec of this.records.values()) {\n\t\t\tif (rec.label === handle) return rec;\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Read a done task's body and mark it collected, dropping the body so it is not\n\t * re-fed to the model. Returns the record (with `body` still populated for this\n\t * one read) or undefined for an unknown handle.\n\t */\n\tcollect(handle: string): { record: InboxRecord; body: string } | undefined {\n\t\tconst rec = this.get(handle);\n\t\tif (!rec || rec.lifecycle !== \"done\" || rec.body === undefined) return undefined;\n\t\tconst body = rec.body;\n\t\trec.lifecycle = \"collected\";\n\t\trec.body = undefined;\n\t\treturn { record: rec, body };\n\t}\n\n\t/** All records, oldest first. */\n\tlist(): InboxRecord[] {\n\t\treturn this.order.map((id) => this.records.get(id)).filter((r): r is InboxRecord => r !== undefined);\n\t}\n\n\t/** Records still doing work. */\n\toutstanding(): InboxRecord[] {\n\t\treturn this.list().filter((r) => isOutstanding(r.lifecycle));\n\t}\n\n\t/** Test/teardown helper. */\n\tclear(): void {\n\t\tthis.records.clear();\n\t\tthis.order = [];\n\t\tthis.labelCounters.clear();\n\t}\n\n\t/** Drop the oldest settled records once past the cap; running records are kept. */\n\tprivate prune(): void {\n\t\tlet settled = this.order.filter((id) => {\n\t\t\tconst r = this.records.get(id);\n\t\t\treturn r !== undefined && !isOutstanding(r.lifecycle);\n\t\t}).length;\n\t\tif (settled <= MAX_SETTLED) return;\n\t\tconst kept: string[] = [];\n\t\tfor (const id of this.order) {\n\t\t\tconst r = this.records.get(id);\n\t\t\tif (r && !isOutstanding(r.lifecycle) && settled > MAX_SETTLED) {\n\t\t\t\tthis.records.delete(id);\n\t\t\t\tsettled--;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tkept.push(id);\n\t\t}\n\t\tthis.order = kept;\n\t}\n}\n\n/** Process-wide subagent inbox shared by the Task and TaskOutput tools. */\nexport const subagentInbox = new SubagentInbox();\n"]}
@@ -36,6 +36,8 @@ function failLifecycle(status) {
36
36
  return "stalled";
37
37
  if (status === "timeout")
38
38
  return "timeout";
39
+ if (status === "cancelled")
40
+ return "cancelled";
39
41
  return "failed";
40
42
  }
41
43
  /** Derive the "currently running X" activity label from a forwarded progress event. */