botmux 2.9.0 → 2.9.2
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 +601 -309
- 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 +36 -0
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +119 -13
- package/dist/im/lark/client.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts +92 -8
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +410 -89
- 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 +2220 -83
- package/dist/worker.js.map +1 -1
- package/package.json +12 -5
|
@@ -0,0 +1,798 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental reader for Claude Code transcript JSONL files.
|
|
3
|
+
*
|
|
4
|
+
* Used by the adopt-bridge pipeline (worker.ts) to:
|
|
5
|
+
* 1. baseline the transcript at attach time so historical messages aren't
|
|
6
|
+
* replayed to Lark.
|
|
7
|
+
* 2. drain newly-appended assistant messages between user turns.
|
|
8
|
+
* 3. tolerate truncation, rotation, half-written JSON lines, and races with
|
|
9
|
+
* Claude Code's writer.
|
|
10
|
+
*
|
|
11
|
+
* The functions are pure (no fs.watch — that's the worker's wakeup concern)
|
|
12
|
+
* to keep them unit-testable.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, openSync, readSync, closeSync, statSync, readdirSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
/** Extract the user-typed prompt text for a "turn start" event — works for
|
|
17
|
+
* both legacy `role:user` events (text in `message.content`) and the
|
|
18
|
+
* type-ahead `attachment(queued_command)` form (text in `attachment.prompt`).
|
|
19
|
+
* Returns '' when neither shape carries usable content. Used at three
|
|
20
|
+
* layers: BridgeTurnQueue.ingest (fingerprint-match the right pending Lark
|
|
21
|
+
* turn), worker emit (local-turn user-text resolution), and tests. */
|
|
22
|
+
export function extractTurnStartText(ev) {
|
|
23
|
+
if (!ev || typeof ev !== 'object')
|
|
24
|
+
return '';
|
|
25
|
+
if (ev.type === 'attachment' && ev.attachment?.type === 'queued_command') {
|
|
26
|
+
const prompt = ev.attachment.prompt;
|
|
27
|
+
if (typeof prompt === 'string')
|
|
28
|
+
return prompt;
|
|
29
|
+
return stringifyUserContent(prompt);
|
|
30
|
+
}
|
|
31
|
+
return stringifyUserContent(ev.message?.content);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Read everything from `path` starting at `fromOffset` and return parsed
|
|
35
|
+
* JSONL events plus the new file offset.
|
|
36
|
+
*
|
|
37
|
+
* - Returns `{ events: [], newOffset: 0, pendingTail: '' }` if the file
|
|
38
|
+
* doesn't exist (caller treats this as "nothing yet").
|
|
39
|
+
* - Detects truncation (size < fromOffset): resets to 0 and re-drains so a
|
|
40
|
+
* rotated/cleared transcript doesn't silently swallow new lines.
|
|
41
|
+
* - Skips malformed JSON lines (logs nothing — robustness over noise).
|
|
42
|
+
* - The trailing partial line (no `\n` yet) is *not* parsed and *not*
|
|
43
|
+
* counted toward `newOffset`, so the next drain re-reads it.
|
|
44
|
+
*/
|
|
45
|
+
export function drainTranscript(path, fromOffset) {
|
|
46
|
+
if (!existsSync(path)) {
|
|
47
|
+
return { events: [], newOffset: 0, pendingTail: '' };
|
|
48
|
+
}
|
|
49
|
+
let size;
|
|
50
|
+
try {
|
|
51
|
+
size = statSync(path).size;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return { events: [], newOffset: fromOffset, pendingTail: '' };
|
|
55
|
+
}
|
|
56
|
+
let start = fromOffset;
|
|
57
|
+
if (size < start) {
|
|
58
|
+
// Truncated/rotated — re-read from the top.
|
|
59
|
+
start = 0;
|
|
60
|
+
}
|
|
61
|
+
if (size === start) {
|
|
62
|
+
return { events: [], newOffset: start, pendingTail: '' };
|
|
63
|
+
}
|
|
64
|
+
const len = size - start;
|
|
65
|
+
const buf = Buffer.alloc(len);
|
|
66
|
+
let read = 0;
|
|
67
|
+
const fd = openSync(path, 'r');
|
|
68
|
+
try {
|
|
69
|
+
read = readSync(fd, buf, 0, len, start);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
closeSync(fd);
|
|
73
|
+
}
|
|
74
|
+
const text = buf.subarray(0, read).toString('utf8');
|
|
75
|
+
// Find the last '\n' — anything after it is a partial line we shouldn't
|
|
76
|
+
// commit yet. Adjust newOffset to exclude the partial tail so the next
|
|
77
|
+
// drain re-reads it.
|
|
78
|
+
const lastNl = text.lastIndexOf('\n');
|
|
79
|
+
let toParse;
|
|
80
|
+
let pendingTail;
|
|
81
|
+
let newOffset;
|
|
82
|
+
if (lastNl < 0) {
|
|
83
|
+
// No complete line at all — treat the whole buffer as pending.
|
|
84
|
+
toParse = '';
|
|
85
|
+
pendingTail = text;
|
|
86
|
+
newOffset = start;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
toParse = text.substring(0, lastNl);
|
|
90
|
+
pendingTail = text.substring(lastNl + 1);
|
|
91
|
+
newOffset = start + Buffer.byteLength(text.substring(0, lastNl + 1), 'utf8');
|
|
92
|
+
}
|
|
93
|
+
const events = [];
|
|
94
|
+
if (toParse) {
|
|
95
|
+
for (const line of toParse.split('\n')) {
|
|
96
|
+
const trimmed = line.trim();
|
|
97
|
+
if (!trimmed)
|
|
98
|
+
continue;
|
|
99
|
+
try {
|
|
100
|
+
const obj = JSON.parse(trimmed);
|
|
101
|
+
if (obj && typeof obj === 'object')
|
|
102
|
+
events.push(obj);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Malformed line — skip silently. Claude Code's writer is atomic per
|
|
106
|
+
// line, so this means a debug/non-JSON line snuck in; not our concern.
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { events, newOffset, pendingTail };
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Filter to assistant text events. Returns only events where:
|
|
114
|
+
* - type === 'assistant' OR message.role === 'assistant'
|
|
115
|
+
* - content has at least one text block
|
|
116
|
+
* - uuid is present
|
|
117
|
+
*
|
|
118
|
+
* Sub-agent / sidechain events (isSidechain === true) are excluded so that
|
|
119
|
+
* spawn-internal Task agent chatter doesn't leak to Lark.
|
|
120
|
+
*/
|
|
121
|
+
export function pickAssistantTextEvents(events) {
|
|
122
|
+
return events.filter(e => {
|
|
123
|
+
if (!e || typeof e !== 'object')
|
|
124
|
+
return false;
|
|
125
|
+
if (e.isSidechain === true)
|
|
126
|
+
return false;
|
|
127
|
+
const role = e.message?.role ?? e.type;
|
|
128
|
+
if (role !== 'assistant')
|
|
129
|
+
return false;
|
|
130
|
+
if (!e.uuid)
|
|
131
|
+
return false;
|
|
132
|
+
const content = e.message?.content;
|
|
133
|
+
if (!content)
|
|
134
|
+
return false;
|
|
135
|
+
if (typeof content === 'string')
|
|
136
|
+
return content.length > 0;
|
|
137
|
+
if (Array.isArray(content))
|
|
138
|
+
return content.some(b => b && b.type === 'text' && typeof b.text === 'string' && b.text.length > 0);
|
|
139
|
+
return false;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Extract the visible text from one assistant event. Walks all `type:'text'`
|
|
144
|
+
* blocks in `message.content` (or the bare string) and joins them with
|
|
145
|
+
* blank lines. Returns '' if no text blocks.
|
|
146
|
+
*/
|
|
147
|
+
export function extractAssistantText(event) {
|
|
148
|
+
const content = event.message?.content;
|
|
149
|
+
if (!content)
|
|
150
|
+
return '';
|
|
151
|
+
if (typeof content === 'string')
|
|
152
|
+
return content;
|
|
153
|
+
if (!Array.isArray(content))
|
|
154
|
+
return '';
|
|
155
|
+
const parts = [];
|
|
156
|
+
for (const block of content) {
|
|
157
|
+
if (block && block.type === 'text' && typeof block.text === 'string' && block.text.length > 0) {
|
|
158
|
+
parts.push(block.text);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return parts.join('\n\n');
|
|
162
|
+
}
|
|
163
|
+
/** Convenience: filter+extract a list of events into a single concatenated string. */
|
|
164
|
+
export function joinAssistantText(events) {
|
|
165
|
+
return pickAssistantTextEvents(events)
|
|
166
|
+
.map(extractAssistantText)
|
|
167
|
+
.filter(s => s.length > 0)
|
|
168
|
+
.join('\n\n');
|
|
169
|
+
}
|
|
170
|
+
/** XML wrappers Claude Code uses for synthetic user events that aren't real
|
|
171
|
+
* prompts (slash command invocation, local-command output caveat, etc.).
|
|
172
|
+
* These should usually carry `isMeta:true` and we'd filter on that — this
|
|
173
|
+
* list is a defense-in-depth check for jsonls where the flag is absent. */
|
|
174
|
+
const SYNTHETIC_USER_PREFIXES = [
|
|
175
|
+
'<command-name>',
|
|
176
|
+
'<command-message>',
|
|
177
|
+
'<command-args>',
|
|
178
|
+
'<local-command-caveat>',
|
|
179
|
+
'<local-command-stdout>',
|
|
180
|
+
'<local-command-stderr>',
|
|
181
|
+
];
|
|
182
|
+
/** True when a `type:'user'` (or `message.role:'user'`) event represents a
|
|
183
|
+
* *real* prompt the human typed — not Claude Code's internal machinery
|
|
184
|
+
* (tool_result, slash-command wrappers, isMeta/isCompactSummary markers,
|
|
185
|
+
* sidechain spawn events). The bridge attribution queue and the adopt
|
|
186
|
+
* preamble extractor share this predicate to ensure they're seeing the
|
|
187
|
+
* same notion of "user input". */
|
|
188
|
+
export function isMeaningfulUserEvent(ev) {
|
|
189
|
+
if (!ev || typeof ev !== 'object')
|
|
190
|
+
return false;
|
|
191
|
+
const role = ev.message?.role ?? ev.type;
|
|
192
|
+
if (role !== 'user')
|
|
193
|
+
return false;
|
|
194
|
+
const flags = ev;
|
|
195
|
+
if (flags.isMeta === true)
|
|
196
|
+
return false;
|
|
197
|
+
if (flags.isCompactSummary === true)
|
|
198
|
+
return false;
|
|
199
|
+
if (flags.isSidechain === true)
|
|
200
|
+
return false;
|
|
201
|
+
const content = ev.message?.content;
|
|
202
|
+
if (isPureToolResultUserEvent(content))
|
|
203
|
+
return false;
|
|
204
|
+
const text = normaliseForFingerprint(stringifyUserContent(content));
|
|
205
|
+
if (text.length === 0)
|
|
206
|
+
return false;
|
|
207
|
+
if (SYNTHETIC_USER_PREFIXES.some(p => text.startsWith(p)))
|
|
208
|
+
return false;
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
/** True when a `type:'attachment'` line carries a queued-command payload
|
|
212
|
+
* representing a real submitted prompt. Claude writes one of these when it
|
|
213
|
+
* dequeues a type-ahead submission (right before the assistant's reply for
|
|
214
|
+
* that turn starts streaming) — the bridge attribution queue treats it
|
|
215
|
+
* exactly like a `role:user` event for turn-start purposes. Filters mirror
|
|
216
|
+
* isMeaningfulUserEvent's defenses (sidechain, empty / synthetic-prefix
|
|
217
|
+
* prompts) so a queued slash command can't false-start a Lark turn. */
|
|
218
|
+
export function isMeaningfulQueuedCommand(ev) {
|
|
219
|
+
if (!ev || typeof ev !== 'object')
|
|
220
|
+
return false;
|
|
221
|
+
if (ev.type !== 'attachment')
|
|
222
|
+
return false;
|
|
223
|
+
if (ev.attachment?.type !== 'queued_command')
|
|
224
|
+
return false;
|
|
225
|
+
if (ev.isSidechain === true)
|
|
226
|
+
return false;
|
|
227
|
+
const text = normaliseForFingerprint(extractTurnStartText(ev));
|
|
228
|
+
if (text.length === 0)
|
|
229
|
+
return false;
|
|
230
|
+
if (SYNTHETIC_USER_PREFIXES.some(p => text.startsWith(p)))
|
|
231
|
+
return false;
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
/** Walk the events forward and return the last *completed* user/assistant
|
|
235
|
+
* exchange. "Completed" here means: a meaningful user prompt followed by
|
|
236
|
+
* at least one assistant event with visible text. tool_use / tool_result
|
|
237
|
+
* events do NOT reset the turn — they're intra-turn machinery, so a
|
|
238
|
+
* prompt → tool_use → tool_result → assistant text sequence still counts
|
|
239
|
+
* as a single turn. Returns null when there's no meaningful user yet, or
|
|
240
|
+
* the last user wasn't followed by any visible assistant text (Claude is
|
|
241
|
+
* mid-tool-use when /adopt fired).
|
|
242
|
+
*
|
|
243
|
+
* Used by adopt-bridge to surface "the previous round" to the Lark thread
|
|
244
|
+
* so the user has context for continuing the conversation. */
|
|
245
|
+
export function extractLastAssistantTurn(events) {
|
|
246
|
+
let userText = null;
|
|
247
|
+
let assistantTexts = [];
|
|
248
|
+
for (const ev of events) {
|
|
249
|
+
if (!ev || typeof ev !== 'object')
|
|
250
|
+
continue;
|
|
251
|
+
if (isMeaningfulUserEvent(ev)) {
|
|
252
|
+
// New turn boundary — reset the assistant accumulator.
|
|
253
|
+
userText = stringifyUserContent(ev.message?.content);
|
|
254
|
+
assistantTexts = [];
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const role = ev.message?.role ?? ev.type;
|
|
258
|
+
if (role !== 'assistant')
|
|
259
|
+
continue;
|
|
260
|
+
if (ev.isSidechain === true)
|
|
261
|
+
continue;
|
|
262
|
+
const text = extractAssistantText(ev);
|
|
263
|
+
if (text.length === 0)
|
|
264
|
+
continue;
|
|
265
|
+
if (userText !== null)
|
|
266
|
+
assistantTexts.push(text);
|
|
267
|
+
}
|
|
268
|
+
if (userText === null || assistantTexts.length === 0)
|
|
269
|
+
return null;
|
|
270
|
+
return {
|
|
271
|
+
userText,
|
|
272
|
+
assistantText: assistantTexts.join('\n\n'),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* True when a user-role event carries ONLY tool_result blocks — Claude
|
|
277
|
+
* Code's representation of "tool returned this output" between an
|
|
278
|
+
* assistant tool_use and the assistant's continuation. Both the bridge
|
|
279
|
+
* attribution queue and the on-disk fingerprint search must skip these:
|
|
280
|
+
*
|
|
281
|
+
* - the queue would treat tool output as fresh local input and disable
|
|
282
|
+
* collection mid-turn,
|
|
283
|
+
* - the fingerprint search would false-positive on log content that
|
|
284
|
+
* happens to contain the Lark fingerprint substring (e.g. a short
|
|
285
|
+
* "hello" message hijacked by an unrelated jsonl whose tool_result
|
|
286
|
+
* dumped a log line containing "hello"). Re-exported by
|
|
287
|
+
* bridge-turn-queue.ts so both consumers share the same predicate
|
|
288
|
+
* and never drift apart.
|
|
289
|
+
*/
|
|
290
|
+
export function isPureToolResultUserEvent(content) {
|
|
291
|
+
if (!Array.isArray(content) || content.length === 0)
|
|
292
|
+
return false;
|
|
293
|
+
return content.every((block) => block?.type === 'tool_result');
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Stringify a transcript user event's content to a flat string. Handles
|
|
297
|
+
* both legacy bare-string content and the array-of-blocks form.
|
|
298
|
+
*
|
|
299
|
+
* Lives here (not in bridge-turn-queue.ts) so the in-process attribution
|
|
300
|
+
* state machine and the on-disk fingerprint search use *exactly* the
|
|
301
|
+
* same text — otherwise multi-line / array-content Lark messages stop
|
|
302
|
+
* matching one path or the other and bridges silently break.
|
|
303
|
+
*/
|
|
304
|
+
export function stringifyUserContent(content) {
|
|
305
|
+
if (typeof content === 'string')
|
|
306
|
+
return content;
|
|
307
|
+
if (!Array.isArray(content))
|
|
308
|
+
return '';
|
|
309
|
+
const parts = [];
|
|
310
|
+
for (const block of content) {
|
|
311
|
+
if (typeof block?.text === 'string')
|
|
312
|
+
parts.push(block.text);
|
|
313
|
+
else if (typeof block?.content === 'string')
|
|
314
|
+
parts.push(block.content);
|
|
315
|
+
}
|
|
316
|
+
return parts.join('\n');
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Collapse whitespace + trim. Same normalisation applied on both sides
|
|
320
|
+
* of the fingerprint compare (the Lark message that produces the
|
|
321
|
+
* fingerprint, and the transcript user content we search through),
|
|
322
|
+
* so newlines / tabs / double-spaces don't break the match.
|
|
323
|
+
*/
|
|
324
|
+
export function normaliseForFingerprint(s) {
|
|
325
|
+
return s.replace(/\s+/g, ' ').trim();
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Find the most recently-modified `.jsonl` file in a Claude Code project
|
|
329
|
+
* directory.
|
|
330
|
+
*
|
|
331
|
+
* `acceptCandidate` lets callers narrow the candidate set — the bridge's
|
|
332
|
+
* quiet-mtime fallback passes a trust-set predicate so a sibling Claude
|
|
333
|
+
* pane writing in the same project dir cannot hijack the watcher.
|
|
334
|
+
* Without it any actively-written sibling jsonl wins the mtime race and
|
|
335
|
+
* the bridge enters a flap loop with the pid resolver pulling it back.
|
|
336
|
+
*
|
|
337
|
+
* Returns null when the directory doesn't exist, has no jsonl files, or
|
|
338
|
+
* every candidate was rejected by `acceptCandidate`.
|
|
339
|
+
*/
|
|
340
|
+
export function findLatestJsonl(dir, opts) {
|
|
341
|
+
if (!existsSync(dir))
|
|
342
|
+
return null;
|
|
343
|
+
let entries;
|
|
344
|
+
try {
|
|
345
|
+
entries = readdirSync(dir);
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
const accept = opts?.acceptCandidate;
|
|
351
|
+
let latestPath = null;
|
|
352
|
+
let latestMtime = -Infinity;
|
|
353
|
+
for (const name of entries) {
|
|
354
|
+
if (!name.endsWith('.jsonl'))
|
|
355
|
+
continue;
|
|
356
|
+
const full = join(dir, name);
|
|
357
|
+
if (accept && !accept(full))
|
|
358
|
+
continue;
|
|
359
|
+
try {
|
|
360
|
+
const st = statSync(full);
|
|
361
|
+
if (!st.isFile())
|
|
362
|
+
continue;
|
|
363
|
+
if (st.mtimeMs > latestMtime) {
|
|
364
|
+
latestMtime = st.mtimeMs;
|
|
365
|
+
latestPath = full;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
// File disappeared between readdir and stat — ignore.
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return latestPath;
|
|
373
|
+
}
|
|
374
|
+
/** Scan a single jsonl file's tail for a Lark message fingerprint. Same
|
|
375
|
+
* parsing rules as `findJsonlContainingFingerprint` (decode role:user content,
|
|
376
|
+
* optionally also queue-operation/enqueue, normalise whitespace, then
|
|
377
|
+
* substring-match the fingerprint). Used by the claude-code adapter when
|
|
378
|
+
* the pid resolver has just switched to a rotated jsonl that may already
|
|
379
|
+
* contain the just-submitted user event. */
|
|
380
|
+
export function jsonlContainsFingerprint(path, fingerprint, opts) {
|
|
381
|
+
if (fingerprint.length === 0 || !existsSync(path))
|
|
382
|
+
return false;
|
|
383
|
+
let size;
|
|
384
|
+
try {
|
|
385
|
+
size = statSync(path).size;
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
if (size === 0)
|
|
391
|
+
return false;
|
|
392
|
+
const includeQueueOps = opts?.includeQueueOperations ?? false;
|
|
393
|
+
const minEventTimestampMs = opts?.minEventTimestampMs;
|
|
394
|
+
const len = Math.min(size, 1024 * 1024);
|
|
395
|
+
let buf;
|
|
396
|
+
try {
|
|
397
|
+
const fd = openSync(path, 'r');
|
|
398
|
+
try {
|
|
399
|
+
buf = Buffer.alloc(len);
|
|
400
|
+
readSync(fd, buf, 0, len, size - len);
|
|
401
|
+
}
|
|
402
|
+
finally {
|
|
403
|
+
closeSync(fd);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
const text = buf.toString('utf8');
|
|
410
|
+
const lines = text.split('\n');
|
|
411
|
+
// Skip the leading partial line when we read a strict tail (size > len).
|
|
412
|
+
const startIdx = size > len ? 1 : 0;
|
|
413
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
414
|
+
const line = lines[i].trim();
|
|
415
|
+
if (!line)
|
|
416
|
+
continue;
|
|
417
|
+
let ev;
|
|
418
|
+
try {
|
|
419
|
+
ev = JSON.parse(line);
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (!ev || typeof ev !== 'object')
|
|
425
|
+
continue;
|
|
426
|
+
// Per-event timestamp guard: short fingerprints would otherwise
|
|
427
|
+
// false-match old user events in unrelated sibling jsonls (file
|
|
428
|
+
// mtime can be recent if a sibling Claude pane is actively writing
|
|
429
|
+
// its own turns). We compare against `event.timestamp` rather than
|
|
430
|
+
// file mtime to be precise.
|
|
431
|
+
if (minEventTimestampMs !== undefined && typeof ev.timestamp === 'string') {
|
|
432
|
+
const evMs = Date.parse(ev.timestamp);
|
|
433
|
+
if (Number.isFinite(evMs) && evMs < minEventTimestampMs)
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
const role = ev.message?.role ?? ev.type;
|
|
437
|
+
let lineText = '';
|
|
438
|
+
if (role === 'user') {
|
|
439
|
+
// Skip pure tool_result events — Claude Code records them as
|
|
440
|
+
// role:user but they're internal turn machinery, not the user's
|
|
441
|
+
// actual prompt. A tool_result that dumps log output containing
|
|
442
|
+
// the fingerprint substring would otherwise hijack the search.
|
|
443
|
+
if (isPureToolResultUserEvent(ev.message?.content))
|
|
444
|
+
continue;
|
|
445
|
+
lineText = stringifyUserContent(ev.message?.content);
|
|
446
|
+
}
|
|
447
|
+
else if (includeQueueOps &&
|
|
448
|
+
ev.type === 'queue-operation' &&
|
|
449
|
+
ev.operation === 'enqueue') {
|
|
450
|
+
lineText = typeof ev.content === 'string' ? ev.content : stringifyUserContent(ev.content);
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
const normalisedText = normaliseForFingerprint(lineText);
|
|
456
|
+
if (normalisedText.length > 0 && normalisedText.includes(fingerprint))
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
export function findJsonlContainingFingerprint(dir, fingerprint, excludePathOrOptions) {
|
|
462
|
+
if (!existsSync(dir) || fingerprint.length === 0)
|
|
463
|
+
return null;
|
|
464
|
+
const opts = typeof excludePathOrOptions === 'string'
|
|
465
|
+
? { excludePath: excludePathOrOptions }
|
|
466
|
+
: (excludePathOrOptions ?? {});
|
|
467
|
+
let entries;
|
|
468
|
+
try {
|
|
469
|
+
entries = readdirSync(dir);
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
// Walk newest-first so a recently-rotated jsonl is found before older
|
|
475
|
+
// ones; if two files contain the fingerprint (rare, e.g. user pasted
|
|
476
|
+
// the same message into two panes) we prefer the more recent.
|
|
477
|
+
const candidates = [];
|
|
478
|
+
for (const name of entries) {
|
|
479
|
+
if (!name.endsWith('.jsonl'))
|
|
480
|
+
continue;
|
|
481
|
+
const full = join(dir, name);
|
|
482
|
+
if (opts.excludePath && full === opts.excludePath)
|
|
483
|
+
continue;
|
|
484
|
+
try {
|
|
485
|
+
const st = statSync(full);
|
|
486
|
+
if (!st.isFile())
|
|
487
|
+
continue;
|
|
488
|
+
if (opts.minMtimeMs !== undefined && st.mtimeMs < opts.minMtimeMs)
|
|
489
|
+
continue;
|
|
490
|
+
candidates.push({ path: full, mtime: st.mtimeMs });
|
|
491
|
+
}
|
|
492
|
+
catch { /* ignore */ }
|
|
493
|
+
}
|
|
494
|
+
candidates.sort((a, b) => b.mtime - a.mtime);
|
|
495
|
+
for (const { path } of candidates) {
|
|
496
|
+
try {
|
|
497
|
+
const fd = openSync(path, 'r');
|
|
498
|
+
try {
|
|
499
|
+
const size = statSync(path).size;
|
|
500
|
+
// Read at most the trailing 1MB — fingerprints land near the end
|
|
501
|
+
// of the jsonl when Claude just wrote them. Cheaper than reading
|
|
502
|
+
// an entire long-lived session.
|
|
503
|
+
const len = Math.min(size, 1024 * 1024);
|
|
504
|
+
const buf = Buffer.alloc(len);
|
|
505
|
+
readSync(fd, buf, 0, len, size - len);
|
|
506
|
+
const text = buf.toString('utf8');
|
|
507
|
+
// We must NOT do a raw includes() here: Claude writes user content
|
|
508
|
+
// as a JSON-encoded string, so any newline in the Lark message is
|
|
509
|
+
// serialized as `\n` on disk while our fingerprint has it
|
|
510
|
+
// collapsed to a single space. Parse each complete jsonl line,
|
|
511
|
+
// pick role:user events, and apply the same stringify+normalise
|
|
512
|
+
// we use in BridgeTurnQueue.ingest. Skip the leading partial line
|
|
513
|
+
// when we read a strict tail (size > len), since it likely begins
|
|
514
|
+
// mid-line.
|
|
515
|
+
const lines = text.split('\n');
|
|
516
|
+
const startIdx = size > len ? 1 : 0;
|
|
517
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
518
|
+
const line = lines[i].trim();
|
|
519
|
+
if (!line)
|
|
520
|
+
continue;
|
|
521
|
+
let ev;
|
|
522
|
+
try {
|
|
523
|
+
ev = JSON.parse(line);
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (!ev || typeof ev !== 'object')
|
|
529
|
+
continue;
|
|
530
|
+
// Per-event timestamp guard — see jsonlContainsFingerprint for
|
|
531
|
+
// the full rationale. Required to keep short fingerprints
|
|
532
|
+
// ("hello", "test") from matching old user lines in unrelated
|
|
533
|
+
// sibling jsonls.
|
|
534
|
+
if (opts.minEventTimestampMs !== undefined && typeof ev.timestamp === 'string') {
|
|
535
|
+
const evMs = Date.parse(ev.timestamp);
|
|
536
|
+
if (Number.isFinite(evMs) && evMs < opts.minEventTimestampMs)
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const role = ev.message?.role ?? ev.type;
|
|
540
|
+
let text = '';
|
|
541
|
+
if (role === 'user') {
|
|
542
|
+
// Skip pure tool_result events — see jsonlContainsFingerprint
|
|
543
|
+
// for the full rationale; in short, tool_result content is
|
|
544
|
+
// log output, not user input, and would false-match short
|
|
545
|
+
// fingerprints like "hello" in unrelated jsonls.
|
|
546
|
+
if (isPureToolResultUserEvent(ev.message?.content))
|
|
547
|
+
continue;
|
|
548
|
+
text = stringifyUserContent(ev.message?.content);
|
|
549
|
+
}
|
|
550
|
+
else if (opts.includeQueueOperations &&
|
|
551
|
+
ev.type === 'queue-operation' &&
|
|
552
|
+
ev.operation === 'enqueue') {
|
|
553
|
+
text = typeof ev.content === 'string' ? ev.content : stringifyUserContent(ev.content);
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
const normalisedText = normaliseForFingerprint(text);
|
|
559
|
+
if (normalisedText.length > 0 && normalisedText.includes(fingerprint)) {
|
|
560
|
+
// Allow caller to veto this candidate (e.g., sibling-pane
|
|
561
|
+
// hijack guard rejecting an untrusted sessionId). On veto,
|
|
562
|
+
// break out of the line loop so we move to the next, older
|
|
563
|
+
// candidate instead of returning `null` after the first
|
|
564
|
+
// fingerprint hit.
|
|
565
|
+
if (opts.acceptCandidate && !opts.acceptCandidate(path)) {
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
return path;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
finally {
|
|
573
|
+
closeSync(fd);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
catch { /* unreadable — skip */ }
|
|
577
|
+
}
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Stronger sibling-pane recovery anchor than the substring fingerprint
|
|
582
|
+
* search. Walks every `.jsonl` in `dir` and returns the paths whose
|
|
583
|
+
* trailing 1MB contains a user/queue event whose normalised text is
|
|
584
|
+
* EXACTLY equal to `normalisedContent` (not a substring), respecting
|
|
585
|
+
* `excludePath`, `minMtimeMs`, `minEventTimestampMs`,
|
|
586
|
+
* `includeQueueOperations`, and `acceptCandidate` the same way as
|
|
587
|
+
* `findJsonlContainingFingerprint`.
|
|
588
|
+
*
|
|
589
|
+
* Returns *all* matches in mtime-descending order — callers must
|
|
590
|
+
* abstain when the result has length > 1, since multiple files containing
|
|
591
|
+
* the same exact normalised content cannot be disambiguated without
|
|
592
|
+
* stronger evidence (and forcing a switch would risk picking the wrong
|
|
593
|
+
* pane). The caller's typical pattern is:
|
|
594
|
+
*
|
|
595
|
+
* - 1 match → switch to it (legitimate post-/clear recovery)
|
|
596
|
+
* - 0 matches → no recovery this tick; wait for stronger signal
|
|
597
|
+
* - >1 match → log and abstain; surface a diagnostic to the user
|
|
598
|
+
*
|
|
599
|
+
* Used by the bridge fingerprint fallback's recovery path for in-pane
|
|
600
|
+
* `/clear`: substring matches risk hijacking on short fingerprints (the
|
|
601
|
+
* literal text "test" matches "run tests" / "test bridge"), but full
|
|
602
|
+
* equality on a Lark message we just wrote is a much stronger anchor.
|
|
603
|
+
*/
|
|
604
|
+
export function findJsonlsContainingExactContent(dir, normalisedContent, options) {
|
|
605
|
+
if (!existsSync(dir) || normalisedContent.length === 0)
|
|
606
|
+
return [];
|
|
607
|
+
const opts = options ?? {};
|
|
608
|
+
let entries;
|
|
609
|
+
try {
|
|
610
|
+
entries = readdirSync(dir);
|
|
611
|
+
}
|
|
612
|
+
catch {
|
|
613
|
+
return [];
|
|
614
|
+
}
|
|
615
|
+
const candidates = [];
|
|
616
|
+
for (const name of entries) {
|
|
617
|
+
if (!name.endsWith('.jsonl'))
|
|
618
|
+
continue;
|
|
619
|
+
const full = join(dir, name);
|
|
620
|
+
if (opts.excludePath && full === opts.excludePath)
|
|
621
|
+
continue;
|
|
622
|
+
try {
|
|
623
|
+
const st = statSync(full);
|
|
624
|
+
if (!st.isFile())
|
|
625
|
+
continue;
|
|
626
|
+
if (opts.minMtimeMs !== undefined && st.mtimeMs < opts.minMtimeMs)
|
|
627
|
+
continue;
|
|
628
|
+
candidates.push({ path: full, mtime: st.mtimeMs });
|
|
629
|
+
}
|
|
630
|
+
catch { /* ignore */ }
|
|
631
|
+
}
|
|
632
|
+
candidates.sort((a, b) => b.mtime - a.mtime);
|
|
633
|
+
const matches = [];
|
|
634
|
+
for (const { path } of candidates) {
|
|
635
|
+
if (opts.acceptCandidate && !opts.acceptCandidate(path))
|
|
636
|
+
continue;
|
|
637
|
+
try {
|
|
638
|
+
const fd = openSync(path, 'r');
|
|
639
|
+
try {
|
|
640
|
+
const size = statSync(path).size;
|
|
641
|
+
const len = Math.min(size, 1024 * 1024);
|
|
642
|
+
const buf = Buffer.alloc(len);
|
|
643
|
+
readSync(fd, buf, 0, len, size - len);
|
|
644
|
+
const text = buf.toString('utf8');
|
|
645
|
+
const lines = text.split('\n');
|
|
646
|
+
const startIdx = size > len ? 1 : 0;
|
|
647
|
+
let hit = false;
|
|
648
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
649
|
+
const line = lines[i].trim();
|
|
650
|
+
if (!line)
|
|
651
|
+
continue;
|
|
652
|
+
let ev;
|
|
653
|
+
try {
|
|
654
|
+
ev = JSON.parse(line);
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
if (!ev || typeof ev !== 'object')
|
|
660
|
+
continue;
|
|
661
|
+
if (opts.minEventTimestampMs !== undefined && typeof ev.timestamp === 'string') {
|
|
662
|
+
const evMs = Date.parse(ev.timestamp);
|
|
663
|
+
if (Number.isFinite(evMs) && evMs < opts.minEventTimestampMs)
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
const role = ev.message?.role ?? ev.type;
|
|
667
|
+
let raw = '';
|
|
668
|
+
if (role === 'user') {
|
|
669
|
+
if (isPureToolResultUserEvent(ev.message?.content))
|
|
670
|
+
continue;
|
|
671
|
+
raw = stringifyUserContent(ev.message?.content);
|
|
672
|
+
}
|
|
673
|
+
else if (opts.includeQueueOperations &&
|
|
674
|
+
ev.type === 'queue-operation' &&
|
|
675
|
+
ev.operation === 'enqueue') {
|
|
676
|
+
raw = typeof ev.content === 'string' ? ev.content : stringifyUserContent(ev.content);
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
const normalised = normaliseForFingerprint(raw);
|
|
682
|
+
if (normalised === normalisedContent) {
|
|
683
|
+
hit = true;
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (hit)
|
|
688
|
+
matches.push(path);
|
|
689
|
+
}
|
|
690
|
+
finally {
|
|
691
|
+
closeSync(fd);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
catch { /* unreadable — skip */ }
|
|
695
|
+
}
|
|
696
|
+
return matches;
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Partition transcript events into history (timestamp ≤ cutoff) and live
|
|
700
|
+
* (timestamp > cutoff, or no parseable timestamp). Used by the bridge
|
|
701
|
+
* watcher when it switches to a new jsonl that may contain pre-existing
|
|
702
|
+
* conversation: anything older than the cutoff (e.g. iTerm-typed turns
|
|
703
|
+
* the user produced before the Lark mark fired) belongs in the seen-set
|
|
704
|
+
* via `BridgeTurnQueue.absorb` so the worker doesn't replay them as
|
|
705
|
+
* "🖥️ 终端本地对话" cards. Anything newer is fed through `ingest()` so
|
|
706
|
+
* the freshly-written Lark user event can match its pending fingerprint.
|
|
707
|
+
*
|
|
708
|
+
* Events with malformed / missing timestamps fall into `live`: better
|
|
709
|
+
* to forward an unattributable event once than to silently drop a real
|
|
710
|
+
* reply because Claude omitted a timestamp.
|
|
711
|
+
*/
|
|
712
|
+
export function splitTranscriptEventsByCutoff(events, cutoffMs) {
|
|
713
|
+
const history = [];
|
|
714
|
+
const live = [];
|
|
715
|
+
for (const ev of events) {
|
|
716
|
+
let evMs = Number.NaN;
|
|
717
|
+
if (typeof ev.timestamp === 'string')
|
|
718
|
+
evMs = Date.parse(ev.timestamp);
|
|
719
|
+
if (Number.isFinite(evMs) && evMs <= cutoffMs)
|
|
720
|
+
history.push(ev);
|
|
721
|
+
else
|
|
722
|
+
live.push(ev);
|
|
723
|
+
}
|
|
724
|
+
return { history, live };
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Read the first event timestamp out of a jsonl. Reads only the leading
|
|
728
|
+
* 4 KB — Claude's `file-history-snapshot` and `SessionStart` events both
|
|
729
|
+
* land in the first few hundred bytes. Returns the parsed millis, or
|
|
730
|
+
* undefined when no parseable timestamp is found in the leading chunk
|
|
731
|
+
* (corrupted file, partial first line, format change).
|
|
732
|
+
*
|
|
733
|
+
* NOTE: not currently wired into the bridge rotation flow. The bridge
|
|
734
|
+
* fingerprint fallback (`decideFingerprintSwitch` in
|
|
735
|
+
* `bridge-rotation-policy.ts`) deliberately rejects candidates outside
|
|
736
|
+
* the pid-derived trust set rather than relying on freshness heuristics
|
|
737
|
+
* — file-creation timestamps cannot prove ownership across panes in
|
|
738
|
+
* the same project dir. Kept here as a reusable primitive for
|
|
739
|
+
* diagnostics and future /clear-recovery work.
|
|
740
|
+
*/
|
|
741
|
+
export function readFirstEventTimestamp(path) {
|
|
742
|
+
let fd;
|
|
743
|
+
try {
|
|
744
|
+
fd = openSync(path, 'r');
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
return undefined;
|
|
748
|
+
}
|
|
749
|
+
try {
|
|
750
|
+
const len = 4096;
|
|
751
|
+
const buf = Buffer.alloc(len);
|
|
752
|
+
let bytesRead = 0;
|
|
753
|
+
try {
|
|
754
|
+
bytesRead = readSync(fd, buf, 0, len, 0);
|
|
755
|
+
}
|
|
756
|
+
catch {
|
|
757
|
+
return undefined;
|
|
758
|
+
}
|
|
759
|
+
if (bytesRead <= 0)
|
|
760
|
+
return undefined;
|
|
761
|
+
const text = buf.subarray(0, bytesRead).toString('utf8');
|
|
762
|
+
const lines = text.split('\n');
|
|
763
|
+
// Drop the trailing partial line if we read exactly `len` bytes — it
|
|
764
|
+
// may not be a complete JSON object. When the whole file is shorter
|
|
765
|
+
// than `len` bytes the last line is complete and we keep it.
|
|
766
|
+
const usable = bytesRead === len ? lines.slice(0, -1) : lines;
|
|
767
|
+
for (const line of usable) {
|
|
768
|
+
const trimmed = line.trim();
|
|
769
|
+
if (!trimmed)
|
|
770
|
+
continue;
|
|
771
|
+
let ev;
|
|
772
|
+
try {
|
|
773
|
+
ev = JSON.parse(trimmed);
|
|
774
|
+
}
|
|
775
|
+
catch {
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
// Top-level `timestamp` field — covers both regular events
|
|
779
|
+
// (user/assistant/attachment) and `file-history-snapshot` records
|
|
780
|
+
// whose `timestamp` lives under `snapshot.timestamp` instead.
|
|
781
|
+
const tsStr = typeof ev?.timestamp === 'string'
|
|
782
|
+
? ev.timestamp
|
|
783
|
+
: typeof ev?.snapshot?.timestamp === 'string'
|
|
784
|
+
? ev.snapshot.timestamp
|
|
785
|
+
: undefined;
|
|
786
|
+
if (!tsStr)
|
|
787
|
+
continue;
|
|
788
|
+
const ms = Date.parse(tsStr);
|
|
789
|
+
if (Number.isFinite(ms))
|
|
790
|
+
return ms;
|
|
791
|
+
}
|
|
792
|
+
return undefined;
|
|
793
|
+
}
|
|
794
|
+
finally {
|
|
795
|
+
closeSync(fd);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
//# sourceMappingURL=claude-transcript.js.map
|