@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.
- package/CHANGELOG.md +34 -0
- package/README.md +4 -1
- package/dist/types/async/job-manager.d.ts +36 -1
- package/dist/types/extensibility/extensions/runner.d.ts +9 -7
- package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
- package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
- package/dist/types/gjc-runtime/team-store.d.ts +0 -4
- package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +21 -4
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
- package/dist/types/modes/components/plugin-settings.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +18 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -3
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -2
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
- package/dist/types/session/agent-session.d.ts +5 -4
- package/dist/types/session/attempt-record-store.d.ts +19 -0
- package/dist/types/session/session-manager.d.ts +1 -0
- package/dist/types/session/session-storage.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +1 -0
- package/dist/types/task/types.d.ts +43 -0
- package/dist/types/tools/subagent.d.ts +15 -0
- package/dist/types/tools/todo-write.d.ts +3 -0
- package/package.json +8 -8
- package/scripts/capture-sticky-viewport-showcase.ts +131 -5
- package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
- package/scripts/verify-sticky-viewport-showcase.ts +620 -93
- package/src/async/job-manager.ts +430 -46
- package/src/config/model-profiles.ts +8 -0
- package/src/config/settings-schema.ts +17 -3
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
- package/src/extensibility/extensions/runner.ts +80 -6
- package/src/extensibility/extensions/wrapper.ts +22 -15
- package/src/extensibility/runtime-skill-discovery.ts +4 -6
- package/src/gjc-runtime/launch-tmux.ts +309 -293
- package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
- package/src/gjc-runtime/psmux-detect.ts +52 -6
- package/src/gjc-runtime/team-launch.ts +50 -2
- package/src/gjc-runtime/team-runtime.ts +716 -251
- package/src/gjc-runtime/team-store.ts +25 -7
- package/src/gjc-runtime/team-workers.ts +161 -1
- package/src/gjc-runtime/tmux-common.ts +16 -4
- package/src/gjc-runtime/tmux-gc.ts +37 -5
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
- package/src/gjc-runtime/tmux-provider-context.ts +451 -0
- package/src/gjc-runtime/tmux-sessions.ts +409 -156
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +360 -156
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/internal-urls/local-protocol.ts +39 -18
- package/src/modes/acp/acp-agent.ts +27 -13
- package/src/modes/components/gjc-bundle-settings.ts +2 -0
- package/src/modes/components/plugin-settings.ts +5 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/prompts/tools/monitor.md +4 -4
- package/src/sdk/broker/broker.ts +93 -10
- package/src/sdk/broker/lifecycle-ledger.ts +227 -11
- package/src/sdk/broker/lifecycle.ts +691 -92
- package/src/sdk/bus/chat-daemon-control.ts +30 -6
- package/src/sdk/bus/index.ts +23 -6
- package/src/sdk/bus/telegram-daemon-contract.ts +2 -2
- package/src/sdk/bus/telegram-daemon.ts +283 -44
- package/src/sdk/bus/topic-registry.ts +133 -10
- package/src/sdk/session.ts +7 -6
- package/src/session/agent-session.ts +752 -339
- package/src/session/attempt-record-store.ts +106 -0
- package/src/session/internal/managed-session-scope.ts +252 -67
- package/src/session/internal/managed-session-storage.ts +1 -0
- package/src/session/session-manager.ts +7 -0
- package/src/session/session-storage.ts +146 -24
- package/src/task/index.ts +230 -19
- package/src/task/provider-retry-status.ts +4 -1
- package/src/task/receipt.ts +2 -0
- package/src/task/types.ts +10 -0
- package/src/tools/monitor.ts +7 -1
- package/src/tools/output-meta.ts +10 -2
- package/src/tools/subagent.ts +97 -75
- 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
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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,
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
|
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 (
|
|
745
|
+
} catch (precommitError) {
|
|
618
746
|
try {
|
|
619
747
|
cleanupExactCreatedTmuxSession(
|
|
620
748
|
nativeSessionId,
|
|
621
749
|
sessionName,
|
|
622
750
|
tmuxCommand,
|
|
623
|
-
|
|
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
|
|
760
|
+
throw precommitError;
|
|
635
761
|
}
|
|
636
|
-
return
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
791
|
-
if (readProfileForExactTarget(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
856
|
-
|
|
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
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
|
|
996
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
1183
|
-
const actualStateFile = readExactOptionForGc(session.name, GJC_TMUX_SESSION_STATE_FILE_OPTION,
|
|
1184
|
-
const actualGeneration = readExactOptionForGc(session.name, GJC_TMUX_OWNER_GENERATION_OPTION,
|
|
1185
|
-
const actualServerKey = readExactOptionForGc(session.name, GJC_TMUX_OWNER_SERVER_KEY_OPTION,
|
|
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 (
|
|
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,
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
1290
|
-
|
|
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
|
-
[
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
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
|
-
|
|
1551
|
+
if (authority) assertGjcTmuxMutationAuthoritySync(authority);
|
|
1552
|
+
if (result.exitCode !== 0) throw new Error(`gjc_tmux_attach_failed:${sessionName}`);
|
|
1553
|
+
process.exit(0);
|
|
1301
1554
|
}
|