@integrity-labs/agt-cli 0.27.7-test.6 → 0.27.8-test.8
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/bin/agt.js +131 -37
- package/dist/bin/agt.js.map +1 -1
- package/dist/{chunk-AACMX6LE.js → chunk-DHF75YYA.js} +185 -60
- package/dist/chunk-DHF75YYA.js.map +1 -0
- package/dist/{chunk-GN4XPQWJ.js → chunk-F4NG4EXD.js} +37 -28
- package/dist/chunk-F4NG4EXD.js.map +1 -0
- package/dist/{chunk-YSBGIXJG.js → chunk-HT6EETEL.js} +1 -1
- package/dist/{claude-pair-runtime-ZBQKBBMT.js → claude-pair-runtime-OBAJZDXK.js} +2 -2
- package/dist/lib/manager-worker.js +14 -10
- package/dist/lib/manager-worker.js.map +1 -1
- package/dist/mcp/direct-chat-channel.js +9 -462
- package/dist/mcp/index.js +44 -25
- package/dist/mcp/slack-channel.js +315 -604
- package/dist/mcp/telegram-channel.js +251 -513
- package/dist/{persistent-session-ICYFLUAM.js → persistent-session-SBSOZG74.js} +3 -3
- package/dist/{responsiveness-probe-WZNQ2762.js → responsiveness-probe-DU4IJ2RZ.js} +3 -3
- package/package.json +1 -1
- package/dist/chunk-AACMX6LE.js.map +0 -1
- package/dist/chunk-GN4XPQWJ.js.map +0 -1
- /package/dist/{chunk-YSBGIXJG.js.map → chunk-HT6EETEL.js.map} +0 -0
- /package/dist/{claude-pair-runtime-ZBQKBBMT.js.map → claude-pair-runtime-OBAJZDXK.js.map} +0 -0
- /package/dist/{persistent-session-ICYFLUAM.js.map → persistent-session-SBSOZG74.js.map} +0 -0
- /package/dist/{responsiveness-probe-WZNQ2762.js.map → responsiveness-probe-DU4IJ2RZ.js.map} +0 -0
|
@@ -14249,6 +14249,105 @@ function decideSenderPolicyForward(evt, policy) {
|
|
|
14249
14249
|
return { forward: true };
|
|
14250
14250
|
}
|
|
14251
14251
|
|
|
14252
|
+
// src/ack-reaction.ts
|
|
14253
|
+
import { readdirSync, readFileSync } from "fs";
|
|
14254
|
+
import { join } from "path";
|
|
14255
|
+
var REPLY_WEDGED_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
14256
|
+
var ACK_STARTUP_GRACE_MS = 6e4;
|
|
14257
|
+
function decideAckReaction(i) {
|
|
14258
|
+
if (!i.hasTarget) return "none";
|
|
14259
|
+
if (!i.integrationReady) return "undeliverable";
|
|
14260
|
+
if (i.tmux === "dead") return "undeliverable";
|
|
14261
|
+
if (!i.withinStartupGrace && i.claude === "dead") return "undeliverable";
|
|
14262
|
+
const threshold = i.pendingStaleThresholdMs ?? REPLY_WEDGED_THRESHOLD_MS;
|
|
14263
|
+
if (i.oldestPendingAgeMs != null && i.oldestPendingAgeMs > threshold) {
|
|
14264
|
+
return "undeliverable";
|
|
14265
|
+
}
|
|
14266
|
+
return "ack";
|
|
14267
|
+
}
|
|
14268
|
+
function decideRecoveryHeal(i) {
|
|
14269
|
+
return i.wasUndeliverable && i.hasTarget ? "heal" : "none";
|
|
14270
|
+
}
|
|
14271
|
+
function oldestPendingMarkerAgeMs(dir, now = Date.now()) {
|
|
14272
|
+
if (!dir) return null;
|
|
14273
|
+
let names;
|
|
14274
|
+
try {
|
|
14275
|
+
names = readdirSync(dir);
|
|
14276
|
+
} catch {
|
|
14277
|
+
return null;
|
|
14278
|
+
}
|
|
14279
|
+
let oldest = null;
|
|
14280
|
+
for (const name of names) {
|
|
14281
|
+
if (!name.endsWith(".json")) continue;
|
|
14282
|
+
let receivedAt;
|
|
14283
|
+
try {
|
|
14284
|
+
const raw = JSON.parse(readFileSync(join(dir, name), "utf-8"));
|
|
14285
|
+
receivedAt = raw.received_at;
|
|
14286
|
+
} catch {
|
|
14287
|
+
continue;
|
|
14288
|
+
}
|
|
14289
|
+
if (!receivedAt) continue;
|
|
14290
|
+
const t = Date.parse(receivedAt);
|
|
14291
|
+
if (Number.isNaN(t)) continue;
|
|
14292
|
+
const age = now - t;
|
|
14293
|
+
if (age < 0) continue;
|
|
14294
|
+
if (oldest == null || age > oldest) oldest = age;
|
|
14295
|
+
}
|
|
14296
|
+
return oldest;
|
|
14297
|
+
}
|
|
14298
|
+
|
|
14299
|
+
// src/session-probe-runtime.ts
|
|
14300
|
+
import { execFileSync } from "child_process";
|
|
14301
|
+
function agentTmuxSessionName(codeName) {
|
|
14302
|
+
return `agt-${codeName}`;
|
|
14303
|
+
}
|
|
14304
|
+
function escapePgrepRegex(value) {
|
|
14305
|
+
return value.replace(/[.[\]{}()*+?^$|\\]/g, "\\$&");
|
|
14306
|
+
}
|
|
14307
|
+
function probeClaudeProcessInTmux(tmuxSession) {
|
|
14308
|
+
const escapedSession = escapePgrepRegex(tmuxSession);
|
|
14309
|
+
const pattern = `(^|[[:space:]])--name ${escapedSession}([[:space:]]|$)`;
|
|
14310
|
+
try {
|
|
14311
|
+
const out = execFileSync("pgrep", ["-f", "--", pattern], {
|
|
14312
|
+
encoding: "utf-8",
|
|
14313
|
+
timeout: 3e3
|
|
14314
|
+
}).trim();
|
|
14315
|
+
return out.length > 0 ? "alive" : "dead";
|
|
14316
|
+
} catch (err) {
|
|
14317
|
+
const e = err;
|
|
14318
|
+
if (e?.code === "ENOENT") return "unknown";
|
|
14319
|
+
return e?.status === 1 ? "dead" : "unknown";
|
|
14320
|
+
}
|
|
14321
|
+
}
|
|
14322
|
+
function probeTmuxSession(tmuxSession) {
|
|
14323
|
+
try {
|
|
14324
|
+
execFileSync("tmux", ["has-session", "-t", tmuxSession], {
|
|
14325
|
+
stdio: "ignore",
|
|
14326
|
+
timeout: 3e3
|
|
14327
|
+
});
|
|
14328
|
+
return "alive";
|
|
14329
|
+
} catch (err) {
|
|
14330
|
+
const e = err;
|
|
14331
|
+
if (e?.code === "ENOENT") return "unknown";
|
|
14332
|
+
return "dead";
|
|
14333
|
+
}
|
|
14334
|
+
}
|
|
14335
|
+
function probeAgentSession(codeName) {
|
|
14336
|
+
const session = agentTmuxSessionName(codeName);
|
|
14337
|
+
const tmux = probeTmuxSession(session);
|
|
14338
|
+
const claude = tmux === "alive" ? probeClaudeProcessInTmux(session) : tmux;
|
|
14339
|
+
return { tmux, claude };
|
|
14340
|
+
}
|
|
14341
|
+
var probeCache = /* @__PURE__ */ new Map();
|
|
14342
|
+
var SESSION_PROBE_TTL_MS = 15e3;
|
|
14343
|
+
function probeAgentSessionCached(codeName, ttlMs = SESSION_PROBE_TTL_MS, now = Date.now()) {
|
|
14344
|
+
const cached2 = probeCache.get(codeName);
|
|
14345
|
+
if (cached2 && now - cached2.at < ttlMs) return cached2.value;
|
|
14346
|
+
const value = probeAgentSession(codeName);
|
|
14347
|
+
probeCache.set(codeName, { at: now, value });
|
|
14348
|
+
return value;
|
|
14349
|
+
}
|
|
14350
|
+
|
|
14252
14351
|
// src/slack-loop-throttle.ts
|
|
14253
14352
|
var DEFAULT_THROTTLE = {
|
|
14254
14353
|
threshold: 3,
|
|
@@ -14351,466 +14450,62 @@ async function isThreadKilled(opts) {
|
|
|
14351
14450
|
}
|
|
14352
14451
|
}
|
|
14353
14452
|
|
|
14354
|
-
// src/slack-
|
|
14355
|
-
var
|
|
14356
|
-
|
|
14357
|
-
|
|
14358
|
-
|
|
14359
|
-
}
|
|
14360
|
-
|
|
14361
|
-
|
|
14362
|
-
|
|
14363
|
-
|
|
14364
|
-
|
|
14365
|
-
const
|
|
14366
|
-
|
|
14367
|
-
|
|
14368
|
-
|
|
14369
|
-
|
|
14370
|
-
|
|
14371
|
-
|
|
14372
|
-
|
|
14373
|
-
const
|
|
14374
|
-
|
|
14375
|
-
|
|
14376
|
-
|
|
14377
|
-
|
|
14378
|
-
|
|
14379
|
-
|
|
14380
|
-
|
|
14381
|
-
|
|
14382
|
-
}
|
|
14383
|
-
|
|
14384
|
-
|
|
14385
|
-
return state.terminal.message ? state.terminal.message : void 0;
|
|
14386
|
-
}
|
|
14387
|
-
const parts = [];
|
|
14388
|
-
if (state.stepNumber) {
|
|
14389
|
-
if (state.stepNumber.total != null) {
|
|
14390
|
-
parts.push(`*Step ${state.stepNumber.current} of ${state.stepNumber.total}*`);
|
|
14391
|
-
} else {
|
|
14392
|
-
parts.push(`*Step ${state.stepNumber.current}*`);
|
|
14393
|
-
}
|
|
14394
|
-
}
|
|
14395
|
-
if (state.stepLabel) parts.push(state.stepLabel);
|
|
14396
|
-
return parts.length > 0 ? parts.join(": ") : void 0;
|
|
14397
|
-
}
|
|
14398
|
-
function renderProgressBlocks(state) {
|
|
14399
|
-
const header = renderHeaderLine(state);
|
|
14400
|
-
const step = renderStepLine(state);
|
|
14401
|
-
const footer = state.terminal ? `_${state.terminal.kind === "completed" ? "completed" : "failed"}: ${formatWallClock(state.lastChangeAt)}_` : `_last update: ${formatWallClock(state.lastChangeAt)}_`;
|
|
14402
|
-
const lines = [header];
|
|
14403
|
-
if (step) lines.push(step);
|
|
14404
|
-
if (state.detail) lines.push(`_${state.detail}_`);
|
|
14405
|
-
lines.push(footer);
|
|
14406
|
-
const fallbackText = state.terminal ? `${state.terminal.kind === "completed" ? "Done" : "Failed"} \u2014 ${state.initialLabel}` : `Working on: ${state.initialLabel}`;
|
|
14407
|
-
return {
|
|
14408
|
-
text: fallbackText,
|
|
14409
|
-
blocks: [
|
|
14410
|
-
{
|
|
14411
|
-
type: "section",
|
|
14412
|
-
text: { type: "mrkdwn", text: lines.join("\n") }
|
|
14413
|
-
}
|
|
14414
|
-
]
|
|
14415
|
-
};
|
|
14416
|
-
}
|
|
14417
|
-
var SLACK_POST_URL = "https://slack.com/api/chat.postMessage";
|
|
14418
|
-
var SLACK_UPDATE_URL = "https://slack.com/api/chat.update";
|
|
14419
|
-
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
14420
|
-
var SlackApiError = class extends Error {
|
|
14421
|
-
constructor(endpoint, slackError, status) {
|
|
14422
|
-
super(`Slack ${endpoint} failed: ${slackError} (status=${status})`);
|
|
14423
|
-
this.endpoint = endpoint;
|
|
14424
|
-
this.slackError = slackError;
|
|
14425
|
-
this.status = status;
|
|
14426
|
-
this.name = "SlackApiError";
|
|
14427
|
-
}
|
|
14428
|
-
};
|
|
14429
|
-
function createSlackProgressFlush(opts) {
|
|
14430
|
-
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
14431
|
-
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
14432
|
-
let anchorTs = null;
|
|
14433
|
-
async function slackCall(endpoint, body) {
|
|
14434
|
-
const url = endpoint === "chat.postMessage" ? SLACK_POST_URL : SLACK_UPDATE_URL;
|
|
14435
|
-
const controller = new AbortController();
|
|
14436
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
14437
|
-
let response;
|
|
14438
|
-
try {
|
|
14439
|
-
response = await fetchImpl(url, {
|
|
14440
|
-
method: "POST",
|
|
14441
|
-
headers: {
|
|
14442
|
-
"Content-Type": "application/json",
|
|
14443
|
-
Authorization: `Bearer ${opts.botToken}`
|
|
14444
|
-
},
|
|
14445
|
-
body: JSON.stringify(body),
|
|
14446
|
-
signal: controller.signal
|
|
14447
|
-
});
|
|
14448
|
-
} finally {
|
|
14449
|
-
clearTimeout(timer);
|
|
14450
|
-
}
|
|
14451
|
-
const parsed = await response.json().catch(() => ({ ok: false, error: "invalid_json" }));
|
|
14452
|
-
if (!parsed.ok) {
|
|
14453
|
-
throw new SlackApiError(endpoint, parsed.error ?? "unknown", response.status);
|
|
14454
|
-
}
|
|
14455
|
-
return parsed;
|
|
14456
|
-
}
|
|
14457
|
-
return async function flush(state) {
|
|
14458
|
-
const payload = renderProgressBlocks(state);
|
|
14459
|
-
if (anchorTs == null) {
|
|
14460
|
-
const body = {
|
|
14461
|
-
channel: opts.channel,
|
|
14462
|
-
text: payload.text,
|
|
14463
|
-
blocks: payload.blocks
|
|
14464
|
-
};
|
|
14465
|
-
if (opts.threadTs) body.thread_ts = opts.threadTs;
|
|
14466
|
-
try {
|
|
14467
|
-
const res = await slackCall("chat.postMessage", body);
|
|
14468
|
-
if (!res.ts) {
|
|
14469
|
-
throw new SlackApiError(
|
|
14470
|
-
"chat.postMessage",
|
|
14471
|
-
"missing ts in successful response",
|
|
14472
|
-
200
|
|
14473
|
-
);
|
|
14474
|
-
}
|
|
14475
|
-
anchorTs = res.ts;
|
|
14476
|
-
opts.onAnchorPosted?.(res.ts);
|
|
14477
|
-
} catch (err) {
|
|
14478
|
-
throw err;
|
|
14479
|
-
}
|
|
14480
|
-
return;
|
|
14481
|
-
}
|
|
14453
|
+
// src/slack-thread-context.ts
|
|
14454
|
+
var SLACK_AUTOLOAD_THREAD_LIMIT = 30;
|
|
14455
|
+
var SLACK_AUTOLOAD_THREAD_MAX_CHARS = 4e3;
|
|
14456
|
+
function formatThreadMessages(messages, nameById) {
|
|
14457
|
+
return messages.map((m) => `[${m.ts}] ${nameById.get(m.user) ?? m.user} (<@${m.user}>): ${m.text}`).join("\n");
|
|
14458
|
+
}
|
|
14459
|
+
function capThreadContext(formatted, maxChars) {
|
|
14460
|
+
if (formatted.length <= maxChars) return formatted;
|
|
14461
|
+
const marker = "[\u2026earlier thread messages omitted \u2014 slack.read_thread for full history\u2026]\n";
|
|
14462
|
+
if (maxChars <= marker.length) return marker.slice(0, maxChars);
|
|
14463
|
+
const tailBudget = maxChars - marker.length;
|
|
14464
|
+
const tail = formatted.slice(formatted.length - tailBudget);
|
|
14465
|
+
const firstNewline = tail.indexOf("\n");
|
|
14466
|
+
const clean = firstNewline >= 0 ? tail.slice(firstNewline + 1) : tail;
|
|
14467
|
+
return `${marker}${clean}`;
|
|
14468
|
+
}
|
|
14469
|
+
async function fetchThreadTranscript(channel, threadTs, limit, deps) {
|
|
14470
|
+
const { botToken, resolveUserName: resolveUserName2, fetchImpl = fetch } = deps;
|
|
14471
|
+
const cappedLimit = Math.min(Math.max(limit, 1), 200);
|
|
14472
|
+
const allMessages = [];
|
|
14473
|
+
let cursor;
|
|
14474
|
+
do {
|
|
14475
|
+
const remaining = cappedLimit - allMessages.length;
|
|
14476
|
+
if (remaining <= 0) break;
|
|
14477
|
+
const params = new URLSearchParams({
|
|
14478
|
+
channel,
|
|
14479
|
+
ts: threadTs,
|
|
14480
|
+
limit: String(Math.min(remaining, 100)),
|
|
14481
|
+
...cursor ? { cursor } : {}
|
|
14482
|
+
});
|
|
14483
|
+
let data;
|
|
14482
14484
|
try {
|
|
14483
|
-
await
|
|
14484
|
-
|
|
14485
|
-
ts: anchorTs,
|
|
14486
|
-
text: payload.text,
|
|
14487
|
-
blocks: payload.blocks
|
|
14485
|
+
const res = await fetchImpl(`https://slack.com/api/conversations.replies?${params}`, {
|
|
14486
|
+
headers: { Authorization: `Bearer ${botToken}` }
|
|
14488
14487
|
});
|
|
14488
|
+
if (!res.ok) return { ok: false, error: `http_${res.status}` };
|
|
14489
|
+
data = await res.json();
|
|
14489
14490
|
} catch (err) {
|
|
14490
|
-
|
|
14491
|
-
|
|
14492
|
-
|
|
14493
|
-
|
|
14494
|
-
|
|
14495
|
-
|
|
14496
|
-
|
|
14497
|
-
|
|
14498
|
-
|
|
14499
|
-
// src/progress-tools.ts
|
|
14500
|
-
import { randomUUID } from "crypto";
|
|
14501
|
-
|
|
14502
|
-
// src/progress-handle.ts
|
|
14503
|
-
async function startProgressHandle(opts) {
|
|
14504
|
-
const now = opts.now ?? (() => Date.now());
|
|
14505
|
-
const setTimer = opts.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
|
|
14506
|
-
const clearTimer = opts.clearTimer ?? ((id) => clearTimeout(id));
|
|
14507
|
-
const debounceMs = opts.debounceMs ?? 1e3;
|
|
14508
|
-
const onPostTerminalUpdate = opts.onPostTerminalUpdate ?? defaultPostTerminalWarn;
|
|
14509
|
-
const state = {
|
|
14510
|
-
initialLabel: opts.initialLabel,
|
|
14511
|
-
mode: opts.initialMode ?? "working",
|
|
14512
|
-
lastChangeAt: now()
|
|
14513
|
-
};
|
|
14514
|
-
let lastFlushAt = 0;
|
|
14515
|
-
let pendingTimer = null;
|
|
14516
|
-
let inFlight = null;
|
|
14517
|
-
let terminal = false;
|
|
14518
|
-
async function doFlush() {
|
|
14519
|
-
while (inFlight) await inFlight;
|
|
14520
|
-
if (pendingTimer != null) {
|
|
14521
|
-
clearTimer(pendingTimer);
|
|
14522
|
-
pendingTimer = null;
|
|
14523
|
-
}
|
|
14524
|
-
lastFlushAt = now();
|
|
14525
|
-
const promise = opts.flush(snapshotState());
|
|
14526
|
-
inFlight = promise.then(
|
|
14527
|
-
() => {
|
|
14528
|
-
inFlight = null;
|
|
14529
|
-
},
|
|
14530
|
-
(err) => {
|
|
14531
|
-
inFlight = null;
|
|
14532
|
-
throw err;
|
|
14533
|
-
}
|
|
14534
|
-
);
|
|
14535
|
-
await inFlight;
|
|
14536
|
-
}
|
|
14537
|
-
function snapshotState() {
|
|
14538
|
-
return {
|
|
14539
|
-
initialLabel: state.initialLabel,
|
|
14540
|
-
mode: state.mode,
|
|
14541
|
-
stepLabel: state.stepLabel,
|
|
14542
|
-
stepNumber: state.stepNumber ? { ...state.stepNumber } : void 0,
|
|
14543
|
-
detail: state.detail,
|
|
14544
|
-
lastChangeAt: state.lastChangeAt,
|
|
14545
|
-
terminal: state.terminal ? { ...state.terminal } : void 0
|
|
14546
|
-
};
|
|
14547
|
-
}
|
|
14548
|
-
function applyUpdate(next) {
|
|
14549
|
-
if (next.mode !== void 0) state.mode = next.mode;
|
|
14550
|
-
if (next.stepLabel !== void 0) state.stepLabel = next.stepLabel;
|
|
14551
|
-
if (next.stepNumber !== void 0) state.stepNumber = { ...next.stepNumber };
|
|
14552
|
-
if (next.detail !== void 0) state.detail = next.detail;
|
|
14553
|
-
state.lastChangeAt = now();
|
|
14554
|
-
}
|
|
14555
|
-
async function scheduleOrFlush() {
|
|
14556
|
-
const elapsed = now() - lastFlushAt;
|
|
14557
|
-
if (elapsed >= debounceMs) {
|
|
14558
|
-
await doFlush();
|
|
14559
|
-
return;
|
|
14560
|
-
}
|
|
14561
|
-
if (pendingTimer != null) return;
|
|
14562
|
-
const remaining = debounceMs - elapsed;
|
|
14563
|
-
pendingTimer = setTimer(() => {
|
|
14564
|
-
pendingTimer = null;
|
|
14565
|
-
void doFlush().catch(() => {
|
|
14491
|
+
return { ok: false, error: err instanceof Error ? err.message : "fetch_failed" };
|
|
14492
|
+
}
|
|
14493
|
+
if (!data.ok) return { ok: false, error: data.error ?? "unknown" };
|
|
14494
|
+
for (const msg of data.messages ?? []) {
|
|
14495
|
+
allMessages.push({
|
|
14496
|
+
user: msg.user ?? msg.bot_id ?? "unknown",
|
|
14497
|
+
text: msg.text ?? "",
|
|
14498
|
+
ts: msg.ts ?? ""
|
|
14566
14499
|
});
|
|
14567
|
-
}, remaining);
|
|
14568
|
-
}
|
|
14569
|
-
lastFlushAt = now();
|
|
14570
|
-
await opts.flush(snapshotState());
|
|
14571
|
-
return {
|
|
14572
|
-
snapshot: snapshotState,
|
|
14573
|
-
async update(next) {
|
|
14574
|
-
if (terminal) {
|
|
14575
|
-
const peeked = snapshotState();
|
|
14576
|
-
Object.assign(peeked, {
|
|
14577
|
-
mode: next.mode ?? peeked.mode,
|
|
14578
|
-
stepLabel: next.stepLabel ?? peeked.stepLabel,
|
|
14579
|
-
stepNumber: next.stepNumber ?? peeked.stepNumber,
|
|
14580
|
-
detail: next.detail ?? peeked.detail
|
|
14581
|
-
});
|
|
14582
|
-
try {
|
|
14583
|
-
onPostTerminalUpdate(peeked);
|
|
14584
|
-
} catch (err) {
|
|
14585
|
-
console.warn(
|
|
14586
|
-
"[progress-handle] onPostTerminalUpdate threw \u2014 swallowed to keep update() non-throwing:",
|
|
14587
|
-
err
|
|
14588
|
-
);
|
|
14589
|
-
}
|
|
14590
|
-
return;
|
|
14591
|
-
}
|
|
14592
|
-
applyUpdate(next);
|
|
14593
|
-
await scheduleOrFlush();
|
|
14594
|
-
},
|
|
14595
|
-
async complete(summary) {
|
|
14596
|
-
if (terminal) return;
|
|
14597
|
-
terminal = true;
|
|
14598
|
-
state.terminal = { kind: "completed", message: summary };
|
|
14599
|
-
state.lastChangeAt = now();
|
|
14600
|
-
await doFlush();
|
|
14601
|
-
},
|
|
14602
|
-
async fail(reason) {
|
|
14603
|
-
if (terminal) return;
|
|
14604
|
-
terminal = true;
|
|
14605
|
-
state.terminal = { kind: "failed", message: reason };
|
|
14606
|
-
state.lastChangeAt = now();
|
|
14607
|
-
await doFlush();
|
|
14608
|
-
},
|
|
14609
|
-
isTerminal: () => terminal
|
|
14610
|
-
};
|
|
14611
|
-
}
|
|
14612
|
-
function defaultPostTerminalWarn(state) {
|
|
14613
|
-
console.warn(
|
|
14614
|
-
`[progress-handle] update() called after terminal transition (${state.terminal?.kind}) \u2014 ignored.`
|
|
14615
|
-
);
|
|
14616
|
-
}
|
|
14617
|
-
|
|
14618
|
-
// src/progress-tools.ts
|
|
14619
|
-
var ProgressToolRegistry = class {
|
|
14620
|
-
constructor(opts) {
|
|
14621
|
-
this.opts = opts;
|
|
14622
|
-
this.toolNames = {
|
|
14623
|
-
start: `${opts.prefix}_progress_start`,
|
|
14624
|
-
update: `${opts.prefix}_progress_update`,
|
|
14625
|
-
complete: `${opts.prefix}_progress_complete`,
|
|
14626
|
-
fail: `${opts.prefix}_progress_fail`
|
|
14627
|
-
};
|
|
14628
|
-
}
|
|
14629
|
-
handles = /* @__PURE__ */ new Map();
|
|
14630
|
-
toolNames;
|
|
14631
|
-
/** Tool definitions to splice into the MCP ListToolsRequest response. */
|
|
14632
|
-
getDefinitions() {
|
|
14633
|
-
const { surfaceDescription, startArgsSchema } = this.opts;
|
|
14634
|
-
const debounceMs = this.opts.debounceMs ?? 1e3;
|
|
14635
|
-
const debounceText = debounceMs === 1e3 ? "per second" : `every ${debounceMs}ms`;
|
|
14636
|
-
return [
|
|
14637
|
-
{
|
|
14638
|
-
name: this.toolNames.start,
|
|
14639
|
-
description: `Post a "still working" anchor to ${surfaceDescription} and return a progress_id. The anchor message updates in place as you call ${this.toolNames.update} \u2014 operators see one message that ticks forward instead of a flood of new ones. Always end with ${this.toolNames.complete} or ${this.toolNames.fail}; otherwise the anchor lingers as "working" until the stale-state sweep flips it. Opt-in \u2014 only call this for tasks that will take more than a few seconds.`,
|
|
14640
|
-
inputSchema: {
|
|
14641
|
-
type: "object",
|
|
14642
|
-
properties: {
|
|
14643
|
-
label: {
|
|
14644
|
-
type: "string",
|
|
14645
|
-
description: 'Short top-line label for the task (e.g. "diagnose vigil"). Stays constant for the lifetime of this progress anchor.'
|
|
14646
|
-
},
|
|
14647
|
-
initial_mode: {
|
|
14648
|
-
type: "string",
|
|
14649
|
-
enum: ["thinking", "working", "waiting"],
|
|
14650
|
-
description: 'Initial mode emoji \u2014 defaults to "working".'
|
|
14651
|
-
},
|
|
14652
|
-
...startArgsSchema.properties
|
|
14653
|
-
},
|
|
14654
|
-
required: ["label", ...startArgsSchema.required]
|
|
14655
|
-
}
|
|
14656
|
-
},
|
|
14657
|
-
{
|
|
14658
|
-
name: this.toolNames.update,
|
|
14659
|
-
description: `Update the progress anchor in place. The state machine debounces calls to one ${surfaceDescription} API call ${debounceText}, so spamming this every step is safe \u2014 only the latest pending state will paint. Any subset of fields can be passed; omitted ones retain their previous value.`,
|
|
14660
|
-
inputSchema: {
|
|
14661
|
-
type: "object",
|
|
14662
|
-
properties: {
|
|
14663
|
-
progress_id: { type: "string", description: "The progress_id returned from start." },
|
|
14664
|
-
step_label: { type: "string", description: 'Short label for the current step (e.g. "Querying CloudWatch logs").' },
|
|
14665
|
-
step_current: { type: "number", description: "1-based step counter \u2014 current." },
|
|
14666
|
-
step_total: { type: "number", description: "Total steps if known. Omit for open-ended progress." },
|
|
14667
|
-
mode: { type: "string", enum: ["thinking", "working", "waiting"] },
|
|
14668
|
-
detail: { type: "string", description: "Free-text detail line under the step label." }
|
|
14669
|
-
},
|
|
14670
|
-
required: ["progress_id"]
|
|
14671
|
-
}
|
|
14672
|
-
},
|
|
14673
|
-
{
|
|
14674
|
-
name: this.toolNames.complete,
|
|
14675
|
-
description: `Mark the progress anchor as \u2705 completed. Flushes any pending debounced state before painting the terminal banner \u2014 the operator never sees a stale "Step N" beside the \u2705. After this, the handle is dead; further updates are no-ops.`,
|
|
14676
|
-
inputSchema: {
|
|
14677
|
-
type: "object",
|
|
14678
|
-
properties: {
|
|
14679
|
-
progress_id: { type: "string" },
|
|
14680
|
-
summary: { type: "string", description: "Optional one-line summary of what was achieved." }
|
|
14681
|
-
},
|
|
14682
|
-
required: ["progress_id"]
|
|
14683
|
-
}
|
|
14684
|
-
},
|
|
14685
|
-
{
|
|
14686
|
-
name: this.toolNames.fail,
|
|
14687
|
-
description: `Mark the progress anchor as \u274C failed. Always include a one-sentence reason \u2014 the operator can't tell *why* from emoji alone. Same flush-then-paint behaviour as complete.`,
|
|
14688
|
-
inputSchema: {
|
|
14689
|
-
type: "object",
|
|
14690
|
-
properties: {
|
|
14691
|
-
progress_id: { type: "string" },
|
|
14692
|
-
reason: { type: "string", description: "One-sentence failure reason." }
|
|
14693
|
-
},
|
|
14694
|
-
required: ["progress_id", "reason"]
|
|
14695
|
-
}
|
|
14696
|
-
}
|
|
14697
|
-
];
|
|
14698
|
-
}
|
|
14699
|
-
/**
|
|
14700
|
-
* Dispatch a CallToolRequest. Returns `undefined` if the tool name
|
|
14701
|
-
* isn't one of ours (so the caller can keep walking its dispatch
|
|
14702
|
-
* chain). Returns a tool result otherwise.
|
|
14703
|
-
*/
|
|
14704
|
-
async handle(name, args) {
|
|
14705
|
-
if (name === this.toolNames.start) return this.handleStart(args);
|
|
14706
|
-
if (name === this.toolNames.update) return this.handleUpdate(args);
|
|
14707
|
-
if (name === this.toolNames.complete) return this.handleComplete(args);
|
|
14708
|
-
if (name === this.toolNames.fail) return this.handleFail(args);
|
|
14709
|
-
return void 0;
|
|
14710
|
-
}
|
|
14711
|
-
/** Live count of in-flight handles. Exposed for tests + diagnostics. */
|
|
14712
|
-
size() {
|
|
14713
|
-
return this.handles.size;
|
|
14714
|
-
}
|
|
14715
|
-
async handleStart(args) {
|
|
14716
|
-
const label = typeof args.label === "string" ? args.label : null;
|
|
14717
|
-
if (!label) return errResult(`${this.toolNames.start}: 'label' must be a non-empty string`);
|
|
14718
|
-
if ("initial_mode" in args && !isMode(args.initial_mode)) {
|
|
14719
|
-
return errResult(
|
|
14720
|
-
`${this.toolNames.start}: 'initial_mode' must be one of 'thinking', 'working', or 'waiting'`
|
|
14721
|
-
);
|
|
14722
|
-
}
|
|
14723
|
-
for (const k of this.opts.startArgsSchema.required) {
|
|
14724
|
-
if (typeof args[k] !== "string" || args[k].length === 0) {
|
|
14725
|
-
return errResult(`${this.toolNames.start}: '${k}' must be a non-empty string`);
|
|
14726
|
-
}
|
|
14727
14500
|
}
|
|
14728
|
-
|
|
14729
|
-
|
|
14730
|
-
|
|
14731
|
-
|
|
14732
|
-
|
|
14733
|
-
|
|
14734
|
-
|
|
14735
|
-
|
|
14736
|
-
debounceMs: this.opts.debounceMs ?? 1e3
|
|
14737
|
-
});
|
|
14738
|
-
this.handles.set(progressId, handle);
|
|
14739
|
-
return okResult(JSON.stringify({ progress_id: progressId }));
|
|
14740
|
-
} catch (err) {
|
|
14741
|
-
return errResult(`${this.toolNames.start} failed: ${err.message ?? String(err)}`);
|
|
14742
|
-
}
|
|
14743
|
-
}
|
|
14744
|
-
async handleUpdate(args) {
|
|
14745
|
-
const id = typeof args.progress_id === "string" ? args.progress_id : null;
|
|
14746
|
-
if (!id) return errResult(`${this.toolNames.update}: 'progress_id' must be a string`);
|
|
14747
|
-
const handle = this.handles.get(id);
|
|
14748
|
-
if (!handle) return errResult(`${this.toolNames.update}: unknown progress_id (already terminated, or never started)`);
|
|
14749
|
-
if ("mode" in args && !isMode(args.mode)) {
|
|
14750
|
-
return errResult(
|
|
14751
|
-
`${this.toolNames.update}: 'mode' must be one of 'thinking', 'working', or 'waiting'`
|
|
14752
|
-
);
|
|
14753
|
-
}
|
|
14754
|
-
if (typeof args.step_total === "number" && typeof args.step_current !== "number") {
|
|
14755
|
-
return errResult(
|
|
14756
|
-
`${this.toolNames.update}: 'step_total' requires 'step_current'`
|
|
14757
|
-
);
|
|
14758
|
-
}
|
|
14759
|
-
const next = {};
|
|
14760
|
-
if (typeof args.step_label === "string") next.stepLabel = args.step_label;
|
|
14761
|
-
if (typeof args.step_current === "number") {
|
|
14762
|
-
next.stepNumber = {
|
|
14763
|
-
current: args.step_current,
|
|
14764
|
-
...typeof args.step_total === "number" ? { total: args.step_total } : {}
|
|
14765
|
-
};
|
|
14766
|
-
}
|
|
14767
|
-
if (isMode(args.mode)) next.mode = args.mode;
|
|
14768
|
-
if (typeof args.detail === "string") next.detail = args.detail;
|
|
14769
|
-
try {
|
|
14770
|
-
await handle.update(next);
|
|
14771
|
-
return okResult("ok");
|
|
14772
|
-
} catch (err) {
|
|
14773
|
-
return errResult(`${this.toolNames.update} failed: ${err.message ?? String(err)}`);
|
|
14774
|
-
}
|
|
14775
|
-
}
|
|
14776
|
-
async handleComplete(args) {
|
|
14777
|
-
const id = typeof args.progress_id === "string" ? args.progress_id : null;
|
|
14778
|
-
if (!id) return errResult(`${this.toolNames.complete}: 'progress_id' must be a string`);
|
|
14779
|
-
const handle = this.handles.get(id);
|
|
14780
|
-
if (!handle) return errResult(`${this.toolNames.complete}: unknown progress_id`);
|
|
14781
|
-
const summary = typeof args.summary === "string" ? args.summary : void 0;
|
|
14782
|
-
try {
|
|
14783
|
-
await handle.complete(summary);
|
|
14784
|
-
this.handles.delete(id);
|
|
14785
|
-
return okResult("ok");
|
|
14786
|
-
} catch (err) {
|
|
14787
|
-
return errResult(`${this.toolNames.complete} failed: ${err.message ?? String(err)}`);
|
|
14788
|
-
}
|
|
14789
|
-
}
|
|
14790
|
-
async handleFail(args) {
|
|
14791
|
-
const id = typeof args.progress_id === "string" ? args.progress_id : null;
|
|
14792
|
-
if (!id) return errResult(`${this.toolNames.fail}: 'progress_id' must be a string`);
|
|
14793
|
-
const reason = typeof args.reason === "string" ? args.reason : null;
|
|
14794
|
-
if (!reason) return errResult(`${this.toolNames.fail}: 'reason' must be a non-empty string`);
|
|
14795
|
-
const handle = this.handles.get(id);
|
|
14796
|
-
if (!handle) return errResult(`${this.toolNames.fail}: unknown progress_id`);
|
|
14797
|
-
try {
|
|
14798
|
-
await handle.fail(reason);
|
|
14799
|
-
this.handles.delete(id);
|
|
14800
|
-
return okResult("ok");
|
|
14801
|
-
} catch (err) {
|
|
14802
|
-
return errResult(`${this.toolNames.fail} failed: ${err.message ?? String(err)}`);
|
|
14803
|
-
}
|
|
14804
|
-
}
|
|
14805
|
-
};
|
|
14806
|
-
function okResult(text) {
|
|
14807
|
-
return { content: [{ type: "text", text }] };
|
|
14808
|
-
}
|
|
14809
|
-
function errResult(text) {
|
|
14810
|
-
return { content: [{ type: "text", text }], isError: true };
|
|
14811
|
-
}
|
|
14812
|
-
function isMode(value) {
|
|
14813
|
-
return value === "thinking" || value === "working" || value === "waiting";
|
|
14501
|
+
cursor = data.response_metadata?.next_cursor || void 0;
|
|
14502
|
+
} while (cursor && allMessages.length < cappedLimit);
|
|
14503
|
+
const uniqueUserIds = [...new Set(allMessages.map((m) => m.user))];
|
|
14504
|
+
const resolved = await Promise.all(
|
|
14505
|
+
uniqueUserIds.map(async (id) => [id, await resolveUserName2(id)])
|
|
14506
|
+
);
|
|
14507
|
+
const nameById = new Map(resolved);
|
|
14508
|
+
return { ok: true, count: allMessages.length, formatted: formatThreadMessages(allMessages, nameById) };
|
|
14814
14509
|
}
|
|
14815
14510
|
|
|
14816
14511
|
// src/impersonation.ts
|
|
@@ -14958,20 +14653,20 @@ import {
|
|
|
14958
14653
|
createWriteStream,
|
|
14959
14654
|
existsSync as existsSync2,
|
|
14960
14655
|
mkdirSync as mkdirSync3,
|
|
14961
|
-
readFileSync as
|
|
14962
|
-
readdirSync,
|
|
14656
|
+
readFileSync as readFileSync4,
|
|
14657
|
+
readdirSync as readdirSync2,
|
|
14963
14658
|
renameSync as renameSync2,
|
|
14964
14659
|
statSync,
|
|
14965
14660
|
unlinkSync as unlinkSync2,
|
|
14966
14661
|
watch,
|
|
14967
14662
|
writeFileSync as writeFileSync3
|
|
14968
14663
|
} from "fs";
|
|
14969
|
-
import { basename, join as
|
|
14664
|
+
import { basename, join as join4, resolve as resolve2 } from "path";
|
|
14970
14665
|
import { homedir as homedir2 } from "os";
|
|
14971
|
-
import { createHash, randomUUID
|
|
14666
|
+
import { createHash, randomUUID } from "crypto";
|
|
14972
14667
|
|
|
14973
14668
|
// src/slack-thread-store.ts
|
|
14974
|
-
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
14669
|
+
import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
14975
14670
|
import { dirname } from "path";
|
|
14976
14671
|
var FILE_VERSION = 1;
|
|
14977
14672
|
var DEFAULT_TTL_DAYS = 30;
|
|
@@ -14982,7 +14677,7 @@ function loadThreadStore(filePath, opts = {}) {
|
|
|
14982
14677
|
const ttlMs = ttlDays * 24 * 60 * 60 * 1e3;
|
|
14983
14678
|
let raw;
|
|
14984
14679
|
try {
|
|
14985
|
-
raw =
|
|
14680
|
+
raw = readFileSync2(filePath, "utf-8");
|
|
14986
14681
|
} catch {
|
|
14987
14682
|
return { threads: /* @__PURE__ */ new Map(), pruned: 0 };
|
|
14988
14683
|
}
|
|
@@ -15097,9 +14792,9 @@ async function runOrRetry(fn, opts) {
|
|
|
15097
14792
|
|
|
15098
14793
|
// src/channel-attachments.ts
|
|
15099
14794
|
import { homedir } from "os";
|
|
15100
|
-
import { join, resolve, sep } from "path";
|
|
14795
|
+
import { join as join2, resolve, sep } from "path";
|
|
15101
14796
|
function resolveChannelInboundDir(codeName, channelSlug) {
|
|
15102
|
-
const base =
|
|
14797
|
+
const base = join2(homedir(), ".augmented");
|
|
15103
14798
|
const allowedSegment = /^[A-Za-z0-9_-]+$/;
|
|
15104
14799
|
if (!allowedSegment.test(codeName) || !allowedSegment.test(channelSlug)) {
|
|
15105
14800
|
throw new Error(
|
|
@@ -15745,12 +15440,12 @@ function createSlackBotUserIdClient(args) {
|
|
|
15745
15440
|
import {
|
|
15746
15441
|
existsSync,
|
|
15747
15442
|
mkdirSync as mkdirSync2,
|
|
15748
|
-
readFileSync as
|
|
15443
|
+
readFileSync as readFileSync3,
|
|
15749
15444
|
renameSync,
|
|
15750
15445
|
unlinkSync,
|
|
15751
15446
|
writeFileSync as writeFileSync2
|
|
15752
15447
|
} from "fs";
|
|
15753
|
-
import { join as
|
|
15448
|
+
import { join as join3 } from "path";
|
|
15754
15449
|
function defaultIsPidAlive(pid) {
|
|
15755
15450
|
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
15756
15451
|
try {
|
|
@@ -15768,7 +15463,7 @@ function acquireMcpSpawnLock(args) {
|
|
|
15768
15463
|
const isPidAlive = options.isPidAlive ?? defaultIsPidAlive;
|
|
15769
15464
|
const selfPid = options.selfPid ?? process.pid;
|
|
15770
15465
|
const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
15771
|
-
const path =
|
|
15466
|
+
const path = join3(agentDir, basename2);
|
|
15772
15467
|
const existing = readLockHolder(path);
|
|
15773
15468
|
if (existing) {
|
|
15774
15469
|
if (existing.pid === selfPid) {
|
|
@@ -15799,7 +15494,7 @@ function releaseMcpSpawnLock(lockPath, opts = {}) {
|
|
|
15799
15494
|
function readLockHolder(path) {
|
|
15800
15495
|
if (!existsSync(path)) return null;
|
|
15801
15496
|
try {
|
|
15802
|
-
const raw =
|
|
15497
|
+
const raw = readFileSync3(path, "utf8");
|
|
15803
15498
|
const parsed = JSON.parse(raw);
|
|
15804
15499
|
const pid = typeof parsed.pid === "number" ? parsed.pid : Number(parsed.pid);
|
|
15805
15500
|
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
@@ -15881,9 +15576,9 @@ var SLACK_PEER_CLASSIFIER_CONFIG = {
|
|
|
15881
15576
|
peers: parsePeersEnv(process.env.SLACK_PEERS, process.env.SLACK_PEERS_GATE),
|
|
15882
15577
|
peer_disabled_mode: SLACK_PEER_DISABLED_MODE
|
|
15883
15578
|
};
|
|
15884
|
-
var SLACK_AGENT_DIR = AGENT_CODE_NAME ?
|
|
15885
|
-
var SLACK_PENDING_INBOUND_DIR = SLACK_AGENT_DIR ?
|
|
15886
|
-
var SLACK_RECOVERY_OUTBOX_DIR = SLACK_AGENT_DIR ?
|
|
15579
|
+
var SLACK_AGENT_DIR = AGENT_CODE_NAME ? join4(homedir2(), ".augmented", AGENT_CODE_NAME) : null;
|
|
15580
|
+
var SLACK_PENDING_INBOUND_DIR = SLACK_AGENT_DIR ? join4(SLACK_AGENT_DIR, "slack-pending-inbound") : null;
|
|
15581
|
+
var SLACK_RECOVERY_OUTBOX_DIR = SLACK_AGENT_DIR ? join4(SLACK_AGENT_DIR, "slack-recovery-outbox") : null;
|
|
15887
15582
|
var SLACK_MAX_RECOVERY_ATTEMPTS = 3;
|
|
15888
15583
|
function redactSlackId(id) {
|
|
15889
15584
|
if (!id) return "<none>";
|
|
@@ -15895,16 +15590,18 @@ function safeSlackMarkerName(channel, threadTs, messageTs) {
|
|
|
15895
15590
|
}
|
|
15896
15591
|
function slackPendingInboundPath(channel, threadTs, messageTs) {
|
|
15897
15592
|
if (!SLACK_PENDING_INBOUND_DIR) return null;
|
|
15898
|
-
return
|
|
15593
|
+
return join4(SLACK_PENDING_INBOUND_DIR, safeSlackMarkerName(channel, threadTs, messageTs));
|
|
15899
15594
|
}
|
|
15900
|
-
function writeSlackPendingInboundMarker(channel, threadTs, messageTs) {
|
|
15595
|
+
function writeSlackPendingInboundMarker(channel, threadTs, messageTs, undeliverable = false) {
|
|
15901
15596
|
const path = slackPendingInboundPath(channel, threadTs, messageTs);
|
|
15902
15597
|
if (!path || !SLACK_PENDING_INBOUND_DIR) return;
|
|
15903
15598
|
const marker = {
|
|
15904
15599
|
channel,
|
|
15905
15600
|
thread_ts: threadTs,
|
|
15906
15601
|
message_ts: messageTs,
|
|
15907
|
-
received_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
15602
|
+
received_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15603
|
+
// Only persist the flag when set — keeps healthy-path markers byte-identical.
|
|
15604
|
+
...undeliverable ? { undeliverable: true } : {}
|
|
15908
15605
|
};
|
|
15909
15606
|
try {
|
|
15910
15607
|
mkdirSync3(SLACK_PENDING_INBOUND_DIR, { recursive: true, mode: 448 });
|
|
@@ -15916,18 +15613,52 @@ function writeSlackPendingInboundMarker(channel, threadTs, messageTs) {
|
|
|
15916
15613
|
);
|
|
15917
15614
|
}
|
|
15918
15615
|
}
|
|
15616
|
+
function healSlackUndeliverable(channel, messageTs) {
|
|
15617
|
+
if (!BOT_TOKEN || !channel || !messageTs) return;
|
|
15618
|
+
const headers = {
|
|
15619
|
+
"Content-Type": "application/json",
|
|
15620
|
+
Authorization: `Bearer ${BOT_TOKEN}`
|
|
15621
|
+
};
|
|
15622
|
+
fetch("https://slack.com/api/reactions.remove", {
|
|
15623
|
+
method: "POST",
|
|
15624
|
+
headers,
|
|
15625
|
+
body: JSON.stringify({ channel, timestamp: messageTs, name: "x" })
|
|
15626
|
+
}).catch(() => {
|
|
15627
|
+
}).finally(() => {
|
|
15628
|
+
fetch("https://slack.com/api/reactions.add", {
|
|
15629
|
+
method: "POST",
|
|
15630
|
+
headers,
|
|
15631
|
+
body: JSON.stringify({ channel, timestamp: messageTs, name: "eyes" })
|
|
15632
|
+
}).catch(() => {
|
|
15633
|
+
});
|
|
15634
|
+
});
|
|
15635
|
+
}
|
|
15636
|
+
function clearSlackMarkerFileWithHeal(fullPath) {
|
|
15637
|
+
let marker = null;
|
|
15638
|
+
try {
|
|
15639
|
+
marker = JSON.parse(readFileSync4(fullPath, "utf-8"));
|
|
15640
|
+
} catch {
|
|
15641
|
+
}
|
|
15642
|
+
if (marker && decideRecoveryHeal({
|
|
15643
|
+
wasUndeliverable: marker.undeliverable === true,
|
|
15644
|
+
hasTarget: Boolean(marker.channel && marker.message_ts)
|
|
15645
|
+
}) === "heal") {
|
|
15646
|
+
healSlackUndeliverable(marker.channel, marker.message_ts);
|
|
15647
|
+
}
|
|
15648
|
+
try {
|
|
15649
|
+
if (existsSync2(fullPath)) unlinkSync2(fullPath);
|
|
15650
|
+
} catch {
|
|
15651
|
+
}
|
|
15652
|
+
}
|
|
15919
15653
|
function clearAllSlackPendingMarkersForThread(channel, threadTs) {
|
|
15920
15654
|
if (!SLACK_PENDING_INBOUND_DIR) return;
|
|
15921
15655
|
const safeChan = channel.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
15922
15656
|
const safeThread = threadTs.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
15923
15657
|
const prefix = `${safeChan}__${safeThread}__`;
|
|
15924
15658
|
try {
|
|
15925
|
-
for (const f of
|
|
15659
|
+
for (const f of readdirSync2(SLACK_PENDING_INBOUND_DIR)) {
|
|
15926
15660
|
if (!f.startsWith(prefix) || !f.endsWith(".json")) continue;
|
|
15927
|
-
|
|
15928
|
-
unlinkSync2(join3(SLACK_PENDING_INBOUND_DIR, f));
|
|
15929
|
-
} catch {
|
|
15930
|
-
}
|
|
15661
|
+
clearSlackMarkerFileWithHeal(join4(SLACK_PENDING_INBOUND_DIR, f));
|
|
15931
15662
|
}
|
|
15932
15663
|
} catch {
|
|
15933
15664
|
}
|
|
@@ -15946,10 +15677,10 @@ function slackNextRetryName(filename) {
|
|
|
15946
15677
|
async function processSlackRecoveryOutboxFile(filename) {
|
|
15947
15678
|
if (!SLACK_RECOVERY_OUTBOX_DIR) return;
|
|
15948
15679
|
if (filename.endsWith(".poison.json") || filename.endsWith(".tmp")) return;
|
|
15949
|
-
const fullPath =
|
|
15680
|
+
const fullPath = join4(SLACK_RECOVERY_OUTBOX_DIR, filename);
|
|
15950
15681
|
let payload;
|
|
15951
15682
|
try {
|
|
15952
|
-
payload = JSON.parse(
|
|
15683
|
+
payload = JSON.parse(readFileSync4(fullPath, "utf-8"));
|
|
15953
15684
|
} catch (err) {
|
|
15954
15685
|
process.stderr.write(
|
|
15955
15686
|
`slack-channel(${AGENT_CODE_NAME}): recovery outbox parse failed (${filename}): ${err.message}
|
|
@@ -16023,7 +15754,7 @@ async function processSlackRecoveryOutboxFile(filename) {
|
|
|
16023
15754
|
const next = slackNextRetryName(filename);
|
|
16024
15755
|
if (next) {
|
|
16025
15756
|
try {
|
|
16026
|
-
renameSync2(fullPath,
|
|
15757
|
+
renameSync2(fullPath, join4(SLACK_RECOVERY_OUTBOX_DIR, next.next));
|
|
16027
15758
|
if (next.attempt >= SLACK_MAX_RECOVERY_ATTEMPTS) {
|
|
16028
15759
|
process.stderr.write(
|
|
16029
15760
|
`slack-channel(${AGENT_CODE_NAME}): ghost-reply recovery exhausted retries \u2014 moved to ${next.next}
|
|
@@ -16053,7 +15784,7 @@ function scanSlackRecoveryRetries() {
|
|
|
16053
15784
|
if (!SLACK_RECOVERY_OUTBOX_DIR) return;
|
|
16054
15785
|
let entries;
|
|
16055
15786
|
try {
|
|
16056
|
-
entries =
|
|
15787
|
+
entries = readdirSync2(SLACK_RECOVERY_OUTBOX_DIR);
|
|
16057
15788
|
} catch {
|
|
16058
15789
|
return;
|
|
16059
15790
|
}
|
|
@@ -16062,7 +15793,7 @@ function scanSlackRecoveryRetries() {
|
|
|
16062
15793
|
if (!f.includes(".retry-") || f.endsWith(".poison.json")) continue;
|
|
16063
15794
|
let mtimeMs;
|
|
16064
15795
|
try {
|
|
16065
|
-
mtimeMs = statSync(
|
|
15796
|
+
mtimeMs = statSync(join4(SLACK_RECOVERY_OUTBOX_DIR, f)).mtimeMs;
|
|
16066
15797
|
} catch {
|
|
16067
15798
|
continue;
|
|
16068
15799
|
}
|
|
@@ -16083,7 +15814,7 @@ function startSlackRecoveryOutboxWatcher() {
|
|
|
16083
15814
|
return;
|
|
16084
15815
|
}
|
|
16085
15816
|
try {
|
|
16086
|
-
for (const f of
|
|
15817
|
+
for (const f of readdirSync2(SLACK_RECOVERY_OUTBOX_DIR)) {
|
|
16087
15818
|
if (isFirstAttemptSlackOutboxFile(f)) void processSlackRecoveryOutboxFile(f);
|
|
16088
15819
|
}
|
|
16089
15820
|
} catch {
|
|
@@ -16092,7 +15823,7 @@ function startSlackRecoveryOutboxWatcher() {
|
|
|
16092
15823
|
const watcher = watch(SLACK_RECOVERY_OUTBOX_DIR, (event, filename) => {
|
|
16093
15824
|
if (event !== "rename" || !filename) return;
|
|
16094
15825
|
if (!isFirstAttemptSlackOutboxFile(filename)) return;
|
|
16095
|
-
if (existsSync2(
|
|
15826
|
+
if (existsSync2(join4(SLACK_RECOVERY_OUTBOX_DIR, filename))) {
|
|
16096
15827
|
void processSlackRecoveryOutboxFile(filename);
|
|
16097
15828
|
}
|
|
16098
15829
|
});
|
|
@@ -16108,15 +15839,15 @@ function startSlackRecoveryOutboxWatcher() {
|
|
|
16108
15839
|
}
|
|
16109
15840
|
startSlackRecoveryOutboxWatcher();
|
|
16110
15841
|
var STALE_MARKER_MS = 24 * 60 * 60 * 1e3;
|
|
16111
|
-
function trackPendingMessage(channel, threadTs, messageTs) {
|
|
16112
|
-
writeSlackPendingInboundMarker(channel, threadTs, messageTs);
|
|
15842
|
+
function trackPendingMessage(channel, threadTs, messageTs, undeliverable = false) {
|
|
15843
|
+
writeSlackPendingInboundMarker(channel, threadTs, messageTs, undeliverable);
|
|
16113
15844
|
}
|
|
16114
15845
|
function sweepSlackStaleMarkersOnBoot() {
|
|
16115
15846
|
if (!SLACK_PENDING_INBOUND_DIR) return;
|
|
16116
15847
|
if (!existsSync2(SLACK_PENDING_INBOUND_DIR)) return;
|
|
16117
15848
|
let filenames;
|
|
16118
15849
|
try {
|
|
16119
|
-
filenames =
|
|
15850
|
+
filenames = readdirSync2(SLACK_PENDING_INBOUND_DIR);
|
|
16120
15851
|
} catch (err) {
|
|
16121
15852
|
process.stderr.write(
|
|
16122
15853
|
`slack-channel(${AGENT_CODE_NAME}): stale-marker readdir failed: ${err.message}
|
|
@@ -16129,10 +15860,10 @@ function sweepSlackStaleMarkersOnBoot() {
|
|
|
16129
15860
|
for (const filename of filenames) {
|
|
16130
15861
|
if (!filename.endsWith(".json")) continue;
|
|
16131
15862
|
if (filename.endsWith(".tmp")) continue;
|
|
16132
|
-
const fullPath =
|
|
15863
|
+
const fullPath = join4(SLACK_PENDING_INBOUND_DIR, filename);
|
|
16133
15864
|
let marker;
|
|
16134
15865
|
try {
|
|
16135
|
-
marker = JSON.parse(
|
|
15866
|
+
marker = JSON.parse(readFileSync4(fullPath, "utf-8"));
|
|
16136
15867
|
} catch (err) {
|
|
16137
15868
|
process.stderr.write(
|
|
16138
15869
|
`slack-channel(${AGENT_CODE_NAME}): stale-marker parse failed for ${redactSlackId(filename)}: ${err.message}
|
|
@@ -16185,7 +15916,7 @@ function noteThreadActivityByMessageTs(channel, messageTs) {
|
|
|
16185
15916
|
if (!existsSync2(SLACK_PENDING_INBOUND_DIR)) return;
|
|
16186
15917
|
let filenames;
|
|
16187
15918
|
try {
|
|
16188
|
-
filenames =
|
|
15919
|
+
filenames = readdirSync2(SLACK_PENDING_INBOUND_DIR);
|
|
16189
15920
|
} catch {
|
|
16190
15921
|
return;
|
|
16191
15922
|
}
|
|
@@ -16196,13 +15927,10 @@ function noteThreadActivityByMessageTs(channel, messageTs) {
|
|
|
16196
15927
|
for (const filename of filenames) {
|
|
16197
15928
|
if (!filename.startsWith(channelPrefix)) continue;
|
|
16198
15929
|
if (!filename.endsWith(messageSuffix)) continue;
|
|
16199
|
-
|
|
16200
|
-
unlinkSync2(join3(SLACK_PENDING_INBOUND_DIR, filename));
|
|
16201
|
-
} catch {
|
|
16202
|
-
}
|
|
15930
|
+
clearSlackMarkerFileWithHeal(join4(SLACK_PENDING_INBOUND_DIR, filename));
|
|
16203
15931
|
}
|
|
16204
15932
|
}
|
|
16205
|
-
var RESTART_FLAGS_DIR =
|
|
15933
|
+
var RESTART_FLAGS_DIR = join4(homedir2(), ".augmented", "restart-flags");
|
|
16206
15934
|
function buildAugmentedSlackMetadata() {
|
|
16207
15935
|
if (!AGT_TEAM_ID) return void 0;
|
|
16208
15936
|
return {
|
|
@@ -16384,7 +16112,7 @@ async function handleSlashCommandEnvelope(payload) {
|
|
|
16384
16112
|
if (!existsSync2(RESTART_FLAGS_DIR)) {
|
|
16385
16113
|
mkdirSync3(RESTART_FLAGS_DIR, { recursive: true });
|
|
16386
16114
|
}
|
|
16387
|
-
const flagPath =
|
|
16115
|
+
const flagPath = join4(RESTART_FLAGS_DIR, `${codeName}.flag`);
|
|
16388
16116
|
const flag = {
|
|
16389
16117
|
codeName,
|
|
16390
16118
|
source: "slack",
|
|
@@ -16394,7 +16122,7 @@ async function handleSlashCommandEnvelope(payload) {
|
|
|
16394
16122
|
...payload.thread_ts ? { thread_ts: payload.thread_ts } : {}
|
|
16395
16123
|
}
|
|
16396
16124
|
};
|
|
16397
|
-
const tmpPath = `${flagPath}.${process.pid}.${
|
|
16125
|
+
const tmpPath = `${flagPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
16398
16126
|
writeFileSync3(tmpPath, JSON.stringify(flag) + "\n", "utf8");
|
|
16399
16127
|
renameSync2(tmpPath, flagPath);
|
|
16400
16128
|
process.stderr.write(
|
|
@@ -16491,7 +16219,7 @@ async function handleRestartCommand(opts) {
|
|
|
16491
16219
|
if (!existsSync2(RESTART_FLAGS_DIR)) {
|
|
16492
16220
|
mkdirSync3(RESTART_FLAGS_DIR, { recursive: true });
|
|
16493
16221
|
}
|
|
16494
|
-
const flagPath =
|
|
16222
|
+
const flagPath = join4(RESTART_FLAGS_DIR, `${codeName}.flag`);
|
|
16495
16223
|
const flag = {
|
|
16496
16224
|
codeName,
|
|
16497
16225
|
source: "slack",
|
|
@@ -16502,7 +16230,7 @@ async function handleRestartCommand(opts) {
|
|
|
16502
16230
|
message_ts: opts.ts
|
|
16503
16231
|
}
|
|
16504
16232
|
};
|
|
16505
|
-
const tmpPath = `${flagPath}.${process.pid}.${
|
|
16233
|
+
const tmpPath = `${flagPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
16506
16234
|
writeFileSync3(tmpPath, JSON.stringify(flag) + "\n", "utf8");
|
|
16507
16235
|
renameSync2(tmpPath, flagPath);
|
|
16508
16236
|
process.stderr.write(
|
|
@@ -16550,7 +16278,7 @@ var THREAD_STORE_TTL_DAYS = parseTtlDays(process.env.SLACK_THREAD_FOLLOW_TTL_DAY
|
|
|
16550
16278
|
var threadPersister = null;
|
|
16551
16279
|
function resolveThreadStorePath() {
|
|
16552
16280
|
if (!AGENT_CODE_NAME) return null;
|
|
16553
|
-
return
|
|
16281
|
+
return join4(homedir2(), ".augmented", AGENT_CODE_NAME, "slack-tracked-threads.json");
|
|
16554
16282
|
}
|
|
16555
16283
|
function parseTtlDays(raw) {
|
|
16556
16284
|
if (!raw) return void 0;
|
|
@@ -16585,9 +16313,9 @@ if (!BOT_TOKEN || !APP_TOKEN) {
|
|
|
16585
16313
|
var slackStderrLogStream = null;
|
|
16586
16314
|
if (AGENT_CODE_NAME) {
|
|
16587
16315
|
try {
|
|
16588
|
-
const logDir =
|
|
16316
|
+
const logDir = join4(homedir2(), ".augmented", AGENT_CODE_NAME);
|
|
16589
16317
|
mkdirSync3(logDir, { recursive: true });
|
|
16590
|
-
slackStderrLogStream = createWriteStream(
|
|
16318
|
+
slackStderrLogStream = createWriteStream(join4(logDir, "slack-channel-stderr.log"), {
|
|
16591
16319
|
flags: "a",
|
|
16592
16320
|
mode: 384
|
|
16593
16321
|
});
|
|
@@ -16657,7 +16385,6 @@ void resolveBotUserIdOrThrow().then((id) => {
|
|
|
16657
16385
|
);
|
|
16658
16386
|
if (authFailed) slackBotUserIdClient?.reportAuthHealth(false);
|
|
16659
16387
|
});
|
|
16660
|
-
var selfIdentityInstruction = `Mentions of your own Slack bot user are directed at you, even inside auto_followed threads.`;
|
|
16661
16388
|
var mcp = new Server(
|
|
16662
16389
|
{ name: "slack", version: "0.1.0" },
|
|
16663
16390
|
{
|
|
@@ -16672,43 +16399,18 @@ var mcp = new Server(
|
|
|
16672
16399
|
// Highest-priority lines first — Claude Code truncates this string at
|
|
16673
16400
|
// 2048 chars, so anything appended late silently disappears.
|
|
16674
16401
|
"CRITICAL: every response to a Slack <channel> tag MUST go through slack.reply. Text in your session WITHOUT a slack.reply call never reaches the user \u2014 the message dies inside the agent process.",
|
|
16675
|
-
|
|
16676
|
-
"Long task (3+ tool calls or >5s)? slack_progress_start (channel + thread_ts), slack_progress_update between steps, slack_progress_complete/_fail to close. One anchor edits in place; avoids thread spam. Not for one-shots.",
|
|
16677
|
-
selfIdentityInstruction,
|
|
16402
|
+
`Inbound: <channel ... thread_ts="..." [thread_context="..."]>. Pass channel + thread_ts to slack.reply on threads. thread_context = thread pre-loaded; ground replies ONLY in it, never another channel's.`,
|
|
16678
16403
|
"Inbound attachments: <channel> `files` is a JSON-serialised array \u2014 JSON.parse it. If an entry has `path`, the image is already downloaded \u2014 Read it directly, do NOT call slack.download_attachment. Use that tool only for entries with `file_id` but NO `path` (PDF, docx, csv): pass file_id + channel verbatim, then Read the returned path. Single-image messages also get a top-level `image_path`. Don't surface internal file-handling errors that don't affect the answer.",
|
|
16679
16404
|
"Address users by user_name, never by raw user ID. In multi-participant threads the CURRENT speaker is the one on the latest <channel> tag.",
|
|
16680
|
-
'Mentioned in a channel \u2192 respond in that thread. DM \u2192 respond directly. auto_followed="true" \u2192 only reply if
|
|
16681
|
-
"Reaction taxonomy (use slack.react sparingly \u2014 prefer a reply): \u{1F440} = ack (already auto-added on inbound, do not duplicate); \u2705 = success. NEVER react to signal failure \u2014 users can't tell why something failed from an emoji. On failure, slack.reply with one sentence explaining what went wrong (no stack traces, no secrets).",
|
|
16405
|
+
'Mentioned in a channel \u2192 respond in that thread. DM \u2192 respond directly. auto_followed="true" \u2192 only reply if useful, OR if your own bot user is @-mentioned (counts even in auto_followed).',
|
|
16406
|
+
"Reaction taxonomy (use slack.react sparingly \u2014 prefer a reply): \u{1F440} = ack (already auto-added on inbound, do not duplicate); \u2705 = success. NEVER react to signal failure of YOUR work \u2014 users can't tell why something failed from an emoji. On failure, slack.reply with one sentence explaining what went wrong (no stack traces, no secrets). (The \u274C you may see on an inbound is applied by the system, not you \u2014 it marks a message that arrived while the agent couldn't reply; never add \u274C yourself.)",
|
|
16682
16407
|
`When a thread message is NOT addressed to you (different @-mention, side conversation, auto_followed catch-up): SILENTLY SKIP \u2014 no reaction, no reply, no "this wasn't for me" message.`,
|
|
16683
16408
|
"To deliver a file: save under your project dir, call slack.upload_file with path + channel + thread_ts."
|
|
16684
16409
|
].join(" ")
|
|
16685
16410
|
}
|
|
16686
16411
|
);
|
|
16687
|
-
var progressRegistry = new ProgressToolRegistry({
|
|
16688
|
-
prefix: "slack",
|
|
16689
|
-
surfaceDescription: "a Slack thread",
|
|
16690
|
-
startArgsSchema: {
|
|
16691
|
-
properties: {
|
|
16692
|
-
channel: {
|
|
16693
|
-
type: "string",
|
|
16694
|
-
description: "Slack channel id (from the `channel` attribute on the inbound <channel> tag)."
|
|
16695
|
-
},
|
|
16696
|
-
thread_ts: {
|
|
16697
|
-
type: "string",
|
|
16698
|
-
description: "Thread `ts` so the anchor lands in the same thread as the request (from the `thread_ts` attribute on the inbound <channel> tag). Omit to post at channel-root."
|
|
16699
|
-
}
|
|
16700
|
-
},
|
|
16701
|
-
required: ["channel"]
|
|
16702
|
-
},
|
|
16703
|
-
createFlush: (args) => createSlackProgressFlush({
|
|
16704
|
-
botToken: BOT_TOKEN,
|
|
16705
|
-
channel: args.channel,
|
|
16706
|
-
threadTs: typeof args.thread_ts === "string" ? args.thread_ts : void 0
|
|
16707
|
-
})
|
|
16708
|
-
});
|
|
16709
16412
|
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
16710
16413
|
tools: [
|
|
16711
|
-
...progressRegistry.getDefinitions(),
|
|
16712
16414
|
{
|
|
16713
16415
|
name: "slack.reply",
|
|
16714
16416
|
description: "Send a message back to a Slack channel or thread",
|
|
@@ -16730,7 +16432,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
16730
16432
|
},
|
|
16731
16433
|
{
|
|
16732
16434
|
name: "slack.react",
|
|
16733
|
-
description: "Add an emoji reaction to a Slack message. Use sparingly \u2014 prefer a text reply. Reaction taxonomy: \u2705 = action completed successfully. NEVER react to signal failure \u2014 users can't tell why it failed from a reaction. On failure, call slack.reply with one sentence explaining what went wrong instead. \u{1F440} (eyes) is already auto-applied on inbound; do not duplicate.",
|
|
16435
|
+
description: "Add an emoji reaction to a Slack message. Use sparingly \u2014 prefer a text reply. Reaction taxonomy: \u2705 = action completed successfully. NEVER react to signal failure of your work \u2014 users can't tell why it failed from a reaction. On failure, call slack.reply with one sentence explaining what went wrong instead. \u{1F440} (eyes) is already auto-applied on inbound; do not duplicate. \u274C (:x:) is reserved for the system to mark an inbound that arrived while the agent couldn't reply \u2014 never add \u274C yourself.",
|
|
16734
16436
|
inputSchema: {
|
|
16735
16437
|
type: "object",
|
|
16736
16438
|
properties: {
|
|
@@ -16743,7 +16445,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
16743
16445
|
},
|
|
16744
16446
|
{
|
|
16745
16447
|
name: "slack.read_thread",
|
|
16746
|
-
description: "Read the full message history of a Slack thread.
|
|
16448
|
+
description: "Read the full message history of a Slack thread. Inbound thread replies already carry the surrounding thread inline as the <channel> tag's thread_context attribute \u2014 ground your reply in that first, and only call this tool when you need MORE history than thread_context shows (it is capped to recent messages) or to catch up on an auto-followed thread.",
|
|
16747
16449
|
inputSchema: {
|
|
16748
16450
|
type: "object",
|
|
16749
16451
|
properties: {
|
|
@@ -16959,8 +16661,6 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
16959
16661
|
if (isImpersonating() && !channelsEnabledOverride() && SLACK_EGRESS_TOOLS.has(name)) {
|
|
16960
16662
|
return buildImpersonationRefusal(name);
|
|
16961
16663
|
}
|
|
16962
|
-
const progressResult = await progressRegistry.handle(name, args ?? {});
|
|
16963
|
-
if (progressResult !== void 0) return progressResult;
|
|
16964
16664
|
if (name === "slack.reply") {
|
|
16965
16665
|
const { channel, text, thread_ts } = args;
|
|
16966
16666
|
if (channel && thread_ts) {
|
|
@@ -17117,46 +16817,22 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
17117
16817
|
const limit = Math.min(Math.max(rawLimit ?? 50, 1), 200);
|
|
17118
16818
|
noteThreadActivity(channel, thread_ts);
|
|
17119
16819
|
try {
|
|
17120
|
-
const
|
|
17121
|
-
|
|
17122
|
-
|
|
17123
|
-
|
|
17124
|
-
|
|
17125
|
-
|
|
17126
|
-
|
|
17127
|
-
|
|
17128
|
-
|
|
17129
|
-
|
|
17130
|
-
});
|
|
17131
|
-
const res = await fetch(`https://slack.com/api/conversations.replies?${params}`, {
|
|
17132
|
-
headers: { Authorization: `Bearer ${BOT_TOKEN}` }
|
|
17133
|
-
});
|
|
17134
|
-
const data = await res.json();
|
|
17135
|
-
if (!data.ok) {
|
|
17136
|
-
return {
|
|
17137
|
-
content: [{ type: "text", text: `Slack error: ${data.error}` }],
|
|
17138
|
-
isError: true
|
|
17139
|
-
};
|
|
17140
|
-
}
|
|
17141
|
-
for (const msg of data.messages ?? []) {
|
|
17142
|
-
allMessages.push({
|
|
17143
|
-
user: msg.user ?? msg.bot_id ?? "unknown",
|
|
17144
|
-
text: msg.text ?? "",
|
|
17145
|
-
ts: msg.ts ?? ""
|
|
17146
|
-
});
|
|
17147
|
-
}
|
|
17148
|
-
cursor = data.response_metadata?.next_cursor || void 0;
|
|
17149
|
-
} while (cursor && allMessages.length < limit);
|
|
17150
|
-
const uniqueUserIds = [...new Set(allMessages.map((m) => m.user))];
|
|
17151
|
-
const resolved = await Promise.all(uniqueUserIds.map(async (id) => [id, await resolveUserName(id)]));
|
|
17152
|
-
const nameById = new Map(resolved);
|
|
17153
|
-
const formatted = allMessages.map((m) => `[${m.ts}] ${nameById.get(m.user) ?? m.user} (<@${m.user}>): ${m.text}`).join("\n");
|
|
16820
|
+
const result = await fetchThreadTranscript(channel, thread_ts, limit, {
|
|
16821
|
+
botToken: BOT_TOKEN,
|
|
16822
|
+
resolveUserName
|
|
16823
|
+
});
|
|
16824
|
+
if (!result.ok) {
|
|
16825
|
+
return {
|
|
16826
|
+
content: [{ type: "text", text: `Slack error: ${result.error}` }],
|
|
16827
|
+
isError: true
|
|
16828
|
+
};
|
|
16829
|
+
}
|
|
17154
16830
|
return {
|
|
17155
16831
|
content: [{
|
|
17156
16832
|
type: "text",
|
|
17157
|
-
text:
|
|
16833
|
+
text: result.count > 0 ? `Thread (${result.count} messages):
|
|
17158
16834
|
|
|
17159
|
-
${formatted}` : "Thread is empty or not found."
|
|
16835
|
+
${result.formatted}` : "Thread is empty or not found."
|
|
17160
16836
|
}]
|
|
17161
16837
|
};
|
|
17162
16838
|
} catch (err) {
|
|
@@ -17206,7 +16882,7 @@ ${formatted}` : "Thread is empty or not found."
|
|
|
17206
16882
|
};
|
|
17207
16883
|
}
|
|
17208
16884
|
size = stat.size;
|
|
17209
|
-
bytes =
|
|
16885
|
+
bytes = readFileSync4(resolvedPath);
|
|
17210
16886
|
} catch (err) {
|
|
17211
16887
|
return {
|
|
17212
16888
|
content: [{ type: "text", text: `Failed to read file: ${err.message}` }],
|
|
@@ -17505,15 +17181,15 @@ async function handleSendStructured(args) {
|
|
|
17505
17181
|
const { validateSlackBlocks: validateSlackBlocks2 } = runtime;
|
|
17506
17182
|
const { channel, blocks, text, thread_ts, interactive } = args;
|
|
17507
17183
|
if (typeof channel !== "string" || !channel) {
|
|
17508
|
-
return
|
|
17184
|
+
return errResult("channel is required");
|
|
17509
17185
|
}
|
|
17510
17186
|
if (typeof text !== "string" || !text) {
|
|
17511
|
-
return
|
|
17187
|
+
return errResult("text is required (used as fallback for push notifications and unfurls)");
|
|
17512
17188
|
}
|
|
17513
17189
|
noteThreadActivity(channel, thread_ts);
|
|
17514
17190
|
const validation = validateSlackBlocks2(blocks);
|
|
17515
17191
|
if (!validation.ok) {
|
|
17516
|
-
return
|
|
17192
|
+
return errResult(`Invalid blocks: ${validation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
|
|
17517
17193
|
}
|
|
17518
17194
|
let effectiveBlocks = blocks;
|
|
17519
17195
|
let ratingCallbackId = null;
|
|
@@ -17521,13 +17197,13 @@ async function handleSendStructured(args) {
|
|
|
17521
17197
|
let ratingTokenised = [];
|
|
17522
17198
|
if (interactive) {
|
|
17523
17199
|
if (interactive.type !== "rate_run") {
|
|
17524
|
-
return
|
|
17200
|
+
return errResult(`Unsupported interactive.type '${interactive.type}'. Supported: rate_run.`);
|
|
17525
17201
|
}
|
|
17526
17202
|
if (typeof interactive.run_id !== "string" || !interactive.run_id) {
|
|
17527
|
-
return
|
|
17203
|
+
return errResult("interactive.run_id is required when interactive is set");
|
|
17528
17204
|
}
|
|
17529
17205
|
if (!interactiveHostAvailable()) {
|
|
17530
|
-
return
|
|
17206
|
+
return errResult(
|
|
17531
17207
|
"interactive=rate_run requires Block Kit plus host MCP wiring (AGT_HOST, AGT_API_KEY, AGT_AGENT_ID)."
|
|
17532
17208
|
);
|
|
17533
17209
|
}
|
|
@@ -17558,7 +17234,7 @@ async function handleSendStructured(args) {
|
|
|
17558
17234
|
effectiveBlocks = [...blocks, ratingBlock];
|
|
17559
17235
|
const finalValidation = runtime.validateSlackBlocks(effectiveBlocks);
|
|
17560
17236
|
if (!finalValidation.ok) {
|
|
17561
|
-
return
|
|
17237
|
+
return errResult(
|
|
17562
17238
|
`Invalid blocks (after appending rating row): ${finalValidation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`
|
|
17563
17239
|
);
|
|
17564
17240
|
}
|
|
@@ -17579,7 +17255,7 @@ async function handleSendStructured(args) {
|
|
|
17579
17255
|
kindData: { run_id: interactive.run_id, scale }
|
|
17580
17256
|
});
|
|
17581
17257
|
} catch (err) {
|
|
17582
|
-
return
|
|
17258
|
+
return errResult(`Failed to register rating row: ${err.message}`);
|
|
17583
17259
|
}
|
|
17584
17260
|
}
|
|
17585
17261
|
const result = await postSlackMessageWithTs({
|
|
@@ -17589,7 +17265,7 @@ async function handleSendStructured(args) {
|
|
|
17589
17265
|
...thread_ts ? { thread_ts } : {}
|
|
17590
17266
|
});
|
|
17591
17267
|
if (!result.ok || !result.ts) {
|
|
17592
|
-
return
|
|
17268
|
+
return errResult(`Slack chat.postMessage failed: ${result.error ?? "unknown"}`);
|
|
17593
17269
|
}
|
|
17594
17270
|
if (ratingCallbackId) {
|
|
17595
17271
|
try {
|
|
@@ -17644,17 +17320,17 @@ async function handleSendStructured(args) {
|
|
|
17644
17320
|
}
|
|
17645
17321
|
async function handleAskUser(args) {
|
|
17646
17322
|
if (!askUserToolAvailable()) {
|
|
17647
|
-
return
|
|
17323
|
+
return errResult("slack.ask_user is disabled or the host MCP is missing AGT_HOST/AGT_API_KEY/AGT_AGENT_ID env wiring.");
|
|
17648
17324
|
}
|
|
17649
17325
|
const { randomUUID: rndUUID } = await import("crypto");
|
|
17650
17326
|
const runtime = await Promise.resolve().then(() => (init_slack_block_kit_runtime(), slack_block_kit_runtime_exports));
|
|
17651
17327
|
const { validateAskUserOptions: validateAskUserOptions2, buildAskUserBlocks: buildAskUserBlocks2 } = runtime;
|
|
17652
17328
|
const { channel, question, options, timeout_seconds, thread_ts } = args;
|
|
17653
|
-
if (typeof channel !== "string" || !channel) return
|
|
17654
|
-
if (typeof question !== "string" || !question) return
|
|
17329
|
+
if (typeof channel !== "string" || !channel) return errResult("channel is required");
|
|
17330
|
+
if (typeof question !== "string" || !question) return errResult("question is required");
|
|
17655
17331
|
const optsValidation = validateAskUserOptions2(options);
|
|
17656
17332
|
if (!optsValidation.ok) {
|
|
17657
|
-
return
|
|
17333
|
+
return errResult(`Invalid options: ${optsValidation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
|
|
17658
17334
|
}
|
|
17659
17335
|
const requestedTimeout = typeof timeout_seconds === "number" ? timeout_seconds : 300;
|
|
17660
17336
|
const timeoutSec = Math.max(10, Math.min(3600, requestedTimeout));
|
|
@@ -17687,7 +17363,7 @@ async function handleAskUser(args) {
|
|
|
17687
17363
|
expiresAt
|
|
17688
17364
|
});
|
|
17689
17365
|
} catch (err) {
|
|
17690
|
-
return
|
|
17366
|
+
return errResult(`Failed to register pending interaction: ${err.message}`);
|
|
17691
17367
|
}
|
|
17692
17368
|
const slackResult = await postSlackMessageWithTs({
|
|
17693
17369
|
channel,
|
|
@@ -17697,7 +17373,7 @@ async function handleAskUser(args) {
|
|
|
17697
17373
|
...thread_ts ? { thread_ts } : {}
|
|
17698
17374
|
});
|
|
17699
17375
|
if (!slackResult.ok || !slackResult.ts) {
|
|
17700
|
-
return
|
|
17376
|
+
return errResult(`Slack chat.postMessage failed: ${slackResult.error ?? "unknown"}`);
|
|
17701
17377
|
}
|
|
17702
17378
|
try {
|
|
17703
17379
|
await runtime.updatePendingInteractionMessageTs(cfg, callbackId, slackResult.ts);
|
|
@@ -17744,7 +17420,7 @@ async function handleAskUser(args) {
|
|
|
17744
17420
|
}
|
|
17745
17421
|
async function handleChannelRequestInput(args) {
|
|
17746
17422
|
if (!askUserToolAvailable()) {
|
|
17747
|
-
return
|
|
17423
|
+
return errResult(
|
|
17748
17424
|
"channel_request_input is disabled or the host MCP is missing AGT_HOST/AGT_API_KEY/AGT_AGENT_ID env wiring."
|
|
17749
17425
|
);
|
|
17750
17426
|
}
|
|
@@ -17752,10 +17428,10 @@ async function handleChannelRequestInput(args) {
|
|
|
17752
17428
|
const { apiCall: apiCall2, waitForResolution: waitForResolution2, validateAskUserOptions: validateAskUserOptions2 } = runtime;
|
|
17753
17429
|
const { thread_id, prompt_text, options, schema, request_id, timeout_seconds } = args;
|
|
17754
17430
|
if (typeof thread_id !== "string" || !thread_id) {
|
|
17755
|
-
return
|
|
17431
|
+
return errResult("thread_id is required (shape: `<channel_id>:<thread_ts>`)");
|
|
17756
17432
|
}
|
|
17757
17433
|
if (typeof prompt_text !== "string" || !prompt_text) {
|
|
17758
|
-
return
|
|
17434
|
+
return errResult("prompt_text is required");
|
|
17759
17435
|
}
|
|
17760
17436
|
const optsValidation = validateAskUserOptions2(options, {
|
|
17761
17437
|
maxOptions: 4,
|
|
@@ -17763,25 +17439,25 @@ async function handleChannelRequestInput(args) {
|
|
|
17763
17439
|
maxValueChars: 2e3
|
|
17764
17440
|
});
|
|
17765
17441
|
if (!optsValidation.ok) {
|
|
17766
|
-
return
|
|
17442
|
+
return errResult(
|
|
17767
17443
|
`Invalid options: ${optsValidation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`
|
|
17768
17444
|
);
|
|
17769
17445
|
}
|
|
17770
17446
|
const optionsArr = options;
|
|
17771
17447
|
if (optionsArr.length < 2) {
|
|
17772
|
-
return
|
|
17448
|
+
return errResult("options must contain at least 2 entries (this is a picker, not a notification)");
|
|
17773
17449
|
}
|
|
17774
17450
|
if (!schema || typeof schema.type !== "string") {
|
|
17775
|
-
return
|
|
17451
|
+
return errResult("schema is required with shape { type: 'yes_no' | 'choice' }");
|
|
17776
17452
|
}
|
|
17777
17453
|
if (schema.type !== "yes_no" && schema.type !== "choice") {
|
|
17778
|
-
return
|
|
17454
|
+
return errResult("schema.type must be 'yes_no' or 'choice'");
|
|
17779
17455
|
}
|
|
17780
17456
|
if (schema.type === "yes_no" && optionsArr.length !== 2) {
|
|
17781
|
-
return
|
|
17457
|
+
return errResult("schema.type='yes_no' requires exactly 2 options");
|
|
17782
17458
|
}
|
|
17783
17459
|
if (request_id !== void 0 && (typeof request_id !== "string" || !request_id)) {
|
|
17784
|
-
return
|
|
17460
|
+
return errResult("request_id must be a non-empty string when provided");
|
|
17785
17461
|
}
|
|
17786
17462
|
const requestedTimeout = typeof timeout_seconds === "number" ? timeout_seconds : 300;
|
|
17787
17463
|
const timeoutSec = Math.max(10, Math.min(3600, requestedTimeout));
|
|
@@ -17803,10 +17479,10 @@ async function handleChannelRequestInput(args) {
|
|
|
17803
17479
|
dispatchPayload = await res.json().catch(() => ({}));
|
|
17804
17480
|
if (!res.ok || !dispatchPayload.ok || !dispatchPayload.callback_id) {
|
|
17805
17481
|
const reason = dispatchPayload.reason ?? `http_${res.status}`;
|
|
17806
|
-
return
|
|
17482
|
+
return errResult(`channel_request_input dispatch failed: ${reason}`);
|
|
17807
17483
|
}
|
|
17808
17484
|
} catch (err) {
|
|
17809
|
-
return
|
|
17485
|
+
return errResult(
|
|
17810
17486
|
`channel_request_input dispatch failed: ${err.message}`
|
|
17811
17487
|
);
|
|
17812
17488
|
}
|
|
@@ -17844,7 +17520,7 @@ async function handleChannelRequestInput(args) {
|
|
|
17844
17520
|
]
|
|
17845
17521
|
};
|
|
17846
17522
|
}
|
|
17847
|
-
function
|
|
17523
|
+
function errResult(text) {
|
|
17848
17524
|
return { content: [{ type: "text", text }], isError: true };
|
|
17849
17525
|
}
|
|
17850
17526
|
var MAX_INBOUND_FILE_BYTES = 25 * 1024 * 1024;
|
|
@@ -18226,19 +17902,29 @@ async function connectSocketMode() {
|
|
|
18226
17902
|
isAutoFollowed,
|
|
18227
17903
|
botUserId
|
|
18228
17904
|
});
|
|
18229
|
-
|
|
17905
|
+
const ackProbe = process.env.TMUX && AGENT_CODE_NAME ? probeAgentSessionCached(AGENT_CODE_NAME) : { tmux: "unknown", claude: "unknown" };
|
|
17906
|
+
const ackDecision = decideAckReaction({
|
|
17907
|
+
hasTarget: decideSlackAckReaction({ channel, ts }),
|
|
17908
|
+
integrationReady: Boolean(BOT_TOKEN),
|
|
17909
|
+
tmux: ackProbe.tmux,
|
|
17910
|
+
claude: ackProbe.claude,
|
|
17911
|
+
withinStartupGrace: process.uptime() * 1e3 < ACK_STARTUP_GRACE_MS,
|
|
17912
|
+
oldestPendingAgeMs: oldestPendingMarkerAgeMs(SLACK_PENDING_INBOUND_DIR)
|
|
17913
|
+
});
|
|
17914
|
+
if (ackDecision !== "none") {
|
|
17915
|
+
const reactionName = ackDecision === "undeliverable" ? "x" : "eyes";
|
|
18230
17916
|
fetch("https://slack.com/api/reactions.add", {
|
|
18231
17917
|
method: "POST",
|
|
18232
17918
|
headers: {
|
|
18233
17919
|
"Content-Type": "application/json",
|
|
18234
17920
|
Authorization: `Bearer ${BOT_TOKEN}`
|
|
18235
17921
|
},
|
|
18236
|
-
body: JSON.stringify({ channel, timestamp: ts, name:
|
|
17922
|
+
body: JSON.stringify({ channel, timestamp: ts, name: reactionName })
|
|
18237
17923
|
}).catch(() => {
|
|
18238
17924
|
});
|
|
18239
17925
|
}
|
|
18240
17926
|
if (channel && ts && shouldEngage) {
|
|
18241
|
-
trackPendingMessage(channel, threadTs, ts);
|
|
17927
|
+
trackPendingMessage(channel, threadTs, ts, ackDecision === "undeliverable");
|
|
18242
17928
|
}
|
|
18243
17929
|
const userName = await resolveUserName(user);
|
|
18244
17930
|
const fileMeta = await buildInboundFileMeta(evt.files, AGENT_CODE_NAME, channel);
|
|
@@ -18246,6 +17932,29 @@ async function connectSocketMode() {
|
|
|
18246
17932
|
(f) => f.kind === "image" && typeof f.path === "string"
|
|
18247
17933
|
);
|
|
18248
17934
|
const imagePath = downloadedImages.length === 1 ? downloadedImages[0].path : void 0;
|
|
17935
|
+
let threadContext;
|
|
17936
|
+
if (isThreadReply && channel && threadTs) {
|
|
17937
|
+
try {
|
|
17938
|
+
const transcript = await fetchThreadTranscript(
|
|
17939
|
+
channel,
|
|
17940
|
+
threadTs,
|
|
17941
|
+
SLACK_AUTOLOAD_THREAD_LIMIT,
|
|
17942
|
+
{ botToken: BOT_TOKEN, resolveUserName }
|
|
17943
|
+
);
|
|
17944
|
+
if (transcript.ok && transcript.count >= 2) {
|
|
17945
|
+
threadContext = capThreadContext(
|
|
17946
|
+
transcript.formatted,
|
|
17947
|
+
SLACK_AUTOLOAD_THREAD_MAX_CHARS
|
|
17948
|
+
);
|
|
17949
|
+
}
|
|
17950
|
+
} catch (err) {
|
|
17951
|
+
const msg2 = err instanceof Error ? err.message : String(err);
|
|
17952
|
+
process.stderr.write(
|
|
17953
|
+
`slack-channel(${AGENT_CODE_NAME}): thread_context fetch failed (channel=${redactSlackId(channel)} thread=${redactSlackId(threadTs)}): ${msg2}
|
|
17954
|
+
`
|
|
17955
|
+
);
|
|
17956
|
+
}
|
|
17957
|
+
}
|
|
18249
17958
|
await mcp.notification({
|
|
18250
17959
|
method: "notifications/claude/channel",
|
|
18251
17960
|
params: {
|
|
@@ -18260,7 +17969,9 @@ async function connectSocketMode() {
|
|
|
18260
17969
|
// Only set these when we actually have attachments to avoid
|
|
18261
17970
|
// bloating every notification with empty metadata.
|
|
18262
17971
|
...fileMeta.length > 0 ? { files: JSON.stringify(fileMeta) } : {},
|
|
18263
|
-
...imagePath ? { image_path: imagePath } : {}
|
|
17972
|
+
...imagePath ? { image_path: imagePath } : {},
|
|
17973
|
+
// ENG-5830: the pre-loaded surrounding thread (thread replies only).
|
|
17974
|
+
...threadContext ? { thread_context: threadContext } : {}
|
|
18264
17975
|
}
|
|
18265
17976
|
}
|
|
18266
17977
|
});
|