mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-10 22:20:27 +00:00
Lead cadence: mandatory check-in timer, harness backstop, sleeping strip
The lead must end active turns with a sleep_for (3-5m, stretch when quiet); a 10-minute backstop wakes a lead that forgot while work is in flight. Sleeping sessions show a strip with the next wake time and an Ask-for-a-status action — a scheduled agent never reads as a dead one.
This commit is contained in:
@@ -900,6 +900,9 @@ export async function mockApi(page: import("@playwright/test").Page) {
|
||||
chat_enabled: !!msg.enable_chat,
|
||||
chat_unread: msg.enable_chat ? 1 : 0,
|
||||
};
|
||||
// The lead sets its check-in timer after staffing — it shows as sleeping.
|
||||
lead.liveness = "sleeping";
|
||||
lead.sleeping_until = new Date(Date.now() + 4 * 60_000).toISOString();
|
||||
for (const [actor, persona, status, item] of [
|
||||
["nia", "swe-worker", "in_progress", "#1 in progress"],
|
||||
["webb", "design-worker", "idle", "idle"],
|
||||
|
||||
@@ -86,6 +86,20 @@ test("enabling chat at the gate adds the # team chat row; posting works with men
|
||||
await expect(page.getByTestId("teamchat-view")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("a sleeping lead shows the strip; Ask for a status wakes it", async ({ page }) => {
|
||||
await proposeTeam(page);
|
||||
await page.getByTestId("teamreq-approve").click();
|
||||
await expect(page.getByText(/Team created/)).toBeVisible();
|
||||
// open the lead's session — it set a check-in timer, so it's sleeping
|
||||
await page.getByText("Build the statements page").click();
|
||||
const strip = page.getByTestId("sleep-strip");
|
||||
await expect(strip).toBeVisible({ timeout: 12_000 });
|
||||
await expect(strip).toContainText("Sleeping until");
|
||||
await expect(strip).toContainText("while the team works");
|
||||
await page.getByTestId("sleep-status-btn").click();
|
||||
await expect(page.getByText(/Echo: Quick status check/)).toBeVisible();
|
||||
});
|
||||
|
||||
test("with chat declined at the gate, no chat row renders", async ({ page }) => {
|
||||
await proposeTeam(page);
|
||||
await page.getByTestId("teamreq-approve").click();
|
||||
|
||||
@@ -1918,6 +1918,34 @@ export function App() {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* A scheduled agent must never read as a dead one: while a self-wake is
|
||||
pending and no turn is running, say so and offer the obvious action. */}
|
||||
{activeInfo?.liveness === "sleeping" && !running && (
|
||||
<div className="sleep-strip" data-testid="sleep-strip">
|
||||
<span className="sleep-dot" />
|
||||
<span className="sleep-text">
|
||||
Sleeping
|
||||
{activeInfo.sleeping_until
|
||||
? ` until ${new Date(activeInfo.sleeping_until).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" })}`
|
||||
: ""}
|
||||
{activeInfo.team?.role === "lead"
|
||||
? " while the team works — it also wakes on board activity."
|
||||
: " — it wakes on its trigger."}{" "}
|
||||
Talk to it anytime.
|
||||
</span>
|
||||
<button
|
||||
className="btn sm"
|
||||
data-testid="sleep-status-btn"
|
||||
onClick={() =>
|
||||
send(
|
||||
"Quick status check, please — what's moving, what's blocked, and does anything need me?",
|
||||
)
|
||||
}
|
||||
>
|
||||
Ask for a status
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<Composer
|
||||
mode={mode}
|
||||
model={model}
|
||||
|
||||
@@ -1781,3 +1781,14 @@ html[data-platform="linux"] ::-webkit-scrollbar-thumb:hover { background-color:
|
||||
.chat-view { position: absolute; inset: 0; z-index: 40; background: var(--paper); display: flex; flex-direction: column; }
|
||||
.chat-view-head { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-bottom: 1px solid var(--line); background: var(--panel); }
|
||||
.chat-view-body { flex: 1; display: flex; flex-direction: column; min-height: 0; max-width: 860px; width: 100%; margin: 0 auto; }
|
||||
|
||||
/* Sleeping strip — a scheduled agent never reads as a dead one. */
|
||||
.sleep-strip {
|
||||
max-width: 760px; margin: 0 auto 8px; display: flex; align-items: center; gap: 9px;
|
||||
padding: 8px 12px; border: 1px solid var(--line); border-radius: 10px;
|
||||
background: var(--panel); font-size: 12.5px; color: var(--muted);
|
||||
}
|
||||
.sleep-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--warn-ink); opacity: 0.7; flex-shrink: 0; animation: sleep-pulse 2.4s ease-in-out infinite; }
|
||||
@keyframes sleep-pulse { 0%, 100% { opacity: 0.35; } 50% { opacity: 0.85; } }
|
||||
.sleep-text { flex: 1; }
|
||||
.sleep-strip .btn.sm { padding: 4px 10px; font-size: 12px; }
|
||||
|
||||
@@ -78,6 +78,8 @@ export interface SessionInfo {
|
||||
attention?: number;
|
||||
// working = in-flight turn; sleeping = a self-wake is pending; idle = neither. A count-less dot.
|
||||
liveness?: "working" | "sleeping" | "idle";
|
||||
// When sleeping: the next timer fire (ISO) — drives the "sleeping until…" strip.
|
||||
sleeping_until?: string | null;
|
||||
// Channels this session listens to (inbound subscriptions).
|
||||
subscriptions?: string[];
|
||||
// §31: set when the session was spawned by a platform mention rather than the user —
|
||||
|
||||
Reference in New Issue
Block a user