@agentconnect.md/daemon 1.0.0-rc.16 → 1.0.0-rc.18
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/index.js +97 -529
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { createInterface } from "node:readline";
|
|
|
12
12
|
import { freemem, homedir, hostname, loadavg, totalmem, type } from "node:os";
|
|
13
13
|
import { spawn as spawn$1 } from "child_process";
|
|
14
14
|
import { Readable, Writable } from "node:stream";
|
|
15
|
+
import { MAX_FRAME_BYTES, buildEnvelope, decodeEnvelope, encode, isFrame } from "@agentconnect.md/protocol";
|
|
15
16
|
import { randomUUID } from "node:crypto";
|
|
16
17
|
import { lstat, open, readdir, realpath, stat as stat$1 } from "node:fs/promises";
|
|
17
18
|
import { DatabaseSync } from "node:sqlite";
|
|
@@ -16871,535 +16872,6 @@ async function runChat(opts) {
|
|
|
16871
16872
|
//#region src/version.ts
|
|
16872
16873
|
const DAEMON_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
16873
16874
|
//#endregion
|
|
16874
|
-
//#region ../protocol/src/envelope.ts
|
|
16875
|
-
/**
|
|
16876
|
-
* The protocol envelope — every frame, both directions, is wrapped in this.
|
|
16877
|
-
* Mirrors daemon-cp-ws-protocol.md §1.1.
|
|
16878
|
-
*
|
|
16879
|
-
* `payload` is left as `unknown` here and validated by the per-type schema in
|
|
16880
|
-
* `frames/*` via `FRAME_SCHEMAS[type]` (see `frame.ts`). This two-step parse is
|
|
16881
|
-
* what lets the codec answer an unknown `type` with `UNKNOWN_FRAME` (a REP)
|
|
16882
|
-
* instead of a hard close — forward-compat, protocol §1.
|
|
16883
|
-
*/
|
|
16884
|
-
const Envelope = object({
|
|
16885
|
-
v: literal(1),
|
|
16886
|
-
id: string().uuid(),
|
|
16887
|
-
ts: string().datetime(),
|
|
16888
|
-
type: string(),
|
|
16889
|
-
corr: string().uuid().optional(),
|
|
16890
|
-
payload: unknown()
|
|
16891
|
-
});
|
|
16892
|
-
object({
|
|
16893
|
-
epoch: number().int(),
|
|
16894
|
-
seq: number().int().optional(),
|
|
16895
|
-
agentId: string().uuid().optional(),
|
|
16896
|
-
launchId: string().uuid().optional()
|
|
16897
|
-
});
|
|
16898
|
-
/** The all-zero UUID used when a frame is malformed past the point of reading `id`. */
|
|
16899
|
-
const NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
16900
|
-
//#endregion
|
|
16901
|
-
//#region ../protocol/src/frames/auth.ts
|
|
16902
|
-
/**
|
|
16903
|
-
* Auth & identity — protocol §3.1 / §3.2.
|
|
16904
|
-
*
|
|
16905
|
-
* `auth` is the first frame after the socket opens. `auth/ok` carries the
|
|
16906
|
-
* minted `sessionEpoch` (the global fencing token), heartbeat cadence, and the
|
|
16907
|
-
* resume verdict.
|
|
16908
|
-
*/
|
|
16909
|
-
const AuthReq = object({
|
|
16910
|
-
daemonToken: string(),
|
|
16911
|
-
daemonId: string().uuid().optional(),
|
|
16912
|
-
machineId: string().uuid().optional(),
|
|
16913
|
-
attestation: string().optional(),
|
|
16914
|
-
agentVersion: string(),
|
|
16915
|
-
resume: object({
|
|
16916
|
-
lastEpoch: number().int(),
|
|
16917
|
-
lastRecvSeq: record(string(), number())
|
|
16918
|
-
}).optional()
|
|
16919
|
-
});
|
|
16920
|
-
const AuthOk = object({
|
|
16921
|
-
daemonId: string().uuid(),
|
|
16922
|
-
sessionEpoch: number().int(),
|
|
16923
|
-
heartbeatSec: number().int(),
|
|
16924
|
-
serverTime: string().datetime(),
|
|
16925
|
-
resume: object({
|
|
16926
|
-
accepted: boolean(),
|
|
16927
|
-
redeliverFromSeq: record(string(), number()).optional()
|
|
16928
|
-
}).optional()
|
|
16929
|
-
});
|
|
16930
|
-
//#endregion
|
|
16931
|
-
//#region ../protocol/src/frames/route.ts
|
|
16932
|
-
/**
|
|
16933
|
-
* Routing & orchestration (C→D control) — protocol §5.
|
|
16934
|
-
*
|
|
16935
|
-
* `SessionKey` is the canonical session primitive shared across route/*,
|
|
16936
|
-
* agent/*, and event/session. Its canonical string form is
|
|
16937
|
-
* `${platform}:${channel}:${thread ?? "-"}`.
|
|
16938
|
-
*/
|
|
16939
|
-
const Platform = _enum(["slack", "telegram"]);
|
|
16940
|
-
const SessionKey = object({
|
|
16941
|
-
platform: Platform,
|
|
16942
|
-
channel: string(),
|
|
16943
|
-
thread: string().optional()
|
|
16944
|
-
});
|
|
16945
|
-
/** Trigger-matching rule for a binding (protocol §5.1). */
|
|
16946
|
-
const BindRule = object({ match: discriminatedUnion("kind", [
|
|
16947
|
-
object({ kind: literal("mention") }),
|
|
16948
|
-
object({ kind: literal("dm") }),
|
|
16949
|
-
object({
|
|
16950
|
-
kind: literal("keyword"),
|
|
16951
|
-
value: string()
|
|
16952
|
-
}),
|
|
16953
|
-
object({ kind: literal("auto") })
|
|
16954
|
-
]) });
|
|
16955
|
-
const RouteAssign = object({
|
|
16956
|
-
sessionKey: SessionKey,
|
|
16957
|
-
agentId: string().uuid(),
|
|
16958
|
-
workspaceId: string().uuid(),
|
|
16959
|
-
bindRules: array(BindRule).default([])
|
|
16960
|
-
});
|
|
16961
|
-
const RouteAssignAck = object({
|
|
16962
|
-
ok: boolean(),
|
|
16963
|
-
sessionKey: SessionKey,
|
|
16964
|
-
reason: string().optional()
|
|
16965
|
-
});
|
|
16966
|
-
const RouteUpdate = object({
|
|
16967
|
-
routingEpoch: number().int(),
|
|
16968
|
-
rules: array(object({
|
|
16969
|
-
match: unknown(),
|
|
16970
|
-
agentId: string().uuid()
|
|
16971
|
-
}))
|
|
16972
|
-
});
|
|
16973
|
-
/** Graceful scale-down / rebalance — protocol §5.3. */
|
|
16974
|
-
const Drain = object({
|
|
16975
|
-
scope: union([
|
|
16976
|
-
object({
|
|
16977
|
-
kind: literal("agent"),
|
|
16978
|
-
agentId: string().uuid()
|
|
16979
|
-
}),
|
|
16980
|
-
object({ kind: literal("daemon") }),
|
|
16981
|
-
object({
|
|
16982
|
-
kind: literal("session"),
|
|
16983
|
-
sessionKey: SessionKey
|
|
16984
|
-
})
|
|
16985
|
-
]),
|
|
16986
|
-
deadline: string().datetime()
|
|
16987
|
-
});
|
|
16988
|
-
const DrainProgress = object({
|
|
16989
|
-
remaining: number().int(),
|
|
16990
|
-
drained: array(SessionKey)
|
|
16991
|
-
});
|
|
16992
|
-
const DrainDone = object({ released: array(SessionKey) });
|
|
16993
|
-
//#endregion
|
|
16994
|
-
//#region ../protocol/src/frames/cron.ts
|
|
16995
|
-
/**
|
|
16996
|
-
* Cron sinks to the daemon (D5) — protocol §5.4.
|
|
16997
|
-
*
|
|
16998
|
-
* The CP owns the definition; the daemon owns firing + last-run persistence so
|
|
16999
|
-
* crons fire even when the CP is down.
|
|
17000
|
-
*/
|
|
17001
|
-
const CronUpsert = object({
|
|
17002
|
-
cronId: string().uuid(),
|
|
17003
|
-
schedule: string(),
|
|
17004
|
-
target: object({ channel: string() }),
|
|
17005
|
-
trigger: string(),
|
|
17006
|
-
enabled: boolean().default(true)
|
|
17007
|
-
});
|
|
17008
|
-
const CronRemove = object({ cronId: string().uuid() });
|
|
17009
|
-
//#endregion
|
|
17010
|
-
//#region ../protocol/src/frames/secrets.ts
|
|
17011
|
-
/**
|
|
17012
|
-
* Secrets (C5 ↔ D10) — protocol §6.
|
|
17013
|
-
*
|
|
17014
|
-
* Lease-based, no plaintext on the wire or in PG. Every frame carries a
|
|
17015
|
-
* REFERENCE to a Vault/KMS path, never the secret material itself.
|
|
17016
|
-
*/
|
|
17017
|
-
const SecretsRequest = object({ scope: object({
|
|
17018
|
-
platform: Platform,
|
|
17019
|
-
workspaceId: string().uuid()
|
|
17020
|
-
}) });
|
|
17021
|
-
const SecretsGrant = object({
|
|
17022
|
-
leaseId: string().uuid(),
|
|
17023
|
-
scope: object({
|
|
17024
|
-
platform: string(),
|
|
17025
|
-
workspaceId: string().uuid()
|
|
17026
|
-
}),
|
|
17027
|
-
ref: string(),
|
|
17028
|
-
ttl: number().int(),
|
|
17029
|
-
renewBeforeSec: number().int()
|
|
17030
|
-
});
|
|
17031
|
-
const SecretsRenew = object({ leaseId: string().uuid() });
|
|
17032
|
-
const SecretsRevoke = object({
|
|
17033
|
-
leaseId: string().uuid(),
|
|
17034
|
-
reason: string()
|
|
17035
|
-
});
|
|
17036
|
-
/** 🅼 Direct-to-store upload/download grant — protocol §3.2 / frame #25. */
|
|
17037
|
-
const ScopeAttestation = object({
|
|
17038
|
-
machineId: string().uuid(),
|
|
17039
|
-
scope: _enum([
|
|
17040
|
-
"attachment.put",
|
|
17041
|
-
"attachment.get",
|
|
17042
|
-
"facts.put"
|
|
17043
|
-
]),
|
|
17044
|
-
resourceRef: string(),
|
|
17045
|
-
jws: string(),
|
|
17046
|
-
exp: string().datetime()
|
|
17047
|
-
});
|
|
17048
|
-
//#endregion
|
|
17049
|
-
//#region ../protocol/src/frames/register.ts
|
|
17050
|
-
/**
|
|
17051
|
-
* Capability upload + the reconcile snapshot — protocol §3.3.
|
|
17052
|
-
*
|
|
17053
|
-
* `register/ok` is the authoritative source of truth: the daemon converges its
|
|
17054
|
-
* local cache to it. CP wins all conflicts, so re-issuing the same snapshot is
|
|
17055
|
-
* idempotent.
|
|
17056
|
-
*/
|
|
17057
|
-
const RegisterReq = object({
|
|
17058
|
-
host: string(),
|
|
17059
|
-
capabilities: object({
|
|
17060
|
-
platforms: array(Platform),
|
|
17061
|
-
runtimes: array(string()),
|
|
17062
|
-
acp: boolean(),
|
|
17063
|
-
features: array(string()).default([])
|
|
17064
|
-
}),
|
|
17065
|
-
maxAgents: number().int(),
|
|
17066
|
-
localState: object({
|
|
17067
|
-
assignments: array(string()),
|
|
17068
|
-
crons: array(string()),
|
|
17069
|
-
leases: array(string())
|
|
17070
|
-
})
|
|
17071
|
-
});
|
|
17072
|
-
const RegisterOk = object({
|
|
17073
|
-
routingEpoch: number().int(),
|
|
17074
|
-
assignments: array(RouteAssign),
|
|
17075
|
-
crons: array(CronUpsert),
|
|
17076
|
-
leases: array(SecretsGrant),
|
|
17077
|
-
drop: object({
|
|
17078
|
-
assignments: array(string()),
|
|
17079
|
-
crons: array(string())
|
|
17080
|
-
})
|
|
17081
|
-
});
|
|
17082
|
-
//#endregion
|
|
17083
|
-
//#region ../protocol/src/frames/agent.ts
|
|
17084
|
-
/**
|
|
17085
|
-
* Agent lifecycle + delivery (protocol §4.3, §4.4, §7.4, §8).
|
|
17086
|
-
*
|
|
17087
|
-
* Body-locality invariant (protocol §12): no frame here carries
|
|
17088
|
-
* `NormalizedMessage.text`. `agent/prompt` ships a `NormalizedMessageRef` — a
|
|
17089
|
-
* reference the daemon resolves against its local body store.
|
|
17090
|
-
*/
|
|
17091
|
-
/**
|
|
17092
|
-
* A REFERENCE/digest of a normalized message, NOT the body (protocol §4.3).
|
|
17093
|
-
* Enough for D4/D6 to fetch the local body and prompt.
|
|
17094
|
-
*/
|
|
17095
|
-
const NormalizedMessageRef = object({
|
|
17096
|
-
sessionKey: SessionKey,
|
|
17097
|
-
platformMsgId: string(),
|
|
17098
|
-
seenUpToSeq: number().int()
|
|
17099
|
-
});
|
|
17100
|
-
const AgentLaunch = object({
|
|
17101
|
-
agentId: string().uuid(),
|
|
17102
|
-
runtime: string(),
|
|
17103
|
-
workspaceId: string().uuid(),
|
|
17104
|
-
capabilities: array(string()),
|
|
17105
|
-
mode: _enum(["long_lived", "per_turn"]).default("long_lived")
|
|
17106
|
-
});
|
|
17107
|
-
const AgentLaunched = object({
|
|
17108
|
-
agentId: string().uuid(),
|
|
17109
|
-
launchId: string().uuid(),
|
|
17110
|
-
acpSessionId: string().optional(),
|
|
17111
|
-
startedAt: string().datetime(),
|
|
17112
|
-
runtime: string()
|
|
17113
|
-
});
|
|
17114
|
-
const AgentStop = object({
|
|
17115
|
-
agentId: string().uuid(),
|
|
17116
|
-
launchId: string().uuid(),
|
|
17117
|
-
reason: string()
|
|
17118
|
-
});
|
|
17119
|
-
const AgentPrompt = object({
|
|
17120
|
-
sessionKey: SessionKey,
|
|
17121
|
-
agentId: string().uuid(),
|
|
17122
|
-
content: NormalizedMessageRef,
|
|
17123
|
-
seenUpToSeq: number().int()
|
|
17124
|
-
});
|
|
17125
|
-
const AgentPromptAck = object({
|
|
17126
|
-
accepted: boolean(),
|
|
17127
|
-
reason: _enum([
|
|
17128
|
-
"queued",
|
|
17129
|
-
"held",
|
|
17130
|
-
"scope_denied",
|
|
17131
|
-
"no_session",
|
|
17132
|
-
"stale"
|
|
17133
|
-
]).optional(),
|
|
17134
|
-
seq: number().int()
|
|
17135
|
-
});
|
|
17136
|
-
const AgentActivity = object({
|
|
17137
|
-
agentId: string().uuid(),
|
|
17138
|
-
launchId: string().uuid(),
|
|
17139
|
-
state: _enum([
|
|
17140
|
-
"thinking",
|
|
17141
|
-
"tool_call",
|
|
17142
|
-
"awaiting_permission",
|
|
17143
|
-
"idle"
|
|
17144
|
-
]),
|
|
17145
|
-
ts: string().datetime()
|
|
17146
|
-
});
|
|
17147
|
-
const AgentScopeDenied = object({
|
|
17148
|
-
agentId: string().uuid(),
|
|
17149
|
-
launchId: string().uuid(),
|
|
17150
|
-
capability: string()
|
|
17151
|
-
});
|
|
17152
|
-
//#endregion
|
|
17153
|
-
//#region ../protocol/src/frame.ts
|
|
17154
|
-
/**
|
|
17155
|
-
* The single source of truth for the wire: `type` string → payload zod schema.
|
|
17156
|
-
*
|
|
17157
|
-
* Mirrors the frame index in daemon-cp-ws-protocol.md §10 (the 29 numbered
|
|
17158
|
-
* frames) plus the correlated REP types named in the "Reply" column
|
|
17159
|
-
* (`route/assign/ack`, `drain/done`, and the generic `ack` replies) that also
|
|
17160
|
-
* travel on the wire and must be decodable.
|
|
17161
|
-
*
|
|
17162
|
-
* `ws/codec.ts` validates every inbound `payload` against `FRAME_SCHEMAS[type]`;
|
|
17163
|
-
* an unknown `type` → `ErrorFrame{code:"UNKNOWN_FRAME"}` (a REP, not a close).
|
|
17164
|
-
*/
|
|
17165
|
-
const FRAME_SCHEMAS = {
|
|
17166
|
-
auth: AuthReq,
|
|
17167
|
-
"auth/ok": AuthOk,
|
|
17168
|
-
register: RegisterReq,
|
|
17169
|
-
"register/ok": RegisterOk,
|
|
17170
|
-
heartbeat: object({
|
|
17171
|
-
load: object({
|
|
17172
|
-
cpu: number(),
|
|
17173
|
-
mem: number(),
|
|
17174
|
-
agents: number().int()
|
|
17175
|
-
}),
|
|
17176
|
-
health: _enum(["ok", "degraded"]),
|
|
17177
|
-
activeSessions: number().int(),
|
|
17178
|
-
degradedScopes: array(string()).default([])
|
|
17179
|
-
}),
|
|
17180
|
-
"agent/launch": AgentLaunch,
|
|
17181
|
-
"agent/launched": AgentLaunched,
|
|
17182
|
-
"agent/stop": AgentStop,
|
|
17183
|
-
"agent/prompt": AgentPrompt,
|
|
17184
|
-
"agent/prompt/ack": AgentPromptAck,
|
|
17185
|
-
"agent/activity": AgentActivity,
|
|
17186
|
-
"agent/scope-denied": AgentScopeDenied,
|
|
17187
|
-
"route/assign": RouteAssign,
|
|
17188
|
-
"route/assign/ack": RouteAssignAck,
|
|
17189
|
-
"route/update": RouteUpdate,
|
|
17190
|
-
"daemon/drain": Drain,
|
|
17191
|
-
"drain/progress": DrainProgress,
|
|
17192
|
-
"drain/done": DrainDone,
|
|
17193
|
-
"cron/upsert": CronUpsert,
|
|
17194
|
-
"cron/remove": CronRemove,
|
|
17195
|
-
"secrets/request": SecretsRequest,
|
|
17196
|
-
"secrets/grant": SecretsGrant,
|
|
17197
|
-
"secrets/renew": SecretsRenew,
|
|
17198
|
-
"secrets/revoke": SecretsRevoke,
|
|
17199
|
-
"scope-attestation": ScopeAttestation,
|
|
17200
|
-
"event/session": object({
|
|
17201
|
-
sessionId: string().uuid(),
|
|
17202
|
-
agentId: string().uuid(),
|
|
17203
|
-
launchId: string().uuid(),
|
|
17204
|
-
phase: _enum([
|
|
17205
|
-
"start",
|
|
17206
|
-
"plan",
|
|
17207
|
-
"problem",
|
|
17208
|
-
"end"
|
|
17209
|
-
]),
|
|
17210
|
-
link: string().optional(),
|
|
17211
|
-
summary: string().optional(),
|
|
17212
|
-
ts: string().datetime()
|
|
17213
|
-
}),
|
|
17214
|
-
"facts/runtime-profile": object({
|
|
17215
|
-
runtime: string(),
|
|
17216
|
-
version: string(),
|
|
17217
|
-
models: array(string()),
|
|
17218
|
-
contextWindow: number().int().optional(),
|
|
17219
|
-
acpSupport: _enum([
|
|
17220
|
-
"full",
|
|
17221
|
-
"partial",
|
|
17222
|
-
"none"
|
|
17223
|
-
]),
|
|
17224
|
-
toolCalling: boolean()
|
|
17225
|
-
}),
|
|
17226
|
-
"config/push": object({ keys: record(string(), unknown()) }),
|
|
17227
|
-
"daemon/restart": object({
|
|
17228
|
-
reason: string(),
|
|
17229
|
-
drainFirst: boolean().default(true)
|
|
17230
|
-
}),
|
|
17231
|
-
"daemon/upgrade": object({
|
|
17232
|
-
targetVersion: string(),
|
|
17233
|
-
drainFirst: boolean().default(true)
|
|
17234
|
-
}),
|
|
17235
|
-
"daemon/control/ack": object({
|
|
17236
|
-
accepted: boolean(),
|
|
17237
|
-
willDrainUntil: string().datetime().optional()
|
|
17238
|
-
}),
|
|
17239
|
-
ack: object({
|
|
17240
|
-
ok: boolean(),
|
|
17241
|
-
reason: string().optional()
|
|
17242
|
-
}),
|
|
17243
|
-
error: object({
|
|
17244
|
-
code: _enum([
|
|
17245
|
-
"UNKNOWN_FRAME",
|
|
17246
|
-
"FRAME_TOO_LARGE",
|
|
17247
|
-
"PROTOCOL_STATE",
|
|
17248
|
-
"BAD_PAYLOAD",
|
|
17249
|
-
"AUTH_FAILED",
|
|
17250
|
-
"ATTESTATION_INVALID",
|
|
17251
|
-
"STALE_EPOCH",
|
|
17252
|
-
"STALE_LAUNCH",
|
|
17253
|
-
"SEQ_GAP",
|
|
17254
|
-
"NO_SESSION",
|
|
17255
|
-
"HELD",
|
|
17256
|
-
"SCOPE_DENIED",
|
|
17257
|
-
"LEASE_EXPIRED",
|
|
17258
|
-
"LEASE_DENIED",
|
|
17259
|
-
"RATE_LIMITED",
|
|
17260
|
-
"INTERNAL"
|
|
17261
|
-
]),
|
|
17262
|
-
message: string(),
|
|
17263
|
-
retryable: boolean(),
|
|
17264
|
-
details: record(string(), unknown()).optional()
|
|
17265
|
-
})
|
|
17266
|
-
};
|
|
17267
|
-
Object.keys(FRAME_SCHEMAS);
|
|
17268
|
-
/**
|
|
17269
|
-
* Builds the envelope schema for one frame `type` with a `type` literal and the
|
|
17270
|
-
* typed payload, so the discriminated union infers `payload` precisely.
|
|
17271
|
-
*/
|
|
17272
|
-
function frame(type, payload) {
|
|
17273
|
-
return object({
|
|
17274
|
-
v: literal(1),
|
|
17275
|
-
id: string().uuid(),
|
|
17276
|
-
ts: string().datetime(),
|
|
17277
|
-
type: literal(type),
|
|
17278
|
-
corr: string().uuid().optional(),
|
|
17279
|
-
payload
|
|
17280
|
-
});
|
|
17281
|
-
}
|
|
17282
|
-
discriminatedUnion("type", [
|
|
17283
|
-
frame("auth", FRAME_SCHEMAS["auth"]),
|
|
17284
|
-
frame("auth/ok", FRAME_SCHEMAS["auth/ok"]),
|
|
17285
|
-
frame("register", FRAME_SCHEMAS["register"]),
|
|
17286
|
-
frame("register/ok", FRAME_SCHEMAS["register/ok"]),
|
|
17287
|
-
frame("heartbeat", FRAME_SCHEMAS["heartbeat"]),
|
|
17288
|
-
frame("agent/launch", FRAME_SCHEMAS["agent/launch"]),
|
|
17289
|
-
frame("agent/launched", FRAME_SCHEMAS["agent/launched"]),
|
|
17290
|
-
frame("agent/stop", FRAME_SCHEMAS["agent/stop"]),
|
|
17291
|
-
frame("agent/prompt", FRAME_SCHEMAS["agent/prompt"]),
|
|
17292
|
-
frame("agent/prompt/ack", FRAME_SCHEMAS["agent/prompt/ack"]),
|
|
17293
|
-
frame("agent/activity", FRAME_SCHEMAS["agent/activity"]),
|
|
17294
|
-
frame("agent/scope-denied", FRAME_SCHEMAS["agent/scope-denied"]),
|
|
17295
|
-
frame("route/assign", FRAME_SCHEMAS["route/assign"]),
|
|
17296
|
-
frame("route/assign/ack", FRAME_SCHEMAS["route/assign/ack"]),
|
|
17297
|
-
frame("route/update", FRAME_SCHEMAS["route/update"]),
|
|
17298
|
-
frame("daemon/drain", FRAME_SCHEMAS["daemon/drain"]),
|
|
17299
|
-
frame("drain/progress", FRAME_SCHEMAS["drain/progress"]),
|
|
17300
|
-
frame("drain/done", FRAME_SCHEMAS["drain/done"]),
|
|
17301
|
-
frame("cron/upsert", FRAME_SCHEMAS["cron/upsert"]),
|
|
17302
|
-
frame("cron/remove", FRAME_SCHEMAS["cron/remove"]),
|
|
17303
|
-
frame("secrets/request", FRAME_SCHEMAS["secrets/request"]),
|
|
17304
|
-
frame("secrets/grant", FRAME_SCHEMAS["secrets/grant"]),
|
|
17305
|
-
frame("secrets/renew", FRAME_SCHEMAS["secrets/renew"]),
|
|
17306
|
-
frame("secrets/revoke", FRAME_SCHEMAS["secrets/revoke"]),
|
|
17307
|
-
frame("scope-attestation", FRAME_SCHEMAS["scope-attestation"]),
|
|
17308
|
-
frame("event/session", FRAME_SCHEMAS["event/session"]),
|
|
17309
|
-
frame("facts/runtime-profile", FRAME_SCHEMAS["facts/runtime-profile"]),
|
|
17310
|
-
frame("config/push", FRAME_SCHEMAS["config/push"]),
|
|
17311
|
-
frame("daemon/restart", FRAME_SCHEMAS["daemon/restart"]),
|
|
17312
|
-
frame("daemon/upgrade", FRAME_SCHEMAS["daemon/upgrade"]),
|
|
17313
|
-
frame("daemon/control/ack", FRAME_SCHEMAS["daemon/control/ack"]),
|
|
17314
|
-
frame("ack", FRAME_SCHEMAS["ack"]),
|
|
17315
|
-
frame("error", FRAME_SCHEMAS["error"])
|
|
17316
|
-
]);
|
|
17317
|
-
//#endregion
|
|
17318
|
-
//#region ../protocol/src/codec.ts
|
|
17319
|
-
/** Soft cap per frame — 256 KiB (protocol §1). Over this → FRAME_TOO_LARGE. */
|
|
17320
|
-
const MAX_FRAME_BYTES = 256 * 1024;
|
|
17321
|
-
const textEncoder = new TextEncoder();
|
|
17322
|
-
function byteLength(text) {
|
|
17323
|
-
return textEncoder.encode(text).length;
|
|
17324
|
-
}
|
|
17325
|
-
function extractControlExt(json) {
|
|
17326
|
-
if (typeof json !== "object" || json === null) return void 0;
|
|
17327
|
-
const o = json;
|
|
17328
|
-
const ext = {};
|
|
17329
|
-
if (typeof o.epoch === "number") ext.epoch = o.epoch;
|
|
17330
|
-
if (typeof o.seq === "number") ext.seq = o.seq;
|
|
17331
|
-
if (typeof o.agentId === "string") ext.agentId = o.agentId;
|
|
17332
|
-
if (typeof o.launchId === "string") ext.launchId = o.launchId;
|
|
17333
|
-
return Object.keys(ext).length > 0 ? ext : void 0;
|
|
17334
|
-
}
|
|
17335
|
-
function decodeEnvelope(text) {
|
|
17336
|
-
if (byteLength(text) > 262144) return {
|
|
17337
|
-
ok: false,
|
|
17338
|
-
id: NIL_UUID,
|
|
17339
|
-
msg: "FRAME_TOO_LARGE"
|
|
17340
|
-
};
|
|
17341
|
-
let json;
|
|
17342
|
-
try {
|
|
17343
|
-
json = JSON.parse(text);
|
|
17344
|
-
} catch {
|
|
17345
|
-
return {
|
|
17346
|
-
ok: false,
|
|
17347
|
-
id: NIL_UUID,
|
|
17348
|
-
msg: "invalid json"
|
|
17349
|
-
};
|
|
17350
|
-
}
|
|
17351
|
-
const env = Envelope.safeParse(json);
|
|
17352
|
-
if (!env.success) return {
|
|
17353
|
-
ok: false,
|
|
17354
|
-
id: typeof json === "object" && json !== null && typeof json.id === "string" ? json.id : NIL_UUID,
|
|
17355
|
-
msg: env.error.message
|
|
17356
|
-
};
|
|
17357
|
-
const schema = FRAME_SCHEMAS[env.data.type];
|
|
17358
|
-
if (!schema) return {
|
|
17359
|
-
ok: false,
|
|
17360
|
-
id: env.data.id,
|
|
17361
|
-
msg: "UNKNOWN_FRAME"
|
|
17362
|
-
};
|
|
17363
|
-
const payload = schema.safeParse(env.data.payload);
|
|
17364
|
-
if (!payload.success) return {
|
|
17365
|
-
ok: false,
|
|
17366
|
-
id: env.data.id,
|
|
17367
|
-
msg: payload.error.message
|
|
17368
|
-
};
|
|
17369
|
-
const ext = extractControlExt(json);
|
|
17370
|
-
return {
|
|
17371
|
-
ok: true,
|
|
17372
|
-
frame: {
|
|
17373
|
-
...env.data,
|
|
17374
|
-
payload: payload.data
|
|
17375
|
-
},
|
|
17376
|
-
...ext ? { ext } : {}
|
|
17377
|
-
};
|
|
17378
|
-
}
|
|
17379
|
-
function buildEnvelope(type, payload, opts = {}) {
|
|
17380
|
-
return {
|
|
17381
|
-
v: 1,
|
|
17382
|
-
id: opts.id ?? randomUUID(),
|
|
17383
|
-
ts: opts.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
17384
|
-
type,
|
|
17385
|
-
payload,
|
|
17386
|
-
...opts.corr ? { corr: opts.corr } : {},
|
|
17387
|
-
...opts.ext ?? {}
|
|
17388
|
-
};
|
|
17389
|
-
}
|
|
17390
|
-
function encode(frame) {
|
|
17391
|
-
return JSON.stringify(frame);
|
|
17392
|
-
}
|
|
17393
|
-
//#endregion
|
|
17394
|
-
//#region ../protocol/src/index.ts
|
|
17395
|
-
/**
|
|
17396
|
-
* Narrowing guard factory: `isFrame("auth")(frame)` narrows a decoded
|
|
17397
|
-
* `AnyFrame` to the member whose `type` matches.
|
|
17398
|
-
*/
|
|
17399
|
-
function isFrame(type) {
|
|
17400
|
-
return (frame) => frame.type === type;
|
|
17401
|
-
}
|
|
17402
|
-
//#endregion
|
|
17403
16875
|
//#region ../../node_modules/.pnpm/ws@8.21.0/node_modules/ws/lib/constants.js
|
|
17404
16876
|
var require_constants$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
17405
16877
|
const BINARY_TYPES = [
|
|
@@ -23126,6 +22598,34 @@ function routeRules(msg, rules, threadOwner) {
|
|
|
23126
22598
|
return null;
|
|
23127
22599
|
}
|
|
23128
22600
|
//#endregion
|
|
22601
|
+
//#region src/commands/commands.ts
|
|
22602
|
+
/** Accepted command prefixes (Slack uses `!`; `/` is for future platforms). */
|
|
22603
|
+
const COMMAND_PREFIXES = ["!", "/"];
|
|
22604
|
+
const STOP_WORDS = /* @__PURE__ */ new Set(["stop", "cancel"]);
|
|
22605
|
+
const QUEUE_WORDS = /* @__PURE__ */ new Set(["queue"]);
|
|
22606
|
+
/**
|
|
22607
|
+
* Parse a leading control command from a message's text. Returns `null` when the
|
|
22608
|
+
* text is not a recognized command (so it flows to the agent unchanged). The
|
|
22609
|
+
* prefix must be the first non-whitespace character and be followed immediately by
|
|
22610
|
+
* a known command word, so ordinary text like `hello!` or `! note` is never a
|
|
22611
|
+
* command.
|
|
22612
|
+
*/
|
|
22613
|
+
function parseCommand(raw) {
|
|
22614
|
+
const text = raw.trimStart();
|
|
22615
|
+
const prefix = COMMAND_PREFIXES.find((p) => text.startsWith(p));
|
|
22616
|
+
if (!prefix) return null;
|
|
22617
|
+
const m = /^([a-zA-Z]+)([\s\S]*)$/.exec(text.slice(prefix.length));
|
|
22618
|
+
if (!m) return null;
|
|
22619
|
+
const word = m[1].toLowerCase();
|
|
22620
|
+
const arg = (m[2] ?? "").trim();
|
|
22621
|
+
if (STOP_WORDS.has(word)) return { kind: "stop" };
|
|
22622
|
+
if (QUEUE_WORDS.has(word)) return {
|
|
22623
|
+
kind: "queue",
|
|
22624
|
+
text: arg
|
|
22625
|
+
};
|
|
22626
|
+
return null;
|
|
22627
|
+
}
|
|
22628
|
+
//#endregion
|
|
23129
22629
|
//#region src/router/routing-rule.ts
|
|
23130
22630
|
/**
|
|
23131
22631
|
* Resolve an agent to its first Slack integration's `{ integrationId, botUserId }`.
|
|
@@ -80536,6 +80036,7 @@ const LOADING_MSGS = [
|
|
|
80536
80036
|
"Crunching through it…",
|
|
80537
80037
|
"Hang tight…"
|
|
80538
80038
|
];
|
|
80039
|
+
const MAX_QUEUED_PER_SESSION = 10;
|
|
80539
80040
|
var Daemon = class {
|
|
80540
80041
|
opts;
|
|
80541
80042
|
store;
|
|
@@ -80697,6 +80198,11 @@ var Daemon = class {
|
|
|
80697
80198
|
}
|
|
80698
80199
|
this.seenMsgIds.add(msg.msgId);
|
|
80699
80200
|
if (this.seenMsgIds.size > 2e3) this.seenMsgIds.clear();
|
|
80201
|
+
const command = parseCommand(msg.text);
|
|
80202
|
+
if (command) {
|
|
80203
|
+
this.handleCommand(command, msg);
|
|
80204
|
+
return;
|
|
80205
|
+
}
|
|
80700
80206
|
const result = routeRules(msg, this.mergedRules(), (c, t) => this.sessions.threadOwner(c, t));
|
|
80701
80207
|
if (!result) {
|
|
80702
80208
|
this.log.debug(`routing: dropped message in ch=${msg.channel} (no agent matched — not a mention of a known bot, not a subscribed 'all' channel, not a thread/DM hit)`);
|
|
@@ -80705,6 +80211,67 @@ var Daemon = class {
|
|
|
80705
80211
|
this.log.info(`routing: ch=${msg.channel} → agent "${result.agentId}" (integration ${result.integrationId})`);
|
|
80706
80212
|
this.dispatch(result.agentId, msg, result.integrationId).catch((err) => this.log.error(`dispatch failed for agent "${result.agentId}": ${err.stack ?? err}`));
|
|
80707
80213
|
}
|
|
80214
|
+
queued = /* @__PURE__ */ new Map();
|
|
80215
|
+
/**
|
|
80216
|
+
* Handle an in-conversation control command. Resolves the target agent via the
|
|
80217
|
+
* same routing ladder as a normal message (so thread-affinity + per-integration
|
|
80218
|
+
* `allowedUserIds` authz apply), then acts on that agent's session in this
|
|
80219
|
+
* (channel, thread).
|
|
80220
|
+
*/
|
|
80221
|
+
handleCommand(command, msg) {
|
|
80222
|
+
const target = routeRules(msg, this.mergedRules(), (c, t) => this.sessions.threadOwner(c, t));
|
|
80223
|
+
if (!target) {
|
|
80224
|
+
this.log.debug(`command: '${command.kind}' in ch=${msg.channel} — no agent resolved, ignoring`);
|
|
80225
|
+
return;
|
|
80226
|
+
}
|
|
80227
|
+
const conn = this.replyConnFor(target.agentId, target.integrationId);
|
|
80228
|
+
const thread = msg.thread ?? msg.msgId;
|
|
80229
|
+
const acpSessionId = this.store.getSession(sessionKey(msg.platform, msg.channel, thread, target.agentId))?.acpSessionId;
|
|
80230
|
+
const inflight = !!(acpSessionId && this.pending.has(acpSessionId));
|
|
80231
|
+
if (command.kind === "stop") {
|
|
80232
|
+
if (!inflight) {
|
|
80233
|
+
conn?.postMessage(msg.channel, "Nothing is running to stop.", thread);
|
|
80234
|
+
return;
|
|
80235
|
+
}
|
|
80236
|
+
this.queued.delete(acpSessionId);
|
|
80237
|
+
this.log.info(`command: stop → agent "${target.agentId}" session ${acpSessionId}`);
|
|
80238
|
+
this.hosts.get(target.agentId)?.cancel(acpSessionId).catch((err) => this.log.error(`command stop: cancel failed: ${err.message}`));
|
|
80239
|
+
conn?.postMessage(msg.channel, "🛑 Stopped.", thread);
|
|
80240
|
+
return;
|
|
80241
|
+
}
|
|
80242
|
+
if (!command.text) {
|
|
80243
|
+
conn?.postMessage(msg.channel, "Usage: `!queue <message>` — runs when the current turn finishes.", thread);
|
|
80244
|
+
return;
|
|
80245
|
+
}
|
|
80246
|
+
const payload = {
|
|
80247
|
+
...msg,
|
|
80248
|
+
text: command.text
|
|
80249
|
+
};
|
|
80250
|
+
if (!inflight) {
|
|
80251
|
+
this.log.info(`command: queue → agent "${target.agentId}" idle, dispatching now`);
|
|
80252
|
+
this.dispatch(target.agentId, payload, target.integrationId).catch((err) => this.log.error(`queued dispatch failed for agent "${target.agentId}": ${err.stack ?? err}`));
|
|
80253
|
+
return;
|
|
80254
|
+
}
|
|
80255
|
+
const q = this.queued.get(acpSessionId) ?? [];
|
|
80256
|
+
if (q.length >= MAX_QUEUED_PER_SESSION) {
|
|
80257
|
+
this.log.warn(`command: queue → agent "${target.agentId}" session ${acpSessionId} full (${q.length}), rejected`);
|
|
80258
|
+
conn?.postMessage(msg.channel, `Queue is full (${MAX_QUEUED_PER_SESSION} pending) — wait for the current turn to finish.`, thread);
|
|
80259
|
+
return;
|
|
80260
|
+
}
|
|
80261
|
+
q.push(payload);
|
|
80262
|
+
this.queued.set(acpSessionId, q);
|
|
80263
|
+
this.log.info(`command: queue → agent "${target.agentId}" session ${acpSessionId} (depth ${q.length})`);
|
|
80264
|
+
conn?.postMessage(msg.channel, `📥 Queued (#${q.length}) — will run when the current turn finishes.`, thread);
|
|
80265
|
+
}
|
|
80266
|
+
/** Drain one buffered message for a session whose turn just finished (FIFO). */
|
|
80267
|
+
flushQueued(agentId, sessionId, integrationId) {
|
|
80268
|
+
const q = this.queued.get(sessionId);
|
|
80269
|
+
if (!q || q.length === 0) return;
|
|
80270
|
+
const next = q.shift();
|
|
80271
|
+
if (q.length === 0) this.queued.delete(sessionId);
|
|
80272
|
+
this.log.info(`queue: dispatching buffered message to agent "${agentId}" session ${sessionId} (${q.length} left)`);
|
|
80273
|
+
this.dispatch(agentId, next, integrationId).catch((err) => this.log.error(`queued dispatch failed for agent "${agentId}": ${err.stack ?? err}`));
|
|
80274
|
+
}
|
|
80708
80275
|
/** Local layer (agent.json) ∪ resolved CP layer; unservable CP rules are dropped + warn-logged. */
|
|
80709
80276
|
mergedRules() {
|
|
80710
80277
|
const local = [...this.agents.values()].flatMap((a) => rulesFromAgent(a, this.botUserIds));
|
|
@@ -80747,6 +80314,7 @@ var Daemon = class {
|
|
|
80747
80314
|
} finally {
|
|
80748
80315
|
this.pending.delete(sessionId);
|
|
80749
80316
|
}
|
|
80317
|
+
this.flushQueued(agentId, sessionId, integrationId);
|
|
80750
80318
|
}
|
|
80751
80319
|
/** Route a converger action: set-status → setStatus (loading_messages only when not clearing); else postMessage. */
|
|
80752
80320
|
async applyAction(action, conn, channel, thread) {
|