thinkpool-pair 0.7.277 → 0.7.278

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
@@ -3124,8 +3124,8 @@ function pendingResolution(pending, payload = {}) {
3124
3124
  const decision = payload.decision || 'deny'
3125
3125
  return pending?.payload?.answerFormat === 'dispatch'
3126
3126
  ? { decision, dispatchFingerprint: payload.dispatchFingerprint || null, controlItemId: payload.controlItemId || null }
3127
- : (pending?.payload?.answerFormat === 'codex' || pending?.payload?.answerFormat === 'hermes')
3128
- ? { decision, answers: payload.answers || {} }
3127
+ : (pending?.payload?.risk === 'ask' || pending?.payload?.answerFormat === 'codex' || pending?.payload?.answerFormat === 'hermes')
3128
+ ? { decision, answers: payload.answers || {}, responder: payload.responder || null }
3129
3129
  : decision
3130
3130
  }
3131
3131
 
@@ -3946,7 +3946,7 @@ channel
3946
3946
  if (p.timer) clearTimeout(p.timer)
3947
3947
  s.pending.delete(payload.id)
3948
3948
  s.permNotifier?.resolve(payload.id)
3949
- p.resolve(pendingResolution(p, { ...payload, decision: authority.decision, answers: authority.answers }))
3949
+ p.resolve(pendingResolution(p, { ...payload, decision: authority.decision, answers: authority.answers, responder: authority.responder }))
3950
3950
  announce()
3951
3951
  })
3952
3952
  })
@@ -99,18 +99,25 @@ export function autoAllow({ toolName, input, mode = 'default', alwaysAllow = new
99
99
  // denies (PreToolUse can't inject a tool_result) and puts the human's pick in
100
100
  // the deny reason, which IS what the model receives. Exported so the feedback
101
101
  // contract is locked in a unit test (mock requestPermission → this output).
102
- // `decision` is the requestPermission return: 'answer:<pick>' when the person
103
- // selected, anything else (including '' / dismissal / a broken path) is treated
104
- // as "no selection".
102
+ // `decision` is the requestPermission return: either the legacy 'answer:<pick>'
103
+ // string or { decision, responder } from the durable room control. Anything else
104
+ // (including '' / dismissal / a broken path) is treated as "no selection".
105
105
  export function askUserQuestionHookOutput(decision) {
106
- const ans = (typeof decision === 'string' && decision.startsWith('answer:')) ? decision.slice(7) : ''
106
+ const rawDecision = decision && typeof decision === 'object' ? decision.decision : decision
107
+ const ans = (typeof rawDecision === 'string' && rawDecision.startsWith('answer:')) ? rawDecision.slice(7) : ''
108
+ const responder = decision && typeof decision === 'object' ? decision.responder : null
109
+ const answeredBy = typeof responder?.name === 'string' && responder.name.trim()
110
+ ? responder.name.trim()
111
+ : typeof responder?.id === 'string' && responder.id
112
+ ? `room member ${responder.id}`
113
+ : 'The user'
107
114
  return {
108
115
  continue: true,
109
116
  hookSpecificOutput: {
110
117
  hookEventName: 'PreToolUse',
111
118
  permissionDecision: 'deny',
112
119
  permissionDecisionReason: ans
113
- ? `The user answered in the ThinkPool room — ${ans}. Treat this as their selection and continue; do not call AskUserQuestion again for the same question.`
120
+ ? `${answeredBy} answered in the ThinkPool room — ${ans}. Treat this as their selection and continue; do not call AskUserQuestion again for the same question.`
114
121
  : 'The user dismissed the question in the ThinkPool room without selecting. Ask in plain prose, or proceed with a sensible default.',
115
122
  },
116
123
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.277",
3
+ "version": "0.7.278",
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": {
@@ -34,6 +34,15 @@ export function verifyPairControlDeliveryAuthority ({ item, attempts, payload, r
34
34
  event?.actor_id === item.resolved_by && Number(event.item_version) === Number(item.version) &&
35
35
  event.correlation_key === payload.attemptKey && event.payload?.data?.transport === 'code-perm') : []
36
36
  if (matching.length !== 1) return denied('delivery_attempt_unverified')
37
+ const durableResponder = resolution.responder
38
+ const responder = durableResponder && durableResponder.id === item.resolved_by
39
+ ? {
40
+ id: item.resolved_by,
41
+ name: typeof durableResponder.name === 'string' && durableResponder.name.trim()
42
+ ? durableResponder.name.trim().slice(0, 120)
43
+ : null,
44
+ }
45
+ : { id: item.resolved_by, name: null }
37
46
  return Object.freeze({
38
47
  ok: true,
39
48
  code: 'durable_delivery_authorized',
@@ -41,6 +50,7 @@ export function verifyPairControlDeliveryAuthority ({ item, attempts, payload, r
41
50
  // App Server input is recovered from the durable resolution only. The public
42
51
  // broadcast intentionally remains a wake-up hint and cannot supply answers.
43
52
  answers: resolution.answers && typeof resolution.answers === 'object' ? resolution.answers : {},
53
+ responder: Object.freeze(responder),
44
54
  itemId: item.id,
45
55
  version: Number(item.version),
46
56
  attemptKey: payload.attemptKey,
@@ -38,9 +38,15 @@ export function questionAnswerResponse (result, questions = []) {
38
38
  if (values.length) answers[String(id)] = { answers: values }
39
39
  }
40
40
  }
41
- if (Object.keys(answers).length) return { answers }
41
+ const responder = result?.responder && typeof result.responder === 'object'
42
+ ? {
43
+ id: typeof result.responder.id === 'string' ? result.responder.id : null,
44
+ name: typeof result.responder.name === 'string' ? result.responder.name : null,
45
+ }
46
+ : null
47
+ if (Object.keys(answers).length) return { answers, ...(responder ? { responder } : {}) }
42
48
  const decision = result && typeof result === 'object' ? result.decision : result
43
- return { answers: legacyQuestionAnswers(decision, questions) }
49
+ return { answers: legacyQuestionAnswers(decision, questions), ...(responder ? { responder } : {}) }
44
50
  }
45
51
 
46
52
  export function hermesUserInputResponse (result, questions = []) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "bundleVersion": 5,
3
+ "bundleVersion": 6,
4
4
  "contracts": [
5
5
  {
6
6
  "id": "room-coordination",
@@ -65,7 +65,7 @@
65
65
  },
66
66
  {
67
67
  "id": "room-question",
68
- "version": 1,
68
+ "version": 2,
69
69
  "routes": [
70
70
  {
71
71
  "id": "room-question",
@@ -74,6 +74,7 @@
74
74
  "prompt": "When a missing choice genuinely blocks useful progress, use request_user_input so the question is answerable in the room; otherwise make a safe in-scope assumption and continue."
75
75
  }
76
76
  ],
77
+ "resultContract": "A resolved room question returns the selected answers plus the durable responder identity {id, name}; the id must match the pair-control row's resolved_by authority.",
77
78
  "impact": [
78
79
  {"path": "bridge/bridge.mjs", "diffPattern": "request_user_input"},
79
80
  {"path": "bridge/claude-session.mjs", "diffPattern": "AskUserQuestion|request_user_input"},