@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.
- package/CHANGELOG.md +26 -1
- package/dist/types/cli/update-cli.d.ts +25 -5
- package/dist/types/config/autorouting-tier-map.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +52 -0
- package/dist/types/extensibility/extensions/types.d.ts +5 -0
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/gjc-runtime/workflow-placeholder.d.ts +6 -0
- package/dist/types/modes/components/hook-selector.d.ts +1 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
- package/dist/types/sdk/cli/session-cli.d.ts +42 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/session-runtime.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +4 -3
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/path-utils.d.ts +5 -6
- package/dist/types/tools/tmux-self-injection-guard.d.ts +15 -0
- package/package.json +7 -7
- package/src/cli/update-cli.ts +102 -1
- package/src/commands/sdk.ts +5 -0
- package/src/config/autorouting-tier-map.ts +1 -0
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +10 -4
- package/src/config/settings-schema.ts +53 -0
- package/src/config/settings.ts +1 -1
- package/src/coordinator-mcp/policy.ts +1 -1
- package/src/deep-interview/render-middleware.ts +6 -2
- package/src/extensibility/extensions/types.ts +5 -0
- package/src/extensibility/shared-events.ts +2 -0
- package/src/gjc-runtime/launch-worktree.ts +44 -7
- package/src/gjc-runtime/ralplan-runtime.ts +20 -2
- package/src/gjc-runtime/tmux-sessions.ts +145 -48
- package/src/gjc-runtime/ultragoal-runtime.ts +5 -0
- package/src/gjc-runtime/workflow-placeholder.ts +33 -0
- package/src/internal-urls/docs-index.generated.ts +10 -9
- package/src/modes/components/hook-selector.ts +2 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/prompt-action-autocomplete.ts +26 -47
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/kind-aware-reconciliation.ts +10 -2
- package/src/sdk/bus/prompt-reconciliation.ts +5 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
- package/src/sdk/cli/session-cli.ts +149 -4
- package/src/sdk/host/host.ts +7 -0
- package/src/sdk/host/session-runtime.test.ts +821 -22
- package/src/sdk/host/session-runtime.ts +669 -67
- package/src/sdk/router/session-router.ts +69 -15
- package/src/session/agent-session.ts +302 -25
- package/src/session/internal/managed-session-storage.ts +64 -26
- package/src/session/session-manager.ts +26 -8
- package/src/tools/ask-contract.ts +54 -3
- package/src/tools/ask.ts +46 -6
- package/src/tools/bash.ts +14 -0
- package/src/tools/path-utils.ts +77 -29
- package/src/tools/tmux-self-injection-guard.ts +359 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
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
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
}
|