@phnx-labs/agents-cli 1.22.60 → 1.22.61
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 +49 -0
- package/dist/cli/command-registry.d.ts +1 -0
- package/dist/cli/command-registry.js +2 -0
- package/dist/commands/browser.js +9 -4
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/exec.js +35 -1
- package/dist/commands/harness-hooks.d.ts +55 -0
- package/dist/commands/harness-hooks.js +104 -0
- package/dist/commands/harness-wizard.d.ts +33 -14
- package/dist/commands/harness-wizard.js +53 -23
- package/dist/commands/harness.d.ts +14 -0
- package/dist/commands/harness.js +86 -5
- package/dist/commands/reminders.d.ts +9 -0
- package/dist/commands/reminders.js +49 -0
- package/dist/commands/run-account-picker.d.ts +14 -0
- package/dist/commands/run-account-picker.js +13 -0
- package/dist/commands/teams.d.ts +1 -1
- package/dist/commands/teams.js +9 -3
- package/dist/index.js +9 -0
- package/dist/lib/accounting/rotate.d.ts +63 -0
- package/dist/lib/accounting/rotate.js +229 -13
- package/dist/lib/browser/drivers/local.d.ts +11 -0
- package/dist/lib/browser/drivers/local.js +26 -0
- package/dist/lib/browser/profiles.js +8 -6
- package/dist/lib/browser/service.d.ts +12 -8
- package/dist/lib/browser/service.js +38 -10
- package/dist/lib/claude-statusline.d.ts +14 -1
- package/dist/lib/claude-statusline.js +27 -2
- package/dist/lib/daemon/runner.js +17 -2
- package/dist/lib/devices/doctor-findings.d.ts +1 -1
- package/dist/lib/devices/doctor-findings.js +22 -4
- package/dist/lib/doctor-diff.d.ts +21 -5
- package/dist/lib/doctor-diff.js +242 -76
- package/dist/lib/feed/events.d.ts +1 -1
- package/dist/lib/feed/events.js +25 -16
- package/dist/lib/github/gh-overload.d.ts +58 -0
- package/dist/lib/github/gh-overload.js +246 -0
- package/dist/lib/github/rest.d.ts +64 -0
- package/dist/lib/github/rest.js +111 -0
- package/dist/lib/harness-connection-test.d.ts +57 -0
- package/dist/lib/harness-connection-test.js +80 -0
- package/dist/lib/heal.js +8 -3
- package/dist/lib/installations/shims.d.ts +22 -0
- package/dist/lib/installations/shims.js +104 -0
- package/dist/lib/linear-project-counts.js +8 -0
- package/dist/lib/linear-rate-limit.d.ts +26 -0
- package/dist/lib/linear-rate-limit.js +163 -0
- package/dist/lib/mcp.d.ts +9 -0
- package/dist/lib/mcp.js +37 -1
- package/dist/lib/open-url.js +5 -3
- package/dist/lib/permissions.d.ts +28 -0
- package/dist/lib/permissions.js +156 -1
- package/dist/lib/refresh.js +9 -1
- package/dist/lib/reminders.d.ts +29 -0
- package/dist/lib/reminders.js +88 -0
- package/dist/lib/resource-content-diff.d.ts +33 -0
- package/dist/lib/resource-content-diff.js +103 -0
- package/dist/lib/rules/compile.d.ts +7 -0
- package/dist/lib/rules/compile.js +7 -1
- package/dist/lib/session/active.d.ts +41 -4
- package/dist/lib/session/active.js +58 -7
- package/dist/lib/session/host-link.d.ts +22 -0
- package/dist/lib/session/host-link.js +40 -4
- package/dist/lib/session/trajectory.d.ts +42 -0
- package/dist/lib/session/trajectory.js +46 -27
- package/dist/lib/ssh-exec.d.ts +30 -0
- package/dist/lib/ssh-exec.js +37 -5
- package/dist/lib/startup/command-registry.js +1 -1
- package/dist/lib/subagents-registry.d.ts +18 -0
- package/dist/lib/subagents-registry.js +79 -0
- package/dist/lib/teams/agents.d.ts +12 -0
- package/dist/lib/teams/agents.js +51 -0
- package/dist/lib/traces/schema2-build.d.ts +85 -0
- package/dist/lib/traces/schema2-build.js +637 -0
- package/dist/lib/traces/schema2-danger.d.ts +36 -0
- package/dist/lib/traces/schema2-danger.js +185 -0
- package/dist/lib/traces/schema2.d.ts +149 -0
- package/dist/lib/traces/schema2.js +20 -0
- package/dist/lib/traces/sync.d.ts +93 -0
- package/dist/lib/traces/sync.js +75 -22
- package/dist/lib/traces/worker-template.js +5 -0
- package/dist/lib/uninstall.js +10 -1
- package/dist/lib/workflows.d.ts +11 -0
- package/dist/lib/workflows.js +67 -8
- package/package.json +1 -1
|
@@ -131,6 +131,26 @@ export function isUsageVerified(candidate, nowMs = Date.now()) {
|
|
|
131
131
|
return false;
|
|
132
132
|
return nowMs - capturedAt.getTime() <= USAGE_DECISION_MAX_AGE_MS;
|
|
133
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Whether this candidate carries a STALE-but-present usage number: a snapshot
|
|
136
|
+
* with windows whose capture time is older than {@link USAGE_DECISION_MAX_AGE_MS}.
|
|
137
|
+
*
|
|
138
|
+
* This is the misleading case the initial route must refuse — the number reads
|
|
139
|
+
* "48% used" with the same confidence whether captured a minute or three days
|
|
140
|
+
* ago, and a box whose refresh is failing stays wrong indefinitely. It is
|
|
141
|
+
* deliberately NARROWER than "not verified": a BLIND candidate with no snapshot
|
|
142
|
+
* (or a plan-only meterless one with no windows) carries no number to be misled
|
|
143
|
+
* by — a worker box whose usage endpoint 403s (RUSH-2392), or a meterless Grok
|
|
144
|
+
* login — so it is not "stale", and an entirely-blind pool still draws a pick
|
|
145
|
+
* (PHNX-3392) rather than fail loud with NO_VERIFIED_USAGE.
|
|
146
|
+
*/
|
|
147
|
+
export function hasStaleUsage(candidate, nowMs = Date.now()) {
|
|
148
|
+
const snapshot = candidate.usageSnapshot;
|
|
149
|
+
const capturedAt = snapshot?.capturedAt;
|
|
150
|
+
if (!capturedAt || !snapshot?.windows.length)
|
|
151
|
+
return false;
|
|
152
|
+
return nowMs - capturedAt.getTime() > USAGE_DECISION_MAX_AGE_MS;
|
|
153
|
+
}
|
|
134
154
|
function hasUsageAvailable(candidate) {
|
|
135
155
|
const snapshot = candidate.usageSnapshot;
|
|
136
156
|
if (snapshot) {
|
|
@@ -313,8 +333,8 @@ export function pickBalancedCandidate(candidates, nowMs = Date.now()) {
|
|
|
313
333
|
if (!deduped.has(c))
|
|
314
334
|
excluded.push(c);
|
|
315
335
|
}
|
|
316
|
-
const { picked, usageUnverified } = preferVerified(sorted, nowMs, (from) => weightedRandomByCapacity(from, nowMs), 'representative');
|
|
317
|
-
return { picked, healthy: sorted, excluded, usageUnverified };
|
|
336
|
+
const { picked, usageUnverified, noVerifiedUsage } = preferVerified(sorted, nowMs, (from) => weightedRandomByCapacity(from, nowMs), 'representative');
|
|
337
|
+
return { picked, healthy: sorted, excluded, usageUnverified, noVerifiedUsage };
|
|
318
338
|
}
|
|
319
339
|
/**
|
|
320
340
|
* Choose from the VERIFIED candidates when they are a representative share of
|
|
@@ -361,9 +381,17 @@ function preferVerified(pool, nowMs, choose, narrowing = 'any-verified') {
|
|
|
361
381
|
const narrow = verified.length > 0 &&
|
|
362
382
|
(narrowing === 'any-verified' || verified.length >= Math.ceil(pool.length / 2));
|
|
363
383
|
const picked = choose(narrow ? verified : pool);
|
|
384
|
+
// Entirely-stale = zero verified AND at least one stale-but-present number. A
|
|
385
|
+
// BLIND pool (no snapshots at all) does NOT trip this — it carries no
|
|
386
|
+
// misleading figure, so it still draws a pick (see {@link hasStaleUsage}). The
|
|
387
|
+
// chosen `picked` is still returned so `healthy` (which includes it) stays
|
|
388
|
+
// whole for bounded post-rejection failover; the INITIAL selection acts on
|
|
389
|
+
// this flag instead of launching that stale pick.
|
|
390
|
+
const noVerifiedUsage = verified.length === 0 && pool.some((c) => hasStaleUsage(c, nowMs));
|
|
364
391
|
return {
|
|
365
392
|
picked,
|
|
366
393
|
usageUnverified: !isUsageVerified(picked, nowMs),
|
|
394
|
+
noVerifiedUsage,
|
|
367
395
|
};
|
|
368
396
|
}
|
|
369
397
|
// capacityWeight + PROJECTION_HORIZON_MIN moved to ./capacity.js (a pure,
|
|
@@ -424,13 +452,19 @@ export function pickAvailableCandidate(candidates, preferredVersion, nowMs = Dat
|
|
|
424
452
|
// unconfirmed "48% used" outranks an accurate "90% used" — the same inversion
|
|
425
453
|
// that put a launch on an exhausted account under `balanced`. It routes on the
|
|
426
454
|
// same cache, so it gets the same rule: confirmed headroom first.
|
|
427
|
-
const { picked: bestVerified, usageUnverified } = preferVerified(sorted, nowMs, (from) => from[0]);
|
|
455
|
+
const { picked: bestVerified, usageUnverified, noVerifiedUsage } = preferVerified(sorted, nowMs, (from) => from[0]);
|
|
428
456
|
// An explicit version preference is an instruction, not a ranking signal, so it
|
|
429
457
|
// still wins — but only while that version is actually eligible.
|
|
430
458
|
const preferred = preferredVersion
|
|
431
459
|
? sorted.find((candidate) => candidate.version === preferredVersion)
|
|
432
460
|
: undefined;
|
|
433
|
-
|
|
461
|
+
// `noVerifiedUsage` rides along even when a `preferred` default resolves: an
|
|
462
|
+
// all-stale pool can only make `preferred` a stale pick too (a verified
|
|
463
|
+
// preferred would make verified.length > 0), and auto-selecting the default
|
|
464
|
+
// pin on a stale number is the very thing PHNX-2526 refuses. The initial
|
|
465
|
+
// route (resolveRunVersion) acts on the flag; the failover chain keeps
|
|
466
|
+
// `healthy` regardless.
|
|
467
|
+
return { picked: preferred ?? bestVerified, healthy: sorted, excluded, usageUnverified, noVerifiedUsage };
|
|
434
468
|
}
|
|
435
469
|
/**
|
|
436
470
|
* Classify every harness's candidates into healthy (with a representative
|
|
@@ -547,6 +581,37 @@ export function formatNoHealthyAccountError(agent, strategy, excluded, nowMs = D
|
|
|
547
581
|
const resetSummary = formatResetSummary(earliestResetAcross(excluded, nowMs));
|
|
548
582
|
return `agents: no healthy ${agent} account under strategy '${strategy}' — excluded: ${excludedStr}; earliest window resets ${resetSummary}. Use --strategy pinned to force the default.`;
|
|
549
583
|
}
|
|
584
|
+
/**
|
|
585
|
+
* How old this candidate's usage snapshot is, in whole minutes, or null when it
|
|
586
|
+
* carries no dated snapshot (a blind account). Used only to explain WHY a route
|
|
587
|
+
* was refused as unverified — never to route on.
|
|
588
|
+
*/
|
|
589
|
+
function snapshotAgeMinutes(candidate, nowMs) {
|
|
590
|
+
const capturedAt = candidate.usageSnapshot?.capturedAt;
|
|
591
|
+
if (!capturedAt || !candidate.usageSnapshot?.windows.length)
|
|
592
|
+
return null;
|
|
593
|
+
return Math.max(0, Math.round((nowMs - capturedAt.getTime()) / 60_000));
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* The all-stale-usage error (PHNX-2526) an UNATTENDED `balanced`/`available`
|
|
597
|
+
* run fails loud with when no account's usage is fresh enough to route on. EXACT
|
|
598
|
+
* contract — it MUST contain the literal `NO_VERIFIED_USAGE` so a machine caller
|
|
599
|
+
* (and the Factory watchdog) can tail-detect it distinctly from the
|
|
600
|
+
* `no healthy` throttle error, which is a different condition (throttled vs
|
|
601
|
+
* merely stale). Names each candidate with how stale its snapshot is, so the
|
|
602
|
+
* operator can see the failing-refresh box rather than guess.
|
|
603
|
+
*/
|
|
604
|
+
export function formatNoVerifiedUsageError(agent, strategy, candidates, nowMs = Date.now()) {
|
|
605
|
+
const detail = candidates.length === 0
|
|
606
|
+
? 'no signed-in accounts'
|
|
607
|
+
: candidates.map((c) => {
|
|
608
|
+
const age = snapshotAgeMinutes(c, nowMs);
|
|
609
|
+
const staleness = age === null ? 'no usage snapshot' : `usage ${age}m old`;
|
|
610
|
+
return `${c.version} (${staleness})`;
|
|
611
|
+
}).join(', ');
|
|
612
|
+
const maxAgeMin = Math.round(USAGE_DECISION_MAX_AGE_MS / 60_000);
|
|
613
|
+
return `agents: NO_VERIFIED_USAGE — no signed-in ${agent} account has usage newer than ${maxAgeMin}m under strategy '${strategy}', so routing refuses to guess on a stale number: ${detail}. Refresh usage (agents view ${agent}) or pin the default with --strategy pinned.`;
|
|
614
|
+
}
|
|
550
615
|
/**
|
|
551
616
|
* The zero-healthy-harness error for `agents run auto` — names each harness's
|
|
552
617
|
* exclusion reason plus the earliest reset across all snapshots.
|
|
@@ -724,6 +789,141 @@ function readRotationStamp(agent) {
|
|
|
724
789
|
catch { /* missing or corrupt — treat as no stamp */ }
|
|
725
790
|
return null;
|
|
726
791
|
}
|
|
792
|
+
/** Cap on candidates serialized into a rotation decision event — a pathological
|
|
793
|
+
* backstop on the log line; a real fleet is ~19 accounts, well under it. The
|
|
794
|
+
* candidates are emitted as a keyed OBJECT (not an array) so the event sink's
|
|
795
|
+
* generic `sanitizeNested` does not truncate them to its 10-element array cap
|
|
796
|
+
* (feed/events.ts) — an object is recursed uncapped, so the whole pool up to
|
|
797
|
+
* this bound survives. */
|
|
798
|
+
const ROTATION_EVENT_CANDIDATE_CAP = 32;
|
|
799
|
+
/**
|
|
800
|
+
* Compact, queryable descriptor of ONE candidate exactly as the router saw it,
|
|
801
|
+
* for the `rotation.resolved`/`rotation.unresolved` event. These are the fields
|
|
802
|
+
* that disambiguate WHY a route landed on a bad account, so a post-mortem reads
|
|
803
|
+
* them from `agents events` instead of guessing:
|
|
804
|
+
*
|
|
805
|
+
* - `usageKey` is the per-org quota key — the ONLY identity that joins the same
|
|
806
|
+
* account across devices (version numbers are device-local and meaningless
|
|
807
|
+
* across the fleet).
|
|
808
|
+
* - `tier` is the freshness class the weighting actually used: `verified`
|
|
809
|
+
* (fresh windowed → routed at real headroom), `stale` (windowed but past the
|
|
810
|
+
* {@link USAGE_DECISION_MAX_AGE_MS} decision window → floored weight), `blind`
|
|
811
|
+
* (no snapshot at all — a worker whose usage endpoint 403s, or a never-synced
|
|
812
|
+
* network:false harness → still drawn at floor weight).
|
|
813
|
+
* - `source`/`ageMs`/`capturedAt` expose staleness and provenance: a `last_seen`
|
|
814
|
+
* snapshot showing `tier: verified` with a large `ageMs` is the cross-host
|
|
815
|
+
* clock-skew failure (capturedAt is stamped by the reading host's clock, this
|
|
816
|
+
* `ageMs` by the routing host's — they disagree under skew).
|
|
817
|
+
*/
|
|
818
|
+
function describeRotationCandidate(c, nowMs) {
|
|
819
|
+
const snap = c.usageSnapshot;
|
|
820
|
+
const capturedAtMs = snap?.capturedAt ? snap.capturedAt.getTime() : null;
|
|
821
|
+
const tier = isUsageVerified(c, nowMs) ? 'verified' : hasStaleUsage(c, nowMs) ? 'stale' : 'blind';
|
|
822
|
+
const readiness = readinessFromCandidate(c);
|
|
823
|
+
return {
|
|
824
|
+
usageKey: c.usageKey,
|
|
825
|
+
// A RUSH-3182 provider/setup-token account carries a null usageKey and
|
|
826
|
+
// shares its `version` with the native login and every sibling provider
|
|
827
|
+
// account, so neither is a unique identity here. `accountKey` is the pool's
|
|
828
|
+
// own dedup boundary (foldRegistryCandidates), and `providerAccount` names
|
|
829
|
+
// the injected account — together they keep same-version rows distinct.
|
|
830
|
+
accountKey: c.accountKey,
|
|
831
|
+
providerAccount: c.providerAccount ?? null,
|
|
832
|
+
email: c.email,
|
|
833
|
+
version: c.version,
|
|
834
|
+
signedIn: c.signedIn,
|
|
835
|
+
// NOT `authVerdict`: the event sink's generic sanitizer redacts any payload
|
|
836
|
+
// key matching /auth/i to "[REDACTED]" (feed/events.ts SENSITIVE_PAYLOAD_KEY),
|
|
837
|
+
// which would silently blank this field on every row. `credentialVerdict`
|
|
838
|
+
// carries the same value ('revoked'/null/…) past the redaction.
|
|
839
|
+
credentialVerdict: c.authVerdict,
|
|
840
|
+
usageStatus: c.usageStatus,
|
|
841
|
+
tier,
|
|
842
|
+
source: snap?.source ?? null,
|
|
843
|
+
sourceLabel: snap?.sourceLabel ?? null,
|
|
844
|
+
capturedAt: snap?.capturedAt ? snap.capturedAt.toISOString() : null,
|
|
845
|
+
ageMs: capturedAtMs === null ? null : nowMs - capturedAtMs,
|
|
846
|
+
windows: (snap?.windows ?? []).map((w) => ({ key: w.key, usedPercent: Math.round(w.usedPercent) })),
|
|
847
|
+
unavailable: snap?.unavailable?.reason ?? null,
|
|
848
|
+
eligible: readiness.ready,
|
|
849
|
+
excludedReason: readiness.ready ? null : readiness.reason,
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Build the enriched `rotation.resolved`/`rotation.unresolved` event payload:
|
|
854
|
+
* the full candidate pool as the router saw it, the pick and WHY, and a
|
|
855
|
+
* freshness tally. Replaces the old `{ version, healthy: <n>, excluded: <n> }`
|
|
856
|
+
* shape, which recorded only counts and a device-local version and so could not
|
|
857
|
+
* tell a blind-pool draw from a skewed-verified pick from a refused-stale route
|
|
858
|
+
* — the exact ambiguity that keeps a bad pick undebuggable from the log. All
|
|
859
|
+
* candidates share ONE `nowMs` so their `tier`/`ageMs` are mutually consistent.
|
|
860
|
+
*/
|
|
861
|
+
export function buildRotationDecisionEvent(rotation, agent, strategy) {
|
|
862
|
+
const nowMs = Date.now();
|
|
863
|
+
const tally = { verified: 0, stale: 0, blind: 0 };
|
|
864
|
+
for (const c of rotation.healthy) {
|
|
865
|
+
const t = isUsageVerified(c, nowMs) ? 'verified' : hasStaleUsage(c, nowMs) ? 'stale' : 'blind';
|
|
866
|
+
tally[t] += 1;
|
|
867
|
+
}
|
|
868
|
+
const pickedTier = isUsageVerified(rotation.picked, nowMs)
|
|
869
|
+
? 'verified'
|
|
870
|
+
: hasStaleUsage(rotation.picked, nowMs)
|
|
871
|
+
? 'stale'
|
|
872
|
+
: 'blind';
|
|
873
|
+
// WHY the pick: a verified-weighted draw is the healthy path; an
|
|
874
|
+
// `unverified-*-draw` names the fallback the router was forced into (a blind
|
|
875
|
+
// worker pool, or a stale-but-plausible number); `refused-no-verified` is the
|
|
876
|
+
// fail-closed exit where no version launches. Read straight from the result's
|
|
877
|
+
// own `usageUnverified`/`noVerifiedUsage`, so the event can never disagree
|
|
878
|
+
// with what rotation actually did.
|
|
879
|
+
const pickReason = rotation.noVerifiedUsage
|
|
880
|
+
? 'refused-no-verified'
|
|
881
|
+
: rotation.usageUnverified
|
|
882
|
+
? `unverified-${pickedTier}-draw`
|
|
883
|
+
: 'verified-weighted';
|
|
884
|
+
const pool = [...rotation.healthy, ...rotation.excluded];
|
|
885
|
+
return {
|
|
886
|
+
module: 'rotate',
|
|
887
|
+
agent,
|
|
888
|
+
strategy,
|
|
889
|
+
version: rotation.picked.version,
|
|
890
|
+
picked: {
|
|
891
|
+
usageKey: rotation.picked.usageKey,
|
|
892
|
+
email: rotation.picked.email,
|
|
893
|
+
version: rotation.picked.version,
|
|
894
|
+
tier: pickedTier,
|
|
895
|
+
},
|
|
896
|
+
pickReason,
|
|
897
|
+
healthy: rotation.healthy.length,
|
|
898
|
+
excluded: rotation.excluded.length,
|
|
899
|
+
freshness: tally,
|
|
900
|
+
// An OBJECT (not an array), so the sink cannot truncate it to 10 entries
|
|
901
|
+
// (see ROTATION_EVENT_CANDIDATE_CAP). Keyed by POOL INDEX, not by version or
|
|
902
|
+
// usageKey: a RUSH-3182 provider-account pool has multiple candidates
|
|
903
|
+
// sharing one `version` AND a null `usageKey` (foldRegistryCandidates), so
|
|
904
|
+
// either would collide and silently drop rows via Object.fromEntries. The
|
|
905
|
+
// index is structural only; each row's identity is its `accountKey` /
|
|
906
|
+
// `usageKey` / `providerAccount`. `candidatesTotal` reveals cap overflow.
|
|
907
|
+
candidates: Object.fromEntries(pool.slice(0, ROTATION_EVENT_CANDIDATE_CAP).map((c, i) => [String(i), describeRotationCandidate(c, nowMs)])),
|
|
908
|
+
candidatesTotal: pool.length,
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Build AND emit a rotation decision event without ever letting an observability
|
|
913
|
+
* bug crash a live route. `emit` swallows its own IO errors, but the payload
|
|
914
|
+
* ARGUMENT is evaluated before `emit` is called, so a future null-deref inside
|
|
915
|
+
* `describeRotationCandidate` would otherwise propagate into `resolveRunVersion`
|
|
916
|
+
* and abort the launch. This wraps both build and emit, so the worst case is a
|
|
917
|
+
* lost log line, never a failed run.
|
|
918
|
+
*/
|
|
919
|
+
function emitRotationDecision(event, rotation, agent, strategy, extra = {}) {
|
|
920
|
+
try {
|
|
921
|
+
emit(event, { ...buildRotationDecisionEvent(rotation, agent, strategy), ...extra });
|
|
922
|
+
}
|
|
923
|
+
catch {
|
|
924
|
+
/* observability must never break a route */
|
|
925
|
+
}
|
|
926
|
+
}
|
|
727
927
|
/**
|
|
728
928
|
* Resolve the version `agents run` should use when the caller did not pin
|
|
729
929
|
* one with `@version`. The caller supplies the effective strategy.
|
|
@@ -740,6 +940,21 @@ function readRotationStamp(agent) {
|
|
|
740
940
|
export async function resolveRunVersion(agent, strategy, cwd = process.cwd(), collect = collectRunCandidates) {
|
|
741
941
|
const fallback = resolveVersion(agent, cwd);
|
|
742
942
|
const candidates = await collect(agent);
|
|
943
|
+
// Entirely stale usage (PHNX-2526): every eligible account carries a
|
|
944
|
+
// stale-but-present number and none is verified. Refuse to auto-pick on a
|
|
945
|
+
// number that looks plausible but is wrong. `rotation` is returned so its
|
|
946
|
+
// `healthy` set survives for BOUNDED post-rejection failover, but `version`
|
|
947
|
+
// is null so the caller diverts — interactive to the account picker,
|
|
948
|
+
// unattended to a loud NO_VERIFIED_USAGE exit. Shared across BOTH rotating
|
|
949
|
+
// paths (the pinned auth-blocked-pin fallback AND balanced/available), since
|
|
950
|
+
// both reuse `pickAvailableCandidate`/`pickBalancedCandidate` and neither may
|
|
951
|
+
// launch a stale pick.
|
|
952
|
+
const refuseStaleUsage = (rotation) => {
|
|
953
|
+
emitRotationDecision('rotation.unresolved', rotation, agent, strategy, {
|
|
954
|
+
reason: 'no_verified_usage',
|
|
955
|
+
});
|
|
956
|
+
return { version: null, rotation, noVerifiedUsage: true };
|
|
957
|
+
};
|
|
743
958
|
if (strategy === 'pinned') {
|
|
744
959
|
const pinnedCandidate = fallback
|
|
745
960
|
? candidates.find((c) => c.version === fallback)
|
|
@@ -748,15 +963,14 @@ export async function resolveRunVersion(agent, strategy, cwd = process.cwd(), co
|
|
|
748
963
|
// guaranteed miss. Prefer a signed-in sibling on this device.
|
|
749
964
|
if (pinnedCandidate && isSignInRecoverable(readinessFromCandidate(pinnedCandidate))) {
|
|
750
965
|
const rotation = pickAvailableCandidate(candidates, fallback);
|
|
966
|
+
// The auth-blocked pin rotates to a sibling — an initial selection, so it
|
|
967
|
+
// gets the same verified-only gate as balanced/available. Without this, a
|
|
968
|
+
// revoked pin with only stale siblings launched one blind (the yosemite-s1
|
|
969
|
+
// trap through the pinned path — PR #3295 review).
|
|
970
|
+
if (rotation && rotation.noVerifiedUsage)
|
|
971
|
+
return refuseStaleUsage(rotation);
|
|
751
972
|
if (rotation) {
|
|
752
|
-
|
|
753
|
-
module: 'rotate',
|
|
754
|
-
agent,
|
|
755
|
-
version: rotation.picked.version,
|
|
756
|
-
strategy,
|
|
757
|
-
healthy: rotation.healthy.length,
|
|
758
|
-
excluded: rotation.excluded.length,
|
|
759
|
-
});
|
|
973
|
+
emitRotationDecision('rotation.resolved', rotation, agent, strategy);
|
|
760
974
|
return { version: rotation.picked.version, rotation };
|
|
761
975
|
}
|
|
762
976
|
return {
|
|
@@ -770,6 +984,8 @@ export async function resolveRunVersion(agent, strategy, cwd = process.cwd(), co
|
|
|
770
984
|
const rotation = strategy === 'available'
|
|
771
985
|
? pickAvailableCandidate(candidates, fallback)
|
|
772
986
|
: pickBalancedCandidate(candidates);
|
|
987
|
+
if (rotation && rotation.noVerifiedUsage)
|
|
988
|
+
return refuseStaleUsage(rotation);
|
|
773
989
|
if (rotation) {
|
|
774
990
|
// `available` is sticky to the pinned default when healthy. Use the 60s
|
|
775
991
|
// anti-collision stamp to nudge parallel callers off the same version.
|
|
@@ -784,7 +1000,7 @@ export async function resolveRunVersion(agent, strategy, cwd = process.cwd(), co
|
|
|
784
1000
|
}
|
|
785
1001
|
recordRotationPick(agent, rotation.picked.version);
|
|
786
1002
|
}
|
|
787
|
-
|
|
1003
|
+
emitRotationDecision('rotation.resolved', rotation, agent, strategy);
|
|
788
1004
|
return { version: rotation.picked.version, rotation };
|
|
789
1005
|
}
|
|
790
1006
|
return { version: fallback, rotation: null, exhausted: candidates.length > 0 ? candidates : undefined };
|
|
@@ -5,6 +5,17 @@ export interface LocalConnection {
|
|
|
5
5
|
port: number;
|
|
6
6
|
pid: number;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Arc is single-instance: relaunching the Arc binary with a fresh
|
|
10
|
+
* `--user-data-dir` does not create a second debuggable process — macOS routes
|
|
11
|
+
* the launch to the running Arc, which was started with no debug port, so the
|
|
12
|
+
* spawn produces a stray window and no CDP endpoint (issue #2779). agents
|
|
13
|
+
* browser therefore ATTACHES to the user's running Arc and never launches its
|
|
14
|
+
* own. When the running Arc exposes no CDP endpoint on the profile's port, we
|
|
15
|
+
* fail loud with the one relaunch that fixes it rather than silently spawning a
|
|
16
|
+
* duplicate. Exported so the contract is unit-testable without a real Arc.
|
|
17
|
+
*/
|
|
18
|
+
export declare function arcAttachRequiredError(profileName: string, port: number): Error;
|
|
8
19
|
export declare function connectLocal(endpoint: string, profile: BrowserProfile,
|
|
9
20
|
/**
|
|
10
21
|
* Runtime key (`<profile>@<endpoint>`) the launched browser's user-data-dir,
|
|
@@ -31,6 +31,24 @@ const TUNNEL_PROCESS_NAMES = new Set(['ssh', 'autossh', 'mosh-client', 'socat'])
|
|
|
31
31
|
function isTunnelProcess(command) {
|
|
32
32
|
return TUNNEL_PROCESS_NAMES.has(command.toLowerCase());
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Arc is single-instance: relaunching the Arc binary with a fresh
|
|
36
|
+
* `--user-data-dir` does not create a second debuggable process — macOS routes
|
|
37
|
+
* the launch to the running Arc, which was started with no debug port, so the
|
|
38
|
+
* spawn produces a stray window and no CDP endpoint (issue #2779). agents
|
|
39
|
+
* browser therefore ATTACHES to the user's running Arc and never launches its
|
|
40
|
+
* own. When the running Arc exposes no CDP endpoint on the profile's port, we
|
|
41
|
+
* fail loud with the one relaunch that fixes it rather than silently spawning a
|
|
42
|
+
* duplicate. Exported so the contract is unit-testable without a real Arc.
|
|
43
|
+
*/
|
|
44
|
+
export function arcAttachRequiredError(profileName, port) {
|
|
45
|
+
return new Error(`Arc is not exposing a CDP endpoint on cdp://127.0.0.1:${port} for profile ` +
|
|
46
|
+
`"${profileName}". Arc is single-instance — agents browser attaches to your ` +
|
|
47
|
+
`RUNNING Arc and never launches a second one, so it will not start an isolated ` +
|
|
48
|
+
`Arc for you. Quit Arc, then relaunch it with remote debugging on this port:\n` +
|
|
49
|
+
` open -a Arc --args --remote-debugging-port=${port}\n` +
|
|
50
|
+
`and retry. The port must match the profile's endpoint (\`agents browser profiles list\`).`);
|
|
51
|
+
}
|
|
34
52
|
export async function connectLocal(endpoint, profile,
|
|
35
53
|
/**
|
|
36
54
|
* Runtime key (`<profile>@<endpoint>`) the launched browser's user-data-dir,
|
|
@@ -71,6 +89,14 @@ key) {
|
|
|
71
89
|
if (err instanceof Error && err.message.startsWith('Browser identity mismatch')) {
|
|
72
90
|
throw err;
|
|
73
91
|
}
|
|
92
|
+
// Arc reached the catch, so the attach above failed: the running Arc is not
|
|
93
|
+
// serving CDP on this port. Never fall through to launchBrowser — that would
|
|
94
|
+
// spawn the duplicate/stray-window this ticket set out to end (#2779,
|
|
95
|
+
// PHNX-2399). Fail loud with the relaunch that makes the user's Arc
|
|
96
|
+
// attachable.
|
|
97
|
+
if (profile.browser === 'arc') {
|
|
98
|
+
throw arcAttachRequiredError(profile.name, port);
|
|
99
|
+
}
|
|
74
100
|
// Distinguish "nothing listening on this port" (fine to launch fresh) from
|
|
75
101
|
// "something is listening but it's not a debuggable browser" (bail loudly —
|
|
76
102
|
// silently launching on a different port leads to confusing `pid 0` and
|
|
@@ -513,13 +513,15 @@ export async function editProfile(name, patch) {
|
|
|
513
513
|
const current = configToProfile(name, local.config, devices);
|
|
514
514
|
const merged = { ...current, ...patch, name };
|
|
515
515
|
const changed = Object.keys(patch).filter((k) => JSON.stringify(current[k]) !== JSON.stringify(merged[k]));
|
|
516
|
-
// A target filter only means anything for
|
|
516
|
+
// A target filter only means anything for a profile that reuses an existing
|
|
517
|
+
// page target rather than creating one — an Electron app or Arc (which crashes
|
|
518
|
+
// on tab creation, so it drives an open tab / Space, PHNX-2399). The gate must
|
|
517
519
|
// read the MERGED record: `--target-filter x` on a profile that is already
|
|
518
|
-
// electron is valid, and `--no-electron` on
|
|
519
|
-
// not. Checking the patch alone would accept both.
|
|
520
|
-
if (merged.targetFilter && !merged.electron) {
|
|
521
|
-
throw new Error(`--target-filter only applies to an Electron profile. ` +
|
|
522
|
-
`Pass --electron, or clear the filter with --target-filter ''.`);
|
|
520
|
+
// electron/arc is valid, and `--no-electron` on a Chromium profile that still
|
|
521
|
+
// carries a filter is not. Checking the patch alone would accept both.
|
|
522
|
+
if (merged.targetFilter && !merged.electron && merged.browser !== 'arc') {
|
|
523
|
+
throw new Error(`--target-filter only applies to an Electron or Arc profile. ` +
|
|
524
|
+
`Pass --electron, use --browser arc, or clear the filter with --target-filter ''.`);
|
|
523
525
|
}
|
|
524
526
|
assertLocalPortFree(merged, { ignore: name });
|
|
525
527
|
// Same rule as create: the binary lives on the remote for an SSH profile, so a
|
|
@@ -53,9 +53,10 @@ interface ProfileConnection {
|
|
|
53
53
|
electron?: boolean;
|
|
54
54
|
/**
|
|
55
55
|
* The profile's declared browser family. Load-bearing for Arc: Arc answers
|
|
56
|
-
* `Browser.getVersion`
|
|
57
|
-
* `Target.createTarget` (verified live, PR #2778), so any
|
|
58
|
-
* must refuse
|
|
56
|
+
* `Browser.getVersion` and DOES expose CDP page targets it honors `Page.navigate`
|
|
57
|
+
* on, but CRASHES on `Target.createTarget` (verified live, PR #2778), so any
|
|
58
|
+
* tab-CREATING path must refuse and drive an EXISTING tab instead rather than
|
|
59
|
+
* crash the user's Arc. See `createPageTarget` / `pickReusableTargetWithoutCreate`.
|
|
59
60
|
*/
|
|
60
61
|
browserType?: BrowserType;
|
|
61
62
|
/** Raw `url:<v>` / `title:<v>` filter copied from the profile config. */
|
|
@@ -88,11 +89,14 @@ interface ProfileConnection {
|
|
|
88
89
|
/** Join error lines so callers get a next command, not a dead-end message. */
|
|
89
90
|
export declare function actionable(...lines: string[]): string;
|
|
90
91
|
/**
|
|
91
|
-
* Arc
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
92
|
+
* Arc DOES expose CDP page targets and honors `Page.navigate` on the ones the
|
|
93
|
+
* user already has open (measured live against Arc: 33 targets, PR #2786), so
|
|
94
|
+
* agents browser drives an attached Arc by reusing existing tabs. What it CANNOT
|
|
95
|
+
* survive is `Target.createTarget` — opening a brand-new tab crashes the user's
|
|
96
|
+
* Arc window (verified live, PR #2778). Tab creation is the one CDP-only op that
|
|
97
|
+
* fails clearly here (PHNX-2399): every tab-creating path throws this actionable
|
|
98
|
+
* error instead of crashing Arc, and steers the caller to reuse an existing
|
|
99
|
+
* tab/Space (via `--target-filter`) or to a Chromium-family browser for new tabs.
|
|
96
100
|
*/
|
|
97
101
|
export declare function arcNotDrivableError(profileName?: string): Error;
|
|
98
102
|
/** Derive a human label from an explicit title or a navigated URL. */
|
|
@@ -247,15 +247,18 @@ export function actionable(...lines) {
|
|
|
247
247
|
return lines.filter((l) => l != null && l !== '').join('\n');
|
|
248
248
|
}
|
|
249
249
|
/**
|
|
250
|
-
* Arc
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
250
|
+
* Arc DOES expose CDP page targets and honors `Page.navigate` on the ones the
|
|
251
|
+
* user already has open (measured live against Arc: 33 targets, PR #2786), so
|
|
252
|
+
* agents browser drives an attached Arc by reusing existing tabs. What it CANNOT
|
|
253
|
+
* survive is `Target.createTarget` — opening a brand-new tab crashes the user's
|
|
254
|
+
* Arc window (verified live, PR #2778). Tab creation is the one CDP-only op that
|
|
255
|
+
* fails clearly here (PHNX-2399): every tab-creating path throws this actionable
|
|
256
|
+
* error instead of crashing Arc, and steers the caller to reuse an existing
|
|
257
|
+
* tab/Space (via `--target-filter`) or to a Chromium-family browser for new tabs.
|
|
255
258
|
*/
|
|
256
259
|
export function arcNotDrivableError(profileName) {
|
|
257
260
|
const scope = profileName ? ` (profile "${profileName}")` : '';
|
|
258
|
-
return new Error(actionable(`Browser "arc"${scope}
|
|
261
|
+
return new Error(actionable(`Browser "arc"${scope} cannot open a NEW tab: Arc crashes when a tab is created`, 'over CDP (Target.createTarget). agents browser drives Arc by attaching to your', 'running window and reusing an EXISTING tab — bind the profile to the tab/Space you', 'want with --target-filter (url:<substring> or title:<substring>), or navigate to a', 'URL that is already open. To open brand-new tabs, use a Chromium-family browser:', ' agents browser profiles create <name> --browser comet'));
|
|
259
262
|
}
|
|
260
263
|
/** Derive a human label from an explicit title or a navigated URL. */
|
|
261
264
|
export function deriveTaskLabel(opts) {
|
|
@@ -481,7 +484,7 @@ export class BrowserService {
|
|
|
481
484
|
// If URL provided, reclaim a tab an abandoned task is holding on it, else
|
|
482
485
|
// create one directly (no about:blank).
|
|
483
486
|
let tabId;
|
|
484
|
-
if (opts.url && !conn.electron) {
|
|
487
|
+
if (opts.url && !conn.electron && conn.browserType !== 'arc') {
|
|
485
488
|
const adopted = opts.fresh ? undefined : await this.adoptTabShowing(conn, opts.url);
|
|
486
489
|
const targetId = adopted ?? (await this.createPageTarget(conn, { url: opts.url })).targetId;
|
|
487
490
|
const shortId = generateShortId();
|
|
@@ -491,7 +494,13 @@ export class BrowserService {
|
|
|
491
494
|
await this.saveTaskState(effectiveKey, conn.tasks);
|
|
492
495
|
tabId = shortId;
|
|
493
496
|
}
|
|
494
|
-
else if (opts.url && conn.electron) {
|
|
497
|
+
else if (opts.url && (conn.electron || conn.browserType === 'arc')) {
|
|
498
|
+
// Electron and Arc share the reuse-in-place path: neither may open a fresh
|
|
499
|
+
// tab here (Electron drives its one window; Arc crashes on Target.createTarget),
|
|
500
|
+
// so the implicit first navigate attaches to an existing tab — honoring the
|
|
501
|
+
// profile's --target-filter — instead of throwing. This is what makes the
|
|
502
|
+
// documented `navigate --profile arc --url …` first-use workflow attach rather
|
|
503
|
+
// than refuse on a task-less profile (PHNX-2399 review).
|
|
495
504
|
const result = await this.navigate(taskName, opts.url, effectiveKey);
|
|
496
505
|
tabId = result.tabId;
|
|
497
506
|
}
|
|
@@ -583,13 +592,32 @@ export class BrowserService {
|
|
|
583
592
|
for (const id of Object.values(t.tabs))
|
|
584
593
|
owned.add(id);
|
|
585
594
|
}
|
|
586
|
-
|
|
595
|
+
let free = targetInfos.filter((t) => t.type === 'page' && !owned.has(t.targetId));
|
|
596
|
+
// A bound --target-filter (url:/title:<substring>) is how the profile names the
|
|
597
|
+
// tab/Space it drives — e.g. an Arc Space pinned with `--target-filter url:notion.so`.
|
|
598
|
+
// Scope the reusable set to tabs matching it; an explicit filter that matches nothing
|
|
599
|
+
// returns undefined so the caller refuses rather than borrowing an unrelated tab (the
|
|
600
|
+
// same contract pickWindowTarget holds for the tab-creating path). Without this the
|
|
601
|
+
// filter was accepted and stored but never consulted when driving Arc (PHNX-2399 review).
|
|
602
|
+
const parsed = parseTargetFilter(conn.targetFilter);
|
|
603
|
+
if (parsed) {
|
|
604
|
+
const needle = parsed.value.toLowerCase();
|
|
605
|
+
free = free.filter((t) => ((parsed.kind === 'url' ? t.url : t.title) ?? '').toLowerCase().includes(needle));
|
|
606
|
+
if (free.length === 0)
|
|
607
|
+
return undefined;
|
|
608
|
+
}
|
|
587
609
|
const wanted = canonicalTabUrl(url);
|
|
588
610
|
const sameUrl = free.find((t) => canonicalTabUrl(t.url) === wanted);
|
|
589
611
|
if (sameUrl)
|
|
590
612
|
return sameUrl.targetId;
|
|
591
613
|
const BLANK = new Set(['', 'about:blank', 'about:newtab', 'chrome://newtab/', 'chrome://new-tab-page/']);
|
|
592
|
-
|
|
614
|
+
const blank = free.find((t) => BLANK.has(t.url));
|
|
615
|
+
if (blank)
|
|
616
|
+
return blank.targetId;
|
|
617
|
+
// With a filter set, every remaining tab already IS the bound Space, so reuse the
|
|
618
|
+
// first even when it is neither the exact URL nor blank — that is the target the
|
|
619
|
+
// operator pinned. With no filter, only an exact-URL or blank tab is safe to borrow.
|
|
620
|
+
return parsed ? free[0]?.targetId : undefined;
|
|
593
621
|
}
|
|
594
622
|
async adoptTabShowing(conn, url) {
|
|
595
623
|
const { targetInfos } = (await conn.cdp.send('Target.getTargets'));
|
|
@@ -9,6 +9,7 @@ export declare const CLAUDE_STATUSLINE_COMMAND = "agents __claude-statusline";
|
|
|
9
9
|
export declare function isStatusLineSelfReference(command: string): boolean;
|
|
10
10
|
interface ClaudeStatusLinePayload {
|
|
11
11
|
cwd?: string;
|
|
12
|
+
session_id?: string;
|
|
12
13
|
workspace?: {
|
|
13
14
|
current_dir?: string;
|
|
14
15
|
};
|
|
@@ -29,7 +30,19 @@ interface ClaudeStatusLinePayload {
|
|
|
29
30
|
}
|
|
30
31
|
export declare function ingestClaudeStatusLineUsage(payload: ClaudeStatusLinePayload, versionHome: string): boolean;
|
|
31
32
|
export declare function renderDelegate(payload: string, versionHome: string): string;
|
|
32
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Format a reminder as a dimmed statusline part (empty string when none). The
|
|
35
|
+
* ◆ marker distinguishes it from the host/model/usage parts, and the ANSI dim
|
|
36
|
+
* keeps it quiet next to the live figures.
|
|
37
|
+
*/
|
|
38
|
+
export declare function formatReminderPart(short: string | undefined): string;
|
|
39
|
+
export declare function renderClaudeStatusLine(payload: ClaudeStatusLinePayload, host?: string, delegated?: string, reminder?: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the per-session reminder for the statusline, or '' when none is
|
|
42
|
+
* configured. A malformed reminders file is swallowed here on purpose — a broken
|
|
43
|
+
* prompt is worse than a missing reminder — while `agents reminders` surfaces it.
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveReminderPart(sessionId?: string): string;
|
|
33
46
|
export declare function runClaudeStatusLine(): Promise<number>;
|
|
34
47
|
export declare function installClaudeStatusLine(versionHome: string): {
|
|
35
48
|
changed: boolean;
|
|
@@ -5,6 +5,7 @@ import { spawnSync } from 'child_process';
|
|
|
5
5
|
import { readClaudeHomeConfig } from './agent-spec/agents.js';
|
|
6
6
|
import { atomicWriteFileSync } from './fs-atomic.js';
|
|
7
7
|
import { mergeClaudeUsageCacheWindows } from './accounting/usage.js';
|
|
8
|
+
import { loadReminders, pickReminderForSession } from './reminders.js';
|
|
8
9
|
export const CLAUDE_STATUSLINE_COMMAND = 'agents __claude-statusline';
|
|
9
10
|
const DELEGATE_FILE = path.join('.agents', 'claude-statusline-delegate');
|
|
10
11
|
// The private subcommand this feature runs. It is only ever invoked internally,
|
|
@@ -103,7 +104,16 @@ export function renderDelegate(payload, versionHome) {
|
|
|
103
104
|
});
|
|
104
105
|
return result.status === 0 ? result.stdout.trim() : '';
|
|
105
106
|
}
|
|
106
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Format a reminder as a dimmed statusline part (empty string when none). The
|
|
109
|
+
* ◆ marker distinguishes it from the host/model/usage parts, and the ANSI dim
|
|
110
|
+
* keeps it quiet next to the live figures.
|
|
111
|
+
*/
|
|
112
|
+
export function formatReminderPart(short) {
|
|
113
|
+
const text = short?.trim();
|
|
114
|
+
return text ? `\x1b[2m◆ ${text}\x1b[22m` : '';
|
|
115
|
+
}
|
|
116
|
+
export function renderClaudeStatusLine(payload, host = os.hostname().split('.')[0] || os.hostname(), delegated = '', reminder = '') {
|
|
107
117
|
const model = payload.model?.display_name?.trim() || payload.model?.id?.trim() || 'model pending';
|
|
108
118
|
const parts = [host, model];
|
|
109
119
|
if (delegated)
|
|
@@ -114,8 +124,23 @@ export function renderClaudeStatusLine(payload, host = os.hostname().split('.')[
|
|
|
114
124
|
parts.push(`5h ${Math.round(fiveHour)}%`);
|
|
115
125
|
if (Number.isFinite(sevenDay))
|
|
116
126
|
parts.push(`7d ${Math.round(sevenDay)}%`);
|
|
127
|
+
if (reminder)
|
|
128
|
+
parts.push(reminder);
|
|
117
129
|
return parts.join(' · ');
|
|
118
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Resolve the per-session reminder for the statusline, or '' when none is
|
|
133
|
+
* configured. A malformed reminders file is swallowed here on purpose — a broken
|
|
134
|
+
* prompt is worse than a missing reminder — while `agents reminders` surfaces it.
|
|
135
|
+
*/
|
|
136
|
+
export function resolveReminderPart(sessionId) {
|
|
137
|
+
try {
|
|
138
|
+
return formatReminderPart(pickReminderForSession(loadReminders(), sessionId)?.short);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return '';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
119
144
|
export async function runClaudeStatusLine() {
|
|
120
145
|
const raw = await new Promise((resolve, reject) => {
|
|
121
146
|
let input = '';
|
|
@@ -135,7 +160,7 @@ export async function runClaudeStatusLine() {
|
|
|
135
160
|
const versionHome = versionHomeFromEnv(process.env);
|
|
136
161
|
if (versionHome)
|
|
137
162
|
ingestClaudeStatusLineUsage(payload, versionHome);
|
|
138
|
-
process.stdout.write(renderClaudeStatusLine(payload, undefined, versionHome ? renderDelegate(raw, versionHome) : ''));
|
|
163
|
+
process.stdout.write(renderClaudeStatusLine(payload, undefined, versionHome ? renderDelegate(raw, versionHome) : '', resolveReminderPart(payload.session_id)));
|
|
139
164
|
return 0;
|
|
140
165
|
}
|
|
141
166
|
export function installClaudeStatusLine(versionHome) {
|