@gajae-code/coding-agent 0.12.5 → 0.12.7

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.
Files changed (91) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +4 -1
  3. package/dist/types/async/job-manager.d.ts +36 -1
  4. package/dist/types/extensibility/extensions/runner.d.ts +9 -7
  5. package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
  6. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
  7. package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
  8. package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
  9. package/dist/types/gjc-runtime/team-store.d.ts +0 -4
  10. package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
  11. package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
  12. package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
  13. package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
  14. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
  15. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +21 -4
  16. package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
  17. package/dist/types/modes/components/plugin-settings.d.ts +2 -0
  18. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  19. package/dist/types/sdk/broker/broker.d.ts +18 -0
  20. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
  21. package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
  22. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -3
  23. package/dist/types/sdk/bus/index.d.ts +4 -0
  24. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -2
  25. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
  26. package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
  27. package/dist/types/session/agent-session.d.ts +5 -4
  28. package/dist/types/session/attempt-record-store.d.ts +19 -0
  29. package/dist/types/session/session-manager.d.ts +1 -0
  30. package/dist/types/session/session-storage.d.ts +8 -0
  31. package/dist/types/task/receipt.d.ts +1 -0
  32. package/dist/types/task/types.d.ts +43 -0
  33. package/dist/types/tools/subagent.d.ts +15 -0
  34. package/dist/types/tools/todo-write.d.ts +3 -0
  35. package/package.json +8 -8
  36. package/scripts/capture-sticky-viewport-showcase.ts +131 -5
  37. package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
  38. package/scripts/verify-sticky-viewport-showcase.ts +620 -93
  39. package/src/async/job-manager.ts +430 -46
  40. package/src/config/model-profiles.ts +8 -0
  41. package/src/config/settings-schema.ts +17 -3
  42. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
  43. package/src/extensibility/extensions/runner.ts +80 -6
  44. package/src/extensibility/extensions/wrapper.ts +22 -15
  45. package/src/extensibility/runtime-skill-discovery.ts +4 -6
  46. package/src/gjc-runtime/launch-tmux.ts +309 -293
  47. package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
  48. package/src/gjc-runtime/psmux-detect.ts +52 -6
  49. package/src/gjc-runtime/team-launch.ts +50 -2
  50. package/src/gjc-runtime/team-runtime.ts +716 -251
  51. package/src/gjc-runtime/team-store.ts +25 -7
  52. package/src/gjc-runtime/team-workers.ts +161 -1
  53. package/src/gjc-runtime/tmux-common.ts +16 -4
  54. package/src/gjc-runtime/tmux-gc.ts +37 -5
  55. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
  56. package/src/gjc-runtime/tmux-provider-context.ts +451 -0
  57. package/src/gjc-runtime/tmux-sessions.ts +409 -156
  58. package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
  59. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
  60. package/src/gjc-runtime/ultragoal-runtime.ts +360 -156
  61. package/src/internal-urls/docs-index.generated.ts +4 -4
  62. package/src/internal-urls/local-protocol.ts +39 -18
  63. package/src/modes/acp/acp-agent.ts +27 -13
  64. package/src/modes/components/gjc-bundle-settings.ts +2 -0
  65. package/src/modes/components/plugin-settings.ts +5 -0
  66. package/src/modes/components/settings-selector.ts +4 -0
  67. package/src/modes/controllers/selector-controller.ts +1 -0
  68. package/src/prompts/tools/monitor.md +4 -4
  69. package/src/sdk/broker/broker.ts +93 -10
  70. package/src/sdk/broker/lifecycle-ledger.ts +227 -11
  71. package/src/sdk/broker/lifecycle.ts +691 -92
  72. package/src/sdk/bus/chat-daemon-control.ts +30 -6
  73. package/src/sdk/bus/index.ts +23 -6
  74. package/src/sdk/bus/telegram-daemon-contract.ts +2 -2
  75. package/src/sdk/bus/telegram-daemon.ts +283 -44
  76. package/src/sdk/bus/topic-registry.ts +133 -10
  77. package/src/sdk/session.ts +7 -6
  78. package/src/session/agent-session.ts +752 -339
  79. package/src/session/attempt-record-store.ts +106 -0
  80. package/src/session/internal/managed-session-scope.ts +252 -67
  81. package/src/session/internal/managed-session-storage.ts +1 -0
  82. package/src/session/session-manager.ts +7 -0
  83. package/src/session/session-storage.ts +146 -24
  84. package/src/task/index.ts +230 -19
  85. package/src/task/provider-retry-status.ts +4 -1
  86. package/src/task/receipt.ts +2 -0
  87. package/src/task/types.ts +10 -0
  88. package/src/tools/monitor.ts +7 -1
  89. package/src/tools/output-meta.ts +10 -2
  90. package/src/tools/subagent.ts +97 -75
  91. package/src/tools/todo-write.ts +47 -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?: import("./tmux-owner-isolation").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 plan.createdSessionId ?? buildGjcTmuxExactSessionTarget(plan.sessionName, { env });
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, { env });
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
- if (!isCreatedTmuxSessionIdentityStable(plan, spawnSync, options, probe))
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 (cleanupError) {
829
- throw new AggregateError([primary, cleanupError], "gjc_tmux_exact_cleanup_uncertain");
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 (plan.isPsmux || !plan.createdSessionId || !plan.createdServerIdentity) return false;
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 '=${plan.sessionName}' (no server running)`;
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
- : findExistingSessionForLaunch({
1002
- env,
1003
- project,
1004
- branch,
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: ReturnType<typeof captureOwnerGenerationBaselineSync>,
1061
- ): ReturnType<typeof resolveManagedOwnerPredecessorSync> {
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, new Error("owner_generation_stale"));
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, rawSpawnSync, context.callerCgroupReader);
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: ReturnType<typeof proveGjcTmuxSessionMutationTarget> | undefined;
1504
+ let existingProof: ProvenTmuxSessionIdentity | undefined;
1432
1505
  if (plan.platform !== "linux") {
1433
- existingTarget = buildGjcTmuxExactSessionTarget(plan.attachSessionName, { env });
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, { env }),
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
- rawSpawnSync,
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
- rawSpawnSync,
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
- rawSpawnSync,
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 (!isCreatedTmuxSessionIdentityStable(plan, spawnSync, controlOptions, ownerIsolationProbe)) {
1708
+ if (!hasExactRequiredPsmuxMetadata()) {
1638
1709
  (context.diagnosticWriter ?? safeStderrWrite)(
1639
- "tmux created session proof failed; preserving session without attach.\n",
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
- } catch (error) {
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 after lifecycle publication; preserving session without attach.\n",
1717
+ "tmux created session proof failed; preserving session without attach.\n",
1664
1718
  );
1665
1719
  return true;
1666
1720
  }
1667
- // attach-session needs PTY inherit for the user-facing attach; keep it unchanged.
1668
- const attached = attachCreatedSession();
1669
- if (attached.exitCode === 0) return true;
1670
- if (isTmuxAttachDisconnectError(attached)) {
1671
- (context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach disconnected", attached.stderr));
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
- if (isWindowsPsmuxAttachConnectionRefused(plan, attached)) {
1675
- waitForWindowsPsmuxAttachRetry();
1676
- const probeAfterAttach = probeHasSession();
1677
- if (probeAfterAttach.exitCode === 0) {
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
- formatTmuxLaunchDiagnostic("attach retry failed", retryAttached.stderr),
1740
+ "tmux required ownership metadata readback failed after lifecycle publication; preserving session without attach.\n",
1696
1741
  );
1697
1742
  return true;
1698
- } else {
1699
- if (!isWindowsPsmuxMissingSessionRegistrationRace(plan, probeAfterAttach)) {
1700
- (context.diagnosticWriter ?? safeStderrWrite)(
1701
- formatTmuxLaunchDiagnostic("attach recovery probe failed", probeAfterAttach.stderr),
1702
- );
1703
- return true;
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
- const retryProbe = probeHasSession();
1713
- if (retry.exitCode === 0 && retryProbe.exitCode === 0) {
1714
- renameTmuxWindow(
1715
- plan.tmuxCommand,
1716
- windowTitle,
1717
- spawnSync,
1718
- controlOptions,
1719
- createdSessionExactTarget(plan, env),
1720
- );
1721
- const retryProfile = applyGjcTmuxProfile(buildProfileInputs());
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
- formatTmuxLaunchDiagnostic("profile tagging failed after retry", retryOwnershipFailure.stderr),
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 after recovery", retryAttached.stderr),
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
- formatTmuxLaunchDiagnostic("attach recovery recreate failed", recoveryFailure),
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
- // Closing an SSH/Windows Terminal tab can make `tmux attach-session`
1784
- // exit with code 1 and no captured stderr while the tmux server correctly
1785
- // keeps the just-created session alive. Preserve that live session so the
1786
- // user can reattach instead of treating the parent client teardown as a
1787
- // launch failure.
1788
- const attachFailureStderr = attached.stderr?.trim() ?? "";
1789
- if (attachFailureStderr.length === 0) {
1790
- const probeAfterAttachFailure = probeHasSession();
1791
- if (probeAfterAttachFailure.exitCode === 0) {
1792
- (context.diagnosticWriter ?? safeStderrWrite)(
1793
- formatTmuxLaunchDiagnostic("attach disconnected", attached.stderr),
1794
- );
1795
- return true;
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
  }