@gotcos/glasses-server 6.99.16 → 6.99.20

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.
@@ -5,6 +5,9 @@
5
5
  * POST /api/work/orchestrator/input {workID, meetingWorkIDs?, snapshot?}
6
6
  * Stores Control's snapshot when one is sent (Phase A: Control pushes, the server keeps the copy), then answers
7
7
  * the card's planner rows, what was left out and why, and the usage snapshot.
8
+ * POST /api/work/orchestrator/snapshot {workID, revision, serverInstanceId, snapshot}
9
+ * Bound to this server and a current canonical card revision. Stores validated Control evidence only;
10
+ * no discovery, usage refresh, model, link/receipt write or dispatch.
8
11
  * GET /api/work/orchestrator/usage[?refresh=1]
9
12
  * The usage snapshot per provider, read on demand: Claude and Codex are re-read when their reading is older than
10
13
  * 10 minutes (`refresh=1`: 2 minutes). No timer reads usage unless COS_WORK_USAGE_POLL=1.
@@ -14,6 +17,11 @@ import { parseSnapshot, type ControlSnapshot } from '../lib/work-orchestrator/sn
14
17
  import type { OrchestratorInput } from '../lib/work-orchestrator/input.js'
15
18
  import type { UsageMonitor, UsageView } from '../lib/work-orchestrator/usage.js'
16
19
  import { orchestratorInputFor, orchestratorSnapshots, orchestratorUsage } from '../lib/work-orchestrator/runtime.js'
20
+ import { listBoard, type TaskBoardRow } from '../lib/task-store.js'
21
+ import { createWorkBoardReader } from '../lib/work-board-reader.js'
22
+ import { getServerInstanceId } from '../lib/server-instance-id.js'
23
+ import { maintenanceAdmissionsOpen, acquireMaintenanceWork } from '../lib/maintenance-lifecycle.js'
24
+ import { AskWorkPlanError, resolveAskWorkTask } from '../lib/ask-work-plan.js'
17
25
 
18
26
  const WORK_ID = /^task:[a-z_]+:[a-f0-9]{12}$/
19
27
  const MEETING_ID = /^meeting:[^\u0000-\u001f]{1,250}$/
@@ -24,6 +32,10 @@ export interface WorkOrchestratorDeps {
24
32
  input: (workID: string, meetingWorkIDs: string[]) => Promise<OrchestratorInput>
25
33
  usage: (refresh: boolean) => Promise<UsageView[]>
26
34
  now: () => number
35
+ board: () => Promise<TaskBoardRow[]>
36
+ serverInstanceId: () => string | null
37
+ admissions: () => boolean
38
+ lease: () => { release(): void }
27
39
  }
28
40
 
29
41
  /** GET /usage reads on demand: a reading older than 10 minutes is re-read (`refresh=1`: 2 minutes). */
@@ -34,12 +46,57 @@ const DEFAULTS: WorkOrchestratorDeps = {
34
46
  input: orchestratorInputFor,
35
47
  usage: refresh => readUsage(refresh),
36
48
  now: () => Date.now(),
49
+ board: () => createWorkBoardReader(() => listBoard()).read(0),
50
+ serverInstanceId: getServerInstanceId,
51
+ admissions: maintenanceAdmissionsOpen,
52
+ lease: () => acquireMaintenanceWork('api_mutation'),
37
53
  }
38
54
 
39
55
  export function createWorkOrchestratorRouter(given: Partial<WorkOrchestratorDeps> = {}) {
40
56
  const deps = { ...DEFAULTS, ...given }
41
57
  const router = Router()
42
58
  router.use('/work/orchestrator', (_req, res, next) => { res.set('Cache-Control', 'private, no-store'); next() })
59
+ router.post('/work/orchestrator/snapshot', async (req, res) => {
60
+ const body = req.body
61
+ if (!body || typeof body !== 'object' || Array.isArray(body)
62
+ || Object.keys(body).some(k => !['workID', 'revision', 'serverInstanceId', 'snapshot'].includes(k))
63
+ || typeof body.workID !== 'string' || body.workID.length > 256 || !WORK_ID.test(body.workID)
64
+ || typeof body.revision !== 'string' || !/^[a-f0-9]{64}$/.test(body.revision)
65
+ || typeof body.serverInstanceId !== 'string'
66
+ || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(body.serverInstanceId)) {
67
+ return res.status(400).json({ error: 'invalid_snapshot_binding' })
68
+ }
69
+ if (Buffer.byteLength(JSON.stringify(body)) > ORCHESTRATOR_BODY_BYTES) return res.status(413).json({ error: 'snapshot_too_large' })
70
+ let snapshot: ControlSnapshot
71
+ try { snapshot = parseSnapshot(body.snapshot, deps.now()) }
72
+ catch { return res.status(400).json({ error: 'invalid_snapshot' }) }
73
+ const binding = () => {
74
+ const id = deps.serverInstanceId()
75
+ if (!id) throw new AskWorkPlanError('server_instance_unavailable', 503)
76
+ if (id !== body.serverInstanceId) throw new AskWorkPlanError('server_instance_changed', 409)
77
+ if (!deps.admissions()) throw new AskWorkPlanError('maintenance', 503)
78
+ return id
79
+ }
80
+ let lease: ReturnType<WorkOrchestratorDeps['lease']> | undefined
81
+ try {
82
+ binding()
83
+ lease = deps.lease()
84
+ let rows: TaskBoardRow[]
85
+ try { rows = await deps.board() }
86
+ catch { throw new AskWorkPlanError('work_board_unavailable', 503) }
87
+ resolveAskWorkTask(body, rows)
88
+ const serverInstanceId = binding()
89
+ if (res.destroyed) return
90
+ // This is the same paired-client evidence cache as /input, not execution authority. parseSnapshot
91
+ // retains only its known fields; arbitrary automatic/forkable/capability fields never enter the cache.
92
+ deps.putSnapshot(snapshot)
93
+ return res.json({ protocolVersion: 1, accepted: true, workID: body.workID, revision: body.revision, serverInstanceId })
94
+ } catch (error) {
95
+ if (res.destroyed) return
96
+ const known = error instanceof AskWorkPlanError
97
+ return res.status(known ? error.status : 503).json({ error: known ? error.code : 'snapshot_unavailable' })
98
+ } finally { lease?.release() }
99
+ })
43
100
  router.post('/work/orchestrator/input', async (req, res) => {
44
101
  const body = req.body
45
102
  const meetings: unknown = body?.meetingWorkIDs ?? []
@@ -172,3 +172,25 @@
172
172
  /* 6.99.16: the COS thread's one quiet line, under the title row. */
173
173
  .cc-thread { flex-basis: 100%; margin: 0; font-size: 12.5px; line-height: 1.35; color: var(--tertiary); }
174
174
  .cc-thread:empty { display: none; }
175
+
176
+ /* 6.99.19: the Prep composer, above the chat's own. The live meeting view's panel, Files row and composer (meetings.css
177
+ .mt-panel, .mt-files, .mt-composer), so it reads as the same tool. */
178
+ .cc-prep { padding: 10px 0 2px; }
179
+ .cc-prep-panel { display: grid; gap: 6px; }
180
+ .cc-prep-panel .mt-ask-line { margin: 0 0 4px; }
181
+ .cc-prep-title { min-width: 0; overflow-wrap: anywhere; }
182
+ .cc-prep-head-acts { display: flex; flex: none; gap: 4px 14px; flex-wrap: wrap; justify-content: flex-end; }
183
+ .cc-prep-panel .mt-files { max-width: none; margin: 0; }
184
+ .cc-prep-panel .mt-files-list li.mt-up { min-height: 44px; align-content: center; }
185
+ .cc-prep-form { margin-top: 2px; }
186
+ .cc-prep-form .field { min-height: 76px; max-height: 220px; }
187
+ .cc-prep-acts { display: flex; align-items: center; gap: 8px; justify-content: flex-end; flex-wrap: wrap; }
188
+ .cc-prep-count { flex: 1; min-width: 0; font-size: 12px; }
189
+ .cc-prep-add { position: relative; cursor: pointer; }
190
+ .cc-prep-add:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
191
+ .cc-prep-acts .cx-btn { height: 40px; }
192
+ .cc-prep-note { margin: 0; font-size: 13px; }
193
+ .cc-prep-note[hidden] { display: none; }
194
+ .cc-prep.dragging .cc-prep-panel { outline: 2px dashed var(--accent); outline-offset: 4px; }
195
+ .cc-prep { scroll-margin-top: calc(64px + var(--safe-top, 0px)); }
196
+ @media (min-width: 860px) { .msg-right .cc-prep { padding: 10px 16px 0; max-height: 55%; overflow-y: auto; flex: none; } }