jefrichat-mcp 0.48.7 → 0.48.9

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.
Files changed (3) hide show
  1. package/dist/http.js +198 -57
  2. package/dist/index.js +72 -6
  3. package/package.json +1 -1
package/dist/http.js CHANGED
@@ -20888,8 +20888,8 @@ var require_application = __commonJS({
20888
20888
  tryRender(view, renderOptions, done);
20889
20889
  };
20890
20890
  app2.listen = function listen() {
20891
- var server = http2.createServer(this);
20892
- return server.listen.apply(server, arguments);
20891
+ var server2 = http2.createServer(this);
20892
+ return server2.listen.apply(server2, arguments);
20893
20893
  };
20894
20894
  function logerror(err) {
20895
20895
  if (this.get("env") !== "test") console.error(err.stack || err.toString());
@@ -33214,10 +33214,10 @@ var require_websocket_server = __commonJS({
33214
33214
  process.nextTick(emitClose, this);
33215
33215
  }
33216
33216
  } else {
33217
- const server = this._server;
33217
+ const server2 = this._server;
33218
33218
  this._removeListeners();
33219
33219
  this._removeListeners = this._server = null;
33220
- server.close(() => {
33220
+ server2.close(() => {
33221
33221
  emitClose(this);
33222
33222
  });
33223
33223
  }
@@ -33402,17 +33402,17 @@ var require_websocket_server = __commonJS({
33402
33402
  }
33403
33403
  };
33404
33404
  module.exports = WebSocketServer2;
33405
- function addListeners(server, map3) {
33406
- for (const event of Object.keys(map3)) server.on(event, map3[event]);
33405
+ function addListeners(server2, map3) {
33406
+ for (const event of Object.keys(map3)) server2.on(event, map3[event]);
33407
33407
  return function removeListeners() {
33408
33408
  for (const event of Object.keys(map3)) {
33409
- server.removeListener(event, map3[event]);
33409
+ server2.removeListener(event, map3[event]);
33410
33410
  }
33411
33411
  };
33412
33412
  }
33413
- function emitClose(server) {
33414
- server._state = CLOSED;
33415
- server.emit("close");
33413
+ function emitClose(server2) {
33414
+ server2._state = CLOSED;
33415
+ server2.emit("close");
33416
33416
  }
33417
33417
  function socketOnError() {
33418
33418
  this.destroy();
@@ -33431,11 +33431,11 @@ var require_websocket_server = __commonJS({
33431
33431
  ` + Object.keys(headers).map((h) => `${h}: ${headers[h]}`).join("\r\n") + "\r\n\r\n" + message
33432
33432
  );
33433
33433
  }
33434
- function abortHandshakeOrEmitwsClientError(server, req, socket, code3, message, headers) {
33435
- if (server.listenerCount("wsClientError")) {
33434
+ function abortHandshakeOrEmitwsClientError(server2, req, socket, code3, message, headers) {
33435
+ if (server2.listenerCount("wsClientError")) {
33436
33436
  const err = new Error(message);
33437
33437
  Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError);
33438
- server.emit("wsClientError", err, socket, req);
33438
+ server2.emit("wsClientError", err, socket, req);
33439
33439
  } else {
33440
33440
  abortHandshake(socket, code3, message, headers);
33441
33441
  }
@@ -60682,8 +60682,48 @@ var JefriClient = class _JefriClient {
60682
60682
  // heartbeat: did we get a pong since the last ping?
60683
60683
  hasConnected = false;
60684
60684
  // armed for auto-reconnect only after the hub `registered` us
60685
- lastPresence = "online";
60686
- // re-announce this on reconnect
60685
+ /** UNACKNOWLEDGED presence intent, or null. Set by presence(), cleared when
60686
+ * the hub's own presence_update for this identity confirms the declaration
60687
+ * landed. Null on reconnect means: send nothing and let the hub's persisted
60688
+ * answer stand.
60689
+ *
60690
+ * Acknowledgment is the point, and it is what the first fix missed: a value
60691
+ * kept after delivery means "ever declared", not "pending" — so a client
60692
+ * that once declared away kept re-asserting it on every reconnect for the
60693
+ * life of the process, stomping whatever a newer device had chosen since.
60694
+ * The same stale-overwrite, one level down. */
60695
+ pendingPresence = null;
60696
+ /** The self statusRev BASELINE captured when the pending declaration was made.
60697
+ * An ack must be CAUSALLY newer than this — the audit reproduced a delayed
60698
+ * rev-9 event spending intent declared at a rev-10 baseline, after which the
60699
+ * server had already moved on. Identity + status matching alone cannot order
60700
+ * events; the revision can. */
60701
+ pendingBaselineRev = 0;
60702
+ /** Highest revision seen for our own status, from registered and from our own
60703
+ * presence_update events. */
60704
+ selfStatusRev = 0;
60705
+ /** Would a reconnect re-send a presence frame? True only while a declaration
60706
+ * is UNACKNOWLEDGED. Exposed for tests: the wire behaviour needs a live hub,
60707
+ * but the rule is checkable without one. */
60708
+ wouldReassertPresence() {
60709
+ return this.pendingPresence !== null;
60710
+ }
60711
+ /** The acknowledgment that turns "pending" back into nothing: the hub
60712
+ * broadcasts presence_update to the declaring identity too, so our own
60713
+ * username carrying our pending status means the declaration LANDED — and
60714
+ * re-asserting it on a later reconnect would stomp whatever a newer device
60715
+ * declares afterwards. A METHOD, not inline in the socket handler, so tests
60716
+ * drive the rule production runs instead of a copy of it — a test-local
60717
+ * re-implementation of this exact logic passed with the real clearing
60718
+ * deleted, which is how the previous round's test enshrined the bug. */
60719
+ notePresenceAck(ev) {
60720
+ if (ev?.event !== "presence_update" || ev.username !== this.identity?.username) return;
60721
+ const frontier = this.selfStatusRev;
60722
+ if (typeof ev.rev === "number" && ev.rev > this.selfStatusRev) this.selfStatusRev = ev.rev;
60723
+ if (this.pendingPresence === null || ev.status !== this.pendingPresence) return;
60724
+ if (typeof ev.rev !== "number" || ev.rev <= this.pendingBaselineRev || ev.rev < frontier) return;
60725
+ this.pendingPresence = null;
60726
+ }
60687
60727
  clientInfo;
60688
60728
  // handshake, re-sent on reconnect
60689
60729
  /** Is the hub socket usable RIGHT NOW?
@@ -60697,15 +60737,15 @@ var JefriClient = class _JefriClient {
60697
60737
  get online() {
60698
60738
  return this.hasConnected && !this.closing && this.ws?.readyState === this.ws?.OPEN;
60699
60739
  }
60700
- constructor(server) {
60701
- this.server = server.replace(/\/$/, "");
60740
+ constructor(server2) {
60741
+ this.server = server2.replace(/\/$/, "");
60702
60742
  }
60703
60743
  static async connect(opts = {}) {
60704
- const server = (opts.server ?? "https://jefrichat.com").replace(/\/$/, "");
60705
- const client = new _JefriClient(server);
60744
+ const server2 = (opts.server ?? "https://jefrichat.com").replace(/\/$/, "");
60745
+ const client = new _JefriClient(server2);
60706
60746
  let token = opts.token;
60707
60747
  if (!token) {
60708
- const res = await fetch(`${server}/api/agents`, {
60748
+ const res = await fetch(`${server2}/api/agents`, {
60709
60749
  method: "POST",
60710
60750
  headers: { "content-type": "application/json" },
60711
60751
  body: JSON.stringify({
@@ -60778,7 +60818,8 @@ var JefriClient = class _JefriClient {
60778
60818
  this.ws.on("open", () => {
60779
60819
  this.startPing();
60780
60820
  try {
60781
- this.ws.send(JSON.stringify({ type: "presence", status: this.lastPresence }));
60821
+ if (this.pendingPresence !== null)
60822
+ this.ws.send(JSON.stringify({ type: "presence", status: this.pendingPresence }));
60782
60823
  if (this.clientInfo)
60783
60824
  this.ws.send(JSON.stringify({ type: "client_info", ...this.clientInfo }));
60784
60825
  } catch {
@@ -60807,12 +60848,15 @@ var JefriClient = class _JefriClient {
60807
60848
  }
60808
60849
  if (ev.event === "registered") {
60809
60850
  this.identity = ev.identity;
60851
+ const rev = ev.identity?.statusRev;
60852
+ if (typeof rev === "number" && rev > this.selfStatusRev) this.selfStatusRev = rev;
60810
60853
  this.hasConnected = true;
60811
60854
  this.reconnectAttempts = 0;
60812
60855
  }
60813
60856
  if (ev.event === "error" && /auth/i.test(ev.message ?? "")) {
60814
60857
  this.authFailed = true;
60815
60858
  }
60859
+ this.notePresenceAck(ev);
60816
60860
  this.emit(ev.event, ev);
60817
60861
  this.emit("*", ev);
60818
60862
  });
@@ -60901,7 +60945,8 @@ var JefriClient = class _JefriClient {
60901
60945
  }
60902
60946
  // --- actions -------------------------------------------------------------
60903
60947
  presence(status) {
60904
- this.lastPresence = status;
60948
+ this.pendingPresence = status;
60949
+ this.pendingBaselineRev = this.selfStatusRev;
60905
60950
  this.send({ type: "presence", status });
60906
60951
  }
60907
60952
  message(to, content3) {
@@ -62005,7 +62050,21 @@ function waitFor(c, event, match, timeoutMs = 4e3) {
62005
62050
  }, timeoutMs);
62006
62051
  });
62007
62052
  }
62008
- var ok3 = (text5) => ({ content: [{ type: "text", text: text5 }] });
62053
+ var upgradeNotice = null;
62054
+ var upgradeShownAt = 0;
62055
+ var UPGRADE_REPEAT_MS = 24 * 60 * 60 * 1e3;
62056
+ function takeUpgradeNotice(now = Date.now()) {
62057
+ if (!upgradeNotice) return null;
62058
+ if (upgradeShownAt && now - upgradeShownAt < UPGRADE_REPEAT_MS) return null;
62059
+ upgradeShownAt = now;
62060
+ return upgradeNotice;
62061
+ }
62062
+ var ok3 = (text5, now = Date.now()) => {
62063
+ const notice = takeUpgradeNotice(now);
62064
+ return { content: [{ type: "text", text: notice ? `${text5}
62065
+
62066
+ ${notice}` : text5 }] };
62067
+ };
62009
62068
  var fail = (text5) => ({ content: [{ type: "text", text: text5 }], isError: true });
62010
62069
  var WATERMARK_FILE = np3.join(os4.homedir(), ".jefri", "inbox.json");
62011
62070
  var inboxWatermark = new Map(
@@ -62024,9 +62083,9 @@ function saveWatermark() {
62024
62083
  } catch {
62025
62084
  }
62026
62085
  }
62027
- function registerAcpTools(server, ctx) {
62086
+ function registerAcpTools(server2, ctx) {
62028
62087
  const ENABLE_E2E = process.env.JEFRI_ENABLE_E2E === "true";
62029
- const e2eServer = ENABLE_E2E ? server : { registerTool: () => void 0 };
62088
+ const e2eServer = ENABLE_E2E ? server2 : { registerTool: () => void 0 };
62030
62089
  const withClient = async (fn) => {
62031
62090
  try {
62032
62091
  const c = await ctx.ensureClient();
@@ -62036,7 +62095,7 @@ function registerAcpTools(server, ctx) {
62036
62095
  ${e?.message ?? e}`);
62037
62096
  }
62038
62097
  };
62039
- server.registerTool(
62098
+ server2.registerTool(
62040
62099
  "jefri_whoami",
62041
62100
  {
62042
62101
  title: "Who am I on Jefri Chat",
@@ -62047,7 +62106,7 @@ ${e?.message ?? e}`);
62047
62106
  async (c) => ok3(`You are "${c.identity.displayName}" (@${c.identity.username}) on Jefri Chat at ${ctx.serverUrl}, status: online.`)
62048
62107
  )
62049
62108
  );
62050
- server.registerTool(
62109
+ server2.registerTool(
62051
62110
  "jefri_agents",
62052
62111
  {
62053
62112
  title: "List your connections",
@@ -62083,7 +62142,7 @@ ${e?.message ?? e}`);
62083
62142
  }
62084
62143
  return { status: res.status, body };
62085
62144
  };
62086
- server.registerTool(
62145
+ server2.registerTool(
62087
62146
  "jefri_agent_apps",
62088
62147
  {
62089
62148
  title: "See where your agents are running",
@@ -62107,7 +62166,7 @@ ${lines.join("\n")}
62107
62166
  Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
62108
62167
  })
62109
62168
  );
62110
- server.registerTool(
62169
+ server2.registerTool(
62111
62170
  "jefri_deactivate",
62112
62171
  {
62113
62172
  title: "Deactivate an agent",
@@ -62120,7 +62179,7 @@ Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
62120
62179
  return ok3(`\u{1F6D1} Deactivated @${agent}. It's disconnected and can't reconnect from any app until you reactivate it.`);
62121
62180
  })
62122
62181
  );
62123
- server.registerTool(
62182
+ server2.registerTool(
62124
62183
  "jefri_activate",
62125
62184
  {
62126
62185
  title: "Reactivate an agent",
@@ -62133,7 +62192,7 @@ Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
62133
62192
  return ok3(`\u2705 Reactivated @${agent}. It can connect again now.`);
62134
62193
  })
62135
62194
  );
62136
- server.registerTool(
62195
+ server2.registerTool(
62137
62196
  "jefri_send",
62138
62197
  {
62139
62198
  title: "Send a message",
@@ -62286,7 +62345,7 @@ ${fp}`);
62286
62345
  return ok3(`Sent private E2E group file.`);
62287
62346
  })
62288
62347
  );
62289
- server.registerTool(
62348
+ server2.registerTool(
62290
62349
  "jefri_send_file",
62291
62350
  {
62292
62351
  title: "Send a file, PDF, or image",
@@ -62370,7 +62429,7 @@ ${psCmd}
62370
62429
  return ok3(`Sent \u{1F4CE} ${name} to @${to}${caption ? ` \u2014 "${caption}"` : ""}`);
62371
62430
  })
62372
62431
  );
62373
- server.registerTool(
62432
+ server2.registerTool(
62374
62433
  "jefri_send_code",
62375
62434
  {
62376
62435
  title: "Send code / text as a file",
@@ -62405,7 +62464,7 @@ ${psCmd}
62405
62464
  return ok3(`Sent \u{1F4C4} ${fileName} (${content3.length} chars) to @${to}${caption ? ` \u2014 "${caption}"` : ""}`);
62406
62465
  })
62407
62466
  );
62408
- server.registerTool(
62467
+ server2.registerTool(
62409
62468
  "jefri_send_folder",
62410
62469
  {
62411
62470
  title: "Send a whole folder / project (zipped)",
@@ -62503,7 +62562,7 @@ ${psCmd}
62503
62562
  );
62504
62563
  })
62505
62564
  );
62506
- server.registerTool(
62565
+ server2.registerTool(
62507
62566
  "jefri_download_file",
62508
62567
  {
62509
62568
  title: "Download / save a file someone sent you",
@@ -62550,7 +62609,7 @@ It downloads the file to your machine (current folder unless you set a path). Fi
62550
62609
  );
62551
62610
  })
62552
62611
  );
62553
- server.registerTool(
62612
+ server2.registerTool(
62554
62613
  "jefri_connect",
62555
62614
  {
62556
62615
  title: "Request a connection",
@@ -62566,7 +62625,7 @@ It downloads the file to your machine (current folder unless you set a path). Fi
62566
62625
  })
62567
62626
  );
62568
62627
  const hubBase = () => ctx.serverUrl.replace(/\/$/, "");
62569
- server.registerTool(
62628
+ server2.registerTool(
62570
62629
  "jefri_departments",
62571
62630
  {
62572
62631
  title: "List your departments",
@@ -62580,7 +62639,7 @@ It downloads the file to your machine (current folder unless you set a path). Fi
62580
62639
  return ok3(d.map((x) => `\u2022 ${x.name} (${x.org}) \u2014 access: ${x.myLevel} [id: ${x.id}]`).join("\n"));
62581
62640
  })
62582
62641
  );
62583
- server.registerTool(
62642
+ server2.registerTool(
62584
62643
  "jefri_dept_files",
62585
62644
  {
62586
62645
  title: "List a department's files",
@@ -62595,7 +62654,7 @@ It downloads the file to your machine (current folder unless you set a path). Fi
62595
62654
  return ok3(files.map((f) => `\u2022 ${f.name} [${f.kind}] \u2014 by ${f.createdBy}, ${new Date(f.updatedAt).toLocaleDateString()} (id: ${f.id})`).join("\n"));
62596
62655
  })
62597
62656
  );
62598
- server.registerTool(
62657
+ server2.registerTool(
62599
62658
  "jefri_dept_read",
62600
62659
  {
62601
62660
  title: "Download a department file",
@@ -62625,7 +62684,7 @@ ${cmd}
62625
62684
  (Set your token first: export JEFRI_TOKEN=\u2026 from the app's Connect dialog \u2014 it isn't printed here on purpose.)`);
62626
62685
  })
62627
62686
  );
62628
- server.registerTool(
62687
+ server2.registerTool(
62629
62688
  "jefri_dept_add_file",
62630
62689
  {
62631
62690
  title: "Add a file to a department",
@@ -62648,7 +62707,7 @@ ${cmd}
62648
62707
  (With 'write' access it's added directly; with 'propose' access it's submitted for an admin to approve; read-only is rejected.)`);
62649
62708
  })
62650
62709
  );
62651
- server.registerTool(
62710
+ server2.registerTool(
62652
62711
  "jefri_my_docs",
62653
62712
  {
62654
62713
  title: "List my documents (memory)",
@@ -62667,7 +62726,7 @@ ${cmd}
62667
62726
  );
62668
62727
  })
62669
62728
  );
62670
- server.registerTool(
62729
+ server2.registerTool(
62671
62730
  "jefri_search_docs",
62672
62731
  {
62673
62732
  title: "Search my document memory",
@@ -62690,7 +62749,7 @@ ${h.text}`).join("\n\n---\n\n")
62690
62749
  );
62691
62750
  })
62692
62751
  );
62693
- server.registerTool(
62752
+ server2.registerTool(
62694
62753
  "jefri_read_doc",
62695
62754
  {
62696
62755
  title: "Read/get one of my documents",
@@ -62715,7 +62774,7 @@ ${cmd}
62715
62774
  (Set your token first: export JEFRI_TOKEN=\u2026 from the app's Connect dialog \u2014 it isn't printed here on purpose.)`);
62716
62775
  })
62717
62776
  );
62718
- server.registerTool(
62777
+ server2.registerTool(
62719
62778
  "jefri_add_doc",
62720
62779
  {
62721
62780
  title: "Add a document to my memory",
@@ -62735,7 +62794,7 @@ Or just upload ${fname} from the agent's page in the web app.`
62735
62794
  );
62736
62795
  })
62737
62796
  );
62738
- server.registerTool(
62797
+ server2.registerTool(
62739
62798
  "jefri_my_tasks",
62740
62799
  {
62741
62800
  title: "My department tasks",
@@ -62752,7 +62811,7 @@ Or just upload ${fname} from the agent's page in the web app.`
62752
62811
  );
62753
62812
  })
62754
62813
  );
62755
- server.registerTool(
62814
+ server2.registerTool(
62756
62815
  "jefri_task_status",
62757
62816
  {
62758
62817
  title: "Update a task's status",
@@ -62774,7 +62833,7 @@ Or just upload ${fname} from the agent's page in the web app.`
62774
62833
  return ok3(`Task marked ${status === "done" ? "\u2705 done" : status}.`);
62775
62834
  })
62776
62835
  );
62777
- server.registerTool(
62836
+ server2.registerTool(
62778
62837
  "jefri_inbox",
62779
62838
  {
62780
62839
  title: "Read unread messages",
@@ -62830,7 +62889,7 @@ Or just upload ${fname} from the agent's page in the web app.`
62830
62889
  ` + lines.join("\n"));
62831
62890
  })
62832
62891
  );
62833
- server.registerTool(
62892
+ server2.registerTool(
62834
62893
  "jefri_groups",
62835
62894
  {
62836
62895
  title: "List your groups",
@@ -62846,7 +62905,7 @@ Or just upload ${fname} from the agent's page in the web app.`
62846
62905
  );
62847
62906
  })
62848
62907
  );
62849
- server.registerTool(
62908
+ server2.registerTool(
62850
62909
  "jefri_send_group",
62851
62910
  {
62852
62911
  title: "Send a group message",
@@ -62864,7 +62923,7 @@ Or just upload ${fname} from the agent's page in the web app.`
62864
62923
  return ok3(`Posted to the group.`);
62865
62924
  })
62866
62925
  );
62867
- server.registerTool(
62926
+ server2.registerTool(
62868
62927
  "jefri_send_group_file",
62869
62928
  {
62870
62929
  title: "Send a file to a group",
@@ -62938,7 +62997,7 @@ Or just upload ${fname} from the agent's page in the web app.`
62938
62997
  return ok3(`Sent \u{1F4CE} ${name} to the group.`);
62939
62998
  })
62940
62999
  );
62941
- server.registerTool(
63000
+ server2.registerTool(
62942
63001
  "jefri_history",
62943
63002
  {
62944
63003
  title: "Read conversation history",
@@ -62978,7 +63037,7 @@ Or just upload ${fname} from the agent's page in the web app.`
62978
63037
  return ok3(lines.join("\n"));
62979
63038
  })
62980
63039
  );
62981
- server.registerTool(
63040
+ server2.registerTool(
62982
63041
  "jefri_search",
62983
63042
  {
62984
63043
  title: "Discover agents",
@@ -62996,7 +63055,7 @@ Or just upload ${fname} from the agent's page in the web app.`
62996
63055
  );
62997
63056
  })
62998
63057
  );
62999
- server.registerTool(
63058
+ server2.registerTool(
63000
63059
  "jefri_set_status",
63001
63060
  {
63002
63061
  title: "Set presence",
@@ -63011,7 +63070,7 @@ Or just upload ${fname} from the agent's page in the web app.`
63011
63070
  })
63012
63071
  );
63013
63072
  if (ctx.local) {
63014
- server.registerTool(
63073
+ server2.registerTool(
63015
63074
  "jefri_notifications",
63016
63075
  {
63017
63076
  title: "Notification settings",
@@ -63090,7 +63149,7 @@ Or just upload ${fname} from the agent's page in the web app.`
63090
63149
  return ok3(summary);
63091
63150
  })
63092
63151
  );
63093
- server.registerTool(
63152
+ server2.registerTool(
63094
63153
  "jefri_autonomous",
63095
63154
  {
63096
63155
  title: "Autonomous mode",
@@ -63167,6 +63226,34 @@ function attachInbox(c, inbox, self, onIncoming) {
63167
63226
  c.on("file_received", capture);
63168
63227
  }
63169
63228
 
63229
+ // src/drain.ts
63230
+ function durationEnv(name, fallback, env = process.env) {
63231
+ const raw = env[name];
63232
+ if (raw === void 0 || raw === "") return fallback;
63233
+ const n = Number(raw);
63234
+ if (!Number.isFinite(n) || n <= 0) {
63235
+ console.error(`[mcp] ignoring ${name}="${raw}" \u2014 expected a positive number; using ${fallback}ms`);
63236
+ return fallback;
63237
+ }
63238
+ return n;
63239
+ }
63240
+ function sweepIdleSessions(map3) {
63241
+ let closed = 0;
63242
+ let busy = 0;
63243
+ for (const s of map3) {
63244
+ if (s.inflight > 0) {
63245
+ busy++;
63246
+ continue;
63247
+ }
63248
+ try {
63249
+ s.transport.close?.();
63250
+ closed++;
63251
+ } catch {
63252
+ }
63253
+ }
63254
+ return { closed, busy };
63255
+ }
63256
+
63170
63257
  // src/http.ts
63171
63258
  var HUB = process.env.JEFRI_SERVER ?? "http://localhost:4000";
63172
63259
  var PORT = Number(process.env.MCP_HTTP_PORT ?? 4001);
@@ -63310,7 +63397,14 @@ app.use((req, res, next) => {
63310
63397
  next();
63311
63398
  });
63312
63399
  app.use(import_express.default.json({ limit: process.env.MCP_MAX_BODY ?? "4mb" }));
63313
- var health = (_req, res) => res.json({ ok: true, service: "jefrichat-mcp-http", hub: HUB, sessions: sessions.size });
63400
+ var draining = false;
63401
+ var health = (_req, res) => res.status(draining ? 503 : 200).json({
63402
+ ok: !draining,
63403
+ draining,
63404
+ service: "jefrichat-mcp-http",
63405
+ hub: HUB,
63406
+ sessions: sessions.size
63407
+ });
63314
63408
  app.get("/health", health);
63315
63409
  app.get("/mcp/health", health);
63316
63410
  app.post("/mcp", async (req, res) => {
@@ -63348,6 +63442,14 @@ app.post("/mcp", async (req, res) => {
63348
63442
  }
63349
63443
  return;
63350
63444
  }
63445
+ if (draining && !sessionId && isInitializeRequest(req.body)) {
63446
+ res.setHeader("retry-after", "1");
63447
+ return res.status(503).json({
63448
+ jsonrpc: "2.0",
63449
+ error: { code: -32e3, message: "server is shutting down \u2014 retry; another instance will accept this session" },
63450
+ id: null
63451
+ });
63452
+ }
63351
63453
  if (!sessionId && isInitializeRequest(req.body)) {
63352
63454
  if (initRateLimited(req)) {
63353
63455
  res.status(429).json({
@@ -63488,13 +63590,52 @@ var bySession = async (req, res) => {
63488
63590
  };
63489
63591
  app.get("/mcp", bySession);
63490
63592
  app.delete("/mcp", bySession);
63491
- http.createServer(app).listen(PORT, () => {
63593
+ var server = http.createServer(app).listen(PORT, () => {
63492
63594
  console.error(`
63493
63595
  \u{1F310} Jefri Chat remote MCP (HTTP) on http://localhost:${PORT}/mcp`);
63494
63596
  console.error(` hub: ${HUB}`);
63495
63597
  console.error(` auth: Authorization: Bearer <jefri_token>
63496
63598
  `);
63497
63599
  });
63600
+ var DRAIN_MS = durationEnv("MCP_DRAIN_MS", 15e3);
63601
+ var UNHEALTHY_MS = durationEnv("MCP_UNHEALTHY_MS", 5e3);
63602
+ var shuttingDown = false;
63603
+ function drain2(signal) {
63604
+ if (shuttingDown) return;
63605
+ shuttingDown = true;
63606
+ draining = true;
63607
+ const deadline = Date.now() + UNHEALTHY_MS + DRAIN_MS;
63608
+ console.error(
63609
+ `[mcp] ${signal} \u2014 unhealthy for ${UNHEALTHY_MS}ms, then draining ${sessions.size} session(s) for up to ${DRAIN_MS}ms`
63610
+ );
63611
+ const closeAndFinish = () => {
63612
+ server.close(() => {
63613
+ console.error("[mcp] drained cleanly");
63614
+ process.exit(0);
63615
+ });
63616
+ const sweep = setInterval(() => {
63617
+ const { busy } = sweepIdleSessions(sessions.values());
63618
+ if (busy === 0) {
63619
+ clearInterval(sweep);
63620
+ console.error("[mcp] all sessions idle \u2014 closing");
63621
+ return;
63622
+ }
63623
+ if (Date.now() >= deadline) {
63624
+ clearInterval(sweep);
63625
+ console.error(`[mcp] drain deadline reached with ${busy} session(s) still busy \u2014 exiting`);
63626
+ process.exit(0);
63627
+ }
63628
+ }, 250);
63629
+ sweep.unref();
63630
+ };
63631
+ setTimeout(closeAndFinish, UNHEALTHY_MS).unref();
63632
+ setTimeout(() => {
63633
+ console.error("[mcp] drain deadline reached \u2014 exiting");
63634
+ process.exit(0);
63635
+ }, UNHEALTHY_MS + DRAIN_MS + 500).unref();
63636
+ }
63637
+ process.on("SIGTERM", () => drain2("SIGTERM"));
63638
+ process.on("SIGINT", () => drain2("SIGINT"));
63498
63639
  /*! Bundled license information:
63499
63640
 
63500
63641
  depd/index.js:
package/dist/index.js CHANGED
@@ -38648,8 +38648,48 @@ var JefriClient = class _JefriClient {
38648
38648
  // heartbeat: did we get a pong since the last ping?
38649
38649
  hasConnected = false;
38650
38650
  // armed for auto-reconnect only after the hub `registered` us
38651
- lastPresence = "online";
38652
- // re-announce this on reconnect
38651
+ /** UNACKNOWLEDGED presence intent, or null. Set by presence(), cleared when
38652
+ * the hub's own presence_update for this identity confirms the declaration
38653
+ * landed. Null on reconnect means: send nothing and let the hub's persisted
38654
+ * answer stand.
38655
+ *
38656
+ * Acknowledgment is the point, and it is what the first fix missed: a value
38657
+ * kept after delivery means "ever declared", not "pending" — so a client
38658
+ * that once declared away kept re-asserting it on every reconnect for the
38659
+ * life of the process, stomping whatever a newer device had chosen since.
38660
+ * The same stale-overwrite, one level down. */
38661
+ pendingPresence = null;
38662
+ /** The self statusRev BASELINE captured when the pending declaration was made.
38663
+ * An ack must be CAUSALLY newer than this — the audit reproduced a delayed
38664
+ * rev-9 event spending intent declared at a rev-10 baseline, after which the
38665
+ * server had already moved on. Identity + status matching alone cannot order
38666
+ * events; the revision can. */
38667
+ pendingBaselineRev = 0;
38668
+ /** Highest revision seen for our own status, from registered and from our own
38669
+ * presence_update events. */
38670
+ selfStatusRev = 0;
38671
+ /** Would a reconnect re-send a presence frame? True only while a declaration
38672
+ * is UNACKNOWLEDGED. Exposed for tests: the wire behaviour needs a live hub,
38673
+ * but the rule is checkable without one. */
38674
+ wouldReassertPresence() {
38675
+ return this.pendingPresence !== null;
38676
+ }
38677
+ /** The acknowledgment that turns "pending" back into nothing: the hub
38678
+ * broadcasts presence_update to the declaring identity too, so our own
38679
+ * username carrying our pending status means the declaration LANDED — and
38680
+ * re-asserting it on a later reconnect would stomp whatever a newer device
38681
+ * declares afterwards. A METHOD, not inline in the socket handler, so tests
38682
+ * drive the rule production runs instead of a copy of it — a test-local
38683
+ * re-implementation of this exact logic passed with the real clearing
38684
+ * deleted, which is how the previous round's test enshrined the bug. */
38685
+ notePresenceAck(ev) {
38686
+ if (ev?.event !== "presence_update" || ev.username !== this.identity?.username) return;
38687
+ const frontier = this.selfStatusRev;
38688
+ if (typeof ev.rev === "number" && ev.rev > this.selfStatusRev) this.selfStatusRev = ev.rev;
38689
+ if (this.pendingPresence === null || ev.status !== this.pendingPresence) return;
38690
+ if (typeof ev.rev !== "number" || ev.rev <= this.pendingBaselineRev || ev.rev < frontier) return;
38691
+ this.pendingPresence = null;
38692
+ }
38653
38693
  clientInfo;
38654
38694
  // handshake, re-sent on reconnect
38655
38695
  /** Is the hub socket usable RIGHT NOW?
@@ -38744,7 +38784,8 @@ var JefriClient = class _JefriClient {
38744
38784
  this.ws.on("open", () => {
38745
38785
  this.startPing();
38746
38786
  try {
38747
- this.ws.send(JSON.stringify({ type: "presence", status: this.lastPresence }));
38787
+ if (this.pendingPresence !== null)
38788
+ this.ws.send(JSON.stringify({ type: "presence", status: this.pendingPresence }));
38748
38789
  if (this.clientInfo)
38749
38790
  this.ws.send(JSON.stringify({ type: "client_info", ...this.clientInfo }));
38750
38791
  } catch {
@@ -38773,12 +38814,15 @@ var JefriClient = class _JefriClient {
38773
38814
  }
38774
38815
  if (ev.event === "registered") {
38775
38816
  this.identity = ev.identity;
38817
+ const rev = ev.identity?.statusRev;
38818
+ if (typeof rev === "number" && rev > this.selfStatusRev) this.selfStatusRev = rev;
38776
38819
  this.hasConnected = true;
38777
38820
  this.reconnectAttempts = 0;
38778
38821
  }
38779
38822
  if (ev.event === "error" && /auth/i.test(ev.message ?? "")) {
38780
38823
  this.authFailed = true;
38781
38824
  }
38825
+ this.notePresenceAck(ev);
38782
38826
  this.emit(ev.event, ev);
38783
38827
  this.emit("*", ev);
38784
38828
  });
@@ -38867,7 +38911,8 @@ var JefriClient = class _JefriClient {
38867
38911
  }
38868
38912
  // --- actions -------------------------------------------------------------
38869
38913
  presence(status) {
38870
- this.lastPresence = status;
38914
+ this.pendingPresence = status;
38915
+ this.pendingBaselineRev = this.selfStatusRev;
38871
38916
  this.send({ type: "presence", status });
38872
38917
  }
38873
38918
  message(to, content3) {
@@ -40788,7 +40833,24 @@ function waitFor(c2, event, match, timeoutMs = 4e3) {
40788
40833
  }, timeoutMs);
40789
40834
  });
40790
40835
  }
40791
- var ok3 = (text5) => ({ content: [{ type: "text", text: text5 }] });
40836
+ var upgradeNotice = null;
40837
+ var upgradeShownAt = 0;
40838
+ var UPGRADE_REPEAT_MS = 24 * 60 * 60 * 1e3;
40839
+ function setUpgradeNotice(text5) {
40840
+ upgradeNotice = text5;
40841
+ }
40842
+ function takeUpgradeNotice(now = Date.now()) {
40843
+ if (!upgradeNotice) return null;
40844
+ if (upgradeShownAt && now - upgradeShownAt < UPGRADE_REPEAT_MS) return null;
40845
+ upgradeShownAt = now;
40846
+ return upgradeNotice;
40847
+ }
40848
+ var ok3 = (text5, now = Date.now()) => {
40849
+ const notice = takeUpgradeNotice(now);
40850
+ return { content: [{ type: "text", text: notice ? `${text5}
40851
+
40852
+ ${notice}` : text5 }] };
40853
+ };
40792
40854
  var fail = (text5) => ({ content: [{ type: "text", text: text5 }], isError: true });
40793
40855
  var WATERMARK_FILE = np4.join(os4.homedir(), ".jefri", "inbox.json");
40794
40856
  var inboxWatermark = new Map(
@@ -42936,10 +42998,14 @@ async function main() {
42936
42998
  log("unhandled rejection:", reason?.stack ?? reason);
42937
42999
  });
42938
43000
  void checkForUpdate().then((u) => {
42939
- if (u?.outdated)
43001
+ if (u?.outdated) {
43002
+ setUpgradeNotice(
43003
+ `\u26A0\uFE0F Jefri Chat connector ${u.current} is out of date (latest ${u.latest}). Restart this agent to upgrade \u2014 a running connector keeps the build it started with. If it was started with npx, that is enough; if installed globally, run: npm update -g jefrichat-mcp`
43004
+ );
42940
43005
  note(
42941
43006
  `\u2191 update available: jefrichat-mcp ${u.current} \u2192 ${u.latest}. Reconnect this agent (npx jefrichat-mcp@latest, or 'npm update -g jefrichat-mcp@latest' if globally installed) to get it.`
42942
43007
  );
43008
+ }
42943
43009
  }).catch(() => {
42944
43010
  });
42945
43011
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jefrichat-mcp",
3
- "version": "0.48.7",
3
+ "version": "0.48.9",
4
4
  "description": "Jefri Chat connector — join the Jefri Chat network (WhatsApp for AI agents) from any MCP client (Claude, Codex, Cursor, …).",
5
5
  "type": "module",
6
6
  "bin": {