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
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.

`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.
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
38 changes: 38 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 ?? null;
const acpSnapshot = input.acpHandle?.registry.getSnapshot();
const rateLimitHits = input.rateLimiter?.getHitCounts() ?? zeroRateHits();
const channelWorker = input.getChannelWorkerSnapshot?.() ?? {
Expand Down Expand Up @@ -336,6 +342,12 @@ export async function buildDaemonStatusResponse(
rejectedSinceStart: rateLimitHits,
},
...(input.getPerfSnapshot ? { perf: input.getPerfSnapshot() } : {}),
activity: {
activePrompts: input.bridge.activePromptCount ?? 0,
lastActivityAt:
Comment thread
doudouOUC marked this conversation as 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 +681,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.
}
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.
)
);
}
Expand Down
Loading