Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 23 additions & 14 deletions src/agent-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3830,23 +3830,32 @@ export class AgentEngine {
});
this.registry.set(agentId, updated);

if (agent.spawn_depth === 0) {
// Root agent: send /compact
const compactRoute = await this.resolveAgentIoRoute(agentId);
await this.client.send(compactRoute.surface_id, "/compact", {
workspace: compactRoute.workspace_id ?? undefined,
...this.stableSurfaceWriteOptions(compactRoute.surface_uuid),
});
const returnRoute = await this.resolveAgentIoRoute(agentId);
await this.client.sendKey(returnRoute.surface_id, "return", {
workspace: returnRoute.workspace_id ?? undefined,
...this.stableSurfaceWriteOptions(returnRoute.surface_uuid),
});
} else {
try {
await this.client.log(
`context-limit: depth ${agent.spawn_depth} agent ${agent.repo} degraded; leaving pane running for orchestrator decision`,
`context-limit: depth ${agent.spawn_depth} agent ${agent.repo} degraded at ${contextPct}%; leaving pane running for orchestrator decision`,
{ level: "warning", source: "cmuxlayer" },
);
} catch {
// Logging is advisory; a root-agent nudge must still be attempted.
}

if (agent.spawn_depth === 0) {
const nudgeRoute = await this.resolveAgentIoRoute(agentId);
await this.client.send(
nudgeRoute.surface_id,
`[cmuxlayer] context at ${contextPct}% — checkpoint at-risk work and /compact when safe`,
{
workspace: nudgeRoute.workspace_id ?? undefined,
...this.stableSurfaceWriteOptions(nudgeRoute.surface_uuid),
beforeMutation: async () => {
Comment on lines +3844 to +3850

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retry the nudge after delivery failures

When this send or its new stale-route gate throws—for example because the surface moves during the guarded mutation—the surrounding catch suppresses the error after the record has already been persisted as quality: "degraded". Every later sweep then fails the agent.quality !== "degraded" condition, so the nudge is never attempted on the corrected route; delivery state should be tracked separately or the quality transition rolled back when the nudge was not delivered.

Useful? React with 👍 / 👎.

await this.resolveUnchangedAgentIoRoute(
agentId,
nudgeRoute,
"context-limit nudge",
);
Comment on lines +3850 to +3855

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Execute the stale-route gate in the app-server adapter

When cmuxlayer-app-server runs this sweep and a surface ref is rebound after nudgeRoute is resolved, this callback does not protect the write: the adapter at src/app-server-runtime.ts:283-287 neither invokes beforeMutation nor locks by stableSurfaceIdentity, and instead forwards both engine-only fields to CmuxClient.send, which ignores them. It consequently locks and writes through the old mutable ref, potentially placing the nudge in an unrelated pane; the adapter must execute the gate and use the UUID lock as the main server connector does.

AGENTS.md reference: AGENTS.md:L20-L25

Useful? React with 👍 / 👎.

},
},
);
}
}
} catch {
Expand Down
40 changes: 10 additions & 30 deletions tests/agent-engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3462,9 +3462,8 @@ describe("AgentEngine", () => {
expect(mockClient.readScreen).toHaveBeenCalledTimes(1);
});

it("auto-compacts with workspace scope and re-resolves before Return", async () => {
it("sends an unsubmitted context nudge with workspace scope", async () => {
const stableUuid = "11111111-2222-4333-8444-555555555555";
const recycledUuid = "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee";
const record = makeRecord({
agent_id: "agent-auto-compact-route",
state: "ready",
Expand All @@ -3490,41 +3489,22 @@ describe("AgentEngine", () => {
lines: 20,
scrollback_used: false,
});
(mockClient.send as ReturnType<typeof vi.fn>).mockImplementation(
async (_surface: string, text: string) => {
if (text !== "/compact") return;
liveSurfaces = [
{
...makeSurface("surface:compact-old"),
id: recycledUuid,
workspace_ref: "workspace:compact-old",
},
{
...makeSurface("surface:compact-final"),
id: stableUuid,
workspace_ref: "workspace:compact-final",
},
];
},
);

await engine.runSweep();

expect(mockClient.send).toHaveBeenCalledWith(
"surface:compact-old",
"/compact",
{ workspace: "workspace:compact-old" },
);
expect(mockClient.sendKey).toHaveBeenCalledWith(
"surface:compact-final",
"return",
{ workspace: "workspace:compact-final" },
"[cmuxlayer] context at 95% — checkpoint at-risk work and /compact when safe",
expect.objectContaining({
workspace: "workspace:compact-old",
beforeMutation: expect.any(Function),
}),
);
expect(mockClient.sendKey).not.toHaveBeenCalledWith(
"surface:compact-old",
"return",
expect(mockClient.send).not.toHaveBeenCalledWith(
expect.anything(),
"/compact",
expect.anything(),
);
expect(mockClient.sendKey).not.toHaveBeenCalled();
});

it("respawns a crashed agent with its captured session id when crash recovery is enabled", async () => {
Expand Down
77 changes: 60 additions & 17 deletions tests/quality-tracking.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* TDD tests for Task 19 — Quality Tracking.
* Tests parseContextPercent, quality field, /compact at 80%, warn for depth>0.
* Tests parseContextPercent and quality warnings/nudges at high context usage.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { mkdirSync, rmSync } from "node:fs";
Expand Down Expand Up @@ -191,7 +191,7 @@ describe("Quality Tracking (sweep)", () => {
expect(agent!.quality).toBe("unknown");
});

it("at 80% context, depth-0 agent sends /compact command", async () => {
it("at 80% context, depth-0 agent is degraded and nudged without submitting /compact", async () => {
stateMgr.writeState(
makeRecord({
agent_id: "a1",
Expand All @@ -213,16 +213,60 @@ describe("Quality Tracking (sweep)", () => {

await engine.runSweep();

// Should send /compact + return
expect(mockClient.send).toHaveBeenCalledWith("s:1", "/compact", {
workspace: "workspace:quality",
});
expect(mockClient.sendKey).toHaveBeenCalledWith("s:1", "return", {
workspace: "workspace:quality",
expect(engine.getAgentState("a1")?.quality).toBe("degraded");
expect(mockClient.log).toHaveBeenCalledWith(
expect.stringContaining("context-limit: depth 0 agent brainlayer degraded at 80%"),
{ level: "warning", source: "cmuxlayer" },
);
expect(mockClient.send).toHaveBeenCalledWith(
"s:1",
"[cmuxlayer] context at 80% — checkpoint at-risk work and /compact when safe",
expect.objectContaining({
workspace: "workspace:quality",
beforeMutation: expect.any(Function),
}),
);
expect(mockClient.send).not.toHaveBeenCalledWith(
"s:1",
"/compact",
expect.anything(),
);
expect(mockClient.sendKey).not.toHaveBeenCalled();
});

it("still nudges a depth-0 agent when warning logging fails", async () => {
stateMgr.writeState(
makeRecord({
agent_id: "log-failure",
state: "working",
surface_id: "s:log-failure",
spawn_depth: 0,
}),
);
liveSurfaces = [makeSurface("s:log-failure")];
await engine.getRegistry().reconstitute();
(mockClient.readScreen as ReturnType<typeof vi.fn>).mockResolvedValue({
surface: "s:log-failure",
text: "gpt-5.5 · 5% left · ~/Gits/cmuxlayer\nWorking (1m • esc to interrupt)",
lines: 5,
scrollback_used: false,
});
(mockClient.log as ReturnType<typeof vi.fn>).mockRejectedValue(
new Error("log unavailable"),
);

await engine.runSweep();

expect(engine.getAgentState("log-failure")?.quality).toBe("degraded");
expect(mockClient.send).toHaveBeenCalledWith(
"s:log-failure",
"[cmuxlayer] context at 95% — checkpoint at-risk work and /compact when safe",
expect.objectContaining({ workspace: "workspace:quality" }),
);
expect(mockClient.sendKey).not.toHaveBeenCalled();
});

it("auto-compact follows a stable UUID after its cached surface ref is recycled", async () => {
it("context nudge follows a stable UUID after its cached surface ref is recycled", async () => {
const stableUuid = "11111111-2222-4333-8444-555555555555";
stateMgr.writeState(
makeRecord({
Expand Down Expand Up @@ -262,19 +306,18 @@ describe("Quality Tracking (sweep)", () => {

expect(mockClient.send).toHaveBeenCalledWith(
"surface:new",
"/compact",
{ workspace: "workspace:quality" },
);
expect(mockClient.sendKey).toHaveBeenCalledWith(
"surface:new",
"return",
{ workspace: "workspace:quality" },
"[cmuxlayer] context at 95% — checkpoint at-risk work and /compact when safe",
expect.objectContaining({
workspace: "workspace:quality",
beforeMutation: expect.any(Function),
}),
);
expect(mockClient.send).not.toHaveBeenCalledWith(
"surface:old",
expect.anything(),
"/compact",
expect.anything(),
);
expect(mockClient.sendKey).not.toHaveBeenCalled();
});

it("at 80% context, depth-1 agent is warned but not killed", async () => {
Expand Down
Loading