@a1hvdy/cc-openclaw 0.10.4 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/dist/src/channels/telegram/card-renderer.d.ts +63 -0
  2. package/dist/src/channels/telegram/card-renderer.js +149 -0
  3. package/dist/src/channels/telegram/card-renderer.js.map +1 -0
  4. package/dist/src/channels/telegram/insight-formatter.d.ts +36 -0
  5. package/dist/src/channels/telegram/insight-formatter.js +36 -0
  6. package/dist/src/channels/telegram/insight-formatter.js.map +1 -0
  7. package/dist/src/channels/telegram/live-card.d.ts +7 -90
  8. package/dist/src/channels/telegram/live-card.js +22 -265
  9. package/dist/src/channels/telegram/live-card.js.map +1 -1
  10. package/dist/src/channels/telegram/logger.d.ts +10 -0
  11. package/dist/src/channels/telegram/logger.js +13 -0
  12. package/dist/src/channels/telegram/logger.js.map +1 -0
  13. package/dist/src/channels/telegram/throttle-controller.d.ts +54 -0
  14. package/dist/src/channels/telegram/throttle-controller.js +132 -0
  15. package/dist/src/channels/telegram/throttle-controller.js.map +1 -0
  16. package/dist/src/channels/telegram/tool-tracker.js +3 -14
  17. package/dist/src/channels/telegram/tool-tracker.js.map +1 -1
  18. package/dist/src/cli/doctor.d.ts +24 -0
  19. package/dist/src/cli/doctor.js +194 -0
  20. package/dist/src/cli/doctor.js.map +1 -0
  21. package/dist/src/cli/index.d.ts +8 -0
  22. package/dist/src/cli/index.js +39 -0
  23. package/dist/src/cli/index.js.map +1 -0
  24. package/dist/src/command-router/cc-handler.js +35 -709
  25. package/dist/src/command-router/cc-handler.js.map +1 -1
  26. package/dist/src/command-router/launch-policy.d.ts +93 -0
  27. package/dist/src/command-router/launch-policy.js +323 -0
  28. package/dist/src/command-router/launch-policy.js.map +1 -0
  29. package/dist/src/command-router/resume-policy.d.ts +18 -0
  30. package/dist/src/command-router/resume-policy.js +236 -0
  31. package/dist/src/command-router/resume-policy.js.map +1 -0
  32. package/dist/src/command-router/turn-formatter.d.ts +19 -0
  33. package/dist/src/command-router/turn-formatter.js +144 -0
  34. package/dist/src/command-router/turn-formatter.js.map +1 -0
  35. package/dist/src/openai-compat/bridges/allowlist.d.ts +10 -0
  36. package/dist/src/openai-compat/bridges/allowlist.js +17 -0
  37. package/dist/src/openai-compat/bridges/allowlist.js.map +1 -0
  38. package/dist/src/openai-compat/bridges/factory.d.ts +30 -0
  39. package/dist/src/openai-compat/bridges/factory.js +84 -0
  40. package/dist/src/openai-compat/bridges/factory.js.map +1 -0
  41. package/dist/src/openai-compat/bridges/media-bridge.d.ts +34 -0
  42. package/dist/src/openai-compat/bridges/media-bridge.js +20 -0
  43. package/dist/src/openai-compat/bridges/media-bridge.js.map +1 -0
  44. package/dist/src/openai-compat/bridges/openclaw-native-tools.d.ts +61 -0
  45. package/dist/src/openai-compat/bridges/openclaw-native-tools.js +171 -0
  46. package/dist/src/openai-compat/bridges/openclaw-native-tools.js.map +1 -0
  47. package/dist/src/openai-compat/bridges/openclaw-tool-registry.d.ts +26 -0
  48. package/dist/src/openai-compat/bridges/openclaw-tool-registry.js +38 -0
  49. package/dist/src/openai-compat/bridges/openclaw-tool-registry.js.map +1 -0
  50. package/dist/src/openai-compat/bridges/tts-media-bridge.d.ts +19 -0
  51. package/dist/src/openai-compat/bridges/tts-media-bridge.js +59 -0
  52. package/dist/src/openai-compat/bridges/tts-media-bridge.js.map +1 -0
  53. package/dist/src/openai-compat/openai-compat.js.map +1 -1
  54. package/dist/src/types/tool-bridge.d.ts +2 -1
  55. package/dist/src/types/tool-bridge.js +1 -0
  56. package/dist/src/types/tool-bridge.js.map +1 -1
  57. package/package.json +1 -1
  58. package/dist/scripts/bench/ab-harness.d.ts +0 -58
  59. package/dist/scripts/bench/ab-harness.d.ts.map +0 -1
  60. package/dist/scripts/bench/ab-harness.js +0 -78
  61. package/dist/scripts/bench/ab-harness.js.map +0 -1
  62. package/dist/src/channels/adapter.d.ts.map +0 -1
  63. package/dist/src/channels/telegram/completion-summary.d.ts.map +0 -1
  64. package/dist/src/channels/telegram/error-renderer.d.ts.map +0 -1
  65. package/dist/src/channels/telegram/event-reducer.d.ts.map +0 -1
  66. package/dist/src/channels/telegram/index.d.ts.map +0 -1
  67. package/dist/src/channels/telegram/injector.d.ts.map +0 -1
  68. package/dist/src/channels/telegram/live-card.d.ts.map +0 -1
  69. package/dist/src/channels/telegram/state-machine.d.ts.map +0 -1
  70. package/dist/src/channels/telegram/tool-tracker.d.ts.map +0 -1
  71. package/dist/src/command-router/cc-handler.d.ts.map +0 -1
  72. package/dist/src/command-router/index.d.ts.map +0 -1
  73. package/dist/src/constants.d.ts.map +0 -1
  74. package/dist/src/council/consensus.d.ts.map +0 -1
  75. package/dist/src/council/council.d.ts.map +0 -1
  76. package/dist/src/council/index.d.ts.map +0 -1
  77. package/dist/src/engines/base-oneshot-session.d.ts.map +0 -1
  78. package/dist/src/engines/index.d.ts.map +0 -1
  79. package/dist/src/engines/persistent-codex-session.d.ts.map +0 -1
  80. package/dist/src/engines/persistent-cursor-session.d.ts.map +0 -1
  81. package/dist/src/engines/persistent-custom-session.d.ts.map +0 -1
  82. package/dist/src/engines/persistent-gemini-session.d.ts.map +0 -1
  83. package/dist/src/engines/persistent-session.d.ts.map +0 -1
  84. package/dist/src/health/handler.d.ts.map +0 -1
  85. package/dist/src/health/index.d.ts.map +0 -1
  86. package/dist/src/index.d.ts.map +0 -1
  87. package/dist/src/lib/auto-recovery.d.ts.map +0 -1
  88. package/dist/src/lib/cache-parity.d.ts.map +0 -1
  89. package/dist/src/lib/circuit-breaker.d.ts.map +0 -1
  90. package/dist/src/lib/config.d.ts.map +0 -1
  91. package/dist/src/lib/drift-detector.d.ts.map +0 -1
  92. package/dist/src/lib/error-formatter.d.ts.map +0 -1
  93. package/dist/src/lib/heartbeat-workaround.d.ts.map +0 -1
  94. package/dist/src/lib/index.d.ts.map +0 -1
  95. package/dist/src/lib/register-guard.d.ts.map +0 -1
  96. package/dist/src/lib/route-flag.d.ts +0 -49
  97. package/dist/src/lib/route-flag.d.ts.map +0 -1
  98. package/dist/src/lib/route-flag.js +0 -52
  99. package/dist/src/lib/route-flag.js.map +0 -1
  100. package/dist/src/lib/sysprompt-strip.d.ts.map +0 -1
  101. package/dist/src/lib/telemetry.d.ts.map +0 -1
  102. package/dist/src/lib/test-mode.d.ts.map +0 -1
  103. package/dist/src/lib/vendor-paths.d.ts.map +0 -1
  104. package/dist/src/logger.d.ts.map +0 -1
  105. package/dist/src/mcp/bridge.d.ts.map +0 -1
  106. package/dist/src/mcp/index.d.ts.map +0 -1
  107. package/dist/src/models.d.ts.map +0 -1
  108. package/dist/src/openai-compat/cli-stream-parser.d.ts.map +0 -1
  109. package/dist/src/openai-compat/index.d.ts.map +0 -1
  110. package/dist/src/openai-compat/openai-compat.d.ts.map +0 -1
  111. package/dist/src/openai-compat/skill-resolver.d.ts.map +0 -1
  112. package/dist/src/openai-compat/sse-translator.d.ts.map +0 -1
  113. package/dist/src/proxy/anthropic-adapter.d.ts.map +0 -1
  114. package/dist/src/proxy/handler.d.ts.map +0 -1
  115. package/dist/src/proxy/index.d.ts.map +0 -1
  116. package/dist/src/proxy/schema-cleaner.d.ts.map +0 -1
  117. package/dist/src/proxy/thought-cache.d.ts.map +0 -1
  118. package/dist/src/session/embedded-server.d.ts.map +0 -1
  119. package/dist/src/session/inbox-manager.d.ts.map +0 -1
  120. package/dist/src/session/index.d.ts.map +0 -1
  121. package/dist/src/session/session-manager.d.ts.map +0 -1
  122. package/dist/src/session-bootstrap/cwd-patch.d.ts.map +0 -1
  123. package/dist/src/session-bootstrap/index.d.ts.map +0 -1
  124. package/dist/src/session-bootstrap/sysprompt-strip.d.ts.map +0 -1
  125. package/dist/src/session-bootstrap/think-conflict-resolver.d.ts.map +0 -1
  126. package/dist/src/types.d.ts.map +0 -1
  127. package/dist/src/validation.d.ts.map +0 -1
  128. package/dist/tests/_helpers/subprocess-mock.d.ts +0 -35
  129. package/dist/tests/_helpers/subprocess-mock.d.ts.map +0 -1
  130. package/dist/tests/_helpers/subprocess-mock.js +0 -136
  131. package/dist/tests/_helpers/subprocess-mock.js.map +0 -1
  132. package/dist/tests/auto-recovery.test.d.ts +0 -2
  133. package/dist/tests/auto-recovery.test.d.ts.map +0 -1
  134. package/dist/tests/auto-recovery.test.js +0 -185
  135. package/dist/tests/auto-recovery.test.js.map +0 -1
  136. package/dist/tests/bench-harness.test.d.ts +0 -2
  137. package/dist/tests/bench-harness.test.d.ts.map +0 -1
  138. package/dist/tests/bench-harness.test.js +0 -21
  139. package/dist/tests/bench-harness.test.js.map +0 -1
  140. package/dist/tests/cache-parity.test.d.ts +0 -2
  141. package/dist/tests/cache-parity.test.d.ts.map +0 -1
  142. package/dist/tests/cache-parity.test.js +0 -401
  143. package/dist/tests/cache-parity.test.js.map +0 -1
  144. package/dist/tests/command-router.test.d.ts +0 -2
  145. package/dist/tests/command-router.test.d.ts.map +0 -1
  146. package/dist/tests/command-router.test.js +0 -60
  147. package/dist/tests/command-router.test.js.map +0 -1
  148. package/dist/tests/council.test.d.ts +0 -2
  149. package/dist/tests/council.test.d.ts.map +0 -1
  150. package/dist/tests/council.test.js +0 -20
  151. package/dist/tests/council.test.js.map +0 -1
  152. package/dist/tests/drift-detector.test.d.ts +0 -2
  153. package/dist/tests/drift-detector.test.d.ts.map +0 -1
  154. package/dist/tests/drift-detector.test.js +0 -268
  155. package/dist/tests/drift-detector.test.js.map +0 -1
  156. package/dist/tests/eager-bootstrap-gating.test.d.ts +0 -9
  157. package/dist/tests/eager-bootstrap-gating.test.d.ts.map +0 -1
  158. package/dist/tests/eager-bootstrap-gating.test.js +0 -97
  159. package/dist/tests/eager-bootstrap-gating.test.js.map +0 -1
  160. package/dist/tests/engines.test.d.ts +0 -2
  161. package/dist/tests/engines.test.d.ts.map +0 -1
  162. package/dist/tests/engines.test.js +0 -8
  163. package/dist/tests/engines.test.js.map +0 -1
  164. package/dist/tests/error-formatter.test.d.ts +0 -2
  165. package/dist/tests/error-formatter.test.d.ts.map +0 -1
  166. package/dist/tests/error-formatter.test.js +0 -220
  167. package/dist/tests/error-formatter.test.js.map +0 -1
  168. package/dist/tests/health.test.d.ts +0 -2
  169. package/dist/tests/health.test.d.ts.map +0 -1
  170. package/dist/tests/health.test.js +0 -110
  171. package/dist/tests/health.test.js.map +0 -1
  172. package/dist/tests/heartbeat-workaround.test.d.ts +0 -2
  173. package/dist/tests/heartbeat-workaround.test.d.ts.map +0 -1
  174. package/dist/tests/heartbeat-workaround.test.js +0 -90
  175. package/dist/tests/heartbeat-workaround.test.js.map +0 -1
  176. package/dist/tests/index.test.d.ts +0 -2
  177. package/dist/tests/index.test.d.ts.map +0 -1
  178. package/dist/tests/index.test.js +0 -7
  179. package/dist/tests/index.test.js.map +0 -1
  180. package/dist/tests/lib-sysprompt-strip.test.d.ts +0 -2
  181. package/dist/tests/lib-sysprompt-strip.test.d.ts.map +0 -1
  182. package/dist/tests/lib-sysprompt-strip.test.js +0 -145
  183. package/dist/tests/lib-sysprompt-strip.test.js.map +0 -1
  184. package/dist/tests/listener-activation.test.d.ts +0 -2
  185. package/dist/tests/listener-activation.test.d.ts.map +0 -1
  186. package/dist/tests/listener-activation.test.js +0 -87
  187. package/dist/tests/listener-activation.test.js.map +0 -1
  188. package/dist/tests/mcp-bridge.test.d.ts +0 -2
  189. package/dist/tests/mcp-bridge.test.d.ts.map +0 -1
  190. package/dist/tests/mcp-bridge.test.js +0 -137
  191. package/dist/tests/mcp-bridge.test.js.map +0 -1
  192. package/dist/tests/openai-compat.test.d.ts +0 -2
  193. package/dist/tests/openai-compat.test.d.ts.map +0 -1
  194. package/dist/tests/openai-compat.test.js +0 -8
  195. package/dist/tests/openai-compat.test.js.map +0 -1
  196. package/dist/tests/proxy-heartbeat-integration.test.d.ts +0 -15
  197. package/dist/tests/proxy-heartbeat-integration.test.d.ts.map +0 -1
  198. package/dist/tests/proxy-heartbeat-integration.test.js +0 -122
  199. package/dist/tests/proxy-heartbeat-integration.test.js.map +0 -1
  200. package/dist/tests/proxy.test.d.ts +0 -2
  201. package/dist/tests/proxy.test.d.ts.map +0 -1
  202. package/dist/tests/proxy.test.js +0 -8
  203. package/dist/tests/proxy.test.js.map +0 -1
  204. package/dist/tests/register-guard-stacking.test.d.ts +0 -2
  205. package/dist/tests/register-guard-stacking.test.d.ts.map +0 -1
  206. package/dist/tests/register-guard-stacking.test.js +0 -61
  207. package/dist/tests/register-guard-stacking.test.js.map +0 -1
  208. package/dist/tests/register-guard.test.d.ts +0 -2
  209. package/dist/tests/register-guard.test.d.ts.map +0 -1
  210. package/dist/tests/register-guard.test.js +0 -129
  211. package/dist/tests/register-guard.test.js.map +0 -1
  212. package/dist/tests/route-flag-rollback.test.d.ts +0 -2
  213. package/dist/tests/route-flag-rollback.test.d.ts.map +0 -1
  214. package/dist/tests/route-flag-rollback.test.js +0 -70
  215. package/dist/tests/route-flag-rollback.test.js.map +0 -1
  216. package/dist/tests/route-flag.test.d.ts +0 -2
  217. package/dist/tests/route-flag.test.d.ts.map +0 -1
  218. package/dist/tests/route-flag.test.js +0 -101
  219. package/dist/tests/route-flag.test.js.map +0 -1
  220. package/dist/tests/session-bootstrap.test.d.ts +0 -2
  221. package/dist/tests/session-bootstrap.test.d.ts.map +0 -1
  222. package/dist/tests/session-bootstrap.test.js +0 -183
  223. package/dist/tests/session-bootstrap.test.js.map +0 -1
  224. package/dist/tests/session.test.d.ts +0 -2
  225. package/dist/tests/session.test.d.ts.map +0 -1
  226. package/dist/tests/session.test.js +0 -17
  227. package/dist/tests/session.test.js.map +0 -1
  228. package/dist/tests/state-machine.test.d.ts +0 -2
  229. package/dist/tests/state-machine.test.d.ts.map +0 -1
  230. package/dist/tests/state-machine.test.js +0 -133
  231. package/dist/tests/state-machine.test.js.map +0 -1
  232. package/dist/tests/streaming/cli-stream-parser.test.d.ts +0 -2
  233. package/dist/tests/streaming/cli-stream-parser.test.d.ts.map +0 -1
  234. package/dist/tests/streaming/cli-stream-parser.test.js +0 -233
  235. package/dist/tests/streaming/cli-stream-parser.test.js.map +0 -1
  236. package/dist/tests/streaming/feature-flag.test.d.ts +0 -14
  237. package/dist/tests/streaming/feature-flag.test.d.ts.map +0 -1
  238. package/dist/tests/streaming/feature-flag.test.js +0 -163
  239. package/dist/tests/streaming/feature-flag.test.js.map +0 -1
  240. package/dist/tests/streaming/no-tools-prompt.test.d.ts +0 -17
  241. package/dist/tests/streaming/no-tools-prompt.test.d.ts.map +0 -1
  242. package/dist/tests/streaming/no-tools-prompt.test.js +0 -229
  243. package/dist/tests/streaming/no-tools-prompt.test.js.map +0 -1
  244. package/dist/tests/streaming/skill-plus-tools.test.d.ts +0 -14
  245. package/dist/tests/streaming/skill-plus-tools.test.d.ts.map +0 -1
  246. package/dist/tests/streaming/skill-plus-tools.test.js +0 -234
  247. package/dist/tests/streaming/skill-plus-tools.test.js.map +0 -1
  248. package/dist/tests/streaming/sse-translator.test.d.ts +0 -2
  249. package/dist/tests/streaming/sse-translator.test.d.ts.map +0 -1
  250. package/dist/tests/streaming/sse-translator.test.js +0 -227
  251. package/dist/tests/streaming/sse-translator.test.js.map +0 -1
  252. package/dist/tests/streaming/tool-result-roundtrip.test.d.ts +0 -11
  253. package/dist/tests/streaming/tool-result-roundtrip.test.d.ts.map +0 -1
  254. package/dist/tests/streaming/tool-result-roundtrip.test.js +0 -215
  255. package/dist/tests/streaming/tool-result-roundtrip.test.js.map +0 -1
  256. package/dist/tests/streaming/tool-use-translation.test.d.ts +0 -10
  257. package/dist/tests/streaming/tool-use-translation.test.d.ts.map +0 -1
  258. package/dist/tests/streaming/tool-use-translation.test.js +0 -251
  259. package/dist/tests/streaming/tool-use-translation.test.js.map +0 -1
  260. package/dist/tests/telegram-bridge.test.d.ts +0 -2
  261. package/dist/tests/telegram-bridge.test.d.ts.map +0 -1
  262. package/dist/tests/telegram-bridge.test.js +0 -17
  263. package/dist/tests/telegram-bridge.test.js.map +0 -1
  264. package/dist/tests/telegram-injector.test.d.ts +0 -2
  265. package/dist/tests/telegram-injector.test.d.ts.map +0 -1
  266. package/dist/tests/telegram-injector.test.js +0 -74
  267. package/dist/tests/telegram-injector.test.js.map +0 -1
  268. package/dist/tests/telemetry.test.d.ts +0 -2
  269. package/dist/tests/telemetry.test.d.ts.map +0 -1
  270. package/dist/tests/telemetry.test.js +0 -405
  271. package/dist/tests/telemetry.test.js.map +0 -1
  272. package/dist/tests/test-mode.test.d.ts +0 -2
  273. package/dist/tests/test-mode.test.d.ts.map +0 -1
  274. package/dist/tests/test-mode.test.js +0 -39
  275. package/dist/tests/test-mode.test.js.map +0 -1
@@ -14,271 +14,29 @@
14
14
  * /cc resume [id|slug] — resume by session ID (8-char hex) or slug
15
15
  * /cc list — show resumable sessions for this chat
16
16
  */
17
- import * as fs from 'node:fs';
18
- import * as path from 'node:path';
19
- import * as crypto from 'node:crypto';
20
- import { request as httpsRequest } from 'node:https';
21
17
  import { defaultRegisterGuard } from '../lib/register-guard.js';
22
18
  import { VENDOR_FILES } from '../lib/vendor-paths.js';
19
+ import { loadBotToken, sendDirectReply, hasBotToken, buildPhase2FlagsLine, buildObservabilityLines, } from './turn-formatter.js';
20
+ import { loadSessionIndex, saveSession, scanAllSessions, makeSlug, generateSessionId, sessionMapKey, launchSession, recoverSessions, } from './launch-policy.js';
21
+ import { handleContinuation, handleResume, } from './resume-policy.js';
23
22
  // ── Constants ─────────────────────────────────────────────────────────────
24
23
  const PLUGIN_TAG = '[cc-openclaw/command-router]';
25
24
  const CC_PLUGIN_PATH = VENDOR_FILES.sessionManager;
26
- const SESSIONS_DIR = '/home/a1xai/.openclaw/sessions';
27
- const SESSIONS_INDEX_PATH = path.join(SESSIONS_DIR, 'index.json');
28
- const DEFAULT_CWD = '/home/a1xai/.openclaw';
29
- const IDLE_TIMEOUT_MS = 15 * 60 * 1000; // 15 min
30
- const ACK_TIMEOUT_MS = 5_000;
31
- // v0.6.0 — post-turn context-management thresholds for Telegram/Savvy path.
32
- // The OpenAI-compat HTTP path has its own AUTO_COMPACT_THRESHOLD; cc-handler
33
- // (Telegram path) was missing this entirely — long sessions accumulated
34
- // indefinitely until manual kill. Importing instead of redefining so a
35
- // single source of truth in constants.ts stays canonical.
36
- import { CC_AUTO_COMPACT_THRESHOLD, CC_HARD_RESET_THRESHOLD } from '../constants.js';
37
- // Track last compaction attempt per session — avoid hammering /compact when
38
- // a turn finishes near-threshold and the next turn pushes it over again
39
- // before /compact's effect lands. 60s cooldown is plenty.
40
- const _lastCompactAt = new Map();
41
- const COMPACT_COOLDOWN_MS = 60_000;
42
- /**
43
- * Post-turn context-pressure check. Called after every successful sendMessage.
44
- * - At ≥ CC_HARD_RESET_THRESHOLD: stop the session (recreated on next turn).
45
- * Surfaces a Telegram message so the user knows context was reset.
46
- * - At ≥ CC_AUTO_COMPACT_THRESHOLD: in-place /compact via stdin. ~10-30s,
47
- * non-blocking for the current turn (fire-and-forget; the compaction lands
48
- * before the user's next message hits the queue per the per-session
49
- * promise chain in SessionManager.sendMessage).
50
- *
51
- * Idempotent: cooldown prevents duplicate /compact calls within 60s.
52
- * Non-fatal: failures logged + swallowed so a compaction error never breaks
53
- * the user-visible answer-delivery path.
54
- */
55
- async function _postTurnContextCheck(mgr, sessionName, chatId, threadId) {
56
- try {
57
- const status = mgr.getStatus(sessionName);
58
- const pct = status?.stats?.contextPercent ?? 0;
59
- if (pct >= CC_HARD_RESET_THRESHOLD) {
60
- logger.warn(`${PLUGIN_TAG} [ctx-mgmt] ${sessionName} at ${pct}% — hard-resetting session`);
61
- try {
62
- await mgr.stopSession(sessionName);
63
- }
64
- catch (e) {
65
- logger.warn(`${PLUGIN_TAG} hard-reset stopSession failed: ${e instanceof Error ? e.message : String(e)}`);
66
- }
67
- if (chatId) {
68
- try {
69
- await sendDirectReply(chatId, threadId, `⚠️ Session context reached ${pct}% — auto-reset. Your next message starts fresh.`);
70
- }
71
- catch { /* best effort */ }
72
- }
73
- return;
74
- }
75
- if (pct >= CC_AUTO_COMPACT_THRESHOLD) {
76
- const lastAttempt = _lastCompactAt.get(sessionName) || 0;
77
- if (Date.now() - lastAttempt < COMPACT_COOLDOWN_MS) {
78
- return; // recent compact attempt — let it settle
79
- }
80
- _lastCompactAt.set(sessionName, Date.now());
81
- logger.info(`${PLUGIN_TAG} [ctx-mgmt] ${sessionName} at ${pct}% — running /compact`);
82
- if (typeof mgr.compactSession === 'function') {
83
- // Fire-and-forget: SessionManager serializes via per-session promise chain,
84
- // so the next user turn will block on this compact (correct behavior).
85
- mgr.compactSession(sessionName).catch((e) => {
86
- logger.warn(`${PLUGIN_TAG} compactSession failed for ${sessionName}: ${e instanceof Error ? e.message : String(e)}`);
87
- });
88
- }
89
- }
90
- }
91
- catch (e) {
92
- logger.warn(`${PLUGIN_TAG} _postTurnContextCheck error: ${e instanceof Error ? e.message : String(e)}`);
93
- }
94
- }
95
- // Max age for session recovery on warm restart.
96
- const RECOVERY_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24h
97
- const OPENCLAW_CONFIG_PATH = '/home/a1xai/.openclaw/openclaw.json';
98
- // ── Telegram Direct Send ──────────────────────────────────────────────────
99
- let BOT_TOKEN = '';
100
- function loadBotToken() {
101
- if (BOT_TOKEN)
102
- return;
103
- try {
104
- const raw = fs.readFileSync(OPENCLAW_CONFIG_PATH, 'utf8');
105
- const cfg = JSON.parse(raw);
106
- const tg = cfg.channels?.telegram;
107
- if (tg?.accounts) {
108
- const acctKey = tg.defaultAccount || 'default';
109
- const acct = tg.accounts[acctKey] || {};
110
- BOT_TOKEN = acct.botToken || '';
111
- }
112
- if (BOT_TOKEN)
113
- logger.info(`${PLUGIN_TAG} Bot token loaded for direct replies`);
114
- }
115
- catch (err) {
116
- logger.warn(`${PLUGIN_TAG} Failed to load bot token: ${err instanceof Error ? err.message : String(err)}`);
117
- }
118
- }
119
- function telegramApiDirect(method, params) {
120
- return new Promise((resolve, reject) => {
121
- const body = JSON.stringify(params);
122
- const options = {
123
- hostname: 'api.telegram.org',
124
- path: `/bot${BOT_TOKEN}/${method}`,
125
- method: 'POST',
126
- headers: {
127
- 'Content-Type': 'application/json',
128
- 'Content-Length': Buffer.byteLength(body),
129
- },
130
- };
131
- const req = httpsRequest(options, (res) => {
132
- let data = '';
133
- res.on('data', (chunk) => (data += chunk));
134
- res.on('end', () => {
135
- try {
136
- resolve(JSON.parse(data));
137
- }
138
- catch {
139
- resolve({ ok: false });
140
- }
141
- });
142
- });
143
- req.on('error', reject);
144
- req.setTimeout(10_000, () => req.destroy(new Error('Telegram API timeout')));
145
- req.write(body);
146
- req.end();
147
- });
148
- }
149
- async function sendDirectReply(chatId, threadId, text) {
150
- if (!BOT_TOKEN || !chatId)
151
- return null;
152
- try {
153
- const params = {
154
- chat_id: chatId,
155
- text,
156
- disable_web_page_preview: true,
157
- };
158
- if (threadId)
159
- params.message_thread_id = Number(threadId);
160
- const res = await telegramApiDirect('sendMessage', params);
161
- if (!res.ok) {
162
- logger.warn(`${PLUGIN_TAG} Direct reply failed: ${JSON.stringify(res.description || res)}`);
163
- return null;
164
- }
165
- return res.result?.message_id ?? null;
166
- }
167
- catch (err) {
168
- logger.warn(`${PLUGIN_TAG} Direct reply error: ${err instanceof Error ? err.message : String(err)}`);
169
- return null;
170
- }
171
- }
172
- // ── Slug Generator ────────────────────────────────────────────────────────
173
- const FILLER_WORDS = new Set([
174
- 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
175
- 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
176
- 'should', 'may', 'might', 'shall', 'can', 'to', 'of', 'in', 'for',
177
- 'on', 'with', 'at', 'by', 'from', 'it', 'its', 'this', 'that', 'and',
178
- 'or', 'but', 'not', 'so', 'if', 'then', 'than', 'just', 'also',
179
- 'very', 'really', 'please', 'me', 'my', 'i',
180
- ]);
181
- function makeSlug(instruction) {
182
- const tokens = instruction
183
- .toLowerCase()
184
- .replace(/[^a-z0-9\s-]/g, '')
185
- .split(/\s+/)
186
- .filter((t) => t.length > 0 && !FILLER_WORDS.has(t));
187
- const suffix = Date.now().toString(36).slice(-4);
188
- if (tokens.length === 0) {
189
- return `task-${suffix}`;
190
- }
191
- let slug = tokens.slice(0, 4).join('-');
192
- if (slug.length > 24) {
193
- slug = slug.slice(0, 24).replace(/-$/, '');
194
- }
195
- return `${slug}-${suffix}`;
196
- }
197
- // ── Session ID Generator ──────────────────────────────────────────────────
198
- function generateSessionId() {
199
- return crypto.randomBytes(4).toString('hex');
200
- }
201
- // ── Session Index ─────────────────────────────────────────────────────────
202
- let sessionIndex = {};
203
- function loadSessionIndex() {
204
- try {
205
- if (fs.existsSync(SESSIONS_INDEX_PATH)) {
206
- sessionIndex = JSON.parse(fs.readFileSync(SESSIONS_INDEX_PATH, 'utf8'));
207
- }
208
- }
209
- catch {
210
- sessionIndex = {};
211
- }
212
- }
213
- function saveSessionIndex() {
214
- try {
215
- fs.mkdirSync(SESSIONS_DIR, { recursive: true });
216
- const tmp = SESSIONS_INDEX_PATH + '.tmp';
217
- fs.writeFileSync(tmp, JSON.stringify(sessionIndex, null, 2));
218
- fs.renameSync(tmp, SESSIONS_INDEX_PATH);
219
- }
220
- catch { /* best effort */ }
221
- }
222
- // ── Session Store ─────────────────────────────────────────────────────────
223
- function saveSession(meta) {
224
- const dir = path.join(SESSIONS_DIR, meta.slug);
225
- fs.mkdirSync(dir, { recursive: true });
226
- const filePath = path.join(dir, 'session.json');
227
- const tmp = filePath + '.tmp';
228
- fs.writeFileSync(tmp, JSON.stringify(meta, null, 2));
229
- fs.renameSync(tmp, filePath);
230
- if (meta.id) {
231
- sessionIndex[meta.id] = meta.slug;
232
- saveSessionIndex();
233
- }
234
- }
235
- function loadSession(slug) {
236
- const filePath = path.join(SESSIONS_DIR, slug, 'session.json');
237
- try {
238
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
239
- }
240
- catch {
241
- return null;
242
- }
243
- }
244
- function loadSessionById(id) {
245
- const slug = sessionIndex[id];
246
- if (slug)
247
- return loadSession(slug);
248
- const all = scanAllSessions();
249
- const match = all.find((s) => s.id === id);
250
- if (match) {
251
- sessionIndex[id] = match.slug;
252
- saveSessionIndex();
253
- }
254
- return match ?? null;
255
- }
256
- function scanAllSessions() {
257
- const results = [];
258
- try {
259
- if (!fs.existsSync(SESSIONS_DIR))
260
- return results;
261
- const entries = fs.readdirSync(SESSIONS_DIR, { withFileTypes: true });
262
- for (const entry of entries) {
263
- if (!entry.isDirectory())
264
- continue;
265
- const meta = loadSession(entry.name);
266
- if (meta && meta.slug && meta.chatId) {
267
- results.push(meta);
268
- }
269
- }
270
- }
271
- catch {
272
- // Best effort — if sessions dir is unreadable, return empty
273
- }
274
- return results;
275
- }
25
+ // DispatchResult is imported from ./launch-policy.js (shared with resume-policy.ts).
26
+ // CcConfig and SessionManagerLike are imported from ./launch-policy.js
27
+ // (Phase 5 v0.11.0 decomposition — single source of truth for shared types).
276
28
  // ── Plugin Entry ──────────────────────────────────────────────────────────
29
+ // (Slug generator, session ID generator, session index, session store, and
30
+ // sessionMapKey were extracted to launch-policy.ts in Phase 5 v0.11.0.)
277
31
  let logger = console;
278
32
  let sessionManager = null;
279
33
  const activeSessions = new Map();
280
- function sessionMapKey(chatId, threadId) {
281
- return threadId ? `${chatId}:${threadId}` : String(chatId);
34
+ // PolicyDeps builder — cc-handler.ts owns singletons; policy functions receive
35
+ // them explicitly per PRP §7 Q3. Call sites use `_deps()` to avoid repeating
36
+ // the three-field literal. `sessionManager!` is safe: every call site has
37
+ // already null-checked or the surrounding service.start guarantees it.
38
+ function _deps() {
39
+ return { sessionManager: sessionManager, activeSessions, logger };
282
40
  }
283
41
  // ── parseCcCommand — pure exported parser ─────────────────────────────────
284
42
  /**
@@ -344,11 +102,6 @@ export function parseCcCommand(input) {
344
102
  return { subcommand: 'new', prompt: afterCc };
345
103
  }
346
104
  // ── Status Handler ────────────────────────────────────────────────────────
347
- function buildPhase2FlagsLine() {
348
- const allowBuiltins = process.env.CC_OPENCLAW_ALLOW_BUILTINS === '1';
349
- const toolStream = process.env.CC_OPENCLAW_TOOL_STREAM === '1';
350
- return `Flags: builtins=${allowBuiltins ? 'on' : 'off'} | toolstream=${toolStream ? 'on' : 'off'}`;
351
- }
352
105
  function handleStatus(chatId, threadId) {
353
106
  const key = sessionMapKey(chatId, threadId);
354
107
  const active = activeSessions.get(key);
@@ -379,53 +132,6 @@ function handleStatus(chatId, threadId) {
379
132
  text: `Active: [${id}] ${active.slug} (${turns} turn${turns > 1 ? 's' : ''}, ${elapsed}s)\n${obsLines}\n${flagsLine}`,
380
133
  };
381
134
  }
382
- /**
383
- * Pillar C v0.4.0 — compose observability lines for /cc status panel.
384
- * Reads from Pillar A metrics, Pillar B trajectory tail, and status-tee
385
- * quota snapshot. Each source is independently fault-tolerant: if one
386
- * fails, the line shows "?" rather than crashing the panel.
387
- */
388
- function buildObservabilityLines() {
389
- const lines = [];
390
- // Plan quota (status-tee)
391
- try {
392
- // Lazy require so this file remains testable without status-tee
393
- // eslint-disable-next-line @typescript-eslint/no-require-imports
394
- const { readQuotaSnapshot, formatQuotaLine } = require('../lib/status-tee-reader.js');
395
- lines.push(formatQuotaLine(readQuotaSnapshot()));
396
- }
397
- catch {
398
- lines.push('Plan: ?%');
399
- }
400
- // Workers + uptime + last error (Pillar A metrics)
401
- try {
402
- // eslint-disable-next-line @typescript-eslint/no-require-imports
403
- const { metricsRegistry } = require('../health/metrics.js');
404
- const out = metricsRegistry.serializePrometheus();
405
- const workersMatch = /cc_openclaw_workers (\d+)/.exec(out);
406
- const uptimeMatch = /cc_openclaw_uptime_seconds (\d+)/.exec(out);
407
- const lastErrMatch = /cc_openclaw_last_error_seconds (\d+)/.exec(out);
408
- const errorsMatch = /cc_openclaw_errors_total (\d+)/.exec(out);
409
- const workers = workersMatch ? workersMatch[1] : '?';
410
- const uptime = uptimeMatch ? Math.floor(parseInt(uptimeMatch[1], 10) / 60) : '?';
411
- lines.push(`Workers: ${workers} | Uptime: ${uptime}m`);
412
- const errCount = errorsMatch ? parseInt(errorsMatch[1], 10) : 0;
413
- const lastErrTs = lastErrMatch ? parseInt(lastErrMatch[1], 10) : 0;
414
- if (errCount === 0 || lastErrTs === 0) {
415
- lines.push('Last error: none');
416
- }
417
- else {
418
- const agoSec = Math.floor(Date.now() / 1000) - lastErrTs;
419
- const agoLabel = agoSec < 60 ? `${agoSec}s` : agoSec < 3600 ? `${Math.floor(agoSec / 60)}m` : `${Math.floor(agoSec / 3600)}h`;
420
- lines.push(`Last error: ${agoLabel} ago (${errCount} total)`);
421
- }
422
- }
423
- catch {
424
- lines.push('Workers: ? | Uptime: ?');
425
- lines.push('Last error: ?');
426
- }
427
- return lines.join('\n');
428
- }
429
135
  // ── List Handler ──────────────────────────────────────────────────────────
430
136
  function handleList(chatId, threadId) {
431
137
  const sessions = scanAllSessions()
@@ -455,78 +161,7 @@ function handleList(chatId, threadId) {
455
161
  };
456
162
  }
457
163
  // ── Continuation Handler ──────────────────────────────────────────────────
458
- function handleContinuation(prompt, chatId, threadId) {
459
- if (!sessionManager) {
460
- return {
461
- handled: true,
462
- text: 'Claude Code handler not ready — try again in a few seconds.',
463
- };
464
- }
465
- const key = sessionMapKey(chatId, threadId);
466
- const active = activeSessions.get(key);
467
- if (!active) {
468
- return {
469
- handled: true,
470
- text: 'No active session to continue. Start one with /cc <prompt> or /cc resume',
471
- };
472
- }
473
- if (active.needsRehydration) {
474
- if (!active.meta.claudeSessionId) {
475
- activeSessions.delete(key);
476
- return {
477
- handled: true,
478
- text: 'Recovered session has no Claude ID — start fresh with /cc <prompt>',
479
- };
480
- }
481
- logger.info(`${PLUGIN_TAG} /cc+ rehydrating recovered session: slug=${active.slug} chat=${chatId} thread=${threadId || 'none'}`);
482
- let resolveAckId;
483
- const ackIdPromise = new Promise((r) => {
484
- resolveAckId = r;
485
- });
486
- (async () => {
487
- try {
488
- await rehydrateSession(active.meta, chatId, threadId);
489
- active.needsRehydration = false;
490
- await continueTurn(active, prompt, chatId, threadId, ackIdPromise);
491
- }
492
- catch (err) {
493
- logger.error(`${PLUGIN_TAG} Rehydrate+continue ${active.slug} failed: ${err instanceof Error ? err.message : String(err)}`);
494
- scheduleIdle(key, active);
495
- }
496
- })();
497
- return {
498
- handled: true,
499
- text: `⏳ [${active.meta.id || '?'}] Resuming & continuing ${active.slug}...`,
500
- _resolveAckId: resolveAckId,
501
- };
502
- }
503
- try {
504
- sessionManager.getStatus(active.sessionName);
505
- }
506
- catch {
507
- activeSessions.delete(key);
508
- return {
509
- handled: true,
510
- text: 'Previous session expired. Use /cc resume or start fresh with /cc <prompt>',
511
- };
512
- }
513
- if (active.idleTimer)
514
- clearTimeout(active.idleTimer);
515
- logger.info(`${PLUGIN_TAG} /cc+ continuation: slug=${active.slug} chat=${chatId} thread=${threadId || 'none'}`);
516
- let resolveAckId;
517
- const ackIdPromise = new Promise((r) => {
518
- resolveAckId = r;
519
- });
520
- continueTurn(active, prompt, chatId, threadId, ackIdPromise).catch((err) => {
521
- logger.error(`${PLUGIN_TAG} Continuation ${active.slug} failed: ${err instanceof Error ? err.message : String(err)}`);
522
- scheduleIdle(key, active);
523
- });
524
- return {
525
- handled: true,
526
- text: `⏳ [${active.meta.id || '?'}] Continuing ${active.slug}...`,
527
- _resolveAckId: resolveAckId,
528
- };
529
- }
164
+ // handleContinuation moved to resume-policy.ts in Step 4 of v0.11.0 decomposition.
530
165
  // ── Stop Handler ──────────────────────────────────────────────────────────
531
166
  function handleStop(chatId, threadId) {
532
167
  if (!sessionManager) {
@@ -563,327 +198,12 @@ function handleStop(chatId, threadId) {
563
198
  };
564
199
  }
565
200
  // ── Resume Handler ────────────────────────────────────────────────────────
566
- function handleResume(chatId, threadId, targetSlug) {
567
- if (!sessionManager) {
568
- return { handled: true, text: 'Claude Code handler not ready.' };
569
- }
570
- const key = sessionMapKey(chatId, threadId);
571
- const currentActive = activeSessions.get(key);
572
- if (currentActive) {
573
- return {
574
- handled: true,
575
- text: `Already have active session: ${currentActive.slug}\nUse /cc stop first, or /cc+ to continue it.`,
576
- };
577
- }
578
- let meta = null;
579
- if (targetSlug) {
580
- const isSessionId = /^[0-9a-f]{8}$/i.test(targetSlug);
581
- if (isSessionId) {
582
- meta = loadSessionById(targetSlug.toLowerCase());
583
- }
584
- if (!meta) {
585
- meta = loadSession(targetSlug);
586
- }
587
- if (!meta) {
588
- return { handled: true, text: `Session "${targetSlug}" not found.` };
589
- }
590
- if (meta.chatId !== chatId) {
591
- return {
592
- handled: true,
593
- text: `Session "${targetSlug}" belongs to a different chat.`,
594
- };
595
- }
596
- }
597
- else {
598
- const candidates = scanAllSessions()
599
- .filter((s) => s.chatId === chatId &&
600
- (s.threadId || undefined) === (threadId || undefined) &&
601
- !!s.claudeSessionId)
602
- .sort((a, b) => {
603
- const timeA = new Date(a.lastContinuedAt || a.completedAt || a.startedAt).getTime();
604
- const timeB = new Date(b.lastContinuedAt || b.completedAt || b.startedAt).getTime();
605
- return timeB - timeA;
606
- });
607
- if (candidates.length === 0) {
608
- return {
609
- handled: true,
610
- text: 'No resumable session found. Start fresh with /cc <prompt>',
611
- };
612
- }
613
- meta = candidates[0];
614
- }
615
- if (!meta.claudeSessionId) {
616
- return {
617
- handled: true,
618
- text: `Session ${meta.slug} has no Claude session ID — cannot resume.\nStart fresh with /cc <prompt>`,
619
- };
620
- }
621
- const { slug, id: resumeId } = meta;
622
- logger.info(`${PLUGIN_TAG} /cc resume: id=${resumeId} slug=${slug} claudeId=${meta.claudeSessionId} chat=${chatId} thread=${threadId || 'none'}`);
623
- rehydrateSession(meta, chatId, threadId).catch((err) => {
624
- logger.error(`${PLUGIN_TAG} Resume ${slug} failed: ${err instanceof Error ? err.message : String(err)}`);
625
- meta.state = 'failed';
626
- meta.error = `Resume failed: ${err instanceof Error ? err.message : String(err)}`;
627
- try {
628
- saveSession(meta);
629
- }
630
- catch { /* best effort */ }
631
- const active = activeSessions.get(key);
632
- if (active?.slug === slug) {
633
- if (active.idleTimer)
634
- clearTimeout(active.idleTimer);
635
- activeSessions.delete(key);
636
- }
637
- });
638
- return { handled: true, text: `⏳ [${resumeId || '?'}] Resuming ${slug}...` };
639
- }
640
- // ── Session Launcher ──────────────────────────────────────────────────────
641
- async function launchSession(sessionName, prompt, slug, meta, chatId, threadId, ackIdPromise) {
642
- logger.info(`${PLUGIN_TAG} Starting session ${sessionName}...`);
643
- const key = sessionMapKey(chatId, threadId);
644
- // v0.8.0 Fix 4: drop customSessionId pre-assignment. Pre-v0.8.0 we
645
- // generated a UUID via crypto.randomUUID() and forced it into claude.exe
646
- // via --session-id, then stored that same UUID as meta.claudeSessionId.
647
- // Risk: if Claude CLI's --session-id semantics ever drift (e.g. it stops
648
- // honoring the override and assigns its own UUID anyway), the stored
649
- // meta.claudeSessionId no longer matches the actual JSONL filename in
650
- // ~/.claude/projects/, breaking resume. Adopt the openai-compat pattern:
651
- // let claude.exe assign its own UUID via system.init event, then read it
652
- // back. meta.claudeSessionId is updated after the first turn completes
653
- // (line below — `meta.claudeSessionId = result?.sessionId`). One source
654
- // of truth: the CLI's authoritative session_id.
655
- meta.state = 'starting';
656
- try {
657
- saveSession(meta);
658
- }
659
- catch { /* best effort */ }
660
- await sessionManager.startSession({
661
- name: sessionName,
662
- cwd: DEFAULT_CWD,
663
- engine: 'claude',
664
- permissionMode: 'bypassPermissions',
665
- effort: 'high',
666
- });
667
- meta.state = 'running';
668
- try {
669
- saveSession(meta);
670
- }
671
- catch { /* best effort */ }
672
- const prevActive = activeSessions.get(key);
673
- if (prevActive && prevActive.sessionName !== sessionName) {
674
- if (prevActive.idleTimer)
675
- clearTimeout(prevActive.idleTimer);
676
- try {
677
- await sessionManager.stopSession(prevActive.sessionName);
678
- }
679
- catch { /* may already be gone */ }
680
- }
681
- const activeEntry = { sessionName, slug, meta, idleTimer: null };
682
- activeSessions.set(key, activeEntry);
683
- logger.info(`${PLUGIN_TAG} Session ${sessionName} started — sending prompt...`);
684
- const opts = { telegramChatId: chatId };
685
- if (threadId)
686
- opts.telegramThreadId = threadId;
687
- const ackMsgId = await Promise.race([
688
- ackIdPromise,
689
- new Promise((r) => setTimeout(() => r(null), ACK_TIMEOUT_MS)),
690
- ]);
691
- if (ackMsgId)
692
- opts.telegramAckMessageId = ackMsgId;
693
- const result = await sessionManager.sendMessage(sessionName, prompt, opts);
694
- meta.state = 'idle';
695
- meta.turns = 1;
696
- meta.completedAt = new Date().toISOString();
697
- meta.output = (result?.output || '').slice(0, 2000);
698
- meta.claudeSessionId = result?.sessionId ?? meta.claudeSessionId;
699
- try {
700
- saveSession(meta);
701
- }
702
- catch { /* best effort */ }
703
- // v0.6.0: post-turn context-pressure check.
704
- // Fires /compact at CC_AUTO_COMPACT_THRESHOLD (70%) and hard-resets at
705
- // CC_HARD_RESET_THRESHOLD (90%). Was previously absent — Telegram path
706
- // had no context-management at all, requiring manual session kill.
707
- void _postTurnContextCheck(sessionManager, sessionName, chatId, threadId);
708
- // 2026-05-07 FIX — direct answer delivery via cc-handler.
709
- // The live-card pipeline's queue.sendMessage path was silently dropping
710
- // /cc answer text (gateOpens=true but Telegram never received). Diagnosed
711
- // via stderr instrumentation 2026-05-07: result.output = "10" extracted
712
- // correctly but never reached the user. cc-handler is the orchestrator —
713
- // it has authoritative knowledge of (chatId, threadId, answer text).
714
- // Delivering directly here closes the loop reliably. If/when cc-openclaw
715
- // Phase 2 ships native tool-stream wire fidelity, this can stay as the
716
- // belt-and-suspenders final-delivery guarantee.
717
- if (result?.output && chatId) {
718
- try {
719
- await sendDirectReply(chatId, threadId, result.output);
720
- }
721
- catch (e) {
722
- logger.warn(`${PLUGIN_TAG} Direct answer delivery failed for ${sessionName}: ${e instanceof Error ? e.message : String(e)}`);
723
- }
724
- }
725
- logger.info(`${PLUGIN_TAG} Session ${sessionName} turn 1 complete — idle for ${IDLE_TIMEOUT_MS / 1000}s`);
726
- scheduleIdle(key, activeEntry);
727
- }
728
- // ── Rehydrate Session ─────────────────────────────────────────────────────
729
- async function rehydrateSession(meta, chatId, threadId) {
730
- const { slug, sessionName, claudeSessionId } = meta;
731
- const key = sessionMapKey(chatId, threadId);
732
- logger.info(`${PLUGIN_TAG} Rehydrating ${sessionName} with claudeSessionId=${claudeSessionId}...`);
733
- await sessionManager.startSession({
734
- name: sessionName,
735
- cwd: meta.cwd || DEFAULT_CWD,
736
- engine: 'claude',
737
- permissionMode: 'bypassPermissions',
738
- effort: 'high',
739
- resumeSessionId: claudeSessionId,
740
- });
741
- const prevActive = activeSessions.get(key);
742
- if (prevActive && prevActive.sessionName !== sessionName) {
743
- if (prevActive.idleTimer)
744
- clearTimeout(prevActive.idleTimer);
745
- try {
746
- await sessionManager.stopSession(prevActive.sessionName);
747
- }
748
- catch { /* ignore */ }
749
- }
750
- meta.state = 'idle';
751
- meta.threadId = threadId;
752
- meta.lastContinuedAt = new Date().toISOString();
753
- try {
754
- saveSession(meta);
755
- }
756
- catch { /* best effort */ }
757
- const activeEntry = { sessionName, slug, meta, idleTimer: null };
758
- activeSessions.set(key, activeEntry);
759
- logger.info(`${PLUGIN_TAG} Session ${sessionName} resumed — idle for ${IDLE_TIMEOUT_MS / 1000}s`);
760
- scheduleIdle(key, activeEntry);
761
- }
762
- // ── Continuation Turn ─────────────────────────────────────────────────────
763
- async function continueTurn(active, prompt, chatId, threadId, ackIdPromise) {
764
- const { sessionName, meta } = active;
765
- const key = sessionMapKey(chatId, threadId);
766
- meta.state = 'running';
767
- try {
768
- saveSession(meta);
769
- }
770
- catch { /* best effort */ }
771
- const opts = { telegramChatId: chatId };
772
- if (threadId)
773
- opts.telegramThreadId = threadId;
774
- const ackMsgId = await Promise.race([
775
- ackIdPromise,
776
- new Promise((r) => setTimeout(() => r(null), ACK_TIMEOUT_MS)),
777
- ]);
778
- if (ackMsgId)
779
- opts.telegramAckMessageId = ackMsgId;
780
- const result = await sessionManager.sendMessage(sessionName, prompt, opts);
781
- meta.state = 'idle';
782
- meta.turns = (meta.turns || 1) + 1;
783
- meta.lastContinuedAt = new Date().toISOString();
784
- meta.output = (result?.output || '').slice(0, 2000);
785
- meta.claudeSessionId = result?.sessionId ?? meta.claudeSessionId;
786
- try {
787
- saveSession(meta);
788
- }
789
- catch { /* best effort */ }
790
- // v0.6.0: post-turn context-pressure check (continuation turn).
791
- // Same logic as the start path — see _postTurnContextCheck.
792
- void _postTurnContextCheck(sessionManager, sessionName, chatId, threadId);
793
- // 2026-05-07 FIX — direct answer delivery (continuation turn).
794
- // See start-path comment for rationale.
795
- if (result?.output && chatId) {
796
- try {
797
- await sendDirectReply(chatId, threadId, result.output);
798
- }
799
- catch (e) {
800
- logger.warn(`${PLUGIN_TAG} Direct answer delivery failed for ${sessionName} (continuation): ${e instanceof Error ? e.message : String(e)}`);
801
- }
802
- }
803
- logger.info(`${PLUGIN_TAG} Session ${sessionName} turn ${meta.turns} complete`);
804
- scheduleIdle(key, active);
805
- }
806
- // ── Idle Timer ────────────────────────────────────────────────────────────
807
- function scheduleIdle(key, active) {
808
- if (active.idleTimer)
809
- clearTimeout(active.idleTimer);
810
- active.idleTimer = setTimeout(() => {
811
- logger.info(`${PLUGIN_TAG} Session ${active.sessionName} idle timeout — stopping`);
812
- try {
813
- sessionManager
814
- ?.stopSession(active.sessionName)
815
- .catch((e) => logger.warn(`${PLUGIN_TAG} idle stop error: ${e.message}`));
816
- }
817
- catch { /* may already be gone */ }
818
- activeSessions.delete(key);
819
- active.meta.state = 'idle';
820
- active.meta.completedAt = new Date().toISOString();
821
- try {
822
- saveSession(active.meta);
823
- }
824
- catch { /* best effort */ }
825
- }, IDLE_TIMEOUT_MS);
826
- }
827
- // ── Warm-Restart Recovery ─────────────────────────────────────────────────
828
- function recoverSessions() {
829
- const now = Date.now();
830
- const allSessions = scanAllSessions();
831
- let indexDirty = false;
832
- for (const meta of allSessions) {
833
- if (!meta.id) {
834
- meta.id = generateSessionId();
835
- sessionIndex[meta.id] = meta.slug;
836
- indexDirty = true;
837
- try {
838
- saveSession(meta);
839
- }
840
- catch { /* best effort */ }
841
- logger.info(`${PLUGIN_TAG} Backfilled session ID ${meta.id} for ${meta.slug}`);
842
- }
843
- else if (!sessionIndex[meta.id]) {
844
- sessionIndex[meta.id] = meta.slug;
845
- indexDirty = true;
846
- }
847
- }
848
- if (indexDirty)
849
- saveSessionIndex();
850
- let recovered = 0;
851
- for (const meta of allSessions) {
852
- if (!['running', 'idle', 'timed_out'].includes(meta.state))
853
- continue;
854
- const lastActivity = new Date(meta.lastContinuedAt || meta.completedAt || meta.startedAt).getTime();
855
- if (now - lastActivity > RECOVERY_MAX_AGE_MS)
856
- continue;
857
- if (!meta.claudeSessionId)
858
- continue;
859
- const recoverKey = sessionMapKey(meta.chatId, meta.threadId);
860
- if (activeSessions.has(recoverKey))
861
- continue;
862
- if (meta.state === 'running') {
863
- meta.state = 'interrupted';
864
- try {
865
- saveSession(meta);
866
- }
867
- catch { /* best effort */ }
868
- }
869
- const activeEntry = {
870
- sessionName: meta.sessionName,
871
- slug: meta.slug,
872
- meta,
873
- idleTimer: null,
874
- needsRehydration: true,
875
- };
876
- activeSessions.set(recoverKey, activeEntry);
877
- recovered++;
878
- logger.info(`${PLUGIN_TAG} Recovered session ${meta.slug} for chat ${meta.chatId} thread=${meta.threadId || 'none'} (was ${meta.state === 'interrupted' ? 'running' : 'idle'})`);
879
- }
880
- if (recovered > 0) {
881
- logger.info(`${PLUGIN_TAG} Warm-restart recovery: ${recovered} session(s) restored`);
882
- }
883
- else {
884
- logger.info(`${PLUGIN_TAG} Warm-restart recovery: no recent sessions to restore`);
885
- }
886
- }
201
+ // handleResume moved to resume-policy.ts in Step 4 of v0.11.0 decomposition.
202
+ // rehydrateSession and continueTurn also moved there (called only by the
203
+ // resume-policy handlers, so they live alongside them).
204
+ // ── Session Launcher / Idle Timer / Recovery ──────────────────────────────
205
+ // launchSession, scheduleIdle, recoverSessions moved to launch-policy.ts
206
+ // in Step 3 of v0.11.0 decomposition.
887
207
  // ── Plugin Object & register() ────────────────────────────────────────────
888
208
  /**
889
209
  * Register the cc-handler plugin with the OpenClaw API.
@@ -917,7 +237,7 @@ export function register(api) {
917
237
  catch {
918
238
  logger.warn(`${PLUGIN_TAG} Could not read openclaw-claude-code config — using defaults`);
919
239
  }
920
- loadBotToken();
240
+ loadBotToken(logger);
921
241
  // ── Lifecycle service ────────────────────────────────────────────────
922
242
  api.registerService({
923
243
  id: 'telegram-cc-handler',
@@ -956,7 +276,7 @@ export function register(api) {
956
276
  }
957
277
  sessionManager = new SessionManager(ccConfig);
958
278
  logger.info(`${PLUGIN_TAG} SessionManager ready — /cc commands active`);
959
- recoverSessions();
279
+ recoverSessions(_deps());
960
280
  }
961
281
  catch (err) {
962
282
  logger.error(`${PLUGIN_TAG} Failed to init SessionManager: ${err instanceof Error ? err.message : String(err)}`);
@@ -998,7 +318,9 @@ export function register(api) {
998
318
  const prompt = ccPlusMatch[1].trim();
999
319
  if (!prompt)
1000
320
  return { handled: true, text: 'Usage: /cc+ <prompt>' };
1001
- return handleContinuation(prompt, chatId, threadId);
321
+ if (!sessionManager)
322
+ return { handled: true, text: 'Claude Code handler not ready — try again in a few seconds.' };
323
+ return handleContinuation(_deps(), prompt, chatId, threadId);
1002
324
  }
1003
325
  if (!content.match(/^\/cc(\s|$)/i))
1004
326
  return undefined;
@@ -1008,7 +330,9 @@ export function register(api) {
1008
330
  const prompt = continueMatch[1].trim();
1009
331
  if (!prompt)
1010
332
  return { handled: true, text: 'Usage: /cc continue <prompt>' };
1011
- return handleContinuation(prompt, chatId, threadId);
333
+ if (!sessionManager)
334
+ return { handled: true, text: 'Claude Code handler not ready — try again in a few seconds.' };
335
+ return handleContinuation(_deps(), prompt, chatId, threadId);
1012
336
  }
1013
337
  if (afterCc.toLowerCase() === 'stop') {
1014
338
  return handleStop(chatId, threadId);
@@ -1016,7 +340,9 @@ export function register(api) {
1016
340
  const resumeMatch = afterCc.match(/^resume(?:\s+(.+))?$/i);
1017
341
  if (resumeMatch) {
1018
342
  const targetSlug = (resumeMatch[1] || '').trim() || null;
1019
- return handleResume(chatId, threadId, targetSlug);
343
+ if (!sessionManager)
344
+ return { handled: true, text: 'Claude Code handler not ready.' };
345
+ return handleResume(_deps(), chatId, threadId, targetSlug);
1020
346
  }
1021
347
  if (afterCc.toLowerCase() === 'status') {
1022
348
  return handleStatus(chatId, threadId);
@@ -1077,7 +403,7 @@ export function register(api) {
1077
403
  resolveAckId = r;
1078
404
  });
1079
405
  const launchKey = sessionMapKey(chatId, threadId);
1080
- launchSession(sessionName, prompt, slug, meta, chatId, threadId, ackIdPromise).catch((err) => {
406
+ launchSession(_deps(), sessionName, prompt, slug, meta, chatId, threadId, ackIdPromise).catch((err) => {
1081
407
  logger.error(`${PLUGIN_TAG} Session ${sessionName} failed: ${err.message}`);
1082
408
  try {
1083
409
  const isTimeout = /timeout/i.test(err.message);
@@ -1108,7 +434,7 @@ export function register(api) {
1108
434
  return result;
1109
435
  if (event && typeof event === 'object')
1110
436
  event.content = '';
1111
- if (result.text && BOT_TOKEN) {
437
+ if (result.text && hasBotToken()) {
1112
438
  const convId = (ctx.conversationId || '').replace(/^telegram:/i, '');
1113
439
  const tm = convId.match(/^(-?\d+):topic:(\d+)$/);
1114
440
  const ackChat = tm
@@ -1118,7 +444,7 @@ export function register(api) {
1118
444
  : event.senderId || '';
1119
445
  const ackThread = tm ? tm[2] : undefined;
1120
446
  logger.info(`${PLUGIN_TAG} direct-send ack to chat=${ackChat} thread=${ackThread || 'none'}`);
1121
- const ackMessageId = await sendDirectReply(ackChat, ackThread, result.text);
447
+ const ackMessageId = await sendDirectReply(ackChat, ackThread, result.text, logger);
1122
448
  if (result._resolveAckId) {
1123
449
  result._resolveAckId(ackMessageId ?? null);
1124
450
  }