Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
1770808
fix(web): bound initial chat hydration
arul28 Jul 22, 2026
ac30fe7
fix(web): request invalidation-only sync
arul28 Jul 22, 2026
c02778e
fix(web): hide desktop connections panel
arul28 Jul 22, 2026
bd56ab0
fix(sync): prioritize browser chat hydration
arul28 Jul 22, 2026
df6e384
refactor(sync): share browser capability constant
arul28 Jul 22, 2026
08ddba4
fix(web): refresh relay leases in background
arul28 Jul 22, 2026
c28ce1b
refactor(sync): import shared capability directly
arul28 Jul 22, 2026
8b458b7
perf(chat): avoid duplicate transcript inflation
arul28 Jul 22, 2026
1e8b66a
Fix active sync host project catalog marker
arul28 Jul 22, 2026
a4a54b1
docs(web): document bounded invalidation sync
arul28 Jul 22, 2026
ee34129
Fix sync handoff cursor and peer fairness
arul28 Jul 22, 2026
2fd65ae
fix(web): bound chat streams and stabilize history paging
arul28 Jul 22, 2026
d40079d
fix(web): require invalidation sync acceptance
arul28 Jul 22, 2026
df210d1
docs(web): record resilient hydration contract
arul28 Jul 22, 2026
6c35f8d
perf(chat): compact transcript cursor cache
arul28 Jul 22, 2026
8a0a3b0
fix(sync): isolate peer transcript polling
arul28 Jul 22, 2026
15edfed
fix(web): isolate active project handoffs
arul28 Jul 22, 2026
57e4453
fix(web): reset chat dedup epochs
arul28 Jul 22, 2026
7ce40b2
fix(web): retire adopted project streams
arul28 Jul 22, 2026
4b65133
perf(sync): bound project catalog artwork
arul28 Jul 22, 2026
30027ac
Fix runtime starvation during project hydration
arul28 Jul 22, 2026
d9824c5
fix(chat): restore returned session controls
arul28 Jul 22, 2026
1de4b62
fix(sync): preserve verified relay routes
arul28 Jul 22, 2026
b0e41fb
fix(github): limit transient probe cooldown
arul28 Jul 22, 2026
3967e37
fix(sync): bound browser invalidation hints
arul28 Jul 22, 2026
320f8bc
perf(runtime): keep inactive project scopes cold
arul28 Jul 22, 2026
442de81
docs(sync): align project artwork budgets
arul28 Jul 22, 2026
18bac25
docs(ios): keep connection status route neutral
arul28 Jul 22, 2026
0b36a44
fix(runtime): harden deferred cleanup work
arul28 Jul 22, 2026
9aa34dc
fix(web): preserve hydration ordering under load
arul28 Jul 22, 2026
61d2d7d
fix(github): bound hosts token cache
arul28 Jul 22, 2026
ae46b84
fix(ios): preserve terminal data behind snapshots
arul28 Jul 22, 2026
937e2a2
fix(ios): finalize terminal snapshot recovery atomically
arul28 Jul 22, 2026
5675ef5
test(ios): reflect post-hello syncing lifecycle
arul28 Jul 22, 2026
370c48f
fix(ios): restore Work timeline tool grouping
arul28 Jul 22, 2026
2ee797f
fix runtime performance deadline layering
arul28 Jul 22, 2026
af91188
test(ios): align Work tool timeline expectations
arul28 Jul 22, 2026
d0189af
fix(web): pin visible chat subscriptions
arul28 Jul 22, 2026
6ea221e
fix(runtime): preserve cleanup and GitHub auth recovery
arul28 Jul 22, 2026
e4c6cf2
fix(runtime): harden sync host switch tail
arul28 Jul 22, 2026
d0f0c2f
fix(sync): await queued role transitions
arul28 Jul 22, 2026
3fc4ff4
perf(usage): aggregate daily stats in SQLite
arul28 Jul 22, 2026
51cae61
perf(usage): isolate historical ledger scans
arul28 Jul 22, 2026
ebabcc4
fix(work): preserve imported CLI launch state
arul28 Jul 22, 2026
efce194
docs(release): add v1.2.34 notes
arul28 Jul 22, 2026
db39d66
fix(deps): patch sync websocket runtime
arul28 Jul 22, 2026
046cbce
fix(runtime): prevent project registration stalls
arul28 Jul 22, 2026
88e800e
fix: harden project switching and usage worker packaging
arul28 Jul 22, 2026
05af0a1
fix: close packaging and continuation cache gaps
arul28 Jul 22, 2026
b39d902
fix: preserve Codex launch state after bounded scans
arul28 Jul 22, 2026
5400562
fix: preserve external session IPC overrides
arul28 Jul 22, 2026
a43c4ea
fix: fail closed on truncated Codex launch recovery
arul28 Jul 22, 2026
1df0a95
fix: normalize persisted Codex permission overrides
arul28 Jul 22, 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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ jobs:
apps/push-relay/node_modules
key: nm-v2-${{ hashFiles('apps/desktop/package-lock.json','apps/ade-cli/package-lock.json','apps/web/package-lock.json','apps/webhook-relay/package-lock.json','apps/push-relay/package-lock.json') }}
- run: cd apps/ade-cli && npm run typecheck
- name: Test release runtime archive guards
run: node --test apps/ade-cli/scripts/native-archive-verification.test.mjs apps/desktop/scripts/mac-runtime-archive-mode.test.mjs
- name: Test release runtime archive and packaging guards
run: node --test apps/ade-cli/scripts/native-archive-verification.test.mjs apps/desktop/scripts/mac-runtime-archive-mode.test.mjs apps/desktop/scripts/packaged-ade-cli-resources.test.mjs

typecheck-web:
needs: install
Expand Down
25 changes: 24 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.2.34] - 2026-07-22

### Web client and sync

- Bounded initial chat hydration, prioritized visible conversations, and replaced broad startup replay with invalidation-only synchronization.
- Isolated project handoff generations, retired abandoned streams, and bounded subscriptions, transcript cursors, artwork, invalidation hints, and inactive project scopes.
- Renewed relay leases in the background and preserved verified secure routes when cloud presence is temporarily stale.
- Kept peer changes, transcript polling, role transitions, and sync-host handoff progressing while foreground chats hydrate.
- Updated desktop and CLI WebSocket parsing to bound fragmented-frame memory use, and refreshed the hosted router and project YAML parser to their patched compatible releases.

### Terminals and mobile

- Made iOS terminal snapshot recovery atomic and retained live bytes that arrive while a snapshot is being applied.
- Restored grouped Work tool activity and stable post-hello synchronization state.

### Desktop and sessions

- Coalesced and bounded project switching, lane snapshots, cleanup work, GitHub probes, and runtime process discovery under load.
- Single-flighted initial project registration so concurrent terminal input cannot amplify runtime startup work or lose input ordering.
- Moved historical usage aggregation into SQLite and isolated workers to reduce main-process memory and event-loop pressure.
- Restored exact model, reasoning, fast-mode, and permission controls for imported and resumed Codex chats without broadening access.

## [1.2.33] - 2026-07-21

### Connection reliability
Expand Down Expand Up @@ -907,7 +929,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Initial public release.

[Unreleased]: https://github.com/arul28/ADE/compare/v1.2.33...HEAD
[Unreleased]: https://github.com/arul28/ADE/compare/v1.2.34...HEAD
[1.2.34]: https://github.com/arul28/ADE/compare/v1.2.33...v1.2.34
[1.2.33]: https://github.com/arul28/ADE/compare/v1.2.32...v1.2.33
[1.2.32]: https://github.com/arul28/ADE/compare/v1.2.31...v1.2.32
[1.2.31]: https://github.com/arul28/ADE/compare/v1.2.30...v1.2.31
Expand Down
52 changes: 49 additions & 3 deletions apps/account-directory/src/directory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type RegisterInput = {
deviceType: string;
pubkey: string | null;
reachableEndpoints: ReachableEndpoint[];
retainRelayEndpoints: boolean;
};

type MachineRecord = {
Expand Down Expand Up @@ -172,10 +173,29 @@ function parseRegisterInput(value: unknown): RegisterInput | null {
const deviceType = requiredString(value, "deviceType");
const pubkey = optionalString(value, "pubkey");
const reachableEndpoints = parseReachableEndpoints(value.reachableEndpoints);
if (!machineKey || !deviceId || !name || !platform || !deviceType || pubkey === undefined || !reachableEndpoints) {
const retainRelayEndpoints = value.retainRelayEndpoints ?? false;
if (
!machineKey
|| !deviceId
|| !name
|| !platform
|| !deviceType
|| pubkey === undefined
|| !reachableEndpoints
|| typeof retainRelayEndpoints !== "boolean"
) {
return null;
}
return { machineKey, deviceId, name, platform, deviceType, pubkey, reachableEndpoints };
return {
machineKey,
deviceId,
name,
platform,
deviceType,
pubkey,
reachableEndpoints,
retainRelayEndpoints,
};
}

function getRemoteJwks(rawUrl: string): ReturnType<typeof createRemoteJWKSet> {
Expand Down Expand Up @@ -331,7 +351,32 @@ async function handleRegister(request: Request, env: Env, userId: string): Promi
platform = excluded.platform,
device_type = excluded.device_type,
pubkey = excluded.pubkey,
reachable_endpoints = excluded.reachable_endpoints,
reachable_endpoints = case
when ? = 1
and json_valid(machines.reachable_endpoints)
and exists (
select 1
from json_each(machines.reachable_endpoints)
where json_extract(value, '$.kind') = 'relay'
)
and not exists (
select 1
from json_each(excluded.reachable_endpoints)
where json_extract(value, '$.kind') = 'relay'
)
then (
select json_group_array(json(endpoint))
from (
select value as endpoint
from json_each(excluded.reachable_endpoints)
union all
select value as endpoint
from json_each(machines.reachable_endpoints)
where json_extract(value, '$.kind') = 'relay'
)
)
else excluded.reachable_endpoints
end,
last_seen_at = excluded.last_seen_at
`).bind(
userId,
Expand All @@ -344,6 +389,7 @@ async function handleRegister(request: Request, env: Env, userId: string): Promi
JSON.stringify(input.reachableEndpoints),
now,
now,
input.retainRelayEndpoints ? 1 : 0,
).run();

const row = await env.DB.prepare(`
Expand Down
112 changes: 112 additions & 0 deletions apps/account-directory/test/directory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ class FakeD1Database {
run(sql: string, values: unknown[]): number {
const normalized = sql.toLowerCase();
if (normalized.includes("insert into machines")) {
const retainRelayEndpoints = values[10] === 1;
const row: StoredMachine = {
user_id: String(values[0]),
machine_key: String(values[1]),
Expand All @@ -169,6 +170,21 @@ class FakeD1Database {
entry.user_id === row.user_id && entry.machine_key === row.machine_key
);
if (existing) {
if (retainRelayEndpoints) {
const nextEndpoints = JSON.parse(row.reachable_endpoints ?? "[]") as Array<{ kind?: string }>;
const existingRelayEndpoints = (
JSON.parse(existing.reachable_endpoints ?? "[]") as Array<{ kind?: string }>
).filter((endpoint) => endpoint.kind === "relay");
if (
!nextEndpoints.some((endpoint) => endpoint.kind === "relay")
&& existingRelayEndpoints.length > 0
) {
row.reachable_endpoints = JSON.stringify([
...nextEndpoints,
...existingRelayEndpoints,
]);
}
}
Object.assign(existing, row, { created_at: existing.created_at });
} else {
this.rows.push(row);
Expand Down Expand Up @@ -431,6 +447,13 @@ function registerBody(machineKey: string, endpoints: unknown = [{ kind: "lan", h
};
}

function registrationWithRelayRetention(machineKey: string, endpoints: unknown) {
return {
...registerBody(machineKey, endpoints),
retainRelayEndpoints: true,
};
}

function request(
method: string,
pathname: string,
Expand Down Expand Up @@ -1031,6 +1054,95 @@ describe("machine directory", () => {
expect(await otherUserList.json()).toEqual({ machines: [] });
});

it("retains the authenticated machine's verified Relay route during a transient health dip", async () => {
const env = makeEnv();
const token = await mintToken({ sub: "user_1" });
const relayEndpoint = { kind: "relay", url: "wss://relay.test/machine-a" };
await register(env, token, "machine-a", [
{ kind: "lan", host: "old.local", port: 8787 },
relayEndpoint,
]);

const transient = await handleRequest(request(
"POST",
"/account/machines/register",
token,
registrationWithRelayRetention("machine-a", [
{ kind: "lan", host: "new.local", port: 8787 },
]),
), env);

expect(transient.status).toBe(200);
expect(await transient.json()).toEqual(expect.objectContaining({
machineKey: "machine-a",
reachableEndpoints: [
{ kind: "lan", host: "new.local", port: 8787 },
relayEndpoint,
],
}));
});

it("never retains Relay routes across owners, deletion, or an authoritative replacement", async () => {
const env = makeEnv();
const firstToken = await mintToken({ sub: "user_1" });
const secondToken = await mintToken({ sub: "user_2" });
const relayEndpoint = { kind: "relay", url: "wss://relay.test/shared-machine" };
await register(env, firstToken, "shared-machine", [relayEndpoint]);

const otherOwner = await handleRequest(request(
"POST",
"/account/machines/register",
secondToken,
registrationWithRelayRetention("shared-machine", [
{ kind: "lan", host: "second.local", port: 8787 },
]),
), env);
expect(await otherOwner.json()).toEqual(expect.objectContaining({
reachableEndpoints: [{ kind: "lan", host: "second.local", port: 8787 }],
}));

const authoritative = await register(env, firstToken, "shared-machine", [
{ kind: "lan", host: "first.local", port: 8787 },
]);
expect(await authoritative.json()).toEqual(expect.objectContaining({
reachableEndpoints: [{ kind: "lan", host: "first.local", port: 8787 }],
}));

await handleRequest(request(
"DELETE",
"/account/machines/shared-machine",
firstToken,
), env);
const afterDelete = await handleRequest(request(
"POST",
"/account/machines/register",
firstToken,
registrationWithRelayRetention("shared-machine", [
{ kind: "lan", host: "after-delete.local", port: 8787 },
]),
), env);
expect(await afterDelete.json()).toEqual(expect.objectContaining({
reachableEndpoints: [{ kind: "lan", host: "after-delete.local", port: 8787 }],
}));
});

it("rejects a non-boolean Relay-retention instruction", async () => {
const env = makeEnv();
const token = await mintToken();
const response = await handleRequest(request(
"POST",
"/account/machines/register",
token,
{
...registerBody("machine-a"),
retainRelayEndpoints: "yes",
},
), env);

expect(response.status).toBe(400);
expect(await response.json()).toEqual({ error: "invalid request body" });
});

it("returns online and offline machines, online first and newest first", async () => {
const env = makeEnv();
const token = await mintToken({ sub: "user_1" });
Expand Down
25 changes: 13 additions & 12 deletions apps/ade-cli/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 23 additions & 5 deletions apps/ade-cli/scripts/verify-built-cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import packagedAdeCliResourcesModule from "../../desktop/scripts/packaged-ade-cli-resources.cjs";

const execFileAsync = promisify(execFile);
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const distRoot = path.join(packageRoot, "dist");
const cliPath = path.join(packageRoot, "dist", "cli.cjs");
const bundledRuntimeEntryPaths = [
cliPath,
path.join(packageRoot, "dist", "bootstrap.cjs"),
path.join(packageRoot, "dist", "adeRpcServer.cjs"),
];
const { packagedAdeCliBuildResources, sourceContainsPath } = packagedAdeCliResourcesModule;
const bundledRuntimeEntryPaths = (await fs.readdir(distRoot, { withFileTypes: true }))
.filter((entry) => entry.isFile() && entry.name.endsWith(".cjs"))
.map((entry) => path.join(distRoot, entry.name));
const tuiPath = path.join(packageRoot, "dist", "tuiClient", "cli.mjs");
const packageJsonPath = path.join(packageRoot, "package.json");

Expand Down Expand Up @@ -96,6 +97,23 @@ for (const entryPath of bundledRuntimeEntryPaths) {
}
}

const packagedBuildResources = packagedAdeCliBuildResources();
for (const entryPath of [...bundledRuntimeEntryPaths, tuiPath]) {
if (packagedBuildResources.some((resource) => sourceContainsPath(resource.sourcePath, entryPath))) {
continue;
}
throw new Error(
`[ade-cli:build] ${path.relative(packageRoot, entryPath)} is not shipped by ` +
"apps/desktop/package.json build.extraResources",
);
}

for (const marker of ["__ade-usage-ledger-worker", "Usage ledger worker input is invalid"]) {
if (!contents.includes(marker)) {
throw new Error(`[ade-cli:build] dist/cli.cjs is missing embedded usage ledger worker marker: ${marker}`);
}
}

const stat = await fs.stat(cliPath);
if (process.platform !== "win32" && (stat.mode & 0o111) === 0) {
throw new Error("[ade-cli:build] dist/cli.cjs is not executable");
Expand Down
16 changes: 16 additions & 0 deletions apps/ade-cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,22 @@ describe("ADE CLI", () => {
});
});

it("recognizes the hidden usage ledger worker entrypoint", () => {
expect(buildCliPlan(["__ade-usage-ledger-worker"])).toEqual({
kind: "usage-ledger-worker",
});
});

it("passes the project root to the hidden icon worker entrypoint", () => {
expect(buildCliPlan([
"__ade-project-icon-worker",
"/tmp/project with spaces",
])).toEqual({
kind: "project-icon-worker",
rootPath: "/tmp/project with spaces",
});
});

it("classifies only ADE temp runtime sockets as ephemeral", () => {
const tempSocket = path.join(os.tmpdir(), "ade-stdio-rpc-test", "sock", "ade.sock");

Expand Down
Loading