fix(tui): a runtime continuation is not the person's message

The terminal counted and drew a host-authored inbox entry as the person's
own pending line, so `d d` on it reached DELETE /inbox/items/<id> and the
kernel dropped the runtime's continuation. The desktop has always filtered
these out; the terminal never decoded the field it filters on.

The mirror now carries origin, readonly and capacity, and a declared-subset
wire-parity check holds it (and the page's copy) to the fields a decision
reads, so dropping one is a `make check` failure instead of a silent zero.

A host continuation is out of the pending rows and out of the count, except
where it is blocked or uncertain and needs a decision only the person can
make; there it is drawn marked as the runtime's. A read-only snapshot offers
no mutation key and says why.
This commit is contained in:
Will committed 2026-10-23 11:59:16 +08:00
1 parent add2e45c05
commit 216ea245fd
8 files changed
+660 -36

No files matched your search

+113
View File
@@ -38,6 +38,10 @@ const (
tsSurfaceFile = "desktop/frontend-next/src/ui/surface/spec.ts"
)
// tuiInboxFile is where the terminal re-declares the kernel's inbox wire types
// by hand. One file holds both the listing and its entries.
const tuiInboxFile = "internal/frontend/tui/client_inbox.go"
// mirroredWireTypes are the Go types the desktop keeps a second, hand-written
// copy of: the graph, whose consumers all fold the same deltas, and the sandbox
// editor, which draws a security posture a field it cannot read would get
@@ -324,6 +328,40 @@ var mirroredWireTypes = []wireMirror{
{"internal/frontend/serve/appearance.go", "wallpaperView", tsLookFile, "Wallpaper"},
}
// partialWireMirrors are hand-written copies honestly smaller than the kernel's
// struct: a terminal draws one row per entry and the page's outbox shows the
// person's lines. Each pair names the fields it must carry and is compared both
// ways over those only; a subset's failure mode is a field quietly dropped, and
// the full-mirror list above is not relaxed for it.
var partialWireMirrors = []partialWireMirror{
// What a surface reads to decide whose a queued line is, whether it may act
// on it, and whether the queue can be changed at all.
{"internal/state/sessioninbox/types.go", "InboxItemMeta", tuiInboxFile, "InboxItem",
[]string{"id", "intent", "state", "preview", "origin", "images", "blockCode", "blockReason"}},
{"internal/state/sessioninbox/types.go", "InboxSnapshot", tuiInboxFile, "InboxSnapshot",
[]string{"revision", "paused", "readonly", "recovered", "recoveredCount", "unrecovered", "items", "capacity"}},
{"internal/state/sessioninbox/types.go", "Capacity", tuiInboxFile, "InboxCapacity",
[]string{"items", "maxItems", "bytes", "maxBytes", "maxItemBytes"}},
// The page reads the same two types through its own hand-written copy. It
// omits the manifest fields neither surface draws, and must keep the ones
// its outbox decides from.
{"internal/state/sessioninbox/types.go", "InboxItemMeta", tsPortFile, "QueueItem",
[]string{"id", "intent", "origin", "state", "preview", "createdAt", "blockReason", "blockCode", "refs", "images"}},
{"internal/state/sessioninbox/types.go", "InboxSnapshot", tsPortFile, "Queue",
[]string{"revision", "paused", "readonly", "recoveredCount", "items", "capacity"}},
{"internal/state/sessioninbox/types.go", "Capacity", tsPortFile, "QueueCapacity",
[]string{"items", "maxItems", "bytes", "maxBytes"}},
}
// partialWireMirror is one declared subset: readerFile's readerType must carry
// every name in fields, the writer must still send each, and the reader must
// choose no name the writer never sends. What is left out is the point.
type partialWireMirror struct {
writerFile, writerType string
readerFile, readerType string
fields []string
}
type wireMirror struct {
goFile, goType, tsFile, tsType string
}
@@ -369,6 +407,16 @@ func (w *wireScan) observe(src *sourceFile) {
}
}
}
for _, m := range partialWireMirrors {
for _, side := range [][2]string{{m.writerFile, m.writerType}, {m.readerFile, m.readerType}} {
if side[0] != src.rel {
continue
}
if names, ok := wireFieldNames(src.file, side[1]); ok {
w.fields[side[0]+"."+side[1]] = names
}
}
}
}
// findings compares each declared pair both ways. Either direction is a defect:
@@ -386,7 +434,21 @@ func (w *wireScan) findings(root string) []Finding {
}
bodies[m.tsFile] = string(raw)
}
for _, m := range partialWireMirrors {
if !strings.HasSuffix(m.readerFile, ".ts") {
continue
}
if _, read := bodies[m.readerFile]; read {
continue
}
raw, err := os.ReadFile(filepath.Join(root, m.readerFile))
if err != nil {
return nil
}
bodies[m.readerFile] = string(raw)
}
out := append(wireParityFindings(mirroredWireTypes, w.fields, bodies), readParityFindings(readMirrors, w.fields)...)
out = append(out, partialWireFindings(partialWireMirrors, w.fields, bodies)...)
return append(out, wireConstantFindings(root, mirroredWireConstants)...)
}
@@ -416,6 +478,57 @@ func readParityFindings(mirrors []readMirror, goFields map[string][]string) []Fi
return out
}
// partialWireFindings checks a declared subset in the three directions that
// matter: the writer must still send each named field, the reader must still
// carry it, and the reader may not read a name nothing sends. What the writer
// sends and a subset ignores is the reason a subset exists.
func partialWireFindings(mirrors []partialWireMirror, goFields map[string][]string, bodies map[string]string) []Finding {
var out []Finding
for _, m := range mirrors {
writer, sends := goFields[m.writerFile+"."+m.writerType]
if !sends {
out = append(out, Finding{m.writerFile, 1, ruleWireParity,
fmt.Sprintf("%s is declared a mirrored wire type and is not a struct here", m.writerType), 1})
continue
}
reader, line, declares := partialReaderFields(m, goFields, bodies)
if !declares {
out = append(out, Finding{m.readerFile, 1, ruleWireParity,
fmt.Sprintf("%s is declared a partial mirror of %s.%s and is not declared here", m.readerType, m.writerFile, m.writerType), 1})
continue
}
for _, name := range m.fields {
if !slices.Contains(writer, name) {
out = append(out, Finding{m.writerFile, 1, ruleWireParity,
fmt.Sprintf("%s no longer sends %q, a field %s decides from", m.writerType, name, m.readerType), 1})
}
if !slices.Contains(reader, name) {
out = append(out, Finding{m.readerFile, line, ruleWireParity,
fmt.Sprintf("%s no longer declares %q, which %s.%s sends and this copy decides from", m.readerType, name, m.writerFile, m.writerType), 1})
}
}
for _, name := range missingFrom(reader, writer) {
if slices.Contains(m.fields, name) {
// Already named as the writer dropping a declared field.
continue
}
out = append(out, Finding{m.readerFile, line, ruleWireParity,
fmt.Sprintf("%s reads %q and %s.%s never sends it", m.readerType, name, m.writerFile, m.writerType), 1})
}
}
return out
}
// partialReaderFields reads one side of a pair from whichever grammar declares
// it: the terminal's copy is Go, the page's is TypeScript.
func partialReaderFields(m partialWireMirror, goFields map[string][]string, bodies map[string]string) ([]string, int, bool) {
if strings.HasSuffix(m.readerFile, ".ts") {
return tsInterfaceFields(bodies[m.readerFile], m.readerType)
}
names, ok := goFields[m.readerFile+"."+m.readerType]
return names, 1, ok
}
// wireParityFindings is the comparison itself, so a fixture can carry the one
// pair a case is about rather than the tree's real ones.
func wireParityFindings(mirrors []wireMirror, goFields map[string][]string, bodies map[string]string) []Finding {
+104
View File
@@ -190,3 +190,107 @@ func TestWireParitySkipsAnEmbeddedTypeFromAnotherPackage(t *testing.T) {
t.Fatalf("names = %v ok=%v, want only the struct's own field", names, ok)
}
}
const partialWriter = `package p
type Item struct {
ID string ` + "`json:\"id\"`" + `
Origin string ` + "`json:\"origin,omitempty\"`" + `
Preview string ` + "`json:\"preview\"`" + `
}
`
const partialReader = `package p
type ItemView struct {
ID string ` + "`json:\"id\"`" + `
Origin string ` + "`json:\"origin\"`" + `
Preview string ` + "`json:\"preview\"`" + `
}
`
var partialPair = []partialWireMirror{
{"w.go", "Item", "r.go", "ItemView", []string{"id", "origin", "preview"}},
}
func partialFindingsFor(t *testing.T, writer, reader string) []Finding {
t.Helper()
w, r := parseBytes("w.go", []byte(writer)), parseBytes("r.go", []byte(reader))
wn, ok := wireFieldNames(w.file, "Item")
if !ok {
t.Fatal("the fixture's writer does not resolve")
}
rn, ok := wireFieldNames(r.file, "ItemView")
if !ok {
t.Fatal("the fixture's reader does not resolve")
}
return partialWireFindings(partialPair, map[string][]string{
"w.go.Item": wn,
"r.go.ItemView": rn,
}, nil)
}
// A subset is allowed to ignore what the writer sends; the names it declares
// are the whole contract.
func TestPartialWireParityAcceptsACopyThatCarriesItsDeclaredFields(t *testing.T) {
if got := partialFindingsFor(t, partialWriter, partialReader); len(got) != 0 {
t.Fatalf("a faithful subset must produce no findings, got %+v", got)
}
}
// The defect the sessioninbox guard exists for: the copy drops a field a
// decision reads, and the empty value looks like a real answer.
func TestPartialWireParityCatchesAFieldTheCopyDropped(t *testing.T) {
reader := strings.Replace(partialReader, "\tOrigin string `json:\"origin\"`\n", "", 1)
got := partialFindingsFor(t, partialWriter, reader)
if len(got) != 1 || !strings.Contains(got[0].Msg, `no longer declares "origin"`) {
t.Fatalf("a dropped decision field was not reported: %+v", got)
}
}
// A rename on the reading side reads as zero as surely as a dropped field.
func TestPartialWireParityCatchesANameTheCopyInvented(t *testing.T) {
reader := strings.Replace(partialReader, `json:"origin"`, `json:"orgin"`, 1)
got := partialFindingsFor(t, partialWriter, reader)
if len(got) != 2 {
t.Fatalf("findings = %+v, want the dropped name and the invented one", got)
}
var sawDropped, sawInvented bool
for _, f := range got {
sawDropped = sawDropped || strings.Contains(f.Msg, `no longer declares "origin"`)
sawInvented = sawInvented || strings.Contains(f.Msg, `reads "orgin"`)
}
if !sawDropped || !sawInvented {
t.Fatalf("findings = %+v, want both directions named", got)
}
}
// The writer's side is checked too: a field the kernel stops sending is a
// decision the copy can no longer make.
func TestPartialWireParityCatchesAFieldTheWriterStoppedSending(t *testing.T) {
writer := strings.Replace(partialWriter, "\tOrigin string `json:\"origin,omitempty\"`\n", "", 1)
got := partialFindingsFor(t, writer, partialReader)
if len(got) != 1 || !strings.Contains(got[0].Msg, `no longer sends "origin"`) {
t.Fatalf("a field the writer stopped sending was not reported: %+v", got)
}
}
// The page's copy is TypeScript and read from its interface, not its wording.
func TestPartialWireParityReadsThePagesInterface(t *testing.T) {
const ts = "export interface ItemView {\n id: string;\n origin?: \"host\";\n preview: string;\n}\n"
mirrors := []partialWireMirror{{"w.go", "Item", tsWireFile, "ItemView", []string{"id", "origin", "preview"}}}
names, ok := wireFieldNames(parseBytes("w.go", []byte(partialWriter)).file, "Item")
if !ok {
t.Fatal("the fixture's writer does not resolve")
}
if got := partialWireFindings(mirrors, map[string][]string{"w.go.Item": names},
map[string]string{tsWireFile: ts}); len(got) != 0 {
t.Fatalf("a faithful TS subset must produce no findings, got %+v", got)
}
dropped := strings.Replace(ts, " origin?: \"host\";\n", "", 1)
got := partialWireFindings(mirrors, map[string][]string{"w.go.Item": names},
map[string]string{tsWireFile: dropped})
if len(got) != 1 || !strings.Contains(got[0].Msg, `no longer declares "origin"`) {
t.Fatalf("a dropped field in the page's copy was not reported: %+v", got)
}
}