18 // rebuildCallLimit is the most model calls of a rebuild. A rebuild summarizes
19 // the whole merged log, which can need more calls than a probe sample, and the
20 // quota stop of the build run still guards the account.
21 const rebuildCallLimit = 1500
23 // rebuild merges added messages with the log of agent, in date order, and
24 // builds a new memory beside it. The caller holds the token lock. The model
25 // uses the original agent prompt and cache key. A failed or stopped build
26 // removes the new directory and leaves the original directory unchanged.
27 // Success copies the other agent files, adds this run's usage, and swaps the
28 // directories, with the original kept as a timestamped backup. Message IDs are
29 // new positions in the merged log, but kind, text, and date stay exact. A child
30 // keeps its staging directory and backup outside children so that neither is
31 // read as a configured child.
32 func rebuild(ctx context.Context, agent *Agent, added []DatedMessage, client *ModelClient, summarizer Summarizer, out io.Writer) (backup string, err error) {
33 err = rebuildIdle(agent.dir)
35 return "", fmt.Errorf("check rebuild work: %w", err)
38 old, err := rebuildReadLog(ctx, agent.dir)
40 return "", fmt.Errorf("read original log: %w", err)
43 entries, err := rebuildMerge(old, added)
45 return "", fmt.Errorf("merge rebuild messages: %w", err)
48 parent := filepath.Dir(agent.dir)
49 backupDir := agent.dir
50 if agent.config.Parent != "" {
51 parent = filepath.Dir(parent)
52 backupDir = filepath.Join(parent, agent.config.Name)
55 root, err := os.MkdirTemp(parent, ".agent-rebuild-")
57 return "", fmt.Errorf("create rebuild directory: %w", err)
61 closeErr := os.RemoveAll(root)
63 err = errors.Join(err, fmt.Errorf("remove rebuild directory: %w", closeErr))
67 memory, err := openMemory(ctx, root, time.Now, summarizer, slog.New(slog.DiscardHandler))
69 return "", fmt.Errorf("open rebuilt memory: %w", err)
72 client.LimitCalls(rebuildCallLimit)
77 usagePath: client.UsagePath(),
80 appended, stop := run.Run(ctx, entries)
81 sizes, err := memory.buildModelLines()
83 err = errors.Join(err, memory.Settle(ctx), memory.rebuildComplete(), memory.Flush(ctx))
86 err = errors.Join(err, memory.Close())
87 records, usageErr := buildReadUsage(client.UsagePath())
88 err = errors.Join(err, usageErr, buildWriteReport(out, appended, stop, records, sizes))
90 err = errors.Join(err, fmt.Errorf("rebuild stopped: %s", stop))
94 return "", fmt.Errorf("finish rebuilt memory: %w", err)
97 err = rebuildCopy(agent.dir, root)
99 return "", fmt.Errorf("copy other agent files: %w", err)
102 usage, err := os.ReadFile(client.UsagePath())
103 if errors.Is(err, os.ErrNotExist) {
108 return "", fmt.Errorf("read rebuild usage: %w", err)
112 err = rebuildAddUsage(filepath.Join(root, "usage.jsonl"), usage)
114 return "", fmt.Errorf("append rebuild usage: %w", err)
118 if ctx.Err() != nil {
122 backup = backupDir + "-backup-" + time.Now().Format("20060102-150405.000000000")
123 err = os.Rename(agent.dir, backup)
125 return "", fmt.Errorf("back up agent: %w", err)
128 err = os.Rename(root, agent.dir)
130 restoreErr := os.Rename(backup, agent.dir)
131 if restoreErr != nil {
132 err = errors.Join(err, fmt.Errorf("restore original agent: %w", restoreErr))
135 return "", fmt.Errorf("install rebuilt agent: %w", err)
141 // rebuildIdle refuses an agent with unfinished subagents, including ones that a
142 // daemon restart must resume. A missing work file lists no work.
143 func rebuildIdle(root string) error {
144 data, err := os.ReadFile(filepath.Join(root, subagentWorkFileName))
145 if errors.Is(err, os.ErrNotExist) {
150 return fmt.Errorf("read running subagents: %w", err)
153 var work SubagentWorkFile
155 err = json.Unmarshal(data, &work)
157 return fmt.Errorf("decode running subagents: %w", err)
160 if len(work.Running) > 0 {
161 return errors.New("rebuild refused: subagents have unfinished work")
167 // rebuildReadLog reads and validates the day files without a write or an index
168 // repair. Even an incomplete final line is an error, not a repair.
169 func rebuildReadLog(ctx context.Context, root string) ([]DatedMessage, error) {
170 paths, err := filepath.Glob(filepath.Join(root, "main", "*.jsonl"))
172 return nil, fmt.Errorf("list message files: %w", err)
177 var entries []DatedMessage
179 for _, path := range paths {
182 day, err = journalDayNumber(strings.TrimSuffix(filepath.Base(path), ".jsonl"))
184 return nil, fmt.Errorf("parse message day: %w", err)
187 err = journalScanFile(ctx, path, day, 0, false, func(line []byte, _ Position) error {
190 err = json.Unmarshal(line, &message)
192 return fmt.Errorf("decode message: %w", err)
195 err = logValidateMessage(message)
197 return fmt.Errorf("validate message: %w", err)
200 if message.ID != len(entries) {
201 return fmt.Errorf("expected message %d, found %d", len(entries), message.ID)
204 entries = append(entries, DatedMessage{
213 return nil, fmt.Errorf("read message file: %w", err)
220 // rebuildMerge preserves the input order for equal dates, with old messages
221 // before added messages. Each date must be explicit and valid.
222 func rebuildMerge(old, added []DatedMessage) ([]DatedMessage, error) {
223 entries := append(slices.Clone(old), added...)
224 dates := make(map[string]time.Time)
225 for _, entry := range entries {
226 err := logValidateMessage(Message{
229 Size: len(entry.Text),
233 return nil, fmt.Errorf("validate dated message: %w", err)
238 date, err = time.Parse(time.RFC3339Nano, entry.Date)
240 return nil, fmt.Errorf("parse merge date: %w", err)
243 dates[entry.Date] = date
246 slices.SortStableFunc(entries, func(a, b DatedMessage) int {
247 return dates[a.Date].Compare(dates[b.Date])
253 // rebuildCopy copies the other agent directories and regular files. It excludes
254 // the memory and its lock, and refuses all other file types.
255 func rebuildCopy(source, target string) error {
256 err := filepath.WalkDir(source, func(path string, entry fs.DirEntry, err error) error {
258 return fmt.Errorf("read agent entry: %w", err)
261 relative, err := filepath.Rel(source, path)
263 return fmt.Errorf("find agent relative path: %w", err)
271 case "main", "tree", "index":
272 return filepath.SkipDir
273 case "lock", "view.json":
277 destination := filepath.Join(target, relative)
280 err = os.Mkdir(destination, 0o700)
282 return fmt.Errorf("create agent directory: %w", err)
284 case entry.Type().IsRegular():
287 data, err = os.ReadFile(path)
289 return fmt.Errorf("read agent file: %w", err)
292 err = os.WriteFile(destination, data, 0o600)
294 return fmt.Errorf("copy agent file: %w", err)
297 return fmt.Errorf("copy agent: unsupported file type for %s", relative)
303 return fmt.Errorf("copy agent files: %w", err)
309 // rebuildAddUsage keeps both the original usage and the calls of this run.
310 func rebuildAddUsage(path string, data []byte) (err error) {
311 file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
313 return fmt.Errorf("open rebuilt usage: %w", err)
317 closeErr := file.Close()
319 err = errors.Join(err, fmt.Errorf("close rebuilt usage: %w", closeErr))
323 _, err = file.Write(data)
325 return fmt.Errorf("append rebuilt usage: %w", err)