linkgravity 1.8.1 → 1.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/hooks/hook.js CHANGED
@@ -9,7 +9,7 @@ const LGY_CONFIG_FILE = path.join(os.homedir(), '.gemini', 'linkgravity', 'lgy.j
9
9
  // Not "localhost" - the server binds 127.0.0.1 and node resolves localhost to ::1 first.
10
10
  const APPROVE_HOST = '127.0.0.1';
11
11
  const APPROVE_PORT = 18080;
12
- const TIMEOUT_MS = 3600 * 1000;
12
+ const TIMEOUT_MS = 86400 * 1000;
13
13
 
14
14
  function emit(payload) {
15
15
  // Exits explicitly: the keep-alive socket and its hour-long timer stay open after the
@@ -20,7 +20,7 @@ const HOOK_REGISTRATIONS = [
20
20
  eventType: 'PreToolUse',
21
21
  name: 'discord-approval',
22
22
  fileName: 'hook.js',
23
- defaultTimeout: 3600,
23
+ defaultTimeout: 86400,
24
24
  wrapInMatcher: true,
25
25
  },
26
26
  {
@@ -130,7 +130,7 @@ function removeRetiredHooks(config) {
130
130
  return removedAny;
131
131
  }
132
132
 
133
- function registerHook({ allowFirstTimeCreate = true, quiet = false } = {}) {
133
+ function registerHook({ allowFirstTimeCreate = true, quiet = false, resetTimeouts = false } = {}) {
134
134
  const config = loadHooksConfig();
135
135
  config.hooks = config.hooks || {};
136
136
 
@@ -180,19 +180,34 @@ function registerHook({ allowFirstTimeCreate = true, quiet = false } = {}) {
180
180
  hookEntry.command = command;
181
181
  console.log(`🔗 Registered agy ${reg.eventType} hook '${reg.name}' -> ${scriptPath}`);
182
182
  wroteChange = true;
183
- } else if (hookEntry.command !== command) {
184
- backupBeforeFirstChange();
185
- console.log(
186
- `🔗 Fixing agy ${reg.eventType} hook '${reg.name}' in ${hooksJsonPath}` +
187
- (backedUp ? ` (previous version backed up to ${hooksJsonPath}.bak)` : '') +
188
- `:\n was: ${hookEntry.command}\n now: ${command}`,
189
- );
190
- hookEntry.command = command;
191
- wroteChange = true;
192
- } else if (!quiet) {
193
- console.log(
194
- `🔗 agy ${reg.eventType} hook '${reg.name}' already up to date -> ${scriptPath}`,
195
- );
183
+ } else {
184
+ let changedThisEntry = false;
185
+ if (hookEntry.command !== command) {
186
+ backupBeforeFirstChange();
187
+ console.log(
188
+ `🔗 Fixing agy ${reg.eventType} hook '${reg.name}' in ${hooksJsonPath}` +
189
+ (backedUp ? ` (previous version backed up to ${hooksJsonPath}.bak)` : '') +
190
+ `:\n was: ${hookEntry.command}\n now: ${command}`,
191
+ );
192
+ hookEntry.command = command;
193
+ changedThisEntry = true;
194
+ }
195
+ // Only setup resets timeout - a passive `update` must not clobber a user-customized value.
196
+ if (resetTimeouts && hookEntry.timeout !== reg.defaultTimeout) {
197
+ backupBeforeFirstChange();
198
+ console.log(
199
+ `🔗 Resetting agy ${reg.eventType} hook '${reg.name}' timeout: ${hookEntry.timeout}s -> ${reg.defaultTimeout}s`,
200
+ );
201
+ hookEntry.timeout = reg.defaultTimeout;
202
+ changedThisEntry = true;
203
+ }
204
+ if (changedThisEntry) {
205
+ wroteChange = true;
206
+ } else if (!quiet) {
207
+ console.log(
208
+ `🔗 agy ${reg.eventType} hook '${reg.name}' already up to date -> ${scriptPath}`,
209
+ );
210
+ }
196
211
  }
197
212
  }
198
213
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linkgravity",
3
- "version": "1.8.1",
3
+ "version": "1.9.1",
4
4
  "description": "Discord/Telegram bot bridge for the Antigravity (agy) CLI, with voice interaction support",
5
5
  "scripts": {
6
6
  "start": "node npm-scripts/run-dev.js",
@@ -208,13 +208,6 @@ async def run_agy(
208
208
  stdout_chunks.append(clean)
209
209
  if stream_queue is not None:
210
210
  await stream_queue.put((clean, False))
211
- if (
212
- "(Calls tool:" in clean
213
- or "Tool Output:" in clean
214
- or "Tool Execute:" in clean
215
- or clean.startswith("● ")
216
- ):
217
- await stream_queue.put(("__SPLIT__", True))
218
211
 
219
212
  async def read_stderr():
220
213
  while True:
@@ -229,7 +222,7 @@ async def run_agy(
229
222
  gather_task = asyncio.create_task(_gather_pipes())
230
223
  wait_task = asyncio.create_task(proc.wait())
231
224
 
232
- # Slices let the timeout pause indefinitely while any approval is pending (hooks/hook.js waits up to 24h).
225
+ # Slices let the timeout pause while an approval is pending (hook.js waits up to 24h).
233
226
  from config import session_manager as _sm
234
227
 
235
228
  poll_slice = 5.0
@@ -351,7 +344,7 @@ async def agy_new_conversation(
351
344
  args = ["--dangerously-skip-permissions", "--print", content, "--print-timeout", "24h"]
352
345
  if model:
353
346
  args.extend(["--model", clean_model_name(model)])
354
- result_text = await run_agy(*args, stream_queue=stream_queue, thread_id=thread_id, cwd=cwd)
347
+ result_text = await run_agy(*args, timeout=86400, stream_queue=stream_queue, thread_id=thread_id, cwd=cwd)
355
348
  conv_id = await _get_latest_conversation_id()
356
349
  return result_text, conv_id
357
350
 
@@ -367,7 +360,7 @@ async def agy_send_message(
367
360
  args = ["--dangerously-skip-permissions", "--print", content, "--conversation", conv_id, "--print-timeout", "24h"]
368
361
  if model:
369
362
  args.extend(["--model", clean_model_name(model)])
370
- return await run_agy(*args, stream_queue=stream_queue, thread_id=thread_id, cwd=cwd)
363
+ return await run_agy(*args, timeout=86400, stream_queue=stream_queue, thread_id=thread_id, cwd=cwd)
371
364
 
372
365
 
373
366
  def get_current_model() -> str:
@@ -14,6 +14,7 @@ class SessionManager:
14
14
  self.sessions: dict[str, dict] = self._load_sessions()
15
15
  self.active_queues: dict[str, asyncio.Queue] = {}
16
16
  self.active_tts_tasks: dict[str, asyncio.Task] = {}
17
+ self.active_handler_tasks: dict[str, asyncio.Task] = {}
17
18
  self.pending_approvals: dict[str, asyncio.Future] = {}
18
19
  self.pending_approval_types: dict[str, str] = {}
19
20
  self.pending_approval_messages: dict[str, Any] = {}
@@ -121,6 +122,15 @@ class SessionManager:
121
122
  def remove_tts_task(self, thread_id: str):
122
123
  self.active_tts_tasks.pop(str(thread_id), None)
123
124
 
125
+ def get_handler_task(self, thread_id: str) -> asyncio.Task | None:
126
+ return self.active_handler_tasks.get(str(thread_id))
127
+
128
+ def set_handler_task(self, thread_id: str, task: asyncio.Task):
129
+ self.active_handler_tasks[str(thread_id)] = task
130
+
131
+ def remove_handler_task(self, thread_id: str):
132
+ self.active_handler_tasks.pop(str(thread_id), None)
133
+
124
134
  def set_pending_approval(
125
135
  self,
126
136
  approval_key: str,
@@ -133,6 +133,17 @@ async def handle_existing_session(
133
133
  cleanup_images(image_paths)
134
134
 
135
135
 
136
+ async def _run_tracked(thread_id: str, coro) -> None:
137
+ # Registered so /stop can cancel the whole turn, not just the agy process.
138
+ task = asyncio.current_task()
139
+ session_manager.set_handler_task(thread_id, task)
140
+ try:
141
+ await coro
142
+ finally:
143
+ if session_manager.get_handler_task(thread_id) is task:
144
+ session_manager.remove_handler_task(thread_id)
145
+
146
+
136
147
  async def handle_thread_reply(bot, incoming: IncomingMessage):
137
148
  session = session_manager.get_session(incoming.conversation_id)
138
149
  if not session:
@@ -202,10 +213,16 @@ async def handle_thread_reply(bot, incoming: IncomingMessage):
202
213
 
203
214
  if not conv_id:
204
215
  if session.get("status") == "pending":
205
- await handle_pending_session(bot, incoming, session, agy_content, content, image_paths)
216
+ await _run_tracked(
217
+ incoming.conversation_id,
218
+ handle_pending_session(bot, incoming, session, agy_content, content, image_paths),
219
+ )
206
220
  return
207
221
  else:
208
222
  await adapter.send_message(thread, "⚠️ Session ID not found. Start a new session with `/new`.")
209
223
  return
210
224
 
211
- await handle_existing_session(bot, incoming, session, conv_id, agy_content, image_paths)
225
+ await _run_tracked(
226
+ incoming.conversation_id,
227
+ handle_existing_session(bot, incoming, session, conv_id, agy_content, image_paths),
228
+ )