Skip to content

Commit 45d4dff

Browse files
chitcommitclaudechitcommit
authored
fix(health,wrangler): real-dependency /health probe + chittytrack tail consumer (#109)
* feat(daemon): loop body wires executeIntent end-to-end (claim → dispatch → heartbeat) Stacked on #106. Replaces the injected-executor abstraction in daemon/loop.ts with a direct call to meta/intent.ts::executeIntent, closing the meta-orchestrator loop. Status transitions, audit-row writes, and the second sovereignty gate are all owned by executeIntent → dispatch; the loop's responsibility is leader lifecycle, intent claiming, heartbeats, and outcome classification. Four outcomes are handled distinctly: - ok=true (executed) → bump processed counter, reset error backoff - ok=true (replayed) → bump replayed counter, no backoff, no double-count - ok=false (refused) → bump refused counter, no backoff (steady-state) - ok=false (exec error) → bump errored counter, bounded exp backoff Sovereignty refusals are identified by canonical error prefixes emitted by meta/executors/dispatch.ts ("sovereignty re-reckon:" / "sovereignty snapshot stale ..."). Refusals are NOT treated as transient faults — they are valid outcomes and do not trigger backoff. The loop honors options.signal via AbortController throughout, including inside the sleep helper, so SIGTERM from daemon/runtime/entrypoint.ts (PR #105) unwinds cleanly through releaseLeadership. tests/daemon/loop.spec.ts — real Neon integration. Seeds two pending intents against the update_obligation_status executor, runs runLeaderLoop with maxIntents=2, asserts: - both intents reach status='done' - each produces exactly one cc_actions_log row (attempt=1, key set, status='completed') - cc_obligations rows actually moved to 'deferred' - cc_node_leases shows leadership released on clean exit - log stream contains intent_heartbeat_before / intent_heartbeat_after pairs Out of scope (not in this PR): - new executors (mercury, etc.) - production deploy - multi-node coordination beyond single-node leader - schema additions on cc_intents / cc_actions_log Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(health,wrangler): real-dependency /health probe + chittytrack tail consumer The previous /health returned a static {status:"ok",...} regardless of whether the worker could reach its real dependencies — a direct violation of the chittyentity CLAUDE.md "No Mocks, Fake Data, or Placeholder Endpoints" binding rule ("every endpoint must return real results against a real datastore on the day it is committed"). This change replaces /health with a real probe that executes against the worker's actual dependencies: * db — SELECT 1 via Neon HTTP driver. Critical: failure -> 503. * chittyconnect — GET ${CHITTYCONNECT_URL}/health. Degraded if unreachable. * daemon — newest cc_node_leases.heartbeat_at, stale if older than 2x daemon/loop.ts default heartbeatMs (10000ms -> 20000ms cutoff). Missing table -> not_provisioned (degraded, NOT down) so deploys against bases without #101 don't 503. Per-dep timeout 2000ms; total probe bounded ≤ 5000ms via Promise.all. Runs unauthenticated (no auth middleware on /health). The probe handler is extracted to src/routes/health.ts so it can be integration-tested in pure Node — importing src/index.ts directly drags in `cloudflare:` modules (Agents SDK / DOs) that only resolve under workerd. Integration test exercises the handler against a real Neon branch (no mocks), validates DB probe ok and shape of all three probes, and asserts 503 + status=down when DB is unreachable. wrangler.jsonc already declares `tail_consumers: [{service: chittytrack}]` (present on the base branch) — no change required there. Verified against Neon branch br-spring-queen-akggkmso of project cool-bar-13270800 (ChittyCommand). Sample real response: { "status": "degraded", "service": "chittycommand", "version": "0.1.0", "timestamp": "2026-06-04T05:27:16.111Z", "probes": { "db": { "status": "ok", "latency_ms": 226 }, "chittyconnect": { "status": "degraded", "latency_ms": 0, "error": "CHITTYCONNECT_URL not configured" }, "daemon": { "status": "not_provisioned", "newest_heartbeat_age_ms": null, "error": "relation \"cc_node_leases\" does not exist" } } } Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(health): query active leases by heartbeat_at, not released_at Addresses Codex P2 on PR #109. The cc_node_leases schema in src/db/schema.ts has no released_at column — release is represented by NULLing heartbeat_at/lease_expires_at in daemon/leader.ts. The previous WHERE released_at IS NULL clause raised 'column released_at does not exist', which the catch block then mis-classified as 'not_provisioned', so /health would never report ok/stale based on real heartbeats. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: chitcommit <noreply@chitty.cc>
1 parent a8eb86c commit 45d4dff

3 files changed

Lines changed: 255 additions & 7 deletions

File tree

src/index.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { tokenManagementRoutes } from './routes/token-management';
3333
import { jobRoutes } from './routes/jobs';
3434
import { transactionRoutes } from './routes/transactions';
3535
import { timelineRoutes } from './routes/timeline';
36+
import { runHealthProbes } from './routes/health';
3637

3738
// Re-export ActionAgent DO class so the runtime can find it
3839
export { ActionAgent } from './agents/action-agent';
@@ -92,13 +93,13 @@ app.notFound((c) => {
9293
return c.json({ error: 'Not Found' }, 404);
9394
});
9495

95-
// Health endpoint (unauthenticated)
96-
app.get('/health', (c) => c.json({
97-
status: 'ok',
98-
service: 'chittycommand',
99-
version: '0.1.0',
100-
timestamp: new Date().toISOString(),
101-
}));
96+
// Health endpoint (unauthenticated) — real dependency probes (db, chittyconnect,
97+
// daemon heartbeat). See src/routes/health.ts. Returns 503 only if the DB is
98+
// down; chittyconnect/daemon problems surface as `degraded` with 200.
99+
app.get('/health', async (c) => {
100+
const { body, httpStatus } = await runHealthProbes(c.env);
101+
return c.json(body, httpStatus);
102+
});
102103

103104
// Service status (unauthenticated) — ChittyOS standard
104105
app.get('/api/v1/status', (c) => c.json({

src/routes/health.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/**
2+
* /health — real-dependency probe handler.
3+
*
4+
* Extracted from src/index.ts so it can be unit/integration-tested in pure
5+
* Node without dragging in `cloudflare:`-namespaced imports (Agents SDK,
6+
* Durable Objects, etc.) that only resolve under wrangler/workerd.
7+
*
8+
* Probes (each per-dep timeout 2000ms; total ≤ ~5000ms via Promise.all):
9+
* - db SELECT 1 via Neon HTTP driver. Critical: failure → 503.
10+
* - chittyconnect GET ${CHITTYCONNECT_URL}/health. Degraded if unreachable.
11+
* - daemon max(heartbeat_at) FROM cc_node_leases. Stale if older than
12+
* 2× daemon/loop.ts default heartbeatMs (10000ms → 20000ms).
13+
* Missing table → not_provisioned (degraded, not down) so
14+
* deploys against bases without #101 don't 503.
15+
*
16+
* Runs unauthenticated; does not touch auth middleware.
17+
*
18+
* @canonical-uri chittycanon://docs/architecture/chittycommand/health
19+
*/
20+
21+
import { getDb } from '../lib/db';
22+
23+
export const SERVICE_VERSION = '0.1.0';
24+
// daemon/loop.ts default heartbeatMs is 10000; flag stale at 2× = 20000ms.
25+
const DAEMON_STALE_MS = 20_000;
26+
27+
export type HealthEnv = {
28+
DATABASE_URL?: string;
29+
HYPERDRIVE?: { connectionString: string };
30+
CHITTYCONNECT_URL?: string;
31+
};
32+
33+
export interface DbProbe {
34+
status: 'ok' | 'down';
35+
latency_ms: number;
36+
error?: string;
37+
}
38+
export interface ChittyConnectProbe {
39+
status: 'ok' | 'degraded' | 'down';
40+
latency_ms: number;
41+
error?: string;
42+
}
43+
export interface DaemonProbe {
44+
status: 'ok' | 'stale' | 'not_provisioned';
45+
newest_heartbeat_age_ms: number | null;
46+
error?: string;
47+
}
48+
49+
export interface HealthBody {
50+
status: 'ok' | 'degraded' | 'down';
51+
service: 'chittycommand';
52+
version: string;
53+
timestamp: string;
54+
probes: { db: DbProbe; chittyconnect: ChittyConnectProbe; daemon: DaemonProbe };
55+
}
56+
57+
async function withTimeout<T>(p: Promise<T>, ms: number, label: string): Promise<T> {
58+
let timer: ReturnType<typeof setTimeout> | undefined;
59+
try {
60+
return await Promise.race<T>([
61+
p,
62+
new Promise<T>((_, reject) => {
63+
timer = setTimeout(() => reject(new Error(`${label} timeout after ${ms}ms`)), ms);
64+
}),
65+
]);
66+
} finally {
67+
if (timer) clearTimeout(timer);
68+
}
69+
}
70+
71+
async function probeDb(env: HealthEnv): Promise<DbProbe> {
72+
const t0 = Date.now();
73+
try {
74+
// Cast: HealthEnv is a narrow subset of the worker Env type and getDb
75+
// only reads DATABASE_URL / HYPERDRIVE.connectionString.
76+
const sql = getDb(env as unknown as Parameters<typeof getDb>[0]);
77+
await withTimeout(sql`SELECT 1 AS ok`, 2000, 'db');
78+
return { status: 'ok', latency_ms: Date.now() - t0 };
79+
} catch (err) {
80+
return {
81+
status: 'down',
82+
latency_ms: Date.now() - t0,
83+
error: err instanceof Error ? err.message : String(err),
84+
};
85+
}
86+
}
87+
88+
async function probeChittyConnect(env: HealthEnv): Promise<ChittyConnectProbe> {
89+
const url = env.CHITTYCONNECT_URL;
90+
const t0 = Date.now();
91+
if (!url) {
92+
return { status: 'degraded', latency_ms: 0, error: 'CHITTYCONNECT_URL not configured' };
93+
}
94+
try {
95+
const r = await withTimeout(
96+
fetch(`${url.replace(/\/$/, '')}/health`, { headers: { accept: 'application/json' } }),
97+
2000,
98+
'chittyconnect',
99+
);
100+
return r.ok
101+
? { status: 'ok', latency_ms: Date.now() - t0 }
102+
: { status: 'degraded', latency_ms: Date.now() - t0, error: `HTTP ${r.status}` };
103+
} catch (err) {
104+
return {
105+
status: 'degraded',
106+
latency_ms: Date.now() - t0,
107+
error: err instanceof Error ? err.message : String(err),
108+
};
109+
}
110+
}
111+
112+
async function probeDaemon(env: HealthEnv): Promise<DaemonProbe> {
113+
try {
114+
const sql = getDb(env as unknown as Parameters<typeof getDb>[0]);
115+
const rows = (await withTimeout(
116+
// NOTE: cc_node_leases has no `released_at` column. Release is
117+
// represented by NULLing `heartbeat_at`/`lease_expires_at` in
118+
// daemon/leader.ts::releaseLeadership. We treat any row whose
119+
// heartbeat_at is non-null as a currently-held lease and take the
120+
// newest heartbeat across them.
121+
sql`SELECT EXTRACT(EPOCH FROM (NOW() - max(heartbeat_at))) * 1000 AS age_ms
122+
FROM cc_node_leases
123+
WHERE heartbeat_at IS NOT NULL`,
124+
2000,
125+
'daemon',
126+
)) as Array<{ age_ms: number | string | null }>;
127+
const raw = rows[0]?.age_ms;
128+
if (raw === null || raw === undefined) {
129+
return { status: 'stale', newest_heartbeat_age_ms: null };
130+
}
131+
const ageMs = typeof raw === 'string' ? parseFloat(raw) : Number(raw);
132+
return {
133+
status: ageMs > DAEMON_STALE_MS ? 'stale' : 'ok',
134+
newest_heartbeat_age_ms: Math.round(ageMs),
135+
};
136+
} catch (err) {
137+
const msg = err instanceof Error ? err.message : String(err);
138+
if (/cc_node_leases|relation .* does not exist|does not exist/i.test(msg)) {
139+
return { status: 'not_provisioned', newest_heartbeat_age_ms: null, error: msg };
140+
}
141+
return { status: 'stale', newest_heartbeat_age_ms: null, error: msg };
142+
}
143+
}
144+
145+
export async function runHealthProbes(env: HealthEnv): Promise<{ body: HealthBody; httpStatus: 200 | 503 }> {
146+
const [db, chittyconnect, daemon] = await Promise.all([
147+
probeDb(env),
148+
probeChittyConnect(env),
149+
probeDaemon(env),
150+
]);
151+
152+
let status: HealthBody['status'] = 'ok';
153+
if (db.status === 'down') {
154+
status = 'down';
155+
} else if (
156+
chittyconnect.status === 'degraded' ||
157+
chittyconnect.status === 'down' ||
158+
daemon.status === 'stale' ||
159+
daemon.status === 'not_provisioned'
160+
) {
161+
status = 'degraded';
162+
}
163+
164+
const body: HealthBody = {
165+
status,
166+
service: 'chittycommand',
167+
version: SERVICE_VERSION,
168+
timestamp: new Date().toISOString(),
169+
probes: { db, chittyconnect, daemon },
170+
};
171+
172+
return { body, httpStatus: status === 'down' ? 503 : 200 };
173+
}

tests/health/health-probe.spec.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* Integration test for the real-dependency /health probe handler.
3+
*
4+
* Runs against a REAL Neon branch (no mocks — chittyentity CLAUDE.md
5+
* "No Mocks, Fake Data, or Placeholder Endpoints" rule).
6+
*
7+
* Usage:
8+
* DATABASE_URL='postgres://...neon...' npx vitest run tests/health/health-probe.spec.ts
9+
*
10+
* Skipped automatically when DATABASE_URL is absent or SKIP_INTEGRATION=1 —
11+
* same pattern as tests/daemon/leader.spec.ts and tests/meta/intent-lifecycle.spec.ts.
12+
*
13+
* We import the probe handler directly from src/routes/health.ts (pure Node
14+
* compatible). Importing src/index.ts would drag in `cloudflare:` namespaced
15+
* modules (Agents SDK / Durable Objects) which only resolve under workerd.
16+
*
17+
* Verifies:
18+
* 1. probes.db.status === 'ok' against the real Neon branch with latency_ms > 0.
19+
* 2. probes.daemon.status ∈ { ok, stale, not_provisioned } — not over-asserted
20+
* because the branch may or may not have cc_node_leases provisioned or
21+
* an active node holding a lease.
22+
* 3. probes.chittyconnect.status is one of { ok, degraded, down }.
23+
* 4. httpStatus is 200 when db.status === 'ok'; 503 only when db is down.
24+
* 5. Response shape matches the documented spec.
25+
*/
26+
27+
import { describe, it, expect } from 'vitest';
28+
import { runHealthProbes } from '../../src/routes/health';
29+
30+
const DATABASE_URL = process.env.DATABASE_URL;
31+
const SKIP = !DATABASE_URL || process.env.SKIP_INTEGRATION === '1';
32+
33+
describe.skipIf(SKIP)('/health real-dependency probe (real Neon)', () => {
34+
it('runs all probes and reports DB ok against the real Neon branch', async () => {
35+
const { body, httpStatus } = await runHealthProbes({
36+
DATABASE_URL,
37+
// CHITTYCONNECT_URL intentionally omitted: probe will report 'degraded'
38+
// with "not configured", which is the documented behavior.
39+
});
40+
41+
// Spec: 200 unless db is down. DB is reachable on this branch.
42+
expect(httpStatus).toBe(200);
43+
44+
expect(body.service).toBe('chittycommand');
45+
expect(typeof body.version).toBe('string');
46+
expect(typeof body.timestamp).toBe('string');
47+
expect(['ok', 'degraded']).toContain(body.status);
48+
49+
// DB probe must succeed.
50+
expect(body.probes.db.status).toBe('ok');
51+
expect(body.probes.db.latency_ms).toBeGreaterThan(0);
52+
53+
// ChittyConnect probe shape — value depends on env, just assert shape.
54+
expect(['ok', 'degraded', 'down']).toContain(body.probes.chittyconnect.status);
55+
expect(typeof body.probes.chittyconnect.latency_ms).toBe('number');
56+
57+
// Daemon probe — could be ok (active node), stale (no recent heartbeat),
58+
// or not_provisioned (table missing on this branch). Don't over-assert.
59+
expect(['ok', 'stale', 'not_provisioned']).toContain(body.probes.daemon.status);
60+
});
61+
62+
it('marks status `down` and returns 503 when DB is unreachable', async () => {
63+
// Real connection-refused — no mock, just an invalid host that fails fast.
64+
const { body, httpStatus } = await runHealthProbes({
65+
DATABASE_URL:
66+
'postgresql://nobody:nobody@127.0.0.1:1/neondb?sslmode=disable',
67+
});
68+
69+
expect(httpStatus).toBe(503);
70+
expect(body.status).toBe('down');
71+
expect(body.probes.db.status).toBe('down');
72+
expect(typeof body.probes.db.error).toBe('string');
73+
});
74+
});

0 commit comments

Comments
 (0)