Closes#146939. Related: #140984.
## What Problem This Solves
Fixes `memory_get` returning a different file from `memory_search` when an explicitly owned multi-agent roster relies on inherited workspace paths.
## User Impact
Search results and subsequent reads agree on the agent's workspace without requiring a per-agent workspace pin. Explicit workspace overrides remain authoritative. No files move, no index reset or storage migration is required, and Markdown containment and read limits are unchanged.
For explicitly owned rosters, an unpinned first agent no longer implicitly reads the parent workspace merely because it appears first. A valid retained migration owner and sole-agent inheritance still follow canonical behavior. Raw reader inputs without explicit ownership keep their existing legacy behavior.
## Why This Change Was Made
The prior path repair in #140984 deliberately deferred workspace-ownership alignment. This PR proposes only that bounded alignment: core and explicitly owned memory-host reads share the compatibility-owner decision through a pure internal helper. The existing private path bridge avoids importing the full agent runtime. The original config object is preserved for retained migration-owner lookup; optional legacy reader IDs remain accepted.
### Maintainer choices — proposed, not previously approved
1. **Accept explicit-roster alignment (recommended and implemented).** Search's canonical workspace ownership is authoritative for reads too. Alternatively, retain the mismatch and require explicit workspace pins; this PR does not silently change search to match the old getter.
2. **Preserve legacy reader compatibility (implemented).** Inputs without `ownership: "explicit"` retain first-agent/default-marker inheritance. Broader normalization of those inputs is intentionally deferred, rather than changing the shipped reader contract in this fix.
3. **Keep adjacent policy out of scope (recommended).** No context-limit merging changes, shared-system-agent workspace redesign, public SDK entrypoint, or broader path allowlist. Those require separate decisions.
ClawSweeper's source review supports this direction but requests a compatibility decision; it is not maintainer approval. Please review the ownership choice before merge. Rollback is reverting this change; the issue's explicit-workspace-pin mitigation remains available.
## Evidence
### After-fix real Gateway proof — September 13, 2026
Built candidate `be5a589a0ee148b54c51f6876ef5281ee9b29cb5`, Node 26.8.1/macOS. Used a loopback-only Gateway, registered Memory Core tools through HTTP `POST /tools/invoke`, synthetic files and real on-disk SQLite state, `provider: none`, and vectors disabled. No test-helper imports, provider calls, or production configuration/state changes.
| Setup | Actual search → get result |
| --- | --- |
| Fresh explicit two-agent roster, neither agent pinned; parent contains a decoy | main: `USER.md`, `status=ok`, `Orchid proof main`; other: `USER.md`, `status=ok`, `Orchid proof other` |
| Fresh setup, parent traversal for each agent | `MEMORY_PATH_NOT_ALLOWED` |
| Installed 2026.9.3, existing explicit workspace pins | main/other: `USER.md`, `status=ok`, matching `Orchid upgrade main` / `Orchid upgrade other` |
| Same shipped-created state after candidate Doctor migration and candidate Gateway startup | Both agents return those same markers with `status=ok` |
For each agent, passed the search result's path/start line/inclusive line count into `memory_get`. Stopped the shipped Gateway before switching binaries. Candidate initially refused the older session-identity database; followed its supported `doctor --fix --non-interactive` instruction (exit 0, both agent databases v19→v20), then repeated the actual HTTP flow. Both existing workspace pins were preserved exactly, and SHA-256 checks showed all three synthetic `USER.md` files unchanged. Doctor did change normal config metadata/defaults; the whole config was **not** unchanged. All proof Gateways were stopped.
This is fresh-runtime and shipped-created-state/Doctor migration proof, **not** an installer, `openclaw update` driver, or live-user upgrade test. Retained legacy-owner shapes beyond these fixtures remain covered by focused tests, not claimed as live proof. The schema migration belongs to the candidate's existing upgrade path; this six-file patch introduces no schema or embedding-metadata changes. Maintainer acceptance of explicit-roster read-root alignment remains required.
CI at this evidence update: the model-login catalog timing case in [large-26](https://github.com/openclaw/openclaw/actions/runs/34761804381/job/103735936253) failed, making the aggregate gate red. Its fixture disables memory and uses one pinned agent without explicit ownership; likely unrelated, but no matching base failure was verified. No CI pass is claimed.
- Focused validation: 162 tests passed across six files (one existing skipped test), including the real-manager tool regression and package-boundary contracts.
- Final-head `node scripts/check-changed.mjs` passed: core and test types, core/extension lint, formatting, dead-export scans, package/SDK boundaries, and runtime import-cycle checks. `git diff --check` passed.
- Full `pnpm build` passed in 3m26s, including plugin SDK exports, CLI bootstrap imports, built plugin-loading checks, and Control UI build. After that build, the only patch change moved an unchanged re-export below imports for lint; the rebase added only unrelated ACP tests. The repository-wide test suite was not run.
- Tests-first reproduction on upstream main: both roster orders returned the parent decoy through `memory_get` after real SQLite-backed `memory_search` found the agent marker.
- The regression exercises both agents, both roster orders, returned path/line readback, and rejection of parent traversal. It uses temporary files and databases with embeddings disabled; no provider request or private installation data.
- Compatibility coverage includes explicit workspace overrides, retained/removed migration owners, ignored legacy markers under explicit ownership, optional legacy IDs, sole/duplicate agents, and absent/empty rosters. Existing legacy path, file-reader, core ownership, and package-boundary coverage is retained.
- Independent clean-context P0–P3 source review found no actionable findings. The repository Auto Review CLI was also attempted but could not authenticate; no successful CLI review is claimed.
AI-assisted implementation and review. Maintainer approval remains required; no merge or automatic repair authority is inferred from the issue labels.
---
## Maintainer addendum — September 14, 2026
The ownership choices proposed above are accepted. This addendum records the final shared legacy-data owner, the preserved durable runtime default, and the refreshed proof. Earlier evidence remains tied to its stated revision.
### Problem
Memory search finds an agent's file, but `memory_get` returns the parent's different file as success.
### Fix and impact
Search and explicit memory reads now share one lightweight legacy-data owner. The two existing cached facts stay with the agent owner. Raw reader behavior, optional IDs, workspace pins, home/profile handling, and extra paths remain supported. No schema, migration, config key, protocol, or dependency changes.
Owner decision: explicit-roster reads must return the searched workspace's file, never a different parent file. This ends the explicit-roster exception retained in #140984. To retain the parent as an agent's workspace, configure that workspace explicitly. Legacy data ownership remains separate from the durable runtime default, preserving #146246.
Production growth is reduced from 46 to 38 net lines:
- `agent-roster.ts` moves existing roster readers into a lightweight module and owns the shared data decision.
- `agent-scope-config.ts` shrinks by 96 lines while preserving public exports and both cache lifetimes.
- Memory-host `config-utils.ts` adds eight net lines to preserve raw-reader compatibility and use the shared owner for explicit inputs.
- `openclaw-runtime-paths.ts` adds the one export needed by that call.
### Evidence
- Main `5576f256da`: the three unpinned regression rows fail with `status: ok` and `text: Parent decoy`; the pinned control passes. Command: `node scripts/run-vitest.mjs extensions/memory-core/src/tools.real-manager.test.ts -t 'reads the indexed agent file with explicit ownership'`.
- Retained HTTP proof at accepted `a016f35b265a96d4cdfd5d43dbf12d461ae417d2` (the rebuilt branch has identical owner files): real Gateway `POST /tools/invoke`, search then get, covers both roster orders, a non-first system agent, explicit pins, and parent traversal. Eight reads return the known agent file bytes; eight traversals are rejected.
- Fresh merge `f48cf5fc4eb0e717f252fefde7929632f24d494a`, parents candidate above and main `f6d984fcbe`: 297 tests pass, one existing skip, across the owner, memory, session/auth, and Doctor suites below.
- Local production/test type checks, changed-file type-aware lint, formatting, architecture, unused-export, line-count, and assertion checks pass. The assertion allowance only shrinks.
- Retained 2026.9.3-created state proof preserves pins, file hashes, and runtime default through Doctor/restart. This proves state compatibility, not the installed update driver.
- Earlier local matrix replay was stopped; its historical failures remain recorded. Current exact-head CI run [34806299349](https://github.com/openclaw/openclaw/actions/runs/34806299349) completed successfully at `1c3602c201d65ed43e45c4c1c856fe4fcc696dc8`, with every executed job passing.
<details>
<summary>HTTP requests and fresh-merge test command</summary>
The isolated Gateway runs through `pnpm openclaw gateway run --port <isolated-port> --bind loopback`.
```json
{"agentId":"main","tool":"memory_search","args":{"query":"AGENT_MAIN_ORCHID","corpus":"memory"}}
{"agentId":"main","tool":"memory_get","args":{"path":"USER.md","from":1,"lines":3}}
{"agentId":"main","tool":"memory_get","args":{"path":"../USER.md","from":1,"lines":2}}
```
The search-returned path and line range drive the read. The read returns two lines: `# main`, followed by `AGENT_MAIN_ORCHID searched source`. Traversal returns `MEMORY_PATH_NOT_ALLOWED`.
```sh
node scripts/run-vitest.mjs \
packages/memory-host-sdk/src/host/config-utils.test.ts \
packages/memory-host-sdk/src/host/read-file.test.ts \
packages/memory-host-sdk/src/host/read-file-manager-compat.test.ts \
extensions/memory-core/src/tools.real-manager.test.ts \
src/agents/agent-scope-config.test.ts \
src/agents/legacy-inherited-auth-dir.test.ts \
src/plugins/contracts/extension-package-project-boundaries.test.ts \
src/config/legacy.roster.test.ts \
src/commands/sessions.default-agent-store.test.ts \
src/commands/doctor/shared/legacy-config-migrations.agent-rosters.test.ts \
src/commands/doctor/shared/legacy-config-migrations.runtime.system-agent.test.ts \
src/commands/doctor/shared/default-agent-role-materialization.test.ts \
src/commands/doctor/shared/default-agent-role-materialization.write.test.ts \
src/commands/doctor-config-flow.workspace-persistence.test.ts \
src/agents/agent-scope.test.ts \
src/agents/agent-scope.workspace-inference.test.ts
```
</details>
## Consumers
The moved roster types retain their `index`, `key`, and `kind` fields. The following typed read sites preserve their source-location contracts; generic word matches elsewhere are not consumers of these fields.
<details>
<summary>Preserved roster-field callers</summary>
census: generic index reviewed — 19 callers listed
- `src/agents/sandbox/secret-owner.ts:53:42` — Report unresolved SSH secret references at the selected agent source path.
- `src/cli/config-model-validation.ts:117:90`, `src/cli/config-model-validation.ts:221:62` — Locate model validation issues and test whether edited paths change model ownership at the correct keyed entry or list index.
- `src/commands/doctor/shared/context-engine-host-compat.ts:113:90`, `src/commands/doctor/shared/context-engine-host-compat.ts:153:90` — Locate context-engine host compatibility warnings and repairs at the source agent path.
- `src/commands/doctor/shared/exec-safe-bins.ts:85:35` — Locate executable allowlist warnings at the source agent tools.exec path.
- `src/commands/doctor/shared/plugin-tool-allowlist-warnings.ts:91:90`, `src/commands/doctor/shared/plugin-tool-allowlist-warnings.ts:293:90` — Locate plugin tool allowlist warnings at the source agent path.
- `src/config/io.write-prepare.ts:993:71`, `src/config/io.write-prepare.ts:1000:41` — Preserve original roster representation, legacy occurrence identity, explicit writes/deletions, authored reference restoration, and source paths across config writes.
- `src/config/validation-core.ts:159:36`, `src/config/validation-core.ts:281:90`, `src/config/validation-core.ts:335:55` — Locate avatar, model-policy, and sandbox environment errors at the authored keyed entry or original list index.
- `src/config/validation.ts:715:90` — Attach heartbeat target validation issues to the correct authored agent path.
- `src/secrets/runtime-config-collectors-core.ts:536:35` — Locate agent text-to-speech secret assignments at the source agent path.
- `src/secrets/runtime-config-collectors-memory.ts:125:90` — Locate memory secret assignments at the source agent path; the separate unchanged implicit-agent producer supplies the legacy key when no roster exists.
- `src/secrets/runtime-config-collectors-sandbox.ts:75:90` — Carry the source agent path into sandbox secret collection candidates.
- `src/security/dangerous-config-flags-core.ts:50:36` — Report dangerous flags at canonical keyed paths when an ID exists; retain original list index for an ID-less legacy row.
- `src/skills/workshop/tool-policy-diagnostic.ts:49:40` — Report canonical keyed tool-policy paths when an ID exists; retain original list index for an ID-less legacy row.
census: generic key reviewed — 18 callers listed
- `src/agents/sandbox/secret-owner.ts:52:45` — Report unresolved SSH secret references at the selected agent source path.
- `src/cli/config-model-validation.ts:117:60`, `src/cli/config-model-validation.ts:221:42` — Locate model validation issues and test whether edited paths change model ownership at the correct keyed entry or list index.
- `src/commands/doctor/shared/context-engine-host-compat.ts:113:60`, `src/commands/doctor/shared/context-engine-host-compat.ts:153:60` — Locate context-engine host compatibility warnings and repairs at the source agent path.
- `src/commands/doctor/shared/exec-safe-bins.ts:84:38` — Locate executable allowlist warnings at the source agent tools.exec path.
- `src/commands/doctor/shared/plugin-tool-allowlist-warnings.ts:91:60`, `src/commands/doctor/shared/plugin-tool-allowlist-warnings.ts:293:60` — Locate plugin tool allowlist warnings at the source agent path.
- `src/config/io.write-prepare.ts:1001:44` — Preserve original roster representation, legacy occurrence identity, explicit writes/deletions, authored reference restoration, and source paths across config writes.
- `src/config/validation-core.ts:158:39`, `src/config/validation-core.ts:281:60`, `src/config/validation-core.ts:334:58` — Locate avatar, model-policy, and sandbox environment errors at the authored keyed entry or original list index.
- `src/config/validation.ts:715:60` — Attach heartbeat target validation issues to the correct authored agent path.
- `src/secrets/runtime-config-collectors-core.ts:535:38` — Locate agent text-to-speech secret assignments at the source agent path.
- `src/secrets/runtime-config-collectors-memory.ts:125:60` — Locate memory secret assignments at the source agent path; the separate unchanged implicit-agent producer supplies the legacy key when no roster exists.
- `src/secrets/runtime-config-collectors-sandbox.ts:75:60` — Carry the source agent path into sandbox secret collection candidates.
- `src/security/dangerous-config-flags-core.ts:46:44` — Report dangerous flags at canonical keyed paths when an ID exists; retain original list index for an ID-less legacy row.
- `src/skills/workshop/tool-policy-diagnostic.ts:46:41` — Report canonical keyed tool-policy paths when an ID exists; retain original list index for an ID-less legacy row.
census: generic kind reviewed — 64 callers listed
- `src/agents/agent-roster.ts:23:15`, `src/agents/agent-roster.ts:35:15` — Select the raw representation before projecting entries; preserve entries precedence and reject invalid representation values.
- `src/agents/agent-scope-config.ts:281:15`, `src/agents/agent-scope-config.ts:294:15`, `src/agents/agent-scope-config.ts:329:15`, `src/agents/agent-scope-config.ts:334:15`, `src/agents/agent-scope-config.ts:316:47` — Preserve direct and mutable roster lookup branches; source.kind in the batch index preserves keyed clone-on-read and legacy list identity.
- `src/agents/sandbox/secret-owner.ts:51:25` — Report unresolved SSH secret references at the selected agent source path.
- `src/cli/config-cli-roster.ts:17:21`, `src/cli/config-cli-roster.ts:35:55`, `src/cli/config-cli-roster.ts:53:19`, `src/cli/config-cli-roster.ts:58:17`, `src/cli/config-cli-roster.ts:90:22`, `src/cli/config-cli-roster.ts:107:38` — Preserve list order and keyed/list path translation across CLI mutations and canonicalization.
- `src/cli/config-model-validation.ts:117:14`, `src/cli/config-model-validation.ts:220:14`, `src/cli/config-model-validation.ts:221:14` — Locate model validation issues and test whether edited paths change model ownership at the correct keyed entry or list index.
- `src/commands/doctor-config-flow.ts:255:45` — Read migrated keyed entries to persist the legacy workspace and stamp explicit ownership for multiple agents.
- `src/commands/doctor/shared/context-engine-host-compat.ts:113:14`, `src/commands/doctor/shared/context-engine-host-compat.ts:153:14` — Locate context-engine host compatibility warnings and repairs at the source agent path.
- `src/commands/doctor/shared/exec-safe-bins.ts:83:16` — Locate executable allowlist warnings at the source agent tools.exec path.
- `src/commands/doctor/shared/legacy-config-core-migrate.ts:30:41`, `src/commands/doctor/shared/legacy-config-core-migrate.ts:35:25`, `src/commands/doctor/shared/legacy-config-core-migrate.ts:45:19`, `src/commands/doctor/shared/legacy-config-core-migrate.ts:46:20`, `src/commands/doctor/shared/legacy-config-core-migrate.ts:108:107` — Validate representation/value agreement, repair the selected roster in its existing shape, and name that shape in repair messages.
- `src/commands/doctor/shared/plugin-tool-allowlist-warnings.ts:91:14`, `src/commands/doctor/shared/plugin-tool-allowlist-warnings.ts:293:14` — Locate plugin tool allowlist warnings at the source agent path.
- `src/config/io.write-prepare.ts:892:14`, `src/config/io.write-prepare.ts:951:20`, `src/config/io.write-prepare.ts:1146:15`, `src/config/io.write-prepare.ts:1177:25`, `src/config/io.write-prepare.ts:1185:26`, `src/config/io.write-prepare.ts:1218:23`, `src/config/io.write-prepare.ts:1233:23`, `src/config/io.write-prepare.ts:1255:25`, `src/config/io.write-prepare.ts:1269:23`, `src/config/io.write-prepare.ts:1287:21`, `src/config/io.write-prepare.ts:1330:23`, `src/config/io.write-prepare.ts:1352:21`, `src/config/io.write-prepare.ts:1354:21`, `src/config/io.write-prepare.ts:1450:23`, `src/config/io.write-prepare.ts:1455:27`, `src/config/io.write-prepare.ts:1467:42`, `src/config/io.write-prepare.ts:1532:72`, `src/config/io.write-prepare.ts:1546:23`, `src/config/io.write-prepare.ts:1551:25`, `src/config/io.write-prepare.ts:993:25`, `src/config/io.write-prepare.ts:999:22` — Preserve original roster representation, legacy occurrence identity, explicit writes/deletions, authored reference restoration, and source paths across config writes.
- `src/config/legacy.roster.ts:106:23`, `src/config/legacy.roster.ts:117:35` — Select legacy list conversion, then consume the keyed roster after migration.
- `src/config/validation-core.ts:157:12`, `src/config/validation-core.ts:281:14`, `src/config/validation-core.ts:333:34` — Locate avatar, model-policy, and sandbox environment errors at the authored keyed entry or original list index.
- `src/config/validation.ts:715:14` — Attach heartbeat target validation issues to the correct authored agent path.
- `src/gateway/server-methods/config.ts:528:15`, `src/gateway/server-methods/config.ts:531:15` — Enumerate explicitly authored agent IDs in either representation before rejecting unapproved removals.
- `src/plugins/provider-auth-choice-helpers.ts:304:52`, `src/plugins/provider-auth-choice-helpers.ts:306:52` — Write normalized agent model references back using the original keyed or list representation.
- `src/secrets/runtime-config-collectors-core.ts:534:16` — Locate agent text-to-speech secret assignments at the source agent path.
- `src/secrets/runtime-config-collectors-memory.ts:125:14` — Locate memory secret assignments at the source agent path; the separate unchanged implicit-agent producer supplies the legacy key when no roster exists.
- `src/secrets/runtime-config-collectors-sandbox.ts:75:14` — Carry the source agent path into sandbox secret collection candidates.
- `src/security/dangerous-config-flags-core.ts:45:21` — Report dangerous flags at canonical keyed paths when an ID exists; retain original list index for an ID-less legacy row.
- `src/skills/workshop/tool-policy-diagnostic.ts:45:19` — Report canonical keyed tool-policy paths when an ID exists; retain original list index for an ID-less legacy row.
</details>
Registered memory tools use the shared data owner for explicit workspace reads. Search manager lookup, forget/reset/backfill, session/auth data locations, and Doctor materialization retain their existing owner contracts. There is no new state writer; config-identity provenance and the separate data/runtime batch facts keep their existing lifetimes.
### Contributor-branch refresh
The accepted content is rebuilt on the original contributor head `be5a589a0ee148b54c51f6876ef5281ee9b29cb5`, followed by a plain merge of main `ae5a4b4a55` and the five maintainer commits in order. There were no conflicts or manual resolutions, and all five messages are unchanged.
The resulting head `1c3602c201d65ed43e45c4c1c856fe4fcc696dc8` has the same complete tree as the accepted revision merged with that main. The 16-file command above passed again on this head: 297 tests, one existing skip, exit 0.
The unchanged local preflight passed on this same head in a physical checkout with its own dependencies. It covered type-aware lint, production types, core/extension/script/root-test types, and protocol checks. The initial nested-checkout attempt failed only the declaration-input isolation guard; physical isolation resolved it without disabling any check.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>