@gajae-code/coding-agent 0.5.4 → 0.6.1
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 +23 -0
- package/dist/types/cli/web-search-cli.d.ts +12 -0
- package/dist/types/commands/rlm.d.ts +10 -0
- package/dist/types/commands/web-search.d.ts +54 -0
- package/dist/types/config/keybindings.d.ts +10 -0
- package/dist/types/config/model-profiles.d.ts +2 -1
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +61 -3
- package/dist/types/edit/notebook.d.ts +3 -0
- package/dist/types/eval/py/executor.d.ts +3 -0
- package/dist/types/eval/py/kernel.d.ts +3 -1
- package/dist/types/eval/py/runtime.d.ts +9 -1
- package/dist/types/exec/bash-executor.d.ts +4 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
- package/dist/types/extensibility/custom-tools/wrapper.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +1 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +6 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +14 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +6 -0
- package/dist/types/gjc-runtime/tmux-gc.d.ts +3 -3
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +4 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +18 -0
- package/dist/types/goals/state.d.ts +1 -1
- package/dist/types/goals/tools/goal-tool.d.ts +2 -0
- package/dist/types/main.d.ts +11 -0
- package/dist/types/modes/components/custom-editor.d.ts +4 -2
- package/dist/types/modes/components/custom-model-preset-wizard.d.ts +12 -0
- package/dist/types/modes/components/model-selector.d.ts +5 -2
- package/dist/types/modes/components/status-line.d.ts +4 -1
- package/dist/types/modes/controllers/input-controller.d.ts +3 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/print-mode.d.ts +6 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +21 -0
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +13 -0
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +1 -0
- package/dist/types/rlm/artifacts.d.ts +9 -0
- package/dist/types/rlm/complete-research-tool.d.ts +35 -0
- package/dist/types/rlm/data-context.d.ts +6 -0
- package/dist/types/rlm/index.d.ts +35 -0
- package/dist/types/rlm/notebook.d.ts +12 -0
- package/dist/types/rlm/preset.d.ts +23 -0
- package/dist/types/rlm/python-tool.d.ts +16 -0
- package/dist/types/rlm/report.d.ts +14 -0
- package/dist/types/rlm/types.d.ts +37 -0
- package/dist/types/sdk.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +21 -0
- package/dist/types/tools/bash-allowed-prefixes.d.ts +6 -1
- package/dist/types/tools/browser/attach.d.ts +19 -3
- package/dist/types/tools/browser/registry.d.ts +15 -0
- package/dist/types/tools/browser/render.d.ts +3 -0
- package/dist/types/tools/browser.d.ts +18 -1
- package/dist/types/tools/computer/render.d.ts +17 -0
- package/dist/types/tools/computer.d.ts +465 -0
- package/dist/types/tools/index.d.ts +24 -1
- package/dist/types/tools/job.d.ts +13 -0
- package/dist/types/tools/tool-timeouts.d.ts +5 -0
- package/dist/types/web/search/index.d.ts +32 -2
- package/dist/types/web/search/providers/base.d.ts +22 -0
- package/dist/types/web/search/providers/xai.d.ts +64 -0
- package/dist/types/web/search/types.d.ts +11 -3
- package/package.json +7 -7
- package/src/cli/web-search-cli.ts +123 -8
- package/src/cli.ts +2 -0
- package/src/commands/rlm.ts +19 -0
- package/src/commands/web-search.ts +66 -0
- package/src/config/keybindings.ts +11 -0
- package/src/config/model-profiles.ts +11 -3
- package/src/config/model-registry.ts +55 -1
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +67 -1
- package/src/edit/notebook.ts +6 -2
- package/src/eval/py/executor.ts +8 -1
- package/src/eval/py/kernel.ts +9 -4
- package/src/eval/py/runtime.ts +153 -32
- package/src/exec/bash-executor.ts +10 -4
- package/src/extensibility/custom-tools/types.ts +2 -0
- package/src/extensibility/custom-tools/wrapper.ts +2 -0
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/extensibility/extensions/wrapper.ts +1 -0
- package/src/gjc-runtime/launch-tmux.ts +129 -1
- package/src/gjc-runtime/session-state-sidecar.ts +61 -1
- package/src/gjc-runtime/tmux-common.ts +26 -2
- package/src/gjc-runtime/tmux-gc.ts +40 -27
- package/src/gjc-runtime/tmux-sessions.ts +13 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +340 -18
- package/src/goals/runtime.ts +4 -3
- package/src/goals/state.ts +1 -1
- package/src/goals/tools/goal-tool.ts +16 -3
- package/src/internal-urls/docs-index.generated.ts +13 -9
- package/src/main.ts +28 -3
- package/src/modes/components/custom-editor.ts +13 -4
- package/src/modes/components/custom-model-preset-wizard.ts +293 -0
- package/src/modes/components/hook-selector.ts +1 -1
- package/src/modes/components/model-selector.ts +72 -29
- package/src/modes/components/skill-message.ts +62 -8
- package/src/modes/components/status-line.ts +13 -1
- package/src/modes/controllers/input-controller.ts +60 -11
- package/src/modes/controllers/selector-controller.ts +39 -0
- package/src/modes/interactive-mode.ts +1 -1
- package/src/modes/print-mode.ts +14 -4
- package/src/modes/rpc/rpc-client.ts +250 -80
- package/src/modes/rpc/rpc-mode.ts +6 -12
- package/src/modes/rpc/rpc-socket-security.ts +103 -0
- package/src/modes/rpc/rpc-types.ts +10 -0
- package/src/modes/shared/agent-wire/command-dispatch.ts +7 -0
- package/src/modes/shared/agent-wire/command-validation.ts +1 -0
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +9 -0
- package/src/modes/utils/hotkeys-markdown.ts +4 -2
- package/src/modes/utils/ui-helpers.ts +2 -2
- package/src/prompts/goals/goal-continuation.md +1 -0
- package/src/prompts/goals/goal-mode-active.md +1 -0
- package/src/prompts/system/rlm-report-command.md +1 -0
- package/src/prompts/system/rlm-research.md +23 -0
- package/src/prompts/tools/bash.md +23 -2
- package/src/prompts/tools/browser.md +7 -3
- package/src/prompts/tools/computer.md +74 -0
- package/src/prompts/tools/goal.md +3 -0
- package/src/prompts/tools/job.md +9 -1
- package/src/prompts/tools/web-search.md +7 -0
- package/src/rlm/artifacts.ts +60 -0
- package/src/rlm/complete-research-tool.ts +163 -0
- package/src/rlm/data-context.ts +26 -0
- package/src/rlm/index.ts +339 -0
- package/src/rlm/notebook.ts +108 -0
- package/src/rlm/preset.ts +76 -0
- package/src/rlm/python-tool.ts +68 -0
- package/src/rlm/report.ts +70 -0
- package/src/rlm/types.ts +40 -0
- package/src/sdk.ts +12 -0
- package/src/session/agent-session.ts +48 -3
- package/src/slash-commands/builtin-registry.ts +17 -0
- package/src/tools/bash-allowed-prefixes.ts +84 -1
- package/src/tools/bash.ts +80 -13
- package/src/tools/browser/attach.ts +103 -3
- package/src/tools/browser/registry.ts +176 -2
- package/src/tools/browser/render.ts +9 -1
- package/src/tools/browser.ts +33 -0
- package/src/tools/computer/render.ts +78 -0
- package/src/tools/computer.ts +640 -0
- package/src/tools/index.ts +41 -1
- package/src/tools/job.ts +88 -5
- package/src/tools/json-tree.ts +42 -29
- package/src/tools/renderers.ts +2 -0
- package/src/tools/tool-timeouts.ts +1 -0
- package/src/web/search/index.ts +27 -2
- package/src/web/search/provider.ts +16 -1
- package/src/web/search/providers/base.ts +22 -0
- package/src/web/search/providers/xai.ts +511 -0
- package/src/web/search/render.ts +7 -0
- package/src/web/search/types.ts +11 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import { safeStderrWrite } from "@gajae-code/utils";
|
|
3
3
|
import type { Args } from "../cli/args";
|
|
4
|
+
import { GJC_COORDINATOR_SESSION_ID_ENV, GJC_COORDINATOR_SESSION_STATE_FILE_ENV } from "./session-state-sidecar";
|
|
4
5
|
import {
|
|
5
6
|
buildGjcTmuxProfileCommands,
|
|
6
7
|
buildGjcTmuxSessionName,
|
|
@@ -26,6 +27,7 @@ export {
|
|
|
26
27
|
|
|
27
28
|
export const GJC_TMUX_LAUNCHED_ENV = "GJC_TMUX_LAUNCHED";
|
|
28
29
|
export const GJC_LAUNCH_POLICY_ENV = "GJC_LAUNCH_POLICY";
|
|
30
|
+
export const GJC_TMUX_WINDOW_LABEL_MAX_WIDTH = 48;
|
|
29
31
|
|
|
30
32
|
type LaunchPolicy = "direct" | "tmux";
|
|
31
33
|
|
|
@@ -77,6 +79,8 @@ export interface TmuxLaunchPlan {
|
|
|
77
79
|
branch?: string | null;
|
|
78
80
|
attachSessionName?: string;
|
|
79
81
|
project?: string | null;
|
|
82
|
+
sessionId?: string | null;
|
|
83
|
+
sessionStateFile?: string | null;
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
export interface GjcTmuxProfileResult {
|
|
@@ -94,12 +98,15 @@ export interface GjcTmuxProfileContext {
|
|
|
94
98
|
branch?: string | null;
|
|
95
99
|
branchSlug?: string | null;
|
|
96
100
|
project?: string | null;
|
|
101
|
+
sessionId?: string | null;
|
|
102
|
+
sessionStateFile?: string | null;
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
interface CommandResolutionContext {
|
|
100
106
|
cwd: string;
|
|
101
107
|
argv: string[];
|
|
102
108
|
execPath: string;
|
|
109
|
+
extraEnv?: Record<string, string>;
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
function parseLaunchPolicy(env: NodeJS.ProcessEnv): LaunchPolicy {
|
|
@@ -137,6 +144,11 @@ function shellQuote(value: string): string {
|
|
|
137
144
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
138
145
|
}
|
|
139
146
|
|
|
147
|
+
function buildEnvAssignments(values: Record<string, string> | undefined): string {
|
|
148
|
+
const entries = Object.entries(values ?? {});
|
|
149
|
+
return entries.length === 0 ? "" : ` ${entries.map(([key, value]) => `${key}=${shellQuote(value)}`).join(" ")}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
140
152
|
export function applyGjcTmuxProfile(context: GjcTmuxProfileContext): GjcTmuxProfileResult {
|
|
141
153
|
const env = context.env ?? process.env;
|
|
142
154
|
const branchSlug = context.branch ? buildGjcTmuxSessionSlug(context.branch) : (context.branchSlug ?? null);
|
|
@@ -144,6 +156,8 @@ export function applyGjcTmuxProfile(context: GjcTmuxProfileContext): GjcTmuxProf
|
|
|
144
156
|
branch: context.branch ?? null,
|
|
145
157
|
branchSlug,
|
|
146
158
|
project: context.project ?? null,
|
|
159
|
+
sessionId: context.sessionId ?? env[GJC_COORDINATOR_SESSION_ID_ENV] ?? null,
|
|
160
|
+
sessionStateFile: context.sessionStateFile ?? env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV] ?? null,
|
|
147
161
|
});
|
|
148
162
|
if (commands.length === 0) return { skipped: true, commands: [], failures: [] };
|
|
149
163
|
const spawnSync = context.spawnSync ?? defaultSpawnSync;
|
|
@@ -173,7 +187,97 @@ function resolveCurrentGjcCommand(context: CommandResolutionContext): string[] {
|
|
|
173
187
|
function buildInnerCommand(context: CommandResolutionContext, rawArgs: string[]): string {
|
|
174
188
|
const command = resolveCurrentGjcCommand(context);
|
|
175
189
|
const quoted = [...command, ...rawArgs].map(shellQuote).join(" ");
|
|
176
|
-
return `exec env ${GJC_TMUX_LAUNCHED_ENV}=1 ${quoted}`;
|
|
190
|
+
return `exec env ${GJC_TMUX_LAUNCHED_ENV}=1${buildEnvAssignments(context.extraEnv)} ${quoted}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function visibleWidth(value: string): number {
|
|
194
|
+
return Bun.stringWidth(value);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function truncateVisible(value: string, maxWidth: number): string {
|
|
198
|
+
if (maxWidth <= 0) return "";
|
|
199
|
+
if (visibleWidth(value) <= maxWidth) return value;
|
|
200
|
+
if (maxWidth === 1) return "…";
|
|
201
|
+
|
|
202
|
+
let result = "";
|
|
203
|
+
for (const char of value) {
|
|
204
|
+
if (visibleWidth(`${result}${char}…`) > maxWidth) break;
|
|
205
|
+
result += char;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return `${result}…`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function truncateVisibleTail(value: string, maxWidth: number): string {
|
|
212
|
+
if (maxWidth <= 0) return "";
|
|
213
|
+
if (visibleWidth(value) <= maxWidth) return value;
|
|
214
|
+
if (maxWidth === 1) return "…";
|
|
215
|
+
|
|
216
|
+
let result = "";
|
|
217
|
+
for (const char of Array.from(value).reverse()) {
|
|
218
|
+
if (visibleWidth(`…${char}${result}`) > maxWidth) break;
|
|
219
|
+
result = `${char}${result}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return `…${result}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function buildGjcTmuxWindowTitle(cwd: string, branch: string | null | undefined): string {
|
|
226
|
+
const project = path.basename(path.resolve(cwd)) || "gjc";
|
|
227
|
+
const trimmedBranch = branch?.trim();
|
|
228
|
+
if (!trimmedBranch) return truncateVisible(project, GJC_TMUX_WINDOW_LABEL_MAX_WIDTH);
|
|
229
|
+
|
|
230
|
+
const separatorWidth = visibleWidth(":");
|
|
231
|
+
const projectWidth = visibleWidth(project);
|
|
232
|
+
const fullTitle = `${project}:${trimmedBranch}`;
|
|
233
|
+
if (visibleWidth(fullTitle) <= GJC_TMUX_WINDOW_LABEL_MAX_WIDTH) return fullTitle;
|
|
234
|
+
|
|
235
|
+
const remainingBranchWidth = GJC_TMUX_WINDOW_LABEL_MAX_WIDTH - projectWidth - separatorWidth;
|
|
236
|
+
if (remainingBranchWidth <= 0) return truncateVisible(project, GJC_TMUX_WINDOW_LABEL_MAX_WIDTH);
|
|
237
|
+
|
|
238
|
+
return `${project}:${truncateVisibleTail(trimmedBranch, remainingBranchWidth)}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function buildTmuxRenameWindowArgs(title: string, target?: string): string[] {
|
|
242
|
+
return target ? ["rename-window", "-t", target, "--", title] : ["rename-window", "--", title];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function renameTmuxWindow(
|
|
246
|
+
tmuxCommand: string,
|
|
247
|
+
title: string,
|
|
248
|
+
spawnSync: TmuxSpawnSync,
|
|
249
|
+
options: TmuxSpawnOptions,
|
|
250
|
+
target?: string,
|
|
251
|
+
): void {
|
|
252
|
+
spawnSync(tmuxCommand, buildTmuxRenameWindowArgs(title, target), options);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function renameExistingTmuxWindowIfNeeded(context: TmuxLaunchContext): void {
|
|
256
|
+
const env = context.env ?? process.env;
|
|
257
|
+
if (!env.TMUX || env[GJC_TMUX_LAUNCHED_ENV] === "1") return;
|
|
258
|
+
if (parseLaunchPolicy(env) === "direct") return;
|
|
259
|
+
|
|
260
|
+
const platform = context.platform ?? process.platform;
|
|
261
|
+
if (platform === "win32") return;
|
|
262
|
+
|
|
263
|
+
const tty = context.tty ?? { stdin: Boolean(process.stdin.isTTY), stdout: Boolean(process.stdout.isTTY) };
|
|
264
|
+
if (!isInteractiveRootLaunch(context.parsed, tty)) return;
|
|
265
|
+
|
|
266
|
+
const tmuxCommand = resolveGjcTmuxCommand(env);
|
|
267
|
+
const tmuxAvailable = context.tmuxAvailable ?? Bun.which(tmuxCommand) !== null;
|
|
268
|
+
if (!tmuxAvailable) return;
|
|
269
|
+
|
|
270
|
+
const cwd = context.cwd ?? process.cwd();
|
|
271
|
+
const branch = context.worktreeBranch ?? context.currentBranch ?? readCurrentBranch(cwd);
|
|
272
|
+
const title = buildGjcTmuxWindowTitle(context.project ?? cwd, branch);
|
|
273
|
+
const spawnSync = context.spawnSync ?? defaultSpawnSync;
|
|
274
|
+
renameTmuxWindow(tmuxCommand, title, spawnSync, {
|
|
275
|
+
cwd,
|
|
276
|
+
env,
|
|
277
|
+
stdin: "inherit",
|
|
278
|
+
stdout: "inherit",
|
|
279
|
+
stderr: "inherit",
|
|
280
|
+
});
|
|
177
281
|
}
|
|
178
282
|
|
|
179
283
|
function readCurrentBranch(cwd: string): string | null {
|
|
@@ -210,6 +314,10 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
210
314
|
const project = context.project ?? cwd;
|
|
211
315
|
const sessionName = buildGjcTmuxSessionName(env, { branch });
|
|
212
316
|
const tmuxCommand = resolveGjcTmuxCommand(env);
|
|
317
|
+
const sessionId = env[GJC_COORDINATOR_SESSION_ID_ENV]?.trim() || sessionName;
|
|
318
|
+
const sessionStateFile =
|
|
319
|
+
env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim() ||
|
|
320
|
+
path.join(cwd, ".gjc", "runtime", "tmux-sessions", `${buildGjcTmuxSessionSlug(sessionName)}.json`);
|
|
213
321
|
const tmuxAvailable = context.tmuxAvailable ?? Bun.which(tmuxCommand) !== null;
|
|
214
322
|
if (!tmuxAvailable) return undefined;
|
|
215
323
|
const existingBranchSessionName =
|
|
@@ -223,6 +331,10 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
223
331
|
cwd,
|
|
224
332
|
argv: context.argv ?? process.argv,
|
|
225
333
|
execPath: context.execPath ?? process.execPath,
|
|
334
|
+
extraEnv: {
|
|
335
|
+
[GJC_COORDINATOR_SESSION_ID_ENV]: sessionId,
|
|
336
|
+
[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]: sessionStateFile,
|
|
337
|
+
},
|
|
226
338
|
},
|
|
227
339
|
context.rawArgs,
|
|
228
340
|
);
|
|
@@ -234,6 +346,8 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
|
|
|
234
346
|
newSessionArgs: ["new-session", "-d", "-s", sessionName, "-c", cwd, innerCommand],
|
|
235
347
|
branch,
|
|
236
348
|
project,
|
|
349
|
+
sessionId,
|
|
350
|
+
sessionStateFile,
|
|
237
351
|
attachSessionName: existingBranchSessionName,
|
|
238
352
|
};
|
|
239
353
|
}
|
|
@@ -251,6 +365,8 @@ function defaultSpawnSync(command: string, args: string[], options: TmuxSpawnOpt
|
|
|
251
365
|
}
|
|
252
366
|
|
|
253
367
|
export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
368
|
+
renameExistingTmuxWindowIfNeeded(context);
|
|
369
|
+
|
|
254
370
|
const plan = buildDefaultTmuxLaunchPlan(context);
|
|
255
371
|
if (!plan) return false;
|
|
256
372
|
const env = context.env ?? process.env;
|
|
@@ -262,12 +378,22 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
262
378
|
stdout: "inherit",
|
|
263
379
|
stderr: "inherit",
|
|
264
380
|
};
|
|
381
|
+
|
|
265
382
|
if (plan.attachSessionName) {
|
|
266
383
|
const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", `=${plan.attachSessionName}`], options);
|
|
267
384
|
return attached.exitCode === 0;
|
|
268
385
|
}
|
|
386
|
+
|
|
269
387
|
const created = spawnSync(plan.tmuxCommand, plan.newSessionArgs, options);
|
|
270
388
|
if (created.exitCode === 0) {
|
|
389
|
+
renameTmuxWindow(
|
|
390
|
+
plan.tmuxCommand,
|
|
391
|
+
buildGjcTmuxWindowTitle(plan.project ?? plan.cwd, plan.branch),
|
|
392
|
+
spawnSync,
|
|
393
|
+
options,
|
|
394
|
+
`=${plan.sessionName}`,
|
|
395
|
+
);
|
|
396
|
+
|
|
271
397
|
const profile = applyGjcTmuxProfile({
|
|
272
398
|
tmuxCommand: plan.tmuxCommand,
|
|
273
399
|
target: plan.sessionName,
|
|
@@ -276,6 +402,8 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
276
402
|
spawnSync,
|
|
277
403
|
branch: plan.branch,
|
|
278
404
|
project: plan.project,
|
|
405
|
+
sessionId: plan.sessionId ?? null,
|
|
406
|
+
sessionStateFile: plan.sessionStateFile ?? null,
|
|
279
407
|
});
|
|
280
408
|
if (profile.failures.length > 0) {
|
|
281
409
|
cleanupCreatedTmuxSession(plan, spawnSync, options);
|
|
@@ -6,7 +6,7 @@ import { logger } from "@gajae-code/utils";
|
|
|
6
6
|
export const GJC_COORDINATOR_SESSION_STATE_FILE_ENV = "GJC_COORDINATOR_SESSION_STATE_FILE";
|
|
7
7
|
export const GJC_COORDINATOR_SESSION_ID_ENV = "GJC_COORDINATOR_SESSION_ID";
|
|
8
8
|
|
|
9
|
-
type RuntimeState = "ready_for_input" | "running" | "needs_user_input" | "completed" | "errored";
|
|
9
|
+
export type RuntimeState = "ready_for_input" | "running" | "needs_user_input" | "completed" | "errored";
|
|
10
10
|
|
|
11
11
|
interface RuntimeStateEvent {
|
|
12
12
|
type: string;
|
|
@@ -19,6 +19,66 @@ interface RuntimeStateContext {
|
|
|
19
19
|
sessionFile?: string | null;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
interface RuntimeStateSidecarPayload {
|
|
23
|
+
schema_version?: unknown;
|
|
24
|
+
session_id?: unknown;
|
|
25
|
+
state?: unknown;
|
|
26
|
+
ready_for_input?: unknown;
|
|
27
|
+
cwd?: unknown;
|
|
28
|
+
session_file?: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type TerminalRuntimeStateStatus =
|
|
32
|
+
| { terminal: true; state: "completed" | "errored" }
|
|
33
|
+
| {
|
|
34
|
+
terminal: false;
|
|
35
|
+
reason:
|
|
36
|
+
| "missing_state_file"
|
|
37
|
+
| "invalid_json"
|
|
38
|
+
| "session_id_mismatch"
|
|
39
|
+
| "cwd_mismatch"
|
|
40
|
+
| "session_file_mismatch"
|
|
41
|
+
| "non_terminal_state";
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function sameResolvedPath(left: string, right: string): boolean {
|
|
45
|
+
return path.resolve(left) === path.resolve(right);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function readTerminalRuntimeStateMarker(input: {
|
|
49
|
+
stateFile?: string | null;
|
|
50
|
+
sessionId?: string | null;
|
|
51
|
+
cwd?: string | null;
|
|
52
|
+
sessionFile?: string | null;
|
|
53
|
+
}): Promise<TerminalRuntimeStateStatus> {
|
|
54
|
+
const stateFile = input.stateFile?.trim();
|
|
55
|
+
const sessionId = input.sessionId?.trim();
|
|
56
|
+
if (!stateFile || !sessionId) return { terminal: false, reason: "missing_state_file" };
|
|
57
|
+
let payload: RuntimeStateSidecarPayload;
|
|
58
|
+
try {
|
|
59
|
+
payload = JSON.parse(await Bun.file(stateFile).text()) as RuntimeStateSidecarPayload;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
const code = (error as { code?: unknown }).code;
|
|
62
|
+
return {
|
|
63
|
+
terminal: false,
|
|
64
|
+
reason: code === "ENOENT" || code === "ENOTDIR" ? "missing_state_file" : "invalid_json",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (payload.session_id !== sessionId) return { terminal: false, reason: "session_id_mismatch" };
|
|
68
|
+
if (input.cwd && typeof payload.cwd === "string" && !sameResolvedPath(payload.cwd, input.cwd)) {
|
|
69
|
+
return { terminal: false, reason: "cwd_mismatch" };
|
|
70
|
+
}
|
|
71
|
+
if (
|
|
72
|
+
input.sessionFile &&
|
|
73
|
+
typeof payload.session_file === "string" &&
|
|
74
|
+
!sameResolvedPath(payload.session_file, input.sessionFile)
|
|
75
|
+
) {
|
|
76
|
+
return { terminal: false, reason: "session_file_mismatch" };
|
|
77
|
+
}
|
|
78
|
+
if (payload.state === "completed" || payload.state === "errored") return { terminal: true, state: payload.state };
|
|
79
|
+
return { terminal: false, reason: "non_terminal_state" };
|
|
80
|
+
}
|
|
81
|
+
|
|
22
82
|
function lastAssistant(messages: unknown[] | undefined): AssistantMessage | undefined {
|
|
23
83
|
if (!messages) return undefined;
|
|
24
84
|
for (let index = messages.length - 1; index >= 0; index--) {
|
|
@@ -8,6 +8,8 @@ export const GJC_TMUX_PROFILE_VALUE = "1";
|
|
|
8
8
|
export const GJC_TMUX_BRANCH_OPTION = "@gjc-branch";
|
|
9
9
|
export const GJC_TMUX_BRANCH_SLUG_OPTION = "@gjc-branch-slug";
|
|
10
10
|
export const GJC_TMUX_PROJECT_OPTION = "@gjc-project";
|
|
11
|
+
export const GJC_TMUX_SESSION_ID_OPTION = "@gjc-session-id";
|
|
12
|
+
export const GJC_TMUX_SESSION_STATE_FILE_OPTION = "@gjc-session-state-file";
|
|
11
13
|
|
|
12
14
|
export interface GjcTmuxProfileCommand {
|
|
13
15
|
description: string;
|
|
@@ -93,7 +95,13 @@ export function buildGjcTmuxSessionName(
|
|
|
93
95
|
|
|
94
96
|
export function buildGjcTmuxRequiredProfileCommands(
|
|
95
97
|
target: string,
|
|
96
|
-
metadata: {
|
|
98
|
+
metadata: {
|
|
99
|
+
branch?: string | null;
|
|
100
|
+
branchSlug?: string | null;
|
|
101
|
+
project?: string | null;
|
|
102
|
+
sessionId?: string | null;
|
|
103
|
+
sessionStateFile?: string | null;
|
|
104
|
+
} = {},
|
|
97
105
|
): GjcTmuxProfileCommand[] {
|
|
98
106
|
const commands: GjcTmuxProfileCommand[] = [
|
|
99
107
|
{
|
|
@@ -116,13 +124,29 @@ export function buildGjcTmuxRequiredProfileCommands(
|
|
|
116
124
|
description: "record GJC project identity",
|
|
117
125
|
args: ["set-option", "-t", target, GJC_TMUX_PROJECT_OPTION, metadata.project],
|
|
118
126
|
});
|
|
127
|
+
if (metadata.sessionId)
|
|
128
|
+
commands.push({
|
|
129
|
+
description: "record GJC session identity",
|
|
130
|
+
args: ["set-option", "-t", target, GJC_TMUX_SESSION_ID_OPTION, metadata.sessionId],
|
|
131
|
+
});
|
|
132
|
+
if (metadata.sessionStateFile)
|
|
133
|
+
commands.push({
|
|
134
|
+
description: "record GJC session state marker",
|
|
135
|
+
args: ["set-option", "-t", target, GJC_TMUX_SESSION_STATE_FILE_OPTION, metadata.sessionStateFile],
|
|
136
|
+
});
|
|
119
137
|
return commands;
|
|
120
138
|
}
|
|
121
139
|
|
|
122
140
|
export function buildGjcTmuxProfileCommands(
|
|
123
141
|
target: string,
|
|
124
142
|
env: NodeJS.ProcessEnv = process.env,
|
|
125
|
-
metadata: {
|
|
143
|
+
metadata: {
|
|
144
|
+
branch?: string | null;
|
|
145
|
+
branchSlug?: string | null;
|
|
146
|
+
project?: string | null;
|
|
147
|
+
sessionId?: string | null;
|
|
148
|
+
sessionStateFile?: string | null;
|
|
149
|
+
} = {},
|
|
126
150
|
): GjcTmuxProfileCommand[] {
|
|
127
151
|
const commands = buildGjcTmuxRequiredProfileCommands(target, metadata);
|
|
128
152
|
if (envDisabled(env[GJC_TMUX_PROFILE_ENV])) return commands;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* GC adapter for gjc-tagged tmux sessions.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* GC adapter for gjc-tagged tmux sessions. Destructive cleanup is authorized
|
|
3
|
+
* only for detached pane-less sessions whose exact runtime marker revalidates a
|
|
4
|
+
* terminal state. Project/branch/orphan heuristics are discovery signals only.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
|
|
9
|
-
import { worktree } from "../utils/git";
|
|
9
|
+
import { GitCommandError, worktree } from "../utils/git";
|
|
10
10
|
import type { GcCollectResult, GcContext, GcPruneOutcome, GcRecord, GcStoreAdapter } from "./gc-runtime";
|
|
11
|
+
import { readTerminalRuntimeStateMarker } from "./session-state-sidecar";
|
|
11
12
|
import { GJC_TMUX_PROFILE_VALUE, GJC_TMUX_SESSION_PREFIX } from "./tmux-common";
|
|
12
13
|
import {
|
|
13
14
|
type GjcTmuxSessionStatus,
|
|
@@ -60,10 +61,18 @@ function branchMatches(candidate: string | undefined, branch: string): boolean {
|
|
|
60
61
|
]);
|
|
61
62
|
return branchNames.has(candidate);
|
|
62
63
|
}
|
|
64
|
+
function isNotGitRepositoryError(error: unknown): boolean {
|
|
65
|
+
return error instanceof GitCommandError && /not a git repository/i.test(error.message);
|
|
66
|
+
}
|
|
63
67
|
|
|
64
68
|
async function hasLiveWorktreeForBranch(project: string, branch: string): Promise<boolean> {
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
try {
|
|
70
|
+
const entries = await worktree.list(project);
|
|
71
|
+
return entries.some(entry => branchMatches(entry.branch, branch));
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (isNotGitRepositoryError(error)) return false;
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
67
76
|
}
|
|
68
77
|
|
|
69
78
|
function isSessionLive(session: Pick<GjcTmuxSessionStatus, "attached" | "panePids">): boolean {
|
|
@@ -102,6 +111,19 @@ function staleRecord(session: GjcTmuxSessionStatus, reason: string): GcRecord {
|
|
|
102
111
|
};
|
|
103
112
|
}
|
|
104
113
|
|
|
114
|
+
async function hasTerminalRuntimeMarker(input: {
|
|
115
|
+
sessionId?: string | null;
|
|
116
|
+
sessionStateFile?: string | null;
|
|
117
|
+
project?: string | null;
|
|
118
|
+
}): Promise<boolean> {
|
|
119
|
+
const marker = await readTerminalRuntimeStateMarker({
|
|
120
|
+
stateFile: input.sessionStateFile,
|
|
121
|
+
sessionId: input.sessionId,
|
|
122
|
+
cwd: input.project,
|
|
123
|
+
});
|
|
124
|
+
return marker.terminal;
|
|
125
|
+
}
|
|
126
|
+
|
|
105
127
|
function isOldEnoughForOrphanGc(session: GjcTmuxSessionStatus): boolean {
|
|
106
128
|
const createdAt = Date.parse(session.createdAt);
|
|
107
129
|
return Number.isFinite(createdAt) && Date.now() - createdAt >= ORPHAN_MAX_AGE_MS;
|
|
@@ -114,14 +136,19 @@ function isGjcOwnedOrphan(session: GjcTmuxSessionStatus): boolean {
|
|
|
114
136
|
async function classifyTaggedSession(session: GjcTmuxSessionStatus): Promise<GcRecord> {
|
|
115
137
|
const { name, project, branch } = session;
|
|
116
138
|
if (isSessionLive(session)) return liveRecord(session, "tmux_session_attached_or_has_live_panes");
|
|
139
|
+
if (await hasTerminalRuntimeMarker(session))
|
|
140
|
+
return staleRecord(session, "terminal_runtime_marker_detached_idle_session");
|
|
117
141
|
if (!project || !branch) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
142
|
+
const reason =
|
|
143
|
+
isGjcOwnedOrphan(session) && isOldEnoughForOrphanGc(session)
|
|
144
|
+
? "metadata_less_gjc_owned_idle_orphan_missing_terminal_marker"
|
|
145
|
+
: "missing_project_or_branch_tag";
|
|
146
|
+
return unclassifiedRecord(name, reason, project, branch);
|
|
122
147
|
}
|
|
123
|
-
if (!pathExists(project))
|
|
124
|
-
|
|
148
|
+
if (!pathExists(project))
|
|
149
|
+
return unclassifiedRecord(name, "project_missing_without_terminal_marker", project, branch);
|
|
150
|
+
if (!(await hasLiveWorktreeForBranch(project, branch)))
|
|
151
|
+
return unclassifiedRecord(name, "branch_no_worktree_without_terminal_marker", project, branch);
|
|
125
152
|
return {
|
|
126
153
|
store: STORE,
|
|
127
154
|
id: name,
|
|
@@ -152,21 +179,7 @@ async function revalidateRemovable(record: GcRecord, env: NodeJS.ProcessEnv): Pr
|
|
|
152
179
|
}
|
|
153
180
|
if (tags.attached || (tags.panePids?.length ?? 0) > 0) return false;
|
|
154
181
|
if (tags.profile !== GJC_TMUX_PROFILE_VALUE) return false;
|
|
155
|
-
|
|
156
|
-
return (
|
|
157
|
-
record.reason === "metadata_less_gjc_owned_idle_orphan" &&
|
|
158
|
-
isGjcOwnedOrphan({
|
|
159
|
-
name: record.id,
|
|
160
|
-
attached: false,
|
|
161
|
-
windows: 0,
|
|
162
|
-
panes: 0,
|
|
163
|
-
panePids: [],
|
|
164
|
-
bindings: "",
|
|
165
|
-
createdAt: tags.createdAt ?? "",
|
|
166
|
-
})
|
|
167
|
-
);
|
|
168
|
-
if (!pathExists(tags.project)) return true;
|
|
169
|
-
return !(await hasLiveWorktreeForBranch(tags.project, tags.branch));
|
|
182
|
+
return await hasTerminalRuntimeMarker(tags);
|
|
170
183
|
}
|
|
171
184
|
|
|
172
185
|
export const tmuxSessionsGcAdapter: GcStoreAdapter = {
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
GJC_TMUX_PROFILE_OPTION,
|
|
9
9
|
GJC_TMUX_PROFILE_VALUE,
|
|
10
10
|
GJC_TMUX_PROJECT_OPTION,
|
|
11
|
+
GJC_TMUX_SESSION_ID_OPTION,
|
|
12
|
+
GJC_TMUX_SESSION_STATE_FILE_OPTION,
|
|
11
13
|
normalizeTmuxCreatedAt,
|
|
12
14
|
resolveGjcTmuxCommand,
|
|
13
15
|
} from "./tmux-common";
|
|
@@ -22,6 +24,8 @@ export interface GjcTmuxSessionStatus {
|
|
|
22
24
|
branch?: string;
|
|
23
25
|
branchSlug?: string;
|
|
24
26
|
project?: string;
|
|
27
|
+
sessionId?: string;
|
|
28
|
+
sessionStateFile?: string;
|
|
25
29
|
panePids: number[];
|
|
26
30
|
profile?: string;
|
|
27
31
|
}
|
|
@@ -31,6 +35,8 @@ export interface GjcTmuxSessionTagsForGc {
|
|
|
31
35
|
project?: string;
|
|
32
36
|
branch?: string;
|
|
33
37
|
branchSlug?: string;
|
|
38
|
+
sessionId?: string;
|
|
39
|
+
sessionStateFile?: string;
|
|
34
40
|
createdAt?: string;
|
|
35
41
|
attached?: boolean;
|
|
36
42
|
panePids?: number[];
|
|
@@ -78,6 +84,8 @@ function parseSessionLine(line: string): GjcTmuxSessionStatus | null {
|
|
|
78
84
|
branch = "",
|
|
79
85
|
branchSlug = "",
|
|
80
86
|
project = "",
|
|
87
|
+
sessionId = "",
|
|
88
|
+
sessionStateFile = "",
|
|
81
89
|
] = line.split("\t");
|
|
82
90
|
if (!name) return null;
|
|
83
91
|
return {
|
|
@@ -95,6 +103,8 @@ function parseSessionLine(line: string): GjcTmuxSessionStatus | null {
|
|
|
95
103
|
branchSlug: branchSlug || undefined,
|
|
96
104
|
project: project || undefined,
|
|
97
105
|
profile: profile || undefined,
|
|
106
|
+
sessionId: sessionId || undefined,
|
|
107
|
+
sessionStateFile: sessionStateFile || undefined,
|
|
98
108
|
};
|
|
99
109
|
}
|
|
100
110
|
|
|
@@ -115,7 +125,7 @@ function runListSessions(format: string, env: NodeJS.ProcessEnv = process.env):
|
|
|
115
125
|
|
|
116
126
|
function listSessionLines(env: NodeJS.ProcessEnv = process.env): string[] {
|
|
117
127
|
return runListSessions(
|
|
118
|
-
`#{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}}`,
|
|
128
|
+
`#{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}}`,
|
|
119
129
|
env,
|
|
120
130
|
);
|
|
121
131
|
}
|
|
@@ -228,6 +238,8 @@ export function readTmuxSessionTagsForGc(
|
|
|
228
238
|
project: readExactOptionForGc(sessionName, GJC_TMUX_PROJECT_OPTION, env),
|
|
229
239
|
branch: readExactOptionForGc(sessionName, GJC_TMUX_BRANCH_OPTION, env),
|
|
230
240
|
branchSlug: readExactOptionForGc(sessionName, GJC_TMUX_BRANCH_SLUG_OPTION, env),
|
|
241
|
+
sessionId: readExactOptionForGc(sessionName, GJC_TMUX_SESSION_ID_OPTION, env),
|
|
242
|
+
sessionStateFile: readExactOptionForGc(sessionName, GJC_TMUX_SESSION_STATE_FILE_OPTION, env),
|
|
231
243
|
createdAt: session?.createdAt,
|
|
232
244
|
attached: session?.attached,
|
|
233
245
|
panePids: session?.panePids,
|