--- a/dsh-host-apiproxy/lib/index.js +++ b/dsh-host-apiproxy/lib/index.js @@ -0,0 +0,0 @@ }); /** session.fork response value (the child session id). */ const sessionForkValueSchema = z$1.object({ sessionId: sessionIdSchema }); +/** session.truncate request payload (atSeq discards that event and everything after from this session log). */ +const sessionTruncateRequestSchema = z$1.object({ + sessionId: sessionIdSchema, + atSeq: z$1.number().int().nonnegative() +}); +/** session.truncate response value (the new last seq after the cut). */ +const sessionTruncateValueSchema = z$1.object({ lastSeq: z$1.number().int().nonnegative() }); /** session.history request payload (beforeSeq/maxMessages page backwards from the window tail). */ const sessionHistoryRequestSchema = z$1.object({ sessionId: sessionIdSchema, @@ -0,0 +0,0 @@ } return ok(request, { sessionId: childId }); }, + async truncate(request) { + const { sessionId, atSeq } = request.payload || {}; + if (typeof atSeq !== "number" || !Number.isSafeInteger(atSeq) || atSeq < 0) return err(request, { code: "invalid-arg", message: "atSeq must be a non-negative safe integer", details: {} }); + // truncate mutates the live session log; require the attached Session instance + // (readSessionState returns an immutable {id,header,events} snapshot with no truncate). + const session = ctx.sessions.get(sessionId); + if (session === void 0) return err(request, { code: "session-not-found", message: `session "${sessionId}" not found`, details: { sessionId } }); + try { + const newSeq = session.truncate(atSeq); + // truncate leaves the token-meter's per-session incremental fold cursor + // stale (cursor > log length), so the next measure/compaction folds an + // assistant/message whose step/start was already cut and throws + // "assistant/message at seq ... has no matching step/start event". + // Drop the cached fold so measure re-folds the shortened log from scratch. + const meter = ctx.get && ctx.get("tokenMeter"); + if (meter && typeof meter.states === "object" && typeof meter.states.delete === "function") { try { meter.states.delete(session); } catch (me) {} } + const persist = ctx.get && ctx.get("sessionPersistence"); + // truncate may roll the cut back to a clean step boundary (newSeq < atSeq); + // persist the SAME cut so the durable artifact matches the live log. + if (persist && typeof persist.truncate === "function") { try { await persist.truncate(sessionId, newSeq); } catch (pe) {} } + if (ctx.sessions && typeof ctx.sessions.flush === "function") { try { ctx.sessions.flush(session); } catch (fe) {} } + broadcast({ type: "session/subscribed", sessionId, lastSeq: newSeq - 1 }); + broadcast({ type: "session/reload", sessionId, lastSeq: newSeq - 1 }); + return ok(request, { lastSeq: newSeq - 1 }); + } catch (error) { + return err(request, { code: "internal", message: String(error), details: {} }); + } + }, async prompt(request) { const { sessionId, mode, content, clientTimeZone } = request.payload; const canonicalTimeZone = clientTimeZone === void 0 ? void 0 : canonicalClientTimeZone(clientTimeZone); @@ -0,0 +0,0 @@ schema: sessionForkRequestSchema, invoke: (api, r) => api.sessions.fork(r) }, + "session.truncate": { + schema: sessionTruncateRequestSchema, + invoke: (api, r) => api.sessions.truncate(r) + }, "session.prompt": { schema: sessionPromptRequestSchema, invoke: (api, r) => api.sessions.prompt(r) @@ -0,0 +0,0 @@ "session.selectModel": sessionSelectModelValueSchema, "session.rename": sessionRenameValueSchema, "session.fork": sessionForkValueSchema, + "session.truncate": sessionTruncateValueSchema, "session.prompt": sessionPromptValueSchema, "session.attachment": sessionAttachmentValueSchema, "session.updateQueue": sessionUpdateQueueValueSchema, @@ -0,0 +0,0 @@ selectModel: (payload, signal) => this.callUnary("session.selectModel", payload, signal), rename: (payload, signal) => this.callUnary("session.rename", payload, signal), fork: (payload, signal) => this.callUnary("session.fork", payload, signal), + truncate: (payload, signal) => this.callUnary("session.truncate", payload, signal), prompt: (payload, signal) => this.callUnary("session.prompt", payload, signal), attachment: (payload, signal) => this.callUnary("session.attachment", payload, signal), updateQueue: (payload, signal) => this.callUnary("session.updateQueue", payload, signal),