Skip to content

fix(runtime-prompt): use macOS product version on Darwin (#95145)#95189

Closed
Sanjays2402 wants to merge 8 commits into
openclaw:mainfrom
Sanjays2402:fix/95145-darwin-os-mapping
Closed

fix(runtime-prompt): use macOS product version on Darwin (#95145)#95189
Sanjays2402 wants to merge 8 commits into
openclaw:mainfrom
Sanjays2402:fix/95145-darwin-os-mapping

Conversation

@Sanjays2402

Copy link
Copy Markdown
Contributor

Closes #95145

What Problem This Solves

Fixes an issue where agents running on macOS 26 (Tahoe) saw the wrong OS in their runtime prompt — os=Darwin 25.5.0 (arm64) instead of os=macOS 26.5.1 (arm64). Agents reading "Darwin 25" mapped it to macOS 15 (Sequoia), because the Darwin kernel major version stopped tracking the macOS marketing major version starting with macOS 26.

The OS line is rendered by buildRuntimeLine() from the runtimeInfo.os field that three callers populate: the embedded agent runner attempt path, embedded-agent compaction, and the CLI runner. All three were building that field with `${os.type()} ${os.release()}` instead of reusing the existing sw_vers-backed helper. src/infra/os-summary.ts and src/infra/system-presence.ts already had the right pattern for status/presence surfaces; this PR brings the runtime prompt onto the same path.

Why This Change Was Made

Adds resolveRuntimePromptOs() next to the existing resolveOsSummary() and routes the three runtime callers through it. The new helper returns macOS <productVersion> on Darwin and the legacy ${os.type()} ${os.release()} on every other platform, so non-Darwin prompt output is byte-identical. Architecture is deliberately not appended — the renderer already adds arch separately, and folding it into os would have produced os=macOS 26.5.1 (arm64) (arm64).

Non-goals (left untouched):

  • os-summary.label (macos 15.4 (arm64) style) — still used by status/diagnostics, not the prompt line.
  • openai-chatgpt-responses.ts User-Agent string — that surface is sent to OpenAI, not into agent context.
  • Caching of sw_vers for the prompt helper — runtime prompt callers fire once per attempt, and resolveOsSummary() already caches when called from status surfaces.

User Impact

Agent runtime prompts on macOS 26 (Tahoe) hosts now correctly report the macOS marketing version, fixing the os= token that downstream prompts and behavior keyed off of. Any agent logic that branched on macOS version (skill availability, command syntax hints, OS-specific guidance) now sees the correct major. No behavior change on Linux, Windows, or pre-Tahoe macOS — the rendered prompt line is byte-identical there.

Evidence

Live host probe (this PR was developed on a macOS 26.5 / Darwin 25.5.0 host):

$ uname -r
25.5.0
$ sw_vers -productVersion
26.5
$ node -e '... probe of the new branch ...'
BEFORE: Darwin 25.5.0
AFTER:  macOS 26.5
RENDERED LINE FRAGMENT: os=macOS 26.5 (arm64)

Vitest — the three suites called out in the issue triage all pass:

$ node scripts/run-vitest.mjs src/infra/os-summary.test.ts \
    src/agents/system-prompt.test.ts \
    src/agents/embedded-agent-runner/run/attempt-system-prompt.test.ts
✓ infra/os-summary.test.ts                 (9 tests)
✓ agents/system-prompt.test.ts             (91 tests)
✓ agents/embedded-agent-runner/run/attempt-system-prompt.test.ts  (4 tests)
3 passed (104 tests)

Typecheck:

$ pnpm tsgo:core         # clean
$ pnpm tsgo:core:test    # clean

New tests added:

  • resolveRuntimePromptOs cases for Tahoe (Darwin 25.x), Sequoia (Darwin 24.x), blank sw_vers fallback, Linux, and Windows.
  • buildRuntimeLine regression that asserts os=macOS 26.5.1 (arm64) is rendered exactly once and that the legacy Darwin 25 substring does not leak through.

…OS line

Adds a shared helper that returns the OS display string used by the agent
runtime prompt line (`buildRuntimeLine`). On Darwin it prefers the macOS
marketing product version reported by `sw_vers -productVersion` (e.g.
`macOS 26.5.1`) instead of the kernel release reported by `os.release()`
(e.g. `Darwin 25.5.0`). On other platforms it preserves the historical
`${os.type()} ${os.release()}` shape that prompts already render.

Architecture is intentionally omitted from the return value; the renderer
appends `arch` separately, so returning `macOS 26.5.1` keeps the rendered
line as `os=macOS 26.5.1 (arm64)` rather than `os=macOS 26.5.1 (arm64) (arm64)`.

Refs openclaw#95145
Adds 5 cases for the new prompt helper:
- darwin with Darwin 25.5.0 + sw_vers 26.5.1 → "macOS 26.5.1" (Tahoe)
- darwin with Darwin 24.5.0 + sw_vers 15.6 → "macOS 15.6" (Sequoia)
- darwin with blank sw_vers → falls back to `macOS ${os.release()}`
- linux → "Linux 6.10.0-amd64" (preserves os.type/os.release shape)
- win32 → "Windows_NT 10.0.26100" (preserves os.type/os.release shape)

The first case is the regression for openclaw#95145 — before this change the runtime
prompt rendered `os=Darwin 25.5.0` on macOS 26 hosts, which agents would
read as macOS 15.5 (Sequoia).
…enclaw#95145)

Replace the kernel-only `${os.type()} ${os.release()}` with the shared
helper that returns the macOS marketing product version on Darwin. On a
macOS 26 (Tahoe / Darwin 25.x) host the runtime prompt line now reads
`os=macOS 26.5.1 (arm64)` instead of `os=Darwin 25.5.0 (arm64)`, which
agents were mapping to macOS 15 (Sequoia).

attempt.ts is the embedded agent runner's main prompt path, so this is
the highest-traffic call site for the broken mapping.
…enclaw#95145)

Compaction reuses the runtime prompt and was carrying the same broken
`${os.type()} ${os.release()}` value for the OS line. Switching to the
shared helper means compacted summaries also see the correct macOS
marketing version on Tahoe hosts.
…penclaw#95145)

Third of the three runtime-prompt call sites that built the OS line from
`os.type()` + `os.release()`. The CLI runner is used by external agents
(e.g. `openclaw run` and provider CLI backends), so without this fix
those flows would still see `Darwin 25.5.0` on a macOS 26 host.

After this commit `grep -nR 'os.type() } ${os.release()}' src/agents`
returns no hits in runtime-prompt code paths.
…e-line shape

Asserts that when callers pass `os: 'macOS 26.5.1'` (the value produced
by `resolveRuntimePromptOs()` on a Tahoe host), `buildRuntimeLine`
renders `os=macOS 26.5.1 (arm64)` exactly once, without the legacy
`Darwin 25.x` kernel release leaking through and without duplicating
the architecture suffix.

This is the end-to-end guard for the runtime-prompt regression in openclaw#95145.
@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S labels Jun 20, 2026
@clawsweeper

clawsweeper Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

Close: the same macOS Tahoe runtime prompt fix has already landed on current main via the merged canonical PR, and this branch is now a conflicting duplicate with no meaningful unique remainder.

Canonical path: Keep the merged current-main implementation from #95225 and close duplicate candidate branches for the same Tahoe runtime prompt bug.

So I’m closing this here and keeping the remaining discussion on #95225.

Review details

Best possible solution:

Keep the merged current-main implementation from #95225 and close duplicate candidate branches for the same Tahoe runtime prompt bug.

Do we have a high-confidence way to reproduce the issue?

No current-main failure remains: current main routes the runtime prompt OS field through the merged sw_vers-backed helper. The original failure remains source-proven in v2026.6.11, where all three prompt producers still pass raw os.type() plus os.release().

Is this the best way to solve the issue?

Yes, the merged current-main path is the best current solution: it keeps the Darwin product-version fix in a shared helper, preserves non-Darwin prompt strings, and avoids changing the raw status release field. This branch solved the same problem but is now redundant and conflicting.

Security review:

Security review cleared: No security or supply-chain concern was found; the PR only changes local OS metadata formatting and focused tests.

AGENTS.md: found and applied where relevant.

What I checked:

Likely related people:

  • sunlit-deng: Authored the merged PR that current main uses for the macOS product-version runtime prompt fix and its regression coverage. (role: canonical merged fix author; confidence: high; commits: 5631c9d4c0ea, 885f45b0a155, 7eb118f2891b; files: src/infra/os-summary.ts, src/infra/os-summary.test.ts, src/agents/cli-runner/helpers.ts)
  • Vincent Koc: Release-tag blame for the pre-fix os-summary helper and raw runtime prompt OS construction points to this author in the affected files. (role: recent area contributor; confidence: medium; commits: e085fa1a3ffd; files: src/infra/os-summary.ts, src/agents/cli-runner/helpers.ts, src/agents/embedded-agent-runner/run/attempt.ts)

Codex review notes: model internal, reasoning high; reviewed against 267898cddb98; fix evidence: commit 825d9a66623a, main fix timestamp 2026-06-29T08:03:56-07:00.

…allers (openclaw#95145)

Addresses the reviewer P1 from clawsweeper on PR openclaw#95189: resolveRuntimePromptOs()
is invoked from embedded attempt, compaction, and CLI runtime-prompt callers
on every prompt build, so the Darwin branch was synchronously spawning sw_vers
once per prompt without reuse. resolveOsSummary() already caches its full
summary by platform/release/arch tuple, but the underlying macosProductVersion()
helper spawned sw_vers unconditionally on each call — so resolveRuntimePromptOs's
direct call to macosProductVersion() bypassed the existing cache entirely.

Add a module-level cachedMacosProductVersionByRelease map keyed by os.release().
The macOS marketing product version is stable for the life of a process for a
given kernel release; once resolved, both resolveOsSummary() and
resolveRuntimePromptOs() reuse the cached string and no longer re-spawn
sw_vers. Non-Darwin output is unchanged.

Also export a test-only __resetOsSummaryCachesForTests() helper that clears
both module-level caches, and wire beforeEach() hooks into the existing test
file. Without the reset, the new cache exposes a pre-existing test-isolation
gap — the 'falls back to os.release on darwin when sw_vers returns blank' case
reuses kernel release 25.5.0 from an earlier case that already cached
'26.5.1', so the cache would return the earlier mock and the fallback case
would never exercise its fallback. The reset hook makes each test case
authoritative for its own mock.

Two new cache regression tests:

  - reuses cached macOS product version across repeated calls (asserts
    spawnSync is called exactly once across 3 invocations)
  - runtime-prompt and os-summary share the cached Darwin product version
    (asserts resolveRuntimePromptOs called after resolveOsSummary for the
    same kernel release does not re-spawn sw_vers — the two helpers share
    the module-level cache)

Verification:
  - node scripts/run-vitest.mjs src/infra/os-summary.test.ts
    src/agents/system-prompt.test.ts
    src/agents/embedded-agent-runner/run/attempt-system-prompt.test.ts
    → all 3 shards pass (11 + 1 + 4 tests)
  - pnpm tsgo:core → clean
  - pnpm tsgo:core:test → clean
@Sanjays2402

Copy link
Copy Markdown
Contributor Author

Thanks @clawsweeper — addressed the cache P1 in 64677b03.

Cache before merge — done

Added a module-level cachedMacosProductVersionByRelease map (keyed by os.release()) inside macosProductVersion() so both resolveOsSummary() and the new resolveRuntimePromptOs() reuse the cached marketing version. The macOS marketing product version is stable per kernel-release for the lifetime of the process, so a single sw_vers spawn per observed release is sufficient — embedded attempt, compaction, and CLI runtime-prompt callers no longer spawn sw_vers per prompt build.

resolveOsSummary()'s existing cachedOsSummaryByKey cache is unchanged; what's new is that the underlying macosProductVersion() is now itself cached, so the previously-uncached resolveRuntimePromptOs() call path also benefits.

Test isolation gap surfaced + fixed

Adding the cache exposed a pre-existing test-isolation hole: the existing "falls back to os.release on darwin when sw_vers returns blank" case reuses kernel release 25.5.0 from an earlier test case that already cached "26.5.1", so the cached value would shadow the fallback's mocked-blank swVersStdout and the test would never actually exercise its fallback. Added a test-only __resetOsSummaryCachesForTests() export and wired beforeEach hooks into all three describe blocks in os-summary.test.ts. Each test case is now authoritative for its own mock.

New cache regression tests

Two new cases under describe("resolveRuntimePromptOs caching"):

  1. reuses cached macOS product version across repeated calls — calls resolveRuntimePromptOs() 3× with the same mocked kernel release; asserts spawnSync was called exactly once (callsAfterFirst - callsBefore === 1 and callsAfterThird - callsAfterFirst === 0).

  2. runtime-prompt and os-summary share the cached Darwin product version — calls resolveOsSummary() then resolveRuntimePromptOs() for the same kernel release; asserts the second call did not re-spawn sw_vers. Pins that the two helpers share the module-level cache.

Verification

$ node scripts/run-vitest.mjs src/infra/os-summary.test.ts \
                              src/agents/system-prompt.test.ts \
                              src/agents/embedded-agent-runner/run/attempt-system-prompt.test.ts
✓ src/infra/os-summary.test.ts                                (11 tests)
✓ src/agents/system-prompt.test.ts                            (1 test)
✓ src/agents/embedded-agent-runner/run/attempt-system-prompt.test.ts (4 tests)
Test Files  3 passed (3)
     Tests  16 passed (16)

$ pnpm tsgo:core         # clean
$ pnpm tsgo:core:test    # clean

Re: canonical-fix overlap (#95158, #95194, #95195)

I see your note that several open PRs target the same linked bug. Happy to defer to whichever lane maintainers prefer — if any of #95158/#95194/#95195 lands first with the cached shape, close this one as superseded. The patch here is intentionally narrow (Darwin-only cached lookup + 2 cache regression tests + 1 test-isolation export) so it should also be straightforward to cherry-pick into another lane if useful.

@clawsweeper clawsweeper Bot added rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Jun 20, 2026
@clawsweeper clawsweeper Bot removed the merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. label Jun 20, 2026
@xuwei-xy

Copy link
Copy Markdown

Nice implementation! I appreciate the clear commit messages and focused changes.

@clawsweeper clawsweeper Bot added rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. and removed rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Jun 21, 2026
…e-dangle

The __resetOsSummaryCachesForTests export tripped oxlint's
eslint/no-underscore-dangle rule (error at src/infra/os-summary.ts:45),
the sole check-lint failure on this branch. Rename it to
resetOsSummaryCachesForTests across the module and its test rather than
adding a lint suppression: the only existing no-underscore-dangle
suppression in production code is for an unrenameable compile-time define
(__OPENCLAW_VERSION__), and every production suppression is tracked by an
explicit allowlist guard test, so a rename is the least-invasive,
convention-matching fix for a test-only internal helper.
@clawsweeper clawsweeper Bot added rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Jul 2, 2026
@clawsweeper

clawsweeper Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper applied the proposed close for this PR.

@clawsweeper clawsweeper Bot closed this Jul 2, 2026
@clawsweeper

clawsweeper Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

🦞👀
ClawSweeper could not autoclose this item.

Reason: structured ClawSweeper close marker: close-required (sha=64af1a46dd21a1bdf99fedd8b8fd8e4cf5ae660c)

Usage: /autoclose <maintainer close reason>. I will close this item and any open same-repo items explicitly referenced in the command text.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: M status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Darwin kernel version 25.x incorrectly mapped to macOS 15.x (should be macOS 26 / Tahoe)

2 participants