thinkpool-pair 0.7.50 → 0.7.52
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 +29 -1
- package/package.json +1 -1
- package/service.mjs +28 -16
package/bridge.mjs
CHANGED
|
@@ -136,8 +136,11 @@ if (argv[0] === 'install-service' || argv[0] === 'uninstall-service') {
|
|
|
136
136
|
const svcRoom = account ? null : arg1.toUpperCase().trim()
|
|
137
137
|
const sd = argv.indexOf('--')
|
|
138
138
|
const svcCmd = sd >= 0 ? argv.slice(sd + 1) : []
|
|
139
|
+
// PINNED to the installed version by default (a bad publish can't auto-roll to the
|
|
140
|
+
// always-on service); --auto-update opts into @latest tracking. (2026-06-22)
|
|
141
|
+
const autoUpdate = argv.includes('--auto-update')
|
|
139
142
|
const svc = await import('./service.mjs')
|
|
140
|
-
if (argv[0] === 'install-service') svc.installService(svcRoom, svcCmd)
|
|
143
|
+
if (argv[0] === 'install-service') svc.installService(svcRoom, svcCmd, { autoUpdate })
|
|
141
144
|
else svc.uninstallService(svcRoom)
|
|
142
145
|
process.exit(0)
|
|
143
146
|
}
|
|
@@ -653,6 +656,22 @@ function pushLog(entry, evt) {
|
|
|
653
656
|
return evt
|
|
654
657
|
}
|
|
655
658
|
|
|
659
|
+
// Resume-idle guard (2026-06-22 cooked-terminal bug): a turn force-killed mid-flight
|
|
660
|
+
// leaves the restored log ending on in-flight events (assistant/you/tool_result) with
|
|
661
|
+
// NO terminal `result`, so the web reads it as "thinking" forever AND Stop is a no-op
|
|
662
|
+
// (the freshly-resumed SDK session has no live turn to interrupt — q.interrupt() on an
|
|
663
|
+
// idle query does nothing). Mirror of the client working-scan in
|
|
664
|
+
// src/pages/code/structured.jsx: walking back from the end, a result/error/propose/pool
|
|
665
|
+
// closes the turn; an assistant/you/tool_result reached first means it's still open.
|
|
666
|
+
function restoredTurnOpen(log) {
|
|
667
|
+
for (let i = log.length - 1; i >= 0; i--) {
|
|
668
|
+
const k = log[i]?.kind
|
|
669
|
+
if (k === 'result' || k === 'error' || k === 'propose' || k === 'pool') return false
|
|
670
|
+
if (k === 'assistant' || k === 'you' || k === 'tool_result') return true
|
|
671
|
+
}
|
|
672
|
+
return false
|
|
673
|
+
}
|
|
674
|
+
|
|
656
675
|
// The Phase-2 path: instead of a PTY, run Claude Code through the Agent SDK and
|
|
657
676
|
// relay STRUCTURED events. onEvent → broadcast `code-event` + print locally +
|
|
658
677
|
// persist to the host file; tool calls round-trip through the perm card; the
|
|
@@ -666,6 +685,15 @@ function openStructured({ id, model, resume, log, commands, mode }) {
|
|
|
666
685
|
// event-id.mjs + src/pages/code/seqDedup.js). pushLog() is the single stamp point.
|
|
667
686
|
entry.seq = makeSeqCounter(maxSeq(entry.log))
|
|
668
687
|
sessions.set(id, entry)
|
|
688
|
+
// Close an interrupted turn so a restored terminal comes back IDLE-with-history
|
|
689
|
+
// instead of stuck "thinking" (see restoredTurnOpen). Idempotent: the closer is
|
|
690
|
+
// persisted, so a later resume sees a closed turn and no-ops.
|
|
691
|
+
if (entry.log.length && restoredTurnOpen(entry.log)) {
|
|
692
|
+
const closer = { kind: 'result', subtype: 'aborted', recovered: true }
|
|
693
|
+
stampEvent(closer)
|
|
694
|
+
pushLog(entry, closer)
|
|
695
|
+
process.stderr.write(`\n ◆ closed an interrupted turn on resume (${id.slice(0, 8)}) — restored idle.\n`)
|
|
696
|
+
}
|
|
669
697
|
// This session's private mockup outbox — render.sh writes manifests here (via
|
|
670
698
|
// TP_MOCKUP_OUTBOX below) and the watcher attributes every card to THIS id, so
|
|
671
699
|
// they land on this session even after other terminals open/close.
|
package/package.json
CHANGED
package/service.mjs
CHANGED
|
@@ -28,6 +28,12 @@ const label = (room) => room ? `io.thinkpool.pair.${room.toLowerCase()}` : 'io.t
|
|
|
28
28
|
const slug = (room) => room || 'account'
|
|
29
29
|
const xml = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
30
30
|
|
|
31
|
+
// The version installing the service. By DEFAULT the service is pinned to this exact
|
|
32
|
+
// version (not @latest) so a future bad npm publish can't auto-roll to every machine's
|
|
33
|
+
// always-on bridge — that's what spread the broken 0.7.49 on 2026-06-22. Updates become
|
|
34
|
+
// deliberate (re-run install-service). `--auto-update` opts back into @latest tracking.
|
|
35
|
+
const VERSION = (() => { try { return JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version } catch { return 'latest' } })()
|
|
36
|
+
|
|
31
37
|
// Resolve the real npx next to the current node, so the service stays seamless:
|
|
32
38
|
// it runs `npx -y thinkpool-pair@latest …`, always picking up the newest publish
|
|
33
39
|
// with no re-install. Absolute path + injected PATH so it works under launchd /
|
|
@@ -40,7 +46,7 @@ function npxPath(platform) {
|
|
|
40
46
|
|
|
41
47
|
// Pure artifact builder — returned shape is testable without side effects.
|
|
42
48
|
// room falsy → ACCOUNT service (bare `thinkpool-pair`, auto-serves all sessions).
|
|
43
|
-
export function buildArtifact(platform, { room = null, cmdArgs = [] } = {}) {
|
|
49
|
+
export function buildArtifact(platform, { room = null, cmdArgs = [], autoUpdate = false } = {}) {
|
|
44
50
|
const npx = npxPath(platform)
|
|
45
51
|
const cwd = process.cwd()
|
|
46
52
|
const id = slug(room)
|
|
@@ -48,10 +54,15 @@ export function buildArtifact(platform, { room = null, cmdArgs = [] } = {}) {
|
|
|
48
54
|
const log = path.join(logDir, `${id}.log`)
|
|
49
55
|
const tail = cmdArgs.length ? ['--', ...cmdArgs] : []
|
|
50
56
|
const desc = room ? `bridge (${room})` : 'account bridge'
|
|
51
|
-
// OS-supervised tiers (launchd KeepAlive / systemd Restart) don't need
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
57
|
+
// OS-supervised tiers (launchd KeepAlive / systemd Restart) don't need --supervise.
|
|
58
|
+
// PINNED by default to `@${VERSION}` so a KeepAlive restart re-runs the SAME known-good
|
|
59
|
+
// version — a bad publish can't auto-roll here. `autoUpdate` opts into `@latest` tracking
|
|
60
|
+
// (and the in-process self-updater via THINKPOOL_PAIR_AUTOUPDATE below).
|
|
61
|
+
const verSpec = autoUpdate ? 'thinkpool-pair@latest' : `thinkpool-pair@${VERSION}`
|
|
62
|
+
const pkgArgs = room ? ['-y', verSpec, room] : ['-y', verSpec]
|
|
63
|
+
// In-process self-update (account.mjs / bridge.mjs) is gated on this env — only on when
|
|
64
|
+
// the user explicitly opted into auto-update.
|
|
65
|
+
const autoUpdateEnv = autoUpdate ? { THINKPOOL_PAIR_AUTOUPDATE: '1' } : {}
|
|
55
66
|
|
|
56
67
|
if (platform === 'darwin') {
|
|
57
68
|
const args = [npx, ...pkgArgs, ...tail] // launchd KeepAlive supervises
|
|
@@ -66,7 +77,7 @@ export function buildArtifact(platform, { room = null, cmdArgs = [] } = {}) {
|
|
|
66
77
|
<key>WorkingDirectory</key><string>${xml(cwd)}</string>
|
|
67
78
|
<key>StandardOutPath</key><string>${xml(log)}</string>
|
|
68
79
|
<key>StandardErrorPath</key><string>${xml(log)}</string>
|
|
69
|
-
<key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(process.env.PATH || '')}</string
|
|
80
|
+
<key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(process.env.PATH || '')}</string>${autoUpdate ? '<key>THINKPOOL_PAIR_AUTOUPDATE</key><string>1</string>' : ''}</dict>
|
|
70
81
|
</dict></plist>\n`
|
|
71
82
|
return { file, content, logDir, post: [`launchctl unload "${file}" 2>/dev/null; launchctl load "${file}"`], note: 'Loaded as a LaunchAgent — starts at login, restarts on crash.' }
|
|
72
83
|
}
|
|
@@ -84,8 +95,7 @@ Restart=always
|
|
|
84
95
|
RestartSec=2
|
|
85
96
|
WorkingDirectory=${cwd}
|
|
86
97
|
Environment=PATH=${process.env.PATH || ''}
|
|
87
|
-
Environment=THINKPOOL_PAIR_AUTOUPDATE=1
|
|
88
|
-
StandardOutput=append:${log}
|
|
98
|
+
${autoUpdate ? 'Environment=THINKPOOL_PAIR_AUTOUPDATE=1\n' : ''}StandardOutput=append:${log}
|
|
89
99
|
StandardError=append:${log}
|
|
90
100
|
|
|
91
101
|
[Install]
|
|
@@ -102,8 +112,8 @@ WantedBy=default.target
|
|
|
102
112
|
const startup = path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup')
|
|
103
113
|
const file = path.join(startup, `thinkpool-pair-${id}.cmd`)
|
|
104
114
|
const inner = room
|
|
105
|
-
? ['npx', '-y',
|
|
106
|
-
: ['npx', '-y',
|
|
115
|
+
? ['npx', '-y', verSpec, room, '--supervise', ...tail].join(' ')
|
|
116
|
+
: ['npx', '-y', verSpec].join(' ')
|
|
107
117
|
const content = `@echo off\r\ntitle thinkpool-pair ${id}\r\n${inner}\r\n`
|
|
108
118
|
return { file, content, logDir, post: [], note: 'Installed to the Startup folder — runs at login' + (room ? ' with --supervise (auto-restart on crash).' : ' (account mode).') + ' Start it now without rebooting by double-clicking the .cmd, or run it from a terminal.' }
|
|
109
119
|
}
|
|
@@ -111,8 +121,8 @@ WantedBy=default.target
|
|
|
111
121
|
throw new Error(`unsupported platform: ${platform}`)
|
|
112
122
|
}
|
|
113
123
|
|
|
114
|
-
export function installService(room, cmdArgs = []) {
|
|
115
|
-
const a = buildArtifact(process.platform, { room, cmdArgs })
|
|
124
|
+
export function installService(room, cmdArgs = [], { autoUpdate = false } = {}) {
|
|
125
|
+
const a = buildArtifact(process.platform, { room, cmdArgs, autoUpdate })
|
|
116
126
|
fs.mkdirSync(a.logDir, { recursive: true })
|
|
117
127
|
fs.mkdirSync(path.dirname(a.file), { recursive: true })
|
|
118
128
|
fs.writeFileSync(a.file, a.content)
|
|
@@ -124,12 +134,14 @@ export function installService(room, cmdArgs = []) {
|
|
|
124
134
|
// roughly every 30 min (THINKPOOL_PAIR_UPDATE_INTERVAL) and only restarts once
|
|
125
135
|
// every served session is idle, so a just-published version can take a while.
|
|
126
136
|
const idleSecs = Math.max(10, parseInt(process.env.THINKPOOL_PAIR_UPDATE_IDLE, 10) || 90)
|
|
127
|
-
const
|
|
128
|
-
?
|
|
129
|
-
|
|
137
|
+
const updateNote = autoUpdate
|
|
138
|
+
? (process.platform === 'win32'
|
|
139
|
+
? 'auto-update ON: a new thinkpool-pair@latest applies on the next login/restart (Windows has no in-process self-update).'
|
|
140
|
+
: `auto-update ON: checks npm ~every 30 min and restarts to apply a new @latest once sessions are idle (≥${idleSecs}s quiet; they resume in place).`)
|
|
141
|
+
: `PINNED to ${VERSION} (safe default — a bad publish can't auto-roll to this always-on bridge). To update: re-run install-service from a newer thinkpool-pair. Pass --auto-update to track @latest instead.`
|
|
130
142
|
const removeArg = room ? ` ${room}` : ''
|
|
131
143
|
const what = room ? `room ${room}` : 'your account (auto-serves every session)'
|
|
132
|
-
process.stderr.write(` ◆ ${what}\n ◆ ${a.note}\n ◆ ${
|
|
144
|
+
process.stderr.write(` ◆ ${what}\n ◆ ${a.note}\n ◆ ${updateNote}\n ◆ logs: ${path.join(a.logDir, `${slug(room)}.log`)}\n ◆ remove with: thinkpool-pair uninstall-service${removeArg}\n\n`)
|
|
133
145
|
}
|
|
134
146
|
|
|
135
147
|
// Is a boot-persistent service already installed? (artifact file present.)
|