@gotcos/glasses-server 6.76.1 → 6.78.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 +40 -0
- package/CHANGELOG.md +56 -0
- package/README.md +12 -1
- package/managed-runtime-contract.json +17 -1
- package/package.json +10 -1
- package/server/index.ts +98 -10
- package/server/lib/agent-session-binding-registry.ts +623 -9
- package/server/lib/boot-resume.ts +552 -0
- package/server/lib/claude-bridge.ts +152 -3
- package/server/lib/claude-cli-support.ts +39 -0
- package/server/lib/claude-steer.ts +259 -0
- package/server/lib/cos-bridge-notes.ts +73 -0
- package/server/lib/decision-provider.ts +9 -0
- package/server/lib/env-switch.ts +17 -0
- package/server/lib/fuzzy-correct.ts +77 -49
- package/server/lib/live-cos-ask.ts +41 -7
- package/server/lib/live-cues-v2-notes.ts +152 -0
- package/server/lib/live-cues-v2-settings.ts +63 -0
- package/server/lib/live-cues-v2.ts +198 -38
- package/server/lib/managed-restart.ts +8 -0
- package/server/lib/provider-actions.ts +18 -0
- package/server/lib/query-job-coordinator.ts +300 -0
- package/server/lib/query-job-feature.ts +39 -0
- package/server/lib/query-job-resume.ts +269 -0
- package/server/lib/query-job-runtime.ts +23 -3
- package/server/lib/query-job-store.ts +516 -7
- package/server/lib/query-job-types.ts +210 -1
- package/server/lib/queue-turn-evidence.ts +213 -0
- package/server/lib/thread-turn-queue-deliver.ts +6 -1
- package/server/lib/thread-turn-queue-store.ts +13 -4
- package/server/lib/thread-turn-queue.ts +151 -1
- package/server/lib/transcript-cleanup-context.ts +234 -0
- package/server/lib/transcript-cleanup-ledger.ts +304 -0
- package/server/lib/transcript-cleanup-model.ts +217 -0
- package/server/lib/transcript-cleanup-screen.ts +249 -0
- package/server/lib/transcript-cleanup-spans.ts +358 -0
- package/server/lib/transcript-cleanup-state.ts +201 -0
- package/server/lib/transcript-cleanup-suggestions.ts +126 -0
- package/server/lib/transcript-cleanup-text.ts +200 -0
- package/server/lib/transcript-cleanup.ts +427 -0
- package/server/routes/agent-session-bindings.ts +169 -11
- package/server/routes/glossary.ts +38 -19
- package/server/routes/health.ts +29 -1
- package/server/routes/live-cues.ts +32 -3
- package/server/routes/maintenance.ts +34 -0
- package/server/routes/meeting.ts +92 -9
- package/server/routes/query-jobs.ts +107 -13
- package/server/routes/recovery.ts +4 -0
- package/server/routes/thread-turn-queue.ts +371 -10
- package/server/routes/transcribe.ts +3 -0
- package/server/routes/transcript-cleanup.ts +237 -0
- package/shared/live-cues-v2.ts +9 -4
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// 6.78.0 restart truth (plan v3.1 P2, the Pi Durable resume planner made real).
|
|
2
|
+
//
|
|
3
|
+
// WHAT A RESTART CUT OFF, TOLD FROM THE JOB'S OWN RECORD. When a restart ends an Ask (a crash
|
|
4
|
+
// then boot, or a graceful shutdown), the store builds one `interruption` record from the
|
|
5
|
+
// job's trail before it writes the terminal row: the step that was running, the side-effect
|
|
6
|
+
// steps that already finished, and whether that list may be missing steps. Pure and in
|
|
7
|
+
// memory, so the graceful path stays far inside its 8-second shutdown budget.
|
|
8
|
+
//
|
|
9
|
+
// NOTHING IS INFERRED AS SAFE. The trail can be absent (tool activity off), bounded (200
|
|
10
|
+
// entries, 32,000 characters), or hold a step whose outcome has no call id to match. Each of
|
|
11
|
+
// those sets `incomplete`, and a call-less outcome finishes nothing. A step's outcome is only
|
|
12
|
+
// ever the row that names its call.
|
|
13
|
+
//
|
|
14
|
+
// STEER ROWS ARE NOT STEPS. Only `tool` and `status` entries are read; anything else in the
|
|
15
|
+
// list (prose, and a steer or any kind a later build adds) is skipped.
|
|
16
|
+
//
|
|
17
|
+
// REPLAY SAFETY IS DECLARED, NEVER INFERRED (the prototype's rule). `planRestartResume` never
|
|
18
|
+
// auto-resumes in 6.78.0: dispatch auto-resume is deferred (V3 N11), so a declared read-only
|
|
19
|
+
// dispatch is handed to a person like every other job.
|
|
20
|
+
//
|
|
21
|
+
// Imports types only, so the Pi Durable canaries can load it without reaching a live module.
|
|
22
|
+
import {
|
|
23
|
+
DISPATCH_ALLOWED_TOOLS,
|
|
24
|
+
QUERY_JOB_INTERRUPTION_LIMITS,
|
|
25
|
+
QUERY_JOB_LIMITS,
|
|
26
|
+
QUERY_JOB_RESTART_MESSAGE,
|
|
27
|
+
interruptionLine,
|
|
28
|
+
type QueryJobActivityMode,
|
|
29
|
+
type QueryJobInterruption,
|
|
30
|
+
type QueryJobRequest,
|
|
31
|
+
type QueryJobRestartCause,
|
|
32
|
+
type QueryJobSnapshot,
|
|
33
|
+
} from './query-job-types.js'
|
|
34
|
+
|
|
35
|
+
export interface RestartStep {
|
|
36
|
+
verb: string
|
|
37
|
+
target: string
|
|
38
|
+
call?: string
|
|
39
|
+
finished: boolean
|
|
40
|
+
ok?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Steps that change nothing outside the run. They are never listed as already done. */
|
|
44
|
+
const SIDE_EFFECT_FREE_VERBS: ReadonlySet<string> = new Set(['read', 'search'])
|
|
45
|
+
|
|
46
|
+
const GERUND: Readonly<Record<string, string>> = {
|
|
47
|
+
read: 'reading', edit: 'editing', write: 'writing', bash: 'running', search: 'searching', task: 'running', other: 'using',
|
|
48
|
+
}
|
|
49
|
+
const PAST: Readonly<Record<string, string>> = {
|
|
50
|
+
read: 'read', edit: 'edited', write: 'wrote', bash: 'ran', search: 'searched', task: 'ran', other: 'used',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type TrailLike = {
|
|
54
|
+
kind?: unknown
|
|
55
|
+
verb?: unknown
|
|
56
|
+
target?: unknown
|
|
57
|
+
call?: unknown
|
|
58
|
+
detail?: unknown
|
|
59
|
+
trailSeq?: unknown
|
|
60
|
+
tool_outcome?: unknown
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function text(value: unknown): string {
|
|
64
|
+
return typeof value === 'string' ? value : ''
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Tool steps in trail order. A `status` row finishes the newest unfinished step whose call it
|
|
69
|
+
* names; an outcome with no call, or one that matches no step, finishes nothing and marks the
|
|
70
|
+
* list ambiguous, as does a step that carries no call id. `openDetail` is Claude Code's own
|
|
71
|
+
* step line (`status.detail`) when it came after the last step, the best words for "cut off
|
|
72
|
+
* during".
|
|
73
|
+
*/
|
|
74
|
+
export function trailSteps(trail: readonly unknown[] | undefined): { steps: RestartStep[]; ambiguous: boolean; openDetail: string } {
|
|
75
|
+
const steps: RestartStep[] = []
|
|
76
|
+
let ambiguous = false
|
|
77
|
+
let openDetail = ''
|
|
78
|
+
for (const raw of trail ?? []) {
|
|
79
|
+
if (!raw || typeof raw !== 'object') continue
|
|
80
|
+
const entry = raw as TrailLike
|
|
81
|
+
if (entry.kind === 'tool') {
|
|
82
|
+
const verb = text(entry.verb), target = text(entry.target)
|
|
83
|
+
if (!verb || !target) continue
|
|
84
|
+
const call = text(entry.call)
|
|
85
|
+
steps.push({ verb, target, ...(call ? { call } : {}), finished: false })
|
|
86
|
+
if (!call) ambiguous = true
|
|
87
|
+
openDetail = ''
|
|
88
|
+
} else if (entry.kind === 'status') {
|
|
89
|
+
const outcome = entry.tool_outcome && typeof entry.tool_outcome === 'object'
|
|
90
|
+
? entry.tool_outcome as { call?: unknown; ok?: unknown } : null
|
|
91
|
+
if (outcome) {
|
|
92
|
+
const call = text(outcome.call)
|
|
93
|
+
const step = call ? [...steps].reverse().find(s => s.call === call && !s.finished) : undefined
|
|
94
|
+
if (step) {
|
|
95
|
+
step.finished = true
|
|
96
|
+
step.ok = outcome.ok !== false
|
|
97
|
+
} else {
|
|
98
|
+
ambiguous = true
|
|
99
|
+
}
|
|
100
|
+
} else if (text(entry.detail)) {
|
|
101
|
+
openDetail = text(entry.detail)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// prose, steer and any later kind: not a step.
|
|
105
|
+
}
|
|
106
|
+
return { steps, ambiguous, openDetail }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function describeOpen(step: RestartStep): string {
|
|
110
|
+
return `${GERUND[step.verb] ?? 'using'} ${step.target}`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function describeDone(step: RestartStep): string {
|
|
114
|
+
return `${PAST[step.verb] ?? 'used'} ${step.target}${step.ok === false ? ' (failed)' : ''}`
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** "Running npm test." reads as "running npm test" after "Cut off during". */
|
|
118
|
+
function lowerLead(value: string): string {
|
|
119
|
+
const trimmed = value.trim().replace(/[.\s]+$/, '')
|
|
120
|
+
return trimmed ? trimmed[0]!.toLowerCase() + trimmed.slice(1) : ''
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface InterruptionInput {
|
|
124
|
+
/** The job as the store holds it when the restart ends it: `trail`, ownership and status. */
|
|
125
|
+
job: Pick<QueryJobSnapshot, 'trail' | 'providerOwnershipConfirmedAt'>
|
|
126
|
+
activityToolMode: QueryJobActivityMode
|
|
127
|
+
cause: QueryJobRestartCause
|
|
128
|
+
at: string
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The interruption record for a job a restart ended (see QueryJobInterruption). */
|
|
132
|
+
export function buildInterruption(input: InterruptionInput): QueryJobInterruption {
|
|
133
|
+
const limits = QUERY_JOB_INTERRUPTION_LIMITS
|
|
134
|
+
const trail = input.job.trail
|
|
135
|
+
const { steps, ambiguous, openDetail } = trailSteps(trail)
|
|
136
|
+
const unfinished = steps.filter(step => !step.finished)
|
|
137
|
+
const open = unfinished.at(-1)
|
|
138
|
+
const lastStep = steps.at(-1)
|
|
139
|
+
// Claude Code's own line describes the newest step only while that step is still open.
|
|
140
|
+
const openWords = open && open === lastStep && openDetail ? lowerLead(openDetail) : open ? describeOpen(open) : ''
|
|
141
|
+
const cutOffDuring = interruptionLine(openWords, limits.cutOffDuringChars) || null
|
|
142
|
+
|
|
143
|
+
const done: string[] = []
|
|
144
|
+
for (const step of steps) {
|
|
145
|
+
if (!step.finished || SIDE_EFFECT_FREE_VERBS.has(step.verb)) continue
|
|
146
|
+
const line = interruptionLine(describeDone(step), limits.alreadyDoneEntryChars)
|
|
147
|
+
if (line && !done.includes(line)) done.push(line)
|
|
148
|
+
}
|
|
149
|
+
const alreadyDone = done.slice(-limits.alreadyDoneMax)
|
|
150
|
+
|
|
151
|
+
const trailCapped = (trail?.length ?? 0) >= QUERY_JOB_LIMITS.trailEntries
|
|
152
|
+
|| (typeof (trail?.[0] as TrailLike | undefined)?.trailSeq === 'number' && Number((trail![0] as TrailLike).trailSeq) > 1)
|
|
153
|
+
const providerStarted = Boolean(input.job.providerOwnershipConfirmedAt)
|
|
154
|
+
// A parked job's starts crashed the server: nothing on record can say what they did.
|
|
155
|
+
const incomplete = input.cause === 'crash_loop_parked'
|
|
156
|
+
|| input.activityToolMode === 'off'
|
|
157
|
+
|| trailCapped
|
|
158
|
+
|| ambiguous
|
|
159
|
+
|| unfinished.length > 1
|
|
160
|
+
|| done.length > alreadyDone.length
|
|
161
|
+
|| (providerStarted && !(trail?.length))
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
cause: input.cause,
|
|
165
|
+
at: input.at,
|
|
166
|
+
cutOffDuring,
|
|
167
|
+
alreadyDone,
|
|
168
|
+
incomplete,
|
|
169
|
+
resumable: input.cause !== 'crash_loop_parked',
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Deferred: the restart-resume planner (QJ-6a) ─────────────────────────────────────────
|
|
174
|
+
// Everything from here to `restartError` ships UNCALLED in 6.78.0 (P7 S11). Nothing in the
|
|
175
|
+
// server reads a plan: the app decides Resume or Retry from `interruption.resumable`, and
|
|
176
|
+
// dispatch auto-resume (QJ-6a, V3 N11) is deferred to a later release. The exports stay
|
|
177
|
+
// because the Pi Durable canaries (QJ-6b, QJ-6d) drive this exact planner, so the release
|
|
178
|
+
// that wires it inherits a tested contract. Marked @internal: not an API.
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @internal Deferred with the planner (QJ-6a): uncalled in 6.78.0.
|
|
182
|
+
* One sentence group for a person: what happened, the step cut off, what was already done.
|
|
183
|
+
*/
|
|
184
|
+
export function restartNoticeText(interruption: QueryJobInterruption): string {
|
|
185
|
+
return [
|
|
186
|
+
interruption.cause === 'crash_loop_parked'
|
|
187
|
+
? 'Stopped after repeated server crashes.'
|
|
188
|
+
: 'The server restarted mid-answer.',
|
|
189
|
+
interruption.cutOffDuring ? `Cut off during: ${interruption.cutOffDuring}.` : '',
|
|
190
|
+
interruption.alreadyDone.length ? `Already done: ${interruption.alreadyDone.join('; ')}.` : '',
|
|
191
|
+
interruption.incomplete ? 'Earlier steps may be missing from this list.' : '',
|
|
192
|
+
].filter(Boolean).join(' ')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** @internal (QJ-6a, uncalled in 6.78.0) The restart cause of a terminal job, or null. Reads 6.78.0 causes and the 6.77.0 messages. */
|
|
196
|
+
export function restartCauseOf(job: Pick<QueryJobSnapshot, 'status' | 'error' | 'interruption'>): QueryJobRestartCause | null {
|
|
197
|
+
if (job.status !== 'interrupted') return null
|
|
198
|
+
if (job.interruption) return job.interruption.cause
|
|
199
|
+
const error = job.error
|
|
200
|
+
if (!error) return null
|
|
201
|
+
if (error.cause === 'server_restarted' || error.cause === 'server_shutdown' || error.cause === 'crash_loop_parked') return error.cause
|
|
202
|
+
if (error.code !== 'interrupted') return null
|
|
203
|
+
// Journals written before 6.78.0: the boot path's message, and the raw graceful reason.
|
|
204
|
+
if (/^Server restarted\./.test(error.message)) return 'server_restarted'
|
|
205
|
+
if (error.message === 'server_shutdown') return 'server_shutdown'
|
|
206
|
+
return null
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** @internal (QJ-6a, uncalled in 6.78.0) Was this job cut off by a server restart (not cancelled, failed or stopped by a person)? */
|
|
210
|
+
export function interruptedByRestart(job: Pick<QueryJobSnapshot, 'status' | 'error' | 'interruption'>): boolean {
|
|
211
|
+
const cause = restartCauseOf(job)
|
|
212
|
+
return cause === 'server_restarted' || cause === 'server_shutdown'
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** @internal (QJ-6a) No lineage is auto-resumed in 6.78.0. */
|
|
216
|
+
export const MAX_AUTO_RESUMES_PER_LINEAGE = 0
|
|
217
|
+
|
|
218
|
+
/** @internal (QJ-6a) */
|
|
219
|
+
export interface ResumePlan {
|
|
220
|
+
action: 'auto_resume' | 'ask_user' | 'wait' | 'none'
|
|
221
|
+
reason: string
|
|
222
|
+
steps: RestartStep[]
|
|
223
|
+
notice: string
|
|
224
|
+
listMayBeIncomplete: boolean
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* @internal (QJ-6a, uncalled in 6.78.0: the canaries drive it; the server does not.)
|
|
229
|
+
* What to do with a job a restart ended. `wait` while a provider child from before the
|
|
230
|
+
* restart may still own the session (the 21-minute orphan fence), `ask_user` otherwise: the
|
|
231
|
+
* person picks Resume or Retry with the notice in front of them. Never `auto_resume` in
|
|
232
|
+
* 6.78.0 (dispatch auto-resume is deferred, V3 N11). Jobs that ended any other way: `none`.
|
|
233
|
+
*/
|
|
234
|
+
export function planRestartResume(
|
|
235
|
+
job: QueryJobSnapshot,
|
|
236
|
+
request: Pick<QueryJobRequest, 'activityToolMode' | 'dispatch'>,
|
|
237
|
+
opts: { now: number; autoResumesSoFar: number },
|
|
238
|
+
): ResumePlan {
|
|
239
|
+
const cause = restartCauseOf(job)
|
|
240
|
+
if (!cause) return { action: 'none', reason: 'not_a_restart', steps: [], notice: '', listMayBeIncomplete: false }
|
|
241
|
+
const steps = trailSteps(job.trail).steps
|
|
242
|
+
const interruption = job.interruption ?? buildInterruption({
|
|
243
|
+
job,
|
|
244
|
+
activityToolMode: request.activityToolMode,
|
|
245
|
+
cause,
|
|
246
|
+
at: job.completedAt ?? job.updatedAt,
|
|
247
|
+
})
|
|
248
|
+
const notice = restartNoticeText(interruption)
|
|
249
|
+
const listMayBeIncomplete = interruption.incomplete
|
|
250
|
+
if (cause === 'crash_loop_parked') {
|
|
251
|
+
return { action: 'ask_user', reason: 'crash_loop_parked', steps, notice, listMayBeIncomplete }
|
|
252
|
+
}
|
|
253
|
+
const fence = job.orphanFenceUntil ? Date.parse(job.orphanFenceUntil) : Number.NaN
|
|
254
|
+
if (Number.isFinite(fence) && fence > opts.now) return { action: 'wait', reason: 'orphan_fence', steps, notice, listMayBeIncomplete }
|
|
255
|
+
const tools = request.dispatch?.restricted === true ? request.dispatch.tools : []
|
|
256
|
+
const declaredReadOnly = tools.length > 0 && tools.every(t => (DISPATCH_ALLOWED_TOOLS as readonly string[]).includes(t))
|
|
257
|
+
return {
|
|
258
|
+
action: 'ask_user',
|
|
259
|
+
reason: declaredReadOnly ? 'auto_resume_deferred' : 'not_declared_replay_safe',
|
|
260
|
+
steps, notice, listMayBeIncomplete,
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── End of the deferred planner. ─────────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
/** The error a restart writes (both paths), with its cause set at construction. */
|
|
267
|
+
export function restartError(cause: Exclude<QueryJobRestartCause, 'crash_loop_parked'>): { code: 'interrupted'; cause: string; message: string; retryable: true } {
|
|
268
|
+
return { code: 'interrupted', cause, message: QUERY_JOB_RESTART_MESSAGE, retryable: true }
|
|
269
|
+
}
|
|
@@ -7,9 +7,11 @@ import { resolveQueryAttachments } from './query-attachments.js'
|
|
|
7
7
|
import { getMediaStore } from './media-store.js'
|
|
8
8
|
import { dataPath } from './data-dir.js'
|
|
9
9
|
import { currentMessageEra, LEGACY_MESSAGE_ERA } from './message-era.js'
|
|
10
|
-
import { durableQueryJobsEnabled, messagesTrailEnabled } from './query-job-feature.js'
|
|
10
|
+
import { durableQueryJobsEnabled, messagesSteerEnabled, messagesTrailEnabled } from './query-job-feature.js'
|
|
11
|
+
import { claudeSupportsSteer } from './claude-cli-support.js'
|
|
12
|
+
import { providerOfModel, resolveRunModel } from './claude-steer.js'
|
|
11
13
|
import { QueryJobCoordinator, type QueryJobRunner } from './query-job-coordinator.js'
|
|
12
|
-
import { QueryJobStore } from './query-job-store.js'
|
|
14
|
+
import { QueryJobStore, createQueryJobJournalStorage } from './query-job-store.js'
|
|
13
15
|
import {
|
|
14
16
|
isCodexModel,
|
|
15
17
|
isCursorModel,
|
|
@@ -24,6 +26,7 @@ import { attachmentHistoryPrefix, defaultAttachmentRequest } from '../../shared/
|
|
|
24
26
|
import {
|
|
25
27
|
findExchangesByJobIdentity,
|
|
26
28
|
flushConversationToDisk,
|
|
29
|
+
getSessionModel,
|
|
27
30
|
reconcileExchangeByJobIdentity,
|
|
28
31
|
removeExchangesByJobIdentity,
|
|
29
32
|
} from './conversation.js'
|
|
@@ -36,6 +39,7 @@ import {
|
|
|
36
39
|
import { acquireMaintenanceWork } from './maintenance-lifecycle.js'
|
|
37
40
|
import type { JobTrailDraft } from './job-trail.js'
|
|
38
41
|
import { registerMessageReservationSource } from './message-reservations.js'
|
|
42
|
+
import { bootResumeGate } from './boot-resume.js'
|
|
39
43
|
|
|
40
44
|
const TOOL_STATUS_MESSAGES: Record<string, string> = {
|
|
41
45
|
WebSearch: 'Searching web...',
|
|
@@ -191,7 +195,7 @@ export function nativeSessionLinkage(linkage: { cliSessionId: string; provider?:
|
|
|
191
195
|
: { provider, cliSessionId: linkage.cliSessionId }
|
|
192
196
|
}
|
|
193
197
|
|
|
194
|
-
const runner: QueryJobRunner = async ({ jobId, turnId, request, signal, callbacks }) => {
|
|
198
|
+
const runner: QueryJobRunner = async ({ jobId, turnId, request, signal, callbacks, steer }) => {
|
|
195
199
|
// Resolve ids again at execution time. This closes the admission/execution
|
|
196
200
|
// TOCTOU window without ever putting paths or bytes in the journal.
|
|
197
201
|
const resolvedAttachments = await resolveQueryAttachments({
|
|
@@ -367,6 +371,8 @@ const runner: QueryJobRunner = async ({ jobId, turnId, request, signal, callback
|
|
|
367
371
|
sessionLockHeld: true,
|
|
368
372
|
...(request.dispatch ? { dispatch: request.dispatch } : {}),
|
|
369
373
|
...(request.sessionName ? { sessionName: request.sessionName } : {}),
|
|
374
|
+
// 6.78.0: only on a run the coordinator decided is steerable.
|
|
375
|
+
...(steer ? { steer } : {}),
|
|
370
376
|
},
|
|
371
377
|
)
|
|
372
378
|
}
|
|
@@ -377,6 +383,9 @@ const queryJobRoot = configuredRoot ? resolve(configuredRoot) : dataPath('query-
|
|
|
377
383
|
export const queryJobStore = new QueryJobStore({
|
|
378
384
|
root: queryJobRoot,
|
|
379
385
|
bootId: randomUUID(),
|
|
386
|
+
// 6.78.0 (MVP-D release 1): reads delta-only chunk rows; writes them only with
|
|
387
|
+
// COS_QUERY_JOB_DELTA_JOURNAL=1 (default off: every append is the 6.77.0 line).
|
|
388
|
+
storage: createQueryJobJournalStorage(),
|
|
380
389
|
// 6.61.3: only when the coordinator will run them. With COS_DURABLE_QUERY_JOBS=0 no
|
|
381
390
|
// coordinator starts, so prior-boot never-started jobs are interrupted as before.
|
|
382
391
|
requeueNeverStartedOnBoot: durableQueryJobsEnabled(),
|
|
@@ -386,6 +395,15 @@ export const queryJobCoordinator = new QueryJobCoordinator(queryJobStore, runner
|
|
|
386
395
|
projectTerminal: projectPublicConversationTerminal,
|
|
387
396
|
acquireSessionLock: acquireModelSessionRunLock,
|
|
388
397
|
acquireMaintenanceWork: () => acquireMaintenanceWork('durable_query', { phase: 'queued' }),
|
|
398
|
+
// 6.78.0: the boot requeue counts crashed boots per lineage and parks one past the limit.
|
|
399
|
+
bootResumeGate,
|
|
400
|
+
// 6.78.0 Messages steer (COS_MESSAGES_STEER, default off). The provider is resolved as
|
|
401
|
+
// model-router resolves it, so a run is steerable only when it will run on Claude.
|
|
402
|
+
steer: {
|
|
403
|
+
enabled: () => messagesSteerEnabled(),
|
|
404
|
+
provider: request => providerOfModel(resolveRunModel(request.model, getSessionModel(request.sessionId), process.env.COS_G2_DEFAULT_MODEL)),
|
|
405
|
+
cliSupported: () => claudeSupportsSteer(),
|
|
406
|
+
},
|
|
389
407
|
})
|
|
390
408
|
|
|
391
409
|
// A job's number is a live reservation from admission until its terminal
|
|
@@ -398,6 +416,8 @@ registerMessageReservationSource(() => queryJobCoordinator.liveMessageReservatio
|
|
|
398
416
|
|
|
399
417
|
export function initQueryJobRuntime() {
|
|
400
418
|
if (!durableQueryJobsEnabled()) return Promise.resolve(queryJobCoordinator.getHealth())
|
|
419
|
+
// 6.78.0: warm the steer probe so /api/models can advertise steer on its first answers.
|
|
420
|
+
if (messagesSteerEnabled()) void claudeSupportsSteer()
|
|
401
421
|
return queryJobCoordinator.init()
|
|
402
422
|
}
|
|
403
423
|
|