path := filepath.Join(dir, "config.json")
data, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
- return createAgent(dir)
+ agent, err := createAgent(dir)
+ if err != nil {
+ return agent, fmt.Errorf("create agent: %w", err)
+ }
+
+ return agent, nil
}
if err != nil {
return nil, fmt.Errorf("prepare agent defaults: %w", err)
}
- return writeAgent(dir, config)
+ agent, err := writeAgent(dir, config)
+ if err != nil {
+ return agent, fmt.Errorf("write new agent: %w", err)
+ }
+
+ return agent, nil
}
// defaultAgentConfig returns the settings of a new agent, without an ID. The
err = errors.Join(err, tokens.Close())
}()
- return newChatGPTAuth(filepath.Join(dir, chatgptTokenFileName), chatgptIssuer).Login(ctx, out)
+ err = newChatGPTAuth(filepath.Join(dir, chatgptTokenFileName), chatgptIssuer).Login(ctx, out)
+ if err != nil {
+ return fmt.Errorf("complete device login: %w", err)
+ }
+
+ return nil
}
// A signal is the normal stop of the daemon and the client, so their
// result does not include ctx.Err().
case len(args) == 1 && args[0] == "daemon":
- return daemon(ctx, os.Stdout)
+ err := daemon(ctx, os.Stdout)
+ if err != nil {
+ return fmt.Errorf("run daemon: %w", err)
+ }
+
+ return nil
case len(args) == 0 || len(args) == 1 && args[0] == "chat":
- return terminal(ctx, os.Stdin, os.Stdout)
+ err := terminal(ctx, os.Stdin, os.Stdout)
+ if err != nil {
+ return fmt.Errorf("run terminal: %w", err)
+ }
+
+ return nil
case len(args) == 2 && args[0] == "backup":
- return backup(ctx, args[1], os.Stdout)
+ err := backup(ctx, args[1], os.Stdout)
+ if err != nil {
+ return fmt.Errorf("run backup: %w", err)
+ }
+
+ return nil
case len(args) == 2 && args[0] == "restore":
- return restore(ctx, args[1], os.Stdout)
+ err := restore(ctx, args[1], os.Stdout)
+ if err != nil {
+ return fmt.Errorf("run restore: %w", err)
+ }
+
+ return nil
case len(args) > 0 && args[0] == "import":
- return whatsappImport(ctx, args[1:], os.Stdout)
+ err := whatsappImport(ctx, args[1:], os.Stdout)
+ if err != nil {
+ return fmt.Errorf("import WhatsApp history: %w", err)
+ }
+
+ return nil
case len(args) > 0 && args[0] == "probe":
err := probe(ctx, args[1:], os.Stdout)
if err != nil {
logger.Info("Telegram bot started")
}
- return daemonServe(ctx, listener, telegram, whatsapp, schedules, func(notify func(Event)) (*TurnLoop, error) {
+ err = daemonServe(ctx, listener, telegram, whatsapp, schedules, func(notify func(Event)) (*TurnLoop, error) {
return root.Loop(whatsapp, notify)
}, agents[1:]...)
+ if err != nil {
+ return fmt.Errorf("serve daemon: %w", err)
+ }
+
+ return nil
}
// daemonListenSocket listens on the Unix socket at path, with mode 0600. The
}
if errors.Is(err, io.EOF) && last {
- return journalCutFile(path, offset)
+ err = journalCutFile(path, offset)
+ if err != nil {
+ return fmt.Errorf("cut incomplete day line: %w", err)
+ }
+
+ return nil
}
if errors.Is(err, io.EOF) {
}
}
- return l.journal.Scan(ctx, start, func(line []byte, place Position) error {
+ err := l.journal.Scan(ctx, start, func(line []byte, place Position) error {
var message Message
err := json.Unmarshal(line, &message)
return fmt.Errorf("message %d: %w", message.ID, err)
}
- return l.addIndex(place)
+ err = l.addIndex(place)
+ if err != nil {
+ return fmt.Errorf("index scanned message: %w", err)
+ }
+
+ return nil
})
+ if err != nil {
+ return fmt.Errorf("scan message journal: %w", err)
+ }
+
+ return nil
}
func (l *Log) addIndex(place Position) error {
}
}
- return m.save()
+ err := m.save()
+ if err != nil {
+ return fmt.Errorf("save caught-up memory: %w", err)
+ }
+
+ return nil
}
// dispatch starts the builds that are ready, up to the limit. A build whose
m.chat.Refresh(node)
m.cview.Refresh(node)
- return m.settle(NodeKey{node.Level, node.Index})
+ err = m.settle(NodeKey{node.Level, node.Index})
+ if err != nil {
+ return fmt.Errorf("settle built node: %w", err)
+ }
+
+ return nil
}
// settle looks at the parent of the built node key when both children are
return ModelResponse{}, errors.Join(err, readErr)
}
- return modelReadStream(httpResponse.Body)
+ response, err = modelReadStream(httpResponse.Body)
+ if err != nil {
+ return response, fmt.Errorf("read model response stream: %w", err)
+ }
+
+ return response, nil
}
// Write appends record to the file with one write.
<-s.slots
}()
- return s.inner.Summarize(ctx, task)
+ text, err := s.inner.Summarize(ctx, task)
+ if err != nil {
+ return text, fmt.Errorf("summarize probe node: %w", err)
+ }
+
+ return text, nil
}
// probe runs "onechat probe LOG COUNT [BUILDS]". It appends the first COUNT
if schedule.Cron == "" {
s.file.Schedules = slices.Delete(s.file.Schedules, index, index+1)
- return s.save()
+ err := s.save()
+ if err != nil {
+ return fmt.Errorf("save completed one-time schedule: %w", err)
+ }
+
+ return nil
}
cron, err := scheduleParseCron(schedule.Cron)
if !found {
s.file.Schedules = slices.Delete(s.file.Schedules, index, index+1)
- return s.save()
+ err = s.save()
+ if err != nil {
+ return fmt.Errorf("save exhausted schedule: %w", err)
+ }
+
+ return nil
}
s.file.Schedules[index].Due = next
- return s.save()
+ err = s.save()
+ if err != nil {
+ return fmt.Errorf("save next schedule run: %w", err)
+ }
+
+ return nil
}
// sorted returns a copy of the schedules by due time, then by ID.
return nil, fmt.Errorf("create subagent directory: %w", err)
}
- return s.Get(ctx, name)
+ subagent, err := s.Get(ctx, name)
+ if err != nil {
+ return subagent, fmt.Errorf("open created worker: %w", err)
+ }
+
+ return subagent, nil
}
return nil, fmt.Errorf("no free subagent name after %d draws", subagentNameDraws)
return "", fmt.Errorf("create work directory: %w", err)
}
- return subagent.name, r.start(ctx, &SubagentRun{
+ err = r.start(ctx, &SubagentRun{
subagent: subagent,
view: view,
effort: effort,
})
+ if err != nil {
+ return subagent.name, fmt.Errorf("start created worker: %w", err)
+ }
+
+ return subagent.name, nil
}
// start runs run in the background and logs its start. Its context keeps the
},
}
- return chain.Run(ctx, input)
+ text, err := chain.Run(ctx, input)
+ if err != nil {
+ return text, fmt.Errorf("run worker chain: %w", err)
+ }
+
+ return text, nil
}
// take returns the messages of Tell that wait for a request of run, and removes
r.mutex.Lock()
defer r.mutex.Unlock()
- return r.save()
+ err = r.save()
+ if err != nil {
+ return fmt.Errorf("save resumed workers: %w", err)
+ }
+
+ return nil
}
// load reads the names of work.json. A missing file names none.
name := test.events.Last("subagent")
require.Equal(t, []string{
- "work: [" + name + "]\nfailed: create model response: model call: HTTP 400: " + subagentTestFullContext + "\n\nFull chat: zoom(\"" + name + "\")",
+ "work: [" + name + "]\nfailed: run worker chain: create model response: model call: HTTP 400: " + subagentTestFullContext + "\n\nFull chat: zoom(\"" + name + "\")",
"reply: It failed.",
}, test.Log(t))
require.Equal(t, subagentFailed, test.events.LastEvent("work").Status)
- require.Equal(t, "<6>subagent started name="+name+"\n<3>subagent failed name="+name+" after=0s: create model response: model call: HTTP 400: "+subagentTestFullContext+"\n", test.logs.String())
+ require.Equal(t, "<6>subagent started name="+name+"\n<3>subagent failed name="+name+" after=0s: run worker chain: create model response: model call: HTTP 400: "+subagentTestFullContext+"\n", test.logs.String())
}
func TestSubagentHasItsOwnEffortAndNoSpawn(t *testing.T) {
}
func TestAFailedSyncFailsEachLaterFlush(t *testing.T) {
+ failure := errors.New("disk error")
s := newSyncer(func() error {
- return errors.New("disk error")
+ return failure
})
s.Written()
- require.ErrorContains(t, s.Flush(t.Context()), "disk error")
+ require.ErrorIs(t, s.Flush(t.Context()), failure)
s.Written()
- require.ErrorContains(t, s.Flush(t.Context()), "disk error")
- require.ErrorContains(t, s.Close(), "disk error")
+ require.ErrorIs(t, s.Flush(t.Context()), failure)
+ require.ErrorIs(t, s.Close(), failure)
}
func TestMemoryFlushSyncsTheChat(t *testing.T) {
return fmt.Errorf(`connect to the daemon (start it with "onechat daemon"): %w`, err)
}
- return terminalSession(ctx, conn, in, out)
+ err = terminalSession(ctx, conn, in, out)
+ if err != nil {
+ return fmt.Errorf("run terminal session: %w", err)
+ }
+
+ return nil
}
// terminalSession runs the client on conn until the input ends, the daemon
return "", fmt.Errorf("open worker log: %w", err)
}
- return subagent.Show()
+ text, err := subagent.Show()
+ if err != nil {
+ return text, fmt.Errorf("read worker log: %w", err)
+ }
+
+ return text, nil
}
// zoom returns message id whole for n = 1, or else the two lines under the line
}
}
- return t.journal.Scan(ctx, start, func(line []byte, place Position) error {
+ err = t.journal.Scan(ctx, start, func(line []byte, place Position) error {
var node Node
err := json.Unmarshal(line, &node)
err = t.check(node)
if err != nil {
- return err
+ return fmt.Errorf("validate scanned node: %w", err)
}
if nodeEnd(node.Level, node.Index) > total {
return fmt.Errorf("node (%d, %d) covers messages after the log", node.Level, node.Index)
}
- return t.addIndex(NodeKey{node.Level, node.Index}, place)
+ err = t.addIndex(NodeKey{node.Level, node.Index}, place)
+ if err != nil {
+ return fmt.Errorf("index scanned node: %w", err)
+ }
+
+ return nil
})
+ if err != nil {
+ return fmt.Errorf("scan node journal: %w", err)
+ }
+
+ return nil
}
// Close closes the files of the tree.
record := make([]byte, positionBytes)
encodePosition(record, place)
- return level.Write(node.index, record)
+ err = level.Write(node.index, record)
+ if err != nil {
+ return fmt.Errorf("write node level record: %w", err)
+ }
+
+ return nil
}
// check makes sure that node is valid and can join the tree now.
return fmt.Errorf("append node order: %w", err)
}
- return t.writeLevel(node, place)
+ err = t.writeLevel(node, place)
+ if err != nil {
+ return fmt.Errorf("write indexed node level: %w", err)
+ }
+
+ return nil
}
// level returns the index of level, and opens it at first use.
require.Equal(t, "user: a", treeReadNode(t, tree, 0, 0).Text)
}
+func TestTreeMissingNodeHasNoErrorAfterWrapping(t *testing.T) {
+ tree, err := openTree(t.Context(), t.TempDir(), 0, newTestClock().Now)
+ require.NoError(t, err)
+
+ t.Cleanup(func() {
+ require.NoError(t, tree.Close())
+ })
+
+ _, built, err := tree.Node(0, 0)
+ require.NoError(t, err)
+ require.False(t, built)
+}
+
+func TestTreeReadFailureKeepsItsCause(t *testing.T) {
+ tree, err := openTree(t.Context(), t.TempDir(), 1, newTestClock().Now)
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ require.NoError(t, tree.Close())
+ })
+
+ require.NoError(t, tree.Add(treeTestNode(0, 0, "user: a")))
+ require.NoError(t, tree.levels[0].Close())
+
+ _, built, err := tree.Node(0, 0)
+ delete(tree.levels, 0)
+ require.ErrorIs(t, err, os.ErrClosed)
+ require.False(t, built)
+ require.ErrorContains(t, err, "find node position")
+}
+
func TestTreeReopenLoadsTheSameNodes(t *testing.T) {
root := t.TempDir()
clock := newTestClock()
v.lines = append(v.lines, line)
v.end++
- return v.fit(tree)
+ merged, err := v.fit(tree)
+ if err != nil {
+ return merged, fmt.Errorf("fit appended view: %w", err)
+ }
+
+ return merged, nil
}
// load keeps the node of line when it is built, and tells if it is.