Repository navigation
Expand file tree
/
Copy pathassignment.go
More file actions
1214 lines (1144 loc) · 51.2 KB
/
Copy pathassignment.go
File metadata and controls
1214 lines (1144 loc) · 51.2 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
// The assignment: what a running task is working towards RIGHT NOW, which is
// not always what it was handed at admission.
//
// A person walks into a running task's room and says "CSV instead of JSON".
// Until this file existed that sentence reached the worker as talk and nothing
// else: `spec.brief` and `spec.acceptance` were frozen at admission, the
// auditor graded the finished work against the frozen text, and the worker's
// own prompt told it that a direction "does not replace your brief or your
// done-condition". So the harness asked the worker to obey a correction and
// then checked it for having obeyed one. That is the contradiction this file
// removes, and it removes it in the one way that keeps verification meaningful:
// the objective moves only with AUTHORITY, and it moves as a versioned,
// attributed, recorded revision that every later reader sees the same way.
//
// ── WHAT IS FROZEN AND WHAT MOVES ──
//
// The admitted spec never changes. `spec.request` is the person's own words,
// `spec.brief` is the contract the conversation groomed, `spec.acceptance` is
// what the work was originally to be judged by, and all three stay byte for
// byte what they were — so a page reopened tomorrow, a checkpoint written by an
// older build, and the record of what was actually agreed all still read true.
//
// What moves is an OVERLAY on top of them: an effective deliverable, an
// effective acceptance, and a block of revisions carrying the person's verbatim
// words. [taskAssignment.effective] folds the two into one snapshot, and the
// three readers that must never disagree — the worker's document, the check's
// harvest, the auditor's packet — all take that one snapshot.
//
// ── AUTHORITY IS NOT DELEGABLE ──
//
// A revision must cite a DIRECTION: one recorded message, with an id, from
// somebody who may revise this work. Only [directionFromPerson] may, and who
// that is comes from the delivery seam's own label rather than from this file's
// guess — [directionOf] is the single mapping, and every origin but the person's
// own door maps to the reading that grants nothing (mailbox.go). The model's
// coordination into a node (`tasks … say`, [Agent.relayToTask]) is recorded and
// readable and refused as a basis for revision: an agent that could revise the
// work it is graded on would be marking its own homework, and a descendant
// phrasing a request as an instruction must not thereby acquire authority.
//
// A worker cannot mint a direction either: the only doors that record one are
// the person's own ([Agent.SteerTask], [Agent.ContinueTask]). The worker's tool
// (assignment_tool.go) may only APPLY one that already exists, once, and the
// person's verbatim words travel with the revision it writes — so a restatement
// that has drifted from what they actually said is visible to the auditor and
// to the person, rather than being the only account left.
//
// ── DISCUSSION IS NOT DIRECTION ──
//
// Nothing here fires on its own. "Why did you do it this way?" is recorded as a
// direction receipt in `pending` and read by the worker as an ordinary message,
// and the assignment's version does not move: an answer is what that question
// wants. Moving it costs one explicit tool call by a worker that has the words
// in front of it, which is why there is no classifier model call in this file —
// the capable turn already reading the message is the reader.
//
// ── VERSION IS NOT ATTEMPT ──
//
// [taskAssignment.version] counts REVISIONS OF THE GOAL and nothing else. A
// node that is stopped, continued, repaired or run a second time after a
// provider fault is the same assignment at the same version; a node that has
// taken one accepted direction is at version 1 whether that was its first turn
// or its fortieth. The two are separate because the questions are: "what is
// this work for" and "how many times have we tried it" have different answers
// and different readers.
import (
"errors"
"fmt"
"strings"
"time"
)
// directionFrom is WHO SAID IT, and it is the whole of what decides whether a
// line may move the goal.
//
// It is deliberately a fact about the message rather than a permission on the
// caller: the same words travel from a person's room door and from another
// agent's coordination call, and by the time a worker reads them the only thing
// that can still tell them apart is what was written down when they arrived.
type directionFrom uint8
const (
// directionFromPerson is the person at a keyboard: the room's steer, and the
// words sent with a continue. These may be cited to revise the assignment.
directionFromPerson directionFrom = iota + 1
// directionFromAgent is another agent's coordination — this conversation's
// model, or a parent node, talking to a running task. It is delivered and
// recorded and it may never revise: see the authority note above.
directionFromAgent
)
// directionOf is THE ONE PLACE the delivery seam's speaker becomes this file's
// question about authority (mailbox.go's [messageOrigin]). Only the person's own
// door maps to the person; another agent, the runtime, and anything a later
// origin adds map to the reading that grants nothing, because an origin this
// file does not recognise must never be able to move a done-condition.
func directionOf(origin messageOrigin) directionFrom {
if origin == fromPerson {
return directionFromPerson
}
return directionFromAgent
}
func (f directionFrom) String() string {
switch f {
case directionFromPerson:
return "person"
case directionFromAgent:
return "agent"
}
return "unknown"
}
// directionState is where one direction has got to, and the three words are the
// three honest answers a surface or a record can give.
//
// pending is "the node has it, no worker has read it yet" — which is also what
// makes a completion stale (task_ledger.go). read is "a worker's request
// carried it", which a direction reaches whether or not it changed anything,
// because most directions are talk. applied is "it produced a revision", and
// the version it produced is on the receipt.
type directionState string
const (
directionPending directionState = "pending"
directionRead directionState = "read"
directionApplied directionState = "applied"
)
// taskDirection is one line said to a running task, as the node's record keeps
// it: what was said, when, by whom, and what became of it.
type taskDirection struct {
id uint64
words string
// at is WHEN THE PERSON SAID IT, which for a line typed into this task's room
// is when it arrived and for a forwarded one is when they typed it in the
// conversation (task_forward.go). It is the order their corrections are
// weighed in ([taskAssignment.lastSpokenApplied]), so the distinction is not
// cosmetic: the two doors do not deliver at the same speed.
at time.Time
from directionFrom
state directionState
// version is the assignment version this direction produced, and 0 until it
// produces one. A direction that was read and changed nothing keeps 0
// forever, which is the ordinary case.
version uint64
// source is the person's message this line was FORWARDED from
// ([personSourceID], task_forward.go), and empty for every line said into the
// node's own room. It is what makes a repeated call the same instruction
// rather than a second one, and what puts the person's own messages in order
// on this record however they arrived: both are decided in
// [taskAssignment.hear], where the receipt is minted.
source personSourceID
}
// taskAssignment is the node's effective instruction and the record it came
// from. The zero value is a node nobody has said anything to, whose effective
// assignment is exactly the spec it was admitted with.
type taskAssignment struct {
// version is 0 for an unrevised node and counts accepted revisions after
// that. It is the number a landing revalidates and a document quotes.
version uint64
// deliverable and acceptance REPLACE the spec's when they are set; empty
// means the admitted text still stands. The brief is not replaced — the
// revision block below is added to the document instead — because the
// assembled brief also carries what the work ahead of this node reported and
// what a continuation found, and a replacement would silently drop them.
deliverable string
acceptance string
// revisions is the person's own words for each accepted revision, in order,
// with whatever the worker restated about what they change. It is what makes
// a revised assignment auditable: the document and the checker's packet
// carry the direction and the restatement together, so a restatement that
// has wandered from what was actually said can be seen to have wandered.
revisions []assignmentRevision
// directions is every line said to this node, in arrival order, including
// the ones that were only ever talk.
directions []taskDirection
next uint64
}
// assignmentRevision is one accepted move of the goal.
type assignmentRevision struct {
version uint64
directionID uint64
said string
work string
deliverable string
acceptance string
at time.Time
}
// assignmentEdit is what a revision asks for. Every field is optional and an
// empty one leaves that part of the assignment where it was; an edit that asks
// for nothing at all is refused, because a revision that changes nothing is a
// version number with no content behind it.
type assignmentEdit struct {
work string
deliverable string
acceptance string
// checks is the repeatable verification THE NEW GOAL is to be checked by, and
// it is the one field here whose ABSENCE is itself a decision. A revision
// always takes the old goal's executable checks away — a command declared
// about JSON output proves nothing about CSV, and passing it would be a
// verdict nobody earned — so a revision that names none leaves the work judged
// by reading, and one that names its own puts those under contract instead
// (task_checks.go).
checks []string
}
// empty is asked of what a revision CHANGES ABOUT THE GOAL, and `checks` is
// deliberately not part of it: a call carrying only commands has said nothing
// about what the work is for, and minting a version for it would let a worker
// re-arm its own checker without the person having moved anything.
func (e assignmentEdit) empty() bool {
return strings.TrimSpace(e.work) == "" &&
strings.TrimSpace(e.deliverable) == "" &&
strings.TrimSpace(e.acceptance) == ""
}
// assignmentNow is one consistent view of what this task is working towards:
// the version it is at, the document sections as they stand, and the revision
// block if there is one. Worker, check and landing all read this and never the
// fields behind it, so there is no version of events where the work was done
// against one acceptance and judged against another.
type assignmentNow struct {
version uint64
brief string
deliverable string
acceptance string
// revised is the block that says what moved and who moved it, empty on an
// unrevised node.
revised string
}
// The refusals a revision can meet. They are values rather than sentences built
// at the call site because the tool prints them to a model and the tests match
// them: one wording per reason, in one place.
var (
errNoSuchDirection = errors.New("no direction with that id was said to this task")
errDirectionNotAsked = errors.New("that line came from another agent and not from the person, so it cannot change what this task is judged by")
errDirectionSpent = errors.New("that direction has already been applied")
errNothingToRevise = errors.New("a revision has to change the deliverable, the done-condition or the work")
errAssignmentSettled = errors.New("this task has settled and its assignment is a fact now")
errNoAssignmentToMove = errors.New("no task to revise")
errStaleVersion = errors.New("the assignment has moved since the version you named")
errStaleDirection = errors.New("that direction is older than the one already in force")
errPublishing = errors.New("this task's work is being published and its assignment is closed")
)
// hear records one line said to this task and answers its id. Every line is
// recorded, including the ones that will never be anything but talk: the
// receipt is what a worker cites, what a landing counts and what a person's
// page can show, and a message nobody wrote down cannot be any of the three.
//
// ── AND A FORWARDED LINE IS ORDERED AGAINST THE RECORD, NOT MERELY ADDED ──
//
// A line said into the node's own room with nothing to name it by is heard when
// it is said, and that is the end of it. A line that carries an IDENTITY —
// forwarded from the conversation (task_forward.go), or sent from a room under
// the surface's own send number (task_room.go's [Agent.SteerTaskFrom]) — is
// ordered against the record instead, and two things follow from that, both
// decided here because here is where the receipt is minted and the graph's lock
// is held:
//
// - THE SAME MESSAGE IS NOT HEARD TWICE. A repeated call — a retried tool
// call, a batch the model sent again, a surface asking again for a send it
// never heard the answer to — answers the receipt already on the record and
// delivers nothing.
// - AND AN OLDER MESSAGE MAY NOT LAND BEHIND A NEWER ONE. Receipt ids are this
// node's arrival order, and [taskAssignment.revise] reads them as the
// person's own order — so an older correction admitted after a newer one
// would carry the higher id, could be folded in over the newer one, and
// would leave the newer one refused as stale. It is refused instead, and
// nothing is written down.
//
// Neither road is open to a line with no source, and that is not an oversight:
// saying the same sentence into a room twice IS saying it twice, and a surface
// with no way to number its sends has said nothing that would tell the two
// apart.
func (a *taskAssignment) hear(words string, from directionFrom, at time.Time, source spokenSource) (uint64, bool, directionOrder) {
words = strings.TrimSpace(words)
if words == "" {
return 0, false, directionRefused
}
if source.id.live() {
if id, order := a.already(source.id, words); order != directionRefused {
return id, true, order
}
for _, said := range a.directions {
if said.source.after(source.id) {
return said.id, true, directionOutOfOrder
}
}
}
// AND IT IS RECORDED AT THE INSTANT THE PERSON SAID IT. For a line typed into
// this room those are the same instant; for a forwarded one they are not, and
// it is the speaking instant that decides which of their corrections is the
// later one ([taskAssignment.lastSpokenApplied]).
if !source.spoken.IsZero() {
at = source.spoken
}
a.next++
a.directions = append(a.directions, taskDirection{
id: a.next,
words: words,
at: at,
from: from,
state: directionPending,
source: source.id,
})
return a.next, true, directionHeardNow
}
// already is what this record ALREADY HOLDS for one named send, and it is asked
// in two places: here, under the graph's lock, where the receipt is minted, and
// before a node's state is even consulted ([TaskNode.heardBefore]) — because a
// send whose answer was lost has to be answerable after the work it was for has
// finished, which is exactly when the person's window comes back and asks.
//
// THE WORDS ARE COMPARED AND NOT ONLY THE NAME. A name that already carries
// different words is not a repeat of anything: it is a caller reusing an
// identity, and answering it with "already on the record" would tell somebody
// their new sentence had arrived when what is on the record is an older one.
// That is refused instead, out loud.
func (a *taskAssignment) already(source personSourceID, words string) (uint64, directionOrder) {
if !source.live() {
return 0, directionRefused
}
words = strings.TrimSpace(words)
for _, said := range a.directions {
if said.source != source {
continue
}
if said.words != words {
return said.id, directionMismatch
}
return said.id, directionAgain
}
return 0, directionRefused
}
// directionOrder is what one arrival came to once the record was consulted.
type directionOrder uint8
const (
// directionRefused is nothing at all: there was no line to hear.
directionRefused directionOrder = iota
// directionHeardNow is the ordinary answer — written down, receipt minted.
directionHeardNow
// directionAgain is the same message this node already holds, from a forward
// or from a surface asking again about a send it got no answer to.
directionAgain
// directionOutOfOrder is a message older than one already here.
directionOutOfOrder
// directionMismatch is one identity carrying two different sentences.
directionMismatch
)
// forget removes a receipt that was minted for words nobody took. It exists
// because the id has to be known BEFORE the line is delivered — it rides under
// the words, so the worker can cite it — and a delivery can still be refused
// after that. A receipt left behind for words no reader ever got would be a
// direction the landing waits for forever.
func (a *taskAssignment) forget(id uint64) {
kept := a.directions[:0]
for _, said := range a.directions {
if said.id == id && said.state == directionPending {
continue
}
kept = append(kept, said)
}
a.directions = kept
}
// direction answers one receipt by id.
func (a *taskAssignment) direction(id uint64) (taskDirection, bool) {
for _, said := range a.directions {
if said.id == id {
return said, true
}
}
return taskDirection{}, false
}
// markCarried moves exactly the named directions to read. Anything not named is
// left pending, whatever else has happened to the work.
func (a *taskAssignment) markCarried(ids []uint64) {
for _, id := range ids {
for index := range a.directions {
if a.directions[index].id == id && a.directions[index].state == directionPending {
a.directions[index].state = directionRead
}
}
}
}
// pendingFrom is every direction from one speaker that no worker has read. The
// landing asks it about the person: their words arriving while the checker ran
// are the whole reason a finished-looking node may not publish (task_ledger.go).
func (a *taskAssignment) pendingFrom(from directionFrom) []taskDirection {
var waiting []taskDirection
for _, said := range a.directions {
if said.state == directionPending && said.from == from {
waiting = append(waiting, said)
}
}
return waiting
}
// revise applies one direction to the assignment and answers the new version.
//
// The four refusals are the whole of the authority law, and they are checked
// here rather than at the tool so that every door — the worker's call today,
// anything that reaches for this seam later — meets the same rules.
// expected is the version the caller believes the assignment is at, and a
// mismatch is refused rather than merged: two directions applied out of order
// would leave the later one's done-condition underneath the earlier one's, which
// is the person's most recent correction being quietly discarded.
func (a *taskAssignment) revise(id uint64, expected uint64, edit assignmentEdit, at time.Time) (uint64, error) {
said, found := a.direction(id)
if !found {
return 0, errNoSuchDirection
}
if said.from != directionFromPerson {
return 0, errDirectionNotAsked
}
if said.state == directionApplied {
return 0, errDirectionSpent
}
// AND NEVER OUT OF ORDER. A direction the person said before the last one
// applied cannot move the goal, whatever version it names: their later
// correction is already in force, and folding an earlier one in on top of it
// would put back a condition they have moved on from. The version check below
// is about concurrent readings; this is about which sentence they said last.
if latest, applied := a.lastSpokenApplied(); applied && said.spokenBefore(latest) {
return 0, fmt.Errorf("%w: direction %d came before %d, which is already in force", errStaleDirection, said.id, latest.id)
}
if expected != a.version {
return 0, fmt.Errorf("%w: it is at revision %d and you named %d", errStaleVersion, a.version, expected)
}
if edit.empty() {
return 0, errNothingToRevise
}
a.version++
revision := assignmentRevision{
version: a.version,
directionID: id,
said: said.words,
work: strings.TrimSpace(edit.work),
deliverable: strings.TrimSpace(edit.deliverable),
acceptance: strings.TrimSpace(edit.acceptance),
at: at,
}
if revision.deliverable != "" {
a.deliverable = revision.deliverable
}
if revision.acceptance != "" {
a.acceptance = revision.acceptance
}
a.revisions = append(a.revisions, revision)
for index := range a.directions {
if a.directions[index].id != id {
continue
}
a.directions[index].state = directionApplied
a.directions[index].version = a.version
}
return a.version, nil
}
// lastSpokenApplied is the direction that has moved this assignment and was
// SAID LAST OF THOSE THAT DID, and false when none has.
//
// ── WHY THIS IS NOT SIMPLY THE HIGHEST RECEIPT ID ──
//
// Receipt ids are this node's ARRIVAL order, and arrival order was the person's
// order only while there was one door into a task. There are two now. A message
// they typed in the main conversation reaches a node when the model gets round to
// forwarding it (task_forward.go), and a line typed into the task's own room
// reaches it the moment they press enter — so the person can say A here, walk
// into the room and say B, and have A arrive second and take the higher id.
// Ordered by id, A would then be the "later" instruction: a worker could fold it
// in over B, putting back the goal they had just moved away from, and B could
// never be applied afterwards because it would read as the older one.
//
// So the order is WHEN THEY SAID IT ([taskDirection.at], which for a forwarded
// line is when they typed it rather than when it was forwarded), with the
// receipt id as the tiebreak for two lines heard in the same instant and for
// records restored from disk, which have no monotonic clock left in them.
func (a *taskAssignment) lastSpokenApplied() (taskDirection, bool) {
var latest taskDirection
found := false
for _, revision := range a.revisions {
said, ok := a.direction(revision.directionID)
if !ok {
continue
}
if !found || latest.spokenBefore(said) {
latest, found = said, true
}
}
return latest, found
}
// spokenBefore says this line was said before the other one. An instant nobody
// recorded — a direction from a build that did not stamp one — falls back to the
// receipt order, which is what this comparison was before there were two doors.
func (d taskDirection) spokenBefore(other taskDirection) bool {
if d.at.IsZero() || other.at.IsZero() || d.at.Equal(other.at) {
return d.id < other.id
}
return d.at.Before(other.at)
}
// effective folds the overlay onto the admitted spec. The brief keeps whatever
// the node assembled — the groomed contract, its prerequisites' reports, a
// continuation's finding — and the revision block is added under it.
func (a *taskAssignment) effective(brief, deliverable, acceptance string) assignmentNow {
now := assignmentNow{
version: a.version,
brief: brief,
deliverable: deliverable,
acceptance: acceptance,
revised: a.revisionBlock(),
}
if a.deliverable != "" {
now.deliverable = a.deliverable
}
if a.acceptance != "" {
now.acceptance = a.acceptance
}
if now.revised != "" {
now.brief = strings.TrimSpace(strings.TrimRight(now.brief, "\n") + "\n\n" + now.revised)
}
return now
}
// assignmentRevisedLead opens the block that says the goal moved. It leads with
// the person's own words because those are the authority; the restatement under
// them is the worker's reading of what they change, and it is labelled as one.
const assignmentRevisedLead = "REVISED WHILE THIS WORK RAN, BY THE PERSON. These words came AFTER the message quoted at the top of this document: where the two differ, these are the later ones and these are what this work is now for. What they did not change stands exactly as it was, and DONE WHEN below is the revised condition — the earlier one is history and nothing is owed for satisfying it."
// revisionBlock renders every accepted revision in order. An unrevised
// assignment renders nothing at all, which is the emptiness law this package
// applies to every other section of a task's document.
func (a *taskAssignment) revisionBlock() string {
if len(a.revisions) == 0 {
return ""
}
var out strings.Builder
out.WriteString(assignmentRevisedLead + "\n")
for _, revision := range a.revisions {
fmt.Fprintf(&out, "\nRevision %d — they said: %s\n", revision.version, revision.said)
if revision.work != "" {
out.WriteString("What it changes about the work: " + revision.work + "\n")
}
if revision.deliverable != "" {
out.WriteString("What must exist now: " + revision.deliverable + "\n")
}
if revision.acceptance != "" {
out.WriteString("Done now means: " + revision.acceptance + "\n")
}
}
return strings.TrimRight(out.String(), "\n")
}
// revisedSaid is every word of the person's that has actually moved this
// assignment, in order, and empty on an unrevised node. It is what the check
// obligation text reads as "the person's message" once there is a later one than
// the one the work was admitted from (wakecause.go).
func (a *taskAssignment) revisedSaid() string {
if len(a.revisions) == 0 {
return ""
}
said := make([]string, 0, len(a.revisions))
for _, revision := range a.revisions {
said = append(said, revision.said)
}
return strings.Join(said, "\n")
}
// directionBlock renders directions for a document that has to hand them over
// without pretending they revised anything: the reopen's finding, which carries
// words the last attempt never got to read.
func directionBlock(said []taskDirection) string {
if len(said) == 0 {
return ""
}
var out strings.Builder
out.WriteString(directionCarriedLead + "\n")
for _, one := range said {
// WHO SAID IT TRAVELS WITH IT. The person's words are given plainly,
// because that is what they are; another agent's coordination is named, so
// that a worker reading both cannot take the second for the first when it
// decides which of them may move the done-condition.
if one.from == directionFromPerson {
fmt.Fprintf(&out, "\nThe person said (direction %d): %s\n", one.id, one.words)
continue
}
fmt.Fprintf(&out, "\nAnother agent said (direction %d, coordination and not the person's authority): %s\n",
one.id, one.words)
}
return strings.TrimRight(out.String(), "\n")
}
// directionCarriedLead opens the unread words a continuation hands to the next
// attempt. It says plainly that they have not been applied, because the next
// worker is the one that decides whether they are a correction to the goal or a
// remark about the work.
const directionCarriedLead = "Said while the last attempt was finishing, and read by no worker — this has NOT been applied to the assignment"
// ── the node's own doors ────────────────────────────────────────────────────
// heardDirection records one line said to this node and answers its receipt id
// and WHICH SIDE OF THE PUBLICATION BOUNDARY it landed on.
//
// The graph's lock is the one every other field of the node is written under,
// and it is also the linearization point: a landing claims the boundary under
// this same lock ([TaskNode.claimPublication]), so a direction is either
// admitted before the claim — and then the claim fails and the work does not
// publish — or after it, and then it belongs to the round after this one. There
// is no third ordering and no window between them.
func (n *TaskNode) heardDirection(words string, from directionFrom, source spokenSource) directionHeard {
if n == nil || n.graph == nil {
return directionHeard{}
}
n.graph.mu.Lock()
defer n.graph.mu.Unlock()
return n.heardDirectionLocked(words, from, source)
}
// heardDirectionLocked is the same admission with the graph's lock ALREADY HELD.
// It exists for the one caller that has to admit and write the checkpoint under
// one hold of it, so that no reader of this record sees a direction whose write
// is still deciding (task_store.go's [TaskGraph.admitWritten]).
func (n *TaskNode) heardDirectionLocked(words string, from directionFrom, source spokenSource) directionHeard {
if n == nil {
return directionHeard{}
}
id, kept, order := n.assignment.hear(words, from, time.Now(), source)
return directionHeard{id: id, inTime: kept && !n.publishing, order: order}
}
// directionHeard is what one line's arrival came to: the receipt, which side of
// the publication boundary it landed on, and how the record answered it.
type directionHeard struct {
id uint64
inTime bool
order directionOrder
}
// fresh says these words were written down now, which is the only answer that
// owes a delivery.
func (h directionHeard) fresh() bool { return h.order == directionHeardNow }
// forgetDirection drops a receipt whose words were never delivered.
func (n *TaskNode) forgetDirection(id uint64) {
if n == nil || n.graph == nil || id == 0 {
return
}
n.graph.mu.Lock()
defer n.graph.mu.Unlock()
n.assignment.forget(id)
}
// forgetDirectionLocked is the same with the graph's lock already held, for
// [heardDirectionLocked]'s reason.
func (n *TaskNode) forgetDirectionLocked(id uint64) {
if n == nil || id == 0 {
return
}
n.assignment.forget(id)
}
// directionReceiptLine is the HARNESS'S OWN LINE under the person's words, and
// it is the one thing this file adds to a message that is otherwise delivered
// exactly as it was typed (task_room.go's no-decoration law).
//
// It is here because a direction cannot become a revision without an id to cite,
// and the id is a fact about the delivery rather than about what was said. It
// goes UNDER the words and says who it is from, so the person's sentence still
// leads and still reads as somebody talking; a frame around them would teach the
// worker to read a person as a system event, which is the thing that law is for.
func directionReceiptLine(id uint64) string {
return fmt.Sprintf("[the harness: that was the person, direction %d.%s]", id, directionReceiptTail(id))
}
// forwardedReceiptLine is the same line for words the person typed in the MAIN
// CONVERSATION about this task rather than into this room (task_forward.go).
//
// The provenance is the whole of the difference, and it is said rather than
// implied: the words are theirs and unedited, so they carry their authority, and
// the ADDRESS was chosen by the conversation's model, so a line that plainly
// belongs to other work is a mis-delivery to report rather than an instruction
// to carry out. Nothing here asks the worker to weigh whether they meant it —
// what it needs is the fact that they were not standing in this room.
func forwardedReceiptLine(id uint64) string {
return fmt.Sprintf("[the harness: that was the person, said in the main conversation about this task and forwarded here word for word — their direction %d, not the conversation's own line.%s "+
"They were not addressing this room, so if what they say plainly belongs to other work, say so in your report and carry on with the brief.]", id, directionReceiptTail(id))
}
// directionReceiptTail is what a worker DOES with a direction, and it is one
// string because both receipts above ask for exactly the same reading.
func directionReceiptTail(id uint64) string {
return fmt.Sprintf(" If it changes what this work is FOR — a different output, a different target, a requirement added or dropped — fold it in with revise_assignment citing %d. If it is a fact or a question, just use it or answer it.", id)
}
// heardBefore is [taskAssignment.already] asked of a node from outside the
// lock. It is what lets a send be answered when the node is no longer running:
// the record outlives the work, and a window coming back to a correction whose
// answer was lost is entitled to learn that it arrived.
func (n *TaskNode) heardBefore(source spokenSource, words string) (uint64, directionOrder) {
if n == nil || n.graph == nil {
return 0, directionRefused
}
n.graph.mu.Lock()
defer n.graph.mu.Unlock()
return n.assignment.already(source.id, words)
}
// directionsNow copies the receipts out for a reader outside the lock.
func (n *TaskNode) directionsNow() []taskDirection {
if n == nil || n.graph == nil {
return nil
}
n.graph.mu.Lock()
defer n.graph.mu.Unlock()
said := make([]taskDirection, len(n.assignment.directions))
copy(said, n.assignment.directions)
return said
}
// unreadPersonDirections is what the landing revalidates against: the person's
// words that arrived while nobody was reading.
func (n *TaskNode) unreadPersonDirections() []taskDirection {
if n == nil || n.graph == nil {
return nil
}
n.graph.mu.Lock()
defer n.graph.mu.Unlock()
return n.assignment.pendingFrom(directionFromPerson)
}
// markDirectionsCarried is called by a WORKER agent at the drain immediately
// before its next provider request, with the receipt ids that request carries
// (agent.go's [Agent.drainSteering]). That is the only moment a direction stops
// being pending, and the distinction matters: a line accepted after the worker's
// last drain was never in front of the model, and marking it read there would
// let the landing publish over words nobody has read.
func (a *Agent) markDirectionsCarried(ids []uint64) {
if len(ids) == 0 || a.config.tasker == nil || a.config.taskID == 0 {
return
}
node := a.config.tasker.node(a.config.taskID)
if node == nil {
return
}
node.graph.mu.Lock()
node.assignment.markCarried(ids)
node.graph.mu.Unlock()
}
// reviseAssignment applies one of the person's directions and answers the new
// version. A settled node refuses: what a landed task was judged against is a
// fact about what happened, and a revision after the fact would rewrite it.
func (n *TaskNode) reviseAssignment(id, expected uint64, edit assignmentEdit) (uint64, error) {
if n == nil || n.graph == nil {
return 0, errNoAssignmentToMove
}
n.graph.mu.Lock()
defer n.graph.mu.Unlock()
if n.state.settled() {
return 0, errAssignmentSettled
}
// AND NOTHING MOVES ONCE A LANDING HAS TAKEN THE BOUNDARY. From that instant
// the work being published is fixed, and a revision squeezed in behind it
// would leave a merge already going out labelled with a condition it was
// never checked against.
if n.publishing {
return 0, errPublishing
}
version, err := n.assignment.revise(id, expected, edit, time.Now())
if err != nil {
return 0, err
}
// AND THE VERIFICATION MOVES IN THE SAME HOLD THE VERSION MOVED IN.
//
// A check is an assertion about a particular goal. The instant this node's
// version says CSV, a command declared about JSON is either a question nobody
// asked or — worse — a pass nobody earned, and the same is true of the
// family's checks this node was carrying for parts it handed out under the old
// goal. So they go with the version, here, rather than through a second call
// that took the lock again: a reader catching the gap between the two would
// see the old goal's commands wearing the new goal's number, which is exactly
// the verdict this is written to prevent (task_run.go's
// [TaskNode.reviseChecksLocked]).
n.reviseChecksLocked(version, edit.checks)
return version, nil
}
// assignmentNow is the snapshot every reader of "what is this work for" takes.
func (n *TaskNode) assignmentNow() assignmentNow {
if n == nil || n.graph == nil {
return assignmentNow{}
}
n.graph.mu.Lock()
defer n.graph.mu.Unlock()
return n.assignmentLocked()
}
// assignmentLocked is the same fold for callers already holding the graph.
func (n *TaskNode) assignmentLocked() assignmentNow {
return n.assignment.effective(n.brief, n.spec.deliverable, n.spec.acceptance)
}
// revisionNote is what a reader who is JUDGING the work is told about a goal
// that moved: which revision governs, the person's own words for each one, and
// that the earlier done-condition no longer counts. It is empty on an unrevised
// node, which is nearly all of them.
func (n *TaskNode) revisionNote() string {
if n == nil || n.graph == nil {
return ""
}
n.graph.mu.Lock()
defer n.graph.mu.Unlock()
if n.assignment.version == 0 {
return ""
}
var out strings.Builder
fmt.Fprintf(&out, "THE PERSON CHANGED THIS WORK WHILE IT RAN. The acceptance above is revision %d and it is the ONE you judge against; what this task was first given no longer governs, and work that still satisfies the earlier condition is not owed for it.\n",
n.assignment.version)
out.WriteString("Their own words, in order:\n")
for _, revision := range n.assignment.revisions {
fmt.Fprintf(&out, " %d. %s\n", revision.version, revision.said)
}
out.WriteString("If the acceptance above does not say what those words say, judge that as the work failing to carry out the person's direction.")
return out.String()
}
// checkAt records the assignment version the work about to be checked was done
// against, which is what the publication boundary compares.
func (n *TaskNode) checkAt(version uint64) {
if n == nil || n.graph == nil {
return
}
n.graph.mu.Lock()
n.checkedAt = version
n.graph.mu.Unlock()
}
// assignmentVersion is the number a landing compares and a record keeps.
func (n *TaskNode) assignmentVersion() uint64 {
if n == nil || n.graph == nil {
return 0
}
n.graph.mu.Lock()
defer n.graph.mu.Unlock()
return n.assignment.version
}
// ── the record ──────────────────────────────────────────────────────────────
// recordedAssignment copies the overlay onto the checkpoint, and answers nil
// for a node nobody has said anything to — which keeps the row a session writes
// byte-identical to what it wrote before this existed.
func recordedAssignment(a taskAssignment) *assignmentRecord {
if a.version == 0 && len(a.directions) == 0 {
return nil
}
record := &assignmentRecord{
Version: a.version,
Deliverable: a.deliverable,
Acceptance: a.acceptance,
Next: a.next,
}
for _, revision := range a.revisions {
record.Revisions = append(record.Revisions, revisionRecord{
Version: revision.version,
Direction: revision.directionID,
Said: revision.said,
Work: revision.work,
Deliverable: revision.deliverable,
Acceptance: revision.acceptance,
At: revision.at,
})
}
for _, said := range a.directions {
record.Directions = append(record.Directions, directionRecord{
ID: said.id,
Words: said.words,
At: said.at,
From: said.from.String(),
State: string(said.state),
Version: said.version,
Source: recordedSource(said.source),
})
}
return record
}
// restoredAssignment reads the overlay back. A record from a build that did not
// write one restores the zero value, and the node's effective assignment is the
// admitted spec — which is what it was.
func restoredAssignment(record *assignmentRecord) taskAssignment {
if record == nil {
return taskAssignment{}
}
restored := taskAssignment{
version: record.Version,
deliverable: record.Deliverable,
acceptance: record.Acceptance,
next: record.Next,
}
for _, revision := range record.Revisions {
restored.revisions = append(restored.revisions, assignmentRevision{
version: revision.Version,
directionID: revision.Direction,
said: revision.Said,
work: revision.Work,
deliverable: revision.Deliverable,
acceptance: revision.Acceptance,
at: revision.At,
})
}
for _, said := range record.Directions {
restored.directions = append(restored.directions, taskDirection{
id: said.ID,
words: said.Words,
at: said.At,
from: directionFromWord(said.From),
state: directionStateWord(said.State),
version: said.Version,
source: restoredSource(said.Source),
})
// A record written by a build with a different idea of the counter still
// leaves this one able to mint ids nothing else holds.
if said.ID > restored.next {
restored.next = said.ID
}
}
return restored
}
// recordedSource is the checkpoint's copy of a forwarded line's identity, and
// nothing at all for a line said into the node's own room — which keeps the row
// a session writes byte-identical to what it wrote before forwarding existed.
func recordedSource(id personSourceID) *sourceRecord {
if !id.live() {
return nil
}
return &sourceRecord{Scope: id.scope, Seq: id.seq}
}
// restoredSource reads one back. A record with half of it missing names no
// message and is restored as none: an identity that cannot be compared must not
// be able to suppress a genuinely new direction as a repeat.
func restoredSource(record *sourceRecord) personSourceID {
if record == nil {
return personSourceID{}
}
id := personSourceID{scope: record.Scope, seq: record.Seq}
if !id.live() {
return personSourceID{}
}
return id
}
// directionFromWord reads a speaker back. Anything a reader cannot recognise —
// a word from a newer build, a corrupted row — is taken as another agent's,
// which is the reading that grants no authority.
func directionFromWord(word string) directionFrom {
if word == directionFromPerson.String() {
return directionFromPerson
}
return directionFromAgent
}
// directionStateWord reads a state back, defaulting to read: an unrecognisable
// state must not be able to hold a landing forever.
func directionStateWord(word string) directionState {
switch directionState(word) {
case directionPending:
return directionPending
case directionApplied:
return directionApplied
}
return directionRead
}
// ── the publication boundary ────────────────────────────────────────────────
//
// PUBLISHING IS THIS HARNESS'S ONE UNDOABLE STEP — merging the work onto the
// person's branch, closing `done`, telling the parent. It is not the only thing
// a task does that cannot be taken back: a task runs commands, and what those
// did to the world is beyond anything here. What this boundary governs is the
// publication, and the question "has the assignment moved?" has to be asked