@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
package/.env.example CHANGED
@@ -334,3 +334,9 @@ BIND_HOST=0.0.0.0
334
334
  # time <scripts>/venv/bin/python3 <scripts>/lightrag_search.py --explore "Some Entity"
335
335
  # Consistently under ~30s, turn it on. Otherwise leave it off — cues still get
336
336
  # memory grounding from Qdrant, which returns in ~1.5s.
337
+
338
+ # Sessions Continue: 0 restores the 6.61 posture (Codex configured sandbox,
339
+ # Cursor Ask). Default 1 preserves session settings; Cursor Run Everything
340
+ # additionally requires a client that acknowledges displaying continue_note.
341
+ # Keep this in ~/.cos-glasses/.env; Control does not yet persist it on update.
342
+ COS_CONTINUE_FULL_PERMISSIONS=1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,42 @@
1
+ ## 6.63.0 — 2026-10-05 — Background forks
2
+
3
+ - Background forks. `POST /api/agent-sessions/:provider/:threadId/fork` accepts an optional `clientForkId`: once every gate passes it answers 202 and runs the fork in the background, and `GET /api/agent-session-forks/:clientForkId` returns `running` or the exact answer the synchronous route would have sent. A repeated id replays its job and never makes a second copy. Outcomes are kept in `<data>/fork-jobs.json` (7 days, 200 rows); a fork still running when the server stops reads back as orphan-possible ("look in Sessions before forking again"), never as failed. Without the id the route is unchanged. Why: on 2026-10-05 COS Control waited 5 minutes for a 6.5-minute fork that succeeded and told Miles the server had stopped. Pairs with COS Control 0.5.257; older clients keep the synchronous route.
4
+ - A fork that recovered from a transient retry (Claude `api_retry` "overloaded"/"rate_limit", Codex "Reconnecting… 429") and then failed is reported by the provider's final error, not the retry: "too long" no longer reads as "usage limit". (6.62.1 QA)
5
+
6
+ ## 6.62.1 — 2026-10-05 — Combined provider parity and handoff hardening
7
+
8
+ - Includes the 6.62.0 provider parity, cancellation, hook readiness, and safe Cursor continuation updates.
9
+ - A failed fork names its copy when available and distinguishes usage limits, authentication, and context overflow.
10
+ - Claude, Codex, and Cursor prompt hooks receive a private, short-lived binding to the current user request.
11
+ - Compatible with existing glasses; build 6.10.600 displays the server continuation note and opts into the matching Cursor behavior.
12
+
13
+ ### Included 6.62.0 candidate work — Provider parity: Claude, Codex and Cursor session visibility and continuation
14
+
15
+ Originally prepared for a candidate release; these changes are included in 6.62.1. The standalone 6.62.0 npm release was skipped. Pairs with glasses 6.10.600 (copy only); older glasses keep working.
16
+
17
+ **Permission change on Continue (read this).** A Continue now carries the session's OWN permissions:
18
+ - Codex: the session's model, effort and sandbox from its latest `turn_context`. A Desktop thread recorded as `danger-full-access` resumes with it, but only when its folder is already trusted in `~/.codex/config.toml`; otherwise `workspace-write` (network as the session had it). Unreadable settings fall back to the 6.61 posture with a note. 6.61 resumed every Codex Continue read-only.
19
+ - Cursor: Run Everything (`--force`) on the session's own model only for clients sending `X-COS-Continue-Note: 1`; older clients retain Ask mode (6.61: Ask mode on Composer 2.5 Fast). Forks stay read-only for every provider; Cursor's Fork is a new read-only chat seeded with the session's context.
20
+ - Claude: unchanged.
21
+ - Kill switch: `COS_CONTINUE_FULL_PERMISSIONS=0` in `~/.cos-glasses/.env` restores the 6.61 argv byte for byte. Detail and the attach verdict carry a `continue_note` (≤60 chars) saying what will run.
22
+
23
+ **Seeing the work.**
24
+ - One working hint for every provider: an open hook turn reads active for up to 5 minutes after its last event (Esc without Stop decays), on top of the 6.61 reading; Cursor rows are decided by their hooks (they were always quiet before). Codex reads its rollout turn markers until its hooks are trusted.
25
+ - Codex hooks: `--hooks install` (and Control's Install hooks) also writes the COS hook into `~/.codex/hooks.json`. Codex runs untrusted hooks silently never, so run `codex` in Terminal and trust them once ("Trust all and continue"). Trust is read through Codex's own `hooks/list`; COS never writes hook trust. Continue never adds project trust; Codex itself may write project trust when starting a new session. Claude's hooks state, banner and desk cancel stay Claude-only.
26
+ - Cursor observer: its spool files were rejected since 6.61.7 (filename nonce); now accepted, recent rejects re-ingested once, `afterAgentThought` gives Cursor a reasoning line (spooled on disk until drained, then memory and stream only; never the ledger). The observer stops adding thoughts when the drain is stale.
27
+ - `reported_model` on detail for all three; list polling avoids model lookups. Cursor titles use the composer name or the first query. Codex rollout completion can close an observed compaction phase; compaction start still needs hook evidence.
28
+ - `/api/health` and `/api/models`: `providers.{claude,codex,cursor}.{observe,act}` with a reason for every "no"; `threadForkProviders`; `features.sessionCancel.deskCodex` / `deskCursor`.
29
+
30
+ **Acting the same.**
31
+ - Cancel run stops desk Codex (trusted hooks) and desk Cursor runs at their next command, like Claude, after provider-stamped hooks have been observed for that thread and the current hook is ready. Codex gets a deny-only halt reply (it fails open on Claude's); Claude's reply bytes are unchanged and the 6.61.7 script stays halt-ready.
32
+ - Live Continue into an open Claude session is on by default (`COS_CONTINUE_LIVE=0` turns it off). A busy Codex Desktop thread takes a Continue straight into its own queue when COS has nothing queued for it. That hop accepts the moving native head and enters the app queue, which is not shown in the COS Queue. An Interrupt kicks the queue like a Stop. Cursor CLI queued turns auto-run after the chat is free; the stored client acknowledgement determines Ask versus Run Everything.
33
+ - Every Cursor CLI spawn runs on its own config dir (copy of the user's config, chats/hooks/MCP/agents/plans linked), and Continue/Fork refuse if isolation fails, so COS no longer overwrites the user's Cursor default model.
34
+ - Codex and Cursor Messages jobs link to their native session at the first event.
35
+
36
+ Engine limits kept, with honest copy: an idle Cursor IDE composer cannot take an outside message; Cursor approvals cannot be answered remotely in `-p`; a ChatGPT.app thread cannot be steered mid-turn; pure `js` Codex steps are not caught by a cancel.
37
+
38
+ Codex hook trust is refreshed through a bounded `codex app-server` call every five minutes. Cursor hook-open turns quiet beyond five minutes stay OPEN (not actively writing) up to the 30-minute ceiling. Claude/Codex end-state visibility may lag by 30 seconds. Control does not yet persist the full-permissions kill switch through updates; set it in `.env`.
39
+
1
40
  ## 6.61.7 — 2026-10-04 — Agent activity and compaction visibility
2
41
 
3
42
  - Add display-only child activity to native Sessions list/detail. Codex uses read-only spawn relationships plus each child's lifecycle, with bounded reads, freshness, deduplication and explicit incomplete/unknown counts.
package/README.md CHANGED
@@ -206,7 +206,7 @@ state drafts; the `cursor`, `epoch` and `id:` line stay.
206
206
  Since 6.49.0 a Continue can land in the running session itself, so the Desktop tab or
207
207
  the `claude` in a terminal that you left open shows the turn and answers it with its own
208
208
  context, instead of a resume child writing to the transcript behind that window. It is
209
- off until `COS_CONTINUE_LIVE=1`. The server writes the turn to the session's own inbox
209
+ on by default since 6.62.0; set `COS_CONTINUE_LIVE=0` to disable it. The server writes the turn to the session's own inbox
210
210
  (the socket Claude Code publishes in `~/.claude/sessions/<pid>.json`, in the line format
211
211
  its help text documents), pinned to the full session id, and calls it delivered only
212
212
  when the session's transcript shows the message accepted; every other outcome takes the
@@ -913,3 +913,7 @@ The package includes a checksummed public Python runtime. It uses a unique owner
913
913
  Run `glasses-memory-setup --runtime-dir /absolute/path/to/cos-memory-runtime --data-root /absolute/path/to/private-memory` with Python 3.11+ installed. Set `COS_SCRIPTS_DIR` to the runtime directory in your server configuration, then restart the server. Add an explicitly granted Markdown/text source using `venv/bin/python3 manage.py grant /path/to/notes.md --title "Project"` from that runtime directory. The bundled README documents captures, reviews, document links, source refresh, upgrade and staged restore. Keep the latest independent deletion checkpoint separately from restorable snapshots.
914
914
 
915
915
  This is a local owner instance, separate from app.gotcos.com tenants and hosted Sessions. Installing the package does not upload your corpus or enable household sharing. A source build or local setup test is not proof of a published registry version; use the release ledger for actual availability.
916
+
917
+ ## Provider parity in 6.62.0
918
+
919
+ See [provider capabilities and limits](docs/provider-parity.md) for Continue permissions, client compatibility, hook trust, observation limits and rollback. Glasses 6.10.600 shows the server's Continue note. Older clients retain Cursor Ask behavior; forks remain read-only.
package/bin/cli.cjs CHANGED
@@ -146,12 +146,13 @@ function setupSpeakerModel() {
146
146
  return 0
147
147
  }
148
148
 
149
- // --hooks install|status|uninstall: the Claude Code session hook (6.48.0). Runs before the
149
+ // --hooks install|status|uninstall: the Claude Code session hook (6.48.0), and the same hook in
150
+ // Codex when Codex is on the Mac (6.62.0; `--codex` for Codex only). Runs before the
150
151
  // banner and the runtime checks, resolving tsx itself; the managed runtime never comes
151
152
  // through here (Control uses POST /api/session-hooks/install on the running server).
152
153
  if (process.argv.includes('--hooks')) {
153
154
  if (!['install', 'status', 'uninstall'].includes(HOOKS_ACTION)) {
154
- console.error('Usage: --hooks install|status|uninstall [--dry-run] [--port N]')
155
+ console.error('Usage: --hooks install|status|uninstall [--dry-run] [--port N] [--codex]')
155
156
  process.exit(64)
156
157
  }
157
158
  let hooksTsx
@@ -183,7 +184,7 @@ if (process.argv.includes('--help') || process.argv.includes('-h')) {
183
184
  console.log(' npx --yes @gotcos/glasses-server@latest --setup-transcription --transcription-tier balanced|max')
184
185
  console.log(' npx --yes @gotcos/glasses-server@latest --setup-speaker-model')
185
186
  console.log(' npx --yes @gotcos/glasses-server@latest --prepare-only')
186
- console.log(' npx --yes @gotcos/glasses-server@latest --hooks install|status|uninstall [--dry-run] [--port N]')
187
+ console.log(' npx --yes @gotcos/glasses-server@latest --hooks install|status|uninstall [--dry-run] [--port N] [--codex]')
187
188
  console.log('')
188
189
  console.log(' Requirements:')
189
190
  console.log(' - Node.js 20.11+')
@@ -280,9 +281,53 @@ function resolveCursorAgentBinary() {
280
281
  const homeLocal = resolve(homedir(), '.local', 'bin', 'agent')
281
282
  return existsSync(homeLocal) ? homeLocal : null
282
283
  }
284
+ // 6.62.0 (plan 3.4): the launcher's two `agent` probes run on a config dir of their own,
285
+ // like every server spawn (server/lib/cursor-spawn-env.ts, which this mirrors: CommonJS
286
+ // cannot import it). Same link list; the census test pins that the two lists agree.
287
+ const CURSOR_SPAWN_LINKS = ['chats', 'hooks.json', 'mcp.json', 'plugins', 'skills-cursor', 'agents', 'plans']
288
+ function cursorSpawnEnvSync() {
289
+ const { mkdtempSync, symlinkSync, rmSync } = require('fs')
290
+ const sourceDir = process.env.CURSOR_CONFIG_DIR?.trim() || join(homedir(), '.cursor')
291
+ const root = join(CONFIG_DIR, 'cursor-cli')
292
+ let dir = null
293
+ const release = () => {
294
+ if (!dir) return
295
+ for (const name of CURSOR_SPAWN_LINKS) {
296
+ try { if (lstatSync(join(dir, name)).isSymbolicLink()) unlinkSync(join(dir, name)) } catch { /* absent */ }
297
+ }
298
+ try { rmSync(dir, { recursive: true, force: true }) } catch { /* best effort */ }
299
+ dir = null
300
+ }
301
+ try {
302
+ mkdirSync(root, { recursive: true, mode: 0o700 })
303
+ dir = mkdtempSync(join(root, 'spawn-'))
304
+ chmodSync(dir, 0o700)
305
+ if (existsSync(join(sourceDir, 'cli-config.json'))) {
306
+ copyFileSync(join(sourceDir, 'cli-config.json'), join(dir, 'cli-config.json'))
307
+ chmodSync(join(dir, 'cli-config.json'), 0o600)
308
+ }
309
+ for (const name of CURSOR_SPAWN_LINKS) {
310
+ if (existsSync(join(sourceDir, name))) symlinkSync(join(sourceDir, name), join(dir, name))
311
+ }
312
+ return { env: { ...process.env, CURSOR_CONFIG_DIR: dir }, release }
313
+ } catch (error) {
314
+ release()
315
+ // 6.62.0 /qa (W6): fail open for these two read-only probes, but never silently.
316
+ console.warn(`[cursor-spawn-env] isolation unavailable for the launcher's agent probe, using the shared config: ${error && error.code ? error.code : error}`)
317
+ return { env: { ...process.env }, release: () => {} }
318
+ }
319
+ }
283
320
  function cursorCliState() {
284
321
  const binary = resolveCursorAgentBinary()
285
322
  if (!binary) return { binary: null, version: null, auth: null }
323
+ const isolation = cursorSpawnEnvSync()
324
+ try {
325
+ return cursorCliStateWith(binary, isolation.env)
326
+ } finally {
327
+ isolation.release()
328
+ }
329
+ }
330
+ function cursorCliStateWith(binary, env) {
286
331
 
287
332
  let version = 'available'
288
333
  try {
@@ -290,6 +335,7 @@ function cursorCliState() {
290
335
  encoding: 'utf8',
291
336
  stdio: ['ignore', 'pipe', 'pipe'],
292
337
  timeout: 5000,
338
+ env,
293
339
  }).trim()
294
340
  const versionLine = about.split('\n').map((line) => line.trim()).find((line) => /CLI Version|cursor|agent/i.test(line))
295
341
  if (versionLine) version = versionLine
@@ -300,6 +346,7 @@ function cursorCliState() {
300
346
  encoding: 'utf8',
301
347
  stdio: ['ignore', 'pipe', 'pipe'],
302
348
  timeout: 7000,
349
+ env,
303
350
  })
304
351
  // Cursor 2026.09 introduced `grok-4.7-high-fast` without the old
305
352
  // `cursor-` prefix. Accept both exact high-fast families, like runtime does.
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
- // Read-only native Cursor lifecycle observer. No network, credentials, commands or prompts.
2
+ // Read-only native Cursor lifecycle observer. No network, credentials, commands or prompts. An agent
3
+ // thought (6.62.0) is capped at 280 characters and display only: it sits in a 0600 spool file until
4
+ // the server drains it, then lives in memory and on the live feed, and is never written to the ledger.
3
5
  const fs=require('node:fs'),path=require('node:path'),crypto=require('node:crypto');
4
6
  const event=process.argv[2],spool=process.argv[3];
5
- const names={subagentStart:'SubagentStart',subagentStop:'SubagentStop',preCompact:'PreCompact',postToolUse:'PostToolUse',afterAgentResponse:'PostCompact',stop:'PostCompact',sessionEnd:'PostCompact'};
7
+ const names={subagentStart:'SubagentStart',subagentStop:'SubagentStop',preCompact:'PreCompact',postToolUse:'PostToolUse',afterAgentResponse:'PostCompact',stop:'PostCompact',sessionEnd:'PostCompact',afterAgentThought:'AgentThought'};
6
8
  let input='',oversize=false;
7
9
  process.stdin.setEncoding('utf8');
8
10
  process.stdin.on('data',chunk=>{if(input.length+chunk.length>1048576){oversize=true;input=''}else if(!oversize)input+=chunk});
@@ -13,13 +15,18 @@ process.stdin.on('end',()=>{
13
15
  const p=JSON.parse(input),id=p.parent_conversation_id||p.conversation_id||p.session_id;
14
16
  if(typeof id==='string'&&/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)){
15
17
  fs.mkdirSync(spool,{recursive:true,mode:0o700});
16
- if(fs.readdirSync(spool).length<2000){
18
+ // 6.62.0 (QA W16): the hook script's guard. A drain stamp older than a day means nobody is
19
+ // reading the spool, so nothing more is written into it (thoughts least of all).
20
+ let stale=false;try{stale=Date.now()-fs.statSync(path.join(spool,'.last-drain')).mtimeMs>86400000}catch{}
21
+ if(!stale&&fs.readdirSync(spool).length<2000){
17
22
  const ts=Date.now(),payload={session_id:id.toLowerCase(),display_only:true};
18
23
  const agent=p.subagent_id||p.agent_id;
19
24
  if(typeof agent==='string'&&/^[a-zA-Z0-9_-]{1,160}$/.test(agent))payload.agent_id=agent;
25
+ // 6.62.0: the thought itself, at most 280 characters. The server keeps it in memory only.
26
+ if(names[event]==='AgentThought'){if(typeof p.text!=='string'||!p.text.trim())throw 0;payload.text=p.text.slice(0,280)}
20
27
  const name=ts+'-'+process.pid+'-'+crypto.randomBytes(6).toString('hex')+'-'+names[event]+'.json';
21
28
  const temp=path.join(spool,'.'+name);
22
- fs.writeFileSync(temp,JSON.stringify({ts,ppid:null,event:names[event],payload}),{mode:0o600,flag:'wx'});
29
+ fs.writeFileSync(temp,JSON.stringify({ts,ppid:null,event:names[event],provider:'cursor',payload}),{mode:0o600,flag:'wx'});
23
30
  fs.renameSync(temp,path.join(spool,name));
24
31
  }
25
32
  }
@@ -1,5 +1,5 @@
1
1
  #!/bin/sh
2
- # COS session hook (glasses-server 6.48.0; Cursor Stop follow-up 6.51.0; halt check 6.53.0; single entry 6.53.3; away hold wait 6.60.0). Installed into ~/.claude/settings.json by
2
+ # COS session hook (glasses-server 6.48.0; Cursor Stop follow-up 6.51.0; halt check 6.53.0; single entry 6.53.3; away hold wait 6.60.0; Codex and Cursor parity 6.62.0). Installed into ~/.claude/settings.json by
3
3
  # `--hooks install`; Claude Code runs it with the hook event name as $1 and the event
4
4
  # payload on stdin. It spools one JSON file per event into the hook spool and exits 0
5
5
  # on every path. Only PermissionRequest ever talks to the server, and only when the
@@ -19,10 +19,19 @@
19
19
  #
20
20
  # The installer bakes COS_GLASSES_HOME and COS_HOOK_SPOOL into the command, so this
21
21
  # script and the server can never disagree about where the token or the spool is.
22
+ #
23
+ # 6.62.0: the same bytes also run from ~/.codex/hooks.json (as their own copy,
24
+ # cos-session-hook-codex), whose command adds COS_HOOK_PROVIDER=codex; a Codex payload
25
+ # reaching the Claude command is recognised too (codex_of, below). Codex changes three things
26
+ # only: the halt reply is deny-only (Codex rejects `continue:false` in a PreToolUse reply and
27
+ # then RUNS the tool, canary C10; the deny alone blocks it, C12), the spooled envelope says
28
+ # `"provider":"codex"`, and the Cursor Stop follow-up below never runs. Claude and Cursor
29
+ # share the Claude command and keep today's bytes; the server tells them apart by the payload.
22
30
  PATH=/usr/bin:/bin:/usr/sbin; umask 077; trap '' PIPE
23
31
  HOME=${HOME:-$(cd ~ 2>/dev/null && pwd)}; [ -n "$HOME" ] || exit 0
24
32
  GH=${COS_GLASSES_HOME:-$HOME/.cos-glasses}
25
33
  SPOOL=${COS_HOOK_SPOOL:-$GH/data/hook-spool}; EVENT="$1"
34
+ PROV=${COS_HOOK_PROVIDER:-}; case "$PROV" in claude|codex|cursor) ;; *) PROV='' ;; esac
26
35
  # 6.53.0, the halt check; 6.53.3, one entry for both synchronous events (review A, option a).
27
36
  # FIRST, before mkdir, perl and the B8 guard below: a full spool or a stale drain stamp must
28
37
  # never stop a cancel. The marker folder sits beside the spool, like the Cursor queue
@@ -38,13 +47,17 @@ case "$EVENT" in PreToolUse|UserPromptSubmit)
38
47
  HALT="${SPOOL%/*}/session-halt"
39
48
  # Any marker at all? A glob is a builtin, and it runs before stdin is touched.
40
49
  ANY=''; for F in "$HALT"/*; do [ -e "$F" ] && ANY=1; break; done
41
- # The FIRST session_id is the payload's own: Claude writes it first, and a tool's input
42
- # that happens to carry one comes later. Any JSON spacing; 36 hex-and-dash characters
43
- # inside a closed string, or nothing.
50
+ # The FIRST session_id is the payload's own: Claude and Codex write it first, and a tool's
51
+ # input that happens to carry one comes later. 6.62.0 (W7): Cursor writes
52
+ # "conversation_id" FIRST (byte 1, the same id) and its session_id ~600 bytes in, behind
53
+ # tool_input, so a large edit pushed it out of this read and an input naming another
54
+ # session matched first. Whichever of the two keys comes first is the payload's own. Any
55
+ # JSON spacing; 36 hex-and-dash characters inside a closed string, or nothing.
44
56
  sid_of() {
45
- SID=''
46
- case "$HEAD" in *'"session_id"'*)
47
- SID=${HEAD#*'"session_id"'}; SID=${SID#"${SID%%[!:[:space:]]*}"}
57
+ SID=''; KEY='"session_id"'; PRE=${HEAD%%'"session_id"'*}
58
+ case "$PRE" in *'"conversation_id"'*) KEY='"conversation_id"' ;; esac
59
+ case "$HEAD" in *"$KEY"*)
60
+ SID=${HEAD#*"$KEY"}; SID=${SID#"${SID%%[!:[:space:]]*}"}
48
61
  case "$SID" in '"'*'"'*) SID=${SID#?}; SID=${SID%%'"'*} ;; *) SID='' ;; esac ;;
49
62
  esac
50
63
  case "$SID" in *[!0-9A-Fa-f-]*) SID='' ;; esac
@@ -73,6 +86,20 @@ case "$EVENT" in PreToolUse|UserPromptSubmit)
73
86
  case "$TN" in '"AskUserQuestion"'*|'"ExitPlanMode"'*) NEED=1 ;; esac ;;
74
87
  esac
75
88
  fi
89
+ # 6.62.0 (QA W13): a COS block Codex imported from the Claude settings runs with no
90
+ # COS_HOOK_PROVIDER, and the Claude reply makes Codex fail open. Codex writes its rollout as
91
+ # the payload's SECOND key, right behind session_id; Claude writes its own transcript there,
92
+ # and the same key further in (a tool that reads a rollout) is not the payload's. Read only
93
+ # when a marker or a spool needs it, so a plain tool call stays one read and a `case`.
94
+ codex_of() {
95
+ case "$HEAD" in *'"session_id"'*'"transcript_path"'*) ;; *) return ;; esac
96
+ A=${HEAD#*'"session_id"'}; B=${A%%'"transcript_path"'*}
97
+ [ ${#B} -le 64 ] || return
98
+ TP=${A#*'"transcript_path"'}; TP=${TP#"${TP%%[!:[:space:]]*}"}
99
+ case "$TP" in '"'*) TP=${TP#?}; TP=${TP%%'"'*} ;; *) return ;; esac
100
+ case "$TP" in */.codex/sessions/*) PROV=codex ;; esac
101
+ }
102
+ [ -n "$PROV" ] || { [ -z "$ANY" ] && [ -z "$NEED" ]; } || codex_of
76
103
  case "$SID" in *[A-F]*) SID=$(printf '%s' "$SID" | tr 'A-F' 'a-f') ;; esac
77
104
  if [ -z "$SID" ]; then :
78
105
  elif [ "$EVENT" = UserPromptSubmit ]; then
@@ -82,7 +109,12 @@ case "$EVENT" in PreToolUse|UserPromptSubmit)
82
109
  # reading the id and clearing it. Then the prompt is spooled as before.
83
110
  rm -f "$HALT/$SID" 2>/dev/null
84
111
  elif [ -f "$HALT/$SID" ]; then
85
- printf '%s' '{"continue":false,"stopReason":"Cancelled from COS","hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Cancelled from COS"}}'
112
+ if [ "$PROV" = codex ]; then
113
+ # 6.62.0 (C10, C12): deny only, and a reason that tells the model to stop.
114
+ printf '%s' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Cancelled from COS. Stop and do not work around this."}}'
115
+ else
116
+ printf '%s' '{"continue":false,"stopReason":"Cancelled from COS","hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Cancelled from COS"}}'
117
+ fi
86
118
  exec cat >/dev/null
87
119
  fi
88
120
  # Every other tool call ends here with nothing written and nothing printed.
@@ -125,14 +157,17 @@ if [ "$EVENT" = PermissionRequest ]; then
125
157
  if [ ${#2} -le 4 ] && [ "$2" -ge 20 ] && [ "$2" -le 3600 ]; then WAIT=$2; STAMP_WAIT=",\"hookWaitS\":$2"; fi ;;
126
158
  esac
127
159
  fi
128
- { printf '{"ts":%s,"ppid":%s,"event":"%s"%s,"payload":' "$TS" "$PPID" "$EVENT" "$STAMP_WAIT"; cat "$T.in"; printf '}\n'; } > "$T"
160
+ # 6.62.0: the provider, when the command said it (only the Codex command does).
161
+ STAMP_PROV=''; [ -z "$PROV" ] || STAMP_PROV=",\"provider\":\"$PROV\""
162
+ { printf '{"ts":%s,"ppid":%s,"event":"%s"%s%s,"payload":' "$TS" "$PPID" "$EVENT" "$STAMP_WAIT" "$STAMP_PROV"; cat "$T.in"; printf '}\n'; } > "$T"
129
163
  rm -f "$T.in"
130
164
  # 6.51.0, CURSOR ONLY. Cursor runs this hook from ~/.claude/settings.json for its own
131
165
  # composers, and submits a Stop hook's {"followup_message":...} as that composer's next
132
166
  # message. A turn COS queued for the composer is handed over here, at the end of its turn.
133
167
  # Claude Code sets no CURSOR_VERSION and sends no cursor_version, so a Claude Stop takes
134
- # the line below this block exactly as before and never touches the network.
135
- if [ "$EVENT" = Stop ] && [ -n "${CURSOR_VERSION:-}" ] && grep -Eq '"cursor_version"[[:space:]]*:[[:space:]]*"' "$T" 2>/dev/null; then
168
+ # the line below this block exactly as before and never touches the network. 6.62.0: nor
169
+ # does a Codex Stop, whatever its environment says: a follow-up is Cursor-only output.
170
+ if [ "$EVENT" = Stop ] && [ "$PROV" != codex ] && [ -n "${CURSOR_VERSION:-}" ] && grep -Eq '"cursor_version"[[:space:]]*:[[:space:]]*"' "$T" 2>/dev/null; then
136
171
  cp "$T" "$T.req" 2>/dev/null; mv "$T" "$SPOOL/$TS-$$-$EVENT.json"
137
172
  # Nothing queued for this composer (almost every Stop): no request at all. The queue
138
173
  # folder sits beside the spool. If it is not there, or the id cannot be read here,
@@ -0,0 +1,43 @@
1
+ # Provider parity (6.62.0)
2
+
3
+ What the server promises about Claude, Codex and Cursor desk sessions, and where each promise stops.
4
+
5
+ ## Contract on the wire
6
+
7
+ - `GET /api/health` and `GET /api/models`: `providers.<claude|codex|cursor>.act` (`continueIdle`, `continueBusy`, `fork`, `cancelDesk`, `approvals`, `nativeQueue`, `keepModel`, `permissions`, each false with a reason code). Both routes also carry `providers.<p>.observe` (`hooks`, `liveState`, `children`, `compaction`, `reasoning`).
8
+ - `threadForkSupported` stays a boolean; `threadForkProviders` lists who can fork.
9
+ - `features.sessionCancel`: `cosTurn`, `deskClaude`, `deskCodex`, `deskCursor`.
10
+ - `sessionHooks`: `state` / `installed` are Claude's (Control's banner reads them). `sessionHooks.codex = {present, installed, trust, scriptOk}`; `sessionHooks.cursorObserver = {installed, nodeOk}`.
11
+ - Session rows: `running` and `running_active`; detail additionally carries `reported_model` and `continue_note` (≤60 chars). List polling does not wait for model lookups.
12
+
13
+ ## Hooks
14
+
15
+ | Engine | Where | Trust | Turn start / end |
16
+ |---|---|---|---|
17
+ | Claude | `~/.claude/settings.json` | n/a | UserPromptSubmit / Stop, registry idle flip |
18
+ | Codex | `~/.codex/hooks.json` (COS blocks replaced in place) | Codex `hooks/list`; untrusted hooks never run | UserPromptSubmit / Stop, Interrupt |
19
+ | Cursor | the Claude hooks (third-party extensibility) + `~/.cursor/hooks.json` observer | n/a | IDE: UserPromptSubmit / Stop; CLI `-p` fires neither |
20
+
21
+ The halt reply is provider-specific: Claude and Cursor keep the 6.53 bytes; Codex gets deny-only (it fails open on `continue:false`).
22
+
23
+ ## Continue posture
24
+
25
+ | Engine | Continue runs with | Fallback |
26
+ |---|---|---|
27
+ | Claude | the session itself (live inbox) or `claude -p --resume` with the user's default mode | unchanged |
28
+ | Codex | session `-m`, effort, `-s` from the latest `turn_context`; `danger-full-access` only in a trusted folder | 6.61 posture + note |
29
+ | Cursor | `--force --model <session slug>` only after client note acknowledgement, on its own config dir | composer-2.5-fast + note |
30
+
31
+ `COS_CONTINUE_FULL_PERMISSIONS=0` restores the 6.61 argv. Fork is read-only for every engine.
32
+
33
+ ## Compatibility and limits
34
+
35
+ Cursor Run Everything requires `X-COS-Continue-Note: 1` on the turn request. Queue admission persists the acknowledgement and replays it on drain. Clients without it retain Ask mode. Continue/Fork refuse if private Cursor configuration isolation fails.
36
+
37
+ Run `codex` in Terminal to review and trust the installed hooks. COS reads hook trust through a bounded app-server call every five minutes; it does not write hook trust. Codex hooks use their own script copy. Desk Cancel requires observed hooks for the specific thread, not merely an installed script. Continue only uses danger-full-access for an already trusted working directory or its canonical Git root, rechecked at spawn; a trusted arbitrary parent does not qualify. Starting new Codex sessions may cause Codex itself to write project trust; resumed sessions do not.
38
+
39
+ Busy Codex Continue accepts the moving head and enters the native app queue, which is not exposed in COS Queue. Cursor CLI queued turns start when the chat is free and use the saved note acknowledgement. Cursor Fork creates a new read-only chat with recent context. Cursor IDE composers cannot accept outside messages, and Cursor CLI approvals cannot be answered remotely.
40
+
41
+ Cursor reasoning is briefly on disk in the observer spool until drained, then in memory and the live stream, never the ledger. A stale drain stops new thought writes. Codex rollout compaction completion closes an observed phase; start needs hook evidence. Cursor open turns quiet beyond five minutes remain OPEN until the 30-minute ceiling. Claude/Codex completion can lag by 30 seconds.
42
+
43
+ Set `COS_CONTINUE_FULL_PERMISSIONS=0` in `~/.cos-glasses/.env` and restart to restore the previous posture; Control does not yet carry this key through updates. Before rolling the server back to 6.61.7, use the pinned 6.62.0 CLI to remove Codex hooks (`--hooks uninstall --codex`), then use Control rollback and reinstall the older hooks. Do not run the old CLI with `--codex`: it does not understand that flag.
@@ -30,6 +30,7 @@
30
30
  "COS_SCRIPTS_DIR",
31
31
  "COS_SESSION_HOOKS",
32
32
  "COS_CONTINUE_LIVE",
33
+ "COS_CONTINUE_FULL_PERMISSIONS",
33
34
  "COS_SESSION_HOOKS_SPOOL_DIR",
34
35
  "COS_SESSION_HOOK_SSE",
35
36
  "COS_PERMISSION_BROKER",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.61.7",
3
+ "version": "6.63.0",
4
4
  "description": "COS Glasses: self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, Cursor Agent CLI, or local Ollama",
5
5
  "type": "module",
6
6
  "bin": {
@@ -50,6 +50,7 @@
50
50
  "!server/scripts/nemotron-mutation-gate.mjs",
51
51
  "!server/scripts/merged-scribe-mutation-gate.mjs",
52
52
  "!server/scripts/nemotron-replay.ts",
53
+ "!server/scripts/turn-request-mutation-gate.mjs",
53
54
  "!server/scripts/mutation-gate.mjs",
54
55
  "!server/lib/meeting-engine/__fixtures__/**",
55
56
  "!server/lib/__fixtures__/**",
@@ -74,7 +75,8 @@
74
75
  "work-task-runtime",
75
76
  "docs/work-task-runtime.md",
76
77
  "!server/scripts/build-work-task-runtime.py",
77
- "bin/hooks/cos-cursor-observer.cjs"
78
+ "bin/hooks/cos-cursor-observer.cjs",
79
+ "docs/provider-parity.md"
78
80
  ],
79
81
  "author": "COS Contributors",
80
82
  "license": "MIT",