Skip to content

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

Open
Duang777 wants to merge 1 commit into
mainfrom
codex/goal-instance-m3-chat-session
Open

Duang777 wants to merge 1 commit 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).

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

@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
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

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