@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
|
@@ -214,6 +214,12 @@ interface PromptWaiter {
|
|
|
214
214
|
/** Whether ANY overlapping cancel attempt for this prompt was acknowledged:
|
|
215
215
|
* a later failed attempt must not erase an earlier success (review thread P2). */
|
|
216
216
|
cancelAcknowledged?: boolean;
|
|
217
|
+
/** Durable cancel intent after a no-effect abort before this waiter starts work; retained until settlement. */
|
|
218
|
+
cancelBeforeAdmission?: boolean;
|
|
219
|
+
/** Abort scope replayed after admission and again at correlated start if no turn was active yet. */
|
|
220
|
+
cancelBeforeAdmissionScope?: AbortScope;
|
|
221
|
+
/** A correlated start may need one final abort after an admitted-but-not-running no-effect response. */
|
|
222
|
+
cancelAfterStartAbortIssued?: boolean;
|
|
217
223
|
/** In-flight cancel attempts for this prompt: cancellation intent must stay
|
|
218
224
|
* set while any attempt can still acknowledge, so a failure only clears
|
|
219
225
|
* the shared flag when no attempt remains pending (review thread P2). */
|
|
@@ -591,6 +597,12 @@ function isAbortAcknowledged(value: unknown, scope: AbortScope): boolean {
|
|
|
591
597
|
if (result.aborted === true) return true;
|
|
592
598
|
return result.ok === true && result.turn === "stopped" && result.selection === scope;
|
|
593
599
|
}
|
|
600
|
+
function isAbortNoEffect(value: unknown): boolean {
|
|
601
|
+
const candidate = object(value);
|
|
602
|
+
const result = object(candidate?.result) ?? candidate;
|
|
603
|
+
return result?.turn === "no_active_turn" || result?.turn === "no_effect" || result?.turn === "no_store";
|
|
604
|
+
}
|
|
605
|
+
|
|
594
606
|
function strictCorrelationFrom(...values: unknown[]): PromptCorrelation | undefined {
|
|
595
607
|
const correlation: PromptCorrelation = {};
|
|
596
608
|
let malformed = false;
|
|
@@ -655,7 +667,9 @@ function logDroppedPromptTerminal(
|
|
|
655
667
|
});
|
|
656
668
|
}
|
|
657
669
|
|
|
658
|
-
type SdkPromptFailedOutcome = Extract<SdkPromptTerminalOutcome, { kind: "failed" }
|
|
670
|
+
type SdkPromptFailedOutcome = Extract<SdkPromptTerminalOutcome, { kind: "failed" }> & {
|
|
671
|
+
deliveryFailure?: { cause: string; frameBytes: number };
|
|
672
|
+
};
|
|
659
673
|
|
|
660
674
|
/**
|
|
661
675
|
* A prompt rejection that still carries the terminal's structured failure classification.
|
|
@@ -791,6 +805,13 @@ function promptFailureWireData(failure: SdkPromptFailedOutcome): Record<string,
|
|
|
791
805
|
category: failure.category,
|
|
792
806
|
retryability: promptFailureRetryability(failure.category),
|
|
793
807
|
...(isSafePromptFailureCode(failure.providerCode) ? { providerCode: failure.providerCode } : {}),
|
|
808
|
+
...(failure.deliveryFailure
|
|
809
|
+
? {
|
|
810
|
+
reason: "delivery_failed",
|
|
811
|
+
cause: failure.deliveryFailure.cause,
|
|
812
|
+
frameBytes: String(failure.deliveryFailure.frameBytes),
|
|
813
|
+
}
|
|
814
|
+
: {}),
|
|
794
815
|
...(operatorMessage === undefined ? {} : { operatorMessage }),
|
|
795
816
|
};
|
|
796
817
|
}
|
|
@@ -909,13 +930,28 @@ function terminalOutcome(event: JsonObject): SdkPromptTerminalOutcome | undefine
|
|
|
909
930
|
typeof outcome.message === "string" &&
|
|
910
931
|
(outcome.provenance === "agent_failed" || outcome.provenance === "deadline")
|
|
911
932
|
) {
|
|
912
|
-
|
|
933
|
+
const failure = failedPromptOutcome({
|
|
913
934
|
code: outcome.code,
|
|
914
935
|
provenance: outcome.provenance,
|
|
915
936
|
...(typeof outcome.providerCode === "string" ? { providerCode: outcome.providerCode } : {}),
|
|
916
937
|
...(isSdkPromptFailurePhase(outcome.phase) ? { phase: outcome.phase } : {}),
|
|
917
938
|
evidence: {},
|
|
918
939
|
});
|
|
940
|
+
const delivery = object(event.error);
|
|
941
|
+
if (
|
|
942
|
+
delivery?.code === "delivery_failed" &&
|
|
943
|
+
isSafePromptFailureCode(delivery.cause) &&
|
|
944
|
+
typeof delivery.frameBytes === "number" &&
|
|
945
|
+
Number.isSafeInteger(delivery.frameBytes) &&
|
|
946
|
+
delivery.frameBytes >= 0
|
|
947
|
+
) {
|
|
948
|
+
const classified: SdkPromptFailedOutcome = {
|
|
949
|
+
...failure,
|
|
950
|
+
deliveryFailure: { cause: delivery.cause, frameBytes: delivery.frameBytes },
|
|
951
|
+
};
|
|
952
|
+
return classified;
|
|
953
|
+
}
|
|
954
|
+
return failure;
|
|
919
955
|
}
|
|
920
956
|
return undefined;
|
|
921
957
|
}
|
|
@@ -1103,6 +1139,15 @@ function frameMessageRole(event: JsonObject | undefined): string | undefined {
|
|
|
1103
1139
|
return typeof role === "string" ? role : undefined;
|
|
1104
1140
|
}
|
|
1105
1141
|
|
|
1142
|
+
function frameMessageHasToolCall(event: JsonObject | undefined): boolean {
|
|
1143
|
+
const content = object(event?.message)?.content;
|
|
1144
|
+
if (!Array.isArray(content)) return false;
|
|
1145
|
+
return content.some(block => {
|
|
1146
|
+
const value = object(block);
|
|
1147
|
+
return value?.type === "toolCall" || value?.type === "tool_use";
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1106
1151
|
const ACP_CONFIG_OPTIONS = [
|
|
1107
1152
|
{ id: MODEL_CONFIG_ID, name: "Model", category: "model", options: [] },
|
|
1108
1153
|
{ id: THINKING_CONFIG_ID, name: "Thinking", category: "thought_level", options: [] },
|
|
@@ -1495,6 +1540,11 @@ export function acpRequestFailure(error: unknown): unknown {
|
|
|
1495
1540
|
const code = typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
|
|
1496
1541
|
if (typeof code !== "string") return error;
|
|
1497
1542
|
const message = error instanceof Error ? error.message : code;
|
|
1543
|
+
// ACP clients render `data.details` after the JSON-RPC message. The MCP launch
|
|
1544
|
+
// diagnostic is already the complete additional message, so repeating it in
|
|
1545
|
+
// `details` produces `Internal error: X.: X.` on the wire.
|
|
1546
|
+
const duplicateMcpLaunchDetails =
|
|
1547
|
+
code === "unavailable" && message.startsWith("MCP server request failed to start (");
|
|
1498
1548
|
// A prompt terminal additionally publishes its classification (issue #5615). The
|
|
1499
1549
|
// spread is empty for every other error, so their payloads are byte-identical to
|
|
1500
1550
|
// before. `code`/`details` and the JSON-RPC code itself are untouched either way:
|
|
@@ -1502,8 +1552,8 @@ export function acpRequestFailure(error: unknown): unknown {
|
|
|
1502
1552
|
const promptFailure = promptFailureFromDirectError(error);
|
|
1503
1553
|
const data =
|
|
1504
1554
|
promptFailure !== undefined
|
|
1505
|
-
? { code, details: message, ...promptFailureWireData(promptFailure) }
|
|
1506
|
-
: { code, details: message };
|
|
1555
|
+
? { code, ...(duplicateMcpLaunchDetails ? {} : { details: message }), ...promptFailureWireData(promptFailure) }
|
|
1556
|
+
: { code, ...(duplicateMcpLaunchDetails ? {} : { details: message }) };
|
|
1507
1557
|
// An abandoned prompt additionally publishes the plan it never finished (issue #5669).
|
|
1508
1558
|
// Same rule as above: `code`/`details` and the JSON-RPC code are untouched, and an abandon
|
|
1509
1559
|
// that observed no plan adds no keys, so its payload stays byte-identical to before.
|
|
@@ -2362,7 +2412,7 @@ export class AcpAgent implements Agent {
|
|
|
2362
2412
|
this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
|
|
2363
2413
|
return await response;
|
|
2364
2414
|
}
|
|
2365
|
-
if (record.cancelRequested && waiter.cancelAcknowledged) {
|
|
2415
|
+
if (record.cancelRequested && (waiter.cancelAcknowledged || waiter.cancelBeforeAdmission)) {
|
|
2366
2416
|
this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
|
|
2367
2417
|
await this.#settleCancelledPrompt(params.sessionId, record, waiter);
|
|
2368
2418
|
return await response;
|
|
@@ -2385,7 +2435,7 @@ export class AcpAgent implements Agent {
|
|
|
2385
2435
|
this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
|
|
2386
2436
|
return await response;
|
|
2387
2437
|
}
|
|
2388
|
-
if (record.cancelRequested && waiter.cancelAcknowledged) {
|
|
2438
|
+
if (record.cancelRequested && (waiter.cancelAcknowledged || waiter.cancelBeforeAdmission)) {
|
|
2389
2439
|
this.#releaseRetiredPromptAcknowledgement(params.sessionId, waiter);
|
|
2390
2440
|
await this.#settleCancelledPrompt(params.sessionId, record, waiter);
|
|
2391
2441
|
return await response;
|
|
@@ -2415,18 +2465,18 @@ export class AcpAgent implements Agent {
|
|
|
2415
2465
|
record.adapter,
|
|
2416
2466
|
);
|
|
2417
2467
|
}
|
|
2418
|
-
waiter.dispatched = true;
|
|
2419
|
-
// The turn is now dispatched to the host: this prompt owned the session's first turn,
|
|
2420
|
-
// so it — not a preflight rejection that threw before this point — is what settles
|
|
2421
|
-
// `firstPromptDone` in `prompt()` (review P2).
|
|
2422
|
-
if (retryReservation) retryReservation.admitted = true;
|
|
2423
2468
|
if (waiter.settled || record.activePrompt !== waiter) {
|
|
2424
2469
|
return await response;
|
|
2425
2470
|
}
|
|
2426
|
-
if (record.cancelRequested && waiter.cancelAcknowledged) {
|
|
2471
|
+
if (record.cancelRequested && (waiter.cancelAcknowledged || waiter.cancelBeforeAdmission)) {
|
|
2427
2472
|
await this.#settleCancelledPrompt(params.sessionId, record, waiter);
|
|
2428
2473
|
return await response;
|
|
2429
2474
|
}
|
|
2475
|
+
waiter.dispatched = true;
|
|
2476
|
+
// The turn is now dispatched to the host: this prompt owned the session's first turn,
|
|
2477
|
+
// so it — not a preflight rejection that threw before this point — is what settles
|
|
2478
|
+
// `firstPromptDone` in `prompt()` (review P2).
|
|
2479
|
+
if (retryReservation) retryReservation.admitted = true;
|
|
2430
2480
|
|
|
2431
2481
|
waiter.acknowledgementPending = true;
|
|
2432
2482
|
const promptAdapter = record.adapter;
|
|
@@ -2546,6 +2596,19 @@ export class AcpAgent implements Agent {
|
|
|
2546
2596
|
this.#rememberSettledPromptCorrelation(params.sessionId, record, acknowledgementCorrelation);
|
|
2547
2597
|
return await response;
|
|
2548
2598
|
}
|
|
2599
|
+
if (waiter.cancelBeforeAdmission && record.cancelRequested) {
|
|
2600
|
+
const scope = waiter.cancelBeforeAdmissionScope ?? "turn";
|
|
2601
|
+
// Keep the intent until an acknowledged abort or the prompt terminal settles it.
|
|
2602
|
+
void this.cancel({
|
|
2603
|
+
sessionId: params.sessionId,
|
|
2604
|
+
_meta: { gjc: { abortScope: scope } },
|
|
2605
|
+
} as CancelNotification).catch(error =>
|
|
2606
|
+
logger.warn("ACP could not retry a cancel after prompt admission", {
|
|
2607
|
+
sessionId: params.sessionId,
|
|
2608
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2609
|
+
}),
|
|
2610
|
+
);
|
|
2611
|
+
}
|
|
2549
2612
|
// Frames held while ownership was unknown belong to this prompt only when the
|
|
2550
2613
|
// acknowledgement proves their complete correlation matches exactly.
|
|
2551
2614
|
const deferredActivityFrames = waiter.deferredActivityFrames.splice(0);
|
|
@@ -2691,7 +2754,10 @@ export class AcpAgent implements Agent {
|
|
|
2691
2754
|
// return the semantically meaningful `cancelled` stop reason, so that Clients
|
|
2692
2755
|
// can reliably confirm the cancellation." Surfacing the transport's `busy`
|
|
2693
2756
|
// rejection instead would show the user a spurious error for their own cancel.
|
|
2694
|
-
if (
|
|
2757
|
+
if (
|
|
2758
|
+
record.cancelRequested &&
|
|
2759
|
+
(waiter.cancelAcknowledged || (waiter.cancelBeforeAdmission && !waiter.acknowledged && !waiter.dispatched))
|
|
2760
|
+
) {
|
|
2695
2761
|
record.cancelRequested = false;
|
|
2696
2762
|
// The client's turn is settled by the return; the advisory idle publication
|
|
2697
2763
|
// must not gate it and must still be attempted so the running phase is
|
|
@@ -2715,6 +2781,8 @@ export class AcpAgent implements Agent {
|
|
|
2715
2781
|
// `_meta.gjc.abortScope: "owned"` (or `GJC_ACP_ABORT_SCOPE=owned`).
|
|
2716
2782
|
const scope = resolveAcpAbortScope(params._meta, process.env);
|
|
2717
2783
|
const waiter = record.activePrompt;
|
|
2784
|
+
const waiterWasUnacknowledged = waiter !== undefined && !waiter.acknowledged;
|
|
2785
|
+
const waiterWasBeforeActivity = waiter !== undefined && !waiter.observedTurnActivity;
|
|
2718
2786
|
if (waiter) {
|
|
2719
2787
|
// Overlapping cancels must not lose an earlier successful
|
|
2720
2788
|
// acknowledgement: ANY successful attempt resolves the shared
|
|
@@ -2733,9 +2801,27 @@ export class AcpAgent implements Agent {
|
|
|
2733
2801
|
}
|
|
2734
2802
|
}
|
|
2735
2803
|
try {
|
|
2736
|
-
|
|
2804
|
+
let acknowledgement = await record.adapter.cancel(scope);
|
|
2805
|
+
let acknowledgementScope = scope;
|
|
2806
|
+
if (
|
|
2807
|
+
!isAbortAcknowledged(acknowledgement, scope) &&
|
|
2808
|
+
isAbortNoEffect(acknowledgement) &&
|
|
2809
|
+
(waiterWasUnacknowledged || waiterWasBeforeActivity) &&
|
|
2810
|
+
waiter &&
|
|
2811
|
+
record.activePrompt === waiter &&
|
|
2812
|
+
!waiter.settled
|
|
2813
|
+
) {
|
|
2814
|
+
waiter.cancelBeforeAdmission = true;
|
|
2815
|
+
waiter.cancelBeforeAdmissionScope =
|
|
2816
|
+
waiter.cancelBeforeAdmissionScope === "owned" || scope === "owned" ? "owned" : "turn";
|
|
2817
|
+
if (waiter.acknowledged) {
|
|
2818
|
+
// The first abort raced prompt admission. Retry now that the turn is admitted.
|
|
2819
|
+
acknowledgementScope = waiter.cancelBeforeAdmissionScope;
|
|
2820
|
+
acknowledgement = await record.adapter.cancel(acknowledgementScope);
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2737
2823
|
const result = object(object(acknowledgement)?.result) ?? object(acknowledgement);
|
|
2738
|
-
if (!isAbortAcknowledged(acknowledgement,
|
|
2824
|
+
if (!isAbortAcknowledged(acknowledgement, acknowledgementScope))
|
|
2739
2825
|
throw new AcpSdkAdapterError(
|
|
2740
2826
|
"abort_unacknowledged",
|
|
2741
2827
|
"SDK did not acknowledge cancellation of the active prompt.",
|
|
@@ -2846,7 +2932,12 @@ export class AcpAgent implements Agent {
|
|
|
2846
2932
|
} finally {
|
|
2847
2933
|
if (waiter) {
|
|
2848
2934
|
waiter.pendingCancelAttempts = Math.max(0, (waiter.pendingCancelAttempts ?? 1) - 1);
|
|
2849
|
-
if (
|
|
2935
|
+
if (
|
|
2936
|
+
waiter.pendingCancelAttempts === 0 &&
|
|
2937
|
+
!waiter.cancelAcknowledged &&
|
|
2938
|
+
!waiter.cancelBeforeAdmission &&
|
|
2939
|
+
record.activePrompt === waiter
|
|
2940
|
+
) {
|
|
2850
2941
|
record.cancelRequested = false;
|
|
2851
2942
|
}
|
|
2852
2943
|
}
|
|
@@ -2869,8 +2960,9 @@ export class AcpAgent implements Agent {
|
|
|
2869
2960
|
|
|
2870
2961
|
async #settleCancelledPrompt(id: string, record: SessionRecord, waiter: PromptWaiter): Promise<void> {
|
|
2871
2962
|
// The authoritative terminal wins whenever it arrives in time; this only runs
|
|
2872
|
-
//
|
|
2873
|
-
|
|
2963
|
+
// after an acknowledged abort or before turn dispatch for this waiter.
|
|
2964
|
+
const preAdmissionCancellation = waiter.cancelBeforeAdmission && !waiter.acknowledged && !waiter.dispatched;
|
|
2965
|
+
if (!waiter.cancelAcknowledged && !preAdmissionCancellation) return;
|
|
2874
2966
|
if (this.#sessions.get(id) !== record || record.activePrompt !== waiter || waiter.settled) return;
|
|
2875
2967
|
if (waiter.terminalReserved) return;
|
|
2876
2968
|
// An overlapping acknowledged cancel does not supersede the earlier
|
|
@@ -2893,6 +2985,8 @@ export class AcpAgent implements Agent {
|
|
|
2893
2985
|
if (!record.backgroundBusy) record.busy = false;
|
|
2894
2986
|
clearPromptWatchdog(waiter);
|
|
2895
2987
|
record.cancelRequested = false;
|
|
2988
|
+
waiter.cancelBeforeAdmission = false;
|
|
2989
|
+
waiter.cancelBeforeAdmissionScope = undefined;
|
|
2896
2990
|
waiter.settled = true;
|
|
2897
2991
|
this.#fenceRetiredPromptAcknowledgement(id, waiter);
|
|
2898
2992
|
waiter.deferredFrames.length = 0;
|
|
@@ -4144,6 +4238,7 @@ export class AcpAgent implements Agent {
|
|
|
4144
4238
|
typeof event?.type === "string" ? event.type : undefined,
|
|
4145
4239
|
typeof event?.toolCallId === "string" ? event.toolCallId : undefined,
|
|
4146
4240
|
frameMessageRole(event),
|
|
4241
|
+
frameMessageHasToolCall(event),
|
|
4147
4242
|
);
|
|
4148
4243
|
}
|
|
4149
4244
|
|
|
@@ -4605,6 +4700,24 @@ export class AcpAgent implements Agent {
|
|
|
4605
4700
|
}
|
|
4606
4701
|
if (toolCallId && event.type === "tool_execution_end") record.toolArgs.delete(toolCallId);
|
|
4607
4702
|
if (event.type === "agent_start") {
|
|
4703
|
+
if (
|
|
4704
|
+
promptOwner?.cancelBeforeAdmission &&
|
|
4705
|
+
record.cancelRequested &&
|
|
4706
|
+
!promptOwner.cancelAcknowledged &&
|
|
4707
|
+
!promptOwner.cancelAfterStartAbortIssued
|
|
4708
|
+
) {
|
|
4709
|
+
promptOwner.cancelAfterStartAbortIssued = true;
|
|
4710
|
+
const scope = promptOwner.cancelBeforeAdmissionScope ?? "turn";
|
|
4711
|
+
void this.cancel({
|
|
4712
|
+
sessionId: id,
|
|
4713
|
+
_meta: { gjc: { abortScope: scope } },
|
|
4714
|
+
} as CancelNotification).catch(error =>
|
|
4715
|
+
logger.warn("ACP could not retry a cancel after prompt start", {
|
|
4716
|
+
sessionId: id,
|
|
4717
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4718
|
+
}),
|
|
4719
|
+
);
|
|
4720
|
+
}
|
|
4608
4721
|
await this.#publishSessionUpdate(
|
|
4609
4722
|
id,
|
|
4610
4723
|
{
|
|
@@ -4874,6 +4987,8 @@ export class AcpAgent implements Agent {
|
|
|
4874
4987
|
record.activePrompt = undefined;
|
|
4875
4988
|
clearPromptWatchdog(waiter);
|
|
4876
4989
|
waiter.settled = true;
|
|
4990
|
+
waiter.cancelBeforeAdmission = false;
|
|
4991
|
+
waiter.cancelBeforeAdmissionScope = undefined;
|
|
4877
4992
|
const { outcome } = waiter.terminal;
|
|
4878
4993
|
this.#rememberSettledPromptCorrelation(id, record, waiter.correlation);
|
|
4879
4994
|
if (outcome.kind === "stopped") {
|
|
@@ -4885,10 +5000,11 @@ export class AcpAgent implements Agent {
|
|
|
4885
5000
|
// connection watched start. Upgrade it from the frames this prompt actually owned, never
|
|
4886
5001
|
// downgrade a terminal that already reported `post_start`, and carry the whole
|
|
4887
5002
|
// classification on the rejection so the first-turn retry gate can read it (review P1).
|
|
4888
|
-
const failure =
|
|
5003
|
+
const failure = (
|
|
4889
5004
|
outcome.phase === "post_start"
|
|
4890
5005
|
? outcome
|
|
4891
|
-
:
|
|
5006
|
+
: rephaseFailedOutcome(outcome, { hasActivity: waiter.observedTurnActivity })
|
|
5007
|
+
) as SdkPromptFailedOutcome;
|
|
4892
5008
|
waiter.reject(new AcpPromptFailureError(failure));
|
|
4893
5009
|
}
|
|
4894
5010
|
|
|
@@ -134,7 +134,9 @@ export class SSHCommandController {
|
|
|
134
134
|
this.ctx.showError("Missing value for --port.");
|
|
135
135
|
return;
|
|
136
136
|
}
|
|
137
|
-
|
|
137
|
+
// Same rule as the ACP `/ssh add` parser: `Number.parseInt` accepts
|
|
138
|
+
// trailing garbage ("22oops" -> 22), which would save a typo as a port.
|
|
139
|
+
const parsed = /^\d+$/.test(value) ? Number.parseInt(value, 10) : Number.NaN;
|
|
138
140
|
if (Number.isNaN(parsed) || parsed < 1 || parsed > 65535) {
|
|
139
141
|
this.ctx.showError("Invalid --port value. Must be an integer between 1 and 65535.");
|
|
140
142
|
return;
|
|
@@ -565,6 +565,13 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
565
565
|
#suspendedActivityIndicator?: Loader;
|
|
566
566
|
#stopped = false;
|
|
567
567
|
#initPromise?: Promise<void>;
|
|
568
|
+
// Until startup's first paint renders the session transcript, chatContainer holds
|
|
569
|
+
// only pre-paint notices. A transcript rebuild before then (e.g. an extension
|
|
570
|
+
// session_start message with display:true) would wipe those notices and leave
|
|
571
|
+
// session components that the first paint preserves and renders again, so every
|
|
572
|
+
// session-derived block would show twice. Rebuilds are no-ops until the first
|
|
573
|
+
// paint, which renders the full session anyway.
|
|
574
|
+
#initialTranscriptPainted = false;
|
|
568
575
|
#stopListeners = new Set<() => void>();
|
|
569
576
|
#eventBus?: EventBus;
|
|
570
577
|
#eventBusUnsubscribers: Array<() => void> = [];
|
|
@@ -1561,6 +1568,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1561
1568
|
}
|
|
1562
1569
|
|
|
1563
1570
|
rebuildChatFromMessages(policy: TranscriptRebuildPolicy): void {
|
|
1571
|
+
if (!this.#initialTranscriptPainted) return;
|
|
1564
1572
|
prepareTranscriptRebuild(this.ui, policy);
|
|
1565
1573
|
this.resetAssistantTextPresentation();
|
|
1566
1574
|
this.chatContainer.clear();
|
|
@@ -2184,11 +2192,13 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2184
2192
|
prebuiltContext?: SessionContext,
|
|
2185
2193
|
options?: { preserveExistingChat?: boolean },
|
|
2186
2194
|
): void {
|
|
2195
|
+
if (!this.#initialTranscriptPainted) return;
|
|
2187
2196
|
prepareTranscriptRebuild(this.ui, policy);
|
|
2188
2197
|
this.#uiHelpers.renderInitialMessages(prebuiltContext, options);
|
|
2189
2198
|
}
|
|
2190
2199
|
renderInitialMessages(prebuiltContext?: SessionContext, options?: { preserveExistingChat?: boolean }): void {
|
|
2191
2200
|
this.#uiHelpers.renderInitialMessages(prebuiltContext, options);
|
|
2201
|
+
this.#initialTranscriptPainted = true;
|
|
2192
2202
|
}
|
|
2193
2203
|
|
|
2194
2204
|
getUserMessageText(message: Message): string {
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import type { AgentMessage } from "@gajae-code/agent-core";
|
|
2
2
|
import type { CompactionSettings } from "@gajae-code/agent-core/compaction";
|
|
3
|
-
import {
|
|
4
|
-
effectiveReserveTokens,
|
|
5
|
-
estimateMessageTokensHeuristic,
|
|
6
|
-
resolveThresholdTokens,
|
|
7
|
-
} from "@gajae-code/agent-core/compaction";
|
|
3
|
+
import { effectiveReserveTokens, estimateMessageTokensHeuristic } from "@gajae-code/agent-core/compaction";
|
|
8
4
|
import type { Model } from "@gajae-code/ai/core";
|
|
9
5
|
import { formatNumber } from "@gajae-code/utils";
|
|
10
6
|
import type { AgentSession } from "../../session/agent-session";
|
|
@@ -135,7 +131,7 @@ export function computeContextBreakdown(
|
|
|
135
131
|
if (contextWindow > 0) {
|
|
136
132
|
const compactionSettings = session.settings.getGroup("compaction") as CompactionSettings;
|
|
137
133
|
if (compactionSettings.enabled && compactionSettings.strategy !== "off") {
|
|
138
|
-
const threshold =
|
|
134
|
+
const threshold = session.getAutoCompactionThresholdTokens(tokensForFreeSpace);
|
|
139
135
|
autoCompactBufferTokens = Math.max(0, contextWindow - threshold);
|
|
140
136
|
} else {
|
|
141
137
|
autoCompactBufferTokens = 0;
|
|
@@ -2,18 +2,12 @@ Launches subagents to parallelize workflows.
|
|
|
2
2
|
|
|
3
3
|
- Results are delivered automatically when complete.
|
|
4
4
|
- The tool result lists the assigned task ids (e.g. `0-AuthLoader`) — those are the live agent ids.
|
|
5
|
-
|
|
6
|
-
- Coordinate with running tasks via `irc` using those ids. `subagent` cancel terminates a task and **cannot carry a message**.
|
|
7
|
-
{{else}}
|
|
5
|
+
- Coordinate with running tasks via `irc` when available, using those ids. `subagent` cancel terminates a task and **cannot carry a message**.
|
|
8
6
|
- Use `subagent` action `inspect` or `list` to snapshot manager state.
|
|
9
|
-
{{/if}}
|
|
10
7
|
- To wait or cancel, use the `subagent` tool; its await/cancel doctrine is authoritative.
|
|
11
8
|
|
|
12
|
-
{{#if ircEnabled}}
|
|
13
|
-
Subagents have no conversation history, but they can reach you and their siblings live via the `irc` tool. Front-load every fact, file path, and direction they need in {{#if contextEnabled}}`context` or `assignment`{{else}}each `assignment`{{/if}}.
|
|
14
|
-
{{else}}
|
|
15
9
|
Subagents have no conversation history. Every fact, file path, and direction they need MUST be explicit in {{#if contextEnabled}}`context` or `assignment`{{else}}each `assignment`{{/if}}.
|
|
16
|
-
|
|
10
|
+
Use `irc` for live coordination when it is available.
|
|
17
11
|
|
|
18
12
|
<parameters>
|
|
19
13
|
- `agent`: agent type for all tasks
|
|
@@ -36,27 +30,16 @@ Subagents have no conversation history. Every fact, file path, and direction the
|
|
|
36
30
|
- HARD runtime gate: calls with more than 4 tasks are rejected before any child launches unless `spawnPlan` is complete.
|
|
37
31
|
- NEVER assign tasks to run project-wide build/test/lint. Caller verifies after the batch.
|
|
38
32
|
- **Subagents do not verify, lint, or format.** Every assignment MUST instruct the subagent to skip all gates and formatters. You run them once at the end across the union of changed files — avoids redundant runs and racing formatter passes.
|
|
39
|
-
|
|
40
|
-
- Each task: ≤3–5 explicit files. Overlapping file sets are tolerable when peers can coordinate via `irc`, but still fan out to a cluster when the scopes are cleanly separable.
|
|
41
|
-
- No globs, no "update all", no package-wide scope.
|
|
42
|
-
{{else}}
|
|
43
|
-
- Each task: ≤3–5 explicit files. No globs, no "update all", no package-wide scope. Fan out to a cluster instead.
|
|
44
|
-
{{/if}}
|
|
33
|
+
- Each task: ≤3–5 explicit files. No globs, no "update all", no package-wide scope. Use `irc` to coordinate overlapping work when it is available; otherwise keep file sets independent.
|
|
45
34
|
- Pass large payloads via `local://<path>` URIs, not inline.
|
|
46
35
|
{{#if contextEnabled}}- Put shared constraints in `context` once; do not duplicate across assignments.{{/if}}
|
|
47
36
|
- Prefer agents that investigate **and** edit in one pass; only spin a read-only discovery step when affected files are genuinely unknown.
|
|
48
37
|
</rules>
|
|
49
38
|
|
|
50
39
|
<parallelization>
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Parallel when tasks touch disjoint files, are independent refactors/tests, or only need occasional clarification that can be resolved peer-to-peer.
|
|
55
|
-
{{else}}
|
|
56
|
-
Test: can task B run correctly without seeing A's output? If no, sequence A → B.
|
|
57
|
-
Sequential when one task produces a contract (types, API, schema, core module) the other consumes.
|
|
58
|
-
Parallel when tasks touch disjoint files or are independent refactors/tests.
|
|
59
|
-
{{/if}}
|
|
40
|
+
Test: can task B run correctly without seeing A's output? If no, sequence A → B, unless `irc` is available and A can answer a small clarification.
|
|
41
|
+
Still sequence when one task produces a large, evolving contract (generated types, schema migration, core module API); live coordination is not a substitute for a finished contract.
|
|
42
|
+
Parallel when tasks touch disjoint files, are independent refactors/tests, or need only occasional peer clarification.
|
|
60
43
|
</parallelization>
|
|
61
44
|
|
|
62
45
|
{{#if contextEnabled}}
|
|
@@ -77,10 +60,8 @@ Parallel when tasks touch disjoint files or are independent refactors/tests.
|
|
|
77
60
|
{{#if spawningDisabled}}
|
|
78
61
|
Agent spawning is disabled for this context.
|
|
79
62
|
{{else}}
|
|
80
|
-
{{#list agents join="
|
|
81
|
-
|
|
82
|
-
{{description}}
|
|
83
|
-
{{/list}}
|
|
63
|
+
Bundled role names: {{#list agents join=", "}}{{name}}{{/list}}. A configured agent may override a bundled role name and takes precedence.
|
|
64
|
+
Other configured agents (project, user, plugin) may also be available; calling with an unknown `agent` lists the agents callable in this session.
|
|
84
65
|
{{/if}}
|
|
85
66
|
</agents>
|
|
86
67
|
|
package/src/sdk/acp/adapter.ts
CHANGED
|
@@ -112,6 +112,16 @@ function credentialFreeLifecycleResult(value: unknown): unknown {
|
|
|
112
112
|
return output;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
const ACP_MCP_FAILURE_MESSAGE_MAX_LENGTH = 240;
|
|
116
|
+
|
|
117
|
+
function credentialFreeLifecycleMessage(error: unknown): string {
|
|
118
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
119
|
+
return message
|
|
120
|
+
.replace(/\b(?:endpoint|token|url)\s*[:=]\s*(?:"[^"]*"|'[^']*'|\S+)/gi, "[redacted]")
|
|
121
|
+
.replace(/https?:\/\/[^\s)]+/gi, "[redacted-url]")
|
|
122
|
+
.slice(0, ACP_MCP_FAILURE_MESSAGE_MAX_LENGTH);
|
|
123
|
+
}
|
|
124
|
+
|
|
115
125
|
/**
|
|
116
126
|
* Lifecycle failures the ACP MCP launch wrapper must report verbatim. Everything else
|
|
117
127
|
* is re-attributed to the configured MCP servers, which is the useful answer for a
|
|
@@ -135,22 +145,47 @@ const ACP_MCP_PRESERVED_LAUNCH_CODES = new Set([
|
|
|
135
145
|
"worktree_in_use",
|
|
136
146
|
]);
|
|
137
147
|
|
|
148
|
+
/**
|
|
149
|
+
* SDK client transport failures happen before the lifecycle request reaches any MCP server and
|
|
150
|
+
* must retain their original retry semantics. The `transport` origin marker is required because
|
|
151
|
+
* broker ERROR frames are also represented as SdkClientError instances with these same codes.
|
|
152
|
+
*/
|
|
153
|
+
const ACP_MCP_PRESERVED_TRANSPORT_CODES = new Set(["connection_closed", "unavailable", "timeout"]);
|
|
154
|
+
|
|
138
155
|
/**
|
|
139
156
|
* The error an ACP session launch must throw once a lifecycle request that carried MCP
|
|
140
|
-
* servers has failed.
|
|
157
|
+
* servers has failed. Broker transport failures from the SDK client are preserved verbatim;
|
|
158
|
+
* other failures keep the adapter's `unavailable` code while carrying a bounded, credential-free
|
|
159
|
+
* copy of the original code and message as their cause diagnostic.
|
|
141
160
|
*/
|
|
142
161
|
export function acpMcpLaunchFailure(error: unknown, mcpServers: SessionLifecycleMcpServer[]): unknown {
|
|
143
162
|
const code =
|
|
144
163
|
typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
|
|
145
164
|
? error.code
|
|
146
165
|
: undefined;
|
|
147
|
-
if (
|
|
166
|
+
if (
|
|
167
|
+
mcpServers.length === 0 ||
|
|
168
|
+
(code !== undefined && ACP_MCP_PRESERVED_LAUNCH_CODES.has(code)) ||
|
|
169
|
+
(error instanceof SdkClientError &&
|
|
170
|
+
error.transport === true &&
|
|
171
|
+
code !== undefined &&
|
|
172
|
+
ACP_MCP_PRESERVED_TRANSPORT_CODES.has(code))
|
|
173
|
+
)
|
|
174
|
+
return error;
|
|
148
175
|
const names = mcpServers
|
|
149
176
|
.slice(0, 8)
|
|
150
177
|
.map(server => server.name)
|
|
151
178
|
.join(", ");
|
|
152
179
|
const suffix = mcpServers.length > 8 ? `, and ${mcpServers.length - 8} more` : "";
|
|
153
|
-
|
|
180
|
+
const originalCode = code ?? "unknown";
|
|
181
|
+
const originalMessage = credentialFreeLifecycleMessage(error);
|
|
182
|
+
const diagnostic = originalMessage ? ` [${originalCode}: ${originalMessage}]` : ` [${originalCode}]`;
|
|
183
|
+
const wrapped = new AcpSdkAdapterError(
|
|
184
|
+
"unavailable",
|
|
185
|
+
`MCP server request failed to start (${names}${suffix}): broker lifecycle failed${diagnostic}`,
|
|
186
|
+
);
|
|
187
|
+
Object.assign(wrapped, { cause: error });
|
|
188
|
+
return wrapped;
|
|
154
189
|
}
|
|
155
190
|
|
|
156
191
|
export type AcpReconnectFailedHandler = (error: SdkClientError) => void;
|
|
@@ -63,6 +63,41 @@ function typedResult(response: unknown): BrokerRestartResult {
|
|
|
63
63
|
return { ok: false, error: { code: "protocol_error", message: "broker returned a malformed restart response" } };
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Sends one restart verb. SdkClient rejects an `{ok:false}` response as an
|
|
68
|
+
* SdkClientError, but that refusal is the broker's own typed decision, so it is
|
|
69
|
+
* returned as the result. Only a request that got no response for its id (its
|
|
70
|
+
* outcome is unknown) propagates as an error.
|
|
71
|
+
*/
|
|
72
|
+
async function restartRequest(
|
|
73
|
+
client: SdkClient,
|
|
74
|
+
operation: string,
|
|
75
|
+
input: Record<string, unknown>,
|
|
76
|
+
): Promise<BrokerRestartResult> {
|
|
77
|
+
let requestFrameId: unknown;
|
|
78
|
+
let refused = false;
|
|
79
|
+
const unsubscribe = client.onFrame(frame => {
|
|
80
|
+
const id = typeof frame.id === "string" ? frame.id : frame.requestId;
|
|
81
|
+
if (requestFrameId !== undefined && id === requestFrameId && (frame.ok === false || frame.status === "error"))
|
|
82
|
+
refused = true;
|
|
83
|
+
});
|
|
84
|
+
try {
|
|
85
|
+
return typedResult(
|
|
86
|
+
await client.global(operation, input, {
|
|
87
|
+
onDispatch: ({ frame }) => {
|
|
88
|
+
requestFrameId = frame.id;
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (refused && error instanceof SdkClientError)
|
|
94
|
+
return { ok: false, error: { code: error.code, message: error.message } };
|
|
95
|
+
throw error;
|
|
96
|
+
} finally {
|
|
97
|
+
unsubscribe();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
66
101
|
export async function prepareDoctorBrokerRestart(
|
|
67
102
|
client: SdkClient,
|
|
68
103
|
discovery: BrokerDiscovery,
|
|
@@ -74,7 +109,7 @@ export async function prepareDoctorBrokerRestart(
|
|
|
74
109
|
deadlineAt: Date.now() + (options.deadlineMs ?? 30_000),
|
|
75
110
|
drain: options.drain,
|
|
76
111
|
};
|
|
77
|
-
return
|
|
112
|
+
return await restartRequest(client, "broker.prepare_restart", { ...input });
|
|
78
113
|
}
|
|
79
114
|
|
|
80
115
|
export async function commitDoctorBrokerRestart(
|
|
@@ -90,11 +125,11 @@ export async function commitDoctorBrokerRestart(
|
|
|
90
125
|
lease: prepared.lease,
|
|
91
126
|
occupancyEpoch: prepared.occupancyEpoch,
|
|
92
127
|
};
|
|
93
|
-
return
|
|
128
|
+
return await restartRequest(client, "broker.commit_restart", { ...input });
|
|
94
129
|
}
|
|
95
130
|
|
|
96
131
|
export async function cancelDoctorBrokerRestart(client: SdkClient, requestId: string): Promise<BrokerRestartResult> {
|
|
97
|
-
return
|
|
132
|
+
return await restartRequest(client, "broker.cancel_restart", { requestId });
|
|
98
133
|
}
|
|
99
134
|
|
|
100
135
|
const OLD_OWNER_EXIT_POLL_MS = 50;
|