Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21ed547bfc |
No files matched your search
+1
-7
@@ -2,17 +2,11 @@
|
|||||||
|
|
||||||
Docs: https://docs.clawd.bot
|
Docs: https://docs.clawd.bot
|
||||||
|
|
||||||
## 2026.1.22 (Unreleased)
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
- BlueBubbles: stop typing indicator on idle/no-reply. (#1439) Thanks @Nicell.
|
|
||||||
- Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla.
|
|
||||||
- Node: launch node host service with `node run` (not `node start`) and align node host hints/docs. (#1461) Thanks @ameno-.
|
|
||||||
|
|
||||||
## 2026.1.21-2
|
## 2026.1.21-2
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- Control UI: ignore bootstrap identity placeholder text for avatar values and fall back to the default avatar. https://docs.clawd.bot/cli/agents https://docs.clawd.bot/web/control-ui
|
- Control UI: ignore bootstrap identity placeholder text for avatar values and fall back to the default avatar. https://docs.clawd.bot/cli/agents https://docs.clawd.bot/web/control-ui
|
||||||
|
- Hooks: suppress session-memory confirmation output. (#1464) Thanks @alfranli123.
|
||||||
|
|
||||||
## 2026.1.21
|
## 2026.1.21
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ Manage the service:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot node status
|
clawdbot node status
|
||||||
|
clawdbot node run
|
||||||
clawdbot node stop
|
clawdbot node stop
|
||||||
clawdbot node restart
|
clawdbot node restart
|
||||||
clawdbot node uninstall
|
clawdbot node uninstall
|
||||||
|
|||||||
+1
-3
@@ -57,11 +57,9 @@ clawdbot node run --host <gateway-host> --port 18789 --display-name "Build Node"
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot node install --host <gateway-host> --port 18789 --display-name "Build Node"
|
clawdbot node install --host <gateway-host> --port 18789 --display-name "Build Node"
|
||||||
clawdbot node status
|
clawdbot node start
|
||||||
```
|
```
|
||||||
|
|
||||||
Install starts the service. Use `clawdbot node restart` if you need to re-launch it.
|
|
||||||
|
|
||||||
### Pair + name
|
### Pair + name
|
||||||
|
|
||||||
On the gateway host:
|
On the gateway host:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/bluebubbles",
|
"name": "@clawdbot/bluebubbles",
|
||||||
"version": "2026.1.22",
|
"version": "2026.1.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot BlueBubbles channel plugin",
|
"description": "Clawdbot BlueBubbles channel plugin",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
@@ -1563,100 +1563,6 @@ describe("BlueBubbles webhook monitor", () => {
|
|||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("stops typing on idle", async () => {
|
|
||||||
const { sendBlueBubblesTyping } = await import("./chat.js");
|
|
||||||
vi.mocked(sendBlueBubblesTyping).mockClear();
|
|
||||||
|
|
||||||
const account = createMockAccount();
|
|
||||||
const config: ClawdbotConfig = {};
|
|
||||||
const core = createMockRuntime();
|
|
||||||
setBlueBubblesRuntime(core);
|
|
||||||
|
|
||||||
unregister = registerBlueBubblesWebhookTarget({
|
|
||||||
account,
|
|
||||||
config,
|
|
||||||
runtime: { log: vi.fn(), error: vi.fn() },
|
|
||||||
core,
|
|
||||||
path: "/bluebubbles-webhook",
|
|
||||||
});
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
type: "new-message",
|
|
||||||
data: {
|
|
||||||
text: "hello",
|
|
||||||
handle: { address: "+15551234567" },
|
|
||||||
isGroup: false,
|
|
||||||
isFromMe: false,
|
|
||||||
guid: "msg-1",
|
|
||||||
chatGuid: "iMessage;-;+15551234567",
|
|
||||||
date: Date.now(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async (params) => {
|
|
||||||
await params.dispatcherOptions.onReplyStart?.();
|
|
||||||
await params.dispatcherOptions.deliver({ text: "replying now" }, { kind: "final" });
|
|
||||||
await params.dispatcherOptions.onIdle?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
|
||||||
const res = createMockResponse();
|
|
||||||
|
|
||||||
await handleBlueBubblesWebhookRequest(req, res);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
||||||
|
|
||||||
expect(sendBlueBubblesTyping).toHaveBeenCalledWith(
|
|
||||||
expect.any(String),
|
|
||||||
false,
|
|
||||||
expect.any(Object),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("stops typing when no reply is sent", async () => {
|
|
||||||
const { sendBlueBubblesTyping } = await import("./chat.js");
|
|
||||||
vi.mocked(sendBlueBubblesTyping).mockClear();
|
|
||||||
|
|
||||||
const account = createMockAccount();
|
|
||||||
const config: ClawdbotConfig = {};
|
|
||||||
const core = createMockRuntime();
|
|
||||||
setBlueBubblesRuntime(core);
|
|
||||||
|
|
||||||
unregister = registerBlueBubblesWebhookTarget({
|
|
||||||
account,
|
|
||||||
config,
|
|
||||||
runtime: { log: vi.fn(), error: vi.fn() },
|
|
||||||
core,
|
|
||||||
path: "/bluebubbles-webhook",
|
|
||||||
});
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
type: "new-message",
|
|
||||||
data: {
|
|
||||||
text: "hello",
|
|
||||||
handle: { address: "+15551234567" },
|
|
||||||
isGroup: false,
|
|
||||||
isFromMe: false,
|
|
||||||
guid: "msg-1",
|
|
||||||
chatGuid: "iMessage;-;+15551234567",
|
|
||||||
date: Date.now(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async () => undefined);
|
|
||||||
|
|
||||||
const req = createMockRequest("POST", "/bluebubbles-webhook", payload);
|
|
||||||
const res = createMockResponse();
|
|
||||||
|
|
||||||
await handleBlueBubblesWebhookRequest(req, res);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
||||||
|
|
||||||
expect(sendBlueBubblesTyping).toHaveBeenCalledWith(
|
|
||||||
expect.any(String),
|
|
||||||
false,
|
|
||||||
expect.any(Object),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("outbound message ids", () => {
|
describe("outbound message ids", () => {
|
||||||
|
|||||||
@@ -1713,17 +1713,8 @@ async function processMessage(
|
|||||||
runtime.error?.(`[bluebubbles] typing start failed: ${String(err)}`);
|
runtime.error?.(`[bluebubbles] typing start failed: ${String(err)}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onIdle: async () => {
|
onIdle: () => {
|
||||||
if (!chatGuidForActions) return;
|
// BlueBubbles typing stop (DELETE) does not clear bubbles reliably; wait for timeout.
|
||||||
if (!baseUrl || !password) return;
|
|
||||||
try {
|
|
||||||
await sendBlueBubblesTyping(chatGuidForActions, false, {
|
|
||||||
cfg: config,
|
|
||||||
accountId: account.accountId,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
logVerbose(core, runtime, `typing stop failed: ${String(err)}`);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onError: (err, info) => {
|
onError: (err, info) => {
|
||||||
runtime.error?.(`BlueBubbles ${info.kind} reply failed: ${String(err)}`);
|
runtime.error?.(`BlueBubbles ${info.kind} reply failed: ${String(err)}`);
|
||||||
@@ -1763,13 +1754,7 @@ async function processMessage(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (chatGuidForActions && baseUrl && password && !sentMessage) {
|
if (chatGuidForActions && baseUrl && password && !sentMessage) {
|
||||||
// Stop typing indicator when no message was sent (e.g., NO_REPLY)
|
// BlueBubbles typing stop (DELETE) does not clear bubbles reliably; wait for timeout.
|
||||||
sendBlueBubblesTyping(chatGuidForActions, false, {
|
|
||||||
cfg: config,
|
|
||||||
accountId: account.accountId,
|
|
||||||
}).catch((err) => {
|
|
||||||
logVerbose(core, runtime, `typing stop (no reply) failed: ${String(err)}`);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/copilot-proxy",
|
"name": "@clawdbot/copilot-proxy",
|
||||||
"version": "2026.1.22",
|
"version": "2026.1.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot Copilot Proxy provider plugin",
|
"description": "Clawdbot Copilot Proxy provider plugin",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/diagnostics-otel",
|
"name": "@clawdbot/diagnostics-otel",
|
||||||
"version": "2026.1.22",
|
"version": "2026.1.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot diagnostics OpenTelemetry exporter",
|
"description": "Clawdbot diagnostics OpenTelemetry exporter",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/discord",
|
"name": "@clawdbot/discord",
|
||||||
"version": "2026.1.22",
|
"version": "2026.1.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot Discord channel plugin",
|
"description": "Clawdbot Discord channel plugin",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/google-antigravity-auth",
|
"name": "@clawdbot/google-antigravity-auth",
|
||||||
"version": "2026.1.22",
|
"version": "2026.1.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot Google Antigravity OAuth provider plugin",
|
"description": "Clawdbot Google Antigravity OAuth provider plugin",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/google-gemini-cli-auth",
|
"name": "@clawdbot/google-gemini-cli-auth",
|
||||||
"version": "2026.1.22",
|
"version": "2026.1.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot Gemini CLI OAuth provider plugin",
|
"description": "Clawdbot Gemini CLI OAuth provider plugin",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/imessage",
|
"name": "@clawdbot/imessage",
|
||||||
"version": "2026.1.22",
|
"version": "2026.1.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot iMessage channel plugin",
|
"description": "Clawdbot iMessage channel plugin",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/lobster",
|
"name": "@clawdbot/lobster",
|
||||||
"version": "2026.1.22",
|
"version": "2026.1.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)",
|
"description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 2026.1.22
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
- Version alignment with core Clawdbot release numbers.
|
|
||||||
|
|
||||||
## 2026.1.21
|
## 2026.1.21
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/matrix",
|
"name": "@clawdbot/matrix",
|
||||||
"version": "2026.1.22",
|
"version": "2026.1.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot Matrix channel plugin",
|
"description": "Clawdbot Matrix channel plugin",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/memory-core",
|
"name": "@clawdbot/memory-core",
|
||||||
"version": "2026.1.22",
|
"version": "2026.1.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot core memory search plugin",
|
"description": "Clawdbot core memory search plugin",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/memory-lancedb",
|
"name": "@clawdbot/memory-lancedb",
|
||||||
"version": "2026.1.22",
|
"version": "2026.1.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot LanceDB-backed long-term memory plugin with auto-recall/capture",
|
"description": "Clawdbot LanceDB-backed long-term memory plugin with auto-recall/capture",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 2026.1.22
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
- Version alignment with core Clawdbot release numbers.
|
|
||||||
|
|
||||||
## 2026.1.21
|
## 2026.1.21
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/msteams",
|
"name": "@clawdbot/msteams",
|
||||||
"version": "2026.1.22",
|
"version": "2026.1.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot Microsoft Teams channel plugin",
|
"description": "Clawdbot Microsoft Teams channel plugin",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/nextcloud-talk",
|
"name": "@clawdbot/nextcloud-talk",
|
||||||
"version": "2026.1.22",
|
"version": "2026.1.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot Nextcloud Talk channel plugin",
|
"description": "Clawdbot Nextcloud Talk channel plugin",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 2026.1.22
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
- Version alignment with core Clawdbot release numbers.
|
|
||||||
|
|
||||||
## 2026.1.21
|
## 2026.1.21
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/nostr",
|
"name": "@clawdbot/nostr",
|
||||||
"version": "2026.1.22",
|
"version": "2026.1.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot Nostr channel plugin for NIP-04 encrypted DMs",
|
"description": "Clawdbot Nostr channel plugin for NIP-04 encrypted DMs",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/signal",
|
"name": "@clawdbot/signal",
|
||||||
"version": "2026.1.22",
|
"version": "2026.1.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot Signal channel plugin",
|
"description": "Clawdbot Signal channel plugin",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/slack",
|
"name": "@clawdbot/slack",
|
||||||
"version": "2026.1.22",
|
"version": "2026.1.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot Slack channel plugin",
|
"description": "Clawdbot Slack channel plugin",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/telegram",
|
"name": "@clawdbot/telegram",
|
||||||
"version": "2026.1.22",
|
"version": "2026.1.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot Telegram channel plugin",
|
"description": "Clawdbot Telegram channel plugin",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 2026.1.22
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
- Version alignment with core Clawdbot release numbers.
|
|
||||||
|
|
||||||
## 2026.1.21
|
## 2026.1.21
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/voice-call",
|
"name": "@clawdbot/voice-call",
|
||||||
"version": "2026.1.22",
|
"version": "2026.1.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot voice-call plugin",
|
"description": "Clawdbot voice-call plugin",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/whatsapp",
|
"name": "@clawdbot/whatsapp",
|
||||||
"version": "2026.1.22",
|
"version": "2026.1.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot WhatsApp channel plugin",
|
"description": "Clawdbot WhatsApp channel plugin",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 2026.1.22
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
- Version alignment with core Clawdbot release numbers.
|
|
||||||
|
|
||||||
## 2026.1.21
|
## 2026.1.21
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/zalo",
|
"name": "@clawdbot/zalo",
|
||||||
"version": "2026.1.22",
|
"version": "2026.1.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot Zalo channel plugin",
|
"description": "Clawdbot Zalo channel plugin",
|
||||||
"clawdbot": {
|
"clawdbot": {
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 2026.1.22
|
|
||||||
|
|
||||||
### Changes
|
|
||||||
- Version alignment with core Clawdbot release numbers.
|
|
||||||
|
|
||||||
## 2026.1.21
|
## 2026.1.21
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@clawdbot/zalouser",
|
"name": "@clawdbot/zalouser",
|
||||||
"version": "2026.1.22",
|
"version": "2026.1.21",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Clawdbot Zalo Personal Account plugin via zca-cli",
|
"description": "Clawdbot Zalo Personal Account plugin via zca-cli",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "clawdbot",
|
"name": "clawdbot",
|
||||||
"version": "2026.1.22",
|
"version": "2026.1.21-2",
|
||||||
"description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent",
|
"description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ describe("block streaming", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function waitForCalls(fn: () => number, calls: number) {
|
async function waitForCalls(fn: () => number, calls: number) {
|
||||||
const deadline = Date.now() + 5000;
|
const deadline = Date.now() + 1500;
|
||||||
while (fn() < calls) {
|
while (fn() < calls) {
|
||||||
if (Date.now() > deadline) {
|
if (Date.now() > deadline) {
|
||||||
throw new Error(`Expected ${calls} call(s), got ${fn()}`);
|
throw new Error(`Expected ${calls} call(s), got ${fn()}`);
|
||||||
|
|||||||
@@ -1,207 +0,0 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
import type { ModelAliasIndex } from "../../agents/model-selection.js";
|
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
|
||||||
import type { SessionEntry } from "../../config/sessions.js";
|
|
||||||
import { parseInlineDirectives } from "./directive-handling.js";
|
|
||||||
import { handleDirectiveOnly } from "./directive-handling.impl.js";
|
|
||||||
|
|
||||||
// Mock dependencies
|
|
||||||
vi.mock("../../agents/agent-scope.js", () => ({
|
|
||||||
resolveAgentConfig: vi.fn(() => ({})),
|
|
||||||
resolveAgentDir: vi.fn(() => "/tmp/agent"),
|
|
||||||
resolveSessionAgentId: vi.fn(() => "main"),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../../agents/sandbox.js", () => ({
|
|
||||||
resolveSandboxRuntimeStatus: vi.fn(() => ({ sandboxed: false })),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../../config/sessions.js", () => ({
|
|
||||||
updateSessionStore: vi.fn(async () => {}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../../infra/system-events.js", () => ({
|
|
||||||
enqueueSystemEvent: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
function baseAliasIndex(): ModelAliasIndex {
|
|
||||||
return { byAlias: new Map(), byKey: new Map() };
|
|
||||||
}
|
|
||||||
|
|
||||||
function baseConfig(): ClawdbotConfig {
|
|
||||||
return {
|
|
||||||
commands: { text: true },
|
|
||||||
agents: { defaults: {} },
|
|
||||||
} as unknown as ClawdbotConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("handleDirectiveOnly model persist behavior (fixes #1435)", () => {
|
|
||||||
const allowedModelKeys = new Set(["anthropic/claude-opus-4-5", "openai/gpt-4o"]);
|
|
||||||
const allowedModelCatalog = [
|
|
||||||
{ provider: "anthropic", id: "claude-opus-4-5" },
|
|
||||||
{ provider: "openai", id: "gpt-4o" },
|
|
||||||
];
|
|
||||||
|
|
||||||
it("shows success message when session state is available", async () => {
|
|
||||||
const directives = parseInlineDirectives("/model openai/gpt-4o");
|
|
||||||
const sessionEntry: SessionEntry = {
|
|
||||||
sessionId: "s1",
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
};
|
|
||||||
const sessionStore = { "agent:main:dm:1": sessionEntry };
|
|
||||||
|
|
||||||
const result = await handleDirectiveOnly({
|
|
||||||
cfg: baseConfig(),
|
|
||||||
directives,
|
|
||||||
sessionEntry,
|
|
||||||
sessionStore,
|
|
||||||
sessionKey: "agent:main:dm:1",
|
|
||||||
storePath: "/tmp/sessions.json",
|
|
||||||
elevatedEnabled: false,
|
|
||||||
elevatedAllowed: false,
|
|
||||||
defaultProvider: "anthropic",
|
|
||||||
defaultModel: "claude-opus-4-5",
|
|
||||||
aliasIndex: baseAliasIndex(),
|
|
||||||
allowedModelKeys,
|
|
||||||
allowedModelCatalog,
|
|
||||||
resetModelOverride: false,
|
|
||||||
provider: "anthropic",
|
|
||||||
model: "claude-opus-4-5",
|
|
||||||
initialModelLabel: "anthropic/claude-opus-4-5",
|
|
||||||
formatModelSwitchEvent: (label) => `Switched to ${label}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result?.text).toContain("Model set to");
|
|
||||||
expect(result?.text).toContain("openai/gpt-4o");
|
|
||||||
expect(result?.text).not.toContain("failed");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows error message when sessionEntry is missing", async () => {
|
|
||||||
const directives = parseInlineDirectives("/model openai/gpt-4o");
|
|
||||||
const sessionStore = {};
|
|
||||||
|
|
||||||
const result = await handleDirectiveOnly({
|
|
||||||
cfg: baseConfig(),
|
|
||||||
directives,
|
|
||||||
sessionEntry: undefined, // Missing!
|
|
||||||
sessionStore,
|
|
||||||
sessionKey: "agent:main:dm:1",
|
|
||||||
storePath: "/tmp/sessions.json",
|
|
||||||
elevatedEnabled: false,
|
|
||||||
elevatedAllowed: false,
|
|
||||||
defaultProvider: "anthropic",
|
|
||||||
defaultModel: "claude-opus-4-5",
|
|
||||||
aliasIndex: baseAliasIndex(),
|
|
||||||
allowedModelKeys,
|
|
||||||
allowedModelCatalog,
|
|
||||||
resetModelOverride: false,
|
|
||||||
provider: "anthropic",
|
|
||||||
model: "claude-opus-4-5",
|
|
||||||
initialModelLabel: "anthropic/claude-opus-4-5",
|
|
||||||
formatModelSwitchEvent: (label) => `Switched to ${label}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result?.text).toContain("failed");
|
|
||||||
expect(result?.text).toContain("session state unavailable");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows error message when sessionStore is missing", async () => {
|
|
||||||
const directives = parseInlineDirectives("/model openai/gpt-4o");
|
|
||||||
const sessionEntry: SessionEntry = {
|
|
||||||
sessionId: "s1",
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await handleDirectiveOnly({
|
|
||||||
cfg: baseConfig(),
|
|
||||||
directives,
|
|
||||||
sessionEntry,
|
|
||||||
sessionStore: undefined, // Missing!
|
|
||||||
sessionKey: "agent:main:dm:1",
|
|
||||||
storePath: "/tmp/sessions.json",
|
|
||||||
elevatedEnabled: false,
|
|
||||||
elevatedAllowed: false,
|
|
||||||
defaultProvider: "anthropic",
|
|
||||||
defaultModel: "claude-opus-4-5",
|
|
||||||
aliasIndex: baseAliasIndex(),
|
|
||||||
allowedModelKeys,
|
|
||||||
allowedModelCatalog,
|
|
||||||
resetModelOverride: false,
|
|
||||||
provider: "anthropic",
|
|
||||||
model: "claude-opus-4-5",
|
|
||||||
initialModelLabel: "anthropic/claude-opus-4-5",
|
|
||||||
formatModelSwitchEvent: (label) => `Switched to ${label}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result?.text).toContain("failed");
|
|
||||||
expect(result?.text).toContain("session state unavailable");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows error message when sessionKey is missing", async () => {
|
|
||||||
const directives = parseInlineDirectives("/model openai/gpt-4o");
|
|
||||||
const sessionEntry: SessionEntry = {
|
|
||||||
sessionId: "s1",
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
};
|
|
||||||
const sessionStore = { "agent:main:dm:1": sessionEntry };
|
|
||||||
|
|
||||||
const result = await handleDirectiveOnly({
|
|
||||||
cfg: baseConfig(),
|
|
||||||
directives,
|
|
||||||
sessionEntry,
|
|
||||||
sessionStore,
|
|
||||||
sessionKey: undefined, // Missing!
|
|
||||||
storePath: "/tmp/sessions.json",
|
|
||||||
elevatedEnabled: false,
|
|
||||||
elevatedAllowed: false,
|
|
||||||
defaultProvider: "anthropic",
|
|
||||||
defaultModel: "claude-opus-4-5",
|
|
||||||
aliasIndex: baseAliasIndex(),
|
|
||||||
allowedModelKeys,
|
|
||||||
allowedModelCatalog,
|
|
||||||
resetModelOverride: false,
|
|
||||||
provider: "anthropic",
|
|
||||||
model: "claude-opus-4-5",
|
|
||||||
initialModelLabel: "anthropic/claude-opus-4-5",
|
|
||||||
formatModelSwitchEvent: (label) => `Switched to ${label}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result?.text).toContain("failed");
|
|
||||||
expect(result?.text).toContain("session state unavailable");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows no model message when no /model directive", async () => {
|
|
||||||
const directives = parseInlineDirectives("hello world");
|
|
||||||
const sessionEntry: SessionEntry = {
|
|
||||||
sessionId: "s1",
|
|
||||||
updatedAt: Date.now(),
|
|
||||||
};
|
|
||||||
const sessionStore = { "agent:main:dm:1": sessionEntry };
|
|
||||||
|
|
||||||
const result = await handleDirectiveOnly({
|
|
||||||
cfg: baseConfig(),
|
|
||||||
directives,
|
|
||||||
sessionEntry,
|
|
||||||
sessionStore,
|
|
||||||
sessionKey: "agent:main:dm:1",
|
|
||||||
storePath: "/tmp/sessions.json",
|
|
||||||
elevatedEnabled: false,
|
|
||||||
elevatedAllowed: false,
|
|
||||||
defaultProvider: "anthropic",
|
|
||||||
defaultModel: "claude-opus-4-5",
|
|
||||||
aliasIndex: baseAliasIndex(),
|
|
||||||
allowedModelKeys,
|
|
||||||
allowedModelCatalog,
|
|
||||||
resetModelOverride: false,
|
|
||||||
provider: "anthropic",
|
|
||||||
model: "claude-opus-4-5",
|
|
||||||
initialModelLabel: "anthropic/claude-opus-4-5",
|
|
||||||
formatModelSwitchEvent: (label) => `Switched to ${label}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// No model directive = no model message
|
|
||||||
expect(result?.text ?? "").not.toContain("Model set to");
|
|
||||||
expect(result?.text ?? "").not.toContain("failed");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -288,7 +288,6 @@ export async function handleDirectiveOnly(params: {
|
|||||||
nextThinkLevel === "xhigh" &&
|
nextThinkLevel === "xhigh" &&
|
||||||
!supportsXHighThinking(resolvedProvider, resolvedModel);
|
!supportsXHighThinking(resolvedProvider, resolvedModel);
|
||||||
|
|
||||||
let didPersistModel = false;
|
|
||||||
if (sessionEntry && sessionStore && sessionKey) {
|
if (sessionEntry && sessionStore && sessionKey) {
|
||||||
const prevElevatedLevel =
|
const prevElevatedLevel =
|
||||||
currentElevatedLevel ??
|
currentElevatedLevel ??
|
||||||
@@ -347,7 +346,6 @@ export async function handleDirectiveOnly(params: {
|
|||||||
selection: modelSelection,
|
selection: modelSelection,
|
||||||
profileOverride,
|
profileOverride,
|
||||||
});
|
});
|
||||||
didPersistModel = true;
|
|
||||||
}
|
}
|
||||||
if (directives.hasQueueDirective && directives.queueReset) {
|
if (directives.hasQueueDirective && directives.queueReset) {
|
||||||
delete sessionEntry.queueMode;
|
delete sessionEntry.queueMode;
|
||||||
@@ -449,7 +447,7 @@ export async function handleDirectiveOnly(params: {
|
|||||||
`Thinking level set to high (xhigh not supported for ${resolvedProvider}/${resolvedModel}).`,
|
`Thinking level set to high (xhigh not supported for ${resolvedProvider}/${resolvedModel}).`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (modelSelection && didPersistModel) {
|
if (modelSelection) {
|
||||||
const label = `${modelSelection.provider}/${modelSelection.model}`;
|
const label = `${modelSelection.provider}/${modelSelection.model}`;
|
||||||
const labelWithAlias = modelSelection.alias ? `${modelSelection.alias} (${label})` : label;
|
const labelWithAlias = modelSelection.alias ? `${modelSelection.alias} (${label})` : label;
|
||||||
parts.push(
|
parts.push(
|
||||||
@@ -460,10 +458,6 @@ export async function handleDirectiveOnly(params: {
|
|||||||
if (profileOverride) {
|
if (profileOverride) {
|
||||||
parts.push(`Auth profile set to ${profileOverride}.`);
|
parts.push(`Auth profile set to ${profileOverride}.`);
|
||||||
}
|
}
|
||||||
} else if (modelSelection && !didPersistModel) {
|
|
||||||
parts.push(
|
|
||||||
`Model switch to ${modelSelection.provider}/${modelSelection.model} failed (session state unavailable).`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (directives.hasQueueDirective && directives.queueMode) {
|
if (directives.hasQueueDirective && directives.queueMode) {
|
||||||
parts.push(formatDirectiveAck(`Queue mode set to ${directives.queueMode}.`));
|
parts.push(formatDirectiveAck(`Queue mode set to ${directives.queueMode}.`));
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ type NodeDaemonStatusOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function renderNodeServiceStartHints(): string[] {
|
function renderNodeServiceStartHints(): string[] {
|
||||||
const base = [formatCliCommand("clawdbot node install"), formatCliCommand("clawdbot node run")];
|
const base = [formatCliCommand("clawdbot node install"), formatCliCommand("clawdbot node start")];
|
||||||
switch (process.platform) {
|
switch (process.platform) {
|
||||||
case "darwin":
|
case "darwin":
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ export async function resolveNodeProgramArguments(params: {
|
|||||||
runtime?: GatewayRuntimePreference;
|
runtime?: GatewayRuntimePreference;
|
||||||
nodePath?: string;
|
nodePath?: string;
|
||||||
}): Promise<GatewayProgramArgs> {
|
}): Promise<GatewayProgramArgs> {
|
||||||
const args = ["node", "run", "--host", params.host, "--port", String(params.port)];
|
const args = ["node", "start", "--host", params.host, "--port", String(params.port)];
|
||||||
if (params.tls || params.tlsFingerprint) args.push("--tls");
|
if (params.tls || params.tlsFingerprint) args.push("--tls");
|
||||||
if (params.tlsFingerprint) args.push("--tls-fingerprint", params.tlsFingerprint);
|
if (params.tlsFingerprint) args.push("--tls-fingerprint", params.tlsFingerprint);
|
||||||
if (params.nodeId) args.push("--node-id", params.nodeId);
|
if (params.nodeId) args.push("--node-id", params.nodeId);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ When you run `/new` to start a fresh session:
|
|||||||
2. **Extracts conversation** - Reads the last 15 lines of conversation from the session
|
2. **Extracts conversation** - Reads the last 15 lines of conversation from the session
|
||||||
3. **Generates descriptive slug** - Uses LLM to create a meaningful filename slug based on conversation content
|
3. **Generates descriptive slug** - Uses LLM to create a meaningful filename slug based on conversation content
|
||||||
4. **Saves to memory** - Creates a new file at `<workspace>/memory/YYYY-MM-DD-slug.md`
|
4. **Saves to memory** - Creates a new file at `<workspace>/memory/YYYY-MM-DD-slug.md`
|
||||||
5. **Sends confirmation** - Notifies you with the file path
|
5. **Logs completion** - Writes the file path to logs (no user-visible confirmation)
|
||||||
|
|
||||||
## Output Format
|
## Output Format
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import handler from "./handler.js";
|
||||||
|
import { createHookEvent } from "../../hooks.js";
|
||||||
|
import type { ClawdbotConfig } from "../../../config/config.js";
|
||||||
|
import { makeTempWorkspace } from "../../../test-helpers/workspace.js";
|
||||||
|
|
||||||
|
describe("session-memory hook", () => {
|
||||||
|
it("writes a memory entry without queuing a user confirmation", async () => {
|
||||||
|
const workspaceDir = await makeTempWorkspace("clawdbot-session-memory-");
|
||||||
|
const cfg: ClawdbotConfig = {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
workspace: workspaceDir,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const context: Record<string, unknown> = {
|
||||||
|
cfg,
|
||||||
|
sessionEntry: {
|
||||||
|
sessionId: "session-123",
|
||||||
|
sessionFile: "",
|
||||||
|
},
|
||||||
|
commandSource: "test",
|
||||||
|
};
|
||||||
|
|
||||||
|
const event = createHookEvent("command", "new", "agent:main:main", context);
|
||||||
|
await handler(event);
|
||||||
|
|
||||||
|
expect(event.messages).toEqual([]);
|
||||||
|
|
||||||
|
const memoryDir = path.join(workspaceDir, "memory");
|
||||||
|
const files = await fs.readdir(memoryDir);
|
||||||
|
expect(files).toHaveLength(1);
|
||||||
|
|
||||||
|
const entry = await fs.readFile(path.join(memoryDir, files[0] ?? ""), "utf-8");
|
||||||
|
expect(entry).toContain("Session Key");
|
||||||
|
expect(entry).toContain("session-123");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user