Repository navigation
Expand file tree
/
Copy pathsessionfile.go
More file actions
3271 lines (3121 loc) · 147 KB
/
Copy pathsessionfile.go
File metadata and controls
3271 lines (3121 loc) · 147 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package session
import (
"bufio"
"bytes"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/Agent-Field/agentfield/sdk/go/ai"
"github.com/Agent-Field/codeaf/internal/filelock"
"github.com/Agent-Field/codeaf/internal/provider"
"github.com/Agent-Field/codeaf/internal/roles"
"github.com/Agent-Field/codeaf/internal/trace"
)
// The session file is JSONL: one header line, then one line per COMPLETED
// message and per compaction pass. Append-only and line-oriented, so a crash
// mid-write costs the last line and nothing before it, and a resume is a
// forward read with no rewrite.
//
// Content is flattened to text. A session's messages are text — the tools
// return text and the person types text — and keeping the part array would
// buy fidelity for a shape that does not occur while making every line
// unreadable to the person the transcript is for.
//
// The one part that is not text is an image, and it is journaled as a REFERENCE
// rather than as content: path, digest, media type. A 4MB photo base64'd into a
// JSONL line is how a session file dies — it becomes unreadable to a person, it
// is re-read into memory on every resume, and it grows the file by more than the
// whole conversation around it. The bytes are already on disk at a path this
// machine can read, so the journal writes where they are and what they were, and
// the replay checks the second before trusting the first (see [journalPart]).
//
// The file is locked while it is open. Two codeaf processes resuming the same
// path would both replay it and both append, and their lines interleave into
// one transcript that belongs to neither — the last writer's resume reads the
// other's messages as its own. A resume picks the newest file by mtime, so the
// two windows converge on the same path by default rather than by accident.
// openSessionFile therefore takes a non-blocking exclusive flock and the loser
// gets ErrSessionLocked, which names the file so the surface can offer "open it
// where it is" or "start a new one".
const sessionFileVersion = 1
// ErrSessionLocked is what a second open of a live session file returns. Match
// it with errors.Is; the *SessionLockedError it wraps carries the path.
var ErrSessionLocked = errors.New("session file is open in another codeaf")
// errNewerFormat is the ONE refusal a reading of a session file can carry that
// is about the file's FORMAT rather than about this machine's luck with it — a
// header declaring a version above [sessionFileVersion].
//
// It is a sentinel because a caller has to be able to tell it apart. "This was
// written by a newer codeaf" is a true and useful thing to say to somebody, and
// saying it about a disk that went away, or a read that was cut off, would be a
// confident wrong answer sending them to upgrade a build that is already fine.
// Match it with errors.Is; the *newerFormatError it wraps carries the path and
// both versions, and prints the sentence people are shown and the manual quotes
// (internal/manual/chat/sessions-and-rewind.md).
var errNewerFormat = errors.New("session file was written by a newer codeaf")
// newerFormatError names the file and the two format versions.
type newerFormatError struct {
Path string
Version int
Reads int
}
func (e *newerFormatError) Error() string {
return fmt.Sprintf("session file: %s was written by a newer codeaf (format version %d; this build reads %d)",
e.Path, e.Version, e.Reads)
}
func (e *newerFormatError) Unwrap() error { return errNewerFormat }
// SessionLockedError names the file another process holds.
type SessionLockedError struct{ Path string }
func (e *SessionLockedError) Error() string {
return fmt.Sprintf("session file: %s is open in another codeaf", e.Path)
}
func (e *SessionLockedError) Unwrap() error { return ErrSessionLocked }
type sessionHeader struct {
Type string `json:"type"`
Version int `json:"version"`
ID string `json:"id"`
Cwd string `json:"cwd"`
Model string `json:"model"`
Timestamp string `json:"timestamp"`
}
type sessionEntry struct {
// Presentation is the source-authored audience of this occurrence, never a
// rule inferred from its words. Older entries omit it and use conservative
// compatibility handling only for complete reserved bookkeeping records.
Presentation *messagePresentation `json:"presentation,omitempty"`
Type string `json:"type"`
Role string `json:"role,omitempty"`
Content string `json:"content,omitempty"`
ToolCalls []ai.ToolCall `json:"toolCalls,omitempty"`
ToolCallID string `json:"toolCallId,omitempty"`
// Reasoning fields are the assistant continuation exactly as it arrived.
// They stay beside the message rather than inside Content so a resumed tool
// loop preserves both the wire contract and what the person actually saw.
ReasoningField string `json:"reasoningField,omitempty"`
Reasoning string `json:"reasoning,omitempty"`
ReasoningDetails json.RawMessage `json:"reasoningDetails,omitempty"`
// ReasoningModel is the slug the working above was produced under, so a
// resumed conversation that has since switched models does not replay
// one endpoint's encrypted thinking to another. Absent in older files.
ReasoningModel string `json:"reasoningModel,omitempty"`
// Parts are the message's non-text content parts as durable references, in
// the order they sit in the message AFTER its text. Absent on every message
// that is only words, which is nearly all of them — a reader of an old file
// and a reader of a new one see the same lines for the same conversation.
Parts []journalPart `json:"parts,omitempty"`
// Note marks a user-role line the SESSION wrote rather than the person: a
// task's completion note and the other news that rides the steering queue
// (agent.go's [Agent.enqueueNote]). The message is user-role because that is
// what the model must read it as, and this is the one bit that says who
// actually said it.
//
// It exists for the replay. Without it a resumed conversation draws the
// harness's own line with the person's "›" in front of it — words in their
// mouth they never typed, and the opposite of what the live surface does with
// the same note ([Agent.wakeLocked], tui3's startFollow). It is absent from
// every file written before it existed, and those lines replay exactly as
// they always did.
Note bool `json:"note,omitempty"`
ReplyTags []TaskReplyTag `json:"replyTags,omitempty"`
// Caption is a `caption` line: the sentence the cheap narrator wrote about
// one BATCH of tool calls while it ran, and the family of work it named
// (caption.go, actioncategory.go).
//
// IT IS ITS OWN LINE BECAUSE IT ARRIVES AFTER THE MESSAGE IT IS ABOUT. The
// assistant message carrying a batch is journaled BEFORE the batch runs; the
// narration lands half a second later at the earliest, so there is no line
// open to write it into. It is anchored instead — [journalCaption.CallID] is
// the batch's first call — which is the same identity a live surface pairs
// on, so the record and the stream name the step the same way.
//
// WITHOUT IT A REOPENED CONVERSATION LOSES THE SENTENCE AND THE MARK. The
// deterministic composite recomposes something from the tool names, so the
// row is not blank — but `test` becomes `run` and "starting the local
// server" becomes "running 1 command", which is a conversation that reads
// differently on Tuesday than it did on Monday. Absent from every line that
// is not one, and from every file written before it existed.
Caption *journalCaption `json:"caption,omitempty"`
// Took is a `took` line: how long ONE tool call's own work ran, keyed by the
// call's id (loop.go's EventToolFinished).
//
// IT IS ITS OWN LINE FOR THE CAPTION'S REASON. The finish is known the
// instant the tool returns, and the tool MESSAGE that answers it is journaled
// only after the whole batch has run — so there is no result line open to
// write the figure into when it is measured. Anchored by call id, it is the
// same identity a live surface pairs on, so the record and the stream name
// the duration the same way.
//
// WITHOUT IT A REOPENED PAGE LOSES THE FIGURE. The live stream writes it onto
// the row as EventToolFinished arrives; a page that opens after the batch —
// or a room rebuilt from the journal after a landing — has only the record,
// and a record that carried Args and Output but not Took drew the rows with
// no duration at all. Absent from every line that is not one, and from every
// file written before the line existed.
Took *journalTook `json:"took,omitempty"`
// Pace is ONE TURN'S DECOMPOSITION: how long the person waited to be sent
// anywhere, how long they then waited for a word, and the worst gap between a
// tool result and the next request. Absent from every line that is not one,
// and from every file written before it existed.
Pace *journalPace `json:"pace,omitempty"`
// Deliveries names the durable deliveries this line is the record of
// ([durableDelivery]): a landing's news, identified by session, task,
// attempt and ending. It is what lets a resumed session tell a landing it
// already recorded from one it still owes, without trusting a checkpoint
// that may not have been written. Absent from every line that is not one,
// and from every file written before it existed.
Deliveries []string `json:"deliveries,omitempty"`
// Steer marks the two lines that belong to STEERING — a sentence the person
// typed into a turn that was already running (steer.go).
//
// On a `message` line it says that this user message did not open the turn it
// sits in: it was spliced into it, and the model read it as part of the same
// question. On a `steer` line — a line that is not a message at all — it says
// the opposite: those words were sent at that turn, no request of it ever
// carried them, and they went on to ask their own question a moment later.
//
// Absent from every line written before it existed, and from every line that
// is not one of those two, so a file this build reads and a file it writes
// tell the same conversation.
Steer *journalSteer `json:"steer,omitempty"`
// Compaction fields.
//
// Summary is LEGACY ONLY: it is the prose a summarizer wrote for every
// marker up to the pass that stopped calling one, and it is still read so a
// session compacted last week resumes as itself. Nothing writes it now.
Summary string `json:"summary,omitempty"`
TokensBefore int `json:"tokensBefore,omitempty"`
// Stubbed and Folded are what the current pass did (loop.go): how many tool
// results became pointers to their own bytes, and how many assistant
// messages went into one marker line.
//
// They are the RECORD and never the instruction. A modern marker rebuilds
// nothing by itself — the pass re-journals the whole rebuilt window behind
// it, so replay reads the stubs and the fold marker as ordinary message
// lines and reconstructs the transcript verbatim rather than from counts.
Stubbed int `json:"stubbed,omitempty"`
Folded int `json:"folded,omitempty"`
// Summarized is how many messages a summary note replaced
// (compact_summary.go). The note itself is in the window like any line.
Summarized int `json:"summarized,omitempty"`
// Window is HOW MANY MESSAGE LINES THE PASS RE-JOURNALED BEHIND THIS MARKER
// — the length of the rebuilt window [sessionFile.appendCompaction] writes
// out after it.
//
// It is the one thing a reader cannot work out for itself, and it is what
// makes the conversation above a marker readable. The lines above are the
// original ones and the lines below are the pass's rewritten copy OF THE SAME
// CONVERSATION, so a reader that showed both would draw the whole session
// twice; it needs to know where the copy ends and the conversation carries on.
// Nothing in the file says that but this number.
//
// ABSENT MEANS UNKNOWN, not zero. Every marker written before this field
// existed — and every legacy marker, whose kept tail was re-journaled with no
// count either — leaves the region above it unplaceable, and a reader must
// then decline to offer it rather than guess (see [compactionOverlap]). It is
// deliberately NOT derived from Stubbed and Folded: those are the RECORD of
// what the pass did, nothing rebuilds from them, and a length derived from a
// count is a length that drifts the day the pass changes.
Window int `json:"window,omitempty"`
// Dropped is how many messages a rewind removed (rewind.go). It is a COUNT
// rather than a cut position because the file is append-only and positions
// in it are not positions in the replayed transcript: a compaction marker
// earlier in the file collapses everything before it into one message. A
// count is applied to whatever the replay is holding when it reaches the
// line, which is exactly the list the rewind was taken against.
Dropped int `json:"dropped,omitempty"`
// Title is the session's name (title.go). It is its own line rather than a
// header field because the header is written ONCE, when the file is
// created, and the name is not known until the first turn has been
// answered. A line is also how a name can be rewritten later without any
// reader having to rewrite the file: the replay takes the LAST title line.
Title string `json:"title,omitempty"`
ShortTitle string `json:"shortTitle,omitempty"` // Deprecated: accepted for old records; never used as a name.
// Usage is what one COMPLETED turn — or one auxiliary call beside it — cost,
// and it is on its own line rather than on the assistant message that ended
// the turn: a turn is several requests and several messages, and hanging the
// bill on one of them would be a number that is true of the line above it and
// of nothing else. Absent from every line that is not a seal, and from every
// file written before it existed.
Usage *journalUsage `json:"usage,omitempty"`
// Call is ONE request's accounting, beside the seal rather than inside it.
// Absent from every line that is not a call line, and from every file
// written before it existed.
Call *journalCall `json:"call,omitempty"`
// Error is ONE CALL THAT FAILED (see [journalError]). It is the call line's
// opposite number and it exists for the same reason: a turn that died on a
// provider refusal left this file saying only that it had ended.
Error *journalError `json:"error,omitempty"`
// Mark is ONE reading taken at a checkpoint mark, and Ceiling is what the
// last mark then did with the turn (checkpoint.go). Absent from every line
// that is not one of those, and from every file written before they existed.
Mark *journalMark `json:"mark,omitempty"`
Ceiling *journalCeiling `json:"ceiling,omitempty"`
// Carry is ONE RUNG of the ladder that decides what a handed-over worker
// opens on (see [journalCarry]). Absent from every line that is not one, and
// from every file written before it existed.
Carry *journalCarry `json:"carry,omitempty"`
// Failure is ONE CLASSIFICATION made at the response boundary (see
// [journalFailure]). Absent from every line that is not one, and from every
// file written before it existed.
Failure *journalFailure `json:"failure,omitempty"`
// Division is ONE division put to the road, whoever asked for it
// (task_divide.go). Absent from every line that is not one, and from every
// file written before it existed.
Division *journalDivision `json:"division,omitempty"`
// Flight is ONE END OF ONE REQUEST made on a node's behalf (see
// [journalFlight]). Absent from every line that is not one, and from every
// file written before it existed.
Flight *journalFlight `json:"flight,omitempty"`
// Principal is ONE MOMENT THE SESSION'S GOAL OWNER DECIDED SOMETHING
// (see [journalPrincipal]). Absent from every line that is not one, and from
// every file written before it existed — which is every attended session,
// because a person decides these things in their own head.
Principal *journalPrincipal `json:"principal,omitempty"`
// Rule is ONE MOMENT OF ONE PROCESS RULE the turn loop enforces (see
// [journalRule]). Absent from every line that is not one, and from every file
// written before it existed.
Rule *journalRule `json:"rule,omitempty"`
// Abandoned is ONE TURN THIS SESSION LET GO OF (see [journalAbandoned]).
// Absent from every line that is not one, and from every file written before
// it existed.
Abandoned *journalAbandoned `json:"abandoned,omitempty"`
// Created is ONE FILE THIS SESSION MADE THAT WAS NOT THERE BEFORE (see
// [journalCreated]). It is a line of its own rather than a field on the
// message that wrote it because the fact it carries — DID THIS EXIST BEFORE
// — can only be measured at the moment of the call and can never be
// recovered from the transcript afterwards.
Created *journalCreated `json:"created,omitempty"`
Timestamp string `json:"timestamp"`
}
// journalPrincipal is ONE MOMENT THIS SESSION'S GOAL OWNER DECIDED SOMETHING
// (principal.go).
//
// IT EXISTS BECAUSE THE DECISIONS ARE THE FEATURE. An unattended run that
// carried itself on for four hours and one that stopped after eighteen minutes
// read IDENTICALLY in this file before it: the acceptance the whole ask was
// measured against existed nowhere, the moment the session decided the work was
// finished existed nowhere, and what it deleted on the way out existed nowhere.
// Every one of those is a thing a person would want to argue with afterwards.
//
// Event is what the moment was: `acceptance` when the done-condition for the
// whole ask was written and frozen, `delivery` when a retained result made its
// requested destination relevant, `decided` for the end of a turn that
// stopped, `checked` for one run of the session's declared checks from clean,
// and `reconciled` for the sweep that puts back what the session left lying
// about. Decision is the verb a `decided` line carries — carry on, done or stop
// — and Reason is why, in the words a person reads.
//
// IT IS EVIDENCE AND NEVER SPEND, for [journalCall]'s reason: what the
// acceptance call cost is already on its own call line.
type journalPrincipal struct {
Delivery *deliveryContract `json:"delivery,omitempty"`
Who string `json:"who,omitempty"`
Event string `json:"event,omitempty"`
Acceptance string `json:"acceptance,omitempty"`
Decision string `json:"decision,omitempty"`
Reason string `json:"reason,omitempty"`
Brief string `json:"brief,omitempty"`
Checks []string `json:"checks,omitempty"`
Failed []string `json:"failed,omitempty"`
// Unread is each declared check the terminal reading did not start, so the
// checked row keeps the same fact the person-facing ending names.
Unread []string `json:"unread,omitempty"`
Removed []string `json:"removed,omitempty"`
Kept []string `json:"kept,omitempty"`
// Ignored is the gitignored files the terminal reading found in the
// deliverable tree that no ledger explains — build products, ignored by
// git, not in the landing ([Agent.ignoredBuildProducts]). They ride the
// `reconciled` row because that row is what a reviewer reads to learn what
// the tree holds, and they are a report and never work for the sweep: a
// gitignored target/ a build made is removed, if ever, by a decision of
// its own.
Ignored []string `json:"ignored,omitempty"`
// Stashed is how many entries `git stash list` named at the terminal
// reading, and it rides the `checked` row: work the session took out of the
// tree and never put back is part of what that reading found, and a run
// that finished over a stashed fix used to be unreadable afterwards.
Stashed int `json:"stashed,omitempty"`
WallMS int64 `json:"wallMs,omitempty"`
CostUSD float64 `json:"costUsd,omitempty"`
}
// journalRule is ONE MOMENT OF ONE PROCESS RULE the turn loop can hold a model
// to (processrule.go).
//
// IT EXISTS BECAUSE THE COUNT IS THE MEASUREMENT. The write-your-notes advisory
// fired thirty-two times in one measured conversation and was obeyed
// approximately never, and that fact had to be reconstructed by grepping a
// transcript for a bracketed word. A conversation that has just held a model's
// tools and one that never had to are otherwise identical in this file, so
// nothing could say whether enforcing the rule changed anything.
//
// Rule is the rule's own slug. Event is what the moment was: `advised` for one
// advisory note, `held` for one submission answered with the demand instead of
// run, and `stopped` for a turn that ended because the rule was never met. Count
// is how many ADVISORIES this rule has spent in this conversation so far — the
// same number on every line, so the ratio a reader wants is one subtraction and
// not a sum of two kinds.
//
// IT IS EVIDENCE AND NEVER SPEND, for [journalCall]'s reason: what the turn cost
// is already on its seal.
type journalRule struct {
Rule string `json:"rule,omitempty"`
Event string `json:"event,omitempty"`
Count int `json:"count,omitempty"`
}
// journalAbandoned is ONE TURN NOBODY WAITED FOR THE END OF.
//
// IT IS THE ONE LINE THAT COULD NOT BE RECONSTRUCTED. Every other record of what
// a turn did is written when the turn ENDS — the seal carries its cost, the
// principal line carries its decision — and the whole definition of an abandoned
// turn is that its ending never came. Before this line a turn let go of at a
// bound was indistinguishable in this file from a turn that simply stopped
// talking, which is exactly the reading that made issue #265's four minutes
// unaccountable: the surface was freed, the person moved on, and the file said a
// turn had been stopped and nothing about the fact that something may still have
// been running under it.
//
// Reason is why the turn was let go of ([AbandonReason]); today the only one is
// the stop bound. The token and money figures are THE TURN'S LAST KNOWN SPEND —
// what [Agent.bank] had moved by the moment the door was opened — and they are
// EVIDENCE AND NEVER SPEND, for [journalCall]'s reason exactly: every one of
// those calls already wrote its own line and moved the machine's ledger, so a
// replay that summed this one too would bill the abandoned turn twice.
//
// A turn abandoned before it had spent anything writes the line with no figures
// on it, which is the emptiness law: the fact worth recording is that the turn
// was let go of, and zeroes would read as a measurement.
//
// THERE IS NO DURATION ON IT, deliberately. A seal carries how long its turn
// took because a turn that ends knows when it ended; nobody waited for this one,
// so the only honest answer would be "how long until somebody stopped waiting",
// which is [tui3.stopGrace] plus whenever the person happened to press the key —
// a fact about the surface and not about the turn. The line's own timestamp says
// when it was let go of, which is the question that can be answered.
type journalAbandoned struct {
Reason string `json:"reason,omitempty"`
Input int `json:"input,omitempty"`
Output int `json:"output,omitempty"`
CacheRead int `json:"cacheRead,omitempty"`
CacheWrite int `json:"cacheWrite,omitempty"`
CostUSD float64 `json:"costUsd,omitempty"`
Calls int `json:"calls,omitempty"`
}
// journalCreated is ONE FILE THIS SESSION MADE.
//
// Path is absolute — what a sweep is actually given — and Shown is the same
// path as a person reads it, relative to the workspace when it is under one.
// Both are kept for [fileChange]'s reason: the answer a person reads names
// files the way they asked for them, and the answer a machine acts on cannot
// depend on where a process happened to be standing.
//
// A MODIFIED FILE NEVER WRITES ONE. The whole value of the line is the word
// CREATED: nothing in this build may remove a file that was there before the
// session started, and a line that could not tell the two apart would be a line
// that cannot be acted on.
type journalCreated struct {
Path string `json:"path,omitempty"`
Shown string `json:"shown,omitempty"`
}
// journalCall is what ONE provider response reported, on its own line.
//
// IT IS EVIDENCE AND NEVER SPEND. The seal above already carries every one of
// these numbers, summed; a replay that added these lines too would bill the
// session twice for the same calls. Nothing reads them back into the session's
// totals, and [replaySessionFile] says so where it drops them.
//
// It exists because a turn is sixty-odd requests with wildly different shapes —
// a cold first call, then fifty that are almost all cache read — and the sum of
// them cannot answer what a call with THIS many cached tokens actually cost.
// That question had to be reconstructed from transcript byte counts once, in a
// cost autopsy that found this surface paying 3.5× its models' list prices; the
// line is so the next one is a read rather than a reconstruction.
//
// Endpoint is who served it, exactly as the router spelled it, and it is the
// field the summed seal could never carry: a turn routed across three endpoints
// has one bill and three tariffs.
//
// EVERY REQUEST THIS SESSION MAKES WRITES ONE, the errands included
// (auxiliary.go's [Agent.callRole]). It did not always: the line was written
// from the turn's own accounting alone, so a measured run's call lines summed to
// $0.123 while the real bill was $0.739 — the difference being three side-calls
// to a mastermind that left `usage` lines and no shape at all. A record that
// covers most of the money is a record that answers cost questions wrongly, so
// the sum of these lines IS the bill.
//
// Role is which errand made the call, spelled as the role registry spells it
// (internal/roles). It is ABSENT on the conversation's own requests rather than
// spelled "chat", because absent is what the whole file means by "this is the
// session itself" — [journalUsage] already writes its own Role the same way —
// and a name invented for the default case is a name that has to be kept in step
// with a registry it is not in.
//
// Arm marks the one kind of row that did not come off a live stream: a request
// whose usage block never arrived, written from the provider's own RECEIPT
// after the fact (loop.go's [Agent.reconciled]). It is "hedge" for a rescue arm
// and "reconciled" for any other late receipt, and absent on every ordinary
// row. It exists because these are exactly the expensive requests — a hedge
// fires when the first answer is slow, a receipt is fetched when a stream was
// cut — and a journal that records every cheap call and none of the dear ones
// answers a cost autopsy wrongly: the measured session's call lines summed to
// barely half its bill until the arms wrote theirs. The money is already banked
// by the reconciliation itself, so the row is evidence here and never spend,
// the same as every other line of this shape.
type journalCall struct {
Model string `json:"model,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
Role string `json:"role,omitempty"`
Arm string `json:"arm,omitempty"`
Input int `json:"input,omitempty"`
CacheRead int `json:"cacheRead,omitempty"`
CacheWrite int `json:"cacheWrite,omitempty"`
Output int `json:"output,omitempty"`
CostUSD float64 `json:"costUsd,omitempty"`
}
// The two words [journalCall.Arm] is ever spelled in. They are constants rather
// than literals at the call site for the reason every vocabulary in this file
// is: the row is a record somebody else's autopsy reads back, and a word
// spelled in two places is a word that drifts.
const (
// journalArmHedge is a rescue arm's receipt: the request ran beside the
// stream it was rescuing, and its cost is waste the session chose to buy
// speed with.
journalArmHedge = "hedge"
// journalArmReconciled is any other late receipt: a stream that ended
// before its usage block — a cut, a torn ending — priced after the fact.
journalArmReconciled = "reconciled"
)
// armCall is the line one reconciled request leaves: what the receipt carries
// and nothing more. A receipt names no endpoint — it is the provider's account
// of the call, not the router's — so the field stays empty rather than
// repeating who the stream THOUGHT was serving (the emptiness law: absent is
// "nobody said", and a guessed endpoint would read as a measured one).
func armCall(receipt provider.Reconciled) journalCall {
arm := journalArmReconciled
if receipt.Hedged {
arm = journalArmHedge
}
return journalCall{
Model: strings.TrimSpace(receipt.Model),
Arm: arm,
Input: receipt.PromptTokens,
CacheRead: receipt.CachedTokens,
Output: receipt.CompletionTokens,
CostUSD: receipt.Cost,
}
}
// journalError is ONE CALL THAT FAILED, written down where the calls that
// succeeded already are.
//
// ── THE MEASURED FAILURE ────────────────────────────────────────────────────
//
// SWE-Marathon run s2, 22:45 UTC. A turn ended with `error: after 3 retries: API
// error (400): Provider returned error`, the session went idle, and the
// benchmark cell settled with five hours of budget unspent. NOTHING ABOUT THE
// 400 REACHED THIS FILE — no row, no status, no endpoint, no provider name, no
// upstream body — so the autopsy could say that a turn had died and nothing
// whatever about why. A journal that records every call that worked and nothing
// about the ones that did not is a journal that answers the easy question.
//
// So: EVERY FAILED CALL WRITES ONE, and it carries what an autopsy has to ask
// for otherwise. Status, Provider and Raw come off the refusal itself
// (internal/provider's APIError); Endpoint is who the router said was serving;
// Attempt is which rung of the retry ladder this was, so three rows for one step
// read as one ladder rather than three steps; and Input is THE ESTIMATE the
// session made of the request it was about to send, which is the only token
// figure a failed call has — the provider counted none.
//
// Output and DurationMS are the exception to "the provider counted none", and
// they exist for ONE class of failure: a guard cut (internal/provider's
// StreamCut). A cut stream ran for a measurable time and delivered a measurable
// amount of answer before it was ended, and those two figures are what tell a
// silent endpoint apart from one that wrote for eighteen minutes and never
// finished. Every other failure leaves both empty, which is the emptiness law:
// a zero here would read as "it produced nothing", and only a cut can say that
// honestly.
//
// IT IS EVIDENCE AND NEVER SPEND, for [journalCall]'s reason and one more: a
// failed call was not billed, so there is nothing here to sum.
type journalError struct {
Model string `json:"model,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
Role string `json:"role,omitempty"`
// Door is WHICH DOOR ENDED THE TURN, for the one kind of failed call that
// is not a provider's: a turn this machine stopped from the inside
// (stopcause.go). It is the [StopDoor] itself and not the sentence built
// from it, because the whole point of a door is that a reader can ask which
// one it was without comparing prose — and the reader this field exists for
// is the one that decides whether the question is still owed an answer
// (resume.go). Absent on every call a provider failed and on every file
// written before it existed, and a stop this build cannot name is a stop it
// will not act on.
Door string `json:"door,omitempty"`
Status int `json:"status,omitempty"`
Provider string `json:"provider,omitempty"`
Message string `json:"message,omitempty"`
Raw string `json:"raw,omitempty"`
Attempt int `json:"attempt,omitempty"`
Input int `json:"input,omitempty"`
Output int `json:"output,omitempty"`
DurationMS int64 `json:"durationMs,omitempty"`
}
// journalFailure is ONE CLASSIFICATION made at the response boundary: what a bad
// response was taken to be, and what the harness did about it
// (internal/taxonomy).
//
// IT IS THE ROW A BENCH COUNTS. [journalError] already says that a call failed
// and what the provider said; what it cannot say is the only question the money
// turns on — whether the harness read that failure as the WIRE, as the MODEL, or
// as the WORK. Three runs of a measured comparison spent 57–82% of their bill on
// a stronger model bought because four bad responses in a row were read as the
// model being unable, and nothing in the file distinguished that from a model
// that had genuinely failed the work. One line per classification makes the two
// countable and the ratio between them readable.
//
// Class, Reason and Action are always written; everything else is the evidence
// that happened to be there, absent when it was not, which is the emptiness law
// as [journalError] applies it.
type journalFailure struct {
Class string `json:"class"`
Reason string `json:"reason,omitempty"`
Action string `json:"action"`
Model string `json:"model,omitempty"`
Role string `json:"role,omitempty"`
Attempt int `json:"attempt,omitempty"`
Status int `json:"status,omitempty"`
Provider string `json:"provider,omitempty"`
Refuted int `json:"refuted,omitempty"`
SpentUSD float64 `json:"spentUsd,omitempty"`
}
// journalMark is ONE reading taken at a checkpoint mark: what the sidecar was
// asked to draw mid-turn, what it drew, what the harness did about it, and what
// the call itself cost (checkpoint.go's [Agent.readMark]).
//
// IT EXISTS BECAUSE A DECISION NOBODY WROTE DOWN CANNOT BE MEASURED. Three of
// these reads were made on one measured run and cost sixty-two cents between
// them — five times the whole of what the work they were judging cost — and
// every one of them answered "carry on". None of that was in the file: the
// spend showed up as three anonymous auxiliary lines, and what was asked, what
// came back and what it decided existed nowhere at all. So the reading is
// journaled where the money already is, and a bench can join the two.
//
// N is which rung of the ladder this was and Rounds is where the turn stood when
// it fired, which together say whether the ladder is landing where the policy
// says it does. Sketch is THE SHAPE LINE ALONE — the legend is a sentence for a
// worker and not evidence for a reader of the file — and Decision is what the
// harness took off it. Kept is the part of that drawing THAT DID NOT TRAVEL — the
// parts about work the conversation was still holding, which stay with it
// (checkpoint.go's [checkpointRead] and checkpoint_custody.go). It is absent on
// every mark that withheld nothing, which is nearly all of them, and it is what
// tells a reader of the file that a worker opened on less than was drawn and
// exactly how much less. Decision is what the harness took off the drawing:
// `split` when the turn was handed over on account of the
// parts, `continue` when nothing happened, `failed` when no reading came back at
// all. The ceiling's own read is a `continue` too: it decides nothing, and the
// ceiling line that follows it says what actually happened.
type journalMark struct {
N int `json:"n,omitempty"`
Rounds int `json:"rounds,omitempty"`
Model string `json:"model,omitempty"`
CostUSD float64 `json:"costUsd,omitempty"`
Sketch string `json:"sketch,omitempty"`
Kept string `json:"kept,omitempty"`
Decision string `json:"decision,omitempty"`
DurationMS int64 `json:"durationMs,omitempty"`
}
// journalCeiling is what a HANDOVER did with the turn: moved the remaining work
// onto the one road, or ended it where it stood.
//
// IT IS WRITTEN ONCE PER HANDOVER ROAD AND NOT ONLY AT THE CEILING. Every door
// into checkpoint.go's [Agent.handOverRunningTurn] writes one — a mark whose
// drawing had parts in it, a turn past its write allowance, and the ceiling that
// gave this record its name — because the ending is decided there and nowhere
// else. Seam below says which door it was.
//
// It is a line of its own rather than a field on the mark above it because the
// two are different facts about different moments — the mark is a reading and
// this is an act — and because a handover can fire with no reading behind it at
// all (a sidecar nobody could reach still meets the ceiling).
//
// Decision is `moved`, `dropped:nothing-left` — the running model declared the
// work finished AND the mark's own reader agreed nothing remained — or one of the
// two ways a handover ends with no task of its own: `dropped:no-brief`, when
// nothing could be written down for anybody, and `dropped:work-already-out`, when
// everything there was to write down was about pieces this conversation is still
// holding and therefore nobody else's to take (checkpoint.go's custody road). On a
// run with a goal owner two more are possible, and both mean the turn was sealed
// at the handover with no task started: `dropped:stopped`, the owner read the
// ending and stopped the run with a reason, and `dropped:done`, the owner read it
// and said the ask was met (checkpoint.go's [Agent.endTurnUnderSteward]). And the
// write seam writes one more of its own, `dropped:delivering-own-result`: the turn
// is finishing the delivery of a result this conversation already owns, so the
// counter stood down and the work stayed here (writeseam.go's
// [Agent.writeSeamFires]). TaskID names the node when one was ADMITTED, and is
// absent otherwise by the emptiness law the rest of the line keeps — the delivery
// row admits nothing and names its result in Reason instead, so a bench counting
// tasks started off that field still counts only tasks.
//
// Carry NAMES THE RUNG THAT SUPPLIED THE BRIEF the task actually opened on
// (checkpoint.go's [Agent.handOverRunningTurn]): `handoff`, `draft` or `ask`.
// Two ceilings that both read `moved` are not the same event — one started a
// worker on a document written out of the turn's findings, the other started it
// on the person's bare sentence — and until this field the file could not tell
// them apart. The [journalCarry] lines directly above say WHY it was that rung;
// this is the one-word answer a bench can count.
//
// ── AND SEAM IS WHICH DOOR TOOK THE ENDING ──
//
// THE NAME OF THIS RECORD IS OLDER THAN WHAT IT RECORDS. It was the CEILING's
// line, because the ceiling was the only door that wrote one — so a handover the
// MARK road or the WRITE SEAM declined left no decision word in the file at all,
// and the real-model runs behind #567 all ended with an empty list of these while
// the refusal had plainly happened. The row now belongs to the ending rather than
// to the clock that noticed, and Seam says which of the three it was: `mark` for a
// drawing with parts in it, `write` for a turn past its write allowance, `ceiling`
// for the last rung of the ladder.
//
// A ROW WITH NO SEAM ON IT WAS WRITTEN BEFORE THIS EXISTED, and it is a ceiling by
// construction, because the ceiling was the only writer. An old file still reads.
//
// Reason is the reason WHERE THERE IS ONE and is empty everywhere else, which is
// the emptiness law and is most of the time: `dropped:no-brief` has the whole
// [journalCarry] ladder above it saying why each rung produced nothing, and
// `dropped:nothing-left` has no reason to give — nothing contradicted the work being
// done. `dropped:work-already-out` carries one, because the decision word alone
// does not say what was already out, and an autopsy grepping the word should get
// the why in the same line (checkpoint.go's carryHeldWork).
type journalCeiling struct {
Rounds int `json:"rounds,omitempty"`
Seam string `json:"seam,omitempty"`
Decision string `json:"decision,omitempty"`
Reason string `json:"reason,omitempty"`
TaskID uint64 `json:"taskId,omitempty"`
Carry string `json:"carry,omitempty"`
}
// journalCarry is ONE RUNG of the ladder that decides what a worker taken off a
// running turn OPENS ON (checkpoint.go's [Agent.handOverRunningTurn]).
//
// ── THE MEASURED FAILURE ────────────────────────────────────────────────────
//
// SWE-Marathon run s4, 00:01:54Z. A turn hit the round-40 ceiling and the ladder
// ran: the running model's draft came back as seven tokens nobody could work
// from, and the mastermind that writes the real brief was then asked and never
// answered — the call was cut by [checkpointHandoffWindow] ninety seconds later,
// to the millisecond. Both upper rungs returned the empty string, the task opened
// on the person's raw request, and the worker spent twelve minutes and seventy
// calls re-deriving what the chat had already found out.
//
// NONE OF THAT REACHED THIS FILE. The journal held a ceiling line saying `moved`
// and nothing else, which is the same line s2 wrote when the handoff worked and
// the worker opened on a 3.5 KB document. A fallback that changes what a worker
// is started on is an EVENT, not a default, and an event nobody wrote down is a
// difference no autopsy can see.
//
// So EVERY RUNG WRITES ONE. Rung is `handoff`, `draft` or `ask`, in ladder order.
// Outcome is what that rung did — `written`, `degenerate` (words that had stopped
// saying new things, or no words at all), `failed` with Reason carrying the
// provider's own sentence, `skipped` with Reason saying why it was never asked,
// `nothing-left` when the remains contract was answered instead, or `empty` when
// there was nothing there to carry. Chars is the size of what it produced, which
// is the one number that says a 3.5 KB dowry apart from a bare sentence. Used
// marks THE ONE rung that supplied the brief, so a reader of these lines alone —
// at the ceiling and at a mark's split, which writes no ceiling line — can see
// which of them the worker actually opened on.
//
// IT IS EVIDENCE AND NEVER SPEND, for [journalCall]'s reason: the money these
// rungs cost is already on their own call lines.
type journalCarry struct {
Rung string `json:"rung,omitempty"`
Outcome string `json:"outcome,omitempty"`
Reason string `json:"reason,omitempty"`
Chars int `json:"chars,omitempty"`
Used bool `json:"used,omitempty"`
}
// journalDivision is ONE piece of work being put to the division road: who asked,
// how many parts they asked for, how many exist afterwards, and what answered
// (task_divide.go).
//
// IT EXISTS BECAUSE THREE COMPLETELY DIFFERENT OUTCOMES USED TO READ THE SAME.
// A task that ran with one worker had NEVER ASKED to divide, had asked and been
// refused by a free gate, or had asked and been refused by the reviewer — and the
// only trace of any of it was the absence of child nodes. Over three measured
// cells whose work a mastermind had already read as four jobs, every one landed
// `parts=0`, and nothing in any file said which of the three had happened. So one
// line, written wherever the road is asked.
//
// Source is `worker` for a division a worker reached for with the verb and
// `sketch` for one the harness submitted on its behalf out of a mark's drawing
// (task_divide_sketch.go). Requested is what was put; Admitted is how many parts
// exist, which differs when the reviewer merges. Decision is `admitted` or
// `refused:` and the gate that said no, so a bench can tell a floor refusal from
// a busy machine from a reviewer that read the parts as one job.
type journalDivision struct {
TaskID uint64 `json:"taskId,omitempty"`
Source string `json:"source,omitempty"`
Requested int `json:"requested,omitempty"`
Admitted int `json:"admitted,omitempty"`
Decision string `json:"decision,omitempty"`
// Error is why a review came to nothing, on the one decision where that is
// not the same fact as the counter's refusal ([divisionRefusedUnreviewed]).
Error string `json:"error,omitempty"`
// Parts is what was asked for, by title, so a refusal reads against
// something and not against a count.
Parts []string `json:"parts,omitempty"`
// Lifted names the parts the worker graded as ordinary work that were minted
// on the careful tier anyway, because the ratings store said work of that
// kind keeps being turned down on this model (task_divide.go, taskgrade.go).
// It is empty on every division nothing was learned about, which is every
// division until the store has seen the same kind of work settle twice — and
// it is the one place an autopsy can tell a part that was CALLED careful from
// one that EARNED it.
Lifted []string `json:"lifted,omitempty"`
// Shared names the check commands that stood in the done-condition of more
// than one part, on the one decision where that is why the division was
// turned down ([divisionRefusedShared]). It is empty on every other line.
//
// IT IS HERE SO THAT AN AUTOPSY CAN PROVE WHICH RUN WAS THE FAMILY'S rather
// than infer it: the alternative is counting shell calls across four
// worktrees that no longer exist, and the fact worth keeping is the one
// command that would have been bought once per part.
Shared []string `json:"shared,omitempty"`
// Frozen is the commit the family tree stood at when this division was
// admitted — the one world every part of it starts from — and Checkpoint is
// the commit this division WROTE to get there, when the parent had work on
// disk that nobody had committed yet (task_divide_wip.go).
//
// THEY ARE TWO FIELDS BECAUSE THEY ARE TWO FACTS. Every family with a tree of
// its own has a Frozen; only a family whose parent had written something has
// a Checkpoint. One field would leave an autopsy unable to tell a parent that
// had written nothing from a family that was never frozen at all — which are
// the ordinary case and the degradation, and telling them apart is most of
// what this line is for.
Frozen string `json:"frozen,omitempty"`
Checkpoint string `json:"checkpoint,omitempty"`
}
// journalUsage is one turn's accounting as the journal holds it — or, when a
// turn was answered by more than one model, ONE MODEL'S SHARE of it: the seal
// then writes one line per model that answered ([sessionFile.appendUsage]),
// because a sum spelled under a single name attributes the whole turn to
// whichever model answered last.
//
// Duration is milliseconds and not a time.Duration because a time.Duration
// marshals as bare nanoseconds, and this is a file a person reads.
//
// Aux marks a line that was NOT a step of the conversation: the title call, a
// memory reflex, a rendered picture, a folded task node. The distinction is
// what lets a replay rebuild both counters the live session keeps — Calls
// counts every request to the provider, Turns only the ones a turn of the
// person's made (see [Agent.addAuxiliaryUsage]).
type journalUsage struct {
Model string `json:"model,omitempty"`
Input int `json:"input,omitempty"`
Output int `json:"output,omitempty"`
CacheRead int `json:"cacheRead,omitempty"`
CacheWrite int `json:"cacheWrite,omitempty"`
CostUSD float64 `json:"costUsd,omitempty"`
Calls int `json:"calls,omitempty"`
DurationMS int64 `json:"durationMs,omitempty"`
Aux bool `json:"aux,omitempty"`
// Empty marks a paid auxiliary call that reached its output ceiling without
// returning any answer. Role says which errand it was; today only a reflex
// writes the mark.
Empty bool `json:"empty,omitempty"`
// Role names WHAT the auxiliary call was for — "title", "taskname" — on the
// lines where knowing it changes what a person can do with the record. Aux
// says a turn did not ask for the call and Model says which model answered
// it; neither says what was being asked, so a name that came back wrong
// could not be traced to the model that gave it. Absent from a turn's own
// seal and from every auxiliary call that does not name itself, by the same
// emptiness law the rest of the line keeps.
Role string `json:"role,omitempty"`
}
// journalSteer is one steer as the journal holds it: the instant the person
// pressed enter, and whether the turn they aimed it at actually carried it.
//
// The instant is the SEND's and not the line's. A steer typed while a long tool
// batch was running is journaled at the boundary that took it, seconds or
// minutes later, and the file's own Timestamp says that — which is the right
// answer to "when was this recorded" and the wrong one to "when did they say
// it". Both facts are worth keeping and they are kept separately.
//
// Consumed carries NO omitempty, deliberately. False is the meaningful answer
// here — the steer fell through — and a field that vanished when it was false
// would leave a reader unable to tell "it did not land" from "this build did not
// say". Every steer line states its outcome outright.
type journalSteer struct {
At string `json:"at,omitempty"`
Consumed bool `json:"consumed"`
Landing string `json:"landing,omitempty"`
}
// journalCaption is one step's narration as the file keeps it: which BATCH it is
// about, what was said, and which family of work that was.
//
// CallID IS THE ANCHOR AND IT IS THE BATCH'S FIRST CALL. A caption is about a
// run of calls rather than about any one of them, and the run's own identity is
// the identity of the call that opened it — which is a provider id the model
// minted, so it is stable across the journal, the wire and the surface, and it
// cannot be confused with the call that opened the batch AFTER it. Anchoring on
// "the newest tool row" instead is what let a late answer retitle a step it was
// never about (caption.go states the race).
//
// Category carries omitempty because a narrator that named no family is the
// ordinary case and a file should not spend bytes saying so; an absent one
// replays as the empty string, which a surface reads as "ask the tools".
type journalCaption struct {
CallID string `json:"callId"`
Text string `json:"text"`
Category ActionCategory `json:"category,omitempty"`
}
// journalTook is ONE call's own duration as the journal holds it: which call,
// and how long it ran from begin to end of its Execute.
//
// DurationMS rather than a stamped interval, because the journal is not a clock
// — it is a figure the surface already knew live (Event.Took) and must be able
// to say again after a reopen. Zero is never written (see [sessionFile.appendTook]).
// journalPace is ONE TURN'S SHAPE IN MILLISECONDS, written once, at the end.
//
// IT EXISTS BECAUSE THE LAST AUDIT HAD TO DERIVE IT. loop.go's law — the only
// wait a person experiences is the main model generating — is a claim about two
// numbers, and until this line neither of them was written anywhere: the call
// census of 2026-09-11 reconstructed them by subtracting request stamps in
// calls.jsonl, which is how a four-second gate in front of every message went
// unnoticed for as long as it did. A law nobody can measure is a law that rots.
//
// SendMS is what this file is FOR: the person's message to the first request
// leaving. FirstWordMS is what they actually waited for. StepGapMS is the worst
// tool-result-to-next-request gap of the turn, which is the same law said about
// the middle of a turn instead of the front of it.
//
// AND WHAT WAS RUNNING BESIDE IT. Aside names the readings that were in flight
// while the work went on, so a reader can tell a fast turn that asked nothing
// from a fast turn that asked several things concurrently — the difference
// between the law being kept and the readings having been deleted.
type journalPace struct {
SendMS int64 `json:"sendMs"`
FirstWordMS int64 `json:"firstWordMs,omitempty"`
StepGapMS int64 `json:"stepGapMs,omitempty"`
Steps int `json:"steps,omitempty"`
Aside []string `json:"aside,omitempty"`
}
type journalTook struct {
CallID string `json:"callId"`
DurationMS int64 `json:"durationMs"`
}
// journalFlight is ONE REQUEST'S LIFE, written at its two ends: once when it
// goes out and once when it comes back, whichever way it came back.
//
// ── THE HOLE IT FILLS ───────────────────────────────────────────────────────
//
// Task 5 of conversation de9eabcb10cc1e45, 2026-09-11. The reading that sized
// the work ran for 219 seconds, wrote its first token at 11.5 and 4,465 tokens
// of thought after that — and the node's own journal held NOTHING about it
// until a `call` line landed at the end. The request that had been out for
// three and a half minutes was reconstructed from the provider's own log, on
// another machine, by matching timestamps. A call line is the bill, written
// when there is a bill; a request that is cancelled writes none at all
// (auxiliary.go), and none of them says when anything began.
//
// So a request made on a node's behalf writes this pair (task_calltrail.go),
// and the journal says what the node was waiting on for as long as it waited:
// a start with no end is a request still out — or one a killed process never
// saw back, which is the same news [taskBeat] carries by its age.
//
// IT IS EVIDENCE AND NEVER SPEND, for [journalCall]'s reason: the money is on
// the call line beside it, and a replay drops this line as it drops that one.
// The counts are the stream's own running estimate, and that is why they are
// named for what they are rather than for a bill.
//
// Phase is internal/provider's word for the end being written — `started` or
// `ended` — and End is how an ended request ended ([provider.CallEnd]). The
// figures are carried on the end alone, as MILLISECONDS FROM THE START, so the