@a1hvdy/cc-openclaw 0.9.2 → 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.
- package/dist/src/channels/telegram/card-renderer.d.ts +63 -0
- package/dist/src/channels/telegram/card-renderer.js +149 -0
- package/dist/src/channels/telegram/card-renderer.js.map +1 -0
- package/dist/src/channels/telegram/completion-summary.js +20 -1
- package/dist/src/channels/telegram/completion-summary.js.map +1 -1
- package/dist/src/channels/telegram/insight-formatter.d.ts +36 -0
- package/dist/src/channels/telegram/insight-formatter.js +36 -0
- package/dist/src/channels/telegram/insight-formatter.js.map +1 -0
- package/dist/src/channels/telegram/live-card.d.ts +7 -90
- package/dist/src/channels/telegram/live-card.js +22 -265
- package/dist/src/channels/telegram/live-card.js.map +1 -1
- package/dist/src/channels/telegram/logger.d.ts +10 -0
- package/dist/src/channels/telegram/logger.js +13 -0
- package/dist/src/channels/telegram/logger.js.map +1 -0
- package/dist/src/channels/telegram/throttle-controller.d.ts +54 -0
- package/dist/src/channels/telegram/throttle-controller.js +132 -0
- package/dist/src/channels/telegram/throttle-controller.js.map +1 -0
- package/dist/src/channels/telegram/tool-tracker.js +3 -14
- package/dist/src/channels/telegram/tool-tracker.js.map +1 -1
- package/dist/src/cli/doctor.d.ts +24 -0
- package/dist/src/cli/doctor.js +194 -0
- package/dist/src/cli/doctor.js.map +1 -0
- package/dist/src/cli/index.d.ts +8 -0
- package/dist/src/cli/index.js +39 -0
- package/dist/src/cli/index.js.map +1 -0
- package/dist/src/command-router/cc-handler.js +35 -709
- package/dist/src/command-router/cc-handler.js.map +1 -1
- package/dist/src/command-router/launch-policy.d.ts +93 -0
- package/dist/src/command-router/launch-policy.js +323 -0
- package/dist/src/command-router/launch-policy.js.map +1 -0
- package/dist/src/command-router/resume-policy.d.ts +18 -0
- package/dist/src/command-router/resume-policy.js +236 -0
- package/dist/src/command-router/resume-policy.js.map +1 -0
- package/dist/src/command-router/turn-formatter.d.ts +19 -0
- package/dist/src/command-router/turn-formatter.js +144 -0
- package/dist/src/command-router/turn-formatter.js.map +1 -0
- package/dist/src/constants.d.ts +33 -2
- package/dist/src/constants.js +33 -2
- package/dist/src/constants.js.map +1 -1
- package/dist/src/lib/config.d.ts +3 -0
- package/dist/src/lib/config.js +46 -0
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/lib/trajectory.d.ts +1 -1
- package/dist/src/lib/trajectory.js.map +1 -1
- package/dist/src/openai-compat/bridges/allowlist.d.ts +10 -0
- package/dist/src/openai-compat/bridges/allowlist.js +17 -0
- package/dist/src/openai-compat/bridges/allowlist.js.map +1 -0
- package/dist/src/openai-compat/bridges/factory.d.ts +30 -0
- package/dist/src/openai-compat/bridges/factory.js +84 -0
- package/dist/src/openai-compat/bridges/factory.js.map +1 -0
- package/dist/src/openai-compat/bridges/media-bridge.d.ts +34 -0
- package/dist/src/openai-compat/bridges/media-bridge.js +20 -0
- package/dist/src/openai-compat/bridges/media-bridge.js.map +1 -0
- package/dist/src/openai-compat/bridges/openclaw-native-tools.d.ts +61 -0
- package/dist/src/openai-compat/bridges/openclaw-native-tools.js +171 -0
- package/dist/src/openai-compat/bridges/openclaw-native-tools.js.map +1 -0
- package/dist/src/openai-compat/bridges/openclaw-tool-registry.d.ts +26 -0
- package/dist/src/openai-compat/bridges/openclaw-tool-registry.js +38 -0
- package/dist/src/openai-compat/bridges/openclaw-tool-registry.js.map +1 -0
- package/dist/src/openai-compat/bridges/tts-media-bridge.d.ts +19 -0
- package/dist/src/openai-compat/bridges/tts-media-bridge.js +59 -0
- package/dist/src/openai-compat/bridges/tts-media-bridge.js.map +1 -0
- package/dist/src/openai-compat/non-streaming-handler.js +52 -3
- package/dist/src/openai-compat/non-streaming-handler.js.map +1 -1
- package/dist/src/openai-compat/openai-compat.js +64 -2
- package/dist/src/openai-compat/openai-compat.js.map +1 -1
- package/dist/src/openai-compat/streaming-handler.js +107 -1
- package/dist/src/openai-compat/streaming-handler.js.map +1 -1
- package/dist/src/openai-compat/voice-recovery.d.ts +56 -0
- package/dist/src/openai-compat/voice-recovery.js +231 -0
- package/dist/src/openai-compat/voice-recovery.js.map +1 -0
- package/dist/src/session/session-manager.d.ts +51 -0
- package/dist/src/session/session-manager.js +165 -1
- package/dist/src/session/session-manager.js.map +1 -1
- package/dist/src/types/tool-bridge.d.ts +2 -1
- package/dist/src/types/tool-bridge.js +1 -0
- package/dist/src/types/tool-bridge.js.map +1 -1
- package/package.json +1 -1
- package/dist/scripts/bench/ab-harness.d.ts +0 -58
- package/dist/scripts/bench/ab-harness.d.ts.map +0 -1
- package/dist/scripts/bench/ab-harness.js +0 -78
- package/dist/scripts/bench/ab-harness.js.map +0 -1
- package/dist/src/channels/adapter.d.ts.map +0 -1
- package/dist/src/channels/telegram/completion-summary.d.ts.map +0 -1
- package/dist/src/channels/telegram/error-renderer.d.ts.map +0 -1
- package/dist/src/channels/telegram/event-reducer.d.ts.map +0 -1
- package/dist/src/channels/telegram/index.d.ts.map +0 -1
- package/dist/src/channels/telegram/injector.d.ts.map +0 -1
- package/dist/src/channels/telegram/live-card.d.ts.map +0 -1
- package/dist/src/channels/telegram/state-machine.d.ts.map +0 -1
- package/dist/src/channels/telegram/tool-tracker.d.ts.map +0 -1
- package/dist/src/command-router/cc-handler.d.ts.map +0 -1
- package/dist/src/command-router/index.d.ts.map +0 -1
- package/dist/src/constants.d.ts.map +0 -1
- package/dist/src/council/consensus.d.ts.map +0 -1
- package/dist/src/council/council.d.ts.map +0 -1
- package/dist/src/council/index.d.ts.map +0 -1
- package/dist/src/engines/base-oneshot-session.d.ts.map +0 -1
- package/dist/src/engines/index.d.ts.map +0 -1
- package/dist/src/engines/persistent-codex-session.d.ts.map +0 -1
- package/dist/src/engines/persistent-cursor-session.d.ts.map +0 -1
- package/dist/src/engines/persistent-custom-session.d.ts.map +0 -1
- package/dist/src/engines/persistent-gemini-session.d.ts.map +0 -1
- package/dist/src/engines/persistent-session.d.ts.map +0 -1
- package/dist/src/health/handler.d.ts.map +0 -1
- package/dist/src/health/index.d.ts.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/lib/auto-recovery.d.ts.map +0 -1
- package/dist/src/lib/cache-parity.d.ts.map +0 -1
- package/dist/src/lib/circuit-breaker.d.ts.map +0 -1
- package/dist/src/lib/config.d.ts.map +0 -1
- package/dist/src/lib/debug-tap.d.ts.map +0 -1
- package/dist/src/lib/drift-detector.d.ts.map +0 -1
- package/dist/src/lib/error-formatter.d.ts.map +0 -1
- package/dist/src/lib/heartbeat-workaround.d.ts.map +0 -1
- package/dist/src/lib/index.d.ts.map +0 -1
- package/dist/src/lib/register-guard.d.ts.map +0 -1
- package/dist/src/lib/route-flag.d.ts +0 -49
- package/dist/src/lib/route-flag.d.ts.map +0 -1
- package/dist/src/lib/route-flag.js +0 -52
- package/dist/src/lib/route-flag.js.map +0 -1
- package/dist/src/lib/sysprompt-strip.d.ts.map +0 -1
- package/dist/src/lib/telemetry.d.ts.map +0 -1
- package/dist/src/lib/test-mode.d.ts.map +0 -1
- package/dist/src/lib/vendor-paths.d.ts.map +0 -1
- package/dist/src/logger.d.ts.map +0 -1
- package/dist/src/mcp/bridge.d.ts.map +0 -1
- package/dist/src/mcp/index.d.ts.map +0 -1
- package/dist/src/models.d.ts.map +0 -1
- package/dist/src/openai-compat/cli-stream-parser.d.ts.map +0 -1
- package/dist/src/openai-compat/index.d.ts.map +0 -1
- package/dist/src/openai-compat/openai-compat.d.ts.map +0 -1
- package/dist/src/openai-compat/skill-resolver.d.ts.map +0 -1
- package/dist/src/openai-compat/sse-translator.d.ts.map +0 -1
- package/dist/src/proxy/anthropic-adapter.d.ts.map +0 -1
- package/dist/src/proxy/handler.d.ts.map +0 -1
- package/dist/src/proxy/index.d.ts.map +0 -1
- package/dist/src/proxy/schema-cleaner.d.ts.map +0 -1
- package/dist/src/proxy/thought-cache.d.ts.map +0 -1
- package/dist/src/session/embedded-server.d.ts.map +0 -1
- package/dist/src/session/inbox-manager.d.ts.map +0 -1
- package/dist/src/session/index.d.ts.map +0 -1
- package/dist/src/session/session-manager.d.ts.map +0 -1
- package/dist/src/session-bootstrap/cwd-patch.d.ts.map +0 -1
- package/dist/src/session-bootstrap/index.d.ts.map +0 -1
- package/dist/src/session-bootstrap/sysprompt-strip.d.ts.map +0 -1
- package/dist/src/session-bootstrap/think-conflict-resolver.d.ts.map +0 -1
- package/dist/src/types.d.ts.map +0 -1
- package/dist/src/validation.d.ts.map +0 -1
- package/dist/tests/_helpers/subprocess-mock.d.ts +0 -35
- package/dist/tests/_helpers/subprocess-mock.d.ts.map +0 -1
- package/dist/tests/_helpers/subprocess-mock.js +0 -136
- package/dist/tests/_helpers/subprocess-mock.js.map +0 -1
- package/dist/tests/auto-recovery.test.d.ts +0 -2
- package/dist/tests/auto-recovery.test.d.ts.map +0 -1
- package/dist/tests/auto-recovery.test.js +0 -189
- package/dist/tests/auto-recovery.test.js.map +0 -1
- package/dist/tests/bench-harness.test.d.ts +0 -2
- package/dist/tests/bench-harness.test.d.ts.map +0 -1
- package/dist/tests/bench-harness.test.js +0 -21
- package/dist/tests/bench-harness.test.js.map +0 -1
- package/dist/tests/cache-parity.test.d.ts +0 -2
- package/dist/tests/cache-parity.test.d.ts.map +0 -1
- package/dist/tests/cache-parity.test.js +0 -401
- package/dist/tests/cache-parity.test.js.map +0 -1
- package/dist/tests/command-router.test.d.ts +0 -2
- package/dist/tests/command-router.test.d.ts.map +0 -1
- package/dist/tests/command-router.test.js +0 -60
- package/dist/tests/command-router.test.js.map +0 -1
- package/dist/tests/council.test.d.ts +0 -2
- package/dist/tests/council.test.d.ts.map +0 -1
- package/dist/tests/council.test.js +0 -20
- package/dist/tests/council.test.js.map +0 -1
- package/dist/tests/drift-detector.test.d.ts +0 -2
- package/dist/tests/drift-detector.test.d.ts.map +0 -1
- package/dist/tests/drift-detector.test.js +0 -268
- package/dist/tests/drift-detector.test.js.map +0 -1
- package/dist/tests/eager-bootstrap-gating.test.d.ts +0 -9
- package/dist/tests/eager-bootstrap-gating.test.d.ts.map +0 -1
- package/dist/tests/eager-bootstrap-gating.test.js +0 -97
- package/dist/tests/eager-bootstrap-gating.test.js.map +0 -1
- package/dist/tests/engines.test.d.ts +0 -2
- package/dist/tests/engines.test.d.ts.map +0 -1
- package/dist/tests/engines.test.js +0 -8
- package/dist/tests/engines.test.js.map +0 -1
- package/dist/tests/error-formatter.test.d.ts +0 -2
- package/dist/tests/error-formatter.test.d.ts.map +0 -1
- package/dist/tests/error-formatter.test.js +0 -220
- package/dist/tests/error-formatter.test.js.map +0 -1
- package/dist/tests/health.test.d.ts +0 -2
- package/dist/tests/health.test.d.ts.map +0 -1
- package/dist/tests/health.test.js +0 -110
- package/dist/tests/health.test.js.map +0 -1
- package/dist/tests/heartbeat-workaround.test.d.ts +0 -2
- package/dist/tests/heartbeat-workaround.test.d.ts.map +0 -1
- package/dist/tests/heartbeat-workaround.test.js +0 -90
- package/dist/tests/heartbeat-workaround.test.js.map +0 -1
- package/dist/tests/index.test.d.ts +0 -2
- package/dist/tests/index.test.d.ts.map +0 -1
- package/dist/tests/index.test.js +0 -7
- package/dist/tests/index.test.js.map +0 -1
- package/dist/tests/lib-sysprompt-strip.test.d.ts +0 -2
- package/dist/tests/lib-sysprompt-strip.test.d.ts.map +0 -1
- package/dist/tests/lib-sysprompt-strip.test.js +0 -145
- package/dist/tests/lib-sysprompt-strip.test.js.map +0 -1
- package/dist/tests/listener-activation.test.d.ts +0 -2
- package/dist/tests/listener-activation.test.d.ts.map +0 -1
- package/dist/tests/listener-activation.test.js +0 -87
- package/dist/tests/listener-activation.test.js.map +0 -1
- package/dist/tests/mcp-bridge.test.d.ts +0 -2
- package/dist/tests/mcp-bridge.test.d.ts.map +0 -1
- package/dist/tests/mcp-bridge.test.js +0 -137
- package/dist/tests/mcp-bridge.test.js.map +0 -1
- package/dist/tests/openai-compat.test.d.ts +0 -2
- package/dist/tests/openai-compat.test.d.ts.map +0 -1
- package/dist/tests/openai-compat.test.js +0 -8
- package/dist/tests/openai-compat.test.js.map +0 -1
- package/dist/tests/proxy-heartbeat-integration.test.d.ts +0 -15
- package/dist/tests/proxy-heartbeat-integration.test.d.ts.map +0 -1
- package/dist/tests/proxy-heartbeat-integration.test.js +0 -122
- package/dist/tests/proxy-heartbeat-integration.test.js.map +0 -1
- package/dist/tests/proxy.test.d.ts +0 -2
- package/dist/tests/proxy.test.d.ts.map +0 -1
- package/dist/tests/proxy.test.js +0 -8
- package/dist/tests/proxy.test.js.map +0 -1
- package/dist/tests/register-guard-stacking.test.d.ts +0 -2
- package/dist/tests/register-guard-stacking.test.d.ts.map +0 -1
- package/dist/tests/register-guard-stacking.test.js +0 -61
- package/dist/tests/register-guard-stacking.test.js.map +0 -1
- package/dist/tests/register-guard.test.d.ts +0 -2
- package/dist/tests/register-guard.test.d.ts.map +0 -1
- package/dist/tests/register-guard.test.js +0 -129
- package/dist/tests/register-guard.test.js.map +0 -1
- package/dist/tests/route-flag-rollback.test.d.ts +0 -2
- package/dist/tests/route-flag-rollback.test.d.ts.map +0 -1
- package/dist/tests/route-flag-rollback.test.js +0 -70
- package/dist/tests/route-flag-rollback.test.js.map +0 -1
- package/dist/tests/route-flag.test.d.ts +0 -2
- package/dist/tests/route-flag.test.d.ts.map +0 -1
- package/dist/tests/route-flag.test.js +0 -101
- package/dist/tests/route-flag.test.js.map +0 -1
- package/dist/tests/session-bootstrap.test.d.ts +0 -2
- package/dist/tests/session-bootstrap.test.d.ts.map +0 -1
- package/dist/tests/session-bootstrap.test.js +0 -183
- package/dist/tests/session-bootstrap.test.js.map +0 -1
- package/dist/tests/session.test.d.ts +0 -2
- package/dist/tests/session.test.d.ts.map +0 -1
- package/dist/tests/session.test.js +0 -17
- package/dist/tests/session.test.js.map +0 -1
- package/dist/tests/state-machine.test.d.ts +0 -2
- package/dist/tests/state-machine.test.d.ts.map +0 -1
- package/dist/tests/state-machine.test.js +0 -133
- package/dist/tests/state-machine.test.js.map +0 -1
- package/dist/tests/streaming/cli-stream-parser.test.d.ts +0 -2
- package/dist/tests/streaming/cli-stream-parser.test.d.ts.map +0 -1
- package/dist/tests/streaming/cli-stream-parser.test.js +0 -233
- package/dist/tests/streaming/cli-stream-parser.test.js.map +0 -1
- package/dist/tests/streaming/feature-flag.test.d.ts +0 -14
- package/dist/tests/streaming/feature-flag.test.d.ts.map +0 -1
- package/dist/tests/streaming/feature-flag.test.js +0 -163
- package/dist/tests/streaming/feature-flag.test.js.map +0 -1
- package/dist/tests/streaming/no-tools-prompt.test.d.ts +0 -17
- package/dist/tests/streaming/no-tools-prompt.test.d.ts.map +0 -1
- package/dist/tests/streaming/no-tools-prompt.test.js +0 -229
- package/dist/tests/streaming/no-tools-prompt.test.js.map +0 -1
- package/dist/tests/streaming/skill-plus-tools.test.d.ts +0 -14
- package/dist/tests/streaming/skill-plus-tools.test.d.ts.map +0 -1
- package/dist/tests/streaming/skill-plus-tools.test.js +0 -234
- package/dist/tests/streaming/skill-plus-tools.test.js.map +0 -1
- package/dist/tests/streaming/sse-translator.test.d.ts +0 -2
- package/dist/tests/streaming/sse-translator.test.d.ts.map +0 -1
- package/dist/tests/streaming/sse-translator.test.js +0 -227
- package/dist/tests/streaming/sse-translator.test.js.map +0 -1
- package/dist/tests/streaming/tool-result-roundtrip.test.d.ts +0 -11
- package/dist/tests/streaming/tool-result-roundtrip.test.d.ts.map +0 -1
- package/dist/tests/streaming/tool-result-roundtrip.test.js +0 -215
- package/dist/tests/streaming/tool-result-roundtrip.test.js.map +0 -1
- package/dist/tests/streaming/tool-use-translation.test.d.ts +0 -10
- package/dist/tests/streaming/tool-use-translation.test.d.ts.map +0 -1
- package/dist/tests/streaming/tool-use-translation.test.js +0 -251
- package/dist/tests/streaming/tool-use-translation.test.js.map +0 -1
- package/dist/tests/telegram-bridge.test.d.ts +0 -2
- package/dist/tests/telegram-bridge.test.d.ts.map +0 -1
- package/dist/tests/telegram-bridge.test.js +0 -17
- package/dist/tests/telegram-bridge.test.js.map +0 -1
- package/dist/tests/telegram-injector.test.d.ts +0 -2
- package/dist/tests/telegram-injector.test.d.ts.map +0 -1
- package/dist/tests/telegram-injector.test.js +0 -74
- package/dist/tests/telegram-injector.test.js.map +0 -1
- package/dist/tests/telemetry.test.d.ts +0 -2
- package/dist/tests/telemetry.test.d.ts.map +0 -1
- package/dist/tests/telemetry.test.js +0 -405
- package/dist/tests/telemetry.test.js.map +0 -1
- package/dist/tests/test-mode.test.d.ts +0 -2
- package/dist/tests/test-mode.test.d.ts.map +0 -1
- package/dist/tests/test-mode.test.js +0 -39
- 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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
281
|
-
|
|
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
|
-
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
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
|
}
|