@agentconnect.md/daemon 1.0.0-rc.100 → 1.0.0-rc.102
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 +188 -72
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7670,7 +7670,8 @@ const CronDefSchema = object({
|
|
|
7670
7670
|
schedule: string(),
|
|
7671
7671
|
target: object({
|
|
7672
7672
|
platform: literal("slack"),
|
|
7673
|
-
channel: string()
|
|
7673
|
+
channel: string(),
|
|
7674
|
+
integrationId: string().optional()
|
|
7674
7675
|
}).optional(),
|
|
7675
7676
|
trigger: string(),
|
|
7676
7677
|
enabled: boolean().default(true),
|
|
@@ -12935,7 +12936,8 @@ const DrainDone = object({ released: array(SessionKey) });
|
|
|
12935
12936
|
*/
|
|
12936
12937
|
const CronTarget = object({
|
|
12937
12938
|
platform: _enum(["slack", "telegram"]).default("slack"),
|
|
12938
|
-
channel: string()
|
|
12939
|
+
channel: string(),
|
|
12940
|
+
integrationId: string().uuid().optional()
|
|
12939
12941
|
});
|
|
12940
12942
|
const CronUpsert = object({
|
|
12941
12943
|
cronId: string().uuid(),
|
|
@@ -12946,6 +12948,36 @@ const CronUpsert = object({
|
|
|
12946
12948
|
enabled: boolean().default(true)
|
|
12947
12949
|
});
|
|
12948
12950
|
const CronRemove = object({ cronId: string().uuid() });
|
|
12951
|
+
/**
|
|
12952
|
+
* `cron/report` (D→C EVT, fire-and-forget) — one CP-owned cron fired. The
|
|
12953
|
+
* daemon stamps the fire into its local store first (it stays authoritative,
|
|
12954
|
+
* §5.4) and reports it here so the console's `lastRunAt` converges; the CP
|
|
12955
|
+
* upsert is latest-wins, so the daemon re-asserting its stored stamps on
|
|
12956
|
+
* reconnect (fires while the CP was unreachable) is idempotent and can never
|
|
12957
|
+
* regress the value. Hand-authored (no-origin) crons are never reported.
|
|
12958
|
+
*
|
|
12959
|
+
* Two reports per fire, keyed by `(cronId, firedAt)`: the FIRE report (no
|
|
12960
|
+
* `status`) opens the run, and the COMPLETION report (with `status` + outcome
|
|
12961
|
+
* fields) closes it once the dispatched turn ends — feeding the console's
|
|
12962
|
+
* per-schedule run history. A completion without a prior fire report (CP was
|
|
12963
|
+
* down at fire time) still creates the run row.
|
|
12964
|
+
*/
|
|
12965
|
+
const CronRunStatus = _enum(["success", "failed"]);
|
|
12966
|
+
const CronReport = object({
|
|
12967
|
+
cronId: string().uuid(),
|
|
12968
|
+
agentId: string().uuid(),
|
|
12969
|
+
firedAt: string().datetime(),
|
|
12970
|
+
status: CronRunStatus.optional(),
|
|
12971
|
+
durationMs: number().int().nonnegative().optional(),
|
|
12972
|
+
sessionId: string().optional(),
|
|
12973
|
+
reason: string().optional()
|
|
12974
|
+
});
|
|
12975
|
+
/**
|
|
12976
|
+
* `cron/run` (C→D REQ → ack) — fire one CP-owned cron NOW (console "Run now").
|
|
12977
|
+
* The daemon accepts (`ok:true`) and runs the fire asynchronously — outcome
|
|
12978
|
+
* arrives as normal `cron/report`s; `ok:false` when it holds no such cron.
|
|
12979
|
+
*/
|
|
12980
|
+
const CronRunNow = object({ cronId: string().uuid() });
|
|
12949
12981
|
//#endregion
|
|
12950
12982
|
//#region ../protocol/dist/frames/secrets.js
|
|
12951
12983
|
/**
|
|
@@ -13546,6 +13578,8 @@ const FRAME_SCHEMAS = {
|
|
|
13546
13578
|
"drain/done": DrainDone,
|
|
13547
13579
|
"cron/upsert": CronUpsert,
|
|
13548
13580
|
"cron/remove": CronRemove,
|
|
13581
|
+
"cron/report": CronReport,
|
|
13582
|
+
"cron/run": CronRunNow,
|
|
13549
13583
|
"integration/upsert": IntegrationUpsert,
|
|
13550
13584
|
"integration/remove": IntegrationRemove,
|
|
13551
13585
|
"integration/channels": IntegrationChannels,
|
|
@@ -13651,6 +13685,8 @@ discriminatedUnion("type", [
|
|
|
13651
13685
|
frame("drain/done", FRAME_SCHEMAS["drain/done"]),
|
|
13652
13686
|
frame("cron/upsert", FRAME_SCHEMAS["cron/upsert"]),
|
|
13653
13687
|
frame("cron/remove", FRAME_SCHEMAS["cron/remove"]),
|
|
13688
|
+
frame("cron/report", FRAME_SCHEMAS["cron/report"]),
|
|
13689
|
+
frame("cron/run", FRAME_SCHEMAS["cron/run"]),
|
|
13654
13690
|
frame("integration/upsert", FRAME_SCHEMAS["integration/upsert"]),
|
|
13655
13691
|
frame("integration/remove", FRAME_SCHEMAS["integration/remove"]),
|
|
13656
13692
|
frame("integration/channels", FRAME_SCHEMAS["integration/channels"]),
|
|
@@ -24896,7 +24932,7 @@ const SLACK_TOOLS = [
|
|
|
24896
24932
|
inputSchema: obj({
|
|
24897
24933
|
text: {
|
|
24898
24934
|
type: "string",
|
|
24899
|
-
description: "Message body, in Slack mrkdwn."
|
|
24935
|
+
description: "Message body, in standard Markdown (CommonMark/GFM). Use `**bold**`, `~~strikethrough~~`, `# headings`, tables, etc. — Slack renders standard Markdown natively (do NOT use single-char Slack mrkdwn like `*bold*` or `~strike~`)."
|
|
24900
24936
|
},
|
|
24901
24937
|
channel: {
|
|
24902
24938
|
type: "string",
|
|
@@ -80973,7 +81009,7 @@ var import_dist = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((expor
|
|
|
80973
81009
|
}
|
|
80974
81010
|
});
|
|
80975
81011
|
})))(), 1);
|
|
80976
|
-
const MENTION_RE = /<@([A-Z0-9]+)>/g;
|
|
81012
|
+
const MENTION_RE$1 = /<@([A-Z0-9]+)>/g;
|
|
80977
81013
|
/** Map a Slack file element to a NormalizedMessage Attachment, dropping any
|
|
80978
81014
|
* without a usable id + fetch URL (e.g. external/tombstoned files). Tolerates a
|
|
80979
81015
|
* malformed (null / non-object) element off the wire without throwing. */
|
|
@@ -80991,7 +81027,7 @@ function toAttachment(f) {
|
|
|
80991
81027
|
}
|
|
80992
81028
|
function normalizeSlackEvent(event, ctx) {
|
|
80993
81029
|
const text = event.text ?? "";
|
|
80994
|
-
const mentionedBots = [...text.matchAll(MENTION_RE)].map((m) => m[1]);
|
|
81030
|
+
const mentionedBots = [...text.matchAll(MENTION_RE$1)].map((m) => m[1]);
|
|
80995
81031
|
const attachments = (event.files ?? []).map(toAttachment).filter((a) => a !== null);
|
|
80996
81032
|
return {
|
|
80997
81033
|
msgId: `slack:${event.channel}:${event.ts}`,
|
|
@@ -81098,6 +81134,21 @@ function consolidate(agents) {
|
|
|
81098
81134
|
}
|
|
81099
81135
|
/** Cap on members enriched per `listChannelMembers` call (bounds users.info fan-out). */
|
|
81100
81136
|
const MEMBER_ENRICH_CAP = 50;
|
|
81137
|
+
/**
|
|
81138
|
+
* Build a chat.postMessage/update payload that renders the body as a Block Kit
|
|
81139
|
+
* `markdown` block — Slack renders standard CommonMark there natively, so the agent's
|
|
81140
|
+
* markdown is sent verbatim (no markdown→mrkdwn conversion). `text` is kept as the
|
|
81141
|
+
* notification/accessibility fallback. Callers pre-chunk to SLACK_MARKDOWN_BLOCK_LIMIT.
|
|
81142
|
+
*/
|
|
81143
|
+
function markdownBlock(text) {
|
|
81144
|
+
return {
|
|
81145
|
+
text,
|
|
81146
|
+
blocks: [{
|
|
81147
|
+
type: "markdown",
|
|
81148
|
+
text
|
|
81149
|
+
}]
|
|
81150
|
+
};
|
|
81151
|
+
}
|
|
81101
81152
|
var SlackConnection = class {
|
|
81102
81153
|
deps;
|
|
81103
81154
|
app;
|
|
@@ -81170,8 +81221,8 @@ var SlackConnection = class {
|
|
|
81170
81221
|
return this.queue.enqueue(async () => {
|
|
81171
81222
|
return (await this.app.client.chat.postMessage({
|
|
81172
81223
|
channel,
|
|
81173
|
-
|
|
81174
|
-
|
|
81224
|
+
thread_ts: threadTs,
|
|
81225
|
+
...markdownBlock(text)
|
|
81175
81226
|
}))?.ts;
|
|
81176
81227
|
});
|
|
81177
81228
|
}
|
|
@@ -81183,7 +81234,7 @@ var SlackConnection = class {
|
|
|
81183
81234
|
await this.app.client.chat.update({
|
|
81184
81235
|
channel,
|
|
81185
81236
|
ts,
|
|
81186
|
-
text
|
|
81237
|
+
...markdownBlock(text)
|
|
81187
81238
|
});
|
|
81188
81239
|
} catch (err) {
|
|
81189
81240
|
this.deps.log?.debug(`slack: chat.update failed (ch=${channel} ts=${ts}): ${err.message}`);
|
|
@@ -81356,6 +81407,31 @@ var SlackConnection = class {
|
|
|
81356
81407
|
}
|
|
81357
81408
|
};
|
|
81358
81409
|
//#endregion
|
|
81410
|
+
//#region src/slack/mentions.ts
|
|
81411
|
+
/**
|
|
81412
|
+
* Slack `<@U…>` user-mention helpers, shared by the name-resolver (which warms the
|
|
81413
|
+
* display-name cache for mentioned users) and the session reader (which rewrites
|
|
81414
|
+
* mentions to `@name` for read-back). Slack encodes a user mention as `<@U123>` or
|
|
81415
|
+
* `<@U123|label>`; the id is a `U…`/`W…` user id (resolvable via users.info even for
|
|
81416
|
+
* a bot's user id, unlike a bot SENDER's `B…` id).
|
|
81417
|
+
*/
|
|
81418
|
+
const MENTION_RE = /<@([UW][A-Z0-9]+)(?:\|[^>]*)?>/g;
|
|
81419
|
+
/** Distinct `U…/W…` user ids mentioned in a message body (empty for none/nullish). */
|
|
81420
|
+
function mentionedUserIds(text) {
|
|
81421
|
+
if (!text) return [];
|
|
81422
|
+
const ids = /* @__PURE__ */ new Set();
|
|
81423
|
+
for (const m of text.matchAll(MENTION_RE)) ids.add(m[1]);
|
|
81424
|
+
return [...ids];
|
|
81425
|
+
}
|
|
81426
|
+
/** Rewrite `<@U123>` / `<@U123|x>` to `@<display name>` when the id is known; an
|
|
81427
|
+
* unknown id is left as the raw token so the web renderer's own fallback applies. */
|
|
81428
|
+
function substituteUserMentions(text, names) {
|
|
81429
|
+
return text.replace(MENTION_RE, (raw, id) => {
|
|
81430
|
+
const name = names.get(id);
|
|
81431
|
+
return name ? `@${name}` : raw;
|
|
81432
|
+
});
|
|
81433
|
+
}
|
|
81434
|
+
//#endregion
|
|
81359
81435
|
//#region src/slack/name-resolver.ts
|
|
81360
81436
|
/**
|
|
81361
81437
|
* Resolves Slack ids (channel "C…", user "U…/W…") to human display names off the
|
|
@@ -81385,10 +81461,14 @@ var SlackNameResolver = class {
|
|
|
81385
81461
|
this.log = log;
|
|
81386
81462
|
this.now = now;
|
|
81387
81463
|
}
|
|
81388
|
-
/** Kick off (cached) resolution for a message's channel
|
|
81464
|
+
/** Kick off (cached) resolution for a message's channel, human sender, and any
|
|
81465
|
+
* `<@U…>` user mentions in its body — so session read-back can render mentions
|
|
81466
|
+
* as `@name` (not the raw id). Mention ids use the `U…/W…` user id (resolvable
|
|
81467
|
+
* via users.info even for a bot's user id), unlike a bot SENDER's `B…` id. */
|
|
81389
81468
|
noteMessage(conn, msg) {
|
|
81390
81469
|
this.resolveChannel(conn, msg.channel);
|
|
81391
81470
|
if (!msg.sender.isBot && /^[UW]/.test(msg.sender.id)) this.resolveUser(conn, msg.sender.id);
|
|
81471
|
+
for (const id of mentionedUserIds(msg.text)) this.resolveUser(conn, id);
|
|
81392
81472
|
}
|
|
81393
81473
|
claim(id) {
|
|
81394
81474
|
const at = this.nextAttemptAt.get(id);
|
|
@@ -81426,61 +81506,22 @@ var SlackNameResolver = class {
|
|
|
81426
81506
|
//#endregion
|
|
81427
81507
|
//#region src/slack/formatter.ts
|
|
81428
81508
|
/**
|
|
81429
|
-
* Last-mile Slack formatting (§9.1 / §9.3).
|
|
81430
|
-
* -
|
|
81431
|
-
* - splitIntoSections: break a message into <= maxLen sections (Slack ~3000/section).
|
|
81432
|
-
* Both are platform-specific; the upstream convergence (ACP->IR) stays platform-agnostic.
|
|
81433
|
-
*/
|
|
81434
|
-
/** Slack section soft cap (§9.3: Slack splits at ~3000 chars per section). */
|
|
81435
|
-
const SLACK_SECTION_LIMIT = 3e3;
|
|
81436
|
-
const CODE_SENTINEL = "\0";
|
|
81437
|
-
const BOLD = "";
|
|
81438
|
-
/** Replace fenced ```code``` and inline `code` with placeholders so inline
|
|
81439
|
-
* formatting rules never fire inside them; returns the masked text + restorer. */
|
|
81440
|
-
function protectCode(input) {
|
|
81441
|
-
const spans = [];
|
|
81442
|
-
const masked = input.replace(/```[\s\S]*?```|`[^`\n]*`/g, (m) => {
|
|
81443
|
-
return `${CODE_SENTINEL}${spans.push(m) - 1}${CODE_SENTINEL}`;
|
|
81444
|
-
});
|
|
81445
|
-
const restore = (s) => s.replace(new RegExp(`${CODE_SENTINEL}(\\d+)${CODE_SENTINEL}`, "g"), (_, i) => spans[Number(i)] ?? "");
|
|
81446
|
-
return {
|
|
81447
|
-
masked,
|
|
81448
|
-
restore
|
|
81449
|
-
};
|
|
81450
|
-
}
|
|
81451
|
-
/**
|
|
81452
|
-
* Convert common Markdown to Slack mrkdwn. Handled: bold (`**`/`__` -> `*`),
|
|
81453
|
-
* italic (`*` -> `_`; bare `_` is already valid mrkdwn italic and left alone to
|
|
81454
|
-
* avoid mangling snake_case), strikethrough (`~~` -> `~`), links (`[t](u)` ->
|
|
81455
|
-
* `<u|t>`), ATX headings (`# H` -> `*H*`), and `-`/`*`/`+` bullets -> `•`. Code
|
|
81456
|
-
* spans and fences are preserved verbatim.
|
|
81509
|
+
* Last-mile Slack formatting (§9.1 / §9.3). One pure helper:
|
|
81510
|
+
* - splitIntoSections: break a message into <= maxLen sections.
|
|
81457
81511
|
*
|
|
81458
|
-
*
|
|
81459
|
-
*
|
|
81460
|
-
*
|
|
81512
|
+
* The daemon posts the agent's reply as a Block Kit `markdown` block (which renders
|
|
81513
|
+
* standard CommonMark natively), so NO markdown→mrkdwn conversion happens here — the
|
|
81514
|
+
* agent's text is sent verbatim. We only chunk it: a single `markdown` block caps at
|
|
81515
|
+
* 12000 chars, so a long body is split across blocks/messages on line boundaries.
|
|
81461
81516
|
*/
|
|
81462
|
-
|
|
81463
|
-
|
|
81464
|
-
const { masked, restore } = protectCode(input);
|
|
81465
|
-
let out = masked;
|
|
81466
|
-
out = out.replace(/^[ \t]*#{1,6}[ \t]+(.+?)[ \t]*#*$/gm, (_m, h) => `${BOLD}${h.replace(/\*+|__/g, "")}${BOLD}`);
|
|
81467
|
-
out = out.replace(/^([ \t]*)[-*+][ \t]+/gm, "$1• ");
|
|
81468
|
-
out = out.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, "<$2|$1>");
|
|
81469
|
-
out = out.replace(/\*\*\*(?!\s)([^\n]+?)(?<!\s)\*\*\*/g, `${BOLD}_$1_${BOLD}`);
|
|
81470
|
-
out = out.replace(/___(?!\s)([^\n]+?)(?<!\s)___/g, `${BOLD}_$1_${BOLD}`);
|
|
81471
|
-
out = out.replace(/\*\*(?!\s)([^\n]+?)(?<!\s)\*\*/g, `${BOLD}$1${BOLD}`);
|
|
81472
|
-
out = out.replace(/__(?!\s)([^\n]+?)(?<!\s)__/g, `${BOLD}$1${BOLD}`);
|
|
81473
|
-
out = out.replace(/\*(?!\s)([^*\n]+?)(?<!\s)\*/g, "_$1_");
|
|
81474
|
-
out = out.split(BOLD).join("*");
|
|
81475
|
-
out = out.replace(/~~([^\n]+?)~~/g, "~$1~");
|
|
81476
|
-
return restore(out);
|
|
81477
|
-
}
|
|
81517
|
+
/** Slack `markdown` block hard cap (12000 chars per block). */
|
|
81518
|
+
const SLACK_MARKDOWN_BLOCK_LIMIT = 12e3;
|
|
81478
81519
|
/**
|
|
81479
81520
|
* Split text into sections no longer than maxLen, preferring line boundaries;
|
|
81480
81521
|
* a single line longer than maxLen is hard-cut. Whitespace-only input yields no
|
|
81481
81522
|
* sections. Content is never lost across sections.
|
|
81482
81523
|
*/
|
|
81483
|
-
function splitIntoSections(text, maxLen =
|
|
81524
|
+
function splitIntoSections(text, maxLen = SLACK_MARKDOWN_BLOCK_LIMIT) {
|
|
81484
81525
|
if (!text.trim()) return [];
|
|
81485
81526
|
if (text.length <= maxLen) return [text];
|
|
81486
81527
|
const sections = [];
|
|
@@ -81520,9 +81561,10 @@ function planIcon(status) {
|
|
|
81520
81561
|
if (status === "in_progress") return ":hourglass_flowing_sand:";
|
|
81521
81562
|
return ":white_large_square:";
|
|
81522
81563
|
}
|
|
81523
|
-
/** Render an ACP plan (full entry list, resent on every update) as a compact summary.
|
|
81564
|
+
/** Render an ACP plan (full entry list, resent on every update) as a compact summary.
|
|
81565
|
+
* Posted as a `markdown` block, so emphasis is CommonMark (`**bold**`), not mrkdwn. */
|
|
81524
81566
|
function renderPlan(entries) {
|
|
81525
|
-
return [":clipboard:
|
|
81567
|
+
return [":clipboard: **Plan**", ...entries.map((e) => `${planIcon(e.status)} ${clampLabel(e.content ?? "")}`)].join("\n");
|
|
81526
81568
|
}
|
|
81527
81569
|
var OutputConverger = class {
|
|
81528
81570
|
mode;
|
|
@@ -81537,8 +81579,9 @@ var OutputConverger = class {
|
|
|
81537
81579
|
hasBuffered() {
|
|
81538
81580
|
return this.buf.trim().length > 0;
|
|
81539
81581
|
}
|
|
81540
|
-
/** Flush the buffered body:
|
|
81541
|
-
* one `post` per section (
|
|
81582
|
+
/** Flush the buffered body: split the agent's verbatim markdown into ≤block-limit
|
|
81583
|
+
* sections, one `post` per section (each posted as a Block Kit `markdown` block, so
|
|
81584
|
+
* no mrkdwn conversion). Public so the daemon's idle timer can call it. */
|
|
81542
81585
|
flushBuffered() {
|
|
81543
81586
|
return this.flush();
|
|
81544
81587
|
}
|
|
@@ -81546,7 +81589,7 @@ var OutputConverger = class {
|
|
|
81546
81589
|
const text = this.buf;
|
|
81547
81590
|
this.buf = "";
|
|
81548
81591
|
if (!text.trim()) return [];
|
|
81549
|
-
return splitIntoSections(
|
|
81592
|
+
return splitIntoSections(text).map((t) => ({
|
|
81550
81593
|
kind: "post",
|
|
81551
81594
|
text: t
|
|
81552
81595
|
}));
|
|
@@ -81637,7 +81680,7 @@ var OutputConverger = class {
|
|
|
81637
81680
|
if (this.mode === "low") return [...this.flush(), clear];
|
|
81638
81681
|
const footer = link ? [{
|
|
81639
81682
|
kind: "notice",
|
|
81640
|
-
text: `:white_check_mark: done —
|
|
81683
|
+
text: `:white_check_mark: done — [details](${link})`
|
|
81641
81684
|
}] : [];
|
|
81642
81685
|
return [
|
|
81643
81686
|
...this.flush(),
|
|
@@ -83001,6 +83044,17 @@ var CpClient = class {
|
|
|
83001
83044
|
if (this.state !== "READY" && this.state !== "DRAINING") return;
|
|
83002
83045
|
this.transport?.send(encode(buildEnvelope("integration/channels", snapshot)));
|
|
83003
83046
|
}
|
|
83047
|
+
/**
|
|
83048
|
+
* Report a CP-owned cron fire (D→C `cron/report` EVT, fire-and-forget) so the
|
|
83049
|
+
* console's `lastRunAt` converges. The local D11 stamp stays authoritative and
|
|
83050
|
+
* the CP upsert is latest-wins, so re-asserting stored stamps on each
|
|
83051
|
+
* (re)connect (see onReady in daemon.ts) makes a dropped report only a delay,
|
|
83052
|
+
* never a loss. No-op unless READY/DRAINING.
|
|
83053
|
+
*/
|
|
83054
|
+
emitCronReport(report) {
|
|
83055
|
+
if (this.state !== "READY" && this.state !== "DRAINING") return;
|
|
83056
|
+
this.transport?.send(encode(buildEnvelope("cron/report", report)));
|
|
83057
|
+
}
|
|
83004
83058
|
request(type, payload) {
|
|
83005
83059
|
const frame = buildEnvelope(type, payload);
|
|
83006
83060
|
return this.correlator.request(frame, (e) => this.transport.send(e));
|
|
@@ -83087,6 +83141,9 @@ var CpClient = class {
|
|
|
83087
83141
|
this.deps.configApply.removeCron(frame.payload.cronId);
|
|
83088
83142
|
this.reply(frame, "ack", { ok: true });
|
|
83089
83143
|
return;
|
|
83144
|
+
case "cron/run":
|
|
83145
|
+
this.reply(frame, "ack", this.deps.configApply.runCron(frame.payload.cronId));
|
|
83146
|
+
return;
|
|
83090
83147
|
case "route/assign": {
|
|
83091
83148
|
const a = frame.payload;
|
|
83092
83149
|
this.deps.configApply.applyRouteAssign(a);
|
|
@@ -83293,11 +83350,16 @@ function createSessionReader(store) {
|
|
|
83293
83350
|
return {
|
|
83294
83351
|
list(req) {
|
|
83295
83352
|
const rows = store.listSessions(req.agentId);
|
|
83296
|
-
const names = store.getDisplayNames(rows.flatMap((r) => [
|
|
83353
|
+
const names = store.getDisplayNames(rows.flatMap((r) => [
|
|
83354
|
+
r.channel,
|
|
83355
|
+
...r.triggeredBy ? [r.triggeredBy] : [],
|
|
83356
|
+
...mentionedUserIds(r.title)
|
|
83357
|
+
]));
|
|
83297
83358
|
return { sessions: rows.map((r) => {
|
|
83298
83359
|
const usage = parseUsage(r.usage);
|
|
83299
83360
|
const channelName = names.get(r.channel);
|
|
83300
83361
|
const triggeredByName = r.triggeredBy ? names.get(r.triggeredBy) : void 0;
|
|
83362
|
+
const title = r.title ? substituteUserMentions(r.title, names) : void 0;
|
|
83301
83363
|
return {
|
|
83302
83364
|
sessionId: r.acpSessionId,
|
|
83303
83365
|
sessionKey: {
|
|
@@ -83309,7 +83371,7 @@ function createSessionReader(store) {
|
|
|
83309
83371
|
status: r.state,
|
|
83310
83372
|
lastActivityAt: new Date(r.updatedAt).toISOString(),
|
|
83311
83373
|
...usage ? { usage } : {},
|
|
83312
|
-
...
|
|
83374
|
+
...title ? { title } : {},
|
|
83313
83375
|
...r.triggeredBy ? { triggeredBy: r.triggeredBy } : {},
|
|
83314
83376
|
...channelName ? { channelName } : {},
|
|
83315
83377
|
...triggeredByName ? { triggeredByName } : {}
|
|
@@ -83325,7 +83387,7 @@ function createSessionReader(store) {
|
|
|
83325
83387
|
const before = req.cursor != null && Number.isFinite(Number(req.cursor)) ? Number(req.cursor) : null;
|
|
83326
83388
|
const { rows, hasMore } = store.transcriptPage(rec.channel, rec.thread, before, req.limit);
|
|
83327
83389
|
const ordered = rows.slice().reverse();
|
|
83328
|
-
const names = store.getDisplayNames(ordered.
|
|
83390
|
+
const names = store.getDisplayNames(ordered.flatMap((r) => [r.sender, ...mentionedUserIds(r.text)]));
|
|
83329
83391
|
const built = ordered.map((r) => {
|
|
83330
83392
|
const senderName = names.get(r.sender);
|
|
83331
83393
|
const base = {
|
|
@@ -83334,7 +83396,7 @@ function createSessionReader(store) {
|
|
|
83334
83396
|
...senderName ? { senderName } : {},
|
|
83335
83397
|
ts: r.ts,
|
|
83336
83398
|
kind: r.kind,
|
|
83337
|
-
text: r.text
|
|
83399
|
+
text: substituteUserMentions(r.text, names)
|
|
83338
83400
|
};
|
|
83339
83401
|
if (r.kind !== "tool" || !r.body) return base;
|
|
83340
83402
|
let full;
|
|
@@ -83707,7 +83769,8 @@ function toCronDef(cron) {
|
|
|
83707
83769
|
schedule: cron.schedule,
|
|
83708
83770
|
...cron.target && cron.target.platform === "slack" ? { target: {
|
|
83709
83771
|
platform: "slack",
|
|
83710
|
-
channel: cron.target.channel
|
|
83772
|
+
channel: cron.target.channel,
|
|
83773
|
+
...cron.target.integrationId ? { integrationId: cron.target.integrationId } : {}
|
|
83711
83774
|
} } : {},
|
|
83712
83775
|
trigger: cron.trigger,
|
|
83713
83776
|
enabled: cron.enabled !== false,
|
|
@@ -84651,7 +84714,7 @@ var Daemon = class Daemon {
|
|
|
84651
84714
|
async dispatch(agentId, msg, integrationId) {
|
|
84652
84715
|
if (this.draining || this.drainingAgents.has(agentId)) {
|
|
84653
84716
|
this.log.debug(`dispatch: skipped for agent "${agentId}" (draining)`);
|
|
84654
|
-
return;
|
|
84717
|
+
return null;
|
|
84655
84718
|
}
|
|
84656
84719
|
const conv = new OutputConverger(this.agents.get(agentId).output.mode);
|
|
84657
84720
|
const rec = new TranscriptRecorder();
|
|
@@ -84723,6 +84786,7 @@ var Daemon = class Daemon {
|
|
|
84723
84786
|
p.resolveDone();
|
|
84724
84787
|
}
|
|
84725
84788
|
this.flushQueued(agentId, sessionId, integrationId);
|
|
84789
|
+
return sessionId;
|
|
84726
84790
|
}
|
|
84727
84791
|
/** After `!stop` sends session/cancel, give the agent `cancelBackstopMs` to yield.
|
|
84728
84792
|
* If the turn is still in flight when the timer fires, the agent ignored the
|
|
@@ -85074,6 +85138,7 @@ var Daemon = class Daemon {
|
|
|
85074
85138
|
applyIntegrationRemove: (integrationId) => this.cpIntegrations?.remove(integrationId),
|
|
85075
85139
|
upsertCron: (cron) => this.cpCrons.upsert(cron),
|
|
85076
85140
|
removeCron: (cronId) => this.cpCrons.remove(cronId),
|
|
85141
|
+
runCron: (cronId) => this.runCronNow(cronId),
|
|
85077
85142
|
applyRouteAssign: (a) => this.cpRouting?.upsertAssign(a),
|
|
85078
85143
|
applyRouteUpdate: (u) => this.cpRouting?.applyUpdate(u),
|
|
85079
85144
|
applyAgentLaunch: async (launch) => {
|
|
@@ -85107,9 +85172,15 @@ var Daemon = class Daemon {
|
|
|
85107
85172
|
* additionally suppresses all platform output in dispatch.
|
|
85108
85173
|
*/
|
|
85109
85174
|
async onCronFire(agentId, msg, cron) {
|
|
85110
|
-
|
|
85175
|
+
const firedAt = this.clock.now();
|
|
85176
|
+
this.store.setCronLastRun(`${agentId}:${cron.id}`, firedAt);
|
|
85177
|
+
if (cron.origin === "cp") this.cpClient?.emitCronReport({
|
|
85178
|
+
cronId: cron.id,
|
|
85179
|
+
agentId,
|
|
85180
|
+
firedAt: new Date(firedAt).toISOString()
|
|
85181
|
+
});
|
|
85111
85182
|
if (cron.target?.channel) {
|
|
85112
|
-
const conn = this.replyConnFor(agentId);
|
|
85183
|
+
const conn = this.replyConnFor(agentId, cron.target.integrationId);
|
|
85113
85184
|
if (!conn) this.log.warn(`cron "${cron.id}": agent "${agentId}" has no live slack connection — running without output`);
|
|
85114
85185
|
else try {
|
|
85115
85186
|
const ts = await conn.postMessage(cron.target.channel, `⏰ ${cron.trigger}`);
|
|
@@ -85121,7 +85192,43 @@ var Daemon = class Daemon {
|
|
|
85121
85192
|
this.log.warn(`cron "${cron.id}": failed to post trigger to ${cron.target.channel} (${formatErr(err)}) — running without anchor`);
|
|
85122
85193
|
}
|
|
85123
85194
|
}
|
|
85124
|
-
|
|
85195
|
+
try {
|
|
85196
|
+
const sessionId = await this.dispatch(agentId, msg, cron.target?.integrationId);
|
|
85197
|
+
if (cron.origin === "cp" && sessionId !== null) this.cpClient?.emitCronReport({
|
|
85198
|
+
cronId: cron.id,
|
|
85199
|
+
agentId,
|
|
85200
|
+
firedAt: new Date(firedAt).toISOString(),
|
|
85201
|
+
status: "success",
|
|
85202
|
+
durationMs: Math.max(0, this.clock.now() - firedAt),
|
|
85203
|
+
sessionId
|
|
85204
|
+
});
|
|
85205
|
+
} catch (err) {
|
|
85206
|
+
if (cron.origin === "cp") this.cpClient?.emitCronReport({
|
|
85207
|
+
cronId: cron.id,
|
|
85208
|
+
agentId,
|
|
85209
|
+
firedAt: new Date(firedAt).toISOString(),
|
|
85210
|
+
status: "failed",
|
|
85211
|
+
durationMs: Math.max(0, this.clock.now() - firedAt),
|
|
85212
|
+
reason: err.message?.slice(0, 300) || "dispatch failed"
|
|
85213
|
+
});
|
|
85214
|
+
throw err;
|
|
85215
|
+
}
|
|
85216
|
+
}
|
|
85217
|
+
/** Console "Run now" (`cron/run` REQ): fire one CP cron immediately. The fire
|
|
85218
|
+
* runs asynchronously — the ack only says whether the cron exists here; the
|
|
85219
|
+
* outcome arrives as normal `cron/report`s. */
|
|
85220
|
+
runCronNow(cronId) {
|
|
85221
|
+
for (const a of this.effectiveAgents()) {
|
|
85222
|
+
const cron = a.crons.find((c) => c.id === cronId && c.origin === "cp");
|
|
85223
|
+
if (!cron) continue;
|
|
85224
|
+
const { msg } = buildSyntheticMessage(a.id, cron, randomUUID());
|
|
85225
|
+
this.onCronFire(a.id, msg, cron).catch((err) => this.log.error(`cron/run dispatch failed for agent "${a.id}": ${formatErr(err)}`));
|
|
85226
|
+
return { ok: true };
|
|
85227
|
+
}
|
|
85228
|
+
return {
|
|
85229
|
+
ok: false,
|
|
85230
|
+
reason: "unknown cron"
|
|
85231
|
+
};
|
|
85125
85232
|
}
|
|
85126
85233
|
/** Real CPU utilization (0..1) across all cores since the previous call — the busy
|
|
85127
85234
|
* fraction of the time delta. Called once per heartbeat, so it reflects load over the
|
|
@@ -85168,6 +85275,15 @@ var Daemon = class Daemon {
|
|
|
85168
85275
|
integrationId,
|
|
85169
85276
|
channels
|
|
85170
85277
|
});
|
|
85278
|
+
for (const a of this.effectiveAgents()) for (const c of a.crons) {
|
|
85279
|
+
if (c.origin !== "cp") continue;
|
|
85280
|
+
const at = this.store.getCronLastRun(`${a.id}:${c.id}`);
|
|
85281
|
+
if (at !== void 0) this.cpClient?.emitCronReport({
|
|
85282
|
+
cronId: c.id,
|
|
85283
|
+
agentId: a.id,
|
|
85284
|
+
firedAt: new Date(at).toISOString()
|
|
85285
|
+
});
|
|
85286
|
+
}
|
|
85171
85287
|
},
|
|
85172
85288
|
localState: () => ({
|
|
85173
85289
|
assignments: [],
|