mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
Merge remote-tracking branch 'origin/worktree/session-format-03-v0-v1-migration' into worktree/session-format-04-live-assistant-stream
# Conflicts: # docs/event-producer-consumer.i18n.yaml # docs/event-producer-consumer.md # docs/event-producer-consumer.zh.md # packages/core/agent/src/runtime-types.ts
This commit is contained in:
@@ -86,6 +86,20 @@ export interface ISession {
|
||||
signal?: AbortSignal,
|
||||
requestId?: SessionRequestId,
|
||||
): Promise<RemoteResult<{ accepted: true }>>
|
||||
/**
|
||||
* Replace the latest current turn-opening human message and rerun from it.
|
||||
* @param messageSeq - selected durable user-message event.
|
||||
* @param expectedLastUserSeq - optimistic conversation revision captured by the editor.
|
||||
* @param text - replacement text; retained non-text blocks remain Host-owned.
|
||||
* @param signal - optional caller cancellation before admission.
|
||||
* @returns the committed replacement event seq, or a business error.
|
||||
*/
|
||||
edit(
|
||||
messageSeq: number,
|
||||
expectedLastUserSeq: number,
|
||||
text: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<RemoteResult<{ accepted: true; messageSeq: number }>>
|
||||
/**
|
||||
* Resolve one durable image referenced by this session.
|
||||
* @param attachmentId - opaque id found in the folded session log.
|
||||
|
||||
@@ -52,12 +52,26 @@ export interface PendingSubmission {
|
||||
readonly images: readonly PendingSubmissionImage[]
|
||||
}
|
||||
|
||||
/** One local same-session edit awaiting its durable replacement message. */
|
||||
export interface PendingEdit {
|
||||
/** RPC identity echoed by the replacement `user/message`. */
|
||||
readonly requestId: SessionRequestId
|
||||
/** Existing user-message event being replaced. */
|
||||
readonly targetSeq: number
|
||||
/** Latest human message observed when edit mode opened. */
|
||||
readonly expectedLastUserSeq: number
|
||||
/** Replacement text submitted by the inline editor. */
|
||||
readonly text: string
|
||||
/** Client wall-clock ms used by the optimistic replacement bubble. */
|
||||
readonly time: number
|
||||
}
|
||||
|
||||
/** History-open lifecycle of a Session event window. */
|
||||
export type OpenState = 'cold' | 'loading' | 'open' | 'error'
|
||||
|
||||
/** Send/stop failure surfaced by Session consumers. */
|
||||
export interface PromptError {
|
||||
readonly op: 'send' | 'stop'
|
||||
readonly op: 'send' | 'stop' | 'edit'
|
||||
readonly error: RemoteFailure
|
||||
}
|
||||
|
||||
@@ -67,6 +81,8 @@ export interface SessionSnapshot {
|
||||
readonly queue: readonly QueuedMessage[]
|
||||
/** Local prompt-submission echoes not yet observed as durable events or queue occurrences. */
|
||||
readonly pendingSubmissions: readonly PendingSubmission[]
|
||||
/** Same-session edit waiting for its replacement user message. */
|
||||
readonly pendingEdit: PendingEdit | null
|
||||
readonly running: boolean
|
||||
readonly subagent: {
|
||||
readonly address: SubagentAddress
|
||||
|
||||
@@ -22,7 +22,7 @@ import type {
|
||||
BeginSubmissionInput, PendingSubmissionRetirement, SessionFace, SubmissionHandle,
|
||||
} from '../contract/session.ts'
|
||||
import type {
|
||||
OpenState, PendingSubmission, PromptError, SessionSnapshot,
|
||||
OpenState, PendingEdit, PendingSubmission, PromptError, SessionSnapshot,
|
||||
} from '../contract/snapshot.ts'
|
||||
import { MutableSessionEventSource } from '../contract/events.ts'
|
||||
import type {
|
||||
@@ -119,6 +119,7 @@ export class Session implements SessionFace {
|
||||
private lastAgentError: string | null = null
|
||||
/** Local submission echoes, insertion-ordered (see SessionSnapshot.pendingSubmissions). */
|
||||
private pendingSubmissions: readonly PendingSubmission[] = []
|
||||
private pendingEdit: PendingEdit | null = null
|
||||
/** Per-echo settlement state; `retiring` latches the first observation so a
|
||||
* queue frame and its durable event cannot both retire one echo. */
|
||||
private readonly submissionSettlements = new Map<SessionRequestId, {
|
||||
@@ -285,6 +286,45 @@ export class Session implements SessionFace {
|
||||
return result
|
||||
}
|
||||
|
||||
/** Submit one same-session edit with an immediate optimistic replacement. */
|
||||
async edit(
|
||||
messageSeq: number,
|
||||
expectedLastUserSeq: number,
|
||||
text: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<RemoteResult<{ accepted: true; messageSeq: number }>> {
|
||||
const requestId = randomUUID() as SessionRequestId
|
||||
this.pendingEdit = { requestId, targetSeq: messageSeq, expectedLastUserSeq, text, time: Date.now() }
|
||||
this.promptError = null
|
||||
this.lastAgentError = null
|
||||
this.notifier.markDirty()
|
||||
let result: RemoteResult<{ accepted: true; messageSeq: number }>
|
||||
try {
|
||||
result = await this.remote.session.edit({
|
||||
requestId,
|
||||
sessionId: this.sessionId,
|
||||
messageSeq,
|
||||
expectedLastUserSeq,
|
||||
text,
|
||||
clientTimeZone: resolvedClientTimeZone(),
|
||||
}, signal)
|
||||
} catch (error: unknown) {
|
||||
const cleared = this.clearPendingEdit(requestId)
|
||||
if (isRemoteFailure(error)) {
|
||||
this.promptError = { op: 'edit', error }
|
||||
this.notifier.markDirty()
|
||||
return { ok: false, error }
|
||||
}
|
||||
if (cleared) this.notifier.markDirty()
|
||||
throw error
|
||||
}
|
||||
if (!result.ok && this.clearPendingEdit(requestId)) {
|
||||
this.promptError = { op: 'edit', error: result.error }
|
||||
this.notifier.markDirty()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve one image referenced by this session into browser-consumable bytes.
|
||||
* @param attachmentId - opaque id found in the folded session log.
|
||||
@@ -588,6 +628,12 @@ export class Session implements SessionFace {
|
||||
|
||||
// ---- Private ----
|
||||
|
||||
private clearPendingEdit(requestId: SessionRequestId): boolean {
|
||||
if (this.pendingEdit === null || this.pendingEdit.requestId !== requestId) return false
|
||||
this.pendingEdit = null
|
||||
return true
|
||||
}
|
||||
|
||||
/** @param generation - openGeneration at launch; stale passes cannot publish after replacement. */
|
||||
private async doOpen(generation: number): Promise<void> {
|
||||
this.openState = 'loading'
|
||||
@@ -696,6 +742,17 @@ export class Session implements SessionFace {
|
||||
|
||||
/** Retire the matching echo when a durable browser-prompt `user/message` becomes visible. */
|
||||
private observeSubmissionEvent(event: { readonly type: string; readonly data?: unknown }): void {
|
||||
if (event.type === 'user/message' && this.pendingEdit !== null) {
|
||||
const source = (event.data as { readonly source?: { readonly rpcId?: unknown } } | undefined)?.source
|
||||
if (source?.rpcId === this.pendingEdit.requestId) {
|
||||
const requestId = this.pendingEdit.requestId
|
||||
scheduleFrame(() => {
|
||||
if (this.pendingEdit?.requestId !== requestId) return
|
||||
this.pendingEdit = null
|
||||
this.notifier.markDirty()
|
||||
})
|
||||
}
|
||||
}
|
||||
if (this.submissionSettlements.size === 0 || event.type !== 'user/message') return
|
||||
// Structural read: window entries may be compact history records, so the
|
||||
// fields are narrowed rather than trusted (same posture as Conversation
|
||||
@@ -769,6 +826,7 @@ export class Session implements SessionFace {
|
||||
sessionId: this.sessionId,
|
||||
queue: this.queueMirror.snapshot(),
|
||||
pendingSubmissions: this.pendingSubmissions,
|
||||
pendingEdit: this.pendingEdit,
|
||||
running: this.running,
|
||||
subagent: this.address === undefined
|
||||
? null
|
||||
|
||||
Reference in New Issue
Block a user