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 +1 -1
- package/npm-scripts/register-hook.js +30 -15
- package/package.json +1 -1
- package/src/core/agy_runner.py +3 -10
- package/src/core/session_manager.py +10 -0
- package/src/handlers/thread_reply.py +19 -2
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 =
|
|
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:
|
|
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
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
package/src/core/agy_runner.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
225
|
+
await _run_tracked(
|
|
226
|
+
incoming.conversation_id,
|
|
227
|
+
handle_existing_session(bot, incoming, session, conv_id, agy_content, image_paths),
|
|
228
|
+
)
|