botmux 2.9.1 → 2.9.3
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/README.en.md +140 -76
- package/README.md +134 -75
- package/dist/adapters/backend/pty-backend.d.ts +6 -0
- package/dist/adapters/backend/pty-backend.d.ts.map +1 -1
- package/dist/adapters/backend/pty-backend.js +10 -0
- package/dist/adapters/backend/pty-backend.js.map +1 -1
- package/dist/adapters/backend/session-backend-selector.d.ts +11 -0
- package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -0
- package/dist/adapters/backend/session-backend-selector.js +26 -0
- package/dist/adapters/backend/session-backend-selector.js.map +1 -0
- package/dist/adapters/backend/tmux-backend.d.ts +80 -3
- package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-backend.js +301 -49
- package/dist/adapters/backend/tmux-backend.js.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.d.ts +100 -0
- package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -0
- package/dist/adapters/backend/tmux-pipe-backend.js +473 -0
- package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -0
- package/dist/adapters/cli/aiden.d.ts.map +1 -1
- package/dist/adapters/cli/aiden.js +5 -0
- package/dist/adapters/cli/aiden.js.map +1 -1
- package/dist/adapters/cli/claude-code.d.ts +40 -1
- package/dist/adapters/cli/claude-code.d.ts.map +1 -1
- package/dist/adapters/cli/claude-code.js +470 -49
- package/dist/adapters/cli/claude-code.js.map +1 -1
- package/dist/adapters/cli/coco.d.ts.map +1 -1
- package/dist/adapters/cli/coco.js +191 -9
- package/dist/adapters/cli/coco.js.map +1 -1
- package/dist/adapters/cli/codex.d.ts.map +1 -1
- package/dist/adapters/cli/codex.js +94 -17
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/adapters/cli/shared-hints.d.ts +2 -2
- package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
- package/dist/adapters/cli/shared-hints.js +7 -5
- package/dist/adapters/cli/shared-hints.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +38 -1
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/autostart.d.ts +14 -0
- package/dist/autostart.d.ts.map +1 -0
- package/dist/autostart.js +357 -0
- package/dist/autostart.js.map +1 -0
- package/dist/bot-registry.d.ts +29 -3
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +91 -12
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli/arg-utils.d.ts +11 -0
- package/dist/cli/arg-utils.d.ts.map +1 -0
- package/dist/cli/arg-utils.js +25 -0
- package/dist/cli/arg-utils.js.map +1 -0
- package/dist/cli/create-group-resolver.d.ts +32 -0
- package/dist/cli/create-group-resolver.d.ts.map +1 -0
- package/dist/cli/create-group-resolver.js +70 -0
- package/dist/cli/create-group-resolver.js.map +1 -0
- package/dist/cli/quoted-render.d.ts +30 -0
- package/dist/cli/quoted-render.d.ts.map +1 -0
- package/dist/cli/quoted-render.js +29 -0
- package/dist/cli/quoted-render.js.map +1 -0
- package/dist/cli.js +916 -272
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -8
- package/dist/config.js.map +1 -1
- package/dist/core/command-handler.d.ts +43 -0
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +167 -64
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/dashboard-events.d.ts +57 -0
- package/dist/core/dashboard-events.d.ts.map +1 -0
- package/dist/core/dashboard-events.js +23 -0
- package/dist/core/dashboard-events.js.map +1 -0
- package/dist/core/dashboard-ipc-server.d.ts +43 -0
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -0
- package/dist/core/dashboard-ipc-server.js +481 -0
- package/dist/core/dashboard-ipc-server.js.map +1 -0
- package/dist/core/dashboard-locate.d.ts +20 -0
- package/dist/core/dashboard-locate.d.ts.map +1 -0
- package/dist/core/dashboard-locate.js +26 -0
- package/dist/core/dashboard-locate.js.map +1 -0
- package/dist/core/dashboard-rows.d.ts +31 -0
- package/dist/core/dashboard-rows.d.ts.map +1 -0
- package/dist/core/dashboard-rows.js +65 -0
- package/dist/core/dashboard-rows.js.map +1 -0
- package/dist/core/inherit-peer.d.ts +14 -0
- package/dist/core/inherit-peer.d.ts.map +1 -0
- package/dist/core/inherit-peer.js +32 -0
- package/dist/core/inherit-peer.js.map +1 -0
- package/dist/core/scheduler.d.ts +24 -0
- package/dist/core/scheduler.d.ts.map +1 -1
- package/dist/core/scheduler.js +93 -2
- package/dist/core/scheduler.js.map +1 -1
- package/dist/core/session-activity.d.ts +3 -0
- package/dist/core/session-activity.d.ts.map +1 -0
- package/dist/core/session-activity.js +20 -0
- package/dist/core/session-activity.js.map +1 -0
- package/dist/core/session-discovery.d.ts +39 -0
- package/dist/core/session-discovery.d.ts.map +1 -1
- package/dist/core/session-discovery.js +114 -21
- package/dist/core/session-discovery.js.map +1 -1
- package/dist/core/session-manager.d.ts +72 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +396 -106
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/types.d.ts +27 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +14 -3
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts +72 -3
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +459 -38
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +645 -314
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/aggregator.d.ts +41 -0
- package/dist/dashboard/aggregator.d.ts.map +1 -0
- package/dist/dashboard/aggregator.js +125 -0
- package/dist/dashboard/aggregator.js.map +1 -0
- package/dist/dashboard/auth.d.ts +23 -0
- package/dist/dashboard/auth.d.ts.map +1 -0
- package/dist/dashboard/auth.js +66 -0
- package/dist/dashboard/auth.js.map +1 -0
- package/dist/dashboard/operator-selector.d.ts +20 -0
- package/dist/dashboard/operator-selector.d.ts.map +1 -0
- package/dist/dashboard/operator-selector.js +39 -0
- package/dist/dashboard/operator-selector.js.map +1 -0
- package/dist/dashboard/registry.d.ts +35 -0
- package/dist/dashboard/registry.d.ts.map +1 -0
- package/dist/dashboard/registry.js +74 -0
- package/dist/dashboard/registry.js.map +1 -0
- package/dist/dashboard/web/app.d.ts +2 -0
- package/dist/dashboard/web/app.d.ts.map +1 -0
- package/dist/dashboard/web/app.js +45 -0
- package/dist/dashboard/web/app.js.map +1 -0
- package/dist/dashboard/web/bot-defaults.d.ts +2 -0
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -0
- package/dist/dashboard/web/bot-defaults.js +201 -0
- package/dist/dashboard/web/bot-defaults.js.map +1 -0
- package/dist/dashboard/web/groups.d.ts +16 -0
- package/dist/dashboard/web/groups.d.ts.map +1 -0
- package/dist/dashboard/web/groups.js +584 -0
- package/dist/dashboard/web/groups.js.map +1 -0
- package/dist/dashboard/web/schedules.d.ts +2 -0
- package/dist/dashboard/web/schedules.d.ts.map +1 -0
- package/dist/dashboard/web/schedules.js +105 -0
- package/dist/dashboard/web/schedules.js.map +1 -0
- package/dist/dashboard/web/sessions.d.ts +2 -0
- package/dist/dashboard/web/sessions.d.ts.map +1 -0
- package/dist/dashboard/web/sessions.js +374 -0
- package/dist/dashboard/web/sessions.js.map +1 -0
- package/dist/dashboard/web/store.d.ts +23 -0
- package/dist/dashboard/web/store.d.ts.map +1 -0
- package/dist/dashboard/web/store.js +82 -0
- package/dist/dashboard/web/store.js.map +1 -0
- package/dist/dashboard-web/app.js +263 -0
- package/dist/dashboard-web/index.html +23 -0
- package/dist/dashboard-web/style.css +93 -0
- package/dist/dashboard.d.ts +2 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +639 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/im/lark/card-builder.d.ts +18 -1
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +70 -9
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +123 -109
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/client.d.ts +35 -0
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +114 -11
- package/dist/im/lark/client.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts +88 -6
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +398 -62
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/forwarded-renderer.d.ts +23 -0
- package/dist/im/lark/forwarded-renderer.d.ts.map +1 -0
- package/dist/im/lark/forwarded-renderer.js +105 -0
- package/dist/im/lark/forwarded-renderer.js.map +1 -0
- package/dist/im/lark/md-card.d.ts +73 -0
- package/dist/im/lark/md-card.d.ts.map +1 -0
- package/dist/im/lark/md-card.js +332 -0
- package/dist/im/lark/md-card.js.map +1 -0
- package/dist/im/lark/merge-forward.d.ts +32 -0
- package/dist/im/lark/merge-forward.d.ts.map +1 -0
- package/dist/im/lark/merge-forward.js +110 -0
- package/dist/im/lark/merge-forward.js.map +1 -0
- package/dist/im/lark/message-parser.d.ts +9 -3
- package/dist/im/lark/message-parser.d.ts.map +1 -1
- package/dist/im/lark/message-parser.js +48 -13
- package/dist/im/lark/message-parser.js.map +1 -1
- package/dist/im/lark/quote-hint.d.ts +18 -0
- package/dist/im/lark/quote-hint.d.ts.map +1 -0
- package/dist/im/lark/quote-hint.js +23 -0
- package/dist/im/lark/quote-hint.js.map +1 -0
- package/dist/services/bridge-fallback-gate.d.ts +42 -0
- package/dist/services/bridge-fallback-gate.d.ts.map +1 -0
- package/dist/services/bridge-fallback-gate.js +12 -0
- package/dist/services/bridge-fallback-gate.js.map +1 -0
- package/dist/services/bridge-rotation-policy.d.ts +139 -0
- package/dist/services/bridge-rotation-policy.d.ts.map +1 -0
- package/dist/services/bridge-rotation-policy.js +125 -0
- package/dist/services/bridge-rotation-policy.js.map +1 -0
- package/dist/services/bridge-turn-queue.d.ts +154 -0
- package/dist/services/bridge-turn-queue.d.ts.map +1 -0
- package/dist/services/bridge-turn-queue.js +316 -0
- package/dist/services/bridge-turn-queue.js.map +1 -0
- package/dist/services/chat-first-seen-store.d.ts +27 -0
- package/dist/services/chat-first-seen-store.d.ts.map +1 -0
- package/dist/services/chat-first-seen-store.js +114 -0
- package/dist/services/chat-first-seen-store.js.map +1 -0
- package/dist/services/claude-transcript.d.ts +268 -0
- package/dist/services/claude-transcript.d.ts.map +1 -0
- package/dist/services/claude-transcript.js +798 -0
- package/dist/services/claude-transcript.js.map +1 -0
- package/dist/services/coco-transcript.d.ts +35 -0
- package/dist/services/coco-transcript.d.ts.map +1 -0
- package/dist/services/coco-transcript.js +192 -0
- package/dist/services/coco-transcript.js.map +1 -0
- package/dist/services/codex-bridge-queue.d.ts +56 -0
- package/dist/services/codex-bridge-queue.d.ts.map +1 -0
- package/dist/services/codex-bridge-queue.js +150 -0
- package/dist/services/codex-bridge-queue.js.map +1 -0
- package/dist/services/codex-transcript.d.ts +84 -0
- package/dist/services/codex-transcript.d.ts.map +1 -0
- package/dist/services/codex-transcript.js +298 -0
- package/dist/services/codex-transcript.js.map +1 -0
- package/dist/services/group-creator.d.ts +23 -0
- package/dist/services/group-creator.d.ts.map +1 -0
- package/dist/services/group-creator.js +75 -0
- package/dist/services/group-creator.js.map +1 -0
- package/dist/services/groups-store.d.ts +98 -0
- package/dist/services/groups-store.d.ts.map +1 -0
- package/dist/services/groups-store.js +213 -0
- package/dist/services/groups-store.js.map +1 -0
- package/dist/services/oncall-store.d.ts +80 -8
- package/dist/services/oncall-store.d.ts.map +1 -1
- package/dist/services/oncall-store.js +265 -55
- package/dist/services/oncall-store.js.map +1 -1
- package/dist/services/project-scanner.d.ts +1 -2
- package/dist/services/project-scanner.d.ts.map +1 -1
- package/dist/services/project-scanner.js +118 -68
- package/dist/services/project-scanner.js.map +1 -1
- package/dist/services/schedule-store.d.ts +5 -0
- package/dist/services/schedule-store.d.ts.map +1 -1
- package/dist/services/schedule-store.js +77 -1
- package/dist/services/schedule-store.js.map +1 -1
- package/dist/services/session-store.d.ts +22 -0
- package/dist/services/session-store.d.ts.map +1 -1
- package/dist/services/session-store.js +62 -4
- package/dist/services/session-store.js.map +1 -1
- package/dist/setup/bots-store.d.ts +3 -0
- package/dist/setup/bots-store.d.ts.map +1 -0
- package/dist/setup/bots-store.js +24 -0
- package/dist/setup/bots-store.js.map +1 -0
- package/dist/setup/detect-platform.d.ts +14 -0
- package/dist/setup/detect-platform.d.ts.map +1 -0
- package/dist/setup/detect-platform.js +139 -0
- package/dist/setup/detect-platform.js.map +1 -0
- package/dist/setup/ensure-fonts.d.ts +13 -0
- package/dist/setup/ensure-fonts.d.ts.map +1 -0
- package/dist/setup/ensure-fonts.js +225 -0
- package/dist/setup/ensure-fonts.js.map +1 -0
- package/dist/setup/ensure-tmux.d.ts +60 -0
- package/dist/setup/ensure-tmux.d.ts.map +1 -0
- package/dist/setup/ensure-tmux.js +236 -0
- package/dist/setup/ensure-tmux.js.map +1 -0
- package/dist/setup/index.d.ts +9 -0
- package/dist/setup/index.d.ts.map +1 -0
- package/dist/setup/index.js +46 -0
- package/dist/setup/index.js.map +1 -0
- package/dist/setup/lark-scopes.json +301 -0
- package/dist/setup/register-app.d.ts +52 -0
- package/dist/setup/register-app.d.ts.map +1 -0
- package/dist/setup/register-app.js +91 -0
- package/dist/setup/register-app.js.map +1 -0
- package/dist/setup/verify-permissions.d.ts +115 -0
- package/dist/setup/verify-permissions.d.ts.map +1 -0
- package/dist/setup/verify-permissions.js +207 -0
- package/dist/setup/verify-permissions.js.map +1 -0
- package/dist/skills/definitions.d.ts +4 -0
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +133 -19
- package/dist/skills/definitions.js.map +1 -1
- package/dist/skills/installer.d.ts +3 -1
- package/dist/skills/installer.d.ts.map +1 -1
- package/dist/skills/installer.js +18 -3
- package/dist/skills/installer.js.map +1 -1
- package/dist/types.d.ts +44 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/bot-routing.d.ts +6 -0
- package/dist/utils/bot-routing.d.ts.map +1 -0
- package/dist/utils/bot-routing.js +50 -0
- package/dist/utils/bot-routing.js.map +1 -0
- package/dist/utils/file-lock.d.ts +2 -0
- package/dist/utils/file-lock.d.ts.map +1 -0
- package/dist/utils/file-lock.js +114 -0
- package/dist/utils/file-lock.js.map +1 -0
- package/dist/utils/font-installer.js +1 -1
- package/dist/utils/font-installer.js.map +1 -1
- package/dist/utils/idle-detector.d.ts +6 -0
- package/dist/utils/idle-detector.d.ts.map +1 -1
- package/dist/utils/idle-detector.js +25 -4
- package/dist/utils/idle-detector.js.map +1 -1
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +60 -8
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/render-dimensions.d.ts +48 -0
- package/dist/utils/render-dimensions.d.ts.map +1 -0
- package/dist/utils/render-dimensions.js +55 -0
- package/dist/utils/render-dimensions.js.map +1 -0
- package/dist/utils/screen-analyzer.d.ts.map +1 -1
- package/dist/utils/screen-analyzer.js +24 -0
- package/dist/utils/screen-analyzer.js.map +1 -1
- package/dist/utils/screenshot-renderer.d.ts.map +1 -1
- package/dist/utils/screenshot-renderer.js +67 -23
- package/dist/utils/screenshot-renderer.js.map +1 -1
- package/dist/utils/terminal-renderer.d.ts +16 -0
- package/dist/utils/terminal-renderer.d.ts.map +1 -1
- package/dist/utils/terminal-renderer.js +40 -23
- package/dist/utils/terminal-renderer.js.map +1 -1
- package/dist/utils/transient-snapshot.d.ts +28 -0
- package/dist/utils/transient-snapshot.d.ts.map +1 -0
- package/dist/utils/transient-snapshot.js +96 -0
- package/dist/utils/transient-snapshot.js.map +1 -0
- package/dist/worker.js +2248 -83
- package/dist/worker.js.map +1 -1
- package/package.json +12 -5
|
@@ -1,16 +1,45 @@
|
|
|
1
|
-
import { existsSync, statSync, openSync, readSync, closeSync } from 'node:fs';
|
|
1
|
+
import { existsSync, statSync, openSync, readSync, closeSync, readFileSync, readdirSync, readlinkSync, realpathSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
4
|
import { resolveCommand } from './registry.js';
|
|
5
|
+
import { findJsonlContainingFingerprint, jsonlContainsFingerprint, normaliseForFingerprint } from '../../services/claude-transcript.js';
|
|
6
|
+
/** Resolve cwd to its canonical (symlink-free) absolute path for project-hash
|
|
7
|
+
* computation. Claude Code itself runs `process.cwd()` which the kernel returns
|
|
8
|
+
* already realpath'd via getcwd(3) — so its on-disk project hash always reflects
|
|
9
|
+
* the realpath, not the symlink we may have spawned it under. We must mirror
|
|
10
|
+
* that here, otherwise a deployment whose `workingDir` is a symlink (e.g.
|
|
11
|
+
* `/home/user` → `/data00/home/user`) computes the wrong project dir, the
|
|
12
|
+
* bridge watcher tails a non-existent file, submit-confirm never sees the
|
|
13
|
+
* user line, and the no-`botmux send` fallback never emits. realpathSync
|
|
14
|
+
* throws on non-existent paths — fall back to the raw cwd in that case so a
|
|
15
|
+
* pre-existence check upstream can still report a useful error. */
|
|
16
|
+
function realpathCwd(cwd) {
|
|
17
|
+
try {
|
|
18
|
+
return realpathSync(cwd);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return cwd;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
5
24
|
/** Resolve the JSONL transcript path Claude Code writes user/assistant turns to.
|
|
6
|
-
* Claude Code's project-hash scheme replaces
|
|
25
|
+
* Claude Code's project-hash scheme replaces every non-[A-Za-z0-9-] char with `-`
|
|
26
|
+
* (observed: `/foo/life_workspace` → `-foo-life-workspace`; `/`, `.`, `_` all become `-`).
|
|
27
|
+
* Always operates on realpath(cwd) — see realpathCwd above. */
|
|
7
28
|
export function claudeJsonlPathForSession(sessionId, cwd) {
|
|
8
|
-
const projectHash = cwd.replace(/[
|
|
29
|
+
const projectHash = realpathCwd(cwd).replace(/[^A-Za-z0-9-]/g, '-');
|
|
9
30
|
return join(homedir(), '.claude', 'projects', projectHash, `${sessionId}.jsonl`);
|
|
10
31
|
}
|
|
11
|
-
/**
|
|
12
|
-
*
|
|
13
|
-
|
|
32
|
+
/** Substrings that indicate Claude Code received our submit. We accept either:
|
|
33
|
+
* - `"role":"user","content":"` — direct submission while idle (the canonical
|
|
34
|
+
* user-message line; tool-result lines have array content `"content":[{...`
|
|
35
|
+
* so they never match).
|
|
36
|
+
* - `"operation":"enqueue"` — type-ahead submission while Claude is busy.
|
|
37
|
+
* Claude Code logs a `{"type":"queue-operation","operation":"enqueue",...}`
|
|
38
|
+
* line at the moment of submit and only later (after the current turn ends)
|
|
39
|
+
* promotes it to a `queued_command` attachment — never to a `role:user`
|
|
40
|
+
* string-content line. Without this marker, every type-ahead submit would
|
|
41
|
+
* falsely report failure. */
|
|
42
|
+
const SUBMIT_MARKERS = ['"role":"user","content":"', '"operation":"enqueue"'];
|
|
14
43
|
function currentFileSize(path) {
|
|
15
44
|
if (!existsSync(path))
|
|
16
45
|
return 0;
|
|
@@ -21,7 +50,7 @@ function currentFileSize(path) {
|
|
|
21
50
|
return 0;
|
|
22
51
|
}
|
|
23
52
|
}
|
|
24
|
-
function
|
|
53
|
+
function deltaHasSubmit(path, fromByte) {
|
|
25
54
|
if (!existsSync(path))
|
|
26
55
|
return false;
|
|
27
56
|
let size;
|
|
@@ -42,108 +71,500 @@ function deltaHasUserSubmit(path, fromByte) {
|
|
|
42
71
|
finally {
|
|
43
72
|
closeSync(fd);
|
|
44
73
|
}
|
|
45
|
-
|
|
74
|
+
const text = buf.toString('utf8');
|
|
75
|
+
return SUBMIT_MARKERS.some(m => text.includes(m));
|
|
46
76
|
}
|
|
47
|
-
async function
|
|
77
|
+
async function waitForSubmit(path, baseByte, timeoutMs) {
|
|
48
78
|
const deadline = Date.now() + timeoutMs;
|
|
49
79
|
while (Date.now() < deadline) {
|
|
50
|
-
if (
|
|
80
|
+
if (deltaHasSubmit(path, baseByte))
|
|
51
81
|
return true;
|
|
52
82
|
await new Promise(r => setTimeout(r, 100));
|
|
53
83
|
}
|
|
54
84
|
return false;
|
|
55
85
|
}
|
|
56
|
-
|
|
86
|
+
function makeSubmitFingerprint(content, len = 30) {
|
|
87
|
+
const collapsed = normaliseForFingerprint(content);
|
|
88
|
+
return collapsed.length > 0 ? collapsed.substring(0, len) : undefined;
|
|
89
|
+
}
|
|
90
|
+
const SESSION_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
91
|
+
/** Returns the absolute path to Claude Code's per-process session state file.
|
|
92
|
+
* Claude writes `{pid, sessionId, cwd, procStart, status, updatedAt, ...}`
|
|
93
|
+
* here. Empirical scope (Claude Code 2.1.123): `status` and `updatedAt`
|
|
94
|
+
* refresh on every state change, but `sessionId` is written ONCE at
|
|
95
|
+
* process start. `--resume` is a fresh spawn → fresh pid file with the
|
|
96
|
+
* resumed id; in-pane `/clear` does NOT rewrite the pid file's
|
|
97
|
+
* `sessionId` even though it rotates the on-disk jsonl. Callers that
|
|
98
|
+
* rely on this for rotation tracking must therefore treat a "matching
|
|
99
|
+
* sessionId" answer as "no spawn-time rotation observed", not "no
|
|
100
|
+
* rotation at all" — the latter requires fingerprint corroboration. */
|
|
101
|
+
export function claudePidStatePath(pid) {
|
|
102
|
+
return join(homedir(), '.claude', 'sessions', `${pid}.json`);
|
|
103
|
+
}
|
|
104
|
+
/** Linux-only: read /proc/<pid>/stat field 22 (starttime). Returns null when
|
|
105
|
+
* /proc isn't available or the stat line is unreadable/malformed; callers
|
|
106
|
+
* decide whether to fail closed or skip validation for their platform. */
|
|
107
|
+
function readProcStarttime(pid) {
|
|
108
|
+
try {
|
|
109
|
+
const raw = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
110
|
+
// pid (comm) state ppid pgrp ... — comm may contain spaces/parens, so
|
|
111
|
+
// anchor on the LAST ')' before splitting the remaining fields.
|
|
112
|
+
const closeParen = raw.lastIndexOf(')');
|
|
113
|
+
if (closeParen < 0)
|
|
114
|
+
return null;
|
|
115
|
+
const fields = raw.slice(closeParen + 2).trim().split(/\s+/);
|
|
116
|
+
// Post-')' field 1 is state; starttime is field 22 → index 19 here.
|
|
117
|
+
return fields[19] ?? null;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Resolve Claude Code's authoritative current session id via
|
|
124
|
+
* ~/.claude/sessions/<pid>.json. Validates pid + sessionId UUID + cwd so a
|
|
125
|
+
* stale or unrelated pid file can't redirect us to the wrong jsonl. On Linux
|
|
126
|
+
* also matches procStart against /proc/<pid>/stat to reject PID reuse. If
|
|
127
|
+
* procStart is present but cannot be verified on Linux, fail closed; callers
|
|
128
|
+
* fall back to fingerprint detection. */
|
|
129
|
+
export function resolveJsonlFromPid(pid, expectedCwd) {
|
|
130
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
131
|
+
return null;
|
|
132
|
+
let parsed;
|
|
133
|
+
try {
|
|
134
|
+
parsed = JSON.parse(readFileSync(claudePidStatePath(pid), 'utf8'));
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
if (!parsed || typeof parsed !== 'object')
|
|
140
|
+
return null;
|
|
141
|
+
if (parsed.pid !== pid)
|
|
142
|
+
return null;
|
|
143
|
+
if (typeof parsed.sessionId !== 'string' || !SESSION_UUID_RE.test(parsed.sessionId))
|
|
144
|
+
return null;
|
|
145
|
+
if (typeof parsed.cwd !== 'string')
|
|
146
|
+
return null;
|
|
147
|
+
// Identity check: procStart matching against /proc/<pid>/stat field 22 is
|
|
148
|
+
// the strong signal that this pid file belongs to the live process (rules
|
|
149
|
+
// out pid reuse). When that holds, Claude's recorded cwd is authoritative
|
|
150
|
+
// even if it disagrees with `expectedCwd` — the worker's cliCwd can drift
|
|
151
|
+
// (e.g. a schedule resumes a session with a different workingDir than the
|
|
152
|
+
// original spawn, but Claude itself loads the session with its own cwd).
|
|
153
|
+
// When procStart is unavailable/unverifiable, fall back to cwd equality as
|
|
154
|
+
// the only remaining sanity check. Realpath both sides so a symlinked
|
|
155
|
+
// workingDir (/home/x → /data00/home/x) still matches Claude's canonical
|
|
156
|
+
// cwd from getcwd(3).
|
|
157
|
+
let procStartVerified = false;
|
|
158
|
+
if (typeof parsed.procStart === 'string') {
|
|
159
|
+
const live = readProcStarttime(pid);
|
|
160
|
+
if (live === null && process.platform === 'linux')
|
|
161
|
+
return null;
|
|
162
|
+
if (live !== null) {
|
|
163
|
+
if (live !== parsed.procStart)
|
|
164
|
+
return null;
|
|
165
|
+
procStartVerified = true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!procStartVerified && realpathCwd(parsed.cwd) !== realpathCwd(expectedCwd))
|
|
169
|
+
return null;
|
|
170
|
+
return {
|
|
171
|
+
path: claudeJsonlPathForSession(parsed.sessionId, parsed.cwd),
|
|
172
|
+
cliSessionId: parsed.sessionId,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/** Linux-only: probe `/proc/<pid>/fd` for any signal that reveals Claude's
|
|
176
|
+
* CURRENT sessionId — not the spawn-time one the pid file records. Two
|
|
177
|
+
* signals are checked:
|
|
178
|
+
* 1. Direct `.jsonl` symlinks under `~/.claude/projects/...` — Claude
|
|
179
|
+
* opens-writes-closes per event, so this only hits if the probe
|
|
180
|
+
* lands during a write window.
|
|
181
|
+
* 2. `~/.claude/tasks/<sessionId>(/...)` symlinks — Claude holds the
|
|
182
|
+
* tasks directory and its `.lock` file open continuously for the
|
|
183
|
+
* duration of the active session, so this signal is reliable even
|
|
184
|
+
* between writes. This is the path that catches in-pane `/clear`
|
|
185
|
+
* rotations the pid file can't see (pid file's `sessionId` is set
|
|
186
|
+
* once at process start; tasks dir tracks every rotation).
|
|
187
|
+
* Returns deduplicated sessionIds in arbitrary order; caller picks one
|
|
188
|
+
* (typically by mtime of the corresponding jsonl). Returns [] on
|
|
189
|
+
* non-Linux platforms or if /proc lookup fails. */
|
|
190
|
+
export function findOpenClaudeSessionIds(pid) {
|
|
191
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
192
|
+
return [];
|
|
193
|
+
if (process.platform !== 'linux')
|
|
194
|
+
return [];
|
|
195
|
+
let entries;
|
|
196
|
+
try {
|
|
197
|
+
entries = readdirSync(`/proc/${pid}/fd`);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
const tasksPrefix = join(homedir(), '.claude', 'tasks') + '/';
|
|
203
|
+
const projectsInfix = '/.claude/projects/';
|
|
204
|
+
const out = new Set();
|
|
205
|
+
for (const name of entries) {
|
|
206
|
+
let target;
|
|
207
|
+
try {
|
|
208
|
+
target = readlinkSync(`/proc/${pid}/fd/${name}`);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (target.startsWith(tasksPrefix)) {
|
|
214
|
+
const sid = target.slice(tasksPrefix.length).split('/')[0];
|
|
215
|
+
if (sid && SESSION_UUID_RE.test(sid))
|
|
216
|
+
out.add(sid);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (target.endsWith('.jsonl') && target.includes(projectsInfix)) {
|
|
220
|
+
const base = target.split('/').pop() ?? '';
|
|
221
|
+
const sid = base.endsWith('.jsonl') ? base.slice(0, -'.jsonl'.length) : '';
|
|
222
|
+
if (sid && SESSION_UUID_RE.test(sid))
|
|
223
|
+
out.add(sid);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return [...out];
|
|
227
|
+
}
|
|
228
|
+
/** Fingerprint search that fans out from the pinned project dir to every
|
|
229
|
+
* sibling under `~/.claude/projects/`. Used as the writeInput fallback
|
|
230
|
+
* when the pinned `claudeJsonlPath` doesn't contain the submit marker —
|
|
231
|
+
* Claude may have written to a different project hash than the worker
|
|
232
|
+
* expected (e.g. a schedule resumed the session with a workingDir that
|
|
233
|
+
* differs from Claude's internal cwd, so the worker computes the wrong
|
|
234
|
+
* -project-hash- but Claude appends to the original session's hash dir).
|
|
235
|
+
* Tries the primary dir first (fast path, unchanged behavior); only fans
|
|
236
|
+
* out when no match is found there. Per-dir, `findJsonlContainingFingerprint`
|
|
237
|
+
* still applies its newest-first ordering and the minMtimeMs guard, so a
|
|
238
|
+
* stale historical match in some unrelated project can't false-positive. */
|
|
239
|
+
function findJsonlAcrossProjectsRoot(searchPath, fingerprint, options) {
|
|
240
|
+
const primaryDir = dirname(searchPath);
|
|
241
|
+
const primary = findJsonlContainingFingerprint(primaryDir, fingerprint, {
|
|
242
|
+
excludePath: searchPath,
|
|
243
|
+
...options,
|
|
244
|
+
});
|
|
245
|
+
if (primary)
|
|
246
|
+
return primary;
|
|
247
|
+
const projectsRoot = dirname(primaryDir);
|
|
248
|
+
if (!existsSync(projectsRoot))
|
|
249
|
+
return null;
|
|
250
|
+
let siblings;
|
|
251
|
+
try {
|
|
252
|
+
siblings = readdirSync(projectsRoot);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
for (const name of siblings) {
|
|
258
|
+
const sib = join(projectsRoot, name);
|
|
259
|
+
if (sib === primaryDir)
|
|
260
|
+
continue;
|
|
261
|
+
const matched = findJsonlContainingFingerprint(sib, fingerprint, {
|
|
262
|
+
excludePath: searchPath,
|
|
263
|
+
...options,
|
|
264
|
+
});
|
|
265
|
+
if (matched)
|
|
266
|
+
return matched;
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
const COMPLETION_RE = /\u2733\s*(?:Worked|Crunched|Cogitated|Cooked|Churned|Saut[eé]ed|Baked|Brewed) for \d+[smh]/;
|
|
57
271
|
export function createClaudeCodeAdapter(pathOverride) {
|
|
58
272
|
const bin = resolveCommand(pathOverride ?? 'claude');
|
|
59
273
|
return {
|
|
60
274
|
id: 'claude-code',
|
|
61
275
|
resolvedBin: bin,
|
|
62
276
|
supportsTypeAhead: true,
|
|
63
|
-
|
|
277
|
+
buildResumeCommand({ sessionId, cliSessionId }) {
|
|
278
|
+
// Claude resumes by reading <id>.jsonl, so we need the most recently
|
|
279
|
+
// observed CLI-native id (rotation can happen mid-run); fall back to the
|
|
280
|
+
// botmux sessionId for the first-turn case where they coincide.
|
|
281
|
+
return `claude --resume ${cliSessionId ?? sessionId}`;
|
|
282
|
+
},
|
|
283
|
+
buildArgs({ sessionId, resume, resumeSessionId, botName, botOpenId }) {
|
|
64
284
|
const args = [];
|
|
65
285
|
if (resume) {
|
|
66
|
-
|
|
286
|
+
// Prefer Claude's most recently observed internal session id when we
|
|
287
|
+
// have one — `--resume` reads `<id>.jsonl`, and after a previous run
|
|
288
|
+
// rotated the id (which we now persist via the pid-file resolver) the
|
|
289
|
+
// botmux sessionId no longer matches Claude's actual transcript file.
|
|
290
|
+
args.push('--resume', resumeSessionId ?? sessionId);
|
|
67
291
|
}
|
|
68
292
|
else {
|
|
69
293
|
args.push('--session-id', sessionId);
|
|
70
294
|
}
|
|
71
295
|
args.push('--dangerously-skip-permissions');
|
|
72
296
|
args.push('--disallowed-tools', 'EnterPlanMode,ExitPlanMode');
|
|
73
|
-
const
|
|
297
|
+
const identityBlock = botName || botOpenId
|
|
74
298
|
? [
|
|
75
299
|
'',
|
|
76
|
-
'
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
'
|
|
80
|
-
'
|
|
81
|
-
'- 只执行明确分给自己的那部分,别抢别的机器人的活',
|
|
82
|
-
'- 整条消息都指派给别的机器人时,保持沉默不要回复',
|
|
83
|
-
'-
|
|
300
|
+
'<identity>',
|
|
301
|
+
` <name>${botName ?? '(未知)'}</name>`,
|
|
302
|
+
` <open_id>${botOpenId ?? '(未知)'}</open_id>`,
|
|
303
|
+
' <routing_rules>',
|
|
304
|
+
' 群里可能有多个机器人,消息里用 `@名字` 和 `open_id` 区分接收方。对照上面的 name/open_id 判断本条消息归属:',
|
|
305
|
+
' - 只执行明确分给自己的那部分,别抢别的机器人的活',
|
|
306
|
+
' - 整条消息都指派给别的机器人时,保持沉默不要回复',
|
|
307
|
+
' - **默认不主动拉别的 bot 进来**。除非用户明确要求、或某段任务只能由对方做,否则一个人做完自己的部分就行。',
|
|
308
|
+
'',
|
|
309
|
+
' **和别的机器人协作的硬性物理事实**:飞书话题群里其他 bot **默认收不到** 你 `botmux send` 出去的消息——',
|
|
310
|
+
' 要让某个 bot 接力干活,**必须** 显式 `--mention <对方 bot 的 open_id>`,不 --mention 对方 bot 完全不会被触发。',
|
|
311
|
+
' - 协作伙伴的 open_id 会列在每条用户消息附带的 `<available_bots>` 块里,也可以 `botmux bots list` 查',
|
|
312
|
+
' - 用法:`botmux send --mention ou_xxx "消息内容"`(多个 bot 重复 `--mention`);正文里写 `@对方名字` 时 botmux 也会自动补上 --mention,但显式传更稳',
|
|
313
|
+
' - 该 --mention 的场景:用户明确要求让对方接力、把任务的某段交给对方、需要对方给最终结论或做独立操作',
|
|
314
|
+
' - 不必 --mention 的场景:纯状态更新/确认/感谢——尽量合并到下一次有内容的消息里再带上,避免互相 ping 触发空转',
|
|
315
|
+
' </routing_rules>',
|
|
316
|
+
'</identity>',
|
|
84
317
|
]
|
|
85
318
|
: [];
|
|
86
319
|
args.push('--append-system-prompt', [
|
|
320
|
+
'<botmux_routing>',
|
|
87
321
|
'你连接到了飞书(Lark)话题群。用户在飞书上阅读,看不到你的终端输出。',
|
|
88
322
|
'想让用户看到的内容必须通过 `botmux send` 命令发送,终端输出不会到达聊天。',
|
|
89
323
|
'',
|
|
90
324
|
'使用指南:',
|
|
91
325
|
'- 用 `botmux send` 发送:关键结论、方案(等用户确认再执行)、最终结果、进度更新。',
|
|
92
|
-
'- 发送纯文本即可:`botmux send "消息"
|
|
326
|
+
'- 发送纯文本即可:`botmux send "消息"`。格式自动处理。',
|
|
327
|
+
'- 多行消息必须用 heredoc,禁止写成 `botmux send "第一行\\n第二行"`;否则 `\\n` 可能按字面量显示在飞书里。',
|
|
328
|
+
" 正确多行示例:\n```bash\nbotmux send <<'EOF'\n第一行\n第二行\nEOF\n```",
|
|
93
329
|
'- 附带图片:`botmux send --images /path/to/img.png "说明文字"`',
|
|
94
330
|
'- 附带文件:`botmux send --files /path/to/file.pdf "请查收"`',
|
|
95
|
-
'- 需要上下文时用 `botmux
|
|
331
|
+
'- 需要上下文时用 `botmux history` 读取之前的对话。',
|
|
96
332
|
'- 查看可协作的机器人:`botmux bots list`',
|
|
97
|
-
|
|
333
|
+
'</botmux_routing>',
|
|
334
|
+
...identityBlock,
|
|
98
335
|
].join('\n'));
|
|
99
336
|
return args;
|
|
100
337
|
},
|
|
101
338
|
injectsSessionContext: true,
|
|
102
339
|
async writeInput(pty, content) {
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
// paste
|
|
109
|
-
//
|
|
110
|
-
//
|
|
340
|
+
// Type content like a human: literal text via send-keys -l, and each
|
|
341
|
+
// newline replaced by `\` + Enter (Claude Code's documented soft-newline
|
|
342
|
+
// idiom — keeps content in the input box without submitting). The final
|
|
343
|
+
// Enter at the bottom is the unambiguous submit. This sidesteps tmux
|
|
344
|
+
// bracketed-paste mode entirely, which was unreliable: Claude Code can
|
|
345
|
+
// toggle bracketed-paste off mid-session (after slash commands etc.),
|
|
346
|
+
// making tmux's paste-buffer drop the markers and turning embedded \r
|
|
347
|
+
// into Enters that fragment the message into multiple submits.
|
|
348
|
+
//
|
|
349
|
+
// Each tmux send-keys is throttled so the cumulative input rate stays
|
|
350
|
+
// below Claude Code's paste-burst threshold — otherwise on long messages
|
|
351
|
+
// (~1300+ chars / ~25+ lines) Ink flips into paste mode mid-stream and
|
|
352
|
+
// subsequent `\` + Enter pairs are kept as literal `\\\r` in the
|
|
353
|
+
// submitted content instead of being consumed as soft-newline markers.
|
|
354
|
+
//
|
|
355
|
+
// Trailing Enter is still subject to Claude Code's paste-burst heuristic
|
|
356
|
+
// (rapid input followed by Enter can be coalesced as paste), so we keep
|
|
357
|
+
// the JSONL retry loop below as the source of truth for "did it submit".
|
|
111
358
|
const hasImagePath = /\.(jpe?g|png|gif|webp|svg|bmp)\b/i.test(content);
|
|
112
359
|
const submitDelay = hasImagePath ? 800 : 500;
|
|
360
|
+
const TYPING_THROTTLE_MS = 30;
|
|
361
|
+
const tick = () => new Promise(r => setTimeout(r, TYPING_THROTTLE_MS));
|
|
113
362
|
const sendEnter = () => {
|
|
114
363
|
if (pty.sendSpecialKeys)
|
|
115
364
|
pty.sendSpecialKeys('Enter');
|
|
116
365
|
else
|
|
117
366
|
pty.write('\r');
|
|
118
367
|
};
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
368
|
+
// Pid-state path resolver: ~/.claude/sessions/<pid>.json carries
|
|
369
|
+
// the spawn-time sessionId (written once at process start; see
|
|
370
|
+
// claudePidStatePath). Read it first so byte accounting locks onto
|
|
371
|
+
// the resume target right away when Claude was started with
|
|
372
|
+
// `--resume`. In-pane `/clear` won't appear here — that's covered
|
|
373
|
+
// by the fingerprint-based mid-flight rotation check below.
|
|
374
|
+
let observedCliSessionId;
|
|
375
|
+
const applyResolved = (resolved) => {
|
|
376
|
+
if (resolved.cliSessionId !== observedCliSessionId)
|
|
377
|
+
observedCliSessionId = resolved.cliSessionId;
|
|
378
|
+
if (resolved.path !== pty.claudeJsonlPath) {
|
|
379
|
+
pty.claudeJsonlPath = resolved.path;
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
return false;
|
|
383
|
+
};
|
|
384
|
+
if (pty.cliPid && pty.cliCwd) {
|
|
385
|
+
const resolved = resolveJsonlFromPid(pty.cliPid, pty.cliCwd);
|
|
386
|
+
if (resolved)
|
|
387
|
+
applyResolved(resolved);
|
|
388
|
+
}
|
|
389
|
+
// baseByte is recomputed at this point (after any entry-time path swap)
|
|
390
|
+
// so future writes are measured against the right transcript. Inside
|
|
391
|
+
// confirmSubmit a mid-flight rotation does NOT advance baseByte — the
|
|
392
|
+
// submit may already be in the rotated jsonl from before our re-resolve.
|
|
393
|
+
let baseByte = pty.claudeJsonlPath ? currentFileSize(pty.claudeJsonlPath) : 0;
|
|
394
|
+
const submitFingerprint = makeSubmitFingerprint(content);
|
|
395
|
+
const submitSearchMinMtime = Date.now() - 60_000;
|
|
396
|
+
if (pty.sendText && pty.sendSpecialKeys) {
|
|
397
|
+
const lines = content.split('\n');
|
|
398
|
+
for (let i = 0; i < lines.length; i++) {
|
|
399
|
+
if (lines[i].length > 0) {
|
|
400
|
+
pty.sendText(lines[i]);
|
|
401
|
+
await tick();
|
|
402
|
+
}
|
|
403
|
+
if (i < lines.length - 1) {
|
|
404
|
+
// Soft-newline: backslash + Enter inserts a newline in Claude
|
|
405
|
+
// Code's input box without submitting.
|
|
406
|
+
pty.sendText('\\');
|
|
407
|
+
await tick();
|
|
408
|
+
pty.sendSpecialKeys('Enter');
|
|
409
|
+
await tick();
|
|
410
|
+
}
|
|
411
|
+
}
|
|
122
412
|
}
|
|
123
413
|
else {
|
|
414
|
+
// Non-tmux fallback (raw PTY): bracketed paste is reliable here since
|
|
415
|
+
// we control the markers directly.
|
|
124
416
|
pty.write('\x1b[200~' + content + '\x1b[201~');
|
|
125
417
|
}
|
|
126
418
|
await new Promise(r => setTimeout(r, submitDelay));
|
|
127
419
|
sendEnter();
|
|
128
420
|
// Without a JSONL path we can't verify — trust the fixed delay and return.
|
|
129
|
-
|
|
130
|
-
|
|
421
|
+
// Still surface any sessionId we observed via the pid resolver so the
|
|
422
|
+
// worker can persist it even on this unverified path.
|
|
423
|
+
if (!pty.claudeJsonlPath) {
|
|
424
|
+
return observedCliSessionId ? { submitted: true, cliSessionId: observedCliSessionId } : undefined;
|
|
425
|
+
}
|
|
426
|
+
const confirmSubmit = async (timeoutMs) => {
|
|
427
|
+
const startPath = pty.claudeJsonlPath;
|
|
428
|
+
if (!startPath)
|
|
429
|
+
return false;
|
|
430
|
+
// First check: did our submit land past baseByte on the currently
|
|
431
|
+
// pinned path? Fast path for the common case (no rotation).
|
|
432
|
+
if (await waitForSubmit(startPath, baseByte, timeoutMs))
|
|
433
|
+
return true;
|
|
434
|
+
// Second: did Claude rotate sessionId mid-flight? The pid file
|
|
435
|
+
// is rewritten by `--resume` (fresh spawn) but NOT by in-pane
|
|
436
|
+
// `/clear` — so this catches the resume case. We re-read and
|
|
437
|
+
// check both:
|
|
438
|
+
// a) the rotated jsonl already contains our submit (the rotation
|
|
439
|
+
// happened between our type+Enter and this resolve — the
|
|
440
|
+
// content lives in the new file from before we knew about it),
|
|
441
|
+
// b) the rotated jsonl is empty / pre-existing but a fresh
|
|
442
|
+
// append is on its way (briefly poll).
|
|
443
|
+
// We do NOT overwrite the original baseByte before the fingerprint
|
|
444
|
+
// check because (a) requires matching content that may already be in
|
|
445
|
+
// the rotated file. For (b), poll from the rotated file's own current
|
|
446
|
+
// size so an older, larger startPath cannot hide a delayed append.
|
|
447
|
+
if (pty.cliPid && pty.cliCwd) {
|
|
448
|
+
const resolved = resolveJsonlFromPid(pty.cliPid, pty.cliCwd);
|
|
449
|
+
if (resolved) {
|
|
450
|
+
const switched = applyResolved(resolved);
|
|
451
|
+
const newPath = pty.claudeJsonlPath;
|
|
452
|
+
const rotatedBaseByte = switched && newPath ? currentFileSize(newPath) : baseByte;
|
|
453
|
+
if (switched && newPath && submitFingerprint) {
|
|
454
|
+
if (jsonlContainsFingerprint(newPath, submitFingerprint, { includeQueueOperations: true })) {
|
|
455
|
+
// Sync baseByte to end-of-file so subsequent confirms in
|
|
456
|
+
// this writeInput pass don't re-trigger on the same line.
|
|
457
|
+
baseByte = currentFileSize(newPath);
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (newPath) {
|
|
462
|
+
if (await waitForSubmit(newPath, rotatedBaseByte, switched ? 200 : 0)) {
|
|
463
|
+
if (switched)
|
|
464
|
+
baseByte = currentFileSize(newPath);
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// Final fallback when the pid file is unavailable / fails validation:
|
|
471
|
+
// scan the pinned project dir for a recently-written jsonl whose
|
|
472
|
+
// tail contains our content fingerprint. Stricter than mtime-based
|
|
473
|
+
// detection so a sibling pane in the same dir can't hijack us.
|
|
474
|
+
// Per-attempt scope is intentionally narrow (dirname only) — the
|
|
475
|
+
// cross-project fan-out only runs once at end-of-writeInput and in
|
|
476
|
+
// the recheck closure, not per retry, to keep the worst case bounded.
|
|
477
|
+
if (submitFingerprint) {
|
|
478
|
+
const searchPath = pty.claudeJsonlPath ?? startPath;
|
|
479
|
+
const matched = findJsonlContainingFingerprint(dirname(searchPath), submitFingerprint, {
|
|
480
|
+
excludePath: searchPath,
|
|
481
|
+
minMtimeMs: submitSearchMinMtime,
|
|
482
|
+
includeQueueOperations: true,
|
|
483
|
+
});
|
|
484
|
+
if (matched) {
|
|
485
|
+
pty.claudeJsonlPath = matched;
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return false;
|
|
490
|
+
};
|
|
491
|
+
const buildResult = (submitted) => {
|
|
492
|
+
return observedCliSessionId
|
|
493
|
+
? { submitted, cliSessionId: observedCliSessionId }
|
|
494
|
+
: { submitted };
|
|
495
|
+
};
|
|
131
496
|
// Retry budget: up to 2 extra Enters (3 sends total), each followed by
|
|
132
|
-
// an 800ms wait for the JSONL
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
497
|
+
// an 800ms wait for the JSONL to record either a direct user-submit line
|
|
498
|
+
// or a type-ahead enqueue line. If the user is concurrently typing in the
|
|
499
|
+
// web terminal, a stray Enter may submit their half-typed text — but we
|
|
500
|
+
// only retry when the JSONL is provably unchanged, so the race window is
|
|
501
|
+
// bounded to cases where submit really did fail.
|
|
136
502
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
137
|
-
if (await
|
|
138
|
-
return;
|
|
503
|
+
if (await confirmSubmit(800)) {
|
|
504
|
+
return observedCliSessionId ? buildResult(true) : undefined;
|
|
505
|
+
}
|
|
139
506
|
sendEnter();
|
|
140
507
|
}
|
|
141
508
|
// Final grace check.
|
|
142
|
-
if (await
|
|
143
|
-
return;
|
|
144
|
-
|
|
509
|
+
if (await confirmSubmit(800)) {
|
|
510
|
+
return observedCliSessionId ? buildResult(true) : undefined;
|
|
511
|
+
}
|
|
512
|
+
// Last-resort cross-project fan-out, run ONCE before declaring failure:
|
|
513
|
+
// catches the case where workingDir/cwd drift made every per-attempt
|
|
514
|
+
// scan look in the wrong project dir AND the pid resolver also failed
|
|
515
|
+
// (e.g. pid file missing, /proc unavailable). minMtimeMs filtering and
|
|
516
|
+
// newest-first ordering keep the cost bounded — only jsonls touched in
|
|
517
|
+
// the last 60s are actually read, which is typically a handful even
|
|
518
|
+
// across all sibling project dirs. Per-attempt scans stay narrow
|
|
519
|
+
// (dirname only) so this work doesn't repeat 4×.
|
|
520
|
+
if (submitFingerprint && pty.claudeJsonlPath) {
|
|
521
|
+
const matched = findJsonlAcrossProjectsRoot(pty.claudeJsonlPath, submitFingerprint, {
|
|
522
|
+
minMtimeMs: submitSearchMinMtime,
|
|
523
|
+
includeQueueOperations: true,
|
|
524
|
+
});
|
|
525
|
+
if (matched) {
|
|
526
|
+
pty.claudeJsonlPath = matched;
|
|
527
|
+
return observedCliSessionId ? buildResult(true) : undefined;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// All retries exhausted and still no submit marker in JSONL. Signal failure
|
|
145
531
|
// so the worker can notify the user in Lark instead of silently dropping.
|
|
146
|
-
|
|
532
|
+
// We still surface observedCliSessionId so the worker can persist Claude's
|
|
533
|
+
// current id even when this particular submit didn't land.
|
|
534
|
+
//
|
|
535
|
+
// Attach a recheck closure: the in-band budget (4 × 800ms) is too short
|
|
536
|
+
// for cold-start sessions and for environments where a slow third-party
|
|
537
|
+
// UserPromptSubmit / SessionStart hook (e.g. superpowers) defers Claude's
|
|
538
|
+
// jsonl append by 5–15s. The worker calls recheck() after a delay, and
|
|
539
|
+
// suppresses the user-facing warning when the line shows up by then.
|
|
540
|
+
const recheck = () => {
|
|
541
|
+
if (!submitFingerprint)
|
|
542
|
+
return false;
|
|
543
|
+
// Latest pid → path; covers post-failure rotations (/clear, /resume).
|
|
544
|
+
if (pty.cliPid && pty.cliCwd) {
|
|
545
|
+
const resolved = resolveJsonlFromPid(pty.cliPid, pty.cliCwd);
|
|
546
|
+
if (resolved)
|
|
547
|
+
applyResolved(resolved);
|
|
548
|
+
}
|
|
549
|
+
const currentPath = pty.claudeJsonlPath;
|
|
550
|
+
if (currentPath && jsonlContainsFingerprint(currentPath, submitFingerprint, { includeQueueOperations: true })) {
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
// Fan out to sibling jsonls in the project dir, then across every
|
|
554
|
+
// sibling project dir under `~/.claude/projects/` (catches workingDir
|
|
555
|
+
// drift like worker thinking `-foo-bar/` while Claude actually appends
|
|
556
|
+
// to `-foo-bar-baz/`). Same minMtime guard as the in-band fingerprint
|
|
557
|
+
// fallback so a stale historical match can't suppress the warning.
|
|
558
|
+
const searchPath = currentPath ?? pty.claudeJsonlPath;
|
|
559
|
+
if (!searchPath)
|
|
560
|
+
return false;
|
|
561
|
+
const matched = findJsonlAcrossProjectsRoot(searchPath, submitFingerprint, {
|
|
562
|
+
minMtimeMs: submitSearchMinMtime,
|
|
563
|
+
includeQueueOperations: true,
|
|
564
|
+
});
|
|
565
|
+
return !!matched;
|
|
566
|
+
};
|
|
567
|
+
return { ...buildResult(false), recheck };
|
|
147
568
|
},
|
|
148
569
|
completionPattern: COMPLETION_RE,
|
|
149
570
|
readyPattern: /❯/,
|