diff --git a/.gitignore b/.gitignore
index c1df70401a..bbb05dc1e3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@ dist/
.artifacts/
artifacts/
dashboard/.wrangler/
+.wrangler/
.clawsweeper-repair/
.claude/
tmp/
diff --git a/dashboard/bay-page.ts b/dashboard/bay-page.ts
index 31c3f33537..c63dcae5a2 100644
--- a/dashboard/bay-page.ts
+++ b/dashboard/bay-page.ts
@@ -31,7 +31,7 @@ button,input,a{font:inherit} button{color:inherit} a{color:inherit}
.repo-bar{margin-top:10px;display:flex;align-items:center;gap:7px;flex-wrap:wrap}.repo-bar>span{font:800 9px ui-monospace,monospace;color:var(--muted);text-transform:uppercase;letter-spacing:.12em}.repo-button{border:1px solid var(--line);background:rgba(255,255,255,.72);border-radius:999px;padding:6px 10px;font-size:10px;cursor:pointer}.repo-button:before{content:"";display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--repo-color,#788);margin-right:6px}.repo-button.active{border-color:var(--repo-color,var(--claw));box-shadow:inset 0 0 0 1px var(--repo-color,var(--claw));background:white}
.notice{display:none;padding:8px 16px;background:#fff5d9;border-bottom:1px solid #e4c57d;color:#6d5421;font-size:11px}.notice.show{display:block}
.beach{position:relative;min-height:690px;overflow:hidden;background:linear-gradient(180deg,var(--sand-light),var(--sand));isolation:isolate}.beach:before{content:"";position:absolute;z-index:-3;right:-2%;top:0;bottom:0;width:20%;background:repeating-radial-gradient(ellipse at 0 50%,transparent 0 21px,rgba(220,250,249,.38) 22px 24px,transparent 25px 42px),linear-gradient(90deg,#86c0bf,var(--sea))}.beach:after{content:"";position:absolute;z-index:-2;left:0;top:0;bottom:0;width:6.5%;background:repeating-linear-gradient(90deg,#8a603d 0 3px,#b98b61 3px 18px,#7f5839 18px 20px);clip-path:polygon(0 0,100% 7%,77% 100%,0 100%)}
-.beach-inner{position:relative;min-height:690px;padding:64px 20% 22px 7.5%}.stage-grid{display:grid;grid-template-columns:repeat(5,minmax(120px,1fr));height:600px}.stage{position:relative;border-right:1px dashed rgba(100,72,41,.22);min-width:0}.stage:last-child{border-right:0}.stage h2,.pool h2{position:absolute;z-index:5;left:50%;top:0;translate:-50% 0;margin:0;padding:9px 12px;border-radius:4px;background:#ad7c50;color:#352417;box-shadow:0 4px 0 #765032,0 8px 16px rgba(66,45,25,.13);font:800 9px ui-monospace,monospace;letter-spacing:.13em;text-transform:uppercase;white-space:nowrap}.stage h2 span,.pool h2 span{margin-left:6px;opacity:.68}.stage-body{position:absolute;inset:58px 5px 0}.empty{position:absolute;left:12px;right:12px;top:48%;text-align:center;color:rgba(87,70,49,.52);font-size:10px}.overflow-note{position:absolute;z-index:7;left:50%;bottom:5px;translate:-50% 0;padding:5px 8px;border:1px solid rgba(100,72,41,.2);border-radius:999px;background:rgba(255,249,235,.92);color:#6f5a42;font:700 8px ui-monospace,monospace;white-space:nowrap}.loading{position:absolute;inset:0;display:grid;place-items:center;color:#765f42;font:700 12px ui-monospace,monospace}.loading i{display:block;width:34px;height:34px;margin:0 auto 10px;border:3px solid rgba(117,89,52,.2);border-top-color:var(--claw);border-radius:50%;animation:spin .8s linear infinite}
+.beach-inner{position:relative;min-height:690px;padding:64px 20% 22px 7.5%}.stage-grid{display:grid;grid-template-columns:repeat(6,minmax(120px,1fr));height:600px}.stage{position:relative;border-right:1px dashed rgba(100,72,41,.22);min-width:0}.stage:last-child{border-right:0}.stage h2,.pool h2{position:absolute;z-index:5;left:50%;top:0;translate:-50% 0;margin:0;padding:9px 12px;border-radius:4px;background:#ad7c50;color:#352417;box-shadow:0 4px 0 #765032,0 8px 16px rgba(66,45,25,.13);font:800 9px ui-monospace,monospace;letter-spacing:.13em;text-transform:uppercase;white-space:nowrap}.stage h2 span,.pool h2 span{margin-left:6px;opacity:.68}.stage-body{position:absolute;inset:58px 5px 0}.empty{position:absolute;left:12px;right:12px;top:48%;text-align:center;color:rgba(87,70,49,.52);font-size:10px}.overflow-note{position:absolute;z-index:7;left:50%;bottom:5px;translate:-50% 0;padding:5px 8px;border:1px solid rgba(100,72,41,.2);border-radius:999px;background:rgba(255,249,235,.92);color:#6f5a42;font:700 8px ui-monospace,monospace;white-space:nowrap}.loading{position:absolute;inset:0;display:grid;place-items:center;color:#765f42;font:700 12px ui-monospace,monospace}.loading i{display:block;width:34px;height:34px;margin:0 auto 10px;border:3px solid rgba(117,89,52,.2);border-top-color:var(--claw);border-radius:50%;animation:spin .8s linear infinite}
.terminal-stack{position:absolute;right:2.8%;top:64px;bottom:24px;width:15.5%;display:flex;flex-direction:column;gap:11px}.pool{position:relative;min-height:130px;border:1px dashed rgba(94,66,37,.25);border-radius:50%;background:rgba(204,210,190,.52);overflow:visible}.pool.failed{background:rgba(236,190,157,.54);border-color:rgba(180,73,45,.45)}.pool.cancelled{background:rgba(206,199,179,.52)}.pool-body{position:absolute;inset:47px 8px 8px}.pool .empty{top:45%}
.critter{position:absolute;left:var(--x);top:var(--y);width:76px;height:72px;border:0;background:transparent;padding:18px 3px 0;cursor:pointer;filter:drop-shadow(0 6px 4px rgba(76,53,29,.23));transition:transform .22s,filter .22s;animation:breathe 3.4s ease-in-out infinite;animation-delay:var(--delay)}.critter:hover,.critter:focus-visible{z-index:25;transform:translateY(-5px) scale(1.1);filter:drop-shadow(0 10px 7px rgba(76,53,29,.3));outline:none}.critter svg{width:67px;height:49px;overflow:visible;filter:hue-rotate(var(--hue)) saturate(var(--sat)) brightness(var(--light));transform:scale(var(--sx),var(--sy)) rotate(var(--tilt));transform-origin:center bottom}.critter .ref{position:absolute;z-index:4;left:50%;top:0;translate:-50% 0;white-space:nowrap;padding:4px 6px;border:1px solid var(--repo-color);border-radius:5px;background:rgba(255,255,255,.94);box-shadow:0 3px 8px rgba(60,45,28,.12);color:#51463a;font:700 8px ui-monospace,monospace}.repo-pin{position:absolute;right:3px;bottom:5px;width:7px;height:7px;border-radius:50%;background:var(--repo-color);border:1px solid white}.pool .critter{width:65px;height:60px;padding-top:16px}.pool .critter svg{width:54px;height:40px}.pool .critter .ref{font-size:7px}.critter.located{z-index:50;animation:found .7s ease-in-out infinite;filter:drop-shadow(0 0 5px white) drop-shadow(0 0 15px #f2a12d)}.find-arrow{display:none;position:absolute;z-index:6;left:50%;top:-45px;translate:-50% 0;padding:5px 7px;border:2px solid #96301f;border-radius:999px;background:var(--claw);color:white;box-shadow:0 4px 0 #96301f;font:900 8px ui-monospace,monospace;white-space:nowrap}.find-arrow:after{content:"↓";position:absolute;left:50%;top:20px;translate:-50%;color:var(--claw);font-size:25px}.critter.located .find-arrow{display:block}.critter.retriggered{animation:emerge 1.7s cubic-bezier(.2,.8,.2,1),breathe 3.4s 1.7s ease-in-out infinite}.critter.terminal-completed .claw{transform-origin:center;animation:wave-claw 1.5s ease-in-out infinite}.tide-imminent .terminal-completed .claw{animation-duration:.38s}.critter.terminal-failed{filter:drop-shadow(0 0 8px rgba(190,65,38,.4))}.critter.terminal-cancelled{filter:grayscale(.55) sepia(.2) drop-shadow(0 5px 4px rgba(76,53,29,.18))}
.master{position:absolute;z-index:14;left:7%;top:50%;width:215px;height:100px;pointer-events:none;transition:left 3.4s cubic-bezier(.42,.04,.22,1),top 3.4s cubic-bezier(.42,.04,.22,1),transform .5s}.master svg{position:absolute;left:0;top:5px;width:135px;height:79px;filter:drop-shadow(0 7px 5px rgba(76,53,29,.25))}.brush{position:absolute;left:116px;top:49px;width:88px;height:9px;border-radius:8px;background:linear-gradient(#775032,#4e321f);transform:rotate(2deg);transform-origin:left center}.brush:after{content:"";position:absolute;right:-3px;top:-18px;width:27px;height:45px;border-radius:5px 10px 10px 5px;background:repeating-linear-gradient(90deg,#c59041 0 3px,#e5be6e 3px 6px);border:1px solid #98682d;transform:rotate(-3deg)}.master.sweeping .brush:after{animation:brush 1s ease-in-out infinite}.station{position:absolute;z-index:2;left:1.5%;top:43%;width:72px;height:155px;color:#206b70}.station strong{position:absolute;top:-22px;left:-3px;background:#28767a;color:white;border-radius:4px;padding:5px 7px;font:800 7px ui-monospace,monospace;text-transform:uppercase;white-space:nowrap}.station:before{content:"";position:absolute;left:17px;top:8px;width:42px;height:61px;border:5px solid #f4eee0;background:repeating-linear-gradient(0deg,#367f82 0 6px,#e9e3d6 6px 12px)}.station:after{content:"";position:absolute;left:9px;top:70px;width:58px;height:72px;border-left:5px solid #eee5d5;border-right:5px solid #eee5d5;clip-path:polygon(20% 0,80% 0,100% 100%,80% 100%,62% 28%,38% 28%,20% 100%,0 100%)}
@@ -87,6 +87,34 @@ dialog{border:0;padding:0;margin:0 0 0 auto;width:min(580px,94vw);height:100vh;m
@media((max-width:820px) and (orientation:portrait)),(max-width:600px){.masthead{min-height:0;gap:7px;padding:8px 11px}.brand{min-width:0}.brand small,.demo-chip,.brush-control>span{display:none}.nav{order:0;flex:1;width:auto;justify-content:flex-end;gap:1px}.nav a{padding:7px 8px;font-size:10px}.brush-control{gap:4px}.brush-control button{padding:6px 8px;font-size:10px}.hero{padding:10px 12px 8px}.title-row{align-items:center;gap:7px;flex-wrap:wrap}.title-row h1{font-size:clamp(29px,9vw,37px)}.title-row p{display:none}.live-chip{padding:4px 7px;font-size:10px}.overall-average{order:4;flex-basis:100%;margin-left:0;align-self:flex-start;padding:6px 9px}.overall-average strong{font-size:12px}.overall-average small{font-size:8px}.tools{margin-top:8px;gap:7px}.finder{padding:7px;flex-wrap:wrap;gap:6px}.finder label{font-size:8px}.finder input{width:auto;min-width:0;flex:1;padding:7px 8px}.finder-status{flex-basis:100%;min-width:0;padding-left:1px}.tide-panel{width:100%;padding:7px 9px}.repo-bar{max-height:72px;overflow:auto;padding-bottom:2px}.repo-bar>span{flex-basis:100%}.beach{min-height:0;overflow:hidden;background-color:#efd8a8;background-image:linear-gradient(180deg,rgba(248,236,204,.08),rgba(106,72,37,.1)),url('/bay-assets/bay-background-portrait.webp?v=1');background-size:100% 100%,cover;background-position:center,center top;background-attachment:scroll,fixed}.beach:after{background:linear-gradient(90deg,rgba(54,34,18,.1),transparent 30%,transparent 74%,rgba(21,104,113,.12))}.beach-inner{min-width:0;min-height:0;padding:142px 13px 0}.stage-grid{grid-template-columns:1fr!important;gap:0;height:auto}.lane-lines{display:none}.stage{min-height:195px;border:0}.stage:before{content:"";position:absolute;z-index:1;left:5%;right:5%;bottom:0;height:10px;border-bottom:3px dashed rgba(103,69,36,.43);filter:drop-shadow(0 2px 0 rgba(255,239,196,.65));transform:rotate(var(--lane-divider-tilt,-.4deg))}.stage:nth-child(even):before{--lane-divider-tilt:.55deg}.stage:after{right:auto;left:50%;bottom:-7px;width:72px;height:14px;transform:translateX(-50%) rotate(-1.8deg)}.stage:nth-child(even):after{transform:translateX(-50%) rotate(2deg)}.stage h2{top:8px;bottom:auto}.stage-body{inset:52px 10px 24px}.overflow-note{bottom:3px}.station{left:2%;top:22px;transform:scale(.78);transform-origin:left top}.master{z-index:15}.terminal-stack{position:relative;right:auto;top:auto;bottom:auto;width:auto;min-width:0;margin:28px 13px 24px;display:grid;grid-template-columns:1fr;gap:18px}.terminal-stack:before{display:none}.pool{min-height:205px!important;flex-grow:0!important;flex-basis:auto!important}.pool.completed{min-height:340px!important}.pool h2{top:12px}.pool-body{inset:54px 12px 16px}.sample-note{position:relative;left:auto;right:auto;bottom:auto;margin:0 13px 12px;padding-bottom:5px}.chat-overlay{z-index:90}.overlay-speech{width:min(220px,calc(100vw - 28px))}.overlay-speech.answer{width:min(250px,calc(100vw - 28px))}}
@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition:none!important}.beach.tide-active .wave{animation:none!important;transform:translate3d(2%,0,0);opacity:.44}.beach.tide-active .tide-wet-sheen{animation:none!important;opacity:.3}.beach.tide-active .tide-wet-gleam{animation:none!important;opacity:.32}.beach.preview-tide-cleared .pool .critter{opacity:.2;transform:none;filter:grayscale(.15)}}
.bay-control-head{display:flex;align-items:center;justify-content:space-between;gap:9px;margin-top:8px}.bay-control-head>span{color:#6f604d;font:800 8px ui-monospace,monospace;letter-spacing:.12em;text-transform:uppercase}.bay-range-controls{display:flex;gap:4px}.bay-range-controls button{border:1px solid rgba(83,66,43,.22);border-radius:999px;background:rgba(255,252,245,.74);color:#665847;padding:4px 7px;font:800 8px ui-monospace,monospace;cursor:pointer}.bay-range-controls button:hover{background:#fff7e8}.bay-range-controls button.active{border-color:#d95534;background:#fff1eb;color:#a33c29}.bay-range-controls button:focus-visible{outline:2px solid #186d72;outline-offset:2px}.bay-control-board{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px;margin-top:5px}.bay-control-card{min-width:0;padding:8px 10px;border:1px solid rgba(83,66,43,.18);border-radius:10px;background:rgba(255,252,245,.84);box-shadow:0 5px 15px rgba(48,45,34,.06)}.bay-control-card h2{margin:0;color:#6f604d;font:800 8px ui-monospace,monospace;letter-spacing:.12em;text-transform:uppercase}.bay-control-summary{display:flex;align-items:baseline;justify-content:space-between;gap:8px;margin-top:4px}.bay-control-summary strong{font:800 16px ui-monospace,monospace;color:#264f51;letter-spacing:-.04em}.bay-control-summary span{font-size:9px;color:var(--muted);text-align:right}.bay-control-charts{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:6px}.bay-control-chart{min-width:0}.bay-control-chart small{display:block;margin-bottom:2px;color:#6f604d;font:800 7px ui-monospace,monospace;letter-spacing:.06em;text-transform:uppercase}.bay-control-chart svg{display:block;width:100%;height:44px;overflow:visible}.bay-control-grid{stroke:rgba(70,88,78,.16);stroke-width:1}.bay-control-axis-label{fill:#766956;font:800 7px ui-monospace,monospace}.bay-control-line{fill:none;stroke:#df552f;stroke-width:2.1;stroke-linejoin:round;stroke-linecap:round}.bay-control-line.rate{stroke:#6859c7}.bay-control-point{fill:#df552f;stroke:#fffaf0;stroke-width:1.1}.bay-control-point.rate{fill:#6859c7}.bay-control-rate{margin-top:5px;color:#287456;font-size:9px;font-weight:800;line-height:1.3}.bay-control-rate.rising{color:#b74331}.bay-control-rate.collecting{color:var(--muted);font-weight:650}.bay-handoff{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px;margin-top:7px}.bay-handoff div{min-width:0}.bay-handoff span{display:block;color:var(--muted);font:800 7px ui-monospace,monospace;text-transform:uppercase;letter-spacing:.08em}.bay-handoff strong{display:block;margin-top:2px;font:800 13px ui-monospace,monospace;color:#294a4b}.bay-handoff-status{display:inline-block;margin-top:7px;padding:3px 5px;border-radius:999px;background:#e8f4ea;color:#287456;font:800 8px ui-monospace,monospace;letter-spacing:.05em;text-transform:uppercase}.bay-handoff-status.degraded,.bay-handoff-status.stalled{background:#fff0e9;color:#a5402c}.bay-control-empty{margin:6px 0 0;color:var(--muted);font-size:10px}.queue-sample-copy{margin:0 0 14px;color:#5d5041;font-size:12px;line-height:1.5}.queue-sample-list{list-style:none;margin:0;padding:0}.queue-sample-list li{display:flex;align-items:baseline;justify-content:space-between;gap:10px;padding:9px 0;border-bottom:1px solid #e5daca;font-size:12px}.queue-sample-list a{color:#a9402c;font-weight:750;text-decoration:none}.queue-sample-list span{color:var(--muted);font-size:10px;text-align:right}@media(max-width:900px){.bay-control-board{grid-template-columns:1fr 1fr}.bay-control-card:last-child{grid-column:1/-1}}@media((max-width:820px) and (orientation:portrait)),(max-width:600px){.bay-control-head{align-items:flex-start}.bay-control-head>span{padding-top:5px}.bay-control-board{grid-template-columns:1fr;margin-top:7px}.bay-control-card:last-child{grid-column:auto}.bay-control-summary strong{font-size:15px}.lane-help-copy{position:fixed;left:14px;right:14px;bottom:18px;translate:0;width:auto;max-width:none}.overflow-note{font-size:10px}}
+.bay-state-writer-stats{display:grid;align-content:center;gap:2px;min-height:44px}.bay-state-writer-stats strong{color:#294a4b;font:800 13px ui-monospace,monospace}.bay-state-writer-stats span{color:var(--muted);font-size:9px;line-height:1.25}@media(min-width:1301px){.bay-control-board{grid-template-columns:repeat(4,minmax(0,1fr))}}@media(min-width:901px) and (max-width:1300px){.bay-control-board{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:900px){.bay-control-board{grid-template-columns:1fr 1fr}.bay-control-card:last-child{grid-column:auto}}
+@media (max-width:970px) and (orientation:landscape){
+ .beach{min-height:0;overflow:hidden}
+ .beach-inner{min-width:0;min-height:0;padding:142px 13px 0}
+ .stage-grid{grid-template-columns:1fr!important;gap:0;height:auto}
+ .lane-lines{display:none}
+ .stage{min-height:195px;border:0}
+ .stage:before{content:"";position:absolute;z-index:1;left:5%;right:5%;bottom:0;height:10px;border-bottom:3px dashed rgba(103,69,36,.43);filter:drop-shadow(0 2px 0 rgba(255,239,196,.65));transform:rotate(var(--lane-divider-tilt,-.4deg))}
+ .stage:nth-child(even):before{--lane-divider-tilt:.55deg}
+ .stage:after{right:auto;left:50%;bottom:-7px;width:72px;height:14px;transform:translateX(-50%) rotate(-1.8deg)}
+ .stage:nth-child(even):after{transform:translateX(-50%) rotate(2deg)}
+ .stage h2{top:8px;bottom:auto}
+ .stage-body{inset:52px 10px 24px}
+ .overflow-note{bottom:3px}
+ .station{left:2%;top:22px;transform:scale(.78);transform-origin:left top}
+ .master{z-index:15}
+ .terminal-stack{position:relative;right:auto;top:auto;bottom:auto;width:auto;min-width:0;margin:28px 13px 24px;display:grid;grid-template-columns:1fr;gap:18px}
+ .terminal-stack:before{display:none}
+ .pool{min-height:205px!important;flex-grow:0!important;flex-basis:auto!important}
+ .pool.completed{min-height:340px!important}
+ .pool h2{top:12px}
+ .pool-body{inset:54px 12px 16px}
+ .sample-note{position:relative;left:auto;right:auto;bottom:auto;margin:0 13px 12px;padding-bottom:5px}
+}
+@media((max-width:820px) and (orientation:portrait)),(max-width:600px){
+ .bay-control-board{grid-template-columns:1fr}
+ .bay-control-card:last-child{grid-column:auto}
+}
@@ -168,9 +196,9 @@ dialog{border:0;padding:0;margin:0 0 0 auto;width:min(580px,94vw);height:100vh;m
diff --git a/dashboard/exact-review-publication-batches.ts b/dashboard/exact-review-publication-batches.ts
index b962f23414..2a2936528f 100644
--- a/dashboard/exact-review-publication-batches.ts
+++ b/dashboard/exact-review-publication-batches.ts
@@ -56,6 +56,10 @@ export type PublicationBatchStats = {
expired: number;
activeItems: number;
activeItemKeys: string[];
+ // This contains only unfinished, currently leased membership. It lets the
+ // read-only Bay projection identify the bounded batch that owns an item
+ // without retaining a separate event history or looking it up through GitHub.
+ activeItemBatches: Array<{ itemKey: string; batchId: string }>;
nextLeaseExpiresAt: number | null;
oldestActiveAt: number | null;
reclaimedItemsRetained: number;
@@ -424,6 +428,7 @@ export class ExactReviewPublicationBatchStore {
expired: counts.get("expired") ?? 0,
activeItems: activeLease.itemKeys.length,
activeItemKeys: activeLease.itemKeys,
+ activeItemBatches: activeLease.items,
nextLeaseExpiresAt: activeLease.nextLeaseExpiresAt,
oldestActiveAt: leased ? Number(leased.oldest_at) : null,
reclaimedItemsRetained,
@@ -470,6 +475,10 @@ export class ExactReviewPublicationBatchStore {
),
);
return {
+ items: rows.map((row) => ({
+ itemKey: String(row.item_key),
+ batchId: String(row.batch_id),
+ })),
itemKeys: rows.map((row) => String(row.item_key)),
activeBatches: new Set(rows.map((row) => String(row.batch_id))).size,
nextLeaseExpiresAt: rows.length
diff --git a/dashboard/exact-review-queue.ts b/dashboard/exact-review-queue.ts
index 9bc96188c2..46c0ef74f3 100644
--- a/dashboard/exact-review-queue.ts
+++ b/dashboard/exact-review-queue.ts
@@ -1880,7 +1880,10 @@ export class ExactReviewQueue {
stateWriterCoordinatorQueuedStaleMs(this.env),
);
const publicationBatches = this.batchStore.stats(now);
- const batchOwnedItemKeys = new Set(publicationBatches.activeItemKeys);
+ const batchByItemKey = new Map(
+ publicationBatches.activeItemBatches.map((batch) => [batch.itemKey, batch] as const),
+ );
+ const batchOwnedItemKeys = new Set(batchByItemKey.keys());
const freshPublicationItemKeys = this.freshPublicationItemKeysSync(state, now);
const legacyExcludedItemKeys = new Set(batchOwnedItemKeys);
if (exactReviewPublicationBatchingEnabled(this.env)) {
@@ -1920,7 +1923,11 @@ export class ExactReviewQueue {
return json({
...stats,
pressure: elevateExactReviewPressureForPublication(stats.pressure, publicationHealth),
- bay_projection: exactReviewQueueBayProjection(Object.values(state.items), bayPriorityKeys),
+ bay_projection: exactReviewQueueBayProjection(
+ Object.values(state.items),
+ bayPriorityKeys,
+ batchByItemKey,
+ ),
lanes: {
review: {
...stats.lanes.review,
@@ -7257,7 +7264,9 @@ function exactReviewQueueLane(item: ExactReviewQueueItem) {
// state. Keep this representation bounded and scrubbed: it is public dashboard
// data, not a queue-inspection API. Live workers remain the authority for the
// reviewing stage; these records only make the otherwise invisible admission,
-// setup, publication, and recovery phases visible.
+// setup, publication, and recovery phases visible. Publication is distinct
+// from the publisher workflow's deterministic follow-up, which the Bay shows
+// from the live worker as Applying.
const EXACT_REVIEW_BAY_SAMPLE_LIMIT = 24;
// The dashboard can retain both a terminal-buffer card and its washed card
// while their live queue retry is pending. Accept all bounded Bay candidates
@@ -7267,6 +7276,7 @@ const EXACT_REVIEW_BAY_STAGES = [
"arriving",
"setting-up",
"reviewing",
+ "publishing",
"applying",
"repairing",
] as const;
@@ -7280,10 +7290,28 @@ type ExactReviewBayProjectionItem = {
created_at: string;
updated_at: string;
next_attempt_at: string;
+ batch_id?: string;
+ batch_created_at?: string;
+};
+
+type ExactReviewBayBatchOwner = {
+ batchId: string;
};
-function exactReviewQueueBayStage(item: ExactReviewQueueItem): ExactReviewBayStage {
- if (exactReviewQueueIsPublication(item)) return "applying";
+function exactReviewQueueBayStage(
+ item: ExactReviewQueueItem,
+ batchByItemKey: ReadonlyMap = new Map(),
+): ExactReviewBayStage {
+ // A parked item is deliberately no longer making normal queue progress. This
+ // includes bounded review-retry exhaustion, permanent dispatch rejection,
+ // and a publication that needs its dead-letter/recovery path. Keep it in the
+ // exception cove instead of making it look like an active setup or publisher.
+ if (item.state === "parked") return "repairing";
+ // The batch publisher's GitHub job is intentionally targetless. Its durable
+ // batch membership is the authoritative bounded source for the individual
+ // items it is currently applying, without another GitHub lookup.
+ if (batchByItemKey.has(item.key)) return "applying";
+ if (exactReviewQueueIsPublication(item)) return "publishing";
if (isLowPriorityExactReviewDecision(item.decision)) return "repairing";
return item.state === "pending" ? "arriving" : "setting-up";
}
@@ -7306,22 +7334,31 @@ function exactReviewQueueBayPriorityKeys(values: string[]) {
function exactReviewQueueBayProjection(
items: ExactReviewQueueItem[],
priorityItemKeys: string[] = [],
+ batchByItemKey: ReadonlyMap = new Map(),
) {
const projected = new Map();
for (const item of items) {
- if (item.state === "parked") continue;
+ // Parked records are not terminal outcomes: they remain bounded durable
+ // queue work that needs recovery. Keep their already-scrubbed identity in
+ // the projection so Bay shows the exception rather than a false empty lane.
const repository = String(item.decision.targetRepo || "").trim();
const itemNumber = Number(item.decision.itemNumber);
if (!repository || !Number.isSafeInteger(itemNumber) || itemNumber <= 0) continue;
+ const batch = batchByItemKey.get(item.key);
const candidate: ExactReviewBayProjectionItem = {
item_key: `${repository}#${itemNumber}`,
repository,
item_number: itemNumber,
- stage: exactReviewQueueBayStage(item),
+ stage: exactReviewQueueBayStage(item, batchByItemKey),
queue_state: item.state,
created_at: new Date(item.createdAt).toISOString(),
updated_at: new Date(item.updatedAt).toISOString(),
next_attempt_at: new Date(item.nextAttemptAt).toISOString(),
+ ...(batch
+ ? {
+ batch_id: batch.batchId,
+ }
+ : {}),
};
const previous = projected.get(candidate.item_key);
const candidateUpdatedAt = Date.parse(candidate.updated_at);
diff --git a/dashboard/worker.ts b/dashboard/worker.ts
index 0fc116c08a..0412309724 100644
--- a/dashboard/worker.ts
+++ b/dashboard/worker.ts
@@ -3255,19 +3255,22 @@ async function activeWorkerSnapshot(
const detailRuns: WorkflowRunSummary[] = runs.slice(0, detailRunLimit);
const results = await mapWithConcurrency(detailRuns, fetchConcurrency, async (run) => {
try {
- const jobs = await workflowJobsForRun(env, repo, run.id, github);
+ const jobs = await workflowJobsForRun(env, repo, run.id, github, run);
+ const activeJobs = jobs.filter((job) => isActiveWorkflowJob(job));
return {
run,
- workers: jobs
- .filter((job) => isActiveWorkflowJob(job) && isCodexWorkerJob(job))
+ workers: activeJobs
+ .filter((job) => isDashboardWorkerJob(job, run))
.map((job) => normalizeWorkerJob(run, job)),
- hasWorkerJobs: jobs.some((job) => isCodexWorkerJob(job)),
+ codexWorkers: activeJobs.filter((job) => isCodexWorkerJob(job)).length,
+ hasWorkerJobs: jobs.some((job) => isDashboardWorkerJob(job, run)),
error: null,
};
} catch (error) {
return {
run,
workers: [],
+ codexWorkers: 0,
hasWorkerJobs: false,
error: error instanceof Error ? error.message : String(error),
};
@@ -3276,12 +3279,15 @@ async function activeWorkerSnapshot(
const workers = [];
const errors = [];
let fallbacks = 0;
+ let codexWorkers = 0;
for (const result of results) {
+ codexWorkers += result.codexWorkers;
if (result.error) {
errors.push(`workflow jobs ${result.run.id}: ${result.error}`);
if (isCodexWorkflowFallback(result.run)) {
workers.push(normalizeFallbackWorker(result.run));
fallbacks += 1;
+ codexWorkers += 1;
}
continue;
}
@@ -3290,12 +3296,14 @@ async function activeWorkerSnapshot(
} else if (!result.hasWorkerJobs && isCodexWorkflowFallback(result.run)) {
workers.push(normalizeFallbackWorker(result.run));
fallbacks += 1;
+ codexWorkers += 1;
}
}
for (const run of runs.slice(detailRunLimit)) {
if (!isCodexWorkflowFallback(run)) continue;
workers.push(normalizeFallbackWorker(run));
fallbacks += 1;
+ codexWorkers += 1;
}
workers.sort(
(left, right) =>
@@ -3305,7 +3313,7 @@ async function activeWorkerSnapshot(
);
await attachWorkerTargets(env, workers, errors);
return {
- count: workers.length,
+ count: codexWorkers,
workers,
detailRuns: detailRuns.length,
fallbacks,
@@ -3348,7 +3356,7 @@ async function recentWorkerHealth(
const results = await mapWithConcurrency(completedRuns, fetchConcurrency, async (run) => {
try {
return {
- attempts: (await workflowJobsForRun(env, repo, run.id, github))
+ attempts: (await workflowJobsForRun(env, repo, run.id, github, run))
.filter((job) => isCodexWorkerJob(job))
.map((job) => workerHealthAttempt(run, job))
.filter(Boolean),
@@ -4244,6 +4252,7 @@ async function workflowJobsForRun(
repo,
runId,
github: GithubJsonReader = (path) => githubJson(env, path),
+ run?: WorkflowRunSummary,
) {
const key = `workflow-jobs:${repo}:${runId}`;
const cached = await readStoredJson(env, key);
@@ -4263,7 +4272,9 @@ async function workflowJobsForRun(
break;
}
}
- const hasActiveWorker = jobs.some((job) => isActiveWorkflowJob(job) && isCodexWorkerJob(job));
+ const hasActiveWorker = jobs.some(
+ (job) => isActiveWorkflowJob(job) && isDashboardWorkerJob(job, run),
+ );
await writeStoredJson(
env,
key,
@@ -4288,6 +4299,25 @@ function isCodexWorkerJob(job) {
);
}
+function isExactReviewPublicationJob(job, run?: WorkflowRunSummary) {
+ const name = String(job?.name || "");
+ const steps = Array.isArray(job?.steps) ? job.steps : [];
+ const workflow = `${run?.name || ""} ${run?.display_title || ""}`;
+ return (
+ /publish (?:exact )?review artifacts?/i.test(name) ||
+ (/publish exact review batch/i.test(workflow) && /^publish$/i.test(name)) ||
+ steps.some((step) =>
+ /claim durable exact review publication|claim one durable publication batch|finalize healthy members under a fenced heartbeat|publish event result and apply safe close|complete durable exact review publication|apply review artifacts|publish review artifact action ledger|commit review records/i.test(
+ String(step?.name || ""),
+ ),
+ )
+ );
+}
+
+function isDashboardWorkerJob(job, run?: WorkflowRunSummary) {
+ return isCodexWorkerJob(job) || isExactReviewPublicationJob(job, run);
+}
+
function normalizeWorkerJob(run, job) {
const runItem = classifyRun(run);
const target = workerTargetFromJob(runItem, job.name);
@@ -4310,6 +4340,7 @@ function normalizeWorkerJob(run, job) {
return {
id: job.id,
source: "job",
+ is_codex_worker: isCodexWorkerJob(job),
name: String(job.name || runItem.title || "Codex worker"),
mode,
work_kind: workKind,
@@ -4369,6 +4400,7 @@ function normalizeFallbackWorker(run) {
return {
id: `run-${run.id}`,
source: "workflow-fallback",
+ is_codex_worker: true,
name: item.title || item.workflow || "Codex worker",
mode: item.mode,
work_kind: workerWorkKind(item, ""),
@@ -6212,7 +6244,7 @@ function workflowRunSummary(run) {
function isCodexWorkflowFallback(run) {
const name = `${run?.name || ""} ${run?.display_title || ""}`;
if (
- /repair comment router|clawsweeper_comment|@publish:|publish exact review artifact|exact.review publication|reconcile exact.review lease|sync codex review comments/i.test(
+ /repair comment router|clawsweeper_comment|@publish:|publish (?:exact )?review (?:artifacts?|batch)|exact.review publication|reconcile exact.review lease|sync codex review comments/i.test(
name,
)
) {
@@ -6231,13 +6263,16 @@ function controlPlaneSnapshot(runs) {
};
for (const run of runs) {
const name = `${run?.name || ""} ${run?.display_title || ""}`;
- const lane = /@publish:|publish exact review artifact|exact.review publication/i.test(name)
- ? snapshot.publishers
- : /repair comment router|clawsweeper_comment|sync codex review comments/i.test(name)
- ? snapshot.comment_routers
- : /reconcile exact.review lease/i.test(name)
- ? snapshot.reconcilers
- : null;
+ const lane =
+ /@publish:|publish (?:exact )?review (?:artifacts?|batch)|exact.review publication/i.test(
+ name,
+ )
+ ? snapshot.publishers
+ : /repair comment router|clawsweeper_comment|sync codex review comments/i.test(name)
+ ? snapshot.comment_routers
+ : /reconcile exact.review lease/i.test(name)
+ ? snapshot.reconcilers
+ : null;
if (!lane) continue;
if (run.status === "in_progress") lane.running += 1;
else lane.waiting += 1;
@@ -8994,6 +9029,7 @@ function laneFlowDetails(laneKey, flow) {
}
function renderSystemMap(data) {
const workers = data.workers || [];
+ const codexWorkers = workers.filter(worker => worker.is_codex_worker !== false);
const pipeline = data.pipeline || [];
const fleet = data.fleet || {};
const workerRunIds = new Set(workers.map(worker => String(worker.run_id)));
@@ -9003,7 +9039,7 @@ function renderSystemMap(data) {
const nodes = [
["01 · Intake", fleet.queued_workflow_runs || 0, "Events and scheduled sweeps waiting to start"],
["02 · Plan", planning, "Runs selecting work or expanding a matrix"],
- ["03 · Workers", workers.length, "Codex jobs reviewing, repairing, or assisting"],
+ ["03 · Workers", codexWorkers.length, "Codex jobs reviewing, repairing, or assisting"],
["04 · Apply", applying, "Deterministic comment, close, merge, and publish lanes"],
["05 · Results", closed, (data.recent?.closed_stats?.window_hours || 24) + "h ClawSweeper closes"]
];
@@ -9011,8 +9047,8 @@ function renderSystemMap(data) {
'' + esc(node[0]) + '' + fmt.format(node[1]) + '' + esc(node[2]) + '
'
).join("");
const budget = Math.max(0, fleet.worker_budget || 0);
- const running = workers.filter(worker => worker.status === "in_progress").length;
- const waiting = workers.length - running;
+ const running = codexWorkers.filter(worker => worker.status === "in_progress").length;
+ const waiting = codexWorkers.length - running;
const free = Math.max(0, budget - running - waiting);
const overflow = Math.max(0, running + waiting - budget);
const share = value => budget ? Math.min(100, (value / budget) * 100) : 0;
@@ -9532,7 +9568,7 @@ function renderDashboard(data, note) {
);
const severity = serverHealth?.severity ||
(handoffStatus === "stalled" || operationalStatus === "stalled" ? "red" : needsAttention ? "amber" : "green");
- const workerCount = (data.workers || []).length;
+ const workerCount = (data.workers || []).filter(worker => worker.is_codex_worker !== false).length;
const repoCount = (data.source.target_repositories || []).length;
document.getElementById("hero-dot").className = "hero-dot " + (severity === "green" ? "ok" : severity);
document.getElementById("hero-headline").textContent =
diff --git a/docs/proof/openclaw-bay/README.md b/docs/proof/openclaw-bay/README.md
index db7688f0f1..1cfe11ea09 100644
--- a/docs/proof/openclaw-bay/README.md
+++ b/docs/proof/openclaw-bay/README.md
@@ -10,7 +10,7 @@ The sequence proves:
- visible partial-telemetry diagnostics;
- the Bay timing badge naming its bounded **review trigger → final review** measurement, completed by the command-status update emitted after the durable review summary;
-- a 390px portrait layout that stacks Arriving through Applying vertically, keeps the terminal pools at the waterline, and has no horizontal page overflow;
+- a 390px portrait layout that stacks Arriving through Publishing and Applying vertically, keeps the terminal pools at the waterline, and has no horizontal page overflow;
- advancing crustacean-claw and master-sweeper animations;
- a READY flag followed by a physical forward sweep and landing;
- a changed run ID using the retrigger tunnel and resurfacing path;
@@ -18,7 +18,7 @@ The sequence proves:
- repository filtering;
- the read-only drawer's safe GitHub item, job, and workflow-run links;
- readable overflow controls that open the known queue sample and explicitly explain when aggregate queue IDs are outside the bounded public projection;
-- compact review-admission and result-publication charts with labelled y-axes, exact point hover labels, and cached 6-hour, 24-hour, and 7-day range controls;
+- compact review-admission, result-publication, and State writer charts with labelled y-axes, exact point hover labels, and cached 6-hour, 24-hour, and 7-day range controls;
- lightweight hover/focus explanations on the beach lane signs;
- the local-only tide preview advancing through incoming, crest, backwash, and restored states while preserving terminal keys and count;
- the short static reduced-motion tide cue preserving the same preview state;
diff --git a/docs/proof/openclaw-bay/run-proof.mjs b/docs/proof/openclaw-bay/run-proof.mjs
index a98c1b946a..b0fb07e3a5 100644
--- a/docs/proof/openclaw-bay/run-proof.mjs
+++ b/docs/proof/openclaw-bay/run-proof.mjs
@@ -247,15 +247,30 @@ let healthHistory = Array.from({ length: 73 }, (_, index) => {
completed_total: 318 + index * 3,
},
},
+ state_writer: {
+ collection_ok: true,
+ mode: "batch",
+ tracked_holding: index % 5 === 0 ? 1 : 0,
+ tracked_waiting: 7 + (index % 4),
+ tracked_releasing: 0,
+ accepted_operations_total: 320 + index,
+ state_commits_total: 120 + index,
+ materialized_items_total: 360 + index * 2,
+ contention_timeouts_total: 0,
+ wait_ms: { p50: 1200, p95: 2800, samples: 4 },
+ hold_ms: { p50: 400, p95: 900, samples: 4 },
+ last_successful_materialization_at: at,
+ },
};
});
let healthHistoryFailure = false;
+let stateWriterTerminalFresh = true;
function queueProjection() {
const bayStages = [
{ stage: "arriving", queue_state: "pending" },
{ stage: "setting-up", queue_state: "leased" },
- { stage: "applying", queue_state: "dispatching" },
+ { stage: "publishing", queue_state: "dispatching" },
{ stage: "repairing", queue_state: "pending" },
];
const items = Array.from({ length: 6 }, (_, batch) =>
@@ -290,10 +305,24 @@ function queueProjection() {
leased: { count: 33 },
},
},
+ state_writer: {
+ collection: { status: "fresh" },
+ mode: "batch",
+ live: { tracked_holding: 1, tracked_waiting: 8 },
+ coordinator: { leased: 1, queued: 8 },
+ last_60_minutes: { state_commits: 9, materialized_items: 17 },
+ },
bay_projection: {
sample_limit: 24,
total: 36,
- stages: { arriving: 9, "setting-up": 9, reviewing: 0, applying: 9, repairing: 9 },
+ stages: {
+ arriving: 9,
+ "setting-up": 9,
+ reviewing: 0,
+ publishing: 9,
+ applying: 0,
+ repairing: 9,
+ },
items,
},
};
@@ -320,7 +349,14 @@ function denseFilteredQueueProjection() {
bay_projection: {
sample_limit: 24,
total: 24,
- stages: { arriving: 24, "setting-up": 0, reviewing: 0, applying: 0, repairing: 0 },
+ stages: {
+ arriving: 24,
+ "setting-up": 0,
+ reviewing: 0,
+ publishing: 0,
+ applying: 0,
+ repairing: 0,
+ },
items,
},
};
@@ -406,6 +442,52 @@ const bayRetryLiveSnapshot = {
},
};
proofSnapshots.push(bayRetryTerminalSnapshot, bayRetryLiveSnapshot);
+const batchApplyingProjection = queueProjection();
+const batchApplyingItemKey = "openclaw/openclaw#108003";
+const batchApplyingItems = batchApplyingProjection.bay_projection.items.map((item) =>
+ item.item_key === batchApplyingItemKey
+ ? {
+ ...item,
+ stage: "applying",
+ queue_state: "pending",
+ batch_id: "exact-review-batch:9001",
+ }
+ : item,
+);
+proofSnapshots.push({
+ ...snapshots[0],
+ exact_review_queue: {
+ ...batchApplyingProjection,
+ bay_projection: {
+ ...batchApplyingProjection.bay_projection,
+ stages: {
+ ...batchApplyingProjection.bay_projection.stages,
+ publishing: batchApplyingProjection.bay_projection.stages.publishing - 1,
+ applying: batchApplyingProjection.bay_projection.stages.applying + 1,
+ },
+ items: batchApplyingItems,
+ },
+ },
+});
+const legacyApplyingProjection = queueProjection();
+const { publishing: legacyPublishingCount, ...legacyStages } =
+ legacyApplyingProjection.bay_projection.stages;
+proofSnapshots.push({
+ ...snapshots[0],
+ exact_review_queue: {
+ ...legacyApplyingProjection,
+ bay_projection: {
+ ...legacyApplyingProjection.bay_projection,
+ stages: {
+ ...legacyStages,
+ applying: legacyPublishingCount,
+ },
+ items: legacyApplyingProjection.bay_projection.items.map((item) =>
+ item.stage === "publishing" ? { ...item, stage: "applying" } : item,
+ ),
+ },
+ },
+});
let fixtureIndex = 0;
const requests = [];
@@ -522,11 +604,15 @@ await page.route("**/*", async (route) => {
const request = route.request();
const url = new URL(request.url());
if (url.pathname === "/api/status") {
+ const status = structuredClone(proofSnapshots[fixtureIndex]);
+ if (!stateWriterTerminalFresh) {
+ status.exact_review_queue.state_writer.collection.status = "stale";
+ }
await route.fulfill({
status: 200,
contentType: "application/json; charset=utf-8",
headers: { "cache-control": "no-store", "x-clawsweeper-cache": "synthetic-proof" },
- body: JSON.stringify(proofSnapshots[fixtureIndex]),
+ body: JSON.stringify(status),
});
return;
}
@@ -647,7 +733,14 @@ try {
arriving_queue_samples: await page
.locator('[data-stage="arriving"] [data-item^="queue:"]')
.count(),
+ publishing_queue_samples: await page
+ .locator('[data-stage="publishing"] [data-item^="queue:"]')
+ .count(),
+ applying_queue_samples: await page
+ .locator('[data-stage="applying"] [data-item^="queue:"]')
+ .count(),
queue_header: await page.locator('[data-stage="arriving"] h2').innerText(),
+ publishing_header: await page.locator('[data-stage="publishing"] h2').innerText(),
queue_omission: await page.locator('[data-stage="arriving"] .overflow-note').count(),
queue_omission_label: await page.locator('[data-stage="arriving"] .overflow-note').innerText(),
queue_omission_role: await page
@@ -658,16 +751,20 @@ try {
lane_help: await page.locator('[data-stage="arriving"] .lane-help summary').count(),
};
assertProof(
- "Bay mirrors cached exact-review admission, publication, and handoff telemetry",
- bayControl.cards === 3 &&
+ "Bay mirrors cached exact-review admission, publication, state-writer, and handoff telemetry",
+ bayControl.cards === 4 &&
/Review admission/i.test(bayControl.review) &&
/Result publication/i.test(bayControl.review) &&
+ /State writer/i.test(bayControl.review) &&
/Queue handoff/i.test(bayControl.review) &&
/waiting/.test(bayControl.waiting_hover_label) &&
/\/ hour/.test(bayControl.rate_hover_label) &&
bayControl.queue_items === 1 &&
bayControl.arriving_queue_samples === 6 &&
+ bayControl.publishing_queue_samples === 6 &&
+ bayControl.applying_queue_samples === 0 &&
/^ARRIVING 9/.test(bayControl.queue_header) &&
+ /^PUBLISHING 9/.test(bayControl.publishing_header) &&
bayControl.queue_omission === 1 &&
bayControl.queue_omission_label === "+3 queued IDs not shown" &&
bayControl.queue_omission_role === null &&
@@ -676,6 +773,25 @@ try {
bayControl.lane_help === 1,
bayControl,
);
+
+ stateWriterTerminalFresh = false;
+ await page.evaluate(() => window.__bayProofPoll());
+ await page.waitForFunction(() =>
+ /terminal metrics unavailable/.test(
+ document.querySelector("#bay-control-board")?.textContent || "",
+ ),
+ );
+ const staleStateWriter = await page.locator("#bay-control-board").innerText();
+ assertProof(
+ "Bay does not present stale State writer terminal totals as current",
+ /State writer/i.test(staleStateWriter) &&
+ /unknown items/.test(staleStateWriter) &&
+ /terminal metrics unavailable/.test(staleStateWriter) &&
+ !/17 items/.test(staleStateWriter),
+ { state_writer: staleStateWriter },
+ );
+ stateWriterTerminalFresh = true;
+ await page.evaluate(() => window.__bayProofPoll());
const terminalPoolCounts = {
completed: await page.locator('[data-stage="completed"] .critter').count(),
attention: await page.locator(".pool.attention .critter").count(),
@@ -705,7 +821,7 @@ try {
await capture(
"01a-mini-control-board",
"Mini queue control board",
- "Bay reuses the dashboard’s cached six-hour review-admission, publication, and handoff telemetry; each sparkline point has an exact hover label.",
+ "Bay reuses the dashboard’s cached six-hour review-admission, publication, state-writer, and handoff telemetry; each sparkline point has an exact hover label.",
);
await page.locator('[data-stage="arriving"] .overflow-note').click();
@@ -859,7 +975,11 @@ try {
const failedCollectionAt = originalHistory.at(-2)?.at;
healthHistory = [
originalHistory.at(-3),
- { at: failedCollectionAt, exact_review: { collection_ok: false } },
+ {
+ at: failedCollectionAt,
+ exact_review: { collection_ok: false },
+ state_writer: { collection_ok: false },
+ },
originalHistory.at(-1),
];
const failedCollectionHistory = page.waitForResponse(
@@ -886,6 +1006,52 @@ try {
(failedCollectionPath?.match(/M/g) || []).length === 2,
{ review_pending_path: failedCollectionPath },
);
+ const failedStateWriterPath = await page
+ .locator("#bay-control-board .bay-control-card")
+ .filter({ hasText: "State writer" })
+ .locator(".bay-control-chart svg path")
+ .first()
+ .getAttribute("d");
+ assertProof(
+ "Bay renders failed State writer history collections as a gap",
+ (failedStateWriterPath?.match(/M/g) || []).length === 2,
+ { state_writer_pending_path: failedStateWriterPath },
+ );
+ healthHistory = [
+ originalHistory.at(-3),
+ originalHistory.at(-2),
+ {
+ at: originalHistory.at(-1)?.at,
+ exact_review: originalHistory.at(-1)?.exact_review,
+ state_writer: { collection_ok: false },
+ },
+ ];
+ const currentStateWriterFailure = page.waitForResponse(
+ (response) =>
+ new URL(response.url()).pathname === "/api/health-history" && response.status() === 200,
+ );
+ await page.evaluate(() => {
+ window.__bayProofSetNow(Date.parse("2026-07-11T18:50:05.000Z"));
+ window.__bayProofPoll();
+ });
+ await currentStateWriterFailure;
+ await page.waitForFunction(() =>
+ /history gap · awaiting current sample/i.test(
+ [...document.querySelectorAll("#bay-control-board .bay-control-card")].find((card) =>
+ /State writer/i.test(card.textContent || ""),
+ )?.textContent || "",
+ ),
+ );
+ const failedStateWriterCopy = await page
+ .locator("#bay-control-board .bay-control-card")
+ .filter({ hasText: "State writer" })
+ .innerText();
+ assertProof(
+ "Bay labels a current failed State writer collection as a history gap",
+ /history gap · awaiting current sample/i.test(failedStateWriterCopy) &&
+ !/history stale/i.test(failedStateWriterCopy),
+ { state_writer: failedStateWriterCopy },
+ );
healthHistory = [
originalHistory.at(-3),
@@ -904,7 +1070,7 @@ try {
new URL(response.url()).pathname === "/api/health-history" && response.status() === 200,
);
await page.evaluate(() => {
- window.__bayProofSetNow(Date.parse("2026-07-11T18:49:04.000Z"));
+ window.__bayProofSetNow(Date.parse("2026-07-11T18:51:06.000Z"));
window.__bayProofPoll();
});
await pendingOnlyHistory;
@@ -925,6 +1091,41 @@ try {
);
healthHistory = originalHistory;
+ await page.setViewportSize({ width: 900, height: 700 });
+ await page.waitForFunction(() =>
+ document.getElementById("stage-grid")?.classList.contains("portrait-stack"),
+ );
+ await page.waitForTimeout(140);
+ const compactLandscapeLayout = await page.evaluate(() => {
+ const stages = Array.from(document.querySelectorAll("#stage-grid .stage")).map((stage) => {
+ const rect = stage.getBoundingClientRect();
+ return {
+ left: Math.round(rect.left),
+ top: Math.round(rect.top),
+ bottom: Math.round(rect.bottom),
+ };
+ });
+ const terminal = document.getElementById("terminal-stack")?.getBoundingClientRect();
+ return {
+ scroll_width: document.documentElement.scrollWidth,
+ viewport_width: window.innerWidth,
+ stages,
+ terminal_top: Math.round(terminal?.top || 0),
+ };
+ });
+ assertProof(
+ "intermediate landscape stacks six Bay lanes before they reach terminal pools",
+ compactLandscapeLayout.scroll_width <= compactLandscapeLayout.viewport_width + 1 &&
+ compactLandscapeLayout.stages.length === 6 &&
+ compactLandscapeLayout.stages.every((stage, index, stages) =>
+ index === 0
+ ? stage.left >= 0
+ : Math.abs(stage.left - stages[0].left) <= 2 && stage.top >= stages[index - 1].bottom,
+ ) &&
+ compactLandscapeLayout.terminal_top >= compactLandscapeLayout.stages.at(-1).bottom,
+ compactLandscapeLayout,
+ );
+
await page.setViewportSize({ width: 390, height: 844 });
await page.waitForFunction(() =>
document.getElementById("stage-grid")?.classList.contains("portrait-stack"),
@@ -941,10 +1142,12 @@ try {
});
const terminal = document.getElementById("terminal-stack")?.getBoundingClientRect();
const grid = document.getElementById("stage-grid");
+ const controlBoard = document.getElementById("bay-control-board");
return {
scroll_width: document.documentElement.scrollWidth,
viewport_width: window.innerWidth,
grid_columns: getComputedStyle(grid).gridTemplateColumns,
+ control_columns: getComputedStyle(controlBoard).gridTemplateColumns,
stages,
terminal_top: Math.round(terminal?.top || 0),
};
@@ -952,7 +1155,8 @@ try {
assertProof(
"portrait layout stacks the workflow from sand to waterline without horizontal overflow",
portraitLayout.scroll_width <= portraitLayout.viewport_width + 1 &&
- portraitLayout.stages.length === 5 &&
+ portraitLayout.stages.length === 6 &&
+ portraitLayout.control_columns.trim().split(/\s+/).length === 1 &&
portraitLayout.stages.every((stage, index, stages) =>
index === 0
? stage.left >= 0
@@ -964,7 +1168,7 @@ try {
await capture(
"01b-portrait-workflow",
"Portrait workflow: top to bottom",
- "Phone portrait mode stacks Arriving through Applying vertically, then places the terminal pools at the waterline without horizontal scrolling.",
+ "Phone portrait mode stacks Arriving through Publishing and Applying vertically, then places the terminal pools at the waterline without horizontal scrolling.",
);
await page.setViewportSize({ width: 1900, height: 1000 });
await page.waitForFunction(
@@ -1529,6 +1733,40 @@ try {
"The stale failed card is absent and the same GitHub reference is visible as queued exact-review work in the earlier lane.",
);
+ fixtureIndex = 9;
+ await page.evaluate(async () => {
+ await window.__bayProofPoll();
+ });
+ const batchPublisherLane = page.locator(
+ `[data-stage="applying"] [data-key="${batchApplyingItemKey}"]`,
+ );
+ await batchPublisherLane.waitFor({ state: "visible", timeout: 5_000 });
+ assertProof(
+ "durable batch ownership places known publication items in Applying & writing",
+ (await batchPublisherLane.count()) === 1 &&
+ /APPLYING & WRITING 1/.test(await page.locator('[data-stage="applying"] h2').innerText()),
+ {
+ item_key: batchApplyingItemKey,
+ stage: "applying",
+ batch_id: "exact-review-batch:9001",
+ },
+ );
+
+ fixtureIndex = 10;
+ await page.evaluate(async () => {
+ await window.__bayProofPoll();
+ });
+ const legacyQueuePublisher = page.locator(
+ `[data-stage="publishing"] [data-key="${batchApplyingItemKey}"]`,
+ );
+ await legacyQueuePublisher.waitFor({ state: "visible", timeout: 5_000 });
+ assertProof(
+ "legacy Applying queue records are normalized into Publishing without duplicate Applying cards",
+ (await legacyQueuePublisher.count()) === 1 &&
+ (await page.locator('[data-stage="applying"] [data-item^="queue:"]').count()) === 0,
+ { item_key: batchApplyingItemKey, legacy_stage: "applying", rendered_stage: "publishing" },
+ );
+
const totalStatusGets = requests.filter((request) => request.path === "/api/status").length;
const healthHistoryGets = requests.filter(
(request) => request.path === "/api/health-history",
@@ -1565,7 +1803,7 @@ try {
});
assertProof(
"mini control board caches each selected dashboard history range",
- healthHistoryGets === 10 &&
+ healthHistoryGets === 11 &&
healthHistoryRanges.filter((range) => range === "24h").length === 1 &&
healthHistoryRanges.filter((range) => range === "7d").length === 1,
{ health_history_gets: healthHistoryGets, ranges: healthHistoryRanges },
diff --git a/test/dashboard-worker.test.ts b/test/dashboard-worker.test.ts
index c5c9aa9923..417038c0d2 100644
--- a/test/dashboard-worker.test.ts
+++ b/test/dashboard-worker.test.ts
@@ -2067,7 +2067,8 @@ test("dashboard status reads the exact-review handoff model from the durable que
arriving: 2,
"setting-up": 1,
reviewing: 0,
- applying: 1,
+ publishing: 1,
+ applying: 0,
repairing: 1,
});
assert.deepEqual(
@@ -2079,7 +2080,7 @@ test("dashboard status reads the exact-review handoff model from the durable que
[
{ item_key: "openclaw/gogcli#597", stage: "arriving", queue_state: "pending" },
{ item_key: "openclaw/gogcli#600", stage: "setting-up", queue_state: "leased" },
- { item_key: "openclaw/gogcli#599", stage: "applying", queue_state: "leased" },
+ { item_key: "openclaw/gogcli#599", stage: "publishing", queue_state: "leased" },
{ item_key: "openclaw/gogcli#601", stage: "repairing", queue_state: "pending" },
{ item_key: "openclaw/gogcli#598", stage: "arriving", queue_state: "pending" },
],
@@ -2097,6 +2098,112 @@ test("dashboard status reads the exact-review handoff model from the durable que
assert.equal(await exactReviewQueueStatusSnapshot({}), null);
});
+test("Bay queue projection maps durable batch-owned publication items to Applying", async () => {
+ const storage = new MemoryDurableStorage();
+ const queue = new ExactReviewQueue(
+ { storage },
+ {
+ EXACT_REVIEW_DISPATCH_DEBOUNCE_MS: "0",
+ EXACT_REVIEW_PUBLICATION_BATCHING_ENABLED: "1",
+ EXACT_REVIEW_PUBLICATION_BATCH_SIZE: "1",
+ },
+ );
+ await queue.fetch(
+ buildExactReviewQueueRequest(
+ "bay-batch-applying",
+ 602,
+ "exact_review_artifact_publish",
+ "issue",
+ undefined,
+ exactReviewPublicationOverrides(602, "6020"),
+ ),
+ );
+ const claim = await queue.fetch(
+ new Request("https://clawsweeper-exact-review-queue/publication-batches/claim", {
+ method: "POST",
+ body: JSON.stringify({
+ claim_id: "bay-applying-batch",
+ lease_owner: "bay-applying-owner",
+ max_items: 1,
+ }),
+ }),
+ );
+ assert.equal((await claim.json()).claimed, true);
+
+ const status = await exactReviewQueueStatusSnapshot({
+ EXACT_REVIEW_QUEUE: new MemoryDurableNamespace(queue),
+ });
+
+ assert.ok(status);
+ assert.deepEqual(status.bay_projection.stages, {
+ arriving: 0,
+ "setting-up": 0,
+ reviewing: 0,
+ publishing: 0,
+ applying: 1,
+ repairing: 0,
+ });
+ assert.deepEqual(
+ status.bay_projection.items.map((item) => ({
+ item_key: item.item_key,
+ stage: item.stage,
+ batch_id: item.batch_id,
+ })),
+ [
+ {
+ item_key: "openclaw/gogcli#602",
+ stage: "applying",
+ batch_id: "bay-applying-batch",
+ },
+ ],
+ );
+});
+
+test("Bay queue projection sends parked review and publication work to Repair Cove", async () => {
+ const storage = new MemoryDurableStorage();
+ const queue = new ExactReviewQueue({ storage }, { EXACT_REVIEW_DISPATCH_DEBOUNCE_MS: "0" });
+ await queue.fetch(buildExactReviewQueueRequest("bay-parked-review", 603, "opened"));
+ await queue.fetch(
+ buildExactReviewQueueRequest(
+ "bay-parked-publication",
+ 604,
+ "exact_review_artifact_publish",
+ "issue",
+ undefined,
+ exactReviewPublicationOverrides(604, "6040"),
+ ),
+ );
+ const state = (await storage.get("exact-review-queue")) as {
+ items: Record;
+ };
+ state.items["openclaw/gogcli#603"].state = "parked";
+ state.items["openclaw/gogcli#603"].parkedReason = "review_retry_exhausted";
+ state.items["openclaw/gogcli#604@publish:6040:1"].state = "parked";
+ state.items["openclaw/gogcli#604@publish:6040:1"].parkedReason = "dead_letter_capacity";
+ await storage.put("exact-review-queue", state);
+
+ const status = await exactReviewQueueStatusSnapshot({
+ EXACT_REVIEW_QUEUE: new MemoryDurableNamespace(queue),
+ });
+
+ assert.ok(status);
+ assert.deepEqual(status.bay_projection.stages, {
+ arriving: 0,
+ "setting-up": 0,
+ reviewing: 0,
+ publishing: 0,
+ applying: 0,
+ repairing: 2,
+ });
+ assert.deepEqual(
+ status.bay_projection.items.map((item) => ({ item_key: item.item_key, stage: item.stage })),
+ [
+ { item_key: "openclaw/gogcli#603", stage: "repairing" },
+ { item_key: "openclaw/gogcli#604", stage: "repairing" },
+ ],
+ );
+});
+
test("Bay queue projection applies its public sample cap across all stages", async () => {
const storage = new MemoryDurableStorage();
const queue = new ExactReviewQueue({ storage }, { EXACT_REVIEW_DISPATCH_DEBOUNCE_MS: "0" });
@@ -2139,6 +2246,7 @@ test("Bay queue projection applies its public sample cap across all stages", asy
arriving: 9,
"setting-up": 9,
reviewing: 0,
+ publishing: 0,
applying: 0,
repairing: 9,
});
@@ -2267,16 +2375,29 @@ test("OpenClaw Bay is an unlisted, hardened demo route", async () => {
assert.match(body, /id="bay-control-board"/);
assert.match(body, /Review admission/);
assert.match(body, /Result publication/);
+ assert.match(body, /State writer/);
assert.match(body, /Queue handoff/);
assert.match(body, /function loadBayHistory/);
assert.match(body, /function bayRateSparkline/);
+ assert.match(body, /function bayStateWriterCard/);
+ assert.match(body, /function bayStateWriterHistory/);
+ assert.match(body, /max-width:970px\) and \(orientation:landscape/);
assert.match(body, /net throughput over .*bayRangeLabel/);
assert.match(body, /data-bay-history-range="24h"/);
assert.match(body, /data-bay-history-range="7d"/);
assert.match(body, /bay-control-axis-label/);
assert.match(body, /api\/health-history\?range="\+encodeURIComponent\(range\)/);
assert.match(body, /function expandQueue/);
+ assert.match(body, /function queueProjectionStage/);
assert.match(body, /Repair cove/);
+ assert.match(body, /"publishing":"Publishing"/);
+ assert.match(body, /Waiting to publish the final review/);
+ assert.match(body, /bounded result-publication queue/);
+ assert.match(
+ body,
+ /var hasPublishing=Object\.prototype\.hasOwnProperty\.call\(stages,"publishing"\)/,
+ );
+ assert.match(body, /if\(stage==="applying"&&!hasPublishing\)reported=0/);
assert.match(body, /id="tunnel-layer"/);
assert.match(body, /function startTunnelJourney/);
assert.doesNotMatch(body, /function drawTunnels/);
@@ -2361,17 +2482,76 @@ test("OpenClaw Bay is an unlisted, hardened demo route", async () => {
assert.ok(new Set(chatContext.copies.map((copy) => copy.answer)).size > 1);
assert.ok(chatContext.copies.every((copy) => copy.answer.includes("7m")));
const runChangedSource = body.match(/function runChanged\([^}]+\}/)?.[0];
+ const stageForSource = body.match(/function stageFor\([^]*?return "arriving";\}/)?.[0];
+ const queueProjectionStageSource = body.match(
+ /function queueProjectionStage\([^]*?return STAGES\.indexOf\(stage\)>=0\?stage:"arriving";\}/,
+ )?.[0];
const transitionKindSource = body.match(
/function transitionKind\([^]*?return oldIndex>=0&&nextIndex>oldIndex\?"forward":null;\}/,
)?.[0];
assert.ok(runChangedSource);
+ assert.ok(stageForSource);
+ assert.ok(queueProjectionStageSource);
assert.ok(transitionKindSource);
+ const normalizeQueueStage = new Script(
+ `${queueProjectionStageSource};queueProjectionStage`,
+ ).runInNewContext({
+ STAGES: ["arriving", "setting-up", "reviewing", "publishing", "applying", "repairing"],
+ });
+ assert.equal(normalizeQueueStage("applying", false), "applying");
+ assert.equal(normalizeQueueStage("applying", true), "publishing");
+ assert.equal(normalizeQueueStage("publishing"), "publishing");
+ const classifyBayStage = new Script(`${stageForSource};stageFor`).runInNewContext({
+ STAGES: ["arriving", "setting-up", "reviewing", "publishing", "applying", "repairing"],
+ });
+ const publicationSteps = [{ name: "Apply review artifacts" }];
+ assert.equal(
+ classifyBayStage({
+ status: "in_progress",
+ current_step: "Validate exact review artifact bundle",
+ steps: publicationSteps,
+ }),
+ "publishing",
+ );
+ assert.equal(
+ classifyBayStage({
+ status: "queued",
+ current_step: "Waiting for runner",
+ steps: publicationSteps,
+ }),
+ "publishing",
+ );
+ assert.equal(
+ classifyBayStage({
+ name: "Publish review artifacts",
+ status: "queued",
+ current_step: "Waiting for runner",
+ steps: [],
+ }),
+ "publishing",
+ );
+ assert.equal(
+ classifyBayStage({
+ status: "in_progress",
+ current_step: "Publish review artifact action ledger",
+ steps: publicationSteps,
+ }),
+ "publishing",
+ );
+ assert.equal(
+ classifyBayStage({
+ status: "in_progress",
+ current_step: "Sync selected review comments",
+ steps: publicationSteps,
+ }),
+ "applying",
+ );
const classifyTransition = new Script(
`${runChangedSource};(${transitionKindSource})`,
).runInNewContext({
- MAIN_STAGES: ["arriving", "setting-up", "reviewing", "applying"],
+ MAIN_STAGES: ["arriving", "setting-up", "reviewing", "publishing", "applying"],
});
- for (const stage of ["setting-up", "reviewing", "applying"]) {
+ for (const stage of ["setting-up", "reviewing", "publishing", "applying"]) {
assert.equal(
classifyTransition({ run_id: "old", stage: "reviewing" }, { run_id: "new", stage }),
"retrigger",
@@ -2387,6 +2567,13 @@ test("OpenClaw Bay is an unlisted, hardened demo route", async () => {
assert.equal(
classifyTransition(
{ run_id: "same", stage: "reviewing" },
+ { run_id: "same", stage: "publishing" },
+ ),
+ "forward",
+ );
+ assert.equal(
+ classifyTransition(
+ { run_id: "same", stage: "publishing" },
{ run_id: "same", stage: "applying" },
),
"forward",
@@ -10585,10 +10772,21 @@ test("dashboard hero treats apply and exact-review handoff health as attention",
/target 32 · pressure ceiling 24 after GitHub rate limit/,
);
assert.match(elementFor("exact-review-lanes").innerHTML, /No backlog history in this range/);
- status.workers = Array.from({ length: 130 }, (_, id) => ({ id, status: "in_progress" }));
+ status.workers = [
+ ...Array.from({ length: 128 }, (_, id) => ({
+ id,
+ status: "in_progress",
+ is_codex_worker: true,
+ })),
+ { id: 128, status: "in_progress", is_codex_worker: false },
+ { id: 129, status: "in_progress", is_codex_worker: false },
+ ];
context.renderSystemMap(status);
- assert.match(elementFor("capacity-rail").innerHTML, /130 running/);
- assert.match(elementFor("capacity-rail").innerHTML, /2 over budget/);
+ assert.match(elementFor("capacity-rail").innerHTML, /128 running/);
+ assert.doesNotMatch(elementFor("capacity-rail").innerHTML, /over budget/);
+ status.workers = [{ id: 130, status: "in_progress", is_codex_worker: false }];
+ context.renderDashboard(status, "");
+ assert.match(elementFor("hero-headline").textContent, /0 claw workers sweeping/);
status.workers = [];
status.recent.apply_health.items = [];
@@ -11023,10 +11221,11 @@ test("dashboard preserves issue titles across generated PR repair events", () =>
test("dashboard exposes active worker jobs and their current steps", async () => {
const originalFetch = globalThis.fetch;
const originalCaches = globalThis.caches;
+ const cache = new MemoryCache();
Object.defineProperty(globalThis, "caches", {
configurable: true,
value: {
- default: new MemoryCache(),
+ default: cache,
},
});
const run = {
@@ -11049,6 +11248,16 @@ test("dashboard exposes active worker jobs and their current steps", async () =>
created_at: isoAgo(30_000),
updated_at: isoAgo(5_000),
};
+ const queuedBatchRun = {
+ id: 44,
+ name: "Publish exact review batch",
+ display_title: "Publish exact review batch",
+ status: "queued",
+ conclusion: null,
+ html_url: "https://github.com/openclaw/clawsweeper/actions/runs/44",
+ created_at: isoAgo(20_000),
+ updated_at: isoAgo(2_000),
+ };
let graphqlRequests = 0;
globalThis.fetch = async (input) => {
const url = new URL(String(input));
@@ -11056,11 +11265,11 @@ test("dashboard exposes active worker jobs and their current steps", async () =>
const status = url.searchParams.get("status");
return jsonResponse({
workflow_runs: !status
- ? [run, queuedRun]
+ ? [run, queuedRun, queuedBatchRun]
: status === "in_progress"
? [run]
: status === "queued"
- ? [queuedRun]
+ ? [queuedRun, queuedBatchRun]
: [],
});
}
@@ -11098,9 +11307,46 @@ test("dashboard exposes active worker jobs and their current steps", async () =>
{
id: 4202,
name: "Publish review artifacts",
- status: "queued",
+ status: "in_progress",
conclusion: null,
- steps: [],
+ html_url: "https://github.com/openclaw/clawsweeper/actions/runs/42/job/4202",
+ started_at: isoAgo(60_000),
+ steps: [
+ {
+ number: 1,
+ name: "Apply review artifacts",
+ status: "completed",
+ conclusion: "success",
+ },
+ {
+ number: 2,
+ name: "Publish review artifact action ledger",
+ status: "in_progress",
+ conclusion: null,
+ },
+ ],
+ },
+ {
+ id: 4203,
+ name: "publish",
+ status: "in_progress",
+ conclusion: null,
+ html_url: "https://github.com/openclaw/clawsweeper/actions/runs/42/job/4203",
+ started_at: isoAgo(55_000),
+ steps: [
+ {
+ number: 1,
+ name: "Claim one durable publication batch",
+ status: "completed",
+ conclusion: "success",
+ },
+ {
+ number: 2,
+ name: "Finalize healthy members under a fenced heartbeat",
+ status: "in_progress",
+ conclusion: null,
+ },
+ ],
},
],
});
@@ -11108,6 +11354,21 @@ test("dashboard exposes active worker jobs and their current steps", async () =>
if (url.pathname === "/repos/openclaw/clawsweeper/actions/runs/43/jobs") {
return jsonResponse({ jobs: [] });
}
+ if (url.pathname === "/repos/openclaw/clawsweeper/actions/runs/44/jobs") {
+ return jsonResponse({
+ jobs: [
+ {
+ id: 4401,
+ name: "publish",
+ status: "queued",
+ conclusion: null,
+ html_url: "https://github.com/openclaw/clawsweeper/actions/runs/44/job/4401",
+ started_at: null,
+ steps: [],
+ },
+ ],
+ });
+ }
if (url.pathname === "/graphql") {
graphqlRequests += 1;
return jsonResponse({
@@ -11164,9 +11425,9 @@ test("dashboard exposes active worker jobs and their current steps", async () =>
);
const status = await response.json();
assert.equal(status.fleet.active_codex_jobs, 2);
- assert.equal(status.fleet.worker_detail_runs, 2);
+ assert.equal(status.fleet.worker_detail_runs, 3);
assert.equal(status.fleet.worker_detail_fallbacks, 1);
- assert.equal(status.workers.length, 2);
+ assert.equal(status.workers.length, 5);
assert.equal(status.workers[0].id, 4201);
assert.equal(status.workers[0].name, "Review shard 0 · openclaw/openclaw#92521,92522");
assert.equal(status.workers[0].repository, "openclaw/openclaw");
@@ -11191,10 +11452,33 @@ test("dashboard exposes active worker jobs and their current steps", async () =>
type: "pull_request",
},
]);
- assert.equal(status.workers[1].id, "run-43");
- assert.equal(status.workers[1].source, "workflow-fallback");
- assert.equal(status.workers[1].current_step, "reviewing");
- assert.equal(status.workers[1].target_items[0].title, "Queued terminal resize follow-up");
+ assert.equal(status.workers[1].id, 4202);
+ assert.equal(status.workers[1].name, "Publish review artifacts");
+ assert.equal(status.workers[1].is_codex_worker, false);
+ assert.equal(status.workers[1].item_number, 92521);
+ assert.equal(status.workers[1].current_step, "Publish review artifact action ledger");
+ assert.equal(status.workers[1].steps[0].name, "Apply review artifacts");
+ assert.equal(status.workers[2].id, 4203);
+ assert.equal(status.workers[2].name, "publish");
+ assert.equal(status.workers[2].is_codex_worker, false);
+ assert.equal(
+ status.workers[2].current_step,
+ "Finalize healthy members under a fenced heartbeat",
+ );
+ const cachedPublisherJobs = await cache.match(
+ new Request("https://clawsweeper.internal/store/workflow-jobs%3Aopenclaw%2Fclawsweeper%3A42"),
+ );
+ assert.equal(cachedPublisherJobs?.headers.get("cache-control"), "public, max-age=60");
+ assert.equal(status.workers[3].id, "run-43");
+ assert.equal(status.workers[3].source, "workflow-fallback");
+ assert.equal(status.workers[3].current_step, "reviewing");
+ assert.equal(status.workers[3].target_items[0].title, "Queued terminal resize follow-up");
+ const queuedBatchPublisher = status.workers.find((entry) => entry.id === 4401);
+ assert.ok(queuedBatchPublisher);
+ assert.equal(queuedBatchPublisher.name, "publish");
+ assert.equal(queuedBatchPublisher.is_codex_worker, false);
+ assert.equal(queuedBatchPublisher.workflow_title, "Publish exact review batch");
+ assert.equal(queuedBatchPublisher.current_step, "Waiting for runner");
const cachedResponse = await worker.fetch(
new Request("https://clawsweeper.openclaw.ai/api/status"),
diff --git a/test/exact-review-publication-batches.test.ts b/test/exact-review-publication-batches.test.ts
index 686f226afc..ed88235cb6 100644
--- a/test/exact-review-publication-batches.test.ts
+++ b/test/exact-review-publication-batches.test.ts
@@ -133,6 +133,10 @@ test("publication batches atomically select ready items without duplicate active
assert.equal(second, null);
assert.equal(first?.configuredBatchSize, 2);
assert.deepEqual(batches.activeLeaseSnapshot(1_500), {
+ items: candidates.slice(0, 2).map((candidate) => ({
+ itemKey: candidate.itemKey,
+ batchId: "batch-1",
+ })),
itemKeys: candidates.slice(0, 2).map((item) => item.itemKey),
activeBatches: 1,
nextLeaseExpiresAt: 2_000,
@@ -183,6 +187,16 @@ test("publication batches allow a bounded number of disjoint active owners", ()
);
assert.equal(third, null);
assert.deepEqual(batches.activeLeaseSnapshot(1_500), {
+ items: [
+ ...candidates.slice(0, 2).map((candidate) => ({
+ itemKey: candidate.itemKey,
+ batchId: "parallel-1",
+ })),
+ ...candidates.slice(2).map((candidate) => ({
+ itemKey: candidate.itemKey,
+ batchId: "parallel-2",
+ })),
+ ],
itemKeys: candidates.map((item) => item.itemKey),
activeBatches: 2,
nextLeaseExpiresAt: 2_000,