revert(cua): remove unrelated Session presence recovery

This commit is contained in:
creatixchu committed 2026-10-25 16:07:14 +08:00
1 parent 57f2832347
commit 31044bf469
7 files changed
+9 -59

No files matched your search

@@ -15,8 +15,8 @@
en: 1fdf707322fb6996
zh: 0204f7e9d4aa3cce
/session-controller/use-this-package:
en: ae5b60a56f67dcf0
zh: 04f64e8891cba464
en: 0c8945a47f87b5f7
zh: b099d66521dca175
/session-controller/client-references:
en: 41bdda1ac32df558
zh: aa5d0e325924066b
+1 -1
View File
@@ -45,7 +45,7 @@ Client list rows and resident Sessions use the current `sessionListMetadata` pro
`session.create` returns a Session for a new conversation, and its response names the Session actually used. A named `sessionId` is reused while it is blank (no turn has started), belongs to the requested directory, is an unarchived member of the requested Workspace, and is not written by another owner; a live Session is reused in place, and a persisted one is resumed while retaining its writer lock. The reused Session runs the requested `agentPreset`, or the configured default when the request names none: its recorded preset is not resolved, and a change is logged as `agent-preset/selected` before the Agent is published. A named Session that has started, fails the Workspace placement, or is written by another owner stays unchanged, and a new Session is created instead. A `sessionId` with no stored Session becomes the new Session's identity. A named Session in another directory fails with `session/conflict`, and one owned by subagent routing with `session/agent-busy`; both checks precede preset resolution. Creations naming one identity run in arrival order, including their preset resolution, and each response reports the preset its own request left the Session running. `session.list` includes persisted blanks using cached metadata, without opening cold log bodies.
Client list refreshes retain unchanged row objects and reuse the items array when order and values match. Each row's `retainedBy` contains positive local reference-source counts; Host metadata refreshes cannot overwrite them. Cache membership checks use a per-refresh ID set, so reconciliation grows linearly with the current list and retained cache sizes. Host summary updates replace running and Agent availability; local create/fork responses only fill missing metadata on existing rows. Removed ordinary Sessions retain projection stores only when their catalogs contain children. A current Host addition or reconciled list restores a retained Session after plugin reload, preserving its draft and clearing the removed state; a removal received during a list request takes precedence over that response.
Client list refreshes retain unchanged row objects and reuse the items array when order and values match. Each row's `retainedBy` contains positive local reference-source counts; Host metadata refreshes cannot overwrite them. Cache membership checks use a per-refresh ID set, so reconciliation grows linearly with the current list and retained cache sizes. Host summary updates replace running and Agent availability; local create/fork responses only fill missing metadata on existing rows. Removed ordinary Sessions retain projection stores only when their catalogs contain children.
Background-job rows and observation streams belong to [`dsh-api-job-controller`](../job-controller/README.md); the control stream carries projections only.
+1 -1
View File
@@ -45,7 +45,7 @@ Client 列表行和驻留 Session 使用当前 `sessionListMetadata` 投影纠
`session.create` 返回一个用于新对话的 Session,响应指明实际使用的 Session。请求指明的 `sessionId` 在满足以下条件时被复用:仍为空白(尚未开始任何轮次)、属于请求的目录、是请求的工作区中未归档的成员,且未被其他写者占用;活动 Session 原地复用,持久化 Session 被恢复并持续持有其写锁。被复用的 Session 运行请求的 `agentPreset`,请求未指明时运行配置的默认 preset:不解析它记录的 preset,变更在 Agent 发布前记录为 `agent-preset/selected`。指明的 Session 若已开始、不满足工作区归属,或被其他写者占用,则保持不变,改为新建 Session。没有对应已存储 Session 的 `sessionId` 成为新 Session 的标识。指明其他目录中的 Session 返回 `session/conflict`,指明由 subagent 路由拥有的 Session 返回 `session/agent-busy`;这两项检查先于 preset 解析。指明同一标识的创建请求按到达顺序执行,preset 解析也包含在内,每个响应报告其自身请求完成时该 Session 运行的 preset。`session.list` 根据缓存元数据列出持久化空白会话,不打开冷日志正文。
Client 列表刷新保留未变化的行对象,并在顺序和值均相同时复用条目数组。每行的 `retainedBy` 包含正数的本地引用来源计数,Host 元数据刷新不能覆盖它们。缓存成员检查使用每次刷新构建的 ID 集合,因此对账成本随当前列表和保留缓存的规模线性增长。 Host 摘要更新会替换运行状态与 Agent 可用性;本地 create/fork 响应只补充已有行缺失的元数据。普通 Session 被移除后,仅在目录仍有子项时保留其投影 store。 当前 Host 新增事件或对账后的列表会恢复插件重载前保留的 Session,清除移除状态并保留草稿;列表请求期间收到的移除事件优先于该次响应。
Client 列表刷新保留未变化的行对象,并在顺序和值均相同时复用条目数组。每行的 `retainedBy` 包含正数的本地引用来源计数,Host 元数据刷新不能覆盖它们。缓存成员检查使用每次刷新构建的 ID 集合,因此对账成本随当前列表和保留缓存的规模线性增长。 Host 摘要更新会替换运行状态与 Agent 可用性;本地 create/fork 响应只补充已有行缺失的元数据。普通 Session 被移除后,仅在目录仍有子项时保留其投影 store。
后台 job 的名册行与观测流属于 [`dsh-api-job-controller`](../job-controller/README.zh.md);控制流只承载投影。
@@ -451,7 +451,6 @@ export class SessionManager {
for (const s of this.summaries) {
const session = this.sessions.get(s.sessionId)
if (session === undefined) continue
session.handlePresence(true)
session.handleBlank(this.effectiveBlank(s))
session.handleRunning(s.running)
}
@@ -672,7 +671,6 @@ export class SessionManager {
handleSessionAdded(summary: SessionSummary): void {
this.mergeSummary(summary)
if (!this.disposed && summary.running) this.engagedSessions.add(summary.sessionId)
this.sessions.get(summary.sessionId)?.handlePresence(true)
this.sessions.get(summary.sessionId)?.handleBlank(this.effectiveBlank(summary))
if (summary.projections !== undefined) this.applyListBlock(summary.sessionId, summary.projections)
}
@@ -712,7 +710,7 @@ export class SessionManager {
? { kind: 'status', sessionId, running: false, agentAvailable: false }
: { kind: 'remove', sessionId })
if (durableSubagent) this.sessions.get(sessionId)?.handleRunning(false)
else this.sessions.get(sessionId)?.handlePresence(false)
else this.sessions.get(sessionId)?.handleRemoved()
const catalog = this.projectionStores.get(sessionId)?.values().subagentCatalog
if (!durableSubagent && (catalog === undefined || catalog.length === 0)) {
this.projectionStores.delete(sessionId)
@@ -577,13 +577,9 @@ export class Session implements SessionFace {
this.notifier.markDirty()
}
/**
* Update Host membership while retaining the resident instance and its draft.
* @param present - Whether a current Host event or reconciled list contains this Session.
*/
handlePresence(present: boolean): void {
if (present === !this.removed) return
this.removed = !present
/** `api-session/removed` relay: flag the snapshot while retaining the resident instance. */
handleRemoved(): void {
this.removed = true
this.notifier.markDirty()
}
@@ -840,17 +840,10 @@ describe('remaining branches', () => {
it('deduplicates repeated running flips and records removal', async ({ mock, start }) => {
const session = await sessionBench(mock, start, SID)
const before = session.getSnapshot()
session.handlePresence(true)
expect(session.getSnapshot()).toBe(before)
session.handleRunning(false) // already false: dedup branch
expect(session.getSnapshot()).toBe(before)
session.handlePresence(false)
session.handleRemoved()
expect(session.getSnapshot().removed).toBe(true)
const removed = session.getSnapshot()
session.handlePresence(false)
expect(session.getSnapshot()).toBe(removed)
session.handlePresence(true)
expect(session.getSnapshot().removed).toBe(false)
})
it('drops live events while cold/error (no window upkeep)', async ({ mock, start }) => {
@@ -1072,43 +1072,6 @@ describe('catalog arrival', () => {
})
})
describe('Host membership recovery', () => {
it.for(['event', 'list'] as const)('restores a retained Session through a current %s after plugin reload', async (source, { bench }) => {
const b = bench()
await feedList(b, [{ id: 's1' }])
using reference = b.svc.retain(sid('s1'), { source: 'controllerOperation' })
await reference.ready
const session = reference.binding.session
b.svc.handleSessionRemoved(sid('s1'))
expect(session.getSnapshot().removed).toBe(true)
if (source === 'event') {
b.svc.handleSessionAdded({ agentAvailable: true, sessionId: sid('s1'), updatedAt: 2, running: false, blank: false })
} else {
await feedList(b, [{ id: 's1' }])
}
expect(b.svc.binding(sid('s1'))).toBe(reference.binding)
expect(session.getSnapshot().removed).toBe(false)
})
it('keeps a removal newer than an in-flight list response', async ({ bench }) => {
const b = bench()
await feedList(b, [{ id: 's1' }])
using reference = b.svc.retain(sid('s1'), { source: 'controllerOperation' })
await reference.ready
const gate: PromiseWithResolvers<void> = Promise.withResolvers()
b.unblock.push(() => { gate.resolve() })
b.mock.remote.session.list.mockImplementation(async () => {
await gate.promise
return ok({ items: [{ agentAvailable: true, sessionId: sid('s1'), updatedAt: 1, running: false, blank: false }] })
})
const refresh = b.svc.refresh()
b.svc.handleSessionRemoved(sid('s1'))
gate.resolve()
await refresh
expect(reference.binding.session.getSnapshot().removed).toBe(true)
})
})
describe('blank mirror', () => {
it('flips blank=false from the running:true status frame (cross-client conversion)', async ({ bench }) => {
const b = bench()