@gotcos/glasses-server 6.61.5 → 6.61.6
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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## 6.61.6 — 2026-10-03
|
|
2
|
+
|
|
3
|
+
- Keep Codex-owned waiting messages visible in Sessions queue counts and queue reads after the COS handoff. Unavailable native reads report unknown, not empty.
|
|
4
|
+
- Edit existing COS and Codex queue slots in place. Return full text for authenticated queue reads; never cancel and resend as an edit. Native messages with attachments or structured spans remain manageable in Codex.
|
|
5
|
+
- Preserve concurrent enqueue, cancel and edit changes while the drainer awaits delivery. Native management uses local Codex queue protocol calls with automation and memory jobs disabled; no model turn is started.
|
|
6
|
+
|
|
7
|
+
|
|
1
8
|
## 6.61.5
|
|
2
9
|
|
|
3
10
|
Local dictation-choice candidate, based on the 6.61.4 hardening candidate.
|
|
@@ -7,6 +14,7 @@ Local dictation-choice candidate, based on the 6.61.4 hardening candidate.
|
|
|
7
14
|
- Run cleanup in a temporary workspace with no MCP. Claude tools/hooks are disabled; Cursor ask mode retains its read tools and uses an isolated subscription profile. Abort/timeout terminates the process tree; failures preserve the glossary-corrected transcript. Model circuit breakers are independent.
|
|
8
15
|
- Local verification: model routing, legacy/default behavior, failure recovery, isolated configuration, cancellation, and real signed-in CLI smoke calls. Hardware acceptance and stable promotion remain outstanding.
|
|
9
16
|
|
|
17
|
+
|
|
10
18
|
## 6.61.4
|
|
11
19
|
|
|
12
20
|
Local hardening candidate; stable promotion remains gated on native UI and G2 acceptance.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.61.
|
|
3
|
+
"version": "6.61.6",
|
|
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": {
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Queue management only. Never loads/starts a thread or starts a model turn.
|
|
2
|
+
// Protocol verified from the installed Codex generate-ts schema (2026-10-03).
|
|
3
|
+
import { spawn, execFile } from 'node:child_process'
|
|
4
|
+
import { promisify } from 'node:util'
|
|
5
|
+
import { existsSync } from 'node:fs'
|
|
6
|
+
import { homedir } from 'node:os'
|
|
7
|
+
import { join } from 'node:path'
|
|
8
|
+
import { resolveProviderBinary } from './provider-binary.js'
|
|
9
|
+
|
|
10
|
+
export interface NativeQueuedSubmission { id: string; input: Array<{ type: string; text?: string; text_elements?: unknown[] }>; clientUserMessageId: string }
|
|
11
|
+
export interface NativeQueueControl {
|
|
12
|
+
list(threadId: string): Promise<NativeQueuedSubmission[]>
|
|
13
|
+
update(threadId: string, id: string, prompt: string): Promise<void>
|
|
14
|
+
cancel(threadId: string, id: string): Promise<boolean>
|
|
15
|
+
}
|
|
16
|
+
const METHODS = new Set(['thread/queue/list', 'thread/queue/update', 'thread/queue/delete'])
|
|
17
|
+
export function codexQueueRpc(method: string, params: Record<string, unknown>, binary?: string): Promise<any> {
|
|
18
|
+
if (!METHODS.has(method)) return Promise.reject(new Error('queue_method_not_allowed'))
|
|
19
|
+
const resolved = binary ? { ok: true as const, path: binary } : resolveProviderBinary('codex')
|
|
20
|
+
if (!resolved.ok) return Promise.reject(new Error('codex_queue_unavailable'))
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
// A separate, short-lived protocol client: automations and memories explicitly
|
|
23
|
+
// disabled. No thread/start, turn/start or queue/start call is allowed here.
|
|
24
|
+
const child = spawn(resolved.path, ['app-server', '--disable', 'in_app_local_automation', '--disable', 'memories'], { stdio: ['pipe', 'pipe', 'ignore'], shell: false })
|
|
25
|
+
let buffer = '', done = false, initialized = false
|
|
26
|
+
const finish = (error: Error | null, value?: unknown) => {
|
|
27
|
+
if (done) return
|
|
28
|
+
done = true; clearTimeout(timer); child.kill(); error ? reject(error) : resolve(value)
|
|
29
|
+
}
|
|
30
|
+
const timer = setTimeout(() => finish(new Error('codex_queue_timeout')), 8000)
|
|
31
|
+
const send = (payload: unknown) => child.stdin.write(JSON.stringify(payload) + '\n')
|
|
32
|
+
child.on('error', () => finish(new Error('codex_queue_unavailable')))
|
|
33
|
+
child.stdin.on('error', () => finish(new Error('codex_queue_unavailable')))
|
|
34
|
+
child.on('exit', () => finish(new Error('codex_queue_closed')))
|
|
35
|
+
child.stdout.on('data', data => {
|
|
36
|
+
buffer += data.toString()
|
|
37
|
+
if (buffer.length > 4 * 1024 * 1024) { finish(new Error('codex_queue_response_too_large')); return }
|
|
38
|
+
let end: number
|
|
39
|
+
while ((end = buffer.indexOf('\n')) >= 0) {
|
|
40
|
+
const line = buffer.slice(0, end); buffer = buffer.slice(end + 1)
|
|
41
|
+
let row: any
|
|
42
|
+
try { row = JSON.parse(line) } catch { continue }
|
|
43
|
+
if (row.id !== 1 && row.id !== 2) continue
|
|
44
|
+
if (row.error) { finish(new Error('codex_queue_request_refused')); return }
|
|
45
|
+
if (row.id === 1 && !initialized) {
|
|
46
|
+
initialized = true
|
|
47
|
+
send({ method: 'initialized' })
|
|
48
|
+
send({ id: 2, method, params })
|
|
49
|
+
} else if (row.id === 2) { finish(null, row.result); return }
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
send({ id: 1, method: 'initialize', params: { clientInfo: { name: 'cos-queue-control', version: '1' }, capabilities: { experimentalApi: true, explicitGatewayOauth: true } } })
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function plainQueueText(row: NativeQueuedSubmission): string | null {
|
|
57
|
+
// Never turn a message containing files, mentions or structured text into plain text.
|
|
58
|
+
return Array.isArray(row.input) && row.input.length === 1 && row.input[0]?.type === 'text'
|
|
59
|
+
&& typeof row.input[0].text === 'string' && !row.input[0].text_elements?.length ? row.input[0].text : null
|
|
60
|
+
}
|
|
61
|
+
export const nativeQueueControl: NativeQueueControl = {
|
|
62
|
+
async list(threadId) {
|
|
63
|
+
const rows: NativeQueuedSubmission[] = []
|
|
64
|
+
let cursor: string | null = null
|
|
65
|
+
const seen = new Set<string>()
|
|
66
|
+
do {
|
|
67
|
+
const result = await codexQueueRpc('thread/queue/list', { threadId, cursor, limit: 100 })
|
|
68
|
+
if (!Array.isArray(result?.data) || result.data.some((r: any) => typeof r?.id !== 'string' || !Array.isArray(r.input))) throw new Error('codex_queue_bad_response')
|
|
69
|
+
rows.push(...result.data)
|
|
70
|
+
cursor = result.nextCursor ?? null
|
|
71
|
+
if (cursor && (seen.has(cursor) || rows.length >= 1000)) throw new Error('codex_queue_incomplete')
|
|
72
|
+
if (cursor) seen.add(cursor)
|
|
73
|
+
} while (cursor)
|
|
74
|
+
return rows
|
|
75
|
+
},
|
|
76
|
+
async update(threadId, id, prompt) {
|
|
77
|
+
const current = (await this.list(threadId)).find(r => r.id === id)
|
|
78
|
+
if (!current || plainQueueText(current) === null) throw new Error('queue_not_editable')
|
|
79
|
+
const result = await codexQueueRpc('thread/queue/update', { threadId, queuedSubmissionId: id, input: [{ type: 'text', text: prompt, text_elements: [] }] })
|
|
80
|
+
if (result?.queuedSubmission?.id !== id) throw new Error('codex_queue_bad_response')
|
|
81
|
+
},
|
|
82
|
+
async cancel(threadId, id) {
|
|
83
|
+
const result = await codexQueueRpc('thread/queue/delete', { threadId, queuedSubmissionId: id })
|
|
84
|
+
if (typeof result?.deleted !== 'boolean') throw new Error('codex_queue_bad_response')
|
|
85
|
+
return result.deleted
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// One read-only aggregate per session-list request, not 80 protocol processes.
|
|
90
|
+
// No queue file is ever edited via SQL; mutations use Codex's own protocol above.
|
|
91
|
+
export async function nativeQueuedCounts(): Promise<Map<string, number> | null> {
|
|
92
|
+
const db = join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'queue_1.sqlite')
|
|
93
|
+
if (!existsSync(db)) return new Map()
|
|
94
|
+
try {
|
|
95
|
+
const { stdout } = await promisify(execFile)('/usr/bin/sqlite3', ['-readonly', '-json', db, 'SELECT thread_id, count(*) AS n FROM queued_items GROUP BY thread_id'], { timeout: 2000, maxBuffer: 1024 * 1024 })
|
|
96
|
+
const rows = JSON.parse(stdout || '[]')
|
|
97
|
+
if (!Array.isArray(rows) || rows.some(r => typeof r.thread_id !== 'string' || !Number.isInteger(r.n))) return null
|
|
98
|
+
return new Map(rows.map(r => [r.thread_id, r.n]))
|
|
99
|
+
} catch { return null }
|
|
100
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nativeQueuedCounts } from './codex-queue-control.js'
|
|
1
2
|
// Durable storage for queued thread turns, and the terminal-record probe.
|
|
2
3
|
//
|
|
3
4
|
// SEPARATE FROM THE DECISIONS. `thread-turn-queue.ts` is pure and holds every rule;
|
|
@@ -104,8 +105,8 @@ export function queuedWaitingLookup(now: number): (provider: string, sessionId:
|
|
|
104
105
|
return (provider, sessionId) => queuedWaitingForSession(queues, provider, sessionId)
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
export function queuedTurnsFields(count: number): { queued_turns?: number } {
|
|
108
|
-
return count > 0 ? { queued_turns: count } : {}
|
|
108
|
+
export function queuedTurnsFields(count: number | null): { queued_turns?: number; queue_unknown?: boolean } {
|
|
109
|
+
return count === null ? { queue_unknown: true } : count > 0 ? { queued_turns: count } : {}
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
/** Every thread with a queue file, for the drain sweep. */
|
|
@@ -182,3 +183,11 @@ export function readTranscriptTailLines(path: string | null, maxBytes: number):
|
|
|
182
183
|
return null
|
|
183
184
|
}
|
|
184
185
|
}
|
|
186
|
+
|
|
187
|
+
/** Native queued submissions remain waiting after COS has handed them over. */
|
|
188
|
+
export async function allQueuedWaitingLookup(now: number): Promise<(provider: string, id: string) => number | null> {
|
|
189
|
+
const cos = queuedWaitingLookup(now)
|
|
190
|
+
const native = await nativeQueuedCounts()
|
|
191
|
+
return (provider, id) => provider !== 'codex' ? cos(provider, id)
|
|
192
|
+
: native === null ? null : cos(provider, id) + (native.get(id) ?? 0)
|
|
193
|
+
}
|
|
@@ -43,7 +43,7 @@ import { claudeSessionNamesVisible, claudeSessionsDir, claudeSessionsEnabled, re
|
|
|
43
43
|
import type { ClaudePeerRecord } from '../lib/claude-session-registry.js'
|
|
44
44
|
import { deriveForRow, signalFor } from '../lib/session-hooks-runtime.js'
|
|
45
45
|
import { derivedRowFields, type DerivedSessionState } from '../lib/session-state-derive.js'
|
|
46
|
-
import { queuedTurnsFields,
|
|
46
|
+
import { queuedTurnsFields, allQueuedWaitingLookup } from '../lib/thread-turn-queue-store.js'
|
|
47
47
|
import { isAttachedTurnActive, sessionStreamKey } from '../lib/session-stream-bus.js'
|
|
48
48
|
import { workspaceFromCwd } from '../lib/claude-session-registry.js'
|
|
49
49
|
import {
|
|
@@ -83,7 +83,7 @@ function asSort(value: unknown): AgentSessionSort {
|
|
|
83
83
|
return String(value ?? '').toLowerCase() === 'opened' ? 'opened' : 'updated'
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
function toSearchHit(row: AgentSessionSearchHit, queuedTurns = 0, derived?: DerivedSessionState) {
|
|
86
|
+
function toSearchHit(row: AgentSessionSearchHit, queuedTurns: number | null = 0, derived?: DerivedSessionState) {
|
|
87
87
|
return {
|
|
88
88
|
...toEntry(row, undefined, derived, queuedTurns),
|
|
89
89
|
snippet: row.snippet,
|
|
@@ -269,7 +269,7 @@ function runningForThread(provider: AgentProvider, threadId: string, mtimeMs: nu
|
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
-
function toEntry(row: AgentSessionRow, activity?: SessionActivity | null, derived?: DerivedSessionState, queuedTurns = 0) {
|
|
272
|
+
function toEntry(row: AgentSessionRow, activity?: SessionActivity | null, derived?: DerivedSessionState, queuedTurns: number | null = 0) {
|
|
273
273
|
return {
|
|
274
274
|
session_id: row.session_id,
|
|
275
275
|
provider: row.provider,
|
|
@@ -397,7 +397,7 @@ agentSessionsRouter.get('/agent-sessions', async (req, res) => {
|
|
|
397
397
|
if (row.provider !== 'cursor' || !signalFor(row.session_id)) continue
|
|
398
398
|
derivedById.set(row.session_id, deriveForRow({ sessionId: row.session_id, now, remember: false }))
|
|
399
399
|
}
|
|
400
|
-
const queuedOf =
|
|
400
|
+
const queuedOf = await allQueuedWaitingLookup(now)
|
|
401
401
|
res.json({
|
|
402
402
|
sessions: sessions.map((row, index) => withRunning(toEntry(row, activity[index], derivedById.get(row.session_id), queuedOf(row.provider, row.session_id)), running)),
|
|
403
403
|
total: sessions.length,
|
|
@@ -428,7 +428,7 @@ agentSessionsRouter.get('/agent-sessions/search', async (req, res) => {
|
|
|
428
428
|
try {
|
|
429
429
|
const limit = boundedInteger(req.query.limit, 20, 1, 50)
|
|
430
430
|
const result = await searchAgentSessions({ query, limit })
|
|
431
|
-
const queuedOf =
|
|
431
|
+
const queuedOf = await allQueuedWaitingLookup(Date.now())
|
|
432
432
|
// 6.48.1: search hits carry the same derived state as list rows (no list/search shape
|
|
433
433
|
// drift), from the registry and the hook signal; a hit has no transcript walk.
|
|
434
434
|
const peers = await liveClaudePeerRecords()
|
|
@@ -532,7 +532,7 @@ agentSessionsRouter.get('/agent-sessions/:provider/:sessionId', async (req, res)
|
|
|
532
532
|
res.json({
|
|
533
533
|
...withRunning({ session_id: parsed.session_id }, running),
|
|
534
534
|
...derivedRowFields(derived),
|
|
535
|
-
...queuedTurnsFields(
|
|
535
|
+
...queuedTurnsFields((await allQueuedWaitingLookup(Date.now()))(provider, parsed.session_id)),
|
|
536
536
|
// The client must be able to tell "this server stamped nothing" from "this
|
|
537
537
|
// server stamped false", because the two demand opposite behaviour: an old
|
|
538
538
|
// server's silence means keep using the hint borrowed from the list row, and
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
// EVERY DEPENDENCY IS INJECTED so the whole path is testable without a live server.
|
|
17
17
|
|
|
18
18
|
import { Router, type Request, type Response } from 'express'
|
|
19
|
-
import {
|
|
19
|
+
import { nativeQueueControl, plainQueueText, type NativeQueueControl } from '../lib/codex-queue-control.js'
|
|
20
|
+
import { COS_SESSION_ID_RE, DEFAULT_MAX_PROMPT_CHARS } from './agent-session-bindings.js'
|
|
20
21
|
import {
|
|
21
22
|
admitToQueue, drainDecision, queueableRefusal, queuePosition,
|
|
22
23
|
MAX_DELIVERY_ATTEMPTS, type DrainObservation, type QueuedThreadTurn,
|
|
@@ -47,6 +48,7 @@ export interface ThreadTurnQueueDeps {
|
|
|
47
48
|
/** The turn route's OWN `retryable` judgement, when it sent one. Authoritative. */
|
|
48
49
|
serverRetryable?: boolean
|
|
49
50
|
}>
|
|
51
|
+
nativeQueue?: NativeQueueControl
|
|
50
52
|
now: () => number
|
|
51
53
|
}
|
|
52
54
|
|
|
@@ -58,6 +60,9 @@ function publicRow(turn: QueuedThreadTurn, position: number): Record<string, unk
|
|
|
58
60
|
queuedAt: turn.queuedAt,
|
|
59
61
|
attempts: turn.attempts,
|
|
60
62
|
position: turn.status === 'waiting' ? position : -1,
|
|
63
|
+
prompt: turn.prompt,
|
|
64
|
+
editable: turn.status === 'waiting',
|
|
65
|
+
owner: 'cos',
|
|
61
66
|
preview: turn.prompt.length > 80 ? `${turn.prompt.slice(0, 79)}…` : turn.prompt,
|
|
62
67
|
...(turn.reason ? { reason: turn.reason } : {}),
|
|
63
68
|
...(turn.settledAt ? { settledAt: turn.settledAt } : {}),
|
|
@@ -116,10 +121,16 @@ export async function drainThread(
|
|
|
116
121
|
if (queue.length === 0) return { delivered: 0, held: 0, retired: 0 }
|
|
117
122
|
|
|
118
123
|
let delivered = 0, held = 0, retired = 0
|
|
119
|
-
|
|
124
|
+
const persist = (turn: QueuedThreadTurn) => {
|
|
125
|
+
const latest = readQueue(provider, threadId, deps.now())
|
|
126
|
+
const index = latest.findIndex(t => t.clientTurnId === turn.clientTurnId)
|
|
127
|
+
if (index >= 0) { latest[index] = turn; writeQueue(provider, threadId, latest) }
|
|
128
|
+
}
|
|
120
129
|
let refusedGate: ReturnType<ThreadTurnQueueDeps['occupancy']> | null = null
|
|
121
130
|
|
|
122
|
-
for (const
|
|
131
|
+
for (const snapshot of queue) {
|
|
132
|
+
const turn = readQueue(provider, threadId, deps.now()).find(t => t.clientTurnId === snapshot.clientTurnId)
|
|
133
|
+
if (!turn) continue
|
|
123
134
|
if (turn.status !== 'waiting') continue
|
|
124
135
|
|
|
125
136
|
// Reuse only refusal within this synchronous pass. Rechecking a busy thread
|
|
@@ -150,7 +161,7 @@ export async function drainThread(
|
|
|
150
161
|
turn.status = decision === 'expire' ? 'expired' : 'refused'
|
|
151
162
|
turn.reason = decision === 'expire' ? 'queued_turn_expired' : 'delivery_attempts_exhausted'
|
|
152
163
|
turn.settledAt = deps.now()
|
|
153
|
-
retired += 1;
|
|
164
|
+
retired += 1; persist(turn)
|
|
154
165
|
continue
|
|
155
166
|
}
|
|
156
167
|
|
|
@@ -159,8 +170,7 @@ export async function drainThread(
|
|
|
159
170
|
// forever; the ceiling only bounds anything if it survives the crash it is bounding.
|
|
160
171
|
turn.attempts += 1
|
|
161
172
|
turn.status = 'delivering'
|
|
162
|
-
|
|
163
|
-
dirty = true
|
|
173
|
+
persist(turn)
|
|
164
174
|
|
|
165
175
|
let outcome: { ok: boolean; reason?: string; serverRetryable?: boolean }
|
|
166
176
|
refusedGate = null
|
|
@@ -209,9 +219,8 @@ export async function drainThread(
|
|
|
209
219
|
turn.reason = outcome.reason
|
|
210
220
|
held += 1
|
|
211
221
|
}
|
|
222
|
+
persist(turn)
|
|
212
223
|
}
|
|
213
|
-
|
|
214
|
-
if (dirty) writeQueue(provider, threadId, queue)
|
|
215
224
|
return { delivered, held, retired }
|
|
216
225
|
}
|
|
217
226
|
|
|
@@ -286,20 +295,59 @@ export function createThreadTurnQueueRouter(deps: ThreadTurnQueueDeps): Router {
|
|
|
286
295
|
})
|
|
287
296
|
|
|
288
297
|
// GET — what is waiting, for the pending row on the lens.
|
|
289
|
-
router.get('/agent-sessions/:provider/:threadId/queued-turns', (req: Request, res: Response) => {
|
|
298
|
+
router.get('/agent-sessions/:provider/:threadId/queued-turns', async (req: Request, res: Response) => {
|
|
290
299
|
res.set('Cache-Control', 'private, no-store')
|
|
291
300
|
const provider = String(req.params.provider ?? '')
|
|
292
301
|
const threadId = String(req.params.threadId ?? '')
|
|
293
302
|
const queue = readQueue(provider, threadId, deps.now())
|
|
294
|
-
|
|
303
|
+
let native: Record<string, unknown>[] = []
|
|
304
|
+
if (provider === 'codex') {
|
|
305
|
+
try {
|
|
306
|
+
native = (await (deps.nativeQueue ?? nativeQueueControl).list(threadId)).map((row, position) => {
|
|
307
|
+
const text = plainQueueText(row)
|
|
308
|
+
return { clientTurnId: `codex-native:${row.id}`, status: 'waiting', position, queuedAt: 0,
|
|
309
|
+
attempts: 0, preview: text?.slice(0, 80) ?? 'Message with attachments · manage in Codex',
|
|
310
|
+
...(text !== null ? { prompt: text } : {}), editable: text !== null, owner: 'codex' }
|
|
311
|
+
})
|
|
312
|
+
} catch { return res.status(503).json({ error: 'native_queue_unavailable' }) }
|
|
313
|
+
}
|
|
314
|
+
// Re-read after the native await: delivery may have changed COS ownership.
|
|
315
|
+
const current = readQueue(provider, threadId, deps.now())
|
|
316
|
+
return res.json({ turns: [...native, ...current.map(t => publicRow(t, queuePosition(current, t.clientTurnId) + native.length))] })
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
// Atomic replacement: no cancel-and-requeue window, and the slot is retained.
|
|
320
|
+
router.patch('/agent-sessions/:provider/:threadId/queued-turns/:clientTurnId', async (req: Request, res: Response) => {
|
|
321
|
+
res.set('Cache-Control', 'private, no-store')
|
|
322
|
+
const provider = String(req.params.provider), threadId = String(req.params.threadId), id = String(req.params.clientTurnId)
|
|
323
|
+
const prompt = req.body?.prompt
|
|
324
|
+
if (typeof prompt !== 'string' || !prompt.trim() || prompt.length > DEFAULT_MAX_PROMPT_CHARS) return res.status(400).json({ error: 'invalid_prompt' })
|
|
325
|
+
if (provider === 'codex' && id.startsWith('codex-native:')) {
|
|
326
|
+
try {
|
|
327
|
+
await (deps.nativeQueue ?? nativeQueueControl).update(threadId, id.slice(13), prompt)
|
|
328
|
+
return res.json({ updated: true })
|
|
329
|
+
} catch { return res.status(409).json({ error: 'queue_edit_unconfirmed' }) }
|
|
330
|
+
}
|
|
331
|
+
const queue = readQueue(provider, threadId, deps.now())
|
|
332
|
+
const row = queue.find(t => t.clientTurnId === id)
|
|
333
|
+
if (!row || row.status !== 'waiting') return res.status(409).json({ error: 'already_delivering' })
|
|
334
|
+
row.prompt = prompt
|
|
335
|
+
writeQueue(provider, threadId, queue)
|
|
336
|
+
return res.json({ updated: true })
|
|
295
337
|
})
|
|
296
338
|
|
|
297
339
|
// DELETE — the cancel control, the same affordance the desktop queue offers.
|
|
298
|
-
router.delete('/agent-sessions/:provider/:threadId/queued-turns/:clientTurnId', (req: Request, res: Response) => {
|
|
340
|
+
router.delete('/agent-sessions/:provider/:threadId/queued-turns/:clientTurnId', async (req: Request, res: Response) => {
|
|
299
341
|
res.set('Cache-Control', 'private, no-store')
|
|
300
342
|
const provider = String(req.params.provider ?? '')
|
|
301
343
|
const threadId = String(req.params.threadId ?? '')
|
|
302
344
|
const clientTurnId = String(req.params.clientTurnId ?? '')
|
|
345
|
+
if (provider === 'codex' && clientTurnId.startsWith('codex-native:')) {
|
|
346
|
+
try {
|
|
347
|
+
const cancelled = await (deps.nativeQueue ?? nativeQueueControl).cancel(threadId, clientTurnId.slice(13))
|
|
348
|
+
return res.status(cancelled ? 200 : 409).json(cancelled ? { cancelled: true, clientTurnId, status: 'cancelled' } : { error: 'already_delivering' })
|
|
349
|
+
} catch { return res.status(503).json({ error: 'native_queue_unavailable' }) }
|
|
350
|
+
}
|
|
303
351
|
const now = deps.now()
|
|
304
352
|
const queue = readQueue(provider, threadId, now)
|
|
305
353
|
const row = queue.find(t => t.clientTurnId === clientTurnId)
|