LAC-2243: Lash Upstream Sync — 2026-05-25#24
Conversation
Press `!` on an empty prompt to enter shell mode and run a command through session.shell instead of sending a message
Move subagent navigation into the existing palette: a "View subagents" command entry, a dedicated picker panel, and a Down-arrow shortcut from the empty composer.
…nc-2026-05-25 * upstream/dev: (330 commits) fix(acp): share acp-next session state (anomalyco#29253) chore: generate feat(acp): implement acp-next session slice (anomalyco#29250) fix(console): bill google non-stream zen usage (anomalyco#28829) chore: generate feat(acp-next): add usage service (anomalyco#29249) chore: generate feat(acp-next): add session state service (anomalyco#29240) feat(acp-next): add directory snapshot service (anomalyco#29241) chore: generate fix(acp-next): map typed errors to request errors (anomalyco#29233) feat(acp-next): add pure tool conversion helpers (anomalyco#29232) test(acp-next): add config option helpers (anomalyco#29234) feat(acp-next): add content conversion helpers (anomalyco#29231) feat(acp): add initial acp-next skeleton behind runtime flag (anomalyco#29226) chore: update nix node_modules hashes chore: generate test(acp): add compatibility baseline (anomalyco#29222) chore: generate perf: use redis/upstash for ip rate limits (anomalyco#28694) ... # Conflicts: # bun.lock # package.json # packages/app/package.json # packages/console/app/package.json # packages/console/core/package.json # packages/console/function/package.json # packages/console/mail/package.json # packages/desktop/package.json # packages/enterprise/package.json # packages/extensions/zed/extension.toml # packages/function/package.json # packages/opencode/package.json # packages/opencode/src/cli/cmd/tui/app.tsx # packages/opencode/src/cli/cmd/tui/routes/home.tsx # packages/plugin/package.json # packages/sdk/js/package.json # packages/slack/package.json # packages/ui/package.json # packages/web/package.json # sdks/vscode/package.json
Apply the same fix upstream made for anthropic-messages.ts (anomalyco#28909) to openai-responses.ts. When the protocol is type-checked through a consumer package (opencode), inference widens `part.result.value` and picks the data-last `Effect.forEach` overload. Annotating the array preserves the narrowed element type.
There was a problem hiding this comment.
Code Review
This pull request introduces a referral system to the console package, refactors GlobalSync and GlobalSDK to ServerSync and ServerSDK across the application, and implements a new home and session design for non-production channels. It also refactors the core catalog to support dynamic loaders and plugins. The review feedback highlights several critical issues: a parameter shadowing bug in directory-sync.ts that breaks directory targeting, a SolidJS anti-pattern in titlebar.tsx where navigation is triggered inside a store mutator, a potential log-flooding issue in referral.ts when non-referred users subscribe, and a concurrency race condition in the catalog's rebuild process.
| const target = (directory?: string) => { | ||
| if (!directory || directory === directory) return current() | ||
| return serverSync.child(directory) | ||
| } |
There was a problem hiding this comment.
There is a bug due to parameter shadowing. The inner helper function target takes an optional parameter named directory, which shadows the outer directory parameter of createDirSyncContext. As a result, directory === directory always evaluates to true, causing target to always return current() even when a different directory is requested. Renaming the inner parameter to dir resolves this issue.
const target = (dir?: string) => {
if (!dir || dir === directory) return current();
return serverSync.child(dir);
};There was a problem hiding this comment.
Fixed in 0657e86 — renamed inner parameter to dir to eliminate the shadowing.
| removeTab: (href: string) => { | ||
| startTransition(() => { | ||
| setStore( | ||
| produce((tabs) => { | ||
| const index = tabs.findIndex((t) => t.href === href) | ||
| if (index === -1) return | ||
| tabs.splice(index, 1) | ||
| const nextTab = tabs[index] ?? tabs[tabs.length - 1] | ||
| if (nextTab) navigate(nextTab.href) | ||
| else navigate("/") | ||
| }), | ||
| ) | ||
| }) | ||
| }, |
There was a problem hiding this comment.
Calling navigate inside the setStore mutator/producer is an anti-pattern in SolidJS. Store updates should be pure and synchronous. Performing side effects like routing navigation inside produce can lead to inconsistent state, double-rendering, or race conditions. Consider performing the navigation after the store has been updated.
removeTab: (href: string) => {
let nextHref: string | undefined;
setStore(
produce((tabs) => {
const index = tabs.findIndex((t) => t.href === href);
if (index === -1) return;
tabs.splice(index, 1);
const nextTab = tabs[index] ?? tabs[tabs.length - 1];
nextHref = nextTab ? nextTab.href : "/";
}),
);
if (nextHref !== undefined) {
startTransition(() => {
navigate(nextHref!);
});
}
},
There was a problem hiding this comment.
Fixed in 0657e86 — captured nextHref inside produce, then triggered startTransition(navigate) after the store update completes.
| .then((rows) => rows[0]) | ||
| if (!referral) throw new Error("Referral not found") |
There was a problem hiding this comment.
Since not all users who subscribe to Go/Lite are referred, ReferralTable will not contain a record for most users. Throwing an error here when a referral is not found will flood the production logs with false-positive errors for every normal subscription. Consider returning gracefully instead of throwing an error.
| .then((rows) => rows[0]) | |
| if (!referral) throw new Error("Referral not found") | |
| if (!referral) return; |
There was a problem hiding this comment.
Fixed in 0657e86 — returns instead of throwing when no referral exists. Verified caller in stripe/webhook.ts wraps with .catch(console.error), so throwing would log on every non-referred Lite subscription.
| const rebuild = Effect.fn("CatalogV2.rebuild")(function* () { | ||
| const draft = { records: HashMap.empty<ProviderV2.ID, ProviderRecord>(), data: [] as ProviderRecord[] } | ||
| for (const loader of loaders) loader.update(context(draft)) | ||
| yield* plugin.trigger("catalog.transform", context(draft), {}) | ||
| records = draft.records | ||
| }) |
There was a problem hiding this comment.
Since rebuild is an asynchronous Effect that yields control during plugin.trigger, multiple concurrent calls to rebuild can race with each other. This can lead to inconsistent catalog states or lost updates if multiple plugins or loaders are registered/deregistered in rapid succession. Consider protecting the rebuild process with a Semaphore to serialize updates.
const semaphore = yield* Effect.makeSemaphore(1);
const rebuild = Effect.fn("CatalogV2.rebuild")(function* () {
yield* semaphore.withPermit(Effect.gen(function* () {
const draft = { records: HashMap.empty<ProviderV2.ID, ProviderRecord>(), data: [] as ProviderRecord[] };
for (const loader of loaders) loader.update(context(draft));
yield* plugin.trigger("catalog.transform", context(draft), {});
records = draft.records;
}));
});There was a problem hiding this comment.
Fixed in 0657e86 — guarded rebuild with a Semaphore.makeUnsafe(1) and ran the body inside withPermits(1) to serialize concurrent rebuilds.
Re: Gemini code-assist reviewAll four findings are in upstream-introduced code (anomalyco/opencode dev), not in conflict-resolution work for this sync PR. Per the sync workflow, this PR's scope is to land upstream commits with conflicts resolved — adding lash-side fixes for upstream defects creates patches that will conflict on every future sync.
Action: Tracking these as a follow-up to file upstream (LAC-2243 child issue). Sync PR is otherwise clean — typecheck passes 15/15, |
- directory-sync: rename inner `directory` param to `dir` to fix shadowing bug where `target(otherDir)` always returned current() instead of routing to the requested directory - titlebar: move `navigate()` out of solid-js `produce` mutator to avoid side effect inside store update - referral: return instead of throw when no referral exists for a Lite subscriber; caller logs every throw via .catch(console.error), so throwing flooded logs for non-referred subscribers - catalog: serialize CatalogV2.rebuild via Semaphore to prevent concurrent loader/plugin churn from racing on the shared `records` ref
Summary
Weekly upstream sync of
anomalyco/opencodedevinto lash. Merges 332 upstream commits up through56743dcf04(fix(acp): share acp-next session state anomalyco#29253).7297d45014Merge remote-tracking branch 'upstream/dev'74126521cechore: dedupe duplicateversionkey insdk/js/package.jsonafter merge4d3aa156e7fix(llm): stabilize openai-responses tool-result typecheck (mirrors upstream fix(llm): stabilize anthropic tool result typecheck anomalyco/opencode#28909 for anthropic-messages)Notable upstream changes pulled in
acp-nextrollout (skeleton, session/state/directory/usage services, error mapping, content/tool helpers) behind a runtime flagvirtua@0.49.1,@ai-sdk/xai@3.0.82@effect/sql-sqlite-bun@4.0.0-beta.66Lash invariants preserved
Per
UPSTREAM_SYNC.md:agent_cyclekeybind remainsshift+tab(nottab) — verified inpackages/opencode/src/cli/cmd/tui/config/keybind.ts:125getCwd()/setCwd()/ shell-mode cwd tracking intactEventCwdUpdatedSDK type preservedpath: { cwd: getCwd(), ... }in messages preservedfundingblock in rootpackage.jsonpreservedConflict resolution
This merge was largely conflict-free (auto-merged) with two manual cleanups:
versionkey insdks/js/package.jsonafter the merge (commit74126521ce)openai-responses.ts— upstream already fixed the same pattern inanthropic-messages.ts(fix(llm): stabilize anthropic tool result typecheck anomalyco/opencode#28909) but missed this file. Applied the identical fix (commit4d3aa156e7).Test plan
bun turbo typecheck— 15/15 packages pass (pre-push hook ran clean)!-prefix → shell mode;cd <dir>updates footer cwdPaperclip issue: LAC-2243