@co0ontty/wand 4.38.0 → 4.39.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/build-info.json +3 -3
- package/dist/cli.js +6 -0
- package/dist/process-manager.d.ts +12 -4
- package/dist/process-manager.js +314 -258
- package/dist/server-session-routes.js +9 -9
- package/dist/server.d.ts +2 -0
- package/dist/server.js +10 -6
- package/dist/session-transport.d.ts +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.js +31 -6
- package/dist/terminal-daemon-client.d.ts +29 -0
- package/dist/terminal-daemon-client.js +406 -0
- package/dist/terminal-daemon-protocol.d.ts +45 -0
- package/dist/terminal-daemon-protocol.js +31 -0
- package/dist/terminal-daemon-server.d.ts +2 -0
- package/dist/terminal-daemon-server.js +349 -0
- package/dist/terminal-host.d.ts +79 -0
- package/dist/terminal-host.js +123 -0
- package/dist/tui/commands.js +220 -33
- package/dist/types.d.ts +4 -0
- package/dist/web-ui/content/scripts.js +65 -65
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +3 -3
- package/package.json +1 -1
package/dist/process-manager.js
CHANGED
|
@@ -5,13 +5,11 @@ import path from "node:path";
|
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import { DatabaseSync } from "node:sqlite";
|
|
8
|
-
import pty from "node-pty";
|
|
9
8
|
import { SessionLogger } from "./session-logger.js";
|
|
10
9
|
import { ClaudePtyBridge } from "./claude-pty-bridge.js";
|
|
11
10
|
import { truncateMessagesForTransport } from "./message-truncator.js";
|
|
12
11
|
import { appendWindow, hasExplicitConfirmSyntax, hasPermissionActionContext, normalizePromptText, PTY_OUTPUT_MAX_SIZE } from "./pty-text-utils.js";
|
|
13
12
|
import { buildChildEnv, isRunningAsRoot } from "./env-utils.js";
|
|
14
|
-
import { ensureNodePtyHelperExecutable } from "./ensure-node-pty-helper.js";
|
|
15
13
|
import { buildLanguageDirective, buildManagedAutonomyDirective } from "./language-prompt.js";
|
|
16
14
|
import { prepareSessionWorktree } from "./git-worktree.js";
|
|
17
15
|
import { getProviderCommandSessionId, getProviderResumeCommandSessionId } from "./resume-policy.js";
|
|
@@ -21,7 +19,8 @@ import { getErrorMessage } from "./error-utils.js";
|
|
|
21
19
|
import { resolveSystemAiContext } from "./session-ai-context.js";
|
|
22
20
|
import { resolveSessionCwd } from "./session-cwd.js";
|
|
23
21
|
import { PtyTerminalState } from "./pty-terminal-state.js";
|
|
24
|
-
import { buildPtyShellLaunchPlan } from "./pty-shell-launch.js";
|
|
22
|
+
import { buildPtyShellLaunchPlan, PtyCliExitMarker } from "./pty-shell-launch.js";
|
|
23
|
+
import { InProcessTerminalHost, } from "./terminal-host.js";
|
|
25
24
|
import { ProviderHistoryScanner, } from "./provider-history-scanner.js";
|
|
26
25
|
function resolveProviderFromCommand(command) {
|
|
27
26
|
if (/^(?:claude|npx\s+claude|[^\s]+\/claude)(?:\s|$)/.test(command.trim()))
|
|
@@ -464,6 +463,17 @@ function recoverClaudeSessionIdFromHistory(snapshot) {
|
|
|
464
463
|
function snapshotMessages(record) {
|
|
465
464
|
return record.ptyBridge?.getMessages() ?? record.messages;
|
|
466
465
|
}
|
|
466
|
+
function restoreTerminalState(state) {
|
|
467
|
+
const snapshot = state.terminalSnapshot;
|
|
468
|
+
const terminal = new PtyTerminalState(snapshot?.cols ?? state.cols, snapshot?.rows ?? state.rows, snapshot?.data ?? state.output);
|
|
469
|
+
for (const operation of snapshot?.pending ?? []) {
|
|
470
|
+
if (operation.type === "data")
|
|
471
|
+
terminal.write(operation.data);
|
|
472
|
+
else
|
|
473
|
+
terminal.resize(operation.cols, operation.rows);
|
|
474
|
+
}
|
|
475
|
+
return terminal;
|
|
476
|
+
}
|
|
467
477
|
const MAX_SESSIONS = 200;
|
|
468
478
|
const ARCHIVE_AFTER_MS = 1000 * 60 * 60 * 24;
|
|
469
479
|
const CONFIRM_WINDOW_SIZE = 800;
|
|
@@ -535,10 +545,12 @@ export class ProcessManager extends EventEmitter {
|
|
|
535
545
|
orphanRecoveredCount = 0;
|
|
536
546
|
topicCoordinator = new SessionTopicCoordinator();
|
|
537
547
|
disposed = false;
|
|
538
|
-
|
|
548
|
+
terminalHost;
|
|
549
|
+
constructor(config, storage, configDir, terminalHost) {
|
|
539
550
|
super();
|
|
540
551
|
this.config = config;
|
|
541
552
|
this.storage = storage;
|
|
553
|
+
this.terminalHost = terminalHost ?? new InProcessTerminalHost();
|
|
542
554
|
this.logger = new SessionLogger(configDir || path.join(process.env.HOME || process.cwd(), ".wand"), config.shortcutLogMaxBytes);
|
|
543
555
|
let startupCodexHistory = null;
|
|
544
556
|
const getStartupCodexHistory = () => {
|
|
@@ -578,116 +590,64 @@ export class ProcessManager extends EventEmitter {
|
|
|
578
590
|
})?.id ?? null
|
|
579
591
|
: null;
|
|
580
592
|
const restoredSessionId = resumeCommandSessionId ?? snapshot.claudeSessionId ?? sessionIdFromHistory;
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
593
|
+
const attached = snapshot.status === "running"
|
|
594
|
+
? this.terminalHost.attach(snapshot.id, snapshot.ptyOutputSeq ?? 0)
|
|
595
|
+
: null;
|
|
596
|
+
const live = attached?.process && attached.state.status === "running" ? attached : null;
|
|
597
|
+
const daemonEnded = attached && attached.state.status === "exited" ? attached : null;
|
|
598
|
+
const recoveredMessages = recoverMessagesFromSnapshot(snapshot);
|
|
599
|
+
const updated = live
|
|
600
|
+
? {
|
|
587
601
|
...snapshot,
|
|
588
602
|
sessionSource: snapshot.sessionSource ?? "interactive",
|
|
589
|
-
status: "
|
|
590
|
-
|
|
603
|
+
status: "running",
|
|
604
|
+
exitCode: null,
|
|
605
|
+
endedAt: null,
|
|
606
|
+
output: live.state.output,
|
|
607
|
+
ptyOutputSeq: live.state.seq,
|
|
608
|
+
ptyLaunchMarkerToken: live.state.launchMarkerToken,
|
|
609
|
+
ptyCols: live.state.cols,
|
|
610
|
+
ptyRows: live.state.rows,
|
|
591
611
|
claudeSessionId: restoredSessionId ?? null,
|
|
592
|
-
messages: recoveredMessages.length > 0 ? recoveredMessages : snapshot.messages,
|
|
593
|
-
};
|
|
594
|
-
this.storage.saveSession(updated);
|
|
595
|
-
if (restoredSessionId && restoredSessionId !== snapshot.claudeSessionId) {
|
|
596
|
-
const label = isCodexCmd ? "Codex thread" : isOpenCodeCmd ? "OpenCode session" : "Claude session";
|
|
597
|
-
process.stderr.write(`[wand] Recovered ${label} ID for orphan PTY ${snapshot.id}: ${restoredSessionId}\n`);
|
|
598
612
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
lastEscalationResult: snapshot.lastEscalationResult ?? null,
|
|
619
|
-
autonomyPolicy: snapshot.autonomyPolicy ?? this.defaultAutonomyPolicy(snapshot.mode),
|
|
620
|
-
approvalPolicy: snapshot.approvalPolicy ?? "ask-every-time",
|
|
621
|
-
allowedScopes: snapshot.allowedScopes ?? [],
|
|
622
|
-
rememberedEscalationScopes: new Set(),
|
|
623
|
-
rememberedEscalationTargets: new Set(),
|
|
624
|
-
storedOutput: snapshot.output,
|
|
625
|
-
messages: snapshot.messages ?? [],
|
|
626
|
-
childProcess: null,
|
|
627
|
-
ptyBridge: null,
|
|
628
|
-
knownClaudeTaskIds: undefined,
|
|
629
|
-
claudeTaskDiscoveryTimer: null,
|
|
630
|
-
knownClaudeProjectMtimes: isClaudeCmd ? listClaudeProjectSessionMtimes(updated.cwd) : undefined,
|
|
631
|
-
knownCodexSessionMtimes: isCodexCmd ? listCodexSessionMtimes(getStartupCodexHistory()) : undefined,
|
|
632
|
-
codexSessionDiscoveryTimer: null,
|
|
633
|
-
knownOpenCodeSessionMtimes: isOpenCodeCmd ? listOpenCodeSessionMtimes() : undefined,
|
|
634
|
-
openCodeSessionDiscoveryTimer: null,
|
|
635
|
-
claudeSessionId: restoredSessionId ?? updated.claudeSessionId,
|
|
636
|
-
approvalStats: snapshot.approvalStats ?? { tool: 0, command: 0, file: 0, total: 0 },
|
|
637
|
-
titleGenerating: false,
|
|
638
|
-
ptyCols: snapshot.ptyCols ?? 120,
|
|
639
|
-
ptyRows: snapshot.ptyRows ?? 36,
|
|
640
|
-
});
|
|
641
|
-
this.orphanRecoveredCount += 1;
|
|
642
|
-
}
|
|
643
|
-
else {
|
|
644
|
-
const updated = restoredSessionId && restoredSessionId !== snapshot.claudeSessionId
|
|
645
|
-
? { ...snapshot, claudeSessionId: restoredSessionId }
|
|
646
|
-
: snapshot;
|
|
647
|
-
if (updated !== snapshot) {
|
|
613
|
+
: snapshot.status === "running"
|
|
614
|
+
? {
|
|
615
|
+
...snapshot,
|
|
616
|
+
sessionSource: snapshot.sessionSource ?? "interactive",
|
|
617
|
+
status: daemonEnded?.state.exitCode === 0 ? "exited" : daemonEnded ? "failed" : "exited",
|
|
618
|
+
exitCode: daemonEnded?.state.exitCode ?? null,
|
|
619
|
+
endedAt: orphanEndedAt,
|
|
620
|
+
output: daemonEnded?.state.output ?? snapshot.output,
|
|
621
|
+
ptyOutputSeq: daemonEnded?.state.seq ?? snapshot.ptyOutputSeq,
|
|
622
|
+
claudeSessionId: restoredSessionId ?? null,
|
|
623
|
+
messages: recoveredMessages.length > 0 ? recoveredMessages : snapshot.messages,
|
|
624
|
+
}
|
|
625
|
+
: restoredSessionId && restoredSessionId !== snapshot.claudeSessionId
|
|
626
|
+
? { ...snapshot, claudeSessionId: restoredSessionId }
|
|
627
|
+
: snapshot;
|
|
628
|
+
if (updated !== snapshot || live) {
|
|
629
|
+
if (snapshot.status === "running")
|
|
630
|
+
this.storage.saveSession(updated);
|
|
631
|
+
else
|
|
648
632
|
this.storage.saveSessionMetadata(updated);
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
pendingEscalation: snapshot.pendingEscalation ?? null,
|
|
668
|
-
lastEscalationResult: snapshot.lastEscalationResult ?? null,
|
|
669
|
-
autonomyPolicy: snapshot.autonomyPolicy ?? this.defaultAutonomyPolicy(snapshot.mode),
|
|
670
|
-
approvalPolicy: snapshot.approvalPolicy ?? "ask-every-time",
|
|
671
|
-
allowedScopes: snapshot.allowedScopes ?? [],
|
|
672
|
-
rememberedEscalationScopes: new Set(),
|
|
673
|
-
rememberedEscalationTargets: new Set(),
|
|
674
|
-
storedOutput: snapshot.output,
|
|
675
|
-
messages: snapshot.messages ?? [],
|
|
676
|
-
childProcess: null,
|
|
677
|
-
ptyBridge: null,
|
|
678
|
-
knownClaudeTaskIds: undefined,
|
|
679
|
-
claudeTaskDiscoveryTimer: null,
|
|
680
|
-
knownClaudeProjectMtimes: isClaudeCmd ? listClaudeProjectSessionMtimes(updated.cwd) : undefined,
|
|
681
|
-
knownCodexSessionMtimes: isCodexCmd ? listCodexSessionMtimes(getStartupCodexHistory()) : undefined,
|
|
682
|
-
codexSessionDiscoveryTimer: null,
|
|
683
|
-
knownOpenCodeSessionMtimes: isOpenCodeCmd ? listOpenCodeSessionMtimes() : undefined,
|
|
684
|
-
openCodeSessionDiscoveryTimer: null,
|
|
685
|
-
claudeSessionId: restoredSessionId ?? updated.claudeSessionId,
|
|
686
|
-
approvalStats: snapshot.approvalStats ?? { tool: 0, command: 0, file: 0, total: 0 },
|
|
687
|
-
titleGenerating: false,
|
|
688
|
-
ptyCols: snapshot.ptyCols ?? 120,
|
|
689
|
-
ptyRows: snapshot.ptyRows ?? 36,
|
|
690
|
-
});
|
|
633
|
+
}
|
|
634
|
+
if (restoredSessionId && restoredSessionId !== snapshot.claudeSessionId) {
|
|
635
|
+
const label = isCodexCmd ? "Codex thread" : isOpenCodeCmd ? "OpenCode session" : "Claude session";
|
|
636
|
+
process.stderr.write(`[wand] Recovered ${label} ID for ${live ? "reattached" : "saved"} PTY ${snapshot.id}: ${restoredSessionId}\n`);
|
|
637
|
+
}
|
|
638
|
+
const record = this.makeRestoredRecord(updated, provider, live?.process ?? null, live?.state ?? null, {
|
|
639
|
+
isClaudeCmd,
|
|
640
|
+
isCodexCmd,
|
|
641
|
+
isOpenCodeCmd,
|
|
642
|
+
startupCodexHistory: getStartupCodexHistory,
|
|
643
|
+
});
|
|
644
|
+
this.sessions.set(snapshot.id, record);
|
|
645
|
+
if (live?.process) {
|
|
646
|
+
this.bindRestoredTerminal(record, live);
|
|
647
|
+
process.stderr.write(`[wand] Reattached live terminal ${snapshot.id} (pid ${live.process.pid}).\n`);
|
|
648
|
+
}
|
|
649
|
+
else if (snapshot.status === "running") {
|
|
650
|
+
this.orphanRecoveredCount += 1;
|
|
691
651
|
}
|
|
692
652
|
}
|
|
693
653
|
this.archiveExpiredSessions();
|
|
@@ -701,6 +661,202 @@ export class ProcessManager extends EventEmitter {
|
|
|
701
661
|
}, 60 * 1000);
|
|
702
662
|
this.archiveTimer.unref?.();
|
|
703
663
|
}
|
|
664
|
+
makeRestoredRecord(snapshot, provider, ptyProcess, terminal, history) {
|
|
665
|
+
return {
|
|
666
|
+
...snapshot,
|
|
667
|
+
sessionSource: snapshot.sessionSource ?? "interactive",
|
|
668
|
+
provider,
|
|
669
|
+
processId: ptyProcess?.pid ?? null,
|
|
670
|
+
ptyProcess,
|
|
671
|
+
terminalState: terminal ? restoreTerminalState(terminal) : null,
|
|
672
|
+
providerShellMarker: terminal?.launchMarkerToken ? new PtyCliExitMarker(terminal.launchMarkerToken) : null,
|
|
673
|
+
providerCliActive: ptyProcess ? snapshot.providerCliActive ?? provider !== undefined : false,
|
|
674
|
+
providerCliExitCode: snapshot.providerCliExitCode ?? null,
|
|
675
|
+
stopRequested: false,
|
|
676
|
+
confirmWindow: "",
|
|
677
|
+
ptyPermissionBlocked: false,
|
|
678
|
+
lastAutoConfirmAt: 0,
|
|
679
|
+
autoApprovePermissions: snapshot.autoApprovePermissions
|
|
680
|
+
?? this.shouldAutoApprovePermissions(snapshot.command, snapshot.mode, provider),
|
|
681
|
+
pendingEscalation: snapshot.pendingEscalation ?? null,
|
|
682
|
+
lastEscalationResult: snapshot.lastEscalationResult ?? null,
|
|
683
|
+
autonomyPolicy: snapshot.autonomyPolicy ?? this.defaultAutonomyPolicy(snapshot.mode),
|
|
684
|
+
approvalPolicy: snapshot.approvalPolicy ?? "ask-every-time",
|
|
685
|
+
allowedScopes: snapshot.allowedScopes ?? [],
|
|
686
|
+
rememberedEscalationScopes: new Set(),
|
|
687
|
+
rememberedEscalationTargets: new Set(),
|
|
688
|
+
storedOutput: terminal?.output ?? snapshot.output,
|
|
689
|
+
output: terminal?.output ?? snapshot.output,
|
|
690
|
+
messages: snapshot.messages ?? [],
|
|
691
|
+
childProcess: null,
|
|
692
|
+
ptyBridge: null,
|
|
693
|
+
knownClaudeTaskIds: undefined,
|
|
694
|
+
claudeTaskDiscoveryTimer: null,
|
|
695
|
+
knownClaudeProjectMtimes: history.isClaudeCmd ? listClaudeProjectSessionMtimes(snapshot.cwd) : undefined,
|
|
696
|
+
knownCodexSessionMtimes: history.isCodexCmd ? listCodexSessionMtimes(history.startupCodexHistory()) : undefined,
|
|
697
|
+
codexSessionDiscoveryTimer: null,
|
|
698
|
+
knownOpenCodeSessionMtimes: history.isOpenCodeCmd ? listOpenCodeSessionMtimes() : undefined,
|
|
699
|
+
openCodeSessionDiscoveryTimer: null,
|
|
700
|
+
claudeSessionId: snapshot.claudeSessionId ?? null,
|
|
701
|
+
approvalStats: snapshot.approvalStats ?? { tool: 0, command: 0, file: 0, total: 0 },
|
|
702
|
+
titleGenerating: false,
|
|
703
|
+
ptyCols: terminal?.cols ?? snapshot.ptyCols ?? 120,
|
|
704
|
+
ptyRows: terminal?.rows ?? snapshot.ptyRows ?? 36,
|
|
705
|
+
ptyOutputSeq: terminal?.seq ?? snapshot.ptyOutputSeq ?? 0,
|
|
706
|
+
ptyLaunchMarkerToken: terminal?.launchMarkerToken ?? snapshot.ptyLaunchMarkerToken ?? null,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
bindRestoredTerminal(record, attached) {
|
|
710
|
+
const child = attached.process;
|
|
711
|
+
if (!child)
|
|
712
|
+
return;
|
|
713
|
+
this.initializeClaudeBridge(record, record.output);
|
|
714
|
+
this.bindTerminalProcess(record, child);
|
|
715
|
+
// The daemon snapshot already contains replayed bytes. Consume only the
|
|
716
|
+
// private provider boundary marker here so semantic CLI state catches up
|
|
717
|
+
// without duplicating terminal output or bridge transcripts.
|
|
718
|
+
for (const event of attached.replay) {
|
|
719
|
+
const boundary = record.providerShellMarker?.consume(event.data);
|
|
720
|
+
record.ptyOutputSeq = Math.max(record.ptyOutputSeq ?? 0, event.seq);
|
|
721
|
+
if (boundary?.exitCode !== null && boundary?.exitCode !== undefined) {
|
|
722
|
+
this.finishProviderCli(record, boundary.exitCode);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
initializeClaudeBridge(record, initialOutput) {
|
|
727
|
+
if (record.provider !== "claude" || !record.providerCliActive)
|
|
728
|
+
return;
|
|
729
|
+
record.ptyBridge?.removeAllListeners();
|
|
730
|
+
record.ptyBridge = new ClaudePtyBridge({
|
|
731
|
+
sessionId: record.id,
|
|
732
|
+
isClaudeCommand: true,
|
|
733
|
+
autoApprove: record.autoApprovePermissions,
|
|
734
|
+
initialMessages: record.messages,
|
|
735
|
+
initialOutput,
|
|
736
|
+
});
|
|
737
|
+
record.ptyBridge.on("event", (event) => {
|
|
738
|
+
if (this.sessions.get(record.id) !== record)
|
|
739
|
+
return;
|
|
740
|
+
this.handleBridgeEvent(record, event);
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
bindTerminalProcess(record, child, onVisibleChunk) {
|
|
744
|
+
child.onExit(({ exitCode }) => this.handleTerminalExit(record, child, exitCode));
|
|
745
|
+
child.onData((event) => this.handleTerminalData(record, child, event, onVisibleChunk));
|
|
746
|
+
if (record.ptyBridge) {
|
|
747
|
+
record.ptyBridge.setPtyWrite((input) => {
|
|
748
|
+
if (this.sessions.get(record.id) !== record || record.ptyProcess !== child)
|
|
749
|
+
return;
|
|
750
|
+
child.write(input);
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
handleTerminalExit(record, child, exitCode) {
|
|
755
|
+
const current = this.sessions.get(record.id);
|
|
756
|
+
// A stopped session can be resumed under the same public id before the old
|
|
757
|
+
// PTY emits its asynchronous exit event. Ignore that stale incarnation.
|
|
758
|
+
if (current !== record || current.ptyProcess !== child)
|
|
759
|
+
return;
|
|
760
|
+
if (current.claudeTaskDiscoveryTimer) {
|
|
761
|
+
clearTimeout(current.claudeTaskDiscoveryTimer);
|
|
762
|
+
current.claudeTaskDiscoveryTimer = null;
|
|
763
|
+
}
|
|
764
|
+
if (current.codexSessionDiscoveryTimer) {
|
|
765
|
+
clearTimeout(current.codexSessionDiscoveryTimer);
|
|
766
|
+
current.codexSessionDiscoveryTimer = null;
|
|
767
|
+
}
|
|
768
|
+
if (current.openCodeSessionDiscoveryTimer) {
|
|
769
|
+
clearTimeout(current.openCodeSessionDiscoveryTimer);
|
|
770
|
+
current.openCodeSessionDiscoveryTimer = null;
|
|
771
|
+
}
|
|
772
|
+
if (current.initialInputTimer) {
|
|
773
|
+
clearTimeout(current.initialInputTimer);
|
|
774
|
+
current.initialInputTimer = null;
|
|
775
|
+
}
|
|
776
|
+
if (current.ptyBridge) {
|
|
777
|
+
current.ptyBridge.onExit(exitCode);
|
|
778
|
+
current.messages = current.ptyBridge.getMessages();
|
|
779
|
+
current.ptyBridge.removeAllListeners();
|
|
780
|
+
current.ptyBridge = null;
|
|
781
|
+
}
|
|
782
|
+
current.providerShellMarker = null;
|
|
783
|
+
current.ptyLaunchMarkerToken = null;
|
|
784
|
+
if (current.providerCliActive) {
|
|
785
|
+
current.providerCliActive = false;
|
|
786
|
+
current.providerCliExitCode = exitCode;
|
|
787
|
+
}
|
|
788
|
+
current.pendingEscalation = null;
|
|
789
|
+
current.ptyPermissionBlocked = false;
|
|
790
|
+
this.captureClaudeSessionId(current, { allowTimeWindowFallback: true });
|
|
791
|
+
this.captureCodexSessionId(current, { allowTimeWindowFallback: true });
|
|
792
|
+
this.captureOpenCodeSessionId(current, { allowTimeWindowFallback: true });
|
|
793
|
+
current.status = current.stopRequested ? "stopped" : exitCode === 0 ? "exited" : "failed";
|
|
794
|
+
current.exitCode = current.stopRequested ? null : exitCode;
|
|
795
|
+
current.endedAt = new Date().toISOString();
|
|
796
|
+
current.ptyProcess = null;
|
|
797
|
+
this.flushPersist(current, true);
|
|
798
|
+
this.emitEvent({ type: "ended", sessionId: record.id, data: this.snapshot(current) });
|
|
799
|
+
}
|
|
800
|
+
handleTerminalData(record, child, event, onVisibleChunk) {
|
|
801
|
+
const current = this.sessions.get(record.id);
|
|
802
|
+
if (current !== record || current.ptyProcess !== child)
|
|
803
|
+
return;
|
|
804
|
+
current.ptyOutputSeq = Math.max(current.ptyOutputSeq ?? 0, event.seq);
|
|
805
|
+
const boundary = current.providerShellMarker?.consume(event.data) ?? { data: event.data, exitCode: null };
|
|
806
|
+
const chunk = boundary.data;
|
|
807
|
+
if (chunk)
|
|
808
|
+
current.terminalState?.write(chunk);
|
|
809
|
+
if (chunk && current.ptyBridge) {
|
|
810
|
+
current.ptyBridge.processChunk(chunk);
|
|
811
|
+
current.output = current.ptyBridge.getRawOutput();
|
|
812
|
+
}
|
|
813
|
+
else if (chunk) {
|
|
814
|
+
current.output = appendWindow(current.output, chunk, PTY_OUTPUT_MAX_SIZE);
|
|
815
|
+
}
|
|
816
|
+
if (chunk)
|
|
817
|
+
this.logger.appendPtyOutput(record.id, chunk);
|
|
818
|
+
if (chunk && !current.ptyBridge) {
|
|
819
|
+
this.emitEvent({
|
|
820
|
+
type: "output",
|
|
821
|
+
sessionId: record.id,
|
|
822
|
+
data: {
|
|
823
|
+
incremental: true,
|
|
824
|
+
chunk,
|
|
825
|
+
permissionBlocked: this.isPermissionBlocked(current),
|
|
826
|
+
},
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
const bridgeSessionId = current.ptyBridge?.getClaudeSessionId();
|
|
830
|
+
if (bridgeSessionId && bridgeSessionId !== current.claudeSessionId) {
|
|
831
|
+
current.claudeSessionId = bridgeSessionId;
|
|
832
|
+
this.markDirty(current.id, { metadata: true });
|
|
833
|
+
this.providerHistory.invalidate("claude");
|
|
834
|
+
process.stderr.write(`[wand] Captured Claude session ID: ${bridgeSessionId}\n`);
|
|
835
|
+
}
|
|
836
|
+
if (current.providerCliActive && boundary.exitCode === null && !current.claudeSessionId && current.knownClaudeTaskIds) {
|
|
837
|
+
current.messages = snapshotMessages(current);
|
|
838
|
+
const discoveredTaskId = getLatestClaudeProjectSessionId({
|
|
839
|
+
cwd: current.cwd,
|
|
840
|
+
startedAt: current.startedAt,
|
|
841
|
+
knownClaudeProjectMtimes: current.knownClaudeProjectMtimes,
|
|
842
|
+
messages: current.messages,
|
|
843
|
+
});
|
|
844
|
+
if (discoveredTaskId) {
|
|
845
|
+
current.claudeSessionId = discoveredTaskId;
|
|
846
|
+
this.markDirty(current.id, { metadata: true });
|
|
847
|
+
current.knownClaudeTaskIds.add(discoveredTaskId);
|
|
848
|
+
process.stderr.write(`[wand] Captured Claude project session ID: ${discoveredTaskId}\n`);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
if (chunk && current.providerCliActive && current.autoApprovePermissions && !current.ptyBridge && current.provider === "claude") {
|
|
852
|
+
this.autoConfirmWithRecord(current, chunk, child);
|
|
853
|
+
}
|
|
854
|
+
if (chunk)
|
|
855
|
+
onVisibleChunk?.(chunk);
|
|
856
|
+
if (boundary.exitCode !== null)
|
|
857
|
+
this.finishProviderCli(current, boundary.exitCode);
|
|
858
|
+
this.schedulePersist(current);
|
|
859
|
+
}
|
|
704
860
|
on(_event, listener) {
|
|
705
861
|
return super.on("process", listener);
|
|
706
862
|
}
|
|
@@ -726,8 +882,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
726
882
|
if (record.ptyBridge) {
|
|
727
883
|
record.messages = record.ptyBridge.getMessages();
|
|
728
884
|
}
|
|
729
|
-
this.cleanupRecord(record);
|
|
730
|
-
if (wasRunning) {
|
|
885
|
+
this.cleanupRecord(record, !this.terminalHost.persistent);
|
|
886
|
+
if (wasRunning && !this.terminalHost.persistent) {
|
|
731
887
|
record.stopRequested = true;
|
|
732
888
|
record.status = "stopped";
|
|
733
889
|
record.exitCode = null;
|
|
@@ -745,6 +901,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
745
901
|
this.topicCoordinator.clear();
|
|
746
902
|
this.removeAllListeners("process");
|
|
747
903
|
this.logger.dispose();
|
|
904
|
+
this.terminalHost.disconnect();
|
|
748
905
|
}
|
|
749
906
|
emitEvent(event) {
|
|
750
907
|
if (this.disposed)
|
|
@@ -787,6 +944,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
787
944
|
}
|
|
788
945
|
}
|
|
789
946
|
this.sessions.delete(id);
|
|
947
|
+
this.terminalHost.forget(id);
|
|
790
948
|
this.lastPersistedMessageState.delete(id);
|
|
791
949
|
this.dirtySessions.delete(id);
|
|
792
950
|
this.storage.deleteSession(id);
|
|
@@ -795,7 +953,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
795
953
|
this.providerHistory.invalidate();
|
|
796
954
|
}
|
|
797
955
|
}
|
|
798
|
-
start(command, cwd, mode, initialInput, opts) {
|
|
956
|
+
async start(command, cwd, mode, initialInput, opts) {
|
|
799
957
|
if (this.disposed)
|
|
800
958
|
throw new Error("ProcessManager has been disposed.");
|
|
801
959
|
if (!opts?.interactiveShell)
|
|
@@ -825,6 +983,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
825
983
|
inheritedSessionSource = stored?.sessionSource;
|
|
826
984
|
inheritedAutomationId = stored?.automationId;
|
|
827
985
|
}
|
|
986
|
+
this.terminalHost.forget(id);
|
|
828
987
|
}
|
|
829
988
|
const worktreeSetup = opts?.worktreeEnabled
|
|
830
989
|
? prepareSessionWorktree({ cwd: baseCwd, sessionId: id })
|
|
@@ -924,33 +1083,21 @@ export class ProcessManager extends EventEmitter {
|
|
|
924
1083
|
thinkingEffort: initialThinkingEffort,
|
|
925
1084
|
ptyCols: opts?.cols !== undefined ? clampDimension(opts.cols, 20, 1000) : 120,
|
|
926
1085
|
ptyRows: opts?.rows !== undefined ? clampDimension(opts.rows, 5, 500) : 36,
|
|
1086
|
+
ptyOutputSeq: 0,
|
|
1087
|
+
ptyLaunchMarkerToken: launchPlan.cliExitMarker?.token ?? null,
|
|
927
1088
|
};
|
|
928
|
-
if (isClaudeProvider) {
|
|
929
|
-
record.ptyBridge = new ClaudePtyBridge({
|
|
930
|
-
sessionId: id,
|
|
931
|
-
isClaudeCommand: true,
|
|
932
|
-
autoApprove: record.autoApprovePermissions,
|
|
933
|
-
initialMessages: priorMessages,
|
|
934
|
-
});
|
|
935
|
-
record.ptyBridge.on("event", (event) => {
|
|
936
|
-
if (this.sessions.get(id) !== record)
|
|
937
|
-
return;
|
|
938
|
-
this.handleBridgeEvent(record, event);
|
|
939
|
-
});
|
|
940
|
-
}
|
|
941
1089
|
this.sessions.set(id, record);
|
|
942
1090
|
this.persist(record, { forceFullSave: true });
|
|
943
1091
|
if (initialClaudeSessionId && (provider === "claude" || provider === "codex")) {
|
|
944
1092
|
this.providerHistory.invalidate(provider);
|
|
945
1093
|
}
|
|
946
1094
|
this.cleanupOldSessions();
|
|
947
|
-
// Self-heal node-pty's spawn-helper +x bit before every spawn: a self-update
|
|
948
|
-
// can re-drop it after this server already ran its startup chmod, which would
|
|
949
|
-
// otherwise make every PTY launch throw "posix_spawnp failed." until restart.
|
|
950
|
-
ensureNodePtyHelperExecutable();
|
|
951
1095
|
let child;
|
|
952
1096
|
try {
|
|
953
|
-
|
|
1097
|
+
const attached = await this.terminalHost.createOrAttach({
|
|
1098
|
+
sessionId: id,
|
|
1099
|
+
file: this.config.shell,
|
|
1100
|
+
args: launchPlan.shellArgs,
|
|
954
1101
|
cwd: resolvedCwd,
|
|
955
1102
|
env: buildChildEnv(this.config.inheritEnv !== false, {
|
|
956
1103
|
WAND_MODE: effectiveMode,
|
|
@@ -961,11 +1108,25 @@ export class ProcessManager extends EventEmitter {
|
|
|
961
1108
|
// 使用 record 上由前端协商好的真实尺寸,避免"先 120 列、几百毫秒后再 resize"
|
|
962
1109
|
// 期间 provider TUI 用错列宽渲染出 \x1b[120G 这类绝对列定位序列。
|
|
963
1110
|
cols: record.ptyCols,
|
|
964
|
-
rows: record.ptyRows
|
|
965
|
-
|
|
1111
|
+
rows: record.ptyRows,
|
|
1112
|
+
launchMarkerToken: launchPlan.cliExitMarker?.token,
|
|
1113
|
+
}, record.ptyOutputSeq);
|
|
1114
|
+
if (!attached.process || attached.state.status !== "running") {
|
|
1115
|
+
throw new Error("Terminal host did not return a running process");
|
|
1116
|
+
}
|
|
1117
|
+
child = attached.process;
|
|
1118
|
+
record.output = attached.state.output;
|
|
1119
|
+
record.storedOutput = attached.state.output;
|
|
1120
|
+
record.ptyOutputSeq = attached.state.seq;
|
|
1121
|
+
record.ptyLaunchMarkerToken = attached.state.launchMarkerToken;
|
|
1122
|
+
record.ptyCols = attached.state.cols;
|
|
1123
|
+
record.ptyRows = attached.state.rows;
|
|
1124
|
+
record.terminalState?.dispose();
|
|
1125
|
+
record.terminalState = restoreTerminalState(attached.state);
|
|
1126
|
+
this.initializeClaudeBridge(record, attached.state.output);
|
|
966
1127
|
}
|
|
967
1128
|
catch (err) {
|
|
968
|
-
console.error("[ProcessManager]
|
|
1129
|
+
console.error("[ProcessManager] terminal spawn failed", { sessionId: id, error: String(err) });
|
|
969
1130
|
record.status = "failed";
|
|
970
1131
|
record.exitCode = -1;
|
|
971
1132
|
record.endedAt = new Date().toISOString();
|
|
@@ -976,60 +1137,6 @@ export class ProcessManager extends EventEmitter {
|
|
|
976
1137
|
record.processId = child.pid;
|
|
977
1138
|
record.ptyProcess = child;
|
|
978
1139
|
record.status = "running";
|
|
979
|
-
child.onExit(({ exitCode }) => {
|
|
980
|
-
const current = this.sessions.get(id);
|
|
981
|
-
// A stopped session can be resumed under the same public id before the
|
|
982
|
-
// old PTY has emitted its asynchronous exit event. Never let that stale
|
|
983
|
-
// callback finalize or clear the replacement run.
|
|
984
|
-
if (current !== record || current.ptyProcess !== child)
|
|
985
|
-
return;
|
|
986
|
-
if (current.claudeTaskDiscoveryTimer) {
|
|
987
|
-
clearTimeout(current.claudeTaskDiscoveryTimer);
|
|
988
|
-
current.claudeTaskDiscoveryTimer = null;
|
|
989
|
-
}
|
|
990
|
-
if (current.codexSessionDiscoveryTimer) {
|
|
991
|
-
clearTimeout(current.codexSessionDiscoveryTimer);
|
|
992
|
-
current.codexSessionDiscoveryTimer = null;
|
|
993
|
-
}
|
|
994
|
-
if (current.openCodeSessionDiscoveryTimer) {
|
|
995
|
-
clearTimeout(current.openCodeSessionDiscoveryTimer);
|
|
996
|
-
current.openCodeSessionDiscoveryTimer = null;
|
|
997
|
-
}
|
|
998
|
-
if (current.initialInputTimer) {
|
|
999
|
-
clearTimeout(current.initialInputTimer);
|
|
1000
|
-
current.initialInputTimer = null;
|
|
1001
|
-
}
|
|
1002
|
-
if (current.ptyBridge) {
|
|
1003
|
-
current.ptyBridge.onExit(exitCode);
|
|
1004
|
-
current.ptyBridge.removeAllListeners();
|
|
1005
|
-
}
|
|
1006
|
-
current.providerShellMarker = null;
|
|
1007
|
-
if (current.providerCliActive) {
|
|
1008
|
-
current.providerCliActive = false;
|
|
1009
|
-
current.providerCliExitCode = exitCode;
|
|
1010
|
-
}
|
|
1011
|
-
current.pendingEscalation = null;
|
|
1012
|
-
current.ptyPermissionBlocked = false;
|
|
1013
|
-
this.captureClaudeSessionId(current, { allowTimeWindowFallback: true });
|
|
1014
|
-
this.captureCodexSessionId(current, { allowTimeWindowFallback: true });
|
|
1015
|
-
this.captureOpenCodeSessionId(current, { allowTimeWindowFallback: true });
|
|
1016
|
-
current.status = current.stopRequested ? "stopped" : exitCode === 0 ? "exited" : "failed";
|
|
1017
|
-
current.exitCode = current.stopRequested ? null : exitCode;
|
|
1018
|
-
current.endedAt = new Date().toISOString();
|
|
1019
|
-
current.ptyProcess = null;
|
|
1020
|
-
this.flushPersist(current, true);
|
|
1021
|
-
this.emitEvent({ type: "ended", sessionId: id, data: this.snapshot(current) });
|
|
1022
|
-
});
|
|
1023
|
-
if (record.ptyBridge) {
|
|
1024
|
-
record.ptyBridge.setPtyWrite((input) => {
|
|
1025
|
-
if (this.sessions.get(id) !== record || record.ptyProcess !== child)
|
|
1026
|
-
return;
|
|
1027
|
-
child.write(input);
|
|
1028
|
-
});
|
|
1029
|
-
}
|
|
1030
|
-
this.emitEvent({ type: "started", sessionId: id, data: this.snapshot(record) });
|
|
1031
|
-
if (initialInput)
|
|
1032
|
-
this.maybeGenerateSessionTopic(id, initialInput);
|
|
1033
1140
|
let initialInputSent = false;
|
|
1034
1141
|
const sendInitialInput = () => {
|
|
1035
1142
|
if (initialInputSent || !initialInput)
|
|
@@ -1047,70 +1154,13 @@ export class ProcessManager extends EventEmitter {
|
|
|
1047
1154
|
child.write(initialInput);
|
|
1048
1155
|
child.write("\r");
|
|
1049
1156
|
};
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
// PTYs may still drain data after kill(). A replacement session can use
|
|
1053
|
-
// the same id, so both record identity and the concrete PTY handle must
|
|
1054
|
-
// match before accepting the chunk.
|
|
1055
|
-
if (rec !== record || rec.ptyProcess !== child)
|
|
1056
|
-
return;
|
|
1057
|
-
const boundary = rec.providerShellMarker?.consume(rawChunk) ?? { data: rawChunk, exitCode: null };
|
|
1058
|
-
const chunk = boundary.data;
|
|
1059
|
-
if (chunk)
|
|
1060
|
-
rec.terminalState?.write(chunk);
|
|
1061
|
-
if (chunk && rec.ptyBridge) {
|
|
1062
|
-
rec.ptyBridge.processChunk(chunk);
|
|
1063
|
-
rec.output = rec.ptyBridge.getRawOutput();
|
|
1064
|
-
}
|
|
1065
|
-
else if (chunk) {
|
|
1066
|
-
rec.output = appendWindow(rec.output, chunk, PTY_OUTPUT_MAX_SIZE);
|
|
1067
|
-
}
|
|
1068
|
-
if (chunk)
|
|
1069
|
-
this.logger.appendPtyOutput(id, chunk);
|
|
1070
|
-
if (chunk && !rec.ptyBridge) {
|
|
1071
|
-
this.emitEvent({
|
|
1072
|
-
type: "output",
|
|
1073
|
-
sessionId: id,
|
|
1074
|
-
data: {
|
|
1075
|
-
incremental: true,
|
|
1076
|
-
chunk,
|
|
1077
|
-
permissionBlocked: this.isPermissionBlocked(rec),
|
|
1078
|
-
},
|
|
1079
|
-
});
|
|
1080
|
-
}
|
|
1081
|
-
const bridgeSessionId = rec.ptyBridge?.getClaudeSessionId();
|
|
1082
|
-
if (bridgeSessionId && bridgeSessionId !== rec.claudeSessionId) {
|
|
1083
|
-
rec.claudeSessionId = bridgeSessionId;
|
|
1084
|
-
this.markDirty(rec.id, { metadata: true });
|
|
1085
|
-
this.providerHistory.invalidate("claude");
|
|
1086
|
-
process.stderr.write(`[wand] Captured Claude session ID: ${bridgeSessionId}\n`);
|
|
1087
|
-
}
|
|
1088
|
-
if (rec.providerCliActive && boundary.exitCode === null && !rec.claudeSessionId && rec.knownClaudeTaskIds) {
|
|
1089
|
-
rec.messages = snapshotMessages(rec);
|
|
1090
|
-
const discoveredTaskId = getLatestClaudeProjectSessionId({
|
|
1091
|
-
cwd: rec.cwd,
|
|
1092
|
-
startedAt: rec.startedAt,
|
|
1093
|
-
knownClaudeProjectMtimes: rec.knownClaudeProjectMtimes,
|
|
1094
|
-
messages: rec.messages
|
|
1095
|
-
});
|
|
1096
|
-
if (discoveredTaskId) {
|
|
1097
|
-
rec.claudeSessionId = discoveredTaskId;
|
|
1098
|
-
this.markDirty(rec.id, { metadata: true });
|
|
1099
|
-
rec.knownClaudeTaskIds.add(discoveredTaskId);
|
|
1100
|
-
process.stderr.write(`[wand] Captured Claude project session ID: ${discoveredTaskId}\n`);
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
if (chunk && rec.providerCliActive && rec.autoApprovePermissions && !rec.ptyBridge && rec.provider === "claude") {
|
|
1104
|
-
this.autoConfirmWithRecord(rec, chunk, child);
|
|
1105
|
-
}
|
|
1106
|
-
if (chunk && boundary.exitCode === null && initialInput && !initialInputSent && (chunk.includes("❯") || chunk.includes("›"))) {
|
|
1157
|
+
this.bindTerminalProcess(record, child, (chunk) => {
|
|
1158
|
+
if (initialInput && !initialInputSent && (chunk.includes("❯") || chunk.includes("›")))
|
|
1107
1159
|
sendInitialInput();
|
|
1108
|
-
}
|
|
1109
|
-
if (boundary.exitCode !== null) {
|
|
1110
|
-
this.finishProviderCli(record, boundary.exitCode);
|
|
1111
|
-
}
|
|
1112
|
-
this.schedulePersist(rec);
|
|
1113
1160
|
});
|
|
1161
|
+
this.emitEvent({ type: "started", sessionId: id, data: this.snapshot(record) });
|
|
1162
|
+
if (initialInput)
|
|
1163
|
+
this.maybeGenerateSessionTopic(id, initialInput);
|
|
1114
1164
|
if (launchPlan.commandToWrite) {
|
|
1115
1165
|
child.write(launchPlan.commandToWrite);
|
|
1116
1166
|
child.write("\r");
|
|
@@ -1505,6 +1555,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1505
1555
|
record.providerCliActive = false;
|
|
1506
1556
|
record.providerCliExitCode = exitCode;
|
|
1507
1557
|
record.providerShellMarker = null;
|
|
1558
|
+
record.ptyLaunchMarkerToken = null;
|
|
1508
1559
|
if (record.claudeTaskDiscoveryTimer) {
|
|
1509
1560
|
clearTimeout(record.claudeTaskDiscoveryTimer);
|
|
1510
1561
|
record.claudeTaskDiscoveryTimer = null;
|
|
@@ -1570,6 +1621,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1570
1621
|
record.stopRequested = true;
|
|
1571
1622
|
record.providerCliActive = false;
|
|
1572
1623
|
record.providerShellMarker = null;
|
|
1624
|
+
record.ptyLaunchMarkerToken = null;
|
|
1573
1625
|
// Kill any running child process (from JSON chat turns)
|
|
1574
1626
|
if (record.childProcess) {
|
|
1575
1627
|
record.childProcess.kill();
|
|
@@ -1600,7 +1652,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1600
1652
|
this.flushPersist(record, true);
|
|
1601
1653
|
return this.snapshot(record);
|
|
1602
1654
|
}
|
|
1603
|
-
cleanupRecord(record) {
|
|
1655
|
+
cleanupRecord(record, terminateTerminal = true) {
|
|
1604
1656
|
if (record.claudeTaskDiscoveryTimer) {
|
|
1605
1657
|
clearTimeout(record.claudeTaskDiscoveryTimer);
|
|
1606
1658
|
record.claudeTaskDiscoveryTimer = null;
|
|
@@ -1623,17 +1675,18 @@ export class ProcessManager extends EventEmitter {
|
|
|
1623
1675
|
this.persistDebounceTimers.delete(record.id);
|
|
1624
1676
|
}
|
|
1625
1677
|
if (record.status === "running") {
|
|
1626
|
-
|
|
1678
|
+
if (terminateTerminal)
|
|
1679
|
+
record.stopRequested = true;
|
|
1627
1680
|
if (record.childProcess) {
|
|
1628
1681
|
const child = record.childProcess;
|
|
1629
1682
|
record.childProcess = null;
|
|
1630
1683
|
child.kill();
|
|
1631
1684
|
}
|
|
1632
|
-
if (record.ptyProcess) {
|
|
1685
|
+
if (record.ptyProcess && terminateTerminal) {
|
|
1633
1686
|
const ptyProcess = record.ptyProcess;
|
|
1634
|
-
record.ptyProcess = null;
|
|
1635
1687
|
ptyProcess.kill();
|
|
1636
1688
|
}
|
|
1689
|
+
record.ptyProcess = null;
|
|
1637
1690
|
}
|
|
1638
1691
|
if (record.ptyBridge) {
|
|
1639
1692
|
record.ptyBridge.removeAllListeners();
|
|
@@ -1692,6 +1745,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1692
1745
|
}
|
|
1693
1746
|
record.terminalState?.dispose();
|
|
1694
1747
|
record.terminalState = null;
|
|
1748
|
+
this.terminalHost.forget(id);
|
|
1695
1749
|
// Delete from persistent storage BEFORE removing from in-memory map,
|
|
1696
1750
|
// so a storage failure doesn't leave orphan records in the database.
|
|
1697
1751
|
this.storage.deleteSession(id);
|
|
@@ -1737,8 +1791,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
1737
1791
|
}
|
|
1738
1792
|
}
|
|
1739
1793
|
}
|
|
1740
|
-
runStartupCommands() {
|
|
1741
|
-
return this.config.startupCommands.map((command) => this.start(command, this.config.defaultCwd, this.config.defaultMode, undefined, { sessionSource: "startup" }));
|
|
1794
|
+
async runStartupCommands() {
|
|
1795
|
+
return Promise.all(this.config.startupCommands.map((command) => this.start(command, this.config.defaultCwd, this.config.defaultMode, undefined, { sessionSource: "startup" })));
|
|
1742
1796
|
}
|
|
1743
1797
|
snapshot(record) {
|
|
1744
1798
|
// Get messages from bridge if available, otherwise use stored messages
|
|
@@ -1788,6 +1842,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
1788
1842
|
thinkingEffort: record.thinkingEffort ?? null,
|
|
1789
1843
|
ptyCols: record.ptyCols,
|
|
1790
1844
|
ptyRows: record.ptyRows,
|
|
1845
|
+
ptyOutputSeq: record.ptyOutputSeq ?? 0,
|
|
1846
|
+
ptyLaunchMarkerToken: record.ptyLaunchMarkerToken ?? null,
|
|
1791
1847
|
};
|
|
1792
1848
|
}
|
|
1793
1849
|
/** Lightweight snapshot for list views — omits output and messages. */
|
|
@@ -1961,11 +2017,11 @@ export class ProcessManager extends EventEmitter {
|
|
|
1961
2017
|
if (dirty.metadata)
|
|
1962
2018
|
this.storage.updateSessionRuntimeMetadata(snapshot);
|
|
1963
2019
|
if (dirty.messages) {
|
|
1964
|
-
this.storage.checkpointSessionMessages(record.id, messages, snapshot.structuredState, dirty.output ? snapshot.output : undefined);
|
|
2020
|
+
this.storage.checkpointSessionMessages(record.id, messages, snapshot.structuredState, dirty.output ? snapshot.output : undefined, dirty.output ? snapshot.ptyOutputSeq : undefined);
|
|
1965
2021
|
dirty.output = false;
|
|
1966
2022
|
}
|
|
1967
2023
|
else if (dirty.output) {
|
|
1968
|
-
this.storage.checkpointSessionOutput(record.id, snapshot.output);
|
|
2024
|
+
this.storage.checkpointSessionOutput(record.id, snapshot.output, snapshot.ptyOutputSeq);
|
|
1969
2025
|
}
|
|
1970
2026
|
}
|
|
1971
2027
|
if (shouldSaveMessages)
|
|
@@ -2240,7 +2296,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
2240
2296
|
return record;
|
|
2241
2297
|
}
|
|
2242
2298
|
/** Start a bare interactive login shell without a provider CLI. */
|
|
2243
|
-
startShell(cwd, mode, opts) {
|
|
2299
|
+
async startShell(cwd, mode, opts) {
|
|
2244
2300
|
return this.start(this.config.shell, cwd, mode, undefined, {
|
|
2245
2301
|
...opts,
|
|
2246
2302
|
interactiveShell: true,
|