@coreplane/switchboard 1.217.0 → 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.
- package/dist/assets/config/config.example.yaml +6 -0
- package/dist/assets/deploy/cloudflare/worker.ts +4 -0
- package/dist/assets/deploy/cloudflare-memory/worker.ts +96 -6
- package/dist/assets/deploy/secrets.manifest.json +12 -0
- package/dist/assets/package-lock.json +3 -3
- package/dist/assets/package.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/agents/registry.ts +18 -0
- package/dist/assets/src/core/runEvents.ts +6 -0
- package/dist/assets/src/core/runFriction.ts +4 -2
- package/dist/assets/src/core/runLedger/sessionLog.ts +22 -0
- package/dist/assets/src/core/runLedger/types.ts +17 -0
- package/dist/assets/src/mcp/registry.ts +33 -5
- package/dist/assets/web/dist/.vite/manifest.json +18 -18
- package/dist/assets/web/dist/assets/{ResidentDetailPage-CWyEd5fq.js → ResidentDetailPage-DvCt96Dk.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-DqFqRTWb.js → ResidentsIndexPage-BZaYja1-.js} +1 -1
- package/dist/assets/web/dist/assets/{RunRoutePage-CghwVr_X.js → RunRoutePage-DI4vzCUu.js} +4 -4
- package/dist/assets/web/dist/assets/{RunsIndexPage-BcYehvW5.js → RunsIndexPage-Ch9uP4p4.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-mKa20Qdd.js → ScheduledPage-DAkAHMCH.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-QMofJG-E.js → StatusDot-CfBonGcV.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-D-zVPexc.js → Tooltip-CBD36E3W.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-D43zKxNV.js → dist-DQ1U73LF.js} +1 -1
- package/dist/assets/web/dist/assets/{main-ClTMtz8n.js → main-CdLnG-yl.js} +2 -2
- package/dist/cli.js +357 -92
- package/package.json +1 -1
|
@@ -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:
|
|
@@ -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",
|
|
@@ -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`,
|
|
2896
|
-
|
|
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
|
|
|
@@ -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"],
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.218.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "switchboard",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.218.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"workspaces": [
|
|
12
12
|
"web",
|
|
@@ -16710,7 +16710,7 @@
|
|
|
16710
16710
|
},
|
|
16711
16711
|
"packages/switchboard": {
|
|
16712
16712
|
"name": "@coreplane/switchboard",
|
|
16713
|
-
"version": "1.
|
|
16713
|
+
"version": "1.218.0",
|
|
16714
16714
|
"license": "Apache-2.0",
|
|
16715
16715
|
"dependencies": {
|
|
16716
16716
|
"@anthropic-ai/sdk": "^0.124.0",
|
package/dist/assets/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchboard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.218.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Mention it in Slack and an agent reviews the PR, ships the fix, or answers the question — on the model you choose, with its tools running where you decide.",
|
|
6
6
|
"license": "Apache-2.0",
|
package/dist/assets/source.json
CHANGED
|
@@ -202,6 +202,14 @@ Whole files: up to 1 GiB where the artifact store is configured (a recording, a
|
|
|
202
202
|
Files the person dropped on the thread that were too large to show you inline are already in ./attachments/ in your workspace when the turn's text names them (a video for ffmpeg, a large PDF, a zip); read them from there — never ask for a re-upload.
|
|
203
203
|
Text stays in your message; do not attach what you can say.`;
|
|
204
204
|
|
|
205
|
+
// Every prompt that can run on the pi harness carries this verbatim
|
|
206
|
+
// (docs/reference/specs/session-log.md item 10; record 0035, "The notepad"): what
|
|
207
|
+
// belongs in the agent's notes for the thread, and why — the one thing sure to
|
|
208
|
+
// survive a compaction and reach the next run there — beside the reach `recall`
|
|
209
|
+
// gives into every earlier turn. Said once so the prompts cannot drift on it.
|
|
210
|
+
const NOTEPAD = `YOUR NOTES AND YOUR REACH BACK. This thread's conversation outlives your context window and this run: every turn — yours, the person's, every tool call and its output, from this run and the runs before it in this thread — is kept in a log you can search with the \`recall\` tool (words → the matching turns with their numbers; a turn number → that turn whole). When something you need is no longer in front of you, recall it instead of redoing the work or guessing.
|
|
211
|
+
Keep notes with the \`notes\` tool: one short document, replaced whole each time, at most 8 KiB — decisions and their reasons, the names of things you found (files, tests, commits, the head your tests were green at), what is not yet proven. They are the one thing sure to survive a compaction and to reach the next run in this thread: they ride your system prompt at its start and come back to you right after a compaction. Write them when you decide something worth keeping, not only at the end.`;
|
|
212
|
+
|
|
205
213
|
const CODING_SYSTEM = `You are Switchboard's coding agent, operating from a Slack request.
|
|
206
214
|
|
|
207
215
|
You work inside a dedicated workspace directory with bash, read_file, and write_file tools. ${SANDBOX_TOOLCHAIN}
|
|
@@ -231,6 +239,8 @@ ${PR_DESCRIPTION_TEMPLATE}
|
|
|
231
239
|
|
|
232
240
|
${SHOW_FILES}
|
|
233
241
|
|
|
242
|
+
${NOTEPAD}
|
|
243
|
+
|
|
234
244
|
Maintain the user-facing status card with the update_status tool: right after you decide your plan, post it as a checklist (○ pending items), then update it whenever an item starts (✱) or finishes (✓). Items are short outcomes ("Clone repo and read the diff", "Run the test suite"), never commands. Mark an item ✓ only after it has actually happened — never pre-mark reporting/posting steps. This is the only progress the user sees while you work.
|
|
235
245
|
|
|
236
246
|
If the request doesn't name a repository and you can't infer it, ask for it instead of guessing.
|
|
@@ -271,6 +281,8 @@ ${PR_DESCRIPTION_TEMPLATE}
|
|
|
271
281
|
|
|
272
282
|
${SHOW_FILES}
|
|
273
283
|
|
|
284
|
+
${NOTEPAD}
|
|
285
|
+
|
|
274
286
|
Maintain the user-facing status card with the update_status tool: right after you decide your plan, post it as a checklist (○ pending items), then update it whenever an item starts (✱) or finishes (✓). Items are short outcomes ("Implement the fix", "Run the test suite"), never commands. Mark an item ✓ only after it has actually happened — never pre-mark reporting/posting steps. This is the only progress the user sees while you work.
|
|
275
287
|
|
|
276
288
|
Report outcomes faithfully: if tests fail or a step was skipped, say so plainly.
|
|
@@ -333,6 +345,8 @@ REVIEW THE PR'S OWN HEAD, NOTHING ELSE: the commit you read must be the PR's hea
|
|
|
333
345
|
|
|
334
346
|
${REVIEW_VERDICT_INSTRUCTION}
|
|
335
347
|
|
|
348
|
+
${NOTEPAD}
|
|
349
|
+
|
|
336
350
|
Maintain the user-facing status card with the update_status tool: post your plan as a checklist (○ pending), update as items start (✱) and finish (✓ — only after they actually happened; never pre-mark reporting steps). Items are short outcomes, never commands.
|
|
337
351
|
|
|
338
352
|
Your final message is posted to Slack. Lead with a one-line verdict, then the findings.`;
|
|
@@ -363,6 +377,8 @@ REVIEW THE PR'S OWN HEAD, NOTHING ELSE: the commit you read must be the PR's hea
|
|
|
363
377
|
|
|
364
378
|
${REVIEW_VERDICT_INSTRUCTION}
|
|
365
379
|
|
|
380
|
+
${NOTEPAD}
|
|
381
|
+
|
|
366
382
|
Maintain the user-facing status card with the update_status tool: post your plan as a checklist (○ pending), update as items start (✱) and finish (✓ — only after they actually happened; never pre-mark reporting steps). Items are short outcomes, never commands.
|
|
367
383
|
|
|
368
384
|
Your final message is posted to Slack. Lead with a one-line verdict, then the findings.`;
|
|
@@ -421,6 +437,8 @@ READ-ONLY: NEVER open a pull request, and never commit or push — no branch, no
|
|
|
421
437
|
|
|
422
438
|
Maintain the user-facing status card with the update_status tool: post your plan as a checklist (○ pending) once you have it, and update items as they start (✱) and finish (✓ — only after they actually happened). Items are short outcomes ("Clone and install", "Time the full suite"), never commands.
|
|
423
439
|
|
|
440
|
+
${NOTEPAD}
|
|
441
|
+
|
|
424
442
|
Report outcomes faithfully: a check you could not run is "could not check", never a guess. Use Slack-friendly formatting (no markdown headers; *bold*, bullets, code blocks — render the claim table as aligned rows inside a code block). Your final message is posted to Slack: lead with the overall verdict in one line, then the claim table, then what a follow-up should do.`;
|
|
425
443
|
|
|
426
444
|
// The conductor (docs/reference/specs/agent-conductor.md): a run that starts
|
|
@@ -387,6 +387,12 @@ export type RunEvent =
|
|
|
387
387
|
* `input`, bounded (newest 20 turns / 256 KiB) and gated by
|
|
388
388
|
* `runHistory.includeContext`. */
|
|
389
389
|
| { type: "context"; text: string; seq?: number; at?: number }
|
|
390
|
+
/** The agent's notes for this thread as the `notes` tool last wrote them
|
|
391
|
+
* (docs/reference/specs/session-log.md item 10; record 0035, "The notepad"):
|
|
392
|
+
* the whole notepad, at most `NOTEPAD_MAX_BYTES`, so the record shows what
|
|
393
|
+
* the next run of the session starts from. Published by the tool on every
|
|
394
|
+
* write; the record's last one is the final text. */
|
|
395
|
+
| { type: "notes"; text: string; spanId?: string; seq?: number; at?: number }
|
|
390
396
|
/** The model's prose BETWEEN tool calls — text content that rode alongside
|
|
391
397
|
* tool_use in one completion. Emitted by the runner, redacted, uncapped. The
|
|
392
398
|
* final text-only completion is NOT one of these (that is the `answer`). */
|
|
@@ -193,14 +193,16 @@ interface PendingCall {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
/** The event types that tell the run's story rather than its steps — the
|
|
196
|
-
* narrative (`input`/`context`/`assistant`/`answer
|
|
197
|
-
|
|
196
|
+
* narrative (`input`/`context`/`assistant`/`answer`, the `notes` the agent
|
|
197
|
+
* kept) and what the run is about. */
|
|
198
|
+
type NarrativeEvent = Extract<RunEvent, { type: "input" | "context" | "assistant" | "answer" | "notes" | "run_meta" }>;
|
|
198
199
|
function isNarrative(ev: RunEvent): ev is NarrativeEvent {
|
|
199
200
|
return (
|
|
200
201
|
ev.type === "input" ||
|
|
201
202
|
ev.type === "context" ||
|
|
202
203
|
ev.type === "assistant" ||
|
|
203
204
|
ev.type === "answer" ||
|
|
205
|
+
ev.type === "notes" ||
|
|
204
206
|
ev.type === "run_meta"
|
|
205
207
|
);
|
|
206
208
|
}
|
|
@@ -62,6 +62,28 @@ export function rowKind(json: string): RowKind {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/** Whose turn a stored row is: the role the row's JSON carries; a compaction
|
|
66
|
+
* row or an unreadable one has none. What `recall` reports beside a hit. */
|
|
67
|
+
export function roleOfStoredRow(json: string): "user" | "assistant" | undefined {
|
|
68
|
+
const stored = parseStored(json);
|
|
69
|
+
if (!stored || "compaction" in stored) return undefined;
|
|
70
|
+
const role = (stored as { role?: unknown }).role;
|
|
71
|
+
return role === "user" || role === "assistant" ? role : undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The notepad's size (record 0035, "The notepad"): one document per session,
|
|
75
|
+
* written whole, at most this many UTF-8 bytes; `notes` refuses over it naming
|
|
76
|
+
* the size, and the object's write route does too. */
|
|
77
|
+
export const NOTEPAD_MAX_BYTES = 8_192;
|
|
78
|
+
|
|
79
|
+
/** The row a follow-up appends when the previous run's record says `broken`
|
|
80
|
+
* (session-log item 9): a user text turn saying the log ends short of what
|
|
81
|
+
* that run saw. Named here so the object can tell a gap row apart for
|
|
82
|
+
* `recall`, which says when a search straddles one. */
|
|
83
|
+
export const GAP_MARKER =
|
|
84
|
+
"[The log of this conversation ends short of what the previous run saw: its connection to the ledger broke, " +
|
|
85
|
+
"so its later turns and its final reply are not here.]";
|
|
86
|
+
|
|
65
87
|
const textOfResultContent = (content: unknown): string => {
|
|
66
88
|
if (typeof content === "string") return content;
|
|
67
89
|
if (!Array.isArray(content)) return "";
|
|
@@ -206,6 +206,23 @@ export interface TranscriptRow {
|
|
|
206
206
|
json: string;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
/** One hit of a session log's full-text search (session-log item 10): the
|
|
210
|
+
* row's turn and part, whose turn it is, what kind of row (`rowKind`), and
|
|
211
|
+
* the text the index held for it. */
|
|
212
|
+
export interface SessionHit {
|
|
213
|
+
idx: number;
|
|
214
|
+
part: number;
|
|
215
|
+
role?: "user" | "assistant";
|
|
216
|
+
kind: string;
|
|
217
|
+
text: string;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** A session's notepad (record 0035, "The notepad"): the text and when it was last written. */
|
|
221
|
+
export interface Notepad {
|
|
222
|
+
text: string;
|
|
223
|
+
updatedAt: number;
|
|
224
|
+
}
|
|
225
|
+
|
|
209
226
|
/** An externalized attachment: base64 data stored once, referenced from a row. */
|
|
210
227
|
export interface TranscriptAttachment {
|
|
211
228
|
ref: string;
|
|
@@ -15,13 +15,19 @@ export type McpAuthKind = "none" | "bearer" | "oauth";
|
|
|
15
15
|
|
|
16
16
|
/** One server as a scope carries it. `tokenEnv` is the static-config way to
|
|
17
17
|
* supply a bearer (an env var on the bot); without it a bearer server's token
|
|
18
|
-
* is the sealed credential the connect page stored.
|
|
18
|
+
* is the sealed credential the connect page stored. `headersEnv` is the
|
|
19
|
+
* static-config way to supply any other request header — header name → the
|
|
20
|
+
* bot env var holding its value — for a server behind a gate that is not the
|
|
21
|
+
* server's own auth (a Cloudflare Access service token: `CF-Access-Client-Id`
|
|
22
|
+
* + `CF-Access-Client-Secret`). It composes with every `auth` kind; the
|
|
23
|
+
* Authorization header itself is `auth`'s and is refused here. */
|
|
19
24
|
export interface McpServerEntry {
|
|
20
25
|
url: string;
|
|
21
26
|
/** Agents whose runs may see it (default general + research). */
|
|
22
27
|
agents?: string[];
|
|
23
28
|
auth: McpAuthKind;
|
|
24
29
|
tokenEnv?: string;
|
|
30
|
+
headersEnv?: Record<string, string>;
|
|
25
31
|
/** Who added it at run time (`slack:U…`, `cli:local`); absent for static config. */
|
|
26
32
|
addedBy?: string;
|
|
27
33
|
addedAt?: number;
|
|
@@ -126,6 +132,24 @@ export const MCP_SERVERS_PER_SCOPE_MAX = 32;
|
|
|
126
132
|
const isStr = (v: unknown, max = 4_096): v is string => typeof v === "string" && v.length > 0 && v.length <= max;
|
|
127
133
|
const isNum = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v);
|
|
128
134
|
|
|
135
|
+
/** At most this many `headersEnv` entries on one server. */
|
|
136
|
+
export const MCP_HEADERS_MAX = 8;
|
|
137
|
+
/** An HTTP header field name (RFC 9110 token), ≤ 64 chars. */
|
|
138
|
+
export const MCP_HEADER_NAME_RE = /^[!#$%&'*+.^_`|~0-9A-Za-z-]{1,64}$/;
|
|
139
|
+
|
|
140
|
+
/** Shape only: a non-empty mapping of header name → env var name, each a
|
|
141
|
+
* string (the config layer holds names to `MCP_HEADER_NAME_RE` and refuses
|
|
142
|
+
* Authorization). */
|
|
143
|
+
function isHeadersEnv(v: unknown): v is Record<string, string> {
|
|
144
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) return false;
|
|
145
|
+
const entries = Object.entries(v as Record<string, unknown>);
|
|
146
|
+
return (
|
|
147
|
+
entries.length > 0 &&
|
|
148
|
+
entries.length <= MCP_HEADERS_MAX &&
|
|
149
|
+
entries.every(([name, envVar]) => isStr(name, 64) && isStr(envVar, 128))
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
129
153
|
function isOutcome(v: unknown): v is NonNullable<McpTicket["outcome"]> {
|
|
130
154
|
if (!v || typeof v !== "object" || Array.isArray(v)) return false;
|
|
131
155
|
const o = v as Record<string, unknown>;
|
|
@@ -148,6 +172,7 @@ export function isMcpServerEntry(v: unknown): v is McpServerEntry {
|
|
|
148
172
|
e.agents.every((a) => isStr(a, 32)))) &&
|
|
149
173
|
(e.auth === "none" || e.auth === "bearer" || e.auth === "oauth") &&
|
|
150
174
|
(e.tokenEnv === undefined || isStr(e.tokenEnv, 128)) &&
|
|
175
|
+
(e.headersEnv === undefined || isHeadersEnv(e.headersEnv)) &&
|
|
151
176
|
(e.addedBy === undefined || isStr(e.addedBy, 260)) &&
|
|
152
177
|
(e.addedAt === undefined || isNum(e.addedAt))
|
|
153
178
|
);
|
|
@@ -194,9 +219,10 @@ export interface McpServerView {
|
|
|
194
219
|
url: string;
|
|
195
220
|
agents: string[];
|
|
196
221
|
auth: McpAuthKind;
|
|
197
|
-
/** `static` =
|
|
198
|
-
*
|
|
199
|
-
*
|
|
222
|
+
/** `static` = the credential is the bot's environment (`tokenEnv`, or
|
|
223
|
+
* `headersEnv` on an `auth: none` server); `connected` = a sealed credential
|
|
224
|
+
* is stored (or no credential is needed); `awaiting_credential` = bearer,
|
|
225
|
+
* nothing stored yet. */
|
|
200
226
|
state: "connected" | "awaiting_credential" | "static";
|
|
201
227
|
source: "config" | "runtime";
|
|
202
228
|
addedBy?: string;
|
|
@@ -212,7 +238,9 @@ export function serverView(
|
|
|
212
238
|
const parsed = parseMcpScopeKey(scopeKey);
|
|
213
239
|
const state: McpServerView["state"] =
|
|
214
240
|
entry.auth === "none"
|
|
215
|
-
?
|
|
241
|
+
? entry.headersEnv
|
|
242
|
+
? "static"
|
|
243
|
+
: "connected"
|
|
216
244
|
: entry.tokenEnv
|
|
217
245
|
? "static"
|
|
218
246
|
: opts.hasCredential
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"src": "../node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-vietnamese-wght-normal.woff2"
|
|
29
29
|
},
|
|
30
30
|
"../node_modules/@pierre/diffs/dist/index.js": {
|
|
31
|
-
"file": "assets/dist-
|
|
31
|
+
"file": "assets/dist-DQ1U73LF.js",
|
|
32
32
|
"name": "dist",
|
|
33
33
|
"src": "../node_modules/@pierre/diffs/dist/index.js",
|
|
34
34
|
"isDynamicEntry": true,
|
|
@@ -2562,16 +2562,16 @@
|
|
|
2562
2562
|
"_seed-DeKSHrt2.js"
|
|
2563
2563
|
]
|
|
2564
2564
|
},
|
|
2565
|
-
"_StatusDot-
|
|
2566
|
-
"file": "assets/StatusDot-
|
|
2565
|
+
"_StatusDot-CfBonGcV.js": {
|
|
2566
|
+
"file": "assets/StatusDot-CfBonGcV.js",
|
|
2567
2567
|
"name": "StatusDot",
|
|
2568
2568
|
"imports": [
|
|
2569
2569
|
"_seed-DeKSHrt2.js",
|
|
2570
|
-
"_Tooltip-
|
|
2570
|
+
"_Tooltip-CBD36E3W.js"
|
|
2571
2571
|
]
|
|
2572
2572
|
},
|
|
2573
|
-
"_Tooltip-
|
|
2574
|
-
"file": "assets/Tooltip-
|
|
2573
|
+
"_Tooltip-CBD36E3W.js": {
|
|
2574
|
+
"file": "assets/Tooltip-CBD36E3W.js",
|
|
2575
2575
|
"name": "Tooltip",
|
|
2576
2576
|
"imports": [
|
|
2577
2577
|
"_AppShell-lYVcj-k7.js",
|
|
@@ -2789,7 +2789,7 @@
|
|
|
2789
2789
|
]
|
|
2790
2790
|
},
|
|
2791
2791
|
"src/main.ts": {
|
|
2792
|
-
"file": "assets/main-
|
|
2792
|
+
"file": "assets/main-CdLnG-yl.js",
|
|
2793
2793
|
"name": "main",
|
|
2794
2794
|
"src": "src/main.ts",
|
|
2795
2795
|
"isEntry": true,
|
|
@@ -2841,19 +2841,19 @@
|
|
|
2841
2841
|
]
|
|
2842
2842
|
},
|
|
2843
2843
|
"src/pages/ResidentDetailPage.vue": {
|
|
2844
|
-
"file": "assets/ResidentDetailPage-
|
|
2844
|
+
"file": "assets/ResidentDetailPage-DvCt96Dk.js",
|
|
2845
2845
|
"name": "ResidentDetailPage",
|
|
2846
2846
|
"src": "src/pages/ResidentDetailPage.vue",
|
|
2847
2847
|
"isDynamicEntry": true,
|
|
2848
2848
|
"imports": [
|
|
2849
2849
|
"_AppShell-lYVcj-k7.js",
|
|
2850
2850
|
"_seed-DeKSHrt2.js",
|
|
2851
|
-
"_StatusDot-
|
|
2851
|
+
"_StatusDot-CfBonGcV.js",
|
|
2852
2852
|
"_residentDiskBudget-BMBKlYRH.js"
|
|
2853
2853
|
]
|
|
2854
2854
|
},
|
|
2855
2855
|
"src/pages/ResidentsIndexPage.vue": {
|
|
2856
|
-
"file": "assets/ResidentsIndexPage-
|
|
2856
|
+
"file": "assets/ResidentsIndexPage-BZaYja1-.js",
|
|
2857
2857
|
"name": "ResidentsIndexPage",
|
|
2858
2858
|
"src": "src/pages/ResidentsIndexPage.vue",
|
|
2859
2859
|
"isDynamicEntry": true,
|
|
@@ -2861,20 +2861,20 @@
|
|
|
2861
2861
|
"_AppShell-lYVcj-k7.js",
|
|
2862
2862
|
"_seed-DeKSHrt2.js",
|
|
2863
2863
|
"_wallClock-61NpVkY6.js",
|
|
2864
|
-
"_StatusDot-
|
|
2864
|
+
"_StatusDot-CfBonGcV.js",
|
|
2865
2865
|
"_residentDiskBudget-BMBKlYRH.js",
|
|
2866
2866
|
"_indexFormat-B-pd4r7Y.js"
|
|
2867
2867
|
]
|
|
2868
2868
|
},
|
|
2869
2869
|
"src/pages/RunRoutePage.vue": {
|
|
2870
|
-
"file": "assets/RunRoutePage-
|
|
2870
|
+
"file": "assets/RunRoutePage-DI4vzCUu.js",
|
|
2871
2871
|
"name": "RunRoutePage",
|
|
2872
2872
|
"src": "src/pages/RunRoutePage.vue",
|
|
2873
2873
|
"isDynamicEntry": true,
|
|
2874
2874
|
"imports": [
|
|
2875
2875
|
"_AppShell-lYVcj-k7.js",
|
|
2876
2876
|
"_seed-DeKSHrt2.js",
|
|
2877
|
-
"_Tooltip-
|
|
2877
|
+
"_Tooltip-CBD36E3W.js",
|
|
2878
2878
|
"src/main.ts",
|
|
2879
2879
|
"_wallClock-61NpVkY6.js",
|
|
2880
2880
|
"_NotFoundPage-BzWS4Ca1.js",
|
|
@@ -2890,31 +2890,31 @@
|
|
|
2890
2890
|
]
|
|
2891
2891
|
},
|
|
2892
2892
|
"src/pages/RunsIndexPage.vue": {
|
|
2893
|
-
"file": "assets/RunsIndexPage-
|
|
2893
|
+
"file": "assets/RunsIndexPage-Ch9uP4p4.js",
|
|
2894
2894
|
"name": "RunsIndexPage",
|
|
2895
2895
|
"src": "src/pages/RunsIndexPage.vue",
|
|
2896
2896
|
"isDynamicEntry": true,
|
|
2897
2897
|
"imports": [
|
|
2898
2898
|
"_AppShell-lYVcj-k7.js",
|
|
2899
2899
|
"_seed-DeKSHrt2.js",
|
|
2900
|
-
"_Tooltip-
|
|
2900
|
+
"_Tooltip-CBD36E3W.js",
|
|
2901
2901
|
"_wallClock-61NpVkY6.js",
|
|
2902
2902
|
"_RunsTabs-DEICQ4FY.js",
|
|
2903
|
-
"_StatusDot-
|
|
2903
|
+
"_StatusDot-CfBonGcV.js",
|
|
2904
2904
|
"_indexFormat-B-pd4r7Y.js",
|
|
2905
2905
|
"_favicon-CWPcvWvp.js",
|
|
2906
2906
|
"_localIso-L06jV29p.js"
|
|
2907
2907
|
]
|
|
2908
2908
|
},
|
|
2909
2909
|
"src/pages/ScheduledPage.vue": {
|
|
2910
|
-
"file": "assets/ScheduledPage-
|
|
2910
|
+
"file": "assets/ScheduledPage-DAkAHMCH.js",
|
|
2911
2911
|
"name": "ScheduledPage",
|
|
2912
2912
|
"src": "src/pages/ScheduledPage.vue",
|
|
2913
2913
|
"isDynamicEntry": true,
|
|
2914
2914
|
"imports": [
|
|
2915
2915
|
"_AppShell-lYVcj-k7.js",
|
|
2916
2916
|
"_seed-DeKSHrt2.js",
|
|
2917
|
-
"_Tooltip-
|
|
2917
|
+
"_Tooltip-CBD36E3W.js",
|
|
2918
2918
|
"_wallClock-61NpVkY6.js",
|
|
2919
2919
|
"_RunsTabs-DEICQ4FY.js",
|
|
2920
2920
|
"_indexFormat-B-pd4r7Y.js",
|