@integrity-labs/agt-cli 0.27.7-test.6 → 0.27.8-test.10
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 +146 -37
- package/dist/bin/agt.js.map +1 -1
- package/dist/{chunk-AACMX6LE.js → chunk-2E5OABKX.js} +241 -61
- package/dist/chunk-2E5OABKX.js.map +1 -0
- package/dist/{chunk-YSBGIXJG.js → chunk-6HFXSNNY.js} +31 -18
- package/dist/chunk-6HFXSNNY.js.map +1 -0
- package/dist/{chunk-GN4XPQWJ.js → chunk-JIETOJQV.js} +37 -28
- package/dist/chunk-JIETOJQV.js.map +1 -0
- package/dist/{claude-pair-runtime-ZBQKBBMT.js → claude-pair-runtime-3UHTNKZ4.js} +2 -2
- package/dist/lib/manager-worker.js +536 -140
- 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 +691 -638
- package/dist/mcp/telegram-channel.js +257 -513
- package/dist/{persistent-session-ICYFLUAM.js → persistent-session-HSHIZC2Q.js} +3 -3
- package/dist/{responsiveness-probe-WZNQ2762.js → responsiveness-probe-RYKS5TRP.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 +0 -1
- /package/dist/{claude-pair-runtime-ZBQKBBMT.js.map → claude-pair-runtime-3UHTNKZ4.js.map} +0 -0
- /package/dist/{persistent-session-ICYFLUAM.js.map → persistent-session-HSHIZC2Q.js.map} +0 -0
- /package/dist/{responsiveness-probe-WZNQ2762.js.map → responsiveness-probe-RYKS5TRP.js.map} +0 -0
|
@@ -14237,17 +14237,184 @@ function extractAugmentedSlackLabel(evt) {
|
|
|
14237
14237
|
agentId: evt.metadata.event_payload?.["augmented_agent_id"]
|
|
14238
14238
|
};
|
|
14239
14239
|
}
|
|
14240
|
-
function
|
|
14240
|
+
function decideModeForward(evt, policy) {
|
|
14241
14241
|
if (policy.mode === "all") return { forward: true };
|
|
14242
|
+
if (policy.mode === "manager_only") {
|
|
14243
|
+
if (policy.principalSlackUserId && evt.user === policy.principalSlackUserId) {
|
|
14244
|
+
return { forward: true };
|
|
14245
|
+
}
|
|
14246
|
+
}
|
|
14242
14247
|
const label = extractAugmentedSlackLabel(evt);
|
|
14243
14248
|
if (!label) return { forward: false, reason: "sender_policy" };
|
|
14244
|
-
if (policy.mode === "team_agents_only") {
|
|
14249
|
+
if (policy.mode === "team_agents_only" || policy.mode === "manager_only") {
|
|
14245
14250
|
if (!policy.teamId || label.teamId !== policy.teamId) {
|
|
14246
14251
|
return { forward: false, reason: "sender_policy" };
|
|
14247
14252
|
}
|
|
14248
14253
|
}
|
|
14249
14254
|
return { forward: true };
|
|
14250
14255
|
}
|
|
14256
|
+
function decideSenderPolicyForward(evt, policy) {
|
|
14257
|
+
if (policy.internalOnly) {
|
|
14258
|
+
if (!policy.homeTeamId) return { forward: false, reason: "sender_policy" };
|
|
14259
|
+
if (evt.team !== policy.homeTeamId) return { forward: false, reason: "sender_policy" };
|
|
14260
|
+
}
|
|
14261
|
+
return decideModeForward(evt, policy);
|
|
14262
|
+
}
|
|
14263
|
+
|
|
14264
|
+
// src/sender-policy-decline.ts
|
|
14265
|
+
function classifySlackPolicyBlock(evt, policy) {
|
|
14266
|
+
if (policy.internalOnly && (!policy.homeTeamId || evt.team !== policy.homeTeamId)) {
|
|
14267
|
+
return "internal_only";
|
|
14268
|
+
}
|
|
14269
|
+
switch (policy.mode) {
|
|
14270
|
+
case "manager_only":
|
|
14271
|
+
return "mode_manager_only";
|
|
14272
|
+
case "team_agents_only":
|
|
14273
|
+
return "mode_team_agents_only";
|
|
14274
|
+
case "agents_only":
|
|
14275
|
+
return "mode_agents_only";
|
|
14276
|
+
case "all":
|
|
14277
|
+
return "internal_only";
|
|
14278
|
+
}
|
|
14279
|
+
}
|
|
14280
|
+
function politeDeclineCopy(reason) {
|
|
14281
|
+
switch (reason) {
|
|
14282
|
+
case "internal_only":
|
|
14283
|
+
return "Sorry, I can only respond to people inside our organisation. This conversation appears to be from outside.";
|
|
14284
|
+
case "mode_manager_only":
|
|
14285
|
+
return "Sorry, I only respond to my manager in this channel.";
|
|
14286
|
+
case "mode_team_agents_only":
|
|
14287
|
+
return "Sorry, I only respond to agents on my team in this channel.";
|
|
14288
|
+
case "mode_agents_only":
|
|
14289
|
+
return "Sorry, I only respond to other agents in this channel.";
|
|
14290
|
+
}
|
|
14291
|
+
}
|
|
14292
|
+
function decideDeclineReply(input) {
|
|
14293
|
+
const last = input.cache.get(input.key);
|
|
14294
|
+
if (last !== void 0) {
|
|
14295
|
+
const elapsed = input.now - last;
|
|
14296
|
+
if (elapsed < input.cooldownMs) {
|
|
14297
|
+
return { reply: false, remainingMs: input.cooldownMs - elapsed };
|
|
14298
|
+
}
|
|
14299
|
+
}
|
|
14300
|
+
input.cache.set(input.key, input.now);
|
|
14301
|
+
return { reply: true };
|
|
14302
|
+
}
|
|
14303
|
+
function declineCacheKey(channelId, senderId, reason) {
|
|
14304
|
+
return `${channelId}|${senderId}|${reason}`;
|
|
14305
|
+
}
|
|
14306
|
+
function readDeclineCooldownMs(envVarName, defaultSeconds = 1800) {
|
|
14307
|
+
const raw = process.env[envVarName];
|
|
14308
|
+
if (!raw) return defaultSeconds * 1e3;
|
|
14309
|
+
const parsed = Number(raw);
|
|
14310
|
+
if (!Number.isFinite(parsed) || parsed < 0) return defaultSeconds * 1e3;
|
|
14311
|
+
return parsed * 1e3;
|
|
14312
|
+
}
|
|
14313
|
+
|
|
14314
|
+
// src/ack-reaction.ts
|
|
14315
|
+
import { readdirSync, readFileSync } from "fs";
|
|
14316
|
+
import { join } from "path";
|
|
14317
|
+
var REPLY_WEDGED_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
14318
|
+
var ACK_STARTUP_GRACE_MS = 6e4;
|
|
14319
|
+
var ACK_PANE_FRESH_THRESHOLD_MS = 6e4;
|
|
14320
|
+
function decideAckReaction(i) {
|
|
14321
|
+
if (!i.hasTarget) return "none";
|
|
14322
|
+
if (!i.integrationReady) return "undeliverable";
|
|
14323
|
+
if (i.tmux === "dead") return "undeliverable";
|
|
14324
|
+
if (!i.withinStartupGrace && i.claude === "dead") return "undeliverable";
|
|
14325
|
+
const threshold = i.pendingStaleThresholdMs ?? REPLY_WEDGED_THRESHOLD_MS;
|
|
14326
|
+
if (i.oldestPendingAgeMs != null && i.oldestPendingAgeMs > threshold) {
|
|
14327
|
+
const paneFreshThreshold = i.paneFreshThresholdMs ?? ACK_PANE_FRESH_THRESHOLD_MS;
|
|
14328
|
+
const paneIsFresh = i.paneLogFreshAgeMs != null && i.paneLogFreshAgeMs <= paneFreshThreshold;
|
|
14329
|
+
if (paneIsFresh && i.tmux === "alive" && i.claude === "alive") {
|
|
14330
|
+
return "ack";
|
|
14331
|
+
}
|
|
14332
|
+
return "undeliverable";
|
|
14333
|
+
}
|
|
14334
|
+
return "ack";
|
|
14335
|
+
}
|
|
14336
|
+
function decideRecoveryHeal(i) {
|
|
14337
|
+
return i.wasUndeliverable && i.hasTarget ? "heal" : "none";
|
|
14338
|
+
}
|
|
14339
|
+
function oldestPendingMarkerAgeMs(dir, now = Date.now()) {
|
|
14340
|
+
if (!dir) return null;
|
|
14341
|
+
let names;
|
|
14342
|
+
try {
|
|
14343
|
+
names = readdirSync(dir);
|
|
14344
|
+
} catch {
|
|
14345
|
+
return null;
|
|
14346
|
+
}
|
|
14347
|
+
let oldest = null;
|
|
14348
|
+
for (const name of names) {
|
|
14349
|
+
if (!name.endsWith(".json")) continue;
|
|
14350
|
+
let receivedAt;
|
|
14351
|
+
try {
|
|
14352
|
+
const raw = JSON.parse(readFileSync(join(dir, name), "utf-8"));
|
|
14353
|
+
receivedAt = raw.received_at;
|
|
14354
|
+
} catch {
|
|
14355
|
+
continue;
|
|
14356
|
+
}
|
|
14357
|
+
if (!receivedAt) continue;
|
|
14358
|
+
const t = Date.parse(receivedAt);
|
|
14359
|
+
if (Number.isNaN(t)) continue;
|
|
14360
|
+
const age = now - t;
|
|
14361
|
+
if (age < 0) continue;
|
|
14362
|
+
if (oldest == null || age > oldest) oldest = age;
|
|
14363
|
+
}
|
|
14364
|
+
return oldest;
|
|
14365
|
+
}
|
|
14366
|
+
|
|
14367
|
+
// src/session-probe-runtime.ts
|
|
14368
|
+
import { execFileSync } from "child_process";
|
|
14369
|
+
function agentTmuxSessionName(codeName) {
|
|
14370
|
+
return `agt-${codeName}`;
|
|
14371
|
+
}
|
|
14372
|
+
function escapePgrepRegex(value) {
|
|
14373
|
+
return value.replace(/[.[\]{}()*+?^$|\\]/g, "\\$&");
|
|
14374
|
+
}
|
|
14375
|
+
function probeClaudeProcessInTmux(tmuxSession) {
|
|
14376
|
+
const escapedSession = escapePgrepRegex(tmuxSession);
|
|
14377
|
+
const pattern = `(^|[[:space:]])--name ${escapedSession}([[:space:]]|$)`;
|
|
14378
|
+
try {
|
|
14379
|
+
const out = execFileSync("pgrep", ["-f", "--", pattern], {
|
|
14380
|
+
encoding: "utf-8",
|
|
14381
|
+
timeout: 3e3
|
|
14382
|
+
}).trim();
|
|
14383
|
+
return out.length > 0 ? "alive" : "dead";
|
|
14384
|
+
} catch (err) {
|
|
14385
|
+
const e = err;
|
|
14386
|
+
if (e?.code === "ENOENT") return "unknown";
|
|
14387
|
+
return e?.status === 1 ? "dead" : "unknown";
|
|
14388
|
+
}
|
|
14389
|
+
}
|
|
14390
|
+
function probeTmuxSession(tmuxSession) {
|
|
14391
|
+
try {
|
|
14392
|
+
execFileSync("tmux", ["has-session", "-t", tmuxSession], {
|
|
14393
|
+
stdio: "ignore",
|
|
14394
|
+
timeout: 3e3
|
|
14395
|
+
});
|
|
14396
|
+
return "alive";
|
|
14397
|
+
} catch (err) {
|
|
14398
|
+
const e = err;
|
|
14399
|
+
if (e?.code === "ENOENT") return "unknown";
|
|
14400
|
+
return "dead";
|
|
14401
|
+
}
|
|
14402
|
+
}
|
|
14403
|
+
function probeAgentSession(codeName) {
|
|
14404
|
+
const session = agentTmuxSessionName(codeName);
|
|
14405
|
+
const tmux = probeTmuxSession(session);
|
|
14406
|
+
const claude = tmux === "alive" ? probeClaudeProcessInTmux(session) : tmux;
|
|
14407
|
+
return { tmux, claude };
|
|
14408
|
+
}
|
|
14409
|
+
var probeCache = /* @__PURE__ */ new Map();
|
|
14410
|
+
var SESSION_PROBE_TTL_MS = 15e3;
|
|
14411
|
+
function probeAgentSessionCached(codeName, ttlMs = SESSION_PROBE_TTL_MS, now = Date.now()) {
|
|
14412
|
+
const cached2 = probeCache.get(codeName);
|
|
14413
|
+
if (cached2 && now - cached2.at < ttlMs) return cached2.value;
|
|
14414
|
+
const value = probeAgentSession(codeName);
|
|
14415
|
+
probeCache.set(codeName, { at: now, value });
|
|
14416
|
+
return value;
|
|
14417
|
+
}
|
|
14251
14418
|
|
|
14252
14419
|
// src/slack-loop-throttle.ts
|
|
14253
14420
|
var DEFAULT_THROTTLE = {
|
|
@@ -14351,466 +14518,62 @@ async function isThreadKilled(opts) {
|
|
|
14351
14518
|
}
|
|
14352
14519
|
}
|
|
14353
14520
|
|
|
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
|
-
}
|
|
14521
|
+
// src/slack-thread-context.ts
|
|
14522
|
+
var SLACK_AUTOLOAD_THREAD_LIMIT = 30;
|
|
14523
|
+
var SLACK_AUTOLOAD_THREAD_MAX_CHARS = 4e3;
|
|
14524
|
+
function formatThreadMessages(messages, nameById) {
|
|
14525
|
+
return messages.map((m) => `[${m.ts}] ${nameById.get(m.user) ?? m.user} (<@${m.user}>): ${m.text}`).join("\n");
|
|
14526
|
+
}
|
|
14527
|
+
function capThreadContext(formatted, maxChars) {
|
|
14528
|
+
if (formatted.length <= maxChars) return formatted;
|
|
14529
|
+
const marker = "[\u2026earlier thread messages omitted \u2014 slack.read_thread for full history\u2026]\n";
|
|
14530
|
+
if (maxChars <= marker.length) return marker.slice(0, maxChars);
|
|
14531
|
+
const tailBudget = maxChars - marker.length;
|
|
14532
|
+
const tail = formatted.slice(formatted.length - tailBudget);
|
|
14533
|
+
const firstNewline = tail.indexOf("\n");
|
|
14534
|
+
const clean = firstNewline >= 0 ? tail.slice(firstNewline + 1) : tail;
|
|
14535
|
+
return `${marker}${clean}`;
|
|
14536
|
+
}
|
|
14537
|
+
async function fetchThreadTranscript(channel, threadTs, limit, deps) {
|
|
14538
|
+
const { botToken, resolveUserName: resolveUserName2, fetchImpl = fetch } = deps;
|
|
14539
|
+
const cappedLimit = Math.min(Math.max(limit, 1), 200);
|
|
14540
|
+
const allMessages = [];
|
|
14541
|
+
let cursor;
|
|
14542
|
+
do {
|
|
14543
|
+
const remaining = cappedLimit - allMessages.length;
|
|
14544
|
+
if (remaining <= 0) break;
|
|
14545
|
+
const params = new URLSearchParams({
|
|
14546
|
+
channel,
|
|
14547
|
+
ts: threadTs,
|
|
14548
|
+
limit: String(Math.min(remaining, 100)),
|
|
14549
|
+
...cursor ? { cursor } : {}
|
|
14550
|
+
});
|
|
14551
|
+
let data;
|
|
14482
14552
|
try {
|
|
14483
|
-
await
|
|
14484
|
-
|
|
14485
|
-
ts: anchorTs,
|
|
14486
|
-
text: payload.text,
|
|
14487
|
-
blocks: payload.blocks
|
|
14553
|
+
const res = await fetchImpl(`https://slack.com/api/conversations.replies?${params}`, {
|
|
14554
|
+
headers: { Authorization: `Bearer ${botToken}` }
|
|
14488
14555
|
});
|
|
14556
|
+
if (!res.ok) return { ok: false, error: `http_${res.status}` };
|
|
14557
|
+
data = await res.json();
|
|
14489
14558
|
} 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(() => {
|
|
14559
|
+
return { ok: false, error: err instanceof Error ? err.message : "fetch_failed" };
|
|
14560
|
+
}
|
|
14561
|
+
if (!data.ok) return { ok: false, error: data.error ?? "unknown" };
|
|
14562
|
+
for (const msg of data.messages ?? []) {
|
|
14563
|
+
allMessages.push({
|
|
14564
|
+
user: msg.user ?? msg.bot_id ?? "unknown",
|
|
14565
|
+
text: msg.text ?? "",
|
|
14566
|
+
ts: msg.ts ?? ""
|
|
14566
14567
|
});
|
|
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
14568
|
}
|
|
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";
|
|
14569
|
+
cursor = data.response_metadata?.next_cursor || void 0;
|
|
14570
|
+
} while (cursor && allMessages.length < cappedLimit);
|
|
14571
|
+
const uniqueUserIds = [...new Set(allMessages.map((m) => m.user))];
|
|
14572
|
+
const resolved = await Promise.all(
|
|
14573
|
+
uniqueUserIds.map(async (id) => [id, await resolveUserName2(id)])
|
|
14574
|
+
);
|
|
14575
|
+
const nameById = new Map(resolved);
|
|
14576
|
+
return { ok: true, count: allMessages.length, formatted: formatThreadMessages(allMessages, nameById) };
|
|
14814
14577
|
}
|
|
14815
14578
|
|
|
14816
14579
|
// src/impersonation.ts
|
|
@@ -14860,6 +14623,69 @@ var SLACK_EGRESS_TOOLS = /* @__PURE__ */ new Set([
|
|
|
14860
14623
|
"slack.upload_file"
|
|
14861
14624
|
]);
|
|
14862
14625
|
|
|
14626
|
+
// src/slack-pending-inbound-cleanup.ts
|
|
14627
|
+
import { existsSync, readdirSync as readdirSync2, statSync, unlinkSync } from "fs";
|
|
14628
|
+
import { join as join2 } from "path";
|
|
14629
|
+
function sanitizeMarkerSegment(value) {
|
|
14630
|
+
return value.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
14631
|
+
}
|
|
14632
|
+
var defaultClearMarkerFile = (fullPath) => {
|
|
14633
|
+
try {
|
|
14634
|
+
if (existsSync(fullPath)) unlinkSync(fullPath);
|
|
14635
|
+
} catch {
|
|
14636
|
+
}
|
|
14637
|
+
};
|
|
14638
|
+
function clearAllSlackPendingMarkersForThread(dir, channel, threadTs, clear = defaultClearMarkerFile) {
|
|
14639
|
+
if (!dir) return 0;
|
|
14640
|
+
const prefix = `${sanitizeMarkerSegment(channel)}__${sanitizeMarkerSegment(threadTs)}__`;
|
|
14641
|
+
let cleared = 0;
|
|
14642
|
+
try {
|
|
14643
|
+
for (const f of readdirSync2(dir)) {
|
|
14644
|
+
if (!f.startsWith(prefix) || !f.endsWith(".json")) continue;
|
|
14645
|
+
clear(join2(dir, f));
|
|
14646
|
+
cleared += 1;
|
|
14647
|
+
}
|
|
14648
|
+
} catch {
|
|
14649
|
+
}
|
|
14650
|
+
return cleared;
|
|
14651
|
+
}
|
|
14652
|
+
function clearSlackPendingMarkerByMessageTs(dir, channel, messageTs, clear = defaultClearMarkerFile) {
|
|
14653
|
+
if (!dir) return 0;
|
|
14654
|
+
const channelPrefix = `${sanitizeMarkerSegment(channel)}__`;
|
|
14655
|
+
const messageSuffix = `__${sanitizeMarkerSegment(messageTs)}.json`;
|
|
14656
|
+
let cleared = 0;
|
|
14657
|
+
try {
|
|
14658
|
+
for (const f of readdirSync2(dir)) {
|
|
14659
|
+
if (!f.startsWith(channelPrefix) || !f.endsWith(messageSuffix)) continue;
|
|
14660
|
+
clear(join2(dir, f));
|
|
14661
|
+
cleared += 1;
|
|
14662
|
+
}
|
|
14663
|
+
} catch {
|
|
14664
|
+
}
|
|
14665
|
+
return cleared;
|
|
14666
|
+
}
|
|
14667
|
+
function clearOldestSlackPendingMarkerInChannel(dir, channel, clear = defaultClearMarkerFile) {
|
|
14668
|
+
if (!dir) return null;
|
|
14669
|
+
const channelPrefix = `${sanitizeMarkerSegment(channel)}__`;
|
|
14670
|
+
try {
|
|
14671
|
+
const entries = readdirSync2(dir).filter((f) => f.startsWith(channelPrefix) && f.endsWith(".json")).map((f) => {
|
|
14672
|
+
const full = join2(dir, f);
|
|
14673
|
+
let mtime = 0;
|
|
14674
|
+
try {
|
|
14675
|
+
mtime = statSync(full).mtimeMs;
|
|
14676
|
+
} catch {
|
|
14677
|
+
}
|
|
14678
|
+
return { name: f, full, mtime };
|
|
14679
|
+
}).filter((e) => e.mtime > 0).sort((a, b) => a.mtime - b.mtime);
|
|
14680
|
+
const oldest = entries[0];
|
|
14681
|
+
if (!oldest) return null;
|
|
14682
|
+
clear(oldest.full);
|
|
14683
|
+
return oldest.name;
|
|
14684
|
+
} catch {
|
|
14685
|
+
return null;
|
|
14686
|
+
}
|
|
14687
|
+
}
|
|
14688
|
+
|
|
14863
14689
|
// ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.27.1_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
|
|
14864
14690
|
import process2 from "process";
|
|
14865
14691
|
|
|
@@ -14956,22 +14782,22 @@ var StdioServerTransport = class {
|
|
|
14956
14782
|
import {
|
|
14957
14783
|
chmodSync,
|
|
14958
14784
|
createWriteStream,
|
|
14959
|
-
existsSync as
|
|
14785
|
+
existsSync as existsSync3,
|
|
14960
14786
|
mkdirSync as mkdirSync3,
|
|
14961
|
-
readFileSync as
|
|
14962
|
-
readdirSync,
|
|
14787
|
+
readFileSync as readFileSync4,
|
|
14788
|
+
readdirSync as readdirSync3,
|
|
14963
14789
|
renameSync as renameSync2,
|
|
14964
|
-
statSync,
|
|
14965
|
-
unlinkSync as
|
|
14790
|
+
statSync as statSync2,
|
|
14791
|
+
unlinkSync as unlinkSync3,
|
|
14966
14792
|
watch,
|
|
14967
14793
|
writeFileSync as writeFileSync3
|
|
14968
14794
|
} from "fs";
|
|
14969
|
-
import { basename, join as
|
|
14795
|
+
import { basename, join as join5, resolve as resolve2 } from "path";
|
|
14970
14796
|
import { homedir as homedir2 } from "os";
|
|
14971
|
-
import { createHash, randomUUID
|
|
14797
|
+
import { createHash, randomUUID } from "crypto";
|
|
14972
14798
|
|
|
14973
14799
|
// src/slack-thread-store.ts
|
|
14974
|
-
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
14800
|
+
import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
14975
14801
|
import { dirname } from "path";
|
|
14976
14802
|
var FILE_VERSION = 1;
|
|
14977
14803
|
var DEFAULT_TTL_DAYS = 30;
|
|
@@ -14982,7 +14808,7 @@ function loadThreadStore(filePath, opts = {}) {
|
|
|
14982
14808
|
const ttlMs = ttlDays * 24 * 60 * 60 * 1e3;
|
|
14983
14809
|
let raw;
|
|
14984
14810
|
try {
|
|
14985
|
-
raw =
|
|
14811
|
+
raw = readFileSync2(filePath, "utf-8");
|
|
14986
14812
|
} catch {
|
|
14987
14813
|
return { threads: /* @__PURE__ */ new Map(), pruned: 0 };
|
|
14988
14814
|
}
|
|
@@ -15097,9 +14923,9 @@ async function runOrRetry(fn, opts) {
|
|
|
15097
14923
|
|
|
15098
14924
|
// src/channel-attachments.ts
|
|
15099
14925
|
import { homedir } from "os";
|
|
15100
|
-
import { join, resolve, sep } from "path";
|
|
14926
|
+
import { join as join3, resolve, sep } from "path";
|
|
15101
14927
|
function resolveChannelInboundDir(codeName, channelSlug) {
|
|
15102
|
-
const base =
|
|
14928
|
+
const base = join3(homedir(), ".augmented");
|
|
15103
14929
|
const allowedSegment = /^[A-Za-z0-9_-]+$/;
|
|
15104
14930
|
if (!allowedSegment.test(codeName) || !allowedSegment.test(channelSlug)) {
|
|
15105
14931
|
throw new Error(
|
|
@@ -15743,14 +15569,14 @@ function createSlackBotUserIdClient(args) {
|
|
|
15743
15569
|
|
|
15744
15570
|
// src/mcp-spawn-lock.ts
|
|
15745
15571
|
import {
|
|
15746
|
-
existsSync,
|
|
15572
|
+
existsSync as existsSync2,
|
|
15747
15573
|
mkdirSync as mkdirSync2,
|
|
15748
|
-
readFileSync as
|
|
15574
|
+
readFileSync as readFileSync3,
|
|
15749
15575
|
renameSync,
|
|
15750
|
-
unlinkSync,
|
|
15576
|
+
unlinkSync as unlinkSync2,
|
|
15751
15577
|
writeFileSync as writeFileSync2
|
|
15752
15578
|
} from "fs";
|
|
15753
|
-
import { join as
|
|
15579
|
+
import { join as join4 } from "path";
|
|
15754
15580
|
function defaultIsPidAlive(pid) {
|
|
15755
15581
|
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
15756
15582
|
try {
|
|
@@ -15768,7 +15594,7 @@ function acquireMcpSpawnLock(args) {
|
|
|
15768
15594
|
const isPidAlive = options.isPidAlive ?? defaultIsPidAlive;
|
|
15769
15595
|
const selfPid = options.selfPid ?? process.pid;
|
|
15770
15596
|
const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
15771
|
-
const path =
|
|
15597
|
+
const path = join4(agentDir, basename2);
|
|
15772
15598
|
const existing = readLockHolder(path);
|
|
15773
15599
|
if (existing) {
|
|
15774
15600
|
if (existing.pid === selfPid) {
|
|
@@ -15792,14 +15618,14 @@ function releaseMcpSpawnLock(lockPath, opts = {}) {
|
|
|
15792
15618
|
if (!existing) return;
|
|
15793
15619
|
if (existing.pid !== selfPid) return;
|
|
15794
15620
|
try {
|
|
15795
|
-
|
|
15621
|
+
unlinkSync2(lockPath);
|
|
15796
15622
|
} catch {
|
|
15797
15623
|
}
|
|
15798
15624
|
}
|
|
15799
15625
|
function readLockHolder(path) {
|
|
15800
|
-
if (!
|
|
15626
|
+
if (!existsSync2(path)) return null;
|
|
15801
15627
|
try {
|
|
15802
|
-
const raw =
|
|
15628
|
+
const raw = readFileSync3(path, "utf8");
|
|
15803
15629
|
const parsed = JSON.parse(raw);
|
|
15804
15630
|
const pid = typeof parsed.pid === "number" ? parsed.pid : Number(parsed.pid);
|
|
15805
15631
|
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
@@ -15820,16 +15646,88 @@ var AGT_AGENT_ID = process.env.AGT_AGENT_ID ?? null;
|
|
|
15820
15646
|
var AGT_TEAM_ID = process.env.AGT_TEAM_ID ?? null;
|
|
15821
15647
|
var SLACK_SENDER_POLICY = (() => {
|
|
15822
15648
|
const raw = (process.env.SLACK_SENDER_POLICY ?? "all").trim().toLowerCase();
|
|
15823
|
-
|
|
15824
|
-
|
|
15649
|
+
const internalOnly = process.env.SLACK_INTERNAL_ONLY === "true";
|
|
15650
|
+
const homeTeamId = process.env.SLACK_HOME_TEAM_ID;
|
|
15651
|
+
if (internalOnly && !homeTeamId) {
|
|
15652
|
+
throw new Error(
|
|
15653
|
+
"SLACK_INTERNAL_ONLY=true requires SLACK_HOME_TEAM_ID. The bot install must have a known team_id (run auth.test against the workspace and persist the result)."
|
|
15654
|
+
);
|
|
15655
|
+
}
|
|
15656
|
+
const internalOnlyFields = internalOnly ? { internalOnly: true, homeTeamId } : {};
|
|
15657
|
+
if (raw === "all") return { mode: "all", ...internalOnlyFields };
|
|
15658
|
+
if (raw === "agents_only") return { mode: "agents_only", ...internalOnlyFields };
|
|
15825
15659
|
if (raw === "team_agents_only") {
|
|
15826
15660
|
if (!AGT_TEAM_ID) {
|
|
15827
15661
|
throw new Error("SLACK_SENDER_POLICY=team_agents_only requires AGT_TEAM_ID");
|
|
15828
15662
|
}
|
|
15829
|
-
return { mode: "team_agents_only", teamId: AGT_TEAM_ID };
|
|
15663
|
+
return { mode: "team_agents_only", teamId: AGT_TEAM_ID, ...internalOnlyFields };
|
|
15664
|
+
}
|
|
15665
|
+
if (raw === "manager_only") {
|
|
15666
|
+
const principalSlackUserId = process.env.SLACK_SENDER_POLICY_PRINCIPAL_ID;
|
|
15667
|
+
if (!AGT_TEAM_ID) {
|
|
15668
|
+
throw new Error("SLACK_SENDER_POLICY=manager_only requires AGT_TEAM_ID");
|
|
15669
|
+
}
|
|
15670
|
+
if (!principalSlackUserId) {
|
|
15671
|
+
throw new Error(
|
|
15672
|
+
"SLACK_SENDER_POLICY=manager_only requires SLACK_SENDER_POLICY_PRINCIPAL_ID. Set agent.reports_to_person with a slack_user_id in contact_preferences and re-run /host/refresh."
|
|
15673
|
+
);
|
|
15674
|
+
}
|
|
15675
|
+
return { mode: "manager_only", teamId: AGT_TEAM_ID, principalSlackUserId, ...internalOnlyFields };
|
|
15830
15676
|
}
|
|
15831
15677
|
throw new Error(`Invalid SLACK_SENDER_POLICY=${JSON.stringify(process.env.SLACK_SENDER_POLICY)}`);
|
|
15832
15678
|
})();
|
|
15679
|
+
var SLACK_POLICY_DECLINE_CACHE = /* @__PURE__ */ new Map();
|
|
15680
|
+
var SLACK_POLICY_DECLINE_COOLDOWN_MS = readDeclineCooldownMs(
|
|
15681
|
+
"SLACK_SENDER_POLICY_REPLY_COOLDOWN_SEC"
|
|
15682
|
+
);
|
|
15683
|
+
async function maybeSendSenderPolicyDecline(args) {
|
|
15684
|
+
if (!BOT_TOKEN) return;
|
|
15685
|
+
if (!args.channel || !args.senderId) return;
|
|
15686
|
+
const key2 = declineCacheKey(args.channel, args.senderId, args.subReason);
|
|
15687
|
+
const decision = decideDeclineReply({
|
|
15688
|
+
cache: SLACK_POLICY_DECLINE_CACHE,
|
|
15689
|
+
key: key2,
|
|
15690
|
+
cooldownMs: SLACK_POLICY_DECLINE_COOLDOWN_MS,
|
|
15691
|
+
now: Date.now()
|
|
15692
|
+
});
|
|
15693
|
+
if (!decision.reply) {
|
|
15694
|
+
process.stderr.write(
|
|
15695
|
+
`slack-channel(${AGENT_CODE_NAME}): decline suppressed by cooldown (channel=${redactSlackId(args.channel)}, sender=${redactSlackId(args.senderId)}, reason=${args.subReason}, remaining=${decision.remainingMs}ms)
|
|
15696
|
+
`
|
|
15697
|
+
);
|
|
15698
|
+
return;
|
|
15699
|
+
}
|
|
15700
|
+
const text = politeDeclineCopy(args.subReason);
|
|
15701
|
+
const controller = new AbortController();
|
|
15702
|
+
const timeoutId = setTimeout(() => controller.abort(), 1e4);
|
|
15703
|
+
try {
|
|
15704
|
+
const res = await fetch("https://slack.com/api/chat.postMessage", {
|
|
15705
|
+
method: "POST",
|
|
15706
|
+
signal: controller.signal,
|
|
15707
|
+
headers: {
|
|
15708
|
+
"Content-Type": "application/json",
|
|
15709
|
+
Authorization: `Bearer ${BOT_TOKEN}`
|
|
15710
|
+
},
|
|
15711
|
+
body: JSON.stringify({
|
|
15712
|
+
channel: args.channel,
|
|
15713
|
+
text,
|
|
15714
|
+
// Reply in-thread if the inbound was a thread message — keeps
|
|
15715
|
+
// the decline next to the message that caused it. For top-level
|
|
15716
|
+
// posts (DMs / channel root) omit thread_ts and post inline.
|
|
15717
|
+
...args.threadTs ? { thread_ts: args.threadTs } : {}
|
|
15718
|
+
})
|
|
15719
|
+
});
|
|
15720
|
+
const data = await res.json();
|
|
15721
|
+
if (!data.ok) {
|
|
15722
|
+
process.stderr.write(
|
|
15723
|
+
`slack-channel(${AGENT_CODE_NAME}): decline post failed: ${data.error ?? "unknown"}
|
|
15724
|
+
`
|
|
15725
|
+
);
|
|
15726
|
+
}
|
|
15727
|
+
} finally {
|
|
15728
|
+
clearTimeout(timeoutId);
|
|
15729
|
+
}
|
|
15730
|
+
}
|
|
15833
15731
|
var BLOCK_KIT_ENABLED = process.env.SLACK_BLOCK_KIT_ENABLED === "true";
|
|
15834
15732
|
var BLOCK_KIT_ASK_USER_ENABLED = process.env.SLACK_BLOCK_KIT_ASK_USER_ENABLED === "true";
|
|
15835
15733
|
var BLOCK_KIT_DISABLED = process.env.SLACK_BLOCK_KIT_DISABLED === "true";
|
|
@@ -15881,9 +15779,9 @@ var SLACK_PEER_CLASSIFIER_CONFIG = {
|
|
|
15881
15779
|
peers: parsePeersEnv(process.env.SLACK_PEERS, process.env.SLACK_PEERS_GATE),
|
|
15882
15780
|
peer_disabled_mode: SLACK_PEER_DISABLED_MODE
|
|
15883
15781
|
};
|
|
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 ?
|
|
15782
|
+
var SLACK_AGENT_DIR = AGENT_CODE_NAME ? join5(homedir2(), ".augmented", AGENT_CODE_NAME) : null;
|
|
15783
|
+
var SLACK_PENDING_INBOUND_DIR = SLACK_AGENT_DIR ? join5(SLACK_AGENT_DIR, "slack-pending-inbound") : null;
|
|
15784
|
+
var SLACK_RECOVERY_OUTBOX_DIR = SLACK_AGENT_DIR ? join5(SLACK_AGENT_DIR, "slack-recovery-outbox") : null;
|
|
15887
15785
|
var SLACK_MAX_RECOVERY_ATTEMPTS = 3;
|
|
15888
15786
|
function redactSlackId(id) {
|
|
15889
15787
|
if (!id) return "<none>";
|
|
@@ -15895,16 +15793,18 @@ function safeSlackMarkerName(channel, threadTs, messageTs) {
|
|
|
15895
15793
|
}
|
|
15896
15794
|
function slackPendingInboundPath(channel, threadTs, messageTs) {
|
|
15897
15795
|
if (!SLACK_PENDING_INBOUND_DIR) return null;
|
|
15898
|
-
return
|
|
15796
|
+
return join5(SLACK_PENDING_INBOUND_DIR, safeSlackMarkerName(channel, threadTs, messageTs));
|
|
15899
15797
|
}
|
|
15900
|
-
function writeSlackPendingInboundMarker(channel, threadTs, messageTs) {
|
|
15798
|
+
function writeSlackPendingInboundMarker(channel, threadTs, messageTs, undeliverable = false) {
|
|
15901
15799
|
const path = slackPendingInboundPath(channel, threadTs, messageTs);
|
|
15902
15800
|
if (!path || !SLACK_PENDING_INBOUND_DIR) return;
|
|
15903
15801
|
const marker = {
|
|
15904
15802
|
channel,
|
|
15905
15803
|
thread_ts: threadTs,
|
|
15906
15804
|
message_ts: messageTs,
|
|
15907
|
-
received_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
15805
|
+
received_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15806
|
+
// Only persist the flag when set — keeps healthy-path markers byte-identical.
|
|
15807
|
+
...undeliverable ? { undeliverable: true } : {}
|
|
15908
15808
|
};
|
|
15909
15809
|
try {
|
|
15910
15810
|
mkdirSync3(SLACK_PENDING_INBOUND_DIR, { recursive: true, mode: 448 });
|
|
@@ -15916,21 +15816,65 @@ function writeSlackPendingInboundMarker(channel, threadTs, messageTs) {
|
|
|
15916
15816
|
);
|
|
15917
15817
|
}
|
|
15918
15818
|
}
|
|
15919
|
-
function
|
|
15920
|
-
if (!
|
|
15921
|
-
const
|
|
15922
|
-
|
|
15923
|
-
|
|
15819
|
+
function healSlackUndeliverable(channel, messageTs) {
|
|
15820
|
+
if (!BOT_TOKEN || !channel || !messageTs) return;
|
|
15821
|
+
const headers = {
|
|
15822
|
+
"Content-Type": "application/json",
|
|
15823
|
+
Authorization: `Bearer ${BOT_TOKEN}`
|
|
15824
|
+
};
|
|
15825
|
+
fetch("https://slack.com/api/reactions.remove", {
|
|
15826
|
+
method: "POST",
|
|
15827
|
+
headers,
|
|
15828
|
+
body: JSON.stringify({ channel, timestamp: messageTs, name: "x" })
|
|
15829
|
+
}).catch(() => {
|
|
15830
|
+
}).finally(() => {
|
|
15831
|
+
fetch("https://slack.com/api/reactions.add", {
|
|
15832
|
+
method: "POST",
|
|
15833
|
+
headers,
|
|
15834
|
+
body: JSON.stringify({ channel, timestamp: messageTs, name: "eyes" })
|
|
15835
|
+
}).catch(() => {
|
|
15836
|
+
});
|
|
15837
|
+
});
|
|
15838
|
+
}
|
|
15839
|
+
function clearSlackMarkerFileWithHeal(fullPath) {
|
|
15840
|
+
let marker = null;
|
|
15924
15841
|
try {
|
|
15925
|
-
|
|
15926
|
-
if (!f.startsWith(prefix) || !f.endsWith(".json")) continue;
|
|
15927
|
-
try {
|
|
15928
|
-
unlinkSync2(join3(SLACK_PENDING_INBOUND_DIR, f));
|
|
15929
|
-
} catch {
|
|
15930
|
-
}
|
|
15931
|
-
}
|
|
15842
|
+
marker = JSON.parse(readFileSync4(fullPath, "utf-8"));
|
|
15932
15843
|
} catch {
|
|
15933
15844
|
}
|
|
15845
|
+
if (marker && decideRecoveryHeal({
|
|
15846
|
+
wasUndeliverable: marker.undeliverable === true,
|
|
15847
|
+
hasTarget: Boolean(marker.channel && marker.message_ts)
|
|
15848
|
+
}) === "heal") {
|
|
15849
|
+
healSlackUndeliverable(marker.channel, marker.message_ts);
|
|
15850
|
+
}
|
|
15851
|
+
try {
|
|
15852
|
+
if (existsSync3(fullPath)) unlinkSync3(fullPath);
|
|
15853
|
+
} catch {
|
|
15854
|
+
}
|
|
15855
|
+
}
|
|
15856
|
+
function clearAllSlackPendingMarkersForThread2(channel, threadTs) {
|
|
15857
|
+
clearAllSlackPendingMarkersForThread(
|
|
15858
|
+
SLACK_PENDING_INBOUND_DIR,
|
|
15859
|
+
channel,
|
|
15860
|
+
threadTs,
|
|
15861
|
+
clearSlackMarkerFileWithHeal
|
|
15862
|
+
);
|
|
15863
|
+
}
|
|
15864
|
+
function clearSlackPendingMarkerByMessageTs2(channel, messageTs) {
|
|
15865
|
+
clearSlackPendingMarkerByMessageTs(
|
|
15866
|
+
SLACK_PENDING_INBOUND_DIR,
|
|
15867
|
+
channel,
|
|
15868
|
+
messageTs,
|
|
15869
|
+
clearSlackMarkerFileWithHeal
|
|
15870
|
+
);
|
|
15871
|
+
}
|
|
15872
|
+
function clearOldestSlackPendingMarkerInChannel2(channel) {
|
|
15873
|
+
clearOldestSlackPendingMarkerInChannel(
|
|
15874
|
+
SLACK_PENDING_INBOUND_DIR,
|
|
15875
|
+
channel,
|
|
15876
|
+
clearSlackMarkerFileWithHeal
|
|
15877
|
+
);
|
|
15934
15878
|
}
|
|
15935
15879
|
function slackNextRetryName(filename) {
|
|
15936
15880
|
const match = filename.match(/^(.*?)(?:\.retry-(\d+))?\.json$/);
|
|
@@ -15946,10 +15890,10 @@ function slackNextRetryName(filename) {
|
|
|
15946
15890
|
async function processSlackRecoveryOutboxFile(filename) {
|
|
15947
15891
|
if (!SLACK_RECOVERY_OUTBOX_DIR) return;
|
|
15948
15892
|
if (filename.endsWith(".poison.json") || filename.endsWith(".tmp")) return;
|
|
15949
|
-
const fullPath =
|
|
15893
|
+
const fullPath = join5(SLACK_RECOVERY_OUTBOX_DIR, filename);
|
|
15950
15894
|
let payload;
|
|
15951
15895
|
try {
|
|
15952
|
-
payload = JSON.parse(
|
|
15896
|
+
payload = JSON.parse(readFileSync4(fullPath, "utf-8"));
|
|
15953
15897
|
} catch (err) {
|
|
15954
15898
|
process.stderr.write(
|
|
15955
15899
|
`slack-channel(${AGENT_CODE_NAME}): recovery outbox parse failed (${filename}): ${err.message}
|
|
@@ -16015,7 +15959,7 @@ async function processSlackRecoveryOutboxFile(filename) {
|
|
|
16015
15959
|
}
|
|
16016
15960
|
if (sendSucceeded) {
|
|
16017
15961
|
try {
|
|
16018
|
-
|
|
15962
|
+
unlinkSync3(fullPath);
|
|
16019
15963
|
} catch {
|
|
16020
15964
|
}
|
|
16021
15965
|
return;
|
|
@@ -16023,7 +15967,7 @@ async function processSlackRecoveryOutboxFile(filename) {
|
|
|
16023
15967
|
const next = slackNextRetryName(filename);
|
|
16024
15968
|
if (next) {
|
|
16025
15969
|
try {
|
|
16026
|
-
renameSync2(fullPath,
|
|
15970
|
+
renameSync2(fullPath, join5(SLACK_RECOVERY_OUTBOX_DIR, next.next));
|
|
16027
15971
|
if (next.attempt >= SLACK_MAX_RECOVERY_ATTEMPTS) {
|
|
16028
15972
|
process.stderr.write(
|
|
16029
15973
|
`slack-channel(${AGENT_CODE_NAME}): ghost-reply recovery exhausted retries \u2014 moved to ${next.next}
|
|
@@ -16053,7 +15997,7 @@ function scanSlackRecoveryRetries() {
|
|
|
16053
15997
|
if (!SLACK_RECOVERY_OUTBOX_DIR) return;
|
|
16054
15998
|
let entries;
|
|
16055
15999
|
try {
|
|
16056
|
-
entries =
|
|
16000
|
+
entries = readdirSync3(SLACK_RECOVERY_OUTBOX_DIR);
|
|
16057
16001
|
} catch {
|
|
16058
16002
|
return;
|
|
16059
16003
|
}
|
|
@@ -16062,7 +16006,7 @@ function scanSlackRecoveryRetries() {
|
|
|
16062
16006
|
if (!f.includes(".retry-") || f.endsWith(".poison.json")) continue;
|
|
16063
16007
|
let mtimeMs;
|
|
16064
16008
|
try {
|
|
16065
|
-
mtimeMs =
|
|
16009
|
+
mtimeMs = statSync2(join5(SLACK_RECOVERY_OUTBOX_DIR, f)).mtimeMs;
|
|
16066
16010
|
} catch {
|
|
16067
16011
|
continue;
|
|
16068
16012
|
}
|
|
@@ -16083,7 +16027,7 @@ function startSlackRecoveryOutboxWatcher() {
|
|
|
16083
16027
|
return;
|
|
16084
16028
|
}
|
|
16085
16029
|
try {
|
|
16086
|
-
for (const f of
|
|
16030
|
+
for (const f of readdirSync3(SLACK_RECOVERY_OUTBOX_DIR)) {
|
|
16087
16031
|
if (isFirstAttemptSlackOutboxFile(f)) void processSlackRecoveryOutboxFile(f);
|
|
16088
16032
|
}
|
|
16089
16033
|
} catch {
|
|
@@ -16092,7 +16036,7 @@ function startSlackRecoveryOutboxWatcher() {
|
|
|
16092
16036
|
const watcher = watch(SLACK_RECOVERY_OUTBOX_DIR, (event, filename) => {
|
|
16093
16037
|
if (event !== "rename" || !filename) return;
|
|
16094
16038
|
if (!isFirstAttemptSlackOutboxFile(filename)) return;
|
|
16095
|
-
if (
|
|
16039
|
+
if (existsSync3(join5(SLACK_RECOVERY_OUTBOX_DIR, filename))) {
|
|
16096
16040
|
void processSlackRecoveryOutboxFile(filename);
|
|
16097
16041
|
}
|
|
16098
16042
|
});
|
|
@@ -16108,15 +16052,15 @@ function startSlackRecoveryOutboxWatcher() {
|
|
|
16108
16052
|
}
|
|
16109
16053
|
startSlackRecoveryOutboxWatcher();
|
|
16110
16054
|
var STALE_MARKER_MS = 24 * 60 * 60 * 1e3;
|
|
16111
|
-
function trackPendingMessage(channel, threadTs, messageTs) {
|
|
16112
|
-
writeSlackPendingInboundMarker(channel, threadTs, messageTs);
|
|
16055
|
+
function trackPendingMessage(channel, threadTs, messageTs, undeliverable = false) {
|
|
16056
|
+
writeSlackPendingInboundMarker(channel, threadTs, messageTs, undeliverable);
|
|
16113
16057
|
}
|
|
16114
16058
|
function sweepSlackStaleMarkersOnBoot() {
|
|
16115
16059
|
if (!SLACK_PENDING_INBOUND_DIR) return;
|
|
16116
|
-
if (!
|
|
16060
|
+
if (!existsSync3(SLACK_PENDING_INBOUND_DIR)) return;
|
|
16117
16061
|
let filenames;
|
|
16118
16062
|
try {
|
|
16119
|
-
filenames =
|
|
16063
|
+
filenames = readdirSync3(SLACK_PENDING_INBOUND_DIR);
|
|
16120
16064
|
} catch (err) {
|
|
16121
16065
|
process.stderr.write(
|
|
16122
16066
|
`slack-channel(${AGENT_CODE_NAME}): stale-marker readdir failed: ${err.message}
|
|
@@ -16129,17 +16073,17 @@ function sweepSlackStaleMarkersOnBoot() {
|
|
|
16129
16073
|
for (const filename of filenames) {
|
|
16130
16074
|
if (!filename.endsWith(".json")) continue;
|
|
16131
16075
|
if (filename.endsWith(".tmp")) continue;
|
|
16132
|
-
const fullPath =
|
|
16076
|
+
const fullPath = join5(SLACK_PENDING_INBOUND_DIR, filename);
|
|
16133
16077
|
let marker;
|
|
16134
16078
|
try {
|
|
16135
|
-
marker = JSON.parse(
|
|
16079
|
+
marker = JSON.parse(readFileSync4(fullPath, "utf-8"));
|
|
16136
16080
|
} catch (err) {
|
|
16137
16081
|
process.stderr.write(
|
|
16138
16082
|
`slack-channel(${AGENT_CODE_NAME}): stale-marker parse failed for ${redactSlackId(filename)}: ${err.message}
|
|
16139
16083
|
`
|
|
16140
16084
|
);
|
|
16141
16085
|
try {
|
|
16142
|
-
|
|
16086
|
+
unlinkSync3(fullPath);
|
|
16143
16087
|
} catch {
|
|
16144
16088
|
}
|
|
16145
16089
|
cleared++;
|
|
@@ -16148,7 +16092,7 @@ function sweepSlackStaleMarkersOnBoot() {
|
|
|
16148
16092
|
const { channel, thread_ts, message_ts, received_at } = marker;
|
|
16149
16093
|
if (!channel || !thread_ts || !message_ts || !received_at) {
|
|
16150
16094
|
try {
|
|
16151
|
-
|
|
16095
|
+
unlinkSync3(fullPath);
|
|
16152
16096
|
} catch {
|
|
16153
16097
|
}
|
|
16154
16098
|
cleared++;
|
|
@@ -16157,7 +16101,7 @@ function sweepSlackStaleMarkersOnBoot() {
|
|
|
16157
16101
|
const receivedAtMs = Date.parse(received_at);
|
|
16158
16102
|
if (!Number.isFinite(receivedAtMs) || receivedAtMs > now || now - receivedAtMs > STALE_MARKER_MS) {
|
|
16159
16103
|
try {
|
|
16160
|
-
|
|
16104
|
+
unlinkSync3(fullPath);
|
|
16161
16105
|
} catch {
|
|
16162
16106
|
}
|
|
16163
16107
|
cleared++;
|
|
@@ -16171,8 +16115,83 @@ function sweepSlackStaleMarkersOnBoot() {
|
|
|
16171
16115
|
}
|
|
16172
16116
|
}
|
|
16173
16117
|
sweepSlackStaleMarkersOnBoot();
|
|
16118
|
+
var SLACK_PROCESS_BOOT_MS = Date.now();
|
|
16119
|
+
var STRANDED_INBOUND_MIN_AGE_FROM_BOOT_MS = 6e4;
|
|
16120
|
+
var STRANDED_INBOUND_MAX_AGE_MS = 5 * 60 * 1e3;
|
|
16121
|
+
var strandedInboundNoticeFired = false;
|
|
16122
|
+
var strandedInboundNoticeInFlight = false;
|
|
16123
|
+
async function notifyStrandedInboundsOnFirstConnect() {
|
|
16124
|
+
if (strandedInboundNoticeFired || strandedInboundNoticeInFlight) return;
|
|
16125
|
+
strandedInboundNoticeInFlight = true;
|
|
16126
|
+
let hadFailure = false;
|
|
16127
|
+
try {
|
|
16128
|
+
if (!SLACK_PENDING_INBOUND_DIR || !existsSync3(SLACK_PENDING_INBOUND_DIR)) return;
|
|
16129
|
+
let filenames;
|
|
16130
|
+
try {
|
|
16131
|
+
filenames = readdirSync3(SLACK_PENDING_INBOUND_DIR);
|
|
16132
|
+
} catch {
|
|
16133
|
+
hadFailure = true;
|
|
16134
|
+
return;
|
|
16135
|
+
}
|
|
16136
|
+
const now = Date.now();
|
|
16137
|
+
const seen = /* @__PURE__ */ new Set();
|
|
16138
|
+
let notified = 0;
|
|
16139
|
+
for (const filename of filenames) {
|
|
16140
|
+
if (!filename.endsWith(".json")) continue;
|
|
16141
|
+
const fullPath = join5(SLACK_PENDING_INBOUND_DIR, filename);
|
|
16142
|
+
let marker;
|
|
16143
|
+
try {
|
|
16144
|
+
marker = JSON.parse(readFileSync4(fullPath, "utf-8"));
|
|
16145
|
+
} catch {
|
|
16146
|
+
continue;
|
|
16147
|
+
}
|
|
16148
|
+
const { channel, thread_ts, received_at } = marker;
|
|
16149
|
+
if (!channel || !thread_ts || !received_at) continue;
|
|
16150
|
+
const receivedAtMs = Date.parse(received_at);
|
|
16151
|
+
if (!Number.isFinite(receivedAtMs)) continue;
|
|
16152
|
+
if (receivedAtMs >= SLACK_PROCESS_BOOT_MS - STRANDED_INBOUND_MIN_AGE_FROM_BOOT_MS) continue;
|
|
16153
|
+
if (now - receivedAtMs > STRANDED_INBOUND_MAX_AGE_MS) continue;
|
|
16154
|
+
const key2 = `${channel}__${thread_ts}`;
|
|
16155
|
+
if (seen.has(key2)) {
|
|
16156
|
+
try {
|
|
16157
|
+
unlinkSync3(fullPath);
|
|
16158
|
+
} catch {
|
|
16159
|
+
}
|
|
16160
|
+
continue;
|
|
16161
|
+
}
|
|
16162
|
+
seen.add(key2);
|
|
16163
|
+
const res = await postSlackMessage({
|
|
16164
|
+
channel,
|
|
16165
|
+
thread_ts,
|
|
16166
|
+
text: "I was restarted mid-conversation and may have missed your last message. Please re-send if I didn't reply."
|
|
16167
|
+
});
|
|
16168
|
+
if (res.ok) {
|
|
16169
|
+
notified += 1;
|
|
16170
|
+
try {
|
|
16171
|
+
unlinkSync3(fullPath);
|
|
16172
|
+
} catch {
|
|
16173
|
+
}
|
|
16174
|
+
} else {
|
|
16175
|
+
hadFailure = true;
|
|
16176
|
+
process.stderr.write(
|
|
16177
|
+
`slack-channel(${AGENT_CODE_NAME}): stranded-inbound notify failed channel=${redactSlackId(channel)} thread=${redactSlackId(thread_ts)} error=${res.error}
|
|
16178
|
+
`
|
|
16179
|
+
);
|
|
16180
|
+
}
|
|
16181
|
+
}
|
|
16182
|
+
if (notified > 0) {
|
|
16183
|
+
process.stderr.write(
|
|
16184
|
+
`slack-channel(${AGENT_CODE_NAME}): notified ${notified} stranded inbound thread(s) after restart
|
|
16185
|
+
`
|
|
16186
|
+
);
|
|
16187
|
+
}
|
|
16188
|
+
} finally {
|
|
16189
|
+
if (!hadFailure) strandedInboundNoticeFired = true;
|
|
16190
|
+
strandedInboundNoticeInFlight = false;
|
|
16191
|
+
}
|
|
16192
|
+
}
|
|
16174
16193
|
function clearPendingMessage(channel, threadTs) {
|
|
16175
|
-
|
|
16194
|
+
clearAllSlackPendingMarkersForThread2(channel, threadTs);
|
|
16176
16195
|
}
|
|
16177
16196
|
function noteThreadActivity(channel, threadTs) {
|
|
16178
16197
|
if (!channel || !threadTs) return;
|
|
@@ -16182,10 +16201,10 @@ function noteThreadActivityByMessageTs(channel, messageTs) {
|
|
|
16182
16201
|
if (!channel || !messageTs) return;
|
|
16183
16202
|
clearPendingMessage(channel, messageTs);
|
|
16184
16203
|
if (!SLACK_PENDING_INBOUND_DIR) return;
|
|
16185
|
-
if (!
|
|
16204
|
+
if (!existsSync3(SLACK_PENDING_INBOUND_DIR)) return;
|
|
16186
16205
|
let filenames;
|
|
16187
16206
|
try {
|
|
16188
|
-
filenames =
|
|
16207
|
+
filenames = readdirSync3(SLACK_PENDING_INBOUND_DIR);
|
|
16189
16208
|
} catch {
|
|
16190
16209
|
return;
|
|
16191
16210
|
}
|
|
@@ -16196,13 +16215,10 @@ function noteThreadActivityByMessageTs(channel, messageTs) {
|
|
|
16196
16215
|
for (const filename of filenames) {
|
|
16197
16216
|
if (!filename.startsWith(channelPrefix)) continue;
|
|
16198
16217
|
if (!filename.endsWith(messageSuffix)) continue;
|
|
16199
|
-
|
|
16200
|
-
unlinkSync2(join3(SLACK_PENDING_INBOUND_DIR, filename));
|
|
16201
|
-
} catch {
|
|
16202
|
-
}
|
|
16218
|
+
clearSlackMarkerFileWithHeal(join5(SLACK_PENDING_INBOUND_DIR, filename));
|
|
16203
16219
|
}
|
|
16204
16220
|
}
|
|
16205
|
-
var RESTART_FLAGS_DIR =
|
|
16221
|
+
var RESTART_FLAGS_DIR = join5(homedir2(), ".augmented", "restart-flags");
|
|
16206
16222
|
function buildAugmentedSlackMetadata() {
|
|
16207
16223
|
if (!AGT_TEAM_ID) return void 0;
|
|
16208
16224
|
return {
|
|
@@ -16381,10 +16397,10 @@ async function handleSlashCommandEnvelope(payload) {
|
|
|
16381
16397
|
return;
|
|
16382
16398
|
}
|
|
16383
16399
|
try {
|
|
16384
|
-
if (!
|
|
16400
|
+
if (!existsSync3(RESTART_FLAGS_DIR)) {
|
|
16385
16401
|
mkdirSync3(RESTART_FLAGS_DIR, { recursive: true });
|
|
16386
16402
|
}
|
|
16387
|
-
const flagPath =
|
|
16403
|
+
const flagPath = join5(RESTART_FLAGS_DIR, `${codeName}.flag`);
|
|
16388
16404
|
const flag = {
|
|
16389
16405
|
codeName,
|
|
16390
16406
|
source: "slack",
|
|
@@ -16394,7 +16410,7 @@ async function handleSlashCommandEnvelope(payload) {
|
|
|
16394
16410
|
...payload.thread_ts ? { thread_ts: payload.thread_ts } : {}
|
|
16395
16411
|
}
|
|
16396
16412
|
};
|
|
16397
|
-
const tmpPath = `${flagPath}.${process.pid}.${
|
|
16413
|
+
const tmpPath = `${flagPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
16398
16414
|
writeFileSync3(tmpPath, JSON.stringify(flag) + "\n", "utf8");
|
|
16399
16415
|
renameSync2(tmpPath, flagPath);
|
|
16400
16416
|
process.stderr.write(
|
|
@@ -16488,10 +16504,10 @@ async function handleHelpCommand(opts) {
|
|
|
16488
16504
|
async function handleRestartCommand(opts) {
|
|
16489
16505
|
const codeName = AGENT_CODE_NAME ?? "unknown";
|
|
16490
16506
|
try {
|
|
16491
|
-
if (!
|
|
16507
|
+
if (!existsSync3(RESTART_FLAGS_DIR)) {
|
|
16492
16508
|
mkdirSync3(RESTART_FLAGS_DIR, { recursive: true });
|
|
16493
16509
|
}
|
|
16494
|
-
const flagPath =
|
|
16510
|
+
const flagPath = join5(RESTART_FLAGS_DIR, `${codeName}.flag`);
|
|
16495
16511
|
const flag = {
|
|
16496
16512
|
codeName,
|
|
16497
16513
|
source: "slack",
|
|
@@ -16502,7 +16518,7 @@ async function handleRestartCommand(opts) {
|
|
|
16502
16518
|
message_ts: opts.ts
|
|
16503
16519
|
}
|
|
16504
16520
|
};
|
|
16505
|
-
const tmpPath = `${flagPath}.${process.pid}.${
|
|
16521
|
+
const tmpPath = `${flagPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
16506
16522
|
writeFileSync3(tmpPath, JSON.stringify(flag) + "\n", "utf8");
|
|
16507
16523
|
renameSync2(tmpPath, flagPath);
|
|
16508
16524
|
process.stderr.write(
|
|
@@ -16550,7 +16566,7 @@ var THREAD_STORE_TTL_DAYS = parseTtlDays(process.env.SLACK_THREAD_FOLLOW_TTL_DAY
|
|
|
16550
16566
|
var threadPersister = null;
|
|
16551
16567
|
function resolveThreadStorePath() {
|
|
16552
16568
|
if (!AGENT_CODE_NAME) return null;
|
|
16553
|
-
return
|
|
16569
|
+
return join5(homedir2(), ".augmented", AGENT_CODE_NAME, "slack-tracked-threads.json");
|
|
16554
16570
|
}
|
|
16555
16571
|
function parseTtlDays(raw) {
|
|
16556
16572
|
if (!raw) return void 0;
|
|
@@ -16585,9 +16601,9 @@ if (!BOT_TOKEN || !APP_TOKEN) {
|
|
|
16585
16601
|
var slackStderrLogStream = null;
|
|
16586
16602
|
if (AGENT_CODE_NAME) {
|
|
16587
16603
|
try {
|
|
16588
|
-
const logDir =
|
|
16604
|
+
const logDir = join5(homedir2(), ".augmented", AGENT_CODE_NAME);
|
|
16589
16605
|
mkdirSync3(logDir, { recursive: true });
|
|
16590
|
-
slackStderrLogStream = createWriteStream(
|
|
16606
|
+
slackStderrLogStream = createWriteStream(join5(logDir, "slack-channel-stderr.log"), {
|
|
16591
16607
|
flags: "a",
|
|
16592
16608
|
mode: 384
|
|
16593
16609
|
});
|
|
@@ -16657,7 +16673,6 @@ void resolveBotUserIdOrThrow().then((id) => {
|
|
|
16657
16673
|
);
|
|
16658
16674
|
if (authFailed) slackBotUserIdClient?.reportAuthHealth(false);
|
|
16659
16675
|
});
|
|
16660
|
-
var selfIdentityInstruction = `Mentions of your own Slack bot user are directed at you, even inside auto_followed threads.`;
|
|
16661
16676
|
var mcp = new Server(
|
|
16662
16677
|
{ name: "slack", version: "0.1.0" },
|
|
16663
16678
|
{
|
|
@@ -16672,43 +16687,18 @@ var mcp = new Server(
|
|
|
16672
16687
|
// Highest-priority lines first — Claude Code truncates this string at
|
|
16673
16688
|
// 2048 chars, so anything appended late silently disappears.
|
|
16674
16689
|
"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,
|
|
16690
|
+
`Inbound: <channel ... thread_ts="..." message_ts="..." [thread_context="..."]>. Pass channel + message_ts to slack.reply (and thread_ts on threads). thread_context = thread pre-loaded; ground replies ONLY in it, never another channel's.`,
|
|
16678
16691
|
"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
16692
|
"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).",
|
|
16693
|
+
'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).',
|
|
16694
|
+
"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
16695
|
`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
16696
|
"To deliver a file: save under your project dir, call slack.upload_file with path + channel + thread_ts."
|
|
16684
16697
|
].join(" ")
|
|
16685
16698
|
}
|
|
16686
16699
|
);
|
|
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
16700
|
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
16710
16701
|
tools: [
|
|
16711
|
-
...progressRegistry.getDefinitions(),
|
|
16712
16702
|
{
|
|
16713
16703
|
name: "slack.reply",
|
|
16714
16704
|
description: "Send a message back to a Slack channel or thread",
|
|
@@ -16722,7 +16712,18 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
16722
16712
|
text: { type: "string", description: "The message to send" },
|
|
16723
16713
|
thread_ts: {
|
|
16724
16714
|
type: "string",
|
|
16725
|
-
description: "Thread timestamp for threaded replies (from the thread_ts attribute)"
|
|
16715
|
+
description: "Thread timestamp for threaded replies (from the thread_ts attribute). Omit for a top-level reply (e.g. a fresh DM)."
|
|
16716
|
+
},
|
|
16717
|
+
// ENG-5861: explicit message_ts so the cleanup gate can match the
|
|
16718
|
+
// exact pending-inbound marker — necessary because top-level DM
|
|
16719
|
+
// replies legitimately have no thread_ts and the marker filename
|
|
16720
|
+
// is `<channel>__<thread_ts>__<message_ts>.json`. Without this
|
|
16721
|
+
// the marker sat undrained, eventually tripping ENG-5832\'s
|
|
16722
|
+
// "wedged" threshold and firing a false-positive red ✗ on the
|
|
16723
|
+
// next inbound.
|
|
16724
|
+
message_ts: {
|
|
16725
|
+
type: "string",
|
|
16726
|
+
description: "The message_ts of the specific inbound this reply addresses (from the message_ts attribute on the <channel> tag). Used by the pending-inbound cleanup gate so the marker is reliably cleared even for top-level DMs that have no thread_ts."
|
|
16726
16727
|
}
|
|
16727
16728
|
},
|
|
16728
16729
|
required: ["channel", "text"]
|
|
@@ -16730,7 +16731,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
16730
16731
|
},
|
|
16731
16732
|
{
|
|
16732
16733
|
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.",
|
|
16734
|
+
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
16735
|
inputSchema: {
|
|
16735
16736
|
type: "object",
|
|
16736
16737
|
properties: {
|
|
@@ -16743,7 +16744,7 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
16743
16744
|
},
|
|
16744
16745
|
{
|
|
16745
16746
|
name: "slack.read_thread",
|
|
16746
|
-
description: "Read the full message history of a Slack thread.
|
|
16747
|
+
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
16748
|
inputSchema: {
|
|
16748
16749
|
type: "object",
|
|
16749
16750
|
properties: {
|
|
@@ -16959,10 +16960,8 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
16959
16960
|
if (isImpersonating() && !channelsEnabledOverride() && SLACK_EGRESS_TOOLS.has(name)) {
|
|
16960
16961
|
return buildImpersonationRefusal(name);
|
|
16961
16962
|
}
|
|
16962
|
-
const progressResult = await progressRegistry.handle(name, args ?? {});
|
|
16963
|
-
if (progressResult !== void 0) return progressResult;
|
|
16964
16963
|
if (name === "slack.reply") {
|
|
16965
|
-
const { channel, text, thread_ts } = args;
|
|
16964
|
+
const { channel, text, thread_ts, message_ts } = args;
|
|
16966
16965
|
if (channel && thread_ts) {
|
|
16967
16966
|
const killed = await isThreadKilled({
|
|
16968
16967
|
channelType: "slack",
|
|
@@ -17021,8 +17020,15 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
17021
17020
|
isError: true
|
|
17022
17021
|
};
|
|
17023
17022
|
}
|
|
17024
|
-
if (channel
|
|
17025
|
-
|
|
17023
|
+
if (channel) {
|
|
17024
|
+
if (message_ts) {
|
|
17025
|
+
clearSlackPendingMarkerByMessageTs2(channel, message_ts);
|
|
17026
|
+
if (thread_ts) clearPendingMessage(channel, thread_ts);
|
|
17027
|
+
} else if (thread_ts) {
|
|
17028
|
+
clearPendingMessage(channel, thread_ts);
|
|
17029
|
+
} else {
|
|
17030
|
+
clearOldestSlackPendingMarkerInChannel2(channel);
|
|
17031
|
+
}
|
|
17026
17032
|
}
|
|
17027
17033
|
try {
|
|
17028
17034
|
const res = await fetch("https://slack.com/api/chat.postMessage", {
|
|
@@ -17117,46 +17123,22 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
17117
17123
|
const limit = Math.min(Math.max(rawLimit ?? 50, 1), 200);
|
|
17118
17124
|
noteThreadActivity(channel, thread_ts);
|
|
17119
17125
|
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");
|
|
17126
|
+
const result = await fetchThreadTranscript(channel, thread_ts, limit, {
|
|
17127
|
+
botToken: BOT_TOKEN,
|
|
17128
|
+
resolveUserName
|
|
17129
|
+
});
|
|
17130
|
+
if (!result.ok) {
|
|
17131
|
+
return {
|
|
17132
|
+
content: [{ type: "text", text: `Slack error: ${result.error}` }],
|
|
17133
|
+
isError: true
|
|
17134
|
+
};
|
|
17135
|
+
}
|
|
17154
17136
|
return {
|
|
17155
17137
|
content: [{
|
|
17156
17138
|
type: "text",
|
|
17157
|
-
text:
|
|
17139
|
+
text: result.count > 0 ? `Thread (${result.count} messages):
|
|
17158
17140
|
|
|
17159
|
-
${formatted}` : "Thread is empty or not found."
|
|
17141
|
+
${result.formatted}` : "Thread is empty or not found."
|
|
17160
17142
|
}]
|
|
17161
17143
|
};
|
|
17162
17144
|
} catch (err) {
|
|
@@ -17198,7 +17180,7 @@ ${formatted}` : "Thread is empty or not found."
|
|
|
17198
17180
|
let bytes;
|
|
17199
17181
|
let size;
|
|
17200
17182
|
try {
|
|
17201
|
-
const stat =
|
|
17183
|
+
const stat = statSync2(resolvedPath);
|
|
17202
17184
|
if (!stat.isFile()) {
|
|
17203
17185
|
return {
|
|
17204
17186
|
content: [{ type: "text", text: `Upload refused: ${resolvedPath} is not a regular file.` }],
|
|
@@ -17206,7 +17188,7 @@ ${formatted}` : "Thread is empty or not found."
|
|
|
17206
17188
|
};
|
|
17207
17189
|
}
|
|
17208
17190
|
size = stat.size;
|
|
17209
|
-
bytes =
|
|
17191
|
+
bytes = readFileSync4(resolvedPath);
|
|
17210
17192
|
} catch (err) {
|
|
17211
17193
|
return {
|
|
17212
17194
|
content: [{ type: "text", text: `Failed to read file: ${err.message}` }],
|
|
@@ -17505,15 +17487,15 @@ async function handleSendStructured(args) {
|
|
|
17505
17487
|
const { validateSlackBlocks: validateSlackBlocks2 } = runtime;
|
|
17506
17488
|
const { channel, blocks, text, thread_ts, interactive } = args;
|
|
17507
17489
|
if (typeof channel !== "string" || !channel) {
|
|
17508
|
-
return
|
|
17490
|
+
return errResult("channel is required");
|
|
17509
17491
|
}
|
|
17510
17492
|
if (typeof text !== "string" || !text) {
|
|
17511
|
-
return
|
|
17493
|
+
return errResult("text is required (used as fallback for push notifications and unfurls)");
|
|
17512
17494
|
}
|
|
17513
17495
|
noteThreadActivity(channel, thread_ts);
|
|
17514
17496
|
const validation = validateSlackBlocks2(blocks);
|
|
17515
17497
|
if (!validation.ok) {
|
|
17516
|
-
return
|
|
17498
|
+
return errResult(`Invalid blocks: ${validation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
|
|
17517
17499
|
}
|
|
17518
17500
|
let effectiveBlocks = blocks;
|
|
17519
17501
|
let ratingCallbackId = null;
|
|
@@ -17521,13 +17503,13 @@ async function handleSendStructured(args) {
|
|
|
17521
17503
|
let ratingTokenised = [];
|
|
17522
17504
|
if (interactive) {
|
|
17523
17505
|
if (interactive.type !== "rate_run") {
|
|
17524
|
-
return
|
|
17506
|
+
return errResult(`Unsupported interactive.type '${interactive.type}'. Supported: rate_run.`);
|
|
17525
17507
|
}
|
|
17526
17508
|
if (typeof interactive.run_id !== "string" || !interactive.run_id) {
|
|
17527
|
-
return
|
|
17509
|
+
return errResult("interactive.run_id is required when interactive is set");
|
|
17528
17510
|
}
|
|
17529
17511
|
if (!interactiveHostAvailable()) {
|
|
17530
|
-
return
|
|
17512
|
+
return errResult(
|
|
17531
17513
|
"interactive=rate_run requires Block Kit plus host MCP wiring (AGT_HOST, AGT_API_KEY, AGT_AGENT_ID)."
|
|
17532
17514
|
);
|
|
17533
17515
|
}
|
|
@@ -17558,7 +17540,7 @@ async function handleSendStructured(args) {
|
|
|
17558
17540
|
effectiveBlocks = [...blocks, ratingBlock];
|
|
17559
17541
|
const finalValidation = runtime.validateSlackBlocks(effectiveBlocks);
|
|
17560
17542
|
if (!finalValidation.ok) {
|
|
17561
|
-
return
|
|
17543
|
+
return errResult(
|
|
17562
17544
|
`Invalid blocks (after appending rating row): ${finalValidation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`
|
|
17563
17545
|
);
|
|
17564
17546
|
}
|
|
@@ -17579,7 +17561,7 @@ async function handleSendStructured(args) {
|
|
|
17579
17561
|
kindData: { run_id: interactive.run_id, scale }
|
|
17580
17562
|
});
|
|
17581
17563
|
} catch (err) {
|
|
17582
|
-
return
|
|
17564
|
+
return errResult(`Failed to register rating row: ${err.message}`);
|
|
17583
17565
|
}
|
|
17584
17566
|
}
|
|
17585
17567
|
const result = await postSlackMessageWithTs({
|
|
@@ -17589,7 +17571,7 @@ async function handleSendStructured(args) {
|
|
|
17589
17571
|
...thread_ts ? { thread_ts } : {}
|
|
17590
17572
|
});
|
|
17591
17573
|
if (!result.ok || !result.ts) {
|
|
17592
|
-
return
|
|
17574
|
+
return errResult(`Slack chat.postMessage failed: ${result.error ?? "unknown"}`);
|
|
17593
17575
|
}
|
|
17594
17576
|
if (ratingCallbackId) {
|
|
17595
17577
|
try {
|
|
@@ -17644,17 +17626,17 @@ async function handleSendStructured(args) {
|
|
|
17644
17626
|
}
|
|
17645
17627
|
async function handleAskUser(args) {
|
|
17646
17628
|
if (!askUserToolAvailable()) {
|
|
17647
|
-
return
|
|
17629
|
+
return errResult("slack.ask_user is disabled or the host MCP is missing AGT_HOST/AGT_API_KEY/AGT_AGENT_ID env wiring.");
|
|
17648
17630
|
}
|
|
17649
17631
|
const { randomUUID: rndUUID } = await import("crypto");
|
|
17650
17632
|
const runtime = await Promise.resolve().then(() => (init_slack_block_kit_runtime(), slack_block_kit_runtime_exports));
|
|
17651
17633
|
const { validateAskUserOptions: validateAskUserOptions2, buildAskUserBlocks: buildAskUserBlocks2 } = runtime;
|
|
17652
17634
|
const { channel, question, options, timeout_seconds, thread_ts } = args;
|
|
17653
|
-
if (typeof channel !== "string" || !channel) return
|
|
17654
|
-
if (typeof question !== "string" || !question) return
|
|
17635
|
+
if (typeof channel !== "string" || !channel) return errResult("channel is required");
|
|
17636
|
+
if (typeof question !== "string" || !question) return errResult("question is required");
|
|
17655
17637
|
const optsValidation = validateAskUserOptions2(options);
|
|
17656
17638
|
if (!optsValidation.ok) {
|
|
17657
|
-
return
|
|
17639
|
+
return errResult(`Invalid options: ${optsValidation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
|
|
17658
17640
|
}
|
|
17659
17641
|
const requestedTimeout = typeof timeout_seconds === "number" ? timeout_seconds : 300;
|
|
17660
17642
|
const timeoutSec = Math.max(10, Math.min(3600, requestedTimeout));
|
|
@@ -17687,7 +17669,7 @@ async function handleAskUser(args) {
|
|
|
17687
17669
|
expiresAt
|
|
17688
17670
|
});
|
|
17689
17671
|
} catch (err) {
|
|
17690
|
-
return
|
|
17672
|
+
return errResult(`Failed to register pending interaction: ${err.message}`);
|
|
17691
17673
|
}
|
|
17692
17674
|
const slackResult = await postSlackMessageWithTs({
|
|
17693
17675
|
channel,
|
|
@@ -17697,7 +17679,7 @@ async function handleAskUser(args) {
|
|
|
17697
17679
|
...thread_ts ? { thread_ts } : {}
|
|
17698
17680
|
});
|
|
17699
17681
|
if (!slackResult.ok || !slackResult.ts) {
|
|
17700
|
-
return
|
|
17682
|
+
return errResult(`Slack chat.postMessage failed: ${slackResult.error ?? "unknown"}`);
|
|
17701
17683
|
}
|
|
17702
17684
|
try {
|
|
17703
17685
|
await runtime.updatePendingInteractionMessageTs(cfg, callbackId, slackResult.ts);
|
|
@@ -17744,7 +17726,7 @@ async function handleAskUser(args) {
|
|
|
17744
17726
|
}
|
|
17745
17727
|
async function handleChannelRequestInput(args) {
|
|
17746
17728
|
if (!askUserToolAvailable()) {
|
|
17747
|
-
return
|
|
17729
|
+
return errResult(
|
|
17748
17730
|
"channel_request_input is disabled or the host MCP is missing AGT_HOST/AGT_API_KEY/AGT_AGENT_ID env wiring."
|
|
17749
17731
|
);
|
|
17750
17732
|
}
|
|
@@ -17752,10 +17734,10 @@ async function handleChannelRequestInput(args) {
|
|
|
17752
17734
|
const { apiCall: apiCall2, waitForResolution: waitForResolution2, validateAskUserOptions: validateAskUserOptions2 } = runtime;
|
|
17753
17735
|
const { thread_id, prompt_text, options, schema, request_id, timeout_seconds } = args;
|
|
17754
17736
|
if (typeof thread_id !== "string" || !thread_id) {
|
|
17755
|
-
return
|
|
17737
|
+
return errResult("thread_id is required (shape: `<channel_id>:<thread_ts>`)");
|
|
17756
17738
|
}
|
|
17757
17739
|
if (typeof prompt_text !== "string" || !prompt_text) {
|
|
17758
|
-
return
|
|
17740
|
+
return errResult("prompt_text is required");
|
|
17759
17741
|
}
|
|
17760
17742
|
const optsValidation = validateAskUserOptions2(options, {
|
|
17761
17743
|
maxOptions: 4,
|
|
@@ -17763,25 +17745,25 @@ async function handleChannelRequestInput(args) {
|
|
|
17763
17745
|
maxValueChars: 2e3
|
|
17764
17746
|
});
|
|
17765
17747
|
if (!optsValidation.ok) {
|
|
17766
|
-
return
|
|
17748
|
+
return errResult(
|
|
17767
17749
|
`Invalid options: ${optsValidation.errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`
|
|
17768
17750
|
);
|
|
17769
17751
|
}
|
|
17770
17752
|
const optionsArr = options;
|
|
17771
17753
|
if (optionsArr.length < 2) {
|
|
17772
|
-
return
|
|
17754
|
+
return errResult("options must contain at least 2 entries (this is a picker, not a notification)");
|
|
17773
17755
|
}
|
|
17774
17756
|
if (!schema || typeof schema.type !== "string") {
|
|
17775
|
-
return
|
|
17757
|
+
return errResult("schema is required with shape { type: 'yes_no' | 'choice' }");
|
|
17776
17758
|
}
|
|
17777
17759
|
if (schema.type !== "yes_no" && schema.type !== "choice") {
|
|
17778
|
-
return
|
|
17760
|
+
return errResult("schema.type must be 'yes_no' or 'choice'");
|
|
17779
17761
|
}
|
|
17780
17762
|
if (schema.type === "yes_no" && optionsArr.length !== 2) {
|
|
17781
|
-
return
|
|
17763
|
+
return errResult("schema.type='yes_no' requires exactly 2 options");
|
|
17782
17764
|
}
|
|
17783
17765
|
if (request_id !== void 0 && (typeof request_id !== "string" || !request_id)) {
|
|
17784
|
-
return
|
|
17766
|
+
return errResult("request_id must be a non-empty string when provided");
|
|
17785
17767
|
}
|
|
17786
17768
|
const requestedTimeout = typeof timeout_seconds === "number" ? timeout_seconds : 300;
|
|
17787
17769
|
const timeoutSec = Math.max(10, Math.min(3600, requestedTimeout));
|
|
@@ -17803,10 +17785,10 @@ async function handleChannelRequestInput(args) {
|
|
|
17803
17785
|
dispatchPayload = await res.json().catch(() => ({}));
|
|
17804
17786
|
if (!res.ok || !dispatchPayload.ok || !dispatchPayload.callback_id) {
|
|
17805
17787
|
const reason = dispatchPayload.reason ?? `http_${res.status}`;
|
|
17806
|
-
return
|
|
17788
|
+
return errResult(`channel_request_input dispatch failed: ${reason}`);
|
|
17807
17789
|
}
|
|
17808
17790
|
} catch (err) {
|
|
17809
|
-
return
|
|
17791
|
+
return errResult(
|
|
17810
17792
|
`channel_request_input dispatch failed: ${err.message}`
|
|
17811
17793
|
);
|
|
17812
17794
|
}
|
|
@@ -17844,7 +17826,7 @@ async function handleChannelRequestInput(args) {
|
|
|
17844
17826
|
]
|
|
17845
17827
|
};
|
|
17846
17828
|
}
|
|
17847
|
-
function
|
|
17829
|
+
function errResult(text) {
|
|
17848
17830
|
return { content: [{ type: "text", text }], isError: true };
|
|
17849
17831
|
}
|
|
17850
17832
|
var MAX_INBOUND_FILE_BYTES = 25 * 1024 * 1024;
|
|
@@ -18038,6 +18020,7 @@ async function connectSocketMode() {
|
|
|
18038
18020
|
process.stderr.write("slack-channel: Socket Mode connected\n");
|
|
18039
18021
|
recordActivity("connect");
|
|
18040
18022
|
void setBotStatus(":large_green_circle:", "Active");
|
|
18023
|
+
void notifyStrandedInboundsOnFirstConnect();
|
|
18041
18024
|
};
|
|
18042
18025
|
ws.onmessage = async (event) => {
|
|
18043
18026
|
try {
|
|
@@ -18099,8 +18082,27 @@ async function connectSocketMode() {
|
|
|
18099
18082
|
}
|
|
18100
18083
|
const senderPolicyDecision = decideSenderPolicyForward(evt, SLACK_SENDER_POLICY);
|
|
18101
18084
|
if (!senderPolicyDecision.forward) {
|
|
18102
|
-
|
|
18085
|
+
const subReason = classifySlackPolicyBlock(evt, SLACK_SENDER_POLICY);
|
|
18086
|
+
process.stderr.write(`slack-channel: dropped message event (reason=sender_policy, sub=${subReason}, mode=${SLACK_SENDER_POLICY.mode}, ts=${evt.ts ?? "n/a"})
|
|
18103
18087
|
`);
|
|
18088
|
+
await maybeSendSenderPolicyDecline({
|
|
18089
|
+
channel: evt.channel,
|
|
18090
|
+
senderId: evt.user,
|
|
18091
|
+
// CR on PR #1623: top-level posts (DMs, channel root messages)
|
|
18092
|
+
// arrive with no `thread_ts` and MUST decline inline, not in a
|
|
18093
|
+
// new thread off the inbound message. The `?? evt.ts` fallback
|
|
18094
|
+
// forced every root message into a thread reply — exactly the
|
|
18095
|
+
// case the helper's "omit thread_ts when undefined" branch was
|
|
18096
|
+
// meant to handle. Pass through as-is: in-thread for thread
|
|
18097
|
+
// replies (thread_ts present), inline for everything else.
|
|
18098
|
+
threadTs: evt.thread_ts,
|
|
18099
|
+
subReason
|
|
18100
|
+
}).catch((err) => {
|
|
18101
|
+
process.stderr.write(
|
|
18102
|
+
`slack-channel(${AGENT_CODE_NAME}): decline reply failed: ${err.message}
|
|
18103
|
+
`
|
|
18104
|
+
);
|
|
18105
|
+
});
|
|
18104
18106
|
return;
|
|
18105
18107
|
}
|
|
18106
18108
|
recordActivity("inbound");
|
|
@@ -18226,19 +18228,38 @@ async function connectSocketMode() {
|
|
|
18226
18228
|
isAutoFollowed,
|
|
18227
18229
|
botUserId
|
|
18228
18230
|
});
|
|
18229
|
-
|
|
18231
|
+
const ackProbe = process.env.TMUX && AGENT_CODE_NAME ? probeAgentSessionCached(AGENT_CODE_NAME) : { tmux: "unknown", claude: "unknown" };
|
|
18232
|
+
let paneLogFreshAgeMs = null;
|
|
18233
|
+
if (SLACK_AGENT_DIR) {
|
|
18234
|
+
try {
|
|
18235
|
+
const paneMtimeMs = statSync2(join5(SLACK_AGENT_DIR, "pane.log")).mtimeMs;
|
|
18236
|
+
paneLogFreshAgeMs = Math.max(0, Date.now() - paneMtimeMs);
|
|
18237
|
+
} catch {
|
|
18238
|
+
}
|
|
18239
|
+
}
|
|
18240
|
+
const ackDecision = decideAckReaction({
|
|
18241
|
+
hasTarget: decideSlackAckReaction({ channel, ts }),
|
|
18242
|
+
integrationReady: Boolean(BOT_TOKEN),
|
|
18243
|
+
tmux: ackProbe.tmux,
|
|
18244
|
+
claude: ackProbe.claude,
|
|
18245
|
+
withinStartupGrace: process.uptime() * 1e3 < ACK_STARTUP_GRACE_MS,
|
|
18246
|
+
oldestPendingAgeMs: oldestPendingMarkerAgeMs(SLACK_PENDING_INBOUND_DIR),
|
|
18247
|
+
paneLogFreshAgeMs
|
|
18248
|
+
});
|
|
18249
|
+
if (ackDecision !== "none") {
|
|
18250
|
+
const reactionName = ackDecision === "undeliverable" ? "x" : "eyes";
|
|
18230
18251
|
fetch("https://slack.com/api/reactions.add", {
|
|
18231
18252
|
method: "POST",
|
|
18232
18253
|
headers: {
|
|
18233
18254
|
"Content-Type": "application/json",
|
|
18234
18255
|
Authorization: `Bearer ${BOT_TOKEN}`
|
|
18235
18256
|
},
|
|
18236
|
-
body: JSON.stringify({ channel, timestamp: ts, name:
|
|
18257
|
+
body: JSON.stringify({ channel, timestamp: ts, name: reactionName })
|
|
18237
18258
|
}).catch(() => {
|
|
18238
18259
|
});
|
|
18239
18260
|
}
|
|
18240
18261
|
if (channel && ts && shouldEngage) {
|
|
18241
|
-
trackPendingMessage(channel, threadTs, ts);
|
|
18262
|
+
trackPendingMessage(channel, threadTs, ts, ackDecision === "undeliverable");
|
|
18242
18263
|
}
|
|
18243
18264
|
const userName = await resolveUserName(user);
|
|
18244
18265
|
const fileMeta = await buildInboundFileMeta(evt.files, AGENT_CODE_NAME, channel);
|
|
@@ -18246,6 +18267,29 @@ async function connectSocketMode() {
|
|
|
18246
18267
|
(f) => f.kind === "image" && typeof f.path === "string"
|
|
18247
18268
|
);
|
|
18248
18269
|
const imagePath = downloadedImages.length === 1 ? downloadedImages[0].path : void 0;
|
|
18270
|
+
let threadContext;
|
|
18271
|
+
if (isThreadReply && channel && threadTs) {
|
|
18272
|
+
try {
|
|
18273
|
+
const transcript = await fetchThreadTranscript(
|
|
18274
|
+
channel,
|
|
18275
|
+
threadTs,
|
|
18276
|
+
SLACK_AUTOLOAD_THREAD_LIMIT,
|
|
18277
|
+
{ botToken: BOT_TOKEN, resolveUserName }
|
|
18278
|
+
);
|
|
18279
|
+
if (transcript.ok && transcript.count >= 2) {
|
|
18280
|
+
threadContext = capThreadContext(
|
|
18281
|
+
transcript.formatted,
|
|
18282
|
+
SLACK_AUTOLOAD_THREAD_MAX_CHARS
|
|
18283
|
+
);
|
|
18284
|
+
}
|
|
18285
|
+
} catch (err) {
|
|
18286
|
+
const msg2 = err instanceof Error ? err.message : String(err);
|
|
18287
|
+
process.stderr.write(
|
|
18288
|
+
`slack-channel(${AGENT_CODE_NAME}): thread_context fetch failed (channel=${redactSlackId(channel)} thread=${redactSlackId(threadTs)}): ${msg2}
|
|
18289
|
+
`
|
|
18290
|
+
);
|
|
18291
|
+
}
|
|
18292
|
+
}
|
|
18249
18293
|
await mcp.notification({
|
|
18250
18294
|
method: "notifications/claude/channel",
|
|
18251
18295
|
params: {
|
|
@@ -18255,12 +18299,21 @@ async function connectSocketMode() {
|
|
|
18255
18299
|
user_name: userName,
|
|
18256
18300
|
channel,
|
|
18257
18301
|
thread_ts: threadTs,
|
|
18302
|
+
// ENG-5861: explicit message_ts so the agent can pass it back
|
|
18303
|
+
// to slack.reply for reliable pending-inbound cleanup. For
|
|
18304
|
+
// top-level messages threadTs === ts; for threaded replies
|
|
18305
|
+
// threadTs is the thread root and message_ts is the specific
|
|
18306
|
+
// reply being addressed — different keys, both needed by the
|
|
18307
|
+
// marker-cleanup gate.
|
|
18308
|
+
message_ts: ts,
|
|
18258
18309
|
event_type: evt.type,
|
|
18259
18310
|
...isAutoFollowed ? { auto_followed: "true" } : {},
|
|
18260
18311
|
// Only set these when we actually have attachments to avoid
|
|
18261
18312
|
// bloating every notification with empty metadata.
|
|
18262
18313
|
...fileMeta.length > 0 ? { files: JSON.stringify(fileMeta) } : {},
|
|
18263
|
-
...imagePath ? { image_path: imagePath } : {}
|
|
18314
|
+
...imagePath ? { image_path: imagePath } : {},
|
|
18315
|
+
// ENG-5830: the pre-loaded surrounding thread (thread replies only).
|
|
18316
|
+
...threadContext ? { thread_context: threadContext } : {}
|
|
18264
18317
|
}
|
|
18265
18318
|
}
|
|
18266
18319
|
});
|