tomo-ai 0.8.2 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -1
- package/README.md +11 -1
- package/defaults/AGENT.md +13 -0
- package/defaults/CONTINUITY.md +1 -1
- package/defaults/skills/system/CONFIG.md +4 -1
- package/defaults/skills/system/SKILL.md +3 -0
- package/dist/agent/audience.d.ts +19 -0
- package/dist/agent/audience.d.ts.map +1 -0
- package/dist/agent/audience.js +28 -0
- package/dist/agent/audience.js.map +1 -0
- package/dist/agent/commands.d.ts +36 -0
- package/dist/agent/commands.d.ts.map +1 -0
- package/dist/agent/commands.js +260 -0
- package/dist/agent/commands.js.map +1 -0
- package/dist/agent/inbound-batcher.d.ts +54 -0
- package/dist/agent/inbound-batcher.d.ts.map +1 -0
- package/dist/agent/inbound-batcher.js +148 -0
- package/dist/agent/inbound-batcher.js.map +1 -0
- package/dist/agent/live-session.d.ts +75 -1
- package/dist/agent/live-session.d.ts.map +1 -1
- package/dist/agent/live-session.js +275 -50
- package/dist/agent/live-session.js.map +1 -1
- package/dist/agent/sdk-options.d.ts +6 -0
- package/dist/agent/sdk-options.d.ts.map +1 -1
- package/dist/agent/sdk-options.js +7 -1
- package/dist/agent/sdk-options.js.map +1 -1
- package/dist/agent.d.ts +65 -37
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +500 -439
- package/dist/agent.js.map +1 -1
- package/dist/channels/attachments.d.ts +8 -0
- package/dist/channels/attachments.d.ts.map +1 -0
- package/dist/channels/attachments.js +46 -0
- package/dist/channels/attachments.js.map +1 -0
- package/dist/channels/imessage.d.ts +0 -1
- package/dist/channels/imessage.d.ts.map +1 -1
- package/dist/channels/imessage.js +72 -93
- package/dist/channels/imessage.js.map +1 -1
- package/dist/channels/telegram.d.ts.map +1 -1
- package/dist/channels/telegram.js +112 -78
- package/dist/channels/telegram.js.map +1 -1
- package/dist/channels/text-utils.d.ts +7 -0
- package/dist/channels/text-utils.d.ts.map +1 -0
- package/dist/channels/text-utils.js +27 -0
- package/dist/channels/text-utils.js.map +1 -0
- package/dist/channels/types.d.ts +6 -1
- package/dist/channels/types.d.ts.map +1 -1
- package/dist/cli/backup.js +24 -9
- package/dist/cli/backup.js.map +1 -1
- package/dist/cli/config/shared.d.ts.map +1 -1
- package/dist/cli/config/shared.js +4 -5
- package/dist/cli/config/shared.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +4 -4
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/sessions.js +11 -7
- package/dist/cli/sessions.js.map +1 -1
- package/dist/cli/start.js +23 -2
- package/dist/cli/start.js.map +1 -1
- package/dist/config.d.ts +18 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +43 -4
- package/dist/config.js.map +1 -1
- package/dist/continuity-defaults.d.ts +4 -0
- package/dist/continuity-defaults.d.ts.map +1 -0
- package/dist/continuity-defaults.js +4 -0
- package/dist/continuity-defaults.js.map +1 -0
- package/dist/continuity.d.ts +2 -0
- package/dist/continuity.d.ts.map +1 -1
- package/dist/continuity.js +19 -3
- package/dist/continuity.js.map +1 -1
- package/dist/cron/scheduler.d.ts +1 -0
- package/dist/cron/scheduler.d.ts.map +1 -1
- package/dist/cron/scheduler.js +22 -5
- package/dist/cron/scheduler.js.map +1 -1
- package/dist/cron/store.d.ts.map +1 -1
- package/dist/cron/store.js +8 -2
- package/dist/cron/store.js.map +1 -1
- package/dist/fs-utils.d.ts +6 -0
- package/dist/fs-utils.d.ts.map +1 -0
- package/dist/fs-utils.js +43 -0
- package/dist/fs-utils.js.map +1 -0
- package/dist/jsonl.d.ts +3 -0
- package/dist/jsonl.d.ts.map +1 -0
- package/dist/jsonl.js +21 -0
- package/dist/jsonl.js.map +1 -0
- package/dist/lcm/blocks.d.ts +0 -7
- package/dist/lcm/blocks.d.ts.map +1 -1
- package/dist/lcm/blocks.js +3 -31
- package/dist/lcm/blocks.js.map +1 -1
- package/dist/lcm/compact.d.ts +35 -0
- package/dist/lcm/compact.d.ts.map +1 -1
- package/dist/lcm/compact.js +14 -26
- package/dist/lcm/compact.js.map +1 -1
- package/dist/lcm/prune-tools.d.ts +5 -0
- package/dist/lcm/prune-tools.d.ts.map +1 -1
- package/dist/lcm/prune-tools.js +100 -32
- package/dist/lcm/prune-tools.js.map +1 -1
- package/dist/lcm/runner.d.ts.map +1 -1
- package/dist/lcm/runner.js +3 -2
- package/dist/lcm/runner.js.map +1 -1
- package/dist/lcm/stats.d.ts.map +1 -1
- package/dist/lcm/stats.js +6 -23
- package/dist/lcm/stats.js.map +1 -1
- package/dist/mcp/internal-server.d.ts.map +1 -1
- package/dist/mcp/internal-server.js +2 -0
- package/dist/mcp/internal-server.js.map +1 -1
- package/dist/mcp/oauth.d.ts.map +1 -1
- package/dist/mcp/oauth.js +7 -3
- package/dist/mcp/oauth.js.map +1 -1
- package/dist/mcp/pet-store.d.ts.map +1 -1
- package/dist/mcp/pet-store.js +10 -5
- package/dist/mcp/pet-store.js.map +1 -1
- package/dist/mcp/pet-tools.d.ts.map +1 -1
- package/dist/mcp/pet-tools.js +3 -1
- package/dist/mcp/pet-tools.js.map +1 -1
- package/dist/router.d.ts +28 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +78 -2
- package/dist/router.js.map +1 -1
- package/dist/sessions/keys.d.ts +20 -0
- package/dist/sessions/keys.d.ts.map +1 -0
- package/dist/sessions/keys.js +45 -0
- package/dist/sessions/keys.js.map +1 -0
- package/dist/sessions/repair.d.ts.map +1 -1
- package/dist/sessions/repair.js +3 -2
- package/dist/sessions/repair.js.map +1 -1
- package/dist/sessions/store.d.ts +17 -2
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +134 -34
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/summon-store.d.ts +36 -0
- package/dist/sessions/summon-store.d.ts.map +1 -0
- package/dist/sessions/summon-store.js +89 -0
- package/dist/sessions/summon-store.js.map +1 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.8.4 (2026-06-19)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- **`/summon` — pull your main session into a group chat.** A configured identity's owner can `/summon` in an allowlisted group so the group's messages run on their unified `dm:` session (full personal context) until `/dismiss` or after `summonExpiryMinutes` of group inactivity (default 60; `0` disables, `TOMO_SUMMON_EXPIRY_MINUTES` to override). Turn output stays in the owner's private DM — group-facing replies require an explicit `send_message` direct-mode call — so private context never auto-posts to the group. Summon state is persisted across daemon restarts, lapses lazily, and an audience-switch note is injected when a `dm:` session's inbound audience flips between private DM and a summoned group.
|
|
8
|
+
- **Configurable continuity heartbeat.** The continuity heartbeat interval is no longer hardcoded and can be tuned via config.
|
|
9
|
+
- **`pet_status` chat command** (#157). Surface the virtual pet's current state directly from chat.
|
|
10
|
+
- **Message steering on by default** (#150). `steering` now defaults to on (`TOMO_STEERING=false` to opt out), and direct sends were cleaned up alongside it.
|
|
11
|
+
|
|
12
|
+
### Bug fixes
|
|
13
|
+
|
|
14
|
+
- **`/summon` no longer emits a spurious "expired — handed back" notice on re-summon** (#160). The "already summoned?" guard read (also used by `/status` and `/dismiss`) lazily expired a lapsed summon and fired the group-facing handback notice, so re-summoning a group whose previous summon had gone stale posted a contradictory pair of messages. The handback notice now fires only on the real group-message routing path; guard reads still clear the `dm:` session's stale "summoned" context, but silently.
|
|
15
|
+
- **Session timeout recovery and LCM typing fixes** (#155).
|
|
16
|
+
- **iMessage typing-indicator lifecycle fix** (#154). The typing indicator is started and cleared correctly across the message lifecycle.
|
|
17
|
+
- **`send_message` MEDIA/STICKER tag parsing in direct mode** (#147). Attachment tags are now parsed in direct sends, with regression coverage.
|
|
18
|
+
- **Virtual pet hunger pacing tuned** (#149).
|
|
19
|
+
|
|
20
|
+
### Other
|
|
21
|
+
|
|
22
|
+
- Internal refactor: extracted inbound batching and chat-command handling out of the `Agent` class (#145).
|
|
23
|
+
- Bump `@anthropic-ai/claude-agent-sdk` `0.3.168` → `0.3.177` → `0.3.181` (#159, #153).
|
|
24
|
+
- Bump dev-dependencies group (#152).
|
|
25
|
+
|
|
26
|
+
## 0.8.3 (2026-06-11)
|
|
27
|
+
|
|
28
|
+
### Features
|
|
29
|
+
|
|
30
|
+
- **Message steering** (#142). New optional `steering` config (`TOMO_STEERING=false` to opt out, default on) lets user messages that arrive during an in-flight tool-using turn bypass the per-session queue and inject at the next tool-call boundary. If the current turn has no boundary left, the message runs as the next follow-up turn. Cron, continuity, and other system-originated turns continue to queue normally.
|
|
31
|
+
|
|
32
|
+
### Bug fixes
|
|
33
|
+
|
|
34
|
+
- **Six high-severity fixes from a codebase review** (#140):
|
|
35
|
+
- Fresh installs no longer crash on every CLI command (including `tomo init` and `--help`) when no channel is configured — channel validation moved from config load to daemon startup, and `tomo start` now validates before spawning the background child instead of printing "started" while the daemon silently dies.
|
|
36
|
+
- The iMessage webhook server binds `127.0.0.1` instead of all interfaces and caps request bodies at 1 MB; the webhook is also registered as the literal `127.0.0.1` (BlueBubbles could resolve `localhost` to `::1` and get connection refused), with stale `localhost`-form registrations from earlier versions cleaned up on upgrade.
|
|
37
|
+
- The cron scheduler no longer re-fires a due job on every 30s poll while a long agent run is still in flight (re-entrancy guard on `tick()`), and tick errors are caught instead of becoming unhandled rejections.
|
|
38
|
+
- Telegram streaming no longer silently drops content when a send/edit fails: progress is only advanced on success, the final flush retries with backoff, blocks over 4096 chars roll over into a new message, and `send()` chunks long text like iMessage does.
|
|
39
|
+
- `tomo backup restore` no longer deletes the preserved `.claude` directory along with the workspace tree; the backup's custom skills are merged in without overwriting live ones.
|
|
40
|
+
- The LCM compact nudge goes through the per-session queue instead of a fire-and-forget run that could overlap the next user message.
|
|
41
|
+
- **Multi-process race and atomic-write fixes** (#141). The daemon and CLI commands (`tomo cron add`, `tomo sessions clear`, `tomo config`, `tomo lcm prune-tools`) mutate the same files and could clobber each other:
|
|
42
|
+
- `SessionStore` registry mutators and list APIs reload from disk before acting, so CLI-side changes are no longer reverted by the daemon's next stale save.
|
|
43
|
+
- `CronStore.markRun`/`remove` reload before saving, so a job added while the daemon executes another job is no longer silently deleted.
|
|
44
|
+
- The MCP OAuth token store, pet store, and SDK session repair now write atomically (temp + rename) — a crash mid-write previously left truncated JSON that could permanently discard every MCP server's refresh token.
|
|
45
|
+
- `tomo lcm prune-tools` rewrites the live SDK JSONL with the same concurrent-append-safe machinery as compaction, instead of a plain read/write that could truncate events the SDK was appending mid-prune.
|
|
46
|
+
|
|
47
|
+
### Other
|
|
48
|
+
|
|
49
|
+
- Raise minimum Node version to 22.12 (#139).
|
|
50
|
+
- Internal cleanup: extracted shared `fs-utils`, `jsonl`, and session-key helpers (#138).
|
|
51
|
+
- Bump `commander` 14.0.3 → 15.0.0 (#123).
|
|
4
52
|
|
|
5
53
|
## 0.8.2 (2026-06-08)
|
|
6
54
|
|
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ That's it. Open Telegram and message your bot.
|
|
|
31
31
|
|
|
32
32
|
## Requirements
|
|
33
33
|
|
|
34
|
-
- Node.js 22+
|
|
34
|
+
- Node.js 22.12+
|
|
35
35
|
- [Claude Code](https://claude.com/claude-code) installed. Authenticate Claude Code for direct Claude models, or configure a LiteLLM gateway for non-Claude backends.
|
|
36
36
|
- At least one channel:
|
|
37
37
|
- **Telegram** — bot token from [@BotFather](https://t.me/BotFather)
|
|
@@ -61,6 +61,7 @@ tomo sessions clear # Reset all sessions
|
|
|
61
61
|
| `/model` | Switch model (Claude aliases or LiteLLM `provider/model` names) |
|
|
62
62
|
| `/restore` | Restore `config.json` from `config.json.bak` and restart |
|
|
63
63
|
| `/status` | Show session info (model, channel, message count) |
|
|
64
|
+
| `/pet` | Check Tomo's pet's mood, growth stage, and stats |
|
|
64
65
|
|
|
65
66
|
## Features
|
|
66
67
|
|
|
@@ -226,6 +227,7 @@ Run `tomo config` for interactive setup, or edit `~/.tomo/config.json` directly:
|
|
|
226
227
|
"maxTurns": 50,
|
|
227
228
|
"saveInboundImages": true,
|
|
228
229
|
"continuity": true,
|
|
230
|
+
"continuityIntervalMinutes": 55,
|
|
229
231
|
"continuityScript": {
|
|
230
232
|
"path": "~/bin/tomo-continuity.sh",
|
|
231
233
|
"timeoutMs": 30000,
|
|
@@ -246,12 +248,16 @@ Environment variables override config file values:
|
|
|
246
248
|
|----------|-------------|
|
|
247
249
|
| `TELEGRAM_BOT_TOKEN` | Override Telegram token |
|
|
248
250
|
| `IMESSAGE_URL` | Override BlueBubbles URL |
|
|
251
|
+
| `IMESSAGE_TYPING_START_DELAY_MS` | Delay before showing iMessage typing for ordinary turns (default: `1200`) |
|
|
252
|
+
| `IMESSAGE_PASSIVE_TYPING_START_DELAY_MS` | Delay before showing iMessage typing for passive iMessage group turns (default: `4000`) |
|
|
249
253
|
| `CLAUDE_MODEL` | Override model |
|
|
250
254
|
| `TOMO_LITELLM_BASE_URL` | Route Claude Agent SDK model calls through a LiteLLM proxy |
|
|
251
255
|
| `TOMO_LITELLM_API_KEY` | API key sent to the LiteLLM proxy as `ANTHROPIC_API_KEY` |
|
|
252
256
|
| `TOMO_LITELLM_MODE` | Optional LiteLLM mode: `anthropic-compatible` or `chatgpt-subscription` |
|
|
253
257
|
| `TOMO_WORKSPACE` | Override workspace directory |
|
|
254
258
|
| `TOMO_MAX_TURNS` | Override per-turn tool-use ceiling (default: `50`) |
|
|
259
|
+
| `TOMO_STEERING` | Override message steering. Defaults to `true`; set `false` to keep mid-turn messages queued. |
|
|
260
|
+
| `TOMO_CONTINUITY_INTERVAL_MINUTES` | Override scheduled continuity heartbeat interval (default: `55`, minimum: `1`) |
|
|
255
261
|
| `TOMO_CONTINUITY_SCRIPT` | Override the optional continuity script path |
|
|
256
262
|
| `TOMO_CONTINUITY_SCRIPT_TIMEOUT_MS` | Override continuity script timeout (default: `30000`) |
|
|
257
263
|
| `TOMO_CONTINUITY_SCRIPT_MAX_OUTPUT_CHARS` | Override continuity script stdout/stderr cap (default: `8000`) |
|
|
@@ -259,6 +265,10 @@ Environment variables override config file values:
|
|
|
259
265
|
|
|
260
266
|
`continuityScript` can also be a simple path string, e.g. `"continuityScript": "~/bin/tomo-continuity.sh"`. Relative paths resolve under `~/.tomo`; the script runs once per scheduled heartbeat and manual `tomo continuity` trigger, and its stdout/stderr or failure status is appended to the normal continuity prompt.
|
|
261
267
|
|
|
268
|
+
### Steering
|
|
269
|
+
|
|
270
|
+
By default, messages you send while Tomo is mid-task are steered into the in-flight turn at the next tool-call boundary — so "stop", "wait", or extra context reaches the model immediately instead of waiting for the current turn to finish. If the turn has no tool calls left, the message runs as its own follow-up turn right after. iMessage fragment settling still applies before injection; system-originated turns (cron, continuity) keep their normal queued behavior. Set `"steering": false` or `TOMO_STEERING=false` to keep mid-turn messages queued behind the active turn.
|
|
271
|
+
|
|
262
272
|
### LiteLLM / ChatGPT Subscription Models
|
|
263
273
|
|
|
264
274
|
Tomo still runs through Claude Agent SDK, but you can point the SDK at a local LiteLLM proxy and select a LiteLLM model name such as `chatgpt/gpt-5.5`. This keeps Tomo's Claude SDK sessions, memory, workspace, MCP tools, and LCM behavior while LiteLLM translates Anthropic `/v1/messages` streaming calls to the ChatGPT subscription backend.
|
package/defaults/AGENT.md
CHANGED
|
@@ -24,3 +24,16 @@ Call `list_sessions` first if you're unsure which group to address. For normal i
|
|
|
24
24
|
You also have `rename_group_chat` for changing the real title of a Telegram or iMessage group. Only use it when the user explicitly asks to rename a group, and pass a group session key from `list_sessions`.
|
|
25
25
|
|
|
26
26
|
You also have `react_to_latest_message` for reacting/tapbacking to the latest inbound message Tomo has seen in a session. Latest-message state is in-memory since startup; if the tool says none is known, wait for a new inbound message. Usually pass the current Session key.
|
|
27
|
+
|
|
28
|
+
## Summoned Groups
|
|
29
|
+
|
|
30
|
+
The user can run `/summon` in a group chat to temporarily route that group's messages into this session (until `/dismiss`, or automatically after a period of group inactivity). Summoned messages arrive tagged like `[group "Title"] Sender: ...`.
|
|
31
|
+
|
|
32
|
+
How to reply to a summoned group message:
|
|
33
|
+
|
|
34
|
+
- **To the group**: call `send_message` with the group's session key and mode `direct`. Compose the message yourself — you are the session with the context. Never use `delegate` for a summoned group; that wakes the group's own session, which is exactly what summoning bypasses.
|
|
35
|
+
- **To the user privately**: plain text replies in a summoned turn go to the user's private DM, not the group. Use that only for side-notes worth telling them privately; otherwise end the turn with `NO_REPLY`.
|
|
36
|
+
- Match the group's tone and reply like a participant — short, no headers, address people by name when natural. Not every message needs a group reply; stay silent (`NO_REPLY`, no tool call) for chatter that isn't for you.
|
|
37
|
+
- The harness flags audience changes (`[System: audience switched ...]`) whenever consecutive messages hop between the private DM and a group, or between groups. Treat that as a hard reset of tone and privacy — trust the tags over conversational momentum.
|
|
38
|
+
|
|
39
|
+
Everyone in the group can read what you send it. Keep private memories and DM context out of group-facing messages — being summoned shares your judgment and knowledge, not the user's private life.
|
package/defaults/CONTINUITY.md
CHANGED
|
@@ -4,7 +4,7 @@ Read this file when a continuity heartbeat fires. This is your guide for free ti
|
|
|
4
4
|
|
|
5
5
|
## What is this?
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
By default, the system sends you a heartbeat every ~55 minutes. This is not a user message — it's the harness giving you unstructured time. Use it however you want.
|
|
8
8
|
|
|
9
9
|
## Ideas for free time
|
|
10
10
|
|
|
@@ -11,6 +11,7 @@ Before direct edits, copy `~/.tomo/config.json` to `~/.tomo/config.json.bak`. Ch
|
|
|
11
11
|
"model": "claude-sonnet-4-6[1m]",
|
|
12
12
|
"city": "Seattle",
|
|
13
13
|
"continuity": false,
|
|
14
|
+
"continuityIntervalMinutes": 55,
|
|
14
15
|
"continuityScript": {
|
|
15
16
|
"path": "~/bin/tomo-continuity.sh",
|
|
16
17
|
"timeoutMs": 30000,
|
|
@@ -79,6 +80,7 @@ Before direct edits, copy `~/.tomo/config.json` to `~/.tomo/config.json.bak`. Ch
|
|
|
79
80
|
| `model` | string | Claude model IDs/aliases, or a LiteLLM `provider/model` name such as `chatgpt/gpt-5.5`. Default model for every session. |
|
|
80
81
|
| `city` | string \| null | Any city name (e.g. `"Seattle"`). Used for weather in continuity pings. `null` or missing = no weather. |
|
|
81
82
|
| `continuity` | boolean | `true` / `false`. Enables periodic proactive heartbeats. Off by default. |
|
|
83
|
+
| `continuityIntervalMinutes` | number | Optional. Minutes between scheduled continuity heartbeats. Default `55`; minimum `1`. Ignored for manual `tomo continuity` triggers. |
|
|
82
84
|
| `continuityScript` | string \| object \| null | Optional script run once per scheduled continuity heartbeat and manual `tomo continuity` trigger before the prompt is sent to Tomo. Use a string path (`"~/bin/tomo-continuity.sh"`) or `{ "path": "...", "timeoutMs": 30000, "maxOutputChars": 8000 }`. Relative paths resolve under `~/.tomo`; `~`, `$VAR`, and `${VAR}` expand. The script runs as the Tomo OS user with `TOMO_CONTINUITY=true`; stdout/stderr are appended to the heartbeat prompt and capped separately by `maxOutputChars`. Non-zero exits, missing files, and timeouts are passed to Tomo as script status instead of aborting the heartbeat. |
|
|
83
85
|
| `groupSecret` | string \| null | Passphrase users send in a group chat to activate Tomo there. `null` disables group chats entirely. |
|
|
84
86
|
| `channels.telegram.token` | string | BotFather token (`123456:...`). Required to enable the Telegram channel. |
|
|
@@ -96,6 +98,7 @@ Before direct edits, copy `~/.tomo/config.json` to `~/.tomo/config.json.bak`. Ch
|
|
|
96
98
|
| `litellm.baseUrl` | string | Optional LiteLLM proxy base URL, e.g. `http://localhost:4000`. When set, Tomo still uses Claude Agent SDK but sends SDK model calls to the proxy via `ANTHROPIC_BASE_URL`. |
|
|
97
99
|
| `litellm.apiKey` | string | LiteLLM proxy key sent as `ANTHROPIC_API_KEY`. This is the proxy key, not an Anthropic key. For ChatGPT subscription models, LiteLLM owns the OAuth device flow and token storage. |
|
|
98
100
|
| `maxTurns` | number | Max agent turns per single user message (one turn ≈ one tool-use round). Default `50`. Raise if you see "max turns exceeded" on long tool chains. |
|
|
101
|
+
| `steering` | boolean | Optional. Default `true`. User messages that arrive while a turn is in flight bypass the per-session queue and are injected at the next tool-call boundary. If the current turn has no boundary left, the message runs as a follow-up turn. Cron, continuity, and other system-originated turns still queue normally. Set `false` or `TOMO_STEERING=false` to keep mid-turn messages queued behind the active turn. |
|
|
99
102
|
| `mcpServers` | object | External MCP servers keyed by server name. Supports stdio (`command`, `args`, `env`), HTTP (`type: "http"`, `url`, `headers`), and SSE (`type: "sse"`, `url`, `headers`). Environment variables like `${HOME}` expand in `url`, `headers`, `env`, and `args`. |
|
|
100
103
|
| `mcpServers.<name>.oauth` | object | Optional harness-managed OAuth for HTTP/SSE MCP servers. Supports `authorizationServer` (optional; omitted = discover from the MCP server), `clientId` (optional if dynamic registration is available), `scopes`, `tokenStoreKey`, `redirectUri`, and `clientName`. Tokens are stored outside agent context in `workspace/secrets/mcp-oauth.json` (`0600`). |
|
|
101
104
|
| `mcpAllowedTools` | string[] | Auto-allowed external MCP tools. If omitted, Tomo defaults to `mcp__<server>__*` for every configured external server. Set this to narrow tool access, e.g. `["mcp__github__list_issues"]`. |
|
|
@@ -132,5 +135,5 @@ Start it with `litellm --config ~/litellm-chatgpt.yaml`, then set Tomo's model t
|
|
|
132
135
|
## Requirements and overrides
|
|
133
136
|
|
|
134
137
|
- **At least one channel must be configured** — either `channels.telegram.token` or `channels.imessage.url`. Startup fails otherwise.
|
|
135
|
-
- **Env vars override file values** where they exist: `TELEGRAM_BOT_TOKEN`, `IMESSAGE_URL`, `IMESSAGE_PASSWORD`, `IMESSAGE_WEBHOOK_PORT`, `CLAUDE_MODEL`, `TOMO_LITELLM_BASE_URL`, `TOMO_LITELLM_API_KEY`, `TOMO_LITELLM_MODE`, `TOMO_CITY`, `TOMO_CONTINUITY`, `TOMO_WORKSPACE`, `SESSIONS_DIR`, `HISTORY_LIMIT`, `TOMO_MAX_TURNS`.
|
|
138
|
+
- **Env vars override file values** where they exist: `TELEGRAM_BOT_TOKEN`, `IMESSAGE_URL`, `IMESSAGE_PASSWORD`, `IMESSAGE_WEBHOOK_PORT`, `CLAUDE_MODEL`, `TOMO_LITELLM_BASE_URL`, `TOMO_LITELLM_API_KEY`, `TOMO_LITELLM_MODE`, `TOMO_CITY`, `TOMO_CONTINUITY`, `TOMO_CONTINUITY_INTERVAL_MINUTES`, `TOMO_WORKSPACE`, `SESSIONS_DIR`, `HISTORY_LIMIT`, `TOMO_MAX_TURNS`, `TOMO_STEERING`.
|
|
136
139
|
- `workspaceDir`, `sessionsDir`, `historyLimit` are env-only — they're not read from the JSON file.
|
|
@@ -72,6 +72,9 @@ Before changing `~/.tomo/config.json` directly, copy the current file to `~/.tom
|
|
|
72
72
|
### Streaming
|
|
73
73
|
Responses stream to Telegram in real-time — messages update every 1.5s as tokens arrive.
|
|
74
74
|
|
|
75
|
+
### Message steering
|
|
76
|
+
By default, user messages that arrive during a long tool-using turn are injected into the in-flight turn at the next tool-call boundary instead of waiting behind it. Use this to treat mid-task corrections like "stop", "wait", or added context as immediately relevant. If there is no remaining tool boundary, the message runs as the next follow-up turn. Cron, continuity, and other system-originated turns still queue normally. Set `steering: false` in `~/.tomo/config.json` to keep mid-turn user messages queued.
|
|
77
|
+
|
|
75
78
|
### MEDIA: tag
|
|
76
79
|
To send an image/file to the user, include `MEDIA:/path/to/file.png` in your response. The harness strips it from text and sends the file. Text before/after becomes the caption.
|
|
77
80
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Audience identifier for an inbound message on a unified dm: session —
|
|
2
|
+
* "dm" for private messages, the raw "<channel>:<chatId>" key for (summoned)
|
|
3
|
+
* group messages. */
|
|
4
|
+
export declare function audienceOf(channelName: string, message: {
|
|
5
|
+
isGroup?: boolean;
|
|
6
|
+
chatId: string;
|
|
7
|
+
}): string;
|
|
8
|
+
/**
|
|
9
|
+
* Note injected when a dm session's inbound audience changes (private DM ↔
|
|
10
|
+
* summoned group, or between two summoned groups). The per-message
|
|
11
|
+
* [group ...] tags say where each message came FROM; this says the audience
|
|
12
|
+
* CHANGED — the moment when tone or private context is most likely to be
|
|
13
|
+
* carried across by mistake. Returns "" when nothing changed.
|
|
14
|
+
*
|
|
15
|
+
* `audiences` lists this turn's inbound audiences in order (one entry for a
|
|
16
|
+
* single message; several for a coalesced batch, which can mix audiences).
|
|
17
|
+
*/
|
|
18
|
+
export declare function audienceSwitchNote(prev: string | undefined, audiences: string[], label: (audience: string) => string): string;
|
|
19
|
+
//# sourceMappingURL=audience.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audience.d.ts","sourceRoot":"","sources":["../../src/agent/audience.ts"],"names":[],"mappings":"AAAA;;sBAEsB;AACtB,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAEtG;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,SAAS,EAAE,MAAM,EAAE,EACnB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAClC,MAAM,CAQR"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Audience identifier for an inbound message on a unified dm: session —
|
|
2
|
+
* "dm" for private messages, the raw "<channel>:<chatId>" key for (summoned)
|
|
3
|
+
* group messages. */
|
|
4
|
+
export function audienceOf(channelName, message) {
|
|
5
|
+
return message.isGroup ? `${channelName}:${message.chatId}` : "dm";
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Note injected when a dm session's inbound audience changes (private DM ↔
|
|
9
|
+
* summoned group, or between two summoned groups). The per-message
|
|
10
|
+
* [group ...] tags say where each message came FROM; this says the audience
|
|
11
|
+
* CHANGED — the moment when tone or private context is most likely to be
|
|
12
|
+
* carried across by mistake. Returns "" when nothing changed.
|
|
13
|
+
*
|
|
14
|
+
* `audiences` lists this turn's inbound audiences in order (one entry for a
|
|
15
|
+
* single message; several for a coalesced batch, which can mix audiences).
|
|
16
|
+
*/
|
|
17
|
+
export function audienceSwitchNote(prev, audiences, label) {
|
|
18
|
+
if (audiences.length === 0)
|
|
19
|
+
return "";
|
|
20
|
+
const distinct = [...new Set(audiences)];
|
|
21
|
+
if (distinct.length > 1) {
|
|
22
|
+
return `[System: audience check — the messages below span ${distinct.map(label).join(" and ")}. Mind each message's tag when replying; group replies only via send_message.]`;
|
|
23
|
+
}
|
|
24
|
+
if (prev === undefined || distinct[0] === prev)
|
|
25
|
+
return "";
|
|
26
|
+
return `[System: audience switched — the previous message in this session was from ${label(prev)}; this one is from ${label(distinct[0])}. Re-anchor tone and privacy to the new audience.]`;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=audience.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audience.js","sourceRoot":"","sources":["../../src/agent/audience.ts"],"names":[],"mappings":"AAAA;;sBAEsB;AACtB,MAAM,UAAU,UAAU,CAAC,WAAmB,EAAE,OAA8C;IAC5F,OAAO,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,WAAW,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACrE,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAChC,IAAwB,EACxB,SAAmB,EACnB,KAAmC;IAEnC,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACtC,MAAM,QAAQ,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC;IACzC,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,OAAO,qDAAqD,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,gFAAgF,CAAC;IAChL,CAAC;IACD,IAAI,IAAI,KAAK,SAAS,IAAI,QAAQ,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,EAAE,CAAC;IAC1D,OAAO,8EAA8E,KAAK,CAAC,IAAI,CAAC,sBAAsB,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,oDAAoD,CAAC;AAC/L,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Channel } from "../channels/types.js";
|
|
2
|
+
import type { IdentityRouter } from "../router.js";
|
|
3
|
+
import type { SessionStore } from "../sessions/index.js";
|
|
4
|
+
/** Back up ~/.tomo/config.json before a programmatic rewrite. */
|
|
5
|
+
export declare function backupConfigFile(): void;
|
|
6
|
+
export interface ChatCommandDeps {
|
|
7
|
+
router: IdentityRouter;
|
|
8
|
+
sessions: SessionStore;
|
|
9
|
+
/** Shared with the Agent — per-session model overrides read at session create time. */
|
|
10
|
+
modelOverrides: Map<string, string>;
|
|
11
|
+
closeLiveSession(key: string): void;
|
|
12
|
+
isSessionLive(key: string): boolean;
|
|
13
|
+
queuePendingNote(sessionKey: string, note: string): void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Handles slash commands typed in chat (/new, /model, /status, /pet, /restore).
|
|
17
|
+
* Wired to Channel.onCommand by the Agent.
|
|
18
|
+
*/
|
|
19
|
+
export declare class ChatCommandHandler {
|
|
20
|
+
private readonly deps;
|
|
21
|
+
private restoringConfig;
|
|
22
|
+
constructor(deps: ChatCommandDeps);
|
|
23
|
+
/** True once /restore has begun — the Agent drops inbound messages until restart. */
|
|
24
|
+
get isRestoring(): boolean;
|
|
25
|
+
handle(channel: Channel, command: string, chatId: string, senderName: string, args?: string, senderId?: string): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* /summon pulls the owner's main dm: session into a group: subsequent group
|
|
28
|
+
* messages run on the dm session (full personal context) while turn output
|
|
29
|
+
* still goes to the owner's private DM. Group-facing replies require an
|
|
30
|
+
* explicit send_message direct tool call to the raw group session key.
|
|
31
|
+
*/
|
|
32
|
+
private handleSummonCommand;
|
|
33
|
+
private persistModelOverride;
|
|
34
|
+
private restoreConfigAndRestart;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=commands.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/agent/commands.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAGpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAYzD,iEAAiE;AACjE,wBAAgB,gBAAgB,IAAI,IAAI,CAGvC;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE,YAAY,CAAC;IACvB,uFAAuF;IACvF,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IACpC,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1D;AAED;;;GAGG;AACH,qBAAa,kBAAkB;IAGjB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,OAAO,CAAC,eAAe,CAAS;gBAEH,IAAI,EAAE,eAAe;IAElD,qFAAqF;IACrF,IAAI,WAAW,IAAI,OAAO,CAEzB;IAEK,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAwJpI;;;;;OAKG;YACW,mBAAmB;IA8DjC,OAAO,CAAC,oBAAoB;YAed,uBAAuB;CAoCtC"}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { config, CONFIG_BACKUP_PATH, CONFIG_PATH, RESTART_REASON_FILE } from "../config.js";
|
|
5
|
+
import { backupFileIfExistsSync, writeJsonAtomicSync } from "../fs-utils.js";
|
|
6
|
+
import { isGroupSessionKey } from "../sessions/keys.js";
|
|
7
|
+
import { MODEL_ALIASES, isLiteLlmProviderModel, modelHelpText, resolveModelName } from "../models.js";
|
|
8
|
+
import { CHATGPT_SUBSCRIPTION_DEFAULT_MODEL, CHATGPT_SUBSCRIPTION_MODE, isChatGptSubscriptionModel, liteLlmModeLabel, } from "../litellm.js";
|
|
9
|
+
import { log } from "../logger.js";
|
|
10
|
+
import { PetStore } from "../mcp/pet-store.js";
|
|
11
|
+
/** Back up ~/.tomo/config.json before a programmatic rewrite. */
|
|
12
|
+
export function backupConfigFile() {
|
|
13
|
+
mkdirSync(dirname(CONFIG_BACKUP_PATH), { recursive: true });
|
|
14
|
+
backupFileIfExistsSync(CONFIG_PATH, CONFIG_BACKUP_PATH);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Handles slash commands typed in chat (/new, /model, /status, /pet, /restore).
|
|
18
|
+
* Wired to Channel.onCommand by the Agent.
|
|
19
|
+
*/
|
|
20
|
+
export class ChatCommandHandler {
|
|
21
|
+
deps;
|
|
22
|
+
restoringConfig = false;
|
|
23
|
+
constructor(deps) {
|
|
24
|
+
this.deps = deps;
|
|
25
|
+
}
|
|
26
|
+
/** True once /restore has begun — the Agent drops inbound messages until restart. */
|
|
27
|
+
get isRestoring() {
|
|
28
|
+
return this.restoringConfig;
|
|
29
|
+
}
|
|
30
|
+
async handle(channel, command, chatId, senderName, args, senderId) {
|
|
31
|
+
const { sessionKey: key } = this.deps.router.resolve(channel.name, chatId, false);
|
|
32
|
+
if (this.restoringConfig) {
|
|
33
|
+
await channel.send({ chatId, text: "Restore is already in progress. Restarting Tomo..." });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (command === "summon" || command === "dismiss") {
|
|
37
|
+
await this.handleSummonCommand(channel, command, chatId, senderName, senderId);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (command === "new") {
|
|
41
|
+
this.deps.closeLiveSession(key);
|
|
42
|
+
this.deps.sessions.clearSdkSessionId(key);
|
|
43
|
+
log.info({ channel: channel.name, chatId, sender: senderName }, "New session started via /new");
|
|
44
|
+
await channel.send({ chatId, text: "New session started." });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (command === "model") {
|
|
48
|
+
const arg = args?.trim();
|
|
49
|
+
if (!arg) {
|
|
50
|
+
const current = this.deps.modelOverrides.get(key) ?? config.model;
|
|
51
|
+
const lines = [`Current: ${current}`, "", "Switch with: /model <name>", ""];
|
|
52
|
+
for (const [shortName, fullName] of Object.entries(MODEL_ALIASES)) {
|
|
53
|
+
const marker = fullName === current ? " (active)" : "";
|
|
54
|
+
lines.push(` ${shortName} — ${fullName}${marker}`);
|
|
55
|
+
}
|
|
56
|
+
if (config.litellm?.baseUrl) {
|
|
57
|
+
lines.push("");
|
|
58
|
+
lines.push(`LiteLLM gateway models are also accepted, e.g. ${CHATGPT_SUBSCRIPTION_DEFAULT_MODEL}`);
|
|
59
|
+
}
|
|
60
|
+
await channel.send({ chatId, text: lines.join("\n") });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const resolved = resolveModelName(arg);
|
|
64
|
+
if (!resolved) {
|
|
65
|
+
await channel.send({ chatId, text: `Unknown model "${arg}". Use ${modelHelpText()}.` });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (isLiteLlmProviderModel(resolved)) {
|
|
69
|
+
if (!config.litellm?.baseUrl) {
|
|
70
|
+
await channel.send({
|
|
71
|
+
chatId,
|
|
72
|
+
text: `"${resolved}" needs a LiteLLM gateway. Run \`tomo config\` → LiteLLM gateway to set one up first.`,
|
|
73
|
+
});
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (config.litellm.mode === CHATGPT_SUBSCRIPTION_MODE && !isChatGptSubscriptionModel(resolved)) {
|
|
77
|
+
await channel.send({
|
|
78
|
+
chatId,
|
|
79
|
+
text: `The configured ChatGPT subscription gateway only routes chatgpt/* models, e.g. ${CHATGPT_SUBSCRIPTION_DEFAULT_MODEL}.`,
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
this.deps.modelOverrides.set(key, resolved);
|
|
85
|
+
this.persistModelOverride(key, resolved);
|
|
86
|
+
// Model changes require a fresh SDK child process, but keep the SDK
|
|
87
|
+
// session ID so continuity survives switching between Claude and LiteLLM.
|
|
88
|
+
// getOrCreateLiveSession repairs provider-specific JSONL quirks before
|
|
89
|
+
// resuming.
|
|
90
|
+
this.deps.closeLiveSession(key);
|
|
91
|
+
log.info({ channel: channel.name, chatId, model: resolved }, "Model switched via /model");
|
|
92
|
+
await channel.send({ chatId, text: `Switched to ${resolved}` });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (command === "restore") {
|
|
96
|
+
await this.restoreConfigAndRestart(channel, chatId);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (command === "pet") {
|
|
100
|
+
const store = new PetStore(join(config.tomoHome, "data", "pet.json"));
|
|
101
|
+
let pet = store.load();
|
|
102
|
+
if (!pet) {
|
|
103
|
+
await channel.send({ chatId, text: "Tomo doesn't have a pet yet. Ask Tomo to hatch one!" });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
pet = store.tick(pet);
|
|
107
|
+
store.save(pet);
|
|
108
|
+
const ageDays = Math.max(0, Math.floor((Date.now() - new Date(pet.born_at).getTime()) / (24 * 60 * 60 * 1000)));
|
|
109
|
+
const dayLabel = ageDays === 1 ? "day" : "days";
|
|
110
|
+
const lines = [
|
|
111
|
+
`🐾 ${pet.name} the ${pet.species}`,
|
|
112
|
+
`Stage: ${pet.stage} · Age: ${ageDays} ${dayLabel} · Mood: ${store.computeMood(pet)}`,
|
|
113
|
+
`Hunger: ${Math.round(pet.hunger)}/100 · Happiness: ${Math.round(pet.happiness)}/100`,
|
|
114
|
+
`Energy: ${Math.round(pet.energy)}/100 · Health: ${Math.round(pet.health)}/100`,
|
|
115
|
+
`Bond: ${store.effectiveAffection(pet)} · Care mistakes: ${pet.care_mistakes}`,
|
|
116
|
+
];
|
|
117
|
+
if (pet.sleeping && pet.sleep_until) {
|
|
118
|
+
lines.push(`Sleeping until: ${new Date(pet.sleep_until).toLocaleString()}`);
|
|
119
|
+
}
|
|
120
|
+
else if (pet.recovering) {
|
|
121
|
+
lines.push("Recovering: yes");
|
|
122
|
+
}
|
|
123
|
+
await channel.send({ chatId, text: lines.join("\n") });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (command === "status") {
|
|
127
|
+
const model = this.deps.modelOverrides.get(key) ?? config.model;
|
|
128
|
+
const session = this.deps.sessions.get(key);
|
|
129
|
+
const entry = this.deps.sessions.getEntry(key);
|
|
130
|
+
const lines = [];
|
|
131
|
+
lines.push(`Session: ${key}`);
|
|
132
|
+
lines.push(`Channel: ${channel.name}`);
|
|
133
|
+
const summoned = this.deps.router.getSummonedIdentity(channel.name, chatId);
|
|
134
|
+
if (summoned)
|
|
135
|
+
lines.push(`Summoned: messages route to dm:${summoned} (/dismiss to hand back)`);
|
|
136
|
+
lines.push(`Model: ${model}`);
|
|
137
|
+
if (config.litellm?.baseUrl) {
|
|
138
|
+
lines.push(`Gateway: LiteLLM (${liteLlmModeLabel(config.litellm.mode)})`);
|
|
139
|
+
}
|
|
140
|
+
lines.push(`Live: ${this.deps.isSessionLive(key) ? "yes" : "no"}`);
|
|
141
|
+
const msgCount = session.messages.filter((m) => m.role === "user").length;
|
|
142
|
+
lines.push(`Messages: ${msgCount} user turns`);
|
|
143
|
+
if (session.createdAt) {
|
|
144
|
+
lines.push(`Created: ${new Date(session.createdAt).toLocaleString()}`);
|
|
145
|
+
}
|
|
146
|
+
if (session.updatedAt) {
|
|
147
|
+
lines.push(`Last active: ${new Date(session.updatedAt).toLocaleString()}`);
|
|
148
|
+
}
|
|
149
|
+
if (entry?.stats) {
|
|
150
|
+
const s = entry.stats;
|
|
151
|
+
lines.push("");
|
|
152
|
+
lines.push(`Queries: ${s.totalQueries}`);
|
|
153
|
+
lines.push(`Cost: $${s.totalCostUsd.toFixed(4)}`);
|
|
154
|
+
lines.push(`Tokens: ${s.totalInputTokens.toLocaleString()} in / ${s.totalOutputTokens.toLocaleString()} out`);
|
|
155
|
+
if (s.contextMax > 0) {
|
|
156
|
+
const pct = ((s.contextUsed / s.contextMax) * 100).toFixed(0);
|
|
157
|
+
lines.push(`Context: ${pct}% (${s.contextUsed.toLocaleString()} / ${s.contextMax.toLocaleString()})`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
await channel.send({ chatId, text: lines.join("\n") });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* /summon pulls the owner's main dm: session into a group: subsequent group
|
|
166
|
+
* messages run on the dm session (full personal context) while turn output
|
|
167
|
+
* still goes to the owner's private DM. Group-facing replies require an
|
|
168
|
+
* explicit send_message direct tool call to the raw group session key.
|
|
169
|
+
*/
|
|
170
|
+
async handleSummonCommand(channel, command, chatId, senderName, senderId) {
|
|
171
|
+
const rawKey = `${channel.name}:${chatId}`;
|
|
172
|
+
if (!isGroupSessionKey(rawKey)) {
|
|
173
|
+
await channel.send({ chatId, text: `/${command} only works in group chats.` });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (!this.deps.router.isAllowed(channel.name, chatId)) {
|
|
177
|
+
log.debug({ channel: channel.name, chatId }, `/${command} blocked (group not in allowlist)`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const groupLabel = this.deps.sessions.getEntry(rawKey)?.chatTitle ?? rawKey;
|
|
181
|
+
if (command === "dismiss") {
|
|
182
|
+
const summoned = this.deps.router.getSummonedIdentity(channel.name, chatId);
|
|
183
|
+
if (!summoned) {
|
|
184
|
+
await channel.send({ chatId, text: "No main session is summoned here." });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
this.deps.router.dismissGroup(channel.name, chatId);
|
|
188
|
+
this.deps.queuePendingNote(`dm:${summoned}`, `[System: You have been dismissed from the group "${groupLabel}" — its messages no longer reach this session, and the group's own Tomo session has taken back over.]`);
|
|
189
|
+
await channel.send({ chatId, text: "Handed back to this group's own Tomo session." });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const identity = senderId ? this.deps.router.identityForSender(channel.name, senderId) : undefined;
|
|
193
|
+
if (!identity) {
|
|
194
|
+
log.info({ channel: channel.name, chatId, sender: senderName }, "/summon refused (sender is not a configured identity)");
|
|
195
|
+
await channel.send({ chatId, text: "Only the owner of a configured identity can summon their main session." });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const already = this.deps.router.getSummonedIdentity(channel.name, chatId);
|
|
199
|
+
if (already) {
|
|
200
|
+
await channel.send({ chatId, text: `Main session dm:${already} is already summoned here. /dismiss first to hand back.` });
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
this.deps.router.summonGroup(channel.name, chatId, identity.name);
|
|
204
|
+
const expiryNote = config.summonExpiryMinutes > 0
|
|
205
|
+
? ` or after ${config.summonExpiryMinutes} minutes of group inactivity`
|
|
206
|
+
: "";
|
|
207
|
+
this.deps.queuePendingNote(`dm:${identity.name.toLowerCase()}`, `[System: ${identity.name} summoned you into the group chat "${groupLabel}" (${rawKey}). Until dismissed${expiryNote}, messages from that group arrive in this session tagged [group ...] with the sender's name. To reply in the group, call send_message with mode "direct" and target "${rawKey}" — compose the reply yourself, with your context. Plain text you output goes to ${identity.name}'s private DM, not the group. Everyone in the group can read what you send it — keep private memory and DM context out of group-facing messages.]`);
|
|
208
|
+
log.info({ channel: channel.name, chatId, identity: identity.name, sender: senderName }, "Group summoned via /summon");
|
|
209
|
+
await channel.send({
|
|
210
|
+
chatId,
|
|
211
|
+
text: `${identity.name}'s main Tomo session is now handling this group. /dismiss hands back to the group's own session${expiryNote ? `; it also hands back automatically after ${config.summonExpiryMinutes}m of inactivity` : ""}.`,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
persistModelOverride(key, model) {
|
|
215
|
+
config.sessionModelOverrides[key] = model;
|
|
216
|
+
const cfg = existsSync(CONFIG_PATH)
|
|
217
|
+
? JSON.parse(readFileSync(CONFIG_PATH, "utf-8"))
|
|
218
|
+
: {};
|
|
219
|
+
const overrides = (cfg.sessionModelOverrides ?? {});
|
|
220
|
+
overrides[key] = model;
|
|
221
|
+
cfg.sessionModelOverrides = overrides;
|
|
222
|
+
mkdirSync(dirname(CONFIG_PATH), { recursive: true });
|
|
223
|
+
backupConfigFile();
|
|
224
|
+
writeJsonAtomicSync(CONFIG_PATH, cfg);
|
|
225
|
+
}
|
|
226
|
+
async restoreConfigAndRestart(channel, chatId) {
|
|
227
|
+
if (!existsSync(CONFIG_BACKUP_PATH)) {
|
|
228
|
+
await channel.send({ chatId, text: "No config backup found at ~/.tomo/config.json.bak." });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
mkdirSync(dirname(CONFIG_PATH), { recursive: true });
|
|
233
|
+
copyFileSync(CONFIG_BACKUP_PATH, CONFIG_PATH);
|
|
234
|
+
const reason = "Restored ~/.tomo/config.json from ~/.tomo/config.json.bak";
|
|
235
|
+
mkdirSync(dirname(RESTART_REASON_FILE), { recursive: true });
|
|
236
|
+
writeFileSync(RESTART_REASON_FILE, reason, "utf-8");
|
|
237
|
+
this.restoringConfig = true;
|
|
238
|
+
await channel.send({ chatId, text: "Restored config.json from config.json.bak. Restarting Tomo..." });
|
|
239
|
+
if (process.env.NODE_ENV === "test")
|
|
240
|
+
return;
|
|
241
|
+
setTimeout(() => {
|
|
242
|
+
const cli = process.argv[1];
|
|
243
|
+
if (!cli) {
|
|
244
|
+
process.kill(process.pid, "SIGTERM");
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const child = spawn(process.execPath, [cli, "restart"], {
|
|
248
|
+
detached: true,
|
|
249
|
+
stdio: "ignore",
|
|
250
|
+
});
|
|
251
|
+
child.unref();
|
|
252
|
+
}, 100);
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
256
|
+
await channel.send({ chatId, text: `[error] restore failed: ${detail}` });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
//# sourceMappingURL=commands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../../src/agent/commands.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Channel, IncomingMessage } from "../channels/types.js";
|
|
2
|
+
import type { SessionResolution } from "../router.js";
|
|
3
|
+
export interface InboundItem {
|
|
4
|
+
channel: Channel;
|
|
5
|
+
message: IncomingMessage;
|
|
6
|
+
/** Resolution captured at receipt time so delayed batches cannot re-route. */
|
|
7
|
+
resolution: SessionResolution;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The narrow surface the batcher needs from the Agent. Kept minimal so the
|
|
11
|
+
* settle/coalesce state machine can be unit-tested without a full Agent.
|
|
12
|
+
*/
|
|
13
|
+
export interface InboundBatcherHost {
|
|
14
|
+
/** Serialize a task on the session's FIFO queue (all ingress paths share it). */
|
|
15
|
+
enqueueForSession(sessionKey: string, task: () => Promise<void>): Promise<unknown>;
|
|
16
|
+
/** Hand a drained batch (1+ messages) to the turn pipeline. */
|
|
17
|
+
processInboundItems(items: InboundItem[], steer?: boolean): Promise<void>;
|
|
18
|
+
/** Is a live turn currently in flight on this session? (steering target check) */
|
|
19
|
+
hasBusyLiveSession(sessionKey: string): boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Per-session inbound message batching. Messages that pile up behind an
|
|
23
|
+
* in-flight turn are merged into a single follow-up turn so the agent sees
|
|
24
|
+
* them together (e.g. "do X" → "wait" → "nevermind" all become one prompt).
|
|
25
|
+
* iMessage additionally gets a settle window so provider-side fragments
|
|
26
|
+
* coalesce before they reach the model.
|
|
27
|
+
*/
|
|
28
|
+
export declare class InboundBatcher {
|
|
29
|
+
private readonly host;
|
|
30
|
+
private pendingBatches;
|
|
31
|
+
private pendingBatchSettleUntil;
|
|
32
|
+
private pendingBatchSettleStartedAt;
|
|
33
|
+
private pendingBatchDrainScheduled;
|
|
34
|
+
private lastImessageReceiptAt;
|
|
35
|
+
constructor(host: InboundBatcherHost);
|
|
36
|
+
/**
|
|
37
|
+
* Queue a message for its session. For DMs and passive groups
|
|
38
|
+
* (`canCoalesce`), messages that arrive in quick succession are coalesced
|
|
39
|
+
* into one turn. Mention-required groups bypass coalescing because
|
|
40
|
+
* per-message mention filtering would be lost.
|
|
41
|
+
*
|
|
42
|
+
* Fire-and-forget: returns as soon as the message is queued, NOT when the
|
|
43
|
+
* SDK turn completes. If a caller (e.g. a channel adapter) awaits this,
|
|
44
|
+
* that's fine — they don't block the next ingress on an in-flight turn,
|
|
45
|
+
* which is what lets rapid messages pile up for the queue to coalesce.
|
|
46
|
+
*/
|
|
47
|
+
enqueue(sessionKey: string, channel: Channel, message: IncomingMessage, canCoalesce: boolean, resolution: SessionResolution): void;
|
|
48
|
+
private settleMs;
|
|
49
|
+
private maxSettleMs;
|
|
50
|
+
private logImessageReceipt;
|
|
51
|
+
private waitForBatchSettle;
|
|
52
|
+
private drainPendingBatch;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=inbound-batcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inbound-batcher.d.ts","sourceRoot":"","sources":["../../src/agent/inbound-batcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAGrE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEtD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,eAAe,CAAC;IACzB,8EAA8E;IAC9E,UAAU,EAAE,iBAAiB,CAAC;CAC/B;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,iFAAiF;IACjF,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnF,+DAA+D;IAC/D,mBAAmB,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1E,kFAAkF;IAClF,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC;CACjD;AAED;;;;;;GAMG;AACH,qBAAa,cAAc;IASb,OAAO,CAAC,QAAQ,CAAC,IAAI;IANjC,OAAO,CAAC,cAAc,CAAoC;IAC1D,OAAO,CAAC,uBAAuB,CAA6B;IAC5D,OAAO,CAAC,2BAA2B,CAA6B;IAChE,OAAO,CAAC,0BAA0B,CAAqB;IACvD,OAAO,CAAC,qBAAqB,CAA6B;gBAE7B,IAAI,EAAE,kBAAkB;IAErD;;;;;;;;;;OAUG;IACH,OAAO,CACL,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,eAAe,EACxB,WAAW,EAAE,OAAO,EACpB,UAAU,EAAE,iBAAiB,GAC5B,IAAI;IA2DP,OAAO,CAAC,QAAQ;IAMhB,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,kBAAkB;YA0BZ,kBAAkB;YAYlB,iBAAiB;CAyBhC"}
|