@gajae-code/coding-agent 0.12.18 → 0.12.20
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 -9
- package/dist/types/commands/gc.d.ts +4 -0
- package/dist/types/commands/sdk.d.ts +66 -0
- package/dist/types/config/settings-schema.d.ts +34 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +97 -1
- package/dist/types/modes/acp/acp-agent.d.ts +33 -1
- package/dist/types/modes/components/bash-execution.d.ts +7 -0
- package/dist/types/modes/components/eval-execution.d.ts +7 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +10 -1
- package/dist/types/sdk/acp/adapter.d.ts +9 -17
- package/dist/types/sdk/acp/index.d.ts +1 -1
- package/dist/types/sdk/broker/broker.d.ts +93 -1
- package/dist/types/sdk/broker/ensure.d.ts +17 -0
- package/dist/types/sdk/broker/index.d.ts +1 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +10 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +81 -2
- package/dist/types/sdk/broker/startup-budget.d.ts +44 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +7 -1
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +11 -1
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +20 -9
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -6
- package/dist/types/sdk/bus/slack-daemon.d.ts +2 -2
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -2
- package/dist/types/sdk/host/query/revision-store.d.ts +5 -0
- package/dist/types/sdk/host/session-runtime.d.ts +4 -0
- package/dist/types/sdk/lifecycle-session.d.ts +13 -2
- package/dist/types/sdk/prompt-failure.d.ts +13 -0
- package/dist/types/sdk/prompt-watchdog.d.ts +82 -0
- package/dist/types/sdk/session-reconnect.d.ts +20 -0
- package/dist/types/sdk/startup-capability.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +14 -4
- package/dist/types/session/blob-store.d.ts +32 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +40 -2
- package/dist/types/session/internal/session-memory-sidecar.d.ts +881 -0
- package/dist/types/session/session-manager.d.ts +301 -27
- package/dist/types/session/session-storage.d.ts +170 -0
- package/dist/types/task/executor.d.ts +2 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tui/output-block.d.ts +6 -0
- package/package.json +8 -8
- package/src/cli/args.ts +75 -34
- package/src/commands/acp.ts +7 -1
- package/src/commands/gc.ts +31 -2
- package/src/commands/sdk.ts +141 -7
- package/src/config/settings-schema.ts +43 -0
- package/src/coordinator-mcp/server.ts +6 -1
- package/src/export/html/index.ts +83 -39
- package/src/gjc-runtime/gc-runtime.ts +1283 -4
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/internal-urls/local-protocol.ts +14 -0
- package/src/main.ts +40 -9
- package/src/modes/acp/acp-agent.ts +564 -37
- package/src/modes/acp/terminal-auth.ts +2 -2
- package/src/modes/components/bash-execution.ts +14 -0
- package/src/modes/components/eval-execution.ts +14 -0
- package/src/modes/controllers/command-controller.ts +63 -8
- package/src/modes/utils/ui-helpers.ts +81 -11
- package/src/sdk/acp/adapter.ts +46 -45
- package/src/sdk/acp/index.ts +1 -1
- package/src/sdk/broker/broker.ts +392 -11
- package/src/sdk/broker/ensure.ts +131 -61
- package/src/sdk/broker/index.ts +1 -0
- package/src/sdk/broker/lifecycle-ledger.ts +59 -5
- package/src/sdk/broker/lifecycle.ts +278 -63
- package/src/sdk/broker/startup-budget.ts +103 -0
- package/src/sdk/bus/chat-daemon-control.ts +9 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +565 -29
- package/src/sdk/bus/conversation-store.ts +1 -1
- package/src/sdk/bus/discord-daemon.ts +2 -1
- package/src/sdk/bus/discord-live-provider.ts +34 -6
- package/src/sdk/bus/index.ts +118 -20
- package/src/sdk/bus/kind-aware-reconciliation.ts +2 -1
- package/src/sdk/bus/prompt-reconciliation.ts +4 -9
- package/src/sdk/bus/slack-daemon.ts +91 -18
- package/src/sdk/bus/telegram-daemon-contract.ts +4 -2
- package/src/sdk/bus/topic-registry.ts +7 -1
- package/src/sdk/cli/session-cli.ts +8 -1
- package/src/sdk/host/query/handlers.ts +1 -1
- package/src/sdk/host/query/revision-store.ts +48 -12
- package/src/sdk/host/session-runtime.test.ts +50 -0
- package/src/sdk/host/session-runtime.ts +12 -2
- package/src/sdk/lifecycle-session.ts +26 -3
- package/src/sdk/mcp/server.ts +6 -1
- package/src/sdk/prompt-failure.ts +42 -0
- package/src/sdk/prompt-watchdog.ts +162 -0
- package/src/sdk/session-reconnect.ts +43 -0
- package/src/sdk/startup-capability.ts +13 -4
- package/src/session/agent-session.ts +525 -222
- package/src/session/blob-store.ts +89 -0
- package/src/session/internal/managed-session-scope.ts +36 -26
- package/src/session/internal/managed-session-storage.ts +303 -32
- package/src/session/internal/session-memory-sidecar.ts +2160 -0
- package/src/session/session-manager.ts +7027 -394
- package/src/session/session-storage.ts +1134 -11
- package/src/task/executor.ts +17 -4
- package/src/tools/bash.ts +109 -7
- package/src/tools/find.ts +55 -26
- package/src/tools/todo-contract.ts +40 -8
- package/src/tui/output-block.ts +11 -1
|
@@ -23,11 +23,11 @@ export function prepareAcpTerminalAuthArgs(rawArgs: readonly string[]): AcpTermi
|
|
|
23
23
|
const args: string[] = [];
|
|
24
24
|
for (let i = 0; i < withoutAuthFlag.length; i++) {
|
|
25
25
|
const arg = withoutAuthFlag[i];
|
|
26
|
-
if (arg === "--mode") {
|
|
26
|
+
if (arg === "--mode" && withoutAuthFlag[i + 1] === "acp") {
|
|
27
27
|
i++;
|
|
28
28
|
continue;
|
|
29
29
|
}
|
|
30
|
-
if (arg
|
|
30
|
+
if (arg === "--mode=acp") {
|
|
31
31
|
continue;
|
|
32
32
|
}
|
|
33
33
|
args.push(arg);
|
|
@@ -51,6 +51,7 @@ export class BashExecutionComponent extends Container {
|
|
|
51
51
|
#chunkGate = false;
|
|
52
52
|
#contentContainer: Container;
|
|
53
53
|
#headerText: Text;
|
|
54
|
+
#resultPersisted = false;
|
|
54
55
|
|
|
55
56
|
constructor(
|
|
56
57
|
private readonly command: string,
|
|
@@ -259,4 +260,17 @@ export class BashExecutionComponent extends Container {
|
|
|
259
260
|
getCommand(): string {
|
|
260
261
|
return this.command;
|
|
261
262
|
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Record that this execution's message reached session state, so a transcript
|
|
266
|
+
* rebuild renders it from the session instead of keeping this live block parked.
|
|
267
|
+
*/
|
|
268
|
+
markResultPersisted(): void {
|
|
269
|
+
this.#resultPersisted = true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Whether the session already holds this execution's message. */
|
|
273
|
+
hasPersistedResult(): boolean {
|
|
274
|
+
return this.#resultPersisted;
|
|
275
|
+
}
|
|
262
276
|
}
|
|
@@ -30,6 +30,7 @@ export class EvalExecutionComponent extends Container {
|
|
|
30
30
|
#expanded = false;
|
|
31
31
|
#contentContainer: Container;
|
|
32
32
|
#headerText: Text;
|
|
33
|
+
#resultPersisted = false;
|
|
33
34
|
|
|
34
35
|
#highlightLang(): "python" | "javascript" {
|
|
35
36
|
return this.language === "js" ? "javascript" : "python";
|
|
@@ -165,4 +166,17 @@ export class EvalExecutionComponent extends Container {
|
|
|
165
166
|
getCode(): string {
|
|
166
167
|
return this.code;
|
|
167
168
|
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Record that this execution's message reached session state, so a transcript
|
|
172
|
+
* rebuild renders it from the session instead of keeping this live block parked.
|
|
173
|
+
*/
|
|
174
|
+
markResultPersisted(): void {
|
|
175
|
+
this.#resultPersisted = true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Whether the session already holds this execution's message. */
|
|
179
|
+
hasPersistedResult(): boolean {
|
|
180
|
+
return this.#resultPersisted;
|
|
181
|
+
}
|
|
168
182
|
}
|
|
@@ -5,7 +5,7 @@ import { CompactionCancelledError, type CompactionOutcome } from "@gajae-code/ag
|
|
|
5
5
|
import { getEnvApiKey, type ToolCall, type UsageLimit, type UsageReport } from "@gajae-code/ai/core";
|
|
6
6
|
import type { ProviderDetails } from "@gajae-code/ai/provider-details";
|
|
7
7
|
import { type Keybinding, Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@gajae-code/tui";
|
|
8
|
-
import { formatDuration, Snowflake, setProjectDir } from "@gajae-code/utils";
|
|
8
|
+
import { formatBytes, formatDuration, Snowflake, setProjectDir } from "@gajae-code/utils";
|
|
9
9
|
import { resolveAppendOnlyMode } from "../../append-only-mode";
|
|
10
10
|
import { jobElapsedMs } from "../../async";
|
|
11
11
|
import { reset as resetCapabilities } from "../../capability";
|
|
@@ -39,7 +39,7 @@ import { getDisplayChangelogEntries } from "../../utils/changelog";
|
|
|
39
39
|
import { copyToClipboard } from "../../utils/clipboard";
|
|
40
40
|
import { openPath } from "../../utils/open";
|
|
41
41
|
import { setSessionTerminalTitle } from "../../utils/title-generator";
|
|
42
|
-
import { prepareTranscriptRebuild } from "../utils/ui-helpers";
|
|
42
|
+
import { addChatChild, prepareTranscriptRebuild, syncPendingExecutionComponents } from "../utils/ui-helpers";
|
|
43
43
|
|
|
44
44
|
type HindsightModule = typeof import("../../hindsight");
|
|
45
45
|
let hindsightModulePromise: Promise<HindsightModule> | undefined;
|
|
@@ -473,6 +473,45 @@ export class CommandController {
|
|
|
473
473
|
const settingLabel = setting === "auto" ? `${setting} (${provider ?? "?"})` : setting;
|
|
474
474
|
info += `${theme.fg("dim", "Append-Only:")} ${activeLabel} (setting: ${settingLabel})\n`;
|
|
475
475
|
}
|
|
476
|
+
if (stats.sessionMemory) {
|
|
477
|
+
const memory = stats.sessionMemory;
|
|
478
|
+
const retirement = memory.coldRetirementActive ? theme.fg("success", "active") : theme.fg("dim", "inactive");
|
|
479
|
+
info += `\n${theme.bold("Session Memory")}\n`;
|
|
480
|
+
info += `${theme.fg("dim", "Cold Retirement:")} ${retirement}\n`;
|
|
481
|
+
info += `${theme.fg("dim", "Hot Region:")} ${formatBytes(memory.hotRegionBytes)}\n`;
|
|
482
|
+
info += `${theme.fg("dim", "Metadata:")} ${formatBytes(memory.metaDescriptorBytes)}\n`;
|
|
483
|
+
info += `${theme.fg("dim", "Accounted:")} ${formatBytes(memory.totalAccountedBytes)}\n`;
|
|
484
|
+
if (memory.coldIndexBytes > 0 || memory.coldIndexBlockCacheBytes > 0 || memory.coldEntryCacheBytes > 0) {
|
|
485
|
+
info += `${theme.fg("dim", "Cold Index/Caches:")} ${formatBytes(memory.coldIndexBytes)} / ${formatBytes(memory.coldIndexBlockCacheBytes)} / ${formatBytes(memory.coldEntryCacheBytes)}\n`;
|
|
486
|
+
}
|
|
487
|
+
if (memory.lazyReopenAttempted) {
|
|
488
|
+
const reopen = memory.lazyReopenSucceeded ? theme.fg("success", "exact") : theme.fg("warning", "fallback");
|
|
489
|
+
info += `${theme.fg("dim", "Lazy Reopen:")} ${reopen}\n`;
|
|
490
|
+
}
|
|
491
|
+
const fallbackReason = memory.retirementFallbackReason ?? memory.lazyReopenFallbackReason;
|
|
492
|
+
if (fallbackReason) info += `${theme.fg("dim", "Fallback Reason:")} ${fallbackReason}\n`;
|
|
493
|
+
if (memory.coldEntriesRetired > 0 || memory.coldEntriesReloaded > 0) {
|
|
494
|
+
info += `${theme.fg("dim", "Cold Retired/Reloaded:")} ${memory.coldEntriesRetired}/${memory.coldEntriesReloaded}\n`;
|
|
495
|
+
}
|
|
496
|
+
if (memory.rangeReadCount > 0 || memory.rangeReadGenerationMismatchCount > 0) {
|
|
497
|
+
info += `${theme.fg("dim", "Cold Range Reads:")} ${memory.rangeReadCount} (${memory.rangeReadGenerationMismatchCount} mismatched)\n`;
|
|
498
|
+
}
|
|
499
|
+
if (memory.sidecarRebuildCount > 0 || memory.transcriptGeneration > 0) {
|
|
500
|
+
info += `${theme.fg("dim", "Sidecar Rebuilds:")} ${memory.sidecarRebuildCount} (gen ${memory.transcriptGeneration})\n`;
|
|
501
|
+
}
|
|
502
|
+
if (memory.coldMutationPromotions > 0 || memory.hotOverflowTransitions > 0) {
|
|
503
|
+
info += `${theme.fg("dim", "Cold Promotions/Overflows:")} ${memory.coldMutationPromotions}/${memory.hotOverflowTransitions}\n`;
|
|
504
|
+
}
|
|
505
|
+
if (memory.labelDiskFallbackCount > 0) {
|
|
506
|
+
info += `${theme.fg("dim", "Label Disk Reads:")} ${memory.labelDiskFallbackCount}\n`;
|
|
507
|
+
}
|
|
508
|
+
if (memory.shadowParityCheckCount > 0) {
|
|
509
|
+
info += `${theme.fg("dim", "Shadow Parity:")} ${memory.shadowParityMismatchCount}/${memory.shadowParityCheckCount} mismatches\n`;
|
|
510
|
+
}
|
|
511
|
+
if (memory.autoDisabledReason) {
|
|
512
|
+
info += `${theme.fg("dim", "Auto-Disabled:")} ${theme.fg("warning", memory.autoDisabledReason)}\n`;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
476
515
|
info += `${theme.bold("Tokens")}\n`;
|
|
477
516
|
info += `${theme.fg("dim", "Input:")} ${stats.tokens.input.toLocaleString()}\n`;
|
|
478
517
|
info += `${theme.fg("dim", "Output:")} ${stats.tokens.output.toLocaleString()}\n`;
|
|
@@ -1147,7 +1186,8 @@ export class CommandController {
|
|
|
1147
1186
|
|
|
1148
1187
|
async handleBashCommand(command: string, excludeFromContext = false): Promise<void> {
|
|
1149
1188
|
const isDeferred = this.ctx.session.isStreaming;
|
|
1150
|
-
|
|
1189
|
+
const component = new BashExecutionComponent(command, this.ctx.ui, excludeFromContext);
|
|
1190
|
+
this.ctx.bashComponent = component;
|
|
1151
1191
|
|
|
1152
1192
|
if (isDeferred) {
|
|
1153
1193
|
this.ctx.pendingMessagesContainer.addChild(this.ctx.bashComponent);
|
|
@@ -1165,7 +1205,9 @@ export class CommandController {
|
|
|
1165
1205
|
this.ctx.bashComponent.appendOutput(chunk);
|
|
1166
1206
|
}
|
|
1167
1207
|
},
|
|
1168
|
-
|
|
1208
|
+
// A transcript rebuild racing this call must drop the parked block once the
|
|
1209
|
+
// session owns its message, otherwise the rebuilt row is rendered twice.
|
|
1210
|
+
{ excludeFromContext, onPersisted: () => component.markResultPersisted() },
|
|
1169
1211
|
);
|
|
1170
1212
|
|
|
1171
1213
|
if (this.ctx.bashComponent) {
|
|
@@ -1182,8 +1224,18 @@ export class CommandController {
|
|
|
1182
1224
|
this.ctx.showError(`Bash command failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1183
1225
|
}
|
|
1184
1226
|
const bashComponent = this.ctx.bashComponent;
|
|
1185
|
-
if (isDeferred && bashComponent
|
|
1186
|
-
this
|
|
1227
|
+
if (isDeferred && bashComponent) {
|
|
1228
|
+
// Parentage is the container's answer, never this bookkeeping's: `/clear`,
|
|
1229
|
+
// `/context-clear`, extension redraws and the selector all call
|
|
1230
|
+
// `pendingMessagesContainer.clear()`, which disposes and evicts the parked
|
|
1231
|
+
// component while leaving it listed in `pendingBashComponents`. Ask the
|
|
1232
|
+
// container whether it still holds a live child before moving anything;
|
|
1233
|
+
// a disposed block is terminal and must never reach a fresh transcript.
|
|
1234
|
+
if (this.ctx.pendingMessagesContainer.hasLiveChild(bashComponent)) {
|
|
1235
|
+
this.ctx.pendingMessagesContainer.detachChild(bashComponent);
|
|
1236
|
+
addChatChild(this.ctx, bashComponent);
|
|
1237
|
+
}
|
|
1238
|
+
syncPendingExecutionComponents(this.ctx);
|
|
1187
1239
|
}
|
|
1188
1240
|
|
|
1189
1241
|
this.ctx.bashComponent = undefined;
|
|
@@ -1192,7 +1244,8 @@ export class CommandController {
|
|
|
1192
1244
|
|
|
1193
1245
|
async handlePythonCommand(code: string, excludeFromContext = false): Promise<void> {
|
|
1194
1246
|
const isDeferred = this.ctx.session.isStreaming;
|
|
1195
|
-
|
|
1247
|
+
const component = new EvalExecutionComponent(code, this.ctx.ui, excludeFromContext);
|
|
1248
|
+
this.ctx.pythonComponent = component;
|
|
1196
1249
|
|
|
1197
1250
|
if (isDeferred) {
|
|
1198
1251
|
this.ctx.pendingMessagesContainer.addChild(this.ctx.pythonComponent);
|
|
@@ -1210,7 +1263,9 @@ export class CommandController {
|
|
|
1210
1263
|
this.ctx.pythonComponent.appendOutput(chunk);
|
|
1211
1264
|
}
|
|
1212
1265
|
},
|
|
1213
|
-
|
|
1266
|
+
// A transcript rebuild racing this call must drop the parked block once the
|
|
1267
|
+
// session owns its message, otherwise the rebuilt row is rendered twice.
|
|
1268
|
+
{ excludeFromContext, onPersisted: () => component.markResultPersisted() },
|
|
1214
1269
|
);
|
|
1215
1270
|
|
|
1216
1271
|
if (this.ctx.pythonComponent) {
|
|
@@ -281,6 +281,31 @@ export function addChatChild(ctx: InteractiveModeContext, component: Component):
|
|
|
281
281
|
trimChatChildren(ctx);
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Parked `!`/`$` execution components are listed in `pendingBashComponents` /
|
|
286
|
+
* `pendingPythonComponents`, but `pendingMessagesContainer` is the only
|
|
287
|
+
* authority on parentage: `/clear`, `/context-clear`, extension redraws and the
|
|
288
|
+
* selector all call `pendingMessagesContainer.clear()`, which disposes and
|
|
289
|
+
* evicts parked components without touching those arrays. Drop every entry the
|
|
290
|
+
* container no longer holds so nothing downstream can move a dead component.
|
|
291
|
+
*/
|
|
292
|
+
export function syncPendingExecutionComponents(ctx: InteractiveModeContext): void {
|
|
293
|
+
const container = ctx.pendingMessagesContainer;
|
|
294
|
+
ctx.pendingBashComponents = ctx.pendingBashComponents.filter(component => container.hasLiveChild(component));
|
|
295
|
+
ctx.pendingPythonComponents = ctx.pendingPythonComponents.filter(component => container.hasLiveChild(component));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Whether the session already owns this execution's message, i.e. the rebuilt
|
|
300
|
+
* transcript renders the block from session state. Set by the controller when
|
|
301
|
+
* `executeBash()` / `executePython()` reports the result as persisted.
|
|
302
|
+
*/
|
|
303
|
+
function hasPersistedExecutionResult(component: Component): boolean {
|
|
304
|
+
if (component instanceof BashExecutionComponent) return component.hasPersistedResult();
|
|
305
|
+
if (component instanceof EvalExecutionComponent) return component.hasPersistedResult();
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
|
|
284
309
|
export function trimChatChildren(ctx: InteractiveModeContext): void {
|
|
285
310
|
const children = ctx.chatContainer.children;
|
|
286
311
|
|
|
@@ -899,10 +924,24 @@ export class UiHelpers {
|
|
|
899
924
|
// This path is used to rebuild the visible chat transcript (e.g. after custom/debug UI).
|
|
900
925
|
// Clear existing rendered chat first to avoid duplicating the full session in the container.
|
|
901
926
|
const preservedChatChildren = options.preserveExistingChat ? this.ctx.chatContainer.children : undefined;
|
|
927
|
+
// A still-running deferred `!`/`$` block is the only rendering of output whose
|
|
928
|
+
// message has not been published to the session yet, so the rebuild must keep it
|
|
929
|
+
// parked instead of disposing it. Finished blocks are dropped here: the rebuilt
|
|
930
|
+
// transcript renders them from the session. A block whose result was persisted
|
|
931
|
+
// while its controller was still suspended is finished for this purpose — keeping
|
|
932
|
+
// it would render the same execution twice.
|
|
933
|
+
const runningExecutionComponents = this.#detachPendingMessages(
|
|
934
|
+
component =>
|
|
935
|
+
(component === this.ctx.bashComponent || component === this.ctx.pythonComponent) &&
|
|
936
|
+
!hasPersistedExecutionResult(component),
|
|
937
|
+
);
|
|
938
|
+
this.ctx.pendingBashComponents = this.ctx.pendingBashComponents.filter(component =>
|
|
939
|
+
runningExecutionComponents.includes(component),
|
|
940
|
+
);
|
|
941
|
+
this.ctx.pendingPythonComponents = this.ctx.pendingPythonComponents.filter(component =>
|
|
942
|
+
runningExecutionComponents.includes(component),
|
|
943
|
+
);
|
|
902
944
|
this.ctx.chatContainer.clear();
|
|
903
|
-
this.ctx.pendingMessagesContainer.clear();
|
|
904
|
-
this.ctx.pendingBashComponents = [];
|
|
905
|
-
this.ctx.pendingPythonComponents = [];
|
|
906
945
|
|
|
907
946
|
// Reuse a pre-built context when available (e.g. from navigateTree) to avoid a second O(N) walk.
|
|
908
947
|
const context = prebuiltContext ?? this.ctx.sessionManager.buildSessionContext();
|
|
@@ -923,6 +962,9 @@ export class UiHelpers {
|
|
|
923
962
|
const times = compactionCount === 1 ? "1 time" : `${compactionCount} times`;
|
|
924
963
|
this.ctx.showStatus(`Session compacted ${times}`);
|
|
925
964
|
}
|
|
965
|
+
for (const component of runningExecutionComponents) {
|
|
966
|
+
this.ctx.pendingMessagesContainer.addChild(component);
|
|
967
|
+
}
|
|
926
968
|
if (preservedChatChildren && preservedChatChildren.length > 0) {
|
|
927
969
|
for (const child of preservedChatChildren) {
|
|
928
970
|
addChatChild(this.ctx, child);
|
|
@@ -978,8 +1020,35 @@ export class UiHelpers {
|
|
|
978
1020
|
this.ctx.ui.requestRender();
|
|
979
1021
|
}
|
|
980
1022
|
|
|
1023
|
+
/**
|
|
1024
|
+
* Empty the pending container, disposing the queued-message chips but handing back
|
|
1025
|
+
* the parked `!`/`$` execution components matched by `retain`, in render order.
|
|
1026
|
+
*
|
|
1027
|
+
* `Container.clear()` disposes every child, which would tear down a running
|
|
1028
|
+
* execution block mid-flight; retained components are reused instances that the
|
|
1029
|
+
* caller re-attaches (pending area or chat transcript).
|
|
1030
|
+
*/
|
|
1031
|
+
#detachPendingMessages(retain: (component: Component) => boolean): Component[] {
|
|
1032
|
+
syncPendingExecutionComponents(this.ctx);
|
|
1033
|
+
const parked = new Set<Component>([...this.ctx.pendingBashComponents, ...this.ctx.pendingPythonComponents]);
|
|
1034
|
+
const retained: Component[] = [];
|
|
1035
|
+
for (const child of this.ctx.pendingMessagesContainer.children) {
|
|
1036
|
+
if (parked.has(child) && retain(child)) {
|
|
1037
|
+
retained.push(child);
|
|
1038
|
+
} else {
|
|
1039
|
+
child.dispose?.();
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
this.ctx.pendingMessagesContainer.detachAll();
|
|
1043
|
+
return retained;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
981
1046
|
updatePendingMessagesDisplay(): void {
|
|
982
|
-
|
|
1047
|
+
// Rebuild only the queued-message chips: parked execution components stay attached
|
|
1048
|
+
// so a mid-turn queue/dequeue event cannot dispose a streaming `!`/`$` block.
|
|
1049
|
+
for (const component of this.#detachPendingMessages(() => true)) {
|
|
1050
|
+
this.ctx.pendingMessagesContainer.addChild(component);
|
|
1051
|
+
}
|
|
983
1052
|
const queuedMessages = this.ctx.session.getQueuedMessages() as QueuedMessages;
|
|
984
1053
|
|
|
985
1054
|
const steeringMessages: Array<{ message: string; label: string }> = [];
|
|
@@ -1237,20 +1306,21 @@ export class UiHelpers {
|
|
|
1237
1306
|
}
|
|
1238
1307
|
}
|
|
1239
1308
|
|
|
1240
|
-
/** Move pending bash components from pending area to chat */
|
|
1309
|
+
/** Move pending bash/python components from the pending area to chat */
|
|
1241
1310
|
flushPendingBashComponents(): void {
|
|
1242
1311
|
// Move (detach, not dispose) the live execution components from the pending
|
|
1243
1312
|
// area into the chat transcript — they are reused instances, so a disposing
|
|
1244
|
-
// removeChild() would tear them down before re-adding.
|
|
1245
|
-
|
|
1313
|
+
// removeChild() would tear them down before re-adding. Walk the container so
|
|
1314
|
+
// the transcript keeps the order the pending area rendered, and so a
|
|
1315
|
+
// component the container no longer holds can never be re-parented.
|
|
1316
|
+
syncPendingExecutionComponents(this.ctx);
|
|
1317
|
+
const parked = new Set<Component>([...this.ctx.pendingBashComponents, ...this.ctx.pendingPythonComponents]);
|
|
1318
|
+
for (const component of [...this.ctx.pendingMessagesContainer.children]) {
|
|
1319
|
+
if (!parked.has(component)) continue;
|
|
1246
1320
|
this.ctx.pendingMessagesContainer.detachChild(component);
|
|
1247
1321
|
addChatChild(this.ctx, component);
|
|
1248
1322
|
}
|
|
1249
1323
|
this.ctx.pendingBashComponents = [];
|
|
1250
|
-
for (const component of this.ctx.pendingPythonComponents) {
|
|
1251
|
-
this.ctx.pendingMessagesContainer.detachChild(component);
|
|
1252
|
-
addChatChild(this.ctx, component);
|
|
1253
|
-
}
|
|
1254
1324
|
this.ctx.pendingPythonComponents = [];
|
|
1255
1325
|
}
|
|
1256
1326
|
|
package/src/sdk/acp/adapter.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import { SdkClient, SdkClientError, type
|
|
2
|
+
import { lifecycleRequestTimeoutMs } from "../broker/startup-budget";
|
|
3
|
+
import { SdkClient, SdkClientError, type SdkFrame } from "../client";
|
|
4
4
|
import { assertReverseResponseFrame, ReverseLeaseError } from "../host/reverse-leases";
|
|
5
5
|
import { validateAdapterControl, validateAdapterSecretFields } from "../protocol/adapter-validation";
|
|
6
|
-
|
|
7
6
|
import { OPERATIONS } from "../protocol/operation-registry";
|
|
7
|
+
import { ACP_SESSION_RECONNECT } from "../session-reconnect";
|
|
8
|
+
import type { SessionLifecycleMcpServer } from "./mcp";
|
|
8
9
|
|
|
9
10
|
type JsonObject = Record<string, unknown>;
|
|
10
11
|
|
|
@@ -40,6 +41,40 @@ export class AcpSdkAdapterError extends Error {
|
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Lifecycle failures the ACP MCP launch wrapper must report verbatim. Everything else
|
|
46
|
+
* is re-attributed to the configured MCP servers, which is the useful answer for a
|
|
47
|
+
* launch that actually reached them — but a startup the broker ended before admission
|
|
48
|
+
* never opened an MCP handshake, so blaming the servers would hide both the real
|
|
49
|
+
* authority reason and the fact that the request is safely retryable.
|
|
50
|
+
*/
|
|
51
|
+
const ACP_MCP_PRESERVED_LAUNCH_CODES = new Set([
|
|
52
|
+
"invalid_input",
|
|
53
|
+
"authentication_failed",
|
|
54
|
+
"unknown_model_profile",
|
|
55
|
+
"model_profile_registry_error",
|
|
56
|
+
"startup_admission_refused",
|
|
57
|
+
"startup_admission_timeout",
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The error an ACP session launch must throw once a lifecycle request that carried MCP
|
|
62
|
+
* servers has failed.
|
|
63
|
+
*/
|
|
64
|
+
export function acpMcpLaunchFailure(error: unknown, mcpServers: SessionLifecycleMcpServer[]): unknown {
|
|
65
|
+
const code =
|
|
66
|
+
typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
|
|
67
|
+
? error.code
|
|
68
|
+
: undefined;
|
|
69
|
+
if (mcpServers.length === 0 || (code !== undefined && ACP_MCP_PRESERVED_LAUNCH_CODES.has(code))) return error;
|
|
70
|
+
const names = mcpServers
|
|
71
|
+
.slice(0, 8)
|
|
72
|
+
.map(server => server.name)
|
|
73
|
+
.join(", ");
|
|
74
|
+
const suffix = mcpServers.length > 8 ? `, and ${mcpServers.length - 8} more` : "";
|
|
75
|
+
return new AcpSdkAdapterError("unavailable", `MCP server request failed to start (${names}${suffix}).`);
|
|
76
|
+
}
|
|
77
|
+
|
|
43
78
|
export type AcpReconnectFailedHandler = (error: SdkClientError) => void;
|
|
44
79
|
export type AcpFrameHandler = (frame: SdkFrame) => void;
|
|
45
80
|
type ReverseRequest = {
|
|
@@ -48,43 +83,7 @@ type ReverseRequest = {
|
|
|
48
83
|
cancelTimer?: NodeJS.Timeout;
|
|
49
84
|
};
|
|
50
85
|
|
|
51
|
-
|
|
52
|
-
Pick<SdkClientOptions, "reconnectAttempts" | "reconnectBackoffMs" | "reconnectMaxBackoffMs">
|
|
53
|
-
>;
|
|
54
|
-
|
|
55
|
-
const ACP_RECONNECT_BACKOFF_MS = 250;
|
|
56
|
-
const ACP_RECONNECT_MAX_BACKOFF_MS = 2_000;
|
|
57
|
-
/** Cover twice the host heartbeat TTL so a reaped stall still has room to recover. */
|
|
58
|
-
const ACP_RECONNECT_BUDGET_MS = 2 * HEARTBEAT_TTL_MS;
|
|
59
|
-
|
|
60
|
-
function attemptsCovering(budgetMs: number): number {
|
|
61
|
-
let elapsed = 0;
|
|
62
|
-
let attempts = 0;
|
|
63
|
-
while (elapsed <= budgetMs) {
|
|
64
|
-
elapsed += Math.min(ACP_RECONNECT_BACKOFF_MS * 2 ** attempts, ACP_RECONNECT_MAX_BACKOFF_MS);
|
|
65
|
-
attempts++;
|
|
66
|
-
}
|
|
67
|
-
return attempts;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Reconnect budget for long-lived ACP sessions.
|
|
72
|
-
*
|
|
73
|
-
* Invariant: the client's total reconnect budget must outlive the host heartbeat
|
|
74
|
-
* TTL ({@link HEARTBEAT_TTL_MS}). The SDK host drops a session whose client has
|
|
75
|
-
* not ponged within that TTL, so a client that gives up reconnecting sooner turns
|
|
76
|
-
* every stall the host reaps into a permanently lost session ("SDK WebSocket
|
|
77
|
-
* reconnect attempts exhausted"). The transport defaults (3 attempts at 25ms base
|
|
78
|
-
* = 175ms total) are correct only for one-shot request clients.
|
|
79
|
-
*
|
|
80
|
-
* Backoff ramps 250 -> 500 -> 1000 and then holds at the 2s cap, so individual
|
|
81
|
-
* sleeps stay short and recovery is prompt once the host answers again.
|
|
82
|
-
*/
|
|
83
|
-
export const ACP_SESSION_RECONNECT: AcpSessionReconnectOptions = {
|
|
84
|
-
reconnectAttempts: attemptsCovering(ACP_RECONNECT_BUDGET_MS),
|
|
85
|
-
reconnectBackoffMs: ACP_RECONNECT_BACKOFF_MS,
|
|
86
|
-
reconnectMaxBackoffMs: ACP_RECONNECT_MAX_BACKOFF_MS,
|
|
87
|
-
};
|
|
86
|
+
export { ACP_SESSION_RECONNECT };
|
|
88
87
|
|
|
89
88
|
const SESSION_GLOBALS: Record<string, string> = {
|
|
90
89
|
newSession: "session.create",
|
|
@@ -224,13 +223,15 @@ export class AcpSdkAdapter {
|
|
|
224
223
|
this.#assertGenericDisposition("global", operation);
|
|
225
224
|
if (isLifecycleOperation(operation) && !idempotencyKey)
|
|
226
225
|
throw new AcpSdkAdapterError("invalid_input", "idempotencyKey is required for lifecycle operations.");
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
226
|
+
// The broker may hold a startup in its admission queue before the readiness
|
|
227
|
+
// clock even starts, so the caller deadline covers the queue wait too; sizing
|
|
228
|
+
// it on readiness alone times out requests the broker is still running. A
|
|
229
|
+
// request that named no readiness budget is queued for the default one, so it
|
|
230
|
+
// needs the same extension rather than the client's generic request deadline.
|
|
231
|
+
const timeoutMs = lifecycleRequestTimeoutMs(operation, input);
|
|
231
232
|
return await this.#client.global(operation, input, {
|
|
232
233
|
idempotencyKey,
|
|
233
|
-
...(
|
|
234
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
234
235
|
});
|
|
235
236
|
}
|
|
236
237
|
|
package/src/sdk/acp/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export type { AcpProviderRegistration, AcpReverseConnection, AcpSdkAdapterOptions } from "./adapter";
|
|
2
|
-
export { ACP_SESSION_RECONNECT, AcpSdkAdapter, AcpSdkAdapterError } from "./adapter";
|
|
2
|
+
export { ACP_SESSION_RECONNECT, AcpSdkAdapter, AcpSdkAdapterError, acpMcpLaunchFailure } from "./adapter";
|