@coreplane/switchboard 1.216.2 → 1.218.0

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.
Files changed (32) hide show
  1. package/dist/assets/config/config.example.yaml +6 -0
  2. package/dist/assets/deploy/cloudflare/package.json +1 -1
  3. package/dist/assets/deploy/cloudflare/worker.ts +4 -0
  4. package/dist/assets/deploy/cloudflare-docs/package.json +1 -1
  5. package/dist/assets/deploy/cloudflare-memory/package.json +1 -1
  6. package/dist/assets/deploy/cloudflare-memory/worker.ts +96 -6
  7. package/dist/assets/deploy/cloudflare-resident/package.json +1 -1
  8. package/dist/assets/deploy/cloudflare-sandbox/package.json +1 -1
  9. package/dist/assets/deploy/secrets.manifest.json +12 -0
  10. package/dist/assets/package-lock.json +5676 -7966
  11. package/dist/assets/package.json +2 -2
  12. package/dist/assets/source.json +3 -3
  13. package/dist/assets/src/agents/registry.ts +18 -0
  14. package/dist/assets/src/core/runEvents.ts +13 -0
  15. package/dist/assets/src/core/runFriction.ts +4 -2
  16. package/dist/assets/src/core/runLedger/sessionLog.ts +22 -0
  17. package/dist/assets/src/core/runLedger/types.ts +17 -0
  18. package/dist/assets/src/core/runRecord.ts +4 -2
  19. package/dist/assets/src/mcp/registry.ts +33 -5
  20. package/dist/assets/web/dist/.vite/manifest.json +18 -18
  21. package/dist/assets/web/dist/assets/{ResidentDetailPage-CvwjhjlG.js → ResidentDetailPage-DvCt96Dk.js} +1 -1
  22. package/dist/assets/web/dist/assets/{ResidentsIndexPage-DoN6XuNx.js → ResidentsIndexPage-BZaYja1-.js} +1 -1
  23. package/dist/assets/web/dist/assets/{RunRoutePage-FzLNexjF.js → RunRoutePage-DI4vzCUu.js} +4 -4
  24. package/dist/assets/web/dist/assets/{RunsIndexPage-DhSUbF9k.js → RunsIndexPage-Ch9uP4p4.js} +1 -1
  25. package/dist/assets/web/dist/assets/{ScheduledPage-Bq0Yg4nT.js → ScheduledPage-DAkAHMCH.js} +1 -1
  26. package/dist/assets/web/dist/assets/{StatusDot-D8Wwt1KC.js → StatusDot-CfBonGcV.js} +1 -1
  27. package/dist/assets/web/dist/assets/{Tooltip-DpDK7jWZ.js → Tooltip-CBD36E3W.js} +1 -1
  28. package/dist/assets/web/dist/assets/dist-DQ1U73LF.js +1542 -0
  29. package/dist/assets/web/dist/assets/{main-BFkEOy3K.js → main-CdLnG-yl.js} +2 -2
  30. package/dist/cli.js +1066 -343
  31. package/package.json +1 -1
  32. package/dist/assets/web/dist/assets/dist-B7BVkB7x.js +0 -1547
@@ -473,6 +473,12 @@ workspaceDir: ./workspaces
473
473
  # auth: bearer
474
474
  # tokenEnv: MCP_GITHUB_TOKEN # static bearer from the bot env (startup fails if unset)
475
475
  # agents: [general, research, coding] # default [general, research]
476
+ # lake: # a server behind Cloudflare Access: the gate wants a service token,
477
+ # url: https://vega.example.com/mcp # not an Authorization header — name the two headers and the bot
478
+ # auth: none # env vars holding them (manifest secrets on Cloudflare); an unset
479
+ # headersEnv: # one makes the server `unavailable` for that run, naming it
480
+ # CF-Access-Client-Id: MCP_ACCESS_CLIENT_ID
481
+ # CF-Access-Client-Secret: MCP_ACCESS_CLIENT_SECRET
476
482
  # channels:
477
483
  # "slack:C0123":
478
484
  # mcpServers:
@@ -24,6 +24,6 @@
24
24
  "@cloudflare/workers-types": "^5.20260904.1",
25
25
  "typescript": "^5.9.3",
26
26
  "vitest": "^5.0.0",
27
- "wrangler": "^4.129.0"
27
+ "wrangler": "^4.129.1"
28
28
  }
29
29
  }
@@ -90,6 +90,8 @@ export interface Env {
90
90
  ANTHROPIC_ADMIN_KEY?: string; // costs dash (optional): Anthropic Admin API key for the LLM cost report
91
91
  MEMORY_TOKEN?: string; // durable memory + friction ledger + schedule firings + MCP registry: bearer for the state Worker
92
92
  MCP_CREDENTIAL_KEY?: string; // MCP registry: the bot-only key that seals server credentials before they reach the McpDO
93
+ MCP_ACCESS_CLIENT_ID?: string; // MCP servers behind Cloudflare Access: the service token's client id, named by a server's `headersEnv`
94
+ MCP_ACCESS_CLIENT_SECRET?: string; // MCP servers behind Cloudflare Access: the service token's client secret, named by a server's `headersEnv`
93
95
  STATE_WORKER_URL?: string; // var: the state Worker's base URL — where this shim records each scheduled firing
94
96
  ARTIFACTS_R2_ACCESS_KEY_ID?: string; // artifact store: the bucket-scoped S3 token the bot signs presigned URLs with
95
97
  ARTIFACTS_R2_SECRET_ACCESS_KEY?: string; // artifact store: the secret half of that token
@@ -124,6 +126,8 @@ const FORWARDED_OPTIONAL = [
124
126
  "BRAVE_SEARCH_API_KEY",
125
127
  "MEMORY_TOKEN",
126
128
  "MCP_CREDENTIAL_KEY",
129
+ "MCP_ACCESS_CLIENT_ID",
130
+ "MCP_ACCESS_CLIENT_SECRET",
127
131
  "STATE_WORKER_URL",
128
132
  "ARTIFACTS_R2_ACCESS_KEY_ID",
129
133
  "ARTIFACTS_R2_SECRET_ACCESS_KEY",
@@ -13,6 +13,6 @@
13
13
  "tail": "wrangler tail"
14
14
  },
15
15
  "devDependencies": {
16
- "wrangler": "^4.129.0"
16
+ "wrangler": "^4.129.1"
17
17
  }
18
18
  }
@@ -20,6 +20,6 @@
20
20
  "@cloudflare/workers-types": "^5.20260904.1",
21
21
  "typescript": "^5.9.3",
22
22
  "vitest": "^4.1.11",
23
- "wrangler": "^4.129.0"
23
+ "wrangler": "^4.129.1"
24
24
  }
25
25
  }
@@ -46,7 +46,10 @@ import {
46
46
  attachmentRefsOf,
47
47
  DEFAULT_SESSION_LOG_MAX_BYTES,
48
48
  droppedToolResultRow,
49
+ GAP_MARKER,
50
+ NOTEPAD_MAX_BYTES,
49
51
  planSessionTrim,
52
+ roleOfStoredRow,
50
53
  rowKind,
51
54
  sessionsToDrop,
52
55
  tailCut,
@@ -80,6 +83,7 @@ import {
80
83
  type LiveRunRow,
81
84
  type ReclaimedRun,
82
85
  type RunState,
86
+ type SessionHit,
83
87
  type StepRecord,
84
88
  type StopMode,
85
89
  type TranscriptAttachment,
@@ -210,6 +214,10 @@ const FTS_CANDIDATES_FLOOR = 50;
210
214
  * tokens and are untouched; the engine still ranks with the FULL query, so the
211
215
  * cap only shapes which rows can become candidates. */
212
216
  const MAX_MATCH_TOKENS = 24;
217
+ /** A `recall` query's size and the most hits one answers (session-log item 10):
218
+ * a query is a few words, and the tool's default is five. */
219
+ const MAX_SEARCH_QUERY_BYTES = 1_024;
220
+ const MAX_SEARCH_HITS = 50;
213
221
  /** Request body ceiling, checked against Content-Length before parsing. A full
214
222
  * batch (50 × 4000-char texts + keywords + envelope) fits comfortably. */
215
223
  const MAX_BODY_BYTES = 512 * 1024;
@@ -2892,18 +2900,65 @@ export class SessionLogDO extends DurableObject<Env> {
2892
2900
  return { ...(await this.read(from)), from };
2893
2901
  }
2894
2902
 
2895
- /** The rows whose text matches `query`, newest first. */
2896
- async search(query: string, limit: number): Promise<Array<{ idx: number; part: number; text: string }>> {
2903
+ /** The rows whose text matches `query`, in relevance order (FTS5's bm25 rank,
2904
+ * the order the memory store's search uses; the newest first among equals),
2905
+ * each with its turn, part, role, kind and indexed text — what `recall`
2906
+ * answers (session-log item 10). */
2907
+ async search(query: string, limit: number): Promise<SessionHit[]> {
2897
2908
  const match = ftsMatchExpr(query);
2898
2909
  if (match === null) return [];
2899
2910
  return this.sql
2900
- .exec<{ idx: number; part: number; text: string }>(
2901
- `SELECT t.idx, t.part, t.text FROM turns_fts f JOIN turns t ON t.id = f.rowid
2902
- WHERE turns_fts MATCH ? ORDER BY t.idx DESC, t.part DESC LIMIT ?`,
2911
+ .exec<{ idx: number; part: number; kind: string; json: string; text: string }>(
2912
+ `SELECT t.idx, t.part, t.kind, t.json, t.text FROM turns_fts f JOIN turns t ON t.id = f.rowid
2913
+ WHERE turns_fts MATCH ? ORDER BY f.rank, t.idx DESC, t.part DESC LIMIT ?`,
2903
2914
  match,
2904
2915
  limit,
2905
2916
  )
2906
- .toArray();
2917
+ .toArray()
2918
+ .map(({ idx, part, kind, json, text }) => {
2919
+ const role = roleOfStoredRow(json);
2920
+ return { idx, part, ...(role !== undefined ? { role } : {}), kind, text };
2921
+ });
2922
+ }
2923
+
2924
+ /** The gap markers (session-log item 9) whose turn lies in `[from, to]`: the
2925
+ * rows a follow-up appended because the run before it detached, so a search
2926
+ * whose hits straddle one can say the log ends short between them. */
2927
+ async gapsBetween(from: number, to: number): Promise<number[]> {
2928
+ return this.sql
2929
+ .exec<{ idx: number }>(
2930
+ `SELECT DISTINCT idx FROM turns WHERE idx >= ? AND idx <= ? AND kind = 'text' AND text = ? ORDER BY idx`,
2931
+ from,
2932
+ to,
2933
+ GAP_MARKER,
2934
+ )
2935
+ .toArray()
2936
+ .map((r) => r.idx);
2937
+ }
2938
+
2939
+ /** The notepad, replaced whole by the live run (session-log item 10): the
2940
+ * same fence as a row write — unknown-run before an owner, fenced for
2941
+ * another generation — and the write's time kept beside the text. */
2942
+ async writeNotepad(gen: string, text: string, now: number): Promise<FenceResult> {
2943
+ let out: FenceResult = { ok: true };
2944
+ this.ctx.storage.transactionSync(() => {
2945
+ const owner = this.owner();
2946
+ if (owner === undefined) {
2947
+ out = { ok: false, reason: "unknown-run" };
2948
+ return;
2949
+ }
2950
+ if (owner.gen !== gen) {
2951
+ out = { ok: false, reason: "fenced" };
2952
+ return;
2953
+ }
2954
+ this.sql.exec(
2955
+ `INSERT INTO notepad (k, text, updated_at) VALUES (1, ?, ?)
2956
+ ON CONFLICT(k) DO UPDATE SET text = excluded.text, updated_at = excluded.updated_at`,
2957
+ text,
2958
+ now,
2959
+ );
2960
+ });
2961
+ return out;
2907
2962
  }
2908
2963
 
2909
2964
  async bytes(): Promise<number> {
@@ -2968,6 +3023,9 @@ const LEDGER_ROUTES = new Set([
2968
3023
  "/runs/session/read",
2969
3024
  "/runs/session/read-tail",
2970
3025
  "/runs/session/clear-owner",
3026
+ "/runs/session/search",
3027
+ "/runs/session/notepad",
3028
+ "/runs/session/notepad/write",
2971
3029
  ]);
2972
3030
 
2973
3031
  /** Routes whose bodies may carry a record, a transcript chunk, or an event batch. */
@@ -3187,6 +3245,38 @@ async function handleLedger(pathname: string, body: unknown, env: Env): Promise<
3187
3245
  if (!g.ok) return json({ error: g.error }, 400);
3188
3246
  return fenced(await stub.clearOwner(runId.value, g.value));
3189
3247
  }
3248
+ // `recall` (session-log item 10): the hits in relevance order, and the gap
3249
+ // markers that lie between the oldest and the newest of them.
3250
+ if (pathname === "/runs/session/search") {
3251
+ if (
3252
+ typeof b.query !== "string" ||
3253
+ b.query.trim().length === 0 ||
3254
+ utf8ByteLength(b.query) > MAX_SEARCH_QUERY_BYTES
3255
+ )
3256
+ return json({ error: `query must be a non-empty string of at most ${MAX_SEARCH_QUERY_BYTES} bytes` }, 400);
3257
+ const limit = b.limit;
3258
+ if (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1 || limit > MAX_SEARCH_HITS)
3259
+ return json({ error: `limit must be an integer in 1..${MAX_SEARCH_HITS}` }, 400);
3260
+ const hits = await stub.search(b.query, limit);
3261
+ const gaps =
3262
+ hits.length > 1
3263
+ ? await stub.gapsBetween(Math.min(...hits.map((h) => h.idx)), Math.max(...hits.map((h) => h.idx)))
3264
+ : [];
3265
+ console.log(`[runs/session/search] ${key.value} -> ${hits.length} hit(s), ${gaps.length} gap(s)`);
3266
+ return json({ hits, gaps });
3267
+ }
3268
+ if (pathname === "/runs/session/notepad") return json({ notepad: await stub.notepad() });
3269
+ if (pathname === "/runs/session/notepad/write") {
3270
+ const g = gen(b.gen);
3271
+ if (!g.ok) return json({ error: g.error }, 400);
3272
+ if (typeof b.text !== "string") return json({ error: "text must be a string" }, 400);
3273
+ const bytes = utf8ByteLength(b.text);
3274
+ if (bytes > NOTEPAD_MAX_BYTES)
3275
+ return json({ error: `text is ${bytes} bytes; the notepad holds at most ${NOTEPAD_MAX_BYTES}` }, 400);
3276
+ const r = await stub.writeNotepad(g.value, b.text, systemClock());
3277
+ console.log(`[runs/session/notepad/write] ${key.value} <- ${bytes} byte(s), ok=${r.ok}`);
3278
+ return fenced(r);
3279
+ }
3190
3280
  return json({ error: "not found" }, 404);
3191
3281
  }
3192
3282
 
@@ -24,6 +24,6 @@
24
24
  "@cloudflare/workers-types": "^5.20260904.1",
25
25
  "typescript": "^5.9.3",
26
26
  "vitest": "^5.0.0",
27
- "wrangler": "^4.129.0"
27
+ "wrangler": "^4.129.1"
28
28
  }
29
29
  }
@@ -21,6 +21,6 @@
21
21
  "devDependencies": {
22
22
  "@cloudflare/workers-types": "^5.20260904.1",
23
23
  "typescript": "^5.9.3",
24
- "wrangler": "^4.129.0"
24
+ "wrangler": "^4.129.1"
25
25
  }
26
26
  }
@@ -99,6 +99,18 @@
99
99
  "optional": true,
100
100
  "note": "AES-256-GCM key (32 bytes, base64: `openssl rand -base64 32`) the bot seals MCP server credentials with before they go to the state Worker's McpDO (docs/reference/specs/mcp-tools.md). Bot-only by design: the Worker stores ciphertext. Rotation re-seals every credential — not built yet; rotate = users re-run `mcp connect`."
101
101
  },
102
+ {
103
+ "name": "MCP_ACCESS_CLIENT_ID",
104
+ "workers": ["bot"],
105
+ "optional": true,
106
+ "note": "Client id of the Cloudflare Access service token the bot presents to MCP servers behind Access — a pinned server's `headersEnv` names it under `CF-Access-Client-Id` (docs/reference/specs/mcp-tools.md item 11). Minted in Zero Trust → Access → Service Auth of the account that guards the server, and enrolled on that Access application's policy. Optional: without it such a server is `unavailable` naming the variable."
107
+ },
108
+ {
109
+ "name": "MCP_ACCESS_CLIENT_SECRET",
110
+ "workers": ["bot"],
111
+ "optional": true,
112
+ "note": "Client secret of that service token — `headersEnv` names it under `CF-Access-Client-Secret`. Shown once at creation; rotate by minting a new token, `deploy secrets bot --only` both names, `deploy restart`, then revoke the old one."
113
+ },
102
114
  {
103
115
  "name": "MEMORY_TOKEN",
104
116
  "workers": ["bot", "resident", "memory"],