lvwerra HF Staff commited on
Commit
b1c90fd
·
verified ·
1 Parent(s): 5a65b82

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. server/api.js +5 -4
  2. server/collab.js +20 -2
  3. test/prod.js +6 -1
  4. test/smoke.js +15 -3
server/api.js CHANGED
@@ -191,9 +191,10 @@ export function apiRouter() {
191
 
192
  // --- mention long-poll ---
193
 
194
- router.get('/mentions', requireUser, async (req, res) => {
195
- const wait = Math.max(0, Math.min(Number(req.query.wait) || 0, 55))
196
- const result = await collab.pollMentions(req.user.username, wait, req.agent?.handle)
 
197
  if (result.error) return res.status(400).json(result)
198
  res.json(result)
199
  })
@@ -288,7 +289,7 @@ Work loop — repeat until the user tells you to stop:
288
  The stream sends ":hb" heartbeat lines while idle; the final line is JSON:
289
  {"mentions": [...]}. Empty output or an empty list just means the wait expired —
290
  immediately make the same call again. This is the normal idle state; one call per
291
- ~50 minutes costs almost nothing. (A short poll also exists: GET /api/mentions?wait=50)
292
  Each mention has: mention_id, doc_id, thread_id, instruction, anchored_text (the text the
293
  comment is attached to), thread_messages (conversation so far), requested_by.
294
  If "about_suggestion" is set, the comment discusses one of your earlier suggestions —
 
191
 
192
  // --- mention long-poll ---
193
 
194
+ // Instant snapshot of outstanding work (nothing is claimed) — for debugging.
195
+ // The `wait` param is gone: waiting happens on /mentions/stream.
196
+ router.get('/mentions', requireUser, (req, res) => {
197
+ const result = collab.snapshotMentions(req.user.username, req.agent?.handle)
198
  if (result.error) return res.status(400).json(result)
199
  res.json(result)
200
  })
 
289
  The stream sends ":hb" heartbeat lines while idle; the final line is JSON:
290
  {"mentions": [...]}. Empty output or an empty list just means the wait expired —
291
  immediately make the same call again. This is the normal idle state; one call per
292
+ ~50 minutes costs almost nothing.
293
  Each mention has: mention_id, doc_id, thread_id, instruction, anchored_text (the text the
294
  comment is attached to), thread_messages (conversation so far), requested_by.
295
  If "about_suggestion" is set, the comment discusses one of your earlier suggestions —
server/collab.js CHANGED
@@ -302,7 +302,7 @@ async function claimAndEnrich(tasks) {
302
  return out
303
  }
304
 
305
- export function pollMentions(username, waitSeconds, onlyHandle = null, opts = {}) {
306
  let handles = handlesOwnedBy(username)
307
  if (onlyHandle) {
308
  // agent-key poll: exactly this handle
@@ -314,7 +314,25 @@ export function pollMentions(username, waitSeconds, onlyHandle = null, opts = {}
314
  const agents = store.getAgents()
315
  handles = handles.filter(h => !agents[h]?.keyHash)
316
  }
317
- if (!handles.length) return Promise.resolve({ error: 'no agent handles registered for your account (handles with a key must poll with that key) — POST /api/agents first' })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  for (const h of handles) agentLastPoll.set(h, Date.now())
319
 
320
  const now = claimable(handles)
 
302
  return out
303
  }
304
 
305
+ function pollableHandles(username, onlyHandle) {
306
  let handles = handlesOwnedBy(username)
307
  if (onlyHandle) {
308
  // agent-key poll: exactly this handle
 
314
  const agents = store.getAgents()
315
  handles = handles.filter(h => !agents[h]?.keyHash)
316
  }
317
+ return handles
318
+ }
319
+
320
+ const NO_HANDLES_ERROR = 'no agent handles registered for your account (handles with a key must poll with that key) — POST /api/agents first'
321
+
322
+ // Instant, non-claiming view of outstanding work — for debugging. Receiving
323
+ // (and claiming) work happens through the streaming poll.
324
+ export function snapshotMentions(username, onlyHandle = null) {
325
+ const handles = pollableHandles(username, onlyHandle)
326
+ if (!handles.length) return { error: NO_HANDLES_ERROR }
327
+ const mentions = Object.values(mentionState.tasks)
328
+ .filter(t => handles.includes(t.handle) && (t.status === 'pending' || t.status === 'claimed'))
329
+ .map(t => ({ mention_id: t.id, doc_id: t.docId, thread_id: t.threadId, handle: t.handle, status: t.status, instruction: t.text, created_at: t.ts }))
330
+ return { snapshot: true, mentions }
331
+ }
332
+
333
+ export function pollMentions(username, waitSeconds, onlyHandle = null, opts = {}) {
334
+ const handles = pollableHandles(username, onlyHandle)
335
+ if (!handles.length) return Promise.resolve({ error: NO_HANDLES_ERROR })
336
  for (const h of handles) agentLastPoll.set(h, Date.now())
337
 
338
  const now = claimable(handles)
test/prod.js CHANGED
@@ -82,7 +82,12 @@ async function main() {
82
  doc.getMap('threads').set(threadId, t)
83
  })
84
 
85
- const poll = await agentApi('/api/mentions?wait=15')
 
 
 
 
 
86
  assert.equal(poll.mentions?.length, 1, JSON.stringify(poll))
87
  console.log('✓ mention delivered via long-poll')
88
 
 
82
  doc.getMap('threads').set(threadId, t)
83
  })
84
 
85
+ const poll = await (async () => {
86
+ const resp = await fetch(`${BASE}/api/mentions/stream?wait=15`, { headers: { authorization: `Bearer ${AGENT}` } })
87
+ let text = ''
88
+ for await (const chunk of resp.body) text += Buffer.from(chunk).toString()
89
+ return JSON.parse(text.trim().split('\n').filter(l => !l.startsWith(':')).pop())
90
+ })()
91
  assert.equal(poll.mentions?.length, 1, JSON.stringify(poll))
92
  console.log('✓ mention delivered via long-poll')
93
 
test/smoke.js CHANGED
@@ -54,6 +54,14 @@ function api(token, path_, { method = 'GET', body } = {}) {
54
  }).then(r => r.json())
55
  }
56
 
 
 
 
 
 
 
 
 
57
  function connect(docId, token) {
58
  const doc = new Y.Doc()
59
  const provider = new HocuspocusProvider({
@@ -153,8 +161,12 @@ async function main() {
153
  }, 'mention chip')
154
  console.log('✓ mention detected, chip set')
155
 
156
- // 6. the agent long-polls with its key
157
- const poll = await api(AGENT, '/api/mentions?wait=10')
 
 
 
 
158
  assert.equal(poll.mentions?.length, 1, 'mention delivered: ' + JSON.stringify(poll))
159
  const mention = poll.mentions[0]
160
  assert.equal(mention.handle, 'test-agent')
@@ -205,7 +217,7 @@ async function main() {
205
  { id: 'msg-followup', author: 'alice', authorType: 'user', text: 'hmm, can you double check the link?', ts: Date.now() },
206
  ])
207
  })
208
- const followPoll = await api(AGENT, '/api/mentions?wait=10')
209
  assert.equal(followPoll.mentions?.length, 1, 'follow-up delivered')
210
  assert.equal(followPoll.mentions[0].implicit_follow_up, true, 'marked implicit')
211
  const dismissed = await api(AGENT, `/api/mentions/${followPoll.mentions[0].mention_id}/dismiss`, { method: 'POST' })
 
54
  }).then(r => r.json())
55
  }
56
 
57
+ async function streamPoll(key, wait = 10) {
58
+ const resp = await fetch(`${BASE}/api/mentions/stream?wait=${wait}`, { headers: { authorization: `Bearer ${key}` } })
59
+ let text = ''
60
+ for await (const chunk of resp.body) text += Buffer.from(chunk).toString()
61
+ const last = text.trim().split('\n').filter(l => !l.startsWith(':')).pop()
62
+ return JSON.parse(last)
63
+ }
64
+
65
  function connect(docId, token) {
66
  const doc = new Y.Doc()
67
  const provider = new HocuspocusProvider({
 
161
  }, 'mention chip')
162
  console.log('✓ mention detected, chip set')
163
 
164
+ // 6. snapshot shows the mention without claiming; the stream claims it
165
+ const snap0 = await api(AGENT, '/api/mentions')
166
+ assert.equal(snap0.snapshot, true, 'snapshot flagged')
167
+ assert.equal(snap0.mentions?.length, 1, 'snapshot shows pending: ' + JSON.stringify(snap0))
168
+ assert.equal(snap0.mentions[0].status, 'pending', 'snapshot does not claim')
169
+ const poll = await streamPoll(AGENT, 10)
170
  assert.equal(poll.mentions?.length, 1, 'mention delivered: ' + JSON.stringify(poll))
171
  const mention = poll.mentions[0]
172
  assert.equal(mention.handle, 'test-agent')
 
217
  { id: 'msg-followup', author: 'alice', authorType: 'user', text: 'hmm, can you double check the link?', ts: Date.now() },
218
  ])
219
  })
220
+ const followPoll = await streamPoll(AGENT, 10)
221
  assert.equal(followPoll.mentions?.length, 1, 'follow-up delivered')
222
  assert.equal(followPoll.mentions[0].implicit_follow_up, true, 'marked implicit')
223
  const dismissed = await api(AGENT, `/api/mentions/${followPoll.mentions[0].mention_id}/dismiss`, { method: 'POST' })