@gajae-code/coding-agent 0.12.19 → 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 +20 -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/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/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
package/src/commands/sdk.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { logger } from "@gajae-code/utils";
|
|
5
6
|
import { Args, CliParseError, Command, Flags, renderCommandHelp } from "@gajae-code/utils/cli";
|
|
6
7
|
import type { Args as ParsedArgs } from "../cli/args";
|
|
7
8
|
import { Settings } from "../config/settings";
|
|
@@ -16,6 +17,9 @@ import {
|
|
|
16
17
|
readSessionLifecycleLaunchRequest,
|
|
17
18
|
type SessionLifecycleLaunchRequest,
|
|
18
19
|
type SessionLifecycleTranscriptIdentity,
|
|
20
|
+
sessionHostAttachedClients,
|
|
21
|
+
sessionHostWorkInFlight,
|
|
22
|
+
startBrokerDeadRegistrationSweep,
|
|
19
23
|
writeSessionLifecycleFailure,
|
|
20
24
|
writeSessionLifecycleReady,
|
|
21
25
|
} from "../sdk/broker/lifecycle";
|
|
@@ -107,6 +111,96 @@ export async function watchSessionHostBrokerLiveness(deps: {
|
|
|
107
111
|
}
|
|
108
112
|
}
|
|
109
113
|
|
|
114
|
+
/**
|
|
115
|
+
* How long a session host that has served at least one client tolerates having
|
|
116
|
+
* no client attached before treating itself as abandoned.
|
|
117
|
+
*
|
|
118
|
+
* This cannot fire during healthy work. "Attached" is the host's own live
|
|
119
|
+
* socket-subscription count, so a client that is merely idle — an editor
|
|
120
|
+
* sitting on an open ACP session, a long agent turn with nobody typing — still
|
|
121
|
+
* holds a socket and resets the window on every poll. Only a client that is
|
|
122
|
+
* actually gone opens it, and 30 minutes is far longer than any client
|
|
123
|
+
* reconnect budget (ACP's is seconds), so a crashed-and-restarted client
|
|
124
|
+
* reattaches long before the window closes.
|
|
125
|
+
*/
|
|
126
|
+
export const SESSION_HOST_DETACHED_IDLE_GRACE_MS = 30 * 60_000;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* How long a freshly spawned session host waits for its very first client
|
|
130
|
+
* before treating itself as abandoned.
|
|
131
|
+
*
|
|
132
|
+
* A host is ready before its client has finished dialing, so a host that has
|
|
133
|
+
* never seen an attachment must not be judged by the detached window above. One
|
|
134
|
+
* hour is orders of magnitude beyond the slowest observed cold start (worktree
|
|
135
|
+
* preparation, MCP server launch, model-profile application) and beyond any
|
|
136
|
+
* lifecycle readiness deadline the broker will wait on, so it can only elapse
|
|
137
|
+
* for a host nobody ever came for.
|
|
138
|
+
*/
|
|
139
|
+
export const SESSION_HOST_FIRST_ATTACH_GRACE_MS = 60 * 60_000;
|
|
140
|
+
|
|
141
|
+
const SESSION_HOST_ATTACHMENT_POLL_MS = 30_000;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Resolves once this host is provably abandoned: either it has been detached
|
|
145
|
+
* from every client for the full idle grace, or nobody has come for it at all
|
|
146
|
+
* for the full first-attach grace.
|
|
147
|
+
*
|
|
148
|
+
* `readAttachedClients` reports the host's own live client/socket subscription
|
|
149
|
+
* count; `undefined` means the SDK endpoint publishes no such evidence yet and
|
|
150
|
+
* is deliberately *not* treated as detachment, so a host can never be reaped on
|
|
151
|
+
* missing evidence — only on observed absence of clients.
|
|
152
|
+
*
|
|
153
|
+
* `readWorkInFlight` reports whether the host is running agent work right now.
|
|
154
|
+
* Work is positive proof that a client did come for this host: a prompt can
|
|
155
|
+
* only arrive over the endpoint a client dialed. So for a host that has never
|
|
156
|
+
* been *observed* attached — a client that connected, prompted and dropped
|
|
157
|
+
* between two 30s polls, or a client count that is momentarily unreadable — the
|
|
158
|
+
* first-attach window is measured from the last moment work was in flight
|
|
159
|
+
* rather than from process start. That still bounds a host whose SDK runtime
|
|
160
|
+
* never came up: with no transport it can never receive a prompt, so it never
|
|
161
|
+
* reports work and its window keeps running from start. And any real turn ends,
|
|
162
|
+
* after which the window resumes; live work defers the bound, never removes it.
|
|
163
|
+
*/
|
|
164
|
+
export async function watchSessionHostClientAttachment(deps: {
|
|
165
|
+
readAttachedClients: () => number | undefined;
|
|
166
|
+
readWorkInFlight?: () => boolean;
|
|
167
|
+
now?: () => number;
|
|
168
|
+
sleep?: (ms: number) => Promise<void>;
|
|
169
|
+
idleGraceMs?: number;
|
|
170
|
+
firstAttachGraceMs?: number;
|
|
171
|
+
pollMs?: number;
|
|
172
|
+
}): Promise<void> {
|
|
173
|
+
const now = deps.now ?? Date.now;
|
|
174
|
+
const sleep = deps.sleep ?? (async ms => await Bun.sleep(ms));
|
|
175
|
+
const readWorkInFlight = deps.readWorkInFlight ?? (() => false);
|
|
176
|
+
const idleGraceMs = deps.idleGraceMs ?? SESSION_HOST_DETACHED_IDLE_GRACE_MS;
|
|
177
|
+
const firstAttachGraceMs = deps.firstAttachGraceMs ?? SESSION_HOST_FIRST_ATTACH_GRACE_MS;
|
|
178
|
+
const pollMs = deps.pollMs ?? SESSION_HOST_ATTACHMENT_POLL_MS;
|
|
179
|
+
let everAttached = false;
|
|
180
|
+
let detachedSince: number | null = null;
|
|
181
|
+
/** Start of the current window in which nobody has been shown to want this host. */
|
|
182
|
+
let unattendedSince = now();
|
|
183
|
+
for (;;) {
|
|
184
|
+
const attached = deps.readAttachedClients();
|
|
185
|
+
if (readWorkInFlight()) unattendedSince = now();
|
|
186
|
+
if (attached === undefined) {
|
|
187
|
+
// No endpoint evidence at all is ambiguity, not abandonment; it still
|
|
188
|
+
// accrues against the first-attach bound so a host whose SDK runtime
|
|
189
|
+
// never came up cannot outlive that bound either.
|
|
190
|
+
if (!everAttached && now() - unattendedSince >= firstAttachGraceMs) return;
|
|
191
|
+
} else if (attached > 0) {
|
|
192
|
+
everAttached = true;
|
|
193
|
+
detachedSince = null;
|
|
194
|
+
} else if (everAttached) {
|
|
195
|
+
detachedSince ??= now();
|
|
196
|
+
if (now() - detachedSince >= idleGraceMs) return;
|
|
197
|
+
} else if (now() - unattendedSince >= firstAttachGraceMs) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
await sleep(pollMs);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
110
204
|
type LifecycleTranscriptSource = {
|
|
111
205
|
cwd: string;
|
|
112
206
|
path: string;
|
|
@@ -222,6 +316,7 @@ export async function openLifecycleSessionManager(
|
|
|
222
316
|
SessionManager.managedDestination(cwd, agentDir),
|
|
223
317
|
undefined,
|
|
224
318
|
migrationPolicy,
|
|
319
|
+
lifecycleSettings.get("sessionMemory.mode"),
|
|
225
320
|
);
|
|
226
321
|
if (opened.kind === "error")
|
|
227
322
|
throw new Error("Lifecycle saved session authority changed while the session host opened it.");
|
|
@@ -238,6 +333,7 @@ export async function openLifecycleSessionManager(
|
|
|
238
333
|
cwd,
|
|
239
334
|
SessionManager.managedDestination(cwd, agentDir),
|
|
240
335
|
migrationPolicy,
|
|
336
|
+
lifecycleSettings.get("sessionMemory.mode"),
|
|
241
337
|
);
|
|
242
338
|
if (forked.kind === "error")
|
|
243
339
|
throw new Error("Lifecycle saved session authority changed while the session host forked it.");
|
|
@@ -246,6 +342,22 @@ export async function openLifecycleSessionManager(
|
|
|
246
342
|
return { parsed, sessionManager };
|
|
247
343
|
}
|
|
248
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Starts the memory backend a lifecycle session deferred past its readiness
|
|
347
|
+
* window.
|
|
348
|
+
*
|
|
349
|
+
* Deliberately not awaited: the local backend summarises every queued rollout
|
|
350
|
+
* through the model, so its duration scales with the backlog and would eat the
|
|
351
|
+
* broker's readiness budget. The session is already published as ready here, so
|
|
352
|
+
* a failure is a degraded-memory condition, not a startup failure — it is
|
|
353
|
+
* logged and swallowed instead of surfacing as an unhandled rejection.
|
|
354
|
+
*/
|
|
355
|
+
export function startMemoryBackendAfterReadiness(start: () => Promise<void>): void {
|
|
356
|
+
void start().catch(error => {
|
|
357
|
+
logger.warn("Deferred memory backend startup failed after readiness", { error: String(error) });
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
249
361
|
/** Runs the same persisted AgentSession bootstrap used by the production CLI. */
|
|
250
362
|
export async function runSessionHost(
|
|
251
363
|
timing: {
|
|
@@ -438,7 +550,7 @@ export async function runSessionHost(
|
|
|
438
550
|
|
|
439
551
|
throw created.failure;
|
|
440
552
|
}
|
|
441
|
-
const { session, capability, rollback } = created;
|
|
553
|
+
const { session, capability, rollback, startDeferredMemoryBackend } = created;
|
|
442
554
|
let sessionDisposal: Promise<void> | undefined;
|
|
443
555
|
const disposeSession = (): Promise<void> => {
|
|
444
556
|
sessionDisposal ??= session.dispose().catch(() => {});
|
|
@@ -553,12 +665,24 @@ export async function runSessionHost(
|
|
|
553
665
|
await failAfterRollback(durableFailure);
|
|
554
666
|
throw error;
|
|
555
667
|
}
|
|
668
|
+
// Readiness is published; the session is live. Memory startup issues one LLM
|
|
669
|
+
// request per queued rollout, so it must run outside the readiness window and
|
|
670
|
+
// its failure must never take the session down.
|
|
671
|
+
startMemoryBackendAfterReadiness(startDeferredMemoryBackend);
|
|
556
672
|
process.once("SIGTERM", stop);
|
|
557
673
|
process.once("SIGINT", stop);
|
|
558
|
-
//
|
|
559
|
-
//
|
|
560
|
-
//
|
|
561
|
-
|
|
674
|
+
// Two independent bounds, either of which reaps this detached host through
|
|
675
|
+
// the same graceful teardown a SIGTERM would take. The first covers a broker
|
|
676
|
+
// that is gone for good; the second covers the opposite case, a perfectly
|
|
677
|
+
// healthy broker whose host nobody is attached to any more and for which no
|
|
678
|
+
// `session.close` will ever arrive.
|
|
679
|
+
await Promise.race([
|
|
680
|
+
watchSessionHostBrokerLiveness({ agentDir }),
|
|
681
|
+
watchSessionHostClientAttachment({
|
|
682
|
+
readAttachedClients: sessionHostAttachedClients,
|
|
683
|
+
readWorkInFlight: sessionHostWorkInFlight,
|
|
684
|
+
}),
|
|
685
|
+
]);
|
|
562
686
|
stop();
|
|
563
687
|
await new Promise<void>(() => {});
|
|
564
688
|
}
|
|
@@ -617,9 +741,19 @@ export default class Sdk extends Command {
|
|
|
617
741
|
});
|
|
618
742
|
await broker.start();
|
|
619
743
|
if (!broker.ownsDiscovery) return;
|
|
620
|
-
|
|
744
|
+
// A live broker must not keep advertising sessions whose host process is
|
|
745
|
+
// gone; the sweep is the broker-side half of the host reaping bound.
|
|
746
|
+
const stopSweep = startBrokerDeadRegistrationSweep(broker);
|
|
747
|
+
const stop = () => {
|
|
748
|
+
stopSweep();
|
|
749
|
+
void broker.stop();
|
|
750
|
+
};
|
|
621
751
|
process.once("SIGTERM", stop);
|
|
622
752
|
process.once("SIGINT", stop);
|
|
623
|
-
|
|
753
|
+
try {
|
|
754
|
+
await completeBrokerProcess(broker);
|
|
755
|
+
} finally {
|
|
756
|
+
stopSweep();
|
|
757
|
+
}
|
|
624
758
|
}
|
|
625
759
|
}
|
|
@@ -2079,6 +2079,20 @@ export const SETTINGS_SCHEMA = {
|
|
|
2079
2079
|
},
|
|
2080
2080
|
},
|
|
2081
2081
|
|
|
2082
|
+
// Bounded-memory cold-session offloading (rollout knobs; budgets are fixed
|
|
2083
|
+
// implementation constants in the sidecar primitives, not user-tunable
|
|
2084
|
+
// fields). "shadow" (default) measures and offloads without changing
|
|
2085
|
+
// observable behavior; canary/default-on are release-channel states, not
|
|
2086
|
+
// user-facing enum values.
|
|
2087
|
+
"sessionMemory.mode": {
|
|
2088
|
+
type: "enum",
|
|
2089
|
+
values: ["off", "shadow", "enabled"] as const,
|
|
2090
|
+
default: "shadow",
|
|
2091
|
+
},
|
|
2092
|
+
// Independent runtime switch for AgentSession's async compact-once overflow
|
|
2093
|
+
// recovery. The synchronous bounded context preflight stays always on.
|
|
2094
|
+
"sessionMemory.contextOverflowRecovery": { type: "boolean", default: true },
|
|
2095
|
+
|
|
2082
2096
|
// ────────────────────────────────────────────────────────────────────────
|
|
2083
2097
|
// Editing
|
|
2084
2098
|
// ────────────────────────────────────────────────────────────────────────
|
|
@@ -2810,6 +2824,29 @@ export const SETTINGS_SCHEMA = {
|
|
|
2810
2824
|
description: "Parent-process RSS (MB) above which idle tabs are opportunistically evicted LRU.",
|
|
2811
2825
|
},
|
|
2812
2826
|
},
|
|
2827
|
+
// `gjc gc --disk` retention policy. Report-only unless `--prune` is also passed;
|
|
2828
|
+
// these knobs never run implicitly and never affect the PID-liveness axis.
|
|
2829
|
+
"gc.sessions.maxAgeDays": {
|
|
2830
|
+
type: "number",
|
|
2831
|
+
default: 60,
|
|
2832
|
+
validate: (value: number) => Number.isFinite(value) && value >= 0,
|
|
2833
|
+
},
|
|
2834
|
+
// 0 disables the size axis; only the age axis retires transcripts then.
|
|
2835
|
+
"gc.sessions.maxTotalBytes": {
|
|
2836
|
+
type: "number",
|
|
2837
|
+
default: 0,
|
|
2838
|
+
validate: (value: number) => Number.isFinite(value) && value >= 0,
|
|
2839
|
+
},
|
|
2840
|
+
"gc.natives.keepVersions": {
|
|
2841
|
+
type: "number",
|
|
2842
|
+
default: 2,
|
|
2843
|
+
validate: (value: number) => Number.isInteger(value) && value >= 0,
|
|
2844
|
+
},
|
|
2845
|
+
"gc.backups.maxAgeDays": {
|
|
2846
|
+
type: "number",
|
|
2847
|
+
default: 30,
|
|
2848
|
+
validate: (value: number) => Number.isFinite(value) && value >= 0,
|
|
2849
|
+
},
|
|
2813
2850
|
"resourceGc.sweepIntervalMs": {
|
|
2814
2851
|
type: "number",
|
|
2815
2852
|
default: 30_000,
|
|
@@ -4136,6 +4173,11 @@ export interface ShellMinimizerSettings {
|
|
|
4136
4173
|
maxCaptureBytes: number;
|
|
4137
4174
|
}
|
|
4138
4175
|
|
|
4176
|
+
export interface SessionMemorySettings {
|
|
4177
|
+
mode: "off" | "shadow" | "enabled";
|
|
4178
|
+
contextOverflowRecovery: boolean;
|
|
4179
|
+
}
|
|
4180
|
+
|
|
4139
4181
|
export interface MemoryGuardSettings {
|
|
4140
4182
|
enabled: boolean;
|
|
4141
4183
|
checkIntervalMs: number;
|
|
@@ -4211,6 +4253,7 @@ export interface GroupTypeMap {
|
|
|
4211
4253
|
thinkingBudgets: ThinkingBudgetsSettings;
|
|
4212
4254
|
stt: SttSettings;
|
|
4213
4255
|
memoryGuard: MemoryGuardSettings;
|
|
4256
|
+
sessionMemory: SessionMemorySettings;
|
|
4214
4257
|
modelRoles: Record<string, ModelSelectorValue>;
|
|
4215
4258
|
modelTags: ModelTagsSettings;
|
|
4216
4259
|
cycleOrder: string[];
|
|
@@ -16,6 +16,7 @@ import { listMcpDelegateHostContexts } from "../hooks/mcp-delegate-host-context"
|
|
|
16
16
|
import type { WorkflowGate, WorkflowGateQueryRecord } from "../modes/shared/agent-wire/workflow-gate-types";
|
|
17
17
|
import type { BrokerDiscovery } from "../sdk/broker/discovery";
|
|
18
18
|
import { type EnsureBrokerSettings, ensureBroker } from "../sdk/broker/ensure";
|
|
19
|
+
import { lifecycleRequestTimeoutMs } from "../sdk/broker/startup-budget";
|
|
19
20
|
import { UnsupportedStateVersionError } from "../sdk/broker/state-version";
|
|
20
21
|
import { SdkClient, SdkClientError } from "../sdk/client/client";
|
|
21
22
|
import { readSdkBrokerDiscovery } from "../sdk/client/discovery";
|
|
@@ -3183,7 +3184,11 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
3183
3184
|
let requestError: SdkClientError | undefined;
|
|
3184
3185
|
let result: unknown;
|
|
3185
3186
|
try {
|
|
3186
|
-
|
|
3187
|
+
const timeoutMs = lifecycleRequestTimeoutMs(operation, input);
|
|
3188
|
+
result = await client.global(operation, input, {
|
|
3189
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
3190
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
3191
|
+
});
|
|
3187
3192
|
} catch (error) {
|
|
3188
3193
|
requestError = toCoordinatorBrokerError("request", error);
|
|
3189
3194
|
}
|
package/src/export/html/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import type { AgentState } from "@gajae-code/agent-core";
|
|
3
4
|
import { APP_NAME, isEnoent } from "@gajae-code/utils";
|
|
4
5
|
import { getResolvedThemeColors, getThemeExportColors } from "../../modes/theme/theme";
|
|
5
|
-
import {
|
|
6
|
+
import { SessionManager } from "../../session/session-manager";
|
|
6
7
|
// Pre-generated template (created by scripts/generate-template.ts at publish time)
|
|
7
8
|
import { TEMPLATE } from "./template.generated";
|
|
8
9
|
|
|
@@ -90,26 +91,72 @@ async function generateThemeVars(themeName?: string): Promise<string> {
|
|
|
90
91
|
return lines.join(" ");
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
class Base64StreamEncoder {
|
|
95
|
+
#carry = Buffer.alloc(0);
|
|
96
|
+
|
|
97
|
+
write(value: string): string {
|
|
98
|
+
const incoming = Buffer.from(value, "utf8");
|
|
99
|
+
const bytes = this.#carry.byteLength === 0 ? incoming : Buffer.concat([this.#carry, incoming]);
|
|
100
|
+
const completeLength = bytes.byteLength - (bytes.byteLength % 3);
|
|
101
|
+
this.#carry = bytes.subarray(completeLength);
|
|
102
|
+
return completeLength === 0 ? "" : bytes.subarray(0, completeLength).toString("base64");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
finish(): string {
|
|
106
|
+
const final = this.#carry.toString("base64");
|
|
107
|
+
this.#carry = Buffer.alloc(0);
|
|
108
|
+
return final;
|
|
109
|
+
}
|
|
99
110
|
}
|
|
100
111
|
|
|
101
|
-
|
|
102
|
-
|
|
112
|
+
async function writeSessionHtml(
|
|
113
|
+
sm: SessionManager,
|
|
114
|
+
state: AgentState | undefined,
|
|
115
|
+
outputPath: string,
|
|
116
|
+
themeName?: string,
|
|
117
|
+
): Promise<void> {
|
|
103
118
|
const themeVars = await generateThemeVars(themeName);
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
() =>
|
|
112
|
-
|
|
119
|
+
const themedTemplate = TEMPLATE.replace("<theme-vars/>", () => `<style>:root { ${themeVars} }</style>`);
|
|
120
|
+
const marker = "{{SESSION_DATA}}";
|
|
121
|
+
const markerOffset = themedTemplate.indexOf(marker);
|
|
122
|
+
if (markerOffset < 0) throw new Error("HTML export template is missing the session-data marker");
|
|
123
|
+
const sourcePath = sm.getSessionFile();
|
|
124
|
+
if (sourcePath) {
|
|
125
|
+
const sourceIdentity = await fs.realpath(sourcePath).catch(() => path.resolve(sourcePath));
|
|
126
|
+
const outputIdentity = await fs.realpath(outputPath).catch(() => path.resolve(outputPath));
|
|
127
|
+
if (sourceIdentity === outputIdentity)
|
|
128
|
+
throw new Error("HTML export output must not overwrite the source transcript");
|
|
129
|
+
}
|
|
130
|
+
const sink = Bun.file(outputPath).writer();
|
|
131
|
+
const encoder = new Base64StreamEncoder();
|
|
132
|
+
const writeEncoded = (value: string): void => {
|
|
133
|
+
const encoded = encoder.write(value);
|
|
134
|
+
if (encoded) sink.write(encoded);
|
|
135
|
+
};
|
|
136
|
+
try {
|
|
137
|
+
sink.write(themedTemplate.slice(0, markerOffset));
|
|
138
|
+
writeEncoded(`{"header":${JSON.stringify(sm.getHeader())},"entries":[`);
|
|
139
|
+
let first = true;
|
|
140
|
+
sm.visitEntriesForExport(entry => {
|
|
141
|
+
writeEncoded(`${first ? "" : ","}${JSON.stringify(entry)}`);
|
|
142
|
+
first = false;
|
|
143
|
+
});
|
|
144
|
+
writeEncoded(`],"leafId":${JSON.stringify(sm.getLeafId())}`);
|
|
145
|
+
if (state?.systemPrompt !== undefined)
|
|
146
|
+
writeEncoded(`,"systemPrompt":${JSON.stringify(state.systemPrompt.join("\n\n"))}`);
|
|
147
|
+
if (state?.tools !== undefined)
|
|
148
|
+
writeEncoded(
|
|
149
|
+
`,"tools":${JSON.stringify(state.tools.map(tool => ({ name: tool.name, description: tool.description })))}`,
|
|
150
|
+
);
|
|
151
|
+
writeEncoded("}");
|
|
152
|
+
const final = encoder.finish();
|
|
153
|
+
if (final) sink.write(final);
|
|
154
|
+
sink.write(themedTemplate.slice(markerOffset + marker.length));
|
|
155
|
+
await sink.end();
|
|
156
|
+
} catch (error) {
|
|
157
|
+
await sink.end();
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
113
160
|
}
|
|
114
161
|
|
|
115
162
|
/** Export session to HTML using SessionManager and AgentState. */
|
|
@@ -123,18 +170,8 @@ export async function exportSessionToHtml(
|
|
|
123
170
|
const sessionFile = sm.getSessionFile();
|
|
124
171
|
if (!sessionFile) throw new Error("Cannot export in-memory session to HTML");
|
|
125
172
|
|
|
126
|
-
const sessionData: SessionData = {
|
|
127
|
-
header: sm.getHeader(),
|
|
128
|
-
entries: sm.getEntriesForExport(),
|
|
129
|
-
leafId: sm.getLeafId(),
|
|
130
|
-
systemPrompt: state?.systemPrompt.join("\n\n"),
|
|
131
|
-
tools: state?.tools?.map(t => ({ name: t.name, description: t.description })),
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const html = await generateHtml(sessionData, opts.themeName);
|
|
135
173
|
const outputPath = opts.outputPath || `${APP_NAME}-session-${path.basename(sessionFile, ".jsonl")}.html`;
|
|
136
|
-
|
|
137
|
-
await Bun.write(outputPath, html);
|
|
174
|
+
await writeSessionHtml(sm, state, outputPath, opts.themeName);
|
|
138
175
|
return outputPath;
|
|
139
176
|
}
|
|
140
177
|
|
|
@@ -143,24 +180,31 @@ export async function exportFromFile(inputPath: string, options?: ExportOptions
|
|
|
143
180
|
const opts: ExportOptions = typeof options === "string" ? { outputPath: options } : options || {};
|
|
144
181
|
|
|
145
182
|
let sm: SessionManager;
|
|
183
|
+
const artifactRoot = inputPath.endsWith(".jsonl") ? inputPath.slice(0, -6) : inputPath;
|
|
184
|
+
const hasExistingSidecars = (
|
|
185
|
+
await Promise.all(
|
|
186
|
+
["idx", "tail", "commit"].map(kind =>
|
|
187
|
+
Bun.file(path.join(artifactRoot, `.session-memory.spill.${kind}`)).exists(),
|
|
188
|
+
),
|
|
189
|
+
)
|
|
190
|
+
).some(Boolean);
|
|
191
|
+
if (!(await Bun.file(inputPath).exists())) throw new Error(`File not found: ${inputPath}`);
|
|
146
192
|
try {
|
|
147
|
-
sm = await SessionManager.open(
|
|
193
|
+
sm = await SessionManager.open(
|
|
194
|
+
inputPath,
|
|
195
|
+
undefined,
|
|
196
|
+
undefined,
|
|
197
|
+
"copy-retain",
|
|
198
|
+
hasExistingSidecars ? "off" : "enabled",
|
|
199
|
+
);
|
|
148
200
|
} catch (err) {
|
|
149
201
|
if (isEnoent(err)) throw new Error(`File not found: ${inputPath}`);
|
|
150
202
|
throw err;
|
|
151
203
|
}
|
|
152
204
|
|
|
153
205
|
try {
|
|
154
|
-
const sessionData: SessionData = {
|
|
155
|
-
header: sm.getHeader(),
|
|
156
|
-
entries: sm.getEntriesForExport(),
|
|
157
|
-
leafId: sm.getLeafId(),
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
const html = await generateHtml(sessionData, opts.themeName);
|
|
161
206
|
const outputPath = opts.outputPath || `${APP_NAME}-session-${path.basename(inputPath, ".jsonl")}.html`;
|
|
162
|
-
|
|
163
|
-
await Bun.write(outputPath, html);
|
|
207
|
+
await writeSessionHtml(sm, undefined, outputPath, opts.themeName);
|
|
164
208
|
return outputPath;
|
|
165
209
|
} finally {
|
|
166
210
|
await sm.close();
|