relay-companion 0.1.84 → 0.1.85

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/overlay/main.cjs CHANGED
@@ -2110,17 +2110,27 @@ async function openPacketInCurrent(packetId) {
2110
2110
  },
2111
2111
  });
2112
2112
  };
2113
- // WAKE TIER (live-proven): when exactly one relay-channel session is alive,
2114
- // push the instruction as a channel event — the session takes a REAL turn on
2115
- // it even when idle, which hook staging can never cause. One instance only:
2116
- // queue events are claimed first-rename-wins, so with several live channel
2117
- // sessions the event could wake a chat the user is not looking at; the
2118
- // session-targeted hook path stays correct for that world. If the claimed
2119
- // event somehow never gets picked up (server died between heartbeats), the
2120
- // watcher falls through to hook staging — the click never dead-ends.
2113
+ // WAKE TIER (live-proven): when the resolved CURRENT chat is itself a
2114
+ // relay-channel session, push the instruction as a channel event — the
2115
+ // session takes a REAL turn on it even when idle, which hook staging can
2116
+ // never cause. The join is by working directory: the channel instance's
2117
+ // recorded cwd must uniquely match the target session's rendezvous cwd, so
2118
+ // a click aimed at a desktop chat NEVER wakes some other terminal session.
2119
+ // If the claimed event somehow never gets picked up (server died between
2120
+ // heartbeats), the watcher falls back to hook staging — never a dead end.
2121
2121
  const tryChannelWake = () =>
2122
2122
  loadChannelDeps().then((channel) => {
2123
- if (channel.countLiveChannelInstances(RELAY_HOME) !== 1) return false;
2123
+ let targetCwd = "";
2124
+ try {
2125
+ const rendezvousPath = path.join(
2126
+ claudeInject.rendezvousDir(RELAY_HOME),
2127
+ `${claudeInject.safeSessionKey(target.sessionId)}.json`,
2128
+ );
2129
+ targetCwd = String(JSON.parse(fs.readFileSync(rendezvousPath, "utf8")).cwd || "");
2130
+ } catch {
2131
+ return false; // no rendezvous -> no join evidence -> hook path
2132
+ }
2133
+ if (!channel.liveChannelInstanceForCwd(RELAY_HOME, targetCwd)) return false;
2124
2134
  const instruction = claudeInject.buildInjectionInstruction({
2125
2135
  relayId: packetId,
2126
2136
  senderName: row.senderName || "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relay-companion",
3
- "version": "0.1.84",
3
+ "version": "0.1.85",
4
4
  "description": "Relay companion for ordinary messages, with dormant coordination features available only by explicit opt-in.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -83,6 +83,32 @@ export function channelInstanceAlive(homeDir = storeDir(), opts = {}) {
83
83
  return countLiveChannelInstances(homeDir, opts) > 0;
84
84
  }
85
85
 
86
+ // The single live channel instance matching a session's working directory, or
87
+ // null. "Current chat" means the chat the user is looking at: the wake tier
88
+ // must fire only when the channel session IS the resolved target, and the only
89
+ // join available today is cwd (channel presence records its process cwd; hook
90
+ // rendezvous records the session's). One instance per cwd; ambiguity -> null.
91
+ export function liveChannelInstanceForCwd(homeDir = storeDir(), cwd, { maxAgeMs = PRESENCE_MS * 3, nowMs = Date.now() } = {}) {
92
+ const wanted = String(cwd || "").replace(/\/+$/, "");
93
+ if (!wanted) return null;
94
+ let entries = [];
95
+ try {
96
+ entries = fs.readdirSync(channelPresenceDir(homeDir));
97
+ } catch {
98
+ return null;
99
+ }
100
+ const matches = [];
101
+ for (const name of entries) {
102
+ if (!name.endsWith(".json")) continue;
103
+ try {
104
+ const row = JSON.parse(fs.readFileSync(path.join(channelPresenceDir(homeDir), name), "utf8"));
105
+ if (nowMs - (Number(row.heartbeatAt) || 0) > maxAgeMs) continue;
106
+ if (String(row.cwd || "").replace(/\/+$/, "") === wanted) matches.push(row);
107
+ } catch {}
108
+ }
109
+ return matches.length === 1 ? matches[0] : null;
110
+ }
111
+
86
112
  function consumeEventFile(file) {
87
113
  const claimed = `${file}.${process.pid}.claimed`;
88
114
  try {