@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
@@ -4,9 +4,10 @@ import * as fsSync from "node:fs";
4
4
  import * as fs from "node:fs/promises";
5
5
  import * as path from "node:path";
6
6
  import { Process } from "@gajae-code/natives";
7
+ import { managedSecurityFailureClassification } from "../session/internal/managed-session-storage";
7
8
  import { readLinuxProcStartTime, readLinuxProcStartTimeSync } from "./linux-proc";
8
9
  import { resolveGjcTmuxBinary } from "./psmux-detect";
9
- import { tmuxRuntimeSessionPath } from "./session-layout";
10
+ import { GJC_DIR, GJC_SESSION_PREFIX, tmuxRuntimeSessionPath } from "./session-layout";
10
11
  import {
11
12
  GJC_COORDINATOR_SESSION_ID_ENV,
12
13
  GJC_COORDINATOR_SESSION_STATE_FILE_ENV,
@@ -15,14 +16,18 @@ import {
15
16
  GJC_TMUX_OWNER_STATE_DIR_ENV,
16
17
  } from "./session-state-sidecar";
17
18
  import {
19
+ assertGjcTmuxMutationAuthoritySync,
20
+ bindGjcTmuxProviderAuthority,
18
21
  buildGjcTmuxExactOptionTarget,
19
22
  buildGjcTmuxExactSessionTarget,
20
23
  buildGjcTmuxProfileCommands,
21
24
  buildGjcTmuxSessionName,
22
25
  buildGjcTmuxSessionSlug,
23
26
  buildGjcTmuxUntaggedSessionError,
27
+ buildTmuxProviderCommand,
24
28
  GJC_TMUX_BRANCH_OPTION,
25
29
  GJC_TMUX_BRANCH_SLUG_OPTION,
30
+ GJC_TMUX_COMMAND_ENV,
26
31
  GJC_TMUX_OWNER_GENERATION_OPTION,
27
32
  GJC_TMUX_OWNER_SERVER_KEY_OPTION,
28
33
  GJC_TMUX_PROFILE_OPTION,
@@ -31,8 +36,13 @@ import {
31
36
  GJC_TMUX_SESSION_ID_OPTION,
32
37
  GJC_TMUX_SESSION_STATE_FILE_OPTION,
33
38
  GJC_TMUX_VERSION_OPTION,
39
+ hasGjcTmuxProviderAuthoritySync,
34
40
  normalizeTmuxCreatedAt,
41
+ type ProviderAuthority,
42
+ persistGjcTmuxProviderAuthoritySync,
43
+ readGjcTmuxProviderAuthoritySync,
35
44
  resolveGjcTmuxCommand,
45
+ resolveGjcTmuxProviderContext,
36
46
  } from "./tmux-common";
37
47
  import {
38
48
  captureOwnerGenerationBaselineSync,
@@ -52,6 +62,11 @@ import {
52
62
  type TmuxOwnerIsolationExecutionResult,
53
63
  type TmuxServerProof,
54
64
  } from "./tmux-owner-isolation";
65
+ import {
66
+ assertGjcTmuxStagedMutationAuthoritySync,
67
+ gjcTmuxAuthorityPlatform,
68
+ listGjcTmuxProviderAuthoritiesSync,
69
+ } from "./tmux-provider-context";
55
70
  import { buildWindowsPowerShellInnerCommand } from "./windows-powershell-command";
56
71
 
57
72
  export interface GjcTmuxSessionStatus {
@@ -69,6 +84,9 @@ export interface GjcTmuxSessionStatus {
69
84
  version?: string;
70
85
  ownerGeneration?: string;
71
86
  nativeSessionId?: string;
87
+ psmuxIncarnation?: string;
88
+ /** Exact durable provider binding used to discover this psmux session. */
89
+ providerAuthority?: ProviderAuthority;
72
90
 
73
91
  panePids: number[];
74
92
  profile?: string;
@@ -84,6 +102,7 @@ export interface GjcTmuxSessionTagsForGc {
84
102
  version?: string;
85
103
  ownerGeneration?: string;
86
104
  nativeSessionId?: string;
105
+ psmuxIncarnation?: string;
87
106
 
88
107
  createdAt?: string;
89
108
  attached?: boolean;
@@ -99,6 +118,7 @@ export interface ProvenTmuxSessionIdentity {
99
118
  nativeSessionId: string;
100
119
  serverPid: number;
101
120
  serverStartTime: string;
121
+ psmuxIncarnation?: string;
102
122
  }
103
123
 
104
124
  export interface ExpectedGjcTmuxSessionIdentity {
@@ -108,6 +128,7 @@ export interface ExpectedGjcTmuxSessionIdentity {
108
128
  sessionStateFile: string;
109
129
  project: string;
110
130
  createdAt: string;
131
+ psmuxIncarnation?: string;
111
132
  }
112
133
 
113
134
  export interface ExactOwnerIdentity {
@@ -128,6 +149,7 @@ export interface ForceCloseOwnerDependencies {
128
149
  sleep(ms: number): Promise<void>;
129
150
  listPanePids(sessionName: string, env: NodeJS.ProcessEnv): number[];
130
151
  }
152
+ const GJC_TMUX_PSMUX_INCARNATION_OPTION = "@gjc-psmux-incarnation";
131
153
 
132
154
  const FORCE_CLOSE_VERDICT_TIMEOUT_MS = 5_000;
133
155
  const FORCE_CLOSE_VERDICT_POLL_MS = 50;
@@ -156,15 +178,57 @@ export function __setMutationServerProofForTests(
156
178
  mutationServerProofTestDependency = dependency;
157
179
  }
158
180
 
159
- function runTmux(args: string[], env: NodeJS.ProcessEnv = process.env): string {
160
- const tmuxCommand = resolveGjcTmuxCommand(env);
161
- const result = Bun.spawnSync([tmuxCommand, ...args], {
162
- stdout: "pipe",
163
- stderr: "pipe",
164
- env,
165
- });
166
- if (result.exitCode === 0) return result.stdout.toString();
167
- throw new Error(result.stderr.toString().trim() || `tmux ${args.join(" ")} failed`);
181
+ function psmuxAuthorityFromEnv(env: NodeJS.ProcessEnv): ProviderAuthority | null {
182
+ const stateDir = env[GJC_TMUX_OWNER_STATE_DIR_ENV]?.trim();
183
+ const sessionId = env[GJC_COORDINATOR_SESSION_ID_ENV]?.trim();
184
+ const generation = env[GJC_TMUX_OWNER_GENERATION_ENV]?.trim();
185
+ if (!stateDir || !sessionId || !generation) return null;
186
+ if (!hasGjcTmuxProviderAuthoritySync({ stateDir, sessionId, generation })) return null;
187
+ return readGjcTmuxProviderAuthoritySync({ stateDir, sessionId, generation });
188
+ }
189
+ function environmentForProviderAuthority(
190
+ env: NodeJS.ProcessEnv,
191
+ authority: ProviderAuthority | undefined,
192
+ ): NodeJS.ProcessEnv {
193
+ if (!authority) return env;
194
+ return {
195
+ ...env,
196
+ [GJC_TMUX_COMMAND_ENV]: authority.command,
197
+ [GJC_TMUX_OWNER_STATE_DIR_ENV]: authority.stateDir,
198
+ [GJC_COORDINATOR_SESSION_ID_ENV]: authority.sessionId,
199
+ [GJC_TMUX_OWNER_GENERATION_ENV]: authority.generation,
200
+ };
201
+ }
202
+
203
+ function runTmux(
204
+ args: string[],
205
+ env: NodeJS.ProcessEnv = process.env,
206
+ provisionalAuthority?: ProviderAuthority,
207
+ ): string {
208
+ const authority = provisionalAuthority ?? psmuxAuthorityFromEnv(env);
209
+ const binary = resolveGjcTmuxBinary({ env });
210
+ if (binary.isPsmux && !authority) throw new Error("gjc_tmux_provider_authority_unavailable");
211
+ const tmuxCommand = authority?.command ?? binary.command;
212
+ if (authority)
213
+ (provisionalAuthority ? assertGjcTmuxStagedMutationAuthoritySync : assertGjcTmuxMutationAuthoritySync)(authority);
214
+ let result: Bun.SyncSubprocess<"pipe", "pipe">;
215
+ try {
216
+ result = Bun.spawnSync(
217
+ [tmuxCommand, ...(authority ? buildTmuxProviderCommand(authority, args[0]!, args.slice(1)) : args)],
218
+ {
219
+ stdout: "pipe",
220
+ stderr: "pipe",
221
+ env,
222
+ },
223
+ );
224
+ } finally {
225
+ if (authority)
226
+ (provisionalAuthority ? assertGjcTmuxStagedMutationAuthoritySync : assertGjcTmuxMutationAuthoritySync)(
227
+ authority,
228
+ );
229
+ }
230
+ if (result.exitCode === 0) return result.stdout?.toString() ?? "";
231
+ throw new Error(result.stderr?.toString().trim() || `tmux ${args.join(" ")} failed`);
168
232
  }
169
233
  function normalizeExactTmuxTarget(sessionTarget: string, env: NodeJS.ProcessEnv, kind: "session" | "option"): string {
170
234
  if (sessionTarget.startsWith("$")) return sessionTarget;
@@ -209,6 +273,7 @@ function parseSessionLine(line: string): GjcTmuxSessionStatus | null {
209
273
  sessionStateFile = "",
210
274
  ownerGeneration = "",
211
275
  version = "",
276
+ psmuxIncarnation = "",
212
277
  nativeSessionId = "",
213
278
  ] = line.split("\t");
214
279
 
@@ -232,7 +297,8 @@ function parseSessionLine(line: string): GjcTmuxSessionStatus | null {
232
297
  sessionStateFile: sessionStateFile || undefined,
233
298
  version: version || undefined,
234
299
  ownerGeneration: ownerGeneration || undefined,
235
- nativeSessionId: nativeSessionId || undefined,
300
+ psmuxIncarnation: nativeSessionId ? psmuxIncarnation || undefined : undefined,
301
+ nativeSessionId: nativeSessionId || (psmuxIncarnation.startsWith("$") ? psmuxIncarnation : undefined),
236
302
  };
237
303
  }
238
304
 
@@ -270,7 +336,7 @@ function runListSessions(format: string, env: NodeJS.ProcessEnv = process.env):
270
336
  const [, name, windows, created] = match;
271
337
  const createdEpoch = String(Math.floor(new Date(`${created} UTC`).getTime() / 1000) || 0);
272
338
 
273
- return [name, windows, "0", createdEpoch, "", "", "0", "", "", "", "", "", "", "", "", ""].join("\t");
339
+ return [name, windows, "0", createdEpoch, "", "", "0", "", "", "", "", "", "", "", "", "", ""].join("\t");
274
340
  });
275
341
  }
276
342
  }
@@ -279,7 +345,7 @@ function runListSessions(format: string, env: NodeJS.ProcessEnv = process.env):
279
345
 
280
346
  function listSessionLines(env: NodeJS.ProcessEnv = process.env): string[] {
281
347
  return runListSessions(
282
- `#{session_name}\t#{session_windows}\t#{session_attached}\t#{session_created}\t#{${GJC_TMUX_PROFILE_OPTION}}\t#{session_key_table}\t#{session_panes}\t#{pane_pid}\t#{${GJC_TMUX_BRANCH_OPTION}}\t#{${GJC_TMUX_BRANCH_SLUG_OPTION}}\t#{${GJC_TMUX_PROJECT_OPTION}}\t#{${GJC_TMUX_SESSION_ID_OPTION}}\t#{${GJC_TMUX_SESSION_STATE_FILE_OPTION}}\t#{${GJC_TMUX_OWNER_GENERATION_OPTION}}\t#{${GJC_TMUX_VERSION_OPTION}}\t#{session_id}`,
348
+ `#{session_name}\t#{session_windows}\t#{session_attached}\t#{session_created}\t#{${GJC_TMUX_PROFILE_OPTION}}\t#{session_key_table}\t#{session_panes}\t#{pane_pid}\t#{${GJC_TMUX_BRANCH_OPTION}}\t#{${GJC_TMUX_BRANCH_SLUG_OPTION}}\t#{${GJC_TMUX_PROJECT_OPTION}}\t#{${GJC_TMUX_SESSION_ID_OPTION}}\t#{${GJC_TMUX_SESSION_STATE_FILE_OPTION}}\t#{${GJC_TMUX_OWNER_GENERATION_OPTION}}\t#{${GJC_TMUX_VERSION_OPTION}}\t#{${GJC_TMUX_PSMUX_INCARNATION_OPTION}}\t#{session_id}`,
283
349
 
284
350
  env,
285
351
  );
@@ -288,28 +354,98 @@ function listSessionLines(env: NodeJS.ProcessEnv = process.env): string[] {
288
354
  function listRawTmuxSessionNames(env: NodeJS.ProcessEnv = process.env): string[] {
289
355
  return runListSessions("#{session_name}", env).map(line => line.split("\t")[0] ?? line);
290
356
  }
357
+ function canonicalProviderStateDirs(cwd: string): string[] {
358
+ const gjcDir = path.join(cwd, GJC_DIR);
359
+ let entries: fsSync.Dirent[];
360
+ try {
361
+ entries = fsSync.readdirSync(gjcDir, { withFileTypes: true });
362
+ } catch (error) {
363
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
364
+ throw error;
365
+ }
366
+ return entries
367
+ .filter(entry => entry.isDirectory() && !entry.isSymbolicLink() && entry.name.startsWith(GJC_SESSION_PREFIX))
368
+ .map(entry => path.join(gjcDir, entry.name, "runtime", "tmux-sessions"))
369
+ .filter(candidate => {
370
+ try {
371
+ return fsSync.lstatSync(candidate).isDirectory();
372
+ } catch (error) {
373
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
374
+ throw error;
375
+ }
376
+ });
377
+ }
378
+
379
+ function psmuxAuthorityEnvironments(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv[] {
380
+ const explicitAuthority = psmuxAuthorityFromEnv(env);
381
+ if (explicitAuthority) return [environmentForProviderAuthority(env, explicitAuthority)];
382
+ const explicitStateDir =
383
+ env[GJC_TMUX_OWNER_STATE_DIR_ENV]?.trim() ??
384
+ (env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV] ? path.dirname(env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]) : "");
385
+ const ambient = resolveGjcTmuxBinary({ env });
386
+ if (gjcTmuxAuthorityPlatform() === "win32") {
387
+ const ambientAvailable = path.isAbsolute(ambient.command)
388
+ ? fsSync.existsSync(ambient.command)
389
+ : Bun.which(ambient.command) !== null;
390
+ const shouldDiscoverPersisted = ambient.isPsmux || (!ambient.viaExplicitOverride && !ambientAvailable);
391
+ if (shouldDiscoverPersisted) {
392
+ const stateDirs = explicitStateDir ? [explicitStateDir] : canonicalProviderStateDirs(process.cwd());
393
+ const authorities = stateDirs.flatMap(stateDir => {
394
+ try {
395
+ return listGjcTmuxProviderAuthoritiesSync(stateDir);
396
+ } catch (error) {
397
+ const classification = managedSecurityFailureClassification(error);
398
+ const message =
399
+ typeof error === "object" && error !== null && "message" in error && typeof error.message === "string"
400
+ ? error.message
401
+ : "";
402
+ const foreignOwner = classification === "owner_mismatch" || message.endsWith(": owner_mismatch");
403
+ if (!explicitStateDir && foreignOwner) return [];
404
+ throw error;
405
+ }
406
+ });
407
+ if (authorities.length > 0)
408
+ return authorities.map(authority => environmentForProviderAuthority(env, authority));
409
+ }
410
+ }
411
+ if (ambient.isPsmux) throw new Error("gjc_tmux_provider_authority_unavailable");
412
+ return [env];
413
+ }
291
414
 
292
415
  export function listGjcTmuxSessions(env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionStatus[] {
293
- return listSessionLines(env)
294
- .map(parseSessionLine)
295
- .filter((session): session is GjcTmuxSessionStatus => session != null)
296
- .map(session => hydrateSessionFromExactOptions(session, env))
297
- .filter((session): session is GjcTmuxSessionStatus => session?.profile === GJC_TMUX_PROFILE_VALUE)
298
- .sort((a, b) => a.name.localeCompare(b.name));
416
+ const discovered = psmuxAuthorityEnvironments(env).flatMap(authorityEnv => {
417
+ const authority = psmuxAuthorityFromEnv(authorityEnv) ?? undefined;
418
+ return listSessionLines(authorityEnv)
419
+ .map(parseSessionLine)
420
+ .filter((session): session is GjcTmuxSessionStatus => session != null)
421
+ .map(session => hydrateSessionFromExactOptions(session, authorityEnv))
422
+ .filter((session): session is GjcTmuxSessionStatus => session?.profile === GJC_TMUX_PROFILE_VALUE)
423
+ .map(session => (authority ? { ...session, providerAuthority: authority } : session));
424
+ });
425
+ const names = new Set<string>();
426
+ for (const session of discovered) {
427
+ if (names.has(session.name)) throw new Error(`gjc_tmux_provider_authority_ambiguous:${session.name}`);
428
+ names.add(session.name);
429
+ }
430
+ return discovered.sort((a, b) => a.name.localeCompare(b.name));
299
431
  }
300
432
 
301
433
  /** @internal */
302
434
  export function listTmuxSessionsForGc(env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionsForGc {
303
- const sessions = listSessionLines(env)
304
- .map(parseSessionLine)
305
- .filter((session): session is GjcTmuxSessionStatus => session != null)
306
- .map(session => hydrateSessionFromExactOptions(session, env));
435
+ const authorityEnvironments = psmuxAuthorityEnvironments(env);
436
+ const sessions = authorityEnvironments.flatMap(authorityEnv =>
437
+ listSessionLines(authorityEnv)
438
+ .map(parseSessionLine)
439
+ .filter((session): session is GjcTmuxSessionStatus => session != null)
440
+ .map(session => hydrateSessionFromExactOptions(session, authorityEnv)),
441
+ );
307
442
  const tagged = sessions
308
443
  .filter(session => session.profile === GJC_TMUX_PROFILE_VALUE)
309
444
  .sort((a, b) => a.name.localeCompare(b.name));
310
445
  const taggedNames = new Set(tagged.map(session => session.name));
311
446
  const byName = new Map(sessions.map(session => [session.name, session]));
312
- const untagged = listRawTmuxSessionNames(env)
447
+ const untagged = authorityEnvironments
448
+ .flatMap(authorityEnv => listRawTmuxSessionNames(authorityEnv))
313
449
  .filter(name => !taggedNames.has(name))
314
450
  .map(
315
451
  name =>
@@ -367,7 +503,8 @@ export function createGjcTmuxSession(
367
503
  options: CreateGjcTmuxSessionOptions = {},
368
504
  ): GjcTmuxSessionStatus {
369
505
  const platform = options.platform ?? process.platform;
370
- const tmuxCommand = resolveGjcTmuxCommand(env, platform);
506
+ const provider = resolveGjcTmuxProviderContext({ env, platform });
507
+ const tmuxCommand = provider.command;
371
508
  const sessionName = buildGjcTmuxSessionName(env);
372
509
  const cwd = process.cwd();
373
510
  const sessionId = env[GJC_COORDINATOR_SESSION_ID_ENV]?.trim() || sessionName;
@@ -376,6 +513,7 @@ export function createGjcTmuxSession(
376
513
  tmuxRuntimeSessionPath(cwd, env.GJC_SESSION_ID?.trim() || sessionId, buildGjcTmuxSessionSlug(sessionName));
377
514
  const stateDir = (platform === "win32" ? path.win32 : path).dirname(stateFile);
378
515
  const generation = crypto.randomUUID();
516
+ const authority = bindGjcTmuxProviderAuthority(provider, { stateDir, sessionId, generation });
379
517
  const childEnvironment: Record<string, string> = {
380
518
  GJC_TMUX_LAUNCHED: "1",
381
519
  [GJC_TMUX_OWNER_GENERATION_ENV]: generation,
@@ -384,6 +522,7 @@ export function createGjcTmuxSession(
384
522
  [GJC_COORDINATOR_SESSION_ID_ENV]: sessionId,
385
523
  [GJC_COORDINATOR_SESSION_STATE_FILE_ENV]: stateFile,
386
524
  };
525
+ const executionEnv = { ...env, ...childEnvironment };
387
526
  const shellQuote = (value: string): string => `'${value.replace(/'/g, `'\\''`)}'`;
388
527
  const command =
389
528
  platform === "win32"
@@ -393,12 +532,13 @@ export function createGjcTmuxSession(
393
532
  .join(" ")} gjc`;
394
533
  const tmuxArgv = [
395
534
  tmuxCommand,
396
- "new-session",
397
- "-d",
398
- "-s",
399
- sessionName,
400
- ...(platform === "win32" ? [] : ["-P", "-F", "#{session_id}"]),
401
- command,
535
+ ...buildTmuxProviderCommand(provider, "new-session", [
536
+ "-d",
537
+ "-s",
538
+ sessionName,
539
+ ...(platform === "win32" ? [] : ["-P", "-F", "#{session_id}"]),
540
+ command,
541
+ ]),
402
542
  ];
403
543
  function probeTmuxServer(tmuxCommand: string, env: NodeJS.ProcessEnv): TmuxServerProof {
404
544
  if (platform !== "linux") {
@@ -446,7 +586,7 @@ export function createGjcTmuxSession(
446
586
  }
447
587
  }
448
588
 
449
- const probeServer = () => probeTmuxServer(tmuxCommand, env);
589
+ const probeServer = () => probeTmuxServer(tmuxCommand, executionEnv);
450
590
 
451
591
  const testOwnerProbe = createOwnerIsolationTestDependencies?.probe;
452
592
  const ownerProbe: OwnerIsolationProbeSync = {
@@ -490,8 +630,6 @@ export function createGjcTmuxSession(
490
630
  }
491
631
  }),
492
632
  };
493
- if (resolveGjcTmuxBinary({ env, platform }).isPsmux)
494
- throw new Error("gjc_tmux_owner_isolation_native_session_identity_unavailable");
495
633
 
496
634
  const baseline = captureOwnerGenerationBaselineSync(stateDir, sessionId);
497
635
  const ownerPlan = planTmuxOwnerIsolationSync(
@@ -510,19 +648,25 @@ export function createGjcTmuxSession(
510
648
  ownerProbe,
511
649
  );
512
650
  if (!ownerPlan.ok) throw new Error(`gjc_tmux_owner_isolation_${ownerPlan.code}:${ownerPlan.diagnostic}`);
651
+ persistGjcTmuxProviderAuthoritySync(authority);
513
652
 
514
653
  const outcome = (createOwnerIsolationTestDependencies?.execute ?? executeTmuxOwnerIsolationPlanSync)(ownerPlan, {
515
654
  socketKey: tmuxCommand,
516
655
  spawn: (argv, stdinLine) => {
517
- const result = stdinLine
518
- ? Bun.spawnSync(argv, {
519
- stdout: "pipe",
520
- stderr: "pipe",
521
- stdin: Buffer.from(stdinLine),
522
- env,
523
- })
524
- : Bun.spawnSync(argv, { stdout: "pipe", stderr: "pipe", env });
525
- return { exitCode: result.exitCode, stdout: result.stdout.toString() };
656
+ assertGjcTmuxStagedMutationAuthoritySync(authority);
657
+ try {
658
+ const result = stdinLine
659
+ ? Bun.spawnSync(argv, {
660
+ stdout: "pipe",
661
+ stderr: "pipe",
662
+ stdin: Buffer.from(stdinLine),
663
+ env: executionEnv,
664
+ })
665
+ : Bun.spawnSync(argv, { stdout: "pipe", stderr: "pipe", env: executionEnv });
666
+ return { exitCode: result.exitCode, stdout: result.stdout.toString() };
667
+ } finally {
668
+ assertGjcTmuxStagedMutationAuthoritySync(authority);
669
+ }
526
670
  },
527
671
  probeServer: ownerProbe.probeServer,
528
672
  isCurrentGeneration: () => isOwnerGenerationBaselineCurrentSync(stateDir, sessionId, baseline),
@@ -532,108 +676,92 @@ export function createGjcTmuxSession(
532
676
  nativeSessionId,
533
677
  execution.attempt_session,
534
678
  tmuxCommand,
535
- env,
679
+ executionEnv,
536
680
  server.pid,
537
681
  server.startTime,
538
682
  server.pidProven,
683
+ authority,
539
684
  );
540
685
  },
541
686
  });
542
687
  if (!outcome.ok) throw new Error(`gjc_tmux_owner_isolation_${outcome.code}:${outcome.diagnostic}`);
543
688
 
544
- const nativeSessionId = outcome.native_session_id;
689
+ const nativeSessionId = outcome.native_session_id ?? (provider.binary.isPsmux ? sessionName : undefined);
545
690
  if (!nativeSessionId) throw new Error("gjc_tmux_owner_isolation_native_session_identity_unavailable");
546
- const server = requireSafeTmuxServerForMutation(tmuxCommand, env);
691
+ const psmuxIncarnation = provider.binary.isPsmux ? crypto.randomUUID() : undefined;
692
+ const server = requireSafeTmuxServerForMutation(tmuxCommand, executionEnv);
547
693
  if (server.pid !== outcome.server_pid || server.startTime !== outcome.server_start_time)
548
694
  throw new Error("gjc_tmux_owner_changed_after_create");
549
- if (!isNativeTmuxSessionBoundToName(nativeSessionId, sessionName, env))
695
+ if (!provider.binary.isPsmux && !isNativeTmuxSessionBoundToName(nativeSessionId, sessionName, executionEnv))
550
696
  throw new Error("gjc_tmux_owner_changed_after_create");
551
697
 
698
+ const createdMetadata = {
699
+ sessionId,
700
+ sessionStateFile: stateFile,
701
+ ownerGeneration: generation,
702
+ ownerServerKey: tmuxCommand,
703
+ version: env.npm_package_version ?? null,
704
+ };
552
705
  try {
553
706
  tagCreatedTmuxSession(
554
707
  nativeSessionId,
555
708
  sessionName,
556
709
  { pid: outcome.server_pid, pidProven: server.pidProven },
557
- env,
558
- {
559
- sessionId,
560
- sessionStateFile: stateFile,
561
- ownerGeneration: generation,
562
- ownerServerKey: tmuxCommand,
563
- version: env.npm_package_version ?? null,
564
- },
710
+ executionEnv,
711
+ { ...createdMetadata },
565
712
  tmuxCommand,
713
+ authority,
566
714
  );
567
- } catch (tagError) {
568
- try {
569
- cleanupExactCreatedTmuxSession(
570
- nativeSessionId,
571
- sessionName,
572
- tmuxCommand,
573
- env,
574
- outcome.server_pid,
575
- outcome.server_start_time,
576
- server.pidProven,
715
+ if (psmuxIncarnation) {
716
+ runTmux(
717
+ [
718
+ "set-option",
719
+ "-t",
720
+ normalizeExactTmuxTarget(sessionName, executionEnv, "option"),
721
+ GJC_TMUX_PSMUX_INCARNATION_OPTION,
722
+ psmuxIncarnation,
723
+ ],
724
+ executionEnv,
725
+ authority,
577
726
  );
578
- } catch (cleanupError) {
579
- throw new AggregateError([tagError, cleanupError], "gjc_tmux_profile_tag_failed_cleanup_failed");
580
727
  }
581
- throw tagError;
582
- }
583
-
584
- if (nativeSessionId) {
585
- const firstServer = requireSafeTmuxServerForMutation(tmuxCommand, env);
728
+ if (!createdTmuxMetadataMatches(nativeSessionId, createdMetadata, psmuxIncarnation, executionEnv, authority))
729
+ throw new Error("gjc_tmux_created_metadata_mismatch");
730
+ const reprovenIdentity = proveGjcTmuxSessionMutationTarget(sessionName, executionEnv, authority);
731
+ if (
732
+ reprovenIdentity.nativeSessionId !== nativeSessionId ||
733
+ reprovenIdentity.serverPid !== outcome.server_pid ||
734
+ reprovenIdentity.serverStartTime !== outcome.server_start_time ||
735
+ reprovenIdentity.psmuxIncarnation !== psmuxIncarnation
736
+ )
737
+ throw new Error("gjc_tmux_owner_changed_after_create");
738
+ const firstServer = requireSafeTmuxServerForMutation(tmuxCommand, executionEnv);
586
739
  if (firstServer.pid !== outcome.server_pid || firstServer.startTime !== outcome.server_start_time)
587
740
  throw new Error("gjc_tmux_owner_changed_after_create");
588
- const status = statusGjcTmuxSessionByNativeId(nativeSessionId, env);
589
- const finalServer = requireSafeTmuxServerForMutation(tmuxCommand, env);
741
+ const finalServer = requireSafeTmuxServerForMutation(tmuxCommand, executionEnv);
590
742
  if (finalServer.pid !== firstServer.pid || finalServer.startTime !== firstServer.startTime)
591
743
  throw new Error("gjc_tmux_owner_changed_after_create");
592
- try {
593
- replaceOwnerGenerationSync(stateDir, sessionId, generation, baseline);
594
- } catch (publicationError) {
595
- try {
596
- cleanupExactCreatedTmuxSession(
597
- nativeSessionId,
598
- sessionName,
599
- tmuxCommand,
600
- env,
601
- outcome.server_pid,
602
- outcome.server_start_time,
603
- finalServer.pidProven,
604
- );
605
- } catch (cleanupError) {
606
- throw new AggregateError(
607
- [publicationError, cleanupError],
608
- "gjc_tmux_owner_generation_publish_failed_cleanup_failed",
609
- );
610
- }
611
- throw publicationError;
612
- }
613
- return status;
614
- }
615
- try {
616
744
  replaceOwnerGenerationSync(stateDir, sessionId, generation, baseline);
617
- } catch (publicationError) {
745
+ } catch (precommitError) {
618
746
  try {
619
747
  cleanupExactCreatedTmuxSession(
620
748
  nativeSessionId,
621
749
  sessionName,
622
750
  tmuxCommand,
623
- env,
751
+ executionEnv,
624
752
  outcome.server_pid,
625
753
  outcome.server_start_time,
626
754
  server.pidProven,
755
+ authority,
627
756
  );
628
757
  } catch (cleanupError) {
629
- throw new AggregateError(
630
- [publicationError, cleanupError],
631
- "gjc_tmux_owner_generation_publish_failed_cleanup_failed",
632
- );
758
+ throw new AggregateError([precommitError, cleanupError], "gjc_tmux_precommit_failed_cleanup_failed");
633
759
  }
634
- throw publicationError;
760
+ throw precommitError;
635
761
  }
636
- return statusGjcTmuxSession(sessionName, env);
762
+ return provider.binary.isPsmux
763
+ ? statusGjcTmuxSession(sessionName, executionEnv)
764
+ : statusGjcTmuxSessionByNativeId(nativeSessionId, executionEnv);
637
765
  }
638
766
 
639
767
  function statusGjcTmuxSessionByNativeId(nativeSessionId: string, env: NodeJS.ProcessEnv): GjcTmuxSessionStatus {
@@ -665,12 +793,16 @@ function guardedTmuxSessionPredicate(
665
793
  nativeSessionId: string,
666
794
  sessionName: string,
667
795
  expectedOwnerGeneration?: string,
796
+ expectedPsmuxIncarnation?: string,
668
797
  ): string {
669
798
  const ownerGenerationPredicate = expectedOwnerGeneration
670
799
  ? `#{==:#{${GJC_TMUX_OWNER_GENERATION_OPTION}},${expectedOwnerGeneration}}`
671
800
  : "1";
672
801
  const serverPidPredicate = expectedServer.pidProven === false ? "1" : `#{==:#{pid},${expectedServer.pid}}`;
673
- return `#{&&:${serverPidPredicate},#{&&:#{==:#{session_id},${nativeSessionId}},#{&&:#{==:#{session_name},${sessionName}},${ownerGenerationPredicate}}}}`;
802
+ if (!expectedPsmuxIncarnation)
803
+ return `#{&&:${serverPidPredicate},#{&&:#{==:#{session_id},${nativeSessionId}},#{&&:#{==:#{session_name},${sessionName}},${ownerGenerationPredicate}}}}`;
804
+ const psmuxIncarnationPredicate = `#{==:#{${GJC_TMUX_PSMUX_INCARNATION_OPTION}},${expectedPsmuxIncarnation}}`;
805
+ return `#{&&:${serverPidPredicate},#{&&:#{==:#{session_id},${nativeSessionId}},#{&&:#{==:#{session_name},${sessionName}},#{&&:${ownerGenerationPredicate},${psmuxIncarnationPredicate}}}}}`;
674
806
  }
675
807
 
676
808
  function runGuardedTmuxSessionCommand(
@@ -680,6 +812,8 @@ function runGuardedTmuxSessionCommand(
680
812
  env: NodeJS.ProcessEnv,
681
813
  thenCommand: string,
682
814
  expectedOwnerGeneration?: string,
815
+ provisionalAuthority?: ProviderAuthority,
816
+ expectedPsmuxIncarnation?: string,
683
817
  ): void {
684
818
  const result = runTmux(
685
819
  [
@@ -687,11 +821,18 @@ function runGuardedTmuxSessionCommand(
687
821
  "-t",
688
822
  normalizeExactTmuxTarget(nativeSessionId, env, "session"),
689
823
  "-F",
690
- guardedTmuxSessionPredicate(expectedServer, nativeSessionId, sessionName, expectedOwnerGeneration),
824
+ guardedTmuxSessionPredicate(
825
+ expectedServer,
826
+ nativeSessionId,
827
+ sessionName,
828
+ expectedOwnerGeneration,
829
+ expectedPsmuxIncarnation,
830
+ ),
691
831
  `${thenCommand} ; display-message -p __gjc_tmux_guarded_mutation_ok__`,
692
832
  "display-message -p __gjc_tmux_guarded_mutation_refused__",
693
833
  ],
694
834
  env,
835
+ provisionalAuthority,
695
836
  ).trim();
696
837
  if (result !== "__gjc_tmux_guarded_mutation_ok__") throw new Error("gjc_tmux_cleanup_target_changed");
697
838
  }
@@ -712,12 +853,21 @@ function tagCreatedTmuxSession(
712
853
  version?: string | null;
713
854
  },
714
855
  tmuxCommand: string,
856
+ provisionalAuthority?: ProviderAuthority,
715
857
  ): void {
716
858
  const target = `${nativeSessionId}:`;
717
859
  const commands = buildGjcTmuxProfileCommands(target, env, metadata, { tmuxCommand })
718
860
  .map(command => command.args.map(tmuxCommandArgument).join(" "))
719
861
  .join(" ; ");
720
- runGuardedTmuxSessionCommand(nativeSessionId, sessionName, expectedServer, env, commands);
862
+ runGuardedTmuxSessionCommand(
863
+ nativeSessionId,
864
+ sessionName,
865
+ expectedServer,
866
+ env,
867
+ commands,
868
+ undefined,
869
+ provisionalAuthority,
870
+ );
721
871
  }
722
872
 
723
873
  function cleanupExactCreatedTmuxSession(
@@ -728,6 +878,7 @@ function cleanupExactCreatedTmuxSession(
728
878
  expectedPid: number,
729
879
  expectedStartTime: string,
730
880
  expectedPidProven?: boolean,
881
+ provisionalAuthority?: ProviderAuthority,
731
882
  ): void {
732
883
  const server = requireSafeTmuxServerForMutation(tmuxCommand, env);
733
884
  if (server.pid !== expectedPid || server.startTime !== expectedStartTime)
@@ -738,6 +889,8 @@ function cleanupExactCreatedTmuxSession(
738
889
  { pid: expectedPid, pidProven: expectedPidProven ?? server.pidProven },
739
890
  env,
740
891
  `kill-session -t ${tmuxCommandArgument(normalizeExactTmuxTarget(nativeSessionId, env, "session"))}`,
892
+ undefined,
893
+ provisionalAuthority,
741
894
  );
742
895
  }
743
896
 
@@ -786,33 +939,44 @@ function requireSafeTmuxServerForMutation(
786
939
  export function proveGjcTmuxSessionMutationTarget(
787
940
  sessionName: string,
788
941
  env: NodeJS.ProcessEnv = process.env,
942
+ provisionalAuthority?: ProviderAuthority,
789
943
  ): ProvenTmuxSessionIdentity {
790
- const session = statusGjcTmuxSession(sessionName, env);
791
- if (readProfileForExactTarget(session.name, env) !== GJC_TMUX_PROFILE_VALUE)
944
+ const target = provisionalAuthority ? sessionName : statusGjcTmuxSession(sessionName, env).name;
945
+ if (readProfileForExactTarget(target, env, provisionalAuthority) !== GJC_TMUX_PROFILE_VALUE)
792
946
  throw new Error(`gjc_tmux_session_not_managed:${sessionName}`);
793
947
  const firstServer = requireSafeTmuxServerForMutation(resolveGjcTmuxCommand(env), env);
794
- if (resolveGjcTmuxBinary({ env }).isPsmux) throw new Error(`gjc_tmux_owner_unverifiable:${sessionName}`);
795
- const nativeSessionId = readNativeTmuxSessionId(session.name, env);
948
+ const nativeSessionId = readNativeTmuxSessionId(target, env, provisionalAuthority);
796
949
  if (!nativeSessionId) throw new Error(`gjc_tmux_owner_unverifiable:${sessionName}`);
797
950
  if (
798
- readNativeTmuxSessionId(nativeSessionId, env) !== nativeSessionId ||
799
- readProfileForExactTarget(nativeSessionId, env) !== GJC_TMUX_PROFILE_VALUE
951
+ readNativeTmuxSessionId(nativeSessionId, env, provisionalAuthority) !== nativeSessionId ||
952
+ readProfileForExactTarget(nativeSessionId, env, provisionalAuthority) !== GJC_TMUX_PROFILE_VALUE
800
953
  )
801
954
  throw new Error(`gjc_tmux_owner_changed:${sessionName}`);
802
955
  const finalServer = requireSafeTmuxServerForMutation(resolveGjcTmuxCommand(env), env);
803
956
  if (finalServer.pid !== firstServer.pid || finalServer.startTime !== firstServer.startTime)
804
957
  throw new Error(`gjc_tmux_owner_changed:${sessionName}`);
958
+ const psmuxIncarnation = resolveGjcTmuxBinary({ env }).isPsmux
959
+ ? readExactOptionForGc(target, GJC_TMUX_PSMUX_INCARNATION_OPTION, env, provisionalAuthority)
960
+ : undefined;
961
+ if (resolveGjcTmuxBinary({ env }).isPsmux && !psmuxIncarnation)
962
+ throw new Error(`gjc_tmux_owner_unverifiable:${sessionName}`);
805
963
  return {
806
964
  nativeSessionId,
807
965
  serverPid: finalServer.pid,
808
966
  serverStartTime: finalServer.startTime,
967
+ psmuxIncarnation,
809
968
  };
810
969
  }
811
970
 
812
- function readProfileForExactTarget(sessionName: string, env: NodeJS.ProcessEnv): string {
971
+ function readProfileForExactTarget(
972
+ sessionName: string,
973
+ env: NodeJS.ProcessEnv,
974
+ provisionalAuthority?: ProviderAuthority,
975
+ ): string {
813
976
  const raw = runTmux(
814
977
  ["show-options", "-qv", "-t", normalizeExactTmuxTarget(sessionName, env, "option"), GJC_TMUX_PROFILE_OPTION],
815
978
  env,
979
+ provisionalAuthority,
816
980
  ).trim();
817
981
  // tmux returns just the value; psmux returns `key value`. Strip the
818
982
  // leading key on psmux so the GJC_TMUX_PROFILE_VALUE equality check
@@ -824,15 +988,20 @@ function readProfileForExactTarget(sessionName: string, env: NodeJS.ProcessEnv):
824
988
  return raw;
825
989
  }
826
990
 
827
- function readExactOptionForGc(sessionName: string, option: string, env: NodeJS.ProcessEnv): string | undefined {
991
+ function readExactOptionForGc(
992
+ sessionName: string,
993
+ option: string,
994
+ env: NodeJS.ProcessEnv,
995
+ provisionalAuthority?: ProviderAuthority,
996
+ ): string | undefined {
828
997
  try {
829
998
  const raw = runTmux(
830
999
  ["show-options", "-qv", "-t", normalizeExactTmuxTarget(sessionName, env, "option"), option],
831
1000
  env,
1001
+ provisionalAuthority,
832
1002
  ).trim();
833
1003
  if (!raw) return undefined;
834
- // tmux returns just the option value (e.g. `1` for @gjc-profile).
835
- // psmux 3.3.0 returns `key value` (or `key "value with space"` for
1004
+ // tmux returns just the value; psmux returns `key value` (or `key "value with space"` for
836
1005
  // @gjc-branch etc.). On psmux, parse the last token and strip any
837
1006
  // surrounding double quotes so both shapes resolve to the same value.
838
1007
  if (resolveGjcTmuxBinary({ env }).isPsmux) {
@@ -851,13 +1020,69 @@ function readExactOptionForGc(sessionName: string, option: string, env: NodeJS.P
851
1020
  return undefined;
852
1021
  }
853
1022
  }
1023
+ function readCreatedTmuxMetadataOption(
1024
+ nativeSessionId: string,
1025
+ option: string,
1026
+ env: NodeJS.ProcessEnv,
1027
+ provisionalAuthority?: ProviderAuthority,
1028
+ ): string | undefined {
1029
+ try {
1030
+ const raw = runTmux(
1031
+ ["show-options", "-qv", "-t", normalizeExactTmuxTarget(nativeSessionId, env, "option"), option],
1032
+ env,
1033
+ provisionalAuthority,
1034
+ ).replace(/\r?\n$/, "");
1035
+ if (!raw) return undefined;
1036
+ if (!resolveGjcTmuxBinary({ env }).isPsmux) return raw;
1037
+ const prefix = `${option} `;
1038
+ if (!raw.startsWith(prefix)) return undefined;
1039
+ const value = raw.slice(prefix.length);
1040
+ if (value.startsWith('"') && value.endsWith('"')) return value.slice(1, -1);
1041
+ return value;
1042
+ } catch {
1043
+ return undefined;
1044
+ }
1045
+ }
1046
+
1047
+ function createdTmuxMetadataMatches(
1048
+ nativeSessionId: string,
1049
+ metadata: {
1050
+ sessionId: string;
1051
+ sessionStateFile: string;
1052
+ ownerGeneration: string;
1053
+ ownerServerKey: string;
1054
+ version: string | null;
1055
+ },
1056
+ psmuxIncarnation: string | undefined,
1057
+ env: NodeJS.ProcessEnv,
1058
+ provisionalAuthority?: ProviderAuthority,
1059
+ ): boolean {
1060
+ const expected: Array<readonly [string, string]> = [
1061
+ [GJC_TMUX_PROFILE_OPTION, GJC_TMUX_PROFILE_VALUE],
1062
+ [GJC_TMUX_SESSION_ID_OPTION, metadata.sessionId],
1063
+ [GJC_TMUX_SESSION_STATE_FILE_OPTION, metadata.sessionStateFile],
1064
+ [GJC_TMUX_OWNER_GENERATION_OPTION, metadata.ownerGeneration],
1065
+ [GJC_TMUX_OWNER_SERVER_KEY_OPTION, metadata.ownerServerKey],
1066
+ ];
1067
+ if (metadata.version) expected.push([GJC_TMUX_VERSION_OPTION, metadata.version]);
1068
+ if (psmuxIncarnation) expected.push([GJC_TMUX_PSMUX_INCARNATION_OPTION, psmuxIncarnation]);
1069
+ return expected.every(
1070
+ ([option, intended]) =>
1071
+ readCreatedTmuxMetadataOption(nativeSessionId, option, env, provisionalAuthority) === intended,
1072
+ );
1073
+ }
854
1074
 
855
- function readNativeTmuxSessionId(sessionTarget: string, env: NodeJS.ProcessEnv): string | undefined {
856
- if (resolveGjcTmuxBinary({ env }).isPsmux) return undefined;
1075
+ function readNativeTmuxSessionId(
1076
+ sessionTarget: string,
1077
+ env: NodeJS.ProcessEnv,
1078
+ provisionalAuthority?: ProviderAuthority,
1079
+ ): string | undefined {
1080
+ if (resolveGjcTmuxBinary({ env }).isPsmux) return sessionTarget;
857
1081
  try {
858
1082
  const sessionId = runTmux(
859
1083
  ["display-message", "-p", "-t", normalizeExactTmuxTarget(sessionTarget, env, "option"), "#{session_id}"],
860
1084
  env,
1085
+ provisionalAuthority,
861
1086
  ).trim();
862
1087
  return sessionId || undefined;
863
1088
  } catch {
@@ -885,12 +1110,13 @@ function isNativeTmuxSessionBoundToName(nativeSessionId: string, sessionName: st
885
1110
  }
886
1111
 
887
1112
  function hydrateSessionFromExactOptions(session: GjcTmuxSessionStatus, env: NodeJS.ProcessEnv): GjcTmuxSessionStatus {
888
- if (session.profile === GJC_TMUX_PROFILE_VALUE) return session;
889
- const profile = readExactOptionForGc(session.name, GJC_TMUX_PROFILE_OPTION, env);
890
- if (profile !== GJC_TMUX_PROFILE_VALUE) return session;
1113
+ if (session.profile !== GJC_TMUX_PROFILE_VALUE) {
1114
+ const profile = readExactOptionForGc(session.name, GJC_TMUX_PROFILE_OPTION, env);
1115
+ if (profile !== GJC_TMUX_PROFILE_VALUE) return session;
1116
+ session = { ...session, profile };
1117
+ }
891
1118
  return {
892
1119
  ...session,
893
- profile,
894
1120
  branch: session.branch ?? readExactOptionForGc(session.name, GJC_TMUX_BRANCH_OPTION, env),
895
1121
  branchSlug: session.branchSlug ?? readExactOptionForGc(session.name, GJC_TMUX_BRANCH_SLUG_OPTION, env),
896
1122
  project: session.project ?? readExactOptionForGc(session.name, GJC_TMUX_PROJECT_OPTION, env),
@@ -899,8 +1125,10 @@ function hydrateSessionFromExactOptions(session: GjcTmuxSessionStatus, env: Node
899
1125
  session.sessionStateFile ?? readExactOptionForGc(session.name, GJC_TMUX_SESSION_STATE_FILE_OPTION, env),
900
1126
  ownerGeneration:
901
1127
  session.ownerGeneration ?? readExactOptionForGc(session.name, GJC_TMUX_OWNER_GENERATION_OPTION, env),
902
-
903
1128
  version: session.version ?? readExactOptionForGc(session.name, GJC_TMUX_VERSION_OPTION, env),
1129
+ psmuxIncarnation:
1130
+ session.psmuxIncarnation ?? readExactOptionForGc(session.name, GJC_TMUX_PSMUX_INCARNATION_OPTION, env),
1131
+ nativeSessionId: session.nativeSessionId ?? readNativeTmuxSessionId(session.name, env),
904
1132
  };
905
1133
  }
906
1134
 
@@ -913,13 +1141,14 @@ export function readTmuxSessionTagsForGc(
913
1141
  return {
914
1142
  profile: readExactOptionForGc(sessionName, GJC_TMUX_PROFILE_OPTION, env),
915
1143
  project: readExactOptionForGc(sessionName, GJC_TMUX_PROJECT_OPTION, env),
1144
+ psmuxIncarnation: readExactOptionForGc(sessionName, GJC_TMUX_PSMUX_INCARNATION_OPTION, env),
916
1145
  branch: readExactOptionForGc(sessionName, GJC_TMUX_BRANCH_OPTION, env),
917
1146
  branchSlug: readExactOptionForGc(sessionName, GJC_TMUX_BRANCH_SLUG_OPTION, env),
918
1147
  sessionId: readExactOptionForGc(sessionName, GJC_TMUX_SESSION_ID_OPTION, env),
919
1148
  sessionStateFile: readExactOptionForGc(sessionName, GJC_TMUX_SESSION_STATE_FILE_OPTION, env),
920
1149
  version: readExactOptionForGc(sessionName, GJC_TMUX_VERSION_OPTION, env),
921
1150
  ownerGeneration: readExactOptionForGc(sessionName, GJC_TMUX_OWNER_GENERATION_OPTION, env),
922
- nativeSessionId: session?.nativeSessionId,
1151
+ nativeSessionId: session?.nativeSessionId ?? readNativeTmuxSessionId(sessionName, env),
923
1152
  createdAt: session?.createdAt,
924
1153
  attached: session?.attached,
925
1154
  panePids: session?.panePids,
@@ -938,6 +1167,7 @@ export function removeGjcTmuxSession(
938
1167
  if (
939
1168
  expectedIdentity &&
940
1169
  (session.nativeSessionId !== expectedIdentity.nativeSessionId ||
1170
+ session.psmuxIncarnation !== expectedIdentity.psmuxIncarnation ||
941
1171
  session.ownerGeneration !== expectedIdentity.ownerGeneration ||
942
1172
  session.sessionId !== expectedIdentity.sessionId ||
943
1173
  session.sessionStateFile !== expectedIdentity.sessionStateFile ||
@@ -948,7 +1178,6 @@ export function removeGjcTmuxSession(
948
1178
  if (readProfileForExactTarget(session.name, env) !== GJC_TMUX_PROFILE_VALUE) {
949
1179
  throw new Error(`gjc_tmux_session_not_managed:${sessionName}`);
950
1180
  }
951
- if (resolveGjcTmuxBinary({ env }).isPsmux) throw new Error(`gjc_tmux_owner_unverifiable:${sessionName}`);
952
1181
  const nativeSessionId = readNativeTmuxSessionId(session.name, env);
953
1182
  if (!nativeSessionId) throw new Error(`gjc_tmux_owner_unverifiable:${sessionName}`);
954
1183
  if (expectedIdentity && nativeSessionId !== expectedIdentity.nativeSessionId)
@@ -992,8 +1221,8 @@ async function readProcessStartTime(pid: number): Promise<string | null> {
992
1221
  function exactManagedOwnerSupervisor(supervisorPid: number, supervisorStartTime: string): Process {
993
1222
  const supervisor = Process.fromPid(supervisorPid);
994
1223
  if (!supervisor) throw new Error("managed_owner_supervisor_unverifiable");
995
- if (process.platform === "linux" && supervisor.incarnation !== `linux:${supervisorStartTime}`)
996
- throw new Error("managed_owner_supervisor_incarnation_mismatch");
1224
+ const expectedIncarnation = process.platform === "linux" ? `linux:${supervisorStartTime}` : supervisorStartTime;
1225
+ if (supervisor.incarnation !== expectedIncarnation) throw new Error("managed_owner_supervisor_incarnation_mismatch");
997
1226
  return supervisor;
998
1227
  }
999
1228
 
@@ -1069,6 +1298,7 @@ async function requireUnchangedOwnerForCompatibilityCleanup(
1069
1298
  identity: ExactOwnerIdentity,
1070
1299
  initialStateFile: string,
1071
1300
  initialServer: { pid: number; startTime: string },
1301
+ initialPsmuxIncarnation: string | undefined,
1072
1302
  listPanePids: (sessionName: string, env: NodeJS.ProcessEnv) => number[],
1073
1303
  readStartTime: (pid: number) => Promise<string | null>,
1074
1304
  ): Promise<boolean> {
@@ -1107,6 +1337,10 @@ async function requireUnchangedOwnerForCompatibilityCleanup(
1107
1337
  readExactOptionForGc(nativeSessionId, GJC_TMUX_OWNER_SERVER_KEY_OPTION, env) !== identity.socketKey
1108
1338
  ? "server_key"
1109
1339
  : null,
1340
+ initialPsmuxIncarnation !== undefined &&
1341
+ readExactOptionForGc(nativeSessionId, GJC_TMUX_PSMUX_INCARNATION_OPTION, env) !== initialPsmuxIncarnation
1342
+ ? "psmux_incarnation"
1343
+ : null,
1110
1344
  ].filter((value): value is string => value !== null);
1111
1345
  if (mismatches.length > 0) throw new Error(`gjc_tmux_owner_changed:${sessionName}:${mismatches.join(",")}`);
1112
1346
  return true;
@@ -1173,35 +1407,44 @@ export async function forceCloseGjcTmuxSession(
1173
1407
  expectedStateFile?: string,
1174
1408
  deps: Partial<ForceCloseOwnerDependencies> = {},
1175
1409
  ): Promise<GjcTmuxSessionStatus> {
1176
- if (resolveGjcTmuxBinary({ env }).isPsmux) throw new Error(`gjc_tmux_owner_unverifiable:${sessionName}`);
1177
1410
  const session = statusGjcTmuxSession(sessionName, env);
1178
- if (readProfileForExactTarget(session.name, env) !== GJC_TMUX_PROFILE_VALUE)
1411
+ const sessionEnv = environmentForProviderAuthority(env, session.providerAuthority);
1412
+ if (readProfileForExactTarget(session.name, sessionEnv) !== GJC_TMUX_PROFILE_VALUE)
1179
1413
  throw new Error(`gjc_tmux_session_not_managed:${sessionName}`);
1180
- const exactPanePids = (deps.listPanePids ?? readExactSessionPanePids)(session.name, env);
1414
+ const exactPanePids = (deps.listPanePids ?? readExactSessionPanePids)(session.name, sessionEnv);
1181
1415
  if (exactPanePids.length !== 1) throw new Error(`gjc_tmux_owner_unverifiable:${sessionName}`);
1182
- const actualSessionId = readExactOptionForGc(session.name, GJC_TMUX_SESSION_ID_OPTION, env);
1183
- const actualStateFile = readExactOptionForGc(session.name, GJC_TMUX_SESSION_STATE_FILE_OPTION, env);
1184
- const actualGeneration = readExactOptionForGc(session.name, GJC_TMUX_OWNER_GENERATION_OPTION, env);
1185
- const actualServerKey = readExactOptionForGc(session.name, GJC_TMUX_OWNER_SERVER_KEY_OPTION, env);
1186
-
1416
+ const actualSessionId = readExactOptionForGc(session.name, GJC_TMUX_SESSION_ID_OPTION, sessionEnv);
1417
+ const actualStateFile = readExactOptionForGc(session.name, GJC_TMUX_SESSION_STATE_FILE_OPTION, sessionEnv);
1418
+ const actualGeneration = readExactOptionForGc(session.name, GJC_TMUX_OWNER_GENERATION_OPTION, sessionEnv);
1419
+ const actualServerKey = readExactOptionForGc(session.name, GJC_TMUX_OWNER_SERVER_KEY_OPTION, sessionEnv);
1420
+ const isPsmux = resolveGjcTmuxBinary({ env: sessionEnv }).isPsmux;
1421
+ const initialPsmuxIncarnation = isPsmux
1422
+ ? readExactOptionForGc(session.name, GJC_TMUX_PSMUX_INCARNATION_OPTION, sessionEnv)
1423
+ : undefined;
1187
1424
  if (expectedSessionId !== undefined && actualSessionId !== expectedSessionId)
1188
1425
  throw new Error(`gjc_tmux_session_id_mismatch:${sessionName}`);
1189
1426
  if (expectedStateFile !== undefined && actualStateFile !== expectedStateFile)
1190
1427
  throw new Error(`gjc_tmux_session_state_file_mismatch:${sessionName}`);
1191
- if (!actualSessionId || !actualStateFile || !actualGeneration || !actualServerKey)
1428
+ if (
1429
+ !actualSessionId ||
1430
+ !actualStateFile ||
1431
+ !actualGeneration ||
1432
+ !actualServerKey ||
1433
+ (isPsmux && !initialPsmuxIncarnation)
1434
+ )
1192
1435
  throw new Error(`gjc_tmux_owner_unverifiable:${sessionName}`);
1193
- const nativeSessionId = readNativeTmuxSessionId(session.name, env);
1436
+ const nativeSessionId = readNativeTmuxSessionId(session.name, sessionEnv);
1194
1437
  if (!nativeSessionId) throw new Error(`gjc_tmux_owner_unverifiable:${sessionName}`);
1195
1438
 
1196
1439
  const resolveOwner =
1197
1440
  deps.resolveOwner ?? ((name, targetEnv) => resolveExactOwner(name, targetEnv, exactPanePids[0]!));
1198
- const identity = await resolveOwner(session.name, env);
1441
+ const identity = await resolveOwner(session.name, sessionEnv);
1199
1442
  if (identity.pid !== exactPanePids[0]) throw new Error(`gjc_tmux_owner_identity_mismatch:${sessionName}`);
1200
1443
  if (identity.sessionId !== actualSessionId || identity.stateDir !== path.dirname(actualStateFile))
1201
1444
  throw new Error(`gjc_tmux_owner_identity_mismatch:${sessionName}`);
1202
1445
  if (identity.generation !== actualGeneration) throw new Error(`gjc_tmux_owner_generation_mismatch:${sessionName}`);
1203
1446
  if (identity.socketKey !== actualServerKey) throw new Error(`gjc_tmux_owner_server_key_mismatch:${sessionName}`);
1204
- const initialServer = requireSafeTmuxServerForMutation(resolveGjcTmuxCommand(env), env);
1447
+ const initialServer = requireSafeTmuxServerForMutation(resolveGjcTmuxCommand(sessionEnv), sessionEnv);
1205
1448
 
1206
1449
  const currentStartTime = await (deps.readProcessStartTime ?? readProcessStartTime)(identity.pid);
1207
1450
  if (currentStartTime !== identity.startTime) throw new Error("owner_pid_identity_mismatch");
@@ -1259,23 +1502,26 @@ export async function forceCloseGjcTmuxSession(
1259
1502
  const cleanupRequired = await requireUnchangedOwnerForCompatibilityCleanup(
1260
1503
  session.name,
1261
1504
  nativeSessionId,
1262
- env,
1505
+ sessionEnv,
1263
1506
  identity,
1264
1507
  actualStateFile,
1265
1508
  initialServer,
1509
+ initialPsmuxIncarnation,
1266
1510
  deps.listPanePids ?? readExactSessionPanePids,
1267
1511
  deps.readProcessStartTime ?? readProcessStartTime,
1268
1512
  );
1269
1513
  if (!cleanupRequired) return;
1270
- if (deps.cleanupSession) deps.cleanupSession(nativeSessionId, env);
1514
+ if (deps.cleanupSession) deps.cleanupSession(nativeSessionId, sessionEnv);
1271
1515
  else
1272
1516
  runGuardedTmuxSessionCommand(
1273
1517
  nativeSessionId,
1274
1518
  session.name,
1275
1519
  initialServer,
1276
- env,
1520
+ sessionEnv,
1277
1521
  `kill-session -t '${nativeSessionId}'`,
1278
1522
  identity.generation,
1523
+ session.providerAuthority,
1524
+ initialPsmuxIncarnation,
1279
1525
  );
1280
1526
  },
1281
1527
  },
@@ -1284,18 +1530,25 @@ export async function forceCloseGjcTmuxSession(
1284
1530
  }
1285
1531
 
1286
1532
  export function attachGjcTmuxSession(sessionName: string, env: NodeJS.ProcessEnv = process.env): never {
1287
- if (resolveGjcTmuxBinary({ env }).isPsmux) throw new Error(`gjc_tmux_owner_unverifiable:${sessionName}`);
1288
1533
  const session = statusGjcTmuxSession(sessionName, env);
1289
- const tmuxCommand = resolveGjcTmuxCommand(env);
1290
- requireSafeTmuxServerForMutation(tmuxCommand, env);
1534
+ const sessionEnv = environmentForProviderAuthority(env, session.providerAuthority);
1535
+ const authority = session.providerAuthority ?? psmuxAuthorityFromEnv(sessionEnv);
1536
+ const tmuxCommand = authority?.command ?? resolveGjcTmuxCommand(sessionEnv);
1537
+ if (authority) assertGjcTmuxMutationAuthoritySync(authority);
1538
+ requireSafeTmuxServerForMutation(tmuxCommand, sessionEnv);
1291
1539
  const result = Bun.spawnSync(
1292
- [tmuxCommand, "attach-session", "-t", buildGjcTmuxExactSessionTarget(session.name, { env })],
1293
- {
1294
- stdin: "inherit",
1295
- stdout: "inherit",
1296
- stderr: "inherit",
1297
- env,
1298
- },
1540
+ [
1541
+ tmuxCommand,
1542
+ ...(authority
1543
+ ? buildTmuxProviderCommand(authority, "attach-session", [
1544
+ "-t",
1545
+ buildGjcTmuxExactSessionTarget(session.name, { binary: authority.binary }),
1546
+ ])
1547
+ : ["attach-session", "-t", buildGjcTmuxExactSessionTarget(session.name, { env: sessionEnv })]),
1548
+ ],
1549
+ { stdin: "inherit", stdout: "inherit", stderr: "inherit", env: sessionEnv },
1299
1550
  );
1300
- process.exit(result.exitCode ?? 1);
1551
+ if (authority) assertGjcTmuxMutationAuthoritySync(authority);
1552
+ if (result.exitCode !== 0) throw new Error(`gjc_tmux_attach_failed:${sessionName}`);
1553
+ process.exit(0);
1301
1554
  }