@agentconnect.md/daemon 1.0.0-rc.29 → 1.0.0-rc.30
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 +178 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -12768,6 +12768,7 @@ var simpleGit = gitInstanceFactory;
|
|
|
12768
12768
|
//#endregion
|
|
12769
12769
|
//#region src/workspace/workspace-manager.ts
|
|
12770
12770
|
const PULL_TIMEOUT_MS = 4500;
|
|
12771
|
+
const cloneInFlight = /* @__PURE__ */ new Map();
|
|
12771
12772
|
async function prepareWorkspace(agent) {
|
|
12772
12773
|
const cwd = agent.workspace.path;
|
|
12773
12774
|
mkdirSync(cwd, { recursive: true });
|
|
@@ -12776,11 +12777,52 @@ async function prepareWorkspace(agent) {
|
|
|
12776
12777
|
if (!existsSync(mem)) writeFileSync(mem, `# ${agent.name} memory\n`);
|
|
12777
12778
|
return cwd;
|
|
12778
12779
|
}
|
|
12779
|
-
if (
|
|
12780
|
+
if (!existsSync(join(cwd, ".git"))) {
|
|
12781
|
+
await cloneRepo(agent);
|
|
12782
|
+
return cwd;
|
|
12783
|
+
}
|
|
12784
|
+
if (agent.workspace.pullOnNewSession) try {
|
|
12780
12785
|
await Promise.race([simpleGit(cwd).pull(["--ff-only"]), new Promise((_, rej) => setTimeout(() => rej(/* @__PURE__ */ new Error("pull timeout")), PULL_TIMEOUT_MS))]);
|
|
12781
12786
|
} catch {}
|
|
12782
12787
|
return cwd;
|
|
12783
12788
|
}
|
|
12789
|
+
/**
|
|
12790
|
+
* Eagerly clone a git-repo workspace that has no checkout yet — for reconcile-time
|
|
12791
|
+
* prefetch so the repo is warm before the first message arrives. No-op for
|
|
12792
|
+
* from-scratch or an already-cloned checkout (the session-time `prepareWorkspace`
|
|
12793
|
+
* stays authoritative and still owns pull + hard-fail). Reuses the single-flight
|
|
12794
|
+
* lock so a prefetch and a concurrent session-start clone never race into the same
|
|
12795
|
+
* dir. The caller invokes this fire-and-forget and logs any rejection — clone
|
|
12796
|
+
* failure here is non-fatal because `prepareWorkspace` re-clones (and hard-fails)
|
|
12797
|
+
* on the first session as the backstop.
|
|
12798
|
+
*/
|
|
12799
|
+
async function prefetchWorkspace(agent) {
|
|
12800
|
+
if (agent.workspace.mode !== "git-repo") return;
|
|
12801
|
+
const cwd = agent.workspace.path;
|
|
12802
|
+
if (existsSync(join(cwd, ".git"))) return;
|
|
12803
|
+
mkdirSync(cwd, { recursive: true });
|
|
12804
|
+
await cloneRepo(agent);
|
|
12805
|
+
}
|
|
12806
|
+
/** Clone agent.workspace.gitRepo @ gitBranch into cwd, single-flight per cwd. */
|
|
12807
|
+
async function cloneRepo(agent) {
|
|
12808
|
+
const cwd = agent.workspace.path;
|
|
12809
|
+
const inflight = cloneInFlight.get(cwd);
|
|
12810
|
+
if (inflight) return inflight;
|
|
12811
|
+
const gitRepo = agent.workspace.gitRepo;
|
|
12812
|
+
if (!gitRepo) throw new Error(`workspace clone: agent "${agent.id}" has git-repo mode but no gitRepo configured`);
|
|
12813
|
+
const branch = agent.workspace.gitBranch;
|
|
12814
|
+
const p = (async () => {
|
|
12815
|
+
await simpleGit().clone(gitRepo, cwd, [
|
|
12816
|
+
"--branch",
|
|
12817
|
+
branch,
|
|
12818
|
+
"--single-branch"
|
|
12819
|
+
]);
|
|
12820
|
+
})().finally(() => {
|
|
12821
|
+
cloneInFlight.delete(cwd);
|
|
12822
|
+
});
|
|
12823
|
+
cloneInFlight.set(cwd, p);
|
|
12824
|
+
return p;
|
|
12825
|
+
}
|
|
12784
12826
|
//#endregion
|
|
12785
12827
|
//#region ../../node_modules/.pnpm/@agentclientprotocol+sdk@1.0.0_zod@4.4.3/node_modules/@agentclientprotocol/sdk/dist/schema/index.js
|
|
12786
12828
|
const AGENT_METHODS = {
|
|
@@ -23121,27 +23163,60 @@ function signature(a) {
|
|
|
23121
23163
|
const { dir, env, ...rest } = a;
|
|
23122
23164
|
return JSON.stringify(rest);
|
|
23123
23165
|
}
|
|
23166
|
+
/** Sub-signature over the dimensions that determine the ACP host subprocess —
|
|
23167
|
+
* the spawn binary (`runtime`) and the child env / system-prompt seed knobs that
|
|
23168
|
+
* are baked into the host at spawn (agentChildEnv + cpRuntimeEnv). A change here
|
|
23169
|
+
* means the cached host must be evicted so the next session respawns it fresh. */
|
|
23170
|
+
function hostSpawnSig(a) {
|
|
23171
|
+
return JSON.stringify({
|
|
23172
|
+
runtime: a.runtime,
|
|
23173
|
+
model: a.runtimeOverrides?.model,
|
|
23174
|
+
description: a.description,
|
|
23175
|
+
reasoningEffort: a.reasoningEffort,
|
|
23176
|
+
executionMode: a.executionMode,
|
|
23177
|
+
env: a.runtimeOverrides?.env
|
|
23178
|
+
});
|
|
23179
|
+
}
|
|
23180
|
+
/** Sub-signature over the workspace dimension — cwd is materialized per session by
|
|
23181
|
+
* prepareWorkspace(agent), so a workspace change must also evict the host so the
|
|
23182
|
+
* next session re-materializes the (possibly re-pointed/re-cloned) checkout. */
|
|
23183
|
+
function workspaceSig(a) {
|
|
23184
|
+
return JSON.stringify(a.workspace);
|
|
23185
|
+
}
|
|
23186
|
+
/** Sub-signature over the integration dimension — Slack app/bot tokens + bindRules.
|
|
23187
|
+
* A change here rebuilds routing (and, where safe, re-opens Slack connections) but
|
|
23188
|
+
* does NOT by itself touch the host. */
|
|
23189
|
+
function integrationsSig(a) {
|
|
23190
|
+
return JSON.stringify(a.integrations);
|
|
23191
|
+
}
|
|
23124
23192
|
/**
|
|
23125
23193
|
* Diff desired (freshly loaded active agents) against the running set.
|
|
23126
|
-
* - `toStart`
|
|
23127
|
-
* - `toStop`
|
|
23128
|
-
* - `
|
|
23129
|
-
*
|
|
23130
|
-
*
|
|
23194
|
+
* - `toStart` — desired ids not currently running.
|
|
23195
|
+
* - `toStop` — running ids no longer desired.
|
|
23196
|
+
* - `toChange` — same id, changed effective config. Emitted whenever the overall
|
|
23197
|
+
* signature differs; the three booleans classify which dimensions moved so the
|
|
23198
|
+
* caller can react minimally (evict host vs rebuild routing vs nothing). All
|
|
23199
|
+
* three false ⇒ a soft-only change. Without `toChange` an in-place agent.json
|
|
23200
|
+
* edit would silently be a no-op.
|
|
23131
23201
|
*/
|
|
23132
23202
|
function diffAgents(desired, actual) {
|
|
23133
23203
|
const desiredIds = new Set(desired.map((a) => a.id));
|
|
23134
23204
|
const toStart = [];
|
|
23135
|
-
const
|
|
23205
|
+
const toChange = [];
|
|
23136
23206
|
for (const a of desired) {
|
|
23137
23207
|
const cur = actual.get(a.id);
|
|
23138
23208
|
if (!cur) toStart.push(a);
|
|
23139
|
-
else if (signature(cur) !== signature(a))
|
|
23209
|
+
else if (signature(cur) !== signature(a)) toChange.push({
|
|
23210
|
+
agent: a,
|
|
23211
|
+
hostRespawn: hostSpawnSig(cur) !== hostSpawnSig(a),
|
|
23212
|
+
workspace: workspaceSig(cur) !== workspaceSig(a),
|
|
23213
|
+
integrations: integrationsSig(cur) !== integrationsSig(a)
|
|
23214
|
+
});
|
|
23140
23215
|
}
|
|
23141
23216
|
return {
|
|
23142
23217
|
toStart,
|
|
23143
23218
|
toStop: [...actual.keys()].filter((id) => !desiredIds.has(id)),
|
|
23144
|
-
|
|
23219
|
+
toChange
|
|
23145
23220
|
};
|
|
23146
23221
|
}
|
|
23147
23222
|
//#endregion
|
|
@@ -79663,6 +79738,10 @@ var SlackConnection = class {
|
|
|
79663
79738
|
deps;
|
|
79664
79739
|
app;
|
|
79665
79740
|
botUserId = "";
|
|
79741
|
+
/** The appToken this socket is keyed by (one socket per unique appToken). */
|
|
79742
|
+
appToken;
|
|
79743
|
+
/** The botToken this socket authenticated with (used to detect a same-appToken swap). */
|
|
79744
|
+
botToken;
|
|
79666
79745
|
constructor(deps, factory = (o) => new App({
|
|
79667
79746
|
token: o.token,
|
|
79668
79747
|
appToken: o.appToken,
|
|
@@ -79670,6 +79749,8 @@ var SlackConnection = class {
|
|
|
79670
79749
|
...deps.boltDebug ? { logLevel: LogLevel.DEBUG } : {}
|
|
79671
79750
|
})) {
|
|
79672
79751
|
this.deps = deps;
|
|
79752
|
+
this.appToken = deps.group.appToken;
|
|
79753
|
+
this.botToken = deps.group.botToken;
|
|
79673
79754
|
this.app = factory({
|
|
79674
79755
|
token: deps.group.botToken,
|
|
79675
79756
|
appToken: deps.group.appToken
|
|
@@ -81564,8 +81645,15 @@ var Daemon = class {
|
|
|
81564
81645
|
const files = this.loadAgentList();
|
|
81565
81646
|
this.fileAgents = new Map(files.map((a) => [a.id, a]));
|
|
81566
81647
|
const desired = this.effectiveAgents();
|
|
81567
|
-
const { toStart, toStop,
|
|
81568
|
-
if (toStart.length || toStop.length ||
|
|
81648
|
+
const { toStart, toStop, toChange } = diffAgents(desired, this.agents);
|
|
81649
|
+
if (toStart.length || toStop.length || toChange.length) this.log.info(`reconcile: ${desired.length} desired agent(s) from ${this.agentsDir}; start=[${toStart.map((a) => a.id).join(", ")}] stop=[${toStop.join(", ")}] change=[${toChange.map((c) => {
|
|
81650
|
+
const dims = [
|
|
81651
|
+
c.hostRespawn && "host",
|
|
81652
|
+
c.workspace && "workspace",
|
|
81653
|
+
c.integrations && "integrations"
|
|
81654
|
+
].filter(Boolean).join("+");
|
|
81655
|
+
return `${c.agent.id}(${dims || "soft"})`;
|
|
81656
|
+
}).join(", ")}]`);
|
|
81569
81657
|
else this.log.debug(`reconcile: no changes (${desired.length} desired agent(s))`);
|
|
81570
81658
|
for (const id of toStop) {
|
|
81571
81659
|
const host = this.hosts.get(id);
|
|
@@ -81576,16 +81664,88 @@ var Daemon = class {
|
|
|
81576
81664
|
this.hostStarts.delete(id);
|
|
81577
81665
|
this.agents.delete(id);
|
|
81578
81666
|
}
|
|
81579
|
-
for (const
|
|
81580
|
-
const
|
|
81581
|
-
|
|
81582
|
-
|
|
81583
|
-
this.hosts.
|
|
81667
|
+
for (const change of toChange) {
|
|
81668
|
+
const a = change.agent;
|
|
81669
|
+
this.agents.set(a.id, a);
|
|
81670
|
+
if (change.hostRespawn || change.workspace) {
|
|
81671
|
+
const host = this.hosts.get(a.id);
|
|
81672
|
+
if (host) {
|
|
81673
|
+
await host.stop();
|
|
81674
|
+
this.hosts.delete(a.id);
|
|
81675
|
+
}
|
|
81676
|
+
this.hostStarts.delete(a.id);
|
|
81584
81677
|
}
|
|
81585
|
-
this.
|
|
81678
|
+
if (change.workspace) this.prefetchClone(a);
|
|
81679
|
+
if (change.integrations) await this.reconcileSlackConnections();
|
|
81680
|
+
}
|
|
81681
|
+
for (const a of toStart) {
|
|
81586
81682
|
this.agents.set(a.id, a);
|
|
81683
|
+
this.prefetchClone(a);
|
|
81684
|
+
}
|
|
81685
|
+
if (toStart.some((a) => a.integrations.some((i) => i.platform === "slack")) || toStop.length) await this.reconcileSlackConnections();
|
|
81686
|
+
}
|
|
81687
|
+
/**
|
|
81688
|
+
* Fire-and-forget eager clone of a git-repo workspace so the checkout is warm
|
|
81689
|
+
* before the first message. Deliberately NOT awaited: reconcile must not block on
|
|
81690
|
+
* the network (design §4.3), and `prepareWorkspace` is the authoritative clone
|
|
81691
|
+
* (awaited + hard-fail) at session start — so a prefetch failure here is only
|
|
81692
|
+
* logged and harmlessly retried then. No-op for from-scratch / already-cloned.
|
|
81693
|
+
*/
|
|
81694
|
+
prefetchClone(agent) {
|
|
81695
|
+
prefetchWorkspace(agent).catch((err) => this.log.warn(`workspace: prefetch clone for "${agent.id}" failed (will retry at first session): ${formatErr(err)}`));
|
|
81696
|
+
}
|
|
81697
|
+
/**
|
|
81698
|
+
* Reconcile the connection-derived Slack state (`botUserIds`, `connByIntegration`,
|
|
81699
|
+
* open sockets) against the live `this.agents` set. Routing itself is rebuilt
|
|
81700
|
+
* implicitly each message (mergedRules reads this.agents) — this only maintains the
|
|
81701
|
+
* socket layer that is otherwise written only at startup.
|
|
81702
|
+
*
|
|
81703
|
+
* Safe-by-construction (per the recon report):
|
|
81704
|
+
* - NEW appToken → construct + start an isolated socket, then fan botUserId/conn
|
|
81705
|
+
* out to every integrationId on that appToken. A failed start() is logged and
|
|
81706
|
+
* leaves all existing sockets untouched (never throws out of reconcile).
|
|
81707
|
+
* - NEW integration reusing an ALREADY-OPEN appToken → no socket churn; just
|
|
81708
|
+
* backfill botUserIds/connByIntegration from the existing conn (mention routing
|
|
81709
|
+
* for the new bot would otherwise silently never match).
|
|
81710
|
+
* - REMOVED appToken / close-or-replace of a still-shared socket → NOT performed
|
|
81711
|
+
* here. Tearing down a socket that other agents or in-flight turns depend on is
|
|
81712
|
+
* the one genuinely dangerous op; it is left as a logged deferred-close TODO so
|
|
81713
|
+
* a stale socket simply lingers (harmless) rather than risking the daemon.
|
|
81714
|
+
*/
|
|
81715
|
+
async reconcileSlackConnections() {
|
|
81716
|
+
const groups = consolidate([...this.agents.values()]);
|
|
81717
|
+
new Set([...this.connByIntegration.values()].map((c) => c.appToken));
|
|
81718
|
+
for (const group of groups.values()) {
|
|
81719
|
+
const existing = [...this.connByIntegration.values()].find((c) => c.appToken === group.appToken);
|
|
81720
|
+
if (existing) {
|
|
81721
|
+
if (existing.botToken !== group.botToken) this.log.warn(`slack: botToken changed for appToken (socket bot user ${existing.botUserId}) — deferred: the live socket keeps the old botToken until it is closed+reopened`);
|
|
81722
|
+
for (const { integrationId } of group.integrations) if (this.connByIntegration.get(integrationId) !== existing) {
|
|
81723
|
+
this.botUserIds[integrationId] = existing.botUserId;
|
|
81724
|
+
this.connByIntegration.set(integrationId, existing);
|
|
81725
|
+
this.log.info(`slack: bound integration ${integrationId} onto existing socket (appToken reuse)`);
|
|
81726
|
+
}
|
|
81727
|
+
continue;
|
|
81728
|
+
}
|
|
81729
|
+
try {
|
|
81730
|
+
const conn = new SlackConnection({
|
|
81731
|
+
group,
|
|
81732
|
+
newTraceId: () => randomUUID(),
|
|
81733
|
+
onMessage: (msg) => this.onInbound(msg),
|
|
81734
|
+
log: this.log,
|
|
81735
|
+
boltDebug: this.cfg.logging.level === "debug" || this.cfg.logging.level === "trace"
|
|
81736
|
+
});
|
|
81737
|
+
this.log.info(`slack: opening new socket at runtime (${group.integrations.length} integration(s): ${group.integrations.map((i) => i.agentId).join(", ")})…`);
|
|
81738
|
+
await conn.start();
|
|
81739
|
+
this.log.info(`slack: runtime socket connected as bot user ${conn.botUserId}`);
|
|
81740
|
+
for (const { integrationId } of group.integrations) {
|
|
81741
|
+
this.botUserIds[integrationId] = conn.botUserId;
|
|
81742
|
+
this.connByIntegration.set(integrationId, conn);
|
|
81743
|
+
}
|
|
81744
|
+
this.connections.push(conn);
|
|
81745
|
+
} catch (err) {
|
|
81746
|
+
this.log.error(`slack: failed to open runtime socket for appToken — leaving existing sockets intact: ${formatErr(err)}`);
|
|
81747
|
+
}
|
|
81587
81748
|
}
|
|
81588
|
-
for (const a of toStart) this.agents.set(a.id, a);
|
|
81589
81749
|
}
|
|
81590
81750
|
ensureHost(agentId, cfg) {
|
|
81591
81751
|
let host = this.hosts.get(agentId);
|