thinkpool-pair 0.7.358 → 0.7.359
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/account.mjs +3 -1
- package/bridge.mjs +288 -16
- package/package.json +3 -1
- package/scheduled-run-admission.mjs +364 -0
- package/scheduled-runs.mjs +268 -0
- package/service.mjs +10 -4
- package/session-store.mjs +117 -0
- package/thinkpool-capabilities.json +6 -4
package/service.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import { hostMemoryAdmission } from './host-memory.mjs'
|
|
|
26
26
|
import { readSupervisorReady, supervisorReadyMatches } from './supervisor-ready.mjs'
|
|
27
27
|
import { pairCli } from './command-guidance.mjs'
|
|
28
28
|
import { loadKeepAwakePreference } from './keep-awake.mjs'
|
|
29
|
+
import { scheduledRunsEnabled } from './scheduled-runs.mjs'
|
|
29
30
|
|
|
30
31
|
// Service identity. Account mode has no room → a single stable id so there's
|
|
31
32
|
// exactly one account service per machine (a second install replaces it).
|
|
@@ -131,7 +132,7 @@ export function provisionRuntime(version, { exec = execSync, root = path.join(os
|
|
|
131
132
|
|
|
132
133
|
// Pure artifact builder — returned shape is testable without side effects.
|
|
133
134
|
// room falsy → ACCOUNT service (bare `thinkpool-pair`, auto-serves all sessions).
|
|
134
|
-
export function buildArtifact(platform, { room = null, cmdArgs = [], autoUpdate = false, version = VERSION, staleProof = false, runtimeEntry = null } = {}) {
|
|
135
|
+
export function buildArtifact(platform, { room = null, cmdArgs = [], autoUpdate = false, version = VERSION, staleProof = false, runtimeEntry = null, scheduledRuns = scheduledRunsEnabled() } = {}) {
|
|
135
136
|
const npx = npxPath(platform)
|
|
136
137
|
// Account mode discovers each room's own bound directory, so its supervisor must use
|
|
137
138
|
// a stable home cwd. Persisting the caller's managed worktree here makes the service
|
|
@@ -161,6 +162,11 @@ export function buildArtifact(platform, { room = null, cmdArgs = [], autoUpdate
|
|
|
161
162
|
// In-process self-update (account.mjs / bridge.mjs) is gated on this env — only on when
|
|
162
163
|
// the user explicitly opted into auto-update.
|
|
163
164
|
const autoUpdateEnv = autoUpdate ? { THINKPOOL_PAIR_AUTOUPDATE: '1' } : {}
|
|
165
|
+
// Persistent services rebuild a deliberately tiny environment. Preserve this
|
|
166
|
+
// activation only as the exact boolean true; arbitrary caller values are omitted.
|
|
167
|
+
const scheduledRunsEnv = scheduledRuns === true
|
|
168
|
+
? { darwin: '<key>TP_SCHEDULED_RUNS_ENABLED</key><string>1</string>', linux: 'Environment=TP_SCHEDULED_RUNS_ENABLED=1\n', win32: 'set "TP_SCHEDULED_RUNS_ENABLED=1"\r\n' }
|
|
169
|
+
: { darwin: '', linux: '', win32: '' }
|
|
164
170
|
|
|
165
171
|
if (platform === 'darwin') {
|
|
166
172
|
// launchd KeepAlive supervises. Stable runtimes execute directly; the legacy npx
|
|
@@ -187,7 +193,7 @@ export function buildArtifact(platform, { room = null, cmdArgs = [], autoUpdate
|
|
|
187
193
|
<key>WorkingDirectory</key><string>${xml(cwd)}</string>
|
|
188
194
|
<key>StandardOutPath</key><string>${xml(log)}</string>
|
|
189
195
|
<key>StandardErrorPath</key><string>${xml(log)}</string>
|
|
190
|
-
<key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(servicePath)}</string>${runtimeEntry ? '' : `<key>NPM_CONFIG_CACHE</key><string>${xml(npmCache)}</string>`}${autoUpdate ? '<key>THINKPOOL_PAIR_AUTOUPDATE</key><string>1</string>' : ''}</dict>
|
|
196
|
+
<key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(servicePath)}</string>${runtimeEntry ? '' : `<key>NPM_CONFIG_CACHE</key><string>${xml(npmCache)}</string>`}${autoUpdate ? '<key>THINKPOOL_PAIR_AUTOUPDATE</key><string>1</string>' : ''}${scheduledRunsEnv.darwin}</dict>
|
|
191
197
|
</dict></plist>\n`
|
|
192
198
|
// The destructive reload is intentionally NOT represented as an inline `post`
|
|
193
199
|
// command. installService stages this plist and hands the transaction to an
|
|
@@ -223,7 +229,7 @@ RestartSec=2
|
|
|
223
229
|
RestartPreventExitStatus=0
|
|
224
230
|
WorkingDirectory=${cwd}
|
|
225
231
|
Environment=PATH=${servicePath}
|
|
226
|
-
${runtimeEntry ? '' : `Environment=NPM_CONFIG_CACHE=${npmCache}\n`}${autoUpdate ? 'Environment=THINKPOOL_PAIR_AUTOUPDATE=1\n' : ''}StandardOutput=append:${log}
|
|
232
|
+
${runtimeEntry ? '' : `Environment=NPM_CONFIG_CACHE=${npmCache}\n`}${autoUpdate ? 'Environment=THINKPOOL_PAIR_AUTOUPDATE=1\n' : ''}${scheduledRunsEnv.linux}StandardOutput=append:${log}
|
|
227
233
|
StandardError=append:${log}
|
|
228
234
|
|
|
229
235
|
[Install]
|
|
@@ -244,7 +250,7 @@ WantedBy=default.target
|
|
|
244
250
|
: room
|
|
245
251
|
? ['npx', '-y', ...onlineFlag, verSpec, room, '--supervise', ...tail].join(' ')
|
|
246
252
|
: ['npx', '-y', ...onlineFlag, verSpec].join(' ')
|
|
247
|
-
const content = `@echo off\r\ntitle thinkpool-pair ${id}\r\n${runtimeEntry ? '' : `set "NPM_CONFIG_CACHE=${npmCache}"\r\n`}${inner}\r\n`
|
|
253
|
+
const content = `@echo off\r\ntitle thinkpool-pair ${id}\r\n${runtimeEntry ? '' : `set "NPM_CONFIG_CACHE=${npmCache}"\r\n`}${scheduledRunsEnv.win32}${inner}\r\n`
|
|
248
254
|
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.' }
|
|
249
255
|
}
|
|
250
256
|
|
package/session-store.mjs
CHANGED
|
@@ -292,6 +292,123 @@ export function deleteSession(room, id) {
|
|
|
292
292
|
catch (error) { persistenceError('archive closed session snapshot', p, error); return false }
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
+
// Scheduled outcomes use a separate host outbox from live session snapshots.
|
|
296
|
+
// A person may close the visible terminal while its idempotent database finish
|
|
297
|
+
// is retrying; keeping the intent here lets a restart complete that write without
|
|
298
|
+
// loadAll() resurrecting the deliberately closed terminal.
|
|
299
|
+
const scheduledOutcomesFile = (room) => path.join(dir(room), '.scheduled-outcomes')
|
|
300
|
+
const safeTerminalId = (value) => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:@-]{0,159}$/.test(value)
|
|
301
|
+
const safeRunId = (value) => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:@-]{0,159}$/.test(value)
|
|
302
|
+
const readScheduledOutcomeMap = (room) => {
|
|
303
|
+
try {
|
|
304
|
+
const value = JSON.parse(fs.readFileSync(scheduledOutcomesFile(room), 'utf8'))
|
|
305
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
|
306
|
+
} catch { return {} }
|
|
307
|
+
}
|
|
308
|
+
// Pending entries from the original format are raw intents. Once the database
|
|
309
|
+
// finish and recorded session snapshot are both durable, replace that raw entry
|
|
310
|
+
// with a versioned acknowledgement tombstone before attempting deletion. The
|
|
311
|
+
// tombstone is intentionally independent of the live/archived session record:
|
|
312
|
+
// restart recovery can therefore identify an acknowledged exact run and retry
|
|
313
|
+
// deletion only, without issuing another finish RPC or reclassifying its result.
|
|
314
|
+
const scheduledOutcomeRecord = (value) => {
|
|
315
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null
|
|
316
|
+
if (value.v === 1 && value.state === 'acknowledged') {
|
|
317
|
+
const intent = value.intent
|
|
318
|
+
return intent && typeof intent === 'object' && !Array.isArray(intent) &&
|
|
319
|
+
safeRunId(intent.runId) && value.runId === intent.runId
|
|
320
|
+
? { intent, acknowledged: true }
|
|
321
|
+
: null
|
|
322
|
+
}
|
|
323
|
+
return safeRunId(value.runId) ? { intent: value, acknowledged: false } : null
|
|
324
|
+
}
|
|
325
|
+
export function savePendingScheduledOutcome(room, terminalId, intent) {
|
|
326
|
+
if (!safeTerminalId(terminalId) || !intent || typeof intent !== 'object' || Array.isArray(intent) || !safeRunId(intent.runId)) return false
|
|
327
|
+
try {
|
|
328
|
+
ensureDir(room)
|
|
329
|
+
const pending = readScheduledOutcomeMap(room)
|
|
330
|
+
pending[terminalId] = intent
|
|
331
|
+
atomicCommit(scheduledOutcomesFile(room), JSON.stringify(pending))
|
|
332
|
+
return true
|
|
333
|
+
} catch (error) {
|
|
334
|
+
persistenceError('write scheduled outcome outbox', scheduledOutcomesFile(room), error)
|
|
335
|
+
return false
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
export function loadPendingScheduledOutcomes(room) {
|
|
339
|
+
return Object.entries(readScheduledOutcomeMap(room))
|
|
340
|
+
.map(([terminalId, value]) => ({ terminalId, record: scheduledOutcomeRecord(value) }))
|
|
341
|
+
.filter(({ terminalId, record }) => safeTerminalId(terminalId) && record)
|
|
342
|
+
.map(({ terminalId, record }) => ({
|
|
343
|
+
terminalId,
|
|
344
|
+
intent: record.intent,
|
|
345
|
+
...(record.acknowledged ? { acknowledged: true } : {}),
|
|
346
|
+
}))
|
|
347
|
+
.slice(0, LIVE_SESSION_MAX)
|
|
348
|
+
}
|
|
349
|
+
export function loadPendingScheduledOutcome(room, terminalId) {
|
|
350
|
+
if (!safeTerminalId(terminalId)) return null
|
|
351
|
+
return scheduledOutcomeRecord(readScheduledOutcomeMap(room)[terminalId])?.intent || null
|
|
352
|
+
}
|
|
353
|
+
export function acknowledgePendingScheduledOutcome(room, terminalId, runId) {
|
|
354
|
+
if (!safeTerminalId(terminalId) || !safeRunId(runId)) return false
|
|
355
|
+
try {
|
|
356
|
+
const pending = readScheduledOutcomeMap(room)
|
|
357
|
+
const record = scheduledOutcomeRecord(pending[terminalId])
|
|
358
|
+
if (!record || record.intent.runId !== runId) return false
|
|
359
|
+
if (record.acknowledged) return true
|
|
360
|
+
pending[terminalId] = {
|
|
361
|
+
v: 1,
|
|
362
|
+
state: 'acknowledged',
|
|
363
|
+
runId,
|
|
364
|
+
intent: record.intent,
|
|
365
|
+
}
|
|
366
|
+
atomicCommit(scheduledOutcomesFile(room), JSON.stringify(pending))
|
|
367
|
+
return true
|
|
368
|
+
} catch (error) {
|
|
369
|
+
persistenceError('acknowledge scheduled outcome outbox', scheduledOutcomesFile(room), error)
|
|
370
|
+
return false
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
export function deletePendingScheduledOutcome(room, terminalId) {
|
|
374
|
+
if (!safeTerminalId(terminalId)) return false
|
|
375
|
+
try {
|
|
376
|
+
const pending = readScheduledOutcomeMap(room)
|
|
377
|
+
if (!(terminalId in pending)) return true
|
|
378
|
+
delete pending[terminalId]
|
|
379
|
+
if (Object.keys(pending).length) atomicCommit(scheduledOutcomesFile(room), JSON.stringify(pending))
|
|
380
|
+
else fs.rmSync(scheduledOutcomesFile(room), { force: true })
|
|
381
|
+
return true
|
|
382
|
+
} catch (error) {
|
|
383
|
+
persistenceError('delete scheduled outcome outbox', scheduledOutcomesFile(room), error)
|
|
384
|
+
return false
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// A database acknowledgement is not locally committed until the corresponding
|
|
389
|
+
// live-session snapshot has synchronously recorded scheduleOutcomeRecorded=true
|
|
390
|
+
// AND the exact outbox is durably marked acknowledged. Keep deletion behind both
|
|
391
|
+
// barriers so a restart always sees one of:
|
|
392
|
+
// 1. pending outbox intent to replay idempotently;
|
|
393
|
+
// 2. an acknowledged outbox tombstone to delete only; or
|
|
394
|
+
// 3. a recorded session snapshot with no outbox.
|
|
395
|
+
// `flushRecordedSnapshot` is intentionally injected by bridge.mjs because only
|
|
396
|
+
// the live entry can produce its complete session payload.
|
|
397
|
+
export function commitRecordedScheduledOutcome(room, terminalId, flushRecordedSnapshot) {
|
|
398
|
+
let snapshotRecorded = false
|
|
399
|
+
try {
|
|
400
|
+
snapshotRecorded = typeof flushRecordedSnapshot === 'function' && flushRecordedSnapshot() === true
|
|
401
|
+
} catch (error) {
|
|
402
|
+
persistenceError('record scheduled outcome in session snapshot', path.join(dir(room), `${terminalId}.json`), error)
|
|
403
|
+
}
|
|
404
|
+
if (!snapshotRecorded) return { ok: false, snapshotRecorded: false, outboxAcknowledged: false, outboxDeleted: false }
|
|
405
|
+
const intent = loadPendingScheduledOutcome(room, terminalId)
|
|
406
|
+
const outboxAcknowledged = acknowledgePendingScheduledOutcome(room, terminalId, intent?.runId)
|
|
407
|
+
if (!outboxAcknowledged) return { ok: false, snapshotRecorded: true, outboxAcknowledged: false, outboxDeleted: false }
|
|
408
|
+
const outboxDeleted = deletePendingScheduledOutcome(room, terminalId)
|
|
409
|
+
return { ok: outboxDeleted, snapshotRecorded: true, outboxAcknowledged: true, outboxDeleted }
|
|
410
|
+
}
|
|
411
|
+
|
|
295
412
|
// Most-recently-saved structured session for the room (drives attached restore).
|
|
296
413
|
export function loadLatest(room) {
|
|
297
414
|
const recs = listRecs(room).sort((a, b) => (b.savedAt || 0) - (a.savedAt || 0))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"bundleVersion":
|
|
3
|
+
"bundleVersion": 19,
|
|
4
4
|
"contracts": [
|
|
5
5
|
{
|
|
6
6
|
"id": "room-coordination",
|
|
@@ -188,9 +188,9 @@
|
|
|
188
188
|
},
|
|
189
189
|
{
|
|
190
190
|
"id": "design-workspace",
|
|
191
|
-
"version":
|
|
192
|
-
"interactionPrompt": "DESIGN EDITING MODEL: every complete bridge preview_capture card and every trusted source-backed mockup card offers Edit in Design. The bridge freezes a safe rendered-DOM snapshot for application previews; Design edits are then applied by the producing lane to the real application source and verified by a fresh correlated desktop+mobile capture. For source-aware application previews, the bridge validates each opaque source identity against its contained host-only map and the current source-file hash before sharing an exact source location privately with the producing lane; uninstrumented elements keep the selector-based fallback, while stale or mismatched identities fail closed. For authored HTML, the producing lane edits the canonical authored HTML source directly. Edit in Design—not ordinary Preview or unrelated artifact delivery—arms a persistent room-level Design workspace, opens the artifact directly in editable mode, and adds Design · Page name beneath the producing terminal in its Ensemble row for both partners; it creates no terminal, agent runtime, or worker slot. The virtual lane can be closed from that Ensemble row without deleting the artifact. Once armed, the Design lane reopens the active artifact at its latest verified revision without another agent turn, history search, HTML/path request, or re-selection. Design is the default canvas there and Preview is only a temporary interaction mode. Direct text, supported movement, and selected-image changes auto-stage as optimistic drafts in the bounded changes queue; queued edits can be reopened, revised, or removed before Apply. Apply changes sends the ordered batch once to the producing lane, and a successful correlated desktop+mobile render advances the verified revision for the room. Never call a staged draft saved or live. When a person asks for many mockups or options that can share a surface, prefer one source-backed multi-option board or gallery in one file and one card so they can compare together; create separate files/cards only when the person explicitly requests independently editable artifacts or the options cannot be represented faithfully together.",
|
|
193
|
-
"turnReminder": "DESIGN ROUTE: intentional application-preview deliverables use preview_capture with card=true; correlated Design recaptures are recognized automatically. Authored HTML uses the source-backed render helper. Both must produce editable Thinkpool Design cards with verified desktop and mobile renders. Ordinary verification captures stay inline evidence and must not create cards. Cards are delivered after the final agent response so they remain the newest transcript item. Edit in Design explicitly arms the persistent Design workspace and adds its virtual Design · Page beneath the producing terminal in the Ensemble row; Preview alone does not arm it. For large direction sets, consolidate compatible options into one source-backed comparison board/gallery and one card; use separate files/cards only when independent editing is explicitly requested or technically necessary. Do not duplicate the card renders as inline PNGs. Use inline PNG evidence only when no interactive Design artifact is available.",
|
|
191
|
+
"version": 8,
|
|
192
|
+
"interactionPrompt": "DESIGN EDITING MODEL: every complete bridge preview_capture card and every trusted source-backed mockup card offers Edit in Design. The bridge freezes a safe rendered-DOM snapshot for application previews; Design edits are then applied by the producing lane to the real application source and verified by a fresh correlated desktop+mobile capture. For source-aware application previews, the bridge validates each opaque source identity against its contained host-only map and the current source-file hash before sharing an exact source location privately with the producing lane; uninstrumented elements keep the selector-based fallback, while stale or mismatched identities fail closed. For authored HTML, the producing lane edits the canonical authored HTML source directly. Edit in Design—not ordinary Preview or unrelated artifact delivery—arms a persistent room-level Design workspace, opens the artifact directly in editable mode, and adds Design · Page name beneath the producing terminal in its Ensemble row for both partners; it creates no terminal, agent runtime, or worker slot. The virtual lane can be closed from that Ensemble row without deleting the artifact. Once armed, the Design lane reopens the active artifact at its latest verified revision without another agent turn, history search, HTML/path request, or re-selection. Design is the default canvas there and Preview is only a temporary interaction mode. A remote discrete selection may show one bounded, normalized location pulse in the partner's color; it never grants authority, persists an event, or loops. Direct text, supported movement, and selected-image changes auto-stage as optimistic drafts in the bounded changes queue; queued edits can be reopened, revised, or removed before Apply. Apply changes sends the ordered batch once to the producing lane, and a successful correlated desktop+mobile render advances the verified revision for the room. Compare is available only for the exact immutable verified parent/current revision pair and uses its captured desktop or mobile images; staged, stale, cross-page, or incomplete pairs fail closed. Design visual runtimes honor live reduced motion, suspend when hidden or off-screen, cap DPR, and tear down deterministically. Never call a staged draft saved or live. When a person asks for many mockups or options that can share a surface, prefer one source-backed multi-option board or gallery in one file and one card so they can compare together; create separate files/cards only when the person explicitly requests independently editable artifacts or the options cannot be represented faithfully together.",
|
|
193
|
+
"turnReminder": "DESIGN ROUTE: intentional application-preview deliverables use preview_capture with card=true; correlated Design recaptures are recognized automatically. Authored HTML uses the source-backed render helper. Both must produce editable Thinkpool Design cards with verified desktop and mobile renders. Ordinary verification captures stay inline evidence and must not create cards. Cards are delivered after the final agent response so they remain the newest transcript item. Edit in Design explicitly arms the persistent Design workspace and adds its virtual Design · Page beneath the producing terminal in the Ensemble row; Preview alone does not arm it. Compare may expose only the exact immutable verified parent/current revision pair; never label a staged or incomplete capture verified. Remote selection pulses are one-shot collaboration receipts, not authority or ambient decoration. For large direction sets, consolidate compatible options into one source-backed comparison board/gallery and one card; use separate files/cards only when independent editing is explicitly requested or technically necessary. Do not duplicate the card renders as inline PNGs. Use inline PNG evidence only when no interactive Design artifact is available.",
|
|
194
194
|
"impact": [
|
|
195
195
|
{"path": "src/pages/code/design/"},
|
|
196
196
|
{"path": "src/pages/code/structured.jsx", "diffPattern": "Edit in Design|openMockup|tp-mockup-view|sourceKnown"},
|
|
@@ -203,6 +203,8 @@
|
|
|
203
203
|
{"path": "src/pages/code/room.jsx", "pattern": "designLaneSelected"},
|
|
204
204
|
{"path": "src/pages/code/design/DesignViewer.jsx", "pattern": "createPortal\\(surface, laneHost\\)"},
|
|
205
205
|
{"path": "src/pages/code/design/DesignViewer.jsx", "pattern": "Apply \\${pendingEditCount}"},
|
|
206
|
+
{"path": "src/pages/code/design/workspace.js", "pattern": "resolveVerifiedDesignComparison"},
|
|
207
|
+
{"path": "src/pages/code/design/visual-runtime.js", "pattern": "DESIGN_VISUAL_RUNTIME_CONTRACT"},
|
|
206
208
|
{"path": "src/pages/code/design/queue.js", "pattern": "designBatchPayload"},
|
|
207
209
|
{"path": "bridge/design-edit.mjs", "pattern": "validateDesignBatchRequest"}
|
|
208
210
|
]
|