@coreplane/switchboard 0.0.0 → 1.18.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 (131) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +18 -1
  3. package/dist/assets/.dockerignore +27 -0
  4. package/dist/assets/.env.example +33 -0
  5. package/dist/assets/Dockerfile +111 -0
  6. package/dist/assets/config/config.example.yaml +359 -0
  7. package/dist/assets/deploy/bin/build-stamp.d.mts +15 -0
  8. package/dist/assets/deploy/bin/build-stamp.mjs +98 -0
  9. package/dist/assets/deploy/bin/cf-logs +32 -0
  10. package/dist/assets/deploy/cloudflare/package.json +29 -0
  11. package/dist/assets/deploy/cloudflare/preflight.mjs +243 -0
  12. package/dist/assets/deploy/cloudflare/tsconfig.json +18 -0
  13. package/dist/assets/deploy/cloudflare/worker.ts +382 -0
  14. package/dist/assets/deploy/cloudflare/wrangler.template.jsonc +67 -0
  15. package/dist/assets/deploy/cloudflare/write-build.d.mts +7 -0
  16. package/dist/assets/deploy/cloudflare/write-build.mjs +53 -0
  17. package/dist/assets/deploy/cloudflare-docs/package.json +18 -0
  18. package/dist/assets/deploy/cloudflare-docs/wrangler.template.jsonc +30 -0
  19. package/dist/assets/deploy/cloudflare-memory/package.json +25 -0
  20. package/dist/assets/deploy/cloudflare-memory/tsconfig.json +17 -0
  21. package/dist/assets/deploy/cloudflare-memory/worker.ts +2635 -0
  22. package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +50 -0
  23. package/dist/assets/deploy/cloudflare-resident/Dockerfile +91 -0
  24. package/dist/assets/deploy/cloudflare-resident/gc.ts +287 -0
  25. package/dist/assets/deploy/cloudflare-resident/node-async-hooks.d.ts +11 -0
  26. package/dist/assets/deploy/cloudflare-resident/package.json +29 -0
  27. package/dist/assets/deploy/cloudflare-resident/preflight.mjs +224 -0
  28. package/dist/assets/deploy/cloudflare-resident/tsconfig.json +19 -0
  29. package/dist/assets/deploy/cloudflare-resident/worker.ts +6637 -0
  30. package/dist/assets/deploy/cloudflare-resident/wrangler.template.jsonc +120 -0
  31. package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +67 -0
  32. package/dist/assets/deploy/cloudflare-sandbox/docker-wrapper.sh +37 -0
  33. package/dist/assets/deploy/cloudflare-sandbox/package.json +26 -0
  34. package/dist/assets/deploy/cloudflare-sandbox/tsconfig.json +20 -0
  35. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +410 -0
  36. package/dist/assets/deploy/cloudflare-sandbox/wrangler.template.jsonc +67 -0
  37. package/dist/assets/deploy/profile.example.json +13 -0
  38. package/dist/assets/deploy/secrets.manifest.json +108 -0
  39. package/dist/assets/docker-entrypoint.sh +15 -0
  40. package/dist/assets/package-lock.json +18407 -0
  41. package/dist/assets/package.json +104 -0
  42. package/dist/assets/project.json +219 -0
  43. package/dist/assets/source.json +5 -0
  44. package/dist/assets/src/core/authz/actor.ts +100 -0
  45. package/dist/assets/src/core/authz/authorize.ts +169 -0
  46. package/dist/assets/src/core/authz/grants.ts +347 -0
  47. package/dist/assets/src/core/authz/policy.ts +281 -0
  48. package/dist/assets/src/core/authz/resource.ts +147 -0
  49. package/dist/assets/src/core/authz/types.ts +164 -0
  50. package/dist/assets/src/core/drain.ts +54 -0
  51. package/dist/assets/src/core/ingressTokens.ts +64 -0
  52. package/dist/assets/src/core/memory/engine.ts +115 -0
  53. package/dist/assets/src/core/memory/scorer.ts +147 -0
  54. package/dist/assets/src/core/memory/types.ts +120 -0
  55. package/dist/assets/src/core/normalizeSpans.ts +299 -0
  56. package/dist/assets/src/core/prDescriptionTypes.ts +54 -0
  57. package/dist/assets/src/core/redact.ts +113 -0
  58. package/dist/assets/src/core/runEvents.ts +537 -0
  59. package/dist/assets/src/core/runFriction.ts +665 -0
  60. package/dist/assets/src/core/runLedger/decisions.ts +126 -0
  61. package/dist/assets/src/core/runLedger/types.ts +177 -0
  62. package/dist/assets/src/core/runRecord.ts +627 -0
  63. package/dist/assets/src/core/runShape.ts +61 -0
  64. package/dist/assets/src/core/schedules.ts +452 -0
  65. package/dist/assets/src/core/time/formatDuration.ts +61 -0
  66. package/dist/assets/src/core/trace/attrs.ts +203 -0
  67. package/dist/assets/src/core/trace/classify.ts +49 -0
  68. package/dist/assets/src/core/trace/clock.ts +6 -0
  69. package/dist/assets/src/core/trace/context.ts +9 -0
  70. package/dist/assets/src/core/trace/ids.ts +23 -0
  71. package/dist/assets/src/core/trace/partition.ts +235 -0
  72. package/dist/assets/src/core/trace/sinks.ts +68 -0
  73. package/dist/assets/src/core/trace/streamSpans.ts +163 -0
  74. package/dist/assets/src/core/trace/traceparent.ts +29 -0
  75. package/dist/assets/src/core/trace/tracer.ts +247 -0
  76. package/dist/assets/src/core/trace/types.ts +125 -0
  77. package/dist/assets/src/core/trace/workerTrace.ts +97 -0
  78. package/dist/assets/src/deploy/buildStamp.ts +93 -0
  79. package/dist/assets/src/deploy/liveGate.ts +203 -0
  80. package/dist/assets/src/deploy/profile.ts +162 -0
  81. package/dist/assets/src/deploy/restart.ts +393 -0
  82. package/dist/assets/src/effort.ts +17 -0
  83. package/dist/assets/src/execution/bashTimeout.ts +78 -0
  84. package/dist/assets/src/execution/bindingPurge.ts +43 -0
  85. package/dist/assets/src/execution/residentBackupTransfer.ts +50 -0
  86. package/dist/assets/src/execution/residentCleanliness.ts +95 -0
  87. package/dist/assets/src/execution/residentCredentials.ts +81 -0
  88. package/dist/assets/src/execution/residentDepCache.ts +321 -0
  89. package/dist/assets/src/execution/residentDepsStore.ts +326 -0
  90. package/dist/assets/src/execution/residentDetach.ts +48 -0
  91. package/dist/assets/src/execution/residentDisk.ts +107 -0
  92. package/dist/assets/src/execution/residentDiskBudget.ts +448 -0
  93. package/dist/assets/src/execution/residentExecWrap.ts +100 -0
  94. package/dist/assets/src/execution/residentHead.ts +85 -0
  95. package/dist/assets/src/execution/residentReadonly.ts +72 -0
  96. package/dist/assets/src/execution/residentRefresh.ts +429 -0
  97. package/dist/assets/src/execution/residentRestoreExtract.ts +130 -0
  98. package/dist/assets/src/execution/residentState.ts +47 -0
  99. package/dist/assets/src/execution/residentStepReport.ts +98 -0
  100. package/dist/assets/src/execution/residentStepTrace.ts +97 -0
  101. package/dist/assets/src/execution/residentSteps.ts +99 -0
  102. package/dist/assets/src/execution/residentText.ts +83 -0
  103. package/dist/assets/src/execution/residentTrace.ts +119 -0
  104. package/dist/assets/src/execution/sandboxEnv.ts +42 -0
  105. package/dist/assets/src/execution/sandboxErrors.ts +159 -0
  106. package/dist/assets/src/execution/sandboxKeepalive.ts +118 -0
  107. package/dist/assets/src/execution/shellQuote.ts +8 -0
  108. package/dist/assets/src/mcp/registry.ts +242 -0
  109. package/dist/assets/src/providers/types.ts +152 -0
  110. package/dist/assets/web/dist/.vite/manifest.json +176 -0
  111. package/dist/assets/web/dist/assets/AppShell-Bk2gbvet.js +1 -0
  112. package/dist/assets/web/dist/assets/CostsPage-CTZcMYYx.js +1 -0
  113. package/dist/assets/web/dist/assets/NotFoundPage-C-BuaSm8.js +1 -0
  114. package/dist/assets/web/dist/assets/ResidentDetailPage-D3shEnzl.js +1 -0
  115. package/dist/assets/web/dist/assets/ResidentsIndexPage-DWIubQ05.js +1 -0
  116. package/dist/assets/web/dist/assets/RunRoutePage-BMjuE-oX.js +126 -0
  117. package/dist/assets/web/dist/assets/RunRoutePage-XVFj0XDc.css +1 -0
  118. package/dist/assets/web/dist/assets/RunsIndexPage-C3_jYIo0.js +1 -0
  119. package/dist/assets/web/dist/assets/RunsTabs-C4krAL9o.js +1 -0
  120. package/dist/assets/web/dist/assets/ScheduledPage-g1W58mtN.js +1 -0
  121. package/dist/assets/web/dist/assets/StatusDot-DcPRw3zu.js +1 -0
  122. package/dist/assets/web/dist/assets/Tooltip-DJUkMYjo.js +1 -0
  123. package/dist/assets/web/dist/assets/favicon-DL1rdWJt.js +1 -0
  124. package/dist/assets/web/dist/assets/localIso-L06jV29p.js +1 -0
  125. package/dist/assets/web/dist/assets/main-BsBGUyMH.css +2 -0
  126. package/dist/assets/web/dist/assets/main-CyM5f4JC.js +28 -0
  127. package/dist/assets/web/dist/assets/residentDiskBudget-BMBKlYRH.js +1 -0
  128. package/dist/assets/web/dist/assets/seed-BglCRKLA.js +6 -0
  129. package/dist/assets/web/dist/assets/wallClock-Ckv3sKoR.js +1 -0
  130. package/dist/cli.js +34494 -0
  131. package/package.json +43 -10
@@ -0,0 +1,126 @@
1
+ // The ledger's decisions (docs/reference/specs/run-history.md items 28–31), pure. The
2
+ // Durable Object applies them inside one transaction; the in-memory ledger
3
+ // applies them in tests; both agree because this is the only copy.
4
+
5
+ import type { ClaimResult, FenceResult, LivePhase } from "./types.js";
6
+
7
+ /** One live run per thread. The existing row, if any, is what `live_runs` holds
8
+ * for the thread; the same run re-claimed by its owner is idempotent (a retry
9
+ * after a lost response), anything else is refused with what the steer
10
+ * message needs. */
11
+ export function decideClaim(
12
+ existing: { runId: string; agent?: string; startedAt: number; ownerGen: string } | undefined,
13
+ req: { runId: string; gen: string },
14
+ ): ClaimResult {
15
+ if (!existing) return { ok: true };
16
+ if (existing.runId === req.runId && existing.ownerGen === req.gen) return { ok: true };
17
+ return {
18
+ ok: false,
19
+ reason: "thread-live",
20
+ live: {
21
+ runId: existing.runId,
22
+ ...(existing.agent !== undefined ? { agent: existing.agent } : {}),
23
+ startedAt: existing.startedAt,
24
+ },
25
+ };
26
+ }
27
+
28
+ /** What an accepted claim does to the thread's row (item 42). `insert`: no row.
29
+ * `promote`: the owner's claim WITH a prompt on its own `attaching` row — the
30
+ * prompt, tools, card and state land and the phase goes `live`, identity and
31
+ * start untouched. `refresh`: the owner re-reserves (a retry after a lost
32
+ * response) — the lease only. `keep`: any owner re-claim on a row past
33
+ * attaching — idempotent, nothing written (as it always was). Decided only
34
+ * after `decideClaim` accepted. */
35
+ export type ClaimWrite = "insert" | "promote" | "refresh" | "keep";
36
+ export function decideClaimWrite(
37
+ existing: { runId: string; ownerGen: string; phase: LivePhase } | undefined,
38
+ req: { runId: string; gen: string; phase?: "attaching" | "live" },
39
+ ): ClaimWrite {
40
+ if (!existing) return "insert";
41
+ if (existing.phase !== "attaching") return "keep";
42
+ return (req.phase ?? "live") === "attaching" ? "refresh" : "promote";
43
+ }
44
+
45
+ /** The phase a reclaimed row lands in: an `attaching` row stays so — the
46
+ * launcher restarts it from its request rather than resuming a transcript it
47
+ * does not have; every other row becomes `live` under the new owner. */
48
+ export function reclaimPhase(from: LivePhase): LivePhase {
49
+ return from === "attaching" ? "attaching" : "live";
50
+ }
51
+
52
+ /** The fencing token: only the generation that holds the lease may write. */
53
+ export function checkFence(row: { ownerGen: string } | undefined, gen: string): FenceResult {
54
+ if (!row) return { ok: false, reason: "unknown-run" };
55
+ return row.ownerGen === gen ? { ok: true } : { ok: false, reason: "fenced" };
56
+ }
57
+
58
+ /** What a reclaiming generation takes over: every run whose lease has expired
59
+ * (a heartbeat lands strictly before `leaseUntil`, so equal is expired) and
60
+ * every run the previous generation handed off — **never a row it owns
61
+ * itself**. The reclaim runs on a sweep inside the live process too, and a
62
+ * lapsed lease on our own row means a heartbeat that could not land (a state
63
+ * Worker blip), not a dead owner: taking it would launch the run a second
64
+ * time in the same process, and the fence, which compares generations, would
65
+ * never stop the first. */
66
+ export function selectReclaim<T extends { leaseUntil: number; phase: LivePhase; ownerGen: string }>(
67
+ rows: readonly T[],
68
+ now: number,
69
+ gen: string,
70
+ ): T[] {
71
+ return rows.filter((r) => r.ownerGen !== gen && (r.phase === "handoff" || r.leaseUntil <= now));
72
+ }
73
+
74
+ /** The compare-and-swap table for a run's phase. `attaching → live` (the
75
+ * prompt landed) or `→ finishing` (the dispatch failed before it — never
76
+ * handoff: an attaching run has nothing to resume from, so the drain waits
77
+ * for it and a dead owner's row is restarted instead), `live → handoff`
78
+ * (SIGTERM), `live → finishing` (before the reply), `handoff → finishing`
79
+ * (the owner finished inside its own handoff window, before any reclaim —
80
+ * the fence on the generation still keeps a reclaimed row from it), and back
81
+ * to `live` from `handoff` or `finishing` by a reclaim. Nothing else. */
82
+ export function phaseTransition(from: LivePhase, to: LivePhase): boolean {
83
+ switch (from) {
84
+ case "attaching":
85
+ return to === "live" || to === "finishing";
86
+ case "live":
87
+ return to === "handoff" || to === "finishing";
88
+ case "handoff":
89
+ return to === "live" || to === "finishing";
90
+ case "finishing":
91
+ return to === "live";
92
+ default:
93
+ return false;
94
+ }
95
+ }
96
+
97
+ export type Completeness = { kind: "resume" } | { kind: "run-step-fresh" } | { kind: "interrupted"; why: string };
98
+
99
+ /** What a resume does with the rows it finds. A step write lands the step's
100
+ * turns first (the previous results turn and this assistant turn: two turns),
101
+ * then the step record with `turnIndex` = the transcript's turn count. So:
102
+ * the count matches the last record → resume there; two more → the next
103
+ * step's turns landed but its record did not, nothing was dispatched, run
104
+ * that step's tools fresh; anything else is a partial write. With no record
105
+ * yet, the seed plays the role of the last record. */
106
+ export function transcriptCompleteness(input: {
107
+ lastStep: { turnIndex: number } | null;
108
+ seedTurns: number;
109
+ transcriptTurns: number;
110
+ }): Completeness {
111
+ const { lastStep, seedTurns, transcriptTurns } = input;
112
+ if (transcriptTurns === 0) return { kind: "interrupted", why: "no transcript stored" };
113
+ const base = lastStep ? lastStep.turnIndex : seedTurns;
114
+ if (transcriptTurns === base) return { kind: "resume" };
115
+ if (transcriptTurns === base + 2) return { kind: "run-step-fresh" };
116
+ if (transcriptTurns < base) {
117
+ return {
118
+ kind: "interrupted",
119
+ why: `transcript has ${transcriptTurns} turns but the last step recorded ${base}`,
120
+ };
121
+ }
122
+ return {
123
+ kind: "interrupted",
124
+ why: `transcript has ${transcriptTurns} turns, one past the last step's ${base}: a partial step write`,
125
+ };
126
+ }
@@ -0,0 +1,177 @@
1
+ // The run ledger's contract (docs/decisions/0019-durable-run-ledger-resume-after-kill.md;
2
+ // docs/reference/specs/run-history.md items 28–34): what a live run leaves on the state
3
+ // Worker so the next bot generation can resume it. Node-free — imported by the
4
+ // bot and by `deploy/cloudflare-memory/worker.ts` alike, the way runRecord.ts is.
5
+
6
+ import type { ChatMessage, ToolDef } from "../../providers/types.js";
7
+ import type { ChannelVisibility } from "../authz/types.js";
8
+ import type { RunEvent } from "../runEvents.js";
9
+
10
+ /** How long a generation's claim on a run lasts without a heartbeat. */
11
+ export const LEASE_MS = 30_000;
12
+ /** How often the owning generation renews the lease. */
13
+ export const HEARTBEAT_MS = 10_000;
14
+ /** The append flusher's batch window and size. */
15
+ export const APPEND_FLUSH_MS = 500;
16
+ export const APPEND_FLUSH_EVENTS = 32;
17
+ /** Request-body ceiling for a transcript write (the Worker's `/runs/put` fence). */
18
+ export const TRANSCRIPT_REQUEST_BYTES = 2 * 1024 * 1024;
19
+ /** One content part per row; a row must stay under the Durable Object's 2 MB
20
+ * row limit with headroom for the JSON envelope. */
21
+ export const TRANSCRIPT_PART_BYTES = 1_500_000;
22
+ /** Base64 attachment data over this size is stored once and referenced. */
23
+ export const ATTACHMENT_REF_BYTES = 1_000_000;
24
+ /** A generation id: the bot's process start plus a random suffix. */
25
+ export const GEN_PATTERN = /^[A-Za-z0-9_.:-]{1,128}$/;
26
+
27
+ /** `attaching`: reserved at admission (item 42) — the row holds the request
28
+ * and no prompt yet; a reclaim restarts the run from the request. `live`:
29
+ * the prompt and seed landed. `handoff`: the owner drained. `finishing`: the
30
+ * owner replied. */
31
+ export type LivePhase = "attaching" | "live" | "handoff" | "finishing";
32
+ export type StopMode = "soft" | "hard";
33
+
34
+ /** Where the run's status card is, so a resumed run edits the same message. */
35
+ export interface CardHandle {
36
+ channel: string;
37
+ ts: string;
38
+ }
39
+
40
+ /** The registry's `RunMeta` fields (spelled out rather than imported: the
41
+ * registry is a Node module and this file is shared with the state Worker)
42
+ * plus what a resume needs that the dispatcher otherwise keeps in closures. */
43
+ export interface LiveRunMeta {
44
+ agent?: string;
45
+ model?: string;
46
+ channelId: string;
47
+ userId: string;
48
+ threadKey: string;
49
+ channelVisibility?: ChannelVisibility;
50
+ repo?: string;
51
+ sourceUrl?: string;
52
+ userName?: string;
53
+ effort?: string;
54
+ ref?: string;
55
+ headSha?: string;
56
+ pr?: number;
57
+ readonly?: boolean;
58
+ /** Which executor the run attached: what `makeExecutor` chose. */
59
+ selection?: "resident" | "sandbox" | "local" | "none";
60
+ /** The worktree path the system prompt names. */
61
+ workspace?: string;
62
+ /** The request the run was admitted for (item 42), in the durable inbox
63
+ * row's shape — text with its directives, sender, link, arrival time, the
64
+ * attachments when they fit — so a reclaim of an `attaching` row can
65
+ * dispatch it again under the same run id and card. */
66
+ request?: Record<string, unknown>;
67
+ }
68
+
69
+ /** Dispatcher-local run state a resume must restore (the `submit_*` callbacks,
70
+ * the checklist, the pushed branch, the review head, the infra counters). */
71
+ export type RunState = Record<string, unknown>;
72
+
73
+ export interface LiveRunRow {
74
+ runId: string;
75
+ threadKey: string;
76
+ ownerGen: string;
77
+ /** Epoch ms. */
78
+ leaseUntil: number;
79
+ startedAt: number;
80
+ phase: LivePhase;
81
+ stop: StopMode | null;
82
+ meta: LiveRunMeta;
83
+ card: CardHandle | null;
84
+ /** The composed system prompt, verbatim. */
85
+ system: string;
86
+ /** The tool definitions the run was started with, verbatim. */
87
+ tools: ToolDef[];
88
+ state: RunState;
89
+ }
90
+
91
+ /** One tool call the step dispatched; `tool` decides how a resume settles it. */
92
+ export interface InFlightCall {
93
+ callId: string;
94
+ tool: string;
95
+ }
96
+
97
+ /** Written BEFORE a step's tools run (after its transcript turns landed). */
98
+ export interface StepRecord {
99
+ step: number;
100
+ /** The registry `seq` at the time, for ordering against events. */
101
+ seq: number;
102
+ /** Transcript turns present when this record was written. */
103
+ turnIndex: number;
104
+ inFlight: InFlightCall[];
105
+ /** The inbox `seq` the run has consumed up to (0 = none). */
106
+ inboxConsumedSeq: number;
107
+ remainingMs: number;
108
+ turn: number;
109
+ iteration: number;
110
+ }
111
+
112
+ export interface InboxItem {
113
+ seq: number;
114
+ /** The `IncomingMessage` minus attachment bytes, plus channel/ts/threadTs. */
115
+ message: Record<string, unknown>;
116
+ }
117
+
118
+ export interface RunJob {
119
+ kind: string;
120
+ payload: unknown;
121
+ }
122
+
123
+ export interface ClaimRequest {
124
+ runId: string;
125
+ threadKey: string;
126
+ gen: string;
127
+ leaseMs: number;
128
+ startedAt: number;
129
+ meta: LiveRunMeta;
130
+ card?: CardHandle | null;
131
+ system: string;
132
+ tools: ToolDef[];
133
+ state?: RunState;
134
+ /** `attaching` reserves the thread at admission with an empty prompt (item
135
+ * 42); absent (or `live`) is the claim with the prompt — which, on the
136
+ * owner's own attaching row, promotes it in place. */
137
+ phase?: "attaching" | "live";
138
+ }
139
+
140
+ export type ClaimResult =
141
+ { ok: true } | { ok: false; reason: "thread-live"; live: { runId: string; agent?: string; startedAt: number } };
142
+
143
+ export type FenceResult = { ok: true } | { ok: false; reason: "fenced" | "unknown-run" };
144
+
145
+ export interface ReclaimedRun {
146
+ /** The row as the new generation now holds it (`phase: live`, its lease). */
147
+ row: LiveRunRow;
148
+ /** The phase the row was in when taken: `live` (an expired lease — the
149
+ * owner died), `handoff` (the owner drained), or `finishing` (the owner
150
+ * had replied and died before `finish` — the run is closed, never resumed). */
151
+ reclaimedFrom: LivePhase;
152
+ lastStep: StepRecord | null;
153
+ inbox: InboxItem[];
154
+ jobs: RunJob[];
155
+ }
156
+
157
+ /** One transcript row: a content part of turn `idx`. */
158
+ export interface TranscriptRow {
159
+ idx: number;
160
+ part: number;
161
+ json: string;
162
+ }
163
+
164
+ /** An externalized attachment: base64 data stored once, referenced from a row. */
165
+ export interface TranscriptAttachment {
166
+ ref: string;
167
+ mediaType: string;
168
+ data: string;
169
+ }
170
+
171
+ /** The turns a step write carries: the previous step's results and this step's assistant turn. */
172
+ export interface TranscriptTurn {
173
+ idx: number;
174
+ message: ChatMessage;
175
+ }
176
+
177
+ export type AppendableEvent = RunEvent & { seq: number };