@getrift/rift 0.1.0-beta.13 → 0.1.0-beta.14
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/README.md +35 -9
- package/dist/src/cli/commands/doctor.d.ts +6 -0
- package/dist/src/cli/commands/doctor.d.ts.map +1 -0
- package/dist/src/cli/commands/doctor.js +183 -0
- package/dist/src/cli/commands/doctor.js.map +1 -0
- package/dist/src/cli/commands/menubar.d.ts +30 -0
- package/dist/src/cli/commands/menubar.d.ts.map +1 -0
- package/dist/src/cli/commands/menubar.js +180 -0
- package/dist/src/cli/commands/menubar.js.map +1 -0
- package/dist/src/cli/commands/onboard.d.ts.map +1 -1
- package/dist/src/cli/commands/onboard.js +37 -30
- package/dist/src/cli/commands/onboard.js.map +1 -1
- package/dist/src/cli/commands/status.d.ts +9 -7
- package/dist/src/cli/commands/status.d.ts.map +1 -1
- package/dist/src/cli/commands/status.js +29 -10
- package/dist/src/cli/commands/status.js.map +1 -1
- package/dist/src/cli/commands/update.d.ts +3 -0
- package/dist/src/cli/commands/update.d.ts.map +1 -1
- package/dist/src/cli/commands/update.js +19 -0
- package/dist/src/cli/commands/update.js.map +1 -1
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +4 -0
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/cli/postinstall-menubar.d.ts +22 -0
- package/dist/src/cli/postinstall-menubar.d.ts.map +1 -0
- package/dist/src/cli/postinstall-menubar.js +39 -0
- package/dist/src/cli/postinstall-menubar.js.map +1 -0
- package/dist/src/diagnostics/doctor.d.ts +106 -0
- package/dist/src/diagnostics/doctor.d.ts.map +1 -0
- package/dist/src/diagnostics/doctor.js +251 -0
- package/dist/src/diagnostics/doctor.js.map +1 -0
- package/dist/src/diagnostics/notify.d.ts +90 -0
- package/dist/src/diagnostics/notify.d.ts.map +1 -0
- package/dist/src/diagnostics/notify.js +177 -0
- package/dist/src/diagnostics/notify.js.map +1 -0
- package/dist/src/diagnostics/repair-prompt.d.ts +49 -0
- package/dist/src/diagnostics/repair-prompt.d.ts.map +1 -0
- package/dist/src/diagnostics/repair-prompt.js +198 -0
- package/dist/src/diagnostics/repair-prompt.js.map +1 -0
- package/dist/src/main.js +43 -4
- package/dist/src/main.js.map +1 -1
- package/dist/src/observability/version-check.d.ts +1 -0
- package/dist/src/observability/version-check.d.ts.map +1 -1
- package/dist/src/observability/version-check.js +2 -1
- package/dist/src/observability/version-check.js.map +1 -1
- package/operator/swiftbar/render-menu.py +444 -0
- package/operator/swiftbar/rift.10s.sh +147 -0
- package/package.json +3 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure diagnosis layer shared by `rift doctor`, the repair-prompt
|
|
3
|
+
* generator, and the daemon's failure notifier.
|
|
4
|
+
*
|
|
5
|
+
* It consumes exactly what `rift status` already consumes — the
|
|
6
|
+
* `/status/friend` payload plus the friend's local signals (Voyage key
|
|
7
|
+
* last-4, MCP-client install presence) — and folds them into a small,
|
|
8
|
+
* severity-ordered list of issues. Each issue carries plain-language
|
|
9
|
+
* copy and one next action, so every surface (CLI, prompt, notification)
|
|
10
|
+
* tells the same story.
|
|
11
|
+
*
|
|
12
|
+
* Issue ordering follows `decideNextLine` in `friend-header.ts` so the
|
|
13
|
+
* two surfaces tell the same story, with one deliberate policy on top:
|
|
14
|
+
* `primary` (the "do this first" action) is always the highest-SEVERITY
|
|
15
|
+
* issue — a `broken` failure outranks any `warning`, regardless of
|
|
16
|
+
* positional order. This is why `rift doctor` can lead with a broken
|
|
17
|
+
* capture failure where `rift status`'s flat, positional Next: line
|
|
18
|
+
* might surface a warning-level inbox rejection first. The lists never
|
|
19
|
+
* contradict each other; `doctor` simply elevates "what is actually
|
|
20
|
+
* broken" to the lead, which is its whole job.
|
|
21
|
+
*
|
|
22
|
+
* This module is intentionally side-effect free: no IO, no clock except
|
|
23
|
+
* the injected `now`, no daemon access. Callers fetch the payload and
|
|
24
|
+
* pass it in.
|
|
25
|
+
*/
|
|
26
|
+
import type { FriendStatusPayload } from "../server/routes/friend-status.js";
|
|
27
|
+
/** Stable identifiers for each diagnosable condition. */
|
|
28
|
+
export type DoctorIssueKind = "daemon_unreachable" | "daemon_auth_failed" | "config_missing" | "voyage_key_missing" | "codex_auth_expired" | "voyage_embed_errors" | "index_write_errors" | "inbox_import_errors" | "capture_failed" | "mcp_not_installed" | "capture_quarantined" | "update_available";
|
|
29
|
+
/**
|
|
30
|
+
* `broken` blocks Rift from doing its job (no capture, no search, no
|
|
31
|
+
* memory). `warning` is degraded-but-working or user-side (a rejected
|
|
32
|
+
* import, a missing optional client, an available update).
|
|
33
|
+
*/
|
|
34
|
+
export type DoctorSeverity = "broken" | "warning";
|
|
35
|
+
export interface DoctorIssue {
|
|
36
|
+
kind: DoctorIssueKind;
|
|
37
|
+
severity: DoctorSeverity;
|
|
38
|
+
/** One-line plain-language headline a friend can read. */
|
|
39
|
+
title: string;
|
|
40
|
+
/** Plain-language explanation of what is wrong and why it matters. */
|
|
41
|
+
detail: string;
|
|
42
|
+
/** Exactly one next action — a command or a concrete step. */
|
|
43
|
+
nextAction: string;
|
|
44
|
+
/**
|
|
45
|
+
* Stable identity of the *cause*, not just the subsystem. For most
|
|
46
|
+
* issues this is the kind, but error-bearing issues fold in the error
|
|
47
|
+
* class (e.g. `voyage_embed_errors:provider_400`) so the notifier can
|
|
48
|
+
* tell "same subsystem, new cause" apart from a still-unresolved
|
|
49
|
+
* repeat. Optional so hand-built issues fall back to `kind`.
|
|
50
|
+
*/
|
|
51
|
+
fingerprint?: string;
|
|
52
|
+
/**
|
|
53
|
+
* True when a *running* daemon can observe this failure and is
|
|
54
|
+
* therefore allowed to raise a native notification about it. A fully
|
|
55
|
+
* dead daemon cannot observe anything, so `daemon_unreachable` is
|
|
56
|
+
* false here — that case needs the Slice D watchdog, not this path.
|
|
57
|
+
*/
|
|
58
|
+
daemonObservable: boolean;
|
|
59
|
+
}
|
|
60
|
+
/** Local-only signals the friend's machine answers without the daemon. */
|
|
61
|
+
export interface DoctorLocalSignals {
|
|
62
|
+
voyageLast4: string | null;
|
|
63
|
+
mcpClients: Array<{
|
|
64
|
+
client: string;
|
|
65
|
+
installed: boolean;
|
|
66
|
+
}>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Why `/status/friend` could not be read. A null status is NOT always
|
|
70
|
+
* "the daemon is down" — the request can fail because there is no config,
|
|
71
|
+
* no token has been issued, or the daemon rejected our auth. Each needs a
|
|
72
|
+
* different next action, so the caller classifies the failure and the
|
|
73
|
+
* diagnosis turns it into the right issue. Defaults to `connection_refused`
|
|
74
|
+
* (the in-process daemon caller never passes null, so this only matters
|
|
75
|
+
* for the CLI).
|
|
76
|
+
*/
|
|
77
|
+
export type DoctorUnreachableReason = "connection_refused" | "auth_failed" | "not_authenticated" | "config_missing" | "unknown";
|
|
78
|
+
export interface DoctorInput {
|
|
79
|
+
/** `/status/friend` payload, or null when it could not be read. */
|
|
80
|
+
status: FriendStatusPayload | null;
|
|
81
|
+
/**
|
|
82
|
+
* When `status` is null, why. Lets the diagnosis distinguish "daemon
|
|
83
|
+
* down" from "cannot authenticate to the daemon" from "no config yet".
|
|
84
|
+
*/
|
|
85
|
+
unreachableReason?: DoctorUnreachableReason;
|
|
86
|
+
localSignals: DoctorLocalSignals;
|
|
87
|
+
/** Injected clock; defaults to Date.now() at the call site. */
|
|
88
|
+
now: number;
|
|
89
|
+
}
|
|
90
|
+
export interface DoctorReport {
|
|
91
|
+
/** True when no `broken` issue is present. Warnings do not flip this. */
|
|
92
|
+
healthy: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* The single most important issue — the one whose `nextAction` a
|
|
95
|
+
* surface should lead with. Null only when there are no issues at all.
|
|
96
|
+
*/
|
|
97
|
+
primary: DoctorIssue | null;
|
|
98
|
+
/** All detected issues, highest priority first. */
|
|
99
|
+
issues: DoctorIssue[];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Diagnose Rift health from the same inputs `rift status` uses. Pure:
|
|
103
|
+
* given identical inputs it always returns the same report.
|
|
104
|
+
*/
|
|
105
|
+
export declare function diagnose(input: DoctorInput): DoctorReport;
|
|
106
|
+
//# sourceMappingURL=doctor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../../src/diagnostics/doctor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAE7E,yDAAyD;AACzD,MAAM,MAAM,eAAe,GACvB,oBAAoB,GACpB,oBAAoB,GACpB,gBAAgB,GAChB,oBAAoB,GACpB,oBAAoB,GACpB,qBAAqB,GACrB,oBAAoB,GACpB,qBAAqB,GACrB,gBAAgB,GAChB,mBAAmB,GACnB,qBAAqB,GACrB,kBAAkB,CAAC;AAEvB;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,SAAS,CAAC;AAElD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,eAAe,CAAC;IACtB,QAAQ,EAAE,cAAc,CAAC;IACzB,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAED,0EAA0E;AAC1E,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAC3D;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,uBAAuB,GAC/B,oBAAoB,GACpB,aAAa,GACb,mBAAmB,GACnB,gBAAgB,GAChB,SAAS,CAAC;AAEd,MAAM,WAAW,WAAW;IAC1B,mEAAmE;IACnE,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACnC;;;OAGG;IACH,iBAAiB,CAAC,EAAE,uBAAuB,CAAC;IAC5C,YAAY,EAAE,kBAAkB,CAAC;IACjC,+DAA+D;IAC/D,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,YAAY;IAC3B,yEAAyE;IACzE,OAAO,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;IAC5B,mDAAmD;IACnD,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB;AA8FD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,YAAY,CAyLzD"}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True only when `iso` parses to a timestamp at/after the daemon's
|
|
3
|
+
* current start. Historical errors that predate the running process are
|
|
4
|
+
* not current and must not be reported as live failures — a healthy
|
|
5
|
+
* respawn should clear them. Mirrors `friend-header.isCurrentUptime`.
|
|
6
|
+
*/
|
|
7
|
+
function isCurrentUptime(iso, daemonStartMs) {
|
|
8
|
+
if (!iso)
|
|
9
|
+
return false;
|
|
10
|
+
const ts = Date.parse(iso);
|
|
11
|
+
if (Number.isNaN(ts))
|
|
12
|
+
return false;
|
|
13
|
+
return ts >= daemonStartMs;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Translate internal error-class names into copy a friend can act on.
|
|
17
|
+
* Falls back to the raw class so a new/unknown value still surfaces.
|
|
18
|
+
* Kept in sync with `friend-header.humanizeErrorClass`.
|
|
19
|
+
*/
|
|
20
|
+
function humanizeErrorClass(reason) {
|
|
21
|
+
if (!reason)
|
|
22
|
+
return "unknown";
|
|
23
|
+
switch (reason) {
|
|
24
|
+
case "schema_mismatch":
|
|
25
|
+
return "index format changed by an update";
|
|
26
|
+
case "lock_conflict":
|
|
27
|
+
return "index busy (a concurrent write held the lock)";
|
|
28
|
+
case "provider_400":
|
|
29
|
+
return "Voyage rejected the request (often an invalid or revoked key)";
|
|
30
|
+
case "network":
|
|
31
|
+
return "a network error reaching Voyage";
|
|
32
|
+
default:
|
|
33
|
+
return reason;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build the single issue for a null `/status/friend`, tailored to *why*
|
|
38
|
+
* the read failed so the next action actually fixes the problem.
|
|
39
|
+
*/
|
|
40
|
+
function unreachableIssue(reason) {
|
|
41
|
+
switch (reason) {
|
|
42
|
+
case "config_missing":
|
|
43
|
+
return {
|
|
44
|
+
kind: "config_missing",
|
|
45
|
+
severity: "broken",
|
|
46
|
+
title: "Rift is not set up on this Mac yet",
|
|
47
|
+
detail: "Rift has no config file, so the command does not know how to " +
|
|
48
|
+
"reach the daemon. This usually means onboarding never finished.",
|
|
49
|
+
nextAction: "Set it up: rift onboard",
|
|
50
|
+
daemonObservable: false,
|
|
51
|
+
};
|
|
52
|
+
case "not_authenticated":
|
|
53
|
+
return {
|
|
54
|
+
kind: "daemon_auth_failed",
|
|
55
|
+
severity: "broken",
|
|
56
|
+
title: "Rift has no access token on this Mac",
|
|
57
|
+
detail: "The daemon may be running, but this command has no token to " +
|
|
58
|
+
"authenticate with it, so it cannot read Rift's status.",
|
|
59
|
+
nextAction: "Issue a token: rift token issue",
|
|
60
|
+
daemonObservable: false,
|
|
61
|
+
};
|
|
62
|
+
case "auth_failed":
|
|
63
|
+
return {
|
|
64
|
+
kind: "daemon_auth_failed",
|
|
65
|
+
severity: "broken",
|
|
66
|
+
title: "Rift's daemon rejected this command's token",
|
|
67
|
+
detail: "The daemon is reachable but refused the token (it may be stale " +
|
|
68
|
+
"or from a different install). This is an authentication problem, " +
|
|
69
|
+
"not a crashed daemon — restarting will not fix it.",
|
|
70
|
+
nextAction: "Re-issue a token: rift token issue",
|
|
71
|
+
daemonObservable: false,
|
|
72
|
+
};
|
|
73
|
+
case "connection_refused":
|
|
74
|
+
case "unknown":
|
|
75
|
+
default:
|
|
76
|
+
return {
|
|
77
|
+
kind: "daemon_unreachable",
|
|
78
|
+
severity: "broken",
|
|
79
|
+
title: "Rift's background engine is not responding",
|
|
80
|
+
detail: "The Rift daemon is the always-on process that captures sessions, " +
|
|
81
|
+
"keeps the search index fresh, and answers your AI tools. Right now " +
|
|
82
|
+
"nothing can reach it, so capture and memory are paused.",
|
|
83
|
+
nextAction: "Restart it: launchctl kickstart -k gui/$UID/com.getrift.daemon",
|
|
84
|
+
daemonObservable: false,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Diagnose Rift health from the same inputs `rift status` uses. Pure:
|
|
90
|
+
* given identical inputs it always returns the same report.
|
|
91
|
+
*/
|
|
92
|
+
export function diagnose(input) {
|
|
93
|
+
const { status, localSignals, now } = input;
|
|
94
|
+
const issues = [];
|
|
95
|
+
// No payload is a root failure, but the *reason* changes the fix:
|
|
96
|
+
// a down daemon needs a restart, an auth failure needs a fresh token,
|
|
97
|
+
// and a missing config needs onboarding. Reporting all three as
|
|
98
|
+
// "restart the daemon" is exactly the misdirection Slice B removes.
|
|
99
|
+
// All are CLI-side diagnoses (the in-process daemon caller always has a
|
|
100
|
+
// payload), so none are daemon-observable — a dead/unauthable daemon
|
|
101
|
+
// cannot notify about itself; that is the Slice D watchdog's job.
|
|
102
|
+
if (!status) {
|
|
103
|
+
const issue = unreachableIssue(input.unreachableReason ?? "connection_refused");
|
|
104
|
+
return { healthy: false, primary: issue, issues: [issue] };
|
|
105
|
+
}
|
|
106
|
+
const daemonStartMs = now - status.daemon.uptime_seconds * 1000;
|
|
107
|
+
// 1. Voyage key missing — without it nothing can be embedded or
|
|
108
|
+
// searched. Highest-priority broken state.
|
|
109
|
+
if (!status.daemon.voyage_key_present) {
|
|
110
|
+
issues.push({
|
|
111
|
+
kind: "voyage_key_missing",
|
|
112
|
+
severity: "broken",
|
|
113
|
+
title: "Voyage search key is missing",
|
|
114
|
+
detail: "Rift uses a Voyage key to turn your text into searchable meaning. " +
|
|
115
|
+
"Without it, nothing new can be indexed and search will not work.",
|
|
116
|
+
nextAction: "Re-add the key: rift onboard --reconfigure-voyage",
|
|
117
|
+
daemonObservable: true,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// 2. Codex auth expired — auto-capture triage runs through the
|
|
121
|
+
// locally-authenticated codex CLI. Expired login halts capture.
|
|
122
|
+
if (status.codex.last_preflight_ok === false) {
|
|
123
|
+
issues.push({
|
|
124
|
+
kind: "codex_auth_expired",
|
|
125
|
+
severity: "broken",
|
|
126
|
+
title: "Codex login has expired",
|
|
127
|
+
detail: "Rift reads your new Claude Code / Codex sessions through the codex " +
|
|
128
|
+
"CLI. Its login has expired, so automatic capture cannot run and new " +
|
|
129
|
+
"conversations are not being saved.",
|
|
130
|
+
nextAction: "Sign back in: codex login (then run: rift capture)",
|
|
131
|
+
daemonObservable: true,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
// 3. Voyage embedding errors during this uptime — the key is present
|
|
135
|
+
// but Voyage is rejecting/dropping calls (often an invalid key or
|
|
136
|
+
// a network problem). Recency-gated so a prior process's error
|
|
137
|
+
// does not haunt a healthy respawn.
|
|
138
|
+
if (isCurrentUptime(status.voyage.last_error_at, daemonStartMs)) {
|
|
139
|
+
issues.push({
|
|
140
|
+
kind: "voyage_embed_errors",
|
|
141
|
+
severity: "broken",
|
|
142
|
+
fingerprint: `voyage_embed_errors:${status.voyage.last_error_reason ?? "unknown"}`,
|
|
143
|
+
title: "Search indexing is failing at the Voyage step",
|
|
144
|
+
detail: "Rift could not turn recent text into searchable meaning: " +
|
|
145
|
+
`${humanizeErrorClass(status.voyage.last_error_reason)}. New content ` +
|
|
146
|
+
"may not be findable until this clears.",
|
|
147
|
+
nextAction: "Inspect with rift status, then check the Voyage key with " +
|
|
148
|
+
"rift onboard --reconfigure-voyage if errors mention the key",
|
|
149
|
+
daemonObservable: true,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
// 4. Index write errors during this uptime — text embedded fine, but
|
|
153
|
+
// the LanceDB write failed (schema mismatch, lock, IO). Distinct
|
|
154
|
+
// from Voyage: "embedded" does not imply "searchable".
|
|
155
|
+
if (isCurrentUptime(status.index.last_error_at, daemonStartMs)) {
|
|
156
|
+
issues.push({
|
|
157
|
+
kind: "index_write_errors",
|
|
158
|
+
severity: "broken",
|
|
159
|
+
fingerprint: `index_write_errors:${status.index.last_error_reason ?? "unknown"}`,
|
|
160
|
+
title: "Search index writes are failing",
|
|
161
|
+
detail: "Rift embedded recent text but could not write it to the search " +
|
|
162
|
+
`index: ${humanizeErrorClass(status.index.last_error_reason)}. ` +
|
|
163
|
+
"Recently captured items may not be searchable yet.",
|
|
164
|
+
nextAction: "Inspect with rift status, then try: rift reindex",
|
|
165
|
+
daemonObservable: true,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
// 5. Capture run failed during this uptime — the auto-capture loop
|
|
169
|
+
// itself threw (distinct from a Codex preflight failure, which is
|
|
170
|
+
// reported above with a more specific action).
|
|
171
|
+
if (status.capture.last_errors > 0 &&
|
|
172
|
+
isCurrentUptime(status.capture.last_run_at, daemonStartMs)) {
|
|
173
|
+
issues.push({
|
|
174
|
+
kind: "capture_failed",
|
|
175
|
+
severity: "broken",
|
|
176
|
+
title: "The last capture run reported errors",
|
|
177
|
+
detail: "Rift's automatic capture ran but hit errors while saving sessions, " +
|
|
178
|
+
"so some recent conversations may not have been stored.",
|
|
179
|
+
nextAction: "Re-run it and watch the output: rift capture",
|
|
180
|
+
daemonObservable: true,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// 6. Inbox import errors — user-side: a dropped export was rejected
|
|
184
|
+
// (malformed JSON, corrupt zip, unsupported source). The fix is to
|
|
185
|
+
// re-drop a fresh export, not to repair the engine. Warning.
|
|
186
|
+
if (status.inbox &&
|
|
187
|
+
isCurrentUptime(status.inbox.last_error_at, daemonStartMs)) {
|
|
188
|
+
issues.push({
|
|
189
|
+
kind: "inbox_import_errors",
|
|
190
|
+
severity: "warning",
|
|
191
|
+
title: "A dropped import could not be read",
|
|
192
|
+
detail: "Rift tried to import a file from your inbox folder but could not " +
|
|
193
|
+
`read it (${status.inbox.last_error_reason ?? "unknown reason"}). ` +
|
|
194
|
+
"This is usually a corrupt or unsupported export, not a Rift fault.",
|
|
195
|
+
nextAction: "Re-export and drop a fresh archive under data/inbox/",
|
|
196
|
+
// User-side; the daemon observes it but the plan scopes
|
|
197
|
+
// notifications to engine failures, not rejected user input.
|
|
198
|
+
daemonObservable: false,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
// 7. MCP not installed for one or more clients — degraded reach, not a
|
|
202
|
+
// broken engine. Warning.
|
|
203
|
+
const missing = localSignals.mcpClients
|
|
204
|
+
.filter((c) => !c.installed)
|
|
205
|
+
.map((c) => c.client);
|
|
206
|
+
if (missing.length > 0) {
|
|
207
|
+
issues.push({
|
|
208
|
+
kind: "mcp_not_installed",
|
|
209
|
+
severity: "warning",
|
|
210
|
+
title: `Rift is not connected to ${missing.join(", ")}`,
|
|
211
|
+
detail: "MCP is the connection that lets your AI tools ask Rift for memory. " +
|
|
212
|
+
`These tools are not wired up yet: ${missing.join(", ")}.`,
|
|
213
|
+
nextAction: `Connect one: rift mcp install --client=${missing[0]}`,
|
|
214
|
+
daemonObservable: false,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
// 8. Quarantined oversized sessions — silent data loss, but a graceful
|
|
218
|
+
// skip rather than an error. Warning.
|
|
219
|
+
if (status.capture.last_quarantined > 0) {
|
|
220
|
+
const n = status.capture.last_quarantined;
|
|
221
|
+
issues.push({
|
|
222
|
+
kind: "capture_quarantined",
|
|
223
|
+
severity: "warning",
|
|
224
|
+
title: `${n} session${n === 1 ? "" : "s"} skipped for being too large`,
|
|
225
|
+
detail: "Rift skipped some oversized sessions during capture instead of " +
|
|
226
|
+
"saving them. They are not lost on disk, but they are not in your " +
|
|
227
|
+
"memory yet.",
|
|
228
|
+
nextAction: "Raise capture.codex_cli.max_session_bytes in config.json and " +
|
|
229
|
+
"restart, or accept the gap",
|
|
230
|
+
daemonObservable: false,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
// 9. Update available — informational nudge, lowest priority.
|
|
234
|
+
if (status.update.available && status.update.latest_beta) {
|
|
235
|
+
issues.push({
|
|
236
|
+
kind: "update_available",
|
|
237
|
+
severity: "warning",
|
|
238
|
+
title: `A newer Rift is available (${status.update.installed} → ${status.update.latest_beta})`,
|
|
239
|
+
detail: "A newer beta build is published.",
|
|
240
|
+
nextAction: "Update now: rift update",
|
|
241
|
+
daemonObservable: false,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
const healthy = !issues.some((i) => i.severity === "broken");
|
|
245
|
+
// Lead with the most severe issue: the first `broken` if any, else the
|
|
246
|
+
// first issue. Broken issues are already pushed ahead of warnings, so
|
|
247
|
+
// this also keeps `primary` stable if the positional order ever shifts.
|
|
248
|
+
const primary = issues.find((i) => i.severity === "broken") ?? issues[0] ?? null;
|
|
249
|
+
return { healthy, primary, issues };
|
|
250
|
+
}
|
|
251
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../../src/diagnostics/doctor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { DoctorIssue, DoctorIssueKind } from "./doctor.js";
|
|
2
|
+
/** Re-notify cadence for an issue that stays unresolved. */
|
|
3
|
+
export declare const RENOTIFY_INTERVAL_MS: number;
|
|
4
|
+
export interface NotificationPayload {
|
|
5
|
+
/** macOS notification title — always the same friend-facing banner. */
|
|
6
|
+
title: string;
|
|
7
|
+
/** One-line subtitle naming the failing subsystem. */
|
|
8
|
+
subtitle: string;
|
|
9
|
+
/** Body: the issue's next action, so the notification is actionable. */
|
|
10
|
+
message: string;
|
|
11
|
+
}
|
|
12
|
+
/** Per-subsystem dedup record. */
|
|
13
|
+
export interface NotifyRecord {
|
|
14
|
+
last_notified_at: string;
|
|
15
|
+
/**
|
|
16
|
+
* The cause fingerprint last notified for this kind (e.g.
|
|
17
|
+
* `voyage_embed_errors:provider_400`). When the live issue's
|
|
18
|
+
* fingerprint differs, the cause changed and the friend should be
|
|
19
|
+
* re-notified immediately — the repair action/risk is now different —
|
|
20
|
+
* rather than staying silent for the rest of the 24h window. Optional
|
|
21
|
+
* for backward compatibility with state written before fingerprinting.
|
|
22
|
+
*/
|
|
23
|
+
fingerprint?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Persisted dedup state. Presence of a kind means it is currently in a
|
|
27
|
+
* failing state and was last notified at `last_notified_at`. Absence
|
|
28
|
+
* means resolved-or-never — so the next occurrence notifies fresh.
|
|
29
|
+
*/
|
|
30
|
+
export interface NotifyState {
|
|
31
|
+
active: Partial<Record<DoctorIssueKind, NotifyRecord>>;
|
|
32
|
+
}
|
|
33
|
+
export interface PlanNotificationsInput {
|
|
34
|
+
/** All diagnosed issues (the full report list, any severity). */
|
|
35
|
+
issues: DoctorIssue[];
|
|
36
|
+
/** Previously persisted dedup state. */
|
|
37
|
+
state: NotifyState;
|
|
38
|
+
now: number;
|
|
39
|
+
}
|
|
40
|
+
export interface PlanNotificationsResult {
|
|
41
|
+
/** Payloads to actually send this tick (already rate-limited). */
|
|
42
|
+
notifications: NotificationPayload[];
|
|
43
|
+
/** State to persist after sending. */
|
|
44
|
+
nextState: NotifyState;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build the friend-facing notification for an issue. Pure. The title is
|
|
48
|
+
* intentionally constant ("Rift needs attention") so the friend learns
|
|
49
|
+
* to recognise it; the subtitle and body carry the specifics.
|
|
50
|
+
*/
|
|
51
|
+
export declare function buildNotificationPayload(issue: DoctorIssue): NotificationPayload;
|
|
52
|
+
/**
|
|
53
|
+
* Decide which notifications to send this tick and what state to persist.
|
|
54
|
+
* Pure: no IO, no clock except the injected `now`.
|
|
55
|
+
*
|
|
56
|
+
* Only `daemonObservable` issues are eligible — the daemon must not
|
|
57
|
+
* notify about things it cannot itself observe (e.g. a dead daemon, or
|
|
58
|
+
* user-side rejected imports).
|
|
59
|
+
*/
|
|
60
|
+
export declare function planNotifications(input: PlanNotificationsInput): PlanNotificationsResult;
|
|
61
|
+
/** Absolute path to the dedup-state file under the data dir. */
|
|
62
|
+
export declare function notifyStatePath(dataDir: string): string;
|
|
63
|
+
/** Read dedup state; returns empty state on any read/parse failure. */
|
|
64
|
+
export declare function readNotifyState(dataDir: string): NotifyState;
|
|
65
|
+
/** Persist dedup state best-effort; swallows IO errors. */
|
|
66
|
+
export declare function writeNotifyState(dataDir: string, state: NotifyState): void;
|
|
67
|
+
/**
|
|
68
|
+
* Send a single notification via macOS `osascript`. Best-effort and
|
|
69
|
+
* fire-and-forget — any failure (non-macOS, osascript missing, user has
|
|
70
|
+
* notifications disabled) is swallowed so capture is never affected.
|
|
71
|
+
*
|
|
72
|
+
* Strings are passed as argv to `execFile` (not interpolated into a
|
|
73
|
+
* shell), and AppleScript string literals are escaped, so notification
|
|
74
|
+
* text cannot inject shell or AppleScript.
|
|
75
|
+
*/
|
|
76
|
+
export declare function sendNotification(payload: NotificationPayload, opts?: {
|
|
77
|
+
platform?: NodeJS.Platform;
|
|
78
|
+
}): void;
|
|
79
|
+
/** Escape a string into an AppleScript double-quoted literal. */
|
|
80
|
+
export declare function asAppleScriptString(value: string): string;
|
|
81
|
+
/**
|
|
82
|
+
* End-to-end daemon helper: given the diagnosed issues, send any due
|
|
83
|
+
* notifications and persist updated dedup state. Best-effort throughout
|
|
84
|
+
* — wired into the capture loop, so it must never throw.
|
|
85
|
+
*/
|
|
86
|
+
export declare function notifyDaemonFailures(dataDir: string, issues: DoctorIssue[], opts?: {
|
|
87
|
+
now?: number;
|
|
88
|
+
platform?: NodeJS.Platform;
|
|
89
|
+
}): void;
|
|
90
|
+
//# sourceMappingURL=notify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../../src/diagnostics/notify.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEhE,4DAA4D;AAC5D,eAAO,MAAM,oBAAoB,QAAsB,CAAC;AAExD,MAAM,WAAW,mBAAmB;IAClC,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,QAAQ,EAAE,MAAM,CAAC;IACjB,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,kCAAkC;AAClC,MAAM,WAAW,YAAY;IAC3B,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,YAAY,CAAC,CAAC,CAAC;CACxD;AAOD,MAAM,WAAW,sBAAsB;IACrC,iEAAiE;IACjE,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,wCAAwC;IACxC,KAAK,EAAE,WAAW,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,uBAAuB;IACtC,kEAAkE;IAClE,aAAa,EAAE,mBAAmB,EAAE,CAAC;IACrC,sCAAsC;IACtC,SAAS,EAAE,WAAW,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,WAAW,GACjB,mBAAmB,CAMrB;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,sBAAsB,GAC5B,uBAAuB,CA+CzB;AAED,gEAAgE;AAChE,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,uEAAuE;AACvE,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,CAW5D;AAED,2DAA2D;AAC3D,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAQ1E;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,mBAAmB,EAC5B,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAA;CAAO,GACxC,IAAI,CAcN;AAED,iEAAiE;AACjE,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,WAAW,EAAE,EACrB,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAA;CAAO,GACtD,IAAI,CAmBN"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native failure notifications for daemon-observed problems.
|
|
3
|
+
*
|
|
4
|
+
* A running daemon can see capture failures, expired Codex login, an
|
|
5
|
+
* invalid/failing Voyage key, and index-write failures. When one of
|
|
6
|
+
* those crosses into a failing state, the friend should get one calm
|
|
7
|
+
* macOS notification that points at the repair surface — not a wall of
|
|
8
|
+
* logs, and not a notification on every poll.
|
|
9
|
+
*
|
|
10
|
+
* What this module does NOT cover: a fully dead daemon cannot notify
|
|
11
|
+
* about itself. That needs a separate watchdog and is explicitly Slice D
|
|
12
|
+
* (see docs/feedback/2026-05-20-friend-onboarding-status-surface.md).
|
|
13
|
+
*
|
|
14
|
+
* Rate-limiting rules (from the status-surface plan):
|
|
15
|
+
* - Notify on state transition, not every poll.
|
|
16
|
+
* - Do not repeat the same unresolved issue more than once per day.
|
|
17
|
+
* - Notify again after the issue resolves and later recurs.
|
|
18
|
+
*
|
|
19
|
+
* The planning logic (`planNotifications`) is pure and fully tested; the
|
|
20
|
+
* IO (reading/writing dedup state, sending via osascript) is thin and
|
|
21
|
+
* best-effort so a notification failure can never break capture.
|
|
22
|
+
*/
|
|
23
|
+
import { execFile } from "node:child_process";
|
|
24
|
+
import fs from "node:fs";
|
|
25
|
+
import path from "node:path";
|
|
26
|
+
/** Re-notify cadence for an issue that stays unresolved. */
|
|
27
|
+
export const RENOTIFY_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
28
|
+
/** Cause identity for an issue: explicit fingerprint, else its kind. */
|
|
29
|
+
function issueFingerprint(issue) {
|
|
30
|
+
return issue.fingerprint ?? issue.kind;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Build the friend-facing notification for an issue. Pure. The title is
|
|
34
|
+
* intentionally constant ("Rift needs attention") so the friend learns
|
|
35
|
+
* to recognise it; the subtitle and body carry the specifics.
|
|
36
|
+
*/
|
|
37
|
+
export function buildNotificationPayload(issue) {
|
|
38
|
+
return {
|
|
39
|
+
title: "Rift needs attention",
|
|
40
|
+
subtitle: issue.title,
|
|
41
|
+
message: issue.nextAction,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Decide which notifications to send this tick and what state to persist.
|
|
46
|
+
* Pure: no IO, no clock except the injected `now`.
|
|
47
|
+
*
|
|
48
|
+
* Only `daemonObservable` issues are eligible — the daemon must not
|
|
49
|
+
* notify about things it cannot itself observe (e.g. a dead daemon, or
|
|
50
|
+
* user-side rejected imports).
|
|
51
|
+
*/
|
|
52
|
+
export function planNotifications(input) {
|
|
53
|
+
const { issues, state, now } = input;
|
|
54
|
+
const eligible = issues.filter((i) => i.daemonObservable);
|
|
55
|
+
const eligibleKinds = new Set(eligible.map((i) => i.kind));
|
|
56
|
+
const nextActive = {};
|
|
57
|
+
const notifications = [];
|
|
58
|
+
for (const issue of eligible) {
|
|
59
|
+
const prior = state.active[issue.kind];
|
|
60
|
+
const fingerprint = issueFingerprint(issue);
|
|
61
|
+
const notify = () => {
|
|
62
|
+
notifications.push(buildNotificationPayload(issue));
|
|
63
|
+
nextActive[issue.kind] = {
|
|
64
|
+
last_notified_at: new Date(now).toISOString(),
|
|
65
|
+
fingerprint,
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
if (!prior) {
|
|
69
|
+
// New transition into failure → notify.
|
|
70
|
+
notify();
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
// The cause changed under the same subsystem (e.g. a Voyage network
|
|
74
|
+
// blip became a provider rejection). The action and risk differ, so
|
|
75
|
+
// treat it as a fresh failure rather than a still-unresolved repeat.
|
|
76
|
+
if ((prior.fingerprint ?? issue.kind) !== fingerprint) {
|
|
77
|
+
notify();
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const priorMs = Date.parse(prior.last_notified_at);
|
|
81
|
+
const stale = Number.isNaN(priorMs) || now - priorMs >= RENOTIFY_INTERVAL_MS;
|
|
82
|
+
if (stale) {
|
|
83
|
+
// Still failing, same cause, a day later → re-notify once.
|
|
84
|
+
notify();
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
// Already notified within the window → stay quiet, keep record.
|
|
88
|
+
nextActive[issue.kind] = prior;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Anything previously active but no longer present has resolved; it is
|
|
92
|
+
// simply absent from `nextActive`, so a later recurrence notifies fresh.
|
|
93
|
+
void eligibleKinds;
|
|
94
|
+
return { notifications, nextState: { active: nextActive } };
|
|
95
|
+
}
|
|
96
|
+
/** Absolute path to the dedup-state file under the data dir. */
|
|
97
|
+
export function notifyStatePath(dataDir) {
|
|
98
|
+
return path.join(dataDir, "observability", "notifications.json");
|
|
99
|
+
}
|
|
100
|
+
/** Read dedup state; returns empty state on any read/parse failure. */
|
|
101
|
+
export function readNotifyState(dataDir) {
|
|
102
|
+
try {
|
|
103
|
+
const raw = fs.readFileSync(notifyStatePath(dataDir), "utf8");
|
|
104
|
+
const parsed = JSON.parse(raw);
|
|
105
|
+
if (parsed && typeof parsed === "object" && parsed.active) {
|
|
106
|
+
return { active: parsed.active };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// fall through
|
|
111
|
+
}
|
|
112
|
+
return { active: {} };
|
|
113
|
+
}
|
|
114
|
+
/** Persist dedup state best-effort; swallows IO errors. */
|
|
115
|
+
export function writeNotifyState(dataDir, state) {
|
|
116
|
+
try {
|
|
117
|
+
const file = notifyStatePath(dataDir);
|
|
118
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
119
|
+
fs.writeFileSync(file, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// best-effort: a failed write just means we may re-notify next tick.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Send a single notification via macOS `osascript`. Best-effort and
|
|
127
|
+
* fire-and-forget — any failure (non-macOS, osascript missing, user has
|
|
128
|
+
* notifications disabled) is swallowed so capture is never affected.
|
|
129
|
+
*
|
|
130
|
+
* Strings are passed as argv to `execFile` (not interpolated into a
|
|
131
|
+
* shell), and AppleScript string literals are escaped, so notification
|
|
132
|
+
* text cannot inject shell or AppleScript.
|
|
133
|
+
*/
|
|
134
|
+
export function sendNotification(payload, opts = {}) {
|
|
135
|
+
const platform = opts.platform ?? process.platform;
|
|
136
|
+
if (platform !== "darwin")
|
|
137
|
+
return;
|
|
138
|
+
const script = `display notification ${asAppleScriptString(payload.message)} ` +
|
|
139
|
+
`with title ${asAppleScriptString(payload.title)} ` +
|
|
140
|
+
`subtitle ${asAppleScriptString(payload.subtitle)}`;
|
|
141
|
+
try {
|
|
142
|
+
execFile("osascript", ["-e", script], () => {
|
|
143
|
+
// ignore stdout/stderr/exit — best-effort.
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// ignore spawn errors entirely.
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** Escape a string into an AppleScript double-quoted literal. */
|
|
151
|
+
export function asAppleScriptString(value) {
|
|
152
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* End-to-end daemon helper: given the diagnosed issues, send any due
|
|
156
|
+
* notifications and persist updated dedup state. Best-effort throughout
|
|
157
|
+
* — wired into the capture loop, so it must never throw.
|
|
158
|
+
*/
|
|
159
|
+
export function notifyDaemonFailures(dataDir, issues, opts = {}) {
|
|
160
|
+
try {
|
|
161
|
+
const now = opts.now ?? Date.now();
|
|
162
|
+
const state = readNotifyState(dataDir);
|
|
163
|
+
const { notifications, nextState } = planNotifications({
|
|
164
|
+
issues,
|
|
165
|
+
state,
|
|
166
|
+
now,
|
|
167
|
+
});
|
|
168
|
+
for (const payload of notifications) {
|
|
169
|
+
sendNotification(payload, opts.platform ? { platform: opts.platform } : {});
|
|
170
|
+
}
|
|
171
|
+
writeNotifyState(dataDir, nextState);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// best-effort: never let notification logic break the daemon.
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=notify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notify.js","sourceRoot":"","sources":["../../../src/diagnostics/notify.ts"],"names":[],"mappings":"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"}
|