@logicseek/hello-cc 0.1.9 → 1.0.1
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/CHANGELOG.md +136 -0
- package/README.md +50 -3
- package/README.zh-CN.md +48 -3
- package/bin/hcc.mjs +3 -5656
- package/docs/README.md +8 -0
- package/docs/README.zh-CN.md +6 -0
- package/docs/architecture.md +29 -18
- package/docs/architecture.zh-CN.md +20 -10
- package/docs/commands.md +13 -4
- package/docs/commands.zh-CN.md +13 -4
- package/docs/guide.md +97 -3
- package/docs/guide.zh-CN.md +89 -2
- package/lib/cli/app.mjs +644 -0
- package/lib/cli/commands/coordination.mjs +249 -0
- package/lib/cli/commands/doctor.mjs +81 -0
- package/lib/cli/commands/gc.mjs +1002 -0
- package/lib/cli/commands/hook.mjs +205 -0
- package/lib/cli/commands/install.mjs +294 -0
- package/lib/cli/commands/lifecycle.mjs +101 -0
- package/lib/cli/commands/lock.mjs +197 -0
- package/lib/cli/commands/misc.mjs +76 -0
- package/lib/cli/commands/msg.mjs +152 -0
- package/lib/cli/commands/peer.mjs +249 -0
- package/lib/cli/commands/query.mjs +96 -0
- package/lib/cli/commands/run.mjs +463 -0
- package/lib/cli/commands/task.mjs +405 -0
- package/lib/cli/commands/team.mjs +131 -0
- package/lib/cli/commands/tmux.mjs +838 -0
- package/lib/cli/commands/up.mjs +135 -0
- package/lib/cli-args.mjs +16 -2
- package/lib/coordination-state.mjs +112 -51
- package/lib/core/coordination/automation.mjs +5 -8
- package/lib/core/coordination/clock-safety.mjs +323 -0
- package/lib/core/coordination/event-retention.mjs +17 -0
- package/lib/core/coordination/gc-plan.mjs +614 -0
- package/lib/core/coordination/lease-renewal.mjs +102 -0
- package/lib/core/coordination/lock-evidence.mjs +77 -0
- package/lib/core/coordination/optimistic-evidence.mjs +26 -0
- package/lib/core/coordination/tasks.mjs +156 -36
- package/lib/core/peers/evidence-runtime.mjs +312 -0
- package/lib/core/peers/evidence.mjs +103 -0
- package/lib/core/peers/hook-owner.mjs +32 -0
- package/lib/core/peers/liveness.mjs +44 -4
- package/lib/core/peers/peer-helpers.mjs +121 -0
- package/lib/core/peers/session.mjs +8 -2
- package/lib/core/peers/tmux-safety.mjs +290 -0
- package/lib/core/sessions/launch.mjs +13 -1
- package/lib/db/connection.mjs +104 -0
- package/lib/db/events.mjs +34 -0
- package/lib/db/migration-backup.mjs +225 -0
- package/lib/db/schema.mjs +102 -5
- package/lib/db/stores/peers.mjs +34 -0
- package/lib/integrations/peers/identity.mjs +62 -16
- package/lib/integrations/shims/script.mjs +111 -9
- package/lib/process/identity.mjs +222 -0
- package/lib/process/pty-lifecycle.mjs +149 -0
- package/lib/runtime/buffer-directory-lease.mjs +48 -0
- package/lib/runtime/buffer-evidence.mjs +336 -0
- package/lib/runtime/buffer-gc-protocol.mjs +167 -0
- package/lib/runtime/buffer-gc.mjs +270 -0
- package/lib/runtime/client.mjs +63 -7
- package/lib/runtime/fatal-shutdown.mjs +54 -0
- package/lib/runtime/project-path.mjs +139 -0
- package/lib/runtime/projects.mjs +320 -40
- package/lib/runtime/state.mjs +227 -27
- package/lib/shared/cleanup-error.mjs +54 -0
- package/lib/shared/clock-grace.mjs +106 -0
- package/lib/shared/errors.mjs +9 -0
- package/lib/shared/file-lock.mjs +377 -0
- package/lib/shared/json-file.mjs +6 -2
- package/lib/shared/redact.mjs +60 -0
- package/lib/shared/socket-lock-worker.mjs +250 -0
- package/lib/terminal/tmux.mjs +131 -27
- package/lib/tmux.mjs +4 -0
- package/lib/ui/help.mjs +53 -15
- package/lib/web/api-version.mjs +42 -0
- package/lib/web/auto-attach.mjs +185 -0
- package/lib/web/buffer-gc-runtime.mjs +175 -0
- package/lib/web/cookie-auth.mjs +119 -0
- package/lib/web/external-sessions.mjs +254 -0
- package/lib/web/http-routes.mjs +542 -0
- package/lib/web/http.mjs +135 -6
- package/lib/web/liveness-reaper.mjs +145 -0
- package/lib/web/peer-actions.mjs +132 -23
- package/lib/web/project-contexts.mjs +183 -0
- package/lib/web/pty-sessions.mjs +203 -0
- package/lib/web/runtime-main.mjs +717 -0
- package/lib/web/runtime.mjs +91 -36
- package/lib/web/session-serialize.mjs +0 -0
- package/lib/web/startup.mjs +335 -0
- package/lib/web/tls.mjs +365 -0
- package/lib/web/tmux-sessions.mjs +1361 -0
- package/lib/web/tmux-stream.mjs +166 -0
- package/lib/web/ui-template.mjs +152 -20
- package/package.json +6 -3
- package/scripts/check-free-identifiers.mjs +290 -0
- package/scripts/regression.mjs +5404 -273
- package/docs/architecture-split-review.md +0 -217
- package/docs/layout-split-stack-audit.md +0 -298
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { tx } from '../../db/schema.mjs';
|
|
2
|
+
import { replaceCleanupFailureRoot } from '../../shared/cleanup-error.mjs';
|
|
3
|
+
import {
|
|
4
|
+
CLOCK_GRACE_SEC,
|
|
5
|
+
readClockGraceUntil,
|
|
6
|
+
writeClockGraceUntil
|
|
7
|
+
} from '../../shared/clock-grace.mjs';
|
|
8
|
+
import { CliError } from '../../shared/errors.mjs';
|
|
9
|
+
import { renewOwnedLocks } from './lease-renewal.mjs';
|
|
10
|
+
|
|
11
|
+
const BACKWARD_TOLERANCE_SEC = 5;
|
|
12
|
+
// CS-01: a gap is only meaningful for a real wall-clock discontinuity. Ordinary
|
|
13
|
+
// forward progress between observations (the 30s reaper tick, on-demand lock
|
|
14
|
+
// ops) must NOT create or extend a gap — otherwise the merged window grows
|
|
15
|
+
// monotonically, every candidate boundary inside it re-crosses on every
|
|
16
|
+
// observation, unknown evidence opens a spurious grace window every 120s, and
|
|
17
|
+
// age-based GC is deferred forever. Steps larger than this threshold (a sleep,
|
|
18
|
+
// an NTP step, or a CLI invoked minutes apart) still produce a gap.
|
|
19
|
+
const FORWARD_GAP_THRESHOLD_SEC = 120;
|
|
20
|
+
const OPERATIONS = new Set(['ownership', 'gc']);
|
|
21
|
+
const EVIDENCE_STATES = new Set(['live', 'dead', 'unknown']);
|
|
22
|
+
|
|
23
|
+
export const CLOCK_SAFETY_PUBLIC_MESSAGE =
|
|
24
|
+
'Clock safety state could not be persisted; ownership was left unchanged';
|
|
25
|
+
|
|
26
|
+
export function clockSafetyUnavailable(error) {
|
|
27
|
+
const publicError = new CliError(
|
|
28
|
+
'CLOCK_SAFETY_UNAVAILABLE',
|
|
29
|
+
CLOCK_SAFETY_PUBLIC_MESSAGE
|
|
30
|
+
);
|
|
31
|
+
return replaceCleanupFailureRoot(error, publicError) || publicError;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function crossedBoundary(previous, current, boundary) {
|
|
35
|
+
return previous < boundary && current >= boundary;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function requireTimestamp(value, name) {
|
|
39
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
40
|
+
throw new TypeError(`${name} must be a non-negative safe integer`);
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function validateCandidates(candidates) {
|
|
46
|
+
if (!Array.isArray(candidates)) {
|
|
47
|
+
throw new TypeError('candidates must be an array');
|
|
48
|
+
}
|
|
49
|
+
return candidates.map((candidate, index) => {
|
|
50
|
+
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
|
|
51
|
+
throw new TypeError(`candidate ${index} must be an object`);
|
|
52
|
+
}
|
|
53
|
+
const boundary = requireTimestamp(candidate.boundary, `candidate ${index} boundary`);
|
|
54
|
+
if (!EVIDENCE_STATES.has(candidate.evidence)) {
|
|
55
|
+
throw new TypeError(`candidate ${index} evidence must be live, dead, or unknown`);
|
|
56
|
+
}
|
|
57
|
+
return { boundary, evidence: candidate.evidence };
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function validateGcCutoffs(gcCutoffs) {
|
|
62
|
+
if (!Array.isArray(gcCutoffs)) {
|
|
63
|
+
throw new TypeError('gcCutoffs must be an array');
|
|
64
|
+
}
|
|
65
|
+
return gcCutoffs.map((cutoff, index) => requireTimestamp(cutoff, `gcCutoff ${index}`));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function decideClockSafety(observation) {
|
|
69
|
+
if (!observation || typeof observation !== 'object' || Array.isArray(observation)) {
|
|
70
|
+
throw new TypeError('clock observation must be an object');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { previous, operation } = observation;
|
|
74
|
+
const current = requireTimestamp(observation.current, 'current');
|
|
75
|
+
if (!OPERATIONS.has(operation)) {
|
|
76
|
+
throw new TypeError('operation must be ownership or gc');
|
|
77
|
+
}
|
|
78
|
+
const candidates = validateCandidates(
|
|
79
|
+
observation.candidates === undefined ? [] : observation.candidates
|
|
80
|
+
);
|
|
81
|
+
const gcCutoffs = validateGcCutoffs(
|
|
82
|
+
observation.gcCutoffs === undefined ? [] : observation.gcCutoffs
|
|
83
|
+
);
|
|
84
|
+
const firstObservation = previous === undefined;
|
|
85
|
+
const previousSec = firstObservation ? undefined : requireTimestamp(previous, 'previous');
|
|
86
|
+
|
|
87
|
+
const backward = !firstObservation
|
|
88
|
+
&& current < previousSec - BACKWARD_TOLERANCE_SEC;
|
|
89
|
+
|
|
90
|
+
const hasUnknown = candidates.some(({ evidence }) => evidence === 'unknown');
|
|
91
|
+
if (backward && (operation === 'gc' || hasUnknown)) {
|
|
92
|
+
return { enterGrace: true, renewOwners: false, reason: 'clock-backward' };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const crossedCandidates = candidates.filter(({ boundary }) => firstObservation
|
|
96
|
+
? current >= boundary
|
|
97
|
+
: crossedBoundary(previousSec, current, boundary));
|
|
98
|
+
const renewOwners = crossedCandidates.some(({ evidence }) => evidence === 'live');
|
|
99
|
+
|
|
100
|
+
if (crossedCandidates.some(({ evidence }) => evidence === 'unknown')) {
|
|
101
|
+
return {
|
|
102
|
+
enterGrace: true,
|
|
103
|
+
renewOwners,
|
|
104
|
+
reason: firstObservation ? 'first-observation' : 'unknown-evidence'
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (operation === 'gc' && gcCutoffs.some((boundary) => (
|
|
109
|
+
firstObservation ? current >= boundary : crossedBoundary(previousSec, current, boundary)
|
|
110
|
+
))) {
|
|
111
|
+
return {
|
|
112
|
+
enterGrace: true,
|
|
113
|
+
renewOwners: false,
|
|
114
|
+
reason: firstObservation ? 'first-observation' : 'gc-cutoff'
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (renewOwners) {
|
|
119
|
+
return { enterGrace: false, renewOwners: true, reason: 'verified-live' };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (crossedCandidates.some(({ evidence }) => evidence === 'dead')) {
|
|
123
|
+
return { enterGrace: false, renewOwners: false, reason: 'verified-dead' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { enterGrace: false, renewOwners: false, reason: 'no-boundary-crossing' };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readObservedAt(db) {
|
|
130
|
+
const row = db.prepare("SELECT value FROM meta WHERE key = 'clock_last_observed_at'").get();
|
|
131
|
+
if (!row) return undefined;
|
|
132
|
+
if (typeof row.value !== 'string' || !/^(0|[1-9][0-9]*)$/.test(row.value)) {
|
|
133
|
+
throw new Error('invalid persisted clock observation watermark');
|
|
134
|
+
}
|
|
135
|
+
const value = Number(row.value);
|
|
136
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
137
|
+
throw new Error('invalid persisted clock observation watermark');
|
|
138
|
+
}
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function writeObservedAt(db, nowSec) {
|
|
143
|
+
db.prepare(`
|
|
144
|
+
INSERT INTO meta(key, value) VALUES ('clock_last_observed_at', ?)
|
|
145
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
146
|
+
`).run(String(nowSec));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function readPendingGap(db) {
|
|
150
|
+
const row = db.prepare("SELECT value FROM meta WHERE key = 'clock_pending_gap'").get();
|
|
151
|
+
if (!row) return null;
|
|
152
|
+
let gap;
|
|
153
|
+
try {
|
|
154
|
+
gap = JSON.parse(row.value);
|
|
155
|
+
} catch {
|
|
156
|
+
throw new Error('invalid persisted pending clock gap');
|
|
157
|
+
}
|
|
158
|
+
if (!gap || typeof gap !== 'object' || Array.isArray(gap) ||
|
|
159
|
+
!Number.isSafeInteger(gap.from) || gap.from < 0 ||
|
|
160
|
+
!Number.isSafeInteger(gap.to) || gap.to < gap.from ||
|
|
161
|
+
typeof gap.backward !== 'boolean' || typeof gap.first !== 'boolean') {
|
|
162
|
+
throw new Error('invalid persisted pending clock gap');
|
|
163
|
+
}
|
|
164
|
+
return { from: gap.from, to: gap.to, backward: gap.backward, first: gap.first };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function writePendingGap(db, gap) {
|
|
168
|
+
db.prepare(`
|
|
169
|
+
INSERT INTO meta(key, value) VALUES ('clock_pending_gap', ?)
|
|
170
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
171
|
+
`).run(JSON.stringify(gap));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function clearPendingGap(db) {
|
|
175
|
+
db.prepare("DELETE FROM meta WHERE key = 'clock_pending_gap'").run();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function clockJumpSpanSec(clockJump) {
|
|
179
|
+
const spanMs = Math.max(
|
|
180
|
+
Number(clockJump?.wallDeltaMs || 0),
|
|
181
|
+
Number(clockJump?.deltaMs || 0),
|
|
182
|
+
Math.abs(Number(clockJump?.driftMs || 0))
|
|
183
|
+
);
|
|
184
|
+
return Number.isFinite(spanMs) ? Math.ceil(spanMs / 1000) : 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function nextObservationGap(previous, current, clockJump) {
|
|
188
|
+
if (previous === undefined) {
|
|
189
|
+
return { from: 0, to: current, backward: false, first: true };
|
|
190
|
+
}
|
|
191
|
+
if (clockJump?.kind === 'forward') {
|
|
192
|
+
const spanSec = clockJumpSpanSec(clockJump);
|
|
193
|
+
return {
|
|
194
|
+
from: spanSec > 0 ? Math.max(0, current - spanSec) : 0,
|
|
195
|
+
to: current,
|
|
196
|
+
backward: false,
|
|
197
|
+
first: false
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (clockJump?.kind === 'backward' || current < previous - BACKWARD_TOLERANCE_SEC) {
|
|
201
|
+
return {
|
|
202
|
+
from: Math.min(previous, current),
|
|
203
|
+
to: Math.max(previous, current),
|
|
204
|
+
backward: true,
|
|
205
|
+
first: false
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
if (current > previous && current - previous > FORWARD_GAP_THRESHOLD_SEC) {
|
|
209
|
+
return { from: previous, to: current, backward: false, first: false };
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function mergePendingGap(persisted, observed) {
|
|
215
|
+
if (!persisted) return observed;
|
|
216
|
+
if (!observed) return persisted;
|
|
217
|
+
return {
|
|
218
|
+
from: Math.min(persisted.from, observed.from),
|
|
219
|
+
to: Math.max(persisted.to, observed.to),
|
|
220
|
+
backward: persisted.backward || observed.backward,
|
|
221
|
+
first: persisted.first || observed.first
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function candidateCrossed(previous, current, candidate) {
|
|
226
|
+
return previous === undefined
|
|
227
|
+
? current >= candidate.boundary
|
|
228
|
+
: crossedBoundary(previous, current, candidate.boundary);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function clockSafetyPreviewState(db, {
|
|
232
|
+
operation,
|
|
233
|
+
candidates = [],
|
|
234
|
+
gcCutoffs = [],
|
|
235
|
+
nowSec,
|
|
236
|
+
clockJump = null
|
|
237
|
+
} = {}) {
|
|
238
|
+
requireTimestamp(nowSec, 'nowSec');
|
|
239
|
+
if (clockJump !== null &&
|
|
240
|
+
(!clockJump || typeof clockJump !== 'object' ||
|
|
241
|
+
!['forward', 'backward'].includes(clockJump.kind))) {
|
|
242
|
+
throw new TypeError('clockJump must be a forward or backward observation');
|
|
243
|
+
}
|
|
244
|
+
const previous = readObservedAt(db);
|
|
245
|
+
const pendingGap = mergePendingGap(
|
|
246
|
+
readPendingGap(db),
|
|
247
|
+
nextObservationGap(previous, nowSec, clockJump)
|
|
248
|
+
);
|
|
249
|
+
const decisionCurrent = pendingGap?.backward ? nowSec : (pendingGap?.to ?? nowSec);
|
|
250
|
+
const decisionPrevious = pendingGap?.backward
|
|
251
|
+
? decisionCurrent + BACKWARD_TOLERANCE_SEC + 1
|
|
252
|
+
: pendingGap?.first
|
|
253
|
+
? undefined
|
|
254
|
+
: pendingGap?.from ?? previous;
|
|
255
|
+
let decision = decideClockSafety({
|
|
256
|
+
previous: decisionPrevious,
|
|
257
|
+
current: decisionCurrent,
|
|
258
|
+
operation,
|
|
259
|
+
candidates,
|
|
260
|
+
gcCutoffs
|
|
261
|
+
});
|
|
262
|
+
const liveCandidateCrossed = (candidate) => pendingGap?.backward
|
|
263
|
+
? candidate.boundary <= nowSec
|
|
264
|
+
: candidateCrossed(decisionPrevious, decisionCurrent, candidate);
|
|
265
|
+
const renewableLiveCandidates = candidates.filter((candidate) =>
|
|
266
|
+
candidate?.evidence === 'live' && liveCandidateCrossed(candidate));
|
|
267
|
+
if (renewableLiveCandidates.length > 0 && !decision.renewOwners) {
|
|
268
|
+
decision = {
|
|
269
|
+
...decision,
|
|
270
|
+
renewOwners: true,
|
|
271
|
+
reason: decision.reason === 'no-boundary-crossing' ? 'verified-live' : decision.reason
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
let graceUntil = readClockGraceUntil(db);
|
|
275
|
+
if (decision.enterGrace) graceUntil = Math.max(graceUntil, nowSec + CLOCK_GRACE_SEC);
|
|
276
|
+
return { decision, graceUntil, pendingGap, renewableLiveCandidates };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function previewClockSafety(db, options = {}) {
|
|
280
|
+
const { decision, graceUntil } = clockSafetyPreviewState(db, options);
|
|
281
|
+
return { decision, graceUntil };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function observeClockSafetyInTransaction(db, options = {}) {
|
|
285
|
+
if (!db?.isTransaction) {
|
|
286
|
+
throw new Error('clock safety observation requires an active transaction');
|
|
287
|
+
}
|
|
288
|
+
const nowSec = options.nowSec;
|
|
289
|
+
const {
|
|
290
|
+
decision,
|
|
291
|
+
graceUntil: previewGraceUntil,
|
|
292
|
+
pendingGap,
|
|
293
|
+
renewableLiveCandidates
|
|
294
|
+
} = clockSafetyPreviewState(db, options);
|
|
295
|
+
let graceUntil = readClockGraceUntil(db);
|
|
296
|
+
if (decision.enterGrace) {
|
|
297
|
+
graceUntil = writeClockGraceUntil(db, previewGraceUntil);
|
|
298
|
+
clearPendingGap(db);
|
|
299
|
+
} else if (pendingGap) {
|
|
300
|
+
writePendingGap(db, pendingGap);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let renewed = 0;
|
|
304
|
+
const liveOwners = new Set(renewableLiveCandidates
|
|
305
|
+
.filter((candidate) => typeof candidate.owner === 'string' && candidate.owner)
|
|
306
|
+
.map((candidate) => candidate.owner));
|
|
307
|
+
if (liveOwners.size > 0) {
|
|
308
|
+
for (const owner of liveOwners) {
|
|
309
|
+
renewed += renewOwnedLocks(db, {
|
|
310
|
+
owner,
|
|
311
|
+
nowSec,
|
|
312
|
+
includeExpired: true
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
writeObservedAt(db, nowSec);
|
|
318
|
+
return { decision, graceUntil, renewed };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function observeClockSafety(db, options = {}) {
|
|
322
|
+
return tx(db, () => observeClockSafetyInTransaction(db, options));
|
|
323
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const CURRENT_TMUX_AUTHORITIES = `
|
|
2
|
+
SELECT MAX(e.id)
|
|
3
|
+
FROM events e
|
|
4
|
+
JOIN peer_bindings b
|
|
5
|
+
ON b.peer = json_extract(e.payload, '$.target_peer')
|
|
6
|
+
WHERE e.type = 'tmux.session.attached'
|
|
7
|
+
AND b.transport = 'tmux'
|
|
8
|
+
AND (b.runtime_target IS NULL OR b.runtime_target = json_extract(e.payload, '$.pane'))
|
|
9
|
+
GROUP BY b.peer, b.runtime_target
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
export function pruneOldEventsPreservingTmuxAuthority(db, cutoff, options = {}) {
|
|
13
|
+
const predicate = `created_at < ? AND id NOT IN (${CURRENT_TMUX_AUTHORITIES})`;
|
|
14
|
+
const count = db.prepare(`SELECT COUNT(*) AS n FROM events WHERE ${predicate}`).get(cutoff).n;
|
|
15
|
+
if (!options.dryRun) db.prepare(`DELETE FROM events WHERE ${predicate}`).run(cutoff);
|
|
16
|
+
return Number(count || 0);
|
|
17
|
+
}
|