Repository navigation
feat(memorylake): actor 取 API key 所属用户,不再回落到机器名 - #23
Merged
Merged
Conversation
… hostname
With no actor configured, engram invented one from the machine hostname. That
made every agent-written conversation belong to a machine rather than to a
person, so nobody could find their own sessions under their own account — and
it left one orphaned actor per machine behind. The engram workspace currently
holds 11 such hostname actors next to 24 real user:: ones, i.e. every install
has been doing this.
MemoryLake answers the question directly: GET /api/v3/defaults/my-actor returns
the actor owning the API key, given nothing but the bearer token. NewBackend now
resolves the actor in three steps, most specific first:
1. cfg.Actor (ENGRAM_MEMORYLAKE_ACTOR or the saved config) — an explicitly set
actor still decides, and the lookup is skipped entirely.
2. the key owner from /defaults/my-actor, using its display_name as the
actor's display name so it reads as a person instead of "user::<uuid>".
3. the hostname, exactly as before.
Step 2 failing is a normal outcome, not an error: a deployment predating the
endpoint falls through to step 3, so this cannot regress against one.
Resolution keeps going through EnsureActor rather than using the id the endpoint
returns, because EnsureActor's tail is what binds the actor to the workspace
(tolerating BINDING_ALREADY_EXISTS); using the id directly would skip the bind.
Cost is one extra GET per backend construction when no actor is configured,
alongside the ResolveWorkspaceID and EnsureActor round trips already there.
The cmd/engram routing, CLI and turn tests now pin ENGRAM_MEMORYLAKE_ACTOR.
They cover backend selection and turn capture, not identity resolution, and
their fake servers fail on any unexpected request — pinning keeps them focused
and insulated from how the actor gets resolved. The resolution itself is covered
in internal/memorylake by three new cases: endpoint honored, configured actor
wins without consulting it, and hostname fallback when it is missing or empty.
Closes #22
Moving the actor off the hostname broke every conversation created under the
old one. MemoryLake validates the sender against the conversation's
participants, so a session that started before the change kept its hostname
actor as the only participant and every further append was rejected:
append turn: memorylake INVALID_ARGUMENT (400): Actor
'actor-b6688f40…' is not a participant of this conversation
Per-turn sync never retries, so that is not a transient hiccup — it is the rest
of that session's turns, silently dropped apart from one line in turn.log. Found
by checking the live hook path on this machine rather than the mocks: the
conversation had stopped at 4 messages while the failures accumulated.
AppendObservation now sends as its own actor when the conversation counts it as
a participant, and as an existing participant otherwise. Nothing is lost by
doing so: the server stamps its own per-conversation participant handle on the
stored message either way, and newly created conversations still carry our
resolved actor, so new sessions are attributed to the key's owner as intended.
Verified against the real conversation that was failing — the one behind this
very session — which resumed appending (4 messages -> 5) with no new turn.log
entry.
RELEASING.md step 3: the manifests must carry the version being tagged before the tag is pushed, or release.yml's check fails ahead of GoReleaser. Minor rather than patch because this branch adds actor resolution from the API key's owner (feat), and the actor change only reaches anyone through a release.
This was referenced Aug 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
问题
cfg.Actor未设置时,engram 用机器名造 actor(backend.go:129-136)。于是 agent 写的 conversation 挂在"某台机器"名下而不是人名下,用户在 MemoryLake 里按自己账号找不到自己的会话,而且每台机器都留下一个孤立 actor。engramworkspace 现状:35 个 actor 里 11 个是机器名型(Chuangxiandebijibendiannao.local、abruzz1deMacBook-Pro.local、VM-0-11-ubuntu…),24 个是user::<id>型。每个装了 engram 的人都踩着这个默认值。改动
GET /api/v3/defaults/my-actor只凭 bearer token 就能回答"这个 key 属于谁"。NewBackend的 actor 解析改为三级,从最具体到最兜底:cfg.Actor(ENGRAM_MEMORYLAKE_ACTOR/ 配置文件)/defaults/my-actor的custom_iddisplay_name作 actor 显示名,比把user::<uuid>当显示名可读engram第 2 步失败是正常结果而非错误:没有该端点的旧部署会落到第 3 步,因此不会对其产生回归。
仍然走
EnsureActor而不是直接用端点返回的id——EnsureActor尾部那步"绑定到 workspace 并容忍BINDING_ALREADY_EXISTS"是必要的,用 id 直连会跳过绑定。代价是 actor 未配置时每次构造 backend 多一次 GET,与本来就有的
ResolveWorkspaceID+EnsureActor同量级。真实环境验证(不只是 mock)
用已安装到 PATH 的新二进制、清空 actor 配置、打真实 MemoryLake:
对照组:改动前同样在 actor 未配置下跑,归属是
actor-35f7a126…(Chuangxiandebijibendiannao.local)。单元测试
internal/memorylake新增 3 个用例(先红后绿):TestNewBackend_ActorFromMyActorEndpointcustom_id+display_name建 actorTestNewBackend_ConfiguredActorWinsOverMyActort.Fatalf)TestNewBackend_FallsBackToHostnameWhenMyActorUnavailable为什么改了 cmd/engram 的测试
新增的请求打破了
cmd/engram里 10 个 routing 测试 + 1 个 turn 测试 —— 它们的 fake server 对未知路径t.Fatalf。这些测试考的是后端选择与 turn 捕获,不是身份解析,所以统一钉上ENGRAM_MEMORYLAKE_ACTOR(routing_test.go用新增的routingTestActor常量,routing_cli_test.go与turn_test.go走 env),既保持它们的关注点,也让它们以后不再受解析逻辑变化影响。go test ./...剩余 8 个失败与git stashbaseline 逐条相同(cmd/engram2 个、plugin5 个 + 1 个子用例),与本 PR 无关。go test -tags e2e ./internal/memorylake/ ./internal/server/全绿。真机验证抓到一个回归,已在本 PR 内修掉
只跑 mock 测试不会发现:actor 一旦从机器名换开,所有在旧 actor 下建立的 conversation 都会永久追加失败。MemoryLake 会校验发送者是否为该会话的参与者:
逐轮同步不重试,所以这不是一次抖动,而是那个会话余下所有轮次静默丢失(只在
turn.log留一行)。是在本机检查真实 hook 路径时发现的 —— 当时这个 session 自己的 conversation 已经停在 4 条消息,失败在累积。修法(
fix(memorylake): append as a participant the conversation already has):新增senderFor—— 自己的 actor 在参与者列表里就用自己,否则用会话已有的参与者。这不损失任何东西:服务端本来就会把它自己的 per-conversation 参与者句柄盖在存下来的消息上;而新建的 conversation 仍带我们解析出的 actor,所以新会话照样归属到 key 所属用户。两个新用例:
TestAppendObservation_AppendsAsAnExistingParticipant(旧会话回落到已有参与者)、TestAppendObservation_KeepsOwnActorWhenItIsAParticipant(自己在列表里时不被覆盖)。真机验证用的正是本 session 那个一直在失败的 conversation:恢复追加,消息数 4 → 5,
turn.log无新增。发布
本分支含
feat:,按 SemVer 走 minor → v0.7.0,三个插件清单已一并 bump(chore(release)),否则release.yml的校验会在 GoReleaser 之前拦下发布。范围外
消息级的
actor_id不在本 PR 范围:新旧 conversation 的消息 actor 是两个不同 id 且GET /api/v3/actors/{id}均返回NOT_FOUND,而真实 actor 查得到 —— 那是服务端按 conversation 派生的内部参与者句柄,engram 无从控制。本 PR 只涉及 conversation 级归属。Closes #22