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.
- package/dist/contracts/release.d.ts +1 -1
- package/dist/contracts/release.js +1 -1
- package/dist/interfaces/http/generation-status.d.ts +19 -0
- package/dist/interfaces/http/generation-status.js +55 -0
- package/dist/interfaces/http/server.js +30 -0
- package/docs/development.md +4 -0
- package/package.json +1 -1
|
@@ -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.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.
|
|
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;
|
package/docs/development.md
CHANGED
|
@@ -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