thinkpool-pair 0.7.168 → 0.7.170

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
@@ -1510,7 +1510,16 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
1510
1510
  // spawnedBy: set when this lane was Dispatched (spawn_terminal). Restored from the
1511
1511
  // session store so the Ensemble flag survives a bridge restart (else a respin
1512
1512
  // stripped it and the lane reverted to a plain tab — the t6 "no chip" bug).
1513
- const entry = { cmd: 'claude', kind: 'structured', log: Array.isArray(log) ? log.slice(-STRUCTURED_LOG_MAX) : [], pending: new Map(), session: null, recovered: false, commands: Array.isArray(commands) ? commands : undefined, mode, model: model || null, provider: provider || null, spawnedBy: spawnedBy || undefined, flowSessionId: flowSessionId || null, flowTaskKey: flowTaskKey || null, cwd: cwd || null,
1513
+ const entry = { cmd: 'claude', kind: 'structured', log: Array.isArray(log) ? log.slice(-STRUCTURED_LOG_MAX) : [], pending: new Map(), session: null, recovered: false, commands: Array.isArray(commands) ? commands : undefined, mode,
1514
+ // model: truthful active-model label. When this lane runs on a custom (non-anthropic)
1515
+ // registered provider the SDK id is impersonated (see the onEvent guard below), so seed
1516
+ // from THAT provider's CONFIGURED model — glm-5.2, not a Claude label — before any turn.
1517
+ // No configured model → the provider's name, never a false Claude id. Built-in Anthropic
1518
+ // keeps the passed model. Keyed on the lane's own provider id, not the bridge default.
1519
+ model: (!provider || provider === 'anthropic')
1520
+ ? (model || null)
1521
+ : (resolveProviderEnv(provider)?.ANTHROPIC_MODEL || providerNameMap()[provider] || model || provider),
1522
+ provider: provider || null, spawnedBy: spawnedBy || undefined, flowSessionId: flowSessionId || null, flowTaskKey: flowTaskKey || null, cwd: cwd || null,
1514
1523
  // Stable creation order — persisted so a bridge restart restores tabs in the SAME
1515
1524
  // order (not readdir/filesystem order). Legacy recs (no openedAt) derive it from the
1516
1525
  // first transcript event ts, so even the first post-fix restart is ordered right.
@@ -2108,7 +2117,18 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
2108
2117
  // LIST (from the `models` event) so the announce carries them to EVERY device.
2109
2118
  // Re-announce on change so a switch / late device sees it without a turn (the
2110
2119
  // old localStorage cache was per-device; branch showed on every other device).
2111
- { const m = evt.model || evt.ctx?.model
2120
+ // When THIS lane runs on a NON-anthropic provider (a registered GLM/Z.ai,
2121
+ // OpenRouter, any Anthropic-compatible proxy — entry.provider is a custom
2122
+ // registry id) the SDK reads the endpoint's response metadata, which IMPERSONATES
2123
+ // a Claude model id (z.ai reports "claude-sonnet-…" while actually serving glm-5.2).
2124
+ // Trusting evt.model made the chip lie "Sonnet 5" on a GLM lane. The truthful label
2125
+ // is the configured provider model (seeded at openStructured) or an explicit /model
2126
+ // pick, both of which live on entry.model already — so DON'T let the impersonated
2127
+ // SDK id overwrite it. Only trust the SDK's model on the built-in Anthropic path.
2128
+ // Keyed on the LANE's provider (entry.provider), not the bridge default — lanes can
2129
+ // run on different registered providers. (bridge-truth slice.)
2130
+ { const onBuiltin = !entry.provider || entry.provider === 'anthropic'
2131
+ const m = onBuiltin ? (evt.model || evt.ctx?.model) : null
2112
2132
  if (m && m !== entry.model) { entry.model = m; announce() }
2113
2133
  if (evt.kind === 'models' && Array.isArray(evt.models) && evt.models.length) { roomModels = evt.models; announce() } }
2114
2134
  // FL-B2 — fold this flow lane's completed-turn output tokens into its budget so the
@@ -660,15 +660,11 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
660
660
  // documented override. opts.env REPLACES the child env, so spread the inherited one.
661
661
  opts.env = { ...(env || process.env), CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION: '1' }
662
662
 
663
- // Before the SDK replays this transcript, heal any cross-provider tool blocks
664
- // it left behind (e.g. a room that ran on Z.ai GLM, now back on anthropic).
665
- // Anthropic 400s on a `server_tool_use` block carrying a foreign id/name, which
666
- // wedges the room on every send. Reclassifying those to plain `tool_use` clears
667
- // it; idempotent + no-op on clean transcripts. See transcript-sanitize.mjs.
668
- if (resume) {
669
- const healed = sanitizeSession(cwd || process.cwd(), resume)
670
- if (healed.blocks) process.stderr.write(`\n ◆ healed ${healed.blocks} malformed tool block(s) in the transcript before resume.\n`)
671
- }
663
+ // Transcript healing lives at the TRUE resume point — see runQuery, right before
664
+ // query() is (re)built. The old factory-time heal was gated on the closure constant
665
+ // `resume`, so it ran only on the first open; a `/model` switch or a restart re-points
666
+ // the live `sessionId` and re-enters runQuery, resuming a transcript that was never
667
+ // re-healed (finding FBL-1). Healing inside runQuery covers EVERY resume path.
672
668
 
673
669
  // One-shot Haiku fallback suggestion. Runs on the SAME subscription auth as the room
674
670
  // (opts.env carries the OAuth — no API key needed), but as a BARE model call: no
@@ -719,6 +715,18 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
719
715
  const onParentAbort = () => { try { myAc.abort() } catch { /* noop */ } }
720
716
  ac.signal.addEventListener('abort', onParentAbort, { once: true })
721
717
  try {
718
+ // Heal the transcript we're about to REPLAY — at the true resume point, keyed on the
719
+ // LIVE `sessionId` (not the factory-time `resume` constant). `sessionId` mutates:
720
+ // recreateForSwitch re-points it to persistedSessionId and the restart / bad-resume
721
+ // forks re-point it too, each re-entering runQuery. A `/model` switch (or restart) that
722
+ // resumed a corrupted transcript — a dangling tool_use / tool_result-in-assistant left
723
+ // by an aborted mid-tool turn — used to hit an Anthropic 400 (H42 class) and wedge the
724
+ // lane on every send (finding FBL-1). sanitizeSession is idempotent + a no-op on clean
725
+ // transcripts, so healing before EVERY resume is free. See transcript-sanitize.mjs.
726
+ if (sessionId) {
727
+ const healed = sanitizeSession(cwd || process.cwd(), sessionId)
728
+ if (healed.blocks) process.stderr.write(`\n ◆ healed ${healed.blocks} malformed tool block(s) in the transcript before resume.\n`)
729
+ }
722
730
  q = query({ prompt: input.stream, options: { ...opts, abortController: myAc, ...(sessionId ? { resume: sessionId } : {}) } })
723
731
  for await (const m of q) {
724
732
  if (closed) break
@@ -756,7 +764,7 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
756
764
  // Cold-start timing — the init system message is when the terminal is
757
765
  // usable, so spawn→here IS the latency the user waits on. One line per
758
766
  // open. Compare a normal open vs one with TP_MCP_STRICT=1 to read off how
759
- // many ms the host MCP servers cost. (resume opens also pay sanitizeSession.)
767
+ // many ms the host MCP servers cost. (each resume also pays a sanitizeSession heal — see runQuery.)
760
768
  if (!readyLogged) {
761
769
  readyLogged = true
762
770
  process.stderr.write(`\n ◆ session ready in ${Date.now() - spawnT0}ms — MCP ${process.env.TP_MCP_STRICT === '1' ? 'OFF' : 'on'}${resume ? ', resume' : ', fresh'}.\n`)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.168",
4
- "description": "Share a local coding-agent CLI (Claude Code, Codex, Gemini, Aider, \u2026) into a ThinkPool Code room, live.",
3
+ "version": "0.7.170",
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": {
7
7
  "thinkpool-pair": "bridge.mjs"