@gajae-code/coding-agent 0.11.5 → 0.11.7
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 +37 -0
- package/dist/types/capability/index.d.ts +10 -20
- package/dist/types/capability/types.d.ts +3 -0
- package/dist/types/commands/daemon.d.ts +2 -2
- package/dist/types/commands/sdk.d.ts +20 -2
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +67 -7
- package/dist/types/eval/py/runner-artifact.d.ts +8 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +5 -5
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +19 -5
- package/dist/types/main.d.ts +24 -1
- package/dist/types/modes/components/btw-panel.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-status-header.d.ts +1 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -0
- package/dist/types/sdk/bus/index.d.ts +1 -1
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
- package/dist/types/sdk/bus/notification-service.d.ts +34 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +13 -3
- package/dist/types/sdk/bus/telegram-daemon.d.ts +61 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
- package/dist/types/sdk/client/discovery.d.ts +2 -0
- package/dist/types/sdk/client/liveness.d.ts +7 -0
- package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
- package/dist/types/sdk/transport/index.d.ts +12 -0
- package/dist/types/sdk/transport/relay.d.ts +43 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
- package/dist/types/sdk/transport/socket.d.ts +7 -0
- package/dist/types/sdk/transport/stdio.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +28 -5
- package/dist/types/session/btw-contract.d.ts +16 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +21 -7
- package/dist/types/session/internal/managed-session-storage.d.ts +10 -8
- package/dist/types/session/session-manager.d.ts +35 -4
- package/dist/types/session/yield-queue.d.ts +8 -0
- package/dist/types/setup/credential-import.d.ts +5 -1
- package/dist/types/setup/provider-onboarding.d.ts +2 -0
- package/dist/types/ssh/utils.d.ts +1 -0
- package/dist/types/task/discovery.d.ts +3 -1
- package/dist/types/tools/fetch.d.ts +5 -8
- package/dist/types/tools/image-gen.d.ts +16 -0
- package/dist/types/tools/skill-discovery.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +7 -0
- package/dist/types/utils/shell-snapshot.d.ts +12 -0
- package/dist/types/web/insane/bridge.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +17 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/src/capability/index.ts +50 -42
- package/src/capability/types.ts +3 -0
- package/src/cli/setup-cli.ts +10 -4
- package/src/cli.ts +34 -9
- package/src/commands/daemon.ts +22 -5
- package/src/commands/sdk.ts +42 -9
- package/src/config/keybindings.ts +1 -1
- package/src/config/model-profiles.ts +18 -0
- package/src/config/settings-schema.ts +71 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
- package/src/discovery/ssh.ts +12 -1
- package/src/eval/py/kernel.ts +1 -21
- package/src/eval/py/runner-artifact.ts +84 -0
- package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
- package/src/gjc-runtime/ultragoal-guard.ts +88 -10
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +275 -12
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +23 -0
- package/src/internal-urls/docs-index.generated.ts +9 -8
- package/src/internal-urls/local-protocol.ts +32 -6
- package/src/main.ts +87 -28
- package/src/modes/action-registry.ts +1 -1
- package/src/modes/components/btw-panel.ts +82 -16
- package/src/modes/components/model-selector.ts +31 -3
- package/src/modes/components/tool-status-header.ts +6 -1
- package/src/modes/controllers/btw-controller.ts +119 -36
- package/src/modes/controllers/command-controller.ts +15 -0
- package/src/modes/controllers/extension-ui-controller.ts +14 -2
- package/src/modes/controllers/input-controller.ts +31 -0
- package/src/modes/controllers/selector-controller.ts +106 -12
- package/src/modes/interactive-mode.ts +16 -7
- package/src/modes/runtime-init.ts +16 -2
- package/src/modes/types.ts +1 -0
- package/src/prompts/system/btw-user.md +3 -8
- package/src/prompts/tools/skill-discovery.md +1 -0
- package/src/runtime-mcp/client.ts +41 -55
- package/src/runtime-mcp/content-limits.ts +71 -0
- package/src/runtime-mcp/json-rpc.ts +23 -9
- package/src/runtime-mcp/transports/http.ts +50 -16
- package/src/sdk/bus/chat-daemon-control.ts +20 -7
- package/src/sdk/bus/index.ts +8 -7
- package/src/sdk/bus/lifecycle-commands.ts +16 -7
- package/src/sdk/bus/notification-service.ts +41 -18
- package/src/sdk/bus/telegram-daemon-contract.ts +13 -3
- package/src/sdk/bus/telegram-daemon-control.ts +26 -8
- package/src/sdk/bus/telegram-daemon.ts +1736 -338
- package/src/sdk/bus/topic-registry.ts +451 -12
- package/src/sdk/client/discovery.ts +19 -3
- package/src/sdk/client/liveness.ts +49 -0
- package/src/sdk/host/control/dispatch.ts +14 -5
- package/src/sdk/protocol/operation-inventory.generated.json +29 -0
- package/src/sdk/session.ts +29 -5
- package/src/sdk/transport/auth-preface.ts +82 -0
- package/src/sdk/transport/index.ts +18 -0
- package/src/sdk/transport/relay.ts +252 -0
- package/src/sdk/transport/serve-cli.ts +105 -0
- package/src/sdk/transport/socket.ts +197 -0
- package/src/sdk/transport/stdio.ts +17 -0
- package/src/session/agent-session.ts +1212 -768
- package/src/session/blob-store.ts +19 -6
- package/src/session/btw-contract.ts +59 -0
- package/src/session/internal/managed-session-scope.ts +313 -57
- package/src/session/internal/managed-session-storage.ts +146 -35
- package/src/session/session-manager.ts +385 -85
- package/src/session/yield-queue.ts +20 -0
- package/src/setup/credential-import.ts +28 -3
- package/src/setup/provider-onboarding.ts +30 -1
- package/src/setup/provider-presets.json +2 -1
- package/src/slash-commands/builtin-registry.ts +37 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- package/src/tools/ask-answer-registry.ts +15 -1
- package/src/tools/ask.ts +1 -1
- package/src/tools/fetch.ts +13 -131
- package/src/tools/image-gen.ts +301 -20
- package/src/tools/read.ts +2 -0
- package/src/tools/skill-discovery.ts +39 -3
- package/src/tools/sqlite-reader.ts +63 -5
- package/src/utils/pasted-image-path.ts +15 -6
- package/src/utils/shell-snapshot.ts +173 -63
- package/src/web/insane/bridge.ts +5 -1
- package/src/web/insane/url-guard.ts +138 -27
- package/src/web/scrapers/types.ts +19 -49
- package/src/web/scrapers/utils.ts +12 -26
|
@@ -15,15 +15,12 @@ import type {
|
|
|
15
15
|
MCPInitializeParams,
|
|
16
16
|
MCPInitializeResult,
|
|
17
17
|
MCPPrompt,
|
|
18
|
-
MCPPromptsListResult,
|
|
19
18
|
MCPRequestOptions,
|
|
20
19
|
MCPResource,
|
|
21
20
|
MCPResourceReadParams,
|
|
22
21
|
MCPResourceReadResult,
|
|
23
22
|
MCPResourceSubscribeParams,
|
|
24
|
-
MCPResourcesListResult,
|
|
25
23
|
MCPResourceTemplate,
|
|
26
|
-
MCPResourceTemplatesListResult,
|
|
27
24
|
MCPServerCapabilities,
|
|
28
25
|
MCPServerConfig,
|
|
29
26
|
MCPServerConnection,
|
|
@@ -43,6 +40,9 @@ const PROTOCOL_VERSION = "2025-03-26";
|
|
|
43
40
|
/** Default connection timeout in ms */
|
|
44
41
|
const CONNECTION_TIMEOUT_MS = 30_000;
|
|
45
42
|
|
|
43
|
+
const MAX_PAGINATION_PAGES = 100,
|
|
44
|
+
MAX_PAGINATION_ITEMS = 10_000;
|
|
45
|
+
|
|
46
46
|
/** Client info sent during initialization */
|
|
47
47
|
const CLIENT_INFO = {
|
|
48
48
|
name: "gjc-coding-agent",
|
|
@@ -109,6 +109,40 @@ function decodeToolsListResult(value: unknown): MCPToolsListResult {
|
|
|
109
109
|
return value.nextCursor === undefined ? { tools } : { tools, nextCursor: value.nextCursor };
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
async function collectPaginated<T>(
|
|
113
|
+
connection: MCPServerConnection,
|
|
114
|
+
options: MCPRequestOptions | undefined,
|
|
115
|
+
method: string,
|
|
116
|
+
itemKey: string,
|
|
117
|
+
items: T[],
|
|
118
|
+
decode?: (value: unknown) => unknown,
|
|
119
|
+
): Promise<void> {
|
|
120
|
+
const seenCursors = new Set<string>();
|
|
121
|
+
const failure = (detail: string) => new MCPExpectedFailure(new Error(`MCP ${method} pagination ${detail}`));
|
|
122
|
+
let cursor: string | undefined;
|
|
123
|
+
for (let page = 1; page <= MAX_PAGINATION_PAGES; page++) {
|
|
124
|
+
const value = await connection.transport.request<unknown>(method, cursor ? { cursor } : {}, options);
|
|
125
|
+
const result = decode ? decode(value) : value;
|
|
126
|
+
if (
|
|
127
|
+
!isRecord(result) ||
|
|
128
|
+
!Array.isArray(result[itemKey]) ||
|
|
129
|
+
(result.nextCursor !== undefined && typeof result.nextCursor !== "string")
|
|
130
|
+
)
|
|
131
|
+
throw new MCPExpectedFailure();
|
|
132
|
+
const nextCursor = result.nextCursor as string | undefined;
|
|
133
|
+
if (nextCursor && seenCursors.has(nextCursor)) throw failure("repeated a cursor");
|
|
134
|
+
const pageItems = result[itemKey] as T[];
|
|
135
|
+
const itemCount = items.length + pageItems.length;
|
|
136
|
+
if (itemCount > MAX_PAGINATION_ITEMS || (itemCount === MAX_PAGINATION_ITEMS && nextCursor))
|
|
137
|
+
throw failure("did not complete within the 10000-item budget");
|
|
138
|
+
items.push(...pageItems);
|
|
139
|
+
if (!nextCursor) return;
|
|
140
|
+
if (page === MAX_PAGINATION_PAGES) throw failure("did not complete within the 100-page budget");
|
|
141
|
+
seenCursors.add(nextCursor);
|
|
142
|
+
cursor = nextCursor;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
112
146
|
/**
|
|
113
147
|
* Default handler for standard MCP server-to-client requests.
|
|
114
148
|
* Handles `ping` and `roots/list`; rejects unknown methods with -32601.
|
|
@@ -292,18 +326,7 @@ export async function listTools(
|
|
|
292
326
|
}
|
|
293
327
|
|
|
294
328
|
const allTools: MCPToolDefinition[] = [];
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
do {
|
|
298
|
-
const params: Record<string, unknown> = {};
|
|
299
|
-
if (cursor) {
|
|
300
|
-
params.cursor = cursor;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const result = decodeToolsListResult(await connection.transport.request<unknown>("tools/list", params, options));
|
|
304
|
-
allTools.push(...result.tools);
|
|
305
|
-
cursor = result.nextCursor;
|
|
306
|
-
} while (cursor);
|
|
329
|
+
await collectPaginated(connection, options, "tools/list", "tools", allTools, decodeToolsListResult);
|
|
307
330
|
|
|
308
331
|
// Cache tools
|
|
309
332
|
connection.tools = allTools;
|
|
@@ -362,18 +385,7 @@ export async function listResources(
|
|
|
362
385
|
}
|
|
363
386
|
|
|
364
387
|
const allResources: MCPResource[] = [];
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
do {
|
|
368
|
-
const params: Record<string, unknown> = {};
|
|
369
|
-
if (cursor) {
|
|
370
|
-
params.cursor = cursor;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const result = await connection.transport.request<MCPResourcesListResult>("resources/list", params, options);
|
|
374
|
-
allResources.push(...result.resources);
|
|
375
|
-
cursor = result.nextCursor;
|
|
376
|
-
} while (cursor);
|
|
388
|
+
await collectPaginated(connection, options, "resources/list", "resources", allResources);
|
|
377
389
|
|
|
378
390
|
connection.resources = allResources;
|
|
379
391
|
return allResources;
|
|
@@ -395,22 +407,7 @@ export async function listResourceTemplates(
|
|
|
395
407
|
}
|
|
396
408
|
|
|
397
409
|
const allTemplates: MCPResourceTemplate[] = [];
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
do {
|
|
401
|
-
const params: Record<string, unknown> = {};
|
|
402
|
-
if (cursor) {
|
|
403
|
-
params.cursor = cursor;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const result = await connection.transport.request<MCPResourceTemplatesListResult>(
|
|
407
|
-
"resources/templates/list",
|
|
408
|
-
params,
|
|
409
|
-
options,
|
|
410
|
-
);
|
|
411
|
-
allTemplates.push(...result.resourceTemplates);
|
|
412
|
-
cursor = result.nextCursor;
|
|
413
|
-
} while (cursor);
|
|
410
|
+
await collectPaginated(connection, options, "resources/templates/list", "resourceTemplates", allTemplates);
|
|
414
411
|
|
|
415
412
|
connection.resourceTemplates = allTemplates;
|
|
416
413
|
return allTemplates;
|
|
@@ -514,18 +511,7 @@ export async function listPrompts(
|
|
|
514
511
|
}
|
|
515
512
|
|
|
516
513
|
const allPrompts: MCPPrompt[] = [];
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
do {
|
|
520
|
-
const params: Record<string, unknown> = {};
|
|
521
|
-
if (cursor) {
|
|
522
|
-
params.cursor = cursor;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
const result = await connection.transport.request<MCPPromptsListResult>("prompts/list", params, options);
|
|
526
|
-
allPrompts.push(...result.prompts);
|
|
527
|
-
cursor = result.nextCursor;
|
|
528
|
-
} while (cursor);
|
|
514
|
+
await collectPaginated(connection, options, "prompts/list", "prompts", allPrompts);
|
|
529
515
|
|
|
530
516
|
connection.prompts = allPrompts;
|
|
531
517
|
return allPrompts;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export const MCP_MAX_CONTENT_BYTES = 16 * 1024 * 1024;
|
|
2
|
+
export const MCP_MAX_ERROR_BYTES = 16 * 1024;
|
|
3
|
+
export const MCP_MAX_SSE_REQUEST_MESSAGES = 1024;
|
|
4
|
+
export const MCP_MAX_SSE_BATCH_MESSAGES = 1024;
|
|
5
|
+
export const MCP_HTTP_TIMEOUT_MS = 30_000;
|
|
6
|
+
|
|
7
|
+
function ignoreCancellation(cancel: () => Promise<unknown>): void {
|
|
8
|
+
try {
|
|
9
|
+
void cancel().catch(() => {});
|
|
10
|
+
} catch {}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function cancelMCPStream(stream: ReadableStream<Uint8Array> | null): void {
|
|
14
|
+
if (stream) ignoreCancellation(() => stream.cancel());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type ByteReader = Pick<ReadableStreamDefaultReader<Uint8Array>, "read">;
|
|
18
|
+
|
|
19
|
+
async function readChunk(reader: ByteReader, signal?: AbortSignal) {
|
|
20
|
+
if (!signal) return reader.read();
|
|
21
|
+
signal.throwIfAborted();
|
|
22
|
+
const aborted = Promise.withResolvers<never>();
|
|
23
|
+
const onAbort = () => aborted.reject(signal.reason);
|
|
24
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
25
|
+
try {
|
|
26
|
+
return await Promise.race([reader.read(), aborted.promise]);
|
|
27
|
+
} finally {
|
|
28
|
+
signal.removeEventListener("abort", onAbort);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function readMCPResponseText(response: Response, limit: number, truncate = false, signal?: AbortSignal) {
|
|
33
|
+
const declared = response.headers.get("Content-Length");
|
|
34
|
+
if (!truncate && declared && /^\d+$/.test(declared) && Number(declared) > limit) {
|
|
35
|
+
cancelMCPStream(response.body);
|
|
36
|
+
throw new Error("MCP response exceeds size limit");
|
|
37
|
+
}
|
|
38
|
+
if (!response.body) return "";
|
|
39
|
+
const reader = response.body.getReader();
|
|
40
|
+
let buffer = new Uint8Array(Math.min(limit, 64 * 1024));
|
|
41
|
+
let total = 0;
|
|
42
|
+
try {
|
|
43
|
+
while (true) {
|
|
44
|
+
const { value, done } = await readChunk(reader, signal);
|
|
45
|
+
if (done) break;
|
|
46
|
+
if (value.length === 0) continue;
|
|
47
|
+
const remaining = limit - total;
|
|
48
|
+
if (value.length > remaining) {
|
|
49
|
+
if (truncate && remaining > 0) buffer.set(value.subarray(0, remaining), total);
|
|
50
|
+
ignoreCancellation(() => reader.cancel());
|
|
51
|
+
if (!truncate) throw new Error("MCP response exceeds size limit");
|
|
52
|
+
return `${new TextDecoder().decode(buffer)}\u2026`;
|
|
53
|
+
}
|
|
54
|
+
if (total + value.length > buffer.length) {
|
|
55
|
+
const grown = new Uint8Array(Math.min(limit, Math.max(total + value.length, buffer.length * 2)));
|
|
56
|
+
grown.set(buffer);
|
|
57
|
+
buffer = grown;
|
|
58
|
+
}
|
|
59
|
+
buffer.set(value, total);
|
|
60
|
+
total += value.length;
|
|
61
|
+
}
|
|
62
|
+
return new TextDecoder().decode(buffer.subarray(0, total));
|
|
63
|
+
} catch (error) {
|
|
64
|
+
ignoreCancellation(() => reader.cancel());
|
|
65
|
+
throw error;
|
|
66
|
+
} finally {
|
|
67
|
+
try {
|
|
68
|
+
reader.releaseLock();
|
|
69
|
+
} catch {}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Lightweight utilities for calling MCP servers directly via HTTP
|
|
5
5
|
* without maintaining persistent connections.
|
|
6
6
|
*/
|
|
7
|
+
// biome-ignore assist/source/organizeImports: Keep independent MCP security imports on separate merge anchors.
|
|
8
|
+
import { cancelMCPStream, MCP_HTTP_TIMEOUT_MS, MCP_MAX_CONTENT_BYTES, readMCPResponseText } from "./content-limits";
|
|
7
9
|
import { logger } from "@gajae-code/utils";
|
|
8
10
|
|
|
9
11
|
/** Parse SSE response format (lines starting with "data: ") */
|
|
@@ -25,6 +27,15 @@ export function parseSSE(text: string): unknown {
|
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
async function translateMCPTimeout<T>(operation: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
31
|
+
try {
|
|
32
|
+
return await operation;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (signal.aborted && error === signal.reason) throw new Error("MCP request timed out");
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
/** JSON-RPC 2.0 response structure */
|
|
29
40
|
export interface JsonRpcResponse<T = unknown> {
|
|
30
41
|
jsonrpc: "2.0";
|
|
@@ -57,22 +68,25 @@ export async function callMCP<T = unknown>(
|
|
|
57
68
|
params: params ?? {},
|
|
58
69
|
};
|
|
59
70
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
Accept: "application/json, text/event-stream",
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
const signal = AbortSignal.timeout(MCP_HTTP_TIMEOUT_MS);
|
|
72
|
+
const response = await translateMCPTimeout(
|
|
73
|
+
fetch(url, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream" },
|
|
76
|
+
body: JSON.stringify(body),
|
|
77
|
+
signal,
|
|
78
|
+
}),
|
|
79
|
+
signal,
|
|
80
|
+
);
|
|
68
81
|
|
|
69
82
|
if (!response.ok) {
|
|
83
|
+
cancelMCPStream(response.body);
|
|
70
84
|
const errorMsg = `MCP request failed: ${response.status} ${response.statusText}`;
|
|
71
85
|
logger.error(errorMsg, { url, method, params });
|
|
72
86
|
throw new Error(errorMsg);
|
|
73
87
|
}
|
|
74
88
|
|
|
75
|
-
const text = await response
|
|
89
|
+
const text = await translateMCPTimeout(readMCPResponseText(response, MCP_MAX_CONTENT_BYTES, false, signal), signal);
|
|
76
90
|
const result = parseSSE(text) as JsonRpcResponse<T> | null;
|
|
77
91
|
|
|
78
92
|
if (!result) {
|
|
@@ -16,6 +16,14 @@ import type {
|
|
|
16
16
|
MCPTransport,
|
|
17
17
|
} from "../../runtime-mcp/types";
|
|
18
18
|
import { MCPExpectedFailure, toJsonRpcError } from "../../runtime-mcp/types";
|
|
19
|
+
import {
|
|
20
|
+
cancelMCPStream,
|
|
21
|
+
MCP_MAX_CONTENT_BYTES,
|
|
22
|
+
MCP_MAX_ERROR_BYTES,
|
|
23
|
+
MCP_MAX_SSE_BATCH_MESSAGES,
|
|
24
|
+
MCP_MAX_SSE_REQUEST_MESSAGES,
|
|
25
|
+
readMCPResponseText,
|
|
26
|
+
} from "../content-limits";
|
|
19
27
|
|
|
20
28
|
/**
|
|
21
29
|
* HTTP transport for MCP servers.
|
|
@@ -73,6 +81,9 @@ export class HttpTransport implements MCPTransport {
|
|
|
73
81
|
if (this.#sseConnection) return;
|
|
74
82
|
|
|
75
83
|
const sseConnection = new AbortController();
|
|
84
|
+
const headerController = new AbortController();
|
|
85
|
+
const headerTimeout = this.config.timeout ?? 30000;
|
|
86
|
+
const headerTimeoutId = setTimeout(() => headerController.abort(), headerTimeout);
|
|
76
87
|
this.#sseConnection = sseConnection;
|
|
77
88
|
const headers: Record<string, string> = {
|
|
78
89
|
Accept: "text/event-stream",
|
|
@@ -88,18 +99,22 @@ export class HttpTransport implements MCPTransport {
|
|
|
88
99
|
response = await fetch(this.config.url, {
|
|
89
100
|
method: "GET",
|
|
90
101
|
headers,
|
|
91
|
-
signal: sseConnection.signal,
|
|
102
|
+
signal: AbortSignal.any([sseConnection.signal, headerController.signal]),
|
|
92
103
|
});
|
|
93
104
|
} catch (error) {
|
|
94
105
|
this.#sseConnection = this.#sseConnection === sseConnection ? null : this.#sseConnection;
|
|
95
|
-
if (
|
|
106
|
+
if (headerController.signal.aborted && !sseConnection.signal.aborted) {
|
|
107
|
+
this.onError?.(new Error(`SSE connection timeout after ${headerTimeout}ms`));
|
|
108
|
+
} else if (error instanceof Error && error.name !== "AbortError") {
|
|
96
109
|
this.onError?.(error);
|
|
97
110
|
}
|
|
98
111
|
return;
|
|
112
|
+
} finally {
|
|
113
|
+
clearTimeout(headerTimeoutId);
|
|
99
114
|
}
|
|
100
115
|
|
|
101
116
|
if (response.status === 405 || !response.ok || !response.body) {
|
|
102
|
-
|
|
117
|
+
cancelMCPStream(response.body);
|
|
103
118
|
this.#sseConnection = this.#sseConnection === sseConnection ? null : this.#sseConnection;
|
|
104
119
|
return;
|
|
105
120
|
}
|
|
@@ -117,8 +132,13 @@ export class HttpTransport implements MCPTransport {
|
|
|
117
132
|
}
|
|
118
133
|
async #readSSEStream(body: ReadableStream<Uint8Array>, signal: AbortSignal): Promise<void> {
|
|
119
134
|
try {
|
|
120
|
-
for await (const message of readSseJson<JsonRpcMessage>(body, signal
|
|
135
|
+
for await (const message of readSseJson<JsonRpcMessage>(body, signal, undefined, {
|
|
136
|
+
maxEventBytes: MCP_MAX_CONTENT_BYTES,
|
|
137
|
+
})) {
|
|
121
138
|
if (!this.#connected) break;
|
|
139
|
+
if (Array.isArray(message) && message.length > MCP_MAX_SSE_BATCH_MESSAGES) {
|
|
140
|
+
throw new Error("MCP SSE batch exceeds message limit");
|
|
141
|
+
}
|
|
122
142
|
this.#dispatchSSEMessage(message);
|
|
123
143
|
}
|
|
124
144
|
} catch (error) {
|
|
@@ -221,7 +241,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
221
241
|
}
|
|
222
242
|
|
|
223
243
|
if (!response.ok) {
|
|
224
|
-
const text = await response
|
|
244
|
+
const text = await readMCPResponseText(response, MCP_MAX_ERROR_BYTES, true, operationSignal);
|
|
225
245
|
const wwwAuthenticate = response.headers.get("WWW-Authenticate");
|
|
226
246
|
const mcpAuthServer = response.headers.get("Mcp-Auth-Server");
|
|
227
247
|
const authHints = [
|
|
@@ -245,7 +265,9 @@ export class HttpTransport implements MCPTransport {
|
|
|
245
265
|
if (!response.body) {
|
|
246
266
|
throw new MCPExpectedFailure();
|
|
247
267
|
}
|
|
248
|
-
const parsedResult =
|
|
268
|
+
const parsedResult = JSON.parse(
|
|
269
|
+
await readMCPResponseText(response, MCP_MAX_CONTENT_BYTES, false, operationSignal),
|
|
270
|
+
) as unknown;
|
|
249
271
|
|
|
250
272
|
if (
|
|
251
273
|
typeof parsedResult !== "object" ||
|
|
@@ -293,6 +315,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
293
315
|
|
|
294
316
|
const { promise, resolve, reject } = Promise.withResolvers<T>();
|
|
295
317
|
let captured = false;
|
|
318
|
+
let messageCount = 0;
|
|
296
319
|
|
|
297
320
|
// Drain this per-request SSE response from a single iterator. Once the
|
|
298
321
|
// matching response arrives, resolve/reject and abort the reader so the
|
|
@@ -304,8 +327,20 @@ export class HttpTransport implements MCPTransport {
|
|
|
304
327
|
this.#streamControllers.add(drainController);
|
|
305
328
|
const drain = async (): Promise<void> => {
|
|
306
329
|
try {
|
|
307
|
-
for await (const raw of readSseJson<JsonRpcMessage | JsonRpcMessage[]>(
|
|
330
|
+
for await (const raw of readSseJson<JsonRpcMessage | JsonRpcMessage[]>(
|
|
331
|
+
response.body!,
|
|
332
|
+
operationSignal,
|
|
333
|
+
undefined,
|
|
334
|
+
{
|
|
335
|
+
maxEventBytes: MCP_MAX_CONTENT_BYTES,
|
|
336
|
+
maxTotalBytes: MCP_MAX_CONTENT_BYTES,
|
|
337
|
+
},
|
|
338
|
+
)) {
|
|
308
339
|
const messages = Array.isArray(raw) ? raw : [raw];
|
|
340
|
+
if (messages.length > MCP_MAX_SSE_BATCH_MESSAGES) throw new Error("MCP SSE batch exceeds message limit");
|
|
341
|
+
messageCount += messages.length;
|
|
342
|
+
if (messageCount > MCP_MAX_SSE_REQUEST_MESSAGES)
|
|
343
|
+
throw new Error("MCP SSE response exceeds message limit");
|
|
309
344
|
for (const message of messages) {
|
|
310
345
|
if (
|
|
311
346
|
!captured &&
|
|
@@ -352,7 +387,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
352
387
|
} finally {
|
|
353
388
|
clearTimeout(timeoutId);
|
|
354
389
|
this.#streamControllers.delete(drainController);
|
|
355
|
-
|
|
390
|
+
cancelMCPStream(response.body);
|
|
356
391
|
}
|
|
357
392
|
};
|
|
358
393
|
|
|
@@ -396,7 +431,7 @@ export class HttpTransport implements MCPTransport {
|
|
|
396
431
|
});
|
|
397
432
|
// Retry once on auth failure if onAuthError is wired
|
|
398
433
|
if (this.onAuthError && (resp.status === 401 || resp.status === 403)) {
|
|
399
|
-
|
|
434
|
+
cancelMCPStream(resp.body);
|
|
400
435
|
const newHeaders = await this.onAuthError();
|
|
401
436
|
if (newHeaders) {
|
|
402
437
|
this.config.headers ??= {};
|
|
@@ -408,11 +443,11 @@ export class HttpTransport implements MCPTransport {
|
|
|
408
443
|
body: JSON.stringify(body),
|
|
409
444
|
signal: AbortSignal.timeout(this.config.timeout ?? 30000),
|
|
410
445
|
});
|
|
411
|
-
|
|
446
|
+
cancelMCPStream(retry.body);
|
|
412
447
|
return;
|
|
413
448
|
}
|
|
414
449
|
}
|
|
415
|
-
|
|
450
|
+
cancelMCPStream(resp.body);
|
|
416
451
|
} catch {
|
|
417
452
|
// Best-effort response delivery — server may have disconnected
|
|
418
453
|
}
|
|
@@ -452,11 +487,9 @@ export class HttpTransport implements MCPTransport {
|
|
|
452
487
|
signal: abortController.signal,
|
|
453
488
|
});
|
|
454
489
|
|
|
455
|
-
clearTimeout(timeoutId);
|
|
456
|
-
|
|
457
490
|
// 202 Accepted is success for notifications
|
|
458
491
|
if (!response.ok && response.status !== 202) {
|
|
459
|
-
const text = await response.
|
|
492
|
+
const text = await readMCPResponseText(response, MCP_MAX_ERROR_BYTES, true, abortController.signal);
|
|
460
493
|
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
461
494
|
}
|
|
462
495
|
|
|
@@ -472,14 +505,15 @@ export class HttpTransport implements MCPTransport {
|
|
|
472
505
|
const reader = this.#readSSEStream(response.body, AbortSignal.any(signals));
|
|
473
506
|
this.#trackReader(reader, streamController);
|
|
474
507
|
} else {
|
|
475
|
-
|
|
508
|
+
cancelMCPStream(response.body);
|
|
476
509
|
}
|
|
477
510
|
} catch (error) {
|
|
478
|
-
clearTimeout(timeoutId);
|
|
479
511
|
if (error instanceof Error && error.name === "AbortError") {
|
|
480
512
|
throw new MCPExpectedFailure(new Error(`Notify timeout after ${timeout}ms`));
|
|
481
513
|
}
|
|
482
514
|
throw error instanceof MCPExpectedFailure ? error : new MCPExpectedFailure(error);
|
|
515
|
+
} finally {
|
|
516
|
+
clearTimeout(timeoutId);
|
|
483
517
|
}
|
|
484
518
|
}
|
|
485
519
|
|
|
@@ -25,10 +25,14 @@ export type ChatDaemonAction = "stop" | "reload";
|
|
|
25
25
|
* Operational generations of the Discord/Slack daemon lifecycle contracts.
|
|
26
26
|
* These are intentionally separate from per-session endpoint generations.
|
|
27
27
|
* Generation 6 carries the retained managed filesystem authority boundary.
|
|
28
|
+
* Generation 7 restores macOS daemon signaling (kill(2) with a start-time
|
|
29
|
+
* incarnation recheck) so a live/hung owner can be replaced without an external
|
|
30
|
+
* `kill -9`. Generation 8 adopts Windows expected-identity ACL verification and
|
|
31
|
+
* repair for shared native authority.
|
|
28
32
|
*/
|
|
29
33
|
export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
|
|
30
|
-
discord:
|
|
31
|
-
slack:
|
|
34
|
+
discord: 8,
|
|
35
|
+
slack: 8,
|
|
32
36
|
};
|
|
33
37
|
|
|
34
38
|
export function chatDaemonGeneration(kind: ChatDaemonKind): number {
|
|
@@ -166,18 +170,27 @@ export interface ChatDaemonProcessReference {
|
|
|
166
170
|
}
|
|
167
171
|
|
|
168
172
|
function defaultProcessReference(pid: number, platform = os.platform()): ChatDaemonProcessReference | undefined {
|
|
169
|
-
// Process.fromPid on Darwin still resolves identity and then signals a numeric
|
|
170
|
-
// PID. A process can exit and its PID be reused in that interval, so daemon
|
|
171
|
-
// control must not use it as privileged signaling authority.
|
|
172
|
-
if (platform === "darwin") return undefined;
|
|
173
173
|
try {
|
|
174
174
|
const processRef = Process.fromPid(pid);
|
|
175
175
|
if (!processRef || !hasProcessIncarnationAuthority(processRef.incarnation)) return undefined;
|
|
176
|
+
const incarnation = processRef.incarnation;
|
|
176
177
|
return {
|
|
177
|
-
incarnation
|
|
178
|
+
incarnation,
|
|
178
179
|
signalRoot: signal => {
|
|
179
180
|
const nativeSignal = os.constants.signals[signal];
|
|
180
181
|
if (nativeSignal === undefined) throw new Error(`Unsupported signal: ${signal}`);
|
|
182
|
+
// macOS exposes no pidfd and the native signal_root is a no-op there, so
|
|
183
|
+
// the daemon control plane previously had NO way to signal a live owner —
|
|
184
|
+
// every stop/reload of a live/hung daemon refused, and only an external
|
|
185
|
+
// `kill -9` could recover it. Signal by numeric PID via kill(2), but
|
|
186
|
+
// re-read the immutable start-time incarnation immediately beforehand so a
|
|
187
|
+
// PID that exited and was reused since capture is never signaled.
|
|
188
|
+
if (platform === "darwin") {
|
|
189
|
+
const current = Process.fromPid(pid) as { incarnation?: unknown } | null;
|
|
190
|
+
if (!current || current.incarnation !== incarnation) throw new Error("Pinned process is already gone");
|
|
191
|
+
process.kill(pid, signal);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
181
194
|
const rootProcess = processRef as typeof processRef & { signalRoot(signal: number): boolean };
|
|
182
195
|
if (!rootProcess.signalRoot(nativeSignal)) throw new Error("Pinned process is already gone");
|
|
183
196
|
},
|
package/src/sdk/bus/index.ts
CHANGED
|
@@ -30,7 +30,7 @@ import { promisify } from "node:util";
|
|
|
30
30
|
import { ThinkingLevel } from "@gajae-code/agent-core";
|
|
31
31
|
import type { ImageContent, TextContent, Tool } from "@gajae-code/ai";
|
|
32
32
|
import { NotificationServer, nativeBuildInfo } from "@gajae-code/natives";
|
|
33
|
-
import { logger, postmortem,
|
|
33
|
+
import { logger, postmortem, VERSION } from "@gajae-code/utils";
|
|
34
34
|
import { Settings } from "../../config/settings";
|
|
35
35
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
|
|
36
36
|
import { toAgentWireEventPayload } from "../../modes/shared/agent-wire/event-envelope";
|
|
@@ -40,7 +40,6 @@ import {
|
|
|
40
40
|
type WorkflowGateTerminalController,
|
|
41
41
|
type WorkflowGateTerminalProof,
|
|
42
42
|
} from "../../modes/shared/agent-wire/workflow-gate-broker";
|
|
43
|
-
import btwUserPrompt from "../../prompts/system/btw-user.md" with { type: "text" };
|
|
44
43
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
45
44
|
import { parseThinkingLevel } from "../../thinking";
|
|
46
45
|
import type {
|
|
@@ -1926,8 +1925,10 @@ function sdkControlSurface(
|
|
|
1926
1925
|
}
|
|
1927
1926
|
return "unknown";
|
|
1928
1927
|
};
|
|
1929
|
-
const sendSteer = (text: string) => {
|
|
1930
|
-
|
|
1928
|
+
const sendSteer = async (text: string) => {
|
|
1929
|
+
// Await admission so a rejection (e.g. handoff in progress) surfaces as a
|
|
1930
|
+
// control error instead of a false `accepted: true`.
|
|
1931
|
+
await api.sendUserMessage(text, { deliverAs: "steer" });
|
|
1931
1932
|
return { commandId: crypto.randomUUID(), accepted: true };
|
|
1932
1933
|
};
|
|
1933
1934
|
const resolveModel = (id: string) => {
|
|
@@ -2776,7 +2777,7 @@ export function createNotificationsExtension(
|
|
|
2776
2777
|
sdkHostModeSupported?: boolean;
|
|
2777
2778
|
|
|
2778
2779
|
onSdkRequest?: (kind: "control" | "query", connectionId: string, frame: Record<string, unknown>) => void;
|
|
2779
|
-
|
|
2780
|
+
runBtwTurn?: (question: string, signal: AbortSignal) => Promise<{ replyText: string }>;
|
|
2780
2781
|
/** Observes settlement of optional session-branch startup after reconciliation completes. */
|
|
2781
2782
|
onBranchStartupSettled?: (receipt: { sessionId: string; status: SessionStartResult["status"] }) => void;
|
|
2782
2783
|
readNotificationFile?: (path: string) => Promise<Buffer>;
|
|
@@ -3585,10 +3586,10 @@ export function createNotificationsExtension(
|
|
|
3585
3586
|
};
|
|
3586
3587
|
|
|
3587
3588
|
const ephemeralTurns = new EphemeralTurnHost(sendSdkFrame, async (question, signal) => {
|
|
3588
|
-
if (!options.
|
|
3589
|
+
if (!options.runBtwTurn) throw new Error("Ephemeral turns are unavailable.");
|
|
3589
3590
|
const generation = initializedRuntime.policyGeneration;
|
|
3590
3591
|
if (initializedRuntime.policySuspended) throw new Error("Notification policy is provisional.");
|
|
3591
|
-
const result = await options.
|
|
3592
|
+
const result = await options.runBtwTurn(question, signal);
|
|
3592
3593
|
if (
|
|
3593
3594
|
initializedRuntime.policySuspended ||
|
|
3594
3595
|
initializedRuntime.policyGeneration !== generation ||
|
|
@@ -238,11 +238,12 @@ function isSafeBranch(value: string): boolean {
|
|
|
238
238
|
/**
|
|
239
239
|
* Map a lifecycle response/error to a user-facing Telegram message (G010).
|
|
240
240
|
*
|
|
241
|
-
* Only derives text from sessionId, mode, reason, a safe message,
|
|
242
|
-
* {sessionId,path} — never a token or prompt. Each
|
|
243
|
-
* actionable copy; an "in progress" pending response
|
|
241
|
+
* Only derives text from sessionId, mode, reason, a safe message, the originating
|
|
242
|
+
* lifecycle verb, and candidate {sessionId,path} — never a token or prompt. Each
|
|
243
|
+
* error reason gets tailored, actionable copy; an "in progress" pending response
|
|
244
|
+
* is surfaced distinctly.
|
|
244
245
|
*/
|
|
245
|
-
export function formatLifecycleOutcome(r: SessionLifecycleResponse): string {
|
|
246
|
+
export function formatLifecycleOutcome(r: SessionLifecycleResponse, verb?: LifecycleCommandVerb): string {
|
|
246
247
|
switch (r.type) {
|
|
247
248
|
case "session_create_response":
|
|
248
249
|
return `\u{1f680} Launching session ${r.sessionId} in tmux. It will appear once ready \u2014 check /session_recent.`;
|
|
@@ -282,9 +283,17 @@ export function formatLifecycleOutcome(r: SessionLifecycleResponse): string {
|
|
|
282
283
|
case "unsupported_platform":
|
|
283
284
|
return "Remote session lifecycle is unavailable on this psmux host because GJC cannot prove immutable session identity. No lifecycle action was performed. Use a local GJC terminal with a supported tmux provider.";
|
|
284
285
|
case "terminal_uncertain":
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
286
|
+
if (/in progress/i.test(r.message)) return "\u23f3 That request is already in progress \u2014 hold on.";
|
|
287
|
+
switch (verb) {
|
|
288
|
+
case "session_create":
|
|
289
|
+
return "\u26a0\ufe0f Create outcome uncertain. The session may already be starting \u2014 check /session_recent before retrying to avoid starting it twice.";
|
|
290
|
+
case "session_close":
|
|
291
|
+
return "\u26a0\ufe0f Close outcome uncertain. The session may already be closed \u2014 check /session_recent before retrying.";
|
|
292
|
+
case "session_resume":
|
|
293
|
+
return "\u26a0\ufe0f Resume outcome uncertain. The session may already be reattached or restarting \u2014 check /session_recent before retrying.";
|
|
294
|
+
default:
|
|
295
|
+
return "\u26a0\ufe0f Outcome uncertain. Check /session_recent before retrying.";
|
|
296
|
+
}
|
|
288
297
|
default:
|
|
289
298
|
return `\u26a0\ufe0f ${r.reason}: ${r.message}`;
|
|
290
299
|
}
|