claude-code-session-manager 0.86.0 → 0.87.0
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/assets/AgentLibrary-DyLWzZDf.js +3 -0
- package/dist/assets/{DataModel-Q4jhl24R.js → DataModel--mISIJ6h.js} +1 -1
- package/dist/assets/{History-Cj2FejEo.js → History-C2ahUXTg.js} +2 -2
- package/dist/assets/{Hooks-CaelQI6t.js → Hooks-BiC6oyR2.js} +3 -3
- package/dist/assets/{HostBilko--v7cMR8I.js → HostBilko-BPleEOld.js} +1 -1
- package/dist/assets/{Library-DgI9oCCZ.js → Library-Dc8Qst1R.js} +1 -1
- package/dist/assets/{ListDetail-DYUZN-x-.js → ListDetail-DIXh-OLX.js} +1 -1
- package/dist/assets/MarkdownEditor-C90bkLXK.js +1 -0
- package/dist/assets/{McpServers-ypCYURh3.js → McpServers-DqcbLOLZ.js} +2 -2
- package/dist/assets/{Memory-C2qYp-3M.js → Memory-CW62MXlh.js} +4 -4
- package/dist/assets/{Panel-Cj2kw-Zv.js → Panel-Bw1FhRuF.js} +1 -1
- package/dist/assets/Permissions-BcUC-5y8.js +3 -0
- package/dist/assets/{Plugins-C1Vj8_dU.js → Plugins-BnKx9flD.js} +2 -2
- package/dist/assets/{ProvenanceBadge-DczPNM5U.js → ProvenanceBadge-Bw5vNVPT.js} +1 -1
- package/dist/assets/{SaveBar-Cd_7U6Gb.js → SaveBar-CWr0O_w-.js} +1 -1
- package/dist/assets/Scheduler-DYdLuUqq.js +14 -0
- package/dist/assets/{ScopeSwitcher-DVSyI44-.js → ScopeSwitcher-CrBLbg8s.js} +1 -1
- package/dist/assets/Settings-DluB-vN1.js +3 -0
- package/dist/assets/{SkillReferenceGraph-CUv1_Q2c.js → SkillReferenceGraph-CHLSseay.js} +1 -1
- package/dist/assets/{Skills-C_YHkAy-.js → Skills-gNdo_HNK.js} +2 -2
- package/dist/assets/{SystemPrompt-B8R7T9xn.js → SystemPrompt-Cru05-Ia.js} +1 -1
- package/dist/assets/{TagLibrary-dj9YHWyy.js → TagLibrary-DNHY0xou.js} +1 -1
- package/dist/assets/{TiptapBody-DnSBUjHE.js → TiptapBody-I4lmbCgP.js} +1 -1
- package/dist/assets/{Toggle-CjV_BJn6.js → Toggle-bWMHjmRh.js} +1 -1
- package/dist/assets/{index-CDo9xBR9.css → index-DV3PorRY.css} +1 -1
- package/dist/assets/{index-CXFQIPhO.js → index-fc_JjdxL.js} +724 -724
- package/dist/assets/settingsSchema-BfhtZnGD.js +3 -0
- package/dist/index.html +2 -2
- package/package.json +14 -14
- package/plugins/CLAUDE.md +61 -0
- package/plugins/session-manager-dev/.claude-plugin/plugin.json +1 -1
- package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +1 -1
- package/plugins/session-manager-dev/skills/ops-sweep/SKILL.md +1 -1
- package/scripts/scheduler-mcp-server.cjs +7 -0
- package/src/main/__tests__/agentModelResolve.test.cjs +100 -9
- package/src/main/__tests__/broadcastCoalescer.test.cjs +18 -0
- package/src/main/__tests__/epicMint.test.cjs +2 -2
- package/src/main/__tests__/health-delegation-chain.test.cjs +2 -1
- package/src/main/__tests__/needsReviewLedger.test.cjs +162 -0
- package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +3 -3
- package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +15 -1
- package/src/main/__tests__/prdCreateDisposition.test.cjs +201 -0
- package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +125 -0
- package/src/main/__tests__/prdLocations.test.cjs +100 -2
- package/src/main/__tests__/prdLocationsArchived.test.cjs +43 -1
- package/src/main/__tests__/prdSetDisposition.test.cjs +222 -0
- package/src/main/__tests__/queue-health-verdict.test.cjs +170 -0
- package/src/main/__tests__/queue-starvation-per-project.test.cjs +14 -2
- package/src/main/__tests__/queueHistory.test.cjs +63 -0
- package/src/main/__tests__/reconcileTiming.test.cjs +135 -0
- package/src/main/__tests__/scheduleJobTransitions.test.cjs +101 -1
- package/src/main/__tests__/scheduler-boot-orphans.test.cjs +2 -2
- package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +121 -0
- package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +43 -0
- package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +344 -0
- package/src/main/__tests__/scheduler-job-budget.test.cjs +172 -0
- package/src/main/__tests__/scheduler-looks-done.test.cjs +93 -3
- package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +164 -0
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +160 -0
- package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +87 -0
- package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +88 -0
- package/src/main/__tests__/scheduler-starve-escalation.test.cjs +14 -4
- package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +14 -0
- package/src/main/chatRunner.cjs +8 -5
- package/src/main/health.cjs +1 -1
- package/src/main/historyAggregator.cjs +5 -0
- package/src/main/index.cjs +95 -44
- package/src/main/ipcSchemas.cjs +47 -0
- package/src/main/lib/__tests__/active-sessions.test.cjs +251 -0
- package/src/main/lib/__tests__/bootSelfHeal.test.cjs +107 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +322 -43
- package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +239 -0
- package/src/main/lib/__tests__/gitWorktree.test.cjs +89 -0
- package/src/main/lib/__tests__/guardShims.test.cjs +151 -0
- package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +5 -5
- package/src/main/lib/__tests__/prdDisposition.test.cjs +224 -0
- package/src/main/lib/__tests__/reaperHelpers.test.cjs +179 -1
- package/src/main/lib/__tests__/usageCircuit.test.cjs +224 -0
- package/src/main/lib/__tests__/watchdog-helpers.test.cjs +312 -0
- package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +193 -0
- package/{scripts → src/main}/lib/activeSessions.cjs +50 -4
- package/src/main/lib/agentModelResolve.cjs +65 -27
- package/src/main/lib/bootSelfHeal.cjs +88 -0
- package/src/main/lib/delegationReadiness.cjs +290 -225
- package/src/main/lib/effectiveModelInfo.cjs +333 -0
- package/src/main/lib/ephemeralCwd.cjs +1 -1
- package/src/main/lib/epicMint.cjs +3 -3
- package/src/main/lib/gitWorktree.cjs +42 -12
- package/src/main/lib/guardShims.cjs +156 -0
- package/src/main/lib/jobDirtFilter.cjs +7 -2
- package/src/main/lib/launchFailure.cjs +2 -1
- package/src/main/lib/mcpToolCatalog.cjs +4 -1
- package/src/main/lib/needsReviewLedger.cjs +205 -0
- package/src/main/lib/opsErrorLog.cjs +1 -1
- package/src/main/lib/opsOwnership.cjs +1 -1
- package/src/main/lib/prdCreate.cjs +56 -1
- package/src/main/lib/prdDisposition.cjs +199 -0
- package/src/main/lib/prdFrontmatter.cjs +8 -2
- package/src/main/lib/prdLocations.cjs +167 -45
- package/src/main/lib/projectHomeAdminRoutes.cjs +4 -4
- package/src/main/lib/projectPageSummarySchema.cjs +1 -1
- package/src/main/lib/projectRootResolve.cjs +1 -1
- package/src/main/lib/queueHistory.cjs +19 -1
- package/src/main/lib/queueStore.cjs +6 -1
- package/src/main/lib/reaperHelpers.cjs +181 -15
- package/src/main/lib/scheduleJobSchema.cjs +8 -0
- package/src/main/lib/scheduleJobTransitions.cjs +33 -0
- package/src/main/lib/schedulerConfig.cjs +24 -0
- package/src/main/lib/usageCircuit.cjs +159 -0
- package/{scripts → src/main}/lib/watchdogHelpers.cjs +1 -1
- package/src/main/scheduler/prdParser.cjs +13 -0
- package/src/main/scheduler.cjs +1285 -143
- package/src/main/templates/PRD_AUTHORING.md +50 -0
- package/src/main/templates/project-pages-catalog.json +1 -1
- package/src/main/usage.cjs +21 -3
- package/src/preload/api.d.ts +92 -1
- package/src/preload/index.cjs +10 -0
- package/web/README.md +41 -0
- package/{scripts/render-project-pages.cjs → web/project-pages/render.cjs} +4 -4
- package/{scripts/render-project-pages → web/project-pages/renderer}/dist/renderer.cjs +1 -1
- package/{scripts/validate-project-pages-summary.cjs → web/project-pages/validate-summary.cjs} +5 -5
- package/dist/assets/AgentLibrary-DTFL7y8G.js +0 -3
- package/dist/assets/MarkdownEditor-DCIubYWf.js +0 -1
- package/dist/assets/Permissions-BiYZNGYW.js +0 -3
- package/dist/assets/Scheduler-DcLBiJBq.js +0 -14
- package/dist/assets/Settings-Cv-pRyms.js +0 -3
- package/dist/assets/settingsSchema-BJVciriw.js +0 -3
- /package/{scripts/project-pages-logic → web/project-pages/logic}/dist/logic.cjs +0 -0
|
@@ -3,14 +3,19 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* agentModelResolve.cjs — resolves the `--model` an Epic's session should
|
|
5
5
|
* launch a headless `claude -p` (or Terminal `claude`) process with: its
|
|
6
|
-
* agentType persona's own `model` field
|
|
7
|
-
*
|
|
6
|
+
* agentType persona's own `model` field, project-overlay-then-global
|
|
7
|
+
* (`epicMint.cjs`'s `resolvePersonaPaths`), when the Epic has one set and
|
|
8
|
+
* it isn't 'inherit', else `fallbackModel`.
|
|
8
9
|
*
|
|
9
10
|
* Extracted from PRD agent-model-default-terminal (which wired this for the
|
|
10
11
|
* Terminal-view launch in EpicTerminalPane.tsx) so chatRunner.cjs's headless
|
|
11
12
|
* Chat-view launch resolves the SAME persona-derived model — Chat and
|
|
12
13
|
* Terminal are two VIEWS over one Epic session (CLAUDE.md) and must agree on
|
|
13
|
-
* which model that session launches with.
|
|
14
|
+
* which model that session launches with. Terminal reaches `resolveEpicModel`
|
|
15
|
+
* through the `agents:resolve-epic-model` IPC (main-process only; the
|
|
16
|
+
* renderer has no fs access) rather than a second implementation, so the two
|
|
17
|
+
* views literally call the same function instead of two functions that are
|
|
18
|
+
* merely supposed to agree.
|
|
14
19
|
*
|
|
15
20
|
* Plain Node module (sync fs) — no Electron deps, mirrors epicMint.cjs's
|
|
16
21
|
* readActiveIndex so this can be called from chatRunner.cjs's synchronous
|
|
@@ -21,7 +26,7 @@ const fs = require('node:fs');
|
|
|
21
26
|
const path = require('node:path');
|
|
22
27
|
const os = require('node:os');
|
|
23
28
|
const { splitFrontmatter } = require('./prdFrontmatter.cjs');
|
|
24
|
-
const { readActiveIndex } = require('./epicMint.cjs');
|
|
29
|
+
const { readActiveIndex, resolvePersonaPaths } = require('./epicMint.cjs');
|
|
25
30
|
const configMgr = require('../config.cjs');
|
|
26
31
|
|
|
27
32
|
/** The hardcoded floor every branch of resolveEpicModel falls back to — --model must never be omitted (CLAUDE.md "Automation model pinning"). */
|
|
@@ -73,30 +78,57 @@ function findAgentTypeByClaudeSessionId(cwd, claudeSessionId, deps = {}) {
|
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
81
|
+
* Shared miss-tolerant reader `readOverlayAwarePersonaModel` below is a thin
|
|
82
|
+
* wrapper over: tries each candidate path in order (`validatePath` then sync
|
|
83
|
+
* `readFileSync`), and returns the first readable file's `model` frontmatter
|
|
84
|
+
* field — even if that field is absent, which is why an empty-`model`
|
|
85
|
+
* overlay file does NOT fall through to a later candidate (matches
|
|
86
|
+
* `getPersonaBody`'s own semantics). Logs the dangling-persona warning at
|
|
87
|
+
* most once per `(deps.cwd, agentType)` only when EVERY candidate misses.
|
|
88
|
+
* Never throws.
|
|
79
89
|
*/
|
|
80
|
-
function
|
|
81
|
-
if (!agentType) return null;
|
|
82
|
-
const globalDir = deps.globalDir || path.join(os.homedir(), '.claude', 'agents');
|
|
90
|
+
function readModelFromCandidatePaths(candidates, agentType, deps) {
|
|
83
91
|
const validatePath = deps.validatePath || configMgr.validatePath;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
for (const candidate of candidates) {
|
|
93
|
+
let real;
|
|
94
|
+
try {
|
|
95
|
+
real = validatePath(candidate);
|
|
96
|
+
} catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const text = fs.readFileSync(real, 'utf8');
|
|
101
|
+
const { fm } = splitFrontmatter(text);
|
|
102
|
+
return fm.model || null;
|
|
103
|
+
} catch {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
97
106
|
}
|
|
98
|
-
|
|
99
|
-
return
|
|
107
|
+
logDanglingPersonaOnce(deps.cwd, agentType);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Reads an Epic's agentType persona's `model` frontmatter field with the SAME
|
|
113
|
+
* project-overlay-then-global precedence `agentLibrary.cjs`'s `getPersonaBody`
|
|
114
|
+
* (and, through it, `resolvePrdPersonaForSpawn` below) already applies for a
|
|
115
|
+
* scheduled PRD — `epicMint.cjs`'s `resolvePersonaPaths` is the shared path
|
|
116
|
+
* resolver both readers go through, so there is exactly one place that decides
|
|
117
|
+
* which of the two files wins.
|
|
118
|
+
*
|
|
119
|
+
* Sync (not `getPersonaBody`'s async `fsp.readFile`) because `resolveEpicModel`
|
|
120
|
+
* must stay synchronous: `chatRunner.cjs`'s `executeRun()` registers its
|
|
121
|
+
* cancel handle into `inFlight` synchronously, and its caller (`pump()`) reads
|
|
122
|
+
* that entry back immediately after invoking the executor — an `await`
|
|
123
|
+
* inserted ahead of that registration would run it a tick late and silently
|
|
124
|
+
* break `cancel()`. Returns null on a total miss (neither location resolves)
|
|
125
|
+
* — never throws.
|
|
126
|
+
*/
|
|
127
|
+
function readOverlayAwarePersonaModel(agentType, deps = {}) {
|
|
128
|
+
if (!agentType) return null;
|
|
129
|
+
const resolvePaths = deps.resolvePersonaPaths || resolvePersonaPaths;
|
|
130
|
+
const { projectPath, globalPath } = resolvePaths(deps.cwd, agentType, deps);
|
|
131
|
+
return readModelFromCandidatePaths([projectPath, globalPath], agentType, deps);
|
|
100
132
|
}
|
|
101
133
|
|
|
102
134
|
/**
|
|
@@ -105,6 +137,12 @@ function readPersonaModel(agentType, deps = {}) {
|
|
|
105
137
|
* file) falls back to `fallbackModel` (default: 'sonnet', the same literal
|
|
106
138
|
* every claude -p call site pins per CLAUDE.md's model-pinning rule).
|
|
107
139
|
*
|
|
140
|
+
* This is the SINGLE authority both Chat (`chatRunner.cjs`, in-process) and
|
|
141
|
+
* Terminal (`EpicTerminalPane.tsx`, via the `agents:resolve-epic-model` IPC
|
|
142
|
+
* added in the same change) call for an Epic-backed launch, so the two views
|
|
143
|
+
* of one Epic agree on which model that session launches with — see this
|
|
144
|
+
* module's header.
|
|
145
|
+
*
|
|
108
146
|
* @param {{ cwd: string, claudeSessionId: string, fallbackModel?: string, deps?: object }} opts
|
|
109
147
|
* @returns {string}
|
|
110
148
|
*/
|
|
@@ -112,7 +150,7 @@ function resolveEpicModel({ cwd, claudeSessionId, fallbackModel = FALLBACK_MODEL
|
|
|
112
150
|
try {
|
|
113
151
|
const agentType = findAgentTypeByClaudeSessionId(cwd, claudeSessionId, deps);
|
|
114
152
|
if (!agentType) return fallbackModel;
|
|
115
|
-
const model =
|
|
153
|
+
const model = readOverlayAwarePersonaModel(agentType, { ...deps, cwd });
|
|
116
154
|
if (!model || model === 'inherit') return fallbackModel;
|
|
117
155
|
return model;
|
|
118
156
|
} catch {
|
|
@@ -181,6 +219,6 @@ module.exports = {
|
|
|
181
219
|
FALLBACK_MODEL,
|
|
182
220
|
resolveEpicModel,
|
|
183
221
|
findAgentTypeByClaudeSessionId,
|
|
184
|
-
|
|
222
|
+
readOverlayAwarePersonaModel,
|
|
185
223
|
resolvePrdPersonaForSpawn,
|
|
186
224
|
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* bootSelfHeal.cjs — the epic-index self-heal scan, split out of
|
|
5
|
+
* app.whenReady() so it can run off the boot critical path.
|
|
6
|
+
*
|
|
7
|
+
* A project whose active-index.json is MISSING or fails to parse has lost
|
|
8
|
+
* every open Epic's status unless the per-Epic status mirror
|
|
9
|
+
* (epicStatusMirror.cjs) can rebuild it. `allProjectCwds()` is a synchronous
|
|
10
|
+
* ~270ms directory scan; looping it straight through every project inside
|
|
11
|
+
* app.whenReady()'s synchronous body blocked the event loop long enough that
|
|
12
|
+
* the renderer's first IPC round trips (schedule.state, billing.fetch,
|
|
13
|
+
* teams.list) hit their 5s deadlines before any handler could dispatch.
|
|
14
|
+
*
|
|
15
|
+
* `scheduleBootSelfHeal` defers `runEpicIndexSelfHeal` until the main window
|
|
16
|
+
* has painted (`did-finish-load`), with an unconditional `setImmediate`
|
|
17
|
+
* fallback for the window-destroyed-before-load and no-window (headless)
|
|
18
|
+
* cases, so the scan still runs exactly once per boot either way. It yields
|
|
19
|
+
* to the event loop between projects so a machine with thousands of
|
|
20
|
+
* projects can't stall it for a second straight.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
async function runEpicIndexSelfHeal(deps) {
|
|
24
|
+
const { fs, allProjectCwds, promptSessionsActiveIndexPath, rebuildActiveIndex, logs } = deps;
|
|
25
|
+
try {
|
|
26
|
+
for (const projectCwd of allProjectCwds()) {
|
|
27
|
+
const indexPath = promptSessionsActiveIndexPath(projectCwd);
|
|
28
|
+
let unclean = false;
|
|
29
|
+
let parseError = null;
|
|
30
|
+
if (!fs.existsSync(indexPath)) {
|
|
31
|
+
unclean = true;
|
|
32
|
+
} else {
|
|
33
|
+
try {
|
|
34
|
+
JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
35
|
+
} catch (e) {
|
|
36
|
+
unclean = true;
|
|
37
|
+
parseError = e.message;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (unclean) {
|
|
41
|
+
try {
|
|
42
|
+
const { rows, skipped } = rebuildActiveIndex(projectCwd);
|
|
43
|
+
logs.writeLine({
|
|
44
|
+
scope: 'active-index-rebuild',
|
|
45
|
+
level: 'info',
|
|
46
|
+
message: 'rebuilt active-index.json from status mirrors',
|
|
47
|
+
meta: { cwd: projectCwd, parseError, restored: rows.length, skipped: skipped.length },
|
|
48
|
+
});
|
|
49
|
+
} catch (e) {
|
|
50
|
+
logs.writeLine({ scope: 'active-index-rebuild', level: 'error', message: 'rebuild failed', meta: { cwd: projectCwd, error: e?.message } });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Yield between projects so a multi-thousand-project machine can't
|
|
54
|
+
// stall the event loop for a second at a time.
|
|
55
|
+
await new Promise((resolve) => { setImmediate(resolve); });
|
|
56
|
+
}
|
|
57
|
+
} catch (e) {
|
|
58
|
+
logs.writeLine({ scope: 'active-index-rebuild', level: 'error', message: 'boot sweep failed', meta: { error: e?.message } });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function scheduleBootSelfHeal(mainWindow, deps) {
|
|
63
|
+
let ran = false;
|
|
64
|
+
const runOnce = () => {
|
|
65
|
+
if (ran) return;
|
|
66
|
+
ran = true;
|
|
67
|
+
runEpicIndexSelfHeal(deps);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const webContents = mainWindow && typeof mainWindow.isDestroyed === 'function' && !mainWindow.isDestroyed()
|
|
71
|
+
? mainWindow.webContents
|
|
72
|
+
: null;
|
|
73
|
+
|
|
74
|
+
if (webContents && !webContents.isDestroyed()) {
|
|
75
|
+
webContents.once('did-finish-load', runOnce);
|
|
76
|
+
// Edge case: the window can be destroyed before it ever finishes
|
|
77
|
+
// loading (closed during boot, crashed renderer). did-finish-load would
|
|
78
|
+
// then never fire and the self-heal would silently never run — fall
|
|
79
|
+
// back to an unconditional setImmediate so it still runs exactly once.
|
|
80
|
+
webContents.once('destroyed', () => setImmediate(runOnce));
|
|
81
|
+
} else {
|
|
82
|
+
// Headless / no-visible-window boot — nothing will ever emit
|
|
83
|
+
// did-finish-load, so run on the very next event-loop turn instead.
|
|
84
|
+
setImmediate(runOnce);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { scheduleBootSelfHeal, runEpicIndexSelfHeal };
|