fix: keep node presence fresh
This commit is contained in:
13
CHANGELOG.md
13
CHANGELOG.md
@@ -9,29 +9,24 @@
|
|||||||
- Sessions: primary session key is fixed to `main` (or `global` for global scope); `session.mainKey` is ignored.
|
- Sessions: primary session key is fixed to `main` (or `global` for global scope); `session.mainKey` is ignored.
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
- Highlight: agent-to-agent ping-pong (reply-back loop) with `REPLY_SKIP` plus target announce step with `ANNOUNCE_SKIP` (max turns configurable, 0–5).
|
|
||||||
- Gateway: support `gateway.port` + `CLAWDIS_GATEWAY_PORT` across CLI, TUI, and macOS app.
|
- Gateway: support `gateway.port` + `CLAWDIS_GATEWAY_PORT` across CLI, TUI, and macOS app.
|
||||||
- Gateway: add config hot reload with hybrid restart strategy (`gateway.reload`) and per-section reload handling.
|
- Gateway: add config hot reload with hybrid restart strategy (`gateway.reload`) and per-section reload handling.
|
||||||
- UI: centralize tool display metadata and show action/detail summaries across Web Chat, SwiftUI, Android, and the TUI.
|
- UI: centralize tool display metadata and show action/detail summaries across Web Chat, SwiftUI, Android, and the TUI.
|
||||||
- Control UI: support configurable base paths (`gateway.controlUi.basePath`, default unchanged) for hosting under URL prefixes.
|
- Control UI: support configurable base paths (`gateway.controlUi.basePath`, default unchanged) for hosting under URL prefixes.
|
||||||
- Onboarding: shared wizard engine powering CLI + macOS via gateway wizard RPC.
|
- Onboarding: shared wizard engine powering CLI + macOS via gateway wizard RPC.
|
||||||
- Config: expose schema + UI hints for generic config forms (Web UI + future clients).
|
- Config: expose schema + UI hints for generic config forms (Web UI + future clients).
|
||||||
- Browser: add multi-profile browser control with per-profile remote CDP URLs — thanks @jamesgroat.
|
|
||||||
- Skills: add blogwatcher skill for RSS/Atom monitoring — thanks @Hyaxia.
|
- Skills: add blogwatcher skill for RSS/Atom monitoring — thanks @Hyaxia.
|
||||||
- Skills: add Notion API skill — thanks @scald.
|
|
||||||
- Discord: emit system events for reaction add/remove with per-guild reaction notifications (off|own|all|allowlist) (#140) — thanks @thewilloftheshadow.
|
- Discord: emit system events for reaction add/remove with per-guild reaction notifications (off|own|all|allowlist) (#140) — thanks @thewilloftheshadow.
|
||||||
- Slack: add socket-mode connector, tools, and UI/docs updates (#170) — thanks @thewilloftheshadow.
|
|
||||||
- Agent: add optional per-session Docker sandbox for tool execution (`agent.sandbox`) with allow/deny policy and auto-pruning.
|
- Agent: add optional per-session Docker sandbox for tool execution (`agent.sandbox`) with allow/deny policy and auto-pruning.
|
||||||
- Agent: add sandboxed Chromium browser (CDP + optional noVNC observer) for sandboxed sessions.
|
- Agent: add sandboxed Chromium browser (CDP + optional noVNC observer) for sandboxed sessions.
|
||||||
- Nodes: add `location.get` with Always/Precise settings on macOS/iOS/Android plus CLI/tool support.
|
- Nodes: add `location.get` with Always/Precise settings on macOS/iOS/Android plus CLI/tool support.
|
||||||
- Android nodes: add `sms.send` with permission-gated capability refresh (#172) — thanks @vsabavat.
|
- Sessions: add agent‑to‑agent post step with `ANNOUNCE_SKIP` to suppress channel announcements.
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- macOS: improve Swift 6 strict concurrency compatibility (#166) — thanks @Nachx639.
|
- Gateway/macOS: keep node presence fresh with periodic beacons + show presence status in Instances (#168) — thanks @mbelinky.
|
||||||
- CI: fix lint ordering after merge cleanup (#156) — thanks @steipete.
|
- CI: fix lint ordering after merge cleanup (#156) — thanks @steipete.
|
||||||
- CI: consolidate checks to avoid redundant installs (#144) — thanks @thewilloftheshadow.
|
- CI: consolidate checks to avoid redundant installs (#144) — thanks @thewilloftheshadow.
|
||||||
- WhatsApp: support `gifPlayback` for MP4 GIF sends via CLI/gateway.
|
- WhatsApp: support `gifPlayback` for MP4 GIF sends via CLI/gateway.
|
||||||
- Gateway: log config hot reloads for dynamic-read changes without restarts.
|
|
||||||
- Sessions: prevent `sessions_send` timeouts by running nested agent turns on a separate lane.
|
- Sessions: prevent `sessions_send` timeouts by running nested agent turns on a separate lane.
|
||||||
- Sessions: use per-send run IDs for gateway agent calls to avoid wait collisions.
|
- Sessions: use per-send run IDs for gateway agent calls to avoid wait collisions.
|
||||||
- Auto-reply: drop final payloads when block streaming to avoid duplicate Discord sends.
|
- Auto-reply: drop final payloads when block streaming to avoid duplicate Discord sends.
|
||||||
@@ -64,7 +59,6 @@
|
|||||||
- Build: drop stale ClawdisCLI product from macOS build-and-run script.
|
- Build: drop stale ClawdisCLI product from macOS build-and-run script.
|
||||||
- Auto-reply: add run-level telemetry + typing TTL guardrails to diagnose stuck replies.
|
- Auto-reply: add run-level telemetry + typing TTL guardrails to diagnose stuck replies.
|
||||||
- WhatsApp: honor per-group mention gating overrides when group ids are stored as session keys.
|
- WhatsApp: honor per-group mention gating overrides when group ids are stored as session keys.
|
||||||
- Slack: add missing deps and wire Slack into cron/heartbeat/hook delivery.
|
|
||||||
- Dependencies: bump pi-mono packages to 0.32.3.
|
- Dependencies: bump pi-mono packages to 0.32.3.
|
||||||
|
|
||||||
### Docs
|
### Docs
|
||||||
@@ -79,8 +73,7 @@
|
|||||||
- Queue: clarify steer-backlog behavior with inline commands and update examples for streaming surfaces.
|
- Queue: clarify steer-backlog behavior with inline commands and update examples for streaming surfaces.
|
||||||
- Sandbox: document per-session agent sandbox setup, browser image, and Docker build.
|
- Sandbox: document per-session agent sandbox setup, browser image, and Docker build.
|
||||||
- macOS: clarify menu bar uses sessionKey from agent events.
|
- macOS: clarify menu bar uses sessionKey from agent events.
|
||||||
- Sessions: document agent-to-agent reply loop (`REPLY_SKIP`) and announce step (`ANNOUNCE_SKIP`).
|
- Sessions: document agent-to-agent post step and `ANNOUNCE_SKIP`.
|
||||||
- Skills: clarify wacli third-party messaging scope and JID format examples.
|
|
||||||
|
|
||||||
## 2.0.0-beta5 — 2026-01-03
|
## 2.0.0-beta5 — 2026-01-03
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ struct InstancesSettings: View {
|
|||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
Text(inst.host ?? "unknown host").font(.subheadline.bold())
|
Text(inst.host ?? "unknown host").font(.subheadline.bold())
|
||||||
|
self.presenceIndicator(inst)
|
||||||
if let ip = inst.ip { Text("(") + Text(ip).monospaced() + Text(")") }
|
if let ip = inst.ip { Text("(") + Text(ip).monospaced() + Text(")") }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +147,29 @@ struct InstancesSettings: View {
|
|||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func presenceIndicator(_ inst: InstanceInfo) -> some View {
|
||||||
|
let status = self.presenceStatus(for: inst)
|
||||||
|
return HStack(spacing: 4) {
|
||||||
|
Circle()
|
||||||
|
.fill(status.color)
|
||||||
|
.frame(width: 6, height: 6)
|
||||||
|
.accessibilityHidden(true)
|
||||||
|
Text(status.label)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.font(.caption)
|
||||||
|
.help("Presence updated \(inst.ageDescription).")
|
||||||
|
.accessibilityLabel("\(status.label) presence")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func presenceStatus(for inst: InstanceInfo) -> (label: String, color: Color) {
|
||||||
|
let nowMs = Date().timeIntervalSince1970 * 1000
|
||||||
|
let ageSeconds = max(0, Int((nowMs - inst.ts) / 1000))
|
||||||
|
if ageSeconds <= 120 { return ("Active", .green) }
|
||||||
|
if ageSeconds <= 300 { return ("Idle", .yellow) }
|
||||||
|
return ("Stale", .gray)
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func leadingDeviceIcon(_ inst: InstanceInfo, device: DevicePresentation?) -> some View {
|
private func leadingDeviceIcon(_ inst: InstanceInfo, device: DevicePresentation?) -> some View {
|
||||||
let symbol = self.leadingDeviceSymbol(inst, device: device)
|
let symbol = self.leadingDeviceSymbol(inst, device: device)
|
||||||
@@ -307,6 +331,10 @@ struct InstancesSettings: View {
|
|||||||
return "Connect"
|
return "Connect"
|
||||||
case "disconnect":
|
case "disconnect":
|
||||||
return "Disconnect"
|
return "Disconnect"
|
||||||
|
case "node-connected":
|
||||||
|
return "Node connect"
|
||||||
|
case "node-disconnected":
|
||||||
|
return "Node disconnect"
|
||||||
case "launch":
|
case "launch":
|
||||||
return "Launch"
|
return "Launch"
|
||||||
case "periodic":
|
case "periodic":
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ Pairing details: `docs/gateway/pairing.md`.
|
|||||||
|
|
||||||
## 5) Verify the node is connected
|
## 5) Verify the node is connected
|
||||||
|
|
||||||
- In the macOS app: **Instances** tab should show something like `iOS Node (...)`.
|
- In the macOS app: **Instances** tab should show something like `iOS Node (...)` with a green “Active” presence dot shortly after connect.
|
||||||
- Via nodes status (paired + connected):
|
- Via nodes status (paired + connected):
|
||||||
```bash
|
```bash
|
||||||
clawdis nodes status
|
clawdis nodes status
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Presence entries are structured objects with (some) fields:
|
|||||||
- `modelIdentifier` (optional): hardware model identifier like `iPad16,6` or `Mac16,6`
|
- `modelIdentifier` (optional): hardware model identifier like `iPad16,6` or `Mac16,6`
|
||||||
- `mode`: e.g. `gateway`, `app`, `webchat`, `cli`
|
- `mode`: e.g. `gateway`, `app`, `webchat`, `cli`
|
||||||
- `lastInputSeconds` (optional): “seconds since last user input” for that client machine
|
- `lastInputSeconds` (optional): “seconds since last user input” for that client machine
|
||||||
- `reason`: a short marker like `self`, `connect`, `periodic`, `instances-refresh`
|
- `reason`: a short marker like `self`, `connect`, `node-connected`, `node-disconnected`, `periodic`, `instances-refresh`
|
||||||
- `text`: legacy/debug summary string (kept for backwards compatibility and UI display)
|
- `text`: legacy/debug summary string (kept for backwards compatibility and UI display)
|
||||||
- `ts`: last update timestamp (ms since epoch)
|
- `ts`: last update timestamp (ms since epoch)
|
||||||
|
|
||||||
@@ -61,6 +61,16 @@ Implementation:
|
|||||||
- Gateway: `src/gateway/server.ts` handles method `system-event` by calling `updateSystemPresence(...)`.
|
- Gateway: `src/gateway/server.ts` handles method `system-event` by calling `updateSystemPresence(...)`.
|
||||||
- mac app beaconing: `apps/macos/Sources/Clawdis/PresenceReporter.swift`.
|
- mac app beaconing: `apps/macos/Sources/Clawdis/PresenceReporter.swift`.
|
||||||
|
|
||||||
|
### 4) Node bridge beacons (gateway-owned presence)
|
||||||
|
|
||||||
|
When a node bridge connection authenticates, the Gateway emits a presence entry
|
||||||
|
for that node and starts periodic refresh beacons so it does not expire.
|
||||||
|
|
||||||
|
- Connect/disconnect markers: `node-connected`, `node-disconnected`
|
||||||
|
- Periodic heartbeat: every 3 minutes (`reason: periodic`)
|
||||||
|
|
||||||
|
Implementation: `src/gateway/server.ts` (node bridge handlers + timer beacons).
|
||||||
|
|
||||||
## Merge + dedupe rules (why `instanceId` matters)
|
## Merge + dedupe rules (why `instanceId` matters)
|
||||||
|
|
||||||
All producers write into a single in-memory presence map.
|
All producers write into a single in-memory presence map.
|
||||||
@@ -109,6 +119,9 @@ Implementation:
|
|||||||
- View: `apps/macos/Sources/Clawdis/InstancesSettings.swift`
|
- View: `apps/macos/Sources/Clawdis/InstancesSettings.swift`
|
||||||
- Store: `apps/macos/Sources/Clawdis/InstancesStore.swift`
|
- Store: `apps/macos/Sources/Clawdis/InstancesStore.swift`
|
||||||
|
|
||||||
|
The Instances rows show a small presence indicator (Active/Idle/Stale) based on
|
||||||
|
the last beacon age. The label is derived from the entry timestamp (`ts`).
|
||||||
|
|
||||||
The store refreshes periodically and also applies `presence` WS events.
|
The store refreshes periodically and also applies `presence` WS events.
|
||||||
|
|
||||||
## Debugging tips
|
## Debugging tips
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ git worktree remove /tmp/issue-99
|
|||||||
|
|
||||||
When submitting PRs to external repos, use this format for quality & maintainer-friendliness:
|
When submitting PRs to external repos, use this format for quality & maintainer-friendliness:
|
||||||
|
|
||||||
```markdown
|
````markdown
|
||||||
## Original Prompt
|
## Original Prompt
|
||||||
[Exact request/problem statement]
|
[Exact request/problem statement]
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ When submitting PRs to external repos, use this format for quality & maintainer-
|
|||||||
# Example
|
# Example
|
||||||
command example
|
command example
|
||||||
```
|
```
|
||||||
```
|
````
|
||||||
|
|
||||||
## Feature intent (maintainer-friendly)
|
## Feature intent (maintainer-friendly)
|
||||||
[Why useful, how it fits, workflows it enables]
|
[Why useful, how it fits, workflows it enables]
|
||||||
|
|||||||
@@ -7,11 +7,7 @@ vi.mock("../gateway/call.js", () => ({
|
|||||||
|
|
||||||
vi.mock("../config/config.js", () => ({
|
vi.mock("../config/config.js", () => ({
|
||||||
loadConfig: () => ({
|
loadConfig: () => ({
|
||||||
session: {
|
session: { mainKey: "main", scope: "per-sender" },
|
||||||
mainKey: "main",
|
|
||||||
scope: "per-sender",
|
|
||||||
agentToAgent: { maxPingPongTurns: 2 },
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
resolveGatewayPort: () => 18789,
|
resolveGatewayPort: () => 18789,
|
||||||
}));
|
}));
|
||||||
@@ -131,28 +127,18 @@ describe("sessions tools", () => {
|
|||||||
let agentCallCount = 0;
|
let agentCallCount = 0;
|
||||||
let _historyCallCount = 0;
|
let _historyCallCount = 0;
|
||||||
let sendCallCount = 0;
|
let sendCallCount = 0;
|
||||||
let lastWaitedRunId: string | undefined;
|
let waitRunId: string | undefined;
|
||||||
const replyByRunId = new Map<string, string>();
|
let nextHistoryIsWaitReply = false;
|
||||||
const requesterKey = "discord:group:req";
|
|
||||||
callGatewayMock.mockImplementation(async (opts: unknown) => {
|
callGatewayMock.mockImplementation(async (opts: unknown) => {
|
||||||
const request = opts as { method?: string; params?: unknown };
|
const request = opts as { method?: string; params?: unknown };
|
||||||
calls.push(request);
|
calls.push(request);
|
||||||
if (request.method === "agent") {
|
if (request.method === "agent") {
|
||||||
agentCallCount += 1;
|
agentCallCount += 1;
|
||||||
const runId = `run-${agentCallCount}`;
|
const runId = `run-${agentCallCount}`;
|
||||||
const params = request.params as
|
const params = request.params as { message?: string } | undefined;
|
||||||
| { message?: string; sessionKey?: string }
|
if (params?.message === "wait") {
|
||||||
| undefined;
|
waitRunId = runId;
|
||||||
const message = params?.message ?? "";
|
|
||||||
let reply = "REPLY_SKIP";
|
|
||||||
if (message === "ping" || message === "wait") {
|
|
||||||
reply = "done";
|
|
||||||
} else if (message === "Agent-to-agent announce step.") {
|
|
||||||
reply = "ANNOUNCE_SKIP";
|
|
||||||
} else if (params?.sessionKey === requesterKey) {
|
|
||||||
reply = "pong";
|
|
||||||
}
|
}
|
||||||
replyByRunId.set(runId, reply);
|
|
||||||
return {
|
return {
|
||||||
runId,
|
runId,
|
||||||
status: "accepted",
|
status: "accepted",
|
||||||
@@ -161,13 +147,15 @@ describe("sessions tools", () => {
|
|||||||
}
|
}
|
||||||
if (request.method === "agent.wait") {
|
if (request.method === "agent.wait") {
|
||||||
const params = request.params as { runId?: string } | undefined;
|
const params = request.params as { runId?: string } | undefined;
|
||||||
lastWaitedRunId = params?.runId;
|
if (params?.runId && params.runId === waitRunId) {
|
||||||
|
nextHistoryIsWaitReply = true;
|
||||||
|
}
|
||||||
return { runId: params?.runId ?? "run-1", status: "ok" };
|
return { runId: params?.runId ?? "run-1", status: "ok" };
|
||||||
}
|
}
|
||||||
if (request.method === "chat.history") {
|
if (request.method === "chat.history") {
|
||||||
_historyCallCount += 1;
|
_historyCallCount += 1;
|
||||||
const text =
|
const text = nextHistoryIsWaitReply ? "done" : "ANNOUNCE_SKIP";
|
||||||
(lastWaitedRunId && replyByRunId.get(lastWaitedRunId)) ?? "";
|
nextHistoryIsWaitReply = false;
|
||||||
return {
|
return {
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
@@ -190,10 +178,9 @@ describe("sessions tools", () => {
|
|||||||
return {};
|
return {};
|
||||||
});
|
});
|
||||||
|
|
||||||
const tool = createClawdisTools({
|
const tool = createClawdisTools().find(
|
||||||
agentSessionKey: requesterKey,
|
(candidate) => candidate.name === "sessions_send",
|
||||||
agentSurface: "discord",
|
);
|
||||||
}).find((candidate) => candidate.name === "sessions_send");
|
|
||||||
expect(tool).toBeDefined();
|
expect(tool).toBeDefined();
|
||||||
if (!tool) throw new Error("missing sessions_send tool");
|
if (!tool) throw new Error("missing sessions_send tool");
|
||||||
|
|
||||||
@@ -204,7 +191,6 @@ describe("sessions tools", () => {
|
|||||||
});
|
});
|
||||||
expect(fire.details).toMatchObject({ status: "accepted", runId: "run-1" });
|
expect(fire.details).toMatchObject({ status: "accepted", runId: "run-1" });
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
||||||
|
|
||||||
const waitPromise = tool.execute("call6", {
|
const waitPromise = tool.execute("call6", {
|
||||||
sessionKey: "main",
|
sessionKey: "main",
|
||||||
@@ -218,14 +204,13 @@ describe("sessions tools", () => {
|
|||||||
});
|
});
|
||||||
expect(typeof (waited.details as { runId?: string }).runId).toBe("string");
|
expect(typeof (waited.details as { runId?: string }).runId).toBe("string");
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
||||||
|
|
||||||
const agentCalls = calls.filter((call) => call.method === "agent");
|
const agentCalls = calls.filter((call) => call.method === "agent");
|
||||||
const waitCalls = calls.filter((call) => call.method === "agent.wait");
|
const waitCalls = calls.filter((call) => call.method === "agent.wait");
|
||||||
const historyOnlyCalls = calls.filter(
|
const historyOnlyCalls = calls.filter(
|
||||||
(call) => call.method === "chat.history",
|
(call) => call.method === "chat.history",
|
||||||
);
|
);
|
||||||
expect(agentCalls).toHaveLength(8);
|
expect(agentCalls).toHaveLength(4);
|
||||||
for (const call of agentCalls) {
|
for (const call of agentCalls) {
|
||||||
expect(call.params).toMatchObject({ lane: "nested" });
|
expect(call.params).toMatchObject({ lane: "nested" });
|
||||||
}
|
}
|
||||||
@@ -246,21 +231,11 @@ describe("sessions tools", () => {
|
|||||||
?.extraSystemPrompt === "string" &&
|
?.extraSystemPrompt === "string" &&
|
||||||
(
|
(
|
||||||
call.params as { extraSystemPrompt?: string }
|
call.params as { extraSystemPrompt?: string }
|
||||||
)?.extraSystemPrompt?.includes("Agent-to-agent reply step"),
|
)?.extraSystemPrompt?.includes("Agent-to-agent post step"),
|
||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(
|
expect(waitCalls).toHaveLength(3);
|
||||||
agentCalls.some(
|
expect(historyOnlyCalls).toHaveLength(3);
|
||||||
(call) =>
|
|
||||||
typeof (call.params as { extraSystemPrompt?: string })
|
|
||||||
?.extraSystemPrompt === "string" &&
|
|
||||||
(
|
|
||||||
call.params as { extraSystemPrompt?: string }
|
|
||||||
)?.extraSystemPrompt?.includes("Agent-to-agent announce step"),
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
expect(waitCalls).toHaveLength(8);
|
|
||||||
expect(historyOnlyCalls).toHaveLength(8);
|
|
||||||
expect(
|
expect(
|
||||||
waitCalls.some(
|
waitCalls.some(
|
||||||
(call) =>
|
(call) =>
|
||||||
@@ -269,110 +244,4 @@ describe("sessions tools", () => {
|
|||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(sendCallCount).toBe(0);
|
expect(sendCallCount).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sessions_send runs ping-pong then announces", async () => {
|
|
||||||
callGatewayMock.mockReset();
|
|
||||||
const calls: Array<{ method?: string; params?: unknown }> = [];
|
|
||||||
let agentCallCount = 0;
|
|
||||||
let lastWaitedRunId: string | undefined;
|
|
||||||
const replyByRunId = new Map<string, string>();
|
|
||||||
const requesterKey = "discord:group:req";
|
|
||||||
const targetKey = "discord:group:target";
|
|
||||||
let sendParams: { to?: string; provider?: string; message?: string } = {};
|
|
||||||
callGatewayMock.mockImplementation(async (opts: unknown) => {
|
|
||||||
const request = opts as { method?: string; params?: unknown };
|
|
||||||
calls.push(request);
|
|
||||||
if (request.method === "agent") {
|
|
||||||
agentCallCount += 1;
|
|
||||||
const runId = `run-${agentCallCount}`;
|
|
||||||
const params = request.params as
|
|
||||||
| {
|
|
||||||
message?: string;
|
|
||||||
sessionKey?: string;
|
|
||||||
extraSystemPrompt?: string;
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
let reply = "initial";
|
|
||||||
if (params?.extraSystemPrompt?.includes("Agent-to-agent reply step")) {
|
|
||||||
reply = params.sessionKey === requesterKey ? "pong-1" : "pong-2";
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
params?.extraSystemPrompt?.includes("Agent-to-agent announce step")
|
|
||||||
) {
|
|
||||||
reply = "announce now";
|
|
||||||
}
|
|
||||||
replyByRunId.set(runId, reply);
|
|
||||||
return {
|
|
||||||
runId,
|
|
||||||
status: "accepted",
|
|
||||||
acceptedAt: 2000 + agentCallCount,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (request.method === "agent.wait") {
|
|
||||||
const params = request.params as { runId?: string } | undefined;
|
|
||||||
lastWaitedRunId = params?.runId;
|
|
||||||
return { runId: params?.runId ?? "run-1", status: "ok" };
|
|
||||||
}
|
|
||||||
if (request.method === "chat.history") {
|
|
||||||
const text =
|
|
||||||
(lastWaitedRunId && replyByRunId.get(lastWaitedRunId)) ?? "";
|
|
||||||
return {
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: "assistant",
|
|
||||||
content: [{ type: "text", text }],
|
|
||||||
timestamp: 20,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (request.method === "send") {
|
|
||||||
const params = request.params as
|
|
||||||
| { to?: string; provider?: string; message?: string }
|
|
||||||
| undefined;
|
|
||||||
sendParams = {
|
|
||||||
to: params?.to,
|
|
||||||
provider: params?.provider,
|
|
||||||
message: params?.message,
|
|
||||||
};
|
|
||||||
return { messageId: "m-announce" };
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
});
|
|
||||||
|
|
||||||
const tool = createClawdisTools({
|
|
||||||
agentSessionKey: requesterKey,
|
|
||||||
agentSurface: "discord",
|
|
||||||
}).find((candidate) => candidate.name === "sessions_send");
|
|
||||||
expect(tool).toBeDefined();
|
|
||||||
if (!tool) throw new Error("missing sessions_send tool");
|
|
||||||
|
|
||||||
const waited = await tool.execute("call7", {
|
|
||||||
sessionKey: targetKey,
|
|
||||||
message: "ping",
|
|
||||||
timeoutSeconds: 1,
|
|
||||||
});
|
|
||||||
expect(waited.details).toMatchObject({
|
|
||||||
status: "ok",
|
|
||||||
reply: "initial",
|
|
||||||
});
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
||||||
|
|
||||||
const replySteps = calls.filter(
|
|
||||||
(call) =>
|
|
||||||
call.method === "agent" &&
|
|
||||||
typeof (call.params as { extraSystemPrompt?: string })
|
|
||||||
?.extraSystemPrompt === "string" &&
|
|
||||||
(
|
|
||||||
call.params as { extraSystemPrompt?: string }
|
|
||||||
)?.extraSystemPrompt?.includes("Agent-to-agent reply step"),
|
|
||||||
);
|
|
||||||
expect(replySteps).toHaveLength(2);
|
|
||||||
expect(sendParams).toMatchObject({
|
|
||||||
to: "channel:target",
|
|
||||||
provider: "discord",
|
|
||||||
message: "announce now",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -946,15 +946,18 @@ export async function startGatewayServer(
|
|||||||
nodePresenceTimers.delete(nodeId);
|
nodePresenceTimers.delete(nodeId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const beaconNodePresence = (node: {
|
const beaconNodePresence = (
|
||||||
nodeId: string;
|
node: {
|
||||||
displayName?: string;
|
nodeId: string;
|
||||||
remoteIp?: string;
|
displayName?: string;
|
||||||
version?: string;
|
remoteIp?: string;
|
||||||
platform?: string;
|
version?: string;
|
||||||
deviceFamily?: string;
|
platform?: string;
|
||||||
modelIdentifier?: string;
|
deviceFamily?: string;
|
||||||
}, reason: string) => {
|
modelIdentifier?: string;
|
||||||
|
},
|
||||||
|
reason: string,
|
||||||
|
) => {
|
||||||
const host = node.displayName?.trim() || node.nodeId;
|
const host = node.displayName?.trim() || node.nodeId;
|
||||||
const rawIp = node.remoteIp?.trim();
|
const rawIp = node.remoteIp?.trim();
|
||||||
const ip = rawIp && !isLoopbackAddress(rawIp) ? rawIp : undefined;
|
const ip = rawIp && !isLoopbackAddress(rawIp) ? rawIp : undefined;
|
||||||
@@ -1826,6 +1829,10 @@ export async function startGatewayServer(
|
|||||||
await stopGmailWatcher();
|
await stopGmailWatcher();
|
||||||
cron.stop();
|
cron.stop();
|
||||||
heartbeatRunner.stop();
|
heartbeatRunner.stop();
|
||||||
|
for (const timer of nodePresenceTimers.values()) {
|
||||||
|
clearInterval(timer);
|
||||||
|
}
|
||||||
|
nodePresenceTimers.clear();
|
||||||
broadcast("shutdown", {
|
broadcast("shutdown", {
|
||||||
reason,
|
reason,
|
||||||
restartExpectedMs,
|
restartExpectedMs,
|
||||||
|
|||||||
Reference in New Issue
Block a user