@gajae-code/coding-agent 0.18.0 → 0.18.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 +95 -0
- package/dist/types/cli/stats-cli.d.ts +0 -5
- package/dist/types/config/autorouting-tier-map.d.ts +29 -2
- package/dist/types/config/model-registry.d.ts +16 -1
- package/dist/types/config/settings-schema.d.ts +2 -2
- package/dist/types/exa/mcp-client.d.ts +1 -5
- package/dist/types/exec/bash-shell-guardian.d.ts +26 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +1 -0
- package/dist/types/hashline/parser.d.ts +28 -5
- package/dist/types/modes/acp/acp-agent.d.ts +6 -1
- package/dist/types/sdk/acp/adapter.d.ts +3 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -1
- package/dist/types/sdk/broker/recovery-backoff.d.ts +62 -0
- package/dist/types/sdk/broker/runtime.d.ts +20 -0
- package/dist/types/sdk/broker/startup-budget.d.ts +5 -1
- package/dist/types/sdk/bus/index.d.ts +1 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/client/client.d.ts +5 -1
- package/dist/types/sdk/host/session-runtime.d.ts +7 -0
- package/dist/types/sdk/prompt-watchdog.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/task/index.d.ts +0 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/descriptors.d.ts +5 -0
- package/dist/types/tools/eval-backends.d.ts +37 -0
- package/dist/types/tools/eval.d.ts +1 -6
- package/dist/types/tools/index.d.ts +1 -36
- package/dist/types/tools/session-descriptions.d.ts +22 -0
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/src/cli/doctor/managed-link.ts +4 -4
- package/src/cli/doctor/repairs.ts +9 -1
- package/src/cli/stats-cli.ts +4 -36
- package/src/cli-main.ts +4 -8
- package/src/cli.ts +4 -8
- package/src/commands/commit.ts +2 -2
- package/src/commands/sdk.ts +1 -2
- package/src/config/autorouting-tier-map.ts +20 -2
- package/src/config/model-preset-registry.ts +17 -2
- package/src/config/model-profile-activation.ts +3 -1
- package/src/config/model-profiles.ts +11 -3
- package/src/config/model-registry.ts +45 -17
- package/src/config/settings-schema.ts +8 -2
- package/src/exa/mcp-client.ts +41 -8
- package/src/exec/bash-shell-guardian.ts +214 -48
- package/src/gjc-runtime/session-state-sidecar.ts +88 -58
- package/src/hashline/execute.ts +51 -3
- package/src/hashline/parser.ts +63 -16
- package/src/internal-urls/docs-index.generated.ts +25 -25
- package/src/modes/acp/acp-agent.ts +136 -20
- package/src/modes/controllers/ssh-command-controller.ts +3 -1
- package/src/modes/interactive-mode.ts +10 -0
- package/src/modes/utils/context-usage.ts +2 -6
- package/src/prompts/tools/task.md +8 -27
- package/src/sdk/acp/adapter.ts +38 -3
- package/src/sdk/broker/doctor-restart.ts +38 -3
- package/src/sdk/broker/lifecycle.ts +408 -218
- package/src/sdk/broker/recovery-backoff.ts +135 -0
- package/src/sdk/broker/runtime.ts +83 -0
- package/src/sdk/broker/startup-budget.ts +6 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +308 -22
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/client/client.ts +53 -35
- package/src/sdk/host/session-runtime.ts +99 -3
- package/src/sdk/prompt-watchdog.ts +17 -4
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/session/agent-session.ts +79 -17
- package/src/session/internal/managed-session-storage.ts +23 -0
- package/src/task/index.ts +21 -66
- package/src/tools/bash.ts +39 -0
- package/src/tools/descriptors.ts +27 -1
- package/src/tools/eval-backends.ts +95 -0
- package/src/tools/eval.ts +3 -15
- package/src/tools/index.ts +9 -88
- package/src/tools/search.ts +2 -7
- package/src/tools/session-descriptions.ts +135 -0
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/dist/types/cli/grep-cli.d.ts +0 -17
- package/dist/types/cli/shell-cli.d.ts +0 -8
- package/dist/types/commands/grep.d.ts +0 -42
- package/dist/types/commands/shell.d.ts +0 -21
- package/src/cli/grep-cli.ts +0 -160
- package/src/cli/shell-cli.ts +0 -176
- package/src/commands/grep.ts +0 -48
- package/src/commands/shell.ts +0 -29
package/src/sdk/client/client.ts
CHANGED
|
@@ -14,6 +14,8 @@ export type SdkErrorCode =
|
|
|
14
14
|
export class SdkClientError extends Error {
|
|
15
15
|
readonly code: SdkErrorCode;
|
|
16
16
|
readonly details: unknown;
|
|
17
|
+
/** True only when the SDK client itself reported a transport failure, never for a server error frame. */
|
|
18
|
+
readonly transport: boolean;
|
|
17
19
|
/**
|
|
18
20
|
* Reconnect-cycle diagnostics, separate from `details` because `details` is an
|
|
19
21
|
* established contract: callers read the terminating transport error straight off
|
|
@@ -22,19 +24,35 @@ export class SdkClientError extends Error {
|
|
|
22
24
|
* break every such reader, so the new attribution rides alongside it instead.
|
|
23
25
|
*/
|
|
24
26
|
readonly reconnect?: SdkReconnectExhaustedDetails;
|
|
25
|
-
constructor(
|
|
27
|
+
constructor(
|
|
28
|
+
code: SdkErrorCode,
|
|
29
|
+
message: string,
|
|
30
|
+
details?: unknown,
|
|
31
|
+
reconnect?: SdkReconnectExhaustedDetails,
|
|
32
|
+
options: { transport?: boolean } = {},
|
|
33
|
+
) {
|
|
26
34
|
super(message);
|
|
27
35
|
this.name = "SdkClientError";
|
|
28
36
|
this.code = code;
|
|
29
37
|
this.details = details;
|
|
38
|
+
this.transport = options.transport === true;
|
|
30
39
|
if (reconnect) this.reconnect = reconnect;
|
|
31
40
|
}
|
|
32
41
|
}
|
|
33
42
|
|
|
43
|
+
function transportError(
|
|
44
|
+
code: SdkErrorCode,
|
|
45
|
+
message: string,
|
|
46
|
+
details?: unknown,
|
|
47
|
+
reconnect?: SdkReconnectExhaustedDetails,
|
|
48
|
+
): SdkClientError {
|
|
49
|
+
return new SdkClientError(code, message, details, reconnect, { transport: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
34
52
|
/** A prepared transport operation failed before any wire handoff. */
|
|
35
53
|
export class SdkPreparedDispatchError extends SdkClientError {
|
|
36
54
|
constructor(error: SdkClientError) {
|
|
37
|
-
super(error.code, error.message, error.details, error.reconnect);
|
|
55
|
+
super(error.code, error.message, error.details, error.reconnect, { transport: error.transport });
|
|
38
56
|
this.name = "SdkPreparedDispatchError";
|
|
39
57
|
}
|
|
40
58
|
}
|
|
@@ -350,18 +368,18 @@ export class SdkClient {
|
|
|
350
368
|
}
|
|
351
369
|
|
|
352
370
|
send(frame: SdkFrame): void {
|
|
353
|
-
if (this.#closed) throw
|
|
371
|
+
if (this.#closed) throw transportError("connection_closed", "SDK client closed");
|
|
354
372
|
this.#throwIfDeadlineElapsed();
|
|
355
373
|
const current = this.#currentSocketRecord ?? this.#opening?.candidate;
|
|
356
374
|
const authoritative =
|
|
357
375
|
this.#isActive(current ?? null) ||
|
|
358
376
|
(!!current && current.phase === "hello" && this.#isCandidate(current.cycle, current));
|
|
359
377
|
if (!current || !authoritative || current.socket.readyState !== WebSocket.OPEN)
|
|
360
|
-
throw
|
|
378
|
+
throw transportError("connection_closed", "SDK WebSocket is not connected");
|
|
361
379
|
try {
|
|
362
380
|
current.socket.send(JSON.stringify(frame));
|
|
363
381
|
} catch (error) {
|
|
364
|
-
throw
|
|
382
|
+
throw transportError("unavailable", "SDK WebSocket send failed", error);
|
|
365
383
|
}
|
|
366
384
|
}
|
|
367
385
|
|
|
@@ -383,19 +401,19 @@ export class SdkClient {
|
|
|
383
401
|
if (cycle.backoffTimer) clearTimeout(cycle.backoffTimer);
|
|
384
402
|
if (cycle.candidate) {
|
|
385
403
|
transports.add(cycle.candidate);
|
|
386
|
-
this.#retire(cycle.candidate,
|
|
404
|
+
this.#retire(cycle.candidate, transportError("connection_closed", "SDK client closed"), false);
|
|
387
405
|
}
|
|
388
|
-
cycle.rejectBackoff?.(
|
|
406
|
+
cycle.rejectBackoff?.(transportError("connection_closed", "SDK client closed"));
|
|
389
407
|
cycle.rejectBackoff = undefined;
|
|
390
408
|
if (this.#opening === cycle) this.#opening = null;
|
|
391
409
|
}
|
|
392
410
|
const current = this.#currentSocketRecord;
|
|
393
411
|
if (current) {
|
|
394
412
|
transports.add(current);
|
|
395
|
-
this.#retire(current,
|
|
413
|
+
this.#retire(current, transportError("connection_closed", "SDK client closed"), false, true);
|
|
396
414
|
}
|
|
397
415
|
for (const [id, pending] of this.#pending)
|
|
398
|
-
this.#settlePending(id, pending,
|
|
416
|
+
this.#settlePending(id, pending, transportError("connection_closed", "SDK client closed"), true);
|
|
399
417
|
try {
|
|
400
418
|
await Promise.all([...transports].map(incarnation => this.#closeTransport(incarnation)));
|
|
401
419
|
} finally {
|
|
@@ -469,7 +487,7 @@ export class SdkClient {
|
|
|
469
487
|
|
|
470
488
|
async #request(frame: Frame, options: SdkRequestOptions, onResponse?: () => void): Promise<unknown> {
|
|
471
489
|
if (this.#closed) {
|
|
472
|
-
const error =
|
|
490
|
+
const error = transportError("connection_closed", "SDK client closed");
|
|
473
491
|
throw options.connectedOnly ? new SdkPreparedDispatchError(error) : error;
|
|
474
492
|
}
|
|
475
493
|
try {
|
|
@@ -526,7 +544,7 @@ export class SdkClient {
|
|
|
526
544
|
this.#settlePending(
|
|
527
545
|
id,
|
|
528
546
|
pending,
|
|
529
|
-
|
|
547
|
+
transportError("timeout", `SDK request timed out after ${timeoutMs}ms`, {
|
|
530
548
|
requestId: id,
|
|
531
549
|
requestSent: pending.sent,
|
|
532
550
|
} satisfies SdkRequestTimeoutDetails),
|
|
@@ -537,7 +555,7 @@ export class SdkClient {
|
|
|
537
555
|
};
|
|
538
556
|
this.#pending.set(id, pending);
|
|
539
557
|
if (!this.#isActive(incarnation) || incarnation.socket.readyState !== WebSocket.OPEN) {
|
|
540
|
-
const error =
|
|
558
|
+
const error = transportError("unavailable", "SDK WebSocket is not connected");
|
|
541
559
|
this.#settlePending(id, pending, options.connectedOnly ? new SdkPreparedDispatchError(error) : error);
|
|
542
560
|
return await deferred.promise;
|
|
543
561
|
}
|
|
@@ -591,9 +609,9 @@ export class SdkClient {
|
|
|
591
609
|
pending,
|
|
592
610
|
options.connectedOnly
|
|
593
611
|
? new SdkPreparedDispatchError(
|
|
594
|
-
|
|
612
|
+
transportError("connection_closed", "SDK client closed during dispatch"),
|
|
595
613
|
)
|
|
596
|
-
:
|
|
614
|
+
: transportError("connection_closed", "SDK client closed during dispatch"),
|
|
597
615
|
);
|
|
598
616
|
return await deferred.promise;
|
|
599
617
|
}
|
|
@@ -603,7 +621,7 @@ export class SdkClient {
|
|
|
603
621
|
return await deferred.promise;
|
|
604
622
|
}
|
|
605
623
|
if (incarnation.socket.readyState !== WebSocket.OPEN) {
|
|
606
|
-
const error =
|
|
624
|
+
const error = transportError("unavailable", "SDK WebSocket is not connected");
|
|
607
625
|
this.#settlePending(id, pending, options.connectedOnly ? new SdkPreparedDispatchError(error) : error);
|
|
608
626
|
return await deferred.promise;
|
|
609
627
|
}
|
|
@@ -638,7 +656,7 @@ export class SdkClient {
|
|
|
638
656
|
const sendError =
|
|
639
657
|
error instanceof SdkClientError
|
|
640
658
|
? error
|
|
641
|
-
:
|
|
659
|
+
: transportError("unavailable", "SDK WebSocket send failed", error);
|
|
642
660
|
this.#settlePending(
|
|
643
661
|
id,
|
|
644
662
|
pending,
|
|
@@ -665,7 +683,7 @@ export class SdkClient {
|
|
|
665
683
|
}
|
|
666
684
|
|
|
667
685
|
#deadlineError(reconnect?: SdkReconnectExhaustedDetails): SdkClientError {
|
|
668
|
-
return
|
|
686
|
+
return transportError("timeout", "SDK client deadline elapsed.", undefined, reconnect);
|
|
669
687
|
}
|
|
670
688
|
|
|
671
689
|
#remainingTimeout(limit = this.#timeoutMs): number {
|
|
@@ -682,7 +700,7 @@ export class SdkClient {
|
|
|
682
700
|
const current = this.#currentSocketRecord;
|
|
683
701
|
if (current && this.#isActive(current) && current.socket.readyState === WebSocket.OPEN) return current;
|
|
684
702
|
if (current)
|
|
685
|
-
this.#retire(current,
|
|
703
|
+
this.#retire(current, transportError("connection_closed", "SDK WebSocket connection closed"), true, true);
|
|
686
704
|
|
|
687
705
|
let cycle = this.#opening;
|
|
688
706
|
if (!cycle) {
|
|
@@ -696,7 +714,7 @@ export class SdkClient {
|
|
|
696
714
|
#requireConnectedIncarnation(): Incarnation {
|
|
697
715
|
const current = this.#currentSocketRecord;
|
|
698
716
|
if (!current || !this.#isActive(current) || current.socket.readyState !== WebSocket.OPEN)
|
|
699
|
-
throw
|
|
717
|
+
throw transportError("connection_closed", "SDK WebSocket disconnected before prepared dispatch.");
|
|
700
718
|
return current;
|
|
701
719
|
}
|
|
702
720
|
|
|
@@ -711,7 +729,7 @@ export class SdkClient {
|
|
|
711
729
|
reason,
|
|
712
730
|
});
|
|
713
731
|
const cancelled = (): SdkClientError =>
|
|
714
|
-
|
|
732
|
+
transportError("connection_closed", "SDK client closed", lastError, diagnostics("cancelled"));
|
|
715
733
|
/**
|
|
716
734
|
* A deadline and retry budget measure different failure axes. One-shot clients
|
|
717
735
|
* need the deadline to fast-fail their operation, while long-lived ACP sessions
|
|
@@ -730,10 +748,10 @@ export class SdkClient {
|
|
|
730
748
|
try {
|
|
731
749
|
const incarnation = await this.#open(cycle);
|
|
732
750
|
if (!this.#isActive(incarnation) && (!this.#isOpening(cycle) || cycle.candidate !== incarnation))
|
|
733
|
-
throw
|
|
751
|
+
throw transportError("connection_closed", "SDK WebSocket is not connected");
|
|
734
752
|
await this.#waitForHello(incarnation);
|
|
735
753
|
if (this.#isActive(incarnation)) return incarnation;
|
|
736
|
-
throw
|
|
754
|
+
throw transportError("connection_closed", "SDK WebSocket is not connected");
|
|
737
755
|
} catch (error) {
|
|
738
756
|
lastError = error;
|
|
739
757
|
if (!this.#isOpening(cycle)) throw cancelled();
|
|
@@ -743,7 +761,7 @@ export class SdkClient {
|
|
|
743
761
|
candidate,
|
|
744
762
|
error instanceof SdkClientError
|
|
745
763
|
? error
|
|
746
|
-
:
|
|
764
|
+
: transportError("unavailable", "SDK WebSocket connection failed", error),
|
|
747
765
|
true,
|
|
748
766
|
);
|
|
749
767
|
if (attempt < this.#reconnectAttempts) {
|
|
@@ -780,7 +798,7 @@ export class SdkClient {
|
|
|
780
798
|
}
|
|
781
799
|
cycle.phase = "complete";
|
|
782
800
|
if (this.#opening === cycle) this.#opening = null;
|
|
783
|
-
const error =
|
|
801
|
+
const error = transportError(
|
|
784
802
|
"reconnect_exhausted",
|
|
785
803
|
"SDK WebSocket reconnect attempts exhausted",
|
|
786
804
|
lastError,
|
|
@@ -891,7 +909,7 @@ export class SdkClient {
|
|
|
891
909
|
if (incarnation.failure) return Promise.reject(incarnation.failure);
|
|
892
910
|
if (this.#isActive(incarnation)) return Promise.resolve();
|
|
893
911
|
if (!this.#isCandidate(incarnation.cycle, incarnation) || incarnation.phase !== "hello")
|
|
894
|
-
return Promise.reject(
|
|
912
|
+
return Promise.reject(transportError("connection_closed", "SDK WebSocket is not connected"));
|
|
895
913
|
const deferred = Promise.withResolvers<void>();
|
|
896
914
|
incarnation.resolveHello = deferred.resolve;
|
|
897
915
|
incarnation.rejectHello = deferred.reject;
|
|
@@ -903,7 +921,7 @@ export class SdkClient {
|
|
|
903
921
|
const error =
|
|
904
922
|
this.#deadline !== undefined && Date.now() >= this.#deadline
|
|
905
923
|
? this.#deadlineError()
|
|
906
|
-
:
|
|
924
|
+
: transportError("timeout", `SDK WebSocket connection timed out after ${timeoutMs}ms`);
|
|
907
925
|
incarnation.rejectOpen?.(error);
|
|
908
926
|
this.#retire(incarnation, error, true);
|
|
909
927
|
}
|
|
@@ -911,20 +929,20 @@ export class SdkClient {
|
|
|
911
929
|
#onSocketFailure(incarnation: Incarnation, event?: Event): void {
|
|
912
930
|
if (!this.#isCandidate(incarnation.cycle, incarnation) && !this.#isActive(incarnation)) return;
|
|
913
931
|
const detail = event as (Event & { error?: unknown; message?: unknown }) | undefined;
|
|
932
|
+
let failureMessage = "SDK WebSocket connection closed";
|
|
933
|
+
if (detail?.error instanceof Error) failureMessage = detail.error.message;
|
|
934
|
+
else if (typeof detail?.message === "string") failureMessage = detail.message;
|
|
914
935
|
const error =
|
|
915
|
-
detail?.error instanceof
|
|
936
|
+
detail?.error instanceof SdkClientError && detail.error.transport
|
|
916
937
|
? detail.error
|
|
917
|
-
:
|
|
918
|
-
"connection_closed",
|
|
919
|
-
typeof detail?.message === "string" ? detail.message : "SDK WebSocket connection closed",
|
|
920
|
-
);
|
|
938
|
+
: transportError("connection_closed", failureMessage);
|
|
921
939
|
if (incarnation.phase === "opening") incarnation.rejectOpen?.(error);
|
|
922
940
|
if (incarnation.phase === "hello") incarnation.rejectHello?.(error);
|
|
923
941
|
this.#retire(
|
|
924
942
|
incarnation,
|
|
925
943
|
error instanceof SdkClientError
|
|
926
944
|
? error
|
|
927
|
-
:
|
|
945
|
+
: transportError("unavailable", "SDK WebSocket connection failed", error),
|
|
928
946
|
true,
|
|
929
947
|
true,
|
|
930
948
|
);
|
|
@@ -1115,7 +1133,7 @@ export class SdkClient {
|
|
|
1115
1133
|
const onClose = (): void => resolve();
|
|
1116
1134
|
socket.addEventListener("close", onClose, { once: true });
|
|
1117
1135
|
const timer = setTimeout(
|
|
1118
|
-
() => reject(
|
|
1136
|
+
() => reject(transportError("timeout", `SDK WebSocket close timed out after ${timeoutMs}ms`)),
|
|
1119
1137
|
timeoutMs,
|
|
1120
1138
|
);
|
|
1121
1139
|
timer.unref?.();
|
|
@@ -1124,9 +1142,9 @@ export class SdkClient {
|
|
|
1124
1142
|
if (Number(socket.readyState) === WebSocket.CLOSED) resolve();
|
|
1125
1143
|
await promise;
|
|
1126
1144
|
} catch (error) {
|
|
1127
|
-
if (error instanceof SdkClientError) throw error;
|
|
1145
|
+
if (error instanceof SdkClientError && error.transport) throw error;
|
|
1128
1146
|
if (Number(socket.readyState) !== WebSocket.CLOSED)
|
|
1129
|
-
throw
|
|
1147
|
+
throw transportError("connection_closed", "SDK WebSocket close failed", error);
|
|
1130
1148
|
} finally {
|
|
1131
1149
|
clearTimeout(timer);
|
|
1132
1150
|
socket.removeEventListener("close", onClose);
|
|
@@ -50,6 +50,12 @@ import { parseThinkingLevel } from "../../thinking";
|
|
|
50
50
|
import { readEndpointFile } from "../broker/endpoint-authority";
|
|
51
51
|
import { ensureBroker } from "../broker/ensure";
|
|
52
52
|
import { processIncarnation } from "../broker/process-incarnation";
|
|
53
|
+
import { RecoveryBackoffTracker } from "../broker/recovery-backoff";
|
|
54
|
+
import {
|
|
55
|
+
captureRuntimeImageIdentity,
|
|
56
|
+
isSdkInternalRuntimeImageReplaced,
|
|
57
|
+
sdkInternalRuntimeImage,
|
|
58
|
+
} from "../broker/runtime";
|
|
53
59
|
import {
|
|
54
60
|
type MasterRoleAttestationV2,
|
|
55
61
|
resolveSessionLocator,
|
|
@@ -716,6 +722,15 @@ export interface CreateSdkSessionRuntimeOptions {
|
|
|
716
722
|
onFailureDiagnosticKeyCountForTests?: (count: number) => void;
|
|
717
723
|
/** Test-only observation after a resolved invocation completion is reconciled. */
|
|
718
724
|
onInvocationCompletionReconciledForTests?: (kind: InvocationKind, correlation: InvocationCorrelation) => void;
|
|
725
|
+
/** Test hook: provides a function to inject a controlled clock for broker recovery backoff assertions. */
|
|
726
|
+
setRecoveryBackoffClockForTest?: (inject: (clock: { now(): number }) => void) => void;
|
|
727
|
+
/** Test hook: inject mock runtime image identity and replacement detection for broker recovery tests. */
|
|
728
|
+
setRuntimeImageIdentityForTest?: (
|
|
729
|
+
register: (
|
|
730
|
+
captureFn: () => Awaited<ReturnType<typeof captureRuntimeImageIdentity>> | undefined,
|
|
731
|
+
isReplacedFn: () => Promise<boolean>,
|
|
732
|
+
) => void,
|
|
733
|
+
) => void;
|
|
719
734
|
}
|
|
720
735
|
|
|
721
736
|
function unavailable(operation: string): () => never {
|
|
@@ -7013,6 +7028,43 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
7013
7028
|
let brokerRegistered = false;
|
|
7014
7029
|
let brokerRegistrationInFlight: Promise<void> | undefined;
|
|
7015
7030
|
let brokerRecoveryStopped = false;
|
|
7031
|
+
let brokerRecoveryRestartRequired = false;
|
|
7032
|
+
const brokerRecoveryBackoff = new RecoveryBackoffTracker();
|
|
7033
|
+
if (options.setRecoveryBackoffClockForTest) {
|
|
7034
|
+
options.setRecoveryBackoffClockForTest((clock: { now(): number }) =>
|
|
7035
|
+
brokerRecoveryBackoff.setClockForTest(clock),
|
|
7036
|
+
);
|
|
7037
|
+
}
|
|
7038
|
+
// Capture runtime image identity at startup to detect replacements
|
|
7039
|
+
let startupRuntimeImageIdentity: Awaited<ReturnType<typeof captureRuntimeImageIdentity>> | undefined;
|
|
7040
|
+
let captureStartupRuntimeImageForTest:
|
|
7041
|
+
| (() => Awaited<ReturnType<typeof captureRuntimeImageIdentity>> | undefined)
|
|
7042
|
+
| undefined;
|
|
7043
|
+
let isRuntimeImageReplacedForTest: (() => Promise<boolean>) | undefined;
|
|
7044
|
+
if (options.setRuntimeImageIdentityForTest) {
|
|
7045
|
+
options.setRuntimeImageIdentityForTest(
|
|
7046
|
+
(
|
|
7047
|
+
captureFn: () => Awaited<ReturnType<typeof captureRuntimeImageIdentity>> | undefined,
|
|
7048
|
+
isReplacedFn: () => Promise<boolean>,
|
|
7049
|
+
) => {
|
|
7050
|
+
captureStartupRuntimeImageForTest = captureFn;
|
|
7051
|
+
isRuntimeImageReplacedForTest = isReplacedFn;
|
|
7052
|
+
},
|
|
7053
|
+
);
|
|
7054
|
+
}
|
|
7055
|
+
const captureStartupRuntimeImage = async (): Promise<void> => {
|
|
7056
|
+
if (options.setRuntimeImageIdentityForTest) {
|
|
7057
|
+
// Test mode: use injected identity
|
|
7058
|
+
startupRuntimeImageIdentity = captureStartupRuntimeImageForTest?.();
|
|
7059
|
+
} else {
|
|
7060
|
+
// Production mode: capture real runtime image
|
|
7061
|
+
const runtimeImage = sdkInternalRuntimeImage();
|
|
7062
|
+
if (runtimeImage) {
|
|
7063
|
+
startupRuntimeImageIdentity = await captureRuntimeImageIdentity(runtimeImage);
|
|
7064
|
+
}
|
|
7065
|
+
}
|
|
7066
|
+
};
|
|
7067
|
+
const startupImageCapture = captureStartupRuntimeImage();
|
|
7016
7068
|
const registerBroker = async (): Promise<void> => {
|
|
7017
7069
|
if (brokerRegistered) return;
|
|
7018
7070
|
if (brokerRegistrationInFlight !== undefined) {
|
|
@@ -7143,17 +7195,58 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
7143
7195
|
let brokerRecoveryTimer: NodeJS.Timeout | undefined;
|
|
7144
7196
|
let brokerRecoveryInFlight: Promise<void> | undefined;
|
|
7145
7197
|
const runBrokerRecovery = async (): Promise<void> => {
|
|
7146
|
-
if (brokerRecoveryStopped || brokerRecoveryInFlight !== undefined) return;
|
|
7198
|
+
if (brokerRecoveryStopped || brokerRecoveryInFlight !== undefined || brokerRecoveryRestartRequired) return;
|
|
7199
|
+
// Checked synchronously so an interval tick cannot start an attempt the backoff forbids.
|
|
7200
|
+
if (!brokerRecoveryBackoff.canAttemptRecovery(options.agentDir)) return;
|
|
7201
|
+
|
|
7202
|
+
// The single-flight slot is claimed before the first await: the interval does not wait
|
|
7203
|
+
// for prior callbacks, so a slow runtime-image probe must not let a second tick through
|
|
7204
|
+
// to ensure the broker and record the same failure twice.
|
|
7147
7205
|
const recovery = (async (): Promise<void> => {
|
|
7206
|
+
// A replaced runtime image cannot launch a broker; restarting the session is the only fix.
|
|
7207
|
+
const imageReplaced = await (isRuntimeImageReplacedForTest
|
|
7208
|
+
? isRuntimeImageReplacedForTest()
|
|
7209
|
+
: isSdkInternalRuntimeImageReplaced(startupRuntimeImageIdentity).catch(() => false));
|
|
7210
|
+
if (imageReplaced) {
|
|
7211
|
+
brokerRecoveryRestartRequired = true;
|
|
7212
|
+
stopBrokerRecovery();
|
|
7213
|
+
logger.warn("sdk broker recovery requires session restart", {
|
|
7214
|
+
reason: "runtime_image_replaced",
|
|
7215
|
+
});
|
|
7216
|
+
return;
|
|
7217
|
+
}
|
|
7148
7218
|
if (brokerRecoveryStopped) return;
|
|
7149
7219
|
if (brokerRegistered) await (options.ensureBrokerImpl ?? ensureBroker)({ agentDir: options.agentDir });
|
|
7150
|
-
else
|
|
7220
|
+
else {
|
|
7221
|
+
// Optional registration logs and resolves on failure; recovery must still count it
|
|
7222
|
+
// as a failed attempt so the backoff holds instead of resetting every tick.
|
|
7223
|
+
await registerBroker();
|
|
7224
|
+
if (!brokerRegistered && !brokerRecoveryStopped)
|
|
7225
|
+
throw Object.assign(new Error("SDK broker registration did not complete."), {
|
|
7226
|
+
code: "registration_incomplete",
|
|
7227
|
+
});
|
|
7228
|
+
}
|
|
7151
7229
|
})();
|
|
7152
7230
|
brokerRecoveryInFlight = recovery;
|
|
7153
7231
|
try {
|
|
7154
7232
|
await recovery;
|
|
7233
|
+
if (brokerRecoveryRestartRequired || brokerRecoveryStopped) return;
|
|
7234
|
+
brokerRecoveryBackoff.recordSuccess(options.agentDir);
|
|
7155
7235
|
} catch (error) {
|
|
7156
|
-
|
|
7236
|
+
const shouldContinue = brokerRecoveryBackoff.recordFailure(options.agentDir);
|
|
7237
|
+
if (!shouldContinue) {
|
|
7238
|
+
// Max recovery attempts reached; mark that restart is required.
|
|
7239
|
+
brokerRecoveryRestartRequired = true;
|
|
7240
|
+
stopBrokerRecovery();
|
|
7241
|
+
logger.warn("sdk broker recovery max attempts exceeded", {
|
|
7242
|
+
code: brokerDiagnosticCode(error),
|
|
7243
|
+
});
|
|
7244
|
+
} else {
|
|
7245
|
+
logger.warn("sdk broker recovery unavailable", {
|
|
7246
|
+
code: brokerDiagnosticCode(error),
|
|
7247
|
+
backoffWaitMs: brokerRecoveryBackoff.getBackoffWaitMs(options.agentDir),
|
|
7248
|
+
});
|
|
7249
|
+
}
|
|
7157
7250
|
} finally {
|
|
7158
7251
|
if (brokerRecoveryInFlight === recovery) brokerRecoveryInFlight = undefined;
|
|
7159
7252
|
}
|
|
@@ -7210,6 +7303,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
7210
7303
|
try {
|
|
7211
7304
|
publishedEndpointUrl = (await runtime.start()).url;
|
|
7212
7305
|
await registerBroker();
|
|
7306
|
+
// Await the startup runtime image capture before arming recovery.
|
|
7307
|
+
// This ensures replacement detection works correctly on the first recovery check.
|
|
7308
|
+
await startupImageCapture;
|
|
7213
7309
|
if (!brokerRecoveryStopped) startBrokerRecovery();
|
|
7214
7310
|
} catch (error) {
|
|
7215
7311
|
active = undefined;
|
|
@@ -97,8 +97,13 @@ export class PromptActivity {
|
|
|
97
97
|
#awaitingModel = false;
|
|
98
98
|
|
|
99
99
|
/** Folds one inbound frame's lifecycle event into the in-flight tool and model state. */
|
|
100
|
-
observe(
|
|
101
|
-
|
|
100
|
+
observe(
|
|
101
|
+
eventType: string | undefined,
|
|
102
|
+
toolCallId: string | undefined,
|
|
103
|
+
messageRole: string | undefined,
|
|
104
|
+
messageHasToolCall = false,
|
|
105
|
+
): void {
|
|
106
|
+
this.#observeInference(eventType, messageRole, messageHasToolCall);
|
|
102
107
|
if (toolCallId === undefined) return;
|
|
103
108
|
if (eventType === "tool_execution_start" || eventType === "tool_execution_update") this.#running.add(toolCallId);
|
|
104
109
|
else if (eventType === "tool_execution_end") this.#running.delete(toolCallId);
|
|
@@ -110,7 +115,11 @@ export class PromptActivity {
|
|
|
110
115
|
* re-invocation that carries a tool result back — until the first frame of that
|
|
111
116
|
* response proves the model answered.
|
|
112
117
|
*/
|
|
113
|
-
#observeInference(
|
|
118
|
+
#observeInference(
|
|
119
|
+
eventType: string | undefined,
|
|
120
|
+
messageRole: string | undefined,
|
|
121
|
+
messageHasToolCall: boolean,
|
|
122
|
+
): void {
|
|
114
123
|
if (eventType === "agent_start" || eventType === "tool_execution_end") {
|
|
115
124
|
this.#awaitingModel = true;
|
|
116
125
|
return;
|
|
@@ -132,7 +141,11 @@ export class PromptActivity {
|
|
|
132
141
|
// Providers that do not stream emit no `message_update`, so the assistant message
|
|
133
142
|
// itself is the first proof. `message_start`/`message_end` also carry the user and
|
|
134
143
|
// tool-result messages the host echoes back, which prove nothing about the model.
|
|
135
|
-
if (
|
|
144
|
+
if (
|
|
145
|
+
(eventType === "message_start" || eventType === "message_end") &&
|
|
146
|
+
messageRole === "assistant" &&
|
|
147
|
+
!messageHasToolCall
|
|
148
|
+
)
|
|
136
149
|
this.#awaitingModel = false;
|
|
137
150
|
}
|
|
138
151
|
|
|
@@ -5506,6 +5506,17 @@
|
|
|
5506
5506
|
"packages/coding-agent/test/sdk-operation-inventory.test.ts"
|
|
5507
5507
|
]
|
|
5508
5508
|
},
|
|
5509
|
+
{
|
|
5510
|
+
"sourceId": "agent_session:getAutoCompactionThresholdTokens",
|
|
5511
|
+
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
|
|
5512
|
+
"sourceKind": "agent_session",
|
|
5513
|
+
"decision": "exclude",
|
|
5514
|
+
"rationale": "internal compaction-threshold accessor feeding the /context breakdown; exposed through context.get, not an independent SDK operation",
|
|
5515
|
+
"exclusionMetadata": {
|
|
5516
|
+
"adapterMappings": "not_applicable",
|
|
5517
|
+
"testIds": "not_applicable"
|
|
5518
|
+
}
|
|
5519
|
+
},
|
|
5509
5520
|
{
|
|
5510
5521
|
"sourceId": "agent_session:getContextUsage",
|
|
5511
5522
|
"sourceFile": "packages/coding-agent/src/session/agent-session.ts",
|
|
@@ -58,23 +58,28 @@ import {
|
|
|
58
58
|
CompactionCancelledError,
|
|
59
59
|
type CompactionPreparation,
|
|
60
60
|
type CompactionResult,
|
|
61
|
+
type CompactionSettings,
|
|
61
62
|
calculateContextTokens,
|
|
62
63
|
calculatePromptTokens,
|
|
63
64
|
collectEntriesForBranchSummary,
|
|
64
65
|
compact,
|
|
65
66
|
type EmergencyCompactionSample,
|
|
67
|
+
effectiveReserveTokens,
|
|
66
68
|
emergencyCompactionReason,
|
|
67
69
|
estimateMessageTokensHeuristic,
|
|
68
70
|
estimateTextTokensHeuristic,
|
|
69
71
|
generateBranchSummary,
|
|
70
72
|
generateHandoff,
|
|
71
73
|
IMAGE_TOKEN_ESTIMATE,
|
|
74
|
+
isDefaultAutoThresholdCeilingApplied,
|
|
72
75
|
prepareCompaction,
|
|
73
76
|
type RemoteCompactionFallbackHealthEvent,
|
|
74
77
|
type RemoteCompactionFallbackHealthHooks,
|
|
78
|
+
resolveThresholdTokens,
|
|
75
79
|
type SummaryOptions,
|
|
76
80
|
shouldCompact,
|
|
77
81
|
} from "@gajae-code/agent-core/compaction";
|
|
82
|
+
|
|
78
83
|
import {
|
|
79
84
|
commitToolOutputPrune,
|
|
80
85
|
createPrunedNotice,
|
|
@@ -19954,10 +19959,15 @@ export class AgentSession {
|
|
|
19954
19959
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
19955
19960
|
const pathEntries = this.#withoutEphemeralCustomMessageEntries(this.sessionManager.getBranch());
|
|
19956
19961
|
|
|
19957
|
-
const preparation = prepareCompaction(
|
|
19958
|
-
|
|
19959
|
-
|
|
19960
|
-
|
|
19962
|
+
const preparation = prepareCompaction(
|
|
19963
|
+
pathEntries,
|
|
19964
|
+
this.#compactionSettingsForContextPromotion(compactionSettings),
|
|
19965
|
+
{
|
|
19966
|
+
contextWindow: this.model.contextWindow,
|
|
19967
|
+
tokenCorrectionRatio: this.#computeCompactionTokenCorrectionRatio(),
|
|
19968
|
+
},
|
|
19969
|
+
);
|
|
19970
|
+
|
|
19961
19971
|
if (!preparation) {
|
|
19962
19972
|
// Check why we can't compact
|
|
19963
19973
|
const lastEntry = pathEntries[pathEntries.length - 1];
|
|
@@ -20974,12 +20984,12 @@ export class AgentSession {
|
|
|
20974
20984
|
}
|
|
20975
20985
|
}
|
|
20976
20986
|
|
|
20977
|
-
#recordAdaptiveCompactionCall
|
|
20987
|
+
#recordAdaptiveCompactionCall(
|
|
20978
20988
|
contextTokens: number,
|
|
20979
|
-
settings:
|
|
20989
|
+
settings: CompactionSettings,
|
|
20980
20990
|
assistantMessage: AssistantMessage,
|
|
20981
|
-
):
|
|
20982
|
-
if (!settings.adaptive?.enabled) return settings;
|
|
20991
|
+
): CompactionSettings {
|
|
20992
|
+
if (!settings.adaptive?.enabled) return this.#compactionSettingsWithAdaptiveState(settings);
|
|
20983
20993
|
this.#adaptiveCompactionTracker.setWindowMs(settings.adaptive.turnWindow * 60_000);
|
|
20984
20994
|
const messageKey = this.#adaptiveCompactionMessageKey(assistantMessage);
|
|
20985
20995
|
if (this.#adaptiveCompactionRecordedMessageKey === messageKey) {
|
|
@@ -20994,11 +21004,31 @@ export class AgentSession {
|
|
|
20994
21004
|
return `${message.provider}\u0000${message.model}\u0000${message.timestamp}\u0000${message.usage?.totalTokens ?? ""}`;
|
|
20995
21005
|
}
|
|
20996
21006
|
|
|
20997
|
-
#compactionSettingsWithAdaptiveState
|
|
20998
|
-
|
|
21007
|
+
#compactionSettingsWithAdaptiveState(settings: CompactionSettings): CompactionSettings {
|
|
21008
|
+
const adaptiveSettings = settings.adaptive?.enabled
|
|
21009
|
+
? { ...settings, adaptiveState: this.#adaptiveCompactionTracker.decisionState() }
|
|
21010
|
+
: settings;
|
|
21011
|
+
return this.#compactionSettingsForContextPromotion(adaptiveSettings);
|
|
21012
|
+
}
|
|
21013
|
+
|
|
21014
|
+
#compactionSettingsForContextPromotion(settings: CompactionSettings): CompactionSettings {
|
|
21015
|
+
const contextWindow = this.model?.contextWindow ?? 0;
|
|
21016
|
+
const isContextPromoted = this.#temporaryProviderSessionScopes.some(
|
|
21017
|
+
scope => scope.token.reason === "context-promotion",
|
|
21018
|
+
);
|
|
21019
|
+
if (
|
|
21020
|
+
!isContextPromoted ||
|
|
21021
|
+
!isDefaultAutoThresholdCeilingApplied(contextWindow, settings)
|
|
21022
|
+
) {
|
|
21023
|
+
return settings;
|
|
21024
|
+
}
|
|
21025
|
+
|
|
21026
|
+
// Context promotion is opt-in and exists to use the larger model's headroom.
|
|
21027
|
+
// Keep the reserve-based default on the promoted model instead of immediately
|
|
21028
|
+
// compacting at the same 300K ceiling that triggered promotion.
|
|
20999
21029
|
return {
|
|
21000
21030
|
...settings,
|
|
21001
|
-
|
|
21031
|
+
thresholdTokens: contextWindow - effectiveReserveTokens(contextWindow, settings),
|
|
21002
21032
|
};
|
|
21003
21033
|
}
|
|
21004
21034
|
|
|
@@ -22304,10 +22334,15 @@ export class AgentSession {
|
|
|
22304
22334
|
// correction only when it SHRINKS the keep window (ratio >= 1), never when
|
|
22305
22335
|
// it would grow it, so recovery cannot re-overflow the provider window.
|
|
22306
22336
|
const overflowRatio = this.#computeCompactionTokenCorrectionRatio();
|
|
22307
|
-
const preparation = prepareCompaction(
|
|
22308
|
-
|
|
22309
|
-
|
|
22310
|
-
|
|
22337
|
+
const preparation = prepareCompaction(
|
|
22338
|
+
pathEntries,
|
|
22339
|
+
this.#compactionSettingsForContextPromotion(compactionSettings),
|
|
22340
|
+
{
|
|
22341
|
+
contextWindow: this.model?.contextWindow,
|
|
22342
|
+
tokenCorrectionRatio: overflowRatio !== undefined ? Math.max(1, overflowRatio) : undefined,
|
|
22343
|
+
},
|
|
22344
|
+
);
|
|
22345
|
+
|
|
22311
22346
|
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
22312
22347
|
|
|
22313
22348
|
if (!preparation) {
|
|
@@ -24390,8 +24425,15 @@ export class AgentSession {
|
|
|
24390
24425
|
if (!managedFallback && assistantMessageHasVisibleOrToolContent(message)) {
|
|
24391
24426
|
return false;
|
|
24392
24427
|
}
|
|
24428
|
+
const canReplayUnexpectedSocketClose =
|
|
24429
|
+
classification === "transient" &&
|
|
24430
|
+
isUnexpectedSocketCloseMessage(message.errorMessage ?? "") &&
|
|
24431
|
+
!hasBareDefaultRetryDisqualifyingFacts(message) &&
|
|
24432
|
+
this.#hasCleanRetryReplaySafety;
|
|
24393
24433
|
// Bare defaults retain their narrow watchdog and provider capacity-overload
|
|
24394
|
-
// admissions. A
|
|
24434
|
+
// admissions. A content-free unexpected socket close is replay-safe when
|
|
24435
|
+
// the attempt has no conflicting facts and the retry scope is clean. A
|
|
24436
|
+
// first-event timeout adds the typed, content-free,
|
|
24395
24437
|
// current-clean-scope requirement above; other transient watchdogs preserve
|
|
24396
24438
|
// legacy behavior. A provider overload is admitted only from that provider's
|
|
24397
24439
|
// own typed overload code on a content-free attempt carrying no conflicting
|
|
@@ -24400,12 +24442,16 @@ export class AgentSession {
|
|
|
24400
24442
|
if (!managedFallback && !legacyRetryConfigured && !canReplayRotatedCredential && !canReplayEmptyResponse) {
|
|
24401
24443
|
if (
|
|
24402
24444
|
(!canReplayProviderOverload &&
|
|
24445
|
+
!canReplayUnexpectedSocketClose &&
|
|
24403
24446
|
!this.#isTypedFirstEventTimeout(message) &&
|
|
24404
24447
|
!messageOnlyWatchdogTimeout &&
|
|
24405
24448
|
(hasBareDefaultRetryDisqualifyingFacts(message) ||
|
|
24406
24449
|
(classification !== "transient" && classification !== "first_event_timeout") ||
|
|
24407
24450
|
!BARE_DEFAULT_WATCHDOG_ERROR.test(message.errorMessage ?? ""))) ||
|
|
24408
|
-
(!firstEventTimeout &&
|
|
24451
|
+
(!firstEventTimeout &&
|
|
24452
|
+
!messageOnlyWatchdogTimeout &&
|
|
24453
|
+
!canReplayUnexpectedSocketClose &&
|
|
24454
|
+
!this.#hasCleanRetryReplaySafety)
|
|
24409
24455
|
) {
|
|
24410
24456
|
return false;
|
|
24411
24457
|
}
|
|
@@ -24414,6 +24460,7 @@ export class AgentSession {
|
|
|
24414
24460
|
!managedFallback &&
|
|
24415
24461
|
classification === "transient" &&
|
|
24416
24462
|
!canReplayProviderOverload &&
|
|
24463
|
+
!canReplayUnexpectedSocketClose &&
|
|
24417
24464
|
!this.#isIdleStreamStallErrorMessage(message.errorMessage ?? "");
|
|
24418
24465
|
|
|
24419
24466
|
const failedSelector = managedFallback ? controller.currentSelector() : undefined;
|
|
@@ -27184,6 +27231,21 @@ export class AgentSession {
|
|
|
27184
27231
|
};
|
|
27185
27232
|
}
|
|
27186
27233
|
|
|
27234
|
+
/** Current auto-compaction threshold, including active model-promotion policy. */
|
|
27235
|
+
getAutoCompactionThresholdTokens(contextTokens?: number): number {
|
|
27236
|
+
const model = this.model;
|
|
27237
|
+
if (!model) return 0;
|
|
27238
|
+
const contextWindow = model.contextWindow ?? 0;
|
|
27239
|
+
if (contextWindow <= 0) return 0;
|
|
27240
|
+
const settings = this.settings.getGroup("compaction") as CompactionSettings;
|
|
27241
|
+
return resolveThresholdTokens(
|
|
27242
|
+
contextWindow,
|
|
27243
|
+
this.#compactionSettingsWithAdaptiveState(settings),
|
|
27244
|
+
0,
|
|
27245
|
+
contextTokens,
|
|
27246
|
+
);
|
|
27247
|
+
}
|
|
27248
|
+
|
|
27187
27249
|
/**
|
|
27188
27250
|
* Get current context usage statistics.
|
|
27189
27251
|
* Uses the last assistant message's usage data when available,
|