@gajae-code/coding-agent 0.9.6 → 0.10.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/CHANGELOG.md +45 -0
- package/dist/types/cli/classify-install-target.d.ts +0 -10
- package/dist/types/cli/daemon-cli.d.ts +2 -0
- package/dist/types/cli/initial-message.d.ts +1 -1
- package/dist/types/cli/malloc-env-guard.d.ts +65 -0
- package/dist/types/cli/malloc-env-guard.test.d.ts +1 -0
- package/dist/types/cli/notify-cli.d.ts +3 -1
- package/dist/types/cli/session-picker.d.ts +4 -3
- package/dist/types/cli/update-cli.d.ts +22 -3
- package/dist/types/cli.d.ts +4 -0
- package/dist/types/commands/daemon.d.ts +6 -2
- package/dist/types/commands/launch.d.ts +1 -0
- package/dist/types/commands/notify.d.ts +6 -0
- package/dist/types/commit/analysis/conventional.d.ts +1 -1
- package/dist/types/commit/analysis/summary.d.ts +1 -1
- package/dist/types/commit/changelog/generate.d.ts +1 -1
- package/dist/types/commit/changelog/index.d.ts +2 -2
- package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
- package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/model-profile-activation.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +47 -7
- package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
- package/dist/types/coordinator-mcp/server.d.ts +67 -6
- package/dist/types/daemon/control-types.d.ts +10 -0
- package/dist/types/daemon/operator-contract.d.ts +47 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
- package/dist/types/discovery/helpers.d.ts +1 -1
- package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
- package/dist/types/gjc-runtime/launch-tmux.d.ts +26 -7
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +34 -3
- package/dist/types/gjc-runtime/tmux-common.d.ts +6 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +309 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +49 -17
- package/dist/types/internal-urls/local-protocol.d.ts +13 -9
- package/dist/types/main.d.ts +66 -2
- package/dist/types/modes/acp/acp-agent.d.ts +19 -2
- package/dist/types/modes/acp/acp-mode.d.ts +12 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -1
- package/dist/types/modes/components/custom-editor.d.ts +2 -0
- package/dist/types/modes/components/gajae-pet-widget.d.ts +59 -0
- package/dist/types/modes/components/hook-selector.d.ts +5 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +32 -0
- package/dist/types/modes/components/pet-selector.d.ts +11 -0
- package/dist/types/modes/components/session-selector.d.ts +22 -9
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +1 -0
- package/dist/types/modes/components/user-message.d.ts +2 -1
- package/dist/types/modes/controllers/event-controller.d.ts +4 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +24 -2
- package/dist/types/modes/irc-observation-ledger.d.ts +22 -0
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +8 -0
- package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +5 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +45 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +42 -2
- package/dist/types/modes/theme/defaults/index.d.ts +568 -568
- package/dist/types/modes/types.d.ts +28 -2
- package/dist/types/modes/utils/abort-message.d.ts +1 -1
- package/dist/types/modes/utils/irc-message.d.ts +38 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +13 -4
- package/dist/types/notifications/config.d.ts +34 -0
- package/dist/types/notifications/daemon-paths.d.ts +7 -0
- package/dist/types/notifications/index.d.ts +27 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +12 -4
- package/dist/types/notifications/notification-service.d.ts +144 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +27 -9
- package/dist/types/notifications/telegram-daemon-control.d.ts +6 -0
- package/dist/types/notifications/telegram-daemon.d.ts +130 -3
- package/dist/types/notifications/telegram-reference.d.ts +19 -2
- package/dist/types/notifications/topic-registry.d.ts +24 -4
- package/dist/types/registry/agent-registry.d.ts +2 -0
- package/dist/types/rlm/index.d.ts +1 -1
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +26 -2
- package/dist/types/session/cache-economics.d.ts +79 -0
- package/dist/types/session/provider-safety-stop.d.ts +5 -0
- package/dist/types/session/session-manager.d.ts +124 -4
- package/dist/types/session/session-storage.d.ts +155 -13
- package/dist/types/setup/credential-auto-import.d.ts +2 -2
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/receipt.d.ts +3 -1
- package/dist/types/task/types.d.ts +8 -0
- package/dist/types/tools/ask-answer-registry.d.ts +5 -0
- package/dist/types/tools/ask.d.ts +25 -1
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
- package/dist/types/tools/index.d.ts +58 -6
- package/dist/types/tools/telegram-send.d.ts +7 -0
- package/dist/types/utils/external-editor.d.ts +2 -0
- package/package.json +9 -9
- package/scripts/g004-tmux-smoke.ts +196 -36
- package/src/cli/daemon-cli.ts +19 -29
- package/src/cli/malloc-env-guard.test.ts +104 -0
- package/src/cli/malloc-env-guard.ts +149 -0
- package/src/cli/notify-cli.ts +78 -14
- package/src/cli/session-picker.ts +23 -40
- package/src/cli/update-cli.ts +56 -37
- package/src/cli.ts +50 -10
- package/src/commands/daemon.ts +19 -5
- package/src/commands/harness.ts +515 -47
- package/src/commands/launch.ts +11 -3
- package/src/commands/notify.ts +5 -1
- package/src/commands/session.ts +6 -1
- package/src/config/keybindings.ts +5 -0
- package/src/config/model-profile-activation.ts +1 -1
- package/src/config/model-profiles.ts +14 -14
- package/src/config/settings-schema.ts +41 -1
- package/src/coordinator-mcp/model-preset.ts +100 -0
- package/src/coordinator-mcp/server.ts +2653 -425
- package/src/daemon/control-types.ts +11 -0
- package/src/daemon/operator-contract.ts +121 -0
- package/src/deep-interview/render-middleware.ts +67 -2
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
- package/src/gjc-runtime/launch-tmux.ts +702 -135
- package/src/gjc-runtime/session-state-sidecar.ts +756 -135
- package/src/gjc-runtime/team-runtime.ts +6 -0
- package/src/gjc-runtime/tmux-common.ts +26 -7
- package/src/gjc-runtime/tmux-gc.ts +52 -9
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +185 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +2060 -0
- package/src/gjc-runtime/tmux-sessions.ts +871 -68
- package/src/internal-urls/docs-index.generated.ts +9 -8
- package/src/internal-urls/local-protocol.ts +29 -10
- package/src/main.ts +298 -75
- package/src/modes/acp/acp-agent.ts +728 -98
- package/src/modes/acp/acp-mode.ts +29 -2
- package/src/modes/components/assistant-message.ts +32 -3
- package/src/modes/components/bash-execution.ts +43 -13
- package/src/modes/components/custom-editor.ts +5 -0
- package/src/modes/components/gajae-pet-widget.ts +360 -0
- package/src/modes/components/hook-selector.ts +12 -0
- package/src/modes/components/irc-sidebar.ts +206 -0
- package/src/modes/components/pet-selector.ts +55 -0
- package/src/modes/components/session-selector.ts +222 -221
- package/src/modes/components/settings-selector.ts +10 -0
- package/src/modes/components/tool-execution.ts +47 -6
- package/src/modes/components/user-message.ts +26 -9
- package/src/modes/controllers/command-controller.ts +28 -2
- package/src/modes/controllers/event-controller.ts +159 -36
- package/src/modes/controllers/extension-ui-controller.ts +36 -17
- package/src/modes/controllers/input-controller.ts +8 -2
- package/src/modes/controllers/selector-controller.ts +62 -15
- package/src/modes/interactive-mode.ts +183 -7
- package/src/modes/irc-observation-ledger.ts +127 -0
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
- package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
- package/src/modes/types.ts +34 -2
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/irc-message.ts +138 -0
- package/src/modes/utils/ui-helpers.ts +216 -50
- package/src/notifications/config.ts +44 -0
- package/src/notifications/daemon-paths.ts +8 -0
- package/src/notifications/index.ts +535 -75
- package/src/notifications/lifecycle-control-runtime.ts +930 -77
- package/src/notifications/notification-service.ts +710 -0
- package/src/notifications/rate-limit-pool.ts +46 -12
- package/src/notifications/telegram-daemon-cli.ts +2 -0
- package/src/notifications/telegram-daemon-control.ts +27 -2
- package/src/notifications/telegram-daemon.ts +635 -94
- package/src/notifications/telegram-reference.ts +73 -13
- package/src/notifications/topic-registry.ts +86 -9
- package/src/prompts/system/irc-peer-roster.md +1 -0
- package/src/prompts/system/subagent-system-prompt.md +1 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/irc.md +1 -1
- package/src/registry/agent-registry.ts +3 -0
- package/src/sdk.ts +52 -1
- package/src/secrets/obfuscator.ts +4 -2
- package/src/session/agent-session.ts +396 -145
- package/src/session/cache-economics.ts +275 -0
- package/src/session/provider-safety-stop.ts +9 -0
- package/src/session/session-dump-format.ts +6 -0
- package/src/session/session-manager.ts +809 -39
- package/src/session/session-storage.ts +582 -38
- package/src/slash-commands/builtin-registry.ts +143 -9
- package/src/task/executor.ts +12 -3
- package/src/task/index.ts +8 -0
- package/src/task/receipt.ts +7 -2
- package/src/task/types.ts +26 -0
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/ask.ts +355 -33
- package/src/tools/browser/tab-supervisor.ts +63 -6
- package/src/tools/browser.ts +8 -3
- package/src/tools/index.ts +81 -6
- package/src/tools/sqlite-reader.ts +8 -4
- package/src/tui/output-block.ts +18 -6
- package/src/utils/external-editor.ts +6 -1
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
|
+
import * as crypto from "node:crypto";
|
|
2
3
|
import * as fs from "node:fs";
|
|
3
4
|
import * as path from "node:path";
|
|
4
5
|
import { VERSION } from "@gajae-code/utils/dirs";
|
|
5
6
|
import { safeStderrWrite } from "@gajae-code/utils/safe-stderr";
|
|
6
7
|
import type { Args } from "../cli/args";
|
|
7
8
|
import { tmuxRuntimeSessionPath } from "./session-layout";
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
GJC_COORDINATOR_SESSION_ID_ENV,
|
|
11
|
+
GJC_COORDINATOR_SESSION_STATE_FILE_ENV,
|
|
12
|
+
GJC_TMUX_OWNER_GENERATION_ENV,
|
|
13
|
+
GJC_TMUX_OWNER_SERVER_KEY_ENV,
|
|
14
|
+
GJC_TMUX_OWNER_STATE_DIR_ENV,
|
|
15
|
+
} from "./session-state-sidecar";
|
|
9
16
|
import {
|
|
10
17
|
buildGjcTmuxExactOptionTarget,
|
|
11
18
|
buildGjcTmuxExactSessionTarget,
|
|
@@ -22,7 +29,23 @@ import {
|
|
|
22
29
|
resolveGjcTmuxBinary,
|
|
23
30
|
resolveGjcTmuxCommand,
|
|
24
31
|
} from "./tmux-common";
|
|
25
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
captureOwnerGenerationBaselineSync,
|
|
34
|
+
classifyCgroup,
|
|
35
|
+
executeTmuxOwnerIsolationPlanSync,
|
|
36
|
+
isOwnerGenerationBaselineCurrentSync,
|
|
37
|
+
lifecyclePaths,
|
|
38
|
+
type OwnerIsolationProbeSync,
|
|
39
|
+
planTmuxOwnerIsolationSync,
|
|
40
|
+
replaceOwnerGenerationSync,
|
|
41
|
+
type TmuxServerProof,
|
|
42
|
+
} from "./tmux-owner-isolation";
|
|
43
|
+
import {
|
|
44
|
+
findGjcTmuxSessionByName,
|
|
45
|
+
findGjcTmuxSessionByScope,
|
|
46
|
+
type GjcTmuxSessionStatus,
|
|
47
|
+
proveGjcTmuxSessionMutationTarget,
|
|
48
|
+
} from "./tmux-sessions";
|
|
26
49
|
|
|
27
50
|
export {
|
|
28
51
|
buildGjcTmuxExactSessionTarget,
|
|
@@ -37,7 +60,7 @@ export {
|
|
|
37
60
|
export const GJC_TMUX_LAUNCHED_ENV = "GJC_TMUX_LAUNCHED";
|
|
38
61
|
export const GJC_LAUNCH_POLICY_ENV = "GJC_LAUNCH_POLICY";
|
|
39
62
|
export const GJC_TMUX_WINDOW_LABEL_MAX_WIDTH = 48;
|
|
40
|
-
|
|
63
|
+
|
|
41
64
|
const WINDOWS_PSMUX_ATTACH_RETRY_DELAY_MS = 100;
|
|
42
65
|
const TERMINAL_TITLE_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
43
66
|
|
|
@@ -67,12 +90,20 @@ export interface TmuxLaunchContext {
|
|
|
67
90
|
existingBranchSessionName?: string | null;
|
|
68
91
|
project?: string | null;
|
|
69
92
|
diagnosticWriter?: (message: string) => void;
|
|
93
|
+
/**
|
|
94
|
+
* Synchronous owner-isolation proof boundary for managed tmux creation.
|
|
95
|
+
* Production uses Linux /proc probes; callers may inject deterministic proofs.
|
|
96
|
+
*/
|
|
97
|
+
ownerIsolationProbe?: OwnerIsolationProbeSync;
|
|
98
|
+
/** Test seam for deterministic default-probe caller cgroup classification. */
|
|
99
|
+
callerCgroupReader?: () => string | null;
|
|
70
100
|
}
|
|
71
101
|
|
|
72
102
|
export interface TmuxSpawnResult {
|
|
73
103
|
exitCode: number | null;
|
|
74
104
|
signalCode?: string | null;
|
|
75
105
|
stderr?: string;
|
|
106
|
+
stdout?: string;
|
|
76
107
|
}
|
|
77
108
|
export interface TmuxTerminalSize {
|
|
78
109
|
columns: number;
|
|
@@ -88,14 +119,12 @@ export interface TmuxSpawnOptions {
|
|
|
88
119
|
stdout: "inherit" | "pipe";
|
|
89
120
|
stderr: "inherit" | "pipe";
|
|
90
121
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* text is also returned in TmuxSpawnResult.stderr for diagnostic
|
|
94
|
-
* surfacing in "attach failed" / "profile tagging failed" messages.
|
|
95
|
-
* Defaults to false to preserve the previous PTY-binding behavior for
|
|
96
|
-
* attach-session and other interactive commands.
|
|
122
|
+
* Captures control-plane stderr for sanitized, bounded diagnostics. PTY-bound
|
|
123
|
+
* commands retain inherited stderr for multiplexer compatibility.
|
|
97
124
|
*/
|
|
98
125
|
captureStderr?: boolean;
|
|
126
|
+
/** Internal scoped-bootstrap input; never used for ordinary tmux commands. */
|
|
127
|
+
stdinLine?: string;
|
|
99
128
|
}
|
|
100
129
|
|
|
101
130
|
export interface TmuxLaunchPlan {
|
|
@@ -110,6 +139,14 @@ export interface TmuxLaunchPlan {
|
|
|
110
139
|
project?: string | null;
|
|
111
140
|
sessionId?: string | null;
|
|
112
141
|
sessionStateFile?: string | null;
|
|
142
|
+
/** Immutable Linux-managed owner provenance, assigned immediately before creation. */
|
|
143
|
+
ownerGeneration?: string;
|
|
144
|
+
/** Generation state captured before owner-isolation planning; required for publication CAS. */
|
|
145
|
+
ownerGenerationBaseline?: import("./tmux-owner-isolation").OwnerGenerationBaseline;
|
|
146
|
+
/** Native tmux session identity emitted atomically by `new-session -P -F`. */
|
|
147
|
+
createdSessionId?: string;
|
|
148
|
+
/** Safe server identity proven immediately after creation. */
|
|
149
|
+
createdServerIdentity?: { pid: number; startTime: string };
|
|
113
150
|
isPsmux: boolean;
|
|
114
151
|
platform: NodeJS.Platform;
|
|
115
152
|
}
|
|
@@ -155,6 +192,8 @@ export interface GjcTmuxProfileContext {
|
|
|
155
192
|
project?: string | null;
|
|
156
193
|
sessionId?: string | null;
|
|
157
194
|
sessionStateFile?: string | null;
|
|
195
|
+
ownerGeneration?: string | null;
|
|
196
|
+
ownerServerKey?: string | null;
|
|
158
197
|
version?: string | null;
|
|
159
198
|
}
|
|
160
199
|
|
|
@@ -222,12 +261,29 @@ function isBunVirtualPath(value: string | undefined): boolean {
|
|
|
222
261
|
return value?.startsWith("/$bunfs/") === true;
|
|
223
262
|
}
|
|
224
263
|
|
|
264
|
+
const MAX_TMUX_DIAGNOSTIC_DETAIL_CODE_POINTS = 240;
|
|
265
|
+
const TERMINAL_DIAGNOSTIC_CONTROLS =
|
|
266
|
+
/(?:\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)?|[PX^_][^\x1b]*(?:\x1b\\)?|.)?|\u009b[0-?]*[ -/]*[@-~]|\u009d[^\x07\x1b\u009c]*(?:\x07|\x1b\\|\u009c)?|[\u0000-\u001f\u007f-\u009f])/g;
|
|
267
|
+
|
|
268
|
+
function sanitizeTmuxDiagnostic(stderr: string | undefined): string {
|
|
269
|
+
const detail = stderr?.replace(TERMINAL_DIAGNOSTIC_CONTROLS, "").trim() ?? "";
|
|
270
|
+
return Array.from(detail).slice(0, MAX_TMUX_DIAGNOSTIC_DETAIL_CODE_POINTS).join("");
|
|
271
|
+
}
|
|
272
|
+
|
|
225
273
|
function formatTmuxLaunchDiagnostic(stage: string, stderr?: string): string {
|
|
226
|
-
const detail = stderr
|
|
227
|
-
const suffix = detail ? ` ${detail
|
|
274
|
+
const detail = sanitizeTmuxDiagnostic(stderr);
|
|
275
|
+
const suffix = detail ? ` ${detail}` : "";
|
|
228
276
|
return `gjc --tmux failed after creating tmux session: ${stage}.${suffix}\n`;
|
|
229
277
|
}
|
|
230
278
|
|
|
279
|
+
function failedRetryDiagnostic(retry: TmuxSpawnResult, retryProbe: TmuxSpawnResult): string | undefined {
|
|
280
|
+
return retry.exitCode !== 0 ? retry.stderr : retryProbe.stderr;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function isExplicitTmuxRequest(context: TmuxLaunchContext): boolean {
|
|
284
|
+
return context.parsed.tmux === true && context.rawArgs.includes("--tmux");
|
|
285
|
+
}
|
|
286
|
+
|
|
231
287
|
/**
|
|
232
288
|
* Detect a corrupted gjc.cmd / gjc.bat wrapper at well-known PATH locations.
|
|
233
289
|
* On Windows, `gjc.cmd` / `gjc.bat` files at the front of PATH that turn out
|
|
@@ -270,16 +326,16 @@ function detectCorruptedGjcWrapper(): string | null {
|
|
|
270
326
|
}
|
|
271
327
|
return null;
|
|
272
328
|
}
|
|
273
|
-
function formatTmuxUnavailableDiagnostic(platform: NodeJS.Platform
|
|
329
|
+
function formatTmuxUnavailableDiagnostic(platform: NodeJS.Platform): string {
|
|
274
330
|
if (platform === "win32") {
|
|
275
331
|
return (
|
|
276
|
-
`gjc --tmux requested but no tmux executable was found;
|
|
277
|
-
|
|
332
|
+
`gjc --tmux requested but no tmux executable was found; cannot continue without a tmux-backed session. ` +
|
|
333
|
+
"GJC searched for psmux, pmux, and tmux on PATH. " +
|
|
278
334
|
"Install psmux (https://github.com/psmux/psmux) for native Windows tmux support, or use WSL with real tmux. " +
|
|
279
335
|
"You can also point GJC at a specific binary via GJC_TMUX_COMMAND.\n"
|
|
280
336
|
);
|
|
281
337
|
}
|
|
282
|
-
return
|
|
338
|
+
return "gjc --tmux requested but no tmux executable was found; cannot continue without a tmux-backed session.\n";
|
|
283
339
|
}
|
|
284
340
|
|
|
285
341
|
function shellQuote(value: string): string {
|
|
@@ -359,6 +415,8 @@ export function applyGjcTmuxProfile(context: GjcTmuxProfileContext): GjcTmuxProf
|
|
|
359
415
|
project: context.project ?? null,
|
|
360
416
|
sessionId: context.sessionId ?? env[GJC_COORDINATOR_SESSION_ID_ENV] ?? null,
|
|
361
417
|
sessionStateFile: context.sessionStateFile ?? env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV] ?? null,
|
|
418
|
+
ownerGeneration: context.ownerGeneration ?? null,
|
|
419
|
+
ownerServerKey: context.ownerServerKey ?? null,
|
|
362
420
|
version: context.version ?? null,
|
|
363
421
|
},
|
|
364
422
|
{ tmuxCommand: context.tmuxCommand },
|
|
@@ -369,9 +427,9 @@ export function applyGjcTmuxProfile(context: GjcTmuxProfileContext): GjcTmuxProf
|
|
|
369
427
|
const options: TmuxSpawnOptions = {
|
|
370
428
|
cwd,
|
|
371
429
|
env,
|
|
372
|
-
stdin: "
|
|
373
|
-
stdout: "
|
|
374
|
-
stderr: "
|
|
430
|
+
stdin: "pipe",
|
|
431
|
+
stdout: "pipe",
|
|
432
|
+
stderr: "pipe",
|
|
375
433
|
captureStderr: true,
|
|
376
434
|
};
|
|
377
435
|
const failures: GjcTmuxProfileResult["failures"] = [];
|
|
@@ -524,13 +582,13 @@ function buildGjcTmuxRootTerminalTitleCommands(
|
|
|
524
582
|
function applyGjcTmuxRootTerminalTitleProfile(context: {
|
|
525
583
|
tmuxCommand: string;
|
|
526
584
|
target: string;
|
|
585
|
+
sessionName: string;
|
|
527
586
|
title: string | undefined;
|
|
528
587
|
spawnSync: TmuxSpawnSync;
|
|
529
588
|
options: TmuxSpawnOptions;
|
|
530
589
|
}): void {
|
|
531
590
|
if (!context.title) return;
|
|
532
|
-
const
|
|
533
|
-
for (const command of buildGjcTmuxRootTerminalTitleCommands(optionTarget, context.target, context.title)) {
|
|
591
|
+
for (const command of buildGjcTmuxRootTerminalTitleCommands(context.target, context.sessionName, context.title)) {
|
|
534
592
|
context.spawnSync(context.tmuxCommand, command.args, context.options);
|
|
535
593
|
}
|
|
536
594
|
}
|
|
@@ -577,9 +635,10 @@ function renameExistingTmuxWindowIfNeeded(context: TmuxLaunchContext): void {
|
|
|
577
635
|
renameTmuxWindow(tmuxCommand, title, spawnSync, {
|
|
578
636
|
cwd,
|
|
579
637
|
env,
|
|
580
|
-
stdin: "
|
|
581
|
-
stdout: "
|
|
582
|
-
stderr: "
|
|
638
|
+
stdin: "pipe",
|
|
639
|
+
stdout: "pipe",
|
|
640
|
+
stderr: "pipe",
|
|
641
|
+
captureStderr: true,
|
|
583
642
|
});
|
|
584
643
|
}
|
|
585
644
|
|
|
@@ -598,21 +657,110 @@ function readCurrentBranch(cwd: string): string | null {
|
|
|
598
657
|
}
|
|
599
658
|
}
|
|
600
659
|
|
|
601
|
-
function
|
|
602
|
-
|
|
660
|
+
function createdSessionTarget(plan: TmuxLaunchPlan): string {
|
|
661
|
+
return plan.createdSessionId ?? plan.sessionName;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function createdSessionExactTarget(plan: TmuxLaunchPlan, env: NodeJS.ProcessEnv): string {
|
|
665
|
+
return plan.createdSessionId ?? buildGjcTmuxExactSessionTarget(plan.sessionName, { env });
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function createdSessionOptionTarget(plan: TmuxLaunchPlan, env: NodeJS.ProcessEnv): string {
|
|
669
|
+
// Native tmux assigns `$N` atomically during creation. Keep that immutable
|
|
670
|
+
// identity (with its required empty-window suffix) for every later option
|
|
671
|
+
// mutation; a reusable session name could resolve to a different session.
|
|
672
|
+
if (plan.createdSessionId) return `${plan.createdSessionId}:`;
|
|
673
|
+
return buildGjcTmuxExactOptionTarget(plan.sessionName, { env });
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function cleanupCreatedTmuxSession(
|
|
677
|
+
plan: TmuxLaunchPlan,
|
|
678
|
+
spawnSync: TmuxSpawnSync,
|
|
679
|
+
options: TmuxSpawnOptions,
|
|
680
|
+
probe: OwnerIsolationProbeSync,
|
|
681
|
+
): void {
|
|
682
|
+
if (!isCreatedTmuxSessionIdentityStable(plan, spawnSync, options, probe))
|
|
683
|
+
throw new Error("gjc_tmux_exact_cleanup_uncertain");
|
|
684
|
+
const nativeSessionId = plan.createdSessionId!;
|
|
685
|
+
const expectedPid = plan.createdServerIdentity!.pid;
|
|
686
|
+
const guarded = spawnSync(
|
|
603
687
|
plan.tmuxCommand,
|
|
604
|
-
[
|
|
688
|
+
[
|
|
689
|
+
"if-shell",
|
|
690
|
+
"-t",
|
|
691
|
+
nativeSessionId,
|
|
692
|
+
"-F",
|
|
693
|
+
`#{&&:#{==:#{pid},${expectedPid}},#{&&:#{==:#{session_id},${nativeSessionId}},#{==:#{session_name},${plan.sessionName}}}}`,
|
|
694
|
+
`kill-session -t ${nativeSessionId} \\; display-message -p __gjc_tmux_guarded_cleanup_ok__`,
|
|
695
|
+
"display-message -p __gjc_tmux_guarded_cleanup_refused__",
|
|
696
|
+
],
|
|
605
697
|
options,
|
|
606
698
|
);
|
|
699
|
+
if (guarded.exitCode !== 0 || guarded.stdout?.trim() !== "__gjc_tmux_guarded_cleanup_ok__")
|
|
700
|
+
throw new Error("gjc_tmux_exact_cleanup_uncertain");
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function cleanupCreatedTmuxSessionAfterFailure(
|
|
704
|
+
plan: TmuxLaunchPlan,
|
|
705
|
+
spawnSync: TmuxSpawnSync,
|
|
706
|
+
options: TmuxSpawnOptions,
|
|
707
|
+
probe: OwnerIsolationProbeSync,
|
|
708
|
+
primary: Error,
|
|
709
|
+
): void {
|
|
710
|
+
try {
|
|
711
|
+
cleanupCreatedTmuxSession(plan, spawnSync, options, probe);
|
|
712
|
+
} catch (cleanupError) {
|
|
713
|
+
throw new AggregateError([primary, cleanupError], "gjc_tmux_exact_cleanup_uncertain");
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function isCreatedTmuxSessionIdentityStable(
|
|
718
|
+
plan: TmuxLaunchPlan,
|
|
719
|
+
spawnSync: TmuxSpawnSync,
|
|
720
|
+
options: TmuxSpawnOptions,
|
|
721
|
+
probe: OwnerIsolationProbeSync,
|
|
722
|
+
): boolean {
|
|
723
|
+
if (plan.isPsmux || !plan.createdSessionId || !plan.createdServerIdentity) return false;
|
|
724
|
+
try {
|
|
725
|
+
const before = probe.probeServer(plan.tmuxCommand);
|
|
726
|
+
if (
|
|
727
|
+
before.state !== "safe" ||
|
|
728
|
+
before.pid !== plan.createdServerIdentity.pid ||
|
|
729
|
+
before.startTime !== plan.createdServerIdentity.startTime
|
|
730
|
+
)
|
|
731
|
+
return false;
|
|
732
|
+
const binding = spawnSync(
|
|
733
|
+
plan.tmuxCommand,
|
|
734
|
+
["display-message", "-p", "-t", plan.createdSessionId, "#{session_id}\t#{session_name}"],
|
|
735
|
+
options,
|
|
736
|
+
);
|
|
737
|
+
const after = probe.probeServer(plan.tmuxCommand);
|
|
738
|
+
return (
|
|
739
|
+
binding.exitCode === 0 &&
|
|
740
|
+
binding.stdout?.trim() === `${plan.createdSessionId}\t${plan.sessionName}` &&
|
|
741
|
+
after.state === "safe" &&
|
|
742
|
+
after.pid === before.pid &&
|
|
743
|
+
after.startTime === before.startTime
|
|
744
|
+
);
|
|
745
|
+
} catch {
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
607
748
|
}
|
|
608
749
|
function isTmuxAttachDisconnectError(result: TmuxSpawnResult): boolean {
|
|
609
750
|
if (result.signalCode === "SIGHUP") return true;
|
|
610
|
-
|
|
611
|
-
return stderr.includes("eio") || stderr.includes("input/output error");
|
|
751
|
+
return /\b(?:EIO|input\/output error)\b/i.test(result.stderr ?? "");
|
|
612
752
|
}
|
|
613
753
|
function isWindowsPsmuxAttachConnectionRefused(plan: TmuxLaunchPlan, result: TmuxSpawnResult): boolean {
|
|
614
754
|
if (plan.platform !== "win32" || !plan.isPsmux) return false;
|
|
615
|
-
return result.stderr
|
|
755
|
+
return /\bos error 10061\b/i.test(result.stderr ?? "");
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function isWindowsPsmuxMissingSessionRegistrationRace(
|
|
759
|
+
plan: TmuxLaunchPlan,
|
|
760
|
+
result: { exitCode?: number | null; stderr?: string },
|
|
761
|
+
): boolean {
|
|
762
|
+
if (plan.platform !== "win32" || !plan.isPsmux || result.exitCode === 0) return false;
|
|
763
|
+
return result.stderr?.trim() === `psmux: can't find session '=${plan.sessionName}' (no server running)`;
|
|
616
764
|
}
|
|
617
765
|
function waitForWindowsPsmuxAttachRetry(): void {
|
|
618
766
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, WINDOWS_PSMUX_ATTACH_RETRY_DELAY_MS);
|
|
@@ -682,7 +830,7 @@ function ensureCreatedTmuxWindowTracksCallerTerminal(
|
|
|
682
830
|
options: TmuxSpawnOptions,
|
|
683
831
|
): void {
|
|
684
832
|
if (!plan.initialSize) return;
|
|
685
|
-
const target =
|
|
833
|
+
const target = createdSessionOptionTarget(plan, options.env);
|
|
686
834
|
if (plan.isPsmux) {
|
|
687
835
|
spawnSync(
|
|
688
836
|
plan.tmuxCommand,
|
|
@@ -728,7 +876,7 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
728
876
|
tmuxRuntimeSessionPath(cwd, gjcSessionId, buildGjcTmuxSessionSlug(sessionName));
|
|
729
877
|
const tmuxAvailable = context.tmuxAvailable ?? Bun.which(tmuxCommand) !== null;
|
|
730
878
|
if (!tmuxAvailable) {
|
|
731
|
-
(context.diagnosticWriter ?? safeStderrWrite)(formatTmuxUnavailableDiagnostic(platform
|
|
879
|
+
(context.diagnosticWriter ?? safeStderrWrite)(formatTmuxUnavailableDiagnostic(platform));
|
|
732
880
|
return undefined;
|
|
733
881
|
}
|
|
734
882
|
const existingSessionName = allowsExistingTmuxAttach(context.parsed, env)
|
|
@@ -778,6 +926,7 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
778
926
|
sessionName,
|
|
779
927
|
"-c",
|
|
780
928
|
cwd,
|
|
929
|
+
...(resolvedBinary.isPsmux ? [] : ["-P", "-F", "#{session_id}"]),
|
|
781
930
|
innerCommand,
|
|
782
931
|
],
|
|
783
932
|
initialSize,
|
|
@@ -789,63 +938,306 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
789
938
|
};
|
|
790
939
|
}
|
|
791
940
|
|
|
941
|
+
function prepareManagedOwnerLifecycle(plan: TmuxLaunchPlan, context: TmuxLaunchContext): void {
|
|
942
|
+
if (plan.ownerGeneration) return;
|
|
943
|
+
const sessionId = plan.sessionId ?? plan.sessionName;
|
|
944
|
+
const stateDir = path.dirname(plan.sessionStateFile ?? path.join(plan.cwd, ".gjc", "runtime"));
|
|
945
|
+
const generation = crypto.randomUUID();
|
|
946
|
+
// Stage the generation in the child command. It becomes current only after
|
|
947
|
+
// immutable creation proof and ownership tagging complete.
|
|
948
|
+
|
|
949
|
+
plan.ownerGeneration = generation;
|
|
950
|
+
const innerCommand = buildInnerCommand(
|
|
951
|
+
{
|
|
952
|
+
cwd: plan.cwd,
|
|
953
|
+
argv: context.argv ?? process.argv,
|
|
954
|
+
execPath: context.execPath ?? process.execPath,
|
|
955
|
+
extraEnv: {
|
|
956
|
+
[GJC_COORDINATOR_SESSION_ID_ENV]: sessionId,
|
|
957
|
+
[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]: plan.sessionStateFile ?? "",
|
|
958
|
+
[GJC_TMUX_ACTIVE_SESSION_ENV]: plan.sessionName,
|
|
959
|
+
[GJC_TMUX_OWNER_GENERATION_ENV]: generation,
|
|
960
|
+
[GJC_TMUX_OWNER_STATE_DIR_ENV]: stateDir,
|
|
961
|
+
[GJC_TMUX_OWNER_SERVER_KEY_ENV]: plan.tmuxCommand,
|
|
962
|
+
},
|
|
963
|
+
// Linux managed owner close signals the pane PID. Do not place the exit-marker shell
|
|
964
|
+
// in front of it; `buildInnerCommand` therefore execs the GJC owner directly.
|
|
965
|
+
tmuxExitMarkerPath: plan.platform === "linux" ? undefined : tmuxExitMarkerPath(plan.sessionStateFile ?? ""),
|
|
966
|
+
platform: plan.platform,
|
|
967
|
+
},
|
|
968
|
+
context.rawArgs,
|
|
969
|
+
);
|
|
970
|
+
plan.innerCommand = innerCommand;
|
|
971
|
+
plan.newSessionArgs = [...plan.newSessionArgs.slice(0, -1), innerCommand];
|
|
972
|
+
}
|
|
973
|
+
|
|
792
974
|
function defaultSpawnSync(command: string, args: string[], options: TmuxSpawnOptions): TmuxSpawnResult {
|
|
793
|
-
//
|
|
794
|
-
//
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
: { stdin: options.stdin, stdout: options.stdout, stderr: options.stderr };
|
|
975
|
+
// Only attach-session is interactive. Every other command is control-plane
|
|
976
|
+
// traffic and must not write unbounded, untrusted terminal bytes directly.
|
|
977
|
+
const interactiveAttach = args[0] === "attach-session";
|
|
978
|
+
const stdin = options.stdinLine === undefined ? options.stdin : Buffer.from(`${options.stdinLine}\n`);
|
|
979
|
+
const stdio = interactiveAttach
|
|
980
|
+
? { stdin, stdout: options.stdout, stderr: options.stderr }
|
|
981
|
+
: { stdin, stdout: options.stdout, stderr: "pipe" as const };
|
|
801
982
|
const result = Bun.spawnSync({
|
|
802
983
|
cmd: [command, ...args],
|
|
803
984
|
cwd: options.cwd,
|
|
804
985
|
env: options.env,
|
|
805
986
|
...stdio,
|
|
806
987
|
});
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
988
|
+
const stderrText = stdio.stderr === "pipe" ? new TextDecoder().decode(result.stderr) : undefined;
|
|
989
|
+
return {
|
|
990
|
+
exitCode: result.exitCode,
|
|
991
|
+
signalCode: result.signalCode,
|
|
992
|
+
stderr: stderrText,
|
|
993
|
+
stdout: result.stdout ? new TextDecoder().decode(result.stdout) : "",
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function defaultOwnerIsolationProbe(
|
|
998
|
+
plan: TmuxLaunchPlan,
|
|
999
|
+
env: NodeJS.ProcessEnv,
|
|
1000
|
+
spawn: TmuxSpawnSync,
|
|
1001
|
+
callerCgroupReader?: () => string | null,
|
|
1002
|
+
): OwnerIsolationProbeSync {
|
|
1003
|
+
const stateDir = path.dirname(plan.sessionStateFile ?? path.join(plan.cwd, ".gjc", "runtime"));
|
|
1004
|
+
const probeServer = (): TmuxServerProof => {
|
|
1005
|
+
if (plan.platform !== "linux") {
|
|
1006
|
+
return { state: "safe", pid: 1, startTime: "not-applicable", cgroup: { classification: "not_applicable" } };
|
|
1007
|
+
}
|
|
1008
|
+
const probe = spawn(plan.tmuxCommand, ["display-message", "-p", "#{pid}"], {
|
|
1009
|
+
cwd: plan.cwd,
|
|
1010
|
+
env,
|
|
1011
|
+
stdin: "pipe",
|
|
1012
|
+
stdout: "pipe",
|
|
1013
|
+
stderr: "pipe",
|
|
1014
|
+
});
|
|
1015
|
+
if (probe.exitCode !== 0) {
|
|
1016
|
+
return /no server running|failed to connect|error connecting/.test(probe.stderr ?? "")
|
|
1017
|
+
? { state: "absent" }
|
|
1018
|
+
: { state: "unverifiable" };
|
|
1019
|
+
}
|
|
1020
|
+
const pid = Number(probe.stdout?.trim());
|
|
1021
|
+
let startTime: string | undefined;
|
|
1022
|
+
let cgroupText: string | null = null;
|
|
1023
|
+
try {
|
|
1024
|
+
const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
1025
|
+
startTime = stat
|
|
1026
|
+
.slice(stat.lastIndexOf(")") + 2)
|
|
1027
|
+
.trim()
|
|
1028
|
+
.split(/\s+/)[19];
|
|
1029
|
+
cgroupText = fs.readFileSync(`/proc/${pid}/cgroup`, "utf8");
|
|
1030
|
+
} catch {}
|
|
1031
|
+
if (!startTime) return { state: "unverifiable" };
|
|
1032
|
+
const cgroup = classifyCgroup({ platform: plan.platform, cgroupText });
|
|
1033
|
+
return {
|
|
1034
|
+
state:
|
|
1035
|
+
cgroup.classification === "safe"
|
|
1036
|
+
? "safe"
|
|
1037
|
+
: cgroup.classification === "unsafe_service"
|
|
1038
|
+
? "unsafe"
|
|
1039
|
+
: "unverifiable",
|
|
1040
|
+
pid,
|
|
1041
|
+
startTime,
|
|
1042
|
+
cgroup,
|
|
1043
|
+
};
|
|
1044
|
+
};
|
|
1045
|
+
return {
|
|
1046
|
+
readCallerCgroup:
|
|
1047
|
+
plan.platform !== "linux"
|
|
1048
|
+
? () => null
|
|
1049
|
+
: (callerCgroupReader ??
|
|
1050
|
+
(() => {
|
|
1051
|
+
try {
|
|
1052
|
+
return fs.readFileSync("/proc/self/cgroup", "utf8");
|
|
1053
|
+
} catch {
|
|
1054
|
+
return null;
|
|
1055
|
+
}
|
|
1056
|
+
})),
|
|
1057
|
+
probeServer,
|
|
1058
|
+
recordAttempt: ({ attempt }) => {
|
|
1059
|
+
const generation = plan.ownerGeneration ?? plan.sessionName;
|
|
1060
|
+
const root = lifecyclePaths(stateDir, plan.sessionId ?? plan.sessionName, generation).root;
|
|
1061
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
1062
|
+
const file = path.join(root, `attempt-${attempt.token}.json`);
|
|
1063
|
+
let descriptor: number | undefined;
|
|
812
1064
|
try {
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1065
|
+
descriptor = fs.openSync(file, "wx", 0o600);
|
|
1066
|
+
fs.writeFileSync(
|
|
1067
|
+
descriptor,
|
|
1068
|
+
`${JSON.stringify({
|
|
1069
|
+
schema_version: 1,
|
|
1070
|
+
generation,
|
|
1071
|
+
session_id: plan.sessionId ?? plan.sessionName,
|
|
1072
|
+
...attempt,
|
|
1073
|
+
created_at: new Date().toISOString(),
|
|
1074
|
+
})}\n`,
|
|
1075
|
+
);
|
|
1076
|
+
fs.fsyncSync(descriptor);
|
|
1077
|
+
fs.closeSync(descriptor);
|
|
1078
|
+
descriptor = undefined;
|
|
1079
|
+
const directory = fs.openSync(root, "r");
|
|
1080
|
+
try {
|
|
1081
|
+
fs.fsyncSync(directory);
|
|
1082
|
+
} finally {
|
|
1083
|
+
fs.closeSync(directory);
|
|
1084
|
+
}
|
|
1085
|
+
} finally {
|
|
1086
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
816
1087
|
}
|
|
817
|
-
}
|
|
1088
|
+
},
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function createIsolatedTmuxSession(
|
|
1093
|
+
plan: TmuxLaunchPlan,
|
|
1094
|
+
spawn: TmuxSpawnSync,
|
|
1095
|
+
options: TmuxSpawnOptions,
|
|
1096
|
+
diagnostic: (message: string) => void,
|
|
1097
|
+
probe: OwnerIsolationProbeSync,
|
|
1098
|
+
): TmuxSpawnResult {
|
|
1099
|
+
const sessionId = plan.sessionId ?? plan.sessionName;
|
|
1100
|
+
const stateDir = path.dirname(plan.sessionStateFile ?? path.join(plan.cwd, ".gjc", "runtime"));
|
|
1101
|
+
const baseline = captureOwnerGenerationBaselineSync(stateDir, sessionId);
|
|
1102
|
+
plan.ownerGenerationBaseline = baseline;
|
|
1103
|
+
const ownerPlan = planTmuxOwnerIsolationSync(
|
|
1104
|
+
{
|
|
1105
|
+
schema_version: 1,
|
|
1106
|
+
op: "plan",
|
|
1107
|
+
platform: plan.platform,
|
|
1108
|
+
session_id: sessionId,
|
|
1109
|
+
owner_generation: plan.ownerGeneration ?? plan.sessionName,
|
|
1110
|
+
baseline,
|
|
1111
|
+
cwd: plan.cwd,
|
|
1112
|
+
state_dir: stateDir,
|
|
1113
|
+
socket_key: plan.tmuxCommand,
|
|
1114
|
+
tmux_argv: [plan.tmuxCommand, ...plan.newSessionArgs],
|
|
1115
|
+
},
|
|
1116
|
+
probe,
|
|
1117
|
+
);
|
|
1118
|
+
let executed: TmuxSpawnResult | undefined;
|
|
1119
|
+
const outcome = executeTmuxOwnerIsolationPlanSync(ownerPlan, {
|
|
1120
|
+
socketKey: plan.tmuxCommand,
|
|
1121
|
+
spawn: (argv, stdinLine) => {
|
|
1122
|
+
executed = spawn(argv[0]!, argv.slice(1), {
|
|
1123
|
+
...options,
|
|
1124
|
+
stdin: stdinLine ? "pipe" : options.stdin,
|
|
1125
|
+
stdinLine,
|
|
1126
|
+
});
|
|
1127
|
+
if (!plan.isPsmux) {
|
|
1128
|
+
const nativeSessionId = executed.stdout?.trim();
|
|
1129
|
+
if (/^\$\d+$/.test(nativeSessionId ?? "")) plan.createdSessionId = nativeSessionId;
|
|
1130
|
+
}
|
|
1131
|
+
return { exitCode: executed.exitCode, stdout: executed.stdout };
|
|
1132
|
+
},
|
|
1133
|
+
probeServer: probe.probeServer,
|
|
1134
|
+
isCurrentGeneration: () => isOwnerGenerationBaselineCurrentSync(stateDir, sessionId, baseline),
|
|
1135
|
+
cleanupSpawned: ({ nativeSessionId, server }) => {
|
|
1136
|
+
plan.createdSessionId = nativeSessionId;
|
|
1137
|
+
plan.createdServerIdentity = { pid: server.pid!, startTime: server.startTime! };
|
|
1138
|
+
cleanupCreatedTmuxSessionAfterFailure(plan, spawn, options, probe, new Error("owner_generation_stale"));
|
|
1139
|
+
},
|
|
1140
|
+
});
|
|
1141
|
+
// A failed planned spawn is the new-session failure. Let its established
|
|
1142
|
+
// diagnostic path report it exactly once instead of replacing it with an
|
|
1143
|
+
// isolation diagnostic.
|
|
1144
|
+
if (executed && executed.exitCode !== 0) return executed;
|
|
1145
|
+
if (!outcome.ok) {
|
|
1146
|
+
diagnostic(`tmux owner isolation failed: ${outcome.code}`);
|
|
1147
|
+
return { exitCode: 1, stderr: outcome.diagnostic };
|
|
1148
|
+
}
|
|
1149
|
+
if (!plan.isPsmux && outcome.native_session_id) plan.createdSessionId = outcome.native_session_id;
|
|
1150
|
+
plan.createdServerIdentity = { pid: outcome.server_pid, startTime: outcome.server_start_time };
|
|
1151
|
+
return executed ?? { exitCode: 1, stderr: "tmux owner isolation did not execute" };
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function requiredProfileFailure(profile: GjcTmuxProfileResult): GjcTmuxProfileResult["failures"][number] | undefined {
|
|
1155
|
+
const requiredOptions = new Set([
|
|
1156
|
+
"@gjc-profile",
|
|
1157
|
+
"@gjc-session-id",
|
|
1158
|
+
"@gjc-session-state-file",
|
|
1159
|
+
"@gjc-owner-generation",
|
|
1160
|
+
"@gjc-owner-server-key",
|
|
1161
|
+
]);
|
|
1162
|
+
return profile.failures.find(item => requiredOptions.has(String(item.command.args[item.command.args.length - 2])));
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function emitOptionalProfileDiagnostics(profile: GjcTmuxProfileResult, diagnostic: (message: string) => void): void {
|
|
1166
|
+
for (const failure of profile.failures) {
|
|
1167
|
+
if (requiredProfileFailure({ ...profile, failures: [failure] })) continue;
|
|
1168
|
+
diagnostic("optional tmux profile command failed");
|
|
818
1169
|
}
|
|
819
|
-
return { exitCode: result.exitCode, signalCode: result.signalCode, stderr: stderrText };
|
|
820
1170
|
}
|
|
821
1171
|
|
|
822
1172
|
export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
1173
|
+
const env = context.env ?? process.env;
|
|
1174
|
+
// Planning performs only applicability checks. It must precede both the ambient
|
|
1175
|
+
// window rename and provider refusal so inapplicable root launches reach main
|
|
1176
|
+
// unchanged, while unsupported managed launches fail before any tmux mutation.
|
|
1177
|
+
const plan = buildDefaultTmuxLaunchPlan(context);
|
|
1178
|
+
if (plan?.isPsmux) {
|
|
1179
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1180
|
+
"psmux cannot provide immutable owner identity; refusing managed session creation.\n",
|
|
1181
|
+
);
|
|
1182
|
+
throw new Error("gjc_tmux_owner_isolation_native_session_identity_unavailable");
|
|
1183
|
+
}
|
|
1184
|
+
// Direct launches inside an ambient tmux session retain their existing title
|
|
1185
|
+
// behavior, but only after managed-launch applicability was ruled out.
|
|
823
1186
|
renameExistingTmuxWindowIfNeeded(context);
|
|
1187
|
+
if (!plan) {
|
|
1188
|
+
const env = context.env ?? process.env;
|
|
1189
|
+
const tty = context.tty ?? {
|
|
1190
|
+
stdin: Boolean(process.stdin.isTTY),
|
|
1191
|
+
stdout: Boolean(process.stdout.isTTY),
|
|
1192
|
+
};
|
|
1193
|
+
const platform = context.platform ?? process.platform;
|
|
1194
|
+
const tmuxCommand = resolveGjcTmuxBinary({ platform, env }).command;
|
|
1195
|
+
const tmuxAvailable = context.tmuxAvailable ?? Bun.which(tmuxCommand) !== null;
|
|
1196
|
+
if (
|
|
1197
|
+
isExplicitTmuxRequest(context) &&
|
|
1198
|
+
parseLaunchPolicy(env) !== "direct" &&
|
|
1199
|
+
!env.TMUX &&
|
|
1200
|
+
env[GJC_TMUX_LAUNCHED_ENV] !== "1" &&
|
|
1201
|
+
isInteractiveRootLaunch(context.parsed, tty) &&
|
|
1202
|
+
!tmuxAvailable
|
|
1203
|
+
)
|
|
1204
|
+
return true;
|
|
1205
|
+
return false;
|
|
1206
|
+
}
|
|
824
1207
|
|
|
825
|
-
const
|
|
826
|
-
if (!plan) return false;
|
|
827
|
-
const env = context.env ?? process.env;
|
|
828
|
-
const spawnSync = context.spawnSync ?? defaultSpawnSync;
|
|
1208
|
+
const rawSpawnSync = context.spawnSync ?? defaultSpawnSync;
|
|
829
1209
|
const options: TmuxSpawnOptions = {
|
|
830
1210
|
cwd: plan.cwd,
|
|
831
1211
|
env,
|
|
1212
|
+
|
|
832
1213
|
stdin: "inherit",
|
|
833
1214
|
stdout: "inherit",
|
|
834
1215
|
stderr: "inherit",
|
|
835
1216
|
};
|
|
1217
|
+
const ownerIsolationProbe =
|
|
1218
|
+
context.ownerIsolationProbe ?? defaultOwnerIsolationProbe(plan, env, rawSpawnSync, context.callerCgroupReader);
|
|
1219
|
+
const spawnSync = rawSpawnSync;
|
|
836
1220
|
const attachOptions: TmuxSpawnOptions = { ...options };
|
|
837
|
-
|
|
838
|
-
//
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1221
|
+
// Psmux is rejected before planning any tmux mutation, including an ambient
|
|
1222
|
+
// TMUX window rename. Keep this defense in depth guard for future plan seams.
|
|
1223
|
+
if (plan.isPsmux) {
|
|
1224
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1225
|
+
"psmux cannot provide immutable owner identity; refusing managed session creation.\n",
|
|
1226
|
+
);
|
|
1227
|
+
throw new Error("gjc_tmux_owner_isolation_native_session_identity_unavailable");
|
|
1228
|
+
}
|
|
1229
|
+
const controlOptions: TmuxSpawnOptions = {
|
|
843
1230
|
...options,
|
|
844
1231
|
stdin: "pipe",
|
|
845
1232
|
stdout: "pipe",
|
|
846
1233
|
stderr: "pipe",
|
|
847
1234
|
captureStderr: true,
|
|
848
1235
|
};
|
|
1236
|
+
// has-session / new-session retry / profile-tagging probe share these
|
|
1237
|
+
// pipe-stdio options. Only attach-session inherits the interactive terminal.
|
|
1238
|
+
const probeOptions: TmuxSpawnOptions = {
|
|
1239
|
+
...controlOptions,
|
|
1240
|
+
};
|
|
849
1241
|
// new-session needs pipe stdio (not inherit) because the user terminal must
|
|
850
1242
|
// remain untouched until attach-session takes over. Inheriting psmux's
|
|
851
1243
|
// stdout/stderr for new-session can corrupt the terminal state or race with
|
|
@@ -867,7 +1259,7 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
867
1259
|
: undefined;
|
|
868
1260
|
const buildProfileInputs = (): GjcTmuxProfileContext => ({
|
|
869
1261
|
tmuxCommand: plan.tmuxCommand,
|
|
870
|
-
target: plan
|
|
1262
|
+
target: createdSessionTarget(plan),
|
|
871
1263
|
cwd: plan.cwd,
|
|
872
1264
|
env,
|
|
873
1265
|
spawnSync,
|
|
@@ -875,67 +1267,133 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
875
1267
|
project: plan.project,
|
|
876
1268
|
sessionId: plan.sessionId ?? null,
|
|
877
1269
|
sessionStateFile: plan.sessionStateFile ?? null,
|
|
1270
|
+
ownerGeneration: plan.ownerGeneration ?? null,
|
|
1271
|
+
ownerServerKey: plan.tmuxCommand,
|
|
878
1272
|
version: VERSION,
|
|
879
1273
|
});
|
|
880
1274
|
const probeHasSession = (): TmuxSpawnResult =>
|
|
881
|
-
spawnSync(
|
|
882
|
-
plan.tmuxCommand,
|
|
883
|
-
["has-session", "-t", buildGjcTmuxExactSessionTarget(plan.sessionName, { env })],
|
|
884
|
-
probeOptions,
|
|
885
|
-
);
|
|
1275
|
+
spawnSync(plan.tmuxCommand, ["has-session", "-t", createdSessionExactTarget(plan, env)], probeOptions);
|
|
886
1276
|
const attachCreatedSession = (): TmuxSpawnResult =>
|
|
887
|
-
spawnSync(
|
|
888
|
-
plan.tmuxCommand,
|
|
889
|
-
["attach-session", "-t", buildGjcTmuxExactSessionTarget(plan.sessionName, { env })],
|
|
890
|
-
attachOptions,
|
|
891
|
-
);
|
|
1277
|
+
spawnSync(plan.tmuxCommand, ["attach-session", "-t", createdSessionExactTarget(plan, env)], attachOptions);
|
|
892
1278
|
|
|
893
1279
|
if (plan.attachSessionName) {
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1280
|
+
let existingTarget: string;
|
|
1281
|
+
let existingProof: ReturnType<typeof proveGjcTmuxSessionMutationTarget> | undefined;
|
|
1282
|
+
if (plan.platform !== "linux") {
|
|
1283
|
+
existingTarget = buildGjcTmuxExactSessionTarget(plan.attachSessionName, { env });
|
|
1284
|
+
} else {
|
|
1285
|
+
try {
|
|
1286
|
+
existingProof = proveGjcTmuxSessionMutationTarget(plan.attachSessionName, env);
|
|
1287
|
+
existingTarget = existingProof.nativeSessionId;
|
|
1288
|
+
} catch {
|
|
1289
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1290
|
+
"tmux existing session proof failed; preserving session without mutation.\n",
|
|
1291
|
+
);
|
|
1292
|
+
return true;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
if (!plan.isPsmux)
|
|
1296
|
+
applyGjcTmuxRootTerminalTitleProfile({
|
|
1297
|
+
tmuxCommand: plan.tmuxCommand,
|
|
1298
|
+
target:
|
|
1299
|
+
plan.platform === "linux" && existingTarget.startsWith("$")
|
|
1300
|
+
? `${existingTarget}:`
|
|
1301
|
+
: buildGjcTmuxExactOptionTarget(plan.attachSessionName, { env }),
|
|
1302
|
+
sessionName: plan.attachSessionName,
|
|
1303
|
+
title: rootTerminalTitle,
|
|
1304
|
+
spawnSync,
|
|
1305
|
+
options: controlOptions,
|
|
1306
|
+
});
|
|
1307
|
+
if (plan.platform === "linux") {
|
|
1308
|
+
try {
|
|
1309
|
+
const proof = proveGjcTmuxSessionMutationTarget(plan.attachSessionName, env);
|
|
1310
|
+
if (
|
|
1311
|
+
!existingProof ||
|
|
1312
|
+
proof.nativeSessionId !== existingProof.nativeSessionId ||
|
|
1313
|
+
proof.serverPid !== existingProof.serverPid ||
|
|
1314
|
+
proof.serverStartTime !== existingProof.serverStartTime
|
|
1315
|
+
)
|
|
1316
|
+
throw new Error("tmux_session_identity_changed");
|
|
1317
|
+
} catch {
|
|
1318
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1319
|
+
"tmux existing session proof failed; preserving session without mutation.\n",
|
|
1320
|
+
);
|
|
1321
|
+
return true;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", existingTarget], attachOptions);
|
|
906
1325
|
if (attached.exitCode === 0) return true;
|
|
907
1326
|
}
|
|
908
|
-
|
|
1327
|
+
try {
|
|
1328
|
+
prepareManagedOwnerLifecycle(plan, context);
|
|
1329
|
+
} catch (error) {
|
|
1330
|
+
(context.diagnosticWriter ?? safeStderrWrite)(`tmux owner lifecycle publication failed: ${String(error)}`);
|
|
1331
|
+
return true;
|
|
1332
|
+
}
|
|
1333
|
+
if (!plan.sessionId || !plan.sessionStateFile || !plan.ownerGeneration || !plan.tmuxCommand) {
|
|
1334
|
+
(context.diagnosticWriter ?? safeStderrWrite)("tmux required ownership metadata was unavailable");
|
|
1335
|
+
return true;
|
|
1336
|
+
}
|
|
1337
|
+
const created = createIsolatedTmuxSession(
|
|
1338
|
+
plan,
|
|
1339
|
+
rawSpawnSync,
|
|
1340
|
+
newSessionOptions,
|
|
1341
|
+
context.diagnosticWriter ?? safeStderrWrite,
|
|
1342
|
+
ownerIsolationProbe,
|
|
1343
|
+
);
|
|
1344
|
+
if (created.exitCode === 0 && !plan.isPsmux && !plan.createdSessionId) {
|
|
1345
|
+
// Native tmux must atomically disclose its immutable `$N` identity. Do not
|
|
1346
|
+
// downgrade to the reusable session name or mutate the unidentified session.
|
|
1347
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1348
|
+
"gjc --tmux failed after creating tmux session: native session identity was unavailable; preserving session for recovery.\n",
|
|
1349
|
+
);
|
|
1350
|
+
return true;
|
|
1351
|
+
}
|
|
909
1352
|
if (created.exitCode === 0) {
|
|
910
|
-
//
|
|
911
|
-
//
|
|
912
|
-
//
|
|
913
|
-
// fails with "can't find session / no server running". Probe the
|
|
914
|
-
// server first, and if the race fired, retry new-session before
|
|
915
|
-
// we hand the session to rename-window / applyGjcTmuxProfile so the
|
|
916
|
-
// profile-tagging path always sees a registered session.
|
|
1353
|
+
// psmux on Windows can return before it registers its new session. Retry
|
|
1354
|
+
// only its documented missing-session registration race; other failed
|
|
1355
|
+
// proofs are preserved without cleanup or a second creation attempt.
|
|
917
1356
|
const probeResult = probeHasSession();
|
|
918
1357
|
if (probeResult.exitCode !== 0) {
|
|
919
|
-
|
|
1358
|
+
if (!isWindowsPsmuxMissingSessionRegistrationRace(plan, probeResult)) {
|
|
1359
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1360
|
+
formatTmuxLaunchDiagnostic("session registration probe failed", probeResult.stderr),
|
|
1361
|
+
);
|
|
1362
|
+
return true;
|
|
1363
|
+
}
|
|
1364
|
+
const retry = createIsolatedTmuxSession(
|
|
1365
|
+
plan,
|
|
1366
|
+
rawSpawnSync,
|
|
1367
|
+
newSessionOptions,
|
|
1368
|
+
context.diagnosticWriter ?? safeStderrWrite,
|
|
1369
|
+
ownerIsolationProbe,
|
|
1370
|
+
);
|
|
920
1371
|
const retryProbe = probeHasSession();
|
|
921
1372
|
if (retry.exitCode !== 0 || retryProbe.exitCode !== 0) {
|
|
922
1373
|
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
923
1374
|
formatTmuxLaunchDiagnostic(
|
|
924
1375
|
"new-session retry failed after missing session",
|
|
925
|
-
retry
|
|
1376
|
+
failedRetryDiagnostic(retry, retryProbe),
|
|
926
1377
|
),
|
|
927
1378
|
);
|
|
928
|
-
|
|
929
|
-
|
|
1379
|
+
cleanupCreatedTmuxSessionAfterFailure(
|
|
1380
|
+
plan,
|
|
1381
|
+
spawnSync,
|
|
1382
|
+
options,
|
|
1383
|
+
ownerIsolationProbe,
|
|
1384
|
+
new Error("new-session retry failed after missing session"),
|
|
1385
|
+
);
|
|
1386
|
+
|
|
1387
|
+
return true;
|
|
930
1388
|
}
|
|
931
1389
|
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
);
|
|
1390
|
+
if (!isCreatedTmuxSessionIdentityStable(plan, spawnSync, controlOptions, ownerIsolationProbe)) {
|
|
1391
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1392
|
+
"tmux created session proof failed; preserving session without mutation.\n",
|
|
1393
|
+
);
|
|
1394
|
+
return true;
|
|
1395
|
+
}
|
|
1396
|
+
renameTmuxWindow(plan.tmuxCommand, windowTitle, spawnSync, controlOptions, createdSessionExactTarget(plan, env));
|
|
939
1397
|
const profile = applyGjcTmuxProfile(buildProfileInputs());
|
|
940
1398
|
// If the @gjc-profile ownership write failed, the cause can be
|
|
941
1399
|
// either (a) a real psmux persistence-tag rejection (e.g.
|
|
@@ -945,34 +1403,56 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
945
1403
|
// "can't find session". Distinguish the two: re-probe; if the
|
|
946
1404
|
// session is genuinely missing, retry new-session and re-apply the
|
|
947
1405
|
// profile. Otherwise, surface the persistence-tag failure.
|
|
948
|
-
const ownershipFailure =
|
|
1406
|
+
const ownershipFailure = requiredProfileFailure(profile);
|
|
1407
|
+
emitOptionalProfileDiagnostics(profile, context.diagnosticWriter ?? safeStderrWrite);
|
|
949
1408
|
if (ownershipFailure) {
|
|
950
1409
|
const probeAfterOwnership = probeHasSession();
|
|
951
|
-
if (
|
|
952
|
-
|
|
953
|
-
|
|
1410
|
+
if (
|
|
1411
|
+
!isWindowsPsmuxMissingSessionRegistrationRace(plan, ownershipFailure) ||
|
|
1412
|
+
!isWindowsPsmuxMissingSessionRegistrationRace(plan, probeAfterOwnership)
|
|
1413
|
+
) {
|
|
954
1414
|
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
955
1415
|
formatTmuxLaunchDiagnostic("profile tagging failed", ownershipFailure.stderr),
|
|
956
1416
|
);
|
|
957
1417
|
return true;
|
|
958
1418
|
}
|
|
959
|
-
|
|
960
|
-
|
|
1419
|
+
const retry = createIsolatedTmuxSession(
|
|
1420
|
+
plan,
|
|
1421
|
+
rawSpawnSync,
|
|
1422
|
+
newSessionOptions,
|
|
1423
|
+
context.diagnosticWriter ?? safeStderrWrite,
|
|
1424
|
+
ownerIsolationProbe,
|
|
1425
|
+
);
|
|
961
1426
|
const retryProbe = probeHasSession();
|
|
962
1427
|
if (retry.exitCode !== 0 || retryProbe.exitCode !== 0) {
|
|
963
|
-
|
|
1428
|
+
cleanupCreatedTmuxSessionAfterFailure(
|
|
1429
|
+
plan,
|
|
1430
|
+
spawnSync,
|
|
1431
|
+
options,
|
|
1432
|
+
ownerIsolationProbe,
|
|
1433
|
+
new Error("new-session retry failed after ownership failure"),
|
|
1434
|
+
);
|
|
1435
|
+
|
|
964
1436
|
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
965
1437
|
formatTmuxLaunchDiagnostic(
|
|
966
1438
|
"new-session retry failed after ownership failure",
|
|
967
|
-
retry
|
|
1439
|
+
failedRetryDiagnostic(retry, retryProbe),
|
|
968
1440
|
),
|
|
969
1441
|
);
|
|
970
1442
|
return true;
|
|
971
1443
|
}
|
|
972
1444
|
const retryProfile = applyGjcTmuxProfile(buildProfileInputs());
|
|
973
|
-
const retryOwnershipFailure = retryProfile
|
|
1445
|
+
const retryOwnershipFailure = requiredProfileFailure(retryProfile);
|
|
1446
|
+
emitOptionalProfileDiagnostics(retryProfile, context.diagnosticWriter ?? safeStderrWrite);
|
|
974
1447
|
if (retryOwnershipFailure) {
|
|
975
|
-
|
|
1448
|
+
cleanupCreatedTmuxSessionAfterFailure(
|
|
1449
|
+
plan,
|
|
1450
|
+
spawnSync,
|
|
1451
|
+
options,
|
|
1452
|
+
ownerIsolationProbe,
|
|
1453
|
+
new Error("profile tagging failed after retry"),
|
|
1454
|
+
);
|
|
1455
|
+
|
|
976
1456
|
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
977
1457
|
formatTmuxLaunchDiagnostic("profile tagging failed after retry", retryOwnershipFailure.stderr),
|
|
978
1458
|
);
|
|
@@ -983,10 +1463,11 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
983
1463
|
ensureCreatedTmuxWindowTracksCallerTerminal(plan, spawnSync, controlOptions);
|
|
984
1464
|
applyGjcTmuxRootTerminalTitleProfile({
|
|
985
1465
|
tmuxCommand: plan.tmuxCommand,
|
|
986
|
-
target: plan
|
|
1466
|
+
target: createdSessionOptionTarget(plan, env),
|
|
1467
|
+
sessionName: plan.sessionName,
|
|
987
1468
|
title: rootTerminalTitle,
|
|
988
1469
|
spawnSync,
|
|
989
|
-
options,
|
|
1470
|
+
options: controlOptions,
|
|
990
1471
|
});
|
|
991
1472
|
}
|
|
992
1473
|
const probeWarning = detectCorruptedGjcWrapper();
|
|
@@ -1001,7 +1482,32 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
1001
1482
|
const stderr = created.stderr;
|
|
1002
1483
|
const suffix = probeWarning ? ` Wrapper warning: ${probeWarning}` : "";
|
|
1003
1484
|
(context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("new-session failed", stderr) + suffix);
|
|
1004
|
-
return
|
|
1485
|
+
return true;
|
|
1486
|
+
}
|
|
1487
|
+
if (!isCreatedTmuxSessionIdentityStable(plan, spawnSync, controlOptions, ownerIsolationProbe)) {
|
|
1488
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1489
|
+
"tmux created session proof failed; preserving session without attach.\n",
|
|
1490
|
+
);
|
|
1491
|
+
return true;
|
|
1492
|
+
}
|
|
1493
|
+
try {
|
|
1494
|
+
replaceOwnerGenerationSync(
|
|
1495
|
+
path.dirname(plan.sessionStateFile!),
|
|
1496
|
+
plan.sessionId!,
|
|
1497
|
+
plan.ownerGeneration!,
|
|
1498
|
+
plan.ownerGenerationBaseline!,
|
|
1499
|
+
);
|
|
1500
|
+
} catch (error) {
|
|
1501
|
+
const publicationError = error instanceof Error ? error : new Error(String(error));
|
|
1502
|
+
cleanupCreatedTmuxSessionAfterFailure(plan, spawnSync, options, ownerIsolationProbe, publicationError);
|
|
1503
|
+
(context.diagnosticWriter ?? safeStderrWrite)(`tmux owner lifecycle publication failed: ${String(error)}`);
|
|
1504
|
+
return true;
|
|
1505
|
+
}
|
|
1506
|
+
if (!isCreatedTmuxSessionIdentityStable(plan, spawnSync, controlOptions, ownerIsolationProbe)) {
|
|
1507
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1508
|
+
"tmux created session proof failed after lifecycle publication; preserving session without attach.\n",
|
|
1509
|
+
);
|
|
1510
|
+
return true;
|
|
1005
1511
|
}
|
|
1006
1512
|
// attach-session needs PTY inherit for the user-facing attach; keep it unchanged.
|
|
1007
1513
|
const attached = attachCreatedSession();
|
|
@@ -1022,8 +1528,32 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
1022
1528
|
);
|
|
1023
1529
|
return true;
|
|
1024
1530
|
}
|
|
1531
|
+
cleanupCreatedTmuxSessionAfterFailure(
|
|
1532
|
+
plan,
|
|
1533
|
+
spawnSync,
|
|
1534
|
+
options,
|
|
1535
|
+
ownerIsolationProbe,
|
|
1536
|
+
new Error("attach retry failed"),
|
|
1537
|
+
);
|
|
1538
|
+
|
|
1539
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1540
|
+
formatTmuxLaunchDiagnostic("attach retry failed", retryAttached.stderr),
|
|
1541
|
+
);
|
|
1542
|
+
return true;
|
|
1025
1543
|
} else {
|
|
1026
|
-
|
|
1544
|
+
if (!isWindowsPsmuxMissingSessionRegistrationRace(plan, probeAfterAttach)) {
|
|
1545
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1546
|
+
formatTmuxLaunchDiagnostic("attach recovery probe failed", probeAfterAttach.stderr),
|
|
1547
|
+
);
|
|
1548
|
+
return true;
|
|
1549
|
+
}
|
|
1550
|
+
const retry = createIsolatedTmuxSession(
|
|
1551
|
+
plan,
|
|
1552
|
+
rawSpawnSync,
|
|
1553
|
+
newSessionOptions,
|
|
1554
|
+
context.diagnosticWriter ?? safeStderrWrite,
|
|
1555
|
+
ownerIsolationProbe,
|
|
1556
|
+
);
|
|
1027
1557
|
const retryProbe = probeHasSession();
|
|
1028
1558
|
if (retry.exitCode === 0 && retryProbe.exitCode === 0) {
|
|
1029
1559
|
renameTmuxWindow(
|
|
@@ -1031,31 +1561,68 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
1031
1561
|
windowTitle,
|
|
1032
1562
|
spawnSync,
|
|
1033
1563
|
controlOptions,
|
|
1034
|
-
|
|
1564
|
+
createdSessionExactTarget(plan, env),
|
|
1035
1565
|
);
|
|
1036
1566
|
const retryProfile = applyGjcTmuxProfile(buildProfileInputs());
|
|
1037
|
-
const retryOwnershipFailure = retryProfile
|
|
1038
|
-
|
|
1039
|
-
)
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
applyGjcTmuxRootTerminalTitleProfile({
|
|
1043
|
-
tmuxCommand: plan.tmuxCommand,
|
|
1044
|
-
target: plan.sessionName,
|
|
1045
|
-
title: rootTerminalTitle,
|
|
1567
|
+
const retryOwnershipFailure = requiredProfileFailure(retryProfile);
|
|
1568
|
+
emitOptionalProfileDiagnostics(retryProfile, context.diagnosticWriter ?? safeStderrWrite);
|
|
1569
|
+
if (retryOwnershipFailure) {
|
|
1570
|
+
cleanupCreatedTmuxSessionAfterFailure(
|
|
1571
|
+
plan,
|
|
1046
1572
|
spawnSync,
|
|
1047
1573
|
options,
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1574
|
+
ownerIsolationProbe,
|
|
1575
|
+
new Error("profile tagging failed after retry"),
|
|
1576
|
+
);
|
|
1577
|
+
|
|
1578
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1579
|
+
formatTmuxLaunchDiagnostic("profile tagging failed after retry", retryOwnershipFailure.stderr),
|
|
1580
|
+
);
|
|
1581
|
+
return true;
|
|
1582
|
+
}
|
|
1583
|
+
ensureCreatedTmuxWindowTracksCallerTerminal(plan, spawnSync, controlOptions);
|
|
1584
|
+
applyGjcTmuxRootTerminalTitleProfile({
|
|
1585
|
+
tmuxCommand: plan.tmuxCommand,
|
|
1586
|
+
target: createdSessionOptionTarget(plan, env),
|
|
1587
|
+
sessionName: plan.sessionName,
|
|
1588
|
+
title: rootTerminalTitle,
|
|
1589
|
+
spawnSync,
|
|
1590
|
+
options: controlOptions,
|
|
1591
|
+
});
|
|
1592
|
+
const retryAttached = attachCreatedSession();
|
|
1593
|
+
if (retryAttached.exitCode === 0) return true;
|
|
1594
|
+
if (isTmuxAttachDisconnectError(retryAttached)) {
|
|
1595
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1596
|
+
formatTmuxLaunchDiagnostic("attach disconnected", retryAttached.stderr),
|
|
1597
|
+
);
|
|
1598
|
+
return true;
|
|
1057
1599
|
}
|
|
1600
|
+
cleanupCreatedTmuxSessionAfterFailure(
|
|
1601
|
+
plan,
|
|
1602
|
+
spawnSync,
|
|
1603
|
+
options,
|
|
1604
|
+
ownerIsolationProbe,
|
|
1605
|
+
new Error("attach failed after recovery"),
|
|
1606
|
+
);
|
|
1607
|
+
|
|
1608
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1609
|
+
formatTmuxLaunchDiagnostic("attach failed after recovery", retryAttached.stderr),
|
|
1610
|
+
);
|
|
1611
|
+
return true;
|
|
1058
1612
|
}
|
|
1613
|
+
cleanupCreatedTmuxSessionAfterFailure(
|
|
1614
|
+
plan,
|
|
1615
|
+
spawnSync,
|
|
1616
|
+
options,
|
|
1617
|
+
ownerIsolationProbe,
|
|
1618
|
+
new Error("attach recovery recreate failed"),
|
|
1619
|
+
);
|
|
1620
|
+
|
|
1621
|
+
const recoveryFailure = retry.exitCode !== 0 ? retry.stderr : retryProbe.stderr;
|
|
1622
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1623
|
+
formatTmuxLaunchDiagnostic("attach recovery recreate failed", recoveryFailure),
|
|
1624
|
+
);
|
|
1625
|
+
return true;
|
|
1059
1626
|
}
|
|
1060
1627
|
}
|
|
1061
1628
|
// Closing an SSH/Windows Terminal tab can make `tmux attach-session`
|
|
@@ -1073,7 +1640,7 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
1073
1640
|
return true;
|
|
1074
1641
|
}
|
|
1075
1642
|
}
|
|
1076
|
-
|
|
1643
|
+
cleanupCreatedTmuxSessionAfterFailure(plan, spawnSync, options, ownerIsolationProbe, new Error("attach failed"));
|
|
1077
1644
|
(context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach failed", attached.stderr));
|
|
1078
1645
|
return true;
|
|
1079
1646
|
}
|