@indigoai-us/hq-cli 5.345.179 → 5.345.181

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 (52) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/command-catalog.generated.d.ts +126 -0
  3. package/dist/command-catalog.generated.js +155 -0
  4. package/dist/commands/bot.d.ts +29 -1
  5. package/dist/commands/bot.js +282 -22
  6. package/dist/commands/channels.d.ts +18 -1
  7. package/dist/commands/channels.js +185 -1
  8. package/dist/commands/lanes.d.ts +1 -1
  9. package/dist/commands/lanes.js +6 -2
  10. package/dist/commands/mesh.js +19 -0
  11. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2285 -1743
  12. package/dist/hq-cloud-sync-runner/package.json +1 -1
  13. package/dist/lib/bot/config.d.ts +8 -0
  14. package/dist/lib/bot/fleet-local.d.ts +7 -3
  15. package/dist/lib/bot/fleet-local.js +234 -64
  16. package/dist/lib/bot/run.js +39 -2
  17. package/dist/lib/bot/runtime-auth.d.ts +119 -0
  18. package/dist/lib/bot/runtime-auth.js +251 -0
  19. package/dist/lib/daemon/catalog.d.ts +1 -0
  20. package/dist/lib/daemon/catalog.js +1 -1
  21. package/dist/lib/daemon/run.d.ts +2 -0
  22. package/dist/lib/daemon/run.js +64 -16
  23. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  24. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  25. package/dist/lib/hot-channels/adapters/meetings.js +3 -1
  26. package/dist/lib/hot-channels/contract.d.ts +6 -0
  27. package/dist/lib/hot-channels/ledger.js +17 -0
  28. package/dist/lib/hot-channels/loop.js +2 -0
  29. package/dist/lib/lanes/accounts.d.ts +1 -17
  30. package/dist/lib/lanes/accounts.js +5 -45
  31. package/dist/lib/lanes/provider-home.d.ts +24 -0
  32. package/dist/lib/lanes/provider-home.js +52 -0
  33. package/dist/lib/lanes/spawn.d.ts +12 -0
  34. package/dist/lib/lanes/spawn.js +27 -0
  35. package/dist/lib/mesh/live/daemon/doctor.d.ts +3 -0
  36. package/dist/lib/mesh/live/daemon/doctor.js +13 -0
  37. package/dist/lib/mesh/live/daemon/owner-inbox-push.d.ts +94 -0
  38. package/dist/lib/mesh/live/daemon/owner-inbox-push.js +273 -0
  39. package/dist/lib/mesh/live/daemon/run.js +31 -0
  40. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +14 -0
  41. package/dist/lib/mesh/live/daemon/work-subscribe.js +210 -3
  42. package/dist/lib/mesh/live/paths.d.ts +2 -0
  43. package/dist/lib/mesh/live/paths.js +2 -0
  44. package/dist/lib/sync-core/flag.d.ts +1 -1
  45. package/dist/lib/sync-core/flag.js +1 -1
  46. package/dist/lib/sync-core/reconcile.d.ts +1 -1
  47. package/dist/lib/sync-core/reconcile.js +9 -7
  48. package/dist/lib/sync-core/release.d.ts +3 -3
  49. package/dist/lib/sync-core/release.js +4 -4
  50. package/dist/lib/sync-core/rollout-state.d.ts +1 -0
  51. package/dist/lib/sync-core/rollout-state.js +1 -0
  52. package/package.json +8 -8
@@ -48,6 +48,7 @@ import * as yaml from "js-yaml";
48
48
  import { isAuthError } from "../../../utils/auth-error.js";
49
49
  import { MAX_CATCH_UP_WINDOW_MS } from "../contract.js";
50
50
  import { assertSafeSegment, HOT_SIGNAL_ORIGIN, writeFileAtomic } from "../landing.js";
51
+ import { SyncLockLostError } from "../sync-lock.js";
51
52
  import { MEETING_NOTE_PROVIDERS, MEETING_PROVIDERS, sameMeeting, } from "./meeting-providers.js";
52
53
  export const MEETINGS_ADAPTER_ID = "meetings";
53
54
  export const MEETINGS_CHANNEL = "meeting-events";
@@ -450,6 +451,7 @@ export function createMeetingsAdapter(options) {
450
451
  mark = new Date(m.startMs).toISOString();
451
452
  continue;
452
453
  }
454
+ await ctx.beforeWrite?.();
453
455
  const ref = await writeNotes(provider, gateway.connectionId, m, content.text);
454
456
  notes.push({ provider: provider.provider, id: m.id, connectionId: gateway.connectionId, identity: m });
455
457
  events.push(providerEvent(provider.provider, m, ref, content.text, ctx.personUid));
@@ -499,7 +501,7 @@ export function createMeetingsAdapter(options) {
499
501
  catch (err) {
500
502
  // A sign-in or revoked connection must reach run.ts so the ledger
501
503
  // records the reconnect; only other provider errors are swallowed.
502
- if (isProviderAuthFailure(err))
504
+ if (isProviderAuthFailure(err) || err instanceof SyncLockLostError)
503
505
  throw err;
504
506
  warn(`meetings: ${provider.provider} ${gateway.connectionId} failed; its watermark stays put: ${err instanceof Error ? err.message : String(err)}`);
505
507
  }
@@ -79,6 +79,12 @@ export interface PollContext {
79
79
  * the vault and also pulls Fireflies or Granola notes).
80
80
  */
81
81
  gateways?: HotChannelGateway[];
82
+ /**
83
+ * Called before an adapter writes to the vault itself (meetings writes
84
+ * provider notes during fetch). Throws to stop the run when the sync lock
85
+ * was lost.
86
+ */
87
+ beforeWrite?: () => Promise<void>;
82
88
  }
83
89
  export interface PollPage {
84
90
  events: HotEvent[];
@@ -154,6 +154,13 @@ export async function runHotChannelSync(input) {
154
154
  crash = err;
155
155
  }
156
156
  finally {
157
+ // Check the lock before the ledger write too. When it is gone, this
158
+ // line records the lost lock (the one write still allowed) and the run stops.
159
+ if (!(crash instanceof SyncLockLostError) && input.beforeWrite) {
160
+ const guardError = await guardFailure(input.beforeWrite);
161
+ if (guardError !== undefined)
162
+ crash = guardError;
163
+ }
157
164
  const failure = crash !== undefined ? errorText(crash) : (result?.error?.message ?? passFailure);
158
165
  const line = {
159
166
  runId,
@@ -180,6 +187,16 @@ export async function runHotChannelSync(input) {
180
187
  }
181
188
  return { runId, results, entries };
182
189
  }
190
+ /** Run the write guard and return what it threw, or undefined when the lock is still held. */
191
+ async function guardFailure(beforeWrite) {
192
+ try {
193
+ await beforeWrite();
194
+ return undefined;
195
+ }
196
+ catch (err) {
197
+ return err;
198
+ }
199
+ }
183
200
  function errorText(err) {
184
201
  return err instanceof Error ? err.message : String(err);
185
202
  }
@@ -126,6 +126,7 @@ async function runPoll(ctx, adapter, result, key, gateway, gateways = []) {
126
126
  const response = await adapter.fetch(cursor, {
127
127
  since,
128
128
  personUid: ctx.personUid,
129
+ beforeWrite: ctx.beforeWrite,
129
130
  ...(gateway ? { gateway } : {}),
130
131
  ...(gateways.length > 0 ? { gateways } : {}),
131
132
  });
@@ -137,6 +138,7 @@ async function runPoll(ctx, adapter, result, key, gateway, gateways = []) {
137
138
  // Every event on this page is landed (or quarantined as malformed), so the cursor can move past it. An
138
139
  // empty page leaves the stored lastEventKey as it was.
139
140
  cursor = response.cursor;
141
+ await ctx.beforeWrite();
140
142
  await ctx.cursorStore.set(ctx.personKey, key, cursor, lastEventKey);
141
143
  result.cursorAfter = cursor;
142
144
  if (!response.hasMore)
@@ -13,23 +13,7 @@
13
13
  import { type LaneRecord, type LaneProvider } from "./types.js";
14
14
  import type { LanesConfig } from "./types.js";
15
15
  import type { ClaudeCredentialReader, ClaudeTokenState } from "./claude-credential.js";
16
- export declare const PROVIDER_HOME_ENV: Record<LaneProvider, string>;
17
- /**
18
- * Claude Code keys its OS keychain credential by `CLAUDE_CONFIG_DIR`: with
19
- * the variable unset it reads the plain `Claude Code-credentials` item, and
20
- * with it set, even to the default `~/.claude`, it reads a hash-suffixed item
21
- * for that path. Setting the variable for the default home therefore makes a
22
- * lane use a different login than the interactive session (incident
23
- * 2026-10-13). A Claude account whose home is the default is launched with
24
- * the variable unset instead.
25
- */
26
- export declare function isDefaultProviderHome(provider: LaneProvider, home: string, homedir?: () => string): boolean;
27
- /**
28
- * Return a copy of `base` with the provider home variable set to `home`,
29
- * or, for a Claude account on the default home, with the variable removed
30
- * so the worker and the interactive session share one credential.
31
- */
32
- export declare function withProviderHomeEnv(base: NodeJS.ProcessEnv, provider: LaneProvider, home: string, homedir?: () => string): NodeJS.ProcessEnv;
16
+ export { PROVIDER_HOME_ENV, isDefaultProviderHome, withProviderHomeEnv } from "./provider-home.js";
33
17
  /** Who `claude auth status` says a home is logged in as. Never a token. */
34
18
  export interface ClaudeIdentity {
35
19
  email: string | null;
@@ -11,57 +11,17 @@
11
11
  * This module never prints, copies, or hashes credentials.
12
12
  */
13
13
  import * as fs from "node:fs";
14
- import * as os from "node:os";
15
14
  import { effectiveAccountHome } from "./account-home.js";
16
15
  import * as path from "node:path";
17
16
  import { AccountResolutionError } from "./errors.js";
18
17
  import { isTerminalLaneState } from "./types.js";
19
- import { DEFAULT_ACCOUNT_HOMES, accountMaxRunning, canonicalizeHome } from "./config.js";
18
+ import { accountMaxRunning } from "./config.js";
20
19
  import { listLaneIds, readLane } from "./store.js";
21
20
  import { loadQuotaState, quotaBlockFor } from "./quota.js";
22
- export const PROVIDER_HOME_ENV = {
23
- claude: "CLAUDE_CONFIG_DIR",
24
- codex: "CODEX_HOME",
25
- grok: "GROK_HOME",
26
- };
27
- /**
28
- * Claude Code keys its OS keychain credential by `CLAUDE_CONFIG_DIR`: with
29
- * the variable unset it reads the plain `Claude Code-credentials` item, and
30
- * with it set, even to the default `~/.claude`, it reads a hash-suffixed item
31
- * for that path. Setting the variable for the default home therefore makes a
32
- * lane use a different login than the interactive session (incident
33
- * 2026-10-13). A Claude account whose home is the default is launched with
34
- * the variable unset instead.
35
- */
36
- export function isDefaultProviderHome(provider, home, homedir = () => os.homedir()) {
37
- const deps = {
38
- homedir,
39
- realpathSync: (p) => fs.realpathSync(p),
40
- existsSync: (p) => fs.existsSync(p),
41
- };
42
- try {
43
- return canonicalizeHome(home, deps) === canonicalizeHome(DEFAULT_ACCOUNT_HOMES[provider], deps);
44
- }
45
- catch {
46
- return false;
47
- }
48
- }
49
- /**
50
- * Return a copy of `base` with the provider home variable set to `home`,
51
- * or, for a Claude account on the default home, with the variable removed
52
- * so the worker and the interactive session share one credential.
53
- */
54
- export function withProviderHomeEnv(base, provider, home, homedir) {
55
- const next = { ...base };
56
- const envVar = PROVIDER_HOME_ENV[provider];
57
- if (provider === "claude" && isDefaultProviderHome(provider, home, homedir)) {
58
- delete next[envVar];
59
- }
60
- else {
61
- next[envVar] = home;
62
- }
63
- return next;
64
- }
21
+ // Provider home helpers live in a leaf module so bot code can use them
22
+ // without importing the lane store; re-exported for existing callers.
23
+ export { PROVIDER_HOME_ENV, isDefaultProviderHome, withProviderHomeEnv } from "./provider-home.js";
24
+ import { PROVIDER_HOME_ENV, isDefaultProviderHome, withProviderHomeEnv } from "./provider-home.js";
65
25
  function optionalString(value) {
66
26
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
67
27
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Provider home environment helpers, split from accounts.ts so callers that
3
+ * only need to point a provider at a home (bots) do not pull in the lane
4
+ * store and its daemon imports. accounts.ts re-exports these.
5
+ */
6
+ import type { LaneProvider } from "./types.js";
7
+ export declare const PROVIDER_HOME_ENV: Record<LaneProvider, string>;
8
+ /**
9
+ * Claude Code keys its OS keychain credential by `CLAUDE_CONFIG_DIR`: with
10
+ * the variable unset it reads the plain `Claude Code-credentials` item, and
11
+ * with it set, even to the default `~/.claude`, it reads a hash-suffixed item
12
+ * for that path. Setting the variable for the default home therefore makes a
13
+ * lane use a different login than the interactive session (incident
14
+ * 2026-10-13). A Claude account whose home is the default is launched with
15
+ * the variable unset instead.
16
+ */
17
+ export declare function isDefaultProviderHome(provider: LaneProvider, home: string, homedir?: () => string): boolean;
18
+ /**
19
+ * Return a copy of `base` with the provider home variable set to `home`,
20
+ * or, for a Claude account on the default home, with the variable removed
21
+ * so the worker and the interactive session share one credential.
22
+ */
23
+ export declare function withProviderHomeEnv(base: NodeJS.ProcessEnv, provider: LaneProvider, home: string, homedir?: () => string): NodeJS.ProcessEnv;
24
+ //# sourceMappingURL=provider-home.d.ts.map
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Provider home environment helpers, split from accounts.ts so callers that
3
+ * only need to point a provider at a home (bots) do not pull in the lane
4
+ * store and its daemon imports. accounts.ts re-exports these.
5
+ */
6
+ import * as fs from "node:fs";
7
+ import * as os from "node:os";
8
+ import { DEFAULT_ACCOUNT_HOMES, canonicalizeHome } from "./config.js";
9
+ export const PROVIDER_HOME_ENV = {
10
+ claude: "CLAUDE_CONFIG_DIR",
11
+ codex: "CODEX_HOME",
12
+ grok: "GROK_HOME",
13
+ };
14
+ /**
15
+ * Claude Code keys its OS keychain credential by `CLAUDE_CONFIG_DIR`: with
16
+ * the variable unset it reads the plain `Claude Code-credentials` item, and
17
+ * with it set, even to the default `~/.claude`, it reads a hash-suffixed item
18
+ * for that path. Setting the variable for the default home therefore makes a
19
+ * lane use a different login than the interactive session (incident
20
+ * 2026-10-13). A Claude account whose home is the default is launched with
21
+ * the variable unset instead.
22
+ */
23
+ export function isDefaultProviderHome(provider, home, homedir = () => os.homedir()) {
24
+ const deps = {
25
+ homedir,
26
+ realpathSync: (p) => fs.realpathSync(p),
27
+ existsSync: (p) => fs.existsSync(p),
28
+ };
29
+ try {
30
+ return canonicalizeHome(home, deps) === canonicalizeHome(DEFAULT_ACCOUNT_HOMES[provider], deps);
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ }
36
+ /**
37
+ * Return a copy of `base` with the provider home variable set to `home`,
38
+ * or, for a Claude account on the default home, with the variable removed
39
+ * so the worker and the interactive session share one credential.
40
+ */
41
+ export function withProviderHomeEnv(base, provider, home, homedir) {
42
+ const next = { ...base };
43
+ const envVar = PROVIDER_HOME_ENV[provider];
44
+ if (provider === "claude" && isDefaultProviderHome(provider, home, homedir)) {
45
+ delete next[envVar];
46
+ }
47
+ else {
48
+ next[envVar] = home;
49
+ }
50
+ return next;
51
+ }
52
+ //# sourceMappingURL=provider-home.js.map
@@ -204,6 +204,18 @@ export declare const FOREGROUND_ONLY_RULE: string;
204
204
  * install has it, the command fails with "No such file".
205
205
  */
206
206
  export declare function tempHomeRule(hqRoot: string): string;
207
+ /**
208
+ * Self-review rule carried next to the temp-home rule in every composed
209
+ * brief, for every provider. A PR URL reaches the core-pr-reviews channel only
210
+ * after two zero-critical reviews of its head: the lane's own review and an
211
+ * independent senior review. The lane supplies the first one and never posts.
212
+ *
213
+ * The report file under workspace/reviews/ uses the format the hq-core
214
+ * channel guard (block-core-pr-reviews-dm.sh) reads before a URL is posted.
215
+ * The skill path is absolute so codex and grok lanes, which have no /review
216
+ * slash command, can read and follow it.
217
+ */
218
+ export declare function selfReviewRule(hqRoot: string): string;
207
219
  /**
208
220
  * The answers as written by the reconcile pass (`answered-questions.json`),
209
221
  * reduced to id + choice. A malformed file reads as no inline answers; the
@@ -1378,6 +1378,33 @@ export function tempHomeRule(hqRoot) {
1378
1378
  "each fresh home re-downloads the Rust toolchain and pnpm store and is never deleted. " +
1379
1379
  "The wrapper keeps the real caches and removes the temp home when the command exits.");
1380
1380
  }
1381
+ /**
1382
+ * Self-review rule carried next to the temp-home rule in every composed
1383
+ * brief, for every provider. A PR URL reaches the core-pr-reviews channel only
1384
+ * after two zero-critical reviews of its head: the lane's own review and an
1385
+ * independent senior review. The lane supplies the first one and never posts.
1386
+ *
1387
+ * The report file under workspace/reviews/ uses the format the hq-core
1388
+ * channel guard (block-core-pr-reviews-dm.sh) reads before a URL is posted.
1389
+ * The skill path is absolute so codex and grok lanes, which have no /review
1390
+ * slash command, can read and follow it.
1391
+ */
1392
+ export function selfReviewRule(hqRoot) {
1393
+ const root = path.resolve(hqRoot);
1394
+ const skill = path.join(root, ".claude", "skills", "review", "SKILL.md");
1395
+ const reviews = path.join(root, "workspace", "reviews");
1396
+ return ("SELF-REVIEW. Before reporting done on work that opens or updates a PR, " +
1397
+ `run the paranoid /review (${skill}) on your own final diff, fix every critical, ` +
1398
+ "and re-run until there are 0 criticals. " +
1399
+ "Put the report in the PR body under a 'Self-review' heading with the head SHA. " +
1400
+ `Also save it as a file under ${reviews}/ whose first lines are ` +
1401
+ "`Review-Report: hq-pr-review.v1`, `PR: <url>`, `Head: <40-char head SHA>`, " +
1402
+ "`Reviewer-Role: self-review` and `Criticals: 0`. " +
1403
+ "Save only the final zero-critical run in that file: no other line may start with " +
1404
+ "`PR:`, `Head:`, `Reviewer-Role:` or `Criticals:`. " +
1405
+ "A new commit needs a new review of the new head. " +
1406
+ "Never post to core-pr-reviews. If you cannot produce the report, mark the PR as draft.");
1407
+ }
1381
1408
  /**
1382
1409
  * The answers as written by the reconcile pass (`answered-questions.json`),
1383
1410
  * reduced to id + choice. A malformed file reads as no inline answers; the
@@ -3,6 +3,7 @@
3
3
  * outbox/last-flush, and unhealthy when unacked spool age > 60s while online.
4
4
  */
5
5
  import { type CognitoActorKind, type CognitoTokenSource } from "../../../../utils/cognito-session.js";
6
+ import { type OwnerPushStatus } from "./owner-inbox-push.js";
6
7
  import { type UnattributedEventCounts } from "../spool.js";
7
8
  import { type DaemonStateFile } from "./state.js";
8
9
  import { type LegacyListenPlatform } from "./legacy-listen.js";
@@ -70,6 +71,8 @@ export interface DaemonDoctorReport {
70
71
  }>;
71
72
  /** True when PATH lists more than one hq. */
72
73
  multipleHqOnPath: boolean;
74
+ /** Owner DM push signal (person accounts). Content-free counter only. */
75
+ ownerPush?: OwnerPushStatus;
73
76
  }
74
77
  export interface DaemonDoctorDeps {
75
78
  home?: string;
@@ -11,6 +11,7 @@ import { readSessionState } from "../../../work-context/state.js";
11
11
  import { workContextRoot } from "../../../work-context/paths.js";
12
12
  import { outboxQuarantineStatusEnabled } from "../../../work-context/outbox-quarantine-status-flag.js";
13
13
  import { formatDroppedPresenceCompany } from "./credentials.js";
14
+ import { formatOwnerPushStatus, ownerPushStatusForAccount, readOwnerInboxPushState, } from "./owner-inbox-push.js";
14
15
  import { countDistinctHeldEvents, countJsonlLines, countUnattributedEvents, } from "../spool.js";
15
16
  import { workMeshDeadLetterPath, workMeshHeldPath, workMeshRoot, workMeshSpoolPath, } from "../paths.js";
16
17
  import { daemonDir, daemonPidPath } from "./paths.js";
@@ -248,6 +249,14 @@ export function collectDaemonDoctor(deps = {}) {
248
249
  if (emitRetry > 0) {
249
250
  unhealthyReasons.push(`direct-emit retry depth=${emitRetry}`);
250
251
  }
252
+ // Only a person account has an owner DM topic; any other account reports off.
253
+ const ownerPush = ownerPushStatusForAccount(readOwnerInboxPushState(meshRoot), now(), auth.actorKind);
254
+ // Only a subscribed push whose heartbeat stopped counts. A rejected or
255
+ // retrying best-effort subscribe writes connected:false, and an offline
256
+ // daemon stops the heartbeat; neither makes the daemon unhealthy.
257
+ if (running && online && ownerPush.connected && ownerPush.status === "stale") {
258
+ unhealthyReasons.push(`owner push stale (last event ${ownerPush.lastAt ?? "never"})`);
259
+ }
251
260
  return {
252
261
  running,
253
262
  pid: running ? record?.pid : undefined,
@@ -282,6 +291,7 @@ export function collectDaemonDoctor(deps = {}) {
282
291
  emitMode,
283
292
  subscribed,
284
293
  emitRetryDepth: emitRetry,
294
+ ownerPush,
285
295
  lastPostAt: emitState?.lastPostAt,
286
296
  lastEmit: emitState
287
297
  ? {
@@ -341,6 +351,9 @@ export function formatDaemonDoctor(report) {
341
351
  ` unassigned=${report.lastEmit.unassigned ?? 0}` +
342
352
  ` rejected=${report.lastEmit.rejected ?? 0}`
343
353
  : ""}`, `emit retry depth: ${report.emitRetryDepth}`);
354
+ if (report.ownerPush) {
355
+ lines.push(formatOwnerPushStatus(report.ownerPush));
356
+ }
344
357
  if (report.presenceRefusal) {
345
358
  lines.push(`presence refusal: ${report.presenceRefusal.code} (HTTP ${report.presenceRefusal.status}); next retry at ${report.presenceRefusal.nextRetryAt}`);
346
359
  }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Owner (prs_) DM doorbell state for the mesh daemon.
3
+ *
4
+ * The server publishes a wake to `hq/{prs}/dm` for each DM, thread reply and
5
+ * channel post the owner should see. The daemon reads only `type`,
6
+ * `channelId` and `eventId` from that payload, keeps a channel event only when
7
+ * the channel belongs to an allowed company, and bumps a content-free counter
8
+ * in `owner-inbox-push.json`. Message text, poster name and email are never
9
+ * stored, logged or forwarded.
10
+ */
11
+ export { OWNER_INBOX_PUSH_STATE_BASENAME } from "../paths.js";
12
+ /** Heartbeat cadence while the owner DM topic is subscribed. */
13
+ export declare const OWNER_PUSH_HEARTBEAT_MS = 5000;
14
+ /** A heartbeat older than this means the push signal is stale. */
15
+ export declare const OWNER_PUSH_STALE_MS = 15000;
16
+ /** Channel list refreshes at most this often when an unknown channel arrives. */
17
+ export declare const OWNER_CHANNEL_REFRESH_MIN_MS = 60000;
18
+ /** A channel-list fetch that runs longer than this is abandoned. */
19
+ export declare const OWNER_CHANNEL_REFRESH_TIMEOUT_MS = 10000;
20
+ export type OwnerDoorbellType = "dm" | "thread" | "channel";
21
+ export interface OwnerDoorbell {
22
+ type: OwnerDoorbellType;
23
+ channelId?: string;
24
+ eventId?: string;
25
+ }
26
+ /** Every key the state file may contain. Nothing else is ever written. */
27
+ export declare const OWNER_INBOX_PUSH_STATE_KEYS: readonly ["connected", "heartbeatAtMs", "count", "lastAt", "lastType", "principalUid"];
28
+ export interface OwnerInboxPushState {
29
+ connected: boolean;
30
+ heartbeatAtMs: number;
31
+ count: number;
32
+ lastAt?: string;
33
+ lastType?: OwnerDoorbellType;
34
+ principalUid?: string;
35
+ }
36
+ /**
37
+ * Read only `type`, `channelId` and `eventId`. Returns null for anything that
38
+ * is not a dm, thread or channel wake. Other payload fields are discarded.
39
+ */
40
+ export declare function parseOwnerDoorbell(payload: Buffer | string): OwnerDoorbell | null;
41
+ /** Read the state file. Missing, non-regular or malformed files return null. */
42
+ export declare function readOwnerInboxPushState(meshRoot: string, onInfo?: (message: string) => void): OwnerInboxPushState | null;
43
+ /** Atomic write (temp file plus rename), mode 0600, allowed keys only. */
44
+ export declare function writeOwnerInboxPushState(meshRoot: string, state: OwnerInboxPushState): void;
45
+ /**
46
+ * Remove the state file. Used when the account is not a person account, so a
47
+ * file left by an earlier person login cannot report the push as stale.
48
+ */
49
+ export declare function clearOwnerInboxPushState(meshRoot: string): void;
50
+ export type OwnerPushStatusKind = "live" | "stale" | "off";
51
+ export interface OwnerPushStatus {
52
+ status: OwnerPushStatusKind;
53
+ connected: boolean;
54
+ count: number;
55
+ lastAt: string | null;
56
+ lastType: OwnerDoorbellType | null;
57
+ heartbeatAt: string | null;
58
+ heartbeatAgeMs: number | null;
59
+ }
60
+ /**
61
+ * live: connected with a fresh heartbeat. stale: the file exists but the
62
+ * subscriber is disconnected or its heartbeat is older than the stale window.
63
+ * off: no state file (the daemon never subscribed on this machine).
64
+ */
65
+ export declare function ownerPushStatus(state: OwnerInboxPushState | null, nowMs: number, staleMs?: number): OwnerPushStatus;
66
+ /**
67
+ * Status for the signed-in account. Only a person account has an owner DM
68
+ * topic, so any other account kind (agent, outpost, or no login) reports off.
69
+ */
70
+ export declare function ownerPushStatusForAccount(state: OwnerInboxPushState | null, nowMs: number, actorKind: string): OwnerPushStatus;
71
+ export declare function formatOwnerPushStatus(status: OwnerPushStatus): string;
72
+ export interface ChannelCompanyRow {
73
+ channelId: string;
74
+ companyUid?: string;
75
+ }
76
+ export interface OwnerChannelScope {
77
+ /** True only when the channel is known and its company is allowed. */
78
+ isInScope(channelId: string): Promise<boolean>;
79
+ }
80
+ /**
81
+ * Channel filter backed by the owner's channel list. A channel with no
82
+ * company, an unknown channel, or a company outside the allowed set is
83
+ * dropped. The allowed set is read on every check so a session lock change
84
+ * applies at once. The list refreshes at most once per refresh window.
85
+ */
86
+ export declare function createOwnerChannelScope(opts: {
87
+ listChannels: () => Promise<ChannelCompanyRow[]>;
88
+ allowedCompanyUids: () => readonly string[] | undefined;
89
+ now?: () => number;
90
+ refreshMinMs?: number;
91
+ refreshTimeoutMs?: number;
92
+ onInfo?: (message: string) => void;
93
+ }): OwnerChannelScope;
94
+ //# sourceMappingURL=owner-inbox-push.d.ts.map