return err
}
- linked := "WhatsApp is not linked"
+ linked := "WhatsApp not linked"
if whatsapp.isLinked() {
- linked = "WhatsApp is linked"
+ linked = "WhatsApp linked"
}
- logger.Printf("%s is ready: socket %s, model %s at %s effort, commands run in %s, %s, active schedules: %d", agent.config.Name, listener.Addr(), agent.config.Model, agent.config.Effort, agent.config.Workdir, linked, schedules.Len())
+ logger.Printf("ready: socket %s, model %s (%s), workdir %s, %s, %d schedules", listener.Addr(), agent.config.Model, agent.config.Effort, agent.config.Workdir, linked, schedules.Len())
var telegram *TelegramBot
if hasBot {
telegram = newTelegramBot(newTelegramAPI(telegramAPIURL, botSettings.Token), botSettings.Owner, filepath.Join(agent.dir, telegramOffsetFileName), daemonLogger(out, "telegram: "))
- logger.Print("the Telegram bot waits for messages of its owner")
+ logger.Print("Telegram bot started")
}
return daemonServe(ctx, listener, telegram, whatsapp, schedules, func(notify func(Event)) (*TurnLoop, error) {
// failed logs the failed build of the node key with its cause err.
func (m *Memory) failed(key NodeKey, err error) {
- m.logger.Printf("a summary of the chat failed, and it is tried again at the next message: node (%d, %d): %v", key.level, key.index, err)
+ m.logger.Printf("summary failed, retry at next message: node (%d, %d): %v", key.level, key.index, err)
}
// ready tells if both children of the node key are built and the node is not.
_, err = memory.Append("echo", memoryLongText(0))
require.NoError(t, err)
require.ErrorIs(t, memory.Settle(t.Context()), errMemoryUnsettled)
- require.Equal(t, "a summary of the chat failed, and it is tried again at the next message: node (0, 0): fake failure\n", logs.String())
+ require.Equal(t, "summary failed, retry at next message: node (0, 0): fake failure\n", logs.String())
summarizer.SetFailing(false)
_, err = memory.Append("user", "retry")
}
if schedule.late(now) {
- s.logger.Printf("schedule %d runs %s late, because the daemon was stopped at its time", schedule.ID, durationText(now.Sub(schedule.Due)))
+ s.logger.Printf("schedule %d ran %s late", schedule.ID, durationText(now.Sub(schedule.Due)))
} else {
- s.logger.Printf("schedule %d runs", schedule.ID)
+ s.logger.Printf("schedule %d ran", schedule.ID)
}
turns = append(turns, message)
require.Equal(t, "[Schedule 1] Sunday 2026-11-08 20:05 -03:00, once\nRemind the user to drink water.\n"+scheduleEmptyReply, texts[len(texts)-1])
require.Equal(t, scheduleSource, test.events.LastEvent("work").Source)
require.Equal(t, "No schedules.", schedules.List())
- require.Equal(t, "schedule 1 runs\n", logs.String())
+ require.Equal(t, "schedule 1 ran\n", logs.String())
}
func TestSchedulesRunMissedSchedulesOnceAtTheStart(t *testing.T) {
"[Schedule 2] Sunday 2026-11-08 10:00 -03:00, repeats 0 * * * *\nTell the user the time.\n" + scheduleLateLine + "\n" + scheduleEmptyReply,
}, texts[len(texts)-2:])
require.Equal(t, "2 | Sunday 2026-11-08 21:00 -03:00 | 0 * * * * | Tell the user the time.", schedules.List())
- require.Equal(t, "schedule 1 runs 11h30m0s late, because the daemon was stopped at its time\nschedule 2 runs 10h30m0s late, because the daemon was stopped at its time\n", logs.String())
+ require.Equal(t, "schedule 1 ran 11h30m0s late\nschedule 2 ran 10h30m0s late\n", logs.String())
}
func TestDaemonRunsADueSchedule(t *testing.T) {
for _, name := range names {
_, err = r.Tell(ctx, name, text)
if err != nil {
- r.logger.Printf("subagent %s cannot go on after the restart, and its report tells the agent: %v", name, err)
+ r.logger.Printf("subagent %s lost at restart: %v", name, err)
r.report(name, subagentReport(name, []string{"failed: " + err.Error()}), subagentFailed)
}
}
switch status {
case subagentStopped:
- r.logger.Printf("subagent %s is stopped by the user", run.subagent.name)
+ r.logger.Printf("subagent %s stopped", run.subagent.name)
case subagentFailed:
- r.logger.Printf("subagent %s failed, and its report tells the agent: %v", run.subagent.name, failure)
+ r.logger.Printf("subagent %s failed: %v", run.subagent.name, failure)
default:
- r.logger.Printf("subagent %s is done, and its report goes to the agent", run.subagent.name)
+ r.logger.Printf("subagent %s done", run.subagent.name)
}
r.report(run.subagent.name, subagentReport(run.subagent.name, replies), status)
Source: name,
Status: subagentFinished,
}, test.events.LastEvent("work"))
- require.Equal(t, "subagent "+name+" is done, and its report goes to the agent\n", test.logs.String())
+ require.Equal(t, "subagent "+name+" done\n", test.logs.String())
var first, second *ModelFakeCall
for n := range server.Calls() {
"reply: It failed.",
}, test.Log(t))
require.Equal(t, subagentFailed, test.events.LastEvent("work").Status)
- require.Equal(t, "subagent "+name+" failed, and its report tells the agent: model call: HTTP 400: "+subagentTestFullContext+"\n", test.logs.String())
+ require.Equal(t, "subagent "+name+" failed: model call: HTTP 400: "+subagentTestFullContext+"\n", test.logs.String())
}
func TestSubagentHasItsOwnEffortAndNoSpawn(t *testing.T) {
"work: " + subagentReport(name, []string{"stopped"}),
}, test.Log(t))
require.Equal(t, subagentStopped, test.events.LastEvent("work").Status)
- require.Equal(t, "subagent "+name+" is stopped by the user\n", test.logs.String())
+ require.Equal(t, "subagent "+name+" stopped\n", test.logs.String())
log, err := subagent.Show()
require.NoError(t, err)
outage := &TelegramOutage{
logger: b.logger,
- loss: "cannot get messages from Telegram",
- back: "messages from Telegram come again",
+ loss: "receive failed",
+ back: "receive back",
}
wait := b.minWait
func (b *TelegramBot) handle(ctx context.Context, l *TurnLoop, whatsapp *WhatsApp, update TelegramUpdate) error {
message := update.Message
if message == nil || message.Chat.Type != "private" || message.Chat.ID != b.owner || message.From == nil || message.From.ID != b.owner {
- b.logger.Print("a Telegram message is ignored, because it is not from the owner in the private chat")
+ b.logger.Print("message ignored: not from the owner")
return nil
}
var answer *TelegramError
if len(part.entities) > 0 && errors.As(err, &answer) && answer.Status == http.StatusBadRequest {
- b.logger.Printf("a reply goes as plain text, because Telegram refused its formatting: %v", err)
+ b.logger.Printf("reply sent as plain text: %v", err)
part.entities = nil
continue
}
if !retry {
- b.logger.Printf("a reply to Telegram is lost: %v", err)
+ b.logger.Printf("reply lost: %v", err)
return
}
}
if telegramRejected(err) {
- b.logger.Printf("a reply goes with simpler formatting, because Telegram refused its full formatting: %v", err)
+ b.logger.Printf("reply sent with simple formatting: %v", err)
for _, part := range telegramReplyParts(telegramUnescapeTags(markdown)) {
b.deliver(ctx, part)
}
}
if !retry {
- b.logger.Printf("a reply to Telegram is lost: %v", err)
+ b.logger.Printf("reply lost: %v", err)
return
}
func (b *TelegramBot) sendOutage() *TelegramOutage {
return &TelegramOutage{
logger: b.logger,
- loss: "cannot send replies to Telegram",
- back: "replies go to Telegram again",
+ loss: "send failed",
+ back: "send back",
}
}
}
o.start = time.Now()
- o.logger.Printf("%s: %v (next try in %s)", o.loss, err, durationText(delay))
+ o.logger.Printf("%s: %v (retry in %s)", o.loss, err, durationText(delay))
}
// End logs the end of the outage that runs, with its length.
return
}
- o.logger.Printf("%s, after %s", o.back, durationText(time.Since(o.start)))
+ o.logger.Printf("%s after %s", o.back, durationText(time.Since(o.start)))
o.start = time.Time{}
}
data, err := io.ReadAll(io.LimitReader(response.Body, telegramAnswerBytes))
err = errors.Join(err, response.Body.Close())
if err != nil {
- return b.networkError("the answer broke off", err, timeout)
+ return b.networkError("answer cut off", err, timeout)
}
var answer TelegramAnswer
}
if err != nil {
- return b.networkError("the answer is not valid JSON", err, timeout)
+ return b.networkError("invalid JSON", err, timeout)
}
if !answer.OK {
err = json.Unmarshal(answer.Result, result)
if err != nil {
- return b.networkError("the result is not valid", err, timeout)
+ return b.networkError("invalid result", err, timeout)
}
return nil
}
// TelegramError is an answer of the Bot API that tells of a failure. Its text
-// gives the HTTP status and the description, such as "Telegram answered 502 Bad
-// Gateway". RetryAfter is the wait that the Bot API asks for after HTTP 429.
-// Refused tells that the answer is a Bot API answer with ok false, not a page
-// of a proxy or of a server error.
+// gives the HTTP status and the description, such as "502 Bad Gateway".
+// RetryAfter is the wait that the Bot API asks for after HTTP 429. Refused
+// tells that the answer is a Bot API answer with ok false, not a page of a
+// proxy or of a server error.
type TelegramError struct {
Status int
Description string
}
func (e *TelegramError) Error() string {
- return fmt.Sprintf("Telegram answered %d %s", e.Status, e.Description)
+ return fmt.Sprintf("%d %s", e.Status, e.Description)
}
// TelegramUpdate is one update of getUpdates. Message is nil for the kinds of
},
retry: true,
wait: 3 * time.Second,
- errorText: "answered 429",
+ errorText: "429",
},
{
name: "server error page is retried",
_, _ = io.WriteString(w, "<html>bad gateway</html>")
},
retry: true,
- errorText: "Telegram answered 502 Bad Gateway",
+ errorText: "502 Bad Gateway",
},
{
name: "token echoed in a description is hidden",
},
retry: false,
rejected: true,
- errorText: "answered 400 Bad Request: can't parse",
+ errorText: "400 Bad Request: can't parse",
},
{
name: "unknown method is a refusal",
},
retry: false,
rejected: true,
- errorText: "answered 404 Not Found",
+ errorText: "404 Not Found",
},
{
name: "not found page of a proxy is not a refusal",
_, _ = io.WriteString(w, "<html>not found</html>")
},
retry: false,
- errorText: "answered 404 Not Found",
+ errorText: "404 Not Found",
},
{
name: "broken answer is retried",
_, _ = io.WriteString(w, `{"ok":`)
},
retry: true,
- errorText: "not valid JSON",
+ errorText: "invalid JSON",
},
}
require.Empty(t, telegram.Sent())
require.NotContains(t, terminal.Output(), "secret")
require.NotContains(t, logs.String(), "secret")
- require.Contains(t, logs.String(), "a Telegram message is ignored")
+ require.Contains(t, logs.String(), "message ignored")
}
func TestBotAnswersThatOnlyTextIsSupported(t *testing.T) {
})
telegramWaitForSent(t, telegram, "ok")
- require.Contains(t, logs.String(), "answered 502")
- require.Contains(t, logs.String(), "answered 429")
+ require.Contains(t, logs.String(), "502")
+ require.Contains(t, logs.String(), "429")
require.NotContains(t, logs.String(), telegramTestToken)
}
require.Equal(t, []string{"hello"}, telegram.Sent())
require.Len(t, telegram.Calls(), 2)
- require.Contains(t, logs.String(), "cannot send replies to Telegram: no answer in 50ms (next try in 1ms)\n")
- require.Contains(t, logs.String(), "replies go to Telegram again, after ")
+ require.Contains(t, logs.String(), "send failed: no answer in 50ms (retry in 1ms)\n")
+ require.Contains(t, logs.String(), "send back after ")
require.Equal(t, 2, strings.Count(logs.String(), "\n"))
require.NotContains(t, logs.String(), telegramTestToken)
}
methods: []string{"sendRichMessage", "sendMessage"},
sent: []string{"a secret word"},
lines: 1,
- logText: "Telegram refused its full formatting",
+ logText: "reply sent with simple formatting",
},
{
name: "unknown method sends the reply once with entities",
methods: []string{"sendRichMessage", "sendMessage"},
sent: []string{"a secret word"},
lines: 1,
- logText: "Telegram refused its full formatting",
+ logText: "reply sent with simple formatting",
},
{
name: "rate limit tries the rich message again",
methods: []string{"sendRichMessage", "sendRichMessage"},
sent: []string{"a **secret** word"},
lines: 2,
- logText: "cannot send replies to Telegram: Telegram answered 429",
+ logText: "send failed: 429",
},
{
name: "server error tries the rich message again",
methods: []string{"sendRichMessage", "sendRichMessage"},
sent: []string{"a **secret** word"},
lines: 2,
- logText: "cannot send replies to Telegram: Telegram answered 500",
+ logText: "send failed: 500",
},
{
name: "other refusal drops the reply",
always: true,
methods: []string{"sendRichMessage"},
lines: 1,
- logText: "a reply to Telegram is lost: Telegram answered 403",
+ logText: "reply lost: 403",
},
}
require.Nil(t, calls[1].params["entities"])
require.Equal(t, []string{"a secret word"}, telegram.Sent())
require.Equal(t, 1, strings.Count(logs.String(), "\n"))
- require.Contains(t, logs.String(), "a reply goes as plain text, because Telegram refused its formatting")
+ require.Contains(t, logs.String(), "reply sent as plain text")
require.NotContains(t, logs.String(), telegramTestToken)
require.NotContains(t, logs.String(), "secret word")
}
daemonStartTestWithBot(t, newModelFake(t), bot)
require.Eventually(t, func() bool {
- return strings.Contains(logs.String(), "cannot get messages from Telegram: no answer: ")
+ return strings.Contains(logs.String(), "receive failed: no answer: ")
}, 10*time.Second, time.Millisecond)
require.NotContains(t, logs.String(), telegramTestToken)
}
telegramWaitForSent(t, telegram, "ok")
lines := strings.Split(strings.TrimSuffix(logs.String(), "\n"), "\n")
require.Len(t, lines, 2)
- require.Equal(t, "cannot get messages from Telegram: Telegram answered 502 Bad Gateway (next try in 1ms)", lines[0])
- require.Regexp(t, `^messages from Telegram come again, after [0-9]+m?s$`, lines[1])
+ require.Equal(t, "receive failed: 502 Bad Gateway (retry in 1ms)", lines[0])
+ require.Regexp(t, `^receive back after [0-9]+m?s$`, lines[1])
require.NotContains(t, logs.String(), "secret word")
require.NotContains(t, logs.String(), strconv.FormatInt(telegramTestOwner, 10))
}
err := l.chain(ctx)
if err != nil && !errors.Is(err, errChainLogWrite) {
if ctx.Err() == nil {
- l.logger.Printf("the turn stops before its end, and the agent waits for the next message: %v", err)
+ l.logger.Printf("turn failed: %v", err)
}
l.notify(Event{
require.Equal(t, 1, server.Calls())
require.Contains(t, test.events.Last("error"), "HTTP 503")
require.Equal(t, 1, strings.Count(test.logs.String(), "\n"))
- require.Contains(t, test.logs.String(), "the turn stops before its end, and the agent waits for the next message: ")
+ require.Contains(t, test.logs.String(), "turn failed: ")
require.Contains(t, test.logs.String(), "HTTP 503")
require.NotContains(t, test.logs.String(), "hello")
}
err := client.ConnectContext(ctx)
if err != nil {
- w.logger.Printf("WhatsApp messages do not come, because the connection failed, and it is tried again only at a restart of the daemon: %s", w.redact(err.Error()))
+ w.logger.Printf("connect failed, restart to retry: %s", w.redact(err.Error()))
}
}
return nil
}
- w.logger.Print("WhatsApp messages stop, because the phone logged out this device (run /whatsapp pair PHONE to link it again)")
+ w.logger.Print("logged out by the phone, run /whatsapp pair PHONE")
err := w.inbox.deliver(ctx, l, w.link(), true)
if err != nil {
return err
err = w.removeSession()
if err != nil {
- w.logger.Printf("the session of the logged out device stays on the disk, because its delete failed: %v", err)
+ w.logger.Printf("session delete failed: %v", err)
}
err = l.note("WhatsApp was unlinked from the phone.")
if err != nil {
- w.logger.Printf("the chat has no note of the logout, because its write failed: %v", err)
+ w.logger.Printf("chat note failed: %v", err)
}
w.broadcast("WhatsApp was unlinked from the phone. Run /whatsapp pair PHONE to link it again.")
return nil
}
- w.logger.Print("WhatsApp messages stop, because the user unlinked WhatsApp")
+ w.logger.Print("unlinked")
err = l.note("WhatsApp is unlinked.")
if err != nil {
- w.logger.Printf("the chat has no note of the unlink, because its write failed: %v", err)
+ w.logger.Printf("chat note failed: %v", err)
}
if client == nil || logoutErr != nil {
case item.Event == whatsmeow.QRChannelSuccess.Event:
linked = true
w.linkedBy(client)
- w.logger.Print("WhatsApp is linked")
+ w.logger.Print("linked")
err = l.note("WhatsApp is linked.")
if err != nil {
- w.logger.Printf("the chat has no note of the link, because its write failed: %v", err)
+ w.logger.Printf("chat note failed: %v", err)
}
answer("WhatsApp is linked to +" + phone + ".")
switch {
case !w.linked || w.connected:
case w.disconnectedAt.IsZero():
- line = "WhatsApp is connected, and new messages come in"
+ line = "connected"
default:
- line = "WhatsApp is connected again, after " + durationText(w.clock().Sub(w.disconnectedAt))
+ line = "connected again after " + durationText(w.clock().Sub(w.disconnectedAt))
}
w.connected = true
}
case *events.Disconnected:
if w.linked && (w.connected || w.disconnectedAt.IsZero()) {
- line = "new WhatsApp messages wait, because the connection is lost (the next try is automatic)"
+ line = "disconnected, retrying"
if !w.connected {
- line = "new WhatsApp messages wait, because the first connection failed (the next try is automatic)"
+ line = "first connect failed, retrying"
}
w.disconnectedAt = w.clock()
w.connected = false
w.noticed = true
notice = "WhatsApp is disconnected and does not connect again by itself: " + event.PermanentDisconnectDescription() + ". Restart the daemon to try again."
- line = "WhatsApp messages do not come, because the connection is lost and it is tried again only at a restart of the daemon: " + whatsappRedact(w.own, event.PermanentDisconnectDescription())
+ line = "disconnected for good, restart to retry: " + whatsappRedact(w.own, event.PermanentDisconnectDescription())
}
}
groups, err := link.client.GetJoinedGroups(ctx)
if err != nil {
- g.logger.Printf("group names can be missing, because the list of groups did not load: %s", whatsappRedact(link.own, err.Error()))
+ g.logger.Printf("group list failed: %s", whatsappRedact(link.own, err.Error()))
}
g.mutex.Lock()
<-fake.connects
require.True(t, fake.Emit(&events.Disconnected{}))
require.Equal(t, "WhatsApp is linked and disconnected since 2026-01-01 12:00.", whatsapp.Status())
- require.Equal(t, "new WhatsApp messages wait, because the first connection failed (the next try is automatic)\n", logs.String())
+ require.Equal(t, "first connect failed, retrying\n", logs.String())
}
func TestWhatsAppLogsEachChangeOfTheConnectionOnce(t *testing.T) {
require.True(t, fake.Emit(&events.Connected{}))
require.Equal(t, []string{
- "WhatsApp is connected, and new messages come in",
- "new WhatsApp messages wait, because the connection is lost (the next try is automatic)",
- "WhatsApp is connected again, after 1m25s",
+ "connected",
+ "disconnected, retrying",
+ "connected again after 1m25s",
}, strings.Split(strings.TrimSuffix(logs.String(), "\n"), "\n"))
require.NotContains(t, logs.String(), whatsappTestNumber)
require.NotContains(t, logs.String(), whatsappTestLID)
require.Eventually(t, func() bool {
return logs.String() != ""
}, 10*time.Second, time.Millisecond)
- require.Equal(t, "WhatsApp messages do not come, because the connection failed, and it is tried again only at a restart of the daemon: no route for OWN@s.whatsapp.net and OWN@lid\n", logs.String())
+ require.Equal(t, "connect failed, restart to retry: no route for OWN@s.whatsapp.net and OWN@lid\n", logs.String())
}
func TestWhatsAppPhone(t *testing.T) {