@gotcos/glasses-server 6.43.3 → 6.44.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/CHANGELOG.md +50 -0
- package/package.json +1 -1
- package/server/index.ts +2 -0
- package/server/lib/__fixtures__/query-jobs-6.43.3/2099-01-01.jsonl +2 -0
- package/server/lib/archive.ts +7 -3
- package/server/lib/claude-bridge.ts +30 -12
- package/server/lib/conversation.ts +27 -0
- package/server/lib/maintenance-lifecycle.ts +6 -0
- package/server/lib/model-router.ts +4 -0
- package/server/lib/morning-brief-config.ts +22 -1
- package/server/lib/morning-brief-prompt.ts +16 -2
- package/server/lib/morning-brief-runtime.ts +32 -1
- package/server/lib/morning-brief-schedule.ts +19 -2
- package/server/lib/morning-brief-scheduler.ts +111 -9
- package/server/lib/python-bridge.ts +18 -4
- package/server/lib/query-job-coordinator.ts +8 -2
- package/server/lib/query-job-runtime.ts +22 -2
- package/server/lib/query-job-store.ts +33 -3
- package/server/lib/query-job-types.ts +147 -2
- package/server/lib/task-dispatcher.ts +642 -0
- package/server/lib/task-store.ts +624 -0
- package/server/routes/health.ts +11 -1
- package/server/routes/message-ref.ts +2 -1
- package/server/routes/query-jobs.ts +1 -1
- package/server/routes/sessions.ts +10 -1
- package/server/routes/tasks.ts +152 -0
|
@@ -79,7 +79,9 @@ sessionsRouter.post('/sessions/:id/context-break', (req, res) => {
|
|
|
79
79
|
res.json({ ok: true })
|
|
80
80
|
})
|
|
81
81
|
|
|
82
|
-
// GET /api/sessions/:id/messages — client-compatible format (paired Q&A)
|
|
82
|
+
// GET /api/sessions/:id/messages — client-compatible format (paired Q&A).
|
|
83
|
+
// Deliberately carries no origin/originId (6.43.4): this is the legacy paired
|
|
84
|
+
// shape. The labelled views are today/all-messages and the day archive.
|
|
83
85
|
sessionsRouter.get('/sessions/:id/messages', (req, res) => {
|
|
84
86
|
const exchanges = getHistory(req.params.id)
|
|
85
87
|
if (exchanges.length === 0) {
|
|
@@ -315,6 +317,8 @@ sessionsRouter.get('/sessions/today/all-messages', (_req, res) => {
|
|
|
315
317
|
globalMsgNum?: number
|
|
316
318
|
messageEra?: string
|
|
317
319
|
modelPreference?: string
|
|
320
|
+
origin?: 'routine' | 'task'
|
|
321
|
+
originId?: string
|
|
318
322
|
attachments?: MediaAttachmentRef[]
|
|
319
323
|
}> = []
|
|
320
324
|
const liveSessions = getActiveSessions()
|
|
@@ -331,6 +335,10 @@ sessionsRouter.get('/sessions/today/all-messages', (_req, res) => {
|
|
|
331
335
|
const messageEra = ex.messageEra ?? next.messageEra
|
|
332
336
|
const attachments = turnAttachments(session.id, globalMsgNum, messageEra, activeEra, ex.attachments, next.attachments)
|
|
333
337
|
const modelPreference = resolveExchangePairModel(ex, next, session.modelPreference)
|
|
338
|
+
// Both halves of a pair are stamped together by the terminal
|
|
339
|
+
// projection; pick BOTH fields from ONE half so a mid-run insert
|
|
340
|
+
// (stamped later) can never pair one half's kind with the other's id.
|
|
341
|
+
const stamped = ex.origin ? ex : next
|
|
334
342
|
liveMessages.push({
|
|
335
343
|
query: ex.content,
|
|
336
344
|
text: next.content,
|
|
@@ -341,6 +349,7 @@ sessionsRouter.get('/sessions/today/all-messages', (_req, res) => {
|
|
|
341
349
|
...(globalMsgNum != null ? { no: globalMsgNum, globalMsgNum } : {}),
|
|
342
350
|
...(messageEra ? { messageEra } : {}),
|
|
343
351
|
...(modelPreference ? { modelPreference } : {}),
|
|
352
|
+
...(stamped.origin ? { origin: stamped.origin, ...(stamped.originId ? { originId: stamped.originId } : {}) } : {}),
|
|
344
353
|
...(attachments.length > 0 ? { attachments } : {}),
|
|
345
354
|
})
|
|
346
355
|
i++
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Router } from 'express'
|
|
2
|
+
import { queryJobCoordinator } from '../lib/query-job-runtime.js'
|
|
3
|
+
import { runTaskNow } from '../lib/task-dispatcher.js'
|
|
4
|
+
import {
|
|
5
|
+
TaskBridgeError,
|
|
6
|
+
TaskRunError,
|
|
7
|
+
captureTask,
|
|
8
|
+
checkTask,
|
|
9
|
+
listBoard,
|
|
10
|
+
listProjectedTaskRuns,
|
|
11
|
+
loadDispatchCap,
|
|
12
|
+
moveTask,
|
|
13
|
+
saveDispatchCap,
|
|
14
|
+
setTaskRunAt,
|
|
15
|
+
tasksGate,
|
|
16
|
+
workBadgeCount,
|
|
17
|
+
} from '../lib/task-store.js'
|
|
18
|
+
|
|
19
|
+
export const tasksRouter = Router()
|
|
20
|
+
|
|
21
|
+
function sendError(res: import('express').Response, error: unknown): void {
|
|
22
|
+
if (error instanceof TaskRunError) {
|
|
23
|
+
if (error.code === 'task_file_locked') res.setHeader('Retry-After', '2')
|
|
24
|
+
res.status(error.status).json({ error: { code: error.code, message: error.message } })
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
if (error instanceof TaskBridgeError) {
|
|
28
|
+
const status = error.code === 'task_file_locked' ? 503
|
|
29
|
+
: error.code === 'task_not_found' || error.code === 'ambiguous_text' ? (error.code === 'ambiguous_text' ? 409 : 404)
|
|
30
|
+
: error.code === 'cos_pipeline_not_configured' ? 503
|
|
31
|
+
: 400
|
|
32
|
+
if (error.code === 'task_file_locked') res.setHeader('Retry-After', '2')
|
|
33
|
+
res.status(status).json({ error: { code: error.code, message: error.message } })
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
console.error('[tasks]', error)
|
|
37
|
+
res.status(500).json({ error: { code: 'task_store_failed', message: 'Task store failed.' } })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function requireDomain(value: unknown): string {
|
|
41
|
+
if (typeof value !== 'string' || !value) throw new TaskRunError(400, 'invalid_domain', 'domain is required')
|
|
42
|
+
return value
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
tasksRouter.get('/tasks/runs', async (req, res) => {
|
|
46
|
+
if (tasksGate() === 'disabled') {
|
|
47
|
+
return res.status(503).json({ error: { code: 'cos_pipeline_not_configured', message: 'COS pipeline is not configured.' }, gate: 'disabled' })
|
|
48
|
+
}
|
|
49
|
+
const limit = Number.parseInt(String(req.query.limit ?? '20'), 10)
|
|
50
|
+
const runs = await listProjectedTaskRuns(
|
|
51
|
+
jobId => queryJobCoordinator.getSnapshot(jobId).catch(() => undefined),
|
|
52
|
+
Number.isFinite(limit) ? limit : 20,
|
|
53
|
+
)
|
|
54
|
+
res.json({ runs })
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
tasksRouter.get('/tasks/next', async (_req, res) => {
|
|
58
|
+
try {
|
|
59
|
+
const rows = await listBoard('today')
|
|
60
|
+
res.json({ task: rows[0] ?? null })
|
|
61
|
+
} catch (error) {
|
|
62
|
+
sendError(res, error)
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
tasksRouter.post('/tasks/capture', async (req, res) => {
|
|
67
|
+
try {
|
|
68
|
+
const body = req.body ?? {}
|
|
69
|
+
const result = await captureTask({
|
|
70
|
+
domain: requireDomain(body.domain),
|
|
71
|
+
text: String(body.text ?? ''),
|
|
72
|
+
section: String(body.section ?? 'inbox'),
|
|
73
|
+
runAt: typeof body.runAt === 'string' ? body.runAt : undefined,
|
|
74
|
+
captureId: typeof body.captureId === 'string' ? body.captureId : undefined,
|
|
75
|
+
})
|
|
76
|
+
res.status(result.replayed ? 200 : 200).json(result)
|
|
77
|
+
} catch (error) {
|
|
78
|
+
sendError(res, error)
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
tasksRouter.patch('/tasks/by-text', async (req, res) => {
|
|
83
|
+
try {
|
|
84
|
+
const body = req.body ?? {}
|
|
85
|
+
if (typeof body.domain !== 'string' || !body.domain) {
|
|
86
|
+
throw new TaskRunError(400, 'invalid_domain', 'domain is required')
|
|
87
|
+
}
|
|
88
|
+
if (typeof body.text !== 'string' || !body.text) {
|
|
89
|
+
throw new TaskRunError(400, 'text_required', 'text is required')
|
|
90
|
+
}
|
|
91
|
+
await checkTask({
|
|
92
|
+
domain: body.domain,
|
|
93
|
+
text: body.text,
|
|
94
|
+
checked: body.checked !== false,
|
|
95
|
+
})
|
|
96
|
+
res.json({ ok: true })
|
|
97
|
+
} catch (error) {
|
|
98
|
+
sendError(res, error)
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
tasksRouter.patch('/tasks/dispatch-cap', (req, res) => {
|
|
103
|
+
try {
|
|
104
|
+
const cap = Number((req.body ?? {}).capPerDay)
|
|
105
|
+
saveDispatchCap(cap)
|
|
106
|
+
res.json({ capPerDay: loadDispatchCap() })
|
|
107
|
+
} catch (error) {
|
|
108
|
+
sendError(res, error)
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
tasksRouter.get('/tasks', async (req, res) => {
|
|
113
|
+
try {
|
|
114
|
+
const column = typeof req.query.column === 'string' ? req.query.column : undefined
|
|
115
|
+
const rows = await listBoard(column)
|
|
116
|
+
res.json({ tasks: rows, workBadge: workBadgeCount(rows), gate: tasksGate() })
|
|
117
|
+
} catch (error) {
|
|
118
|
+
sendError(res, error)
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
tasksRouter.patch('/tasks/:id', async (req, res) => {
|
|
123
|
+
try {
|
|
124
|
+
const body = req.body ?? {}
|
|
125
|
+
const domain = requireDomain(body.domain)
|
|
126
|
+
if ('runAt' in body) {
|
|
127
|
+
await setTaskRunAt(domain, req.params.id, body.runAt == null || body.runAt === '' ? null : String(body.runAt))
|
|
128
|
+
} else if ('section' in body) {
|
|
129
|
+
await moveTask(domain, req.params.id, String(body.section))
|
|
130
|
+
} else if ('checked' in body) {
|
|
131
|
+
await checkTask({ domain, id: req.params.id, checked: body.checked === true })
|
|
132
|
+
} else {
|
|
133
|
+
throw new TaskRunError(400, 'invalid_patch', 'Expected runAt, section, or checked.')
|
|
134
|
+
}
|
|
135
|
+
res.json({ ok: true })
|
|
136
|
+
} catch (error) {
|
|
137
|
+
sendError(res, error)
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
tasksRouter.post('/tasks/:id/run', async (req, res) => {
|
|
142
|
+
try {
|
|
143
|
+
const domain = requireDomain((req.body ?? {}).domain)
|
|
144
|
+
const result = await runTaskNow(req.params.id, domain)
|
|
145
|
+
res.status(202).json(result)
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error instanceof TaskRunError && error.code === 'dispatch_slots_busy') {
|
|
148
|
+
res.setHeader('Retry-After', '30')
|
|
149
|
+
}
|
|
150
|
+
sendError(res, error)
|
|
151
|
+
}
|
|
152
|
+
})
|