Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/steer-frame-prompt-ids.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/kap-server": patch
"@moonshot-ai/transcript": patch
Comment thread
liruifengv marked this conversation as resolved.
---

Stamp cold-rebuilt steer frames with the promptIds paired from prompt.steered records, and flush pending steer/notification frames in arrival order so post-turn heal can no longer drop prompt pairing or misorder frames.
65 changes: 34 additions & 31 deletions packages/kap-server/src/services/transcript/coreEventMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,12 +193,15 @@ export interface ToolFrameRecord {
export class AgentTranscriptProjector {
private currentTurn: TurnHeader | undefined;
private currentStep: StepHeader | undefined;
private pendingTaskNotifications: { text: string; taskId: string | undefined }[] = [];
private pendingSteers: {
input: readonly ContentPart[];
promptIds: readonly string[] | undefined;
origin: TranscriptUserOrigin;
}[] = [];
private pendingUserFrames: (
| { kind: 'notification'; text: string; taskId: string | undefined }
| {
kind: 'steer';
input: readonly ContentPart[];
promptIds: readonly string[] | undefined;
origin: TranscriptUserOrigin;
}
)[] = [];
private unpairedSteerPromptIds: string[][] = [];
private readonly stepOrdinals = new Map<string, number>();
private frameOrdinal = 0;
Expand Down Expand Up @@ -403,8 +406,7 @@ export class AgentTranscriptProjector {
startedAt: nowIso(),
};
this.currentStep = undefined;
this.pendingTaskNotifications = [];
this.pendingSteers = [];
this.pendingUserFrames = [];
this.openText = undefined;
this.openThinking = undefined;
ops.push({ op: 'turn.upsert', turn: this.currentTurn });
Expand All @@ -428,7 +430,8 @@ export class AgentTranscriptProjector {
this.currentStep = step;
ops.push({ op: 'step.upsert', turnId: step.turnId, step });
}
if (this.currentStep === undefined && this.pendingSteers.length > 0) {
const pendingSteers = this.pendingUserFrames.filter((pending) => pending.kind === 'steer');
if (this.currentStep === undefined && pendingSteers.length > 0) {
const ordinal = (this.stepOrdinals.get(turnId) ?? this.lookups?.stepOrdinal?.(turnId) ?? 0) + 1;
const step: StepHeader = {
kind: 'step',
Expand All @@ -443,7 +446,7 @@ export class AgentTranscriptProjector {
ops.push({ op: 'step.upsert', turnId, step });
}
if (this.currentStep !== undefined) {
for (const pending of this.pendingSteers) {
for (const pending of pendingSteers) {
this.steerUserFrame(
ops,
turnId,
Expand All @@ -454,7 +457,7 @@ export class AgentTranscriptProjector {
);
}
}
this.pendingSteers = [];
this.pendingUserFrames = [];
const prev =
this.currentTurn?.turnId === turnId ? this.currentTurn : this.lookups?.turn?.(turnId);
const state = mapTurnEndState(event.reason);
Expand All @@ -476,7 +479,6 @@ export class AgentTranscriptProjector {
ops.push({ op: 'turn.upsert', turn: this.currentTurn });
ops.push({ op: 'meta.merge', meta: { activity: 'idle' } });
this.currentStep = undefined;
this.pendingTaskNotifications = [];
if (event.reason === 'cancelled' && event.interruptReason === 'user_cancelled') {
ops.push(
this.markerOp('interruption', { turnId: event.turnId, reason: event.interruptReason }),
Expand Down Expand Up @@ -523,25 +525,25 @@ export class AgentTranscriptProjector {
this.openText = undefined;
this.openThinking = undefined;
const ops: TranscriptOperation[] = [{ op: 'step.upsert', turnId, step: this.currentStep }];
for (const pending of this.pendingTaskNotifications) {
ops.push({
op: 'frame.upsert',
turnId,
stepId,
frame: {
kind: 'text',
frameId: `${stepId}.f${++this.frameOrdinal}`,
role: 'user',
text: pending.text,
taskId: pending.taskId,
},
});
}
this.pendingTaskNotifications = [];
for (const pending of this.pendingSteers) {
for (const pending of this.pendingUserFrames) {
if (pending.kind === 'notification') {
ops.push({
op: 'frame.upsert',
turnId,
stepId,
frame: {
kind: 'text',
frameId: `${stepId}.f${++this.frameOrdinal}`,
role: 'user',
text: pending.text,
taskId: pending.taskId,
},
});
continue;
}
this.steerUserFrame(ops, turnId, stepId, pending.input, pending.promptIds, pending.origin);
}
this.pendingSteers = [];
this.pendingUserFrames = [];
return ops;
}

Expand Down Expand Up @@ -892,7 +894,7 @@ export class AgentTranscriptProjector {
return [{ op: 'frame.upsert', turnId: turn.turnId, stepId: step.stepId, frame }];
}
if (turn.origin?.kind === 'task' && (turn.origin.taskId === undefined || turn.origin.taskId === event.sourceId)) return [];
this.pendingTaskNotifications.push({ text, taskId: event.sourceId });
this.pendingUserFrames.push({ kind: 'notification', text, taskId: event.sourceId });
return [];
}

Expand Down Expand Up @@ -1458,7 +1460,8 @@ export class AgentTranscriptProjector {
);
return ops;
}
this.pendingSteers.push({
this.pendingUserFrames.push({
kind: 'steer',
input,
promptIds: this.unpairedSteerPromptIds.shift(),
origin: frameOrigin,
Expand Down
63 changes: 59 additions & 4 deletions packages/kap-server/src/services/transcript/transcriptService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,28 +469,76 @@ export class TranscriptService {
}
const messages = [...reduceContextTranscript(records).entries];
const taskOriginTurnTaskIds = new Set<string>();
const steeredContents = new Map<string, Map<string, number>>();
const anchorStack: { taskIdsSnapshot: Set<string> }[] = [];
let steeredContents = new Map<string, Map<string, number>>();
const steeredPromptIds: (readonly string[] | undefined)[] = [];
const pendingSteerPromptIds: (readonly string[])[] = [];
const anchorStack: {
taskIdsSnapshot: Set<string>;
steeredSnapshot: Map<string, Map<string, number>>;
pendingSteerCount: number;
steeredIdsCount: number;
}[] = [];
let anchorFloor = 0;
let floorSteerState:
| {
steeredSnapshot: Map<string, Map<string, number>>;
pendingSteerCount: number;
steeredIdsCount: number;
}
| undefined;
let sawTurnPrompt = false;
for (const record of records) {
if (record.type === 'context.undo') {
const count = typeof record['count'] === 'number' ? (record['count'] as number) : 0;
let poppedAny = false;
for (let i = 0; i < count && anchorStack.length > anchorFloor; i++) {
const popped = anchorStack.pop()!;
poppedAny = true;
taskOriginTurnTaskIds.clear();
for (const id of popped.taskIdsSnapshot) taskOriginTurnTaskIds.add(id);
}
if (poppedAny) {
const top =
anchorStack.length > anchorFloor ? anchorStack[anchorStack.length - 1] : undefined;
pendingSteerPromptIds.length = top?.pendingSteerCount ?? floorSteerState?.pendingSteerCount ?? 0;
steeredPromptIds.length = top?.steeredIdsCount ?? floorSteerState?.steeredIdsCount ?? 0;
steeredContents = new Map(
[...(top?.steeredSnapshot ?? floorSteerState?.steeredSnapshot ?? [])].map(
([key, byKind]) => [key, new Map(byKind)],
),
);
}
continue;
}
if (record.type === 'context.clear') {
anchorFloor = anchorStack.length;
floorSteerState = {
steeredSnapshot: new Map(
[...steeredContents].map(([key, byKind]) => [key, new Map(byKind)]),
),
pendingSteerCount: pendingSteerPromptIds.length,
steeredIdsCount: steeredPromptIds.length,
};
continue;
}
if (record.type === 'context.append_message') {
const message = (record as { message?: ContextMessage }).message;
if (message !== undefined && isUndoAnchor(message)) {
anchorStack.push({ taskIdsSnapshot: new Set(taskOriginTurnTaskIds) });
anchorStack.push({
taskIdsSnapshot: new Set(taskOriginTurnTaskIds),
steeredSnapshot: new Map(
[...steeredContents].map(([key, byKind]) => [key, new Map(byKind)]),
),
pendingSteerCount: pendingSteerPromptIds.length,
steeredIdsCount: steeredPromptIds.length,
});
}
continue;
}
if (record.type === 'prompt.steered') {
const promptIds = record['promptIds'];
if (Array.isArray(promptIds) && promptIds.every((id) => typeof id === 'string')) {
pendingSteerPromptIds.push(promptIds as readonly string[]);
}
continue;
}
Expand All @@ -503,6 +551,11 @@ export class TranscriptService {
const byKind = steeredContents.get(key) ?? new Map<string, number>();
byKind.set(kind, (byKind.get(kind) ?? 0) + 1);
steeredContents.set(key, byKind);
steeredPromptIds.push(
kind === 'user' && pendingSteerPromptIds.length > 0
? pendingSteerPromptIds.shift()
: undefined,
);
Comment thread
liruifengv marked this conversation as resolved.
}
continue;
}
Expand All @@ -519,7 +572,9 @@ export class TranscriptService {
}
const base = groupMessagesIntoSnapshot(
messages,
sawTurnPrompt || steeredContents.size > 0 ? { taskOriginTurnTaskIds, steeredContents } : undefined,
sawTurnPrompt || steeredContents.size > 0
? { taskOriginTurnTaskIds, steeredContents, steeredPromptIds }
: undefined,
);
const folded = foldWireRecordFacts(projectQuestionInteractionRecords(records, sessionId), base, {
resolvePlanRevisionKey: (key) =>
Expand Down
Loading
Loading