spur-board 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { H as requiredSecretKeys, N as ERRORS, U as SLACK_BOT_SCOPES, V as configSchema, l as OPERATIONS, n as SYSTEMD_SERVICE_SETTINGS } from "./src-C-NNf__4.mjs";
2
- import { a as errorExitCode, i as CliError, n as requestOperation, o as renderCliError, r as requestTokenEnrollment, s as escapeTerminalControls } from "./client-Co0Pogjy.mjs";
3
- import { a as memoryVerdictCache, c as loadEnrollmentConfig, i as fileVerdictCache, l as resolveDaemonAddress, n as cliPackageVersion, o as configFile, s as loadClientConfig, t as checkCatalog } from "./config-DL_Tm7un.mjs";
1
+ import { H as requiredSecretKeys, N as ERRORS, U as SLACK_BOT_SCOPES, V as configSchema, l as OPERATIONS, n as SYSTEMD_SERVICE_SETTINGS } from "./src-DyDmiehC.mjs";
2
+ import { a as errorExitCode, i as CliError, n as requestOperation, o as renderCliError, r as requestTokenEnrollment, s as escapeTerminalControls } from "./client-C4Ee3Gfs.mjs";
3
+ import { a as memoryVerdictCache, c as loadEnrollmentConfig, i as fileVerdictCache, l as resolveDaemonAddress, n as cliPackageVersion, o as configFile, s as loadClientConfig, t as checkCatalog } from "./config-BmT8hPi8.mjs";
4
4
  import { createHash, randomBytes, randomUUID } from "node:crypto";
5
5
  import { chmodSync, closeSync, constants, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
6
6
  import { homedir, hostname, userInfo } from "node:os";
@@ -1,4 +1,4 @@
1
- import { F as errorEnvelopeSchema, I as successEnvelopeSchema, N as ERRORS, P as errorContextSchema, d as renderJsonError } from "./src-C-NNf__4.mjs";
1
+ import { F as errorEnvelopeSchema, I as successEnvelopeSchema, N as ERRORS, P as errorContextSchema, d as renderJsonError } from "./src-DyDmiehC.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { z } from "zod";
4
4
  import { request } from "node:http";
package/dist/client.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { n as requestOperation, r as requestTokenEnrollment, t as fetchCatalogVersion } from "./client-Co0Pogjy.mjs";
1
+ import { n as requestOperation, r as requestTokenEnrollment, t as fetchCatalogVersion } from "./client-C4Ee3Gfs.mjs";
2
2
  export { fetchCatalogVersion, requestOperation, requestTokenEnrollment };
@@ -1,5 +1,5 @@
1
- import { V as configSchema, l as OPERATIONS, s as buildCatalogSnapshot } from "./src-C-NNf__4.mjs";
2
- import { i as CliError, t as fetchCatalogVersion } from "./client-Co0Pogjy.mjs";
1
+ import { V as configSchema, l as OPERATIONS, s as buildCatalogSnapshot } from "./src-DyDmiehC.mjs";
2
+ import { i as CliError, t as fetchCatalogVersion } from "./client-C4Ee3Gfs.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
5
5
  import { homedir } from "node:os";
package/dist/daemon.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { A as parseActorId, B as RESERVED_PROJECT, C as prFactsSchema, D as actorIdSchema, E as ticketFactsSchema, H as requiredSecretKeys, I as successEnvelopeSchema, M as taskKeySchema, N as ERRORS, O as encodeActorId, S as SESSION_STATUSES, T as sessionFactsSchema, U as SLACK_BOT_SCOPES, V as configSchema, _ as messageRefSchema, a as entityRefSchema, b as eventPayloadSchemas, c as roleCanUseOperation, g as interactionRefSchema, h as inboundEventSchema, i as batchSchema, j as parseRef$1, k as errorCodeOf, l as OPERATIONS, m as conversationRefSchema, o as sourceCursorSchema, p as SPACE_IDS, r as AdapterError, s as buildCatalogSnapshot, t as BOARD_TICK_MS, u as renderHttpProblem, v as userIdSchema, w as prRefSchema, x as SESSION_HEALTH_STATES, y as EVENT_KINDS, z as DEFAULT_SLACK_COMMAND } from "./src-C-NNf__4.mjs";
1
+ import { A as parseActorId, B as RESERVED_PROJECT, C as prFactsSchema, D as actorIdSchema, E as ticketFactsSchema, H as requiredSecretKeys, I as successEnvelopeSchema, M as taskKeySchema, N as ERRORS, O as encodeActorId, S as SESSION_STATUSES, T as sessionFactsSchema, U as SLACK_BOT_SCOPES, V as configSchema, _ as messageRefSchema, a as entityRefSchema, b as eventPayloadSchemas, c as roleCanUseOperation, g as interactionRefSchema, h as inboundEventSchema, i as batchSchema, j as parseRef$1, k as errorCodeOf, l as OPERATIONS, m as conversationRefSchema, o as sourceCursorSchema, p as SPACE_IDS, r as AdapterError, s as buildCatalogSnapshot, t as BOARD_TICK_MS, u as renderHttpProblem, v as userIdSchema, w as prRefSchema, x as SESSION_HEALTH_STATES, y as EVENT_KINDS, z as DEFAULT_SLACK_COMMAND } from "./src-DyDmiehC.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import { closeSync, existsSync, fstatSync, ftruncateSync, mkdirSync, openSync, readFileSync, readdirSync, writeSync } from "node:fs";
@@ -9828,6 +9828,53 @@ function commentOf(body) {
9828
9828
  default: return null;
9829
9829
  }
9830
9830
  }
9831
+ /** The kinds whose rows wait in one of `spaces`. */
9832
+ function kindsIn(spaces) {
9833
+ return OUTBOX_KINDS.filter((kind) => {
9834
+ const space = KIND_SPACE[kind];
9835
+ return space !== null && spaces.includes(space);
9836
+ });
9837
+ }
9838
+ /**
9839
+ * Raises the gateway's one transport item while it has health rows and clears it once none are
9840
+ * left. Call it in the transaction that changed the rows, only when they changed.
9841
+ */
9842
+ async function syncTransportItem(trx, gateway, at) {
9843
+ const itemKey = `transport:${gateway}`;
9844
+ const rows = await trx.selectFrom("transport_health").select(["reason", "since"]).where("gateway", "=", gateway).orderBy("since").orderBy("space").execute();
9845
+ const first = rows[0];
9846
+ if (first !== void 0) {
9847
+ const reason = rows.find((r) => r.reason !== "down")?.reason ?? "connection lost";
9848
+ const text = `${gateway} delivery down since ${first.since} (${reason})`;
9849
+ const item = await raiseItem(trx, {
9850
+ itemKey,
9851
+ kind: "transport_health",
9852
+ text,
9853
+ url: "",
9854
+ taskId: null,
9855
+ blockId: null,
9856
+ priority: 1,
9857
+ suppressFeed: true,
9858
+ at
9859
+ });
9860
+ if (item.text !== text) await trx.updateTable("operator_item").set({ text }).where("id", "=", item.id).execute();
9861
+ return;
9862
+ }
9863
+ const active = await trx.selectFrom("operator_item").select("id").where("item_key", "=", itemKey).where("cleared_at", "is", null).executeTakeFirst();
9864
+ if (active !== void 0) await clearItem(trx, active.id, at);
9865
+ }
9866
+ /**
9867
+ * Makes the next tick probe every space of `gateway` that has a health row: the probe clock is
9868
+ * set one interval back and waiting rows there become due at `at` (ISO-8601 UTC). Returns the
9869
+ * number of spaces armed; 0 when no outage is recorded.
9870
+ */
9871
+ async function retryTransport(trx, gateway, at) {
9872
+ const rows = await trx.updateTable("transport_health").set({ probe_at: (/* @__PURE__ */ new Date(Date.parse(at) - HEALTH_PROBE_MS)).toISOString() }).where("gateway", "=", gateway).returning("space").execute();
9873
+ const kinds = kindsIn(rows.map((row) => row.space));
9874
+ if (kinds.length === 0) return rows.length;
9875
+ await trx.updateTable("outbox").set({ next_at: at }).where("state", "=", "pending").where("kind", "in", kinds).where("next_at", ">", at).execute();
9876
+ return rows.length;
9877
+ }
9831
9878
  async function setComment(trx, body, patch) {
9832
9879
  const commentId = commentOf(body);
9833
9880
  if (commentId === null) return;
@@ -9912,6 +9959,8 @@ function createSender(options) {
9912
9959
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
9913
9960
  const spaceIntervalMs = options.spaceIntervalMs ?? SPACE_INTERVAL_MS;
9914
9961
  let downSince;
9962
+ /** Set once this outage's down rows were written; reset on `up`. */
9963
+ let downRecorded = false;
9915
9964
  /** The last tick started; a new tick runs after it, so two ticks never interleave. */
9916
9965
  let ticking = Promise.resolve();
9917
9966
  /** Sets the comments of `rows` that carry one to `superseded`. */
@@ -9947,14 +9996,14 @@ function createSender(options) {
9947
9996
  * there, so a row that retried before the outage gets a full window once the space recovers.
9948
9997
  */
9949
9998
  async function suspendWindows(trx, spaces) {
9950
- const kinds = OUTBOX_KINDS.filter((kind) => {
9951
- const space = KIND_SPACE[kind];
9952
- return space !== null && spaces.includes(space);
9953
- });
9999
+ const kinds = kindsIn(spaces);
9954
10000
  if (kinds.length === 0) return;
9955
10001
  await trx.updateTable("outbox").set({ first_attempt_at: null }).where("state", "in", ["pending", "in_flight"]).where("kind", "in", kinds).execute();
9956
10002
  }
9957
- /** tx 1: cancels stale rows, raises down-health and claims at most one due row per lane. */
10003
+ /**
10004
+ * tx 1: cancels stale rows, raises down-health and claims at most one due row per lane.
10005
+ * `ranThreshold` says the outage's down rows were written; the caller records that after commit.
10006
+ */
9958
10007
  async function claim(trx, at) {
9959
10008
  const nowIso = new Date(at).toISOString();
9960
10009
  await reconcileDoorbells(trx, new Date(at), options.config);
@@ -9978,16 +10027,19 @@ function createSender(options) {
9978
10027
  }
9979
10028
  await kill(trx, stale, "superseded");
9980
10029
  await cascade(trx);
9981
- if (gateway !== void 0 && downSince !== void 0 && at - downSince > DOWN_THRESHOLD_MS) {
10030
+ let ranThreshold = false;
10031
+ if (gateway !== void 0 && !downRecorded && downSince !== void 0 && at - downSince > DOWN_THRESHOLD_MS) {
9982
10032
  const since = new Date(downSince).toISOString();
9983
- await trx.insertInto("transport_health").values(SPACE_IDS.map((space) => ({
10033
+ const inserted = await trx.insertInto("transport_health").values(SPACE_IDS.map((space) => ({
9984
10034
  gateway: gatewayKind,
9985
10035
  space,
9986
10036
  reason: "down",
9987
10037
  since,
9988
10038
  probe_at: since
9989
- }))).onConflict((oc) => oc.columns(["gateway", "space"]).doNothing()).execute();
10039
+ }))).onConflict((oc) => oc.columns(["gateway", "space"]).doNothing()).returning("space").execute();
9990
10040
  await suspendWindows(trx, SPACE_IDS);
10041
+ if (inserted.length > 0) await syncTransportItem(trx, gatewayKind, new Date(at));
10042
+ ranThreshold = true;
9991
10043
  }
9992
10044
  const live = await trx.selectFrom("outbox").leftJoin("outbox as parent", "parent.id", "outbox.parent_outbox_id").selectAll("outbox").select(["parent.state as parentState", "parent.external_ref as parentRef"]).where("outbox.state", "=", "pending").where("outbox.next_at", "<=", nowIso).execute();
9993
10045
  const sentAt = new Map((gateway === void 0 ? [] : await trx.selectFrom("space_slot").select(["space", "sent_at"]).where("gateway", "=", gatewayKind).execute()).map((slot) => [slot.space, Date.parse(slot.sent_at)]));
@@ -10039,7 +10091,10 @@ function createSender(options) {
10039
10091
  attempts: eb("attempts", "+", 1),
10040
10092
  attempt_started: 0
10041
10093
  })).where("id", "=", c.id).execute();
10042
- return claims.sort((a, b) => Number(a.space === null) - Number(b.space === null));
10094
+ return {
10095
+ claims: claims.sort((a, b) => Number(a.space === null) - Number(b.space === null)),
10096
+ ranThreshold
10097
+ };
10043
10098
  }
10044
10099
  /**
10045
10100
  * Stamps the start of a send, just before its IO: the row's retry window if none is open and,
@@ -10227,7 +10282,9 @@ function createSender(options) {
10227
10282
  conversation_ref: conversation,
10228
10283
  message_ref: outcome.message
10229
10284
  });
10230
- if (space !== null) await trx.deleteFrom("transport_health").where("gateway", "=", gatewayKind).where("space", "=", space).execute();
10285
+ if (space !== null) {
10286
+ if ((await trx.deleteFrom("transport_health").where("gateway", "=", gatewayKind).where("space", "=", space).returning("space").execute()).length > 0) await syncTransportItem(trx, gatewayKind, new Date(at));
10287
+ }
10231
10288
  return;
10232
10289
  }
10233
10290
  case "retry": {
@@ -10252,13 +10309,13 @@ function createSender(options) {
10252
10309
  }
10253
10310
  case "failed":
10254
10311
  if (outcome.scope === "message" || space === null) return postFailed({ last_error: outcome.reason });
10255
- await trx.insertInto("transport_health").values({
10312
+ if ((await trx.insertInto("transport_health").values({
10256
10313
  gateway: gatewayKind,
10257
10314
  space,
10258
10315
  reason: outcome.reason,
10259
10316
  since: nowIso,
10260
10317
  probe_at: new Date(startedAt).toISOString()
10261
- }).onConflict((oc) => oc.columns(["gateway", "space"]).doNothing()).execute();
10318
+ }).onConflict((oc) => oc.columns(["gateway", "space"]).doUpdateSet({ reason: outcome.reason }).where("transport_health.reason", "=", "down")).returning("space").execute()).length > 0) await syncTransportItem(trx, gatewayKind, new Date(at));
10262
10319
  await suspendWindows(trx, [space]);
10263
10320
  await row.set({
10264
10321
  state: "pending",
@@ -10272,7 +10329,8 @@ function createSender(options) {
10272
10329
  async function runTick(signal) {
10273
10330
  if (signal?.aborted) return;
10274
10331
  const claimedAt = now().getTime();
10275
- const claims = await scanTransaction(store, (trx) => claim(trx, claimedAt));
10332
+ const { claims, ranThreshold } = await scanTransaction(store, (trx) => claim(trx, claimedAt));
10333
+ if (ranThreshold) downRecorded = true;
10276
10334
  for (const [i, c] of claims.entries()) {
10277
10335
  if (signal?.aborted) {
10278
10336
  const ids = claims.slice(i).map((c) => c.id);
@@ -10325,8 +10383,20 @@ function createSender(options) {
10325
10383
  return run;
10326
10384
  },
10327
10385
  recordConnection(state, at) {
10328
- if (state === "up") downSince = void 0;
10329
- else downSince ??= at.getTime();
10386
+ const run = ticking.then(async () => {
10387
+ if (state === "down") {
10388
+ downSince ??= at.getTime();
10389
+ return;
10390
+ }
10391
+ downSince = void 0;
10392
+ downRecorded = false;
10393
+ if (gateway === void 0) return;
10394
+ await scanTransaction(store, async (trx) => {
10395
+ if ((await trx.deleteFrom("transport_health").where("gateway", "=", gatewayKind).where("reason", "=", "down").returning("space").execute()).length > 0) await syncTransportItem(trx, gatewayKind, at);
10396
+ });
10397
+ });
10398
+ ticking = run.catch(() => void 0);
10399
+ return run;
10330
10400
  }
10331
10401
  };
10332
10402
  }
@@ -10352,7 +10422,10 @@ function view(item, task) {
10352
10422
  function notApplicable(hint) {
10353
10423
  return new CommandError("retry_not_applicable", hint);
10354
10424
  }
10355
- /** Item actions: retry runs the action behind the item's kind; close dismisses it. Both clear it. */
10425
+ /**
10426
+ * Item actions: retry runs the action behind the item's kind; close dismisses it. Both clear it,
10427
+ * except a retry on a transport item, which the sender clears when the transport is back.
10428
+ */
10356
10429
  function createItemMutations(deps) {
10357
10430
  async function retryAction(trx, item, at) {
10358
10431
  const closeHint = `close it with \`board item close I${String(item.id)}\`.`;
@@ -10382,6 +10455,10 @@ function createItemMutations(deps) {
10382
10455
  const { item, task } = await loadItem(trx, input.item);
10383
10456
  if (task?.membership === "archived") throw new CommandError("task_archived", "Add the task again before retrying its item.");
10384
10457
  if (item.cleared_at !== null) throw notApplicable("The item is already cleared.");
10458
+ if (item.kind === "transport_health") {
10459
+ if (await retryTransport(trx, item.item_key.slice(10), ctx.at) === 0) throw notApplicable("No transport outage is recorded; the item clears on its own.");
10460
+ return view(item, task);
10461
+ }
10385
10462
  await retryAction(trx, item, ctx.at);
10386
10463
  await clearItem(trx, item.id, new Date(ctx.at), ctx.actor);
10387
10464
  return view(item, task);
@@ -10617,7 +10694,7 @@ function interval(config, source) {
10617
10694
  }
10618
10695
  /** Reads the board's latest facts, items and source availability. */
10619
10696
  async function getStatus(deps, projectId, visibleProjects, includeBoardItems = visibleProjects === void 0) {
10620
- const { store, config, clock, completeSince } = deps;
10697
+ const { store, config, clock, completeSince, gateways = [] } = deps;
10621
10698
  const projects = projectId === void 0 ? config.projects.filter((project) => visibleProjects === void 0 || visibleProjects.has(project.id)) : config.projects.filter((project) => project.id === projectId && (visibleProjects === void 0 || visibleProjects.has(project.id)));
10622
10699
  if (projects.length === 0) throw new CommandError("not_found", "Check the project ID.");
10623
10700
  const projectIds = new Set(projects.map((project) => project.id));
@@ -10690,6 +10767,23 @@ async function getStatus(deps, projectId, visibleProjects, includeBoardItems = v
10690
10767
  });
10691
10768
  const host = await store.db.selectFrom("meta").select("value").where("key", "=", "host_load_episode").executeTakeFirst();
10692
10769
  const spur = sourceById.get("spur");
10770
+ const healthRows = await store.db.selectFrom("transport_health").select([
10771
+ "gateway",
10772
+ "space",
10773
+ "reason",
10774
+ "since"
10775
+ ]).orderBy("since").orderBy("space").execute();
10776
+ const transport = [.../* @__PURE__ */ new Set([...gateways, ...healthRows.map((row) => row.gateway)])].sort().map((gateway) => {
10777
+ const down = healthRows.filter((row) => row.gateway === gateway);
10778
+ const first = down[0];
10779
+ return {
10780
+ gateway,
10781
+ state: first === void 0 ? "up" : "down",
10782
+ since: first?.since ?? null,
10783
+ reason: first?.reason ?? null,
10784
+ spaces: down.map((row) => row.space).sort()
10785
+ };
10786
+ });
10693
10787
  return {
10694
10788
  tasks,
10695
10789
  items,
@@ -10698,7 +10792,8 @@ async function getStatus(deps, projectId, visibleProjects, includeBoardItems = v
10698
10792
  sources,
10699
10793
  warmingUp: sources.some((source) => source.warmingUp),
10700
10794
  spurUnavailable: spur?.blind_reason === null || spur === void 0 ? null : `Spur unavailable: ${spur.blind_message ?? spur.blind_reason}`,
10701
- hostLoad: host?.value !== void 0 && host.value !== "null" ? "Host under load" : null
10795
+ hostLoad: host?.value !== void 0 && host.value !== "null" ? "Host under load" : null,
10796
+ transport
10702
10797
  };
10703
10798
  }
10704
10799
  //#endregion
@@ -10892,7 +10987,7 @@ function createTaskQueries(config) {
10892
10987
  const catalog = await buildCatalogSnapshot(OPERATIONS);
10893
10988
  /** Concrete handlers shared by the daemon's HTTP and inbound transports. */
10894
10989
  function createOperationRegistry(deps) {
10895
- const { config, clock, completeSince } = deps;
10990
+ const { config, clock, completeSince, gateways } = deps;
10896
10991
  const items = createItemMutations(deps);
10897
10992
  const membership = createMembershipMutations(config);
10898
10993
  const links = createLinkMutations(config);
@@ -10940,7 +11035,8 @@ function createOperationRegistry(deps) {
10940
11035
  store: { db: trx },
10941
11036
  config,
10942
11037
  clock,
10943
- completeSince
11038
+ completeSince,
11039
+ gateways
10944
11040
  }, input.project, visibleProjects(config, actor), authorize([{ grant: "operator" }], actor, {
10945
11041
  config,
10946
11042
  target: { kind: "missing" }
@@ -16242,7 +16338,10 @@ function slackSettings(config, secrets) {
16242
16338
  requestTimeoutMs: DELIVERY_REQUEST_TIMEOUT_MS
16243
16339
  };
16244
16340
  }
16245
- /** Own the timer and wait for the last queued sender tick before the store closes. */
16341
+ /**
16342
+ * Own the timer and wait for the last queued sender tick and connection update before the store
16343
+ * closes.
16344
+ */
16246
16345
  function createOutboxModule(ctx, gateway, options = {}) {
16247
16346
  const spur = options.spur ?? createSpurPort();
16248
16347
  const sender = createSender({
@@ -16254,6 +16353,7 @@ function createOutboxModule(ctx, gateway, options = {}) {
16254
16353
  });
16255
16354
  let timer;
16256
16355
  let active = Promise.resolve();
16356
+ let updating = Promise.resolve();
16257
16357
  let stopped = false;
16258
16358
  let signal;
16259
16359
  const schedule = () => {
@@ -16282,8 +16382,17 @@ function createOutboxModule(ctx, gateway, options = {}) {
16282
16382
  stopped = true;
16283
16383
  if (timer !== void 0) clearTimeout(timer);
16284
16384
  await active;
16385
+ await updating;
16285
16386
  },
16286
- pulse: tick
16387
+ pulse: tick,
16388
+ connection(state) {
16389
+ if (stopped) return Promise.resolve();
16390
+ const update = sender.recordConnection(state, new Date(ctx.clock.now())).catch((err) => {
16391
+ ctx.log.error({ err }, "transport state update failed");
16392
+ });
16393
+ updating = Promise.all([updating, update]).then(() => void 0);
16394
+ return update;
16395
+ }
16287
16396
  };
16288
16397
  }
16289
16398
  //#endregion
@@ -16401,10 +16510,14 @@ function createDaemonModules(ctxFor, ports = {}) {
16401
16510
  clock: board.clock,
16402
16511
  ...ports.productVersion === void 0 ? {} : { productVersion: ports.productVersion }
16403
16512
  });
16513
+ const ctx = ctxFor("gateway-slack");
16514
+ const settings = slackSettings(ctx.config, ctx.secrets);
16515
+ const gateway = ports.gateway ?? (settings === null ? void 0 : createSlackGateway(settings));
16404
16516
  const registry = createOperationRegistry({
16405
16517
  config: board.config,
16406
16518
  clock: board.clock,
16407
16519
  completeSince: core.completeSince,
16520
+ gateways: gateway === void 0 ? [] : [gateway.kind],
16408
16521
  reapplyInbound: () => inbound?.reapply
16409
16522
  });
16410
16523
  const notifier = createSystemdNotifier({
@@ -16435,16 +16548,16 @@ function createDaemonModules(ctxFor, ports = {}) {
16435
16548
  notifier.watchdog();
16436
16549
  }
16437
16550
  }));
16438
- const ctx = ctxFor("gateway-slack");
16439
- const settings = slackSettings(ctx.config, ctx.secrets);
16440
- const gateway = ports.gateway ?? (settings === null ? void 0 : createSlackGateway(settings));
16441
16551
  const inbound = gateway === void 0 || ports.gateway === void 0 && settings?.appToken === void 0 ? void 0 : createInboundModule(ctxFor("inbound"), gateway, registry);
16442
16552
  if (settings !== null && inbound === void 0) ctx.log.info("Slack inbound disabled: SLACK_APP_TOKEN is absent");
16553
+ const outbox = createOutboxModule(ctxFor("outbox"), gateway, ports.spur === void 0 ? {} : { spur: ports.spur });
16443
16554
  if (gateway !== void 0) entries.push({
16444
16555
  name: "gateway-slack",
16445
16556
  stopStage: "final",
16446
16557
  module: {
16447
- start: () => gateway.start(inbound === void 0 ? () => Promise.resolve() : (event) => inbound.sink(event)),
16558
+ start: () => gateway.start(async (event) => {
16559
+ await Promise.all([inbound?.sink(event), event.kind === "connection" ? outbox.connection(event.state) : void 0]);
16560
+ }),
16448
16561
  async stop() {
16449
16562
  try {
16450
16563
  await gateway.stop();
@@ -16459,7 +16572,6 @@ function createDaemonModules(ctxFor, ports = {}) {
16459
16572
  stopStage: "pollers",
16460
16573
  module: inbound
16461
16574
  });
16462
- const outbox = createOutboxModule(ctxFor("outbox"), gateway, ports.spur === void 0 ? {} : { spur: ports.spur });
16463
16575
  entries.push({
16464
16576
  name: "outbox",
16465
16577
  stopStage: "outbox",
package/dist/mcp.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { N as ERRORS, f as renderMcpError, l as OPERATIONS } from "./src-C-NNf__4.mjs";
2
- import { i as CliError, n as requestOperation } from "./client-Co0Pogjy.mjs";
3
- import { a as memoryVerdictCache, r as compareCatalogs, s as loadClientConfig, t as checkCatalog } from "./config-DL_Tm7un.mjs";
1
+ import { N as ERRORS, f as renderMcpError, l as OPERATIONS } from "./src-DyDmiehC.mjs";
2
+ import { i as CliError, n as requestOperation } from "./client-C4Ee3Gfs.mjs";
3
+ import { a as memoryVerdictCache, r as compareCatalogs, s as loadClientConfig, t as checkCatalog } from "./config-BmT8hPi8.mjs";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { readFileSync } from "node:fs";
6
6
  import { z } from "zod";
@@ -1638,7 +1638,14 @@ const OPERATIONS = [
1638
1638
  sources: z.array(sourceViewSchema).describe("Freshness and availability of each source."),
1639
1639
  warmingUp: z.boolean().describe("Whether required sources still need a complete poll."),
1640
1640
  spurUnavailable: z.string().nullable().describe("One combined Spur availability line, or null when available."),
1641
- hostLoad: z.string().nullable().describe("Host-load episode line, or null when absent.")
1641
+ hostLoad: z.string().nullable().describe("Host-load episode line, or null when absent."),
1642
+ transport: z.array(z.strictObject({
1643
+ gateway: z.string().min(1).describe("Chat gateway kind."),
1644
+ state: z.enum(["up", "down"]).describe("down while delivery to any space of the gateway is suspended."),
1645
+ since: z.iso.datetime().nullable().describe("When the earliest outage started, ISO 8601 UTC, or null when up."),
1646
+ reason: z.string().nullable().describe("Reason of the earliest outage, or null when up."),
1647
+ spaces: z.array(z.enum(SPACE_IDS)).describe("Spaces whose delivery is suspended.")
1648
+ }).describe("Transport health of one gateway.")).describe("Transport health of each chat gateway.")
1642
1649
  }),
1643
1650
  grants: operatorOrProjectAgent,
1644
1651
  errors: ["not_found"],
@@ -1667,7 +1674,8 @@ const OPERATIONS = [
1667
1674
  sources: [],
1668
1675
  warmingUp: true,
1669
1676
  spurUnavailable: null,
1670
- hostLoad: null
1677
+ hostLoad: null,
1678
+ transport: []
1671
1679
  }
1672
1680
  }]
1673
1681
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spur-board",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "license": "MIT",
5
5
  "engines": {
6
6
  "node": "^24.12.0 || ^26.0.0"