Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
7 changes: 7 additions & 0 deletions docs/developers/qwen-serve-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,11 @@ Response shape:
"inbound": { "count": 0, "totalBytes": 0, "maxBytes": 0 },
"outbound": { "count": 0, "totalBytes": 0, "maxBytes": 0 }
}
},
"activity": {
"activePrompts": 0,
"lastActivityAt": null,
"idleSinceMs": null
}
}
}
Expand All @@ -346,6 +351,8 @@ mounted, `/daemon/status` may report `daemon_runtime_starting`; if the async
runtime mount fails, it reports `daemon_runtime_failed` while non-status
runtime routes return `503`.

`runtime.activity` reports daemon-wide prompt activity. `activePrompts` counts sessions with an in-flight prompt. `lastActivityAt` is the ISO 8601 timestamp of the last prompt start/end or session spawn; `null` when the daemon has never processed any activity since boot. `idleSinceMs` is computed from `lastActivityAt` at response generation time.
Comment thread
doudouOUC marked this conversation as resolved.
Resolved

`runtime.channel.live` reports the ACP bridge channel inside the daemon. It is
not the channel-adapter worker. Daemon-managed channels use
`runtime.channelWorker`, whose `state` is one of `disabled`, `starting`,
Expand Down
63 changes: 63 additions & 0 deletions packages/cli/src/serve/daemon-status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,37 @@ describe('buildDaemonStatusResponse', () => {
});
});

it('summarizes MCP server health in workspace.mcp.summary', async () => {
const response = await buildDaemonStatusResponse(
'full',
makeOptions({
mcpStatus: {
v: 1,
workspaceCwd: BASE_WORKSPACE,
initialized: true,
servers: [
{ name: 'a', mcpStatus: 'connected', disabled: false },
{ name: 'b', mcpStatus: 'connected', disabled: false },
{
name: 'c',
mcpStatus: 'disconnected',
status: 'error',
disabled: false,
},
{ name: 'd', disabled: true },
],
},
}),
);
const mcpSummary = response.full?.workspace?.['mcp']?.summary;
expect(mcpSummary).toMatchObject({
serversCount: 4,
serversConnected: 2,
serversErrored: 1,
serversDisabled: 1,
});
});

it('marks a timed-out full workspace section unavailable', async () => {
vi.useFakeTimers();

Expand Down Expand Up @@ -399,6 +430,34 @@ describe('buildDaemonStatusResponse', () => {

expect(response.runtime).not.toHaveProperty('perf');
});

it('includes activity fields in runtime', async () => {
Comment thread
doudouOUC marked this conversation as resolved.
Resolved
vi.useFakeTimers({ now: 1719990005000 });
const response = await buildDaemonStatusResponse(
'summary',
makeOptions({
activePromptCount: 3,
lastActivityAt: 1719990000000,
}),
);
expect(response.runtime.activity).toEqual({
activePrompts: 3,
lastActivityAt: '2024-07-03T07:00:00.000Z',
idleSinceMs: 5000,
});
});

it('reports null activity when daemon has never been active', async () => {
const response = await buildDaemonStatusResponse(
'summary',
makeOptions({ activePromptCount: 0, lastActivityAt: null }),
);
expect(response.runtime.activity).toEqual({
activePrompts: 0,
lastActivityAt: null,
idleSinceMs: null,
});
});
});

interface MakeOptionsInput {
Expand All @@ -418,6 +477,8 @@ interface MakeOptionsInput {
outbound: { count: number; totalBytes: number; maxBytes: number };
};
};
activePromptCount?: number;
lastActivityAt?: number | null;
}

function makeOptions(input: MakeOptionsInput = {}): BuildDaemonStatusOptions {
Expand All @@ -431,6 +492,8 @@ function makeOptions(input: MakeOptionsInput = {}): BuildDaemonStatusOptions {
getDaemonStatusSnapshot: () => input.bridgeSnapshot ?? BASE_BRIDGE_SNAPSHOT,
getWorkspaceToolsStatus: async () =>
input.toolsStatus ?? okStatus({ tools: [] }),
activePromptCount: input.activePromptCount ?? 0,
lastActivityAt: input.lastActivityAt ?? null,
} as unknown as AcpSessionBridge;
const workspace = {
getWorkspaceMcpStatus: async () =>
Expand Down
41 changes: 41 additions & 0 deletions packages/cli/src/serve/daemon-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ interface DaemonStatusRuntime {
rejectedSinceStart: Record<RateLimitTier, number>;
};
perf?: DaemonPerfSnapshot;
activity: {
activePrompts: number;
lastActivityAt: string | null;
idleSinceMs: number | null;
};
process: NodeJS.MemoryUsage;
}

Expand Down Expand Up @@ -239,6 +244,7 @@ export async function buildDaemonStatusResponse(
input: BuildDaemonStatusOptions,
): Promise<DaemonStatusResponse> {
const bridgeSnapshot = input.bridge.getDaemonStatusSnapshot();
const lastActivity = input.bridge.lastActivityAt;
const acpSnapshot = input.acpHandle?.registry.getSnapshot();
const rateLimitHits = input.rateLimiter?.getHitCounts() ?? zeroRateHits();
const channelWorker = input.getChannelWorkerSnapshot?.() ?? {
Expand Down Expand Up @@ -336,6 +342,15 @@ export async function buildDaemonStatusResponse(
rejectedSinceStart: rateLimitHits,
},
...(input.getPerfSnapshot ? { perf: input.getPerfSnapshot() } : {}),
activity: {
activePrompts: input.bridge.activePromptCount,
Comment thread
doudouOUC marked this conversation as resolved.
Resolved
lastActivityAt:
Comment thread
doudouOUC marked this conversation as resolved.
Resolved
lastActivity !== null
? new Date(lastActivity).toISOString()
: null,
idleSinceMs:
lastActivity !== null ? Date.now() - lastActivity : null,
},
process: process.memoryUsage(),
},
...(full ? { full } : {}),
Expand Down Expand Up @@ -669,9 +684,35 @@ function summarizeStatusData(data: unknown): SectionSummary {
}
}

summarizeMcpServers(data, summary);

return summary;
}

function summarizeMcpServers(
data: StatusRecord,
summary: SectionSummary,
): void {
const servers = data['servers'];
if (!Array.isArray(servers)) return;
let connected = 0;
let errored = 0;
let disabled = 0;
for (const server of servers) {
if (!isRecord(server)) continue;
if (server['disabled'] === true) {
disabled++;
} else if (server['status'] === 'error') {
errored++;
} else if (server['mcpStatus'] === 'connected') {
connected++;
}
Comment thread
doudouOUC marked this conversation as resolved.
Resolved
}
summary['serversConnected'] = connected;
summary['serversErrored'] = errored;
summary['serversDisabled'] = disabled;
}

function collectStatuses(data: unknown): string[] {
const statuses: string[] = [];
visitStatusContainers(data, (record) => {
Expand Down
7 changes: 6 additions & 1 deletion packages/cli/src/serve/run-qwen-serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1140,6 +1140,11 @@ function createBootstrapServeApp(input: {
read: 0,
},
},
activity: {
activePrompts: 0,
lastActivityAt: null,
idleSinceMs: null,
},
process: process.memoryUsage(),
},
...(detail.detail === 'full'
Expand Down Expand Up @@ -1240,7 +1245,7 @@ function isCorsPreflightRequest(req: Request): boolean {
Boolean(req.headers.origin) &&
Boolean(
req.headers['access-control-request-method'] ||
req.headers['access-control-request-headers'],
req.headers['access-control-request-headers'],
Comment thread
doudouOUC marked this conversation as resolved.
Resolved
)
);
}
Expand Down
Loading