@pleri/olam-cli 0.1.182 → 0.1.185
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/agent-sdk-to-chunks.js +44 -30
- package/dist/ask/checkout.d.ts +19 -0
- package/dist/ask/checkout.d.ts.map +1 -0
- package/dist/ask/checkout.js +40 -0
- package/dist/ask/checkout.js.map +1 -0
- package/dist/ask/knowledge-pack-builder.d.ts +72 -0
- package/dist/ask/knowledge-pack-builder.d.ts.map +1 -0
- package/dist/ask/knowledge-pack-builder.js +91 -0
- package/dist/ask/knowledge-pack-builder.js.map +1 -0
- package/dist/ask/knowledge-pack.generated.d.ts +8 -0
- package/dist/ask/knowledge-pack.generated.d.ts.map +1 -0
- package/dist/ask/knowledge-pack.generated.js +1947 -0
- package/dist/ask/knowledge-pack.generated.js.map +1 -0
- package/dist/ask/one-shot.d.ts +21 -0
- package/dist/ask/one-shot.d.ts.map +1 -0
- package/dist/ask/one-shot.js +50 -0
- package/dist/ask/one-shot.js.map +1 -0
- package/dist/ask/repl.d.ts +30 -0
- package/dist/ask/repl.d.ts.map +1 -0
- package/dist/ask/repl.js +109 -0
- package/dist/ask/repl.js.map +1 -0
- package/dist/ask/sdk-client.d.ts +87 -0
- package/dist/ask/sdk-client.d.ts.map +1 -0
- package/dist/ask/sdk-client.js +118 -0
- package/dist/ask/sdk-client.js.map +1 -0
- package/dist/ask/system-prompt.d.ts +30 -0
- package/dist/ask/system-prompt.d.ts.map +1 -0
- package/dist/ask/system-prompt.js +31 -0
- package/dist/ask/system-prompt.js.map +1 -0
- package/dist/commands/ask.d.ts +27 -0
- package/dist/commands/ask.d.ts.map +1 -0
- package/dist/commands/ask.js +63 -0
- package/dist/commands/ask.js.map +1 -0
- package/dist/commands/auth-list-json.d.ts +53 -0
- package/dist/commands/auth-list-json.d.ts.map +1 -0
- package/dist/commands/auth-list-json.js +47 -0
- package/dist/commands/auth-list-json.js.map +1 -0
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +13 -0
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/doctor.js +11 -11
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/keys-list-json.d.ts +55 -0
- package/dist/commands/keys-list-json.d.ts.map +1 -0
- package/dist/commands/keys-list-json.js +54 -0
- package/dist/commands/keys-list-json.js.map +1 -0
- package/dist/commands/keys.d.ts.map +1 -1
- package/dist/commands/keys.js +6 -0
- package/dist/commands/keys.js.map +1 -1
- package/dist/commands/lanes-list-json.d.ts +69 -0
- package/dist/commands/lanes-list-json.d.ts.map +1 -0
- package/dist/commands/lanes-list-json.js +42 -0
- package/dist/commands/lanes-list-json.js.map +1 -0
- package/dist/commands/lanes.d.ts.map +1 -1
- package/dist/commands/lanes.js +18 -7
- package/dist/commands/lanes.js.map +1 -1
- package/dist/commands/plans-list-json.d.ts +77 -0
- package/dist/commands/plans-list-json.d.ts.map +1 -0
- package/dist/commands/plans-list-json.js +61 -0
- package/dist/commands/plans-list-json.js.map +1 -0
- package/dist/commands/plans.d.ts.map +1 -1
- package/dist/commands/plans.js +10 -0
- package/dist/commands/plans.js.map +1 -1
- package/dist/commands/repos-list-json.d.ts +58 -0
- package/dist/commands/repos-list-json.d.ts.map +1 -0
- package/dist/commands/repos-list-json.js +45 -0
- package/dist/commands/repos-list-json.js.map +1 -0
- package/dist/commands/repos.d.ts +1 -1
- package/dist/commands/repos.d.ts.map +1 -1
- package/dist/commands/repos.js +12 -2
- package/dist/commands/repos.js.map +1 -1
- package/dist/commands/services.d.ts +47 -1
- package/dist/commands/services.d.ts.map +1 -1
- package/dist/commands/services.js +59 -33
- package/dist/commands/services.js.map +1 -1
- package/dist/commands/skills.d.ts +27 -0
- package/dist/commands/skills.d.ts.map +1 -1
- package/dist/commands/skills.js +17 -2
- package/dist/commands/skills.js.map +1 -1
- package/dist/commands/workspace-list-json.d.ts +73 -0
- package/dist/commands/workspace-list-json.d.ts.map +1 -0
- package/dist/commands/workspace-list-json.js +59 -0
- package/dist/commands/workspace-list-json.js.map +1 -0
- package/dist/commands/workspace.d.ts.map +1 -1
- package/dist/commands/workspace.js +7 -1
- package/dist/commands/workspace.js.map +1 -1
- package/dist/image-digests.json +8 -8
- package/dist/index.js +3170 -580
- package/dist/index.js.map +1 -1
- package/dist/lib/k8s-bootstrap.d.ts.map +1 -1
- package/dist/lib/k8s-bootstrap.js +13 -1
- package/dist/lib/k8s-bootstrap.js.map +1 -1
- package/dist/lib/k8s-secret-render.d.ts +2 -0
- package/dist/lib/k8s-secret-render.d.ts.map +1 -1
- package/dist/lib/k8s-secret-render.js +27 -0
- package/dist/lib/k8s-secret-render.js.map +1 -1
- package/dist/lib/peripheral-registry.d.ts +1 -1
- package/dist/lib/peripheral-registry.d.ts.map +1 -1
- package/dist/lib/peripheral-registry.js +13 -0
- package/dist/lib/peripheral-registry.js.map +1 -1
- package/dist/lib/upgrade-kubernetes.d.ts +6 -0
- package/dist/lib/upgrade-kubernetes.d.ts.map +1 -1
- package/dist/lib/upgrade-kubernetes.js +7 -1
- package/dist/lib/upgrade-kubernetes.js.map +1 -1
- package/dist/mcp-server.js +1167 -37
- package/hermes-bundle/version.json +1 -1
- package/host-cp/k8s/manifests/30-configmap.yaml +11 -6
- package/host-cp/k8s/manifests/50-deployment.yaml +15 -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/k8s/templates/chunks-postgres-secret-template.yaml +24 -0
- package/host-cp/k8s/templates/plan-chat-service-secret-template.yaml +35 -0
- package/host-cp/src/plan-chat-service.mjs +99 -74
- package/host-cp/src/server.mjs +141 -5
- package/package.json +4 -2
|
@@ -24,12 +24,17 @@ data:
|
|
|
24
24
|
OLAM_WORLDS_DB: "/data/worlds.db"
|
|
25
25
|
OLAM_PLAN_DB_PATH: "/data/plan.db"
|
|
26
26
|
OLAM_PLAN_DIR: "/data/plan"
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
|
|
27
|
+
# Phase B Model B: bearer file is now sourced from the shared
|
|
28
|
+
# olam-plan-chat-secret Kubernetes Secret (mounted at /etc/olam-plan-chat/).
|
|
29
|
+
# Two readers, one source-of-truth — replaces the per-pod /data/plan-chat-secret
|
|
30
|
+
# file that couldn't be shared across pods on RWO PVCs. The plan-chat-service
|
|
31
|
+
# pod also mounts the SAME Secret at the SAME path so bearer comparisons
|
|
32
|
+
# work both ways.
|
|
33
|
+
OLAM_PLAN_CHAT_SECRET_PATH: "/etc/olam-plan-chat/secret"
|
|
34
|
+
# In-cluster plan-chat-service URL. Rewritten by upgrade-kubernetes.ts step 2.5
|
|
35
|
+
# (buildK8sDnsUrl) — the default below is a sane fallback for raw
|
|
36
|
+
# `kubectl apply -f` operators who skip the CLI wrapper.
|
|
37
|
+
PLAN_CHAT_SERVICE_URL: "http://olam-plan-chat-service.olam.svc.cluster.local:3200"
|
|
33
38
|
# NDJSON span sink + recovery ledger — route to the writable PVC mount at
|
|
34
39
|
# /data rather than the default ~/.olam/logs (which resolves to
|
|
35
40
|
# /home/node/.olam/logs and is not writable with readOnlyRootFilesystem: true).
|
|
@@ -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:843bea5159a7b5ec792a7d0a9766c49eb0443180023526c0c21d56e32982fd4e
|
|
122
122
|
imagePullPolicy: IfNotPresent
|
|
123
123
|
securityContext:
|
|
124
124
|
runAsNonRoot: true
|
|
@@ -147,6 +147,13 @@ spec:
|
|
|
147
147
|
readOnly: true
|
|
148
148
|
- name: tmp
|
|
149
149
|
mountPath: /tmp
|
|
150
|
+
# Phase B Model B: shared olam-plan-chat-secret mounted read-only
|
|
151
|
+
# so renderSpaShell can inject window.__OLAM_PLAN_CHAT_BEARER__.
|
|
152
|
+
# Plan-chat-service mounts the SAME Secret at the SAME path so
|
|
153
|
+
# bearer compares match across pods.
|
|
154
|
+
- name: plan-chat-secret
|
|
155
|
+
mountPath: /etc/olam-plan-chat
|
|
156
|
+
readOnly: true
|
|
150
157
|
# docker-socket volumeMount REMOVED in olam-k3d-on-mac-substrate-
|
|
151
158
|
# decision Phase B B2. Docker access now goes via TCP to the
|
|
152
159
|
# docker-socket-proxy ExternalName Service in the olam namespace.
|
|
@@ -191,6 +198,13 @@ spec:
|
|
|
191
198
|
type: DirectoryOrCreate
|
|
192
199
|
- name: tmp
|
|
193
200
|
emptyDir: {}
|
|
201
|
+
- name: plan-chat-secret
|
|
202
|
+
secret:
|
|
203
|
+
secretName: olam-plan-chat-secret
|
|
204
|
+
defaultMode: 0400
|
|
205
|
+
items:
|
|
206
|
+
- key: PLAN_CHAT_SECRET
|
|
207
|
+
path: secret
|
|
194
208
|
# host-colima + docker-socket volumes REMOVED in olam-k3d-on-mac-
|
|
195
209
|
# substrate-decision Phase B B2 (2026-05-21). R3-A's two-volume
|
|
196
210
|
# hostPath approach is fully retracted: round-4 R4-W2-F demonstrated
|
|
@@ -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:59de0618b656c45ed75465aefab637bd3b50ef803ae8d802c828c26d97183328
|
|
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:b53af63d452bb04420a1f89b5834888a324ee3995f45c908fa9321ec76b84047
|
|
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:635c1518e45cc0a1b5859cfb412d5a6eb9ab389630553eb9cdb6c903d97d8d15
|
|
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:efad791c760420e6bccc68120621aba97a2532da517eb3f7d0e0349f6a2f8a06
|
|
74
74
|
imagePullPolicy: IfNotPresent
|
|
75
75
|
securityContext:
|
|
76
76
|
runAsNonRoot: true
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Secret TEMPLATE for olam-chunks-postgres.
|
|
2
|
+
#
|
|
3
|
+
# Generates a random 64-char hex POSTGRES_PASSWORD on first apply (via
|
|
4
|
+
# k8s-secret-render.ts generate-if-missing). The Secret is consumed by:
|
|
5
|
+
# - chunks-postgres StatefulSet (envFrom → POSTGRES_PASSWORD)
|
|
6
|
+
# - chunks-electric Deployment (env: valueFrom.secretKeyRef)
|
|
7
|
+
# - plan-chat-service Deployment (env: valueFrom.secretKeyRef)
|
|
8
|
+
#
|
|
9
|
+
# All three resolve the SAME random value because the secret-renderer
|
|
10
|
+
# persists generated values in ~/.olam/k8s-secrets-state.json so reapply
|
|
11
|
+
# is idempotent (no rotation unless --rotate-secrets).
|
|
12
|
+
apiVersion: v1
|
|
13
|
+
kind: Secret
|
|
14
|
+
metadata:
|
|
15
|
+
name: olam-chunks-postgres-secret
|
|
16
|
+
namespace: olam
|
|
17
|
+
labels:
|
|
18
|
+
app: olam-chunks-postgres
|
|
19
|
+
olam.io/component: substrate
|
|
20
|
+
type: Opaque
|
|
21
|
+
stringData:
|
|
22
|
+
# Postgres superuser password. Generated by the CLI's secret-renderer on
|
|
23
|
+
# first apply (no host-side file to read; this is in-cluster-only state).
|
|
24
|
+
POSTGRES_PASSWORD: "REPLACE_ME_GENERATE_RANDOM_HEX"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Secret TEMPLATE for olam-plan-chat-secret.
|
|
2
|
+
#
|
|
3
|
+
# This file is a TEMPLATE — it MUST NOT be applied directly without substituting
|
|
4
|
+
# the placeholder values. The placeholders are intentionally invalid; a raw
|
|
5
|
+
# `kubectl apply` will result in auth failures rather than silently shipping
|
|
6
|
+
# fake credentials.
|
|
7
|
+
#
|
|
8
|
+
# Preferred substitution (keeps secrets out of git):
|
|
9
|
+
# kubectl create secret generic olam-plan-chat-secret -n olam \
|
|
10
|
+
# --from-literal=PLAN_CHAT_SECRET=$(cat ~/.olam/plan-chat-secret) \
|
|
11
|
+
# --dry-run=client -o yaml | kubectl apply -f -
|
|
12
|
+
#
|
|
13
|
+
# This template lives in packages/host-cp/k8s/templates/ (NOT manifests/)
|
|
14
|
+
# so that `kubectl apply -f manifests/plan-chat-service/` does NOT apply it —
|
|
15
|
+
# operators must explicitly handle Secret provisioning before applying manifests.
|
|
16
|
+
#
|
|
17
|
+
# Architecture: this Secret is mounted by BOTH the host-cp pod (so its
|
|
18
|
+
# renderSpaShell can inject window.__OLAM_PLAN_CHAT_BEARER__) AND the
|
|
19
|
+
# plan-chat-service pod (so its bearer-auth gate timing-safe-compares incoming
|
|
20
|
+
# Authorization: Bearer headers against the same value). One source-of-truth,
|
|
21
|
+
# two readers — replaces the previous "/data/plan-chat-secret in host-cp PVC"
|
|
22
|
+
# pattern that couldn't be shared across pods (RWO PVC).
|
|
23
|
+
apiVersion: v1
|
|
24
|
+
kind: Secret
|
|
25
|
+
metadata:
|
|
26
|
+
name: olam-plan-chat-secret
|
|
27
|
+
namespace: olam
|
|
28
|
+
labels:
|
|
29
|
+
olam.io/component: substrate
|
|
30
|
+
type: Opaque
|
|
31
|
+
stringData:
|
|
32
|
+
# Shared bearer secret for plan-chat-service's POST /v1/chunks and
|
|
33
|
+
# GET /v1/shape endpoints. host-cp injects this into window.__OLAM_PLAN_CHAT_BEARER__.
|
|
34
|
+
# Source: cat ~/.olam/plan-chat-secret
|
|
35
|
+
PLAN_CHAT_SECRET: "REPLACE_ME_FROM_HOME_DOTOLAM_PLAN_CHAT_SECRET"
|
|
@@ -327,88 +327,113 @@ export function createHandler({
|
|
|
327
327
|
|
|
328
328
|
// B4 — capture message_usage when the adapter forwarded token usage.
|
|
329
329
|
// body.usage is optional (only present on assistant turns with SDK usage).
|
|
330
|
+
//
|
|
331
|
+
// perf: SQL-2 through SQL-5 (INSERT message_usage, SUM, dedup check,
|
|
332
|
+
// INSERT threshold chunk) are batched inside a single BEGIN/COMMIT so
|
|
333
|
+
// the 4 sequential round-trips share one connection checkout and one
|
|
334
|
+
// server-side transaction flush instead of 4 independent pool checkouts.
|
|
335
|
+
// SQL-1 (INSERT chunks above) stays outside the txn so its 23505 dedup
|
|
336
|
+
// guard reaches the caller independently as a 409 — folding it in would
|
|
337
|
+
// cause a threshold-chunk PK conflict to silently roll back the primary
|
|
338
|
+
// chunk write.
|
|
330
339
|
if (body.usage && typeof body.usage === 'object') {
|
|
331
340
|
const usage = body.usage;
|
|
332
341
|
const model = typeof body.model === 'string' && body.model.length > 0
|
|
333
342
|
? body.model
|
|
334
343
|
: 'claude-sonnet-4-6';
|
|
335
|
-
await pool.
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
// cap. If so, and no prior threshold chunk exists for (session_id,
|
|
358
|
-
// actor_id), emit exactly one dedup'd system chunk.
|
|
359
|
-
//
|
|
360
|
-
// Token math: sum(input_tokens + cache_read_tokens + cache_create_tokens)
|
|
361
|
-
// per (session_id, actor_id), matching Claude Code statusline.
|
|
362
|
-
const contextCap = CONTEXT_CAPS_LOCAL[model] ?? DEFAULT_CONTEXT_CAP;
|
|
363
|
-
const sumResult = await pool.query(
|
|
364
|
-
`SELECT COALESCE(SUM(input_tokens + cache_read_tokens + cache_create_tokens), 0) AS total_used
|
|
365
|
-
FROM message_usage
|
|
366
|
-
WHERE session_id = $1 AND actor_id = $2`,
|
|
367
|
-
[body.session_id, principal.actorId],
|
|
368
|
-
);
|
|
369
|
-
const totalUsed = Number(sumResult.rows[0]?.total_used ?? 0);
|
|
370
|
-
const usedPct = Math.floor((totalUsed / contextCap) * 100);
|
|
371
|
-
|
|
372
|
-
if (usedPct >= THRESHOLD_PCT) {
|
|
373
|
-
// Dedup check: scan chunks for an existing threshold-crossing system
|
|
374
|
-
// chunk for this (session_id, actor_id). Prefix-match is sufficient
|
|
375
|
-
// because THRESHOLD_CHUNK_PREFIX is unique to this purpose.
|
|
376
|
-
const dupResult = await pool.query(
|
|
377
|
-
`SELECT 1 FROM chunks
|
|
378
|
-
WHERE session_id = $1 AND actor_id = $2 AND actor_type = 'system'
|
|
379
|
-
AND chunk LIKE $3
|
|
380
|
-
LIMIT 1`,
|
|
381
|
-
[body.session_id, principal.actorId, `${THRESHOLD_CHUNK_PREFIX}%`],
|
|
344
|
+
const client = await pool.connect();
|
|
345
|
+
try {
|
|
346
|
+
await client.query('BEGIN');
|
|
347
|
+
|
|
348
|
+
// SQL-2 — INSERT message_usage
|
|
349
|
+
await client.query(
|
|
350
|
+
`INSERT INTO message_usage
|
|
351
|
+
(world_id, session_id, message_id, actor_id, model,
|
|
352
|
+
input_tokens, output_tokens, cache_read_tokens, cache_create_tokens)
|
|
353
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
354
|
+
ON CONFLICT (message_id, actor_id) DO NOTHING`,
|
|
355
|
+
[
|
|
356
|
+
body.world_id,
|
|
357
|
+
body.session_id,
|
|
358
|
+
body.message_id,
|
|
359
|
+
principal.actorId,
|
|
360
|
+
model,
|
|
361
|
+
Number(usage.input_tokens ?? 0),
|
|
362
|
+
Number(usage.output_tokens ?? 0),
|
|
363
|
+
Number(usage.cache_read_input_tokens ?? 0),
|
|
364
|
+
Number(usage.cache_creation_input_tokens ?? 0),
|
|
365
|
+
],
|
|
382
366
|
);
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
367
|
+
|
|
368
|
+
// B6 — 80% threshold dedup'd system chunk.
|
|
369
|
+
// After inserting message_usage, check if the per-actor cumulative
|
|
370
|
+
// token count for this session has crossed 80% of the model's context
|
|
371
|
+
// cap. If so, and no prior threshold chunk exists for (session_id,
|
|
372
|
+
// actor_id), emit exactly one dedup'd system chunk.
|
|
373
|
+
//
|
|
374
|
+
// Token math: sum(input_tokens + cache_read_tokens + cache_create_tokens)
|
|
375
|
+
// per (session_id, actor_id), matching Claude Code statusline.
|
|
376
|
+
//
|
|
377
|
+
// SQL-3 — SUM cumulative tokens (reads within the same txn so the
|
|
378
|
+
// just-inserted usage row is visible without waiting for COMMIT).
|
|
379
|
+
const contextCap = CONTEXT_CAPS_LOCAL[model] ?? DEFAULT_CONTEXT_CAP;
|
|
380
|
+
const sumResult = await client.query(
|
|
381
|
+
`SELECT COALESCE(SUM(input_tokens + cache_read_tokens + cache_create_tokens), 0) AS total_used
|
|
382
|
+
FROM message_usage
|
|
383
|
+
WHERE session_id = $1 AND actor_id = $2`,
|
|
384
|
+
[body.session_id, principal.actorId],
|
|
385
|
+
);
|
|
386
|
+
const totalUsed = Number(sumResult.rows[0]?.total_used ?? 0);
|
|
387
|
+
const usedPct = Math.floor((totalUsed / contextCap) * 100);
|
|
388
|
+
|
|
389
|
+
if (usedPct >= THRESHOLD_PCT) {
|
|
390
|
+
// SQL-4 — dedup check: scan chunks for an existing threshold-crossing
|
|
391
|
+
// system chunk for this (session_id, actor_id). Prefix-match is
|
|
392
|
+
// sufficient because THRESHOLD_CHUNK_PREFIX is unique to this purpose.
|
|
393
|
+
const dupResult = await client.query(
|
|
394
|
+
`SELECT 1 FROM chunks
|
|
395
|
+
WHERE session_id = $1 AND actor_id = $2 AND actor_type = 'system'
|
|
396
|
+
AND chunk LIKE $3
|
|
397
|
+
LIMIT 1`,
|
|
398
|
+
[body.session_id, principal.actorId, `${THRESHOLD_CHUNK_PREFIX}%`],
|
|
410
399
|
);
|
|
400
|
+
const alreadyEmitted = (dupResult.rows ?? []).length > 0;
|
|
401
|
+
|
|
402
|
+
if (!alreadyEmitted) {
|
|
403
|
+
const personaLabel = principal.actorId;
|
|
404
|
+
const thresholdChunk =
|
|
405
|
+
`${THRESHOLD_CHUNK_PREFIX} ${personaLabel} at ${usedPct}%`;
|
|
406
|
+
// SQL-5 — Emit the threshold system chunk. Re-use the same message_id +
|
|
407
|
+
// a seq derived from the current seq + 1 to avoid PK conflict.
|
|
408
|
+
// Use body.seq + 1000 as a high-offset seq so it sorts AFTER the
|
|
409
|
+
// triggering chunk but doesn't collide with normal seq values.
|
|
410
|
+
const thresholdSeq = body.seq + 1000;
|
|
411
|
+
await client.query(
|
|
412
|
+
`INSERT INTO chunks
|
|
413
|
+
(world_id, session_id, message_id, seq, actor_id, actor_type, role, chunk, chunk_type)
|
|
414
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
415
|
+
ON CONFLICT (message_id, seq) DO NOTHING`,
|
|
416
|
+
[
|
|
417
|
+
body.world_id,
|
|
418
|
+
body.session_id,
|
|
419
|
+
body.message_id,
|
|
420
|
+
thresholdSeq,
|
|
421
|
+
principal.actorId,
|
|
422
|
+
'system',
|
|
423
|
+
'system',
|
|
424
|
+
thresholdChunk,
|
|
425
|
+
'text',
|
|
426
|
+
],
|
|
427
|
+
);
|
|
428
|
+
}
|
|
411
429
|
}
|
|
430
|
+
|
|
431
|
+
await client.query('COMMIT');
|
|
432
|
+
} catch (txnErr) {
|
|
433
|
+
await client.query('ROLLBACK').catch(() => undefined);
|
|
434
|
+
throw txnErr;
|
|
435
|
+
} finally {
|
|
436
|
+
client.release();
|
|
412
437
|
}
|
|
413
438
|
}
|
|
414
439
|
// H2 (plan-chat-spa-canonical-surface Phase G) — extract commit_plan /
|
package/host-cp/src/server.mjs
CHANGED
|
@@ -95,6 +95,7 @@ import {
|
|
|
95
95
|
} from './routes/process-port.mjs';
|
|
96
96
|
import { instrumentHandler, renderMetrics } from './metrics.mjs';
|
|
97
97
|
import { handleDispatchFromEmail } from './lib/email-dispatch.mjs';
|
|
98
|
+
import { handleDispatchFromLinear } from './lib/linear-dispatch.mjs';
|
|
98
99
|
import { emitTierSuggestion } from '../dispatch/auto-tier-scheduler.mjs';
|
|
99
100
|
import { isServeOnly, isOrchestrationRoute, ORCHESTRATION_UNAVAILABLE } from './serve-only-config.mjs';
|
|
100
101
|
|
|
@@ -190,6 +191,18 @@ const OLAM_EMAIL_ATTACHMENTS_ROOT =
|
|
|
190
191
|
(HOST_CP_MODE === 'container'
|
|
191
192
|
? '/data/email-attachments'
|
|
192
193
|
: path.join(os.homedir(), '.olam', 'email-attachments'));
|
|
194
|
+
// Linear-webhook trigger (POST /v1/dispatch-from-linear).
|
|
195
|
+
// OLAM_LINEAR_WEBHOOK_SECRET — shared secret set in Linear webhook settings.
|
|
196
|
+
// OLAM_LINEAR_WORLD_ID — optional; when set, route events to this worldId
|
|
197
|
+
// instead of spawning a new world each time.
|
|
198
|
+
// See docs/architecture/linear-as-trigger.md.
|
|
199
|
+
const OLAM_LINEAR_WEBHOOK_SECRET = process.env.OLAM_LINEAR_WEBHOOK_SECRET ?? '';
|
|
200
|
+
const OLAM_LINEAR_WORLD_ID = process.env.OLAM_LINEAR_WORLD_ID ?? '';
|
|
201
|
+
|
|
202
|
+
// In-flight delivery IDs for deduplication. Linear retries on non-2xx;
|
|
203
|
+
// we MUST return 2xx for duplicates so retries terminate. Never use 409.
|
|
204
|
+
const linearInFlight = new Set();
|
|
205
|
+
|
|
193
206
|
const WORLD_NAMES_PATH =
|
|
194
207
|
process.env.OLAM_WORLD_NAMES_PATH ??
|
|
195
208
|
(HOST_CP_MODE === 'container'
|
|
@@ -319,6 +332,28 @@ function readAnthropicBaseUrl() {
|
|
|
319
332
|
return '';
|
|
320
333
|
}
|
|
321
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Resolve the default `repoUrl` to inject into cloud-dispatch bodies. The
|
|
337
|
+
* cloud Sandbox coding loop clones this repo into `/workspace/repo` before
|
|
338
|
+
* running claude. Source order: OLAM_DOGFOOD_REPO_URL env, then
|
|
339
|
+
* ~/.olam/dogfood-repo-url file (one bare URL, chmod 600). Absent → empty
|
|
340
|
+
* string → no injection (dispatch runs text-only, the pre-coding-default
|
|
341
|
+
* behaviour). Mirrors readAnthropicBaseUrl() so operators have ONE pattern
|
|
342
|
+
* for cloud-dispatch defaults.
|
|
343
|
+
*/
|
|
344
|
+
function readDogfoodRepoUrl() {
|
|
345
|
+
const fromOlamEnv = process.env['OLAM_DOGFOOD_REPO_URL'];
|
|
346
|
+
if (fromOlamEnv && fromOlamEnv.length > 0) return fromOlamEnv.trim();
|
|
347
|
+
try {
|
|
348
|
+
const file = path.join(os.homedir(), '.olam', 'dogfood-repo-url');
|
|
349
|
+
const content = fs.readFileSync(file, 'utf-8').trim();
|
|
350
|
+
if (content.length > 0) return content;
|
|
351
|
+
} catch {
|
|
352
|
+
// file absent — fall through
|
|
353
|
+
}
|
|
354
|
+
return '';
|
|
355
|
+
}
|
|
356
|
+
|
|
322
357
|
/** @type {Record<string, number>} */
|
|
323
358
|
let WORLDS = {};
|
|
324
359
|
|
|
@@ -974,6 +1009,7 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
|
|
|
974
1009
|
header_name: 'authorization',
|
|
975
1010
|
header_format: 'Bearer <token>',
|
|
976
1011
|
hint: 'SPA: set document.cookie = `olam_host_cp_token=${token}; path=/; samesite=strict` then fetch (`/api/world/...`) freely.',
|
|
1012
|
+
cloud_enabled: Boolean(process.env.OLAM_CLOUD_URL && process.env.OLAM_SHOWCASE_PASSWORD),
|
|
977
1013
|
});
|
|
978
1014
|
}
|
|
979
1015
|
|
|
@@ -2447,6 +2483,62 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
|
|
|
2447
2483
|
return;
|
|
2448
2484
|
}
|
|
2449
2485
|
|
|
2486
|
+
// POST /v1/dispatch-from-linear — Linear webhook receiver.
|
|
2487
|
+
//
|
|
2488
|
+
// Linear POSTs a JSON payload and an HMAC-SHA256 signature in
|
|
2489
|
+
// X-Linear-Signature. host-cp validates the signature, deduplicates
|
|
2490
|
+
// by webhookId (Linear retries on non-2xx — duplicates MUST return 202),
|
|
2491
|
+
// and either routes to a pinned world (OLAM_LINEAR_WORLD_ID) or returns
|
|
2492
|
+
// a spawn_pending descriptor for the MCP/CLI layer.
|
|
2493
|
+
//
|
|
2494
|
+
// Dedup rule: HTTP 202 { action: 'deduplicated' } — NEVER 409.
|
|
2495
|
+
// Body cap: 1 MiB (Linear payloads are small JSON, no attachments).
|
|
2496
|
+
if (url.pathname === '/v1/dispatch-from-linear' && req.method === 'POST') {
|
|
2497
|
+
const chunks = [];
|
|
2498
|
+
let size = 0;
|
|
2499
|
+
const MAX_BODY = 1 * 1024 * 1024;
|
|
2500
|
+
let aborted = false;
|
|
2501
|
+
req.on('data', (chunk) => {
|
|
2502
|
+
size += chunk.length;
|
|
2503
|
+
if (size > MAX_BODY) {
|
|
2504
|
+
aborted = true;
|
|
2505
|
+
jsonReply(res, 413, { error: 'body_too_large', maxBytes: MAX_BODY });
|
|
2506
|
+
req.destroy();
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
chunks.push(chunk);
|
|
2510
|
+
});
|
|
2511
|
+
req.on('end', async () => {
|
|
2512
|
+
if (aborted) return;
|
|
2513
|
+
const rawBody = Buffer.concat(chunks);
|
|
2514
|
+
const signature = req.headers['x-linear-signature'] ?? '';
|
|
2515
|
+
let payload;
|
|
2516
|
+
try {
|
|
2517
|
+
payload = JSON.parse(rawBody.toString('utf8') || '{}');
|
|
2518
|
+
} catch (err) {
|
|
2519
|
+
return jsonReply(res, 400, { error: 'invalid_json', message: err.message });
|
|
2520
|
+
}
|
|
2521
|
+
try {
|
|
2522
|
+
const result = await handleDispatchFromLinear({
|
|
2523
|
+
rawBody,
|
|
2524
|
+
payload,
|
|
2525
|
+
signature,
|
|
2526
|
+
secret: OLAM_LINEAR_WEBHOOK_SECRET,
|
|
2527
|
+
worlds: WORLDS,
|
|
2528
|
+
inFlight: linearInFlight,
|
|
2529
|
+
targetWorldId: OLAM_LINEAR_WORLD_ID || undefined,
|
|
2530
|
+
});
|
|
2531
|
+
return jsonReply(res, result.status, result.body);
|
|
2532
|
+
} catch (err) {
|
|
2533
|
+
return jsonReply(res, 500, {
|
|
2534
|
+
error: 'dispatch_failed',
|
|
2535
|
+
message: err instanceof Error ? err.message : String(err),
|
|
2536
|
+
});
|
|
2537
|
+
}
|
|
2538
|
+
});
|
|
2539
|
+
return;
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2450
2542
|
if (url.pathname === '/api/cloud-dispatch' && req.method === 'POST') {
|
|
2451
2543
|
const cloudUrl = process.env.OLAM_CLOUD_URL;
|
|
2452
2544
|
const showcasePw = process.env.OLAM_SHOWCASE_PASSWORD;
|
|
@@ -2473,14 +2565,38 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
|
|
|
2473
2565
|
const planId = parsed.session_id ?? 'default';
|
|
2474
2566
|
const basicAuth = Buffer.from(`operator:${showcasePw}`).toString('base64');
|
|
2475
2567
|
|
|
2568
|
+
// SPA shape → plan-DO shape normalisation: the SPA posts a chat-shaped
|
|
2569
|
+
// body ({ messages: [{ role, content }] }) while plan-DO's /v1/dispatch
|
|
2570
|
+
// expects a top-level `prompt` string. Synthesise `prompt` from
|
|
2571
|
+
// `messages` when it is absent so both call shapes reach plan-DO intact.
|
|
2572
|
+
// Existing curl/CLI callers that already supply `prompt` are unaffected.
|
|
2573
|
+
if (Array.isArray(parsed.messages) && !parsed.prompt) {
|
|
2574
|
+
parsed = {
|
|
2575
|
+
...parsed,
|
|
2576
|
+
prompt: parsed.messages.map((m) => m?.content ?? '').filter(Boolean).join('\n\n'),
|
|
2577
|
+
};
|
|
2578
|
+
// Keep `messages` too — plan-DO may use it for multi-turn fidelity later.
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2476
2581
|
// Gap 3: enrich the dispatch body with the operator's anthropicBaseUrl
|
|
2477
2582
|
// so plan-DO can propagate it to spawned CF Sandbox child worlds.
|
|
2478
2583
|
// Only injected when not already set by the SPA (SPA has no auth-worker
|
|
2479
2584
|
// config knowledge — host-cp is the sole injection point).
|
|
2480
2585
|
const anthropicBaseUrl = readAnthropicBaseUrl();
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2586
|
+
// Sibling injection: default repoUrl for the cloud Sandbox coding loop.
|
|
2587
|
+
// The SPA has no repo-config knowledge today (Decision 2 in
|
|
2588
|
+
// olam-builds-olam-cloud-dogfood: no SPA repo-selector); host-cp is the
|
|
2589
|
+
// injection point. Source order: OLAM_DOGFOOD_REPO_URL env, then
|
|
2590
|
+
// ~/.olam/dogfood-repo-url file. Absent → no injection (the dispatch
|
|
2591
|
+
// runs text-only, like before this change).
|
|
2592
|
+
const dogfoodRepoUrl = readDogfoodRepoUrl();
|
|
2593
|
+
let enrichedObj = null;
|
|
2594
|
+
if (anthropicBaseUrl && !parsed.anthropicBaseUrl) enrichedObj = { ...(enrichedObj ?? parsed), anthropicBaseUrl };
|
|
2595
|
+
if (dogfoodRepoUrl && !parsed.repoUrl) enrichedObj = { ...(enrichedObj ?? parsed), repoUrl: dogfoodRepoUrl };
|
|
2596
|
+
// Use `parsed` (not raw `body`) as the no-enrichment fallback so that the
|
|
2597
|
+
// messages→prompt normalisation above is always forwarded even when no
|
|
2598
|
+
// env-var enrichments are applied.
|
|
2599
|
+
const enriched = enrichedObj ? JSON.stringify(enrichedObj) : JSON.stringify(parsed);
|
|
2484
2600
|
|
|
2485
2601
|
// Phase H h2: attach CF Access service-token headers when configured
|
|
2486
2602
|
// (machine-to-machine auth). Additive alongside Basic auth. CF Access
|
|
@@ -3282,6 +3398,25 @@ function buildPlanChatBearerInjection() {
|
|
|
3282
3398
|
}
|
|
3283
3399
|
}
|
|
3284
3400
|
|
|
3401
|
+
/**
|
|
3402
|
+
* Build the cloud-enabled flag injection. plan-chat-spa's CloudToggleChip
|
|
3403
|
+
* (`getCloudAvailability` in `lib/plan-config.ts`) reads
|
|
3404
|
+
* `window.__OLAM_CLOUD_ENABLED__` to decide whether the Cloud toggle is
|
|
3405
|
+
* live; absent → the chip stays disabled and only links to the setup doc.
|
|
3406
|
+
*
|
|
3407
|
+
* The toggle should be live exactly when host-cp can actually proxy
|
|
3408
|
+
* `/api/cloud-dispatch` → plan-DO — which is the SAME condition the
|
|
3409
|
+
* cloud-dispatch handler guards with its `503 cloud_dispatch_not_configured`:
|
|
3410
|
+
* BOTH `OLAM_CLOUD_URL` and `OLAM_SHOWCASE_PASSWORD` set. Mirror it exactly
|
|
3411
|
+
* so the UI never offers a toggle that would 503 on dispatch.
|
|
3412
|
+
*/
|
|
3413
|
+
function buildCloudEnabledInjection() {
|
|
3414
|
+
const enabled = Boolean(
|
|
3415
|
+
process.env.OLAM_CLOUD_URL && process.env.OLAM_SHOWCASE_PASSWORD,
|
|
3416
|
+
);
|
|
3417
|
+
return enabled ? `<script>window.__OLAM_CLOUD_ENABLED__=true;</script>` : '';
|
|
3418
|
+
}
|
|
3419
|
+
|
|
3285
3420
|
// Phase E5 (ATOMIC SERVING CUTOVER) — BOOTSTRAP_SCRIPT no longer injected.
|
|
3286
3421
|
//
|
|
3287
3422
|
// host-cp now serves plan-chat-spa exclusively. plan-chat-spa's own
|
|
@@ -3307,6 +3442,7 @@ function buildPlanChatBearerInjection() {
|
|
|
3307
3442
|
async function renderSpaShell(filePath, pathname) {
|
|
3308
3443
|
const stat = fs.statSync(filePath);
|
|
3309
3444
|
const bearerInjection = buildPlanChatBearerInjection();
|
|
3445
|
+
const cloudInjection = buildCloudEnabledInjection();
|
|
3310
3446
|
// Phase E5: BOOTSTRAP_SCRIPT is never injected — plan-chat-spa's own
|
|
3311
3447
|
// worldFetch.ts shim owns the cookie-bootstrap + path-rewrite contract.
|
|
3312
3448
|
// We still assert the wildcard invariant so a future re-narrowing of
|
|
@@ -3315,7 +3451,7 @@ async function renderSpaShell(filePath, pathname) {
|
|
|
3315
3451
|
const skipBootstrap = isPlanningPath(pathname);
|
|
3316
3452
|
// Cache key includes bearer length (the only per-render-varying input
|
|
3317
3453
|
// now that bootstrap injection is constant-empty).
|
|
3318
|
-
const cacheKey = stat.mtimeMs + ':' + bearerInjection.length;
|
|
3454
|
+
const cacheKey = stat.mtimeMs + ':' + bearerInjection.length + ':' + cloudInjection.length;
|
|
3319
3455
|
const cached = _spaCacheByKey.get(cacheKey);
|
|
3320
3456
|
if (cached !== undefined) return cached;
|
|
3321
3457
|
let html = fs.readFileSync(filePath, 'utf-8');
|
|
@@ -3329,7 +3465,7 @@ async function renderSpaShell(filePath, pathname) {
|
|
|
3329
3465
|
// is set before the SPA bundle reads it. No bootstrap shim — see the
|
|
3330
3466
|
// block comment above (Phase E5 cutover).
|
|
3331
3467
|
void skipBootstrap; // wildcard invariant: always true; documents intent
|
|
3332
|
-
html = html.replace(/<head>/i, `<head>\n ${bearerInjection}`);
|
|
3468
|
+
html = html.replace(/<head>/i, `<head>\n ${bearerInjection}${cloudInjection}`);
|
|
3333
3469
|
_spaCacheByKey.set(cacheKey, html);
|
|
3334
3470
|
return html;
|
|
3335
3471
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pleri/olam-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.185",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"olam": "./bin/olam.cjs"
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
|
-
"build": "tsc",
|
|
29
|
+
"build": "npm run gen:knowledge-pack && tsc",
|
|
30
|
+
"gen:knowledge-pack": "node scripts/gen-knowledge-pack.mjs",
|
|
30
31
|
"dev": "tsx src/index.ts",
|
|
31
32
|
"test": "vitest run --passWithNoTests",
|
|
32
33
|
"test:ci": "vitest run --reporter=basic --passWithNoTests",
|
|
@@ -45,6 +46,7 @@
|
|
|
45
46
|
"ssh2": "^1.16.0",
|
|
46
47
|
"yaml": "^2.7.0",
|
|
47
48
|
"@inquirer/prompts": "^7.0.0",
|
|
49
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.56",
|
|
48
50
|
"zod-to-json-schema": "^3.24.0",
|
|
49
51
|
"playwright-core": "~1.59.0",
|
|
50
52
|
"@napi-rs/keyring": "^1.1.6"
|