@phnx-labs/agents-cli 1.22.25 → 1.22.26
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 +183 -0
- package/README.md +17 -2
- package/dist/bin/agents +0 -0
- package/dist/browser.js +14 -4
- package/dist/commands/apply.js +52 -8
- package/dist/commands/browser.js +35 -0
- package/dist/commands/doctor.js +8 -0
- package/dist/commands/insights.d.ts +25 -19
- package/dist/commands/insights.js +107 -33
- package/dist/commands/reconnect.d.ts +46 -0
- package/dist/commands/reconnect.js +109 -0
- package/dist/commands/routines.js +2 -2
- package/dist/commands/secrets.d.ts +2 -8
- package/dist/commands/secrets.js +29 -105
- package/dist/commands/sessions.js +4 -0
- package/dist/commands/setup-secrets.d.ts +1 -0
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/setup.d.ts +26 -3
- package/dist/commands/setup.js +105 -46
- package/dist/commands/teams.d.ts +6 -0
- package/dist/commands/teams.js +43 -0
- package/dist/commands/trends.d.ts +8 -0
- package/dist/commands/trends.js +10 -156
- package/dist/index.js +1 -1
- package/dist/lib/agents.d.ts +11 -0
- package/dist/lib/agents.js +29 -2
- package/dist/lib/analytics/dashboard.d.ts +10 -6
- package/dist/lib/analytics/dashboard.js +6 -4
- package/dist/lib/analytics/mix-commands.d.ts +53 -0
- package/dist/lib/analytics/mix-commands.js +229 -0
- package/dist/lib/analytics/recipes.d.ts +19 -14
- package/dist/lib/analytics/recipes.js +4 -2
- package/dist/lib/browser/ipc.d.ts +26 -0
- package/dist/lib/browser/ipc.js +139 -24
- package/dist/lib/browser/profiles.d.ts +11 -0
- package/dist/lib/browser/profiles.js +1 -1
- package/dist/lib/browser/stream.d.ts +14 -0
- package/dist/lib/browser/stream.js +71 -0
- package/dist/lib/channels/owner-sink.d.ts +27 -0
- package/dist/lib/channels/owner-sink.js +93 -0
- package/dist/lib/devices/doctor-findings.d.ts +7 -1
- package/dist/lib/devices/doctor-findings.js +33 -1
- package/dist/lib/fleet/apply.d.ts +59 -3
- package/dist/lib/fleet/apply.js +183 -6
- package/dist/lib/fleet/types.d.ts +21 -2
- package/dist/lib/hooks/cache.js +15 -0
- package/dist/lib/hosts/passthrough.d.ts +23 -0
- package/dist/lib/hosts/passthrough.js +45 -0
- package/dist/lib/hosts/ready.d.ts +2 -0
- package/dist/lib/hosts/ready.js +10 -1
- package/dist/lib/hosts/reconnect.d.ts +14 -12
- package/dist/lib/hosts/reconnect.js +41 -40
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/routines.js +14 -2
- package/dist/lib/runner.d.ts +0 -3
- package/dist/lib/runner.js +1 -14
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/push.d.ts +94 -0
- package/dist/lib/secrets/push.js +145 -0
- package/dist/lib/secrets/reaper.d.ts +15 -1
- package/dist/lib/secrets/reaper.js +30 -3
- package/dist/lib/session/db.d.ts +21 -3
- package/dist/lib/session/db.js +221 -13
- package/dist/lib/session/discover.d.ts +1 -0
- package/dist/lib/session/discover.js +115 -19
- package/dist/lib/session/insights.d.ts +18 -0
- package/dist/lib/session/insights.js +143 -1
- package/dist/lib/session/tool-index.js +133 -22
- package/dist/lib/session/tool-store.d.ts +26 -2
- package/dist/lib/session/tool-store.js +36 -17
- package/dist/lib/ssh-exec.js +8 -2
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +4 -0
- package/dist/lib/teams/agents.d.ts +13 -0
- package/dist/lib/teams/agents.js +75 -7
- package/dist/lib/teams/placement-probe.d.ts +21 -0
- package/dist/lib/teams/placement-probe.js +135 -0
- package/dist/lib/teams/scheduler.d.ts +74 -1
- package/dist/lib/teams/scheduler.js +187 -10
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import { StringDecoder } from 'string_decoder';
|
|
3
|
-
import { getDB } from './db.js';
|
|
3
|
+
import { getDB, maintainSessionSearchIndex } from './db.js';
|
|
4
4
|
import { parseSession } from './parse.js';
|
|
5
5
|
import { TOOL_INDEX_VERSION, TOOL_INDEX_LIMIT_ORDINAL, ToolCallCollector, collectClaudeToolCalls, collectCodexToolCalls, toolCallEvidenceBytes, toolCallsFromEvents, } from './tool-calls.js';
|
|
6
|
-
import { persistToolCalls, purgeToolCalls, toolEvidenceSourcePath, } from './tool-store.js';
|
|
6
|
+
import { canonicalToolLedgerPath, persistToolCalls, purgeToolCalls, toolEvidenceSourcePath, } from './tool-store.js';
|
|
7
7
|
const BACKFILL_MAX_FILES = 25;
|
|
8
8
|
const BACKFILL_MAX_BYTES = 16 * 1024 * 1024;
|
|
9
9
|
const BACKFILL_MAX_IN_MEMORY_SOURCE_BYTES = 16 * 1024 * 1024;
|
|
@@ -39,16 +39,71 @@ export function toolSearchRemoteReceiveBudget(envelope) {
|
|
|
39
39
|
const localBytes = serializedToolSearchEnvelopeBytes(envelope);
|
|
40
40
|
return Math.max(0, TOOL_QUERY_MAX_SERIALIZED_BYTES - TOOL_QUERY_MERGE_OVERHEAD_BYTES - localBytes);
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The ledger columns every candidate session is judged on. Deliberately NOT
|
|
44
|
+
* `parser_state`: this runs once per session in the scan's warm path, and that
|
|
45
|
+
* column holds a serialized collector snapshot that can reach a megabyte. It is
|
|
46
|
+
* read separately, only for the sessions that turn out to need indexing.
|
|
47
|
+
*/
|
|
48
|
+
function readToolLedger(db, sessionId) {
|
|
49
|
+
return db.prepare(`
|
|
50
|
+
SELECT file_path, file_mtime_ms, file_size, extractor_version, parsed_offset
|
|
45
51
|
FROM tool_scan_ledger WHERE session_id = ?
|
|
46
52
|
`).get(sessionId);
|
|
53
|
+
}
|
|
54
|
+
function readToolParserState(db, sessionId) {
|
|
55
|
+
const row = db.prepare(`SELECT parser_state FROM tool_scan_ledger WHERE session_id = ?`)
|
|
56
|
+
.get(sessionId);
|
|
57
|
+
return row?.parser_state ?? null;
|
|
58
|
+
}
|
|
59
|
+
function needsIndex(row, stamp) {
|
|
47
60
|
return !row
|
|
48
61
|
|| row.file_mtime_ms !== stamp.fileMtimeMs
|
|
49
62
|
|| row.file_size !== stamp.fileSize
|
|
50
63
|
|| row.extractor_version !== TOOL_INDEX_VERSION;
|
|
51
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Where to start reading a session whose transcript changed.
|
|
67
|
+
*
|
|
68
|
+
* A live session's transcript is append-only, so re-reading it from byte 0 on
|
|
69
|
+
* every scan re-parses the entire history to discover the handful of records
|
|
70
|
+
* that are new — the cost that makes a large session's tool index quadratic in
|
|
71
|
+
* the number of scans. When the ledger carries a resume point that the current
|
|
72
|
+
* file still agrees with, the scan reads only the appended bytes and merges the
|
|
73
|
+
* result (`append`); anything else re-reads the whole file (`replace`).
|
|
74
|
+
*
|
|
75
|
+
* Each check below rejects a case where the stored prefix may no longer describe
|
|
76
|
+
* the file: a harness the streaming parser cannot resume, a different extractor,
|
|
77
|
+
* no recorded resume point, a source path the ledger row does not describe, a
|
|
78
|
+
* file that shrank below what was already parsed (a rewrite or truncation, not
|
|
79
|
+
* an append), or a snapshot that does not read back.
|
|
80
|
+
*/
|
|
81
|
+
function planToolScan(db, sessionId, row, sourcePath, stamp, resumable) {
|
|
82
|
+
const full = { mode: 'replace', startOffset: 0 };
|
|
83
|
+
if (!resumable || !row)
|
|
84
|
+
return full;
|
|
85
|
+
if (row.extractor_version !== TOOL_INDEX_VERSION)
|
|
86
|
+
return full;
|
|
87
|
+
if (row.parsed_offset === null)
|
|
88
|
+
return full;
|
|
89
|
+
if (row.file_path !== canonicalToolLedgerPath(sourcePath))
|
|
90
|
+
return full;
|
|
91
|
+
if (stamp.fileSize < row.file_size || stamp.fileSize < row.parsed_offset)
|
|
92
|
+
return full;
|
|
93
|
+
const parserState = readToolParserState(db, sessionId);
|
|
94
|
+
if (parserState === null)
|
|
95
|
+
return full;
|
|
96
|
+
let snapshot;
|
|
97
|
+
try {
|
|
98
|
+
snapshot = JSON.parse(parserState);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return full;
|
|
102
|
+
}
|
|
103
|
+
if (snapshot?.v !== 1 || !Number.isSafeInteger(snapshot.nextOrdinal))
|
|
104
|
+
return full;
|
|
105
|
+
return { mode: 'append', startOffset: row.parsed_offset, snapshot };
|
|
106
|
+
}
|
|
52
107
|
/** Read index completeness from SQLite only; never stat or parse transcripts. */
|
|
53
108
|
export function readToolIndexCoverage(sessions) {
|
|
54
109
|
const db = getDB();
|
|
@@ -90,14 +145,23 @@ function backfillLimitCall(session, reason) {
|
|
|
90
145
|
};
|
|
91
146
|
}
|
|
92
147
|
/** Stream Claude/Codex JSONL without ever retaining an oversized record. */
|
|
93
|
-
async function streamJsonlToolCalls(session) {
|
|
94
|
-
const collector = new ToolCallCollector();
|
|
95
|
-
const stream = fs.createReadStream(session.filePath, {
|
|
148
|
+
async function streamJsonlToolCalls(session, from = { startOffset: 0 }) {
|
|
149
|
+
const collector = new ToolCallCollector(from.snapshot);
|
|
150
|
+
const stream = fs.createReadStream(session.filePath, {
|
|
151
|
+
highWaterMark: 64 * 1024,
|
|
152
|
+
start: from.startOffset,
|
|
153
|
+
});
|
|
96
154
|
const decoder = new StringDecoder('utf8');
|
|
97
155
|
let pending = '';
|
|
98
156
|
let pendingBytes = 0;
|
|
99
157
|
let droppingOversizedLine = false;
|
|
100
158
|
let skippedOversizedLine = false;
|
|
159
|
+
// Byte offset just past the last complete record applied. Only a complete,
|
|
160
|
+
// newline-terminated record advances it, so resuming here can never re-apply a
|
|
161
|
+
// record (which would mint a second ordinal for it) nor skip a partial tail.
|
|
162
|
+
let parsedOffset = from.startOffset;
|
|
163
|
+
/** Bytes of the record currently being assembled, across chunk boundaries. */
|
|
164
|
+
let lineBytes = 0;
|
|
101
165
|
const applyLine = (line) => {
|
|
102
166
|
if (!line.trim())
|
|
103
167
|
return;
|
|
@@ -119,8 +183,14 @@ async function streamJsonlToolCalls(session) {
|
|
|
119
183
|
const newline = text.indexOf('\n', start);
|
|
120
184
|
const end = newline >= 0 ? newline : text.length;
|
|
121
185
|
const segment = text.slice(start, end);
|
|
186
|
+
const segmentBytes = Buffer.byteLength(segment);
|
|
187
|
+
// Counted outside the drop guard and across chunk boundaries: this is the
|
|
188
|
+
// record's true size on disk, which is what the resume offset is measured
|
|
189
|
+
// in. `pendingBytes` cannot stand in for it — that one resets when an
|
|
190
|
+
// oversized record is dropped, and a record split over two 64 KiB reads
|
|
191
|
+
// would lose the part carried in from the previous chunk.
|
|
192
|
+
lineBytes += segmentBytes;
|
|
122
193
|
if (!droppingOversizedLine) {
|
|
123
|
-
const segmentBytes = Buffer.byteLength(segment);
|
|
124
194
|
if (pendingBytes + segmentBytes <= BACKFILL_MAX_JSONL_RECORD_BYTES) {
|
|
125
195
|
pending += segment;
|
|
126
196
|
pendingBytes += segmentBytes;
|
|
@@ -136,6 +206,8 @@ async function streamJsonlToolCalls(session) {
|
|
|
136
206
|
break;
|
|
137
207
|
if (!droppingOversizedLine)
|
|
138
208
|
applyLine(pending);
|
|
209
|
+
parsedOffset += lineBytes + 1; // + the newline itself
|
|
210
|
+
lineBytes = 0;
|
|
139
211
|
pending = '';
|
|
140
212
|
pendingBytes = 0;
|
|
141
213
|
droppingOversizedLine = false;
|
|
@@ -145,25 +217,48 @@ async function streamJsonlToolCalls(session) {
|
|
|
145
217
|
for await (const chunk of stream)
|
|
146
218
|
consume(decoder.write(chunk));
|
|
147
219
|
consume(decoder.end());
|
|
220
|
+
// Snapshot BEFORE the unterminated trailing record, and pair it with an offset
|
|
221
|
+
// that stops short of that record. The writer may be mid-append, so the record
|
|
222
|
+
// is indexed now (its evidence is real) but is re-read by the next scan — which
|
|
223
|
+
// resumes with the same next-ordinal and so re-derives the same ordinals,
|
|
224
|
+
// making the re-read an idempotent upsert rather than a duplicate.
|
|
225
|
+
const resume = skippedOversizedLine
|
|
226
|
+
// A dropped oversized record left the ordinals and the pending map out of
|
|
227
|
+
// step with the file; nothing here can be resumed from.
|
|
228
|
+
? null
|
|
229
|
+
: { parserState: JSON.stringify(collector.snapshot()), parsedOffset };
|
|
148
230
|
if (!droppingOversizedLine && pending.length > 0)
|
|
149
231
|
applyLine(pending);
|
|
150
232
|
const calls = collector.drainChanged();
|
|
151
233
|
if (skippedOversizedLine && !calls.some((call) => call.ordinal === TOOL_INDEX_LIMIT_ORDINAL)) {
|
|
152
234
|
calls.push(backfillLimitCall(session, 'At least one JSONL record exceeded the 1 MiB tool-backfill parser limit.'));
|
|
153
235
|
}
|
|
154
|
-
return calls;
|
|
236
|
+
return { calls, resume };
|
|
237
|
+
}
|
|
238
|
+
/** True for the harnesses whose transcript the streaming parser can resume. */
|
|
239
|
+
function isResumableToolSource(agent) {
|
|
240
|
+
return agent === 'claude' || agent === 'codex';
|
|
155
241
|
}
|
|
156
|
-
async function toolCallsForBackfill(session, sourceBytes) {
|
|
157
|
-
if (session.agent
|
|
242
|
+
async function toolCallsForBackfill(session, sourceBytes, from = { startOffset: 0 }) {
|
|
243
|
+
if (isResumableToolSource(session.agent)) {
|
|
158
244
|
if (sourceBytes > BACKFILL_MAX_STREAM_SOURCE_BYTES) {
|
|
159
|
-
return
|
|
245
|
+
return {
|
|
246
|
+
calls: [backfillLimitCall(session, 'Transcript exceeds the 64 MiB safe streaming tool-backfill limit.')],
|
|
247
|
+
resume: null,
|
|
248
|
+
};
|
|
160
249
|
}
|
|
161
|
-
return streamJsonlToolCalls(session);
|
|
250
|
+
return streamJsonlToolCalls(session, from);
|
|
162
251
|
}
|
|
163
252
|
if (sourceBytes > BACKFILL_MAX_IN_MEMORY_SOURCE_BYTES) {
|
|
164
|
-
return
|
|
253
|
+
return {
|
|
254
|
+
calls: [backfillLimitCall(session, 'Transcript exceeds the 16 MiB safe in-memory tool-backfill parser limit.')],
|
|
255
|
+
resume: null,
|
|
256
|
+
};
|
|
165
257
|
}
|
|
166
|
-
|
|
258
|
+
// Every other harness is parsed whole into memory by parseSession, which
|
|
259
|
+
// exposes no byte offset to resume from — so these stay full replaces and
|
|
260
|
+
// record no resume point, rather than storing one this path cannot honour.
|
|
261
|
+
return { calls: toolCallsFromEvents(parseSession(session.filePath, session.agent)), resume: null };
|
|
167
262
|
}
|
|
168
263
|
/**
|
|
169
264
|
* Fill one bounded chunk of the independent tool index. A warm call performs
|
|
@@ -179,6 +274,7 @@ export async function ensureToolIndex(sessions, limits = {}) {
|
|
|
179
274
|
if (!session.filePath)
|
|
180
275
|
continue;
|
|
181
276
|
const sourcePath = toolEvidenceSourcePath(session.filePath, session.agent);
|
|
277
|
+
const ledger = readToolLedger(db, session.id);
|
|
182
278
|
const mustStatSource = limits.verifySourceStamps || sourcePath !== session.filePath;
|
|
183
279
|
const indexed = !mustStatSource
|
|
184
280
|
? db.prepare(`
|
|
@@ -199,8 +295,15 @@ export async function ensureToolIndex(sessions, limits = {}) {
|
|
|
199
295
|
continue;
|
|
200
296
|
}
|
|
201
297
|
}
|
|
202
|
-
if (needsIndex(
|
|
203
|
-
|
|
298
|
+
if (!needsIndex(ledger, stamp))
|
|
299
|
+
continue;
|
|
300
|
+
const plan = planToolScan(db, session.id, ledger, sourcePath, stamp, isResumableToolSource(session.agent));
|
|
301
|
+
pending.push({
|
|
302
|
+
session,
|
|
303
|
+
stamp,
|
|
304
|
+
plan,
|
|
305
|
+
readBytes: Math.max(0, stamp.fileSize - plan.startOffset),
|
|
306
|
+
});
|
|
204
307
|
}
|
|
205
308
|
let indexedFiles = 0;
|
|
206
309
|
let indexedCalls = 0;
|
|
@@ -212,13 +315,16 @@ export async function ensureToolIndex(sessions, limits = {}) {
|
|
|
212
315
|
// The byte budget is a batch boundary, not a correctness boundary. Admit
|
|
213
316
|
// one oversized transcript by itself so it can never wedge the ledger or
|
|
214
317
|
// silently disappear from results; the next invocation resumes afterward.
|
|
215
|
-
|
|
318
|
+
// Budgeted on the bytes this scan reads, not the file's size: a resumed
|
|
319
|
+
// session costs only its appended tail, so a batch can cover far more
|
|
320
|
+
// growing sessions than it could when every one was re-read whole.
|
|
321
|
+
if (attemptedFiles > 0 && consumedBytes + item.readBytes > maxBytes)
|
|
216
322
|
break;
|
|
217
323
|
attemptedFiles++;
|
|
218
|
-
consumedBytes += item.
|
|
324
|
+
consumedBytes += item.readBytes;
|
|
219
325
|
try {
|
|
220
|
-
const calls = await toolCallsForBackfill(item.session, item.stamp.fileSize);
|
|
221
|
-
persistToolCalls(db, item.session, calls, item.stamp);
|
|
326
|
+
const { calls, resume } = await toolCallsForBackfill(item.session, item.stamp.fileSize, item.plan);
|
|
327
|
+
persistToolCalls(db, item.session, calls, item.stamp, { mode: item.plan.mode, resume });
|
|
222
328
|
indexedFiles++;
|
|
223
329
|
indexedCalls += calls.length;
|
|
224
330
|
}
|
|
@@ -226,6 +332,11 @@ export async function ensureToolIndex(sessions, limits = {}) {
|
|
|
226
332
|
skippedFiles++;
|
|
227
333
|
}
|
|
228
334
|
}
|
|
335
|
+
// The scan just wrote a batch of FTS segments; pay a bounded slice of the
|
|
336
|
+
// merge they need so the index converges here instead of degrading until
|
|
337
|
+
// someone runs `agents sessions optimize` by hand (RUSH-2208).
|
|
338
|
+
if (indexedFiles > 0)
|
|
339
|
+
maintainSessionSearchIndex(db);
|
|
229
340
|
const remainingFiles = Math.max(0, pending.length - attemptedFiles);
|
|
230
341
|
const limitedSessionIds = new Set();
|
|
231
342
|
const sessionIds = sessions.map((session) => session.id);
|
|
@@ -8,8 +8,32 @@ export declare function toolEvidenceSourcePath(filePath: string, agent: string):
|
|
|
8
8
|
export declare function purgeToolCalls(db: Database.Database, sessionId: string): void;
|
|
9
9
|
/** Purge deleted direct children when a transcript directory's stamp changes. */
|
|
10
10
|
export declare function purgeMissingToolCallsInDirectory(db: Database.Database, dirPath: string, currentFilePaths: string[]): number;
|
|
11
|
-
/**
|
|
11
|
+
/** The resume point a later incremental scan starts from. */
|
|
12
|
+
export interface ToolScanResumePoint {
|
|
13
|
+
/** Serialized ToolCallCollector snapshot at `parsedOffset`. */
|
|
14
|
+
parserState: string;
|
|
15
|
+
/** Byte offset just past the last complete record consumed. */
|
|
16
|
+
parsedOffset: number;
|
|
17
|
+
}
|
|
18
|
+
export interface PersistToolCallsOptions {
|
|
19
|
+
/**
|
|
20
|
+
* `replace` drops the session's stored evidence first — correct for a parse
|
|
21
|
+
* that started at byte 0. `append` merges the batch into what is already
|
|
22
|
+
* stored and requires an existing ledger row; use it only for a parse that
|
|
23
|
+
* resumed from that row's `parsedOffset`.
|
|
24
|
+
*/
|
|
25
|
+
mode?: 'replace' | 'append';
|
|
26
|
+
/**
|
|
27
|
+
* Where a later scan may resume. Omitted (or null) clears any stored resume
|
|
28
|
+
* point, which forces the next scan of this session to re-read from byte 0 —
|
|
29
|
+
* the correct outcome whenever the parse could not cover the whole prefix
|
|
30
|
+
* (an oversized record, a size-capped transcript, a non-streaming harness).
|
|
31
|
+
*/
|
|
32
|
+
resume?: ToolScanResumePoint | null;
|
|
33
|
+
maxSessionBytes?: number;
|
|
34
|
+
}
|
|
35
|
+
/** Persist one parser batch, its file stamp, and its resume point atomically. */
|
|
12
36
|
export declare function persistToolCalls(db: Database.Database, session: SessionMeta, calls: IndexedToolCall[], sourceStamp: {
|
|
13
37
|
fileMtimeMs: number;
|
|
14
38
|
fileSize: number;
|
|
15
|
-
},
|
|
39
|
+
}, options?: PersistToolCallsOptions): void;
|
|
@@ -28,11 +28,17 @@ export function toolEvidenceSourcePath(filePath, agent) {
|
|
|
28
28
|
return filePath;
|
|
29
29
|
}
|
|
30
30
|
function deleteSessionCalls(db, sessionId) {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
const rows = db.prepare(`SELECT rowid, call_key FROM tool_calls WHERE session_id = ?`)
|
|
32
|
+
.all(sessionId);
|
|
33
|
+
const deletePrograms = db.prepare(`DELETE FROM tool_call_programs WHERE call_key = ?`);
|
|
34
|
+
const deleteOccurrences = db.prepare(`DELETE FROM tool_program_occurrences WHERE call_key = ?`);
|
|
35
|
+
// Addressed by rowid, never by the UNINDEXED call_key — see the tool_call_text
|
|
36
|
+
// schema comment in db.ts. A call_key predicate scans the whole FTS index.
|
|
37
|
+
const deleteText = db.prepare(`DELETE FROM tool_call_text WHERE rowid = ?`);
|
|
38
|
+
for (const { rowid, call_key } of rows) {
|
|
39
|
+
deletePrograms.run(call_key);
|
|
40
|
+
deleteOccurrences.run(call_key);
|
|
41
|
+
deleteText.run(rowid);
|
|
36
42
|
}
|
|
37
43
|
db.prepare(`DELETE FROM tool_calls WHERE session_id = ?`).run(sessionId);
|
|
38
44
|
}
|
|
@@ -61,8 +67,11 @@ export function purgeMissingToolCallsInDirectory(db, dirPath, currentFilePaths)
|
|
|
61
67
|
}
|
|
62
68
|
return purged;
|
|
63
69
|
}
|
|
64
|
-
/** Persist one parser batch
|
|
65
|
-
export function persistToolCalls(db, session, calls, sourceStamp,
|
|
70
|
+
/** Persist one parser batch, its file stamp, and its resume point atomically. */
|
|
71
|
+
export function persistToolCalls(db, session, calls, sourceStamp, options = {}) {
|
|
72
|
+
const mode = options.mode ?? 'replace';
|
|
73
|
+
const maxSessionBytes = options.maxSessionBytes ?? TOOL_SESSION_EVIDENCE_MAX_BYTES;
|
|
74
|
+
const resume = options.resume ?? null;
|
|
66
75
|
const sourcePath = toolEvidenceSourcePath(session.filePath, session.agent);
|
|
67
76
|
const insertCall = db.prepare(`
|
|
68
77
|
INSERT INTO tool_calls (
|
|
@@ -89,16 +98,19 @@ export function persistToolCalls(db, session, calls, sourceStamp, mode = 'replac
|
|
|
89
98
|
INSERT INTO tool_program_occurrences (call_key, occurrence_ordinal, program, role)
|
|
90
99
|
VALUES (?, ?, ?, ?)
|
|
91
100
|
`);
|
|
92
|
-
|
|
101
|
+
// tool_call_text rows are addressed by the rowid of the tool_calls row they
|
|
102
|
+
// describe (db.ts schema comment): its UNINDEXED call_key cannot be seeked.
|
|
103
|
+
const insertText = db.prepare(`INSERT INTO tool_call_text (rowid, call_key, tool, input, output, error) VALUES (?, ?, ?, ?, ?, ?)`);
|
|
104
|
+
const callRowid = db.prepare(`SELECT rowid FROM tool_calls WHERE call_key = ?`);
|
|
93
105
|
const deletePrograms = db.prepare(`DELETE FROM tool_call_programs WHERE call_key = ?`);
|
|
94
106
|
const deleteOccurrences = db.prepare(`DELETE FROM tool_program_occurrences WHERE call_key = ?`);
|
|
95
|
-
const deleteText = db.prepare(`DELETE FROM tool_call_text WHERE
|
|
107
|
+
const deleteText = db.prepare(`DELETE FROM tool_call_text WHERE rowid = ?`);
|
|
96
108
|
const deleteCall = db.prepare(`DELETE FROM tool_calls WHERE call_key = ?`);
|
|
97
109
|
const writeLedger = db.prepare(`
|
|
98
110
|
INSERT INTO tool_scan_ledger (
|
|
99
111
|
session_id, file_path, file_mtime_ms, file_size, extractor_version, indexed_at, call_count,
|
|
100
|
-
evidence_bytes
|
|
101
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
112
|
+
evidence_bytes, parser_state, parsed_offset
|
|
113
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
102
114
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
103
115
|
file_path = excluded.file_path,
|
|
104
116
|
file_mtime_ms = excluded.file_mtime_ms,
|
|
@@ -106,7 +118,9 @@ export function persistToolCalls(db, session, calls, sourceStamp, mode = 'replac
|
|
|
106
118
|
extractor_version = excluded.extractor_version,
|
|
107
119
|
indexed_at = excluded.indexed_at,
|
|
108
120
|
call_count = excluded.call_count,
|
|
109
|
-
evidence_bytes = excluded.evidence_bytes
|
|
121
|
+
evidence_bytes = excluded.evidence_bytes,
|
|
122
|
+
parser_state = excluded.parser_state,
|
|
123
|
+
parsed_offset = excluded.parsed_offset
|
|
110
124
|
`);
|
|
111
125
|
const txn = db.transaction(() => {
|
|
112
126
|
if (mode === 'replace')
|
|
@@ -119,7 +133,7 @@ export function persistToolCalls(db, session, calls, sourceStamp, mode = 'replac
|
|
|
119
133
|
throw new Error('Append tool evidence requires an existing tool ledger row.');
|
|
120
134
|
}
|
|
121
135
|
if (mode === 'append' && calls.length === 0) {
|
|
122
|
-
writeLedger.run(session.id, ledgerPath, sourceStamp.fileMtimeMs, sourceStamp.fileSize, TOOL_INDEX_VERSION, Date.now(), priorLedger.call_count, priorLedger.evidence_bytes);
|
|
136
|
+
writeLedger.run(session.id, ledgerPath, sourceStamp.fileMtimeMs, sourceStamp.fileSize, TOOL_INDEX_VERSION, Date.now(), priorLedger.call_count, priorLedger.evidence_bytes, resume?.parserState ?? null, resume?.parsedOffset ?? null);
|
|
123
137
|
return;
|
|
124
138
|
}
|
|
125
139
|
const limitKey = toolCallKey(session.id, TOOL_INDEX_LIMIT_ORDINAL);
|
|
@@ -129,9 +143,11 @@ export function persistToolCalls(db, session, calls, sourceStamp, mode = 'replac
|
|
|
129
143
|
const priorLimit = mode === 'append'
|
|
130
144
|
? existingSize.get(session.id, TOOL_INDEX_LIMIT_ORDINAL)
|
|
131
145
|
: undefined;
|
|
146
|
+
const limitRow = callRowid.get(limitKey);
|
|
132
147
|
deletePrograms.run(limitKey);
|
|
133
148
|
deleteOccurrences.run(limitKey);
|
|
134
|
-
|
|
149
|
+
if (limitRow)
|
|
150
|
+
deleteText.run(limitRow.rowid);
|
|
135
151
|
deleteCall.run(limitKey);
|
|
136
152
|
const existingRows = mode === 'append'
|
|
137
153
|
? calls.map((call) => ({
|
|
@@ -182,17 +198,20 @@ export function persistToolCalls(db, session, calls, sourceStamp, mode = 'replac
|
|
|
182
198
|
for (const call of accepted) {
|
|
183
199
|
const key = toolCallKey(session.id, call.ordinal);
|
|
184
200
|
insertCall.run(key, session.id, call.ordinal, call.sourceCallId ?? null, call.timestamp, call.tool, call.input, call.outcome, call.exitCode ?? null, call.statusCode ?? null, call.errorCode ?? null, call.output ?? null, call.error ?? null, call.parseError ?? null, toolCallEvidenceBytes(call));
|
|
201
|
+
// The upsert above preserves the rowid of a call it updated, so this is
|
|
202
|
+
// the same rowid the existing text row (if any) was written under.
|
|
203
|
+
const { rowid } = callRowid.get(key);
|
|
185
204
|
deletePrograms.run(key);
|
|
186
205
|
deleteOccurrences.run(key);
|
|
187
|
-
deleteText.run(
|
|
206
|
+
deleteText.run(rowid);
|
|
188
207
|
for (const program of call.programs)
|
|
189
208
|
insertProgram.run(key, program);
|
|
190
209
|
call.programOccurrences.forEach((occurrence, occurrenceOrdinal) => {
|
|
191
210
|
insertOccurrence.run(key, occurrenceOrdinal, occurrence.program, occurrence.role);
|
|
192
211
|
});
|
|
193
|
-
insertText.run(key, call.tool, call.input, call.output ?? '', call.error ?? '');
|
|
212
|
+
insertText.run(rowid, key, call.tool, call.input, call.output ?? '', call.error ?? '');
|
|
194
213
|
}
|
|
195
|
-
writeLedger.run(session.id, ledgerPath, sourceStamp.fileMtimeMs, sourceStamp.fileSize, TOOL_INDEX_VERSION, Date.now(), count, storedBytes);
|
|
214
|
+
writeLedger.run(session.id, ledgerPath, sourceStamp.fileMtimeMs, sourceStamp.fileSize, TOOL_INDEX_VERSION, Date.now(), count, storedBytes, resume?.parserState ?? null, resume?.parsedOffset ?? null);
|
|
196
215
|
});
|
|
197
216
|
txn();
|
|
198
217
|
}
|
package/dist/lib/ssh-exec.js
CHANGED
|
@@ -117,7 +117,11 @@ export function sshExec(target, remoteCmd, opts = {}) {
|
|
|
117
117
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
118
118
|
windowsHide: true,
|
|
119
119
|
});
|
|
120
|
-
|
|
120
|
+
// Node's spawnSync with a `timeout` option kills the child via SIGTERM and sets
|
|
121
|
+
// res.signal (e.g. 'SIGTERM') — it does NOT set res.error.code = 'ETIMEDOUT'.
|
|
122
|
+
// Check both so the detection fires whether the timeout is signalled or errored.
|
|
123
|
+
const timedOut = !!(res.error && res.error.code === 'ETIMEDOUT') ||
|
|
124
|
+
(!!opts.timeoutMs && res.signal !== null);
|
|
121
125
|
return {
|
|
122
126
|
code: typeof res.status === 'number' ? res.status : null,
|
|
123
127
|
stdout: res.stdout ?? '',
|
|
@@ -206,7 +210,9 @@ export function sshExecRaw(target, remoteCmd, opts = {}) {
|
|
|
206
210
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
207
211
|
windowsHide: true,
|
|
208
212
|
});
|
|
209
|
-
|
|
213
|
+
// Same fix as sshExec: spawnSync sets res.signal, not error.code = 'ETIMEDOUT'.
|
|
214
|
+
const timedOut = !!(res.error && res.error.code === 'ETIMEDOUT') ||
|
|
215
|
+
(!!opts.timeoutMs && res.signal !== null);
|
|
210
216
|
return {
|
|
211
217
|
code: typeof res.status === 'number' ? res.status : null,
|
|
212
218
|
stdout: res.stdout ?? Buffer.alloc(0),
|
|
@@ -47,6 +47,7 @@ export declare const loadMonitors: ModuleLoader;
|
|
|
47
47
|
export declare const loadProjects: ModuleLoader;
|
|
48
48
|
export declare const loadRun: ModuleLoader;
|
|
49
49
|
export declare const loadResume: ModuleLoader;
|
|
50
|
+
export declare const loadReconnect: ModuleLoader;
|
|
50
51
|
export declare const loadFork: ModuleLoader;
|
|
51
52
|
export declare const loadDefaults: ModuleLoader;
|
|
52
53
|
export declare const loadSet: ModuleLoader;
|
|
@@ -25,6 +25,7 @@ export const loadMonitors = async () => (await import('../../commands/monitors.j
|
|
|
25
25
|
export const loadProjects = async () => (await import('../../commands/projects.js')).registerProjectsCommands;
|
|
26
26
|
export const loadRun = async () => (await import('../../commands/exec.js')).registerRunCommand;
|
|
27
27
|
export const loadResume = async () => (await import('../../commands/resume.js')).registerResumeCommand;
|
|
28
|
+
export const loadReconnect = async () => (await import('../../commands/reconnect.js')).registerReconnectCommand;
|
|
28
29
|
export const loadFork = async () => (await import('../../commands/fork.js')).registerForkCommand;
|
|
29
30
|
export const loadDefaults = async () => (await import('../../commands/defaults.js')).registerDefaultsCommands;
|
|
30
31
|
export const loadSet = async () => (await import('../../commands/set.js')).registerSetCommand;
|
|
@@ -53,6 +54,7 @@ export const loadUsage = async () => (await import('../../commands/usage.js')).r
|
|
|
53
54
|
export const loadCost = async () => (await import('../../commands/cost.js')).registerCostCommand;
|
|
54
55
|
export const loadInsights = async () => (await import('../../commands/insights.js')).registerInsightsCommand;
|
|
55
56
|
export const loadPerf = async () => (await import('../../commands/perf.js')).registerPerfCommand;
|
|
57
|
+
// Thin deprecated alias of `agents insights mix` — no second mix implementation.
|
|
56
58
|
export const loadTrends = async () => (await import('../../commands/trends.js')).registerTrendsCommand;
|
|
57
59
|
export const loadOutput = async () => (await import('../../commands/output.js')).registerOutputCommand;
|
|
58
60
|
export const loadBudget = async () => (await import('../../commands/budget.js')).registerBudgetCommand;
|
|
@@ -98,6 +100,7 @@ export const loadHumans = async () => (await import('../../commands/humans.js'))
|
|
|
98
100
|
export const LAZY_COMMAND_NAMES = new Set([
|
|
99
101
|
'sessions',
|
|
100
102
|
'resume',
|
|
103
|
+
'reconnect',
|
|
101
104
|
'roster',
|
|
102
105
|
'teams',
|
|
103
106
|
'cloud',
|
|
@@ -156,6 +159,7 @@ export const COMMAND_LOADERS = {
|
|
|
156
159
|
projects: [loadProjects],
|
|
157
160
|
run: [loadRun],
|
|
158
161
|
resume: [loadResume],
|
|
162
|
+
reconnect: [loadReconnect],
|
|
159
163
|
fork: [loadFork],
|
|
160
164
|
defaults: [loadDefaults],
|
|
161
165
|
set: [loadSet],
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { captureProcessStartTime } from '../platform/index.js';
|
|
2
2
|
import { AgentType } from './parsers.js';
|
|
3
|
+
import { NoViableDeviceError } from './scheduler.js';
|
|
3
4
|
/**
|
|
4
5
|
* Compute the Lowest Common Ancestor (LCA) of multiple file paths.
|
|
5
6
|
* Returns the deepest common directory shared by all paths.
|
|
@@ -405,6 +406,18 @@ export declare class AgentManager {
|
|
|
405
406
|
* teammate schedules identically no matter how it was fired.
|
|
406
407
|
*/
|
|
407
408
|
private maybeSchedulePlacement;
|
|
409
|
+
/** Human label of a teammate's agent for the placement fail-loud message. */
|
|
410
|
+
private placementAgentLabel;
|
|
411
|
+
/**
|
|
412
|
+
* Pre-flight the team pool at `teams start` (RUSH-2002): probe the pool and
|
|
413
|
+
* confirm each distinct agent among the pending, unpinned, pooled teammates
|
|
414
|
+
* has at least one viable device. Returns the first {@link NoViableDeviceError}
|
|
415
|
+
* so the command can fail loud BEFORE launching a wave, rather than silently
|
|
416
|
+
* leaving teammates stranded pending. A poolless team (no `--devices`) or a
|
|
417
|
+
* probe that could not gather positive evidence returns null — fail-loud fires
|
|
418
|
+
* only on proof that no device can run the agent, never on a probe miss.
|
|
419
|
+
*/
|
|
420
|
+
preflightPlacement(taskName: string): Promise<NoViableDeviceError | null>;
|
|
408
421
|
/**
|
|
409
422
|
* One-ssh-per-host batched liveness/exit pre-pass for a team's remote teammates.
|
|
410
423
|
* The supervisor calls this each wave BEFORE listByTask() so the per-teammate
|
package/dist/lib/teams/agents.js
CHANGED
|
@@ -34,7 +34,8 @@ import { resolveRemoteOsSync } from '../hosts/remote-os.js';
|
|
|
34
34
|
import { pullRemoteLogDelta, REMOTE_MIRROR_MAX_BYTES } from '../hosts/progress.js';
|
|
35
35
|
import { createRemoteWorktree, ensureRemoteRepo } from './remoteWorktree.js';
|
|
36
36
|
import { getTeam } from './registry.js';
|
|
37
|
-
import { resolvePlacement,
|
|
37
|
+
import { resolvePlacement, classifyExclusions, NoViableDeviceError } from './scheduler.js';
|
|
38
|
+
import { probePoolSignals } from './placement-probe.js';
|
|
38
39
|
import { readMaxConcurrentCaps } from '../device-config.js';
|
|
39
40
|
import chalk from 'chalk';
|
|
40
41
|
let lastMemoryWarnAt = 0;
|
|
@@ -1756,7 +1757,7 @@ export class AgentManager {
|
|
|
1756
1757
|
* (immediate add-launch) and startReady() (staged launch) so an unpinned pool
|
|
1757
1758
|
* teammate schedules identically no matter how it was fired.
|
|
1758
1759
|
*/
|
|
1759
|
-
async maybeSchedulePlacement(agent, taskName) {
|
|
1760
|
+
async maybeSchedulePlacement(agent, taskName, opts = {}) {
|
|
1760
1761
|
if (agent.hostName || agent.cloudProvider)
|
|
1761
1762
|
return;
|
|
1762
1763
|
const teamMeta = await getTeam(taskName);
|
|
@@ -1765,15 +1766,79 @@ export class AgentManager {
|
|
|
1765
1766
|
const roster = await this.listByTask(taskName);
|
|
1766
1767
|
const pool = teamMeta.devices ?? [];
|
|
1767
1768
|
const maxConcurrent = pool.length > 1 ? readMaxConcurrentCaps(pool) : undefined;
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1769
|
+
// On the start path (opts.probe), gather live signals so the pick is health-,
|
|
1770
|
+
// harness-, and load-aware (RUSH-2002); the add path stays the cap-only
|
|
1771
|
+
// roster count so `teams add` never blocks on an SSH fan-out. Cached per
|
|
1772
|
+
// (pool, agent), so a wave placing many teammates probes the pool once.
|
|
1773
|
+
const signals = opts.probe && pool.length > 0
|
|
1774
|
+
? await probePoolSignals(pool, agent.agentType, { now: Date.now() })
|
|
1775
|
+
: undefined;
|
|
1776
|
+
const placeOpts = {
|
|
1777
|
+
maxConcurrent,
|
|
1778
|
+
signals,
|
|
1779
|
+
agentLabel: this.placementAgentLabel(agent),
|
|
1780
|
+
};
|
|
1781
|
+
if (signals) {
|
|
1782
|
+
for (const e of classifyExclusions(pool, roster, placeOpts).excluded) {
|
|
1783
|
+
const why = e.reason === 'capped'
|
|
1784
|
+
? `at its agents.max-concurrent cap (${e.detail} running)`
|
|
1785
|
+
: e.reason === 'not-installed'
|
|
1786
|
+
? `does not have ${this.placementAgentLabel(agent)} installed`
|
|
1787
|
+
: e.reason;
|
|
1788
|
+
console.error(chalk.dim(`[placement] '${e.device}' excluded from auto-pick — ${why}`));
|
|
1771
1789
|
}
|
|
1772
1790
|
}
|
|
1773
|
-
const { device } = resolvePlacement(teamMeta, null, roster,
|
|
1791
|
+
const { device } = resolvePlacement(teamMeta, null, roster, placeOpts);
|
|
1774
1792
|
if (device)
|
|
1775
1793
|
await this.resolveScheduledPlacement(agent, device, taskName);
|
|
1776
1794
|
}
|
|
1795
|
+
/** Human label of a teammate's agent for the placement fail-loud message. */
|
|
1796
|
+
placementAgentLabel(agent) {
|
|
1797
|
+
return agent.version ? `${agent.agentType}@${agent.version}` : String(agent.agentType);
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* Pre-flight the team pool at `teams start` (RUSH-2002): probe the pool and
|
|
1801
|
+
* confirm each distinct agent among the pending, unpinned, pooled teammates
|
|
1802
|
+
* has at least one viable device. Returns the first {@link NoViableDeviceError}
|
|
1803
|
+
* so the command can fail loud BEFORE launching a wave, rather than silently
|
|
1804
|
+
* leaving teammates stranded pending. A poolless team (no `--devices`) or a
|
|
1805
|
+
* probe that could not gather positive evidence returns null — fail-loud fires
|
|
1806
|
+
* only on proof that no device can run the agent, never on a probe miss.
|
|
1807
|
+
*/
|
|
1808
|
+
async preflightPlacement(taskName) {
|
|
1809
|
+
await this.initialize();
|
|
1810
|
+
const teamMeta = await getTeam(taskName);
|
|
1811
|
+
const pool = teamMeta?.devices ?? [];
|
|
1812
|
+
if (!teamMeta || pool.length === 0)
|
|
1813
|
+
return null;
|
|
1814
|
+
const roster = await this.listByTask(taskName);
|
|
1815
|
+
const pending = roster.filter((a) => a.status === AgentStatus.PENDING && !a.hostName && !a.cloudProvider);
|
|
1816
|
+
if (pending.length === 0)
|
|
1817
|
+
return null;
|
|
1818
|
+
const maxConcurrent = pool.length > 1 ? readMaxConcurrentCaps(pool) : undefined;
|
|
1819
|
+
// One representative teammate per distinct agent type — the signal + gate is
|
|
1820
|
+
// per harness, not per teammate.
|
|
1821
|
+
const byAgent = new Map();
|
|
1822
|
+
for (const a of pending)
|
|
1823
|
+
if (!byAgent.has(a.agentType))
|
|
1824
|
+
byAgent.set(a.agentType, a);
|
|
1825
|
+
for (const [, rep] of byAgent) {
|
|
1826
|
+
const signals = await probePoolSignals(pool, rep.agentType, { now: Date.now() });
|
|
1827
|
+
try {
|
|
1828
|
+
resolvePlacement(teamMeta, null, roster, {
|
|
1829
|
+
maxConcurrent,
|
|
1830
|
+
signals,
|
|
1831
|
+
agentLabel: this.placementAgentLabel(rep),
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
catch (err) {
|
|
1835
|
+
if (err instanceof NoViableDeviceError)
|
|
1836
|
+
return err;
|
|
1837
|
+
throw err;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
return null;
|
|
1841
|
+
}
|
|
1777
1842
|
/**
|
|
1778
1843
|
* One-ssh-per-host batched liveness/exit pre-pass for a team's remote teammates.
|
|
1779
1844
|
* The supervisor calls this each wave BEFORE listByTask() so the per-teammate
|
|
@@ -1866,9 +1931,12 @@ export class AgentManager {
|
|
|
1866
1931
|
// immediate-add and staged paths agree. A null pick keeps hostName null →
|
|
1867
1932
|
// local spawn, unchanged. Cloud teammates never schedule.
|
|
1868
1933
|
try {
|
|
1869
|
-
await this.maybeSchedulePlacement(agent, taskName);
|
|
1934
|
+
await this.maybeSchedulePlacement(agent, taskName, { probe: true });
|
|
1870
1935
|
}
|
|
1871
1936
|
catch (err) {
|
|
1937
|
+
// A NoViableDeviceError means the pool cannot host this teammate right
|
|
1938
|
+
// now — leave it PENDING (never a silent local fallback) and surface the
|
|
1939
|
+
// reason; a transient device loss is retried next wave.
|
|
1872
1940
|
console.error(`Could not schedule ${agent.agentId} onto the team pool:`, err);
|
|
1873
1941
|
continue;
|
|
1874
1942
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type AgentType } from './agents.js';
|
|
2
|
+
import type { DevicePlacementSignal } from './scheduler.js';
|
|
3
|
+
/** Per-remote readiness probe budget — matches the health probe's short window
|
|
4
|
+
* (`agents view` on a warm box is sub-second; a wedged one degrades to unknown). */
|
|
5
|
+
export declare const READY_PROBE_TIMEOUT_MS = 8000;
|
|
6
|
+
/** How long a probed pool snapshot is reused within a `teams start` wave. Short
|
|
7
|
+
* enough that a device coming online / going overloaded is seen next wave, long
|
|
8
|
+
* enough that placing a wave of teammates does not re-fan-out per teammate. */
|
|
9
|
+
export declare const SIGNAL_TTL_MS = 15000;
|
|
10
|
+
/** Clear the probe cache — for tests and after a device-registry change. */
|
|
11
|
+
export declare function clearPlacementSignalCache(): void;
|
|
12
|
+
/**
|
|
13
|
+
* Probe every device in the team pool and return a name→signal map for the pure
|
|
14
|
+
* placement pick. Devices with no data at all are omitted (the pick then neither
|
|
15
|
+
* excludes nor prefers them). Never throws — a probe failure degrades to a
|
|
16
|
+
* missing/partial signal.
|
|
17
|
+
*/
|
|
18
|
+
export declare function probePoolSignals(pool: string[], agent: AgentType, opts?: {
|
|
19
|
+
force?: boolean;
|
|
20
|
+
now?: number;
|
|
21
|
+
}): Promise<Map<string, DevicePlacementSignal>>;
|