@agentconnect.md/daemon 1.0.0-rc.76 → 1.0.0-rc.78
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 +246 -82
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7659,14 +7659,22 @@ const IntegrationSchema = object({
|
|
|
7659
7659
|
platform: literal("slack"),
|
|
7660
7660
|
slack: SlackConfigSchema
|
|
7661
7661
|
});
|
|
7662
|
+
/** A scheduled trigger for THIS agent: every `schedule` tick, prompt the agent
|
|
7663
|
+
* with `trigger`. `target` is optional output routing — when present the daemon
|
|
7664
|
+
* posts the trigger into that channel and the session replies in its thread;
|
|
7665
|
+
* absent ⇒ headless fire (no platform output). `origin:"cp"` marks CP-pushed
|
|
7666
|
+
* entries (written by cron/upsert, pruned by drop.crons); hand-authored entries
|
|
7667
|
+
* have no origin and are never touched by the CP. */
|
|
7662
7668
|
const CronDefSchema = object({
|
|
7663
7669
|
id: string(),
|
|
7664
7670
|
schedule: string(),
|
|
7665
7671
|
target: object({
|
|
7666
7672
|
platform: literal("slack"),
|
|
7667
7673
|
channel: string()
|
|
7668
|
-
}),
|
|
7669
|
-
trigger: string()
|
|
7674
|
+
}).optional(),
|
|
7675
|
+
trigger: string(),
|
|
7676
|
+
enabled: boolean().default(true),
|
|
7677
|
+
origin: literal("cp").optional()
|
|
7670
7678
|
});
|
|
7671
7679
|
const AgentSchema = object({
|
|
7672
7680
|
id: string(),
|
|
@@ -12912,13 +12920,27 @@ const DrainDone = object({ released: array(SessionKey) });
|
|
|
12912
12920
|
/**
|
|
12913
12921
|
* Cron sinks to the daemon (D5) — protocol §5.4.
|
|
12914
12922
|
*
|
|
12915
|
-
*
|
|
12916
|
-
*
|
|
12923
|
+
* A cron periodically triggers ONE AGENT with a synthetic prompt (`trigger`) to
|
|
12924
|
+
* carry out some work. The CP owns the definition; the daemon owns firing +
|
|
12925
|
+
* last-run persistence, so crons fire even when the CP is down. On receipt the
|
|
12926
|
+
* daemon persists the def into the owning agent's `agent.json` `crons[]` (the
|
|
12927
|
+
* single source of truth, same model as integrations) — surviving a restart
|
|
12928
|
+
* with the CP down.
|
|
12929
|
+
*
|
|
12930
|
+
* `target` is OPTIONAL output routing: when present, the daemon posts the
|
|
12931
|
+
* trigger as a real message in that channel and the agent's session replies in
|
|
12932
|
+
* its thread; when absent the fire is headless (the agent works with no
|
|
12933
|
+
* platform output).
|
|
12917
12934
|
*/
|
|
12935
|
+
const CronTarget = object({
|
|
12936
|
+
platform: _enum(["slack", "telegram"]).default("slack"),
|
|
12937
|
+
channel: string()
|
|
12938
|
+
});
|
|
12918
12939
|
const CronUpsert = object({
|
|
12919
12940
|
cronId: string().uuid(),
|
|
12941
|
+
agentId: string().uuid(),
|
|
12920
12942
|
schedule: string(),
|
|
12921
|
-
target:
|
|
12943
|
+
target: CronTarget.optional(),
|
|
12922
12944
|
trigger: string(),
|
|
12923
12945
|
enabled: boolean().default(true)
|
|
12924
12946
|
});
|
|
@@ -23846,6 +23868,11 @@ var LocalStore = class {
|
|
|
23846
23868
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
23847
23869
|
routingEpoch INTEGER, assignments TEXT, globalRules TEXT
|
|
23848
23870
|
);
|
|
23871
|
+
-- Authoritative last-run per cron (protocol §5.4 — missed-fire compensation).
|
|
23872
|
+
-- key = "<agentId>:<cronId>" (cron defs themselves live in agent.json).
|
|
23873
|
+
CREATE TABLE IF NOT EXISTS cron_runs (
|
|
23874
|
+
key TEXT PRIMARY KEY, lastRunAt INTEGER NOT NULL
|
|
23875
|
+
);
|
|
23849
23876
|
`);
|
|
23850
23877
|
this.migrateSessionUsage();
|
|
23851
23878
|
}
|
|
@@ -24046,6 +24073,17 @@ var LocalStore = class {
|
|
|
24046
24073
|
globalRules
|
|
24047
24074
|
});
|
|
24048
24075
|
}
|
|
24076
|
+
/** Stamp a cron fire (key = `<agentId>:<cronId>`). */
|
|
24077
|
+
setCronLastRun(key, lastRunAt) {
|
|
24078
|
+
this.db.prepare(`INSERT INTO cron_runs (key, lastRunAt) VALUES (@key, @lastRunAt)
|
|
24079
|
+
ON CONFLICT(key) DO UPDATE SET lastRunAt=excluded.lastRunAt`).run({
|
|
24080
|
+
key,
|
|
24081
|
+
lastRunAt
|
|
24082
|
+
});
|
|
24083
|
+
}
|
|
24084
|
+
getCronLastRun(key) {
|
|
24085
|
+
return this.db.prepare("SELECT lastRunAt FROM cron_runs WHERE key = ?").get(key)?.lastRunAt;
|
|
24086
|
+
}
|
|
24049
24087
|
close() {
|
|
24050
24088
|
this.db.close();
|
|
24051
24089
|
}
|
|
@@ -82082,7 +82120,7 @@ function buildSyntheticMessage(agentId, cron, traceId) {
|
|
|
82082
82120
|
traceId,
|
|
82083
82121
|
source: "cron",
|
|
82084
82122
|
platform: "slack",
|
|
82085
|
-
channel: cron.target.
|
|
82123
|
+
channel: cron.target?.channel ?? `cron:${cron.id}`,
|
|
82086
82124
|
thread: `cron:${cron.id}:${traceId}`,
|
|
82087
82125
|
sender: {
|
|
82088
82126
|
id: `cron:${cron.id}`,
|
|
@@ -82091,25 +82129,52 @@ function buildSyntheticMessage(agentId, cron, traceId) {
|
|
|
82091
82129
|
text: cron.trigger,
|
|
82092
82130
|
mentionedBots: [],
|
|
82093
82131
|
isDm: false,
|
|
82094
|
-
trigger: "cron"
|
|
82132
|
+
trigger: "cron",
|
|
82133
|
+
...cron.target ? {} : { headless: true }
|
|
82095
82134
|
}
|
|
82096
82135
|
};
|
|
82097
82136
|
}
|
|
82137
|
+
/**
|
|
82138
|
+
* Local scheduler for `agent.json.crons[]` (D5). Jobs are keyed per agent so the
|
|
82139
|
+
* reconciler can converge them on any agent change (design §5.2: crons change →
|
|
82140
|
+
* Scheduler upsert/remove) — `sync` replaces an agent's whole job set, `unregister`
|
|
82141
|
+
* drops it. A malformed schedule skips that one cron (warned), never the rest.
|
|
82142
|
+
*/
|
|
82098
82143
|
var Scheduler = class {
|
|
82099
82144
|
deps;
|
|
82100
|
-
jobs =
|
|
82145
|
+
jobs = /* @__PURE__ */ new Map();
|
|
82101
82146
|
constructor(deps) {
|
|
82102
82147
|
this.deps = deps;
|
|
82103
82148
|
}
|
|
82104
|
-
|
|
82105
|
-
|
|
82106
|
-
|
|
82107
|
-
|
|
82108
|
-
|
|
82149
|
+
/** Converge one agent's job set to its current `crons[]` (replace-all, idempotent).
|
|
82150
|
+
* Disabled crons stay on disk but are not scheduled. */
|
|
82151
|
+
sync(agentId, crons) {
|
|
82152
|
+
this.unregister(agentId);
|
|
82153
|
+
const jobs = [];
|
|
82154
|
+
for (const cron of crons) {
|
|
82155
|
+
if (cron.enabled === false) continue;
|
|
82156
|
+
try {
|
|
82157
|
+
jobs.push(new E(cron.schedule, () => {
|
|
82158
|
+
const { msg } = buildSyntheticMessage(agentId, cron, this.deps.newTraceId());
|
|
82159
|
+
this.deps.onFire(agentId, msg, cron);
|
|
82160
|
+
}));
|
|
82161
|
+
} catch (err) {
|
|
82162
|
+
this.deps.warn?.(`scheduler: skipping cron "${cron.id}" of agent "${agentId}": ${err.message}`);
|
|
82163
|
+
}
|
|
82164
|
+
}
|
|
82165
|
+
if (jobs.length) this.jobs.set(agentId, jobs);
|
|
82166
|
+
}
|
|
82167
|
+
/** Stop and drop an agent's jobs (agent removed / deactivated). */
|
|
82168
|
+
unregister(agentId) {
|
|
82169
|
+
for (const j of this.jobs.get(agentId) ?? []) j.stop();
|
|
82170
|
+
this.jobs.delete(agentId);
|
|
82171
|
+
}
|
|
82172
|
+
/** Live job count for one agent (0 when none). */
|
|
82173
|
+
count(agentId) {
|
|
82174
|
+
return this.jobs.get(agentId)?.length ?? 0;
|
|
82109
82175
|
}
|
|
82110
82176
|
stop() {
|
|
82111
|
-
for (const
|
|
82112
|
-
this.jobs = [];
|
|
82177
|
+
for (const agentId of [...this.jobs.keys()]) this.unregister(agentId);
|
|
82113
82178
|
}
|
|
82114
82179
|
};
|
|
82115
82180
|
//#endregion
|
|
@@ -83061,41 +83126,6 @@ async function fetchHeadMtime(root) {
|
|
|
83061
83126
|
}
|
|
83062
83127
|
}
|
|
83063
83128
|
//#endregion
|
|
83064
|
-
//#region src/cp/cp-cron.ts
|
|
83065
|
-
/**
|
|
83066
|
-
* `CpCronRegistry` — the daemon's set of CP-pushed crons, keyed by `cronId`
|
|
83067
|
-
* (distinct from per-agent `agent.crons`). On fire it calls `onFire(cron)`; the
|
|
83068
|
-
* daemon resolves `cron.target.channel` to an owning agent and dispatches.
|
|
83069
|
-
* `converge` makes the live set exactly a register/ok snapshot.
|
|
83070
|
-
*/
|
|
83071
|
-
var CpCronRegistry = class {
|
|
83072
|
-
deps;
|
|
83073
|
-
jobs = /* @__PURE__ */ new Map();
|
|
83074
|
-
constructor(deps) {
|
|
83075
|
-
this.deps = deps;
|
|
83076
|
-
}
|
|
83077
|
-
upsert(cron) {
|
|
83078
|
-
this.remove(cron.cronId);
|
|
83079
|
-
if (cron.enabled === false) return;
|
|
83080
|
-
this.jobs.set(cron.cronId, new E(cron.schedule, () => this.deps.onFire(cron)));
|
|
83081
|
-
}
|
|
83082
|
-
remove(cronId) {
|
|
83083
|
-
this.jobs.get(cronId)?.stop();
|
|
83084
|
-
this.jobs.delete(cronId);
|
|
83085
|
-
}
|
|
83086
|
-
converge(crons) {
|
|
83087
|
-
const keep = new Set(crons.map((c) => c.cronId));
|
|
83088
|
-
for (const id of [...this.jobs.keys()]) if (!keep.has(id)) this.remove(id);
|
|
83089
|
-
for (const c of crons) this.upsert(c);
|
|
83090
|
-
}
|
|
83091
|
-
ids() {
|
|
83092
|
-
return [...this.jobs.keys()];
|
|
83093
|
-
}
|
|
83094
|
-
stop() {
|
|
83095
|
-
for (const id of [...this.jobs.keys()]) this.remove(id);
|
|
83096
|
-
}
|
|
83097
|
-
};
|
|
83098
|
-
//#endregion
|
|
83099
83129
|
//#region src/agents/write-agent.ts
|
|
83100
83130
|
/**
|
|
83101
83131
|
* Persist CP-owned agent spec onto the daemon's on-disk `agent.json`, which is
|
|
@@ -83228,6 +83258,127 @@ function removeAgent(agentsDir, agentId) {
|
|
|
83228
83258
|
});
|
|
83229
83259
|
}
|
|
83230
83260
|
//#endregion
|
|
83261
|
+
//#region src/agents/write-cron.ts
|
|
83262
|
+
/**
|
|
83263
|
+
* Persist CP-owned crons onto the owning agent's on-disk `agent.json` `crons[]`,
|
|
83264
|
+
* which is the SINGLE SOURCE OF TRUTH (same model as write-integration.ts) — so
|
|
83265
|
+
* CP crons survive a daemon restart with the Control Plane down: the Scheduler
|
|
83266
|
+
* re-registers them from disk alone at start.
|
|
83267
|
+
*
|
|
83268
|
+
* CP entries are marked `origin:"cp"` so they coexist with hand-authored crons:
|
|
83269
|
+
* the CP replaces/removes only its own entries and never touches user ones.
|
|
83270
|
+
*
|
|
83271
|
+
* CRITICAL: like write-agent.ts, we operate on the RAW JSON TEXT (readFileSync →
|
|
83272
|
+
* JSON.parse → mutate only `crons[]` → writeFileSync). We never serialize the
|
|
83273
|
+
* parsed/interpolated `LoadedAgent` — that has `${VAR}` already substituted and
|
|
83274
|
+
* `workspace.path` rewritten absolute. Editing the raw file preserves both.
|
|
83275
|
+
*/
|
|
83276
|
+
/** Map the wire def to the agent.json `CronDef` shape (agent-schema). */
|
|
83277
|
+
function toCronDef(cron) {
|
|
83278
|
+
return {
|
|
83279
|
+
id: cron.cronId,
|
|
83280
|
+
schedule: cron.schedule,
|
|
83281
|
+
...cron.target && cron.target.platform === "slack" ? { target: {
|
|
83282
|
+
platform: "slack",
|
|
83283
|
+
channel: cron.target.channel
|
|
83284
|
+
} } : {},
|
|
83285
|
+
trigger: cron.trigger,
|
|
83286
|
+
enabled: cron.enabled !== false,
|
|
83287
|
+
origin: "cp"
|
|
83288
|
+
};
|
|
83289
|
+
}
|
|
83290
|
+
/**
|
|
83291
|
+
* Upsert one CP cron into the owning agent's `agent.json` `crons[]` (replace the
|
|
83292
|
+
* `origin:"cp"` entry with that `id`, else append). Returns whether the file
|
|
83293
|
+
* CHANGED — an identical re-apply (every register/ok converge re-sends the full
|
|
83294
|
+
* set) skips the write so the file-watcher doesn't churn a reconcile per
|
|
83295
|
+
* reconnect. Returns false (with a warn) when no agent with `cron.agentId`
|
|
83296
|
+
* exists on disk — an orphan cron is not persisted; it self-heals when the
|
|
83297
|
+
* agent spec lands and the CP re-sends.
|
|
83298
|
+
*
|
|
83299
|
+
* The replace slot is matched by `id` AND `origin:"cp"` (mirroring
|
|
83300
|
+
* {@link removeCronDef}): a hand-authored entry that happens to share the id is
|
|
83301
|
+
* NEVER overwritten — the CP touches only its own entries. Such a collision is
|
|
83302
|
+
* skipped with a warn rather than silently converting a user cron into a CP one.
|
|
83303
|
+
*/
|
|
83304
|
+
function writeCronDef(agentsDir, cron, deps) {
|
|
83305
|
+
const file = findAgentFileById(agentsDir, cron.agentId);
|
|
83306
|
+
if (!file) {
|
|
83307
|
+
deps.warn?.(`cp: cron ${cron.cronId} references missing agent ${cron.agentId} — not persisted (will retry on next push)`);
|
|
83308
|
+
return false;
|
|
83309
|
+
}
|
|
83310
|
+
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
83311
|
+
const list = Array.isArray(raw.crons) ? raw.crons : [];
|
|
83312
|
+
const next = toCronDef(cron);
|
|
83313
|
+
const at = (c) => typeof c === "object" && c !== null && c.id === next.id;
|
|
83314
|
+
if (list.find((c) => at(c) && c.origin !== "cp")) {
|
|
83315
|
+
deps.warn?.(`cp: cron ${cron.cronId} collides with a hand-authored cron of the same id on agent ${cron.agentId} — not persisted`);
|
|
83316
|
+
return false;
|
|
83317
|
+
}
|
|
83318
|
+
const idx = list.findIndex((c) => at(c) && c.origin === "cp");
|
|
83319
|
+
if (idx >= 0) {
|
|
83320
|
+
if (JSON.stringify(list[idx]) === JSON.stringify(next)) return false;
|
|
83321
|
+
list[idx] = next;
|
|
83322
|
+
} else list.push(next);
|
|
83323
|
+
raw.crons = list;
|
|
83324
|
+
writeFileSync(file, JSON.stringify(raw, null, 2) + "\n");
|
|
83325
|
+
return true;
|
|
83326
|
+
}
|
|
83327
|
+
/**
|
|
83328
|
+
* Remove one CP cron (by id) from whichever agent.json holds it. Scans all agent
|
|
83329
|
+
* files since the owning agent is not part of `cron/remove`. Only `origin:"cp"`
|
|
83330
|
+
* entries are eligible — a hand-authored cron that happens to share the id is
|
|
83331
|
+
* never deleted. Returns whether an entry was removed.
|
|
83332
|
+
*/
|
|
83333
|
+
function removeCronDef(agentsDir, cronId) {
|
|
83334
|
+
for (const file of findAgentFiles(agentsDir)) {
|
|
83335
|
+
let raw;
|
|
83336
|
+
try {
|
|
83337
|
+
raw = JSON.parse(readFileSync(file, "utf8"));
|
|
83338
|
+
} catch {
|
|
83339
|
+
continue;
|
|
83340
|
+
}
|
|
83341
|
+
if (!Array.isArray(raw.crons)) continue;
|
|
83342
|
+
const list = raw.crons;
|
|
83343
|
+
const idx = list.findIndex((c) => typeof c === "object" && c !== null && c.id === cronId && c.origin === "cp");
|
|
83344
|
+
if (idx === -1) continue;
|
|
83345
|
+
list.splice(idx, 1);
|
|
83346
|
+
writeFileSync(file, JSON.stringify(raw, null, 2) + "\n");
|
|
83347
|
+
return true;
|
|
83348
|
+
}
|
|
83349
|
+
return false;
|
|
83350
|
+
}
|
|
83351
|
+
//#endregion
|
|
83352
|
+
//#region src/cp/cp-cron.ts
|
|
83353
|
+
var CpCronRegistry = class {
|
|
83354
|
+
agentsDir;
|
|
83355
|
+
deps;
|
|
83356
|
+
onChange;
|
|
83357
|
+
constructor(agentsDir, deps, onChange) {
|
|
83358
|
+
this.agentsDir = agentsDir;
|
|
83359
|
+
this.deps = deps;
|
|
83360
|
+
this.onChange = onChange;
|
|
83361
|
+
}
|
|
83362
|
+
/** Add or replace one CP cron on the owning agent's disk file (cron/upsert REQ). */
|
|
83363
|
+
upsert(cron) {
|
|
83364
|
+
if (writeCronDef(this.agentsDir, cron, this.deps)) this.onChange();
|
|
83365
|
+
}
|
|
83366
|
+
/** Splice one CP cron out of whichever agent.json holds it (cron/remove REQ). */
|
|
83367
|
+
remove(cronId) {
|
|
83368
|
+
if (removeCronDef(this.agentsDir, cronId)) this.onChange();
|
|
83369
|
+
}
|
|
83370
|
+
/**
|
|
83371
|
+
* Apply the register/ok snapshot: upsert EACH entry (identical re-applies skip
|
|
83372
|
+
* the write, so a reconnect with an unchanged set is a no-op). Stale ids
|
|
83373
|
+
* arrive separately via `drop.crons[]` → {@link remove}.
|
|
83374
|
+
*/
|
|
83375
|
+
converge(crons) {
|
|
83376
|
+
let changed = false;
|
|
83377
|
+
for (const cron of crons ?? []) if (writeCronDef(this.agentsDir, cron, this.deps)) changed = true;
|
|
83378
|
+
if (changed) this.onChange();
|
|
83379
|
+
}
|
|
83380
|
+
};
|
|
83381
|
+
//#endregion
|
|
83231
83382
|
//#region src/cp/cp-agent-registry.ts
|
|
83232
83383
|
var CpAgentRegistry = class {
|
|
83233
83384
|
agentsDir;
|
|
@@ -83575,6 +83726,7 @@ var Daemon = class Daemon {
|
|
|
83575
83726
|
warn: (m) => this.log.warn(m)
|
|
83576
83727
|
}, () => void this.reconcile().catch((err) => this.log.error(`cp: agent reconcile failed: ${err.stack ?? err}`)));
|
|
83577
83728
|
this.cpIntegrations = new CpIntegrationRegistry(this.agentsDir, { warn: (m) => this.log.warn(m) }, () => void this.reconcile().catch((err) => this.log.error(`cp: integration reconcile failed: ${err.stack ?? err}`)));
|
|
83729
|
+
this.cpCrons = new CpCronRegistry(this.agentsDir, { warn: (m) => this.log.warn(m) }, () => void this.reconcile().catch((err) => this.log.error(`cp: cron reconcile failed: ${err.stack ?? err}`)));
|
|
83578
83730
|
for (const a of this.effectiveAgents()) {
|
|
83579
83731
|
this.agents.set(a.id, a);
|
|
83580
83732
|
this.prefetchClone(a);
|
|
@@ -83622,8 +83774,9 @@ var Daemon = class Daemon {
|
|
|
83622
83774
|
fetchThreadHistory: (agentId, channel, threadTs) => this.fetchThreadHistory(agentId, channel, threadTs)
|
|
83623
83775
|
});
|
|
83624
83776
|
this.scheduler = new Scheduler({
|
|
83625
|
-
onFire: (agentId, msg) => void this.
|
|
83626
|
-
newTraceId: () => randomUUID()
|
|
83777
|
+
onFire: (agentId, msg, cron) => void this.onCronFire(agentId, msg, cron).catch((err) => this.log.error(`cron dispatch failed for agent "${agentId}": ${formatErr(err)}`)),
|
|
83778
|
+
newTraceId: () => randomUUID(),
|
|
83779
|
+
warn: (m) => this.log.warn(m)
|
|
83627
83780
|
});
|
|
83628
83781
|
const groups = consolidate(agents);
|
|
83629
83782
|
this.botUserIds = {};
|
|
@@ -83651,11 +83804,8 @@ var Daemon = class Daemon {
|
|
|
83651
83804
|
this.connections.push(conn);
|
|
83652
83805
|
this.refreshChannels(conn);
|
|
83653
83806
|
}
|
|
83654
|
-
|
|
83655
|
-
|
|
83656
|
-
this.scheduler.register(a.id, c);
|
|
83657
|
-
cronCount++;
|
|
83658
|
-
}
|
|
83807
|
+
for (const a of agents) this.scheduler.sync(a.id, a.crons);
|
|
83808
|
+
const cronCount = agents.reduce((n, a) => n + this.scheduler.count(a.id), 0);
|
|
83659
83809
|
if (cronCount) this.log.info(`registered ${cronCount} cron(s)`);
|
|
83660
83810
|
this.watcher = watch$1(this.agentsDir, {
|
|
83661
83811
|
ignoreInitial: true,
|
|
@@ -83740,10 +83890,12 @@ var Daemon = class Daemon {
|
|
|
83740
83890
|
}
|
|
83741
83891
|
this.hostStarts.delete(id);
|
|
83742
83892
|
this.agents.delete(id);
|
|
83893
|
+
this.scheduler.unregister(id);
|
|
83743
83894
|
}
|
|
83744
83895
|
for (const change of toChange) {
|
|
83745
83896
|
const a = change.agent;
|
|
83746
83897
|
this.agents.set(a.id, a);
|
|
83898
|
+
this.scheduler.sync(a.id, a.crons);
|
|
83747
83899
|
if (change.hostRespawn || change.workspace) {
|
|
83748
83900
|
const host = this.hosts.get(a.id);
|
|
83749
83901
|
if (host) {
|
|
@@ -83758,6 +83910,7 @@ var Daemon = class Daemon {
|
|
|
83758
83910
|
for (const a of toStart) {
|
|
83759
83911
|
this.agents.set(a.id, a);
|
|
83760
83912
|
this.prefetchClone(a);
|
|
83913
|
+
this.scheduler.sync(a.id, a.crons);
|
|
83761
83914
|
}
|
|
83762
83915
|
if (toStart.some((a) => a.integrations.some((i) => i.platform === "slack")) || toStop.length) await this.reconcileSlackConnections();
|
|
83763
83916
|
}
|
|
@@ -84069,7 +84222,7 @@ var Daemon = class Daemon {
|
|
|
84069
84222
|
}
|
|
84070
84223
|
const conv = new OutputConverger(this.agents.get(agentId).output.mode);
|
|
84071
84224
|
const rec = new TranscriptRecorder();
|
|
84072
|
-
const replyConn = this.replyConnFor(agentId, integrationId);
|
|
84225
|
+
const replyConn = msg.headless ? void 0 : this.replyConnFor(agentId, integrationId);
|
|
84073
84226
|
const wasRunning = this.hostStarts.has(agentId);
|
|
84074
84227
|
const statusThread = msg.thread ?? msg.msgId;
|
|
84075
84228
|
const key = sessionKey(msg.platform, msg.channel, statusThread, agentId);
|
|
@@ -84270,6 +84423,13 @@ var Daemon = class Daemon {
|
|
|
84270
84423
|
const intId = integrationId ?? this.agents.get(agentId)?.integrations[0]?.id;
|
|
84271
84424
|
return intId ? this.connByIntegration.get(intId) : void 0;
|
|
84272
84425
|
}
|
|
84426
|
+
/** The CP-owned cron ids currently on disk (register `localState.crons` — the
|
|
84427
|
+
* CP computes `drop.crons` against this; hand-authored crons are not reported). */
|
|
84428
|
+
cpCronIds() {
|
|
84429
|
+
const out = [];
|
|
84430
|
+
for (const a of this.effectiveAgents()) for (const c of a.crons) if (c.origin === "cp") out.push(c.id);
|
|
84431
|
+
return out;
|
|
84432
|
+
}
|
|
84273
84433
|
/** Stop and evict an ACP adapter child, returning the agent to `provisioned`
|
|
84274
84434
|
* (config kept; the next message lazily re-spawns it). Idempotent. The teardown
|
|
84275
84435
|
* is registered in `hostStopping` so a concurrent ensureHostAsync waits for it
|
|
@@ -84444,9 +84604,10 @@ var Daemon = class Daemon {
|
|
|
84444
84604
|
if (ignored.length) this.log.warn(`cp: ignored config keys: ${ignored.join(", ")}`);
|
|
84445
84605
|
},
|
|
84446
84606
|
applyReconcileSnapshot: (snap) => {
|
|
84447
|
-
this.cpCrons?.converge(snap.crons);
|
|
84448
84607
|
this.cpAgents?.converge(snap.agents);
|
|
84449
84608
|
this.cpIntegrations?.converge(snap.integrations);
|
|
84609
|
+
for (const id of snap.drop.crons) this.cpCrons?.remove(id);
|
|
84610
|
+
this.cpCrons?.converge(snap.crons);
|
|
84450
84611
|
this.cpRouting?.converge({
|
|
84451
84612
|
routingEpoch: snap.routingEpoch,
|
|
84452
84613
|
assignments: snap.assignments,
|
|
@@ -84486,26 +84647,31 @@ var Daemon = class Daemon {
|
|
|
84486
84647
|
applyDaemonUpgrade: (req) => this.scheduleFleetExit("upgrade", req.targetVersion)
|
|
84487
84648
|
};
|
|
84488
84649
|
}
|
|
84489
|
-
/**
|
|
84490
|
-
|
|
84491
|
-
|
|
84492
|
-
|
|
84493
|
-
|
|
84494
|
-
|
|
84495
|
-
|
|
84496
|
-
|
|
84497
|
-
|
|
84498
|
-
|
|
84499
|
-
|
|
84500
|
-
|
|
84501
|
-
|
|
84502
|
-
}
|
|
84503
|
-
|
|
84504
|
-
|
|
84505
|
-
|
|
84506
|
-
|
|
84507
|
-
|
|
84508
|
-
|
|
84650
|
+
/**
|
|
84651
|
+
* A cron fired for `agentId` (§8.6 bypass — the cron names its agent; no
|
|
84652
|
+
* routing). The fire is stamped into D11 first (the daemon owns last-run,
|
|
84653
|
+
* protocol §5.4). With a target channel, the trigger text is posted there as a
|
|
84654
|
+
* real ANCHOR message and the session threads its replies under it —
|
|
84655
|
+
* equivalent to a user posting the trigger in-channel. Without one (or when
|
|
84656
|
+
* the anchor can't be posted) the turn still runs; a `headless` fire
|
|
84657
|
+
* additionally suppresses all platform output in dispatch.
|
|
84658
|
+
*/
|
|
84659
|
+
async onCronFire(agentId, msg, cron) {
|
|
84660
|
+
this.store.setCronLastRun(`${agentId}:${cron.id}`, this.clock.now());
|
|
84661
|
+
if (cron.target?.channel) {
|
|
84662
|
+
const conn = this.replyConnFor(agentId);
|
|
84663
|
+
if (!conn) this.log.warn(`cron "${cron.id}": agent "${agentId}" has no live slack connection — running without output`);
|
|
84664
|
+
else try {
|
|
84665
|
+
const ts = await conn.postMessage(cron.target.channel, `⏰ ${cron.trigger}`);
|
|
84666
|
+
if (ts) msg = {
|
|
84667
|
+
...msg,
|
|
84668
|
+
thread: ts
|
|
84669
|
+
};
|
|
84670
|
+
} catch (err) {
|
|
84671
|
+
this.log.warn(`cron "${cron.id}": failed to post trigger to ${cron.target.channel} (${formatErr(err)}) — running without anchor`);
|
|
84672
|
+
}
|
|
84673
|
+
}
|
|
84674
|
+
await this.dispatch(agentId, msg);
|
|
84509
84675
|
}
|
|
84510
84676
|
/** Real CPU utilization (0..1) across all cores since the previous call — the busy
|
|
84511
84677
|
* fraction of the time delta. Called once per heartbeat, so it reflects load over the
|
|
@@ -84525,7 +84691,6 @@ var Daemon = class Daemon {
|
|
|
84525
84691
|
return;
|
|
84526
84692
|
}
|
|
84527
84693
|
const echoDaemonId = this.opts.overrides?.daemonId;
|
|
84528
|
-
this.cpCrons = new CpCronRegistry({ onFire: (cron) => this.onCpCronFire(cron) });
|
|
84529
84694
|
const url = cp.url;
|
|
84530
84695
|
this.cpClient = new CpClient({
|
|
84531
84696
|
url,
|
|
@@ -84556,7 +84721,7 @@ var Daemon = class Daemon {
|
|
|
84556
84721
|
},
|
|
84557
84722
|
localState: () => ({
|
|
84558
84723
|
assignments: [],
|
|
84559
|
-
crons: this.
|
|
84724
|
+
crons: this.cpCronIds(),
|
|
84560
84725
|
leases: []
|
|
84561
84726
|
}),
|
|
84562
84727
|
loadSnapshot: () => ({
|
|
@@ -84640,7 +84805,6 @@ var Daemon = class Daemon {
|
|
|
84640
84805
|
const errors = [];
|
|
84641
84806
|
this.scheduler?.stop();
|
|
84642
84807
|
await Promise.resolve(this.cpClient?.stop()).catch((e) => errors.push(e));
|
|
84643
|
-
this.cpCrons?.stop();
|
|
84644
84808
|
for (const c of this.connections) await Promise.resolve(c.stop()).catch((e) => errors.push(e));
|
|
84645
84809
|
for (const h of this.hosts.values()) await Promise.resolve(h.stop()).catch((e) => errors.push(e));
|
|
84646
84810
|
await Promise.resolve(this.mcp?.stop()).catch((e) => errors.push(e));
|