Skip to content

feat(chat): bind attached sessions to GoalRef - #5130

Open
Duang777 wants to merge 3 commits into
mainfrom
codex/goal-instance-m3-chat-session
Open

Duang777 wants to merge 3 commits into
mainfrom
codex/goal-instance-m3-chat-session

Conversation

@Duang777

Copy link
Copy Markdown
Collaborator

Goal And Delivered Outcome

  • Goal/source and gap: qualify the attached_host_chat_session owner for the source-session Goal lifetime model. Attached Chat sessions previously persisted only goal_id, so a recreated Goal could reuse or mutate work from the retired lifetime.
  • Observable before -> after, with the validation row that proves it: attached bind, enqueue, resume, claim, replay, and completion now use the exact current GoalRef under the M2 lifetime guard. Historical admitted work may return to its original Session, while new work against a stale Session is rejected.
  • Issue/task and intended base: Related to fix(app): preserve conversational intent and plan App-first continuity #5064. Intended base is main; this change is independent of feat(collaboration): bind inbox continuity to GoalRef #5106.

Scope And Continuation

  • Completed scope and remaining work: this PR qualifies the attached_host_chat_session inventory row. It keeps managed provider startup blocked for source_session_v1, leaves public broker payloads unchanged, and preserves legacy serialized bytes when the profile is not active.
  • Slice boundary / successor: first_party_host_runtime and the remaining M3 owner rows retain the overall activation hold. No Desktop or UI code changes are included.

Validation

  • Tested revision: f7b48f7e4a9417a41f7f1cae58b4c0c10ea33d00
  • Run state: finished
  • Input classes: synthetic, public_fixture
Check kind Result Public-safe evidence / limitation
unit passed npm run test:control-plane: 3154 tests, 3124 passed, 30 environment-gated skips, 0 failures.
integration passed 84 Chat/attached and architecture tests passed, including Goal A/B isolation, historical result return, registry disappearance, CLI binding, queue/claim behavior, and resume/recreation lock ordering.
static passed Targeted ruff check, npm run typecheck:control-plane, git diff --check origin/main, and the project-registry I/O manifest validator passed.
real_entrypoint passed loopx canary premerge --from-git-diff: 19/19 checks passed across control-plane, docs, public-boundary, and Python surfaces; no manual holds.
regression_parity passed Non-source writers omit Goal instance fields; public Session and broker payloads omit private lifetime identity; missing-registry fallback remains available only when no exact Session history exists.
  • Coverage and gaps: no external provider process is started because this slice deliberately blocks managed provider startup for the source-session profile. The attached-host broker and CLI paths run against real filesystem registries and Session stores with synthetic fixtures.

See validation disclosure guidance.

Frontend / Visual Evidence

  • UI impact: none
  • Before: N/A
  • After: N/A
  • States and viewports shown: N/A
  • Source data: none
  • Attention review: N/A; public response shapes remain unchanged and no visual surface changed.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Refactoring (no functional changes)
  • Documentation update
  • Test update

LoopX Area

  • Control plane (goals, todos, quota, scheduler, registry, runtime)
  • Benchmark boundary (adapters, runners, verifiers, scoring, evidence)
  • Capability or extension (providers, adapters, skills)
  • Public docs or presentation surface (README, protocols, dashboard)
  • Build, packaging, installer, or CI
  • Host or runtime integration

Technical Direction

  • Direction / acceptance reference, when applicable: Shared Goal Authority and cross-host coordination; M3 attached_host_chat_session qualification in goal-instance-identity-and-orphan-recovery-v0.

Shared-authority RFC fixture impact

  • Production-scale fixture schema: no authority-store fixture schema change. The owner-local Chat Session and Turn records gain optional exact instance fields only for source_session_v1.
  • Semantic dimensions changed, or reviewed no-impact rationale: attached Session selection and new-work admission are exact-GoalRef decisions owned by TypeScript. Historical completion requires the persisted Turn admission to match its Session lifetime.
  • Provider conformance arms run: file-backed source registry plus owner-local Chat Session store; the 84-test integration set covers bind, enqueue, claim, replay, completion, resume, and Goal recreation.
  • Read-only legacy/file/PostgreSQL three-arm rehearsal (required for promotion, runtime-routing, or compatibility-projection changes): not applicable. This PR does not promote an authority provider or change runtime routing, and managed provider startup remains blocked.

Boundary Checklist

  • Neither the diff nor this PR body/comments/attachments disclose private state, credentials, raw traces or verifier output, internal links, or local machine paths (including .loopx/, .codex/goals/, and live ACTIVE_GOAL_STATE.md).
  • I did not duplicate maintainer-owned benchmark work unless a maintainer split out a public issue for it.
  • I kept the change scoped to the linked issue/task.
  • I completed the visual evidence section for UI changes, or marked UI impact none.
  • Every commit includes a DCO Signed-off-by trailer (git commit -s).

@Duang777 Duang777 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Exact-head self-review

Reviewed commit: f7b48f7e4a9417a41f7f1cae58b4c0c10ea33d00

Verdict: no blocking findings; ready for maintainer review.

The review checked Goal A/B isolation, stale enqueue/claim/resume rejection, historical completion admission, missing-registry fail-closed behavior after exact history exists, legacy byte compatibility, public payload omission, and lifetime-lock ordering. A resume TOCTOU found during review was fixed before this head and covered by a concurrent recreation regression test.

Validation on this exact head:

  • 84 Chat/attached and architecture tests passed.
  • TypeScript control-plane suite: 3154 tests, 3124 passed, 30 environment-gated skips, 0 failures.
  • Ruff, TypeScript typecheck, registry I/O manifest validation, and diff checks passed.
  • Standard premerge gate passed 19/19 checks with no manual holds.

Scope remains limited to attached_host_chat_session; managed provider startup and the remaining M3 owner rows stay blocked. This review does not grant merge authority.

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

动机

目标是把 attached-host chat 的 Session 与真正的 Goal 实例绑定起来,避免同一 Goal key 被重建以后,旧会话继续获得新实例的执行权。这个 M3 增量有真实调用路径,也复用了现有身份词汇;但不能仅凭 stamped Session 的成功路径就宣称整个 attached family 已 qualified。保留的未绑定 Session 同样是启用后必须安全处理的对象。

改动思路

TS lifecycle 决策复用已有 source-session lifetime/GoalRef,Python 负责传递事实、持锁和写入,方向合理。Session 的实例绑定与 Turn 的 admitted binding 是历史权威事实,不是需要用户额外手工维护的展示字段;当前 GoalRef 不能代替旧 Turn 的已接受身份。

普通非 source 项目仍走既有格式;已经接受的历史 A Turn 可以完成或重放自身结果,而不能给 B 实例新的执行权。关键问题是兼容路径必须由权威 execution profile 决定,不能由某条记录“有没有 stamp”决定。

具体改动

全 PR 为 17 个文件、1785 行新增/62 行删除,包含生产接线、两个内部身份字段、typed decision/RPC、针对性测试、registry IO manifest 和双语 M3 状态说明。API/UI 继续使用 opaque Session id,没有把私有实例身份变成客户端输入;现有 controller 与 worker broker 确实接入,不是只写 serializer。源码路径改变了 attached-host 行为,没有实现或开放整个 firstparty_runtime family,RFC 中的其他 hold 应继续保留。

关键代码讲解

  • decideChatSessionLifecycle(TS 第 260 行):负责当前 Session admission 和历史 Turn return 的类型化判断,source profile 下缺少实例本应拒绝。
  • _current_attached_session_guard(Python 第 516 行):有 stamp 时持 lifetime 锁并重新读取;第 529 行却在没有 stamp 时直接 yield,resume 因而跳过 registry/typed gate。
  • enqueue_attached_agent_turn(第 593 行):第 608–614 行在没有 stamp 时直接创建 queued Turn;submit 和 enqueue 都到这条分支。
  • _claim_attached_turn_once(第 646 行):第 662 行同样直接 legacy claim,没有先确认 source profile 是否已经启用。

对主干的风险

[P1] 已启用的严格 profile 可被未绑定 Session 绕过(loopx/attached_session.py:608–614,同根因还有 529、662 行)。

复现条件:用现有测试的 _register(tmp_path) 创建真实 fresh source-session registry,再通过现有 store API 创建同 Goal/agent/host、但没有 goal_instance_id 的保留 Session。分别调用 controller submit、enqueue、resume 和 broker claim。四个独立反例全部接受并修改 Session,没有抛出契约要求的 goal_instance_id_missing。它们运行真实文件 backend,没有 mock admission 判定或写入结果。

最小 enqueue 复现(在 PR 根目录保存为一个 pytest 文件运行):

import runpy
import pytest
from loopx.chat_runtime import ChatRuntimeController
from loopx.chat_store import CHAT_SESSION_MODE_ATTACHED

helpers = runpy.run_path("tests/test_attached_session_goal_instance.py")

def test_strict_profile_rejects_unstamped_session(tmp_path):
    registry, _, store, _ = helpers["_register"](tmp_path)
    session = store.create_session(
        goal_id=helpers["GOAL_ID"],
        agent_id=helpers["AGENT_ID"],
        adapter_kind="attached_host_session",
        upstream_thread_id=helpers["HOST_SESSION_ID"],
        session_mode=CHAT_SESSION_MODE_ATTACHED,
        host_surface=helpers["HOST_SURFACE"],
    )
    runtime = ChatRuntimeController(
        store=store, codex_bin="missing-codex", registry_path=registry
    )
    with pytest.raises(ValueError, match="goal_instance_id_missing"):
        runtime.enqueue_turn(
            session_id=session["session_id"], client_turn_id="new",
            message="must reject", work_dir=tmp_path, objective="fixture"
        )
    assert store.turn_for_client(session["session_id"], "new") is None

运行 uv run --extra test python -m pytest -q <reproducer.py>,当前 head 会失败于 DID NOT RAISE。

最小修复是先读取权威 profile/current lifetime,再把未绑定 Session 也交给现有 typed decision;仅确认 profile 未启用后才允许 legacy 路径。不要通过自动给历史记录补 stamp 来消除反例。submit/enqueue/resume/claim/completion 都应验证“拒绝发生在任何 Session、Turn、ingress/event 写入之前”。

语义与 CI 对齐

这复用的是已有 GoalRef 语义,不是未来 RFC 属性的新门槛:现有 RFC §4 identity matrix 对当前实例缺少绑定要求 execution reject,§11 明确启用项目不能 legacy fallback。当前 Python dispatch 与这个义务冲突;inventory 的 m3_qualified/exact_goal_ref_enforced 需要在修复后才能成立。

我独立完成 91 个 Python 测试、18 个 TS 测试、control-plane typecheck、registry IO manifest(250 sites)、改动 Python 文件的 Ruff 与 diff check,均通过。起初本地缺 TypeScript 依赖造成一次架构测试环境错误,安装声明依赖后同项通过;它不是本 PR 的缺陷。未查询或等待远端 CI。阻塞来自额外四个真实负例,不来自无关红 CI。

我的整体评价

请求修改。long_horizon 与 user_experience 的严格身份承诺仍存在 regression:旧记录可以继续执行,却没有明确的缺绑定错误/恢复入口。与此同时,普通关闭路径的独立 base/head harness 覆盖 bind、submit、enqueue、claim、complete、resume 以及单独存储/公开 readback;仅规范化 UUID 和时间,完整结果一致,未发现默认关闭漂移。

范围本身是一个可审阅的 attached-host 增量,不要求把整个 RFC 或 TS 重构一并做完。相邻的 bounded refactor 建议与本修复一起完成:共享权威 profile/lifetime context,消除几处“null 就等于 legacy”的并行判断,继续让 TS 拥有语义。保留未启用项目的 persisted compatibility,但不能因此跳过已启用的约束。修复后请重跑当前实例/替换/历史完成正负例、上面的未绑定入口矩阵及同一 legacy base/head 对照,再重新判断完整 PR。

English verdict: REQUEST_CHANGES - exact head f7b48f7; active source-session admission is bypassed by unstamped attached Sessions in four independently reproduced public paths.

Comment thread loopx/attached_session.py Outdated
if session is None or session.get("status") == "closed":
raise KeyError("chat session was not found")
goal_instance_id = session.get("goal_instance_id")
if goal_instance_id is None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] Check the authoritative execution profile before taking the unstamped compatibility path. On an active source_session_v1 Goal, a retained attached Session without goal_instance_id reaches this branch and creates a queued Turn without registry/lifetime validation. Independent public controller submit/enqueue, resume and broker claim cases all accept and mutate state instead of raising goal_instance_id_missing. The existing TS lifecycle decision already rejects this state, but this branch never calls it. Select legacy only after verifying a non-source profile; otherwise reject before effects, and cover all public entrypoints plus no-effect readback while retaining inactive-profile parity.

@mergify

mergify Bot commented Sep 27, 2026

Copy link
Copy Markdown

This pull request has merge conflicts with main and cannot be merged
until they are resolved. Please rebase or merge the base branch, @Duang777.

Choose the remote for the base repository, not an out-of-date fork.
For a fork clone, first inspect git remote -v; upstream must point
to https://github.com/loopx-project/loopx.git. If it is absent, add it
with git remote add upstream https://github.com/loopx-project/loopx.git.
Then run:

git fetch upstream
git rebase upstream/main
# Resolve each conflict, git add the resolved files, then git rebase --continue.
git push --force-with-lease origin HEAD

For a same-repository clone whose origin points to
https://github.com/loopx-project/loopx.git, use origin instead of
upstream for fetch/rebase. If you prefer merging the base, use
git merge <base-remote>/main and push normally.

Keep the DCO Signed-off-by trailer on every commit when you rebase.
https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

Signed-off-by: duanjialing.777 <duanjialing.777@bytedance.com>
@Duang777
Duang777 force-pushed the codex/goal-instance-m3-chat-session branch from b0b5552 to 61d2b08 Compare September 28, 2026 05:55

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

结论:REQUEST_CHANGES — 一个 P1 阻塞项。精确审查 head:61d2b08a05fdb4062734e843c9969e2c66fdd09a,immutable base:6643f367064b9921c864db75a042979fddc4b8c3。发布前检测到 rebase 后重新建 checkout、重跑验证;PR 最终 diff 字节未变,没有沿用旧 head 的验证结论。

[P1] registry 缺失不能把已知 strict context 中的旧 Session 降级为 legacy。 _attached_session_lifetime 的缺失分支 只检查当前 Session 是否有 goal_instance_id。只要选择的是未绑定的保留 Session,它就返回 legacy context;即使同一 Goal/Agent/channel 已有 exact-bound Session 历史,也不会读取或拒绝该上下文。submit、enqueue、resume、claim、complete 随即绕过 TS admission 并写入真实状态。selection 路径 对同样情况却会拒绝。完整复现与最小修复见下文及行内评论。

动机

本 PR 对应 source-owned GoalRef 生命周期的 M3 attached-host Chat 切片:同名 Goal A 被 B 替换后,不应让 A 的旧 Session 获得 B 的新执行权;已对 A 持久化 admission 的迟到结果可以只返回 A。这是新鲜 source profile 的有限 owner qualification,不是整个 M3、既有项目 activation 或 managed provider 的完成。当前 RFC 的 missing-binding matrix、source 不可用时 fail-closed、禁止猜测/自动升级历史记录,是本轮判断依据。

改动思路

decideChatSessionLifecycle 在既有 TS effect-runtime 中拥有 select/admit/claim/replay/complete 的精确身份规则;Python 收集 source/session/turn facts,并在既有 source-session lifetime guard 内执行 File effects。Session、queued Turn 和 claim admission 记录可选 identity 字段,非 source writer 仍省略。正确方向是让 durable binding 决定受允许的关系,同时禁止 alias、历史记录或缺失 locator 生成新的 authority。

具体改动

17 个文件、+1,841/-55:attached_session 增加 source-aware bind/selection/shared lifetime 和受 guard 保护的 enqueue/resume/claim/complete;chat_runtime 接入这些路径并继续拒绝未资格化的 strict managed startup。chat_store 抽出完整/可恢复 candidate 枚举,保存可选 Session/Turn/admission identity;API 与 worker-bridge CLI 使用 source-aware registry owner。TS handler 与 tsconfig 注册新 decision,inventory/census 更新 owner/read locations,两份 RFC 记入 bounded M3 candidate,Python/TS/architecture 测试覆盖 ABA、历史结果、锁、CLI 与 compatibility。

registry 存在时,前次指出的 unstamped Session 绕过已修好:五个入口都拒绝且不改 Session/Turn。正向 ABA 对照也通过:A 已领取结果只写回 A,A 未领取队列拒绝继续,新建精确 B Session 可以 submit→claim→complete,重启读回与重复 completion 只有一条各自响应,B 不接受 A 的结果。

缺口在 registry 不存在的分支:exact_session=false 不是“profile 已关闭”的证据。它也不应与 selection 的 known-exact-history refusal 分裂成两套上下文解释。

对主干的风险

独立使用真实临时 source registry 与 File Session store,先保留同一上下文的 exact-bound 历史,再选择 unstamped Session,仅删除测试 registry。12 个隔离 arm 中,五个 mutation 入口全部错误接受并改变 business files;五个 exact Session 和两个 selection 对照全部正确拒绝、文件 SHA-256 不变。这不是只看错误码、mock admission 或篡改活跃 Goal。

同时跑了 21 个 legacy base/head 对照,覆盖 preview/bind/重复提交、queue、claim/replay、complete/replay、resume、公开读回及无 registry/无 exact history 的正常 legacy 流程。仅归一化随机 ID、时间和临时路径后,完整返回值及持久化字节一致;no-write 断言使用归一化前的文件 SHA-256,保留 ID 关系而不合并不同 Turn。应修 strict 降级,同时保留这些相反方向的 compatibility 控制。

在新 base/head 上完整重跑:head 108 项、base 94 项通过;各有同一个 census 失败,明确不作为该 PR 的阻塞项。失败 detail 是 loopx/contract.py::<module>.check_contract::codec_read:load_registry#1 metadata changed:main #5239 把该调用移到第 1027 行,两侧 manifest 都仍是 1014,且本 PR 未改动这一 row。18 TS 项、control-plane typecheck、changed-file Ruff 与配置中的 19 文件 mypy 通过。current source worktree 的 standard premerge 19 selected + 5 direct 命令全部通过;聚合 gate 为 quality_invalid_receipt,质量回执如实记录上述 P1 及原始 census 基线失败。review 已把 census 标为 pre_existing_unrelated,仅 P1 需要此 PR 修改;不拿普通绿测试覆盖这一缺口,也不因不相关 CI 要求改这个 PR。没有查询/等待 GitHub CI,未安装 candidate、调用 live provider、改变 active Goal 或迁移 store。UI 没有新增 identity 输入/视觉 schema,既有 public payload 隐藏 identity;这不代表 packaged App 或整个 M3 已完成。

我的整体评价

总体边界与 TS owner 选择合理,之前的修复和历史结果保护是实质进展;但 attached owner inventory 现在写 m3_qualified/exact_goal_ref_enforced,不能在这个入口仍 fail-open 时交付。最小修复是统一 source-context 判断:已知 strict 上下文遇到 source unavailable 应拒绝所有 mutation;历史只可提供否决证据,不能猜当前 GoalRef 或自动升级旧 Session。保留纯 legacy/no-exact-history 路径并补五入口 negative + opposite legacy control 后再资格化。

future-facing pass 已通过 shared lifetime/candidate seam 做了一部分;这次应在同一 owning boundary 修掉 selection 与 mutation 的缺失分支分歧,不扩成新的框架或平行 Python state machine。仅在限定 attached-host Chat owner 完整通过后恢复其 qualification;managed provider、first-party host 与整体 activation 的既有 holds 继续独立存在。

English verdict: REQUEST_CHANGES - 61d2b08; missing source registry lets five unstamped-session mutation entrypoints fall back to legacy despite exact-bound context history. Current strict/ABA and 21 legacy parity controls pass, but the reproduced fail-closed gap must be fixed before attached-owner qualification.

Comment thread loopx/attached_session.py Outdated
Comment on lines +484 to +488
if not registry_path.exists():
if exact_session:
raise FileNotFoundError(registry_path)
yield session, None, None
return

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] Do not downgrade a known strict source context when its registry is unavailable

This branch treats an unstamped selected Session as proof of legacy mode. That does not hold when an exact-bound Session already exists for the same Goal/Agent/channel. After only the disposable source registry is removed, all five real mutation paths (submit, enqueue, resume, claim, complete) accept the unstamped Session and change business files, while five exact-Session controls and both selection controls correctly refuse without writes. Selection already checks exact-bound context history at lines 521–540; mutations bypass that check here and never reach typed admission. Share the source-context availability rule: known strict context must fail closed when source is unavailable; history may supply refusal evidence but must not invent a current GoalRef or automatically stamp legacy records. Preserve genuine legacy/no-exact-history behavior, and cover all five operations with opposite legacy controls before marking the attached owner qualified.

@huangruiteng

Copy link
Copy Markdown
Collaborator

Frame alignment — exact head 61d2b08a05fdb4062734e843c9969e2c66fdd09a

REQUEST_CHANGES for one reproduced P1 in the current attached-host Chat owner. The identity/recovery RFC requires source-unavailable known-strict context to fail closed, while durable historical admission permits completion only to the original instance. This does not demand closure of all M3 or existing-project activation; managed-provider and first-party holds remain separate.

Publication freshness: detected the new rebase head, fetched a new detached checkout and immutable base6643f367, and reran qualification. Final PR diff is byte-identical, but old-head validation was not silently reused. Fresh six-arm ABA qualification reaches useful current-B completion/restart/idempotent readback, and21 complete legacy return/error/business-byte case arrays match base/head. The previous profile-present unstamped bypass is fixed.

Remaining P1: with exact-bound same-context history and only the disposable source registry removed, all five unstamped mutation entrypoints accept and write; five exact and two selection controls refuse without writes. Share selection/mutation source-context availability handling before inventory can truthfully claim attached m3_qualified/exact_goal_ref_enforced. History provides refusal evidence, never a guessed current identity or automatic stamping.

Fresh native current head108pass/1fail; immutable base94pass/1fail. The one census failure has the same named test/detail at each: unchanged contract.py manifest row points to1014 but main#5239 moved load_registry to1027. It is pre_existing_unrelated and not a PR blocker.18 TS and declared typecheck/Ruff/mypy pass. Current-source premerge19 selected+5 direct commands pass; quality_invalid_receipt preserves the reproduced P1 and raw baseline check report. Review requests changes only for P1; merge readiness and quality receipts are separate, and no hard check was relaxed.

The focused companion refactor is the existing shared source-context seam, not a parallel authority or framework. GitHub CI was not consulted. No installation, live Goal mutation, migration or merge occurred.

Signed-off-by: duanjialing.777 <duanjialing.777@bytedance.com>
Signed-off-by: duanjialing.777 <duanjialing.777@bytedance.com>

This branch has not been deployed

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

Labels

needs-rebase Mergify: the pull request has merge conflicts with its base branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants