catence 0.2.0-beta.8 → 0.2.0-beta.9
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/README.md +16 -2
- package/config.example.json +5 -1
- package/dist/contracts/release.d.ts +1 -1
- package/dist/contracts/release.js +1 -1
- package/dist/core/logging.d.ts +16 -0
- package/dist/core/logging.js +68 -0
- package/dist/elt/application/management.js +20 -6
- package/dist/elt/ingestion/importer.d.ts +3 -2
- package/dist/elt/ingestion/importer.js +4 -3
- package/dist/elt/ingestion/providers/intervals.d.ts +3 -1
- package/dist/elt/ingestion/providers/intervals.js +4 -1
- package/dist/elt/ingestion/providers/strava/service.js +7 -1
- package/dist/elt/storage/database.js +2 -0
- package/docs/llm-providers.md +43 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -215,7 +215,9 @@ docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.e
|
|
|
215
215
|
|
|
216
216
|
### Per-model reasoning effort
|
|
217
217
|
|
|
218
|
-
Models that support reasoning effort
|
|
218
|
+
Models that support reasoning effort can set a fixed level per model in `config.json`, or expose a per-chat **Thinking effort** dropdown with custom levels via `variants`.
|
|
219
|
+
|
|
220
|
+
**Fixed level** (all requests for the model use it):
|
|
219
221
|
|
|
220
222
|
```json
|
|
221
223
|
{
|
|
@@ -239,7 +241,19 @@ Models that support reasoning effort (e.g., DeepSeek V4 Flash on Opencode Zen) c
|
|
|
239
241
|
}
|
|
240
242
|
```
|
|
241
243
|
|
|
242
|
-
|
|
244
|
+
**Custom variants** (per-chat dropdown): `variants` maps each dropdown label to the value sent to the provider. This is how non-OpenAI models (for example DeepSeek V4 Flash, which uses its own Default/High/Max scheme rather than the OpenAI-standard list) expose their native levels:
|
|
245
|
+
|
|
246
|
+
```json
|
|
247
|
+
"deepseek-v4-flash-free": {
|
|
248
|
+
"label": "DeepSeek V4 Flash Free",
|
|
249
|
+
"model": "openai/deepseek-v4-flash-free",
|
|
250
|
+
"variants": { "High": "high", "Max": "max" }
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
With `variants`, the chat **Thinking effort** dropdown shows `Provider default` plus each label; selecting one sends that exact value (e.g. `reasoning_effort: "high"`). Without `variants`, the dropdown falls back to the OpenAI-standard set (`minimal`, `low`, `medium`, `high`, `xhigh`).
|
|
255
|
+
|
|
256
|
+
A fixed `reasoningEffort` overrides any profile-level `defaultReasoningEffort`; the per-chat dropdown selection (a variant value) overrides both. If nothing is set, the provider default is used.
|
|
243
257
|
|
|
244
258
|
For source development:
|
|
245
259
|
|
package/config.example.json
CHANGED
|
@@ -49,7 +49,11 @@
|
|
|
49
49
|
"label": "OpenCode Zen",
|
|
50
50
|
"defaultModel": "deepseek-v4-flash-free",
|
|
51
51
|
"models": {
|
|
52
|
-
"deepseek-v4-flash-free": {
|
|
52
|
+
"deepseek-v4-flash-free": {
|
|
53
|
+
"label": "DeepSeek V4 Flash Free",
|
|
54
|
+
"model": "openai/deepseek-v4-flash-free",
|
|
55
|
+
"variants": { "High": "high", "Max": "max" }
|
|
56
|
+
},
|
|
53
57
|
"nemotron-3-ultra-free": { "label": "Nemotron 3 Ultra Free", "model": "openai/nemotron-3-ultra-free" },
|
|
54
58
|
"big-pickle": { "label": "Big Pickle", "model": "openai/big-pickle" }
|
|
55
59
|
},
|
|
@@ -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.0-beta.
|
|
6
|
+
export declare const CATENCE_RUNTIME_VERSION = "0.2.0-beta.9";
|
|
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.0-beta.
|
|
6
|
+
export const CATENCE_RUNTIME_VERSION = '0.2.0-beta.9';
|
|
7
7
|
export const CATENCE_PROTOCOL_VERSION = 1;
|
|
8
8
|
export function catenceRuntimeHealth() {
|
|
9
9
|
return {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CatencePaths } from '../contracts/runtime.js';
|
|
2
|
+
export type SyncLogger = {
|
|
3
|
+
info: (message: string, context?: Record<string, unknown>) => void;
|
|
4
|
+
warn: (message: string, context?: Record<string, unknown>) => void;
|
|
5
|
+
error: (message: string, context?: Record<string, unknown>) => void;
|
|
6
|
+
debug: (message: string, context?: Record<string, unknown>) => void;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the sync log file path.
|
|
10
|
+
*
|
|
11
|
+
* `CATENCE_LOG` may be an absolute path or a path relative to the catalog home.
|
|
12
|
+
* When unset (the default), logs go to stderr only so the console stays clean
|
|
13
|
+
* and stdout remains machine-readable for callers like sync.sh.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveLogFile(paths: CatencePaths, environment?: NodeJS.ProcessEnv): string | null;
|
|
16
|
+
export declare function createSyncLogger(paths: CatencePaths, environment?: NodeJS.ProcessEnv): SyncLogger;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { appendFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const LEVEL_WEIGHT = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
4
|
+
function configuredLevel(environment = process.env) {
|
|
5
|
+
const raw = (environment.CATENCE_LOG_LEVEL ?? 'info').toLowerCase();
|
|
6
|
+
if (raw === 'debug')
|
|
7
|
+
return LEVEL_WEIGHT.debug;
|
|
8
|
+
if (raw === 'warn')
|
|
9
|
+
return LEVEL_WEIGHT.warn;
|
|
10
|
+
if (raw === 'error')
|
|
11
|
+
return LEVEL_WEIGHT.error;
|
|
12
|
+
return LEVEL_WEIGHT.info;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the sync log file path.
|
|
16
|
+
*
|
|
17
|
+
* `CATENCE_LOG` may be an absolute path or a path relative to the catalog home.
|
|
18
|
+
* When unset (the default), logs go to stderr only so the console stays clean
|
|
19
|
+
* and stdout remains machine-readable for callers like sync.sh.
|
|
20
|
+
*/
|
|
21
|
+
export function resolveLogFile(paths, environment = process.env) {
|
|
22
|
+
const configured = environment.CATENCE_LOG;
|
|
23
|
+
if (!configured)
|
|
24
|
+
return null;
|
|
25
|
+
return path.isAbsolute(configured) ? configured : path.join(paths.root, configured);
|
|
26
|
+
}
|
|
27
|
+
export function createSyncLogger(paths, environment = process.env) {
|
|
28
|
+
const threshold = configuredLevel(environment);
|
|
29
|
+
const logFile = resolveLogFile(paths, environment);
|
|
30
|
+
const write = (level, message, context) => {
|
|
31
|
+
if (LEVEL_WEIGHT[level] < threshold)
|
|
32
|
+
return;
|
|
33
|
+
const line = formatLogLine(level, message, context);
|
|
34
|
+
process.stderr.write(`${line}\n`);
|
|
35
|
+
if (logFile) {
|
|
36
|
+
// Fire-and-forget: never block a sync run on log persistence.
|
|
37
|
+
appendLogLine(logFile, `${line}\n`).catch(() => { });
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
info: (message, context) => write('info', message, context),
|
|
42
|
+
warn: (message, context) => write('warn', message, context),
|
|
43
|
+
error: (message, context) => write('error', message, context),
|
|
44
|
+
debug: (message, context) => write('debug', message, context),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function formatLogLine(level, message, context) {
|
|
48
|
+
const timestamp = new Date().toISOString();
|
|
49
|
+
const tag = level.toUpperCase().padEnd(5);
|
|
50
|
+
let line = `[${timestamp}] [${tag}] ${message}`;
|
|
51
|
+
if (context && Object.keys(context).length > 0) {
|
|
52
|
+
line += ` ${serializeContext(context)}`;
|
|
53
|
+
}
|
|
54
|
+
return line;
|
|
55
|
+
}
|
|
56
|
+
function serializeContext(context) {
|
|
57
|
+
try {
|
|
58
|
+
return JSON.stringify(context);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
let appendLogLine = async (file, line) => {
|
|
65
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
66
|
+
await appendFile(file, line, 'utf8');
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=logging.js.map
|
|
@@ -8,6 +8,7 @@ import { promisify } from 'node:util';
|
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { buildRetrievalIndex } from '../../core/retrieval/index.js';
|
|
10
10
|
import { defaultFromDate, ensurePaths, requireIntervalsConfig } from '../../core/runtime/configuration.js';
|
|
11
|
+
import { createSyncLogger } from '../../core/logging.js';
|
|
11
12
|
import { athleteProviderEnvironment } from '../../core/runtime/secrets.js';
|
|
12
13
|
import { importJsonl } from '../ingestion/importer.js';
|
|
13
14
|
import { IntervalsExtractor } from '../ingestion/providers/intervals.js';
|
|
@@ -19,7 +20,7 @@ const execFileAsync = promisify(execFile);
|
|
|
19
20
|
function packageRoot() {
|
|
20
21
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..');
|
|
21
22
|
}
|
|
22
|
-
async function syncProvider(database, paths, provider, options) {
|
|
23
|
+
async function syncProvider(database, paths, provider, options, log) {
|
|
23
24
|
const explicitWindow = options.explicitFrom ? { fromDate: options.explicitFrom, toDate: options.toDate, source: 'explicit' } : null;
|
|
24
25
|
const uncoveredExplicitWindow = async (cursorName) => {
|
|
25
26
|
if (!explicitWindow)
|
|
@@ -33,6 +34,7 @@ async function syncProvider(database, paths, provider, options) {
|
|
|
33
34
|
const activityWindow = explicitWindow ? await uncoveredExplicitWindow('activities') : await database.resolveIncrementalWindow(provider, 'activities', options.toDate, defaultFromDate(), 14);
|
|
34
35
|
const hasProviderWork = provider === 'intervals' ? activityWindow !== null : dailyWindow !== null || activityWindow !== null;
|
|
35
36
|
if (!hasProviderWork) {
|
|
37
|
+
log.debug('No uncovered work; skipping provider', { provider, daily: dailyWindow ?? null, activities: activityWindow ?? null });
|
|
36
38
|
return {
|
|
37
39
|
runId: null, provider, fromDate: null,
|
|
38
40
|
dailyFromDate: dailyWindow?.fromDate ?? null, dailyToDate: dailyWindow?.toDate ?? null,
|
|
@@ -43,13 +45,19 @@ async function syncProvider(database, paths, provider, options) {
|
|
|
43
45
|
}
|
|
44
46
|
const fromDate = [dailyWindow?.fromDate, activityWindow?.fromDate].filter((date) => Boolean(date)).sort()[0];
|
|
45
47
|
const runId = await database.beginRun(provider, fromDate);
|
|
48
|
+
log.info('Starting sync run', {
|
|
49
|
+
runId, provider, fromDate, toDate: options.toDate,
|
|
50
|
+
dailyFrom: dailyWindow?.fromDate ?? null, dailyTo: dailyWindow?.toDate ?? null,
|
|
51
|
+
activityFrom: activityWindow?.fromDate ?? null, activityTo: activityWindow?.toDate ?? null,
|
|
52
|
+
backfill: options.backfill, refreshActivities: options.refreshActivities,
|
|
53
|
+
});
|
|
46
54
|
let extractionStarted = false;
|
|
47
55
|
try {
|
|
48
56
|
if (provider === 'intervals') {
|
|
49
57
|
const environment = await athleteProviderEnvironment(paths);
|
|
50
58
|
const config = requireIntervalsConfig(environment);
|
|
51
59
|
extractionStarted = true;
|
|
52
|
-
await new IntervalsExtractor(database, paths, config.apiKey, config.athleteId).sync(runId, activityWindow, !options.backfill || options.refreshActivities);
|
|
60
|
+
await new IntervalsExtractor(database, paths, config.apiKey, config.athleteId, log).sync(runId, activityWindow, !options.backfill || options.refreshActivities);
|
|
53
61
|
}
|
|
54
62
|
else {
|
|
55
63
|
const output = path.join(paths.staging, 'garmin', `${runId}.jsonl`);
|
|
@@ -77,14 +85,18 @@ async function syncProvider(database, paths, provider, options) {
|
|
|
77
85
|
});
|
|
78
86
|
if (!existsSync(output))
|
|
79
87
|
throw new Error('Garmin staging worker completed without writing a JSONL manifest.');
|
|
80
|
-
await importJsonl(database, runId, output);
|
|
88
|
+
await importJsonl(database, runId, output, log);
|
|
81
89
|
}
|
|
82
90
|
}
|
|
83
91
|
catch (error) {
|
|
84
|
-
|
|
92
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
93
|
+
log.error('Sync run failed', { runId, provider, error: message, stack: error instanceof Error ? error.stack : undefined });
|
|
94
|
+
await database.addError(runId, provider, 'sync', null, message, true);
|
|
85
95
|
}
|
|
86
96
|
finally {
|
|
87
97
|
await database.finishRun(runId);
|
|
98
|
+
const [run] = await database.rows('SELECT status, error_count FROM sync_runs WHERE run_id = $runId', { runId });
|
|
99
|
+
log.info('Sync run finished', { runId, provider, status: run?.status ?? 'unknown', errorCount: run?.error_count ?? 0 });
|
|
88
100
|
}
|
|
89
101
|
if (options.advanceCursor && !options.explicitFrom && extractionStarted) {
|
|
90
102
|
await database.advanceIncrementalCursor(provider, 'daily', runId, options.toDate, 3);
|
|
@@ -109,12 +121,13 @@ export async function syncData(paths, provider, explicitFrom, advanceCursor = tr
|
|
|
109
121
|
if (provider === 'strava')
|
|
110
122
|
return { provider, result: await syncStravaGear(paths) };
|
|
111
123
|
const database = await CatenceDatabase.open(paths);
|
|
124
|
+
const log = createSyncLogger(paths);
|
|
112
125
|
try {
|
|
113
126
|
const providers = provider === 'all' ? ['intervals', 'garmin'] : [provider];
|
|
114
127
|
const toDate = new Date().toISOString().slice(0, 10);
|
|
115
128
|
const runs = [];
|
|
116
129
|
for (const source of providers)
|
|
117
|
-
runs.push(await syncProvider(database, paths, source, { explicitFrom, toDate, advanceCursor, refreshActivities, backfill }));
|
|
130
|
+
runs.push(await syncProvider(database, paths, source, { explicitFrom, toDate, advanceCursor, refreshActivities, backfill }, log));
|
|
118
131
|
return { runIds: runs.flatMap((run) => run.runId ? [run.runId] : []), runs };
|
|
119
132
|
}
|
|
120
133
|
finally {
|
|
@@ -130,7 +143,8 @@ export async function retryDataSync(paths, previousRunId) {
|
|
|
130
143
|
throw new Error(`No sync run found for ${previousRunId}.`);
|
|
131
144
|
if (previous.provider === 'strava')
|
|
132
145
|
throw new Error('Retry Strava gear sync with `catence-data sync --provider strava`.');
|
|
133
|
-
const
|
|
146
|
+
const log = createSyncLogger(paths);
|
|
147
|
+
const run = await syncProvider(database, paths, previous.provider, { explicitFrom: previous.from_date, toDate: new Date().toISOString().slice(0, 10), advanceCursor: false, refreshActivities: false, backfill: false }, log);
|
|
134
148
|
return { retriedRun: previousRunId, runId: run.runId, run };
|
|
135
149
|
}
|
|
136
150
|
finally {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { CatenceDatabase } from '../storage/database.js';
|
|
2
2
|
import { type StagingRecord } from '../../contracts/staging.js';
|
|
3
|
-
|
|
4
|
-
export declare function
|
|
3
|
+
import type { SyncLogger } from '../../core/logging.js';
|
|
4
|
+
export declare function importRecord(database: CatenceDatabase, runId: string, record: StagingRecord, log?: SyncLogger): Promise<void>;
|
|
5
|
+
export declare function importJsonl(database: CatenceDatabase, runId: string, filePath: string, log?: SyncLogger): Promise<number>;
|
|
5
6
|
export declare function importSingleStagingFile(database: CatenceDatabase, runId: string, filePath: string): Promise<number>;
|
|
6
7
|
export declare function queueWorkItem(database: CatenceDatabase, runId: string, provider: 'intervals' | 'garmin' | 'strava', endpoint: string, remoteId: string | null, scope: unknown): Promise<string>;
|
|
7
8
|
export declare function completeWorkItem(database: CatenceDatabase, itemId: string): Promise<void>;
|
|
@@ -7,7 +7,7 @@ import { importSourceEntity, refreshActivityQuality } from '../normalization/nor
|
|
|
7
7
|
import { normalizeStravaEntity } from '../normalization/segments/strava-normalizer.js';
|
|
8
8
|
import { reconcileActivityLink } from '../normalization/activities/linking.js';
|
|
9
9
|
import { json } from '../storage/sql.js';
|
|
10
|
-
export async function importRecord(database, runId, record) {
|
|
10
|
+
export async function importRecord(database, runId, record, log) {
|
|
11
11
|
if (record.kind === 'run_manifest') {
|
|
12
12
|
if (record.runId !== runId)
|
|
13
13
|
throw new Error(`Staging manifest run ${record.runId} does not match importer run ${runId}.`);
|
|
@@ -54,8 +54,9 @@ export async function importRecord(database, runId, record) {
|
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
56
|
await database.addError(runId, record.provider, record.endpoint, record.remoteId, record.message, record.retryable);
|
|
57
|
+
log?.warn('Staging record reported an extraction error', { runId, provider: record.provider, endpoint: record.endpoint, remoteId: record.remoteId, error: record.message });
|
|
57
58
|
}
|
|
58
|
-
export async function importJsonl(database, runId, filePath) {
|
|
59
|
+
export async function importJsonl(database, runId, filePath, log) {
|
|
59
60
|
let count = 0;
|
|
60
61
|
const file = createReadStream(filePath, { encoding: 'utf8' });
|
|
61
62
|
const lines = createInterface({ input: file, crlfDelay: Infinity });
|
|
@@ -63,7 +64,7 @@ export async function importJsonl(database, runId, filePath) {
|
|
|
63
64
|
if (!line.trim())
|
|
64
65
|
continue;
|
|
65
66
|
const record = stagingRecordSchema.parse(JSON.parse(line));
|
|
66
|
-
await importRecord(database, runId, record);
|
|
67
|
+
await importRecord(database, runId, record, log);
|
|
67
68
|
count += 1;
|
|
68
69
|
}
|
|
69
70
|
return count;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { CatenceDatabase } from '../../storage/database.js';
|
|
2
2
|
import type { CatencePaths } from '../../../contracts/runtime.js';
|
|
3
|
+
import type { SyncLogger } from '../../../core/logging.js';
|
|
3
4
|
export declare class IntervalsExtractor {
|
|
4
5
|
private readonly database;
|
|
5
6
|
private readonly paths;
|
|
6
7
|
private readonly apiKey;
|
|
7
8
|
private readonly athleteId;
|
|
9
|
+
private readonly log;
|
|
8
10
|
private readonly client;
|
|
9
|
-
constructor(database: CatenceDatabase, paths: CatencePaths, apiKey: string, athleteId: string);
|
|
11
|
+
constructor(database: CatenceDatabase, paths: CatencePaths, apiKey: string, athleteId: string, log: SyncLogger);
|
|
10
12
|
sync(runId: string, activityWindow: {
|
|
11
13
|
fromDate: string;
|
|
12
14
|
toDate: string;
|
|
@@ -42,12 +42,14 @@ export class IntervalsExtractor {
|
|
|
42
42
|
paths;
|
|
43
43
|
apiKey;
|
|
44
44
|
athleteId;
|
|
45
|
+
log;
|
|
45
46
|
client;
|
|
46
|
-
constructor(database, paths, apiKey, athleteId) {
|
|
47
|
+
constructor(database, paths, apiKey, athleteId, log) {
|
|
47
48
|
this.database = database;
|
|
48
49
|
this.paths = paths;
|
|
49
50
|
this.apiKey = apiKey;
|
|
50
51
|
this.athleteId = athleteId;
|
|
52
|
+
this.log = log;
|
|
51
53
|
// The typed client is used for provider-compatible authentication and the
|
|
52
54
|
// primary identity request. Manifest-only calls below intentionally use
|
|
53
55
|
// the same read-only credentials to cover endpoint families not wrapped by it.
|
|
@@ -189,6 +191,7 @@ export class IntervalsExtractor {
|
|
|
189
191
|
catch (error) {
|
|
190
192
|
const message = error instanceof Error ? error.message : String(error);
|
|
191
193
|
await failWorkItem(this.database, workId, message);
|
|
194
|
+
this.log.warn('Endpoint capture failed', { runId, provider: 'intervals', endpoint, remoteId, error: message });
|
|
192
195
|
await this.database.addError(runId, 'intervals', endpoint, remoteId, message, true);
|
|
193
196
|
return undefined;
|
|
194
197
|
}
|
|
@@ -8,6 +8,7 @@ import { ensurePaths, loadCatenceConfig } from '../../../../core/runtime/configu
|
|
|
8
8
|
import { athleteProviderEnvironment } from '../../../../core/runtime/secrets.js';
|
|
9
9
|
import { CatenceDatabase, openReadOnlyRepository } from '../../../storage/database.js';
|
|
10
10
|
import { importJsonl } from '../../importer.js';
|
|
11
|
+
import { createSyncLogger } from '../../../../core/logging.js';
|
|
11
12
|
import { withDataWriteLock } from '../../../storage/write-lock.js';
|
|
12
13
|
const execFileAsync = promisify(execFile);
|
|
13
14
|
export class StravaRateLimitError extends Error {
|
|
@@ -108,6 +109,7 @@ async function cachedState(database, resourceType, remoteId) {
|
|
|
108
109
|
async function runReadOperation(paths, operation, extraArgs, resource, refresh = false) {
|
|
109
110
|
return withDataWriteLock(paths, async () => {
|
|
110
111
|
await ensurePaths(paths);
|
|
112
|
+
const log = createSyncLogger(paths);
|
|
111
113
|
const database = await CatenceDatabase.open(paths);
|
|
112
114
|
try {
|
|
113
115
|
if (!refresh) {
|
|
@@ -117,6 +119,7 @@ async function runReadOperation(paths, operation, extraArgs, resource, refresh =
|
|
|
117
119
|
}
|
|
118
120
|
await checkBudget(database, paths);
|
|
119
121
|
const runId = await database.beginRun('strava', new Date().toISOString().slice(0, 10));
|
|
122
|
+
log.info('Starting Strava read operation', { runId, operation, resource: resource.id });
|
|
120
123
|
const directory = path.join(paths.staging, 'strava');
|
|
121
124
|
const stagingPath = path.join(directory, `${runId}.jsonl`);
|
|
122
125
|
const resultPath = path.join(directory, `${runId}.result.json`);
|
|
@@ -124,17 +127,20 @@ async function runReadOperation(paths, operation, extraArgs, resource, refresh =
|
|
|
124
127
|
try {
|
|
125
128
|
result = await invokeWorker(paths, ['--mode', operation, '--run-id', runId, '--output', stagingPath, ...extraArgs], resultPath);
|
|
126
129
|
if (existsSync(stagingPath))
|
|
127
|
-
await importJsonl(database, runId, stagingPath);
|
|
130
|
+
await importJsonl(database, runId, stagingPath, log);
|
|
128
131
|
await storeRateState(database, result);
|
|
129
132
|
await writeState(database, resource.type, resource.id, result);
|
|
130
133
|
}
|
|
131
134
|
catch (error) {
|
|
132
135
|
const message = error instanceof Error ? error.message : String(error);
|
|
136
|
+
log.error('Strava read operation failed', { runId, operation, resource: resource.id, error: message, stack: error instanceof Error ? error.stack : undefined });
|
|
133
137
|
await database.addError(runId, 'strava', operation, resource.id, message, true);
|
|
134
138
|
throw error;
|
|
135
139
|
}
|
|
136
140
|
finally {
|
|
137
141
|
await database.finishRun(runId);
|
|
142
|
+
const [run] = await database.rows('SELECT status, error_count FROM sync_runs WHERE run_id = $runId', { runId });
|
|
143
|
+
log.info('Strava read operation finished', { runId, operation, status: run?.status ?? 'unknown', errorCount: run?.error_count ?? 0 });
|
|
138
144
|
}
|
|
139
145
|
if (result.status === 'error')
|
|
140
146
|
throw new StravaEnrichmentError(result.message ?? 'Strava did not complete the requested enrichment.');
|
|
@@ -94,9 +94,11 @@ export class CatenceDatabase {
|
|
|
94
94
|
const rawObjects = await this.rows('SELECT count(*)::INTEGER AS count FROM raw_objects');
|
|
95
95
|
const activities = await this.rows('SELECT count(*)::INTEGER AS count FROM activities');
|
|
96
96
|
const streams = await this.rows('SELECT count(*)::INTEGER AS count FROM stream_manifest');
|
|
97
|
+
const errorDetails = await this.rows('SELECT run_id, provider, endpoint, remote_id, message, cast(created_at AS VARCHAR) AS created_at FROM normalization_errors WHERE resolved_at IS NULL ORDER BY created_at DESC LIMIT 100');
|
|
97
98
|
const cursors = await this.rows('SELECT provider, cursor_name, cast(covered_through_date AS VARCHAR) AS covered_through_date, cast(latest_source_date AS VARCHAR) AS latest_source_date, lookback_days, last_successful_run_id, cast(last_completed_at AS VARCHAR) AS last_completed_at, status FROM sync_cursors ORDER BY provider, cursor_name');
|
|
98
99
|
return {
|
|
99
100
|
runs, unresolvedErrors: errors[0]?.count ?? 0, rawObjects: rawObjects[0]?.count ?? 0, activities: activities[0]?.count ?? 0, streams: streams[0]?.count ?? 0,
|
|
101
|
+
errors: errorDetails,
|
|
100
102
|
cursors: cursors.map((cursor) => ({ ...cursor, next_from_date: subtractDays(cursor.covered_through_date, Number(cursor.lookback_days)) })),
|
|
101
103
|
};
|
|
102
104
|
}
|
package/docs/llm-providers.md
CHANGED
|
@@ -373,6 +373,49 @@ settings then offer a per-deployment choice.
|
|
|
373
373
|
}
|
|
374
374
|
```
|
|
375
375
|
|
|
376
|
+
## Reasoning effort
|
|
377
|
+
|
|
378
|
+
Models expose a per-chat **Thinking effort** dropdown in the Console settings.
|
|
379
|
+
By default it offers the OpenAI-standard levels — `minimal`, `low`, `medium`,
|
|
380
|
+
`high`, `xhigh` — which suit OpenAI and most OpenAI-compatible endpoints.
|
|
381
|
+
|
|
382
|
+
Some models (for example DeepSeek V4 Flash) use their own reasoning-effort
|
|
383
|
+
scheme rather than the OpenAI-standard list. For those, declare `variants` on
|
|
384
|
+
the model: a mapping of dropdown label to the exact value sent to the provider.
|
|
385
|
+
The chat dropdown then shows `Provider default` plus each label instead of the
|
|
386
|
+
OpenAI-standard list.
|
|
387
|
+
|
|
388
|
+
```jsonc
|
|
389
|
+
{
|
|
390
|
+
"console": {
|
|
391
|
+
"profiles": {
|
|
392
|
+
"opencode-zen": {
|
|
393
|
+
"label": "OpenCode Zen",
|
|
394
|
+
"defaultModel": "deepseek-v4-flash-free",
|
|
395
|
+
"models": {
|
|
396
|
+
"deepseek-v4-flash-free": {
|
|
397
|
+
"label": "DeepSeek V4 Flash Free",
|
|
398
|
+
"model": "openai/deepseek-v4-flash-free",
|
|
399
|
+
"variants": { "High": "high", "Max": "max" }
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
"apiKeyEnv": "OPENCODE_API_KEY",
|
|
403
|
+
"apiBaseEnv": "OPENCODE_ZEN_API_BASE"
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Selecting a variant sends that value verbatim as `reasoning_effort` (with
|
|
411
|
+
`allowed_openai_params` so LiteLLM forwards it for OpenAI-compatible models).
|
|
412
|
+
The `Provider default` option sends no reasoning effort at all.
|
|
413
|
+
|
|
414
|
+
You can also pin a fixed level with `reasoningEffort` on the model, or a
|
|
415
|
+
profile-wide default with `defaultReasoningEffort` (OpenAI-standard values
|
|
416
|
+
only). Precedence: per-chat dropdown selection > model `reasoningEffort` >
|
|
417
|
+
profile `defaultReasoningEffort` > provider default.
|
|
418
|
+
|
|
376
419
|
## Verifying a profile
|
|
377
420
|
|
|
378
421
|
`catence-console doctor` reports, for each profile, which environment variables
|
package/package.json
CHANGED