@phnx-labs/agents-cli 1.22.57 → 1.22.59
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 +294 -0
- package/README.md +29 -0
- package/dist/bootstrap.js +39 -1
- package/dist/commands/accounts.js +7 -3
- package/dist/commands/apply.js +10 -2
- package/dist/commands/fork.d.ts +23 -10
- package/dist/commands/fork.js +115 -58
- package/dist/commands/monitors.js +198 -23
- package/dist/commands/prune.js +5 -3
- package/dist/commands/routines.d.ts +8 -0
- package/dist/commands/routines.js +57 -3
- package/dist/commands/routines.test-fixture.js +5 -0
- package/dist/commands/send.d.ts +2 -1
- package/dist/commands/send.js +7 -5
- package/dist/commands/sessions-picker.d.ts +11 -0
- package/dist/commands/sessions-picker.js +16 -0
- package/dist/commands/sessions-stats.js +37 -5
- package/dist/commands/sessions.js +40 -5
- package/dist/commands/share.d.ts +14 -0
- package/dist/commands/share.js +43 -2
- package/dist/commands/ssh.js +12 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/sync.js +83 -7
- package/dist/commands/traces.js +7 -0
- package/dist/commands/versions.js +12 -4
- package/dist/commands/view.js +7 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -1
- package/dist/lib/account-registry.d.ts +5 -1
- package/dist/lib/account-registry.js +47 -14
- package/dist/lib/accounting/capacity.d.ts +18 -7
- package/dist/lib/accounting/capacity.js +19 -8
- package/dist/lib/accounting/usage-sync.d.ts +29 -1
- package/dist/lib/accounting/usage-sync.js +76 -2
- package/dist/lib/accounting/usage.js +7 -1
- package/dist/lib/auth-mint.d.ts +11 -1
- package/dist/lib/auth-mint.js +21 -6
- package/dist/lib/auto-pull-worker.js +7 -2
- package/dist/lib/browser/ipc.d.ts +8 -0
- package/dist/lib/browser/ipc.js +87 -0
- package/dist/lib/browser/service.d.ts +19 -0
- package/dist/lib/browser/service.js +96 -11
- package/dist/lib/browser/sessions-list.js +10 -1
- package/dist/lib/cloud/rush.d.ts +7 -0
- package/dist/lib/cloud/rush.js +29 -1
- package/dist/lib/daemon/daemon.d.ts +22 -0
- package/dist/lib/daemon/daemon.js +39 -0
- package/dist/lib/daemon/runner.d.ts +3 -0
- package/dist/lib/daemon/runner.js +86 -45
- package/dist/lib/daemon/session-index-service.js +9 -1
- package/dist/lib/daemon/usage-sync-service.d.ts +3 -3
- package/dist/lib/daemon/usage-sync-service.js +14 -8
- package/dist/lib/daemon-services.js +1 -1
- package/dist/lib/daemon-ticks.d.ts +15 -0
- package/dist/lib/daemon-ticks.js +26 -0
- package/dist/lib/device-config.d.ts +5 -1
- package/dist/lib/device-config.js +2 -2
- package/dist/lib/devices/connect.d.ts +17 -8
- package/dist/lib/devices/connect.js +31 -14
- package/dist/lib/devices/health.js +5 -1
- package/dist/lib/devices/pool.d.ts +25 -2
- package/dist/lib/devices/pool.js +32 -2
- package/dist/lib/devices/stats-cache.d.ts +0 -6
- package/dist/lib/devices/stats-cache.js +2 -9
- package/dist/lib/doctor-diff.d.ts +14 -0
- package/dist/lib/doctor-diff.js +120 -9
- package/dist/lib/fleet/manifest.d.ts +17 -0
- package/dist/lib/fleet/manifest.js +26 -0
- package/dist/lib/git.d.ts +38 -0
- package/dist/lib/git.js +58 -0
- package/dist/lib/hooks/install.d.ts +27 -11
- package/dist/lib/hooks/install.js +42 -17
- package/dist/lib/hosts/ready.d.ts +8 -0
- package/dist/lib/hosts/ready.js +13 -2
- package/dist/lib/hosts/reconnect.d.ts +52 -203
- package/dist/lib/hosts/reconnect.js +64 -284
- package/dist/lib/installations/migrate.d.ts +6 -120
- package/dist/lib/installations/migrate.js +27 -259
- package/dist/lib/installations/shims.d.ts +13 -95
- package/dist/lib/installations/shims.js +22 -139
- package/dist/lib/installations/store.js +1 -1
- package/dist/lib/installations/versions.d.ts +43 -133
- package/dist/lib/installations/versions.js +94 -206
- package/dist/lib/monitors/config.d.ts +71 -3
- package/dist/lib/monitors/config.js +100 -12
- package/dist/lib/monitors/pid-watch.d.ts +35 -0
- package/dist/lib/monitors/pid-watch.js +45 -0
- package/dist/lib/monitors/remote.d.ts +18 -0
- package/dist/lib/monitors/remote.js +11 -0
- package/dist/lib/permissions.js +7 -2
- package/dist/lib/plugins/plugins.d.ts +17 -3
- package/dist/lib/plugins/plugins.js +84 -9
- package/dist/lib/plugins/skills.d.ts +8 -1
- package/dist/lib/plugins/skills.js +18 -2
- package/dist/lib/pty-server.d.ts +14 -0
- package/dist/lib/pty-server.js +49 -5
- package/dist/lib/refresh.d.ts +9 -0
- package/dist/lib/refresh.js +3 -1
- package/dist/lib/routine-readiness.d.ts +15 -1
- package/dist/lib/routine-readiness.js +41 -0
- package/dist/lib/sandbox.d.ts +4 -1
- package/dist/lib/sandbox.js +30 -1
- package/dist/lib/secrets/agent.d.ts +80 -225
- package/dist/lib/secrets/agent.js +139 -401
- package/dist/lib/secrets/bundles.d.ts +73 -222
- package/dist/lib/secrets/bundles.js +168 -467
- package/dist/lib/secrets/drivers/rush.js +5 -0
- package/dist/lib/secrets/reaper.d.ts +28 -70
- package/dist/lib/secrets/reaper.js +30 -85
- package/dist/lib/secrets/remote.d.ts +42 -129
- package/dist/lib/secrets/remote.js +55 -173
- package/dist/lib/self-heal/checks/install-staging.d.ts +4 -0
- package/dist/lib/self-heal/checks/install-staging.js +96 -0
- package/dist/lib/self-heal/registry.js +2 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/self-update.d.ts +65 -0
- package/dist/lib/self-update.js +138 -0
- package/dist/lib/session/active.d.ts +13 -1
- package/dist/lib/session/active.js +2 -0
- package/dist/lib/session/cloud.js +5 -0
- package/dist/lib/session/db.d.ts +51 -6
- package/dist/lib/session/db.js +266 -20
- package/dist/lib/session/fork.d.ts +45 -26
- package/dist/lib/session/fork.js +32 -95
- package/dist/lib/session/tool-calls.d.ts +43 -1
- package/dist/lib/session/tool-calls.js +74 -44
- package/dist/lib/session/tool-store.d.ts +33 -2
- package/dist/lib/session/tool-store.js +56 -3
- package/dist/lib/smart-launch.d.ts +6 -0
- package/dist/lib/smart-launch.js +5 -2
- package/dist/lib/staleness/writers/plugins.js +5 -2
- package/dist/lib/staleness/writers/sources.d.ts +5 -0
- package/dist/lib/staleness/writers/sources.js +2 -1
- package/dist/lib/staleness/writers/subagents.js +13 -3
- package/dist/lib/state.d.ts +7 -4
- package/dist/lib/state.js +7 -4
- package/dist/lib/subagents.js +8 -2
- package/dist/lib/sync-status.d.ts +22 -0
- package/dist/lib/sync-status.js +27 -0
- package/dist/lib/sync-umbrella.d.ts +9 -0
- package/dist/lib/sync-umbrella.js +21 -2
- package/dist/lib/teams/scheduler.d.ts +10 -0
- package/dist/lib/teams/scheduler.js +8 -0
- package/dist/lib/traces/insights.d.ts +47 -14
- package/dist/lib/traces/insights.js +92 -21
- package/dist/lib/traces/phenotype.d.ts +23 -3
- package/dist/lib/traces/phenotype.js +72 -24
- package/dist/lib/traces/sync.d.ts +128 -6
- package/dist/lib/traces/sync.js +294 -35
- package/dist/lib/traces/worker-template.js +154 -1
- package/dist/lib/view-types.d.ts +12 -0
- package/package.json +2 -2
|
@@ -10,12 +10,21 @@ export declare const TOOL_CHANGED_MAX_CALLS = 10000;
|
|
|
10
10
|
export declare const TOOL_INDEX_LIMIT_ORDINAL: number;
|
|
11
11
|
export declare const TOOL_TEXT_PROCESSING_MAX_BYTES: number;
|
|
12
12
|
export declare const TOOL_SHELL_PARSE_MAX_BYTES: number;
|
|
13
|
-
export declare const TOOL_INDEX_VERSION =
|
|
13
|
+
export declare const TOOL_INDEX_VERSION = 8;
|
|
14
14
|
export type ToolCallOutcome = 'ok' | 'error' | 'unknown';
|
|
15
15
|
export interface IndexedToolCall {
|
|
16
16
|
ordinal: number;
|
|
17
17
|
sourceCallId?: string;
|
|
18
18
|
timestamp: string;
|
|
19
|
+
/**
|
|
20
|
+
* When the call's RESULT record arrived — the call's own end time, taken from
|
|
21
|
+
* the tool_result transcript record at `finish()` (PHNX-3437). `timestamp` is
|
|
22
|
+
* the start; `endTimestamp - timestamp` is the call's own blocking duration,
|
|
23
|
+
* which the traces insight engine attributes as a failed call's wasted time.
|
|
24
|
+
* Undefined for a call that never produced a result (still pending at scan end)
|
|
25
|
+
* and for rows produced by an older extractor.
|
|
26
|
+
*/
|
|
27
|
+
endTimestamp?: string;
|
|
19
28
|
tool: string;
|
|
20
29
|
programs: string[];
|
|
21
30
|
programOccurrences: ShellProgramOccurrence[];
|
|
@@ -81,6 +90,39 @@ export declare class ToolCallCollector {
|
|
|
81
90
|
private removePending;
|
|
82
91
|
private markChanged;
|
|
83
92
|
}
|
|
93
|
+
/** The prior scan's resume point for an append-only event stream. */
|
|
94
|
+
export interface EventToolScanResumePoint {
|
|
95
|
+
/** ToolCallCollector snapshot after folding `eventCount` events. */
|
|
96
|
+
snapshot: ToolCallCollectorSnapshot;
|
|
97
|
+
/** How many events had been folded when the snapshot was taken. */
|
|
98
|
+
eventCount: number;
|
|
99
|
+
}
|
|
100
|
+
export interface EventToolScanResult {
|
|
101
|
+
/** The CHANGED calls — an append-safe upsert set, not the whole history. */
|
|
102
|
+
calls: IndexedToolCall[];
|
|
103
|
+
/** Serialized-ready snapshot to persist for the next incremental scan. */
|
|
104
|
+
snapshot: ToolCallCollectorSnapshot;
|
|
105
|
+
/** Events folded so far — the next scan's resume offset into `events`. */
|
|
106
|
+
eventCount: number;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Derive tool calls from a full-file harness's normalized events, optionally
|
|
110
|
+
* RESUMING from a prior scan of the same append-only stream.
|
|
111
|
+
*
|
|
112
|
+
* Without `prior` this is a full parse from event 0 (identical to the legacy
|
|
113
|
+
* {@link toolCallsFromEvents}). With `prior`, the collector is seeded from the
|
|
114
|
+
* prior snapshot and only events at or after `prior.eventCount` are folded — so
|
|
115
|
+
* an active session that grew by a few turns re-derives (and re-redacts) only
|
|
116
|
+
* those new tool calls instead of re-sanitizing its entire history on every
|
|
117
|
+
* daemon warm tick (PHNX-3411). The ordinals continue deterministically from the
|
|
118
|
+
* snapshot, so folding [0..k) then [k..n) yields the same index as folding
|
|
119
|
+
* [0..n) once, and the CHANGED set is safe to persist with `mode: 'append'`.
|
|
120
|
+
*
|
|
121
|
+
* The caller is responsible for only supplying `prior` when the stream is still
|
|
122
|
+
* an append of what was scanned before (same source, un-truncated,
|
|
123
|
+
* `prior.eventCount <= events.length`); anything else must full-scan.
|
|
124
|
+
*/
|
|
125
|
+
export declare function scanEventToolCalls(events: SessionEvent[], prior?: EventToolScanResumePoint): EventToolScanResult;
|
|
84
126
|
/** Build indexed calls from the normalized parser contract used by full-file harnesses. */
|
|
85
127
|
export declare function toolCallsFromEvents(events: SessionEvent[]): IndexedToolCall[];
|
|
86
128
|
export declare function toolCallKey(sessionId: string, ordinal: number): string;
|
|
@@ -12,7 +12,10 @@ export const TOOL_CHANGED_MAX_CALLS = 10_000;
|
|
|
12
12
|
export const TOOL_INDEX_LIMIT_ORDINAL = Number.MAX_SAFE_INTEGER;
|
|
13
13
|
export const TOOL_TEXT_PROCESSING_MAX_BYTES = 64 * 1024;
|
|
14
14
|
export const TOOL_SHELL_PARSE_MAX_BYTES = 64 * 1024;
|
|
15
|
-
|
|
15
|
+
// Bumped to 8 for the per-call end timestamp (PHNX-3437): the extractor now
|
|
16
|
+
// records when a call's result arrived, so a re-index re-derives it for rows
|
|
17
|
+
// stored by an older extractor.
|
|
18
|
+
export const TOOL_INDEX_VERSION = 8;
|
|
16
19
|
const BASE64_BLOCK = /(?:[A-Za-z0-9+/]{256,}={0,2})/g;
|
|
17
20
|
const SECRET_FIELD = /(?:token|secret|password|authorization|cookie|api[_-]?key|private[_-]?key)$/i;
|
|
18
21
|
const KNOWN_SECRET_VALUES = knownSecretValuesFromEnv();
|
|
@@ -310,7 +313,7 @@ function buildCall(ordinal, timestamp, tool, args, command, sourceCallId) {
|
|
|
310
313
|
}
|
|
311
314
|
export function toolCallEvidenceBytes(call) {
|
|
312
315
|
return Buffer.byteLength([
|
|
313
|
-
call.sourceCallId, call.timestamp, call.tool, call.input, call.errorCode,
|
|
316
|
+
call.sourceCallId, call.timestamp, call.endTimestamp, call.tool, call.input, call.errorCode,
|
|
314
317
|
call.output, call.error, call.parseError, ...call.programs,
|
|
315
318
|
...call.programOccurrences.map((occurrence) => `${occurrence.role}:${occurrence.program}`),
|
|
316
319
|
].filter((value) => typeof value === 'string').join('\0'));
|
|
@@ -363,6 +366,9 @@ export class ToolCallCollector {
|
|
|
363
366
|
const call = this.takePending(args.sourceCallId, tool);
|
|
364
367
|
if (!call)
|
|
365
368
|
return undefined;
|
|
369
|
+
if (typeof args.timestamp === 'string' && args.timestamp.length > 0) {
|
|
370
|
+
call.endTimestamp = sanitizeToolEvidenceText(args.timestamp, 128);
|
|
371
|
+
}
|
|
366
372
|
const outcome = args.outcome === 'ok' || args.outcome === 'error' || args.outcome === 'unknown'
|
|
367
373
|
? args.outcome
|
|
368
374
|
: undefined;
|
|
@@ -471,50 +477,74 @@ export class ToolCallCollector {
|
|
|
471
477
|
return true;
|
|
472
478
|
}
|
|
473
479
|
}
|
|
480
|
+
/** Fold one normalized event into a collector (the full-file harness contract). */
|
|
481
|
+
function foldEventIntoCollector(collector, event) {
|
|
482
|
+
if (event.type === 'tool_use') {
|
|
483
|
+
collector.start({
|
|
484
|
+
timestamp: event.timestamp,
|
|
485
|
+
tool: event.tool || 'unknown',
|
|
486
|
+
input: event.args,
|
|
487
|
+
command: event.command,
|
|
488
|
+
sourceCallId: event.callId,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
else if (event.type === 'tool_result') {
|
|
492
|
+
collector.finish({
|
|
493
|
+
timestamp: event.timestamp,
|
|
494
|
+
sourceCallId: event.callId,
|
|
495
|
+
tool: event.tool,
|
|
496
|
+
success: event.success,
|
|
497
|
+
outcome: event.outcome,
|
|
498
|
+
exitCode: event.exitCode,
|
|
499
|
+
statusCode: event.statusCode,
|
|
500
|
+
errorCode: event.errorCode,
|
|
501
|
+
output: event.output,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
else if (event.type === 'error' && event.tool) {
|
|
505
|
+
const structuredError = event.outcome === 'error' || event.success === false;
|
|
506
|
+
const evidence = event.content || event.output || 'Tool execution failed';
|
|
507
|
+
collector.finish({
|
|
508
|
+
timestamp: event.timestamp,
|
|
509
|
+
sourceCallId: event.callId,
|
|
510
|
+
tool: event.tool,
|
|
511
|
+
success: structuredError ? false : undefined,
|
|
512
|
+
outcome: event.outcome,
|
|
513
|
+
exitCode: event.exitCode,
|
|
514
|
+
statusCode: event.statusCode,
|
|
515
|
+
errorCode: event.errorCode,
|
|
516
|
+
error: structuredError ? evidence : undefined,
|
|
517
|
+
output: structuredError ? undefined : evidence,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Derive tool calls from a full-file harness's normalized events, optionally
|
|
523
|
+
* RESUMING from a prior scan of the same append-only stream.
|
|
524
|
+
*
|
|
525
|
+
* Without `prior` this is a full parse from event 0 (identical to the legacy
|
|
526
|
+
* {@link toolCallsFromEvents}). With `prior`, the collector is seeded from the
|
|
527
|
+
* prior snapshot and only events at or after `prior.eventCount` are folded — so
|
|
528
|
+
* an active session that grew by a few turns re-derives (and re-redacts) only
|
|
529
|
+
* those new tool calls instead of re-sanitizing its entire history on every
|
|
530
|
+
* daemon warm tick (PHNX-3411). The ordinals continue deterministically from the
|
|
531
|
+
* snapshot, so folding [0..k) then [k..n) yields the same index as folding
|
|
532
|
+
* [0..n) once, and the CHANGED set is safe to persist with `mode: 'append'`.
|
|
533
|
+
*
|
|
534
|
+
* The caller is responsible for only supplying `prior` when the stream is still
|
|
535
|
+
* an append of what was scanned before (same source, un-truncated,
|
|
536
|
+
* `prior.eventCount <= events.length`); anything else must full-scan.
|
|
537
|
+
*/
|
|
538
|
+
export function scanEventToolCalls(events, prior) {
|
|
539
|
+
const startIndex = prior ? Math.min(prior.eventCount, events.length) : 0;
|
|
540
|
+
const collector = new ToolCallCollector(prior?.snapshot);
|
|
541
|
+
for (let i = startIndex; i < events.length; i++)
|
|
542
|
+
foldEventIntoCollector(collector, events[i]);
|
|
543
|
+
return { calls: collector.drainChanged(), snapshot: collector.snapshot(), eventCount: events.length };
|
|
544
|
+
}
|
|
474
545
|
/** Build indexed calls from the normalized parser contract used by full-file harnesses. */
|
|
475
546
|
export function toolCallsFromEvents(events) {
|
|
476
|
-
|
|
477
|
-
for (const event of events) {
|
|
478
|
-
if (event.type === 'tool_use') {
|
|
479
|
-
collector.start({
|
|
480
|
-
timestamp: event.timestamp,
|
|
481
|
-
tool: event.tool || 'unknown',
|
|
482
|
-
input: event.args,
|
|
483
|
-
command: event.command,
|
|
484
|
-
sourceCallId: event.callId,
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
else if (event.type === 'tool_result') {
|
|
488
|
-
collector.finish({
|
|
489
|
-
timestamp: event.timestamp,
|
|
490
|
-
sourceCallId: event.callId,
|
|
491
|
-
tool: event.tool,
|
|
492
|
-
success: event.success,
|
|
493
|
-
outcome: event.outcome,
|
|
494
|
-
exitCode: event.exitCode,
|
|
495
|
-
statusCode: event.statusCode,
|
|
496
|
-
errorCode: event.errorCode,
|
|
497
|
-
output: event.output,
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
else if (event.type === 'error' && event.tool) {
|
|
501
|
-
const structuredError = event.outcome === 'error' || event.success === false;
|
|
502
|
-
const evidence = event.content || event.output || 'Tool execution failed';
|
|
503
|
-
collector.finish({
|
|
504
|
-
timestamp: event.timestamp,
|
|
505
|
-
sourceCallId: event.callId,
|
|
506
|
-
tool: event.tool,
|
|
507
|
-
success: structuredError ? false : undefined,
|
|
508
|
-
outcome: event.outcome,
|
|
509
|
-
exitCode: event.exitCode,
|
|
510
|
-
statusCode: event.statusCode,
|
|
511
|
-
errorCode: event.errorCode,
|
|
512
|
-
error: structuredError ? evidence : undefined,
|
|
513
|
-
output: structuredError ? undefined : evidence,
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
return collector.drainChanged();
|
|
547
|
+
return scanEventToolCalls(events).calls;
|
|
518
548
|
}
|
|
519
549
|
export function toolCallKey(sessionId, ordinal) {
|
|
520
550
|
return createHash('sha256').update(`${sessionId}\0${ordinal}`).digest('hex').slice(0, 20);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type Database from '../sqlite.js';
|
|
2
|
-
import { type IndexedToolCall } from './tool-calls.js';
|
|
2
|
+
import { type IndexedToolCall, type EventToolScanResumePoint } from './tool-calls.js';
|
|
3
3
|
import type { SessionMeta } from './types.js';
|
|
4
4
|
export declare function canonicalToolLedgerPath(filePath: string): string;
|
|
5
5
|
/** Resolve the transcript that actually carries tool events for split-file harnesses. */
|
|
@@ -12,7 +12,14 @@ export declare function purgeMissingToolCallsInDirectory(db: Database.Database,
|
|
|
12
12
|
export interface ToolScanResumePoint {
|
|
13
13
|
/** Serialized ToolCallCollector snapshot at `parsedOffset`. */
|
|
14
14
|
parserState: string;
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Where a later scan may resume. For the streaming (claude/codex) path this is
|
|
17
|
+
* a BYTE offset just past the last complete record consumed. For the full-file
|
|
18
|
+
* harness path (`planEventToolResume`) it is instead the COUNT of normalized
|
|
19
|
+
* events already folded — the two never share a session (a session is one
|
|
20
|
+
* agent, and the streaming path is claude/codex only), so the column carries
|
|
21
|
+
* whichever meaning that agent's resumer wrote.
|
|
22
|
+
*/
|
|
16
23
|
parsedOffset: number;
|
|
17
24
|
}
|
|
18
25
|
export interface PersistToolCallsOptions {
|
|
@@ -37,3 +44,27 @@ export declare function persistToolCalls(db: Database.Database, session: Session
|
|
|
37
44
|
fileMtimeMs: number;
|
|
38
45
|
fileSize: number;
|
|
39
46
|
}, options?: PersistToolCallsOptions): void;
|
|
47
|
+
/**
|
|
48
|
+
* Decide whether a changed full-file-harness session can RESUME its tool index
|
|
49
|
+
* from the last scan instead of re-deriving every call (PHNX-3411).
|
|
50
|
+
*
|
|
51
|
+
* The warm-tick indexer re-derives tool calls for a changed session on every
|
|
52
|
+
* tick, and for an ACTIVE large session that means re-sanitizing tens of
|
|
53
|
+
* thousands of calls each time — the synchronous work that wedged the daemon
|
|
54
|
+
* event loop. Streaming harnesses (claude/codex) already resume from a byte
|
|
55
|
+
* offset; every other harness re-parses the whole file, so this brings them the
|
|
56
|
+
* same benefit at the EVENT level: the ledger stores how many events were folded
|
|
57
|
+
* (`parsed_offset`) plus the collector snapshot (`parser_state`), and a later
|
|
58
|
+
* scan folds only the newly appended events.
|
|
59
|
+
*
|
|
60
|
+
* Returns the prior resume point when it is safe to append, or `null` (full
|
|
61
|
+
* re-scan) whenever the stored prefix may no longer describe the current file:
|
|
62
|
+
* a different extractor, no recorded resume point, a source the ledger row does
|
|
63
|
+
* not describe, a transcript that shrank below what was already parsed (a
|
|
64
|
+
* truncation/rewrite, not an append), more events already folded than the file
|
|
65
|
+
* now yields, or a snapshot that does not read back.
|
|
66
|
+
*/
|
|
67
|
+
export declare function planEventToolResume(db: Database.Database, sessionId: string, sourcePath: string, stamp: {
|
|
68
|
+
fileMtimeMs: number;
|
|
69
|
+
fileSize: number;
|
|
70
|
+
}, currentEventCount: number): EventToolScanResumePoint | null;
|
|
@@ -75,13 +75,14 @@ export function persistToolCalls(db, session, calls, sourceStamp, options = {})
|
|
|
75
75
|
const sourcePath = toolEvidenceSourcePath(session.filePath, session.agent);
|
|
76
76
|
const insertCall = db.prepare(`
|
|
77
77
|
INSERT INTO tool_calls (
|
|
78
|
-
call_key, session_id, ordinal, source_call_id, timestamp, tool, input,
|
|
78
|
+
call_key, session_id, ordinal, source_call_id, timestamp, end_timestamp, tool, input,
|
|
79
79
|
outcome, exit_code, status_code, error_code, output, error, parse_error
|
|
80
80
|
, evidence_bytes
|
|
81
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
81
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
82
82
|
ON CONFLICT(session_id, ordinal) DO UPDATE SET
|
|
83
83
|
source_call_id = excluded.source_call_id,
|
|
84
84
|
timestamp = excluded.timestamp,
|
|
85
|
+
end_timestamp = excluded.end_timestamp,
|
|
85
86
|
tool = excluded.tool,
|
|
86
87
|
input = excluded.input,
|
|
87
88
|
outcome = excluded.outcome,
|
|
@@ -197,7 +198,7 @@ export function persistToolCalls(db, session, calls, sourceStamp, options = {})
|
|
|
197
198
|
}
|
|
198
199
|
for (const call of accepted) {
|
|
199
200
|
const key = toolCallKey(session.id, call.ordinal);
|
|
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
|
+
insertCall.run(key, session.id, call.ordinal, call.sourceCallId ?? null, call.timestamp, call.endTimestamp ?? null, 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
202
|
// The upsert above preserves the rowid of a call it updated, so this is
|
|
202
203
|
// the same rowid the existing text row (if any) was written under.
|
|
203
204
|
const { rowid } = callRowid.get(key);
|
|
@@ -215,3 +216,55 @@ export function persistToolCalls(db, session, calls, sourceStamp, options = {})
|
|
|
215
216
|
});
|
|
216
217
|
txn();
|
|
217
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* Decide whether a changed full-file-harness session can RESUME its tool index
|
|
221
|
+
* from the last scan instead of re-deriving every call (PHNX-3411).
|
|
222
|
+
*
|
|
223
|
+
* The warm-tick indexer re-derives tool calls for a changed session on every
|
|
224
|
+
* tick, and for an ACTIVE large session that means re-sanitizing tens of
|
|
225
|
+
* thousands of calls each time — the synchronous work that wedged the daemon
|
|
226
|
+
* event loop. Streaming harnesses (claude/codex) already resume from a byte
|
|
227
|
+
* offset; every other harness re-parses the whole file, so this brings them the
|
|
228
|
+
* same benefit at the EVENT level: the ledger stores how many events were folded
|
|
229
|
+
* (`parsed_offset`) plus the collector snapshot (`parser_state`), and a later
|
|
230
|
+
* scan folds only the newly appended events.
|
|
231
|
+
*
|
|
232
|
+
* Returns the prior resume point when it is safe to append, or `null` (full
|
|
233
|
+
* re-scan) whenever the stored prefix may no longer describe the current file:
|
|
234
|
+
* a different extractor, no recorded resume point, a source the ledger row does
|
|
235
|
+
* not describe, a transcript that shrank below what was already parsed (a
|
|
236
|
+
* truncation/rewrite, not an append), more events already folded than the file
|
|
237
|
+
* now yields, or a snapshot that does not read back.
|
|
238
|
+
*/
|
|
239
|
+
export function planEventToolResume(db, sessionId, sourcePath, stamp, currentEventCount) {
|
|
240
|
+
const row = db.prepare(`
|
|
241
|
+
SELECT file_path, file_size, extractor_version, parsed_offset, parser_state
|
|
242
|
+
FROM tool_scan_ledger WHERE session_id = ?
|
|
243
|
+
`).get(sessionId);
|
|
244
|
+
if (!row)
|
|
245
|
+
return null;
|
|
246
|
+
if (row.extractor_version !== TOOL_INDEX_VERSION)
|
|
247
|
+
return null;
|
|
248
|
+
if (row.parsed_offset === null || !Number.isSafeInteger(row.parsed_offset) || row.parsed_offset < 0)
|
|
249
|
+
return null;
|
|
250
|
+
if (row.file_path !== canonicalToolLedgerPath(sourcePath))
|
|
251
|
+
return null;
|
|
252
|
+
if (stamp.fileSize < row.file_size)
|
|
253
|
+
return null;
|
|
254
|
+
// The file grew but reports fewer events than were already folded — the prefix
|
|
255
|
+
// was rewritten, not appended to. Re-scan from scratch.
|
|
256
|
+
if (row.parsed_offset > currentEventCount)
|
|
257
|
+
return null;
|
|
258
|
+
if (row.parser_state === null)
|
|
259
|
+
return null;
|
|
260
|
+
let snapshot;
|
|
261
|
+
try {
|
|
262
|
+
snapshot = JSON.parse(row.parser_state);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
if (snapshot?.v !== 1 || !Number.isSafeInteger(snapshot.nextOrdinal))
|
|
268
|
+
return null;
|
|
269
|
+
return { snapshot, eventCount: row.parsed_offset };
|
|
270
|
+
}
|
|
@@ -95,6 +95,12 @@ export declare function resolveDeviceAuto(agent?: string, opts?: {
|
|
|
95
95
|
/** Route only to devices with a row the interactive account picker can launch. */
|
|
96
96
|
accountPicker?: boolean;
|
|
97
97
|
probe?: (pool: string[], agent?: AgentType) => Promise<Map<string, DevicePlacementSignal>>;
|
|
98
|
+
/**
|
|
99
|
+
* Preferred hosts (`auto-launch.preferred`) that get the ranking boost.
|
|
100
|
+
* Defaults to the stored fleet block resolved over the candidate pool;
|
|
101
|
+
* injectable so a test pins it without touching disk.
|
|
102
|
+
*/
|
|
103
|
+
preferred?: ReadonlySet<string>;
|
|
98
104
|
}): Promise<DeviceAutoPlan>;
|
|
99
105
|
/**
|
|
100
106
|
* Resolve host for `--device auto`. Does NOT pick harness or accounts.
|
package/dist/lib/smart-launch.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { queryAffinityRollup } from './session/db.js';
|
|
9
9
|
import { localMachineId } from './session/origin-machine.js';
|
|
10
10
|
import { loadDevicesSync } from './devices/registry.js';
|
|
11
|
-
import { describeAutoPool, filterAutoPool, isAutoPoolMember } from './devices/pool.js';
|
|
11
|
+
import { autoLaunchPreferredSet, describeAutoPool, filterAutoPool, isAutoPoolMember } from './devices/pool.js';
|
|
12
12
|
import { normalizeHost } from './machine-id.js';
|
|
13
13
|
import { probePoolSignals } from './teams/placement-probe.js';
|
|
14
14
|
import { pickBestDevice } from './teams/scheduler.js';
|
|
@@ -153,7 +153,10 @@ export async function resolveDeviceAuto(agent, opts = {}) {
|
|
|
153
153
|
if (eligiblePool.length === 0) {
|
|
154
154
|
throw new Error(formatNoHealthyDeviceError(pool, signals, agent));
|
|
155
155
|
}
|
|
156
|
-
|
|
156
|
+
// `agents devices prefer <name>` boosts a device in the ranking — resolved
|
|
157
|
+
// over the same pool so a fleet default reaches a doc-less box.
|
|
158
|
+
const preferred = opts.preferred ?? autoLaunchPreferredSet(pool, { roster: pool });
|
|
159
|
+
const picked = pickBestDevice(eligiblePool, [], { signals, agentLabel: agent, preferred });
|
|
157
160
|
return {
|
|
158
161
|
host: picked === local ? null : picked,
|
|
159
162
|
pickedDeviceKey: picked,
|
|
@@ -8,8 +8,11 @@ function buildPluginsWriter(agent) {
|
|
|
8
8
|
write({ version, versionHome, selection }) {
|
|
9
9
|
const all = discoverPlugins();
|
|
10
10
|
const map = new Map(all.map(p => [p.name, p]));
|
|
11
|
-
// Clean orphan plugin-skills from plugins that no longer exist.
|
|
12
|
-
|
|
11
|
+
// Clean orphan plugin-skills from plugins that no longer exist. Pass the
|
|
12
|
+
// discovered plugins (not just names) so a stale install under one
|
|
13
|
+
// marketplace is trashed even when another marketplace still ships that
|
|
14
|
+
// name — the PHNX-2618 shadow `code` plugin.
|
|
15
|
+
cleanOrphanedPluginSkills(agent, versionHome, all);
|
|
13
16
|
const synced = [];
|
|
14
17
|
for (const name of selection) {
|
|
15
18
|
const plugin = map.get(name);
|
|
@@ -7,6 +7,11 @@ export type EnabledExtra = {
|
|
|
7
7
|
export declare function trustedSourceBases(): {
|
|
8
8
|
dir: string;
|
|
9
9
|
}[];
|
|
10
|
+
/** Every `plugins/<plugin>/skills` dir across trusted source bases, filtered to an optional plugin/agent scope. */
|
|
11
|
+
export declare function pluginSkillDirs(options?: {
|
|
12
|
+
agent?: AgentId;
|
|
13
|
+
plugins?: Set<string>;
|
|
14
|
+
}): string[];
|
|
10
15
|
/** Find the trusted source for a command markdown by name. */
|
|
11
16
|
export declare function resolveCommandSource(name: string): string | null;
|
|
12
17
|
/** Find the trusted source directory for a skill by name. */
|
|
@@ -54,7 +54,8 @@ function pluginSupportsAgent(manifest, agent) {
|
|
|
54
54
|
return true;
|
|
55
55
|
return !manifest.agents || manifest.agents.length === 0 || manifest.agents.includes(agent);
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
/** Every `plugins/<plugin>/skills` dir across trusted source bases, filtered to an optional plugin/agent scope. */
|
|
58
|
+
export function pluginSkillDirs(options = {}) {
|
|
58
59
|
const dirs = [];
|
|
59
60
|
for (const base of trustedSourceBases()) {
|
|
60
61
|
const pluginsDir = path.join(base.dir, 'plugins');
|
|
@@ -25,10 +25,16 @@ function buildSubagentsWriter(agent) {
|
|
|
25
25
|
const dir = target.dir(versionHome);
|
|
26
26
|
const synced = [];
|
|
27
27
|
const paths = [];
|
|
28
|
+
const errors = [];
|
|
28
29
|
for (const name of selection) {
|
|
29
30
|
const sub = map.get(name);
|
|
30
|
-
if (!sub)
|
|
31
|
+
if (!sub) {
|
|
32
|
+
// Requested but not discoverable as an installed central subagent —
|
|
33
|
+
// e.g. its AGENT.md failed to parse. Say so instead of silently
|
|
34
|
+
// dropping it, which read as an unactionable doctor "hold" (PHNX-3187).
|
|
35
|
+
errors.push(`subagent '${name}': no parseable AGENT.md in ~/.agents/subagents`);
|
|
31
36
|
continue;
|
|
37
|
+
}
|
|
32
38
|
try {
|
|
33
39
|
target.write(dir, sub);
|
|
34
40
|
synced.push(sub.name);
|
|
@@ -39,9 +45,13 @@ function buildSubagentsWriter(agent) {
|
|
|
39
45
|
paths.push(entry.path);
|
|
40
46
|
}
|
|
41
47
|
}
|
|
42
|
-
catch {
|
|
48
|
+
catch (e) {
|
|
49
|
+
// A genuine fs/transform failure must surface with its reason, not
|
|
50
|
+
// vanish behind a bare `catch` (RUSH-2677 / PHNX-3187).
|
|
51
|
+
errors.push(`subagent '${sub.name}': ${e.message}`);
|
|
52
|
+
}
|
|
43
53
|
}
|
|
44
|
-
return { synced, paths };
|
|
54
|
+
return errors.length > 0 ? { synced, paths, errors } : { synced, paths };
|
|
45
55
|
},
|
|
46
56
|
};
|
|
47
57
|
}
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -198,10 +198,13 @@ export declare function getMonitorsDir(): string;
|
|
|
198
198
|
* Path to built-in monitor definitions shipped in the system repo
|
|
199
199
|
* (`~/.agents/.system/monitors/`). Unioned under the user monitors dir by
|
|
200
200
|
* listMonitors()/readMonitor(): a monitor shipped here is available on every
|
|
201
|
-
* install, and a user monitor of the same name overrides it. A built-in
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
201
|
+
* install, and a user monitor of the same name overrides it. A built-in is
|
|
202
|
+
* enabled by default like every other system-layer resource — it runs on every
|
|
203
|
+
* install unless the user shadows it with `enabled: false` (via `agents monitors
|
|
204
|
+
* pause`, which materializes a user copy; writes never touch this pull-only
|
|
205
|
+
* mirror). A shared-input built-in still carries its own `device:` owner pin in
|
|
206
|
+
* the shipped YAML so exactly one box fires it (SING-9). The directory need not
|
|
207
|
+
* exist.
|
|
205
208
|
*/
|
|
206
209
|
export declare function getSystemMonitorsDir(): string;
|
|
207
210
|
/** Path to the durable per-monitor state-diff store + fire history
|
package/dist/lib/state.js
CHANGED
|
@@ -491,10 +491,13 @@ export function getMonitorsDir() { return process.env.AGENTS_MONITORS_DIR ?? MON
|
|
|
491
491
|
* Path to built-in monitor definitions shipped in the system repo
|
|
492
492
|
* (`~/.agents/.system/monitors/`). Unioned under the user monitors dir by
|
|
493
493
|
* listMonitors()/readMonitor(): a monitor shipped here is available on every
|
|
494
|
-
* install, and a user monitor of the same name overrides it. A built-in
|
|
495
|
-
*
|
|
496
|
-
*
|
|
497
|
-
*
|
|
494
|
+
* install, and a user monitor of the same name overrides it. A built-in is
|
|
495
|
+
* enabled by default like every other system-layer resource — it runs on every
|
|
496
|
+
* install unless the user shadows it with `enabled: false` (via `agents monitors
|
|
497
|
+
* pause`, which materializes a user copy; writes never touch this pull-only
|
|
498
|
+
* mirror). A shared-input built-in still carries its own `device:` owner pin in
|
|
499
|
+
* the shipped YAML so exactly one box fires it (SING-9). The directory need not
|
|
500
|
+
* exist.
|
|
498
501
|
*/
|
|
499
502
|
export function getSystemMonitorsDir() { return process.env.AGENTS_SYSTEM_MONITORS_DIR ?? SYSTEM_MONITORS_DIR; }
|
|
500
503
|
/** Path to the durable per-monitor state-diff store + fire history
|
package/dist/lib/subagents.js
CHANGED
|
@@ -23,7 +23,12 @@ export function parseSubagentFrontmatter(filePath) {
|
|
|
23
23
|
}
|
|
24
24
|
try {
|
|
25
25
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
26
|
-
|
|
26
|
+
// Split on CRLF or LF: git checks text files out with CRLF on Windows
|
|
27
|
+
// (core.autocrlf), so a plain split('\n') leaves a trailing '\r' on every
|
|
28
|
+
// line and the frontmatter fence `'---\r' !== '---'` never matches —
|
|
29
|
+
// silently dropping the subagent from discovery so it can never be
|
|
30
|
+
// installed or reconciled (PHNX-3187).
|
|
31
|
+
const lines = content.split(/\r?\n/);
|
|
27
32
|
// Check for YAML frontmatter
|
|
28
33
|
if (lines[0] === '---') {
|
|
29
34
|
const endIndex = lines.slice(1).findIndex((l) => l === '---');
|
|
@@ -52,7 +57,8 @@ export function getSubagentBody(filePath) {
|
|
|
52
57
|
return '';
|
|
53
58
|
}
|
|
54
59
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
55
|
-
|
|
60
|
+
// CRLF-robust for the same reason as parseSubagentFrontmatter (PHNX-3187).
|
|
61
|
+
const lines = content.split(/\r?\n/);
|
|
56
62
|
// Skip YAML frontmatter
|
|
57
63
|
if (lines[0] === '---') {
|
|
58
64
|
const endIndex = lines.slice(1).findIndex((l) => l === '---');
|
|
@@ -93,6 +93,28 @@ export interface UnifiedSyncStatus {
|
|
|
93
93
|
agentsNeedingSync: number;
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Residual drift for a single (agent, version) after a reconcile — the drifted
|
|
98
|
+
* and missing rows that a sync claimed to fix but did not. `orphan` rows are
|
|
99
|
+
* deliberately excluded: sync never removes them (that is `agents prune`'s job),
|
|
100
|
+
* so they are not "unfinished sync". Empty `drifted`+`missing` ⇒ converged.
|
|
101
|
+
*/
|
|
102
|
+
export interface ResidualDrift {
|
|
103
|
+
agent: AgentId;
|
|
104
|
+
version: string;
|
|
105
|
+
rows: ResourceStatusRow[];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Re-check that a version's home now matches its resolved sources after a
|
|
109
|
+
* reconcile. This is the post-write verification the `agents sync` success line
|
|
110
|
+
* depends on (PHNX-3186): the exit line must not read "reconciled" while drift
|
|
111
|
+
* it was asked to fix stays put. Resolves against non-project layers only
|
|
112
|
+
* (`excludeProject: true`), mirroring what the sync writer targets. Returns null
|
|
113
|
+
* when the version converged (no drifted/missing), else the residual rows.
|
|
114
|
+
*/
|
|
115
|
+
export declare function verifyVersionConverged(agent: AgentId, version: string, cwd?: string): ResidualDrift | null;
|
|
116
|
+
/** One-line-per-resource description of residual drift, for the sync exit line. */
|
|
117
|
+
export declare function formatResidualDrift(residual: ResidualDrift[]): string[];
|
|
96
118
|
export interface SyncStatusOptions {
|
|
97
119
|
cwd?: string;
|
|
98
120
|
/** Restrict to specific agent ids; undefined = every supported agent. */
|
package/dist/lib/sync-status.js
CHANGED
|
@@ -27,6 +27,33 @@ import { getSystemAgentsDir, getUserAgentsDir } from './state.js';
|
|
|
27
27
|
import * as fs from 'fs';
|
|
28
28
|
import { isGitRepo, readOriginUrl } from './git.js';
|
|
29
29
|
import { detectConfigDrift } from './config-drift.js';
|
|
30
|
+
/**
|
|
31
|
+
* Re-check that a version's home now matches its resolved sources after a
|
|
32
|
+
* reconcile. This is the post-write verification the `agents sync` success line
|
|
33
|
+
* depends on (PHNX-3186): the exit line must not read "reconciled" while drift
|
|
34
|
+
* it was asked to fix stays put. Resolves against non-project layers only
|
|
35
|
+
* (`excludeProject: true`), mirroring what the sync writer targets. Returns null
|
|
36
|
+
* when the version converged (no drifted/missing), else the residual rows.
|
|
37
|
+
*/
|
|
38
|
+
export function verifyVersionConverged(agent, version, cwd = process.cwd()) {
|
|
39
|
+
const report = diffVersionResources(agent, version, { cwd, excludeProject: true });
|
|
40
|
+
const rows = rowsFromReport(agent, version, report)
|
|
41
|
+
.filter((r) => r.status === 'drifted' || r.status === 'missing');
|
|
42
|
+
if (rows.length === 0)
|
|
43
|
+
return null;
|
|
44
|
+
return { agent, version, rows };
|
|
45
|
+
}
|
|
46
|
+
/** One-line-per-resource description of residual drift, for the sync exit line. */
|
|
47
|
+
export function formatResidualDrift(residual) {
|
|
48
|
+
const lines = [];
|
|
49
|
+
for (const r of residual) {
|
|
50
|
+
for (const row of r.rows) {
|
|
51
|
+
const detail = row.detail ? ` (${row.detail})` : '';
|
|
52
|
+
lines.push(`${r.agent}@${r.version}: ${row.status} ${row.kind} '${row.name}'${detail}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return lines;
|
|
56
|
+
}
|
|
30
57
|
const STATUS_MAP = {
|
|
31
58
|
ok: 'synced',
|
|
32
59
|
diff: 'drifted',
|
|
@@ -64,6 +64,15 @@ export interface UmbrellaResult {
|
|
|
64
64
|
* Empty when nothing was declined — never conflated with "nothing to do".
|
|
65
65
|
*/
|
|
66
66
|
declined: string[];
|
|
67
|
+
/**
|
|
68
|
+
* The (agent, version) pairs the reconcile stage actually wrote into — the set
|
|
69
|
+
* the caller re-verifies for residual drift so the `✓ sync: reconciled` line is
|
|
70
|
+
* never printed while drift it was asked to fix stays put (PHNX-3186).
|
|
71
|
+
*/
|
|
72
|
+
reconciledVersions: Array<{
|
|
73
|
+
agent: string;
|
|
74
|
+
version: string;
|
|
75
|
+
}>;
|
|
67
76
|
}
|
|
68
77
|
export interface RunUmbrellaArgs {
|
|
69
78
|
flags: UmbrellaFlags;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* arrives with `agents secrets vault unlock`, #366/#367)
|
|
16
16
|
* reconcile-> refresh({ skipPrompts }) — re-materialize resources into homes
|
|
17
17
|
*/
|
|
18
|
-
import { pullRepo } from './git.js';
|
|
18
|
+
import { pullRepo, adoptUserRepoIfNeeded } from './git.js';
|
|
19
19
|
import { getUserAgentsDir, getEnabledExtraRepos } from './state.js';
|
|
20
20
|
import { listRemoteBundles, pullBundle } from './secrets/sync.js';
|
|
21
21
|
import { SYNC_PASSPHRASE_ENV } from './secrets/sync-passphrase.js';
|
|
@@ -55,7 +55,7 @@ export function planUmbrellaStages(f) {
|
|
|
55
55
|
export async function runUmbrellaSync(args) {
|
|
56
56
|
const { flags, log, yes, passphrase, quiet = false } = args;
|
|
57
57
|
const plan = planUmbrellaStages(flags);
|
|
58
|
-
const result = { plan, reconciled: false, declined: [] };
|
|
58
|
+
const result = { plan, reconciled: false, declined: [], reconciledVersions: [] };
|
|
59
59
|
if (plan.fetchRepos) {
|
|
60
60
|
const dirs = [
|
|
61
61
|
{ alias: 'user', dir: getUserAgentsDir() },
|
|
@@ -64,6 +64,24 @@ export async function runUmbrellaSync(args) {
|
|
|
64
64
|
let pulled = 0;
|
|
65
65
|
const errors = [];
|
|
66
66
|
for (const { alias, dir } of dirs) {
|
|
67
|
+
// A box where `~/.agents` is present but not a git repo (or lost its
|
|
68
|
+
// `.git`) is a partial install: `pullRepo` there silently fails while the
|
|
69
|
+
// umbrella still reported `✓ reconciled`, so fleet dotfiles/resources never
|
|
70
|
+
// propagated and nothing said so (PHNX-3239, m0). Adopt it in place first —
|
|
71
|
+
// the same self-heal `agents sync user` runs (PHNX-3301) — so the pull has a
|
|
72
|
+
// real repo to fast-forward. A box that cannot be adopted (no recorded
|
|
73
|
+
// remote) fails LOUD into `errors` with the reason, never a silent no-op.
|
|
74
|
+
if (alias === 'user') {
|
|
75
|
+
const adopted = await adoptUserRepoIfNeeded(dir);
|
|
76
|
+
if (adopted && !adopted.success) {
|
|
77
|
+
const hint = adopted.needsUrl ? ' — git-back it: agents repo pull user <git-url>' : '';
|
|
78
|
+
errors.push(`${alias}: ${adopted.error}${hint}`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (adopted?.success) {
|
|
82
|
+
log(`repos: ${alias} adopted in place → ${adopted.commit} (${adopted.materialized} file(s) materialized)`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
67
85
|
const r = await pullRepo(dir);
|
|
68
86
|
if (r.success) {
|
|
69
87
|
pulled++;
|
|
@@ -116,6 +134,7 @@ export async function runUmbrellaSync(args) {
|
|
|
116
134
|
const refreshed = await refresh({ skipPrompts: yes, quiet });
|
|
117
135
|
result.reconciled = true;
|
|
118
136
|
result.declined = refreshed.declined;
|
|
137
|
+
result.reconciledVersions = refreshed.reconciled;
|
|
119
138
|
// Keep already-registered devices' reachability current, and surface newly
|
|
120
139
|
// appeared tailnet nodes as "pending" for the menu-bar Register/Ignore gate
|
|
121
140
|
// rather than silently adding them (refresh mode). Soft: a machine without
|