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/1634-busy-ack-placeholder.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The busy ack now quotes your own message exactly: typing `{d}` or `{t}` in a line no longer makes the ack swap in the running tool's words (or drop it) ([#1634](https://github.com/ThinkFlowLab/FlyBot/issues/1634)).
4 changes: 3 additions & 1 deletion core/src/host/busy-ack.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ export function composeBusyAck({ message, tool, uiLanguage, seed = 0, previous }
const doing = doingOf(tool, lang)
const kind = topic && doing ? 'topicDoing' : topic ? 'topic' : doing ? 'doing' : 'bare'
const pool = TEMPLATES[lang][kind]
const fill = index => pool[index % pool.length].replaceAll('{t}', topic).replaceAll('{d}', doing ?? '')
// One pass only: the topic is the person's own text, so a `{t}`/`{d}` they typed
// must not be rescanned as template markup after it is inserted (#1634).
const fill = index => pool[index % pool.length].replace(/\{(t|d)\}/g, (_, key) => (key === 't' ? topic : (doing ?? '')))
const start = Math.abs(Math.trunc(seed)) % pool.length
for (let step = 0; step < pool.length; step += 1) {
const text = fill(start + step)
Expand Down
27 changes: 27 additions & 0 deletions core/test/busy-ack.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,30 @@ test('consecutive acks in one chat differ, and clear on turn/end and agent idle'
emit('agent/status', { agent: { session: { id: 'a' } }, status: 'idle' })
assert.deepEqual(rt.busyAcksView(), {})
})

// The quoted topic is the person's own text: a `{d}` / `{t}` they typed is not template
// markup and must be echoed unchanged. Messages stay short on purpose — topicOf() clips
// at TOPIC_MAX, and a long sentence would hide the rewrite behind the ellipsis.
test('a {d} typed in the message is quoted back verbatim, with and without a running tool', () => {
for (const message of ['改 {d} 这行', 'fix "{d}" here']) {
for (const tool of ['bash', undefined]) {
for (let seed = 0; seed < 6; seed += 1) {
const text = composeBusyAck({ message, tool, seed, uiLanguage: 'zh-CN' })
assert.ok(text.includes('{d}'), `${message} / ${tool ?? 'no tool'} / seed ${seed}: ${text}`)
assert.ok(text.includes(topicOf(message, ackLanguage(message, 'zh-CN'))), `${message} / seed ${seed}: ${text}`)
}
}
}
})

test('a {t} typed in the message is quoted back verbatim, with and without a running tool', () => {
for (const message of ['改 {t} 这行', 'fix "{t}" here']) {
for (const tool of ['bash', undefined]) {
for (let seed = 0; seed < 6; seed += 1) {
const text = composeBusyAck({ message, tool, seed, uiLanguage: 'zh-CN' })
assert.ok(text.includes('{t}'), `${message} / ${tool ?? 'no tool'} / seed ${seed}: ${text}`)
assert.ok(text.includes(topicOf(message, ackLanguage(message, 'zh-CN'))), `${message} / seed ${seed}: ${text}`)
}
}
}
})
2 changes: 1 addition & 1 deletion docs/subsystems/core-host.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,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`.
## Busy-send acknowledgment (#1584)

When a human `user/message` lands in a Bot's own chat while that Session already has a live turn, the host records a short spoken acknowledgment on the live payload (`busyAcks`). The line is composed on the host with no model call ([#1629](https://github.com/ThinkFlowLab/FlyBot/issues/1629)): it echoes a clipped topic from the message and the running tool in words (`bash` → running a command), follows the message's language, rotates its wording, and never repeats the chat's previous ack. The client draws it as one bot bubble (and also from a local pending queue/steer submission) so the chat is not silent while a blocking job wait holds the next step. Related but separate: [#1509](https://github.com/ThinkFlowLab/FlyBot/issues/1509) (Steer does not abort a tool wait). Host: `core/src/host/busy-ack.js`. Client: `core/src/client/busy-ack.js`. Tests: `core/test/busy-ack.test.js`, `core/test/busy-ack-client.test.js`.
When a human `user/message` lands in a Bot's own chat while that Session already has a live turn, the host records a short spoken acknowledgment on the live payload (`busyAcks`). The line is composed on the host with no model call ([#1629](https://github.com/ThinkFlowLab/FlyBot/issues/1629)): it echoes a clipped topic from the message verbatim — a `{t}` / `{d}` the person typed is quoted as typed, not treated as markup ([#1634](https://github.com/ThinkFlowLab/FlyBot/issues/1634)) — and the running tool in words (`bash` → running a command), follows the message's language, rotates its wording, and never repeats the chat's previous ack. The client draws it as one bot bubble (and also from a local pending queue/steer submission) so the chat is not silent while a blocking job wait holds the next step. Related but separate: [#1509](https://github.com/ThinkFlowLab/FlyBot/issues/1509) (Steer does not abort a tool wait). Host: `core/src/host/busy-ack.js`. Client: `core/src/client/busy-ack.js`. Tests: `core/test/busy-ack.test.js`, `core/test/busy-ack-client.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`.
Expand Down
4 changes: 3 additions & 1 deletion patches/ds-bot/files/src/host/busy-ack.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ export function composeBusyAck({ message, tool, uiLanguage, seed = 0, previous }
const doing = doingOf(tool, lang)
const kind = topic && doing ? 'topicDoing' : topic ? 'topic' : doing ? 'doing' : 'bare'
const pool = TEMPLATES[lang][kind]
const fill = index => pool[index % pool.length].replaceAll('{t}', topic).replaceAll('{d}', doing ?? '')
// One pass only: the topic is the person's own text, so a `{t}`/`{d}` they typed
// must not be rescanned as template markup after it is inserted (#1634).
const fill = index => pool[index % pool.length].replace(/\{(t|d)\}/g, (_, key) => (key === 't' ? topic : (doing ?? '')))
const start = Math.abs(Math.trunc(seed)) % pool.length
for (let step = 0; step < pool.length; step += 1) {
const text = fill(start + step)
Expand Down
27 changes: 27 additions & 0 deletions patches/ds-bot/files/test/busy-ack.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,30 @@ test('consecutive acks in one chat differ, and clear on turn/end and agent idle'
emit('agent/status', { agent: { session: { id: 'a' } }, status: 'idle' })
assert.deepEqual(rt.busyAcksView(), {})
})

// The quoted topic is the person's own text: a `{d}` / `{t}` they typed is not template
// markup and must be echoed unchanged. Messages stay short on purpose — topicOf() clips
// at TOPIC_MAX, and a long sentence would hide the rewrite behind the ellipsis.
test('a {d} typed in the message is quoted back verbatim, with and without a running tool', () => {
for (const message of ['改 {d} 这行', 'fix "{d}" here']) {
for (const tool of ['bash', undefined]) {
for (let seed = 0; seed < 6; seed += 1) {
const text = composeBusyAck({ message, tool, seed, uiLanguage: 'zh-CN' })
assert.ok(text.includes('{d}'), `${message} / ${tool ?? 'no tool'} / seed ${seed}: ${text}`)
assert.ok(text.includes(topicOf(message, ackLanguage(message, 'zh-CN'))), `${message} / seed ${seed}: ${text}`)
}
}
}
})

test('a {t} typed in the message is quoted back verbatim, with and without a running tool', () => {
for (const message of ['改 {t} 这行', 'fix "{t}" here']) {
for (const tool of ['bash', undefined]) {
for (let seed = 0; seed < 6; seed += 1) {
const text = composeBusyAck({ message, tool, seed, uiLanguage: 'zh-CN' })
assert.ok(text.includes('{t}'), `${message} / ${tool ?? 'no tool'} / seed ${seed}: ${text}`)
assert.ok(text.includes(topicOf(message, ackLanguage(message, 'zh-CN'))), `${message} / seed ${seed}: ${text}`)
}
}
}
})
Loading