thinkpool-pair 0.7.263 → 0.7.265
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 +19 -24
- package/codex-session.mjs +2 -39
- package/dispatch-permission-cleanup.mjs +13 -7
- package/package.json +2 -1
- package/question-response.mjs +52 -0
package/bridge.mjs
CHANGED
|
@@ -60,6 +60,7 @@ import { canonicalRoomFilePath, waitForNativeImages } from './codex-images.mjs'
|
|
|
60
60
|
import { createManagedLaneWorktree, removeManagedLaneWorktree } from './lane-worktree.mjs'
|
|
61
61
|
import { commandOnPath } from './agent-detect.mjs'
|
|
62
62
|
import { requiresSdkAdmission, sdkSmokePassed } from './sdk-admission.mjs'
|
|
63
|
+
import { hermesUserInputResponse } from './question-response.mjs'
|
|
63
64
|
|
|
64
65
|
const STRUCTURED_MODES = new Set(['default', 'acceptEdits', 'plan', 'review', 'bypassPermissions'])
|
|
65
66
|
import { FLOW_CONDUCTOR_PROMPT, FLOW_LANE_PROMPT, FLOW_CODEX_CONDUCTOR_PROMPT, FLOW_CODEX_LANE_PROMPT, buildConductorEnv, assembleCrossWaveContext, buildLanePrompt } from './flow-conductor.mjs'
|
|
@@ -2189,10 +2190,7 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2189
2190
|
answerFormat: 'hermes',
|
|
2190
2191
|
autoResolutionMs: args.autoResolutionMs,
|
|
2191
2192
|
})
|
|
2192
|
-
|
|
2193
|
-
const answers = result && typeof result === 'object' && result.answers && typeof result.answers === 'object' ? result.answers : {}
|
|
2194
|
-
const status = String(decision || '').startsWith('answer:') ? 'answered' : decision === 'timeout' ? 'timed_out' : 'dismissed'
|
|
2195
|
-
return { content: [{ type: 'text', text: JSON.stringify({ status, answers }) }] }
|
|
2193
|
+
return { content: [{ type: 'text', text: JSON.stringify(hermesUserInputResponse(result, questions)) }] }
|
|
2196
2194
|
},
|
|
2197
2195
|
)] : []),
|
|
2198
2196
|
...createViewportTools({ tool, z, manager: entry.viewport }),
|
|
@@ -3104,23 +3102,20 @@ function cancelPendingDurablePermissions(s, permissionIds = null, reason = 'perm
|
|
|
3104
3102
|
})
|
|
3105
3103
|
}
|
|
3106
3104
|
|
|
3107
|
-
|
|
3105
|
+
let restoredPermissionCleanupPending = false
|
|
3108
3106
|
|
|
3109
|
-
async function cancelRestoredDurablePermissions(
|
|
3110
|
-
|
|
3111
|
-
const
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
}))
|
|
3122
|
-
for (const outcome of outcomes) if (outcome.ok) restoredPermissionCleanupPending.delete(outcome.terminalId)
|
|
3123
|
-
return { ok: restoredPermissionCleanupPending.size === 0, pending: [...restoredPermissionCleanupPending] }
|
|
3107
|
+
async function cancelRestoredDurablePermissions() {
|
|
3108
|
+
restoredPermissionCleanupPending = true
|
|
3109
|
+
const result = await cancelDurablePermissions({
|
|
3110
|
+
supabaseUrl: SUPABASE_URL, anonKey: SUPABASE_ANON, token: codeAuthToken, roomCode: room,
|
|
3111
|
+
// A new process gets a fresh bridge id. Every approval bound to any older
|
|
3112
|
+
// id has lost its process-local resolver, including archived terminals that
|
|
3113
|
+
// no longer have a session file and therefore never enter loadAll().
|
|
3114
|
+
currentBridgeId: BRIDGE_ID, reason: 'bridge_restarted',
|
|
3115
|
+
})
|
|
3116
|
+
if (result.ok) restoredPermissionCleanupPending = false
|
|
3117
|
+
else if (process.env.TP_DEBUG) process.stderr.write(`\n ◇ restored permission cleanup incomplete (${result.code})\n`)
|
|
3118
|
+
return result
|
|
3124
3119
|
}
|
|
3125
3120
|
|
|
3126
3121
|
function drainPending(s) {
|
|
@@ -4065,7 +4060,7 @@ channel
|
|
|
4065
4060
|
// Cancel their durable dispatch rows BEFORE opening a restored SDK
|
|
4066
4061
|
// session, so a new permission raised during resume can never be
|
|
4067
4062
|
// mistaken for a pre-restart card from the same terminal.
|
|
4068
|
-
await cancelRestoredDurablePermissions(
|
|
4063
|
+
await cancelRestoredDurablePermissions()
|
|
4069
4064
|
if (all.length) for (const rec of all) {
|
|
4070
4065
|
const wasInterrupted = restoredTurnOpen(rec.log || [])
|
|
4071
4066
|
// Codex can persist a thread id before its rollout receives the
|
|
@@ -4608,9 +4603,9 @@ if (process.env.THINKPOOL_PAIR_AUTOUPDATE === '1' && VERSION) {
|
|
|
4608
4603
|
})
|
|
4609
4604
|
refreshOwnerPlan()
|
|
4610
4605
|
// A restart sweep may have raced an expiring owner JWT. Keep failed
|
|
4611
|
-
//
|
|
4612
|
-
// retry
|
|
4613
|
-
if (restoredPermissionCleanupPending
|
|
4606
|
+
// room sweep queued until the supervisor hands us its next fresh token,
|
|
4607
|
+
// then retry without leaving archived-terminal questions behind.
|
|
4608
|
+
if (restoredPermissionCleanupPending) void cancelRestoredDurablePermissions()
|
|
4614
4609
|
}
|
|
4615
4610
|
})
|
|
4616
4611
|
setInterval(() => { try { process.send({ t: 'idle', idle: idleNow(), between: betweenTurns(), webPeer: webPeerPresent() }) } catch { /* parent gone */ } }, 5000).unref()
|
package/codex-session.mjs
CHANGED
|
@@ -31,6 +31,7 @@ import { buildCodexAppServerArgs, codexAppServerGate, createCodexAppServer } fro
|
|
|
31
31
|
import { CodexEventMapper } from './codex-event-mapper.mjs'
|
|
32
32
|
import { startCodexMcpHttp } from './codex-mcp-http.mjs'
|
|
33
33
|
import { CODEX_THINKPOOL_FIRST_TURN_PREAMBLE, buildThinkPoolTurnGuidance, createRoomContextSelector, usesFullThinkPoolReminder } from './thinkpool-room-prompt.mjs'
|
|
34
|
+
import { questionAnswerResponse } from './question-response.mjs'
|
|
34
35
|
|
|
35
36
|
const DEFAULT_SANDBOX = 'workspace-write'
|
|
36
37
|
const SAFE_SANDBOXES = new Set(['read-only', 'workspace-write', 'danger-full-access'])
|
|
@@ -316,46 +317,8 @@ export function codexApprovalResponse(decision) {
|
|
|
316
317
|
return { decision: decision === 'always' ? 'acceptForSession' : decision === 'allow' ? 'accept' : 'decline' }
|
|
317
318
|
}
|
|
318
319
|
|
|
319
|
-
function legacyQuestionAnswers(decision, questions) {
|
|
320
|
-
if (typeof decision !== 'string' || !decision.startsWith('answer:')) return {}
|
|
321
|
-
const text = decision.slice(7).trim()
|
|
322
|
-
if (!text || !questions.length) return {}
|
|
323
|
-
if (questions.length === 1) {
|
|
324
|
-
const question = questions[0]
|
|
325
|
-
const prefix = `${question.header || 'Q1'}: `
|
|
326
|
-
const value = text.startsWith(prefix) ? text.slice(prefix.length) : text
|
|
327
|
-
return { [String(question.id || 0)]: { answers: [value] } }
|
|
328
|
-
}
|
|
329
|
-
const answers = {}
|
|
330
|
-
questions.forEach((question, index) => {
|
|
331
|
-
const id = String(question.id || index)
|
|
332
|
-
const marker = `${index ? '; ' : ''}${question.header || `Q${index + 1}`}: `
|
|
333
|
-
const start = text.indexOf(marker)
|
|
334
|
-
if (start < 0) return
|
|
335
|
-
const valueStart = start + marker.length
|
|
336
|
-
const nextQuestion = questions[index + 1]
|
|
337
|
-
const nextMarker = nextQuestion ? `; ${nextQuestion.header || `Q${index + 2}`}: ` : null
|
|
338
|
-
const end = nextMarker ? text.indexOf(nextMarker, valueStart) : -1
|
|
339
|
-
const value = text.slice(valueStart, end < 0 ? undefined : end).trim()
|
|
340
|
-
if (value) answers[id] = { answers: [value] }
|
|
341
|
-
})
|
|
342
|
-
return answers
|
|
343
|
-
}
|
|
344
|
-
|
|
345
320
|
export function codexUserInputResponse(result, questions = []) {
|
|
346
|
-
|
|
347
|
-
const answers = {}
|
|
348
|
-
if (supplied && typeof supplied === 'object') {
|
|
349
|
-
const allowed = new Set(questions.map((question, index) => String(question?.id || index)))
|
|
350
|
-
for (const [id, value] of Object.entries(supplied)) {
|
|
351
|
-
if (!allowed.has(String(id)) || !Array.isArray(value?.answers)) continue
|
|
352
|
-
const values = value.answers.map((answer) => String(answer)).filter(Boolean)
|
|
353
|
-
if (values.length) answers[String(id)] = { answers: values }
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
if (Object.keys(answers).length) return { answers }
|
|
357
|
-
const decision = result && typeof result === 'object' ? result.decision : result
|
|
358
|
-
return { answers: legacyQuestionAnswers(decision, questions) }
|
|
321
|
+
return questionAnswerResponse(result, questions)
|
|
359
322
|
}
|
|
360
323
|
|
|
361
324
|
function appServerItemForMapper(item) {
|
|
@@ -9,14 +9,17 @@ const SETTLED = new Set(['denied', 'idempotent_replay', 'already_resolved', 'sta
|
|
|
9
9
|
|
|
10
10
|
const safeString = (value) => typeof value === 'string' && value.length > 0
|
|
11
11
|
|
|
12
|
-
export function pendingDurablePermissionItems (items, { bridgeId = null, terminalId, permissionIds = null, actionKinds = ['approval', 'dispatch'] } = {}) {
|
|
13
|
-
if ((bridgeId != null && !safeString(bridgeId)) || !safeString(
|
|
12
|
+
export function pendingDurablePermissionItems (items, { bridgeId = null, currentBridgeId = null, terminalId = null, permissionIds = null, actionKinds = ['approval', 'dispatch'] } = {}) {
|
|
13
|
+
if ((bridgeId != null && !safeString(bridgeId)) || (currentBridgeId != null && !safeString(currentBridgeId)) ||
|
|
14
|
+
(terminalId != null && !safeString(terminalId)) || (terminalId == null && currentBridgeId == null)) return []
|
|
14
15
|
const requested = permissionIds == null ? null : new Set(permissionIds.filter(safeString))
|
|
15
16
|
const actions = new Set(actionKinds)
|
|
16
17
|
return (Array.isArray(items) ? items : []).filter((item) => {
|
|
17
18
|
const permissionId = item?.request_context?.permission_id
|
|
18
19
|
return PENDING.has(item?.status) && item?.item_kind === 'approval' && actions.has(item?.action_kind) &&
|
|
19
|
-
(!bridgeId || item?.bridge_authority_id === bridgeId) &&
|
|
20
|
+
(!bridgeId || item?.bridge_authority_id === bridgeId) &&
|
|
21
|
+
(!currentBridgeId || item?.bridge_authority_id !== currentBridgeId) &&
|
|
22
|
+
(!terminalId || item?.local_authority_id === terminalId) &&
|
|
20
23
|
safeString(permissionId) && (!requested || requested.has(permissionId))
|
|
21
24
|
})
|
|
22
25
|
}
|
|
@@ -37,10 +40,13 @@ const rpc = async ({ fetchImpl, supabaseUrl, headers, name, body }) => {
|
|
|
37
40
|
// room-member identity. Never accept an id supplied by a public broadcast.
|
|
38
41
|
export async function cancelDurablePermissions ({
|
|
39
42
|
fetchImpl = globalThis.fetch, supabaseUrl, anonKey, token, roomCode, bridgeId,
|
|
40
|
-
|
|
43
|
+
currentBridgeId = null, terminalId = null, permissionIds = null,
|
|
44
|
+
reason = 'permission_aborted', actionKinds = ['approval', 'dispatch'],
|
|
41
45
|
} = {}) {
|
|
42
46
|
if (typeof fetchImpl !== 'function' || !safeString(supabaseUrl) || !safeString(anonKey) ||
|
|
43
|
-
!safeString(token) || !safeString(roomCode) || (bridgeId != null && !safeString(bridgeId)) ||
|
|
47
|
+
!safeString(token) || !safeString(roomCode) || (bridgeId != null && !safeString(bridgeId)) ||
|
|
48
|
+
(currentBridgeId != null && !safeString(currentBridgeId)) ||
|
|
49
|
+
(terminalId != null && !safeString(terminalId)) || (terminalId == null && currentBridgeId == null)) {
|
|
44
50
|
return Object.freeze({ ok: false, code: 'cleanup_unavailable', canceled: 0 })
|
|
45
51
|
}
|
|
46
52
|
if (permissionIds != null && (!Array.isArray(permissionIds) || !permissionIds.length)) {
|
|
@@ -51,7 +57,7 @@ export async function cancelDurablePermissions ({
|
|
|
51
57
|
const listed = await rpc({ fetchImpl, supabaseUrl, headers, name: 'list_code_pair_controls', body: {
|
|
52
58
|
p_session_code: String(roomCode).toUpperCase(), p_status: null, p_limit: 100,
|
|
53
59
|
} })
|
|
54
|
-
const candidates = pendingDurablePermissionItems(listed, { bridgeId, terminalId, permissionIds, actionKinds })
|
|
60
|
+
const candidates = pendingDurablePermissionItems(listed, { bridgeId, currentBridgeId, terminalId, permissionIds, actionKinds })
|
|
55
61
|
const outcomes = await Promise.all(candidates.map(async (item) => {
|
|
56
62
|
const result = await rpc({ fetchImpl, supabaseUrl, headers, name: 'resolve_code_pair_control', body: {
|
|
57
63
|
p_item_id: item.id,
|
|
@@ -63,7 +69,7 @@ export async function cancelDurablePermissions ({
|
|
|
63
69
|
// by its immutable authority binding, so resolve it with that exact
|
|
64
70
|
// value after the owner-authenticated RLS list—not a guessed current ID.
|
|
65
71
|
p_bridge_authority_id: item.bridge_authority_id,
|
|
66
|
-
p_local_authority_id:
|
|
72
|
+
p_local_authority_id: item.local_authority_id,
|
|
67
73
|
p_idempotency_key: `cancel-permission:${item.id}:${reason}`.slice(0, 160),
|
|
68
74
|
} })
|
|
69
75
|
return { id: item.id, code: result?.code || 'invalid_response', ok: Boolean(result?.ok) && SETTLED.has(result.code) }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thinkpool-pair",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.265",
|
|
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": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"context-windows.mjs",
|
|
17
17
|
"claude-session.mjs",
|
|
18
18
|
"codex-session.mjs",
|
|
19
|
+
"question-response.mjs",
|
|
19
20
|
"thinkpool-room-prompt.mjs",
|
|
20
21
|
"thinkpool-prompt-contracts.mjs",
|
|
21
22
|
"thinkpool-capabilities.json",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function legacyQuestionAnswers (decision, questions) {
|
|
2
|
+
if (typeof decision !== 'string' || !decision.startsWith('answer:')) return {}
|
|
3
|
+
const text = decision.slice(7).trim()
|
|
4
|
+
if (!text || !questions.length) return {}
|
|
5
|
+
if (questions.length === 1) {
|
|
6
|
+
const question = questions[0]
|
|
7
|
+
const prefix = `${question.header || 'Q1'}: `
|
|
8
|
+
const value = text.startsWith(prefix) ? text.slice(prefix.length) : text
|
|
9
|
+
return { [String(question.id || 0)]: { answers: [value] } }
|
|
10
|
+
}
|
|
11
|
+
const answers = {}
|
|
12
|
+
questions.forEach((question, index) => {
|
|
13
|
+
const id = String(question.id || index)
|
|
14
|
+
const marker = `${index ? '; ' : ''}${question.header || `Q${index + 1}`}: `
|
|
15
|
+
const start = text.indexOf(marker)
|
|
16
|
+
if (start < 0) return
|
|
17
|
+
const valueStart = start + marker.length
|
|
18
|
+
const nextQuestion = questions[index + 1]
|
|
19
|
+
const nextMarker = nextQuestion ? `; ${nextQuestion.header || `Q${index + 2}`}: ` : null
|
|
20
|
+
const end = nextMarker ? text.indexOf(nextMarker, valueStart) : -1
|
|
21
|
+
const value = text.slice(valueStart, end < 0 ? undefined : end).trim()
|
|
22
|
+
if (value) answers[id] = { answers: [value] }
|
|
23
|
+
})
|
|
24
|
+
return answers
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Codex and Hermes use different native tool protocols, but the room answer is
|
|
28
|
+
// one contract. Keep filtering, multi-select preservation, and legacy fallback
|
|
29
|
+
// in one pure adapter so the two runtimes cannot drift independently.
|
|
30
|
+
export function questionAnswerResponse (result, questions = []) {
|
|
31
|
+
const supplied = result && typeof result === 'object' ? result.answers : null
|
|
32
|
+
const answers = {}
|
|
33
|
+
if (supplied && typeof supplied === 'object') {
|
|
34
|
+
const allowed = new Set(questions.map((question, index) => String(question?.id || index)))
|
|
35
|
+
for (const [id, value] of Object.entries(supplied)) {
|
|
36
|
+
if (!allowed.has(String(id)) || !Array.isArray(value?.answers)) continue
|
|
37
|
+
const values = value.answers.map((answer) => String(answer)).filter(Boolean)
|
|
38
|
+
if (values.length) answers[String(id)] = { answers: values }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (Object.keys(answers).length) return { answers }
|
|
42
|
+
const decision = result && typeof result === 'object' ? result.decision : result
|
|
43
|
+
return { answers: legacyQuestionAnswers(decision, questions) }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function hermesUserInputResponse (result, questions = []) {
|
|
47
|
+
const decision = result && typeof result === 'object' ? result.decision : result
|
|
48
|
+
return {
|
|
49
|
+
status: String(decision || '').startsWith('answer:') ? 'answered' : decision === 'timeout' ? 'timed_out' : 'dismissed',
|
|
50
|
+
...questionAnswerResponse(result, questions),
|
|
51
|
+
}
|
|
52
|
+
}
|