salidium 0.4.0 → 0.5.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/bundle/native/SalidiumMenuBar.swift +686 -98
- package/bundle/salidium.mjs +384 -38
- package/bundle/ui/assets/{index-DdBwm8Cm.css → index-BR7P08_S.css} +1 -1
- package/bundle/ui/assets/index-DIfdilCQ.js +78 -0
- package/bundle/ui/index.html +2 -2
- package/package.json +1 -1
- package/bundle/ui/assets/index-U_KufSwb.js +0 -78
package/bundle/salidium.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
existsSync as existsSync21,
|
|
14
14
|
mkdirSync as mkdirSync12,
|
|
15
15
|
openSync as openSync7,
|
|
16
|
+
readFileSync as readFileSync13,
|
|
16
17
|
statfsSync as statfsSync3,
|
|
17
18
|
statSync as statSync15
|
|
18
19
|
} from "node:fs";
|
|
@@ -21,6 +22,7 @@ import { arch, homedir as homedir5, platform as platform2, release } from "node:
|
|
|
21
22
|
import { join as join23, resolve as resolve6, sep as sep3 } from "node:path";
|
|
22
23
|
import { createInterface } from "node:readline/promises";
|
|
23
24
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
25
|
+
import { parentPort } from "node:worker_threads";
|
|
24
26
|
|
|
25
27
|
// ../core/dist/claims/markdown.js
|
|
26
28
|
var FENCE = /(^|\n)\s*(```|~~~)/;
|
|
@@ -277,6 +279,17 @@ function headlineOf(text, max = 160) {
|
|
|
277
279
|
return "";
|
|
278
280
|
}
|
|
279
281
|
|
|
282
|
+
// ../core/dist/format/bytes.js
|
|
283
|
+
function formatBytes(bytes) {
|
|
284
|
+
if (bytes < 1e3)
|
|
285
|
+
return `${bytes} B`;
|
|
286
|
+
if (bytes < 1e3 * 1e3)
|
|
287
|
+
return `${(bytes / 1e3).toFixed(1)} KB`;
|
|
288
|
+
if (bytes < 1e3 * 1e3 * 1e3)
|
|
289
|
+
return `${(bytes / (1e3 * 1e3)).toFixed(1)} MB`;
|
|
290
|
+
return `${(bytes / (1e3 * 1e3 * 1e3)).toFixed(2)} GB`;
|
|
291
|
+
}
|
|
292
|
+
|
|
280
293
|
// ../core/dist/verification/classifyCommand.js
|
|
281
294
|
var RULES = [
|
|
282
295
|
{ runner: "vitest", method: "test", pattern: /^vitest(\s|$)/ },
|
|
@@ -18978,6 +18991,30 @@ var OperationsStoreMeasurementSchema = external_exports.object({
|
|
|
18978
18991
|
retention: RetentionDaysSchema.nullable(),
|
|
18979
18992
|
lastIngestAt: CanonicalTimestampSchema.nullable()
|
|
18980
18993
|
}).strict();
|
|
18994
|
+
var StorageCompositionPartSchema = external_exports.object({
|
|
18995
|
+
key: external_exports.enum(["sessions", "checkpoints", "provenance", "structure", "reusable"]),
|
|
18996
|
+
bytes: external_exports.number().int().nonnegative()
|
|
18997
|
+
}).strict();
|
|
18998
|
+
var StorageCompositionProjectSchema = external_exports.object({
|
|
18999
|
+
/** Repository root, else working directory, else empty when the session recorded neither. */
|
|
19000
|
+
path: external_exports.string(),
|
|
19001
|
+
sessions: external_exports.number().int().nonnegative(),
|
|
19002
|
+
bytes: external_exports.number().int().nonnegative()
|
|
19003
|
+
}).strict();
|
|
19004
|
+
var StorageCompositionSchema = external_exports.object({
|
|
19005
|
+
contractVersion: external_exports.literal(OPERATIONS_CONTRACT_VERSION),
|
|
19006
|
+
state: external_exports.enum(["absent", "running", "ready", "failed"]),
|
|
19007
|
+
computedAt: CanonicalTimestampSchema.nullable(),
|
|
19008
|
+
elapsedMs: external_exports.number().int().nonnegative().nullable(),
|
|
19009
|
+
/** The database file at the moment of measurement, from page count and page size. */
|
|
19010
|
+
fileBytes: external_exports.number().int().nonnegative().nullable(),
|
|
19011
|
+
sessions: external_exports.number().int().nonnegative().nullable(),
|
|
19012
|
+
parts: external_exports.array(StorageCompositionPartSchema),
|
|
19013
|
+
projects: external_exports.array(StorageCompositionProjectSchema),
|
|
19014
|
+
/** Projects measured but not listed, so a truncated list can say so rather than imply totality. */
|
|
19015
|
+
projectsOmitted: external_exports.number().int().nonnegative(),
|
|
19016
|
+
failure: external_exports.string().max(500).nullable()
|
|
19017
|
+
}).strict();
|
|
18981
19018
|
var MaintenancePhaseSchema = external_exports.enum([
|
|
18982
19019
|
"idle",
|
|
18983
19020
|
"pause",
|
|
@@ -19089,6 +19126,17 @@ var LocalAlertSchema = external_exports.object({
|
|
|
19089
19126
|
state: external_exports.enum(["active", "acknowledged", "recovered"]),
|
|
19090
19127
|
title: external_exports.string().min(1).max(160),
|
|
19091
19128
|
detail: external_exports.string().min(1).max(500),
|
|
19129
|
+
/*
|
|
19130
|
+
* What to say once the condition is over.
|
|
19131
|
+
*
|
|
19132
|
+
* Without these a recovered alert can only be shown in the words that announced it, and every
|
|
19133
|
+
* surface that renders one inherits the present tense: the macOS notification read "Recovered:
|
|
19134
|
+
* The durable queue is growing", which is the all-clear and the alarm in one line. Optional
|
|
19135
|
+
* because a ledger written before this field existed still has to parse; readers fall back to
|
|
19136
|
+
* `title` and `detail`.
|
|
19137
|
+
*/
|
|
19138
|
+
recoveryTitle: external_exports.string().min(1).max(160).optional(),
|
|
19139
|
+
recoveryDetail: external_exports.string().min(1).max(500).optional(),
|
|
19092
19140
|
firstSeenAt: CanonicalTimestampSchema,
|
|
19093
19141
|
lastSeenAt: CanonicalTimestampSchema,
|
|
19094
19142
|
lastTransitionAt: CanonicalTimestampSchema,
|
|
@@ -24640,7 +24688,9 @@ function conditions(snapshot, config2, priorHookTrust, priorGapFingerprint, now)
|
|
|
24640
24688
|
kind: "queue-age",
|
|
24641
24689
|
severity: "warning",
|
|
24642
24690
|
title: "Queued work is aging",
|
|
24643
|
-
detail: `The oldest
|
|
24691
|
+
detail: `The oldest item waiting to be stored is at least ${values2.alerts.queueAgeMinutes.value} minutes old.`,
|
|
24692
|
+
recoveryTitle: "Queued work is moving again",
|
|
24693
|
+
recoveryDetail: `Nothing has been waiting longer than ${values2.alerts.queueAgeMinutes.value} minutes. No action is needed.`
|
|
24644
24694
|
});
|
|
24645
24695
|
const velocity = snapshot.estimates.queueVelocity;
|
|
24646
24696
|
if (velocity && velocity.value > 0 && velocity.value * (velocity.sampleWindowSeconds / 60) >= values2.alerts.queueGrowthFiles.value)
|
|
@@ -24648,8 +24698,10 @@ function conditions(snapshot, config2, priorHookTrust, priorGapFingerprint, now)
|
|
|
24648
24698
|
key: "queue-growth",
|
|
24649
24699
|
kind: "queue-growth",
|
|
24650
24700
|
severity: "warning",
|
|
24651
|
-
title: "
|
|
24652
|
-
detail: `
|
|
24701
|
+
title: "Salidium is falling behind",
|
|
24702
|
+
detail: `Your agents are producing work faster than Salidium is storing it: ${values2.alerts.queueGrowthFiles.value} more files are waiting than when this window started. Nothing is lost while it waits.`,
|
|
24703
|
+
recoveryTitle: "Salidium caught up",
|
|
24704
|
+
recoveryDetail: "The backlog stopped growing. No action is needed."
|
|
24653
24705
|
});
|
|
24654
24706
|
if (snapshot.store.totalBytes !== null && snapshot.store.totalBytes >= values2.alerts.databaseSizeBytes.value)
|
|
24655
24707
|
out.push({
|
|
@@ -24657,31 +24709,45 @@ function conditions(snapshot, config2, priorHookTrust, priorGapFingerprint, now)
|
|
|
24657
24709
|
kind: "database-size",
|
|
24658
24710
|
severity: "notice",
|
|
24659
24711
|
title: "Local storage crossed its warning size",
|
|
24660
|
-
detail: `
|
|
24712
|
+
detail: `Salidium is using ${formatBytes(snapshot.store.totalBytes)} on this Mac, past the ${formatBytes(values2.alerts.databaseSizeBytes.value)} mark. Retention is set to ${snapshot.store.retention === "forever" ? "keep everything forever" : `${snapshot.store.retention} days`}.`,
|
|
24713
|
+
recoveryTitle: "Local storage is back under its warning size",
|
|
24714
|
+
/*
|
|
24715
|
+
* No measurement in here. Recovery wording is composed while the condition is still true and
|
|
24716
|
+
* refreshed only for as long as it stays true, so quoting `store.totalBytes` would put the
|
|
24717
|
+
* size that raised the alert into the sentence saying the alert is over: "Salidium is using
|
|
24718
|
+
* 5.01 GB, below the 5.00 GB mark". The threshold is config and does not have that problem.
|
|
24719
|
+
*/
|
|
24720
|
+
recoveryDetail: `Salidium is back below ${formatBytes(values2.alerts.databaseSizeBytes.value)}. Open Salidium to see the current size.`
|
|
24661
24721
|
});
|
|
24662
24722
|
if (snapshot.gaps.latestFingerprint && (snapshot.gaps.active > 0 || snapshot.gaps.latestFingerprint !== priorGapFingerprint))
|
|
24663
24723
|
out.push({
|
|
24664
24724
|
key: `collection-gap:${snapshot.gaps.latestFingerprint}`,
|
|
24665
24725
|
kind: "collection-gap",
|
|
24666
24726
|
severity: snapshot.gaps.active > 0 ? "critical" : "warning",
|
|
24667
|
-
title: snapshot.gaps.active > 0 ? "
|
|
24668
|
-
detail: "
|
|
24727
|
+
title: snapshot.gaps.active > 0 ? "Salidium is missing some activity" : "Salidium missed some activity",
|
|
24728
|
+
detail: snapshot.gaps.active > 0 ? "Agent activity is happening that Salidium is not recording. Reports covering this period will be incomplete. How much was missed cannot be counted." : "A period of agent activity went unrecorded. Reports covering it will be incomplete. How much was missed cannot be counted.",
|
|
24729
|
+
recoveryTitle: "Salidium is recording everything again",
|
|
24730
|
+
recoveryDetail: "Collection is complete from here on. Reports covering the earlier gap stay incomplete."
|
|
24669
24731
|
});
|
|
24670
24732
|
if (snapshot.daemon.state !== "running")
|
|
24671
24733
|
out.push({
|
|
24672
24734
|
key: "daemon-health",
|
|
24673
24735
|
kind: "daemon-health",
|
|
24674
24736
|
severity: snapshot.daemon.state === "unresponsive" ? "critical" : "warning",
|
|
24675
|
-
title: snapshot.daemon.state === "unresponsive" ? "
|
|
24676
|
-
detail:
|
|
24737
|
+
title: snapshot.daemon.state === "unresponsive" ? "Salidium is not responding" : "Salidium has stopped",
|
|
24738
|
+
detail: snapshot.daemon.state === "unresponsive" ? "Salidium is running but not answering. Agent activity is not being recorded while this lasts." : "Salidium is not running. Agent activity is not being recorded until it starts again.",
|
|
24739
|
+
recoveryTitle: "Salidium is running again",
|
|
24740
|
+
recoveryDetail: "Recording has resumed. No action is needed."
|
|
24677
24741
|
});
|
|
24678
24742
|
if (snapshot.maintenance?.phase === "failure" || snapshot.maintenance?.phase === "recovery")
|
|
24679
24743
|
out.push({
|
|
24680
24744
|
key: `maintenance-failure:${snapshot.maintenance.operationId}`,
|
|
24681
24745
|
kind: "maintenance-failure",
|
|
24682
24746
|
severity: "critical",
|
|
24683
|
-
title: "Maintenance
|
|
24684
|
-
detail: (snapshot.maintenance.failure ?? snapshot.maintenance.message).slice(0, 500)
|
|
24747
|
+
title: "Maintenance did not finish",
|
|
24748
|
+
detail: (snapshot.maintenance.failure ?? snapshot.maintenance.message).slice(0, 500),
|
|
24749
|
+
recoveryTitle: "Maintenance finished",
|
|
24750
|
+
recoveryDetail: "The operation that needed attention completed. No action is needed."
|
|
24685
24751
|
});
|
|
24686
24752
|
for (const hook of snapshot.hooks) {
|
|
24687
24753
|
const prior = priorHookTrust[hook.id];
|
|
@@ -24692,8 +24758,10 @@ function conditions(snapshot, config2, priorHookTrust, priorGapFingerprint, now)
|
|
|
24692
24758
|
key: `hook-trust-change:${hook.id}`,
|
|
24693
24759
|
kind: "hook-trust-change",
|
|
24694
24760
|
severity: hook.trust === "modified" || hook.trust === "untrusted" ? "critical" : "notice",
|
|
24695
|
-
title:
|
|
24696
|
-
detail: `
|
|
24761
|
+
title: unsafe ? `The ${hook.name} hook is no longer approved` : `The ${hook.name} hook was approved`,
|
|
24762
|
+
detail: unsafe ? `The hook file changed since you approved it (${prior} to ${hook.trust}). Salidium will not trust it until you approve the new version.` : `Approval state went from ${prior} to ${hook.trust}.`,
|
|
24763
|
+
recoveryTitle: `The ${hook.name} hook is approved again`,
|
|
24764
|
+
recoveryDetail: "The hook file matches an approved version. No action is needed."
|
|
24697
24765
|
});
|
|
24698
24766
|
}
|
|
24699
24767
|
return out;
|
|
@@ -24724,6 +24792,8 @@ async function evaluateLocalAlerts(home, snapshot, config2, options = {}) {
|
|
|
24724
24792
|
existing.severity = condition.severity;
|
|
24725
24793
|
existing.title = condition.title;
|
|
24726
24794
|
existing.detail = condition.detail;
|
|
24795
|
+
existing.recoveryTitle = condition.recoveryTitle;
|
|
24796
|
+
existing.recoveryDetail = condition.recoveryDetail;
|
|
24727
24797
|
existing.notificationEligible = false;
|
|
24728
24798
|
continue;
|
|
24729
24799
|
}
|
|
@@ -24737,6 +24807,8 @@ async function evaluateLocalAlerts(home, snapshot, config2, options = {}) {
|
|
|
24737
24807
|
state: "active",
|
|
24738
24808
|
title: condition.title,
|
|
24739
24809
|
detail: condition.detail,
|
|
24810
|
+
recoveryTitle: condition.recoveryTitle,
|
|
24811
|
+
recoveryDetail: condition.recoveryDetail,
|
|
24740
24812
|
firstSeenAt: at,
|
|
24741
24813
|
lastSeenAt: at,
|
|
24742
24814
|
lastTransitionAt: at,
|
|
@@ -24814,7 +24886,13 @@ var DEFAULT_OPERATIONAL_CONFIG = {
|
|
|
24814
24886
|
alerts: {
|
|
24815
24887
|
queueAgeMinutes: 10,
|
|
24816
24888
|
queueGrowthFiles: 100,
|
|
24817
|
-
|
|
24889
|
+
/*
|
|
24890
|
+
* Decimal, to pair with how the size is shown. Left at 5 GiB it read "warns at 5.37 GB", which
|
|
24891
|
+
* is not a number anyone chose, and the rail's own picker offered GiB steps that no longer
|
|
24892
|
+
* matched any label. This lowers the warning by about seven percent; it is a notice about a
|
|
24893
|
+
* local file, and `alerts.databaseSizeBytes` still overrides it.
|
|
24894
|
+
*/
|
|
24895
|
+
databaseSizeBytes: 5 * 1e3 * 1e3 * 1e3,
|
|
24818
24896
|
cooldownMinutes: 30,
|
|
24819
24897
|
nativeNotifications: false
|
|
24820
24898
|
},
|
|
@@ -26860,6 +26938,87 @@ var SqliteStore = class {
|
|
|
26860
26938
|
});
|
|
26861
26939
|
return true;
|
|
26862
26940
|
}
|
|
26941
|
+
/*
|
|
26942
|
+
* What the store is made of, measured rather than estimated.
|
|
26943
|
+
*
|
|
26944
|
+
* Every part here is one pass over a table. The events pass is the expensive one: `length(json)`
|
|
26945
|
+
* does not read a blob's payload, but it does read the header of every row, so on a store with
|
|
26946
|
+
* 1.3 million events that is a ten second scan and the reason this never runs on the health path
|
|
26947
|
+
* or on a timer. The daemon runs it on a worker with its own read-only connection for the same
|
|
26948
|
+
* reason it runs usage backfill there: a ten second synchronous scan on the main loop is ten
|
|
26949
|
+
* seconds of unanswered hooks.
|
|
26950
|
+
*
|
|
26951
|
+
* `structure` is deliberately a subtraction. Naming what each index costs needs `dbstat`, which
|
|
26952
|
+
* is a further nine to fifteen seconds because it walks every page in the file, and the answer
|
|
26953
|
+
* it adds is "your indexes are large" rather than anything a reader can act on. The difference
|
|
26954
|
+
* is honest, arrives free, and is labelled as a remainder wherever it is shown.
|
|
26955
|
+
*/
|
|
26956
|
+
storageComposition(projectLimit = 12) {
|
|
26957
|
+
const started = Date.now();
|
|
26958
|
+
const scalar = (sql) => {
|
|
26959
|
+
const row = this.db.prepare(sql).get();
|
|
26960
|
+
const value2 = row ? Object.values(row)[0] : 0;
|
|
26961
|
+
return typeof value2 === "number" ? value2 : Number(value2 ?? 0);
|
|
26962
|
+
};
|
|
26963
|
+
const pageSize = scalar("PRAGMA page_size");
|
|
26964
|
+
const fileBytes = scalar("PRAGMA page_count") * pageSize;
|
|
26965
|
+
const reusable = scalar("PRAGMA freelist_count") * pageSize;
|
|
26966
|
+
const sessions = this.db.prepare(`SELECT id, COALESCE(NULLIF(repo_root, ''), NULLIF(cwd, ''), '') AS project FROM sessions`).all();
|
|
26967
|
+
const projectOf = new Map(sessions.map((row) => [row.id, row.project]));
|
|
26968
|
+
const perProject = /* @__PURE__ */ new Map();
|
|
26969
|
+
for (const row of sessions) {
|
|
26970
|
+
const entry2 = perProject.get(row.project) ?? { sessions: 0, bytes: 0 };
|
|
26971
|
+
entry2.sessions += 1;
|
|
26972
|
+
perProject.set(row.project, entry2);
|
|
26973
|
+
}
|
|
26974
|
+
let sessionBytes = 0;
|
|
26975
|
+
let checkpointBytes = 0;
|
|
26976
|
+
const accumulate = (sql, into) => {
|
|
26977
|
+
for (const row of this.db.prepare(sql).all()) {
|
|
26978
|
+
const bytes = Number(row.b ?? 0);
|
|
26979
|
+
into(bytes);
|
|
26980
|
+
const project = projectOf.get(row.id);
|
|
26981
|
+
if (project === void 0)
|
|
26982
|
+
continue;
|
|
26983
|
+
const entry2 = perProject.get(project);
|
|
26984
|
+
if (entry2)
|
|
26985
|
+
entry2.bytes += bytes;
|
|
26986
|
+
}
|
|
26987
|
+
};
|
|
26988
|
+
accumulate("SELECT session_id AS id, SUM(length(json)) AS b FROM events GROUP BY session_id", (b) => {
|
|
26989
|
+
sessionBytes += b;
|
|
26990
|
+
});
|
|
26991
|
+
accumulate("SELECT session_id AS id, SUM(length(json)) AS b FROM changes GROUP BY session_id", (b) => {
|
|
26992
|
+
sessionBytes += b;
|
|
26993
|
+
});
|
|
26994
|
+
accumulate("SELECT session_id AS id, SUM(length(state_json)) AS b FROM checkpoints GROUP BY session_id", (b) => {
|
|
26995
|
+
checkpointBytes += b;
|
|
26996
|
+
});
|
|
26997
|
+
const provenanceBytes = scalar(`SELECT COALESCE(SUM(length(session_id) + length(event_id) + length(path)
|
|
26998
|
+
+ length(record_hash) + length(captured_at) + length(COALESCE(origin, ''))), 0) AS b
|
|
26999
|
+
FROM raw_record_fingerprints`);
|
|
27000
|
+
const named = sessionBytes + checkpointBytes + provenanceBytes + reusable;
|
|
27001
|
+
const ranked = [...perProject.entries()].map(([path, value2]) => ({ path, sessions: value2.sessions, bytes: value2.bytes })).sort((a, b) => b.bytes - a.bytes || b.sessions - a.sessions);
|
|
27002
|
+
return {
|
|
27003
|
+
contractVersion: OPERATIONS_CONTRACT_VERSION,
|
|
27004
|
+
state: "ready",
|
|
27005
|
+
computedAt: new Date(started).toISOString(),
|
|
27006
|
+
elapsedMs: Date.now() - started,
|
|
27007
|
+
fileBytes,
|
|
27008
|
+
sessions: sessions.length,
|
|
27009
|
+
parts: [
|
|
27010
|
+
{ key: "sessions", bytes: sessionBytes },
|
|
27011
|
+
{ key: "checkpoints", bytes: checkpointBytes },
|
|
27012
|
+
{ key: "provenance", bytes: provenanceBytes },
|
|
27013
|
+
// A store measured while it is being written to can name more than the file holds.
|
|
27014
|
+
{ key: "structure", bytes: Math.max(fileBytes - named, 0) },
|
|
27015
|
+
{ key: "reusable", bytes: reusable }
|
|
27016
|
+
],
|
|
27017
|
+
projects: ranked.slice(0, projectLimit),
|
|
27018
|
+
projectsOmitted: Math.max(ranked.length - projectLimit, 0),
|
|
27019
|
+
failure: null
|
|
27020
|
+
};
|
|
27021
|
+
}
|
|
26863
27022
|
/** Offline space reclamation after bounded cleanup batches. */
|
|
26864
27023
|
compact() {
|
|
26865
27024
|
if (!this.usageBackfillProgress().complete)
|
|
@@ -27715,9 +27874,14 @@ function runStorageOptimizationMaintenance(home, options = {}) {
|
|
|
27715
27874
|
import { spawn as spawn3 } from "node:child_process";
|
|
27716
27875
|
import { delimiter as delimiter3 } from "node:path";
|
|
27717
27876
|
function notificationText(alert) {
|
|
27877
|
+
if (alert.state === "recovered")
|
|
27878
|
+
return {
|
|
27879
|
+
title: alert.recoveryTitle ?? `Recovered: ${alert.title}`,
|
|
27880
|
+
detail: alert.recoveryDetail ?? alert.detail
|
|
27881
|
+
};
|
|
27718
27882
|
const detail = alert.kind === "maintenance-failure" ? "Maintenance needs local review. Open Salidium or run salidium maintenance status for details." : alert.detail;
|
|
27719
27883
|
return {
|
|
27720
|
-
title: alert.
|
|
27884
|
+
title: alert.title,
|
|
27721
27885
|
detail: `${detail} Open Salidium or run salidium status for details.`
|
|
27722
27886
|
};
|
|
27723
27887
|
}
|
|
@@ -28699,6 +28863,13 @@ function createHttpServer(deps) {
|
|
|
28699
28863
|
}
|
|
28700
28864
|
if (req.method === "POST" && url2.pathname === "/api/operations/maintenance/drain" && deps.operations)
|
|
28701
28865
|
return json2(res, 200, deps.operations.drainQueue());
|
|
28866
|
+
if (url2.pathname === "/api/operations/storage" && deps.operations) {
|
|
28867
|
+
if (req.method === "GET")
|
|
28868
|
+
return json2(res, 200, deps.operations.storageComposition());
|
|
28869
|
+
if (req.method === "POST")
|
|
28870
|
+
return json2(res, 200, deps.operations.analyzeStorage());
|
|
28871
|
+
return json2(res, 405, { error: "method not allowed" });
|
|
28872
|
+
}
|
|
28702
28873
|
const acknowledge = /^\/api\/operations\/alerts\/([^/]+)\/acknowledge$/.exec(url2.pathname);
|
|
28703
28874
|
if (req.method === "POST" && acknowledge?.[1] && deps.operations) {
|
|
28704
28875
|
try {
|
|
@@ -29438,6 +29609,67 @@ async function startDaemon(overrides = {}) {
|
|
|
29438
29609
|
refreshHealthSampling?.();
|
|
29439
29610
|
return effective2;
|
|
29440
29611
|
};
|
|
29612
|
+
let composition = {
|
|
29613
|
+
contractVersion: OPERATIONS_CONTRACT_VERSION,
|
|
29614
|
+
state: "absent",
|
|
29615
|
+
computedAt: null,
|
|
29616
|
+
elapsedMs: null,
|
|
29617
|
+
fileBytes: null,
|
|
29618
|
+
sessions: null,
|
|
29619
|
+
parts: [],
|
|
29620
|
+
projects: [],
|
|
29621
|
+
projectsOmitted: 0,
|
|
29622
|
+
failure: null
|
|
29623
|
+
};
|
|
29624
|
+
let compositionWorker;
|
|
29625
|
+
const analyzeStorage = () => {
|
|
29626
|
+
if (compositionWorker)
|
|
29627
|
+
return composition;
|
|
29628
|
+
const sibling = new URL("./storage/compositionWorker.js", import.meta.url);
|
|
29629
|
+
const worker = existsSync16(fileURLToPath(sibling)) ? new Worker(sibling, { workerData: paths.db }) : (() => {
|
|
29630
|
+
const runtime = process.argv[1];
|
|
29631
|
+
if (!runtime)
|
|
29632
|
+
return void 0;
|
|
29633
|
+
return new Worker(resolve4(runtime), {
|
|
29634
|
+
argv: ["__storage-composition", paths.db]
|
|
29635
|
+
});
|
|
29636
|
+
})();
|
|
29637
|
+
if (!worker) {
|
|
29638
|
+
composition = {
|
|
29639
|
+
...composition,
|
|
29640
|
+
state: "failed",
|
|
29641
|
+
failure: "Salidium could not locate a runtime to measure the store with."
|
|
29642
|
+
};
|
|
29643
|
+
return composition;
|
|
29644
|
+
}
|
|
29645
|
+
compositionWorker = worker;
|
|
29646
|
+
worker.unref();
|
|
29647
|
+
composition = { ...composition, state: "running", failure: null };
|
|
29648
|
+
worker.once("message", (message) => {
|
|
29649
|
+
composition = message;
|
|
29650
|
+
});
|
|
29651
|
+
worker.once("error", (error51) => {
|
|
29652
|
+
log.warn("storage composition worker failed", { err: String(error51) });
|
|
29653
|
+
composition = { ...composition, state: "failed", failure: String(error51).slice(0, 500) };
|
|
29654
|
+
});
|
|
29655
|
+
worker.once("exit", (code) => {
|
|
29656
|
+
if (compositionWorker === worker)
|
|
29657
|
+
compositionWorker = void 0;
|
|
29658
|
+
if (code !== 0 && composition.state === "running")
|
|
29659
|
+
composition = {
|
|
29660
|
+
...composition,
|
|
29661
|
+
state: "failed",
|
|
29662
|
+
failure: `The measurement stopped with status ${code}.`
|
|
29663
|
+
};
|
|
29664
|
+
else if (composition.state === "running")
|
|
29665
|
+
composition = {
|
|
29666
|
+
...composition,
|
|
29667
|
+
state: "failed",
|
|
29668
|
+
failure: "The measurement produced no result."
|
|
29669
|
+
};
|
|
29670
|
+
});
|
|
29671
|
+
return composition;
|
|
29672
|
+
};
|
|
29441
29673
|
let port = config2.port;
|
|
29442
29674
|
const server = createHttpServer({
|
|
29443
29675
|
registry: registry2,
|
|
@@ -29473,7 +29705,9 @@ async function startDaemon(overrides = {}) {
|
|
|
29473
29705
|
return inspectQueue(config2.home, { entryLimit: limit });
|
|
29474
29706
|
},
|
|
29475
29707
|
drainQueue: () => runQueueDrainMaintenance(config2.home, () => hooks.drainSpool()),
|
|
29476
|
-
acknowledgeAlert: (id) => acknowledgeLocalAlert(config2.home, id)
|
|
29708
|
+
acknowledgeAlert: (id) => acknowledgeLocalAlert(config2.home, id),
|
|
29709
|
+
storageComposition: () => composition,
|
|
29710
|
+
analyzeStorage
|
|
29477
29711
|
},
|
|
29478
29712
|
settings: {
|
|
29479
29713
|
explainer: explainerSettings,
|
|
@@ -29564,6 +29798,8 @@ async function startDaemon(overrides = {}) {
|
|
|
29564
29798
|
clearTimeout(usageBackfillRetryTimer);
|
|
29565
29799
|
if (usageBackfillWorker)
|
|
29566
29800
|
void usageBackfillWorker.terminate();
|
|
29801
|
+
if (compositionWorker)
|
|
29802
|
+
void compositionWorker.terminate();
|
|
29567
29803
|
if (healthTimer)
|
|
29568
29804
|
clearTimeout(healthTimer);
|
|
29569
29805
|
if (hookTrustTimer)
|
|
@@ -31510,6 +31746,15 @@ async function runFirstRunOnboarding(context, io, options = {}) {
|
|
|
31510
31746
|
};
|
|
31511
31747
|
}
|
|
31512
31748
|
|
|
31749
|
+
// src/pauseOnRun.ts
|
|
31750
|
+
function clearsPauseOnRun(command, argument, argv) {
|
|
31751
|
+
if (argv.includes("--no-resume")) return false;
|
|
31752
|
+
if (["pause", "resume", "stop", "service"].includes(command)) return false;
|
|
31753
|
+
if (command.startsWith("__")) return false;
|
|
31754
|
+
if (command === "storage" && argument === "optimize") return false;
|
|
31755
|
+
return true;
|
|
31756
|
+
}
|
|
31757
|
+
|
|
31513
31758
|
// src/render.ts
|
|
31514
31759
|
var H = "\u2500";
|
|
31515
31760
|
var CONTROL = /[\u0000-\u001F\u007F-\u009F]/g;
|
|
@@ -31740,6 +31985,7 @@ Usage:
|
|
|
31740
31985
|
salidium retention apply Apply one cleanup batch now (daemon must be stopped)
|
|
31741
31986
|
salidium retention compact Return reusable SQLite pages to the OS (daemon must be stopped)
|
|
31742
31987
|
salidium storage Show the lossless storage layout and page size
|
|
31988
|
+
salidium storage composition Measure what is using the space, by part and by project
|
|
31743
31989
|
salidium storage optimize Coordinate, copy, verify, and install the compact layout
|
|
31744
31990
|
salidium pin [session] Exempt a session from automatic retention
|
|
31745
31991
|
salidium unpin [session] Remove the retention exemption
|
|
@@ -31761,7 +32007,9 @@ Environment:
|
|
|
31761
32007
|
|
|
31762
32008
|
Native Windows imports transcript history but does not install the POSIX live-hook relay.
|
|
31763
32009
|
Ordinary commands resume an expired or manual pause. pause, stop, service commands, and coordinated
|
|
31764
|
-
storage optimize do not; resume changes collection state explicitly.
|
|
32010
|
+
storage optimize do not; resume changes collection state explicitly. Add --no-resume to any command
|
|
32011
|
+
to leave a pause in place, for a caller that has already seen the daemon answer and so cannot be
|
|
32012
|
+
recovering a marker its dead owner left behind.
|
|
31765
32013
|
`;
|
|
31766
32014
|
var require3 = createRequire2(import.meta.url);
|
|
31767
32015
|
var VERSION2 = (() => {
|
|
@@ -31781,11 +32029,10 @@ async function main(argv) {
|
|
|
31781
32029
|
const jsonOutput = argv.includes("--json");
|
|
31782
32030
|
const quiet = argv.includes("--quiet");
|
|
31783
32031
|
const positional = argv.filter(
|
|
31784
|
-
(value2) => !["--yes", "-y", "--no-open", "--json", "--quiet"].includes(value2)
|
|
32032
|
+
(value2) => !["--yes", "-y", "--no-open", "--json", "--quiet", "--no-resume"].includes(value2)
|
|
31785
32033
|
);
|
|
31786
32034
|
const [cmd = "up", arg, ...args] = positional;
|
|
31787
|
-
if (
|
|
31788
|
-
await implicitlyResumeCollection();
|
|
32035
|
+
if (clearsPauseOnRun(cmd, arg, argv)) await implicitlyResumeCollection();
|
|
31789
32036
|
switch (cmd) {
|
|
31790
32037
|
case "help":
|
|
31791
32038
|
case "--help":
|
|
@@ -31828,6 +32075,24 @@ async function main(argv) {
|
|
|
31828
32075
|
store.close();
|
|
31829
32076
|
}
|
|
31830
32077
|
}
|
|
32078
|
+
/*
|
|
32079
|
+
* The other private worker entrypoint. Measuring what the store is made of reads the header of
|
|
32080
|
+
* every stored event, which is ten seconds on a three gigabyte store, and `node:sqlite` is
|
|
32081
|
+
* synchronous: run on the daemon's loop that is ten seconds of hooks going unanswered and
|
|
32082
|
+
* spooling to disk. It runs here instead, on its own event loop and its own connection, and
|
|
32083
|
+
* posts one message back.
|
|
32084
|
+
*/
|
|
32085
|
+
case "__storage-composition": {
|
|
32086
|
+
if (!arg || !resolve6(arg).startsWith(`${resolve6(salidiumHome)}${sep3}`))
|
|
32087
|
+
throw new Error("storage composition store must be inside the Salidium state directory");
|
|
32088
|
+
const store = new SqliteStore(resolve6(arg), { concurrentWriter: true });
|
|
32089
|
+
try {
|
|
32090
|
+
parentPort?.postMessage(store.storageComposition());
|
|
32091
|
+
return 0;
|
|
32092
|
+
} finally {
|
|
32093
|
+
store.close();
|
|
32094
|
+
}
|
|
32095
|
+
}
|
|
31831
32096
|
case "start": {
|
|
31832
32097
|
const running = await ensureDaemon();
|
|
31833
32098
|
const explanations = await currentExplanationState(running, "reachable");
|
|
@@ -32442,9 +32707,9 @@ Change with: salidium explanations off|when-done|each-reply
|
|
|
32442
32707
|
`);
|
|
32443
32708
|
process.stdout.write(`Store: ${formatBytes(storeBytes)}
|
|
32444
32709
|
`);
|
|
32445
|
-
if (storeBytes >=
|
|
32710
|
+
if (storeBytes >= 1e3 * 1e3 * 1e3)
|
|
32446
32711
|
process.stdout.write(
|
|
32447
|
-
inspectStoreLayout(db).optimized ? "Storage warning: history is over 1
|
|
32712
|
+
inspectStoreLayout(db).optimized ? "Storage warning: history is over 1 GB. Preview exactly what retention would delete before opting in.\n" : "Lossless storage optimization is available before deleting history. Run `salidium storage`, then `salidium storage optimize`; it coordinates queue drain and daemon stop.\n"
|
|
32448
32713
|
);
|
|
32449
32714
|
process.stdout.write(`Pinned: ${store.pinnedSessionIds().length}
|
|
32450
32715
|
`);
|
|
@@ -32464,10 +32729,54 @@ Change with: salidium explanations off|when-done|each-reply
|
|
|
32464
32729
|
`);
|
|
32465
32730
|
return 1;
|
|
32466
32731
|
}
|
|
32467
|
-
if (arg !== void 0 && arg !== "optimize") {
|
|
32468
|
-
process.stderr.write("storage accepts only `optimize`\n");
|
|
32732
|
+
if (arg !== void 0 && arg !== "optimize" && arg !== "composition") {
|
|
32733
|
+
process.stderr.write("storage accepts only `composition` or `optimize`\n");
|
|
32469
32734
|
return 2;
|
|
32470
32735
|
}
|
|
32736
|
+
if (arg === "composition") {
|
|
32737
|
+
const store = new SqliteStore(db, { concurrentWriter: true });
|
|
32738
|
+
try {
|
|
32739
|
+
const measured = store.storageComposition();
|
|
32740
|
+
if (jsonOutput) {
|
|
32741
|
+
process.stdout.write(`${JSON.stringify(measured)}
|
|
32742
|
+
`);
|
|
32743
|
+
return 0;
|
|
32744
|
+
}
|
|
32745
|
+
const label = {
|
|
32746
|
+
sessions: "Recorded sessions",
|
|
32747
|
+
checkpoints: "Replay checkpoints",
|
|
32748
|
+
provenance: "Provenance records",
|
|
32749
|
+
structure: "Indexes and internal structure",
|
|
32750
|
+
reusable: "Reusable space"
|
|
32751
|
+
};
|
|
32752
|
+
process.stdout.write(`On this Mac: ${formatBytes(measured.fileBytes ?? 0)}
|
|
32753
|
+
`);
|
|
32754
|
+
for (const part of measured.parts)
|
|
32755
|
+
process.stdout.write(
|
|
32756
|
+
` ${(label[part.key] ?? part.key).padEnd(30)} ${formatBytes(part.bytes)}
|
|
32757
|
+
`
|
|
32758
|
+
);
|
|
32759
|
+
process.stdout.write(
|
|
32760
|
+
`
|
|
32761
|
+
${measured.sessions ?? 0} sessions across ${measured.projects.length + measured.projectsOmitted} projects.
|
|
32762
|
+
`
|
|
32763
|
+
);
|
|
32764
|
+
for (const project of measured.projects)
|
|
32765
|
+
process.stdout.write(
|
|
32766
|
+
` ${formatBytes(project.bytes).padStart(9)} ${String(project.sessions).padStart(4)} ${project.sessions === 1 ? "session " : "sessions"} ${project.path || "No project recorded"}
|
|
32767
|
+
`
|
|
32768
|
+
);
|
|
32769
|
+
if (measured.projectsOmitted > 0)
|
|
32770
|
+
process.stdout.write(` ${measured.projectsOmitted} more not listed.
|
|
32771
|
+
`);
|
|
32772
|
+
process.stdout.write(
|
|
32773
|
+
"\nIndexes and internal structure is the remainder after the named parts, not a separate measurement.\n"
|
|
32774
|
+
);
|
|
32775
|
+
return 0;
|
|
32776
|
+
} finally {
|
|
32777
|
+
store.close();
|
|
32778
|
+
}
|
|
32779
|
+
}
|
|
32471
32780
|
if (arg === void 0) {
|
|
32472
32781
|
const layout = inspectStoreLayout(db);
|
|
32473
32782
|
if (quiet) return layout.optimized ? 0 : 3;
|
|
@@ -32767,10 +33076,11 @@ async function readOperationsOverview(daemon, presence, providers) {
|
|
|
32767
33076
|
alerts: await evaluateLocalAlerts(salidiumHome, health, config2)
|
|
32768
33077
|
};
|
|
32769
33078
|
}
|
|
32770
|
-
function estimateLabel(estimate2) {
|
|
32771
|
-
if (!estimate2) return
|
|
33079
|
+
function estimateLabel(estimate2, absent = "Unavailable (needs two measurements)") {
|
|
33080
|
+
if (!estimate2) return absent;
|
|
32772
33081
|
const value2 = estimate2.unit === "bytes/minute" ? `${formatBytes(Math.abs(estimate2.value))}/minute` : Math.abs(estimate2.value).toFixed(1);
|
|
32773
|
-
const
|
|
33082
|
+
const zero = /^[0.]+(?: B\/minute)?$/.test(value2);
|
|
33083
|
+
const sign = zero ? "" : estimate2.value < 0 ? "\u2212" : "+";
|
|
32774
33084
|
const unit = estimate2.unit === "bytes/minute" ? "" : ` ${estimate2.unit}`;
|
|
32775
33085
|
return `${sign}${value2}${unit} \xB7 derived from ${estimate2.samples} samples over ${Math.round(estimate2.sampleWindowSeconds)}s`;
|
|
32776
33086
|
}
|
|
@@ -32934,10 +33244,9 @@ async function statusCommand(options) {
|
|
|
32934
33244
|
);
|
|
32935
33245
|
} else {
|
|
32936
33246
|
const health = operations.health;
|
|
32937
|
-
if (health.daemon.state === "stopped") process.stdout.write("not running\n");
|
|
32938
33247
|
process.stdout.write(
|
|
32939
33248
|
health.daemon.state === "running" ? `Daemon: Running \xB7 pid ${health.daemon.pid} \xB7 since ${health.daemon.startedAt}
|
|
32940
|
-
` : health.daemon.state === "unresponsive" ? `Daemon
|
|
33249
|
+
` : health.daemon.state === "unresponsive" ? `Daemon: Not answering \xB7 pid ${health.daemon.pid}
|
|
32941
33250
|
` : "Daemon: Stopped\n"
|
|
32942
33251
|
);
|
|
32943
33252
|
process.stdout.write(
|
|
@@ -32963,12 +33272,24 @@ async function statusCommand(options) {
|
|
|
32963
33272
|
`Store (exact): ${health.store.totalBytes === null ? "Unavailable" : formatBytes(health.store.totalBytes)}, retention ${retentionLabel(health.store.retention)}, last ingest ${health.store.lastIngestAt ?? "Unavailable"}
|
|
32964
33273
|
`
|
|
32965
33274
|
);
|
|
33275
|
+
const projection = storageProjection(
|
|
33276
|
+
health.store.totalBytes,
|
|
33277
|
+
health.estimates.storageGrowth,
|
|
33278
|
+
operations.config.values.alerts.databaseSizeBytes.value
|
|
33279
|
+
);
|
|
33280
|
+
if (projection) process.stdout.write(`Store outlook: ${projection}
|
|
33281
|
+
`);
|
|
32966
33282
|
process.stdout.write(
|
|
32967
33283
|
`Queue velocity (estimate): ${estimateLabel(health.estimates.queueVelocity)}
|
|
32968
33284
|
`
|
|
32969
33285
|
);
|
|
33286
|
+
const sampled = health.estimates.queueVelocity !== null;
|
|
33287
|
+
const notShrinking = "Unavailable (the queue is not shrinking)";
|
|
32970
33288
|
process.stdout.write(
|
|
32971
|
-
`Drain rate (estimate): ${estimateLabel(
|
|
33289
|
+
`Drain rate (estimate): ${estimateLabel(
|
|
33290
|
+
health.estimates.drainRate,
|
|
33291
|
+
sampled ? notShrinking : void 0
|
|
33292
|
+
)}
|
|
32972
33293
|
`
|
|
32973
33294
|
);
|
|
32974
33295
|
process.stdout.write(
|
|
@@ -32976,7 +33297,7 @@ async function statusCommand(options) {
|
|
|
32976
33297
|
`
|
|
32977
33298
|
);
|
|
32978
33299
|
process.stdout.write(
|
|
32979
|
-
`Time to empty (estimate): ${health.estimates.timeToEmpty ? `${Math.round(health.estimates.timeToEmpty.value)} seconds \xB7 derived` : "Unavailable"}
|
|
33300
|
+
`Time to empty (estimate): ${health.estimates.timeToEmpty ? `${Math.round(health.estimates.timeToEmpty.value)} seconds \xB7 derived` : health.queue.files === 0 ? "Already empty" : sampled ? notShrinking : "Unavailable (needs two measurements)"}
|
|
32980
33301
|
`
|
|
32981
33302
|
);
|
|
32982
33303
|
process.stdout.write(
|
|
@@ -32986,7 +33307,7 @@ async function statusCommand(options) {
|
|
|
32986
33307
|
process.stdout.write(`Explanations: ${explanationStateLabel(explanations)}
|
|
32987
33308
|
`);
|
|
32988
33309
|
process.stdout.write(
|
|
32989
|
-
`Maintenance: ${health.maintenance ? `${health.maintenance.phase} \xB7 ${health.maintenance.message}` : "Idle"}
|
|
33310
|
+
`Maintenance: ${health.maintenance ? `${maintenancePhaseLabel(health.maintenance.phase)} \xB7 ${health.maintenance.message}` : "Idle"}
|
|
32990
33311
|
`
|
|
32991
33312
|
);
|
|
32992
33313
|
process.stdout.write(
|
|
@@ -33220,11 +33541,25 @@ async function maintenanceCommand(action, args, options) {
|
|
|
33220
33541
|
process.stderr.write("maintenance accepts status, queue, drain, optimize, or acknowledge\n");
|
|
33221
33542
|
return 2;
|
|
33222
33543
|
}
|
|
33223
|
-
function
|
|
33224
|
-
if (
|
|
33225
|
-
if (
|
|
33226
|
-
if (
|
|
33227
|
-
|
|
33544
|
+
function maintenancePhaseLabel(phase) {
|
|
33545
|
+
if (phase === "failure") return "Did not finish";
|
|
33546
|
+
if (phase === "recovery") return "Recovering";
|
|
33547
|
+
if (phase === "completed") return "Finished";
|
|
33548
|
+
if (phase === "running") return "Running";
|
|
33549
|
+
return phase;
|
|
33550
|
+
}
|
|
33551
|
+
function storageProjection(total, growth, warnAt) {
|
|
33552
|
+
const parts = [];
|
|
33553
|
+
if (growth && growth.value > 0)
|
|
33554
|
+
parts.push(`about ${formatBytes(growth.value * 60 * 24)} a day at this rate`);
|
|
33555
|
+
if (total !== null) {
|
|
33556
|
+
const mark2 = formatBytes(warnAt);
|
|
33557
|
+
const room = formatBytes(warnAt - total);
|
|
33558
|
+
parts.push(
|
|
33559
|
+
total >= warnAt ? `past its ${mark2} warning mark` : room === mark2 ? `warns at ${mark2}` : `${room} below its ${mark2} warning mark`
|
|
33560
|
+
);
|
|
33561
|
+
}
|
|
33562
|
+
return parts.join(" \xB7 ");
|
|
33228
33563
|
}
|
|
33229
33564
|
function queueLabel(status) {
|
|
33230
33565
|
return `${status.queue.files.toLocaleString()} file${status.queue.files === 1 ? "" : "s"}, ${formatBytes(status.queue.bytes)}`;
|
|
@@ -33496,11 +33831,22 @@ async function ensureDaemon() {
|
|
|
33496
33831
|
for (let i = 0; i < 100; i++) {
|
|
33497
33832
|
await sleep(100);
|
|
33498
33833
|
if (childFailure)
|
|
33499
|
-
throw new Error(
|
|
33834
|
+
throw new Error(
|
|
33835
|
+
`daemon ${childFailure} before it became ready: ${startupFailureReason(paths.startupLogFile)}`
|
|
33836
|
+
);
|
|
33500
33837
|
const d = readDaemonJson(salidiumHome);
|
|
33501
33838
|
if (d && d.pid === child.pid && await alive(d)) return d;
|
|
33502
33839
|
}
|
|
33503
|
-
throw new Error(`daemon did not start
|
|
33840
|
+
throw new Error(`daemon did not start: ${startupFailureReason(paths.startupLogFile)}`);
|
|
33841
|
+
}
|
|
33842
|
+
function startupFailureReason(logFile) {
|
|
33843
|
+
try {
|
|
33844
|
+
const lines = readFileSync13(logFile, "utf8").split("\n").map((line) => line.trim()).filter(Boolean);
|
|
33845
|
+
const last = lines.at(-1);
|
|
33846
|
+
if (last) return `${last.slice(0, 200)} (see ${logFile})`;
|
|
33847
|
+
} catch {
|
|
33848
|
+
}
|
|
33849
|
+
return `see ${logFile}`;
|
|
33504
33850
|
}
|
|
33505
33851
|
function compareSemver(left, right) {
|
|
33506
33852
|
const parse3 = (value2) => {
|