thinkpool-pair 0.7.278 → 0.7.279
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/codex-session.mjs +40 -1
- package/package.json +1 -1
package/codex-session.mjs
CHANGED
|
@@ -389,6 +389,12 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
389
389
|
let appServer = null
|
|
390
390
|
let appServerThreadReady = false
|
|
391
391
|
let activeTurnId = null
|
|
392
|
+
// App Server notifications are asynchronous to turn/completed. In production a
|
|
393
|
+
// stopped turn emitted its durable `aborted` boundary, then delivered a queued
|
|
394
|
+
// item/started 89ms later and a Bash result 159.9s after that. Without a native
|
|
395
|
+
// turn fence those late rows resurrected the lane after Stop. Keep a bounded set
|
|
396
|
+
// of closed native turns and accept turn-scoped traffic only for the one active id.
|
|
397
|
+
const closedAppServerTurnIds = new Set()
|
|
392
398
|
const appServerItems = new Map()
|
|
393
399
|
const streamedAgentItems = new Map() // item id → { cid, text }
|
|
394
400
|
let steerChain = Promise.resolve()
|
|
@@ -423,6 +429,18 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
423
429
|
}
|
|
424
430
|
|
|
425
431
|
const note = (text) => { try { onEvent?.({ kind: 'note', text }) } catch { /* noop */ } }
|
|
432
|
+
|
|
433
|
+
const closeAppServerTurn = (turnId = activeTurnId) => {
|
|
434
|
+
const id = String(turnId || '')
|
|
435
|
+
if (!id) return
|
|
436
|
+
closedAppServerTurnIds.add(id)
|
|
437
|
+
while (closedAppServerTurnIds.size > 64) closedAppServerTurnIds.delete(closedAppServerTurnIds.values().next().value)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const appServerTurnIsCurrent = (params = {}) => {
|
|
441
|
+
const turnId = String(params.turnId || '')
|
|
442
|
+
return !!turnId && !ended && !aborted && !closedAppServerTurnIds.has(turnId) && turnId === String(activeTurnId || '')
|
|
443
|
+
}
|
|
426
444
|
if (requestedResume && !resumeUsable) {
|
|
427
445
|
queueMicrotask(() => note('The stopped Codex turn had no resumable context; continuing in a fresh thread.'))
|
|
428
446
|
}
|
|
@@ -439,9 +457,15 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
439
457
|
|
|
440
458
|
function appServerNotification(method, params = {}) {
|
|
441
459
|
if (method === 'turn/started' && params.turn?.id) {
|
|
442
|
-
|
|
460
|
+
const turnId = String(params.turn.id)
|
|
461
|
+
if (ended || aborted) { closeAppServerTurn(turnId); return }
|
|
462
|
+
if (!activeTurnId) activeTurnId = turnId
|
|
443
463
|
return
|
|
444
464
|
}
|
|
465
|
+
// Every mapped App Server notification below is turn-scoped in the tested
|
|
466
|
+
// protocol and carries turnId. Fail closed on missing, stale, completed, or
|
|
467
|
+
// interrupted ids so a provider's buffered stdout cannot reopen a closed turn.
|
|
468
|
+
if ((method === 'turn/plan/updated' || method === 'item/agentMessage/delta' || method === 'item/started' || method === 'item/completed' || method === 'error') && !appServerTurnIsCurrent(params)) return
|
|
445
469
|
if (method === 'turn/plan/updated') {
|
|
446
470
|
const todos = (Array.isArray(params.plan) ? params.plan : []).map(({ step, status }) => ({
|
|
447
471
|
content: step || '',
|
|
@@ -486,6 +510,13 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
486
510
|
}
|
|
487
511
|
|
|
488
512
|
async function appServerRequest(method, params = {}) {
|
|
513
|
+
// A request racing Stop must never raise a fresh room card after the turn's
|
|
514
|
+
// durable boundary. Respond with the protocol's ordinary denial shape so the
|
|
515
|
+
// native runtime can settle without reopening any ThinkPool state.
|
|
516
|
+
if (!appServerTurnIsCurrent(params)) {
|
|
517
|
+
if (method === 'item/tool/requestUserInput') return { answers: {} }
|
|
518
|
+
return { decision: 'decline' }
|
|
519
|
+
}
|
|
489
520
|
const card = codexApprovalCard(method, params, appServerItems.get(params.itemId))
|
|
490
521
|
if (!card) throw new Error(`Unsupported Codex App Server request: ${method}`)
|
|
491
522
|
// Missing/broken room permission plumbing denies safely. App Server expects
|
|
@@ -578,10 +609,12 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
578
609
|
// id. abort() already closed the room boundary; interrupt the now-known
|
|
579
610
|
// native turn before waiting, and never let it continue invisibly.
|
|
580
611
|
if (aborted || ended) {
|
|
612
|
+
closeAppServerTurn(activeTurnId)
|
|
581
613
|
try { await appServer.interrupt({ threadId: sessionId, turnId: activeTurnId }) } catch { /* boundary is already closed */ }
|
|
582
614
|
return true
|
|
583
615
|
}
|
|
584
616
|
const completed = await appServer.waitForTurn(activeTurnId)
|
|
617
|
+
closeAppServerTurn(completed?.turn?.id || activeTurnId)
|
|
585
618
|
const status = completed?.turn?.status
|
|
586
619
|
if (aborted || status === 'interrupted') {
|
|
587
620
|
emitTurnBoundary({ kind: 'result', subtype: 'aborted', sessionId, model: activeModel, costUsd: null, usage: null, numTurns: 1, durationMs: completed?.turn?.durationMs, denials: 0, resultText: null })
|
|
@@ -596,6 +629,7 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
596
629
|
// turns only; they can safely resume this thread through the stable driver.
|
|
597
630
|
appServerDisabled = true
|
|
598
631
|
appServerThreadReady = false
|
|
632
|
+
closeAppServerTurn(activeTurnId)
|
|
599
633
|
try { appServer?.end() } catch { /* noop */ }
|
|
600
634
|
appServer = null
|
|
601
635
|
if (!ended) pushMappedEvent({ type: 'turn.failed', message: `codex app-server turn failed: ${error?.message || error}` })
|
|
@@ -625,10 +659,12 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
625
659
|
activeTurnId = started?.turn?.id || started?.turnId
|
|
626
660
|
if (!activeTurnId) throw new Error('Codex review/start returned no turn id')
|
|
627
661
|
if (aborted || ended) {
|
|
662
|
+
closeAppServerTurn(activeTurnId)
|
|
628
663
|
try { await appServer.interrupt({ threadId: sessionId, turnId: activeTurnId }) } catch { /* boundary is already closed */ }
|
|
629
664
|
return true
|
|
630
665
|
}
|
|
631
666
|
const completed = await appServer.waitForTurn(activeTurnId)
|
|
667
|
+
closeAppServerTurn(completed?.turn?.id || activeTurnId)
|
|
632
668
|
const status = completed?.turn?.status
|
|
633
669
|
if (aborted || status === 'interrupted') {
|
|
634
670
|
emitTurnBoundary({ kind: 'result', subtype: 'aborted', sessionId, model: activeModel, costUsd: null, usage: null, numTurns: 1, durationMs: completed?.turn?.durationMs, denials: 0, resultText: null })
|
|
@@ -640,6 +676,7 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
640
676
|
} catch (error) {
|
|
641
677
|
appServerDisabled = true
|
|
642
678
|
appServerThreadReady = false
|
|
679
|
+
closeAppServerTurn(activeTurnId)
|
|
643
680
|
try { appServer?.end() } catch { /* noop */ }
|
|
644
681
|
appServer = null
|
|
645
682
|
if (!ended) pushMappedEvent({ type: 'turn.failed', message: `codex review failed: ${error?.message || error}` })
|
|
@@ -834,6 +871,7 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
834
871
|
const bootstrapOnly = turnActive && !activeTurnId && !child
|
|
835
872
|
aborted = true
|
|
836
873
|
queue.length = 0
|
|
874
|
+
closeAppServerTurn(activeTurnId)
|
|
837
875
|
if (appServer && activeTurnId) { void appServer.interrupt({ threadId: sessionId, turnId: activeTurnId }).catch(() => {}) ; return }
|
|
838
876
|
if (child) { try { child.kill('SIGTERM') } catch { /* noop */ } ; setTimeout(() => { try { child?.kill('SIGKILL') } catch { /* noop */ } }, 1500) }
|
|
839
877
|
else if (bootstrapOnly) {
|
|
@@ -847,6 +885,7 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
847
885
|
ended = true
|
|
848
886
|
turnActive = false
|
|
849
887
|
queue.length = 0
|
|
888
|
+
closeAppServerTurn(activeTurnId)
|
|
850
889
|
if (child) { try { child.kill() } catch { /* noop */ } }
|
|
851
890
|
try { appServer?.end() } catch { /* noop */ }
|
|
852
891
|
void mcpHttpPromise?.then((h) => h.close()).catch(() => {})
|