claude-code-session-manager 0.122.0 → 0.123.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-BmJ8hIPw.js → AgentLibrary-DxWN3FSN.js} +2 -2
- package/dist/assets/{Button-BS4kmz4p.js → Button-CISLFSKb.js} +1 -1
- package/dist/assets/DataModel-CYPbhGrx.js +1 -0
- package/dist/assets/{History-pg00b2Gd.js → History-Fhouektw.js} +2 -2
- package/dist/assets/{Hooks-BTEs0UZY.js → Hooks-COfIcyYV.js} +3 -3
- package/dist/assets/{Library-DuaJ-VSn.js → Library-Bt5WOyyu.js} +7 -7
- package/dist/assets/ListDetail-DH7uzYd5.js +1 -0
- package/dist/assets/{McpServers-Dy-wnG_X.js → McpServers-CwyVEzMN.js} +2 -2
- package/dist/assets/{Memory-Bs1T1Jo7.js → Memory-k9TWT9N4.js} +8 -8
- package/dist/assets/{Panel-8UhLXEOW.js → Panel-Di8JqCYN.js} +1 -1
- package/dist/assets/{Permissions-BfwzvmBS.js → Permissions-FFs6Rq1H.js} +3 -3
- package/dist/assets/{Plugins-McjyvvKh.js → Plugins-uK5vl_RE.js} +2 -2
- package/dist/assets/{ProvenanceBadge-D0Ov_6WJ.js → ProvenanceBadge-CGDyGDoN.js} +1 -1
- package/dist/assets/{SaveBar-Cd6hJR6K.js → SaveBar-Cnwc_XfF.js} +1 -1
- package/dist/assets/{Scheduler-BZXKqYJP.js → Scheduler-aACa6_mC.js} +7 -7
- package/dist/assets/{ScopeSwitcher-C24ysV1K.js → ScopeSwitcher-CoG3CSlC.js} +1 -1
- package/dist/assets/Settings-BG0RM6Di.js +4 -0
- package/dist/assets/{SkillReferenceGraph-BsWuiTbw.js → SkillReferenceGraph-COffGu_A.js} +1 -1
- package/dist/assets/Skills--qQpFjjf.js +2 -0
- package/dist/assets/SystemPrompt-Cr9RRSiZ.js +1 -0
- package/dist/assets/TagLibrary-D_ZbXwBv.js +1 -0
- package/dist/assets/{TiptapBody-C4kIvIMH.js → TiptapBody-CCHsa8Jv.js} +1 -1
- package/dist/assets/{Toggle-pP19zlyk.js → Toggle-BUOj4pws.js} +1 -1
- package/dist/assets/{ViewTabs-CpzN8e5I.js → ViewTabs-BdpsL7yr.js} +1 -1
- package/dist/assets/{historyTotals-Caq78bb5.js → historyTotals-Dx6ca881.js} +1 -1
- package/dist/assets/{index-Dd7UQabV.css → index-BnKaYERU.css} +1 -1
- package/dist/assets/{index-Bjtguv6I.js → index-CkNGj8FA.js} +151 -151
- package/dist/assets/{settingsSchema-CC2ySu3d.js → settingsSchema-BZhkpr9e.js} +1 -1
- package/dist/assets/{skillFrontmatter-hDhNo8jE.js → skillFrontmatter-N13KGqbs.js} +1 -1
- package/dist/assets/{useAgentsDirWatch-BuOtY25U.js → useAgentsDirWatch-De9UsbDt.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/plugins/session-manager-dev/skills/develop/SKILL.md +29 -24
- package/scripts/scheduler-mcp-server.cjs +142 -78
- package/src/main/agentLibrary.cjs +8 -3
- package/src/main/build-info.json +4 -4
- package/src/main/crashDiagnostics.cjs +24 -0
- package/src/main/historyAggregator.cjs +41 -7
- package/src/main/index.cjs +13 -3
- package/src/main/ipcSchemas.cjs +30 -22
- package/src/main/lib/activeSessions.cjs +93 -32
- package/src/main/lib/epic/delegationReadiness.cjs +2 -0
- package/src/main/lib/epic/delegationSelfHeal.cjs +77 -0
- package/src/main/lib/epic/epicMint.cjs +1 -1
- package/src/main/lib/epic/epicStandingInstructions.cjs +73 -0
- package/src/main/lib/epic/guardSpawnSettings.cjs +76 -0
- package/src/main/lib/feedback-remote/crossProjectFeedback.cjs +9 -2
- package/src/main/lib/feedback-remote/remoteClientSessions.cjs +3 -11
- package/src/main/lib/feedback-remote/remoteE2eCrypto.cjs +1 -1
- package/src/main/lib/git/gitWorktree.cjs +9 -4
- package/src/main/lib/git/worktreeDeps.cjs +13 -2
- package/src/main/lib/git/worktreeSetup.cjs +212 -0
- package/src/main/lib/mcpToolCatalog.cjs +36 -9
- package/src/main/lib/persona-macro/agentPersonaSchema.cjs +4 -0
- package/src/main/lib/persona-macro/shippedPersonaSeeds.cjs +23 -0
- package/src/main/lib/prd/prdAgentType.cjs +28 -0
- package/src/main/lib/prd/prdBodyPatch.cjs +138 -0
- package/src/main/lib/prd/prdCreate.cjs +245 -27
- package/src/main/lib/prd/prdGateFiles.cjs +31 -0
- package/src/main/lib/scheduler/definitionOfDone.cjs +24 -3
- package/src/main/lib/scheduler/queueStore.cjs +5 -4
- package/src/main/lib/scheduler/scheduleJobSchema.cjs +1 -0
- package/src/main/lib/scheduler/schedulerBatch.cjs +43 -5
- package/src/main/lib/scheduler/schedulerPaths.cjs +2 -0
- package/src/main/lib/scheduler/watchdogHelpers.cjs +114 -22
- package/src/main/lib/telemetry/mainHeapSnapshot.cjs +69 -0
- package/src/main/lib/telemetry/telemetryBoot.cjs +25 -3
- package/src/main/lib/telemetry/telemetryClient.cjs +12 -0
- package/src/main/lib/telemetry/telemetryCounters.cjs +25 -2
- package/src/main/pty.cjs +0 -53
- package/src/main/runVerify.cjs +38 -11
- package/src/main/scheduler/prdParser.cjs +3 -0
- package/src/main/scheduler/prdPaths.cjs +48 -2
- package/src/main/scheduler.cjs +135 -31
- package/src/main/seedDevPlugin.cjs +1 -1
- package/src/main/seedSchedulerMcp.cjs +1 -1
- package/src/main/templates/PRD_AUTHORING.md +8 -4
- package/src/main/webRemote.cjs +38 -132
- package/src/preload/api.d.ts +4 -52
- package/src/preload/index.cjs +0 -24
- package/src/seed/agents/dev-lead.md +2 -1
- package/src/seed/agents/validator.md +2 -1
- package/dist/assets/DataModel-DZHBZrfL.js +0 -1
- package/dist/assets/ListDetail-Cud72s-O.js +0 -1
- package/dist/assets/RemoteMachines-gOlFTvWq.js +0 -3
- package/dist/assets/Settings-DWnN2qxn.js +0 -4
- package/dist/assets/Skills-6pHn6glu.js +0 -2
- package/dist/assets/SystemPrompt-DT0LBH-K.js +0 -1
- package/dist/assets/TagLibrary-DbJ0zbYL.js +0 -1
- package/dist/assets/useRemotePeers-CNQmRBpt.js +0 -1
- package/src/main/lib/feedback-remote/relayClientAuth.cjs +0 -133
- package/src/main/remoteClient.cjs +0 -334
- package/src/main/remoteClientIpc.cjs +0 -133
|
@@ -244,6 +244,36 @@ function renderFilesSection(files) {
|
|
|
244
244
|
];
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Inverse of renderFilesSection: read the bullet entries under the `# Files`
|
|
249
|
+
* heading (up to the next top-level heading) back out of a PRD body. Returns
|
|
250
|
+
* [] when there is no such section. Strips only the renderer's `- ` prefix
|
|
251
|
+
* (plus tolerated wrapping backticks); prose lines are ignored.
|
|
252
|
+
*
|
|
253
|
+
* @param {string} body
|
|
254
|
+
* @returns {string[]}
|
|
255
|
+
*/
|
|
256
|
+
function parseFilesSection(body) {
|
|
257
|
+
if (typeof body !== 'string') return [];
|
|
258
|
+
const lines = body.split(/\r?\n/);
|
|
259
|
+
const start = lines.findIndex((l) => /^#\s+Files\s*$/.test(l));
|
|
260
|
+
if (start === -1) return [];
|
|
261
|
+
const out = [];
|
|
262
|
+
let inFence = false;
|
|
263
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
264
|
+
const line = lines[i];
|
|
265
|
+
if (/^\s*```/.test(line)) { inFence = !inFence; continue; }
|
|
266
|
+
if (!inFence && /^#\s/.test(line)) break;
|
|
267
|
+
const m = line.match(/^[-*]\s+(.+?)\s*$/);
|
|
268
|
+
if (!m) continue;
|
|
269
|
+
let entry = m[1];
|
|
270
|
+
const tick = entry.match(/^`([^`]+)`$/);
|
|
271
|
+
if (tick) entry = tick[1];
|
|
272
|
+
if (entry && !out.includes(entry)) out.push(entry);
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
|
|
247
277
|
/**
|
|
248
278
|
* Render the `# Gate` body section as an array of lines. `commandsOrNone`
|
|
249
279
|
* must already be validated (validateGate's `commands` output) — either
|
|
@@ -280,5 +310,6 @@ module.exports = {
|
|
|
280
310
|
validateGate,
|
|
281
311
|
validateFiles,
|
|
282
312
|
renderFilesSection,
|
|
313
|
+
parseFilesSection,
|
|
283
314
|
renderGateSection,
|
|
284
315
|
};
|
|
@@ -427,7 +427,8 @@ function extractAcCommand(prdBody) {
|
|
|
427
427
|
* Runs the resolved gate with `runGateSequence`, the same runner the
|
|
428
428
|
* scheduler's shadow gate uses: `&&` semantics, each run gets its own
|
|
429
429
|
* isolated TMPDIR and `CI=1`, and at most one gate runs machine-wide at a
|
|
430
|
-
* time
|
|
430
|
+
* time (the flag-free variant is runIsolatedSequence, which never reports
|
|
431
|
+
* 'busy'). This never spawns `claude` and never writes to queue.json — it only
|
|
431
432
|
* re-runs the already-authored gate command.
|
|
432
433
|
*
|
|
433
434
|
* Result `status` meanings:
|
|
@@ -756,6 +757,7 @@ function writeReport(key, { acResults = [], riskFlags = [], runsDir } = {}) {
|
|
|
756
757
|
// records the outcome next to the verdict. Not a claude -p run, so it takes no
|
|
757
758
|
// slot from sessionSlots; instead a single module-level flag keeps at most one
|
|
758
759
|
// shadow gate in flight machine-wide (the scheduler is one process).
|
|
760
|
+
// runIsolatedSequence is the flag-free variant: same run, no single-flight.
|
|
759
761
|
let gateInFlight = false;
|
|
760
762
|
const OUTPUT_TAIL_CHARS = 1000;
|
|
761
763
|
// Bounded full-output ring kept for baseline comparison (failing test ids).
|
|
@@ -829,9 +831,28 @@ function runOneGateCommand(cmd, { cwd, env }) {
|
|
|
829
831
|
*/
|
|
830
832
|
async function runGateSequence(sequence, { cwd }) {
|
|
831
833
|
if (gateInFlight) return { status: 'busy', results: [] };
|
|
834
|
+
gateInFlight = true;
|
|
835
|
+
try {
|
|
836
|
+
return await runIsolatedSequence(sequence, { cwd });
|
|
837
|
+
} finally {
|
|
838
|
+
gateInFlight = false;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Flag-free variant of runGateSequence: same `&&` semantics, own mkdtemp
|
|
844
|
+
* TMPDIR (removed afterwards) and CI=1, but it never reads or writes
|
|
845
|
+
* `gateInFlight`, so it never returns 'busy' and never makes a concurrent
|
|
846
|
+
* runGateSequence return 'busy'. Meant for per-worktree work (e.g. worktree
|
|
847
|
+
* setup) whose callers bound their own parallelism.
|
|
848
|
+
*
|
|
849
|
+
* @param {Array<{argv:string[], timeoutMs:number, env?:object}>} sequence
|
|
850
|
+
* @param {{ cwd: string }} opts
|
|
851
|
+
* @returns {Promise<{ status: 'green'|'red'|'unavailable', results: object[] }>}
|
|
852
|
+
*/
|
|
853
|
+
async function runIsolatedSequence(sequence, { cwd }) {
|
|
832
854
|
if (!Array.isArray(sequence) || !sequence.length) return { status: 'unavailable', results: [] };
|
|
833
855
|
try { fs.statSync(cwd); } catch { return { status: 'unavailable', results: [] }; }
|
|
834
|
-
gateInFlight = true;
|
|
835
856
|
let tmp = null;
|
|
836
857
|
try {
|
|
837
858
|
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-gate-shadow-'));
|
|
@@ -847,7 +868,6 @@ async function runGateSequence(sequence, { cwd }) {
|
|
|
847
868
|
} catch {
|
|
848
869
|
return { status: 'unavailable', results: [] };
|
|
849
870
|
} finally {
|
|
850
|
-
gateInFlight = false;
|
|
851
871
|
if (tmp) { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* best-effort */ } }
|
|
852
872
|
}
|
|
853
873
|
}
|
|
@@ -899,6 +919,7 @@ module.exports = {
|
|
|
899
919
|
isNoneGate,
|
|
900
920
|
resolveGate,
|
|
901
921
|
runGateSequence,
|
|
922
|
+
runIsolatedSequence,
|
|
902
923
|
extractSection,
|
|
903
924
|
reverifyAc,
|
|
904
925
|
reverifyBatch,
|
|
@@ -218,10 +218,11 @@ function stateCwds(opts) {
|
|
|
218
218
|
|
|
219
219
|
function bustCwdCache() {
|
|
220
220
|
cwdCache = { at: 0, cwds: Object.freeze([]) };
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
// brand-new project's
|
|
224
|
-
//
|
|
221
|
+
// Shallow bust: activeSessions keeps its per-dir cache and the next scan
|
|
222
|
+
// only picks up new project dirs. That is enough for new-project
|
|
223
|
+
// visibility — a brand-new project's transcript dir changes
|
|
224
|
+
// ~/.claude/projects' mtime, which the incremental path already sees.
|
|
225
|
+
// Callers needing a never-stale scan pass { deep: true } themselves.
|
|
225
226
|
bustProjectCwdCache();
|
|
226
227
|
}
|
|
227
228
|
|
|
@@ -88,6 +88,7 @@ const ScheduleJobSchema = z
|
|
|
88
88
|
runtime: ScheduleJobRuntimeSchema.optional(),
|
|
89
89
|
verifierVerdict: z.string().optional(),
|
|
90
90
|
dependsOn: z.array(z.string()).optional(),
|
|
91
|
+
files: z.array(z.string()).optional(),
|
|
91
92
|
originSessionId: z.string().nullable().optional(),
|
|
92
93
|
sourceTabId: z.string().nullable().optional(),
|
|
93
94
|
sourcePromptId: z.string().nullable().optional(),
|
|
@@ -141,8 +141,32 @@ function findBlockingDep(job, projectJobs, satisfiedSlugs = new Set()) {
|
|
|
141
141
|
});
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/** Normalise a `files` entry for overlap comparison (`./a//b` -> `a/b`, trailing `/` kept). */
|
|
145
|
+
function normFilePath(f) {
|
|
146
|
+
const raw = String(f ?? '').trim().replace(/\\/g, '/').replace(/^(\.\/)+/, '').replace(/\/{2,}/g, '/');
|
|
147
|
+
return raw;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Equal paths, or a folder entry (ending in `/`) that prefixes the other path. */
|
|
151
|
+
function pathsOverlap(a, b) {
|
|
152
|
+
if (!a || !b) return false;
|
|
153
|
+
if (a === b) return true;
|
|
154
|
+
if (a.endsWith('/') && b.startsWith(a)) return true;
|
|
155
|
+
if (b.endsWith('/') && a.startsWith(b)) return true;
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** True when two jobs' `files` arrays overlap. Empty/missing files never overlap. */
|
|
160
|
+
function filesOverlap(aFiles, bFiles) {
|
|
161
|
+
if (!Array.isArray(aFiles) || !Array.isArray(bFiles)) return false;
|
|
162
|
+
const a = aFiles.map(normFilePath).filter(Boolean);
|
|
163
|
+
const b = bFiles.map(normFilePath).filter(Boolean);
|
|
164
|
+
return a.some((x) => b.some((y) => pathsOverlap(x, y)));
|
|
165
|
+
}
|
|
166
|
+
|
|
144
167
|
/**
|
|
145
|
-
* Per-project batch picker. `dependsOn` is the
|
|
168
|
+
* Per-project batch picker. `dependsOn` is the only ORDERING primitive;
|
|
169
|
+
* files-overlap (see below) is a mutual-exclusion hold, never an order.
|
|
146
170
|
*
|
|
147
171
|
* Rules:
|
|
148
172
|
* 1. Eligible = pending, not already running, and no blocking dependency.
|
|
@@ -386,10 +410,24 @@ function pickForProject(projectJobs, runningSlugsInProject, slots, heldSlugs = n
|
|
|
386
410
|
// parallelGroup is a PRIORITY HINT, not a barrier: when there are more
|
|
387
411
|
// eligible jobs than slots, the lower (earlier-authored) PRD numbers go
|
|
388
412
|
// first. Everything else about the number is display-only.
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
413
|
+
// files-overlap: mutual exclusion only. An eligible job is skipped while a
|
|
414
|
+
// running job (or one picked earlier in this call) shares a path with it.
|
|
415
|
+
const inFlight = projectJobs.filter((j) => j.status === 'running' || runningSlugsInProject.has(j.slug));
|
|
416
|
+
const batch = [];
|
|
417
|
+
for (const j of pending.slice().sort((a, b) => (a.parallelGroup ?? 99) - (b.parallelGroup ?? 99))) {
|
|
418
|
+
if (batch.length >= effectiveSlots) break;
|
|
419
|
+
const other = [...inFlight, ...batch].find((o) => o.slug !== j.slug && filesOverlap(j.files, o.files));
|
|
420
|
+
if (other) {
|
|
421
|
+
holds.push({ slug: j.slug, dep: null, depStatus: null, reason: `files-overlap:${other.slug}` });
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
batch.push(j);
|
|
425
|
+
}
|
|
426
|
+
if (batch.length === 0) {
|
|
427
|
+
const reason = `[scheduler] files-overlap [${projectCwd}]: holding ${pending.length} eligible job(s) behind running jobs sharing files`;
|
|
428
|
+
console.log(reason);
|
|
429
|
+
return { batch: [], reason, holds };
|
|
430
|
+
}
|
|
393
431
|
console.log(
|
|
394
432
|
`[scheduler] concurrency [${projectCwd}]: firing ${batch.length} of ${pending.length} ` +
|
|
395
433
|
`eligible job(s) [${batch.map((j) => j.slug).join(', ')}]`,
|
|
@@ -119,6 +119,7 @@ function watchdogRelaunchLogPath() {
|
|
|
119
119
|
);
|
|
120
120
|
}
|
|
121
121
|
function historyRollupStampPath() { return assertNotLiveRoot(path.join(schedulerHome(), 'history-rollup.stamp'), 'historyRollupStampPath'); }
|
|
122
|
+
function historyRollupCursorPath() { return assertNotLiveRoot(path.join(schedulerHome(), 'history-rollup.cursor.json'), 'historyRollupCursorPath'); }
|
|
122
123
|
function historyRollupLockPath() { return assertNotLiveRoot(path.join(schedulerHome(), 'history-rollup.lock'), 'historyRollupLockPath'); }
|
|
123
124
|
function historyRollupPath() { return path.join(schedulerHome(), 'history-rollup.jsonl'); }
|
|
124
125
|
function auditLogPath() { return path.join(schedulerHome(), 'audit-log.jsonl'); }
|
|
@@ -206,6 +207,7 @@ module.exports = {
|
|
|
206
207
|
watchdogRelaunchStatePath,
|
|
207
208
|
watchdogRelaunchLogPath,
|
|
208
209
|
historyRollupStampPath,
|
|
210
|
+
historyRollupCursorPath,
|
|
209
211
|
historyRollupLockPath,
|
|
210
212
|
historyRollupPath,
|
|
211
213
|
auditLogPath,
|
|
@@ -158,6 +158,8 @@ function evaluateDispatchLiveness(entry, now = Date.now(), { deadMs = DEFAULT_DI
|
|
|
158
158
|
|
|
159
159
|
const DEFAULT_RELAUNCH_DEBOUNCE_MS = 90_000;
|
|
160
160
|
const DEFAULT_MAX_RELAUNCH_ATTEMPTS = 3;
|
|
161
|
+
// After the fast attempts are spent, keep retrying forever on this bounded backoff.
|
|
162
|
+
const DEFAULT_CAPPED_RETRY_MS = 30 * 60_000;
|
|
161
163
|
|
|
162
164
|
/**
|
|
163
165
|
* checkAppLiveness(opts?) → { alive: boolean, reason: string }
|
|
@@ -188,12 +190,26 @@ function readRelaunchState(statePath = schedulerPaths.watchdogRelaunchStatePath(
|
|
|
188
190
|
return {
|
|
189
191
|
lastAttemptTs: typeof raw.lastAttemptTs === 'number' ? raw.lastAttemptTs : null,
|
|
190
192
|
attemptCount: typeof raw.attemptCount === 'number' ? raw.attemptCount : 0,
|
|
193
|
+
lastBackoffLogTs: typeof raw.lastBackoffLogTs === 'number' ? raw.lastBackoffLogTs : null,
|
|
191
194
|
};
|
|
192
195
|
} catch {
|
|
193
|
-
return { lastAttemptTs: null, attemptCount: 0 };
|
|
196
|
+
return { lastAttemptTs: null, attemptCount: 0, lastBackoffLogTs: null };
|
|
194
197
|
}
|
|
195
198
|
}
|
|
196
199
|
|
|
200
|
+
/**
|
|
201
|
+
* resetRelaunchState — zero the attempt counter. Called by the watchdog on every
|
|
202
|
+
* fresh, non-degraded heartbeat. Writes only when the state is not already zeroed.
|
|
203
|
+
*/
|
|
204
|
+
function resetRelaunchState({ statePath = schedulerPaths.watchdogRelaunchStatePath() } = {}) {
|
|
205
|
+
const state = readRelaunchState(statePath);
|
|
206
|
+
if (state.attemptCount === 0 && state.lastAttemptTs === null && state.lastBackoffLogTs === null) {
|
|
207
|
+
return { reset: false };
|
|
208
|
+
}
|
|
209
|
+
writeRelaunchState(statePath, { lastAttemptTs: null, attemptCount: 0 });
|
|
210
|
+
return { reset: true };
|
|
211
|
+
}
|
|
212
|
+
|
|
197
213
|
/** Atomic write via atomicFs.cjs (mirrors config.cjs writeJsonSync). */
|
|
198
214
|
function writeRelaunchState(statePath, state) {
|
|
199
215
|
atomicFs.writeJsonAtomicSync(statePath, state);
|
|
@@ -231,15 +247,73 @@ function relaunchSpec(opts) {
|
|
|
231
247
|
return { command: 'npx', args: ['claude-code-session-manager@latest'], packaged: false };
|
|
232
248
|
}
|
|
233
249
|
|
|
234
|
-
|
|
235
|
-
|
|
250
|
+
/** Resolve an executable on a PATH string (no shell). Returns null when absent. */
|
|
251
|
+
function findOnPath(name, envPath) {
|
|
252
|
+
if (typeof envPath !== 'string') return null;
|
|
253
|
+
for (const dir of envPath.split(':')) {
|
|
254
|
+
if (!dir) continue;
|
|
255
|
+
const candidate = path.join(dir, name);
|
|
256
|
+
try {
|
|
257
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
258
|
+
return candidate;
|
|
259
|
+
} catch { /* keep scanning */ }
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* relaunchInvocation — pure. Under systemd (INVOCATION_ID set) the relaunch must
|
|
266
|
+
* live in its own transient user unit, or the oneshot watchdog unit's cgroup
|
|
267
|
+
* teardown kills it. KillMode=process so a successor spawned by the old app's
|
|
268
|
+
* npx survives that npx exiting.
|
|
269
|
+
*/
|
|
270
|
+
function relaunchInvocation({ spec, env, logPath, now, systemdRunPath }) {
|
|
271
|
+
if (typeof env?.INVOCATION_ID === 'string' && env.INVOCATION_ID !== '' && typeof systemdRunPath === 'string') {
|
|
272
|
+
return {
|
|
273
|
+
mode: 'systemd-run',
|
|
274
|
+
command: systemdRunPath,
|
|
275
|
+
args: [
|
|
276
|
+
'--user', '--collect', '--quiet',
|
|
277
|
+
`--unit=session-manager-relaunch-${now}`,
|
|
278
|
+
'-p', 'KillMode=process',
|
|
279
|
+
'-p', `StandardOutput=append:${logPath}`,
|
|
280
|
+
'-p', `StandardError=append:${logPath}`,
|
|
281
|
+
`--setenv=PATH=${env.PATH}`,
|
|
282
|
+
'--',
|
|
283
|
+
spec.command, ...spec.args,
|
|
284
|
+
],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
return { mode: 'detached', command: spec.command, args: spec.args };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function defaultSpawnRelaunch({
|
|
291
|
+
logPath = schedulerPaths.watchdogRelaunchLogPath(), packaged, execPath, spawnFn, spawnSyncFn,
|
|
292
|
+
env: envIn = process.env, now = Date.now(),
|
|
293
|
+
} = {}) {
|
|
294
|
+
const cp = require('node:child_process');
|
|
295
|
+
const spawn = spawnFn || cp.spawn;
|
|
296
|
+
const spawnSync = spawnSyncFn || cp.spawnSync;
|
|
236
297
|
const spec = relaunchSpec({ packaged, execPath });
|
|
237
298
|
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
299
|
+
const env = { ...envIn };
|
|
300
|
+
// The watchdog may itself run as node via the app binary; the relaunched app must not.
|
|
301
|
+
if (spec.packaged) delete env.ELECTRON_RUN_AS_NODE;
|
|
302
|
+
const inv = relaunchInvocation({
|
|
303
|
+
spec, env, logPath, now,
|
|
304
|
+
systemdRunPath: env.INVOCATION_ID ? findOnPath('systemd-run', env.PATH) : null,
|
|
305
|
+
});
|
|
306
|
+
let result = { mode: 'detached' };
|
|
307
|
+
if (inv.mode === 'systemd-run') {
|
|
308
|
+
const r = spawnSync(inv.command, inv.args, { timeout: 15000, stdio: 'ignore', env });
|
|
309
|
+
if (r && !r.error && r.status === 0) {
|
|
310
|
+
return { mode: 'systemd-run', ok: true, unit: `session-manager-relaunch-${now}` };
|
|
311
|
+
}
|
|
312
|
+
logRelaunchLine(logPath, `systemd-run failed (${r?.error?.message || `status ${r?.status}`}) — falling back to detached spawn`);
|
|
313
|
+
result = { mode: 'detached-fallback' };
|
|
314
|
+
}
|
|
238
315
|
const fd = fs.openSync(logPath, 'a');
|
|
239
316
|
try {
|
|
240
|
-
const env = { ...process.env };
|
|
241
|
-
// The watchdog may itself run as node via the app binary; the relaunched app must not.
|
|
242
|
-
if (spec.packaged) delete env.ELECTRON_RUN_AS_NODE;
|
|
243
317
|
const child = spawn(spec.command, spec.args, {
|
|
244
318
|
detached: true,
|
|
245
319
|
stdio: ['ignore', fd, fd],
|
|
@@ -252,6 +326,7 @@ function defaultSpawnRelaunch({ logPath = schedulerPaths.watchdogRelaunchLogPath
|
|
|
252
326
|
} finally {
|
|
253
327
|
fs.closeSync(fd);
|
|
254
328
|
}
|
|
329
|
+
return result;
|
|
255
330
|
}
|
|
256
331
|
|
|
257
332
|
/**
|
|
@@ -259,14 +334,14 @@ function defaultSpawnRelaunch({ logPath = schedulerPaths.watchdogRelaunchLogPath
|
|
|
259
334
|
*
|
|
260
335
|
* Debounced, capped auto-relaunch:
|
|
261
336
|
* - alive (heartbeat fresh or pid alive) → no-op, reset attempt state.
|
|
262
|
-
* - dead + attemptCount
|
|
337
|
+
* - dead + attemptCount at maxAttempts, last attempt < cappedRetryMs ago
|
|
338
|
+
* → skip ('backoff'), one log line per window.
|
|
339
|
+
* - dead + attemptCount at maxAttempts, window elapsed → spawn again (retries forever).
|
|
263
340
|
* - dead + last attempt < debounceMs ago → skip (still booting).
|
|
264
341
|
* - dead + debounce elapsed + under cap → spawn, bump attemptCount, log.
|
|
265
342
|
*
|
|
266
|
-
* attemptCount
|
|
267
|
-
*
|
|
268
|
-
* produce a fresh heartbeat permanently cap further attempts until a human
|
|
269
|
-
* intervenes or the app comes up some other way.
|
|
343
|
+
* attemptCount resets to 0 when the alive branch here sees a live app, and the
|
|
344
|
+
* watchdog script also calls resetRelaunchState() on every fresh heartbeat.
|
|
270
345
|
*/
|
|
271
346
|
function maybeRelaunchApp({
|
|
272
347
|
heartbeatPath = schedulerPaths.heartbeatPath(),
|
|
@@ -275,6 +350,7 @@ function maybeRelaunchApp({
|
|
|
275
350
|
logPath = schedulerPaths.watchdogRelaunchLogPath(),
|
|
276
351
|
debounceMs = DEFAULT_RELAUNCH_DEBOUNCE_MS,
|
|
277
352
|
maxAttempts = DEFAULT_MAX_RELAUNCH_ATTEMPTS,
|
|
353
|
+
cappedRetryMs = DEFAULT_CAPPED_RETRY_MS,
|
|
278
354
|
now = Date.now(),
|
|
279
355
|
spawnFn = defaultSpawnRelaunch,
|
|
280
356
|
restartingMarkerPath = schedulerPaths.restartingMarkerPath(),
|
|
@@ -283,7 +359,7 @@ function maybeRelaunchApp({
|
|
|
283
359
|
const state = readRelaunchState(statePath);
|
|
284
360
|
|
|
285
361
|
if (liveness.alive) {
|
|
286
|
-
if (state.attemptCount !== 0 || state.lastAttemptTs !== null) {
|
|
362
|
+
if (state.attemptCount !== 0 || state.lastAttemptTs !== null || state.lastBackoffLogTs !== null) {
|
|
287
363
|
writeRelaunchState(statePath, { lastAttemptTs: null, attemptCount: 0 });
|
|
288
364
|
}
|
|
289
365
|
return { relaunched: false, reason: 'alive', attemptCount: 0 };
|
|
@@ -298,23 +374,35 @@ function maybeRelaunchApp({
|
|
|
298
374
|
}
|
|
299
375
|
|
|
300
376
|
if (state.attemptCount >= maxAttempts) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
377
|
+
const sinceLast = state.lastAttemptTs === null ? Infinity : now - state.lastAttemptTs;
|
|
378
|
+
if (sinceLast < cappedRetryMs) {
|
|
379
|
+
const nextRetryInMs = cappedRetryMs - sinceLast;
|
|
380
|
+
const logged = state.lastBackoffLogTs;
|
|
381
|
+
if (logged === null || logged < state.lastAttemptTs || now - logged >= cappedRetryMs) {
|
|
382
|
+
logRelaunchLine(
|
|
383
|
+
logPath,
|
|
384
|
+
`backoff: ${state.attemptCount} relaunch attempt(s) made and heartbeat is still stale — next retry in ${Math.ceil(nextRetryInMs / 60_000)} min`,
|
|
385
|
+
);
|
|
386
|
+
writeRelaunchState(statePath, {
|
|
387
|
+
lastAttemptTs: state.lastAttemptTs, attemptCount: state.attemptCount, lastBackoffLogTs: now,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
return { relaunched: false, reason: 'backoff', attemptCount: state.attemptCount, nextRetryInMs };
|
|
391
|
+
}
|
|
392
|
+
// Window elapsed: fall through to spawn (skips debounce, which is shorter).
|
|
393
|
+
} else if (state.lastAttemptTs !== null && (now - state.lastAttemptTs) < debounceMs) {
|
|
309
394
|
return { relaunched: false, reason: 'debounce', attemptCount: state.attemptCount };
|
|
310
395
|
}
|
|
311
396
|
|
|
312
397
|
const attemptCount = state.attemptCount + 1;
|
|
313
|
-
spawnFn({ logPath });
|
|
398
|
+
const spawned = spawnFn({ logPath, now });
|
|
399
|
+
const how = spawned && spawned.mode === 'systemd-run'
|
|
400
|
+
? `via systemd-run unit ${spawned.unit || `session-manager-relaunch-${now}`}${spawned.ok === false ? ' (failed)' : ''}`
|
|
401
|
+
: spawned && spawned.mode === 'detached-fallback' ? 'detached (systemd-run fallback)' : 'detached';
|
|
314
402
|
writeRelaunchState(statePath, { lastAttemptTs: now, attemptCount });
|
|
315
403
|
logRelaunchLine(
|
|
316
404
|
logPath,
|
|
317
|
-
`relaunch attempt ${attemptCount}
|
|
405
|
+
`relaunch attempt ${attemptCount}${attemptCount > maxAttempts ? ' (backoff retry)' : `/${maxAttempts}`}: spawning ${how} (heartbeat stale, last-known pid dead)`,
|
|
318
406
|
);
|
|
319
407
|
return { relaunched: true, reason: 'launched', attemptCount };
|
|
320
408
|
}
|
|
@@ -463,8 +551,11 @@ module.exports = {
|
|
|
463
551
|
releaseLock,
|
|
464
552
|
checkAppLiveness,
|
|
465
553
|
readRelaunchState,
|
|
554
|
+
resetRelaunchState,
|
|
466
555
|
writeRelaunchState,
|
|
467
556
|
defaultSpawnRelaunch,
|
|
557
|
+
relaunchInvocation,
|
|
558
|
+
findOnPath,
|
|
468
559
|
relaunchSpec,
|
|
469
560
|
maybeRelaunchApp,
|
|
470
561
|
DEFAULT_MAX_AGE_MS,
|
|
@@ -474,6 +565,7 @@ module.exports = {
|
|
|
474
565
|
DEFAULT_FINALIZE_BUDGET_MS,
|
|
475
566
|
DEFAULT_RELAUNCH_DEBOUNCE_MS,
|
|
476
567
|
DEFAULT_MAX_RELAUNCH_ATTEMPTS,
|
|
568
|
+
DEFAULT_CAPPED_RETRY_MS,
|
|
477
569
|
};
|
|
478
570
|
|
|
479
571
|
// Lazy default-path getters (SM_SCHEDULER_HOME resolved at read, never at require).
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mainHeapSnapshot.cjs — bounded, automatic V8 heap snapshot of the MAIN process.
|
|
3
|
+
* Captures at most once per process when heapUsed crosses a threshold, keeps at
|
|
4
|
+
* most 2 `main-heap-*.heapsnapshot` files, and refuses when disk is tight.
|
|
5
|
+
* Synchronous (v8.writeHeapSnapshot) — acceptable once per boot for diagnosis.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
|
|
13
|
+
const DEFAULT_THRESHOLD_MB = 1800;
|
|
14
|
+
const KEEP = 2;
|
|
15
|
+
const PREFIX = 'main-heap-';
|
|
16
|
+
const SUFFIX = '.heapsnapshot';
|
|
17
|
+
|
|
18
|
+
const defaultState = { captured: false };
|
|
19
|
+
|
|
20
|
+
function resolveThreshold(thresholdMb) {
|
|
21
|
+
if (Number.isFinite(thresholdMb) && thresholdMb > 0) return thresholdMb;
|
|
22
|
+
const env = Number(process.env.SM_MAIN_HEAP_SNAPSHOT_MB);
|
|
23
|
+
return Number.isFinite(env) && env > 0 ? env : DEFAULT_THRESHOLD_MB;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function pruneOld(dir, keep) {
|
|
27
|
+
let names = [];
|
|
28
|
+
try { names = fs.readdirSync(dir).filter((n) => n.startsWith(PREFIX) && n.endsWith(SUFFIX)); } catch { return; }
|
|
29
|
+
// ISO timestamps sort lexically; oldest first.
|
|
30
|
+
names.sort();
|
|
31
|
+
for (const n of names.slice(0, Math.max(0, names.length - keep))) {
|
|
32
|
+
try { fs.unlinkSync(path.join(dir, n)); } catch { /* best-effort */ }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {{ heapUsedMb:number, thresholdMb?:number, dir?:string, now?:Date,
|
|
38
|
+
* writeFn?:(p:string)=>any, statfsFn?:Function, state?:{captured:boolean} }} opts
|
|
39
|
+
* @returns {{ captured:boolean, filePath?:string, reason?:string }}
|
|
40
|
+
*/
|
|
41
|
+
function maybeCaptureMainHeap(opts = {}) {
|
|
42
|
+
const state = opts.state || defaultState;
|
|
43
|
+
if (process.env.SM_MAIN_HEAP_SNAPSHOT === '0') return { captured: false, reason: 'disabled' };
|
|
44
|
+
if (state.captured) return { captured: false, reason: 'already-captured' };
|
|
45
|
+
const heapUsedMb = Number(opts.heapUsedMb);
|
|
46
|
+
if (!Number.isFinite(heapUsedMb) || heapUsedMb < resolveThreshold(opts.thresholdMb)) {
|
|
47
|
+
return { captured: false, reason: 'below-threshold' };
|
|
48
|
+
}
|
|
49
|
+
const dir = opts.dir || require('../scheduler/schedulerPaths.cjs').heapSnapshotDir();
|
|
50
|
+
const writeFn = opts.writeFn || ((p) => require('node:v8').writeHeapSnapshot(p));
|
|
51
|
+
const statfsFn = opts.statfsFn || fs.statfsSync;
|
|
52
|
+
const now = opts.now instanceof Date ? opts.now : new Date();
|
|
53
|
+
try {
|
|
54
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
55
|
+
const st = statfsFn(dir);
|
|
56
|
+
const freeMb = (Number(st.bavail) * Number(st.bsize)) / 1048576;
|
|
57
|
+
if (!(freeMb >= 3 * heapUsedMb)) return { captured: false, reason: 'low-disk' };
|
|
58
|
+
// Leave room for the new file: keep KEEP-1 old ones, so KEEP remain after the write.
|
|
59
|
+
pruneOld(dir, KEEP - 1);
|
|
60
|
+
state.captured = true;
|
|
61
|
+
const filePath = path.join(dir, `${PREFIX}${now.toISOString().replace(/[:.]/g, '-')}${SUFFIX}`);
|
|
62
|
+
writeFn(filePath);
|
|
63
|
+
return { captured: true, filePath };
|
|
64
|
+
} catch (err) {
|
|
65
|
+
return { captured: false, reason: `error: ${err && err.message}` };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { maybeCaptureMainHeap, DEFAULT_THRESHOLD_MB };
|
|
@@ -24,9 +24,31 @@ function resolveDeps(deps = {}) {
|
|
|
24
24
|
telemetryClient: deps.telemetryClient || require('./telemetryClient.cjs'),
|
|
25
25
|
buildMachineProfile: deps.buildMachineProfile || require('../machineProfile.cjs').buildMachineProfile,
|
|
26
26
|
telemetryCounters: deps.telemetryCounters || require('./telemetryCounters.cjs'),
|
|
27
|
+
getSeedStatus: deps.getSeedStatus || (() => require('../../seedStatus.cjs').getSeedStatus()),
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Maps getSeedStatus() to enum-only strings (never the fix text or a path).
|
|
33
|
+
* Any failure or malformed shape yields null — never a throw.
|
|
34
|
+
*/
|
|
35
|
+
function buildSeeds(getSeedStatus) {
|
|
36
|
+
try {
|
|
37
|
+
const s = getSeedStatus();
|
|
38
|
+
const pick = (id) => {
|
|
39
|
+
const v = s && s[id] && s[id].status;
|
|
40
|
+
return typeof v === 'string' ? v : null;
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
devPlugin: pick('dev-plugin'),
|
|
44
|
+
schedulerMcp: pick('scheduler-mcp'),
|
|
45
|
+
agentPersonas: pick('agent-personas'),
|
|
46
|
+
};
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
30
52
|
/**
|
|
31
53
|
* bootSequence({ now, appVersion, installChannel, deps }) — call once per
|
|
32
54
|
* process from app.whenReady.
|
|
@@ -52,7 +74,7 @@ function resolveDeps(deps = {}) {
|
|
|
52
74
|
*/
|
|
53
75
|
async function bootSequence({ now = Date.now(), appVersion, installChannel, deps: depsOverride } = {}) {
|
|
54
76
|
const deps = resolveDeps(depsOverride);
|
|
55
|
-
const { telemetrySettings, telemetryClient, buildMachineProfile, telemetryCounters } = deps;
|
|
77
|
+
const { telemetrySettings, telemetryClient, buildMachineProfile, telemetryCounters, getSeedStatus } = deps;
|
|
56
78
|
|
|
57
79
|
const settings = await telemetrySettings.load();
|
|
58
80
|
const versionChanged = settings.lastMachineReportVersion !== appVersion;
|
|
@@ -63,7 +85,7 @@ async function bootSequence({ now = Date.now(), appVersion, installChannel, deps
|
|
|
63
85
|
}
|
|
64
86
|
|
|
65
87
|
if (telemetrySettings.isMachineReportDue(settings, { now, appVersion })) {
|
|
66
|
-
const profile = await buildMachineProfile();
|
|
88
|
+
const profile = { ...(await buildMachineProfile()), seeds: buildSeeds(getSeedStatus) };
|
|
67
89
|
const reportResult = await telemetryClient.reportInstall(profile);
|
|
68
90
|
if (reportResult && reportResult.accepted) {
|
|
69
91
|
await telemetrySettings.save({
|
|
@@ -76,4 +98,4 @@ async function bootSequence({ now = Date.now(), appVersion, installChannel, deps
|
|
|
76
98
|
telemetryCounters.trackAppLaunch({ installChannel, appVersion });
|
|
77
99
|
}
|
|
78
100
|
|
|
79
|
-
module.exports = { bootSequence };
|
|
101
|
+
module.exports = { bootSequence, buildSeeds };
|
|
@@ -580,9 +580,21 @@ function buildInstallWire(profile, installId) {
|
|
|
580
580
|
// not the module-singleton one) plus the current process's test-runner
|
|
581
581
|
// signal, same three-value contract as the other three channels.
|
|
582
582
|
env: machineProfile.resolveEnv({ isTestRunner: isTestEnvironment(), installChannel: p.installChannel }),
|
|
583
|
+
// First-boot seeder outcomes (enum strings only) or null — see telemetryBoot.buildSeeds.
|
|
584
|
+
seeds: sanitizeSeeds(p.seeds),
|
|
583
585
|
};
|
|
584
586
|
}
|
|
585
587
|
|
|
588
|
+
const SEED_ENUM = new Set(['done', 'pending', 'failed', 'exhausted']);
|
|
589
|
+
function sanitizeSeeds(seeds) {
|
|
590
|
+
if (!seeds || typeof seeds !== 'object') return null;
|
|
591
|
+
const out = {};
|
|
592
|
+
for (const k of ['devPlugin', 'schedulerMcp', 'agentPersonas']) {
|
|
593
|
+
out[k] = SEED_ENUM.has(seeds[k]) ? seeds[k] : null;
|
|
594
|
+
}
|
|
595
|
+
return out;
|
|
596
|
+
}
|
|
597
|
+
|
|
586
598
|
/**
|
|
587
599
|
* Reports the once-per-cadence install/liveness record. Unlike track/logLine/
|
|
588
600
|
* reportError this isn't arbitrary user content, so it skips redactDeep() —
|
|
@@ -29,8 +29,30 @@ function trackSessionOpen(deps) {
|
|
|
29
29
|
track('session.open', {}, deps);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
// Enum mappers — user-authored values never leave the machine; only the closed
|
|
33
|
+
// shipped sets do. Sets are required lazily so this module stays light.
|
|
34
|
+
function mapTag(tag) {
|
|
35
|
+
if (typeof tag !== 'string' || !tag) return null;
|
|
36
|
+
try {
|
|
37
|
+
const { EpicTagSchema } = require('../promptSessionSchema.cjs');
|
|
38
|
+
return EpicTagSchema.options.includes(tag) ? tag : 'other';
|
|
39
|
+
} catch { return 'other'; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function mapAgentType(agentType) {
|
|
43
|
+
if (typeof agentType !== 'string' || !agentType) return null;
|
|
44
|
+
try {
|
|
45
|
+
const { PERSONAS } = require('../../seedAgentPersonas.cjs');
|
|
46
|
+
return PERSONAS.includes(agentType) ? agentType : 'custom';
|
|
47
|
+
} catch { return 'custom'; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function trackEpicCreate({ tag, agentType } = {}, deps) {
|
|
51
|
+
track('epic.create', { tag: mapTag(tag), agentType: mapAgentType(agentType) }, deps);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function trackPrdCreate({ agentType } = {}, deps) {
|
|
55
|
+
track('prd.create', { agentType: mapAgentType(agentType) }, deps);
|
|
34
56
|
}
|
|
35
57
|
|
|
36
58
|
function trackSchedulerJobFinish({ status } = {}, deps) {
|
|
@@ -41,5 +63,6 @@ module.exports = {
|
|
|
41
63
|
trackAppLaunch,
|
|
42
64
|
trackSessionOpen,
|
|
43
65
|
trackEpicCreate,
|
|
66
|
+
trackPrdCreate,
|
|
44
67
|
trackSchedulerJobFinish,
|
|
45
68
|
};
|