@gajae-code/coding-agent 0.12.5 → 0.12.6
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 +32 -0
- package/README.md +4 -1
- package/dist/types/eval/backend.d.ts +2 -2
- package/dist/types/exec/bash-executor.d.ts +10 -8
- 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-review-source.d.ts +83 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +57 -4
- package/dist/types/internal-urls/registry-helpers.d.ts +18 -0
- 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 +4 -3
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -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/sdk/host/host.d.ts +23 -2
- package/dist/types/sdk/host/reverse-leases.d.ts +7 -4
- package/dist/types/session/agent-session.d.ts +5 -4
- package/dist/types/session/artifacts.d.ts +8 -5
- package/dist/types/session/attempt-record-store.d.ts +19 -0
- package/dist/types/session/client-bridge.d.ts +14 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +5 -2
- package/dist/types/session/messages.d.ts +4 -1
- package/dist/types/session/session-manager.d.ts +6 -4
- package/dist/types/session/session-storage.d.ts +8 -0
- package/dist/types/session/streaming-output.d.ts +32 -0
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +178 -0
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/fetch.d.ts +3 -0
- package/dist/types/tools/output-meta.d.ts +37 -5
- package/dist/types/tools/tool-result.d.ts +1 -0
- package/dist/types/utils/artifact-id.d.ts +2 -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/config/model-profiles.ts +8 -0
- package/src/config/settings-schema.ts +17 -3
- package/src/cursor.ts +20 -13
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +5 -2
- package/src/eval/backend.ts +2 -2
- package/src/exec/bash-executor.ts +425 -151
- 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-review-source.ts +378 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +640 -159
- package/src/gjc-runtime/workflow-manifest.generated.json +40 -1
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/internal-urls/agent-protocol.ts +98 -38
- package/src/internal-urls/artifact-protocol.ts +28 -17
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/internal-urls/local-protocol.ts +39 -18
- package/src/internal-urls/registry-helpers.ts +62 -0
- package/src/modes/acp/acp-agent.ts +51 -13
- package/src/modes/acp/acp-client-bridge.ts +154 -11
- 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/bash.md +2 -2
- package/src/prompts/tools/monitor.md +4 -4
- package/src/sdk/acp/adapter.ts +200 -9
- 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 +32 -6
- package/src/sdk/bus/index.ts +347 -14
- package/src/sdk/bus/telegram-daemon-contract.ts +4 -2
- package/src/sdk/bus/telegram-daemon.ts +283 -44
- package/src/sdk/bus/topic-registry.ts +133 -10
- package/src/sdk/host/host.ts +187 -9
- package/src/sdk/host/reverse-leases.ts +249 -42
- package/src/sdk/session.ts +7 -6
- package/src/session/agent-session.ts +930 -354
- package/src/session/artifacts.ts +211 -74
- package/src/session/attempt-record-store.ts +106 -0
- package/src/session/client-bridge.ts +15 -0
- package/src/session/internal/managed-session-scope.ts +252 -67
- package/src/session/internal/managed-session-storage.ts +105 -5
- package/src/session/messages.ts +32 -7
- package/src/session/session-manager.ts +19 -21
- package/src/session/session-storage.ts +146 -24
- package/src/session/streaming-output.ts +198 -40
- package/src/task/index.ts +45 -0
- package/src/task/provider-retry-status.ts +4 -1
- package/src/task/receipt.ts +4 -0
- package/src/task/render.ts +30 -0
- package/src/task/types.ts +34 -0
- package/src/tools/bash.ts +818 -239
- package/src/tools/eval.ts +99 -4
- package/src/tools/fetch.ts +77 -15
- package/src/tools/find.ts +7 -3
- package/src/tools/monitor.ts +7 -1
- package/src/tools/output-meta.ts +706 -116
- package/src/tools/search.ts +9 -4
- package/src/tools/tool-result.ts +1 -0
- package/src/utils/artifact-id.ts +6 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** File-backed team task store, claims, leases, and completion evidence. */
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import * as fs from "node:fs/promises";
|
|
4
5
|
import * as path from "node:path";
|
|
@@ -324,15 +325,32 @@ async function writeJson(filePath: string, value: unknown): Promise<void> {
|
|
|
324
325
|
}
|
|
325
326
|
|
|
326
327
|
/**
|
|
327
|
-
* Serializes mutable team facts across processes.
|
|
328
|
-
*
|
|
328
|
+
* Serializes mutable team facts across processes. Nested operations inherit the
|
|
329
|
+
* outer filesystem lock and execute directly; independent callers queue locally
|
|
330
|
+
* before contending for the cross-process lock.
|
|
329
331
|
*/
|
|
332
|
+
const teamMutationFenceTails = new Map<string, Promise<void>>();
|
|
333
|
+
const activeTeamMutationFences = new AsyncLocalStorage<ReadonlySet<string>>();
|
|
334
|
+
|
|
330
335
|
export async function withGjcTeamMutationFence<T>(dir: string, fn: () => Promise<T>): Promise<T> {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
);
|
|
336
|
+
const lockPath = path.join(dir, "operations", "team-mutation.json");
|
|
337
|
+
const active = activeTeamMutationFences.getStore();
|
|
338
|
+
if (active?.has(lockPath)) return await fn();
|
|
339
|
+
const previous = teamMutationFenceTails.get(lockPath) ?? Promise.resolve();
|
|
340
|
+
let releaseQueue!: () => void;
|
|
341
|
+
const queued = new Promise<void>(resolve => {
|
|
342
|
+
releaseQueue = resolve;
|
|
343
|
+
});
|
|
344
|
+
teamMutationFenceTails.set(lockPath, queued);
|
|
345
|
+
await previous;
|
|
346
|
+
try {
|
|
347
|
+
return await activeTeamMutationFences.run(new Set([...(active ?? []), lockPath]), () =>
|
|
348
|
+
withWorkflowStateLock(lockPath, fn, writerOptions(lockPath, "state", "mutation-fence")),
|
|
349
|
+
);
|
|
350
|
+
} finally {
|
|
351
|
+
releaseQueue();
|
|
352
|
+
if (teamMutationFenceTails.get(lockPath) === queued) teamMutationFenceTails.delete(lockPath);
|
|
353
|
+
}
|
|
336
354
|
}
|
|
337
355
|
function optionalString(value: unknown): string | undefined {
|
|
338
356
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
@@ -42,6 +42,7 @@ export interface GjcTeamWorkerRuntime {
|
|
|
42
42
|
workerDir(dir: string, worker: string): string;
|
|
43
43
|
readJson<T>(filePath: string): Promise<T | null>;
|
|
44
44
|
writeJson(filePath: string, value: unknown): Promise<void>;
|
|
45
|
+
withTaskMutation<T>(dir: string, fn: (capability: GjcTeamTaskMutationCapability) => Promise<T>): Promise<T>;
|
|
45
46
|
appendEvent(
|
|
46
47
|
dir: string,
|
|
47
48
|
event: { type: string; worker?: string; task_id?: string; message: string; data?: Record<string, unknown> },
|
|
@@ -368,6 +369,140 @@ export async function writeGjcWorkerStartupAck(
|
|
|
368
369
|
});
|
|
369
370
|
return ack;
|
|
370
371
|
}
|
|
372
|
+
export const GJC_TEAM_CONTINUATION_ACK_POLL_MS = 50;
|
|
373
|
+
export function isValidGjcContinuationAck(
|
|
374
|
+
value: unknown,
|
|
375
|
+
reservation: Record<string, unknown>,
|
|
376
|
+
incident: string,
|
|
377
|
+
attempt: number,
|
|
378
|
+
): value is Record<string, unknown> {
|
|
379
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
380
|
+
const ack = value as Record<string, unknown>;
|
|
381
|
+
const keys = [
|
|
382
|
+
"schema_version",
|
|
383
|
+
"incident_hash",
|
|
384
|
+
"attempt",
|
|
385
|
+
"attempt_nonce",
|
|
386
|
+
"reservation_sha256",
|
|
387
|
+
"worker",
|
|
388
|
+
"pane_id",
|
|
389
|
+
"worker_incarnation",
|
|
390
|
+
"claim_token",
|
|
391
|
+
"acknowledged_at",
|
|
392
|
+
];
|
|
393
|
+
return (
|
|
394
|
+
Object.keys(ack).length === keys.length &&
|
|
395
|
+
Object.keys(ack).every(key => keys.includes(key)) &&
|
|
396
|
+
ack.schema_version === 1 &&
|
|
397
|
+
ack.incident_hash === incident &&
|
|
398
|
+
ack.attempt === attempt &&
|
|
399
|
+
ack.attempt_nonce === reservation.attempt_nonce &&
|
|
400
|
+
ack.reservation_sha256 === gjcContinuationReservationDigest(reservation) &&
|
|
401
|
+
ack.worker === reservation.worker &&
|
|
402
|
+
ack.pane_id === reservation.pane_id &&
|
|
403
|
+
ack.worker_incarnation === reservation.worker_incarnation &&
|
|
404
|
+
ack.claim_token === reservation.claim_token &&
|
|
405
|
+
typeof ack.acknowledged_at === "string" &&
|
|
406
|
+
Number.isFinite(Date.parse(ack.acknowledged_at))
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export async function writeGjcWorkerContinuationAck(
|
|
411
|
+
runtime: GjcTeamWorkerRuntime,
|
|
412
|
+
teamName: string,
|
|
413
|
+
worker: string,
|
|
414
|
+
cwd: string,
|
|
415
|
+
env: NodeJS.ProcessEnv,
|
|
416
|
+
input: Record<string, unknown>,
|
|
417
|
+
): Promise<Record<string, unknown>> {
|
|
418
|
+
const dir = await runtime.findTeamDir(teamName, cwd, env);
|
|
419
|
+
const inputKeys = ["team_name", "worker_id", "incident_hash", "attempt"];
|
|
420
|
+
if (
|
|
421
|
+
Object.keys(input).length !== inputKeys.length ||
|
|
422
|
+
Object.keys(input).some(key => !inputKeys.includes(key)) ||
|
|
423
|
+
input.team_name !== teamName ||
|
|
424
|
+
input.worker_id !== worker ||
|
|
425
|
+
typeof input.incident_hash !== "string" ||
|
|
426
|
+
!/^[0-9a-f]{24}$/.test(input.incident_hash) ||
|
|
427
|
+
(input.attempt !== 1 && input.attempt !== 2)
|
|
428
|
+
)
|
|
429
|
+
throw new Error("invalid_continuation_ack_reference");
|
|
430
|
+
const incident = input.incident_hash;
|
|
431
|
+
const attempt = input.attempt;
|
|
432
|
+
return runtime.withTaskMutation(dir, async () => {
|
|
433
|
+
const config = await runtime.readConfig(dir);
|
|
434
|
+
const teamWorker = runtime.findKnownWorker(config, worker);
|
|
435
|
+
const reservation = await readRuntimeJson<Record<string, unknown>>(
|
|
436
|
+
runtime,
|
|
437
|
+
path.join(runtime.workerDir(dir, worker), "continuations", incident, `attempt-0${attempt}.reservation.json`),
|
|
438
|
+
);
|
|
439
|
+
if (!reservation) throw new Error("missing_continuation_reservation");
|
|
440
|
+
const task = await readRuntimeJson<unknown>(
|
|
441
|
+
runtime,
|
|
442
|
+
path.join(dir, "tasks", `${String(reservation.task_id)}.json`),
|
|
443
|
+
);
|
|
444
|
+
const claim = await readRuntimeJson<unknown>(
|
|
445
|
+
runtime,
|
|
446
|
+
path.join(dir, "claims", `${String(reservation.task_id)}.json`),
|
|
447
|
+
);
|
|
448
|
+
if (
|
|
449
|
+
!isCanonicalPersistedGjcTeamTask(task, String(reservation.task_id)) ||
|
|
450
|
+
!isCanonicalPersistedGjcTeamTaskClaim(claim) ||
|
|
451
|
+
task.claim?.owner !== worker ||
|
|
452
|
+
task.claim?.token !== reservation.claim_token ||
|
|
453
|
+
claim.owner !== worker ||
|
|
454
|
+
claim.token !== reservation.claim_token
|
|
455
|
+
)
|
|
456
|
+
throw new Error("continuation_ack_claim_authority_changed");
|
|
457
|
+
const heartbeat = await readRuntimeJson<WorkerHeartbeatFile>(runtime, heartbeatPath(runtime, dir, worker));
|
|
458
|
+
if (
|
|
459
|
+
!heartbeat?.last_turn_at ||
|
|
460
|
+
!isValidGjcContinuationReservation(
|
|
461
|
+
reservation,
|
|
462
|
+
incident,
|
|
463
|
+
attempt,
|
|
464
|
+
config,
|
|
465
|
+
worker,
|
|
466
|
+
task,
|
|
467
|
+
claim,
|
|
468
|
+
heartbeat.last_turn_at,
|
|
469
|
+
teamWorker.pane_id ?? "",
|
|
470
|
+
)
|
|
471
|
+
)
|
|
472
|
+
throw new Error("invalid_continuation_reservation");
|
|
473
|
+
const lifecycle = await readWorkerLifecycleRecord(runtime, dir, teamWorker);
|
|
474
|
+
const incarnation = `${teamWorker.pane_id ?? ""}:${lifecycle.started_at ?? lifecycle.updated_at}`;
|
|
475
|
+
if (
|
|
476
|
+
reservation.worker !== worker ||
|
|
477
|
+
reservation.pane_id !== teamWorker.pane_id ||
|
|
478
|
+
reservation.pane_id !== lifecycle.pane_id ||
|
|
479
|
+
reservation.worker_incarnation !== incarnation
|
|
480
|
+
)
|
|
481
|
+
throw new Error("continuation_ack_binding_mismatch");
|
|
482
|
+
const ack = {
|
|
483
|
+
schema_version: 1,
|
|
484
|
+
incident_hash: incident,
|
|
485
|
+
attempt,
|
|
486
|
+
attempt_nonce: reservation.attempt_nonce,
|
|
487
|
+
reservation_sha256: gjcContinuationReservationDigest(reservation),
|
|
488
|
+
worker,
|
|
489
|
+
pane_id: reservation.pane_id,
|
|
490
|
+
worker_incarnation: reservation.worker_incarnation,
|
|
491
|
+
claim_token: reservation.claim_token,
|
|
492
|
+
acknowledged_at: runtime.now(),
|
|
493
|
+
};
|
|
494
|
+
await runtime.writeJson(
|
|
495
|
+
path.join(runtime.workerDir(dir, worker), "continuations", incident, `attempt-0${attempt}.ack.json`),
|
|
496
|
+
ack,
|
|
497
|
+
);
|
|
498
|
+
await runtime.appendEvent(dir, {
|
|
499
|
+
type: "worker_continuation_ack",
|
|
500
|
+
worker,
|
|
501
|
+
message: `Worker ${worker} acknowledged continuation`,
|
|
502
|
+
});
|
|
503
|
+
return ack;
|
|
504
|
+
});
|
|
505
|
+
}
|
|
371
506
|
|
|
372
507
|
export async function writeGjcShutdownRequest(
|
|
373
508
|
runtime: GjcTeamWorkerRuntime,
|
|
@@ -513,6 +648,21 @@ function isContinuationLifecycleEligible(lifecycle: GjcTeamWorkerLifecycle, stat
|
|
|
513
648
|
export const GJC_TEAM_CONTINUATION_PROMPT =
|
|
514
649
|
"Continue only your current claimed GJC team task. Re-read current GJC team state; do not replay prior output; report status.";
|
|
515
650
|
|
|
651
|
+
export function buildGjcContinuationPrompt(reservation: Record<string, unknown>): string {
|
|
652
|
+
const teamName = typeof reservation.team_name === "string" ? reservation.team_name : "";
|
|
653
|
+
const worker = typeof reservation.worker === "string" ? reservation.worker : "";
|
|
654
|
+
const incident = typeof reservation.incident_hash === "string" ? reservation.incident_hash : "";
|
|
655
|
+
const attempt = reservation.attempt === 1 || reservation.attempt === 2 ? reservation.attempt : 0;
|
|
656
|
+
if (!teamName || !worker || !incident || !attempt) throw new Error("invalid_continuation_reservation_reference");
|
|
657
|
+
const input = JSON.stringify({
|
|
658
|
+
team_name: teamName,
|
|
659
|
+
worker_id: worker,
|
|
660
|
+
incident_hash: incident,
|
|
661
|
+
attempt,
|
|
662
|
+
});
|
|
663
|
+
return `${GJC_TEAM_CONTINUATION_PROMPT} ACK now: gjc team api worker-continuation-ack --input '${input}' --json.`;
|
|
664
|
+
}
|
|
665
|
+
|
|
516
666
|
function canonicalJson(value: unknown): string {
|
|
517
667
|
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
518
668
|
if (typeof value === "object" && value !== null) {
|
|
@@ -542,7 +692,9 @@ const reservationKeys = new Set([
|
|
|
542
692
|
"heartbeat_at",
|
|
543
693
|
"pane_id",
|
|
544
694
|
"tmux_target",
|
|
695
|
+
"worker_incarnation",
|
|
545
696
|
"attempt",
|
|
697
|
+
"attempt_nonce",
|
|
546
698
|
"reserved_at",
|
|
547
699
|
"hold_until",
|
|
548
700
|
"prompt_version",
|
|
@@ -584,10 +736,14 @@ export function isValidGjcContinuationReservation(
|
|
|
584
736
|
record.heartbeat_at === heartbeatAt &&
|
|
585
737
|
record.pane_id === paneId &&
|
|
586
738
|
record.tmux_target === config.tmux_target &&
|
|
739
|
+
typeof record.worker_incarnation === "string" &&
|
|
740
|
+
record.worker_incarnation.length > 0 &&
|
|
587
741
|
record.attempt === attempt &&
|
|
742
|
+
typeof record.attempt_nonce === "string" &&
|
|
743
|
+
/^[0-9a-f-]{36}$/i.test(record.attempt_nonce) &&
|
|
588
744
|
record.prompt_version === 1 &&
|
|
589
745
|
record.dispatch_protocol === "tmux_command_sequence_v1" &&
|
|
590
|
-
record.prompt_sha256 === createHash("sha256").update(
|
|
746
|
+
record.prompt_sha256 === createHash("sha256").update(buildGjcContinuationPrompt(record)).digest("hex") &&
|
|
591
747
|
Number.isFinite(reservedAt) &&
|
|
592
748
|
Number.isFinite(holdUntil) &&
|
|
593
749
|
Number.isFinite(leaseUntil) &&
|
|
@@ -683,6 +839,10 @@ export function isValidGjcContinuationOutcome(
|
|
|
683
839
|
exit === undefined &&
|
|
684
840
|
error === undefined &&
|
|
685
841
|
hasExactKeys(baseKeys)) ||
|
|
842
|
+
(outcome.reason === "tmux_exit_zero_unacknowledged" &&
|
|
843
|
+
exit === 0 &&
|
|
844
|
+
error === undefined &&
|
|
845
|
+
hasExactKeys([...baseKeys, "tmux_exit_code"])) ||
|
|
686
846
|
(outcome.reason === "tmux_nonzero_exit" &&
|
|
687
847
|
typeof exit === "number" &&
|
|
688
848
|
Number.isInteger(exit) &&
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import type { ResolvedTmuxBinary } from "./psmux-detect";
|
|
2
2
|
import { resolveGjcTmuxBinary } from "./psmux-detect";
|
|
3
3
|
|
|
4
|
+
export {
|
|
5
|
+
assertGjcTmuxMutationAuthoritySync,
|
|
6
|
+
bindGjcTmuxProviderAuthority,
|
|
7
|
+
buildTmuxProviderCommand,
|
|
8
|
+
hasGjcTmuxProviderAuthoritySync,
|
|
9
|
+
type ProviderAuthority,
|
|
10
|
+
type ProviderContext,
|
|
11
|
+
persistGjcTmuxProviderAuthoritySync,
|
|
12
|
+
readGjcTmuxProviderAuthoritySync,
|
|
13
|
+
resolveGjcTmuxProviderContext,
|
|
14
|
+
type TmuxProviderKind,
|
|
15
|
+
} from "./tmux-provider-context";
|
|
16
|
+
|
|
4
17
|
export const GJC_DEFAULT_TMUX_SESSION = "gajae_code";
|
|
5
18
|
export const GJC_TMUX_SESSION_PREFIX = `${GJC_DEFAULT_TMUX_SESSION}_`;
|
|
6
19
|
export const GJC_TMUX_COMMAND_ENV = "GJC_TMUX_COMMAND";
|
|
@@ -112,10 +125,9 @@ export function buildGjcTmuxUntaggedSessionHint(tmuxCommand: string): string {
|
|
|
112
125
|
return (
|
|
113
126
|
`the active multiplexer "${tmuxCommand}" lists this session but did not return GJC's ${GJC_TMUX_PROFILE_OPTION} ownership tag; ` +
|
|
114
127
|
"GJC-managed sessions and `gjc team` require a tmux provider that round-trips tmux user options. " +
|
|
115
|
-
"
|
|
116
|
-
"
|
|
117
|
-
"
|
|
118
|
-
"use real tmux for GJC-managed session and team flows."
|
|
128
|
+
"On Windows psmux, GJC persists a ProviderAuthority that binds the exact executable identity and an isolated `-L <namespace>` server namespace for the owner generation. " +
|
|
129
|
+
"Recover through GJC so it reuses that persisted authority; do not retry against ambient tmux/psmux or a raw `-L` namespace. " +
|
|
130
|
+
"GJC_TMUX_COMMAND and GJC_TEAM_TMUX_COMMAND are binary overrides, not shell command lines."
|
|
119
131
|
);
|
|
120
132
|
}
|
|
121
133
|
|
|
@@ -5,11 +5,17 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
8
9
|
|
|
9
10
|
import { GitCommandError, worktree } from "../utils/git";
|
|
10
11
|
import type { GcCollectResult, GcContext, GcPruneOutcome, GcRecord, GcStoreAdapter } from "./gc-runtime";
|
|
11
|
-
import {
|
|
12
|
-
|
|
12
|
+
import {
|
|
13
|
+
GJC_COORDINATOR_SESSION_ID_ENV,
|
|
14
|
+
GJC_TMUX_OWNER_GENERATION_ENV,
|
|
15
|
+
GJC_TMUX_OWNER_STATE_DIR_ENV,
|
|
16
|
+
readTerminalRuntimeStateMarker,
|
|
17
|
+
} from "./session-state-sidecar";
|
|
18
|
+
import { GJC_TMUX_PROFILE_VALUE, GJC_TMUX_SESSION_PREFIX, hasGjcTmuxProviderAuthoritySync } from "./tmux-common";
|
|
13
19
|
import {
|
|
14
20
|
type GjcTmuxSessionStatus,
|
|
15
21
|
type GjcTmuxSessionsForGc,
|
|
@@ -29,6 +35,9 @@ type CollectedTmuxIdentity = {
|
|
|
29
35
|
sessionStateFile: string;
|
|
30
36
|
project: string;
|
|
31
37
|
createdAt: string;
|
|
38
|
+
providerKind: "native-tmux" | "windows-psmux";
|
|
39
|
+
psmuxIncarnation?: string;
|
|
40
|
+
providerCommand?: string;
|
|
32
41
|
};
|
|
33
42
|
|
|
34
43
|
const collectedIdentities = new WeakMap<GcRecord, CollectedTmuxIdentity>();
|
|
@@ -43,6 +52,15 @@ function collectedIdentity(session: GjcTmuxSessionStatus): CollectedTmuxIdentity
|
|
|
43
52
|
!session.createdAt
|
|
44
53
|
)
|
|
45
54
|
return undefined;
|
|
55
|
+
const stateDir = path.dirname(session.sessionStateFile);
|
|
56
|
+
const providerKind = hasGjcTmuxProviderAuthoritySync({
|
|
57
|
+
stateDir,
|
|
58
|
+
sessionId: session.sessionId,
|
|
59
|
+
generation: session.ownerGeneration,
|
|
60
|
+
})
|
|
61
|
+
? "windows-psmux"
|
|
62
|
+
: "native-tmux";
|
|
63
|
+
if (providerKind === "windows-psmux" && !session.psmuxIncarnation) return undefined;
|
|
46
64
|
return {
|
|
47
65
|
nativeSessionId: session.nativeSessionId,
|
|
48
66
|
ownerGeneration: session.ownerGeneration,
|
|
@@ -50,6 +68,19 @@ function collectedIdentity(session: GjcTmuxSessionStatus): CollectedTmuxIdentity
|
|
|
50
68
|
sessionStateFile: session.sessionStateFile,
|
|
51
69
|
project: session.project,
|
|
52
70
|
createdAt: session.createdAt,
|
|
71
|
+
psmuxIncarnation: session.psmuxIncarnation,
|
|
72
|
+
providerKind,
|
|
73
|
+
providerCommand: session.providerAuthority?.command,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function authorityEnv(identity: CollectedTmuxIdentity, env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
77
|
+
if (identity.providerKind !== "windows-psmux") return env;
|
|
78
|
+
return {
|
|
79
|
+
...env,
|
|
80
|
+
GJC_TMUX_COMMAND: identity.providerCommand,
|
|
81
|
+
[GJC_COORDINATOR_SESSION_ID_ENV]: identity.sessionId,
|
|
82
|
+
[GJC_TMUX_OWNER_GENERATION_ENV]: identity.ownerGeneration,
|
|
83
|
+
[GJC_TMUX_OWNER_STATE_DIR_ENV]: path.dirname(identity.sessionStateFile),
|
|
53
84
|
};
|
|
54
85
|
}
|
|
55
86
|
|
|
@@ -204,14 +235,15 @@ async function revalidateRemovable(
|
|
|
204
235
|
identity: CollectedTmuxIdentity,
|
|
205
236
|
env: NodeJS.ProcessEnv,
|
|
206
237
|
): Promise<boolean> {
|
|
207
|
-
const tags = readTmuxSessionTagsForGc(record.id, env);
|
|
238
|
+
const tags = readTmuxSessionTagsForGc(record.id, authorityEnv(identity, env));
|
|
208
239
|
if (
|
|
209
240
|
tags.nativeSessionId !== identity.nativeSessionId ||
|
|
210
241
|
tags.ownerGeneration !== identity.ownerGeneration ||
|
|
211
242
|
tags.sessionId !== identity.sessionId ||
|
|
212
243
|
tags.sessionStateFile !== identity.sessionStateFile ||
|
|
213
244
|
tags.project !== identity.project ||
|
|
214
|
-
tags.createdAt !== identity.createdAt
|
|
245
|
+
tags.createdAt !== identity.createdAt ||
|
|
246
|
+
tags.psmuxIncarnation !== identity.psmuxIncarnation
|
|
215
247
|
)
|
|
216
248
|
return false;
|
|
217
249
|
if (tags.attached || (tags.panePids?.length ?? 0) > 0) return false;
|
|
@@ -273,7 +305,7 @@ export const tmuxSessionsGcAdapter: GcStoreAdapter = {
|
|
|
273
305
|
if (!identity || !(await revalidateRemovable(record, identity, ctx.env))) {
|
|
274
306
|
return { removed: false, skipped: TOCTOU_SKIP };
|
|
275
307
|
}
|
|
276
|
-
removeGjcTmuxSession(record.id, ctx.env, identity);
|
|
308
|
+
removeGjcTmuxSession(record.id, authorityEnv(identity, ctx.env), identity);
|
|
277
309
|
return { removed: true };
|
|
278
310
|
} catch (error) {
|
|
279
311
|
return { removed: false, error: error instanceof Error ? error.message : String(error) };
|
|
@@ -2182,6 +2182,8 @@ async function fsyncDirectory(directoryPath: string): Promise<void> {
|
|
|
2182
2182
|
const directory = await fs.open(directoryPath, "r");
|
|
2183
2183
|
try {
|
|
2184
2184
|
await directory.sync();
|
|
2185
|
+
} catch (error) {
|
|
2186
|
+
if (process.platform !== "win32" || (error as NodeJS.ErrnoException).code !== "EPERM") throw error;
|
|
2185
2187
|
} finally {
|
|
2186
2188
|
await directory.close();
|
|
2187
2189
|
}
|