Skip to content

Commit 480791d

Browse files
Lenouwclaude
andcommitted
fix(external-agent): refresh editor online lease in registry touch
registry.touch() validated the editor and renewed store ownership but never updated lastSeen, so an idle-but-polling editor dropped out of isConnected() 35s after registration. From there every MCP flow deadlocked: browser bindings were rejected as disconnected while the stale registration still blocked the offline fallback ("already has an active browser or offline editor"). The regression guard added in #70 (broker-poll-refresh.verify.ts) actually fails from its introducing commit for this reason; run-affected-verifies maps no suite to these files, which is likely how it slipped through. This one-line fix makes the existing guard pass and adds a direct-touch scenario that covers bridge-route touches outside the long-poll wait loop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 49245f0 commit 480791d

2 files changed

Lines changed: 31 additions & 0 deletions

File tree

‎server/external-agent/broker-poll-refresh.verify.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
nextEditorCall,
66
registerEditor,
77
resetExternalAgentBrokerForTest,
8+
touchEditor,
89
} from './broker.ts';
910

1011
const projectId = 'project-poll-refresh';
@@ -50,4 +51,29 @@ try {
5051
resetExternalAgentBrokerForTest();
5152
}
5253

54+
// Direct-touch scenario: bridge routes touch the editor outside the long-poll
55+
// wait loop (poll entry, tool settle). A validated touch must refresh the
56+
// online lease too, or an idle editor drops out of isConnected() after
57+
// ONLINE_MS while its registration still blocks the offline fallback.
58+
resetExternalAgentBrokerForTest();
59+
mock.timers.enable({ apis: ['Date', 'setTimeout'] });
60+
try {
61+
const registrationCapability = registerEditor(projectId, editorId, revision, tools, undefined, null);
62+
mock.timers.setTime(30_000);
63+
assert.equal(
64+
await touchEditor(projectId, editorId, revision, registrationCapability),
65+
true,
66+
'touch accepts the registered editor',
67+
);
68+
mock.timers.setTime(60_000);
69+
assert.equal(
70+
isProjectConnected(projectId),
71+
true,
72+
'a validated touch refreshes the editor online lease',
73+
);
74+
} finally {
75+
mock.timers.reset();
76+
resetExternalAgentBrokerForTest();
77+
}
78+
5379
console.log('broker-poll-refresh.verify: ok');

‎server/external-agent/broker-registry.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,11 @@ export class EditorConnectionRegistry {
165165
if (!editor || editor.editorInstanceId !== editorInstanceId) return false;
166166
if (registrationCapability !== undefined
167167
&& !capabilityMatches(editor.capability, registrationCapability)) return false;
168+
// A successful touch proves the editor transport is alive: refresh the
169+
// online lease so an idle-but-polling editor does not drop out of
170+
// isConnected() after ONLINE_MS, which would reject new browser bindings
171+
// while the registered editor still blocks the offline fallback.
172+
editor.lastSeen = Date.now();
168173
if (editor.ownership) {
169174
const renewed = await renewProjectEditOwnership(editor.ownership, baseRevision ?? editor.baseRevision);
170175
if (renewed.status !== 'renewed') {

0 commit comments

Comments
 (0)