@gajae-code/coding-agent 0.11.6 → 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 +56 -6
- 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 +2 -1
- 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 +8 -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 +25 -4
- 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 +2 -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 +57 -6
- 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 +8 -7
- package/src/internal-urls/local-protocol.ts +32 -6
- package/src/main.ts +86 -27
- 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/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/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 +4 -3
- package/src/sdk/bus/index.ts +4 -5
- 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 +8 -3
- package/src/sdk/bus/telegram-daemon-control.ts +2 -2
- package/src/sdk/bus/telegram-daemon.ts +1663 -375
- 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/protocol/operation-inventory.generated.json +11 -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 +240 -149
- 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 +319 -85
- 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 +1 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- 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
|
@@ -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
|
|
|
@@ -27,11 +27,12 @@ export type ChatDaemonAction = "stop" | "reload";
|
|
|
27
27
|
* Generation 6 carries the retained managed filesystem authority boundary.
|
|
28
28
|
* Generation 7 restores macOS daemon signaling (kill(2) with a start-time
|
|
29
29
|
* incarnation recheck) so a live/hung owner can be replaced without an external
|
|
30
|
-
* `kill -9`.
|
|
30
|
+
* `kill -9`. Generation 8 adopts Windows expected-identity ACL verification and
|
|
31
|
+
* repair for shared native authority.
|
|
31
32
|
*/
|
|
32
33
|
export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
|
|
33
|
-
discord:
|
|
34
|
-
slack:
|
|
34
|
+
discord: 8,
|
|
35
|
+
slack: 8,
|
|
35
36
|
};
|
|
36
37
|
|
|
37
38
|
export function chatDaemonGeneration(kind: ChatDaemonKind): number {
|
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 {
|
|
@@ -2778,7 +2777,7 @@ export function createNotificationsExtension(
|
|
|
2778
2777
|
sdkHostModeSupported?: boolean;
|
|
2779
2778
|
|
|
2780
2779
|
onSdkRequest?: (kind: "control" | "query", connectionId: string, frame: Record<string, unknown>) => void;
|
|
2781
|
-
|
|
2780
|
+
runBtwTurn?: (question: string, signal: AbortSignal) => Promise<{ replyText: string }>;
|
|
2782
2781
|
/** Observes settlement of optional session-branch startup after reconciliation completes. */
|
|
2783
2782
|
onBranchStartupSettled?: (receipt: { sessionId: string; status: SessionStartResult["status"] }) => void;
|
|
2784
2783
|
readNotificationFile?: (path: string) => Promise<Buffer>;
|
|
@@ -3587,10 +3586,10 @@ export function createNotificationsExtension(
|
|
|
3587
3586
|
};
|
|
3588
3587
|
|
|
3589
3588
|
const ephemeralTurns = new EphemeralTurnHost(sendSdkFrame, async (question, signal) => {
|
|
3590
|
-
if (!options.
|
|
3589
|
+
if (!options.runBtwTurn) throw new Error("Ephemeral turns are unavailable.");
|
|
3591
3590
|
const generation = initializedRuntime.policyGeneration;
|
|
3592
3591
|
if (initializedRuntime.policySuspended) throw new Error("Notification policy is provisional.");
|
|
3593
|
-
const result = await options.
|
|
3592
|
+
const result = await options.runBtwTurn(question, signal);
|
|
3594
3593
|
if (
|
|
3595
3594
|
initializedRuntime.policySuspended ||
|
|
3596
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
|
}
|
|
@@ -251,19 +251,29 @@ export function formatNotificationStatusReport(report: NotificationStatusReport)
|
|
|
251
251
|
|
|
252
252
|
// --- endpoint / daemon file readers -------------------------------------
|
|
253
253
|
|
|
254
|
-
interface
|
|
254
|
+
export interface NotificationEndpointView {
|
|
255
255
|
sessionId: string;
|
|
256
256
|
pid: number | undefined;
|
|
257
257
|
stale: boolean;
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
type
|
|
261
|
-
|
|
260
|
+
export type NotificationEndpointLiveness = "live" | "dead" | "unknown";
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Classification used by recovery and startup takeover. A file is an endpoint
|
|
264
|
+
* only when it has endpoint authority fields; lifecycle/audit records are never
|
|
265
|
+
* candidates for endpoint cleanup.
|
|
266
|
+
*/
|
|
267
|
+
export type NotificationEndpointClassification =
|
|
268
|
+
| {
|
|
269
|
+
kind: "endpoint";
|
|
270
|
+
view: NotificationEndpointView;
|
|
271
|
+
liveness: NotificationEndpointLiveness;
|
|
272
|
+
identity: NotificationEndpointFileIdentity;
|
|
273
|
+
}
|
|
262
274
|
| { kind: "non-endpoint" }
|
|
263
275
|
| { kind: "unreadable" };
|
|
264
276
|
|
|
265
|
-
type EndpointLiveness = "live" | "dead" | "unknown";
|
|
266
|
-
|
|
267
277
|
/**
|
|
268
278
|
* Classify an endpoint using owner-proof semantics. An endpoint is only `dead`
|
|
269
279
|
* with positive proof: an explicit `stale` tombstone, or a recorded pid that is
|
|
@@ -271,7 +281,10 @@ type EndpointLiveness = "live" | "dead" | "unknown";
|
|
|
271
281
|
* must never be treated as dead — removing it could delete a live session's
|
|
272
282
|
* discovery file that simply omitted a pid.
|
|
273
283
|
*/
|
|
274
|
-
function
|
|
284
|
+
export function notificationEndpointLiveness(
|
|
285
|
+
view: NotificationEndpointView,
|
|
286
|
+
pidAlive: (pid: number) => boolean,
|
|
287
|
+
): NotificationEndpointLiveness {
|
|
275
288
|
if (view.stale) return "dead";
|
|
276
289
|
if (view.pid === undefined) return "unknown";
|
|
277
290
|
return pidAlive(view.pid) ? "live" : "dead";
|
|
@@ -314,11 +327,19 @@ function isCanonicalLifecycleArtifactName(name: string): boolean {
|
|
|
314
327
|
);
|
|
315
328
|
}
|
|
316
329
|
|
|
317
|
-
function unreadableEndpointResult(file: string):
|
|
330
|
+
function unreadableEndpointResult(file: string): NotificationEndpointClassification {
|
|
318
331
|
return isCanonicalLifecycleArtifactName(path.basename(file)) ? { kind: "non-endpoint" } : { kind: "unreadable" };
|
|
319
332
|
}
|
|
320
333
|
|
|
321
|
-
|
|
334
|
+
/**
|
|
335
|
+
* Read and classify one endpoint candidate. The returned identity belongs to
|
|
336
|
+
* exactly the bytes inspected and is required for any later deletion.
|
|
337
|
+
*/
|
|
338
|
+
export async function classifyNotificationEndpoint(
|
|
339
|
+
fs: Pick<NotificationServiceFs, "readEndpointFile">,
|
|
340
|
+
file: string,
|
|
341
|
+
pidAlive: (pid: number) => boolean,
|
|
342
|
+
): Promise<NotificationEndpointClassification> {
|
|
322
343
|
let endpoint: NotificationEndpointFile;
|
|
323
344
|
let raw: string;
|
|
324
345
|
try {
|
|
@@ -334,16 +355,19 @@ async function readEndpointView(fs: NotificationServiceFs, file: string): Promis
|
|
|
334
355
|
return unreadableEndpointResult(file);
|
|
335
356
|
}
|
|
336
357
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { kind: "non-endpoint" };
|
|
358
|
+
if (path.basename(file) === "broker.json") return { kind: "non-endpoint" };
|
|
337
359
|
const rec = parsed as Record<string, unknown>;
|
|
338
360
|
if (isLifecycleArtifact(rec) || typeof rec.url !== "string" || typeof rec.token !== "string")
|
|
339
361
|
return { kind: "non-endpoint" };
|
|
362
|
+
const view: NotificationEndpointView = {
|
|
363
|
+
sessionId: typeof rec.sessionId === "string" ? rec.sessionId : path.basename(file, ".json"),
|
|
364
|
+
pid: safePositiveInteger(rec.pid),
|
|
365
|
+
stale: rec.stale === true,
|
|
366
|
+
};
|
|
340
367
|
return {
|
|
341
368
|
kind: "endpoint",
|
|
342
|
-
view
|
|
343
|
-
|
|
344
|
-
pid: safePositiveInteger(rec.pid),
|
|
345
|
-
stale: rec.stale === true,
|
|
346
|
-
},
|
|
369
|
+
view,
|
|
370
|
+
liveness: notificationEndpointLiveness(view, pidAlive),
|
|
347
371
|
identity: endpoint.identity,
|
|
348
372
|
};
|
|
349
373
|
}
|
|
@@ -604,14 +628,13 @@ export async function checkNotificationHealth(opts: HealthOptions): Promise<Noti
|
|
|
604
628
|
let unknownEndpoints = 0;
|
|
605
629
|
let unreadable = 0;
|
|
606
630
|
for (const name of files) {
|
|
607
|
-
const record = await
|
|
631
|
+
const record = await classifyNotificationEndpoint(fs, path.join(dir, name), pidAlive);
|
|
608
632
|
if (record.kind === "non-endpoint") continue;
|
|
609
633
|
if (record.kind === "unreadable") {
|
|
610
634
|
unreadable += 1;
|
|
611
635
|
continue;
|
|
612
636
|
}
|
|
613
|
-
|
|
614
|
-
switch (endpointLiveness(view, pidAlive)) {
|
|
637
|
+
switch (record.liveness) {
|
|
615
638
|
case "live":
|
|
616
639
|
live += 1;
|
|
617
640
|
break;
|
|
@@ -1040,7 +1063,7 @@ export async function recoverNotifications(opts: RecoveryOptions): Promise<Notif
|
|
|
1040
1063
|
let unreadable = 0;
|
|
1041
1064
|
for (const name of files) {
|
|
1042
1065
|
const file = path.join(dir, name);
|
|
1043
|
-
const record = await
|
|
1066
|
+
const record = await classifyNotificationEndpoint(fs, file, pidAlive);
|
|
1044
1067
|
if (record.kind === "non-endpoint") continue;
|
|
1045
1068
|
if (record.kind === "unreadable") {
|
|
1046
1069
|
// Leave unparseable files untouched: they may be mid-write by a live server.
|
|
@@ -1048,7 +1071,7 @@ export async function recoverNotifications(opts: RecoveryOptions): Promise<Notif
|
|
|
1048
1071
|
continue;
|
|
1049
1072
|
}
|
|
1050
1073
|
const view = record.view;
|
|
1051
|
-
if (
|
|
1074
|
+
if (record.liveness !== "dead") {
|
|
1052
1075
|
// Keep live AND unknown (PID-less) endpoints: only positive proof of
|
|
1053
1076
|
// death (a stale tombstone or a dead pid) authorizes removal.
|
|
1054
1077
|
kept += 1;
|
|
@@ -21,7 +21,12 @@ export const NOTIFICATION_PROTOCOL_VERSION = 3;
|
|
|
21
21
|
* confirmed-dead owner's lock in generation 10, legacy stopped-tombstone
|
|
22
22
|
* reclamation in generation 11, force-escalated SIGKILL of an unresponsive
|
|
23
23
|
* older-generation owner during automatic generation-upgrade reload in
|
|
24
|
-
* generation 12,
|
|
25
|
-
* incarnation recheck, replacing the darwin no-op) in generation 13
|
|
24
|
+
* generation 12, restored macOS daemon signaling (kill(2) with a start-time
|
|
25
|
+
* incarnation recheck, replacing the darwin no-op) in generation 13, retained
|
|
26
|
+
* legacy stopped-lock reclamation in generation 14, Windows expected-identity
|
|
27
|
+
* ACL verification and repair in generation 15, identity-fenced stale endpoint
|
|
28
|
+
* startup recovery in generation 16, Telegram topic recovery authority fencing
|
|
29
|
+
* in generation 17, and fail-closed blank-token validation plus lifecycle-startup
|
|
30
|
+
* stop fencing in generation 18.
|
|
26
31
|
*/
|
|
27
|
-
export const DAEMON_GENERATION =
|
|
32
|
+
export const DAEMON_GENERATION = 18;
|