@gajae-code/coding-agent 0.12.5 → 0.12.6
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 +32 -0
- package/README.md +4 -1
- package/dist/types/eval/backend.d.ts +2 -2
- package/dist/types/exec/bash-executor.d.ts +10 -8
- package/dist/types/extensibility/extensions/runner.d.ts +9 -7
- package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
- package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
- package/dist/types/gjc-runtime/team-store.d.ts +0 -4
- package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
- package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +83 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +57 -4
- package/dist/types/internal-urls/registry-helpers.d.ts +18 -0
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
- package/dist/types/modes/components/plugin-settings.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +18 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -3
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -2
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
- package/dist/types/sdk/host/host.d.ts +23 -2
- package/dist/types/sdk/host/reverse-leases.d.ts +7 -4
- package/dist/types/session/agent-session.d.ts +5 -4
- package/dist/types/session/artifacts.d.ts +8 -5
- package/dist/types/session/attempt-record-store.d.ts +19 -0
- package/dist/types/session/client-bridge.d.ts +14 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +5 -2
- package/dist/types/session/messages.d.ts +4 -1
- package/dist/types/session/session-manager.d.ts +6 -4
- package/dist/types/session/session-storage.d.ts +8 -0
- package/dist/types/session/streaming-output.d.ts +32 -0
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +178 -0
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/fetch.d.ts +3 -0
- package/dist/types/tools/output-meta.d.ts +37 -5
- package/dist/types/tools/tool-result.d.ts +1 -0
- package/dist/types/utils/artifact-id.d.ts +2 -0
- package/package.json +8 -8
- package/scripts/capture-sticky-viewport-showcase.ts +131 -5
- package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
- package/scripts/verify-sticky-viewport-showcase.ts +620 -93
- package/src/config/model-profiles.ts +8 -0
- package/src/config/settings-schema.ts +17 -3
- package/src/cursor.ts +20 -13
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +5 -2
- package/src/eval/backend.ts +2 -2
- package/src/exec/bash-executor.ts +425 -151
- package/src/extensibility/extensions/runner.ts +80 -6
- package/src/extensibility/extensions/wrapper.ts +22 -15
- package/src/extensibility/runtime-skill-discovery.ts +4 -6
- package/src/gjc-runtime/launch-tmux.ts +309 -293
- package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
- package/src/gjc-runtime/psmux-detect.ts +52 -6
- package/src/gjc-runtime/team-launch.ts +50 -2
- package/src/gjc-runtime/team-runtime.ts +716 -251
- package/src/gjc-runtime/team-store.ts +25 -7
- package/src/gjc-runtime/team-workers.ts +161 -1
- package/src/gjc-runtime/tmux-common.ts +16 -4
- package/src/gjc-runtime/tmux-gc.ts +37 -5
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
- package/src/gjc-runtime/tmux-provider-context.ts +451 -0
- package/src/gjc-runtime/tmux-sessions.ts +409 -156
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
- package/src/gjc-runtime/ultragoal-review-source.ts +378 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +640 -159
- package/src/gjc-runtime/workflow-manifest.generated.json +40 -1
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/internal-urls/agent-protocol.ts +98 -38
- package/src/internal-urls/artifact-protocol.ts +28 -17
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/internal-urls/local-protocol.ts +39 -18
- package/src/internal-urls/registry-helpers.ts +62 -0
- package/src/modes/acp/acp-agent.ts +51 -13
- package/src/modes/acp/acp-client-bridge.ts +154 -11
- package/src/modes/components/gjc-bundle-settings.ts +2 -0
- package/src/modes/components/plugin-settings.ts +5 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/monitor.md +4 -4
- package/src/sdk/acp/adapter.ts +200 -9
- package/src/sdk/broker/broker.ts +93 -10
- package/src/sdk/broker/lifecycle-ledger.ts +227 -11
- package/src/sdk/broker/lifecycle.ts +691 -92
- package/src/sdk/bus/chat-daemon-control.ts +32 -6
- package/src/sdk/bus/index.ts +347 -14
- package/src/sdk/bus/telegram-daemon-contract.ts +4 -2
- package/src/sdk/bus/telegram-daemon.ts +283 -44
- package/src/sdk/bus/topic-registry.ts +133 -10
- package/src/sdk/host/host.ts +187 -9
- package/src/sdk/host/reverse-leases.ts +249 -42
- package/src/sdk/session.ts +7 -6
- package/src/session/agent-session.ts +930 -354
- package/src/session/artifacts.ts +211 -74
- package/src/session/attempt-record-store.ts +106 -0
- package/src/session/client-bridge.ts +15 -0
- package/src/session/internal/managed-session-scope.ts +252 -67
- package/src/session/internal/managed-session-storage.ts +105 -5
- package/src/session/messages.ts +32 -7
- package/src/session/session-manager.ts +19 -21
- package/src/session/session-storage.ts +146 -24
- package/src/session/streaming-output.ts +198 -40
- package/src/task/index.ts +45 -0
- package/src/task/provider-retry-status.ts +4 -1
- package/src/task/receipt.ts +4 -0
- package/src/task/render.ts +30 -0
- package/src/task/types.ts +34 -0
- package/src/tools/bash.ts +818 -239
- package/src/tools/eval.ts +99 -4
- package/src/tools/fetch.ts +77 -15
- package/src/tools/find.ts +7 -3
- package/src/tools/monitor.ts +7 -1
- package/src/tools/output-meta.ts +706 -116
- package/src/tools/search.ts +9 -4
- package/src/tools/tool-result.ts +1 -0
- package/src/utils/artifact-id.ts +6 -0
|
@@ -19,16 +19,15 @@ import {
|
|
|
19
19
|
} from "./managed-owner-supervisor";
|
|
20
20
|
import { tmuxRuntimeSessionPath } from "./session-layout";
|
|
21
21
|
import {
|
|
22
|
-
GJC_COORDINATOR_SESSION_BRANCH_ENV,
|
|
23
22
|
GJC_COORDINATOR_SESSION_ID_ENV,
|
|
24
|
-
GJC_COORDINATOR_SESSION_LAUNCH_ID_ENV,
|
|
25
|
-
GJC_COORDINATOR_SESSION_READINESS_FILE_ENV,
|
|
26
23
|
GJC_COORDINATOR_SESSION_STATE_FILE_ENV,
|
|
27
24
|
GJC_TMUX_OWNER_GENERATION_ENV,
|
|
28
25
|
GJC_TMUX_OWNER_SERVER_KEY_ENV,
|
|
29
26
|
GJC_TMUX_OWNER_STATE_DIR_ENV,
|
|
30
27
|
} from "./session-state-sidecar";
|
|
31
28
|
import {
|
|
29
|
+
assertGjcTmuxMutationAuthoritySync,
|
|
30
|
+
bindGjcTmuxProviderAuthority,
|
|
32
31
|
buildGjcTmuxExactOptionTarget,
|
|
33
32
|
buildGjcTmuxExactSessionTarget,
|
|
34
33
|
buildGjcTmuxProfileCommands,
|
|
@@ -41,8 +40,12 @@ import {
|
|
|
41
40
|
GJC_TMUX_PROFILE_ENV,
|
|
42
41
|
GJC_TMUX_SESSION_PREFIX,
|
|
43
42
|
type GjcTmuxProfileCommand,
|
|
43
|
+
type ProviderAuthority,
|
|
44
|
+
persistGjcTmuxProviderAuthoritySync,
|
|
45
|
+
readGjcTmuxProviderAuthoritySync,
|
|
44
46
|
resolveGjcTmuxBinary,
|
|
45
47
|
resolveGjcTmuxCommand,
|
|
48
|
+
resolveGjcTmuxProviderContext,
|
|
46
49
|
} from "./tmux-common";
|
|
47
50
|
import {
|
|
48
51
|
captureOwnerGenerationBaselineSync,
|
|
@@ -50,16 +53,20 @@ import {
|
|
|
50
53
|
executeTmuxOwnerIsolationPlanSync,
|
|
51
54
|
isOwnerGenerationBaselineCurrentSync,
|
|
52
55
|
lifecyclePaths,
|
|
56
|
+
type ManagedOwnerPredecessorEvidence,
|
|
57
|
+
type OwnerGenerationBaseline,
|
|
53
58
|
type OwnerIsolationProbeSync,
|
|
54
59
|
planTmuxOwnerIsolationSync,
|
|
55
60
|
replaceOwnerGenerationSync,
|
|
56
61
|
resolveManagedOwnerPredecessorSync,
|
|
57
62
|
type TmuxServerProof,
|
|
58
63
|
} from "./tmux-owner-isolation";
|
|
64
|
+
import { assertGjcTmuxStagedMutationAuthoritySync } from "./tmux-provider-context";
|
|
59
65
|
import {
|
|
60
66
|
findGjcTmuxSessionByName,
|
|
61
67
|
findGjcTmuxSessionByScope,
|
|
62
68
|
type GjcTmuxSessionStatus,
|
|
69
|
+
type ProvenTmuxSessionIdentity,
|
|
63
70
|
proveGjcTmuxSessionMutationTarget,
|
|
64
71
|
} from "./tmux-sessions";
|
|
65
72
|
import {
|
|
@@ -85,6 +92,7 @@ export const GJC_LAUNCH_POLICY_ENV = "GJC_LAUNCH_POLICY";
|
|
|
85
92
|
export const GJC_TMUX_WINDOW_LABEL_MAX_WIDTH = 48;
|
|
86
93
|
|
|
87
94
|
const WINDOWS_PSMUX_ATTACH_RETRY_DELAY_MS = 100;
|
|
95
|
+
const GJC_TMUX_PSMUX_INCARNATION_OPTION = "@gjc-psmux-incarnation";
|
|
88
96
|
const TERMINAL_TITLE_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
89
97
|
|
|
90
98
|
type LaunchPolicy = "direct" | "tmux";
|
|
@@ -106,6 +114,23 @@ export interface TmuxLaunchContext {
|
|
|
106
114
|
platform?: NodeJS.Platform;
|
|
107
115
|
tty?: TtyState;
|
|
108
116
|
spawnSync?: TmuxSpawnSync;
|
|
117
|
+
/**
|
|
118
|
+
* Provider authority boundaries. Production resolves, persists, and asserts
|
|
119
|
+
* the durable psmux authority; tests may inject deterministic equivalents.
|
|
120
|
+
*/
|
|
121
|
+
providerAuthorityResolver?: (input: {
|
|
122
|
+
platform: NodeJS.Platform;
|
|
123
|
+
env: NodeJS.ProcessEnv;
|
|
124
|
+
command: string;
|
|
125
|
+
stateDir: string;
|
|
126
|
+
sessionId: string;
|
|
127
|
+
generation: string;
|
|
128
|
+
}) => ProviderAuthority;
|
|
129
|
+
providerAuthorityPersist?: (authority: ProviderAuthority) => void;
|
|
130
|
+
/** Re-proves an immutable staged authority before generation publication. */
|
|
131
|
+
providerAuthorityStagedAssert?: (authority: ProviderAuthority) => void;
|
|
132
|
+
/** Re-proves a current authority after generation publication. */
|
|
133
|
+
providerAuthorityAssert?: (authority: ProviderAuthority) => void;
|
|
109
134
|
tmuxAvailable?: boolean;
|
|
110
135
|
tmuxStatusLines?: number;
|
|
111
136
|
worktreeBranch?: string | null;
|
|
@@ -168,12 +193,13 @@ export interface TmuxLaunchPlan {
|
|
|
168
193
|
ownerRunId?: string;
|
|
169
194
|
ownerIncarnation?: string;
|
|
170
195
|
/** Generation state captured before owner-isolation planning; required for publication CAS. */
|
|
171
|
-
ownerGenerationBaseline?:
|
|
196
|
+
ownerGenerationBaseline?: OwnerGenerationBaseline;
|
|
172
197
|
/** Native tmux session identity emitted atomically by `new-session -P -F`. */
|
|
173
198
|
createdSessionId?: string;
|
|
174
199
|
/** Safe server identity proven immediately after creation. */
|
|
175
200
|
createdServerIdentity?: { pid: number; startTime: string; pidProven?: boolean };
|
|
176
201
|
isPsmux: boolean;
|
|
202
|
+
authority?: ProviderAuthority;
|
|
177
203
|
platform: NodeJS.Platform;
|
|
178
204
|
}
|
|
179
205
|
|
|
@@ -189,108 +215,6 @@ function allowsExistingTmuxAttach(parsed: Args, env: NodeJS.ProcessEnv): boolean
|
|
|
189
215
|
// value-less resume can show the session picker and valued resume can honor the target.
|
|
190
216
|
return Boolean(parsed.continue || explicitTmuxSessionName(env));
|
|
191
217
|
}
|
|
192
|
-
type WindowsPsmuxCompatibilityState = "fresh" | "continuation" | "managed";
|
|
193
|
-
|
|
194
|
-
function windowsPsmuxCompatibilityState(plan: TmuxLaunchPlan, env: NodeJS.ProcessEnv): WindowsPsmuxCompatibilityState {
|
|
195
|
-
const marker = (value: string | undefined): boolean => Boolean(value?.trim());
|
|
196
|
-
if (
|
|
197
|
-
marker(env.GJC_SESSION_ID) ||
|
|
198
|
-
marker(env[GJC_COORDINATOR_SESSION_ID_ENV]) ||
|
|
199
|
-
marker(env[GJC_COORDINATOR_SESSION_BRANCH_ENV]) ||
|
|
200
|
-
marker(env[GJC_COORDINATOR_SESSION_LAUNCH_ID_ENV]) ||
|
|
201
|
-
marker(env[GJC_COORDINATOR_SESSION_READINESS_FILE_ENV]) ||
|
|
202
|
-
marker(env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]) ||
|
|
203
|
-
marker(env[GJC_TMUX_ACTIVE_SESSION_ENV]) ||
|
|
204
|
-
marker(env[GJC_TMUX_OWNER_GENERATION_ENV]) ||
|
|
205
|
-
marker(env[GJC_TMUX_OWNER_STATE_DIR_ENV]) ||
|
|
206
|
-
marker(env[GJC_TMUX_OWNER_SERVER_KEY_ENV]) ||
|
|
207
|
-
Object.entries(env).some(([key, value]) => key.startsWith("GJC_TEAM_") && marker(value))
|
|
208
|
-
)
|
|
209
|
-
return "managed";
|
|
210
|
-
return plan.attachSessionName ? "continuation" : "fresh";
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
type PsmuxSessionInventory =
|
|
214
|
-
| { kind: "available"; names: ReadonlySet<string> }
|
|
215
|
-
| { kind: "no-server" }
|
|
216
|
-
| { kind: "unverifiable"; result: TmuxSpawnResult };
|
|
217
|
-
|
|
218
|
-
function classifyPsmuxSessionInventory(result: TmuxSpawnResult): PsmuxSessionInventory {
|
|
219
|
-
if (result.exitCode === 0) {
|
|
220
|
-
const names = new Set(
|
|
221
|
-
(result.stdout ?? "")
|
|
222
|
-
.split(/\r?\n/)
|
|
223
|
-
.map(name => name.trim())
|
|
224
|
-
.filter(Boolean),
|
|
225
|
-
);
|
|
226
|
-
return { kind: "available", names };
|
|
227
|
-
}
|
|
228
|
-
const stderr = result.stderr?.trim() ?? "";
|
|
229
|
-
if (/\bno server running\b/i.test(stderr)) return { kind: "no-server" };
|
|
230
|
-
return { kind: "unverifiable", result };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function launchWindowsPsmuxCompatibilitySession(
|
|
234
|
-
plan: TmuxLaunchPlan,
|
|
235
|
-
env: NodeJS.ProcessEnv,
|
|
236
|
-
spawnSync: TmuxSpawnSync,
|
|
237
|
-
diagnostic: (message: string) => void,
|
|
238
|
-
): boolean {
|
|
239
|
-
const options: TmuxSpawnOptions = {
|
|
240
|
-
cwd: plan.cwd,
|
|
241
|
-
env,
|
|
242
|
-
stdin: "pipe",
|
|
243
|
-
stdout: "pipe",
|
|
244
|
-
stderr: "pipe",
|
|
245
|
-
captureStderr: true,
|
|
246
|
-
};
|
|
247
|
-
const attachOptions: TmuxSpawnOptions = { ...options, stdin: "inherit", stdout: "inherit", stderr: "inherit" };
|
|
248
|
-
const state = windowsPsmuxCompatibilityState(plan, env);
|
|
249
|
-
if (state === "managed") {
|
|
250
|
-
diagnostic("psmux cannot provide immutable owner identity; refusing managed session creation.\n");
|
|
251
|
-
throw new Error("gjc_tmux_owner_isolation_native_session_identity_unavailable");
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const targetName = plan.attachSessionName ?? plan.sessionName;
|
|
255
|
-
const inventory = (): PsmuxSessionInventory =>
|
|
256
|
-
classifyPsmuxSessionInventory(spawnSync(plan.tmuxCommand, ["list-sessions", "-F", "#{session_name}"], options));
|
|
257
|
-
|
|
258
|
-
if (state === "continuation") {
|
|
259
|
-
const existing = inventory();
|
|
260
|
-
const detail = existing.kind === "unverifiable" ? existing.result.stderr : undefined;
|
|
261
|
-
diagnostic(
|
|
262
|
-
formatTmuxLaunchDiagnostic(
|
|
263
|
-
existing.kind === "available" && existing.names.has(targetName)
|
|
264
|
-
? "existing psmux session is name-only and cannot be attached safely"
|
|
265
|
-
: "existing session target not found",
|
|
266
|
-
detail,
|
|
267
|
-
),
|
|
268
|
-
);
|
|
269
|
-
return true;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const before = inventory();
|
|
273
|
-
if (before.kind === "unverifiable") {
|
|
274
|
-
diagnostic(formatTmuxLaunchDiagnostic("fresh session inventory failed", before.result.stderr));
|
|
275
|
-
return true;
|
|
276
|
-
}
|
|
277
|
-
if (before.kind === "available" && before.names.has(targetName)) {
|
|
278
|
-
diagnostic("tmux fresh session target already exists; preserving session without mutation.\n");
|
|
279
|
-
return true;
|
|
280
|
-
}
|
|
281
|
-
const detachedIndex = plan.newSessionArgs.indexOf("-d");
|
|
282
|
-
const foregroundArgs =
|
|
283
|
-
detachedIndex < 0
|
|
284
|
-
? plan.newSessionArgs
|
|
285
|
-
: [...plan.newSessionArgs.slice(0, detachedIndex), ...plan.newSessionArgs.slice(detachedIndex + 1)];
|
|
286
|
-
const created = spawnSync(plan.tmuxCommand, foregroundArgs, attachOptions);
|
|
287
|
-
if (created.exitCode !== 0) {
|
|
288
|
-
const wrapperWarning = detectCorruptedGjcWrapper();
|
|
289
|
-
const suffix = wrapperWarning ? ` Wrapper warning: ${wrapperWarning}` : "";
|
|
290
|
-
diagnostic(formatTmuxLaunchDiagnostic("foreground new-session failed", created.stderr) + suffix);
|
|
291
|
-
}
|
|
292
|
-
return true;
|
|
293
|
-
}
|
|
294
218
|
|
|
295
219
|
function findExistingSessionForLaunch(context: {
|
|
296
220
|
env: NodeJS.ProcessEnv;
|
|
@@ -323,6 +247,7 @@ export interface GjcTmuxProfileContext {
|
|
|
323
247
|
ownerGeneration?: string | null;
|
|
324
248
|
ownerServerKey?: string | null;
|
|
325
249
|
version?: string | null;
|
|
250
|
+
psmuxIncarnation?: string | null;
|
|
326
251
|
}
|
|
327
252
|
|
|
328
253
|
function tmuxExitMarkerPath(sessionStateFile: string): string {
|
|
@@ -497,6 +422,11 @@ export function applyGjcTmuxProfile(context: GjcTmuxProfileContext): GjcTmuxProf
|
|
|
497
422
|
},
|
|
498
423
|
{ tmuxCommand: context.tmuxCommand },
|
|
499
424
|
);
|
|
425
|
+
if (context.psmuxIncarnation)
|
|
426
|
+
commands.push({
|
|
427
|
+
description: "record psmux incarnation",
|
|
428
|
+
args: ["set-option", "-t", context.target, GJC_TMUX_PSMUX_INCARNATION_OPTION, context.psmuxIncarnation],
|
|
429
|
+
});
|
|
500
430
|
if (commands.length === 0) return { skipped: true, commands: [], failures: [] };
|
|
501
431
|
const spawnSync = context.spawnSync ?? defaultSpawnSync;
|
|
502
432
|
const cwd = context.cwd ?? process.cwd();
|
|
@@ -556,8 +486,8 @@ function resolveCurrentGjcCommand(context: CommandResolutionContext): string[] {
|
|
|
556
486
|
function isWindowsPlatform(platform: NodeJS.Platform | undefined): boolean {
|
|
557
487
|
return platform === "win32";
|
|
558
488
|
}
|
|
559
|
-
function pathModuleForPlatform(platform: NodeJS.Platform | undefined): typeof path.win32 | typeof path {
|
|
560
|
-
return isWindowsPlatform(platform) ? path.win32 : path;
|
|
489
|
+
function pathModuleForPlatform(platform: NodeJS.Platform | undefined): typeof path.win32 | typeof path.posix {
|
|
490
|
+
return isWindowsPlatform(platform) ? path.win32 : path.posix;
|
|
561
491
|
}
|
|
562
492
|
|
|
563
493
|
function buildInnerCommand(context: CommandResolutionContext, rawArgs: string[]): string {
|
|
@@ -768,12 +698,15 @@ function readCurrentBranch(cwd: string): string | null {
|
|
|
768
698
|
}
|
|
769
699
|
}
|
|
770
700
|
|
|
771
|
-
function createdSessionTarget(plan: TmuxLaunchPlan): string {
|
|
772
|
-
return plan.createdSessionId ?? plan.sessionName;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
701
|
function createdSessionExactTarget(plan: TmuxLaunchPlan, env: NodeJS.ProcessEnv): string {
|
|
776
|
-
return
|
|
702
|
+
return (
|
|
703
|
+
plan.createdSessionId ??
|
|
704
|
+
buildGjcTmuxExactSessionTarget(plan.sessionName, {
|
|
705
|
+
env,
|
|
706
|
+
platform: plan.platform,
|
|
707
|
+
binary: { command: plan.tmuxCommand, isPsmux: plan.isPsmux, viaExplicitOverride: true },
|
|
708
|
+
})
|
|
709
|
+
);
|
|
777
710
|
}
|
|
778
711
|
|
|
779
712
|
function createdSessionOptionTarget(plan: TmuxLaunchPlan, env: NodeJS.ProcessEnv): string {
|
|
@@ -781,7 +714,11 @@ function createdSessionOptionTarget(plan: TmuxLaunchPlan, env: NodeJS.ProcessEnv
|
|
|
781
714
|
// identity (with its required empty-window suffix) for every later option
|
|
782
715
|
// mutation; a reusable session name could resolve to a different session.
|
|
783
716
|
if (plan.createdSessionId) return `${plan.createdSessionId}:`;
|
|
784
|
-
return buildGjcTmuxExactOptionTarget(plan.sessionName, {
|
|
717
|
+
return buildGjcTmuxExactOptionTarget(plan.sessionName, {
|
|
718
|
+
env,
|
|
719
|
+
platform: plan.platform,
|
|
720
|
+
binary: { command: plan.tmuxCommand, isPsmux: plan.isPsmux, viaExplicitOverride: true },
|
|
721
|
+
});
|
|
785
722
|
}
|
|
786
723
|
|
|
787
724
|
function cleanupCreatedTmuxSession(
|
|
@@ -790,7 +727,9 @@ function cleanupCreatedTmuxSession(
|
|
|
790
727
|
options: TmuxSpawnOptions,
|
|
791
728
|
probe: OwnerIsolationProbeSync,
|
|
792
729
|
): void {
|
|
793
|
-
|
|
730
|
+
// psmux does not disclose an immutable session ID. Never turn its reusable
|
|
731
|
+
// name readback into a destructive cleanup target.
|
|
732
|
+
if (plan.isPsmux || !isCreatedTmuxSessionIdentityStable(plan, spawnSync, options, probe))
|
|
794
733
|
throw new Error("gjc_tmux_exact_cleanup_uncertain");
|
|
795
734
|
const nativeSessionId = plan.createdSessionId!;
|
|
796
735
|
// Emit the `#{pid}` clause only when the server proof proved a PID. Non-Linux
|
|
@@ -815,29 +754,50 @@ function cleanupCreatedTmuxSession(
|
|
|
815
754
|
if (guarded.exitCode !== 0 || guarded.stdout?.trim() !== "__gjc_tmux_guarded_cleanup_ok__")
|
|
816
755
|
throw new Error("gjc_tmux_exact_cleanup_uncertain");
|
|
817
756
|
}
|
|
818
|
-
|
|
819
|
-
function cleanupCreatedTmuxSessionAfterFailure(
|
|
757
|
+
function cleanupCreatedTmuxSessionBeforePublicationFailure(
|
|
820
758
|
plan: TmuxLaunchPlan,
|
|
821
759
|
spawnSync: TmuxSpawnSync,
|
|
822
760
|
options: TmuxSpawnOptions,
|
|
823
761
|
probe: OwnerIsolationProbeSync,
|
|
824
|
-
primary: Error,
|
|
825
762
|
): void {
|
|
763
|
+
// Generation publication is the commit point. Before it, clean up only when
|
|
764
|
+
// the immutable session and server proof still identify our provisional
|
|
765
|
+
// session; a failed proof must preserve it for recovery.
|
|
766
|
+
if (!isCreatedTmuxSessionIdentityStable(plan, spawnSync, options, probe)) return;
|
|
826
767
|
try {
|
|
827
768
|
cleanupCreatedTmuxSession(plan, spawnSync, options, probe);
|
|
828
|
-
} catch
|
|
829
|
-
|
|
769
|
+
} catch {
|
|
770
|
+
// The guarded command either refused or its proof changed after the
|
|
771
|
+
// preflight. In both cases preserving the session is safer than retrying.
|
|
830
772
|
}
|
|
831
773
|
}
|
|
832
774
|
|
|
775
|
+
function cleanupCreatedTmuxSessionAfterFailure(
|
|
776
|
+
plan: TmuxLaunchPlan,
|
|
777
|
+
spawnSync: TmuxSpawnSync,
|
|
778
|
+
options: TmuxSpawnOptions,
|
|
779
|
+
probe: OwnerIsolationProbeSync,
|
|
780
|
+
): void {
|
|
781
|
+
cleanupCreatedTmuxSessionBeforePublicationFailure(plan, spawnSync, options, probe);
|
|
782
|
+
}
|
|
783
|
+
|
|
833
784
|
function isCreatedTmuxSessionIdentityStable(
|
|
834
785
|
plan: TmuxLaunchPlan,
|
|
835
786
|
spawnSync: TmuxSpawnSync,
|
|
836
787
|
options: TmuxSpawnOptions,
|
|
837
788
|
probe: OwnerIsolationProbeSync,
|
|
838
789
|
): boolean {
|
|
839
|
-
if (
|
|
790
|
+
if (!plan.createdServerIdentity) return false;
|
|
840
791
|
try {
|
|
792
|
+
if (plan.isPsmux) {
|
|
793
|
+
const binding = spawnSync(
|
|
794
|
+
plan.tmuxCommand,
|
|
795
|
+
["display-message", "-p", "-t", createdSessionExactTarget(plan, options.env), "#{session_name}"],
|
|
796
|
+
options,
|
|
797
|
+
);
|
|
798
|
+
return binding.exitCode === 0 && binding.stdout?.trim() === plan.sessionName;
|
|
799
|
+
}
|
|
800
|
+
if (!plan.createdSessionId) return false;
|
|
841
801
|
const before = probe.probeServer(plan.tmuxCommand);
|
|
842
802
|
if (
|
|
843
803
|
before.state !== "safe" ||
|
|
@@ -876,7 +836,7 @@ function isWindowsPsmuxMissingSessionRegistrationRace(
|
|
|
876
836
|
result: { exitCode?: number | null; stderr?: string },
|
|
877
837
|
): boolean {
|
|
878
838
|
if (plan.platform !== "win32" || !plan.isPsmux || result.exitCode === 0) return false;
|
|
879
|
-
return result.stderr?.trim() === `psmux: can't find session '
|
|
839
|
+
return result.stderr?.trim() === `psmux: can't find session '${plan.sessionName}' (no server running)`;
|
|
880
840
|
}
|
|
881
841
|
function waitForWindowsPsmuxAttachRetry(): void {
|
|
882
842
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, WINDOWS_PSMUX_ATTACH_RETRY_DELAY_MS);
|
|
@@ -900,6 +860,7 @@ function parseTmuxStatusLineCount(value: string): number {
|
|
|
900
860
|
}
|
|
901
861
|
|
|
902
862
|
function readTmuxStatusLineCount(tmuxCommand: string, cwd: string, env: NodeJS.ProcessEnv): number {
|
|
863
|
+
if (resolveGjcTmuxBinary({ env }).isPsmux) return 0;
|
|
903
864
|
const result = Bun.spawnSync([tmuxCommand, "show-options", "-gqv", "status"], {
|
|
904
865
|
cwd,
|
|
905
866
|
env,
|
|
@@ -998,11 +959,13 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
998
959
|
const existingSessionName = allowsExistingTmuxAttach(context.parsed, env)
|
|
999
960
|
? "existingBranchSessionName" in context
|
|
1000
961
|
? (context.existingBranchSessionName ?? undefined)
|
|
1001
|
-
:
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
962
|
+
: resolvedBinary.isPsmux && platform === "win32"
|
|
963
|
+
? explicitTmuxSessionName(env)
|
|
964
|
+
: findExistingSessionForLaunch({
|
|
965
|
+
env,
|
|
966
|
+
project,
|
|
967
|
+
branch,
|
|
968
|
+
})
|
|
1006
969
|
: undefined;
|
|
1007
970
|
const innerCommand = buildInnerCommand(
|
|
1008
971
|
{
|
|
@@ -1057,8 +1020,8 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
1057
1020
|
function trustedReplacementAuthority(
|
|
1058
1021
|
stateDir: string,
|
|
1059
1022
|
sessionId: string,
|
|
1060
|
-
baseline:
|
|
1061
|
-
):
|
|
1023
|
+
baseline: OwnerGenerationBaseline,
|
|
1024
|
+
): ManagedOwnerPredecessorEvidence | undefined {
|
|
1062
1025
|
return resolveManagedOwnerPredecessorSync(stateDir, sessionId, baseline);
|
|
1063
1026
|
}
|
|
1064
1027
|
|
|
@@ -1284,7 +1247,7 @@ function createIsolatedTmuxSession(
|
|
|
1284
1247
|
startTime: server.startTime!,
|
|
1285
1248
|
pidProven: server.pidProven,
|
|
1286
1249
|
};
|
|
1287
|
-
cleanupCreatedTmuxSessionAfterFailure(plan, spawn, options, probe
|
|
1250
|
+
cleanupCreatedTmuxSessionAfterFailure(plan, spawn, options, probe);
|
|
1288
1251
|
},
|
|
1289
1252
|
});
|
|
1290
1253
|
// A failed planned spawn is the new-session failure. Let its established
|
|
@@ -1311,6 +1274,7 @@ function requiredProfileFailure(profile: GjcTmuxProfileResult): GjcTmuxProfileRe
|
|
|
1311
1274
|
"@gjc-session-state-file",
|
|
1312
1275
|
"@gjc-owner-generation",
|
|
1313
1276
|
"@gjc-owner-server-key",
|
|
1277
|
+
GJC_TMUX_PSMUX_INCARNATION_OPTION,
|
|
1314
1278
|
]);
|
|
1315
1279
|
return profile.failures.find(item => requiredOptions.has(String(item.command.args[item.command.args.length - 2])));
|
|
1316
1280
|
}
|
|
@@ -1332,13 +1296,6 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
1332
1296
|
const ambientProvider = resolveGjcTmuxBinary({ platform: "win32", env });
|
|
1333
1297
|
if (ambientProvider.isPsmux) return false;
|
|
1334
1298
|
}
|
|
1335
|
-
if (plan?.isPsmux && plan.platform === "win32" && !env.TMUX)
|
|
1336
|
-
return launchWindowsPsmuxCompatibilitySession(
|
|
1337
|
-
plan,
|
|
1338
|
-
env,
|
|
1339
|
-
context.spawnSync ?? defaultSpawnSync,
|
|
1340
|
-
context.diagnosticWriter ?? safeStderrWrite,
|
|
1341
|
-
);
|
|
1342
1299
|
// Direct launches inside an ambient tmux session retain their existing title
|
|
1343
1300
|
// behavior, but only after managed-launch applicability was ruled out.
|
|
1344
1301
|
renameExistingTmuxWindowIfNeeded(context);
|
|
@@ -1364,17 +1321,87 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
1364
1321
|
}
|
|
1365
1322
|
|
|
1366
1323
|
const rawSpawnSync = context.spawnSync ?? defaultSpawnSync;
|
|
1324
|
+
if (plan.isPsmux && plan.platform === "win32") {
|
|
1325
|
+
try {
|
|
1326
|
+
if (plan.attachSessionName) {
|
|
1327
|
+
const stateDir = env[GJC_TMUX_OWNER_STATE_DIR_ENV]?.trim();
|
|
1328
|
+
const sessionId = env[GJC_COORDINATOR_SESSION_ID_ENV]?.trim();
|
|
1329
|
+
const generation = env[GJC_TMUX_OWNER_GENERATION_ENV]?.trim();
|
|
1330
|
+
if (!stateDir || !sessionId || !generation) throw new Error("gjc_tmux_provider_authority_unavailable");
|
|
1331
|
+
plan.authority = readGjcTmuxProviderAuthoritySync({ stateDir, sessionId, generation });
|
|
1332
|
+
} else {
|
|
1333
|
+
prepareManagedOwnerLifecycle(plan, context);
|
|
1334
|
+
if (!plan.sessionId || !plan.ownerGeneration || !plan.sessionStateFile)
|
|
1335
|
+
throw new Error("gjc_tmux_provider_authority_missing_lifecycle_identity");
|
|
1336
|
+
const stateDir = path.dirname(plan.sessionStateFile);
|
|
1337
|
+
const previousAuthority =
|
|
1338
|
+
plan.ownerGenerationBaseline?.state === "current"
|
|
1339
|
+
? readGjcTmuxProviderAuthoritySync({
|
|
1340
|
+
stateDir,
|
|
1341
|
+
sessionId: plan.sessionId,
|
|
1342
|
+
generation: plan.ownerGenerationBaseline.generation,
|
|
1343
|
+
})
|
|
1344
|
+
: undefined;
|
|
1345
|
+
plan.authority = previousAuthority
|
|
1346
|
+
? bindGjcTmuxProviderAuthority(previousAuthority, {
|
|
1347
|
+
stateDir,
|
|
1348
|
+
sessionId: plan.sessionId,
|
|
1349
|
+
generation: plan.ownerGeneration,
|
|
1350
|
+
})
|
|
1351
|
+
: (
|
|
1352
|
+
context.providerAuthorityResolver ??
|
|
1353
|
+
(input =>
|
|
1354
|
+
bindGjcTmuxProviderAuthority(
|
|
1355
|
+
resolveGjcTmuxProviderContext({
|
|
1356
|
+
platform: input.platform,
|
|
1357
|
+
env: input.env,
|
|
1358
|
+
binary: { command: input.command, isPsmux: true, viaExplicitOverride: true },
|
|
1359
|
+
}),
|
|
1360
|
+
input,
|
|
1361
|
+
))
|
|
1362
|
+
)({
|
|
1363
|
+
platform: plan.platform,
|
|
1364
|
+
env,
|
|
1365
|
+
command: plan.tmuxCommand,
|
|
1366
|
+
stateDir,
|
|
1367
|
+
sessionId: plan.sessionId,
|
|
1368
|
+
generation: plan.ownerGeneration,
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
plan.tmuxCommand = plan.authority.command;
|
|
1372
|
+
if (!plan.attachSessionName) {
|
|
1373
|
+
(context.providerAuthorityPersist ?? persistGjcTmuxProviderAuthoritySync)(plan.authority);
|
|
1374
|
+
(context.providerAuthorityStagedAssert ?? assertGjcTmuxStagedMutationAuthoritySync)(plan.authority);
|
|
1375
|
+
}
|
|
1376
|
+
} catch (error) {
|
|
1377
|
+
(context.diagnosticWriter ?? safeStderrWrite)(`tmux provider authority resolution failed: ${String(error)}`);
|
|
1378
|
+
return true;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
let providerAuthorityPublished = !plan.authority || Boolean(plan.attachSessionName);
|
|
1382
|
+
const spawnSync: TmuxSpawnSync = (command, args, options) => {
|
|
1383
|
+
const authority = plan.authority;
|
|
1384
|
+
if (!authority) return rawSpawnSync(command, args, options);
|
|
1385
|
+
const assertAuthority = providerAuthorityPublished
|
|
1386
|
+
? (context.providerAuthorityAssert ?? assertGjcTmuxMutationAuthoritySync)
|
|
1387
|
+
: (context.providerAuthorityStagedAssert ?? assertGjcTmuxStagedMutationAuthoritySync);
|
|
1388
|
+
assertAuthority(authority);
|
|
1389
|
+
try {
|
|
1390
|
+
return rawSpawnSync(command, [...authority.commandPrefix, ...args], options);
|
|
1391
|
+
} finally {
|
|
1392
|
+
assertAuthority(authority);
|
|
1393
|
+
}
|
|
1394
|
+
};
|
|
1395
|
+
const creationSpawn = plan.authority ? spawnSync : rawSpawnSync;
|
|
1367
1396
|
const options: TmuxSpawnOptions = {
|
|
1368
1397
|
cwd: plan.cwd,
|
|
1369
1398
|
env,
|
|
1370
|
-
|
|
1371
1399
|
stdin: "inherit",
|
|
1372
1400
|
stdout: "inherit",
|
|
1373
1401
|
stderr: "inherit",
|
|
1374
1402
|
};
|
|
1375
1403
|
const ownerIsolationProbe =
|
|
1376
|
-
context.ownerIsolationProbe ?? defaultOwnerIsolationProbe(plan, env,
|
|
1377
|
-
const spawnSync = rawSpawnSync;
|
|
1404
|
+
context.ownerIsolationProbe ?? defaultOwnerIsolationProbe(plan, env, spawnSync, context.callerCgroupReader);
|
|
1378
1405
|
const attachOptions: TmuxSpawnOptions = { ...options };
|
|
1379
1406
|
const controlOptions: TmuxSpawnOptions = {
|
|
1380
1407
|
...options,
|
|
@@ -1409,7 +1436,6 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
1409
1436
|
: undefined;
|
|
1410
1437
|
const buildProfileInputs = (): GjcTmuxProfileContext => ({
|
|
1411
1438
|
tmuxCommand: plan.tmuxCommand,
|
|
1412
|
-
target: createdSessionTarget(plan),
|
|
1413
1439
|
cwd: plan.cwd,
|
|
1414
1440
|
env,
|
|
1415
1441
|
spawnSync,
|
|
@@ -1420,7 +1446,54 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
1420
1446
|
ownerGeneration: plan.ownerGeneration ?? null,
|
|
1421
1447
|
ownerServerKey: plan.tmuxCommand,
|
|
1422
1448
|
version: VERSION,
|
|
1449
|
+
psmuxIncarnation: plan.isPsmux ? (plan.ownerIncarnation ?? null) : null,
|
|
1450
|
+
target: createdSessionOptionTarget(plan, env),
|
|
1423
1451
|
});
|
|
1452
|
+
const hasExactRequiredPsmuxMetadata = (): boolean => {
|
|
1453
|
+
if (!plan.isPsmux || !plan.authority) return true;
|
|
1454
|
+
const required = [
|
|
1455
|
+
...buildGjcTmuxProfileCommands(createdSessionOptionTarget(plan, env), env, {
|
|
1456
|
+
sessionId: plan.sessionId,
|
|
1457
|
+
sessionStateFile: plan.sessionStateFile,
|
|
1458
|
+
ownerGeneration: plan.ownerGeneration,
|
|
1459
|
+
ownerServerKey: plan.tmuxCommand,
|
|
1460
|
+
version: VERSION,
|
|
1461
|
+
}),
|
|
1462
|
+
...(plan.ownerIncarnation
|
|
1463
|
+
? [
|
|
1464
|
+
{
|
|
1465
|
+
description: "record psmux incarnation",
|
|
1466
|
+
args: [
|
|
1467
|
+
"set-option",
|
|
1468
|
+
"-t",
|
|
1469
|
+
createdSessionOptionTarget(plan, env),
|
|
1470
|
+
GJC_TMUX_PSMUX_INCARNATION_OPTION,
|
|
1471
|
+
plan.ownerIncarnation,
|
|
1472
|
+
],
|
|
1473
|
+
},
|
|
1474
|
+
]
|
|
1475
|
+
: []),
|
|
1476
|
+
].filter(command =>
|
|
1477
|
+
[
|
|
1478
|
+
"@gjc-profile",
|
|
1479
|
+
"@gjc-session-id",
|
|
1480
|
+
"@gjc-session-state-file",
|
|
1481
|
+
"@gjc-owner-generation",
|
|
1482
|
+
"@gjc-owner-server-key",
|
|
1483
|
+
GJC_TMUX_PSMUX_INCARNATION_OPTION,
|
|
1484
|
+
].includes(command.args[command.args.length - 2] ?? ""),
|
|
1485
|
+
);
|
|
1486
|
+
return required.every(command => {
|
|
1487
|
+
const option = command.args[command.args.length - 2]!;
|
|
1488
|
+
const expected = command.args[command.args.length - 1]!;
|
|
1489
|
+
const readback = spawnSync(
|
|
1490
|
+
plan.tmuxCommand,
|
|
1491
|
+
["display-message", "-p", "-t", createdSessionOptionTarget(plan, env), `#{${option}}`],
|
|
1492
|
+
controlOptions,
|
|
1493
|
+
);
|
|
1494
|
+
return readback.exitCode === 0 && readback.stdout?.trim() === expected;
|
|
1495
|
+
});
|
|
1496
|
+
};
|
|
1424
1497
|
const probeHasSession = (): TmuxSpawnResult =>
|
|
1425
1498
|
spawnSync(plan.tmuxCommand, ["has-session", "-t", createdSessionExactTarget(plan, env)], probeOptions);
|
|
1426
1499
|
const attachCreatedSession = (): TmuxSpawnResult =>
|
|
@@ -1428,9 +1501,13 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
1428
1501
|
|
|
1429
1502
|
if (plan.attachSessionName) {
|
|
1430
1503
|
let existingTarget: string;
|
|
1431
|
-
let existingProof:
|
|
1504
|
+
let existingProof: ProvenTmuxSessionIdentity | undefined;
|
|
1432
1505
|
if (plan.platform !== "linux") {
|
|
1433
|
-
existingTarget = buildGjcTmuxExactSessionTarget(plan.attachSessionName, {
|
|
1506
|
+
existingTarget = buildGjcTmuxExactSessionTarget(plan.attachSessionName, {
|
|
1507
|
+
env,
|
|
1508
|
+
platform: plan.platform,
|
|
1509
|
+
binary: { command: plan.tmuxCommand, isPsmux: plan.isPsmux, viaExplicitOverride: true },
|
|
1510
|
+
});
|
|
1434
1511
|
} else {
|
|
1435
1512
|
try {
|
|
1436
1513
|
existingProof = proveGjcTmuxSessionMutationTarget(plan.attachSessionName, env);
|
|
@@ -1448,7 +1525,11 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
1448
1525
|
target:
|
|
1449
1526
|
plan.platform === "linux" && existingTarget.startsWith("$")
|
|
1450
1527
|
? `${existingTarget}:`
|
|
1451
|
-
: buildGjcTmuxExactOptionTarget(plan.attachSessionName, {
|
|
1528
|
+
: buildGjcTmuxExactOptionTarget(plan.attachSessionName, {
|
|
1529
|
+
env,
|
|
1530
|
+
platform: plan.platform,
|
|
1531
|
+
binary: { command: plan.tmuxCommand, isPsmux: plan.isPsmux, viaExplicitOverride: true },
|
|
1532
|
+
}),
|
|
1452
1533
|
sessionName: plan.attachSessionName,
|
|
1453
1534
|
title: rootTerminalTitle,
|
|
1454
1535
|
spawnSync,
|
|
@@ -1486,7 +1567,7 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
1486
1567
|
}
|
|
1487
1568
|
const created = createIsolatedTmuxSession(
|
|
1488
1569
|
plan,
|
|
1489
|
-
|
|
1570
|
+
creationSpawn,
|
|
1490
1571
|
newSessionOptions,
|
|
1491
1572
|
context.diagnosticWriter ?? safeStderrWrite,
|
|
1492
1573
|
ownerIsolationProbe,
|
|
@@ -1513,7 +1594,7 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
1513
1594
|
}
|
|
1514
1595
|
const retry = createIsolatedTmuxSession(
|
|
1515
1596
|
plan,
|
|
1516
|
-
|
|
1597
|
+
creationSpawn,
|
|
1517
1598
|
newSessionOptions,
|
|
1518
1599
|
context.diagnosticWriter ?? safeStderrWrite,
|
|
1519
1600
|
ownerIsolationProbe,
|
|
@@ -1526,13 +1607,7 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
1526
1607
|
failedRetryDiagnostic(retry, retryProbe),
|
|
1527
1608
|
),
|
|
1528
1609
|
);
|
|
1529
|
-
cleanupCreatedTmuxSessionAfterFailure(
|
|
1530
|
-
plan,
|
|
1531
|
-
spawnSync,
|
|
1532
|
-
options,
|
|
1533
|
-
ownerIsolationProbe,
|
|
1534
|
-
new Error("new-session retry failed after missing session"),
|
|
1535
|
-
);
|
|
1610
|
+
cleanupCreatedTmuxSessionAfterFailure(plan, spawnSync, options, ownerIsolationProbe);
|
|
1536
1611
|
|
|
1537
1612
|
return true;
|
|
1538
1613
|
}
|
|
@@ -1564,24 +1639,19 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
1564
1639
|
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1565
1640
|
formatTmuxLaunchDiagnostic("profile tagging failed", ownershipFailure.stderr),
|
|
1566
1641
|
);
|
|
1642
|
+
cleanupCreatedTmuxSessionBeforePublicationFailure(plan, spawnSync, controlOptions, ownerIsolationProbe);
|
|
1567
1643
|
return true;
|
|
1568
1644
|
}
|
|
1569
1645
|
const retry = createIsolatedTmuxSession(
|
|
1570
1646
|
plan,
|
|
1571
|
-
|
|
1647
|
+
creationSpawn,
|
|
1572
1648
|
newSessionOptions,
|
|
1573
1649
|
context.diagnosticWriter ?? safeStderrWrite,
|
|
1574
1650
|
ownerIsolationProbe,
|
|
1575
1651
|
);
|
|
1576
1652
|
const retryProbe = probeHasSession();
|
|
1577
1653
|
if (retry.exitCode !== 0 || retryProbe.exitCode !== 0) {
|
|
1578
|
-
cleanupCreatedTmuxSessionAfterFailure(
|
|
1579
|
-
plan,
|
|
1580
|
-
spawnSync,
|
|
1581
|
-
options,
|
|
1582
|
-
ownerIsolationProbe,
|
|
1583
|
-
new Error("new-session retry failed after ownership failure"),
|
|
1584
|
-
);
|
|
1654
|
+
cleanupCreatedTmuxSessionAfterFailure(plan, spawnSync, options, ownerIsolationProbe);
|
|
1585
1655
|
|
|
1586
1656
|
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1587
1657
|
formatTmuxLaunchDiagnostic(
|
|
@@ -1595,19 +1665,20 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
1595
1665
|
const retryOwnershipFailure = requiredProfileFailure(retryProfile);
|
|
1596
1666
|
emitOptionalProfileDiagnostics(retryProfile, context.diagnosticWriter ?? safeStderrWrite);
|
|
1597
1667
|
if (retryOwnershipFailure) {
|
|
1598
|
-
cleanupCreatedTmuxSessionAfterFailure(
|
|
1599
|
-
plan,
|
|
1600
|
-
spawnSync,
|
|
1601
|
-
options,
|
|
1602
|
-
ownerIsolationProbe,
|
|
1603
|
-
new Error("profile tagging failed after retry"),
|
|
1604
|
-
);
|
|
1668
|
+
cleanupCreatedTmuxSessionAfterFailure(plan, spawnSync, options, ownerIsolationProbe);
|
|
1605
1669
|
|
|
1606
1670
|
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1607
1671
|
formatTmuxLaunchDiagnostic("profile tagging failed after retry", retryOwnershipFailure.stderr),
|
|
1608
1672
|
);
|
|
1609
1673
|
return true;
|
|
1610
1674
|
}
|
|
1675
|
+
if (!hasExactRequiredPsmuxMetadata()) {
|
|
1676
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1677
|
+
"tmux required ownership metadata readback failed; preserving session without publication.\n",
|
|
1678
|
+
);
|
|
1679
|
+
cleanupCreatedTmuxSessionBeforePublicationFailure(plan, spawnSync, controlOptions, ownerIsolationProbe);
|
|
1680
|
+
return true;
|
|
1681
|
+
}
|
|
1611
1682
|
// Recovery succeeded via retry — fall through to attach-session below.
|
|
1612
1683
|
}
|
|
1613
1684
|
ensureCreatedTmuxWindowTracksCallerTerminal(plan, spawnSync, controlOptions);
|
|
@@ -1634,116 +1705,64 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
1634
1705
|
(context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("new-session failed", stderr) + suffix);
|
|
1635
1706
|
return true;
|
|
1636
1707
|
}
|
|
1637
|
-
if (!
|
|
1708
|
+
if (!hasExactRequiredPsmuxMetadata()) {
|
|
1638
1709
|
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1639
|
-
"tmux
|
|
1640
|
-
);
|
|
1641
|
-
return true;
|
|
1642
|
-
}
|
|
1643
|
-
try {
|
|
1644
|
-
resolveManagedOwnerPredecessorSync(
|
|
1645
|
-
path.dirname(plan.sessionStateFile!),
|
|
1646
|
-
plan.sessionId!,
|
|
1647
|
-
plan.ownerGenerationBaseline!,
|
|
1648
|
-
);
|
|
1649
|
-
replaceOwnerGenerationSync(
|
|
1650
|
-
path.dirname(plan.sessionStateFile!),
|
|
1651
|
-
plan.sessionId!,
|
|
1652
|
-
plan.ownerGeneration!,
|
|
1653
|
-
plan.ownerGenerationBaseline!,
|
|
1710
|
+
"tmux required ownership metadata readback failed; preserving session without publication.\n",
|
|
1654
1711
|
);
|
|
1655
|
-
|
|
1656
|
-
const publicationError = error instanceof Error ? error : new Error(String(error));
|
|
1657
|
-
cleanupCreatedTmuxSessionAfterFailure(plan, spawnSync, options, ownerIsolationProbe, publicationError);
|
|
1658
|
-
(context.diagnosticWriter ?? safeStderrWrite)(`tmux owner lifecycle publication failed: ${String(error)}`);
|
|
1712
|
+
cleanupCreatedTmuxSessionBeforePublicationFailure(plan, spawnSync, controlOptions, ownerIsolationProbe);
|
|
1659
1713
|
return true;
|
|
1660
1714
|
}
|
|
1661
1715
|
if (!isCreatedTmuxSessionIdentityStable(plan, spawnSync, controlOptions, ownerIsolationProbe)) {
|
|
1662
1716
|
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1663
|
-
"tmux created session proof failed
|
|
1717
|
+
"tmux created session proof failed; preserving session without attach.\n",
|
|
1664
1718
|
);
|
|
1665
1719
|
return true;
|
|
1666
1720
|
}
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1721
|
+
try {
|
|
1722
|
+
const stateDir = path.dirname(plan.sessionStateFile!);
|
|
1723
|
+
resolveManagedOwnerPredecessorSync(stateDir, plan.sessionId!, plan.ownerGenerationBaseline!);
|
|
1724
|
+
replaceOwnerGenerationSync(stateDir, plan.sessionId!, plan.ownerGeneration!, plan.ownerGenerationBaseline!);
|
|
1725
|
+
providerAuthorityPublished = true;
|
|
1726
|
+
} catch (error) {
|
|
1727
|
+
cleanupCreatedTmuxSessionBeforePublicationFailure(plan, spawnSync, controlOptions, ownerIsolationProbe);
|
|
1728
|
+
(context.diagnosticWriter ?? safeStderrWrite)(`tmux owner lifecycle publication failed: ${String(error)}`);
|
|
1672
1729
|
return true;
|
|
1673
1730
|
}
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
const retryAttached = attachCreatedSession();
|
|
1679
|
-
if (retryAttached.exitCode === 0) return true;
|
|
1680
|
-
if (isTmuxAttachDisconnectError(retryAttached)) {
|
|
1681
|
-
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1682
|
-
formatTmuxLaunchDiagnostic("attach disconnected", retryAttached.stderr),
|
|
1683
|
-
);
|
|
1684
|
-
return true;
|
|
1685
|
-
}
|
|
1686
|
-
cleanupCreatedTmuxSessionAfterFailure(
|
|
1687
|
-
plan,
|
|
1688
|
-
spawnSync,
|
|
1689
|
-
options,
|
|
1690
|
-
ownerIsolationProbe,
|
|
1691
|
-
new Error("attach retry failed"),
|
|
1731
|
+
try {
|
|
1732
|
+
if (!isCreatedTmuxSessionIdentityStable(plan, spawnSync, controlOptions, ownerIsolationProbe)) {
|
|
1733
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1734
|
+
"tmux created session proof failed after lifecycle publication; preserving session without attach.\n",
|
|
1692
1735
|
);
|
|
1693
|
-
|
|
1736
|
+
return true;
|
|
1737
|
+
}
|
|
1738
|
+
if (!hasExactRequiredPsmuxMetadata()) {
|
|
1694
1739
|
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1695
|
-
|
|
1740
|
+
"tmux required ownership metadata readback failed after lifecycle publication; preserving session without attach.\n",
|
|
1696
1741
|
);
|
|
1697
1742
|
return true;
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
const retry = createIsolatedTmuxSession(
|
|
1706
|
-
plan,
|
|
1707
|
-
rawSpawnSync,
|
|
1708
|
-
newSessionOptions,
|
|
1709
|
-
context.diagnosticWriter ?? safeStderrWrite,
|
|
1710
|
-
ownerIsolationProbe,
|
|
1743
|
+
}
|
|
1744
|
+
// attach-session needs PTY inherit for the user-facing attach; keep it unchanged.
|
|
1745
|
+
const attached = attachCreatedSession();
|
|
1746
|
+
if (attached.exitCode === 0) return true;
|
|
1747
|
+
if (isTmuxAttachDisconnectError(attached)) {
|
|
1748
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1749
|
+
formatTmuxLaunchDiagnostic("attach disconnected", attached.stderr),
|
|
1711
1750
|
);
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
const retryOwnershipFailure = requiredProfileFailure(retryProfile);
|
|
1723
|
-
emitOptionalProfileDiagnostics(retryProfile, context.diagnosticWriter ?? safeStderrWrite);
|
|
1724
|
-
if (retryOwnershipFailure) {
|
|
1725
|
-
cleanupCreatedTmuxSessionAfterFailure(
|
|
1726
|
-
plan,
|
|
1727
|
-
spawnSync,
|
|
1728
|
-
options,
|
|
1729
|
-
ownerIsolationProbe,
|
|
1730
|
-
new Error("profile tagging failed after retry"),
|
|
1731
|
-
);
|
|
1732
|
-
|
|
1751
|
+
return true;
|
|
1752
|
+
}
|
|
1753
|
+
if (isWindowsPsmuxAttachConnectionRefused(plan, attached)) {
|
|
1754
|
+
waitForWindowsPsmuxAttachRetry();
|
|
1755
|
+
const probeAfterAttach = probeHasSession();
|
|
1756
|
+
if (probeAfterAttach.exitCode === 0) {
|
|
1757
|
+
if (
|
|
1758
|
+
!isCreatedTmuxSessionIdentityStable(plan, spawnSync, controlOptions, ownerIsolationProbe) ||
|
|
1759
|
+
!hasExactRequiredPsmuxMetadata()
|
|
1760
|
+
) {
|
|
1733
1761
|
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1734
|
-
|
|
1762
|
+
"tmux created session proof failed after attach recovery probe; preserving session without attach.\n",
|
|
1735
1763
|
);
|
|
1736
1764
|
return true;
|
|
1737
1765
|
}
|
|
1738
|
-
ensureCreatedTmuxWindowTracksCallerTerminal(plan, spawnSync, controlOptions);
|
|
1739
|
-
applyGjcTmuxRootTerminalTitleProfile({
|
|
1740
|
-
tmuxCommand: plan.tmuxCommand,
|
|
1741
|
-
target: createdSessionOptionTarget(plan, env),
|
|
1742
|
-
sessionName: plan.sessionName,
|
|
1743
|
-
title: rootTerminalTitle,
|
|
1744
|
-
spawnSync,
|
|
1745
|
-
options: controlOptions,
|
|
1746
|
-
});
|
|
1747
1766
|
const retryAttached = attachCreatedSession();
|
|
1748
1767
|
if (retryAttached.exitCode === 0) return true;
|
|
1749
1768
|
if (isTmuxAttachDisconnectError(retryAttached)) {
|
|
@@ -1752,50 +1771,47 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
1752
1771
|
);
|
|
1753
1772
|
return true;
|
|
1754
1773
|
}
|
|
1755
|
-
cleanupCreatedTmuxSessionAfterFailure(
|
|
1756
|
-
plan,
|
|
1757
|
-
spawnSync,
|
|
1758
|
-
options,
|
|
1759
|
-
ownerIsolationProbe,
|
|
1760
|
-
new Error("attach failed after recovery"),
|
|
1761
|
-
);
|
|
1762
1774
|
|
|
1763
1775
|
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1764
|
-
formatTmuxLaunchDiagnostic("attach failed
|
|
1776
|
+
formatTmuxLaunchDiagnostic("attach retry failed", retryAttached.stderr),
|
|
1777
|
+
);
|
|
1778
|
+
return true;
|
|
1779
|
+
}
|
|
1780
|
+
if (!isWindowsPsmuxMissingSessionRegistrationRace(plan, probeAfterAttach)) {
|
|
1781
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1782
|
+
formatTmuxLaunchDiagnostic("attach recovery probe failed", probeAfterAttach.stderr),
|
|
1765
1783
|
);
|
|
1766
1784
|
return true;
|
|
1767
1785
|
}
|
|
1768
|
-
cleanupCreatedTmuxSessionAfterFailure(
|
|
1769
|
-
plan,
|
|
1770
|
-
spawnSync,
|
|
1771
|
-
options,
|
|
1772
|
-
ownerIsolationProbe,
|
|
1773
|
-
new Error("attach recovery recreate failed"),
|
|
1774
|
-
);
|
|
1775
|
-
|
|
1776
|
-
const recoveryFailure = retry.exitCode !== 0 ? retry.stderr : retryProbe.stderr;
|
|
1777
1786
|
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1778
|
-
|
|
1787
|
+
"tmux attach recovery found the published session missing; preserving lifecycle state without recreation.\n",
|
|
1779
1788
|
);
|
|
1780
1789
|
return true;
|
|
1781
1790
|
}
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1791
|
+
// Closing an SSH/Windows Terminal tab can make `tmux attach-session`
|
|
1792
|
+
// exit with code 1 and no captured stderr while the tmux server correctly
|
|
1793
|
+
// keeps the just-created session alive. Preserve that live session so the
|
|
1794
|
+
// user can reattach instead of treating the parent client teardown as a
|
|
1795
|
+
// launch failure.
|
|
1796
|
+
const attachFailureStderr = attached.stderr?.trim() ?? "";
|
|
1797
|
+
if (attachFailureStderr.length === 0) {
|
|
1798
|
+
const probeAfterAttachFailure = probeHasSession();
|
|
1799
|
+
if (probeAfterAttachFailure.exitCode === 0) {
|
|
1800
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1801
|
+
formatTmuxLaunchDiagnostic("attach disconnected", attached.stderr),
|
|
1802
|
+
);
|
|
1803
|
+
return true;
|
|
1804
|
+
}
|
|
1796
1805
|
}
|
|
1806
|
+
(context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach failed", attached.stderr));
|
|
1807
|
+
return true;
|
|
1808
|
+
} catch (error) {
|
|
1809
|
+
(context.diagnosticWriter ?? safeStderrWrite)(
|
|
1810
|
+
formatTmuxLaunchDiagnostic(
|
|
1811
|
+
"post-publication verification or attach failed; preserving committed session",
|
|
1812
|
+
String(error),
|
|
1813
|
+
),
|
|
1814
|
+
);
|
|
1815
|
+
return true;
|
|
1797
1816
|
}
|
|
1798
|
-
cleanupCreatedTmuxSessionAfterFailure(plan, spawnSync, options, ownerIsolationProbe, new Error("attach failed"));
|
|
1799
|
-
(context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach failed", attached.stderr));
|
|
1800
|
-
return true;
|
|
1801
1817
|
}
|