@gajae-code/coding-agent 0.12.4 → 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.
Files changed (139) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +4 -1
  3. package/dist/types/async/job-manager.d.ts +9 -1
  4. package/dist/types/eval/backend.d.ts +2 -2
  5. package/dist/types/exec/bash-executor.d.ts +10 -8
  6. package/dist/types/extensibility/extensions/runner.d.ts +9 -7
  7. package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
  8. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
  9. package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
  10. package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
  11. package/dist/types/gjc-runtime/team-store.d.ts +0 -4
  12. package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
  13. package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
  14. package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
  15. package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
  16. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
  17. package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +83 -0
  18. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +57 -4
  19. package/dist/types/internal-urls/registry-helpers.d.ts +18 -0
  20. package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
  21. package/dist/types/modes/components/plugin-settings.d.ts +2 -0
  22. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  23. package/dist/types/sdk/broker/broker.d.ts +18 -0
  24. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
  25. package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
  26. package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -2
  27. package/dist/types/sdk/bus/index.d.ts +4 -0
  28. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -3
  29. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
  30. package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
  31. package/dist/types/sdk/host/host.d.ts +23 -2
  32. package/dist/types/sdk/host/reverse-leases.d.ts +7 -4
  33. package/dist/types/session/agent-session.d.ts +5 -4
  34. package/dist/types/session/artifacts.d.ts +8 -5
  35. package/dist/types/session/attempt-record-store.d.ts +19 -0
  36. package/dist/types/session/client-bridge.d.ts +14 -0
  37. package/dist/types/session/internal/managed-session-storage.d.ts +7 -23
  38. package/dist/types/session/messages.d.ts +4 -1
  39. package/dist/types/session/session-manager.d.ts +7 -5
  40. package/dist/types/session/session-storage.d.ts +8 -0
  41. package/dist/types/session/streaming-output.d.ts +32 -0
  42. package/dist/types/task/executor.d.ts +1 -1
  43. package/dist/types/task/index.d.ts +2 -1
  44. package/dist/types/task/receipt.d.ts +3 -0
  45. package/dist/types/task/types.d.ts +194 -0
  46. package/dist/types/task/worktree.d.ts +13 -6
  47. package/dist/types/tools/bash.d.ts +1 -1
  48. package/dist/types/tools/fetch.d.ts +3 -0
  49. package/dist/types/tools/output-meta.d.ts +37 -5
  50. package/dist/types/tools/subagent.d.ts +2 -0
  51. package/dist/types/tools/tool-result.d.ts +1 -0
  52. package/dist/types/utils/artifact-id.d.ts +2 -0
  53. package/dist/types/utils/git.d.ts +1 -0
  54. package/package.json +8 -8
  55. package/scripts/capture-sticky-viewport-showcase.ts +131 -5
  56. package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
  57. package/scripts/verify-sticky-viewport-showcase.ts +620 -93
  58. package/src/async/job-manager.ts +13 -5
  59. package/src/config/model-profiles.ts +8 -0
  60. package/src/config/settings-schema.ts +17 -3
  61. package/src/cursor.ts +20 -13
  62. package/src/defaults/gjc/skills/ultragoal/SKILL.md +5 -2
  63. package/src/eval/backend.ts +2 -2
  64. package/src/exec/bash-executor.ts +425 -151
  65. package/src/extensibility/extensions/runner.ts +80 -6
  66. package/src/extensibility/extensions/wrapper.ts +22 -15
  67. package/src/extensibility/runtime-skill-discovery.ts +4 -6
  68. package/src/gjc-runtime/launch-tmux.ts +309 -293
  69. package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
  70. package/src/gjc-runtime/psmux-detect.ts +52 -6
  71. package/src/gjc-runtime/team-launch.ts +50 -2
  72. package/src/gjc-runtime/team-runtime.ts +716 -251
  73. package/src/gjc-runtime/team-store.ts +25 -7
  74. package/src/gjc-runtime/team-workers.ts +161 -1
  75. package/src/gjc-runtime/tmux-common.ts +16 -4
  76. package/src/gjc-runtime/tmux-gc.ts +37 -5
  77. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
  78. package/src/gjc-runtime/tmux-provider-context.ts +451 -0
  79. package/src/gjc-runtime/tmux-sessions.ts +409 -156
  80. package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
  81. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
  82. package/src/gjc-runtime/ultragoal-review-source.ts +378 -0
  83. package/src/gjc-runtime/ultragoal-runtime.ts +640 -159
  84. package/src/gjc-runtime/workflow-manifest.generated.json +40 -1
  85. package/src/gjc-runtime/workflow-manifest.ts +12 -0
  86. package/src/internal-urls/agent-protocol.ts +98 -38
  87. package/src/internal-urls/artifact-protocol.ts +28 -17
  88. package/src/internal-urls/docs-index.generated.ts +6 -6
  89. package/src/internal-urls/local-protocol.ts +39 -18
  90. package/src/internal-urls/registry-helpers.ts +62 -0
  91. package/src/modes/acp/acp-agent.ts +51 -13
  92. package/src/modes/acp/acp-client-bridge.ts +154 -11
  93. package/src/modes/components/gjc-bundle-settings.ts +2 -0
  94. package/src/modes/components/plugin-settings.ts +5 -0
  95. package/src/modes/components/settings-selector.ts +4 -0
  96. package/src/modes/controllers/selector-controller.ts +1 -0
  97. package/src/prompts/tools/bash.md +2 -2
  98. package/src/prompts/tools/monitor.md +4 -4
  99. package/src/sdk/acp/adapter.ts +200 -9
  100. package/src/sdk/broker/broker.ts +93 -10
  101. package/src/sdk/broker/lifecycle-ledger.ts +227 -11
  102. package/src/sdk/broker/lifecycle.ts +692 -98
  103. package/src/sdk/bus/chat-daemon-control.ts +33 -5
  104. package/src/sdk/bus/index.ts +347 -14
  105. package/src/sdk/bus/telegram-daemon-contract.ts +7 -3
  106. package/src/sdk/bus/telegram-daemon.ts +283 -44
  107. package/src/sdk/bus/topic-registry.ts +133 -10
  108. package/src/sdk/host/host.ts +187 -9
  109. package/src/sdk/host/reverse-leases.ts +249 -42
  110. package/src/sdk/session.ts +8 -6
  111. package/src/session/agent-session.ts +930 -354
  112. package/src/session/artifacts.ts +211 -74
  113. package/src/session/attempt-record-store.ts +106 -0
  114. package/src/session/client-bridge.ts +15 -0
  115. package/src/session/internal/managed-session-scope.ts +332 -198
  116. package/src/session/internal/managed-session-storage.ts +162 -450
  117. package/src/session/messages.ts +32 -7
  118. package/src/session/session-manager.ts +31 -28
  119. package/src/session/session-storage.ts +190 -53
  120. package/src/session/streaming-output.ts +198 -40
  121. package/src/task/executor.ts +5 -2
  122. package/src/task/index.ts +277 -64
  123. package/src/task/provider-retry-status.ts +4 -1
  124. package/src/task/receipt.ts +26 -2
  125. package/src/task/render.ts +42 -1
  126. package/src/task/types.ts +52 -0
  127. package/src/task/worktree.ts +122 -43
  128. package/src/tools/bash.ts +818 -239
  129. package/src/tools/eval.ts +99 -4
  130. package/src/tools/fetch.ts +77 -15
  131. package/src/tools/find.ts +7 -3
  132. package/src/tools/monitor.ts +7 -1
  133. package/src/tools/output-meta.ts +706 -116
  134. package/src/tools/search.ts +9 -4
  135. package/src/tools/subagent-render.ts +5 -2
  136. package/src/tools/subagent.ts +8 -0
  137. package/src/tools/tool-result.ts +1 -0
  138. package/src/utils/artifact-id.ts +6 -0
  139. package/src/utils/git.ts +2 -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. Callers must not re-enter this
328
- * fence; use unlocked helpers when composing operations under one transaction.
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
- return withWorkflowStateLock(
332
- path.join(dir, "operations", "team-mutation.json"),
333
- fn,
334
- writerOptions(path.join(dir, "operations", "team-mutation.json"), "state", "mutation-fence"),
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(GJC_TEAM_CONTINUATION_PROMPT).digest("hex") &&
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
- "For psmux on Windows, cwd/start-directory flags such as `-c` do not isolate the server namespace; psmux uses the tmux-compatible global `-L <namespace>` flag for that. " +
116
- "GJC_TMUX_COMMAND and GJC_TEAM_TMUX_COMMAND are binary overrides, not shell command lines, so `psmux -L name` is not a supported value. " +
117
- "Alternative multiplexers such as psmux on Windows do not reliably persist user options yet, so the Windows-native psmux path is not fully supported; " +
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 { readTerminalRuntimeStateMarker } from "./session-state-sidecar";
12
- import { GJC_TMUX_PROFILE_VALUE, GJC_TMUX_SESSION_PREFIX } from "./tmux-common";
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
  }