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.
@@ -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
- const names = v.filter((n) => n.trim().length > 0).map((n) => n.trim());
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 lookupGrant(ref) {
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
- log32.debug("primed session with slack history", { channel: opts.channel, lines: lines.length });
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 log32, NAME_TTL_MS, NameResolver;
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
- log32 = logger.child("slack:history");
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 log33, JOB_ROOT_CAPACITY, JOB_ROOT_METADATA_EVENT_TYPE, JobRootRegistry, jobRoots, ROOT_TEXT_MAX, ROOT_TEXT_MIN, JOB_ID_MAX, CHANNEL_ID_RE, TS_RE;
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
- log33 = logger.child("slack:job-roots");
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
- log33.info("registered slack job root", { jobId: root.jobId, channel: root.channel });
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
- log34.warn("could not read slack.persona; turn runs with no persona", {
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
- log34.warn("slack.persona is whitespace only; turn runs with no persona");
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
- log34.warn("slack.persona exceeds the maximum length; turn runs with no persona", {
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 log34;
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
- log34 = logger.child("slack-persona");
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
- log35.warn("upload ticket was missing a url or file id");
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
- log35.info("uploaded the full answer as a snippet", { channel: opts.channel, bytes: length });
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
- log35.info("file uploads unavailable; long answers will continue as thread replies", { code });
9781
+ log36.info("file uploads unavailable; long answers will continue as thread replies", { code });
9663
9782
  return { ok: false, reason: "unsupported" };
9664
9783
  }
9665
- log35.warn("snippet upload failed; falling back to chunked thread replies", { code });
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 log35, SNIPPET_CHUNK_THRESHOLD, SNIPPET_FILENAME, SNIPPET_TITLE, UNSUPPORTED_UPLOAD_CODES, filesUploadSupport;
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
- log35 = logger.child("slack:snippet");
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 log36, 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;
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
- log36 = logger.child("slack:card");
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
- log36.error("seal delivery hit the attempt cap with the answer undelivered", {
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
- log36.error("sealing the card failed at the transport layer; not retrying (double-post risk)", {
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
- log36.error("sealing the card failed with an ambiguous outcome; not retrying (double-post risk)", {
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
- log36.error("seal delivery step failed", { step: step.name, attempt: attempts, code: err.code });
10042
+ log37.error("seal delivery step failed", { step: step.name, attempt: attempts, code: err.code });
9924
10043
  }
9925
10044
  }
9926
- log36.error("the turn produced an answer that could not be delivered to Slack", { attempts, lastCode });
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
- log36.info("streaming API unavailable; using batched progress messages", { code });
10168
+ log37.info("streaming API unavailable; using batched progress messages", { code });
10050
10169
  } else {
10051
- log36.warn("chat.startStream failed; falling back to a batched card for this turn", { code });
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 log37, BACKOFF_BASE_MS, BACKOFF_MAX_MS, SocketModeClient;
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
- log37 = logger.child("slack:socket");
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
- log37.error("socket loop exited unexpectedly", { err: err instanceof Error ? err.message : String(err) });
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
- log37.warn("apps.connections.open failed; retrying", {
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
- log37.warn("socket closed; reconnecting", { attempt: this.attempt, waitMs: wait, reason: closeReason });
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
- log37.debug("unparseable socket frame dropped");
10405
+ log38.debug("unparseable socket frame dropped");
10287
10406
  return;
10288
10407
  }
10289
10408
  if (envelope.type === "hello") {
10290
10409
  this.attempt = 0;
10291
- log37.info("socket mode connected");
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
- log37.info("socket disconnect requested by slack", { reason: disconnectReason });
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
- log37.warn("ack send failed", { err: err instanceof Error ? err.message : String(err) });
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
- log37.error("envelope handler threw", { err: err instanceof Error ? err.message : String(err) });
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
- log37.warn("socket error", { err: err.message });
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
- log38.debug("thread status unavailable for this thread", { code });
10475
+ log39.debug("thread status unavailable for this thread", { code });
10357
10476
  stopped = true;
10358
10477
  clear();
10359
10478
  return;
10360
10479
  }
10361
- log38.debug("setStatus failed", { code });
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 log38, KEEPALIVE_MS, MAX_DURATION_MS, TERMINAL;
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
- log38 = logger.child("slack:status");
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
- log39.debug("link_shared carried no renderable gateway links", { channel: event.channel });
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
- log39.debug("unfurled gateway links", { channel: event.channel, links: Object.keys(unfurls).length });
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
- log39.debug("entity_details_requested for a url this gateway does not own");
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
- log39.debug("entity_details_requested for an object that no longer exists", { kind: target.kind });
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
- log39.debug("presented entity details", { kind: target.kind });
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
- log39.info("work-object previews unavailable; links will render as plain urls", { method, code });
10686
+ log40.info("work-object previews unavailable; links will render as plain urls", { method, code });
10568
10687
  return;
10569
10688
  }
10570
- log39.debug("work-object preview failed", { method, code });
10689
+ log40.debug("work-object preview failed", { method, code });
10571
10690
  }
10572
- var log39, UNSUPPORTED_UNFURL_CODES, unfurlSupport, liveStatusSource;
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
- log39 = logger.child("slack:work-objects");
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
- log40.info("slack connector disabled in settings");
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
- log40.warn("slack connector not started: SLACK_BOT_TOKEN and SLACK_APP_TOKEN must both be set");
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
- log40.error("slack connector failed to start", { err: err instanceof Error ? err.message : String(err) });
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 log40, MISSING_SESSION_RE, CONVERSATION_CAPACITY, SlackConnector, active;
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
- log40 = logger.child("slack");
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
- log40.info("slack auth ok", { botUserId: this.botUserId, team: auth.team, teamId: this.teamId });
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
- log40.debug("slack event received", {
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
- log40.info("duplicate slack event dropped", { dedupeKey: event.dedupeKey, retryAttempt: event.retryAttempt });
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
- log40.info("assistant thread event", { kind: event.kind, channel: event.channel });
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
- log40[level]("slack event refused by allowlist", {
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
- log40.debug("slack event filtered", { reason: drop, kind: event.kind, dedupeKey: event.dedupeKey });
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
- log40.debug("slack event filtered", {
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
- log40.error("slack dispatch failed", { err: err instanceof Error ? err.message : String(err) });
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
- log40.debug("link_shared ignored: channel is not one this gateway is configured for", { channel: event.channel });
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
- log40.debug("link preview failed", { err: err instanceof Error ? err.message : String(err) });
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
- log40.debug("entity_details_requested without a trigger id or url");
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
- log40.debug("entity_details_requested ignored: channel is not one this gateway is configured for", {
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
- log40.debug("entity details failed", { err: err instanceof Error ? err.message : String(err) });
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
- log40.info("slack turn dispatched", {
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.slack.historyLimit, jobId);
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
- log40.info("slack conversation session no longer exists; rebuilding from thread history", {
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.slack.historyLimit, jobId);
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
- log40.error("slack route failed", { channel: target.channel, thread: target.thread, err: detail });
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
- log40.warn("channels.mcp names servers absent from the claude config; skipping them", {
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
- log40.warn("channels.mcp grant resolved to no servers; turn runs with none", { channel });
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
- log40.warn("could not write the channel MCP config; turn runs with no servers", {
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
- log40.info("channel MCP grant resolved", { channel, servers: names.join(", ") });
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
- log40.warn('channels.mcp grants "gateway" alongside a claude.ai connector; the gateway MCP server is unavailable in deny mode', { channel });
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
- log40.warn("ambient MCP config unreadable; deny-mode turn runs with no servers rather than under-subtracting", {
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
- log40.warn("channels.mcp names local servers absent from the claude config; they resolve to nothing", {
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
- log40.warn("channels.mcp grants connectors missing from slack.knownConnectors; other deny-mode channels cannot subtract them", {
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
- log40.info("channel MCP grant resolved (deny mode)", {
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 and
11253
- * channel/thread history context on the first turn of a conversation only. */
11254
- async buildPrompt(event, target, isResume, historyLimit, jobId) {
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
- log40.info("reaction trigger dropped: its message could not be read", {
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
- log40.info("slack turn sealed", { sessionId, outcome: effective, cardTs, delivered: turn.card.delivered });
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
- log41.info("job root post delivered", { jobId: validated.jobId, channel: validated.channel });
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 log41;
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
- log41 = logger.child("routes:slack");
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
- if (value === "full") {
11584
- mcpMap[key] = "full";
11585
- continue;
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
- log42.info("settings updated");
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 log42, 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;
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
- log42 = logger.child("settings");
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
- log43.warn("static read failed", { err: err.message });
12199
+ log44.warn("static read failed", { err: err.message });
11996
12200
  res.end();
11997
12201
  }).pipe(res);
11998
12202
  }
11999
- var log43, MIME, SERVE_STATIC;
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
- log43 = logger.child("static");
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
- log44.warn("job references a profile that could not be resolved \u2014 leaving the job and its profile key alone", {
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
- log44.warn("failed to rewrite job while folding its profile \u2014 leaving it unchanged", {
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
- log44.info("folded profile into job", {
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
- log44.info("keeping a folded profile file: another job still names it", { profile: name });
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
- log44.info("deleted consumed profile file", { profile: name });
12336
+ log45.info("deleted consumed profile file", { profile: name });
12133
12337
  } catch (err) {
12134
- log44.warn("failed to delete a consumed profile file", {
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
- log44.info("profile files no job referenced were left in place for review", {
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
- log44.info("profile fold complete", {
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 log44, profilesDir, FOLDED_KEYS, PROFILE_NAME_RE, DEFAULT_PROFILE_NAME;
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
- log44 = logger.child("profile-fold");
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
- log45.error("Unhandled request error", e);
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
- log45.info(`Received ${signal}, shutting down gracefully...`);
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) => log45.error("slack connector shutdown failed", 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
- log45.error("interruptAll during shutdown failed", err);
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
- log45.info("Server closed");
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
- log45.error("Forced shutdown after timeout");
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
- log45.error(`Refusing configured server.bindHost ${JSON.stringify(bind.refused.configured)}: ${bind.refused.reason}. Binding ${bind.host} instead.`);
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
- log45.warn(`unexpected server.address() shape; skipping boot signal: ${JSON.stringify(rawAddr)}`);
12502
+ log46.warn(`unexpected server.address() shape; skipping boot signal: ${JSON.stringify(rawAddr)}`);
12299
12503
  } else {
12300
12504
  const addr = rawAddr;
12301
- log45.info("TeamworkOS ready", buildBootSignalFields({
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 log45;
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
- log45 = logger.child("gateway");
12537
+ log46 = logger.child("gateway");
12334
12538
  }
12335
12539
  });
12336
12540