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/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": 18,
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": 7,
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
  ]