catence 0.2.0-beta.20 → 0.2.0-beta.21

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.0-beta.20";
6
+ export declare const CATENCE_RUNTIME_VERSION = "0.2.0-beta.21";
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.20';
6
+ export const CATENCE_RUNTIME_VERSION = '0.2.0-beta.21';
7
7
  export const CATENCE_PROTOCOL_VERSION = 1;
8
8
  export function catenceRuntimeHealth() {
9
9
  return {
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Read + classify the generation progress sidecar written by the Catence
3
+ * Console agent turn (see `catence_console/generation_sidecar.py`). Mirrors the
4
+ * detached-sync sidecar logic: a run is "running" until it reaches a terminal
5
+ * stage, and "stale" (i.e. likely stuck) when its heartbeat is older than the
6
+ * threshold. This powers the chat UI's "still thinking vs stalled" signal.
7
+ */
8
+ export interface GenerationStatus {
9
+ threadId: string;
10
+ stage?: string;
11
+ running: boolean;
12
+ stale: boolean;
13
+ heartbeatAt?: string;
14
+ startedAt?: string;
15
+ updatedAt?: string;
16
+ toolCallCount: number;
17
+ lastTool?: string | null;
18
+ }
19
+ export declare function generationStatus(homeRoot: string, threadId: string): Promise<GenerationStatus>;
@@ -0,0 +1,55 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ const TERMINAL_STAGES = new Set([
4
+ 'completed',
5
+ 'failed',
6
+ 'interrupted',
7
+ 'timed_out'
8
+ ]);
9
+ const STALE_MS = 5 * 60 * 1000; // 5 minutes
10
+ function emptyStatus(threadId) {
11
+ return {
12
+ threadId,
13
+ running: false,
14
+ stale: false,
15
+ toolCallCount: 0
16
+ };
17
+ }
18
+ export async function generationStatus(homeRoot, threadId) {
19
+ const safe = threadId.replace(/[^a-zA-Z0-9_-]/g, '');
20
+ if (!safe)
21
+ return emptyStatus(threadId);
22
+ const file = path.join(homeRoot, 'generation', `${safe}.generation.json`);
23
+ let raw;
24
+ try {
25
+ raw = await readFile(file, 'utf8');
26
+ }
27
+ catch {
28
+ return emptyStatus(threadId);
29
+ }
30
+ let data;
31
+ try {
32
+ data = JSON.parse(raw);
33
+ }
34
+ catch {
35
+ return emptyStatus(threadId);
36
+ }
37
+ const base = {
38
+ threadId,
39
+ stage: typeof data.stage === 'string' ? data.stage : undefined,
40
+ heartbeatAt: typeof data.heartbeatAt === 'string' ? data.heartbeatAt : undefined,
41
+ startedAt: typeof data.startedAt === 'string' ? data.startedAt : undefined,
42
+ updatedAt: typeof data.updatedAt === 'string' ? data.updatedAt : undefined,
43
+ toolCallCount: typeof data.toolCallCount === 'number' ? data.toolCallCount : 0,
44
+ lastTool: data.lastTool === null || typeof data.lastTool === 'string'
45
+ ? data.lastTool
46
+ : undefined
47
+ };
48
+ if (base.stage && TERMINAL_STAGES.has(base.stage)) {
49
+ return { ...base, running: false, stale: false };
50
+ }
51
+ const hb = base.heartbeatAt ? Date.parse(base.heartbeatAt) : NaN;
52
+ const stale = !Number.isFinite(hb) || Date.now() - hb > STALE_MS;
53
+ return { ...base, running: true, stale };
54
+ }
55
+ //# sourceMappingURL=generation-status.js.map
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4
4
  import { catenceRuntimeHealth, DashboardSnapshotService, DetachedSyncBusyError, DETACHED_SYNC_PROVIDERS, jsonSafe, loadCatalog, mergeOpenCodeGoConsoleProfiles, openReadOnlyRepository, resolveAthlete, resolveCatalogPaths, startDetachedSync, syncProgress, } from '../../runtime/index.js';
5
5
  import { createCatenceMcpServer } from '../mcp/server.js';
6
+ import { generationStatus } from './generation-status.js';
6
7
  const MAX_REQUEST_BYTES = 1_000_000;
7
8
  function json(response, statusCode, value) {
8
9
  const body = JSON.stringify(jsonSafe(value));
@@ -239,6 +240,10 @@ export function createCatenceHttpServer(options = {}) {
239
240
  json(response, 200, catenceRuntimeHealth());
240
241
  return;
241
242
  }
243
+ if (pathname === '/api/v1/health' && request.method === 'GET') {
244
+ json(response, 200, catenceRuntimeHealth());
245
+ return;
246
+ }
242
247
  if (pathname === '/api/v1/athletes' && request.method === 'GET') {
243
248
  if (!catalogPaths) {
244
249
  json(response, 200, { defaultAthleteId: 'local', athletes: [{ id: 'local', label: 'Local athlete' }] });
@@ -335,6 +340,31 @@ export function createCatenceHttpServer(options = {}) {
335
340
  }
336
341
  return;
337
342
  }
343
+ if (pathname.startsWith('/api/v1/threads/') &&
344
+ pathname.endsWith('/generation') &&
345
+ request.method === 'GET') {
346
+ try {
347
+ const segments = pathname.split('/').filter(Boolean);
348
+ const threadId = segments[segments.length - 2];
349
+ if (!threadId)
350
+ throw new Error('Missing threadId in request path.');
351
+ const base = (catalogPaths ?? staticPaths ?? resolveCatalogPaths());
352
+ const root = base?.root;
353
+ if (!root || typeof root !== 'string') {
354
+ throw new Error('No Catence data store is configured.');
355
+ }
356
+ json(response, 200, await generationStatus(root, threadId));
357
+ }
358
+ catch (error) {
359
+ json(response, 400, {
360
+ error: {
361
+ code: 'generation_status_unavailable',
362
+ message: error instanceof Error ? error.message : String(error)
363
+ }
364
+ });
365
+ }
366
+ return;
367
+ }
338
368
  if (pathname === '/mcp') {
339
369
  await handleMcpRequest(request, response, catalogPaths ?? staticPaths ?? resolveCatalogPaths());
340
370
  return;
@@ -321,6 +321,10 @@ Recommended dev defaults:
321
321
  - The deployed scaffold's `sync.sh` / `hash-password.sh` / `doctor.sh` helpers are generated by `scripts/deploy-console.sh`; the dev container does not generate them — use the `npm run` / `uv run` equivalents above, or run the same `catence-data --home /data` commands via `docker compose exec`.
322
322
  - If the Console settings panel shows no Model dropdown, the data home's `config.json` has no `console.profiles` — re-copy `config.example.json` or re-run the discovery script, then restart the Console.
323
323
 
324
+ ### Status vs Dashboard
325
+
326
+ The **Dashboard** (`/dashboard`) is now a pure analytics view — it renders charts and recent activities from `GET /api/v1/dashboard` and, when a detached sync holds the DuckDB write lock, shows a non-blocking amber banner (“Data temporarily unavailable — a sync is running. View live progress on the Status page.”) instead of a full-page `dashboard_unavailable` error. The **Status** page (`/status`) is the lock-safe control plane: it reads `GET /api/v1/health` (runtime `catenceRuntimeHealth()`), `GET /api/v1/sync/status` (sidecar heartbeats + `lastSync`), and `GET /api/v1/athletes` (roster), so it never needs a successful `dashboardSnapshot` and never 400s under lock. The interactive **Sync** button (with instant grey-out via `startingRef`) lives only on Status; Dashboard keeps a read-only “Syncing…” pill and a “Sync moved to Status →” banner.
327
+
324
328
  ## Troubleshooting
325
329
 
326
330
  - **Doctor reports runtime unreachable** — the Console was started without `--mcp-url` and its auto-started runtime is on a different port. Start the runtime explicitly and pass `--mcp-url http://127.0.0.1:8787/mcp` to the Console, or use `uv run --project console catence-console doctor --home "$CATENCE_HOME" --mcp-url http://127.0.0.1:8787/mcp`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "catence",
3
- "version": "0.2.0-beta.20",
3
+ "version": "0.2.0-beta.21",
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": {