@gajae-code/coding-agent 0.15.4 → 0.15.5
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 +20 -1
- package/dist/types/cli/update-cli.d.ts +25 -5
- package/dist/types/config/autorouting-tier-map.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
- package/dist/types/sdk/cli/session-cli.d.ts +42 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/session-runtime.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +4 -3
- package/dist/types/tools/path-utils.d.ts +5 -6
- package/dist/types/tools/tmux-self-injection-guard.d.ts +15 -0
- package/package.json +7 -7
- package/src/cli/update-cli.ts +102 -1
- package/src/commands/sdk.ts +5 -0
- package/src/config/autorouting-tier-map.ts +1 -0
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +10 -4
- package/src/coordinator-mcp/policy.ts +1 -1
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/shared-events.ts +2 -0
- package/src/gjc-runtime/launch-worktree.ts +44 -7
- package/src/gjc-runtime/ralplan-runtime.ts +14 -1
- package/src/gjc-runtime/tmux-sessions.ts +145 -48
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/modes/prompt-action-autocomplete.ts +26 -47
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/kind-aware-reconciliation.ts +10 -2
- package/src/sdk/bus/prompt-reconciliation.ts +5 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
- package/src/sdk/cli/session-cli.ts +149 -4
- package/src/sdk/host/host.ts +7 -0
- package/src/sdk/host/session-runtime.test.ts +821 -22
- package/src/sdk/host/session-runtime.ts +669 -67
- package/src/sdk/router/session-router.ts +69 -15
- package/src/session/agent-session.ts +232 -15
- package/src/session/internal/managed-session-storage.ts +64 -26
- package/src/session/session-manager.ts +26 -8
- package/src/tools/bash.ts +14 -0
- package/src/tools/path-utils.ts +77 -29
- package/src/tools/tmux-self-injection-guard.ts +359 -0
|
@@ -536,6 +536,7 @@ export interface ExtensionContext {
|
|
|
536
536
|
onPreflightAcceptCommit?: () => void | Promise<void>;
|
|
537
537
|
onSkillPrepared?: (meta: { name: string; path: string; lineCount?: number; cleanedArgs?: string }) => void;
|
|
538
538
|
preflightSignal?: AbortSignal;
|
|
539
|
+
sdkRunToken?: string;
|
|
539
540
|
},
|
|
540
541
|
): Promise<unknown>;
|
|
541
542
|
setPlanMode?(on: boolean): unknown;
|
|
@@ -754,6 +755,8 @@ export interface AgentStartEvent extends SharedAgentStartEvent {
|
|
|
754
755
|
export interface AgentFailedEvent {
|
|
755
756
|
type: "agent_failed";
|
|
756
757
|
error: AgentFailureDiagnostic;
|
|
758
|
+
/** Internal SDK queue-owner binding for exact lifecycle attribution. */
|
|
759
|
+
sdkRunToken?: string;
|
|
757
760
|
/** Attempt correlation for the failing run, when scoped (matches the
|
|
758
761
|
* agent_start/agent_end scope contract). */
|
|
759
762
|
scope?: AttemptScope;
|
|
@@ -184,6 +184,8 @@ export interface AgentStartEvent {
|
|
|
184
184
|
export interface AgentEndEvent {
|
|
185
185
|
type: "agent_end";
|
|
186
186
|
messages: AgentMessage[];
|
|
187
|
+
/** Internal SDK queue-owner binding for exact lifecycle attribution. */
|
|
188
|
+
sdkRunToken?: string;
|
|
187
189
|
/** Indicates whether the loop ended normally, suspended, cancelled, or entered maintenance. */
|
|
188
190
|
stopReason?: "completed" | "paused" | "cancelled" | "maintenance";
|
|
189
191
|
/** Present for maintenance checkpoints; non-aborted checkpoints are not terminal. */
|
|
@@ -78,7 +78,7 @@ const WORKTREE_BUCKET_ENV = "GJC_WORKTREE_DIR";
|
|
|
78
78
|
* what keeps two repositories that share a branch name from resolving to one worktree.
|
|
79
79
|
*/
|
|
80
80
|
const REPO_NAME_PLACEHOLDER = "{repo}";
|
|
81
|
-
const DEFAULT_WORKTREE_BUCKET = `${REPO_NAME_PLACEHOLDER}
|
|
81
|
+
const DEFAULT_WORKTREE_BUCKET = `${REPO_NAME_PLACEHOLDER}/.worktrees`;
|
|
82
82
|
|
|
83
83
|
function expandHomePrefix(value: string, home: string, pathApi: typeof path.posix): string {
|
|
84
84
|
if (value === "~") return home;
|
|
@@ -108,14 +108,36 @@ export function resolveWorktreeBucketForPath(
|
|
|
108
108
|
/**
|
|
109
109
|
* Directory that holds this repository's launch worktrees.
|
|
110
110
|
*
|
|
111
|
-
* A relative override resolves against the repository's
|
|
112
|
-
*
|
|
113
|
-
*
|
|
111
|
+
* A relative override resolves against the repository's parent directory. The default
|
|
112
|
+
* `{repo}/.worktrees` template places managed worktrees inside the repository, while
|
|
113
|
+
* `{repo}.worktrees` adopts an existing sibling bucket. An absolute override is used verbatim.
|
|
114
114
|
*/
|
|
115
115
|
function resolveWorktreeBucket(repoRoot: string): string {
|
|
116
116
|
return resolveWorktreeBucketForPath(repoRoot, process.env[WORKTREE_BUCKET_ENV], os.homedir(), path);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
function ensureRepositoryBucketIgnored(repoRoot: string, bucketPath: string): void {
|
|
120
|
+
const relativeBucket = path.relative(repoRoot, bucketPath);
|
|
121
|
+
if (relativeBucket.startsWith(`..${path.sep}`) || relativeBucket === ".." || path.isAbsolute(relativeBucket)) return;
|
|
122
|
+
const ignoreProbe = path.join(relativeBucket, ".gjc-worktree-probe");
|
|
123
|
+
for (const candidate of [relativeBucket, ignoreProbe]) {
|
|
124
|
+
const result = Bun.spawnSync(["git", "check-ignore", "--quiet", "--", candidate], {
|
|
125
|
+
cwd: repoRoot,
|
|
126
|
+
stdout: "ignore",
|
|
127
|
+
stderr: "pipe",
|
|
128
|
+
});
|
|
129
|
+
if (result.exitCode === 0) return;
|
|
130
|
+
}
|
|
131
|
+
throw new Error(
|
|
132
|
+
[
|
|
133
|
+
"worktree_bucket_not_ignored",
|
|
134
|
+
"The GJC launch worktree bucket is inside the repository but is not ignored by Git.",
|
|
135
|
+
`Path: ${formatBucketPath(bucketPath)}`,
|
|
136
|
+
`Safe remediation: add /${relativeBucket.replaceAll(path.sep, "/")} to ${path.join(repoRoot, ".gitignore")}, then relaunch.`,
|
|
137
|
+
].join("\n"),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
119
141
|
function resolveSourceBranchSlug(repoRoot: string, baseRef: string): string {
|
|
120
142
|
const branch = tryRunGit(repoRoot, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
121
143
|
if (branch) return sanitizePathToken(branch);
|
|
@@ -301,6 +323,7 @@ function pruneStaleWorktreePath(repoRoot: string): void {
|
|
|
301
323
|
|
|
302
324
|
function readWorktreeEntryFromPath(repoRoot: string, worktreePath: string): GitWorktreeEntry | null {
|
|
303
325
|
if (!fs.existsSync(worktreePath)) return null;
|
|
326
|
+
if (!fs.existsSync(path.join(worktreePath, ".git"))) return null;
|
|
304
327
|
const repoCommonDir = tryRunGit(repoRoot, ["rev-parse", "--git-common-dir"]);
|
|
305
328
|
const worktreeCommonDir = tryRunGit(worktreePath, ["rev-parse", "--git-common-dir"]);
|
|
306
329
|
if (!repoCommonDir || !worktreeCommonDir) return null;
|
|
@@ -379,6 +402,7 @@ export function planLaunchWorktree(
|
|
|
379
402
|
if (branchName) validateBranchName(repoRoot, branchName);
|
|
380
403
|
const worktreeSlug = mode.detached ? resolveSourceBranchSlug(repoRoot, baseRef) : sanitizePathToken(mode.name);
|
|
381
404
|
const worktreePath = path.join(resolveWorktreeBucket(repoRoot), worktreeSlug);
|
|
405
|
+
if (path.resolve(worktreePath) === path.resolve(repoRoot)) throw new Error(`worktree_path_conflict:${worktreePath}`);
|
|
382
406
|
return { enabled: true, repoRoot, worktreePath, detached: mode.detached, baseRef, branchName };
|
|
383
407
|
}
|
|
384
408
|
|
|
@@ -386,6 +410,9 @@ export function ensureLaunchWorktree(
|
|
|
386
410
|
plan: GjcLaunchWorktreePlan | { enabled: false },
|
|
387
411
|
): GjcLaunchWorktreeResult | { enabled: false } {
|
|
388
412
|
if (!plan.enabled) return { enabled: false };
|
|
413
|
+
const bucketPath = path.dirname(plan.worktreePath);
|
|
414
|
+
inspectBucketDir(bucketPath);
|
|
415
|
+
ensureRepositoryBucketIgnored(plan.repoRoot, bucketPath);
|
|
389
416
|
let allWorktrees = listWorktrees(plan.repoRoot);
|
|
390
417
|
const staleAtPath = findWorktreeByPath(allWorktrees, plan.worktreePath);
|
|
391
418
|
if (staleAtPath && !fs.existsSync(staleAtPath.path)) {
|
|
@@ -393,9 +420,19 @@ export function ensureLaunchWorktree(
|
|
|
393
420
|
allWorktrees = listWorktrees(plan.repoRoot);
|
|
394
421
|
}
|
|
395
422
|
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
423
|
+
const listedAtPath = findWorktreeByPath(allWorktrees, plan.worktreePath);
|
|
424
|
+
const existingAtPath = readWorktreeEntryFromPath(plan.repoRoot, plan.worktreePath);
|
|
425
|
+
if (listedAtPath && !existingAtPath) {
|
|
426
|
+
if (fs.existsSync(plan.worktreePath)) throw new Error(`worktree_path_conflict:${plan.worktreePath}`);
|
|
427
|
+
throw new Error(
|
|
428
|
+
[
|
|
429
|
+
"worktree_path_unavailable",
|
|
430
|
+
"The requested launch worktree is still registered by Git but its directory is unavailable or locked.",
|
|
431
|
+
`Path: ${formatBucketPath(plan.worktreePath)}`,
|
|
432
|
+
"Safe remediation: inspect the worktree lock and remove or repair it with git worktree remove/prune when it is no longer needed, then relaunch. GJC did not delete or replace the entry.",
|
|
433
|
+
].join("\n"),
|
|
434
|
+
);
|
|
435
|
+
}
|
|
399
436
|
const expectedBranchRef = plan.branchName ? `refs/heads/${plan.branchName}` : null;
|
|
400
437
|
|
|
401
438
|
if (existingAtPath) {
|
|
@@ -756,9 +756,22 @@ export async function resolveRalplanTargetRoot(
|
|
|
756
756
|
stdout: "pipe",
|
|
757
757
|
stderr: "pipe",
|
|
758
758
|
});
|
|
759
|
-
|
|
759
|
+
const [verifiedStatus, verifiedOutput] = await Promise.all([verified.exited, new Response(verified.stdout).text()]);
|
|
760
|
+
const headObject = verifiedOutput.trim();
|
|
761
|
+
if (verifiedStatus !== 0 || !/^[0-9a-f]{40,64}$/u.test(headObject)) {
|
|
760
762
|
throw new RalplanCommandError(2, `ralplan --worktree-root is not a valid git worktree: ${canonical}`);
|
|
761
763
|
}
|
|
764
|
+
const objectType = Bun.spawn(["git", "-C", canonical, "cat-file", "-t", headObject], {
|
|
765
|
+
stdout: "pipe",
|
|
766
|
+
stderr: "pipe",
|
|
767
|
+
});
|
|
768
|
+
const [objectTypeStatus, objectTypeOutput] = await Promise.all([
|
|
769
|
+
objectType.exited,
|
|
770
|
+
new Response(objectType.stdout).text(),
|
|
771
|
+
]);
|
|
772
|
+
if (objectTypeStatus !== 0 || objectTypeOutput.trim() !== "commit") {
|
|
773
|
+
throw new RalplanCommandError(2, `ralplan --worktree-root is not a commit worktree: ${canonical}`);
|
|
774
|
+
}
|
|
762
775
|
let worktreeRoot: string;
|
|
763
776
|
try {
|
|
764
777
|
worktreeRoot = await fs.realpath(repository.repoRoot);
|
|
@@ -153,6 +153,8 @@ export interface ForceCloseOwnerDependencies {
|
|
|
153
153
|
waitForOwnerExitVerdict?(): Promise<OwnerVerdict>;
|
|
154
154
|
}
|
|
155
155
|
const GJC_TMUX_PSMUX_INCARNATION_OPTION = "@gjc-psmux-incarnation";
|
|
156
|
+
const effectiveSessionEnvironments = new WeakMap<GjcTmuxSessionStatus, NodeJS.ProcessEnv>();
|
|
157
|
+
const fallbackSessionEnvironments = new WeakSet<GjcTmuxSessionStatus>();
|
|
156
158
|
|
|
157
159
|
const FORCE_CLOSE_VERDICT_TIMEOUT_MS = 15_000;
|
|
158
160
|
const FORCE_CLOSE_VERDICT_POLL_MS = 50;
|
|
@@ -305,20 +307,79 @@ function parseSessionLine(line: string): GjcTmuxSessionStatus | null {
|
|
|
305
307
|
};
|
|
306
308
|
}
|
|
307
309
|
|
|
308
|
-
|
|
310
|
+
/** tmux failure shapes that mean "this socket has no server", not "tmux is broken". */
|
|
311
|
+
function isMissingServerFailure(message: string): boolean {
|
|
312
|
+
return (
|
|
313
|
+
message.includes("no server running") ||
|
|
314
|
+
(message.includes("failed to connect to server") && /(?:no such file or directory|not found)/iu.test(message))
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function isMissingSocketFailure(message: string): boolean {
|
|
319
|
+
return (
|
|
320
|
+
message.toLowerCase().includes("error connecting to ") &&
|
|
321
|
+
/(?:no such file or directory|not found)/iu.test(message)
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* `$TMUX` is inherited from whatever launched gjc and names the socket tmux
|
|
327
|
+
* talks to. Terminal hosts that emulate tmux for their own agents can export a
|
|
328
|
+
* `$TMUX` pointing at a socket they never created, and tmux then fails with
|
|
329
|
+
* `error connecting to <socket>` — the same message shape as a legitimate
|
|
330
|
+
* "no server" miss. Returning the socket path lets callers tell the two apart
|
|
331
|
+
* instead of reporting zero sessions while sessions are live on the default
|
|
332
|
+
* socket.
|
|
333
|
+
*/
|
|
334
|
+
function inheritedTmuxSocketPath(env: NodeJS.ProcessEnv): string | null {
|
|
335
|
+
const socket = env.TMUX?.split(",")[0]?.trim();
|
|
336
|
+
return socket ? socket : null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function namesInheritedTmuxSocket(message: string, socket: string): boolean {
|
|
340
|
+
const normalizedMessage = process.platform === "win32" ? message.toLowerCase() : message;
|
|
341
|
+
const normalizedSocket = process.platform === "win32" ? socket.toLowerCase() : socket;
|
|
342
|
+
return (
|
|
343
|
+
(normalizedMessage.includes(`error connecting to ${normalizedSocket} `) ||
|
|
344
|
+
normalizedMessage.includes(`failed to connect to server: ${normalizedSocket} `)) &&
|
|
345
|
+
/(?:no such file or directory|not found)/iu.test(message)
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function envWithoutInheritedTmux(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
350
|
+
const withoutTmux: NodeJS.ProcessEnv = { ...env };
|
|
351
|
+
delete withoutTmux.TMUX;
|
|
352
|
+
return withoutTmux;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
interface ListedTmuxSessions {
|
|
356
|
+
lines: string[];
|
|
357
|
+
env: NodeJS.ProcessEnv;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function runListSessions(format: string, env: NodeJS.ProcessEnv = process.env): ListedTmuxSessions {
|
|
361
|
+
let effectiveEnv = env;
|
|
309
362
|
let output = "";
|
|
310
363
|
try {
|
|
311
364
|
output = runTmux(["list-sessions", "-F", format], env);
|
|
312
365
|
} catch (error) {
|
|
313
366
|
const message = error instanceof Error ? error.message : String(error);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
367
|
+
const inheritedSocket = inheritedTmuxSocketPath(env);
|
|
368
|
+
// A miss on the default socket is a real empty list; only the inherited
|
|
369
|
+
// socket is suspect, so retry without it before concluding there is
|
|
370
|
+
// nothing to list.
|
|
371
|
+
if (inheritedSocket && namesInheritedTmuxSocket(message, inheritedSocket)) {
|
|
372
|
+
try {
|
|
373
|
+
effectiveEnv = envWithoutInheritedTmux(env);
|
|
374
|
+
output = runTmux(["list-sessions", "-F", format], effectiveEnv);
|
|
375
|
+
} catch (retryError) {
|
|
376
|
+
const retryMessage = retryError instanceof Error ? retryError.message : String(retryError);
|
|
377
|
+
if (isMissingServerFailure(retryMessage) || isMissingSocketFailure(retryMessage))
|
|
378
|
+
return { lines: [], env: effectiveEnv };
|
|
379
|
+
throw retryError;
|
|
380
|
+
}
|
|
381
|
+
} else if (isMissingServerFailure(message) || isMissingSocketFailure(message)) return { lines: [], env };
|
|
382
|
+
else throw error;
|
|
322
383
|
}
|
|
323
384
|
const lines = output
|
|
324
385
|
.split("\n")
|
|
@@ -333,20 +394,25 @@ function runListSessions(format: string, env: NodeJS.ProcessEnv = process.env):
|
|
|
333
394
|
if (lines.length > 0 && !lines[0].includes("\t")) {
|
|
334
395
|
const binary = resolveGjcTmuxBinary({ env });
|
|
335
396
|
if (binary.isPsmux) {
|
|
336
|
-
return
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
397
|
+
return {
|
|
398
|
+
lines: lines.map(line => {
|
|
399
|
+
const match = line.match(/^([^:]+):\s*(\d+)\s+windows?\s+\(created\s+([^)]+)\)/);
|
|
400
|
+
if (!match) return line;
|
|
401
|
+
const [, name, windows, created] = match;
|
|
402
|
+
const createdEpoch = String(Math.floor(new Date(`${created} UTC`).getTime() / 1000) || 0);
|
|
403
|
+
|
|
404
|
+
return [name, windows, "0", createdEpoch, "", "", "0", "", "", "", "", "", "", "", "", "", ""].join(
|
|
405
|
+
"\t",
|
|
406
|
+
);
|
|
407
|
+
}),
|
|
408
|
+
env: effectiveEnv,
|
|
409
|
+
};
|
|
344
410
|
}
|
|
345
411
|
}
|
|
346
|
-
return lines;
|
|
412
|
+
return { lines, env: effectiveEnv };
|
|
347
413
|
}
|
|
348
414
|
|
|
349
|
-
function listSessionLines(env: NodeJS.ProcessEnv = process.env):
|
|
415
|
+
function listSessionLines(env: NodeJS.ProcessEnv = process.env): ListedTmuxSessions {
|
|
350
416
|
return runListSessions(
|
|
351
417
|
`#{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}`,
|
|
352
418
|
|
|
@@ -355,8 +421,13 @@ function listSessionLines(env: NodeJS.ProcessEnv = process.env): string[] {
|
|
|
355
421
|
}
|
|
356
422
|
|
|
357
423
|
function listRawTmuxSessionNames(env: NodeJS.ProcessEnv = process.env): string[] {
|
|
358
|
-
return runListSessions("#{session_name}", env).map(line => line.split("\t")[0] ?? line);
|
|
424
|
+
return runListSessions("#{session_name}", env).lines.map(line => line.split("\t")[0] ?? line);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function assertMutationAuthority(session: GjcTmuxSessionStatus): void {
|
|
428
|
+
if (fallbackSessionEnvironments.has(session)) throw new Error("gjc_tmux_fallback_authority_unconfirmed");
|
|
359
429
|
}
|
|
430
|
+
|
|
360
431
|
function canonicalProviderStateDirs(cwd: string): string[] {
|
|
361
432
|
const gjcDir = path.join(cwd, GJC_DIR);
|
|
362
433
|
let entries: fsSync.Dirent[];
|
|
@@ -424,13 +495,24 @@ function psmuxAuthorityEnvironments(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv[]
|
|
|
424
495
|
|
|
425
496
|
export function listGjcTmuxSessions(env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionStatus[] {
|
|
426
497
|
const discovered = psmuxAuthorityEnvironments(env).flatMap(authorityEnv => {
|
|
427
|
-
const
|
|
428
|
-
|
|
498
|
+
const listed = listSessionLines(authorityEnv);
|
|
499
|
+
const authority = psmuxAuthorityFromEnv(listed.env) ?? undefined;
|
|
500
|
+
return listed.lines
|
|
429
501
|
.map(parseSessionLine)
|
|
430
502
|
.filter((session): session is GjcTmuxSessionStatus => session != null)
|
|
431
|
-
.map(session =>
|
|
503
|
+
.map(session => {
|
|
504
|
+
const hydrated = hydrateSessionFromExactOptions(session, listed.env);
|
|
505
|
+
effectiveSessionEnvironments.set(hydrated, listed.env);
|
|
506
|
+
if (listed.env !== authorityEnv) fallbackSessionEnvironments.add(hydrated);
|
|
507
|
+
return hydrated;
|
|
508
|
+
})
|
|
432
509
|
.filter((session): session is GjcTmuxSessionStatus => session?.profile === GJC_TMUX_PROFILE_VALUE)
|
|
433
|
-
.map(session =>
|
|
510
|
+
.map(session => {
|
|
511
|
+
const result = authority ? { ...session, providerAuthority: authority } : session;
|
|
512
|
+
effectiveSessionEnvironments.set(result, listed.env);
|
|
513
|
+
if (listed.env !== authorityEnv) fallbackSessionEnvironments.add(result);
|
|
514
|
+
return result;
|
|
515
|
+
});
|
|
434
516
|
});
|
|
435
517
|
const names = new Set<string>();
|
|
436
518
|
for (const session of discovered) {
|
|
@@ -443,12 +525,18 @@ export function listGjcTmuxSessions(env: NodeJS.ProcessEnv = process.env): GjcTm
|
|
|
443
525
|
/** @internal */
|
|
444
526
|
export function listTmuxSessionsForGc(env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionsForGc {
|
|
445
527
|
const authorityEnvironments = psmuxAuthorityEnvironments(env);
|
|
446
|
-
const sessions = authorityEnvironments.flatMap(authorityEnv =>
|
|
447
|
-
listSessionLines(authorityEnv)
|
|
528
|
+
const sessions = authorityEnvironments.flatMap(authorityEnv => {
|
|
529
|
+
const listed = listSessionLines(authorityEnv);
|
|
530
|
+
return listed.lines
|
|
448
531
|
.map(parseSessionLine)
|
|
449
532
|
.filter((session): session is GjcTmuxSessionStatus => session != null)
|
|
450
|
-
.map(session =>
|
|
451
|
-
|
|
533
|
+
.map(session => {
|
|
534
|
+
const hydrated = hydrateSessionFromExactOptions(session, listed.env);
|
|
535
|
+
effectiveSessionEnvironments.set(hydrated, listed.env);
|
|
536
|
+
if (listed.env !== authorityEnv) fallbackSessionEnvironments.add(hydrated);
|
|
537
|
+
return hydrated;
|
|
538
|
+
});
|
|
539
|
+
});
|
|
452
540
|
const tagged = sessions
|
|
453
541
|
.filter(session => session.profile === GJC_TMUX_PROFILE_VALUE)
|
|
454
542
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -1148,17 +1236,18 @@ export function readTmuxSessionTagsForGc(
|
|
|
1148
1236
|
env: NodeJS.ProcessEnv = process.env,
|
|
1149
1237
|
): GjcTmuxSessionTagsForGc {
|
|
1150
1238
|
const session = listGjcTmuxSessions(env).find(candidate => candidate.name === sessionName);
|
|
1239
|
+
const sessionEnv = session ? (effectiveSessionEnvironments.get(session) ?? env) : env;
|
|
1151
1240
|
return {
|
|
1152
|
-
profile: readExactOptionForGc(sessionName, GJC_TMUX_PROFILE_OPTION,
|
|
1153
|
-
project: readExactOptionForGc(sessionName, GJC_TMUX_PROJECT_OPTION,
|
|
1154
|
-
psmuxIncarnation: readExactOptionForGc(sessionName, GJC_TMUX_PSMUX_INCARNATION_OPTION,
|
|
1155
|
-
branch: readExactOptionForGc(sessionName, GJC_TMUX_BRANCH_OPTION,
|
|
1156
|
-
branchSlug: readExactOptionForGc(sessionName, GJC_TMUX_BRANCH_SLUG_OPTION,
|
|
1157
|
-
sessionId: readExactOptionForGc(sessionName, GJC_TMUX_SESSION_ID_OPTION,
|
|
1158
|
-
sessionStateFile: readExactOptionForGc(sessionName, GJC_TMUX_SESSION_STATE_FILE_OPTION,
|
|
1159
|
-
version: readExactOptionForGc(sessionName, GJC_TMUX_VERSION_OPTION,
|
|
1160
|
-
ownerGeneration: readExactOptionForGc(sessionName, GJC_TMUX_OWNER_GENERATION_OPTION,
|
|
1161
|
-
nativeSessionId: session?.nativeSessionId ?? readNativeTmuxSessionId(sessionName,
|
|
1241
|
+
profile: readExactOptionForGc(sessionName, GJC_TMUX_PROFILE_OPTION, sessionEnv),
|
|
1242
|
+
project: readExactOptionForGc(sessionName, GJC_TMUX_PROJECT_OPTION, sessionEnv),
|
|
1243
|
+
psmuxIncarnation: readExactOptionForGc(sessionName, GJC_TMUX_PSMUX_INCARNATION_OPTION, sessionEnv),
|
|
1244
|
+
branch: readExactOptionForGc(sessionName, GJC_TMUX_BRANCH_OPTION, sessionEnv),
|
|
1245
|
+
branchSlug: readExactOptionForGc(sessionName, GJC_TMUX_BRANCH_SLUG_OPTION, sessionEnv),
|
|
1246
|
+
sessionId: readExactOptionForGc(sessionName, GJC_TMUX_SESSION_ID_OPTION, sessionEnv),
|
|
1247
|
+
sessionStateFile: readExactOptionForGc(sessionName, GJC_TMUX_SESSION_STATE_FILE_OPTION, sessionEnv),
|
|
1248
|
+
version: readExactOptionForGc(sessionName, GJC_TMUX_VERSION_OPTION, sessionEnv),
|
|
1249
|
+
ownerGeneration: readExactOptionForGc(sessionName, GJC_TMUX_OWNER_GENERATION_OPTION, sessionEnv),
|
|
1250
|
+
nativeSessionId: session?.nativeSessionId ?? readNativeTmuxSessionId(sessionName, sessionEnv),
|
|
1162
1251
|
createdAt: session?.createdAt,
|
|
1163
1252
|
attached: session?.attached,
|
|
1164
1253
|
panePids: session?.panePids,
|
|
@@ -1171,6 +1260,9 @@ export function removeGjcTmuxSession(
|
|
|
1171
1260
|
expectedIdentity?: ExpectedGjcTmuxSessionIdentity,
|
|
1172
1261
|
): GjcTmuxSessionStatus {
|
|
1173
1262
|
const session = statusGjcTmuxSession(sessionName, env);
|
|
1263
|
+
assertMutationAuthority(session);
|
|
1264
|
+
const sessionEnv =
|
|
1265
|
+
effectiveSessionEnvironments.get(session) ?? environmentForProviderAuthority(env, session.providerAuthority);
|
|
1174
1266
|
if (session.attached || session.panePids.length > 0) {
|
|
1175
1267
|
throw new Error(`gjc_tmux_session_live:${sessionName}`);
|
|
1176
1268
|
}
|
|
@@ -1185,32 +1277,33 @@ export function removeGjcTmuxSession(
|
|
|
1185
1277
|
session.createdAt !== expectedIdentity.createdAt)
|
|
1186
1278
|
)
|
|
1187
1279
|
throw new Error(`gjc_tmux_owner_changed:${sessionName}`);
|
|
1188
|
-
if (readProfileForExactTarget(session.name,
|
|
1280
|
+
if (readProfileForExactTarget(session.name, sessionEnv) !== GJC_TMUX_PROFILE_VALUE) {
|
|
1189
1281
|
throw new Error(`gjc_tmux_session_not_managed:${sessionName}`);
|
|
1190
1282
|
}
|
|
1191
|
-
const nativeSessionId = readNativeTmuxSessionId(session.name,
|
|
1283
|
+
const nativeSessionId = readNativeTmuxSessionId(session.name, sessionEnv);
|
|
1192
1284
|
if (!nativeSessionId) throw new Error(`gjc_tmux_owner_unverifiable:${sessionName}`);
|
|
1193
1285
|
if (expectedIdentity && nativeSessionId !== expectedIdentity.nativeSessionId)
|
|
1194
1286
|
throw new Error(`gjc_tmux_owner_changed:${sessionName}`);
|
|
1195
1287
|
if (
|
|
1196
1288
|
expectedIdentity &&
|
|
1197
|
-
readExactOptionForGc(session.name, GJC_TMUX_OWNER_GENERATION_OPTION,
|
|
1289
|
+
readExactOptionForGc(session.name, GJC_TMUX_OWNER_GENERATION_OPTION, sessionEnv) !==
|
|
1290
|
+
expectedIdentity.ownerGeneration
|
|
1198
1291
|
)
|
|
1199
1292
|
throw new Error(`gjc_tmux_owner_changed:${sessionName}`);
|
|
1200
|
-
const firstServer = requireSafeTmuxServerForMutation(resolveGjcTmuxCommand(
|
|
1293
|
+
const firstServer = requireSafeTmuxServerForMutation(resolveGjcTmuxCommand(sessionEnv), sessionEnv);
|
|
1201
1294
|
if (
|
|
1202
|
-
readNativeTmuxSessionId(nativeSessionId,
|
|
1203
|
-
readProfileForExactTarget(nativeSessionId,
|
|
1295
|
+
readNativeTmuxSessionId(nativeSessionId, sessionEnv) !== nativeSessionId ||
|
|
1296
|
+
readProfileForExactTarget(nativeSessionId, sessionEnv) !== GJC_TMUX_PROFILE_VALUE
|
|
1204
1297
|
)
|
|
1205
1298
|
throw new Error(`gjc_tmux_owner_changed:${sessionName}`);
|
|
1206
|
-
const finalServer = requireSafeTmuxServerForMutation(resolveGjcTmuxCommand(
|
|
1299
|
+
const finalServer = requireSafeTmuxServerForMutation(resolveGjcTmuxCommand(sessionEnv), sessionEnv);
|
|
1207
1300
|
if (finalServer.pid !== firstServer.pid || finalServer.startTime !== firstServer.startTime)
|
|
1208
1301
|
throw new Error(`gjc_tmux_owner_changed:${sessionName}`);
|
|
1209
1302
|
runGuardedTmuxSessionCommand(
|
|
1210
1303
|
nativeSessionId,
|
|
1211
1304
|
session.name,
|
|
1212
1305
|
finalServer,
|
|
1213
|
-
|
|
1306
|
+
sessionEnv,
|
|
1214
1307
|
`kill-session -t '${nativeSessionId}'`,
|
|
1215
1308
|
expectedIdentity?.ownerGeneration,
|
|
1216
1309
|
);
|
|
@@ -1426,7 +1519,9 @@ export async function forceCloseGjcTmuxSession(
|
|
|
1426
1519
|
deps: Partial<ForceCloseOwnerDependencies> = {},
|
|
1427
1520
|
): Promise<GjcTmuxSessionStatus> {
|
|
1428
1521
|
const session = statusGjcTmuxSession(sessionName, env);
|
|
1429
|
-
|
|
1522
|
+
assertMutationAuthority(session);
|
|
1523
|
+
const sessionEnv =
|
|
1524
|
+
effectiveSessionEnvironments.get(session) ?? environmentForProviderAuthority(env, session.providerAuthority);
|
|
1430
1525
|
if (readProfileForExactTarget(session.name, sessionEnv) !== GJC_TMUX_PROFILE_VALUE)
|
|
1431
1526
|
throw new Error(`gjc_tmux_session_not_managed:${sessionName}`);
|
|
1432
1527
|
const exactPanePids = (deps.listPanePids ?? readExactSessionPanePids)(session.name, sessionEnv);
|
|
@@ -1560,7 +1655,9 @@ export async function forceCloseGjcTmuxSession(
|
|
|
1560
1655
|
|
|
1561
1656
|
export function attachGjcTmuxSession(sessionName: string, env: NodeJS.ProcessEnv = process.env): never {
|
|
1562
1657
|
const session = statusGjcTmuxSession(sessionName, env);
|
|
1563
|
-
|
|
1658
|
+
assertMutationAuthority(session);
|
|
1659
|
+
const sessionEnv =
|
|
1660
|
+
effectiveSessionEnvironments.get(session) ?? environmentForProviderAuthority(env, session.providerAuthority);
|
|
1564
1661
|
const authority = session.providerAuthority ?? psmuxAuthorityFromEnv(sessionEnv);
|
|
1565
1662
|
const tmuxCommand = authority?.command ?? resolveGjcTmuxCommand(sessionEnv);
|
|
1566
1663
|
if (authority) assertGjcTmuxMutationAuthoritySync(authority);
|