claude-code-session-manager 0.85.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-DRH-Ty20.js → DataModel--mISIJ6h.js} +1 -1
- package/dist/assets/{History-CfRhT1Im.js → History-C2ahUXTg.js} +2 -2
- package/dist/assets/{Hooks-wEmh_U6c.js → Hooks-BiC6oyR2.js} +3 -3
- package/dist/assets/{HostBilko-D_t7Rbi7.js → HostBilko-BPleEOld.js} +1 -1
- package/dist/assets/{Library-CpArQ-OJ.js → Library-Dc8Qst1R.js} +1 -1
- package/dist/assets/{ListDetail-pjaKYs84.js → ListDetail-DIXh-OLX.js} +1 -1
- package/dist/assets/MarkdownEditor-C90bkLXK.js +1 -0
- package/dist/assets/{McpServers-ftqaV3kn.js → McpServers-DqcbLOLZ.js} +2 -2
- package/dist/assets/{Memory-ChMWkNd0.js → Memory-CW62MXlh.js} +4 -4
- package/dist/assets/{Panel-D9Kr40Ai.js → Panel-Bw1FhRuF.js} +1 -1
- package/dist/assets/Permissions-BcUC-5y8.js +3 -0
- package/dist/assets/{Plugins-BtChISho.js → Plugins-BnKx9flD.js} +2 -2
- package/dist/assets/{ProvenanceBadge-DBA5EcYy.js → ProvenanceBadge-Bw5vNVPT.js} +1 -1
- package/dist/assets/{SaveBar-I0_dWNTX.js → SaveBar-CWr0O_w-.js} +1 -1
- package/dist/assets/Scheduler-DYdLuUqq.js +14 -0
- package/dist/assets/{ScopeSwitcher-5GTEveb2.js → ScopeSwitcher-CrBLbg8s.js} +1 -1
- package/dist/assets/Settings-DluB-vN1.js +3 -0
- package/dist/assets/{SkillReferenceGraph-DNBFGrYE.js → SkillReferenceGraph-CHLSseay.js} +1 -1
- package/dist/assets/{Skills-DJB6-bBM.js → Skills-gNdo_HNK.js} +2 -2
- package/dist/assets/{SystemPrompt-BiDDrJUA.js → SystemPrompt-Cru05-Ia.js} +1 -1
- package/dist/assets/{TagLibrary-_Wrevtop.js → TagLibrary-DNHY0xou.js} +1 -1
- package/dist/assets/{TiptapBody-OWWXdLRy.js → TiptapBody-I4lmbCgP.js} +1 -1
- package/dist/assets/{Toggle-B122N0HL.js → Toggle-bWMHjmRh.js} +1 -1
- package/dist/assets/{index-CDo9xBR9.css → index-DV3PorRY.css} +1 -1
- package/dist/assets/{index-DhvuQL4C.js → index-fc_JjdxL.js} +724 -724
- package/dist/assets/settingsSchema-BfhtZnGD.js +3 -0
- package/dist/index.html +2 -2
- package/package.json +15 -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__/health-starve-escalation.test.cjs +94 -0
- package/src/main/__tests__/loadGateDetailTick.test.cjs +31 -0
- package/src/main/__tests__/machineProfile.test.cjs +19 -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__/pty-session-open-telemetry.test.cjs +96 -0
- package/src/main/__tests__/queue-health-verdict.test.cjs +170 -0
- package/src/main/__tests__/queue-starvation-per-project.test.cjs +147 -0
- 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-failed-autoreset.test.cjs +121 -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-needs-review-autoresolve.test.cjs +189 -0
- package/src/main/__tests__/scheduler-no-dead-end-status.test.cjs +152 -0
- package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +164 -0
- package/src/main/__tests__/scheduler-quarantine-autoresolve.test.cjs +165 -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 +154 -0
- package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +14 -0
- package/src/main/__tests__/telemetryClient.test.cjs +75 -5
- package/src/main/__tests__/telemetrySettings.test.cjs +32 -0
- package/src/main/chatRunner.cjs +8 -5
- package/src/main/health.cjs +76 -3
- 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__/loadGate.test.cjs +103 -2
- 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__/telemetryBoot.test.cjs +11 -0
- 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/loadGate.cjs +23 -1
- package/src/main/lib/machineProfile.cjs +15 -0
- 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/schedulerBatch.cjs +12 -1
- package/src/main/lib/schedulerConfig.cjs +37 -0
- package/src/main/lib/telemetryBoot.cjs +11 -8
- package/src/main/lib/telemetryClient.cjs +44 -2
- package/src/main/lib/telemetrySettings.cjs +20 -3
- package/src/main/lib/usageCircuit.cjs +159 -0
- package/{scripts → src/main}/lib/watchdogHelpers.cjs +1 -1
- package/src/main/pty.cjs +9 -0
- package/src/main/scheduler/prdParser.cjs +13 -0
- package/src/main/scheduler.cjs +1869 -193
- 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-Bkv-HcP1.js +0 -3
- package/dist/assets/MarkdownEditor-Xc141kjj.js +0 -1
- package/dist/assets/Permissions-DKoNVgzj.js +0 -3
- package/dist/assets/Scheduler-CbES7MC8.js +0 -14
- package/dist/assets/Settings-BX3FElXk.js +0 -3
- package/dist/assets/settingsSchema-sGoCTd7J.js +0 -3
- /package/{scripts/project-pages-logic → web/project-pages/logic}/dist/logic.cjs +0 -0
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
|
|
33
33
|
const { appendAuditEvent } = require('./auditLog.cjs');
|
|
34
34
|
const telemetryCounters = require('./telemetryCounters.cjs');
|
|
35
|
+
const queueHistory = require('./queueHistory.cjs');
|
|
36
|
+
const { buildNeedsReviewEntryLine, buildNeedsReviewResolutionLine } = require('./needsReviewLedger.cjs');
|
|
35
37
|
|
|
36
38
|
// A run genuinely finished when it leaves 'running' for one of these —
|
|
37
39
|
// distinct from every other legal edge in LEGAL_TRANSITIONS (retries,
|
|
@@ -195,6 +197,37 @@ function transitionJob(job, toStatus, { reason, source, allowAnyFrom = false } =
|
|
|
195
197
|
telemetryCounters.trackSchedulerJobFinish({ status: toStatus });
|
|
196
198
|
}
|
|
197
199
|
|
|
200
|
+
// needs_review ledger (PRD: needs_review durability). This IS the single
|
|
201
|
+
// funnel every needs_review transition — entry or resolution — passes
|
|
202
|
+
// through (see this file's own header comment: transitionJob is the ONE
|
|
203
|
+
// place `status` is assigned), so hooking here, rather than any one of
|
|
204
|
+
// needs_review's several distinct entry call sites in scheduler.cjs,
|
|
205
|
+
// guarantees no path can bypass it.
|
|
206
|
+
//
|
|
207
|
+
// Fire-and-forget, exactly like every other best-effort side-channel write
|
|
208
|
+
// in this codebase (rcaReport.cjs, auditLog.cjs's own posture) — a ledger
|
|
209
|
+
// write failure must never fail or delay the transition it's recording.
|
|
210
|
+
if (toStatus === 'needs_review' && from !== 'needs_review') {
|
|
211
|
+
// ENTRY: stamp the episode's runId/enteredAt on the job itself so the
|
|
212
|
+
// RESOLUTION line (below) can reference the SAME runId even if the job's
|
|
213
|
+
// own `runId` is reassigned in between (e.g. a resume-recovery
|
|
214
|
+
// re-dispatch mints a fresh one before this episode resolves).
|
|
215
|
+
job.needsReviewEntryRunId = job.runId ?? null;
|
|
216
|
+
job.needsReviewEnteredAt = entry.at;
|
|
217
|
+
queueHistory.appendHistory([buildNeedsReviewEntryLine(job, entry)]).catch((e) => {
|
|
218
|
+
console.error('[scheduleJobTransitions] failed to append needs_review entry', e?.message ?? String(e));
|
|
219
|
+
});
|
|
220
|
+
} else if (from === 'needs_review' && toStatus !== 'needs_review') {
|
|
221
|
+
// RESOLUTION: read the episode stamped at entry, then clear it — the
|
|
222
|
+
// episode is over regardless of what this resolution line looks like.
|
|
223
|
+
const episode = { runId: job.needsReviewEntryRunId ?? null, enteredAt: job.needsReviewEnteredAt ?? null };
|
|
224
|
+
delete job.needsReviewEntryRunId;
|
|
225
|
+
delete job.needsReviewEnteredAt;
|
|
226
|
+
queueHistory.appendHistory([buildNeedsReviewResolutionLine(job, entry, episode)]).catch((e) => {
|
|
227
|
+
console.error('[scheduleJobTransitions] failed to append needs_review resolution', e?.message ?? String(e));
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
198
231
|
return true;
|
|
199
232
|
}
|
|
200
233
|
|
|
@@ -102,9 +102,20 @@ function findBlockingDep(job, projectJobs, satisfiedSlugs = new Set()) {
|
|
|
102
102
|
}
|
|
103
103
|
return satisfiedBareSlugs.has(bareSlug(slug));
|
|
104
104
|
};
|
|
105
|
+
// A dep row is blocking unless it's 'completed', OR it's a 'skipped' row
|
|
106
|
+
// stamped `needsReviewAutoResolvedSkip` — the bounded needs_review
|
|
107
|
+
// auto-resolve terminal decision (scheduler.cjs's
|
|
108
|
+
// applyNeedsReviewAutoResolve). That skip is deliberately NOT the
|
|
109
|
+
// "PRD source vanished, a human must author a fresh PRD" skip the
|
|
110
|
+
// heldBySkippedDep messaging above describes — it already got every
|
|
111
|
+
// bounded chance to resolve itself, so treating it as a permanent
|
|
112
|
+
// dependsOn block would defeat the whole point of that auto-resolve pass
|
|
113
|
+
// (a chain that never drains without an operator).
|
|
114
|
+
const isSatisfiedRow = (dep) => dep.status === 'completed'
|
|
115
|
+
|| (dep.status === 'skipped' && dep.needsReviewAutoResolvedSkip === true);
|
|
105
116
|
return (job.dependsOn ?? []).find((slug) => {
|
|
106
117
|
const rows = rowsForDep(slug);
|
|
107
|
-
if (rows.length > 0) return rows.some((dep) => dep
|
|
118
|
+
if (rows.length > 0) return rows.some((dep) => !isSatisfiedRow(dep));
|
|
108
119
|
return !isKnownSatisfied(slug);
|
|
109
120
|
});
|
|
110
121
|
}
|
|
@@ -56,6 +56,16 @@ module.exports = {
|
|
|
56
56
|
// (pause/resume, job start/finish/reap/reset) bypass this via
|
|
57
57
|
// broadcast({ flush: true }).
|
|
58
58
|
BROADCAST_COALESCE_MS: 200,
|
|
59
|
+
// reconcile() pass duration above which a single warn-level breakdown is
|
|
60
|
+
// logged (per-phase ms, PRD file count, resolved dir count) — the only
|
|
61
|
+
// evidence trail for a slow pass otherwise requires hand-timing the scan
|
|
62
|
+
// from a throwaway script. Baseline measured 2026-09-13 on a machine with
|
|
63
|
+
// ~160 PRD dirs: allProjectCwds() ~270ms x5/pass, resolvePrdsDirs()
|
|
64
|
+
// ~430ms, historyTerminalBySlug() ~418ms over 784 entries — a healthy pass
|
|
65
|
+
// today is comfortably under 2s; this should trip well before a
|
|
66
|
+
// regression back toward those numbers compounds into the reported
|
|
67
|
+
// "schedule.state timed out after 5000ms" toast.
|
|
68
|
+
RECONCILE_SLOW_PASS_MS: 2_000,
|
|
59
69
|
// Terminal (completed/failed) jobs older than this move from queue.json's
|
|
60
70
|
// hot jobs[] into the append-only history.jsonl sidecar. See
|
|
61
71
|
// src/main/lib/queueHistory.cjs.
|
|
@@ -81,6 +91,19 @@ module.exports = {
|
|
|
81
91
|
// 1086). Escalation only — warn + audit, never dispatch.
|
|
82
92
|
STARVATION_ESCALATE_MS: 45 * 60_000,
|
|
83
93
|
|
|
94
|
+
// A project still meeting findStarvedProjects' STARVED verdict past THIS
|
|
95
|
+
// (later) threshold gets a bounded, automated consequence beyond the
|
|
96
|
+
// repeating project_starved WARN above: a one-shot 'project_starve_escalated'
|
|
97
|
+
// audit event + a user-facing toast naming the project and the last tick's
|
|
98
|
+
// hold reason (scheduler.cjs's `lastTick`). Escalation only — never
|
|
99
|
+
// auto-resets, auto-cancels, or bypasses a gate; the queue's own state is
|
|
100
|
+
// untouched. The 2026-09-12 audit log showed /home/bilko/Projects/Bilko
|
|
101
|
+
// emit project_starved for 19h straight (ageMs 60.5M → 68.9M) with zero
|
|
102
|
+
// consequence and no toast — this is the fix. Latched per-cwd so the SAME
|
|
103
|
+
// starve stretch escalates exactly once; the latch resets the moment that
|
|
104
|
+
// cwd stops appearing in findStarvedProjects.
|
|
105
|
+
STARVE_ESCALATION_MS: 60 * 60_000,
|
|
106
|
+
|
|
84
107
|
// A RUNNING job that has overrun its own PRD's `estimateMinutes` by this
|
|
85
108
|
// factor is escalated. Distinct from MAX_JOB_DURATION_MS (4h), which is a
|
|
86
109
|
// deadman kill: a 20-minute PRD still running at 3h is 9x over estimate but
|
|
@@ -96,6 +119,20 @@ module.exports = {
|
|
|
96
119
|
// SM_JOB_OVERRUN_FLOOR_MINUTES.
|
|
97
120
|
JOB_OVERRUN_FLOOR_MS: 45 * 60_000,
|
|
98
121
|
|
|
122
|
+
// Wall-clock KILL budget (distinct from JOB_OVERRUN_FACTOR/JOB_OVERRUN_FLOOR_MS
|
|
123
|
+
// above, which only ever ESCALATE — log + audit, deliberately never kill).
|
|
124
|
+
// This one ACTS: a RUNNING job past `budgetMs = clamp(estimateMinutes *
|
|
125
|
+
// JOB_BUDGET_FACTOR, JOB_BUDGET_FLOOR_MS, JOB_BUDGET_CEILING_MS)` is
|
|
126
|
+
// SIGTERMed and parked needs_review. Calibrated against 606 run-meta
|
|
127
|
+
// records since 2026-08-25: p50=13m, p90=60m, max=240m, against a
|
|
128
|
+
// concurrencyCap of 4 — two 4h outliers hold half the machine's dispatch
|
|
129
|
+
// capacity for an afternoon with nothing noticing or acting on it. Override
|
|
130
|
+
// with SM_JOB_BUDGET_FACTOR / SM_JOB_BUDGET_FLOOR_MINUTES /
|
|
131
|
+
// SM_JOB_BUDGET_CEILING_MINUTES.
|
|
132
|
+
JOB_BUDGET_FACTOR: 3,
|
|
133
|
+
JOB_BUDGET_FLOOR_MS: 45 * 60_000,
|
|
134
|
+
JOB_BUDGET_CEILING_MS: 180 * 60_000,
|
|
135
|
+
|
|
99
136
|
// A 'running' row with no runtime.pid recorded (spawnJob's status:running
|
|
100
137
|
// mutate at scheduler.cjs:~3524 landed, but the pid-bearing runtime={}
|
|
101
138
|
// mutate that follows executeJob's spawn callback never did — the spawn
|
|
@@ -38,8 +38,10 @@ function resolveDeps(deps = {}) {
|
|
|
38
38
|
* signal that also gates the machine-profile heartbeat below).
|
|
39
39
|
* 3. install upsert — sent once via telemetryClient.reportInstall(profile)
|
|
40
40
|
* when telemetrySettings.isMachineReportDue() is true (a version bump, OR
|
|
41
|
-
* the 30-day liveness heartbeat)
|
|
42
|
-
* persisted
|
|
41
|
+
* the 30-day liveness heartbeat). lastMachineReportAt/Version are only
|
|
42
|
+
* persisted when reportInstall() reports success — a failed/dropped
|
|
43
|
+
* upsert stays due so the next boot retries, rather than being marked
|
|
44
|
+
* done regardless of outcome.
|
|
43
45
|
* This lands in bilko.run's app_installs table (the sole source for
|
|
44
46
|
* every install-shaped number on the site). It replaces the former
|
|
45
47
|
* track('install.machine', profile) call, which duplicated the same
|
|
@@ -62,12 +64,13 @@ async function bootSequence({ now = Date.now(), appVersion, installChannel, deps
|
|
|
62
64
|
|
|
63
65
|
if (telemetrySettings.isMachineReportDue(settings, { now, appVersion })) {
|
|
64
66
|
const profile = await buildMachineProfile();
|
|
65
|
-
await telemetryClient.reportInstall(profile);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
const reportResult = await telemetryClient.reportInstall(profile);
|
|
68
|
+
if (reportResult && reportResult.accepted) {
|
|
69
|
+
await telemetrySettings.save({
|
|
70
|
+
lastMachineReportAt: new Date(now).toISOString(),
|
|
71
|
+
lastMachineReportVersion: appVersion,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
telemetryCounters.trackAppLaunch({ installChannel, appVersion });
|
|
@@ -91,8 +91,24 @@ const S = {
|
|
|
91
91
|
lastError: null,
|
|
92
92
|
lastFlushAt: null,
|
|
93
93
|
lastFlushReason: null,
|
|
94
|
+
logger: null,
|
|
94
95
|
};
|
|
95
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Best-effort warn logger for failures this module would otherwise swallow
|
|
99
|
+
* (e.g. reportInstall()'s bare catch). Lazily requires logs.cjs — which
|
|
100
|
+
* requires('electron') at module scope — so this stays a no-op under plain
|
|
101
|
+
* vitest with no Electron runtime, exactly like machineProfile.cjs's
|
|
102
|
+
* resolveElectronApp(). Injectable via _setLogger so tests can observe it
|
|
103
|
+
* without an Electron runtime.
|
|
104
|
+
*/
|
|
105
|
+
function logWarn(message, meta) {
|
|
106
|
+
try {
|
|
107
|
+
const fn = S.logger || require('../logs.cjs').writeLine;
|
|
108
|
+
fn({ scope: 'telemetry', level: 'warn', message, meta });
|
|
109
|
+
} catch { /* logs unavailable outside an Electron runtime */ }
|
|
110
|
+
}
|
|
111
|
+
|
|
96
112
|
/**
|
|
97
113
|
* SM_TELEMETRY_SPOOL, when set, overrides the spool directory in place of
|
|
98
114
|
* `~/.config/session-manager` — the explicit opt-in a test that genuinely
|
|
@@ -130,6 +146,18 @@ function getBeaconToken() {
|
|
|
130
146
|
return process.env.SM_BEACON_KEY || FALLBACK_BEACON_KEY;
|
|
131
147
|
}
|
|
132
148
|
|
|
149
|
+
/**
|
|
150
|
+
* The wire-level `env` discriminator for the currently-running process,
|
|
151
|
+
* resolved from real signals (never from an appVersion string — see
|
|
152
|
+
* machineProfile.cjs's resolveEnv header comment for why that matters).
|
|
153
|
+
*/
|
|
154
|
+
function currentEnv() {
|
|
155
|
+
return machineProfile.resolveEnv({
|
|
156
|
+
isTestRunner: isTestEnvironment(),
|
|
157
|
+
installChannel: S.profile && S.profile.installChannel,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
133
161
|
// ─── safe coercion helpers (never throw, regardless of input shape) ───────
|
|
134
162
|
|
|
135
163
|
function safeObj(v) {
|
|
@@ -436,6 +464,7 @@ async function track(name, props) {
|
|
|
436
464
|
props: meta,
|
|
437
465
|
session_id: S.sessionId,
|
|
438
466
|
visitor_id: S.settings.installId,
|
|
467
|
+
env: currentEnv(),
|
|
439
468
|
};
|
|
440
469
|
return await appendRecord('event', wire, recordId);
|
|
441
470
|
} catch {
|
|
@@ -465,6 +494,7 @@ async function logLine(opts) {
|
|
|
465
494
|
session_id: S.sessionId,
|
|
466
495
|
fields: meta,
|
|
467
496
|
ts: Date.now(),
|
|
497
|
+
env: currentEnv(),
|
|
468
498
|
};
|
|
469
499
|
return await appendRecord('log', wire, recordId);
|
|
470
500
|
} catch {
|
|
@@ -501,6 +531,7 @@ async function reportError(opts) {
|
|
|
501
531
|
session_id: S.sessionId,
|
|
502
532
|
context: meta,
|
|
503
533
|
ts: Date.now(),
|
|
534
|
+
env: currentEnv(),
|
|
504
535
|
};
|
|
505
536
|
return await appendRecord('error', wire, recordId);
|
|
506
537
|
} catch {
|
|
@@ -527,6 +558,11 @@ function buildInstallWire(profile, installId) {
|
|
|
527
558
|
// the numeric UTC offset (see machineProfile.cjs) — so that is what maps
|
|
528
559
|
// onto the server's `timezone` column.
|
|
529
560
|
timezone: safeStr(p.timezoneOffsetMinutes, 20),
|
|
561
|
+
// Resolved from this passed-in profile's own installChannel (never
|
|
562
|
+
// S.profile — reportInstall() is always given a freshly-built profile,
|
|
563
|
+
// not the module-singleton one) plus the current process's test-runner
|
|
564
|
+
// signal, same three-value contract as the other three channels.
|
|
565
|
+
env: machineProfile.resolveEnv({ isTestRunner: isTestEnvironment(), installChannel: p.installChannel }),
|
|
530
566
|
};
|
|
531
567
|
}
|
|
532
568
|
|
|
@@ -544,7 +580,8 @@ async function reportInstall(profile) {
|
|
|
544
580
|
const recordId = crypto.randomUUID();
|
|
545
581
|
const wire = buildInstallWire(profile, S.settings.installId);
|
|
546
582
|
return await appendRecord('install', wire, recordId);
|
|
547
|
-
} catch {
|
|
583
|
+
} catch (e) {
|
|
584
|
+
logWarn('reportInstall failed', { error: e && e.message ? e.message : String(e) });
|
|
548
585
|
return { accepted: false, reason: 'error' };
|
|
549
586
|
}
|
|
550
587
|
}
|
|
@@ -684,7 +721,7 @@ async function flushImpl(reason) {
|
|
|
684
721
|
await persistSent();
|
|
685
722
|
|
|
686
723
|
if (safeReason === 'daily' && result.failed.length === 0) {
|
|
687
|
-
S.settings = await telemetrySettings.save({
|
|
724
|
+
S.settings = await telemetrySettings.save({ lastDailyFlushAt: new Date().toISOString() });
|
|
688
725
|
}
|
|
689
726
|
} catch { /* fail-inert */ }
|
|
690
727
|
S.lastFlushAt = Date.now();
|
|
@@ -749,6 +786,10 @@ function _setMachineProfileBuilder(fn) {
|
|
|
749
786
|
S.profileBuilder = typeof fn === 'function' ? fn : machineProfile.buildMachineProfile;
|
|
750
787
|
}
|
|
751
788
|
|
|
789
|
+
function _setLogger(fn) {
|
|
790
|
+
S.logger = typeof fn === 'function' ? fn : null;
|
|
791
|
+
}
|
|
792
|
+
|
|
752
793
|
module.exports = {
|
|
753
794
|
track,
|
|
754
795
|
logLine,
|
|
@@ -764,4 +805,5 @@ module.exports = {
|
|
|
764
805
|
sentPath,
|
|
765
806
|
_setFetchImpl,
|
|
766
807
|
_setMachineProfileBuilder,
|
|
808
|
+
_setLogger,
|
|
767
809
|
};
|
|
@@ -78,11 +78,28 @@ function normalize(cfg) {
|
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Persists `patch` merged onto a FRESHLY-READ copy of the on-disk config —
|
|
83
|
+
* never onto a caller-held snapshot. Two independent modules (telemetryBoot's
|
|
84
|
+
* install-report stamp, telemetryClient's own daily-flush stamp) each keep
|
|
85
|
+
* their own in-memory copy of settings loaded at different times; if save()
|
|
86
|
+
* blindly wrote a caller's full snapshot, whichever call landed last would
|
|
87
|
+
* silently revert every field the OTHER caller had just written (a lost
|
|
88
|
+
* update — the root cause behind lastMachineReportAt never sticking). Calls
|
|
89
|
+
* are still serialized through the existing writeQueue chain, so the fresh
|
|
90
|
+
* read for call N+1 always observes call N's write.
|
|
91
|
+
*/
|
|
81
92
|
let writeQueue = Promise.resolve();
|
|
82
|
-
async function save(
|
|
83
|
-
if (!
|
|
84
|
-
const
|
|
93
|
+
async function save(patch) {
|
|
94
|
+
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) throw new Error('Invalid telemetry config');
|
|
95
|
+
for (const k of Object.keys(patch)) {
|
|
96
|
+
if (!KNOWN_KEYS.has(k)) throw new Error('Invalid telemetry config');
|
|
97
|
+
}
|
|
85
98
|
const run = async () => {
|
|
99
|
+
const current = await readRaw();
|
|
100
|
+
const merged = { ...current, ...patch };
|
|
101
|
+
if (!isValid(merged)) throw new Error('Invalid telemetry config');
|
|
102
|
+
const next = normalize(merged);
|
|
86
103
|
await config.writeTextAtomic(storePath(), JSON.stringify(next, null, 2) + '\n', { mode: 0o600 });
|
|
87
104
|
return next;
|
|
88
105
|
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* usageCircuit.cjs — a breaker over the /api/oauth/usage meter (PRD ss-02).
|
|
5
|
+
* Degraded mode is explicit, never a silent 0: repeated failures open the
|
|
6
|
+
* circuit with jittered backoff; degradedBudget() carries forward the last
|
|
7
|
+
* known-good BINDING window's utilization (never 0), pinning to 100 on a
|
|
8
|
+
* fresh executor-observed 429 and capping concurrency at 2. Pure Node.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const FLOOR_MS = 30_000;
|
|
12
|
+
const CAP_MS = 8 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
/** Doubles from FLOOR_MS, capped at CAP_MS. 0/falsy prevMs starts at the floor. */
|
|
15
|
+
function doubledBase(prevMs) {
|
|
16
|
+
return prevMs > 0 ? Math.min(prevMs * 2, CAP_MS) : FLOOR_MS;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Next backoff interval: doubles `prevMs` (floor FLOOR_MS, cap CAP_MS), then
|
|
21
|
+
* jitters by 0.5 + rand()*0.5 so concurrent instances don't retry in lockstep.
|
|
22
|
+
*/
|
|
23
|
+
function backoffWithJitter(prevMs) {
|
|
24
|
+
const base = doubledBase(prevMs);
|
|
25
|
+
return base * (0.5 + Math.random() * 0.5);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A cached reset timestamp is fresh only if it's strictly in the future
|
|
30
|
+
* relative to `now` — null/unparseable/at-or-before-now all read as stale,
|
|
31
|
+
* since a passed reset no longer describes a live window.
|
|
32
|
+
*/
|
|
33
|
+
function isResetFresh(iso, now) {
|
|
34
|
+
if (!iso) return false;
|
|
35
|
+
const resetMs = Date.parse(iso);
|
|
36
|
+
if (!Number.isFinite(resetMs)) return false;
|
|
37
|
+
const nowMs = now instanceof Date ? now.getTime() : Number(now);
|
|
38
|
+
if (!Number.isFinite(nowMs)) return false;
|
|
39
|
+
return resetMs > nowMs;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Which window is actually binding dispatch. Prefers `payload.limits[]`'s
|
|
44
|
+
* `is_active` entry (the richer per-window shape); falls back to the flat
|
|
45
|
+
* `five_hour` field when `limits[]` is absent, matching usage.cjs's shape.
|
|
46
|
+
*/
|
|
47
|
+
function bindingWindow(payload) {
|
|
48
|
+
if (payload && Array.isArray(payload.limits) && payload.limits.length) {
|
|
49
|
+
const active = payload.limits.find((l) => l && l.is_active);
|
|
50
|
+
if (active) {
|
|
51
|
+
return {
|
|
52
|
+
name: active.type || active.name || 'unknown',
|
|
53
|
+
utilization: active.utilization,
|
|
54
|
+
resets_at: active.resets_at ?? null,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const fh = payload && payload.five_hour;
|
|
59
|
+
return {
|
|
60
|
+
name: 'five_hour',
|
|
61
|
+
utilization: fh ? fh.utilization : undefined,
|
|
62
|
+
resets_at: fh ? fh.resets_at : null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Conservative budget to run on while the meter is down. utilization is
|
|
68
|
+
* carried forward from the last known-good BINDING window — never 0, which
|
|
69
|
+
* would read as "plenty of headroom" instead of "we don't know." A fresh
|
|
70
|
+
* executor-observed 429 (its own window not yet passed) pins utilization to
|
|
71
|
+
* 100 regardless of the stale cached value. concurrencyCap never exceeds 2.
|
|
72
|
+
*/
|
|
73
|
+
function degradedBudget(lastGoodPayload, executorEvidence = {}) {
|
|
74
|
+
const window = bindingWindow(lastGoodPayload);
|
|
75
|
+
let utilization = Number.isFinite(window.utilization) && window.utilization > 0
|
|
76
|
+
? window.utilization
|
|
77
|
+
: 100;
|
|
78
|
+
|
|
79
|
+
const configuredCap = Number.isFinite(executorEvidence.configuredCap)
|
|
80
|
+
? executorEvidence.configuredCap
|
|
81
|
+
: 2;
|
|
82
|
+
const concurrencyCap = Math.min(configuredCap, 2);
|
|
83
|
+
|
|
84
|
+
if (executorEvidence.observed429 && isResetFresh(executorEvidence.resetsAt, executorEvidence.now)) {
|
|
85
|
+
utilization = 100;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { utilization, concurrencyCap };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Wraps `fn` so concurrent callers awaiting an in-flight call share one promise. */
|
|
92
|
+
function singleFlight(fn) {
|
|
93
|
+
let inFlight = null;
|
|
94
|
+
return function singleFlightWrapped(...args) {
|
|
95
|
+
if (inFlight) return inFlight;
|
|
96
|
+
inFlight = Promise.resolve(fn(...args)).finally(() => { inFlight = null; });
|
|
97
|
+
return inFlight;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Breaker state machine over the meter: closed -> open after 3 consecutive
|
|
103
|
+
* failures; open allows exactly one probe per backoff interval (half_open);
|
|
104
|
+
* a successful probe closes the circuit and resets backoff, a failed probe
|
|
105
|
+
* reopens it with backoff doubled.
|
|
106
|
+
*/
|
|
107
|
+
function createUsageCircuit({ now = () => Date.now() } = {}) {
|
|
108
|
+
let consecutiveFailures = 0;
|
|
109
|
+
let currentState = 'closed';
|
|
110
|
+
let backoffBaseMs = 0;
|
|
111
|
+
let nextProbeAtMs = 0;
|
|
112
|
+
let probeInFlight = false;
|
|
113
|
+
|
|
114
|
+
function scheduleNextProbe() {
|
|
115
|
+
const intervalMs = backoffWithJitter(backoffBaseMs);
|
|
116
|
+
backoffBaseMs = doubledBase(backoffBaseMs);
|
|
117
|
+
nextProbeAtMs = now() + intervalMs;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function state() {
|
|
121
|
+
if (currentState === 'open' && !probeInFlight && now() >= nextProbeAtMs) {
|
|
122
|
+
currentState = 'half_open';
|
|
123
|
+
probeInFlight = true;
|
|
124
|
+
}
|
|
125
|
+
return currentState;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function recordFailure(kind) { // eslint-disable-line no-unused-vars
|
|
129
|
+
consecutiveFailures += 1;
|
|
130
|
+
if (currentState === 'half_open') {
|
|
131
|
+
probeInFlight = false;
|
|
132
|
+
currentState = 'open';
|
|
133
|
+
scheduleNextProbe();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (currentState === 'closed' && consecutiveFailures >= 3) {
|
|
137
|
+
currentState = 'open';
|
|
138
|
+
scheduleNextProbe();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function recordSuccess(payload) { // eslint-disable-line no-unused-vars
|
|
143
|
+
consecutiveFailures = 0;
|
|
144
|
+
probeInFlight = false;
|
|
145
|
+
backoffBaseMs = 0;
|
|
146
|
+
currentState = 'closed';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { recordSuccess, recordFailure, state };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = {
|
|
153
|
+
createUsageCircuit,
|
|
154
|
+
backoffWithJitter,
|
|
155
|
+
isResetFresh,
|
|
156
|
+
bindingWindow,
|
|
157
|
+
degradedBudget,
|
|
158
|
+
singleFlight,
|
|
159
|
+
};
|
|
@@ -362,7 +362,7 @@ function releaseLock(lockPath) {
|
|
|
362
362
|
* dependency fails inside maybeFinalizeHistory's try/catch, not at require
|
|
363
363
|
* time for the whole watchdog script. */
|
|
364
364
|
function defaultFinalizeClosedDays(opts) {
|
|
365
|
-
const { finalizeClosedDays } = require('
|
|
365
|
+
const { finalizeClosedDays } = require('../historyAggregator.cjs');
|
|
366
366
|
return finalizeClosedDays(opts);
|
|
367
367
|
}
|
|
368
368
|
|
package/src/main/pty.cjs
CHANGED
|
@@ -109,6 +109,15 @@ class PtyManager {
|
|
|
109
109
|
// renderer will re-register its data/exit listeners on the same IPC
|
|
110
110
|
// channels. The data stream is live; pre-reattach output is lost, which
|
|
111
111
|
// is acceptable for a dev reload.
|
|
112
|
+
//
|
|
113
|
+
// Telemetry: this branch deliberately does NOT call
|
|
114
|
+
// telemetryCounters.trackSessionOpen() (see below, after the real spawn).
|
|
115
|
+
// Tab = claudeSessionId is a 1:1 mapping (CLAUDE.md domain model) — a
|
|
116
|
+
// reattach is the SAME session process still running, not a new one, so
|
|
117
|
+
// counting it here would double-count every renderer reload and every
|
|
118
|
+
// switch back to an already-open Epic's Terminal pane within the same
|
|
119
|
+
// Electron process. `session.open` counts fresh PTY spawns only; see
|
|
120
|
+
// telemetry.md for the full rationale and its trade-off.
|
|
112
121
|
const existing = this.sessions.get(tabId);
|
|
113
122
|
if (existing) {
|
|
114
123
|
console.log('[pty] reattach to existing session tabId=', tabId, 'pid=', existing.proc.pid);
|
|
@@ -119,10 +119,23 @@ async function parsePrdRaw(filePath) {
|
|
|
119
119
|
// dependent follow-up PRD (scheduler-launch-as-persona) resolves this
|
|
120
120
|
// into a launch decision, which this module does not do.
|
|
121
121
|
agentType: fm.agentType || null,
|
|
122
|
+
// Wave-authoring decision (scheduler wave-disposition PRD): 'append' or
|
|
123
|
+
// 'new-head', set only when createPrd() actually had a disposition to
|
|
124
|
+
// record — see prdCreate.cjs/prdDisposition.cjs. Absent on every PRD
|
|
125
|
+
// authored before this field existed, and on any PRD that never needed
|
|
126
|
+
// the decision (first-ever PRD in its Epic, or one with its own explicit
|
|
127
|
+
// dependsOn).
|
|
128
|
+
disposition: (fm.disposition === 'append' || fm.disposition === 'new-head') ? fm.disposition : null,
|
|
122
129
|
// Opt-in exclusive-lease flag (PRD 1107, quietMachineLease.cjs +
|
|
123
130
|
// schedulerBatch.cjs's pickNextBatch). Only a literal `true` opts in —
|
|
124
131
|
// matches prdFrontmatter.cjs's applyKey semantics for this field.
|
|
125
132
|
quietMachine: fm.quietMachine === 'true',
|
|
133
|
+
// Explicit opt-out of the wall-clock budget kill (scheduler.cjs's budget
|
|
134
|
+
// watchdog) — only a literal `true` opts out, matching quietMachine's
|
|
135
|
+
// parse semantics above. quietMachine jobs are exempt regardless of this
|
|
136
|
+
// field; this is for a job that isn't quiet-machine-eligible but still
|
|
137
|
+
// needs no wall-clock kill ceiling.
|
|
138
|
+
budgetExempt: fm.budgetExempt === 'true',
|
|
126
139
|
body: body.trim(),
|
|
127
140
|
};
|
|
128
141
|
}
|