mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(web): fence plan mode projection races
This commit is contained in:
@@ -18,7 +18,7 @@ Which plugins mount and with what defaults is decided only here — shells must
|
||||
|
||||
## ApiProxy implementation notes
|
||||
|
||||
Unary methods take the narrow `RpcRequest<P>` and echo `request.rpcId`; a prompt's rpcId rides `MessageSource` into the `user/message` event so clients can promote optimistic echoes. `history`/`prompt` on a cold session implicitly resume it, deduplicating concurrent calls through an in-flight table; `history` paginates backwards on message boundaries (never mid-message). `planMode` and `setPlanMode` use the same resume path, project the optional `ctx.planMode` service, and return `null` when it is not mounted. The mux stream replays a `session/subscribed` baseline per attached session and every still-pending question with its original rpcId. Question responses, including blank per-item answers, are validated against the owning session and exact request before an atomic first-wins claim; answer, whole-request cancellation, owner abort, and provider disposal broadcast `question/resolved`. The host stream carries session lifecycle, running flips, and `agent/error` as the only outlet for live failures with no turn position.
|
||||
Unary methods take the narrow `RpcRequest<P>` and echo `request.rpcId`; a prompt's rpcId rides `MessageSource` into the `user/message` event so clients can promote optimistic echoes. `history`/`prompt` on a cold session implicitly resume it, deduplicating concurrent calls through an in-flight table; `history` paginates backwards on message boundaries (never mid-message). `planMode` and `setPlanMode` use the same resume path, project the optional `ctx.planMode` service, canonicalize a net-zero cleanup intent by omitting `pending`, and return `null` when the service is not mounted. The mux stream replays a `session/subscribed` baseline per attached session and every still-pending question with its original rpcId. Question responses, including blank per-item answers, are validated against the owning session and exact request before an atomic first-wins claim; answer, whole-request cancellation, owner abort, and provider disposal broadcast `question/resolved`. The host stream carries session lifecycle, running flips, and `agent/error` as the only outlet for live failures with no turn position.
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ import type { JsonValue, Session, SessionEvent, SessionHeader, SessionId } from
|
||||
import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence'
|
||||
import { foldSessionTitle } from '@deepseek-ai/dsh-session-title'
|
||||
import type {
|
||||
ApiProxy, HistoryEntry, HostFrame, MuxFrame, QuestionResponsePayload, SessionSummary, ToolEventView,
|
||||
ApiProxy, HistoryEntry, HostFrame, MuxFrame, PlanModeState, QuestionResponsePayload, SessionSummary,
|
||||
ToolEventView,
|
||||
} from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||
import { questionResponsePayloadSchema } from '@deepseek-ai/dsh-host-apiproxy/api/questions.schema'
|
||||
import type { ClientResponse, RpcError, RpcReceipt, RpcRequest, RpcResponse } from '@deepseek-ai/dsh-host-apiproxy/api/rpc'
|
||||
@@ -65,6 +66,16 @@ function ok<T>(request: RpcRequest<unknown>, value: T): RpcResponse<T> {
|
||||
return { rpcId: request.rpcId, result: { ok: true, value } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Project the service's boundary-cleanup intent into canonical wire state.
|
||||
* A target equal to the committed value has no user-visible pending effect.
|
||||
*/
|
||||
function projectPlanModeState(state: PlanModeState): PlanModeState {
|
||||
return state.pending !== undefined && state.pending === state.active
|
||||
? { active: state.active }
|
||||
: state
|
||||
}
|
||||
|
||||
/** Wrap an error result echoing the request's rpcId. */
|
||||
function err<T>(request: RpcRequest<unknown>, error: RpcError): RpcResponse<T> {
|
||||
return { rpcId: request.rpcId, result: { ok: false, error } }
|
||||
@@ -466,7 +477,7 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
const found = await agentFor(request.payload.sessionId)
|
||||
if ('error' in found) return err(request, found.error)
|
||||
const planMode = ctx.get('planMode')
|
||||
return ok(request, planMode?.get(found.agent) ?? null)
|
||||
return ok(request, planMode === undefined ? null : projectPlanModeState(planMode.get(found.agent)))
|
||||
},
|
||||
|
||||
async setPlanMode(request) {
|
||||
@@ -475,7 +486,7 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
const planMode = ctx.get('planMode')
|
||||
if (planMode === undefined) return ok(request, null)
|
||||
planMode.set(found.agent, request.payload.active)
|
||||
return ok(request, planMode.get(found.agent))
|
||||
return ok(request, projectPlanModeState(planMode.get(found.agent)))
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -256,8 +256,8 @@ describe('sessions.planMode / setPlanMode', () => {
|
||||
})
|
||||
expect(expectOk(await running.api.sessions.setPlanMode(request({ sessionId, active: false })))).toEqual({
|
||||
active: false,
|
||||
pending: false,
|
||||
})
|
||||
expect(expectOk(await running.api.sessions.planMode(request({ sessionId })))).toEqual({ active: false })
|
||||
})
|
||||
|
||||
it('returns the normal session-not-found error for both methods', async () => {
|
||||
|
||||
Reference in New Issue
Block a user