From: Anderson A. Giacomolli Date: Fri, 6 Nov 2026 20:04:29 +0000 (-0300) Subject: Send the end of the view in one part with the task X-Git-Url: http://git.giacomolli.uk/onechat/commitdiff_plain/4b9b226?ds=inline;hp=56129a5fcbcbe51a7ac7d6b009d4551ed9f71109 Send the end of the view in one part with the task 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 --- diff --git a/compactor.go b/compactor.go index fe8a00f..11497b8 100644 --- a/compactor.go +++ b/compactor.go @@ -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 -// 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 @@ -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) { - view := viewParts(task.Context) text, err := compactionTask(task) if err != nil { return "", err } + first := compactionParts(task.Context, text) var lines []string note := "" for len(lines) < compactionTries { - parts := slices.Concat(view, []string{text}) + parts := first if note != "" { - parts = append(parts, note) + parts = append(slices.Clip(first), note) } var response Response @@ -103,11 +104,12 @@ func (c *Compactor) Summarize(ctx context.Context, task Task) (string, error) { 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, "\n"), "\n") whole := len(lines) - 1 - (len(lines)-1)%viewBlock parts := []string{"\n"} @@ -115,7 +117,7 @@ func viewParts(context string) []string { 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. diff --git a/compactor_test.go b/compactor_test.go index cc99f33..20862c7 100644 --- a/compactor_test.go +++ b/compactor_test.go @@ -44,34 +44,34 @@ func TestCompactorSendsThePromptToolsAndTask(t *testing.T) { require.Equal(t, []string{ "\n", "0+4|a\n4+1|b\n5+1|c\n6+1|d\n", - "7+1|e\n\n", - "Compaction: compress message 7 into one line of at most 512 bytes\n" + + "7+1|e\n\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" + "\nuser: " + strings.Repeat("x", 600) + "\n", }, parts) } -func TestViewPartsSendWholeBlocksOfLines(t *testing.T) { +func TestCompactionPartsEndWithTheRestAndTheTask(t *testing.T) { 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: "\n", - want: []string{"\n", "\n"}, + want: []string{"\n", "\nTASK"}, }, { - name: "lines short of a block go with the closing tag", + name: "lines short of a block go into the last part", context: "\n0+1|a\n1+1|b\n2+1|c\n", - want: []string{"\n", "0+1|a\n1+1|b\n2+1|c\n\n"}, + want: []string{"\n", "0+1|a\n1+1|b\n2+1|c\n\nTASK"}, }, { name: "one whole block", context: "\n0+1|a\n1+1|b\n2+1|c\n3+1|d\n", - want: []string{"\n", "0+1|a\n1+1|b\n2+1|c\n3+1|d\n", "\n"}, + want: []string{"\n", "0+1|a\n1+1|b\n2+1|c\n3+1|d\n", "\nTASK"}, }, { name: "two whole blocks and a rest", @@ -80,23 +80,24 @@ func TestViewPartsSendWholeBlocksOfLines(t *testing.T) { "\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\n", + "9+1|i\n\nTASK", }, }, } 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.context+"\n", strings.Join(parts, "")) + require.Equal(t, tc.context+"\nTASK", strings.Join(parts, "")) }) } } -func TestViewPartsKeepTheBlocksOfAShorterView(t *testing.T) { - shorter := viewParts("\n0+1|a\n1+1|b\n2+1|c\n3+1|d\n4+1|e\n") - longer := viewParts("\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") +func TestCompactionPartsOfALongerViewBeginWithTheBlocksOfAShorterOne(t *testing.T) { + shorter := compactionParts("\n0+1|a\n1+1|b\n2+1|c\n3+1|d\n4+1|e\n", "task 5") + longer := compactionParts("\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", "task 8") + require.Len(t, shorter, 3) 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)) - 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) - 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", - "text": parts[3], + "text": parts[2], }) require.NoError(t, err) require.LessOrEqual(t, len(call.raw), len(server.Call(0).raw)+len(",")+len(note))