@coreplane/switchboard 1.254.0 → 1.255.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 (63) hide show
  1. package/dist/assets/.dockerignore +3 -0
  2. package/dist/assets/Dockerfile +12 -1
  3. package/dist/assets/config/config.example.yaml +6 -1
  4. package/dist/assets/deploy/cloudflare/worker.ts +39 -23
  5. package/dist/assets/deploy/cloudflare-memory/worker.ts +552 -5
  6. package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +5 -0
  7. package/dist/assets/deploy/cloudflare-resident/Dockerfile +13 -1
  8. package/dist/assets/deploy/cloudflare-resident/levels.ts +84 -0
  9. package/dist/assets/deploy/cloudflare-resident/prepare-commit-msg +17 -0
  10. package/dist/assets/deploy/cloudflare-resident/worker.ts +202 -31
  11. package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +11 -1
  12. package/dist/assets/deploy/cloudflare-sandbox/prepare-commit-msg +17 -0
  13. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +21 -4
  14. package/dist/assets/deploy/hooks/prepare-commit-msg +17 -0
  15. package/dist/assets/deploy/secrets.manifest.json +12 -0
  16. package/dist/assets/package-lock.json +3 -3
  17. package/dist/assets/package.json +1 -1
  18. package/dist/assets/source.json +3 -3
  19. package/dist/assets/src/agents/registry.ts +21 -0
  20. package/dist/assets/src/core/budgets.ts +35 -2
  21. package/dist/assets/src/core/coordinator/contract.ts +6 -0
  22. package/dist/assets/src/core/coordinator/driver.ts +49 -6
  23. package/dist/assets/src/core/costs.ts +39 -16
  24. package/dist/assets/src/core/pipelineStanding.ts +5 -0
  25. package/dist/assets/src/core/plane/decide.ts +389 -22
  26. package/dist/assets/src/core/refusal.ts +3 -0
  27. package/dist/assets/src/core/reviewVerdict.ts +15 -2
  28. package/dist/assets/src/core/runEvents.ts +32 -13
  29. package/dist/assets/src/core/runLedger/sessionLog.ts +128 -0
  30. package/dist/assets/src/core/runLedger/types.ts +3 -0
  31. package/dist/assets/src/core/runRecord.ts +24 -7
  32. package/dist/assets/src/core/ship/coordinator.ts +327 -61
  33. package/dist/assets/src/core/trace/workerTrace.ts +3 -0
  34. package/dist/assets/src/execution/sandboxErrors.ts +77 -6
  35. package/dist/assets/web/dist/.vite/manifest.json +59 -58
  36. package/dist/assets/web/dist/assets/CostsPage-BaeWnm-o.js +1 -0
  37. package/dist/assets/web/dist/assets/{DeliveryPage-ngPsO2to.js → DeliveryPage-BBAyLwPq.js} +1 -1
  38. package/dist/assets/web/dist/assets/{HomePage-DvxTHzPx.js → HomePage-Be7jLLnU.js} +1 -1
  39. package/dist/assets/web/dist/assets/PendingTurnRow-DGINv9XT.js +1 -0
  40. package/dist/assets/web/dist/assets/{PlanePage-DpWfiX4C.js → PlanePage-JEj-lqgz.js} +1 -1
  41. package/dist/assets/web/dist/assets/{ResidentDetailPage-DG86v39Y.js → ResidentDetailPage-D_RD6wLo.js} +1 -1
  42. package/dist/assets/web/dist/assets/{ResidentsIndexPage-x6p689VH.js → ResidentsIndexPage-BLkSuCxo.js} +1 -1
  43. package/dist/assets/web/dist/assets/RunFoldRow-CfuZqf_O.js +1 -0
  44. package/dist/assets/web/dist/assets/{RunRoutePage-ysJBY8xQ.js → RunRoutePage-CmWYGR36.js} +4 -4
  45. package/dist/assets/web/dist/assets/{RunsIndexPage-CuzFchcn.js → RunsIndexPage-DW-HHuZa.js} +1 -1
  46. package/dist/assets/web/dist/assets/{ScheduledPage-BuLmfcbG.js → ScheduledPage-3aYsDf-q.js} +1 -1
  47. package/dist/assets/web/dist/assets/{SettingsPage-BujWkdU_.js → SettingsPage-DG-p5Xy1.js} +1 -1
  48. package/dist/assets/web/dist/assets/{StatusDot-C8Bc0pTX.js → StatusDot-CEnGlyAL.js} +1 -1
  49. package/dist/assets/web/dist/assets/{Tooltip-BWwJx27K.js → Tooltip-CiunVowT.js} +1 -1
  50. package/dist/assets/web/dist/assets/UnitRoutePage-iOYnmTcQ.js +1 -0
  51. package/dist/assets/web/dist/assets/budgets-c1eumrqD.js +1 -0
  52. package/dist/assets/web/dist/assets/{dist-CpnyQGOb.js → dist-luhv3YSo.js} +1 -1
  53. package/dist/assets/web/dist/assets/indexRow-DborJPFp.js +1 -0
  54. package/dist/assets/web/dist/assets/{main-Comxmwi4.js → main-mAKx_zo9.js} +2 -2
  55. package/dist/assets/web/dist/assets/{sseReplay-IzTdD4-3.js → sseReplay-DE6wv1Ua.js} +6 -6
  56. package/dist/cli.js +4154 -3061
  57. package/package.json +1 -1
  58. package/dist/assets/web/dist/assets/CostsPage-BuKjw3nv.js +0 -1
  59. package/dist/assets/web/dist/assets/PendingTurnRow-ZYIRCCZ2.js +0 -1
  60. package/dist/assets/web/dist/assets/RunFoldRow-DG29LOTs.js +0 -1
  61. package/dist/assets/web/dist/assets/UnitRoutePage-B9kjA1AT.js +0 -1
  62. package/dist/assets/web/dist/assets/budgets-CbIyPAER.js +0 -1
  63. package/dist/assets/web/dist/assets/indexRow-DABQtONT.js +0 -1
@@ -77,15 +77,21 @@ import {
77
77
  reclaimPhase,
78
78
  selectReclaim,
79
79
  } from "../../src/core/runLedger/decisions.ts";
80
- import { intakeReceiptRetentionMs } from "../../src/core/budgets.ts";
80
+ import { intakeReceiptRetentionMs, minutesToMs, PLANE } from "../../src/core/budgets.ts";
81
81
  import {
82
82
  decide,
83
+ effectCapRefusal,
84
+ planeAskAnswerOf,
83
85
  planeAskWordOf,
86
+ RESIDENT_DRAIN_WINDOW,
87
+ type PlaneAskAnswer,
88
+ type PlaneLevelRow,
84
89
  type PlaneAckOutcome,
85
90
  type PlaneEffect,
86
91
  type PlaneEvent,
87
92
  type PlaneOutcomePost,
88
93
  type PlaneQueueRow,
94
+ type PlaneReservation,
89
95
  type PlaneStage,
90
96
  type PlaneState,
91
97
  type PlaneWrite,
@@ -220,6 +226,11 @@ export interface Env {
220
226
  /** The dataset's name, rendered beside the binding, so `/healthz` can answer
221
227
  * `runMetrics:<dataset>` and the bot's boot probe can compare names. */
222
228
  RUN_METRICS_DATASET?: string;
229
+ /** The bot Worker, for the plane's effect push (record 0064, "Where it
230
+ * lives"): committed effects are POSTed to its bearer-gated
231
+ * `/plane/effects`, which forwards to the container. Optional like
232
+ * SHIP_COORDINATOR — without it effects ride the heartbeat answers alone. */
233
+ BOT?: Fetcher;
223
234
  MEMORY_TOKEN?: string;
224
235
  }
225
236
 
@@ -1777,6 +1788,22 @@ export class RunHistoryDO extends DurableObject<Env> {
1777
1788
  * dispatch it describes claimed the thread must not read its own claim as
1778
1789
  * "thread live" (orchestration-plane item 8). */
1779
1790
  private planeState(excludeRunId?: string): PlaneState {
1791
+ const now = systemClock();
1792
+ // A reservation not yet promoted into a live row expires after its window:
1793
+ // a dispatch that died between the admission answer and its claim must not
1794
+ // hold the thread forever. Promotion deletes the row (the live row holds
1795
+ // the thread from there), so age alone is the test.
1796
+ const reservations = this.sql
1797
+ .exec<{ kind: string; key: string; run_id: string; at: number }>(
1798
+ `SELECT * FROM plane_reservations WHERE kind = 'thread' AND at > ?`,
1799
+ now - minutesToMs(PLANE.reservationMinutes),
1800
+ )
1801
+ .toArray()
1802
+ .map((r): PlaneReservation => ({ kind: "thread", key: r.key, runId: r.run_id, at: r.at }));
1803
+ const openWindows = this.sql
1804
+ .exec<{ kind: string }>(`SELECT kind FROM plane_windows WHERE phase = 'open'`)
1805
+ .toArray()
1806
+ .map((r) => r.kind);
1780
1807
  const queue = this.sql
1781
1808
  .exec<{
1782
1809
  run_id: string;
@@ -1805,7 +1832,19 @@ export class RunHistoryDO extends DurableObject<Env> {
1805
1832
  .exec<{ thread_key: string }>(`SELECT thread_key FROM live_runs WHERE run_id IS NOT ?`, excludeRunId ?? null)
1806
1833
  .toArray()
1807
1834
  .map((r) => r.thread_key);
1808
- return { queue, liveThreads };
1835
+ const levels = this.sql
1836
+ .exec<{ resident: string; name: string; side: string; reported_at: number; generation: string }>(
1837
+ `SELECT * FROM plane_levels`,
1838
+ )
1839
+ .toArray()
1840
+ .map((r): PlaneLevelRow => ({
1841
+ resident: r.resident,
1842
+ name: r.name as PlaneLevelRow["name"],
1843
+ side: r.side as PlaneLevelRow["side"],
1844
+ reportedAt: r.reported_at,
1845
+ generation: r.generation,
1846
+ }));
1847
+ return { queue, liveThreads, reservations, openWindows, levels };
1809
1848
  }
1810
1849
 
1811
1850
  /** The decider's writes, applied inside the same `transactionSync` that read
@@ -1829,7 +1868,67 @@ export class RunHistoryDO extends DurableObject<Env> {
1829
1868
  );
1830
1869
  } else if (w.table === "plane_queue" && w.op === "state") {
1831
1870
  this.sql.exec(`UPDATE plane_queue SET state = ? WHERE run_id = ?`, w.state, w.runId);
1871
+ } else if (w.table === "plane_reservations" && w.op === "put") {
1872
+ this.sql.exec(
1873
+ `INSERT OR REPLACE INTO plane_reservations (kind, key, run_id, at) VALUES (?, ?, ?, ?)`,
1874
+ w.row.kind,
1875
+ w.row.key,
1876
+ w.row.runId,
1877
+ w.row.at,
1878
+ );
1879
+ } else if (w.table === "plane_reservations" && w.op === "del") {
1880
+ this.sql.exec(`DELETE FROM plane_reservations WHERE kind = 'thread' AND key = ?`, w.key);
1881
+ } else if (w.table === "plane_windows" && w.op === "put") {
1882
+ this.sql.exec(
1883
+ `INSERT OR REPLACE INTO plane_windows (kind, key, phase, opened_at, reason_json) VALUES (?, ?, 'open', ?, '{}')`,
1884
+ w.window,
1885
+ w.window,
1886
+ w.at,
1887
+ );
1888
+ } else if (w.table === "plane_windows" && w.op === "del") {
1889
+ this.sql.exec(`DELETE FROM plane_windows WHERE kind = ?`, w.window);
1890
+ } else if (w.table === "plane_levels") {
1891
+ this.sql.exec(
1892
+ `INSERT OR REPLACE INTO plane_levels (resident, name, side, reported_at, generation) VALUES (?, ?, ?, ?, ?)`,
1893
+ w.row.resident,
1894
+ w.row.name,
1895
+ w.row.side,
1896
+ w.row.reportedAt,
1897
+ w.row.generation,
1898
+ );
1832
1899
  } else {
1900
+ // The effect bounds (record 0064): an offer past the per-run or total
1901
+ // cap is refused by the cap's name — the throw aborts the transaction,
1902
+ // so the queue row stays waiting and the next event re-decides.
1903
+ const total = Number(
1904
+ this.sql.exec<{ n: number }>(`SELECT COUNT(*) AS n FROM plane_effects WHERE acked_at IS NULL`).toArray()[0]
1905
+ ?.n ?? 0,
1906
+ );
1907
+ const forRun =
1908
+ w.effect.kind === "admit"
1909
+ ? Number(
1910
+ this.sql
1911
+ .exec<{
1912
+ n: number;
1913
+ }>(
1914
+ // Exact id matching (`admit:<runId>`): the seal runs with bot-minted
1915
+ // run ids, and a LIKE would read `%`/`_` in one as wildcards.
1916
+ `SELECT COUNT(*) AS n FROM plane_effects WHERE acked_at IS NULL AND id = 'admit:' || ?`,
1917
+ w.effect.runId,
1918
+ )
1919
+ .toArray()[0]?.n ?? 0,
1920
+ )
1921
+ : 0;
1922
+ const refusal = effectCapRefusal({ total, forRun }, w.effect);
1923
+ if (refusal !== undefined) throw new Error(refusal);
1924
+ // A re-offer lands after an ack for any kind — a probe re-probes after
1925
+ // its ack (one open probe per resident, record 0064), and an observation
1926
+ // re-enters an admitted run whose acked `admit:<runId>` row would
1927
+ // otherwise swallow the walk's re-offer, stranding the run `admitted`
1928
+ // with no effect delivered. The decider only re-emits an effect when
1929
+ // its subject is waiting again, and the bot's own getById dedup guards
1930
+ // a genuine duplicate admit, so the acked row is history, not a fence.
1931
+ this.sql.exec(`DELETE FROM plane_effects WHERE id = ? AND acked_at IS NOT NULL`, w.effect.id);
1833
1932
  // An offer keeps its first `offered_at`: a re-decided admit after a
1834
1933
  // roll is the same effect, not a younger one.
1835
1934
  this.sql.exec(
@@ -1838,6 +1937,22 @@ export class RunHistoryDO extends DurableObject<Env> {
1838
1937
  JSON.stringify(w.effect),
1839
1938
  w.at,
1840
1939
  );
1940
+ // The admitted run's attaching row, in the same transaction as the
1941
+ // effect (record 0064, "The queue"): owner `plane`, lease already
1942
+ // expired, request in the meta — exactly the row a reserved run whose
1943
+ // owner died leaves (run-history item 42), so the bot's reclaim sweep
1944
+ // restarts it from the stored request under this id.
1945
+ if (w.effect.kind === "admit") {
1946
+ this.sql.exec(
1947
+ `INSERT OR IGNORE INTO live_runs (run_id, thread_key, owner_gen, lease_until, started_at, phase, stop, meta_json, card_json, system_text, tools_json, state_json)
1948
+ VALUES (?, ?, 'plane', ?, ?, 'attaching', NULL, ?, NULL, '', '[]', '{}')`,
1949
+ w.effect.runId,
1950
+ w.effect.threadKey,
1951
+ w.at,
1952
+ w.at,
1953
+ JSON.stringify({ request: w.effect.request }),
1954
+ );
1955
+ }
1841
1956
  }
1842
1957
  }
1843
1958
  }
@@ -1851,9 +1966,200 @@ export class RunHistoryDO extends DurableObject<Env> {
1851
1966
  this.applyPlaneWrites(decision.writes);
1852
1967
  effects = decision.effects;
1853
1968
  });
1969
+ this.pushPlaneEffects(effects);
1854
1970
  return { effects };
1855
1971
  }
1856
1972
 
1973
+ /** The admission ask (`POST /plane/admit`, record 0064 "The queue"): one
1974
+ * transaction decides and writes — `admitted` reserves the thread,
1975
+ * `queued` stores the request under the minted id. */
1976
+ planeAdmit(
1977
+ post: {
1978
+ runId: string;
1979
+ requester: string;
1980
+ threadKey: string;
1981
+ request: Record<string, unknown>;
1982
+ stage?: PlaneStage;
1983
+ resident?: string;
1984
+ restartOf?: boolean;
1985
+ reaskMs?: number;
1986
+ },
1987
+ now: number,
1988
+ ): PlaneAskAnswer {
1989
+ let answer!: PlaneAskAnswer;
1990
+ this.ctx.storage.transactionSync(() => {
1991
+ if (post.reaskMs !== undefined)
1992
+ this.sql.exec(`INSERT OR REPLACE INTO meta (key, value) VALUES ('plane_reask_ms', ?)`, String(post.reaskMs));
1993
+ const decision = decide(this.planeState(), {
1994
+ kind: "ask",
1995
+ at: now,
1996
+ runId: post.runId,
1997
+ requester: post.requester,
1998
+ threadKey: post.threadKey,
1999
+ stage: post.stage ?? "admission",
2000
+ request: post.request,
2001
+ ...(post.resident !== undefined ? { resident: post.resident } : {}),
2002
+ ...(post.restartOf !== undefined ? { restartOf: post.restartOf } : {}),
2003
+ });
2004
+ this.applyPlaneWrites(decision.writes);
2005
+ answer = planeAskAnswerOf(decision, post.runId);
2006
+ });
2007
+ if (answer.kind === "queued") void this.ensurePlaneReaskAlarm(now);
2008
+ console.log(
2009
+ `[plane/admit] ${post.threadKey} → ${answer.kind}${answer.kind === "queued" ? ` position ${answer.position}` : ""} (run ${post.runId})`,
2010
+ );
2011
+ return answer;
2012
+ }
2013
+
2014
+ /** A resident's level report (`POST /plane/level`, record 0064): `seat` and `memory`
2015
+ * land as level events — a `below` side walks the queue; the registry's
2016
+ * drain posts land as the resident-drain window's open (`above`) and lift
2017
+ * (`below` — a `cleared` or the alarm's `expired`). */
2018
+ planeLevel(
2019
+ post: { resident: string; name: "seat" | "memory" | "drain"; side: "below" | "above"; generation: string },
2020
+ now: number,
2021
+ ): { admitted: number } {
2022
+ const r =
2023
+ post.name === "drain"
2024
+ ? this.planeApply({
2025
+ kind: "window",
2026
+ at: now,
2027
+ window: RESIDENT_DRAIN_WINDOW,
2028
+ phase: post.side === "above" ? "opened" : "lifted",
2029
+ })
2030
+ : this.planeApply({
2031
+ kind: "level",
2032
+ at: now,
2033
+ resident: post.resident,
2034
+ name: post.name,
2035
+ side: post.side,
2036
+ generation: post.generation,
2037
+ });
2038
+ const admitted = r.effects.filter((e) => e.kind === "admit").length;
2039
+ console.log(`[plane/level] ${post.resident} ${post.name} ${post.side} — ${admitted} admission(s)`);
2040
+ return { admitted };
2041
+ }
2042
+
2043
+ /** A refusal-by-name the bot met at attach or exec (`POST /plane/observe`,
2044
+ * record 0064): an admitted run re-enters the queue at its old position. */
2045
+ planeObserve(post: { runId: string; resident: string; refusal: string }, now: number): { reentered: boolean } {
2046
+ let reentered = false;
2047
+ this.ctx.storage.transactionSync(() => {
2048
+ const decision = decide(this.planeState(), {
2049
+ kind: "observation",
2050
+ at: now,
2051
+ runId: post.runId,
2052
+ resident: post.resident,
2053
+ refusal: post.refusal,
2054
+ });
2055
+ this.applyPlaneWrites(decision.writes);
2056
+ reentered = decision.writes.length > 0;
2057
+ });
2058
+ if (reentered) void this.ensurePlaneReaskAlarm(now);
2059
+ console.log(
2060
+ `[plane/observe] run ${post.runId} on ${post.resident}: ${post.refusal.slice(0, 60)} — ${reentered ? "re-entered" : "no-op"}`,
2061
+ );
2062
+ return { reentered };
2063
+ }
2064
+
2065
+ /** The re-ask cadence (record 0064): while a queued row waits on a resident,
2066
+ * the object's alarm fires within the cadence — the sweep's own 6 h alarm
2067
+ * is pulled forward, never pushed back. */
2068
+ private planeReaskMs(): number {
2069
+ const row = this.sql.exec<{ value: string }>(`SELECT value FROM meta WHERE key = 'plane_reask_ms'`).toArray()[0];
2070
+ const stored = row ? Number(row.value) : NaN;
2071
+ return Number.isFinite(stored) && stored > 0 ? stored : minutesToMs(PLANE.reaskMinutes);
2072
+ }
2073
+
2074
+ private planeWaitsOnResident(): boolean {
2075
+ return this.planeState().queue.some(
2076
+ (r) => r.state === "waiting" && r.conditions.some((c) => c.kind === "seat" || c.kind === "memory"),
2077
+ );
2078
+ }
2079
+
2080
+ private async ensurePlaneReaskAlarm(now: number): Promise<void> {
2081
+ if (!this.planeWaitsOnResident()) return;
2082
+ const due = now + this.planeReaskMs();
2083
+ const set = await this.ctx.storage.getAlarm();
2084
+ if (set === null || set > due) await this.ctx.storage.setAlarm(due);
2085
+ }
2086
+
2087
+ /** A window's open or lift over the RPC seam (`/plane/deploy`; a later
2088
+ * unit's `plane window lift`): kind `deploy` is the pending deploy. */
2089
+ planeWindow(window: string, phase: "opened" | "lifted", now: number): { admitted: number } {
2090
+ const r = this.planeApply({ kind: "window", at: now, window, phase });
2091
+ return { admitted: r.effects.length };
2092
+ }
2093
+
2094
+ /** `runs stop` on a queued id (record 0064): the waiting row is withdrawn;
2095
+ * an id the queue does not hold waiting answers false. */
2096
+ planeWithdraw(runId: string, now: number): { withdrawn: boolean } {
2097
+ let withdrawn = false;
2098
+ this.ctx.storage.transactionSync(() => {
2099
+ const decision = decide(this.planeState(), { kind: "withdraw", at: now, runId });
2100
+ this.applyPlaneWrites(decision.writes);
2101
+ withdrawn = decision.writes.length > 0;
2102
+ });
2103
+ return { withdrawn };
2104
+ }
2105
+
2106
+ /** One queued row, for the queued id's page. */
2107
+ planeQueueRowOf(runId: string): PlaneQueueRow | null {
2108
+ return this.planeState().queue.find((r) => r.runId === runId) ?? null;
2109
+ }
2110
+
2111
+ /** The seal (record 0064, "The queue"): the run's own open effects are
2112
+ * dropped — an admit for a run that just ended is stale — then the sealed
2113
+ * event frees the thread and walks the queue, all in one transaction. */
2114
+ private planeSealed(runId: string, threadKey: string, now: number): void {
2115
+ let effects: PlaneEffect[] = [];
2116
+ try {
2117
+ this.ctx.storage.transactionSync(() => {
2118
+ // Exact id matching (`admit:<runId>`), never LIKE: a bot-minted run id
2119
+ // can carry `%` or `_`, which a pattern would read as wildcards.
2120
+ this.sql.exec(`DELETE FROM plane_effects WHERE acked_at IS NULL AND id = 'admit:' || ?`, runId);
2121
+ const decision = decide(this.planeState(), { kind: "sealed", at: now, threadKey });
2122
+ this.applyPlaneWrites(decision.writes);
2123
+ effects = decision.effects;
2124
+ });
2125
+ } catch (err) {
2126
+ // A cap refusal here must not undo the finish that already committed.
2127
+ console.warn(`[plane/sealed] ${threadKey}: ${err instanceof Error ? err.message : String(err)}`);
2128
+ return;
2129
+ }
2130
+ this.pushPlaneEffects(effects);
2131
+ }
2132
+
2133
+ /** The transport's push (record 0064, "Where it lives"): committed effects
2134
+ * are pushed to the bot Worker over the service binding, which forwards to
2135
+ * the container. Fire and forget — a push that fails is not retried by a
2136
+ * timer; the effect rides the next heartbeat or reclaim-sweep answer. */
2137
+ private pushPlaneEffects(effects: PlaneEffect[]): void {
2138
+ if (effects.length === 0) return;
2139
+ const bot = this.env.BOT;
2140
+ if (!bot) return;
2141
+ void bot
2142
+ .fetch("https://bot/plane/effects", {
2143
+ method: "POST",
2144
+ headers: {
2145
+ "content-type": "application/json",
2146
+ authorization: `Bearer ${this.env.MEMORY_TOKEN ?? ""}`,
2147
+ },
2148
+ body: JSON.stringify({ effects }),
2149
+ })
2150
+ .then((r) => {
2151
+ if (!r.ok)
2152
+ console.warn(
2153
+ `[plane/push] ${effects.length} effect(s) → bot answered ${r.status} — they ride the next heartbeat`,
2154
+ );
2155
+ })
2156
+ .catch((err: unknown) => {
2157
+ console.warn(
2158
+ `[plane/push] ${effects.length} effect(s) not delivered: ${err instanceof Error ? err.message : String(err)} — they ride the next heartbeat`,
2159
+ );
2160
+ });
2161
+ }
2162
+
1857
2163
  /** The unacknowledged effects, oldest first, at most `PLANE_EFFECTS_PER_ANSWER`
1858
2164
  * (orchestration-plane item 7) — what every heartbeat and reclaim answer carries. Public: the
1859
2165
  * reclaim route composes it beside the runs it took. */
@@ -1970,6 +2276,29 @@ export class RunHistoryDO extends DurableObject<Env> {
1970
2276
  return row ? (JSON.parse(row.json) as CoordinatorInstance) : null;
1971
2277
  }
1972
2278
 
2279
+ /** The hard stop's mark on the instance row (record 0060; issue 1924).
2280
+ * Idempotent: a marked row keeps its first mark. */
2281
+ async markInstanceStopped(id: string, at: number): Promise<{ ok: true } | { ok: false; reason: "unknown_instance" }> {
2282
+ let out: { ok: true } | { ok: false; reason: "unknown_instance" } = { ok: true };
2283
+ this.ctx.storage.transactionSync(() => {
2284
+ const row = this.sql
2285
+ .exec<{ json: string }>(`SELECT json FROM coordinator_instances WHERE instance_id = ?`, id)
2286
+ .toArray()[0];
2287
+ if (!row) {
2288
+ out = { ok: false, reason: "unknown_instance" };
2289
+ return;
2290
+ }
2291
+ const instance = JSON.parse(row.json) as CoordinatorInstance;
2292
+ if (instance.stop !== undefined) return;
2293
+ this.sql.exec(
2294
+ `UPDATE coordinator_instances SET json = ? WHERE instance_id = ?`,
2295
+ JSON.stringify({ ...instance, stop: { at } }),
2296
+ id,
2297
+ );
2298
+ });
2299
+ return out;
2300
+ }
2301
+
1973
2302
  // ---- the units of the plan an instance runs (run-history item 50) -----------
1974
2303
 
1975
2304
  /** Each row replaced whole under its (instance, unit); a replace keeps the row's place. */
@@ -2152,6 +2481,10 @@ export class RunHistoryDO extends DurableObject<Env> {
2152
2481
  req,
2153
2482
  );
2154
2483
  if (!out.ok) return;
2484
+ // The claim promotes the thread's reservation (record 0064, "The queue"):
2485
+ // the live row holds the thread from here, so the reservation row retires
2486
+ // in the same transaction that writes the claim.
2487
+ this.sql.exec(`DELETE FROM plane_reservations WHERE kind = 'thread' AND key = ?`, req.threadKey);
2155
2488
  switch (decideClaimWrite(existing, req)) {
2156
2489
  case "keep":
2157
2490
  return;
@@ -2359,6 +2692,9 @@ export class RunHistoryDO extends DurableObject<Env> {
2359
2692
  out = { ok: true, stored: put.stored };
2360
2693
  });
2361
2694
  if (!out.ok) return out;
2695
+ // The seal flips `thread_free` (record 0064, "The queue"): the plane frees
2696
+ // the thread, drops the sealed run's own open effects and walks the queue.
2697
+ this.planeSealed(runId, record.threadKey, systemClock());
2362
2698
  // The point after the commit, never inside it (`sendRunFinished`'s placement):
2363
2699
  // the finish usually replaces the start tombstone, so this is where most
2364
2700
  // runs are counted (run-metrics.md item 2).
@@ -2376,11 +2712,16 @@ export class RunHistoryDO extends DurableObject<Env> {
2376
2712
  * started. Fenced. */
2377
2713
  async abandon(runId: string, gen: string): Promise<FenceResult> {
2378
2714
  let out: FenceResult = { ok: true };
2715
+ let threadKey: string | undefined;
2379
2716
  this.ctx.storage.transactionSync(() => {
2380
- out = checkFence(this.liveRow(runId), gen);
2717
+ const row = this.liveRow(runId);
2718
+ out = checkFence(row, gen);
2381
2719
  if (!out.ok) return;
2720
+ threadKey = row?.threadKey;
2382
2721
  this.deleteLiveRows([runId]);
2383
2722
  });
2723
+ // An abandoned reservation seals like a finish does: the thread frees and the queue walks.
2724
+ if (out.ok && threadKey !== undefined) this.planeSealed(runId, threadKey, systemClock());
2384
2725
  return out;
2385
2726
  }
2386
2727
 
@@ -2848,7 +3189,18 @@ export class RunHistoryDO extends DurableObject<Env> {
2848
3189
  console.log(
2849
3190
  `[runs/alarm] swept ${deleted} rows outside policy, pruned ${receipts} intake receipt(s), dropped ${dropped} session log(s)`,
2850
3191
  );
3192
+ // The plane's re-ask (record 0064): while a queued row waits on a resident
3193
+ // that has said nothing within the cadence, one probe effect per
3194
+ // resident — and the next alarm is pulled forward to the cadence, so a
3195
+ // silent resident is probed, never waited on forever. The sweep rides
3196
+ // the same alarm; a probing cadence re-runs it, which is only indexed
3197
+ // deletes and only while something waits.
3198
+ if (this.planeWaitsOnResident()) {
3199
+ const probes = this.planeApply({ kind: "reask", at: now, cadenceMs: this.planeReaskMs() }).effects;
3200
+ if (probes.length > 0) console.log(`[plane/reask] ${probes.map((e) => e.id).join(", ")}`);
3201
+ }
2851
3202
  await this.ctx.storage.setAlarm(now + RUN_SWEEP_INTERVAL_MS);
3203
+ await this.ensurePlaneReaskAlarm(now);
2852
3204
  root.end("ok", { swept: deleted });
2853
3205
  } catch (err) {
2854
3206
  root.fail(err);
@@ -3782,6 +4134,17 @@ export class SessionLogDO extends DurableObject<Env> {
3782
4134
  );
3783
4135
  CREATE TABLE IF NOT EXISTS notepad (k INTEGER PRIMARY KEY CHECK (k = 1), text TEXT NOT NULL, updated_at INTEGER NOT NULL);
3784
4136
  `);
4137
+ // The keyed append's identity (session-log item 13): a row group's id, on
4138
+ // its first part; a partial unique index holds the idempotency line. Added
4139
+ // by ALTER so a log written before the column keeps its rows.
4140
+ const columns = new Set(
4141
+ this.sql
4142
+ .exec<{ name: string }>(`SELECT name FROM pragma_table_info('turns')`)
4143
+ .toArray()
4144
+ .map((r) => r.name),
4145
+ );
4146
+ if (!columns.has("row_id")) this.sql.exec(`ALTER TABLE turns ADD COLUMN row_id TEXT`);
4147
+ this.sql.exec(`CREATE UNIQUE INDEX IF NOT EXISTS turns_row_id ON turns(row_id) WHERE row_id IS NOT NULL`);
3785
4148
  }
3786
4149
 
3787
4150
  /** The index the next row lands at: one past the newest turn, 0 for an empty
@@ -3877,6 +4240,31 @@ export class SessionLogDO extends DurableObject<Env> {
3877
4240
  this.sql.exec(`INSERT INTO turns_fts (rowid, text) VALUES (?, ?)`, id, text);
3878
4241
  }
3879
4242
 
4243
+ /** The idempotent keyed append (session-log item 13): the parts of ONE turn
4244
+ * land at the tail under `rowId` — a fold of a `ship_unit` event, a
4245
+ * connector's turn, a migrated row. A row id the log has seen appends
4246
+ * nothing, so a fold read twice yields one row; two appends keep their
4247
+ * arrival order, since each lands at the tail inside one transaction. No
4248
+ * owner fence: a thread session has no one owning run. */
4249
+ async appendKeyed(
4250
+ rowId: string,
4251
+ rows: Array<{ part: number; json: string }>,
4252
+ ): Promise<{ ok: true; appended: boolean }> {
4253
+ let appended = false;
4254
+ this.ctx.storage.transactionSync(() => {
4255
+ const seen = this.sql.exec(`SELECT 1 FROM turns WHERE row_id = ? LIMIT 1`, rowId).toArray().length > 0;
4256
+ if (seen) return;
4257
+ const idx = this.next();
4258
+ for (const [i, r] of rows.entries()) {
4259
+ this.putRow({ idx, part: r.part, json: r.json }, r.json, false);
4260
+ if (i === 0) this.sql.exec(`UPDATE turns SET row_id = ? WHERE idx = ? AND part = ?`, rowId, idx, r.part);
4261
+ }
4262
+ appended = true;
4263
+ this.enforceBytePolicy();
4264
+ });
4265
+ return { ok: true, appended };
4266
+ }
4267
+
3880
4268
  async write(
3881
4269
  gen: string,
3882
4270
  rows: TranscriptRow[],
@@ -3963,7 +4351,10 @@ export class SessionLogDO extends DurableObject<Env> {
3963
4351
  if (ids.length === 0) break;
3964
4352
  for (const id of ids) {
3965
4353
  const row = this.sql
3966
- .exec<SessionTurnRow>(`SELECT id, idx, part, json, text FROM turns WHERE id = ?`, id)
4354
+ .exec<SessionTurnRow & { row_id: string | null }>(
4355
+ `SELECT id, idx, part, json, text, row_id FROM turns WHERE id = ?`,
4356
+ id,
4357
+ )
3967
4358
  .toArray()[0];
3968
4359
  if (!row) continue;
3969
4360
  const marker = droppedToolResultRow(row.json);
@@ -3974,6 +4365,11 @@ export class SessionLogDO extends DurableObject<Env> {
3974
4365
  }
3975
4366
  const refs = attachmentRefsOf(row.json);
3976
4367
  this.putRow({ idx: row.idx, part: row.part, json: marker }, marker, true);
4368
+ // The marker keeps the row's keyed-append id (item 13): `putRow` deletes
4369
+ // the old row, so without this the partial unique index forgets the id
4370
+ // and a replayed migration or fold would re-append the trimmed turn.
4371
+ if (row.row_id !== null)
4372
+ this.sql.exec(`UPDATE turns SET row_id = ? WHERE idx = ? AND part = ?`, row.row_id, row.idx, row.part);
3977
4373
  // The marker references nothing, so an attachment only this row showed is now orphaned.
3978
4374
  for (const ref of refs) {
3979
4375
  if (!this.referencedElsewhere(ref, -1)) this.sql.exec(`DELETE FROM attachments WHERE ref = ?`, ref);
@@ -4135,6 +4531,7 @@ const LEDGER_ROUTES = new Set([
4135
4531
  "/runs/coordinator/put",
4136
4532
  "/runs/coordinator/replace",
4137
4533
  "/runs/coordinator/get",
4534
+ "/runs/coordinator/stop",
4138
4535
  "/runs/coordinator/units/put",
4139
4536
  "/runs/coordinator/units/list",
4140
4537
  "/runs/coordinator/events/append",
@@ -4165,6 +4562,7 @@ const LEDGER_ROUTES = new Set([
4165
4562
  "/runs/session/tail",
4166
4563
  "/runs/session/owner",
4167
4564
  "/runs/session/write",
4565
+ "/runs/session/append",
4168
4566
  "/runs/session/read",
4169
4567
  "/runs/session/read-tail",
4170
4568
  "/runs/session/clear-owner",
@@ -4176,7 +4574,19 @@ const LEDGER_ROUTES = new Set([
4176
4574
  /** The plane's routes (record 0064; orchestration-plane items 7 and 8): the shadow outcome post and the
4177
4575
  * effect acknowledgement. Both land on the ledger object of the given store
4178
4576
  * key, like every `/runs/*` route. */
4179
- const PLANE_ROUTES = new Set(["/plane/outcome", "/plane/ack"]);
4577
+ const PLANE_ROUTES = new Set([
4578
+ "/plane/outcome",
4579
+ "/plane/ack",
4580
+ "/plane/admit",
4581
+ "/plane/withdraw",
4582
+ "/plane/deploy",
4583
+ "/plane/queued",
4584
+ "/plane/level",
4585
+ "/plane/observe",
4586
+ ]);
4587
+
4588
+ const PLANE_LEVEL_NAMES = new Set(["seat", "memory", "drain"]);
4589
+ const PLANE_LEVEL_SIDES = new Set(["below", "above"]);
4180
4590
 
4181
4591
  const PLANE_STAGES = new Set(["admission", "runner", "resident"]);
4182
4592
  const PLANE_OUTCOME = /^(proceeded|refused:[a-z0-9-]+|fell_cold:[a-z0-9_-]+)$/;
@@ -4224,6 +4634,103 @@ async function handlePlane(pathname: string, body: unknown, env: Env): Promise<R
4224
4634
  return json({ error: "outcome must be done, skipped or deferred" }, 400);
4225
4635
  return json(await stub.planeAck(b.id, b.outcome as PlaneAckOutcome, now));
4226
4636
  }
4637
+ if (pathname === "/plane/admit") {
4638
+ // The admission-stage ask (record 0064, "The queue"): the thread key, the
4639
+ // requester and the request in the durable inbox's shape. The route mints
4640
+ // the run id: `queued` stores the request under it, `admitted` names it as
4641
+ // the reservation.
4642
+ if (typeof b.threadKey !== "string" || b.threadKey.length === 0)
4643
+ return json({ error: "threadKey must be a non-empty string" }, 400);
4644
+ if (typeof b.requester !== "string" || b.requester.length === 0)
4645
+ return json({ error: "requester must be a non-empty string" }, 400);
4646
+ if (typeof b.request !== "object" || b.request === null || Array.isArray(b.request))
4647
+ return json({ error: "request must be a JSON object" }, 400);
4648
+ if (b.stage !== undefined && (typeof b.stage !== "string" || !PLANE_STAGES.has(b.stage)))
4649
+ return json({ error: "stage must be admission, runner or resident" }, 400);
4650
+ if (b.resident !== undefined && (typeof b.resident !== "string" || b.resident.length === 0))
4651
+ return json({ error: "resident must be a non-empty string" }, 400);
4652
+ if (b.restartOf !== undefined && typeof b.restartOf !== "boolean")
4653
+ return json({ error: "restartOf must be a boolean" }, 400);
4654
+ if (b.reaskMs !== undefined && (typeof b.reaskMs !== "number" || !(b.reaskMs > 0)))
4655
+ return json({ error: "reaskMs must be a positive number of milliseconds" }, 400);
4656
+ try {
4657
+ const answer = await stub.planeAdmit(
4658
+ {
4659
+ runId: crypto.randomUUID(),
4660
+ requester: b.requester,
4661
+ threadKey: b.threadKey,
4662
+ request: b.request as Record<string, unknown>,
4663
+ ...(b.stage !== undefined ? { stage: b.stage as PlaneStage } : {}),
4664
+ ...(b.resident !== undefined ? { resident: b.resident } : {}),
4665
+ ...(b.restartOf !== undefined ? { restartOf: b.restartOf } : {}),
4666
+ ...(b.reaskMs !== undefined ? { reaskMs: b.reaskMs } : {}),
4667
+ },
4668
+ now,
4669
+ );
4670
+ return json(answer);
4671
+ } catch (err) {
4672
+ // The effect caps refuse by name (record 0064): the ask is answered with
4673
+ // the cap's own sentence, never queued silently.
4674
+ return json({ error: err instanceof Error ? err.message : String(err) }, 409);
4675
+ }
4676
+ }
4677
+ if (pathname === "/plane/level") {
4678
+ // A resident's level report (record 0064): forwarded by the bot from the levels a
4679
+ // resident answer carried, or from the registry's drain outbox.
4680
+ if (typeof b.resident !== "string" || b.resident.length === 0)
4681
+ return json({ error: "resident must be a non-empty string" }, 400);
4682
+ if (typeof b.name !== "string" || !PLANE_LEVEL_NAMES.has(b.name))
4683
+ return json({ error: "name must be seat, memory or drain" }, 400);
4684
+ if (typeof b.side !== "string" || !PLANE_LEVEL_SIDES.has(b.side))
4685
+ return json({ error: "side must be below or above" }, 400);
4686
+ if (typeof b.generation !== "string") return json({ error: "generation must be a string" }, 400);
4687
+ return json(
4688
+ await stub.planeLevel(
4689
+ {
4690
+ resident: b.resident,
4691
+ name: b.name as "seat" | "memory" | "drain",
4692
+ side: b.side as "below" | "above",
4693
+ generation: b.generation,
4694
+ },
4695
+ now,
4696
+ ),
4697
+ );
4698
+ }
4699
+ if (pathname === "/plane/observe") {
4700
+ // A refusal-by-name met at attach or exec (record 0064).
4701
+ const parsed = parseRunId(b.runId);
4702
+ if (!parsed.ok) return json({ error: parsed.error }, 400);
4703
+ if (typeof b.resident !== "string" || b.resident.length === 0)
4704
+ return json({ error: "resident must be a non-empty string" }, 400);
4705
+ if (typeof b.refusal !== "string" || b.refusal.length === 0)
4706
+ return json({ error: "refusal must be a non-empty string" }, 400);
4707
+ return json(await stub.planeObserve({ runId: parsed.value, resident: b.resident, refusal: b.refusal }, now));
4708
+ }
4709
+ if (pathname === "/plane/withdraw") {
4710
+ const parsed = parseRunId(b.runId);
4711
+ if (!parsed.ok) return json({ error: parsed.error }, 400);
4712
+ return json(await stub.planeWithdraw(parsed.value, now));
4713
+ }
4714
+ if (pathname === "/plane/queued") {
4715
+ const parsed = parseRunId(b.runId);
4716
+ if (!parsed.ok) return json({ error: parsed.error }, 400);
4717
+ return json({ row: await stub.planeQueueRowOf(parsed.value) });
4718
+ }
4719
+ if (pathname === "/plane/deploy") {
4720
+ // The deploy runner's post (record 0064, "The queue"): `landed` lifts the
4721
+ // pending-deploy window — `deploy_settled` flips and the queue walks;
4722
+ // `pending` opens it. Version and workers ride the log line only.
4723
+ if (b.phase !== "landed" && b.phase !== "pending") return json({ error: "phase must be landed or pending" }, 400);
4724
+ try {
4725
+ const r = await stub.planeWindow("deploy", b.phase === "landed" ? "lifted" : "opened", now);
4726
+ console.log(
4727
+ `[plane/deploy] ${b.phase}${typeof b.version === "string" ? ` ${b.version}` : ""} — ${r.admitted} admission(s)`,
4728
+ );
4729
+ return json({ ok: true, admitted: r.admitted });
4730
+ } catch (err) {
4731
+ return json({ error: err instanceof Error ? err.message : String(err) }, 409);
4732
+ }
4733
+ }
4227
4734
  return json({ error: "not found" }, 404);
4228
4735
  }
4229
4736
 
@@ -4234,6 +4741,7 @@ const WIDE_BODY_ROUTES = new Set([
4234
4741
  "/runs/append",
4235
4742
  "/runs/transcript/write",
4236
4743
  "/runs/session/write",
4744
+ "/runs/session/append",
4237
4745
  ]);
4238
4746
  /** A delivery snapshot written whole, or a refresh's patch: every merged pull request's reviews and
4239
4747
  * its branch's workflow runs — about 7 KB a pull request (measured: 291 pull requests, 2.1 MB), so
@@ -4357,6 +4865,24 @@ function parseTranscriptRows(v: unknown): Validated<TranscriptRow[]> {
4357
4865
  return { ok: true, value: v as TranscriptRow[] };
4358
4866
  }
4359
4867
 
4868
+ /** The keyed append's rows (session-log item 13): the parts of one turn, no index — the object assigns the tail's. */
4869
+ function parseKeyedRows(v: unknown): Validated<Array<{ part: number; json: string }>> {
4870
+ if (!Array.isArray(v) || v.length === 0) return invalid("rows must be a non-empty array");
4871
+ for (const r of v) {
4872
+ const row = r as Record<string, unknown>;
4873
+ if (typeof row?.part !== "number" || !Number.isInteger(row.part) || row.part < 0 || typeof row?.json !== "string")
4874
+ return invalid("rows entries must be {part, json}");
4875
+ }
4876
+ return { ok: true, value: v as Array<{ part: number; json: string }> };
4877
+ }
4878
+
4879
+ /** The keyed append's row id (session-log item 13): non-empty, bounded — an event id, a message id with its edit stamp, a migrated row's name. */
4880
+ function parseRowId(v: unknown): Validated<string> {
4881
+ if (typeof v !== "string" || v.length === 0 || v.length > 512)
4882
+ return invalid("rowId must be a string of 1..512 characters");
4883
+ return { ok: true, value: v };
4884
+ }
4885
+
4360
4886
  function parseSessionKey(v: unknown): Validated<string> {
4361
4887
  if (typeof v !== "string" || !SESSION_KEY_PATTERN.test(v)) return invalid("key must be a session key");
4362
4888
  return { ok: true, value: v };
@@ -4422,6 +4948,17 @@ async function handleLedger(pathname: string, body: unknown, env: Env): Promise<
4422
4948
  );
4423
4949
  return fenced(r);
4424
4950
  }
4951
+ if (pathname === "/runs/session/append") {
4952
+ const rowId = parseRowId(b.rowId);
4953
+ if (!rowId.ok) return json({ error: rowId.error }, 400);
4954
+ const rows = parseKeyedRows(b.rows);
4955
+ if (!rows.ok) return json({ error: rows.error }, 400);
4956
+ const r = await stub.appendKeyed(rowId.value, rows.value);
4957
+ console.log(
4958
+ `[runs/session/append] ${key.value} <- ${rows.value.length} row(s) under ${rowId.value}, appended=${r.appended}`,
4959
+ );
4960
+ return json(r);
4961
+ }
4425
4962
  if (pathname === "/runs/session/read") {
4426
4963
  const from = parseLogIndex(b.from, "from");
4427
4964
  if (!from.ok) return json({ error: from.error }, 400);
@@ -4571,6 +5108,16 @@ async function handleLedger(pathname: string, body: unknown, env: Env): Promise<
4571
5108
  return json({ error: "id must be a Workflow instance id" }, 400);
4572
5109
  return json({ instance: await stub.getInstance(b.id) });
4573
5110
  }
5111
+ // The hard stop's mark on the instance row (record 0060; issue 1924): the
5112
+ // bot writes it when the hosted parent is sealed; the runner reads it back.
5113
+ if (pathname === "/runs/coordinator/stop") {
5114
+ if (typeof b.instanceId !== "string" || !INSTANCE_ID_PATTERN.test(b.instanceId))
5115
+ return json({ error: "instanceId must be a Workflow instance id" }, 400);
5116
+ if (typeof b.at !== "number" || !Number.isFinite(b.at)) return json({ error: "at must be a time" }, 400);
5117
+ const r = await stub.markInstanceStopped(b.instanceId, b.at);
5118
+ console.log(`[runs/coordinator/stop] ${key.value} ${b.instanceId} → ${r.ok ? "marked" : r.reason}`);
5119
+ return r.ok ? json(r) : json(r, 409);
5120
+ }
4574
5121
  // The units of the plan an instance runs (run-history item 50): rows
4575
5122
  // validated by the shared contract, each replaced whole; a list by instance.
4576
5123
  if (pathname === "/runs/coordinator/units/put") {