botmux 2.55.0 → 2.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/backend/herdr-backend.d.ts +80 -0
- package/dist/adapters/backend/herdr-backend.d.ts.map +1 -0
- package/dist/adapters/backend/herdr-backend.js +519 -0
- package/dist/adapters/backend/herdr-backend.js.map +1 -0
- package/dist/adapters/backend/session-backend-selector.d.ts +2 -2
- package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -1
- package/dist/adapters/backend/session-backend-selector.js +19 -1
- package/dist/adapters/backend/session-backend-selector.js.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.d.ts +29 -0
- package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.js +51 -4
- package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
- package/dist/adapters/backend/types.d.ts +7 -0
- package/dist/adapters/backend/types.d.ts.map +1 -1
- package/dist/adapters/backend/types.js.map +1 -1
- package/dist/adapters/cli/codex.d.ts.map +1 -1
- package/dist/adapters/cli/codex.js +11 -0
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/bot-registry.d.ts +2 -1
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +262 -15
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +2 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +13 -3
- package/dist/config.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +12 -7
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/pending-response.d.ts +16 -1
- package/dist/core/pending-response.d.ts.map +1 -1
- package/dist/core/pending-response.js +19 -1
- package/dist/core/pending-response.js.map +1 -1
- package/dist/core/session-discovery.d.ts +20 -4
- package/dist/core/session-discovery.d.ts.map +1 -1
- package/dist/core/session-discovery.js +228 -72
- package/dist/core/session-discovery.js.map +1 -1
- package/dist/core/session-manager.d.ts +2 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +77 -45
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/types.d.ts +8 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +74 -62
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.js +4 -4
- package/dist/daemon.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +1 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +1 -0
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts +2 -1
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +19 -2
- 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 +5 -3
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/event-dispatcher.js +3 -3
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/setup/agent-preset.d.ts +78 -0
- package/dist/setup/agent-preset.d.ts.map +1 -0
- package/dist/setup/agent-preset.js +127 -0
- package/dist/setup/agent-preset.js.map +1 -0
- package/dist/setup/bot-config-editor.js +2 -2
- package/dist/setup/bot-config-editor.js.map +1 -1
- package/dist/setup/ensure-herdr-integrations.d.ts +26 -0
- package/dist/setup/ensure-herdr-integrations.d.ts.map +1 -0
- package/dist/setup/ensure-herdr-integrations.js +127 -0
- package/dist/setup/ensure-herdr-integrations.js.map +1 -0
- package/dist/setup/ensure-herdr.d.ts +12 -0
- package/dist/setup/ensure-herdr.d.ts.map +1 -0
- package/dist/setup/ensure-herdr.js +70 -0
- package/dist/setup/ensure-herdr.js.map +1 -0
- package/dist/setup/ensure-opus.d.ts +13 -0
- package/dist/setup/ensure-opus.d.ts.map +1 -0
- package/dist/setup/ensure-opus.js +64 -0
- package/dist/setup/ensure-opus.js.map +1 -0
- package/dist/setup/ensure-tmux.d.ts +13 -0
- package/dist/setup/ensure-tmux.d.ts.map +1 -1
- package/dist/setup/ensure-tmux.js +4 -4
- package/dist/setup/ensure-tmux.js.map +1 -1
- package/dist/setup/index.d.ts +4 -0
- package/dist/setup/index.d.ts.map +1 -1
- package/dist/setup/index.js +78 -1
- package/dist/setup/index.js.map +1 -1
- package/dist/types.d.ts +14 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/transient-snapshot.d.ts +3 -3
- package/dist/utils/transient-snapshot.js +3 -3
- package/dist/worker.js +251 -179
- package/dist/worker.js.map +1 -1
- package/dist/workflows/attempt-resume.d.ts +2 -1
- package/dist/workflows/attempt-resume.d.ts.map +1 -1
- package/dist/workflows/attempt-resume.js.map +1 -1
- package/dist/workflows/definition.d.ts +22 -22
- package/package.json +1 -1
package/dist/worker.js
CHANGED
|
@@ -34,6 +34,7 @@ import { DEFAULT_RENDER_COLS, DEFAULT_RENDER_ROWS, MAX_RENDER_COLS, MAX_RENDER_R
|
|
|
34
34
|
import { createCliAdapterSync } from './adapters/cli/registry.js';
|
|
35
35
|
import { claudeJsonlPathForSession, resolveJsonlFromPid, findOpenClaudeSessionIds, DEFAULT_CLAUDE_DATA_DIR } from './adapters/cli/claude-code.js';
|
|
36
36
|
import { mtrSessionIdForBotmuxSession } from './adapters/cli/mtr.js';
|
|
37
|
+
import { HerdrBackend } from './adapters/backend/herdr-backend.js';
|
|
37
38
|
import { TmuxBackend } from './adapters/backend/tmux-backend.js';
|
|
38
39
|
import { TmuxPipeBackend } from './adapters/backend/tmux-pipe-backend.js';
|
|
39
40
|
import { ZellijBackend, ZELLIJ_CONFIG_KDL } from './adapters/backend/zellij-backend.js';
|
|
@@ -64,6 +65,7 @@ let isTmuxMode = false;
|
|
|
64
65
|
* web-terminal updates flow through the shared scrollback fan-out instead
|
|
65
66
|
* of per-WS attach-session PTYs. Set in spawnCli's adopt branch. */
|
|
66
67
|
let isPipeMode = false;
|
|
68
|
+
let effectiveBackendType = 'pty';
|
|
67
69
|
/** pty-under-zellij backend (BACKEND_TYPE=zellij). Behaves like the non-tmux
|
|
68
70
|
* pty path for the worker (renderer screenshots, relay web terminal) but owns
|
|
69
71
|
* a persistent zellij session that survives daemon restart. */
|
|
@@ -210,6 +212,8 @@ let bridgePendingTail = '';
|
|
|
210
212
|
const bridgeQueue = new BridgeTurnQueue();
|
|
211
213
|
let bridgeWatcher = null;
|
|
212
214
|
let bridgeFallbackTimer = null;
|
|
215
|
+
let herdrAdoptBridgeQuietTimer = null;
|
|
216
|
+
const HERDR_ADOPT_BRIDGE_QUIET_MS = 3_000;
|
|
213
217
|
/** True once we successfully baselined the transcript file. Until then,
|
|
214
218
|
* any data we see is treated as history — absorbed into the queue's seen
|
|
215
219
|
* set without being attributed to a pending Lark turn. This protects the
|
|
@@ -421,6 +425,37 @@ function bridgeProbeOpenSessionIds() {
|
|
|
421
425
|
for (const path of opened)
|
|
422
426
|
bridgeRememberSessionIdForPath(path);
|
|
423
427
|
}
|
|
428
|
+
function bridgeShouldEmitAfterTranscriptQuiet() {
|
|
429
|
+
return lastInitConfig?.adoptMode === true
|
|
430
|
+
&& lastInitConfig?.adoptSource === 'herdr'
|
|
431
|
+
&& lastInitConfig?.cliId === 'claude-code'
|
|
432
|
+
&& !!bridgeJsonlPath;
|
|
433
|
+
}
|
|
434
|
+
function clearHerdrAdoptBridgeQuietTimer() {
|
|
435
|
+
if (!herdrAdoptBridgeQuietTimer)
|
|
436
|
+
return;
|
|
437
|
+
clearTimeout(herdrAdoptBridgeQuietTimer);
|
|
438
|
+
herdrAdoptBridgeQuietTimer = null;
|
|
439
|
+
}
|
|
440
|
+
function scheduleHerdrAdoptBridgeQuietEmit() {
|
|
441
|
+
if (!bridgeShouldEmitAfterTranscriptQuiet())
|
|
442
|
+
return;
|
|
443
|
+
clearHerdrAdoptBridgeQuietTimer();
|
|
444
|
+
herdrAdoptBridgeQuietTimer = setTimeout(() => {
|
|
445
|
+
herdrAdoptBridgeQuietTimer = null;
|
|
446
|
+
if (!bridgeShouldEmitAfterTranscriptQuiet())
|
|
447
|
+
return;
|
|
448
|
+
try {
|
|
449
|
+
bridgeDrainAndMaybeEmit();
|
|
450
|
+
markPromptReady();
|
|
451
|
+
log('Bridge quiet emit attempted — herdr adopt mode');
|
|
452
|
+
}
|
|
453
|
+
catch (err) {
|
|
454
|
+
log(`Bridge quiet emit error: ${err.message}`);
|
|
455
|
+
}
|
|
456
|
+
}, HERDR_ADOPT_BRIDGE_QUIET_MS);
|
|
457
|
+
herdrAdoptBridgeQuietTimer.unref?.();
|
|
458
|
+
}
|
|
424
459
|
function bridgeAbsorbBaseline() {
|
|
425
460
|
if (!bridgeJsonlPath)
|
|
426
461
|
return;
|
|
@@ -523,7 +558,7 @@ function bridgeApplyFingerprintSwitch(matched, reason, cutoffMs) {
|
|
|
523
558
|
try {
|
|
524
559
|
bridgeWatcher = fsWatch(matched, { persistent: false }, () => {
|
|
525
560
|
try {
|
|
526
|
-
|
|
561
|
+
performBridgeIngestAndScheduleQuietEmit();
|
|
527
562
|
}
|
|
528
563
|
catch (err) {
|
|
529
564
|
log(`Bridge ingest error: ${err.message}`);
|
|
@@ -800,7 +835,7 @@ function performRotationSwitch(newPath, cutoffMs, reason) {
|
|
|
800
835
|
try {
|
|
801
836
|
bridgeWatcher = fsWatch(newPath, { persistent: false }, () => {
|
|
802
837
|
try {
|
|
803
|
-
|
|
838
|
+
performBridgeIngestAndScheduleQuietEmit();
|
|
804
839
|
}
|
|
805
840
|
catch (err) {
|
|
806
841
|
log(`Bridge ingest error: ${err.message}`);
|
|
@@ -985,7 +1020,7 @@ function maybeFollowSessionRotationViaPid() {
|
|
|
985
1020
|
try {
|
|
986
1021
|
bridgeWatcher = fsWatch(resolved.path, { persistent: false }, () => {
|
|
987
1022
|
try {
|
|
988
|
-
|
|
1023
|
+
performBridgeIngestAndScheduleQuietEmit();
|
|
989
1024
|
}
|
|
990
1025
|
catch (err) {
|
|
991
1026
|
log(`Bridge ingest error: ${err.message}`);
|
|
@@ -1083,6 +1118,14 @@ function bridgeIngest() {
|
|
|
1083
1118
|
bridgePendingTail = result.pendingTail;
|
|
1084
1119
|
bridgeQueue.ingest(result.events, bridgeJsonlPath);
|
|
1085
1120
|
}
|
|
1121
|
+
function performBridgeIngestAndScheduleQuietEmit() {
|
|
1122
|
+
const beforePath = bridgeJsonlPath;
|
|
1123
|
+
const beforeOffset = bridgeOffset;
|
|
1124
|
+
bridgeIngest();
|
|
1125
|
+
if (bridgeJsonlPath && (bridgeJsonlPath !== beforePath || bridgeOffset > beforeOffset)) {
|
|
1126
|
+
scheduleHerdrAdoptBridgeQuietEmit();
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1086
1129
|
function startBridgeWatcher(jsonlPath, opts) {
|
|
1087
1130
|
bridgeJsonlPath = jsonlPath;
|
|
1088
1131
|
bridgeJsonlDir = dirname(jsonlPath);
|
|
@@ -1172,7 +1215,7 @@ function startBridgeWatcher(jsonlPath, opts) {
|
|
|
1172
1215
|
try {
|
|
1173
1216
|
bridgeWatcher = fsWatch(bridgeJsonlPath, { persistent: false }, () => {
|
|
1174
1217
|
try {
|
|
1175
|
-
|
|
1218
|
+
performBridgeIngestAndScheduleQuietEmit();
|
|
1176
1219
|
}
|
|
1177
1220
|
catch (err) {
|
|
1178
1221
|
log(`Bridge ingest error: ${err.message}`);
|
|
@@ -1184,7 +1227,7 @@ function startBridgeWatcher(jsonlPath, opts) {
|
|
|
1184
1227
|
}
|
|
1185
1228
|
bridgeFallbackTimer = setInterval(() => {
|
|
1186
1229
|
try {
|
|
1187
|
-
|
|
1230
|
+
performBridgeIngestAndScheduleQuietEmit();
|
|
1188
1231
|
}
|
|
1189
1232
|
catch (err) {
|
|
1190
1233
|
log(`Bridge ingest error: ${err.message}`);
|
|
@@ -1192,6 +1235,7 @@ function startBridgeWatcher(jsonlPath, opts) {
|
|
|
1192
1235
|
}, 1000);
|
|
1193
1236
|
}
|
|
1194
1237
|
function stopBridgeWatcher() {
|
|
1238
|
+
clearHerdrAdoptBridgeQuietTimer();
|
|
1195
1239
|
if (bridgeWatcher) {
|
|
1196
1240
|
try {
|
|
1197
1241
|
bridgeWatcher.close();
|
|
@@ -2776,7 +2820,170 @@ function stopScreenUpdates() {
|
|
|
2776
2820
|
lastAnalyzerSnapshot = '';
|
|
2777
2821
|
}
|
|
2778
2822
|
// ─── PTY Management ──────────────────────────────────────────────────────────
|
|
2823
|
+
function setupAdoptTranscriptBridges(cfg) {
|
|
2824
|
+
if (cfg.bridgeJsonlPath) {
|
|
2825
|
+
startBridgeWatcher(cfg.bridgeJsonlPath, {
|
|
2826
|
+
cliPid: cfg.adoptCliPid,
|
|
2827
|
+
cliCwd: cfg.adoptCwd,
|
|
2828
|
+
});
|
|
2829
|
+
}
|
|
2830
|
+
else if (cfg.cliId === 'codex') {
|
|
2831
|
+
const adoptStartMs = Date.now();
|
|
2832
|
+
codexAdoptStartMs = adoptStartMs;
|
|
2833
|
+
codexBridgeQueue.setLocalTurns(true, adoptStartMs);
|
|
2834
|
+
let rolloutPath;
|
|
2835
|
+
if (cfg.cliSessionId)
|
|
2836
|
+
rolloutPath = findCodexRolloutBySessionId(cfg.cliSessionId);
|
|
2837
|
+
if (!rolloutPath && cfg.adoptCliPid) {
|
|
2838
|
+
const probed = findCodexRolloutByPid(cfg.adoptCliPid);
|
|
2839
|
+
if (probed)
|
|
2840
|
+
rolloutPath = probed.path;
|
|
2841
|
+
}
|
|
2842
|
+
if (rolloutPath) {
|
|
2843
|
+
codexBridgeAttach(rolloutPath, 'split-live');
|
|
2844
|
+
}
|
|
2845
|
+
else {
|
|
2846
|
+
if (cfg.cliSessionId)
|
|
2847
|
+
codexBridgePendingSessionId = cfg.cliSessionId;
|
|
2848
|
+
codexAdoptPendingPid = cfg.adoptCliPid;
|
|
2849
|
+
codexBridgeStartTimer();
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
else if (cfg.cliId === 'coco') {
|
|
2853
|
+
const adoptStartMs = Date.now();
|
|
2854
|
+
codexAdoptStartMs = adoptStartMs;
|
|
2855
|
+
codexBridgeQueue.setLocalTurns(true, adoptStartMs);
|
|
2856
|
+
let eventsPath;
|
|
2857
|
+
if (cfg.cliSessionId)
|
|
2858
|
+
eventsPath = cocoEventsPathForSession(cfg.cliSessionId);
|
|
2859
|
+
if (!eventsPath && cfg.adoptCliPid) {
|
|
2860
|
+
const probed = findCocoSessionByPid(cfg.adoptCliPid);
|
|
2861
|
+
if (probed)
|
|
2862
|
+
eventsPath = probed.eventsPath;
|
|
2863
|
+
}
|
|
2864
|
+
if (eventsPath) {
|
|
2865
|
+
const sessionDir = dirname(eventsPath);
|
|
2866
|
+
if (!existsSync(sessionDir)) {
|
|
2867
|
+
send({
|
|
2868
|
+
type: 'final_output',
|
|
2869
|
+
content: '⚠️ 当前 CoCo 进程的会话目录已被删除(可能是 e2e 测试清理或手动 rm),写到 events.jsonl 的内容会落到一个失效 inode 上,桥接读不到。请重启 CoCo 后重新 /adopt。',
|
|
2870
|
+
lastUuid: `coco-adopt-stale-${randomBytes(4).toString('hex')}`,
|
|
2871
|
+
turnId: 'coco-adopt-stale',
|
|
2872
|
+
});
|
|
2873
|
+
log(`CoCo adopt: session dir missing, bridge disabled (${sessionDir})`);
|
|
2874
|
+
}
|
|
2875
|
+
else {
|
|
2876
|
+
codexBridgeAttach(eventsPath, 'split-live');
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
else {
|
|
2880
|
+
codexAdoptPendingPid = cfg.adoptCliPid;
|
|
2881
|
+
}
|
|
2882
|
+
codexBridgeStartTimer();
|
|
2883
|
+
}
|
|
2884
|
+
else if (cfg.cliId === 'mtr') {
|
|
2885
|
+
const adoptStartMs = Date.now();
|
|
2886
|
+
codexAdoptStartMs = adoptStartMs;
|
|
2887
|
+
codexBridgeQueue.setLocalTurns(true, adoptStartMs);
|
|
2888
|
+
if (cfg.cliSessionId)
|
|
2889
|
+
codexBridgePendingSessionId = cfg.cliSessionId;
|
|
2890
|
+
const source = findMtrSessionById(cfg.cliSessionId)
|
|
2891
|
+
?? findLatestMtrSessionByDirectory(cfg.adoptCwd ?? cfg.workingDir);
|
|
2892
|
+
if (source) {
|
|
2893
|
+
codexBridgePendingSessionId = undefined;
|
|
2894
|
+
mtrBridgeAttach(source, 'split-live');
|
|
2895
|
+
}
|
|
2896
|
+
else {
|
|
2897
|
+
codexBridgeStartTimer();
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
function adoptIdleAdapter(cfg) {
|
|
2902
|
+
return cfg.bridgeJsonlPath
|
|
2903
|
+
? createCliAdapterSync('claude-code', undefined)
|
|
2904
|
+
: cfg.cliId === 'codex' || cfg.cliId === 'coco' || cfg.cliId === 'mtr'
|
|
2905
|
+
? createCliAdapterSync(cfg.cliId, undefined)
|
|
2906
|
+
: { completionPattern: undefined, readyPattern: undefined };
|
|
2907
|
+
}
|
|
2908
|
+
function setupAdoptInputAdapter(cfg) {
|
|
2909
|
+
if (cfg.cliId === 'codex') {
|
|
2910
|
+
cliAdapter = createCliAdapterSync('codex', cfg.cliPathOverride);
|
|
2911
|
+
}
|
|
2912
|
+
else if (cfg.cliId === 'mtr') {
|
|
2913
|
+
cliAdapter = createCliAdapterSync('mtr', cfg.cliPathOverride);
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
function setupAdoptIdleDetection(cfg, label) {
|
|
2917
|
+
idleDetector = new IdleDetector(adoptIdleAdapter(cfg));
|
|
2918
|
+
idleDetector.onIdle(() => {
|
|
2919
|
+
log(`Prompt detected (idle) — ${label} adopt mode`);
|
|
2920
|
+
try {
|
|
2921
|
+
bridgeDrainAndMaybeEmit();
|
|
2922
|
+
}
|
|
2923
|
+
catch (err) {
|
|
2924
|
+
log(`Bridge emit error: ${err.message}`);
|
|
2925
|
+
}
|
|
2926
|
+
try {
|
|
2927
|
+
codexBridgeDrainAndMaybeEmit();
|
|
2928
|
+
}
|
|
2929
|
+
catch (err) {
|
|
2930
|
+
log(`Codex bridge emit error: ${err.message}`);
|
|
2931
|
+
}
|
|
2932
|
+
markPromptReady();
|
|
2933
|
+
});
|
|
2934
|
+
}
|
|
2935
|
+
function seedBackendScreen(source, be) {
|
|
2936
|
+
try {
|
|
2937
|
+
const initial = be.captureCurrentScreen?.() ?? '';
|
|
2938
|
+
if (initial.length > 0)
|
|
2939
|
+
onPtyData(initial);
|
|
2940
|
+
}
|
|
2941
|
+
catch (err) {
|
|
2942
|
+
log(`${source} captureCurrentScreen failed: ${err.message}`);
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2779
2945
|
function spawnCli(cfg) {
|
|
2946
|
+
// ── Adopt mode: observe the user's existing terminal backend (no attach) ──
|
|
2947
|
+
if (cfg.adoptMode && cfg.adoptSource === 'herdr' && cfg.adoptHerdrSessionName && (cfg.adoptHerdrPaneId || cfg.adoptHerdrTarget)) {
|
|
2948
|
+
isTmuxMode = false;
|
|
2949
|
+
isPipeMode = true;
|
|
2950
|
+
isZellijMode = false;
|
|
2951
|
+
const cols = cfg.adoptPaneCols ?? PTY_COLS;
|
|
2952
|
+
const rows = cfg.adoptPaneRows ?? PTY_ROWS;
|
|
2953
|
+
const target = cfg.adoptHerdrTarget ?? cfg.adoptHerdrPaneId;
|
|
2954
|
+
const herdrBe = new HerdrBackend(cfg.adoptHerdrSessionName, {
|
|
2955
|
+
externalTarget: {
|
|
2956
|
+
sessionName: cfg.adoptHerdrSessionName,
|
|
2957
|
+
target,
|
|
2958
|
+
paneId: cfg.adoptHerdrPaneId,
|
|
2959
|
+
},
|
|
2960
|
+
});
|
|
2961
|
+
effectiveBackendType = 'herdr';
|
|
2962
|
+
backend = herdrBe;
|
|
2963
|
+
herdrBe.spawn('', [], {
|
|
2964
|
+
cwd: cfg.workingDir,
|
|
2965
|
+
cols,
|
|
2966
|
+
rows,
|
|
2967
|
+
env: process.env,
|
|
2968
|
+
});
|
|
2969
|
+
seedBackendScreen('herdr adopt', herdrBe);
|
|
2970
|
+
setupAdoptTranscriptBridges(cfg);
|
|
2971
|
+
setupAdoptInputAdapter(cfg);
|
|
2972
|
+
setupAdoptIdleDetection(cfg, 'herdr');
|
|
2973
|
+
backend.onData(onPtyData);
|
|
2974
|
+
backend.onExit((code, signal) => {
|
|
2975
|
+
log(`Adopted herdr stream ended (code: ${code}, signal: ${signal})`);
|
|
2976
|
+
backend = null;
|
|
2977
|
+
isPromptReady = false;
|
|
2978
|
+
stopBridgeWatcher();
|
|
2979
|
+
send({ type: 'claude_exit', code, signal });
|
|
2980
|
+
});
|
|
2981
|
+
awaitingFirstPrompt = false;
|
|
2982
|
+
renderer?.markNewTurn();
|
|
2983
|
+
log(`Adopt mode (herdr): observing ${cfg.adoptHerdrSessionName}:${target} (${cols}x${rows})`);
|
|
2984
|
+
return;
|
|
2985
|
+
}
|
|
2986
|
+
// ── Adopt mode: pipe-pane the user's existing tmux pane (no attach) ──
|
|
2780
2987
|
// ── Adopt mode: observe the user's existing pane (no attach / non-invasive) ──
|
|
2781
2988
|
// tmux: pipe-pane (raw stream). zellij: dump-screen poll + action drive.
|
|
2782
2989
|
if (cfg.adoptMode && (cfg.adoptTmuxTarget || cfg.adoptZellijPaneId)) {
|
|
@@ -2786,11 +2993,13 @@ function spawnCli(cfg) {
|
|
|
2786
2993
|
// PTY-per-WS — we don't attach to anything).
|
|
2787
2994
|
isTmuxMode = true;
|
|
2788
2995
|
isPipeMode = true;
|
|
2996
|
+
isZellijMode = !!cfg.adoptZellijPaneId;
|
|
2789
2997
|
const cols = cfg.adoptPaneCols ?? PTY_COLS;
|
|
2790
2998
|
const rows = cfg.adoptPaneRows ?? PTY_ROWS;
|
|
2791
2999
|
const observeBe = cfg.adoptZellijPaneId
|
|
2792
3000
|
? new ZellijObserveBackend(cfg.adoptZellijSession ?? '', cfg.adoptZellijPaneId, { cliPid: cfg.adoptCliPid })
|
|
2793
3001
|
: new TmuxPipeBackend(cfg.adoptTmuxTarget);
|
|
3002
|
+
effectiveBackendType = cfg.adoptZellijPaneId ? 'zellij' : 'tmux';
|
|
2794
3003
|
backend = observeBe;
|
|
2795
3004
|
observeBe.spawn('', [], {
|
|
2796
3005
|
cwd: cfg.workingDir,
|
|
@@ -2801,162 +3010,10 @@ function spawnCli(cfg) {
|
|
|
2801
3010
|
// Seed the shared scrollback with the pane's current screen so any
|
|
2802
3011
|
// already-connected (or future) WS clients render meaningful content
|
|
2803
3012
|
// immediately, instead of waiting for the next observe tick.
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
}
|
|
2809
|
-
catch (err) {
|
|
2810
|
-
log(`captureCurrentScreen failed: ${err.message}`);
|
|
2811
|
-
}
|
|
2812
|
-
// Bridge mode: tail the adopted CLI's transcript to harvest assistant
|
|
2813
|
-
// turns out-of-band. Two paths:
|
|
2814
|
-
// - claude-code: cfg.bridgeJsonlPath is set when adopt knew the sid.
|
|
2815
|
-
// - codex: locate rollout via cliSessionId (daemon's discovery probe)
|
|
2816
|
-
// or by reading /proc/<pid>/fd. Both modes enable adopt-only local
|
|
2817
|
-
// turn synthesis so iTerm-typed conversation also reaches Lark.
|
|
2818
|
-
if (cfg.bridgeJsonlPath) {
|
|
2819
|
-
startBridgeWatcher(cfg.bridgeJsonlPath, {
|
|
2820
|
-
cliPid: cfg.adoptCliPid,
|
|
2821
|
-
cliCwd: cfg.adoptCwd,
|
|
2822
|
-
});
|
|
2823
|
-
}
|
|
2824
|
-
else if (cfg.cliId === 'codex') {
|
|
2825
|
-
const adoptStartMs = Date.now();
|
|
2826
|
-
codexAdoptStartMs = adoptStartMs;
|
|
2827
|
-
codexBridgeQueue.setLocalTurns(true, adoptStartMs);
|
|
2828
|
-
let rolloutPath;
|
|
2829
|
-
if (cfg.cliSessionId)
|
|
2830
|
-
rolloutPath = findCodexRolloutBySessionId(cfg.cliSessionId);
|
|
2831
|
-
if (!rolloutPath && cfg.adoptCliPid) {
|
|
2832
|
-
const probed = findCodexRolloutByPid(cfg.adoptCliPid);
|
|
2833
|
-
if (probed)
|
|
2834
|
-
rolloutPath = probed.path;
|
|
2835
|
-
}
|
|
2836
|
-
if (rolloutPath) {
|
|
2837
|
-
// Adopt-time attach: split-live so any iTerm activity that
|
|
2838
|
-
// happened in the brief window between adopt detection and worker
|
|
2839
|
-
// spawn (or between codex's own startup writes and now) lands as
|
|
2840
|
-
// live, not absorbed history.
|
|
2841
|
-
codexBridgeAttach(rolloutPath, 'split-live');
|
|
2842
|
-
}
|
|
2843
|
-
else {
|
|
2844
|
-
// Couldn't locate yet — start poller. The 1s timer keeps trying
|
|
2845
|
-
// both findCodexRolloutBySessionId (if cliSessionId is set) and
|
|
2846
|
-
// findCodexRolloutByPid (passed via the discovery hooks below).
|
|
2847
|
-
if (cfg.cliSessionId)
|
|
2848
|
-
codexBridgePendingSessionId = cfg.cliSessionId;
|
|
2849
|
-
codexAdoptPendingPid = cfg.adoptCliPid;
|
|
2850
|
-
codexBridgeStartTimer();
|
|
2851
|
-
}
|
|
2852
|
-
}
|
|
2853
|
-
else if (cfg.cliId === 'coco') {
|
|
2854
|
-
// CoCo adopt: parallel to codex, but the events.jsonl path is
|
|
2855
|
-
// deterministic from cliSessionId, so once the daemon-side discovery
|
|
2856
|
-
// surfaced an sid we know the path immediately. The file may not
|
|
2857
|
-
// exist yet (CoCo creates it on first event); codexBridgeAttach's
|
|
2858
|
-
// split-live-with-missing-file branch degrades to fresh, and the
|
|
2859
|
-
// late-attach poller catches re-creation.
|
|
2860
|
-
const adoptStartMs = Date.now();
|
|
2861
|
-
codexAdoptStartMs = adoptStartMs;
|
|
2862
|
-
codexBridgeQueue.setLocalTurns(true, adoptStartMs);
|
|
2863
|
-
let eventsPath;
|
|
2864
|
-
if (cfg.cliSessionId)
|
|
2865
|
-
eventsPath = cocoEventsPathForSession(cfg.cliSessionId);
|
|
2866
|
-
if (!eventsPath && cfg.adoptCliPid) {
|
|
2867
|
-
const probed = findCocoSessionByPid(cfg.adoptCliPid);
|
|
2868
|
-
if (probed)
|
|
2869
|
-
eventsPath = probed.eventsPath;
|
|
2870
|
-
}
|
|
2871
|
-
if (eventsPath) {
|
|
2872
|
-
// If the session DIRECTORY is missing (not just events.jsonl), CoCo
|
|
2873
|
-
// is operating on an unlinked inode — common after an e2e test or
|
|
2874
|
-
// manual cleanup wiped the dir while CoCo kept its fds open. The
|
|
2875
|
-
// bridge file will never appear, so warn the user once via Lark
|
|
2876
|
-
// instead of polling forever in silence.
|
|
2877
|
-
const sessionDir = dirname(eventsPath);
|
|
2878
|
-
if (!existsSync(sessionDir)) {
|
|
2879
|
-
send({
|
|
2880
|
-
type: 'final_output',
|
|
2881
|
-
content: '⚠️ 当前 CoCo 进程的会话目录已被删除(可能是 e2e 测试清理或手动 rm),写到 events.jsonl 的内容会落到一个失效 inode 上,桥接读不到。请重启 CoCo 后重新 /adopt。',
|
|
2882
|
-
lastUuid: `coco-adopt-stale-${randomBytes(4).toString('hex')}`,
|
|
2883
|
-
turnId: 'coco-adopt-stale',
|
|
2884
|
-
});
|
|
2885
|
-
log(`CoCo adopt: session dir missing, bridge disabled (${sessionDir})`);
|
|
2886
|
-
}
|
|
2887
|
-
else {
|
|
2888
|
-
codexBridgeAttach(eventsPath, 'split-live');
|
|
2889
|
-
}
|
|
2890
|
-
}
|
|
2891
|
-
else {
|
|
2892
|
-
// No sid known yet — fall back to PID-walk in the late-attach
|
|
2893
|
-
// poller. Reuses codexAdoptPendingPid since the timer dispatches
|
|
2894
|
-
// by cliId at probe time (see codexBridgeStartTimer).
|
|
2895
|
-
codexAdoptPendingPid = cfg.adoptCliPid;
|
|
2896
|
-
}
|
|
2897
|
-
// Always run the bridge poller for CoCo adopt — events.jsonl is created
|
|
2898
|
-
// lazily on first event, so fs.watch typically ENOENTs at attach time.
|
|
2899
|
-
// The 1s timer covers ingest + emit even when the watcher never armed,
|
|
2900
|
-
// and is idempotent (no-op if already started).
|
|
2901
|
-
codexBridgeStartTimer();
|
|
2902
|
-
}
|
|
2903
|
-
else if (cfg.cliId === 'mtr') {
|
|
2904
|
-
const adoptStartMs = Date.now();
|
|
2905
|
-
codexAdoptStartMs = adoptStartMs;
|
|
2906
|
-
codexBridgeQueue.setLocalTurns(true, adoptStartMs);
|
|
2907
|
-
if (cfg.cliSessionId)
|
|
2908
|
-
codexBridgePendingSessionId = cfg.cliSessionId;
|
|
2909
|
-
const source = findMtrSessionById(cfg.cliSessionId)
|
|
2910
|
-
?? findLatestMtrSessionByDirectory(cfg.adoptCwd ?? cfg.workingDir);
|
|
2911
|
-
if (source) {
|
|
2912
|
-
codexBridgePendingSessionId = undefined;
|
|
2913
|
-
mtrBridgeAttach(source, 'split-live');
|
|
2914
|
-
}
|
|
2915
|
-
else {
|
|
2916
|
-
codexBridgeStartTimer();
|
|
2917
|
-
}
|
|
2918
|
-
}
|
|
2919
|
-
// Idle detection. In bridge mode we use the adopted CLI's real
|
|
2920
|
-
// completion/ready patterns (e.g. "Worked for Xs") so tool-execution
|
|
2921
|
-
// pauses don't trigger a premature emit. Other adopt cases keep the
|
|
2922
|
-
// minimal output-quiescence-only detector.
|
|
2923
|
-
const idleAdapter = cfg.bridgeJsonlPath
|
|
2924
|
-
? createCliAdapterSync('claude-code', undefined)
|
|
2925
|
-
: cfg.cliId === 'codex' || cfg.cliId === 'coco' || cfg.cliId === 'mtr'
|
|
2926
|
-
? createCliAdapterSync(cfg.cliId, undefined)
|
|
2927
|
-
: { completionPattern: undefined, readyPattern: undefined };
|
|
2928
|
-
idleDetector = new IdleDetector(idleAdapter);
|
|
2929
|
-
// Codex adopt write path: route Lark messages through the codex
|
|
2930
|
-
// adapter's writeInput so they pick up the 200 ms paste-detection
|
|
2931
|
-
// delay + Enter-retry + ~/.codex/history.jsonl verification loop
|
|
2932
|
-
// (see src/adapters/cli/codex.ts:125-178). Without it, Codex TUI's
|
|
2933
|
-
// "\n treated as Enter" handling leaves multi-line submits stuck
|
|
2934
|
-
// in the input box. Other adopt CLIs keep the simpler raw
|
|
2935
|
-
// sendText+Enter path — claude-code adopt has its own bridge
|
|
2936
|
-
// verify path; gemini / coco / opencode / aiden haven't surfaced
|
|
2937
|
-
// this failure mode and we don't want to risk regressing them.
|
|
2938
|
-
if (cfg.cliId === 'codex') {
|
|
2939
|
-
cliAdapter = createCliAdapterSync('codex', cfg.cliPathOverride);
|
|
2940
|
-
}
|
|
2941
|
-
else if (cfg.cliId === 'mtr') {
|
|
2942
|
-
cliAdapter = createCliAdapterSync('mtr', cfg.cliPathOverride);
|
|
2943
|
-
}
|
|
2944
|
-
idleDetector.onIdle(() => {
|
|
2945
|
-
log('Prompt detected (idle) — adopt mode');
|
|
2946
|
-
try {
|
|
2947
|
-
bridgeDrainAndMaybeEmit();
|
|
2948
|
-
}
|
|
2949
|
-
catch (err) {
|
|
2950
|
-
log(`Bridge emit error: ${err.message}`);
|
|
2951
|
-
}
|
|
2952
|
-
try {
|
|
2953
|
-
codexBridgeDrainAndMaybeEmit();
|
|
2954
|
-
}
|
|
2955
|
-
catch (err) {
|
|
2956
|
-
log(`Codex bridge emit error: ${err.message}`);
|
|
2957
|
-
}
|
|
2958
|
-
markPromptReady();
|
|
2959
|
-
});
|
|
3013
|
+
seedBackendScreen(`${effectiveBackendType} adopt`, observeBe);
|
|
3014
|
+
setupAdoptTranscriptBridges(cfg);
|
|
3015
|
+
setupAdoptIdleDetection(cfg, 'pipe');
|
|
3016
|
+
setupAdoptInputAdapter(cfg);
|
|
2960
3017
|
backend.onData(onPtyData);
|
|
2961
3018
|
backend.onExit((code, signal) => {
|
|
2962
3019
|
log(`Adopted pipe-pane stream ended (code: ${code}, signal: ${signal})`);
|
|
@@ -2967,7 +3024,8 @@ function spawnCli(cfg) {
|
|
|
2967
3024
|
});
|
|
2968
3025
|
awaitingFirstPrompt = false;
|
|
2969
3026
|
renderer?.markNewTurn();
|
|
2970
|
-
|
|
3027
|
+
const target = cfg.adoptZellijPaneId ? `${cfg.adoptZellijSession}/${cfg.adoptZellijPaneId}` : cfg.adoptTmuxTarget;
|
|
3028
|
+
log(`Adopt mode (${effectiveBackendType}): observing ${target} (${cols}x${rows})`);
|
|
2971
3029
|
return;
|
|
2972
3030
|
}
|
|
2973
3031
|
cliAdapter = createCliAdapterSync(cfg.cliId, cfg.cliPathOverride);
|
|
@@ -2981,10 +3039,15 @@ function spawnCli(cfg) {
|
|
|
2981
3039
|
log('tmux backend requested but functional probe failed — falling back to PTY backend');
|
|
2982
3040
|
effectiveBackend = 'pty';
|
|
2983
3041
|
}
|
|
3042
|
+
if (effectiveBackend === 'herdr' && !HerdrBackend.isAvailable()) {
|
|
3043
|
+
log('herdr backend requested but probe failed — falling back to PTY backend');
|
|
3044
|
+
effectiveBackend = 'pty';
|
|
3045
|
+
}
|
|
2984
3046
|
if (effectiveBackend === 'zellij' && !ZellijBackend.isAvailable()) {
|
|
2985
3047
|
log('zellij backend requested but functional probe failed (need zellij >= 0.44) — falling back to PTY backend');
|
|
2986
3048
|
effectiveBackend = 'pty';
|
|
2987
3049
|
}
|
|
3050
|
+
effectiveBackendType = effectiveBackend;
|
|
2988
3051
|
const selectedBackend = selectSessionBackend({ sessionId: cfg.sessionId, backendType: effectiveBackend });
|
|
2989
3052
|
isTmuxMode = selectedBackend.isTmuxMode;
|
|
2990
3053
|
isPipeMode = selectedBackend.isPipeMode;
|
|
@@ -3049,13 +3112,22 @@ function spawnCli(cfg) {
|
|
|
3049
3112
|
// just `tmux attach-session`s — logging `Spawning: <new bin>` in that case
|
|
3050
3113
|
// is misleading and has cost real debugging time. (CliId-mismatch reattach
|
|
3051
3114
|
// is now blocked upstream in restoreActiveSessions / killStalePids.)
|
|
3052
|
-
const
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3115
|
+
const persistentSessionName = effectiveBackendType === 'tmux'
|
|
3116
|
+
? TmuxBackend.sessionName(cfg.sessionId)
|
|
3117
|
+
: effectiveBackendType === 'herdr'
|
|
3118
|
+
? HerdrBackend.sessionName(cfg.sessionId)
|
|
3119
|
+
: effectiveBackendType === 'zellij'
|
|
3120
|
+
? ZellijBackend.sessionName(cfg.sessionId)
|
|
3121
|
+
: undefined;
|
|
3122
|
+
const willReattachPersistent = persistentSessionName
|
|
3123
|
+
? effectiveBackendType === 'tmux'
|
|
3124
|
+
? TmuxBackend.hasSession(persistentSessionName)
|
|
3125
|
+
: effectiveBackendType === 'zellij'
|
|
3126
|
+
? ZellijBackend.hasSession(persistentSessionName)
|
|
3127
|
+
: HerdrBackend.hasSession(persistentSessionName)
|
|
3128
|
+
: false;
|
|
3129
|
+
if (willReattachPersistent) {
|
|
3130
|
+
log(`Re-attaching to existing ${effectiveBackendType} session: ${persistentSessionName} (requested CLI: ${cliAdapter.resolvedBin})`);
|
|
3059
3131
|
}
|
|
3060
3132
|
else {
|
|
3061
3133
|
log(`Spawning fresh CLI: ${cliAdapter.resolvedBin} ${args.join(' ')} (cwd: ${cfg.workingDir})`);
|
|
@@ -3253,16 +3325,9 @@ function spawnCli(cfg) {
|
|
|
3253
3325
|
isPromptReady = false;
|
|
3254
3326
|
send({ type: 'claude_exit', code, signal });
|
|
3255
3327
|
});
|
|
3256
|
-
if (isPipeMode && backend
|
|
3257
|
-
log(`Re-attached to existing
|
|
3258
|
-
|
|
3259
|
-
const initial = backend.captureCurrentScreen();
|
|
3260
|
-
if (initial.length > 0)
|
|
3261
|
-
onPtyData(initial);
|
|
3262
|
-
}
|
|
3263
|
-
catch (err) {
|
|
3264
|
-
log(`captureCurrentScreen failed: ${err.message}`);
|
|
3265
|
-
}
|
|
3328
|
+
if (isPipeMode && backend && 'isReattach' in backend && backend.isReattach) {
|
|
3329
|
+
log(`Re-attached to existing ${effectiveBackendType} session via pipe backend: ${persistentSessionName}`);
|
|
3330
|
+
seedBackendScreen(`${effectiveBackendType} reattach`, backend);
|
|
3266
3331
|
}
|
|
3267
3332
|
// Fallback: if the CLI takes too long to show its prompt (e.g. slow
|
|
3268
3333
|
// plugin init), unblock screen updates so the card doesn't stay at
|
|
@@ -4014,6 +4079,13 @@ process.on('message', async (raw) => {
|
|
|
4014
4079
|
break;
|
|
4015
4080
|
}
|
|
4016
4081
|
log('Restart requested');
|
|
4082
|
+
// Must destroySession(), not kill(): for persistent backends (tmux/herdr)
|
|
4083
|
+
// kill() only detaches — the backing session + CLI process keep running,
|
|
4084
|
+
// so the resume:true spawnCli below would re-attach to the SAME live CLI
|
|
4085
|
+
// (selectSessionBackend reattaches whenever hasSession() is true) and the
|
|
4086
|
+
// process would never actually restart. destroySession() tears the session
|
|
4087
|
+
// down so the respawn starts a fresh CLI. (PTY has no destroySession, so
|
|
4088
|
+
// the ?. no-ops and killCli()'s kill() does the teardown.)
|
|
4017
4089
|
backend?.destroySession?.();
|
|
4018
4090
|
killCli();
|
|
4019
4091
|
awaitingFirstPrompt = true;
|