Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
36cfb17
feat(app): define durable local AI conversation contracts
NarwhalChen Jul 30, 2026
2ce7aa1
feat(app): persist native AI provider sessions
NarwhalChen Jul 30, 2026
4e1c426
feat(app): align chat lifecycle with native sessions
NarwhalChen Jul 30, 2026
03f9d5c
feat(app): add subscription-native memory curator
NarwhalChen Jul 30, 2026
b0bd83b
feat(app): implement Letta memory runtime
NarwhalChen Jul 30, 2026
973fd3b
fix(app): harden memory lifecycle transitions
NarwhalChen Jul 30, 2026
9c4d1ca
feat(app): wire memory into Electron runtime
NarwhalChen Jul 30, 2026
3aeec37
test(app): cover privileged local AI lifecycle APIs
NarwhalChen Jul 30, 2026
1834325
test(app): verify real Codex session persistence
NarwhalChen Jul 30, 2026
4dc8275
fix(app): prevent stale memory writes during teardown
NarwhalChen Jul 30, 2026
f74065a
fix(app): validate and protect session state
NarwhalChen Jul 30, 2026
0e5c2e7
fix(app): drain memory hooks before shutdown
NarwhalChen Jul 30, 2026
75688b6
fix(app): preserve authoritative chat shutdown
NarwhalChen Jul 30, 2026
8a7261f
fix(app): bound durable memory job history
NarwhalChen Jul 30, 2026
db99ae1
fix(app): bind sends to durable conversation state
NarwhalChen Jul 30, 2026
0c0e924
fix(app): recover Letta memory mutations durably
NarwhalChen Jul 30, 2026
b82d3e7
fix(app): quiesce subconscious memory work safely
NarwhalChen Jul 30, 2026
591c970
fix(app): close memory lifecycle consistency gaps
NarwhalChen Jul 30, 2026
5306db3
style(app): format local AI abort regression
NarwhalChen Jul 30, 2026
be1ee4a
fix(app): make Letta memory recovery source-safe
NarwhalChen Jul 30, 2026
06c40e1
fix(app): harden durable native session delivery
NarwhalChen Jul 30, 2026
4864101
fix(app): recover renderer conversation lifecycle
NarwhalChen Jul 30, 2026
04c9a09
feat(app): expose local-only memory settings
NarwhalChen Jul 31, 2026
e60e39c
feat(app): replace Letta with durable local memory
NarwhalChen Jul 31, 2026
9b8acaa
fix(app): enforce text-only memory curation
NarwhalChen Jul 31, 2026
e7121c9
merge: integrate local memory with multi-agent main
NarwhalChen Jul 31, 2026
885f633
test(app): cover local memory context transitions
NarwhalChen Jul 31, 2026
00d0ffd
chore(app): format merged multi-agent artifacts
NarwhalChen Jul 31, 2026
3661ac8
fix(app): normalize Electron automation binary path
NarwhalChen Jul 31, 2026
dd8a5a5
fix(app): keep standalone runtime cwd executable
NarwhalChen Jul 31, 2026
7ac1460
fix(app): reject unsupported automation runtimes
NarwhalChen Jul 31, 2026
28645d2
fix(app): stabilize local AI and automation startup
NarwhalChen Jul 31, 2026
9b569e4
test(app): add Off Local memory benchmark
NarwhalChen Jul 31, 2026
0366c12
chore(app): ignore automation artifacts in formatting
NarwhalChen Jul 31, 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
3 changes: 2 additions & 1 deletion packages/app/.prettierignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/dist
/coverage
/.automation
/.vite
.vite/
/out
Expand All @@ -11,4 +12,4 @@ README.md
template/**
template/

src/renderer/routes/routeTree.gen.ts
src/renderer/routes/routeTree.gen.ts
11 changes: 11 additions & 0 deletions packages/app/automation/check-runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { assertSupportedAutomationNodeVersion } from "./runtime.js";

try {
assertSupportedAutomationNodeVersion();
} catch (error) {
console.error(
"Cannot prepare Electron automation:",
error instanceof Error ? error.message : error,
);
process.exitCode = 1;
}
18 changes: 15 additions & 3 deletions packages/app/automation/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
import {
APP_ROOT,
automationProfilePath,
electronBinaryPath,
preparedChromedriverPath,
RUNTIME_DIR,
withAutomationLoopbackNoProxy,
WORKSPACE_ROOT,
} from "./runtime.js";

Expand All @@ -22,6 +24,11 @@ import {
* UND_ERR_INVALID_ARG. Install a real Undici dispatcher that also respects the
* user's proxy and NO_PROXY settings.
*/
const automationNoProxy = withAutomationLoopbackNoProxy(
process.env.NO_PROXY ?? process.env.no_proxy,
);
process.env.NO_PROXY = automationNoProxy;
process.env.no_proxy = automationNoProxy;
setGlobalDispatcher(new EnvHttpProxyAgent());

const DEFAULT_ENTRY_POINT = path.join(APP_ROOT, ".vite", "build", "main.js");
Expand Down Expand Up @@ -208,9 +215,14 @@ export class ConveraDriver {
appArgs.push(`--user-data-dir=${userDataPath}`);
}

const launchTarget = binaryPath
? { appBinaryPath: binaryPath }
: { appEntryPoint: entryPoint };
// electron-service's appEntryPoint conversion resolves
// node_modules/.bin/electron, which is a short-lived JavaScript launcher.
// Chromedriver must own the actual Electron executable or it reports the
// launcher's clean exit as a misleading user-data-directory lock.
if (!binaryPath) appArgs.unshift(`--app=${entryPoint}`);
const launchTarget = {
appBinaryPath: binaryPath ?? electronBinaryPath(),
};
const capabilities = Object.assign(
createElectronCapabilities({
...launchTarget,
Expand Down
48 changes: 48 additions & 0 deletions packages/app/automation/runtime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { existsSync } from "node:fs";
import { describe, expect, it } from "vitest";
import {
assertSupportedAutomationNodeVersion,
chromiumVersion,
electronBinaryPath,
normalizeElectronBinaryPath,
withAutomationLoopbackNoProxy,
} from "./runtime.js";

describe("automation runtime discovery", () => {
it("keeps every local WebDriver endpoint out of host proxies", () => {
expect(withAutomationLoopbackNoProxy("example.com,127.0.0.1")).toBe(
"example.com,127.0.0.1,localhost,0.0.0.0,::1",
);
expect(withAutomationLoopbackNoProxy("*")).toBe("*");
});

it("normalizes whitespace accidentally retained by Electron path metadata", () => {
expect(normalizeElectronBinaryPath(" /tmp/Electron\n")).toBe(
"/tmp/Electron",
);
expect(() => normalizeElectronBinaryPath(" \n ")).toThrow(
"Electron did not provide an executable path",
);
});

it("discovers an executable Electron and its Chromium version", () => {
expect(existsSync(electronBinaryPath())).toBe(true);
expect(chromiumVersion()).toMatch(/^\d+\.\d+\.\d+\.\d+$/);
});

it.each(["20.20.2", "22.18.0", "24.4.1"])(
"accepts supported Node %s",
(version) => {
expect(() => assertSupportedAutomationNodeVersion(version)).not.toThrow();
},
);

it.each(["19.9.0", "26.0.0", "invalid"])(
"rejects unsupported Node %s",
(version) => {
expect(() => assertSupportedAutomationNodeVersion(version)).toThrow(
/requires Node 20-24/,
);
},
);
});
45 changes: 40 additions & 5 deletions packages/app/automation/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,43 @@ export const RUNTIME_DIR = path.join(APP_ROOT, ".automation");

const require = createRequire(import.meta.url);

const LOOPBACK_NO_PROXY_HOSTS = [
"localhost",
"127.0.0.1",
"0.0.0.0",
"::1",
] as const;

export function withAutomationLoopbackNoProxy(value?: string): string {
const entries = (value ?? "")
.split(",")
.map((entry) => entry.trim())
.filter(Boolean);
if (entries.includes("*")) return "*";
return [...new Set([...entries, ...LOOPBACK_NO_PROXY_HOSTS])].join(",");
}

export function normalizeElectronBinaryPath(value: unknown): string {
if (typeof value !== "string" || value.trim().length === 0) {
throw new Error("Electron did not provide an executable path.");
}
return value.trim();
}

export function assertSupportedAutomationNodeVersion(
version = process.versions.node,
): void {
const major = Number.parseInt(version.split(".", 1)[0] ?? "", 10);
if (!Number.isInteger(major) || major < 20 || major >= 26) {
throw new Error(
`Electron automation packaging requires Node 20-24; received Node ${version}. ` +
"Electron Forge 7 can exit before writing the packaged app under Node 26.",
);
}
}

export function electronBinaryPath() {
return require("electron") as string;
return normalizeElectronBinaryPath(require("electron"));
}

export function automationProfilePath(profileId: string) {
Expand All @@ -37,11 +72,11 @@ export function chromiumVersion() {
env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" },
},
);
const version = result.stdout.trim();
const version = result.stdout?.trim() ?? "";
if (result.status !== 0 || !/^\d+\.\d+\.\d+\.\d+$/.test(version)) {
throw new Error(
`Could not read Chromium version from Electron: ${result.stderr.trim() || "unknown error"}`,
);
const detail =
result.error?.message || result.stderr?.trim() || "unknown error";
throw new Error(`Could not read Chromium version from Electron: ${detail}`);
}
return version;
}
Expand Down
Loading
Loading