@gotcos/glasses-server 6.47.0 → 6.48.1
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 +26 -0
- package/README.md +35 -0
- package/bin/cli.cjs +22 -0
- package/bin/hooks/cos-session-hook +43 -0
- package/managed-runtime-contract.json +7 -1
- package/package.json +3 -1
- package/server/index.ts +78 -6
- package/server/lib/claude-hooks-installer.ts +403 -0
- package/server/lib/claude-session-registry.ts +44 -0
- package/server/lib/occupancy-probes.ts +43 -0
- package/server/lib/session-hook-events.ts +205 -0
- package/server/lib/session-hook-ledger.ts +133 -0
- package/server/lib/session-hook-spool.ts +272 -0
- package/server/lib/session-hooks-runtime.ts +338 -0
- package/server/lib/session-signal-store.ts +389 -0
- package/server/lib/session-state-derive.ts +249 -0
- package/server/lib/session-stream-bus.ts +93 -9
- package/server/lib/session-stream-events.ts +144 -0
- package/server/lib/thread-drain-kick.ts +153 -0
- package/server/lib/thread-occupancy.ts +36 -1
- package/server/lib/thread-turn-queue-deliver.ts +17 -6
- package/server/lib/thread-turn-queue-store.ts +70 -23
- package/server/lib/thread-turn-queue.ts +19 -3
- package/server/routes/agent-session-stream.ts +169 -11
- package/server/routes/agent-sessions.ts +88 -9
- package/server/routes/claude-sessions.ts +42 -21
- package/server/routes/health.ts +2 -0
- package/server/routes/session-hooks.ts +80 -0
- package/server/routes/thread-turn-queue.ts +15 -0
- package/server/scripts/hooks-cli.ts +48 -0
- package/server/lib/__fixtures__/query-jobs-6.43.3/2099-01-01.jsonl +0 -2
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
import { Router } from 'express'
|
|
19
19
|
import { existsSync } from 'node:fs'
|
|
20
20
|
import { lstat, readdir, readFile, stat } from 'node:fs/promises'
|
|
21
|
-
import {
|
|
22
|
-
import { join, resolve } from 'node:path'
|
|
21
|
+
import { join } from 'node:path'
|
|
23
22
|
import {
|
|
24
23
|
REGISTRY_FILENAME,
|
|
25
24
|
countPeers,
|
|
@@ -28,7 +27,15 @@ import {
|
|
|
28
27
|
type ClaudePeer,
|
|
29
28
|
type PeerProbes,
|
|
30
29
|
type RawClaudeSession,
|
|
30
|
+
type ClaudePeerRecord,
|
|
31
|
+
peerRecordFacts,
|
|
32
|
+
toWirePeer,
|
|
33
|
+
claudeSessionsDir,
|
|
31
34
|
} from '../lib/claude-session-registry.js'
|
|
35
|
+
import { deriveForRow } from '../lib/session-hooks-runtime.js'
|
|
36
|
+
import { derivedRowFields, type RegistryFacts } from '../lib/session-state-derive.js'
|
|
37
|
+
import { queuedTurnsFields, queuedWaitingLookup } from '../lib/thread-turn-queue-store.js'
|
|
38
|
+
import { isAttachedTurnActive, sessionStreamKey } from '../lib/session-stream-bus.js'
|
|
32
39
|
|
|
33
40
|
export const claudeSessionsRouter = Router()
|
|
34
41
|
|
|
@@ -56,20 +63,8 @@ export function claudeSessionNamesVisible(): boolean {
|
|
|
56
63
|
return process.env.COS_CLAUDE_SESSIONS_SHOW_NAMES === '1'
|
|
57
64
|
}
|
|
58
65
|
|
|
59
|
-
/**
|
|
60
|
-
|
|
61
|
-
*
|
|
62
|
-
* `COS_CLAUDE_SESSIONS_DIR` first because it is both the override for a non-standard
|
|
63
|
-
* install AND the test seam — `homedir()` is not mockable, so without an env hook the
|
|
64
|
-
* only testable path would be the real one. Then CLAUDE_CONFIG_DIR, which real
|
|
65
|
-
* installs do set; hardcoding ~/.claude breaks those.
|
|
66
|
-
*/
|
|
67
|
-
export function claudeSessionsDir(): string {
|
|
68
|
-
const explicit = process.env.COS_CLAUDE_SESSIONS_DIR
|
|
69
|
-
if (explicit) return resolve(explicit)
|
|
70
|
-
const configDir = process.env.CLAUDE_CONFIG_DIR
|
|
71
|
-
return join(configDir ? resolve(configDir) : join(homedir(), '.claude'), 'sessions')
|
|
72
|
-
}
|
|
66
|
+
/** Where the registry lives; the definition moved to the registry lib in 6.48.1. */
|
|
67
|
+
export { claudeSessionsDir }
|
|
73
68
|
|
|
74
69
|
const realProbes: PeerProbes = {
|
|
75
70
|
isAlive: pid => {
|
|
@@ -92,6 +87,15 @@ export async function readClaudePeers(
|
|
|
92
87
|
probes: PeerProbes = realProbes,
|
|
93
88
|
showNames = claudeSessionNamesVisible(),
|
|
94
89
|
): Promise<ClaudePeer[]> {
|
|
90
|
+
return (await readClaudePeerRecords(dir, probes, showNames)).map(toWirePeer)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The peers with the deriver's facts attached. Server-side only; see `toWirePeer`. */
|
|
94
|
+
export async function readClaudePeerRecords(
|
|
95
|
+
dir: string,
|
|
96
|
+
probes: PeerProbes = realProbes,
|
|
97
|
+
showNames = claudeSessionNamesVisible(),
|
|
98
|
+
): Promise<ClaudePeerRecord[]> {
|
|
95
99
|
let names: string[]
|
|
96
100
|
try {
|
|
97
101
|
names = await readdir(dir)
|
|
@@ -101,7 +105,7 @@ export async function readClaudePeers(
|
|
|
101
105
|
// separately so this cannot be mistaken for "the feature is off".
|
|
102
106
|
return []
|
|
103
107
|
}
|
|
104
|
-
const peers:
|
|
108
|
+
const peers: ClaudePeerRecord[] = []
|
|
105
109
|
for (const name of names.filter(n => REGISTRY_FILENAME.test(n)).slice(0, MAX_REGISTRY_FILES)) {
|
|
106
110
|
const full = join(dir, name)
|
|
107
111
|
try {
|
|
@@ -114,7 +118,8 @@ export async function readClaudePeers(
|
|
|
114
118
|
let mtimeMs: number | null = null
|
|
115
119
|
try { mtimeMs = (await stat(full)).mtimeMs } catch { /* raced the reaper */ }
|
|
116
120
|
const peer = toPeer(raw, probes, mtimeMs, showNames)
|
|
117
|
-
|
|
121
|
+
const facts = peerRecordFacts(raw)
|
|
122
|
+
if (peer && facts) peers.push({ ...peer, ...facts })
|
|
118
123
|
} catch {
|
|
119
124
|
// ENOENT between readdir and read is NORMAL here — the reaper is actively
|
|
120
125
|
// unlinking these — and a torn read is expected because writes are
|
|
@@ -122,7 +127,12 @@ export async function readClaudePeers(
|
|
|
122
127
|
continue
|
|
123
128
|
}
|
|
124
129
|
}
|
|
125
|
-
return sortPeers(peers)
|
|
130
|
+
return sortPeers(peers) as ClaudePeerRecord[]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The deriver's view of a registry record. */
|
|
134
|
+
export function registryFacts(record: ClaudePeerRecord): RegistryFacts {
|
|
135
|
+
return { alive: record.alive, status: record.status, waitingFor: record.waitingFor, statusUpdatedAt: record.statusUpdatedAt, lastActiveAt: record.lastActiveAt }
|
|
126
136
|
}
|
|
127
137
|
|
|
128
138
|
function boundedInteger(value: unknown, fallback: number, min: number, max: number): number {
|
|
@@ -148,9 +158,20 @@ claudeSessionsRouter.get('/claude-sessions', async (req, res) => {
|
|
|
148
158
|
}
|
|
149
159
|
try {
|
|
150
160
|
const limit = boundedInteger(req.query.limit, 30, 1, 100)
|
|
151
|
-
const
|
|
161
|
+
const records = await readClaudePeerRecords(claudeSessionsDir())
|
|
162
|
+
const peers = records.map(toWirePeer)
|
|
163
|
+
const queuedOf = queuedWaitingLookup(Date.now())
|
|
164
|
+
// 6.48.0: the same derived state every surface reads, stamped ADDITIVELY on the wire
|
|
165
|
+
// peer. `toPeer` stays byte-identical (its key set is pinned); the eight extra keys
|
|
166
|
+
// come from the signal store and the registry facts an older client never sees.
|
|
167
|
+
// 6.48.1: queued_turns joins here too so a peer-only row (no agent-sessions hit)
|
|
168
|
+
// still shows a follow-up waiting.
|
|
152
169
|
res.json({
|
|
153
|
-
peers:
|
|
170
|
+
peers: records.slice(0, limit).map(record => ({
|
|
171
|
+
...toWirePeer(record),
|
|
172
|
+
...derivedRowFields(deriveForRow({ sessionId: record.sessionId, registry: registryFacts(record), attachedTurn: isAttachedTurnActive(sessionStreamKey('claude', record.sessionId)) })),
|
|
173
|
+
...queuedTurnsFields(queuedOf('claude', record.sessionId)),
|
|
174
|
+
})),
|
|
154
175
|
counts: countPeers(peers),
|
|
155
176
|
enabled: true,
|
|
156
177
|
generatedAt: Date.now(),
|
package/server/routes/health.ts
CHANGED
|
@@ -79,6 +79,7 @@ import {
|
|
|
79
79
|
threadAttachCapability,
|
|
80
80
|
threadAttachHealthFields,
|
|
81
81
|
} from '../lib/thread-attach-capability.js'
|
|
82
|
+
import { sessionHooksHealthFields } from '../lib/session-hooks-runtime.js'
|
|
82
83
|
|
|
83
84
|
export const healthRouter = Router()
|
|
84
85
|
|
|
@@ -349,6 +350,7 @@ healthRouter.get('/health', async (_req, res) => {
|
|
|
349
350
|
...checks,
|
|
350
351
|
server_version: managedServerVersion(),
|
|
351
352
|
...threadAttachHealthFields(threadAttach),
|
|
353
|
+
...sessionHooksHealthFields(),
|
|
352
354
|
server_instance_id: getServerInstanceId(),
|
|
353
355
|
boot_id: serverMetrics.bootId,
|
|
354
356
|
generation_id: getServerGenerationId(),
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Session hooks: status, install, uninstall, and today's runs.
|
|
2
|
+
//
|
|
3
|
+
// `GET /api/session-hooks/status` is what Control's banner keys off. Five outcomes on
|
|
4
|
+
// the client side and this route owns three of them: 200 with `installed` (no banner),
|
|
5
|
+
// 200 with `drift`, `missing`, `script_outdated`, `settings_*` (banner with Install). A
|
|
6
|
+
// 404 means a server older than 6.48.0 (`route_absent`), and no answer at all is
|
|
7
|
+
// `unreachable`; neither is "not installed", and Control must never say so for them.
|
|
8
|
+
|
|
9
|
+
import { Router } from 'express'
|
|
10
|
+
import { installClaudeHooks, uninstallClaudeHooks } from '../lib/claude-hooks-installer.js'
|
|
11
|
+
import { deskIdleSeconds, invalidateHookStatus, sessionHooksHealthFields, sessionSignalStore } from '../lib/session-hooks-runtime.js'
|
|
12
|
+
import { workspaceFromCwd } from '../lib/claude-session-registry.js'
|
|
13
|
+
|
|
14
|
+
/** The registry entrypoints a person sits at; everything else (`sdk-cli`, unknown) may be a job. */
|
|
15
|
+
export function isInteractiveEntrypoint(entrypoint: string | null): boolean {
|
|
16
|
+
return entrypoint === 'claude-desktop' || entrypoint === 'cli'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createSessionHooksRouter(options: { port: number }): Router {
|
|
20
|
+
const router = Router()
|
|
21
|
+
|
|
22
|
+
router.get('/session-hooks/status', (_req, res) => {
|
|
23
|
+
res.set('Cache-Control', 'private, no-store')
|
|
24
|
+
res.json({ ok: true, ...sessionHooksHealthFields() })
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
router.post('/session-hooks/install', (req, res) => {
|
|
28
|
+
const dryRun = req.query.dryRun === '1' || (req.body && typeof req.body === 'object' && (req.body as { dryRun?: unknown }).dryRun === true)
|
|
29
|
+
const result = installClaudeHooks({ port: options.port, deskIdleSeconds: deskIdleSeconds(), dryRun })
|
|
30
|
+
invalidateHookStatus()
|
|
31
|
+
if (!result.ok) {
|
|
32
|
+
res.status(409).json({ ok: false, reason: result.reason ?? 'install_failed', status: result.status })
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
res.json({ ok: true, changed: result.changed, scriptCopied: result.scriptCopied, backupPath: result.backupPath, status: result.status, ...(dryRun ? { merged: result.merged } : {}) })
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
router.post('/session-hooks/uninstall', (req, res) => {
|
|
39
|
+
const dryRun = req.query.dryRun === '1'
|
|
40
|
+
const result = uninstallClaudeHooks({ dryRun })
|
|
41
|
+
invalidateHookStatus()
|
|
42
|
+
if (!result.ok) {
|
|
43
|
+
res.status(409).json({ ok: false, reason: result.reason ?? 'uninstall_failed', status: result.status })
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
res.json({ ok: true, changed: result.changed, backupPath: result.backupPath, status: result.status, ...(dryRun ? { merged: result.merged } : {}) })
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Sessions the hooks saw start and end, for Control's scheduled-job ledger. A run
|
|
50
|
+
// shorter than the pet's 20 s poll is recorded here where the poll never saw it.
|
|
51
|
+
router.get('/session-hooks/runs', (req, res) => {
|
|
52
|
+
res.set('Cache-Control', 'private, no-store')
|
|
53
|
+
const since = Number(req.query.since)
|
|
54
|
+
const sinceMs = Number.isFinite(since) && since > 0 ? since : Date.now() - 24 * 60 * 60_000
|
|
55
|
+
// A Desktop tab or a terminal session is not a run (6.48.1): the ledger wants the
|
|
56
|
+
// `claude -p` jobs (`sdk-cli`). `?all=1` lists every session the hooks saw.
|
|
57
|
+
const all = req.query.all === '1'
|
|
58
|
+
const runs: Array<Record<string, unknown>> = []
|
|
59
|
+
for (const signal of sessionSignalStore.snapshot()) {
|
|
60
|
+
if (signal.firstSeenAt < sinceMs && !(signal.ended && signal.ended.at >= sinceMs)) continue
|
|
61
|
+
if (!all && isInteractiveEntrypoint(signal.entrypoint)) continue
|
|
62
|
+
runs.push({
|
|
63
|
+
session_id: signal.sessionId,
|
|
64
|
+
entrypoint: signal.entrypoint,
|
|
65
|
+
started_at: new Date(signal.firstSeenAt).toISOString(),
|
|
66
|
+
ended_at: signal.ended ? new Date(signal.ended.at).toISOString() : null,
|
|
67
|
+
end_reason: signal.ended?.reason ?? null,
|
|
68
|
+
// The registry route reduces cwd to a workspace name on the wire; so does this one.
|
|
69
|
+
workspace: workspaceFromCwd(signal.cwd),
|
|
70
|
+
keep_warm: signal.keepWarm,
|
|
71
|
+
child_events: signal.childEvents,
|
|
72
|
+
last_reply: signal.lastReply || null,
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
runs.sort((a, b) => String(b.started_at).localeCompare(String(a.started_at)))
|
|
76
|
+
res.json({ ok: true, runs, since: new Date(sinceMs).toISOString() })
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
return router
|
|
80
|
+
}
|
|
@@ -16,6 +16,7 @@
|
|
|
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 { COS_SESSION_ID_RE } from './agent-session-bindings.js'
|
|
19
20
|
import {
|
|
20
21
|
admitToQueue, drainDecision, queueableRefusal, queuePosition,
|
|
21
22
|
MAX_DELIVERY_ATTEMPTS, type DrainObservation, type QueuedThreadTurn,
|
|
@@ -27,6 +28,8 @@ export interface ThreadTurnQueueDeps {
|
|
|
27
28
|
occupancy: (provider: string, threadId: string) => { attachable: boolean; reason: string | null }
|
|
28
29
|
/** Did the holder's last transcript record end a turn? */
|
|
29
30
|
turnEnded: (provider: string, threadId: string) => boolean
|
|
31
|
+
/** 6.48.1, optional: is the holder's turn positively OPEN right now (see DrainObservation.turnOpen)? */
|
|
32
|
+
turnOpen?: (provider: string, threadId: string) => boolean
|
|
30
33
|
/** The 30s transcript clock, as a backstop. */
|
|
31
34
|
activity: (provider: string, threadId: string) => 'working' | 'idle' | 'unknown'
|
|
32
35
|
/**
|
|
@@ -77,6 +80,11 @@ function publicRow(turn: QueuedThreadTurn, position: number): Record<string, unk
|
|
|
77
80
|
* return false and spend an attempt. A new refusal added upstream is therefore bounded
|
|
78
81
|
* by default rather than silently retried forever.
|
|
79
82
|
*/
|
|
83
|
+
/** A throwing turnOpen probe is not evidence either way. */
|
|
84
|
+
function safeTurnOpen(deps: ThreadTurnQueueDeps, provider: string, threadId: string): boolean | undefined {
|
|
85
|
+
try { return deps.turnOpen!(provider, threadId) } catch { return undefined }
|
|
86
|
+
}
|
|
87
|
+
|
|
80
88
|
function isRetryableDelivery(outcome: { reason?: string; serverRetryable?: boolean }): boolean {
|
|
81
89
|
// The turn route publishes its own verdict; it outranks our inference either way.
|
|
82
90
|
if (outcome.serverRetryable === false) return false
|
|
@@ -116,6 +124,7 @@ export async function drainThread(
|
|
|
116
124
|
const seen: DrainObservation = {
|
|
117
125
|
attachable: gate.attachable,
|
|
118
126
|
turnEnded: deps.turnEnded(provider, threadId),
|
|
127
|
+
turnOpen: deps.turnOpen ? safeTurnOpen(deps, provider, threadId) : undefined,
|
|
119
128
|
activity: deps.activity(provider, threadId),
|
|
120
129
|
reason: gate.reason,
|
|
121
130
|
}
|
|
@@ -212,6 +221,12 @@ export function createThreadTurnQueueRouter(deps: ThreadTurnQueueDeps): Router {
|
|
|
212
221
|
if (!clientTurnId || !cosSessionId || !prompt.trim()) {
|
|
213
222
|
return res.status(400).json({ error: 'invalid_request' })
|
|
214
223
|
}
|
|
224
|
+
// 6.48.1: the attach route refuses an id outside COS_SESSION_ID_RE as invalid_request,
|
|
225
|
+
// which the drainer treats as retryable, so a bad id used to sit in the queue for the
|
|
226
|
+
// whole TTL. Refuse it here, where the client can still fix it.
|
|
227
|
+
if (!COS_SESSION_ID_RE.test(cosSessionId)) {
|
|
228
|
+
return res.status(400).json({ error: 'invalid_request', reason: 'invalid_cos_session_id' })
|
|
229
|
+
}
|
|
215
230
|
|
|
216
231
|
if (provider === 'cursor') {
|
|
217
232
|
return res.status(423).json({ error: 'unsupported_provider', queueable: false })
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
// Install, inspect or remove the COS session hook in ~/.claude/settings.json.
|
|
3
|
+
//
|
|
4
|
+
// npx --yes @gotcos/glasses-server@latest --hooks install [--dry-run] [--port 3141]
|
|
5
|
+
// npx --yes @gotcos/glasses-server@latest --hooks status
|
|
6
|
+
// npx --yes @gotcos/glasses-server@latest --hooks uninstall [--dry-run]
|
|
7
|
+
//
|
|
8
|
+
// Prints one JSON document. Exit 0 on success, 2 on a refusal (unparseable or symlinked
|
|
9
|
+
// settings, a missing packaged script), 64 on a bad argument. Never prints the token.
|
|
10
|
+
|
|
11
|
+
import { hookStatus, installClaudeHooks, uninstallClaudeHooks } from '../lib/claude-hooks-installer.js'
|
|
12
|
+
import { sessionHooksEnabled } from '../lib/session-hooks-runtime.js'
|
|
13
|
+
|
|
14
|
+
const args = process.argv.slice(2)
|
|
15
|
+
const action = args.find(a => !a.startsWith('--'))
|
|
16
|
+
const dryRun = args.includes('--dry-run')
|
|
17
|
+
const portIndex = args.indexOf('--port')
|
|
18
|
+
const port = portIndex >= 0 ? Number(args[portIndex + 1]) : Number(process.env.PORT ?? 3141)
|
|
19
|
+
const deskIndex = args.indexOf('--desk-idle-s')
|
|
20
|
+
const deskIdleSeconds = deskIndex >= 0 ? Number(args[deskIndex + 1]) : Number(process.env.COS_PERMISSION_BROKER_DESK_IDLE_S ?? 90)
|
|
21
|
+
|
|
22
|
+
function print(value: unknown): void {
|
|
23
|
+
console.log(JSON.stringify(value, null, 2))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// `serverApplies` says whether the running server would READ the spool into rows: the
|
|
27
|
+
// hooks can be installed while the feature is off (COS_CLAUDE_SESSIONS_ENABLED unset),
|
|
28
|
+
// and a status that said only "installed" would hide that.
|
|
29
|
+
const serverApplies = sessionHooksEnabled()
|
|
30
|
+
const flagsNote = serverApplies ? undefined : 'Rows change only when the server runs with COS_CLAUDE_SESSIONS_ENABLED=1 (or COS_SESSION_HOOKS=1).'
|
|
31
|
+
|
|
32
|
+
if (action === 'status') {
|
|
33
|
+
print({ action, serverApplies, ...(flagsNote ? { note: flagsNote } : {}), ...hookStatus() })
|
|
34
|
+
process.exit(0)
|
|
35
|
+
}
|
|
36
|
+
if (action === 'install') {
|
|
37
|
+
if (!Number.isFinite(port) || port <= 0) { console.error('Bad --port'); process.exit(64) }
|
|
38
|
+
const result = installClaudeHooks({ port, deskIdleSeconds: Number.isFinite(deskIdleSeconds) ? deskIdleSeconds : 90, dryRun })
|
|
39
|
+
print({ action, dryRun, serverApplies, ...(flagsNote ? { note: flagsNote } : {}), ...result })
|
|
40
|
+
process.exit(result.ok ? 0 : 2)
|
|
41
|
+
}
|
|
42
|
+
if (action === 'uninstall') {
|
|
43
|
+
const result = uninstallClaudeHooks({ dryRun })
|
|
44
|
+
print({ action, dryRun, ...result })
|
|
45
|
+
process.exit(result.ok ? 0 : 2)
|
|
46
|
+
}
|
|
47
|
+
console.error('Usage: --hooks install|status|uninstall [--dry-run] [--port N] [--desk-idle-s N]')
|
|
48
|
+
process.exit(64)
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
{"schemaVersion":1,"recordId":"02b09947-ed7d-47ef-99c5-4985af3ec924","partitionDay":"2099-01-01","persistedAt":"2099-01-01T12:00:00.000Z","bootId":"boot-6-43-3-fixture","jobId":"43b397bd-dd67-4312-9a5c-4c5d4b246f1d","clientJobId":"11111111-1111-4111-8111-111111111111","generation":2,"turnId":"a456fd1d-1e82-4b2d-8a20-5d34681201fd","requestFingerprint":"024966024134bf0d46090335da03a732a7fb7eaea161b1fd07dc4c5b9d536e95","eventSeq":1,"type":"accepted","status":"accepted","request":{"clientJobId":"11111111-1111-4111-8111-111111111111","generation":2,"query":"written by 6.43.3","sessionId":"session-6-43-3","model":"opus","effort":"high","cursorExecutionMode":"ask","messageEra":"era1","globalMsgNum":78,"reference":{"query":"earlier q","response":"earlier a"},"handoffCode":"ABCD","handoffLatest":true,"clientQueueItemId":"q1","attachmentIds":[],"attachmentRefs":[],"activityToolMode":"status"},"patch":{},"eventData":{"requestFingerprint":"024966024134bf0d46090335da03a732a7fb7eaea161b1fd07dc4c5b9d536e95"}}
|
|
2
|
-
{"schemaVersion":1,"recordId":"a1145a40-2ced-4da3-9cf7-76d84cffc946","partitionDay":"2099-01-01","persistedAt":"2099-01-01T12:00:00.000Z","bootId":"boot-6-43-3-fixture","jobId":"64c5b502-13f2-4871-b6c4-007b91be2414","clientJobId":"22222222-2222-4222-8222-222222222222","generation":1,"turnId":"843e633f-0f92-4fe3-b2a1-9dc683715791","requestFingerprint":"91edfdeeb088e9fb3b1ffab4202bca02d3e7a23674d8b54dd51a50ce9adbf638","eventSeq":1,"type":"accepted","status":"accepted","request":{"clientJobId":"22222222-2222-4222-8222-222222222222","generation":1,"query":"minimal by 6.43.3","sessionId":"session-6-43-3-min","attachmentIds":[],"attachmentRefs":[],"activityToolMode":"status"},"patch":{},"eventData":{"requestFingerprint":"91edfdeeb088e9fb3b1ffab4202bca02d3e7a23674d8b54dd51a50ce9adbf638"}}
|