@turns/dev 0.0.25 → 0.0.27
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/dist/local-session-sources.js +73 -0
- package/dist/native-inventory-worker.js +58 -0
- package/dist/queue-lifecycle.js +66 -0
- package/dist/queue-worker.js +118 -78
- package/dist/upload-sessions.js +124 -44
- package/package.json +1 -1
|
@@ -294,6 +294,36 @@ function fileDescriptor(harness, file) {
|
|
|
294
294
|
createdAt: (stat.birthtimeMs > 0 ? stat.birthtime : stat.mtime).toISOString(),
|
|
295
295
|
};
|
|
296
296
|
}
|
|
297
|
+
/** Resolve only ordinary transcript files from their authoritative store.
|
|
298
|
+
* Database projections require discovery; never interpret their source IDs as
|
|
299
|
+
* paths or allow a server-provided path outside the selected harness store. */
|
|
300
|
+
export function nativeDescriptorForSource(harness, sourcePath) {
|
|
301
|
+
const store = Object.hasOwn(EXTRA_FILE_STORES, harness) ? EXTRA_FILE_STORES[harness] : null;
|
|
302
|
+
if (!store || !sourcePath || sourcePath.includes('\0'))
|
|
303
|
+
return null;
|
|
304
|
+
const file = sourcePath.startsWith('~/') ? path.join(HOME, sourcePath.slice(2)) : sourcePath;
|
|
305
|
+
if (!path.isAbsolute(file))
|
|
306
|
+
return null;
|
|
307
|
+
const relative = path.relative(store.root, file);
|
|
308
|
+
if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
|
|
309
|
+
return null;
|
|
310
|
+
if (!store.include(file))
|
|
311
|
+
return null;
|
|
312
|
+
// Discovery does not traverse symlinked entries. Keep this direct lookup
|
|
313
|
+
// from admitting a transcript that an inventory walk would never discover.
|
|
314
|
+
try {
|
|
315
|
+
let candidate = store.root;
|
|
316
|
+
for (const part of relative.split(path.sep)) {
|
|
317
|
+
candidate = path.join(candidate, part);
|
|
318
|
+
if (fs.lstatSync(candidate).isSymbolicLink())
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
return harness === 'fx' ? fxDescriptor(file) : fileDescriptor(harness, file);
|
|
326
|
+
}
|
|
297
327
|
/** Fx's ledger path identifies the session; its adjacent projections provide
|
|
298
328
|
* the workspace and display title without requiring a conversation scan. */
|
|
299
329
|
function fxDescriptor(file) {
|
|
@@ -934,6 +964,45 @@ function firstRecords(descriptor, limit = 32, maxBytes = 1024 * 1024) {
|
|
|
934
964
|
return [];
|
|
935
965
|
}
|
|
936
966
|
}
|
|
967
|
+
/** Claude copies a backgrounded session into a new transcript and ends the
|
|
968
|
+
* old one with `{"type":"continued-in","continuedInSessionId":...}`, followed
|
|
969
|
+
* only by small bookkeeping rows. Read just the file's tail for that marker. */
|
|
970
|
+
function claudeContinuedInSessionId(file) {
|
|
971
|
+
let handle;
|
|
972
|
+
try {
|
|
973
|
+
const stat = fs.statSync(file);
|
|
974
|
+
const bytesToRead = Math.min(stat.size, 16 * 1024);
|
|
975
|
+
handle = fs.openSync(file, 'r');
|
|
976
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
977
|
+
const bytesRead = fs.readSync(handle, buffer, 0, buffer.length, stat.size - bytesToRead);
|
|
978
|
+
const text = buffer.subarray(0, bytesRead).toString('utf8');
|
|
979
|
+
if (!text.includes('"continued-in"'))
|
|
980
|
+
return '';
|
|
981
|
+
let continuedIn = '';
|
|
982
|
+
for (const line of text.split('\n')) {
|
|
983
|
+
if (!line.includes('"continued-in"'))
|
|
984
|
+
continue;
|
|
985
|
+
try {
|
|
986
|
+
const record = jsonObject(JSON.parse(line));
|
|
987
|
+
const id = String(record?.continuedInSessionId || '').trim();
|
|
988
|
+
if (record?.type === 'continued-in' && /^[0-9a-f-]{8,64}$/iu.test(id))
|
|
989
|
+
continuedIn = id;
|
|
990
|
+
}
|
|
991
|
+
catch { }
|
|
992
|
+
}
|
|
993
|
+
return continuedIn;
|
|
994
|
+
}
|
|
995
|
+
catch {
|
|
996
|
+
return '';
|
|
997
|
+
}
|
|
998
|
+
finally {
|
|
999
|
+
try {
|
|
1000
|
+
if (handle !== undefined)
|
|
1001
|
+
fs.closeSync(handle);
|
|
1002
|
+
}
|
|
1003
|
+
catch { }
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
937
1006
|
/** Native identity and parentage carried by a bounded transcript prefix. */
|
|
938
1007
|
export function nativeDescriptorLineage(descriptor) {
|
|
939
1008
|
const sourcePath = String(descriptor?.sourcePath || descriptor?.file || '').replaceAll('\\', '/');
|
|
@@ -1012,10 +1081,14 @@ export function nativeDescriptorLineage(descriptor) {
|
|
|
1012
1081
|
}
|
|
1013
1082
|
if (parentNativeSessionId)
|
|
1014
1083
|
isSubagent = true;
|
|
1084
|
+
const continuedInNativeSessionId = descriptor?.harness === 'claude' && !isSubagent && descriptor?.file
|
|
1085
|
+
? claudeContinuedInSessionId(String(descriptor.file))
|
|
1086
|
+
: '';
|
|
1015
1087
|
const value = {
|
|
1016
1088
|
nativeSessionId,
|
|
1017
1089
|
parentNativeSessionId,
|
|
1018
1090
|
isSubagent,
|
|
1091
|
+
...(continuedInNativeSessionId ? { continuedInNativeSessionId } : {}),
|
|
1019
1092
|
...(structural?.uploadable === false ? { uploadable: false } : {}),
|
|
1020
1093
|
...(parentSourcePath ? { parentSourcePath } : {}),
|
|
1021
1094
|
...(claudeMetadata?.description ? { nativeTitle: claudeMetadata.description } : {}),
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { isMainThread, parentPort, Worker, workerData } from 'node:worker_threads';
|
|
2
|
+
import { discoverLocalSessions } from "./local-session-sources.js";
|
|
3
|
+
// Keep the synchronous store scans and their caches off the control socket's
|
|
4
|
+
// event loop. One worker serves all scans, including periodic rediscovery.
|
|
5
|
+
if (!isMainThread && workerData === 'turns-native-inventory') {
|
|
6
|
+
parentPort.on('message', ({ id, only }) => {
|
|
7
|
+
try {
|
|
8
|
+
parentPort.postMessage({ id, sessions: discoverLocalSessions(only) });
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
parentPort.postMessage({ id, error: String(error?.stack || error) });
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export class NativeInventoryWorker {
|
|
16
|
+
worker;
|
|
17
|
+
nextId = 0;
|
|
18
|
+
pending = new Map();
|
|
19
|
+
failure = null;
|
|
20
|
+
constructor(entry = new URL(import.meta.url)) {
|
|
21
|
+
this.worker = new Worker(entry, { workerData: 'turns-native-inventory' });
|
|
22
|
+
this.worker.on('message', ({ id, sessions, error }) => {
|
|
23
|
+
const request = this.pending.get(id);
|
|
24
|
+
if (!request)
|
|
25
|
+
return;
|
|
26
|
+
this.pending.delete(id);
|
|
27
|
+
if (error)
|
|
28
|
+
request.reject(new Error(error));
|
|
29
|
+
else
|
|
30
|
+
request.resolve(sessions);
|
|
31
|
+
if (!this.pending.size)
|
|
32
|
+
this.worker.unref();
|
|
33
|
+
});
|
|
34
|
+
this.worker.on('error', (error) => this.fail(error));
|
|
35
|
+
this.worker.on('exit', (code) => this.fail(new Error(`Native inventory worker exited (${code}).`)));
|
|
36
|
+
this.worker.unref();
|
|
37
|
+
}
|
|
38
|
+
fail(error) {
|
|
39
|
+
this.failure = error;
|
|
40
|
+
for (const request of this.pending.values())
|
|
41
|
+
request.reject(error);
|
|
42
|
+
this.pending.clear();
|
|
43
|
+
}
|
|
44
|
+
scan(only = '') {
|
|
45
|
+
if (this.failure)
|
|
46
|
+
return Promise.reject(this.failure);
|
|
47
|
+
const id = ++this.nextId;
|
|
48
|
+
this.worker.ref();
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
this.pending.set(id, { resolve, reject });
|
|
51
|
+
this.worker.postMessage({ id, only });
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
close() {
|
|
55
|
+
this.fail(new Error('Native inventory stopped.'));
|
|
56
|
+
return this.worker.terminate();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const QUEUE_RESTART_MESSAGE = 'The local daemon restarted before your message was sent. Retrying automatically.';
|
|
2
|
+
/** The synchronous stopping/dispatch gates serialize shutdown against local
|
|
3
|
+
* delivery. Once delivery is uncertain, only anchor recovery may reconcile it. */
|
|
4
|
+
export class QueueDispatchLifecycle {
|
|
5
|
+
stopping = false;
|
|
6
|
+
acquiring = new Set();
|
|
7
|
+
handoffs = new Set();
|
|
8
|
+
claims = new Map();
|
|
9
|
+
stopped = null;
|
|
10
|
+
assertRunning() {
|
|
11
|
+
if (this.stopping)
|
|
12
|
+
throw new Error('The local daemon is restarting.');
|
|
13
|
+
}
|
|
14
|
+
async acquire(request, release) {
|
|
15
|
+
this.assertRunning();
|
|
16
|
+
const pending = request().then((response) => {
|
|
17
|
+
if (response.claim)
|
|
18
|
+
this.claims.set(response.claim, { started: false, release: () => release(response.claim) });
|
|
19
|
+
return response;
|
|
20
|
+
});
|
|
21
|
+
this.acquiring.add(pending);
|
|
22
|
+
try {
|
|
23
|
+
return await pending;
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
this.acquiring.delete(pending);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async dispatch(claim, send) {
|
|
30
|
+
this.assertRunning();
|
|
31
|
+
const held = this.claims.get(claim);
|
|
32
|
+
if (!held)
|
|
33
|
+
throw new Error('Queue dispatch has no tracked claim.');
|
|
34
|
+
held.started = true;
|
|
35
|
+
const pending = send();
|
|
36
|
+
this.handoffs.add(pending);
|
|
37
|
+
try {
|
|
38
|
+
return await pending;
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
this.handoffs.delete(pending);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
forget(claim) {
|
|
45
|
+
this.claims.delete(claim);
|
|
46
|
+
}
|
|
47
|
+
stop() {
|
|
48
|
+
if (this.stopped)
|
|
49
|
+
return this.stopped;
|
|
50
|
+
this.stopping = true;
|
|
51
|
+
this.stopped = (async () => {
|
|
52
|
+
// A claim response can arrive after SIGTERM. Include it before releasing
|
|
53
|
+
// unsent work; new acquire/dispatch calls are already blocked above.
|
|
54
|
+
const releaseUnsent = () => [...this.claims.values()]
|
|
55
|
+
.filter((claim) => !claim.started)
|
|
56
|
+
.map((claim) => {
|
|
57
|
+
claim.releasing ||= Promise.resolve().then(claim.release);
|
|
58
|
+
return claim.releasing;
|
|
59
|
+
});
|
|
60
|
+
// Do not let one stalled claim response delay release of known claims.
|
|
61
|
+
await Promise.allSettled([...releaseUnsent(), ...this.acquiring]);
|
|
62
|
+
await Promise.allSettled([...releaseUnsent(), ...this.handoffs]);
|
|
63
|
+
})();
|
|
64
|
+
return this.stopped;
|
|
65
|
+
}
|
|
66
|
+
}
|
package/dist/queue-worker.js
CHANGED
|
@@ -5,6 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import { Readable } from 'node:stream';
|
|
6
6
|
import { pipeline } from 'node:stream/promises';
|
|
7
7
|
import { logError } from "./logging.js";
|
|
8
|
+
import { QueueDispatchLifecycle, QUEUE_RESTART_MESSAGE } from "./queue-lifecycle.js";
|
|
8
9
|
import { anchorId, connectExistingAgentAnchors, launchAgentAnchor, waitForAgentTurn } from "./agent-anchor-client.js";
|
|
9
10
|
import { createCodexUpdateManager } from "./codex-update.js";
|
|
10
11
|
import { isAsyncQuestionId } from "./codex-async-question-recovery.js";
|
|
@@ -352,7 +353,7 @@ export async function actualHarnessStatus(entry) {
|
|
|
352
353
|
entry.snapshot = snapshot;
|
|
353
354
|
return snapshot.status === 'running' ? 'running' : 'idle';
|
|
354
355
|
}
|
|
355
|
-
export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors, stableSessionId, redactor = DEFAULT_REDACTOR, }) {
|
|
356
|
+
export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors, resolveDescriptor = async (_session) => null, descriptorsReady = Promise.resolve(), lifecycle = new QueueDispatchLifecycle(), stableSessionId, redactor = DEFAULT_REDACTOR, }) {
|
|
356
357
|
const workerId = `${machine}:turns-queue-worker`;
|
|
357
358
|
const workerInstanceId = crypto.randomUUID();
|
|
358
359
|
const baseUrl = httpBaseUrl(options.url);
|
|
@@ -628,6 +629,8 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
628
629
|
const runClaim = async (sessionId, claim, recovery = null) => {
|
|
629
630
|
if (claims.has(sessionId))
|
|
630
631
|
return;
|
|
632
|
+
if (!recovery)
|
|
633
|
+
lifecycle.assertRunning();
|
|
631
634
|
// Publish the optimistic user turn before native-session lookup, attachment
|
|
632
635
|
// download, or a cold anchor launch. The stable event key makes recovery
|
|
633
636
|
// idempotent, and the outbound protocol frame will replace this placeholder.
|
|
@@ -640,9 +643,71 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
640
643
|
let creating = false;
|
|
641
644
|
let cwd = '';
|
|
642
645
|
let entry = recovery?.entry;
|
|
646
|
+
const queued = {
|
|
647
|
+
...claim,
|
|
648
|
+
entry,
|
|
649
|
+
renewTimer: null,
|
|
650
|
+
// Recovery only observes turns that an anchor already accepted. Treat
|
|
651
|
+
// an uncertain recovery failure conservatively for the same reason as a
|
|
652
|
+
// failed live turn: the prompt may already have produced side effects.
|
|
653
|
+
promptStarted: Boolean(recovery),
|
|
654
|
+
};
|
|
655
|
+
claims.set(sessionId, queued);
|
|
656
|
+
const renewClaim = () => request({
|
|
657
|
+
type: 'queue.renew',
|
|
658
|
+
sessionId,
|
|
659
|
+
promptId: claim.prompt.id,
|
|
660
|
+
claimToken: claim.claimToken,
|
|
661
|
+
});
|
|
662
|
+
const beginRenewal = () => {
|
|
663
|
+
if (queued.renewTimer)
|
|
664
|
+
return;
|
|
665
|
+
queued.renewTimer = setInterval(() => {
|
|
666
|
+
if (lifecycle.stopping) {
|
|
667
|
+
stopClaimRenewal(queued);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (entry?.connection.closed) {
|
|
671
|
+
stopClaimRenewal(queued);
|
|
672
|
+
void reportHarness(sessionId, 'stopped', entry).catch(() => { });
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
void renewClaim().catch((error) => logError(`Queue lease renewal failed: ${error?.message || error}\n`));
|
|
676
|
+
// Never renew lifecycle state from an old in-memory snapshot. The
|
|
677
|
+
// anchor may have disconnected since the claim began, which would
|
|
678
|
+
// otherwise resurrect a canonical stopped edge as running.
|
|
679
|
+
if (!entry)
|
|
680
|
+
return;
|
|
681
|
+
void reportActualHarness(sessionId, entry).catch(async () => {
|
|
682
|
+
if (lifecycle.stopping) {
|
|
683
|
+
stopClaimRenewal(queued);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
if (entry.connection.closed) {
|
|
687
|
+
stopClaimRenewal(queued);
|
|
688
|
+
await reportHarness(sessionId, 'stopped', entry).catch(() => { });
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
}, CLAIM_RENEW_MS);
|
|
692
|
+
queued.renewTimer.unref?.();
|
|
693
|
+
};
|
|
694
|
+
beginRenewal();
|
|
695
|
+
// Verify and extend the inherited lease before waiting on a recovered
|
|
696
|
+
// anchor. If the server has already replaced it, normal claiming plus the
|
|
697
|
+
// anchor's prompt idempotency will reconcile the surviving turn instead.
|
|
698
|
+
if (recovery)
|
|
699
|
+
await renewClaim();
|
|
643
700
|
if (!recovery) {
|
|
644
|
-
|
|
645
|
-
let discovered = descriptorMap.get(sessionId);
|
|
701
|
+
let descriptorMap = descriptors();
|
|
702
|
+
let discovered = descriptorMap.get(sessionId) || (await resolveDescriptor(claim.session));
|
|
703
|
+
if (!discovered && claim.session?.sourcePath) {
|
|
704
|
+
await descriptorsReady;
|
|
705
|
+
lifecycle.assertRunning();
|
|
706
|
+
descriptorMap = descriptors();
|
|
707
|
+
discovered =
|
|
708
|
+
descriptorMap.get(sessionId) ||
|
|
709
|
+
[...descriptorMap.values()].find((candidate) => candidate.harness === claim.session.harness && candidate.sourcePath === claim.session.sourcePath);
|
|
710
|
+
}
|
|
646
711
|
const claimedAgentSessionId = String(claim.session?.agentSessionId || '');
|
|
647
712
|
if (!discovered && claimedAgentSessionId) {
|
|
648
713
|
for (const candidate of descriptorMap.values()) {
|
|
@@ -689,54 +754,12 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
689
754
|
// Codex installation leaves a recoverable unsent prompt in the queue.
|
|
690
755
|
if (descriptor.harness === 'codex')
|
|
691
756
|
resolveHarnessCommand('codex');
|
|
757
|
+
lifecycle.assertRunning();
|
|
692
758
|
entry = await usableAnchor(sessionId, descriptor.harness, agentSessionId, Boolean(claim.session?.modelSettings));
|
|
693
759
|
}
|
|
694
760
|
if (!entry)
|
|
695
761
|
throw new Error(`No surviving agent anchor exists for ${sessionId}.`);
|
|
696
|
-
|
|
697
|
-
...claim,
|
|
698
|
-
entry,
|
|
699
|
-
renewTimer: null,
|
|
700
|
-
// Recovery only observes turns that an anchor already accepted. Treat
|
|
701
|
-
// an uncertain recovery failure conservatively for the same reason as a
|
|
702
|
-
// failed live turn: the prompt may already have produced side effects.
|
|
703
|
-
promptStarted: Boolean(recovery),
|
|
704
|
-
};
|
|
705
|
-
claims.set(sessionId, queued);
|
|
706
|
-
const renewClaim = () => request({
|
|
707
|
-
type: 'queue.renew',
|
|
708
|
-
sessionId,
|
|
709
|
-
promptId: claim.prompt.id,
|
|
710
|
-
claimToken: claim.claimToken,
|
|
711
|
-
});
|
|
712
|
-
const beginRenewal = () => {
|
|
713
|
-
if (queued.renewTimer)
|
|
714
|
-
return;
|
|
715
|
-
queued.renewTimer = setInterval(() => {
|
|
716
|
-
if (entry.connection.closed) {
|
|
717
|
-
stopClaimRenewal(queued);
|
|
718
|
-
void reportHarness(sessionId, 'stopped', entry).catch(() => { });
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
void renewClaim().catch((error) => logError(`Queue lease renewal failed: ${error?.message || error}\n`));
|
|
722
|
-
// Never renew lifecycle state from an old in-memory snapshot. The
|
|
723
|
-
// anchor may have disconnected since the claim began, which would
|
|
724
|
-
// otherwise resurrect a canonical stopped edge as running.
|
|
725
|
-
void reportActualHarness(sessionId, entry).catch(async () => {
|
|
726
|
-
if (entry.connection.closed) {
|
|
727
|
-
stopClaimRenewal(queued);
|
|
728
|
-
await reportHarness(sessionId, 'stopped', entry).catch(() => { });
|
|
729
|
-
}
|
|
730
|
-
});
|
|
731
|
-
}, CLAIM_RENEW_MS);
|
|
732
|
-
queued.renewTimer.unref?.();
|
|
733
|
-
};
|
|
734
|
-
beginRenewal();
|
|
735
|
-
// Verify and extend the inherited lease before waiting on a recovered
|
|
736
|
-
// anchor. If the server has already replaced it, normal claiming plus the
|
|
737
|
-
// anchor's prompt idempotency will reconcile the surviving turn instead.
|
|
738
|
-
if (recovery)
|
|
739
|
-
await renewClaim();
|
|
762
|
+
queued.entry = entry;
|
|
740
763
|
const prompt = recovery?.prompt ||
|
|
741
764
|
(await preparePrompt({
|
|
742
765
|
prompt: claim.prompt,
|
|
@@ -761,33 +784,35 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
761
784
|
const eventCursor = entry.connection.eventCursor();
|
|
762
785
|
// Persist before local delivery: either acknowledgement can be lost
|
|
763
786
|
// after acceptance. Uncertain delivery must never trigger automatic replay.
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
787
|
+
await lifecycle.dispatch(claim, async () => {
|
|
788
|
+
queued.promptStarted = true;
|
|
789
|
+
await request({
|
|
790
|
+
type: 'queue.renew',
|
|
791
|
+
sessionId,
|
|
792
|
+
promptId: claim.prompt.id,
|
|
793
|
+
claimToken: claim.claimToken,
|
|
794
|
+
dispatchStarted: true,
|
|
795
|
+
});
|
|
796
|
+
await entry.connection.request('send_message', {
|
|
797
|
+
source: descriptor.harness,
|
|
798
|
+
sessionId,
|
|
799
|
+
agentSessionId,
|
|
800
|
+
cwd,
|
|
801
|
+
transcriptPath: descriptor.file,
|
|
802
|
+
workerId,
|
|
803
|
+
messageId: claim.prompt.id,
|
|
804
|
+
claimToken: claim.claimToken,
|
|
805
|
+
prompt,
|
|
806
|
+
expectedText: claim.prompt.text,
|
|
807
|
+
createSession: creating,
|
|
808
|
+
permissionMode: String(claim.session?.permissionMode || 'bypassPermissions'),
|
|
809
|
+
model: String(claim.session?.model || ''),
|
|
810
|
+
thinkingLevel: String(claim.session?.modelEffort || ''),
|
|
811
|
+
codexHistoryMode: String(claim.session?.codexHistoryMode || ''),
|
|
812
|
+
timeoutMs: QUEUE_TURN_TIMEOUT_MS,
|
|
813
|
+
idleTimeoutMs: QUEUE_WARM_MS,
|
|
814
|
+
}, 30_000);
|
|
771
815
|
});
|
|
772
|
-
await entry.connection.request('send_message', {
|
|
773
|
-
source: descriptor.harness,
|
|
774
|
-
sessionId,
|
|
775
|
-
agentSessionId,
|
|
776
|
-
cwd,
|
|
777
|
-
transcriptPath: descriptor.file,
|
|
778
|
-
workerId,
|
|
779
|
-
messageId: claim.prompt.id,
|
|
780
|
-
claimToken: claim.claimToken,
|
|
781
|
-
prompt,
|
|
782
|
-
expectedText: claim.prompt.text,
|
|
783
|
-
createSession: creating,
|
|
784
|
-
permissionMode: String(claim.session?.permissionMode || 'bypassPermissions'),
|
|
785
|
-
model: String(claim.session?.model || ''),
|
|
786
|
-
thinkingLevel: String(claim.session?.modelEffort || ''),
|
|
787
|
-
codexHistoryMode: String(claim.session?.codexHistoryMode || ''),
|
|
788
|
-
timeoutMs: QUEUE_TURN_TIMEOUT_MS,
|
|
789
|
-
idleTimeoutMs: QUEUE_WARM_MS,
|
|
790
|
-
}, 30_000);
|
|
791
816
|
entry.snapshot = await entry.connection.request('snapshot', {}, 5_000).catch(() => entry.snapshot);
|
|
792
817
|
// Registration after acceptance is safe: the connection retains recent
|
|
793
818
|
// events and the snapshot carries a turn that completed immediately.
|
|
@@ -804,12 +829,19 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
804
829
|
await finishClaim(sessionId, queued, entry, result);
|
|
805
830
|
};
|
|
806
831
|
const tryClaim = async (sessionId) => {
|
|
807
|
-
if (claims.has(sessionId) || claimStarts.has(sessionId))
|
|
832
|
+
if (lifecycle.stopping || claims.has(sessionId) || claimStarts.has(sessionId))
|
|
808
833
|
return;
|
|
809
834
|
claimStarts.add(sessionId);
|
|
810
835
|
let claim;
|
|
811
836
|
try {
|
|
812
|
-
const response = await request({ type: 'queue.claim', sessionId, dispatchTracking: true })
|
|
837
|
+
const response = await lifecycle.acquire(() => request({ type: 'queue.claim', sessionId, dispatchTracking: true }), (held) => request({
|
|
838
|
+
type: 'queue.fail',
|
|
839
|
+
sessionId,
|
|
840
|
+
promptId: held.prompt.id,
|
|
841
|
+
claimToken: held.claimToken,
|
|
842
|
+
error: QUEUE_RESTART_MESSAGE,
|
|
843
|
+
retryable: true,
|
|
844
|
+
}, 2_000));
|
|
813
845
|
claim = response.claim;
|
|
814
846
|
if (!claim)
|
|
815
847
|
return;
|
|
@@ -833,7 +865,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
833
865
|
else if (held?.entry) {
|
|
834
866
|
await reportHarness(sessionId, 'stopped', held.entry).catch(() => { });
|
|
835
867
|
}
|
|
836
|
-
if (claim) {
|
|
868
|
+
if (claim && !lifecycle.stopping) {
|
|
837
869
|
await request({
|
|
838
870
|
type: 'queue.fail',
|
|
839
871
|
sessionId,
|
|
@@ -846,6 +878,8 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
846
878
|
claims.delete(sessionId);
|
|
847
879
|
}
|
|
848
880
|
finally {
|
|
881
|
+
if (claim && !lifecycle.stopping)
|
|
882
|
+
lifecycle.forget(claim);
|
|
849
883
|
claimStarts.delete(sessionId);
|
|
850
884
|
}
|
|
851
885
|
};
|
|
@@ -1359,6 +1393,8 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
1359
1393
|
return fileEditChain;
|
|
1360
1394
|
};
|
|
1361
1395
|
const heartbeat = setInterval(() => {
|
|
1396
|
+
if (lifecycle.stopping)
|
|
1397
|
+
return;
|
|
1362
1398
|
for (const [sessionId, entry] of anchors) {
|
|
1363
1399
|
if (entry.connection.closed)
|
|
1364
1400
|
continue;
|
|
@@ -1367,6 +1403,8 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
1367
1403
|
}, HARNESS_HEARTBEAT_MS);
|
|
1368
1404
|
heartbeat.unref?.();
|
|
1369
1405
|
rpc.onEvent((message) => {
|
|
1406
|
+
if (lifecycle.stopping)
|
|
1407
|
+
return;
|
|
1370
1408
|
if (message.type === 'queue.ready')
|
|
1371
1409
|
void tryClaim(message.sessionId);
|
|
1372
1410
|
if (message.type === 'session.steer')
|
|
@@ -1395,7 +1433,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
1395
1433
|
const registered = new WeakSet();
|
|
1396
1434
|
const registering = new WeakSet();
|
|
1397
1435
|
const registerControl = async (control) => {
|
|
1398
|
-
if (registered.has(control) || registering.has(control))
|
|
1436
|
+
if (lifecycle.stopping || registered.has(control) || registering.has(control))
|
|
1399
1437
|
return;
|
|
1400
1438
|
registering.add(control);
|
|
1401
1439
|
try {
|
|
@@ -1431,8 +1469,10 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
1431
1469
|
void registerControl(control);
|
|
1432
1470
|
});
|
|
1433
1471
|
await registerControl(await rpc.current());
|
|
1434
|
-
|
|
1472
|
+
while (!lifecycle.stopping) {
|
|
1435
1473
|
await backgroundWait(MACHINE_HEARTBEAT_MS);
|
|
1474
|
+
if (lifecycle.stopping)
|
|
1475
|
+
break;
|
|
1436
1476
|
try {
|
|
1437
1477
|
await registerControl(await rpc.current());
|
|
1438
1478
|
const status = await request({ type: 'machine.heartbeat' }, MACHINE_HEARTBEAT_TIMEOUT_MS);
|
package/dist/upload-sessions.js
CHANGED
|
@@ -7,10 +7,12 @@ import process from 'node:process';
|
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import WebSocket from 'ws';
|
|
9
9
|
import { logError, logInfo } from "./logging.js";
|
|
10
|
+
import { NativeInventoryWorker } from "./native-inventory-worker.js";
|
|
11
|
+
import { QueueDispatchLifecycle } from "./queue-lifecycle.js";
|
|
10
12
|
import { streamDeepseekTrajectoryLines } from "./deepseek-trajectories.js";
|
|
11
13
|
import { normalizeAllowedDirectories, workingDirectoryIsAllowed } from "./directory-policy.js";
|
|
12
14
|
import { discoverGitAssociation } from "./git-association.js";
|
|
13
|
-
import { discoverLocalSessions, externallyManagedNativeDescriptorKeys, grokSpawnedSubagentIds, nativeDescriptorAgentSessionId, nativeDescriptorLineage, nativeDescriptorWorkingDirectory, nativeSessionShallowWatchRoots, nativeSessionWatchRoots, } from "./local-session-sources.js";
|
|
15
|
+
import { discoverLocalSessions, externallyManagedNativeDescriptorKeys, grokSpawnedSubagentIds, nativeDescriptorAgentSessionId, nativeDescriptorForSource, nativeDescriptorLineage, nativeDescriptorWorkingDirectory, nativeSessionShallowWatchRoots, nativeSessionWatchRoots, } from "./local-session-sources.js";
|
|
14
16
|
import { compactNativeRaw } from "./native-record-compaction.js";
|
|
15
17
|
import { createPathResolver, pathCandidates } from "./path-resolution.js";
|
|
16
18
|
import { httpBaseUrl, watchQueuedPrompts } from "./queue-worker.js";
|
|
@@ -315,6 +317,19 @@ export function linkNativeSessionParents(machine, descriptors) {
|
|
|
315
317
|
candidates.push(record.descriptor);
|
|
316
318
|
byNativeId.set(key, candidates);
|
|
317
319
|
}
|
|
320
|
+
// A continued transcript names its successor; key it by the successor's
|
|
321
|
+
// store directory so equal native IDs in other projects never match.
|
|
322
|
+
const nativeDirectory = (descriptor) => path.posix.dirname(String(descriptor?.sourcePath || '').replaceAll('\\', '/'));
|
|
323
|
+
const predecessorsBySuccessor = new Map();
|
|
324
|
+
for (const record of records) {
|
|
325
|
+
const successorId = record.lineage.continuedInNativeSessionId;
|
|
326
|
+
if (!successorId || successorId === record.lineage.nativeSessionId)
|
|
327
|
+
continue;
|
|
328
|
+
const key = `${record.descriptor.harness}\0${nativeDirectory(record.descriptor)}\0${successorId}`;
|
|
329
|
+
const candidates = predecessorsBySuccessor.get(key) || [];
|
|
330
|
+
candidates.push(record.descriptor);
|
|
331
|
+
predecessorsBySuccessor.set(key, candidates);
|
|
332
|
+
}
|
|
318
333
|
const grokParentsByChildId = new Map();
|
|
319
334
|
for (const record of records) {
|
|
320
335
|
if (record.descriptor.harness !== 'grok')
|
|
@@ -365,6 +380,10 @@ export function linkNativeSessionParents(machine, descriptors) {
|
|
|
365
380
|
// native upload until its store exposes enough lineage to nest it safely.
|
|
366
381
|
if (isSubagent && !parentSourcePath)
|
|
367
382
|
return [];
|
|
383
|
+
const predecessors = !isSubagent && lineage.nativeSessionId
|
|
384
|
+
? predecessorsBySuccessor.get(`${descriptor.harness}\0${nativeDirectory(descriptor)}\0${lineage.nativeSessionId}`) || []
|
|
385
|
+
: [];
|
|
386
|
+
const predecessor = predecessors.length === 1 ? predecessors[0] : null;
|
|
368
387
|
return [
|
|
369
388
|
{
|
|
370
389
|
...descriptor,
|
|
@@ -377,6 +396,9 @@ export function linkNativeSessionParents(machine, descriptors) {
|
|
|
377
396
|
parentSessionId: stableNativeSessionId(machine, descriptor.harness, parentSourcePath),
|
|
378
397
|
}
|
|
379
398
|
: {}),
|
|
399
|
+
...(predecessor
|
|
400
|
+
? { supersedesSessionId: stableNativeSessionId(machine, descriptor.harness, predecessor.sourcePath) }
|
|
401
|
+
: {}),
|
|
380
402
|
},
|
|
381
403
|
];
|
|
382
404
|
});
|
|
@@ -389,6 +411,8 @@ export function nativeDescriptorFingerprint(descriptor) {
|
|
|
389
411
|
String(descriptor.parentSessionId || ''),
|
|
390
412
|
NATIVE_DESCRIPTOR_VERSION,
|
|
391
413
|
nativeDescriptorMetadataFingerprint(descriptor),
|
|
414
|
+
// Appended only when present so unrelated sessions keep their fingerprint.
|
|
415
|
+
...(descriptor.supersedesSessionId ? [String(descriptor.supersedesSessionId)] : []),
|
|
392
416
|
]);
|
|
393
417
|
}
|
|
394
418
|
function nativeDescriptorMetadataFingerprint(descriptor) {
|
|
@@ -992,6 +1016,7 @@ export async function uploadNativeSession(rpc, machine, descriptor, cursor = {},
|
|
|
992
1016
|
...(descriptor.readOnly === true ? { readOnly: true } : {}),
|
|
993
1017
|
...(agentSessionId ? { agentSessionId } : {}),
|
|
994
1018
|
...(descriptor.parentSessionId ? { parentSessionId: descriptor.parentSessionId } : {}),
|
|
1019
|
+
...(descriptor.supersedesSessionId ? { supersedesSessionId: descriptor.supersedesSessionId } : {}),
|
|
995
1020
|
},
|
|
996
1021
|
});
|
|
997
1022
|
if (opened.type === 'session_tombstoned') {
|
|
@@ -1355,8 +1380,8 @@ export async function filterNativeSessionsByDirectories(sessions, allowedDirecto
|
|
|
1355
1380
|
export function inventoryIsAuthoritative(options) {
|
|
1356
1381
|
return !(options.only || options.rewriteSession || options.onlyDirectory || options.allowedDirectories?.length);
|
|
1357
1382
|
}
|
|
1358
|
-
async function nativeInventory(options, machine) {
|
|
1359
|
-
let sessions = linkNativeSessionParents(machine,
|
|
1383
|
+
async function nativeInventory(options, machine, discover = async (only) => discoverLocalSessions(only)) {
|
|
1384
|
+
let sessions = linkNativeSessionParents(machine, await discover(options.rewriteSession ? '' : options.only));
|
|
1360
1385
|
sessions = await filterNativeSessionsByOnlyDirectory(sessions, options.onlyDirectory);
|
|
1361
1386
|
sessions = await filterNativeSessionsByDirectories(sessions, options.allowedDirectories);
|
|
1362
1387
|
if (!options.rewriteSession)
|
|
@@ -1452,8 +1477,98 @@ export function nativeUploadProgressSnapshot({ machine, descriptors, seen, finge
|
|
|
1452
1477
|
};
|
|
1453
1478
|
}
|
|
1454
1479
|
async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
|
|
1455
|
-
|
|
1456
|
-
const
|
|
1480
|
+
const descriptorRegistry = { sessions: [] };
|
|
1481
|
+
const lifecycle = new QueueDispatchLifecycle();
|
|
1482
|
+
const inventory = options.watch && !options.dryRun ? new NativeInventoryWorker() : null;
|
|
1483
|
+
const scanInventory = () => nativeInventory(options, machine, inventory ? (only) => inventory.scan(only) : undefined);
|
|
1484
|
+
let state;
|
|
1485
|
+
let nativeMonitor = null;
|
|
1486
|
+
// Queue registration and anchor recovery must not wait for historical stores.
|
|
1487
|
+
// The shared transport still owns the sole machine-presence connection.
|
|
1488
|
+
let rpc = options.dryRun
|
|
1489
|
+
? null
|
|
1490
|
+
: options.watch
|
|
1491
|
+
? new SharedUploaderConnection(options, machine)
|
|
1492
|
+
: await connectUploader(options, machine);
|
|
1493
|
+
let resolveDescriptors;
|
|
1494
|
+
const descriptorsReady = new Promise((resolve) => {
|
|
1495
|
+
resolveDescriptors = resolve;
|
|
1496
|
+
});
|
|
1497
|
+
if (options.watch && !options.dryRun) {
|
|
1498
|
+
let shuttingDown = false;
|
|
1499
|
+
const shutdown = async () => {
|
|
1500
|
+
if (shuttingDown)
|
|
1501
|
+
return;
|
|
1502
|
+
shuttingDown = true;
|
|
1503
|
+
// stop() closes the dispatch gate synchronously, before any awaited work.
|
|
1504
|
+
const stopped = lifecycle.stop();
|
|
1505
|
+
void inventory?.close();
|
|
1506
|
+
const deadline = setTimeout(() => process.exit(0), 5_000);
|
|
1507
|
+
try {
|
|
1508
|
+
await stopped;
|
|
1509
|
+
}
|
|
1510
|
+
finally {
|
|
1511
|
+
state?.close();
|
|
1512
|
+
nativeMonitor?.close();
|
|
1513
|
+
rpc.close();
|
|
1514
|
+
clearTimeout(deadline);
|
|
1515
|
+
process.exit(0);
|
|
1516
|
+
}
|
|
1517
|
+
};
|
|
1518
|
+
process.once('SIGTERM', () => {
|
|
1519
|
+
void shutdown();
|
|
1520
|
+
});
|
|
1521
|
+
process.once('SIGINT', () => {
|
|
1522
|
+
void shutdown();
|
|
1523
|
+
});
|
|
1524
|
+
void watchQueuedPrompts({
|
|
1525
|
+
rpc,
|
|
1526
|
+
options,
|
|
1527
|
+
machine,
|
|
1528
|
+
getDescriptors: () => descriptorRegistry.sessions,
|
|
1529
|
+
descriptorsReady,
|
|
1530
|
+
resolveDescriptor: async (session) => {
|
|
1531
|
+
const descriptor = nativeDescriptorForSource(String(session?.harness || ''), String(session?.sourcePath || ''));
|
|
1532
|
+
if (!descriptor)
|
|
1533
|
+
return null;
|
|
1534
|
+
const only = String(options.only || '').toLowerCase();
|
|
1535
|
+
if (only &&
|
|
1536
|
+
!descriptor.harness.toLowerCase().includes(only) &&
|
|
1537
|
+
!descriptor.sourcePath.toLowerCase().includes(only))
|
|
1538
|
+
return null;
|
|
1539
|
+
if (options.rewriteSession &&
|
|
1540
|
+
stableNativeSessionId(machine, descriptor.harness, descriptor.sourcePath) !== options.rewriteSession)
|
|
1541
|
+
return null;
|
|
1542
|
+
// Apply the same workspace policy as discovery before using the fast path.
|
|
1543
|
+
const exact = await filterNativeSessionsByOnlyDirectory([descriptor], options.onlyDirectory);
|
|
1544
|
+
return (await filterNativeSessionsByDirectories(exact, options.allowedDirectories))[0] || null;
|
|
1545
|
+
},
|
|
1546
|
+
lifecycle,
|
|
1547
|
+
stableSessionId: stableNativeSessionId,
|
|
1548
|
+
redactor,
|
|
1549
|
+
}).catch((error) => {
|
|
1550
|
+
if (!lifecycle.stopping)
|
|
1551
|
+
logError(`Queue worker stopped unexpectedly: ${error?.stack || error}\n`);
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
let initialSessions;
|
|
1555
|
+
try {
|
|
1556
|
+
initialSessions = await scanInventory();
|
|
1557
|
+
}
|
|
1558
|
+
catch (error) {
|
|
1559
|
+
if (lifecycle.stopping)
|
|
1560
|
+
return;
|
|
1561
|
+
// A failed initial inventory cannot leave the control worker alive with an
|
|
1562
|
+
// unresolved descriptor barrier and an uploader that has already failed.
|
|
1563
|
+
void lifecycle.stop();
|
|
1564
|
+
rpc?.close();
|
|
1565
|
+
void inventory?.close();
|
|
1566
|
+
throw error;
|
|
1567
|
+
}
|
|
1568
|
+
if (lifecycle.stopping)
|
|
1569
|
+
return;
|
|
1570
|
+
descriptorRegistry.sessions = initialSessions;
|
|
1571
|
+
resolveDescriptors();
|
|
1457
1572
|
const byHarness = new Map();
|
|
1458
1573
|
for (const session of initialSessions)
|
|
1459
1574
|
byHarness.set(session.harness, (byHarness.get(session.harness) || 0) + 1);
|
|
@@ -1462,7 +1577,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
|
|
|
1462
1577
|
logInfo(` ${harness}: ${count}\n`);
|
|
1463
1578
|
if (options.dryRun)
|
|
1464
1579
|
return;
|
|
1465
|
-
|
|
1580
|
+
state = createNativeUploadState(options.nativeStateFile, machine);
|
|
1466
1581
|
const seen = new Map();
|
|
1467
1582
|
const cursors = new Map();
|
|
1468
1583
|
const fingerprints = new Map();
|
|
@@ -1473,7 +1588,6 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
|
|
|
1473
1588
|
const redactionStarted = new Set();
|
|
1474
1589
|
const livePriority = new Set();
|
|
1475
1590
|
const tombstoned = new Set();
|
|
1476
|
-
let nativeMonitor = null;
|
|
1477
1591
|
const authoritativeInventory = inventoryIsAuthoritative(options);
|
|
1478
1592
|
const rememberInventory = (sessions, retainState = false, forceMetadataDirectories = new Set()) => {
|
|
1479
1593
|
const previousFingerprints = new Map(fingerprints);
|
|
@@ -1541,14 +1655,6 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
|
|
|
1541
1655
|
nativeMonitor?.addFiles(sessions.map((descriptor) => descriptor.file));
|
|
1542
1656
|
};
|
|
1543
1657
|
rememberInventory(initialSessions);
|
|
1544
|
-
// A single request-id-multiplexed transport carries ingestion, ownership,
|
|
1545
|
-
// queue control, and live rows. These tasks remain concurrent without
|
|
1546
|
-
// duplicating authentication or machine-presence sockets.
|
|
1547
|
-
let rpc = options.watch ? new SharedUploaderConnection(options, machine) : await connectUploader(options, machine);
|
|
1548
|
-
// Only the shared connection has a `current()` handle, and only the watch path builds one.
|
|
1549
|
-
// Only the shared connection has a `current()` handle, and only the watch path builds one.
|
|
1550
|
-
if (options.watch)
|
|
1551
|
-
await rpc.current();
|
|
1552
1658
|
if (options.watch)
|
|
1553
1659
|
void watchExternalManagement(rpc, machine, () => descriptorRegistry.sessions);
|
|
1554
1660
|
// Paths an agent wrote about are resolved from the rows themselves and
|
|
@@ -1578,18 +1684,6 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
|
|
|
1578
1684
|
}
|
|
1579
1685
|
},
|
|
1580
1686
|
});
|
|
1581
|
-
if (options.watch) {
|
|
1582
|
-
void watchQueuedPrompts({
|
|
1583
|
-
rpc,
|
|
1584
|
-
options,
|
|
1585
|
-
machine,
|
|
1586
|
-
getDescriptors: () => descriptorRegistry.sessions,
|
|
1587
|
-
stableSessionId: stableNativeSessionId,
|
|
1588
|
-
redactor,
|
|
1589
|
-
}).catch((error) => {
|
|
1590
|
-
logError(`Queue worker stopped unexpectedly: ${error?.stack || error}\n`);
|
|
1591
|
-
});
|
|
1592
|
-
}
|
|
1593
1687
|
const totals = { current: 0, uploaded: 0, rows: 0, empty: 0, failed: 0 };
|
|
1594
1688
|
const failures = new Map();
|
|
1595
1689
|
const rewritePending = new Set(options.rewriteSession ? [options.rewriteSession] : []);
|
|
@@ -1797,7 +1891,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
|
|
|
1797
1891
|
break;
|
|
1798
1892
|
settlePass += 1;
|
|
1799
1893
|
if (settlePass >= MAX_NATIVE_SETTLE_PASSES) {
|
|
1800
|
-
const sessions = await
|
|
1894
|
+
const sessions = await scanInventory();
|
|
1801
1895
|
rememberInventory(sessions);
|
|
1802
1896
|
const unsettled = changedSessions();
|
|
1803
1897
|
if (unsettled.length) {
|
|
@@ -1806,7 +1900,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
|
|
|
1806
1900
|
break;
|
|
1807
1901
|
}
|
|
1808
1902
|
await wait(250);
|
|
1809
|
-
const sessions = await
|
|
1903
|
+
const sessions = await scanInventory();
|
|
1810
1904
|
rememberInventory(sessions);
|
|
1811
1905
|
}
|
|
1812
1906
|
state.close();
|
|
@@ -1949,20 +2043,6 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
|
|
|
1949
2043
|
requestSweep();
|
|
1950
2044
|
}, NATIVE_DISCOVERY_INTERVAL_MS);
|
|
1951
2045
|
discoveryTimer.unref?.();
|
|
1952
|
-
const shutdown = () => {
|
|
1953
|
-
state.close();
|
|
1954
|
-
nativeMonitor.close();
|
|
1955
|
-
clearInterval(pollTimer);
|
|
1956
|
-
clearInterval(discoveryTimer);
|
|
1957
|
-
};
|
|
1958
|
-
process.once('SIGTERM', () => {
|
|
1959
|
-
shutdown();
|
|
1960
|
-
process.exit(0);
|
|
1961
|
-
});
|
|
1962
|
-
process.once('SIGINT', () => {
|
|
1963
|
-
shutdown();
|
|
1964
|
-
process.exit(0);
|
|
1965
|
-
});
|
|
1966
2046
|
logInfo(`Watching ${initialSessions.length} native sessions via ${nativeMonitor.count} directories; ` +
|
|
1967
2047
|
`polling known files every ${NATIVE_POLL_INTERVAL_MS / 1000}s and auditing every ` +
|
|
1968
2048
|
`${NATIVE_DISCOVERY_INTERVAL_MS / 1000}s…\n`);
|
|
@@ -1973,7 +2053,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
|
|
|
1973
2053
|
if (discoveryPending) {
|
|
1974
2054
|
discoveryPending = false;
|
|
1975
2055
|
try {
|
|
1976
|
-
initialSessions = await
|
|
2056
|
+
initialSessions = await scanInventory();
|
|
1977
2057
|
const forceMetadataDirectories = new Set(pendingMetadataDirectories);
|
|
1978
2058
|
pendingMetadataDirectories.clear();
|
|
1979
2059
|
rememberInventory(initialSessions, true, forceMetadataDirectories);
|