@phnx-labs/agents-cli 1.22.8 → 1.22.9

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +5 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/exec.js +41 -9
  5. package/dist/commands/feed.js +38 -1
  6. package/dist/commands/harness.d.ts +40 -1
  7. package/dist/commands/harness.js +316 -76
  8. package/dist/commands/profiles.d.ts +42 -0
  9. package/dist/commands/profiles.js +91 -3
  10. package/dist/commands/sessions-picker.js +3 -3
  11. package/dist/commands/sessions-render.d.ts +12 -0
  12. package/dist/commands/sessions-render.js +124 -0
  13. package/dist/commands/sessions.js +28 -1
  14. package/dist/commands/ssh.js +26 -3
  15. package/dist/commands/teams.js +12 -1
  16. package/dist/index.js +9 -2
  17. package/dist/lib/crabbox/cli.d.ts +35 -0
  18. package/dist/lib/crabbox/cli.js +46 -0
  19. package/dist/lib/crabbox/config.d.ts +21 -0
  20. package/dist/lib/crabbox/config.js +43 -0
  21. package/dist/lib/crabbox/lease.d.ts +15 -5
  22. package/dist/lib/crabbox/lease.js +57 -17
  23. package/dist/lib/daemon.js +12 -11
  24. package/dist/lib/devices/resolve-target.d.ts +4 -3
  25. package/dist/lib/devices/resolve-target.js +4 -3
  26. package/dist/lib/hosts/passthrough.js +18 -1
  27. package/dist/lib/hosts/registry.d.ts +4 -0
  28. package/dist/lib/hosts/registry.js +16 -0
  29. package/dist/lib/hosts/remote-cmd.js +1 -0
  30. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  31. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  32. package/dist/lib/observe-aliases.d.ts +30 -0
  33. package/dist/lib/observe-aliases.js +56 -0
  34. package/dist/lib/redact.js +4 -0
  35. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  36. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  37. package/dist/lib/session/parse.d.ts +5 -1
  38. package/dist/lib/session/parse.js +23 -13
  39. package/dist/lib/session/prompt.js +5 -0
  40. package/dist/lib/session/render.d.ts +3 -0
  41. package/dist/lib/session/render.js +33 -10
  42. package/dist/lib/startup/command-registry.js +15 -1
  43. package/dist/lib/usage-refresh.d.ts +19 -11
  44. package/dist/lib/usage-refresh.js +75 -43
  45. package/dist/lib/usage.d.ts +12 -0
  46. package/dist/lib/usage.js +37 -6
  47. package/package.json +1 -1
@@ -888,14 +888,14 @@ export async function runDaemon() {
888
888
  };
889
889
  const fleetCacheInterval = setInterval(() => { void runFleetCacheWarm(); }, 3 * 60_000);
890
890
  const fleetCacheKickoff = setTimeout(() => { void runFleetCacheWarm(); }, 60_000);
891
- // Adaptive usage refresh: keep the usage cache the `agents run` router reads
891
+ // Usage refresh: keep the usage cache the `agents run` router reads
892
892
  // (RUSH-2061, readOnly hot path) fresh, WITHOUT the hot path ever fetching.
893
- // This host is the sole writer for its own local accounts. The tick wakes at
894
- // the 90s floor, but per-account cadence gates the actual live fetches: an
895
- // account racing toward its 5h cap is polled sooner (down to 90s), an idle one
896
- // rarely (up to 15min), capped at ~6 provider calls/account/hour and skipped
897
- // entirely while its provider is under a 429 backoff. Overlap-guarded like the
898
- // probes above; a box signed into no networked-usage account is a clean no-op.
893
+ // This host is the sole writer for its own local accounts. The tick wakes
894
+ // every 60s (USAGE_REFRESH_TICK_MS) to consider due accounts; each account
895
+ // is scheduled at a fixed 5-minute cadence (REFRESH_INTERVAL_MS), capped at
896
+ // ~12 provider calls/account/hour, skipped under 429 backoff, and fetched
897
+ // with fileOnly credentials so a background tick never pops macOS Touch ID.
898
+ // Overlap-guarded: a slow pass cannot stack concurrent refresh loops.
899
899
  let refreshingUsage = false;
900
900
  const runUsageRefreshTick = async () => {
901
901
  if (refreshingUsage)
@@ -910,9 +910,9 @@ export async function runDaemon() {
910
910
  writeUsageCache: writeClaudeUsageCache,
911
911
  backoffUntil: usageRateLimitedUntil,
912
912
  });
913
- if (r.refreshed > 0 || r.failed > 0) {
914
- log('INFO', `usage refresh: ${r.refreshed} refreshed, ${r.failed} failed, ${r.skippedNotDue} not-due, ${r.skippedBackoff} backed-off, ${r.skippedCap} capped`);
915
- }
913
+ // Always log a compact summary so "is refresh working?" is greppable even
914
+ // when every account was not-due (proves the tick ran).
915
+ log('INFO', `usage refresh: ${r.refreshed} refreshed, ${r.failed} failed, ${r.skippedNotDue} not-due, ${r.skippedBackoff} backed-off, ${r.skippedCap} capped`);
916
916
  }
917
917
  catch (err) {
918
918
  log('ERROR', `usage refresh failed: ${err.message}`);
@@ -921,7 +921,8 @@ export async function runDaemon() {
921
921
  refreshingUsage = false;
922
922
  }
923
923
  };
924
- const usageRefreshInterval = setInterval(() => { void runUsageRefreshTick(); }, 90_000);
924
+ // 60s wake matches USAGE_REFRESH_TICK_MS in usage-refresh.ts (keep in sync).
925
+ const usageRefreshInterval = setInterval(() => { void runUsageRefreshTick(); }, 60_000);
925
926
  const usageRefreshKickoff = setTimeout(() => { void runUsageRefreshTick(); }, 30_000);
926
927
  // RUSH-1817: the startup host decision above is one-shot. If a standalone
927
928
  // broker answered agentPing() at daemon start, the daemon declined to host —
@@ -1,4 +1,4 @@
1
- import { splitUserHost } from '../hosts/registry.js';
1
+ import { splitUserHost, type MatchHostOptions } from '../hosts/registry.js';
2
2
  import { type DeviceProfile } from './registry.js';
3
3
  export { splitUserHost };
4
4
  /** A dialable peer: the ssh target, the machine id used to tag its rows, a
@@ -21,9 +21,10 @@ export interface ResolvedExplicitTargetSet {
21
21
  * `user@host` / IP / FQDN literal yields a synthesized key-auth profile. A bare
22
22
  * unregistered alias (no `@`/dot) — or an ssh_config-only alias, which `agents
23
23
  * ssh` has never dialed — returns undefined so the caller reports "Unknown
24
- * device" rather than dialing a literal.
24
+ * device" rather than dialing a literal. `token` may also be the `auto`
25
+ * affinity sentinel (RUSH-2185); `opts.resolveAuto` overrides the pick (tests).
25
26
  */
26
- export declare function resolveDeviceTarget(token: string): Promise<DeviceProfile | undefined>;
27
+ export declare function resolveDeviceTarget(token: string, opts?: Pick<MatchHostOptions, 'resolveAuto'>): Promise<DeviceProfile | undefined>;
27
28
  /**
28
29
  * Resolve an explicit `--host`/`--device` list to dialable targets. A token that
29
30
  * fails the injection guard or names nothing reachable is skipped with a stderr
@@ -71,10 +71,11 @@ async function toResolvedTarget(token) {
71
71
  * `user@host` / IP / FQDN literal yields a synthesized key-auth profile. A bare
72
72
  * unregistered alias (no `@`/dot) — or an ssh_config-only alias, which `agents
73
73
  * ssh` has never dialed — returns undefined so the caller reports "Unknown
74
- * device" rather than dialing a literal.
74
+ * device" rather than dialing a literal. `token` may also be the `auto`
75
+ * affinity sentinel (RUSH-2185); `opts.resolveAuto` overrides the pick (tests).
75
76
  */
76
- export async function resolveDeviceTarget(token) {
77
- const host = await matchHost(token, { allowBareLiteral: true });
77
+ export async function resolveDeviceTarget(token, opts = {}) {
78
+ const host = await matchHost(token, { allowBareLiteral: true, ...opts });
78
79
  if (!host)
79
80
  return undefined;
80
81
  if (host.device) {
@@ -24,6 +24,7 @@ import { dispatchAgentsCommand, withActorEnv } from './dispatch.js';
24
24
  import { stripRoutingFlags, buildRemoteAgentsInvocation, HOST_ROUTING_SPECS, } from './remote-cmd.js';
25
25
  import { resolveRemoteOsSync } from './remote-os.js';
26
26
  import { machineId } from '../session/sync/config.js';
27
+ import { isDeviceAuto, resolveDeviceAffinity } from '../smart-launch.js';
27
28
  import { loadDevices } from '../devices/registry.js';
28
29
  import { isSelfHost } from '../devices/self-host.js';
29
30
  import { fanOutDevices, planFleetTargets, runLocalCommand, runOnDevice, } from '../devices/fleet.js';
@@ -396,7 +397,7 @@ export async function maybeRunOnHost(command, allArgs, opts) {
396
397
  const deviceFlag = flagValue(allArgs, 'device');
397
398
  const hostsFlag = flagValue(allArgs, 'hosts');
398
399
  const devicesFlag = flagValue(allArgs, 'devices');
399
- const hostName = hostFlag ?? deviceFlag;
400
+ let hostName = hostFlag ?? deviceFlag;
400
401
  const fleetAll = isFleetAllSentinel(hostFlag, deviceFlag, hostsFlag, devicesFlag);
401
402
  // Proceed when any routing flag is present, including the plural fleet flags
402
403
  // that may carry the `all` sentinel.
@@ -460,6 +461,22 @@ export async function maybeRunOnHost(command, allArgs, opts) {
460
461
  // flags and bare flags were already handled.
461
462
  if (!hostName)
462
463
  return false;
464
+ // `auto` is the same affinity sentinel `agents run --device auto` resolves
465
+ // (RUSH-2185) — pick the concrete target up front via resolveDeviceAffinity so
466
+ // the isSelfHost check right below (which compares a literal name, not "auto")
467
+ // still catches a local pick and runs the command locally rather than
468
+ // resolving to a real Host and self-SSHing (or, if this box isn't itself a
469
+ // registered device, dialing a literal, nonexistent host named "auto").
470
+ if (isDeviceAuto(hostName)) {
471
+ const plan = resolveDeviceAffinity({});
472
+ if (!plan.host) {
473
+ const stripped = stripRoutingFlags(allArgs, STRIP_SPECS);
474
+ process.argv = [process.argv[0], process.argv[1], ...stripped];
475
+ return false;
476
+ }
477
+ process.stderr.write(chalk.gray(`[agents] device=auto → ${plan.host}\n`));
478
+ hostName = plan.host;
479
+ }
463
480
  // Running against your own machine is just a local run — skip the SSH round-trip.
464
481
  // Match EVERY identity the box answers to (short id, loopback, tailscale
465
482
  // dnsName), not just machineId() — a `--host <self-dnsName>` used to slip past a
@@ -19,6 +19,7 @@
19
19
  import type { Host, HostProvider, HostProviderId } from './types.js';
20
20
  import { DeviceOffloadUnsupportedError } from './types.js';
21
21
  import { type DeviceProfile } from '../devices/registry.js';
22
+ import { type DeviceAffinityPlan } from '../smart-launch.js';
22
23
  export { DeviceOffloadUnsupportedError };
23
24
  export declare function getProvider(id: HostProviderId): HostProvider;
24
25
  export declare function getAllProviders(): HostProvider[];
@@ -52,6 +53,9 @@ export interface MatchHostOptions {
52
53
  * "Unknown device" verdict reachable).
53
54
  */
54
55
  allowBareLiteral?: boolean;
56
+ /** Override the affinity pick for the `auto` sentinel (tests). Defaults to
57
+ * `resolveDeviceAffinity({})` — the same engine `agents run --device auto` uses. */
58
+ resolveAuto?: () => DeviceAffinityPlan;
55
59
  }
56
60
  /**
57
61
  * The one place a `--host` / `--device` token becomes a resolved host. Reads the
@@ -25,6 +25,8 @@ import { readMeta } from '../state.js';
25
25
  import { isSshConfigHost } from './ssh-config.js';
26
26
  import { resolveRemoteOsSync } from './remote-os.js';
27
27
  import { loadDevices, isControlDevice } from '../devices/registry.js';
28
+ import { isDeviceAuto, resolveDeviceAffinity } from '../smart-launch.js';
29
+ import { localMachineId } from '../session/origin-machine.js';
28
30
  // Re-export so existing importers (tests, commands) keep their path; the class
29
31
  // itself lives in types.ts so providers can throw it without a circular import.
30
32
  export { DeviceOffloadUnsupportedError };
@@ -146,6 +148,20 @@ function literalHost(token, host, user) {
146
148
  * into a dispatch verdict.
147
149
  */
148
150
  export async function matchHost(name, opts = {}) {
151
+ // `auto` is the same affinity sentinel `agents run --device auto` resolves
152
+ // (isDeviceAuto / resolveDeviceAffinity in ../smart-launch.js) — shared here so
153
+ // every caller through this one core (ssh, teams, dispatch/passthrough) picks a
154
+ // device the SAME way `run` does instead of rejecting it as "Unknown device
155
+ // 'auto'" (RUSH-2185). A `null` plan.host means the affinity engine picked this
156
+ // very machine; resolve that as the local device/host entry (if any) rather than
157
+ // returning nothing — callers that already special-case "target is this
158
+ // machine" (teams add/create, the passthrough self-host check) then treat it as
159
+ // local exactly as they would if the user had typed the local name.
160
+ if (isDeviceAuto(name)) {
161
+ const plan = (opts.resolveAuto ?? (() => resolveDeviceAffinity({})))();
162
+ const picked = plan.host ?? normalizeHost(localMachineId());
163
+ return matchHost(picked, opts);
164
+ }
149
165
  try {
150
166
  assertValidSshTarget(name);
151
167
  }
@@ -113,6 +113,7 @@ export const RUN_OPTION_FORWARDING = {
113
113
  lease: 'local-only',
114
114
  box: 'local-only',
115
115
  keepBox: 'local-only',
116
+ fresh: 'local-only', // skips the warm-pool reuse for --lease; the lease path is always local
116
117
  reuse: 'local-only', // reuse-picker choice for --lease; the lease path is always local
117
118
  bare: 'local-only', // skips the local setup-copy push; lease-only concern
118
119
  tailscale: 'local-only', // --tailscale/--no-tailscale gate the lease net mode; never forwarded
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Observe-umbrella aliases (Phase 3 surface consolidation).
3
+ *
4
+ * Thin name → real command expansion. No store merge — feed / sessions / events
5
+ * remain the stores; these are doors that point at the right reader.
6
+ *
7
+ * inbox → feed (needs-you default)
8
+ * timeline → feed --filter updates (agent progress stream)
9
+ * roster → sessions --active (live agent roster)
10
+ *
11
+ * `audit` is NOT an alias here — `agents audit` is already the tamper-evident
12
+ * run-dispatch log. Ops trail = `agents events` (optionally `--audit`).
13
+ */
14
+ export type ObserveAlias = 'inbox' | 'timeline' | 'roster';
15
+ export declare const OBSERVE_ALIASES: readonly ObserveAlias[];
16
+ export interface ObserveExpandResult {
17
+ /** argv for the real command (no program name): e.g. ['feed', '--filter', 'updates'] */
18
+ argv: string[];
19
+ /** One-line note for stderr (optional); empty when silent. */
20
+ note: string;
21
+ }
22
+ /** True when `rest` already carries a `--filter` / `--filter=…` flag. */
23
+ export declare function hasFilterFlag(rest: readonly string[]): boolean;
24
+ /** True when `rest` already carries `--active`. */
25
+ export declare function hasActiveFlag(rest: readonly string[]): boolean;
26
+ /**
27
+ * Expand an observe alias + remaining user args into the real command argv.
28
+ * Returns null when `alias` is not an observe alias.
29
+ */
30
+ export declare function expandObserveAlias(alias: string, rest?: readonly string[]): ObserveExpandResult | null;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Observe-umbrella aliases (Phase 3 surface consolidation).
3
+ *
4
+ * Thin name → real command expansion. No store merge — feed / sessions / events
5
+ * remain the stores; these are doors that point at the right reader.
6
+ *
7
+ * inbox → feed (needs-you default)
8
+ * timeline → feed --filter updates (agent progress stream)
9
+ * roster → sessions --active (live agent roster)
10
+ *
11
+ * `audit` is NOT an alias here — `agents audit` is already the tamper-evident
12
+ * run-dispatch log. Ops trail = `agents events` (optionally `--audit`).
13
+ */
14
+ export const OBSERVE_ALIASES = ['inbox', 'timeline', 'roster'];
15
+ /** True when `rest` already carries a `--filter` / `--filter=…` flag. */
16
+ export function hasFilterFlag(rest) {
17
+ return rest.some((a) => a === '--filter' || a.startsWith('--filter='));
18
+ }
19
+ /** True when `rest` already carries `--active`. */
20
+ export function hasActiveFlag(rest) {
21
+ return rest.some((a) => a === '--active');
22
+ }
23
+ /**
24
+ * Expand an observe alias + remaining user args into the real command argv.
25
+ * Returns null when `alias` is not an observe alias.
26
+ */
27
+ export function expandObserveAlias(alias, rest = []) {
28
+ const tail = [...rest];
29
+ switch (alias) {
30
+ case 'inbox':
31
+ return {
32
+ argv: ['feed', ...tail],
33
+ note: 'agents inbox → agents feed (needs-you inbox)',
34
+ };
35
+ case 'timeline': {
36
+ const argv = hasFilterFlag(tail)
37
+ ? ['feed', ...tail]
38
+ : ['feed', '--filter', 'updates', ...tail];
39
+ return {
40
+ argv,
41
+ note: 'agents timeline → agents feed --filter updates',
42
+ };
43
+ }
44
+ case 'roster': {
45
+ const argv = hasActiveFlag(tail)
46
+ ? ['sessions', ...tail]
47
+ : ['sessions', '--active', ...tail];
48
+ return {
49
+ argv,
50
+ note: 'agents roster → agents sessions --active',
51
+ };
52
+ }
53
+ default:
54
+ return null;
55
+ }
56
+ }
@@ -2,6 +2,10 @@
2
2
  * Shared redaction helpers for text that may be exported or logged.
3
3
  */
4
4
  const SECRET_PATTERNS = [
5
+ // Local home paths identify operators and disclose internal filesystem layout.
6
+ // Keep the useful path suffix while masking the machine-specific home prefix.
7
+ [/(^|[\s,"'`(=:])\/(?:home|Users)\/[^/\s,"'`]+/g, '$1[HOME]'],
8
+ [/(^|[\s,"'`(=])[A-Z]:\\Users\\[^\\\s,"'`]+/gi, '$1[HOME]'],
5
9
  [/\bAKIA[0-9A-Z]{16}\b/g, '[REDACTED_AWS_KEY]'],
6
10
  // GitHub: classic PATs (ghp_), OAuth (gho_), app/refresh/server tokens
7
11
  // (ghs_/ghr_), and fine-grained PATs (github_pat_). All share the 36-char
@@ -31,7 +31,11 @@ export declare function safeReadSessionFile(filePath: string, maxBytes?: number)
31
31
  /**
32
32
  * Auto-detect agent type from file path and parse the session.
33
33
  */
34
- export declare function parseSession(filePath: string, agent?: SessionAgentId): SessionEvent[];
34
+ export interface ParseSessionOptions {
35
+ /** Keep normalized tool results compact by default; renderers can request full output. */
36
+ maxToolOutputChars?: number;
37
+ }
38
+ export declare function parseSession(filePath: string, agent?: SessionAgentId, opts?: ParseSessionOptions): SessionEvent[];
35
39
  /** Infer the agent type from a session file path using known directory conventions. */
36
40
  export declare function detectAgent(filePath: string): SessionAgentId | null;
37
41
  /**
@@ -112,10 +112,12 @@ export function safeReadSessionFile(filePath, maxBytes = SESSION_FILE_MAX_BYTES)
112
112
  }
113
113
  return fs.readFileSync(filePath, 'utf-8');
114
114
  }
115
- /**
116
- * Auto-detect agent type from file path and parse the session.
117
- */
118
- export function parseSession(filePath, agent) {
115
+ function truncateNormalizedToolOutput(output, maxChars) {
116
+ if (!Number.isFinite(maxChars) || output.length <= maxChars)
117
+ return output;
118
+ return `${output.slice(0, maxChars)}\n\n[Output truncated: ${output.length - maxChars} characters omitted.]`;
119
+ }
120
+ export function parseSession(filePath, agent, opts = {}) {
119
121
  const detected = agent || detectAgent(filePath);
120
122
  if (!detected) {
121
123
  throw new Error(`Cannot detect agent type from path: ${filePath}`);
@@ -166,7 +168,11 @@ export function parseSession(filePath, agent) {
166
168
  // from `!`-prefix runs, `<system-reminder>`, etc.) as `_synthetic` so turn
167
169
  // slicing and `--include user` count only genuine user intent — one place,
168
170
  // every harness, instead of per-consumer regex.
171
+ const maxToolOutputChars = opts.maxToolOutputChars ?? 500;
169
172
  for (const e of events) {
173
+ if (e.type === 'tool_result' && e.output) {
174
+ e.output = truncateNormalizedToolOutput(e.output, maxToolOutputChars);
175
+ }
170
176
  sanitizeEvent(e);
171
177
  if (e.type === 'message' && e.role === 'user' && isSyntheticUserMessage(e.content)) {
172
178
  e._synthetic = true;
@@ -496,7 +502,11 @@ export function parseClaudeContent(content) {
496
502
  exitCode: structured.exitCode,
497
503
  statusCode: structured.statusCode,
498
504
  errorCode: structured.errorCode,
499
- output: output.length > 500 ? output.slice(0, 497) + '...' : output,
505
+ // Not truncated here: `parseSession` caps every event's `output`
506
+ // centrally via `maxToolOutputChars` (default 500, the same
507
+ // bound this site used to hardcode), so the render path can ask
508
+ // for the full text with `Infinity`.
509
+ output,
500
510
  });
501
511
  }
502
512
  if (toolId)
@@ -686,7 +696,7 @@ export function parseCodexContent(content) {
686
696
  exitCode: result.exitCode,
687
697
  statusCode: result.statusCode,
688
698
  errorCode: result.errorCode,
689
- output: result.text.length > 500 ? result.text.slice(0, 497) + '...' : result.text,
699
+ output: result.text,
690
700
  });
691
701
  if (callId)
692
702
  callMap.delete(callId);
@@ -741,7 +751,7 @@ export function parseCodexContent(content) {
741
751
  exitCode: result.exitCode,
742
752
  statusCode: result.statusCode,
743
753
  errorCode: result.errorCode,
744
- output: result.text.length > 500 ? result.text.slice(0, 497) + '...' : result.text,
754
+ output: result.text,
745
755
  });
746
756
  if (callId)
747
757
  callMap.delete(callId);
@@ -865,7 +875,7 @@ export function parseGemini(filePath) {
865
875
  tool: toolName,
866
876
  callId: typeof tc.id === 'string' ? tc.id : undefined,
867
877
  success: tc.status === 'success',
868
- output: output.length > 500 ? output.slice(0, 497) + '...' : output,
878
+ output,
869
879
  });
870
880
  }
871
881
  }
@@ -1242,7 +1252,7 @@ export function parseGrok(filePath) {
1242
1252
  callId,
1243
1253
  success: structuredError ? false : structuredSuccess ? true : undefined,
1244
1254
  outcome: structuredError ? 'error' : structuredSuccess ? 'ok' : 'unknown',
1245
- output: output.length > 500 ? output.slice(0, 497) + '...' : output,
1255
+ output: output,
1246
1256
  });
1247
1257
  if (callId)
1248
1258
  toolCallMap.delete(callId);
@@ -1360,7 +1370,7 @@ export function parseOpenCode(filePath) {
1360
1370
  tool: toolName,
1361
1371
  callId,
1362
1372
  success: state.status === 'completed',
1363
- output: outputStr.length > 500 ? outputStr.slice(0, 497) + '...' : outputStr,
1373
+ output: outputStr,
1364
1374
  });
1365
1375
  }
1366
1376
  break;
@@ -1470,7 +1480,7 @@ export function parseRush(filePath) {
1470
1480
  tool: info?.tool ?? (typeof raw.name === 'string' ? raw.name : 'unknown'),
1471
1481
  callId,
1472
1482
  success,
1473
- output: outputStr.length > 500 ? outputStr.slice(0, 497) + '...' : outputStr,
1483
+ output: outputStr,
1474
1484
  });
1475
1485
  if (callId)
1476
1486
  toolCallMap.delete(callId);
@@ -1680,7 +1690,7 @@ export function parseKimi(filePath) {
1680
1690
  callId,
1681
1691
  success: structuredError ? false : structuredSuccess ? true : undefined,
1682
1692
  outcome: structuredError ? 'error' : structuredSuccess ? 'ok' : 'unknown',
1683
- output: output.length > 500 ? output.slice(0, 497) + '...' : output,
1693
+ output: output,
1684
1694
  });
1685
1695
  if (callId) {
1686
1696
  toolCallMap.delete(callId);
@@ -1825,7 +1835,7 @@ function parseAnthropicMessageJsonl(filePath, agent) {
1825
1835
  else {
1826
1836
  events.push({
1827
1837
  type: 'tool_result', agent, timestamp, tool: toolInfo?.tool, callId: toolId, success: true,
1828
- output: output.length > 500 ? output.slice(0, 497) + '...' : output,
1838
+ output: output,
1829
1839
  });
1830
1840
  }
1831
1841
  if (toolId)
@@ -46,6 +46,11 @@ const SYNTHETIC_USER_MESSAGE_PATTERNS = [
46
46
  /^\s*<\/?user-prompt-submit-hook>/i,
47
47
  /^\s*<\/?persisted-output>/i,
48
48
  /^\s*<permissions instructions>/i,
49
+ /^\s*<(?:apps|plugins|skills)_instructions>/i,
50
+ /^\s*<(?:multi_agent_mode|environment_context)>/i,
51
+ /^\s*## (?:In-flight in this repo|Host & Fleet)/i,
52
+ /^\s*Your current session id is\b/i,
53
+ /^\s*Linear context skipped:/i,
49
54
  /^\s*<collaboration_mode>/i,
50
55
  /^\s*Caveat: The messages below were generated by the user while running/i,
51
56
  /^\s*\[Request interrupted/i,
@@ -108,6 +108,9 @@ export declare function filterEvents(events: SessionEvent[], opts: FilterOptions
108
108
  */
109
109
  export interface RenderConversationMarkdownOptions {
110
110
  redact?: boolean;
111
+ knownSecrets?: readonly string[];
112
+ reasoning?: 'omit' | 'fold' | 'include';
113
+ maxToolOutputChars?: number;
111
114
  }
112
115
  export declare function renderConversationMarkdown(events: SessionEvent[], opts?: RenderConversationMarkdownOptions): string;
113
116
  /**
@@ -923,10 +923,23 @@ export function filterEvents(events, opts) {
923
923
  const sliced = applyTurnSlice(events, opts);
924
924
  return applyRoleFilter(sliced, opts);
925
925
  }
926
+ function markdownFence(content, language = '') {
927
+ const fence = content.includes('```') ? '````' : '```';
928
+ return `${fence}${language}\n${content}\n${fence}`;
929
+ }
930
+ function truncateToolOutput(content, maxChars) {
931
+ if (content.length <= maxChars)
932
+ return content;
933
+ return `${content.slice(0, maxChars)}\n\n[Output truncated: ${content.length - maxChars} characters omitted.]`;
934
+ }
926
935
  export function renderConversationMarkdown(events, opts = {}) {
927
936
  const parts = [];
928
937
  const shouldRedact = opts.redact !== false;
929
- const sanitize = (text) => shouldRedact ? redactSecrets(text) : text;
938
+ const sanitize = (text) => shouldRedact ? redactSecrets(text, opts.knownSecrets) : text;
939
+ // Preserve the long-standing `sessions <id> --markdown` full-fidelity default.
940
+ // The shareable `sessions render` surface passes `omit` explicitly.
941
+ const reasoning = opts.reasoning ?? 'include';
942
+ const maxToolOutputChars = opts.maxToolOutputChars ?? 4000;
930
943
  for (const event of events) {
931
944
  if (event.type === 'message') {
932
945
  if (event.role === 'user') {
@@ -937,27 +950,37 @@ export function renderConversationMarkdown(events, opts = {}) {
937
950
  }
938
951
  }
939
952
  else if (event.type === 'thinking') {
940
- if (event.content)
941
- parts.push(`### Thinking\n\n${sanitize(event.content)}`);
953
+ if (event.content && reasoning === 'include') {
954
+ parts.push(`### Reasoning\n\n${sanitize(event.content)}`);
955
+ }
956
+ else if (event.content && reasoning === 'fold') {
957
+ parts.push(`<details>\n<summary>Reasoning</summary>\n\n${sanitize(event.content)}\n\n</details>`);
958
+ }
942
959
  }
943
960
  else if (event.type === 'tool_use') {
944
961
  const tool = event.tool || 'unknown';
945
962
  if (event.command) {
946
- parts.push(`### Tool: ${tool}\n\n\`\`\`bash\n${sanitize(event.command)}\n\`\`\``);
963
+ const args = event.args && Object.keys(event.args).length > 0
964
+ ? `\n\nArguments:\n\n${markdownFence(sanitize(JSON.stringify(event.args, null, 2)), 'json')}`
965
+ : '';
966
+ parts.push(`### Tool: ${tool}\n\n${markdownFence(sanitize(event.command), 'bash')}${args}`);
967
+ }
968
+ else if (event.args && Object.keys(event.args).length > 0) {
969
+ parts.push(`### Tool: ${tool}\n\nArguments:\n\n${markdownFence(sanitize(JSON.stringify(event.args, null, 2)), 'json')}`);
947
970
  }
948
971
  else if (event.path) {
949
- parts.push(`### Tool: ${tool}\n\n\`${shortenPathTrace(event.path)}\``);
972
+ parts.push(`### Tool: ${tool}\n\n\`${sanitize(shortenPathTrace(event.path))}\``);
950
973
  }
951
974
  else {
952
975
  const summary = summarizeToolUse(tool, event.args);
953
- parts.push(`### Tool: ${tool}\n\n${summary}`);
976
+ parts.push(`### Tool: ${tool}\n\n${sanitize(summary)}`);
954
977
  }
955
978
  }
956
979
  else if (event.type === 'tool_result') {
957
- if (event.content) {
958
- const truncated = event.content.length > 2000 ? event.content.slice(0, 2000) + '\n…' : event.content;
959
- const body = sanitize(truncated);
960
- parts.push(`### Tool Result\n\n\`\`\`\n${body}\n\`\`\``);
980
+ const output = event.content || event.output;
981
+ if (output) {
982
+ const body = sanitize(truncateToolOutput(output, maxToolOutputChars));
983
+ parts.push(`### Tool Result${event.tool ? `: ${event.tool}` : ''}\n\n${markdownFence(body)}`);
961
984
  }
962
985
  }
963
986
  else if (event.type === 'error') {
@@ -89,7 +89,16 @@ export const loadFunnel = async () => (await import('../../commands/funnel.js'))
89
89
  * inherit the root's custom help formatter rather than getting the per-command
90
90
  * recursive pass. Keeping that ordering preserves their `--help` output exactly.
91
91
  */
92
- export const LAZY_COMMAND_NAMES = new Set(['sessions', 'teams', 'cloud', 'message', 'serve']);
92
+ // `roster` is an observe-umbrella alias of `sessions --active` — same module,
93
+ // same SQLite stack, same post-help registration order as sessions.
94
+ export const LAZY_COMMAND_NAMES = new Set([
95
+ 'sessions',
96
+ 'roster',
97
+ 'teams',
98
+ 'cloud',
99
+ 'message',
100
+ 'serve',
101
+ ]);
93
102
  /**
94
103
  * User-typed top-level command name -> ordered list of module loaders to run.
95
104
  *
@@ -194,12 +203,17 @@ export const COMMAND_LOADERS = {
194
203
  setup: [loadSetup],
195
204
  uninstall: [loadUninstall],
196
205
  sessions: [loadSessions],
206
+ // Observe-umbrella alias of sessions --active (same lazy module).
207
+ roster: [loadSessions],
197
208
  teams: [loadTeams],
198
209
  cloud: [loadCloud],
199
210
  message: [loadMessage],
200
211
  send: [loadSend],
201
212
  notify: [loadSend],
202
213
  feed: [loadFeed],
214
+ // Observe-umbrella aliases of feed / feed --filter updates.
215
+ inbox: [loadFeed],
216
+ timeline: [loadFeed],
203
217
  mailboxes: [loadMailboxes],
204
218
  mailbox: [loadMailboxes],
205
219
  serve: [loadServe],
@@ -1,16 +1,23 @@
1
1
  import { type UsageHeadroom, type UsageSnapshot, type UsageInfo } from './usage.js';
2
2
  import type { AgentId } from './types.js';
3
- /** Floor on the adaptive interval: an account seconds from its cap still isn't
4
- * polled faster than this. */
3
+ /**
4
+ * Default schedule between successful (or attempted) live usage fetches for one
5
+ * account. Floor and ceiling of the delay helper are pinned to this so the
6
+ * daemon does not poll faster than 5 minutes even under high burn, and does not
7
+ * let an idle account rot longer than 5 minutes between attempts.
8
+ */
9
+ export declare const REFRESH_INTERVAL_MS: number;
10
+ /** @deprecated alias — use {@link REFRESH_INTERVAL_MS}. Kept for test imports. */
5
11
  export declare const REFRESH_MIN_MS: number;
6
- /** Ceiling on the adaptive interval: an idle account is still re-checked at
7
- * least this often so a cache never silently rots. */
12
+ /** @deprecated alias — use {@link REFRESH_INTERVAL_MS}. Kept for test imports. */
8
13
  export declare const REFRESH_MAX_MS: number;
9
- /** Schedule the next refresh at `minutesToLimit / K` — poll well before the cap,
10
- * not exactly at it. */
14
+ /** Burn-rate divisor retained for the pure delay helper / tests; with min=max
15
+ * the divisor does not change the scheduled interval. */
11
16
  export declare const REFRESH_BURN_DIVISOR = 4;
12
- /** At most this many live fetches per account per rolling hour. */
13
- export declare const HOURLY_CALL_CAP = 6;
17
+ /** At most this many live fetches per account per rolling hour (5m cadence ⇒ 12). */
18
+ export declare const HOURLY_CALL_CAP = 12;
19
+ /** How often the daemon wakes to *consider* a refresh pass (due accounts only). */
20
+ export declare const USAGE_REFRESH_TICK_MS: number;
14
21
  /**
15
22
  * One account's refresh state + published headroom. `sessionUsedPercent` /
16
23
  * `capturedAt` are the prior sample the NEXT tick projects the burn rate from;
@@ -38,9 +45,10 @@ export declare function readHeadroomEntry(usageKey: string): HeadroomEntry | nul
38
45
  /** Merge entries into the cache (best-effort; preserves other accounts' rows). */
39
46
  export declare function writeHeadroomEntries(entries: Record<string, HeadroomEntry>): void;
40
47
  /**
41
- * The adaptive interval until the next refresh, clamped to [MIN, MAX]. A
42
- * shorter `minutesToLimit` (closer to the cap) polls sooner; `null` (unknown /
43
- * idle / not burning) waits the full ceiling.
48
+ * Interval until the next refresh attempt, clamped to [minMs, maxMs].
49
+ * Defaults pin both ends to {@link REFRESH_INTERVAL_MS} (5 minutes) so the
50
+ * live daemon path is a fixed schedule. Tests may pass a wider range to
51
+ * exercise burn-aware scheduling without changing production cadence.
44
52
  */
45
53
  export declare function computeNextRefreshDelayMs(minutesToLimit: number | null, opts?: {
46
54
  minMs?: number;