draken 0.5.1 → 0.7.0
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/CHANGELOG.md +40 -0
- package/MEMORY.md +43 -6
- package/PERSONA.md +9 -2
- package/README.md +14 -14
- package/SETUP.md +23 -3
- package/TRAY.md +4 -4
- package/VOICE.md +1 -1
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +0 -13
- package/dist/auth/middleware.js.map +1 -1
- package/dist/cli.js +6 -2
- package/dist/cli.js.map +1 -1
- package/dist/data-dir.d.ts +2 -0
- package/dist/data-dir.d.ts.map +1 -0
- package/dist/data-dir.js +12 -0
- package/dist/data-dir.js.map +1 -0
- package/dist/db/evidence.d.ts +2 -1
- package/dist/db/evidence.d.ts.map +1 -1
- package/dist/db/evidence.js +6 -5
- package/dist/db/evidence.js.map +1 -1
- package/dist/db/pointer-migration.js +4 -4
- package/dist/db/pointer-migration.js.map +1 -1
- package/dist/db/schema.d.ts +0 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +31 -7
- package/dist/db/schema.js.map +1 -1
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +12 -0
- package/dist/errors.js.map +1 -0
- package/dist/memory/context.d.ts +2 -9
- package/dist/memory/context.d.ts.map +1 -1
- package/dist/memory/context.js +7 -84
- package/dist/memory/context.js.map +1 -1
- package/dist/memory/embedding-contract.d.ts +5 -0
- package/dist/memory/embedding-contract.d.ts.map +1 -0
- package/dist/memory/embedding-contract.js +12 -0
- package/dist/memory/embedding-contract.js.map +1 -0
- package/dist/memory/embedding-pool.d.ts +1 -2
- package/dist/memory/embedding-pool.d.ts.map +1 -1
- package/dist/memory/embedding-pool.js +9 -9
- package/dist/memory/embedding-pool.js.map +1 -1
- package/dist/memory/embedding-worker.js +2 -1
- package/dist/memory/embedding-worker.js.map +1 -1
- package/dist/{voice/history-focus.d.ts → memory/focus.d.ts} +6 -6
- package/dist/memory/focus.d.ts.map +1 -0
- package/dist/{voice/history-focus.js → memory/focus.js} +6 -6
- package/dist/memory/focus.js.map +1 -0
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/memory/index.js +12 -15
- package/dist/memory/index.js.map +1 -1
- package/dist/memory/model-assets.d.ts +5 -0
- package/dist/memory/model-assets.d.ts.map +1 -0
- package/dist/memory/model-assets.js +84 -0
- package/dist/memory/model-assets.js.map +1 -0
- package/dist/memory/model.d.ts +2 -7
- package/dist/memory/model.d.ts.map +1 -1
- package/dist/memory/model.js +13 -83
- package/dist/memory/model.js.map +1 -1
- package/dist/memory/packet.d.ts +9 -0
- package/dist/memory/packet.d.ts.map +1 -0
- package/dist/memory/packet.js +78 -0
- package/dist/memory/packet.js.map +1 -0
- package/dist/memory/passages.d.ts +15 -0
- package/dist/memory/passages.d.ts.map +1 -0
- package/dist/memory/passages.js +22 -0
- package/dist/memory/passages.js.map +1 -0
- package/dist/memory/readiness.d.ts.map +1 -1
- package/dist/memory/readiness.js +2 -6
- package/dist/memory/readiness.js.map +1 -1
- package/dist/memory/service.d.ts +2 -1
- package/dist/memory/service.d.ts.map +1 -1
- package/dist/memory/service.js +46 -22
- package/dist/memory/service.js.map +1 -1
- package/dist/memory/sources.d.ts +0 -1
- package/dist/memory/sources.d.ts.map +1 -1
- package/dist/memory/sources.js +3 -7
- package/dist/memory/sources.js.map +1 -1
- package/dist/memory/stamps.d.ts +3 -3
- package/dist/memory/stamps.d.ts.map +1 -1
- package/dist/memory/stamps.js +3 -3
- package/dist/memory/stamps.js.map +1 -1
- package/dist/memory/storage.d.ts.map +1 -1
- package/dist/memory/storage.js +3 -2
- package/dist/memory/storage.js.map +1 -1
- package/dist/memory/types.d.ts +2 -1
- package/dist/memory/types.d.ts.map +1 -1
- package/dist/memory/types.js +2 -1
- package/dist/memory/types.js.map +1 -1
- package/dist/pi/runtime.js +7 -7
- package/dist/pi/runtime.js.map +1 -1
- package/dist/pi/setup.d.ts +0 -1
- package/dist/pi/setup.d.ts.map +1 -1
- package/dist/pi/setup.js +2 -11
- package/dist/pi/setup.js.map +1 -1
- package/dist/routes/auth.d.ts.map +1 -1
- package/dist/routes/auth.js +0 -5
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/http.d.ts +6 -0
- package/dist/routes/http.d.ts.map +1 -0
- package/dist/routes/http.js +22 -0
- package/dist/routes/http.js.map +1 -0
- package/dist/routes/live-voice.d.ts.map +1 -1
- package/dist/routes/live-voice.js +18 -32
- package/dist/routes/live-voice.js.map +1 -1
- package/dist/routes/memory.d.ts.map +1 -1
- package/dist/routes/memory.js +8 -14
- package/dist/routes/memory.js.map +1 -1
- package/dist/routes/sync.d.ts.map +1 -1
- package/dist/routes/sync.js +65 -65
- package/dist/routes/sync.js.map +1 -1
- package/dist/routes/voice.d.ts.map +1 -1
- package/dist/routes/voice.js +7 -7
- package/dist/routes/voice.js.map +1 -1
- package/dist/server-ownership.d.ts +5 -0
- package/dist/server-ownership.d.ts.map +1 -0
- package/dist/server-ownership.js +34 -0
- package/dist/server-ownership.js.map +1 -0
- package/dist/server-state.js +3 -3
- package/dist/server-state.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +40 -10
- package/dist/server.js.map +1 -1
- package/dist/sync/activity.d.ts +26 -0
- package/dist/sync/activity.d.ts.map +1 -0
- package/dist/sync/activity.js +80 -0
- package/dist/sync/activity.js.map +1 -0
- package/dist/{memory/automatic.d.ts → sync/background.d.ts} +7 -12
- package/dist/sync/background.d.ts.map +1 -0
- package/dist/sync/background.js +184 -0
- package/dist/sync/background.js.map +1 -0
- package/dist/sync/collector.d.ts.map +1 -1
- package/dist/sync/collector.js +7 -6
- package/dist/sync/collector.js.map +1 -1
- package/dist/sync/manager.d.ts +1 -4
- package/dist/sync/manager.d.ts.map +1 -1
- package/dist/sync/manager.js +29 -49
- package/dist/sync/manager.js.map +1 -1
- package/dist/sync/models.d.ts +3 -0
- package/dist/sync/models.d.ts.map +1 -0
- package/dist/sync/models.js +12 -0
- package/dist/sync/models.js.map +1 -0
- package/dist/sync/pi.d.ts +1 -1
- package/dist/sync/pi.d.ts.map +1 -1
- package/dist/sync/pi.js +32 -19
- package/dist/sync/pi.js.map +1 -1
- package/dist/{voice → sync}/preparation.d.ts +0 -1
- package/dist/sync/preparation.d.ts.map +1 -0
- package/dist/{voice → sync}/preparation.js +10 -18
- package/dist/sync/preparation.js.map +1 -0
- package/dist/sync/retrieval.d.ts +3 -0
- package/dist/sync/retrieval.d.ts.map +1 -0
- package/dist/sync/retrieval.js +12 -0
- package/dist/sync/retrieval.js.map +1 -0
- package/dist/sync/source-files.d.ts +2 -4
- package/dist/sync/source-files.d.ts.map +1 -1
- package/dist/sync/source-files.js +5 -20
- package/dist/sync/source-files.js.map +1 -1
- package/dist/sync/store.d.ts +5 -15
- package/dist/sync/store.d.ts.map +1 -1
- package/dist/sync/store.js +41 -90
- package/dist/sync/store.js.map +1 -1
- package/dist/sync/summary-preparation.d.ts +14 -0
- package/dist/sync/summary-preparation.d.ts.map +1 -0
- package/dist/sync/summary-preparation.js +205 -0
- package/dist/sync/summary-preparation.js.map +1 -0
- package/dist/sync/tree.d.ts +17 -1
- package/dist/sync/tree.d.ts.map +1 -1
- package/dist/sync/tree.js +109 -49
- package/dist/sync/tree.js.map +1 -1
- package/dist/sync/usage.d.ts +3 -1
- package/dist/sync/usage.d.ts.map +1 -1
- package/dist/sync/usage.js +27 -9
- package/dist/sync/usage.js.map +1 -1
- package/dist/tray/launcher.js +2 -2
- package/dist/tray/launcher.js.map +1 -1
- package/dist/tray/worker.js +4 -4
- package/dist/tray/worker.js.map +1 -1
- package/dist/types.d.ts +9 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/voice/history-delivery.js +3 -3
- package/dist/voice/history-delivery.js.map +1 -1
- package/dist/voice/live-context.js +3 -3
- package/dist/voice/live-context.js.map +1 -1
- package/dist/voice/live.d.ts +1 -0
- package/dist/voice/live.d.ts.map +1 -1
- package/dist/voice/live.js +57 -34
- package/dist/voice/live.js.map +1 -1
- package/package.json +1 -1
- package/public/assets/index-5bTQNQYG.js +131 -0
- package/public/index.html +1 -1
- package/voice/realtime/README.md +1 -1
- package/voice/realtime/helper-process.mjs +54 -0
- package/voice/realtime/native.mjs +55 -15
- package/voice/realtime/probe-server.mjs +24 -4
- package/voice/realtime/probe.mjs +49 -11
- package/voice/realtime/rpc.mjs +7 -32
- package/voice/realtime/setup.mjs +82 -19
- package/dist/memory/automatic.d.ts.map +0 -1
- package/dist/memory/automatic.js +0 -200
- package/dist/memory/automatic.js.map +0 -1
- package/dist/sync/automatic.d.ts +0 -7
- package/dist/sync/automatic.d.ts.map +0 -1
- package/dist/sync/automatic.js +0 -56
- package/dist/sync/automatic.js.map +0 -1
- package/dist/voice/history-focus.d.ts.map +0 -1
- package/dist/voice/history-focus.js.map +0 -1
- package/dist/voice/preparation.d.ts.map +0 -1
- package/dist/voice/preparation.js.map +0 -1
- package/public/assets/index-DrGvDGTG.js +0 -131
|
@@ -2,8 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { timingSafeEqual } from 'node:crypto';
|
|
5
|
-
import {
|
|
6
|
-
import { promisify } from 'node:util';
|
|
5
|
+
import { NativeHelperProcess } from './helper-process.mjs';
|
|
7
6
|
import { NativeRpc } from './rpc.mjs';
|
|
8
7
|
import { createReadOnlyBackend } from './backend.mjs';
|
|
9
8
|
|
|
@@ -19,7 +18,47 @@ export function codexExecutable(root, platform = process.platform, arch = proces
|
|
|
19
18
|
}
|
|
20
19
|
export const defaultBinary = codexExecutable(realtimeRoot);
|
|
21
20
|
export const defaultHome = path.join(realtimeRoot, 'account');
|
|
22
|
-
const
|
|
21
|
+
const checks = new Set();
|
|
22
|
+
let checksStopping = false;
|
|
23
|
+
|
|
24
|
+
function inspectNative({ binary, home, signal }) {
|
|
25
|
+
let helper;
|
|
26
|
+
if (!checksStopping && !signal?.aborted) {
|
|
27
|
+
try { helper = new NativeHelperProcess(binary, ['--version'], { env: helperEnvironment(home) }); } catch { /* Structural status only. */ }
|
|
28
|
+
}
|
|
29
|
+
if (!helper) return { stop() {}, done: Promise.resolve({ installed: false, exited: true }) };
|
|
30
|
+
let stdout = '', failed = false;
|
|
31
|
+
const stop = () => { failed = true; helper.close(); };
|
|
32
|
+
const check = { helper, stop }; checks.add(check);
|
|
33
|
+
const timer = setTimeout(stop, 5000);
|
|
34
|
+
helper.child.stdout.setEncoding('utf8');
|
|
35
|
+
helper.child.stdout.on('data', chunk => {
|
|
36
|
+
if (failed) return;
|
|
37
|
+
if (Buffer.byteLength(stdout) + Buffer.byteLength(chunk) > 4096) { stop(); return; }
|
|
38
|
+
stdout += chunk;
|
|
39
|
+
});
|
|
40
|
+
helper.child.on('error', stop); helper.child.stdin.on('error', stop); helper.child.stdin.end();
|
|
41
|
+
signal?.addEventListener('abort', stop, { once: true });
|
|
42
|
+
if (signal?.aborted) stop();
|
|
43
|
+
const done = helper.waitForExit(7500).then(exited => {
|
|
44
|
+
clearTimeout(timer); signal?.removeEventListener('abort', stop);
|
|
45
|
+
if (exited) checks.delete(check);
|
|
46
|
+
return { installed: exited && !failed && helper.child.exitCode === 0 && stdout.trim() === `codex-cli ${CODEX_VERSION}`, exited };
|
|
47
|
+
});
|
|
48
|
+
return { stop, done };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function stopNativeChecks() {
|
|
52
|
+
checksStopping = true;
|
|
53
|
+
const pending = [...checks];
|
|
54
|
+
for (const check of pending) check.stop();
|
|
55
|
+
const results = await Promise.all(pending.map(async check => {
|
|
56
|
+
const exited = await check.helper.waitForExit();
|
|
57
|
+
if (exited) checks.delete(check);
|
|
58
|
+
return exited;
|
|
59
|
+
}));
|
|
60
|
+
if (results.some(exited => !exited)) throw new Error('Native status helper exit could not be confirmed.');
|
|
61
|
+
}
|
|
23
62
|
|
|
24
63
|
// Positive allowlist: no API keys, pi session ownership, cloud credentials,
|
|
25
64
|
// proxy overrides, provider URLs, NODE_OPTIONS, or user agent configuration.
|
|
@@ -36,12 +75,8 @@ export function hasNativeLogin(home) {
|
|
|
36
75
|
['access_token', 'refresh_token', 'id_token'].every(key => typeof value.tokens?.[key] === 'string' && value.tokens[key].length > 0);
|
|
37
76
|
} catch { return false; }
|
|
38
77
|
}
|
|
39
|
-
export async function nativeStatus({ binary = defaultBinary, home = defaultHome } = {}) {
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
const { stdout } = await exec(binary, ['--version'], { env: helperEnvironment(home), timeout: 5000, maxBuffer: 4096 });
|
|
43
|
-
installed = stdout.trim() === `codex-cli ${CODEX_VERSION}`;
|
|
44
|
-
} catch { /* Report a structural status only; never leak subprocess output. */ }
|
|
78
|
+
export async function nativeStatus({ binary = defaultBinary, home = defaultHome, signal } = {}) {
|
|
79
|
+
const { installed } = await inspectNative({ binary, home, signal }).done;
|
|
45
80
|
return { installed, expectedVersion: CODEX_VERSION, nativeLoginPresent: hasNativeLogin(home),
|
|
46
81
|
readyForProbe: installed && hasNativeLogin(home), audioVerified: false };
|
|
47
82
|
}
|
|
@@ -97,6 +132,7 @@ export class NativeVoice {
|
|
|
97
132
|
#answer;
|
|
98
133
|
#cleanup;
|
|
99
134
|
#unsubscribe;
|
|
135
|
+
#preflight;
|
|
100
136
|
constructor({ binary = defaultBinary, home = defaultHome, answer } = {}) { this.#binary = binary; this.#home = home; this.#answer = answer; }
|
|
101
137
|
subscribe(listener) { this.#listeners.add(listener); return () => this.#listeners.delete(listener); }
|
|
102
138
|
#emit(event) { for (const listener of this.#listeners) { try { listener(event); } catch { void this.close(); } } }
|
|
@@ -104,15 +140,16 @@ export class NativeVoice {
|
|
|
104
140
|
if (this.#closed || this.#opened) throw new Error('A native voice instance can open only once.');
|
|
105
141
|
this.#opened = true;
|
|
106
142
|
if (typeof sdp !== 'string' || sdp.length > 64_000 || !sdp.startsWith('v=0') || (sdp.match(/^m=audio /gm) || []).length !== 1 || /^m=video /m.test(sdp)) throw new Error('One audio track and a bounded SDP offer are required.');
|
|
107
|
-
const status = await nativeStatus({ binary: this.#binary, home: this.#home });
|
|
108
|
-
if (!status.installed) throw new Error(`Install the pinned Codex ${CODEX_VERSION} helper first.`);
|
|
109
|
-
if (!status.nativeLoginPresent) throw new Error('A separate native ChatGPT sign-in is required. Run npm run voice:realtime:login. The pi login is not copied or converted.');
|
|
110
|
-
if (fs.existsSync(path.join(this.#home, 'config.toml'))) throw new Error('Use the dedicated voice account directory without a custom Codex config.');
|
|
111
|
-
if (signal?.aborted || this.#closed) throw new Error('Native voice setup cancelled.');
|
|
112
143
|
const abort = () => { void this.close(); };
|
|
113
144
|
signal?.addEventListener('abort', abort, { once: true });
|
|
114
145
|
this.#cleanup = () => signal?.removeEventListener('abort', abort);
|
|
115
146
|
try {
|
|
147
|
+
this.#preflight = inspectNative({ binary: this.#binary, home: this.#home, signal });
|
|
148
|
+
const status = await this.#preflight.done;
|
|
149
|
+
if (signal?.aborted || this.#closed) throw new Error('Native voice setup cancelled.');
|
|
150
|
+
if (!status.installed) throw new Error(`Install the pinned Codex ${CODEX_VERSION} helper first.`);
|
|
151
|
+
if (!hasNativeLogin(this.#home)) throw new Error('A separate native ChatGPT sign-in is required. Run npm run voice:realtime:login. The pi login is not copied or converted.');
|
|
152
|
+
if (fs.existsSync(path.join(this.#home, 'config.toml'))) throw new Error('Use the dedicated voice account directory without a custom Codex config.');
|
|
116
153
|
this.#deny = await createReadOnlyBackend({ answer: this.#answer, maxRequests: 120,
|
|
117
154
|
authorize: headers => {
|
|
118
155
|
// Native auth already accompanies the helper's backing requests. Match
|
|
@@ -197,11 +234,14 @@ export class NativeVoice {
|
|
|
197
234
|
}
|
|
198
235
|
async #closeResources() {
|
|
199
236
|
this.#cleanup?.(); this.#unsubscribe?.();
|
|
237
|
+
this.#preflight?.stop();
|
|
238
|
+
const preflightExited = !this.#preflight || (await this.#preflight.done).exited;
|
|
200
239
|
if (this.#thread && this.#rpc && !this.#rpc.closed) {
|
|
201
240
|
try { await this.#rpc.request('thread/realtime/stop', { threadId: this.#thread }, { timeoutMs: 1500 }); } catch { /* Terminate below, even if stop was rejected. */ }
|
|
202
241
|
}
|
|
203
242
|
this.#rpc?.close();
|
|
204
|
-
const
|
|
243
|
+
const rpcExited = !this.#rpc || await this.#rpc.waitForExit();
|
|
244
|
+
const exited = preflightExited && rpcExited;
|
|
205
245
|
await this.#deny?.close();
|
|
206
246
|
this.#emit({ method: 'draken/closed', params: { reason: exited ? 'Native voice connection closed.' : 'Native helper exit could not be confirmed.', helperExited: exited } });
|
|
207
247
|
this.#listeners.clear();
|
|
@@ -11,9 +11,16 @@ export async function createProbeServer({ voiceFactory = () => new NativeVoice({
|
|
|
11
11
|
}), leaseMs = 10_000, lifetimeMs = 90_000 } = {}) {
|
|
12
12
|
const token = randomBytes(32).toString('hex');
|
|
13
13
|
const expected = Buffer.from(`Bearer ${token}`);
|
|
14
|
-
let call, accepted = false, touched = Date.now(), origin, closing;
|
|
14
|
+
let call, accepted = false, touched = Date.now(), origin, closing, serverClosing;
|
|
15
15
|
const events = []; let sequence = 0;
|
|
16
|
-
const finish = () => {
|
|
16
|
+
const finish = () => {
|
|
17
|
+
if (!closing) {
|
|
18
|
+
closing = Promise.resolve().then(() => call?.close());
|
|
19
|
+
// Event/lease shutdown may precede the terminal owner's awaited close.
|
|
20
|
+
closing.catch(() => {});
|
|
21
|
+
}
|
|
22
|
+
return closing;
|
|
23
|
+
};
|
|
17
24
|
const body = async req => {
|
|
18
25
|
req.setEncoding('utf8');
|
|
19
26
|
let data = ''; for await (const chunk of req) { data += chunk; if (data.length > 64_000) throw new Error('Request too large.'); }
|
|
@@ -73,7 +80,7 @@ export async function createProbeServer({ voiceFactory = () => new NativeVoice({
|
|
|
73
80
|
res.end(JSON.stringify({ helperExited: result?.helperExited !== false })); return;
|
|
74
81
|
}
|
|
75
82
|
res.writeHead(404); res.end();
|
|
76
|
-
} catch { await finish(); if (!res.headersSent) res.writeHead(502); res.end('Native voice qualification failed. No provider fallback was attempted.'); }
|
|
83
|
+
} catch { await finish().catch(() => {}); if (!res.headersSent) res.writeHead(502); res.end('Native voice qualification failed. No provider fallback was attempted.'); }
|
|
77
84
|
});
|
|
78
85
|
server.requestTimeout = 65_000;
|
|
79
86
|
server.headersTimeout = 10_000;
|
|
@@ -82,5 +89,18 @@ export async function createProbeServer({ voiceFactory = () => new NativeVoice({
|
|
|
82
89
|
const sweep = setInterval(() => { if (accepted && Date.now() - touched > leaseMs) void finish(); }, Math.min(1000, leaseMs)); sweep.unref();
|
|
83
90
|
const deadline = setTimeout(() => { void finish(); }, lifetimeMs); deadline.unref();
|
|
84
91
|
return { origin, token, events,
|
|
85
|
-
|
|
92
|
+
close() {
|
|
93
|
+
if (!serverClosing) {
|
|
94
|
+
clearInterval(sweep); clearTimeout(deadline);
|
|
95
|
+
const completion = finish();
|
|
96
|
+
serverClosing = (async () => {
|
|
97
|
+
const listener = new Promise(resolve => server.close(resolve));
|
|
98
|
+
server.closeAllConnections();
|
|
99
|
+
const [helper] = await Promise.allSettled([completion, listener]);
|
|
100
|
+
if (helper.status === 'rejected') throw helper.reason;
|
|
101
|
+
if (helper.value?.helperExited === false) throw new Error('Native probe helper exit could not be confirmed.');
|
|
102
|
+
})();
|
|
103
|
+
}
|
|
104
|
+
return serverClosing;
|
|
105
|
+
} };
|
|
86
106
|
}
|
package/voice/realtime/probe.mjs
CHANGED
|
@@ -1,32 +1,59 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { realtimeRoot, nativeStatus } from './native.mjs';
|
|
3
|
+
import { realtimeRoot, nativeStatus, stopNativeChecks } from './native.mjs';
|
|
4
4
|
import { createProbeServer } from './probe-server.mjs';
|
|
5
5
|
|
|
6
6
|
process.umask(0o077);
|
|
7
|
-
let browser, lab,
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
let browser, lab, browserClosing, checksClosing;
|
|
8
|
+
const controller = new AbortController();
|
|
9
|
+
const { signal } = controller;
|
|
10
|
+
const hold = setInterval(() => {}, 1000);
|
|
11
|
+
const stopChecks = () => {
|
|
12
|
+
if (!checksClosing) { checksClosing = stopNativeChecks(); checksClosing.catch(() => {}); }
|
|
13
|
+
return checksClosing;
|
|
11
14
|
};
|
|
12
|
-
|
|
15
|
+
const closeBrowser = () => {
|
|
16
|
+
if (browser && !browserClosing) { browserClosing = browser.close(); browserClosing.catch(() => {}); }
|
|
17
|
+
return browserClosing;
|
|
18
|
+
};
|
|
19
|
+
const onSignal = name => {
|
|
20
|
+
if (signal.aborted) return;
|
|
21
|
+
process.exitCode = name === 'SIGINT' ? 130 : 143;
|
|
22
|
+
controller.abort();
|
|
23
|
+
void stopChecks(); void closeBrowser(); void lab?.close().catch(() => {});
|
|
24
|
+
};
|
|
25
|
+
const active = () => { if (signal.aborted) throw new Error('Native voice probe cancelled.'); };
|
|
26
|
+
process.on('SIGINT', onSignal); process.on('SIGTERM', onSignal);
|
|
13
27
|
try {
|
|
14
28
|
const flags = process.argv.slice(2);
|
|
15
29
|
if (flags.length !== 1 || !['--check', '--allow-provider'].includes(flags[0])) throw new Error('Use --check, or explicitly --allow-provider for one bounded synthetic audio test.');
|
|
16
|
-
const status = await nativeStatus();
|
|
30
|
+
const status = await nativeStatus({ signal });
|
|
31
|
+
active();
|
|
17
32
|
if (flags[0] === '--check') console.log(JSON.stringify(status, null, 2));
|
|
18
33
|
else {
|
|
19
34
|
if (!status.readyForProbe) throw new Error('Native ChatGPT sign-in is missing or the pinned helper is unavailable. Run npm run voice:realtime:setup, then npm run voice:realtime:login. No provider request was made.');
|
|
20
35
|
const { chromium } = await import('@playwright/test');
|
|
21
|
-
|
|
36
|
+
active();
|
|
37
|
+
lab = await createProbeServer();
|
|
38
|
+
active();
|
|
39
|
+
browser = await chromium.launch();
|
|
40
|
+
active();
|
|
22
41
|
const context = await browser.newContext({ extraHTTPHeaders: { Authorization: `Bearer ${lab.token}` } });
|
|
42
|
+
if (signal.aborted) { await context.close(); active(); }
|
|
23
43
|
// Do not let page HTTP requests carry the lab credential to any other origin.
|
|
24
44
|
await context.route('**/*', route => new URL(route.request().url()).origin === lab.origin ? route.continue() : route.abort());
|
|
45
|
+
active();
|
|
25
46
|
const page = await context.newPage(); const pageErrors = [];
|
|
47
|
+
active();
|
|
26
48
|
page.on('pageerror', () => pageErrors.push('Browser runtime error.'));
|
|
27
|
-
await page.goto(lab.origin);
|
|
49
|
+
await page.goto(lab.origin);
|
|
50
|
+
active();
|
|
51
|
+
await page.getByRole('button', { name: 'Run synthetic call' }).click();
|
|
52
|
+
active();
|
|
28
53
|
await page.waitForFunction(() => window.voiceResult?.completed, {}, { timeout: 85_000 });
|
|
54
|
+
active();
|
|
29
55
|
const result = await page.evaluate(() => window.voiceResult);
|
|
56
|
+
active();
|
|
30
57
|
result.errors.push(...pageErrors);
|
|
31
58
|
result.success = result.success && result.localCleanup && result.helperCleanup && result.questions.length === 2 && result.audioFrames > 10 && !result.errors.length;
|
|
32
59
|
result.audioVerified = result.success;
|
|
@@ -38,5 +65,16 @@ try {
|
|
|
38
65
|
receivedAudioFrames: result.audioFrames, transcripts: result.transcripts.length, localCleanup: result.localCleanup, helperCleanup: result.helperCleanup, report }, null, 2));
|
|
39
66
|
if (!result.success) process.exitCode = 1;
|
|
40
67
|
}
|
|
41
|
-
} catch (error) { console.error(error.message); process.exitCode = 1; }
|
|
42
|
-
finally {
|
|
68
|
+
} catch (error) { console.error(signal.aborted ? 'Native voice probe cancelled.' : error.message); if (!signal.aborted) process.exitCode = 1; }
|
|
69
|
+
finally {
|
|
70
|
+
const warning = setTimeout(() => console.error('Native probe cleanup is still pending. Keeping this terminal host alive.'), 10_000);
|
|
71
|
+
const results = await Promise.allSettled([stopChecks(), closeBrowser(), lab?.close()]);
|
|
72
|
+
clearTimeout(warning);
|
|
73
|
+
if (results.some(result => result.status === 'rejected')) {
|
|
74
|
+
process.exitCode = 1;
|
|
75
|
+
console.error('Native probe cleanup could not be confirmed. Keeping this terminal host alive; no further operation will start.');
|
|
76
|
+
await new Promise(() => {});
|
|
77
|
+
}
|
|
78
|
+
clearInterval(hold);
|
|
79
|
+
process.removeListener('SIGINT', onSignal); process.removeListener('SIGTERM', onSignal);
|
|
80
|
+
}
|
package/voice/realtime/rpc.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NativeHelperProcess } from './helper-process.mjs';
|
|
2
2
|
|
|
3
3
|
/** Private stdio only. No HTTP listener, shell, generic command route, or raw logging. */
|
|
4
4
|
export class NativeRpc {
|
|
@@ -8,24 +8,15 @@ export class NativeRpc {
|
|
|
8
8
|
#sequence = 0;
|
|
9
9
|
#buffer = '';
|
|
10
10
|
#closed = false;
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
constructor(binary, args, { cwd, env, spawnProcess = spawn } = {}) {
|
|
16
|
-
this.#exitPromise = new Promise(resolve => { this.#resolveExit = resolve; });
|
|
17
|
-
this.#child = spawnProcess(binary, args, { cwd, env, shell: false, detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
11
|
+
#helper;
|
|
12
|
+
constructor(binary, args, options = {}) {
|
|
13
|
+
this.#helper = new NativeHelperProcess(binary, args, options);
|
|
14
|
+
this.#child = this.#helper.child;
|
|
18
15
|
this.#child.stdout.setEncoding('utf8');
|
|
19
16
|
this.#child.stdout.on('data', chunk => this.#receive(chunk));
|
|
20
|
-
// Native diagnostics can contain prompts, transcripts, paths and auth details.
|
|
21
|
-
// Drain them, but never copy them into routine logs or browser errors.
|
|
22
|
-
this.#child.stderr.resume();
|
|
23
17
|
this.#child.stdin.on('error', () => this.close('Native voice input closed.'));
|
|
24
18
|
this.#child.on('error', () => this.close('Cannot start the pinned native voice helper.'));
|
|
25
19
|
this.#child.on('close', () => {
|
|
26
|
-
this.#exited = true;
|
|
27
|
-
clearTimeout(this.#killTimer);
|
|
28
|
-
this.#resolveExit(true);
|
|
29
20
|
this.close('Native voice helper stopped.');
|
|
30
21
|
});
|
|
31
22
|
}
|
|
@@ -89,29 +80,13 @@ export class NativeRpc {
|
|
|
89
80
|
if (signal?.aborted) abort();
|
|
90
81
|
});
|
|
91
82
|
}
|
|
92
|
-
|
|
93
|
-
let timer;
|
|
94
|
-
try { return await Promise.race([this.#exitPromise, new Promise(resolve => { timer = setTimeout(() => resolve(false), timeoutMs); })]); }
|
|
95
|
-
finally { clearTimeout(timer); }
|
|
96
|
-
}
|
|
97
|
-
#terminate(signal) {
|
|
98
|
-
// The official npm launcher owns a native child. On Unix terminate the
|
|
99
|
-
// entire group, including escalation if a launcher exits before its child.
|
|
100
|
-
if (process.platform !== 'win32' && Number.isInteger(this.#child.pid) && this.#child.pid > 1) {
|
|
101
|
-
try { process.kill(-this.#child.pid, signal); return; } catch { /* Already gone, or test double. */ }
|
|
102
|
-
}
|
|
103
|
-
try { this.#child.kill(signal); } catch { /* Exit confirmation is separate. */ }
|
|
104
|
-
}
|
|
83
|
+
waitForExit(timeoutMs) { return this.#helper.waitForExit(timeoutMs); }
|
|
105
84
|
close(reason = 'Native voice connection closed.') {
|
|
106
85
|
if (this.#closed) return;
|
|
107
86
|
this.#closed = true; this.#buffer = '';
|
|
108
87
|
for (const pending of this.#pending.values()) { pending.cleanup(); pending.reject(new Error(reason)); }
|
|
109
88
|
this.#pending.clear();
|
|
110
|
-
this.#
|
|
111
|
-
if (!this.#exited) {
|
|
112
|
-
this.#terminate('SIGTERM');
|
|
113
|
-
this.#killTimer = setTimeout(() => this.#terminate('SIGKILL'), 1500); this.#killTimer.unref();
|
|
114
|
-
}
|
|
89
|
+
this.#helper.close();
|
|
115
90
|
this.#emit({ method: 'draken/closed', params: { reason } });
|
|
116
91
|
this.#listeners.clear();
|
|
117
92
|
}
|
package/voice/realtime/setup.mjs
CHANGED
|
@@ -1,32 +1,95 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { npmCli } from '../../dist/npm-cli.js';
|
|
5
|
-
import {
|
|
5
|
+
import { NativeHelperProcess } from './helper-process.mjs';
|
|
6
|
+
import { CODEX_VERSION, realtimeRoot, defaultBinary, defaultHome, helperEnvironment, nativeStatus, stopNativeChecks } from './native.mjs';
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
/** One terminal workflow owns both preflight and explicit install/login children. */
|
|
9
|
+
export async function voiceSetup(action) {
|
|
10
|
+
if (!['setup', 'login', 'status'].includes(action)) throw new Error('Usage: draken voice setup | login | status');
|
|
11
|
+
process.umask(0o077);
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const { signal } = controller;
|
|
14
|
+
let command, checksClosing;
|
|
15
|
+
// A pending cleanup promise alone does not keep Node alive.
|
|
16
|
+
const hold = setInterval(() => {}, 1000);
|
|
17
|
+
const stopChecks = () => {
|
|
18
|
+
if (!checksClosing) { checksClosing = stopNativeChecks(); checksClosing.catch(() => {}); }
|
|
19
|
+
return checksClosing;
|
|
20
|
+
};
|
|
21
|
+
const onSignal = name => {
|
|
22
|
+
if (signal.aborted) return;
|
|
23
|
+
process.exitCode = name === 'SIGINT' ? 130 : 143;
|
|
24
|
+
controller.abort(); command?.close(); void stopChecks();
|
|
25
|
+
};
|
|
26
|
+
process.on('SIGINT', onSignal); process.on('SIGTERM', onSignal);
|
|
27
|
+
const active = () => { if (signal.aborted) throw new Error('Native voice operation cancelled.'); };
|
|
28
|
+
const run = async (binary, args, env) => {
|
|
29
|
+
active();
|
|
30
|
+
command = new NativeHelperProcess(binary, args, { env, stdio: 'inherit' });
|
|
31
|
+
const helper = command;
|
|
32
|
+
await new Promise(resolve => {
|
|
33
|
+
const done = () => {
|
|
34
|
+
helper.child.removeListener('close', done); helper.child.removeListener('error', done);
|
|
35
|
+
signal.removeEventListener('abort', done); resolve();
|
|
36
|
+
};
|
|
37
|
+
helper.child.once('close', done); helper.child.once('error', done);
|
|
38
|
+
signal.addEventListener('abort', done, { once: true });
|
|
39
|
+
if (signal.aborted) done();
|
|
40
|
+
});
|
|
41
|
+
if (signal.aborted) helper.close();
|
|
42
|
+
if (!(await helper.waitForExit())) throw new Error('Native terminal helper exit could not be confirmed.');
|
|
43
|
+
command = undefined;
|
|
44
|
+
active();
|
|
45
|
+
if (helper.child.exitCode !== 0) throw new Error('The requested helper did not complete.');
|
|
46
|
+
};
|
|
17
47
|
try {
|
|
18
|
-
|
|
48
|
+
active();
|
|
49
|
+
if (action === 'setup') {
|
|
19
50
|
fs.mkdirSync(realtimeRoot, { recursive: true, mode: 0o700 });
|
|
20
51
|
await run(process.execPath, [npmCli(), 'install', '--prefix', path.join(realtimeRoot, 'runtime'), '--ignore-scripts', '--no-audit', '--no-fund', '--save-exact', `@openai/codex@${CODEX_VERSION}`], helperEnvironment(defaultHome));
|
|
21
52
|
}
|
|
22
|
-
if (
|
|
23
|
-
|
|
53
|
+
if (action === 'login') {
|
|
54
|
+
const status = await nativeStatus({ signal });
|
|
55
|
+
active();
|
|
56
|
+
if (!status.installed) throw new Error('Run npm run voice:realtime:setup first.');
|
|
24
57
|
fs.mkdirSync(defaultHome, { recursive: true, mode: 0o700 });
|
|
25
58
|
if (fs.existsSync(path.join(defaultHome, 'config.toml'))) throw new Error('The dedicated voice account directory must not contain a custom config.toml.');
|
|
26
|
-
//
|
|
27
|
-
// This is native ChatGPT sign-in, not API-key login and not pi's direct-token registration.
|
|
59
|
+
// Explicit native ChatGPT sign-in, not API-key login or pi token conversion.
|
|
28
60
|
await run(defaultBinary, ['-c', 'cli_auth_credentials_store="file"', '-c', 'forced_login_method="chatgpt"', 'login', '--device-auth'], helperEnvironment(defaultHome));
|
|
29
61
|
}
|
|
30
|
-
|
|
31
|
-
|
|
62
|
+
active();
|
|
63
|
+
const status = await nativeStatus({ signal });
|
|
64
|
+
active();
|
|
65
|
+
console.log(JSON.stringify(status, null, 2));
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(error.message);
|
|
68
|
+
if (!signal.aborted) process.exitCode = 1;
|
|
69
|
+
} finally {
|
|
70
|
+
command?.close();
|
|
71
|
+
const warning = setTimeout(() => console.error('Native voice cleanup is still pending. Keeping this terminal host alive.'), 10_000);
|
|
72
|
+
const results = await Promise.allSettled([stopChecks(), command ? command.waitForExit().then(exited => {
|
|
73
|
+
if (!exited) throw new Error('Native terminal helper exit could not be confirmed.');
|
|
74
|
+
}) : Promise.resolve()]);
|
|
75
|
+
clearTimeout(warning);
|
|
76
|
+
if (results.some(result => result.status === 'rejected')) {
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
console.error('Native voice cleanup could not be confirmed. Keeping this terminal host alive; no further operation will start.');
|
|
79
|
+
// Keep signal handlers and the host alive rather than abandon owned children.
|
|
80
|
+
await new Promise(() => {});
|
|
81
|
+
}
|
|
82
|
+
clearInterval(hold);
|
|
83
|
+
process.removeListener('SIGINT', onSignal); process.removeListener('SIGTERM', onSignal);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
88
|
+
const flags = process.argv.slice(2);
|
|
89
|
+
const action = ({ '--install': 'setup', '--login': 'login', '--check': 'status' })[flags[0]];
|
|
90
|
+
if (flags.length !== 1 || !action) {
|
|
91
|
+
console.error('Usage: node voice/realtime/setup.mjs --install | --login | --check'); process.exitCode = 2;
|
|
92
|
+
} else {
|
|
93
|
+
void voiceSetup(action).catch(error => { console.error(error.message); process.exitCode = 1; });
|
|
94
|
+
}
|
|
32
95
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"automatic.d.ts","sourceRoot":"","sources":["../../src/memory/automatic.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAGlD,KAAK,KAAK,GAAG,IAAI,CAAC,iBAAiB,EAAE,OAAO,GAAG,iBAAiB,GAAG,mBAAmB,GAAG,OAAO,CAAC,CAAC;AAClG,UAAU,UAAU;IAClB,OAAO,IAAI,OAAO,CAAC;IAAC,IAAI,IAAI,OAAO,CAAC;IAAC,GAAG,IAAI,MAAM,CAAC;IACnD,IAAI,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IAC3D,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3C,QAAQ,CAAC,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IAC7C,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,OAAO,CAAC,IAAI,MAAM,CAAC;IACnB,UAAU,CAAC,IAAI,OAAO,CAAC;IACvB,QAAQ,CAAC,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,OAAO,CAAC,IAAI,MAAM,CAAC;IACnB,SAAS,CAAC,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAGD;;;iFAGiF;AACjF,cAAM,WAAW;IAMH,OAAO,CAAC,GAAG;IALvB,KAAK,QAAa;IAClB,OAAO,CAAC,QAAQ,CAAC,CAAsB;IACvC,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,IAAI,CAAC,CAAkB;IAC/B,OAAO,CAAC,GAAG,CAAC,CAAgB;gBACR,GAAG,EAAE,UAAU;IAC7B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAW3B,KAAK;IAIC,IAAI;IACV,KAAK;YACS,GAAG;CAmDlB;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,UAAU,eAAkC;AAOnF,wBAAgB,uBAAuB,SAStC;AACD,wBAAsB,sBAAsB,kBAG3C;AACD,wBAAsB,uBAAuB,kBAA4B;AACzE,wBAAgB,iBAAiB,IAAI,iBAAiB,CAUrD;AACD,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,iBAAiB,CAW9D"}
|
package/dist/memory/automatic.js
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createAutoIndexer = createAutoIndexer;
|
|
4
|
-
exports.startBackgroundIndexing = startBackgroundIndexing;
|
|
5
|
-
exports.stopBackgroundIndexing = stopBackgroundIndexing;
|
|
6
|
-
exports.yieldBackgroundIndexing = yieldBackgroundIndexing;
|
|
7
|
-
exports.autoIndexSettings = autoIndexSettings;
|
|
8
|
-
exports.setAutoIndex = setAutoIndex;
|
|
9
|
-
const schema_1 = require("../db/schema");
|
|
10
|
-
const stamps_1 = require("./stamps");
|
|
11
|
-
const collector_1 = require("../sync/collector");
|
|
12
|
-
const store_1 = require("../sync/store");
|
|
13
|
-
const automatic_1 = require("../sync/automatic");
|
|
14
|
-
const service_1 = require("./service");
|
|
15
|
-
const CHECK_MS = 60_000, RETRY_MS = 5 * 60_000, BATCH = 64;
|
|
16
|
-
const initial = () => ({ phase: 'off', pending_records: 0, last_completed_at: null, error: '' });
|
|
17
|
-
/** One enabled pipeline: source discovery, local search and a ready briefing.
|
|
18
|
-
* Full batches start at 64 changes or waiting entries; interrupted work resumes.
|
|
19
|
-
* Smaller new tails wait for foreground preparation, with no timed flush.
|
|
20
|
-
* Provider failures require explicit retry; cancellation retains cached work. */
|
|
21
|
-
class AutoIndexer {
|
|
22
|
-
ops;
|
|
23
|
-
state = initial();
|
|
24
|
-
baseline;
|
|
25
|
-
retryAt = 0;
|
|
26
|
-
life;
|
|
27
|
-
job;
|
|
28
|
-
constructor(ops) {
|
|
29
|
-
this.ops = ops;
|
|
30
|
-
}
|
|
31
|
-
async tick() {
|
|
32
|
-
if (this.job)
|
|
33
|
-
return this.job;
|
|
34
|
-
if (!this.ops.enabled()) {
|
|
35
|
-
this.state.phase = 'off';
|
|
36
|
-
return Promise.resolve();
|
|
37
|
-
}
|
|
38
|
-
if (!this.ops.idle()) {
|
|
39
|
-
this.state.phase = 'paused';
|
|
40
|
-
return Promise.resolve();
|
|
41
|
-
}
|
|
42
|
-
const problem = this.ops.problem?.();
|
|
43
|
-
if (problem) {
|
|
44
|
-
this.state.phase = 'error';
|
|
45
|
-
this.state.error = problem;
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
if (this.ops.now() < this.retryAt)
|
|
49
|
-
return Promise.resolve();
|
|
50
|
-
const life = new AbortController();
|
|
51
|
-
this.life = life;
|
|
52
|
-
this.job = this.run(life.signal).finally(() => { this.life = undefined; this.job = undefined; });
|
|
53
|
-
return this.job;
|
|
54
|
-
}
|
|
55
|
-
pause() {
|
|
56
|
-
this.retryAt = Math.max(this.retryAt, this.ops.now() + CHECK_MS);
|
|
57
|
-
this.life?.abort();
|
|
58
|
-
this.state.phase = this.life ? 'stopping' : this.ops.enabled() ? 'paused' : 'off';
|
|
59
|
-
}
|
|
60
|
-
async stop() { this.pause(); await this.job; }
|
|
61
|
-
retry() { this.retryAt = 0; this.state.error = ''; if (!this.job)
|
|
62
|
-
this.state.phase = this.ops.enabled() ? 'waiting' : 'off'; }
|
|
63
|
-
async run(signal) {
|
|
64
|
-
try {
|
|
65
|
-
this.state.phase = 'scanning';
|
|
66
|
-
this.state.error = '';
|
|
67
|
-
const scanned = await this.ops.scan(signal);
|
|
68
|
-
if (signal.aborted || !this.ops.enabled())
|
|
69
|
-
return;
|
|
70
|
-
if (scanned.warnings.length)
|
|
71
|
-
throw new Error('Scan incomplete');
|
|
72
|
-
const before = new Date(this.ops.now()).toISOString();
|
|
73
|
-
const baseline = this.ops.baseline ? this.ops.baseline() : this.baseline;
|
|
74
|
-
if (this.ops.baseline && !baseline && !this.ops.briefing) {
|
|
75
|
-
this.state.phase = 'waiting';
|
|
76
|
-
this.state.pending_records = 0;
|
|
77
|
-
this.state.error = 'Prepare history once with a foreground call or unfiltered history search to establish the background checkpoint for this older index.';
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
const current = this.ops.stamp(before);
|
|
81
|
-
let changed = 0;
|
|
82
|
-
for (const [key, value] of current)
|
|
83
|
-
if (baseline?.get(key) !== value)
|
|
84
|
-
changed++;
|
|
85
|
-
if (baseline)
|
|
86
|
-
for (const key of baseline.keys())
|
|
87
|
-
if (!current.has(key))
|
|
88
|
-
changed++;
|
|
89
|
-
const waiting = this.ops.waiting?.() || 0, unfinished = this.ops.unfinished?.() || false;
|
|
90
|
-
this.state.pending_records = Math.max(changed, waiting);
|
|
91
|
-
if (changed < BATCH && waiting < BATCH && !unfinished) {
|
|
92
|
-
this.state.phase = 'waiting';
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
if (!this.ops.idle()) {
|
|
96
|
-
this.state.phase = 'paused';
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
if (changed >= BATCH || !baseline) {
|
|
100
|
-
this.state.phase = 'indexing';
|
|
101
|
-
await this.ops.prepare(before, signal);
|
|
102
|
-
this.baseline = current;
|
|
103
|
-
}
|
|
104
|
-
if (signal.aborted || !this.ops.enabled())
|
|
105
|
-
return;
|
|
106
|
-
if (this.ops.briefing && (waiting > 0 || unfinished)) {
|
|
107
|
-
if (!this.ops.idle())
|
|
108
|
-
throw new store_1.HttpError(409, 'Foreground work has priority');
|
|
109
|
-
this.state.phase = 'briefing';
|
|
110
|
-
await this.ops.briefing(signal);
|
|
111
|
-
}
|
|
112
|
-
if (signal.aborted || !this.ops.enabled())
|
|
113
|
-
return;
|
|
114
|
-
const completed = new Date(this.ops.now()).toISOString();
|
|
115
|
-
this.ops.completed?.(completed);
|
|
116
|
-
this.state = { phase: 'waiting', pending_records: 0, last_completed_at: completed, error: '' };
|
|
117
|
-
}
|
|
118
|
-
catch (error) {
|
|
119
|
-
const problem = this.ops.problem?.();
|
|
120
|
-
if (!signal.aborted && problem) {
|
|
121
|
-
this.state.phase = 'error';
|
|
122
|
-
this.state.error = problem;
|
|
123
|
-
}
|
|
124
|
-
else if (!signal.aborted && error instanceof store_1.HttpError && error.status === 409) {
|
|
125
|
-
this.state.phase = 'paused';
|
|
126
|
-
this.retryAt = this.ops.now() + CHECK_MS;
|
|
127
|
-
}
|
|
128
|
-
else if (!signal.aborted) {
|
|
129
|
-
this.state.phase = 'error';
|
|
130
|
-
this.state.error = 'Automatic preparation could not finish. Check session sources and model availability. Completed work is retained; local preparation retries in five minutes.';
|
|
131
|
-
this.retryAt = this.ops.now() + RETRY_MS;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
finally {
|
|
135
|
-
if (signal.aborted || !this.ops.enabled())
|
|
136
|
-
this.state.phase = this.ops.enabled() ? 'paused' : 'off';
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
function createAutoIndexer(ops) { return new AutoIndexer(ops); }
|
|
141
|
-
// Old local-index-only consent must not silently become paid model consent on
|
|
142
|
-
// another installation. Fresh databases seed this combined scope at creation;
|
|
143
|
-
// existing databases and explicit choices are never defaulted by this getter.
|
|
144
|
-
const enabled = () => (0, schema_1.getDb)().prepare("SELECT value FROM sync_settings WHERE key='auto_prepare_conversations'").get()?.value === 'true';
|
|
145
|
-
let worker, timer;
|
|
146
|
-
function startBackgroundIndexing() {
|
|
147
|
-
if (timer)
|
|
148
|
-
return;
|
|
149
|
-
worker = createAutoIndexer({ enabled, idle: () => !store_1.busySyncs.size && !store_1.activeVoiceSyncs.size && !(0, service_1.historyProgress)().active && !(0, service_1.historyProgress)().queued,
|
|
150
|
-
now: Date.now, scan: collector_1.scanSessions, stamp: before => (0, stamps_1.sourceStamps)((0, schema_1.getDb)(), before), baseline: () => (0, stamps_1.loadSourceCheckpoint)((0, schema_1.getDataDir)()), prepare: (before, signal) => (0, service_1.prepareHistory)(before, signal, true),
|
|
151
|
-
waiting: store_1.availableCount, unfinished: () => (0, automatic_1.pendingAutomaticBriefing)() !== undefined, briefing: automatic_1.prepareAutomaticBriefing, problem: automatic_1.automaticBriefingError,
|
|
152
|
-
completed: at => { (0, schema_1.getDb)().prepare("INSERT INTO sync_settings(key,value) VALUES ('auto_last_completed',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(at); } });
|
|
153
|
-
timer = setInterval(() => {
|
|
154
|
-
void worker?.tick().catch(() => {
|
|
155
|
-
if (worker) {
|
|
156
|
-
worker.state.phase = 'error';
|
|
157
|
-
worker.state.error = 'Background indexing is unavailable. Check server storage and settings.';
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
}, CHECK_MS);
|
|
161
|
-
timer.unref();
|
|
162
|
-
}
|
|
163
|
-
async function stopBackgroundIndexing() {
|
|
164
|
-
clearInterval(timer);
|
|
165
|
-
timer = undefined;
|
|
166
|
-
const prior = worker;
|
|
167
|
-
worker = undefined;
|
|
168
|
-
await prior?.stop();
|
|
169
|
-
}
|
|
170
|
-
async function yieldBackgroundIndexing() { await worker?.stop(); }
|
|
171
|
-
function autoIndexSettings() {
|
|
172
|
-
const on = enabled(), syncId = [...store_1.backgroundSyncs][0] ?? (0, automatic_1.pendingAutomaticBriefing)() ?? null;
|
|
173
|
-
const progress = syncId === null ? null : (0, schema_1.getDb)().prepare('SELECT stage,processed_entries AS completed,total_entries AS total,invocations,max_invocations FROM sync_runs WHERE sync_id=? ORDER BY id DESC LIMIT 1').get(syncId);
|
|
174
|
-
const prepared = (0, store_1.automaticSyncId)(true), problem = on ? (0, automatic_1.automaticBriefingError)() : '';
|
|
175
|
-
const last = (0, schema_1.getDb)().prepare("SELECT value FROM sync_settings WHERE key='auto_last_completed'").get();
|
|
176
|
-
return { enabled: on, check_seconds: CHECK_MS / 1000, batch_records: BATCH,
|
|
177
|
-
pending_entries: (0, store_1.availableCount)(), sync_id: syncId, ready_sync_id: prepared !== undefined && (0, service_1.historyReadiness)(true).ready ? prepared : null, progress: progress || null,
|
|
178
|
-
...(worker?.state || initial()), last_completed_at: worker?.state.last_completed_at || last?.value || null, ...(!on && worker?.state.phase !== 'stopping' ? { phase: 'off' }
|
|
179
|
-
: on && (!worker || worker.state.phase === 'off') ? { phase: 'waiting' } : {}),
|
|
180
|
-
...(problem ? { phase: 'error', error: problem } : {}) };
|
|
181
|
-
}
|
|
182
|
-
function setAutoIndex(value) {
|
|
183
|
-
if (typeof value !== 'boolean')
|
|
184
|
-
throw new store_1.HttpError(400, 'enabled must be a boolean');
|
|
185
|
-
(0, schema_1.getDb)().transaction(() => {
|
|
186
|
-
const save = (0, schema_1.getDb)().prepare('INSERT INTO sync_settings(key,value) VALUES (?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value');
|
|
187
|
-
for (const key of ['auto_index', 'auto_prepare_conversations'])
|
|
188
|
-
save.run(key, String(value));
|
|
189
|
-
})();
|
|
190
|
-
startBackgroundIndexing();
|
|
191
|
-
if (!value)
|
|
192
|
-
worker.pause();
|
|
193
|
-
else {
|
|
194
|
-
(0, automatic_1.retryAutomaticBriefing)();
|
|
195
|
-
worker.retry();
|
|
196
|
-
}
|
|
197
|
-
// The periodic server timer owns work; neither GET nor rendering starts a job.
|
|
198
|
-
return autoIndexSettings();
|
|
199
|
-
}
|
|
200
|
-
//# sourceMappingURL=automatic.js.map
|