thinkpool-pair 0.7.309 → 0.7.310
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/abort-turn-barrier.mjs +23 -0
- package/bridge.mjs +12 -1
- package/codex-session.mjs +11 -1
- package/hermes-session.mjs +8 -2
- package/package.json +2 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Realtime delivers code-abort and the browser's newly-flushed code-turn through
|
|
2
|
+
// independent handlers. Keep that next human turn behind the runtime's real abort
|
|
3
|
+
// completion so a late aborted boundary cannot close the new turn.
|
|
4
|
+
|
|
5
|
+
export function queueAbortBarrier(entry, runAbort = () => entry?.session?.abort?.()) {
|
|
6
|
+
if (!entry) return Promise.resolve()
|
|
7
|
+
const prior = entry._abortBarrier || Promise.resolve()
|
|
8
|
+
let barrier
|
|
9
|
+
barrier = Promise.resolve(prior)
|
|
10
|
+
.catch(() => {})
|
|
11
|
+
.then(() => runAbort())
|
|
12
|
+
.catch(() => {})
|
|
13
|
+
.finally(() => {
|
|
14
|
+
if (entry._abortBarrier === barrier) entry._abortBarrier = null
|
|
15
|
+
})
|
|
16
|
+
entry._abortBarrier = barrier
|
|
17
|
+
return barrier
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function waitForAbortBarrier(entry) {
|
|
21
|
+
const barrier = entry?._abortBarrier
|
|
22
|
+
if (barrier) await barrier
|
|
23
|
+
}
|
package/bridge.mjs
CHANGED
|
@@ -65,6 +65,7 @@ import { commandOnPath } from './agent-detect.mjs'
|
|
|
65
65
|
import { requiresSdkAdmission, sdkSmokePassed } from './sdk-admission.mjs'
|
|
66
66
|
import { hermesUserInputResponse } from './question-response.mjs'
|
|
67
67
|
import { hostMemoryAdmission } from './host-memory.mjs'
|
|
68
|
+
import { queueAbortBarrier, waitForAbortBarrier } from './abort-turn-barrier.mjs'
|
|
68
69
|
|
|
69
70
|
const STRUCTURED_MODES = new Set(['default', 'acceptEdits', 'plan', 'review', 'bypassPermissions'])
|
|
70
71
|
import { FLOW_CONDUCTOR_PROMPT, FLOW_LANE_PROMPT, FLOW_CODEX_CONDUCTOR_PROMPT, FLOW_CODEX_LANE_PROMPT, buildConductorEnv, assembleCrossWaveContext, buildLanePrompt } from './flow-conductor.mjs'
|
|
@@ -3996,6 +3997,12 @@ channel
|
|
|
3996
3997
|
.on('broadcast', { event: 'code-turn' }, async ({ payload }) => {
|
|
3997
3998
|
const s = payload?.term && sessions.get(payload.term)
|
|
3998
3999
|
if (!s || payload.text == null) return
|
|
4000
|
+
// Stop and the browser's queue flush are separate realtime frames. Codex and
|
|
4001
|
+
// Hermes need their native cancellation to settle before the flushed turn is
|
|
4002
|
+
// accepted; otherwise the old aborted result can land after the new `you` line
|
|
4003
|
+
// and close it. Re-check identity after the await in case the lane was closed.
|
|
4004
|
+
await waitForAbortBarrier(s)
|
|
4005
|
+
if (sessions.get(payload.term) !== s || !s.session) return
|
|
3999
4006
|
markActivity()
|
|
4000
4007
|
// B2/C: a real human turn is hop 0 with fresh cross-terminal budgets. This is
|
|
4001
4008
|
// the loop-breaker reset point — an injected (cross-posted) turn does NOT come
|
|
@@ -4290,7 +4297,11 @@ channel
|
|
|
4290
4297
|
})
|
|
4291
4298
|
.on('broadcast', { event: 'code-abort' }, ({ payload }) => {
|
|
4292
4299
|
const s = payload?.term && sessions.get(payload.term)
|
|
4293
|
-
if (s) {
|
|
4300
|
+
if (s) {
|
|
4301
|
+
drainPending(s) // settle any open permission card so the hook doesn't hang
|
|
4302
|
+
queueAbortBarrier(s).finally(() => announce())
|
|
4303
|
+
announce()
|
|
4304
|
+
}
|
|
4294
4305
|
})
|
|
4295
4306
|
.on('broadcast', { event: 'code-mode' }, ({ payload }) => {
|
|
4296
4307
|
const s = payload?.term && sessions.get(payload.term)
|
package/codex-session.mjs
CHANGED
|
@@ -589,6 +589,9 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
589
589
|
}
|
|
590
590
|
|
|
591
591
|
async function runAppServer(prompt, options = {}) {
|
|
592
|
+
// Stop can win before the queued pump enters native startup. Do not launch a
|
|
593
|
+
// fresh App Server merely to discover the accepted room turn was cancelled.
|
|
594
|
+
if (ended || aborted) return true
|
|
592
595
|
if (!await ensureAppServer()) return false
|
|
593
596
|
// Stop can land while the cold App Server / MCP bootstrap is still awaiting.
|
|
594
597
|
// The accepted turn was already closed at the room boundary; never start it
|
|
@@ -651,6 +654,7 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
651
654
|
}
|
|
652
655
|
|
|
653
656
|
async function runAppServerReview(commandText) {
|
|
657
|
+
if (ended || aborted) return true
|
|
654
658
|
if (!await ensureAppServer()) return false
|
|
655
659
|
if (ended || aborted) return true
|
|
656
660
|
mapper.setUsageBaseline(sessionId ? readCodexThreadUsage(sessionId) : null)
|
|
@@ -869,11 +873,13 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
869
873
|
return true
|
|
870
874
|
},
|
|
871
875
|
abort() {
|
|
876
|
+
const activeChain = chain
|
|
872
877
|
const bootstrapOnly = turnActive && !activeTurnId && !child
|
|
873
878
|
aborted = true
|
|
874
879
|
queue.length = 0
|
|
875
880
|
closeAppServerTurn(activeTurnId)
|
|
876
|
-
|
|
881
|
+
let interrupt = Promise.resolve()
|
|
882
|
+
if (appServer && activeTurnId) interrupt = appServer.interrupt({ threadId: sessionId, turnId: activeTurnId }).catch(() => {})
|
|
877
883
|
if (child) { try { child.kill('SIGTERM') } catch { /* noop */ } ; setTimeout(() => { try { child?.kill('SIGKILL') } catch { /* noop */ } }, 1500) }
|
|
878
884
|
else if (bootstrapOnly) {
|
|
879
885
|
// No native turn exists to produce a boundary. Close the optimistic
|
|
@@ -881,6 +887,10 @@ export function startCodexSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
881
887
|
// aborted check discard the delayed startup.
|
|
882
888
|
emitTurnBoundary({ kind: 'result', subtype: 'aborted', sessionId, model: activeModel, costUsd: null, usage: null, numTurns: 0, durationMs: undefined, denials: 0, resultText: null })
|
|
883
889
|
}
|
|
890
|
+
// `code-turn` may already be queued behind Stop in Realtime. Resolve only
|
|
891
|
+
// after the interrupted native turn's chain has fully unwound; accepting a
|
|
892
|
+
// new turn earlier lets the old finally/result close the new lifecycle.
|
|
893
|
+
return interrupt.then(() => activeChain).catch(() => activeChain).then(() => {})
|
|
884
894
|
},
|
|
885
895
|
end() {
|
|
886
896
|
ended = true
|
package/hermes-session.mjs
CHANGED
|
@@ -566,7 +566,7 @@ export function startHermesSession({
|
|
|
566
566
|
})
|
|
567
567
|
return true
|
|
568
568
|
},
|
|
569
|
-
abort() {
|
|
569
|
+
async abort() {
|
|
570
570
|
if (!turnActive) return
|
|
571
571
|
queuedTurns.length = 0
|
|
572
572
|
queueDrainPending = false
|
|
@@ -582,14 +582,16 @@ export function startHermesSession({
|
|
|
582
582
|
retireClient()
|
|
583
583
|
client?.end()
|
|
584
584
|
finishAbortedTurn()
|
|
585
|
+
await promptChain.catch(() => {})
|
|
585
586
|
return
|
|
586
587
|
}
|
|
587
588
|
if (!sessionId || !client?.alive) {
|
|
588
589
|
finishAbortedTurn()
|
|
590
|
+
await promptChain.catch(() => {})
|
|
589
591
|
return
|
|
590
592
|
}
|
|
591
593
|
finishAbortedTurn()
|
|
592
|
-
|
|
594
|
+
await client.notify('session/cancel', { sessionId }).catch((error) => {
|
|
593
595
|
// Never display a stopped lane while the un-cancelled Hermes process
|
|
594
596
|
// might still be executing. A transport failure tears down the ACP
|
|
595
597
|
// process and makes the lane honestly non-resumable until restart.
|
|
@@ -597,6 +599,10 @@ export function startHermesSession({
|
|
|
597
599
|
client?.end()
|
|
598
600
|
emit({ kind: 'error', message: `Hermes cancellation delivery failed; ACP process stopped: ${error?.message || error}`, recoverable: true })
|
|
599
601
|
})
|
|
602
|
+
// The ACP cancellation notification is not itself the completion edge.
|
|
603
|
+
// Wait for the active prompt promise to unwind (including the 0.18.2
|
|
604
|
+
// cancelled-response recovery) before a bridge queue flush starts anew.
|
|
605
|
+
await promptChain.catch(() => {})
|
|
600
606
|
},
|
|
601
607
|
end() {
|
|
602
608
|
ended = true
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thinkpool-pair",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.310",
|
|
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": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bridge.mjs",
|
|
11
|
+
"abort-turn-barrier.mjs",
|
|
11
12
|
"host-memory.mjs",
|
|
12
13
|
"sdk-smoke.mjs",
|
|
13
14
|
"sdk-admission.mjs",
|