@phnx-labs/agents-cli 1.22.115 → 1.22.116

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 (88) hide show
  1. package/CHANGELOG.md +149 -14
  2. package/README.md +1 -1
  3. package/dist/commands/browser.js +11 -0
  4. package/dist/commands/exec.js +212 -141
  5. package/dist/commands/feed.js +65 -14
  6. package/dist/commands/sessions-picker.d.ts +11 -0
  7. package/dist/commands/sessions-picker.js +88 -7
  8. package/dist/commands/sessions.d.ts +21 -2
  9. package/dist/commands/sessions.js +157 -3
  10. package/dist/commands/setup-secrets.d.ts +2 -2
  11. package/dist/commands/setup-term.d.ts +24 -0
  12. package/dist/commands/setup-term.js +70 -0
  13. package/dist/commands/setup.d.ts +1 -1
  14. package/dist/commands/setup.js +12 -4
  15. package/dist/commands/ssh.js +1 -69
  16. package/dist/lib/accounting/rotate.d.ts +63 -1
  17. package/dist/lib/accounting/rotate.js +56 -0
  18. package/dist/lib/accounts/add.js +2 -2
  19. package/dist/lib/accounts/slots.js +32 -2
  20. package/dist/lib/answer-router.d.ts +11 -2
  21. package/dist/lib/answer-router.js +26 -2
  22. package/dist/lib/auth-mint.d.ts +5 -4
  23. package/dist/lib/auth-mint.js +4 -3
  24. package/dist/lib/browser/drivers/arc.d.ts +1 -1
  25. package/dist/lib/browser/service.d.ts +10 -0
  26. package/dist/lib/browser/service.js +208 -36
  27. package/dist/lib/browser/types.d.ts +18 -0
  28. package/dist/lib/config-keys.d.ts +1 -1
  29. package/dist/lib/config-keys.js +5 -0
  30. package/dist/lib/device-config.js +61 -0
  31. package/dist/lib/devices/doctor-findings.js +2 -6
  32. package/dist/lib/feed/answer.d.ts +153 -4
  33. package/dist/lib/feed/answer.js +716 -105
  34. package/dist/lib/feed/feed.d.ts +61 -1
  35. package/dist/lib/feed/feed.js +226 -14
  36. package/dist/lib/feed/hub-server.d.ts +58 -3
  37. package/dist/lib/feed/hub-server.js +306 -54
  38. package/dist/lib/feed/pr-status.d.ts +8 -0
  39. package/dist/lib/feed/pr-status.js +9 -1
  40. package/dist/lib/feed-outcome.d.ts +1 -1
  41. package/dist/lib/feed-outcome.js +9 -2
  42. package/dist/lib/feed-policy.js +9 -3
  43. package/dist/lib/fleet/auth-sync.d.ts +2 -55
  44. package/dist/lib/fleet/auth-sync.js +2 -89
  45. package/dist/lib/harness-auth-capabilities.js +7 -2
  46. package/dist/lib/hosts/dispatch.d.ts +20 -1
  47. package/dist/lib/hosts/dispatch.js +52 -30
  48. package/dist/lib/hosts/remote-cmd.d.ts +21 -0
  49. package/dist/lib/hosts/remote-cmd.js +26 -2
  50. package/dist/lib/mailbox.d.ts +12 -0
  51. package/dist/lib/mailbox.js +16 -2
  52. package/dist/lib/menubar/snapshot.d.ts +51 -0
  53. package/dist/lib/menubar/snapshot.js +42 -3
  54. package/dist/lib/open-url.js +2 -2
  55. package/dist/lib/projects.d.ts +23 -0
  56. package/dist/lib/projects.js +78 -0
  57. package/dist/lib/secrets-cli.d.ts +3 -3
  58. package/dist/lib/secrets-cli.js +1 -1
  59. package/dist/lib/session/active.d.ts +1 -0
  60. package/dist/lib/session/active.js +8 -0
  61. package/dist/lib/session/db.d.ts +67 -3
  62. package/dist/lib/session/db.js +381 -126
  63. package/dist/lib/session/prompt.d.ts +23 -7
  64. package/dist/lib/session/prompt.js +46 -8
  65. package/dist/lib/session/remote/remote-list.d.ts +20 -0
  66. package/dist/lib/session/remote/remote-list.js +22 -6
  67. package/dist/lib/session/remote/watch.d.ts +12 -0
  68. package/dist/lib/session/remote/watch.js +9 -0
  69. package/dist/lib/session/remote-preview-cache.d.ts +29 -0
  70. package/dist/lib/session/remote-preview-cache.js +373 -0
  71. package/dist/lib/session/tail.d.ts +50 -0
  72. package/dist/lib/session/tail.js +219 -0
  73. package/dist/lib/setup-tool-install.js +2 -1
  74. package/dist/lib/setup-tool-status.d.ts +1 -1
  75. package/dist/lib/setup-tool-status.js +6 -1
  76. package/dist/lib/signin-badge.d.ts +19 -4
  77. package/dist/lib/signin-badge.js +29 -11
  78. package/dist/lib/term-driver.d.ts +24 -0
  79. package/dist/lib/term-driver.js +36 -0
  80. package/dist/lib/terminal/index.d.ts +1 -1
  81. package/dist/lib/terminal/index.js +1 -1
  82. package/dist/lib/terminal/inject.d.ts +38 -0
  83. package/dist/lib/terminal/inject.js +55 -9
  84. package/dist/lib/terminal/transport.d.ts +15 -5
  85. package/dist/lib/terminal/transport.js +61 -11
  86. package/package.json +1 -1
  87. package/dist/lib/fleet/remote-login.d.ts +0 -170
  88. package/dist/lib/fleet/remote-login.js +0 -568
@@ -2,7 +2,9 @@ import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { buildRoutineListJson } from '../scheduling/routines.js';
4
4
  import { backfillActiveRowsFromIndex, isRunningLiveSession, serializeActiveSessionsForJson, serializeSessionsJson } from '../session/active.js';
5
- import { getConfigValue, loadAutoLaunchPreferences } from '../device-config.js';
5
+ import { getConfigValue, listConfiguredDeviceRoles, loadAutoLaunchPreferences } from '../device-config.js';
6
+ import { filterAutoPool } from '../devices/pool.js';
7
+ import { isFreshDeviceStats, readStatsCache } from '../devices/stats-cache.js';
6
8
  import { MENUBAR_MENU_PROPERTIES } from '../config-keys.js';
7
9
  import { migrateMenubarPreferencesFromUserDefaults } from './migrate-prefs.js';
8
10
  import { loadDevices } from '../devices/registry.js';
@@ -35,12 +37,20 @@ export function buildMenuPreferences() {
35
37
  */
36
38
  async function buildMenubarDevices() {
37
39
  const reg = await loadDevices();
40
+ const roster = Object.keys(reg);
38
41
  // Pass the roster so a fleet-wide default (fleet.defaults.config) reaches
39
42
  // devices that have no doc of their own.
40
- const prefs = loadAutoLaunchPreferences(Object.keys(reg));
43
+ const prefs = loadAutoLaunchPreferences(roster);
44
+ const roles = listConfiguredDeviceRoles(roster);
45
+ // The placement verdict from the one canonical filter — not a re-implementation
46
+ // of the role rule (`devices/pool.ts` owns it).
47
+ const autoEligible = new Set(filterAutoPool(roster, { roles, autoLaunch: prefs }));
48
+ // Cache read only: no ssh, no probe, so this still rides the existing snapshot
49
+ // poll (docs/menubar.md: the menu bar must not probe the fleet per render).
50
+ const stats = readStatsCache();
41
51
  const interactiveHost = getConfigValue('interactive.host').value;
42
52
  const self = machineId();
43
- return Object.keys(reg)
53
+ return roster
44
54
  .sort()
45
55
  .map((name) => ({
46
56
  name,
@@ -50,8 +60,37 @@ async function buildMenubarDevices() {
50
60
  interactive: name === interactiveHost,
51
61
  isLocal: name === self,
52
62
  preferred: prefs[name]?.preferred === true,
63
+ role: roles[name] ?? 'unknown',
64
+ autoEligible: autoEligible.has(name),
65
+ stats: projectDeviceStats(stats[name]),
53
66
  }));
54
67
  }
68
+ /**
69
+ * Project one cached {@link DeviceStats} row into the snapshot shape, or `null`
70
+ * when the device has no cached reading at all.
71
+ *
72
+ * `?? null` rather than a default: an absent number means "not observed", and the
73
+ * menu renders that as unavailable. A 0 would be indistinguishable from a real
74
+ * measurement of an idle box or a full disk.
75
+ */
76
+ export function projectDeviceStats(row) {
77
+ if (!row)
78
+ return null;
79
+ return {
80
+ reachable: row.reachable,
81
+ observedAt: new Date(row.fetchedAt).toISOString(),
82
+ stale: !isFreshDeviceStats(row),
83
+ cpus: row.ncpu ?? null,
84
+ memTotalBytes: row.memTotalBytes ?? null,
85
+ memFreeBytes: row.memFreeBytes ?? null,
86
+ memPercent: row.memPercent ?? null,
87
+ diskTotalBytes: row.diskTotalBytes ?? null,
88
+ diskFreeBytes: row.diskFreeBytes ?? null,
89
+ diskUsedPercent: row.diskUsedPercent ?? null,
90
+ loadPercent: row.loadPercent ?? null,
91
+ specsObservedAt: row.specsFetchedAt ? new Date(row.specsFetchedAt).toISOString() : null,
92
+ };
93
+ }
55
94
  export function readLastWatchdogTick(stateDir = path.join(getRuntimeStateDir(), 'watchdog')) {
56
95
  try {
57
96
  return JSON.parse(fs.readFileSync(path.join(stateDir, 'last-tick.json'), 'utf-8'));
@@ -9,10 +9,10 @@
9
9
  * The OS handler has none of that.
10
10
  *
11
11
  * Before this seam existed, `agents browser navigate` honoured the configured
12
- * profile and nothing else did: `fleet login`, `devices lease`, `feedback`, and
12
+ * profile and nothing else did: `devices lease`, `feedback`, and
13
13
  * the browser-session artifact opener each shelled straight to `open`/`xdg-open`,
14
14
  * so every one of them landed in whatever the OS handler happened to be. This
15
- * module replaces all of those call sites; do not add a sixth raw `open`.
15
+ * module replaces all of those call sites; do not add a new raw `open`.
16
16
  *
17
17
  * Never throws. A viewer that cannot be reached degrades to the OS handler with
18
18
  * one stderr line naming the reason, and a total failure returns `via: 'none'`
@@ -207,6 +207,29 @@ export declare function projectDirsAbs(def: ProjectDef, opts: {
207
207
  * home-relative variant lands (see docs/concepts.md).
208
208
  */
209
209
  export declare function projectNameForCwd(cwd: string | undefined, defs: ProjectDef[]): string | undefined;
210
+ export declare function listProjectDefsCached(): ProjectDef[];
211
+ /** Drop the {@link listProjectDefsCached} memo — for tests that rewrite the dir within one mtime tick. */
212
+ export declare function resetProjectDefsCache(): void;
213
+ /**
214
+ * The **confirmed** project for a working directory, or `undefined` when the
215
+ * association is not confirmed (PHNX-3999 F08/F09).
216
+ *
217
+ * Confirmed means exactly one thing: a registered project definition
218
+ * ({@link projectNameForCwd}) whose root contains this path. Being inside *some*
219
+ * git repository is NOT a project association — a repo nobody registered, a
220
+ * checkout of someone else's code, or a loose directory that merely has a
221
+ * `.git` in it would each invent a project name out of a folder, which is the
222
+ * wrong-grouping the owner's recording shows at 01:30–01:56. So is a bare
223
+ * directory basename ({@link resolveProjectKey}, which always answers).
224
+ *
225
+ * A consumer renders `undefined` as Uncategorized and keeps the session
226
+ * reachable in its full list — nothing is hidden by not being grouped.
227
+ *
228
+ * Use {@link resolveProjectNameForCwd} instead when you want a best-effort
229
+ * bucket KEY for joining rows; use this when the answer is shown to a person as
230
+ * "this work belongs to that project".
231
+ */
232
+ export declare function confirmedProjectForCwd(cwd: string | undefined | null, defs?: ProjectDef[]): string | undefined;
210
233
  /**
211
234
  * The canonical project label for a cwd, for every surface that buckets work by
212
235
  * project (the activity timeline, feed posts, the sessions overview): the
@@ -455,6 +455,84 @@ export function projectNameForCwd(cwd, defs) {
455
455
  }
456
456
  return best ?? weakBest;
457
457
  }
458
+ /**
459
+ * Definition list, memoized against a stamp over the definition FILES, for the
460
+ * per-tick readers (PHNX-3999).
461
+ *
462
+ * {@link listProjectDefs} parses every `<name>.yaml`, which is the right cost for
463
+ * a command but not for a row builder on the `feed watch` / `sessions watch`
464
+ * stream — that runs twice a second for as long as an editor window is open (see
465
+ * `AGENTS.md`, every tick is budgeted). One `readdir` plus one `stat` per
466
+ * definition replaces N YAML parses.
467
+ *
468
+ * The stamp is each file's name + mtime + size, NOT the directory's mtime: editing
469
+ * a definition in place — retargeting a project's `root`, which is exactly what
470
+ * changes which sessions belong to it — moves the FILE's mtime and leaves the
471
+ * directory's untouched, so a directory stamp would serve a stale association for
472
+ * the life of the process. An unreadable directory is not cached: the next call
473
+ * retries.
474
+ */
475
+ let projectDefsMemo = null;
476
+ function projectDefsStamp() {
477
+ try {
478
+ const dir = getProjectsDir();
479
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.yaml')).sort();
480
+ return files
481
+ .map((f) => {
482
+ try {
483
+ const st = fs.statSync(path.join(dir, f));
484
+ return `${f}:${st.mtimeMs}:${st.size}`;
485
+ }
486
+ catch {
487
+ // Removed between readdir and stat — its absence is part of the stamp.
488
+ return `${f}:gone`;
489
+ }
490
+ })
491
+ .join('\n');
492
+ }
493
+ catch {
494
+ return null;
495
+ }
496
+ }
497
+ export function listProjectDefsCached() {
498
+ const stamp = projectDefsStamp();
499
+ // No projects dir (or unreadable): nothing is confirmed, and nothing to cache.
500
+ if (stamp === null)
501
+ return [];
502
+ if (projectDefsMemo && projectDefsMemo.stamp === stamp)
503
+ return projectDefsMemo.defs;
504
+ const defs = listProjectDefs();
505
+ projectDefsMemo = { stamp, defs };
506
+ return defs;
507
+ }
508
+ /** Drop the {@link listProjectDefsCached} memo — for tests that rewrite the dir within one mtime tick. */
509
+ export function resetProjectDefsCache() {
510
+ projectDefsMemo = null;
511
+ }
512
+ /**
513
+ * The **confirmed** project for a working directory, or `undefined` when the
514
+ * association is not confirmed (PHNX-3999 F08/F09).
515
+ *
516
+ * Confirmed means exactly one thing: a registered project definition
517
+ * ({@link projectNameForCwd}) whose root contains this path. Being inside *some*
518
+ * git repository is NOT a project association — a repo nobody registered, a
519
+ * checkout of someone else's code, or a loose directory that merely has a
520
+ * `.git` in it would each invent a project name out of a folder, which is the
521
+ * wrong-grouping the owner's recording shows at 01:30–01:56. So is a bare
522
+ * directory basename ({@link resolveProjectKey}, which always answers).
523
+ *
524
+ * A consumer renders `undefined` as Uncategorized and keeps the session
525
+ * reachable in its full list — nothing is hidden by not being grouped.
526
+ *
527
+ * Use {@link resolveProjectNameForCwd} instead when you want a best-effort
528
+ * bucket KEY for joining rows; use this when the answer is shown to a person as
529
+ * "this work belongs to that project".
530
+ */
531
+ export function confirmedProjectForCwd(cwd, defs = listProjectDefsCached()) {
532
+ if (!cwd)
533
+ return undefined;
534
+ return projectNameForCwd(cwd, defs);
535
+ }
458
536
  /**
459
537
  * The canonical project label for a cwd, for every surface that buckets work by
460
538
  * project (the activity timeline, feed posts, the sessions overview): the
@@ -1,9 +1,9 @@
1
1
  export declare const SECRETS_CLI_NAME = "secrets";
2
2
  export declare const SECRETS_CLI_PACKAGE = "@phnx-labs/secrets-cli";
3
3
  /** Published standalone `agents setup secrets` installs; bump with the protocol. */
4
- export declare const SECRETS_CLI_VERSION = "0.1.4";
5
- export declare const SECRETS_CLI_SPEC = "@phnx-labs/secrets-cli@0.1.4";
6
- export declare const SECRETS_CLI_INSTALL_HINT = "npm i -g @phnx-labs/secrets-cli@0.1.4";
4
+ export declare const SECRETS_CLI_VERSION = "0.1.5";
5
+ export declare const SECRETS_CLI_SPEC = "@phnx-labs/secrets-cli@0.1.5";
6
+ export declare const SECRETS_CLI_INSTALL_HINT = "npm i -g @phnx-labs/secrets-cli@0.1.5";
7
7
  /**
8
8
  * True when `$SECRETS_BIN` is set or a real `secrets` executable is on PATH
9
9
  * outside agents-cli's shims dir. Does not spawn the binary.
@@ -15,7 +15,7 @@ import { findInPath } from './agent-spec/agents.js';
15
15
  export const SECRETS_CLI_NAME = 'secrets';
16
16
  export const SECRETS_CLI_PACKAGE = '@phnx-labs/secrets-cli';
17
17
  /** Published standalone `agents setup secrets` installs; bump with the protocol. */
18
- export const SECRETS_CLI_VERSION = '0.1.4';
18
+ export const SECRETS_CLI_VERSION = '0.1.5';
19
19
  export const SECRETS_CLI_SPEC = `${SECRETS_CLI_PACKAGE}@${SECRETS_CLI_VERSION}`;
20
20
  export const SECRETS_CLI_INSTALL_HINT = `npm i -g ${SECRETS_CLI_SPEC}`;
21
21
  /**
@@ -71,6 +71,7 @@ export declare function activeSessionProjectKey(s: Pick<ActiveSession, 'cwd' | '
71
71
  export declare function serializeActiveSessionsForJson(sessions: ActiveSession[]): Array<Omit<ActiveSession, 'viewingIn'> & {
72
72
  ticketId: string | null;
73
73
  project: string;
74
+ confirmedProject: string | null;
74
75
  prLink: string | null;
75
76
  viewingIn: string | null;
76
77
  }>;
@@ -38,6 +38,7 @@ import { computeTokPerSec } from './throughput.js';
38
38
  import { inferSessionState } from './state.js';
39
39
  import { isSessionTrackedAgent, SESSION_AGENTS, AG_TMUX_NAME_RE } from './types.js';
40
40
  import { AGENTS } from '../agents.js';
41
+ import { confirmedProjectForCwd, listProjectDefsCached } from '../projects.js';
41
42
  import { detectProvenance } from './provenance.js';
42
43
  import { loadDevices } from '../devices/registry.js';
43
44
  import { machineId, normalizeHost } from '../machine-id.js';
@@ -227,10 +228,17 @@ export function activeSessionProjectKey(s) {
227
228
  return s.context === 'cloud' ? 'cloud' : 'other';
228
229
  }
229
230
  export function serializeActiveSessionsForJson(sessions) {
231
+ // One definition read for the whole batch, not per row (PHNX-3999 F08/F09).
232
+ const defs = listProjectDefsCached();
230
233
  return sessions.map((s) => ({
231
234
  ...s,
232
235
  ticketId: s.ticket?.id ?? null,
233
236
  project: activeSessionProjectKey(s),
237
+ // The project a person is shown this row under, or null for Uncategorized.
238
+ // `project` above is the always-present join KEY (basename of the cwd, or the
239
+ // explicit cloud/other bucket) and stays exactly as it was — grouping by it is
240
+ // what filed unbound directories as projects of their own.
241
+ confirmedProject: confirmedProjectForCwd(s.cwd, defs) ?? null,
234
242
  prLink: s.pr?.url ?? null,
235
243
  viewingIn: viewingInLabel(s) ?? null,
236
244
  }));
@@ -31,8 +31,13 @@ export declare const SCHEMA_VERSION = 50;
31
31
  * v4 (PHNX-3621 leftover) invalidates Grok rows stamped before the bounded
32
32
  * chat_history.jsonl prefix read filled firstUserMessage.
33
33
  * v5 (PHNX-3939) restores first-meta Codex fork ownership, including cold files.
34
+ * v6 (PHNX-3999) invalidates rows whose stored `label` is harness scaffolding — a
35
+ * `<bash-input>` shell echo, a `<command-name>` wrapper, a bare `/clear` — which
36
+ * `cleanGeneratedSessionLabel` now rejects. The label is written at index time, so
37
+ * without this bump every already-indexed session keeps showing the junk title
38
+ * until its transcript happens to change.
34
39
  */
35
- export declare const CONTENT_INDEX_VERSION = 5;
40
+ export declare const CONTENT_INDEX_VERSION = 6;
36
41
  /**
37
42
  * Bumping this invalidates every cached facet row without touching the schema
38
43
  * version, so a change to the extraction logic (a new metric, a corrected bucket)
@@ -106,7 +111,9 @@ export interface QueryOptions {
106
111
  plugin?: string;
107
112
  }
108
113
  /** Open (or return the cached) sessions database, applying migrations as needed. */
109
- export declare function getDB(): Database.Database;
114
+ export declare function getDB(initialBusyTimeoutMs?: number): Database.Database;
115
+ /** Bound synchronous cache contention without changing other database callers. */
116
+ export declare function withSessionDBTimeout<T>(timeoutMs: number, operation: () => T): T;
110
117
  /** Close the cached database connection. */
111
118
  export declare function closeDB(): void;
112
119
  interface FtsOptimizeResult {
@@ -428,6 +435,60 @@ export declare function writeSessionPreviewCache<T>(entry: {
428
435
  fileSize: number | null;
429
436
  preview: T;
430
437
  }): void;
438
+ /** Bump when the cached remote preview envelope shape changes so cached rows recompute (PHNX-3999 v1). */
439
+ export declare const REMOTE_PREVIEW_SCHEMA_VERSION = 1;
440
+ /**
441
+ * Per-envelope cache-write cap. The bounded fields above make a well-formed
442
+ * envelope small; this exists to refuse an outlier (a version-skewed peer, or
443
+ * a future field that forgets to bound itself) rather than let ONE session
444
+ * blow the total budget. A refused write is NOT an error — the caller still
445
+ * gets the live envelope for this call, it simply is not persisted, so the
446
+ * next call re-fetches instead of silently caching a giant blob.
447
+ */
448
+ export declare const REMOTE_PREVIEW_ENVELOPE_MAX_BYTES: number;
449
+ export interface RemotePreviewCacheRow {
450
+ /** When the envelope currently stored here (if `ok`) was fetched. */
451
+ fetchedAt: number;
452
+ /** Whether `envelope` is a real, successfully-fetched payload. */
453
+ ok: boolean;
454
+ envelope?: unknown;
455
+ failureReason?: string;
456
+ consecutiveFailures: number;
457
+ /** Epoch ms before which a fresh fetch attempt should be skipped (negative backoff). */
458
+ nextAttemptAt: number;
459
+ /** The caller's own last-observed `--revision` cursor, or undefined if no
460
+ * caller has ever supplied one for this (device, sessionId) pair. */
461
+ lastCallerRevision?: string;
462
+ }
463
+ /** Read the durable cached remote preview row for one (device, sessionId) pair,
464
+ * regardless of whether it currently holds a successful envelope. Undefined
465
+ * means this box has never attempted (or recorded) a fetch for that pair. */
466
+ export declare function readRemotePreviewCache(device: string, sessionId: string): RemotePreviewCacheRow | undefined;
467
+ /**
468
+ * Record the caller's own `--revision` cursor for one (device, sessionId)
469
+ * pair, independent of any fetch. A no-op if no row exists yet (nothing to
470
+ * compare against on a session this box has never fetched). This is what lets
471
+ * "same revision as last time" serve the durable cache with zero SSH
472
+ * indefinitely — the comparison is against THIS value, never against the
473
+ * envelope's own `details.sourceRevision` (which may be a different format).
474
+ */
475
+ export declare function writeRemotePreviewCallerRevision(device: string, sessionId: string, revision: string): void;
476
+ /**
477
+ * Record a successful remote fetch: replaces the payload and resets backoff.
478
+ * An envelope over {@link REMOTE_PREVIEW_ENVELOPE_MAX_BYTES} is refused —
479
+ * this is a write-path bound, not a request failure: the caller already has
480
+ * the live envelope for this call from the fetch that produced it, this only
481
+ * decides whether it is worth persisting.
482
+ */
483
+ export declare function writeRemotePreviewCacheSuccess(device: string, sessionId: string, envelope: unknown, fetchedAt?: number, revision?: string): void;
484
+ /**
485
+ * Record a failed remote fetch attempt with exponential backoff, WITHOUT
486
+ * discarding a prior good envelope — a session that answered once and is now
487
+ * offline still degrades to that last-good payload (read back via
488
+ * {@link readRemotePreviewCache}'s `ok`/`envelope`), annotated stale by the
489
+ * caller, rather than losing it the moment one attempt fails.
490
+ */
491
+ export declare function writeRemotePreviewCacheFailure(device: string, sessionId: string, reason: string, backoffMs: (consecutiveFailures: number) => number, now?: number): void;
431
492
  /**
432
493
  * The session's durable user-turn text, as stored in the `session_text` FTS
433
494
  * `content` column at scan time (all harnesses, keyed by session_id). This is
@@ -510,7 +571,10 @@ export declare function readSessionTimelineEntry(id: string): SessionTimelineCac
510
571
  * read path. Deliberately does NOT parse the resume state: a live row needs the
511
572
  * 8 steps and the request, never the fold's bookkeeping.
512
573
  */
513
- export declare function readSessionTimelineAny(id: string): SessionTimelineProjection | undefined;
574
+ export declare function readSessionTimelineAny(id: string, stamp?: {
575
+ fileMtimeMs: number;
576
+ fileSize: number;
577
+ }): SessionTimelineProjection | undefined;
514
578
  /** Persist a folded timeline against the exact transcript bytes it was folded to. */
515
579
  export declare function writeSessionTimeline(entry: {
516
580
  id: string;