@gotcos/glasses-server 6.61.7 → 6.63.0

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.
Files changed (70) hide show
  1. package/.env.example +6 -0
  2. package/CHANGELOG.md +39 -0
  3. package/README.md +5 -1
  4. package/bin/cli.cjs +50 -3
  5. package/bin/hooks/cos-cursor-observer.cjs +11 -4
  6. package/bin/hooks/cos-session-hook +46 -11
  7. package/docs/provider-parity.md +43 -0
  8. package/managed-runtime-contract.json +1 -0
  9. package/package.json +4 -2
  10. package/server/index.ts +141 -17
  11. package/server/lib/agent-session-binding-store.ts +9 -3
  12. package/server/lib/agent-session-store.ts +7 -4
  13. package/server/lib/attached-provider-adapter.ts +158 -45
  14. package/server/lib/banned-permission-args.ts +42 -1
  15. package/server/lib/claude-bridge.ts +17 -8
  16. package/server/lib/claude-hooks-installer.ts +22 -2
  17. package/server/lib/codex-bridge.ts +17 -2
  18. package/server/lib/codex-hooks-installer.ts +538 -0
  19. package/server/lib/codex-live-queue.ts +1 -1
  20. package/server/lib/codex-model-catalog.ts +49 -2
  21. package/server/lib/codex-rollout-state.ts +90 -0
  22. package/server/lib/codex-session-posture.ts +528 -0
  23. package/server/lib/continue-plan.ts +180 -0
  24. package/server/lib/continue-read-health.ts +26 -0
  25. package/server/lib/cursor-bridge.ts +26 -6
  26. package/server/lib/cursor-fork-context.ts +140 -0
  27. package/server/lib/cursor-model-catalog.ts +62 -5
  28. package/server/lib/cursor-observer-installer.ts +48 -7
  29. package/server/lib/cursor-session-model.ts +339 -0
  30. package/server/lib/cursor-spawn-env.ts +210 -0
  31. package/server/lib/fork-job-ledger.ts +194 -0
  32. package/server/lib/fork-thread.ts +169 -25
  33. package/server/lib/health-static-probes.ts +21 -11
  34. package/server/lib/lens-gist-engines.ts +15 -4
  35. package/server/lib/lens-gist.ts +1 -1
  36. package/server/lib/live-cues-cursor.ts +16 -6
  37. package/server/lib/occupancy-probes.ts +45 -1
  38. package/server/lib/permission-broker.ts +24 -9
  39. package/server/lib/provider-actions.ts +149 -0
  40. package/server/lib/provider-failure-text.ts +27 -0
  41. package/server/lib/provider-observe.ts +212 -0
  42. package/server/lib/provider-proof.ts +23 -14
  43. package/server/lib/query-job-runtime.ts +16 -1
  44. package/server/lib/query-job-types.ts +2 -1
  45. package/server/lib/session-act-fields.ts +61 -0
  46. package/server/lib/session-cancel.ts +95 -4
  47. package/server/lib/session-continue-facts-real.ts +74 -0
  48. package/server/lib/session-continue-facts.ts +267 -0
  49. package/server/lib/session-halt.ts +67 -6
  50. package/server/lib/session-hook-events.ts +57 -0
  51. package/server/lib/session-hook-ledger.ts +21 -3
  52. package/server/lib/session-hook-spool.ts +68 -6
  53. package/server/lib/session-hooks-runtime.ts +32 -2
  54. package/server/lib/session-peer-inbox-deps.ts +7 -6
  55. package/server/lib/session-recommendation.ts +3 -2
  56. package/server/lib/session-signal-store.ts +74 -5
  57. package/server/lib/session-stream-events.ts +32 -1
  58. package/server/lib/thread-attach-capability.ts +15 -1
  59. package/server/lib/thread-drain-kick.ts +4 -2
  60. package/server/lib/thread-occupancy.ts +44 -0
  61. package/server/lib/thread-turn-queue-deliver.ts +6 -1
  62. package/server/lib/thread-turn-queue.ts +6 -0
  63. package/server/lib/turn-request.ts +274 -0
  64. package/server/routes/agent-session-bindings.ts +349 -26
  65. package/server/routes/agent-session-stream.ts +26 -7
  66. package/server/routes/agent-sessions.ts +155 -10
  67. package/server/routes/health.ts +27 -2
  68. package/server/routes/session-hooks.ts +56 -5
  69. package/server/routes/thread-turn-queue.ts +37 -9
  70. package/server/scripts/hooks-cli.ts +33 -7
@@ -121,13 +121,17 @@ import {
121
121
  type BindingState,
122
122
  type NativeBinding,
123
123
  } from '../lib/agent-session-binding-store.js'
124
+ import { boundedContinueNote, CONTINUE_NOTE_HEADER, continueNoteAcknowledged, fallbackContinueNote } from '../lib/continue-plan.js'
124
125
  import type { RegistryCheck, RegistryRejection, RegistryResult } from '../lib/agent-session-binding-registry.js'
125
126
  import { recordCosSpawn, releaseCosSpawn } from '../lib/agent-session-ownership-store.js'
126
127
  import { isValidNativeThreadId } from '../lib/native-thread-id.js'
128
+ import { FORK_PROVIDER_FAILURES, type ForkProviderFailure } from '../lib/fork-thread.js'
129
+ import { CLIENT_FORK_ID_RE, ForkJobLedger, type ForkJobRow } from '../lib/fork-job-ledger.js'
127
130
  import { PEER_VERIFY_TIMEOUT_MS } from '../lib/session-peer-inbox.js'
128
131
  import {
129
132
  CANCEL_QUEUE_HOLD_MS,
130
133
  CLIENT_CANCEL_ID_RE,
134
+ DESK_RUN_EFFECT_COPY,
131
135
  cancelRefusalCopy,
132
136
  cancelTargetFor,
133
137
  type CancelFacts,
@@ -257,6 +261,11 @@ export interface AttachedTurnRequest {
257
261
  * simply never calls it.
258
262
  */
259
263
  onSpawn: (pid: number) => boolean
264
+ /**
265
+ * 6.62.0 /qa (Q3): the client sent `X-COS-Continue-Note: 1`, i.e. it shows `continue_note`.
266
+ * Only then may a Cursor Continue run Run Everything; otherwise it keeps the 6.61 Ask posture.
267
+ */
268
+ continueNoteAck?: boolean
260
269
  /**
261
270
  * 6.53.0: aborted by POST .../cancel. The adapter checks it before the spawn and
262
271
  * before the prompt write, and stops the child's process group after. Optional so an
@@ -370,6 +379,29 @@ export interface AgentSessionBindingsDeps {
370
379
  */
371
380
  deliverCodexLiveTurn?: (request: { provider: string; sessionId: string; prompt: string; foreignHolder: boolean }) => Promise<{ ok: boolean; reason: string; verifiedBy?: string | null; queuedId?: string | null }>
372
381
 
382
+ /**
383
+ * 6.62.0 (plan 3.6, W8): may a BUSY Codex thread the Codex app holds take a turn through
384
+ * that app's own queue right now? Production: `COS_CODEX_LIVE_QUEUE` is on AND the COS
385
+ * queue for the thread is empty (so a new turn never jumps one already waiting). Absent
386
+ * or anything but `true`: a busy holder refuses `native_thread_working`, as in 6.61.
387
+ */
388
+ busyCodexHop?: (threadId: string) => boolean
389
+
390
+ /**
391
+ * 6.62.0 (plan 3.12): what a Continue on this thread would run with, in 60 characters
392
+ * or less (`session-continue-facts.ts`). Added to the attachability verdict as
393
+ * `continue_note`. Optional; absent, null, slow (past `CONTINUE_NOTE_BUDGET_MS`) or a
394
+ * throw omits the field, and the verdict is unchanged.
395
+ */
396
+ continueNote?: (provider: BindableProvider, threadId: string, options?: { noteAck: boolean }) => Promise<string | null> | string | null
397
+
398
+ /**
399
+ * 6.62.0 /qa (W9): the note when `continueNote` misses its budget, throws, or answers
400
+ * nothing usable, so a Cursor Continue never runs Run Everything unannounced. Defaults to
401
+ * `fallbackContinueNote` with this process's environment.
402
+ */
403
+ continueNoteFallback?: (provider: BindableProvider, options: { noteAck: boolean }) => string | null
404
+
373
405
  /**
374
406
  * The self-recursion ledger. Defaults to the real process-wide one.
375
407
  *
@@ -406,7 +438,7 @@ export interface AgentSessionBindingsDeps {
406
438
  * write a NEW session rather than the one asked for, which for a fork means the
407
439
  * copy silently lands in the wrong project.
408
440
  */
409
- resolveForkWorkspace?: (provider: BindableProvider, nativeThreadId: string) => string | null
441
+ resolveForkWorkspace?: (provider: BindableProvider, nativeThreadId: string) => string | null | Promise<string | null>
410
442
 
411
443
  /**
412
444
  * Where an opaque fork reference is exchanged for the thread it names.
@@ -417,6 +449,13 @@ export interface AgentSessionBindingsDeps {
417
449
  */
418
450
  forkRefs?: ForkRefStore
419
451
 
452
+ /**
453
+ * 6.63.0: where a background fork (`clientForkId` in the body) records its outcome for
454
+ * `GET /agent-session-forks/:clientForkId`. Defaults to an in-memory ledger per router;
455
+ * the server wires the durable one (`<data>/fork-jobs.json`).
456
+ */
457
+ forkJobs?: ForkJobLedger
458
+
420
459
  /** Lease TTL for a new binding. */
421
460
  attachTtlMs?: number
422
461
  /** Upper bound on one prompt. A prompt is not a payload. */
@@ -459,7 +498,7 @@ export interface CancelDeps {
459
498
  * have reached) the open session. Writes the halt marker, and re-arms it once when that
460
499
  * turn's own UserPromptSubmit deletes it (`haltHandedOffTurn`). False: not armed.
461
500
  */
462
- haltHandedOffTurn?: (sessionId: string, marker: { at: number; clientCancelId: string }, turn: { prompt: string; sentAt: number; unverified?: boolean }) => boolean
501
+ haltHandedOffTurn?: (sessionId: string, marker: { at: number; clientCancelId: string }, turn: { prompt: string; sentAt: number; unverified?: boolean; verbatim?: boolean; deferMarker?: boolean }) => boolean
463
502
  /** Append to `data/session-cancel.jsonl`. */
464
503
  ledger?: (row: SessionCancelLedgerRow) => void
465
504
  }
@@ -597,6 +636,13 @@ export type WriteRefusal =
597
636
  | 'fork_failed'
598
637
  | 'fork_source_mutated'
599
638
  | 'fork_orphan_possible'
639
+ // 6.62.1 (2026-10-05): the copy exists and is named, but the provider refused its
640
+ // first turn. Each says WHY, because the next step differs: a smaller source, a
641
+ // reset, or a sign-in. None is retryable: a retry makes another full copy.
642
+ | 'fork_turn_failed'
643
+ | 'fork_context_too_long'
644
+ | 'fork_provider_limit'
645
+ | 'fork_provider_auth'
600
646
 
601
647
  /**
602
648
  * Footer copy for the refusals that are not occupancy reasons.
@@ -680,8 +726,18 @@ export const WRITE_REASON_COPY: Record<Exclude<WriteRefusal, OccupancyReason>, s
680
726
  // touches it.
681
727
  fork_source_mutated:
682
728
  'The original thread changed while COS was copying it. Open the original on your Mac and check it before doing anything else.',
729
+ // 6.62.1: no "untouched" here. This outcome is reached exactly when COS could not
730
+ // read the copy's result, so it cannot vouch for anything after the spawn either.
683
731
  fork_orphan_possible:
684
- 'COS lost track of the copy it was making. Your original is untouched, but a partial copy may exist on your Mac.',
732
+ 'COS did not get an answer from the copy it was making. A copy may exist on your Mac; check your sessions before forking again.',
733
+ fork_turn_failed:
734
+ 'COS made the copy, but the assistant did not answer in it. Open the copy to see why before trying again.',
735
+ fork_context_too_long:
736
+ 'This conversation is too long to continue in a copy: the assistant refused it as too long. Start a new session with the task instead.',
737
+ fork_provider_limit:
738
+ 'The assistant hit its usage limit, so the copy got no answer. Try again after the limit resets, or start a new session with another assistant.',
739
+ fork_provider_auth:
740
+ 'The assistant needs you to sign in again on this Mac before COS can copy this conversation.',
685
741
  }
686
742
 
687
743
  export function writeReasonCopy(reason: WriteRefusal): string {
@@ -814,7 +870,7 @@ export function projectAttachability(verdict: Occupancy): AttachabilityBody {
814
870
  const owners = Array.isArray(verdict?.owners) ? verdict.owners : null
815
871
  // Strictly `=== true`. An idleHolder of 1, 'yes', or {} is a malformed verdict,
816
872
  // and a malformed verdict must not buy an exemption.
817
- const foreignOwnerDeclared = verdict?.idleHolder === true
873
+ const foreignOwnerDeclared = verdict?.idleHolder === true || verdict?.busyHolder === true
818
874
  const sound =
819
875
  verdict?.attachable === true &&
820
876
  verdict.reason === null &&
@@ -1412,9 +1468,23 @@ export class ForkRefStore {
1412
1468
  export type ForkOutcome =
1413
1469
  | { kind: 'created'; newNativeThreadId: string; integrity: 'verified_unchanged' | 'unverified' }
1414
1470
  | { kind: 'mutated' }
1415
- | { kind: 'orphan_possible' }
1471
+ // 6.62.1: the copy is named but its first turn failed; `cause` is the provider's own
1472
+ // reason when its error event said one.
1473
+ | { kind: 'turn_failed'; newNativeThreadId: string; cause: ForkProviderFailure | null }
1474
+ | { kind: 'orphan_possible'; cause?: ForkProviderFailure | null }
1416
1475
  | { kind: 'failed' }
1417
1476
 
1477
+ const forkProviderFailure = (detail: unknown): ForkProviderFailure | null =>
1478
+ typeof detail === 'string' && (FORK_PROVIDER_FAILURES as readonly string[]).includes(detail) ? detail as ForkProviderFailure : null
1479
+
1480
+ /** The refusal reason for a provider-stated cause, or the generic one. */
1481
+ export function forkFailureReason(cause: ForkProviderFailure | null | undefined, generic: WriteRefusal): WriteRefusal {
1482
+ if (cause === 'context_too_long') return 'fork_context_too_long'
1483
+ if (cause === 'provider_limit') return 'fork_provider_limit'
1484
+ if (cause === 'provider_auth') return 'fork_provider_auth'
1485
+ return generic
1486
+ }
1487
+
1418
1488
  /**
1419
1489
  * Read a fork result without believing anything it did not say.
1420
1490
  *
@@ -1427,7 +1497,7 @@ export type ForkOutcome =
1427
1497
  */
1428
1498
  export function classifyFork(result: unknown, sourceNativeThreadId: string): ForkOutcome {
1429
1499
  if (!result || typeof result !== 'object' || Array.isArray(result)) return { kind: 'orphan_possible' }
1430
- const { ok, newNativeThreadId, sourceIntegrity, forkState, reason } = result as Record<string, unknown>
1500
+ const { ok, newNativeThreadId, sourceIntegrity, forkState, reason, detail } = result as Record<string, unknown>
1431
1501
 
1432
1502
  if (ok === true) {
1433
1503
  // The single invariant this route re-checks itself rather than inheriting.
@@ -1449,11 +1519,35 @@ export function classifyFork(result: unknown, sourceNativeThreadId: string): For
1449
1519
  if (reason === 'source_thread_mutated' || sourceIntegrity === 'mutated') return { kind: 'mutated' }
1450
1520
  // Only an explicit "no child was created" earns the clean failure.
1451
1521
  if (forkState === 'none') return { kind: 'failed' }
1522
+ // 6.62.1: a named copy is not an orphan. Same id rules as success: valid, and
1523
+ // never the source (that would be an append reported as a copy).
1524
+ if (isValidNativeThreadId(newNativeThreadId) && newNativeThreadId !== sourceNativeThreadId) {
1525
+ return { kind: 'turn_failed', newNativeThreadId, cause: forkProviderFailure(detail) }
1526
+ }
1527
+ return { kind: 'orphan_possible', cause: forkProviderFailure(detail) }
1452
1528
  }
1453
1529
  return { kind: 'orphan_possible' }
1454
1530
  }
1455
1531
 
1456
1532
  export const FORKED_COPY = 'Copied into a new thread. Your original is untouched.'
1533
+ /** 6.63.0: the 202 for a background fork. The outcome is read from the status route. */
1534
+ export const FORK_RUNNING_COPY = 'COS is making the copy and sending your instruction. This can take several minutes.'
1535
+ export const FORK_JOB_UNKNOWN_COPY = 'This server has no record of that fork. Look for the copy in Sessions before forking again.'
1536
+
1537
+ /** What a background fork's id answers right now: running, or the recorded outcome. */
1538
+ export function projectForkJob(row: ForkJobRow): { status: number; body: Record<string, unknown> } {
1539
+ if (row.state === 'running') {
1540
+ return { status: 200, body: { state: 'running', clientForkId: row.clientForkId, acceptedAt: row.acceptedAt, reasonCopy: FORK_RUNNING_COPY } }
1541
+ }
1542
+ // `status` is the ORIGINAL answer's code, surfaced under its own name: a refused fork
1543
+ // read back correctly is a successful read.
1544
+ return {
1545
+ status: 200,
1546
+ body: { ...(row.result ?? {}), state: 'done', clientForkId: row.clientForkId, recordedStatus: row.status ?? null, polled: true },
1547
+ }
1548
+ }
1549
+ /** 6.62.1: `unverified` must never be rendered as confirmed untouched (see the success body). */
1550
+ export const FORKED_COPY_UNVERIFIED = 'Copied into a new thread. COS could not re-read your original afterwards to confirm it is unchanged.'
1457
1551
 
1458
1552
  /**
1459
1553
  * Both fingerprints present, bounded, and strings.
@@ -1500,6 +1594,12 @@ export const TURN_SENT_CODEX_QUEUE_COPY = 'Queued in the Codex app. It runs when
1500
1594
  */
1501
1595
  export const LIVE_VERIFY_BUDGET_MS = PEER_VERIFY_TIMEOUT_MS
1502
1596
 
1597
+ /**
1598
+ * 6.62.0: how long the attachability verdict waits for its `continue_note`. The verdict is
1599
+ * a menu-open read; a cold Codex posture scan or a busy Cursor database must not hold it.
1600
+ */
1601
+ export const CONTINUE_NOTE_BUDGET_MS = 1_500
1602
+
1503
1603
  /**
1504
1604
  * The 202 copy. Says QUEUED and not sent, because at this instant the provider has
1505
1605
  * not been spawned — claiming otherwise would be the same silent-success lie the
@@ -1572,6 +1672,7 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
1572
1672
  // One per router. Injectable so the follow-on (attach accepting a `forkRef`)
1573
1673
  // shares this instance rather than standing up a second, disconnected one.
1574
1674
  const forkRefs = deps?.forkRefs instanceof ForkRefStore ? deps.forkRefs : new ForkRefStore()
1675
+ const forkJobs = deps?.forkJobs instanceof ForkJobLedger ? deps.forkJobs : new ForkJobLedger(randomUUID())
1575
1676
  const attachTtlMs =
1576
1677
  Number.isFinite(deps?.attachTtlMs) && (deps.attachTtlMs as number) > 0
1577
1678
  ? (deps.attachTtlMs as number)
@@ -1623,6 +1724,7 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
1623
1724
  }
1624
1725
  return {
1625
1726
  provider,
1727
+ threadId,
1626
1728
  cosTurnInFlight,
1627
1729
  runningOutsideCos,
1628
1730
  hooksEnabled: cancelProbe(() => cancelDeps.hooksEnabled?.() === true, false),
@@ -1660,7 +1762,7 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
1660
1762
  provider: string,
1661
1763
  threadId: string,
1662
1764
  reached: 'none' | 'reached' | 'maybe',
1663
- turn: { prompt: string; sentAt: number },
1765
+ turn: { prompt: string; sentAt: number; busy?: boolean },
1664
1766
  ): void => {
1665
1767
  const latch = entry.latched
1666
1768
  if (latch === null) return
@@ -1673,6 +1775,7 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
1673
1775
  if (reached !== 'none') {
1674
1776
  target = cancelTargetFor({
1675
1777
  provider,
1778
+ threadId,
1676
1779
  cosTurnInFlight: false,
1677
1780
  runningOutsideCos: true,
1678
1781
  hooksEnabled: cancelProbe(() => cancelDeps.hooksEnabled?.() === true, false),
@@ -1685,6 +1788,13 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
1685
1788
  prompt: turn.prompt,
1686
1789
  sentAt: turn.sentAt,
1687
1790
  unverified: reached === 'maybe',
1791
+ // 6.62.0 (plan 3.6): a Codex turn went into the app's own queue, which hands its
1792
+ // text to the session's UserPromptSubmit as is: matched verbatim, no peer unwrap.
1793
+ ...(provider === 'codex' ? { verbatim: true } : {}),
1794
+ // 6.62.0 /qa (W3): on a BUSY hop the session is still running Miles's OWN desk turn.
1795
+ // A marker written now would stop that turn at its next tool, so only the re-arm is
1796
+ // registered: the marker lands on the delivered turn's own UserPromptSubmit.
1797
+ ...(turn.busy === true ? { deferMarker: true } : {}),
1688
1798
  }) === true, false)
1689
1799
  if (armed) {
1690
1800
  const settledPermissions = cancelProbe(() => cancelDeps.settlePermissions?.(threadId) ?? 0, 0)
@@ -1692,6 +1802,7 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
1692
1802
  cancelled: true,
1693
1803
  target,
1694
1804
  effective: 'next_tool_call',
1805
+ effectCopy: DESK_RUN_EFFECT_COPY,
1695
1806
  ...common,
1696
1807
  settledPermissions: Number.isInteger(settledPermissions) && settledPermissions > 0 ? settledPermissions : 0,
1697
1808
  }
@@ -1743,8 +1854,29 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
1743
1854
  return { attachable: false, owners: [], reason: 'probe_failed' }
1744
1855
  }
1745
1856
  }
1857
+ /**
1858
+ * 6.62.0 (plan 3.6, W8): a WORKING Codex holder that is not ours, on a thread the hop may
1859
+ * take, reads attachable through a declared `busyHolder`. Shared by the probe, attach and
1860
+ * turns, so all three agree. Anything less than every condition keeps the verdict as is.
1861
+ */
1862
+ const withBusyCodexHop = (provider: string, threadId: string, detected: Occupancy): Occupancy => {
1863
+ if (provider !== 'codex' || detected.attachable || detected.reason !== 'native_thread_working') return detected
1864
+ if (typeof deps.deliverCodexLiveTurn !== 'function' || typeof deps.busyCodexHop !== 'function') return detected
1865
+ const owners = Array.isArray(detected.owners) ? detected.owners : []
1866
+ if (!owners.some(owner => owner?.selfOwned !== true && owner?.source === 'codex-writer-lock')) return detected
1867
+ let allowed = false
1868
+ try {
1869
+ allowed = deps.busyCodexHop(threadId) === true
1870
+ } catch (error) {
1871
+ console.error(`[agent-session-bindings] busy codex hop check threw: ${error instanceof Error ? error.message : error}`)
1872
+ allowed = false
1873
+ }
1874
+ return allowed ? { attachable: true, owners, reason: null, busyHolder: true } : detected
1875
+ }
1876
+ const routeOccupancy = (provider: string, threadId: string): Occupancy =>
1877
+ withBusyCodexHop(provider, threadId, detectOccupancy(provider, threadId))
1746
1878
  const runOccupancy = (provider: string, threadId: string): AttachabilityBody =>
1747
- projectAttachability(detectOccupancy(provider, threadId))
1879
+ projectAttachability(routeOccupancy(provider, threadId))
1748
1880
 
1749
1881
  const refuseAttach = (res: Response, reason: WriteRefusal): void => {
1750
1882
  res.status(refusalStatus(reason)).json({
@@ -1853,7 +1985,46 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
1853
1985
  res.json({ released: true, target, provider: outcome.row.provider })
1854
1986
  })
1855
1987
 
1856
- router.get('/agent-sessions/:provider/:threadId/attachability', (req, res) => {
1988
+ /**
1989
+ * 6.62.0 (plan 3.12): `continue_note` for the verdict, bounded in time and length. 6.62.0
1990
+ * /qa (W9): a read that misses the budget, throws, or answers nothing usable gets the
1991
+ * provider's fixed note instead, never no note (Claude has none either way).
1992
+ */
1993
+ const continueNoteFor = async (provider: string, threadId: string, noteAck: boolean): Promise<{ continue_note?: string }> => {
1994
+ const read = deps.continueNote
1995
+ if (typeof read !== 'function' || !isBindableProvider(provider) || !isValidNativeThreadId(threadId)) return {}
1996
+ const fallback = (): { continue_note?: string } => {
1997
+ let note: string | null = null
1998
+ try {
1999
+ note = deps.continueNoteFallback
2000
+ ? deps.continueNoteFallback(provider, { noteAck })
2001
+ : fallbackContinueNote(provider, { noteAck })
2002
+ } catch {
2003
+ note = null
2004
+ }
2005
+ const bounded = boundedContinueNote(note)
2006
+ return bounded ? { continue_note: bounded } : {}
2007
+ }
2008
+ let timer: ReturnType<typeof setTimeout> | null = null
2009
+ let missed = false
2010
+ try {
2011
+ const note = await Promise.race([
2012
+ Promise.resolve(read(provider, threadId, { noteAck })),
2013
+ new Promise<null>(resolve => { timer = setTimeout(() => { missed = true; resolve(null) }, CONTINUE_NOTE_BUDGET_MS) }),
2014
+ ])
2015
+ const bounded = boundedContinueNote(note)
2016
+ if (bounded) return { continue_note: bounded }
2017
+ if (missed) console.warn(`[agent-session-bindings] continue note missed its ${CONTINUE_NOTE_BUDGET_MS} ms budget provider=${provider}: fixed note sent`)
2018
+ return fallback()
2019
+ } catch (error) {
2020
+ console.error(`[agent-session-bindings] continue note failed: ${error instanceof Error ? error.message : error}`)
2021
+ return fallback()
2022
+ } finally {
2023
+ if (timer) clearTimeout(timer)
2024
+ }
2025
+ }
2026
+
2027
+ router.get('/agent-sessions/:provider/:threadId/attachability', async (req, res) => {
1857
2028
  // An occupancy verdict is a liveness answer with a lifetime of roughly now.
1858
2029
  // A cached `attachable: true` is indistinguishable from a stale one, which is
1859
2030
  // the whole failure this feature exists to prevent.
@@ -1899,7 +2070,17 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
1899
2070
  // truncated-id hole opened. The tests pin the ordering behaviorally instead:
1900
2071
  // probes that throw on every call still return `unsupported_provider` /
1901
2072
  // `invalid_thread_id`, which is only possible if nothing was probed.
1902
- res.json(withCancel(runOccupancy(provider, threadId)))
2073
+ // 6.62.0 /qa (Q2): the busy Codex hop makes the verdict attachable, but a desk run IS
2074
+ // working on that thread, so the cancel row reads the RAW reason. Built from the verdict's
2075
+ // reason, the lens hid Cancel run on every busy Codex Desktop thread.
2076
+ const detected = routeOccupancy(provider, threadId)
2077
+ const verdictBody = projectAttachability(detected)
2078
+ const cancelReason = detected.busyHolder === true ? 'native_thread_working' : verdictBody.reason
2079
+ res.json({
2080
+ ...verdictBody,
2081
+ cancel: cancelFieldFor(provider, threadId, cancelReason),
2082
+ ...(await continueNoteFor(provider, threadId, continueNoteAcknowledged(req.headers[CONTINUE_NOTE_HEADER]))),
2083
+ })
1903
2084
  })
1904
2085
 
1905
2086
  // ------------------------------------------------------------------ cancel
@@ -1989,7 +2170,11 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
1989
2170
  activeEntry.controller.abort()
1990
2171
  // Noted as what the run becomes if the hop lands it: a desk run (Claude) or one only
1991
2172
  // its app can stop. Either way only the engine's own end discounts its open turn.
1992
- cancelProbe(() => cancelDeps.noteCancelled?.(provider, threadId, now, provider === 'claude' ? 'desk_run' : 'unsupported'), undefined)
2173
+ // 6.62.0: a landed Codex or Cursor hop is a desk run COS can stop when that engine's
2174
+ // hooks can (cancelTargetFor's own rule); Claude keeps its 6.53.3 answer unchanged.
2175
+ const landedAs = provider === 'claude' ? 'desk_run'
2176
+ : cancelTargetFor({ ...cancelFactsFor(provider, threadId, null), cosTurnInFlight: false, runningOutsideCos: true }) === 'desk_run' ? 'desk_run' : 'unsupported'
2177
+ cancelProbe(() => cancelDeps.noteCancelled?.(provider, threadId, now, landedAs), undefined)
1993
2178
  }
1994
2179
  activeEntry.latched.replayKeys.push(replayKey)
1995
2180
  return answer(202, 'cos_turn', 'accepted', {
@@ -2034,6 +2219,8 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
2034
2219
  cancelled: true,
2035
2220
  target,
2036
2221
  effective: 'next_tool_call',
2222
+ // 6.62.0 (plan 3.8): the words for what the tap did, the same for every engine.
2223
+ effectCopy: DESK_RUN_EFFECT_COPY,
2037
2224
  queuedHeld: queuedHeld(),
2038
2225
  queuedHoldMs: CANCEL_QUEUE_HOLD_MS,
2039
2226
  settledPermissions: Number.isInteger(settledPermissions) && settledPermissions > 0 ? settledPermissions : 0,
@@ -2281,10 +2468,23 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
2281
2468
 
2282
2469
  /** The per-source serialisation claim, released in the finally. */
2283
2470
  let claimedForkKey: string | null = null
2471
+ /**
2472
+ * 6.63.0: set once a background fork has answered 202. From then on the ledger is the
2473
+ * ONLY channel for the outcome, exactly like a queued turn: `answer` settles it there.
2474
+ */
2475
+ let backgroundForkId: string | null = null
2284
2476
 
2285
- const refuseFork = (reason: WriteRefusal, extra: Record<string, unknown> = {}): void => {
2477
+ const answer = (status: number, payload: Record<string, unknown>): void => {
2478
+ if (backgroundForkId !== null) {
2479
+ forkJobs.settle(backgroundForkId, status, payload, readNow() ?? Date.now())
2480
+ return
2481
+ }
2286
2482
  if (res.headersSent) return
2287
- res.status(refusalStatus(reason)).json({
2483
+ res.status(status).json(payload)
2484
+ }
2485
+
2486
+ const refuseFork = (reason: WriteRefusal, extra: Record<string, unknown> = {}): void => {
2487
+ answer(refusalStatus(reason), {
2288
2488
  forked: false,
2289
2489
  forkRef: null,
2290
2490
  sourceIntegrity: null,
@@ -2298,6 +2498,16 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
2298
2498
  })
2299
2499
  }
2300
2500
 
2501
+ /** Answers a known background fork id with its job, never a second spawn. */
2502
+ const replayForkJob = (row: ForkJobRow): void => {
2503
+ const projected = projectForkJob(row)
2504
+ res.status(row.state === 'running' ? 202 : projected.status).json({
2505
+ ...projected.body,
2506
+ accepted: row.state === 'running' ? true : undefined,
2507
+ replayed: true,
2508
+ })
2509
+ }
2510
+
2301
2511
  try {
2302
2512
  const fork = deps.forkThread
2303
2513
  if (typeof fork !== 'function') return refuseFork('fork_unwired')
@@ -2315,6 +2525,12 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
2315
2525
  if (typeof prompt !== 'string' || prompt.trim().length === 0 || prompt.length > maxPromptChars) {
2316
2526
  return refuseFork('invalid_request')
2317
2527
  }
2528
+ // 6.63.0: optional. With it, the fork answers 202 once every gate has passed and runs
2529
+ // in the background; without it, the route behaves exactly as before.
2530
+ const clientForkId = body.clientForkId
2531
+ if (clientForkId !== undefined && (typeof clientForkId !== 'string' || !CLIENT_FORK_ID_RE.test(clientForkId))) {
2532
+ return refuseFork('invalid_request')
2533
+ }
2318
2534
 
2319
2535
  const providerParam = String(req.params.provider ?? '')
2320
2536
  const threadIdParam = String(req.params.threadId ?? '')
@@ -2324,6 +2540,14 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
2324
2540
  if (!isForkableProvider(providerParam)) return refuseFork('fork_unsupported_provider')
2325
2541
  if (!isValidNativeThreadId(threadIdParam)) return refuseFork('fork_invalid_thread_id')
2326
2542
 
2543
+ // A repeat of a known id replays its job and never spawns: a client that lost the
2544
+ // 202 (or restarted) must be able to resend without making a second copy.
2545
+ const forkSource = opaqueRevision(targetKey(providerParam, threadIdParam))
2546
+ if (typeof clientForkId === 'string') {
2547
+ const known = forkJobs.get(clientForkId)
2548
+ if (known) return known.source === forkSource ? replayForkJob(known) : refuseFork('invalid_request')
2549
+ }
2550
+
2327
2551
  const now = readNow()
2328
2552
  if (now === null) return refuseFork('fork_failed')
2329
2553
 
@@ -2331,7 +2555,9 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
2331
2555
  if (typeof resolveWorkspace !== 'function') return refuseFork('fork_workspace_unresolvable')
2332
2556
  let cwd: string | null = null
2333
2557
  try {
2334
- cwd = resolveWorkspace(providerParam, threadIdParam)
2558
+ // 6.62.0: may answer later (a Cursor IDE composer's folder is read from Cursor's own
2559
+ // store). Still before the claim below, which stays the last synchronous step.
2560
+ cwd = await resolveWorkspace(providerParam, threadIdParam)
2335
2561
  } catch (error) {
2336
2562
  console.error(`[agent-session-bindings] fork workspace resolve threw: ${error instanceof Error ? error.message : error}`)
2337
2563
  cwd = null
@@ -2350,9 +2576,36 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
2350
2576
  // continuation or be blocked by one — `targetKey` is length-prefixed and
2351
2577
  // therefore unambiguous, and this prefix cannot collide with one.
2352
2578
  const forkKey = `fork:${targetKey(providerParam, threadIdParam)}`
2353
- if (!guard.tryClaim(forkKey, forkKey)) return refuseFork('fork_in_progress')
2579
+ if (!guard.tryClaim(forkKey, forkKey)) {
2580
+ // The same id arriving twice while the first is still resolving its workspace: the
2581
+ // first holds the claim and has begun the job by now, so replay it.
2582
+ const raced = typeof clientForkId === 'string' ? forkJobs.get(clientForkId) : null
2583
+ if (raced && raced.source === forkSource) return replayForkJob(raced)
2584
+ return refuseFork('fork_in_progress')
2585
+ }
2354
2586
  claimedForkKey = forkKey
2355
2587
 
2588
+ if (typeof clientForkId === 'string') {
2589
+ // Recorded BEFORE the spawn and before the 202: a job the ledger cannot hold would
2590
+ // have an outcome nobody can read, so it does not start.
2591
+ const begun = forkJobs.begin({ clientForkId, source: forkSource, provider: providerParam }, now)
2592
+ if (begun.kind === 'existing') return replayForkJob(begun.row)
2593
+ if (begun.kind !== 'started') return refuseFork(begun.kind === 'conflict' ? 'invalid_request' : 'fork_failed')
2594
+ res.status(202).json({
2595
+ accepted: true,
2596
+ state: 'running',
2597
+ clientForkId,
2598
+ forked: null,
2599
+ forkRef: null,
2600
+ orphanPossible: false,
2601
+ retryable: false,
2602
+ reason: null,
2603
+ reasonCopy: FORK_RUNNING_COPY,
2604
+ })
2605
+ backgroundForkId = clientForkId
2606
+ console.log(`[agent-session-bindings] fork accepted provider=${providerParam} background=true`)
2607
+ }
2608
+
2356
2609
  let raw: unknown
2357
2610
  try {
2358
2611
  raw = await fork({
@@ -2371,14 +2624,32 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
2371
2624
  }
2372
2625
 
2373
2626
  const outcome = classifyFork(raw, threadIdParam)
2627
+ {
2628
+ // 6.62.1: one line per attempt. On 2026-10-05 a fork's real cause existed only
2629
+ // in the copy's transcript; nothing reached the server log. Enums and numbers
2630
+ // only: no prompt, no provider text, no thread id.
2631
+ const r = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>
2632
+ console.log(`[agent-session-bindings] fork outcome provider=${providerParam} outcome=${outcome.kind} cause=${forkProviderFailure(r.detail) ?? 'none'} exit=${typeof r.exitCode === 'number' && Number.isFinite(r.exitCode) ? r.exitCode : 'none'} ms=${typeof r.durationMs === 'number' && Number.isFinite(r.durationMs) ? r.durationMs : 'unknown'}`)
2633
+ }
2374
2634
 
2375
2635
  if (outcome.kind === 'mutated') {
2376
2636
  // The original moved. Loudest outcome in the feature, and not retryable:
2377
2637
  // the user needs to look at their own thread before anything else touches it.
2378
2638
  return refuseFork('fork_source_mutated', { orphanPossible: true, retryable: false })
2379
2639
  }
2640
+ if (outcome.kind === 'turn_failed') {
2641
+ // The copy exists and Control can open it (the helper resolves the digest to the
2642
+ // local transcript). Not an orphan, and not retryable: another try copies again.
2643
+ const copyRef = forkRefs.remember(providerParam, outcome.newNativeThreadId, now)
2644
+ return refuseFork(forkFailureReason(outcome.cause, 'fork_turn_failed'), {
2645
+ forkRef: copyRef, turnFailed: true, failureClass: outcome.cause, orphanPossible: false, retryable: false,
2646
+ })
2647
+ }
2380
2648
  if (outcome.kind === 'orphan_possible') {
2381
- return refuseFork('fork_orphan_possible', { orphanPossible: true })
2649
+ const cause = outcome.cause ?? null
2650
+ return refuseFork(forkFailureReason(cause, 'fork_orphan_possible'), {
2651
+ orphanPossible: true, failureClass: cause, retryable: cause === null,
2652
+ })
2382
2653
  }
2383
2654
  if (outcome.kind === 'failed') return refuseFork('fork_failed')
2384
2655
 
@@ -2386,10 +2657,10 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
2386
2657
  // a fresh fork exactly as for an existing thread.
2387
2658
  const forkRef = forkRefs.remember(providerParam, outcome.newNativeThreadId, now)
2388
2659
 
2389
- res.status(201).json({
2660
+ answer(201, {
2390
2661
  forked: true,
2391
2662
  reason: null,
2392
- reasonCopy: FORKED_COPY,
2663
+ reasonCopy: outcome.integrity === 'verified_unchanged' ? FORKED_COPY : FORKED_COPY_UNVERIFIED,
2393
2664
  forkRef,
2394
2665
  // Reported, never assumed. `unverified` means COS could not read the
2395
2666
  // original at both ends — which is not the same as, and must never be
@@ -2404,7 +2675,39 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
2404
2675
  refuseFork('fork_orphan_possible', { orphanPossible: true })
2405
2676
  } finally {
2406
2677
  if (claimedForkKey !== null) guard.release(claimedForkKey, claimedForkKey)
2678
+ // A background job must never be left `running` by a path that forgot to answer:
2679
+ // its caller would poll a fork that can no longer finish. Cautious outcome only.
2680
+ if (backgroundForkId !== null && forkJobs.get(backgroundForkId)?.state === 'running') {
2681
+ console.error('[agent-session-bindings] background fork ended without an outcome; recorded as orphan-possible')
2682
+ forkJobs.settle(backgroundForkId, refusalStatus('fork_orphan_possible'), {
2683
+ forked: false, forkRef: null, sourceIntegrity: null, orphanPossible: true, retryable: false,
2684
+ reason: 'fork_orphan_possible', reasonCopy: writeReasonCopy('fork_orphan_possible'),
2685
+ }, readNow() ?? Date.now())
2686
+ }
2687
+ }
2688
+ })
2689
+
2690
+ // ----------------------------------------------------- background fork status
2691
+ //
2692
+ // 6.63.0: the outcome of a fork started with a `clientForkId`. Its own path segment
2693
+ // (`/agent-session-forks`), so it can never be read as `/agent-sessions/:provider/:threadId`.
2694
+ // UNGATED, like the fork route it reports on.
2695
+ router.get('/agent-session-forks/:clientForkId', (req, res) => {
2696
+ res.set('Cache-Control', 'private, no-store')
2697
+ const clientForkId = String(req.params.clientForkId ?? '')
2698
+ if (!CLIENT_FORK_ID_RE.test(clientForkId)) {
2699
+ res.status(400).json({ state: 'invalid_request', reasonCopy: FORK_JOB_UNKNOWN_COPY })
2700
+ return
2407
2701
  }
2702
+ const row = forkJobs.get(clientForkId)
2703
+ if (row === null) {
2704
+ // Not "failed": an older server, a dropped row or an id never admitted all read this way,
2705
+ // and any of them may have a copy.
2706
+ res.status(404).json({ state: 'unknown', clientForkId, reasonCopy: FORK_JOB_UNKNOWN_COPY })
2707
+ return
2708
+ }
2709
+ const projected = projectForkJob(row)
2710
+ res.status(projected.status).json(projected.body)
2408
2711
  })
2409
2712
 
2410
2713
  // ----------------------------------------------------------------- turns
@@ -2656,14 +2959,16 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
2656
2959
  // session started in the gap is exactly the residual risk option B leaves
2657
2960
  // open, and it is terminal here rather than a warning because COS has no
2658
2961
  // cross-process lock that could fence a live desktop writer.
2659
- const detected = detectOccupancy(binding.provider, binding.nativeThreadId)
2962
+ const detected = routeOccupancy(binding.provider, binding.nativeThreadId)
2660
2963
  const verdict = projectAttachability(detected)
2661
2964
  stage('gate')
2662
2965
  if (!verdict.attachable) return refuseTurn(verdict.reason ?? 'probe_failed')
2663
2966
  // 6.51.0: the SAME verdict says whether a live process that is not ours holds the
2664
- // thread. Only the declared idle-holder exemption can carry one on an attachable
2665
- // verdict (`projectAttachability` refuses anything else), so this is that fact.
2666
- const foreignHolder = detected.idleHolder === true
2967
+ // thread. Only the declared idle-holder exemption (or, 6.62.0, the busy Codex hop)
2968
+ // can carry one on an attachable verdict (`projectAttachability` refuses anything
2969
+ // else), so this is that fact.
2970
+ const busyCodexHop = detected.busyHolder === true
2971
+ const foreignHolder = (detected.idleHolder === true || busyCodexHop)
2667
2972
  && Array.isArray(detected.owners)
2668
2973
  && detected.owners.some(owner => owner?.selfOwned !== true)
2669
2974
 
@@ -2677,7 +2982,15 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
2677
2982
  // reads as diverged, because the first turn is what moved the head.
2678
2983
  const baseline = guard.acknowledgedHead(bindingId) ?? binding.nativeHeadAtAttach
2679
2984
  if (typeof baseline !== 'string' || baseline.length === 0) return refuseTurn('native_head_unavailable')
2680
- if (head.digest !== baseline) {
2985
+ if (head.digest !== baseline && busyCodexHop) {
2986
+ // 6.62.0 (plan 3.6): a busy thread moves while it works; that is what busy means.
2987
+ // The hop puts the turn into the app's own queue, which runs it after the current
2988
+ // turn, the same "append to whatever the thread is when it frees" the COS queue's
2989
+ // watermark exemption already grants a parked turn. Recorded, so the next
2990
+ // interactive turn compares against this head.
2991
+ console.log(`[agent-session-bindings] busy codex hop acknowledges a moving head turnId=${turnId} bindingId=${bindingId}`)
2992
+ guard.acknowledgeHead(bindingId, head.digest)
2993
+ } else if (head.digest !== baseline) {
2681
2994
  // Only a changed/not-changed signal and a new opaque revision. No diff, no
2682
2995
  // content, no path — the client is told THAT it moved, never to what.
2683
2996
  if (acknowledged !== head.digest) {
@@ -2813,7 +3126,7 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
2813
3126
  if (live?.ok) {
2814
3127
  // 6.53.3: queued in the Codex app, which only the app can stop ("Stop it there").
2815
3128
  const latched = latchedHere()
2816
- if (latched) settleLatchedCancel(latched, binding.provider, binding.nativeThreadId, 'reached', { prompt, sentAt: handOffAt })
3129
+ if (latched) settleLatchedCancel(latched, binding.provider, binding.nativeThreadId, 'reached', { prompt, sentAt: handOffAt, busy: busyCodexHop })
2817
3130
  console.log(`[agent-session-bindings] turn delivered live provider=codex turnId=${turnId} bindingId=${bindingId} verifiedBy=${live.verifiedBy ?? 'unknown'} queuedId=${live.queuedId ?? 'null'}`)
2818
3131
  respond(200, {
2819
3132
  turnId,
@@ -2832,10 +3145,18 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
2832
3145
  // latched on the way that is the whole turn: cancelled, and no child.
2833
3146
  const codexLatched = latchedHere()
2834
3147
  if (codexLatched && live !== null && live.reason !== 'unverified') {
2835
- settleLatchedCancel(codexLatched, binding.provider, binding.nativeThreadId, 'none', { prompt, sentAt: handOffAt })
3148
+ settleLatchedCancel(codexLatched, binding.provider, binding.nativeThreadId, 'none', { prompt, sentAt: handOffAt, busy: busyCodexHop })
2836
3149
  console.log(`[agent-session-bindings] turn cancelled during live hand-off provider=codex turnId=${turnId} bindingId=${bindingId} hop=${live.reason}`)
2837
3150
  return refuseTurn('turn_cancelled', { retryable: false })
2838
3151
  }
3152
+ // 6.62.0 (plan 3.6): a BUSY thread never falls through to a child (the writer lock
3153
+ // is held, so the spawn would fail after the prompt and fence). Whatever reached no
3154
+ // queue row is the 6.61 answer instead: working, and queueable, so the COS queue
3155
+ // takes it. A row that MAY be queued is still the ambiguous fence below.
3156
+ if (busyCodexHop && live !== null && live.reason !== 'unverified') {
3157
+ console.log(`[agent-session-bindings] busy codex hop declined provider=codex turnId=${turnId} bindingId=${bindingId} hop=${live.reason}`)
3158
+ return refuseTurn('native_thread_working')
3159
+ }
2839
3160
  // The operator switched this off: nothing was queued, so the abortable
2840
3161
  // 6.50 child path is cancellable again before it starts.
2841
3162
  if (live?.reason === 'disabled' && liveEntry?.turnId === turnId) liveEntry.cancellable = true
@@ -2860,7 +3181,7 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
2860
3181
  })
2861
3182
  console.warn(`[agent-session-bindings] fence set site=codex_live provider=codex target=${opaqueRevision(key)} turnId=${turnId} bindingId=${bindingId} headBefore=${head.digest} adapterReason=codex_queue_unverified`)
2862
3183
  // 6.53.3: a row MAY be in the Codex app's queue; a latched cancel says where to stop it.
2863
- if (codexLatched) settleLatchedCancel(codexLatched, binding.provider, binding.nativeThreadId, 'maybe', { prompt, sentAt: handOffAt })
3184
+ if (codexLatched) settleLatchedCancel(codexLatched, binding.provider, binding.nativeThreadId, 'maybe', { prompt, sentAt: handOffAt, busy: busyCodexHop })
2864
3185
  return reportAmbiguous()
2865
3186
  }
2866
3187
  // Nothing was queued, and the child cannot run against a held thread. Not
@@ -2912,6 +3233,8 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
2912
3233
  expectedNativeHead: head.raw,
2913
3234
  prompt,
2914
3235
  abortSignal: cancelController.signal,
3236
+ // 6.62.0 /qa (Q3): only a client that shows `continue_note` gets Cursor Run Everything.
3237
+ continueNoteAck: continueNoteAcknowledged(req.headers[CONTINUE_NOTE_HEADER]),
2915
3238
  onSpawn: (pid: number): boolean => {
2916
3239
  // THE SELF-RECURSION ORDER. The child registers itself against the id
2917
3240
  // we are targeting, so unless it is in the ledger the next occupancy