Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1233-early-compaction.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A Bot condenses its conversation earlier: the steward's check line is now 45% of the model's usable window instead of 80%. Every call re-sends the fixed block and the conversation is most of each prompt, so condensing sooner cuts the prompt of a long turn by about a quarter; the cost is that a checkpoint arrives sooner and a condensed conversation keeps less detail. `checkRatio` still overrides it in the profile ([#1233](https://github.com/ThinkFlowLab/FlyBot/issues/1233)).
2 changes: 1 addition & 1 deletion core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Options go in the profile's `cordis.patch.yml`, under the `bot` entry. All of th
config:
workspace: /path/to/workspace # where the Bots work; default <home>/workspace
home: /path/to/bot-data # where the team is saved; default $DSH_HOME/bot
checkRatio: 0.8 # share of the model's usable window where the steward decides
checkRatio: 0.45 # share of the model's usable window where the steward decides; default 0.45
browser: true # force the Bot browser on or off; default on in Desktop only
memory: true # false turns memory off: no memory tools, section or panel
memoryReview: true # false leaves memory to the Bots' own remember calls
Expand Down
4 changes: 3 additions & 1 deletion core/src/host/steward.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import { estimateTokens } from './text.js'

// For a model that declares no window.
export const DEFAULT_WINDOW = 500_000
export const DEFAULT_CHECK_RATIO = 0.8
// Earlier than upstream's 0.8: the fixed block is re-sent on every call and the
// history is most of each prompt, so condensing sooner costs the prompt less (#1233).
export const DEFAULT_CHECK_RATIO = 0.45
// Context sizes between requests are estimates, so the hard line stays a little
// below what the model can actually take.
const HARD_SHARE = 0.95
Expand Down
2 changes: 1 addition & 1 deletion core/test/steward.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ test('a model without a window counts as the default, and odd settings fall back
// A reply that would take the whole window still leaves half of it.
assert.equal(linesFor({ window: 100_000, reserve: 200_000 }).usable, 50_000)
for (const checkRatio of [0, 1, 1.5, -1, 'x', undefined]) {
assert.equal(linesFor({ window: 100_000, reserve: 0, checkRatio }).check, 80_000, String(checkRatio))
assert.equal(linesFor({ window: 100_000, reserve: 0, checkRatio }).check, 45_000, String(checkRatio))
}
// The check line never passes the hard line.
assert.equal(linesFor({ window: 100_000, reserve: 0, checkRatio: 0.99 }).check, 95_000)
Expand Down
1 change: 1 addition & 0 deletions docs/subsystems/core-host.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ Work a member hands a Bot with `message_bot` from its group Session runs in a hi
- A friend's line in a Bot's own chat (#420): `deliver()` (`noteFriendLine`) stamps the receiving Bot with `friendLineAt` (published in the roster, so the save moves the revision) and keeps the senders seen per Bot in `state.remoteContacts[botId][handle]` (host only, capped at 200, dropped with the Bot). A sender's first line carries `source.remote.firstContact`. Group lines, and re-deliveries (`deferred`, `resumed`, `retried`, `resent`), stamp nothing. Covered by `core/test/flybot.test.js`.
- **Pickup after a restart** (`resume.js`): every running Bot turn and every delivered-but-not-taken message is noted in `inflight.json`. At the next start a noted turn is picked up once only if a restart really cut it: the durable Session log's `turn/end` for that turn is `interrupted` (or an abort caused by `disposed`, or there is no end). A turn that finished while this plugin was not listening — `completed`, `blocked`, `max-tokens`, `forked`, or aborted by the user / hook / parent — is not repeated; a `turn/start` seen after the start means the noted turn ended too. Notes from a build before the record carried `turn` keep the old behaviour. A pickup is confirmed before its note is dropped: the notes read at start are seeded into the live `turns`/`queued` (so the file keeps everything still owed), a turn's note stays while its pickup runs and is dropped only once handed over (unless the resumed turn has written its own note first), and a queued message whose resend fails is kept for the next start, an exchange is noted once (a resend delivers the `exchangeId` the notes already hold, and noting it again left a copy behind after a turn took it up, so the start after that sent the message once more), and a turn taking up a message forgets every note of its exchange; a crash or a failed delivery during the pickup no longer loses the work — a pickup is at-least-once, not once: a crash between handing the turn over and the write that drops its note repeats that pickup once, because the `taken` set and the inbox that would tell the next start otherwise are process-local. That is the safe side of the trade, a repeated turn the user can see instead of work the user was told about going silently dead. Covered by `core/test/follow-through.test.js`.
- A repeated Bot-to-Bot line becomes one turn (#641): `deliver()` keys the last 60 s of Bot-to-Bot text by sender + recipient Session + the text with its whitespace collapsed (`DEDUPE_MS`, `duplicateDelivery`), drops a repeat before it is recorded as an exchange or handed to the Session, and journals the drop as `delivery: dedupe <sender> -> <session>` in the host warning log. The key includes the Session, so the same line to another Session (a group copy, another Bot) is its own delivery; `deferred` / `resumed` / `retried` / `resent` carry on a message the Session already has and are never judged, and a friend's line, the user's own message and a group round are not Bot-to-Bot. Routing (parent and direct children) and the existing unread-queue refusal are unchanged, and the map is process-local and capped at 500 entries. Covered by `core/test/repeated-delivery.test.js`.
- **The check line is 45% of the usable window, not 80%** (`steward.js` `linesFor`): a Bot's conversation is condensed once its estimated size passes `DEFAULT_CHECK_RATIO` of the model's declared window less the room its reply needs, so the conversation is condensed earlier than upstream ds-bot does. The share is a floor on the prompt of a long turn, because every call re-sends the fixed block and the history is most of each prompt (#796); the trade is a checkpoint sooner and less detail kept (#1233). `checkRatio` in the profile still sets the share, and the hard line stays at 95% of the usable window, so a share at or above 0.95 can never lift the check line above it. Covered by `core/test/steward.test.js`.
- **A checkpoint is judged by the prompt that is left** (`parts.js` `pressureLeft`): the token meter stays anchored on the provider usage from before the span was replaced, and its surface delta only subtracts a 4-characters-per-token estimate of the removed text. A denser report — the mock's 3 characters per token, or CJK on a real model — can therefore still read above the check line after the checkpoint has replaced the span, and the part would move on even though the steward said to condense in place. After a compaction, the part stays when the heuristic size of the remaining surface plus the tool schemas is under the check line. A surface that is still over it moves on, as before. The decision to cross the line in the first place still uses the anchored total (#917).
- **A part is committed before its handoff runs** (`parts.js` `moveOn`): the Bot's `sessionId` / `room.sessions`, the new segment and the `rescued` mark are saved first, so the turn the handoff starts can never see an uncommitted part — a failure handler that did would start a second switch against it and lose the previous part's note, and the rescue would not know it may only happen once. The handoff package is written next; if that fails, the pointer is rolled back (best effort saved, the fresh Session archived, the rescued mark dropped), so the old part is current and the finalizer hands the input on there. Two windows stay open, and both are written down rather than discovered later: a crash after that save and before the handoff package is flushed leaves the fresh part current with no package — the stopped turn's input still goes to it, so no work is lost, but the note is, and the rollback never runs; and the rollback's own save is best effort (`rt.save().catch(() => {})`), so a failing rollback leaves the file showing the fresh part while memory holds the old one, and the next start behaves like the first window. Neither is a reason to clear the pointer before the save: a note lost this way is a part without a handoff note, while clearing first loses the handoff package itself. The move is marked done only after the stopped turn's input and the old inbox messages are forwarded; a switch that never starts (no place, another switch, a read-only host) lets the step continue instead of rejecting its messages, which DSH has already taken out of the inbox. An armed scheduled task keeps the earlier Session active and DSH refuses to archive it, so the switch keeps the `moveSchedules` promise and waits for the move before archiving the earlier part; when the move failed and the refusal reports `schedule` as the only activity, the plain archive is retried with `{ stopActivity: true }` (the way a deletion does, `roster.js:450`), so a finished part is never left active with a task nobody sees; a refusal caused by a job, turn or subagent is not retried, because `stopActivity` would stop that too (#896). Covered by `core/test/work-dir.test.js`, `core/test/part-archive.test.js` and `core/test/bot-messages.test.js`.

Expand Down
29 changes: 28 additions & 1 deletion patches/ds-bot/0001-core-fixes.patch
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ diff --git a/README.md b/README.md
- **Usage**: Settings → Usage shows token usage by time (24 hours to 12 months, or days picked on a calendar), by Bot and by model, with a page for each Bot and each model. Calls that a Session logs are read from the logs, so older chats count too. Calls that leave no log (checkpoints, handoff notes, the steward, image readings, dsh titles and compaction) are counted while they stream, from the time the plugin runs. Tokens only, no cost.
- **Make it yours**: name, role, instructions, model and look (20 characters and 12 colors, or your own photo) for each Bot; a theme, an accent color and an avatar animation level (quiet, normal or lively) for the team. A pinned Bot or group chat shows a small pushpin in the sidebar. Settings has these pages: General (look), Bots (Main Bots and each Bot's settings), Group chats (each group's settings), Connectors, Capabilities, Scheduled tasks, Usage, Secrets, Feedback and About DS Bot (versions, updates and links).

@@ -71,9 +71,12 @@ Options go in the profile's `cordis.patch.yml`, under the `bot` entry. All of th
@@ -69,11 +69,14 @@ Options go in the profile's `cordis.patch.yml`, under the `bot` entry. All of th
- checkRatio: 0.8 # share of the model's usable window where the steward decides
+ checkRatio: 0.45 # share of the model's usable window where the steward decides; default 0.45
browser: true # force the Bot browser on or off; default on in Desktop only
memory: true # false turns memory off: no memory tools, section or panel
memoryReview: true # false leaves memory to the Bots' own remember calls
diagnosticsFile: false # keep the error log in memory only; default <home>/logs/diagnostics.jsonl
Expand Down Expand Up @@ -9271,3 +9274,27 @@ diff --git a/test/work-dir.test.js b/test/work-dir.test.js
assert.deepEqual(third.created, [{ cwd: third.project }])
assert.match(await third.tool('list_bots', {}, 'a'), new RegExp(`- Builder \\(Build, works in ${escapeRegExp(third.project)}\\)`))
})
diff --git a/src/host/steward.js b/src/host/steward.js
--- a/src/host/steward.js
+++ b/src/host/steward.js
@@ -11,6 +11,8 @@
// For a model that declares no window.
export const DEFAULT_WINDOW = 500_000
-export const DEFAULT_CHECK_RATIO = 0.8
+// Earlier than upstream's 0.8: the fixed block is re-sent on every call and the
+// history is most of each prompt, so condensing sooner costs the prompt less (#1233).
+export const DEFAULT_CHECK_RATIO = 0.45
// Context sizes between requests are estimates, so the hard line stays a little
// below what the model can actually take.
const HARD_SHARE = 0.95
diff --git a/test/steward.test.js b/test/steward.test.js
--- a/test/steward.test.js
+++ b/test/steward.test.js
@@ -17,6 +17,6 @@ test('a model without a window counts as the default, and odd settings fall back', () => {
assert.equal(linesFor({ window: 100_000, reserve: 200_000 }).usable, 50_000)
for (const checkRatio of [0, 1, 1.5, -1, 'x', undefined]) {
- assert.equal(linesFor({ window: 100_000, reserve: 0, checkRatio }).check, 80_000, String(checkRatio))
+ assert.equal(linesFor({ window: 100_000, reserve: 0, checkRatio }).check, 45_000, String(checkRatio))
}
// The check line never passes the hard line.
assert.equal(linesFor({ window: 100_000, reserve: 0, checkRatio: 0.99 }).check, 95_000)
4 changes: 3 additions & 1 deletion patches/ds-bot/topics.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@
"test/follow-through.test.js",
"src/host/images.js",
"test/images.test.js",
"test/session-identity.test.js"
"test/session-identity.test.js",
"src/host/steward.js",
"test/steward.test.js"
],
"0002-flybot-api-host": [
"index.js",
Expand Down
Loading