@gotcos/glasses-server 6.57.0 → 6.57.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
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## 6.57.1
|
|
2
|
+
|
|
3
|
+
### Session suggestions for meeting reviews
|
|
4
|
+
|
|
5
|
+
- Miles, 2026-09-28: the meeting review "Retail Liquor Summit Campaign Launch" got no session suggestion, although a session named "Retail Liquor Summit campaign launch" existed. Suggestions only took a board task. `POST /api/work-board/session-recommendation` now also takes exactly `{reviewId, sessions}`. The server reads the review's meeting title and reviewed follow-up from its own review store; the client never supplies the work text, the same rule as for tasks. An unknown review answers 404 `review_not_found`, a server with reviews off answers 404 `reviews_unavailable`, a review request with a bad id or session list answers 400 `invalid_review_request` (its own code, so Control can tell it from a 6.57.0 server), and a mixed or unknown body answers 400. Task requests are unchanged. Needs COS Control 0.5.243 to ask for reviews.
|
|
6
|
+
- The review is read with a new `peek`, straight from the review store. The existing `get` reconciled every review row on each call, and suggestions ask on each Work visit. A lookup that throws answers 503 `review_store_unavailable` rather than "not found" (defensive: the stock lookup reads memory). `WorkReviewStore.get` now clones only the matched row.
|
|
7
|
+
- The local Work candidate passes its own review runtime the same way.
|
|
8
|
+
- Tests: the route test covers the review body, the missing review, reviews off, the exact-shape refusals and the failure fallback; the runtime test covers `peek` (no reconcile, no meeting resolution). The mutation gate adds seven mutants for the new branches (including `peek` and the review 400 code) and now runs the review runtime tests. docs/work-intake.md documents the review request.
|
|
9
|
+
|
|
1
10
|
## 6.57.0
|
|
2
11
|
|
|
3
12
|
### Jev in the server: session suggestions and a key you can set from COS Control
|
package/docs/work-intake.md
CHANGED
|
@@ -30,3 +30,5 @@ The journal is `work-intake/intake.json` in the server data directory, owned by
|
|
|
30
30
|
## Jev key and session suggestions
|
|
31
31
|
|
|
32
32
|
`POST /api/jev-key/set` `{ key }` validates the key against TypeSafe and saves it to `data/jev-key.json`; `GET /api/jev-key/status` returns `{ configured, source, usedToday, dailyCap, breakerOpenUntil, lastError }` and never the key; `DELETE /api/jev-key` removes the saved key. `POST /api/work-board/session-recommendation` `{ domain, id, sessions: [{ id, provider, title, summary? }] }` returns `{ provider: "jev", action: "continue" | "fork" | "new", sessionId, confidence, reason, alternatives }` or `{ provider: "none", reason }`. The COS meeting producer reads the same saved key when its own `.env` has none.
|
|
33
|
+
|
|
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).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.57.
|
|
3
|
+
"version": "6.57.1",
|
|
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": {
|
package/server/index.ts
CHANGED
|
@@ -629,8 +629,8 @@ app.use('/api', createWorkBoardRouter())
|
|
|
629
629
|
// Work intake (6.57.0): meeting-derived suggestions, asks and review items. Optional: a bad journal never blocks boot.
|
|
630
630
|
const workIntakeStore = createOptionalWorkIntakeStore(() => new WorkIntakeStore(dataPath('work-intake')))
|
|
631
631
|
app.use('/api', createWorkIntakeRouter({ store: workIntakeStore }))
|
|
632
|
-
// Jev key (Control Settings) and Continue/Fork/New session recommendations for Work tasks (6.57.0).
|
|
633
|
-
app.use('/api', createJevRouter())
|
|
632
|
+
// Jev key (Control Settings) and Continue/Fork/New session recommendations for Work tasks (6.57.0) and meeting reviews (6.57.1).
|
|
633
|
+
app.use('/api', createJevRouter(workReviewRuntime ? { review: async id => workReviewRuntime.peek(id) } : {}))
|
|
634
634
|
app.use('/api', queryRouter)
|
|
635
635
|
// The scheduled start-of-day brief: settings, status, run-now. Same auth as
|
|
636
636
|
// every other settings route; the brief itself is an ordinary durable job.
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { createHash } from 'node:crypto'
|
|
9
9
|
import { estimateJevTokens, JevError, type JevClient } from './jev.js'
|
|
10
|
+
import { FORKABLE_PROVIDERS } from './agent-session-binding-store.js'
|
|
10
11
|
|
|
11
12
|
export const CONTINUE_AT = 0.5 // P(a session is already doing this exact task)
|
|
12
13
|
export const FORK_AT = 0.6 // P(some session shares the context) = 1 - P(none)
|
|
@@ -25,8 +26,9 @@ const BEST = "Which one of the user's agent sessions is the natural place to do
|
|
|
25
26
|
const EXACT = "Which one of the user's agent sessions is already doing this exact task, so the work should simply continue there? "
|
|
26
27
|
+ 'A session on the same project doing different work does not count. Choose none if no session is doing this task.'
|
|
27
28
|
|
|
28
|
-
/** Providers
|
|
29
|
-
|
|
29
|
+
/** Providers a Work handoff can fork natively (Control 0.5.243 applies a Fork suggestion as a same-platform fork).
|
|
30
|
+
* One list with the fork route (agent-session-binding-store FORKABLE_PROVIDERS). */
|
|
31
|
+
export const FORKABLE = new Set<string>(FORKABLE_PROVIDERS)
|
|
30
32
|
const clip = (text: string | undefined, max: number) => (text ?? '').replace(/\s+/g, ' ').trim().slice(0, max)
|
|
31
33
|
const round = (n: number) => Math.round(n * 100) / 100
|
|
32
34
|
|
|
@@ -174,6 +174,12 @@ export class WorkReviewRuntime {
|
|
|
174
174
|
await this.serial(async () => { if (this.stopping) return; for (const row of this.deps.store.list()) await this.reconcileRow(row, allowAdmission) })
|
|
175
175
|
}
|
|
176
176
|
async list(): Promise<WorkReview[]> { await this.reconcile(false); return this.deps.store.list().map(publicReview) }
|
|
177
|
+
/** 6.57.1: one review as stored, without reconciling every row. Session advice reads it on each Work visit.
|
|
178
|
+
* Exact id lookup only (the route validates the `wr_` pattern first). */
|
|
179
|
+
peek(id: string): WorkReview | null {
|
|
180
|
+
const row = this.deps.store.get(id)
|
|
181
|
+
return row ? publicReview(row) : null
|
|
182
|
+
}
|
|
177
183
|
async get(id: string): Promise<WorkReview> {
|
|
178
184
|
if (!/^wr_[a-f0-9]{32}$/.test(id)) throw new WorkReviewError('invalid_review_id',400)
|
|
179
185
|
await this.reconcile(false); const row = this.deps.store.get(id)
|
|
@@ -84,7 +84,8 @@ export class WorkReviewStore {
|
|
|
84
84
|
} catch { this.close(); throw new WorkReviewError('review_store_recovery_required', 503) }
|
|
85
85
|
}
|
|
86
86
|
list(): StoredWorkReview[] { return structuredClone(this.rows) }
|
|
87
|
-
|
|
87
|
+
// 6.57.1: clone the one row, not the whole journal (session advice reads a review on each Work visit).
|
|
88
|
+
get(id: string): StoredWorkReview | undefined { const row = this.rows.find(r => r.id === id); return row ? structuredClone(row) : undefined }
|
|
88
89
|
put(row: StoredWorkReview): void {
|
|
89
90
|
if (this.closed) throw new WorkReviewError('review_store_closed', 503)
|
|
90
91
|
const next = this.list(); const index = next.findIndex(r => r.id === row.id)
|
package/server/routes/jev.ts
CHANGED
|
@@ -12,12 +12,17 @@ import { SessionRecommender, parseCandidates } from '../lib/session-recommendati
|
|
|
12
12
|
import { listBoard } from '../lib/task-store.js'
|
|
13
13
|
import { isSafeDomainName } from '../lib/domains.js'
|
|
14
14
|
|
|
15
|
+
/** What the route reads from a meeting review (6.57.1): the server's own record, never client text. */
|
|
16
|
+
export interface ReviewForAdvice { source: { title: string; domain: string }; markdown?: string }
|
|
17
|
+
|
|
15
18
|
export interface JevRouteDependencies {
|
|
16
19
|
jev: JevClient
|
|
17
20
|
recommender: Pick<SessionRecommender, 'recommend'>
|
|
18
21
|
list: typeof listBoard
|
|
19
22
|
validate: typeof validateJevKey
|
|
20
23
|
save: typeof saveJevKey
|
|
24
|
+
/** Looks a meeting review up by its `wr_` id; throws or returns null when it does not exist. Absent when reviews are off. */
|
|
25
|
+
review?: (id: string) => Promise<ReviewForAdvice | null>
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
export function createJevRouter(overrides: Partial<JevRouteDependencies> = {}): Router {
|
|
@@ -53,10 +58,34 @@ export function createJevRouter(overrides: Partial<JevRouteDependencies> = {}):
|
|
|
53
58
|
return res.json({ ok: true, ...deps.jev.status() })
|
|
54
59
|
})
|
|
55
60
|
|
|
56
|
-
/** Continue / Fork / New for one task. The
|
|
61
|
+
/** Continue / Fork / New for one task or meeting review. The work comes from the board or the review store; the
|
|
62
|
+
* client sends only candidate sessions. */
|
|
57
63
|
router.post('/work-board/session-recommendation', async (req, res) => {
|
|
58
64
|
const body = req.body
|
|
59
65
|
const sessions = parseCandidates(body?.sessions)
|
|
66
|
+
const keys = body && typeof body === 'object' ? Object.keys(body).sort().join(',') : ''
|
|
67
|
+
// 6.57.1: a meeting review is named by its id alone (`{reviewId, sessions}`), exactly.
|
|
68
|
+
if (keys === 'reviewId,sessions') {
|
|
69
|
+
if (typeof body.reviewId !== 'string' || !/^wr_[a-f0-9]{32}$/.test(body.reviewId) || !sessions) {
|
|
70
|
+
// Its own code: 6.57.0 answers a review body with invalid_recommendation_request, and Control reads THAT as an old server.
|
|
71
|
+
return res.status(400).json({ error: { code: 'invalid_review_request', message: 'Select an exact meeting review and send at most 80 sessions.' } })
|
|
72
|
+
}
|
|
73
|
+
if (!deps.review) return res.status(404).json({ error: { code: 'reviews_unavailable', message: 'Meeting reviews are not available on this server.' } })
|
|
74
|
+
let review: ReviewForAdvice | null
|
|
75
|
+
try { review = await deps.review(body.reviewId) } catch (e) { // defensive: the stock lookup reads memory and does not throw
|
|
76
|
+
console.error('[jev] review lookup failed:', e instanceof Error ? e.message : e)
|
|
77
|
+
return res.status(503).json({ error: { code: 'review_store_unavailable', message: 'Meeting reviews could not be read. Try again shortly.' } })
|
|
78
|
+
}
|
|
79
|
+
if (!review) return res.status(404).json({ error: { code: 'review_not_found', message: 'That meeting review changed or was removed. Refresh Work.' } })
|
|
80
|
+
try {
|
|
81
|
+
const title = review.source.title || 'Meeting follow-up'
|
|
82
|
+
return res.json(await deps.recommender.recommend({
|
|
83
|
+
task: { text: [title, review.markdown ?? ''].filter(Boolean).join('\n\n'), domain: review.source.domain, meetings: [title] }, sessions }))
|
|
84
|
+
} catch (e) {
|
|
85
|
+
console.error('[jev] session recommendation failed:', e instanceof Error ? e.message : e)
|
|
86
|
+
return res.json({ provider: 'none', reason: 'recommendation_unavailable' })
|
|
87
|
+
}
|
|
88
|
+
}
|
|
60
89
|
if (!body || typeof body !== 'object' || Object.keys(body).some(k => !['domain', 'id', 'sessions'].includes(k))
|
|
61
90
|
|| typeof body.domain !== 'string' || !isSafeDomainName(body.domain) || typeof body.id !== 'string' || !/^[a-f0-9]{12}$/.test(body.id) || !sessions) {
|
|
62
91
|
return res.status(400).json({ error: { code: 'invalid_recommendation_request', message: 'Select an exact task and send at most 80 sessions.' } })
|