@gajae-code/coding-agent 0.15.3 → 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.
Files changed (55) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/dist/types/cli/update-cli.d.ts +25 -5
  3. package/dist/types/config/autorouting-tier-map.d.ts +3 -0
  4. package/dist/types/config/settings-schema.d.ts +52 -0
  5. package/dist/types/extensibility/extensions/types.d.ts +5 -0
  6. package/dist/types/extensibility/shared-events.d.ts +2 -0
  7. package/dist/types/gjc-runtime/workflow-placeholder.d.ts +6 -0
  8. package/dist/types/modes/components/hook-selector.d.ts +1 -0
  9. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
  10. package/dist/types/sdk/cli/session-cli.d.ts +42 -1
  11. package/dist/types/sdk/host/host.d.ts +2 -0
  12. package/dist/types/sdk/host/session-runtime.d.ts +4 -0
  13. package/dist/types/session/agent-session.d.ts +4 -2
  14. package/dist/types/session/internal/managed-session-storage.d.ts +4 -3
  15. package/dist/types/tools/ask.d.ts +1 -0
  16. package/dist/types/tools/path-utils.d.ts +5 -6
  17. package/dist/types/tools/tmux-self-injection-guard.d.ts +15 -0
  18. package/package.json +7 -7
  19. package/src/cli/update-cli.ts +102 -1
  20. package/src/commands/sdk.ts +5 -0
  21. package/src/config/autorouting-tier-map.ts +1 -0
  22. package/src/config/model-profiles.ts +5 -5
  23. package/src/config/model-registry.ts +10 -4
  24. package/src/config/settings-schema.ts +53 -0
  25. package/src/config/settings.ts +1 -1
  26. package/src/coordinator-mcp/policy.ts +1 -1
  27. package/src/deep-interview/render-middleware.ts +6 -2
  28. package/src/extensibility/extensions/types.ts +5 -0
  29. package/src/extensibility/shared-events.ts +2 -0
  30. package/src/gjc-runtime/launch-worktree.ts +44 -7
  31. package/src/gjc-runtime/ralplan-runtime.ts +20 -2
  32. package/src/gjc-runtime/tmux-sessions.ts +145 -48
  33. package/src/gjc-runtime/ultragoal-runtime.ts +5 -0
  34. package/src/gjc-runtime/workflow-placeholder.ts +33 -0
  35. package/src/internal-urls/docs-index.generated.ts +10 -9
  36. package/src/modes/components/hook-selector.ts +2 -1
  37. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  38. package/src/modes/prompt-action-autocomplete.ts +26 -47
  39. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  40. package/src/sdk/bus/kind-aware-reconciliation.ts +10 -2
  41. package/src/sdk/bus/prompt-reconciliation.ts +5 -2
  42. package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
  43. package/src/sdk/cli/session-cli.ts +149 -4
  44. package/src/sdk/host/host.ts +7 -0
  45. package/src/sdk/host/session-runtime.test.ts +821 -22
  46. package/src/sdk/host/session-runtime.ts +669 -67
  47. package/src/sdk/router/session-router.ts +69 -15
  48. package/src/session/agent-session.ts +302 -25
  49. package/src/session/internal/managed-session-storage.ts +64 -26
  50. package/src/session/session-manager.ts +26 -8
  51. package/src/tools/ask-contract.ts +54 -3
  52. package/src/tools/ask.ts +46 -6
  53. package/src/tools/bash.ts +14 -0
  54. package/src/tools/path-utils.ts +77 -29
  55. package/src/tools/tmux-self-injection-guard.ts +359 -0
@@ -8,7 +8,7 @@ import {
8
8
  isSessionAuthorityEligible,
9
9
  type SessionIndex,
10
10
  } from "../broker/session-index";
11
- import { lifecycleRequestTimeoutMs } from "../broker/startup-budget";
11
+ import { cancellableSleep, lifecycleRequestTimeoutMs } from "../broker/startup-budget";
12
12
  import { SdkClient, SdkClientError, type SdkDispatchContext, type SdkDispatchHandler } from "../client/client";
13
13
  import { readSdkBrokerDiscovery, readSdkSessionEndpoint, type SdkSessionEndpoint } from "../client/discovery";
14
14
  import {
@@ -295,6 +295,8 @@ type AttachedSession = {
295
295
  readonly capability: SessionAttachment;
296
296
  readonly notificationSubscription: NotificationSubscription;
297
297
  notificationCancelled: boolean;
298
+ /** Consecutive refused notification publications; reset by the next delivered frame. */
299
+ notificationFailures: number;
298
300
  readonly notificationCursor: { generation: number; seq: number };
299
301
  published: boolean;
300
302
  initializingPublication: boolean;
@@ -342,6 +344,9 @@ const STARTUP_ATTACH_BUDGET_MS = 5_000;
342
344
  const STARTUP_STRAGGLER_LOG_LIMIT = 8;
343
345
  const ATTACH_CONNECT_TIMEOUT_MS = 10_000;
344
346
  const NOTIFICATION_WORK_TIMEOUT_MS = 5_000;
347
+ // Core shutdown waits only for Router reconciliation and client close; provider
348
+ // notification work is bounded separately by NOTIFICATION_WORK_TIMEOUT_MS.
349
+ const ROUTER_SHUTDOWN_TIMEOUT_MS = 5_000;
345
350
  /**
346
351
  * Idle liveness-sweep cadence (#4689). When the session index is unchanged and
347
352
  * no adoption is pending, the 2s reconcile tick is only a change-stamp check;
@@ -352,6 +357,17 @@ const NOTIFICATION_WORK_TIMEOUT_MS = 5_000;
352
357
  */
353
358
  const SESSION_ROUTER_IDLE_SWEEP_MS = 30_000;
354
359
  const NOTIFICATION_WORK_TIMEOUT = Symbol("notification_work_timeout");
360
+ /**
361
+ * Consecutive local notification-dispatch failures tolerated before the
362
+ * subscription is conceded. A provider that refuses ONE publication is not a
363
+ * dead subscription: it has already settled that publication as rejected on its
364
+ * own side, and the next frame normally succeeds. Cancelling on the first
365
+ * rejection latched `notificationCancelled` for the life of the
366
+ * `AttachedSession`, which only a new attachment clears -- a still-running
367
+ * session never gets one, so its mirroring stayed dead until the session was
368
+ * restarted. Mirrors {@link DELIVERY_ATTEMPT_LIMIT} on the ordered path.
369
+ */
370
+ const NOTIFICATION_LOCAL_FAILURE_LIMIT = 5;
355
371
  /**
356
372
  * Client-message types the native session server authorizes with the
357
373
  * per-session endpoint token (`tokens_match` in crates/gjc-sdk server.rs).
@@ -552,10 +568,16 @@ export class SessionRouter {
552
568
  // initial replay delivered. Past that budget the pass continues in the
553
569
  // background instead of owning the caller's deadline.
554
570
  const initialPass = this.#serialReconcile(runEpoch, false, true);
571
+ // Cancelled as soon as the race settles. Promise.race leaves the loser pending,
572
+ // and a pending Bun.sleep keeps the event loop alive, so a startup that settled
573
+ // in milliseconds still owed the rest of the budget before the process could exit.
574
+ const startupCutoff = new AbortController();
555
575
  const settled = await Promise.race([
556
576
  initialPass.then(() => true),
557
- Bun.sleep(this.#deps.startupAttachBudgetMs ?? STARTUP_ATTACH_BUDGET_MS).then(() => false),
558
- ]);
577
+ cancellableSleep(this.#deps.startupAttachBudgetMs ?? STARTUP_ATTACH_BUDGET_MS, startupCutoff.signal).then(
578
+ () => false,
579
+ ),
580
+ ]).finally(() => startupCutoff.abort());
559
581
  if (!settled) {
560
582
  // Name the sessions the budget was spent on. Without this the lapse is
561
583
  // silent and the only symptom is a slow startup somewhere else, which
@@ -586,6 +608,10 @@ export class SessionRouter {
586
608
  () => this.#schedule(this.#serialReconcile(runEpoch, true)),
587
609
  2_000,
588
610
  );
611
+ // Reconciliation is background upkeep and must not be the reason a host process
612
+ // stays alive. stop() still clears it on the normal path; this only keeps a
613
+ // caller that never stops the Router from hanging at exit.
614
+ timer.unref?.();
589
615
  this.#stopTimer = () => (this.#deps.clearInterval ?? clearInterval)(timer);
590
616
  } catch (error) {
591
617
  if (this.#running(runEpoch)) await this.stop();
@@ -695,13 +721,18 @@ export class SessionRouter {
695
721
  // Provider notification work is detached and bounded independently; core
696
722
  // shutdown waits only for Router reconciliation and client close.
697
723
  const pending = Promise.allSettled([this.#reconcileTail, ...shutdownTasks]);
724
+ // Cancelled once the race settles, so a completed shutdown does not hold the
725
+ // event loop open for the remainder of the timeout.
726
+ const shutdownCutoff = new AbortController();
698
727
  const outcome = await Promise.race([
699
728
  pending.then(results => ({ kind: "settled" as const, results })),
700
- Bun.sleep(5_000).then(() => ({ kind: "timeout" as const })),
701
- ]);
729
+ cancellableSleep(ROUTER_SHUTDOWN_TIMEOUT_MS, shutdownCutoff.signal).then(() => ({
730
+ kind: "timeout" as const,
731
+ })),
732
+ ]).finally(() => shutdownCutoff.abort());
702
733
  if (outcome.kind === "timeout") {
703
734
  logger.warn(
704
- "SessionRouter shutdown exceeded 5000ms; authority is revoked and cleanup continues in background.",
735
+ `SessionRouter shutdown exceeded ${ROUTER_SHUTDOWN_TIMEOUT_MS}ms; authority is revoked and cleanup continues in background.`,
705
736
  );
706
737
  return;
707
738
  }
@@ -1548,6 +1579,7 @@ export class SessionRouter {
1548
1579
  capability,
1549
1580
  notificationSubscription,
1550
1581
  notificationCancelled: false,
1582
+ notificationFailures: 0,
1551
1583
  notificationCursor,
1552
1584
  published: false,
1553
1585
  publication,
@@ -1780,10 +1812,20 @@ export class SessionRouter {
1780
1812
  }
1781
1813
 
1782
1814
  async #boundedNotificationWork<T>(work: () => Promise<T> | T): Promise<T | typeof NOTIFICATION_WORK_TIMEOUT> {
1783
- const timeout: Promise<typeof NOTIFICATION_WORK_TIMEOUT> = Bun.sleep(NOTIFICATION_WORK_TIMEOUT_MS).then(
1784
- () => NOTIFICATION_WORK_TIMEOUT,
1785
- );
1786
- return await Promise.race([Promise.resolve().then(work), timeout]);
1815
+ // The dominant cost of a one-shot `gjc sdk` command: notification work that
1816
+ // finished in milliseconds left this timeout pending, and a pending Bun.sleep
1817
+ // keeps the event loop alive, so the process wrote its response and then idled
1818
+ // for the full budget before exiting.
1819
+ const cutoff = new AbortController();
1820
+ const timeout: Promise<typeof NOTIFICATION_WORK_TIMEOUT> = cancellableSleep(
1821
+ NOTIFICATION_WORK_TIMEOUT_MS,
1822
+ cutoff.signal,
1823
+ ).then(() => NOTIFICATION_WORK_TIMEOUT);
1824
+ try {
1825
+ return await Promise.race([Promise.resolve().then(work), timeout]);
1826
+ } finally {
1827
+ cutoff.abort();
1828
+ }
1787
1829
  }
1788
1830
 
1789
1831
  #detachNotification(
@@ -1827,15 +1869,25 @@ export class SessionRouter {
1827
1869
  this.#detachNotification(attached, "cancelled");
1828
1870
  return;
1829
1871
  }
1872
+ // A delivered frame ends the consecutive-failure run.
1873
+ attached.notificationFailures = 0;
1830
1874
  if (frame.seq !== undefined)
1831
1875
  attached.notificationSubscription.advanceCursor(frame.generation ?? attached.generation, frame.seq);
1832
1876
  },
1833
1877
  (error: unknown) => {
1834
- this.#detachNotification(attached, "cancelled");
1878
+ // Concede only after a bounded consecutive run: a single refused
1879
+ // publication drops that frame, it does not end the subscription.
1880
+ attached.notificationFailures += 1;
1881
+ const detail = error instanceof Error ? error.message : String(error);
1882
+ if (attached.notificationFailures >= NOTIFICATION_LOCAL_FAILURE_LIMIT) {
1883
+ this.#detachNotification(attached, "cancelled");
1884
+ logger.warn(
1885
+ `SDK notification subscription ${attached.notificationSubscription.subscriptionId} failed locally ${attached.notificationFailures} times in a row; cancelling: ${detail}`,
1886
+ );
1887
+ return;
1888
+ }
1835
1889
  logger.warn(
1836
- `SDK notification subscription ${attached.notificationSubscription.subscriptionId} failed locally: ${
1837
- error instanceof Error ? error.message : String(error)
1838
- }`,
1890
+ `SDK notification subscription ${attached.notificationSubscription.subscriptionId} refused one frame (${attached.notificationFailures}/${NOTIFICATION_LOCAL_FAILURE_LIMIT}); subscription retained: ${detail}`,
1839
1891
  );
1840
1892
  },
1841
1893
  );
@@ -2092,7 +2144,9 @@ export class SessionRouter {
2092
2144
  this.#failBarrier(attached, "replay went unanswered");
2093
2145
  return;
2094
2146
  }
2095
- await Bun.sleep(REPLAY_RETRY_BACKOFF_MS * 2 ** attempt);
2147
+ // Awaited backoff rather than a raced timeout, but it must still not sleep
2148
+ // through a shutdown: cancelling on stop lets the process exit promptly.
2149
+ await cancellableSleep(REPLAY_RETRY_BACKOFF_MS * 2 ** attempt, this.#stopController.signal);
2096
2150
  if (attached.barrier.held !== held || !this.#attachmentLive(attached)) return;
2097
2151
  }
2098
2152
  }