@rubytech/create-maxy-code 0.1.476 → 0.1.478
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/package.json +1 -1
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1704-data-portal-standing-audit.md +673 -0
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1704-data-portal-standing-audit-design.md +177 -0
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1820-cpu-triage-admin-tools-design.md +97 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js +44 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/audit.d.ts +56 -0
- package/payload/platform/lib/storage-broker/dist/audit.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/audit.js +34 -0
- package/payload/platform/lib/storage-broker/dist/audit.js.map +1 -1
- package/payload/platform/lib/storage-broker/src/__tests__/audit.test.ts +70 -1
- package/payload/platform/lib/storage-broker/src/audit.ts +111 -0
- package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +7 -1
- package/payload/platform/plugins/admin/mcp/dist/index.js +72 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/tools/cpu-triage.d.ts +94 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/cpu-triage.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/cpu-triage.js +229 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/cpu-triage.js.map +1 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +2 -2
- package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
- package/payload/platform/plugins/email/PLUGIN.md +2 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/confirm-sent-copy.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/confirm-sent-copy.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/confirm-sent-copy.test.js +92 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/confirm-sent-copy.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-send.test.js +112 -12
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-send.test.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-send-sent-copy.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-send-sent-copy.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-send-sent-copy.test.js +54 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-send-sent-copy.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/imap-drafts.test.js +170 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/imap-drafts.test.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-note.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-note.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-note.test.js +86 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-note.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-sweep.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-sweep.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-sweep.test.js +164 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-sweep.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/sent-ledger.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/sent-ledger.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/sent-ledger.test.js +82 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/sent-ledger.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +95 -0
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js +223 -0
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-note.d.ts +18 -0
- package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-note.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-note.js +59 -0
- package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-note.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-sweep.d.ts +48 -0
- package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-sweep.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-sweep.js +79 -0
- package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-sweep.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/sent-ledger.d.ts +44 -0
- package/payload/platform/plugins/email/mcp/dist/lib/sent-ledger.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/sent-ledger.js +63 -0
- package/payload/platform/plugins/email/mcp/dist/lib/sent-ledger.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/scripts/sent-copy-sweep.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/scripts/sent-copy-sweep.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/scripts/sent-copy-sweep.js +55 -0
- package/payload/platform/plugins/email/mcp/dist/scripts/sent-copy-sweep.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-draft-send.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-draft-send.js +64 -13
- package/payload/platform/plugins/email/mcp/dist/tools/email-draft-send.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-reply.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js +11 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-send.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-send.js +16 -3
- package/payload/platform/plugins/email/mcp/dist/tools/email-send.js.map +1 -1
- package/payload/platform/plugins/email/references/email-reference.md +36 -0
- package/payload/platform/plugins/scheduling/mcp/dist/__tests__/sent-sweep-throttle.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/__tests__/sent-sweep-throttle.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/__tests__/sent-sweep-throttle.test.js +18 -0
- package/payload/platform/plugins/scheduling/mcp/dist/__tests__/sent-sweep-throttle.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-block-sqlite.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-block-sqlite.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-block-sqlite.test.js +150 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-block-sqlite.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-block.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-block.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-block.test.js +248 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-block.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts +90 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js +105 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/sent-sweep-throttle.d.ts +13 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/sent-sweep-throttle.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/sent-sweep-throttle.js +10 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/sent-sweep-throttle.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +53 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +88 -10
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
- package/payload/platform/scripts/cpu-triage-run.sh +117 -0
- package/payload/platform/scripts/cpu-triage.sh +332 -0
- package/payload/platform/services/claude-session-manager/dist/activity-range.d.ts +40 -0
- package/payload/platform/services/claude-session-manager/dist/activity-range.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/activity-range.js +68 -0
- package/payload/platform/services/claude-session-manager/dist/activity-range.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/activity-rows.d.ts +40 -8
- package/payload/platform/services/claude-session-manager/dist/activity-rows.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/activity-rows.js +74 -7
- package/payload/platform/services/claude-session-manager/dist/activity-rows.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +2 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +9 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/server/{chunk-56WJMBQQ.js → chunk-3XLLTG6R.js} +14 -0
- package/payload/server/{chunk-LBCMFD4O.js → chunk-XGNRSM57.js} +129 -37
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/activity.html +6 -6
- package/payload/server/public/assets/{AdminLoginScreens-DximwPlS.js → AdminLoginScreens-DR5eLF-a.js} +1 -1
- package/payload/server/public/assets/AdminShell-D9zaUxPd.js +2 -0
- package/payload/server/public/assets/{Checkbox-DM-eHqOS.js → Checkbox-DcbbAIe5.js} +1 -1
- package/payload/server/public/assets/activity-C9P5NjEZ.js +1 -0
- package/payload/server/public/assets/{admin-CAkDnGdk.js → admin-D2KLZnQW.js} +1 -1
- package/payload/server/public/assets/{browser-DXl8hOoi.js → browser-CDWqEiCD.js} +1 -1
- package/payload/server/public/assets/{calendar-DP6hn4-6.js → calendar-BG7fOpbB.js} +1 -1
- package/payload/server/public/assets/chat-RyH-WYQC.js +1 -0
- package/payload/server/public/assets/chevron-left-DmXCr6nB.js +1 -0
- package/payload/server/public/assets/data-BaY_LkLR.js +1 -0
- package/payload/server/public/assets/{graph-CHcYoEJ5.js → graph-DRloNic4.js} +1 -1
- package/payload/server/public/assets/{graph-labels-CZykslZM.js → graph-labels-DD-GqS4v.js} +1 -1
- package/payload/server/public/assets/{maximize-2-31AZEcMS.js → maximize-2-CzDaBugs.js} +1 -1
- package/payload/server/public/assets/{operator-BGGpdsO_.js → operator-BZeeyait.js} +1 -1
- package/payload/server/public/assets/{page-BloC6ygA.js → page-B0obcg3b.js} +1 -1
- package/payload/server/public/assets/{page-DOeiiqbR.js → page-ef-6rRPL.js} +1 -1
- package/payload/server/public/assets/{public-CGgO6IZv.js → public-9D3MMOkC.js} +1 -1
- package/payload/server/public/assets/{rotate-ccw-BlAi6iFg.js → rotate-ccw-DXEKUhXP.js} +1 -1
- package/payload/server/public/assets/tasks-BB0L7e5a.js +1 -0
- package/payload/server/public/assets/{time-entry-format-CiUUQdq7.js → time-entry-format-CsYAjplS.js} +1 -1
- package/payload/server/public/assets/{triangle-alert-CVqKuLoa.js → triangle-alert-0lSBIUg-.js} +1 -1
- package/payload/server/public/assets/{useCopyFeedback-ksyUe_g1.js → useCopyFeedback-jTBQ2h0V.js} +1 -1
- package/payload/server/public/assets/{useSelectionMode-BPrBkdTd.js → useSelectionMode-BooArySZ.js} +1 -1
- package/payload/server/public/assets/useSubAccountSwitcher-D9knHOKq.css +1 -0
- package/payload/server/public/assets/{useVoiceRecorder-CTXP8oTW.js → useVoiceRecorder-PUT8q_sm.js} +1 -1
- package/payload/server/public/browser.html +5 -5
- package/payload/server/public/calendar.html +6 -6
- package/payload/server/public/chat.html +12 -12
- package/payload/server/public/data.html +11 -11
- package/payload/server/public/graph.html +11 -11
- package/payload/server/public/index.html +14 -14
- package/payload/server/public/operator.html +14 -14
- package/payload/server/public/public.html +12 -12
- package/payload/server/public/tasks.html +5 -5
- package/payload/server/server.js +325 -77
- package/payload/server/{src-JYPKMWJR.js → src-XQ63FPRE.js} +3 -1
- package/payload/server/public/assets/AdminShell-BehbH-Oa.js +0 -2
- package/payload/server/public/assets/activity-hDwUeQd6.js +0 -1
- package/payload/server/public/assets/chat-DhOfVGYM.js +0 -1
- package/payload/server/public/assets/chevron-left-DYNBoLii.js +0 -1
- package/payload/server/public/assets/data-bq8fTo_s.js +0 -1
- package/payload/server/public/assets/tasks-C1giFRRL.js +0 -1
- package/payload/server/public/assets/useSubAccountSwitcher-K4kYeN6i.css +0 -1
- /package/payload/server/public/assets/{useSubAccountSwitcher-DzgzTRRn.js → useSubAccountSwitcher-BZnzc5C3.js} +0 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cpu-triage — start/read pair for the device CPU diagnostic.
|
|
3
|
+
*
|
|
4
|
+
* The measurement samples for at least one full 120 s reconcile cycle, because
|
|
5
|
+
* anything shorter catches a random phase of a sawtooth and produces
|
|
6
|
+
* contradictory answers (see `platform/scripts/cpu-triage.sh`). No admin tool
|
|
7
|
+
* exceeds a 12 s envelope, so the run is detached and the caller polls.
|
|
8
|
+
*
|
|
9
|
+
* Three files per run live under `<configDir>/logs/`:
|
|
10
|
+
*
|
|
11
|
+
* cpu-triage-<id>.meta.json written here at start
|
|
12
|
+
* cpu-triage-<id>.json written by the runner, STREAMING
|
|
13
|
+
* cpu-triage-<id>.status written by the runner, last
|
|
14
|
+
*
|
|
15
|
+
* `.status` is the only readiness signal. The result file exists from the first
|
|
16
|
+
* byte of output and is half-written for the whole window, so keying on it
|
|
17
|
+
* reports a sampling run as complete.
|
|
18
|
+
*/
|
|
19
|
+
/** Shortest window the measurement is trustworthy over — one reconcile cycle. */
|
|
20
|
+
export declare const MIN_WINDOW_SEC = 120;
|
|
21
|
+
/** Runs retained on disk. Older runs are pruned when a new one starts. */
|
|
22
|
+
export declare const RETAINED_RUNS = 20;
|
|
23
|
+
export interface CpuTriagePaths {
|
|
24
|
+
/** `platform/scripts/cpu-triage.sh` */
|
|
25
|
+
scriptPath: string;
|
|
26
|
+
/** `platform/scripts/cpu-triage-run.sh` */
|
|
27
|
+
runnerPath: string;
|
|
28
|
+
/** `<configDir>/logs` — where per-run files live */
|
|
29
|
+
logsDir: string;
|
|
30
|
+
/** brand log receiving the op=start line */
|
|
31
|
+
serverLog: string;
|
|
32
|
+
}
|
|
33
|
+
export interface StartOptions {
|
|
34
|
+
windowSec?: number;
|
|
35
|
+
intervalSec?: number;
|
|
36
|
+
thresholdPct?: number;
|
|
37
|
+
/** A peer brand on the previous build, for like-for-like comparison. */
|
|
38
|
+
control?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface RunMeta {
|
|
41
|
+
runId: string;
|
|
42
|
+
startedAt: string;
|
|
43
|
+
expectedCompleteAt: string;
|
|
44
|
+
windowSec: number;
|
|
45
|
+
args: string[];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `fault` separates a genuine defect the operator must act on (a missing
|
|
49
|
+
* script) from an ordinary empty state (nothing has been started yet). Only a
|
|
50
|
+
* fault is surfaced as an MCP error.
|
|
51
|
+
*/
|
|
52
|
+
export interface Refusal {
|
|
53
|
+
ok: false;
|
|
54
|
+
reason: "script-missing" | "runner-missing" | "window-too-short" | "no-runs" | "run-not-found";
|
|
55
|
+
fault: boolean;
|
|
56
|
+
detail: string;
|
|
57
|
+
}
|
|
58
|
+
export type StartOutcome = ({
|
|
59
|
+
ok: true;
|
|
60
|
+
resultPath: string;
|
|
61
|
+
} & RunMeta) | Refusal;
|
|
62
|
+
export type ReadOutcome = {
|
|
63
|
+
ok: true;
|
|
64
|
+
status: "running";
|
|
65
|
+
runId: string;
|
|
66
|
+
startedAt: string;
|
|
67
|
+
expectedCompleteAt: string;
|
|
68
|
+
windowSec: number;
|
|
69
|
+
} | {
|
|
70
|
+
ok: true;
|
|
71
|
+
status: "complete";
|
|
72
|
+
runId: string;
|
|
73
|
+
exitCode: number;
|
|
74
|
+
report: unknown;
|
|
75
|
+
} | {
|
|
76
|
+
ok: true;
|
|
77
|
+
status: "failed";
|
|
78
|
+
runId: string;
|
|
79
|
+
exitCode: number;
|
|
80
|
+
stderr: string;
|
|
81
|
+
detail?: string;
|
|
82
|
+
} | Refusal;
|
|
83
|
+
/** Injection seam so tests drive start without spawning a real process. */
|
|
84
|
+
export type SpawnRunner = (command: string, args: string[]) => void;
|
|
85
|
+
/**
|
|
86
|
+
* Start a detached triage run. Returns as soon as the run handle is on disk;
|
|
87
|
+
* the measurement continues in a process this one does not hold open.
|
|
88
|
+
*/
|
|
89
|
+
export declare function startCpuTriage(paths: CpuTriagePaths, opts: StartOptions, spawnRunner?: SpawnRunner): StartOutcome;
|
|
90
|
+
/** Read a triage run. Defaults to the most recent. */
|
|
91
|
+
export declare function readCpuTriage(paths: CpuTriagePaths, opts: {
|
|
92
|
+
runId?: string;
|
|
93
|
+
}): ReadOutcome;
|
|
94
|
+
//# sourceMappingURL=cpu-triage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cpu-triage.d.ts","sourceRoot":"","sources":["../../src/tools/cpu-triage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAOH,iFAAiF;AACjF,eAAO,MAAM,cAAc,MAAM,CAAC;AAElC,0EAA0E;AAC1E,eAAO,MAAM,aAAa,KAAK,CAAC;AAKhC,MAAM,WAAW,cAAc;IAC7B,uCAAuC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED;;;;GAIG;AACH,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,GAAG,kBAAkB,GAAG,SAAS,GAAG,eAAe,CAAC;IAC/F,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,YAAY,GACpB,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,GAC5C,OAAO,CAAC;AAEZ,MAAM,MAAM,WAAW,GACnB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAChH;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAClF;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAChG,OAAO,CAAC;AAEZ,2EAA2E;AAC3E,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;AAqDpE;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,YAAY,EAClB,WAAW,GAAE,WAA2B,GACvC,YAAY,CAgEd;AAED,sDAAsD;AACtD,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,WAAW,CA8F1F"}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cpu-triage — start/read pair for the device CPU diagnostic.
|
|
3
|
+
*
|
|
4
|
+
* The measurement samples for at least one full 120 s reconcile cycle, because
|
|
5
|
+
* anything shorter catches a random phase of a sawtooth and produces
|
|
6
|
+
* contradictory answers (see `platform/scripts/cpu-triage.sh`). No admin tool
|
|
7
|
+
* exceeds a 12 s envelope, so the run is detached and the caller polls.
|
|
8
|
+
*
|
|
9
|
+
* Three files per run live under `<configDir>/logs/`:
|
|
10
|
+
*
|
|
11
|
+
* cpu-triage-<id>.meta.json written here at start
|
|
12
|
+
* cpu-triage-<id>.json written by the runner, STREAMING
|
|
13
|
+
* cpu-triage-<id>.status written by the runner, last
|
|
14
|
+
*
|
|
15
|
+
* `.status` is the only readiness signal. The result file exists from the first
|
|
16
|
+
* byte of output and is half-written for the whole window, so keying on it
|
|
17
|
+
* reports a sampling run as complete.
|
|
18
|
+
*/
|
|
19
|
+
import { spawn } from "node:child_process";
|
|
20
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
22
|
+
import { randomBytes } from "node:crypto";
|
|
23
|
+
/** Shortest window the measurement is trustworthy over — one reconcile cycle. */
|
|
24
|
+
export const MIN_WINDOW_SEC = 120;
|
|
25
|
+
/** Runs retained on disk. Older runs are pruned when a new one starts. */
|
|
26
|
+
export const RETAINED_RUNS = 20;
|
|
27
|
+
/** Grace added to the window when reporting when a run should be readable. */
|
|
28
|
+
const COMPLETION_SLACK_MS = 30_000;
|
|
29
|
+
const spawnDetached = (command, args) => {
|
|
30
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
31
|
+
child.unref();
|
|
32
|
+
};
|
|
33
|
+
function metaPath(paths, runId) {
|
|
34
|
+
return join(paths.logsDir, `cpu-triage-${runId}.meta.json`);
|
|
35
|
+
}
|
|
36
|
+
function resultPath(paths, runId) {
|
|
37
|
+
return join(paths.logsDir, `cpu-triage-${runId}.json`);
|
|
38
|
+
}
|
|
39
|
+
function statusPath(paths, runId) {
|
|
40
|
+
return join(paths.logsDir, `cpu-triage-${runId}.status`);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Timestamp-prefixed so "most recent" is a filename sort rather than a stat
|
|
44
|
+
* walk, and so a run id is legible in a log line without a lookup.
|
|
45
|
+
*/
|
|
46
|
+
function mintRunId(now) {
|
|
47
|
+
const iso = now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "");
|
|
48
|
+
return `${iso}-${randomBytes(2).toString("hex")}`;
|
|
49
|
+
}
|
|
50
|
+
/** Run ids present on disk, oldest first. */
|
|
51
|
+
function listRunIds(paths) {
|
|
52
|
+
if (!existsSync(paths.logsDir))
|
|
53
|
+
return [];
|
|
54
|
+
return readdirSync(paths.logsDir)
|
|
55
|
+
.filter((f) => f.startsWith("cpu-triage-") && f.endsWith(".meta.json"))
|
|
56
|
+
.map((f) => f.slice("cpu-triage-".length, -".meta.json".length))
|
|
57
|
+
.sort();
|
|
58
|
+
}
|
|
59
|
+
function prune(paths, keep) {
|
|
60
|
+
const ids = listRunIds(paths);
|
|
61
|
+
for (const id of ids.slice(0, Math.max(0, ids.length - keep))) {
|
|
62
|
+
for (const p of [metaPath(paths, id), resultPath(paths, id), statusPath(paths, id)]) {
|
|
63
|
+
rmSync(p, { force: true });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function logLine(paths, line) {
|
|
68
|
+
try {
|
|
69
|
+
mkdirSync(dirname(paths.serverLog), { recursive: true });
|
|
70
|
+
appendFileSync(paths.serverLog, `${new Date().toISOString()} ${line}\n`);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Best-effort. A log write must never take the diagnostic down with it.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Start a detached triage run. Returns as soon as the run handle is on disk;
|
|
78
|
+
* the measurement continues in a process this one does not hold open.
|
|
79
|
+
*/
|
|
80
|
+
export function startCpuTriage(paths, opts, spawnRunner = spawnDetached) {
|
|
81
|
+
const windowSec = opts.windowSec ?? 300;
|
|
82
|
+
// The script refuses a short window with exit 2, but detached that refusal
|
|
83
|
+
// arrives one full window late. It has to be synchronous.
|
|
84
|
+
if (windowSec < MIN_WINDOW_SEC) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
reason: "window-too-short",
|
|
88
|
+
fault: true,
|
|
89
|
+
detail: `windowSec=${windowSec} is shorter than the ${MIN_WINDOW_SEC}s reconcile cycle. ` +
|
|
90
|
+
`Samples shorter than the cycle land on a random phase of a sawtooth and contradict each other.`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (!existsSync(paths.scriptPath)) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
reason: "script-missing",
|
|
97
|
+
fault: true,
|
|
98
|
+
detail: `cpu-triage.sh is not on this device at ${paths.scriptPath}. No measurement was taken.`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (!existsSync(paths.runnerPath)) {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
reason: "runner-missing",
|
|
105
|
+
fault: true,
|
|
106
|
+
detail: `cpu-triage-run.sh is not on this device at ${paths.runnerPath}. No measurement was taken.`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const now = new Date();
|
|
110
|
+
const runId = mintRunId(now);
|
|
111
|
+
const measureArgs = ["--window", String(windowSec)];
|
|
112
|
+
if (opts.intervalSec !== undefined)
|
|
113
|
+
measureArgs.push("--interval", String(opts.intervalSec));
|
|
114
|
+
if (opts.thresholdPct !== undefined)
|
|
115
|
+
measureArgs.push("--threshold", String(opts.thresholdPct));
|
|
116
|
+
if (opts.control)
|
|
117
|
+
measureArgs.push("--control", opts.control);
|
|
118
|
+
const meta = {
|
|
119
|
+
runId,
|
|
120
|
+
startedAt: now.toISOString(),
|
|
121
|
+
expectedCompleteAt: new Date(now.getTime() + windowSec * 1000 + COMPLETION_SLACK_MS).toISOString(),
|
|
122
|
+
windowSec,
|
|
123
|
+
args: measureArgs,
|
|
124
|
+
};
|
|
125
|
+
mkdirSync(paths.logsDir, { recursive: true });
|
|
126
|
+
// Prune before the new run lands so it can never prune itself.
|
|
127
|
+
prune(paths, RETAINED_RUNS - 1);
|
|
128
|
+
writeFileSync(metaPath(paths, runId), JSON.stringify(meta, null, 2));
|
|
129
|
+
logLine(paths, `[cpu-triage] op=start runId=${runId} windowSec=${windowSec}`);
|
|
130
|
+
spawnRunner(paths.runnerPath, [
|
|
131
|
+
"--run-id", runId,
|
|
132
|
+
"--out-dir", paths.logsDir,
|
|
133
|
+
"--log", paths.serverLog,
|
|
134
|
+
"--script", paths.scriptPath,
|
|
135
|
+
"--", ...measureArgs,
|
|
136
|
+
]);
|
|
137
|
+
return { ok: true, ...meta, resultPath: resultPath(paths, runId) };
|
|
138
|
+
}
|
|
139
|
+
/** Read a triage run. Defaults to the most recent. */
|
|
140
|
+
export function readCpuTriage(paths, opts) {
|
|
141
|
+
const ids = listRunIds(paths);
|
|
142
|
+
if (ids.length === 0) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
reason: "no-runs",
|
|
146
|
+
fault: false,
|
|
147
|
+
detail: "No cpu-triage run has been started on this device. Call cpu-triage-start first.",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const runId = opts.runId ?? ids[ids.length - 1];
|
|
151
|
+
if (!ids.includes(runId)) {
|
|
152
|
+
return {
|
|
153
|
+
ok: false,
|
|
154
|
+
reason: "run-not-found",
|
|
155
|
+
fault: false,
|
|
156
|
+
detail: `No run ${runId} on this device. Available: ${ids.join(", ")}`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
let meta;
|
|
160
|
+
try {
|
|
161
|
+
meta = JSON.parse(readFileSync(metaPath(paths, runId), "utf-8"));
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
return {
|
|
165
|
+
ok: true,
|
|
166
|
+
status: "failed",
|
|
167
|
+
runId,
|
|
168
|
+
exitCode: -1,
|
|
169
|
+
stderr: "",
|
|
170
|
+
detail: `run metadata unreadable: ${err instanceof Error ? err.message : String(err)}`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
// The status marker is the only readiness signal. The result file is
|
|
174
|
+
// streamed and exists half-written for the whole sampling window.
|
|
175
|
+
const status = statusPath(paths, runId);
|
|
176
|
+
if (!existsSync(status)) {
|
|
177
|
+
return {
|
|
178
|
+
ok: true,
|
|
179
|
+
status: "running",
|
|
180
|
+
runId,
|
|
181
|
+
startedAt: meta.startedAt,
|
|
182
|
+
expectedCompleteAt: meta.expectedCompleteAt,
|
|
183
|
+
windowSec: meta.windowSec,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const statusText = readFileSync(status, "utf-8");
|
|
187
|
+
const match = statusText.match(/^exit=(-?\d+)/);
|
|
188
|
+
const exitCode = match ? Number(match[1]) : -1;
|
|
189
|
+
// Split on the first newline rather than slicing from indexOf: a status file
|
|
190
|
+
// with no trailing newline has indexOf === -1, and slice(0) would hand back
|
|
191
|
+
// the whole text — reporting `exit=0` itself as the run's stderr.
|
|
192
|
+
const newline = statusText.indexOf("\n");
|
|
193
|
+
const stderr = newline === -1 ? "" : statusText.slice(newline + 1).trim();
|
|
194
|
+
// Exit 1 is the script's sustained-hot-core gate — a finding, not a failure.
|
|
195
|
+
// Only a precondition failure (2) or an unexpected code is failed.
|
|
196
|
+
if (exitCode !== 0 && exitCode !== 1) {
|
|
197
|
+
return { ok: true, status: "failed", runId, exitCode, stderr };
|
|
198
|
+
}
|
|
199
|
+
const result = resultPath(paths, runId);
|
|
200
|
+
// An absent result file and an unparseable one are different conditions and
|
|
201
|
+
// must not share a message — "will not parse" sends the reader looking at
|
|
202
|
+
// JSON when the file was never written.
|
|
203
|
+
if (!existsSync(result)) {
|
|
204
|
+
return {
|
|
205
|
+
ok: true,
|
|
206
|
+
status: "failed",
|
|
207
|
+
runId,
|
|
208
|
+
exitCode,
|
|
209
|
+
stderr,
|
|
210
|
+
detail: `the run exited ${exitCode} but wrote no report at ${result}`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
let report;
|
|
214
|
+
try {
|
|
215
|
+
report = JSON.parse(readFileSync(result, "utf-8"));
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
return {
|
|
219
|
+
ok: true,
|
|
220
|
+
status: "failed",
|
|
221
|
+
runId,
|
|
222
|
+
exitCode,
|
|
223
|
+
stderr,
|
|
224
|
+
detail: `the run exited ${exitCode} but its report will not parse: ${err instanceof Error ? err.message : String(err)}`,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return { ok: true, status: "complete", runId, exitCode, report };
|
|
228
|
+
}
|
|
229
|
+
//# sourceMappingURL=cpu-triage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cpu-triage.js","sourceRoot":"","sources":["../../src/tools/cpu-triage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAClH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,iFAAiF;AACjF,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,CAAC;AAElC,0EAA0E;AAC1E,MAAM,CAAC,MAAM,aAAa,GAAG,EAAE,CAAC;AAEhC,8EAA8E;AAC9E,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAsDnC,MAAM,aAAa,GAAgB,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE;IACnD,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;IACxE,KAAK,CAAC,KAAK,EAAE,CAAC;AAChB,CAAC,CAAC;AAEF,SAAS,QAAQ,CAAC,KAAqB,EAAE,KAAa;IACpD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,cAAc,KAAK,YAAY,CAAC,CAAC;AAC9D,CAAC;AACD,SAAS,UAAU,CAAC,KAAqB,EAAE,KAAa;IACtD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,CAAC;AACzD,CAAC;AACD,SAAS,UAAU,CAAC,KAAqB,EAAE,KAAa;IACtD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,cAAc,KAAK,SAAS,CAAC,CAAC;AAC3D,CAAC;AAED;;;GAGG;AACH,SAAS,SAAS,CAAC,GAAS;IAC1B,MAAM,GAAG,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IAC1E,OAAO,GAAG,GAAG,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;AACpD,CAAC;AAED,6CAA6C;AAC7C,SAAS,UAAU,CAAC,KAAqB;IACvC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,CAAC;IAC1C,OAAO,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC;SAC9B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;SACtE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;SAC/D,IAAI,EAAE,CAAC;AACZ,CAAC;AAED,SAAS,KAAK,CAAC,KAAqB,EAAE,IAAY;IAChD,MAAM,GAAG,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;IAC9B,KAAK,MAAM,EAAE,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,EAAE,CAAC;QAC9D,KAAK,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,UAAU,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,UAAU,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC;YACpF,MAAM,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,OAAO,CAAC,KAAqB,EAAE,IAAY;IAClD,IAAI,CAAC;QACH,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACzD,cAAc,CAAC,KAAK,CAAC,SAAS,EAAE,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,IAAI,IAAI,IAAI,CAAC,CAAC;IAC3E,CAAC;IAAC,MAAM,CAAC;QACP,wEAAwE;IAC1E,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAC5B,KAAqB,EACrB,IAAkB,EAClB,cAA2B,aAAa;IAExC,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,GAAG,CAAC;IAExC,2EAA2E;IAC3E,0DAA0D;IAC1D,IAAI,SAAS,GAAG,cAAc,EAAE,CAAC;QAC/B,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,kBAAkB;YAC1B,KAAK,EAAE,IAAI;YACX,MAAM,EACJ,aAAa,SAAS,wBAAwB,cAAc,qBAAqB;gBACjF,gGAAgG;SACnG,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;QAClC,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,gBAAgB;YACxB,KAAK,EAAE,IAAI;YACX,MAAM,EAAE,0CAA0C,KAAK,CAAC,UAAU,6BAA6B;SAChG,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;QAClC,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,gBAAgB;YACxB,KAAK,EAAE,IAAI;YACX,MAAM,EAAE,8CAA8C,KAAK,CAAC,UAAU,6BAA6B;SACpG,CAAC;IACJ,CAAC;IAED,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IACvB,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;IAE7B,MAAM,WAAW,GAAa,CAAC,UAAU,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;IAC9D,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS;QAAE,WAAW,CAAC,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;IAC7F,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS;QAAE,WAAW,CAAC,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;IAChG,IAAI,IAAI,CAAC,OAAO;QAAE,WAAW,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAE9D,MAAM,IAAI,GAAY;QACpB,KAAK;QACL,SAAS,EAAE,GAAG,CAAC,WAAW,EAAE;QAC5B,kBAAkB,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,SAAS,GAAG,IAAI,GAAG,mBAAmB,CAAC,CAAC,WAAW,EAAE;QAClG,SAAS;QACT,IAAI,EAAE,WAAW;KAClB,CAAC;IAEF,SAAS,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,+DAA+D;IAC/D,KAAK,CAAC,KAAK,EAAE,aAAa,GAAG,CAAC,CAAC,CAAC;IAChC,aAAa,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAErE,OAAO,CAAC,KAAK,EAAE,+BAA+B,KAAK,cAAc,SAAS,EAAE,CAAC,CAAC;IAE9E,WAAW,CAAC,KAAK,CAAC,UAAU,EAAE;QAC5B,UAAU,EAAE,KAAK;QACjB,WAAW,EAAE,KAAK,CAAC,OAAO;QAC1B,OAAO,EAAE,KAAK,CAAC,SAAS;QACxB,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,IAAI,EAAE,GAAG,WAAW;KACrB,CAAC,CAAC;IAEH,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC;AACrE,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,aAAa,CAAC,KAAqB,EAAE,IAAwB;IAC3E,MAAM,GAAG,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;IAC9B,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,SAAS;YACjB,KAAK,EAAE,KAAK;YACZ,MAAM,EAAE,iFAAiF;SAC1F,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAChD,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,eAAe;YACvB,KAAK,EAAE,KAAK;YACZ,MAAM,EAAE,UAAU,KAAK,+BAA+B,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;SACvE,CAAC;IACJ,CAAC;IAED,IAAI,IAAa,CAAC;IAClB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,CAAY,CAAC;IAC9E,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO;YACL,EAAE,EAAE,IAAI;YACR,MAAM,EAAE,QAAQ;YAChB,KAAK;YACL,QAAQ,EAAE,CAAC,CAAC;YACZ,MAAM,EAAE,EAAE;YACV,MAAM,EAAE,4BAA4B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE;SACvF,CAAC;IACJ,CAAC;IAED,qEAAqE;IACrE,kEAAkE;IAClE,MAAM,MAAM,GAAG,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACxC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QACxB,OAAO;YACL,EAAE,EAAE,IAAI;YACR,MAAM,EAAE,SAAS;YACjB,KAAK;YACL,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;YAC3C,SAAS,EAAE,IAAI,CAAC,SAAS;SAC1B,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,GAAG,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjD,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;IAChD,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/C,6EAA6E;IAC7E,4EAA4E;IAC5E,kEAAkE;IAClE,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAE1E,6EAA6E;IAC7E,mEAAmE;IACnE,IAAI,QAAQ,KAAK,CAAC,IAAI,QAAQ,KAAK,CAAC,EAAE,CAAC;QACrC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACjE,CAAC;IAED,MAAM,MAAM,GAAG,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACxC,4EAA4E;IAC5E,0EAA0E;IAC1E,wCAAwC;IACxC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QACxB,OAAO;YACL,EAAE,EAAE,IAAI;YACR,MAAM,EAAE,QAAQ;YAChB,KAAK;YACL,QAAQ;YACR,MAAM;YACN,MAAM,EAAE,kBAAkB,QAAQ,2BAA2B,MAAM,EAAE;SACtE,CAAC;IACJ,CAAC;IAED,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACrD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO;YACL,EAAE,EAAE,IAAI;YACR,MAAM,EAAE,QAAQ;YAChB,KAAK;YACL,QAAQ;YACR,MAAM;YACN,MAAM,EAAE,kBAAkB,QAAQ,mCAAmC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE;SACxH,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;AACnE,CAAC"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: platform-architecture
|
|
3
3
|
description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
|
|
4
|
-
content-hash: sha256:
|
|
4
|
+
content-hash: sha256:6009a50cf8e577364b02bee6ca6a47c112f463807f891975c445989d027176f0
|
|
5
5
|
brand: maxy-code
|
|
6
6
|
product-name: Maxy
|
|
7
7
|
---
|
|
@@ -2943,7 +2943,7 @@ either is a regression.
|
|
|
2943
2943
|
|
|
2944
2944
|
**Row title resolution.** Both the sidebar (`/sidebar-sessions`) and the manager's own row payload resolve a row's title in the same order: operator rename → Claude Code `ai-title` → first non-CLI user message → 8-char sessionId prefix. The operator-rename tier is the on-disk `<accountDir>/session-titles.json` (the manager's `UserTitleStore`), keyed by the CC sessionId — the sidebar reads that same file, so a write to the store lights up both surfaces with one write.
|
|
2945
2945
|
|
|
2946
|
-
**Session row delete, rename, and select mode.** The sidebar Sessions row supports three operator affordances. **Rename** (kebab → Rename) opens the inline edit input with its existing title pre-selected on focus, so the first keystroke replaces the whole title rather than appending to it — on both the sidebar (`conv-name-edit`) and the `/chat` Conversations flyout (`op-conv-rename-input`). **Delete** (kebab → Delete) arms a confirm that names the session (its label plus the 8-char id prefix) and renders as a popover anchored beside the row it will delete — reusing the same `AnchoredPopover` the kebab menu uses, not the shared bottom-right info dock — so the target is unambiguous; it still states the delete is permanent and, for a live row, that the session is stopped first. **Select mode** (a "Select" control in the Sessions header) turns each row into a checkbox and shows a "Delete N" bar; the row body toggles selection instead of opening, the per-row kebab is hidden, and confirming a count-based `BulkDeleteConfirmModal` (which names N and how many are live) loops the single-id `session-delete` over the selection (no route change — `/api/admin/session-delete` stays single-id). Bulk deletion is instrumented with a `[admin-ui] sidebar-bulk-delete op=start count=<N> live=<M>` / `op=done requested=<N> ok=<X> failed=<Y>` envelope around the per-session `sidebar-session-delete` lines, so a silently-dropped item shows as `ok+failed < requested`. Outside select mode the single-delete and row-open behaviours are unchanged.
|
|
2946
|
+
**Session row delete, rename, and select mode.** The sidebar Sessions row supports three operator affordances. **Rename** (kebab → Rename) opens the inline edit input with its existing title pre-selected on focus, so the first keystroke replaces the whole title rather than appending to it — on both the sidebar (`conv-name-edit`) and the `/chat` Conversations flyout (`op-conv-rename-input`). **Delete** (kebab → Delete) arms a confirm that names the session (its label plus the 8-char id prefix) and renders as a popover anchored beside the row it will delete — reusing the same `AnchoredPopover` the kebab menu uses, not the shared bottom-right info dock — so the target is unambiguous; it still states the delete is permanent and, for a live row, that the session is stopped first. **Usage & cost** (kebab → Usage & cost) opens the per-day active-time / token / GBP-cost table for that session, and anchors beside its row through the same `AnchoredPopover`, with no scrim, so the rest of the dashboard stays legible and interactive while it is open. `AnchoredPopover` right-aligns to the anchor's rect and clamps horizontally against both viewport edges. Vertically it places below when the popover fits there, above when it fits there instead, and otherwise on whichever side has more room — and in every case it bounds the popover's height to the room on the side it chose. That bound is what keeps a long session's panel anchored: a 30-day usage table is taller than the viewport, and clamping it to fit would push it up over the very row it describes, which is not "anchored" to anything. Instead it sits flush against its row and scrolls internally. Because the bound is derived from the anchor rect and the viewport rather than from the popover's own height, the `ResizeObserver` pass it triggers recomputes the same values and settles in one extra pass. The horizontal right-edge clamp does not engage for a left-docked sidebar row (a popover wider than its anchor overflows left, which the left clamp handles); it is kept because the component is general-purpose and is covered by unit test only. Positioning is instrumented, because a mispositioned client-side panel throws nothing and the operator just sees a menu item that did nothing: every positioning pass (each open, each `ResizeObserver` callback, each window resize) emits `[anchored-popover] op=anchor id=<class>:<sessionId> present=<bool> rect=<x,y,w,h>` then `op=placed … maxH=<px> flipped=<bool> clampedLeft=<bool> clampedRight=<bool>`, and `op=offscreen` at `console.error` when the anchor is detached from layout or a placed rect misses the viewport. A pass that computes exactly what the previous pass did is suppressed, so mount and its trailing `ResizeObserver` callback log once rather than twice; the `op=offscreen` assertion is never suppressed. A healthy open is `present=true` with a non-zero rect followed by `op=placed` and no `op=offscreen`. `present=false` means the row is not in `rowRefs` — the row unmounted or its ref callback did not run — and the panel docks bottom-right rather than vanishing, because an invisible panel is indistinguishable from a broken menu item. An all-zero rect means the anchor is detached. The assertion is gated on the popover having non-zero measured size, so an unlaid-out popover (pre-paint, or jsdom) stays silent; a genuinely zero-size popover in production is therefore not reported. **Select mode** (a "Select" control in the Sessions header) turns each row into a checkbox and shows a "Delete N" bar; the row body toggles selection instead of opening, the per-row kebab is hidden, and confirming a count-based `BulkDeleteConfirmModal` (which names N and how many are live) loops the single-id `session-delete` over the selection (no route change — `/api/admin/session-delete` stays single-id). Bulk deletion is instrumented with a `[admin-ui] sidebar-bulk-delete op=start count=<N> live=<M>` / `op=done requested=<N> ok=<X> failed=<Y>` envelope around the per-session `sidebar-session-delete` lines, so a silently-dropped item shows as `ok+failed < requested`. Outside select mode the single-delete and row-open behaviours are unchanged.
|
|
2947
2947
|
|
|
2948
2948
|
**`/chat` empty-state starter chips.** While the admin webchat has no conversation JSONL yet (`GET /api/webchat/session` → `projectDir:null`), `app/chat/page.tsx` renders a "What's up next, {givenName}?" greeting (given name = first whitespace token of the admin session's `userName`; degrades to "What's up next?" without one) above three one-tap starter chips (replacing the earlier read-only lists): **Catch me up** (`Inbox`), **My day** (`CalendarClock`), **To-do** (`ListTodo`). `GreetingPanel` no longer fetches `GET /api/webchat/greeting` — the headline reads from the `userName` prop alone. A tap dispatches the chip's verbatim prompt through the existing `send()` with zero typing, via the same arm/fire path as the `?q=` handoff seed: `dispatchChip` logs `[admin-ui] op=chip-tap id=<catch-up|my-day|to-do> chars=<n>`, sets the composer text, and arms a one-shot effect that fires `send()` once the text matches. Chips gate to `variant` admin/operator; the public surface shows the headline alone. The first journaled turn replaces the panel with the transcript. The `/api/webchat/greeting` route (`server/routes/webchat-greeting.ts`) and `specialist-roster.ts` are now unconsumed by this surface — slimming them is a separate follow-up. The full webchat architecture lives in `.docs/admin-webchat-native-channel.md`.
|
|
2949
2949
|
|
|
@@ -65,7 +65,7 @@ either is a regression.
|
|
|
65
65
|
|
|
66
66
|
**Row title resolution.** Both the sidebar (`/sidebar-sessions`) and the manager's own row payload resolve a row's title in the same order: operator rename → Claude Code `ai-title` → first non-CLI user message → 8-char sessionId prefix. The operator-rename tier is the on-disk `<accountDir>/session-titles.json` (the manager's `UserTitleStore`), keyed by the CC sessionId — the sidebar reads that same file, so a write to the store lights up both surfaces with one write.
|
|
67
67
|
|
|
68
|
-
**Session row delete, rename, and select mode.** The sidebar Sessions row supports three operator affordances. **Rename** (kebab → Rename) opens the inline edit input with its existing title pre-selected on focus, so the first keystroke replaces the whole title rather than appending to it — on both the sidebar (`conv-name-edit`) and the `/chat` Conversations flyout (`op-conv-rename-input`). **Delete** (kebab → Delete) arms a confirm that names the session (its label plus the 8-char id prefix) and renders as a popover anchored beside the row it will delete — reusing the same `AnchoredPopover` the kebab menu uses, not the shared bottom-right info dock — so the target is unambiguous; it still states the delete is permanent and, for a live row, that the session is stopped first. **Select mode** (a "Select" control in the Sessions header) turns each row into a checkbox and shows a "Delete N" bar; the row body toggles selection instead of opening, the per-row kebab is hidden, and confirming a count-based `BulkDeleteConfirmModal` (which names N and how many are live) loops the single-id `session-delete` over the selection (no route change — `/api/admin/session-delete` stays single-id). Bulk deletion is instrumented with a `[admin-ui] sidebar-bulk-delete op=start count=<N> live=<M>` / `op=done requested=<N> ok=<X> failed=<Y>` envelope around the per-session `sidebar-session-delete` lines, so a silently-dropped item shows as `ok+failed < requested`. Outside select mode the single-delete and row-open behaviours are unchanged.
|
|
68
|
+
**Session row delete, rename, and select mode.** The sidebar Sessions row supports three operator affordances. **Rename** (kebab → Rename) opens the inline edit input with its existing title pre-selected on focus, so the first keystroke replaces the whole title rather than appending to it — on both the sidebar (`conv-name-edit`) and the `/chat` Conversations flyout (`op-conv-rename-input`). **Delete** (kebab → Delete) arms a confirm that names the session (its label plus the 8-char id prefix) and renders as a popover anchored beside the row it will delete — reusing the same `AnchoredPopover` the kebab menu uses, not the shared bottom-right info dock — so the target is unambiguous; it still states the delete is permanent and, for a live row, that the session is stopped first. **Usage & cost** (kebab → Usage & cost) opens the per-day active-time / token / GBP-cost table for that session, and anchors beside its row through the same `AnchoredPopover`, with no scrim, so the rest of the dashboard stays legible and interactive while it is open. `AnchoredPopover` right-aligns to the anchor's rect and clamps horizontally against both viewport edges. Vertically it places below when the popover fits there, above when it fits there instead, and otherwise on whichever side has more room — and in every case it bounds the popover's height to the room on the side it chose. That bound is what keeps a long session's panel anchored: a 30-day usage table is taller than the viewport, and clamping it to fit would push it up over the very row it describes, which is not "anchored" to anything. Instead it sits flush against its row and scrolls internally. Because the bound is derived from the anchor rect and the viewport rather than from the popover's own height, the `ResizeObserver` pass it triggers recomputes the same values and settles in one extra pass. The horizontal right-edge clamp does not engage for a left-docked sidebar row (a popover wider than its anchor overflows left, which the left clamp handles); it is kept because the component is general-purpose and is covered by unit test only. Positioning is instrumented, because a mispositioned client-side panel throws nothing and the operator just sees a menu item that did nothing: every positioning pass (each open, each `ResizeObserver` callback, each window resize) emits `[anchored-popover] op=anchor id=<class>:<sessionId> present=<bool> rect=<x,y,w,h>` then `op=placed … maxH=<px> flipped=<bool> clampedLeft=<bool> clampedRight=<bool>`, and `op=offscreen` at `console.error` when the anchor is detached from layout or a placed rect misses the viewport. A pass that computes exactly what the previous pass did is suppressed, so mount and its trailing `ResizeObserver` callback log once rather than twice; the `op=offscreen` assertion is never suppressed. A healthy open is `present=true` with a non-zero rect followed by `op=placed` and no `op=offscreen`. `present=false` means the row is not in `rowRefs` — the row unmounted or its ref callback did not run — and the panel docks bottom-right rather than vanishing, because an invisible panel is indistinguishable from a broken menu item. An all-zero rect means the anchor is detached. The assertion is gated on the popover having non-zero measured size, so an unlaid-out popover (pre-paint, or jsdom) stays silent; a genuinely zero-size popover in production is therefore not reported. **Select mode** (a "Select" control in the Sessions header) turns each row into a checkbox and shows a "Delete N" bar; the row body toggles selection instead of opening, the per-row kebab is hidden, and confirming a count-based `BulkDeleteConfirmModal` (which names N and how many are live) loops the single-id `session-delete` over the selection (no route change — `/api/admin/session-delete` stays single-id). Bulk deletion is instrumented with a `[admin-ui] sidebar-bulk-delete op=start count=<N> live=<M>` / `op=done requested=<N> ok=<X> failed=<Y>` envelope around the per-session `sidebar-session-delete` lines, so a silently-dropped item shows as `ok+failed < requested`. Outside select mode the single-delete and row-open behaviours are unchanged.
|
|
69
69
|
|
|
70
70
|
**`/chat` empty-state starter chips.** While the admin webchat has no conversation JSONL yet (`GET /api/webchat/session` → `projectDir:null`), `app/chat/page.tsx` renders a "What's up next, {givenName}?" greeting (given name = first whitespace token of the admin session's `userName`; degrades to "What's up next?" without one) above three one-tap starter chips (replacing the earlier read-only lists): **Catch me up** (`Inbox`), **My day** (`CalendarClock`), **To-do** (`ListTodo`). `GreetingPanel` no longer fetches `GET /api/webchat/greeting` — the headline reads from the `userName` prop alone. A tap dispatches the chip's verbatim prompt through the existing `send()` with zero typing, via the same arm/fire path as the `?q=` handoff seed: `dispatchChip` logs `[admin-ui] op=chip-tap id=<catch-up|my-day|to-do> chars=<n>`, sets the composer text, and arms a one-shot effect that fires `send()` once the text matches. Chips gate to `variant` admin/operator; the public surface shows the headline alone. The first journaled turn replaces the panel with the transcript. The `/api/webchat/greeting` route (`server/routes/webchat-greeting.ts`) and `specialist-roster.ts` are now unconsumed by this surface — slimming them is a separate follow-up. The full webchat architecture lives in `.docs/admin-webchat-native-channel.md`.
|
|
71
71
|
|
|
@@ -89,7 +89,8 @@ Manages the agent's own dedicated email account — IMAP for reading, SMTP for s
|
|
|
89
89
|
- **Reply:** `email-reply` — threaded reply to an existing email by `messageId`. Resolves the original envelope via IMAP `SEARCH HEADER Message-ID` (INBOX then `\Sent`), so threading works for any message still present on the server even if the graph never ingested it. Supports `replyAll`, `cc`/`bcc`, and `attachments` (same path rules as `email-send`). Explicit `cc`/`bcc` are added in addition to whatever `replyAll` places in `To`, passed through as given (no overlap de-duplication against `To`).
|
|
90
90
|
- **Compose to Drafts:** `email-draft` — builds the same MIME message as `email-send` (same `cc`/`bcc`/`attachments` rules; unlike a dispatched send, the stored draft keeps its `Bcc:` header, because the draft bytes are the only recipient carrier) but APPENDs it to the mailbox's `\Drafts` special-use folder with the `\Draft` flag instead of dispatching it. Nothing is sent. Pass `messageId` to compose a threaded draft-reply: the parent is resolved via IMAP and the draft carries `In-Reply-To`/`References` plus a `Re:` subject from the parent, exactly like `email-reply` (the `subject` argument is then optional and ignored). The success message names the Drafts folder, the APPEND UID, and the draft's own `Message-ID` (`draft id`) so a saved draft is never indistinguishable from a silent no-op and a later edit has a stable handle. Fails naming the mailbox when the server advertises no `\Drafts` special-use folder — no fallback folder is picked, unless a per-account `draftsFolder` override is configured (via `email-setup`), which is honoured before special-use discovery for hosts whose client-visible Drafts is a differently-named mailbox. Use when the operator wants to review and send by hand.
|
|
91
91
|
- **Revise a draft:** `email-draft-edit` — replace a stored draft. The `targetUid` is a fast-path hint; the draft's own `Message-ID` (`draftMessageId`) is the stable identity, because Gmail renumbers a draft's UID when it is touched. The tool tries the UID first, then relocates by a `Message-ID` header search, then composes the replacement from the parameters (same shape as `email-draft`, plus `targetUid`, optional `draftMessageId`, and an optional `messageId` for a threaded revision), APPENDs it with `\Draft`, and deletes the resolved old copy — the replacement is always appended before the old copy is expunged, so a mid-cycle failure never leaves zero copies. When neither identity resolves a live draft, the prior draft is gone: the replacement is saved as a fresh draft (`created`) rather than failing. A single transient transport drop is retried on a fresh connection; auth and other permanent errors surface immediately. Nothing is sent. The confirmation names the folder, the new UID, and the new draft id.
|
|
92
|
-
- **Send a draft:** `email-draft-send` — dispatch a stored draft addressed by its Drafts UID via SMTP, then
|
|
92
|
+
- **Send a draft:** `email-draft-send` — dispatch a stored draft addressed by its Drafts UID via SMTP, then relocate it into `\Sent`. Recipients come from the draft's own To/Cc/Bcc headers (the `\Drafts` copy keeps its `Bcc:`), deduped, sent from the agent alias with an explicit SMTP envelope. The draft is relocated only after SMTP accepts; an SMTP failure leaves it in place and surfaces verbatim. A draft with no recipients is refused before sending. Operator-approved sends only.
|
|
93
|
+
- **The Sent copy is verified, not assumed.** All three send paths read `\Sent` back after SMTP acceptance and search it by Message-ID, then state what is actually there as a fact separate from the send. A folder and UID are named only when they were read back; "no copy was found", "this server advertises no `\Sent` folder", and "the Sent folder could not be checked" are reported as three distinct outcomes, never collapsed. A send whose copy cannot be located is still a successful send. Each send also appends its observed outcome to `config/email-sent-ledger.jsonl`, and the install heartbeat spawns `dist/scripts/sent-copy-sweep.js` hourly to ask `\Sent`, per mailbox, whether each Message-ID sent in the trailing 24 h is actually present (`[email-sent-sweep] op=reconcile …`). Presence is asked per Message-ID rather than by comparing folder totals, because `\Sent` also holds everything the owner sends from other clients. See [`references/email-reference.md`](references/email-reference.md).
|
|
93
94
|
- **Ingestion — fetch:** `email-fetch` — list new IMAP messages since the stored high-water mark. Metadata + body preview only — including attachment names, mimetypes, and sizes so the operator sees what's attached before approving. No bytes downloaded, no graph writes. Stages the batch in-memory for `email-ingest`.
|
|
94
95
|
- **Ingestion — apply:** `email-ingest` — apply the operator's per-message decisions to the staged batch. Each Message-ID must carry a disposition (`ingest` or `discard`). Approved messages land on `:ConversationArchive {source:'email'}` via the source-agnostic conversation-archive pipeline (one archive per thread, sessionised into `:Section` chunks). Attachment bytes are fetched, archived to `{accountDir}/archive/email/<uidValidity>-<uid>/<sha256>-<filename>` (0o600, 25 MB cap), and each attachment lands as a content-addressed `:DigitalDocument` keyed on `attachmentId = sha256(bytes)`; the `:HAS_ENCLOSURE` edge from the parent `:ConversationArchive` is wired by `memory-ingest` on its next pass via the `pendingArchiveEdges` queue. Advances the inbox high-water mark only after a successful write.
|
|
95
96
|
- **Recall/history:** `email-graph-query` — search stored email archives in Neo4j by natural language, participant address, or date range. Each thread is one `:ConversationArchive {source:'email'}` with `:Section` chunks; operator participants attach via `:PARTICIPANT_IN` edges from `:Person`/`:AdminUser`. List mode surfaces a per-archive summary built from concatenated `:Section.summary` values; query mode runs vector search over the `section_embedding` index and dedups to the parent archive.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"confirm-sent-copy.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/confirm-sent-copy.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
const mockClient = vi.hoisted(() => ({
|
|
3
|
+
connect: vi.fn(),
|
|
4
|
+
list: vi.fn(),
|
|
5
|
+
getMailboxLock: vi.fn(),
|
|
6
|
+
search: vi.fn(),
|
|
7
|
+
logout: vi.fn(),
|
|
8
|
+
close: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
vi.mock("imapflow", () => ({
|
|
11
|
+
// A constructible function — `new ImapFlow(...)` returns the spy client.
|
|
12
|
+
ImapFlow: function ImapFlow() {
|
|
13
|
+
return mockClient;
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
const config = {
|
|
17
|
+
email: "a@example.com",
|
|
18
|
+
agentAddress: "a@example.com",
|
|
19
|
+
imapHost: "imap.example.com",
|
|
20
|
+
imapPort: 993,
|
|
21
|
+
imapSecurity: "tls",
|
|
22
|
+
smtpHost: "smtp.example.com",
|
|
23
|
+
smtpPort: 465,
|
|
24
|
+
smtpSecurity: "tls",
|
|
25
|
+
};
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
vi.clearAllMocks();
|
|
28
|
+
mockClient.connect.mockResolvedValue(undefined);
|
|
29
|
+
mockClient.logout.mockResolvedValue(undefined);
|
|
30
|
+
mockClient.getMailboxLock.mockResolvedValue({ release: vi.fn() });
|
|
31
|
+
mockClient.list.mockResolvedValue([
|
|
32
|
+
{ path: "INBOX", specialUse: undefined },
|
|
33
|
+
{ path: "[Gmail]/Sent Mail", specialUse: "\\Sent" },
|
|
34
|
+
]);
|
|
35
|
+
});
|
|
36
|
+
describe("confirmSentCopy", () => {
|
|
37
|
+
it("reports the folder and UID when the copy is found", async () => {
|
|
38
|
+
const { confirmSentCopy } = await import("../lib/imap.js");
|
|
39
|
+
mockClient.search.mockResolvedValue([42]);
|
|
40
|
+
const result = await confirmSentCopy(config, "pw", "abc@example.com");
|
|
41
|
+
expect(result).toEqual({ found: true, sentFolder: "[Gmail]/Sent Mail", sentUid: 42 });
|
|
42
|
+
});
|
|
43
|
+
it("searches the raw header value with angle brackets restored", async () => {
|
|
44
|
+
const { confirmSentCopy } = await import("../lib/imap.js");
|
|
45
|
+
mockClient.search.mockResolvedValue([7]);
|
|
46
|
+
await confirmSentCopy(config, "pw", "abc@example.com");
|
|
47
|
+
expect(mockClient.search).toHaveBeenCalledWith({ header: { "message-id": "<abc@example.com>" } }, { uid: true });
|
|
48
|
+
});
|
|
49
|
+
it("reports not-in-sent with the folder named when the search stays empty", async () => {
|
|
50
|
+
const { confirmSentCopy } = await import("../lib/imap.js");
|
|
51
|
+
mockClient.search.mockResolvedValue([]);
|
|
52
|
+
const result = await confirmSentCopy(config, "pw", "abc@example.com", {
|
|
53
|
+
attempts: 2,
|
|
54
|
+
delayMs: 0,
|
|
55
|
+
});
|
|
56
|
+
expect(result).toEqual({
|
|
57
|
+
found: false,
|
|
58
|
+
reason: "not-in-sent",
|
|
59
|
+
sentFolder: "[Gmail]/Sent Mail",
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
it("retries on provider filing lag and reports found when a later attempt hits", async () => {
|
|
63
|
+
const { confirmSentCopy } = await import("../lib/imap.js");
|
|
64
|
+
mockClient.search.mockResolvedValueOnce([]).mockResolvedValueOnce([99]);
|
|
65
|
+
const result = await confirmSentCopy(config, "pw", "abc@example.com", {
|
|
66
|
+
attempts: 3,
|
|
67
|
+
delayMs: 0,
|
|
68
|
+
});
|
|
69
|
+
expect(result).toEqual({ found: true, sentFolder: "[Gmail]/Sent Mail", sentUid: 99 });
|
|
70
|
+
expect(mockClient.search).toHaveBeenCalledTimes(2);
|
|
71
|
+
});
|
|
72
|
+
it("reports no-sent-folder when the server advertises none", async () => {
|
|
73
|
+
const { confirmSentCopy } = await import("../lib/imap.js");
|
|
74
|
+
mockClient.list.mockResolvedValue([{ path: "INBOX", specialUse: undefined }]);
|
|
75
|
+
const result = await confirmSentCopy(config, "pw", "abc@example.com");
|
|
76
|
+
expect(result).toEqual({ found: false, reason: "no-sent-folder" });
|
|
77
|
+
expect(mockClient.search).not.toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
it("reports check-failed rather than throwing when the connection dies", async () => {
|
|
80
|
+
const { confirmSentCopy } = await import("../lib/imap.js");
|
|
81
|
+
mockClient.connect.mockRejectedValueOnce(new Error("ECONNREFUSED"));
|
|
82
|
+
const result = await confirmSentCopy(config, "pw", "abc@example.com");
|
|
83
|
+
expect(result).toEqual({ found: false, reason: "check-failed", error: "ECONNREFUSED" });
|
|
84
|
+
});
|
|
85
|
+
it("logs out even when the search throws", async () => {
|
|
86
|
+
const { confirmSentCopy } = await import("../lib/imap.js");
|
|
87
|
+
mockClient.search.mockRejectedValue(new Error("boom"));
|
|
88
|
+
await confirmSentCopy(config, "pw", "abc@example.com");
|
|
89
|
+
expect(mockClient.logout).toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
//# sourceMappingURL=confirm-sent-copy.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"confirm-sent-copy.test.js","sourceRoot":"","sources":["../../src/__tests__/confirm-sent-copy.test.ts"],"names":[],"mappings":"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"}
|