@gotcos/glasses-server 6.8.0 → 6.10.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.
- package/.env.example +8 -0
- package/CHANGELOG.md +44 -0
- package/README.md +13 -2
- package/package.json +1 -1
- package/server/index.ts +40 -11
- package/server/lib/claude-bridge.ts +115 -12
- package/server/lib/codex-bridge.ts +151 -28
- package/server/lib/conversation.ts +194 -1
- package/server/lib/display-bus.ts +1 -1
- package/server/lib/model-router.ts +6 -6
- package/server/lib/query-job-coordinator.ts +580 -0
- package/server/lib/query-job-feature.ts +20 -0
- package/server/lib/query-job-runtime.ts +255 -0
- package/server/lib/query-job-store.ts +1102 -0
- package/server/lib/query-job-types.ts +358 -0
- package/server/lib/run-output-images.ts +44 -0
- package/server/routes/health.ts +62 -2
- package/server/routes/prompt-drafts.ts +12 -1
- package/server/routes/query-jobs.ts +294 -0
package/.env.example
CHANGED
|
@@ -28,6 +28,14 @@ BIND_HOST=0.0.0.0
|
|
|
28
28
|
# server after Wi-Fi/Tailscale changes and process restarts.
|
|
29
29
|
# COS_SERVER_INSTANCE_ID_PATH=/path/to/server-instance-id
|
|
30
30
|
|
|
31
|
+
# Build 204+ can opt into server-owned durable query jobs. The server fsyncs an
|
|
32
|
+
# accepted prompt before returning 202, then keeps provider work running if the
|
|
33
|
+
# phone backgrounds, reloads, or changes networks. Reopening COS reattaches to
|
|
34
|
+
# the same job. Set to 0 (or remove) to send NEW prompts through the legacy
|
|
35
|
+
# streaming route; already accepted durable jobs remain readable/cancellable
|
|
36
|
+
# and drain safely.
|
|
37
|
+
# COS_DURABLE_QUERY_JOBS=1
|
|
38
|
+
|
|
31
39
|
# ── THE LLM (chat) ──────────────────────────────────────────────────────
|
|
32
40
|
# Chat runs through your LOCAL agent CLI — NOT an API key:
|
|
33
41
|
# Opus / Fable / Sonnet -> Claude Code CLI (https://claude.ai/download, then `claude login`)
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 6.10.0
|
|
4
|
+
|
|
5
|
+
Opt-in server-owned durable query jobs for COS Glasses build 204+.
|
|
6
|
+
|
|
7
|
+
- **Accepted means durable.** With `COS_DURABLE_QUERY_JOBS=1`, the server
|
|
8
|
+
appends and fsyncs an immutable job before returning 202. Provider execution
|
|
9
|
+
is no longer owned by the phone's current request, WebView, or SSE subscriber.
|
|
10
|
+
- **Reconnect without duplication.** The client can recover an ambiguous
|
|
11
|
+
admission by its stable client job ID, replay ordered bounded events, and
|
|
12
|
+
acknowledge one terminal projection idempotently after message, queue,
|
|
13
|
+
counter, and session state are durable on the phone.
|
|
14
|
+
- **Crash and cancellation fences.** Provider ownership is persisted before
|
|
15
|
+
input, session-scoped leases prevent overlapping orphan continuations after a
|
|
16
|
+
restart, cancellation is durable, and answer-ready ownership gates
|
|
17
|
+
conversation, image, notification, and Done side effects.
|
|
18
|
+
- **Private bounded storage.** The append-only journal uses private directory
|
|
19
|
+
and file modes, repairs torn tails, bounds progress/activity payloads, and
|
|
20
|
+
retains terminal jobs for exactly seven days.
|
|
21
|
+
- **Safe rollout and rollback.** The health capability advertises exact protocol
|
|
22
|
+
version 1 only when configured and the store is ready. Removing the flag
|
|
23
|
+
blocks new durable admissions but leaves GET/events/cancel/ack available so
|
|
24
|
+
accepted jobs drain; legacy queries, first turns, handoffs, and older clients
|
|
25
|
+
remain unchanged.
|
|
26
|
+
|
|
27
|
+
## 6.9.0
|
|
28
|
+
|
|
29
|
+
Live recoverable prompt transcription for COS Glasses builds 200+.
|
|
30
|
+
|
|
31
|
+
- **Words appear while speaking.** After each audio chunk is durably acknowledged,
|
|
32
|
+
its sanitized fast/local transcript is published on the existing authenticated,
|
|
33
|
+
replayable display stream as `prompt_transcript`; the phone/G2 client can fill
|
|
34
|
+
the Listening body without adding another recorder, polling loop, or ASR job.
|
|
35
|
+
- **Recovery remains authoritative.** The event is optional presentation state.
|
|
36
|
+
Stored WAV chunks, final HQ transcription, glossary cleanup, editing, retry,
|
|
37
|
+
and send behavior remain unchanged and continue even if no display client is
|
|
38
|
+
connected.
|
|
39
|
+
- **Stale retries cannot repaint.** The server rechecks the exact draft, chunk
|
|
40
|
+
index, and audio bytes after warm transcription. Replaced audio never emits
|
|
41
|
+
its obsolete words, while client-side draft scoping, ordering, and replay
|
|
42
|
+
deduplication handle reconnects safely.
|
|
43
|
+
- **Public boundary retained.** This release adds no private COS paths, personal
|
|
44
|
+
data, LaunchAgent controls, remote restart authority, or machine-management
|
|
45
|
+
endpoints.
|
|
46
|
+
|
|
3
47
|
## 6.8.0
|
|
4
48
|
|
|
5
49
|
Public-safe meeting finalization for COS Glasses build 199.
|
package/README.md
CHANGED
|
@@ -56,6 +56,10 @@ The built-in IP allowlist blocks public-internet traffic regardless.
|
|
|
56
56
|
## What it does
|
|
57
57
|
|
|
58
58
|
- Ask anything, get a streamed answer on the lens (`/api/query`, `/v1/chat/completions`)
|
|
59
|
+
- With COS Glasses build 204+, opt into server-owned durable queries with
|
|
60
|
+
`COS_DURABLE_QUERY_JOBS=1`: accepted work survives phone backgrounding,
|
|
61
|
+
WebView reloads, and network handoffs, then reattaches without duplicate work
|
|
62
|
+
or duplicate replies
|
|
59
63
|
- Choose Opus, Fable, Sonnet, GPT Frontier, or GPT Balanced plus High, Extra
|
|
60
64
|
High, Max, or Ultracode effort; optional redacted tool activity streams only
|
|
61
65
|
to the authenticated query that requested it
|
|
@@ -64,7 +68,9 @@ The built-in IP allowlist blocks public-internet traffic regardless.
|
|
|
64
68
|
- Send phone photos with queued prompts, and review assistant-selected generated,
|
|
65
69
|
research, or explicitly used email images in Messages and on the G2 lens
|
|
66
70
|
- Recover long voice prompts after phone, network, or server interruptions. Audio
|
|
67
|
-
chunks are saved before transcription and retained locally for 72 hours.
|
|
71
|
+
chunks are saved before transcription and retained locally for 72 hours. On
|
|
72
|
+
compatible app builds, their warm transcript also appears live while speaking;
|
|
73
|
+
final HQ transcription remains authoritative.
|
|
68
74
|
- Live voice capture + transcription during meetings
|
|
69
75
|
- Local whisper.cpp transcription (free) with OpenAI fallback (optional)
|
|
70
76
|
- Tasks / calendar / people context **if** you run the
|
|
@@ -76,7 +82,8 @@ The built-in IP allowlist blocks public-internet traffic regardless.
|
|
|
76
82
|
Config lives at `~/.cos-glasses/.env` (created on first run). Every key is
|
|
77
83
|
optional except an installed CLI. Highlights: `BIND_HOST`, `PORT`,
|
|
78
84
|
`COS_API_TOKEN` (auto if unset), `OPENAI_API_KEY` (cloud voice fallback),
|
|
79
|
-
`COS_SCRIPTS_DIR` (full pipeline),
|
|
85
|
+
`COS_SCRIPTS_DIR` (full pipeline), `COS_DURABLE_QUERY_JOBS=1` (build 204+
|
|
86
|
+
server-owned query recovery), and `COS_MEDIA_ROOT` (optional image-store
|
|
80
87
|
location; default `~/.cos-glasses/data/media`). Your name + transcription vocabulary live in
|
|
81
88
|
`~/.cos-glasses/.cos-profile.json` (see `.cos-profile.example.json`).
|
|
82
89
|
|
|
@@ -97,6 +104,10 @@ BIND_HOST=0.0.0.0 npm run start:server
|
|
|
97
104
|
- *Voice getting billed?* — install `whisper-cpp` for free local transcription.
|
|
98
105
|
- *Photos unavailable?* — install `ffmpeg`, restart the server, and confirm `/api/health` reports `features.mediaProcessingReady: true`.
|
|
99
106
|
- *Prompt recovery unavailable?* — update with `npx @gotcos/glasses-server@latest`, then confirm `/api/health` reports `features.promptRecovery: true`.
|
|
107
|
+
- *Durable query recovery unavailable?* — build 204+ requires server 6.10.0+ and
|
|
108
|
+
`COS_DURABLE_QUERY_JOBS=1`. Restart once, then confirm `/api/health` reports
|
|
109
|
+
`features.durableQueryJobs: true`, protocol `1`, and state `ready`. To roll
|
|
110
|
+
back, remove the flag; accepted jobs still drain while new prompts use legacy streaming.
|
|
100
111
|
|
|
101
112
|
## License
|
|
102
113
|
|
package/package.json
CHANGED
package/server/index.ts
CHANGED
|
@@ -43,6 +43,13 @@ import { getMediaStore } from './lib/media-store.js'
|
|
|
43
43
|
import { listenRequiredServers, type RequiredListener } from './lib/listener-startup.js'
|
|
44
44
|
import { serverMetrics } from './lib/server-metrics.js'
|
|
45
45
|
import { initializeServerInstanceId } from './lib/server-instance-id.js'
|
|
46
|
+
import { createQueryJobsRouter } from './routes/query-jobs.js'
|
|
47
|
+
import {
|
|
48
|
+
initQueryJobRuntime,
|
|
49
|
+
preparePublicDurableQueryAdmission,
|
|
50
|
+
queryJobCoordinator,
|
|
51
|
+
shutdownQueryJobRuntime,
|
|
52
|
+
} from './lib/query-job-runtime.js'
|
|
46
53
|
|
|
47
54
|
const app = express()
|
|
48
55
|
const PORT = parseInt(process.env.PORT ?? '3141', 10)
|
|
@@ -139,6 +146,9 @@ app.use((_req, _res, next) => {
|
|
|
139
146
|
// API routes
|
|
140
147
|
app.use('/api', healthRouter)
|
|
141
148
|
app.use('/api', diagRouter)
|
|
149
|
+
app.use('/api', createQueryJobsRouter(queryJobCoordinator, {
|
|
150
|
+
prepareAdmission: preparePublicDurableQueryAdmission,
|
|
151
|
+
}))
|
|
142
152
|
app.use('/api', queryRouter)
|
|
143
153
|
app.use('/api', transcribeRouter)
|
|
144
154
|
app.use('/api', displayRouter)
|
|
@@ -173,21 +183,28 @@ app.get('/', (_req, res) => {
|
|
|
173
183
|
)
|
|
174
184
|
})
|
|
175
185
|
|
|
176
|
-
// Graceful shutdown
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
186
|
+
// Graceful shutdown persists an interrupted terminal before provider abort.
|
|
187
|
+
// The bounded force-exit keeps service managers from hanging forever on a
|
|
188
|
+
// broken disk while retaining the previous session/catalog/Whisper cleanup.
|
|
189
|
+
let gracefulShutdownStarted = false
|
|
190
|
+
async function gracefulShutdown(): Promise<void> {
|
|
191
|
+
if (gracefulShutdownStarted) return
|
|
192
|
+
gracefulShutdownStarted = true
|
|
193
|
+
const forceExit = setTimeout(() => process.exit(1), 8_000)
|
|
194
|
+
forceExit.unref?.()
|
|
195
|
+
try {
|
|
196
|
+
await shutdownQueryJobRuntime('server_shutdown')
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error('[query-jobs] graceful interruption failed:', error)
|
|
199
|
+
}
|
|
180
200
|
try { logActiveSessionsOnShutdown() } catch { /* best-effort flush */ }
|
|
181
201
|
stopCodexModelCatalogRefresh()
|
|
182
202
|
stopWhisperServer()
|
|
203
|
+
clearTimeout(forceExit)
|
|
183
204
|
process.exit(0)
|
|
184
|
-
}
|
|
185
|
-
process.on('
|
|
186
|
-
|
|
187
|
-
stopCodexModelCatalogRefresh()
|
|
188
|
-
stopWhisperServer()
|
|
189
|
-
process.exit(0)
|
|
190
|
-
})
|
|
205
|
+
}
|
|
206
|
+
process.on('SIGTERM', () => { void gracefulShutdown() })
|
|
207
|
+
process.on('SIGINT', () => { void gracefulShutdown() })
|
|
191
208
|
|
|
192
209
|
// Crash protection for runtime work. Listener failures are handled separately
|
|
193
210
|
// and exit immediately so a supervisor can restart a clean, unified process.
|
|
@@ -228,6 +245,18 @@ listenRequiredServers(listeners).then(() => {
|
|
|
228
245
|
console.log(`[COS API] Server instance: ${serverInstanceId}`)
|
|
229
246
|
console.log(`[COS API] Mode: ${COS_MODE ? 'COS pipeline' : 'standalone'}`)
|
|
230
247
|
|
|
248
|
+
void initQueryJobRuntime().then(health => {
|
|
249
|
+
if (process.env.COS_DURABLE_QUERY_JOBS === '1') {
|
|
250
|
+
console.log(`[COS API] Durable query jobs: ${health.store.state} · ${health.store.retainedIdentities} retained`)
|
|
251
|
+
} else {
|
|
252
|
+
console.log('[COS API] Durable query jobs: disabled (set COS_DURABLE_QUERY_JOBS=1 to enable)')
|
|
253
|
+
}
|
|
254
|
+
}).catch(error => {
|
|
255
|
+
// The store remains degraded and rejects admission. Legacy /api/query is
|
|
256
|
+
// still mounted, so disabling the feature flag is an immediate rollback.
|
|
257
|
+
console.error('[COS API] Durable query-job store unavailable:', error)
|
|
258
|
+
})
|
|
259
|
+
|
|
231
260
|
// Print ADDRESSES THE PHONE CAN ACTUALLY REACH. The bind address (0.0.0.0) is
|
|
232
261
|
// not paste-able — enumerate real interfaces and label the Tailscale one.
|
|
233
262
|
try {
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
type ActivityPreviewLine,
|
|
39
39
|
} from './activity-preview.js'
|
|
40
40
|
import {
|
|
41
|
+
collectRunOutputImagesBounded,
|
|
41
42
|
createRunOutputImagePublisher,
|
|
42
43
|
isRunOutputImagePublisherCommand,
|
|
43
44
|
type RunOutputImageCollectionStats,
|
|
@@ -291,19 +292,34 @@ function isExtendedQuery(query: string): boolean {
|
|
|
291
292
|
}
|
|
292
293
|
|
|
293
294
|
export interface ModelRunMetadata {
|
|
295
|
+
claudeRunId?: string
|
|
296
|
+
clientJobId?: string
|
|
297
|
+
generation?: number
|
|
294
298
|
codexRunId?: string
|
|
295
299
|
codexThreadId?: string
|
|
296
300
|
outputAttachments?: MediaAttachmentRef[]
|
|
297
301
|
outputImageStats?: RunOutputImageCollectionStats
|
|
298
302
|
}
|
|
299
303
|
|
|
304
|
+
/** Public-safe provider launch metadata for durable job coordination. It
|
|
305
|
+
* deliberately exposes no ChildProcess object, kill handle, paths, or env. */
|
|
306
|
+
export interface ProviderProcessMetadata {
|
|
307
|
+
provider: 'claude' | 'codex'
|
|
308
|
+
runId: string
|
|
309
|
+
pid?: number
|
|
310
|
+
clientJobId?: string
|
|
311
|
+
generation?: number
|
|
312
|
+
}
|
|
313
|
+
|
|
300
314
|
export interface StreamCallbacks {
|
|
301
315
|
onChunk: (text: string) => void
|
|
302
|
-
|
|
303
|
-
|
|
316
|
+
onAnswerReady?: (fullText: string) => boolean | void | Promise<boolean | void>
|
|
317
|
+
onDone: (fullText: string, model: ModelPreference, cliSessionId?: string, metadata?: ModelRunMetadata) => boolean | void | Promise<boolean | void>
|
|
318
|
+
onError: (error: string) => void | Promise<void>
|
|
304
319
|
onToolStatus?: (toolName: string) => void
|
|
305
320
|
onActivityLine?: (line: ActivityPreviewLine) => void
|
|
306
321
|
onStart?: (model: ModelPreference, sessionId: string, cliSessionId?: string, metadata?: ModelRunMetadata) => void
|
|
322
|
+
onProviderProcess?: (metadata: ProviderProcessMetadata) => boolean | void | Promise<boolean | void>
|
|
307
323
|
}
|
|
308
324
|
|
|
309
325
|
/** Claude CLI can emit `subtype: success` with `is_error: true`; the boolean
|
|
@@ -336,6 +352,10 @@ export interface CallOptions {
|
|
|
336
352
|
lightweight?: boolean // Skip async context fetch — use cached context instantly (G2 speed path)
|
|
337
353
|
abortSignal?: AbortSignal
|
|
338
354
|
effort?: EffortPreference
|
|
355
|
+
clientJobId?: string
|
|
356
|
+
generation?: number
|
|
357
|
+
/** Durable coordinator already owns the per-session provider lease. */
|
|
358
|
+
sessionLockHeld?: boolean
|
|
339
359
|
}
|
|
340
360
|
|
|
341
361
|
export async function callClaudeStreaming(
|
|
@@ -367,7 +387,10 @@ export async function callClaudeStreaming(
|
|
|
367
387
|
// Pass existing CLI session ID if resuming (new sessions get it after first result)
|
|
368
388
|
const resolvedCliKey = cliSessionKey(sid, resolvedModel)
|
|
369
389
|
let existingCliSession = cliSessionMap.get(resolvedCliKey)
|
|
370
|
-
callbacks.onStart?.(resolvedModel, sid, existingCliSession
|
|
390
|
+
callbacks.onStart?.(resolvedModel, sid, existingCliSession, {
|
|
391
|
+
clientJobId: options?.clientJobId,
|
|
392
|
+
generation: options?.generation,
|
|
393
|
+
})
|
|
371
394
|
|
|
372
395
|
// Phase: context loading (skipped in lightweight mode)
|
|
373
396
|
let phase: Phase = 'context'
|
|
@@ -414,7 +437,18 @@ export async function callClaudeStreaming(
|
|
|
414
437
|
// Record user message (with [Photo]/[N Photos] prefix for vision queries)
|
|
415
438
|
const photoPrefix = imagePaths.length === 1 ? '[Photo]' : imagePaths.length > 1 ? `[${imagePaths.length} Photos]` : ''
|
|
416
439
|
const historyQuery = photoPrefix ? `${photoPrefix} ${query || 'What do you see?'}` : query
|
|
417
|
-
const
|
|
440
|
+
const exchangeProvenance = {
|
|
441
|
+
clientJobId: options?.clientJobId,
|
|
442
|
+
generation: options?.generation,
|
|
443
|
+
}
|
|
444
|
+
const pendingUserExchange = addExchange(
|
|
445
|
+
sid,
|
|
446
|
+
'user',
|
|
447
|
+
historyQuery,
|
|
448
|
+
globalMsgNum,
|
|
449
|
+
undefined,
|
|
450
|
+
exchangeProvenance,
|
|
451
|
+
)
|
|
418
452
|
|
|
419
453
|
// Vision queries need the Read tool to see the image files
|
|
420
454
|
const baseTools = imagePaths.length > 0 ? 'WebSearch,WebFetch,Read' : 'WebSearch,WebFetch'
|
|
@@ -519,15 +553,62 @@ export async function callClaudeStreaming(
|
|
|
519
553
|
cleanupModelImageInputs(imageInputs)
|
|
520
554
|
}
|
|
521
555
|
|
|
556
|
+
function abandonLostDurableOwnership(message: string) {
|
|
557
|
+
finalized = true
|
|
558
|
+
cleanup()
|
|
559
|
+
cleanupImages()
|
|
560
|
+
outputImagePublisher?.cleanup()
|
|
561
|
+
removeExchange(sid, pendingUserExchange)
|
|
562
|
+
if (cliSessionMap.delete(resolvedCliKey)) scheduleCliSessionSave()
|
|
563
|
+
finishClaudeRun(run.runId, {
|
|
564
|
+
status: 'failed',
|
|
565
|
+
startedAtMs: startTime,
|
|
566
|
+
error: message,
|
|
567
|
+
exitCode: null,
|
|
568
|
+
})
|
|
569
|
+
}
|
|
570
|
+
|
|
522
571
|
async function finalize(text: string) {
|
|
523
572
|
if (finalized) return
|
|
524
573
|
finalized = true
|
|
525
574
|
cleanup()
|
|
526
575
|
cleanupImages()
|
|
527
576
|
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
|
|
577
|
+
// The coordinator persists the final provider text before conversation
|
|
578
|
+
// mutation, condensation, or output-image normalization can stall/crash.
|
|
579
|
+
try {
|
|
580
|
+
const answerOwned = await callbacks.onAnswerReady?.(text)
|
|
581
|
+
if (answerOwned === false) {
|
|
582
|
+
abandonLostDurableOwnership('claude-bridge: durable answer ownership was lost.')
|
|
583
|
+
return
|
|
584
|
+
}
|
|
585
|
+
} catch (error) {
|
|
586
|
+
console.error('[claude-bridge] durable answer barrier failed:', error)
|
|
587
|
+
outputImagePublisher?.cleanup()
|
|
588
|
+
removeExchange(sid, pendingUserExchange)
|
|
589
|
+
if (cliSessionMap.delete(resolvedCliKey)) scheduleCliSessionSave()
|
|
590
|
+
finishClaudeRun(run.runId, {
|
|
591
|
+
status: 'failed',
|
|
592
|
+
startedAtMs: startTime,
|
|
593
|
+
error: 'claude-bridge: durable answer persistence failed.',
|
|
594
|
+
exitCode: null,
|
|
595
|
+
})
|
|
596
|
+
try {
|
|
597
|
+
await callbacks.onError('claude-bridge: durable answer persistence failed.')
|
|
598
|
+
} catch (callbackError) {
|
|
599
|
+
console.error('[claude-bridge] durable barrier error callback failed:', callbackError)
|
|
600
|
+
}
|
|
601
|
+
return
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const assistantExchange = addExchange(
|
|
605
|
+
sid,
|
|
606
|
+
'assistant',
|
|
607
|
+
text,
|
|
608
|
+
globalMsgNum,
|
|
609
|
+
undefined,
|
|
610
|
+
exchangeProvenance,
|
|
611
|
+
)
|
|
531
612
|
if (imagePaths.length > 0) {
|
|
532
613
|
replaceLastExchangeWithSummary(sid, query, text, imagePaths.length)
|
|
533
614
|
}
|
|
@@ -539,7 +620,9 @@ export async function callClaudeStreaming(
|
|
|
539
620
|
const preparingHeartbeat = setInterval(() => callbacks.onToolStatus?.('Preparing images...'), HEARTBEAT_INTERVAL_MS)
|
|
540
621
|
preparingHeartbeat.unref?.()
|
|
541
622
|
try {
|
|
542
|
-
outputAttachments = await outputImagePublisher
|
|
623
|
+
outputAttachments = await collectRunOutputImagesBounded(outputImagePublisher, {
|
|
624
|
+
signal: options?.abortSignal,
|
|
625
|
+
})
|
|
543
626
|
} catch (err) {
|
|
544
627
|
console.error('[claude-bridge] output image collection failed:', err)
|
|
545
628
|
} finally {
|
|
@@ -576,10 +659,14 @@ export async function callClaudeStreaming(
|
|
|
576
659
|
exitCode: 0,
|
|
577
660
|
})
|
|
578
661
|
|
|
579
|
-
callbacks.onDone(text, resolvedModel, cliSessionMap.get(resolvedCliKey), {
|
|
662
|
+
const terminalOwned = await callbacks.onDone(text, resolvedModel, cliSessionMap.get(resolvedCliKey), {
|
|
663
|
+
claudeRunId: run.runId,
|
|
664
|
+
clientJobId: options?.clientJobId,
|
|
665
|
+
generation: options?.generation,
|
|
580
666
|
...(outputAttachments.length > 0 ? { outputAttachments } : {}),
|
|
581
667
|
...(outputImageStats && outputImageStats.published > 0 ? { outputImageStats } : {}),
|
|
582
668
|
})
|
|
669
|
+
if (terminalOwned === false) return
|
|
583
670
|
|
|
584
671
|
// Telegram notifications — fire and forget
|
|
585
672
|
if (isFirstQuery) {
|
|
@@ -589,7 +676,7 @@ export async function callClaudeStreaming(
|
|
|
589
676
|
notifyExchange(sid, query, text)
|
|
590
677
|
}
|
|
591
678
|
|
|
592
|
-
function finalizeError(
|
|
679
|
+
async function finalizeError(
|
|
593
680
|
msg: string,
|
|
594
681
|
exitCode?: number | null,
|
|
595
682
|
status: Exclude<ClaudeRunStatus, 'running'> = 'failed',
|
|
@@ -610,7 +697,7 @@ export async function callClaudeStreaming(
|
|
|
610
697
|
error: msg,
|
|
611
698
|
exitCode,
|
|
612
699
|
})
|
|
613
|
-
callbacks.onError(msg)
|
|
700
|
+
await callbacks.onError(msg)
|
|
614
701
|
}
|
|
615
702
|
|
|
616
703
|
function handleAbort() {
|
|
@@ -827,11 +914,27 @@ export async function callClaudeStreaming(
|
|
|
827
914
|
? `${fullQuery}\n\n${ULTRACODE_KEYWORD}`
|
|
828
915
|
: fullQuery
|
|
829
916
|
try {
|
|
917
|
+
const providerOwned = await callbacks.onProviderProcess?.({
|
|
918
|
+
provider: 'claude',
|
|
919
|
+
runId: run.runId,
|
|
920
|
+
pid: proc.pid,
|
|
921
|
+
clientJobId: options?.clientJobId,
|
|
922
|
+
generation: options?.generation,
|
|
923
|
+
})
|
|
924
|
+
if (providerOwned === false) {
|
|
925
|
+
proc.kill('SIGTERM')
|
|
926
|
+
abandonLostDurableOwnership('claude-bridge: durable provider ownership was lost.')
|
|
927
|
+
return sid
|
|
928
|
+
}
|
|
929
|
+
if (finalized) return sid
|
|
830
930
|
proc.stdin.write(cliQuery)
|
|
831
931
|
proc.stdin.end()
|
|
832
932
|
} catch (err) {
|
|
833
933
|
const message = err instanceof Error ? err.message : String(err)
|
|
834
|
-
|
|
934
|
+
if (!finalized) {
|
|
935
|
+
proc.kill('SIGTERM')
|
|
936
|
+
await finalizeError(`claude-bridge: provider start failed — ${message}`, null)
|
|
937
|
+
}
|
|
835
938
|
}
|
|
836
939
|
|
|
837
940
|
return sid
|