Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fd3d174
feat(runtime-host): expose historical graph epochs
me2seeks Aug 13, 2026
8c17cc3
feat(desktop): browse historical agent graph runs
me2seeks Aug 13, 2026
50d2e74
feat(tui): inspect historical agent graph runs
me2seeks Aug 13, 2026
9564a49
fix(runtime): read the epoch page and current marker from one storage…
me2seeks Aug 18, 2026
22798f3
fix(runtime-host): harden graph history browsing at the client edges
me2seeks Aug 18, 2026
4b5bdae
fix(desktop): keep in-flight graph panel reads committable during not…
me2seeks Aug 18, 2026
6212969
fix(desktop): fence graph history selection state
me2seeks Aug 18, 2026
fa2daf4
fix(agent-graph): validate historical epoch queries
me2seeks Aug 19, 2026
1ce7687
fix(agent-graph): preserve retirement epoch cleanup
me2seeks Aug 19, 2026
8039139
feat(storage): persist selected graph result inputs
me2seeks Aug 13, 2026
fdca647
feat(runtime): admit results from earlier graph epochs
me2seeks Aug 13, 2026
6294782
feat(runtime-host): project graph result lineage
me2seeks Aug 13, 2026
8a0209d
fix(runtime): page historical graph result discovery
me2seeks Aug 13, 2026
46b321d
fix(runtime): bound selected graph result admission
me2seeks Aug 13, 2026
e878928
fix(runtime): preserve graph input provenance
me2seeks Aug 13, 2026
aaf8b8f
fix(runtime): isolate and cache historical result resolution per reco…
me2seeks Aug 18, 2026
5ca0359
ci: retrigger flaky desktop e2e
me2seeks Aug 18, 2026
ad0752e
ci: retrigger hosted-model lane
me2seeks Aug 18, 2026
8c00bdf
fix(runtime-host): cap combined current and historical graph inputs a…
me2seeks Aug 18, 2026
edd1d5c
fix(runtime): cache unresolved historical results
me2seeks Aug 18, 2026
31b9faf
fix(runtime): scope historical results to scheduled work
me2seeks Aug 18, 2026
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
224 changes: 215 additions & 9 deletions apps/desktop/src/main/__tests__/agent-graph-panel.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,10 +4,33 @@ import { parseHTML } from 'linkedom';
import { act, createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import type { AgentGraphClientSnapshot } from '@maka/runtime/stream-graph-read-model';
import type { AgentGraphEpochSummary } from '@maka/runtime-host/protocol';
import { AgentGraphPanel } from '../../renderer/agent-graph-panel.js';

type GraphListener = () => void;

interface DeferredRead {
started: Promise<void>;
release(): void;
}

interface DeferredReadGate extends DeferredRead {
markStarted(): void;
waitForRelease: Promise<void>;
}

function deferredReadGate(): DeferredReadGate {
let markStarted = () => {};
let release = () => {};
const started = new Promise<void>((resolve) => {
markStarted = resolve;
});
const waitForRelease = new Promise<void>((resolve) => {
release = resolve;
});
return { started, markStarted, release, waitForRelease };
}

const originalGlobals = {
document: globalThis.document,
window: globalThis.window,
Expand DownExpand Up@@ -59,11 +82,18 @@ function snapshot(
};
}

function installGraphRenderer(initial: AgentGraphClientSnapshot): {
function installGraphRenderer(
initial: AgentGraphClientSnapshot,
historical: readonly AgentGraphClientSnapshot[] = [],
): {
container: Element;
root: Root;
setSnapshot(next: AgentGraphClientSnapshot): Promise<void>;
evict(graphId: string): void;
renderSession(sessionId: string): Promise<void>;
holdNextEpochList(sessionId: string): DeferredRead;
holdNextSnapshot(graphId: string): DeferredRead;
stopCalls: string[];
} {
const { document, window } = parseHTML('<div id="root"></div>');
const matchMedia = (media: string) => ({
Expand All@@ -88,15 +118,54 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
IS_REACT_ACT_ENVIRONMENT: true,
});

const snapshots = new Map<string, AgentGraphClientSnapshot>([
[initial.rootSessionId, initial],
]);
const snapshots = new Map(
[initial, ...historical].map((entry) => [entry.graphId, entry] as const),
);
const currentGraphIds = new Map<string, string>();
for (const entry of [initial, ...historical]) {
if (!currentGraphIds.has(entry.rootSessionId)) {
currentGraphIds.set(entry.rootSessionId, entry.graphId);
}
}
const listeners = new Set<GraphListener>();
const epochListGates = new Map<string, DeferredReadGate>();
const snapshotGates = new Map<string, DeferredReadGate>();
const stopCalls: string[] = [];
(window as unknown as { maka: unknown }).maka = {
graphs: {
getSnapshot: async (sessionId: string) => {
const next = snapshots.get(sessionId);
if (!next) throw new Error(`missing graph snapshot for ${sessionId}`);
listEpochs: async (sessionId: string) => {
const gate = epochListGates.get(sessionId);
if (gate) {
epochListGates.delete(sessionId);
gate.markStarted();
await gate.waitForRelease;
}
const currentGraphId = currentGraphIds.get(sessionId);
const entries = [...snapshots.values()]
.filter((entry) => entry.rootSessionId === sessionId)
.sort((left, right) => Number(right.graphId === currentGraphId) - Number(left.graphId === currentGraphId));
return {
epochs: entries.map((entry, index) => ({
epoch: entries.length - index,
graphId: entry.graphId,
createdAt: index + 1,
current: currentGraphIds.get(sessionId) === entry.graphId,
})),
truncated: false,
};
},
getSnapshot: async (sessionId: string, options?: { graphId?: string }) => {
const graphId = options?.graphId ?? currentGraphIds.get(sessionId);
const gate = graphId ? snapshotGates.get(graphId) : undefined;
if (gate && graphId) {
snapshotGates.delete(graphId);
gate.markStarted();
await gate.waitForRelease;
}
const next = graphId ? snapshots.get(graphId) : undefined;
if (!next || next.rootSessionId !== sessionId) {
throw new Error(`missing graph snapshot for ${sessionId}`);
}
return next;
},
inspectOperator: async () => {
Expand All@@ -108,7 +177,9 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
listeners.delete(listener);
};
},
stop: async () => undefined,
stop: async (sessionId: string) => {
stopCalls.push(sessionId);
},
},
};

Expand All@@ -119,12 +190,16 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
container,
root,
async setSnapshot(next) {
snapshots.set(next.rootSessionId, next);
snapshots.set(next.graphId, next);
currentGraphIds.set(next.rootSessionId, next.graphId);
await act(async () => {
for (const listener of [...listeners]) listener();
await Promise.resolve();
});
},
evict(graphId) {
snapshots.delete(graphId);
},
async renderSession(sessionId) {
await act(async () => {
root.render(
Expand All@@ -138,6 +213,17 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
await Promise.resolve();
});
},
holdNextEpochList(sessionId) {
const gate = deferredReadGate();
epochListGates.set(sessionId, gate);
return gate;
},
holdNextSnapshot(graphId) {
const gate = deferredReadGate();
snapshotGates.set(graphId, gate);
return gate;
},
stopCalls,
};
}

Expand All@@ -160,6 +246,126 @@ async function renderPanel(
}

describe('AgentGraphPanel dismiss', () => {
it('keeps the new session loading when a disposed read settles later', async () => {
const sessionA = snapshot({ graphId: 'graph-a', status: 'active' });
const sessionB = snapshot({
graphId: 'graph-b',
status: 'active',
rootSessionId: 'session-2',
});
const harness = installGraphRenderer(sessionA, [sessionB]);
const readA = harness.holdNextEpochList('session-1');
await harness.renderSession('session-1');
await readA.started;

const readB = harness.holdNextEpochList('session-2');
await harness.renderSession('session-2');
await readB.started;
assert.match(harness.container.textContent ?? '', /Loading graph state/);

await act(async () => {
readA.release();
await Promise.resolve();
});
assert.match(harness.container.textContent ?? '', /Loading graph state/);

await act(async () => {
readB.release();
await Promise.resolve();
});
assert.match(harness.container.textContent ?? '', /Agent Graph/);
await act(async () => harness.root.unmount());
});

it('switches to a historical epoch without exposing current-graph controls', async () => {
const current = snapshot({ graphId: 'graph-2', status: 'active' });
const previous = snapshot({ graphId: 'graph-1', status: 'completed' });
const harness = installGraphRenderer(current, [previous]);
await act(async () => {
harness.root.render(
createElement(AgentGraphPanel, {
rootSessionId: 'session-1',
enabled: true,
locale: 'en',
onOpenSession: () => undefined,
}),
);
await Promise.resolve();
});

const selector = harness.container.querySelector('[role="combobox"]');
assert.ok(selector);
assert.match(harness.container.textContent ?? '', /Stop graph/);
await act(async () => {
(selector as HTMLElement).click();
await Promise.resolve();
});
const historyOption = [...document.querySelectorAll('[role="option"]')].find((option) =>
option.textContent?.includes('History'),
);
assert.ok(historyOption);
const historyRead = harness.holdNextSnapshot('graph-1');
await act(async () => {
(historyOption as HTMLElement).click();
await historyRead.started;
});

assert.match(harness.container.textContent ?? '', /#1 · History \(read-only\)/);
assert.doesNotMatch(harness.container.textContent ?? '', /Stop graph/);
assert.equal(harness.container.querySelector('.maka-agent-graph-dismiss'), null);
assert.deepEqual(harness.stopCalls, []);
await act(async () => {
historyRead.release();
await Promise.resolve();
});
await act(async () => harness.root.unmount());
});

it('resumes following the current epoch after the selected history expires', async () => {
const current = snapshot({ graphId: 'graph-2', status: 'active' });
const previous = snapshot({ graphId: 'graph-1', status: 'completed' });
const harness = installGraphRenderer(current, [previous]);
await act(async () => {
harness.root.render(
createElement(AgentGraphPanel, {
rootSessionId: 'session-1',
enabled: true,
locale: 'en',
onOpenSession: () => undefined,
}),
);
await Promise.resolve();
});

const selector = harness.container.querySelector('[role="combobox"]');
assert.ok(selector);
await act(async () => {
(selector as HTMLElement).click();
await Promise.resolve();
});
const historyOption = [...document.querySelectorAll('[role="option"]')].find((option) =>
option.textContent?.includes('History'),
);
assert.ok(historyOption);
await act(async () => {
(historyOption as HTMLElement).click();
await Promise.resolve();
});
const combobox = () => harness.container.querySelector('[role="combobox"]')?.textContent ?? '';
assert.match(combobox(), /#1 · History \(read-only\)/);

// The selected epoch leaves the bounded directory while the graph rolls over.
harness.evict('graph-1');
await harness.setSnapshot(snapshot({ graphId: 'graph-3', status: 'active' }));
assert.match(combobox(), /#2 · Current/);

// Follow restored: the next rollover refreshes the panel instead of
// staying pinned on the fallback graph.
await harness.setSnapshot(snapshot({ graphId: 'graph-4', status: 'waiting' }));
assert.match(combobox(), /#3 · Current/);
await act(async () => harness.root.unmount());
});

it('shows dismiss only after the graph has settled', async () => {
const active = await renderPanel(snapshot({ graphId: 'graph-1', status: 'active' }));
assert.ok(active.container.querySelector('.maka-agent-graph-panel'));
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fd3d174
feat(runtime-host): expose historical graph epochs
me2seeks Aug 13, 2026
8c17cc3
feat(desktop): browse historical agent graph runs
me2seeks Aug 13, 2026
50d2e74
feat(tui): inspect historical agent graph runs
me2seeks Aug 13, 2026
9564a49
fix(runtime): read the epoch page and current marker from one storage…
me2seeks Aug 18, 2026
22798f3
fix(runtime-host): harden graph history browsing at the client edges
me2seeks Aug 18, 2026
4b5bdae
fix(desktop): keep in-flight graph panel reads committable during not…
me2seeks Aug 18, 2026
6212969
fix(desktop): fence graph history selection state
me2seeks Aug 18, 2026
fa2daf4
fix(agent-graph): validate historical epoch queries
me2seeks Aug 19, 2026
1ce7687
fix(agent-graph): preserve retirement epoch cleanup
me2seeks Aug 19, 2026
8039139
feat(storage): persist selected graph result inputs
me2seeks Aug 13, 2026
fdca647
feat(runtime): admit results from earlier graph epochs
me2seeks Aug 13, 2026
6294782
feat(runtime-host): project graph result lineage
me2seeks Aug 13, 2026
8a0209d
fix(runtime): page historical graph result discovery
me2seeks Aug 13, 2026
46b321d
fix(runtime): bound selected graph result admission
me2seeks Aug 13, 2026
e878928
fix(runtime): preserve graph input provenance
me2seeks Aug 13, 2026
aaf8b8f
fix(runtime): isolate and cache historical result resolution per reco…
me2seeks Aug 18, 2026
5ca0359
ci: retrigger flaky desktop e2e
me2seeks Aug 18, 2026
ad0752e
ci: retrigger hosted-model lane
me2seeks Aug 18, 2026
8c00bdf
fix(runtime-host): cap combined current and historical graph inputs a…
me2seeks Aug 18, 2026
edd1d5c
fix(runtime): cache unresolved historical results
me2seeks Aug 18, 2026
31b9faf
fix(runtime): scope historical results to scheduled work
me2seeks Aug 18, 2026
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
224 changes: 215 additions & 9 deletions apps/desktop/src/main/__tests__/agent-graph-panel.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,10 +4,33 @@ import { parseHTML } from 'linkedom';
import { act, createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import type { AgentGraphClientSnapshot } from '@maka/runtime/stream-graph-read-model';
import type { AgentGraphEpochSummary } from '@maka/runtime-host/protocol';
import { AgentGraphPanel } from '../../renderer/agent-graph-panel.js';

type GraphListener = () => void;

interface DeferredRead {
started: Promise<void>;
release(): void;
}

interface DeferredReadGate extends DeferredRead {
markStarted(): void;
waitForRelease: Promise<void>;
}

function deferredReadGate(): DeferredReadGate {
let markStarted = () => {};
let release = () => {};
const started = new Promise<void>((resolve) => {
markStarted = resolve;
});
const waitForRelease = new Promise<void>((resolve) => {
release = resolve;
});
return { started, markStarted, release, waitForRelease };
}

const originalGlobals = {
document: globalThis.document,
window: globalThis.window,
Expand DownExpand Up@@ -59,11 +82,18 @@ function snapshot(
};
}

function installGraphRenderer(initial: AgentGraphClientSnapshot): {
function installGraphRenderer(
initial: AgentGraphClientSnapshot,
historical: readonly AgentGraphClientSnapshot[] = [],
): {
container: Element;
root: Root;
setSnapshot(next: AgentGraphClientSnapshot): Promise<void>;
evict(graphId: string): void;
renderSession(sessionId: string): Promise<void>;
holdNextEpochList(sessionId: string): DeferredRead;
holdNextSnapshot(graphId: string): DeferredRead;
stopCalls: string[];
} {
const { document, window } = parseHTML('<div id="root"></div>');
const matchMedia = (media: string) => ({
Expand All@@ -88,15 +118,54 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
IS_REACT_ACT_ENVIRONMENT: true,
});

const snapshots = new Map<string, AgentGraphClientSnapshot>([
[initial.rootSessionId, initial],
]);
const snapshots = new Map(
[initial, ...historical].map((entry) => [entry.graphId, entry] as const),
);
const currentGraphIds = new Map<string, string>();
for (const entry of [initial, ...historical]) {
if (!currentGraphIds.has(entry.rootSessionId)) {
currentGraphIds.set(entry.rootSessionId, entry.graphId);
}
}
const listeners = new Set<GraphListener>();
const epochListGates = new Map<string, DeferredReadGate>();
const snapshotGates = new Map<string, DeferredReadGate>();
const stopCalls: string[] = [];
(window as unknown as { maka: unknown }).maka = {
graphs: {
getSnapshot: async (sessionId: string) => {
const next = snapshots.get(sessionId);
if (!next) throw new Error(`missing graph snapshot for ${sessionId}`);
listEpochs: async (sessionId: string) => {
const gate = epochListGates.get(sessionId);
if (gate) {
epochListGates.delete(sessionId);
gate.markStarted();
await gate.waitForRelease;
}
const currentGraphId = currentGraphIds.get(sessionId);
const entries = [...snapshots.values()]
.filter((entry) => entry.rootSessionId === sessionId)
.sort((left, right) => Number(right.graphId === currentGraphId) - Number(left.graphId === currentGraphId));
return {
epochs: entries.map((entry, index) => ({
epoch: entries.length - index,
graphId: entry.graphId,
createdAt: index + 1,
current: currentGraphIds.get(sessionId) === entry.graphId,
})),
truncated: false,
};
},
getSnapshot: async (sessionId: string, options?: { graphId?: string }) => {
const graphId = options?.graphId ?? currentGraphIds.get(sessionId);
const gate = graphId ? snapshotGates.get(graphId) : undefined;
if (gate && graphId) {
snapshotGates.delete(graphId);
gate.markStarted();
await gate.waitForRelease;
}
const next = graphId ? snapshots.get(graphId) : undefined;
if (!next || next.rootSessionId !== sessionId) {
throw new Error(`missing graph snapshot for ${sessionId}`);
}
return next;
},
inspectOperator: async () => {
Expand All@@ -108,7 +177,9 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
listeners.delete(listener);
};
},
stop: async () => undefined,
stop: async (sessionId: string) => {
stopCalls.push(sessionId);
},
},
};

Expand All@@ -119,12 +190,16 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
container,
root,
async setSnapshot(next) {
snapshots.set(next.rootSessionId, next);
snapshots.set(next.graphId, next);
currentGraphIds.set(next.rootSessionId, next.graphId);
await act(async () => {
for (const listener of [...listeners]) listener();
await Promise.resolve();
});
},
evict(graphId) {
snapshots.delete(graphId);
},
async renderSession(sessionId) {
await act(async () => {
root.render(
Expand All@@ -138,6 +213,17 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
await Promise.resolve();
});
},
holdNextEpochList(sessionId) {
const gate = deferredReadGate();
epochListGates.set(sessionId, gate);
return gate;
},
holdNextSnapshot(graphId) {
const gate = deferredReadGate();
snapshotGates.set(graphId, gate);
return gate;
},
stopCalls,
};
}

Expand All@@ -160,6 +246,126 @@ async function renderPanel(
}

describe('AgentGraphPanel dismiss', () => {
it('keeps the new session loading when a disposed read settles later', async () => {
const sessionA = snapshot({ graphId: 'graph-a', status: 'active' });
const sessionB = snapshot({
graphId: 'graph-b',
status: 'active',
rootSessionId: 'session-2',
});
const harness = installGraphRenderer(sessionA, [sessionB]);
const readA = harness.holdNextEpochList('session-1');
await harness.renderSession('session-1');
await readA.started;

const readB = harness.holdNextEpochList('session-2');
await harness.renderSession('session-2');
await readB.started;
assert.match(harness.container.textContent ?? '', /Loading graph state/);

await act(async () => {
readA.release();
await Promise.resolve();
});
assert.match(harness.container.textContent ?? '', /Loading graph state/);

await act(async () => {
readB.release();
await Promise.resolve();
});
assert.match(harness.container.textContent ?? '', /Agent Graph/);
await act(async () => harness.root.unmount());
});

it('switches to a historical epoch without exposing current-graph controls', async () => {
const current = snapshot({ graphId: 'graph-2', status: 'active' });
const previous = snapshot({ graphId: 'graph-1', status: 'completed' });
const harness = installGraphRenderer(current, [previous]);
await act(async () => {
harness.root.render(
createElement(AgentGraphPanel, {
rootSessionId: 'session-1',
enabled: true,
locale: 'en',
onOpenSession: () => undefined,
}),
);
await Promise.resolve();
});

const selector = harness.container.querySelector('[role="combobox"]');
assert.ok(selector);
assert.match(harness.container.textContent ?? '', /Stop graph/);
await act(async () => {
(selector as HTMLElement).click();
await Promise.resolve();
});
const historyOption = [...document.querySelectorAll('[role="option"]')].find((option) =>
option.textContent?.includes('History'),
);
assert.ok(historyOption);
const historyRead = harness.holdNextSnapshot('graph-1');
await act(async () => {
(historyOption as HTMLElement).click();
await historyRead.started;
});

assert.match(harness.container.textContent ?? '', /#1 · History \(read-only\)/);
assert.doesNotMatch(harness.container.textContent ?? '', /Stop graph/);
assert.equal(harness.container.querySelector('.maka-agent-graph-dismiss'), null);
assert.deepEqual(harness.stopCalls, []);
await act(async () => {
historyRead.release();
await Promise.resolve();
});
await act(async () => harness.root.unmount());
});

it('resumes following the current epoch after the selected history expires', async () => {
const current = snapshot({ graphId: 'graph-2', status: 'active' });
const previous = snapshot({ graphId: 'graph-1', status: 'completed' });
const harness = installGraphRenderer(current, [previous]);
await act(async () => {
harness.root.render(
createElement(AgentGraphPanel, {
rootSessionId: 'session-1',
enabled: true,
locale: 'en',
onOpenSession: () => undefined,
}),
);
await Promise.resolve();
});

const selector = harness.container.querySelector('[role="combobox"]');
assert.ok(selector);
await act(async () => {
(selector as HTMLElement).click();
await Promise.resolve();
});
const historyOption = [...document.querySelectorAll('[role="option"]')].find((option) =>
option.textContent?.includes('History'),
);
assert.ok(historyOption);
await act(async () => {
(historyOption as HTMLElement).click();
await Promise.resolve();
});
const combobox = () => harness.container.querySelector('[role="combobox"]')?.textContent ?? '';
assert.match(combobox(), /#1 · History \(read-only\)/);

// The selected epoch leaves the bounded directory while the graph rolls over.
harness.evict('graph-1');
await harness.setSnapshot(snapshot({ graphId: 'graph-3', status: 'active' }));
assert.match(combobox(), /#2 · Current/);

// Follow restored: the next rollover refreshes the panel instead of
// staying pinned on the fallback graph.
await harness.setSnapshot(snapshot({ graphId: 'graph-4', status: 'waiting' }));
assert.match(combobox(), /#3 · Current/);
await act(async () => harness.root.unmount());
});

it('shows dismiss only after the graph has settled', async () => {
const active = await renderPanel(snapshot({ graphId: 'graph-1', status: 'active' }));
assert.ok(active.container.querySelector('.maka-agent-graph-panel'));
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fd3d174
feat(runtime-host): expose historical graph epochs
me2seeks Aug 13, 2026
8c17cc3
feat(desktop): browse historical agent graph runs
me2seeks Aug 13, 2026
50d2e74
feat(tui): inspect historical agent graph runs
me2seeks Aug 13, 2026
9564a49
fix(runtime): read the epoch page and current marker from one storage…
me2seeks Aug 18, 2026
22798f3
fix(runtime-host): harden graph history browsing at the client edges
me2seeks Aug 18, 2026
4b5bdae
fix(desktop): keep in-flight graph panel reads committable during not…
me2seeks Aug 18, 2026
6212969
fix(desktop): fence graph history selection state
me2seeks Aug 18, 2026
fa2daf4
fix(agent-graph): validate historical epoch queries
me2seeks Aug 19, 2026
1ce7687
fix(agent-graph): preserve retirement epoch cleanup
me2seeks Aug 19, 2026
8039139
feat(storage): persist selected graph result inputs
me2seeks Aug 13, 2026
fdca647
feat(runtime): admit results from earlier graph epochs
me2seeks Aug 13, 2026
6294782
feat(runtime-host): project graph result lineage
me2seeks Aug 13, 2026
8a0209d
fix(runtime): page historical graph result discovery
me2seeks Aug 13, 2026
46b321d
fix(runtime): bound selected graph result admission
me2seeks Aug 13, 2026
e878928
fix(runtime): preserve graph input provenance
me2seeks Aug 13, 2026
aaf8b8f
fix(runtime): isolate and cache historical result resolution per reco…
me2seeks Aug 18, 2026
5ca0359
ci: retrigger flaky desktop e2e
me2seeks Aug 18, 2026
ad0752e
ci: retrigger hosted-model lane
me2seeks Aug 18, 2026
8c00bdf
fix(runtime-host): cap combined current and historical graph inputs a…
me2seeks Aug 18, 2026
edd1d5c
fix(runtime): cache unresolved historical results
me2seeks Aug 18, 2026
31b9faf
fix(runtime): scope historical results to scheduled work
me2seeks Aug 18, 2026
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
224 changes: 215 additions & 9 deletions apps/desktop/src/main/__tests__/agent-graph-panel.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,10 +4,33 @@ import { parseHTML } from 'linkedom';
import { act, createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import type { AgentGraphClientSnapshot } from '@maka/runtime/stream-graph-read-model';
import type { AgentGraphEpochSummary } from '@maka/runtime-host/protocol';
import { AgentGraphPanel } from '../../renderer/agent-graph-panel.js';

type GraphListener = () => void;

interface DeferredRead {
started: Promise<void>;
release(): void;
}

interface DeferredReadGate extends DeferredRead {
markStarted(): void;
waitForRelease: Promise<void>;
}

function deferredReadGate(): DeferredReadGate {
let markStarted = () => {};
let release = () => {};
const started = new Promise<void>((resolve) => {
markStarted = resolve;
});
const waitForRelease = new Promise<void>((resolve) => {
release = resolve;
});
return { started, markStarted, release, waitForRelease };
}

const originalGlobals = {
document: globalThis.document,
window: globalThis.window,
Expand DownExpand Up@@ -59,11 +82,18 @@ function snapshot(
};
}

function installGraphRenderer(initial: AgentGraphClientSnapshot): {
function installGraphRenderer(
initial: AgentGraphClientSnapshot,
historical: readonly AgentGraphClientSnapshot[] = [],
): {
container: Element;
root: Root;
setSnapshot(next: AgentGraphClientSnapshot): Promise<void>;
evict(graphId: string): void;
renderSession(sessionId: string): Promise<void>;
holdNextEpochList(sessionId: string): DeferredRead;
holdNextSnapshot(graphId: string): DeferredRead;
stopCalls: string[];
} {
const { document, window } = parseHTML('<div id="root"></div>');
const matchMedia = (media: string) => ({
Expand All@@ -88,15 +118,54 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
IS_REACT_ACT_ENVIRONMENT: true,
});

const snapshots = new Map<string, AgentGraphClientSnapshot>([
[initial.rootSessionId, initial],
]);
const snapshots = new Map(
[initial, ...historical].map((entry) => [entry.graphId, entry] as const),
);
const currentGraphIds = new Map<string, string>();
for (const entry of [initial, ...historical]) {
if (!currentGraphIds.has(entry.rootSessionId)) {
currentGraphIds.set(entry.rootSessionId, entry.graphId);
}
}
const listeners = new Set<GraphListener>();
const epochListGates = new Map<string, DeferredReadGate>();
const snapshotGates = new Map<string, DeferredReadGate>();
const stopCalls: string[] = [];
(window as unknown as { maka: unknown }).maka = {
graphs: {
getSnapshot: async (sessionId: string) => {
const next = snapshots.get(sessionId);
if (!next) throw new Error(`missing graph snapshot for ${sessionId}`);
listEpochs: async (sessionId: string) => {
const gate = epochListGates.get(sessionId);
if (gate) {
epochListGates.delete(sessionId);
gate.markStarted();
await gate.waitForRelease;
}
const currentGraphId = currentGraphIds.get(sessionId);
const entries = [...snapshots.values()]
.filter((entry) => entry.rootSessionId === sessionId)
.sort((left, right) => Number(right.graphId === currentGraphId) - Number(left.graphId === currentGraphId));
return {
epochs: entries.map((entry, index) => ({
epoch: entries.length - index,
graphId: entry.graphId,
createdAt: index + 1,
current: currentGraphIds.get(sessionId) === entry.graphId,
})),
truncated: false,
};
},
getSnapshot: async (sessionId: string, options?: { graphId?: string }) => {
const graphId = options?.graphId ?? currentGraphIds.get(sessionId);
const gate = graphId ? snapshotGates.get(graphId) : undefined;
if (gate && graphId) {
snapshotGates.delete(graphId);
gate.markStarted();
await gate.waitForRelease;
}
const next = graphId ? snapshots.get(graphId) : undefined;
if (!next || next.rootSessionId !== sessionId) {
throw new Error(`missing graph snapshot for ${sessionId}`);
}
return next;
},
inspectOperator: async () => {
Expand All@@ -108,7 +177,9 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
listeners.delete(listener);
};
},
stop: async () => undefined,
stop: async (sessionId: string) => {
stopCalls.push(sessionId);
},
},
};

Expand All@@ -119,12 +190,16 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
container,
root,
async setSnapshot(next) {
snapshots.set(next.rootSessionId, next);
snapshots.set(next.graphId, next);
currentGraphIds.set(next.rootSessionId, next.graphId);
await act(async () => {
for (const listener of [...listeners]) listener();
await Promise.resolve();
});
},
evict(graphId) {
snapshots.delete(graphId);
},
async renderSession(sessionId) {
await act(async () => {
root.render(
Expand All@@ -138,6 +213,17 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
await Promise.resolve();
});
},
holdNextEpochList(sessionId) {
const gate = deferredReadGate();
epochListGates.set(sessionId, gate);
return gate;
},
holdNextSnapshot(graphId) {
const gate = deferredReadGate();
snapshotGates.set(graphId, gate);
return gate;
},
stopCalls,
};
}

Expand All@@ -160,6 +246,126 @@ async function renderPanel(
}

describe('AgentGraphPanel dismiss', () => {
it('keeps the new session loading when a disposed read settles later', async () => {
const sessionA = snapshot({ graphId: 'graph-a', status: 'active' });
const sessionB = snapshot({
graphId: 'graph-b',
status: 'active',
rootSessionId: 'session-2',
});
const harness = installGraphRenderer(sessionA, [sessionB]);
const readA = harness.holdNextEpochList('session-1');
await harness.renderSession('session-1');
await readA.started;

const readB = harness.holdNextEpochList('session-2');
await harness.renderSession('session-2');
await readB.started;
assert.match(harness.container.textContent ?? '', /Loading graph state/);

await act(async () => {
readA.release();
await Promise.resolve();
});
assert.match(harness.container.textContent ?? '', /Loading graph state/);

await act(async () => {
readB.release();
await Promise.resolve();
});
assert.match(harness.container.textContent ?? '', /Agent Graph/);
await act(async () => harness.root.unmount());
});

it('switches to a historical epoch without exposing current-graph controls', async () => {
const current = snapshot({ graphId: 'graph-2', status: 'active' });
const previous = snapshot({ graphId: 'graph-1', status: 'completed' });
const harness = installGraphRenderer(current, [previous]);
await act(async () => {
harness.root.render(
createElement(AgentGraphPanel, {
rootSessionId: 'session-1',
enabled: true,
locale: 'en',
onOpenSession: () => undefined,
}),
);
await Promise.resolve();
});

const selector = harness.container.querySelector('[role="combobox"]');
assert.ok(selector);
assert.match(harness.container.textContent ?? '', /Stop graph/);
await act(async () => {
(selector as HTMLElement).click();
await Promise.resolve();
});
const historyOption = [...document.querySelectorAll('[role="option"]')].find((option) =>
option.textContent?.includes('History'),
);
assert.ok(historyOption);
const historyRead = harness.holdNextSnapshot('graph-1');
await act(async () => {
(historyOption as HTMLElement).click();
await historyRead.started;
});

assert.match(harness.container.textContent ?? '', /#1 · History \(read-only\)/);
assert.doesNotMatch(harness.container.textContent ?? '', /Stop graph/);
assert.equal(harness.container.querySelector('.maka-agent-graph-dismiss'), null);
assert.deepEqual(harness.stopCalls, []);
await act(async () => {
historyRead.release();
await Promise.resolve();
});
await act(async () => harness.root.unmount());
});

it('resumes following the current epoch after the selected history expires', async () => {
const current = snapshot({ graphId: 'graph-2', status: 'active' });
const previous = snapshot({ graphId: 'graph-1', status: 'completed' });
const harness = installGraphRenderer(current, [previous]);
await act(async () => {
harness.root.render(
createElement(AgentGraphPanel, {
rootSessionId: 'session-1',
enabled: true,
locale: 'en',
onOpenSession: () => undefined,
}),
);
await Promise.resolve();
});

const selector = harness.container.querySelector('[role="combobox"]');
assert.ok(selector);
await act(async () => {
(selector as HTMLElement).click();
await Promise.resolve();
});
const historyOption = [...document.querySelectorAll('[role="option"]')].find((option) =>
option.textContent?.includes('History'),
);
assert.ok(historyOption);
await act(async () => {
(historyOption as HTMLElement).click();
await Promise.resolve();
});
const combobox = () => harness.container.querySelector('[role="combobox"]')?.textContent ?? '';
assert.match(combobox(), /#1 · History \(read-only\)/);

// The selected epoch leaves the bounded directory while the graph rolls over.
harness.evict('graph-1');
await harness.setSnapshot(snapshot({ graphId: 'graph-3', status: 'active' }));
assert.match(combobox(), /#2 · Current/);

// Follow restored: the next rollover refreshes the panel instead of
// staying pinned on the fallback graph.
await harness.setSnapshot(snapshot({ graphId: 'graph-4', status: 'waiting' }));
assert.match(combobox(), /#3 · Current/);
await act(async () => harness.root.unmount());
});

it('shows dismiss only after the graph has settled', async () => {
const active = await renderPanel(snapshot({ graphId: 'graph-1', status: 'active' }));
assert.ok(active.container.querySelector('.maka-agent-graph-panel'));
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fd3d174
feat(runtime-host): expose historical graph epochs
me2seeks Aug 13, 2026
8c17cc3
feat(desktop): browse historical agent graph runs
me2seeks Aug 13, 2026
50d2e74
feat(tui): inspect historical agent graph runs
me2seeks Aug 13, 2026
9564a49
fix(runtime): read the epoch page and current marker from one storage…
me2seeks Aug 18, 2026
22798f3
fix(runtime-host): harden graph history browsing at the client edges
me2seeks Aug 18, 2026
4b5bdae
fix(desktop): keep in-flight graph panel reads committable during not…
me2seeks Aug 18, 2026
6212969
fix(desktop): fence graph history selection state
me2seeks Aug 18, 2026
fa2daf4
fix(agent-graph): validate historical epoch queries
me2seeks Aug 19, 2026
1ce7687
fix(agent-graph): preserve retirement epoch cleanup
me2seeks Aug 19, 2026
8039139
feat(storage): persist selected graph result inputs
me2seeks Aug 13, 2026
fdca647
feat(runtime): admit results from earlier graph epochs
me2seeks Aug 13, 2026
6294782
feat(runtime-host): project graph result lineage
me2seeks Aug 13, 2026
8a0209d
fix(runtime): page historical graph result discovery
me2seeks Aug 13, 2026
46b321d
fix(runtime): bound selected graph result admission
me2seeks Aug 13, 2026
e878928
fix(runtime): preserve graph input provenance
me2seeks Aug 13, 2026
aaf8b8f
fix(runtime): isolate and cache historical result resolution per reco…
me2seeks Aug 18, 2026
5ca0359
ci: retrigger flaky desktop e2e
me2seeks Aug 18, 2026
ad0752e
ci: retrigger hosted-model lane
me2seeks Aug 18, 2026
8c00bdf
fix(runtime-host): cap combined current and historical graph inputs a…
me2seeks Aug 18, 2026
edd1d5c
fix(runtime): cache unresolved historical results
me2seeks Aug 18, 2026
31b9faf
fix(runtime): scope historical results to scheduled work
me2seeks Aug 18, 2026
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
224 changes: 215 additions & 9 deletions apps/desktop/src/main/__tests__/agent-graph-panel.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,10 +4,33 @@ import { parseHTML } from 'linkedom';
import { act, createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import type { AgentGraphClientSnapshot } from '@maka/runtime/stream-graph-read-model';
import type { AgentGraphEpochSummary } from '@maka/runtime-host/protocol';
import { AgentGraphPanel } from '../../renderer/agent-graph-panel.js';

type GraphListener = () => void;

interface DeferredRead {
started: Promise<void>;
release(): void;
}

interface DeferredReadGate extends DeferredRead {
markStarted(): void;
waitForRelease: Promise<void>;
}

function deferredReadGate(): DeferredReadGate {
let markStarted = () => {};
let release = () => {};
const started = new Promise<void>((resolve) => {
markStarted = resolve;
});
const waitForRelease = new Promise<void>((resolve) => {
release = resolve;
});
return { started, markStarted, release, waitForRelease };
}

const originalGlobals = {
document: globalThis.document,
window: globalThis.window,
Expand DownExpand Up@@ -59,11 +82,18 @@ function snapshot(
};
}

function installGraphRenderer(initial: AgentGraphClientSnapshot): {
function installGraphRenderer(
initial: AgentGraphClientSnapshot,
historical: readonly AgentGraphClientSnapshot[] = [],
): {
container: Element;
root: Root;
setSnapshot(next: AgentGraphClientSnapshot): Promise<void>;
evict(graphId: string): void;
renderSession(sessionId: string): Promise<void>;
holdNextEpochList(sessionId: string): DeferredRead;
holdNextSnapshot(graphId: string): DeferredRead;
stopCalls: string[];
} {
const { document, window } = parseHTML('<div id="root"></div>');
const matchMedia = (media: string) => ({
Expand All@@ -88,15 +118,54 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
IS_REACT_ACT_ENVIRONMENT: true,
});

const snapshots = new Map<string, AgentGraphClientSnapshot>([
[initial.rootSessionId, initial],
]);
const snapshots = new Map(
[initial, ...historical].map((entry) => [entry.graphId, entry] as const),
);
const currentGraphIds = new Map<string, string>();
for (const entry of [initial, ...historical]) {
if (!currentGraphIds.has(entry.rootSessionId)) {
currentGraphIds.set(entry.rootSessionId, entry.graphId);
}
}
const listeners = new Set<GraphListener>();
const epochListGates = new Map<string, DeferredReadGate>();
const snapshotGates = new Map<string, DeferredReadGate>();
const stopCalls: string[] = [];
(window as unknown as { maka: unknown }).maka = {
graphs: {
getSnapshot: async (sessionId: string) => {
const next = snapshots.get(sessionId);
if (!next) throw new Error(`missing graph snapshot for ${sessionId}`);
listEpochs: async (sessionId: string) => {
const gate = epochListGates.get(sessionId);
if (gate) {
epochListGates.delete(sessionId);
gate.markStarted();
await gate.waitForRelease;
}
const currentGraphId = currentGraphIds.get(sessionId);
const entries = [...snapshots.values()]
.filter((entry) => entry.rootSessionId === sessionId)
.sort((left, right) => Number(right.graphId === currentGraphId) - Number(left.graphId === currentGraphId));
return {
epochs: entries.map((entry, index) => ({
epoch: entries.length - index,
graphId: entry.graphId,
createdAt: index + 1,
current: currentGraphIds.get(sessionId) === entry.graphId,
})),
truncated: false,
};
},
getSnapshot: async (sessionId: string, options?: { graphId?: string }) => {
const graphId = options?.graphId ?? currentGraphIds.get(sessionId);
const gate = graphId ? snapshotGates.get(graphId) : undefined;
if (gate && graphId) {
snapshotGates.delete(graphId);
gate.markStarted();
await gate.waitForRelease;
}
const next = graphId ? snapshots.get(graphId) : undefined;
if (!next || next.rootSessionId !== sessionId) {
throw new Error(`missing graph snapshot for ${sessionId}`);
}
return next;
},
inspectOperator: async () => {
Expand All@@ -108,7 +177,9 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
listeners.delete(listener);
};
},
stop: async () => undefined,
stop: async (sessionId: string) => {
stopCalls.push(sessionId);
},
},
};

Expand All@@ -119,12 +190,16 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
container,
root,
async setSnapshot(next) {
snapshots.set(next.rootSessionId, next);
snapshots.set(next.graphId, next);
currentGraphIds.set(next.rootSessionId, next.graphId);
await act(async () => {
for (const listener of [...listeners]) listener();
await Promise.resolve();
});
},
evict(graphId) {
snapshots.delete(graphId);
},
async renderSession(sessionId) {
await act(async () => {
root.render(
Expand All@@ -138,6 +213,17 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
await Promise.resolve();
});
},
holdNextEpochList(sessionId) {
const gate = deferredReadGate();
epochListGates.set(sessionId, gate);
return gate;
},
holdNextSnapshot(graphId) {
const gate = deferredReadGate();
snapshotGates.set(graphId, gate);
return gate;
},
stopCalls,
};
}

Expand All@@ -160,6 +246,126 @@ async function renderPanel(
}

describe('AgentGraphPanel dismiss', () => {
it('keeps the new session loading when a disposed read settles later', async () => {
const sessionA = snapshot({ graphId: 'graph-a', status: 'active' });
const sessionB = snapshot({
graphId: 'graph-b',
status: 'active',
rootSessionId: 'session-2',
});
const harness = installGraphRenderer(sessionA, [sessionB]);
const readA = harness.holdNextEpochList('session-1');
await harness.renderSession('session-1');
await readA.started;

const readB = harness.holdNextEpochList('session-2');
await harness.renderSession('session-2');
await readB.started;
assert.match(harness.container.textContent ?? '', /Loading graph state/);

await act(async () => {
readA.release();
await Promise.resolve();
});
assert.match(harness.container.textContent ?? '', /Loading graph state/);

await act(async () => {
readB.release();
await Promise.resolve();
});
assert.match(harness.container.textContent ?? '', /Agent Graph/);
await act(async () => harness.root.unmount());
});

it('switches to a historical epoch without exposing current-graph controls', async () => {
const current = snapshot({ graphId: 'graph-2', status: 'active' });
const previous = snapshot({ graphId: 'graph-1', status: 'completed' });
const harness = installGraphRenderer(current, [previous]);
await act(async () => {
harness.root.render(
createElement(AgentGraphPanel, {
rootSessionId: 'session-1',
enabled: true,
locale: 'en',
onOpenSession: () => undefined,
}),
);
await Promise.resolve();
});

const selector = harness.container.querySelector('[role="combobox"]');
assert.ok(selector);
assert.match(harness.container.textContent ?? '', /Stop graph/);
await act(async () => {
(selector as HTMLElement).click();
await Promise.resolve();
});
const historyOption = [...document.querySelectorAll('[role="option"]')].find((option) =>
option.textContent?.includes('History'),
);
assert.ok(historyOption);
const historyRead = harness.holdNextSnapshot('graph-1');
await act(async () => {
(historyOption as HTMLElement).click();
await historyRead.started;
});

assert.match(harness.container.textContent ?? '', /#1 · History \(read-only\)/);
assert.doesNotMatch(harness.container.textContent ?? '', /Stop graph/);
assert.equal(harness.container.querySelector('.maka-agent-graph-dismiss'), null);
assert.deepEqual(harness.stopCalls, []);
await act(async () => {
historyRead.release();
await Promise.resolve();
});
await act(async () => harness.root.unmount());
});

it('resumes following the current epoch after the selected history expires', async () => {
const current = snapshot({ graphId: 'graph-2', status: 'active' });
const previous = snapshot({ graphId: 'graph-1', status: 'completed' });
const harness = installGraphRenderer(current, [previous]);
await act(async () => {
harness.root.render(
createElement(AgentGraphPanel, {
rootSessionId: 'session-1',
enabled: true,
locale: 'en',
onOpenSession: () => undefined,
}),
);
await Promise.resolve();
});

const selector = harness.container.querySelector('[role="combobox"]');
assert.ok(selector);
await act(async () => {
(selector as HTMLElement).click();
await Promise.resolve();
});
const historyOption = [...document.querySelectorAll('[role="option"]')].find((option) =>
option.textContent?.includes('History'),
);
assert.ok(historyOption);
await act(async () => {
(historyOption as HTMLElement).click();
await Promise.resolve();
});
const combobox = () => harness.container.querySelector('[role="combobox"]')?.textContent ?? '';
assert.match(combobox(), /#1 · History \(read-only\)/);

// The selected epoch leaves the bounded directory while the graph rolls over.
harness.evict('graph-1');
await harness.setSnapshot(snapshot({ graphId: 'graph-3', status: 'active' }));
assert.match(combobox(), /#2 · Current/);

// Follow restored: the next rollover refreshes the panel instead of
// staying pinned on the fallback graph.
await harness.setSnapshot(snapshot({ graphId: 'graph-4', status: 'waiting' }));
assert.match(combobox(), /#3 · Current/);
await act(async () => harness.root.unmount());
});

it('shows dismiss only after the graph has settled', async () => {
const active = await renderPanel(snapshot({ graphId: 'graph-1', status: 'active' }));
assert.ok(active.container.querySelector('.maka-agent-graph-panel'));
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fd3d174
feat(runtime-host): expose historical graph epochs
me2seeks Aug 13, 2026
8c17cc3
feat(desktop): browse historical agent graph runs
me2seeks Aug 13, 2026
50d2e74
feat(tui): inspect historical agent graph runs
me2seeks Aug 13, 2026
9564a49
fix(runtime): read the epoch page and current marker from one storage…
me2seeks Aug 18, 2026
22798f3
fix(runtime-host): harden graph history browsing at the client edges
me2seeks Aug 18, 2026
4b5bdae
fix(desktop): keep in-flight graph panel reads committable during not…
me2seeks Aug 18, 2026
6212969
fix(desktop): fence graph history selection state
me2seeks Aug 18, 2026
fa2daf4
fix(agent-graph): validate historical epoch queries
me2seeks Aug 19, 2026
1ce7687
fix(agent-graph): preserve retirement epoch cleanup
me2seeks Aug 19, 2026
8039139
feat(storage): persist selected graph result inputs
me2seeks Aug 13, 2026
fdca647
feat(runtime): admit results from earlier graph epochs
me2seeks Aug 13, 2026
6294782
feat(runtime-host): project graph result lineage
me2seeks Aug 13, 2026
8a0209d
fix(runtime): page historical graph result discovery
me2seeks Aug 13, 2026
46b321d
fix(runtime): bound selected graph result admission
me2seeks Aug 13, 2026
e878928
fix(runtime): preserve graph input provenance
me2seeks Aug 13, 2026
aaf8b8f
fix(runtime): isolate and cache historical result resolution per reco…
me2seeks Aug 18, 2026
5ca0359
ci: retrigger flaky desktop e2e
me2seeks Aug 18, 2026
ad0752e
ci: retrigger hosted-model lane
me2seeks Aug 18, 2026
8c00bdf
fix(runtime-host): cap combined current and historical graph inputs a…
me2seeks Aug 18, 2026
edd1d5c
fix(runtime): cache unresolved historical results
me2seeks Aug 18, 2026
31b9faf
fix(runtime): scope historical results to scheduled work
me2seeks Aug 18, 2026
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
224 changes: 215 additions & 9 deletions apps/desktop/src/main/__tests__/agent-graph-panel.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,10 +4,33 @@ import { parseHTML } from 'linkedom';
import { act, createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import type { AgentGraphClientSnapshot } from '@maka/runtime/stream-graph-read-model';
import type { AgentGraphEpochSummary } from '@maka/runtime-host/protocol';
import { AgentGraphPanel } from '../../renderer/agent-graph-panel.js';

type GraphListener = () => void;

interface DeferredRead {
started: Promise<void>;
release(): void;
}

interface DeferredReadGate extends DeferredRead {
markStarted(): void;
waitForRelease: Promise<void>;
}

function deferredReadGate(): DeferredReadGate {
let markStarted = () => {};
let release = () => {};
const started = new Promise<void>((resolve) => {
markStarted = resolve;
});
const waitForRelease = new Promise<void>((resolve) => {
release = resolve;
});
return { started, markStarted, release, waitForRelease };
}

const originalGlobals = {
document: globalThis.document,
window: globalThis.window,
Expand DownExpand Up@@ -59,11 +82,18 @@ function snapshot(
};
}

function installGraphRenderer(initial: AgentGraphClientSnapshot): {
function installGraphRenderer(
initial: AgentGraphClientSnapshot,
historical: readonly AgentGraphClientSnapshot[] = [],
): {
container: Element;
root: Root;
setSnapshot(next: AgentGraphClientSnapshot): Promise<void>;
evict(graphId: string): void;
renderSession(sessionId: string): Promise<void>;
holdNextEpochList(sessionId: string): DeferredRead;
holdNextSnapshot(graphId: string): DeferredRead;
stopCalls: string[];
} {
const { document, window } = parseHTML('<div id="root"></div>');
const matchMedia = (media: string) => ({
Expand All@@ -88,15 +118,54 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
IS_REACT_ACT_ENVIRONMENT: true,
});

const snapshots = new Map<string, AgentGraphClientSnapshot>([
[initial.rootSessionId, initial],
]);
const snapshots = new Map(
[initial, ...historical].map((entry) => [entry.graphId, entry] as const),
);
const currentGraphIds = new Map<string, string>();
for (const entry of [initial, ...historical]) {
if (!currentGraphIds.has(entry.rootSessionId)) {
currentGraphIds.set(entry.rootSessionId, entry.graphId);
}
}
const listeners = new Set<GraphListener>();
const epochListGates = new Map<string, DeferredReadGate>();
const snapshotGates = new Map<string, DeferredReadGate>();
const stopCalls: string[] = [];
(window as unknown as { maka: unknown }).maka = {
graphs: {
getSnapshot: async (sessionId: string) => {
const next = snapshots.get(sessionId);
if (!next) throw new Error(`missing graph snapshot for ${sessionId}`);
listEpochs: async (sessionId: string) => {
const gate = epochListGates.get(sessionId);
if (gate) {
epochListGates.delete(sessionId);
gate.markStarted();
await gate.waitForRelease;
}
const currentGraphId = currentGraphIds.get(sessionId);
const entries = [...snapshots.values()]
.filter((entry) => entry.rootSessionId === sessionId)
.sort((left, right) => Number(right.graphId === currentGraphId) - Number(left.graphId === currentGraphId));
return {
epochs: entries.map((entry, index) => ({
epoch: entries.length - index,
graphId: entry.graphId,
createdAt: index + 1,
current: currentGraphIds.get(sessionId) === entry.graphId,
})),
truncated: false,
};
},
getSnapshot: async (sessionId: string, options?: { graphId?: string }) => {
const graphId = options?.graphId ?? currentGraphIds.get(sessionId);
const gate = graphId ? snapshotGates.get(graphId) : undefined;
if (gate && graphId) {
snapshotGates.delete(graphId);
gate.markStarted();
await gate.waitForRelease;
}
const next = graphId ? snapshots.get(graphId) : undefined;
if (!next || next.rootSessionId !== sessionId) {
throw new Error(`missing graph snapshot for ${sessionId}`);
}
return next;
},
inspectOperator: async () => {
Expand All@@ -108,7 +177,9 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
listeners.delete(listener);
};
},
stop: async () => undefined,
stop: async (sessionId: string) => {
stopCalls.push(sessionId);
},
},
};

Expand All@@ -119,12 +190,16 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
container,
root,
async setSnapshot(next) {
snapshots.set(next.rootSessionId, next);
snapshots.set(next.graphId, next);
currentGraphIds.set(next.rootSessionId, next.graphId);
await act(async () => {
for (const listener of [...listeners]) listener();
await Promise.resolve();
});
},
evict(graphId) {
snapshots.delete(graphId);
},
async renderSession(sessionId) {
await act(async () => {
root.render(
Expand All@@ -138,6 +213,17 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
await Promise.resolve();
});
},
holdNextEpochList(sessionId) {
const gate = deferredReadGate();
epochListGates.set(sessionId, gate);
return gate;
},
holdNextSnapshot(graphId) {
const gate = deferredReadGate();
snapshotGates.set(graphId, gate);
return gate;
},
stopCalls,
};
}

Expand All@@ -160,6 +246,126 @@ async function renderPanel(
}

describe('AgentGraphPanel dismiss', () => {
it('keeps the new session loading when a disposed read settles later', async () => {
const sessionA = snapshot({ graphId: 'graph-a', status: 'active' });
const sessionB = snapshot({
graphId: 'graph-b',
status: 'active',
rootSessionId: 'session-2',
});
const harness = installGraphRenderer(sessionA, [sessionB]);
const readA = harness.holdNextEpochList('session-1');
await harness.renderSession('session-1');
await readA.started;

const readB = harness.holdNextEpochList('session-2');
await harness.renderSession('session-2');
await readB.started;
assert.match(harness.container.textContent ?? '', /Loading graph state/);

await act(async () => {
readA.release();
await Promise.resolve();
});
assert.match(harness.container.textContent ?? '', /Loading graph state/);

await act(async () => {
readB.release();
await Promise.resolve();
});
assert.match(harness.container.textContent ?? '', /Agent Graph/);
await act(async () => harness.root.unmount());
});

it('switches to a historical epoch without exposing current-graph controls', async () => {
const current = snapshot({ graphId: 'graph-2', status: 'active' });
const previous = snapshot({ graphId: 'graph-1', status: 'completed' });
const harness = installGraphRenderer(current, [previous]);
await act(async () => {
harness.root.render(
createElement(AgentGraphPanel, {
rootSessionId: 'session-1',
enabled: true,
locale: 'en',
onOpenSession: () => undefined,
}),
);
await Promise.resolve();
});

const selector = harness.container.querySelector('[role="combobox"]');
assert.ok(selector);
assert.match(harness.container.textContent ?? '', /Stop graph/);
await act(async () => {
(selector as HTMLElement).click();
await Promise.resolve();
});
const historyOption = [...document.querySelectorAll('[role="option"]')].find((option) =>
option.textContent?.includes('History'),
);
assert.ok(historyOption);
const historyRead = harness.holdNextSnapshot('graph-1');
await act(async () => {
(historyOption as HTMLElement).click();
await historyRead.started;
});

assert.match(harness.container.textContent ?? '', /#1 · History \(read-only\)/);
assert.doesNotMatch(harness.container.textContent ?? '', /Stop graph/);
assert.equal(harness.container.querySelector('.maka-agent-graph-dismiss'), null);
assert.deepEqual(harness.stopCalls, []);
await act(async () => {
historyRead.release();
await Promise.resolve();
});
await act(async () => harness.root.unmount());
});

it('resumes following the current epoch after the selected history expires', async () => {
const current = snapshot({ graphId: 'graph-2', status: 'active' });
const previous = snapshot({ graphId: 'graph-1', status: 'completed' });
const harness = installGraphRenderer(current, [previous]);
await act(async () => {
harness.root.render(
createElement(AgentGraphPanel, {
rootSessionId: 'session-1',
enabled: true,
locale: 'en',
onOpenSession: () => undefined,
}),
);
await Promise.resolve();
});

const selector = harness.container.querySelector('[role="combobox"]');
assert.ok(selector);
await act(async () => {
(selector as HTMLElement).click();
await Promise.resolve();
});
const historyOption = [...document.querySelectorAll('[role="option"]')].find((option) =>
option.textContent?.includes('History'),
);
assert.ok(historyOption);
await act(async () => {
(historyOption as HTMLElement).click();
await Promise.resolve();
});
const combobox = () => harness.container.querySelector('[role="combobox"]')?.textContent ?? '';
assert.match(combobox(), /#1 · History \(read-only\)/);

// The selected epoch leaves the bounded directory while the graph rolls over.
harness.evict('graph-1');
await harness.setSnapshot(snapshot({ graphId: 'graph-3', status: 'active' }));
assert.match(combobox(), /#2 · Current/);

// Follow restored: the next rollover refreshes the panel instead of
// staying pinned on the fallback graph.
await harness.setSnapshot(snapshot({ graphId: 'graph-4', status: 'waiting' }));
assert.match(combobox(), /#3 · Current/);
await act(async () => harness.root.unmount());
});

it('shows dismiss only after the graph has settled', async () => {
const active = await renderPanel(snapshot({ graphId: 'graph-1', status: 'active' }));
assert.ok(active.container.querySelector('.maka-agent-graph-panel'));
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fd3d174
feat(runtime-host): expose historical graph epochs
me2seeks Aug 13, 2026
8c17cc3
feat(desktop): browse historical agent graph runs
me2seeks Aug 13, 2026
50d2e74
feat(tui): inspect historical agent graph runs
me2seeks Aug 13, 2026
9564a49
fix(runtime): read the epoch page and current marker from one storage…
me2seeks Aug 18, 2026
22798f3
fix(runtime-host): harden graph history browsing at the client edges
me2seeks Aug 18, 2026
4b5bdae
fix(desktop): keep in-flight graph panel reads committable during not…
me2seeks Aug 18, 2026
6212969
fix(desktop): fence graph history selection state
me2seeks Aug 18, 2026
fa2daf4
fix(agent-graph): validate historical epoch queries
me2seeks Aug 19, 2026
1ce7687
fix(agent-graph): preserve retirement epoch cleanup
me2seeks Aug 19, 2026
8039139
feat(storage): persist selected graph result inputs
me2seeks Aug 13, 2026
fdca647
feat(runtime): admit results from earlier graph epochs
me2seeks Aug 13, 2026
6294782
feat(runtime-host): project graph result lineage
me2seeks Aug 13, 2026
8a0209d
fix(runtime): page historical graph result discovery
me2seeks Aug 13, 2026
46b321d
fix(runtime): bound selected graph result admission
me2seeks Aug 13, 2026
e878928
fix(runtime): preserve graph input provenance
me2seeks Aug 13, 2026
aaf8b8f
fix(runtime): isolate and cache historical result resolution per reco…
me2seeks Aug 18, 2026
5ca0359
ci: retrigger flaky desktop e2e
me2seeks Aug 18, 2026
ad0752e
ci: retrigger hosted-model lane
me2seeks Aug 18, 2026
8c00bdf
fix(runtime-host): cap combined current and historical graph inputs a…
me2seeks Aug 18, 2026
edd1d5c
fix(runtime): cache unresolved historical results
me2seeks Aug 18, 2026
31b9faf
fix(runtime): scope historical results to scheduled work
me2seeks Aug 18, 2026
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
224 changes: 215 additions & 9 deletions apps/desktop/src/main/__tests__/agent-graph-panel.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,10 +4,33 @@ import { parseHTML } from 'linkedom';
import { act, createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import type { AgentGraphClientSnapshot } from '@maka/runtime/stream-graph-read-model';
import type { AgentGraphEpochSummary } from '@maka/runtime-host/protocol';
import { AgentGraphPanel } from '../../renderer/agent-graph-panel.js';

type GraphListener = () => void;

interface DeferredRead {
started: Promise<void>;
release(): void;
}

interface DeferredReadGate extends DeferredRead {
markStarted(): void;
waitForRelease: Promise<void>;
}

function deferredReadGate(): DeferredReadGate {
let markStarted = () => {};
let release = () => {};
const started = new Promise<void>((resolve) => {
markStarted = resolve;
});
const waitForRelease = new Promise<void>((resolve) => {
release = resolve;
});
return { started, markStarted, release, waitForRelease };
}

const originalGlobals = {
document: globalThis.document,
window: globalThis.window,
Expand DownExpand Up@@ -59,11 +82,18 @@ function snapshot(
};
}

function installGraphRenderer(initial: AgentGraphClientSnapshot): {
function installGraphRenderer(
initial: AgentGraphClientSnapshot,
historical: readonly AgentGraphClientSnapshot[] = [],
): {
container: Element;
root: Root;
setSnapshot(next: AgentGraphClientSnapshot): Promise<void>;
evict(graphId: string): void;
renderSession(sessionId: string): Promise<void>;
holdNextEpochList(sessionId: string): DeferredRead;
holdNextSnapshot(graphId: string): DeferredRead;
stopCalls: string[];
} {
const { document, window } = parseHTML('<div id="root"></div>');
const matchMedia = (media: string) => ({
Expand All@@ -88,15 +118,54 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
IS_REACT_ACT_ENVIRONMENT: true,
});

const snapshots = new Map<string, AgentGraphClientSnapshot>([
[initial.rootSessionId, initial],
]);
const snapshots = new Map(
[initial, ...historical].map((entry) => [entry.graphId, entry] as const),
);
const currentGraphIds = new Map<string, string>();
for (const entry of [initial, ...historical]) {
if (!currentGraphIds.has(entry.rootSessionId)) {
currentGraphIds.set(entry.rootSessionId, entry.graphId);
}
}
const listeners = new Set<GraphListener>();
const epochListGates = new Map<string, DeferredReadGate>();
const snapshotGates = new Map<string, DeferredReadGate>();
const stopCalls: string[] = [];
(window as unknown as { maka: unknown }).maka = {
graphs: {
getSnapshot: async (sessionId: string) => {
const next = snapshots.get(sessionId);
if (!next) throw new Error(`missing graph snapshot for ${sessionId}`);
listEpochs: async (sessionId: string) => {
const gate = epochListGates.get(sessionId);
if (gate) {
epochListGates.delete(sessionId);
gate.markStarted();
await gate.waitForRelease;
}
const currentGraphId = currentGraphIds.get(sessionId);
const entries = [...snapshots.values()]
.filter((entry) => entry.rootSessionId === sessionId)
.sort((left, right) => Number(right.graphId === currentGraphId) - Number(left.graphId === currentGraphId));
return {
epochs: entries.map((entry, index) => ({
epoch: entries.length - index,
graphId: entry.graphId,
createdAt: index + 1,
current: currentGraphIds.get(sessionId) === entry.graphId,
})),
truncated: false,
};
},
getSnapshot: async (sessionId: string, options?: { graphId?: string }) => {
const graphId = options?.graphId ?? currentGraphIds.get(sessionId);
const gate = graphId ? snapshotGates.get(graphId) : undefined;
if (gate && graphId) {
snapshotGates.delete(graphId);
gate.markStarted();
await gate.waitForRelease;
}
const next = graphId ? snapshots.get(graphId) : undefined;
if (!next || next.rootSessionId !== sessionId) {
throw new Error(`missing graph snapshot for ${sessionId}`);
}
return next;
},
inspectOperator: async () => {
Expand All@@ -108,7 +177,9 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
listeners.delete(listener);
};
},
stop: async () => undefined,
stop: async (sessionId: string) => {
stopCalls.push(sessionId);
},
},
};

Expand All@@ -119,12 +190,16 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
container,
root,
async setSnapshot(next) {
snapshots.set(next.rootSessionId, next);
snapshots.set(next.graphId, next);
currentGraphIds.set(next.rootSessionId, next.graphId);
await act(async () => {
for (const listener of [...listeners]) listener();
await Promise.resolve();
});
},
evict(graphId) {
snapshots.delete(graphId);
},
async renderSession(sessionId) {
await act(async () => {
root.render(
Expand All@@ -138,6 +213,17 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
await Promise.resolve();
});
},
holdNextEpochList(sessionId) {
const gate = deferredReadGate();
epochListGates.set(sessionId, gate);
return gate;
},
holdNextSnapshot(graphId) {
const gate = deferredReadGate();
snapshotGates.set(graphId, gate);
return gate;
},
stopCalls,
};
}

Expand All@@ -160,6 +246,126 @@ async function renderPanel(
}

describe('AgentGraphPanel dismiss', () => {
it('keeps the new session loading when a disposed read settles later', async () => {
const sessionA = snapshot({ graphId: 'graph-a', status: 'active' });
const sessionB = snapshot({
graphId: 'graph-b',
status: 'active',
rootSessionId: 'session-2',
});
const harness = installGraphRenderer(sessionA, [sessionB]);
const readA = harness.holdNextEpochList('session-1');
await harness.renderSession('session-1');
await readA.started;

const readB = harness.holdNextEpochList('session-2');
await harness.renderSession('session-2');
await readB.started;
assert.match(harness.container.textContent ?? '', /Loading graph state/);

await act(async () => {
readA.release();
await Promise.resolve();
});
assert.match(harness.container.textContent ?? '', /Loading graph state/);

await act(async () => {
readB.release();
await Promise.resolve();
});
assert.match(harness.container.textContent ?? '', /Agent Graph/);
await act(async () => harness.root.unmount());
});

it('switches to a historical epoch without exposing current-graph controls', async () => {
const current = snapshot({ graphId: 'graph-2', status: 'active' });
const previous = snapshot({ graphId: 'graph-1', status: 'completed' });
const harness = installGraphRenderer(current, [previous]);
await act(async () => {
harness.root.render(
createElement(AgentGraphPanel, {
rootSessionId: 'session-1',
enabled: true,
locale: 'en',
onOpenSession: () => undefined,
}),
);
await Promise.resolve();
});

const selector = harness.container.querySelector('[role="combobox"]');
assert.ok(selector);
assert.match(harness.container.textContent ?? '', /Stop graph/);
await act(async () => {
(selector as HTMLElement).click();
await Promise.resolve();
});
const historyOption = [...document.querySelectorAll('[role="option"]')].find((option) =>
option.textContent?.includes('History'),
);
assert.ok(historyOption);
const historyRead = harness.holdNextSnapshot('graph-1');
await act(async () => {
(historyOption as HTMLElement).click();
await historyRead.started;
});

assert.match(harness.container.textContent ?? '', /#1 · History \(read-only\)/);
assert.doesNotMatch(harness.container.textContent ?? '', /Stop graph/);
assert.equal(harness.container.querySelector('.maka-agent-graph-dismiss'), null);
assert.deepEqual(harness.stopCalls, []);
await act(async () => {
historyRead.release();
await Promise.resolve();
});
await act(async () => harness.root.unmount());
});

it('resumes following the current epoch after the selected history expires', async () => {
const current = snapshot({ graphId: 'graph-2', status: 'active' });
const previous = snapshot({ graphId: 'graph-1', status: 'completed' });
const harness = installGraphRenderer(current, [previous]);
await act(async () => {
harness.root.render(
createElement(AgentGraphPanel, {
rootSessionId: 'session-1',
enabled: true,
locale: 'en',
onOpenSession: () => undefined,
}),
);
await Promise.resolve();
});

const selector = harness.container.querySelector('[role="combobox"]');
assert.ok(selector);
await act(async () => {
(selector as HTMLElement).click();
await Promise.resolve();
});
const historyOption = [...document.querySelectorAll('[role="option"]')].find((option) =>
option.textContent?.includes('History'),
);
assert.ok(historyOption);
await act(async () => {
(historyOption as HTMLElement).click();
await Promise.resolve();
});
const combobox = () => harness.container.querySelector('[role="combobox"]')?.textContent ?? '';
assert.match(combobox(), /#1 · History \(read-only\)/);

// The selected epoch leaves the bounded directory while the graph rolls over.
harness.evict('graph-1');
await harness.setSnapshot(snapshot({ graphId: 'graph-3', status: 'active' }));
assert.match(combobox(), /#2 · Current/);

// Follow restored: the next rollover refreshes the panel instead of
// staying pinned on the fallback graph.
await harness.setSnapshot(snapshot({ graphId: 'graph-4', status: 'waiting' }));
assert.match(combobox(), /#3 · Current/);
await act(async () => harness.root.unmount());
});

it('shows dismiss only after the graph has settled', async () => {
const active = await renderPanel(snapshot({ graphId: 'graph-1', status: 'active' }));
assert.ok(active.container.querySelector('.maka-agent-graph-panel'));
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fd3d174
feat(runtime-host): expose historical graph epochs
me2seeks Aug 13, 2026
8c17cc3
feat(desktop): browse historical agent graph runs
me2seeks Aug 13, 2026
50d2e74
feat(tui): inspect historical agent graph runs
me2seeks Aug 13, 2026
9564a49
fix(runtime): read the epoch page and current marker from one storage…
me2seeks Aug 18, 2026
22798f3
fix(runtime-host): harden graph history browsing at the client edges
me2seeks Aug 18, 2026
4b5bdae
fix(desktop): keep in-flight graph panel reads committable during not…
me2seeks Aug 18, 2026
6212969
fix(desktop): fence graph history selection state
me2seeks Aug 18, 2026
fa2daf4
fix(agent-graph): validate historical epoch queries
me2seeks Aug 19, 2026
1ce7687
fix(agent-graph): preserve retirement epoch cleanup
me2seeks Aug 19, 2026
8039139
feat(storage): persist selected graph result inputs
me2seeks Aug 13, 2026
fdca647
feat(runtime): admit results from earlier graph epochs
me2seeks Aug 13, 2026
6294782
feat(runtime-host): project graph result lineage
me2seeks Aug 13, 2026
8a0209d
fix(runtime): page historical graph result discovery
me2seeks Aug 13, 2026
46b321d
fix(runtime): bound selected graph result admission
me2seeks Aug 13, 2026
e878928
fix(runtime): preserve graph input provenance
me2seeks Aug 13, 2026
aaf8b8f
fix(runtime): isolate and cache historical result resolution per reco…
me2seeks Aug 18, 2026
5ca0359
ci: retrigger flaky desktop e2e
me2seeks Aug 18, 2026
ad0752e
ci: retrigger hosted-model lane
me2seeks Aug 18, 2026
8c00bdf
fix(runtime-host): cap combined current and historical graph inputs a…
me2seeks Aug 18, 2026
edd1d5c
fix(runtime): cache unresolved historical results
me2seeks Aug 18, 2026
31b9faf
fix(runtime): scope historical results to scheduled work
me2seeks Aug 18, 2026
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
224 changes: 215 additions & 9 deletions apps/desktop/src/main/__tests__/agent-graph-panel.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,10 +4,33 @@ import { parseHTML } from 'linkedom';
import { act, createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import type { AgentGraphClientSnapshot } from '@maka/runtime/stream-graph-read-model';
import type { AgentGraphEpochSummary } from '@maka/runtime-host/protocol';
import { AgentGraphPanel } from '../../renderer/agent-graph-panel.js';

type GraphListener = () => void;

interface DeferredRead {
started: Promise<void>;
release(): void;
}

interface DeferredReadGate extends DeferredRead {
markStarted(): void;
waitForRelease: Promise<void>;
}

function deferredReadGate(): DeferredReadGate {
let markStarted = () => {};
let release = () => {};
const started = new Promise<void>((resolve) => {
markStarted = resolve;
});
const waitForRelease = new Promise<void>((resolve) => {
release = resolve;
});
return { started, markStarted, release, waitForRelease };
}

const originalGlobals = {
document: globalThis.document,
window: globalThis.window,
Expand DownExpand Up@@ -59,11 +82,18 @@ function snapshot(
};
}

function installGraphRenderer(initial: AgentGraphClientSnapshot): {
function installGraphRenderer(
initial: AgentGraphClientSnapshot,
historical: readonly AgentGraphClientSnapshot[] = [],
): {
container: Element;
root: Root;
setSnapshot(next: AgentGraphClientSnapshot): Promise<void>;
evict(graphId: string): void;
renderSession(sessionId: string): Promise<void>;
holdNextEpochList(sessionId: string): DeferredRead;
holdNextSnapshot(graphId: string): DeferredRead;
stopCalls: string[];
} {
const { document, window } = parseHTML('<div id="root"></div>');
const matchMedia = (media: string) => ({
Expand All@@ -88,15 +118,54 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
IS_REACT_ACT_ENVIRONMENT: true,
});

const snapshots = new Map<string, AgentGraphClientSnapshot>([
[initial.rootSessionId, initial],
]);
const snapshots = new Map(
[initial, ...historical].map((entry) => [entry.graphId, entry] as const),
);
const currentGraphIds = new Map<string, string>();
for (const entry of [initial, ...historical]) {
if (!currentGraphIds.has(entry.rootSessionId)) {
currentGraphIds.set(entry.rootSessionId, entry.graphId);
}
}
const listeners = new Set<GraphListener>();
const epochListGates = new Map<string, DeferredReadGate>();
const snapshotGates = new Map<string, DeferredReadGate>();
const stopCalls: string[] = [];
(window as unknown as { maka: unknown }).maka = {
graphs: {
getSnapshot: async (sessionId: string) => {
const next = snapshots.get(sessionId);
if (!next) throw new Error(`missing graph snapshot for ${sessionId}`);
listEpochs: async (sessionId: string) => {
const gate = epochListGates.get(sessionId);
if (gate) {
epochListGates.delete(sessionId);
gate.markStarted();
await gate.waitForRelease;
}
const currentGraphId = currentGraphIds.get(sessionId);
const entries = [...snapshots.values()]
.filter((entry) => entry.rootSessionId === sessionId)
.sort((left, right) => Number(right.graphId === currentGraphId) - Number(left.graphId === currentGraphId));
return {
epochs: entries.map((entry, index) => ({
epoch: entries.length - index,
graphId: entry.graphId,
createdAt: index + 1,
current: currentGraphIds.get(sessionId) === entry.graphId,
})),
truncated: false,
};
},
getSnapshot: async (sessionId: string, options?: { graphId?: string }) => {
const graphId = options?.graphId ?? currentGraphIds.get(sessionId);
const gate = graphId ? snapshotGates.get(graphId) : undefined;
if (gate && graphId) {
snapshotGates.delete(graphId);
gate.markStarted();
await gate.waitForRelease;
}
const next = graphId ? snapshots.get(graphId) : undefined;
if (!next || next.rootSessionId !== sessionId) {
throw new Error(`missing graph snapshot for ${sessionId}`);
}
return next;
},
inspectOperator: async () => {
Expand All@@ -108,7 +177,9 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
listeners.delete(listener);
};
},
stop: async () => undefined,
stop: async (sessionId: string) => {
stopCalls.push(sessionId);
},
},
};

Expand All@@ -119,12 +190,16 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
container,
root,
async setSnapshot(next) {
snapshots.set(next.rootSessionId, next);
snapshots.set(next.graphId, next);
currentGraphIds.set(next.rootSessionId, next.graphId);
await act(async () => {
for (const listener of [...listeners]) listener();
await Promise.resolve();
});
},
evict(graphId) {
snapshots.delete(graphId);
},
async renderSession(sessionId) {
await act(async () => {
root.render(
Expand All@@ -138,6 +213,17 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
await Promise.resolve();
});
},
holdNextEpochList(sessionId) {
const gate = deferredReadGate();
epochListGates.set(sessionId, gate);
return gate;
},
holdNextSnapshot(graphId) {
const gate = deferredReadGate();
snapshotGates.set(graphId, gate);
return gate;
},
stopCalls,
};
}

Expand All@@ -160,6 +246,126 @@ async function renderPanel(
}

describe('AgentGraphPanel dismiss', () => {
it('keeps the new session loading when a disposed read settles later', async () => {
const sessionA = snapshot({ graphId: 'graph-a', status: 'active' });
const sessionB = snapshot({
graphId: 'graph-b',
status: 'active',
rootSessionId: 'session-2',
});
const harness = installGraphRenderer(sessionA, [sessionB]);
const readA = harness.holdNextEpochList('session-1');
await harness.renderSession('session-1');
await readA.started;

const readB = harness.holdNextEpochList('session-2');
await harness.renderSession('session-2');
await readB.started;
assert.match(harness.container.textContent ?? '', /Loading graph state/);

await act(async () => {
readA.release();
await Promise.resolve();
});
assert.match(harness.container.textContent ?? '', /Loading graph state/);

await act(async () => {
readB.release();
await Promise.resolve();
});
assert.match(harness.container.textContent ?? '', /Agent Graph/);
await act(async () => harness.root.unmount());
});

it('switches to a historical epoch without exposing current-graph controls', async () => {
const current = snapshot({ graphId: 'graph-2', status: 'active' });
const previous = snapshot({ graphId: 'graph-1', status: 'completed' });
const harness = installGraphRenderer(current, [previous]);
await act(async () => {
harness.root.render(
createElement(AgentGraphPanel, {
rootSessionId: 'session-1',
enabled: true,
locale: 'en',
onOpenSession: () => undefined,
}),
);
await Promise.resolve();
});

const selector = harness.container.querySelector('[role="combobox"]');
assert.ok(selector);
assert.match(harness.container.textContent ?? '', /Stop graph/);
await act(async () => {
(selector as HTMLElement).click();
await Promise.resolve();
});
const historyOption = [...document.querySelectorAll('[role="option"]')].find((option) =>
option.textContent?.includes('History'),
);
assert.ok(historyOption);
const historyRead = harness.holdNextSnapshot('graph-1');
await act(async () => {
(historyOption as HTMLElement).click();
await historyRead.started;
});

assert.match(harness.container.textContent ?? '', /#1 · History \(read-only\)/);
assert.doesNotMatch(harness.container.textContent ?? '', /Stop graph/);
assert.equal(harness.container.querySelector('.maka-agent-graph-dismiss'), null);
assert.deepEqual(harness.stopCalls, []);
await act(async () => {
historyRead.release();
await Promise.resolve();
});
await act(async () => harness.root.unmount());
});

it('resumes following the current epoch after the selected history expires', async () => {
const current = snapshot({ graphId: 'graph-2', status: 'active' });
const previous = snapshot({ graphId: 'graph-1', status: 'completed' });
const harness = installGraphRenderer(current, [previous]);
await act(async () => {
harness.root.render(
createElement(AgentGraphPanel, {
rootSessionId: 'session-1',
enabled: true,
locale: 'en',
onOpenSession: () => undefined,
}),
);
await Promise.resolve();
});

const selector = harness.container.querySelector('[role="combobox"]');
assert.ok(selector);
await act(async () => {
(selector as HTMLElement).click();
await Promise.resolve();
});
const historyOption = [...document.querySelectorAll('[role="option"]')].find((option) =>
option.textContent?.includes('History'),
);
assert.ok(historyOption);
await act(async () => {
(historyOption as HTMLElement).click();
await Promise.resolve();
});
const combobox = () => harness.container.querySelector('[role="combobox"]')?.textContent ?? '';
assert.match(combobox(), /#1 · History \(read-only\)/);

// The selected epoch leaves the bounded directory while the graph rolls over.
harness.evict('graph-1');
await harness.setSnapshot(snapshot({ graphId: 'graph-3', status: 'active' }));
assert.match(combobox(), /#2 · Current/);

// Follow restored: the next rollover refreshes the panel instead of
// staying pinned on the fallback graph.
await harness.setSnapshot(snapshot({ graphId: 'graph-4', status: 'waiting' }));
assert.match(combobox(), /#3 · Current/);
await act(async () => harness.root.unmount());
});

it('shows dismiss only after the graph has settled', async () => {
const active = await renderPanel(snapshot({ graphId: 'graph-1', status: 'active' }));
assert.ok(active.container.querySelector('.maka-agent-graph-panel'));
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fd3d174
feat(runtime-host): expose historical graph epochs
me2seeks Aug 13, 2026
8c17cc3
feat(desktop): browse historical agent graph runs
me2seeks Aug 13, 2026
50d2e74
feat(tui): inspect historical agent graph runs
me2seeks Aug 13, 2026
9564a49
fix(runtime): read the epoch page and current marker from one storage…
me2seeks Aug 18, 2026
22798f3
fix(runtime-host): harden graph history browsing at the client edges
me2seeks Aug 18, 2026
4b5bdae
fix(desktop): keep in-flight graph panel reads committable during not…
me2seeks Aug 18, 2026
6212969
fix(desktop): fence graph history selection state
me2seeks Aug 18, 2026
fa2daf4
fix(agent-graph): validate historical epoch queries
me2seeks Aug 19, 2026
1ce7687
fix(agent-graph): preserve retirement epoch cleanup
me2seeks Aug 19, 2026
8039139
feat(storage): persist selected graph result inputs
me2seeks Aug 13, 2026
fdca647
feat(runtime): admit results from earlier graph epochs
me2seeks Aug 13, 2026
6294782
feat(runtime-host): project graph result lineage
me2seeks Aug 13, 2026
8a0209d
fix(runtime): page historical graph result discovery
me2seeks Aug 13, 2026
46b321d
fix(runtime): bound selected graph result admission
me2seeks Aug 13, 2026
e878928
fix(runtime): preserve graph input provenance
me2seeks Aug 13, 2026
aaf8b8f
fix(runtime): isolate and cache historical result resolution per reco…
me2seeks Aug 18, 2026
5ca0359
ci: retrigger flaky desktop e2e
me2seeks Aug 18, 2026
ad0752e
ci: retrigger hosted-model lane
me2seeks Aug 18, 2026
8c00bdf
fix(runtime-host): cap combined current and historical graph inputs a…
me2seeks Aug 18, 2026
edd1d5c
fix(runtime): cache unresolved historical results
me2seeks Aug 18, 2026
31b9faf
fix(runtime): scope historical results to scheduled work
me2seeks Aug 18, 2026
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
224 changes: 215 additions & 9 deletions apps/desktop/src/main/__tests__/agent-graph-panel.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,10 +4,33 @@ import { parseHTML } from 'linkedom';
import { act, createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import type { AgentGraphClientSnapshot } from '@maka/runtime/stream-graph-read-model';
import type { AgentGraphEpochSummary } from '@maka/runtime-host/protocol';
import { AgentGraphPanel } from '../../renderer/agent-graph-panel.js';

type GraphListener = () => void;

interface DeferredRead {
started: Promise<void>;
release(): void;
}

interface DeferredReadGate extends DeferredRead {
markStarted(): void;
waitForRelease: Promise<void>;
}

function deferredReadGate(): DeferredReadGate {
let markStarted = () => {};
let release = () => {};
const started = new Promise<void>((resolve) => {
markStarted = resolve;
});
const waitForRelease = new Promise<void>((resolve) => {
release = resolve;
});
return { started, markStarted, release, waitForRelease };
}

const originalGlobals = {
document: globalThis.document,
window: globalThis.window,
Expand DownExpand Up@@ -59,11 +82,18 @@ function snapshot(
};
}

function installGraphRenderer(initial: AgentGraphClientSnapshot): {
function installGraphRenderer(
initial: AgentGraphClientSnapshot,
historical: readonly AgentGraphClientSnapshot[] = [],
): {
container: Element;
root: Root;
setSnapshot(next: AgentGraphClientSnapshot): Promise<void>;
evict(graphId: string): void;
renderSession(sessionId: string): Promise<void>;
holdNextEpochList(sessionId: string): DeferredRead;
holdNextSnapshot(graphId: string): DeferredRead;
stopCalls: string[];
} {
const { document, window } = parseHTML('<div id="root"></div>');
const matchMedia = (media: string) => ({
Expand All@@ -88,15 +118,54 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
IS_REACT_ACT_ENVIRONMENT: true,
});

const snapshots = new Map<string, AgentGraphClientSnapshot>([
[initial.rootSessionId, initial],
]);
const snapshots = new Map(
[initial, ...historical].map((entry) => [entry.graphId, entry] as const),
);
const currentGraphIds = new Map<string, string>();
for (const entry of [initial, ...historical]) {
if (!currentGraphIds.has(entry.rootSessionId)) {
currentGraphIds.set(entry.rootSessionId, entry.graphId);
}
}
const listeners = new Set<GraphListener>();
const epochListGates = new Map<string, DeferredReadGate>();
const snapshotGates = new Map<string, DeferredReadGate>();
const stopCalls: string[] = [];
(window as unknown as { maka: unknown }).maka = {
graphs: {
getSnapshot: async (sessionId: string) => {
const next = snapshots.get(sessionId);
if (!next) throw new Error(`missing graph snapshot for ${sessionId}`);
listEpochs: async (sessionId: string) => {
const gate = epochListGates.get(sessionId);
if (gate) {
epochListGates.delete(sessionId);
gate.markStarted();
await gate.waitForRelease;
}
const currentGraphId = currentGraphIds.get(sessionId);
const entries = [...snapshots.values()]
.filter((entry) => entry.rootSessionId === sessionId)
.sort((left, right) => Number(right.graphId === currentGraphId) - Number(left.graphId === currentGraphId));
return {
epochs: entries.map((entry, index) => ({
epoch: entries.length - index,
graphId: entry.graphId,
createdAt: index + 1,
current: currentGraphIds.get(sessionId) === entry.graphId,
})),
truncated: false,
};
},
getSnapshot: async (sessionId: string, options?: { graphId?: string }) => {
const graphId = options?.graphId ?? currentGraphIds.get(sessionId);
const gate = graphId ? snapshotGates.get(graphId) : undefined;
if (gate && graphId) {
snapshotGates.delete(graphId);
gate.markStarted();
await gate.waitForRelease;
}
const next = graphId ? snapshots.get(graphId) : undefined;
if (!next || next.rootSessionId !== sessionId) {
throw new Error(`missing graph snapshot for ${sessionId}`);
}
return next;
},
inspectOperator: async () => {
Expand All@@ -108,7 +177,9 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
listeners.delete(listener);
};
},
stop: async () => undefined,
stop: async (sessionId: string) => {
stopCalls.push(sessionId);
},
},
};

Expand All@@ -119,12 +190,16 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
container,
root,
async setSnapshot(next) {
snapshots.set(next.rootSessionId, next);
snapshots.set(next.graphId, next);
currentGraphIds.set(next.rootSessionId, next.graphId);
await act(async () => {
for (const listener of [...listeners]) listener();
await Promise.resolve();
});
},
evict(graphId) {
snapshots.delete(graphId);
},
async renderSession(sessionId) {
await act(async () => {
root.render(
Expand All@@ -138,6 +213,17 @@ function installGraphRenderer(initial: AgentGraphClientSnapshot): {
await Promise.resolve();
});
},
holdNextEpochList(sessionId) {
const gate = deferredReadGate();
epochListGates.set(sessionId, gate);
return gate;
},
holdNextSnapshot(graphId) {
const gate = deferredReadGate();
snapshotGates.set(graphId, gate);
return gate;
},
stopCalls,
};
}

Expand All@@ -160,6 +246,126 @@ async function renderPanel(
}

describe('AgentGraphPanel dismiss', () => {
it('keeps the new session loading when a disposed read settles later', async () => {
const sessionA = snapshot({ graphId: 'graph-a', status: 'active' });
const sessionB = snapshot({
graphId: 'graph-b',
status: 'active',
rootSessionId: 'session-2',
});
const harness = installGraphRenderer(sessionA, [sessionB]);
const readA = harness.holdNextEpochList('session-1');
await harness.renderSession('session-1');
await readA.started;

const readB = harness.holdNextEpochList('session-2');
await harness.renderSession('session-2');
await readB.started;
assert.match(harness.container.textContent ?? '', /Loading graph state/);

await act(async () => {
readA.release();
await Promise.resolve();
});
assert.match(harness.container.textContent ?? '', /Loading graph state/);

await act(async () => {
readB.release();
await Promise.resolve();
});
assert.match(harness.container.textContent ?? '', /Agent Graph/);
await act(async () => harness.root.unmount());
});

it('switches to a historical epoch without exposing current-graph controls', async () => {
const current = snapshot({ graphId: 'graph-2', status: 'active' });
const previous = snapshot({ graphId: 'graph-1', status: 'completed' });
const harness = installGraphRenderer(current, [previous]);
await act(async () => {
harness.root.render(
createElement(AgentGraphPanel, {
rootSessionId: 'session-1',
enabled: true,
locale: 'en',
onOpenSession: () => undefined,
}),
);
await Promise.resolve();
});

const selector = harness.container.querySelector('[role="combobox"]');
assert.ok(selector);
assert.match(harness.container.textContent ?? '', /Stop graph/);
await act(async () => {
(selector as HTMLElement).click();
await Promise.resolve();
});
const historyOption = [...document.querySelectorAll('[role="option"]')].find((option) =>
option.textContent?.includes('History'),
);
assert.ok(historyOption);
const historyRead = harness.holdNextSnapshot('graph-1');
await act(async () => {
(historyOption as HTMLElement).click();
await historyRead.started;
});

assert.match(harness.container.textContent ?? '', /#1 · History \(read-only\)/);
assert.doesNotMatch(harness.container.textContent ?? '', /Stop graph/);
assert.equal(harness.container.querySelector('.maka-agent-graph-dismiss'), null);
assert.deepEqual(harness.stopCalls, []);
await act(async () => {
historyRead.release();
await Promise.resolve();
});
await act(async () => harness.root.unmount());
});

it('resumes following the current epoch after the selected history expires', async () => {
const current = snapshot({ graphId: 'graph-2', status: 'active' });
const previous = snapshot({ graphId: 'graph-1', status: 'completed' });
const harness = installGraphRenderer(current, [previous]);
await act(async () => {
harness.root.render(
createElement(AgentGraphPanel, {
rootSessionId: 'session-1',
enabled: true,
locale: 'en',
onOpenSession: () => undefined,
}),
);
await Promise.resolve();
});

const selector = harness.container.querySelector('[role="combobox"]');
assert.ok(selector);
await act(async () => {
(selector as HTMLElement).click();
await Promise.resolve();
});
const historyOption = [...document.querySelectorAll('[role="option"]')].find((option) =>
option.textContent?.includes('History'),
);
assert.ok(historyOption);
await act(async () => {
(historyOption as HTMLElement).click();
await Promise.resolve();
});
const combobox = () => harness.container.querySelector('[role="combobox"]')?.textContent ?? '';
assert.match(combobox(), /#1 · History \(read-only\)/);

// The selected epoch leaves the bounded directory while the graph rolls over.
harness.evict('graph-1');
await harness.setSnapshot(snapshot({ graphId: 'graph-3', status: 'active' }));
assert.match(combobox(), /#2 · Current/);

// Follow restored: the next rollover refreshes the panel instead of
// staying pinned on the fallback graph.
await harness.setSnapshot(snapshot({ graphId: 'graph-4', status: 'waiting' }));
assert.match(combobox(), /#3 · Current/);
await act(async () => harness.root.unmount());
});

it('shows dismiss only after the graph has settled', async () => {
const active = await renderPanel(snapshot({ graphId: 'graph-1', status: 'active' }));
assert.ok(active.container.querySelector('.maka-agent-graph-panel'));
Expand Down
Loading
Loading