@gotcos/glasses-server 6.61.6 → 6.62.1
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/.env.example +6 -0
- package/CHANGELOG.md +41 -0
- package/README.md +5 -1
- package/bin/cli.cjs +50 -3
- package/bin/hooks/cos-cursor-observer.cjs +36 -0
- package/bin/hooks/cos-session-hook +46 -11
- package/docs/provider-parity.md +43 -0
- package/managed-runtime-contract.json +1 -0
- package/package.json +5 -2
- package/server/index.ts +135 -16
- package/server/lib/agent-session-binding-store.ts +9 -3
- package/server/lib/agent-session-store.ts +7 -4
- package/server/lib/attached-provider-adapter.ts +158 -45
- package/server/lib/banned-permission-args.ts +42 -1
- package/server/lib/claude-bridge.ts +17 -8
- package/server/lib/claude-hooks-installer.ts +28 -5
- package/server/lib/codex-bridge.ts +17 -2
- package/server/lib/codex-hooks-installer.ts +538 -0
- package/server/lib/codex-live-queue.ts +1 -1
- package/server/lib/codex-model-catalog.ts +49 -2
- package/server/lib/codex-rollout-state.ts +90 -0
- package/server/lib/codex-session-posture.ts +528 -0
- package/server/lib/codex-subagent-activity.ts +119 -0
- package/server/lib/continue-plan.ts +180 -0
- package/server/lib/continue-read-health.ts +26 -0
- package/server/lib/cursor-bridge.ts +26 -6
- package/server/lib/cursor-fork-context.ts +140 -0
- package/server/lib/cursor-model-catalog.ts +62 -5
- package/server/lib/cursor-observer-installer.ts +79 -0
- package/server/lib/cursor-session-model.ts +339 -0
- package/server/lib/cursor-spawn-env.ts +210 -0
- package/server/lib/fork-thread.ts +143 -25
- package/server/lib/health-static-probes.ts +21 -11
- package/server/lib/lens-gist-engines.ts +15 -4
- package/server/lib/lens-gist.ts +1 -1
- package/server/lib/live-cues-cursor.ts +16 -6
- package/server/lib/occupancy-probes.ts +45 -1
- package/server/lib/permission-broker.ts +24 -9
- package/server/lib/provider-actions.ts +149 -0
- package/server/lib/provider-failure-text.ts +27 -0
- package/server/lib/provider-observe.ts +212 -0
- package/server/lib/provider-proof.ts +23 -14
- package/server/lib/query-job-runtime.ts +16 -1
- package/server/lib/query-job-types.ts +2 -1
- package/server/lib/session-act-fields.ts +61 -0
- package/server/lib/session-cancel.ts +95 -4
- package/server/lib/session-continue-facts-real.ts +74 -0
- package/server/lib/session-continue-facts.ts +267 -0
- package/server/lib/session-halt.ts +67 -6
- package/server/lib/session-hook-events.ts +60 -0
- package/server/lib/session-hook-ledger.ts +21 -3
- package/server/lib/session-hook-spool.ts +68 -6
- package/server/lib/session-hooks-runtime.ts +40 -3
- package/server/lib/session-peer-inbox-deps.ts +7 -6
- package/server/lib/session-recommendation.ts +3 -2
- package/server/lib/session-signal-store.ts +81 -3
- package/server/lib/session-stream-events.ts +32 -1
- package/server/lib/session-transcript-watcher.ts +25 -1
- package/server/lib/session-work-observation.ts +46 -0
- package/server/lib/thread-attach-capability.ts +15 -1
- package/server/lib/thread-drain-kick.ts +4 -2
- package/server/lib/thread-occupancy.ts +44 -0
- package/server/lib/thread-turn-queue-deliver.ts +6 -1
- package/server/lib/thread-turn-queue.ts +6 -0
- package/server/lib/turn-request.ts +274 -0
- package/server/routes/agent-session-bindings.ts +224 -22
- package/server/routes/agent-session-stream.ts +26 -7
- package/server/routes/agent-sessions.ts +163 -10
- package/server/routes/health.ts +27 -2
- package/server/routes/session-hooks.ts +60 -3
- package/server/routes/thread-turn-queue.ts +37 -9
- package/server/scripts/hooks-cli.ts +38 -9
package/server/index.ts
CHANGED
|
@@ -24,9 +24,10 @@ import { agentSessionStreamRouter } from './routes/agent-session-stream.js'
|
|
|
24
24
|
import { createAttachedTurnStream } from './lib/session-stream-producer.js'
|
|
25
25
|
import { claudeSessionsRouter } from './routes/claude-sessions.js'
|
|
26
26
|
import { createSessionHooksRouter } from './routes/session-hooks.js'
|
|
27
|
-
import { cachedHookStatus, claudeDeskRunning, onSessionRowEnded, deskIdleSeconds, deskTurnEndedAt, haltHandedOffTurn, registerDrainKickStats, registryIdleAfterStop, sessionHooksEnabled, sessionSignalStore, signalFor, startSessionHooksRuntime } from './lib/session-hooks-runtime.js'
|
|
28
|
-
import { writeHaltMarker } from './lib/session-halt.js'
|
|
29
|
-
import {
|
|
27
|
+
import { cachedHookStatus, claudeDeskRunning, onSessionRowEnded, deskIdleSeconds, deskTurnEndedAt, haltHandedOffTurn, providerHookSeen, registerDrainKickStats, registryIdleAfterStop, sessionHooksEnabled, sessionSignalStore, signalFor, startSessionHooksRuntime } from './lib/session-hooks-runtime.js'
|
|
28
|
+
import { clearingHaltOnSpawn, writeHaltMarker } from './lib/session-halt.js'
|
|
29
|
+
import { startProviderObserveRefresh } from './lib/provider-observe.js'
|
|
30
|
+
import { appendSessionCancelLedger, cancelHoldUntil as cancelHoldUntilFor, noteThreadCancelled, setDeskCancelReaders, threadCancel } from './lib/session-cancel.js'
|
|
30
31
|
import { makeQueueTurnEvidence } from './lib/queue-turn-evidence.js'
|
|
31
32
|
import {
|
|
32
33
|
PermissionBroker,
|
|
@@ -55,6 +56,14 @@ import { readCodexTurnEndedAtMs } from './lib/codex-turn-clock.js'
|
|
|
55
56
|
import { codexLiveQueueEnabled } from './lib/codex-live-queue.js'
|
|
56
57
|
import { realAttachedWorkspaceDeps, resolveAttachedWorkspace } from './lib/attached-workspace.js'
|
|
57
58
|
import { deliverAttachedTurn, realAttachedTurnDeps } from './lib/attached-provider-adapter.js'
|
|
59
|
+
import { resolveSessionContinueFacts } from './lib/session-continue-facts.js'
|
|
60
|
+
import { fallbackContinueNote } from './lib/continue-plan.js'
|
|
61
|
+
import { codexFolderTrustedNow, realContinueFactsDeps } from './lib/session-continue-facts-real.js'
|
|
62
|
+
import { prepareCursorFork, resolveCursorForkWorkspace } from './lib/cursor-fork-context.js'
|
|
63
|
+
import { readComposerFacts } from './lib/cursor-session-model.js'
|
|
64
|
+
import { MAX_PROMPT_CHARS } from './lib/attached-provider-adapter.js'
|
|
65
|
+
import { readRecentSessionTurns } from './lib/agent-session-turns.js'
|
|
66
|
+
import { parseAgentSession } from './lib/agent-session-store.js'
|
|
58
67
|
import { makeLiveTurnDeliverer } from './lib/session-peer-inbox-deps.js'
|
|
59
68
|
import { makeCodexLiveDeliverer } from './lib/codex-live-queue-deps.js'
|
|
60
69
|
import { forkThread, realForkDeps } from './lib/fork-thread.js'
|
|
@@ -105,6 +114,7 @@ import { skillsRouter } from './routes/skills.js'
|
|
|
105
114
|
import { shutdownLiveCues } from './lib/live-cues-engine.js'
|
|
106
115
|
import { prewarmContext } from './lib/context-builder.js'
|
|
107
116
|
import { preWarmCLI } from './lib/claude-bridge.js'
|
|
117
|
+
import { sweepStaleTurnRequests } from './lib/turn-request.js'
|
|
108
118
|
import { getCodexRunConfig } from './lib/codex-run-ledger.js'
|
|
109
119
|
import {
|
|
110
120
|
startCodexModelCatalogRefresh,
|
|
@@ -166,7 +176,7 @@ const app = express()
|
|
|
166
176
|
import { createThreadTurnQueueRouter, drainAllThreads } from './routes/thread-turn-queue.js'
|
|
167
177
|
import { createCursorStopFollowupRouter } from './routes/cursor-stop-followup.js'
|
|
168
178
|
import { queuedThreadKeys, readQueue, transcriptTurnVerdict, writeQueue } from './lib/thread-turn-queue-store.js'
|
|
169
|
-
import { hookHaltReady, readHookToken } from './lib/claude-hooks-installer.js'
|
|
179
|
+
import { cursorDeskHaltReady, hookHaltReady, readHookToken } from './lib/claude-hooks-installer.js'
|
|
170
180
|
import { OPEN_TURN_CEILING_MS } from './lib/session-state-derive.js'
|
|
171
181
|
import { createDrainKick, kickPlanFor } from './lib/thread-drain-kick.js'
|
|
172
182
|
import { transcriptPathFor } from './lib/native-head.js'
|
|
@@ -461,6 +471,8 @@ bindingReapTimer.unref()
|
|
|
461
471
|
const occupancyDirs = realOccupancyDirs()
|
|
462
472
|
const nativeHeadDeps = realNativeHeadDeps()
|
|
463
473
|
const attachedWorkspaceDeps = realAttachedWorkspaceDeps(nativeHeadDeps)
|
|
474
|
+
// 6.62.0: one set of readers for the Continue plan, rows, detail and the attach verdict.
|
|
475
|
+
const continueFactsDeps = realContinueFactsDeps()
|
|
464
476
|
/**
|
|
465
477
|
* THE ONE PLACE the idle-holder relaxation is switched on, on the ONE flag
|
|
466
478
|
* that also registers the write routes (6.33.0).
|
|
@@ -507,6 +519,15 @@ const occupancyProbes = withCursorComposerTurn(withCodexTurnClock(sessionHooksEn
|
|
|
507
519
|
// before any router is registered so the first list request already sees the replayed
|
|
508
520
|
// ledger. With COS_SESSION_HOOKS off it still drains and stamps the spool (see the module).
|
|
509
521
|
const sessionHooksRuntime = startSessionHooksRuntime({ port: PORT })
|
|
522
|
+
// 6.62.0: the Codex hooks, their trust and the Cursor observer, read in the background for
|
|
523
|
+
// health and the desk cancel (never on a request path; see lib/provider-observe.ts).
|
|
524
|
+
startProviderObserveRefresh()
|
|
525
|
+
// 6.62.0 (QA W1, W2): a desk Cursor cancel needs the CURRENT script, and a Codex or Cursor one
|
|
526
|
+
// needs that engine's hooks already seen for the thread (see lib/session-cancel.ts).
|
|
527
|
+
setDeskCancelReaders({
|
|
528
|
+
cursorReady: () => cursorDeskHaltReady(cachedHookStatus()),
|
|
529
|
+
threadHookSeen: (provider, threadId) => providerHookSeen(provider, threadId),
|
|
530
|
+
})
|
|
510
531
|
|
|
511
532
|
/**
|
|
512
533
|
* The shim between the route's request shape and the adapter's.
|
|
@@ -540,6 +561,8 @@ const deliverAttachedTurnForRoute = async (request: {
|
|
|
540
561
|
onSpawn: (pid: number) => boolean
|
|
541
562
|
/** 6.53.0: the cancel from the lens, passed straight through to the adapter. */
|
|
542
563
|
abortSignal?: AbortSignal
|
|
564
|
+
/** 6.62.0 /qa (Q3): the client shows `continue_note`; only then Cursor Run Everything. */
|
|
565
|
+
continueNoteAck?: boolean
|
|
543
566
|
}): Promise<unknown> => {
|
|
544
567
|
// Resolved here, not stored on the binding: the cwd is read from the transcript,
|
|
545
568
|
// which records it verbatim. Decoding the project slug is lossy — this Mac's own
|
|
@@ -568,6 +591,27 @@ const deliverAttachedTurnForRoute = async (request: {
|
|
|
568
591
|
// turn sees `working` rather than silence, and closed in a `finally` because the
|
|
569
592
|
// duplicate-suppression gate it holds must be released on EVERY exit path -- a stuck
|
|
570
593
|
// gate would silence Phase 2 for that session permanently.
|
|
594
|
+
// 6.62.0 (D1): what this Continue runs with. Claude: no plan, unchanged. Codex: the
|
|
595
|
+
// session's own sandbox, model and effort. Cursor: Run Everything on its own model. A
|
|
596
|
+
// read that fails plans the 6.61 posture with a note; never anything broader. With
|
|
597
|
+
// COS_CONTINUE_FULL_PERMISSIONS=0 no plan is resolved and the adapter builds the 6.61 argv.
|
|
598
|
+
let plan: unknown
|
|
599
|
+
if (request.provider !== 'claude') {
|
|
600
|
+
try {
|
|
601
|
+
const facts = await resolveSessionContinueFacts({
|
|
602
|
+
provider: request.provider,
|
|
603
|
+
threadId: request.nativeThreadId,
|
|
604
|
+
transcriptPath: attachedWorkspaceDeps.transcriptPath(request.provider, request.nativeThreadId),
|
|
605
|
+
cwd: workspace.path,
|
|
606
|
+
noteAck: request.continueNoteAck === true,
|
|
607
|
+
}, continueFactsDeps)
|
|
608
|
+
plan = facts.plan
|
|
609
|
+
} catch (error) {
|
|
610
|
+
console.error(`[attached] continue plan failed provider=${request.provider}: ${error instanceof Error ? error.message : error}`)
|
|
611
|
+
plan = undefined
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
571
615
|
const live = createAttachedTurnStream({
|
|
572
616
|
provider: request.provider,
|
|
573
617
|
sessionId: request.nativeThreadId,
|
|
@@ -579,12 +623,18 @@ const deliverAttachedTurnForRoute = async (request: {
|
|
|
579
623
|
nativeThreadId: request.nativeThreadId,
|
|
580
624
|
prompt: request.prompt,
|
|
581
625
|
cwd: workspace.path,
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
|
|
626
|
+
// 6.62.0: the session's own posture, carried by `plan`. The adapter refuses any other
|
|
627
|
+
// policy word, validates the plan, and still bans every bypass flag but the one the
|
|
628
|
+
// plan's allowance names, at both of its checks (plan 3.1).
|
|
629
|
+
policy: 'session_posture',
|
|
630
|
+
plan,
|
|
585
631
|
abortSignal: request.abortSignal,
|
|
586
632
|
deps: {
|
|
587
|
-
|
|
633
|
+
// 6.62.0 /qa (Q4): the child is about to start, every gate passed (a Continue or a
|
|
634
|
+
// queue drain): no earlier desk-cancel marker may deny COS's own turn.
|
|
635
|
+
...clearingHaltOnSpawn(base, request.nativeThreadId, 'continue'),
|
|
636
|
+
// 6.62.0 /qa (W5): full access is re-checked against Codex's own trust at the spawn.
|
|
637
|
+
codexFolderTrusted: cwd => codexFolderTrustedNow(cwd),
|
|
588
638
|
// `startMs` is deliberately unused: the adapter already probed it as a GATE
|
|
589
639
|
// (a null there aborts before this is reached), and the route probes again
|
|
590
640
|
// as the recorder. One record, one authority.
|
|
@@ -630,16 +680,55 @@ const deliverAttachedTurnForRoute = async (request: {
|
|
|
630
680
|
* ever report `unverified`, and it would report that honestly rather than
|
|
631
681
|
* claiming a verification it never performed.
|
|
632
682
|
*/
|
|
633
|
-
const forkThreadForRoute = (request: {
|
|
634
|
-
provider: 'claude' | 'codex'
|
|
683
|
+
const forkThreadForRoute = async (request: {
|
|
684
|
+
provider: 'claude' | 'codex' | 'cursor'
|
|
635
685
|
nativeThreadId: string
|
|
636
686
|
prompt: string
|
|
637
687
|
cwd: string
|
|
638
688
|
policy: 'read_only'
|
|
639
|
-
}): Promise<unknown> =>
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
689
|
+
}): Promise<unknown> => {
|
|
690
|
+
// 6.62.0 (D4, plan 3.7): Cursor has no native fork, so its "fork" is a NEW read-only chat
|
|
691
|
+
// whose first prompt carries a bounded bundle of the source, then the message, on the
|
|
692
|
+
// session's own model. Claude and Codex are unchanged.
|
|
693
|
+
let prompt = request.prompt
|
|
694
|
+
let cursorModel: string | null = null
|
|
695
|
+
if (request.provider === 'cursor') {
|
|
696
|
+
const prepared = await prepareCursorFork(request.nativeThreadId, request.prompt, {
|
|
697
|
+
transcriptPath: threadId => attachedWorkspaceDeps.transcriptPath('cursor', threadId),
|
|
698
|
+
readTurns: async (path, limit) => (await readRecentSessionTurns('cursor', path, limit)).turns,
|
|
699
|
+
readTitle: async path => {
|
|
700
|
+
const parsed = await parseAgentSession('cursor', path)
|
|
701
|
+
return { title: parsed.display_label || null, firstPrompt: parsed.first_prompt || null }
|
|
702
|
+
},
|
|
703
|
+
sessionModel: async (threadId, path) => {
|
|
704
|
+
const facts = await resolveSessionContinueFacts({ provider: 'cursor', threadId, transcriptPath: path, cwd: request.cwd }, continueFactsDeps)
|
|
705
|
+
if (facts.plan?.provider === 'cursor' && facts.plan.matched) return facts.plan.model
|
|
706
|
+
return facts.reportedModel && continueFactsDeps.isKnownCursorModel(facts.reportedModel) ? facts.reportedModel : null
|
|
707
|
+
},
|
|
708
|
+
maxChars: MAX_PROMPT_CHARS,
|
|
709
|
+
})
|
|
710
|
+
prompt = prepared.prompt
|
|
711
|
+
cursorModel = prepared.cursorModel
|
|
712
|
+
}
|
|
713
|
+
return forkThread({
|
|
714
|
+
...request,
|
|
715
|
+
prompt,
|
|
716
|
+
...(cursorModel ? { cursorModel } : {}),
|
|
717
|
+
deps: forkDepsClearingIdleHalt(request.provider, request.nativeThreadId),
|
|
718
|
+
})
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* 6.62.0 /qa (Q4): the fork deps, with the source thread's halt marker cleared at the spawn,
|
|
723
|
+
* but ONLY when no desk run is working there. A fork has no occupancy gate (it may run while
|
|
724
|
+
* the desk works), and clearing then would undo the person's own cancel.
|
|
725
|
+
*/
|
|
726
|
+
function forkDepsClearingIdleHalt(provider: 'claude' | 'codex' | 'cursor', threadId: string) {
|
|
727
|
+
return clearingHaltOnSpawn(realForkDeps((p, id) => nativeHead(p, id, nativeHeadDeps)), threadId, 'fork', () => {
|
|
728
|
+
const reason = threadOccupancy(provider, threadId, occupancyProbes, occupancyDirs).reason
|
|
729
|
+
return reason !== 'native_thread_working' && reason !== 'live_desktop_process'
|
|
730
|
+
})
|
|
731
|
+
}
|
|
643
732
|
|
|
644
733
|
// API routes
|
|
645
734
|
app.use('/api', healthRouter)
|
|
@@ -802,6 +891,8 @@ if (threadAttachEnabled()) {
|
|
|
802
891
|
deliver: (turn: QueuedThreadTurn) => deliverQueuedTurnOverLoopback(turn, PORT, API_TOKEN),
|
|
803
892
|
// 6.53.0: parked turns hold for two minutes after a cancel on their thread.
|
|
804
893
|
cancelHoldUntil: (provider: string, threadId: string) => cancelHoldUntilFor(provider, threadId),
|
|
894
|
+
// 6.62.0 (W14): a busy Agent CLI chat's parked turn drains here (no Stop hook fires for `-p`).
|
|
895
|
+
cursorCliChat: (threadId: string) => continueFactsDeps.cursorChatDir(threadId) !== null,
|
|
805
896
|
now: () => Date.now(),
|
|
806
897
|
}
|
|
807
898
|
app.use('/api', createThreadTurnQueueRouter(queueDeps))
|
|
@@ -882,13 +973,36 @@ app.use('/api', createAgentSessionBindingsRouter({
|
|
|
882
973
|
// (`codex queue`), never into a resume child that cannot open a held thread.
|
|
883
974
|
// `COS_CODEX_LIVE_QUEUE=0` restores the 6.50 path.
|
|
884
975
|
deliverCodexLiveTurn: makeCodexLiveDeliverer(),
|
|
976
|
+
// 6.62.0 (plan 3.6, W8): a BUSY thread the Codex app holds takes the same hop, but only
|
|
977
|
+
// while the COS queue holds nothing for it, so a new turn never jumps a parked one. The
|
|
978
|
+
// turn route's clientTurnId ledger is the one idempotency key per turn.
|
|
979
|
+
busyCodexHop: (threadId: string) => codexLiveQueueEnabled()
|
|
980
|
+
&& readQueue('codex', threadId, Date.now()).every(turn => turn.status !== 'waiting' && turn.status !== 'delivering'),
|
|
981
|
+
// 6.62.0 (plan 3.12): the attach verdict says what a Continue would run with.
|
|
982
|
+
continueNote: async (provider, threadId, options) => (await resolveSessionContinueFacts({
|
|
983
|
+
provider,
|
|
984
|
+
threadId,
|
|
985
|
+
transcriptPath: attachedWorkspaceDeps.transcriptPath(provider, threadId),
|
|
986
|
+
cwd: resolveAttachedWorkspace(provider, threadId, attachedWorkspaceDeps)?.path ?? null,
|
|
987
|
+
noteAck: options?.noteAck === true,
|
|
988
|
+
}, continueFactsDeps)).note,
|
|
989
|
+
// 6.62.0 /qa (W9): the fixed note when the read misses its budget.
|
|
990
|
+
continueNoteFallback: (provider, options) => fallbackContinueNote(provider, { noteAck: options.noteAck, codexFallbackSandbox: continueFactsDeps.codexFallbackSandbox() }),
|
|
885
991
|
forkThread: forkThreadForRoute,
|
|
886
992
|
// The fork's real spawn directory. Separate from `resolveTarget` above, which
|
|
887
993
|
// deliberately yields only fingerprints because plan 3.3 keeps a filesystem path
|
|
888
994
|
// off anything client-visible. Null refuses: never fall back to the server's own
|
|
889
995
|
// working directory, which is wherever the LaunchAgent happened to start.
|
|
890
|
-
|
|
891
|
-
|
|
996
|
+
// 6.62.0 (W9): a Cursor IDE composer has no spawn spelling; its fork runs in the hook
|
|
997
|
+
// signal's cwd, else the composer's own recorded folder, else the CLI chat's spelling.
|
|
998
|
+
resolveForkWorkspace: (provider, threadId) => provider === 'cursor'
|
|
999
|
+
? resolveCursorForkWorkspace(threadId, {
|
|
1000
|
+
signalCwd: id => signalFor(id)?.cwd ?? null,
|
|
1001
|
+
composerWorkspace: async id => (await readComposerFacts([id], continueFactsDeps.cursorComposerDb)).get(id)?.workspace ?? null,
|
|
1002
|
+
spawnWorkspace: id => attachedWorkspaceDeps.cursorSpawnWorkspace?.(id) ?? null,
|
|
1003
|
+
dirExists: path => attachedWorkspaceDeps.dirExists(path),
|
|
1004
|
+
})
|
|
1005
|
+
: resolveAttachedWorkspace(provider, threadId, attachedWorkspaceDeps)?.path ?? null,
|
|
892
1006
|
// One instance per process. The epoch high-water mark is only monotonic if a
|
|
893
1007
|
// single reader owns the durable store, so this must never be constructed twice.
|
|
894
1008
|
// `open()` never throws: an unreadable store yields a DEGRADED registry whose
|
|
@@ -1227,6 +1341,11 @@ listenRequiredServers(listeners).then(() => {
|
|
|
1227
1341
|
})
|
|
1228
1342
|
.catch(error => console.warn('[meeting-merge] pending re-drive failed', error))
|
|
1229
1343
|
|
|
1344
|
+
// Turn-request files (lib/turn-request.ts) carry a user's words for the COS memory hook
|
|
1345
|
+
// and die with their child. One left by a crash or a kill must not outlive an hour.
|
|
1346
|
+
const staleTurnRequests = sweepStaleTurnRequests()
|
|
1347
|
+
if (staleTurnRequests > 0) console.log(`[turn-request] removed ${staleTurnRequests} stale file(s)`)
|
|
1348
|
+
|
|
1230
1349
|
void initQueryJobRuntime().then(health => {
|
|
1231
1350
|
if (process.env.COS_DURABLE_QUERY_JOBS !== '0') {
|
|
1232
1351
|
console.log(`[COS API] Durable query jobs: ${health.store.state} · ${health.store.retainedIdentities} retained · ${health.store.requeuedOnBoot ?? 0} requeued from the last boot`)
|
|
@@ -39,12 +39,18 @@ import type { AgentProvider } from './agent-session-store.js'
|
|
|
39
39
|
|
|
40
40
|
export type BindingState = 'staging' | 'active' | 'detaching' | 'detached'
|
|
41
41
|
|
|
42
|
-
/** Providers that can carry a Continue binding.
|
|
42
|
+
/** Providers that can carry a Continue binding. */
|
|
43
43
|
export const BINDABLE_PROVIDERS = ['claude', 'codex', 'cursor'] as const
|
|
44
44
|
export type BindableProvider = (typeof BINDABLE_PROVIDERS)[number]
|
|
45
45
|
|
|
46
|
-
/**
|
|
47
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Providers the fork route serves. 6.62.0 (D4, plan 3.7): Cursor Agent has no
|
|
48
|
+
* `--fork-session`, so a Cursor "fork" is a NEW read-only chat seeded with a bounded
|
|
49
|
+
* context bundle ("New session with this context"). Work's own fork list
|
|
50
|
+
* (`work-activity.ts` FORK_PROVIDERS) and the Jev recommender stay Claude and Codex until
|
|
51
|
+
* the Control release (W9).
|
|
52
|
+
*/
|
|
53
|
+
export const FORKABLE_PROVIDERS = ['claude', 'codex', 'cursor'] as const
|
|
48
54
|
export type ForkableProvider = (typeof FORKABLE_PROVIDERS)[number]
|
|
49
55
|
|
|
50
56
|
export interface NativeBinding {
|
|
@@ -1270,10 +1270,12 @@ export async function listCursorSessions(
|
|
|
1270
1270
|
const candidate = candidates[i]
|
|
1271
1271
|
const peek = await peekCursorDiscussion(candidate.file)
|
|
1272
1272
|
const firstPrompt = await firstCursorUserTitle(candidate.file) ?? peek.lastUser ?? ''
|
|
1273
|
+
// 6.62.0 (plan 1.6): the composer's name, else the FIRST query, as the detail titles it.
|
|
1274
|
+
// The newest query is what changes turn to turn; a title that moved with every prompt
|
|
1275
|
+
// never named the session.
|
|
1273
1276
|
const title = composerNames.get(candidate.sessionDir)
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
?? 'Cursor session'
|
|
1277
|
+
|| firstPrompt
|
|
1278
|
+
|| 'Cursor session'
|
|
1277
1279
|
if (isKeepWarmSessionTitle(title)) continue
|
|
1278
1280
|
const alive = now.getTime() - candidate.mtimeMs < 180_000
|
|
1279
1281
|
rows.push(row({
|
|
@@ -1835,7 +1837,8 @@ export async function parseAgentSession(
|
|
|
1835
1837
|
const query = cursorUserTitle(text, true) ?? cursorUserTitle(text, false)
|
|
1836
1838
|
collectTurn(query ?? text, tail)
|
|
1837
1839
|
if (query) {
|
|
1838
|
-
|
|
1840
|
+
// 6.62.0 (plan 1.6): the FIRST query titles the session, as on its list row.
|
|
1841
|
+
if (!title) title = query
|
|
1839
1842
|
if (!firstPrompt) firstPrompt = query
|
|
1840
1843
|
}
|
|
1841
1844
|
} else {
|
|
@@ -90,6 +90,7 @@
|
|
|
90
90
|
// process and lets a RECYCLED pid inherit our self-ownership claim, which
|
|
91
91
|
// is the one input that can turn a live foreign owner into `attachable`.
|
|
92
92
|
|
|
93
|
+
import { PROVIDER_QUOTA_RE } from './provider-failure-text.js'
|
|
93
94
|
import { accessSync, constants as fsConstants, statSync } from 'node:fs'
|
|
94
95
|
import { delimiter, isAbsolute, join } from 'node:path'
|
|
95
96
|
import { homedir } from 'node:os'
|
|
@@ -101,19 +102,30 @@ import { recordCosSpawn, releaseCosSpawn } from './agent-session-ownership-store
|
|
|
101
102
|
import { interpretPsLstart, processStartMs as realProcessStartMs } from './occupancy-probes.js'
|
|
102
103
|
import { getCodexTrustMode } from './codex-run-ledger.js'
|
|
103
104
|
import { CURSOR_SLOT_MODEL_IDS } from './cursor-model-catalog.js'
|
|
105
|
+
import { cursorIsolationUnavailableError, cursorSpawnEnv, isCursorIsolationUnavailable, releaseCursorSpawnOnExit } from './cursor-spawn-env.js'
|
|
106
|
+
import type { PermissionAllowance } from './banned-permission-args.js'
|
|
107
|
+
import {
|
|
108
|
+
continueAllowanceFor,
|
|
109
|
+
validateContinuePlan,
|
|
110
|
+
type AttachedContinuePlan,
|
|
111
|
+
type CodexContinuePlan,
|
|
112
|
+
type CursorContinuePlan,
|
|
113
|
+
} from './continue-plan.js'
|
|
104
114
|
|
|
105
115
|
/** Providers with a certified attached path. Cursor is bindable, not forkable. */
|
|
106
116
|
export type AttachedProvider = BindableProvider
|
|
107
117
|
|
|
108
118
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
119
|
+
* 6.62.0 (D1): a Continue runs with the SESSION'S OWN posture, described by the request's
|
|
120
|
+
* `plan` (`continue-plan.ts`). Until 6.61 this was `'read_only'`, a name that had stopped
|
|
121
|
+
* being true for Claude on 2026-08-16. The type still has one member on purpose: `agent`
|
|
122
|
+
* and anything else is a value this module refuses, and the plan, not the policy word,
|
|
123
|
+
* is what may ever widen the argv.
|
|
112
124
|
*/
|
|
113
|
-
export type AttachedPermissionPolicy = '
|
|
125
|
+
export type AttachedPermissionPolicy = 'session_posture'
|
|
114
126
|
|
|
115
127
|
export function isAttachedPermissionPolicy(value: unknown): value is AttachedPermissionPolicy {
|
|
116
|
-
return value === '
|
|
128
|
+
return value === 'session_posture'
|
|
117
129
|
}
|
|
118
130
|
|
|
119
131
|
/** Terminal reason on failure. Never carries prompt or transcript text. */
|
|
@@ -284,6 +296,11 @@ export interface AttachedSpawnRequest {
|
|
|
284
296
|
args: readonly string[]
|
|
285
297
|
cwd: string
|
|
286
298
|
env: NodeJS.ProcessEnv
|
|
299
|
+
/**
|
|
300
|
+
* 6.62.0: which CLI this is, so the real spawn can isolate a Cursor child's config dir
|
|
301
|
+
* (`cursor-spawn-env.ts`). Optional so every test double keeps its shape.
|
|
302
|
+
*/
|
|
303
|
+
provider?: AttachedProvider
|
|
287
304
|
}
|
|
288
305
|
|
|
289
306
|
/** Result of the final occupancy re-check (plan 4.3 step 6). */
|
|
@@ -341,7 +358,7 @@ export interface AttachedTurnDeps {
|
|
|
341
358
|
* Exists ONLY so a test can hand back an argv carrying a banned permission flag
|
|
342
359
|
* and prove the turn is refused with zero spawns. Production never sets it.
|
|
343
360
|
*/
|
|
344
|
-
buildArgs?: (provider: AttachedProvider, nativeThreadId: string, cwd: string) => string[]
|
|
361
|
+
buildArgs?: (provider: AttachedProvider, nativeThreadId: string, cwd: string, plan?: AttachedContinuePlan) => string[]
|
|
345
362
|
/**
|
|
346
363
|
* A PASSIVE reader of the child's stdout. Optional; omitted changes nothing.
|
|
347
364
|
*
|
|
@@ -361,6 +378,13 @@ export interface AttachedTurnDeps {
|
|
|
361
378
|
* future edit can let it modify what the scan sees.
|
|
362
379
|
*/
|
|
363
380
|
observeStdout?: (chunk: string) => void
|
|
381
|
+
/**
|
|
382
|
+
* 6.62.0 /qa (W5, N1): does Codex itself trust this cwd RIGHT NOW (the cwd or its git
|
|
383
|
+
* root is a trusted `[projects]` entry)? Asked at the spawn for a full-access plan only;
|
|
384
|
+
* absent, false, or a throw refuses that plan. The plan was built from the same rule
|
|
385
|
+
* seconds earlier; this is the second check, so the allowance never outlives the trust.
|
|
386
|
+
*/
|
|
387
|
+
codexFolderTrusted?: (cwd: string) => boolean
|
|
364
388
|
}
|
|
365
389
|
|
|
366
390
|
export interface AttachedTurnRequest {
|
|
@@ -369,6 +393,12 @@ export interface AttachedTurnRequest {
|
|
|
369
393
|
prompt: unknown
|
|
370
394
|
cwd: unknown
|
|
371
395
|
policy: unknown
|
|
396
|
+
/**
|
|
397
|
+
* 6.62.0: what this Continue runs with (`continue-plan.ts`). Absent: the 6.61 argv for
|
|
398
|
+
* every provider. Validated here, structurally, before any argv is built; a plan for
|
|
399
|
+
* another provider or with an unusable value refuses the turn rather than being ignored.
|
|
400
|
+
*/
|
|
401
|
+
plan?: unknown
|
|
372
402
|
deps: AttachedTurnDeps
|
|
373
403
|
/** Wall-clock budget for the provider run. Omitted uses the default. */
|
|
374
404
|
timeoutMs?: number
|
|
@@ -506,7 +536,13 @@ export function buildClaudeAttachedArgs(nativeThreadId: string): string[] {
|
|
|
506
536
|
}
|
|
507
537
|
|
|
508
538
|
/**
|
|
509
|
-
* Codex: `codex exec -
|
|
539
|
+
* Codex: `codex exec [-m <model>] [-c model_reasoning_effort=…] --sandbox <s> [-c
|
|
540
|
+
* sandbox_workspace_write.network_access=…] --cd <cwd> resume --json <id> -`.
|
|
541
|
+
*
|
|
542
|
+
* 6.62.0 (D1, canary C11): with a plan, the session's OWN model, effort and sandbox are
|
|
543
|
+
* passed, because `exec resume` otherwise takes the config defaults for all three. Every
|
|
544
|
+
* one of them is an option of `exec` and precedes `resume`. With no plan (the kill switch,
|
|
545
|
+
* or an older caller) the argv is the 6.61 one, byte for byte.
|
|
510
546
|
*
|
|
511
547
|
* Verified against codex-cli 0.148.0-alpha.9 on 2026-08-15:
|
|
512
548
|
* `codex exec resume [OPTIONS] [SESSION_ID] [PROMPT]`. `--sandbox` and `--cd`
|
|
@@ -518,33 +554,61 @@ export function buildClaudeAttachedArgs(nativeThreadId: string): string[] {
|
|
|
518
554
|
* `--ephemeral` is deliberately absent: the whole point is that the turn
|
|
519
555
|
* persists into the user's rollout.
|
|
520
556
|
*/
|
|
521
|
-
export function buildCodexAttachedArgs(nativeThreadId: string, cwd: string): string[] {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
557
|
+
export function buildCodexAttachedArgs(nativeThreadId: string, cwd: string, plan?: CodexContinuePlan): string[] {
|
|
558
|
+
if (!plan) {
|
|
559
|
+
return [
|
|
560
|
+
'exec',
|
|
561
|
+
// The posture ordinary Codex runs use on this host, not a stricter one
|
|
562
|
+
// invented here. `COS_CODEX_SANDBOX=workspace-write` opts in; absent stays
|
|
563
|
+
// read-only, so this is never MORE permissive than the rest of the server.
|
|
564
|
+
'--sandbox', getCodexTrustMode(),
|
|
565
|
+
'--cd', cwd,
|
|
566
|
+
'resume',
|
|
567
|
+
'--json',
|
|
568
|
+
'--skip-git-repo-check',
|
|
569
|
+
nativeThreadId,
|
|
570
|
+
'-',
|
|
571
|
+
]
|
|
572
|
+
}
|
|
573
|
+
const args = ['exec']
|
|
574
|
+
if (plan.model) args.push('-m', plan.model)
|
|
575
|
+
if (plan.effort) args.push('-c', `model_reasoning_effort="${plan.effort}"`)
|
|
576
|
+
args.push('--sandbox', plan.sandbox)
|
|
577
|
+
if (plan.sandbox === 'workspace-write' && typeof plan.networkAccess === 'boolean') {
|
|
578
|
+
args.push('-c', `sandbox_workspace_write.network_access=${plan.networkAccess}`)
|
|
579
|
+
}
|
|
580
|
+
args.push('--cd', cwd, 'resume', '--json', '--skip-git-repo-check', nativeThreadId, '-')
|
|
581
|
+
return args
|
|
535
582
|
}
|
|
536
583
|
|
|
537
584
|
/**
|
|
538
|
-
* Cursor Agent CLI:
|
|
585
|
+
* Cursor Agent CLI: resume into an existing thread.
|
|
586
|
+
*
|
|
587
|
+
* 6.62.0 (D1, canaries C4/C5): with a plan, Run Everything (`--force`) on the session's
|
|
588
|
+
* own model. Agent mode alone is not full permissions (C4: the CLI's allowlist still
|
|
589
|
+
* blocked `touch`), and `--resume` without `--model` runs the GLOBAL default (C5). `--force`
|
|
590
|
+
* stays on the ban list: only the plan's allowance lets this one argv slot through, at
|
|
591
|
+
* both checks. With no plan (the kill switch) the argv is the 6.61 ask-mode one.
|
|
539
592
|
*
|
|
540
|
-
*
|
|
541
|
-
* spawn spelling from `spawnWorkspace`, never realpath of `meta.json.cwd`.
|
|
593
|
+
* Workspace is the spawn spelling from `spawnWorkspace`, never realpath of `meta.json.cwd`.
|
|
542
594
|
*/
|
|
543
|
-
export function buildCursorAttachedArgs(nativeThreadId: string, cwd: string): string[] {
|
|
595
|
+
export function buildCursorAttachedArgs(nativeThreadId: string, cwd: string, plan?: CursorContinuePlan): string[] {
|
|
596
|
+
if (!plan) {
|
|
597
|
+
return [
|
|
598
|
+
'-p',
|
|
599
|
+
'--mode', 'ask',
|
|
600
|
+
'--model', CURSOR_SLOT_MODEL_IDS['cursor-composer'],
|
|
601
|
+
'--output-format', 'stream-json',
|
|
602
|
+
'--stream-partial-output',
|
|
603
|
+
'--trust',
|
|
604
|
+
'--workspace', cwd,
|
|
605
|
+
'--resume', nativeThreadId,
|
|
606
|
+
]
|
|
607
|
+
}
|
|
544
608
|
return [
|
|
545
609
|
'-p',
|
|
546
|
-
'--
|
|
547
|
-
'--model',
|
|
610
|
+
'--force',
|
|
611
|
+
'--model', plan.model,
|
|
548
612
|
'--output-format', 'stream-json',
|
|
549
613
|
'--stream-partial-output',
|
|
550
614
|
'--trust',
|
|
@@ -567,13 +631,20 @@ export function buildCursorAttachedArgs(nativeThreadId: string, cwd: string): st
|
|
|
567
631
|
* than returning a sentinel: there is no safe degraded argv, and the caller's
|
|
568
632
|
* outer catch already maps a throw to a terminal refusal before any spawn.
|
|
569
633
|
*/
|
|
570
|
-
function
|
|
634
|
+
export function buildAttachedArgs(
|
|
635
|
+
provider: AttachedProvider,
|
|
636
|
+
nativeThreadId: string,
|
|
637
|
+
cwd: string,
|
|
638
|
+
plan?: AttachedContinuePlan,
|
|
639
|
+
allowance?: PermissionAllowance,
|
|
640
|
+
): string[] {
|
|
571
641
|
const args = provider === 'claude'
|
|
572
642
|
? buildClaudeAttachedArgs(nativeThreadId)
|
|
573
643
|
: provider === 'codex'
|
|
574
|
-
? buildCodexAttachedArgs(nativeThreadId, cwd)
|
|
575
|
-
: buildCursorAttachedArgs(nativeThreadId, cwd)
|
|
576
|
-
|
|
644
|
+
? buildCodexAttachedArgs(nativeThreadId, cwd, plan?.provider === 'codex' ? plan : undefined)
|
|
645
|
+
: buildCursorAttachedArgs(nativeThreadId, cwd, plan?.provider === 'cursor' ? plan : undefined)
|
|
646
|
+
// 6.62.0: the SAME allowance object the spawn boundary below receives (plan 3.1).
|
|
647
|
+
const banned = findBannedPermissionArg(args, allowance)
|
|
577
648
|
if (banned !== null) {
|
|
578
649
|
// The flag name only. Never the argv, which carries the thread id and cwd.
|
|
579
650
|
throw new Error(`attached argv carries a banned permission flag: ${banned}`)
|
|
@@ -644,7 +715,8 @@ export function classifyStderr(text: string): AttachedStderrClass {
|
|
|
644
715
|
return 'thread_not_found'
|
|
645
716
|
}
|
|
646
717
|
if (/unauthor|forbidden|not logged in|sign in|login|auth|credential|token expired/.test(sample)) return 'auth'
|
|
647
|
-
|
|
718
|
+
// 6.62.1: the shared pattern, so "hit your session limit" / "usage limit" class as a limit here too.
|
|
719
|
+
if (/rate limit|too many requests|quota|overloaded/.test(sample) || PROVIDER_QUOTA_RE.test(sample)) return 'rate_limit'
|
|
648
720
|
if (/permission|denied|read-only|sandbox|operation not permitted|eacces/.test(sample)) return 'permission'
|
|
649
721
|
return 'unclassified'
|
|
650
722
|
}
|
|
@@ -757,6 +829,17 @@ async function run(
|
|
|
757
829
|
return fail('unsupported_policy', 'not_attempted', { ...base, durationMs: duration() })
|
|
758
830
|
}
|
|
759
831
|
|
|
832
|
+
// 6.62.0: the plan, validated before anything is built from it. A wrong-provider or
|
|
833
|
+
// malformed plan is a wiring bug, and a wiring bug refuses rather than falling back to
|
|
834
|
+
// a different argv than the caller meant.
|
|
835
|
+
const checkedPlan = validateContinuePlan(provider, request.plan)
|
|
836
|
+
if (checkedPlan === 'invalid') {
|
|
837
|
+
return fail('unsupported_policy', 'not_attempted', { ...base, detail: 'invalid_plan', durationMs: duration() })
|
|
838
|
+
}
|
|
839
|
+
const plan: AttachedContinuePlan | undefined = checkedPlan ?? undefined
|
|
840
|
+
// ONE allowance, built once, handed to the builder AND the spawn-boundary recheck.
|
|
841
|
+
const allowance = continueAllowanceFor(plan)
|
|
842
|
+
|
|
760
843
|
if (typeof request.prompt !== 'string' || request.prompt.length === 0) {
|
|
761
844
|
return fail('invalid_prompt', 'not_attempted', { ...base, detail: 'empty', durationMs: duration() })
|
|
762
845
|
}
|
|
@@ -824,6 +907,21 @@ async function run(
|
|
|
824
907
|
return fail('native_owner_appeared', 'not_attempted', { ...base, detail: reasonText, durationMs: duration() })
|
|
825
908
|
}
|
|
826
909
|
|
|
910
|
+
// 6.62.0 /qa (W5, N1): a full-access Codex plan is re-checked against Codex's own trust at
|
|
911
|
+
// the spawn. A folder that stopped being trusted (or never was) refuses, never downgrades:
|
|
912
|
+
// the note promised full access, and a quieter posture would make the note a lie.
|
|
913
|
+
if (plan?.provider === 'codex' && plan.sandbox === 'danger-full-access') {
|
|
914
|
+
let trusted = false
|
|
915
|
+
try {
|
|
916
|
+
trusted = typeof deps.codexFolderTrusted === 'function' && deps.codexFolderTrusted(cwd) === true
|
|
917
|
+
} catch {
|
|
918
|
+
trusted = false
|
|
919
|
+
}
|
|
920
|
+
if (!trusted) {
|
|
921
|
+
return fail('unsupported_policy', 'not_attempted', { ...base, detail: 'codex_folder_untrusted', durationMs: duration() })
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
827
925
|
// --- 4. Spawn ---------------------------------------------------------------
|
|
828
926
|
// Built through an injectable seam so the banned-flag check below is reachable
|
|
829
927
|
// by a test. Without the seam no test could make the builders emit a banned
|
|
@@ -832,14 +930,16 @@ async function run(
|
|
|
832
930
|
// nowhere.
|
|
833
931
|
let args: string[]
|
|
834
932
|
try {
|
|
835
|
-
args =
|
|
933
|
+
args = deps.buildArgs
|
|
934
|
+
? deps.buildArgs(provider, nativeThreadId, cwd, plan)
|
|
935
|
+
: buildAttachedArgs(provider, nativeThreadId, cwd, plan, allowance)
|
|
836
936
|
} catch {
|
|
837
937
|
return fail('unsupported_policy', 'not_attempted', { ...base, detail: 'argv_build_failed', durationMs: duration() })
|
|
838
938
|
}
|
|
839
939
|
// Plan 4.7, enforced against the argv that is about to be spawned rather than
|
|
840
940
|
// against the source that builds it. No safe degraded argv exists, so this is a
|
|
841
941
|
// terminal refusal before any process is created.
|
|
842
|
-
const bannedArg = findBannedPermissionArg(args)
|
|
942
|
+
const bannedArg = findBannedPermissionArg(args, allowance)
|
|
843
943
|
if (bannedArg !== null) {
|
|
844
944
|
return fail('unsupported_policy', 'not_attempted', { ...base, detail: `banned_arg:${bannedArg}`, durationMs: duration() })
|
|
845
945
|
}
|
|
@@ -852,10 +952,11 @@ async function run(
|
|
|
852
952
|
}
|
|
853
953
|
let child: AttachedChildProcess
|
|
854
954
|
try {
|
|
855
|
-
child = deps.spawn({ binaryPath, args, cwd, env: buildAttachedEnv() })
|
|
856
|
-
} catch {
|
|
955
|
+
child = deps.spawn({ binaryPath, args, cwd, env: buildAttachedEnv(), provider })
|
|
956
|
+
} catch (error) {
|
|
857
957
|
// Includes ENOENT. No process exists, so no turn can have landed.
|
|
858
|
-
|
|
958
|
+
const detail = isCursorIsolationUnavailable(error) ? 'cursor_isolation_unavailable' : 'threw'
|
|
959
|
+
return fail('spawn_failed', 'not_attempted', { ...base, detail, durationMs: duration() })
|
|
859
960
|
}
|
|
860
961
|
if (!child || typeof child !== 'object' || typeof child.on !== 'function') {
|
|
861
962
|
return fail('spawn_failed', 'not_attempted', { ...base, detail: 'no_child', durationMs: duration() })
|
|
@@ -1764,14 +1865,26 @@ export function realAttachedTurnDeps(preflight: () => AttachedPreflightVerdict):
|
|
|
1764
1865
|
// after it. This is the floor that keeps group ownership (rule 4 above) from ever
|
|
1765
1866
|
// claiming a process that predates the turn.
|
|
1766
1867
|
const floor = Date.now() - OWNED_START_SLACK_MS
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1868
|
+
// 6.62.0 (plan 3.4): a Cursor child gets a config dir of its own, so `--model` cannot
|
|
1869
|
+
// rewrite the person's CLI default (C9). Child env only; removed when it exits.
|
|
1870
|
+
const isolation = request.provider === 'cursor' ? cursorSpawnEnv({ baseEnv: request.env }) : null
|
|
1871
|
+
// 6.62.0 /qa (W6): a Continue never falls back to the shared Cursor config.
|
|
1872
|
+
if (isolation && !isolation.isolated) throw cursorIsolationUnavailableError()
|
|
1873
|
+
let child: ReturnType<typeof nodeSpawn>
|
|
1874
|
+
try {
|
|
1875
|
+
child = nodeSpawn(request.binaryPath, [...request.args], {
|
|
1876
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1877
|
+
cwd: request.cwd,
|
|
1878
|
+
env: isolation ? isolation.env : request.env,
|
|
1879
|
+
// Group leader, so the whole provider tree can be signalled on timeout —
|
|
1880
|
+
// matching what both ordinary bridges do.
|
|
1881
|
+
detached: true,
|
|
1882
|
+
})
|
|
1883
|
+
} catch (error) {
|
|
1884
|
+
isolation?.release()
|
|
1885
|
+
throw error
|
|
1886
|
+
}
|
|
1887
|
+
if (isolation) releaseCursorSpawnOnExit(child, isolation)
|
|
1775
1888
|
const pid = child.pid
|
|
1776
1889
|
if (typeof pid === 'number' && Number.isSafeInteger(pid) && pid > 0) treeMemory.floorByRoot.set(pid, floor)
|
|
1777
1890
|
return child as unknown as AttachedChildProcess
|