linkgravity 1.5.10 → 1.5.12
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/bin/cli.js +18 -9
- package/package.json +1 -1
- package/src/core/agy_runner.py +6 -2
- package/src/handlers/thread_reply.py +9 -6
package/bin/cli.js
CHANGED
|
@@ -32,17 +32,27 @@ function success(msg) {
|
|
|
32
32
|
console.log(`${color.green}✔${color.reset} ${msg}`);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// Resolved rather than run through npx: pm2 is a direct dependency, and npx wraps it in
|
|
36
|
+
// "npm exec" + "sh -c", which leaves the real process orphaned when we try to kill it.
|
|
37
|
+
const PM2_BIN = require.resolve('pm2/bin/pm2');
|
|
38
|
+
|
|
35
39
|
function info(msg) {
|
|
36
40
|
console.log(`\n${color.cyan}▶${color.reset} ${msg}`);
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
function runPm2(args, silent = true) {
|
|
40
44
|
const stdioOpt = silent ? 'pipe' : 'inherit';
|
|
41
|
-
const result = spawnSync(
|
|
45
|
+
const result = spawnSync(process.execPath, [PM2_BIN, ...args], {
|
|
42
46
|
stdio: stdioOpt,
|
|
43
47
|
cwd: path.join(__dirname, '..'),
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
env: {
|
|
49
|
+
...process.env,
|
|
50
|
+
// pm2 gives Python a pipe not a TTY, so it block-buffers stdout and can sit on log lines indefinitely - force line buffering.
|
|
51
|
+
PYTHONUNBUFFERED: '1',
|
|
52
|
+
// Version managers (fnm, nvm) put node on PATH from a shell hook the daemon never runs,
|
|
53
|
+
// so the bot's own `node` lookup for voice-service would fail without this.
|
|
54
|
+
PATH: `${path.dirname(process.execPath)}${path.delimiter}${process.env.PATH || ''}`,
|
|
55
|
+
},
|
|
46
56
|
});
|
|
47
57
|
|
|
48
58
|
if (result.error) {
|
|
@@ -119,7 +129,7 @@ function colorizeLevel(line) {
|
|
|
119
129
|
}
|
|
120
130
|
|
|
121
131
|
function runPm2LogsStream(args, printLine) {
|
|
122
|
-
const cp = spawn(
|
|
132
|
+
const cp = spawn(process.execPath, [PM2_BIN, ...args], { cwd: path.join(__dirname, '..') });
|
|
123
133
|
|
|
124
134
|
const isNoise = (line) =>
|
|
125
135
|
line.trim().length === 0 ||
|
|
@@ -160,6 +170,8 @@ function runPm2LogsStream(args, printLine) {
|
|
|
160
170
|
stdoutHandler.flush();
|
|
161
171
|
stderrHandler.flush();
|
|
162
172
|
});
|
|
173
|
+
// Ctrl-C reaches pm2 too (same process group), but an unhandled parent exit would leave it tailing.
|
|
174
|
+
process.on('exit', () => cp.kill());
|
|
163
175
|
}
|
|
164
176
|
|
|
165
177
|
function runPm2LogsClean(args, showStamps = false) {
|
|
@@ -175,10 +187,7 @@ function verifyStartup() {
|
|
|
175
187
|
`${color.cyan}▶${color.reset} Verifying startup status (waiting for bot to come online)...`,
|
|
176
188
|
);
|
|
177
189
|
|
|
178
|
-
|
|
179
|
-
// reaps only the wrapper and leaves the real pm2 logs process orphaned onto init.
|
|
180
|
-
const pm2Bin = require.resolve('pm2/bin/pm2');
|
|
181
|
-
let cp = spawn(process.execPath, [pm2Bin, 'logs', LGY_PM2_NAME, '--raw', '--lines', '0'], {
|
|
190
|
+
let cp = spawn(process.execPath, [PM2_BIN, 'logs', LGY_PM2_NAME, '--raw', '--lines', '0'], {
|
|
182
191
|
cwd: path.join(__dirname, '..'),
|
|
183
192
|
});
|
|
184
193
|
|
|
@@ -282,7 +291,7 @@ function findAgyBin() {
|
|
|
282
291
|
}
|
|
283
292
|
|
|
284
293
|
function getPm2Proc() {
|
|
285
|
-
const jlist = spawnSync(
|
|
294
|
+
const jlist = spawnSync(process.execPath, [PM2_BIN, 'jlist'], { stdio: 'pipe' });
|
|
286
295
|
if (jlist.status !== 0) return null;
|
|
287
296
|
|
|
288
297
|
// pm2 can print noise before the real JSON (version banners, ANSI escapes, daemon-spawn logs) that
|
package/package.json
CHANGED
package/src/core/agy_runner.py
CHANGED
|
@@ -13,6 +13,10 @@ _intentionally_stopped = set()
|
|
|
13
13
|
|
|
14
14
|
_STDOUT_BUFFER_SIZE = 65536
|
|
15
15
|
|
|
16
|
+
# Returned as ordinary output, not raised - callers that need to distinguish failure from a
|
|
17
|
+
# real answer have to compare against this.
|
|
18
|
+
TIMEOUT_MESSAGE = "🛑 AI Task timed out."
|
|
19
|
+
|
|
16
20
|
|
|
17
21
|
@functools.lru_cache(maxsize=1)
|
|
18
22
|
def _find_preload_lib() -> tuple[str, str] | None:
|
|
@@ -315,7 +319,7 @@ async def run_agy(
|
|
|
315
319
|
logger.warning("[AGY RETRY] Global timeout. Retrying...")
|
|
316
320
|
await asyncio.sleep(2.0)
|
|
317
321
|
continue
|
|
318
|
-
msg =
|
|
322
|
+
msg = TIMEOUT_MESSAGE
|
|
319
323
|
if stream_queue is not None:
|
|
320
324
|
await stream_queue.put(("\n\n" + msg, True))
|
|
321
325
|
return msg
|
|
@@ -388,7 +392,7 @@ async def generate_thread_title(user_input: str, response: str) -> str:
|
|
|
388
392
|
)
|
|
389
393
|
title = await run_agy("--print", prompt, timeout=30)
|
|
390
394
|
title = title.strip().strip('"').strip("'")
|
|
391
|
-
if not title or len(title) > 80:
|
|
395
|
+
if not title or len(title) > 80 or title == TIMEOUT_MESSAGE:
|
|
392
396
|
return fallback
|
|
393
397
|
return title
|
|
394
398
|
except Exception as e:
|
|
@@ -73,13 +73,9 @@ async def handle_pending_session(
|
|
|
73
73
|
await queue.put(("__END__", True))
|
|
74
74
|
await stream_task
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
if adapter.should_auto_title(thread):
|
|
78
|
-
new_title = await generate_thread_title(content, response_text)
|
|
79
|
-
await adapter.rename_conversation(thread, new_title)
|
|
80
|
-
await update_agy_conversation_title(new_conv_id, new_title)
|
|
76
|
+
needs_title = adapter.should_auto_title(thread)
|
|
81
77
|
|
|
82
|
-
response_text = await render_thought_process(new_conv_id, ctx,
|
|
78
|
+
response_text = await render_thought_process(new_conv_id, ctx, result_text, thread)
|
|
83
79
|
|
|
84
80
|
session["conversation_id"] = new_conv_id
|
|
85
81
|
session["created_at"] = datetime.now().isoformat()
|
|
@@ -87,6 +83,13 @@ async def handle_pending_session(
|
|
|
87
83
|
session_manager.save_sessions()
|
|
88
84
|
|
|
89
85
|
await send_agy_response(thread, response_text, session, ctx, start_time, new_conv_id)
|
|
86
|
+
|
|
87
|
+
# Outside the typing indicator: the answer is already on screen, and leaving "typing"
|
|
88
|
+
# up during the extra title call reads as if more output is still coming.
|
|
89
|
+
if needs_title:
|
|
90
|
+
new_title = await generate_thread_title(content, result_text)
|
|
91
|
+
await adapter.rename_conversation(thread, new_title)
|
|
92
|
+
await update_agy_conversation_title(new_conv_id, new_title)
|
|
90
93
|
finally:
|
|
91
94
|
cleanup_images(image_paths)
|
|
92
95
|
|