]> git.giacomolli.uk - onechat/commitdiff
Send the end of the view in one part with the task
authorAnderson A. Giacomolli <agiacomolli@gmail.com>
Fri, 6 Nov 2026 20:04:29 +0000 (17:04 -0300)
committerAnderson A. Giacomolli <agiacomolli@gmail.com>
Fri, 6 Nov 2026 20:04:29 +0000 (17:04 -0300)
The first try of a compaction sends the opening tag and each whole block
of four view lines as parts of their own, and then one last part with
the rest of the lines, the closing tag, and the task. The note of a
retry is the part after that. Joined, the parts are the same bytes as
before.

A pair check showed that a compaction whose view shares whole blocks
with an earlier one, but has its own rest part, reads nothing from the
cache, and that an exact repeat does. A rule that fits all results so
far: the ChatGPT backend stores a cache entry for a request without its
last part, and a later request reads it only if it begins with that
whole entry. With the end of the view in the last part, the entry of a
compaction ends at its last whole block, and a later compaction with a
longer view begins with it.

Issue: 8c5fd2a

compactor.go
compactor_test.go

index fe8a00ffd8c3df5038117400cf0b8de6f7b9a437..11497b8ddd6aeb919a75cf37c7ae08e8f2c25c59 100644 (file)
@@ -24,9 +24,10 @@ var ruler = strings.Repeat("-", nodeBytes)
 // Compactor builds the nodes of one agent with a model. A compaction is a call
 // like a turn, with the same system prompt and tools, so it reads them from the
 // prompt cache of the turns. The tools are never called. After them comes one
 // Compactor builds the nodes of one agent with a model. A compaction is a call
 // like a turn, with the same system prompt and tools, so it reads them from the
 // prompt cache of the turns. The tools are never called. After them comes one
-// user message with the compaction view up to the node and the task. The view
-// goes in parts of viewBlock lines, as viewParts gives them, so compactions
-// whose views end at different lines still send the same first parts.
+// user message with the compaction view up to the node and the task, in the
+// parts that compactionParts gives. Only the last part holds the end of the
+// view, so compactions whose views end at different lines begin with the same
+// parts up to their last shared block.
 //
 // When the line is over nodeBytes, the next try sends the same view and task,
 // plus a note with the size of the newest line and its cut at the limit. A try
 //
 // When the line is over nodeBytes, the next try sends the same view and task,
 // plus a note with the size of the newest line and its cut at the limit. A try
@@ -55,18 +56,18 @@ func newCompactor(client *Client, name string, model ModelConfig) *Compactor {
 
 // Summarize returns the line of the node of task.
 func (c *Compactor) Summarize(ctx context.Context, task Task) (string, error) {
 
 // Summarize returns the line of the node of task.
 func (c *Compactor) Summarize(ctx context.Context, task Task) (string, error) {
-       view := viewParts(task.Context)
        text, err := compactionTask(task)
        if err != nil {
                return "", err
        }
 
        text, err := compactionTask(task)
        if err != nil {
                return "", err
        }
 
+       first := compactionParts(task.Context, text)
        var lines []string
        note := ""
        for len(lines) < compactionTries {
        var lines []string
        note := ""
        for len(lines) < compactionTries {
-               parts := slices.Concat(view, []string{text})
+               parts := first
                if note != "" {
                if note != "" {
-                       parts = append(parts, note)
+                       parts = append(slices.Clip(first), note)
                }
 
                var response Response
                }
 
                var response Response
@@ -103,11 +104,12 @@ func (c *Compactor) Summarize(ctx context.Context, task Task) (string, error) {
        return shortest(lines), nil
 }
 
        return shortest(lines), nil
 }
 
-// viewParts returns the parts of the compaction view context: the opening tag,
-// one part for each whole block of viewBlock lines, and the rest of the lines
-// with the closing tag and a newline. Joined, the parts are context and a
-// newline.
-func viewParts(context string) []string {
+// compactionParts returns the input parts of a first try for the compaction
+// view context and the task text: the opening tag, one part for each whole
+// block of viewBlock lines, and then one part with the rest of the lines, the
+// closing tag, a newline, and the task. Joined, the parts are the view, a
+// newline, and the task.
+func compactionParts(context, task string) []string {
        lines := strings.SplitAfter(strings.TrimPrefix(context, "<chat>\n"), "\n")
        whole := len(lines) - 1 - (len(lines)-1)%viewBlock
        parts := []string{"<chat>\n"}
        lines := strings.SplitAfter(strings.TrimPrefix(context, "<chat>\n"), "\n")
        whole := len(lines) - 1 - (len(lines)-1)%viewBlock
        parts := []string{"<chat>\n"}
@@ -115,7 +117,7 @@ func viewParts(context string) []string {
                parts = append(parts, strings.Join(lines[n:n+viewBlock], ""))
        }
 
                parts = append(parts, strings.Join(lines[n:n+viewBlock], ""))
        }
 
-       return append(parts, strings.Join(lines[whole:], "")+"\n")
+       return append(parts, strings.Join(lines[whole:], "")+"\n"+task)
 }
 
 // compactionTask returns the task of a build, with its input.
 }
 
 // compactionTask returns the task of a build, with its input.
index cc99f330720ec5049895161db5af8fb627c207fe..20862c77e825022e31d8866da08731e1f5a3473a 100644 (file)
@@ -44,34 +44,34 @@ func TestCompactorSendsThePromptToolsAndTask(t *testing.T) {
        require.Equal(t, []string{
                "<chat>\n",
                "0+4|a\n4+1|b\n5+1|c\n6+1|d\n",
        require.Equal(t, []string{
                "<chat>\n",
                "0+4|a\n4+1|b\n5+1|c\n6+1|d\n",
-               "7+1|e\n</chat>\n",
-               "Compaction: compress message 7 into one line of at most 512 bytes\n" +
+               "7+1|e\n</chat>\n" +
+                       "Compaction: compress message 7 into one line of at most 512 bytes\n" +
                        "(about 70 words), the length of this ruler:\n" +
                        strings.Repeat("-", 512) + "\n" +
                        "<input>\nuser: " + strings.Repeat("x", 600) + "\n</input>",
        }, parts)
 }
 
                        "(about 70 words), the length of this ruler:\n" +
                        strings.Repeat("-", 512) + "\n" +
                        "<input>\nuser: " + strings.Repeat("x", 600) + "\n</input>",
        }, parts)
 }
 
-func TestViewPartsSendWholeBlocksOfLines(t *testing.T) {
+func TestCompactionPartsEndWithTheRestAndTheTask(t *testing.T) {
        testCases := []struct {
                name    string
                context string
                want    []string
        }{
                {
        testCases := []struct {
                name    string
                context string
                want    []string
        }{
                {
-                       name:    "empty view has the tags only",
+                       name:    "empty view has the opening tag and the last part",
                        context: "<chat>\n</chat>",
                        context: "<chat>\n</chat>",
-                       want:    []string{"<chat>\n", "</chat>\n"},
+                       want:    []string{"<chat>\n", "</chat>\nTASK"},
                },
                {
                },
                {
-                       name:    "lines short of a block go with the closing tag",
+                       name:    "lines short of a block go into the last part",
                        context: "<chat>\n0+1|a\n1+1|b\n2+1|c\n</chat>",
                        context: "<chat>\n0+1|a\n1+1|b\n2+1|c\n</chat>",
-                       want:    []string{"<chat>\n", "0+1|a\n1+1|b\n2+1|c\n</chat>\n"},
+                       want:    []string{"<chat>\n", "0+1|a\n1+1|b\n2+1|c\n</chat>\nTASK"},
                },
                {
                        name:    "one whole block",
                        context: "<chat>\n0+1|a\n1+1|b\n2+1|c\n3+1|d\n</chat>",
                },
                {
                        name:    "one whole block",
                        context: "<chat>\n0+1|a\n1+1|b\n2+1|c\n3+1|d\n</chat>",
-                       want:    []string{"<chat>\n", "0+1|a\n1+1|b\n2+1|c\n3+1|d\n", "</chat>\n"},
+                       want:    []string{"<chat>\n", "0+1|a\n1+1|b\n2+1|c\n3+1|d\n", "</chat>\nTASK"},
                },
                {
                        name:    "two whole blocks and a rest",
                },
                {
                        name:    "two whole blocks and a rest",
@@ -80,23 +80,24 @@ func TestViewPartsSendWholeBlocksOfLines(t *testing.T) {
                                "<chat>\n",
                                "0+2|a\n2+1|b\n3+1|c\n4+1|d\n",
                                "5+1|e\n6+1|f\n7+1|g\n8+1|h\n",
                                "<chat>\n",
                                "0+2|a\n2+1|b\n3+1|c\n4+1|d\n",
                                "5+1|e\n6+1|f\n7+1|g\n8+1|h\n",
-                               "9+1|i\n</chat>\n",
+                               "9+1|i\n</chat>\nTASK",
                        },
                },
        }
 
        for _, tc := range testCases {
                t.Run(tc.name, func(t *testing.T) {
                        },
                },
        }
 
        for _, tc := range testCases {
                t.Run(tc.name, func(t *testing.T) {
-                       parts := viewParts(tc.context)
+                       parts := compactionParts(tc.context, "TASK")
                        require.Equal(t, tc.want, parts)
                        require.Equal(t, tc.want, parts)
-                       require.Equal(t, tc.context+"\n", strings.Join(parts, ""))
+                       require.Equal(t, tc.context+"\nTASK", strings.Join(parts, ""))
                })
        }
 }
 
                })
        }
 }
 
-func TestViewPartsKeepTheBlocksOfAShorterView(t *testing.T) {
-       shorter := viewParts("<chat>\n0+1|a\n1+1|b\n2+1|c\n3+1|d\n4+1|e\n</chat>")
-       longer := viewParts("<chat>\n0+1|a\n1+1|b\n2+1|c\n3+1|d\n4+1|e\n5+1|f\n6+1|g\n7+1|h\n</chat>")
+func TestCompactionPartsOfALongerViewBeginWithTheBlocksOfAShorterOne(t *testing.T) {
+       shorter := compactionParts("<chat>\n0+1|a\n1+1|b\n2+1|c\n3+1|d\n4+1|e\n</chat>", "task 5")
+       longer := compactionParts("<chat>\n0+1|a\n1+1|b\n2+1|c\n3+1|d\n4+1|e\n5+1|f\n6+1|g\n7+1|h\n</chat>", "task 8")
+       require.Len(t, shorter, 3)
        require.Equal(t, shorter[:2], longer[:2])
        require.NotEqual(t, shorter[2], longer[2])
 }
        require.Equal(t, shorter[:2], longer[:2])
        require.NotEqual(t, shorter[2], longer[2])
 }
@@ -137,18 +138,18 @@ func TestCompactorRetriesALongLineWithTheNewestNote(t *testing.T) {
        }
 
        first := inputParts(t, server.Call(0))
        }
 
        first := inputParts(t, server.Call(0))
-       require.Len(t, first, 3)
+       require.Len(t, first, 2)
        for n, want := range []string{long[0], long[1]} {
                call := server.Call(n + 1)
                parts := inputParts(t, call)
        for n, want := range []string{long[0], long[1]} {
                call := server.Call(n + 1)
                parts := inputParts(t, call)
-               require.Equal(t, first, parts[:3])
-               require.Equal(t, []string{tooLong(want)}, parts[3:])
-               require.Contains(t, parts[3], "was "+strconv.Itoa(len(want))+" bytes,")
-               require.Contains(t, parts[3], "\n"+want[:512]+"| <- LIMIT")
+               require.Equal(t, first, parts[:2])
+               require.Equal(t, []string{tooLong(want)}, parts[2:])
+               require.Contains(t, parts[2], "was "+strconv.Itoa(len(want))+" bytes,")
+               require.Contains(t, parts[2], "\n"+want[:512]+"| <- LIMIT")
 
                note, err := json.Marshal(map[string]string{
                        "type": "input_text",
 
                note, err := json.Marshal(map[string]string{
                        "type": "input_text",
-                       "text": parts[3],
+                       "text": parts[2],
                })
                require.NoError(t, err)
                require.LessOrEqual(t, len(call.raw), len(server.Call(0).raw)+len(",")+len(note))
                })
                require.NoError(t, err)
                require.LessOrEqual(t, len(call.raw), len(server.Call(0).raw)+len(",")+len(note))