teamwork-os 2.0.4 → 2.1.0
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/bin/teamos.js +8 -0
- package/package.json +1 -1
- package/packages/gateway/dist/index.js +362 -158
- package/packages/gateway/package.json +1 -1
|
@@ -917,6 +917,10 @@ var init_settings_defaults = __esm({
|
|
|
917
917
|
channels: {
|
|
918
918
|
directory: {},
|
|
919
919
|
mcp: {}
|
|
920
|
+
// `mcpDefault` is deliberately ABSENT rather than set to a value. Absent
|
|
921
|
+
// is the default grant — strict, no servers — so an existing install that
|
|
922
|
+
// never heard of this key behaves exactly as before. Setting it is the
|
|
923
|
+
// opt-in that opens every unnamed channel at once.
|
|
920
924
|
},
|
|
921
925
|
slack: {
|
|
922
926
|
enabled: true,
|
|
@@ -932,6 +936,10 @@ var init_settings_defaults = __esm({
|
|
|
932
936
|
historyLimit: 20,
|
|
933
937
|
allowUnmappedChannels: false,
|
|
934
938
|
dmAllowlist: [],
|
|
939
|
+
// On by default: the cost of the block is one paragraph on one turn per
|
|
940
|
+
// channel, and the thing it explains (channels.directory) is the setting
|
|
941
|
+
// a new channel is most likely to be missing.
|
|
942
|
+
onboardNewChannels: true,
|
|
935
943
|
// The deny universe for claude.ai account connectors — see
|
|
936
944
|
// SlackSettings.knownConnectors. Deny mode can only subtract a name it
|
|
937
945
|
// knows, so an unlisted connector is inherited by every deny-mode
|
|
@@ -1185,11 +1193,13 @@ function mergeSettings(parsed) {
|
|
|
1185
1193
|
mcpMap[k] = "full";
|
|
1186
1194
|
continue;
|
|
1187
1195
|
}
|
|
1188
|
-
|
|
1189
|
-
if (names.length > 0)
|
|
1190
|
-
mcpMap[k] = names;
|
|
1196
|
+
mcpMap[k] = v.filter((n) => n.trim().length > 0).map((n) => n.trim());
|
|
1191
1197
|
}
|
|
1192
1198
|
}
|
|
1199
|
+
let mcpDefault;
|
|
1200
|
+
if (isChannelMcpGrant(channels.mcpDefault)) {
|
|
1201
|
+
mcpDefault = channels.mcpDefault === "full" ? "full" : channels.mcpDefault.filter((n) => n.trim().length > 0).map((n) => n.trim());
|
|
1202
|
+
}
|
|
1193
1203
|
const allowedHosts = Array.isArray(server.allowedHosts) ? server.allowedHosts.filter((v) => typeof v === "string" && v.trim().length > 0).map((v) => v.trim()) : [...DEFAULT_SETTINGS.server.allowedHosts];
|
|
1194
1204
|
return {
|
|
1195
1205
|
server: {
|
|
@@ -1210,7 +1220,10 @@ function mergeSettings(parsed) {
|
|
|
1210
1220
|
},
|
|
1211
1221
|
channels: {
|
|
1212
1222
|
directory: directoryMap,
|
|
1213
|
-
mcp: mcpMap
|
|
1223
|
+
mcp: mcpMap,
|
|
1224
|
+
// Spread conditionally so an install that never set it has no key at
|
|
1225
|
+
// all, rather than one holding `undefined`.
|
|
1226
|
+
...mcpDefault !== void 0 ? { mcpDefault } : {}
|
|
1214
1227
|
},
|
|
1215
1228
|
slack: {
|
|
1216
1229
|
enabled: typeof slack.enabled === "boolean" ? slack.enabled : DEFAULT_SETTINGS.slack.enabled,
|
|
@@ -1229,6 +1242,7 @@ function mergeSettings(parsed) {
|
|
|
1229
1242
|
// falls back to the shipped default, which is the safer posture: a
|
|
1230
1243
|
// malformed file must not silently empty the deny universe.
|
|
1231
1244
|
knownConnectors: Array.isArray(slack.knownConnectors) ? slack.knownConnectors.filter((v) => typeof v === "string" && v.trim().length > 0).map((v) => v.trim()).slice(0, SLACK_KNOWN_CONNECTORS_MAX) : [...DEFAULT_SETTINGS.slack.knownConnectors],
|
|
1245
|
+
onboardNewChannels: typeof slack.onboardNewChannels === "boolean" ? slack.onboardNewChannels : DEFAULT_SETTINGS.slack.onboardNewChannels,
|
|
1232
1246
|
// Free text, carried VERBATIM — no trim, no clamp. The persona reaches
|
|
1233
1247
|
// argv byte-for-byte as the operator wrote it, which is what lets the
|
|
1234
1248
|
// prompt-cache prefix survive across the turns of a session; normalizing
|
|
@@ -3237,6 +3251,7 @@ function applySchema(target) {
|
|
|
3237
3251
|
target.exec(CREATE_MESSAGES);
|
|
3238
3252
|
target.exec(CREATE_QUEUE_ITEMS);
|
|
3239
3253
|
target.exec(CREATE_JOB_SPEND);
|
|
3254
|
+
target.exec(CREATE_SLACK_CHANNELS);
|
|
3240
3255
|
carryOverProfileSpend(target);
|
|
3241
3256
|
for (const idx of INDEXES) {
|
|
3242
3257
|
target.exec(idx);
|
|
@@ -3285,7 +3300,7 @@ function closeDb() {
|
|
|
3285
3300
|
db = null;
|
|
3286
3301
|
dbPath = null;
|
|
3287
3302
|
}
|
|
3288
|
-
var db, dbPath, CREATE_SESSIONS, CREATE_MESSAGES, CREATE_QUEUE_ITEMS, CREATE_JOB_SPEND, INDEXES;
|
|
3303
|
+
var db, dbPath, CREATE_SESSIONS, CREATE_MESSAGES, CREATE_QUEUE_ITEMS, CREATE_JOB_SPEND, CREATE_SLACK_CHANNELS, INDEXES;
|
|
3289
3304
|
var init_registry_schema = __esm({
|
|
3290
3305
|
"packages/gateway/dist/sessions/registry-schema.js"() {
|
|
3291
3306
|
"use strict";
|
|
@@ -3334,6 +3349,12 @@ CREATE TABLE IF NOT EXISTS job_spend (
|
|
|
3334
3349
|
session_id TEXT,
|
|
3335
3350
|
cost_usd REAL NOT NULL,
|
|
3336
3351
|
created_at TEXT NOT NULL
|
|
3352
|
+
)`;
|
|
3353
|
+
CREATE_SLACK_CHANNELS = `
|
|
3354
|
+
CREATE TABLE IF NOT EXISTS slack_channels (
|
|
3355
|
+
channel_id TEXT PRIMARY KEY,
|
|
3356
|
+
first_seen_at TEXT NOT NULL,
|
|
3357
|
+
last_seen_at TEXT NOT NULL
|
|
3337
3358
|
)`;
|
|
3338
3359
|
INDEXES = [
|
|
3339
3360
|
"CREATE INDEX IF NOT EXISTS idx_messages_session ON messages (session_id, created_at)",
|
|
@@ -8485,7 +8506,7 @@ var init_sessions = __esm({
|
|
|
8485
8506
|
});
|
|
8486
8507
|
|
|
8487
8508
|
// packages/gateway/dist/sessions/channel-mcp.js
|
|
8488
|
-
function
|
|
8509
|
+
function lookupChannelEntry(ref) {
|
|
8489
8510
|
const map = readSettings().channels.mcp;
|
|
8490
8511
|
if (!map || Object.keys(map).length === 0)
|
|
8491
8512
|
return void 0;
|
|
@@ -8504,6 +8525,26 @@ function lookupGrant(ref) {
|
|
|
8504
8525
|
}
|
|
8505
8526
|
return void 0;
|
|
8506
8527
|
}
|
|
8528
|
+
function isChannelIdKey(key) {
|
|
8529
|
+
return /^[A-Z][A-Z0-9]+$/.test(key);
|
|
8530
|
+
}
|
|
8531
|
+
function lookupGrant(ref) {
|
|
8532
|
+
const entry = lookupChannelEntry(ref);
|
|
8533
|
+
if (entry !== void 0)
|
|
8534
|
+
return entry;
|
|
8535
|
+
const settings = readSettings();
|
|
8536
|
+
const fallback = settings.channels.mcpDefault;
|
|
8537
|
+
if (fallback === void 0)
|
|
8538
|
+
return void 0;
|
|
8539
|
+
if (!ref.channelLabel) {
|
|
8540
|
+
const unresolvable = Object.keys(settings.channels.mcp).filter((k) => !isChannelIdKey(k));
|
|
8541
|
+
if (unresolvable.length > 0) {
|
|
8542
|
+
log28.warn("channels.mcp holds label keys that cannot be matched against a channel id, so channels.mcpDefault is withheld; key these by channel id", { channel: ref.channelId, keys: unresolvable.join(", ") });
|
|
8543
|
+
return void 0;
|
|
8544
|
+
}
|
|
8545
|
+
}
|
|
8546
|
+
return fallback;
|
|
8547
|
+
}
|
|
8507
8548
|
function resolveChannelMcpGrant(ref) {
|
|
8508
8549
|
if (!ref || !ref.channelId && !ref.channelLabel)
|
|
8509
8550
|
return void 0;
|
|
@@ -8950,6 +8991,44 @@ var init_ack_reaction = __esm({
|
|
|
8950
8991
|
}
|
|
8951
8992
|
});
|
|
8952
8993
|
|
|
8994
|
+
// packages/gateway/dist/connectors/slack/channel-record.js
|
|
8995
|
+
function hasSeenChannel(channelId) {
|
|
8996
|
+
if (!channelId)
|
|
8997
|
+
return true;
|
|
8998
|
+
try {
|
|
8999
|
+
const row = initDb().prepare("SELECT 1 as seen FROM slack_channels WHERE channel_id = ?").get(channelId);
|
|
9000
|
+
return row !== void 0;
|
|
9001
|
+
} catch (err) {
|
|
9002
|
+
log32.warn("could not read the slack channel record; treating the channel as already seen", {
|
|
9003
|
+
channel: channelId,
|
|
9004
|
+
err: err instanceof Error ? err.message : String(err)
|
|
9005
|
+
});
|
|
9006
|
+
return true;
|
|
9007
|
+
}
|
|
9008
|
+
}
|
|
9009
|
+
function recordChannelSeen(channelId) {
|
|
9010
|
+
if (!channelId)
|
|
9011
|
+
return;
|
|
9012
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9013
|
+
try {
|
|
9014
|
+
initDb().prepare("INSERT INTO slack_channels (channel_id, first_seen_at, last_seen_at) VALUES (?, ?, ?) ON CONFLICT(channel_id) DO UPDATE SET last_seen_at = excluded.last_seen_at").run(channelId, now, now);
|
|
9015
|
+
} catch (err) {
|
|
9016
|
+
log32.warn("could not record the slack channel; onboarding may repeat once in this channel", {
|
|
9017
|
+
channel: channelId,
|
|
9018
|
+
err: err instanceof Error ? err.message : String(err)
|
|
9019
|
+
});
|
|
9020
|
+
}
|
|
9021
|
+
}
|
|
9022
|
+
var log32;
|
|
9023
|
+
var init_channel_record = __esm({
|
|
9024
|
+
"packages/gateway/dist/connectors/slack/channel-record.js"() {
|
|
9025
|
+
"use strict";
|
|
9026
|
+
init_logger();
|
|
9027
|
+
init_registry_schema();
|
|
9028
|
+
log32 = logger.child("slack:channels");
|
|
9029
|
+
}
|
|
9030
|
+
});
|
|
9031
|
+
|
|
8953
9032
|
// packages/gateway/dist/connectors/slack/history.js
|
|
8954
9033
|
function sanitize(raw) {
|
|
8955
9034
|
return raw.replace(/[\[\]<>|`]/g, "").trim().slice(0, 64) || "unknown";
|
|
@@ -8987,7 +9066,7 @@ async function buildHistoryContext(opts) {
|
|
|
8987
9066
|
}
|
|
8988
9067
|
if (lines.length === 0)
|
|
8989
9068
|
return "";
|
|
8990
|
-
|
|
9069
|
+
log33.debug("primed session with slack history", { channel: opts.channel, lines: lines.length });
|
|
8991
9070
|
return [
|
|
8992
9071
|
`You are replying in the Slack ${opts.threadTs ? "thread" : "channel"} #${channelName}.`,
|
|
8993
9072
|
"Recent messages, oldest first, for context only \u2014 do not repeat or re-answer them:",
|
|
@@ -8998,12 +9077,12 @@ async function buildHistoryContext(opts) {
|
|
|
8998
9077
|
""
|
|
8999
9078
|
].join("\n");
|
|
9000
9079
|
}
|
|
9001
|
-
var
|
|
9080
|
+
var log33, NAME_TTL_MS, NameResolver;
|
|
9002
9081
|
var init_history = __esm({
|
|
9003
9082
|
"packages/gateway/dist/connectors/slack/history.js"() {
|
|
9004
9083
|
"use strict";
|
|
9005
9084
|
init_logger();
|
|
9006
|
-
|
|
9085
|
+
log33 = logger.child("slack:history");
|
|
9007
9086
|
NAME_TTL_MS = 60 * 60 * 1e3;
|
|
9008
9087
|
NameResolver = class {
|
|
9009
9088
|
web;
|
|
@@ -9041,6 +9120,46 @@ var init_history = __esm({
|
|
|
9041
9120
|
}
|
|
9042
9121
|
});
|
|
9043
9122
|
|
|
9123
|
+
// packages/gateway/dist/connectors/slack/onboarding.js
|
|
9124
|
+
function sanitizeChannelId(raw) {
|
|
9125
|
+
return raw.replace(/[^A-Za-z0-9_-]/g, "").slice(0, 32);
|
|
9126
|
+
}
|
|
9127
|
+
function buildOnboardingContext(channelId) {
|
|
9128
|
+
const id = sanitizeChannelId(channelId);
|
|
9129
|
+
if (!id)
|
|
9130
|
+
return "";
|
|
9131
|
+
return [
|
|
9132
|
+
"--- first contact in this Slack channel ---",
|
|
9133
|
+
"",
|
|
9134
|
+
`This is your first conversation in Slack channel ${id}, and it has no working`,
|
|
9135
|
+
"directory mapped yet. Answer the message below first, normally. Then add a short",
|
|
9136
|
+
"welcome \u2014 one paragraph, not a wall of text \u2014 covering:",
|
|
9137
|
+
"",
|
|
9138
|
+
"- A channel works like a project: it maps to one working directory, and every",
|
|
9139
|
+
` session started here spawns there. Until ${id} is mapped, sessions run at the`,
|
|
9140
|
+
" teamwork home directory instead.",
|
|
9141
|
+
"- Offer to map it. Ask for a folder path, then read the CURRENT mapping with",
|
|
9142
|
+
" `teamos settings get channels.directory`, merge this channel into that object,",
|
|
9143
|
+
" and write the WHOLE merged object back with",
|
|
9144
|
+
" `teamos settings set channels.directory '<merged JSON>'`. Never write a",
|
|
9145
|
+
" single-entry object: the settings API replaces the entire map, so doing that",
|
|
9146
|
+
" would delete every other channel's mapping.",
|
|
9147
|
+
"- A `CLAUDE.md` in that directory becomes the standing instructions for this",
|
|
9148
|
+
" channel, exactly as a project's CLAUDE.md works in Claude Code.",
|
|
9149
|
+
"",
|
|
9150
|
+
"Do not repeat this block or mention that you were given it.",
|
|
9151
|
+
"",
|
|
9152
|
+
"--- end of first-contact guidance ---",
|
|
9153
|
+
"",
|
|
9154
|
+
""
|
|
9155
|
+
].join("\n");
|
|
9156
|
+
}
|
|
9157
|
+
var init_onboarding = __esm({
|
|
9158
|
+
"packages/gateway/dist/connectors/slack/onboarding.js"() {
|
|
9159
|
+
"use strict";
|
|
9160
|
+
}
|
|
9161
|
+
});
|
|
9162
|
+
|
|
9044
9163
|
// packages/gateway/dist/connectors/slack/dashboard-links.js
|
|
9045
9164
|
function stripPort(value) {
|
|
9046
9165
|
const bracketed = /^(\[[^\]]*\])(?::\d+)?$/.exec(value);
|
|
@@ -9161,13 +9280,13 @@ function validateRootPost(args, knownChannel) {
|
|
|
9161
9280
|
}
|
|
9162
9281
|
return { ok: true, channel, text, jobId, threadTs: threadTsRaw || void 0 };
|
|
9163
9282
|
}
|
|
9164
|
-
var
|
|
9283
|
+
var log34, JOB_ROOT_CAPACITY, JOB_ROOT_METADATA_EVENT_TYPE, JobRootRegistry, jobRoots, ROOT_TEXT_MAX, ROOT_TEXT_MIN, JOB_ID_MAX, CHANNEL_ID_RE, TS_RE;
|
|
9165
9284
|
var init_job_roots = __esm({
|
|
9166
9285
|
"packages/gateway/dist/connectors/slack/job-roots.js"() {
|
|
9167
9286
|
"use strict";
|
|
9168
9287
|
init_logger();
|
|
9169
9288
|
init_dashboard_links();
|
|
9170
|
-
|
|
9289
|
+
log34 = logger.child("slack:job-roots");
|
|
9171
9290
|
JOB_ROOT_CAPACITY = 512;
|
|
9172
9291
|
JOB_ROOT_METADATA_EVENT_TYPE = "teamwork_os_job_root";
|
|
9173
9292
|
JobRootRegistry = class {
|
|
@@ -9187,7 +9306,7 @@ var init_job_roots = __esm({
|
|
|
9187
9306
|
break;
|
|
9188
9307
|
this.roots.delete(oldest);
|
|
9189
9308
|
}
|
|
9190
|
-
|
|
9309
|
+
log34.info("registered slack job root", { jobId: root.jobId, channel: root.channel });
|
|
9191
9310
|
return record;
|
|
9192
9311
|
}
|
|
9193
9312
|
get(channel, ts) {
|
|
@@ -9393,7 +9512,7 @@ function resolveSlackPersona() {
|
|
|
9393
9512
|
try {
|
|
9394
9513
|
persona = readSettings().slack.persona;
|
|
9395
9514
|
} catch (err) {
|
|
9396
|
-
|
|
9515
|
+
log35.warn("could not read slack.persona; turn runs with no persona", {
|
|
9397
9516
|
err: err instanceof Error ? err.message : String(err)
|
|
9398
9517
|
});
|
|
9399
9518
|
return void 0;
|
|
@@ -9403,11 +9522,11 @@ function resolveSlackPersona() {
|
|
|
9403
9522
|
if (persona.length === 0)
|
|
9404
9523
|
return void 0;
|
|
9405
9524
|
if (persona.trim().length === 0) {
|
|
9406
|
-
|
|
9525
|
+
log35.warn("slack.persona is whitespace only; turn runs with no persona");
|
|
9407
9526
|
return void 0;
|
|
9408
9527
|
}
|
|
9409
9528
|
if (persona.length > SLACK_PERSONA_MAX_LEN) {
|
|
9410
|
-
|
|
9529
|
+
log35.warn("slack.persona exceeds the maximum length; turn runs with no persona", {
|
|
9411
9530
|
length: persona.length,
|
|
9412
9531
|
max: SLACK_PERSONA_MAX_LEN
|
|
9413
9532
|
});
|
|
@@ -9415,13 +9534,13 @@ function resolveSlackPersona() {
|
|
|
9415
9534
|
}
|
|
9416
9535
|
return persona;
|
|
9417
9536
|
}
|
|
9418
|
-
var
|
|
9537
|
+
var log35;
|
|
9419
9538
|
var init_persona = __esm({
|
|
9420
9539
|
"packages/gateway/dist/connectors/slack/persona.js"() {
|
|
9421
9540
|
"use strict";
|
|
9422
9541
|
init_logger();
|
|
9423
9542
|
init_config();
|
|
9424
|
-
|
|
9543
|
+
log35 = logger.child("slack-persona");
|
|
9425
9544
|
}
|
|
9426
9545
|
});
|
|
9427
9546
|
|
|
@@ -9641,7 +9760,7 @@ async function uploadAnswerSnippet(opts) {
|
|
|
9641
9760
|
try {
|
|
9642
9761
|
const ticket = await opts.web.call("files.getUploadURLExternal", { filename, length });
|
|
9643
9762
|
if (!ticket.upload_url || !ticket.file_id) {
|
|
9644
|
-
|
|
9763
|
+
log36.warn("upload ticket was missing a url or file id");
|
|
9645
9764
|
return { ok: false, reason: "failed" };
|
|
9646
9765
|
}
|
|
9647
9766
|
const uploaded = await opts.web.uploadFileBytes(ticket.upload_url, filename, opts.content);
|
|
@@ -9653,16 +9772,16 @@ async function uploadAnswerSnippet(opts) {
|
|
|
9653
9772
|
...opts.threadTs ? { thread_ts: opts.threadTs } : {}
|
|
9654
9773
|
});
|
|
9655
9774
|
filesUploadSupport = "yes";
|
|
9656
|
-
|
|
9775
|
+
log36.info("uploaded the full answer as a snippet", { channel: opts.channel, bytes: length });
|
|
9657
9776
|
return { ok: true, permalink: done.files?.[0]?.permalink };
|
|
9658
9777
|
} catch (err) {
|
|
9659
9778
|
const code = err instanceof SlackApiError ? err.code : "unknown_error";
|
|
9660
9779
|
if (UNSUPPORTED_UPLOAD_CODES.has(code)) {
|
|
9661
9780
|
filesUploadSupport = "no";
|
|
9662
|
-
|
|
9781
|
+
log36.info("file uploads unavailable; long answers will continue as thread replies", { code });
|
|
9663
9782
|
return { ok: false, reason: "unsupported" };
|
|
9664
9783
|
}
|
|
9665
|
-
|
|
9784
|
+
log36.warn("snippet upload failed; falling back to chunked thread replies", { code });
|
|
9666
9785
|
return { ok: false, reason: "failed" };
|
|
9667
9786
|
}
|
|
9668
9787
|
}
|
|
@@ -9673,13 +9792,13 @@ _That is the first part \u2014 the full answer is attached here: <${permalink}|$
|
|
|
9673
9792
|
|
|
9674
9793
|
_That is the first part \u2014 the full answer is attached to this thread as \`${SNIPPET_FILENAME}\`._`;
|
|
9675
9794
|
}
|
|
9676
|
-
var
|
|
9795
|
+
var log36, SNIPPET_CHUNK_THRESHOLD, SNIPPET_FILENAME, SNIPPET_TITLE, UNSUPPORTED_UPLOAD_CODES, filesUploadSupport;
|
|
9677
9796
|
var init_snippet = __esm({
|
|
9678
9797
|
"packages/gateway/dist/connectors/slack/snippet.js"() {
|
|
9679
9798
|
"use strict";
|
|
9680
9799
|
init_logger();
|
|
9681
9800
|
init_web_client();
|
|
9682
|
-
|
|
9801
|
+
log36 = logger.child("slack:snippet");
|
|
9683
9802
|
SNIPPET_CHUNK_THRESHOLD = 3;
|
|
9684
9803
|
SNIPPET_FILENAME = "answer.md";
|
|
9685
9804
|
SNIPPET_TITLE = "Full answer";
|
|
@@ -9693,7 +9812,7 @@ function chrome(text) {
|
|
|
9693
9812
|
const converted = markdownToSlackMrkdwn(text).replace(/[\r\n]+/g, " ").replace(/`{3,}/g, "`").trim();
|
|
9694
9813
|
return converted.length > CHROME_MAX ? `${converted.slice(0, CHROME_MAX - 1)}\u2026` : converted;
|
|
9695
9814
|
}
|
|
9696
|
-
var
|
|
9815
|
+
var log37, WORK_THRESHOLD_MS, WORK_THRESHOLD_TOOLS, CHECKLIST_MAX, PROGRESS_APPEND_BUDGET, SEAL_DELIVERY_ATTEMPTS, TRANSIENT_SEAL_CODES, AMBIGUOUS_SEAL_CODES, streamingSupport, UNSUPPORTED_CODES, OUTCOME_HEADER, ProgressCard, CHROME_MAX;
|
|
9697
9816
|
var init_progress_card = __esm({
|
|
9698
9817
|
"packages/gateway/dist/connectors/slack/progress-card.js"() {
|
|
9699
9818
|
"use strict";
|
|
@@ -9702,7 +9821,7 @@ var init_progress_card = __esm({
|
|
|
9702
9821
|
init_phrases();
|
|
9703
9822
|
init_snippet();
|
|
9704
9823
|
init_web_client();
|
|
9705
|
-
|
|
9824
|
+
log37 = logger.child("slack:card");
|
|
9706
9825
|
WORK_THRESHOLD_MS = 4e3;
|
|
9707
9826
|
WORK_THRESHOLD_TOOLS = 2;
|
|
9708
9827
|
CHECKLIST_MAX = 8;
|
|
@@ -9892,7 +10011,7 @@ var init_progress_card = __esm({
|
|
|
9892
10011
|
if (step.onlyForTransient && !TRANSIENT_SEAL_CODES.has(lastCode))
|
|
9893
10012
|
continue;
|
|
9894
10013
|
if (attempts >= SEAL_DELIVERY_ATTEMPTS) {
|
|
9895
|
-
|
|
10014
|
+
log37.error("seal delivery hit the attempt cap with the answer undelivered", {
|
|
9896
10015
|
attempts,
|
|
9897
10016
|
lastCode,
|
|
9898
10017
|
skipped: step.name
|
|
@@ -9905,14 +10024,14 @@ var init_progress_card = __esm({
|
|
|
9905
10024
|
return true;
|
|
9906
10025
|
} catch (err) {
|
|
9907
10026
|
if (!(err instanceof SlackApiError)) {
|
|
9908
|
-
|
|
10027
|
+
log37.error("sealing the card failed at the transport layer; not retrying (double-post risk)", {
|
|
9909
10028
|
step: step.name,
|
|
9910
10029
|
err: err instanceof Error ? err.message : String(err)
|
|
9911
10030
|
});
|
|
9912
10031
|
return false;
|
|
9913
10032
|
}
|
|
9914
10033
|
if (AMBIGUOUS_SEAL_CODES.has(err.code)) {
|
|
9915
|
-
|
|
10034
|
+
log37.error("sealing the card failed with an ambiguous outcome; not retrying (double-post risk)", {
|
|
9916
10035
|
step: step.name,
|
|
9917
10036
|
attempt: attempts,
|
|
9918
10037
|
code: err.code
|
|
@@ -9920,10 +10039,10 @@ var init_progress_card = __esm({
|
|
|
9920
10039
|
return false;
|
|
9921
10040
|
}
|
|
9922
10041
|
lastCode = err.code;
|
|
9923
|
-
|
|
10042
|
+
log37.error("seal delivery step failed", { step: step.name, attempt: attempts, code: err.code });
|
|
9924
10043
|
}
|
|
9925
10044
|
}
|
|
9926
|
-
|
|
10045
|
+
log37.error("the turn produced an answer that could not be delivered to Slack", { attempts, lastCode });
|
|
9927
10046
|
return false;
|
|
9928
10047
|
}
|
|
9929
10048
|
/**
|
|
@@ -10046,9 +10165,9 @@ ${head}`
|
|
|
10046
10165
|
const recipientMayBeAtFault = sentRecipient && code === "invalid_arguments";
|
|
10047
10166
|
if (UNSUPPORTED_CODES.has(code) && !recipientMayBeAtFault) {
|
|
10048
10167
|
streamingSupport = "no";
|
|
10049
|
-
|
|
10168
|
+
log37.info("streaming API unavailable; using batched progress messages", { code });
|
|
10050
10169
|
} else {
|
|
10051
|
-
|
|
10170
|
+
log37.warn("chat.startStream failed; falling back to a batched card for this turn", { code });
|
|
10052
10171
|
}
|
|
10053
10172
|
}
|
|
10054
10173
|
}
|
|
@@ -10184,12 +10303,12 @@ var init_routing = __esm({
|
|
|
10184
10303
|
|
|
10185
10304
|
// packages/gateway/dist/connectors/slack/socket-client.js
|
|
10186
10305
|
import { WebSocket as WebSocket2 } from "ws";
|
|
10187
|
-
var
|
|
10306
|
+
var log38, BACKOFF_BASE_MS, BACKOFF_MAX_MS, SocketModeClient;
|
|
10188
10307
|
var init_socket_client = __esm({
|
|
10189
10308
|
"packages/gateway/dist/connectors/slack/socket-client.js"() {
|
|
10190
10309
|
"use strict";
|
|
10191
10310
|
init_logger();
|
|
10192
|
-
|
|
10311
|
+
log38 = logger.child("slack:socket");
|
|
10193
10312
|
BACKOFF_BASE_MS = 1e3;
|
|
10194
10313
|
BACKOFF_MAX_MS = 6e4;
|
|
10195
10314
|
SocketModeClient = class {
|
|
@@ -10211,7 +10330,7 @@ var init_socket_client = __esm({
|
|
|
10211
10330
|
return;
|
|
10212
10331
|
this.stopped = false;
|
|
10213
10332
|
this.connectLoop = this.run().catch((err) => {
|
|
10214
|
-
|
|
10333
|
+
log38.error("socket loop exited unexpectedly", { err: err instanceof Error ? err.message : String(err) });
|
|
10215
10334
|
});
|
|
10216
10335
|
}
|
|
10217
10336
|
async stop() {
|
|
@@ -10237,7 +10356,7 @@ var init_socket_client = __esm({
|
|
|
10237
10356
|
} catch (err) {
|
|
10238
10357
|
this.attempt++;
|
|
10239
10358
|
const wait2 = this.backoff();
|
|
10240
|
-
|
|
10359
|
+
log38.warn("apps.connections.open failed; retrying", {
|
|
10241
10360
|
attempt: this.attempt,
|
|
10242
10361
|
waitMs: wait2,
|
|
10243
10362
|
err: err instanceof Error ? err.message : String(err)
|
|
@@ -10255,7 +10374,7 @@ var init_socket_client = __esm({
|
|
|
10255
10374
|
}
|
|
10256
10375
|
this.attempt++;
|
|
10257
10376
|
const wait = this.backoff();
|
|
10258
|
-
|
|
10377
|
+
log38.warn("socket closed; reconnecting", { attempt: this.attempt, waitMs: wait, reason: closeReason });
|
|
10259
10378
|
await sleep(wait);
|
|
10260
10379
|
}
|
|
10261
10380
|
}
|
|
@@ -10283,18 +10402,18 @@ var init_socket_client = __esm({
|
|
|
10283
10402
|
try {
|
|
10284
10403
|
envelope = JSON.parse(String(raw));
|
|
10285
10404
|
} catch {
|
|
10286
|
-
|
|
10405
|
+
log38.debug("unparseable socket frame dropped");
|
|
10287
10406
|
return;
|
|
10288
10407
|
}
|
|
10289
10408
|
if (envelope.type === "hello") {
|
|
10290
10409
|
this.attempt = 0;
|
|
10291
|
-
|
|
10410
|
+
log38.info("socket mode connected");
|
|
10292
10411
|
this.opts.onConnectionChange?.(true, 0);
|
|
10293
10412
|
return;
|
|
10294
10413
|
}
|
|
10295
10414
|
if (envelope.type === "disconnect") {
|
|
10296
10415
|
disconnectReason = envelope.reason ?? "disconnect";
|
|
10297
|
-
|
|
10416
|
+
log38.info("socket disconnect requested by slack", { reason: disconnectReason });
|
|
10298
10417
|
try {
|
|
10299
10418
|
socket.close(1e3, "slack requested disconnect");
|
|
10300
10419
|
} catch {
|
|
@@ -10306,17 +10425,17 @@ var init_socket_client = __esm({
|
|
|
10306
10425
|
try {
|
|
10307
10426
|
socket.send(JSON.stringify({ envelope_id: envelope.envelope_id }));
|
|
10308
10427
|
} catch (err) {
|
|
10309
|
-
|
|
10428
|
+
log38.warn("ack send failed", { err: err instanceof Error ? err.message : String(err) });
|
|
10310
10429
|
}
|
|
10311
10430
|
}
|
|
10312
10431
|
try {
|
|
10313
10432
|
this.opts.onEnvelope(envelope);
|
|
10314
10433
|
} catch (err) {
|
|
10315
|
-
|
|
10434
|
+
log38.error("envelope handler threw", { err: err instanceof Error ? err.message : String(err) });
|
|
10316
10435
|
}
|
|
10317
10436
|
});
|
|
10318
10437
|
socket.on("error", (err) => {
|
|
10319
|
-
|
|
10438
|
+
log38.warn("socket error", { err: err.message });
|
|
10320
10439
|
});
|
|
10321
10440
|
socket.on("close", () => {
|
|
10322
10441
|
settle(disconnectReason);
|
|
@@ -10353,12 +10472,12 @@ function startThreadStatus(opts) {
|
|
|
10353
10472
|
} catch (err) {
|
|
10354
10473
|
const code = err instanceof SlackApiError ? err.code : "unknown_error";
|
|
10355
10474
|
if (TERMINAL.has(code)) {
|
|
10356
|
-
|
|
10475
|
+
log39.debug("thread status unavailable for this thread", { code });
|
|
10357
10476
|
stopped = true;
|
|
10358
10477
|
clear();
|
|
10359
10478
|
return;
|
|
10360
10479
|
}
|
|
10361
|
-
|
|
10480
|
+
log39.debug("setStatus failed", { code });
|
|
10362
10481
|
}
|
|
10363
10482
|
};
|
|
10364
10483
|
void send(phrase);
|
|
@@ -10389,13 +10508,13 @@ function startThreadStatus(opts) {
|
|
|
10389
10508
|
}
|
|
10390
10509
|
};
|
|
10391
10510
|
}
|
|
10392
|
-
var
|
|
10511
|
+
var log39, KEEPALIVE_MS, MAX_DURATION_MS, TERMINAL;
|
|
10393
10512
|
var init_thread_status = __esm({
|
|
10394
10513
|
"packages/gateway/dist/connectors/slack/thread-status.js"() {
|
|
10395
10514
|
"use strict";
|
|
10396
10515
|
init_logger();
|
|
10397
10516
|
init_web_client();
|
|
10398
|
-
|
|
10517
|
+
log39 = logger.child("slack:status");
|
|
10399
10518
|
KEEPALIVE_MS = 3e3;
|
|
10400
10519
|
MAX_DURATION_MS = 6e5;
|
|
10401
10520
|
TERMINAL = /* @__PURE__ */ new Set(["invalid_thread_ts", "not_allowed_token_type", "missing_scope", "unknown_method"]);
|
|
@@ -10513,7 +10632,7 @@ async function handleLinkShared(event, deps) {
|
|
|
10513
10632
|
metadata ??= entityMetadata(target, card, link.url);
|
|
10514
10633
|
}
|
|
10515
10634
|
if (Object.keys(unfurls).length === 0) {
|
|
10516
|
-
|
|
10635
|
+
log40.debug("link_shared carried no renderable gateway links", { channel: event.channel });
|
|
10517
10636
|
return false;
|
|
10518
10637
|
}
|
|
10519
10638
|
try {
|
|
@@ -10526,7 +10645,7 @@ async function handleLinkShared(event, deps) {
|
|
|
10526
10645
|
...metadata ? { metadata } : {}
|
|
10527
10646
|
});
|
|
10528
10647
|
unfurlSupport = "yes";
|
|
10529
|
-
|
|
10648
|
+
log40.debug("unfurled gateway links", { channel: event.channel, links: Object.keys(unfurls).length });
|
|
10530
10649
|
return true;
|
|
10531
10650
|
} catch (err) {
|
|
10532
10651
|
noteUnfurlFailure("chat.unfurl", err);
|
|
@@ -10540,12 +10659,12 @@ async function handleEntityDetails(event, deps) {
|
|
|
10540
10659
|
const source = deps.source ?? liveStatusSource;
|
|
10541
10660
|
const target = parseDashboardUrl(event.url, gatewayHosts(settings));
|
|
10542
10661
|
if (!target) {
|
|
10543
|
-
|
|
10662
|
+
log40.debug("entity_details_requested for a url this gateway does not own");
|
|
10544
10663
|
return false;
|
|
10545
10664
|
}
|
|
10546
10665
|
const card = buildStatusCard(target, source);
|
|
10547
10666
|
if (!card) {
|
|
10548
|
-
|
|
10667
|
+
log40.debug("entity_details_requested for an object that no longer exists", { kind: target.kind });
|
|
10549
10668
|
return false;
|
|
10550
10669
|
}
|
|
10551
10670
|
try {
|
|
@@ -10553,7 +10672,7 @@ async function handleEntityDetails(event, deps) {
|
|
|
10553
10672
|
trigger_id: event.triggerId,
|
|
10554
10673
|
metadata: entityMetadata(target, card, event.url)
|
|
10555
10674
|
});
|
|
10556
|
-
|
|
10675
|
+
log40.debug("presented entity details", { kind: target.kind });
|
|
10557
10676
|
return true;
|
|
10558
10677
|
} catch (err) {
|
|
10559
10678
|
noteUnfurlFailure("entity.presentDetails", err);
|
|
@@ -10564,12 +10683,12 @@ function noteUnfurlFailure(method, err) {
|
|
|
10564
10683
|
const code = err instanceof SlackApiError ? err.code : "transport_error";
|
|
10565
10684
|
if (UNSUPPORTED_UNFURL_CODES.has(code)) {
|
|
10566
10685
|
unfurlSupport = "no";
|
|
10567
|
-
|
|
10686
|
+
log40.info("work-object previews unavailable; links will render as plain urls", { method, code });
|
|
10568
10687
|
return;
|
|
10569
10688
|
}
|
|
10570
|
-
|
|
10689
|
+
log40.debug("work-object preview failed", { method, code });
|
|
10571
10690
|
}
|
|
10572
|
-
var
|
|
10691
|
+
var log40, UNSUPPORTED_UNFURL_CODES, unfurlSupport, liveStatusSource;
|
|
10573
10692
|
var init_work_objects = __esm({
|
|
10574
10693
|
"packages/gateway/dist/connectors/slack/work-objects.js"() {
|
|
10575
10694
|
"use strict";
|
|
@@ -10580,7 +10699,7 @@ var init_work_objects = __esm({
|
|
|
10580
10699
|
init_registry();
|
|
10581
10700
|
init_dashboard_links();
|
|
10582
10701
|
init_web_client();
|
|
10583
|
-
|
|
10702
|
+
log40 = logger.child("slack:work-objects");
|
|
10584
10703
|
UNSUPPORTED_UNFURL_CODES = /* @__PURE__ */ new Set([
|
|
10585
10704
|
"missing_scope",
|
|
10586
10705
|
"not_allowed_token_type",
|
|
@@ -10600,6 +10719,9 @@ var init_work_objects = __esm({
|
|
|
10600
10719
|
|
|
10601
10720
|
// packages/gateway/dist/connectors/slack/connector.js
|
|
10602
10721
|
import { randomBytes as randomBytes7 } from "node:crypto";
|
|
10722
|
+
function isDirectMessage(event) {
|
|
10723
|
+
return event.channelType === "im" || event.channel.startsWith("D");
|
|
10724
|
+
}
|
|
10603
10725
|
function lastAssistantText(sessionId) {
|
|
10604
10726
|
const messages = getMessages(sessionId);
|
|
10605
10727
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -10616,13 +10738,13 @@ async function startSlackConnector() {
|
|
|
10616
10738
|
return;
|
|
10617
10739
|
const settings = readSettings();
|
|
10618
10740
|
if (!settings.slack.enabled) {
|
|
10619
|
-
|
|
10741
|
+
log41.info("slack connector disabled in settings");
|
|
10620
10742
|
return;
|
|
10621
10743
|
}
|
|
10622
10744
|
const botToken = process.env.SLACK_BOT_TOKEN?.trim();
|
|
10623
10745
|
const appToken = process.env.SLACK_APP_TOKEN?.trim();
|
|
10624
10746
|
if (!botToken || !appToken) {
|
|
10625
|
-
|
|
10747
|
+
log41.warn("slack connector not started: SLACK_BOT_TOKEN and SLACK_APP_TOKEN must both be set");
|
|
10626
10748
|
return;
|
|
10627
10749
|
}
|
|
10628
10750
|
const connector = new SlackConnector({ botToken, appToken });
|
|
@@ -10630,7 +10752,7 @@ async function startSlackConnector() {
|
|
|
10630
10752
|
await connector.start();
|
|
10631
10753
|
active = connector;
|
|
10632
10754
|
} catch (err) {
|
|
10633
|
-
|
|
10755
|
+
log41.error("slack connector failed to start", { err: err instanceof Error ? err.message : String(err) });
|
|
10634
10756
|
}
|
|
10635
10757
|
}
|
|
10636
10758
|
async function stopSlackConnector() {
|
|
@@ -10638,7 +10760,7 @@ async function stopSlackConnector() {
|
|
|
10638
10760
|
active = null;
|
|
10639
10761
|
await connector?.stop();
|
|
10640
10762
|
}
|
|
10641
|
-
var
|
|
10763
|
+
var log41, MISSING_SESSION_RE, CONVERSATION_CAPACITY, SlackConnector, active;
|
|
10642
10764
|
var init_connector = __esm({
|
|
10643
10765
|
"packages/gateway/dist/connectors/slack/connector.js"() {
|
|
10644
10766
|
"use strict";
|
|
@@ -10652,7 +10774,9 @@ var init_connector = __esm({
|
|
|
10652
10774
|
init_claude_json();
|
|
10653
10775
|
init_resolver();
|
|
10654
10776
|
init_ack_reaction();
|
|
10777
|
+
init_channel_record();
|
|
10655
10778
|
init_history();
|
|
10779
|
+
init_onboarding();
|
|
10656
10780
|
init_job_roots();
|
|
10657
10781
|
init_normalize2();
|
|
10658
10782
|
init_persona();
|
|
@@ -10663,11 +10787,10 @@ var init_connector = __esm({
|
|
|
10663
10787
|
init_thread_status();
|
|
10664
10788
|
init_web_client();
|
|
10665
10789
|
init_work_objects();
|
|
10666
|
-
|
|
10790
|
+
log41 = logger.child("slack");
|
|
10667
10791
|
MISSING_SESSION_RE = /^Session .+ not found$/;
|
|
10668
10792
|
CONVERSATION_CAPACITY = 512;
|
|
10669
10793
|
SlackConnector = class {
|
|
10670
|
-
opts;
|
|
10671
10794
|
web;
|
|
10672
10795
|
appWeb;
|
|
10673
10796
|
names;
|
|
@@ -10713,8 +10836,33 @@ var init_connector = __esm({
|
|
|
10713
10836
|
* after an event survives the allowlist and every filter, so traffic the
|
|
10714
10837
|
* connector refused never widens the bound. */
|
|
10715
10838
|
seenChannels = /* @__PURE__ */ new Set();
|
|
10839
|
+
/**
|
|
10840
|
+
* Channels this process has already decided to onboard, claimed
|
|
10841
|
+
* SYNCHRONOUSLY the moment the decision is made.
|
|
10842
|
+
*
|
|
10843
|
+
* The durable `slack_channels` row is what makes onboarding once-per-channel
|
|
10844
|
+
* across restarts, but it is written after the turn is dispatched — and
|
|
10845
|
+
* between the decision and that write sit an ack reaction and `route()`,
|
|
10846
|
+
* both real round-trips. Two first messages in a new channel land on two
|
|
10847
|
+
* DIFFERENT conversation keys, so the per-conversation gate does not
|
|
10848
|
+
* serialize them: both would read "never seen" and both would carry the
|
|
10849
|
+
* block. Reproduced with an 8ms ack: 2 of 2 prompts.
|
|
10850
|
+
*
|
|
10851
|
+
* Same shape as the memoized `ensureCard` promise (see this package's
|
|
10852
|
+
* CLAUDE.md): a check against state that only becomes true after an await
|
|
10853
|
+
* is not a guard. The claim is a synchronous Set write, so two dispatches
|
|
10854
|
+
* cannot interleave between the check and the claim.
|
|
10855
|
+
*
|
|
10856
|
+
* A claim by a turn that is then refused is deliberately NOT released. The
|
|
10857
|
+
* cost is that a channel whose only-ever turn failed onboards again after a
|
|
10858
|
+
* restart — one extra block, the same direction a failed row write already
|
|
10859
|
+
* degrades in, and better than the duplicate this prevents.
|
|
10860
|
+
*/
|
|
10861
|
+
onboardedChannels = /* @__PURE__ */ new Set();
|
|
10862
|
+
// A plain parameter, not a parameter property: nothing outside the
|
|
10863
|
+
// constructor reads it, and a `private readonly opts` field would be dead
|
|
10864
|
+
// state that reads as if the connector kept its tokens around.
|
|
10716
10865
|
constructor(opts) {
|
|
10717
|
-
this.opts = opts;
|
|
10718
10866
|
const factory = opts.webFactory ?? ((token) => new SlackWebClient({ token }));
|
|
10719
10867
|
this.web = factory(opts.botToken);
|
|
10720
10868
|
this.appWeb = factory(opts.appToken);
|
|
@@ -10742,7 +10890,7 @@ var init_connector = __esm({
|
|
|
10742
10890
|
this.botUserId = auth.user_id;
|
|
10743
10891
|
this.botId = auth.bot_id;
|
|
10744
10892
|
this.teamId = auth.team_id ?? "";
|
|
10745
|
-
|
|
10893
|
+
log41.info("slack auth ok", { botUserId: this.botUserId, team: auth.team, teamId: this.teamId });
|
|
10746
10894
|
this.socket = new SocketModeClient({
|
|
10747
10895
|
appWeb: this.appWeb,
|
|
10748
10896
|
onEnvelope: (envelope) => this.onEnvelope(envelope),
|
|
@@ -10778,7 +10926,7 @@ var init_connector = __esm({
|
|
|
10778
10926
|
const event = normalizeEnvelope(envelope);
|
|
10779
10927
|
if (!event)
|
|
10780
10928
|
return;
|
|
10781
|
-
|
|
10929
|
+
log41.debug("slack event received", {
|
|
10782
10930
|
kind: event.kind,
|
|
10783
10931
|
channel: event.channel,
|
|
10784
10932
|
user: event.user || void 0,
|
|
@@ -10787,11 +10935,11 @@ var init_connector = __esm({
|
|
|
10787
10935
|
retryAttempt: event.retryAttempt
|
|
10788
10936
|
});
|
|
10789
10937
|
if (!this.dedupe.admit(event.dedupeKey)) {
|
|
10790
|
-
|
|
10938
|
+
log41.info("duplicate slack event dropped", { dedupeKey: event.dedupeKey, retryAttempt: event.retryAttempt });
|
|
10791
10939
|
return;
|
|
10792
10940
|
}
|
|
10793
10941
|
if (event.kind === "assistant_thread_started" || event.kind === "assistant_thread_context_changed") {
|
|
10794
|
-
|
|
10942
|
+
log41.info("assistant thread event", { kind: event.kind, channel: event.channel });
|
|
10795
10943
|
return;
|
|
10796
10944
|
}
|
|
10797
10945
|
if (event.kind === "link_shared") {
|
|
@@ -10808,7 +10956,7 @@ var init_connector = __esm({
|
|
|
10808
10956
|
if (refusal) {
|
|
10809
10957
|
const selfAuthored = Boolean(event.botId) || this.botUserId.length > 0 && event.user === this.botUserId;
|
|
10810
10958
|
const level = selfAuthored || event.subtype ? "debug" : "info";
|
|
10811
|
-
|
|
10959
|
+
log41[level]("slack event refused by allowlist", {
|
|
10812
10960
|
reason: refusal,
|
|
10813
10961
|
channel: event.channel,
|
|
10814
10962
|
kind: event.kind,
|
|
@@ -10826,12 +10974,12 @@ var init_connector = __esm({
|
|
|
10826
10974
|
threadMapped: this.conversations.has(key) || Boolean(event.threadTs) && jobRoots.has(event.channel, event.threadTs)
|
|
10827
10975
|
});
|
|
10828
10976
|
if (drop) {
|
|
10829
|
-
|
|
10977
|
+
log41.debug("slack event filtered", { reason: drop, kind: event.kind, dedupeKey: event.dedupeKey });
|
|
10830
10978
|
return;
|
|
10831
10979
|
}
|
|
10832
10980
|
const identity = messageIdentityKey(event);
|
|
10833
10981
|
if (identity && !this.messageIdentity.admit(identity)) {
|
|
10834
|
-
|
|
10982
|
+
log41.debug("slack event filtered", {
|
|
10835
10983
|
reason: "duplicate-message-identity",
|
|
10836
10984
|
kind: event.kind,
|
|
10837
10985
|
dedupeKey: event.dedupeKey,
|
|
@@ -10841,8 +10989,11 @@ var init_connector = __esm({
|
|
|
10841
10989
|
}
|
|
10842
10990
|
void this.dispatch(event).then((started) => {
|
|
10843
10991
|
if (started) {
|
|
10844
|
-
if (event.channel)
|
|
10992
|
+
if (event.channel) {
|
|
10845
10993
|
this.seenChannels.add(event.channel);
|
|
10994
|
+
if (!isDirectMessage(event))
|
|
10995
|
+
recordChannelSeen(event.channel);
|
|
10996
|
+
}
|
|
10846
10997
|
return;
|
|
10847
10998
|
}
|
|
10848
10999
|
if (identity)
|
|
@@ -10850,7 +11001,7 @@ var init_connector = __esm({
|
|
|
10850
11001
|
}).catch((err) => {
|
|
10851
11002
|
if (identity)
|
|
10852
11003
|
this.messageIdentity.release(identity);
|
|
10853
|
-
|
|
11004
|
+
log41.error("slack dispatch failed", { err: err instanceof Error ? err.message : String(err) });
|
|
10854
11005
|
});
|
|
10855
11006
|
}
|
|
10856
11007
|
/**
|
|
@@ -10868,7 +11019,7 @@ var init_connector = __esm({
|
|
|
10868
11019
|
if (links.length === 0)
|
|
10869
11020
|
return;
|
|
10870
11021
|
if (!this.knowsChannel(event.channel)) {
|
|
10871
|
-
|
|
11022
|
+
log41.debug("link_shared ignored: channel is not one this gateway is configured for", { channel: event.channel });
|
|
10872
11023
|
return;
|
|
10873
11024
|
}
|
|
10874
11025
|
try {
|
|
@@ -10880,7 +11031,7 @@ var init_connector = __esm({
|
|
|
10880
11031
|
source: event.unfurlSource
|
|
10881
11032
|
}, { web: this.web });
|
|
10882
11033
|
} catch (err) {
|
|
10883
|
-
|
|
11034
|
+
log41.debug("link preview failed", { err: err instanceof Error ? err.message : String(err) });
|
|
10884
11035
|
}
|
|
10885
11036
|
}
|
|
10886
11037
|
/**
|
|
@@ -10914,11 +11065,11 @@ var init_connector = __esm({
|
|
|
10914
11065
|
*/
|
|
10915
11066
|
async presentEntityDetails(event) {
|
|
10916
11067
|
if (!event.triggerId || !event.linkUrl) {
|
|
10917
|
-
|
|
11068
|
+
log41.debug("entity_details_requested without a trigger id or url");
|
|
10918
11069
|
return;
|
|
10919
11070
|
}
|
|
10920
11071
|
if (event.channel && !this.knowsChannel(event.channel)) {
|
|
10921
|
-
|
|
11072
|
+
log41.debug("entity_details_requested ignored: channel is not one this gateway is configured for", {
|
|
10922
11073
|
channel: event.channel
|
|
10923
11074
|
});
|
|
10924
11075
|
return;
|
|
@@ -10926,7 +11077,7 @@ var init_connector = __esm({
|
|
|
10926
11077
|
try {
|
|
10927
11078
|
await handleEntityDetails({ triggerId: event.triggerId, url: event.linkUrl, externalRefId: event.externalRefId }, { web: this.web });
|
|
10928
11079
|
} catch (err) {
|
|
10929
|
-
|
|
11080
|
+
log41.debug("entity details failed", { err: err instanceof Error ? err.message : String(err) });
|
|
10930
11081
|
}
|
|
10931
11082
|
}
|
|
10932
11083
|
/**
|
|
@@ -11021,7 +11172,7 @@ var init_connector = __esm({
|
|
|
11021
11172
|
threadTs: target.thread,
|
|
11022
11173
|
trigger: event.kind === "reaction_added" ? "reaction" : event.kind === "app_mention" ? "mention" : event.channelType === "im" ? "assistant" : "message"
|
|
11023
11174
|
});
|
|
11024
|
-
|
|
11175
|
+
log41.info("slack turn dispatched", {
|
|
11025
11176
|
sessionId,
|
|
11026
11177
|
channel: target.channel,
|
|
11027
11178
|
thread: target.thread,
|
|
@@ -11038,7 +11189,7 @@ var init_connector = __esm({
|
|
|
11038
11189
|
*/
|
|
11039
11190
|
async resolveSession(event, target, key, jobId, settings) {
|
|
11040
11191
|
const existingSessionId = this.conversations.get(key);
|
|
11041
|
-
const prompt = await this.buildPrompt(event, target, Boolean(existingSessionId), settings
|
|
11192
|
+
const prompt = await this.buildPrompt(event, target, Boolean(existingSessionId), settings, jobId);
|
|
11042
11193
|
if (!prompt)
|
|
11043
11194
|
return null;
|
|
11044
11195
|
const ack = target.triggerTs ? await addAck(this.web, settings.slack.ackEmoji, target.channel, target.triggerTs) : null;
|
|
@@ -11050,12 +11201,12 @@ var init_connector = __esm({
|
|
|
11050
11201
|
} catch (err) {
|
|
11051
11202
|
const detail = err instanceof Error ? err.message : String(err);
|
|
11052
11203
|
if (existingSessionId && MISSING_SESSION_RE.test(detail)) {
|
|
11053
|
-
|
|
11204
|
+
log41.info("slack conversation session no longer exists; rebuilding from thread history", {
|
|
11054
11205
|
key,
|
|
11055
11206
|
sessionId: existingSessionId
|
|
11056
11207
|
});
|
|
11057
11208
|
this.conversations.delete(key);
|
|
11058
|
-
const revived = await this.buildPrompt(event, target, false, settings
|
|
11209
|
+
const revived = await this.buildPrompt(event, target, false, settings, jobId);
|
|
11059
11210
|
try {
|
|
11060
11211
|
sessionId = await this.routeTurn(revived || prompt, void 0, target, jobId);
|
|
11061
11212
|
notice = SESSION_REVIVED_NOTICE;
|
|
@@ -11068,7 +11219,7 @@ var init_connector = __esm({
|
|
|
11068
11219
|
}
|
|
11069
11220
|
if (sessionId === null) {
|
|
11070
11221
|
const detail = failure ?? "unknown error";
|
|
11071
|
-
|
|
11222
|
+
log41.error("slack route failed", { channel: target.channel, thread: target.thread, err: detail });
|
|
11072
11223
|
await swapAck(this.web, ack, settings.slack.errorEmoji);
|
|
11073
11224
|
await this.web.tryCall("chat.postMessage", {
|
|
11074
11225
|
channel: target.channel,
|
|
@@ -11172,7 +11323,7 @@ var init_connector = __esm({
|
|
|
11172
11323
|
const wantsGateway = named.includes(GATEWAY_MCP_NAME);
|
|
11173
11324
|
const { servers, missing } = resolveNamedMcpServers(named.filter((n) => n !== GATEWAY_MCP_NAME));
|
|
11174
11325
|
if (missing.length > 0) {
|
|
11175
|
-
|
|
11326
|
+
log41.warn("channels.mcp names servers absent from the claude config; skipping them", {
|
|
11176
11327
|
channel,
|
|
11177
11328
|
missing: missing.join(", ")
|
|
11178
11329
|
});
|
|
@@ -11180,7 +11331,7 @@ var init_connector = __esm({
|
|
|
11180
11331
|
const resolved = wantsGateway ? resolveMcpConfig(servers, { sessionId, jobId }).mcpServers : servers;
|
|
11181
11332
|
const names = Object.keys(resolved);
|
|
11182
11333
|
if (names.length === 0) {
|
|
11183
|
-
|
|
11334
|
+
log41.warn("channels.mcp grant resolved to no servers; turn runs with none", { channel });
|
|
11184
11335
|
return noop;
|
|
11185
11336
|
}
|
|
11186
11337
|
const configKey = `slack-${randomBytes7(8).toString("hex")}`;
|
|
@@ -11188,14 +11339,14 @@ var init_connector = __esm({
|
|
|
11188
11339
|
try {
|
|
11189
11340
|
configPath = writeMcpConfigFile({ mcpServers: resolved }, configKey);
|
|
11190
11341
|
} catch (err) {
|
|
11191
|
-
|
|
11342
|
+
log41.warn("could not write the channel MCP config; turn runs with no servers", {
|
|
11192
11343
|
channel,
|
|
11193
11344
|
err: err instanceof Error ? err.message : String(err)
|
|
11194
11345
|
});
|
|
11195
11346
|
cleanupMcpConfigFile(configKey);
|
|
11196
11347
|
return noop;
|
|
11197
11348
|
}
|
|
11198
|
-
|
|
11349
|
+
log41.info("channel MCP grant resolved", { channel, servers: names.join(", ") });
|
|
11199
11350
|
return { inherit: false, configPath, cleanup: () => cleanupMcpConfigFile(configKey) };
|
|
11200
11351
|
}
|
|
11201
11352
|
/**
|
|
@@ -11214,11 +11365,11 @@ var init_connector = __esm({
|
|
|
11214
11365
|
} };
|
|
11215
11366
|
const grant = named.filter((n) => n !== GATEWAY_MCP_NAME);
|
|
11216
11367
|
if (grant.length !== named.length) {
|
|
11217
|
-
|
|
11368
|
+
log41.warn('channels.mcp grants "gateway" alongside a claude.ai connector; the gateway MCP server is unavailable in deny mode', { channel });
|
|
11218
11369
|
}
|
|
11219
11370
|
const ambient = readAmbientMcpServersResult();
|
|
11220
11371
|
if (!ambient.ok) {
|
|
11221
|
-
|
|
11372
|
+
log41.warn("ambient MCP config unreadable; deny-mode turn runs with no servers rather than under-subtracting", {
|
|
11222
11373
|
channel,
|
|
11223
11374
|
reason: ambient.reason ?? "unknown"
|
|
11224
11375
|
});
|
|
@@ -11230,18 +11381,18 @@ var init_connector = __esm({
|
|
|
11230
11381
|
knownConnectors: readSettings().slack.knownConnectors
|
|
11231
11382
|
});
|
|
11232
11383
|
if (unresolvedLocals.length > 0) {
|
|
11233
|
-
|
|
11384
|
+
log41.warn("channels.mcp names local servers absent from the claude config; they resolve to nothing", {
|
|
11234
11385
|
channel,
|
|
11235
11386
|
names: unresolvedLocals.join(", ")
|
|
11236
11387
|
});
|
|
11237
11388
|
}
|
|
11238
11389
|
if (unlistedConnectors.length > 0) {
|
|
11239
|
-
|
|
11390
|
+
log41.warn("channels.mcp grants connectors missing from slack.knownConnectors; other deny-mode channels cannot subtract them", {
|
|
11240
11391
|
channel,
|
|
11241
11392
|
names: unlistedConnectors.join(", ")
|
|
11242
11393
|
});
|
|
11243
11394
|
}
|
|
11244
|
-
|
|
11395
|
+
log41.info("channel MCP grant resolved (deny mode)", {
|
|
11245
11396
|
channel,
|
|
11246
11397
|
granted: grant.length > 0 ? grant.join(", ") : "(none)",
|
|
11247
11398
|
denied: denies.length
|
|
@@ -11249,9 +11400,10 @@ var init_connector = __esm({
|
|
|
11249
11400
|
return { inherit: true, disallowedTools: denies, cleanup: () => {
|
|
11250
11401
|
} };
|
|
11251
11402
|
}
|
|
11252
|
-
/** Assemble the prompt: the user's text, plus the job-root
|
|
11253
|
-
* channel/thread history context on the first turn of a conversation
|
|
11254
|
-
|
|
11403
|
+
/** Assemble the prompt: the user's text, plus the first-contact, job-root
|
|
11404
|
+
* and channel/thread history context on the first turn of a conversation
|
|
11405
|
+
* only. */
|
|
11406
|
+
async buildPrompt(event, target, isResume, settings, jobId) {
|
|
11255
11407
|
if (event.kind === "reaction_added") {
|
|
11256
11408
|
const r = await this.web.tryCall("conversations.history", {
|
|
11257
11409
|
channel: event.channel,
|
|
@@ -11262,13 +11414,13 @@ var init_connector = __esm({
|
|
|
11262
11414
|
});
|
|
11263
11415
|
const text2 = r?.messages?.[0]?.text?.trim() ?? "";
|
|
11264
11416
|
if (!text2) {
|
|
11265
|
-
|
|
11417
|
+
log41.info("reaction trigger dropped: its message could not be read", {
|
|
11266
11418
|
channel: event.channel,
|
|
11267
11419
|
itemTs: event.itemTs
|
|
11268
11420
|
});
|
|
11269
11421
|
return "";
|
|
11270
11422
|
}
|
|
11271
|
-
return stripLeadingMention(text2, this.botUserId)
|
|
11423
|
+
return `${this.onboardingContext(event, isResume, settings)}${stripLeadingMention(text2, this.botUserId)}`;
|
|
11272
11424
|
}
|
|
11273
11425
|
const text = stripLeadingMention(event.text, this.botUserId);
|
|
11274
11426
|
if (isResume)
|
|
@@ -11279,11 +11431,53 @@ var init_connector = __esm({
|
|
|
11279
11431
|
names: this.names,
|
|
11280
11432
|
channel: target.channel,
|
|
11281
11433
|
threadTs: threadRoot,
|
|
11282
|
-
limit: historyLimit,
|
|
11434
|
+
limit: settings.slack.historyLimit,
|
|
11283
11435
|
excludeTs: event.ts
|
|
11284
11436
|
});
|
|
11285
11437
|
const job = jobId ? buildJobRootContext(jobId) : "";
|
|
11286
|
-
return `${job}${history}${text}`;
|
|
11438
|
+
return `${this.onboardingContext(event, isResume, settings)}${job}${history}${text}`;
|
|
11439
|
+
}
|
|
11440
|
+
/**
|
|
11441
|
+
* The first-contact guidance block, or an empty string — which is what every
|
|
11442
|
+
* turn but one per channel gets.
|
|
11443
|
+
*
|
|
11444
|
+
* Five conditions, all of which must hold:
|
|
11445
|
+
*
|
|
11446
|
+
* - **not a resume.** A conversation already under way has had its first
|
|
11447
|
+
* turn; the block belongs to the channel's first turn, not to every new
|
|
11448
|
+
* thread in it. (Belt and braces next to the row check below, which a
|
|
11449
|
+
* resumed turn already fails — unless the row write itself failed.);
|
|
11450
|
+
* - **the setting is on** — `slack.onboardNewChannels`, default true;
|
|
11451
|
+
* - **not a DM.** A DM is reached through `slack.dmAllowlist`, which is a
|
|
11452
|
+
* deliberate per-user act by the operator; there is nobody there to
|
|
11453
|
+
* onboard. See `isDirectMessage` for why the `channel_type` test alone is
|
|
11454
|
+
* not enough on a reaction-triggered turn;
|
|
11455
|
+
* - **the channel has no `channels.directory` entry.** An operator who
|
|
11456
|
+
* mapped a channel already knows the mapping exists — explaining it back
|
|
11457
|
+
* to them is noise. Id-only lookup, matching `allowlistRefusal` and
|
|
11458
|
+
* `knowsChannel`: the connector holds channel ids and never resolves the
|
|
11459
|
+
* `#label` form those maps also accept;
|
|
11460
|
+
* - **no `slack_channels` row**, which is the "ever, not this boot" half
|
|
11461
|
+
* and the only one that survives a restart.
|
|
11462
|
+
*/
|
|
11463
|
+
onboardingContext(event, isResume, settings) {
|
|
11464
|
+
if (isResume)
|
|
11465
|
+
return "";
|
|
11466
|
+
if (!settings.slack.onboardNewChannels)
|
|
11467
|
+
return "";
|
|
11468
|
+
if (!event.channel)
|
|
11469
|
+
return "";
|
|
11470
|
+
if (isDirectMessage(event))
|
|
11471
|
+
return "";
|
|
11472
|
+
if (Object.prototype.hasOwnProperty.call(settings.channels.directory, event.channel))
|
|
11473
|
+
return "";
|
|
11474
|
+
if (this.onboardedChannels.has(event.channel))
|
|
11475
|
+
return "";
|
|
11476
|
+
if (hasSeenChannel(event.channel))
|
|
11477
|
+
return "";
|
|
11478
|
+
this.onboardedChannels.add(event.channel);
|
|
11479
|
+
log41.info("priming first-contact guidance for a new slack channel", { channel: event.channel });
|
|
11480
|
+
return buildOnboardingContext(event.channel);
|
|
11287
11481
|
}
|
|
11288
11482
|
// ── Session lifecycle → the card ────────────────────────────────────────
|
|
11289
11483
|
onGatewayEvent(event, payload) {
|
|
@@ -11354,7 +11548,7 @@ ${answer}` : answer;
|
|
|
11354
11548
|
outcome: effective,
|
|
11355
11549
|
cardTs
|
|
11356
11550
|
});
|
|
11357
|
-
|
|
11551
|
+
log41.info("slack turn sealed", { sessionId, outcome: effective, cardTs, delivered: turn.card.delivered });
|
|
11358
11552
|
}
|
|
11359
11553
|
};
|
|
11360
11554
|
active = null;
|
|
@@ -11404,7 +11598,7 @@ async function slackRoutes(req, res, p, method) {
|
|
|
11404
11598
|
}
|
|
11405
11599
|
const rootTs = validated.threadTs ?? ts;
|
|
11406
11600
|
jobRoots.register({ channel: validated.channel, rootTs, jobId: validated.jobId, externalRefId });
|
|
11407
|
-
|
|
11601
|
+
log42.info("job root post delivered", { jobId: validated.jobId, channel: validated.channel });
|
|
11408
11602
|
json(res, { ok: true, ts, rootTs, channel: validated.channel, externalRef: externalRefId }, 200, req);
|
|
11409
11603
|
} catch (err) {
|
|
11410
11604
|
json(res, { error: errMsg(err) }, 502, req);
|
|
@@ -11413,7 +11607,7 @@ async function slackRoutes(req, res, p, method) {
|
|
|
11413
11607
|
}
|
|
11414
11608
|
return false;
|
|
11415
11609
|
}
|
|
11416
|
-
var
|
|
11610
|
+
var log42;
|
|
11417
11611
|
var init_slack = __esm({
|
|
11418
11612
|
"packages/gateway/dist/routes/slack.js"() {
|
|
11419
11613
|
"use strict";
|
|
@@ -11424,11 +11618,32 @@ var init_slack = __esm({
|
|
|
11424
11618
|
init_job_roots();
|
|
11425
11619
|
init_dashboard_links();
|
|
11426
11620
|
init_logger();
|
|
11427
|
-
|
|
11621
|
+
log42 = logger.child("routes:slack");
|
|
11428
11622
|
}
|
|
11429
11623
|
});
|
|
11430
11624
|
|
|
11431
11625
|
// packages/gateway/dist/routes/settings.js
|
|
11626
|
+
function validateMcpGrant(label, value) {
|
|
11627
|
+
if (value === "full")
|
|
11628
|
+
return { ok: true, grant: "full" };
|
|
11629
|
+
if (!Array.isArray(value)) {
|
|
11630
|
+
return { ok: false, error: `${label} must be "full" or an array of MCP server names` };
|
|
11631
|
+
}
|
|
11632
|
+
if (value.length > CHANNEL_MCP_NAMES_MAX) {
|
|
11633
|
+
return { ok: false, error: `${label} must name at most ${CHANNEL_MCP_NAMES_MAX} servers` };
|
|
11634
|
+
}
|
|
11635
|
+
const names = [];
|
|
11636
|
+
for (const raw of value) {
|
|
11637
|
+
if (typeof raw !== "string" || raw.trim().length === 0 || raw.length > MCP_SERVER_NAME_MAX_LEN) {
|
|
11638
|
+
return {
|
|
11639
|
+
ok: false,
|
|
11640
|
+
error: `${label} entries must be non-empty server names of at most ${MCP_SERVER_NAME_MAX_LEN} chars`
|
|
11641
|
+
};
|
|
11642
|
+
}
|
|
11643
|
+
names.push(raw.trim());
|
|
11644
|
+
}
|
|
11645
|
+
return { ok: true, grant: names };
|
|
11646
|
+
}
|
|
11432
11647
|
function applySettingsPatch(current, patch, ctx) {
|
|
11433
11648
|
for (const key of Object.keys(patch)) {
|
|
11434
11649
|
if (!KNOWN_TOP_LEVEL_KEYS.has(key)) {
|
|
@@ -11540,7 +11755,7 @@ function applySettingsPatch(current, patch, ctx) {
|
|
|
11540
11755
|
}
|
|
11541
11756
|
const c = patch.channels;
|
|
11542
11757
|
for (const key of Object.keys(c)) {
|
|
11543
|
-
if (key !== "directory" && key !== "mcp") {
|
|
11758
|
+
if (key !== "directory" && key !== "mcp" && key !== "mcpDefault") {
|
|
11544
11759
|
return { ok: false, error: `Unknown settings key: channels.${key.slice(0, 64)}` };
|
|
11545
11760
|
}
|
|
11546
11761
|
}
|
|
@@ -11580,36 +11795,24 @@ function applySettingsPatch(current, patch, ctx) {
|
|
|
11580
11795
|
if (key.length === 0 || key.length > CHANNEL_KEY_MAX_LEN) {
|
|
11581
11796
|
return { ok: false, error: `channels.mcp keys must be 1-${CHANNEL_KEY_MAX_LEN} chars` };
|
|
11582
11797
|
}
|
|
11583
|
-
|
|
11584
|
-
|
|
11585
|
-
|
|
11586
|
-
|
|
11587
|
-
if (!Array.isArray(value)) {
|
|
11588
|
-
return {
|
|
11589
|
-
ok: false,
|
|
11590
|
-
error: `channels.mcp["${key.slice(0, 64)}"] must be "full" or an array of MCP server names`
|
|
11591
|
-
};
|
|
11592
|
-
}
|
|
11593
|
-
if (value.length > CHANNEL_MCP_NAMES_MAX) {
|
|
11594
|
-
return {
|
|
11595
|
-
ok: false,
|
|
11596
|
-
error: `channels.mcp["${key.slice(0, 64)}"] must name at most ${CHANNEL_MCP_NAMES_MAX} servers`
|
|
11597
|
-
};
|
|
11598
|
-
}
|
|
11599
|
-
const names = [];
|
|
11600
|
-
for (const raw of value) {
|
|
11601
|
-
if (typeof raw !== "string" || raw.trim().length === 0 || raw.length > MCP_SERVER_NAME_MAX_LEN) {
|
|
11602
|
-
return {
|
|
11603
|
-
ok: false,
|
|
11604
|
-
error: `channels.mcp["${key.slice(0, 64)}"] entries must be non-empty server names of at most ${MCP_SERVER_NAME_MAX_LEN} chars`
|
|
11605
|
-
};
|
|
11606
|
-
}
|
|
11607
|
-
names.push(raw.trim());
|
|
11608
|
-
}
|
|
11609
|
-
mcpMap[key] = names;
|
|
11798
|
+
const grant = validateMcpGrant(`channels.mcp["${key.slice(0, 64)}"]`, value);
|
|
11799
|
+
if (!grant.ok)
|
|
11800
|
+
return grant;
|
|
11801
|
+
mcpMap[key] = grant.grant;
|
|
11610
11802
|
}
|
|
11611
11803
|
channels = { ...channels, mcp: mcpMap };
|
|
11612
11804
|
}
|
|
11805
|
+
if (c.mcpDefault !== void 0) {
|
|
11806
|
+
if (c.mcpDefault === null) {
|
|
11807
|
+
const { mcpDefault: _cleared, ...rest } = channels;
|
|
11808
|
+
channels = rest;
|
|
11809
|
+
} else {
|
|
11810
|
+
const grant = validateMcpGrant("channels.mcpDefault", c.mcpDefault);
|
|
11811
|
+
if (!grant.ok)
|
|
11812
|
+
return grant;
|
|
11813
|
+
channels = { ...channels, mcpDefault: grant.grant };
|
|
11814
|
+
}
|
|
11815
|
+
}
|
|
11613
11816
|
}
|
|
11614
11817
|
let scheduler = current.scheduler;
|
|
11615
11818
|
if (patch.scheduler !== void 0) {
|
|
@@ -11638,7 +11841,7 @@ function applySettingsPatch(current, patch, ctx) {
|
|
|
11638
11841
|
if (!SLACK_KNOWN_KEYS.has(key))
|
|
11639
11842
|
return { ok: false, error: `Unknown settings key: slack.${key.slice(0, 64)}` };
|
|
11640
11843
|
}
|
|
11641
|
-
for (const key of ["enabled", "requireMention", "allowUnmappedChannels"]) {
|
|
11844
|
+
for (const key of ["enabled", "requireMention", "allowUnmappedChannels", "onboardNewChannels"]) {
|
|
11642
11845
|
if (sl[key] !== void 0) {
|
|
11643
11846
|
if (typeof sl[key] !== "boolean")
|
|
11644
11847
|
return { ok: false, error: `slack.${key} must be a boolean` };
|
|
@@ -11765,14 +11968,14 @@ async function settingsRoutes(req, res, p, method) {
|
|
|
11765
11968
|
return true;
|
|
11766
11969
|
}
|
|
11767
11970
|
writeSettings(result2.settings);
|
|
11768
|
-
|
|
11971
|
+
log43.info("settings updated");
|
|
11769
11972
|
emit(SETTINGS_UPDATED, { settings: result2.settings });
|
|
11770
11973
|
json(res, result2.settings, 200, req);
|
|
11771
11974
|
return true;
|
|
11772
11975
|
}
|
|
11773
11976
|
return false;
|
|
11774
11977
|
}
|
|
11775
|
-
var
|
|
11978
|
+
var log43, RETENTION_DAYS_MIN2, RETENTION_DAYS_MAX2, BIND_HOST_MAX_LEN, KNOWN_TOP_LEVEL_KEYS, ALLOWED_HOSTS_MAX_ENTRIES, MAX_CONCURRENT_JOBS_MAX, MODEL_MAX_LEN2, TIMEOUT_MS_MAX, RETRIES_MAX, CHANNEL_KEY_MAX_LEN, CHANNEL_PATH_MAX_LEN, CHANNEL_MAP_MAX_ENTRIES, CHANNEL_MCP_NAMES_MAX, MCP_SERVER_NAME_MAX_LEN, SLACK_EMOJI_RE, SLACK_TRIGGERS_MAX, SLACK_USER_ID_RE, SLACK_KNOWN_KEYS, MCP_SERVER_NAME_RE;
|
|
11776
11979
|
var init_settings = __esm({
|
|
11777
11980
|
"packages/gateway/dist/routes/settings.js"() {
|
|
11778
11981
|
"use strict";
|
|
@@ -11784,7 +11987,7 @@ var init_settings = __esm({
|
|
|
11784
11987
|
init_bind_host();
|
|
11785
11988
|
init_websocket();
|
|
11786
11989
|
init_logger();
|
|
11787
|
-
|
|
11990
|
+
log43 = logger.child("settings");
|
|
11788
11991
|
RETENTION_DAYS_MIN2 = 1;
|
|
11789
11992
|
RETENTION_DAYS_MAX2 = 3650;
|
|
11790
11993
|
BIND_HOST_MAX_LEN = 253;
|
|
@@ -11814,6 +12017,7 @@ var init_settings = __esm({
|
|
|
11814
12017
|
"allowUnmappedChannels",
|
|
11815
12018
|
"dmAllowlist",
|
|
11816
12019
|
"knownConnectors",
|
|
12020
|
+
"onboardNewChannels",
|
|
11817
12021
|
"persona"
|
|
11818
12022
|
]);
|
|
11819
12023
|
MCP_SERVER_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
@@ -11992,17 +12196,17 @@ function serveStatic(req, res) {
|
|
|
11992
12196
|
headers["Cache-Control"] = filePath.startsWith(assetsPrefix) ? "public, max-age=31536000, immutable" : "no-cache";
|
|
11993
12197
|
res.writeHead(200, headers);
|
|
11994
12198
|
fs25.createReadStream(filePath).on("error", (err) => {
|
|
11995
|
-
|
|
12199
|
+
log44.warn("static read failed", { err: err.message });
|
|
11996
12200
|
res.end();
|
|
11997
12201
|
}).pipe(res);
|
|
11998
12202
|
}
|
|
11999
|
-
var
|
|
12203
|
+
var log44, MIME, SERVE_STATIC;
|
|
12000
12204
|
var init_static = __esm({
|
|
12001
12205
|
"packages/gateway/dist/static.js"() {
|
|
12002
12206
|
"use strict";
|
|
12003
12207
|
init_config();
|
|
12004
12208
|
init_logger();
|
|
12005
|
-
|
|
12209
|
+
log44 = logger.child("static");
|
|
12006
12210
|
MIME = {
|
|
12007
12211
|
".html": "text/html; charset=utf-8",
|
|
12008
12212
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -12078,7 +12282,7 @@ function foldProfilesIntoJobs() {
|
|
|
12078
12282
|
continue;
|
|
12079
12283
|
if (!profile) {
|
|
12080
12284
|
if (profileName !== DEFAULT_PROFILE_NAME) {
|
|
12081
|
-
|
|
12285
|
+
log45.warn("job references a profile that could not be resolved \u2014 leaving the job and its profile key alone", {
|
|
12082
12286
|
jobId: id,
|
|
12083
12287
|
profile: profileName
|
|
12084
12288
|
});
|
|
@@ -12104,7 +12308,7 @@ function foldProfilesIntoJobs() {
|
|
|
12104
12308
|
try {
|
|
12105
12309
|
writeJsonAtomicPreservingMode(jobPath2, merged);
|
|
12106
12310
|
} catch (err) {
|
|
12107
|
-
|
|
12311
|
+
log45.warn("failed to rewrite job while folding its profile \u2014 leaving it unchanged", {
|
|
12108
12312
|
jobId: id,
|
|
12109
12313
|
profile: profileName,
|
|
12110
12314
|
err: err instanceof Error ? err.message : String(err)
|
|
@@ -12113,7 +12317,7 @@ function foldProfilesIntoJobs() {
|
|
|
12113
12317
|
continue;
|
|
12114
12318
|
}
|
|
12115
12319
|
folded.push(id);
|
|
12116
|
-
|
|
12320
|
+
log45.info("folded profile into job", {
|
|
12117
12321
|
jobId: id,
|
|
12118
12322
|
profile: profileName,
|
|
12119
12323
|
inherited: inherited.length > 0 ? inherited.join(",") : "none"
|
|
@@ -12122,16 +12326,16 @@ function foldProfilesIntoJobs() {
|
|
|
12122
12326
|
const stillReferenced = collectReferencedProfiles();
|
|
12123
12327
|
for (const name of [...consumed].sort()) {
|
|
12124
12328
|
if (stillReferenced.has(name)) {
|
|
12125
|
-
|
|
12329
|
+
log45.info("keeping a folded profile file: another job still names it", { profile: name });
|
|
12126
12330
|
continue;
|
|
12127
12331
|
}
|
|
12128
12332
|
const profilePath = path26.join(profilesDir(), `${name}.json`);
|
|
12129
12333
|
try {
|
|
12130
12334
|
fs26.unlinkSync(profilePath);
|
|
12131
12335
|
deletedProfiles.push(name);
|
|
12132
|
-
|
|
12336
|
+
log45.info("deleted consumed profile file", { profile: name });
|
|
12133
12337
|
} catch (err) {
|
|
12134
|
-
|
|
12338
|
+
log45.warn("failed to delete a consumed profile file", {
|
|
12135
12339
|
profile: name,
|
|
12136
12340
|
err: err instanceof Error ? err.message : String(err)
|
|
12137
12341
|
});
|
|
@@ -12139,13 +12343,13 @@ function foldProfilesIntoJobs() {
|
|
|
12139
12343
|
}
|
|
12140
12344
|
const leftovers = listUnconsumedProfiles(consumed);
|
|
12141
12345
|
if (leftovers.length > 0) {
|
|
12142
|
-
|
|
12346
|
+
log45.info("profile files no job referenced were left in place for review", {
|
|
12143
12347
|
profiles: leftovers.join(","),
|
|
12144
12348
|
dir: profilesDir()
|
|
12145
12349
|
});
|
|
12146
12350
|
}
|
|
12147
12351
|
if (folded.length > 0 || deletedProfiles.length > 0 || unresolved.length > 0) {
|
|
12148
|
-
|
|
12352
|
+
log45.info("profile fold complete", {
|
|
12149
12353
|
folded: folded.length,
|
|
12150
12354
|
deletedProfiles: deletedProfiles.length,
|
|
12151
12355
|
unresolved: unresolved.length
|
|
@@ -12179,14 +12383,14 @@ function listUnconsumedProfiles(consumed) {
|
|
|
12179
12383
|
}
|
|
12180
12384
|
return files.filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5)).filter((name) => !consumed.has(name)).sort();
|
|
12181
12385
|
}
|
|
12182
|
-
var
|
|
12386
|
+
var log45, profilesDir, FOLDED_KEYS, PROFILE_NAME_RE, DEFAULT_PROFILE_NAME;
|
|
12183
12387
|
var init_profile_fold = __esm({
|
|
12184
12388
|
"packages/gateway/dist/cron/profile-fold.js"() {
|
|
12185
12389
|
"use strict";
|
|
12186
12390
|
init_paths();
|
|
12187
12391
|
init_logger();
|
|
12188
12392
|
init_job_store();
|
|
12189
|
-
|
|
12393
|
+
log45 = logger.child("profile-fold");
|
|
12190
12394
|
profilesDir = () => path26.join(teamworkHomeDir(), "profiles");
|
|
12191
12395
|
FOLDED_KEYS = ["model", "persona", "pluginDirs", "mcpServers", "spendCapUsd"];
|
|
12192
12396
|
PROFILE_NAME_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
@@ -12234,7 +12438,7 @@ function startGateway() {
|
|
|
12234
12438
|
res.end("TeamworkOS gateway is running. Start the Vite dev server for the dashboard.");
|
|
12235
12439
|
}
|
|
12236
12440
|
} catch (e) {
|
|
12237
|
-
|
|
12441
|
+
log46.error("Unhandled request error", e);
|
|
12238
12442
|
json(res, { error: "Internal server error" }, 500, req);
|
|
12239
12443
|
}
|
|
12240
12444
|
});
|
|
@@ -12245,22 +12449,22 @@ function startGateway() {
|
|
|
12245
12449
|
startRetentionSweeper();
|
|
12246
12450
|
void startSlackConnector();
|
|
12247
12451
|
function gracefulShutdown(signal) {
|
|
12248
|
-
|
|
12452
|
+
log46.info(`Received ${signal}, shutting down gracefully...`);
|
|
12249
12453
|
stopSweeper();
|
|
12250
12454
|
stopScheduler();
|
|
12251
12455
|
stopAuthProbe();
|
|
12252
12456
|
stopRetentionSweeper();
|
|
12253
|
-
void stopSlackConnector().catch((err) =>
|
|
12457
|
+
void stopSlackConnector().catch((err) => log46.error("slack connector shutdown failed", err));
|
|
12254
12458
|
try {
|
|
12255
12459
|
sessionManager.interruptAll();
|
|
12256
12460
|
} catch (err) {
|
|
12257
|
-
|
|
12461
|
+
log46.error("interruptAll during shutdown failed", err);
|
|
12258
12462
|
}
|
|
12259
12463
|
closeAllClients().catch(() => {
|
|
12260
12464
|
});
|
|
12261
12465
|
server.closeAllConnections?.();
|
|
12262
12466
|
server.close(() => {
|
|
12263
|
-
|
|
12467
|
+
log46.info("Server closed");
|
|
12264
12468
|
try {
|
|
12265
12469
|
closeDb();
|
|
12266
12470
|
} catch {
|
|
@@ -12268,7 +12472,7 @@ function startGateway() {
|
|
|
12268
12472
|
process.exit(0);
|
|
12269
12473
|
});
|
|
12270
12474
|
setTimeout(() => {
|
|
12271
|
-
|
|
12475
|
+
log46.error("Forced shutdown after timeout");
|
|
12272
12476
|
process.exit(1);
|
|
12273
12477
|
}, 1e4).unref();
|
|
12274
12478
|
}
|
|
@@ -12276,7 +12480,7 @@ function startGateway() {
|
|
|
12276
12480
|
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
|
|
12277
12481
|
const bind = resolveBindHost(settings.server.bindHost, currentAuthToken() !== null);
|
|
12278
12482
|
if (bind.refused) {
|
|
12279
|
-
|
|
12483
|
+
log46.error(`Refusing configured server.bindHost ${JSON.stringify(bind.refused.configured)}: ${bind.refused.reason}. Binding ${bind.host} instead.`);
|
|
12280
12484
|
}
|
|
12281
12485
|
server.listen(PORT, bind.host, () => {
|
|
12282
12486
|
const homeDisplay = teamworkHomeDir().replace(process.env.HOME ?? "", "~");
|
|
@@ -12295,10 +12499,10 @@ function startGateway() {
|
|
|
12295
12499
|
console.log("");
|
|
12296
12500
|
const rawAddr = server.address();
|
|
12297
12501
|
if (rawAddr === null || typeof rawAddr === "string") {
|
|
12298
|
-
|
|
12502
|
+
log46.warn(`unexpected server.address() shape; skipping boot signal: ${JSON.stringify(rawAddr)}`);
|
|
12299
12503
|
} else {
|
|
12300
12504
|
const addr = rawAddr;
|
|
12301
|
-
|
|
12505
|
+
log46.info("TeamworkOS ready", buildBootSignalFields({
|
|
12302
12506
|
teamworkHome: teamworkHomeDir(),
|
|
12303
12507
|
port: addr.port
|
|
12304
12508
|
}));
|
|
@@ -12306,7 +12510,7 @@ function startGateway() {
|
|
|
12306
12510
|
});
|
|
12307
12511
|
return server;
|
|
12308
12512
|
}
|
|
12309
|
-
var
|
|
12513
|
+
var log46;
|
|
12310
12514
|
var init_server = __esm({
|
|
12311
12515
|
"packages/gateway/dist/server.js"() {
|
|
12312
12516
|
"use strict";
|
|
@@ -12330,7 +12534,7 @@ var init_server = __esm({
|
|
|
12330
12534
|
init_paths();
|
|
12331
12535
|
init_helpers();
|
|
12332
12536
|
init_logger();
|
|
12333
|
-
|
|
12537
|
+
log46 = logger.child("gateway");
|
|
12334
12538
|
}
|
|
12335
12539
|
});
|
|
12336
12540
|
|