thinkpool-pair 0.7.151 → 0.7.152

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bridge.mjs CHANGED
@@ -988,11 +988,25 @@ const mockupSeen = new Map() // `${term}/${slug}` → mtimeMs of the last mani
988
988
  // in the Map" guess meant closing the generating terminal moved the cards to
989
989
  // whatever terminal was now first — they leaked into Terminal 1.)
990
990
  const handleManifest = async (box, file, term) => {
991
- if (!file || !file.endsWith('.json')) return
991
+ // Ignore our own delivery receipts — writing <slug>.receipt.json into the
992
+ // watched outbox re-fires fs.watch; without this guard it would re-trigger
993
+ // handleManifest in a loop (it still ends in .json). Must come first.
994
+ if (!file || file.endsWith('.receipt.json') || !file.endsWith('.json')) return
992
995
  const full = path.join(box, file)
993
996
  let stat
994
997
  try { stat = fs.statSync(full) } catch { return } // tmp/removed mid-write
995
998
  const slug = file.replace(/\.json$/, '')
999
+ // Delivery receipt (atomic tmp+mv) so render.sh can confirm the card actually
1000
+ // reached the room — written on success AND failure. Best-effort: a receipt
1001
+ // that can't be written just leaves render.sh at "delivery unconfirmed".
1002
+ const writeReceipt = (ok, status) => {
1003
+ try {
1004
+ const rc = path.join(box, `${slug}.receipt.json`)
1005
+ const tmp = `${rc}.tmp`
1006
+ fs.writeFileSync(tmp, JSON.stringify({ ok, status, ts: Date.now() }))
1007
+ fs.renameSync(tmp, rc)
1008
+ } catch { /* receipts are best-effort */ }
1009
+ }
996
1010
  const seenKey = `${term}/${slug}`
997
1011
  if (mockupSeen.get(seenKey) === stat.mtimeMs) return // already handled
998
1012
  mockupSeen.set(seenKey, stat.mtimeMs)
@@ -1017,11 +1031,16 @@ const handleManifest = async (box, file, term) => {
1017
1031
  desktopPng: readB64(m.desktop), mobilePng: readB64(m.mobile), html: readTxt(m.html),
1018
1032
  }),
1019
1033
  })
1020
- if (!res.ok) { process.stderr.write(`\n ◇ mockup "${m.slug}" upload failed (${res.status}).\n`); return }
1034
+ if (!res.ok) {
1035
+ writeReceipt(false, res.status)
1036
+ process.stderr.write(`\n ◇ mockup "${m.slug}" upload failed (${res.status}).\n`); return
1037
+ }
1021
1038
  const { paths } = await res.json()
1022
1039
  bcast('code-event', { term, evt: { kind: 'mockup', __struct: true, cid, term, slug: m.slug, title: m.title || m.slug, ts: m.ts, paths } })
1040
+ writeReceipt(true, 200)
1023
1041
  process.stderr.write(`\n ◆ mockup "${m.title || m.slug}" pushed to the room.\n`)
1024
1042
  } catch (e) {
1043
+ writeReceipt(false, 'error')
1025
1044
  process.stderr.write(`\n ◇ mockup "${m.slug}" send failed: ${e?.message || e}\n`)
1026
1045
  }
1027
1046
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.151",
3
+ "version": "0.7.152",
4
4
  "description": "Share a local coding-agent CLI (Claude Code, Codex, Gemini, Aider, …) into a ThinkPool Code room, live.",
5
5
  "type": "module",
6
6
  "bin": {
package/sdk-smoke.mjs CHANGED
@@ -46,6 +46,7 @@ const version = ver()
46
46
  try {
47
47
  const sdk = await import('@anthropic-ai/claude-agent-sdk')
48
48
  if (typeof sdk.query !== 'function') { emit('FAIL', version, 'query is not a function'); clearTimeout(guard); process.exit(1) }
49
+ // eslint-disable-next-line require-yield -- deliberately empty: an async generator that ends immediately (the SDK's "no input" stream), never yields.
49
50
  async function* noInput() { return } // ends immediately — never blocks the construct
50
51
  const q = sdk.query({ prompt: noInput(), options: { permissionMode: 'default' } })
51
52
  const need = ['interrupt', 'setModel', 'setPermissionMode', 'supportedModels']