catence 0.2.1-beta.1 → 0.2.1-beta.3
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.
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Keep these values synchronized with release/manifest.json; release checks
|
|
4
4
|
* deliberately reject drift before a package can be published.
|
|
5
5
|
*/
|
|
6
|
-
export declare const CATENCE_RUNTIME_VERSION = "0.2.1-beta.
|
|
6
|
+
export declare const CATENCE_RUNTIME_VERSION = "0.2.1-beta.3";
|
|
7
7
|
export declare const CATENCE_PROTOCOL_VERSION = 1;
|
|
8
8
|
export type CatenceRuntimeCapabilities = {
|
|
9
9
|
mcp: true;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Keep these values synchronized with release/manifest.json; release checks
|
|
4
4
|
* deliberately reject drift before a package can be published.
|
|
5
5
|
*/
|
|
6
|
-
export const CATENCE_RUNTIME_VERSION = '0.2.1-beta.
|
|
6
|
+
export const CATENCE_RUNTIME_VERSION = '0.2.1-beta.3';
|
|
7
7
|
export const CATENCE_PROTOCOL_VERSION = 1;
|
|
8
8
|
export function catenceRuntimeHealth() {
|
|
9
9
|
return {
|
|
@@ -19,7 +19,7 @@ import { completeStravaAuthorization, disconnectStrava, getStravaAuthorizationUr
|
|
|
19
19
|
import { setManualActivityLink, unlinkActivitySource } from '../normalization/activities/linking.js';
|
|
20
20
|
import { importSourceEntity } from '../normalization/normalizers.js';
|
|
21
21
|
import { CatenceDatabase, openReadOnlyRepository, ReadOnlyDatabaseError } from '../storage/database.js';
|
|
22
|
-
import { mergeProgress, ProgressPump, readRunningProgress, removeProgressSidecar } from '../storage/progress-sidecar.js';
|
|
22
|
+
import { mergeProgress, ProgressPump, readRunningProgress, removeProgressSidecar, writeProgressSidecar, PROGRESS_KEEPALIVE_MS } from '../storage/progress-sidecar.js';
|
|
23
23
|
import { withDataWriteLock } from '../storage/write-lock.js';
|
|
24
24
|
function packageRoot() {
|
|
25
25
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..');
|
|
@@ -430,8 +430,42 @@ export async function syncData(paths, provider, explicitFrom, advanceCursor = tr
|
|
|
430
430
|
runs.push(await syncProvider(database, paths, source, { explicitFrom, toDate, advanceCursor, refreshActivities, backfill }, log));
|
|
431
431
|
const executedRuns = runs.filter((run) => !run.skipped);
|
|
432
432
|
if (executedRuns.length > 0) {
|
|
433
|
-
|
|
434
|
-
|
|
433
|
+
// The per-provider pumps already stopped (sidecars removed, runs marked
|
|
434
|
+
// completed), but buildRetrievalIndex still holds the single-writer DB
|
|
435
|
+
// lock, so /dashboard reads as unavailable. Hold a non-terminal sidecar
|
|
436
|
+
// for the last executed run so /sync/status keeps reporting running and
|
|
437
|
+
// the Status Sync button stays disabled through local post-processing.
|
|
438
|
+
const last = [...executedRuns].reverse().find((run) => run.runId);
|
|
439
|
+
let indexingKeepalive = null;
|
|
440
|
+
const holdIndexingProgress = () => last?.runId
|
|
441
|
+
? writeProgressSidecar(paths, last.runId, {
|
|
442
|
+
...baseProgress(last.runId, last.provider, 'importing'),
|
|
443
|
+
currentStep: 'building retrieval index',
|
|
444
|
+
percentComplete: 99,
|
|
445
|
+
}).catch((error) => {
|
|
446
|
+
log.debug('Indexing progress sidecar write failed', {
|
|
447
|
+
runId: last.runId,
|
|
448
|
+
error: error instanceof Error ? error.message : String(error),
|
|
449
|
+
});
|
|
450
|
+
})
|
|
451
|
+
: Promise.resolve();
|
|
452
|
+
if (last?.runId) {
|
|
453
|
+
await holdIndexingProgress();
|
|
454
|
+
indexingKeepalive = setInterval(() => {
|
|
455
|
+
void holdIndexingProgress();
|
|
456
|
+
}, PROGRESS_KEEPALIVE_MS);
|
|
457
|
+
indexingKeepalive.unref?.();
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
const index = await buildRetrievalIndex(database);
|
|
461
|
+
log.info('Rebuilt retrieval index after sync', { documents: index.documents, mode: index.mode, watermark: index.watermark });
|
|
462
|
+
}
|
|
463
|
+
finally {
|
|
464
|
+
if (indexingKeepalive)
|
|
465
|
+
clearInterval(indexingKeepalive);
|
|
466
|
+
if (last?.runId)
|
|
467
|
+
await removeProgressSidecar(paths, last.runId).catch(() => undefined);
|
|
468
|
+
}
|
|
435
469
|
}
|
|
436
470
|
return { runIds: runs.flatMap((run) => run.runId ? [run.runId] : []), runs, timedOutRuns: timedOutRuns.runIds };
|
|
437
471
|
}
|
|
@@ -48,7 +48,16 @@ export declare class ProgressPump {
|
|
|
48
48
|
private lastHeartbeatAt;
|
|
49
49
|
private interval;
|
|
50
50
|
private stageOverride;
|
|
51
|
+
/**
|
|
52
|
+
* Serializes heartbeat writes in call order. publish() used to fire its
|
|
53
|
+
* database and sidecar writes without awaiting them, so a throttled write
|
|
54
|
+
* could win the sidecar rename race against a later publishFinal() and
|
|
55
|
+
* leave a stale non-terminal stage behind.
|
|
56
|
+
*/
|
|
57
|
+
private writeTail;
|
|
51
58
|
constructor(runId: string, provider: string, options: ProgressPumpOptions);
|
|
59
|
+
private enqueueHeartbeat;
|
|
60
|
+
private writeHeartbeat;
|
|
52
61
|
start(): void;
|
|
53
62
|
publish(state: SyncProgressState): void;
|
|
54
63
|
/** Force subsequent keep-alive heartbeats to report the given stage. */
|
|
@@ -133,11 +133,36 @@ export class ProgressPump {
|
|
|
133
133
|
lastHeartbeatAt = 0;
|
|
134
134
|
interval = null;
|
|
135
135
|
stageOverride = null;
|
|
136
|
+
/**
|
|
137
|
+
* Serializes heartbeat writes in call order. publish() used to fire its
|
|
138
|
+
* database and sidecar writes without awaiting them, so a throttled write
|
|
139
|
+
* could win the sidecar rename race against a later publishFinal() and
|
|
140
|
+
* leave a stale non-terminal stage behind.
|
|
141
|
+
*/
|
|
142
|
+
writeTail = Promise.resolve();
|
|
136
143
|
constructor(runId, provider, options) {
|
|
137
144
|
this.runId = runId;
|
|
138
145
|
this.provider = provider;
|
|
139
146
|
this.options = options;
|
|
140
147
|
}
|
|
148
|
+
enqueueHeartbeat(state) {
|
|
149
|
+
const run = this.writeTail.then(() => this.writeHeartbeat(state));
|
|
150
|
+
// A write failure is already logged inside writeHeartbeat; never let it
|
|
151
|
+
// break the chain (or surface as an unhandled rejection) for later beats.
|
|
152
|
+
this.writeTail = run.then(() => undefined, () => undefined);
|
|
153
|
+
return run;
|
|
154
|
+
}
|
|
155
|
+
async writeHeartbeat(state) {
|
|
156
|
+
const { database, paths, log } = this.options;
|
|
157
|
+
await Promise.all([
|
|
158
|
+
database.heartbeatRun(this.runId, state).catch((error) => {
|
|
159
|
+
log.debug('Progress heartbeat failed', { runId: this.runId, error: error instanceof Error ? error.message : String(error) });
|
|
160
|
+
}),
|
|
161
|
+
writeProgressSidecar(paths, this.runId, state).catch((error) => {
|
|
162
|
+
log.debug('Progress sidecar write failed', { runId: this.runId, error: error instanceof Error ? error.message : String(error) });
|
|
163
|
+
}),
|
|
164
|
+
]);
|
|
165
|
+
}
|
|
141
166
|
start() {
|
|
142
167
|
if (this.interval)
|
|
143
168
|
return;
|
|
@@ -156,13 +181,7 @@ export class ProgressPump {
|
|
|
156
181
|
}
|
|
157
182
|
this.lastHeartbeatAt = now;
|
|
158
183
|
this.lastProgress = state;
|
|
159
|
-
|
|
160
|
-
void database.heartbeatRun(this.runId, state).catch((error) => {
|
|
161
|
-
log.debug('Progress heartbeat failed', { runId: this.runId, error: error instanceof Error ? error.message : String(error) });
|
|
162
|
-
});
|
|
163
|
-
void writeProgressSidecar(paths, this.runId, state).catch((error) => {
|
|
164
|
-
log.debug('Progress sidecar write failed', { runId: this.runId, error: error instanceof Error ? error.message : String(error) });
|
|
165
|
-
});
|
|
184
|
+
void this.enqueueHeartbeat(state).catch(() => undefined);
|
|
166
185
|
}
|
|
167
186
|
/** Force subsequent keep-alive heartbeats to report the given stage. */
|
|
168
187
|
setStage(stage) {
|
|
@@ -180,15 +199,11 @@ export class ProgressPump {
|
|
|
180
199
|
async publishFinal(state) {
|
|
181
200
|
this.lastHeartbeatAt = Date.now();
|
|
182
201
|
this.lastProgress = state;
|
|
183
|
-
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
});
|
|
189
|
-
await writeProgressSidecar(paths, this.runId, state).catch((error) => {
|
|
190
|
-
log.debug('Progress sidecar write failed', { runId: this.runId, error: error instanceof Error ? error.message : String(error) });
|
|
191
|
-
});
|
|
202
|
+
// Awaiting the chained write guarantees every earlier heartbeat (in
|
|
203
|
+
// particular a throttled publish still in flight) lands first, so the
|
|
204
|
+
// terminal stage is always the last one persisted before the caller
|
|
205
|
+
// removes the sidecar.
|
|
206
|
+
await this.enqueueHeartbeat(state);
|
|
192
207
|
}
|
|
193
208
|
stop() {
|
|
194
209
|
if (this.interval) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "catence",
|
|
3
|
-
"version": "0.2.1-beta.
|
|
3
|
+
"version": "0.2.1-beta.3",
|
|
4
4
|
"description": "Local MCP access to normalized Garmin, Intervals.icu, and targeted Strava fitness data.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"test:watch": "vitest",
|
|
56
56
|
"catence-data": "tsx src/interfaces/cli/main.ts",
|
|
57
57
|
"mcp": "tsx src/interfaces/mcp/main.ts",
|
|
58
|
+
"dev": "node scripts/dev.mjs",
|
|
58
59
|
"console": "uv run --project console catence-console serve",
|
|
59
60
|
"console:doctor": "uv run --project console catence-console doctor",
|
|
60
61
|
"discover:opencode-go": "node scripts/discover-opencode-go.mjs",
|
|
@@ -79,11 +80,12 @@
|
|
|
79
80
|
"@types/node": "^24.3.0",
|
|
80
81
|
"tsx": "^4.20.5",
|
|
81
82
|
"typescript": "^5.9.2",
|
|
82
|
-
"vitest": "^
|
|
83
|
+
"vitest": "^5.0.0"
|
|
83
84
|
},
|
|
84
85
|
"overrides": {
|
|
85
|
-
"fast-uri": "3.1.
|
|
86
|
-
"hono": "4.
|
|
87
|
-
"nanoid": "3.3.18"
|
|
86
|
+
"fast-uri": "3.1.7",
|
|
87
|
+
"hono": "4.13.7",
|
|
88
|
+
"nanoid": "3.3.18",
|
|
89
|
+
"qs": "6.16.0"
|
|
88
90
|
}
|
|
89
91
|
}
|