Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
23db062
fix(storage): ignore retired artifact metadata
Astro-Han Sep 4, 2026
8ad070e
fix(runtime): return only child output artifacts
Astro-Han Sep 4, 2026
5eed3d6
refactor(core): remove unused artifact sources
Astro-Han Sep 4, 2026
cfc1b20
refactor(storage): remove retired capture sweep
Astro-Han Sep 4, 2026
3cdceb2
refactor(artifacts): replace tombstones with physical deletion
Astro-Han Sep 4, 2026
7178a4c
fix(desktop): allow deleting all artifacts
Astro-Han Sep 4, 2026
0ac5479
refactor(storage): remove self-managed artifact writes
Astro-Han Sep 4, 2026
82f4937
refactor(artifacts): remove dead change notifications
Astro-Han Sep 4, 2026
cb7e089
refactor(storage): retire artifact status metadata
Astro-Han Sep 4, 2026
1cccae2
refactor(storage): remove global artifact deletion
Astro-Han Sep 4, 2026
b0f7001
refactor(artifacts): require explicit source ownership
Astro-Han Sep 4, 2026
ab02d01
refactor(storage): use artifact id as metadata key
Astro-Han Sep 4, 2026
5e20eb8
refactor(storage): remove artifact metadata readiness shim
Astro-Han Sep 4, 2026
7a20ce2
refactor(storage): remove artifact first-read lock state
Astro-Han Sep 4, 2026
d748745
refactor(storage): remove artifact metadata interface
Astro-Han Sep 4, 2026
c22624a
refactor(storage): remove unscoped attachment reads
Astro-Han Sep 4, 2026
53db39d
refactor(storage): remove unscoped artifact reads
Astro-Han Sep 4, 2026
fd088b9
refactor(storage): remove artifact purge journal
Astro-Han Sep 4, 2026
6545bee
refactor(storage): remove artifact store facade type
Astro-Han Sep 4, 2026
ea50a50
fix(storage): discard invalid artifact metadata
Astro-Han Sep 4, 2026
96cd629
refactor(storage): remove artifact publication recovery
Astro-Han Sep 4, 2026
4eefcaa
refactor(storage): remove artifact recovery API
Astro-Han Sep 4, 2026
549fd70
refactor(storage): inline artifact lock layout
Astro-Han Sep 4, 2026
9160177
test(artifacts): align disposable residue gates
Astro-Han Sep 5, 2026
4ca24f0
test(desktop): use visible artifact story fixture
Astro-Han Sep 5, 2026
80c7b5e
fix(storage): preserve durable artifacts across v1 upgrades
Astro-Han Sep 5, 2026
06e24ac
fix(desktop): refresh visible artifact panes after publication
Astro-Han Sep 5, 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
2 changes: 1 addition & 1 deletion apps/desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ Sub-folders hold OS-facing implementations such as `browser/`, `computer-use/`,
Three patterns, all rooted in preload's `maka` namespace. Channel names are `<domain>:<action>`.

- **Request/response** — `ipcRenderer.invoke('<domain>:<action>', …args)` in preload ↔ `ipcMain.handle('<domain>:<action>', …)`. Runtime domains are projected by `runtime-host-*-ipc-main.ts`; OS-facing client domains use a focused `*-ipc-main.ts` module.
- **Main→renderer push** — main sends through the safe-send guard (`safeSendToRenderer` via `mainWindowController.send`), not raw `webContents.send` (which throws when the window/`webContents` is destroyed); preload subscribes via `ipcRenderer.on` and returns an unsubscribe fn (e.g. `sessions:changed`, `scheduled-tasks:changed`, `artifacts:changed`). The guard checks both the `BrowserWindow` and its `webContents` before delivery. Route every new main-window push through it.
- **Main→renderer push** — main sends through the safe-send guard (`safeSendToRenderer` via `mainWindowController.send`), not raw `webContents.send` (which throws when the window/`webContents` is destroyed); preload subscribes via `ipcRenderer.on` and returns an unsubscribe fn (e.g. `sessions:changed`, `scheduled-tasks:changed`). The guard checks both the `BrowserWindow` and its `webContents` before delivery. Route every new main-window push through it.
- **Renderer→main fire-and-forget** — `ipcRenderer.send('<domain>:<action>', …)` in preload ↔ `ipcMain.on('<domain>:<action>', …)`. Used when no response is needed (e.g. `browser:active-session`, `browser:setViewport`).

Adding a new IPC surface: if extracting, write the `*-ipc-main.ts` exporting a `register*Ipc(...)`, import it in `main.ts`, and call it inside `registerIpc()`; add the matching method to the `maka` namespace in `preload.ts`; add the method to the `window.maka` type in `src/global.d.ts` (the renderer's typed bridge — without it, renderer calls get a TS error); keep the `<domain>:<action>` channel naming. A handler file that isn't registered in `registerIpc()` compiles but never mounts.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ function attachmentReadHandler(
streamArtifact,
} as never,
mainWindowController: {} as never,
sendToRenderer() {},
showItemInFolder() {},
});
const handler = handlers.get("attachments:readBytes");
Expand All @@ -78,7 +77,6 @@ test("Runtime Host Artifact IPC preserves previews and streams complete exports"
const content = Buffer.alloc(70 * 1024, 5);
const handlers = new Map<string, Handler>();
const opened: string[] = [];
const events: unknown[] = [];
const artifact = {
id: "artifact-1",
sessionId: "session-1",
Expand All @@ -105,7 +103,7 @@ test("Runtime Host Artifact IPC preserves previews and streams complete exports"
return { ok: false, reason: "unsupported_mime" };
},
async deleteArtifact() {
return { kind: "deleted", artifact: { ...artifact, status: "deleted" } };
return { kind: "deleted" };
},
async streamArtifact(
_sessionId: string,
Expand All @@ -128,7 +126,6 @@ test("Runtime Host Artifact IPC preserves previews and streams complete exports"
mainWindowController: {
showSaveDialog: async () => ({ canceled: false, filePath: savedPath }),
} as never,
sendToRenderer: (_channel, event) => events.push(event),
showItemInFolder: (path) => opened.push(path),
presentationRoot,
});
Expand Down Expand Up @@ -159,7 +156,6 @@ test("Runtime Host Artifact IPC preserves previews and streams complete exports"
assert.deepEqual(await readFile(opened[0]!), content);

await handlers.get("artifacts:delete")?.({}, "session-1", "artifact-1");
assert.equal((events[0] as { reason: string }).reason, "deleted");
} finally {
await rm(root, { recursive: true, force: true });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ function createBridgeRecorder(): {
'browser.setViewport',
'browser.onState',
'browser.onLive',
'artifacts.subscribeChanges',
'inspector.subscribeUsageChanges',
]);
// Adapters that reshape a bridge answer need one to reshape.
Expand Down Expand Up @@ -137,11 +136,10 @@ describe('createDesktopWorkbarServices', () => {
services.browser.subscribeState(eventHandler)();
services.browser.subscribeLive(eventHandler)();

await services.artifacts.list('s', { includeDeleted: true });
await services.artifacts.list('s');
await services.artifacts.readText('s', 'a');
await services.artifacts.readBinary('s', 'a');
await services.artifacts.delete('s', 'a');
services.artifacts.subscribeChanges(eventHandler)();
await services.artifacts.openPath('s', 'a');
await services.artifacts.saveAs('s', 'a');

Expand Down Expand Up @@ -214,7 +212,6 @@ describe('createDesktopWorkbarServices', () => {
'artifacts.readText',
'artifacts.readBinary',
'artifacts.delete',
'artifacts.subscribeChanges',
'app.openArtifactPath',
'app.saveArtifactAs',
'inspector.trace',
Expand Down
32 changes: 5 additions & 27 deletions apps/desktop/src/main/runtime-host-artifacts-ipc-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ interface RuntimeHostArtifactsIpcDeps {
readonly ipcMain: ReconnectableReadIpcMain;
readonly client: DesktopRuntimeHostClient;
readonly mainWindowController: ReturnType<typeof createMainWindowController>;
readonly sendToRenderer: (channel: string, ...args: unknown[]) => void;
readonly showItemInFolder: (path: string) => void;
readonly presentationRoot?: string;
}
Expand All @@ -61,16 +60,7 @@ export function registerRuntimeHostArtifactsIpc(
handleReconnectableRead(
deps.ipcMain,
"artifacts:list",
async (
_event,
sessionId: string,
options?: { includeDeleted?: boolean },
) => {
const artifacts = await deps.client.listArtifacts(sessionId);
return options?.includeDeleted
? artifacts
: artifacts.filter(({ status }) => status !== "deleted");
},
(_event, sessionId: string) => deps.client.listArtifacts(sessionId),
);
handleReconnectableRead(
deps.ipcMain,
Expand All @@ -86,23 +76,15 @@ export function registerRuntimeHostArtifactsIpc(
);
deps.ipcMain.handle(
"artifacts:delete",
async (_event, sessionId: string, artifactId: string) => {
const result = await deps.client.deleteArtifact(sessionId, artifactId);
deps.sendToRenderer("artifacts:changed", {
reason: "deleted",
artifactId,
sessionId,
ts: Date.now(),
});
return result;
},
(_event, sessionId: string, artifactId: string) =>
deps.client.deleteArtifact(sessionId, artifactId),
);
registerRuntimeHostAttachmentPreviewIpc(deps);
deps.ipcMain.handle(
"app:openArtifactPath",
async (_event, sessionId: string, artifactId: string) => {
const artifact = await deps.client.getArtifact(sessionId, artifactId);
if (!artifact || artifact.status === "deleted") {
if (!artifact) {
return { ok: false as const, reason: "missing" as const };
}
try {
Expand All @@ -128,7 +110,6 @@ export function registerRuntimeHostArtifactsIpc(
): Promise<ArtifactSaveResult> => {
const artifact = await deps.client.getArtifact(sessionId, artifactId);
if (!artifact) return { ok: false, reason: "not_found" };
if (artifact.status === "deleted") return { ok: false, reason: "deleted" };
const result = await deps.mainWindowController.showSaveDialog({
title: `另存为 ${artifact.name}`,
defaultPath: artifact.name,
Expand Down Expand Up @@ -161,10 +142,7 @@ export function registerRuntimeHostAttachmentPreviewIpc(
"attachments:readBytes",
async (_event, sessionId: string, artifactId: string) => {
const artifact = await deps.client.getArtifact(sessionId, artifactId);
if (
!artifact ||
artifact.status === "deleted"
) {
if (!artifact) {
return { ok: false as const, reason: "not_found" };
}
const preview = resolveArtifactImagePreview(artifact);
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/main/runtime-host-boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1499,7 +1499,6 @@ function registerHostClientIpc(
ipcMain: scopedIpc,
client,
mainWindowController,
sendToRenderer,
showItemInFolder: (path) => shell.showItemInFolder(path),
});
registerRuntimeHostOAuthIpc({
Expand Down
4 changes: 1 addition & 3 deletions apps/desktop/src/preload/bridge-contract.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ import type {
} from '@maka/core/git-review';
import type {
ArtifactBinaryReadResult,
ArtifactChangedEvent,
ArtifactDescriptor,
ArtifactSaveResult,
ArtifactTextReadResult,
Expand Down Expand Up @@ -1810,11 +1809,10 @@ export interface MakaBridge {
getState(): Promise<E2eFixtureState | null>;
};
artifacts: {
list(sessionId: string, opts?: { includeDeleted?: boolean }): Promise<ArtifactDescriptor[]>;
list(sessionId: string): Promise<ArtifactDescriptor[]>;
readText(sessionId: string, artifactId: string): Promise<ArtifactTextReadResult>;
readBinary(sessionId: string, artifactId: string): Promise<ArtifactBinaryReadResult>;
delete(sessionId: string, artifactId: string): Promise<void>;
subscribeChanges(handler: (event: ArtifactChangedEvent) => void): () => void;
};
skills: {
list(host?: DesktopRuntimeHostRef): Promise<SkillEntry[]>;
Expand Down
13 changes: 2 additions & 11 deletions apps/desktop/src/preload/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ import type {
} from '@maka/core/git-review';
import type {
ArtifactBinaryReadResult,
ArtifactChangedEvent,
ArtifactDescriptor,
ArtifactSaveResult,
ArtifactTextReadResult,
Expand Down Expand Up @@ -3598,8 +3597,8 @@ const makaBridge = {
},
},
artifacts: {
list(sessionId: string, opts?: { includeDeleted?: boolean }): Promise<ArtifactDescriptor[]> {
return invokeProjectedSessionRuntimeHost('artifacts:list', sessionId, opts);
list(sessionId: string): Promise<ArtifactDescriptor[]> {
return invokeProjectedSessionRuntimeHost('artifacts:list', sessionId);
},
readText(sessionId: string, artifactId: string): Promise<ArtifactTextReadResult> {
return invokeSessionRuntimeHost('artifacts:readText', sessionId, artifactId);
Expand All @@ -3610,14 +3609,6 @@ const makaBridge = {
delete(sessionId: string, artifactId: string): Promise<void> {
return invokeSessionRuntimeHost('artifacts:delete', sessionId, artifactId);
},
subscribeChanges(handler: (event: ArtifactChangedEvent) => void): () => void {
return subscribeEveryRuntimeHostEvent('artifacts:changed', (scope, event: ArtifactChangedEvent) =>
handler({
...event,
sessionId: recordRuntimeHostSessionScope(scope, event.sessionId),
}),
);
},
},
skills: {
list(host?: DesktopRuntimeHostRef): Promise<SkillEntry[]> {
Expand Down
9 changes: 1 addition & 8 deletions apps/desktop/src/renderer/features/workbar/ports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import type {
} from '@maka/core/events';
import type {
ArtifactBinaryReadResult,
ArtifactChangedEvent,
ArtifactDescriptor,
ArtifactSaveResult,
ArtifactTextReadResult,
Expand Down Expand Up @@ -134,10 +133,7 @@ export type WorkbarOpenArtifactResult =
};

export interface WorkbarArtifactsService {
list(
sessionId: string,
options?: { includeDeleted?: boolean },
): Promise<ArtifactDescriptor[]>;
list(sessionId: string): Promise<ArtifactDescriptor[]>;
readText(
sessionId: string,
artifactId: string,
Expand All @@ -147,9 +143,6 @@ export interface WorkbarArtifactsService {
artifactId: string,
): Promise<ArtifactBinaryReadResult>;
delete(sessionId: string, artifactId: string): Promise<void>;
subscribeChanges(
handler: (event: ArtifactChangedEvent) => void,
): WorkbarUnsubscribe;
openPath(
sessionId: string,
artifactId: string,
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/renderer/features/workbar/testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ export function createFakeWorkbarServices(
readText: async () => ({ ok: false, reason: 'not_found' }),
readBinary: async () => ({ ok: false, reason: 'not_found' }),
delete: async () => undefined,
subscribeChanges: noopSubscription,
openPath: async () => ({ ok: false, reason: 'missing' }),
saveAs: async () => ({ ok: false, reason: 'canceled' }),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ import type { UiLocale } from '@maka/core/ui-locale';
import { formatRelativeTimestamp } from '@maka/core/relative-time';
import { generalizedErrorMessageForLocale, redactSecrets } from '@maka/core/redaction';
import {
Badge,
Banner,
Button,
MoreMenu,
Expand All @@ -84,6 +83,7 @@ import { useWorkbarServices } from '../../services-context.js';

export function ArtifactPane(props: {
sessionId: string;
active: boolean;
onCountChange?: (count: number) => void;
onDismiss?: () => void;
}) {
Expand Down Expand Up @@ -133,7 +133,7 @@ export function ArtifactPane(props: {
setSelectedId(null);
}, [sessionId]);

const refresh = useCallback(async () => {
const refresh = useCallback(async (showFailureToast = true) => {
const requestSeq = ++artifactListRequestSeqRef.current;
if (!sessionId) {
recordsSessionIdRef.current = undefined;
Expand All @@ -143,9 +143,7 @@ export function ArtifactPane(props: {
return;
}
try {
const next = await artifacts.list(sessionId, {
includeDeleted: true,
});
const next = await artifacts.list(sessionId);
if (artifactPaneMountedRef.current && requestSeq === artifactListRequestSeqRef.current) {
recordsSessionIdRef.current = sessionId;
setRecordsSessionId(sessionId);
Expand All @@ -160,30 +158,30 @@ export function ArtifactPane(props: {
recordsSessionIdRef.current = undefined;
setRecordsSessionId(undefined);
setRecords([]);
} else {
} else if (showFailureToast) {
toast.error(copy.pane.refreshFailed, message, undefined, { sessionId });
}
}
}
}, [artifacts, copy, locale, sessionId, toast]);

useEffect(() => {
void refresh();
if (!sessionId) return;
// Keep the list in sync without polling. The
// backend emits `{ reason: 'created' | 'deleted' | 'purged' }` on the
// `artifacts:changed` channel; we just re-list since the list is bounded
// (one session's worth) and the metadata is already in memory on main.
const unsubscribe = artifacts.subscribeChanges((event) => {
if (event.sessionId === sessionId) {
void refresh();
}
});
if (!props.active) return;
let stopped = false;
let timer: ReturnType<typeof setTimeout> | undefined;
// Writeback can commit after the terminal Session event. Read the existing
// catalog while visible instead of treating that event as a commit signal.
const poll = async () => {
await refresh(false);
if (!stopped) timer = setTimeout(() => void poll(), 2_000);
};
void poll();
return () => {
stopped = true;
clearTimeout(timer);
artifactListRequestSeqRef.current += 1;
unsubscribe();
};
}, [artifacts, sessionId, refresh]);
}, [props.active, sessionId, refresh]);

const activeRecords = useMemo(
() => (recordsSessionId === sessionId ? filterUserVisibleArtifacts(records) : []),
Expand All @@ -194,7 +192,6 @@ export function ArtifactPane(props: {
props.onCountChange?.(activeRecords.length);
}, [activeRecords.length, props.onCountChange]);

// 已删除墓碑记录保持可选,用于展示明确失败态;只有选中 id 彻底消失时才回退到最新 live artifact。
useEffect(() => {
if (activeRecords.length === 0) {
if (selectedId !== null) setSelectedId(null);
Expand Down Expand Up @@ -494,7 +491,6 @@ export function ArtifactPane(props: {
// ArrowUp/Down.
tabIndex={-1}
data-selected={record.id === selectedId ? 'true' : 'false'}
data-deleted={record.status === 'deleted' ? 'true' : 'false'}
onClick={() => openPreview(record.id)}
label={record.name}
icon={(
Expand All @@ -508,9 +504,6 @@ export function ArtifactPane(props: {
<span className="maka-artifact-row-time">
{formatRelativeTimestamp(record.createdAt, Date.now(), locale)}
</span>
{record.status === 'deleted' && (
<Badge variant="error" className="maka-artifact-row-badge" label={copy.pane.deletedBadge} />
)}
</span>
)}
/>
Expand Down Expand Up @@ -568,15 +561,8 @@ export function ArtifactPane(props: {
: []),
{ type: 'divider' as const },
{
label:
previewRecord.source === 'deep_research' ||
previewRecord.source === 'tool_result_archive'
? copy.pane.deleteReadOnly
: copy.pane.delete,
label: copy.pane.delete,
icon: <Trash2 size={ICON_SIZE.control} aria-hidden="true" />,
isDisabled:
previewRecord.source === 'deep_research' ||
previewRecord.source === 'tool_result_archive',
onClick: () => void runArtifactAction(
`${previewRecord.id}:delete`,
() => deleteArtifact(previewRecord.id),
Expand Down Expand Up @@ -619,8 +605,6 @@ function saveArtifactFailureCopy(reason: string, copy: ArtifactCopy): string {
return copy.pane.saveFailures.not_found;
case 'not_allowed':
return copy.pane.saveFailures.not_allowed;
case 'deleted':
return copy.pane.saveFailures.deleted;
case 'write_failed':
return copy.pane.saveFailures.write_failed;
default:
Expand Down Expand Up @@ -662,5 +646,5 @@ function KindIcon(props: { kind: ArtifactKind }) {
formatter cache. Removed; we import the shared helper. */

function preferredArtifactSelectionId(records: readonly ArtifactDescriptor[]): string | null {
return (records.find((record) => record.status !== 'deleted') ?? records[0])?.id ?? null;
return records[0]?.id ?? null;
}
Loading