diff --git a/android/app/src/main/java/com/hyzq/boss/MainActivity.java b/android/app/src/main/java/com/hyzq/boss/MainActivity.java index 7d0be6b..e940947 100644 --- a/android/app/src/main/java/com/hyzq/boss/MainActivity.java +++ b/android/app/src/main/java/com/hyzq/boss/MainActivity.java @@ -591,13 +591,17 @@ public class MainActivity extends AppCompatActivity { } private boolean shouldRefreshConversationsTab(BossRealtimeEvent event) { - if (!hasProjectId(event)) { - return false; + if ("conversation.context_indicator.updated".equals(event.eventName)) { + return true; } - return "conversation.updated".equals(event.eventName) - || "project.messages.updated".equals(event.eventName) - || "master_agent.task.updated".equals(event.eventName) - || "conversation.context_indicator.updated".equals(event.eventName); + if ("conversation.updated".equals(event.eventName)) { + return hasProjectId(event) || hasDeviceId(event); + } + if ("project.messages.updated".equals(event.eventName) + || "master_agent.task.updated".equals(event.eventName)) { + return hasProjectId(event); + } + return false; } private boolean shouldRefreshDevicesTab(BossRealtimeEvent event) { diff --git a/android/app/src/test/java/com/hyzq/boss/MainActivityRealtimeTest.java b/android/app/src/test/java/com/hyzq/boss/MainActivityRealtimeTest.java index 4a576c7..d613bbe 100644 --- a/android/app/src/test/java/com/hyzq/boss/MainActivityRealtimeTest.java +++ b/android/app/src/test/java/com/hyzq/boss/MainActivityRealtimeTest.java @@ -74,6 +74,24 @@ public class MainActivityRealtimeTest { assertEquals(0, activity.conversationRefreshCount); } + @Test + public void deviceScopedConversationEventRefreshesVisibleConversationTab() throws Exception { + TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get(); + ReflectionHelpers.callInstanceMethod(activity, "showContent"); + ReflectionHelpers.callInstanceMethod( + activity, + "handleRealtimeEvent", + ReflectionHelpers.ClassParameter.from( + BossRealtimeEvent.class, + new BossRealtimeEvent("conversation.updated", new JSONObject().put("deviceId", "mac-studio")) + ) + ); + Shadows.shadowOf(activity.getMainLooper()).idle(); + + assertEquals(1, activity.conversationRefreshCount); + assertEquals(0, activity.deviceRefreshCount); + } + @Test public void contextIndicatorEventRefreshesVisibleConversationTab() throws Exception { TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get(); @@ -95,6 +113,27 @@ public class MainActivityRealtimeTest { assertEquals(0, activity.deviceRefreshCount); } + @Test + public void contextIndicatorSnapshotWithoutProjectIdRefreshesVisibleConversationTab() throws Exception { + TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get(); + ReflectionHelpers.callInstanceMethod(activity, "showContent"); + ReflectionHelpers.callInstanceMethod( + activity, + "handleRealtimeEvent", + ReflectionHelpers.ClassParameter.from( + BossRealtimeEvent.class, + new BossRealtimeEvent( + "conversation.context_indicator.updated", + new JSONObject().put("conversations", new JSONArray()) + ) + ) + ); + Shadows.shadowOf(activity.getMainLooper()).idle(); + + assertEquals(1, activity.conversationRefreshCount); + assertEquals(0, activity.deviceRefreshCount); + } + @Test public void distinctConversationEventsBackToBackBothRefreshVisibleConversationTab() throws Exception { TestMainActivity activity = Robolectric.buildActivity(TestMainActivity.class).setup().resume().get(); diff --git a/local-agent/config.cloud.json b/local-agent/config.cloud.json index e1499d5..7bc2854 100644 --- a/local-agent/config.cloud.json +++ b/local-agent/config.cloud.json @@ -1,7 +1,7 @@ { "bindHost": "127.0.0.1", "port": 4317, - "heartbeatIntervalMs": 60000, + "heartbeatIntervalMs": 15000, "masterAgentPollIntervalMs": 3000, "controlPlaneUrl": "https://boss.hyzq.net", "skillsDir": "/Users/kris/.codex/skills", diff --git a/local-agent/config.example.json b/local-agent/config.example.json index afab96a..b707edb 100644 --- a/local-agent/config.example.json +++ b/local-agent/config.example.json @@ -1,7 +1,7 @@ { "bindHost": "127.0.0.1", "port": 4317, - "heartbeatIntervalMs": 60000, + "heartbeatIntervalMs": 15000, "masterAgentPollIntervalMs": 3000, "controlPlaneUrl": "http://127.0.0.1:3000", "skillsDir": "/Users/kris/.codex/skills", diff --git a/local-agent/server.mjs b/local-agent/server.mjs index 362379c..33e1dba 100755 --- a/local-agent/server.mjs +++ b/local-agent/server.mjs @@ -704,7 +704,7 @@ void (async () => { setInterval(() => { void heartbeat(); -}, config.heartbeatIntervalMs ?? 60000); +}, config.heartbeatIntervalMs ?? 15000); setInterval(() => { void pollMasterAgentTasks(config, runtime); diff --git a/src/components/app-ui.tsx b/src/components/app-ui.tsx index c4c2944..16bafc9 100644 --- a/src/components/app-ui.tsx +++ b/src/components/app-ui.tsx @@ -2181,7 +2181,7 @@ export function DeviceEnrollmentBuilder() { { bindHost: "127.0.0.1", port: 4317, - heartbeatIntervalMs: 60000, + heartbeatIntervalMs: 15000, controlPlaneUrl: "http://127.0.0.1:3000", deviceId: result.device?.id, pairingCode: result.enrollment.pairingCode, diff --git a/tests/local-agent-heartbeat-defaults.test.mjs b/tests/local-agent-heartbeat-defaults.test.mjs new file mode 100644 index 0000000..00d115a --- /dev/null +++ b/tests/local-agent-heartbeat-defaults.test.mjs @@ -0,0 +1,29 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); + +test("shipped local-agent configs use the faster heartbeat default", async () => { + const exampleConfig = JSON.parse( + await readFile(path.join(repoRoot, "local-agent", "config.example.json"), "utf8"), + ); + const cloudConfig = JSON.parse( + await readFile(path.join(repoRoot, "local-agent", "config.cloud.json"), "utf8"), + ); + + assert.equal(exampleConfig.heartbeatIntervalMs, 15_000); + assert.equal(cloudConfig.heartbeatIntervalMs, 15_000); +}); + +test("device enrollment snippet advertises the faster heartbeat default", async () => { + const source = await readFile(path.join(repoRoot, "src", "components", "app-ui.tsx"), "utf8"); + assert.match(source, /heartbeatIntervalMs:\s*15000/); +}); + +test("local-agent runtime falls back to the faster heartbeat default", async () => { + const source = await readFile(path.join(repoRoot, "local-agent", "server.mjs"), "utf8"); + assert.match(source, /heartbeatIntervalMs\s*\?\?\s*15000/); +});