@rubytech/create-sitedesk-code 0.1.515 → 0.1.517
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/dist/__tests__/launchd-bootstrap.test.js +164 -18
- package/dist/index.js +15 -14
- package/dist/launchd-bootstrap.js +86 -17
- package/package.json +1 -1
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +53 -4
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +10 -0
- package/payload/platform/plugins/docs/references/admin-ui.md +25 -2
- package/payload/platform/plugins/docs/references/deployment.md +4 -0
- package/payload/platform/plugins/email/PLUGIN.md +1 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-metadata-calendar.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-metadata-calendar.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-metadata-calendar.test.js +94 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-metadata-calendar.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/index.js +1 -1
- package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +21 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js +49 -8
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
- package/payload/platform/plugins/email/references/email-reference.md +4 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-retry.test.js +143 -2
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client-retry.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client.test.js +33 -3
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/graph-client.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-search-paging.test.js +142 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-search-paging.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts +8 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js +40 -0
- package/payload/platform/plugins/outlook/mcp/dist/lib/graph-client.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.js +54 -3
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-search.js.map +1 -1
- package/payload/platform/plugins/outlook/references/graph-surfaces.md +9 -3
- package/payload/server/public/activity.html +5 -5
- package/payload/server/public/agents.html +4 -4
- package/payload/server/public/assets/{AdminLoginScreens-Cs1jSnOF.js → AdminLoginScreens-B5qKb3YK.js} +1 -1
- package/payload/server/public/assets/AdminLoginScreens-B5qKb3YK.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-B5qKb3YK.js.gz +0 -0
- package/payload/server/public/assets/{AdminShell-DkrURZnF.js → AdminShell-BgJEq2zJ.js} +1 -1
- package/payload/server/public/assets/AdminShell-BgJEq2zJ.js.br +0 -0
- package/payload/server/public/assets/AdminShell-BgJEq2zJ.js.gz +0 -0
- package/payload/server/public/assets/{activity-BgZISMuE.js → activity-DmnVUDYO.js} +1 -1
- package/payload/server/public/assets/activity-DmnVUDYO.js.br +0 -0
- package/payload/server/public/assets/activity-DmnVUDYO.js.gz +0 -0
- package/payload/server/public/assets/{admin-DjFysc3-.js → admin-Dpge4OG9.js} +1 -1
- package/payload/server/public/assets/admin-Dpge4OG9.js.br +0 -0
- package/payload/server/public/assets/admin-Dpge4OG9.js.gz +0 -0
- package/payload/server/public/assets/{agents-Bx-MSZW6.js → agents-Bmq9X7P3.js} +1 -1
- package/payload/server/public/assets/agents-Bmq9X7P3.js.br +0 -0
- package/payload/server/public/assets/agents-Bmq9X7P3.js.gz +0 -0
- package/payload/server/public/assets/{browser-1I7syhHu.js → browser-B1G-W80B.js} +1 -1
- package/payload/server/public/assets/browser-B1G-W80B.js.br +0 -0
- package/payload/server/public/assets/browser-B1G-W80B.js.gz +0 -0
- package/payload/server/public/assets/{calendar-D_04JIo1.js → calendar-CHgrOFIN.js} +1 -1
- package/payload/server/public/assets/calendar-CHgrOFIN.js.br +0 -0
- package/payload/server/public/assets/calendar-CHgrOFIN.js.gz +0 -0
- package/payload/server/public/assets/chat-CIwmmtwP.js +1 -0
- package/payload/server/public/assets/chat-CIwmmtwP.js.br +0 -0
- package/payload/server/public/assets/chat-CIwmmtwP.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-EQjfaQ-n.js +1 -0
- package/payload/server/public/assets/chevron-left-EQjfaQ-n.js.br +0 -0
- package/payload/server/public/assets/chevron-right-Ct8jZUGP.js +1 -0
- package/payload/server/public/assets/chevron-right-Ct8jZUGP.js.br +0 -0
- package/payload/server/public/assets/clock-Cxz5ybck.js +1 -0
- package/payload/server/public/assets/clock-Cxz5ybck.js.br +0 -0
- package/payload/server/public/assets/clock-Cxz5ybck.js.gz +0 -0
- package/payload/server/public/assets/data-BQVXKVsJ.js +1 -0
- package/payload/server/public/assets/data-BQVXKVsJ.js.br +1 -0
- package/payload/server/public/assets/data-BQVXKVsJ.js.gz +0 -0
- package/payload/server/public/assets/{file-text-DkvKqHMx.js → file-text-BVqqaDun.js} +1 -1
- package/payload/server/public/assets/file-text-BVqqaDun.js.br +0 -0
- package/payload/server/public/assets/file-text-BVqqaDun.js.gz +0 -0
- package/payload/server/public/assets/{graph-CCZhrwZX.js → graph-CEcME0lp.js} +1 -1
- package/payload/server/public/assets/graph-CEcME0lp.js.br +0 -0
- package/payload/server/public/assets/graph-CEcME0lp.js.gz +0 -0
- package/payload/server/public/assets/{graph-labels-BJ7puQkX.js → graph-labels-Zl32_AOH.js} +1 -1
- package/payload/server/public/assets/graph-labels-Zl32_AOH.js.br +0 -0
- package/payload/server/public/assets/graph-labels-Zl32_AOH.js.gz +0 -0
- package/payload/server/public/assets/{maximize-2-Xdj804U1.js → maximize-2-rkkMyAhf.js} +1 -1
- package/payload/server/public/assets/maximize-2-rkkMyAhf.js.br +0 -0
- package/payload/server/public/assets/maximize-2-rkkMyAhf.js.gz +0 -0
- package/payload/server/public/assets/{operator-BkfNpBs7.js → operator-DlL023ch.js} +1 -1
- package/payload/server/public/assets/operator-DlL023ch.js.br +0 -0
- package/payload/server/public/assets/operator-DlL023ch.js.gz +0 -0
- package/payload/server/public/assets/{page-D5n8ShE7.js → page-BAmYYqxp.js} +1 -1
- package/payload/server/public/assets/page-BAmYYqxp.js.br +0 -0
- package/payload/server/public/assets/page-BAmYYqxp.js.gz +0 -0
- package/payload/server/public/assets/{page-BoYc9c4a.js → page-k0BhBkA2.js} +1 -1
- package/payload/server/public/assets/page-k0BhBkA2.js.br +0 -0
- package/payload/server/public/assets/page-k0BhBkA2.js.gz +0 -0
- package/payload/server/public/assets/{public-CX5XSrxD.js → public-Cqgfmo1D.js} +1 -1
- package/payload/server/public/assets/public-Cqgfmo1D.js.br +0 -0
- package/payload/server/public/assets/public-Cqgfmo1D.js.gz +0 -0
- package/payload/server/public/assets/{rotate-ccw-dh_OhL23.js → rotate-ccw-O3RmqoTy.js} +1 -1
- package/payload/server/public/assets/rotate-ccw-O3RmqoTy.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-O3RmqoTy.js.gz +0 -0
- package/payload/server/public/assets/{routines-bb7dxcrr.js → routines-DMYObMUy.js} +1 -1
- package/payload/server/public/assets/routines-DMYObMUy.js.br +0 -0
- package/payload/server/public/assets/routines-DMYObMUy.js.gz +0 -0
- package/payload/server/public/assets/{skills-TeMwxXmJ.js → skills-DON1BKu5.js} +1 -1
- package/payload/server/public/assets/skills-DON1BKu5.js.br +0 -0
- package/payload/server/public/assets/skills-DON1BKu5.js.gz +0 -0
- package/payload/server/public/assets/{tasks-BqBBJlkg.js → tasks-C2PhUSrf.js} +1 -1
- package/payload/server/public/assets/tasks-C2PhUSrf.js.br +0 -0
- package/payload/server/public/assets/tasks-C2PhUSrf.js.gz +0 -0
- package/payload/server/public/assets/{time-entry-format-Cy_T-wiD.js → time-entry-format-4zgmuQI0.js} +1 -1
- package/payload/server/public/assets/time-entry-format-4zgmuQI0.js.br +0 -0
- package/payload/server/public/assets/time-entry-format-4zgmuQI0.js.gz +0 -0
- package/payload/server/public/assets/{triangle-alert-D-bSbPnX.js → triangle-alert-By5D7wuI.js} +1 -1
- package/payload/server/public/assets/triangle-alert-By5D7wuI.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-By5D7wuI.js.gz +0 -0
- package/payload/server/public/assets/{useCopyFeedback-L7L55nFl.js → useCopyFeedback-Dx0vtm9s.js} +1 -1
- package/payload/server/public/assets/useCopyFeedback-Dx0vtm9s.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-Dx0vtm9s.js.gz +0 -0
- package/payload/server/public/assets/{useSubAccountSwitcher-BYl2c7xA.css → useSubAccountSwitcher-UQQDcj0B.css} +1 -1
- package/payload/server/public/assets/useSubAccountSwitcher-UQQDcj0B.css.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-UQQDcj0B.css.gz +0 -0
- package/payload/server/public/assets/{useVoiceRecorder-MuZ7lpet.js → useVoiceRecorder-BgwOCCwj.js} +1 -1
- package/payload/server/public/assets/useVoiceRecorder-BgwOCCwj.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-BgwOCCwj.js.gz +0 -0
- package/payload/server/public/assets/{wrench-DUG1SZQB.js → wrench-CHfMwLza.js} +1 -1
- package/payload/server/public/assets/wrench-CHfMwLza.js.br +0 -0
- package/payload/server/public/assets/wrench-CHfMwLza.js.gz +0 -0
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +7 -7
- package/payload/server/public/chat.html +13 -13
- package/payload/server/public/data.html +11 -11
- package/payload/server/public/graph.html +9 -9
- package/payload/server/public/index.html +14 -14
- package/payload/server/public/operator.html +14 -14
- package/payload/server/public/public.html +13 -13
- package/payload/server/public/routines.html +6 -6
- package/payload/server/public/skills.html +5 -5
- package/payload/server/public/tasks.html +6 -6
- package/payload/server/server.js +38 -4
- package/payload/platform/plugins/email/Archive.zip +0 -0
- package/payload/server/public/assets/AdminLoginScreens-Cs1jSnOF.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-Cs1jSnOF.js.gz +0 -0
- package/payload/server/public/assets/AdminShell-DkrURZnF.js.br +0 -0
- package/payload/server/public/assets/AdminShell-DkrURZnF.js.gz +0 -0
- package/payload/server/public/assets/activity-BgZISMuE.js.br +0 -0
- package/payload/server/public/assets/activity-BgZISMuE.js.gz +0 -0
- package/payload/server/public/assets/admin-DjFysc3-.js.br +0 -0
- package/payload/server/public/assets/admin-DjFysc3-.js.gz +0 -0
- package/payload/server/public/assets/agents-Bx-MSZW6.js.br +0 -0
- package/payload/server/public/assets/agents-Bx-MSZW6.js.gz +0 -0
- package/payload/server/public/assets/browser-1I7syhHu.js.br +0 -0
- package/payload/server/public/assets/browser-1I7syhHu.js.gz +0 -0
- package/payload/server/public/assets/calendar-D_04JIo1.js.br +0 -0
- package/payload/server/public/assets/calendar-D_04JIo1.js.gz +0 -0
- package/payload/server/public/assets/chat-BFH3FM5a.js +0 -1
- package/payload/server/public/assets/chat-BFH3FM5a.js.br +0 -0
- package/payload/server/public/assets/chat-BFH3FM5a.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-Ux9-qLkk.js +0 -1
- package/payload/server/public/assets/chevron-right-BwIuCfyG.js +0 -1
- package/payload/server/public/assets/chevron-right-BwIuCfyG.js.br +0 -0
- package/payload/server/public/assets/clock-DhJdZRhC.js +0 -1
- package/payload/server/public/assets/clock-DhJdZRhC.js.br +0 -0
- package/payload/server/public/assets/data-ChY_uSIk.js +0 -1
- package/payload/server/public/assets/data-ChY_uSIk.js.br +0 -0
- package/payload/server/public/assets/data-ChY_uSIk.js.gz +0 -0
- package/payload/server/public/assets/file-text-DkvKqHMx.js.br +0 -0
- package/payload/server/public/assets/file-text-DkvKqHMx.js.gz +0 -0
- package/payload/server/public/assets/graph-CCZhrwZX.js.br +0 -0
- package/payload/server/public/assets/graph-CCZhrwZX.js.gz +0 -0
- package/payload/server/public/assets/graph-labels-BJ7puQkX.js.br +0 -0
- package/payload/server/public/assets/graph-labels-BJ7puQkX.js.gz +0 -0
- package/payload/server/public/assets/maximize-2-Xdj804U1.js.br +0 -0
- package/payload/server/public/assets/maximize-2-Xdj804U1.js.gz +0 -0
- package/payload/server/public/assets/operator-BkfNpBs7.js.br +0 -0
- package/payload/server/public/assets/operator-BkfNpBs7.js.gz +0 -0
- package/payload/server/public/assets/page-BoYc9c4a.js.br +0 -0
- package/payload/server/public/assets/page-BoYc9c4a.js.gz +0 -0
- package/payload/server/public/assets/page-D5n8ShE7.js.br +0 -0
- package/payload/server/public/assets/page-D5n8ShE7.js.gz +0 -0
- package/payload/server/public/assets/public-CX5XSrxD.js.br +0 -0
- package/payload/server/public/assets/public-CX5XSrxD.js.gz +0 -0
- package/payload/server/public/assets/rotate-ccw-dh_OhL23.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-dh_OhL23.js.gz +0 -0
- package/payload/server/public/assets/routines-bb7dxcrr.js.br +0 -0
- package/payload/server/public/assets/routines-bb7dxcrr.js.gz +0 -0
- package/payload/server/public/assets/skills-TeMwxXmJ.js.br +0 -0
- package/payload/server/public/assets/skills-TeMwxXmJ.js.gz +0 -0
- package/payload/server/public/assets/tasks-BqBBJlkg.js.br +0 -0
- package/payload/server/public/assets/tasks-BqBBJlkg.js.gz +0 -0
- package/payload/server/public/assets/time-entry-format-Cy_T-wiD.js.br +0 -3
- package/payload/server/public/assets/time-entry-format-Cy_T-wiD.js.gz +0 -0
- package/payload/server/public/assets/triangle-alert-D-bSbPnX.js.br +0 -1
- package/payload/server/public/assets/triangle-alert-D-bSbPnX.js.gz +0 -0
- package/payload/server/public/assets/useCopyFeedback-L7L55nFl.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-L7L55nFl.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-BYl2c7xA.css.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-BYl2c7xA.css.gz +0 -0
- package/payload/server/public/assets/useVoiceRecorder-MuZ7lpet.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-MuZ7lpet.js.gz +0 -0
- package/payload/server/public/assets/wrench-DUG1SZQB.js.br +0 -0
- package/payload/server/public/assets/wrench-DUG1SZQB.js.gz +0 -0
- /package/payload/server/public/assets/{useSubAccountSwitcher-DH0P6QMV.js → useSubAccountSwitcher-CRWoQXuU.js} +0 -0
- /package/payload/server/public/assets/{useSubAccountSwitcher-DH0P6QMV.js.br → useSubAccountSwitcher-CRWoQXuU.js.br} +0 -0
- /package/payload/server/public/assets/{useSubAccountSwitcher-DH0P6QMV.js.gz → useSubAccountSwitcher-CRWoQXuU.js.gz} +0 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
// acceptance grid for launchd-bootstrap.ts (Task 1401).
|
|
1
|
+
// acceptance grid for launchd-bootstrap.ts (Task 1401, reshaped by Task 2069).
|
|
2
2
|
//
|
|
3
|
-
// Locks the darwin
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// Locks the darwin LaunchAgent bootstrap contract: bootout (idempotent, rc 3
|
|
4
|
+
// "No such process" ignored), then poll `launchctl print` until the label
|
|
5
|
+
// leaves the domain, then bootstrap, with a bounded retry on exit 5
|
|
6
|
+
// ("Input/output error" — returned for as long as the previous instance is
|
|
7
|
+
// still terminating). Pure logic — the launchctl runner, the sleep and the
|
|
8
|
+
// clock are all injected, so no real launchctl and no wall-clock delay runs
|
|
9
|
+
// here.
|
|
8
10
|
//
|
|
9
11
|
// Compiles to dist/__tests__/launchd-bootstrap.test.js so
|
|
10
12
|
// `node --test dist/__tests__/*.test.js` picks it up after build.
|
|
@@ -13,33 +15,74 @@ import assert from "node:assert/strict";
|
|
|
13
15
|
import { bootstrapLaunchAgent } from "../launchd-bootstrap.js";
|
|
14
16
|
const OPTS = { gui: "gui/502", label: "com.rubytech.host-claude-session-manager", plistPath: "/tmp/sm.plist" };
|
|
15
17
|
const noSleep = () => { };
|
|
16
|
-
|
|
18
|
+
/** A fake clock the injected sleepFn advances, so the wait-budget arithmetic
|
|
19
|
+
* runs with no wall-clock cost. */
|
|
20
|
+
function fakeClock() {
|
|
21
|
+
let t = 0;
|
|
22
|
+
return { now: () => t, sleep: (ms) => { t += ms; } };
|
|
23
|
+
}
|
|
24
|
+
test("waits for the label to leave the domain before bootstrapping", () => {
|
|
25
|
+
const clock = fakeClock();
|
|
17
26
|
const calls = [];
|
|
27
|
+
let prints = 0;
|
|
18
28
|
const run = (args) => {
|
|
19
29
|
calls.push(args);
|
|
20
|
-
|
|
30
|
+
if (args[0] === "print") {
|
|
31
|
+
prints += 1;
|
|
32
|
+
// The old instance lingers for 3 polls, then leaves the domain.
|
|
33
|
+
return prints <= 3 ? { status: 0, stderr: "" } : { status: 113, stderr: "" };
|
|
34
|
+
}
|
|
35
|
+
if (args[0] === "bootstrap")
|
|
36
|
+
return { status: 0, stderr: "" };
|
|
37
|
+
return { status: 0, stderr: "" };
|
|
21
38
|
};
|
|
22
|
-
const r = bootstrapLaunchAgent(run, { ...OPTS, sleepFn:
|
|
39
|
+
const r = bootstrapLaunchAgent(run, { ...OPTS, sleepFn: clock.sleep, nowFn: clock.now });
|
|
23
40
|
assert.equal(r.ok, true);
|
|
24
41
|
assert.equal(r.attempts, 1);
|
|
42
|
+
assert.equal(prints, 4); // 3 polls with the label present, 1 showing it gone
|
|
43
|
+
assert.equal(r.totalWaitedMs, 1500); // 3 sleeps of 500ms before the gone poll
|
|
44
|
+
const bootstrapIdx = calls.findIndex((c) => c[0] === "bootstrap");
|
|
45
|
+
assert.ok(bootstrapIdx > 3, `bootstrap issued at index ${bootstrapIdx}, before the label was gone`);
|
|
46
|
+
});
|
|
47
|
+
test("fresh install — bootout, one absence poll, bootstrap, attempts=1", () => {
|
|
48
|
+
const clock = fakeClock();
|
|
49
|
+
const calls = [];
|
|
50
|
+
const run = (args) => {
|
|
51
|
+
calls.push(args);
|
|
52
|
+
if (args[0] === "print")
|
|
53
|
+
return { status: 113, stderr: "" };
|
|
54
|
+
if (args[0] === "bootstrap")
|
|
55
|
+
return { status: 0, stderr: "" };
|
|
56
|
+
return { status: 3, stderr: "No such process" };
|
|
57
|
+
};
|
|
58
|
+
const r = bootstrapLaunchAgent(run, { ...OPTS, sleepFn: clock.sleep, nowFn: clock.now });
|
|
59
|
+
assert.equal(r.ok, true);
|
|
60
|
+
assert.equal(r.attempts, 1);
|
|
61
|
+
assert.equal(r.totalWaitedMs, 0); // gone on the first poll — a fresh install never waits
|
|
25
62
|
assert.deepEqual(calls[0], ["bootout", "gui/502/com.rubytech.host-claude-session-manager"]);
|
|
26
|
-
assert.deepEqual(calls[1], ["
|
|
27
|
-
assert.
|
|
63
|
+
assert.deepEqual(calls[1], ["print", "gui/502/com.rubytech.host-claude-session-manager"]);
|
|
64
|
+
assert.deepEqual(calls[2], ["bootstrap", "gui/502", "/tmp/sm.plist"]);
|
|
65
|
+
assert.equal(calls.length, 3);
|
|
28
66
|
});
|
|
29
67
|
test("exit 5 twice then 0 — retries and reports the winning attempt", () => {
|
|
68
|
+
const clock = fakeClock();
|
|
30
69
|
let bootstraps = 0;
|
|
31
70
|
const sleeps = [];
|
|
32
71
|
const run = (args) => {
|
|
72
|
+
if (args[0] === "print")
|
|
73
|
+
return { status: 113, stderr: "" };
|
|
33
74
|
if (args[0] !== "bootstrap")
|
|
34
75
|
return { status: 3, stderr: "No such process" };
|
|
35
76
|
bootstraps += 1;
|
|
36
77
|
return bootstraps < 3 ? { status: 5, stderr: "Bootstrap failed: 5: Input/output error" } : { status: 0, stderr: "" };
|
|
37
78
|
};
|
|
38
|
-
const r = bootstrapLaunchAgent(run, {
|
|
79
|
+
const r = bootstrapLaunchAgent(run, {
|
|
80
|
+
...OPTS, sleepFn: (ms) => { sleeps.push(ms); clock.sleep(ms); }, nowFn: clock.now,
|
|
81
|
+
});
|
|
39
82
|
assert.equal(r.ok, true);
|
|
40
83
|
assert.equal(r.attempts, 3);
|
|
41
84
|
assert.equal(bootstraps, 3);
|
|
42
|
-
assert.equal(sleeps.length, 2); // slept before each of the two retries
|
|
85
|
+
assert.equal(sleeps.filter((ms) => ms === 500).length, 2); // slept before each of the two retries
|
|
43
86
|
});
|
|
44
87
|
test("exit 5 on every attempt — fails after the attempt cap, not before", () => {
|
|
45
88
|
let bootstraps = 0;
|
|
@@ -69,17 +112,120 @@ test("non-5 non-zero bootstrap aborts immediately — no retry", () => {
|
|
|
69
112
|
assert.equal(r.bootstrapStatus, 9);
|
|
70
113
|
assert.equal(bootstraps, 1);
|
|
71
114
|
});
|
|
72
|
-
test("
|
|
115
|
+
test("label never leaves the domain — bootstraps anyway at the budget, wait is reported", () => {
|
|
116
|
+
const clock = fakeClock();
|
|
117
|
+
const lines = [];
|
|
118
|
+
const run = (args) => {
|
|
119
|
+
if (args[0] === "print")
|
|
120
|
+
return { status: 0, stderr: "" }; // always present
|
|
121
|
+
if (args[0] === "bootstrap")
|
|
122
|
+
return { status: 5, stderr: "Bootstrap failed: 5: Input/output error" };
|
|
123
|
+
return { status: 0, stderr: "" };
|
|
124
|
+
};
|
|
125
|
+
const r = bootstrapLaunchAgent(run, {
|
|
126
|
+
...OPTS, maxAttempts: 1, sleepFn: clock.sleep, nowFn: clock.now, logger: (l) => lines.push(l),
|
|
127
|
+
});
|
|
128
|
+
assert.equal(r.ok, false);
|
|
129
|
+
assert.equal(r.lastPresent, true);
|
|
130
|
+
assert.equal(r.totalWaitedMs, 60_000);
|
|
131
|
+
assert.ok(lines.some((l) => /op=wait-timeout waitedMs=60000 polls=\d+ present=true probeFailures=0 action=bootstrap-anyway/.test(l)));
|
|
132
|
+
});
|
|
133
|
+
test("the wait budget is shared across attempts, not spent per attempt", () => {
|
|
134
|
+
const clock = fakeClock();
|
|
135
|
+
const run = (args) => {
|
|
136
|
+
if (args[0] === "print")
|
|
137
|
+
return { status: 0, stderr: "" };
|
|
138
|
+
if (args[0] === "bootstrap")
|
|
139
|
+
return { status: 5, stderr: "eio" };
|
|
140
|
+
return { status: 0, stderr: "" };
|
|
141
|
+
};
|
|
142
|
+
const r = bootstrapLaunchAgent(run, { ...OPTS, maxAttempts: 3, sleepFn: clock.sleep, nowFn: clock.now });
|
|
143
|
+
assert.equal(r.attempts, 3);
|
|
144
|
+
// 60s of waiting in total across three attempts, not 180s.
|
|
145
|
+
assert.equal(r.totalWaitedMs, 60_000);
|
|
146
|
+
});
|
|
147
|
+
test("each exit-5 retry re-enters the wait rather than firing after retryDelayMs", () => {
|
|
148
|
+
const clock = fakeClock();
|
|
149
|
+
const seq = [];
|
|
150
|
+
let prints = 0;
|
|
151
|
+
const run = (args) => {
|
|
152
|
+
seq.push(args[0]);
|
|
153
|
+
if (args[0] === "print") {
|
|
154
|
+
prints += 1;
|
|
155
|
+
// present for one poll per attempt, then gone
|
|
156
|
+
return prints % 2 === 1 ? { status: 0, stderr: "" } : { status: 113, stderr: "" };
|
|
157
|
+
}
|
|
158
|
+
if (args[0] === "bootstrap")
|
|
159
|
+
return { status: 5, stderr: "eio" };
|
|
160
|
+
return { status: 0, stderr: "" };
|
|
161
|
+
};
|
|
162
|
+
bootstrapLaunchAgent(run, { ...OPTS, maxAttempts: 2, sleepFn: clock.sleep, nowFn: clock.now });
|
|
163
|
+
assert.deepEqual(seq, [
|
|
164
|
+
"bootout", "print", "print", "bootstrap",
|
|
165
|
+
"bootout", "print", "print", "bootstrap",
|
|
166
|
+
]);
|
|
167
|
+
});
|
|
168
|
+
test("a probe that cannot run is not read as an absent label", () => {
|
|
169
|
+
const clock = fakeClock();
|
|
170
|
+
const lines = [];
|
|
171
|
+
const run = (args) => {
|
|
172
|
+
// status null = launchctl could not be spawned, or was killed by the
|
|
173
|
+
// spawnSync timeout. That is not evidence the label is gone.
|
|
174
|
+
if (args[0] === "print")
|
|
175
|
+
return { status: null, stderr: "" };
|
|
176
|
+
if (args[0] === "bootstrap")
|
|
177
|
+
return { status: 5, stderr: "eio" };
|
|
178
|
+
return { status: 0, stderr: "" };
|
|
179
|
+
};
|
|
180
|
+
const r = bootstrapLaunchAgent(run, {
|
|
181
|
+
...OPTS, maxAttempts: 1, sleepFn: clock.sleep, nowFn: clock.now, logger: (l) => lines.push(l),
|
|
182
|
+
});
|
|
183
|
+
assert.equal(r.lastPresent, true); // never confirmed gone
|
|
184
|
+
assert.equal(r.totalWaitedMs, 60_000); // the full wait was served, not skipped
|
|
185
|
+
assert.ok(r.probeFailures > 0);
|
|
186
|
+
assert.ok(lines.some((l) => /op=wait-poll present=true probeRc=null/.test(l)));
|
|
187
|
+
});
|
|
188
|
+
test("the wait terminates on the poll cap even if the clock never advances", () => {
|
|
189
|
+
// A wall clock that steps backwards (an NTP correction) would leave the
|
|
190
|
+
// elapsed-time test permanently unsatisfied. Termination must not depend on
|
|
191
|
+
// time moving forwards.
|
|
192
|
+
const frozenNow = () => 0;
|
|
193
|
+
let polls = 0;
|
|
194
|
+
const run = (args) => {
|
|
195
|
+
if (args[0] === "print") {
|
|
196
|
+
polls += 1;
|
|
197
|
+
return { status: 0, stderr: "" };
|
|
198
|
+
}
|
|
199
|
+
if (args[0] === "bootstrap")
|
|
200
|
+
return { status: 5, stderr: "eio" };
|
|
201
|
+
return { status: 0, stderr: "" };
|
|
202
|
+
};
|
|
203
|
+
const r = bootstrapLaunchAgent(run, {
|
|
204
|
+
...OPTS, maxAttempts: 1, sleepFn: noSleep, nowFn: frozenNow,
|
|
205
|
+
});
|
|
206
|
+
assert.equal(r.ok, false);
|
|
207
|
+
assert.equal(r.totalWaitedMs, 0); // the frozen clock measures no elapsed time
|
|
208
|
+
assert.equal(polls, 121); // ceil(60000/500) + 1 — the cap, not an infinite loop
|
|
209
|
+
});
|
|
210
|
+
test("emits one structured line per lifecycle step, correlated by agent label", () => {
|
|
211
|
+
const clock = fakeClock();
|
|
73
212
|
let bootstraps = 0;
|
|
74
213
|
const lines = [];
|
|
75
214
|
const run = (args) => {
|
|
215
|
+
if (args[0] === "print")
|
|
216
|
+
return { status: 113, stderr: "" };
|
|
76
217
|
if (args[0] !== "bootstrap")
|
|
77
218
|
return { status: 3, stderr: "No such process" };
|
|
78
219
|
bootstraps += 1;
|
|
79
220
|
return bootstraps < 2 ? { status: 5, stderr: "eio" } : { status: 0, stderr: "" };
|
|
80
221
|
};
|
|
81
|
-
bootstrapLaunchAgent(run, { ...OPTS, sleepFn:
|
|
82
|
-
|
|
83
|
-
assert.
|
|
84
|
-
|
|
222
|
+
bootstrapLaunchAgent(run, { ...OPTS, sleepFn: clock.sleep, nowFn: clock.now, logger: (l) => lines.push(l) });
|
|
223
|
+
const ops = lines.map((l) => l.match(/op=([a-z-]+)/)[1]);
|
|
224
|
+
assert.deepEqual(ops, [
|
|
225
|
+
"bootout", "wait-poll", "gone", "bootstrap",
|
|
226
|
+
"bootout", "wait-poll", "gone", "bootstrap", "result",
|
|
227
|
+
]);
|
|
228
|
+
assert.ok(lines.every((l) => l.startsWith("[launchd] agent=com.rubytech.host-claude-session-manager ")));
|
|
229
|
+
assert.match(lines[3], /op=bootstrap rc=5 attempt=1 waitedMs=0/);
|
|
230
|
+
assert.match(lines[8], /op=result ok=true attempts=2 totalWaitedMs=0 probeFailures=0 rc=0/);
|
|
85
231
|
});
|
package/dist/index.js
CHANGED
|
@@ -1361,8 +1361,9 @@ function resetNeo4jAuth(port = DEFAULT_NEO4J_PORT, dataDir = "/var/lib/neo4j", p
|
|
|
1361
1361
|
const label = `${launchdLabel()}-neo4j`;
|
|
1362
1362
|
console.log(" Resetting dedicated Neo4j auth (launchd)...");
|
|
1363
1363
|
spawnSync("launchctl", ["bootout", `${gui()}/${label}`], { stdio: "pipe" });
|
|
1364
|
-
// launchctl bootout returns
|
|
1365
|
-
// graceful multi-second store flush on SIGTERM — so
|
|
1364
|
+
// launchctl bootout returns as soon as launchd accepts the request (Task
|
|
1365
|
+
// 2069), and neo4j does a graceful multi-second store flush on SIGTERM — so
|
|
1366
|
+
// the JVM can still hold
|
|
1366
1367
|
// the port (and the store) after bootout returns. Wait for the bolt port to
|
|
1367
1368
|
// free before clearing the auth db and re-seeding: set-initial-password only
|
|
1368
1369
|
// takes effect against a stopped store, and rm must not race a live flush.
|
|
@@ -1386,7 +1387,7 @@ function resetNeo4jAuth(port = DEFAULT_NEO4J_PORT, dataDir = "/var/lib/neo4j", p
|
|
|
1386
1387
|
const plistPath = join(launchAgentsDir(), `${label}.plist`);
|
|
1387
1388
|
const boot = bootstrapLaunchAgent((a) => { const r = spawnSync("launchctl", a, { stdio: "pipe", encoding: "utf-8", timeout: 15_000 }); return { status: r.status, stderr: (r.stderr ?? "").toString() }; }, { gui: gui(), label, plistPath, logger: (l) => { console.log(` ${l}`); logFile(` ${l}`); } });
|
|
1388
1389
|
if (!boot.ok) {
|
|
1389
|
-
throw new Error(`launchctl bootstrap ${label} failed (exit ${boot.bootstrapStatus}) after ${boot.attempts} attempt(s) during dedicated darwin auth reset: ${boot.stderr}`);
|
|
1390
|
+
throw new Error(`launchctl bootstrap ${label} failed (exit ${boot.bootstrapStatus}) after ${boot.attempts} attempt(s), waited ${boot.totalWaitedMs}ms, stillPresent=${boot.lastPresent}, probeFailures=${boot.probeFailures} during dedicated darwin auth reset: ${boot.stderr}`);
|
|
1390
1391
|
}
|
|
1391
1392
|
logFile(` [neo4j-darwin-auth-reset] dedicated datadir=${dataDir} conf=${p.confDir} label=${label}`);
|
|
1392
1393
|
}
|
|
@@ -1804,7 +1805,7 @@ function setupDedicatedNeo4jDarwin() {
|
|
|
1804
1805
|
logFile(` ${plistPath} written (${plist.length} bytes)`);
|
|
1805
1806
|
const boot = bootstrapLaunchAgent((a) => { const r = spawnSync("launchctl", a, { stdio: "pipe", encoding: "utf-8", timeout: 15_000 }); return { status: r.status, stderr: (r.stderr ?? "").toString() }; }, { gui: gui(), label, plistPath, logger: (l) => { console.log(` ${l}`); logFile(` ${l}`); } });
|
|
1806
1807
|
if (!boot.ok) {
|
|
1807
|
-
throw new Error(`launchctl bootstrap ${label} failed (exit ${boot.bootstrapStatus}) after ${boot.attempts} attempt(s): ${boot.stderr}`);
|
|
1808
|
+
throw new Error(`launchctl bootstrap ${label} failed (exit ${boot.bootstrapStatus}) after ${boot.attempts} attempt(s), waited ${boot.totalWaitedMs}ms, stillPresent=${boot.lastPresent}, probeFailures=${boot.probeFailures}: ${boot.stderr}`);
|
|
1808
1809
|
}
|
|
1809
1810
|
// Health-check the brand's bolt port — loud failure, no silent fallback to the
|
|
1810
1811
|
// shared instance (matches the Linux dedicated bind check).
|
|
@@ -3891,12 +3892,12 @@ function installServiceDarwin() {
|
|
|
3891
3892
|
});
|
|
3892
3893
|
if (smBoot.ok) {
|
|
3893
3894
|
console.log(` [launchd] bootstrap ${gui()}/${smLabel} ok`);
|
|
3894
|
-
logFile(` [create-maxy] launchd-plist=${smLabel} loaded=true attempts=${smBoot.attempts}`);
|
|
3895
|
+
logFile(` [create-maxy] launchd-plist=${smLabel} loaded=true attempts=${smBoot.attempts} waitedMs=${smBoot.totalWaitedMs}`);
|
|
3895
3896
|
}
|
|
3896
3897
|
else {
|
|
3897
|
-
console.error(` [launchd] bootstrap ${smLabel} returned ${smBoot.bootstrapStatus} after ${smBoot.attempts} attempt(s): ${smBoot.stderr}`);
|
|
3898
|
-
logFile(` [create-maxy] launchd-plist=${smLabel} loaded=false exit=${smBoot.bootstrapStatus} attempts=${smBoot.attempts}`);
|
|
3899
|
-
throw new Error(`launchctl bootstrap ${smLabel} failed (exit ${smBoot.bootstrapStatus}) after ${smBoot.attempts} attempt(s): ${smBoot.stderr}`);
|
|
3898
|
+
console.error(` [launchd] bootstrap ${smLabel} returned ${smBoot.bootstrapStatus} after ${smBoot.attempts} attempt(s), waited ${smBoot.totalWaitedMs}ms, stillPresent=${smBoot.lastPresent}, probeFailures=${smBoot.probeFailures}: ${smBoot.stderr}`);
|
|
3899
|
+
logFile(` [create-maxy] launchd-plist=${smLabel} loaded=false exit=${smBoot.bootstrapStatus} attempts=${smBoot.attempts} waitedMs=${smBoot.totalWaitedMs} stillPresent=${smBoot.lastPresent} probeFailures=${smBoot.probeFailures}`);
|
|
3900
|
+
throw new Error(`launchctl bootstrap ${smLabel} failed (exit ${smBoot.bootstrapStatus}) after ${smBoot.attempts} attempt(s), waited ${smBoot.totalWaitedMs}ms, stillPresent=${smBoot.lastPresent}, probeFailures=${smBoot.probeFailures}: ${smBoot.stderr}`);
|
|
3900
3901
|
}
|
|
3901
3902
|
// Health-probe the manager (/healthz) so a silent boot failure surfaces at
|
|
3902
3903
|
// install time, not at first session spawn as `manager-unreachable`.
|
|
@@ -3947,11 +3948,11 @@ function installServiceDarwin() {
|
|
|
3947
3948
|
logger: (line) => { console.log(` ${line}`); logFile(` ${line}`); },
|
|
3948
3949
|
});
|
|
3949
3950
|
if (hbBoot.ok) {
|
|
3950
|
-
logFile(` [create-maxy] launchd-plist=${hbLabel} loaded=true attempts=${hbBoot.attempts}`);
|
|
3951
|
+
logFile(` [create-maxy] launchd-plist=${hbLabel} loaded=true attempts=${hbBoot.attempts} waitedMs=${hbBoot.totalWaitedMs}`);
|
|
3951
3952
|
}
|
|
3952
3953
|
else {
|
|
3953
|
-
console.error(` WARNING: heartbeat LaunchAgent ${hbLabel} bootstrap returned ${hbBoot.bootstrapStatus} after ${hbBoot.attempts} attempt(s): ${hbBoot.stderr} — scheduling ticks will not fire until a re-install succeeds.`);
|
|
3954
|
-
logFile(` [create-maxy] launchd-plist=${hbLabel} loaded=false exit=${hbBoot.bootstrapStatus} attempts=${hbBoot.attempts}`);
|
|
3954
|
+
console.error(` WARNING: heartbeat LaunchAgent ${hbLabel} bootstrap returned ${hbBoot.bootstrapStatus} after ${hbBoot.attempts} attempt(s), waited ${hbBoot.totalWaitedMs}ms, stillPresent=${hbBoot.lastPresent}, probeFailures=${hbBoot.probeFailures}: ${hbBoot.stderr} — scheduling ticks will not fire until a re-install succeeds.`);
|
|
3955
|
+
logFile(` [create-maxy] launchd-plist=${hbLabel} loaded=false exit=${hbBoot.bootstrapStatus} attempts=${hbBoot.attempts} waitedMs=${hbBoot.totalWaitedMs} stillPresent=${hbBoot.lastPresent} probeFailures=${hbBoot.probeFailures}`);
|
|
3955
3956
|
}
|
|
3956
3957
|
// Task 1706 — the log rotator. On Linux a */15 entry in the per-brand cron
|
|
3957
3958
|
// block ticks logs-rotate.sh (installCrons/buildBrandCronBlock); darwin has no
|
|
@@ -3987,11 +3988,11 @@ function installServiceDarwin() {
|
|
|
3987
3988
|
logger: (line) => { console.log(` ${line}`); logFile(` ${line}`); },
|
|
3988
3989
|
});
|
|
3989
3990
|
if (lrBoot.ok) {
|
|
3990
|
-
logFile(` [create-maxy] launchd-plist=${lrLabel} loaded=true attempts=${lrBoot.attempts}`);
|
|
3991
|
+
logFile(` [create-maxy] launchd-plist=${lrLabel} loaded=true attempts=${lrBoot.attempts} waitedMs=${lrBoot.totalWaitedMs}`);
|
|
3991
3992
|
}
|
|
3992
3993
|
else {
|
|
3993
|
-
console.error(` WARNING: logs-rotate LaunchAgent ${lrLabel} bootstrap returned ${lrBoot.bootstrapStatus} after ${lrBoot.attempts} attempt(s): ${lrBoot.stderr} — brand logs will not be rotated until a re-install succeeds.`);
|
|
3994
|
-
logFile(` [create-maxy] launchd-plist=${lrLabel} loaded=false exit=${lrBoot.bootstrapStatus} attempts=${lrBoot.attempts}`);
|
|
3994
|
+
console.error(` WARNING: logs-rotate LaunchAgent ${lrLabel} bootstrap returned ${lrBoot.bootstrapStatus} after ${lrBoot.attempts} attempt(s), waited ${lrBoot.totalWaitedMs}ms, stillPresent=${lrBoot.lastPresent}, probeFailures=${lrBoot.probeFailures}: ${lrBoot.stderr} — brand logs will not be rotated until a re-install succeeds.`);
|
|
3995
|
+
logFile(` [create-maxy] launchd-plist=${lrLabel} loaded=false exit=${lrBoot.bootstrapStatus} attempts=${lrBoot.attempts} waitedMs=${lrBoot.totalWaitedMs} stillPresent=${lrBoot.lastPresent} probeFailures=${lrBoot.probeFailures}`);
|
|
3995
3996
|
}
|
|
3996
3997
|
// Wait for the server to come up.
|
|
3997
3998
|
console.log(" Waiting for web server...");
|
|
@@ -1,19 +1,34 @@
|
|
|
1
|
-
// Task 1401 — darwin
|
|
1
|
+
// Task 1401 / Task 2069 — darwin LaunchAgent bootstrap, made idempotent and
|
|
2
2
|
// EIO-tolerant.
|
|
3
3
|
//
|
|
4
|
-
// `launchctl bootstrap`
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
4
|
+
// `launchctl bootstrap` returns exit 5 ("Bootstrap failed: 5: Input/output
|
|
5
|
+
// error") when the label's previous instance has not finished terminating.
|
|
6
|
+
// 1401 read this as a transient racey failure and retried three times 500 ms
|
|
7
|
+
// apart; 2069 measured it instead. `launchctl bootout` returns as soon as
|
|
8
|
+
// launchd accepts the request, but the job's process keeps running — a
|
|
9
|
+
// dedicated Neo4j's JVM took 14.2 s to reach "Stopped." on the Muvin iMac while
|
|
10
|
+
// all three retries fired inside 1.043 s and every one got exit 5. The failure
|
|
11
|
+
// is deterministic for the length of that shutdown, not random.
|
|
10
12
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
// So this helper boots the label out, polls `launchctl print <gui>/<label>`
|
|
14
|
+
// until the label leaves the domain, and only then bootstraps. `print` returns
|
|
15
|
+
// 0 for the whole time the old process lives and non-zero once it exits
|
|
16
|
+
// (measured: rc 113), which makes it an exit-code probe rather than prose
|
|
17
|
+
// parsing. The exit-5 retry is kept for the residual case, and each retry
|
|
18
|
+
// re-enters the wait.
|
|
19
|
+
//
|
|
20
|
+
// What `print`'s exit code does NOT report is liveness: a job that loads and
|
|
21
|
+
// exits immediately still returns rc 0, with "state = not running" appearing
|
|
22
|
+
// only in stdout. Detecting a dead-on-arrival agent therefore needs a different
|
|
23
|
+
// primitive and is not attempted here.
|
|
24
|
+
//
|
|
25
|
+
// The launchctl runner, the sleep, and the clock are all injected, so the logic
|
|
26
|
+
// is unit-tested with no real launchctl and no wall-clock delay. Kept
|
|
27
|
+
// synchronous to match the spawnSync-based installer body (installService is a
|
|
28
|
+
// synchronous `void`).
|
|
29
|
+
/** `launchctl bootstrap` exit code for "Input/output error" — returned for as
|
|
30
|
+
* long as the label's previous instance is still terminating. The wait above
|
|
31
|
+
* is what removes that window; the retry covers what is left. */
|
|
17
32
|
const BOOTSTRAP_EIO = 5;
|
|
18
33
|
/** Blocking sleep with no imports, safe inside the synchronous installer. */
|
|
19
34
|
function blockingSleep(ms) {
|
|
@@ -24,20 +39,69 @@ function blockingSleep(ms) {
|
|
|
24
39
|
export function bootstrapLaunchAgent(run, o) {
|
|
25
40
|
const maxAttempts = Math.max(1, o.maxAttempts ?? 3);
|
|
26
41
|
const retryDelayMs = o.retryDelayMs ?? 500;
|
|
42
|
+
const waitBudgetMs = o.waitBudgetMs ?? 60_000;
|
|
43
|
+
const pollIntervalMs = Math.max(1, o.pollIntervalMs ?? 500);
|
|
44
|
+
const now = o.nowFn ?? Date.now;
|
|
27
45
|
const sleepFn = o.sleepFn ?? blockingSleep;
|
|
28
46
|
const log = o.logger ?? (() => { });
|
|
47
|
+
const target = `${o.gui}/${o.label}`;
|
|
48
|
+
const tag = `[launchd] agent=${o.label}`;
|
|
49
|
+
// The wall clock is not monotonic — an NTP step backwards during the wait
|
|
50
|
+
// would leave the elapsed-time test below permanently unsatisfied. This poll
|
|
51
|
+
// allowance bounds the loop independently of the clock, so termination never
|
|
52
|
+
// depends on time moving forwards.
|
|
53
|
+
let pollsRemaining = Math.ceil(waitBudgetMs / pollIntervalMs) + 1;
|
|
54
|
+
let totalWaitedMs = 0;
|
|
55
|
+
let lastPresent = false;
|
|
56
|
+
let probeFailures = 0;
|
|
29
57
|
let attempt = 0;
|
|
30
58
|
let last = { status: null, stderr: "" };
|
|
31
59
|
for (attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
32
60
|
// Idempotent: bootout any prior instance so the following bootstrap starts
|
|
33
61
|
// clean. rc 3 ("No such process") is the expected fresh case; it and any
|
|
34
62
|
// other bootout rc are ignored — bootout is cleanup, not the verified op.
|
|
35
|
-
const bootout = run(["bootout",
|
|
63
|
+
const bootout = run(["bootout", target]);
|
|
64
|
+
log(`${tag} op=bootout rc=${bootout.status ?? "null"} attempt=${attempt}`);
|
|
65
|
+
// bootout returns as soon as launchd accepts the request, but the job's
|
|
66
|
+
// process can keep running for seconds (Neo4j's JVM: 14.2 s measured on the
|
|
67
|
+
// Muvin iMac). `launchctl print` returns 0 for that whole window and
|
|
68
|
+
// non-zero once the process is gone, so it is the absence probe.
|
|
69
|
+
// Bootstrapping before that is what returns EIO.
|
|
70
|
+
const waitStart = now();
|
|
71
|
+
let present = true;
|
|
72
|
+
let polls = 0;
|
|
73
|
+
for (;;) {
|
|
74
|
+
const p = run(["print", target]);
|
|
75
|
+
polls += 1;
|
|
76
|
+
pollsRemaining -= 1;
|
|
77
|
+
// Three outcomes, not two. rc 0 = still in the domain. A non-zero rc
|
|
78
|
+
// (113) = gone. A null status means the probe could not run at all —
|
|
79
|
+
// that is not evidence of absence, so the wait continues rather than
|
|
80
|
+
// treating an unrunnable probe as a clear domain.
|
|
81
|
+
const gone = p.status !== null && p.status !== 0;
|
|
82
|
+
if (p.status === null)
|
|
83
|
+
probeFailures += 1;
|
|
84
|
+
present = !gone;
|
|
85
|
+
const elapsedMs = now() - waitStart;
|
|
86
|
+
log(`${tag} op=wait-poll present=${present} probeRc=${p.status ?? "null"} elapsedMs=${elapsedMs}`);
|
|
87
|
+
if (gone) {
|
|
88
|
+
log(`${tag} op=gone goneMs=${elapsedMs} polls=${polls} attempt=${attempt}`);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
if (totalWaitedMs + elapsedMs >= waitBudgetMs || pollsRemaining <= 0) {
|
|
92
|
+
log(`${tag} op=wait-timeout waitedMs=${totalWaitedMs + elapsedMs} polls=${polls} present=true probeFailures=${probeFailures} action=bootstrap-anyway`);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
sleepFn(pollIntervalMs);
|
|
96
|
+
}
|
|
97
|
+
totalWaitedMs += Math.max(0, now() - waitStart);
|
|
98
|
+
lastPresent = present;
|
|
36
99
|
const bootstrap = run(["bootstrap", o.gui, o.plistPath]);
|
|
37
100
|
last = { status: bootstrap.status, stderr: (bootstrap.stderr ?? "").trim() };
|
|
38
|
-
log(
|
|
101
|
+
log(`${tag} op=bootstrap rc=${bootstrap.status ?? "null"} attempt=${attempt} waitedMs=${totalWaitedMs}`);
|
|
39
102
|
if (bootstrap.status === 0) {
|
|
40
|
-
|
|
103
|
+
log(`${tag} op=result ok=true attempts=${attempt} totalWaitedMs=${totalWaitedMs} probeFailures=${probeFailures} rc=0`);
|
|
104
|
+
return { ok: true, attempts: attempt, bootstrapStatus: 0, stderr: last.stderr, totalWaitedMs, lastPresent, probeFailures };
|
|
41
105
|
}
|
|
42
106
|
// Only exit 5 (EIO) is retried, and only while attempts remain. Every other
|
|
43
107
|
// non-zero status is a real failure that aborts now rather than burning the
|
|
@@ -48,10 +112,15 @@ export function bootstrapLaunchAgent(run, o) {
|
|
|
48
112
|
}
|
|
49
113
|
break;
|
|
50
114
|
}
|
|
115
|
+
const attempts = Math.min(attempt, maxAttempts);
|
|
116
|
+
log(`${tag} op=result ok=false attempts=${attempts} totalWaitedMs=${totalWaitedMs} probeFailures=${probeFailures} rc=${last.status ?? "null"}`);
|
|
51
117
|
return {
|
|
52
118
|
ok: false,
|
|
53
|
-
attempts
|
|
119
|
+
attempts,
|
|
54
120
|
bootstrapStatus: last.status,
|
|
55
121
|
stderr: last.stderr,
|
|
122
|
+
totalWaitedMs,
|
|
123
|
+
lastPresent,
|
|
124
|
+
probeFailures,
|
|
56
125
|
};
|
|
57
126
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: platform-architecture
|
|
3
3
|
description: Use when grounding any documented-surface claim about what SiteDesk 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:f164b44ebf79eb2e637d1157465fb53eb4d226989b350d3dbcf2df4fc8afd6b2
|
|
5
5
|
brand: sitedesk-code
|
|
6
6
|
product-name: SiteDesk
|
|
7
7
|
---
|
|
@@ -793,7 +793,29 @@ cat ~/Library/LaunchAgents/com.rubytech.maxy-code.plist
|
|
|
793
793
|
|
|
794
794
|
The plist points at the wrapper script the installer wrote and at log files under `$HOME/.maxy-code/logs/`. `launchctl bootstrap`'s exit code is recorded in the install log as `[create-maxy] launchd-plist=… loaded=true|false`.
|
|
795
795
|
|
|
796
|
-
Each brand runs
|
|
796
|
+
Each brand runs the main service (`com.rubytech.maxy-code`) plus three supporting LaunchAgents: a session manager (`…-claude-session-manager`), a heartbeat (`…-heartbeat`) and a log rotator (`…-logs-rotate`). A brand with a dedicated Neo4j adds a fifth, `…-neo4j`.
|
|
797
|
+
|
|
798
|
+
On a re-install the installer boots an agent out before bootstrapping it back. `launchctl bootout` returns as soon as launchd accepts the request, but the job's process keeps running — Neo4j's JVM took 14.2 s to finish its store flush on one host — and `launchctl bootstrap` returns `exit 5: Input/output error` for that entire window. So after the bootout the installer polls `launchctl print gui/$UID/<label>` every 500 ms until the label leaves the domain, up to 60 s in total across all attempts, and only then bootstraps. If the label has still not left when the budget runs out, it bootstraps anyway and the resulting exit code decides.
|
|
799
|
+
|
|
800
|
+
This applies to the four supporting agents and the dedicated Neo4j agent. The main service (`com.rubytech.maxy-code`) bootstraps directly and does not wait.
|
|
801
|
+
|
|
802
|
+
Every step is one line in the install log, keyed by `agent=<label>`:
|
|
803
|
+
|
|
804
|
+
```
|
|
805
|
+
[launchd] agent=<label> op=bootout rc=<n> attempt=<n>
|
|
806
|
+
[launchd] agent=<label> op=wait-poll present=<bool> probeRc=<n|null> elapsedMs=<n>
|
|
807
|
+
[launchd] agent=<label> op=gone goneMs=<n> polls=<n> attempt=<n>
|
|
808
|
+
[launchd] agent=<label> op=bootstrap rc=<n> attempt=<n> waitedMs=<n>
|
|
809
|
+
[launchd] agent=<label> op=result ok=<bool> attempts=<n> totalWaitedMs=<n> probeFailures=<n> rc=<n>
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
Read one agent's whole lifeline with:
|
|
813
|
+
|
|
814
|
+
```bash
|
|
815
|
+
grep '\[launchd\]' ~/.maxy-code/logs/install-*.log | grep 'agent=com.rubytech.maxy-code-neo4j'
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
`op=gone goneMs=<n>` is how long the previous instance actually took to exit. `op=wait-timeout` means it never left within 60 s. `probeRc=null` on a `wait-poll` line means `launchctl print` could not run at all — the wait then continues rather than treating an unrunnable probe as a clear domain, and `probeFailures=<n>` on the result line counts how often that happened. Only an EIO that persists across every attempt aborts the install.
|
|
797
819
|
|
|
798
820
|
## 4. Open the admin UI
|
|
799
821
|
|
|
@@ -3134,6 +3156,22 @@ both `kind: 'specialist'` and differ only by origin.
|
|
|
3134
3156
|
highest-capability agents on the box were the ones the operator could not see.
|
|
3135
3157
|
These are the only agents that can be disabled, and the route enforces that
|
|
3136
3158
|
rather than relying on the UI to withhold the control.
|
|
3159
|
+
**A file in the LIVE directory whose name also exists under
|
|
3160
|
+
`<accountDir>/plugins/*/agents/` is excluded from this set.** `agent-builder`
|
|
3161
|
+
copies the authored file into `specialists/agents/` because presence there is
|
|
3162
|
+
what makes an agent dispatchable, so the live directory says nothing about who
|
|
3163
|
+
wrote the agent and every activated user-created specialist used to render
|
|
3164
|
+
twice, the second time as Shipped. The account plugin tree is the authorship
|
|
3165
|
+
evidence and its row wins, which is also why `disable` refuses a slug present
|
|
3166
|
+
under `plugins/*/agents/`: quarantine exists to stop provisioning re-delivering
|
|
3167
|
+
a file, and provisioning never writes that tree. Both gates read filenames off
|
|
3168
|
+
disk, so a plugin file whose frontmatter does not parse still suppresses its
|
|
3169
|
+
live twin — it is authored either way, and `specialistsSkipped` is what reports
|
|
3170
|
+
the parse failure. The exclusion applies to the live copy only: a copy found in
|
|
3171
|
+
quarantine is operator state, keeps its Shipped row and its `disabled:true`,
|
|
3172
|
+
and is therefore still switchable back on. Each exclusion logs
|
|
3173
|
+
`op=list-shipped-dedup file=<name>` and the count rides `op=list` and the
|
|
3174
|
+
response as `deduped`.
|
|
3137
3175
|
|
|
3138
3176
|
The former "Public" flyout toggle in the account menu was removed; this page is
|
|
3139
3177
|
the sole agent surface.
|
|
@@ -3201,9 +3239,9 @@ explicit `?plugin=` parameter, not inferred from `--` in the slug.
|
|
|
3201
3239
|
|
|
3202
3240
|
| Route | Behaviour |
|
|
3203
3241
|
|-------|-----------|
|
|
3204
|
-
| `GET /api/admin/agents` | Session-scoped. Lists the session account's public `agents/*/` dirs (never the `admin` agent), its user-created specialists from `plugins/*/agents/*.md`, and its shipped specialists, each row tagged `kind` and `origin` and carrying `risk`, `riskiestTool`, `unresolved` and `disabled`. Returns `{agents, accountId, skipped, specialistsSkipped, shippedSkipped}`. 401 when the session maps to no account. |
|
|
3242
|
+
| `GET /api/admin/agents` | Session-scoped. Lists the session account's public `agents/*/` dirs (never the `admin` agent), its user-created specialists from `plugins/*/agents/*.md`, and its shipped specialists, each row tagged `kind` and `origin` and carrying `risk`, `riskiestTool`, `unresolved` and `disabled`. A live-directory candidate whose filename also exists under `plugins/*/agents/` is dropped and counted, so an activated user-created agent is listed once. Returns `{agents, accountId, skipped, specialistsSkipped, shippedSkipped, deduped, riskSurfaceFailed}`. 401 when the session maps to no account. |
|
|
3205
3243
|
| `GET /api/admin/agents/:slug` | Session-scoped. Without `?plugin=` or `?origin=`, returns a public agent's config fields + four owned docs + a `present` map (a missing or unreadable doc is `''`/`present.<role>=false`, never a 500). With `?plugin=`, that plugin's user-created specialist; with `?origin=shipped`, the shipped one. Both specialist shapes add `{risk, riskiestTool, unresolved, byTool, disabled}`, where `byTool` is one class per declared tool. Selection is explicit rather than inferred, because a premium file and a user-created specialist under a plugin of the same name produce the same slug. Unknown 404s, 401 as above. |
|
|
3206
|
-
| `POST /api/admin/agents/:slug/disable?accountId=` | Shipped agents only, enforced here: a slug present in none of the three shipped directories 404s. Moves the file from `specialists/agents/` to `specialists/agents-disabled/` and records the basename in the 0600 store. Returns `{ok, moved}`; `moved:false` means the agent existed only as a bundled template, so nothing was there to move and the store entry is what stops the next provisioning run delivering it. The store is read strictly before anything moves, so an unreadable store 500s with the file untouched rather than rewriting the file whole from an empty set and dropping every other disabled agent. Returns `routingReconciled`, which is `false` when the AGENTS.md reconcile threw: the move and the store write already happened, so the request is not failed, and the caller is told rather than left assuming the prose is correct. Same `accountId` contract as delete. |
|
|
3244
|
+
| `POST /api/admin/agents/:slug/disable?accountId=` | Shipped agents only, enforced here: a slug present in none of the three shipped directories 404s, and so does a slug present under `plugins/*/agents/` (a user-created specialist, `reason=user-created-specialist`), because its activated copy in the live directory would otherwise pass the shipped test. Moves the file from `specialists/agents/` to `specialists/agents-disabled/` and records the basename in the 0600 store. Returns `{ok, moved}`; `moved:false` means the agent existed only as a bundled template, so nothing was there to move and the store entry is what stops the next provisioning run delivering it. The store is read strictly before anything moves, so an unreadable store 500s with the file untouched rather than rewriting the file whole from an empty set and dropping every other disabled agent. Returns `routingReconciled`, which is `false` when the AGENTS.md reconcile threw: the move and the store write already happened, so the request is not failed, and the caller is told rather than left assuming the prose is correct. Same `accountId` contract as delete. |
|
|
3207
3245
|
| `POST /api/admin/agents/:slug/enable?accountId=` | Covers both of disable's outcomes. Returns `{ok, restored}`: a quarantined file moves back (`restored:true`); a bundled-only agent has nothing to move, so clearing the store entry is the whole job (`restored:false`), which is what stops provisioning withholding it. Treating that second case as "nothing to restore" made disable a one-way door. 404 only when neither directory nor store knows the agent. Carries the same `routingReconciled` field; the restored file is what puts the routing line back. |
|
|
3208
3246
|
| `DELETE /api/admin/agents/:slug?accountId=` | Public agents only, on the named validated account. Removes the dir after `deleteAgentProjection`; refuses the `admin` slug (403) and a missing/unknown `accountId` (400) with no write. Loud-fail: a graph-cleanup throw aborts the file removal. |
|
|
3209
3247
|
| `POST /api/admin/agents/:slug/project?accountId=` | Re-projects the named account's on-disk agent into the graph. Same `accountId` contract as delete. |
|
|
@@ -3231,6 +3269,13 @@ Agents page survived undiagnosed on a multi-account install.
|
|
|
3231
3269
|
- `op=delete accountId=<id8> slug=<…> outcome=<ok|failed>` with
|
|
3232
3270
|
`reason=<graph-cleanup-failed|rm-error>` on failure (files preserved on a
|
|
3233
3271
|
graph-cleanup throw).
|
|
3272
|
+
- `op=list-shipped-dedup file=<name>` — one line per live-directory candidate
|
|
3273
|
+
dropped as an authored duplicate, with `deduped=<n>` on the `op=list` line and
|
|
3274
|
+
in the response. `shipped + shippedSkipped + deduped` equals the number of
|
|
3275
|
+
distinct `.md` names across the three source directories, so a listing that
|
|
3276
|
+
does not add up is visible without reproducing anything. The over-exclusion to
|
|
3277
|
+
watch for is a `deduped` line naming a file that is also a bundled or premium
|
|
3278
|
+
agent: the authored file then hides a genuinely shipped one.
|
|
3234
3279
|
- `op=classify account=<id8> agent=<name> origin=<shipped|public|specialist>
|
|
3235
3280
|
risk=<green|amber|red> riskiestTool=<tool> tools=<n> unresolved=<comma-list|none>
|
|
3236
3281
|
disabled=<bool>` — one line per classified agent per listing, so it fires on
|
|
@@ -4795,6 +4840,10 @@ npx -y @rubytech/create-realagent-code # realagent brand
|
|
|
4795
4840
|
|
|
4796
4841
|
**LaunchAgent.** The installer registers `SiteDesk` as a launchd LaunchAgent at `~/Library/LaunchAgents/com.rubytech.<brand-hostname>.plist` — for example `com.rubytech.maxy-code.plist`. Survives logout/login and reboot via `KeepAlive=true` and `RunAtLoad=true`. Two brands on the same Mac get two distinct plists (brand-hostname-keyed), so install order is independent. Use `launchctl print gui/$UID/com.rubytech.<brand-hostname>` for service state. The `[create-maxy] launchd-plist=<path> loaded=true` line in the install log confirms `launchctl bootstrap` accepted the plist; `loaded=false exit=<n>` is the failure signal (run `plutil -lint <path>` to diagnose).
|
|
4797
4842
|
|
|
4843
|
+
**Restart waits for the old instance.** This covers the session-manager, heartbeat, logs-rotate and dedicated-Neo4j agents — not the main service above, which bootstraps directly. A re-install boots the agent out first, but `launchctl bootout` returns as soon as launchd accepts the request: the job's process can keep running for seconds afterwards (a dedicated Neo4j's JVM took 14.2 s to finish its store flush), and `launchctl bootstrap` returns `exit 5: Input/output error` for that whole window. The installer therefore polls `launchctl print gui/$UID/<label>` every 500 ms until the label leaves the domain, up to 60 s total across all attempts, before bootstrapping; if it never leaves, the installer bootstraps anyway and the exit code decides. The lifecycle is one log line per step, keyed `agent=<label>`: `op=bootout`, `op=wait-poll present=<bool> probeRc=<n|null>`, `op=gone goneMs=<n> polls=<n>`, `op=bootstrap rc=<n> waitedMs=<n>`, `op=result ok=<bool> totalWaitedMs=<n> probeFailures=<n>`. `op=gone goneMs=<n>` is how long the previous instance actually took to exit; `op=wait-timeout` means it never left within 60 s; `probeFailures=<n>` counts probes that could not run at all, which makes the wait blind.
|
|
4844
|
+
|
|
4845
|
+
Note `launchctl print`'s exit code reports domain membership, not liveness — a job that loads and exits immediately still returns 0. Detecting a dead-on-arrival agent needs a different primitive and is not attempted; for the main service and the session manager the existing `/healthz` probe is what catches it.
|
|
4846
|
+
|
|
4798
4847
|
**Cloudflare on darwin.** The installer brew-installs the `cloudflared` binary so it is on PATH, but does not invoke `cloudflared service install` or `cloudflared tunnel route dns` — public reach is opt-in. After install, the operator runs `cloudflared tunnel login` (browser-driven) followed by the existing tunnel-setup flow if they want a public address. Grep `[create-maxy] darwin-cloudflare-skip=true` in the install log to confirm the installer took the documented skip path.
|
|
4799
4848
|
|
|
4800
4849
|
**Uninstall.** `npx -y @rubytech/create-maxy-code uninstall` (or the realagent equivalent) bootsout the LaunchAgent, removes the plist, and deletes `~/.<brand>/`. Homebrew-installed dependencies (curl, git, unzip, jq, poppler, ffmpeg, node@22, neo4j, cloudflared) remain — remove them with `brew uninstall` if you want a clean slate.
|
|
@@ -9,6 +9,16 @@ Invoked by the admin agent directly.
|
|
|
9
9
|
|
|
10
10
|
This is the platform's release timeline, newest first. Each entry shows the date it shipped and the version it shipped in, so you can tell the operator how current their install is. To compare, read the installed version from `capabilities-here` and match it against the versions below. Keep answers high level and in plain English; this is a summary, not a full commit log.
|
|
11
11
|
|
|
12
|
+
## 2026-07-27 (0.1.517)
|
|
13
|
+
|
|
14
|
+
- Three stray zip files that had been packaged into the install by mistake are gone, including one that was served on your own site address. They held duplicate logo images and an out-of-date copy of some internal code, and nothing used them. The packaging step now refuses to include a zip at all, so this cannot happen again.
|
|
15
|
+
|
|
16
|
+
## 2026-07-27 (0.1.516)
|
|
17
|
+
|
|
18
|
+
- Meeting invitations attached to an email are no longer invisible: a calendar part now counts as an attachment on every read path.
|
|
19
|
+
- The Agents page no longer lists a specialist you created twice, once as your own and again as shipped.
|
|
20
|
+
- Outlook errors now say what actually happened. A refusal on an item that was deleted reads as deletion rather than a missing permission, and a mailbox being moved gets its own message instead of a bare server error.
|
|
21
|
+
|
|
12
22
|
## 2026-07-27 (0.1.515)
|
|
13
23
|
|
|
14
24
|
- Disabling an agent now takes effect straight away: it is dropped from the routing table, reported as disabled rather than missing, and the agents file is reconciled on both disable and enable.
|