fix/heartbeat ok delivery filter (#246)
* cron: skip delivery for HEARTBEAT_OK responses When an isolated cron job has deliver:true, skip message delivery if the response is just HEARTBEAT_OK (or contains HEARTBEAT_OK at edges with short remaining content <= 30 chars). This allows cron jobs to silently ack when nothing to report but still deliver actual content when there is something meaningful to say. Media is still delivered even if text is HEARTBEAT_OK, since the presence of media indicates there's something to share. * fix(heartbeat): make ack padding configurable * chore(deps): update to latest --------- Co-authored-by: Josh Lehman <josh@martian.engineering>
This commit is contained in:
committed by
GitHub
parent
dae7f560a5
commit
f790f3f3ba
@@ -28,11 +28,13 @@
|
|||||||
- Block streaming: preserve leading indentation in block replies (lists, indented fences).
|
- Block streaming: preserve leading indentation in block replies (lists, indented fences).
|
||||||
- Docs: document systemd lingering and logged-in session requirements on macOS/Windows.
|
- Docs: document systemd lingering and logged-in session requirements on macOS/Windows.
|
||||||
- Auto-reply: unify tool/block/final delivery across providers and apply consistent heartbeat/prefix handling. Thanks @MSch for PR #225 (superseded commit 92c953d0749143eb2a3f31f3cd6ad0e8eabf48c3).
|
- Auto-reply: unify tool/block/final delivery across providers and apply consistent heartbeat/prefix handling. Thanks @MSch for PR #225 (superseded commit 92c953d0749143eb2a3f31f3cd6ad0e8eabf48c3).
|
||||||
|
- Heartbeat: make HEARTBEAT_OK ack padding configurable across heartbeat and cron delivery. (#238) — thanks @jalehman
|
||||||
|
|
||||||
### Maintenance
|
### Maintenance
|
||||||
- Deps: bump pi-* stack, Slack SDK, discord-api-types, file-type, zod, and Biome.
|
- Deps: bump pi-* stack, Slack SDK, discord-api-types, file-type, zod, and Biome.
|
||||||
- Skills: add CodexBar model usage helper with macOS requirement metadata.
|
- Skills: add CodexBar model usage helper with macOS requirement metadata.
|
||||||
- Lint: organize imports and wrap long lines in reply commands.
|
- Lint: organize imports and wrap long lines in reply commands.
|
||||||
|
- Deps: update to latest across the repo.
|
||||||
|
|
||||||
## 2026.1.5-3
|
## 2026.1.5-3
|
||||||
|
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ Example:
|
|||||||
|
|
||||||
When `agent.heartbeat.every` is set to a positive interval, CLAWDBOT periodically runs a heartbeat prompt (default: `HEARTBEAT`).
|
When `agent.heartbeat.every` is set to a positive interval, CLAWDBOT periodically runs a heartbeat prompt (default: `HEARTBEAT`).
|
||||||
|
|
||||||
- If the agent replies with `HEARTBEAT_OK` (exact token), CLAWDBOT suppresses outbound delivery for that heartbeat.
|
- If the agent replies with `HEARTBEAT_OK` (optionally with short padding; see `agent.heartbeat.ackMaxChars`), CLAWDBOT suppresses outbound delivery for that heartbeat.
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -560,6 +560,7 @@ Z.AI models are available as `zai/<model>` (e.g. `zai/glm-4.7`) and require
|
|||||||
- `target`: optional delivery channel (`last`, `whatsapp`, `telegram`, `discord`, `imessage`, `none`). Default: `last`.
|
- `target`: optional delivery channel (`last`, `whatsapp`, `telegram`, `discord`, `imessage`, `none`). Default: `last`.
|
||||||
- `to`: optional recipient override (E.164 for WhatsApp, chat id for Telegram).
|
- `to`: optional recipient override (E.164 for WhatsApp, chat id for Telegram).
|
||||||
- `prompt`: optional override for the heartbeat body (default: `HEARTBEAT`).
|
- `prompt`: optional override for the heartbeat body (default: `HEARTBEAT`).
|
||||||
|
- `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery (default: 30).
|
||||||
|
|
||||||
`agent.bash` configures background bash defaults:
|
`agent.bash` configures background bash defaults:
|
||||||
- `backgroundMs`: time before auto-background (ms, default 10000)
|
- `backgroundMs`: time before auto-background (ms, default 10000)
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ surface anything that needs attention without spamming the user.
|
|||||||
|
|
||||||
## Prompt contract
|
## Prompt contract
|
||||||
- Heartbeat body defaults to `HEARTBEAT` (configurable via `agent.heartbeat.prompt`).
|
- Heartbeat body defaults to `HEARTBEAT` (configurable via `agent.heartbeat.prompt`).
|
||||||
- If nothing needs attention, the model should reply **exactly** `HEARTBEAT_OK`.
|
- If nothing needs attention, the model should reply `HEARTBEAT_OK`.
|
||||||
- During heartbeat runs, Clawdbot treats `HEARTBEAT_OK` as an ack when it appears at
|
- During heartbeat runs, Clawdbot treats `HEARTBEAT_OK` as an ack when it appears at
|
||||||
the **start or end** of the reply. Clawdbot strips the token and discards the
|
the **start or end** of the reply. Clawdbot strips the token and discards the
|
||||||
reply if the remaining content is **≤ 30 characters**.
|
reply if the remaining content is **≤ `ackMaxChars`** (default: 30).
|
||||||
- If `HEARTBEAT_OK` is in the **middle** of a reply, it is not treated specially.
|
- If `HEARTBEAT_OK` is in the **middle** of a reply, it is not treated specially.
|
||||||
- For alerts, do **not** include `HEARTBEAT_OK`; return only the alert text.
|
- For alerts, do **not** include `HEARTBEAT_OK`; return only the alert text.
|
||||||
|
|
||||||
@@ -39,7 +39,8 @@ and final replies:
|
|||||||
model: "anthropic/claude-opus-4-5",
|
model: "anthropic/claude-opus-4-5",
|
||||||
target: "last", // last | whatsapp | telegram | none
|
target: "last", // last | whatsapp | telegram | none
|
||||||
to: "+15551234567", // optional override for whatsapp/telegram
|
to: "+15551234567", // optional override for whatsapp/telegram
|
||||||
prompt: "HEARTBEAT" // optional override
|
prompt: "HEARTBEAT", // optional override
|
||||||
|
ackMaxChars: 30 // max chars allowed after HEARTBEAT_OK
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,6 +56,7 @@ and final replies:
|
|||||||
- `none`: do not deliver externally; output stays in the session (WebChat-visible).
|
- `none`: do not deliver externally; output stays in the session (WebChat-visible).
|
||||||
- `to`: optional recipient override (E.164 for WhatsApp, chat id for Telegram).
|
- `to`: optional recipient override (E.164 for WhatsApp, chat id for Telegram).
|
||||||
- `prompt`: optional override for the heartbeat body (default: `HEARTBEAT`).
|
- `prompt`: optional override for the heartbeat body (default: `HEARTBEAT`).
|
||||||
|
- `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery (default: 30).
|
||||||
|
|
||||||
## Behavior
|
## Behavior
|
||||||
- Runs in the main session (`main`, or `global` when scope is global).
|
- Runs in the main session (`main`, or `global` when scope is global).
|
||||||
|
|||||||
@@ -111,8 +111,8 @@
|
|||||||
"qrcode-terminal": "^0.12.0",
|
"qrcode-terminal": "^0.12.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"tslog": "^4.10.2",
|
"tslog": "^4.10.2",
|
||||||
"undici": "^7.16.0",
|
"undici": "^7.18.0",
|
||||||
"ws": "^8.18.3",
|
"ws": "^8.19.0",
|
||||||
"zod": "^4.3.5"
|
"zod": "^4.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
"lucide": "^0.562.0",
|
"lucide": "^0.562.0",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"ollama": "^0.6.3",
|
"ollama": "^0.6.3",
|
||||||
"oxlint": "^1.36.0",
|
"oxlint": "^1.37.0",
|
||||||
"oxlint-tsgolint": "^0.10.1",
|
"oxlint-tsgolint": "^0.10.1",
|
||||||
"quicktype-core": "^23.2.6",
|
"quicktype-core": "^23.2.6",
|
||||||
"rolldown": "1.0.0-beta.58",
|
"rolldown": "1.0.0-beta.58",
|
||||||
|
|||||||
154
pnpm-lock.yaml
generated
154
pnpm-lock.yaml
generated
@@ -27,13 +27,13 @@ importers:
|
|||||||
version: 1.3.4
|
version: 1.3.4
|
||||||
'@mariozechner/pi-agent-core':
|
'@mariozechner/pi-agent-core':
|
||||||
specifier: ^0.36.0
|
specifier: ^0.36.0
|
||||||
version: 0.36.0(ws@8.18.3)(zod@4.3.5)
|
version: 0.36.0(ws@8.19.0)(zod@4.3.5)
|
||||||
'@mariozechner/pi-ai':
|
'@mariozechner/pi-ai':
|
||||||
specifier: ^0.36.0
|
specifier: ^0.36.0
|
||||||
version: 0.36.0(patch_hash=628fb051b6f4886984a846a5ee7aa0a571c3360d35b8d114e4684e5edcd100c5)(ws@8.18.3)(zod@4.3.5)
|
version: 0.36.0(patch_hash=628fb051b6f4886984a846a5ee7aa0a571c3360d35b8d114e4684e5edcd100c5)(ws@8.19.0)(zod@4.3.5)
|
||||||
'@mariozechner/pi-coding-agent':
|
'@mariozechner/pi-coding-agent':
|
||||||
specifier: ^0.36.0
|
specifier: ^0.36.0
|
||||||
version: 0.36.0(ws@8.18.3)(zod@4.3.5)
|
version: 0.36.0(ws@8.19.0)(zod@4.3.5)
|
||||||
'@mariozechner/pi-tui':
|
'@mariozechner/pi-tui':
|
||||||
specifier: ^0.36.0
|
specifier: ^0.36.0
|
||||||
version: 0.36.0
|
version: 0.36.0
|
||||||
@@ -110,11 +110,11 @@ importers:
|
|||||||
specifier: ^4.10.2
|
specifier: ^4.10.2
|
||||||
version: 4.10.2
|
version: 4.10.2
|
||||||
undici:
|
undici:
|
||||||
specifier: ^7.16.0
|
specifier: ^7.18.0
|
||||||
version: 7.16.0
|
version: 7.18.0
|
||||||
ws:
|
ws:
|
||||||
specifier: ^8.18.3
|
specifier: ^8.19.0
|
||||||
version: 8.18.3
|
version: 8.19.0
|
||||||
zod:
|
zod:
|
||||||
specifier: ^4.3.5
|
specifier: ^4.3.5
|
||||||
version: 4.3.5
|
version: 4.3.5
|
||||||
@@ -171,8 +171,8 @@ importers:
|
|||||||
specifier: ^0.6.3
|
specifier: ^0.6.3
|
||||||
version: 0.6.3
|
version: 0.6.3
|
||||||
oxlint:
|
oxlint:
|
||||||
specifier: ^1.36.0
|
specifier: ^1.37.0
|
||||||
version: 1.36.0(oxlint-tsgolint@0.10.1)
|
version: 1.37.0(oxlint-tsgolint@0.10.1)
|
||||||
oxlint-tsgolint:
|
oxlint-tsgolint:
|
||||||
specifier: ^0.10.1
|
specifier: ^0.10.1
|
||||||
version: 0.10.1
|
version: 0.10.1
|
||||||
@@ -870,43 +870,43 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@oxlint/darwin-arm64@1.36.0':
|
'@oxlint/darwin-arm64@1.37.0':
|
||||||
resolution: {integrity: sha512-MJkj82GH+nhvWKJhSIM6KlZ8tyGKdogSQXtNdpIyP02r/tTayFJQaAEWayG2Jhsn93kske+nimg5MYFhwO/rlg==}
|
resolution: {integrity: sha512-qDa8qf4Th3sbk6P6wRbsv5paGeZ8EEOy8PtnT2IkAYSzjDHavw8nMK/lQvf6uS7LArjcmOfM1Y3KnZUFoNZZqg==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@oxlint/darwin-x64@1.36.0':
|
'@oxlint/darwin-x64@1.37.0':
|
||||||
resolution: {integrity: sha512-VvEhfkqj/99dCTqOcfkyFXOSbx4lIy5u2m2GHbK4WCMDySokOcMTNRHGw8fH/WgQ5cDrDMSTYIGQTmnBGi9tiQ==}
|
resolution: {integrity: sha512-FM0h0KyOQ4HCdhIX1ne6d80BxRra75h1ORce0jYNwQ49HT4RU8+9ywSMC7rQ79xWsmaahvkQPB7tMPyfjsQwAg==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@oxlint/linux-arm64-gnu@1.36.0':
|
'@oxlint/linux-arm64-gnu@1.37.0':
|
||||||
resolution: {integrity: sha512-EMx92X5q+hHc3olTuj/kgkx9+yP0p/AVs4yvHbUfzZhBekXNpUWxWvg4hIKmQWn+Ee2j4o80/0ACGO0hDYJ9mg==}
|
resolution: {integrity: sha512-2axK0lftGwM6Q7wOuY2sassUqa4MKrG3iemVVyEpXzJ6g5QosxhCoFPp9v81/gmLT5kAdd2gskoDcfpDJliDNw==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@oxlint/linux-arm64-musl@1.36.0':
|
'@oxlint/linux-arm64-musl@1.37.0':
|
||||||
resolution: {integrity: sha512-7YCxtrPIctVYLqWrWkk8pahdCxch6PtsaucfMLC7TOlDt4nODhnQd4yzEscKqJ8Gjrw1bF4g+Ngob1gB+Qr9Fw==}
|
resolution: {integrity: sha512-f3YROyGMIdUeXx0yD7RsAUBzBvD222D4l2GQRYF3AMxyp9mya17Rq/3wNLR4JDnAnboOul3DAEKNm+09lo3uZw==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@oxlint/linux-x64-gnu@1.36.0':
|
'@oxlint/linux-x64-gnu@1.37.0':
|
||||||
resolution: {integrity: sha512-lnaJVlx5r3NWmoOMesfQXJSf78jHTn8Z+sdAf795Kgteo72+qGC1Uax2SToCJVN2J8PNG3oRV5bLriiCNR2i6Q==}
|
resolution: {integrity: sha512-FANOdOVQ2c4acYLM0dvtSoKELHSSnDBxDdm8OlXNzSRanQILrNpLgUqCXHFsfiHipFfNzz3Z417PxV6X4aBYog==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@oxlint/linux-x64-musl@1.36.0':
|
'@oxlint/linux-x64-musl@1.37.0':
|
||||||
resolution: {integrity: sha512-AhuEU2Qdl66lSfTGu/Htirq8r/8q2YnZoG3yEXLMQWnPMn7efy8spD/N1NA7kH0Hll+cdfwgQkQqC2G4MS2lPQ==}
|
resolution: {integrity: sha512-eYnSKT9knXdOQ9h+6nSjEHSx0+pW8PkGwtMNGXtCYR+/ZPKYIbtZVS0nZsFy+qizP+TRVSJrgc/JY3Xr0wjcQg==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@oxlint/win32-arm64@1.36.0':
|
'@oxlint/win32-arm64@1.37.0':
|
||||||
resolution: {integrity: sha512-GlWCBjUJY2QgvBFuNRkiRJu7K/djLmM0UQKfZV8IN+UXbP/JbjZHWKRdd4LXlQmzoz7M5Hd6p+ElCej8/90FCg==}
|
resolution: {integrity: sha512-2oHxNc4jcocfNWGWVVWQdEG+reZ5ncBZsmDoICJQ1rbCDx4Yimx8VUf1Ub9cCoJRcPiSLBxMqaeMaDClKixJIQ==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@oxlint/win32-x64@1.36.0':
|
'@oxlint/win32-x64@1.37.0':
|
||||||
resolution: {integrity: sha512-J+Vc00Utcf8p77lZPruQgb0QnQXuKnFogN88kCnOqs2a83I+vTBB8ILr0+L9sTwVRvIDMSC0pLdeQH4svWGFZg==}
|
resolution: {integrity: sha512-w+pBuTjGmGCGPhDjFhj/97K2tlGyq5LKAU6S7FHxROPuJRWJD6uio1L75Lsb8fKhwtw2rm54LLOX30Yi+nILxw==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
@@ -1951,8 +1951,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
|
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
hookified@1.14.0:
|
hookified@1.15.0:
|
||||||
resolution: {integrity: sha512-pi1ynXIMFx/uIIwpWJ/5CEtOHLGtnUB0WhGeeYT+fKcQ+WCQbm3/rrkAXnpfph++PgepNqPdTC2WTj8A6k6zoQ==}
|
resolution: {integrity: sha512-51w+ZZGt7Zw5q7rM3nC4t3aLn/xvKDETsXqMczndvwyVQhAHfUmUuFBRFcos8Iyebtk7OAE9dL26wFNzZVVOkw==}
|
||||||
|
|
||||||
html-escaper@2.0.2:
|
html-escaper@2.0.2:
|
||||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||||
@@ -2418,8 +2418,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-EEHNdo5cW2w1xwYdBQ7d3IXDqWAtMkfVFrh+9gQ4kYbYJwygY4QXSh1eH80/xVipZdVKujAwBgg/nNNHk56kxQ==}
|
resolution: {integrity: sha512-EEHNdo5cW2w1xwYdBQ7d3IXDqWAtMkfVFrh+9gQ4kYbYJwygY4QXSh1eH80/xVipZdVKujAwBgg/nNNHk56kxQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
oxlint@1.36.0:
|
oxlint@1.37.0:
|
||||||
resolution: {integrity: sha512-IicUdXfXgI8OKrDPnoSjvBfeEF8PkKtm+CoLlg4LYe4ypc8U+T4r7730XYshdBGZdelg+JRw8GtCb2w/KaaZvw==}
|
resolution: {integrity: sha512-MAw0JH8M5/vt9E2WxSsmJu53bVLmG6qNlVw1OXFenJYItTPbMBtW7j3n53+tgNhNuxFPundM1DR7V8E39qOOrg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2436,8 +2436,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==}
|
resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
p-queue@9.0.1:
|
p-queue@9.1.0:
|
||||||
resolution: {integrity: sha512-RhBdVhSwJb7Ocn3e8ULk4NMwBEuOxe+1zcgphUy9c2e5aR/xbEsdVXxHJ3lynw6Qiqu7OINEyHlZkiblEpaq7w==}
|
resolution: {integrity: sha512-O/ZPaXuQV29uSLbxWBGGZO1mCQXV2BLIwUr59JUU9SoH76mnYvtms7aafH/isNSNGwuEfP6W/4xD0/TJXxrizw==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
p-retry@4.6.2:
|
p-retry@4.6.2:
|
||||||
@@ -2933,8 +2933,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==}
|
resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==}
|
||||||
engines: {node: '>=18.17'}
|
engines: {node: '>=18.17'}
|
||||||
|
|
||||||
undici@7.16.0:
|
undici@7.18.0:
|
||||||
resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==}
|
resolution: {integrity: sha512-CfPufgPFHCYu0W4h1NiKW9+tNJ39o3kWm7Cm29ET1enSJx+AERfz7A2wAr26aY0SZbYzZlTBQtcHy15o60VZfQ==}
|
||||||
engines: {node: '>=20.18.1'}
|
engines: {node: '>=20.18.1'}
|
||||||
|
|
||||||
unicode-properties@1.4.1:
|
unicode-properties@1.4.1:
|
||||||
@@ -3073,8 +3073,8 @@ packages:
|
|||||||
wrappy@1.0.2:
|
wrappy@1.0.2:
|
||||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
|
|
||||||
ws@8.18.3:
|
ws@8.19.0:
|
||||||
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
|
resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
bufferutil: ^4.0.1
|
bufferutil: ^4.0.1
|
||||||
@@ -3186,13 +3186,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@cacheable/utils': 2.3.3
|
'@cacheable/utils': 2.3.3
|
||||||
'@keyv/bigmap': 1.3.0(keyv@5.5.5)
|
'@keyv/bigmap': 1.3.0(keyv@5.5.5)
|
||||||
hookified: 1.14.0
|
hookified: 1.15.0
|
||||||
keyv: 5.5.5
|
keyv: 5.5.5
|
||||||
|
|
||||||
'@cacheable/node-cache@1.7.6':
|
'@cacheable/node-cache@1.7.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
cacheable: 2.3.1
|
cacheable: 2.3.1
|
||||||
hookified: 1.14.0
|
hookified: 1.15.0
|
||||||
keyv: 5.5.5
|
keyv: 5.5.5
|
||||||
|
|
||||||
'@cacheable/utils@2.3.3':
|
'@cacheable/utils@2.3.3':
|
||||||
@@ -3290,7 +3290,7 @@ snapshots:
|
|||||||
'@vladfrangu/async_event_emitter': 2.4.7
|
'@vladfrangu/async_event_emitter': 2.4.7
|
||||||
discord-api-types: 0.38.37
|
discord-api-types: 0.38.37
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
ws: 8.18.3
|
ws: 8.19.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
@@ -3397,7 +3397,7 @@ snapshots:
|
|||||||
'@google/genai@1.34.0':
|
'@google/genai@1.34.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
google-auth-library: 10.5.0
|
google-auth-library: 10.5.0
|
||||||
ws: 8.18.3
|
ws: 8.19.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -3548,7 +3548,7 @@ snapshots:
|
|||||||
'@keyv/bigmap@1.3.0(keyv@5.5.5)':
|
'@keyv/bigmap@1.3.0(keyv@5.5.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
hashery: 1.4.0
|
hashery: 1.4.0
|
||||||
hookified: 1.14.0
|
hookified: 1.15.0
|
||||||
keyv: 5.5.5
|
keyv: 5.5.5
|
||||||
|
|
||||||
'@keyv/serialize@1.1.1': {}
|
'@keyv/serialize@1.1.1': {}
|
||||||
@@ -3585,9 +3585,9 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- tailwindcss
|
- tailwindcss
|
||||||
|
|
||||||
'@mariozechner/pi-agent-core@0.36.0(ws@8.18.3)(zod@4.3.5)':
|
'@mariozechner/pi-agent-core@0.36.0(ws@8.19.0)(zod@4.3.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mariozechner/pi-ai': 0.36.0(patch_hash=628fb051b6f4886984a846a5ee7aa0a571c3360d35b8d114e4684e5edcd100c5)(ws@8.18.3)(zod@4.3.5)
|
'@mariozechner/pi-ai': 0.36.0(patch_hash=628fb051b6f4886984a846a5ee7aa0a571c3360d35b8d114e4684e5edcd100c5)(ws@8.19.0)(zod@4.3.5)
|
||||||
'@mariozechner/pi-tui': 0.36.0
|
'@mariozechner/pi-tui': 0.36.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@modelcontextprotocol/sdk'
|
- '@modelcontextprotocol/sdk'
|
||||||
@@ -3597,7 +3597,7 @@ snapshots:
|
|||||||
- ws
|
- ws
|
||||||
- zod
|
- zod
|
||||||
|
|
||||||
'@mariozechner/pi-ai@0.36.0(patch_hash=628fb051b6f4886984a846a5ee7aa0a571c3360d35b8d114e4684e5edcd100c5)(ws@8.18.3)(zod@4.3.5)':
|
'@mariozechner/pi-ai@0.36.0(patch_hash=628fb051b6f4886984a846a5ee7aa0a571c3360d35b8d114e4684e5edcd100c5)(ws@8.19.0)(zod@4.3.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@anthropic-ai/sdk': 0.71.2(zod@4.3.5)
|
'@anthropic-ai/sdk': 0.71.2(zod@4.3.5)
|
||||||
'@google/genai': 1.34.0
|
'@google/genai': 1.34.0
|
||||||
@@ -3606,7 +3606,7 @@ snapshots:
|
|||||||
ajv: 8.17.1
|
ajv: 8.17.1
|
||||||
ajv-formats: 3.0.1(ajv@8.17.1)
|
ajv-formats: 3.0.1(ajv@8.17.1)
|
||||||
chalk: 5.6.2
|
chalk: 5.6.2
|
||||||
openai: 6.10.0(ws@8.18.3)(zod@4.3.5)
|
openai: 6.10.0(ws@8.19.0)(zod@4.3.5)
|
||||||
partial-json: 0.1.7
|
partial-json: 0.1.7
|
||||||
zod-to-json-schema: 3.25.1(zod@4.3.5)
|
zod-to-json-schema: 3.25.1(zod@4.3.5)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -3617,11 +3617,11 @@ snapshots:
|
|||||||
- ws
|
- ws
|
||||||
- zod
|
- zod
|
||||||
|
|
||||||
'@mariozechner/pi-coding-agent@0.36.0(ws@8.18.3)(zod@4.3.5)':
|
'@mariozechner/pi-coding-agent@0.36.0(ws@8.19.0)(zod@4.3.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@crosscopy/clipboard': 0.2.8
|
'@crosscopy/clipboard': 0.2.8
|
||||||
'@mariozechner/pi-agent-core': 0.36.0(ws@8.18.3)(zod@4.3.5)
|
'@mariozechner/pi-agent-core': 0.36.0(ws@8.19.0)(zod@4.3.5)
|
||||||
'@mariozechner/pi-ai': 0.36.0(patch_hash=628fb051b6f4886984a846a5ee7aa0a571c3360d35b8d114e4684e5edcd100c5)(ws@8.18.3)(zod@4.3.5)
|
'@mariozechner/pi-ai': 0.36.0(patch_hash=628fb051b6f4886984a846a5ee7aa0a571c3360d35b8d114e4684e5edcd100c5)(ws@8.19.0)(zod@4.3.5)
|
||||||
'@mariozechner/pi-tui': 0.36.0
|
'@mariozechner/pi-tui': 0.36.0
|
||||||
chalk: 5.6.2
|
chalk: 5.6.2
|
||||||
cli-highlight: 2.1.11
|
cli-highlight: 2.1.11
|
||||||
@@ -3691,28 +3691,28 @@ snapshots:
|
|||||||
'@oxlint-tsgolint/win32-x64@0.10.1':
|
'@oxlint-tsgolint/win32-x64@0.10.1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@oxlint/darwin-arm64@1.36.0':
|
'@oxlint/darwin-arm64@1.37.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@oxlint/darwin-x64@1.36.0':
|
'@oxlint/darwin-x64@1.37.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@oxlint/linux-arm64-gnu@1.36.0':
|
'@oxlint/linux-arm64-gnu@1.37.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@oxlint/linux-arm64-musl@1.36.0':
|
'@oxlint/linux-arm64-musl@1.37.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@oxlint/linux-x64-gnu@1.36.0':
|
'@oxlint/linux-x64-gnu@1.37.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@oxlint/linux-x64-musl@1.36.0':
|
'@oxlint/linux-x64-musl@1.37.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@oxlint/win32-arm64@1.36.0':
|
'@oxlint/win32-arm64@1.37.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@oxlint/win32-x64@1.36.0':
|
'@oxlint/win32-x64@1.37.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@pinojs/redact@0.4.0': {}
|
'@pinojs/redact@0.4.0': {}
|
||||||
@@ -3907,7 +3907,7 @@ snapshots:
|
|||||||
'@types/node': 25.0.3
|
'@types/node': 25.0.3
|
||||||
'@types/ws': 8.18.1
|
'@types/ws': 8.18.1
|
||||||
eventemitter3: 5.0.1
|
eventemitter3: 5.0.1
|
||||||
ws: 8.18.3
|
ws: 8.19.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
- debug
|
- debug
|
||||||
@@ -4094,7 +4094,7 @@ snapshots:
|
|||||||
sirv: 3.0.2
|
sirv: 3.0.2
|
||||||
tinyrainbow: 3.0.3
|
tinyrainbow: 3.0.3
|
||||||
vitest: 4.0.16(@types/node@25.0.3)(@vitest/browser-playwright@4.0.16)(@vitest/browser-preview@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
|
vitest: 4.0.16(@types/node@25.0.3)(@vitest/browser-playwright@4.0.16)(@vitest/browser-preview@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
ws: 8.18.3
|
ws: 8.19.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
- msw
|
- msw
|
||||||
@@ -4196,11 +4196,11 @@ snapshots:
|
|||||||
libsignal: '@whiskeysockets/libsignal-node@https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67'
|
libsignal: '@whiskeysockets/libsignal-node@https://codeload.github.com/whiskeysockets/libsignal-node/tar.gz/1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67'
|
||||||
lru-cache: 11.2.4
|
lru-cache: 11.2.4
|
||||||
music-metadata: 11.10.4
|
music-metadata: 11.10.4
|
||||||
p-queue: 9.0.1
|
p-queue: 9.1.0
|
||||||
pino: 9.14.0
|
pino: 9.14.0
|
||||||
protobufjs: 7.5.4
|
protobufjs: 7.5.4
|
||||||
sharp: 0.34.5
|
sharp: 0.34.5
|
||||||
ws: 8.18.3
|
ws: 8.19.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
audio-decode: 2.2.3
|
audio-decode: 2.2.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -4361,7 +4361,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@cacheable/memory': 2.0.7
|
'@cacheable/memory': 2.0.7
|
||||||
'@cacheable/utils': 2.3.3
|
'@cacheable/utils': 2.3.3
|
||||||
hookified: 1.14.0
|
hookified: 1.15.0
|
||||||
keyv: 5.5.5
|
keyv: 5.5.5
|
||||||
qified: 0.5.3
|
qified: 0.5.3
|
||||||
|
|
||||||
@@ -4838,7 +4838,7 @@ snapshots:
|
|||||||
|
|
||||||
hashery@1.4.0:
|
hashery@1.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
hookified: 1.14.0
|
hookified: 1.15.0
|
||||||
|
|
||||||
hasown@2.0.2:
|
hasown@2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4848,7 +4848,7 @@ snapshots:
|
|||||||
|
|
||||||
highlight.js@11.11.1: {}
|
highlight.js@11.11.1: {}
|
||||||
|
|
||||||
hookified@1.14.0: {}
|
hookified@1.15.0: {}
|
||||||
|
|
||||||
html-escaper@2.0.2: {}
|
html-escaper@2.0.2: {}
|
||||||
|
|
||||||
@@ -5256,9 +5256,9 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
wrappy: 1.0.2
|
wrappy: 1.0.2
|
||||||
|
|
||||||
openai@6.10.0(ws@8.18.3)(zod@4.3.5):
|
openai@6.10.0(ws@8.19.0)(zod@4.3.5):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
ws: 8.18.3
|
ws: 8.19.0
|
||||||
zod: 4.3.5
|
zod: 4.3.5
|
||||||
|
|
||||||
opus-decoder@0.7.11:
|
opus-decoder@0.7.11:
|
||||||
@@ -5275,16 +5275,16 @@ snapshots:
|
|||||||
'@oxlint-tsgolint/win32-arm64': 0.10.1
|
'@oxlint-tsgolint/win32-arm64': 0.10.1
|
||||||
'@oxlint-tsgolint/win32-x64': 0.10.1
|
'@oxlint-tsgolint/win32-x64': 0.10.1
|
||||||
|
|
||||||
oxlint@1.36.0(oxlint-tsgolint@0.10.1):
|
oxlint@1.37.0(oxlint-tsgolint@0.10.1):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@oxlint/darwin-arm64': 1.36.0
|
'@oxlint/darwin-arm64': 1.37.0
|
||||||
'@oxlint/darwin-x64': 1.36.0
|
'@oxlint/darwin-x64': 1.37.0
|
||||||
'@oxlint/linux-arm64-gnu': 1.36.0
|
'@oxlint/linux-arm64-gnu': 1.37.0
|
||||||
'@oxlint/linux-arm64-musl': 1.36.0
|
'@oxlint/linux-arm64-musl': 1.37.0
|
||||||
'@oxlint/linux-x64-gnu': 1.36.0
|
'@oxlint/linux-x64-gnu': 1.37.0
|
||||||
'@oxlint/linux-x64-musl': 1.36.0
|
'@oxlint/linux-x64-musl': 1.37.0
|
||||||
'@oxlint/win32-arm64': 1.36.0
|
'@oxlint/win32-arm64': 1.37.0
|
||||||
'@oxlint/win32-x64': 1.36.0
|
'@oxlint/win32-x64': 1.37.0
|
||||||
oxlint-tsgolint: 0.10.1
|
oxlint-tsgolint: 0.10.1
|
||||||
|
|
||||||
p-finally@1.0.0: {}
|
p-finally@1.0.0: {}
|
||||||
@@ -5294,7 +5294,7 @@ snapshots:
|
|||||||
eventemitter3: 4.0.7
|
eventemitter3: 4.0.7
|
||||||
p-timeout: 3.2.0
|
p-timeout: 3.2.0
|
||||||
|
|
||||||
p-queue@9.0.1:
|
p-queue@9.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
eventemitter3: 5.0.1
|
eventemitter3: 5.0.1
|
||||||
p-timeout: 7.0.1
|
p-timeout: 7.0.1
|
||||||
@@ -5453,7 +5453,7 @@ snapshots:
|
|||||||
|
|
||||||
qified@0.5.3:
|
qified@0.5.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
hookified: 1.14.0
|
hookified: 1.15.0
|
||||||
|
|
||||||
qoa-format@1.0.1:
|
qoa-format@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5876,7 +5876,7 @@ snapshots:
|
|||||||
|
|
||||||
undici@6.21.3: {}
|
undici@6.21.3: {}
|
||||||
|
|
||||||
undici@7.16.0: {}
|
undici@7.18.0: {}
|
||||||
|
|
||||||
unicode-properties@1.4.1:
|
unicode-properties@1.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5995,7 +5995,7 @@ snapshots:
|
|||||||
|
|
||||||
wrappy@1.0.2: {}
|
wrappy@1.0.2: {}
|
||||||
|
|
||||||
ws@8.18.3: {}
|
ws@8.19.0: {}
|
||||||
|
|
||||||
y18n@5.0.8: {}
|
y18n@5.0.8: {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { ThinkLevel } from "../auto-reply/thinking.js";
|
||||||
import {
|
import {
|
||||||
isRateLimitAssistantError,
|
isRateLimitAssistantError,
|
||||||
pickFallbackThinkingLevel,
|
pickFallbackThinkingLevel,
|
||||||
} from "./pi-embedded-helpers.js";
|
} from "./pi-embedded-helpers.js";
|
||||||
import type { ThinkLevel } from "../auto-reply/thinking.js";
|
|
||||||
|
|
||||||
const asAssistant = (overrides: Partial<AssistantMessage>) =>
|
const asAssistant = (overrides: Partial<AssistantMessage>) =>
|
||||||
({ role: "assistant", stopReason: "error", ...overrides }) as AssistantMessage;
|
({
|
||||||
|
role: "assistant",
|
||||||
|
stopReason: "error",
|
||||||
|
...overrides,
|
||||||
|
}) as AssistantMessage;
|
||||||
|
|
||||||
describe("isRateLimitAssistantError", () => {
|
describe("isRateLimitAssistantError", () => {
|
||||||
it("detects 429 rate limit payloads", () => {
|
it("detects 429 rate limit payloads", () => {
|
||||||
@@ -57,8 +60,7 @@ describe("pickFallbackThinkingLevel", () => {
|
|||||||
it("skips already attempted levels", () => {
|
it("skips already attempted levels", () => {
|
||||||
const attempted = new Set<ThinkLevel>(["low", "medium"]);
|
const attempted = new Set<ThinkLevel>(["low", "medium"]);
|
||||||
const next = pickFallbackThinkingLevel({
|
const next = pickFallbackThinkingLevel({
|
||||||
message:
|
message: "Supported values are: 'medium', 'high', and 'xhigh'.",
|
||||||
"Supported values are: 'medium', 'high', and 'xhigh'.",
|
|
||||||
attempted,
|
attempted,
|
||||||
});
|
});
|
||||||
expect(next).toBe("high");
|
expect(next).toBe("high");
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import type {
|
|||||||
AgentToolResult,
|
AgentToolResult,
|
||||||
} from "@mariozechner/pi-agent-core";
|
} from "@mariozechner/pi-agent-core";
|
||||||
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
||||||
import { normalizeThinkLevel, type ThinkLevel } from "../auto-reply/thinking.js";
|
import {
|
||||||
|
normalizeThinkLevel,
|
||||||
|
type ThinkLevel,
|
||||||
|
} from "../auto-reply/thinking.js";
|
||||||
|
|
||||||
import { sanitizeContentBlocksImages } from "./tool-images.js";
|
import { sanitizeContentBlocksImages } from "./tool-images.js";
|
||||||
import type { WorkspaceBootstrapFile } from "./workspace.js";
|
import type { WorkspaceBootstrapFile } from "./workspace.js";
|
||||||
|
|||||||
@@ -341,328 +341,336 @@ export async function runEmbeddedPiAgent(params: {
|
|||||||
let restoreSkillEnv: (() => void) | undefined;
|
let restoreSkillEnv: (() => void) | undefined;
|
||||||
process.chdir(resolvedWorkspace);
|
process.chdir(resolvedWorkspace);
|
||||||
try {
|
try {
|
||||||
const shouldLoadSkillEntries =
|
const shouldLoadSkillEntries =
|
||||||
!params.skillsSnapshot || !params.skillsSnapshot.resolvedSkills;
|
!params.skillsSnapshot || !params.skillsSnapshot.resolvedSkills;
|
||||||
const skillEntries = shouldLoadSkillEntries
|
const skillEntries = shouldLoadSkillEntries
|
||||||
? loadWorkspaceSkillEntries(resolvedWorkspace)
|
? loadWorkspaceSkillEntries(resolvedWorkspace)
|
||||||
: [];
|
: [];
|
||||||
const skillsSnapshot =
|
const skillsSnapshot =
|
||||||
params.skillsSnapshot ??
|
params.skillsSnapshot ??
|
||||||
buildWorkspaceSkillSnapshot(resolvedWorkspace, {
|
buildWorkspaceSkillSnapshot(resolvedWorkspace, {
|
||||||
|
config: params.config,
|
||||||
|
entries: skillEntries,
|
||||||
|
});
|
||||||
|
const sandboxSessionKey =
|
||||||
|
params.sessionKey?.trim() || params.sessionId;
|
||||||
|
const sandbox = await resolveSandboxContext({
|
||||||
config: params.config,
|
config: params.config,
|
||||||
entries: skillEntries,
|
sessionKey: sandboxSessionKey,
|
||||||
});
|
|
||||||
const sandboxSessionKey = params.sessionKey?.trim() || params.sessionId;
|
|
||||||
const sandbox = await resolveSandboxContext({
|
|
||||||
config: params.config,
|
|
||||||
sessionKey: sandboxSessionKey,
|
|
||||||
workspaceDir: resolvedWorkspace,
|
|
||||||
});
|
|
||||||
restoreSkillEnv = params.skillsSnapshot
|
|
||||||
? applySkillEnvOverridesFromSnapshot({
|
|
||||||
snapshot: params.skillsSnapshot,
|
|
||||||
config: params.config,
|
|
||||||
})
|
|
||||||
: applySkillEnvOverrides({
|
|
||||||
skills: skillEntries ?? [],
|
|
||||||
config: params.config,
|
|
||||||
});
|
|
||||||
|
|
||||||
const bootstrapFiles =
|
|
||||||
await loadWorkspaceBootstrapFiles(resolvedWorkspace);
|
|
||||||
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
|
||||||
const promptSkills = resolvePromptSkills(skillsSnapshot, skillEntries);
|
|
||||||
// Tool schemas must be provider-compatible (OpenAI requires top-level `type: "object"`).
|
|
||||||
// `createClawdbotCodingTools()` normalizes schemas so the session can pass them through unchanged.
|
|
||||||
const tools = createClawdbotCodingTools({
|
|
||||||
bash: {
|
|
||||||
...params.config?.agent?.bash,
|
|
||||||
elevated: params.bashElevated,
|
|
||||||
},
|
|
||||||
sandbox,
|
|
||||||
surface: params.surface,
|
|
||||||
sessionKey: params.sessionKey ?? params.sessionId,
|
|
||||||
config: params.config,
|
|
||||||
});
|
|
||||||
const machineName = await getMachineDisplayName();
|
|
||||||
const runtimeInfo = {
|
|
||||||
host: machineName,
|
|
||||||
os: `${os.type()} ${os.release()}`,
|
|
||||||
arch: os.arch(),
|
|
||||||
node: process.version,
|
|
||||||
model: `${provider}/${modelId}`,
|
|
||||||
};
|
|
||||||
const sandboxInfo = buildEmbeddedSandboxInfo(sandbox);
|
|
||||||
const reasoningTagHint = provider === "ollama";
|
|
||||||
const systemPrompt = buildSystemPrompt({
|
|
||||||
appendPrompt: buildAgentSystemPromptAppend({
|
|
||||||
workspaceDir: resolvedWorkspace,
|
workspaceDir: resolvedWorkspace,
|
||||||
defaultThinkLevel: thinkLevel,
|
});
|
||||||
extraSystemPrompt: params.extraSystemPrompt,
|
restoreSkillEnv = params.skillsSnapshot
|
||||||
ownerNumbers: params.ownerNumbers,
|
? applySkillEnvOverridesFromSnapshot({
|
||||||
reasoningTagHint,
|
snapshot: params.skillsSnapshot,
|
||||||
runtimeInfo,
|
config: params.config,
|
||||||
sandboxInfo,
|
})
|
||||||
toolNames: tools.map((tool) => tool.name),
|
: applySkillEnvOverrides({
|
||||||
}),
|
skills: skillEntries ?? [],
|
||||||
contextFiles,
|
config: params.config,
|
||||||
skills: promptSkills,
|
});
|
||||||
cwd: resolvedWorkspace,
|
|
||||||
tools,
|
|
||||||
});
|
|
||||||
|
|
||||||
const sessionManager = SessionManager.open(params.sessionFile);
|
const bootstrapFiles =
|
||||||
const settingsManager = SettingsManager.create(
|
await loadWorkspaceBootstrapFiles(resolvedWorkspace);
|
||||||
resolvedWorkspace,
|
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
||||||
agentDir,
|
const promptSkills = resolvePromptSkills(
|
||||||
);
|
skillsSnapshot,
|
||||||
|
skillEntries,
|
||||||
// Split tools into built-in (recognized by pi-coding-agent SDK) and custom (clawdbot-specific)
|
|
||||||
const builtInToolNames = new Set(["read", "bash", "edit", "write"]);
|
|
||||||
const builtInTools = tools.filter((t) => builtInToolNames.has(t.name));
|
|
||||||
const customTools = toToolDefinitions(
|
|
||||||
tools.filter((t) => !builtInToolNames.has(t.name)),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { session } = await createAgentSession({
|
|
||||||
cwd: resolvedWorkspace,
|
|
||||||
agentDir,
|
|
||||||
authStorage,
|
|
||||||
modelRegistry,
|
|
||||||
model,
|
|
||||||
thinkingLevel,
|
|
||||||
systemPrompt,
|
|
||||||
// Built-in tools recognized by pi-coding-agent SDK
|
|
||||||
tools: builtInTools,
|
|
||||||
// Custom clawdbot tools (browser, canvas, nodes, cron, etc.)
|
|
||||||
customTools,
|
|
||||||
sessionManager,
|
|
||||||
settingsManager,
|
|
||||||
skills: promptSkills,
|
|
||||||
contextFiles,
|
|
||||||
});
|
|
||||||
|
|
||||||
const prior = await sanitizeSessionMessagesImages(
|
|
||||||
session.messages,
|
|
||||||
"session:history",
|
|
||||||
);
|
|
||||||
if (prior.length > 0) {
|
|
||||||
session.agent.replaceMessages(prior);
|
|
||||||
}
|
|
||||||
let aborted = Boolean(params.abortSignal?.aborted);
|
|
||||||
const abortRun = () => {
|
|
||||||
aborted = true;
|
|
||||||
void session.abort();
|
|
||||||
};
|
|
||||||
const queueHandle: EmbeddedPiQueueHandle = {
|
|
||||||
queueMessage: async (text: string) => {
|
|
||||||
await session.steer(text);
|
|
||||||
},
|
|
||||||
isStreaming: () => session.isStreaming,
|
|
||||||
abort: abortRun,
|
|
||||||
};
|
|
||||||
ACTIVE_EMBEDDED_RUNS.set(params.sessionId, queueHandle);
|
|
||||||
|
|
||||||
const {
|
|
||||||
assistantTexts,
|
|
||||||
toolMetas,
|
|
||||||
unsubscribe,
|
|
||||||
waitForCompactionRetry,
|
|
||||||
} = subscribeEmbeddedPiSession({
|
|
||||||
session,
|
|
||||||
runId: params.runId,
|
|
||||||
verboseLevel: params.verboseLevel,
|
|
||||||
shouldEmitToolResult: params.shouldEmitToolResult,
|
|
||||||
onToolResult: params.onToolResult,
|
|
||||||
onBlockReply: params.onBlockReply,
|
|
||||||
blockReplyBreak: params.blockReplyBreak,
|
|
||||||
blockReplyChunking: params.blockReplyChunking,
|
|
||||||
onPartialReply: params.onPartialReply,
|
|
||||||
onAgentEvent: params.onAgentEvent,
|
|
||||||
enforceFinalTag: params.enforceFinalTag,
|
|
||||||
});
|
|
||||||
|
|
||||||
let abortWarnTimer: NodeJS.Timeout | undefined;
|
|
||||||
const abortTimer = setTimeout(
|
|
||||||
() => {
|
|
||||||
log.warn(
|
|
||||||
`embedded run timeout: runId=${params.runId} sessionId=${params.sessionId} timeoutMs=${params.timeoutMs}`,
|
|
||||||
);
|
|
||||||
abortRun();
|
|
||||||
if (!abortWarnTimer) {
|
|
||||||
abortWarnTimer = setTimeout(() => {
|
|
||||||
if (!session.isStreaming) return;
|
|
||||||
log.warn(
|
|
||||||
`embedded run abort still streaming: runId=${params.runId} sessionId=${params.sessionId}`,
|
|
||||||
);
|
|
||||||
}, 10_000);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Math.max(1, params.timeoutMs),
|
|
||||||
);
|
|
||||||
|
|
||||||
let messagesSnapshot: AgentMessage[] = [];
|
|
||||||
let sessionIdUsed = session.sessionId;
|
|
||||||
const onAbort = () => {
|
|
||||||
abortRun();
|
|
||||||
};
|
|
||||||
if (params.abortSignal) {
|
|
||||||
if (params.abortSignal.aborted) {
|
|
||||||
onAbort();
|
|
||||||
} else {
|
|
||||||
params.abortSignal.addEventListener("abort", onAbort, {
|
|
||||||
once: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let promptError: unknown = null;
|
|
||||||
try {
|
|
||||||
const promptStartedAt = Date.now();
|
|
||||||
log.debug(
|
|
||||||
`embedded run prompt start: runId=${params.runId} sessionId=${params.sessionId}`,
|
|
||||||
);
|
);
|
||||||
|
// Tool schemas must be provider-compatible (OpenAI requires top-level `type: "object"`).
|
||||||
|
// `createClawdbotCodingTools()` normalizes schemas so the session can pass them through unchanged.
|
||||||
|
const tools = createClawdbotCodingTools({
|
||||||
|
bash: {
|
||||||
|
...params.config?.agent?.bash,
|
||||||
|
elevated: params.bashElevated,
|
||||||
|
},
|
||||||
|
sandbox,
|
||||||
|
surface: params.surface,
|
||||||
|
sessionKey: params.sessionKey ?? params.sessionId,
|
||||||
|
config: params.config,
|
||||||
|
});
|
||||||
|
const machineName = await getMachineDisplayName();
|
||||||
|
const runtimeInfo = {
|
||||||
|
host: machineName,
|
||||||
|
os: `${os.type()} ${os.release()}`,
|
||||||
|
arch: os.arch(),
|
||||||
|
node: process.version,
|
||||||
|
model: `${provider}/${modelId}`,
|
||||||
|
};
|
||||||
|
const sandboxInfo = buildEmbeddedSandboxInfo(sandbox);
|
||||||
|
const reasoningTagHint = provider === "ollama";
|
||||||
|
const systemPrompt = buildSystemPrompt({
|
||||||
|
appendPrompt: buildAgentSystemPromptAppend({
|
||||||
|
workspaceDir: resolvedWorkspace,
|
||||||
|
defaultThinkLevel: thinkLevel,
|
||||||
|
extraSystemPrompt: params.extraSystemPrompt,
|
||||||
|
ownerNumbers: params.ownerNumbers,
|
||||||
|
reasoningTagHint,
|
||||||
|
runtimeInfo,
|
||||||
|
sandboxInfo,
|
||||||
|
toolNames: tools.map((tool) => tool.name),
|
||||||
|
}),
|
||||||
|
contextFiles,
|
||||||
|
skills: promptSkills,
|
||||||
|
cwd: resolvedWorkspace,
|
||||||
|
tools,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sessionManager = SessionManager.open(params.sessionFile);
|
||||||
|
const settingsManager = SettingsManager.create(
|
||||||
|
resolvedWorkspace,
|
||||||
|
agentDir,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Split tools into built-in (recognized by pi-coding-agent SDK) and custom (clawdbot-specific)
|
||||||
|
const builtInToolNames = new Set(["read", "bash", "edit", "write"]);
|
||||||
|
const builtInTools = tools.filter((t) =>
|
||||||
|
builtInToolNames.has(t.name),
|
||||||
|
);
|
||||||
|
const customTools = toToolDefinitions(
|
||||||
|
tools.filter((t) => !builtInToolNames.has(t.name)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { session } = await createAgentSession({
|
||||||
|
cwd: resolvedWorkspace,
|
||||||
|
agentDir,
|
||||||
|
authStorage,
|
||||||
|
modelRegistry,
|
||||||
|
model,
|
||||||
|
thinkingLevel,
|
||||||
|
systemPrompt,
|
||||||
|
// Built-in tools recognized by pi-coding-agent SDK
|
||||||
|
tools: builtInTools,
|
||||||
|
// Custom clawdbot tools (browser, canvas, nodes, cron, etc.)
|
||||||
|
customTools,
|
||||||
|
sessionManager,
|
||||||
|
settingsManager,
|
||||||
|
skills: promptSkills,
|
||||||
|
contextFiles,
|
||||||
|
});
|
||||||
|
|
||||||
|
const prior = await sanitizeSessionMessagesImages(
|
||||||
|
session.messages,
|
||||||
|
"session:history",
|
||||||
|
);
|
||||||
|
if (prior.length > 0) {
|
||||||
|
session.agent.replaceMessages(prior);
|
||||||
|
}
|
||||||
|
let aborted = Boolean(params.abortSignal?.aborted);
|
||||||
|
const abortRun = () => {
|
||||||
|
aborted = true;
|
||||||
|
void session.abort();
|
||||||
|
};
|
||||||
|
const queueHandle: EmbeddedPiQueueHandle = {
|
||||||
|
queueMessage: async (text: string) => {
|
||||||
|
await session.steer(text);
|
||||||
|
},
|
||||||
|
isStreaming: () => session.isStreaming,
|
||||||
|
abort: abortRun,
|
||||||
|
};
|
||||||
|
ACTIVE_EMBEDDED_RUNS.set(params.sessionId, queueHandle);
|
||||||
|
|
||||||
|
const {
|
||||||
|
assistantTexts,
|
||||||
|
toolMetas,
|
||||||
|
unsubscribe,
|
||||||
|
waitForCompactionRetry,
|
||||||
|
} = subscribeEmbeddedPiSession({
|
||||||
|
session,
|
||||||
|
runId: params.runId,
|
||||||
|
verboseLevel: params.verboseLevel,
|
||||||
|
shouldEmitToolResult: params.shouldEmitToolResult,
|
||||||
|
onToolResult: params.onToolResult,
|
||||||
|
onBlockReply: params.onBlockReply,
|
||||||
|
blockReplyBreak: params.blockReplyBreak,
|
||||||
|
blockReplyChunking: params.blockReplyChunking,
|
||||||
|
onPartialReply: params.onPartialReply,
|
||||||
|
onAgentEvent: params.onAgentEvent,
|
||||||
|
enforceFinalTag: params.enforceFinalTag,
|
||||||
|
});
|
||||||
|
|
||||||
|
let abortWarnTimer: NodeJS.Timeout | undefined;
|
||||||
|
const abortTimer = setTimeout(
|
||||||
|
() => {
|
||||||
|
log.warn(
|
||||||
|
`embedded run timeout: runId=${params.runId} sessionId=${params.sessionId} timeoutMs=${params.timeoutMs}`,
|
||||||
|
);
|
||||||
|
abortRun();
|
||||||
|
if (!abortWarnTimer) {
|
||||||
|
abortWarnTimer = setTimeout(() => {
|
||||||
|
if (!session.isStreaming) return;
|
||||||
|
log.warn(
|
||||||
|
`embedded run abort still streaming: runId=${params.runId} sessionId=${params.sessionId}`,
|
||||||
|
);
|
||||||
|
}, 10_000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Math.max(1, params.timeoutMs),
|
||||||
|
);
|
||||||
|
|
||||||
|
let messagesSnapshot: AgentMessage[] = [];
|
||||||
|
let sessionIdUsed = session.sessionId;
|
||||||
|
const onAbort = () => {
|
||||||
|
abortRun();
|
||||||
|
};
|
||||||
|
if (params.abortSignal) {
|
||||||
|
if (params.abortSignal.aborted) {
|
||||||
|
onAbort();
|
||||||
|
} else {
|
||||||
|
params.abortSignal.addEventListener("abort", onAbort, {
|
||||||
|
once: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let promptError: unknown = null;
|
||||||
try {
|
try {
|
||||||
await session.prompt(params.prompt);
|
const promptStartedAt = Date.now();
|
||||||
} catch (err) {
|
|
||||||
promptError = err;
|
|
||||||
} finally {
|
|
||||||
log.debug(
|
log.debug(
|
||||||
`embedded run prompt end: runId=${params.runId} sessionId=${params.sessionId} durationMs=${Date.now() - promptStartedAt}`,
|
`embedded run prompt start: runId=${params.runId} sessionId=${params.sessionId}`,
|
||||||
);
|
);
|
||||||
|
try {
|
||||||
|
await session.prompt(params.prompt);
|
||||||
|
} catch (err) {
|
||||||
|
promptError = err;
|
||||||
|
} finally {
|
||||||
|
log.debug(
|
||||||
|
`embedded run prompt end: runId=${params.runId} sessionId=${params.sessionId} durationMs=${Date.now() - promptStartedAt}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await waitForCompactionRetry();
|
||||||
|
messagesSnapshot = session.messages.slice();
|
||||||
|
sessionIdUsed = session.sessionId;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(abortTimer);
|
||||||
|
if (abortWarnTimer) {
|
||||||
|
clearTimeout(abortWarnTimer);
|
||||||
|
abortWarnTimer = undefined;
|
||||||
|
}
|
||||||
|
unsubscribe();
|
||||||
|
if (ACTIVE_EMBEDDED_RUNS.get(params.sessionId) === queueHandle) {
|
||||||
|
ACTIVE_EMBEDDED_RUNS.delete(params.sessionId);
|
||||||
|
notifyEmbeddedRunEnded(params.sessionId);
|
||||||
|
}
|
||||||
|
session.dispose();
|
||||||
|
params.abortSignal?.removeEventListener?.("abort", onAbort);
|
||||||
}
|
}
|
||||||
await waitForCompactionRetry();
|
if (promptError && !aborted) {
|
||||||
messagesSnapshot = session.messages.slice();
|
const fallbackThinking = pickFallbackThinkingLevel({
|
||||||
sessionIdUsed = session.sessionId;
|
message:
|
||||||
} finally {
|
promptError instanceof Error
|
||||||
clearTimeout(abortTimer);
|
? promptError.message
|
||||||
if (abortWarnTimer) {
|
: String(promptError),
|
||||||
clearTimeout(abortWarnTimer);
|
attempted: attemptedThinking,
|
||||||
abortWarnTimer = undefined;
|
});
|
||||||
|
if (fallbackThinking) {
|
||||||
|
log.warn(
|
||||||
|
`unsupported thinking level for ${provider}/${modelId}; retrying with ${fallbackThinking}`,
|
||||||
|
);
|
||||||
|
thinkLevel = fallbackThinking;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw promptError;
|
||||||
}
|
}
|
||||||
unsubscribe();
|
|
||||||
if (ACTIVE_EMBEDDED_RUNS.get(params.sessionId) === queueHandle) {
|
const lastAssistant = messagesSnapshot
|
||||||
ACTIVE_EMBEDDED_RUNS.delete(params.sessionId);
|
.slice()
|
||||||
notifyEmbeddedRunEnded(params.sessionId);
|
.reverse()
|
||||||
}
|
.find((m) => (m as AgentMessage)?.role === "assistant") as
|
||||||
session.dispose();
|
| AssistantMessage
|
||||||
params.abortSignal?.removeEventListener?.("abort", onAbort);
|
| undefined;
|
||||||
}
|
|
||||||
if (promptError && !aborted) {
|
|
||||||
const fallbackThinking = pickFallbackThinkingLevel({
|
const fallbackThinking = pickFallbackThinkingLevel({
|
||||||
message:
|
message: lastAssistant?.errorMessage,
|
||||||
promptError instanceof Error
|
|
||||||
? promptError.message
|
|
||||||
: String(promptError),
|
|
||||||
attempted: attemptedThinking,
|
attempted: attemptedThinking,
|
||||||
});
|
});
|
||||||
if (fallbackThinking) {
|
if (fallbackThinking && !aborted) {
|
||||||
log.warn(
|
log.warn(
|
||||||
`unsupported thinking level for ${provider}/${modelId}; retrying with ${fallbackThinking}`,
|
`unsupported thinking level for ${provider}/${modelId}; retrying with ${fallbackThinking}`,
|
||||||
);
|
);
|
||||||
thinkLevel = fallbackThinking;
|
thinkLevel = fallbackThinking;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
throw promptError;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastAssistant = messagesSnapshot
|
const fallbackConfigured =
|
||||||
.slice()
|
(params.config?.agent?.modelFallbacks?.length ?? 0) > 0;
|
||||||
.reverse()
|
if (fallbackConfigured && isRateLimitAssistantError(lastAssistant)) {
|
||||||
.find((m) => (m as AgentMessage)?.role === "assistant") as
|
const message =
|
||||||
| AssistantMessage
|
lastAssistant?.errorMessage?.trim() ||
|
||||||
| undefined;
|
(lastAssistant ? formatAssistantErrorText(lastAssistant) : "") ||
|
||||||
|
"LLM request rate limited.";
|
||||||
const fallbackThinking = pickFallbackThinkingLevel({
|
throw new Error(message);
|
||||||
message: lastAssistant?.errorMessage,
|
|
||||||
attempted: attemptedThinking,
|
|
||||||
});
|
|
||||||
if (fallbackThinking && !aborted) {
|
|
||||||
log.warn(
|
|
||||||
`unsupported thinking level for ${provider}/${modelId}; retrying with ${fallbackThinking}`,
|
|
||||||
);
|
|
||||||
thinkLevel = fallbackThinking;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fallbackConfigured =
|
|
||||||
(params.config?.agent?.modelFallbacks?.length ?? 0) > 0;
|
|
||||||
if (fallbackConfigured && isRateLimitAssistantError(lastAssistant)) {
|
|
||||||
const message =
|
|
||||||
lastAssistant?.errorMessage?.trim() ||
|
|
||||||
(lastAssistant ? formatAssistantErrorText(lastAssistant) : "") ||
|
|
||||||
"LLM request rate limited.";
|
|
||||||
throw new Error(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const usage = lastAssistant?.usage;
|
|
||||||
const agentMeta: EmbeddedPiAgentMeta = {
|
|
||||||
sessionId: sessionIdUsed,
|
|
||||||
provider: lastAssistant?.provider ?? provider,
|
|
||||||
model: lastAssistant?.model ?? model.id,
|
|
||||||
usage: usage
|
|
||||||
? {
|
|
||||||
input: usage.input,
|
|
||||||
output: usage.output,
|
|
||||||
cacheRead: usage.cacheRead,
|
|
||||||
cacheWrite: usage.cacheWrite,
|
|
||||||
total: usage.totalTokens,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const replyItems: Array<{ text: string; media?: string[] }> = [];
|
|
||||||
|
|
||||||
const errorText = lastAssistant
|
|
||||||
? formatAssistantErrorText(lastAssistant)
|
|
||||||
: undefined;
|
|
||||||
if (errorText) replyItems.push({ text: errorText });
|
|
||||||
|
|
||||||
const inlineToolResults =
|
|
||||||
params.verboseLevel === "on" &&
|
|
||||||
!params.onPartialReply &&
|
|
||||||
!params.onToolResult &&
|
|
||||||
toolMetas.length > 0;
|
|
||||||
if (inlineToolResults) {
|
|
||||||
for (const { toolName, meta } of toolMetas) {
|
|
||||||
const agg = formatToolAggregate(toolName, meta ? [meta] : []);
|
|
||||||
const { text: cleanedText, mediaUrls } = splitMediaFromOutput(agg);
|
|
||||||
if (cleanedText)
|
|
||||||
replyItems.push({ text: cleanedText, media: mediaUrls });
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for (const text of assistantTexts.length
|
const usage = lastAssistant?.usage;
|
||||||
? assistantTexts
|
const agentMeta: EmbeddedPiAgentMeta = {
|
||||||
: lastAssistant
|
sessionId: sessionIdUsed,
|
||||||
? [extractAssistantText(lastAssistant)]
|
provider: lastAssistant?.provider ?? provider,
|
||||||
: []) {
|
model: lastAssistant?.model ?? model.id,
|
||||||
const { text: cleanedText, mediaUrls } = splitMediaFromOutput(text);
|
usage: usage
|
||||||
if (!cleanedText && (!mediaUrls || mediaUrls.length === 0)) continue;
|
? {
|
||||||
replyItems.push({ text: cleanedText, media: mediaUrls });
|
input: usage.input,
|
||||||
}
|
output: usage.output,
|
||||||
|
cacheRead: usage.cacheRead,
|
||||||
|
cacheWrite: usage.cacheWrite,
|
||||||
|
total: usage.totalTokens,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
const payloads = replyItems
|
const replyItems: Array<{ text: string; media?: string[] }> = [];
|
||||||
.map((item) => ({
|
|
||||||
text: item.text?.trim() ? item.text.trim() : undefined,
|
const errorText = lastAssistant
|
||||||
mediaUrls: item.media?.length ? item.media : undefined,
|
? formatAssistantErrorText(lastAssistant)
|
||||||
mediaUrl: item.media?.[0],
|
: undefined;
|
||||||
}))
|
if (errorText) replyItems.push({ text: errorText });
|
||||||
.filter(
|
|
||||||
(p) =>
|
const inlineToolResults =
|
||||||
p.text || p.mediaUrl || (p.mediaUrls && p.mediaUrls.length > 0),
|
params.verboseLevel === "on" &&
|
||||||
|
!params.onPartialReply &&
|
||||||
|
!params.onToolResult &&
|
||||||
|
toolMetas.length > 0;
|
||||||
|
if (inlineToolResults) {
|
||||||
|
for (const { toolName, meta } of toolMetas) {
|
||||||
|
const agg = formatToolAggregate(toolName, meta ? [meta] : []);
|
||||||
|
const { text: cleanedText, mediaUrls } =
|
||||||
|
splitMediaFromOutput(agg);
|
||||||
|
if (cleanedText)
|
||||||
|
replyItems.push({ text: cleanedText, media: mediaUrls });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const text of assistantTexts.length
|
||||||
|
? assistantTexts
|
||||||
|
: lastAssistant
|
||||||
|
? [extractAssistantText(lastAssistant)]
|
||||||
|
: []) {
|
||||||
|
const { text: cleanedText, mediaUrls } = splitMediaFromOutput(text);
|
||||||
|
if (!cleanedText && (!mediaUrls || mediaUrls.length === 0))
|
||||||
|
continue;
|
||||||
|
replyItems.push({ text: cleanedText, media: mediaUrls });
|
||||||
|
}
|
||||||
|
|
||||||
|
const payloads = replyItems
|
||||||
|
.map((item) => ({
|
||||||
|
text: item.text?.trim() ? item.text.trim() : undefined,
|
||||||
|
mediaUrls: item.media?.length ? item.media : undefined,
|
||||||
|
mediaUrl: item.media?.[0],
|
||||||
|
}))
|
||||||
|
.filter(
|
||||||
|
(p) =>
|
||||||
|
p.text || p.mediaUrl || (p.mediaUrls && p.mediaUrls.length > 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
`embedded run done: runId=${params.runId} sessionId=${params.sessionId} durationMs=${Date.now() - started} aborted=${aborted}`,
|
||||||
);
|
);
|
||||||
|
return {
|
||||||
log.debug(
|
payloads: payloads.length ? payloads : undefined,
|
||||||
`embedded run done: runId=${params.runId} sessionId=${params.sessionId} durationMs=${Date.now() - started} aborted=${aborted}`,
|
meta: {
|
||||||
);
|
durationMs: Date.now() - started,
|
||||||
return {
|
agentMeta,
|
||||||
payloads: payloads.length ? payloads : undefined,
|
aborted,
|
||||||
meta: {
|
},
|
||||||
durationMs: Date.now() - started,
|
};
|
||||||
agentMeta,
|
|
||||||
aborted,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} finally {
|
} finally {
|
||||||
restoreSkillEnv?.();
|
restoreSkillEnv?.();
|
||||||
process.chdir(prevCwd);
|
process.chdir(prevCwd);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { HEARTBEAT_TOKEN } from "./tokens.js";
|
import { HEARTBEAT_TOKEN } from "./tokens.js";
|
||||||
|
|
||||||
export const HEARTBEAT_PROMPT = "HEARTBEAT";
|
export const HEARTBEAT_PROMPT = "HEARTBEAT";
|
||||||
|
export const DEFAULT_HEARTBEAT_ACK_MAX_CHARS = 30;
|
||||||
|
|
||||||
export type StripHeartbeatMode = "heartbeat" | "message";
|
export type StripHeartbeatMode = "heartbeat" | "message";
|
||||||
|
|
||||||
@@ -44,7 +45,10 @@ export function stripHeartbeatToken(
|
|||||||
if (!trimmed) return { shouldSkip: true, text: "", didStrip: false };
|
if (!trimmed) return { shouldSkip: true, text: "", didStrip: false };
|
||||||
|
|
||||||
const mode: StripHeartbeatMode = opts.mode ?? "message";
|
const mode: StripHeartbeatMode = opts.mode ?? "message";
|
||||||
const maxAckChars = Math.max(0, opts.maxAckChars ?? 30);
|
const maxAckChars = Math.max(
|
||||||
|
0,
|
||||||
|
opts.maxAckChars ?? DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||||
|
);
|
||||||
|
|
||||||
if (!trimmed.includes(HEARTBEAT_TOKEN)) {
|
if (!trimmed.includes(HEARTBEAT_TOKEN)) {
|
||||||
return { shouldSkip: false, text: trimmed, didStrip: false };
|
return { shouldSkip: false, text: trimmed, didStrip: false };
|
||||||
|
|||||||
@@ -726,6 +726,8 @@ export type ClawdbotConfig = {
|
|||||||
to?: string;
|
to?: string;
|
||||||
/** Override the heartbeat prompt body (default: "HEARTBEAT"). */
|
/** Override the heartbeat prompt body (default: "HEARTBEAT"). */
|
||||||
prompt?: string;
|
prompt?: string;
|
||||||
|
/** Max chars allowed after HEARTBEAT_OK before delivery (default: 30). */
|
||||||
|
ackMaxChars?: number;
|
||||||
};
|
};
|
||||||
/** Max concurrent agent runs across all conversations. Default: 1 (sequential). */
|
/** Max concurrent agent runs across all conversations. Default: 1 (sequential). */
|
||||||
maxConcurrent?: number;
|
maxConcurrent?: number;
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ const HeartbeatSchema = z
|
|||||||
.optional(),
|
.optional(),
|
||||||
to: z.string().optional(),
|
to: z.string().optional(),
|
||||||
prompt: z.string().optional(),
|
prompt: z.string().optional(),
|
||||||
|
ackMaxChars: z.number().int().nonnegative().optional(),
|
||||||
})
|
})
|
||||||
.superRefine((val, ctx) => {
|
.superRefine((val, ctx) => {
|
||||||
if (!val.every) return;
|
if (!val.every) return;
|
||||||
|
|||||||
@@ -553,4 +553,48 @@ describe("runCronIsolatedAgentTurn", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("delivers when heartbeat ack padding exceeds configured limit", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
const storePath = await writeSessionStore(home);
|
||||||
|
const deps: CliDeps = {
|
||||||
|
sendMessageWhatsApp: vi.fn(),
|
||||||
|
sendMessageTelegram: vi.fn().mockResolvedValue({
|
||||||
|
messageId: "t1",
|
||||||
|
chatId: "123",
|
||||||
|
}),
|
||||||
|
sendMessageDiscord: vi.fn(),
|
||||||
|
sendMessageSignal: vi.fn(),
|
||||||
|
sendMessageIMessage: vi.fn(),
|
||||||
|
};
|
||||||
|
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
||||||
|
payloads: [{ text: "HEARTBEAT_OK 🦞" }],
|
||||||
|
meta: {
|
||||||
|
durationMs: 5,
|
||||||
|
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const cfg = makeCfg(home, storePath);
|
||||||
|
cfg.agent = { ...cfg.agent, heartbeat: { ackMaxChars: 0 } };
|
||||||
|
|
||||||
|
const res = await runCronIsolatedAgentTurn({
|
||||||
|
cfg,
|
||||||
|
deps,
|
||||||
|
job: makeJob({
|
||||||
|
kind: "agentTurn",
|
||||||
|
message: "do it",
|
||||||
|
deliver: true,
|
||||||
|
channel: "telegram",
|
||||||
|
to: "123",
|
||||||
|
}),
|
||||||
|
message: "do it",
|
||||||
|
sessionKey: "cron:job-1",
|
||||||
|
lane: "cron",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).toBe("ok");
|
||||||
|
expect(deps.sendMessageTelegram).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ import {
|
|||||||
ensureAgentWorkspace,
|
ensureAgentWorkspace,
|
||||||
} from "../agents/workspace.js";
|
} from "../agents/workspace.js";
|
||||||
import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
||||||
import { stripHeartbeatToken } from "../auto-reply/heartbeat.js";
|
import {
|
||||||
|
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||||
|
stripHeartbeatToken,
|
||||||
|
} from "../auto-reply/heartbeat.js";
|
||||||
import { normalizeThinkLevel } from "../auto-reply/thinking.js";
|
import { normalizeThinkLevel } from "../auto-reply/thinking.js";
|
||||||
import type { CliDeps } from "../cli/deps.js";
|
import type { CliDeps } from "../cli/deps.js";
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
@@ -64,6 +67,7 @@ function pickSummaryFromPayloads(
|
|||||||
*/
|
*/
|
||||||
function isHeartbeatOnlyResponse(
|
function isHeartbeatOnlyResponse(
|
||||||
payloads: Array<{ text?: string; mediaUrl?: string; mediaUrls?: string[] }>,
|
payloads: Array<{ text?: string; mediaUrl?: string; mediaUrls?: string[] }>,
|
||||||
|
ackMaxChars: number,
|
||||||
) {
|
) {
|
||||||
if (payloads.length === 0) return true;
|
if (payloads.length === 0) return true;
|
||||||
return payloads.every((payload) => {
|
return payloads.every((payload) => {
|
||||||
@@ -72,11 +76,13 @@ function isHeartbeatOnlyResponse(
|
|||||||
(payload.mediaUrls?.length ?? 0) > 0 || Boolean(payload.mediaUrl);
|
(payload.mediaUrls?.length ?? 0) > 0 || Boolean(payload.mediaUrl);
|
||||||
if (hasMedia) return false;
|
if (hasMedia) return false;
|
||||||
// Use heartbeat mode to check if text is just HEARTBEAT_OK or short ack.
|
// Use heartbeat mode to check if text is just HEARTBEAT_OK or short ack.
|
||||||
const result = stripHeartbeatToken(payload.text, { mode: "heartbeat" });
|
const result = stripHeartbeatToken(payload.text, {
|
||||||
|
mode: "heartbeat",
|
||||||
|
maxAckChars: ackMaxChars,
|
||||||
|
});
|
||||||
return result.shouldSkip;
|
return result.shouldSkip;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveDeliveryTarget(
|
function resolveDeliveryTarget(
|
||||||
cfg: ClawdbotConfig,
|
cfg: ClawdbotConfig,
|
||||||
jobPayload: {
|
jobPayload: {
|
||||||
@@ -366,7 +372,10 @@ export async function runCronIsolatedAgentTurn(params: {
|
|||||||
// Skip delivery for heartbeat-only responses (HEARTBEAT_OK with no real content).
|
// Skip delivery for heartbeat-only responses (HEARTBEAT_OK with no real content).
|
||||||
// This allows cron jobs to silently ack when nothing to report but still deliver
|
// This allows cron jobs to silently ack when nothing to report but still deliver
|
||||||
// actual content when there is something to say.
|
// actual content when there is something to say.
|
||||||
const skipHeartbeatDelivery = delivery && isHeartbeatOnlyResponse(payloads);
|
const ackMaxChars =
|
||||||
|
params.cfg.agent?.heartbeat?.ackMaxChars ?? DEFAULT_HEARTBEAT_ACK_MAX_CHARS;
|
||||||
|
const skipHeartbeatDelivery =
|
||||||
|
delivery && isHeartbeatOnlyResponse(payloads, Math.max(0, ackMaxChars));
|
||||||
|
|
||||||
if (delivery && !skipHeartbeatDelivery) {
|
if (delivery && !skipHeartbeatDelivery) {
|
||||||
if (resolvedDelivery.channel === "whatsapp") {
|
if (resolvedDelivery.channel === "whatsapp") {
|
||||||
|
|||||||
@@ -181,6 +181,64 @@ describe("runHeartbeatOnce", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("respects ackMaxChars for heartbeat acks", async () => {
|
||||||
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-hb-"));
|
||||||
|
const storePath = path.join(tmpDir, "sessions.json");
|
||||||
|
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
|
||||||
|
try {
|
||||||
|
await fs.writeFile(
|
||||||
|
storePath,
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
main: {
|
||||||
|
sessionId: "sid",
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
lastChannel: "whatsapp",
|
||||||
|
lastTo: "+1555",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const cfg: ClawdbotConfig = {
|
||||||
|
agent: {
|
||||||
|
heartbeat: {
|
||||||
|
every: "5m",
|
||||||
|
target: "whatsapp",
|
||||||
|
to: "+1555",
|
||||||
|
ackMaxChars: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
whatsapp: { allowFrom: ["*"] },
|
||||||
|
session: { store: storePath },
|
||||||
|
};
|
||||||
|
|
||||||
|
replySpy.mockResolvedValue({ text: "HEARTBEAT_OK 🦞" });
|
||||||
|
const sendWhatsApp = vi.fn().mockResolvedValue({
|
||||||
|
messageId: "m1",
|
||||||
|
toJid: "jid",
|
||||||
|
});
|
||||||
|
|
||||||
|
await runHeartbeatOnce({
|
||||||
|
cfg,
|
||||||
|
deps: {
|
||||||
|
sendWhatsApp,
|
||||||
|
getQueueSize: () => 0,
|
||||||
|
nowMs: () => 0,
|
||||||
|
webAuthExists: async () => true,
|
||||||
|
hasActiveWebListener: () => true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendWhatsApp).toHaveBeenCalled();
|
||||||
|
} finally {
|
||||||
|
replySpy.mockRestore();
|
||||||
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("skips WhatsApp delivery when not linked or running", async () => {
|
it("skips WhatsApp delivery when not linked or running", async () => {
|
||||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-hb-"));
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-hb-"));
|
||||||
const storePath = path.join(tmpDir, "sessions.json");
|
const storePath = path.join(tmpDir, "sessions.json");
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||||
HEARTBEAT_PROMPT,
|
HEARTBEAT_PROMPT,
|
||||||
stripHeartbeatToken,
|
stripHeartbeatToken,
|
||||||
} from "../auto-reply/heartbeat.js";
|
} from "../auto-reply/heartbeat.js";
|
||||||
@@ -102,6 +103,13 @@ export function resolveHeartbeatPrompt(cfg: ClawdbotConfig) {
|
|||||||
return trimmed || HEARTBEAT_PROMPT;
|
return trimmed || HEARTBEAT_PROMPT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveHeartbeatAckMaxChars(cfg: ClawdbotConfig) {
|
||||||
|
return Math.max(
|
||||||
|
0,
|
||||||
|
cfg.agent?.heartbeat?.ackMaxChars ?? DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function resolveHeartbeatSession(cfg: ClawdbotConfig) {
|
function resolveHeartbeatSession(cfg: ClawdbotConfig) {
|
||||||
const sessionCfg = cfg.session;
|
const sessionCfg = cfg.session;
|
||||||
const scope = sessionCfg?.scope ?? "per-sender";
|
const scope = sessionCfg?.scope ?? "per-sender";
|
||||||
@@ -277,11 +285,12 @@ async function restoreHeartbeatUpdatedAt(params: {
|
|||||||
|
|
||||||
function normalizeHeartbeatReply(
|
function normalizeHeartbeatReply(
|
||||||
payload: ReplyPayload,
|
payload: ReplyPayload,
|
||||||
responsePrefix?: string,
|
responsePrefix: string | undefined,
|
||||||
|
ackMaxChars: number,
|
||||||
) {
|
) {
|
||||||
const stripped = stripHeartbeatToken(payload.text, {
|
const stripped = stripHeartbeatToken(payload.text, {
|
||||||
mode: "heartbeat",
|
mode: "heartbeat",
|
||||||
maxAckChars: 30,
|
maxAckChars: ackMaxChars,
|
||||||
});
|
});
|
||||||
const hasMedia = Boolean(
|
const hasMedia = Boolean(
|
||||||
payload.mediaUrl || (payload.mediaUrls?.length ?? 0) > 0,
|
payload.mediaUrl || (payload.mediaUrls?.length ?? 0) > 0,
|
||||||
@@ -478,9 +487,11 @@ export async function runHeartbeatOnce(opts: {
|
|||||||
return { status: "ran", durationMs: Date.now() - startedAt };
|
return { status: "ran", durationMs: Date.now() - startedAt };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ackMaxChars = resolveHeartbeatAckMaxChars(cfg);
|
||||||
const normalized = normalizeHeartbeatReply(
|
const normalized = normalizeHeartbeatReply(
|
||||||
replyPayload,
|
replyPayload,
|
||||||
cfg.messages?.responsePrefix,
|
cfg.messages?.responsePrefix,
|
||||||
|
ackMaxChars,
|
||||||
);
|
);
|
||||||
if (normalized.shouldSkip && !normalized.hasMedia) {
|
if (normalized.shouldSkip && !normalized.hasMedia) {
|
||||||
await restoreHeartbeatUpdatedAt({
|
await restoreHeartbeatUpdatedAt({
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
parseActivationCommand,
|
parseActivationCommand,
|
||||||
} from "../auto-reply/group-activation.js";
|
} from "../auto-reply/group-activation.js";
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||||
HEARTBEAT_PROMPT,
|
HEARTBEAT_PROMPT,
|
||||||
stripHeartbeatToken,
|
stripHeartbeatToken,
|
||||||
} from "../auto-reply/heartbeat.js";
|
} from "../auto-reply/heartbeat.js";
|
||||||
@@ -369,9 +370,13 @@ export async function runWebHeartbeatOnce(opts: {
|
|||||||
const hasMedia = Boolean(
|
const hasMedia = Boolean(
|
||||||
replyPayload.mediaUrl || (replyPayload.mediaUrls?.length ?? 0) > 0,
|
replyPayload.mediaUrl || (replyPayload.mediaUrls?.length ?? 0) > 0,
|
||||||
);
|
);
|
||||||
|
const ackMaxChars = Math.max(
|
||||||
|
0,
|
||||||
|
cfg.agent?.heartbeat?.ackMaxChars ?? DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
||||||
|
);
|
||||||
const stripped = stripHeartbeatToken(replyPayload.text, {
|
const stripped = stripHeartbeatToken(replyPayload.text, {
|
||||||
mode: "heartbeat",
|
mode: "heartbeat",
|
||||||
maxAckChars: 30,
|
maxAckChars: ackMaxChars,
|
||||||
});
|
});
|
||||||
if (stripped.shouldSkip && !hasMedia) {
|
if (stripped.shouldSkip && !hasMedia) {
|
||||||
// Don't let heartbeats keep sessions alive: restore previous updatedAt so idle expiry still works.
|
// Don't let heartbeats keep sessions alive: restore previous updatedAt so idle expiry still works.
|
||||||
|
|||||||
Reference in New Issue
Block a user