Skip to content
Closed
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 src/agentbox/http-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ describe("http-server — /health + /api/sessions + /api/models", () => {
schemaVersion: AGENT_SYNC_STATUS_SCHEMA_VERSION,
knowledge: {
syncedAt: "2026-08-18T08:00:00.000Z",
sessionRefresh: { policy: "next_prompt", residentSessions: 0, pendingSessions: 0 },
repos: [{ id: "kb-1", name: "hardware", version: 2, sha256: "abc", fileCount: 12 }],
},
skills: { names: ["k8s-debug"] },
Expand Down
3 changes: 2 additions & 1 deletion src/agentbox/http-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {
readBoxSyncStatus,
type KnowledgeSyncHandler,
} from "./sync-handlers.js";
import type { ObservedMcpServer } from "../shared/agentbox-sync-status.js";
import { knowledgeSessionRefresh, type ObservedMcpServer } from "../shared/agentbox-sync-status.js";
import { GATEWAY_SYNC_DESCRIPTORS, type AgentBoxSyncHandler, type GatewaySyncType } from "../shared/gateway-sync.js";
import { detectLanguage } from "../shared/detect-language.js";
import { stripLanguageDirective } from "../shared/strip-language-directive.js";
Expand Down Expand Up @@ -833,6 +833,7 @@ export function createHttpServer(
const servers = liveManager ? liveManager.getServerConnections() : observedMcpServers;
sendJson(res, 200, {
...inventory,
knowledge: { ...inventory.knowledge, sessionRefresh: knowledgeSessionRefresh(sessionManager.list()) },
mcp: { ...inventory.mcp, ...(servers ? { servers } : {}) },
model: observedModel,
harness: observedHarness,
Expand Down
18 changes: 17 additions & 1 deletion src/shared/agentbox-sync-status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* ways of "saying nothing" mean opposite things.
*/
import { describe, it, expect } from "vitest";
import { AGENT_SYNC_STATUS_SCHEMA_VERSION, normalizeBoxSyncStatus } from "./agentbox-sync-status.js";
import { AGENT_SYNC_STATUS_SCHEMA_VERSION, knowledgeSessionRefresh, normalizeBoxSyncStatus } from "./agentbox-sync-status.js";

const REV_A = "a".repeat(64);
const REV_B = "b".repeat(64);
Expand Down Expand Up @@ -165,3 +165,19 @@ describe("mcp.servers observation", () => {
expect(status.mcp.servers?.[0].error).toEqual({ kind: "unknown", message: "boom" });
});
});


describe("knowledge session refresh evidence", () => {
it("reports invalidated sessions separately from installed files", () => {
const refresh = knowledgeSessionRefresh([{ _invalidated: true }, { _invalidated: false }]);
expect(refresh).toEqual({ policy: "next_prompt", residentSessions: 2, pendingSessions: 1 });
const status = normalizeBoxSyncStatus(payload({ knowledge: { repos: [], sessionRefresh: refresh } }));
expect(status.knowledge.sessionRefresh).toEqual(refresh);
});
it("preserves missing legacy evidence and rejects inconsistent counts", () => {
expect(normalizeBoxSyncStatus(payload()).knowledge.sessionRefresh).toBeUndefined();
expect(normalizeBoxSyncStatus(payload({ knowledge: { repos: [], sessionRefresh: {
policy: "next_prompt", residentSessions: 1, pendingSessions: 2,
} } })).knowledge.sessionRefresh).toBeUndefined();
});
});
32 changes: 31 additions & 1 deletion src/shared/agentbox-sync-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,26 @@ export interface ObservedKnowledgeRepo {
* session has initialized MCP; a v3 box cannot, so a consumer must not read an
* absent `servers` as "all connected".
*/
export const AGENT_SYNC_STATUS_SCHEMA_VERSION = 4;
// 5 adds knowledge.sessionRefresh: materialized files and deferred warm-session
// invalidation are distinct; older boxes leave this evidence absent.
export const AGENT_SYNC_STATUS_SCHEMA_VERSION = 5;

/** Installed files and warm-session refresh are separate observations. */
export interface KnowledgeSessionRefresh {
policy: "next_prompt";
residentSessions: number;
pendingSessions: number;
}

export function knowledgeSessionRefresh(
sessions: ReadonlyArray<{ _invalidated: boolean }>,
): KnowledgeSessionRefresh {
return {
policy: "next_prompt",
residentSessions: sessions.length,
pendingSessions: sessions.filter((session) => session._invalidated).length,
};
}

/** Closed vocabulary shared with core/mcp-client.ts; see McpConnectErrorKind. */
export type ObservedMcpErrorKind =
Expand Down Expand Up @@ -64,6 +83,7 @@ export interface BoxSyncStatus {
knowledge: {
syncedAt: string | null;
repos: ObservedKnowledgeRepo[];
sessionRefresh?: KnowledgeSessionRefresh;
};
skills: { names: string[] };
mcp: {
Expand Down Expand Up @@ -193,6 +213,11 @@ export function normalizeBoxSyncStatus(value: unknown): BoxSyncStatus {
if (!root) throw new Error("invalid AgentBox sync status");

const knowledge = record(root.knowledge);
const refresh = record(knowledge?.sessionRefresh);
const validRefresh = refresh?.policy === "next_prompt"
&& Number.isSafeInteger(refresh.residentSessions) && (refresh.residentSessions as number) >= 0
&& Number.isSafeInteger(refresh.pendingSessions) && (refresh.pendingSessions as number) >= 0
&& (refresh.pendingSessions as number) <= (refresh.residentSessions as number);
const repos = Array.isArray(knowledge?.repos)
? knowledge.repos.flatMap((item): ObservedKnowledgeRepo[] => {
const repo = record(item);
Expand Down Expand Up @@ -229,6 +254,11 @@ export function normalizeBoxSyncStatus(value: unknown): BoxSyncStatus {
knowledge: {
syncedAt: typeof knowledge?.syncedAt === "string" ? knowledge.syncedAt : null,
repos,
...(validRefresh ? { sessionRefresh: {
policy: "next_prompt" as const,
residentSessions: refresh!.residentSessions as number,
pendingSessions: refresh!.pendingSessions as number,
} } : {}),
},
skills: { names: strings(record(root.skills)?.names) },
mcp: normalizeObservedMcp(record(root.mcp)),
Expand Down
Loading