Effective state
@@ -5717,6 +5748,7 @@
Confirm governance change
});
let scopeDir = null;
+ let environmentDir = [];
let scopeDirNote = "Loading scopes…";
async function loadScopeDirectory() {
const r = await api("GET", "/api/scopes");
@@ -5728,6 +5760,7 @@
Confirm governance change
}
viewLoadedAt.history = Date.now();
scopeDir = r.data.scopes || [];
+ environmentDir = r.data.environments || [];
if (SCOPED.has(view) && !urlToState().session) {
const memoryEditor = view === "memory" && !(orgWideView() && urlToState().mem !== "edit");
@@ -6244,6 +6277,19 @@
Confirm governance change
);
}
window.addEventListener("scroll", syncGovernanceSectionNav, { passive: true });
+ function renderEnvironmentNotice(data) {
+ const notice = $("environment-notice");
+ const attachment = data?.environmentAttachment;
+ notice.classList.toggle("hidden", !attachment);
+ if (!attachment) return;
+ const name = attachment.environmentName || shortName(attachment.environmentId);
+ $("environment-notice-title").textContent = "Uses named environment " + name;
+ $("environment-notice-detail").textContent =
+ "Computer files and working memory resolve to this environment. Governance and conversation history remain scoped here.";
+ $("environment-notice-open").textContent = "Open " + name;
+ $("environment-notice-open").onclick = () =>
+ go({ view: "governance", scope: attachment.environmentId, session: null, page: 1 });
+ }
let governanceReq = 0;
async function loadScope() {
const requestedScope = scope;
@@ -6259,6 +6305,7 @@
Confirm governance change
);
return;
}
+ renderEnvironmentNotice(r.data);
renderGovernanceOverview(r.data);
syncGovernanceSectionNav();
loadedCommandPolicyPresent = r.data.commandPolicy != null;
@@ -11451,6 +11498,21 @@
Confirm governance change
actions: [sortControl],
});
const activityTime = (s) => (scopeSort === "human" ? s.lastConversationActivity || 0 : s.lastActivity || 0);
+ if (environmentDir.length) {
+ const environments = denseList(
+ environmentDir,
+ (environment) => ({
+ name: environment.name || shortName(environment.id),
+ preview: plural(environment.attachedScopes?.length || 0, "attached scope"),
+ href: stateToUrl({ view: "history", scope: environment.id, historyKind }),
+ }),
+ (environment) => selectScope(environment.id),
+ "No named environments.",
+ );
+ root.appendChild(
+ dataCard("Named environments", "Named computers and working memory that scopes can share.", environments),
+ );
+ }
const t = denseList(
activeRows,
(s) => {
diff --git a/plugins/admin/src/index.ts b/plugins/admin/src/index.ts
index 25106783a..31b62cb25 100644
--- a/plugins/admin/src/index.ts
+++ b/plugins/admin/src/index.ts
@@ -141,7 +141,7 @@ async function forward(
res.writeHead(r.status, { "content-type": "application/json" });
pipeBody(res, r.body);
} catch (err) {
- console.error("[admin] core request failed:", err);
+ console.error("[admin] core request failed:", String(err));
json(res, 502, { error: "core_unreachable", message: "core unavailable" });
}
}
@@ -176,7 +176,7 @@ async function forwardDownload(res: ServerResponse, principal: string, corePath:
res.writeHead(r.status, headers);
pipeBody(res, r.body);
} catch (err) {
- console.error("[admin] core download failed:", err);
+ console.error("[admin] core download failed:", String(err));
json(res, 502, { error: "core_unreachable", message: "core unavailable" });
}
}
@@ -236,7 +236,7 @@ async function uploadFileFromRequest(
});
return forward(req, res, principal, "POST", corePath, body);
} catch (err) {
- console.error("[admin] upload failed:", err);
+ console.error("[admin] upload failed:", String(err));
return json(res, 502, { error: "core_unreachable", message: "core unavailable" });
}
}
@@ -301,7 +301,7 @@ const server = createServer((req, res) => {
void portalTokenStore
.run(token, () => handle(req, res))
.catch((err: unknown) => {
- console.error("[admin] unhandled request error:", err);
+ console.error("[admin] unhandled request error:", String(err));
json(res, 500, { error: "internal_error", message: "internal server error" });
});
});
@@ -402,7 +402,7 @@ async function handle(req: IncomingMessage, res: ServerResponse): Promise
res.writeHead(r.status, { "content-type": "application/json" });
res.end(text);
} catch (err) {
- console.error("[admin] core request failed:", err);
+ console.error("[admin] core request failed:", String(err));
json(res, 502, { error: "core_unreachable", message: "core unavailable" });
}
return;
diff --git a/plugins/admin/test/environments.test.ts b/plugins/admin/test/environments.test.ts
new file mode 100644
index 000000000..3ec8c9fb8
--- /dev/null
+++ b/plugins/admin/test/environments.test.ts
@@ -0,0 +1,13 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import { join } from "node:path";
+
+const html = readFileSync(join(import.meta.dirname, "../public/index.html"), "utf8");
+
+test("the admin UI lists named environments and links attachment warnings", () => {
+ assert.match(html, /Named environments/);
+ assert.match(html, /id="environment-notice"/);
+ assert.match(html, /Uses named environment/);
+ assert.match(html, /scope: attachment\.environmentId/);
+});
diff --git a/plugins/auth/src/index.ts b/plugins/auth/src/index.ts
index ee2533206..b63ba85a3 100644
--- a/plugins/auth/src/index.ts
+++ b/plugins/auth/src/index.ts
@@ -32,7 +32,7 @@ export async function startServer(): Promise {
});
const server = createServer((req, res) => {
void handle(req, res).catch((err: unknown) => {
- console.error(`[auth] 500 ${req.method ?? "?"} ${(req.url ?? "?").split("?")[0]}:`, err);
+ console.error("[auth] 500 %s %s: %s", req.method ?? "?", (req.url ?? "?").split("?")[0], String(err));
if (!res.headersSent) json(res, 500, { error: "internal_error" });
else res.end();
});
diff --git a/plugins/chassis/src/branding.ts b/plugins/chassis/src/branding.ts
index 82ccb7e9e..3ba6758de 100644
--- a/plugins/chassis/src/branding.ts
+++ b/plugins/chassis/src/branding.ts
@@ -29,7 +29,7 @@ export function createBrandingCache(fetchBranding: () => Promise):
warmed = true;
nextAt = Date.now() + REFRESH_MS;
} catch (err) {
- if (process.env.BRANDING_DEBUG) console.error("[branding] fetch failed:", err);
+ if (process.env.BRANDING_DEBUG) console.error("[branding] fetch failed:", String(err));
nextAt = Date.now() + RETRY_MS;
} finally {
inflight = null;
diff --git a/plugins/portal/src/index.ts b/plugins/portal/src/index.ts
index 2ecde29cd..395064e37 100644
--- a/plugins/portal/src/index.ts
+++ b/plugins/portal/src/index.ts
@@ -812,7 +812,7 @@ function renewSessionCookie(req: IncomingMessage, res: ServerResponse): void {
const server = createServer((req, res) => {
void handle(req, res).catch((err: unknown) => {
- console.error(`[portal] 500 ${req.method ?? "?"} ${(req.url ?? "?").split("?")[0]}:`, err);
+ console.error("[portal] 500 %s %s: %s", req.method ?? "?", (req.url ?? "?").split("?")[0], String(err));
if (!res.headersSent) json(res, 500, { error: "internal_error" });
else res.end();
});
diff --git a/plugins/web-ui/server/index.ts b/plugins/web-ui/server/index.ts
index 08d0b2a46..f05ebb1e4 100644
--- a/plugins/web-ui/server/index.ts
+++ b/plugins/web-ui/server/index.ts
@@ -1918,7 +1918,7 @@ export const handler = async (req: IncomingMessage, res: ServerResponse) => {
const server = createServer((req, res) => {
void handler(req, res).catch((err: unknown) => {
- console.error(`[web-ui] 502 ${req.method ?? "?"} ${req.url ?? "?"}:`, err);
+ console.error("[web-ui] 502 %s %s: %s", req.method ?? "?", req.url ?? "?", String(err));
if (!res.headersSent) json(res, 502, { error: "bad_gateway", message: "upstream error" });
else res.end();
});
@@ -1942,7 +1942,7 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
});
})
.catch((err: unknown) => {
- console.error("[web-ui] failed to start:", err);
+ console.error("[web-ui] failed to start:", String(err));
process.exit(1);
});
}
diff --git a/src/admin/postgres-audit-log.ts b/src/admin/postgres-audit-log.ts
index 8a6023056..fda9e3ddd 100644
--- a/src/admin/postgres-audit-log.ts
+++ b/src/admin/postgres-audit-log.ts
@@ -1,6 +1,7 @@
import { createPgPool } from "../persistence/pg-pool.ts";
import type { ScopeId } from "../types.ts";
import type { AuditEvent, AuditLog } from "../audit/audit-log.ts";
+import { errMessage } from "../util/errors.ts";
function rowToEvent(r: Record): AuditEvent {
return {
@@ -61,7 +62,7 @@ export function createPostgresAuditLog(connectionString: string): AuditLog {
e.detail ?? null,
])
.then(() => undefined)
- .catch((err) => console.error("[audit] failed to persist event to durable store:", err));
+ .catch((err) => console.error("[audit] failed to persist event to durable store:", errMessage(err)));
pendingWrites.add(write);
void write.finally(() => pendingWrites.delete(write));
},
diff --git a/src/admin/postgres-metrics-sink.ts b/src/admin/postgres-metrics-sink.ts
index 0fbdf1f03..29187f828 100644
--- a/src/admin/postgres-metrics-sink.ts
+++ b/src/admin/postgres-metrics-sink.ts
@@ -1,5 +1,6 @@
import { createPostgresEventSink, type EventColumn } from "./scoped-event-sink.ts";
import type { MetricsSink, TurnMetricSample } from "./metrics-sink.ts";
+import { errMessage } from "../util/errors.ts";
const COLUMNS: readonly EventColumn[] = [
["ts", "ts", "BIGINT", "number", true],
@@ -76,7 +77,7 @@ export function createPostgresMetricsSink(connectionString: string): MetricsSink
params.push(runId);
await sink
.q(`UPDATE turn_metrics SET ${sets.join(", ")} WHERE run_id = $${params.length}`, params)
- .catch((err) => console.error("[metrics] failed to patch turn metric:", err));
+ .catch((err) => console.error("[metrics] failed to patch turn metric:", errMessage(err)));
},
list: (opts = {}) => sink.list(opts),
};
diff --git a/src/admin/scoped-event-sink.ts b/src/admin/scoped-event-sink.ts
index 25f1dc3ac..48eff01f0 100644
--- a/src/admin/scoped-event-sink.ts
+++ b/src/admin/scoped-event-sink.ts
@@ -1,5 +1,6 @@
import type { ScopeId } from "../types.ts";
import { createPgPool, type PgPool } from "../persistence/pg-pool.ts";
+import { errMessage } from "../util/errors.ts";
export interface ScopedEvent {
scopeLabel: ScopeId;
@@ -162,7 +163,7 @@ export function createPostgresEventSink(cfg: PostgresEventSinkConfig): Pos
const s = input as Record;
const values = cfg.columns.map(([, js]) => (js === "ts" ? Date.now() : (s[js] ?? null)));
const write = q(insertSql, values)
- .catch((err) => console.error(cfg.persistErrorMessage, err))
+ .catch((err) => console.error(cfg.persistErrorMessage, errMessage(err)))
.finally(() => pendingWrites.delete(write));
pendingWrites.add(write);
},
diff --git a/src/api/routes/admin/scope-config.ts b/src/api/routes/admin/scope-config.ts
index 22acc4f7e..17d206168 100644
--- a/src/api/routes/admin/scope-config.ts
+++ b/src/api/routes/admin/scope-config.ts
@@ -132,10 +132,25 @@ export async function listAdminScopes(ctx: ApiCtx): Promise {
const crons = await app.listCrons();
const deployments = await app.listDeployments();
const skills = await app.listSkills();
+ const environmentRows = await app.listEnvironments();
+ const environments = environmentRows.map(({ environment, attachments }) => ({
+ id: environment.id,
+ name: environment.name,
+ ownerActorId: environment.ownerActorId,
+ attachedScopes: attachments.map((attachment) => attachment.scopeId).sort(),
+ }));
+ const environmentById = new Map(environments.map((environment) => [environment.id, environment]));
+ const attachmentByScope = new Map(
+ environments.flatMap((environment) =>
+ environment.attachedScopes.map((attachedScope) => [attachedScope, environment] as const),
+ ),
+ );
const owners = [
...crons.map((c) => c.ownerScopeId),
...deployments.map((d) => d.ownerScopeId),
...skills.map((s) => s.scopeId),
+ ...environments.map((environment) => environment.id),
+ ...environments.flatMap((environment) => environment.attachedScopes),
];
const labels = await discoverScopes(app, deps, owners);
const countBy = (ids: string[]): Map => {
@@ -171,18 +186,31 @@ export async function listAdminScopes(ctx: ApiCtx): Promise {
const cronN = countBy(crons.map((c) => c.ownerScopeId));
const deployN = countBy(deployments.map((d) => d.ownerScopeId));
const skillN = countBy(skills.map((s) => s.scopeId));
- const scopes = [...labels].map(([id, label]) => ({
- scopeId: id,
- ...(label ? { label } : {}),
- sessions: sessionN.get(id) ?? 0,
- backgroundSessions: backgroundN.get(id) ?? 0,
- lastActivity: lastActivityBy.get(id) ?? 0,
- lastConversationActivity: lastConversationBy.get(id) ?? 0,
- lastMessage: lastMessageBy.get(id) ?? "",
- crons: cronN.get(id) ?? 0,
- deployments: deployN.get(id) ?? 0,
- skills: skillN.get(id) ?? 0,
- }));
+ const scopes = [...labels].map(([id, label]) => {
+ const environment = environmentById.get(id);
+ const attachment = attachmentByScope.get(id);
+ return {
+ scopeId: id,
+ ...(label ? { label } : {}),
+ ...(environment?.name ? { environmentName: environment.name } : {}),
+ ...(attachment
+ ? {
+ environmentAttachment: {
+ environmentId: attachment.id,
+ environmentName: attachment.name,
+ },
+ }
+ : {}),
+ sessions: sessionN.get(id) ?? 0,
+ backgroundSessions: backgroundN.get(id) ?? 0,
+ lastActivity: lastActivityBy.get(id) ?? 0,
+ lastConversationActivity: lastConversationBy.get(id) ?? 0,
+ lastMessage: lastMessageBy.get(id) ?? "",
+ crons: cronN.get(id) ?? 0,
+ deployments: deployN.get(id) ?? 0,
+ skills: skillN.get(id) ?? 0,
+ };
+ });
scopes.sort(
(a, b) =>
b.lastActivity - a.lastActivity ||
@@ -190,7 +218,35 @@ export async function listAdminScopes(ctx: ApiCtx): Promise {
b.backgroundSessions - a.backgroundSessions ||
a.scopeId.localeCompare(b.scopeId),
);
- return sendJson(res, 200, { scopeId: scope, scopes });
+ return sendJson(res, 200, { scopeId: scope, scopes, environments });
+}
+
+interface ScopeEnvironmentMetadata {
+ environment?: { id: string; name: string; ownerActorId: string | null };
+ environmentAttachment?: { environmentId: string; environmentName: string | null };
+}
+
+async function scopeEnvironmentMetadata(deps: ApiCtx["deps"], targetScope: string): Promise {
+ const store = deps.environments;
+ if (!store) return {};
+
+ const [environment, attachment] = await Promise.all([store.get(targetScope), store.getAttachment(targetScope)]);
+ const metadata: ScopeEnvironmentMetadata = {};
+ if (environment?.name) {
+ metadata.environment = {
+ id: environment.id,
+ name: environment.name,
+ ownerActorId: environment.ownerActorId,
+ };
+ }
+ if (attachment) {
+ const attachedEnvironment = await store.get(attachment.environmentId);
+ metadata.environmentAttachment = {
+ environmentId: attachment.environmentId,
+ environmentName: attachedEnvironment?.name ?? null,
+ };
+ }
+ return metadata;
}
export async function getScopeConfig(ctx: ApiCtx): Promise {
@@ -202,6 +258,7 @@ export async function getScopeConfig(ctx: ApiCtx): Promise {
if (!actor) return;
await deps.config.refreshScope(targetScope);
audit(deps, { principalId: actor.id, action: "config.read", resource: "config", scopeLabel: targetScope });
+ const environmentMetadata = await scopeEnvironmentMetadata(deps, targetScope);
const serviceCredentials = await Promise.all(
(deps.serviceCreds ? await deps.serviceCreds.listServiceCredentials(targetScope) : []).map(async (c) => {
const usage = (await deps.credentialUsage?.list({ slug: c.slug, limit: 5000 })) ?? [];
@@ -261,6 +318,7 @@ export async function getScopeConfig(ctx: ApiCtx): Promise {
};
return sendJson(res, 200, {
scopeId: targetScope,
+ ...environmentMetadata,
...values,
soulVersion: deps.config.soulVersion(targetScope),
soulHistory: deps.config.soulHistory(targetScope),
diff --git a/src/api/server.ts b/src/api/server.ts
index 8b82a11fb..7cd533842 100644
--- a/src/api/server.ts
+++ b/src/api/server.ts
@@ -299,7 +299,7 @@ function respondError(req: IncomingMessage, res: ServerResponse, err: unknown):
else res.destroy();
return;
}
- console.error(`[server] 500 ${req.method ?? "?"} ${req.url ?? "?"}:`, err);
+ console.error("[server] 500 %s %s: %s", req.method ?? "?", req.url ?? "?", errMessage(err));
if (!res.headersSent) sendJson(res, 500, { error: "internal_error", message: "internal server error" });
else res.destroy();
}
@@ -368,7 +368,7 @@ function buildFastify(wiring: Wiring, server: Server): { fastify: FastifyInstanc
fastify.decorateRequest("gate", undefined);
fastify.setErrorHandler((err, request, reply) => {
- console.error(`[server] 500 ${request.raw.method ?? "?"} ${request.raw.url ?? "?"}:`, err);
+ console.error(`[server] 500 ${request.raw.method ?? "?"} ${request.raw.url ?? "?"}:`, errMessage(err));
return reply.code(500).send({ error: "internal_error", message: "internal server error" });
});
@@ -451,7 +451,7 @@ function buildServer(app: App, deps: ServerOptions, allowUnsignedSourceAuth: boo
});
const { fastify, routing } = buildFastify(wiring, server);
const ready = Promise.resolve(fastify.ready());
- ready.catch((err: unknown) => console.error("[server] fastify initialization failed:", err));
+ ready.catch((err: unknown) => console.error("[server] fastify initialization failed:", errMessage(err)));
server.requestTimeout = 30_000;
server.headersTimeout = 10_000;
server.keepAliveTimeout = 5_000;
diff --git a/src/core/orchestrator.ts b/src/core/orchestrator.ts
index cadd8abae..227a10d9d 100644
--- a/src/core/orchestrator.ts
+++ b/src/core/orchestrator.ts
@@ -511,7 +511,7 @@ export function createOrchestrator(deps: OrchestratorDeps): Orchestrator {
try {
await deps.sessions.recordLlmRequest(screenSession.id, { ...rec, scopeLabel: scopeId });
} catch (err) {
- console.error("[orchestrator] failed to persist security screen request snapshot:", err);
+ console.error("[orchestrator] failed to persist security screen request snapshot:", errMessage(err));
}
};
let screenedOverheard: OverheardEntryPayload[] = [];
@@ -2282,7 +2282,7 @@ export function createOrchestrator(deps: OrchestratorDeps): Orchestrator {
try {
await deps.sessions.recordLlmRequest(session.id, { ...rec, scopeLabel: scopeId });
} catch (err) {
- console.error("[orchestrator] failed to persist LLM request snapshot:", err);
+ console.error("[orchestrator] failed to persist LLM request snapshot:", errMessage(err));
}
},
});
diff --git a/src/cron/scheduler.ts b/src/cron/scheduler.ts
index 066d79114..2f9237834 100644
--- a/src/cron/scheduler.ts
+++ b/src/cron/scheduler.ts
@@ -188,12 +188,12 @@ export function createScheduler(deps: SchedulerDeps): Scheduler {
const t = nowArg ?? now();
await leaderLease.hold(TICK_LEASE_KEY, async () => {
await fireDue(t);
- await deps.sweepAsks?.(t).catch((e: unknown) => console.error("[scheduler] ask sweep failed:", e));
+ await deps.sweepAsks?.(t).catch((e: unknown) => console.error("[scheduler] ask sweep failed:", errMessage(e)));
});
};
const sweeper = createSweeper(
- () => tick().catch((e: unknown) => console.error("[scheduler] tick failed:", e)),
+ () => tick().catch((e: unknown) => console.error("[scheduler] tick failed:", errMessage(e))),
1000,
{ label: "scheduler" },
);
@@ -227,7 +227,7 @@ export function createScheduler(deps: SchedulerDeps): Scheduler {
return;
}
} catch (e) {
- console.error("[scheduler] fire failed:", e);
+ console.error("[scheduler] fire failed:", errMessage(e));
await deps.crons.unclaimSlot(job.cronId, slot, t, cron.lastFiredAt);
return;
}
@@ -242,9 +242,9 @@ export function createScheduler(deps: SchedulerDeps): Scheduler {
if (slot !== undefined) await deps.jobQueue!.enqueueFire({ cronId: cron.id, scheduledAt: slot });
}
} catch (e) {
- console.error("[scheduler] tick failed:", e);
+ console.error("[scheduler] tick failed:", errMessage(e));
}
- await deps.sweepAsks?.(now()).catch((e: unknown) => console.error("[scheduler] ask sweep failed:", e));
+ await deps.sweepAsks?.(now()).catch((e: unknown) => console.error("[scheduler] ask sweep failed:", errMessage(e)));
}
const leaseGuard = createSweeper(
diff --git a/src/delivery/run-result-delivery.ts b/src/delivery/run-result-delivery.ts
index 4f8c3959e..588655a10 100644
--- a/src/delivery/run-result-delivery.ts
+++ b/src/delivery/run-result-delivery.ts
@@ -4,6 +4,7 @@ import type { DeliveryStore } from "./delivery-store.ts";
import type { Task, TaskStore } from "../tasks/task-store.ts";
import { SECURITY_QUARANTINE_REFUSAL_TEXT } from "../../plugins/chassis/src/security-quarantine.ts";
import { resolveTurnOrigin } from "../core/turn-origin.ts";
+import { errMessage } from "../util/errors.ts";
export interface RunResultDelivery {
destination: Destination;
@@ -56,6 +57,8 @@ export function wireRunResultDeliveries(runs: RunStore, deliveries: DeliveryStor
const delivery = runResultDelivery(run, taskList);
if (!delivery) return;
await deliveries.enqueue(delivery);
- })().catch((err) => console.error(`[delivery] failed to enqueue recovery delivery for run ${run.id}:`, err));
+ })().catch((err) =>
+ console.error(`[delivery] failed to enqueue recovery delivery for run ${run.id}:`, errMessage(err)),
+ );
});
}
diff --git a/src/harness/pi-harness.ts b/src/harness/pi-harness.ts
index 0c8a96b34..4e79d379c 100644
--- a/src/harness/pi-harness.ts
+++ b/src/harness/pi-harness.ts
@@ -76,6 +76,7 @@ import { ELIDED_IMAGE_TEXT, planTapeSeed } from "./tape-fold.ts";
import { compactTranscript, deterministicCompactSummary, estimateHistoryTokens } from "./context-compaction.ts";
import { countTokens } from "../util/tokens.ts";
import { parseSecurityScreenVerdict, SECURITY_SCREEN_SYSTEM_PROMPT } from "../security/security-posture.ts";
+import { errMessage } from "../util/errors.ts";
export interface PiHarnessOptions {
modelId?: string | ((scope?: ScopeId) => string | undefined);
@@ -1271,7 +1272,7 @@ export function createPiHarness(opts?: PiHarnessOptions): Harness {
try {
reconstructed = reconstructMessagesFromHistory(history);
} catch (err) {
- console.error("[pi-harness] history reconstruction failed; will fall back:", err);
+ console.error("[pi-harness] history reconstruction failed; will fall back:", errMessage(err));
reconstructed = null;
}
let foldSeed: PiReplayMessage[] | null = null;
@@ -1290,7 +1291,7 @@ export function createPiHarness(opts?: PiHarnessOptions): Harness {
);
}
} catch (err) {
- console.error(`${tag} fold threw:`, err);
+ console.error(`${tag} fold threw:`, errMessage(err));
}
}
const seedSource = foldSeed ?? reconstructed;
@@ -1337,7 +1338,7 @@ export function createPiHarness(opts?: PiHarnessOptions): Harness {
try {
seedRawMessagesIntoSession(session, seedSource!);
} catch (err) {
- console.error("[pi-harness] failed to seed reconstructed history (continuing without it):", err);
+ console.error("[pi-harness] failed to seed reconstructed history (continuing without it):", errMessage(err));
}
} else if (seedPlan === "priorTurns") {
try {
@@ -1356,7 +1357,7 @@ export function createPiHarness(opts?: PiHarnessOptions): Harness {
}
}
} catch (err) {
- console.error("[pi-harness] failed to seed prior turns (continuing without them):", err);
+ console.error("[pi-harness] failed to seed prior turns (continuing without them):", errMessage(err));
}
}
diff --git a/src/idempotency/idempotency-store.ts b/src/idempotency/idempotency-store.ts
index 012931ee7..f700b64bc 100644
--- a/src/idempotency/idempotency-store.ts
+++ b/src/idempotency/idempotency-store.ts
@@ -1,4 +1,5 @@
import type { DurableMap } from "../persistence/durable-map.ts";
+import { errMessage } from "../util/errors.ts";
export interface IdempotencyRecord {
key: string;
@@ -41,7 +42,7 @@ export function createIdempotencyStore(
if (r.at < cutoff) await backing.delete(r.key);
}
})().catch((e: unknown) => {
- console.error("[idempotency] retention prune failed:", e);
+ console.error("[idempotency] retention prune failed:", errMessage(e));
});
}
diff --git a/src/monitors/monitor-poller.ts b/src/monitors/monitor-poller.ts
index ca8d65307..966a0cc69 100644
--- a/src/monitors/monitor-poller.ts
+++ b/src/monitors/monitor-poller.ts
@@ -264,13 +264,13 @@ export function createMonitorPoller(deps: MonitorPollerDeps): MonitorPoller {
if (await poll(sandbox, handle, m, t)) fires++;
} catch (e) {
await deps.monitors.recordError(m.id, errMessage(e));
- console.error(`[monitor] poll failed for ${m.id}:`, e);
+ console.error(`[monitor] poll failed for ${m.id}:`, errMessage(e));
}
}
} finally {
for (const handle of handles.values()) {
await sandbox.teardown(handle, { keepWarm: true }).catch((e: unknown) => {
- console.error("[monitor] teardown failed:", e);
+ console.error("[monitor] teardown failed:", errMessage(e));
});
}
}
@@ -282,7 +282,7 @@ export function createMonitorPoller(deps: MonitorPollerDeps): MonitorPoller {
};
const sweeper = createSweeper(
- () => tick().catch((e: unknown) => console.error("[monitor] tick failed:", e)),
+ () => tick().catch((e: unknown) => console.error("[monitor] tick failed:", errMessage(e))),
10_000,
{ label: "monitor" },
);
diff --git a/src/persistence/pg-pool.ts b/src/persistence/pg-pool.ts
index 976bd8e14..d2ed10295 100644
--- a/src/persistence/pg-pool.ts
+++ b/src/persistence/pg-pool.ts
@@ -1,5 +1,6 @@
import type { Pool, PoolClient } from "pg";
import { swallowAs } from "../util/errors.ts";
+import { errMessage } from "../util/errors.ts";
export type { Pool, PoolClient };
@@ -63,7 +64,7 @@ export function createPgPool(connectionString: string, statements: string[]): Pg
poolP = (async () => {
const pg = (await import("pg")).default;
const p = new pg.Pool({ connectionString });
- p.on("error", (err) => console.error("[pg] idle client error:", err));
+ p.on("error", (err) => console.error("[pg] idle client error:", errMessage(err)));
try {
await applyDdl(p, schema);
} catch (e) {
diff --git a/src/processes/process-reaper.ts b/src/processes/process-reaper.ts
index 3f8ad1b21..51fb3341a 100644
--- a/src/processes/process-reaper.ts
+++ b/src/processes/process-reaper.ts
@@ -4,6 +4,7 @@ import { createNoopLeaderLease, type LeaderLease } from "../persistence/leader-l
import { awaitProcessExit } from "../sandbox/await-process-exit.ts";
import { processIsGone } from "../sandbox/process-poll.ts";
import type { ProcessSandbox } from "../sandbox/sandbox.ts";
+import { errMessage } from "../util/errors.ts";
const PROCESS_REAPER_LEASE_KEY = "processes:reaper";
@@ -66,7 +67,9 @@ export function createProcessReaper(registry: ProcessRegistry, opts: ProcessReap
if (!flipped) continue;
reaped++;
if (opts.onReaped)
- await opts.onReaped(rec).catch((err) => console.error("[process-reaper] onReaped hook failed:", err));
+ await opts
+ .onReaped(rec)
+ .catch((err) => console.error("[process-reaper] onReaped hook failed:", errMessage(err)));
}
return { reaped };
}
diff --git a/src/ratelimit/postgres-budget.ts b/src/ratelimit/postgres-budget.ts
index dde1f32ce..d09c8409e 100644
--- a/src/ratelimit/postgres-budget.ts
+++ b/src/ratelimit/postgres-budget.ts
@@ -1,6 +1,7 @@
import { createPgPool } from "../persistence/pg-pool.ts";
import type { BudgetTracker } from "./budget.ts";
import { DEFAULT_BUDGET_WINDOW_MS } from "./budget.ts";
+import { errMessage } from "../util/errors.ts";
export function createPostgresBudgetTracker(
connectionString: string,
@@ -45,7 +46,7 @@ export function createPostgresBudgetTracker(
);
}
} catch (err) {
- console.error("[budget] failed to persist spend:", err);
+ console.error("[budget] failed to persist spend:", errMessage(err));
}
},
};
diff --git a/src/resolution/config-store.ts b/src/resolution/config-store.ts
index 754afd0b7..66e029c72 100644
--- a/src/resolution/config-store.ts
+++ b/src/resolution/config-store.ts
@@ -19,6 +19,7 @@ import {
type PublicConnectorClient,
type DecryptedConnectorClient,
} from "../connectors/connector-client-store.ts";
+import { errMessage } from "../util/errors.ts";
export interface PersistedSoul {
scopeId: ScopeId;
@@ -267,7 +268,8 @@ export function createMemoryConfigStore(
const browseModelStore = opts.browseModels ?? createMemoryMap();
const turnWallClockStore = opts.turnWallClocks ?? createMemoryMap();
const deploymentIdentity = opts.deploymentIdentity ?? createMemoryMap();
- const persistWarn = (what: string) => (e: unknown) => console.error(`[config] failed to persist ${what}:`, e);
+ const persistWarn = (what: string) => (e: unknown) =>
+ console.error(`[config] failed to persist ${what}:`, errMessage(e));
const writeQueue = createKeyedQueue();
const pendingWrites = new Map>();
const persist = (key: string, what: string, op: () => Promise): void => {
@@ -285,7 +287,7 @@ export function createMemoryConfigStore(
try {
listener(id);
} catch (e) {
- console.error("[config] runtime-selection listener failed:", e);
+ console.error("[config] runtime-selection listener failed:", errMessage(e));
}
}
};
diff --git a/src/wiring.ts b/src/wiring.ts
index 83540e3d8..9f4d1860a 100644
--- a/src/wiring.ts
+++ b/src/wiring.ts
@@ -512,13 +512,13 @@ export function buildApp(
}
};
skillsReady = Promise.all([
- installCatalogs().catch((e) => console.error("[seed] failed to install seed skills:", e)),
- deploymentLayerReady.catch((e) => console.error("[seed] deployment layer not ready:", e)),
+ installCatalogs().catch((e) => console.error("[seed] failed to install seed skills:", errMessage(e))),
+ deploymentLayerReady.catch((e) => console.error("[seed] deployment layer not ready:", errMessage(e))),
]).then(() => undefined);
} else {
skillsReady = deploymentLayerReady.then(
() => undefined,
- (e) => console.error("[seed] deployment layer not ready:", e),
+ (e) => console.error("[seed] deployment layer not ready:", errMessage(e)),
);
}
const rateLimitOpts = { maxPerWindow: config.rateLimitPerWindow, windowMs: config.rateLimitWindowMs };
diff --git a/test/admin-scopes-directory.test.ts b/test/admin-scopes-directory.test.ts
index dcb280e50..b604750af 100644
--- a/test/admin-scopes-directory.test.ts
+++ b/test/admin-scopes-directory.test.ts
@@ -16,6 +16,8 @@ function start() {
admin: built.admin,
auditLog: built.auditLog,
sessions: built.sessions,
+ config: built.config,
+ environments: built.environments,
});
server.listen(0);
const base = `http://localhost:${(server.address() as AddressInfo).port}`;
@@ -118,3 +120,34 @@ test("scopes without a session label fall back to the org directory (people's na
await s.close();
}
});
+
+test("the admin scope directory exposes named environments and attached scopes", async () => {
+ const s = start();
+ try {
+ await s.built.app.upsertChannels([
+ { channelId: "A", name: "source" },
+ { channelId: "B", name: "attached" },
+ ]);
+ await s.built.app.createEnvironment({ scopeId: "channel:A", name: "A-permanent", actorId: "U1" });
+ await s.built.app.attachScope({ scopeId: "channel:B", environmentId: "channel:A", actorId: "U1" });
+
+ const directory = await json(await fetch(`${s.base}/v1/admin/scopes`, { headers: ALICE_ADMIN }));
+ const byId = new Map(directory.scopes.map((row: any) => [row.scopeId, row]));
+ assert.deepEqual(directory.environments, [
+ { id: "channel:A", name: "A-permanent", ownerActorId: "U1", attachedScopes: ["channel:B"] },
+ ]);
+ assert.equal((byId.get("channel:A") as any).environmentName, "A-permanent");
+ assert.deepEqual((byId.get("channel:B") as any).environmentAttachment, {
+ environmentId: "channel:A",
+ environmentName: "A-permanent",
+ });
+
+ const attached = await json(await fetch(`${s.base}/v1/admin/scopes/channel:B`, { headers: ALICE_ADMIN }));
+ assert.deepEqual(attached.environmentAttachment, {
+ environmentId: "channel:A",
+ environmentName: "A-permanent",
+ });
+ } finally {
+ await s.close();
+ }
+});