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
44 changes: 43 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,36 @@ function getBaseUrl(): string {
return `http://localhost:${getRestPort()}`;
}

let discoveredViewerPort: number | null = null;

export async function discoverViewerPort(): Promise<void> {
if (discoveredViewerPort !== null) return;
try {
const res = await fetch(`${getBaseUrl()}/agentmemory/livez`, {
signal: AbortSignal.timeout(1000),
});
if (res.ok) {
const data = await res.json() as { viewerPort?: number | null };
if (typeof data.viewerPort === "number") {
discoveredViewerPort = data.viewerPort;
}
}
} catch {}
}

function getViewerUrl(): string {
const envUrl = process.env["AGENTMEMORY_VIEWER_URL"];
if (envUrl) return envUrl.replace(/\/+$/, "");

if (discoveredViewerPort !== null) {
try {
const u = new URL(getBaseUrl());
return `${u.protocol}//${u.hostname}:${discoveredViewerPort}`;
} catch {
return `http://localhost:${discoveredViewerPort}`;
}
}

try {
const u = new URL(getBaseUrl());
const vPort =
Expand Down Expand Up @@ -257,7 +284,18 @@ async function isAgentmemoryReady(): Promise<boolean> {
const res = await fetch(`${getBaseUrl()}/agentmemory/livez`, {
signal: AbortSignal.timeout(2000),
});
return res.ok;
if (!res.ok) return false;
try {
const data = await res.json() as { viewerPort?: number | null; viewerSkipped?: boolean };
if (typeof data.viewerPort === "number") {
discoveredViewerPort = data.viewerPort;
return true;
}
if (data.viewerSkipped) return true;
return false;
} catch {
return false;
}
} catch {
return false;
}
Expand Down Expand Up @@ -1101,6 +1139,9 @@ async function runStatus() {
apiFetch<any>(base, "config/flags"),
]);

if (typeof healthRes?.viewerPort === "number") {
discoveredViewerPort = healthRes.viewerPort;
}
const h = healthRes?.health;
const status = healthRes?.status || "unknown";
const version = healthRes?.version || "?";
Expand Down Expand Up @@ -1260,6 +1301,7 @@ function buildDoctorEffects(): DoctorEffects {
iiiBinaryVersion: (binPath: string) => iiiBinVersion(binPath),
viewerReachable: async (timeoutMs = 2000) => {
try {
await discoverViewerPort();
const res = await fetch(getViewerUrl(), {
signal: AbortSignal.timeout(timeoutMs),
});
Expand Down
5 changes: 4 additions & 1 deletion src/triggers/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { ResilientProvider } from "../providers/resilient.js";
import { VERSION } from "../version.js";
import { timingSafeCompare } from "../auth.js";
import { renderViewerDocument } from "../viewer/document.js";
import { getBoundViewerPort, getViewerSkipped } from "../viewer/server.js";
import { MAX_FILES_UPPER_BOUND } from "../functions/replay.js";
import {
isGraphExtractionEnabled,
Expand Down Expand Up @@ -143,7 +144,7 @@ export function registerApiTriggers(
sdk.registerFunction("api::liveness",
async (): Promise<Response> => ({
status_code: 200,
body: { status: "ok", service: "agentmemory" },
body: { status: "ok", service: "agentmemory", viewerPort: getBoundViewerPort(), viewerSkipped: getViewerSkipped() },
}),
);
sdk.registerTrigger({
Expand Down Expand Up @@ -244,6 +245,8 @@ export function registerApiTriggers(
health: health || null,
functionMetrics,
circuitBreaker,
viewerPort: getBoundViewerPort(),
viewerSkipped: getViewerSkipped(),
},
};
},
Expand Down
24 changes: 24 additions & 0 deletions src/viewer/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,27 @@ function readBody(req: IncomingMessage): Promise<string> {

const MAX_VIEWER_PORT_RETRIES = 10;

let boundViewerPort: number | null = null;
let viewerSkipped = false;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export function getBoundViewerPort(): number | null {
return boundViewerPort;
}
export function getViewerSkipped(): boolean {
return viewerSkipped;
}

export function startViewerServer(
port: number,
_kv: unknown,
_sdk: unknown,
secret?: string,
restPort?: number,
): Server {
// Reset exported runtime state for each start attempt.
boundViewerPort = null;
viewerSkipped = false;

const resolvedRestPort = restPort ?? port - 2;
const requestedPort = port;
// Computed lazily on first request — `port` may be 0 here (OS-assigned)
Expand Down Expand Up @@ -227,6 +241,12 @@ export function startViewerServer(
};

server.on("listening", () => {
const addr = server.address();
boundViewerPort =
addr && typeof addr === "object" && "port" in addr
? addr.port
: currentPort;
viewerSkipped = false;
if (currentPort === requestedPort) {
console.log(`[agentmemory] Viewer: http://localhost:${currentPort}`);
} else {
Expand All @@ -244,10 +264,14 @@ export function startViewerServer(
return;
}
if (err.code === "EADDRINUSE") {
boundViewerPort = null;
viewerSkipped = true;
console.warn(
`[agentmemory] Viewer ports ${requestedPort}-${requestedPort + MAX_VIEWER_PORT_RETRIES} all in use, skipping viewer.`,
);
} else {
boundViewerPort = null;
viewerSkipped = true;
console.error(`[agentmemory] Viewer error:`, err.message);
}
});
Expand Down