dsh-code 1.0.6 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +123 -26
- package/README.md +124 -27
- package/bin/deepseek.mjs +283 -35
- package/cordis.patch.yml +97 -0
- package/lib/index.mjs +5008 -881
- package/lib/session-query.mjs +150 -0
- package/lib/startup.mjs +4 -4
- package/lib/{theme-DCT8Y2xf.mjs → theme-7u5Qo3dF.mjs} +657 -20
- package/lib/types/app.d.ts +106 -62
- package/lib/types/authorization-panel.d.ts +3 -3
- package/lib/types/git-workflow.d.ts +91 -2
- package/lib/types/i18n.d.ts +39 -0
- package/lib/types/index.d.ts +100 -1
- package/lib/types/input-split.d.ts +1 -1
- package/lib/types/kernel-panels.d.ts +107 -29
- package/lib/types/language-panel.d.ts +12 -0
- package/lib/types/locales/en.d.ts +450 -0
- package/lib/types/locales/zh.d.ts +9 -0
- package/lib/types/mentions.d.ts +7 -3
- package/lib/types/models.d.ts +14 -0
- package/lib/types/panel-accent.d.ts +28 -0
- package/lib/types/rainbow.d.ts +69 -0
- package/lib/types/render/animations.d.ts +42 -0
- package/lib/types/render/editor.d.ts +4 -3
- package/lib/types/render/ime-cursor.d.ts +60 -0
- package/lib/types/render/inspector.d.ts +26 -0
- package/lib/types/render/lines.d.ts +21 -1
- package/lib/types/render/markdown.d.ts +1 -1
- package/lib/types/render/projection.d.ts +130 -4
- package/lib/types/render/status.d.ts +9 -9
- package/lib/types/render/text.d.ts +6 -0
- package/lib/types/render/usage.d.ts +113 -0
- package/lib/types/session-directory.d.ts +17 -0
- package/lib/types/session-query.d.ts +92 -0
- package/lib/types/startup.d.ts +1 -1
- package/lib/types/terminal-title.d.ts +66 -0
- package/lib/types/theme-panel.d.ts +2 -2
- package/lib/types/theme.d.ts +271 -52
- package/lib/types/update-panel.d.ts +49 -0
- package/lib/types/update.d.ts +75 -0
- package/lib/types/version.d.ts +4 -3
- package/package.json +246 -90
- package/src/app.ts +1369 -509
- package/src/approval.ts +166 -166
- package/src/authorization-panel.ts +19 -16
- package/src/editor-keys.ts +371 -371
- package/src/git-workflow.ts +229 -3
- package/src/i18n.ts +68 -0
- package/src/index.ts +534 -80
- package/src/input-split.ts +3 -3
- package/src/internals.ts +5 -0
- package/src/kernel-panels.ts +554 -86
- package/src/keyboard.ts +5 -4
- package/src/language-panel.ts +53 -0
- package/src/locales/en.ts +489 -0
- package/src/locales/zh.ts +488 -0
- package/src/mentions.ts +8 -4
- package/src/models.ts +264 -212
- package/src/panel-accent.ts +41 -0
- package/src/presets.ts +1 -1
- package/src/provider-settings.ts +1 -1
- package/src/rainbow.ts +208 -0
- package/src/render/animations.ts +104 -6
- package/src/render/editor.ts +25 -24
- package/src/render/export.ts +116 -95
- package/src/render/ime-cursor.ts +147 -0
- package/src/render/inspector.ts +42 -0
- package/src/render/lines.ts +628 -415
- package/src/render/markdown.ts +15 -3
- package/src/render/projection.ts +572 -21
- package/src/render/status.ts +59 -39
- package/src/render/text.ts +14 -0
- package/src/render/tool-preview.ts +77 -77
- package/src/render/usage.ts +430 -0
- package/src/render/width.ts +2 -2
- package/src/session-directory.ts +8 -6
- package/src/session-query.ts +239 -0
- package/src/startup.ts +3 -3
- package/src/subagents.ts +229 -229
- package/src/terminal-title.ts +190 -0
- package/src/theme-panel.ts +17 -21
- package/src/theme.ts +281 -33
- package/src/update-panel.ts +256 -0
- package/src/update.ts +126 -0
- package/src/version.ts +58 -20
- package/src/whale-glyph.ts +23 -23
package/src/render/projection.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type {} from '@deepseek-ai/dsh-goal'
|
|
|
23
23
|
import type {} from '@deepseek-ai/dsh-llm-retry'
|
|
24
24
|
import type {} from '@deepseek-ai/dsh-plan-mode'
|
|
25
25
|
import type {} from '@deepseek-ai/dsh-permission-presets'
|
|
26
|
+
import type {} from '@deepseek-ai/dsh-schedule'
|
|
26
27
|
import type {} from '@deepseek-ai/dsh-sandbox-policy'
|
|
27
28
|
import type {} from '@deepseek-ai/dsh-session-title'
|
|
28
29
|
// The subagent package's durable catalog event joins the union the same way
|
|
@@ -62,6 +63,12 @@ export interface UserEntry {
|
|
|
62
63
|
images?: readonly ImageBlock['attachment'][]
|
|
63
64
|
/** Durable file references carried by this prompt (0.1.5 file blocks). */
|
|
64
65
|
files?: readonly FileAttachmentRef[]
|
|
66
|
+
/**
|
|
67
|
+
* How the prompt reached the agent, when it did not arrive as an ordinary
|
|
68
|
+
* submission: `queued` waited for this turn, `steered` joined it mid-flight.
|
|
69
|
+
* Absent for a prompt typed straight into an idle composer.
|
|
70
|
+
*/
|
|
71
|
+
delivery?: 'queued' | 'steered'
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
/** One user message waiting in the agent inbox (the web's queued-message row). */
|
|
@@ -121,6 +128,169 @@ export interface ToolEntry {
|
|
|
121
128
|
* result lands and only when something renderable exists.
|
|
122
129
|
*/
|
|
123
130
|
detail: ToolDetail | undefined
|
|
131
|
+
/**
|
|
132
|
+
* Nested PTC sub-dispatches (`run_code`) in start order, bounded to the
|
|
133
|
+
* newest {@link MAX_TOOL_SUB_DISPATCHES} rows.
|
|
134
|
+
*/
|
|
135
|
+
subs: readonly ToolSubDispatch[]
|
|
136
|
+
/** Sub-dispatches evicted from the bounded window. */
|
|
137
|
+
subsDropped: number
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* One nested PTC sub-dispatch under a `run_code` parent call: the durable
|
|
142
|
+
* `tool/ptc-dispatch-start`/`tool/ptc-dispatch` pair folded to one bounded
|
|
143
|
+
* row (upstream contract: pair by `subCallId`, every start settles, and the
|
|
144
|
+
* settle carries `tool/result`'s own vocabulary).
|
|
145
|
+
*/
|
|
146
|
+
export interface ToolSubDispatch {
|
|
147
|
+
/** Opaque sub-call id pairing the start with its settle. */
|
|
148
|
+
subCallId: string
|
|
149
|
+
/** Sub-call tool name. */
|
|
150
|
+
name: string
|
|
151
|
+
/** Bounded arguments preview. */
|
|
152
|
+
preview: string
|
|
153
|
+
/** Lifecycle; `running` until the paired settle lands. */
|
|
154
|
+
state: 'running' | 'done' | 'error'
|
|
155
|
+
/** Bounded first text block of the settle content, '' until it lands. */
|
|
156
|
+
summary: string
|
|
157
|
+
/** Wall-clock duration (settle − start), 0 while running. */
|
|
158
|
+
durationMs: number
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Bounded sub-dispatch window per tool card (display budget only). */
|
|
162
|
+
export const MAX_TOOL_SUB_DISPATCHES = 12
|
|
163
|
+
|
|
164
|
+
/** Bounded member window per workflow run card (display budget only). */
|
|
165
|
+
export const MAX_WORKFLOW_MEMBERS = 12
|
|
166
|
+
|
|
167
|
+
/** One workflow member (an `agent()` call inside a `workflow` script). */
|
|
168
|
+
export interface WorkflowMember {
|
|
169
|
+
/** Member sequence within the run (the agent-start/agent-end pairing key). */
|
|
170
|
+
seq: number
|
|
171
|
+
/** Display label. */
|
|
172
|
+
label: string
|
|
173
|
+
/** Declared phase title, '' when none. */
|
|
174
|
+
phase: string
|
|
175
|
+
/** Child session id (cross-links the subagent feed's rows). */
|
|
176
|
+
childId: string
|
|
177
|
+
/** Settlement; `running` until the paired `tool-workflow/agent-end`. */
|
|
178
|
+
outcome: 'running' | 'completed' | 'failed' | 'cancelled'
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* One durable workflow run (the `tool-workflow/*` record a `workflow` or
|
|
183
|
+
* `ralph` tool appends to the parent session): run identity plus its
|
|
184
|
+
* bounded member list, live until `tool-workflow/run-end` settles.
|
|
185
|
+
*/
|
|
186
|
+
export interface WorkflowEntry {
|
|
187
|
+
kind: 'workflow'
|
|
188
|
+
/** Stable run identity shared by every event of the run. */
|
|
189
|
+
runId: string
|
|
190
|
+
/** Display name of the run. */
|
|
191
|
+
name: string
|
|
192
|
+
/** Members in sequence order, bounded to the newest window. */
|
|
193
|
+
members: readonly WorkflowMember[]
|
|
194
|
+
/** Members evicted from the bounded window. */
|
|
195
|
+
membersDropped: number
|
|
196
|
+
/** Run settlement; `running` until `tool-workflow/run-end`. */
|
|
197
|
+
state: 'running' | 'completed' | 'cancelled' | 'error'
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Payload of `tool-workflow/run-start` (dsh-tool-workflow's map merge). */
|
|
201
|
+
interface WorkflowRunStartData {
|
|
202
|
+
runId: string
|
|
203
|
+
name: string
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Payload of `tool-workflow/agent-start`. */
|
|
207
|
+
interface WorkflowAgentStartData {
|
|
208
|
+
runId: string
|
|
209
|
+
seq: number
|
|
210
|
+
label: string
|
|
211
|
+
phase?: string
|
|
212
|
+
childId: string
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Payload of `tool-workflow/agent-end`. */
|
|
216
|
+
interface WorkflowAgentEndData {
|
|
217
|
+
runId: string
|
|
218
|
+
seq: number
|
|
219
|
+
outcome: 'completed' | 'failed' | 'cancelled'
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Payload of `tool-workflow/run-end`. */
|
|
223
|
+
interface WorkflowRunEndData {
|
|
224
|
+
runId: string
|
|
225
|
+
stopReason: 'completed' | 'cancelled' | 'error'
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Append one member to a run entry, evicting past the bounded window. */
|
|
229
|
+
function pushWorkflowMember(entry: WorkflowEntry, member: WorkflowMember): WorkflowEntry {
|
|
230
|
+
if (entry.members.length >= MAX_WORKFLOW_MEMBERS) {
|
|
231
|
+
return { ...entry, members: [...entry.members.slice(1), member], membersDropped: entry.membersDropped + 1 }
|
|
232
|
+
}
|
|
233
|
+
return { ...entry, members: [...entry.members, member] }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Settle one member by its sequence number (pure; unmatched stays a no-op). */
|
|
237
|
+
function settleWorkflowMember(entry: WorkflowEntry, data: WorkflowAgentEndData): WorkflowEntry {
|
|
238
|
+
let paired = false
|
|
239
|
+
const members = entry.members.map(member => {
|
|
240
|
+
if (member.seq !== data.seq) return member
|
|
241
|
+
paired = true
|
|
242
|
+
return { ...member, outcome: data.outcome }
|
|
243
|
+
})
|
|
244
|
+
return paired ? { ...entry, members } : entry
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Payload of `tool/ptc-dispatch-start` (`@deepseek-ai/dsh-tools`'s
|
|
249
|
+
* SessionEventMap merge — the bundle does not depend on that package, so
|
|
250
|
+
* the fold guards the discriminator by string instead of by type).
|
|
251
|
+
*/
|
|
252
|
+
interface PtcDispatchStartData {
|
|
253
|
+
rootCallId: string
|
|
254
|
+
subCallId: string
|
|
255
|
+
name: string
|
|
256
|
+
arguments: unknown
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Payload of `tool/ptc-dispatch` (a start plus its settled outcome). */
|
|
260
|
+
interface PtcDispatchData extends PtcDispatchStartData {
|
|
261
|
+
isError: boolean
|
|
262
|
+
content: readonly ContentBlock[]
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** JSON-stringify one sub-dispatch argument value for the preview helper. */
|
|
266
|
+
function subDispatchArguments(arguments_: unknown): string {
|
|
267
|
+
if (typeof arguments_ === 'string') return arguments_
|
|
268
|
+
try {
|
|
269
|
+
return JSON.stringify(arguments_) ?? ''
|
|
270
|
+
} catch {
|
|
271
|
+
return ''
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Append one running sub-dispatch under its parent, evicting past the cap. */
|
|
276
|
+
function pushSubDispatch(entry: ToolEntry, sub: ToolSubDispatch): ToolEntry {
|
|
277
|
+
if (entry.subs.length >= MAX_TOOL_SUB_DISPATCHES) {
|
|
278
|
+
return { ...entry, subs: [...entry.subs.slice(1), sub], subsDropped: entry.subsDropped + 1 }
|
|
279
|
+
}
|
|
280
|
+
return { ...entry, subs: [...entry.subs, sub] }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Fold one settled sub-dispatch into its matching row (pure). */
|
|
284
|
+
function settleSubDispatch(entry: ToolEntry, data: PtcDispatchData, summary: string, durationMs: number): ToolEntry {
|
|
285
|
+
let paired = false
|
|
286
|
+
const subs = entry.subs.map(sub => {
|
|
287
|
+
if (sub.subCallId !== data.subCallId) return sub
|
|
288
|
+
paired = true
|
|
289
|
+
return { ...sub, state: data.isError === true ? 'error' as const : 'done' as const, summary, durationMs }
|
|
290
|
+
})
|
|
291
|
+
// A settle without a live row (the sub left the bounded window, or the
|
|
292
|
+
// parent entry was never seen) stays a provable no-op.
|
|
293
|
+
return paired ? { ...entry, subs } : entry
|
|
124
294
|
}
|
|
125
295
|
|
|
126
296
|
/** One slash-command execution dispatched through `ctx.commands`. */
|
|
@@ -193,7 +363,7 @@ export interface FilesEntry {
|
|
|
193
363
|
}
|
|
194
364
|
|
|
195
365
|
/** Ordered transcript items the renderer draws. */
|
|
196
|
-
export type TranscriptEntry = UserEntry | PendingEntry | AssistantEntry | ToolEntry | CommandEntry | ErrorEntry | TurnMarkerEntry | CompactionEntry | RetryEntry | FilesEntry
|
|
366
|
+
export type TranscriptEntry = UserEntry | PendingEntry | AssistantEntry | ToolEntry | CommandEntry | ErrorEntry | TurnMarkerEntry | CompactionEntry | RetryEntry | FilesEntry | WorkflowEntry
|
|
197
367
|
|
|
198
368
|
/** The live goal the status line badges, folded from `goal/change`. */
|
|
199
369
|
export interface GoalFold {
|
|
@@ -208,14 +378,21 @@ export interface GoalFold {
|
|
|
208
378
|
blocked: string
|
|
209
379
|
}
|
|
210
380
|
|
|
211
|
-
/**
|
|
381
|
+
/**
|
|
382
|
+
* Cumulative token accounting folded from `assistant/message` usage reports.
|
|
383
|
+
* The buckets are disjoint and mirror the provider's report, so the prompt
|
|
384
|
+
* side is never double counted: reasoning tokens are already inside
|
|
385
|
+
* `outputTokens`, and cache reads are never folded into the uncached input.
|
|
386
|
+
*/
|
|
212
387
|
export interface UsageTotals {
|
|
213
|
-
/** Prompt-side
|
|
214
|
-
|
|
388
|
+
/** Prompt-side tokens billed outside the cache. */
|
|
389
|
+
uncachedInputTokens: number
|
|
215
390
|
/** Completion-side tokens over the whole log. */
|
|
216
391
|
outputTokens: number
|
|
217
392
|
/** Cache-read tokens over the whole log (0 when the adapter reports none). */
|
|
218
393
|
cacheReadTokens: number
|
|
394
|
+
/** Cache-write tokens over the whole log (0 when the adapter reports none). */
|
|
395
|
+
cacheWriteTokens: number
|
|
219
396
|
}
|
|
220
397
|
|
|
221
398
|
/**
|
|
@@ -273,6 +450,86 @@ export interface TranscriptStats {
|
|
|
273
450
|
reasoningEffort: string
|
|
274
451
|
}
|
|
275
452
|
|
|
453
|
+
/** One active reminder folded from durable `schedule/change` events. */
|
|
454
|
+
export interface ScheduleRow {
|
|
455
|
+
readonly id: string
|
|
456
|
+
readonly kind: 'after' | 'at' | 'every'
|
|
457
|
+
readonly prompt: string
|
|
458
|
+
/** Next due time (epoch ms); the /schedule panel derives overdue/relative labels. */
|
|
459
|
+
readonly targetAt: number
|
|
460
|
+
/** Recurrence seconds for 'every' rows, undefined otherwise. */
|
|
461
|
+
readonly everySeconds?: number
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* The durable `schedule/change` payload shape this fold consumes. Upstream
|
|
466
|
+
* strict-decodes the whole transition stream before appending, so unknown
|
|
467
|
+
* ids here are corrupt-input edges that degrade to a no-op.
|
|
468
|
+
*/
|
|
469
|
+
export interface ScheduleChangeLike {
|
|
470
|
+
readonly operation: 'create' | 'delete' | 'dispatch'
|
|
471
|
+
readonly schedule?: {
|
|
472
|
+
readonly id: string
|
|
473
|
+
readonly kind: 'after' | 'at' | 'every'
|
|
474
|
+
readonly prompt: string
|
|
475
|
+
readonly afterSeconds?: number
|
|
476
|
+
readonly everySeconds?: number
|
|
477
|
+
readonly scheduledAt: string
|
|
478
|
+
}
|
|
479
|
+
readonly id?: string
|
|
480
|
+
readonly acceptedAt?: string
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/*
|
|
484
|
+
* Upstream record semantics (dsh-schedule types): `scheduledAt` is ALREADY
|
|
485
|
+
* the due instant — AfterScheduleRecord carries the RFC 3339 UTC target
|
|
486
|
+
* (delay included), EveryScheduleRecord carries the earliest anchor-aligned
|
|
487
|
+
* occurrence not yet dispatched. No kind ever adds its own interval on top.
|
|
488
|
+
*/
|
|
489
|
+
|
|
490
|
+
/** Fold one `schedule/change` into the active-reminder list (create/delete/dispatch). */
|
|
491
|
+
export function applyScheduleChange(rows: readonly ScheduleRow[], data: ScheduleChangeLike): readonly ScheduleRow[] {
|
|
492
|
+
if (data.operation === 'create' && data.schedule !== undefined) {
|
|
493
|
+
const schedule = data.schedule
|
|
494
|
+
const row: ScheduleRow = {
|
|
495
|
+
id: schedule.id,
|
|
496
|
+
kind: schedule.kind,
|
|
497
|
+
prompt: schedule.prompt,
|
|
498
|
+
targetAt: Date.parse(schedule.scheduledAt),
|
|
499
|
+
...(schedule.kind === 'every' ? { everySeconds: schedule.everySeconds ?? 0 } : {}),
|
|
500
|
+
}
|
|
501
|
+
return [...rows.filter(existing => existing.id !== schedule.id), row]
|
|
502
|
+
}
|
|
503
|
+
if (data.operation === 'delete' && data.id !== undefined) {
|
|
504
|
+
return rows.filter(existing => existing.id !== data.id)
|
|
505
|
+
}
|
|
506
|
+
if (data.operation === 'dispatch' && data.id !== undefined) {
|
|
507
|
+
// A dispatched one-shot reminder is finished. An 'every' reminder
|
|
508
|
+
// advances PAST every missed occurrence in one step: the next target is
|
|
509
|
+
// the first anchor-aligned instant strictly after acceptedAt, stepping
|
|
510
|
+
// from the previous aligned target (upstream advances the same way).
|
|
511
|
+
if (data.acceptedAt === undefined) return rows.filter(existing => existing.id !== data.id)
|
|
512
|
+
const accepted = Date.parse(data.acceptedAt)
|
|
513
|
+
return rows.map(existing => existing.id === data.id
|
|
514
|
+
? { ...existing, targetAt: nextEveryTarget(existing.targetAt, accepted, existing.everySeconds ?? 0) }
|
|
515
|
+
: existing)
|
|
516
|
+
}
|
|
517
|
+
return rows
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** First anchor-aligned target after `acceptedAt`, stepping from the previous aligned target. */
|
|
521
|
+
export function nextEveryTarget(previousTarget: number, acceptedAt: number, everySeconds: number): number {
|
|
522
|
+
const interval = Math.max(1, everySeconds) * 1000
|
|
523
|
+
if (acceptedAt <= previousTarget) return previousTarget + interval
|
|
524
|
+
const missed = Math.ceil((acceptedAt - previousTarget + 1) / interval)
|
|
525
|
+
return previousTarget + missed * interval
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Plugin snapshot sources folded into token stats but never rendered as rows. */
|
|
529
|
+
const HIDDEN_SNAPSHOT_PLUGINS = new Set(['time-context', 'tmux-context'])
|
|
530
|
+
/** Plugin prompt sources rendered as full user rows (they ARE the conversation). */
|
|
531
|
+
const REMINDER_PLUGINS = new Set(['schedule'])
|
|
532
|
+
|
|
276
533
|
/** The complete TUI transcript view for one session. */
|
|
277
534
|
export interface TranscriptView {
|
|
278
535
|
/** Settled entries in log order. */
|
|
@@ -319,12 +576,22 @@ export interface TranscriptView {
|
|
|
319
576
|
sandbox: string
|
|
320
577
|
/** Current long-running goal folded from the last `goal/change`, undefined when cleared. */
|
|
321
578
|
goal: GoalFold | undefined
|
|
579
|
+
/** Active reminders folded from `schedule/change` events, oldest target first at render. */
|
|
580
|
+
schedules: readonly ScheduleRow[]
|
|
322
581
|
/**
|
|
323
582
|
* Ordered live message ids per inbox target, mirrored from
|
|
324
583
|
* `agent/inbox/spliced` exactly like the upstream Inbox projection — the
|
|
325
584
|
* coordinates later removals resolve against.
|
|
326
585
|
*/
|
|
327
586
|
pending: { 'next-turn': readonly string[]; 'next-step': readonly string[] }
|
|
587
|
+
/**
|
|
588
|
+
* In-flight inbox messages by the list they were inserted into, kept until
|
|
589
|
+
* the durable user message that claims them lands. The claim itself is a
|
|
590
|
+
* plain splice that empties the pending row first, so this map — not
|
|
591
|
+
* {@link pending} — is what lets a settled prompt say it was queued or
|
|
592
|
+
* steered rather than typed into an idle composer.
|
|
593
|
+
*/
|
|
594
|
+
claimOrigin: ReadonlyMap<string, 'next-turn' | 'next-step'>
|
|
328
595
|
/**
|
|
329
596
|
* Fold-internal timing anchors, never rendered: open step and tool-call
|
|
330
597
|
* start timestamps the next `assistant/message` / `tool/result` resolves
|
|
@@ -336,6 +603,8 @@ export interface TranscriptView {
|
|
|
336
603
|
readonly anchors: {
|
|
337
604
|
stepStart: Map<string, number>
|
|
338
605
|
toolStart: Map<string, number>
|
|
606
|
+
/** Open PTC sub-dispatch starts by `subCallId` (duration anchors). */
|
|
607
|
+
subStart: Map<string, number>
|
|
339
608
|
firstChunkAt: Map<string, number>
|
|
340
609
|
compactionTokens: Map<string, number>
|
|
341
610
|
lastPruneTokens: number
|
|
@@ -399,6 +668,7 @@ function cloneViewAnchors(anchors: TranscriptView['anchors']): TranscriptView['a
|
|
|
399
668
|
return {
|
|
400
669
|
stepStart: new Map(anchors.stepStart),
|
|
401
670
|
toolStart: new Map(anchors.toolStart),
|
|
671
|
+
subStart: new Map(anchors.subStart),
|
|
402
672
|
firstChunkAt: new Map(anchors.firstChunkAt),
|
|
403
673
|
compactionTokens: new Map(anchors.compactionTokens),
|
|
404
674
|
lastPruneTokens: anchors.lastPruneTokens,
|
|
@@ -491,9 +761,11 @@ export function createTranscriptView(): TranscriptView {
|
|
|
491
761
|
systemPrompt: '',
|
|
492
762
|
sandbox: '',
|
|
493
763
|
goal: undefined,
|
|
764
|
+
schedules: [],
|
|
494
765
|
pending: { 'next-turn': [], 'next-step': [] },
|
|
495
|
-
|
|
496
|
-
|
|
766
|
+
claimOrigin: new Map(),
|
|
767
|
+
stats: { turns: 0, steps: 0, llmMs: 0, toolMs: 0, usage: { uncachedInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }, lastPromptTokens: 0, contextWindow: 0, contextSegments: { system: 0, prompt: 0, assistant: 0, thinking: 0, tools: 0 }, ttftMs: 0, ttftSteps: 0, decodeMs: 0, decodeTokens: 0, reasoningEffort: '' },
|
|
768
|
+
anchors: { stepStart: new Map(), toolStart: new Map(), subStart: new Map(), firstChunkAt: new Map(), compactionTokens: new Map(), lastPruneTokens: 0, turnFiles: new Map(), turnSteps: new Map(), turnTools: new Map(), systemNodes: new Map() },
|
|
497
769
|
}
|
|
498
770
|
}
|
|
499
771
|
|
|
@@ -538,6 +810,18 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
|
|
|
538
810
|
pending = { ...pending, [target]: pending[target].filter((_, i) => i !== index) }
|
|
539
811
|
entries = entries.filter(entry => !(entry.kind === 'pending' && entry.messageId === message.id))
|
|
540
812
|
}
|
|
813
|
+
// How this message reached the agent. The claim that precedes this event
|
|
814
|
+
// is a plain splice (no `canceled` outcome), so it has already emptied
|
|
815
|
+
// the pending row above; `claimOrigin` is what survives to say whether
|
|
816
|
+
// the prompt was queued for this turn or steered into it.
|
|
817
|
+
const origin = view.claimOrigin.get(message.id)
|
|
818
|
+
let claimOrigin = view.claimOrigin
|
|
819
|
+
if (origin !== undefined) {
|
|
820
|
+
const next = new Map(claimOrigin)
|
|
821
|
+
next.delete(message.id)
|
|
822
|
+
claimOrigin = next
|
|
823
|
+
}
|
|
824
|
+
const delivery = origin === undefined ? {} : { delivery: origin === 'next-turn' ? 'queued' as const : 'steered' as const }
|
|
541
825
|
// Injected context (plugin/model-continuation sources) stays collapsed
|
|
542
826
|
// to a bounded notice row, exactly like collapsed transcript context
|
|
543
827
|
// elsewhere in the product; only direct human prompts render in full.
|
|
@@ -548,7 +832,41 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
|
|
|
548
832
|
return {
|
|
549
833
|
...view,
|
|
550
834
|
pending,
|
|
551
|
-
|
|
835
|
+
claimOrigin,
|
|
836
|
+
entries: [...entries, { kind: 'user', text, notice: false, ...delivery, ...(images.length === 0 ? {} : { images }), ...(files.length === 0 ? {} : { files }) }],
|
|
837
|
+
stats: {
|
|
838
|
+
...view.stats,
|
|
839
|
+
contextSegments: {
|
|
840
|
+
...view.stats.contextSegments,
|
|
841
|
+
prompt: view.stats.contextSegments.prompt + estimateTokens(text),
|
|
842
|
+
},
|
|
843
|
+
},
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
// Snapshot injections (time/tmux context) still spend model context but
|
|
847
|
+
// render nothing; the schedule reminder is a real prompt and renders in
|
|
848
|
+
// full — the model acts on it, so the transcript must show it.
|
|
849
|
+
if (message.source.kind === 'plugin' && HIDDEN_SNAPSHOT_PLUGINS.has(message.source.plugin)) {
|
|
850
|
+
return {
|
|
851
|
+
...view,
|
|
852
|
+
pending,
|
|
853
|
+
claimOrigin,
|
|
854
|
+
entries,
|
|
855
|
+
stats: {
|
|
856
|
+
...view.stats,
|
|
857
|
+
contextSegments: {
|
|
858
|
+
...view.stats.contextSegments,
|
|
859
|
+
system: view.stats.contextSegments.system + estimateTokens(text),
|
|
860
|
+
},
|
|
861
|
+
},
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
if (message.source.kind === 'plugin' && REMINDER_PLUGINS.has(message.source.plugin)) {
|
|
865
|
+
return {
|
|
866
|
+
...view,
|
|
867
|
+
pending,
|
|
868
|
+
claimOrigin,
|
|
869
|
+
entries: [...entries, { kind: 'user', text, notice: false, ...delivery, ...(images.length === 0 ? {} : { images }), ...(files.length === 0 ? {} : { files }) }],
|
|
552
870
|
stats: {
|
|
553
871
|
...view.stats,
|
|
554
872
|
contextSegments: {
|
|
@@ -560,11 +878,14 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
|
|
|
560
878
|
}
|
|
561
879
|
const notice = message.source.kind === 'plugin' && message.source.form === 'notice'
|
|
562
880
|
? message.source.summary
|
|
563
|
-
: message.source.kind
|
|
881
|
+
: message.source.kind === 'plugin'
|
|
882
|
+
? message.source.plugin
|
|
883
|
+
: message.source.kind
|
|
564
884
|
const summary = boundContextSummary(notice)
|
|
565
885
|
return {
|
|
566
886
|
...view,
|
|
567
887
|
pending,
|
|
888
|
+
claimOrigin,
|
|
568
889
|
entries: [...entries, { kind: 'user', text: summary, notice: true }],
|
|
569
890
|
stats: {
|
|
570
891
|
...view.stats,
|
|
@@ -579,9 +900,29 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
|
|
|
579
900
|
// The durable inbox mutation (web queue-mirror contract, event-sourced):
|
|
580
901
|
// removals drop the projected rows at their inbox coordinates, inserted
|
|
581
902
|
// messages gain a pending row at their log position.
|
|
582
|
-
const { target, start, removedCount = 0, inserted } = event.data
|
|
903
|
+
const { target, start, removedCount = 0, inserted, outcome } = event.data
|
|
583
904
|
const ids = view.pending[target]
|
|
584
905
|
const removed = ids.slice(start, start + removedCount)
|
|
906
|
+
// Which list each in-flight message came from, so the durable user
|
|
907
|
+
// message it later becomes can say how it was delivered. Only messages
|
|
908
|
+
// that arrived while a turn was already running count: `followup` is
|
|
909
|
+
// the ordinary submission path too, so an idle submission lands in
|
|
910
|
+
// next-turn exactly like a queued one and must stay unmarked. A claim
|
|
911
|
+
// (no `outcome`) keeps the entry because the user message is still
|
|
912
|
+
// coming; a real cancellation retires it.
|
|
913
|
+
const inFlight = view.busy
|
|
914
|
+
let claimOrigin: ReadonlyMap<string, 'next-turn' | 'next-step'> = view.claimOrigin
|
|
915
|
+
let mutableOrigins: Map<string, 'next-turn' | 'next-step'> | undefined
|
|
916
|
+
const remember = (id: string): void => {
|
|
917
|
+
mutableOrigins ??= new Map(claimOrigin)
|
|
918
|
+
mutableOrigins.set(id, target)
|
|
919
|
+
claimOrigin = mutableOrigins
|
|
920
|
+
}
|
|
921
|
+
if (outcome === 'canceled' && removed.length > 0) {
|
|
922
|
+
const next = new Map(claimOrigin)
|
|
923
|
+
for (const id of removed) next.delete(id)
|
|
924
|
+
claimOrigin = next
|
|
925
|
+
}
|
|
585
926
|
// In-place upstream semantics: the kernel's authoritative fold is
|
|
586
927
|
// `inbox.splice(start, removedCount, ...inserted)` — inserted ids land
|
|
587
928
|
// AT the splice position (prepend/replace shapes), never at the tail.
|
|
@@ -600,6 +941,7 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
|
|
|
600
941
|
!(entry.kind === 'pending' && entry.target === target && removedSet.has(entry.messageId)))
|
|
601
942
|
}
|
|
602
943
|
for (const message of inserted) {
|
|
944
|
+
if (inFlight) remember(message.id)
|
|
603
945
|
entries = [...entries, {
|
|
604
946
|
kind: 'pending',
|
|
605
947
|
messageId: message.id,
|
|
@@ -609,7 +951,7 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
|
|
|
609
951
|
...filesOf(message.content).length === 0 ? {} : { files: filesOf(message.content) },
|
|
610
952
|
}]
|
|
611
953
|
}
|
|
612
|
-
return { ...view, entries, pending: { ...view.pending, [target]: nextIds } }
|
|
954
|
+
return { ...view, entries, claimOrigin, pending: { ...view.pending, [target]: nextIds } }
|
|
613
955
|
}
|
|
614
956
|
case 'system/message': {
|
|
615
957
|
// Session-log v3 carries the system prompt as surface nodes (the
|
|
@@ -693,18 +1035,25 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
|
|
|
693
1035
|
const totals = stats.usage
|
|
694
1036
|
const text = textOf(event.data.message.content)
|
|
695
1037
|
const reasoning = reasoningOf(event.data.message.content)
|
|
1038
|
+
// A tool-only step settles with no text, no reasoning, and (unless the
|
|
1039
|
+
// attempt was interrupted) nothing to render: appending an entry would
|
|
1040
|
+
// create an invisible zero-line card that pads the inspector's ←→
|
|
1041
|
+
// walk and an empty block in /export. The event still updates timing
|
|
1042
|
+
// and usage above; only the transcript entry is skipped.
|
|
1043
|
+
const renderable = text !== '' || reasoning !== '' || event.data.interrupted === true
|
|
696
1044
|
return {
|
|
697
1045
|
...view,
|
|
698
1046
|
streaming: '',
|
|
699
1047
|
streamingReasoning: '',
|
|
700
|
-
entries: [...view.entries, { kind: 'assistant', text, reasoning, interrupted: event.data.interrupted === true ? true : undefined }],
|
|
1048
|
+
...(renderable ? { entries: [...view.entries, { kind: 'assistant', text, reasoning, interrupted: event.data.interrupted === true ? true : undefined }] } : {}),
|
|
701
1049
|
stats: {
|
|
702
1050
|
...stats,
|
|
703
1051
|
llmMs: stats.llmMs + (started === undefined ? 0 : Math.max(0, event.time - started)),
|
|
704
1052
|
usage: usage === undefined ? totals : {
|
|
705
|
-
|
|
1053
|
+
uncachedInputTokens: totals.uncachedInputTokens + usage.inputTokens,
|
|
706
1054
|
outputTokens: totals.outputTokens + usage.outputTokens,
|
|
707
1055
|
cacheReadTokens: totals.cacheReadTokens + (usage.cacheReadTokens ?? 0),
|
|
1056
|
+
cacheWriteTokens: totals.cacheWriteTokens + (usage.cacheWriteTokens ?? 0),
|
|
708
1057
|
},
|
|
709
1058
|
lastPromptTokens: usage === undefined ? stats.lastPromptTokens
|
|
710
1059
|
: usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0),
|
|
@@ -745,6 +1094,8 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
|
|
|
745
1094
|
state: 'running',
|
|
746
1095
|
summary: '',
|
|
747
1096
|
detail: undefined,
|
|
1097
|
+
subs: [],
|
|
1098
|
+
subsDropped: 0,
|
|
748
1099
|
}],
|
|
749
1100
|
stats: {
|
|
750
1101
|
...view.stats,
|
|
@@ -963,6 +1314,11 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
|
|
|
963
1314
|
entries: line === undefined ? view.entries : [...view.entries, { kind: 'turn-marker', text: line }],
|
|
964
1315
|
}
|
|
965
1316
|
}
|
|
1317
|
+
case 'schedule/change':
|
|
1318
|
+
// Non-conversational catalog state: the /schedule panel renders the
|
|
1319
|
+
// active list, the transcript shows only the reminder prompts
|
|
1320
|
+
// (handled at user/message above).
|
|
1321
|
+
return { ...view, schedules: applyScheduleChange(view.schedules, event.data) }
|
|
966
1322
|
case 'session/title':
|
|
967
1323
|
// Latest-wins title snapshot, log-only; the status line prefers it.
|
|
968
1324
|
return { ...view, title: event.data.title }
|
|
@@ -1043,8 +1399,86 @@ export function projectEvent(view: TranscriptView, event: SessionEvent): Transcr
|
|
|
1043
1399
|
})
|
|
1044
1400
|
return { ...view, entries }
|
|
1045
1401
|
}
|
|
1046
|
-
default:
|
|
1402
|
+
default: {
|
|
1403
|
+
// `tool/ptc-dispatch*` live in @deepseek-ai/dsh-tools' map merge,
|
|
1404
|
+
// which this bundle does not depend on; the string guards keep the
|
|
1405
|
+
// fold decoupled while following the upstream contract exactly (pair
|
|
1406
|
+
// by subCallId; every start settles; settle speaks tool/result's own
|
|
1407
|
+
// vocabulary). Log-only upstream: deriveMessages ignores them, so the
|
|
1408
|
+
// sub rows are pure display state on the parent run_code card.
|
|
1409
|
+
const type = event.type as string
|
|
1410
|
+
if (type === 'tool/ptc-dispatch-start') {
|
|
1411
|
+
const data = event.data as PtcDispatchStartData
|
|
1412
|
+
view.anchors.subStart.set(data.subCallId, event.time)
|
|
1413
|
+
const entries = view.entries.map(entry => entry.kind !== 'tool' || entry.callId !== data.rootCallId
|
|
1414
|
+
? entry
|
|
1415
|
+
: pushSubDispatch(entry, {
|
|
1416
|
+
subCallId: data.subCallId,
|
|
1417
|
+
name: data.name,
|
|
1418
|
+
preview: toolArgumentsPreview(subDispatchArguments(data.arguments), data.name),
|
|
1419
|
+
state: 'running',
|
|
1420
|
+
summary: '',
|
|
1421
|
+
durationMs: 0,
|
|
1422
|
+
}))
|
|
1423
|
+
return { ...view, entries }
|
|
1424
|
+
}
|
|
1425
|
+
if (type === 'tool/ptc-dispatch') {
|
|
1426
|
+
const data = event.data as PtcDispatchData
|
|
1427
|
+
const started = view.anchors.subStart.get(data.subCallId)
|
|
1428
|
+
view.anchors.subStart.delete(data.subCallId)
|
|
1429
|
+
const summary = boundContextSummary(textOf(data.content))
|
|
1430
|
+
const durationMs = started === undefined ? 0 : Math.max(0, event.time - started)
|
|
1431
|
+
const entries = view.entries.map(entry => entry.kind !== 'tool' || entry.callId !== data.rootCallId
|
|
1432
|
+
? entry
|
|
1433
|
+
: settleSubDispatch(entry, data, summary, durationMs))
|
|
1434
|
+
return { ...view, entries }
|
|
1435
|
+
}
|
|
1436
|
+
// The durable workflow record (`tool-workflow/*`, appended by the
|
|
1437
|
+
// workflow/ralph tools): one bounded card per run, members paired by
|
|
1438
|
+
// their sequence number, the run settling on run-end.
|
|
1439
|
+
if (type === 'tool-workflow/run-start') {
|
|
1440
|
+
const data = event.data as WorkflowRunStartData
|
|
1441
|
+
return {
|
|
1442
|
+
...view,
|
|
1443
|
+
entries: [...view.entries, {
|
|
1444
|
+
kind: 'workflow',
|
|
1445
|
+
runId: data.runId,
|
|
1446
|
+
name: data.name,
|
|
1447
|
+
members: [],
|
|
1448
|
+
membersDropped: 0,
|
|
1449
|
+
state: 'running',
|
|
1450
|
+
}],
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
if (type === 'tool-workflow/agent-start') {
|
|
1454
|
+
const data = event.data as WorkflowAgentStartData
|
|
1455
|
+
const entries = view.entries.map(entry => entry.kind !== 'workflow' || entry.runId !== data.runId
|
|
1456
|
+
? entry
|
|
1457
|
+
: pushWorkflowMember(entry, {
|
|
1458
|
+
seq: data.seq,
|
|
1459
|
+
label: data.label,
|
|
1460
|
+
phase: data.phase ?? '',
|
|
1461
|
+
childId: data.childId,
|
|
1462
|
+
outcome: 'running',
|
|
1463
|
+
}))
|
|
1464
|
+
return { ...view, entries }
|
|
1465
|
+
}
|
|
1466
|
+
if (type === 'tool-workflow/agent-end') {
|
|
1467
|
+
const data = event.data as WorkflowAgentEndData
|
|
1468
|
+
const entries = view.entries.map(entry => entry.kind !== 'workflow' || entry.runId !== data.runId
|
|
1469
|
+
? entry
|
|
1470
|
+
: settleWorkflowMember(entry, data))
|
|
1471
|
+
return { ...view, entries }
|
|
1472
|
+
}
|
|
1473
|
+
if (type === 'tool-workflow/run-end') {
|
|
1474
|
+
const data = event.data as WorkflowRunEndData
|
|
1475
|
+
const entries = view.entries.map(entry => entry.kind !== 'workflow' || entry.runId !== data.runId
|
|
1476
|
+
? entry
|
|
1477
|
+
: { ...entry, state: data.stopReason })
|
|
1478
|
+
return { ...view, entries }
|
|
1479
|
+
}
|
|
1047
1480
|
return view
|
|
1481
|
+
}
|
|
1048
1482
|
}
|
|
1049
1483
|
}
|
|
1050
1484
|
|
|
@@ -1079,11 +1513,15 @@ export interface ReplayAccumulator {
|
|
|
1079
1513
|
retryIndex: Map<string, number[]>
|
|
1080
1514
|
/** messageId → every index into `entries` holding a `pending` row with that id. */
|
|
1081
1515
|
pendingIndex: Map<string, number[]>
|
|
1516
|
+
/** runId → every index into `entries` holding a `workflow` row with that id. */
|
|
1517
|
+
workflowIndex: Map<string, number[]>
|
|
1082
1518
|
/** Tombstone count; zero means `entries` is already the final array. */
|
|
1083
1519
|
removedCount: number
|
|
1084
1520
|
/** Mutable inbox id lists, mirroring `view.pending` order per target. */
|
|
1085
1521
|
pendingTurn: string[]
|
|
1086
1522
|
pendingStep: string[]
|
|
1523
|
+
/** Mutable mirror of `view.claimOrigin` (see the reducer's field doc). */
|
|
1524
|
+
claimOrigin: Map<string, 'next-turn' | 'next-step'>
|
|
1087
1525
|
streaming: string
|
|
1088
1526
|
streamingReasoning: string
|
|
1089
1527
|
todos: readonly TodoItem[]
|
|
@@ -1098,9 +1536,12 @@ export interface ReplayAccumulator {
|
|
|
1098
1536
|
systemPrompt: string
|
|
1099
1537
|
sandbox: string
|
|
1100
1538
|
goal: GoalFold | undefined
|
|
1539
|
+
schedules: readonly ScheduleRow[]
|
|
1101
1540
|
stats: TranscriptStats
|
|
1102
1541
|
stepStart: Map<string, number>
|
|
1103
1542
|
toolStart: Map<string, number>
|
|
1543
|
+
/** Open PTC sub-dispatch starts by `subCallId` (duration anchors). */
|
|
1544
|
+
subStart: Map<string, number>
|
|
1104
1545
|
firstChunkAt: Map<string, number>
|
|
1105
1546
|
compactionTokens: Map<string, number>
|
|
1106
1547
|
lastPruneTokens: number
|
|
@@ -1121,9 +1562,11 @@ export function createReplayAccumulator(): ReplayAccumulator {
|
|
|
1121
1562
|
commandIndex: new Map(),
|
|
1122
1563
|
retryIndex: new Map(),
|
|
1123
1564
|
pendingIndex: new Map(),
|
|
1565
|
+
workflowIndex: new Map(),
|
|
1124
1566
|
removedCount: 0,
|
|
1125
1567
|
pendingTurn: [],
|
|
1126
1568
|
pendingStep: [],
|
|
1569
|
+
claimOrigin: new Map(),
|
|
1127
1570
|
streaming: '',
|
|
1128
1571
|
streamingReasoning: '',
|
|
1129
1572
|
todos: [],
|
|
@@ -1137,9 +1580,11 @@ export function createReplayAccumulator(): ReplayAccumulator {
|
|
|
1137
1580
|
systemPrompt: '',
|
|
1138
1581
|
sandbox: '',
|
|
1139
1582
|
goal: undefined,
|
|
1140
|
-
|
|
1583
|
+
schedules: [],
|
|
1584
|
+
stats: { turns: 0, steps: 0, llmMs: 0, toolMs: 0, usage: { uncachedInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }, lastPromptTokens: 0, contextWindow: 0, contextSegments: { system: 0, prompt: 0, assistant: 0, thinking: 0, tools: 0 }, ttftMs: 0, ttftSteps: 0, decodeMs: 0, decodeTokens: 0, reasoningEffort: '' },
|
|
1141
1585
|
stepStart: new Map(),
|
|
1142
1586
|
toolStart: new Map(),
|
|
1587
|
+
subStart: new Map(),
|
|
1143
1588
|
firstChunkAt: new Map(),
|
|
1144
1589
|
compactionTokens: new Map(),
|
|
1145
1590
|
lastPruneTokens: 0,
|
|
@@ -1270,8 +1715,14 @@ export function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent):
|
|
|
1270
1715
|
const text = textOf(message.content)
|
|
1271
1716
|
const images = imagesOf(message.content)
|
|
1272
1717
|
const files = filesOf(message.content)
|
|
1273
|
-
|
|
1274
|
-
|
|
1718
|
+
// Same delivery-origin rule as the reducer: the claim splice that
|
|
1719
|
+
// precedes this event never carried the origin, so `claimOrigin` is the
|
|
1720
|
+
// only surviving record of how the prompt was submitted.
|
|
1721
|
+
const origin = acc.claimOrigin.get(message.id)
|
|
1722
|
+
const delivery = origin === undefined ? {} : { delivery: origin === 'next-turn' ? 'queued' as const : 'steered' as const }
|
|
1723
|
+
acc.claimOrigin.delete(message.id)
|
|
1724
|
+
if (message.source.kind === 'user' || (message.source.kind === 'plugin' && REMINDER_PLUGINS.has(message.source.plugin))) {
|
|
1725
|
+
appendReplayEntry(acc, { kind: 'user', text, notice: false, ...delivery, ...(images.length === 0 ? {} : { images }), ...(files.length === 0 ? {} : { files }) })
|
|
1275
1726
|
acc.stats = {
|
|
1276
1727
|
...acc.stats,
|
|
1277
1728
|
contextSegments: {
|
|
@@ -1281,9 +1732,21 @@ export function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent):
|
|
|
1281
1732
|
}
|
|
1282
1733
|
return true
|
|
1283
1734
|
}
|
|
1735
|
+
if (message.source.kind === 'plugin' && HIDDEN_SNAPSHOT_PLUGINS.has(message.source.plugin)) {
|
|
1736
|
+
acc.stats = {
|
|
1737
|
+
...acc.stats,
|
|
1738
|
+
contextSegments: {
|
|
1739
|
+
...acc.stats.contextSegments,
|
|
1740
|
+
system: acc.stats.contextSegments.system + estimateTokens(text),
|
|
1741
|
+
},
|
|
1742
|
+
}
|
|
1743
|
+
return true
|
|
1744
|
+
}
|
|
1284
1745
|
const notice = message.source.kind === 'plugin' && message.source.form === 'notice'
|
|
1285
1746
|
? message.source.summary
|
|
1286
|
-
: message.source.kind
|
|
1747
|
+
: message.source.kind === 'plugin'
|
|
1748
|
+
? message.source.plugin
|
|
1749
|
+
: message.source.kind
|
|
1287
1750
|
const summary = boundContextSummary(notice)
|
|
1288
1751
|
appendReplayEntry(acc, { kind: 'user', text: summary, notice: true })
|
|
1289
1752
|
acc.stats = {
|
|
@@ -1296,12 +1759,15 @@ export function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent):
|
|
|
1296
1759
|
return true
|
|
1297
1760
|
}
|
|
1298
1761
|
case 'agent/inbox/spliced': {
|
|
1299
|
-
const { target, start, removedCount = 0, inserted } = event.data
|
|
1762
|
+
const { target, start, removedCount = 0, inserted, outcome } = event.data
|
|
1300
1763
|
const ids = target === 'next-turn' ? acc.pendingTurn : acc.pendingStep
|
|
1301
1764
|
const removed = ids.slice(start, start + removedCount)
|
|
1302
1765
|
acc.ops += removed.length
|
|
1303
1766
|
ids.splice(start, removedCount)
|
|
1304
1767
|
acc.ops += removed.length
|
|
1768
|
+
// A claim keeps the origin for the user message still to come; a real
|
|
1769
|
+
// cancellation retires it with the row.
|
|
1770
|
+
if (outcome === 'canceled') for (const id of removed) acc.claimOrigin.delete(id)
|
|
1305
1771
|
for (const id of removed) {
|
|
1306
1772
|
const list = acc.pendingIndex.get(id)
|
|
1307
1773
|
if (list === undefined) continue
|
|
@@ -1318,6 +1784,9 @@ export function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent):
|
|
|
1318
1784
|
// later inbox event.
|
|
1319
1785
|
ids.splice(start, 0, ...inserted.map(message => message.id))
|
|
1320
1786
|
for (const message of inserted) {
|
|
1787
|
+
// Same rule as the reducer: only a submission that arrived while a
|
|
1788
|
+
// turn was running is "queued"/"steered" rather than ordinary.
|
|
1789
|
+
if (acc.busy) acc.claimOrigin.set(message.id, target)
|
|
1321
1790
|
const images = imagesOf(message.content)
|
|
1322
1791
|
const files = filesOf(message.content)
|
|
1323
1792
|
appendReplayEntry(acc, { kind: 'pending', messageId: message.id, target, text: pendingText(message.content), ...(images.length === 0 ? {} : { images }), ...(files.length === 0 ? {} : { files }) })
|
|
@@ -1394,14 +1863,19 @@ export function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent):
|
|
|
1394
1863
|
const reasoning = reasoningOf(event.data.message.content)
|
|
1395
1864
|
acc.streaming = ''
|
|
1396
1865
|
acc.streamingReasoning = ''
|
|
1397
|
-
|
|
1866
|
+
// Same zero-line guard as the live fold: tool-only settlements carry
|
|
1867
|
+
// timing/usage but no renderable transcript entry.
|
|
1868
|
+
if (text !== '' || reasoning !== '' || event.data.interrupted === true) {
|
|
1869
|
+
appendReplayEntry(acc, { kind: 'assistant', text, reasoning, interrupted: event.data.interrupted === true ? true : undefined })
|
|
1870
|
+
}
|
|
1398
1871
|
acc.stats = {
|
|
1399
1872
|
...acc.stats,
|
|
1400
1873
|
llmMs: acc.stats.llmMs + (started === undefined ? 0 : Math.max(0, event.time - started)),
|
|
1401
1874
|
usage: usage === undefined ? totals : {
|
|
1402
|
-
|
|
1875
|
+
uncachedInputTokens: totals.uncachedInputTokens + usage.inputTokens,
|
|
1403
1876
|
outputTokens: totals.outputTokens + usage.outputTokens,
|
|
1404
1877
|
cacheReadTokens: totals.cacheReadTokens + (usage.cacheReadTokens ?? 0),
|
|
1878
|
+
cacheWriteTokens: totals.cacheWriteTokens + (usage.cacheWriteTokens ?? 0),
|
|
1405
1879
|
},
|
|
1406
1880
|
lastPromptTokens: usage === undefined ? acc.stats.lastPromptTokens
|
|
1407
1881
|
: usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0),
|
|
@@ -1433,6 +1907,8 @@ export function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent):
|
|
|
1433
1907
|
state: 'running',
|
|
1434
1908
|
summary: '',
|
|
1435
1909
|
detail: undefined,
|
|
1910
|
+
subs: [],
|
|
1911
|
+
subsDropped: 0,
|
|
1436
1912
|
})
|
|
1437
1913
|
indexList(acc.toolIndex, data.callId).push(acc.entries.length - 1)
|
|
1438
1914
|
acc.stats = {
|
|
@@ -1603,6 +2079,10 @@ export function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent):
|
|
|
1603
2079
|
if (line !== undefined) appendReplayEntry(acc, { kind: 'turn-marker', text: line })
|
|
1604
2080
|
return true
|
|
1605
2081
|
}
|
|
2082
|
+
case 'schedule/change':
|
|
2083
|
+
acc.schedules = applyScheduleChange(acc.schedules, event.data)
|
|
2084
|
+
acc.ops += 1
|
|
2085
|
+
return true
|
|
1606
2086
|
case 'session/title':
|
|
1607
2087
|
acc.title = event.data.title
|
|
1608
2088
|
return true
|
|
@@ -1664,8 +2144,75 @@ export function replayProjectEvent(acc: ReplayAccumulator, event: SessionEvent):
|
|
|
1664
2144
|
updateReplayById<CommandEntry>(acc, acc.commandIndex, data.commandId, entry => entry.commandId === data.commandId, update)
|
|
1665
2145
|
return true
|
|
1666
2146
|
}
|
|
1667
|
-
default:
|
|
2147
|
+
default: {
|
|
2148
|
+
// PTC sub-dispatch pair, string-guarded like the live fold above; the
|
|
2149
|
+
// parent run_code row is addressed through the same call-id index the
|
|
2150
|
+
// tool/result case uses.
|
|
2151
|
+
const type = event.type as string
|
|
2152
|
+
if (type === 'tool/ptc-dispatch-start') {
|
|
2153
|
+
const data = event.data as PtcDispatchStartData
|
|
2154
|
+
acc.subStart.set(data.subCallId, event.time)
|
|
2155
|
+
updateReplayById<ToolEntry>(acc, acc.toolIndex, data.rootCallId, entry => entry.callId === data.rootCallId, entry =>
|
|
2156
|
+
pushSubDispatch(entry, {
|
|
2157
|
+
subCallId: data.subCallId,
|
|
2158
|
+
name: data.name,
|
|
2159
|
+
preview: toolArgumentsPreview(subDispatchArguments(data.arguments), data.name),
|
|
2160
|
+
state: 'running',
|
|
2161
|
+
summary: '',
|
|
2162
|
+
durationMs: 0,
|
|
2163
|
+
}))
|
|
2164
|
+
return true
|
|
2165
|
+
}
|
|
2166
|
+
if (type === 'tool/ptc-dispatch') {
|
|
2167
|
+
const data = event.data as PtcDispatchData
|
|
2168
|
+
const started = acc.subStart.get(data.subCallId)
|
|
2169
|
+
acc.subStart.delete(data.subCallId)
|
|
2170
|
+
const summary = boundContextSummary(textOf(data.content))
|
|
2171
|
+
const durationMs = started === undefined ? 0 : Math.max(0, event.time - started)
|
|
2172
|
+
updateReplayById<ToolEntry>(acc, acc.toolIndex, data.rootCallId, entry => entry.callId === data.rootCallId, entry =>
|
|
2173
|
+
settleSubDispatch(entry, data, summary, durationMs))
|
|
2174
|
+
return true
|
|
2175
|
+
}
|
|
2176
|
+
// Durable workflow record, string-guarded like the live fold above.
|
|
2177
|
+
if (type === 'tool-workflow/run-start') {
|
|
2178
|
+
const data = event.data as WorkflowRunStartData
|
|
2179
|
+
appendReplayEntry(acc, {
|
|
2180
|
+
kind: 'workflow',
|
|
2181
|
+
runId: data.runId,
|
|
2182
|
+
name: data.name,
|
|
2183
|
+
members: [],
|
|
2184
|
+
membersDropped: 0,
|
|
2185
|
+
state: 'running',
|
|
2186
|
+
})
|
|
2187
|
+
indexList(acc.workflowIndex, data.runId).push(acc.entries.length - 1)
|
|
2188
|
+
return true
|
|
2189
|
+
}
|
|
2190
|
+
if (type === 'tool-workflow/agent-start') {
|
|
2191
|
+
const data = event.data as WorkflowAgentStartData
|
|
2192
|
+
updateReplayById<WorkflowEntry>(acc, acc.workflowIndex, data.runId, entry => entry.runId === data.runId, entry =>
|
|
2193
|
+
pushWorkflowMember(entry, {
|
|
2194
|
+
seq: data.seq,
|
|
2195
|
+
label: data.label,
|
|
2196
|
+
phase: data.phase ?? '',
|
|
2197
|
+
childId: data.childId,
|
|
2198
|
+
outcome: 'running',
|
|
2199
|
+
}))
|
|
2200
|
+
return true
|
|
2201
|
+
}
|
|
2202
|
+
if (type === 'tool-workflow/agent-end') {
|
|
2203
|
+
const data = event.data as WorkflowAgentEndData
|
|
2204
|
+
updateReplayById<WorkflowEntry>(acc, acc.workflowIndex, data.runId, entry => entry.runId === data.runId, entry =>
|
|
2205
|
+
settleWorkflowMember(entry, data))
|
|
2206
|
+
return true
|
|
2207
|
+
}
|
|
2208
|
+
if (type === 'tool-workflow/run-end') {
|
|
2209
|
+
const data = event.data as WorkflowRunEndData
|
|
2210
|
+
updateReplayById<WorkflowEntry>(acc, acc.workflowIndex, data.runId, entry => entry.runId === data.runId, entry =>
|
|
2211
|
+
({ ...entry, state: data.stopReason }))
|
|
2212
|
+
return true
|
|
2213
|
+
}
|
|
1668
2214
|
return false
|
|
2215
|
+
}
|
|
1669
2216
|
}
|
|
1670
2217
|
}
|
|
1671
2218
|
|
|
@@ -1715,13 +2262,16 @@ function materializeReplayView(acc: ReplayAccumulator, copy: boolean): Transcrip
|
|
|
1715
2262
|
systemPrompt: acc.systemPrompt,
|
|
1716
2263
|
sandbox: acc.sandbox,
|
|
1717
2264
|
goal: acc.goal,
|
|
2265
|
+
schedules: acc.schedules,
|
|
1718
2266
|
pending: { 'next-turn': [...acc.pendingTurn], 'next-step': [...acc.pendingStep] },
|
|
2267
|
+
claimOrigin: new Map(acc.claimOrigin),
|
|
1719
2268
|
stats: acc.stats,
|
|
1720
2269
|
// Handed-out views get their own anchors snapshot: the accumulator keeps
|
|
1721
2270
|
// folding its live containers, and no consumer may observe that.
|
|
1722
2271
|
anchors: {
|
|
1723
2272
|
stepStart: new Map(acc.stepStart),
|
|
1724
2273
|
toolStart: new Map(acc.toolStart),
|
|
2274
|
+
subStart: new Map(acc.subStart),
|
|
1725
2275
|
firstChunkAt: new Map(acc.firstChunkAt),
|
|
1726
2276
|
compactionTokens: new Map(acc.compactionTokens),
|
|
1727
2277
|
lastPruneTokens: acc.lastPruneTokens,
|
|
@@ -1828,6 +2378,7 @@ export function settledEntryCount(entries: readonly TranscriptEntry[]): number {
|
|
|
1828
2378
|
if (entry.kind === 'tool' && entry.state === 'running') return index
|
|
1829
2379
|
if (entry.kind === 'retry' && entry.state === 'running') return index
|
|
1830
2380
|
if (entry.kind === 'command' && entry.state === 'running') return index
|
|
2381
|
+
if (entry.kind === 'workflow' && entry.state === 'running') return index
|
|
1831
2382
|
}
|
|
1832
2383
|
return entries.length
|
|
1833
2384
|
}
|