@gotcos/glasses-server 6.58.1 → 6.58.2
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 +11 -0
- package/docs/work-intake.md +2 -0
- package/package.json +1 -1
- package/server/lib/agent-session-store.ts +11 -1
- package/server/lib/claude-bridge.ts +32 -1
- package/server/lib/claude-cli-support.ts +37 -0
- package/server/lib/query-job-coordinator.ts +7 -0
- package/server/lib/query-job-runtime.ts +3 -0
- package/server/lib/query-job-types.ts +42 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
## 6.58.2
|
|
2
|
+
|
|
3
|
+
### A New session from Work is named after its task, and visible while it runs
|
|
4
|
+
|
|
5
|
+
- Miles, 2026-09-29 at 20:40, on the first COS Control 0.5.249 New session (job c3878966, 20:36:49 to 20:39:11): Control opened it in Claude 31 s after the run ended, and it sat at the top of the MU-Chief-Staff sidebar as "General coding session". "This is super generic. It should be based off of the same name of the task that we're sending in." And while it ran: "I don't see anything in the sidebar ... We should probably show the session behind this." The card said "session live status unavailable" and Sessions listed the run as a COS server job.
|
|
6
|
+
- **A new Claude session is named.** A query job may carry `sessionName`. The server cleans it: control characters, zero-width spaces and direction marks and overrides become spaces (the joiners U+200C and U+200D stay), whitespace collapses, and a name over 100 characters is cut at 100, back to its last space when that space is in the second half; the cut counts whole characters, so it never splits an emoji. When the job starts a Claude session of its own, the server runs `claude --name=<name>`, which writes the session's title; the Claude app shows it in the sidebar. Never on a Continue into a session the COS session already has, never on a read-only dispatch, and a named session never takes the session the server warmed at boot (that one opens with "ready" under the glasses prompt); it starts clean. The server asks `claude --help` once whether the installed CLI takes `--name`; one that does not runs the session unnamed, as before. The name is outside the request fingerprint, so a retry that renames the same job is the same job, and an older server drops the field.
|
|
7
|
+
- **The job names its Claude session while it runs.** Claude announces its session id in its first stream event (a hook or init line, before any answer text); before this the server recorded it only with the final result, so a New session from Work could not be linked, shown or opened while it ran. The server now records it on the run and on the job (`cliSessionId`, through the same link update as the provider process) from that first event, only while the job is running; a job that already ended keeps what it had. At completion the result's id is recorded as before. Claude only: Codex, Cursor and Ollama jobs are unchanged.
|
|
8
|
+
- **Session titles in long transcripts.** The session list reads a Claude session's custom title from the end of its transcript; a transcript longer than the 256 KB window whose only title sits near the top now falls back to the top.
|
|
9
|
+
- Proven on this Mac first (claude 2.1.285): `claude -p --name` writes `{"type":"custom-title",...}` into the transcript (and again as the session goes on), the same line the Claude app shows as a session's name; `--resume <id> --name` renames; a name that starts with a dash, in the `--name=` form, is taken as the name; and every stream event, from the first hook line, carries the session id.
|
|
10
|
+
- Tests: the name's cleaning, joiners kept, the cut (at the last space, exactly at the limit, a run of emoji and of joined family emoji cut in whole characters), the request carrying it outside the fingerprint; `--name=` only for a session the COS session starts (not a Continue, not a dispatch, not the warmed session, not on a CLI without the option, a dash-leading name kept as one argument); the help probe (asked once, a CLI or help failure is a no); the first event with an id linking once (not an event without one, not a dispatch); the job showing the session while still running and keeping it at completion; a link after the job ended changing nothing; the runtime passing the name down and the early id up; a title at either end of a long transcript.
|
|
11
|
+
|
|
1
12
|
## 6.58.1
|
|
2
13
|
|
|
3
14
|
### A copy that lost its audio no longer holds the ring
|
package/docs/work-intake.md
CHANGED
|
@@ -34,3 +34,5 @@ The journal is `work-intake/intake.json` in the server data directory, owned by
|
|
|
34
34
|
Since 6.57.1 `POST /api/work-board/session-recommendation` also takes exactly `{ reviewId, sessions }` for a meeting review (`reviewId` = `wr_` + 32 hex). The server reads the review's meeting title and reviewed follow-up from its own store; the client never sends work text. Errors: 400 `invalid_review_request` (bad id or session list), 404 `review_not_found`, 404 `reviews_unavailable` (reviews off), 503 `review_store_unavailable` (defensive; the stock lookup reads memory).
|
|
35
35
|
|
|
36
36
|
Since 6.58.0 `POST /api/work-board/completion-check` takes exactly `{ domain, id, provider, sessionId }` plus an optional `after` (ISO time). It asks Jev whether the session's replies since `after` (or its newest reply) show the task finished, judged against its Done when or, without one, its own text. The server reads the task from the board and the replies from its own transcripts; the client sends names and a time only. It answers `{ provider: "jev", verdict: "done" | "not_done" | "unclear", confidence, basis: "done_when" | "task", model, cached }` or `{ provider: "none", reason }` (`no_reply`, `no_task_text`, a Jev condition, `completion_unavailable`). Errors: 400 `invalid_completion_request`, 404 `task_not_found`, 404 `session_not_found`. COS Control 0.5.247 asks only while the session is idle and its newest reply since the handoff has no `COS-WORK` status line, at most twice per handoff, and moves a card to QA only on done at 0.8 (0.85 without a Done when).
|
|
37
|
+
|
|
38
|
+
Since 6.58.2 `POST /api/query-jobs` also takes an optional `sessionName` (a string; the server cleans it as the 6.58.2 changelog says and keeps at most 100 characters; anything else is no name, never an error). It names only a Claude session the job starts itself (`claude --name=<name>`, when the installed CLI lists `--name`); a Continue, a dispatch, a Codex, Cursor or Ollama job, and an older server ignore it, and it is not part of the request fingerprint. A Claude job's snapshot carries `cliSessionId` from Claude's first stream event while the job is still `running`, not only at completion. COS Control 0.5.250 sends the Work item's title for New session, Start work and Fork to Claude, and links its receipt as soon as the running job names the session.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.58.
|
|
3
|
+
"version": "6.58.2",
|
|
4
4
|
"description": "COS Glasses \u2014 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": {
|
|
@@ -580,7 +580,17 @@ export function parseJsonLine(line: string): Record<string, unknown> | null {
|
|
|
580
580
|
}
|
|
581
581
|
|
|
582
582
|
export async function lastCustomTitle(path: string): Promise<string | null> {
|
|
583
|
-
|
|
583
|
+
// The CLI re-writes its custom-title line as a session goes on, so the tail normally holds the
|
|
584
|
+
// newest. 6.58.2: a transcript longer than the window whose only title sits near the top (a
|
|
585
|
+
// `claude -p --name` run that wrote a long answer) falls back to the head.
|
|
586
|
+
const tail = customTitleIn(await readWindow(path, true))
|
|
587
|
+
if (tail) return tail
|
|
588
|
+
const st = await fileStat(path)
|
|
589
|
+
if (!st?.isFile || st.size <= HEAD_BYTES) return null
|
|
590
|
+
return customTitleIn(await readWindow(path, false))
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function customTitleIn(text: string): string | null {
|
|
584
594
|
let found: string | null = null
|
|
585
595
|
for (const line of text.split('\n')) {
|
|
586
596
|
if (!line.includes('custom-title')) continue
|
|
@@ -60,6 +60,7 @@ import { terminalProviderAuthFailure } from './provider-terminal-error.js'
|
|
|
60
60
|
import { claudePermissionArgs, getClaudeTrustMode } from './claude-permissions.js'
|
|
61
61
|
import { terminateProviderProcess } from './provider-process-lifecycle.js'
|
|
62
62
|
import { teeJobTrail, type JobTrailDraft } from './job-trail.js'
|
|
63
|
+
import { claudeSupportsSessionName } from './claude-cli-support.js'
|
|
63
64
|
|
|
64
65
|
// Inactivity = no stdout data for this long → kill (catches stalls)
|
|
65
66
|
const INACTIVITY_BY_MODEL: Record<ClaudeModelPreference, number> = {
|
|
@@ -340,6 +341,12 @@ export interface StreamCallbacks {
|
|
|
340
341
|
/** 6.52.0: readable steps of a durable Messages job (lib/job-trail.ts). Optional, and
|
|
341
342
|
* absent on every path that is not a durable job with the trail on. */
|
|
342
343
|
onTrail?: (draft: JobTrailDraft) => void
|
|
344
|
+
/**
|
|
345
|
+
* 6.58.2: the provider named its own session, at the start of the run. Claude announces
|
|
346
|
+
* its session id in the first stream event; before this the id reached the job only with
|
|
347
|
+
* the result, so COS Control could not show a New session from Work while it ran.
|
|
348
|
+
*/
|
|
349
|
+
onNativeSession?: (linkage: { cliSessionId: string }) => void
|
|
343
350
|
}
|
|
344
351
|
|
|
345
352
|
/** Claude CLI can emit `subtype: success` with `is_error: true`; the boolean
|
|
@@ -395,6 +402,8 @@ export interface CallOptions {
|
|
|
395
402
|
sessionLockHeld?: boolean
|
|
396
403
|
/** Read-only scheduled-task dispatch. Same shape as QueryJobRequest.dispatch. */
|
|
397
404
|
dispatch?: { restricted: true; tools: readonly string[] }
|
|
405
|
+
/** 6.58.2: `claude --name` for a session this COS session starts fresh (QueryJobRequest.sessionName). */
|
|
406
|
+
sessionName?: string
|
|
398
407
|
}
|
|
399
408
|
|
|
400
409
|
export async function callClaudeStreaming(
|
|
@@ -426,6 +435,9 @@ export async function callClaudeStreaming(
|
|
|
426
435
|
// Pass existing CLI session ID if resuming (new sessions get it after first result)
|
|
427
436
|
const resolvedCliKey = cliSessionKey(sid, resolvedModel)
|
|
428
437
|
let existingCliSession = options?.dispatch ? undefined : cliSessionMap.get(resolvedCliKey)
|
|
438
|
+
// 6.58.2: decided before the pre-warmed session below stands in: a COS session that has no
|
|
439
|
+
// Claude session of its own yet starts one here, and only that one may be named.
|
|
440
|
+
const startsOwnCliSession = !existingCliSession
|
|
429
441
|
callbacks.onStart?.(resolvedModel, sid, existingCliSession, {
|
|
430
442
|
clientJobId: options?.clientJobId,
|
|
431
443
|
generation: options?.generation,
|
|
@@ -531,7 +543,10 @@ export async function callClaudeStreaming(
|
|
|
531
543
|
// Check if we have a prior CLI session for this COS session.
|
|
532
544
|
// If not, use the pre-warmed session (eliminates 2-15s cold start on first query).
|
|
533
545
|
// Restricted dispatch never steals the pre-warmed unrestricted session.
|
|
534
|
-
|
|
546
|
+
// 6.58.2: nor does a named New session (COS Control Work). The warmed session opens with the
|
|
547
|
+
// boot prompt "ready" under the glasses prompt; a session someone will open in the Claude app
|
|
548
|
+
// under its task's name starts clean.
|
|
549
|
+
if (!options?.dispatch && !options?.sessionName && !existingCliSession && preWarmedCliSessionId && resolvedModel === DEFAULT_MODEL) {
|
|
535
550
|
// Only the default model consumes the pre-warmed session.
|
|
536
551
|
// Hey Even (Haiku) cold-starts its own session to avoid model contamination.
|
|
537
552
|
existingCliSession = preWarmedCliSessionId
|
|
@@ -576,6 +591,11 @@ export async function callClaudeStreaming(
|
|
|
576
591
|
// Resume prior CLI session — reuses cached context, avoids cold start
|
|
577
592
|
args.push('--resume', existingCliSession)
|
|
578
593
|
}
|
|
594
|
+
// 6.58.2: name the session this COS session starts (never a Continue, never a dispatch), when
|
|
595
|
+
// the installed CLI takes --name. The `=` form keeps a name that starts with a dash a value.
|
|
596
|
+
if (startsOwnCliSession && !options?.dispatch && options?.sessionName && await claudeSupportsSessionName()) {
|
|
597
|
+
args.push(`--name=${options.sessionName}`)
|
|
598
|
+
}
|
|
579
599
|
|
|
580
600
|
// Strip CLAUDECODE env var so claude -p doesn't think it's nested
|
|
581
601
|
const env = { ...process.env }
|
|
@@ -856,6 +876,8 @@ export async function callClaudeStreaming(
|
|
|
856
876
|
|
|
857
877
|
// ─── Process stdout ───
|
|
858
878
|
|
|
879
|
+
// 6.58.2: onNativeSession fires once per run.
|
|
880
|
+
let nativeSessionAnnounced = false
|
|
859
881
|
proc.stdout.on('data', (chunk: Buffer) => {
|
|
860
882
|
resetInactivity()
|
|
861
883
|
buffer += chunk.toString()
|
|
@@ -871,6 +893,15 @@ export async function callClaudeStreaming(
|
|
|
871
893
|
try {
|
|
872
894
|
const event = JSON.parse(trimmed)
|
|
873
895
|
|
|
896
|
+
// 6.58.2: the first event that carries the session id (a hook or init line, before any
|
|
897
|
+
// answer text) links the run and the job to it. At completion the result's id is
|
|
898
|
+
// recorded as before, so the finished job names the session the result names.
|
|
899
|
+
if (!nativeSessionAnnounced && !options?.dispatch && typeof event.session_id === 'string' && event.session_id) {
|
|
900
|
+
nativeSessionAnnounced = true
|
|
901
|
+
updateClaudeRun(run.runId, { cliSessionId: event.session_id })
|
|
902
|
+
callbacks.onNativeSession?.({ cliSessionId: event.session_id })
|
|
903
|
+
}
|
|
904
|
+
|
|
874
905
|
if (event.type === 'system' && event.subtype === 'init' && typeof event.model === 'string') {
|
|
875
906
|
updateClaudeRun(run.runId, { resolvedModelId: event.model })
|
|
876
907
|
} else if (event.type === 'stream_event') {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 6.58.2: does the installed Claude CLI take `--name`? A CLI that predates the option rejects
|
|
5
|
+
* it as unknown and the whole run fails, so the server asks `claude --help` once, in the
|
|
6
|
+
* background (it answers in about 50 ms on this Mac), and names a session only when the help
|
|
7
|
+
* lists the option. Any failure to ask is a no: the session then runs unnamed, as before 6.58.2.
|
|
8
|
+
*/
|
|
9
|
+
let sessionNameSupport: Promise<boolean> | null = null
|
|
10
|
+
|
|
11
|
+
const HELP_TIMEOUT_MS = 5_000
|
|
12
|
+
|
|
13
|
+
export function claudeSupportsSessionName(): Promise<boolean> {
|
|
14
|
+
if (!sessionNameSupport) {
|
|
15
|
+
sessionNameSupport = new Promise<boolean>(resolve => {
|
|
16
|
+
try {
|
|
17
|
+
execFile('claude', ['--help'], { timeout: HELP_TIMEOUT_MS, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => {
|
|
18
|
+
const supported = !error && helpListsNameOption(`${stdout}\n${stderr}`)
|
|
19
|
+
if (!supported) console.warn('[claude-bridge] this Claude CLI does not list --name; new sessions stay unnamed')
|
|
20
|
+
resolve(supported)
|
|
21
|
+
})
|
|
22
|
+
} catch {
|
|
23
|
+
resolve(false)
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
return sessionNameSupport
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The help line reads `-n, --name <name>` on Claude Code 2.1.285. */
|
|
31
|
+
export function helpListsNameOption(help: string): boolean {
|
|
32
|
+
return /(^|[\s,])--name[\s=<[]/m.test(help)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resetClaudeCliSupportForTests(): void {
|
|
36
|
+
sessionNameSupport = null
|
|
37
|
+
}
|
|
@@ -39,6 +39,10 @@ export interface QueryJobRunnerCallbacks {
|
|
|
39
39
|
/** True only when provider ownership was durably fsynced for this active
|
|
40
40
|
* generation. False means cancel/shutdown won; no prompt may be written. */
|
|
41
41
|
onProviderProcess: (linkage: QueryJobProviderLinkage) => boolean | Promise<boolean>
|
|
42
|
+
/** 6.58.2: a link the provider names mid-run (the Claude session id). Persisted only while
|
|
43
|
+
* the job runs (like every link update it restamps providerOwnershipConfirmedAt); unlike
|
|
44
|
+
* onProviderProcess it gates nothing, and a job that already ended keeps what it had. */
|
|
45
|
+
onLinkage?: (linkage: QueryJobProviderLinkage) => Promise<void>
|
|
42
46
|
onChunk: (text: string) => void
|
|
43
47
|
onToolStatus: (text: string) => void
|
|
44
48
|
onActivityLine: (line: { kind: 'input' | 'output'; text: string }) => void
|
|
@@ -340,6 +344,9 @@ export class QueryJobCoordinator {
|
|
|
340
344
|
})
|
|
341
345
|
},
|
|
342
346
|
onProviderProcess: linkage => this.providerProcessReady(active, linkage),
|
|
347
|
+
onLinkage: linkage => this.enqueueCallback(active, async () => {
|
|
348
|
+
await this.store.updateLinkage(active.jobId, linkage)
|
|
349
|
+
}),
|
|
343
350
|
onChunk: (text) => { this.queuePartial(active, text) },
|
|
344
351
|
onToolStatus: (text) => {
|
|
345
352
|
if (active.request.activityToolMode === 'off') text = 'Processing...'
|
|
@@ -234,6 +234,8 @@ const runner: QueryJobRunner = async ({ jobId, turnId, request, signal, callback
|
|
|
234
234
|
? { ollamaRunId: metadata.runId }
|
|
235
235
|
: { codexRunId: metadata.runId }),
|
|
236
236
|
}),
|
|
237
|
+
// 6.58.2: the provider's own session id, as soon as it names it (Claude: first stream event).
|
|
238
|
+
onNativeSession: linkage => { void callbacks.onLinkage?.({ provider: 'claude', cliSessionId: linkage.cliSessionId }) },
|
|
237
239
|
onChunk: text => { callbacks.onChunk(text) },
|
|
238
240
|
onToolStatus: toolName => {
|
|
239
241
|
const message = request.activityToolMode === 'off'
|
|
@@ -349,6 +351,7 @@ const runner: QueryJobRunner = async ({ jobId, turnId, request, signal, callback
|
|
|
349
351
|
attachmentPromptBlock: resolvedAttachments.promptBlock,
|
|
350
352
|
sessionLockHeld: true,
|
|
351
353
|
...(request.dispatch ? { dispatch: request.dispatch } : {}),
|
|
354
|
+
...(request.sessionName ? { sessionName: request.sessionName } : {}),
|
|
352
355
|
},
|
|
353
356
|
)
|
|
354
357
|
}
|
|
@@ -120,6 +120,45 @@ export interface QueryJobRequest {
|
|
|
120
120
|
origin?: QueryJobOrigin
|
|
121
121
|
/** Read-only dispatch constraint. Excluded from the fingerprint. */
|
|
122
122
|
dispatch?: { restricted: true; tools: readonly string[] }
|
|
123
|
+
/**
|
|
124
|
+
* 6.58.2: the display name a NEW Claude session gets (`claude --name`), so a session COS
|
|
125
|
+
* Control starts for a Work item shows under that item's name in the Claude app instead of
|
|
126
|
+
* "General coding session". Sanitized by sanitizeSessionName. Excluded from the fingerprint:
|
|
127
|
+
* it names the session, it does not change what is asked.
|
|
128
|
+
*/
|
|
129
|
+
sessionName?: string
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 6.58.2: longest session name kept (the Claude sidebar shows about 40 characters). */
|
|
133
|
+
export const QUERY_JOB_SESSION_NAME_MAX = 100
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 6.58.2: a display name, or undefined. Control characters, zero-width spaces and direction
|
|
137
|
+
* marks and overrides become spaces (the joiners U+200C and U+200D stay: scripts and emoji need
|
|
138
|
+
* them), runs of whitespace collapse, and a name longer than QUERY_JOB_SESSION_NAME_MAX
|
|
139
|
+
* characters (grapheme clusters, as Swift counts them) is cut there, back to its last space when
|
|
140
|
+
* that space is in the second half. The cut never splits a character. Never throws: a bad name
|
|
141
|
+
* is no name. COS Control applies the same rules before it sends the name.
|
|
142
|
+
*/
|
|
143
|
+
export function sanitizeSessionName(raw: unknown): string | undefined {
|
|
144
|
+
if (typeof raw !== 'string') return undefined
|
|
145
|
+
// eslint-disable-next-line no-control-regex
|
|
146
|
+
const flat = raw.replace(/[\u0000-\u001f\u007f-\u009f\u200b\u200e\u200f\u202a-\u202e\u2066-\u2069\ufeff]/g, ' ').replace(/\s+/g, ' ').trim()
|
|
147
|
+
if (!flat) return undefined
|
|
148
|
+
const chars = graphemes(flat)
|
|
149
|
+
if (chars.length <= QUERY_JOB_SESSION_NAME_MAX) return flat
|
|
150
|
+
const cut = chars.slice(0, QUERY_JOB_SESSION_NAME_MAX)
|
|
151
|
+
const space = cut.lastIndexOf(' ')
|
|
152
|
+
return (space >= QUERY_JOB_SESSION_NAME_MAX / 2 ? cut.slice(0, space) : cut).join('').trim()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const graphemeSegmenter = typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function'
|
|
156
|
+
? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
|
|
157
|
+
: null
|
|
158
|
+
|
|
159
|
+
/** Grapheme clusters when the runtime can segment them, else code points (never half a pair). */
|
|
160
|
+
function graphemes(text: string): string[] {
|
|
161
|
+
return graphemeSegmenter ? Array.from(graphemeSegmenter.segment(text), part => part.segment) : Array.from(text)
|
|
123
162
|
}
|
|
124
163
|
|
|
125
164
|
export const DISPATCH_ALLOWED_TOOLS = ['Read', 'Grep', 'Glob'] as const
|
|
@@ -325,6 +364,7 @@ export function parseQueryJobRequest(raw: unknown, options: ParseQueryJobRequest
|
|
|
325
364
|
const messageEra = optionalString(input.messageEra, 'message_era', 80)
|
|
326
365
|
const handoffCode = optionalString(input.handoffCode, 'handoff_code', 128)
|
|
327
366
|
const clientQueueItemId = optionalString(input.clientQueueItemId, 'client_queue_item_id', 120)
|
|
367
|
+
const sessionName = sanitizeSessionName(input.sessionName)
|
|
328
368
|
const globalMsgNum = input.globalMsgNum == null ? undefined : Number(input.globalMsgNum)
|
|
329
369
|
if (globalMsgNum != null && (!Number.isSafeInteger(globalMsgNum) || globalMsgNum < 1)) {
|
|
330
370
|
throw new QueryJobValidationError('invalid_global_msg_num')
|
|
@@ -395,6 +435,7 @@ export function parseQueryJobRequest(raw: unknown, options: ParseQueryJobRequest
|
|
|
395
435
|
...(handoffCode ? { handoffCode } : {}),
|
|
396
436
|
...(input.handoffLatest === true ? { handoffLatest: true } : {}),
|
|
397
437
|
...(clientQueueItemId ? { clientQueueItemId } : {}),
|
|
438
|
+
...(sessionName ? { sessionName } : {}),
|
|
398
439
|
attachmentIds,
|
|
399
440
|
attachmentRefs,
|
|
400
441
|
activityToolMode,
|
|
@@ -464,7 +505,7 @@ export const FINGERPRINT_KEYS = [
|
|
|
464
505
|
/** The request keys deliberately OUTSIDE the identity. Every key of
|
|
465
506
|
* `QueryJobRequest` must appear in exactly one of the two lists: adding a key
|
|
466
507
|
* without naming it here or above fails to compile, so the author picks a side. */
|
|
467
|
-
export const FINGERPRINT_EXCLUDED = ['origin', 'dispatch'] as const satisfies readonly (keyof QueryJobRequest)[]
|
|
508
|
+
export const FINGERPRINT_EXCLUDED = ['origin', 'dispatch', 'sessionName'] as const satisfies readonly (keyof QueryJobRequest)[]
|
|
468
509
|
type FingerprintUncovered = Exclude<keyof QueryJobRequest, typeof FINGERPRINT_KEYS[number] | typeof FINGERPRINT_EXCLUDED[number]>
|
|
469
510
|
export const FINGERPRINT_KEYS_COVER_REQUEST: FingerprintUncovered extends never ? true : never = true
|
|
470
511
|
|