linkgravity 1.5.9 → 1.5.11
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 -12
- package/npm-scripts/ensure-env.js +30 -21
- 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,13 +32,17 @@ 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
48
|
// pm2 gives Python a pipe not a TTY, so it block-buffers stdout and can sit on log lines indefinitely - force line buffering.
|
|
@@ -119,7 +123,7 @@ function colorizeLevel(line) {
|
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
function runPm2LogsStream(args, printLine) {
|
|
122
|
-
const cp = spawn(
|
|
126
|
+
const cp = spawn(process.execPath, [PM2_BIN, ...args], { cwd: path.join(__dirname, '..') });
|
|
123
127
|
|
|
124
128
|
const isNoise = (line) =>
|
|
125
129
|
line.trim().length === 0 ||
|
|
@@ -160,6 +164,8 @@ function runPm2LogsStream(args, printLine) {
|
|
|
160
164
|
stdoutHandler.flush();
|
|
161
165
|
stderrHandler.flush();
|
|
162
166
|
});
|
|
167
|
+
// Ctrl-C reaches pm2 too (same process group), but an unhandled parent exit would leave it tailing.
|
|
168
|
+
process.on('exit', () => cp.kill());
|
|
163
169
|
}
|
|
164
170
|
|
|
165
171
|
function runPm2LogsClean(args, showStamps = false) {
|
|
@@ -175,10 +181,7 @@ function verifyStartup() {
|
|
|
175
181
|
`${color.cyan}▶${color.reset} Verifying startup status (waiting for bot to come online)...`,
|
|
176
182
|
);
|
|
177
183
|
|
|
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'], {
|
|
184
|
+
let cp = spawn(process.execPath, [PM2_BIN, 'logs', LGY_PM2_NAME, '--raw', '--lines', '0'], {
|
|
182
185
|
cwd: path.join(__dirname, '..'),
|
|
183
186
|
});
|
|
184
187
|
|
|
@@ -282,7 +285,7 @@ function findAgyBin() {
|
|
|
282
285
|
}
|
|
283
286
|
|
|
284
287
|
function getPm2Proc() {
|
|
285
|
-
const jlist = spawnSync(
|
|
288
|
+
const jlist = spawnSync(process.execPath, [PM2_BIN, 'jlist'], { stdio: 'pipe' });
|
|
286
289
|
if (jlist.status !== 0) return null;
|
|
287
290
|
|
|
288
291
|
// pm2 can print noise before the real JSON (version banners, ANSI escapes, daemon-spawn logs) that
|
|
@@ -376,11 +379,8 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
|
|
|
376
379
|
}
|
|
377
380
|
|
|
378
381
|
if (!isEnvironmentReady()) {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
`run ${color.cyan}lgy setup${color.reset} first (it installs everything on its first run).\n`,
|
|
382
|
-
);
|
|
383
|
-
process.exit(1);
|
|
382
|
+
info('Some dependencies are missing - installing them first...');
|
|
383
|
+
require('../npm-scripts/ensure-env').ensureEnvironment();
|
|
384
384
|
}
|
|
385
385
|
|
|
386
386
|
info('Starting LinkGravity daemon...');
|
|
@@ -602,6 +602,12 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
|
|
|
602
602
|
}
|
|
603
603
|
success(`Installed v${latestVersion}.`);
|
|
604
604
|
|
|
605
|
+
// The new install ships without voice-service/node_modules, so restore anything the
|
|
606
|
+
// directory swap dropped. Loaded here rather than at the top of the file: by now npm has
|
|
607
|
+
// replaced this package on disk, and the copy required at startup is the pre-update one.
|
|
608
|
+
delete require.cache[require.resolve('../npm-scripts/ensure-env')];
|
|
609
|
+
require('../npm-scripts/ensure-env').ensureEnvironment();
|
|
610
|
+
|
|
605
611
|
if (!procBeforeUpdate) {
|
|
606
612
|
info("Daemon wasn't running - starting it fresh...");
|
|
607
613
|
runPm2(['start', LGY_SCRIPT_PATH, '--interpreter', pythonExe, '--name', LGY_PM2_NAME]);
|
|
@@ -8,31 +8,40 @@ const { pip: venvPip, python: venvPython, workspaceDir, repoRoot } = require('./
|
|
|
8
8
|
const isWin = os.platform() === 'win32';
|
|
9
9
|
const pyCmd = isWin ? 'python' : 'python3';
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
const voiceServiceDir = path.join(repoRoot, 'voice-service');
|
|
12
|
+
|
|
13
|
+
function isVenvReady() {
|
|
12
14
|
return fs.existsSync(venvPython);
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
function isVoiceServiceReady() {
|
|
18
|
+
return fs.existsSync(path.join(voiceServiceDir, 'node_modules'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isEnvironmentReady() {
|
|
22
|
+
return isVenvReady() && isVoiceServiceReady();
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
function ensureEnvironment() {
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
stdio: 'inherit',
|
|
34
|
-
|
|
35
|
-
});
|
|
26
|
+
if (!isVenvReady()) {
|
|
27
|
+
console.log('⚙️ Setting up Python Virtual Environment...');
|
|
28
|
+
console.log(
|
|
29
|
+
` (in ${path.join(workspaceDir, 'venv')} - not inside this install, so it survives`,
|
|
30
|
+
);
|
|
31
|
+
console.log(' package updates/reinstalls and works the same whether this is a global');
|
|
32
|
+
console.log(' `npm install -g linkgravity` or a local dev clone.)');
|
|
33
|
+
|
|
34
|
+
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
35
|
+
execSync(`${pyCmd} -m venv "${path.join(workspaceDir, 'venv')}"`, { stdio: 'inherit' });
|
|
36
|
+
|
|
37
|
+
console.log('📦 Installing Python dependencies...');
|
|
38
|
+
execSync(`"${venvPip}" install -r requirements.txt`, { stdio: 'inherit', cwd: repoRoot });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!isVoiceServiceReady()) {
|
|
42
|
+
console.log('🎙️ Installing Voice Service dependencies...');
|
|
43
|
+
execSync('npm install', { stdio: 'inherit', cwd: voiceServiceDir });
|
|
44
|
+
}
|
|
36
45
|
|
|
37
46
|
console.log('✅ Environment ready.');
|
|
38
47
|
}
|
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
|
|