@agentconnect.md/daemon 1.0.0-rc.233 → 1.0.0-rc.235
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/dist/{acp-host-CF3j4yST.js → acp-host-D9z7hKHJ.js} +146 -19
- package/dist/acp-host-D9z7hKHJ.js.map +1 -0
- package/dist/{chat-CGFDCD-I.js → chat-ca2lVcnb.js} +2 -2
- package/dist/{chat-CGFDCD-I.js.map → chat-ca2lVcnb.js.map} +1 -1
- package/dist/index.js +3 -3
- package/dist/{login-Bchp4kkl.js → login-UiyjTM47.js} +7 -4
- package/dist/login-UiyjTM47.js.map +1 -0
- package/dist/{run-foreground-DrL_bDro.js → run-foreground-B5RyE_L1.js} +1265 -1042
- package/dist/run-foreground-B5RyE_L1.js.map +1 -0
- package/dist/run-foreground-CSpqnnI0.js +2 -0
- package/package.json +1 -1
- package/dist/acp-host-CF3j4yST.js.map +0 -1
- package/dist/login-Bchp4kkl.js.map +0 -1
- package/dist/run-foreground-CgCBnrA0.js +0 -2
- package/dist/run-foreground-DrL_bDro.js.map +0 -1
|
@@ -4,13 +4,13 @@ import { t as DAEMON_VERSION } from "./version-DGaRXjBK.js";
|
|
|
4
4
|
import { C as _coercedNumber, T as NEVER, a as array, d as literal, f as looseObject, i as any, m as object, o as boolean, p as number$1, t as ZodNumber, v as string, x as url$4 } from "./schemas-CQkO9hC5.js";
|
|
5
5
|
import { a as agentChildEnv, i as selectAgent, n as findAgentFiles, o as loadConfig, r as loadAgents, s as persistDaemonId } from "./load-agents-tb8Rcwan.js";
|
|
6
6
|
import { l as resolveRoot, o as mcpSocketPath, u as statePath } from "./paths-DEcUO8lC.js";
|
|
7
|
-
import { _ as
|
|
7
|
+
import { C as resolveRuntimes, S as cachedRuntimeVersions, _ as encode, a as initGitInjection, b as RESERVED_MCP_SERVER_NAME, c as simpleGit, d as RELAY_DAEMON_SUBPROTOCOL, f as RELAY_DAEMON_WS_PATH, g as decodeEnvelope, h as buildEnvelope, i as prepareWorkspace, m as decodeRelayDaemonFrame, n as installedRuntimes, o as probeGitVersion, p as buildRelayDaemonFrame, r as prefetchWorkspace, s as sessionGitEnv, t as AcpHost, u as normalizeGitUrl, v as MAX_FRAME_BYTES, x as cachedRuntimeNames, y as MEMORY_INDEX } from "./acp-host-D9z7hKHJ.js";
|
|
8
8
|
import { i as GitCredentialCache, n as gitcredSocketPath, r as writeGitcredShim, t as GitCredServer } from "./gitcred-server-Dcsq6QJe.js";
|
|
9
9
|
import { B as isInitializedNotification, C as JSONRPCMessageSchema, D as ListResourceTemplatesResultSchema, E as ListPromptsResultSchema, F as ReadResourceResultSchema, G as safeParse, H as isJSONRPCResultResponse, I as ResourceListChangedNotificationSchema, K as decodeFrames, L as SUPPORTED_PROTOCOL_VERSIONS, N as McpError, O as ListResourcesResultSchema, P as PromptListChangedNotificationSchema, T as ListChangedOptionsBaseSchema, U as getObjectShape, V as isJSONRPCRequest, W as isZ4Schema, _ as EmptyResultSchema, a as AjvJsonSchemaValidator, d as CreateMessageRequestSchema, f as CreateMessageResultSchema, g as ElicitResultSchema, h as ElicitRequestSchema, i as assertToolsCallTaskCapability, j as ListToolsResultSchema, l as CallToolResultSchema, m as CreateTaskResultSchema, n as serializeMessage, o as Protocol, p as CreateMessageResultWithToolsSchema, q as encodeFrame, r as assertClientRequestTaskCapability, s as mergeCapabilities, t as ReadBuffer, u as CompleteResultSchema, v as ErrorCode, w as LATEST_PROTOCOL_VERSION, x as InitializeResultSchema, y as GetPromptResultSchema, z as ToolListChangedNotificationSchema } from "./stdio-CwrB9yrX.js";
|
|
10
10
|
import { URL as URL$1 } from "url";
|
|
11
11
|
import * as tls from "tls";
|
|
12
12
|
import * as net$2 from "net";
|
|
13
|
-
import * as
|
|
13
|
+
import * as path$13 from "node:path";
|
|
14
14
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
15
15
|
import * as http$7 from "http";
|
|
16
16
|
import { Agent } from "https";
|
|
@@ -48,6 +48,145 @@ function number(params) {
|
|
|
48
48
|
return _coercedNumber(ZodNumber, params);
|
|
49
49
|
}
|
|
50
50
|
//#endregion
|
|
51
|
+
//#region ../connection/dist/clock.js
|
|
52
|
+
var SystemClock = class {
|
|
53
|
+
now() {
|
|
54
|
+
return Date.now();
|
|
55
|
+
}
|
|
56
|
+
setTimeout(fn, ms) {
|
|
57
|
+
return globalThis.setTimeout(fn, ms);
|
|
58
|
+
}
|
|
59
|
+
clearTimeout(h) {
|
|
60
|
+
globalThis.clearTimeout(h);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const systemClock = new SystemClock();
|
|
64
|
+
/**
|
|
65
|
+
* A small stateful backoff helper: `next()` yields the delay for the current
|
|
66
|
+
* attempt and advances the counter; `reset()` returns to attempt 0 (call on a
|
|
67
|
+
* successful connect). Keeps the exponent/jitter math in one place instead of
|
|
68
|
+
* inline in each client FSM.
|
|
69
|
+
*/
|
|
70
|
+
var Backoff = class {
|
|
71
|
+
attempt = 0;
|
|
72
|
+
baseMs;
|
|
73
|
+
capMs;
|
|
74
|
+
jitter;
|
|
75
|
+
constructor(opts = {}) {
|
|
76
|
+
this.baseMs = opts.baseMs ?? 1e3;
|
|
77
|
+
this.capMs = opts.capMs ?? 3e4;
|
|
78
|
+
this.jitter = opts.jitter ?? Math.random;
|
|
79
|
+
}
|
|
80
|
+
/** The delay for the current attempt, then advance (so the next call grows). */
|
|
81
|
+
next() {
|
|
82
|
+
const base = Math.min(this.capMs, this.baseMs * 2 ** this.attempt);
|
|
83
|
+
const delay = Math.min(this.capMs, base + Math.floor(this.jitter() * base));
|
|
84
|
+
this.attempt += 1;
|
|
85
|
+
return delay;
|
|
86
|
+
}
|
|
87
|
+
/** Reset to attempt 0 — call once a connect succeeds. */
|
|
88
|
+
reset() {
|
|
89
|
+
this.attempt = 0;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region ../connection/dist/correlator.js
|
|
94
|
+
/** Transport-free representation of a typed wire `error` REP. */
|
|
95
|
+
var WireError = class extends Error {
|
|
96
|
+
code;
|
|
97
|
+
retryable;
|
|
98
|
+
details;
|
|
99
|
+
constructor(code, message, retryable = false, details) {
|
|
100
|
+
super(message);
|
|
101
|
+
this.name = "WireError";
|
|
102
|
+
this.code = code;
|
|
103
|
+
this.retryable = retryable;
|
|
104
|
+
if (details) this.details = details;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
var ReqRep = class {
|
|
108
|
+
clock;
|
|
109
|
+
ackTimeoutMs;
|
|
110
|
+
maxTries;
|
|
111
|
+
pending = /* @__PURE__ */ new Map();
|
|
112
|
+
constructor(clock, ackTimeoutMs, maxTries = 5) {
|
|
113
|
+
this.clock = clock;
|
|
114
|
+
this.ackTimeoutMs = ackTimeoutMs;
|
|
115
|
+
this.maxTries = maxTries;
|
|
116
|
+
}
|
|
117
|
+
request(frame, write, opts = {}) {
|
|
118
|
+
const encoded = JSON.stringify(frame);
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
const entry = {
|
|
121
|
+
id: frame.id,
|
|
122
|
+
encoded,
|
|
123
|
+
resolve,
|
|
124
|
+
reject,
|
|
125
|
+
tries: 1,
|
|
126
|
+
maxTries: opts.maxTries ?? this.maxTries
|
|
127
|
+
};
|
|
128
|
+
this.pending.set(frame.id, entry);
|
|
129
|
+
this.arm(entry, write, opts.ackTimeoutMs ?? this.ackTimeoutMs);
|
|
130
|
+
try {
|
|
131
|
+
write(encoded);
|
|
132
|
+
} catch (e) {
|
|
133
|
+
this.fail(entry, new WireError("INTERNAL", `send failed: ${e.message}`, true));
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
arm(entry, write, ackTimeoutMs) {
|
|
138
|
+
entry.timer = this.clock.setTimeout(() => {
|
|
139
|
+
if (!this.pending.has(entry.id)) return;
|
|
140
|
+
if (entry.tries >= entry.maxTries) {
|
|
141
|
+
this.pending.delete(entry.id);
|
|
142
|
+
entry.reject(new WireError("INTERNAL", `no ack after ${entry.maxTries} tries for ${entry.id}`, true));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
entry.tries += 1;
|
|
146
|
+
this.arm(entry, write, ackTimeoutMs);
|
|
147
|
+
try {
|
|
148
|
+
write(entry.encoded);
|
|
149
|
+
} catch (e) {
|
|
150
|
+
this.fail(entry, new WireError("INTERNAL", `send failed: ${e.message}`, true));
|
|
151
|
+
}
|
|
152
|
+
}, ackTimeoutMs);
|
|
153
|
+
}
|
|
154
|
+
fail(entry, err) {
|
|
155
|
+
if (!this.pending.has(entry.id)) return;
|
|
156
|
+
this.pending.delete(entry.id);
|
|
157
|
+
if (entry.timer !== void 0) this.clock.clearTimeout(entry.timer);
|
|
158
|
+
entry.reject(err);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Settle a pending REQ from an inbound correlated frame. Returns true when
|
|
162
|
+
* `frame.corr` matched a pending REQ (a REP resolves it; an `error` frame
|
|
163
|
+
* rejects with a {@link WireError}), false when it correlates nothing.
|
|
164
|
+
*/
|
|
165
|
+
settle(frame) {
|
|
166
|
+
const corr = frame.corr;
|
|
167
|
+
if (!corr) return false;
|
|
168
|
+
const entry = this.pending.get(corr);
|
|
169
|
+
if (!entry) return false;
|
|
170
|
+
this.pending.delete(corr);
|
|
171
|
+
if (entry.timer !== void 0) this.clock.clearTimeout(entry.timer);
|
|
172
|
+
if (frame.type === "error") {
|
|
173
|
+
const e = frame.payload;
|
|
174
|
+
entry.reject(new WireError(e.code, e.message, e.retryable, e.details));
|
|
175
|
+
} else entry.resolve(frame);
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
inflight() {
|
|
179
|
+
return this.pending.size;
|
|
180
|
+
}
|
|
181
|
+
rejectAll(err) {
|
|
182
|
+
for (const entry of this.pending.values()) {
|
|
183
|
+
if (entry.timer !== void 0) this.clock.clearTimeout(entry.timer);
|
|
184
|
+
entry.reject(err);
|
|
185
|
+
}
|
|
186
|
+
this.pending.clear();
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
//#endregion
|
|
51
190
|
//#region ../../node_modules/.pnpm/ws@8.21.0/node_modules/ws/lib/constants.js
|
|
52
191
|
var require_constants$2 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
53
192
|
const BINARY_TYPES = [
|
|
@@ -3616,9 +3755,6 @@ var require_websocket_server = /* @__PURE__ */ __commonJSMin(((exports, module)
|
|
|
3616
3755
|
} else abortHandshake(socket, code, message, headers);
|
|
3617
3756
|
}
|
|
3618
3757
|
}));
|
|
3619
|
-
//#endregion
|
|
3620
|
-
//#region ../../node_modules/.pnpm/ws@8.21.0/node_modules/ws/wrapper.mjs
|
|
3621
|
-
var import_src = /* @__PURE__ */ __toESM(require_src$2(), 1);
|
|
3622
3758
|
require_stream();
|
|
3623
3759
|
require_extension();
|
|
3624
3760
|
require_permessage_deflate();
|
|
@@ -3629,25 +3765,25 @@ var import_websocket = /* @__PURE__ */ __toESM(require_websocket(), 1);
|
|
|
3629
3765
|
require_websocket_server();
|
|
3630
3766
|
var wrapper_default = import_websocket.default;
|
|
3631
3767
|
//#endregion
|
|
3632
|
-
//#region
|
|
3768
|
+
//#region ../connection/dist/ws-client-transport.js
|
|
3633
3769
|
/**
|
|
3634
|
-
*
|
|
3635
|
-
*
|
|
3636
|
-
*
|
|
3637
|
-
* the
|
|
3770
|
+
* Dial-out WS transport seam — the no-real-socket boundary a client FSM depends
|
|
3771
|
+
* on, so tests inject a fake. {@link ClientTransport} is the production impl: it
|
|
3772
|
+
* DIALS OUT (the daemon/relay connects to its peer, never the reverse), resolves
|
|
3773
|
+
* once the socket is open, and arms a receive-idle watchdog to detect a half-open
|
|
3774
|
+
* peer. Wire-agnostic: the subprotocol and dial path are passed in, so the same
|
|
3775
|
+
* transport serves the daemon↔CP, relay↔CP, and relay↔daemon wires.
|
|
3638
3776
|
*/
|
|
3639
|
-
const
|
|
3640
|
-
const
|
|
3641
|
-
const
|
|
3642
|
-
const SELF_PING_MS = 15e3;
|
|
3643
|
-
const RX_IDLE_MS = 45e3;
|
|
3777
|
+
const DEFAULT_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
3778
|
+
const DEFAULT_SELF_PING_MS = 15e3;
|
|
3779
|
+
const DEFAULT_RX_IDLE_MS = 45e3;
|
|
3644
3780
|
/**
|
|
3645
3781
|
* Arm a receive-idle watchdog on an open socket: every `selfPingMs` send a ping
|
|
3646
3782
|
* (the peer auto-pongs, which counts as inbound), and if NOTHING has arrived for
|
|
3647
3783
|
* `idleMs`, `terminate()` the socket — routing a dead half-open connection into
|
|
3648
3784
|
* the normal `close` → reconnect path. Any inbound frame (message/ping/pong)
|
|
3649
|
-
* resets the idle clock. Cleared on `close`. Kept off the FSM's injected Clock
|
|
3650
|
-
*
|
|
3785
|
+
* resets the idle clock. Cleared on `close`. Kept off the FSM's injected Clock so
|
|
3786
|
+
* it never perturbs the fake-clock client tests.
|
|
3651
3787
|
*/
|
|
3652
3788
|
function armRxWatchdog(ws, opts) {
|
|
3653
3789
|
const now = opts.now ?? (() => Date.now());
|
|
@@ -3675,24 +3811,26 @@ var ClientTransport = class ClientTransport {
|
|
|
3675
3811
|
get subprotocol() {
|
|
3676
3812
|
return this.ws.protocol;
|
|
3677
3813
|
}
|
|
3678
|
-
constructor(ws) {
|
|
3814
|
+
constructor(ws, selfPingMs, idleMs) {
|
|
3679
3815
|
this.ws = ws;
|
|
3680
3816
|
armRxWatchdog(ws, {
|
|
3681
|
-
selfPingMs
|
|
3682
|
-
idleMs
|
|
3817
|
+
selfPingMs,
|
|
3818
|
+
idleMs
|
|
3683
3819
|
});
|
|
3684
3820
|
}
|
|
3685
3821
|
/**
|
|
3686
|
-
* Dial
|
|
3822
|
+
* Dial `<url normalized to end in opts.path>` with `opts.subprotocol`.
|
|
3687
3823
|
* Resolves once the socket is open; rejects on any error before open
|
|
3688
3824
|
* (refused, bad handshake/subprotocol → HTTP 400).
|
|
3689
3825
|
*/
|
|
3690
|
-
static dial(url) {
|
|
3691
|
-
const wsUrl = url.endsWith(
|
|
3826
|
+
static dial(url, opts) {
|
|
3827
|
+
const wsUrl = url.endsWith(opts.path) ? url : url.replace(/\/+$/, "") + opts.path;
|
|
3828
|
+
const selfPingMs = opts.selfPingMs ?? DEFAULT_SELF_PING_MS;
|
|
3829
|
+
const idleMs = opts.idleMs ?? DEFAULT_RX_IDLE_MS;
|
|
3692
3830
|
return new Promise((resolve, reject) => {
|
|
3693
|
-
const ws = new wrapper_default(wsUrl, [
|
|
3831
|
+
const ws = new wrapper_default(wsUrl, [opts.subprotocol], {
|
|
3694
3832
|
maxPayload: MAX_FRAME_BYTES,
|
|
3695
|
-
handshakeTimeout:
|
|
3833
|
+
handshakeTimeout: opts.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS
|
|
3696
3834
|
});
|
|
3697
3835
|
const onPreOpenError = (err) => {
|
|
3698
3836
|
ws.removeAllListeners();
|
|
@@ -3702,7 +3840,7 @@ var ClientTransport = class ClientTransport {
|
|
|
3702
3840
|
ws.once("open", () => {
|
|
3703
3841
|
ws.removeListener("error", onPreOpenError);
|
|
3704
3842
|
ws.on("error", () => ws.terminate());
|
|
3705
|
-
resolve(new ClientTransport(ws));
|
|
3843
|
+
resolve(new ClientTransport(ws, selfPingMs, idleMs));
|
|
3706
3844
|
});
|
|
3707
3845
|
});
|
|
3708
3846
|
}
|
|
@@ -3723,6 +3861,957 @@ var ClientTransport = class ClientTransport {
|
|
|
3723
3861
|
}
|
|
3724
3862
|
};
|
|
3725
3863
|
//#endregion
|
|
3864
|
+
//#region src/cp/wire-slice.ts
|
|
3865
|
+
var import_src = /* @__PURE__ */ __toESM(require_src$2(), 1);
|
|
3866
|
+
/**
|
|
3867
|
+
* Pure helpers for slicing file bytes into a single WS frame — shared by the
|
|
3868
|
+
* workspace and memory readers so both honour the 256 KiB wire cap and never
|
|
3869
|
+
* split a UTF-8 character across slices.
|
|
3870
|
+
*/
|
|
3871
|
+
/** Encoded-payload ceiling, leaving headroom under MAX_FRAME_BYTES for the
|
|
3872
|
+
* envelope (id/ts/type/corr + fencing ext, well under 4 KiB). */
|
|
3873
|
+
const REPLY_BUDGET$1 = MAX_FRAME_BYTES - 4096;
|
|
3874
|
+
/** The encoded size of the payload the wire will carry (JSON.stringify matches
|
|
3875
|
+
* the codec's `encode`), measured in bytes. */
|
|
3876
|
+
function encodedBytes$1(payload) {
|
|
3877
|
+
return Buffer.byteLength(JSON.stringify(payload));
|
|
3878
|
+
}
|
|
3879
|
+
/** Largest index ≤ `len` that lands on a UTF-8 character boundary — i.e. never
|
|
3880
|
+
* cuts a multi-byte sequence. If the slice's trailing char is incomplete, the
|
|
3881
|
+
* boundary is placed before that char (its bytes belong to the next slice). */
|
|
3882
|
+
function utf8Boundary$1(buf, len) {
|
|
3883
|
+
if (len >= buf.length) len = buf.length;
|
|
3884
|
+
if (len <= 0) return 0;
|
|
3885
|
+
let start = len - 1;
|
|
3886
|
+
while (start > 0 && ((buf[start] ?? 0) & 192) === 128) start--;
|
|
3887
|
+
const lead = buf[start] ?? 0;
|
|
3888
|
+
const seqLen = lead < 128 ? 1 : (lead & 224) === 192 ? 2 : (lead & 240) === 224 ? 3 : (lead & 248) === 240 ? 4 : 1;
|
|
3889
|
+
return start + seqLen <= len ? len : start;
|
|
3890
|
+
}
|
|
3891
|
+
/** Shrink `content` (a utf8 view of `slice[0..end]`) until its JSON-escaped size
|
|
3892
|
+
* fits `REPLY_BUDGET`, keeping the cut on a UTF-8 boundary. Returns the final
|
|
3893
|
+
* `{ end, content }`. Control-byte-heavy text escapes ~6×, so the raw byte limit
|
|
3894
|
+
* is not enough on its own. */
|
|
3895
|
+
function fitToBudget(slice, end) {
|
|
3896
|
+
let content = slice.toString("utf8", 0, end);
|
|
3897
|
+
while (end > 0 && encodedBytes$1(content) > REPLY_BUDGET$1) {
|
|
3898
|
+
const factor = REPLY_BUDGET$1 / encodedBytes$1(content);
|
|
3899
|
+
const shrunk = Math.max(1, Math.floor(end * factor * .9));
|
|
3900
|
+
end = utf8Boundary$1(slice, Math.min(shrunk, end - 1));
|
|
3901
|
+
content = slice.toString("utf8", 0, end);
|
|
3902
|
+
}
|
|
3903
|
+
return {
|
|
3904
|
+
end,
|
|
3905
|
+
content
|
|
3906
|
+
};
|
|
3907
|
+
}
|
|
3908
|
+
//#endregion
|
|
3909
|
+
//#region src/cp/workspace-reader.ts
|
|
3910
|
+
/**
|
|
3911
|
+
* `WorkspaceReader` — the read-only seam answering the CP's `workspace/list` and
|
|
3912
|
+
* `workspace/read` REQs from the agent's local workspace directory. File bytes
|
|
3913
|
+
* live only on the daemon (§1/§12); this streams single pages/slices to the CP,
|
|
3914
|
+
* never whole trees or files.
|
|
3915
|
+
*
|
|
3916
|
+
* Containment is to `workspace.path` EXACTLY — the parent directory holds
|
|
3917
|
+
* `agent.json` and `.env` secrets, so escaping even one level is a secret leak.
|
|
3918
|
+
* Every request path is (1) rejected if absolute, (2) lexically resolved against
|
|
3919
|
+
* the root and prefix-checked, then (3) canonicalised with `realpath` and
|
|
3920
|
+
* re-verified so symlinks (including an intermediate directory component swapped
|
|
3921
|
+
* to a symlink after the check) cannot smuggle a target outside the root; all
|
|
3922
|
+
* I/O runs on the canonical path. Violations throw `WorkspaceViolationError`,
|
|
3923
|
+
* which the dispatcher maps to a `BAD_PAYLOAD` error frame; missing paths are
|
|
3924
|
+
* data (`exists:false`), not errors. Never log file contents.
|
|
3925
|
+
*
|
|
3926
|
+
* Frame-size safety: every REP must serialise under the 256 KiB wire cap. JSON
|
|
3927
|
+
* escaping of control bytes is a 6× blowup, so `read` bounds the slice by the
|
|
3928
|
+
* *encoded* reply size (not the raw byte `limit`) and `list` stops a page early
|
|
3929
|
+
* when the encoded entries would overflow — both leave headroom for the envelope.
|
|
3930
|
+
*/
|
|
3931
|
+
/** Bytes sniffed from the head of a file for binary (NUL byte) detection. */
|
|
3932
|
+
const SNIFF_BYTES = 8192;
|
|
3933
|
+
/** Path-containment / bad-request violation → `BAD_PAYLOAD` on the wire. */
|
|
3934
|
+
var WorkspaceViolationError = class extends Error {
|
|
3935
|
+
constructor(message) {
|
|
3936
|
+
super(message);
|
|
3937
|
+
this.name = "WorkspaceViolationError";
|
|
3938
|
+
}
|
|
3939
|
+
};
|
|
3940
|
+
function isErrno$1(err, code) {
|
|
3941
|
+
return err?.code === code;
|
|
3942
|
+
}
|
|
3943
|
+
function under(root, p) {
|
|
3944
|
+
return p === root || p.startsWith(root + path$13.sep);
|
|
3945
|
+
}
|
|
3946
|
+
function createWorkspaceReader(workspaceRootByAgent) {
|
|
3947
|
+
function rootFor(agentId) {
|
|
3948
|
+
const root = workspaceRootByAgent(agentId);
|
|
3949
|
+
if (!root) throw new WorkspaceViolationError(`unknown agent "${agentId}"`);
|
|
3950
|
+
return root;
|
|
3951
|
+
}
|
|
3952
|
+
/**
|
|
3953
|
+
* Lexical containment. Rejects absolute paths and `..` escapes; returns the
|
|
3954
|
+
* lexically-resolved path plus the canonical (realpath'd) root, or `null` for
|
|
3955
|
+
* `realRoot` when the root does not exist yet (callers report `exists:false`).
|
|
3956
|
+
*/
|
|
3957
|
+
async function resolveContained(root, relPath) {
|
|
3958
|
+
if (path$13.isAbsolute(relPath)) throw new WorkspaceViolationError("absolute paths are not allowed");
|
|
3959
|
+
const resolved = path$13.resolve(root, relPath);
|
|
3960
|
+
if (!under(root, resolved)) throw new WorkspaceViolationError("path escapes the workspace root");
|
|
3961
|
+
try {
|
|
3962
|
+
return {
|
|
3963
|
+
resolved,
|
|
3964
|
+
realRoot: await promises.realpath(root)
|
|
3965
|
+
};
|
|
3966
|
+
} catch (err) {
|
|
3967
|
+
if (isErrno$1(err, "ENOENT")) return {
|
|
3968
|
+
resolved,
|
|
3969
|
+
realRoot: null
|
|
3970
|
+
};
|
|
3971
|
+
throw err;
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
/** Canonicalise an existing target and re-verify containment on the REAL path,
|
|
3975
|
+
* closing the check-vs-use gap (a symlinked intermediate component resolves
|
|
3976
|
+
* out and is rejected here). Returns the canonical path used for I/O. */
|
|
3977
|
+
async function canonicalUnder(realRoot, abs) {
|
|
3978
|
+
const canon = await promises.realpath(abs);
|
|
3979
|
+
if (!under(realRoot, canon)) throw new WorkspaceViolationError("path resolves outside the workspace root");
|
|
3980
|
+
return canon;
|
|
3981
|
+
}
|
|
3982
|
+
return {
|
|
3983
|
+
async list(req) {
|
|
3984
|
+
const { resolved, realRoot } = await resolveContained(rootFor(req.agentId), req.path);
|
|
3985
|
+
const notFound = {
|
|
3986
|
+
agentId: req.agentId,
|
|
3987
|
+
path: req.path,
|
|
3988
|
+
exists: false,
|
|
3989
|
+
entries: []
|
|
3990
|
+
};
|
|
3991
|
+
if (realRoot === null) return notFound;
|
|
3992
|
+
let target;
|
|
3993
|
+
try {
|
|
3994
|
+
target = await canonicalUnder(realRoot, resolved);
|
|
3995
|
+
} catch (err) {
|
|
3996
|
+
if (isErrno$1(err, "ENOENT")) return notFound;
|
|
3997
|
+
throw err;
|
|
3998
|
+
}
|
|
3999
|
+
let dirents;
|
|
4000
|
+
try {
|
|
4001
|
+
dirents = await promises.readdir(target, { withFileTypes: true });
|
|
4002
|
+
} catch (err) {
|
|
4003
|
+
if (isErrno$1(err, "ENOENT") || isErrno$1(err, "ENOTDIR")) return notFound;
|
|
4004
|
+
throw err;
|
|
4005
|
+
}
|
|
4006
|
+
const entries = [];
|
|
4007
|
+
for (const d of dirents) {
|
|
4008
|
+
if (d.name === ".git") continue;
|
|
4009
|
+
const type = d.isDirectory() ? "dir" : d.isFile() ? "file" : d.isSymbolicLink() ? "symlink" : "other";
|
|
4010
|
+
const entry = {
|
|
4011
|
+
name: d.name,
|
|
4012
|
+
type
|
|
4013
|
+
};
|
|
4014
|
+
if (type === "file") try {
|
|
4015
|
+
const st = await promises.lstat(path$13.join(target, d.name));
|
|
4016
|
+
entry.size = st.size;
|
|
4017
|
+
entry.mtime = st.mtime.toISOString();
|
|
4018
|
+
} catch {}
|
|
4019
|
+
entries.push(entry);
|
|
4020
|
+
}
|
|
4021
|
+
entries.sort(compareEntries);
|
|
4022
|
+
const start = req.cursor !== void 0 && /^\d+$/.test(req.cursor) ? Number(req.cursor) : 0;
|
|
4023
|
+
const page = [];
|
|
4024
|
+
let acc = 0;
|
|
4025
|
+
for (let i = start; i < entries.length && page.length < req.limit; i++) {
|
|
4026
|
+
const e = entries[i];
|
|
4027
|
+
const enc = encodedBytes$1(e) + 1;
|
|
4028
|
+
if (page.length > 0 && acc + enc > REPLY_BUDGET$1) break;
|
|
4029
|
+
page.push(e);
|
|
4030
|
+
acc += enc;
|
|
4031
|
+
}
|
|
4032
|
+
const consumed = start + page.length;
|
|
4033
|
+
return {
|
|
4034
|
+
agentId: req.agentId,
|
|
4035
|
+
path: req.path,
|
|
4036
|
+
exists: true,
|
|
4037
|
+
entries: page,
|
|
4038
|
+
...consumed < entries.length ? { nextCursor: String(consumed) } : {}
|
|
4039
|
+
};
|
|
4040
|
+
},
|
|
4041
|
+
async read(req) {
|
|
4042
|
+
const { resolved, realRoot } = await resolveContained(rootFor(req.agentId), req.path);
|
|
4043
|
+
const notFound = {
|
|
4044
|
+
agentId: req.agentId,
|
|
4045
|
+
path: req.path,
|
|
4046
|
+
exists: false
|
|
4047
|
+
};
|
|
4048
|
+
if (realRoot === null) return notFound;
|
|
4049
|
+
let st;
|
|
4050
|
+
try {
|
|
4051
|
+
st = await promises.lstat(resolved);
|
|
4052
|
+
} catch (err) {
|
|
4053
|
+
if (isErrno$1(err, "ENOENT")) return notFound;
|
|
4054
|
+
throw err;
|
|
4055
|
+
}
|
|
4056
|
+
if (!st.isFile()) throw new WorkspaceViolationError("not a regular file");
|
|
4057
|
+
const target = await canonicalUnder(realRoot, resolved);
|
|
4058
|
+
const size = st.size;
|
|
4059
|
+
const mtime = st.mtime.toISOString();
|
|
4060
|
+
const fh = await promises.open(target, "r");
|
|
4061
|
+
try {
|
|
4062
|
+
const sniffLen = Math.min(SNIFF_BYTES, size);
|
|
4063
|
+
if (sniffLen > 0) {
|
|
4064
|
+
const sniff = Buffer.alloc(sniffLen);
|
|
4065
|
+
const { bytesRead } = await fh.read(sniff, 0, sniffLen, 0);
|
|
4066
|
+
if (sniff.subarray(0, bytesRead).includes(0)) return {
|
|
4067
|
+
agentId: req.agentId,
|
|
4068
|
+
path: req.path,
|
|
4069
|
+
exists: true,
|
|
4070
|
+
size,
|
|
4071
|
+
mtime,
|
|
4072
|
+
encoding: "none"
|
|
4073
|
+
};
|
|
4074
|
+
}
|
|
4075
|
+
const want = Math.min(req.limit, Math.max(0, size - req.offset));
|
|
4076
|
+
let slice = Buffer.alloc(0);
|
|
4077
|
+
if (want > 0) {
|
|
4078
|
+
const buf = Buffer.alloc(want);
|
|
4079
|
+
const { bytesRead } = await fh.read(buf, 0, want, req.offset);
|
|
4080
|
+
slice = buf.subarray(0, bytesRead);
|
|
4081
|
+
}
|
|
4082
|
+
let end = utf8Boundary$1(slice, slice.length);
|
|
4083
|
+
const fitted = fitToBudget(slice, end);
|
|
4084
|
+
end = fitted.end;
|
|
4085
|
+
const content = fitted.content;
|
|
4086
|
+
const nextOffset = req.offset + end;
|
|
4087
|
+
return {
|
|
4088
|
+
agentId: req.agentId,
|
|
4089
|
+
path: req.path,
|
|
4090
|
+
exists: true,
|
|
4091
|
+
size,
|
|
4092
|
+
mtime,
|
|
4093
|
+
encoding: "utf8",
|
|
4094
|
+
content,
|
|
4095
|
+
offset: req.offset,
|
|
4096
|
+
nextOffset,
|
|
4097
|
+
truncated: nextOffset < size
|
|
4098
|
+
};
|
|
4099
|
+
} finally {
|
|
4100
|
+
await fh.close();
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
};
|
|
4104
|
+
}
|
|
4105
|
+
/** dirs first, then case-insensitive alphabetical (stable within the page). */
|
|
4106
|
+
function compareEntries(a, b) {
|
|
4107
|
+
const aDir = a.type === "dir" ? 0 : 1;
|
|
4108
|
+
const bDir = b.type === "dir" ? 0 : 1;
|
|
4109
|
+
if (aDir !== bDir) return aDir - bDir;
|
|
4110
|
+
const al = a.name.toLowerCase();
|
|
4111
|
+
const bl = b.name.toLowerCase();
|
|
4112
|
+
if (al !== bl) return al < bl ? -1 : 1;
|
|
4113
|
+
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
4114
|
+
}
|
|
4115
|
+
//#endregion
|
|
4116
|
+
//#region src/agents/memory.ts
|
|
4117
|
+
/**
|
|
4118
|
+
* Agent memory — a DIRECTORY at the agent's ROOT (`<agent-root>/memory/`), OUTSIDE
|
|
4119
|
+
* the workspace so it survives a workspace reset / re-clone and is never committed
|
|
4120
|
+
* into a github-workspace repo.
|
|
4121
|
+
*
|
|
4122
|
+
* Index-plus-topics shape (the structure Claude Code's auto-memory uses): a lean
|
|
4123
|
+
* `MEMORY.md` index — injected into the prompt every session, so it must stay small
|
|
4124
|
+
* — plus any number of `<topic>.md` files the agent reads on demand. The agent
|
|
4125
|
+
* maintains it via the `readMemory` / `writeMemory` / `listMemory` MCP tools; the
|
|
4126
|
+
* daemon serves the same directory to the CP for the console.
|
|
4127
|
+
*
|
|
4128
|
+
* SECURITY: topic paths are agent/console-supplied, so every path is contained to
|
|
4129
|
+
* the memory dir — absolute paths and `..` escapes are rejected, and the resolved
|
|
4130
|
+
* path is re-checked so a symlink can't smuggle a target out. Flat by design (one
|
|
4131
|
+
* level): a topic is a file directly under `memory/`, no nested dirs.
|
|
4132
|
+
*/
|
|
4133
|
+
const MEMORY_DIRNAME = "memory";
|
|
4134
|
+
/** Max bytes of the index injected into the prompt each session — keeps a large
|
|
4135
|
+
* memory from swelling the context (mirrors Claude Code's 25 KB entrypoint cap). */
|
|
4136
|
+
const MAX_INDEX_INJECT_BYTES = 25e3;
|
|
4137
|
+
/** Hard cap on a single memory file. Injected/read every session, so an unbounded
|
|
4138
|
+
* file would swell the prompt and the wire; writes over this are rejected. */
|
|
4139
|
+
const MAX_MEMORY_FILE_BYTES = 256e3;
|
|
4140
|
+
/** Raised when a memory path escapes the memory dir. Surfaces as `BAD_PAYLOAD`. */
|
|
4141
|
+
var MemoryPathError = class extends Error {
|
|
4142
|
+
constructor(message) {
|
|
4143
|
+
super(message);
|
|
4144
|
+
this.name = "MemoryPathError";
|
|
4145
|
+
}
|
|
4146
|
+
};
|
|
4147
|
+
/** Raised when a write exceeds {@link MAX_MEMORY_FILE_BYTES}. `BAD_PAYLOAD`. */
|
|
4148
|
+
var MemoryTooLargeError = class extends Error {
|
|
4149
|
+
constructor(message) {
|
|
4150
|
+
super(message);
|
|
4151
|
+
this.name = "MemoryTooLargeError";
|
|
4152
|
+
}
|
|
4153
|
+
};
|
|
4154
|
+
/** Raised when an `ifMatchMtime` precondition fails (the file changed under the
|
|
4155
|
+
* writer — e.g. a console edit racing a newer agent write). Surfaces as CONFLICT. */
|
|
4156
|
+
var MemoryConflictError = class extends Error {
|
|
4157
|
+
constructor(message) {
|
|
4158
|
+
super(message);
|
|
4159
|
+
this.name = "MemoryConflictError";
|
|
4160
|
+
}
|
|
4161
|
+
};
|
|
4162
|
+
/** The memory directory for an agent, given its root dir (where agent.json lives). */
|
|
4163
|
+
function memoryDir(agentDir) {
|
|
4164
|
+
return join(agentDir, MEMORY_DIRNAME);
|
|
4165
|
+
}
|
|
4166
|
+
/** Resolve a memory-dir-relative path to an absolute one, contained to the dir.
|
|
4167
|
+
* Rejects absolute paths, `..` escapes, and any nested directory (flat only). */
|
|
4168
|
+
function resolveInMemoryDir(agentDir, relPath) {
|
|
4169
|
+
if (isAbsolute(relPath)) throw new MemoryPathError("absolute paths are not allowed");
|
|
4170
|
+
const dir = memoryDir(agentDir);
|
|
4171
|
+
const abs = resolve(dir, relPath);
|
|
4172
|
+
if (abs !== dir && !abs.startsWith(dir + sep)) throw new MemoryPathError("path escapes the memory dir");
|
|
4173
|
+
const rel = abs.slice(dir.length + 1);
|
|
4174
|
+
if (rel.includes(sep)) throw new MemoryPathError("memory is a flat directory (no subdirectories)");
|
|
4175
|
+
if (rel.length === 0) throw new MemoryPathError("a file name is required");
|
|
4176
|
+
return abs;
|
|
4177
|
+
}
|
|
4178
|
+
/** Seed `<agent-root>/memory/MEMORY.md` with a header if the dir/index is absent
|
|
4179
|
+
* (idempotent). Called at session start so a brand-new agent always has an index. */
|
|
4180
|
+
function ensureMemory(agentDir, agentName) {
|
|
4181
|
+
const dir = memoryDir(agentDir);
|
|
4182
|
+
mkdirSync(dir, { recursive: true });
|
|
4183
|
+
const index = join(dir, MEMORY_INDEX);
|
|
4184
|
+
if (!existsSync(index)) writeFileSync(index, `# ${agentName} memory\n\nYour long-term memory index. Keep it short — link out to topic files (e.g. \`[deploys](deploys.md)\`) for detail and read them on demand with the readMemory tool.\n`);
|
|
4185
|
+
}
|
|
4186
|
+
/** Read the memory index (`MEMORY.md`) for prompt injection, trimmed to the inject
|
|
4187
|
+
* cap so a large index can't blow up the prompt; '' when absent. */
|
|
4188
|
+
async function readIndex(agentDir) {
|
|
4189
|
+
const text = await readMemoryFile(agentDir, MEMORY_INDEX);
|
|
4190
|
+
if (Buffer.byteLength(text) <= 25e3) return text;
|
|
4191
|
+
return Buffer.from(text, "utf8").subarray(0, MAX_INDEX_INJECT_BYTES).toString("utf8") + "\n\n[…memory index truncated — trim MEMORY.md]";
|
|
4192
|
+
}
|
|
4193
|
+
/** Read a memory file's text; '' when it does not exist (never throws on ENOENT). */
|
|
4194
|
+
async function readMemoryFile(agentDir, relPath) {
|
|
4195
|
+
const abs = resolveInMemoryDir(agentDir, relPath);
|
|
4196
|
+
try {
|
|
4197
|
+
return await promises.readFile(abs, "utf8");
|
|
4198
|
+
} catch (err) {
|
|
4199
|
+
if (err.code === "ENOENT") return "";
|
|
4200
|
+
throw err;
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
4203
|
+
/** Overwrite a memory file with `content` (creating the dir if needed). Atomic
|
|
4204
|
+
* (tmp + rename) so a concurrent read never sees a partial file.
|
|
4205
|
+
*
|
|
4206
|
+
* - Rejects content over {@link MAX_MEMORY_FILE_BYTES} ({@link MemoryTooLargeError}).
|
|
4207
|
+
* - `ifMatchMtime` (optimistic concurrency): when given, the current file's mtime
|
|
4208
|
+
* must equal it, else {@link MemoryConflictError} — so a console edit can't
|
|
4209
|
+
* clobber a newer write. A brand-new file (no mtime) matches `ifMatchMtime`
|
|
4210
|
+
* only when the caller passes none (or the empty string). */
|
|
4211
|
+
async function writeMemoryFile(agentDir, relPath, content, ifMatchMtime) {
|
|
4212
|
+
if (Buffer.byteLength(content) > 256e3) throw new MemoryTooLargeError(`memory file exceeds the ${MAX_MEMORY_FILE_BYTES}-byte limit`);
|
|
4213
|
+
const abs = resolveInMemoryDir(agentDir, relPath);
|
|
4214
|
+
if (ifMatchMtime) {
|
|
4215
|
+
let current = null;
|
|
4216
|
+
try {
|
|
4217
|
+
current = (await promises.stat(abs)).mtime.toISOString();
|
|
4218
|
+
} catch (err) {
|
|
4219
|
+
if (err.code !== "ENOENT") throw err;
|
|
4220
|
+
}
|
|
4221
|
+
if (current !== ifMatchMtime) throw new MemoryConflictError("the memory file changed since it was read; reload and retry");
|
|
4222
|
+
}
|
|
4223
|
+
await promises.mkdir(memoryDir(agentDir), { recursive: true });
|
|
4224
|
+
const tmp = `${abs}.tmp`;
|
|
4225
|
+
await promises.writeFile(tmp, content, "utf8");
|
|
4226
|
+
await promises.rename(tmp, abs);
|
|
4227
|
+
const st = await promises.stat(abs);
|
|
4228
|
+
return {
|
|
4229
|
+
size: st.size,
|
|
4230
|
+
mtime: st.mtime.toISOString()
|
|
4231
|
+
};
|
|
4232
|
+
}
|
|
4233
|
+
/** List the files in the memory dir (flat; index + topics), sorted with the index
|
|
4234
|
+
* first. Empty when the dir does not exist. `.tmp` write artifacts are skipped. */
|
|
4235
|
+
async function listMemory(agentDir) {
|
|
4236
|
+
const dir = memoryDir(agentDir);
|
|
4237
|
+
let dirents;
|
|
4238
|
+
try {
|
|
4239
|
+
dirents = await promises.readdir(dir, { withFileTypes: true });
|
|
4240
|
+
} catch (err) {
|
|
4241
|
+
if (err.code === "ENOENT") return [];
|
|
4242
|
+
throw err;
|
|
4243
|
+
}
|
|
4244
|
+
const files = [];
|
|
4245
|
+
for (const d of dirents) {
|
|
4246
|
+
if (!d.isFile() || d.name.endsWith(".tmp")) continue;
|
|
4247
|
+
try {
|
|
4248
|
+
const st = await promises.stat(join(dir, d.name));
|
|
4249
|
+
files.push({
|
|
4250
|
+
name: d.name,
|
|
4251
|
+
size: st.size,
|
|
4252
|
+
mtime: st.mtime.toISOString()
|
|
4253
|
+
});
|
|
4254
|
+
} catch {}
|
|
4255
|
+
}
|
|
4256
|
+
files.sort((a, b) => a.name === "MEMORY.md" ? -1 : b.name === "MEMORY.md" ? 1 : a.name.localeCompare(b.name));
|
|
4257
|
+
return files;
|
|
4258
|
+
}
|
|
4259
|
+
//#endregion
|
|
4260
|
+
//#region src/cp/memory-reader.ts
|
|
4261
|
+
/**
|
|
4262
|
+
* `MemoryReader` — the seam answering the CP's `memory/list`, `memory/read` and
|
|
4263
|
+
* `memory/write` REQs for an agent's memory directory (`<agent-root>/memory/`).
|
|
4264
|
+
* The files live only on the daemon (§1/§12); reads stream single byte slices,
|
|
4265
|
+
* writes replace one whole file.
|
|
4266
|
+
*
|
|
4267
|
+
* Path containment lives in `agents/memory.ts` (`resolveInMemoryDir`): a topic
|
|
4268
|
+
* path that escapes the memory dir throws `MemoryPathError` → `BAD_PAYLOAD`. An
|
|
4269
|
+
* unknown agent is also `BAD_PAYLOAD`; a not-yet-created file/dir is data
|
|
4270
|
+
* (`exists:false`), not an error. Frame-size safety mirrors the workspace reader:
|
|
4271
|
+
* the read slice is bounded by the *encoded* reply size and ends on a UTF-8
|
|
4272
|
+
* boundary. Never log memory contents.
|
|
4273
|
+
*/
|
|
4274
|
+
/** Unknown-agent violation → `BAD_PAYLOAD` on the wire (path escapes surface as
|
|
4275
|
+
* MemoryPathError from agents/memory.ts, mapped the same way by the dispatcher). */
|
|
4276
|
+
var MemoryViolationError = class extends Error {
|
|
4277
|
+
constructor(message) {
|
|
4278
|
+
super(message);
|
|
4279
|
+
this.name = "MemoryViolationError";
|
|
4280
|
+
}
|
|
4281
|
+
};
|
|
4282
|
+
function isErrno(err, code) {
|
|
4283
|
+
return err?.code === code;
|
|
4284
|
+
}
|
|
4285
|
+
/** `agentDirByAgent` resolves an agent id → its root dir (which holds `memory/`),
|
|
4286
|
+
* or undefined for an unknown agent. */
|
|
4287
|
+
function createMemoryReader(agentDirByAgent) {
|
|
4288
|
+
function dirFor(agentId) {
|
|
4289
|
+
const dir = agentDirByAgent(agentId);
|
|
4290
|
+
if (!dir) throw new MemoryViolationError(`unknown agent "${agentId}"`);
|
|
4291
|
+
return dir;
|
|
4292
|
+
}
|
|
4293
|
+
return {
|
|
4294
|
+
async list(req) {
|
|
4295
|
+
const files = await listMemory(dirFor(req.agentId));
|
|
4296
|
+
return {
|
|
4297
|
+
agentId: req.agentId,
|
|
4298
|
+
exists: files.length > 0,
|
|
4299
|
+
entries: files
|
|
4300
|
+
};
|
|
4301
|
+
},
|
|
4302
|
+
async read(req) {
|
|
4303
|
+
const file = resolveInMemoryDir(dirFor(req.agentId), req.path);
|
|
4304
|
+
const notFound = {
|
|
4305
|
+
agentId: req.agentId,
|
|
4306
|
+
path: req.path,
|
|
4307
|
+
exists: false
|
|
4308
|
+
};
|
|
4309
|
+
let st;
|
|
4310
|
+
try {
|
|
4311
|
+
st = await promises.stat(file);
|
|
4312
|
+
} catch (err) {
|
|
4313
|
+
if (isErrno(err, "ENOENT")) return notFound;
|
|
4314
|
+
throw err;
|
|
4315
|
+
}
|
|
4316
|
+
if (!st.isFile()) return notFound;
|
|
4317
|
+
const size = st.size;
|
|
4318
|
+
const mtime = st.mtime.toISOString();
|
|
4319
|
+
const fh = await promises.open(file, "r");
|
|
4320
|
+
try {
|
|
4321
|
+
const want = Math.min(req.limit, Math.max(0, size - req.offset));
|
|
4322
|
+
let slice = Buffer.alloc(0);
|
|
4323
|
+
if (want > 0) {
|
|
4324
|
+
const buf = Buffer.alloc(want);
|
|
4325
|
+
const { bytesRead } = await fh.read(buf, 0, want, req.offset);
|
|
4326
|
+
slice = buf.subarray(0, bytesRead);
|
|
4327
|
+
}
|
|
4328
|
+
const { end, content } = fitToBudget(slice, utf8Boundary$1(slice, slice.length));
|
|
4329
|
+
const nextOffset = req.offset + end;
|
|
4330
|
+
return {
|
|
4331
|
+
agentId: req.agentId,
|
|
4332
|
+
path: req.path,
|
|
4333
|
+
exists: true,
|
|
4334
|
+
size,
|
|
4335
|
+
mtime,
|
|
4336
|
+
content,
|
|
4337
|
+
offset: req.offset,
|
|
4338
|
+
nextOffset,
|
|
4339
|
+
truncated: nextOffset < size
|
|
4340
|
+
};
|
|
4341
|
+
} finally {
|
|
4342
|
+
await fh.close();
|
|
4343
|
+
}
|
|
4344
|
+
},
|
|
4345
|
+
async write(req) {
|
|
4346
|
+
const { size, mtime } = await writeMemoryFile(dirFor(req.agentId), req.path, req.content, req.ifMatchMtime);
|
|
4347
|
+
return {
|
|
4348
|
+
agentId: req.agentId,
|
|
4349
|
+
path: req.path,
|
|
4350
|
+
size,
|
|
4351
|
+
mtime
|
|
4352
|
+
};
|
|
4353
|
+
}
|
|
4354
|
+
};
|
|
4355
|
+
}
|
|
4356
|
+
//#endregion
|
|
4357
|
+
//#region src/cp/client.ts
|
|
4358
|
+
/** The daemon↔CP wire's subprotocol + mount path — the two values the shared
|
|
4359
|
+
* `ClientTransport.dial` needs (formerly hardcoded in the daemon-local transport). */
|
|
4360
|
+
const CP_SUBPROTOCOL = "agentconnect.v1";
|
|
4361
|
+
const CP_WS_PATH = "/daemon/ws";
|
|
4362
|
+
const ACK_TIMEOUT_MS$1 = 5e3;
|
|
4363
|
+
const BACKOFF_BASE_MS = 1e3;
|
|
4364
|
+
const BACKOFF_CAP_MS = 3e4;
|
|
4365
|
+
var CpClient = class {
|
|
4366
|
+
deps;
|
|
4367
|
+
state = "CLOSED";
|
|
4368
|
+
sessionEpoch = 0;
|
|
4369
|
+
routingEpoch = 0;
|
|
4370
|
+
transport;
|
|
4371
|
+
correlator;
|
|
4372
|
+
stopped = false;
|
|
4373
|
+
fatal = false;
|
|
4374
|
+
attempt = 0;
|
|
4375
|
+
reconnectTimer;
|
|
4376
|
+
lastAuthedEpoch = 0;
|
|
4377
|
+
heartbeatTimer;
|
|
4378
|
+
heartbeatMs = 0;
|
|
4379
|
+
constructor(deps) {
|
|
4380
|
+
this.deps = deps;
|
|
4381
|
+
this.correlator = new ReqRep(deps.clock, ACK_TIMEOUT_MS$1);
|
|
4382
|
+
}
|
|
4383
|
+
/** Non-blocking: kicks off the connect loop and returns. */
|
|
4384
|
+
start() {
|
|
4385
|
+
this.stopped = false;
|
|
4386
|
+
this.fatal = false;
|
|
4387
|
+
this.attemptConnect();
|
|
4388
|
+
}
|
|
4389
|
+
async stop() {
|
|
4390
|
+
this.stopped = true;
|
|
4391
|
+
if (this.reconnectTimer !== void 0) {
|
|
4392
|
+
this.deps.clock.clearTimeout(this.reconnectTimer);
|
|
4393
|
+
this.reconnectTimer = void 0;
|
|
4394
|
+
}
|
|
4395
|
+
this.stopHeartbeat();
|
|
4396
|
+
this.correlator.rejectAll(/* @__PURE__ */ new Error("stopping"));
|
|
4397
|
+
this.transport?.close(1e3, "shutdown");
|
|
4398
|
+
this.state = "CLOSED";
|
|
4399
|
+
}
|
|
4400
|
+
async attemptConnect() {
|
|
4401
|
+
if (this.stopped || this.fatal) return;
|
|
4402
|
+
this.state = "CONNECTING";
|
|
4403
|
+
try {
|
|
4404
|
+
const t = await this.deps.connect();
|
|
4405
|
+
if (this.stopped || this.fatal) {
|
|
4406
|
+
t.close(1e3, "shutdown");
|
|
4407
|
+
return;
|
|
4408
|
+
}
|
|
4409
|
+
this.transport = t;
|
|
4410
|
+
t.onMessage((txt) => void this.onText(txt));
|
|
4411
|
+
t.onClose((c, r) => this.onClose(c, r));
|
|
4412
|
+
await this.handshake();
|
|
4413
|
+
this.attempt = 0;
|
|
4414
|
+
} catch (err) {
|
|
4415
|
+
this.deps.log.warn(`cp: connect/handshake failed: ${err.message}`);
|
|
4416
|
+
this.transport?.close(1011, "handshake failed");
|
|
4417
|
+
this.transport = void 0;
|
|
4418
|
+
this.scheduleReconnect();
|
|
4419
|
+
}
|
|
4420
|
+
}
|
|
4421
|
+
scheduleReconnect() {
|
|
4422
|
+
if (this.stopped || this.fatal) return;
|
|
4423
|
+
if (this.reconnectTimer !== void 0) return;
|
|
4424
|
+
const jitter = this.deps.jitter ?? Math.random;
|
|
4425
|
+
const base = Math.min(BACKOFF_CAP_MS, BACKOFF_BASE_MS * 2 ** this.attempt);
|
|
4426
|
+
const delay = Math.min(BACKOFF_CAP_MS, base + Math.floor(jitter() * base));
|
|
4427
|
+
this.attempt += 1;
|
|
4428
|
+
this.reconnectTimer = this.deps.clock.setTimeout(() => {
|
|
4429
|
+
this.reconnectTimer = void 0;
|
|
4430
|
+
this.attemptConnect();
|
|
4431
|
+
}, delay);
|
|
4432
|
+
}
|
|
4433
|
+
async handshake() {
|
|
4434
|
+
this.state = "AUTHENTICATING";
|
|
4435
|
+
const authPayload = {
|
|
4436
|
+
apiKey: this.deps.token,
|
|
4437
|
+
agentVersion: this.deps.agentVersion
|
|
4438
|
+
};
|
|
4439
|
+
if (this.deps.daemonId) authPayload.daemonId = this.deps.daemonId;
|
|
4440
|
+
if (this.lastAuthedEpoch > 0) authPayload.resume = { lastEpoch: this.lastAuthedEpoch };
|
|
4441
|
+
const ok = (await this.request("auth", authPayload)).payload;
|
|
4442
|
+
this.sessionEpoch = ok.sessionEpoch;
|
|
4443
|
+
this.lastAuthedEpoch = ok.sessionEpoch;
|
|
4444
|
+
if (ok.daemonId && ok.daemonId !== this.deps.daemonId) {
|
|
4445
|
+
this.deps.daemonId = ok.daemonId;
|
|
4446
|
+
this.deps.onDaemonId?.(ok.daemonId);
|
|
4447
|
+
}
|
|
4448
|
+
this.deps.onWebAppUrl?.(ok.webAppUrl);
|
|
4449
|
+
this.deps.onOrgSlug?.(ok.orgSlug);
|
|
4450
|
+
this.state = "REGISTERING";
|
|
4451
|
+
const snap = (await this.request("register", {
|
|
4452
|
+
host: this.deps.host,
|
|
4453
|
+
capabilities: this.deps.capabilities(),
|
|
4454
|
+
maxAgents: this.deps.maxAgents,
|
|
4455
|
+
localState: this.deps.localState()
|
|
4456
|
+
})).payload;
|
|
4457
|
+
this.routingEpoch = snap.routingEpoch;
|
|
4458
|
+
this.deps.configApply.applyReconcileSnapshot(snap);
|
|
4459
|
+
this.state = "READY";
|
|
4460
|
+
this.heartbeatMs = ok.heartbeatSec > 0 ? ok.heartbeatSec * 1e3 : this.deps.heartbeatDefaultMs;
|
|
4461
|
+
this.armHeartbeat();
|
|
4462
|
+
this.deps.log.info(`cp: READY (epoch=${this.sessionEpoch}, routingEpoch=${this.routingEpoch})`);
|
|
4463
|
+
this.emitDaemonRuntimes(this.deps.runtimeProfiles(), this.deps.mcpServerFacts?.() ?? []);
|
|
4464
|
+
this.deps.onReady?.();
|
|
4465
|
+
}
|
|
4466
|
+
/**
|
|
4467
|
+
* Push the full runtime snapshot (D→C `facts/daemon-runtimes` EVT,
|
|
4468
|
+
* fire-and-forget) — sent on each register and again when a probe sweep
|
|
4469
|
+
* completes. REPLACE semantics on the CP: it reconciles the daemon's stored
|
|
4470
|
+
* runtime list to exactly this snapshot, pruning runtimes that are no longer
|
|
4471
|
+
* installed. No-op unless READY/DRAINING.
|
|
4472
|
+
*/
|
|
4473
|
+
emitDaemonRuntimes(runtimes, mcpServers = []) {
|
|
4474
|
+
if (this.state !== "READY" && this.state !== "DRAINING") return;
|
|
4475
|
+
this.transport?.send(encode(buildEnvelope("facts/daemon-runtimes", {
|
|
4476
|
+
runtimes,
|
|
4477
|
+
mcpServers
|
|
4478
|
+
})));
|
|
4479
|
+
}
|
|
4480
|
+
/**
|
|
4481
|
+
* Report a session's cumulative token usage (D→C `usage/report` EVT,
|
|
4482
|
+
* fire-and-forget). No-op unless READY/DRAINING — usage is dashboard telemetry,
|
|
4483
|
+
* so a dropped report just means that turn's delta is absent from the CP's
|
|
4484
|
+
* historical aggregates until the next report (which re-sends the cumulative
|
|
4485
|
+
* snapshot, latest-wins). Never blocks the turn.
|
|
4486
|
+
*/
|
|
4487
|
+
emitUsageReport(report) {
|
|
4488
|
+
if (this.state !== "READY" && this.state !== "DRAINING") return;
|
|
4489
|
+
this.transport?.send(encode(buildEnvelope("usage/report", report)));
|
|
4490
|
+
}
|
|
4491
|
+
/**
|
|
4492
|
+
* Report a session's converged milestone + sessionKey echo (D→C `event/session`
|
|
4493
|
+
* EVT, fire-and-forget, latest-wins per `sessionId`). This is what lets the CP
|
|
4494
|
+
* store session metadata so a deep-link detail page (…/sessions/:id) resolves
|
|
4495
|
+
* even when the daemon is offline. Metadata only — never the message stream
|
|
4496
|
+
* (that stays daemon-local, §1/§12). No-op unless READY/DRAINING; a dropped
|
|
4497
|
+
* `start` is re-covered by the next milestone (the CP upsert is latest-wins).
|
|
4498
|
+
*/
|
|
4499
|
+
emitEventSession(event) {
|
|
4500
|
+
if (this.state !== "READY" && this.state !== "DRAINING") return;
|
|
4501
|
+
this.transport?.send(encode(buildEnvelope("event/session", event)));
|
|
4502
|
+
}
|
|
4503
|
+
/**
|
|
4504
|
+
* Report an integration's channel-membership snapshot (D→C `integration/channels`
|
|
4505
|
+
* EVT, fire-and-forget, latest-wins). No-op unless READY/DRAINING — the daemon
|
|
4506
|
+
* re-emits its cached snapshots on each (re)connect (see onReady in daemon.ts),
|
|
4507
|
+
* so a dropped report only delays the console's channel list, never loses it.
|
|
4508
|
+
*/
|
|
4509
|
+
emitIntegrationChannels(snapshot) {
|
|
4510
|
+
if (this.state !== "READY" && this.state !== "DRAINING") return;
|
|
4511
|
+
this.transport?.send(encode(buildEnvelope("integration/channels", snapshot)));
|
|
4512
|
+
}
|
|
4513
|
+
/**
|
|
4514
|
+
* Report a CP-owned cron fire (D→C `cron/report` EVT, fire-and-forget) so the
|
|
4515
|
+
* console's `lastRunAt` converges. The local D11 stamp stays authoritative and
|
|
4516
|
+
* the CP upsert is latest-wins, so re-asserting stored stamps on each
|
|
4517
|
+
* (re)connect (see onReady in daemon.ts) makes a dropped report only a delay,
|
|
4518
|
+
* never a loss. No-op unless READY/DRAINING.
|
|
4519
|
+
*/
|
|
4520
|
+
emitCronReport(report) {
|
|
4521
|
+
if (this.state !== "READY" && this.state !== "DRAINING") return;
|
|
4522
|
+
this.transport?.send(encode(buildEnvelope("cron/report", report)));
|
|
4523
|
+
}
|
|
4524
|
+
/**
|
|
4525
|
+
* Emit one chunk of a webchat turn's reply stream (D→C `webchat/output` EVT).
|
|
4526
|
+
* Ordered per-connection (TCP); `output.index` is the per-turn assembly counter.
|
|
4527
|
+
* No-op unless READY/DRAINING — a dropped chunk is lost (unlike latest-wins
|
|
4528
|
+
* telemetry), so the daemon only emits while connected. Content is inline, bounded
|
|
4529
|
+
* by the 256 KiB frame cap; the caller chunks any oversize single event.
|
|
4530
|
+
*/
|
|
4531
|
+
emitWebchatOutput(output) {
|
|
4532
|
+
if (this.state !== "READY" && this.state !== "DRAINING") return;
|
|
4533
|
+
this.transport?.send(encode(buildEnvelope("webchat/output", output)));
|
|
4534
|
+
}
|
|
4535
|
+
/** Emit a webchat turn's completion (D→C `webchat/done` EVT). No-op unless READY/DRAINING. */
|
|
4536
|
+
emitWebchatDone(done) {
|
|
4537
|
+
if (this.state !== "READY" && this.state !== "DRAINING") return;
|
|
4538
|
+
this.transport?.send(encode(buildEnvelope("webchat/done", done)));
|
|
4539
|
+
}
|
|
4540
|
+
request(type, payload) {
|
|
4541
|
+
const frame = buildEnvelope(type, payload);
|
|
4542
|
+
return this.correlator.request(frame, (e) => this.transport.send(e));
|
|
4543
|
+
}
|
|
4544
|
+
/**
|
|
4545
|
+
* `gitcred/request` (D→C REQ) → grant payload. The FIRST post-handshake
|
|
4546
|
+
* daemon-issued REQ, so unlike the handshake `request()` it is state-GATED:
|
|
4547
|
+
* outside READY/DRAINING it fails immediately (the credential cache degrades
|
|
4548
|
+
* to its unexpired copy or errors) — never queued, never a bare
|
|
4549
|
+
* `transport!.send` on a dead socket. Single send + 10s budget: the default
|
|
4550
|
+
* 5s×5 retransmit would blow every git-side timeout, and CP-side mint
|
|
4551
|
+
* single-flight makes a later fresh request cheap anyway.
|
|
4552
|
+
*/
|
|
4553
|
+
async requestGitCred(payload) {
|
|
4554
|
+
if (this.state !== "READY" && this.state !== "DRAINING" || !this.transport) throw new WireError("INTERNAL", `control plane unreachable (client ${this.state})`, true);
|
|
4555
|
+
const frame = buildEnvelope("gitcred/request", payload);
|
|
4556
|
+
const rep = await this.correlator.request(frame, (e) => this.transport.send(e), {
|
|
4557
|
+
maxTries: 1,
|
|
4558
|
+
ackTimeoutMs: 1e4
|
|
4559
|
+
});
|
|
4560
|
+
if (rep.type !== "gitcred/grant") throw new WireError("INTERNAL", `expected gitcred/grant, got ${rep.type}`, false);
|
|
4561
|
+
return rep.payload;
|
|
4562
|
+
}
|
|
4563
|
+
async onText(text) {
|
|
4564
|
+
const decoded = decodeEnvelope(text);
|
|
4565
|
+
if (!decoded.ok) {
|
|
4566
|
+
this.sendError(decoded.id, this.decodeErrorCode(decoded.msg), decoded.msg, false);
|
|
4567
|
+
return;
|
|
4568
|
+
}
|
|
4569
|
+
const frame = decoded.frame;
|
|
4570
|
+
if (frame.corr && this.correlator.settle(frame)) return;
|
|
4571
|
+
if (this.state !== "READY" && this.state !== "DRAINING") {
|
|
4572
|
+
this.sendError(frame.id, "PROTOCOL_STATE", `${frame.type} illegal in ${this.state}`, false);
|
|
4573
|
+
return;
|
|
4574
|
+
}
|
|
4575
|
+
if (decoded.ext?.epoch !== void 0 && decoded.ext.epoch < this.sessionEpoch) {
|
|
4576
|
+
this.sendError(frame.id, "STALE_EPOCH", "epoch < current", true);
|
|
4577
|
+
return;
|
|
4578
|
+
}
|
|
4579
|
+
this.dispatchControl(frame);
|
|
4580
|
+
}
|
|
4581
|
+
decodeErrorCode(msg) {
|
|
4582
|
+
if (msg === "FRAME_TOO_LARGE") return "FRAME_TOO_LARGE";
|
|
4583
|
+
if (msg === "UNKNOWN_FRAME") return "UNKNOWN_FRAME";
|
|
4584
|
+
return "BAD_PAYLOAD";
|
|
4585
|
+
}
|
|
4586
|
+
sendError(corr, code, message, retryable) {
|
|
4587
|
+
if (!this.transport) return;
|
|
4588
|
+
this.transport.send(encode(buildEnvelope("error", {
|
|
4589
|
+
code,
|
|
4590
|
+
message,
|
|
4591
|
+
retryable
|
|
4592
|
+
}, { corr })));
|
|
4593
|
+
}
|
|
4594
|
+
onClose(code, _reason) {
|
|
4595
|
+
this.stopHeartbeat();
|
|
4596
|
+
this.transport = void 0;
|
|
4597
|
+
this.correlator.rejectAll(new WireError("INTERNAL", "connection closed", true));
|
|
4598
|
+
if (code === 4401) {
|
|
4599
|
+
this.fatal = true;
|
|
4600
|
+
this.state = "CLOSED";
|
|
4601
|
+
this.deps.log.error("cp: AUTH_FAILED (4401) — not reconnecting; re-mint the daemon token");
|
|
4602
|
+
return;
|
|
4603
|
+
}
|
|
4604
|
+
if (this.stopped) {
|
|
4605
|
+
this.state = "CLOSED";
|
|
4606
|
+
return;
|
|
4607
|
+
}
|
|
4608
|
+
this.state = "DEGRADED";
|
|
4609
|
+
this.scheduleReconnect();
|
|
4610
|
+
}
|
|
4611
|
+
armHeartbeat() {
|
|
4612
|
+
this.heartbeatTimer = this.deps.clock.setTimeout(() => {
|
|
4613
|
+
if (this.state === "READY") this.transport?.send(encode(buildEnvelope("heartbeat", {
|
|
4614
|
+
load: this.deps.loadSnapshot(),
|
|
4615
|
+
health: "ok",
|
|
4616
|
+
activeSessions: this.deps.activeSessions(),
|
|
4617
|
+
degradedScopes: this.deps.degradedScopes?.() ?? []
|
|
4618
|
+
})));
|
|
4619
|
+
if (this.state === "READY" || this.state === "DRAINING") this.armHeartbeat();
|
|
4620
|
+
}, this.heartbeatMs);
|
|
4621
|
+
}
|
|
4622
|
+
stopHeartbeat() {
|
|
4623
|
+
if (this.heartbeatTimer !== void 0) {
|
|
4624
|
+
this.deps.clock.clearTimeout(this.heartbeatTimer);
|
|
4625
|
+
this.heartbeatTimer = void 0;
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4628
|
+
/** C→D control dispatch. The CP changes config, never live routing. */
|
|
4629
|
+
dispatchControl(frame) {
|
|
4630
|
+
switch (frame.type) {
|
|
4631
|
+
case "config/push":
|
|
4632
|
+
this.deps.configApply.applyConfigPush(frame.payload.keys);
|
|
4633
|
+
return;
|
|
4634
|
+
case "cron/upsert":
|
|
4635
|
+
try {
|
|
4636
|
+
this.deps.configApply.upsertCron(frame.payload);
|
|
4637
|
+
this.reply(frame, "ack", { ok: true });
|
|
4638
|
+
} catch (err) {
|
|
4639
|
+
this.sendError(frame.id, "BAD_PAYLOAD", `cron upsert failed: ${err.message}`, false);
|
|
4640
|
+
}
|
|
4641
|
+
return;
|
|
4642
|
+
case "cron/remove":
|
|
4643
|
+
this.deps.configApply.removeCron(frame.payload.cronId);
|
|
4644
|
+
this.reply(frame, "ack", { ok: true });
|
|
4645
|
+
return;
|
|
4646
|
+
case "cron/run":
|
|
4647
|
+
this.reply(frame, "ack", this.deps.configApply.runCron(frame.payload.cronId));
|
|
4648
|
+
return;
|
|
4649
|
+
case "route/assign": {
|
|
4650
|
+
const a = frame.payload;
|
|
4651
|
+
this.deps.configApply.applyRouteAssign(a);
|
|
4652
|
+
this.reply(frame, "route/assign/ack", {
|
|
4653
|
+
ok: true,
|
|
4654
|
+
sessionKey: a.sessionKey
|
|
4655
|
+
});
|
|
4656
|
+
return;
|
|
4657
|
+
}
|
|
4658
|
+
case "route/update":
|
|
4659
|
+
this.deps.configApply.applyRouteUpdate(frame.payload);
|
|
4660
|
+
return;
|
|
4661
|
+
case "relay/roster":
|
|
4662
|
+
this.deps.configApply.applyRelayRoster(frame.payload.relays);
|
|
4663
|
+
return;
|
|
4664
|
+
case "agent/upsert": {
|
|
4665
|
+
const u = frame.payload;
|
|
4666
|
+
this.deps.configApply.applyAgentUpsert(u.agentId, u.spec);
|
|
4667
|
+
return;
|
|
4668
|
+
}
|
|
4669
|
+
case "agent/remove":
|
|
4670
|
+
this.deps.configApply.applyAgentRemove(frame.payload.agentId);
|
|
4671
|
+
return;
|
|
4672
|
+
case "integration/upsert":
|
|
4673
|
+
this.deps.configApply.applyIntegrationUpsert(frame.payload);
|
|
4674
|
+
return;
|
|
4675
|
+
case "integration/remove":
|
|
4676
|
+
this.deps.configApply.applyIntegrationRemove(frame.payload.integrationId);
|
|
4677
|
+
return;
|
|
4678
|
+
case "agent/launch": {
|
|
4679
|
+
const launch = frame.payload;
|
|
4680
|
+
this.deps.configApply.applyAgentLaunch(launch).then((launched) => this.reply(frame, "agent/launched", launched)).catch((err) => this.sendError(frame.id, "INTERNAL", `agent/launch failed: ${err.message}`, false));
|
|
4681
|
+
return;
|
|
4682
|
+
}
|
|
4683
|
+
case "agent/stop": {
|
|
4684
|
+
const stop = frame.payload;
|
|
4685
|
+
this.deps.configApply.applyAgentStop(stop).then((ack) => this.reply(frame, "ack", ack)).catch((err) => this.sendError(frame.id, "INTERNAL", `agent/stop failed: ${err.message}`, false));
|
|
4686
|
+
return;
|
|
4687
|
+
}
|
|
4688
|
+
case "daemon/drain": {
|
|
4689
|
+
const drain = frame.payload;
|
|
4690
|
+
this.state = "DRAINING";
|
|
4691
|
+
this.deps.configApply.applyDaemonDrain(drain, (p) => this.emit("drain/progress", p)).then((done) => {
|
|
4692
|
+
this.reply(frame, "drain/done", done);
|
|
4693
|
+
if (this.state === "DRAINING") this.state = "READY";
|
|
4694
|
+
}).catch((err) => {
|
|
4695
|
+
this.sendError(frame.id, "INTERNAL", `drain failed: ${err.message}`, false);
|
|
4696
|
+
if (this.state === "DRAINING") this.state = "READY";
|
|
4697
|
+
});
|
|
4698
|
+
return;
|
|
4699
|
+
}
|
|
4700
|
+
case "daemon/restart":
|
|
4701
|
+
this.reply(frame, "daemon/control/ack", this.deps.configApply.applyDaemonRestart(frame.payload));
|
|
4702
|
+
return;
|
|
4703
|
+
case "daemon/upgrade":
|
|
4704
|
+
this.reply(frame, "daemon/control/ack", this.deps.configApply.applyDaemonUpgrade(frame.payload));
|
|
4705
|
+
return;
|
|
4706
|
+
case "session/list":
|
|
4707
|
+
try {
|
|
4708
|
+
this.reply(frame, "session/list/page", this.deps.sessionRead.list(frame.payload));
|
|
4709
|
+
} catch (err) {
|
|
4710
|
+
this.sendError(frame.id, "INTERNAL", `session/list failed: ${err.message}`, false);
|
|
4711
|
+
}
|
|
4712
|
+
return;
|
|
4713
|
+
case "session/history":
|
|
4714
|
+
try {
|
|
4715
|
+
this.reply(frame, "session/history/page", this.deps.sessionRead.history(frame.payload));
|
|
4716
|
+
} catch (err) {
|
|
4717
|
+
this.sendError(frame.id, "INTERNAL", `session/history failed: ${err.message}`, false);
|
|
4718
|
+
}
|
|
4719
|
+
return;
|
|
4720
|
+
case "session/tool-body":
|
|
4721
|
+
try {
|
|
4722
|
+
this.reply(frame, "session/tool-body/chunk", this.deps.sessionRead.toolBody(frame.payload));
|
|
4723
|
+
} catch (err) {
|
|
4724
|
+
this.sendError(frame.id, "INTERNAL", `session/tool-body failed: ${err.message}`, false);
|
|
4725
|
+
}
|
|
4726
|
+
return;
|
|
4727
|
+
case "workspace/list":
|
|
4728
|
+
this.deps.workspaceRead.list(frame.payload).then((page) => this.reply(frame, "workspace/list/page", page)).catch((err) => this.workspaceError(frame.id, "workspace/list", err));
|
|
4729
|
+
return;
|
|
4730
|
+
case "workspace/read":
|
|
4731
|
+
this.deps.workspaceRead.read(frame.payload).then((content) => this.reply(frame, "workspace/read/content", content)).catch((err) => this.workspaceError(frame.id, "workspace/read", err));
|
|
4732
|
+
return;
|
|
4733
|
+
case "workspace/gitstatus":
|
|
4734
|
+
this.deps.workspaceGit.status(frame.payload.agentId).then((status) => this.reply(frame, "workspace/gitstatus/result", status)).catch((err) => this.workspaceError(frame.id, "workspace/gitstatus", err));
|
|
4735
|
+
return;
|
|
4736
|
+
case "workspace/gitpull":
|
|
4737
|
+
this.deps.workspaceGit.pull(frame.payload.agentId).then((result) => this.reply(frame, "workspace/gitpull/result", result)).catch((err) => this.workspaceError(frame.id, "workspace/gitpull", err));
|
|
4738
|
+
return;
|
|
4739
|
+
case "memory/list":
|
|
4740
|
+
this.deps.memoryReader.list(frame.payload).then((page) => this.reply(frame, "memory/list/page", page)).catch((err) => this.memoryError(frame.id, "memory/list", err));
|
|
4741
|
+
return;
|
|
4742
|
+
case "memory/read":
|
|
4743
|
+
this.deps.memoryReader.read(frame.payload).then((content) => this.reply(frame, "memory/read/content", content)).catch((err) => this.memoryError(frame.id, "memory/read", err));
|
|
4744
|
+
return;
|
|
4745
|
+
case "memory/write":
|
|
4746
|
+
this.deps.memoryReader.write(frame.payload).then((ok) => this.reply(frame, "memory/write/ok", ok)).catch((err) => this.memoryError(frame.id, "memory/write", err));
|
|
4747
|
+
return;
|
|
4748
|
+
case "webchat/message":
|
|
4749
|
+
if (!this.deps.onWebchatMessage) {
|
|
4750
|
+
this.sendError(frame.id, "INTERNAL", "webchat not supported", false);
|
|
4751
|
+
return;
|
|
4752
|
+
}
|
|
4753
|
+
try {
|
|
4754
|
+
this.reply(frame, "webchat/ack", this.deps.onWebchatMessage(frame.payload));
|
|
4755
|
+
} catch (err) {
|
|
4756
|
+
this.sendError(frame.id, "INTERNAL", `webchat/message failed: ${err.message}`, false);
|
|
4757
|
+
}
|
|
4758
|
+
return;
|
|
4759
|
+
case "webchat/close":
|
|
4760
|
+
this.deps.onWebchatClose?.(frame.payload);
|
|
4761
|
+
return;
|
|
4762
|
+
case "webchat/set-model":
|
|
4763
|
+
this.deps.onWebchatSetModel?.(frame.payload);
|
|
4764
|
+
return;
|
|
4765
|
+
case "webchat/set-effort":
|
|
4766
|
+
this.deps.onWebchatSetEffort?.(frame.payload);
|
|
4767
|
+
return;
|
|
4768
|
+
case "webchat/set-permission-mode":
|
|
4769
|
+
this.deps.onWebchatSetPermissionMode?.(frame.payload);
|
|
4770
|
+
return;
|
|
4771
|
+
case "webchat/set-fast":
|
|
4772
|
+
this.deps.onWebchatSetFast?.(frame.payload);
|
|
4773
|
+
return;
|
|
4774
|
+
case "webchat/cancel":
|
|
4775
|
+
this.deps.onWebchatCancel?.(frame.payload);
|
|
4776
|
+
return;
|
|
4777
|
+
default:
|
|
4778
|
+
this.deps.log.debug(`cp: ignoring ${frame.type}`);
|
|
4779
|
+
return;
|
|
4780
|
+
}
|
|
4781
|
+
}
|
|
4782
|
+
/** Map a workspace read failure onto the wire: containment/bad-request
|
|
4783
|
+
* violations → BAD_PAYLOAD (their messages are hand-written and path-free);
|
|
4784
|
+
* anything else → INTERNAL with a GENERIC message — raw fs errors (ELOOP,
|
|
4785
|
+
* EACCES, …) embed absolute host paths that must not leak to the CP/UI. */
|
|
4786
|
+
workspaceError(corr, op, err) {
|
|
4787
|
+
if (err instanceof WorkspaceViolationError) {
|
|
4788
|
+
this.sendError(corr, "BAD_PAYLOAD", `${op} failed: ${err.message}`, false);
|
|
4789
|
+
return;
|
|
4790
|
+
}
|
|
4791
|
+
this.deps.log.warn(`cp: ${op} failed: ${err?.message}`);
|
|
4792
|
+
this.sendError(corr, "INTERNAL", `${op} failed`, false);
|
|
4793
|
+
}
|
|
4794
|
+
memoryError(corr, op, err) {
|
|
4795
|
+
if (err instanceof MemoryConflictError) {
|
|
4796
|
+
this.sendError(corr, "CONFLICT", `${op} failed: ${err.message}`, false);
|
|
4797
|
+
return;
|
|
4798
|
+
}
|
|
4799
|
+
if (err instanceof MemoryViolationError || err instanceof MemoryPathError || err instanceof MemoryTooLargeError) {
|
|
4800
|
+
this.sendError(corr, "BAD_PAYLOAD", `${op} failed: ${err.message}`, false);
|
|
4801
|
+
return;
|
|
4802
|
+
}
|
|
4803
|
+
this.deps.log.warn(`cp: ${op} failed: ${err?.message}`);
|
|
4804
|
+
this.sendError(corr, "INTERNAL", `${op} failed`, false);
|
|
4805
|
+
}
|
|
4806
|
+
reply(req, type, payload) {
|
|
4807
|
+
this.transport?.send(encode(buildEnvelope(type, payload, { corr: req.id })));
|
|
4808
|
+
}
|
|
4809
|
+
/** Emit an uncorrelated EVT (e.g. `drain/progress`). */
|
|
4810
|
+
emit(type, payload) {
|
|
4811
|
+
this.transport?.send(encode(buildEnvelope(type, payload)));
|
|
4812
|
+
}
|
|
4813
|
+
};
|
|
4814
|
+
//#endregion
|
|
3726
4815
|
//#region ../../node_modules/.pnpm/readdirp@5.0.0/node_modules/readdirp/index.js
|
|
3727
4816
|
const EntryTypes = {
|
|
3728
4817
|
FILE_TYPE: "files",
|
|
@@ -4252,7 +5341,7 @@ const binaryExtensions = /* @__PURE__ */ new Set([
|
|
|
4252
5341
|
"zip",
|
|
4253
5342
|
"zipx"
|
|
4254
5343
|
]);
|
|
4255
|
-
const isBinaryPath = (filePath) => binaryExtensions.has(
|
|
5344
|
+
const isBinaryPath = (filePath) => binaryExtensions.has(path$13.extname(filePath).slice(1).toLowerCase());
|
|
4256
5345
|
const foreach = (val, fn) => {
|
|
4257
5346
|
if (val instanceof Set) val.forEach(fn);
|
|
4258
5347
|
else fn(val);
|
|
@@ -4287,7 +5376,7 @@ function createFsWatchInstance(path, options, listener, errHandler, emitRaw) {
|
|
|
4287
5376
|
const handleEvent = (rawEvent, evPath) => {
|
|
4288
5377
|
listener(path);
|
|
4289
5378
|
emitRaw(rawEvent, evPath, { watchedPath: path });
|
|
4290
|
-
if (evPath && path !== evPath) fsWatchBroadcast(
|
|
5379
|
+
if (evPath && path !== evPath) fsWatchBroadcast(path$13.resolve(path, evPath), KEY_LISTENERS, path$13.join(path, evPath));
|
|
4291
5380
|
};
|
|
4292
5381
|
try {
|
|
4293
5382
|
return watch(path, { persistent: options.persistent }, handleEvent);
|
|
@@ -4429,10 +5518,10 @@ var NodeFsHandler = class {
|
|
|
4429
5518
|
*/
|
|
4430
5519
|
_watchWithNodeFs(path, listener) {
|
|
4431
5520
|
const opts = this.fsw.options;
|
|
4432
|
-
const directory =
|
|
4433
|
-
const basename =
|
|
5521
|
+
const directory = path$13.dirname(path);
|
|
5522
|
+
const basename = path$13.basename(path);
|
|
4434
5523
|
this.fsw._getWatchedDir(directory).add(basename);
|
|
4435
|
-
const absolutePath =
|
|
5524
|
+
const absolutePath = path$13.resolve(path);
|
|
4436
5525
|
const options = { persistent: opts.persistent };
|
|
4437
5526
|
if (!listener) listener = EMPTY_FN;
|
|
4438
5527
|
let closer;
|
|
@@ -4455,8 +5544,8 @@ var NodeFsHandler = class {
|
|
|
4455
5544
|
*/
|
|
4456
5545
|
_handleFile(file, stats, initialAdd) {
|
|
4457
5546
|
if (this.fsw.closed) return;
|
|
4458
|
-
const dirname =
|
|
4459
|
-
const basename =
|
|
5547
|
+
const dirname = path$13.dirname(file);
|
|
5548
|
+
const basename = path$13.basename(file);
|
|
4460
5549
|
const parent = this.fsw._getWatchedDir(dirname);
|
|
4461
5550
|
let prevStats = stats;
|
|
4462
5551
|
if (parent.has(basename)) return;
|
|
@@ -4530,7 +5619,7 @@ var NodeFsHandler = class {
|
|
|
4530
5619
|
this.fsw._symlinkPaths.set(full, true);
|
|
4531
5620
|
}
|
|
4532
5621
|
_handleRead(directory, initialAdd, wh, target, dir, depth, throttler) {
|
|
4533
|
-
directory =
|
|
5622
|
+
directory = path$13.join(directory, "");
|
|
4534
5623
|
const throttleKey = target ? `${directory}:${target}` : directory;
|
|
4535
5624
|
throttler = this.fsw._throttle("readdir", throttleKey, 1e3);
|
|
4536
5625
|
if (!throttler) return;
|
|
@@ -4547,7 +5636,7 @@ var NodeFsHandler = class {
|
|
|
4547
5636
|
return;
|
|
4548
5637
|
}
|
|
4549
5638
|
const item = entry.path;
|
|
4550
|
-
let path =
|
|
5639
|
+
let path = path$13.join(directory, item);
|
|
4551
5640
|
current.add(item);
|
|
4552
5641
|
if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path, item)) return;
|
|
4553
5642
|
if (this.fsw.closed) {
|
|
@@ -4556,7 +5645,7 @@ var NodeFsHandler = class {
|
|
|
4556
5645
|
}
|
|
4557
5646
|
if (item === target || !target && !previous.has(item)) {
|
|
4558
5647
|
this.fsw._incrReadyCount();
|
|
4559
|
-
path =
|
|
5648
|
+
path = path$13.join(dir, path$13.relative(dir, path));
|
|
4560
5649
|
this._addToNodeFs(path, initialAdd, wh, depth + 1);
|
|
4561
5650
|
}
|
|
4562
5651
|
}).on(EV.ERROR, this._boundHandleError);
|
|
@@ -4591,10 +5680,10 @@ var NodeFsHandler = class {
|
|
|
4591
5680
|
* @returns closer for the watcher instance.
|
|
4592
5681
|
*/
|
|
4593
5682
|
async _handleDir(dir, stats, initialAdd, depth, target, wh, realpath) {
|
|
4594
|
-
const parentDir = this.fsw._getWatchedDir(
|
|
4595
|
-
const tracked = parentDir.has(
|
|
5683
|
+
const parentDir = this.fsw._getWatchedDir(path$13.dirname(dir));
|
|
5684
|
+
const tracked = parentDir.has(path$13.basename(dir));
|
|
4596
5685
|
if (!(initialAdd && this.fsw.options.ignoreInitial) && !target && !tracked) this.fsw._emit(EV.ADD_DIR, dir, stats);
|
|
4597
|
-
parentDir.add(
|
|
5686
|
+
parentDir.add(path$13.basename(dir));
|
|
4598
5687
|
this.fsw._getWatchedDir(dir);
|
|
4599
5688
|
let throttler;
|
|
4600
5689
|
let closer;
|
|
@@ -4641,7 +5730,7 @@ var NodeFsHandler = class {
|
|
|
4641
5730
|
const follow = this.fsw.options.followSymlinks;
|
|
4642
5731
|
let closer;
|
|
4643
5732
|
if (stats.isDirectory()) {
|
|
4644
|
-
const absPath =
|
|
5733
|
+
const absPath = path$13.resolve(path);
|
|
4645
5734
|
const targetPath = follow ? await realpath(path) : path;
|
|
4646
5735
|
if (this.fsw.closed) return;
|
|
4647
5736
|
closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
|
|
@@ -4650,12 +5739,12 @@ var NodeFsHandler = class {
|
|
|
4650
5739
|
} else if (stats.isSymbolicLink()) {
|
|
4651
5740
|
const targetPath = follow ? await realpath(path) : path;
|
|
4652
5741
|
if (this.fsw.closed) return;
|
|
4653
|
-
const parent =
|
|
5742
|
+
const parent = path$13.dirname(wh.watchPath);
|
|
4654
5743
|
this.fsw._getWatchedDir(parent).add(wh.watchPath);
|
|
4655
5744
|
this.fsw._emit(EV.ADD, wh.watchPath, stats);
|
|
4656
5745
|
closer = await this._handleDir(parent, stats, initialAdd, depth, path, wh, targetPath);
|
|
4657
5746
|
if (this.fsw.closed) return;
|
|
4658
|
-
if (targetPath !== void 0) this.fsw._symlinkPaths.set(
|
|
5747
|
+
if (targetPath !== void 0) this.fsw._symlinkPaths.set(path$13.resolve(path), targetPath);
|
|
4659
5748
|
} else closer = this._handleFile(wh.watchPath, stats, initialAdd);
|
|
4660
5749
|
ready();
|
|
4661
5750
|
if (closer) this.fsw._addPathCloser(path, closer);
|
|
@@ -4691,9 +5780,9 @@ function createPattern(matcher) {
|
|
|
4691
5780
|
if (typeof matcher === "object" && matcher !== null) return (string) => {
|
|
4692
5781
|
if (matcher.path === string) return true;
|
|
4693
5782
|
if (matcher.recursive) {
|
|
4694
|
-
const relative =
|
|
5783
|
+
const relative = path$13.relative(matcher.path, string);
|
|
4695
5784
|
if (!relative) return false;
|
|
4696
|
-
return !relative.startsWith("..") && !
|
|
5785
|
+
return !relative.startsWith("..") && !path$13.isAbsolute(relative);
|
|
4697
5786
|
}
|
|
4698
5787
|
return false;
|
|
4699
5788
|
};
|
|
@@ -4701,7 +5790,7 @@ function createPattern(matcher) {
|
|
|
4701
5790
|
}
|
|
4702
5791
|
function normalizePath(path) {
|
|
4703
5792
|
if (typeof path !== "string") throw new Error("string expected");
|
|
4704
|
-
path =
|
|
5793
|
+
path = path$13.normalize(path);
|
|
4705
5794
|
path = path.replace(/\\/g, "/");
|
|
4706
5795
|
let prepend = false;
|
|
4707
5796
|
if (path.startsWith("//")) prepend = true;
|
|
@@ -4738,14 +5827,14 @@ const toUnix = (string) => {
|
|
|
4738
5827
|
if (prepend) str = SLASH + str;
|
|
4739
5828
|
return str;
|
|
4740
5829
|
};
|
|
4741
|
-
const normalizePathToUnix = (path) => toUnix(
|
|
5830
|
+
const normalizePathToUnix = (path) => toUnix(path$13.normalize(toUnix(path)));
|
|
4742
5831
|
const normalizeIgnored = (cwd = "") => (path) => {
|
|
4743
|
-
if (typeof path === "string") return normalizePathToUnix(
|
|
5832
|
+
if (typeof path === "string") return normalizePathToUnix(path$13.isAbsolute(path) ? path : path$13.join(cwd, path));
|
|
4744
5833
|
else return path;
|
|
4745
5834
|
};
|
|
4746
5835
|
const getAbsolutePath = (path, cwd) => {
|
|
4747
|
-
if (
|
|
4748
|
-
return
|
|
5836
|
+
if (path$13.isAbsolute(path)) return path;
|
|
5837
|
+
return path$13.join(cwd, path);
|
|
4749
5838
|
};
|
|
4750
5839
|
const EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
|
|
4751
5840
|
/**
|
|
@@ -4774,7 +5863,7 @@ var DirEntry = class {
|
|
|
4774
5863
|
try {
|
|
4775
5864
|
await readdir(dir);
|
|
4776
5865
|
} catch (err) {
|
|
4777
|
-
if (this._removeWatcher) this._removeWatcher(
|
|
5866
|
+
if (this._removeWatcher) this._removeWatcher(path$13.dirname(dir), path$13.basename(dir));
|
|
4778
5867
|
}
|
|
4779
5868
|
}
|
|
4780
5869
|
has(item) {
|
|
@@ -4810,7 +5899,7 @@ var WatchHelper = class {
|
|
|
4810
5899
|
const watchPath = path;
|
|
4811
5900
|
this.path = path = path.replace(REPLACER_RE, "");
|
|
4812
5901
|
this.watchPath = watchPath;
|
|
4813
|
-
this.fullWatchPath =
|
|
5902
|
+
this.fullWatchPath = path$13.resolve(watchPath);
|
|
4814
5903
|
this.dirParts = [];
|
|
4815
5904
|
this.dirParts.forEach((parts) => {
|
|
4816
5905
|
if (parts.length > 1) parts.pop();
|
|
@@ -4819,7 +5908,7 @@ var WatchHelper = class {
|
|
|
4819
5908
|
this.statMethod = follow ? STAT_METHOD_F : STAT_METHOD_L;
|
|
4820
5909
|
}
|
|
4821
5910
|
entryPath(entry) {
|
|
4822
|
-
return
|
|
5911
|
+
return path$13.join(this.watchPath, path$13.relative(this.watchPath, entry.fullPath));
|
|
4823
5912
|
}
|
|
4824
5913
|
filterPath(entry) {
|
|
4825
5914
|
const { stats } = entry;
|
|
@@ -4955,7 +6044,7 @@ var FSWatcher = class extends EventEmitter {
|
|
|
4955
6044
|
})).then((results) => {
|
|
4956
6045
|
if (this.closed) return;
|
|
4957
6046
|
results.forEach((item) => {
|
|
4958
|
-
if (item) this.add(
|
|
6047
|
+
if (item) this.add(path$13.dirname(item), path$13.basename(_origAdd || item));
|
|
4959
6048
|
});
|
|
4960
6049
|
});
|
|
4961
6050
|
return this;
|
|
@@ -4968,9 +6057,9 @@ var FSWatcher = class extends EventEmitter {
|
|
|
4968
6057
|
const paths = unifyPaths(paths_);
|
|
4969
6058
|
const { cwd } = this.options;
|
|
4970
6059
|
paths.forEach((path) => {
|
|
4971
|
-
if (!
|
|
4972
|
-
if (cwd) path =
|
|
4973
|
-
path =
|
|
6060
|
+
if (!path$13.isAbsolute(path) && !this._closers.has(path)) {
|
|
6061
|
+
if (cwd) path = path$13.join(cwd, path);
|
|
6062
|
+
path = path$13.resolve(path);
|
|
4974
6063
|
}
|
|
4975
6064
|
this._closePath(path);
|
|
4976
6065
|
this._addIgnoredPath(path);
|
|
@@ -5014,7 +6103,7 @@ var FSWatcher = class extends EventEmitter {
|
|
|
5014
6103
|
getWatched() {
|
|
5015
6104
|
const watchList = {};
|
|
5016
6105
|
this._watched.forEach((entry, dir) => {
|
|
5017
|
-
const index = (this.options.cwd ?
|
|
6106
|
+
const index = (this.options.cwd ? path$13.relative(this.options.cwd, dir) : dir) || ONE_DOT;
|
|
5018
6107
|
watchList[index] = entry.getChildren().sort();
|
|
5019
6108
|
});
|
|
5020
6109
|
return watchList;
|
|
@@ -5034,8 +6123,8 @@ var FSWatcher = class extends EventEmitter {
|
|
|
5034
6123
|
async _emit(event, path, stats) {
|
|
5035
6124
|
if (this.closed) return;
|
|
5036
6125
|
const opts = this.options;
|
|
5037
|
-
if (isWindows) path =
|
|
5038
|
-
if (opts.cwd) path =
|
|
6126
|
+
if (isWindows) path = path$13.normalize(path);
|
|
6127
|
+
if (opts.cwd) path = path$13.relative(opts.cwd, path);
|
|
5039
6128
|
const args = [path];
|
|
5040
6129
|
if (stats != null) args.push(stats);
|
|
5041
6130
|
const awf = opts.awaitWriteFinish;
|
|
@@ -5080,7 +6169,7 @@ var FSWatcher = class extends EventEmitter {
|
|
|
5080
6169
|
if (!this._throttle(EVENTS.CHANGE, path, 50)) return this;
|
|
5081
6170
|
}
|
|
5082
6171
|
if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
|
|
5083
|
-
const fullPath = opts.cwd ?
|
|
6172
|
+
const fullPath = opts.cwd ? path$13.join(opts.cwd, path) : path;
|
|
5084
6173
|
let stats;
|
|
5085
6174
|
try {
|
|
5086
6175
|
stats = await stat$1(fullPath);
|
|
@@ -5151,7 +6240,7 @@ var FSWatcher = class extends EventEmitter {
|
|
|
5151
6240
|
const pollInterval = awf.pollInterval;
|
|
5152
6241
|
let timeoutHandler;
|
|
5153
6242
|
let fullPath = path;
|
|
5154
|
-
if (this.options.cwd && !
|
|
6243
|
+
if (this.options.cwd && !path$13.isAbsolute(path)) fullPath = path$13.join(this.options.cwd, path);
|
|
5155
6244
|
const now = /* @__PURE__ */ new Date();
|
|
5156
6245
|
const writes = this._pendingWrites;
|
|
5157
6246
|
function awaitWriteFinishFn(prevStat) {
|
|
@@ -5208,7 +6297,7 @@ var FSWatcher = class extends EventEmitter {
|
|
|
5208
6297
|
* @param directory path of the directory
|
|
5209
6298
|
*/
|
|
5210
6299
|
_getWatchedDir(directory) {
|
|
5211
|
-
const dir =
|
|
6300
|
+
const dir = path$13.resolve(directory);
|
|
5212
6301
|
if (!this._watched.has(dir)) this._watched.set(dir, new DirEntry(dir, this._boundRemove));
|
|
5213
6302
|
return this._watched.get(dir);
|
|
5214
6303
|
}
|
|
@@ -5227,8 +6316,8 @@ var FSWatcher = class extends EventEmitter {
|
|
|
5227
6316
|
* @param item base path of item/directory
|
|
5228
6317
|
*/
|
|
5229
6318
|
_remove(directory, item, isDirectory) {
|
|
5230
|
-
const path =
|
|
5231
|
-
const fullPath =
|
|
6319
|
+
const path = path$13.join(directory, item);
|
|
6320
|
+
const fullPath = path$13.resolve(path);
|
|
5232
6321
|
isDirectory = isDirectory != null ? isDirectory : this._watched.has(path) || this._watched.has(fullPath);
|
|
5233
6322
|
if (!this._throttle("remove", path, 100)) return;
|
|
5234
6323
|
if (!isDirectory && this._watched.size === 1) this.add(directory, item, true);
|
|
@@ -5238,7 +6327,7 @@ var FSWatcher = class extends EventEmitter {
|
|
|
5238
6327
|
parent.remove(item);
|
|
5239
6328
|
if (this._symlinkPaths.has(fullPath)) this._symlinkPaths.delete(fullPath);
|
|
5240
6329
|
let relPath = path;
|
|
5241
|
-
if (this.options.cwd) relPath =
|
|
6330
|
+
if (this.options.cwd) relPath = path$13.relative(this.options.cwd, path);
|
|
5242
6331
|
if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
|
|
5243
6332
|
if (this._pendingWrites.get(relPath).cancelWait() === EVENTS.ADD) return;
|
|
5244
6333
|
}
|
|
@@ -5253,8 +6342,8 @@ var FSWatcher = class extends EventEmitter {
|
|
|
5253
6342
|
*/
|
|
5254
6343
|
_closePath(path) {
|
|
5255
6344
|
this._closeFile(path);
|
|
5256
|
-
const dir =
|
|
5257
|
-
this._getWatchedDir(dir).remove(
|
|
6345
|
+
const dir = path$13.dirname(path);
|
|
6346
|
+
this._getWatchedDir(dir).remove(path$13.basename(path));
|
|
5258
6347
|
}
|
|
5259
6348
|
/**
|
|
5260
6349
|
* Closes only file-specific watchers
|
|
@@ -5820,150 +6909,6 @@ function monotonicTs() {
|
|
|
5820
6909
|
return String(last);
|
|
5821
6910
|
}
|
|
5822
6911
|
//#endregion
|
|
5823
|
-
//#region src/agents/memory.ts
|
|
5824
|
-
/**
|
|
5825
|
-
* Agent memory — a DIRECTORY at the agent's ROOT (`<agent-root>/memory/`), OUTSIDE
|
|
5826
|
-
* the workspace so it survives a workspace reset / re-clone and is never committed
|
|
5827
|
-
* into a github-workspace repo.
|
|
5828
|
-
*
|
|
5829
|
-
* Index-plus-topics shape (the structure Claude Code's auto-memory uses): a lean
|
|
5830
|
-
* `MEMORY.md` index — injected into the prompt every session, so it must stay small
|
|
5831
|
-
* — plus any number of `<topic>.md` files the agent reads on demand. The agent
|
|
5832
|
-
* maintains it via the `readMemory` / `writeMemory` / `listMemory` MCP tools; the
|
|
5833
|
-
* daemon serves the same directory to the CP for the console.
|
|
5834
|
-
*
|
|
5835
|
-
* SECURITY: topic paths are agent/console-supplied, so every path is contained to
|
|
5836
|
-
* the memory dir — absolute paths and `..` escapes are rejected, and the resolved
|
|
5837
|
-
* path is re-checked so a symlink can't smuggle a target out. Flat by design (one
|
|
5838
|
-
* level): a topic is a file directly under `memory/`, no nested dirs.
|
|
5839
|
-
*/
|
|
5840
|
-
const MEMORY_DIRNAME = "memory";
|
|
5841
|
-
/** Max bytes of the index injected into the prompt each session — keeps a large
|
|
5842
|
-
* memory from swelling the context (mirrors Claude Code's 25 KB entrypoint cap). */
|
|
5843
|
-
const MAX_INDEX_INJECT_BYTES = 25e3;
|
|
5844
|
-
/** Hard cap on a single memory file. Injected/read every session, so an unbounded
|
|
5845
|
-
* file would swell the prompt and the wire; writes over this are rejected. */
|
|
5846
|
-
const MAX_MEMORY_FILE_BYTES = 256e3;
|
|
5847
|
-
/** Raised when a memory path escapes the memory dir. Surfaces as `BAD_PAYLOAD`. */
|
|
5848
|
-
var MemoryPathError = class extends Error {
|
|
5849
|
-
constructor(message) {
|
|
5850
|
-
super(message);
|
|
5851
|
-
this.name = "MemoryPathError";
|
|
5852
|
-
}
|
|
5853
|
-
};
|
|
5854
|
-
/** Raised when a write exceeds {@link MAX_MEMORY_FILE_BYTES}. `BAD_PAYLOAD`. */
|
|
5855
|
-
var MemoryTooLargeError = class extends Error {
|
|
5856
|
-
constructor(message) {
|
|
5857
|
-
super(message);
|
|
5858
|
-
this.name = "MemoryTooLargeError";
|
|
5859
|
-
}
|
|
5860
|
-
};
|
|
5861
|
-
/** Raised when an `ifMatchMtime` precondition fails (the file changed under the
|
|
5862
|
-
* writer — e.g. a console edit racing a newer agent write). Surfaces as CONFLICT. */
|
|
5863
|
-
var MemoryConflictError = class extends Error {
|
|
5864
|
-
constructor(message) {
|
|
5865
|
-
super(message);
|
|
5866
|
-
this.name = "MemoryConflictError";
|
|
5867
|
-
}
|
|
5868
|
-
};
|
|
5869
|
-
/** The memory directory for an agent, given its root dir (where agent.json lives). */
|
|
5870
|
-
function memoryDir(agentDir) {
|
|
5871
|
-
return join(agentDir, MEMORY_DIRNAME);
|
|
5872
|
-
}
|
|
5873
|
-
/** Resolve a memory-dir-relative path to an absolute one, contained to the dir.
|
|
5874
|
-
* Rejects absolute paths, `..` escapes, and any nested directory (flat only). */
|
|
5875
|
-
function resolveInMemoryDir(agentDir, relPath) {
|
|
5876
|
-
if (isAbsolute(relPath)) throw new MemoryPathError("absolute paths are not allowed");
|
|
5877
|
-
const dir = memoryDir(agentDir);
|
|
5878
|
-
const abs = resolve(dir, relPath);
|
|
5879
|
-
if (abs !== dir && !abs.startsWith(dir + sep)) throw new MemoryPathError("path escapes the memory dir");
|
|
5880
|
-
const rel = abs.slice(dir.length + 1);
|
|
5881
|
-
if (rel.includes(sep)) throw new MemoryPathError("memory is a flat directory (no subdirectories)");
|
|
5882
|
-
if (rel.length === 0) throw new MemoryPathError("a file name is required");
|
|
5883
|
-
return abs;
|
|
5884
|
-
}
|
|
5885
|
-
/** Seed `<agent-root>/memory/MEMORY.md` with a header if the dir/index is absent
|
|
5886
|
-
* (idempotent). Called at session start so a brand-new agent always has an index. */
|
|
5887
|
-
function ensureMemory(agentDir, agentName) {
|
|
5888
|
-
const dir = memoryDir(agentDir);
|
|
5889
|
-
mkdirSync(dir, { recursive: true });
|
|
5890
|
-
const index = join(dir, MEMORY_INDEX);
|
|
5891
|
-
if (!existsSync(index)) writeFileSync(index, `# ${agentName} memory\n\nYour long-term memory index. Keep it short — link out to topic files (e.g. \`[deploys](deploys.md)\`) for detail and read them on demand with the readMemory tool.\n`);
|
|
5892
|
-
}
|
|
5893
|
-
/** Read the memory index (`MEMORY.md`) for prompt injection, trimmed to the inject
|
|
5894
|
-
* cap so a large index can't blow up the prompt; '' when absent. */
|
|
5895
|
-
async function readIndex(agentDir) {
|
|
5896
|
-
const text = await readMemoryFile(agentDir, MEMORY_INDEX);
|
|
5897
|
-
if (Buffer.byteLength(text) <= 25e3) return text;
|
|
5898
|
-
return Buffer.from(text, "utf8").subarray(0, MAX_INDEX_INJECT_BYTES).toString("utf8") + "\n\n[…memory index truncated — trim MEMORY.md]";
|
|
5899
|
-
}
|
|
5900
|
-
/** Read a memory file's text; '' when it does not exist (never throws on ENOENT). */
|
|
5901
|
-
async function readMemoryFile(agentDir, relPath) {
|
|
5902
|
-
const abs = resolveInMemoryDir(agentDir, relPath);
|
|
5903
|
-
try {
|
|
5904
|
-
return await promises.readFile(abs, "utf8");
|
|
5905
|
-
} catch (err) {
|
|
5906
|
-
if (err.code === "ENOENT") return "";
|
|
5907
|
-
throw err;
|
|
5908
|
-
}
|
|
5909
|
-
}
|
|
5910
|
-
/** Overwrite a memory file with `content` (creating the dir if needed). Atomic
|
|
5911
|
-
* (tmp + rename) so a concurrent read never sees a partial file.
|
|
5912
|
-
*
|
|
5913
|
-
* - Rejects content over {@link MAX_MEMORY_FILE_BYTES} ({@link MemoryTooLargeError}).
|
|
5914
|
-
* - `ifMatchMtime` (optimistic concurrency): when given, the current file's mtime
|
|
5915
|
-
* must equal it, else {@link MemoryConflictError} — so a console edit can't
|
|
5916
|
-
* clobber a newer write. A brand-new file (no mtime) matches `ifMatchMtime`
|
|
5917
|
-
* only when the caller passes none (or the empty string). */
|
|
5918
|
-
async function writeMemoryFile(agentDir, relPath, content, ifMatchMtime) {
|
|
5919
|
-
if (Buffer.byteLength(content) > 256e3) throw new MemoryTooLargeError(`memory file exceeds the ${MAX_MEMORY_FILE_BYTES}-byte limit`);
|
|
5920
|
-
const abs = resolveInMemoryDir(agentDir, relPath);
|
|
5921
|
-
if (ifMatchMtime) {
|
|
5922
|
-
let current = null;
|
|
5923
|
-
try {
|
|
5924
|
-
current = (await promises.stat(abs)).mtime.toISOString();
|
|
5925
|
-
} catch (err) {
|
|
5926
|
-
if (err.code !== "ENOENT") throw err;
|
|
5927
|
-
}
|
|
5928
|
-
if (current !== ifMatchMtime) throw new MemoryConflictError("the memory file changed since it was read; reload and retry");
|
|
5929
|
-
}
|
|
5930
|
-
await promises.mkdir(memoryDir(agentDir), { recursive: true });
|
|
5931
|
-
const tmp = `${abs}.tmp`;
|
|
5932
|
-
await promises.writeFile(tmp, content, "utf8");
|
|
5933
|
-
await promises.rename(tmp, abs);
|
|
5934
|
-
const st = await promises.stat(abs);
|
|
5935
|
-
return {
|
|
5936
|
-
size: st.size,
|
|
5937
|
-
mtime: st.mtime.toISOString()
|
|
5938
|
-
};
|
|
5939
|
-
}
|
|
5940
|
-
/** List the files in the memory dir (flat; index + topics), sorted with the index
|
|
5941
|
-
* first. Empty when the dir does not exist. `.tmp` write artifacts are skipped. */
|
|
5942
|
-
async function listMemory(agentDir) {
|
|
5943
|
-
const dir = memoryDir(agentDir);
|
|
5944
|
-
let dirents;
|
|
5945
|
-
try {
|
|
5946
|
-
dirents = await promises.readdir(dir, { withFileTypes: true });
|
|
5947
|
-
} catch (err) {
|
|
5948
|
-
if (err.code === "ENOENT") return [];
|
|
5949
|
-
throw err;
|
|
5950
|
-
}
|
|
5951
|
-
const files = [];
|
|
5952
|
-
for (const d of dirents) {
|
|
5953
|
-
if (!d.isFile() || d.name.endsWith(".tmp")) continue;
|
|
5954
|
-
try {
|
|
5955
|
-
const st = await promises.stat(join(dir, d.name));
|
|
5956
|
-
files.push({
|
|
5957
|
-
name: d.name,
|
|
5958
|
-
size: st.size,
|
|
5959
|
-
mtime: st.mtime.toISOString()
|
|
5960
|
-
});
|
|
5961
|
-
} catch {}
|
|
5962
|
-
}
|
|
5963
|
-
files.sort((a, b) => a.name === "MEMORY.md" ? -1 : b.name === "MEMORY.md" ? 1 : a.name.localeCompare(b.name));
|
|
5964
|
-
return files;
|
|
5965
|
-
}
|
|
5966
|
-
//#endregion
|
|
5967
6912
|
//#region src/session/attachment-block.ts
|
|
5968
6913
|
/**
|
|
5969
6914
|
* Turn one Slack attachment into the richest ACP content block the agent can
|
|
@@ -119024,469 +119969,41 @@ function makeLogger(level = "info") {
|
|
|
119024
119969
|
};
|
|
119025
119970
|
}
|
|
119026
119971
|
//#endregion
|
|
119027
|
-
//#region src/cp/
|
|
119028
|
-
/**
|
|
119029
|
-
* Pure helpers for slicing file bytes into a single WS frame — shared by the
|
|
119030
|
-
* workspace and memory readers so both honour the 256 KiB wire cap and never
|
|
119031
|
-
* split a UTF-8 character across slices.
|
|
119032
|
-
*/
|
|
119033
|
-
/** Encoded-payload ceiling, leaving headroom under MAX_FRAME_BYTES for the
|
|
119034
|
-
* envelope (id/ts/type/corr + fencing ext, well under 4 KiB). */
|
|
119035
|
-
const REPLY_BUDGET$1 = MAX_FRAME_BYTES - 4096;
|
|
119036
|
-
/** The encoded size of the payload the wire will carry (JSON.stringify matches
|
|
119037
|
-
* the codec's `encode`), measured in bytes. */
|
|
119038
|
-
function encodedBytes$1(payload) {
|
|
119039
|
-
return Buffer.byteLength(JSON.stringify(payload));
|
|
119040
|
-
}
|
|
119041
|
-
/** Largest index ≤ `len` that lands on a UTF-8 character boundary — i.e. never
|
|
119042
|
-
* cuts a multi-byte sequence. If the slice's trailing char is incomplete, the
|
|
119043
|
-
* boundary is placed before that char (its bytes belong to the next slice). */
|
|
119044
|
-
function utf8Boundary$1(buf, len) {
|
|
119045
|
-
if (len >= buf.length) len = buf.length;
|
|
119046
|
-
if (len <= 0) return 0;
|
|
119047
|
-
let start = len - 1;
|
|
119048
|
-
while (start > 0 && ((buf[start] ?? 0) & 192) === 128) start--;
|
|
119049
|
-
const lead = buf[start] ?? 0;
|
|
119050
|
-
const seqLen = lead < 128 ? 1 : (lead & 224) === 192 ? 2 : (lead & 240) === 224 ? 3 : (lead & 248) === 240 ? 4 : 1;
|
|
119051
|
-
return start + seqLen <= len ? len : start;
|
|
119052
|
-
}
|
|
119053
|
-
/** Shrink `content` (a utf8 view of `slice[0..end]`) until its JSON-escaped size
|
|
119054
|
-
* fits `REPLY_BUDGET`, keeping the cut on a UTF-8 boundary. Returns the final
|
|
119055
|
-
* `{ end, content }`. Control-byte-heavy text escapes ~6×, so the raw byte limit
|
|
119056
|
-
* is not enough on its own. */
|
|
119057
|
-
function fitToBudget(slice, end) {
|
|
119058
|
-
let content = slice.toString("utf8", 0, end);
|
|
119059
|
-
while (end > 0 && encodedBytes$1(content) > REPLY_BUDGET$1) {
|
|
119060
|
-
const factor = REPLY_BUDGET$1 / encodedBytes$1(content);
|
|
119061
|
-
const shrunk = Math.max(1, Math.floor(end * factor * .9));
|
|
119062
|
-
end = utf8Boundary$1(slice, Math.min(shrunk, end - 1));
|
|
119063
|
-
content = slice.toString("utf8", 0, end);
|
|
119064
|
-
}
|
|
119065
|
-
return {
|
|
119066
|
-
end,
|
|
119067
|
-
content
|
|
119068
|
-
};
|
|
119069
|
-
}
|
|
119070
|
-
//#endregion
|
|
119071
|
-
//#region src/cp/workspace-reader.ts
|
|
119972
|
+
//#region src/cp/relay-client.ts
|
|
119072
119973
|
/**
|
|
119073
|
-
* `
|
|
119074
|
-
*
|
|
119075
|
-
*
|
|
119076
|
-
* never whole trees or files.
|
|
119974
|
+
* `RelayClient` — the daemon's dial-out client to ONE relay over the `rd/*` wire
|
|
119975
|
+
* (shared-bot-relay.md §11 / §7.2). The daemon holds one to every relay in its
|
|
119976
|
+
* roster; {@link RelayManager} set-converges them.
|
|
119077
119977
|
*
|
|
119078
|
-
*
|
|
119079
|
-
*
|
|
119080
|
-
*
|
|
119081
|
-
*
|
|
119082
|
-
*
|
|
119083
|
-
*
|
|
119084
|
-
*
|
|
119085
|
-
* which the dispatcher maps to a `BAD_PAYLOAD` error frame; missing paths are
|
|
119086
|
-
* data (`exists:false`), not errors. Never log file contents.
|
|
119087
|
-
*
|
|
119088
|
-
* Frame-size safety: every REP must serialise under the 256 KiB wire cap. JSON
|
|
119089
|
-
* escaping of control bytes is a 6× blowup, so `read` bounds the slice by the
|
|
119090
|
-
* *encoded* reply size (not the raw byte `limit`) and `list` stops a page early
|
|
119091
|
-
* when the encoded entries would overflow — both leave headroom for the envelope.
|
|
119978
|
+
* Milestone A2 establishes the connection: dial → `rd/hello` (presenting the
|
|
119979
|
+
* daemon's existing CP API key, which the relay delegates to the CP via
|
|
119980
|
+
* `rc/verify`) → `rd/hello/ok`, whose echoed `relayId` MUST match the roster entry
|
|
119981
|
+
* dialed (a mismatch = a pool LB in front of the daemon dial path → close +
|
|
119982
|
+
* backoff, never continue on the wrong instance, §5). Content (`rd/msg`) flows in
|
|
119983
|
+
* PR 3. Built on the shared `@agentconnect.md/connection` primitives — the first
|
|
119984
|
+
* daemon consumer of that package.
|
|
119092
119985
|
*/
|
|
119093
|
-
/** Bytes sniffed from the head of a file for binary (NUL byte) detection. */
|
|
119094
|
-
const SNIFF_BYTES = 8192;
|
|
119095
|
-
/** Path-containment / bad-request violation → `BAD_PAYLOAD` on the wire. */
|
|
119096
|
-
var WorkspaceViolationError = class extends Error {
|
|
119097
|
-
constructor(message) {
|
|
119098
|
-
super(message);
|
|
119099
|
-
this.name = "WorkspaceViolationError";
|
|
119100
|
-
}
|
|
119101
|
-
};
|
|
119102
|
-
function isErrno$1(err, code) {
|
|
119103
|
-
return err?.code === code;
|
|
119104
|
-
}
|
|
119105
|
-
function under(root, p) {
|
|
119106
|
-
return p === root || p.startsWith(root + sp.sep);
|
|
119107
|
-
}
|
|
119108
|
-
function createWorkspaceReader(workspaceRootByAgent) {
|
|
119109
|
-
function rootFor(agentId) {
|
|
119110
|
-
const root = workspaceRootByAgent(agentId);
|
|
119111
|
-
if (!root) throw new WorkspaceViolationError(`unknown agent "${agentId}"`);
|
|
119112
|
-
return root;
|
|
119113
|
-
}
|
|
119114
|
-
/**
|
|
119115
|
-
* Lexical containment. Rejects absolute paths and `..` escapes; returns the
|
|
119116
|
-
* lexically-resolved path plus the canonical (realpath'd) root, or `null` for
|
|
119117
|
-
* `realRoot` when the root does not exist yet (callers report `exists:false`).
|
|
119118
|
-
*/
|
|
119119
|
-
async function resolveContained(root, relPath) {
|
|
119120
|
-
if (sp.isAbsolute(relPath)) throw new WorkspaceViolationError("absolute paths are not allowed");
|
|
119121
|
-
const resolved = sp.resolve(root, relPath);
|
|
119122
|
-
if (!under(root, resolved)) throw new WorkspaceViolationError("path escapes the workspace root");
|
|
119123
|
-
try {
|
|
119124
|
-
return {
|
|
119125
|
-
resolved,
|
|
119126
|
-
realRoot: await promises.realpath(root)
|
|
119127
|
-
};
|
|
119128
|
-
} catch (err) {
|
|
119129
|
-
if (isErrno$1(err, "ENOENT")) return {
|
|
119130
|
-
resolved,
|
|
119131
|
-
realRoot: null
|
|
119132
|
-
};
|
|
119133
|
-
throw err;
|
|
119134
|
-
}
|
|
119135
|
-
}
|
|
119136
|
-
/** Canonicalise an existing target and re-verify containment on the REAL path,
|
|
119137
|
-
* closing the check-vs-use gap (a symlinked intermediate component resolves
|
|
119138
|
-
* out and is rejected here). Returns the canonical path used for I/O. */
|
|
119139
|
-
async function canonicalUnder(realRoot, abs) {
|
|
119140
|
-
const canon = await promises.realpath(abs);
|
|
119141
|
-
if (!under(realRoot, canon)) throw new WorkspaceViolationError("path resolves outside the workspace root");
|
|
119142
|
-
return canon;
|
|
119143
|
-
}
|
|
119144
|
-
return {
|
|
119145
|
-
async list(req) {
|
|
119146
|
-
const { resolved, realRoot } = await resolveContained(rootFor(req.agentId), req.path);
|
|
119147
|
-
const notFound = {
|
|
119148
|
-
agentId: req.agentId,
|
|
119149
|
-
path: req.path,
|
|
119150
|
-
exists: false,
|
|
119151
|
-
entries: []
|
|
119152
|
-
};
|
|
119153
|
-
if (realRoot === null) return notFound;
|
|
119154
|
-
let target;
|
|
119155
|
-
try {
|
|
119156
|
-
target = await canonicalUnder(realRoot, resolved);
|
|
119157
|
-
} catch (err) {
|
|
119158
|
-
if (isErrno$1(err, "ENOENT")) return notFound;
|
|
119159
|
-
throw err;
|
|
119160
|
-
}
|
|
119161
|
-
let dirents;
|
|
119162
|
-
try {
|
|
119163
|
-
dirents = await promises.readdir(target, { withFileTypes: true });
|
|
119164
|
-
} catch (err) {
|
|
119165
|
-
if (isErrno$1(err, "ENOENT") || isErrno$1(err, "ENOTDIR")) return notFound;
|
|
119166
|
-
throw err;
|
|
119167
|
-
}
|
|
119168
|
-
const entries = [];
|
|
119169
|
-
for (const d of dirents) {
|
|
119170
|
-
if (d.name === ".git") continue;
|
|
119171
|
-
const type = d.isDirectory() ? "dir" : d.isFile() ? "file" : d.isSymbolicLink() ? "symlink" : "other";
|
|
119172
|
-
const entry = {
|
|
119173
|
-
name: d.name,
|
|
119174
|
-
type
|
|
119175
|
-
};
|
|
119176
|
-
if (type === "file") try {
|
|
119177
|
-
const st = await promises.lstat(sp.join(target, d.name));
|
|
119178
|
-
entry.size = st.size;
|
|
119179
|
-
entry.mtime = st.mtime.toISOString();
|
|
119180
|
-
} catch {}
|
|
119181
|
-
entries.push(entry);
|
|
119182
|
-
}
|
|
119183
|
-
entries.sort(compareEntries);
|
|
119184
|
-
const start = req.cursor !== void 0 && /^\d+$/.test(req.cursor) ? Number(req.cursor) : 0;
|
|
119185
|
-
const page = [];
|
|
119186
|
-
let acc = 0;
|
|
119187
|
-
for (let i = start; i < entries.length && page.length < req.limit; i++) {
|
|
119188
|
-
const e = entries[i];
|
|
119189
|
-
const enc = encodedBytes$1(e) + 1;
|
|
119190
|
-
if (page.length > 0 && acc + enc > REPLY_BUDGET$1) break;
|
|
119191
|
-
page.push(e);
|
|
119192
|
-
acc += enc;
|
|
119193
|
-
}
|
|
119194
|
-
const consumed = start + page.length;
|
|
119195
|
-
return {
|
|
119196
|
-
agentId: req.agentId,
|
|
119197
|
-
path: req.path,
|
|
119198
|
-
exists: true,
|
|
119199
|
-
entries: page,
|
|
119200
|
-
...consumed < entries.length ? { nextCursor: String(consumed) } : {}
|
|
119201
|
-
};
|
|
119202
|
-
},
|
|
119203
|
-
async read(req) {
|
|
119204
|
-
const { resolved, realRoot } = await resolveContained(rootFor(req.agentId), req.path);
|
|
119205
|
-
const notFound = {
|
|
119206
|
-
agentId: req.agentId,
|
|
119207
|
-
path: req.path,
|
|
119208
|
-
exists: false
|
|
119209
|
-
};
|
|
119210
|
-
if (realRoot === null) return notFound;
|
|
119211
|
-
let st;
|
|
119212
|
-
try {
|
|
119213
|
-
st = await promises.lstat(resolved);
|
|
119214
|
-
} catch (err) {
|
|
119215
|
-
if (isErrno$1(err, "ENOENT")) return notFound;
|
|
119216
|
-
throw err;
|
|
119217
|
-
}
|
|
119218
|
-
if (!st.isFile()) throw new WorkspaceViolationError("not a regular file");
|
|
119219
|
-
const target = await canonicalUnder(realRoot, resolved);
|
|
119220
|
-
const size = st.size;
|
|
119221
|
-
const mtime = st.mtime.toISOString();
|
|
119222
|
-
const fh = await promises.open(target, "r");
|
|
119223
|
-
try {
|
|
119224
|
-
const sniffLen = Math.min(SNIFF_BYTES, size);
|
|
119225
|
-
if (sniffLen > 0) {
|
|
119226
|
-
const sniff = Buffer.alloc(sniffLen);
|
|
119227
|
-
const { bytesRead } = await fh.read(sniff, 0, sniffLen, 0);
|
|
119228
|
-
if (sniff.subarray(0, bytesRead).includes(0)) return {
|
|
119229
|
-
agentId: req.agentId,
|
|
119230
|
-
path: req.path,
|
|
119231
|
-
exists: true,
|
|
119232
|
-
size,
|
|
119233
|
-
mtime,
|
|
119234
|
-
encoding: "none"
|
|
119235
|
-
};
|
|
119236
|
-
}
|
|
119237
|
-
const want = Math.min(req.limit, Math.max(0, size - req.offset));
|
|
119238
|
-
let slice = Buffer.alloc(0);
|
|
119239
|
-
if (want > 0) {
|
|
119240
|
-
const buf = Buffer.alloc(want);
|
|
119241
|
-
const { bytesRead } = await fh.read(buf, 0, want, req.offset);
|
|
119242
|
-
slice = buf.subarray(0, bytesRead);
|
|
119243
|
-
}
|
|
119244
|
-
let end = utf8Boundary$1(slice, slice.length);
|
|
119245
|
-
const fitted = fitToBudget(slice, end);
|
|
119246
|
-
end = fitted.end;
|
|
119247
|
-
const content = fitted.content;
|
|
119248
|
-
const nextOffset = req.offset + end;
|
|
119249
|
-
return {
|
|
119250
|
-
agentId: req.agentId,
|
|
119251
|
-
path: req.path,
|
|
119252
|
-
exists: true,
|
|
119253
|
-
size,
|
|
119254
|
-
mtime,
|
|
119255
|
-
encoding: "utf8",
|
|
119256
|
-
content,
|
|
119257
|
-
offset: req.offset,
|
|
119258
|
-
nextOffset,
|
|
119259
|
-
truncated: nextOffset < size
|
|
119260
|
-
};
|
|
119261
|
-
} finally {
|
|
119262
|
-
await fh.close();
|
|
119263
|
-
}
|
|
119264
|
-
}
|
|
119265
|
-
};
|
|
119266
|
-
}
|
|
119267
|
-
/** dirs first, then case-insensitive alphabetical (stable within the page). */
|
|
119268
|
-
function compareEntries(a, b) {
|
|
119269
|
-
const aDir = a.type === "dir" ? 0 : 1;
|
|
119270
|
-
const bDir = b.type === "dir" ? 0 : 1;
|
|
119271
|
-
if (aDir !== bDir) return aDir - bDir;
|
|
119272
|
-
const al = a.name.toLowerCase();
|
|
119273
|
-
const bl = b.name.toLowerCase();
|
|
119274
|
-
if (al !== bl) return al < bl ? -1 : 1;
|
|
119275
|
-
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
119276
|
-
}
|
|
119277
|
-
//#endregion
|
|
119278
|
-
//#region src/cp/memory-reader.ts
|
|
119279
|
-
/**
|
|
119280
|
-
* `MemoryReader` — the seam answering the CP's `memory/list`, `memory/read` and
|
|
119281
|
-
* `memory/write` REQs for an agent's memory directory (`<agent-root>/memory/`).
|
|
119282
|
-
* The files live only on the daemon (§1/§12); reads stream single byte slices,
|
|
119283
|
-
* writes replace one whole file.
|
|
119284
|
-
*
|
|
119285
|
-
* Path containment lives in `agents/memory.ts` (`resolveInMemoryDir`): a topic
|
|
119286
|
-
* path that escapes the memory dir throws `MemoryPathError` → `BAD_PAYLOAD`. An
|
|
119287
|
-
* unknown agent is also `BAD_PAYLOAD`; a not-yet-created file/dir is data
|
|
119288
|
-
* (`exists:false`), not an error. Frame-size safety mirrors the workspace reader:
|
|
119289
|
-
* the read slice is bounded by the *encoded* reply size and ends on a UTF-8
|
|
119290
|
-
* boundary. Never log memory contents.
|
|
119291
|
-
*/
|
|
119292
|
-
/** Unknown-agent violation → `BAD_PAYLOAD` on the wire (path escapes surface as
|
|
119293
|
-
* MemoryPathError from agents/memory.ts, mapped the same way by the dispatcher). */
|
|
119294
|
-
var MemoryViolationError = class extends Error {
|
|
119295
|
-
constructor(message) {
|
|
119296
|
-
super(message);
|
|
119297
|
-
this.name = "MemoryViolationError";
|
|
119298
|
-
}
|
|
119299
|
-
};
|
|
119300
|
-
function isErrno(err, code) {
|
|
119301
|
-
return err?.code === code;
|
|
119302
|
-
}
|
|
119303
|
-
/** `agentDirByAgent` resolves an agent id → its root dir (which holds `memory/`),
|
|
119304
|
-
* or undefined for an unknown agent. */
|
|
119305
|
-
function createMemoryReader(agentDirByAgent) {
|
|
119306
|
-
function dirFor(agentId) {
|
|
119307
|
-
const dir = agentDirByAgent(agentId);
|
|
119308
|
-
if (!dir) throw new MemoryViolationError(`unknown agent "${agentId}"`);
|
|
119309
|
-
return dir;
|
|
119310
|
-
}
|
|
119311
|
-
return {
|
|
119312
|
-
async list(req) {
|
|
119313
|
-
const files = await listMemory(dirFor(req.agentId));
|
|
119314
|
-
return {
|
|
119315
|
-
agentId: req.agentId,
|
|
119316
|
-
exists: files.length > 0,
|
|
119317
|
-
entries: files
|
|
119318
|
-
};
|
|
119319
|
-
},
|
|
119320
|
-
async read(req) {
|
|
119321
|
-
const file = resolveInMemoryDir(dirFor(req.agentId), req.path);
|
|
119322
|
-
const notFound = {
|
|
119323
|
-
agentId: req.agentId,
|
|
119324
|
-
path: req.path,
|
|
119325
|
-
exists: false
|
|
119326
|
-
};
|
|
119327
|
-
let st;
|
|
119328
|
-
try {
|
|
119329
|
-
st = await promises.stat(file);
|
|
119330
|
-
} catch (err) {
|
|
119331
|
-
if (isErrno(err, "ENOENT")) return notFound;
|
|
119332
|
-
throw err;
|
|
119333
|
-
}
|
|
119334
|
-
if (!st.isFile()) return notFound;
|
|
119335
|
-
const size = st.size;
|
|
119336
|
-
const mtime = st.mtime.toISOString();
|
|
119337
|
-
const fh = await promises.open(file, "r");
|
|
119338
|
-
try {
|
|
119339
|
-
const want = Math.min(req.limit, Math.max(0, size - req.offset));
|
|
119340
|
-
let slice = Buffer.alloc(0);
|
|
119341
|
-
if (want > 0) {
|
|
119342
|
-
const buf = Buffer.alloc(want);
|
|
119343
|
-
const { bytesRead } = await fh.read(buf, 0, want, req.offset);
|
|
119344
|
-
slice = buf.subarray(0, bytesRead);
|
|
119345
|
-
}
|
|
119346
|
-
const { end, content } = fitToBudget(slice, utf8Boundary$1(slice, slice.length));
|
|
119347
|
-
const nextOffset = req.offset + end;
|
|
119348
|
-
return {
|
|
119349
|
-
agentId: req.agentId,
|
|
119350
|
-
path: req.path,
|
|
119351
|
-
exists: true,
|
|
119352
|
-
size,
|
|
119353
|
-
mtime,
|
|
119354
|
-
content,
|
|
119355
|
-
offset: req.offset,
|
|
119356
|
-
nextOffset,
|
|
119357
|
-
truncated: nextOffset < size
|
|
119358
|
-
};
|
|
119359
|
-
} finally {
|
|
119360
|
-
await fh.close();
|
|
119361
|
-
}
|
|
119362
|
-
},
|
|
119363
|
-
async write(req) {
|
|
119364
|
-
const { size, mtime } = await writeMemoryFile(dirFor(req.agentId), req.path, req.content, req.ifMatchMtime);
|
|
119365
|
-
return {
|
|
119366
|
-
agentId: req.agentId,
|
|
119367
|
-
path: req.path,
|
|
119368
|
-
size,
|
|
119369
|
-
mtime
|
|
119370
|
-
};
|
|
119371
|
-
}
|
|
119372
|
-
};
|
|
119373
|
-
}
|
|
119374
|
-
//#endregion
|
|
119375
|
-
//#region src/cp/correlator.ts
|
|
119376
|
-
/** Transport-free representation of a typed wire `error` REP. */
|
|
119377
|
-
var WireError = class extends Error {
|
|
119378
|
-
code;
|
|
119379
|
-
retryable;
|
|
119380
|
-
details;
|
|
119381
|
-
constructor(code, message, retryable = false, details) {
|
|
119382
|
-
super(message);
|
|
119383
|
-
this.name = "WireError";
|
|
119384
|
-
this.code = code;
|
|
119385
|
-
this.retryable = retryable;
|
|
119386
|
-
if (details) this.details = details;
|
|
119387
|
-
}
|
|
119388
|
-
};
|
|
119389
|
-
var ReqRep = class {
|
|
119390
|
-
clock;
|
|
119391
|
-
ackTimeoutMs;
|
|
119392
|
-
maxTries;
|
|
119393
|
-
pending = /* @__PURE__ */ new Map();
|
|
119394
|
-
constructor(clock, ackTimeoutMs, maxTries = 5) {
|
|
119395
|
-
this.clock = clock;
|
|
119396
|
-
this.ackTimeoutMs = ackTimeoutMs;
|
|
119397
|
-
this.maxTries = maxTries;
|
|
119398
|
-
}
|
|
119399
|
-
request(frame, write, opts = {}) {
|
|
119400
|
-
const encoded = JSON.stringify(frame);
|
|
119401
|
-
return new Promise((resolve, reject) => {
|
|
119402
|
-
const entry = {
|
|
119403
|
-
id: frame.id,
|
|
119404
|
-
encoded,
|
|
119405
|
-
resolve,
|
|
119406
|
-
reject,
|
|
119407
|
-
tries: 1,
|
|
119408
|
-
maxTries: opts.maxTries ?? this.maxTries
|
|
119409
|
-
};
|
|
119410
|
-
this.pending.set(frame.id, entry);
|
|
119411
|
-
this.arm(entry, write, opts.ackTimeoutMs ?? this.ackTimeoutMs);
|
|
119412
|
-
try {
|
|
119413
|
-
write(encoded);
|
|
119414
|
-
} catch (e) {
|
|
119415
|
-
this.fail(entry, new WireError("INTERNAL", `send failed: ${e.message}`, true));
|
|
119416
|
-
}
|
|
119417
|
-
});
|
|
119418
|
-
}
|
|
119419
|
-
arm(entry, write, ackTimeoutMs) {
|
|
119420
|
-
entry.timer = this.clock.setTimeout(() => {
|
|
119421
|
-
if (!this.pending.has(entry.id)) return;
|
|
119422
|
-
if (entry.tries >= entry.maxTries) {
|
|
119423
|
-
this.pending.delete(entry.id);
|
|
119424
|
-
entry.reject(new WireError("INTERNAL", `no ack after ${entry.maxTries} tries for ${entry.id}`, true));
|
|
119425
|
-
return;
|
|
119426
|
-
}
|
|
119427
|
-
entry.tries += 1;
|
|
119428
|
-
this.arm(entry, write, ackTimeoutMs);
|
|
119429
|
-
try {
|
|
119430
|
-
write(entry.encoded);
|
|
119431
|
-
} catch (e) {
|
|
119432
|
-
this.fail(entry, new WireError("INTERNAL", `send failed: ${e.message}`, true));
|
|
119433
|
-
}
|
|
119434
|
-
}, ackTimeoutMs);
|
|
119435
|
-
}
|
|
119436
|
-
fail(entry, err) {
|
|
119437
|
-
if (!this.pending.has(entry.id)) return;
|
|
119438
|
-
this.pending.delete(entry.id);
|
|
119439
|
-
if (entry.timer !== void 0) this.clock.clearTimeout(entry.timer);
|
|
119440
|
-
entry.reject(err);
|
|
119441
|
-
}
|
|
119442
|
-
settle(frame) {
|
|
119443
|
-
const corr = frame.corr;
|
|
119444
|
-
if (!corr) return false;
|
|
119445
|
-
const entry = this.pending.get(corr);
|
|
119446
|
-
if (!entry) return false;
|
|
119447
|
-
this.pending.delete(corr);
|
|
119448
|
-
if (entry.timer !== void 0) this.clock.clearTimeout(entry.timer);
|
|
119449
|
-
if (frame.type === "error") {
|
|
119450
|
-
const e = frame.payload;
|
|
119451
|
-
entry.reject(new WireError(e.code, e.message, e.retryable, e.details));
|
|
119452
|
-
} else entry.resolve(frame);
|
|
119453
|
-
return true;
|
|
119454
|
-
}
|
|
119455
|
-
inflight() {
|
|
119456
|
-
return this.pending.size;
|
|
119457
|
-
}
|
|
119458
|
-
rejectAll(err) {
|
|
119459
|
-
for (const entry of this.pending.values()) {
|
|
119460
|
-
if (entry.timer !== void 0) this.clock.clearTimeout(entry.timer);
|
|
119461
|
-
entry.reject(err);
|
|
119462
|
-
}
|
|
119463
|
-
this.pending.clear();
|
|
119464
|
-
}
|
|
119465
|
-
};
|
|
119466
|
-
//#endregion
|
|
119467
|
-
//#region src/cp/client.ts
|
|
119468
119986
|
const ACK_TIMEOUT_MS = 5e3;
|
|
119469
|
-
|
|
119470
|
-
const
|
|
119471
|
-
var
|
|
119987
|
+
/** rd/hello definitively rejected (bad/revoked key, daemonId mismatch) → stop dialing THIS relay. */
|
|
119988
|
+
const CLOSE_AUTH_FAILED = 4401;
|
|
119989
|
+
var RelayClient = class {
|
|
119990
|
+
relayId;
|
|
119991
|
+
url;
|
|
119472
119992
|
deps;
|
|
119473
119993
|
state = "CLOSED";
|
|
119474
|
-
sessionEpoch = 0;
|
|
119475
|
-
routingEpoch = 0;
|
|
119476
119994
|
transport;
|
|
119477
119995
|
correlator;
|
|
119996
|
+
backoff;
|
|
119478
119997
|
stopped = false;
|
|
119479
119998
|
fatal = false;
|
|
119480
|
-
attempt = 0;
|
|
119481
119999
|
reconnectTimer;
|
|
119482
|
-
|
|
119483
|
-
|
|
119484
|
-
|
|
119485
|
-
constructor(deps) {
|
|
120000
|
+
constructor(relayId, url, deps) {
|
|
120001
|
+
this.relayId = relayId;
|
|
120002
|
+
this.url = url;
|
|
119486
120003
|
this.deps = deps;
|
|
119487
120004
|
this.correlator = new ReqRep(deps.clock, ACK_TIMEOUT_MS);
|
|
120005
|
+
this.backoff = new Backoff(deps.jitter ? { jitter: deps.jitter } : {});
|
|
119488
120006
|
}
|
|
119489
|
-
/** Non-blocking: kicks off the connect loop and returns. */
|
|
119490
120007
|
start() {
|
|
119491
120008
|
this.stopped = false;
|
|
119492
120009
|
this.fatal = false;
|
|
@@ -119498,16 +120015,24 @@ var CpClient = class {
|
|
|
119498
120015
|
this.deps.clock.clearTimeout(this.reconnectTimer);
|
|
119499
120016
|
this.reconnectTimer = void 0;
|
|
119500
120017
|
}
|
|
119501
|
-
this.stopHeartbeat();
|
|
119502
120018
|
this.correlator.rejectAll(/* @__PURE__ */ new Error("stopping"));
|
|
119503
120019
|
this.transport?.close(1e3, "shutdown");
|
|
119504
120020
|
this.state = "CLOSED";
|
|
119505
120021
|
}
|
|
120022
|
+
isReady() {
|
|
120023
|
+
return this.state === "READY";
|
|
120024
|
+
}
|
|
119506
120025
|
async attemptConnect() {
|
|
119507
120026
|
if (this.stopped || this.fatal) return;
|
|
120027
|
+
const daemonId = this.deps.daemonId();
|
|
120028
|
+
if (!daemonId) {
|
|
120029
|
+
this.state = "DEGRADED";
|
|
120030
|
+
this.scheduleReconnect();
|
|
120031
|
+
return;
|
|
120032
|
+
}
|
|
119508
120033
|
this.state = "CONNECTING";
|
|
119509
120034
|
try {
|
|
119510
|
-
const t = await this.deps.connect();
|
|
120035
|
+
const t = await this.deps.connect(this.url);
|
|
119511
120036
|
if (this.stopped || this.fatal) {
|
|
119512
120037
|
t.close(1e3, "shutdown");
|
|
119513
120038
|
return;
|
|
@@ -119515,196 +120040,44 @@ var CpClient = class {
|
|
|
119515
120040
|
this.transport = t;
|
|
119516
120041
|
t.onMessage((txt) => void this.onText(txt));
|
|
119517
120042
|
t.onClose((c, r) => this.onClose(c, r));
|
|
119518
|
-
await this.handshake();
|
|
119519
|
-
this.
|
|
120043
|
+
await this.handshake(daemonId);
|
|
120044
|
+
this.backoff.reset();
|
|
119520
120045
|
} catch (err) {
|
|
119521
|
-
this.deps.log.warn(`
|
|
120046
|
+
this.deps.log.warn(`relay(${this.relayId}): connect/hello failed: ${err.message}`);
|
|
119522
120047
|
this.transport?.close(1011, "handshake failed");
|
|
119523
120048
|
this.transport = void 0;
|
|
119524
120049
|
this.scheduleReconnect();
|
|
119525
120050
|
}
|
|
119526
120051
|
}
|
|
119527
|
-
|
|
119528
|
-
|
|
119529
|
-
|
|
119530
|
-
|
|
119531
|
-
|
|
119532
|
-
|
|
119533
|
-
|
|
119534
|
-
|
|
119535
|
-
|
|
119536
|
-
this.attemptConnect();
|
|
119537
|
-
}, delay);
|
|
119538
|
-
}
|
|
119539
|
-
async handshake() {
|
|
119540
|
-
this.state = "AUTHENTICATING";
|
|
119541
|
-
const authPayload = {
|
|
119542
|
-
apiKey: this.deps.token,
|
|
119543
|
-
agentVersion: this.deps.agentVersion
|
|
119544
|
-
};
|
|
119545
|
-
if (this.deps.daemonId) authPayload.daemonId = this.deps.daemonId;
|
|
119546
|
-
if (this.lastAuthedEpoch > 0) authPayload.resume = { lastEpoch: this.lastAuthedEpoch };
|
|
119547
|
-
const ok = (await this.request("auth", authPayload)).payload;
|
|
119548
|
-
this.sessionEpoch = ok.sessionEpoch;
|
|
119549
|
-
this.lastAuthedEpoch = ok.sessionEpoch;
|
|
119550
|
-
if (ok.daemonId && ok.daemonId !== this.deps.daemonId) {
|
|
119551
|
-
this.deps.daemonId = ok.daemonId;
|
|
119552
|
-
this.deps.onDaemonId?.(ok.daemonId);
|
|
119553
|
-
}
|
|
119554
|
-
this.deps.onWebAppUrl?.(ok.webAppUrl);
|
|
119555
|
-
this.deps.onOrgSlug?.(ok.orgSlug);
|
|
119556
|
-
this.state = "REGISTERING";
|
|
119557
|
-
const snap = (await this.request("register", {
|
|
119558
|
-
host: this.deps.host,
|
|
119559
|
-
capabilities: this.deps.capabilities(),
|
|
119560
|
-
maxAgents: this.deps.maxAgents,
|
|
119561
|
-
localState: this.deps.localState()
|
|
119562
|
-
})).payload;
|
|
119563
|
-
this.routingEpoch = snap.routingEpoch;
|
|
119564
|
-
this.deps.configApply.applyReconcileSnapshot(snap);
|
|
120052
|
+
async handshake(daemonId) {
|
|
120053
|
+
this.state = "HELLO";
|
|
120054
|
+
const rep = await this.sendRequest(buildRelayDaemonFrame("rd/hello", {
|
|
120055
|
+
apiKey: this.deps.apiKey(),
|
|
120056
|
+
daemonId
|
|
120057
|
+
}));
|
|
120058
|
+
if (rep.type !== "rd/hello/ok") throw new WireError("INTERNAL", `expected rd/hello/ok, got ${rep.type}`, false);
|
|
120059
|
+
const ok = rep.payload;
|
|
120060
|
+
if (ok.relayId !== this.relayId) throw new WireError("INTERNAL", `relay misroute: dialed ${this.relayId}, reached ${ok.relayId}`, true);
|
|
119565
120061
|
this.state = "READY";
|
|
119566
|
-
this.
|
|
119567
|
-
this.armHeartbeat();
|
|
119568
|
-
this.deps.log.info(`cp: READY (epoch=${this.sessionEpoch}, routingEpoch=${this.routingEpoch})`);
|
|
119569
|
-
this.emitDaemonRuntimes(this.deps.runtimeProfiles(), this.deps.mcpServerFacts?.() ?? []);
|
|
119570
|
-
this.deps.onReady?.();
|
|
119571
|
-
}
|
|
119572
|
-
/**
|
|
119573
|
-
* Push the full runtime snapshot (D→C `facts/daemon-runtimes` EVT,
|
|
119574
|
-
* fire-and-forget) — sent on each register and again when a probe sweep
|
|
119575
|
-
* completes. REPLACE semantics on the CP: it reconciles the daemon's stored
|
|
119576
|
-
* runtime list to exactly this snapshot, pruning runtimes that are no longer
|
|
119577
|
-
* installed. No-op unless READY/DRAINING.
|
|
119578
|
-
*/
|
|
119579
|
-
emitDaemonRuntimes(runtimes, mcpServers = []) {
|
|
119580
|
-
if (this.state !== "READY" && this.state !== "DRAINING") return;
|
|
119581
|
-
this.transport?.send(encode(buildEnvelope("facts/daemon-runtimes", {
|
|
119582
|
-
runtimes,
|
|
119583
|
-
mcpServers
|
|
119584
|
-
})));
|
|
119585
|
-
}
|
|
119586
|
-
/**
|
|
119587
|
-
* Report a session's cumulative token usage (D→C `usage/report` EVT,
|
|
119588
|
-
* fire-and-forget). No-op unless READY/DRAINING — usage is dashboard telemetry,
|
|
119589
|
-
* so a dropped report just means that turn's delta is absent from the CP's
|
|
119590
|
-
* historical aggregates until the next report (which re-sends the cumulative
|
|
119591
|
-
* snapshot, latest-wins). Never blocks the turn.
|
|
119592
|
-
*/
|
|
119593
|
-
emitUsageReport(report) {
|
|
119594
|
-
if (this.state !== "READY" && this.state !== "DRAINING") return;
|
|
119595
|
-
this.transport?.send(encode(buildEnvelope("usage/report", report)));
|
|
119596
|
-
}
|
|
119597
|
-
/**
|
|
119598
|
-
* Report a session's converged milestone + sessionKey echo (D→C `event/session`
|
|
119599
|
-
* EVT, fire-and-forget, latest-wins per `sessionId`). This is what lets the CP
|
|
119600
|
-
* store session metadata so a deep-link detail page (…/sessions/:id) resolves
|
|
119601
|
-
* even when the daemon is offline. Metadata only — never the message stream
|
|
119602
|
-
* (that stays daemon-local, §1/§12). No-op unless READY/DRAINING; a dropped
|
|
119603
|
-
* `start` is re-covered by the next milestone (the CP upsert is latest-wins).
|
|
119604
|
-
*/
|
|
119605
|
-
emitEventSession(event) {
|
|
119606
|
-
if (this.state !== "READY" && this.state !== "DRAINING") return;
|
|
119607
|
-
this.transport?.send(encode(buildEnvelope("event/session", event)));
|
|
119608
|
-
}
|
|
119609
|
-
/**
|
|
119610
|
-
* Report an integration's channel-membership snapshot (D→C `integration/channels`
|
|
119611
|
-
* EVT, fire-and-forget, latest-wins). No-op unless READY/DRAINING — the daemon
|
|
119612
|
-
* re-emits its cached snapshots on each (re)connect (see onReady in daemon.ts),
|
|
119613
|
-
* so a dropped report only delays the console's channel list, never loses it.
|
|
119614
|
-
*/
|
|
119615
|
-
emitIntegrationChannels(snapshot) {
|
|
119616
|
-
if (this.state !== "READY" && this.state !== "DRAINING") return;
|
|
119617
|
-
this.transport?.send(encode(buildEnvelope("integration/channels", snapshot)));
|
|
120062
|
+
this.deps.log.info(`relay(${this.relayId}): rd/* connected`);
|
|
119618
120063
|
}
|
|
119619
|
-
|
|
119620
|
-
* Report a CP-owned cron fire (D→C `cron/report` EVT, fire-and-forget) so the
|
|
119621
|
-
* console's `lastRunAt` converges. The local D11 stamp stays authoritative and
|
|
119622
|
-
* the CP upsert is latest-wins, so re-asserting stored stamps on each
|
|
119623
|
-
* (re)connect (see onReady in daemon.ts) makes a dropped report only a delay,
|
|
119624
|
-
* never a loss. No-op unless READY/DRAINING.
|
|
119625
|
-
*/
|
|
119626
|
-
emitCronReport(report) {
|
|
119627
|
-
if (this.state !== "READY" && this.state !== "DRAINING") return;
|
|
119628
|
-
this.transport?.send(encode(buildEnvelope("cron/report", report)));
|
|
119629
|
-
}
|
|
119630
|
-
/**
|
|
119631
|
-
* Emit one chunk of a webchat turn's reply stream (D→C `webchat/output` EVT).
|
|
119632
|
-
* Ordered per-connection (TCP); `output.index` is the per-turn assembly counter.
|
|
119633
|
-
* No-op unless READY/DRAINING — a dropped chunk is lost (unlike latest-wins
|
|
119634
|
-
* telemetry), so the daemon only emits while connected. Content is inline, bounded
|
|
119635
|
-
* by the 256 KiB frame cap; the caller chunks any oversize single event.
|
|
119636
|
-
*/
|
|
119637
|
-
emitWebchatOutput(output) {
|
|
119638
|
-
if (this.state !== "READY" && this.state !== "DRAINING") return;
|
|
119639
|
-
this.transport?.send(encode(buildEnvelope("webchat/output", output)));
|
|
119640
|
-
}
|
|
119641
|
-
/** Emit a webchat turn's completion (D→C `webchat/done` EVT). No-op unless READY/DRAINING. */
|
|
119642
|
-
emitWebchatDone(done) {
|
|
119643
|
-
if (this.state !== "READY" && this.state !== "DRAINING") return;
|
|
119644
|
-
this.transport?.send(encode(buildEnvelope("webchat/done", done)));
|
|
119645
|
-
}
|
|
119646
|
-
request(type, payload) {
|
|
119647
|
-
const frame = buildEnvelope(type, payload);
|
|
120064
|
+
sendRequest(frame) {
|
|
119648
120065
|
return this.correlator.request(frame, (e) => this.transport.send(e));
|
|
119649
120066
|
}
|
|
119650
|
-
/**
|
|
119651
|
-
* `gitcred/request` (D→C REQ) → grant payload. The FIRST post-handshake
|
|
119652
|
-
* daemon-issued REQ, so unlike the handshake `request()` it is state-GATED:
|
|
119653
|
-
* outside READY/DRAINING it fails immediately (the credential cache degrades
|
|
119654
|
-
* to its unexpired copy or errors) — never queued, never a bare
|
|
119655
|
-
* `transport!.send` on a dead socket. Single send + 10s budget: the default
|
|
119656
|
-
* 5s×5 retransmit would blow every git-side timeout, and CP-side mint
|
|
119657
|
-
* single-flight makes a later fresh request cheap anyway.
|
|
119658
|
-
*/
|
|
119659
|
-
async requestGitCred(payload) {
|
|
119660
|
-
if (this.state !== "READY" && this.state !== "DRAINING" || !this.transport) throw new WireError("INTERNAL", `control plane unreachable (client ${this.state})`, true);
|
|
119661
|
-
const frame = buildEnvelope("gitcred/request", payload);
|
|
119662
|
-
const rep = await this.correlator.request(frame, (e) => this.transport.send(e), {
|
|
119663
|
-
maxTries: 1,
|
|
119664
|
-
ackTimeoutMs: 1e4
|
|
119665
|
-
});
|
|
119666
|
-
if (rep.type !== "gitcred/grant") throw new WireError("INTERNAL", `expected gitcred/grant, got ${rep.type}`, false);
|
|
119667
|
-
return rep.payload;
|
|
119668
|
-
}
|
|
119669
120067
|
async onText(text) {
|
|
119670
|
-
const decoded =
|
|
119671
|
-
if (!decoded.ok)
|
|
119672
|
-
this.sendError(decoded.id, this.decodeErrorCode(decoded.msg), decoded.msg, false);
|
|
119673
|
-
return;
|
|
119674
|
-
}
|
|
120068
|
+
const decoded = decodeRelayDaemonFrame(text);
|
|
120069
|
+
if (!decoded.ok) return;
|
|
119675
120070
|
const frame = decoded.frame;
|
|
119676
120071
|
if (frame.corr && this.correlator.settle(frame)) return;
|
|
119677
|
-
|
|
119678
|
-
this.sendError(frame.id, "PROTOCOL_STATE", `${frame.type} illegal in ${this.state}`, false);
|
|
119679
|
-
return;
|
|
119680
|
-
}
|
|
119681
|
-
if (decoded.ext?.epoch !== void 0 && decoded.ext.epoch < this.sessionEpoch) {
|
|
119682
|
-
this.sendError(frame.id, "STALE_EPOCH", "epoch < current", true);
|
|
119683
|
-
return;
|
|
119684
|
-
}
|
|
119685
|
-
this.dispatchControl(frame);
|
|
119686
|
-
}
|
|
119687
|
-
decodeErrorCode(msg) {
|
|
119688
|
-
if (msg === "FRAME_TOO_LARGE") return "FRAME_TOO_LARGE";
|
|
119689
|
-
if (msg === "UNKNOWN_FRAME") return "UNKNOWN_FRAME";
|
|
119690
|
-
return "BAD_PAYLOAD";
|
|
119691
|
-
}
|
|
119692
|
-
sendError(corr, code, message, retryable) {
|
|
119693
|
-
if (!this.transport) return;
|
|
119694
|
-
this.transport.send(encode(buildEnvelope("error", {
|
|
119695
|
-
code,
|
|
119696
|
-
message,
|
|
119697
|
-
retryable
|
|
119698
|
-
}, { corr })));
|
|
120072
|
+
this.deps.log.debug(`relay(${this.relayId}): ignoring ${frame.type}`);
|
|
119699
120073
|
}
|
|
119700
120074
|
onClose(code, _reason) {
|
|
119701
|
-
this.stopHeartbeat();
|
|
119702
120075
|
this.transport = void 0;
|
|
119703
120076
|
this.correlator.rejectAll(new WireError("INTERNAL", "connection closed", true));
|
|
119704
|
-
if (code ===
|
|
120077
|
+
if (code === CLOSE_AUTH_FAILED) {
|
|
119705
120078
|
this.fatal = true;
|
|
119706
120079
|
this.state = "CLOSED";
|
|
119707
|
-
this.deps.log.
|
|
120080
|
+
this.deps.log.warn(`relay(${this.relayId}): rd/hello rejected (4401) — not redialing`);
|
|
119708
120081
|
return;
|
|
119709
120082
|
}
|
|
119710
120083
|
if (this.stopped) {
|
|
@@ -119714,204 +120087,50 @@ var CpClient = class {
|
|
|
119714
120087
|
this.state = "DEGRADED";
|
|
119715
120088
|
this.scheduleReconnect();
|
|
119716
120089
|
}
|
|
119717
|
-
|
|
119718
|
-
this.
|
|
119719
|
-
|
|
119720
|
-
|
|
119721
|
-
|
|
119722
|
-
|
|
119723
|
-
|
|
119724
|
-
|
|
119725
|
-
if (this.state === "READY" || this.state === "DRAINING") this.armHeartbeat();
|
|
119726
|
-
}, this.heartbeatMs);
|
|
119727
|
-
}
|
|
119728
|
-
stopHeartbeat() {
|
|
119729
|
-
if (this.heartbeatTimer !== void 0) {
|
|
119730
|
-
this.deps.clock.clearTimeout(this.heartbeatTimer);
|
|
119731
|
-
this.heartbeatTimer = void 0;
|
|
119732
|
-
}
|
|
119733
|
-
}
|
|
119734
|
-
/** C→D control dispatch. The CP changes config, never live routing. */
|
|
119735
|
-
dispatchControl(frame) {
|
|
119736
|
-
switch (frame.type) {
|
|
119737
|
-
case "config/push":
|
|
119738
|
-
this.deps.configApply.applyConfigPush(frame.payload.keys);
|
|
119739
|
-
return;
|
|
119740
|
-
case "cron/upsert":
|
|
119741
|
-
try {
|
|
119742
|
-
this.deps.configApply.upsertCron(frame.payload);
|
|
119743
|
-
this.reply(frame, "ack", { ok: true });
|
|
119744
|
-
} catch (err) {
|
|
119745
|
-
this.sendError(frame.id, "BAD_PAYLOAD", `cron upsert failed: ${err.message}`, false);
|
|
119746
|
-
}
|
|
119747
|
-
return;
|
|
119748
|
-
case "cron/remove":
|
|
119749
|
-
this.deps.configApply.removeCron(frame.payload.cronId);
|
|
119750
|
-
this.reply(frame, "ack", { ok: true });
|
|
119751
|
-
return;
|
|
119752
|
-
case "cron/run":
|
|
119753
|
-
this.reply(frame, "ack", this.deps.configApply.runCron(frame.payload.cronId));
|
|
119754
|
-
return;
|
|
119755
|
-
case "route/assign": {
|
|
119756
|
-
const a = frame.payload;
|
|
119757
|
-
this.deps.configApply.applyRouteAssign(a);
|
|
119758
|
-
this.reply(frame, "route/assign/ack", {
|
|
119759
|
-
ok: true,
|
|
119760
|
-
sessionKey: a.sessionKey
|
|
119761
|
-
});
|
|
119762
|
-
return;
|
|
119763
|
-
}
|
|
119764
|
-
case "route/update":
|
|
119765
|
-
this.deps.configApply.applyRouteUpdate(frame.payload);
|
|
119766
|
-
return;
|
|
119767
|
-
case "agent/upsert": {
|
|
119768
|
-
const u = frame.payload;
|
|
119769
|
-
this.deps.configApply.applyAgentUpsert(u.agentId, u.spec);
|
|
119770
|
-
return;
|
|
119771
|
-
}
|
|
119772
|
-
case "agent/remove":
|
|
119773
|
-
this.deps.configApply.applyAgentRemove(frame.payload.agentId);
|
|
119774
|
-
return;
|
|
119775
|
-
case "integration/upsert":
|
|
119776
|
-
this.deps.configApply.applyIntegrationUpsert(frame.payload);
|
|
119777
|
-
return;
|
|
119778
|
-
case "integration/remove":
|
|
119779
|
-
this.deps.configApply.applyIntegrationRemove(frame.payload.integrationId);
|
|
119780
|
-
return;
|
|
119781
|
-
case "agent/launch": {
|
|
119782
|
-
const launch = frame.payload;
|
|
119783
|
-
this.deps.configApply.applyAgentLaunch(launch).then((launched) => this.reply(frame, "agent/launched", launched)).catch((err) => this.sendError(frame.id, "INTERNAL", `agent/launch failed: ${err.message}`, false));
|
|
119784
|
-
return;
|
|
119785
|
-
}
|
|
119786
|
-
case "agent/stop": {
|
|
119787
|
-
const stop = frame.payload;
|
|
119788
|
-
this.deps.configApply.applyAgentStop(stop).then((ack) => this.reply(frame, "ack", ack)).catch((err) => this.sendError(frame.id, "INTERNAL", `agent/stop failed: ${err.message}`, false));
|
|
119789
|
-
return;
|
|
119790
|
-
}
|
|
119791
|
-
case "daemon/drain": {
|
|
119792
|
-
const drain = frame.payload;
|
|
119793
|
-
this.state = "DRAINING";
|
|
119794
|
-
this.deps.configApply.applyDaemonDrain(drain, (p) => this.emit("drain/progress", p)).then((done) => {
|
|
119795
|
-
this.reply(frame, "drain/done", done);
|
|
119796
|
-
if (this.state === "DRAINING") this.state = "READY";
|
|
119797
|
-
}).catch((err) => {
|
|
119798
|
-
this.sendError(frame.id, "INTERNAL", `drain failed: ${err.message}`, false);
|
|
119799
|
-
if (this.state === "DRAINING") this.state = "READY";
|
|
119800
|
-
});
|
|
119801
|
-
return;
|
|
119802
|
-
}
|
|
119803
|
-
case "daemon/restart":
|
|
119804
|
-
this.reply(frame, "daemon/control/ack", this.deps.configApply.applyDaemonRestart(frame.payload));
|
|
119805
|
-
return;
|
|
119806
|
-
case "daemon/upgrade":
|
|
119807
|
-
this.reply(frame, "daemon/control/ack", this.deps.configApply.applyDaemonUpgrade(frame.payload));
|
|
119808
|
-
return;
|
|
119809
|
-
case "session/list":
|
|
119810
|
-
try {
|
|
119811
|
-
this.reply(frame, "session/list/page", this.deps.sessionRead.list(frame.payload));
|
|
119812
|
-
} catch (err) {
|
|
119813
|
-
this.sendError(frame.id, "INTERNAL", `session/list failed: ${err.message}`, false);
|
|
119814
|
-
}
|
|
119815
|
-
return;
|
|
119816
|
-
case "session/history":
|
|
119817
|
-
try {
|
|
119818
|
-
this.reply(frame, "session/history/page", this.deps.sessionRead.history(frame.payload));
|
|
119819
|
-
} catch (err) {
|
|
119820
|
-
this.sendError(frame.id, "INTERNAL", `session/history failed: ${err.message}`, false);
|
|
119821
|
-
}
|
|
119822
|
-
return;
|
|
119823
|
-
case "session/tool-body":
|
|
119824
|
-
try {
|
|
119825
|
-
this.reply(frame, "session/tool-body/chunk", this.deps.sessionRead.toolBody(frame.payload));
|
|
119826
|
-
} catch (err) {
|
|
119827
|
-
this.sendError(frame.id, "INTERNAL", `session/tool-body failed: ${err.message}`, false);
|
|
119828
|
-
}
|
|
119829
|
-
return;
|
|
119830
|
-
case "workspace/list":
|
|
119831
|
-
this.deps.workspaceRead.list(frame.payload).then((page) => this.reply(frame, "workspace/list/page", page)).catch((err) => this.workspaceError(frame.id, "workspace/list", err));
|
|
119832
|
-
return;
|
|
119833
|
-
case "workspace/read":
|
|
119834
|
-
this.deps.workspaceRead.read(frame.payload).then((content) => this.reply(frame, "workspace/read/content", content)).catch((err) => this.workspaceError(frame.id, "workspace/read", err));
|
|
119835
|
-
return;
|
|
119836
|
-
case "workspace/gitstatus":
|
|
119837
|
-
this.deps.workspaceGit.status(frame.payload.agentId).then((status) => this.reply(frame, "workspace/gitstatus/result", status)).catch((err) => this.workspaceError(frame.id, "workspace/gitstatus", err));
|
|
119838
|
-
return;
|
|
119839
|
-
case "workspace/gitpull":
|
|
119840
|
-
this.deps.workspaceGit.pull(frame.payload.agentId).then((result) => this.reply(frame, "workspace/gitpull/result", result)).catch((err) => this.workspaceError(frame.id, "workspace/gitpull", err));
|
|
119841
|
-
return;
|
|
119842
|
-
case "memory/list":
|
|
119843
|
-
this.deps.memoryReader.list(frame.payload).then((page) => this.reply(frame, "memory/list/page", page)).catch((err) => this.memoryError(frame.id, "memory/list", err));
|
|
119844
|
-
return;
|
|
119845
|
-
case "memory/read":
|
|
119846
|
-
this.deps.memoryReader.read(frame.payload).then((content) => this.reply(frame, "memory/read/content", content)).catch((err) => this.memoryError(frame.id, "memory/read", err));
|
|
119847
|
-
return;
|
|
119848
|
-
case "memory/write":
|
|
119849
|
-
this.deps.memoryReader.write(frame.payload).then((ok) => this.reply(frame, "memory/write/ok", ok)).catch((err) => this.memoryError(frame.id, "memory/write", err));
|
|
119850
|
-
return;
|
|
119851
|
-
case "webchat/message":
|
|
119852
|
-
if (!this.deps.onWebchatMessage) {
|
|
119853
|
-
this.sendError(frame.id, "INTERNAL", "webchat not supported", false);
|
|
119854
|
-
return;
|
|
119855
|
-
}
|
|
119856
|
-
try {
|
|
119857
|
-
this.reply(frame, "webchat/ack", this.deps.onWebchatMessage(frame.payload));
|
|
119858
|
-
} catch (err) {
|
|
119859
|
-
this.sendError(frame.id, "INTERNAL", `webchat/message failed: ${err.message}`, false);
|
|
119860
|
-
}
|
|
119861
|
-
return;
|
|
119862
|
-
case "webchat/close":
|
|
119863
|
-
this.deps.onWebchatClose?.(frame.payload);
|
|
119864
|
-
return;
|
|
119865
|
-
case "webchat/set-model":
|
|
119866
|
-
this.deps.onWebchatSetModel?.(frame.payload);
|
|
119867
|
-
return;
|
|
119868
|
-
case "webchat/set-effort":
|
|
119869
|
-
this.deps.onWebchatSetEffort?.(frame.payload);
|
|
119870
|
-
return;
|
|
119871
|
-
case "webchat/set-permission-mode":
|
|
119872
|
-
this.deps.onWebchatSetPermissionMode?.(frame.payload);
|
|
119873
|
-
return;
|
|
119874
|
-
case "webchat/set-fast":
|
|
119875
|
-
this.deps.onWebchatSetFast?.(frame.payload);
|
|
119876
|
-
return;
|
|
119877
|
-
case "webchat/cancel":
|
|
119878
|
-
this.deps.onWebchatCancel?.(frame.payload);
|
|
119879
|
-
return;
|
|
119880
|
-
default:
|
|
119881
|
-
this.deps.log.debug(`cp: ignoring ${frame.type}`);
|
|
119882
|
-
return;
|
|
119883
|
-
}
|
|
120090
|
+
scheduleReconnect() {
|
|
120091
|
+
if (this.stopped || this.fatal) return;
|
|
120092
|
+
if (this.reconnectTimer !== void 0) return;
|
|
120093
|
+
const delay = this.backoff.next();
|
|
120094
|
+
this.reconnectTimer = this.deps.clock.setTimeout(() => {
|
|
120095
|
+
this.reconnectTimer = void 0;
|
|
120096
|
+
this.attemptConnect();
|
|
120097
|
+
}, delay);
|
|
119884
120098
|
}
|
|
119885
|
-
|
|
119886
|
-
|
|
119887
|
-
|
|
119888
|
-
|
|
119889
|
-
|
|
119890
|
-
|
|
119891
|
-
|
|
119892
|
-
|
|
119893
|
-
}
|
|
119894
|
-
this.deps.log.warn(`cp: ${op} failed: ${err?.message}`);
|
|
119895
|
-
this.sendError(corr, "INTERNAL", `${op} failed`, false);
|
|
120099
|
+
};
|
|
120100
|
+
//#endregion
|
|
120101
|
+
//#region src/cp/relay-manager.ts
|
|
120102
|
+
var RelayManager = class {
|
|
120103
|
+
deps;
|
|
120104
|
+
clients = /* @__PURE__ */ new Map();
|
|
120105
|
+
constructor(deps) {
|
|
120106
|
+
this.deps = deps;
|
|
119896
120107
|
}
|
|
119897
|
-
|
|
119898
|
-
|
|
119899
|
-
|
|
119900
|
-
|
|
120108
|
+
/**
|
|
120109
|
+
* Converge to EXACTLY `roster`: stop clients for relays no longer present (or whose
|
|
120110
|
+
* url moved), and start a dial loop for newly-listed relays. Idempotent — re-applying
|
|
120111
|
+
* the same roster is a no-op.
|
|
120112
|
+
*/
|
|
120113
|
+
converge(roster) {
|
|
120114
|
+
const desired = new Map(roster.map((r) => [r.relayId, r.url]));
|
|
120115
|
+
for (const [relayId, client] of this.clients) if (desired.get(relayId) !== client.url) {
|
|
120116
|
+
client.stop();
|
|
120117
|
+
this.clients.delete(relayId);
|
|
119901
120118
|
}
|
|
119902
|
-
|
|
119903
|
-
this.
|
|
119904
|
-
|
|
120119
|
+
for (const { relayId, url } of roster) {
|
|
120120
|
+
if (this.clients.has(relayId)) continue;
|
|
120121
|
+
const client = new RelayClient(relayId, url, this.deps);
|
|
120122
|
+
this.clients.set(relayId, client);
|
|
120123
|
+
client.start();
|
|
119905
120124
|
}
|
|
119906
|
-
this.deps.log.warn(`cp: ${op} failed: ${err?.message}`);
|
|
119907
|
-
this.sendError(corr, "INTERNAL", `${op} failed`, false);
|
|
119908
120125
|
}
|
|
119909
|
-
|
|
119910
|
-
|
|
120126
|
+
/** Tear down every relay connection (daemon shutdown). */
|
|
120127
|
+
async stop() {
|
|
120128
|
+
await Promise.allSettled([...this.clients.values()].map((c) => c.stop()));
|
|
120129
|
+
this.clients.clear();
|
|
119911
120130
|
}
|
|
119912
|
-
/**
|
|
119913
|
-
|
|
119914
|
-
this.
|
|
120131
|
+
/** Number of relays currently being dialed (observability / tests). */
|
|
120132
|
+
size() {
|
|
120133
|
+
return this.clients.size;
|
|
119915
120134
|
}
|
|
119916
120135
|
};
|
|
119917
120136
|
//#endregion
|
|
@@ -120776,20 +120995,6 @@ function mergeConfigPush(cfg, keys) {
|
|
|
120776
120995
|
};
|
|
120777
120996
|
}
|
|
120778
120997
|
//#endregion
|
|
120779
|
-
//#region src/cp/clock.ts
|
|
120780
|
-
var SystemClock = class {
|
|
120781
|
-
now() {
|
|
120782
|
-
return Date.now();
|
|
120783
|
-
}
|
|
120784
|
-
setTimeout(fn, ms) {
|
|
120785
|
-
return globalThis.setTimeout(fn, ms);
|
|
120786
|
-
}
|
|
120787
|
-
clearTimeout(h) {
|
|
120788
|
-
globalThis.clearTimeout(h);
|
|
120789
|
-
}
|
|
120790
|
-
};
|
|
120791
|
-
const systemClock = new SystemClock();
|
|
120792
|
-
//#endregion
|
|
120793
120998
|
//#region ../../node_modules/.pnpm/systeminformation@5.31.12/node_modules/systeminformation/package.json
|
|
120794
120999
|
var require_package = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
120795
121000
|
module.exports = {
|
|
@@ -136507,6 +136712,7 @@ var Daemon = class Daemon {
|
|
|
136507
136712
|
probing = false;
|
|
136508
136713
|
lastProbeAtMs = 0;
|
|
136509
136714
|
cpClient;
|
|
136715
|
+
relays;
|
|
136510
136716
|
cpCrons;
|
|
136511
136717
|
channelSnapshots = /* @__PURE__ */ new Map();
|
|
136512
136718
|
cpAgents;
|
|
@@ -138608,6 +138814,7 @@ var Daemon = class Daemon {
|
|
|
138608
138814
|
applyReconcileSnapshot: (snap) => {
|
|
138609
138815
|
this.cpAgents?.converge(snap.agents);
|
|
138610
138816
|
this.cpIntegrations?.converge(snap.integrations);
|
|
138817
|
+
this.relays?.converge(snap.relays);
|
|
138611
138818
|
for (const id of snap.drop.crons) this.cpCrons?.remove(id);
|
|
138612
138819
|
this.cpCrons?.converge(snap.crons);
|
|
138613
138820
|
this.cpRouting?.converge({
|
|
@@ -138635,6 +138842,7 @@ var Daemon = class Daemon {
|
|
|
138635
138842
|
runCron: (cronId) => this.runCronNow(cronId),
|
|
138636
138843
|
applyRouteAssign: (a) => this.cpRouting?.upsertAssign(a),
|
|
138637
138844
|
applyRouteUpdate: (u) => this.cpRouting?.applyUpdate(u),
|
|
138845
|
+
applyRelayRoster: (relays) => this.relays?.converge(relays),
|
|
138638
138846
|
applyAgentLaunch: async (launch) => {
|
|
138639
138847
|
const agent = this.agents.get(launch.agentId);
|
|
138640
138848
|
if (!agent) throw new Error(`agent/launch: unknown agent ${launch.agentId}`);
|
|
@@ -138737,6 +138945,17 @@ var Daemon = class Daemon {
|
|
|
138737
138945
|
this.metrics.start();
|
|
138738
138946
|
const echoDaemonId = this.opts.overrides?.daemonId;
|
|
138739
138947
|
const url = cp.url;
|
|
138948
|
+
const cpKey = cp.key;
|
|
138949
|
+
this.relays = new RelayManager({
|
|
138950
|
+
apiKey: () => cpKey,
|
|
138951
|
+
daemonId: () => this.cfg.daemonId,
|
|
138952
|
+
clock: systemClock,
|
|
138953
|
+
connect: (relayUrl) => ClientTransport.dial(relayUrl, {
|
|
138954
|
+
subprotocol: RELAY_DAEMON_SUBPROTOCOL,
|
|
138955
|
+
path: RELAY_DAEMON_WS_PATH
|
|
138956
|
+
}),
|
|
138957
|
+
log: this.log
|
|
138958
|
+
});
|
|
138740
138959
|
this.cpClient = new CpClient({
|
|
138741
138960
|
url,
|
|
138742
138961
|
token: cp.key,
|
|
@@ -138809,7 +139028,10 @@ var Daemon = class Daemon {
|
|
|
138809
139028
|
onWebchatSetFast: (m) => this.handleWebchatSetFast(m),
|
|
138810
139029
|
onWebchatCancel: (m) => this.handleWebchatCancel(m),
|
|
138811
139030
|
clock: systemClock,
|
|
138812
|
-
connect: () => ClientTransport.dial(url
|
|
139031
|
+
connect: () => ClientTransport.dial(url, {
|
|
139032
|
+
subprotocol: CP_SUBPROTOCOL,
|
|
139033
|
+
path: CP_WS_PATH
|
|
139034
|
+
}),
|
|
138813
139035
|
log: this.log
|
|
138814
139036
|
});
|
|
138815
139037
|
this.cpClient.start();
|
|
@@ -138897,6 +139119,7 @@ var Daemon = class Daemon {
|
|
|
138897
139119
|
const errors = [];
|
|
138898
139120
|
this.scheduler?.stop();
|
|
138899
139121
|
await Promise.resolve(this.cpClient?.stop()).catch((e) => errors.push(e));
|
|
139122
|
+
await Promise.resolve(this.relays?.stop()).catch((e) => errors.push(e));
|
|
138900
139123
|
for (const c of this.connections) await Promise.resolve(c.stop()).catch((e) => errors.push(e));
|
|
138901
139124
|
for (const c of this.telegramConns) await Promise.resolve(c.stop()).catch((e) => errors.push(e));
|
|
138902
139125
|
for (const h of this.hosts.values()) await Promise.resolve(h.stop()).catch((e) => errors.push(e));
|
|
@@ -138932,6 +139155,6 @@ async function runForeground(opts, deps = {}) {
|
|
|
138932
139155
|
});
|
|
138933
139156
|
}
|
|
138934
139157
|
//#endregion
|
|
138935
|
-
export { ClientTransport as n, runForeground as t };
|
|
139158
|
+
export { ClientTransport as i, CP_SUBPROTOCOL as n, CP_WS_PATH as r, runForeground as t };
|
|
138936
139159
|
|
|
138937
|
-
//# sourceMappingURL=run-foreground-
|
|
139160
|
+
//# sourceMappingURL=run-foreground-B5RyE_L1.js.map
|