kojee-mcp 0.7.1 → 0.7.3

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 (32) hide show
  1. package/dist/{chunk-KPMD72FY.js → chunk-6XWTUDWW.js} +65 -26
  2. package/dist/{chunk-GNLCUJBK.js → chunk-A4IOKD4Z.js} +100 -2
  3. package/dist/{chunk-FBJCPRVH.js → chunk-ABECLEYE.js} +62 -11
  4. package/dist/{chunk-SSW5AQSR.js → chunk-FJUAMJHU.js} +3 -6
  5. package/dist/{chunk-FSKGQ6GT.js → chunk-HI42GBQ3.js} +5 -0
  6. package/dist/chunk-IZN7IZPW.js +132 -0
  7. package/dist/{chunk-UPJV7GBE.js → chunk-TCWIXG5C.js} +1 -1
  8. package/dist/{chunk-QJFMU4QC.js → chunk-TMCNB4JH.js} +1 -1
  9. package/dist/chunk-XLRF5ATG.js +103 -0
  10. package/dist/{chunk-UFHGZUST.js → chunk-XPIW4N55.js} +22 -14
  11. package/dist/cli.js +23 -14
  12. package/dist/codex-prompt-submit-hook-J5PZEJSK.js +39 -0
  13. package/dist/codex-stop-hook-32W4BIOM.js +136 -0
  14. package/dist/{connect-handler-4DRTFMOB.js → connect-handler-NZINEMG3.js} +14 -7
  15. package/dist/{doctor-5QJ3HGNR.js → doctor-WUU5BVPT.js} +2 -2
  16. package/dist/doctor-codex-PJFWIABF.js +370 -0
  17. package/dist/{event-stream-KRYWEYWO.js → event-stream-WPN3EN7C.js} +5 -1
  18. package/dist/index.js +5 -5
  19. package/dist/{install-V7LSQCYZ.js → install-JQNDGAAQ.js} +14 -2
  20. package/dist/lib.d.ts +40 -15
  21. package/dist/lib.js +7 -7
  22. package/dist/pending-state-6TVRR63P.js +134 -0
  23. package/dist/{registry-ZZZ26WGA.js → registry-2QS42EQF.js} +114 -51
  24. package/dist/{server-ITPFQVTK.js → server-DU3LFS32.js} +4 -2
  25. package/dist/{setup-handler-JFM45NCN.js → setup-handler-44ASXYMS.js} +7 -7
  26. package/dist/{stop-hook-5ABGTC2O.js → stop-hook-N6TX4YQT.js} +2 -2
  27. package/dist/{tail-stream-NBGHHBS4.js → tail-stream-N43D53RC.js} +99 -19
  28. package/package.json +1 -1
  29. package/skills/using-tandems/SKILL.md +2 -2
  30. package/dist/chunk-EBYUJM3H.js +0 -14
  31. package/dist/codex-stop-hook-BMOJVM6O.js +0 -96
  32. package/dist/doctor-codex-VGJKTX2E.js +0 -163
@@ -0,0 +1,134 @@
1
+ import {
2
+ recordDrained
3
+ } from "./chunk-IZN7IZPW.js";
4
+
5
+ // src/tandem/pending-state.ts
6
+ var PENDING_TOOL_RUNTIMES = /* @__PURE__ */ new Set(["codex"]);
7
+ var LEDGER_DRAIN_RUNTIMES = /* @__PURE__ */ new Set(["codex"]);
8
+ var TANDEM_PENDING_TOOL_NAME = "tandem_pending";
9
+ var REJOIN_ADVICE = "if you were in rooms before a restart, rejoin with your previous seat_name \u2014 the room rebinds your seat";
10
+ var MAX_TRACKED_ROOMS = 256;
11
+ function usableCursor(cursor) {
12
+ if (!Number.isFinite(cursor) || cursor < 0) return null;
13
+ return Math.floor(cursor);
14
+ }
15
+ function createSessionPendingState() {
16
+ const rooms = /* @__PURE__ */ new Map();
17
+ let clock = 0;
18
+ const touch = (tandemId) => {
19
+ let rec = rooms.get(tandemId);
20
+ if (rec === void 0) {
21
+ rec = { seat: false, delivered: 0, drained: 0, touched: 0 };
22
+ rooms.set(tandemId, rec);
23
+ evictIfOverCap(tandemId);
24
+ }
25
+ rec.touched = ++clock;
26
+ return rec;
27
+ };
28
+ const evictIfOverCap = (justInserted) => {
29
+ while (rooms.size > MAX_TRACKED_ROOMS) {
30
+ let victim = null;
31
+ let victimTouched = Number.POSITIVE_INFINITY;
32
+ let victimPending = true;
33
+ for (const [id, rec] of rooms) {
34
+ if (id === justInserted) continue;
35
+ const isPending = rec.delivered > rec.drained;
36
+ const better = victimPending && !isPending || victimPending === isPending && rec.touched < victimTouched;
37
+ if (better) {
38
+ victim = id;
39
+ victimTouched = rec.touched;
40
+ victimPending = isPending;
41
+ }
42
+ }
43
+ if (victim === null) return;
44
+ rooms.delete(victim);
45
+ }
46
+ };
47
+ return {
48
+ noteSeat(tandemId) {
49
+ if (tandemId) touch(tandemId).seat = true;
50
+ },
51
+ dropSeat(tandemId) {
52
+ rooms.delete(tandemId);
53
+ },
54
+ noteDelivered(tandemId, cursor) {
55
+ const next = usableCursor(cursor);
56
+ if (tandemId === "" || next === null) return;
57
+ const rec = touch(tandemId);
58
+ rec.delivered = Math.max(rec.delivered, next);
59
+ },
60
+ noteDrained(tandemId, cursor) {
61
+ const next = usableCursor(cursor);
62
+ if (tandemId === "" || next === null) return;
63
+ const rec = touch(tandemId);
64
+ rec.drained = Math.max(rec.drained, next);
65
+ },
66
+ snapshot(filterTandemId) {
67
+ const pending = [];
68
+ let sessionHasSeats = false;
69
+ for (const [tandemId, rec] of rooms) {
70
+ if (!rec.seat && rec.delivered === 0) continue;
71
+ sessionHasSeats = true;
72
+ if (filterTandemId !== void 0 && tandemId !== filterTandemId) continue;
73
+ if (rec.delivered > rec.drained) {
74
+ pending.push({
75
+ tandem_id: tandemId,
76
+ since_cursor: rec.drained,
77
+ delivered_cursor: rec.delivered
78
+ });
79
+ }
80
+ }
81
+ const advice = !sessionHasSeats ? REJOIN_ADVICE : pending.length > 0 ? "drain each pending room with tandem_messages(tandem_id, since=since_cursor), then reply in the room" : "no pending Tandem events for this window";
82
+ return { pending, session_has_seats: sessionHasSeats, advice };
83
+ },
84
+ trackedRoomCount() {
85
+ return rooms.size;
86
+ }
87
+ };
88
+ }
89
+ function tandemPendingToolDefinition() {
90
+ return {
91
+ name: TANDEM_PENDING_TOOL_NAME,
92
+ description: "Report THIS window's pending Tandem events \u2014 answered entirely from this proxy's in-process state (rooms this session holds seats in, cursors delivered on this proxy's stream, cursors drained through this proxy's tool calls). Precise per-window: it never reads shared machine-wide files. Call it when the turn-end bell says events may be pending, then drain each listed room with tandem_messages(tandem_id, since=since_cursor) and reply in the room. If it lists nothing, the bell was for another window \u2014 ignore it. After a proxy restart it correctly reports nothing until you rejoin (use your previous seat_name \u2014 the room rebinds your seat).",
93
+ inputSchema: {
94
+ type: "object",
95
+ properties: {
96
+ tandem_id: {
97
+ type: "string",
98
+ description: "Optional: report only this room."
99
+ }
100
+ }
101
+ }
102
+ };
103
+ }
104
+ function installPendingTool(runtime, registry) {
105
+ if (!PENDING_TOOL_RUNTIMES.has(runtime)) return null;
106
+ const state = createSessionPendingState();
107
+ registry.registerLocalTool(tandemPendingToolDefinition(), (args) => {
108
+ const filter = typeof args["tandem_id"] === "string" && args["tandem_id"] !== "" ? args["tandem_id"] : void 0;
109
+ return {
110
+ content: [{ type: "text", text: JSON.stringify(state.snapshot(filter)) }],
111
+ isError: false
112
+ };
113
+ });
114
+ return state;
115
+ }
116
+ function createRuntimeDrainHook(runtime, state) {
117
+ const writesLedger = LEDGER_DRAIN_RUNTIMES.has(runtime);
118
+ if (!writesLedger && state === null) return void 0;
119
+ return (tandemId, cursor) => {
120
+ if (writesLedger) recordDrained(tandemId, cursor);
121
+ state?.noteDrained(tandemId, cursor);
122
+ };
123
+ }
124
+ export {
125
+ LEDGER_DRAIN_RUNTIMES,
126
+ MAX_TRACKED_ROOMS,
127
+ PENDING_TOOL_RUNTIMES,
128
+ REJOIN_ADVICE,
129
+ TANDEM_PENDING_TOOL_NAME,
130
+ createRuntimeDrainHook,
131
+ createSessionPendingState,
132
+ installPendingTool,
133
+ tandemPendingToolDefinition
134
+ };
@@ -1,17 +1,24 @@
1
+ import {
2
+ CODEX_STATUS_HEARTBEAT_MS,
3
+ sweepDeadCodexStatusFiles,
4
+ writeCodexStatus
5
+ } from "./chunk-XLRF5ATG.js";
1
6
  import {
2
7
  createCachedOpenclawWakeResolver,
3
8
  postOpenclawWake
4
9
  } from "./chunk-NE2F5CKS.js";
5
10
  import "./chunk-PHXO5P25.js";
6
11
  import {
7
- codexPendingMarkerPath
8
- } from "./chunk-EBYUJM3H.js";
12
+ recordDelivered
13
+ } from "./chunk-IZN7IZPW.js";
9
14
  import {
10
15
  claudeCodeAdapter
11
- } from "./chunk-UPJV7GBE.js";
12
- import "./chunk-GNLCUJBK.js";
13
- import "./chunk-5DHIUN73.js";
14
- import "./chunk-SSW5AQSR.js";
16
+ } from "./chunk-TCWIXG5C.js";
17
+ import "./chunk-A4IOKD4Z.js";
18
+ import {
19
+ VERSION
20
+ } from "./chunk-5DHIUN73.js";
21
+ import "./chunk-FJUAMJHU.js";
15
22
  import "./chunk-PPTKGWFF.js";
16
23
 
17
24
  // src/delivery/lib/fanout.ts
@@ -42,16 +49,16 @@ async function deliverEvent(event, sinks) {
42
49
  } catch (err) {
43
50
  console.error("[event-stream] webhook enqueue failed:", err);
44
51
  }
45
- if (sinks.codexPendingMarker) {
52
+ if (sinks.pendingLedger) {
46
53
  try {
47
- const pending = sinks.codexPendingMarker(event);
54
+ const pending = sinks.pendingLedger(event);
48
55
  if (pending && typeof pending.then === "function") {
49
56
  pending.catch((err) => {
50
- console.error("[event-stream] codex pending-marker write failed:", err);
57
+ console.error("[event-stream] pending-ledger record failed:", err);
51
58
  });
52
59
  }
53
60
  } catch (err) {
54
- console.error("[event-stream] codex pending-marker write failed:", err);
61
+ console.error("[event-stream] pending-ledger record failed:", err);
55
62
  }
56
63
  }
57
64
  }
@@ -119,8 +126,8 @@ function createClaudeCodeDelivery() {
119
126
  const { resubscribeMemberships } = await import("./resubscribe-G5OGDZJD.js");
120
127
  const { resolveWebhookConfig } = await import("./webhook-config-O4WMQ532.js");
121
128
  const { createWebhookSink } = await import("./webhook-sink-N6AUTFL3.js");
122
- const { startEventStream } = await import("./event-stream-KRYWEYWO.js");
123
- const { createMcpServer } = await import("./server-ITPFQVTK.js");
129
+ const { startEventStream } = await import("./event-stream-WPN3EN7C.js");
130
+ const { createMcpServer } = await import("./server-DU3LFS32.js");
124
131
  const { deriveDiscoveryKey } = await import("./ancestry-A2F5KQ6A.js");
125
132
  const { resolveSharedSessionId } = await import("./cc-session-id-RURNIHHC.js");
126
133
  sweepStaleDiscovery();
@@ -282,6 +289,9 @@ function createClaudeCodeDelivery() {
282
289
  // claude-code, so it is always the channel-capable path.
283
290
  formatChannelNotification,
284
291
  ...webhookSink ? { webhookSink } : {}
292
+ // No pendingLedger sink: claude-code's event log is already
293
+ // room-qualified (tandem=<id> cursor=<n> per line) and its stop-hook
294
+ // /poll feed is per-session — the general ledger adds nothing here.
285
295
  });
286
296
  },
287
297
  async stop() {
@@ -302,7 +312,7 @@ function createClaudeCodeDelivery() {
302
312
  }
303
313
 
304
314
  // src/delivery/hermes.ts
305
- function createWebhookDelivery(name, codexPendingMarker) {
315
+ function createWebhookDelivery(name, pendingLedger) {
306
316
  let eventLog = null;
307
317
  let webhookSink = null;
308
318
  let streamHandle = null;
@@ -313,8 +323,8 @@ function createWebhookDelivery(name, codexPendingMarker) {
313
323
  const { resolveWebhookConfig } = await import("./webhook-config-O4WMQ532.js");
314
324
  const { createWebhookSink } = await import("./webhook-sink-N6AUTFL3.js");
315
325
  const { resubscribeMemberships } = await import("./resubscribe-G5OGDZJD.js");
316
- const { startEventStream } = await import("./event-stream-KRYWEYWO.js");
317
- const { createMcpServer } = await import("./server-ITPFQVTK.js");
326
+ const { startEventStream } = await import("./event-stream-WPN3EN7C.js");
327
+ const { createMcpServer } = await import("./server-DU3LFS32.js");
318
328
  sweepStaleEventLogs();
319
329
  eventLog = startEventLog({
320
330
  key: ctx.instanceKey,
@@ -384,8 +394,8 @@ function createWebhookDelivery(name, codexPendingMarker) {
384
394
  await deliverEvent(event, {
385
395
  ...eventLog ? { eventLog } : {},
386
396
  ...webhookSink ? { webhookSink } : {},
387
- // OPTIONAL codex-only marker sink (undefined ⇒ byte-identical hermes path).
388
- ...codexPendingMarker ? { codexPendingMarker } : {}
397
+ // OPTIONAL pending-ledger sink (undefined ⇒ byte-identical hermes path).
398
+ ...pendingLedger ? { pendingLedger } : {}
389
399
  });
390
400
  },
391
401
  async stop() {
@@ -402,39 +412,92 @@ function createWebhookDelivery(name, codexPendingMarker) {
402
412
  };
403
413
  }
404
414
 
405
- // src/delivery/codex-pending-marker.ts
406
- import fsp from "fs/promises";
407
- import path from "path";
408
- import crypto from "crypto";
409
- var MAX_MARKER_BODY_CHARS = 128;
410
- var ensuredDir = null;
411
- async function writeCodexPendingMarker(event) {
412
- if (!Number.isFinite(event.cursor) || event.cursor < 1) return;
413
- const markerPath = codexPendingMarkerPath();
414
- const dir = path.dirname(markerPath);
415
- const tmp = `${markerPath}.tmp-${process.pid}-${crypto.randomUUID()}`;
416
- try {
417
- if (ensuredDir !== dir) {
418
- await fsp.mkdir(dir, { recursive: true });
419
- ensuredDir = dir;
415
+ // src/delivery/codex.ts
416
+ var MAX_TANDEMS_SEEN = 32;
417
+ function createCodexDelivery(onDelivered) {
418
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
419
+ let deliveredEvents = 0;
420
+ let lastLedgerWriteAt = null;
421
+ let lastEventAt = null;
422
+ let subscribedTandemCount = -1;
423
+ const tandemsSeen = /* @__PURE__ */ new Set();
424
+ let heartbeat = null;
425
+ const ledgerSink = (event) => {
426
+ recordDelivered(event.tandem_id, event.cursor);
427
+ if (typeof event.tandem_id === "string" && event.tandem_id !== "" && Number.isFinite(event.cursor) && event.cursor >= 1) {
428
+ lastLedgerWriteAt = (/* @__PURE__ */ new Date()).toISOString();
420
429
  }
421
- const tandem = String(event.tandem_id ?? "").replace(/\s+/g, "");
422
- const cursorTok = String(Math.floor(event.cursor));
423
- const body = (tandem ? `${cursorTok} ${tandem}` : cursorTok).slice(0, MAX_MARKER_BODY_CHARS);
424
- await fsp.writeFile(tmp, body, { mode: 384 });
425
- await fsp.rename(tmp, markerPath);
426
- } catch {
427
- ensuredDir = null;
428
- try {
429
- await fsp.unlink(tmp);
430
- } catch {
430
+ if (onDelivered) {
431
+ try {
432
+ onDelivered(event);
433
+ } catch {
434
+ }
431
435
  }
432
- }
433
- }
434
-
435
- // src/delivery/codex.ts
436
- function createCodexDelivery() {
437
- return createWebhookDelivery("codex", writeCodexPendingMarker);
436
+ };
437
+ const inner = createWebhookDelivery("codex", ledgerSink);
438
+ const innerDeliver = inner.deliver.bind(inner);
439
+ const deliverWithStatus = async (event) => {
440
+ await innerDeliver(event);
441
+ deliveredEvents += 1;
442
+ lastEventAt = (/* @__PURE__ */ new Date()).toISOString();
443
+ const tid = typeof event.tandem_id === "string" ? event.tandem_id : "";
444
+ if (tid && tandemsSeen.size < MAX_TANDEMS_SEEN) tandemsSeen.add(tid);
445
+ refreshStatus();
446
+ };
447
+ inner.deliver = deliverWithStatus;
448
+ const streamStatus = () => {
449
+ const health = inner.health?.();
450
+ const stream = health?.detail?.stream;
451
+ const iso = (v) => typeof v === "number" && Number.isFinite(v) ? new Date(v).toISOString() : null;
452
+ return {
453
+ connected: health ? health.connected : null,
454
+ connected_since: iso(stream?.["connectedSince"]),
455
+ last_event_at: iso(stream?.["lastEventAt"]) ?? lastEventAt,
456
+ reconnects: typeof stream?.["reconnectCount"] === "number" ? stream["reconnectCount"] : null
457
+ };
458
+ };
459
+ const snapshot = () => ({
460
+ pid: process.pid,
461
+ proxy_version: VERSION,
462
+ started_at: startedAt,
463
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
464
+ stream: streamStatus(),
465
+ delivered_events: deliveredEvents,
466
+ last_marker_write_at: lastLedgerWriteAt,
467
+ subscribed_tandem_count: subscribedTandemCount,
468
+ tandems_seen: [...tandemsSeen]
469
+ });
470
+ const refreshStatus = () => {
471
+ void writeCodexStatus(snapshot()).catch(() => {
472
+ });
473
+ };
474
+ return {
475
+ name: inner.name,
476
+ async start(ctx) {
477
+ const started = await inner.start(ctx);
478
+ subscribedTandemCount = ctx.tandemMembershipCount;
479
+ void sweepDeadCodexStatusFiles().catch(() => {
480
+ });
481
+ refreshStatus();
482
+ heartbeat = setInterval(refreshStatus, CODEX_STATUS_HEARTBEAT_MS);
483
+ heartbeat.unref?.();
484
+ started.teardown.push(() => {
485
+ if (heartbeat) clearInterval(heartbeat);
486
+ heartbeat = null;
487
+ });
488
+ return started;
489
+ },
490
+ deliver: deliverWithStatus,
491
+ async stop() {
492
+ if (heartbeat) clearInterval(heartbeat);
493
+ heartbeat = null;
494
+ await inner.stop();
495
+ refreshStatus();
496
+ },
497
+ health() {
498
+ return inner.health?.() ?? { connected: null };
499
+ }
500
+ };
438
501
  }
439
502
 
440
503
  // src/delivery/openclaw.ts
@@ -454,8 +517,8 @@ function createOpenclawDelivery(deps = {}) {
454
517
  async start(ctx) {
455
518
  const { startEventLog, sweepStaleEventLogs } = await import("./event-log-2NBJEIEP.js");
456
519
  const { resubscribeMemberships } = await import("./resubscribe-G5OGDZJD.js");
457
- const { startEventStream } = await import("./event-stream-KRYWEYWO.js");
458
- const { createMcpServer } = await import("./server-ITPFQVTK.js");
520
+ const { startEventStream } = await import("./event-stream-WPN3EN7C.js");
521
+ const { createMcpServer } = await import("./server-DU3LFS32.js");
459
522
  sweepStaleEventLogs();
460
523
  eventLog = startEventLog({
461
524
  key: ctx.instanceKey,
@@ -549,7 +612,7 @@ function selectDelivery(runtime, opts) {
549
612
  return createClaudeCodeDelivery();
550
613
  }
551
614
  if (runtime === "codex") {
552
- return createCodexDelivery();
615
+ return createCodexDelivery(opts.onDelivered);
553
616
  }
554
617
  if (runtime === "openclaw") {
555
618
  return createOpenclawDelivery();
@@ -3,15 +3,17 @@ import {
3
3
  buildNonChannelInstructions,
4
4
  createMcpServer,
5
5
  executeToolCall,
6
+ extractDrainCursor,
6
7
  startMcpServer
7
- } from "./chunk-GNLCUJBK.js";
8
+ } from "./chunk-A4IOKD4Z.js";
8
9
  import "./chunk-5DHIUN73.js";
9
- import "./chunk-SSW5AQSR.js";
10
+ import "./chunk-FJUAMJHU.js";
10
11
  import "./chunk-PPTKGWFF.js";
11
12
  export {
12
13
  buildChannelInstructions,
13
14
  buildNonChannelInstructions,
14
15
  createMcpServer,
15
16
  executeToolCall,
17
+ extractDrainCursor,
16
18
  startMcpServer
17
19
  };
@@ -3,17 +3,17 @@ import {
3
3
  pairSlotFor,
4
4
  reconcileConnectPairSlot,
5
5
  runWizard
6
- } from "./chunk-UFHGZUST.js";
6
+ } from "./chunk-XPIW4N55.js";
7
+ import "./chunk-6XWTUDWW.js";
7
8
  import "./chunk-E6WMFMM2.js";
8
- import "./chunk-KPMD72FY.js";
9
- import "./chunk-QJFMU4QC.js";
9
+ import {
10
+ isWizardRuntime
11
+ } from "./chunk-77HWBSRH.js";
12
+ import "./chunk-TMCNB4JH.js";
10
13
  import "./chunk-D6JKFJ6A.js";
11
14
  import "./chunk-EIH2LNF4.js";
12
15
  import "./chunk-RDLF4NQC.js";
13
16
  import "./chunk-SQL56SEB.js";
14
- import {
15
- isWizardRuntime
16
- } from "./chunk-77HWBSRH.js";
17
17
  import "./chunk-V5VZPYMZ.js";
18
18
  import {
19
19
  runPair
@@ -26,7 +26,7 @@ import "./chunk-MIEI4PLB.js";
26
26
  import "./chunk-6G6YYST6.js";
27
27
  import "./chunk-U5HHHRXA.js";
28
28
  import "./chunk-5DHIUN73.js";
29
- import "./chunk-SSW5AQSR.js";
29
+ import "./chunk-FJUAMJHU.js";
30
30
 
31
31
  // src/wizard/setup-handler.ts
32
32
  var SETUP_SUPPORTED_RUNTIMES = ["claude-code", "codex"];
@@ -1,10 +1,10 @@
1
+ import "./chunk-XLKGPGZT.js";
1
2
  import {
2
3
  resolveHookDiscovery
3
4
  } from "./chunk-WBXU27BF.js";
4
5
  import {
5
6
  readHookStdin
6
7
  } from "./chunk-LSUB6QMP.js";
7
- import "./chunk-XLKGPGZT.js";
8
8
  import {
9
9
  controlTokenAuthHeaders
10
10
  } from "./chunk-GKXHKR3B.js";
@@ -19,7 +19,7 @@ import "./chunk-67F67AQ6.js";
19
19
  import "./chunk-U5HHHRXA.js";
20
20
  import {
21
21
  buildMonitorNudge
22
- } from "./chunk-SSW5AQSR.js";
22
+ } from "./chunk-FJUAMJHU.js";
23
23
  import "./chunk-XJEBJIQE.js";
24
24
  import "./chunk-KNEJTD6G.js";
25
25
 
@@ -6,8 +6,10 @@ import {
6
6
  } from "./chunk-TNZITJAB.js";
7
7
  import "./chunk-67F67AQ6.js";
8
8
  import {
9
+ LOG_HEARTBEAT_INTERVAL_MS,
10
+ UNARMED_FALLBACK_MS,
9
11
  createAdaptiveWatchdog
10
- } from "./chunk-FSKGQ6GT.js";
12
+ } from "./chunk-HI42GBQ3.js";
11
13
  import "./chunk-Z5LPNJQ6.js";
12
14
  import "./chunk-MIEI4PLB.js";
13
15
  import "./chunk-U5HHHRXA.js";
@@ -19,17 +21,87 @@ var HEARTBEAT_FIELDS_RE = /(^|\s)status=heartbeat(\s|$)/;
19
21
  var ROTATED_FIELDS_RE = /(^|\s)status=rotated(\s|$)/;
20
22
  var ROTATED_DROPPED_RE = /(^|\s)dropped=(\d+)(\s|$)/;
21
23
  var MESSAGE_CURSOR_RE = /(^|\s)cursor=(\S+)(\s|$)/;
22
- var HEARTBEAT_STALE_FLOOR_MS = envInt("KOJEE_TAIL_STALE_MS", 12e4);
24
+ var TAIL_ABS_FLOOR_MS = 5 * 6e4;
25
+ var TAIL_FLOOR_SLACK_MS = 3e4;
26
+ var TAIL_STALE_FLOOR_MS = envInt(
27
+ "KOJEE_TAIL_STALE_MS",
28
+ Math.max(TAIL_ABS_FLOOR_MS, 2 * LOG_HEARTBEAT_INTERVAL_MS + TAIL_FLOOR_SLACK_MS)
29
+ );
23
30
  var STALE_CHECK_INTERVAL_MS = envInt("KOJEE_TAIL_CHECK_MS", 5e3);
31
+ var TAIL_REWARN_AFTER_MS = 30 * 6e4;
32
+ var TAIL_HEALTHY_RESET_MS = 30 * 6e4;
33
+ var TAIL_BACKOFF_CAP_CYCLES = 3;
24
34
  function envInt(name, fallback) {
25
35
  const v = Number.parseInt(process.env[name] ?? "", 10);
26
36
  return Number.isFinite(v) && v > 0 ? v : fallback;
27
37
  }
38
+ function createStallDetector(startNow, options = {}) {
39
+ const heartbeatIntervalMs = options.heartbeatIntervalMs ?? LOG_HEARTBEAT_INTERVAL_MS;
40
+ const floorMs = options.floorMs ?? Math.max(TAIL_ABS_FLOOR_MS, 2 * heartbeatIntervalMs + TAIL_FLOOR_SLACK_MS);
41
+ const rewarnAfterMs = options.rewarnAfterMs ?? TAIL_REWARN_AFTER_MS;
42
+ const healthyResetMs = options.healthyResetMs ?? TAIL_HEALTHY_RESET_MS;
43
+ const backoffCapCycles = options.backoffCapCycles ?? TAIL_BACKOFF_CAP_CYCLES;
44
+ const watchdog = createAdaptiveWatchdog({
45
+ floorMs,
46
+ multiplier: options.multiplier,
47
+ unarmedFallbackMs: options.unarmedFallbackMs
48
+ });
49
+ let lastLifeAt = startNow;
50
+ let stale = false;
51
+ let warnedAt = 0;
52
+ let rewarned = false;
53
+ let backoffCycles = 0;
54
+ let lastRecoveredAt = null;
55
+ let outageSpanned = false;
56
+ return {
57
+ onLife(now) {
58
+ const silentMs = now - lastLifeAt;
59
+ lastLifeAt = now;
60
+ if (stale) {
61
+ stale = false;
62
+ rewarned = false;
63
+ backoffCycles = Math.min(backoffCycles + 1, backoffCapCycles);
64
+ lastRecoveredAt = now;
65
+ return { kind: "recovered", silentMs };
66
+ }
67
+ if (backoffCycles > 0 && lastRecoveredAt !== null && now - lastRecoveredAt >= healthyResetMs) {
68
+ backoffCycles = 0;
69
+ }
70
+ return null;
71
+ },
72
+ onHeartbeat(now) {
73
+ if (outageSpanned) {
74
+ watchdog.resetCadenceAnchor();
75
+ outageSpanned = false;
76
+ }
77
+ watchdog.onHeartbeat(now);
78
+ },
79
+ check(now) {
80
+ if (stale) {
81
+ if (!rewarned && now - warnedAt >= rewarnAfterMs) {
82
+ rewarned = true;
83
+ return { kind: "rewarn", silentMs: now - lastLifeAt };
84
+ }
85
+ return null;
86
+ }
87
+ const base = watchdog.armedThresholdMs() ?? (options.unarmedFallbackMs ?? UNARMED_FALLBACK_MS);
88
+ const thresholdMs = Math.min(
89
+ base * 2 ** backoffCycles,
90
+ Math.max(floorMs, rewarnAfterMs)
91
+ );
92
+ const silentMs = now - lastLifeAt;
93
+ if (silentMs < thresholdMs) return null;
94
+ stale = true;
95
+ warnedAt = now;
96
+ rewarned = false;
97
+ outageSpanned = true;
98
+ return { kind: "warn", silentMs, thresholdMs };
99
+ }
100
+ };
101
+ }
28
102
  async function runTail(messagesPath) {
29
103
  const statusPath = statusLogPath(messagesPath);
30
- let lastLifeAt = Date.now();
31
- const watchdog = createAdaptiveWatchdog({ floorMs: HEARTBEAT_STALE_FLOOR_MS });
32
- let staleAnnounced = false;
104
+ const detector = createStallDetector(Date.now(), { floorMs: TAIL_STALE_FLOOR_MS });
33
105
  const heartbeatPath = monitorHeartbeatPath(messagesPath);
34
106
  const touchHeartbeat = () => {
35
107
  const now = /* @__PURE__ */ new Date();
@@ -44,10 +116,10 @@ async function runTail(messagesPath) {
44
116
  };
45
117
  let lastCursor = null;
46
118
  function onMessageLine(line) {
47
- lastLifeAt = Date.now();
48
- if (staleAnnounced) {
49
- staleAnnounced = false;
50
- process.stdout.write("[kojee] stream recovered \u2014 events flowing again.\n");
119
+ if (detector.onLife(Date.now())?.kind === "recovered") {
120
+ process.stdout.write(
121
+ "[kojee] stream recovered \u2014 events flowing again (recovery is automatic; no action needed).\n"
122
+ );
51
123
  }
52
124
  const cm = MESSAGE_CURSOR_RE.exec(line);
53
125
  if (cm && cm[2]) lastCursor = cm[2];
@@ -59,12 +131,12 @@ async function runTail(messagesPath) {
59
131
  const m = STATUS_LINE_RE.exec(line);
60
132
  const fields = m ? m[1] ?? "" : "";
61
133
  if (HEARTBEAT_FIELDS_RE.test(fields)) {
62
- watchdog.onHeartbeat(now);
134
+ detector.onHeartbeat(now);
63
135
  }
64
- lastLifeAt = now;
65
- if (staleAnnounced) {
66
- staleAnnounced = false;
67
- process.stdout.write("[kojee] stream recovered \u2014 heartbeats resumed.\n");
136
+ if (detector.onLife(now)?.kind === "recovered") {
137
+ process.stdout.write(
138
+ "[kojee] stream recovered \u2014 heartbeats resumed (the stall was transient, likely idle-period timing or brief host load; no action needed).\n"
139
+ );
68
140
  }
69
141
  if (statusSeeded && ROTATED_FIELDS_RE.test(fields)) {
70
142
  const dm = ROTATED_DROPPED_RE.exec(fields);
@@ -88,13 +160,20 @@ async function runTail(messagesPath) {
88
160
  ]).catch(() => {
89
161
  });
90
162
  const staleTimer = setInterval(() => {
91
- if (staleAnnounced) return;
92
163
  const now = Date.now();
93
- if (!watchdog.shouldAbort(lastLifeAt, now)) return;
94
- staleAnnounced = true;
95
- const silentMs = now - lastLifeAt;
164
+ const signal = detector.check(now);
165
+ if (!signal) return;
166
+ const silentS = Math.round(signal.silentMs / 1e3);
167
+ if (signal.kind === "rewarn") {
168
+ process.stdout.write(
169
+ `[kojee] WARNING: still no events or heartbeat after ${silentS}s \u2014 the earlier stall never recovered, so the stream may be dead; run \`npx kojee-mcp doctor\`.
170
+ `
171
+ );
172
+ return;
173
+ }
174
+ const thresholdS = Math.round((signal.thresholdMs ?? 0) / 1e3);
96
175
  process.stdout.write(
97
- `[kojee] WARNING: no events or heartbeat for ${Math.round(silentMs / 1e3)}s (learned cadence) \u2014 stream may be dead; run \`npx kojee-mcp doctor\`.
176
+ `[kojee] WARNING: no events or heartbeat for ${silentS}s (threshold ${thresholdS}s) \u2014 stream may be dead, but this is often benign (idle stream, or brief host load delaying heartbeats) and recovery is automatic; if it persists, run \`npx kojee-mcp doctor\`.
98
177
  `
99
178
  );
100
179
  }, STALE_CHECK_INTERVAL_MS);
@@ -198,6 +277,7 @@ function sleep(ms) {
198
277
  return new Promise((r) => setTimeout(r, ms));
199
278
  }
200
279
  export {
280
+ createStallDetector,
201
281
  makeFollower,
202
282
  runTail
203
283
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kojee-mcp",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
@@ -23,9 +23,9 @@ not your every sentence. When the hats tension, the principal wins.
23
23
  - **Sync before you speak.** Catch up to the latest cursor; a gap means you missed messages — fetch them. Never answer from a stale view.
24
24
  - **Answer in the channel you were approached in.** Approached in the **room** (the Tandem)? Answer in the room. Approached in your **session** (your principal's direct channel — including gate / review surfaces)? Answer in the session. It does **not** matter whether it's a 1:1 or a group chat — match the inbound channel; never migrate a conversation to a channel it didn't start in. Use `tandem_send` only for what originates in or belongs to the room, not to echo your session replies.
25
25
  - **Close every loop.** Taking an item, done (with evidence), or blocked (name the blocker + who clears it) — say so. Silence is a bug on a team.
26
- - **Plan your wake path per task, re-plan as it changes.** Heads-down → mentions + a heartbeat floor; waiting on one reply → listen/filter to that seat; co-working → wake on all; standby → mentions + hourly heartbeat. Move your filter deliberately, and tell the room your posture.
26
+ - **Plan your wake path per task, re-plan as it changes.** Heads-down → mentions + a heartbeat floor; waiting on one reply → listen/filter to that seat; co-working → wake on all; standby → mentions + hourly heartbeat. Move your filter deliberately, and tell the room your posture when it changes — a plain re-join/reconnect with the same posture needs no check-in message.
27
27
  - **Don't assume others use the room well — engineer around it.** Poll for gaps, confirm receipt when it matters, resend a mention that didn't land, never block forever on a peer who may be dark.
28
- - **Signal cheap, wake rarely.** Acknowledge with a non-waking react/ack; reserve a message for what *changes someone's next move*.
28
+ - **Signal cheap, wake rarely.** Acknowledge with a reaction or ack (non-waking); reserve a message for what *changes someone's next move*. Cheap signals are reactions/acks — never `kind=status` messages (status is lifecycle-only, system-generated for joined/left events; never send it yourself).
29
29
  - **Take your lane.** Don't duplicate, collide, or redo a teammate's work; settle unclear ownership in one message first.
30
30
  - **Be a reliable, considerate presence.** Recover yourself from a dropped session — don't make others restart you — and pace your load on the shared account.
31
31
 
@@ -1,14 +0,0 @@
1
- // src/hooks/codex-pending-path.ts
2
- import os from "os";
3
- import path from "path";
4
- function codexPendingMarkerPath() {
5
- return path.join(os.homedir(), ".kojee", "codex-pending");
6
- }
7
- function codexPendingAckPath() {
8
- return codexPendingMarkerPath() + ".ack";
9
- }
10
-
11
- export {
12
- codexPendingMarkerPath,
13
- codexPendingAckPath
14
- };