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
1 change: 1 addition & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- Fixed compaction retaining runtime resources after an explicitly deleted subagent had a transient cleanup failure.
- Fixed long-running thinking timers to display hours and days instead of unbounded minutes.
- Fixed overlapping daemon snapshot catch-ups closing healthy workers and preventing new sessions from starting.
- Changed daemon and RPC session state to report literal queued actions separately from active scheduler work.

## [0.4.0] - 2026-08-01

Expand Down
4 changes: 2 additions & 2 deletions packages/coding-agent/docs/json.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ Events are defined in [`AgentSessionEvent`](../src/core/agent-session.ts):
```typescript
type AgentSessionEvent =
| AgentEvent
| { type: "queue_update"; steering: readonly string[]; followUp: readonly string[] }
| { type: "session_action_update"; actions: SessionActionSnapshot }
| { type: "compaction_start"; reason: "manual" | "threshold" | "overflow" }
| { type: "compaction_end"; reason: "manual" | "threshold" | "overflow"; result: CompactionResult | undefined; aborted: boolean; willRetry: boolean; errorMessage?: string }
| { type: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }
| { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string };
```

`queue_update` emits the full pending steering and follow-up queues whenever they change. `compaction_start` and `compaction_end` cover both manual and automatic compaction.
`session_action_update` emits literal queued actions separately from active scheduler work whenever either projection changes. `compaction_start` and `compaction_end` cover both manual and automatic compaction.

Base events from [`AgentEvent`](../../agent/src/types.ts):

Expand Down
23 changes: 16 additions & 7 deletions packages/coding-agent/docs/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,12 @@ Response:
"sessionName": "my-feature-work",
"autoCompactionEnabled": true,
"messageCount": 5,
"pendingMessageCount": 0
"unfinishedActionCount": 0,
"sessionActions": {
"queuedCount": 0,
"steering": [],
"followUps": []
}
}
}
```
Expand Down Expand Up @@ -795,7 +800,7 @@ Events are streamed to stdout as JSON lines during agent operation. Events do NO
| `tool_execution_start` | Tool begins execution |
| `tool_execution_update` | Tool execution progress (streaming output) |
| `tool_execution_end` | Tool completes |
| `queue_update` | Pending steering/follow-up queue changed |
| `session_action_update` | Pending steering/follow-up queue changed |
| `compaction_start` | Compaction begins |
| `compaction_end` | Compaction completes |
| `auto_retry_start` | Auto-retry begins (after transient error) |
Expand Down Expand Up @@ -933,15 +938,19 @@ When complete:

Use `toolCallId` to correlate events. The `partialResult` in `tool_execution_update` contains the accumulated output so far (not just the delta), allowing clients to simply replace their display on each update.

### queue_update
### session_action_update

Emitted whenever the pending steering or follow-up queue changes.
Emitted whenever literal queued actions or the active scheduler action changes.

```json
{
"type": "queue_update",
"steering": ["Focus on error handling"],
"followUp": ["After that, summarize the result"]
"type": "session_action_update",
"actions": {
"queuedCount": 2,
"steering": ["Focus on error handling"],
"followUps": ["After that, summarize the result"],
"active": { "kind": "session_command", "phase": "running", "label": "/compact" }
}
}
```

Expand Down
4 changes: 2 additions & 2 deletions packages/coding-agent/docs/sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,8 @@ session.subscribe((event) => {
break;

// Session events (queue, compaction, retry)
case "queue_update":
console.log(event.steering, event.followUp);
case "session_action_update":
console.log(event.actions.steering, event.actions.followUps);
break;
case "compaction_start":
case "compaction_end":
Expand Down
4 changes: 2 additions & 2 deletions packages/coding-agent/examples/sdk/13-session-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ async function bindSession() {
const session = runtime.session;
await session.bindExtensions({});
unsubscribe = session.subscribe((event) => {
if (event.type === "queue_update") {
console.log("Queued:", event.steering.length + event.followUp.length);
if (event.type === "session_action_update") {
console.log("Queued:", event.actions.steering.length + event.actions.followUps.length);
}
});
return session;
Expand Down
18 changes: 2 additions & 16 deletions packages/coding-agent/src/cli-main.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { enableCompileCache } from "node:module";
import { maybeStartDaemonEarly, shouldUseLegacyOwnedSessionWorkerFrontend } from "./cli/daemon-launch.js";
import { maybeStartDaemonEarly } from "./cli/daemon-launch.js";
import {
classifyOwnedSessionWorkerInvocation,
closeOwnedSessionWorkerOwnerWatch,
installOwnedSessionWorkerOwnerWatch,
isOwnedSessionWorkerProcess,
maybeRunOwnedSessionWorkerFrontend,
} from "./cli/owned-session-worker.js";
import { APP_NAME } from "./config.js";
import { defaultDaemonSocketPath } from "./modes/daemon/daemon-socket.js";

export async function runCli(): Promise<void> {
try {
Expand All @@ -24,12 +22,7 @@ export async function runCli(): Promise<void> {
installOwnedSessionWorkerOwnerWatch();

const args = process.argv.slice(2);
const ownedWorkerProfile = classifyOwnedSessionWorkerInvocation(args, process.stdin.isTTY);
const useLegacyOwnedWorker =
process.env.PRIME_AGENT_INTERNAL_LEGACY_OWNED_WORKER_FRONTEND !== "1" &&
ownedWorkerProfile !== undefined &&
(await shouldUseLegacyOwnedSessionWorkerFrontendFromArgs(args));
const handledByOwnedWorker = await maybeRunOwnedSessionWorkerFrontend(args, useLegacyOwnedWorker);
const handledByOwnedWorker = await maybeRunOwnedSessionWorkerFrontend(args);
if (!handledByOwnedWorker) {
if (!isOwnedSessionWorkerProcess()) {
// Boot a cold daemon concurrently with this process's heavy imports.
Expand All @@ -51,10 +44,3 @@ export async function runCli(): Promise<void> {
}
}
}

async function shouldUseLegacyOwnedSessionWorkerFrontendFromArgs(args: readonly string[]): Promise<boolean> {
const socketIndex = args.indexOf("--daemon-socket");
const socketPath =
socketIndex !== -1 && args[socketIndex + 1] ? (args[socketIndex + 1] as string) : defaultDaemonSocketPath();
return shouldUseLegacyOwnedSessionWorkerFrontend(socketPath);
}
16 changes: 12 additions & 4 deletions packages/coding-agent/src/cli/daemon-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1451,10 +1451,12 @@ class DaemonAttachTerminal {
case "tool_execution_end":
this.writeLine(chalk.dim(`Tool ${event.isError ? "failed" : "finished"}: ${event.toolName}`));
return;
case "queue_update":
if (event.steering.length > 0 || event.followUp.length > 0) {
case "session_action_update":
if (event.actions.queuedCount > 0) {
this.writeLine(
chalk.dim(`Queued: ${event.steering.length} steering, ${event.followUp.length} follow-up`),
chalk.dim(
`Queued: ${event.actions.steering.length} steering, ${event.actions.followUps.length} follow-up`,
),
);
}
return;
Expand Down Expand Up @@ -1648,11 +1650,17 @@ function isSessionSummary(value: unknown): value is SessionSummary {
typeof candidate.cwd === "string" &&
typeof candidate.lifecycle === "string" &&
typeof candidate.activity === "string" &&
typeof candidate.isSessionActive === "boolean" &&
typeof candidate.isStreaming === "boolean" &&
typeof candidate.isCompacting === "boolean" &&
typeof candidate.attachedClients === "number" &&
typeof candidate.messageCount === "number" &&
typeof candidate.pendingMessageCount === "number"
(candidate.unfinishedActionCount === undefined || typeof candidate.unfinishedActionCount === "number") &&
typeof candidate.sessionActions === "object" &&
candidate.sessionActions !== null &&
typeof candidate.sessionActions.queuedCount === "number" &&
Array.isArray(candidate.sessionActions.steering) &&
Array.isArray(candidate.sessionActions.followUps)
);
}

Expand Down
44 changes: 6 additions & 38 deletions packages/coding-agent/src/cli/daemon-launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ async function canConnectToDaemon(socketPath: string, timeoutMs: number): Promis
type DaemonVersionProbe =
| { status: "absent" }
| { status: "current"; hello: DaemonHello }
| { status: "stale"; hello?: DaemonHello; compatible: boolean };
| { status: "stale"; hello?: DaemonHello };

/** Connect to a running daemon and check whether it matches this client's protocol and app version. */
export async function probeDaemonVersion(socketPath: string): Promise<DaemonVersionProbe> {
Expand Down Expand Up @@ -96,15 +96,11 @@ export async function probeDaemonVersion(socketPath: string): Promise<DaemonVers
if (current) {
return { status: "current", hello };
}
return {
status: "stale",
hello,
compatible: hello.protocol.version === 3,
};
return { status: "stale", hello };
} catch {
// Connected but no recognizable greeting: assume a stale daemon.
logDaemonLaunch(`running daemon on ${socketPath} sent no recognizable hello; treating as stale`);
return { status: "stale", compatible: false };
return { status: "stale" };
} finally {
client.close();
}
Expand All @@ -121,11 +117,7 @@ async function queryActiveDaemonSessions(
client: DaemonClient,
options: { includeClientOwned?: boolean } = {},
): Promise<{ sessions: SessionSummary[]; busyClientOwnedSessionCount: number }> {
const hello = await client.waitForHello(2000).catch(() => undefined);
const response =
hello && hello.protocol.version < DAEMON_PROTOCOL_VERSION
? await client.requestLegacy({ type: "list", includeClientOwned: options.includeClientOwned })
: await client.request({ type: "list", includeClientOwned: options.includeClientOwned });
const response = await client.request({ type: "list", includeClientOwned: options.includeClientOwned });
if (!response.success) {
throw new Error(response.error);
}
Expand Down Expand Up @@ -249,11 +241,7 @@ export async function shutdownConnectedDaemonAndWait(
let shutdownAccepted = false;
const expectedIdentity = processIdentityFromDaemonHello(hello);
try {
const request =
hello && hello.protocol.version < DAEMON_PROTOCOL_VERSION
? client.requestLegacy.bind(client)
: client.request.bind(client);
const response = await request({ type: "shutdown" }).catch(() => undefined);
const response = await client.request({ type: "shutdown" }).catch(() => undefined);
shutdownAccepted = response?.success === true;
} catch {
// A connect failure isn't treated as "gone"; waitForDaemonGone is the source of truth.
Expand Down Expand Up @@ -309,20 +297,6 @@ export async function probeRunningDaemonSessions(socketPath: string): Promise<Ru
}
}

export async function shouldUseLegacyOwnedSessionWorkerFrontend(socketPath: string): Promise<boolean> {
const version = await probeDaemonVersion(socketPath);
if (version.status !== "stale") {
return false;
}
const running = await probeRunningDaemonSessions(socketPath);
return (
running.reachable &&
(running.activeSessions === undefined ||
(running.busyClientOwnedSessionCount ?? 0) > 0 ||
running.activeSessions.some((summary) => isSessionBusy(summary)))
);
}

// Idle-but-loaded sessions reload from disk on the fresh daemon, so only a busy
// session blocks replacing a stale daemon.
async function shutdownStaleDaemonIfNotBusy(socketPath: string): Promise<boolean> {
Expand Down Expand Up @@ -368,13 +342,7 @@ async function ensureDaemonRunning(socketPath: string, spawnCwd?: string): Promi
}
if (probe.status === "stale") {
const stopped = await shutdownStaleDaemonIfNotBusy(socketPath);
if (!stopped) {
if (probe.compatible) {
logDaemonLaunch(`using compatible legacy daemon on ${socketPath} while busy work completes`);
return;
}
throw new StaleDaemonError(socketPath, probe.hello);
}
if (!stopped) throw new StaleDaemonError(socketPath, probe.hello);
}

const entrypoint = process.argv[1];
Expand Down
8 changes: 4 additions & 4 deletions packages/coding-agent/src/core/agent-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export interface AgentSessionMessageSender extends Partial<AgentSessionMessageEn
export interface AgentSessionMessageAgentSummary extends AgentSessionMessageEndpoint {
cwd: string;
isStreaming: boolean;
pendingMessageCount: number;
unfinishedActionCount: number;
parentActiveSessionId?: string;
rlmChildId?: string;
}
Expand Down Expand Up @@ -134,12 +134,12 @@ export function assertDirectAgentMessageTarget(target: string): string {
}

export function assertAgentMessageQueueCapacity(
pendingMessageCount: number,
unfinishedActionCount: number,
maxPending = DEFAULT_AGENT_MESSAGE_MAX_PENDING_PER_SESSION,
): void {
if (pendingMessageCount >= maxPending) {
if (unfinishedActionCount >= maxPending) {
throw new Error(
`Target session has too many pending messages: ${pendingMessageCount} pending, limit is ${maxPending}`,
`Target session has too many pending messages: ${unfinishedActionCount} unfinished, limit is ${maxPending}`,
);
}
}
Expand Down
3 changes: 2 additions & 1 deletion packages/coding-agent/src/core/agent-observe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export interface AgentObserveAgentSummary {
isCompacting: boolean;
attachedClients: number;
messageCount: number;
pendingMessageCount: number;
queuedCount: number;
isSessionActive: boolean;
parentActiveSessionId?: string;
parentSessionId?: string;
rlmChildId?: string;
Expand Down
Loading