@indigoai-us/hq-cli 5.345.179 → 5.345.180
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 +20 -0
- package/dist/command-catalog.generated.d.ts +126 -0
- package/dist/command-catalog.generated.js +155 -0
- package/dist/commands/bot.d.ts +29 -1
- package/dist/commands/bot.js +245 -13
- package/dist/commands/channels.d.ts +18 -1
- package/dist/commands/channels.js +185 -1
- package/dist/commands/lanes.d.ts +1 -1
- package/dist/commands/lanes.js +6 -2
- package/dist/commands/mesh.js +19 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2285 -1743
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/bot/config.d.ts +8 -0
- package/dist/lib/bot/fleet-local.d.ts +1 -1
- package/dist/lib/bot/fleet-local.js +82 -30
- package/dist/lib/bot/run.js +39 -2
- package/dist/lib/bot/runtime-auth.d.ts +119 -0
- package/dist/lib/bot/runtime-auth.js +251 -0
- package/dist/lib/daemon/catalog.d.ts +1 -0
- package/dist/lib/daemon/catalog.js +1 -1
- package/dist/lib/daemon/run.d.ts +2 -0
- package/dist/lib/daemon/run.js +64 -16
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/hot-channels/adapters/meetings.js +3 -1
- package/dist/lib/hot-channels/contract.d.ts +6 -0
- package/dist/lib/hot-channels/ledger.js +17 -0
- package/dist/lib/hot-channels/loop.js +2 -0
- package/dist/lib/lanes/accounts.d.ts +1 -17
- package/dist/lib/lanes/accounts.js +5 -45
- package/dist/lib/lanes/provider-home.d.ts +24 -0
- package/dist/lib/lanes/provider-home.js +52 -0
- package/dist/lib/lanes/spawn.d.ts +12 -0
- package/dist/lib/lanes/spawn.js +27 -0
- package/dist/lib/mesh/live/daemon/doctor.d.ts +3 -0
- package/dist/lib/mesh/live/daemon/doctor.js +13 -0
- package/dist/lib/mesh/live/daemon/owner-inbox-push.d.ts +94 -0
- package/dist/lib/mesh/live/daemon/owner-inbox-push.js +273 -0
- package/dist/lib/mesh/live/daemon/run.js +31 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +14 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.js +210 -3
- package/dist/lib/mesh/live/paths.d.ts +2 -0
- package/dist/lib/mesh/live/paths.js +2 -0
- package/dist/lib/sync-core/flag.d.ts +1 -1
- package/dist/lib/sync-core/flag.js +1 -1
- package/dist/lib/sync-core/reconcile.d.ts +1 -1
- package/dist/lib/sync-core/reconcile.js +9 -7
- package/dist/lib/sync-core/release.d.ts +3 -3
- package/dist/lib/sync-core/release.js +4 -4
- package/dist/lib/sync-core/rollout-state.d.ts +1 -0
- package/dist/lib/sync-core/rollout-state.js +1 -0
- 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
|
|
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 {
|
|
18
|
+
import { accountMaxRunning } from "./config.js";
|
|
20
19
|
import { listLaneIds, readLane } from "./store.js";
|
|
21
20
|
import { loadQuotaState, quotaBlockFor } from "./quota.js";
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -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
|