hotsheet 0.17.0-beta.21 → 0.17.0-beta.23

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/channel.js CHANGED
@@ -34708,6 +34708,40 @@ function slugifyDataDir(dataDir2) {
34708
34708
  return slug !== "" ? slug : "project";
34709
34709
  }
34710
34710
 
34711
+ // src/channelLog.ts
34712
+ import { appendFileSync, renameSync, statSync } from "fs";
34713
+ var CHANNEL_LOG_MAX_BYTES = 1048576;
34714
+ function createChannelLogger(logPath) {
34715
+ let injectedBlankLine = false;
34716
+ return {
34717
+ log(event, details) {
34718
+ try {
34719
+ const size = safeStatSize(logPath);
34720
+ if (size >= CHANNEL_LOG_MAX_BYTES) {
34721
+ try {
34722
+ renameSync(logPath, `${logPath}.old`);
34723
+ } catch {
34724
+ }
34725
+ }
34726
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
34727
+ const detailsText = details === void 0 || details === "" ? "" : ` ${details}`;
34728
+ const prefix = !injectedBlankLine && size > 0 ? "\n" : "";
34729
+ injectedBlankLine = true;
34730
+ appendFileSync(logPath, `${prefix}[${ts}] [pid ${process.pid}] ${event}:${detailsText}
34731
+ `, "utf-8");
34732
+ } catch {
34733
+ }
34734
+ }
34735
+ };
34736
+ }
34737
+ function safeStatSize(path) {
34738
+ try {
34739
+ return statSync(path).size;
34740
+ } catch {
34741
+ return 0;
34742
+ }
34743
+ }
34744
+
34711
34745
  // src/channelPermissions.ts
34712
34746
  var PERMISSION_TTL_MS = 12e4;
34713
34747
  var queue = [];
@@ -34731,6 +34765,48 @@ function clearAllPermissions() {
34731
34765
  queue.length = 0;
34732
34766
  }
34733
34767
 
34768
+ // src/channelStdioWatcher.ts
34769
+ function installStdioDisconnectHandler(opts) {
34770
+ const { stdin, stdout, onDisconnect, log } = opts;
34771
+ let fired = false;
34772
+ const fire = (reason) => {
34773
+ if (fired) return;
34774
+ fired = true;
34775
+ if (log !== void 0) {
34776
+ log(`stdio disconnected (${reason}) \u2014 triggering channel-server cleanup`);
34777
+ }
34778
+ try {
34779
+ onDisconnect(reason);
34780
+ } catch (err) {
34781
+ if (log !== void 0) {
34782
+ log(`onDisconnect threw: ${err instanceof Error ? err.message : String(err)}`);
34783
+ }
34784
+ }
34785
+ };
34786
+ const onStdinEnd = () => {
34787
+ fire("stdin-end");
34788
+ };
34789
+ const onStdinClose = () => {
34790
+ fire("stdin-close");
34791
+ };
34792
+ const onStdoutError = (err) => {
34793
+ if (err.code === "EPIPE" || err.code === "ECONNRESET" || err.code === "ECANCELED") {
34794
+ fire("stdout-error");
34795
+ }
34796
+ };
34797
+ stdin.on("end", onStdinEnd);
34798
+ stdin.on("close", onStdinClose);
34799
+ stdout.on("error", onStdoutError);
34800
+ let disposed = false;
34801
+ return () => {
34802
+ if (disposed) return;
34803
+ disposed = true;
34804
+ stdin.off("end", onStdinEnd);
34805
+ stdin.off("close", onStdinClose);
34806
+ stdout.off("error", onStdoutError);
34807
+ };
34808
+ }
34809
+
34734
34810
  // src/channel.ts
34735
34811
  var CHANNEL_VERSION = 7;
34736
34812
  var dataDir = ".hotsheet";
@@ -34744,6 +34820,8 @@ for (let i = 0; i < args.length; i++) {
34744
34820
  var portFile = join3(dataDir, "channel-port");
34745
34821
  var serverSlug = slugifyDataDir(dataDir);
34746
34822
  var serverName = `hotsheet-channel-${serverSlug}`;
34823
+ var channelLog = createChannelLogger(join3(dataDir, "mcp.log"));
34824
+ channelLog.log("process-start", `argv=${process.argv.slice(2).join(" ")} dataDir=${dataDir} serverName=${serverName}`);
34747
34825
  var mcp = new Server(
34748
34826
  { name: serverName, version: "0.1.0" },
34749
34827
  {
@@ -34774,10 +34852,25 @@ mcp.setRequestHandler(ListToolsRequestSchema, () => {
34774
34852
  });
34775
34853
  mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
34776
34854
  const { name, arguments: args2 } = request.params;
34855
+ channelLog.log("tools/call", `name=${name}`);
34777
34856
  const result = await callTool(name, args2 ?? {}, dataDir);
34778
34857
  return result;
34779
34858
  });
34780
34859
  await mcp.connect(new StdioServerTransport());
34860
+ channelLog.log("mcp-connect", "StdioServerTransport ready");
34861
+ installStdioDisconnectHandler({
34862
+ stdin: process.stdin,
34863
+ stdout: process.stdout,
34864
+ log: (msg) => {
34865
+ process.stderr.write(`${serverName}: ${msg}
34866
+ `);
34867
+ channelLog.log("stdio-watcher", msg);
34868
+ },
34869
+ onDisconnect: (reason) => {
34870
+ channelLog.log("disconnect", `reason=${reason} \u2014 invoking cleanup`);
34871
+ void cleanup(`stdio-${reason}`);
34872
+ }
34873
+ });
34781
34874
  var PermissionRequestSchema = external_exports3.object({
34782
34875
  method: external_exports3.literal("notifications/claude/channel/permission_request"),
34783
34876
  params: external_exports3.object({
@@ -34921,6 +35014,7 @@ var httpServer = createServer(async (req, res) => {
34921
35014
  }
34922
35015
  body += String(chunk);
34923
35016
  }
35017
+ channelLog.log("trigger", `bodyBytes=${body.length}`);
34924
35018
  try {
34925
35019
  await mcp.notification({
34926
35020
  method: "notifications/claude/channel",
@@ -34932,6 +35026,7 @@ var httpServer = createServer(async (req, res) => {
34932
35026
  res.writeHead(200, { "Content-Type": "application/json" });
34933
35027
  res.end(JSON.stringify({ ok: true }));
34934
35028
  } catch (err) {
35029
+ channelLog.log("trigger-error", String(err));
34935
35030
  res.writeHead(500, { "Content-Type": "application/json" });
34936
35031
  res.end(JSON.stringify({ error: String(err) }));
34937
35032
  }
@@ -34983,10 +35078,18 @@ httpServer.listen(0, "127.0.0.1", () => {
34983
35078
  }
34984
35079
  process.stderr.write(`${serverName} listening on port ${port}
34985
35080
  `);
35081
+ channelLog.log("http-listen", `port=${port}`);
34986
35082
  void notifyMainServer();
34987
35083
  }
34988
35084
  });
34989
- async function cleanup() {
35085
+ var heartbeatInterval = setInterval(() => {
35086
+ const mem = process.memoryUsage();
35087
+ channelLog.log("heartbeat", `uptime=${process.uptime().toFixed(1)}s rss=${(mem.rss / 1024 / 1024).toFixed(1)}MiB`);
35088
+ }, 6e4);
35089
+ heartbeatInterval.unref();
35090
+ async function cleanup(reason = "unspecified") {
35091
+ channelLog.log("cleanup-start", `reason=${reason}`);
35092
+ clearInterval(heartbeatInterval);
34990
35093
  try {
34991
35094
  unlinkSync(portFile);
34992
35095
  } catch {
@@ -34997,11 +35100,23 @@ async function cleanup() {
34997
35100
  await notifyMainServer(controller.signal);
34998
35101
  } catch {
34999
35102
  }
35103
+ channelLog.log("cleanup-end", `reason=${reason} \u2014 exiting`);
35000
35104
  process.exit(0);
35001
35105
  }
35002
- process.on("SIGTERM", () => void cleanup());
35003
- process.on("SIGINT", () => void cleanup());
35004
- process.on("exit", () => {
35106
+ process.on("SIGTERM", () => {
35107
+ channelLog.log("signal", "SIGTERM");
35108
+ void cleanup("SIGTERM");
35109
+ });
35110
+ process.on("SIGINT", () => {
35111
+ channelLog.log("signal", "SIGINT");
35112
+ void cleanup("SIGINT");
35113
+ });
35114
+ process.on("SIGHUP", () => {
35115
+ channelLog.log("signal", "SIGHUP");
35116
+ void cleanup("SIGHUP");
35117
+ });
35118
+ process.on("exit", (code) => {
35119
+ channelLog.log("exit", `code=${code}`);
35005
35120
  try {
35006
35121
  unlinkSync(portFile);
35007
35122
  } catch {
package/dist/cli.js CHANGED
@@ -21039,7 +21039,15 @@ var init_global_config = __esm({
21039
21039
  // HS-8290 — terminal-dashboard settings (formerly stored per-project but
21040
21040
  // are inherently cross-project since the dashboard shows tiles for every
21041
21041
  // registered project in one view). See docs/39-visibility-groupings.md.
21042
- dashboard: DashboardConfigSchema2.optional()
21042
+ dashboard: DashboardConfigSchema2.optional(),
21043
+ // HS-8446 — global diagnostics opt-in. When true, the slow-server
21044
+ // banner (HS-8175 / HS-8226) is allowed to surface AND the HS-8054
21045
+ // UI-hang toast fires. Default false so the noisier diagnostic
21046
+ // surfaces stay opt-in across every project on this machine. The
21047
+ // freeze-log entries (`<dataDir>/freeze.log`) and the server-side
21048
+ // event-loop heartbeat continue to fire regardless — the gate only
21049
+ // suppresses the in-window UI surfaces.
21050
+ diagnosticsEnabled: external_exports.boolean().optional()
21043
21051
  }).strict();
21044
21052
  }
21045
21053
  });
@@ -27276,14 +27284,17 @@ pageRoutes.get("/", (c) => {
27276
27284
  ] })
27277
27285
  ] }) }),
27278
27286
  /* @__PURE__ */ jsx("div", { className: "settings-section", style: "margin-top:16px", children: [
27279
- /* @__PURE__ */ jsx("div", { className: "settings-section-header", children: /* @__PURE__ */ jsx("h3", { children: "Diagnostics" }) }),
27287
+ /* @__PURE__ */ jsx("div", { className: "settings-section-header", children: /* @__PURE__ */ jsx("h3", { children: [
27288
+ "Diagnostics ",
27289
+ /* @__PURE__ */ jsx("span", { className: "global-setting-badge", title: "This setting applies to every project on this machine.", children: "Global Setting" })
27290
+ ] }) }),
27280
27291
  /* @__PURE__ */ jsx("div", { className: "settings-field settings-field-checkbox", children: [
27281
27292
  /* @__PURE__ */ jsx("label", { children: [
27282
- /* @__PURE__ */ jsx("input", { type: "checkbox", id: "settings-diagnostics-freeze-toast" }),
27283
- " Show UI-hang toast when freezes are detected"
27293
+ /* @__PURE__ */ jsx("input", { type: "checkbox", id: "settings-diagnostics-enabled" }),
27294
+ " Enable diagnostic UI surfaces (slow-server banner + UI-hang toast)"
27284
27295
  ] }),
27285
27296
  /* @__PURE__ */ jsx("span", { className: "settings-hint", children: [
27286
- "When on, a small toast pops in for each \u2265 500 ms UI hang the HS-8054 longtask observer catches (rate-limited to once every 10 s). Off by default. Freezes are always logged to ",
27297
+ "When on, the slow-server banner (HS-8175 / HS-8226) surfaces when an HTTP request stays in flight past 3 s, and the HS-8054 longtask observer emits a small toast for each \u2265 500 ms UI hang (rate-limited to once every 10 s). Off by default \u2014 both surfaces are primarily useful when actively investigating event-loop blocks. Freezes are always logged to ",
27287
27298
  /* @__PURE__ */ jsx("code", { children: "<dataDir>/freeze.log" }),
27288
27299
  " for diagnostics regardless of this setting."
27289
27300
  ] })