@pleri/olam-cli 0.1.170 → 0.1.173
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/agent-stream/driver-runner.js +13 -0
- package/dist/image-digests.json +8 -8
- package/dist/index.js +4 -1
- package/dist/mcp-server.js +1230 -375
- package/hermes-bundle/version.json +1 -1
- package/host-cp/k8s/manifests/50-deployment.yaml +1 -1
- package/host-cp/k8s/manifests/auth-service/50-deployment.yaml +1 -1
- package/host-cp/k8s/manifests/kg-service/50-deployment.yaml +1 -1
- package/host-cp/k8s/manifests/mcp-auth-service/50-deployment.yaml +1 -1
- package/host-cp/k8s/manifests/memory-service/50-deployment.yaml +1 -1
- package/host-cp/observability/ndjson-span-sink.mjs +52 -0
- package/host-cp/src/linear-sync.mjs +43 -0
- package/host-cp/src/plan-chat-service.mjs +129 -1
- package/host-cp/src/server.mjs +89 -1
- package/package.json +1 -1
|
@@ -118,7 +118,7 @@ spec:
|
|
|
118
118
|
# k3d), started by `olam upgrade` Step 0.7 — not inside this Pod.
|
|
119
119
|
containers:
|
|
120
120
|
- name: olam-host-cp
|
|
121
|
-
image: ghcr.io/pleri/olam-host-cp@sha256:
|
|
121
|
+
image: ghcr.io/pleri/olam-host-cp@sha256:3043df80469fa58319de53688991c81575522beba48b7a1b6956d0e3f2d03b45
|
|
122
122
|
imagePullPolicy: IfNotPresent
|
|
123
123
|
securityContext:
|
|
124
124
|
runAsNonRoot: true
|
|
@@ -70,7 +70,7 @@ spec:
|
|
|
70
70
|
mountPath: /data
|
|
71
71
|
containers:
|
|
72
72
|
- name: olam-auth-service
|
|
73
|
-
image: ghcr.io/pleri/olam-auth@sha256:
|
|
73
|
+
image: ghcr.io/pleri/olam-auth@sha256:2e40e0c1f0469331dfa98a2e194c922c710149d8d5d3816bc660e530be6a9b97
|
|
74
74
|
imagePullPolicy: IfNotPresent
|
|
75
75
|
securityContext:
|
|
76
76
|
runAsNonRoot: true
|
|
@@ -61,7 +61,7 @@ spec:
|
|
|
61
61
|
mountPath: /data
|
|
62
62
|
containers:
|
|
63
63
|
- name: olam-kg-service
|
|
64
|
-
image: ghcr.io/pleri/olam-kg-service@sha256:
|
|
64
|
+
image: ghcr.io/pleri/olam-kg-service@sha256:225dd3460bce1f2572e6076e55a875ff526a5217a2d8f311d14b6dee591e8a38
|
|
65
65
|
imagePullPolicy: IfNotPresent
|
|
66
66
|
securityContext:
|
|
67
67
|
runAsNonRoot: true
|
|
@@ -68,7 +68,7 @@ spec:
|
|
|
68
68
|
mountPath: /data
|
|
69
69
|
containers:
|
|
70
70
|
- name: olam-mcp-auth-service
|
|
71
|
-
image: ghcr.io/pleri/olam-mcp-auth@sha256:
|
|
71
|
+
image: ghcr.io/pleri/olam-mcp-auth@sha256:f61bf653ada702d59aca0a309b224b01e0f151a89e01583a76f94d8604101d20
|
|
72
72
|
imagePullPolicy: IfNotPresent
|
|
73
73
|
securityContext:
|
|
74
74
|
runAsNonRoot: true
|
|
@@ -70,7 +70,7 @@ spec:
|
|
|
70
70
|
# bootstrap-placeholder comment + run `npm run refresh:manifest-digests`
|
|
71
71
|
# once ghcr.io/pleri/olam-memory-service has a real published digest.
|
|
72
72
|
# bootstrap-placeholder: pre-publish; refresh after first release
|
|
73
|
-
image: ghcr.io/pleri/olam-memory-service@sha256:
|
|
73
|
+
image: ghcr.io/pleri/olam-memory-service@sha256:86ce43a8bfec3edf0a9ac1aea63bf3ecd922209a7ab2a0f589ae9e1cedc0134a
|
|
74
74
|
imagePullPolicy: IfNotPresent
|
|
75
75
|
securityContext:
|
|
76
76
|
runAsNonRoot: true
|
|
@@ -102,6 +102,58 @@ export async function createNdjsonSpanSink({
|
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Subscribe an NDJSON sink to `@olam/auth-client`'s `betaResponseEmitter`.
|
|
107
|
+
* Each `beta-response` event becomes a `withCredential.beta-response` span
|
|
108
|
+
* with the beta payload exploded onto `attributes` — downstream `jq`
|
|
109
|
+
* consumers can query e.g.
|
|
110
|
+
*
|
|
111
|
+
* jq 'select(.name == "withCredential.beta-response")
|
|
112
|
+
* | {ts: .startedAt, cred: .attributes.credentialName,
|
|
113
|
+
* cache: .attributes.cacheStatus,
|
|
114
|
+
* thinking: .attributes.thinkingTokens,
|
|
115
|
+
* latencyMs: .durationMs}' ~/.olam/logs/host.trace.ndjson
|
|
116
|
+
*
|
|
117
|
+
* Wire is opt-in (call from server boot). Returns a detach function so the
|
|
118
|
+
* subscription can be removed in tests or on shutdown.
|
|
119
|
+
*
|
|
120
|
+
* Pure additive: spans flowing from other sources (docker lifecycle,
|
|
121
|
+
* plan-orchestrator, etc.) are unaffected.
|
|
122
|
+
*/
|
|
123
|
+
export function attachBetaResponseEvents({ sink, emitter }) {
|
|
124
|
+
if (!sink || typeof sink.recordSpan !== 'function') {
|
|
125
|
+
throw new Error('attachBetaResponseEvents: sink.recordSpan required');
|
|
126
|
+
}
|
|
127
|
+
if (!emitter || typeof emitter.on !== 'function') {
|
|
128
|
+
throw new Error('attachBetaResponseEvents: emitter.on required');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const handler = (info) => {
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
const latency = typeof info?.latencyMs === 'number' ? info.latencyMs : 0;
|
|
134
|
+
sink.recordSpan({
|
|
135
|
+
name: 'withCredential.beta-response',
|
|
136
|
+
startedAt: now - latency,
|
|
137
|
+
endedAt: now,
|
|
138
|
+
attributes: {
|
|
139
|
+
credentialName: info?.credentialName ?? null,
|
|
140
|
+
credId: info?.credId ?? null,
|
|
141
|
+
betas: Array.isArray(info?.betas) ? [...info.betas] : [],
|
|
142
|
+
cacheStatus: info?.cacheStatus ?? null,
|
|
143
|
+
thinkingTokens: info?.tokenCounts?.thinking ?? null,
|
|
144
|
+
statusCode: typeof info?.statusCode === 'number' ? info.statusCode : null,
|
|
145
|
+
extraHeaders: info?.extraHeaders && typeof info.extraHeaders === 'object'
|
|
146
|
+
? { ...info.extraHeaders }
|
|
147
|
+
: {},
|
|
148
|
+
},
|
|
149
|
+
exit: { _tag: 'Success' },
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
emitter.on('beta-response', handler);
|
|
154
|
+
return () => emitter.off('beta-response', handler);
|
|
155
|
+
}
|
|
156
|
+
|
|
105
157
|
// Duck-typed ServerResponse for host-stream's `addSink`. Parses SSE frames
|
|
106
158
|
// (`event: <type>\ndata: <json>\n\n`) and dispatches `event: span` payloads
|
|
107
159
|
// to `onSpan`. All other event types are silently ignored — host-stream
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// H6 (Phase G) — Linear outbound sync skeleton.
|
|
2
|
+
//
|
|
3
|
+
// When a planning_artifacts row is created via H2 chunk extraction AND
|
|
4
|
+
// the operator has Linear MCP active, host-cp posts a new Linear issue
|
|
5
|
+
// (OR appends a comment to an existing linked issue if linear_issue_url
|
|
6
|
+
// is already populated on the row).
|
|
7
|
+
//
|
|
8
|
+
// PHASE G SHIPS SKELETON ONLY. The MCP-from-host wiring is not yet in
|
|
9
|
+
// host-cp; full Linear outbound posting lands in a follow-up commit
|
|
10
|
+
// when the MCP runtime story for host-cp is settled (today MCP lives in
|
|
11
|
+
// the operator's Claude Code runtime, not host-cp).
|
|
12
|
+
//
|
|
13
|
+
// Reverse channel (incoming Linear webhooks update artifact row status):
|
|
14
|
+
// EXPLICITLY OUT OF SCOPE for Phase G per plan body Out of scope list.
|
|
15
|
+
// Deferred to follow-up plan.
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Best-effort Linear outbound sync. Returns null when MCP is unavailable
|
|
19
|
+
* (the typical case today — silent no-op). When MCP wires in, this
|
|
20
|
+
* function resolves to the posted issue URL which the caller can persist
|
|
21
|
+
* back to planning_artifacts.linear_issue_url.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} args
|
|
24
|
+
* @param {string} args.artifactId
|
|
25
|
+
* @param {string} args.title
|
|
26
|
+
* @param {unknown} args.body — JSON body of the artifact
|
|
27
|
+
* @param {string} args.sessionId
|
|
28
|
+
* @returns {Promise<string | null>} — Linear issue URL or null
|
|
29
|
+
*/
|
|
30
|
+
export async function syncArtifactToLinear({ artifactId, title, body, sessionId }) {
|
|
31
|
+
// Probe for Linear MCP availability via host-cp's bootstrap config (TBD).
|
|
32
|
+
// Until that surface exists, return null. Logging the intent surfaces
|
|
33
|
+
// the wiring gap to operators inspecting host-cp logs.
|
|
34
|
+
void artifactId;
|
|
35
|
+
void title;
|
|
36
|
+
void body;
|
|
37
|
+
void sessionId;
|
|
38
|
+
console.log(
|
|
39
|
+
`[linear-sync] outbound skip — Linear MCP not yet wired to host-cp; ` +
|
|
40
|
+
`artifactId=${artifactId} sessionId=${sessionId}`,
|
|
41
|
+
);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
@@ -68,7 +68,7 @@ const SCOPE_ID_RE = /^[A-Za-z0-9_.-]+$/;
|
|
|
68
68
|
// /v1/shape. Only these tables have server-side where-rewrite support; any
|
|
69
69
|
// other table=... param gets a 400. Guards against a client enumerating
|
|
70
70
|
// tables the service doesn't own.
|
|
71
|
-
const ALLOWED_SHAPE_TABLES = new Set(['chunks', 'message_usage']);
|
|
71
|
+
const ALLOWED_SHAPE_TABLES = new Set(['chunks', 'message_usage', 'planning_artifacts']);
|
|
72
72
|
|
|
73
73
|
// B6 (plan-chat-context-window-display Phase B): context-window caps per
|
|
74
74
|
// model. Mirrors CONTEXT_CAPS from @olam/intelligence/src/llm-router/providers/claude.ts.
|
|
@@ -399,6 +399,44 @@ export function createHandler({
|
|
|
399
399
|
}
|
|
400
400
|
}
|
|
401
401
|
}
|
|
402
|
+
// H2 (plan-chat-spa-canonical-surface Phase G) — extract commit_plan /
|
|
403
|
+
// propose_plan tool_use chunks into the planning_artifacts mutable
|
|
404
|
+
// table. The chunk's content carries JSON-stringified {name, input}
|
|
405
|
+
// per the substrate contract; we parse it once + write a single row.
|
|
406
|
+
// Failure to extract is logged + swallowed — the chunk itself is
|
|
407
|
+
// already persisted; artifact row absence is recoverable via
|
|
408
|
+
// re-extraction from chunks in a follow-up batch job.
|
|
409
|
+
if (body.chunk_type === 'tool_use') {
|
|
410
|
+
try {
|
|
411
|
+
const parsed = JSON.parse(body.chunk);
|
|
412
|
+
const toolName = typeof parsed?.name === 'string' ? parsed.name : '';
|
|
413
|
+
const artifactType = (toolName === 'commit_plan' || toolName === 'propose_plan')
|
|
414
|
+
? 'commit_plan'
|
|
415
|
+
: toolName === 'component_scaffold'
|
|
416
|
+
? 'component_scaffold'
|
|
417
|
+
: toolName === 'design_jam'
|
|
418
|
+
? 'design_jam'
|
|
419
|
+
: null;
|
|
420
|
+
if (artifactType && parsed.input && typeof parsed.input === 'object') {
|
|
421
|
+
const input = parsed.input;
|
|
422
|
+
const title = typeof input.title === 'string' && input.title.length > 0
|
|
423
|
+
? input.title.slice(0, 200)
|
|
424
|
+
: `Untitled ${artifactType}`;
|
|
425
|
+
const artifactId = `${body.message_id}:${body.seq}`;
|
|
426
|
+
await pool.query(
|
|
427
|
+
`INSERT INTO planning_artifacts
|
|
428
|
+
(id, world_id, session_id, type, title, body)
|
|
429
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
430
|
+
ON CONFLICT (id) DO NOTHING`,
|
|
431
|
+
[artifactId, body.world_id, body.session_id, artifactType, title, input],
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
} catch (artifactErr) {
|
|
435
|
+
console.warn(
|
|
436
|
+
`[plan-chat-service] planning_artifacts extraction failed for message=${body.message_id} seq=${body.seq}: ${artifactErr?.message ?? artifactErr}`,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
402
440
|
} catch (err) {
|
|
403
441
|
if (err && typeof err === 'object' && 'code' in err && err.code === '23505') {
|
|
404
442
|
return send(res, 409, { error: 'duplicate', message: '(message_id, seq) already exists' });
|
|
@@ -634,6 +672,26 @@ export function createHandler({
|
|
|
634
672
|
createWorld,
|
|
635
673
|
destroyWorld,
|
|
636
674
|
});
|
|
675
|
+
// H7 (Phase G) — back-fill crystallized_world_id to all planning_artifacts
|
|
676
|
+
// rows for this session. The status pill state machine (Phase E E4) reads
|
|
677
|
+
// this field; the editor view + diagrams viewer use it for the
|
|
678
|
+
// "View world →" CTA. Failure here is logged + swallowed — the
|
|
679
|
+
// crystallize itself already succeeded; back-fill is a best-effort
|
|
680
|
+
// sync that an operator can re-run via a CLI helper.
|
|
681
|
+
if (result.worldId) {
|
|
682
|
+
try {
|
|
683
|
+
await pool.query(
|
|
684
|
+
`UPDATE planning_artifacts
|
|
685
|
+
SET crystallized_world_id = $1, status = 'crystallized'
|
|
686
|
+
WHERE session_id = $2 AND status = 'open'`,
|
|
687
|
+
[result.worldId, body.session_id],
|
|
688
|
+
);
|
|
689
|
+
} catch (backfillErr) {
|
|
690
|
+
console.warn(
|
|
691
|
+
`[plan-chat-service] crystallize back-fill failed for session=${body.session_id}: ${backfillErr?.message ?? backfillErr}`,
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
637
695
|
return send(res, 200, {
|
|
638
696
|
ok: true,
|
|
639
697
|
created_world_id: result.worldId,
|
|
@@ -649,6 +707,68 @@ export function createHandler({
|
|
|
649
707
|
}
|
|
650
708
|
}
|
|
651
709
|
|
|
710
|
+
// H4 (Phase G) — GET + PATCH /v1/artifacts/:id endpoint pair backing
|
|
711
|
+
// the SPA's editor-view round-trip. GET returns the artifact row;
|
|
712
|
+
// PATCH updates body (the JSON payload) + bumps updated_at via trigger.
|
|
713
|
+
// SCOPE_ID_RE on :id; bearer auth identical to the chunks endpoints.
|
|
714
|
+
async function handleGetArtifact(req, res, id) {
|
|
715
|
+
if (!checkAuth(req)) return unauthorized(res);
|
|
716
|
+
if (!SCOPE_ID_RE.test(id)) return badRequest(res, 'invalid artifact id');
|
|
717
|
+
try {
|
|
718
|
+
const result = await pool.query(
|
|
719
|
+
`SELECT id, world_id, session_id, type, title, body, status,
|
|
720
|
+
linear_issue_url, crystallized_world_id,
|
|
721
|
+
created_at, updated_at
|
|
722
|
+
FROM planning_artifacts WHERE id = $1`,
|
|
723
|
+
[id],
|
|
724
|
+
);
|
|
725
|
+
const row = result.rows[0];
|
|
726
|
+
if (!row) return send(res, 404, { error: 'not-found' });
|
|
727
|
+
return send(res, 200, row);
|
|
728
|
+
} catch (err) {
|
|
729
|
+
return send(res, 500, { error: 'query-failed', message: String(err?.message ?? err) });
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
async function handlePatchArtifact(req, res, id) {
|
|
734
|
+
if (!checkAuth(req)) return unauthorized(res);
|
|
735
|
+
if (!SCOPE_ID_RE.test(id)) return badRequest(res, 'invalid artifact id');
|
|
736
|
+
let body;
|
|
737
|
+
try {
|
|
738
|
+
body = await readJson(req);
|
|
739
|
+
} catch {
|
|
740
|
+
return badRequest(res, 'malformed JSON body');
|
|
741
|
+
}
|
|
742
|
+
const sets = [];
|
|
743
|
+
const values = [id];
|
|
744
|
+
if (body.body !== undefined) {
|
|
745
|
+
values.push(body.body);
|
|
746
|
+
sets.push(`body = $${values.length}::jsonb`);
|
|
747
|
+
}
|
|
748
|
+
if (typeof body.title === 'string') {
|
|
749
|
+
values.push(body.title.slice(0, 200));
|
|
750
|
+
sets.push(`title = $${values.length}`);
|
|
751
|
+
}
|
|
752
|
+
if (typeof body.status === 'string' && ['open', 'crystallized', 'failed', 'archived'].includes(body.status)) {
|
|
753
|
+
values.push(body.status);
|
|
754
|
+
sets.push(`status = $${values.length}`);
|
|
755
|
+
}
|
|
756
|
+
if (sets.length === 0) {
|
|
757
|
+
return badRequest(res, 'no patchable fields supplied (body | title | status)');
|
|
758
|
+
}
|
|
759
|
+
try {
|
|
760
|
+
const result = await pool.query(
|
|
761
|
+
`UPDATE planning_artifacts SET ${sets.join(', ')} WHERE id = $1 RETURNING *`,
|
|
762
|
+
values,
|
|
763
|
+
);
|
|
764
|
+
const row = result.rows[0];
|
|
765
|
+
if (!row) return send(res, 404, { error: 'not-found' });
|
|
766
|
+
return send(res, 200, row);
|
|
767
|
+
} catch (err) {
|
|
768
|
+
return send(res, 500, { error: 'update-failed', message: String(err?.message ?? err) });
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
652
772
|
return async function handler(req, res) {
|
|
653
773
|
const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
|
|
654
774
|
if (req.method === 'GET' && url.pathname === '/livez') return send(res, 200, { ok: true });
|
|
@@ -656,6 +776,14 @@ export function createHandler({
|
|
|
656
776
|
if (req.method === 'GET' && url.pathname === '/v1/shape') return handleGetShape(req, res, url);
|
|
657
777
|
if (req.method === 'GET' && url.pathname === '/v1/planning-sessions') return handleGetPlanningSessions(req, res, url);
|
|
658
778
|
if (req.method === 'POST' && url.pathname === '/v1/crystallize') return handlePostCrystallize(req, res);
|
|
779
|
+
// H4 — /v1/artifacts/:id pair
|
|
780
|
+
const artifactMatch = /^\/v1\/artifacts\/([^/]+)$/.exec(url.pathname);
|
|
781
|
+
if (artifactMatch) {
|
|
782
|
+
const id = decodeURIComponent(artifactMatch[1]);
|
|
783
|
+
if (req.method === 'GET') return handleGetArtifact(req, res, id);
|
|
784
|
+
if (req.method === 'PATCH') return handlePatchArtifact(req, res, id);
|
|
785
|
+
return send(res, 405, { error: 'method-not-allowed' });
|
|
786
|
+
}
|
|
659
787
|
return send(res, 404, { error: 'not-found' });
|
|
660
788
|
};
|
|
661
789
|
}
|
package/host-cp/src/server.mjs
CHANGED
|
@@ -41,7 +41,11 @@ import {
|
|
|
41
41
|
WorldStartupFailureKind,
|
|
42
42
|
} from '../lifecycle/index.mjs';
|
|
43
43
|
import { createHostStream, newStreamId } from './host-stream.mjs';
|
|
44
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
createNdjsonSpanSink,
|
|
46
|
+
attachBetaResponseEvents,
|
|
47
|
+
} from '../observability/ndjson-span-sink.mjs';
|
|
48
|
+
import { betaResponseEmitter } from '@olam/auth-client';
|
|
45
49
|
import { attemptRecovery, findScenarioForKind } from '../recovery/index.mjs';
|
|
46
50
|
import { detectHaltChunk } from './halt-detect.mjs';
|
|
47
51
|
import { spawnUpgraderContainer } from './upgrade-spawner.mjs';
|
|
@@ -83,6 +87,7 @@ import {
|
|
|
83
87
|
} from './routes/process-port.mjs';
|
|
84
88
|
import { instrumentHandler, renderMetrics } from './metrics.mjs';
|
|
85
89
|
import { handleDispatchFromEmail } from './lib/email-dispatch.mjs';
|
|
90
|
+
import { emitTierSuggestion } from '../dispatch/auto-tier-scheduler.mjs';
|
|
86
91
|
|
|
87
92
|
// ── Deployment-mode detection ─────────────────────────────────────
|
|
88
93
|
//
|
|
@@ -490,6 +495,20 @@ const ndjsonSpanSink = await createNdjsonSpanSink({ hostStream }).catch((err) =>
|
|
|
490
495
|
return null;
|
|
491
496
|
});
|
|
492
497
|
|
|
498
|
+
// Wire @olam/auth-client `beta-response` events (Anthropic SDK 0.96+ beta
|
|
499
|
+
// flags — thinking-token-count, cache-diagnostics, future passthrough) into
|
|
500
|
+
// the NDJSON trace as `withCredential.beta-response` spans. Opt-in via the
|
|
501
|
+
// caller's `withCredential('claude', fn, { betas: [...] })` options; when
|
|
502
|
+
// no caller opts in, the emitter never fires and this subscription is a
|
|
503
|
+
// no-op. See docs/decisions/047-anthropic-sdk-beta-flags.md.
|
|
504
|
+
if (ndjsonSpanSink) {
|
|
505
|
+
try {
|
|
506
|
+
attachBetaResponseEvents({ sink: ndjsonSpanSink, emitter: betaResponseEmitter });
|
|
507
|
+
} catch (err) {
|
|
508
|
+
console.warn(`[trace] beta-response wire unavailable: ${err?.message ?? err}`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
493
512
|
// A4: coalesce docker-event bursts into a single servers.snapshot. World
|
|
494
513
|
// boot fires `create` + `start` + healthcheck transitions in <100ms; we
|
|
495
514
|
// don't want a broadcast storm. Window matches plan-source.md P3 target.
|
|
@@ -922,6 +941,58 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
|
|
|
922
941
|
if (handled) return;
|
|
923
942
|
}
|
|
924
943
|
|
|
944
|
+
// /api/telemetry/planning-sessions — B9: aggregate planning_sessions by
|
|
945
|
+
// session_source for the canonical-surface bet's adoption signal. Per
|
|
946
|
+
// plan-chat-spa-canonical-surface plan § Operator workflow seam falsification
|
|
947
|
+
// trigger: if plan-chat-spa weekly-active sessions < 60% of control-plane/app
|
|
948
|
+
// by 2026-Q3, freeze plan-chat-spa feature work. This endpoint is the
|
|
949
|
+
// data source for that measurement.
|
|
950
|
+
//
|
|
951
|
+
// Query param: ?since=YYYY-MM-DD (required; rejects with 400 otherwise).
|
|
952
|
+
// Response: { plan_chat_spa: N, control_plane_app: M, unknown: K, ratio: pct }
|
|
953
|
+
// where ratio = plan_chat_spa / (plan_chat_spa + control_plane_app) * 100,
|
|
954
|
+
// null if denominator is 0.
|
|
955
|
+
if (url.pathname === '/api/telemetry/planning-sessions' && req.method === 'GET') {
|
|
956
|
+
const since = url.searchParams.get('since');
|
|
957
|
+
if (!since || !/^\d{4}-\d{2}-\d{2}$/.test(since)) {
|
|
958
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
959
|
+
return res.end(JSON.stringify({
|
|
960
|
+
error: 'bad_request',
|
|
961
|
+
message: 'Missing or malformed `since` query param. Expected YYYY-MM-DD.',
|
|
962
|
+
}));
|
|
963
|
+
}
|
|
964
|
+
// B9 ships the endpoint CONTRACT + the session_source schema column.
|
|
965
|
+
// The query implementation goes through plan-chat-service.mjs (which
|
|
966
|
+
// owns the pg pool); this host-cp handler currently emits a 503 with
|
|
967
|
+
// a structured "not_implemented" marker so callers can verify the
|
|
968
|
+
// endpoint shape + auth + query-param parsing without the data path.
|
|
969
|
+
//
|
|
970
|
+
// Phase G of this epic adds the plan-chat-service handler that this
|
|
971
|
+
// endpoint will proxy to. Until then operators can run the SQL
|
|
972
|
+
// directly:
|
|
973
|
+
// SELECT COALESCE(session_source, 'unknown'), COUNT(*)
|
|
974
|
+
// FROM planning_sessions
|
|
975
|
+
// WHERE created_at >= $since
|
|
976
|
+
// GROUP BY 1;
|
|
977
|
+
//
|
|
978
|
+
// Notify-C: ship contract + schema; defer data path to Phase G.
|
|
979
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
980
|
+
return res.end(JSON.stringify({
|
|
981
|
+
error: 'not_implemented',
|
|
982
|
+
message: 'B9 ships the endpoint contract + session_source schema column. ' +
|
|
983
|
+
'Aggregation handler scaffolded in plan-chat-service.mjs lands in Phase G.',
|
|
984
|
+
since,
|
|
985
|
+
contractShape: {
|
|
986
|
+
plan_chat_spa: 0,
|
|
987
|
+
control_plane_app: 0,
|
|
988
|
+
unknown: 0,
|
|
989
|
+
ratio: null,
|
|
990
|
+
since: '<YYYY-MM-DD>',
|
|
991
|
+
asOf: '<ISO 8601>',
|
|
992
|
+
},
|
|
993
|
+
}));
|
|
994
|
+
}
|
|
995
|
+
|
|
925
996
|
// /api/version/status: returns the current version snapshot (baked SHA
|
|
926
997
|
// vs operator's local HEAD). No auth required beyond the existing gate
|
|
927
998
|
// (already applied above). Phase 1 only — detection, no auto-upgrade.
|
|
@@ -2224,6 +2295,23 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
|
|
|
2224
2295
|
return jsonReply(res, 400, { error: 'invalid_json', message: err.message });
|
|
2225
2296
|
}
|
|
2226
2297
|
try {
|
|
2298
|
+
// Auto-tier-scheduler v1 (ADR 042): emit an informational
|
|
2299
|
+
// `dispatch.tier-suggestion` event BEFORE handing off to the
|
|
2300
|
+
// dispatch handler. Pure-informational — never changes which
|
|
2301
|
+
// provider actually runs. The dispatch payload's optional
|
|
2302
|
+
// `tierSpec` ({ kind?, expectedDurationMs?, explicitTier? })
|
|
2303
|
+
// carries the shape; absent it, the heuristic falls through to
|
|
2304
|
+
// its default (`cloudflare-sandbox`).
|
|
2305
|
+
if (dispatch && typeof dispatch.worldId === 'string') {
|
|
2306
|
+
try {
|
|
2307
|
+
emitTierSuggestion({
|
|
2308
|
+
worldId: dispatch.worldId,
|
|
2309
|
+
dispatchSpec: dispatch.tierSpec ?? {},
|
|
2310
|
+
currentTier: null,
|
|
2311
|
+
hostStream,
|
|
2312
|
+
});
|
|
2313
|
+
} catch { /* never let a hint surface break dispatch */ }
|
|
2314
|
+
}
|
|
2227
2315
|
const result = await handleDispatchFromEmail({
|
|
2228
2316
|
dispatch,
|
|
2229
2317
|
worlds: WORLDS,
|