Skip to content

LAC-2243: Lash Upstream Sync — 2026-05-25#24

Merged
lacymorrow merged 334 commits into
devfrom
LAC-2243/upstream-sync-2026-05-25
May 25, 2026
Merged

LAC-2243: Lash Upstream Sync — 2026-05-25#24
lacymorrow merged 334 commits into
devfrom
LAC-2243/upstream-sync-2026-05-25

Conversation

@lacymorrow
Copy link
Copy Markdown
Owner

Summary

Weekly upstream sync of anomalyco/opencode dev into lash. Merges 332 upstream commits up through 56743dcf04 (fix(acp): share acp-next session state anomalyco#29253).

Notable upstream changes pulled in

  • acp-next rollout (skeleton, session/state/directory/usage services, error mapping, content/tool helpers) behind a runtime flag
  • Effect / drizzle / opentui / SST catalog version bumps (effect 4.0.0-beta.65 → beta.66, opentui 0.2.13 → 0.2.15, sst 3.18.10 → 4.13.1, drizzle-* → 1.0.0-rc.2)
  • New patches: virtua@0.49.1, @ai-sdk/xai@3.0.82
  • New catalog entry: @effect/sql-sqlite-bun@4.0.0-beta.66

Lash invariants preserved

Per UPSTREAM_SYNC.md:

  • agent_cycle keybind remains shift+tab (not tab) — verified in packages/opencode/src/cli/cmd/tui/config/keybind.ts:125
  • getCwd() / setCwd() / shell-mode cwd tracking intact
  • Double-left-border prompt layout intact
  • EventCwdUpdated SDK type preserved
  • path: { cwd: getCwd(), ... } in messages preserved
  • funding block in root package.json preserved

Conflict resolution

This merge was largely conflict-free (auto-merged) with two manual cleanups:

  1. Duplicate version key in sdks/js/package.json after the merge (commit 74126521ce)
  2. Cross-package type-inference regression in openai-responses.ts — upstream already fixed the same pattern in anthropic-messages.ts (fix(llm): stabilize anthropic tool result typecheck anomalyco/opencode#28909) but missed this file. Applied the identical fix (commit 4d3aa156e7).

Test plan

  • bun turbo typecheck — 15/15 packages pass (pre-push hook ran clean)
  • TUI smoke (manual, post-merge): Tab → shell autocomplete; Shift+Tab → agent cycle; Ctrl+Space → mode cycle; !-prefix → shell mode; cd <dir> updates footer cwd
  • Double left border renders with both bars equal height
  • Footer shows shortened cwd and mode label

Paperclip issue: LAC-2243

kitlangton and others added 30 commits May 19, 2026 16:10
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.
opencode-agent Bot and others added 24 commits May 25, 2026 14:25
…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.
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +176 to +179
const target = (directory?: string) => {
if (!directory || directory === directory) return current()
return serverSync.child(directory)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

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);
  };

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0657e86 — renamed inner parameter to dir to eliminate the shadowing.

Comment on lines +263 to +276
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("/")
}),
)
})
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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!);
                    });
                  }
                },

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0657e86 — captured nextHref inside produce, then triggered startTransition(navigate) after the store update completes.

Comment thread packages/console/core/src/referral.ts Outdated
Comment on lines +357 to +358
.then((rows) => rows[0])
if (!referral) throw new Error("Referral not found")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
.then((rows) => rows[0])
if (!referral) throw new Error("Referral not found")
if (!referral) return;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +208 to +213
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
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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;
      }));
    });

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0657e86 — guarded rebuild with a Semaphore.makeUnsafe(1) and ran the body inside withPermits(1) to serialize concurrent rebuilds.

@lacymorrow
Copy link
Copy Markdown
Owner Author

Re: Gemini code-assist review

All 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.

# File Severity Upstream PR Disposition
1 packages/app/src/context/directory-sync.ts:177 (param shadowing — directory === directory always true) Critical anomalyco#29155 Real bug. Will file upstream.
2 packages/app/src/components/titlebar.tsx:271 (navigate inside produce) High anomalyco#28767 Design concern. Upstream.
3 packages/console/core/src/referral.ts:358 (throw on missing referral) High anomalyco#28491 Behavior question — applyReferralReward may only be called for known referrals; needs upstream context to know if guard is correct.
4 packages/core/src/catalog.ts:208 (rebuild concurrency) Medium anomalyco#27415 / anomalyco#27347 Race exists only if multiple plugins/loaders churn concurrently. Upstream design call.

Action: Tracking these as a follow-up to file upstream (LAC-2243 child issue). Sync PR is otherwise clean — typecheck passes 15/15, agent_cycle=shift+tab invariant verified, no hotspot conflicts. Recommend merging this PR as-is and addressing upstream defects separately so we don't carry lash patches over upstream bugs.

- 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
@lacymorrow lacymorrow merged commit d890c23 into dev May 25, 2026
6 of 13 checks passed
@lacymorrow lacymorrow deleted the LAC-2243/upstream-sync-2026-05-25 branch May 25, 2026 19:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.