Merge bounded selection and verified cold CLI checks

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* codex/hermes-routines:
  fix(ui): bound manual agent selection to the switch transition
This commit is contained in:
DottaandPaperclip committed 2026-10-09 05:46:27 -05:00
commit ab98588317
4 files changed
+34 -5

No files matched your search

@@ -250,7 +250,9 @@ async function waitForServer(
output: { stdout: string[]; stderr: string[] },
) {
const startedAt = Date.now();
while (Date.now() - startedAt < 30_000) {
// The source CLI runs doctor and imports the cold server graph. Keep this
// health deadline inside the existing 60-second database/setup hook budget.
while (Date.now() - startedAt < 45_000) {
if (child.exitCode !== null) {
throw new Error(
`paperclipai run exited before healthcheck succeeded.\nstdout:\n${output.stdout.join("")}\nstderr:\n${output.stderr.join("")}`,
@@ -67,6 +67,12 @@ describe("buildImportTransferManifest", () => {
});
});
const CRC32_TABLE = Uint32Array.from({ length: 256 }, (_, value) => {
let crc = value;
for (let bit = 0; bit < 8; bit += 1) crc = (crc & 1) === 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1;
return crc >>> 0;
});
// Minimal single-entry DEFLATE zip, byte-compatible with the shared reader —
// the stored-zip helper cannot model a small-compressed/large-inflated entry.
function buildDeflateZip(entryPath: string, text: string): Uint8Array {
@@ -75,8 +81,7 @@ function buildDeflateZip(entryPath: string, text: string): Uint8Array {
const name = Buffer.from(entryPath, "utf8");
let crc = 0xffffffff;
for (const byte of raw) {
crc ^= byte;
for (let bit = 0; bit < 8; bit += 1) crc = (crc & 1) === 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1;
crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ byte) & 0xff]!;
}
crc = (crc ^ 0xffffffff) >>> 0;
const local = Buffer.alloc(30 + name.length);
@@ -1349,6 +1349,8 @@ test.describe("Exact failed chat run retry", () => {
await expect(page.getByText(foreignTitle, { exact: true }).first()).toBeVisible();
await expect.poll(() => page.evaluate(() => localStorage.getItem("paperclip.selectedCompanyId")))
.toBe(other.companyId);
const dismissAnnouncement = page.getByRole("button", { name: "Dismiss announcement", exact: true });
if (await dismissAnnouncement.isVisible()) await dismissAnnouncement.click();
await page.getByRole("button", { name: `Open ${foreignCompany.name} organization switcher`, exact: true }).click();
const menuItems = await page.getByRole("menuitem").allTextContents();
const scopedOption = page.getByRole("menuitem").filter({ hasText: scopedCompany.name });
@@ -1362,6 +1364,7 @@ test.describe("Exact failed chat run retry", () => {
afterSwitchSelectedCompany: await page.evaluate(() => localStorage.getItem("paperclip.selectedCompanyId")),
}, null, 2));
await expect(page).toHaveURL(new RegExp(`/${seed.prefix}/dashboard$`));
if (await dismissAnnouncement.isVisible()) await dismissAnnouncement.click();
await page.getByRole("link", { name: "Agents", exact: true }).first().click();
await page.getByRole("link", { name: "Maya", exact: true }).first().click();
await expect(page).toHaveURL(new RegExp(`/${seed.prefix}/agents/maya(?:/overview)?$`));
@@ -1379,6 +1382,20 @@ test.describe("Exact failed chat run retry", () => {
await expect.poll(() => page.evaluate(() => localStorage.getItem("paperclip.selectedCompanyId")))
.toBe(seed.companyId);
await page.screenshot({ path: testInfo.outputPath("cross-company-canonical-agent.png") });
// A manual selection must not prevent a later history navigation from
// following the authorized agent's company in the sidebar as well.
await page.goBack();
await expect(page).toHaveURL(new RegExp(`/${seed.prefix}/agents/all$`));
await page.goBack();
await expect(page).toHaveURL(new RegExp(`/${seed.prefix}/dashboard$`));
await page.goBack();
await expect(page).toHaveURL(new RegExp(`/${other.prefix}/agents/maya/overview$`));
await expect(page.getByText(foreignTitle, { exact: true }).first()).toBeVisible();
await expect.poll(() => page.evaluate(() => localStorage.getItem("paperclip.selectedCompanyId")))
.toBe(other.companyId);
await expect(page.getByRole("button", { name: `Open ${foreignCompany.name} organization switcher`, exact: true }))
.toBeVisible();
await page.screenshot({ path: testInfo.outputPath("cross-company-history-selection.png") });
} finally {
release();
}
+7 -2
View File
@@ -797,6 +797,7 @@ export function AgentDetail() {
const [configSaving, setConfigSaving] = useState(false);
const saveConfigActionRef = useRef<(() => void) | null>(null);
const cancelConfigActionRef = useRef<(() => void) | null>(null);
const previousSelectedCompanyId = useRef(selectedCompanyId);
const { isMobile } = useSidebar();
const routeAgentRef = agentId ?? "";
const routeCompanyId = useMemo(() => {
@@ -993,9 +994,13 @@ export function AgentDetail() {
}, [agent, agentCompany, companyPrefix, routeAgentRef, canonicalAgentRef, urlRunId, urlTab, activeView, legacyAuditSection, navigate, queryClient]);
useEffect(() => {
const manualSelectionChanged = selectionSource === "manual"
&& previousSelectedCompanyId.current !== selectedCompanyId;
previousSelectedCompanyId.current = selectedCompanyId;
if (!agent?.companyId || agent.companyId === selectedCompanyId) return;
// Let an explicit organization switch finish its navigation first.
if (selectionSource === "manual") return;
// Yield only for the selection change itself. Later agent links and history
// navigation must follow the authorized company even if the source is manual.
if (manualSelectionChanged) return;
// The route owns selection until canonical navigation reaches this company.
// Competing with Layout's old-prefix selection can cause an update loop.
if (routeCompanyId && routeCompanyId !== agent.companyId) return;