@gotcos/glasses-server 6.81.0 → 6.82.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 +12 -0
- package/package.json +3 -1
- package/server/index.ts +7 -0
- package/server/lib/claude-bridge.ts +6 -1
- package/server/lib/codex-bridge.ts +1 -1
- package/server/lib/cursor-bridge.ts +1 -1
- package/server/lib/direct-bridge.ts +1 -1
- package/server/lib/morning-brief-schedule.ts +13 -7
- package/server/lib/query-job-runtime.ts +3 -0
- package/server/lib/routines-config.ts +421 -0
- package/server/lib/routines-registry.ts +151 -0
- package/server/lib/routines-runtime.ts +64 -0
- package/server/lib/routines-scheduler.ts +509 -0
- package/server/routes/routines.ts +101 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// 6.82.0 Routines: one list for every routine on this server. Routine #1 is the morning brief,
|
|
2
|
+
// served by its own scheduler with its settings and behaviour unchanged (morning-brief-golden.test.ts);
|
|
3
|
+
// routine #2 onward are skill routines (routines-scheduler.ts). Every surface (the phone, COS Control,
|
|
4
|
+
// curl) lists, edits, pauses and runs both through /api/routines, and the morning brief keeps its
|
|
5
|
+
// own /api/morning-brief route for the surfaces that already use it.
|
|
6
|
+
|
|
7
|
+
import { MORNING_BRIEF_LIMITS, MorningBriefConfigError } from './morning-brief-config.js'
|
|
8
|
+
import { MORNING_BRIEF_ROUTINE_ID, MorningBriefRunError, type MorningBriefRunView, type MorningBriefScheduler } from './morning-brief-scheduler.js'
|
|
9
|
+
import { ROUTINE_DEFAULT_MODEL, ROUTINE_LIMITS, RoutineConfigError } from './routines-config.js'
|
|
10
|
+
import { RoutineRunError, type RoutineGate, type RoutineScheduler, type SkillRoutineView } from './routines-scheduler.js'
|
|
11
|
+
|
|
12
|
+
export interface MorningBriefRoutineView {
|
|
13
|
+
id: typeof MORNING_BRIEF_ROUTINE_ID
|
|
14
|
+
kind: 'morning-brief'
|
|
15
|
+
name: string
|
|
16
|
+
enabled: boolean
|
|
17
|
+
time: string
|
|
18
|
+
timezone: string
|
|
19
|
+
days: number[]
|
|
20
|
+
model: string | null
|
|
21
|
+
effort: string | null
|
|
22
|
+
nextRunAt: string | null
|
|
23
|
+
gate: RoutineGate
|
|
24
|
+
/** The brief's own stops, in the shape every routine row shows. */
|
|
25
|
+
limits: { scheduledPerDay: 1; runNowPerDay: number; submitAttemptsPerDay: number }
|
|
26
|
+
/** Sources on, in order, for the row's second line. Full settings stay on /api/morning-brief. */
|
|
27
|
+
sourcesOn: string[]
|
|
28
|
+
lastRun: MorningBriefRunView | null
|
|
29
|
+
updatedAt: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type RoutineListEntry = MorningBriefRoutineView | (SkillRoutineView & { model: string })
|
|
33
|
+
|
|
34
|
+
export interface RoutinesEnvelope {
|
|
35
|
+
protocolVersion: 1
|
|
36
|
+
/** COS_ROUTINES: off stops every skill routine (the brief has its own switch). */
|
|
37
|
+
masterSwitch: boolean
|
|
38
|
+
limits: { maxRoutines: number; dailyCapMax: number; globalRunsPerDay: number; defaultModel: string }
|
|
39
|
+
routines: RoutineListEntry[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A body or a call the registry refuses, with the HTTP status the route sends. */
|
|
43
|
+
export class RoutineRegistryError extends Error {
|
|
44
|
+
constructor(readonly status: number, readonly code: string, message: string, readonly detail: Record<string, unknown> = {}) {
|
|
45
|
+
super(message)
|
|
46
|
+
this.name = 'RoutineRegistryError'
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function mapError(error: unknown): never {
|
|
51
|
+
if (error instanceof RoutineRegistryError) throw error
|
|
52
|
+
if (error instanceof RoutineConfigError) throw new RoutineRegistryError(error.status, error.code, error.message)
|
|
53
|
+
if (error instanceof MorningBriefConfigError) throw new RoutineRegistryError(400, error.code, error.message)
|
|
54
|
+
if (error instanceof RoutineRunError) throw new RoutineRegistryError(error.status, error.code, error.message, error.detail)
|
|
55
|
+
if (error instanceof MorningBriefRunError) throw new RoutineRegistryError(error.status, error.code, error.message)
|
|
56
|
+
throw error
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Fields a routine edit may carry for the brief. Anything else on the brief is refused, not ignored. */
|
|
60
|
+
const BRIEF_FIELDS = new Set(['enabled', 'time', 'timezone', 'days', 'model', 'effort', 'catchUpMinutes', 'sources', 'closingInstruction'])
|
|
61
|
+
|
|
62
|
+
export class RoutineRegistry {
|
|
63
|
+
constructor(private readonly brief: () => MorningBriefScheduler, private readonly skills: () => RoutineScheduler) {}
|
|
64
|
+
|
|
65
|
+
private async briefView(): Promise<MorningBriefRoutineView> {
|
|
66
|
+
const scheduler = this.brief()
|
|
67
|
+
const config = scheduler.getConfig()
|
|
68
|
+
const status = await scheduler.status()
|
|
69
|
+
const gate: RoutineGate = status.gate === 'durable_jobs_off' || status.gate === 'admissions_closed' || status.gate === 'disabled' ? status.gate : 'ready'
|
|
70
|
+
return {
|
|
71
|
+
id: MORNING_BRIEF_ROUTINE_ID,
|
|
72
|
+
kind: 'morning-brief',
|
|
73
|
+
name: 'Morning brief',
|
|
74
|
+
enabled: config.enabled,
|
|
75
|
+
time: config.time,
|
|
76
|
+
timezone: config.timezone,
|
|
77
|
+
days: [...config.days],
|
|
78
|
+
model: config.model ?? null,
|
|
79
|
+
effort: config.effort ?? null,
|
|
80
|
+
nextRunAt: status.nextRunAt,
|
|
81
|
+
gate,
|
|
82
|
+
limits: { scheduledPerDay: 1, runNowPerDay: MORNING_BRIEF_LIMITS.manualRunsPerDay, submitAttemptsPerDay: MORNING_BRIEF_LIMITS.scheduledAttemptsPerDay },
|
|
83
|
+
sourcesOn: config.sources.filter(source => source.enabled).map(source => source.id),
|
|
84
|
+
lastRun: status.lastRun,
|
|
85
|
+
updatedAt: config.updatedAt,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async list(): Promise<RoutinesEnvelope> {
|
|
90
|
+
const skills = this.skills()
|
|
91
|
+
return {
|
|
92
|
+
protocolVersion: 1,
|
|
93
|
+
masterSwitch: skills.masterSwitchOn(),
|
|
94
|
+
limits: { maxRoutines: ROUTINE_LIMITS.maxRoutines, dailyCapMax: ROUTINE_LIMITS.dailyCapMax, globalRunsPerDay: ROUTINE_LIMITS.globalRunsPerDay, defaultModel: ROUTINE_DEFAULT_MODEL },
|
|
95
|
+
routines: [await this.briefView(), ...await skills.views()],
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async get(id: string): Promise<RoutineListEntry> {
|
|
100
|
+
try {
|
|
101
|
+
return id === MORNING_BRIEF_ROUTINE_ID ? await this.briefView() : await this.skills().get(id)
|
|
102
|
+
} catch (error) { return mapError(error) }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async create(body: unknown): Promise<RoutineListEntry> {
|
|
106
|
+
try {
|
|
107
|
+
const created = await this.skills().create(body)
|
|
108
|
+
return this.skills().get(created.id)
|
|
109
|
+
} catch (error) { return mapError(error) }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async update(id: string, body: unknown): Promise<RoutineListEntry> {
|
|
113
|
+
try {
|
|
114
|
+
if (id === MORNING_BRIEF_ROUTINE_ID) {
|
|
115
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) throw new RoutineRegistryError(400, 'invalid_patch', 'Expected a JSON object.')
|
|
116
|
+
const extra = Object.keys(body).filter(key => !BRIEF_FIELDS.has(key))
|
|
117
|
+
if (extra.length) throw new RoutineRegistryError(400, 'not_a_brief_field', `The morning brief does not take ${extra.join(', ')}. Its sources and options are on /api/morning-brief.`)
|
|
118
|
+
this.brief().updateConfig(body)
|
|
119
|
+
return this.briefView()
|
|
120
|
+
}
|
|
121
|
+
await this.skills().update(id, body)
|
|
122
|
+
return this.skills().get(id)
|
|
123
|
+
} catch (error) { return mapError(error) }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
setEnabled(id: string, enabled: boolean): Promise<RoutineListEntry> {
|
|
127
|
+
return this.update(id, { enabled })
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async remove(id: string): Promise<void> {
|
|
131
|
+
try {
|
|
132
|
+
if (id === MORNING_BRIEF_ROUTINE_ID) throw new RoutineRegistryError(409, 'cannot_delete_brief', 'The morning brief cannot be deleted. Turn it off instead.')
|
|
133
|
+
await this.skills().remove(id)
|
|
134
|
+
} catch (error) { return mapError(error) }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async runNow(id: string): Promise<Record<string, unknown>> {
|
|
138
|
+
try {
|
|
139
|
+
const run = id === MORNING_BRIEF_ROUTINE_ID ? await this.brief().runNow() : await this.skills().runNow(id)
|
|
140
|
+
return { ...run }
|
|
141
|
+
} catch (error) { return mapError(error) }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async runs(id: string | undefined, limit: number): Promise<unknown[]> {
|
|
145
|
+
try {
|
|
146
|
+
if (id === MORNING_BRIEF_ROUTINE_ID) return await this.brief().listRuns(limit)
|
|
147
|
+
if (id) await this.skills().get(id)
|
|
148
|
+
return await this.skills().listRuns(id, limit)
|
|
149
|
+
} catch (error) { return mapError(error) }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// 6.82.0 Routines: the production singletons, wired to the real coordinator.
|
|
2
|
+
// Mirrors morning-brief-runtime.ts; tests construct their own RoutineScheduler with fake deps.
|
|
3
|
+
|
|
4
|
+
import { createSession } from './conversation.js'
|
|
5
|
+
import { currentMessageEra } from './message-era.js'
|
|
6
|
+
import { getOwnerName } from './profile.js'
|
|
7
|
+
import { durableQueryJobsEnabled } from './query-job-feature.js'
|
|
8
|
+
import { queryJobCoordinator } from './query-job-runtime.js'
|
|
9
|
+
import { maintenanceAdmissionsOpen } from './maintenance-lifecycle.js'
|
|
10
|
+
import { registerMessageReservationSource } from './message-reservations.js'
|
|
11
|
+
import { currentMessageMax, getMorningBriefScheduler } from './morning-brief-runtime.js'
|
|
12
|
+
import { routinePaths } from './routines-config.js'
|
|
13
|
+
import { RoutineRegistry } from './routines-registry.js'
|
|
14
|
+
import { RoutineScheduler } from './routines-scheduler.js'
|
|
15
|
+
import { listWorkspaceSkills, type WorkspaceSkill } from './workspace-skills.js'
|
|
16
|
+
|
|
17
|
+
/** The skills a routine may run: this COS workspace's own (`.agents/skills`, `.claude/skills`). */
|
|
18
|
+
export function routineSkillCatalog(): WorkspaceSkill[] {
|
|
19
|
+
return listWorkspaceSkills().filter(skill => skill.where === 'workspace')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function routineSkillExists(name: string): boolean {
|
|
23
|
+
return routineSkillCatalog().some(skill => skill.name === name)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let scheduler: RoutineScheduler | null = null
|
|
27
|
+
let registry: RoutineRegistry | null = null
|
|
28
|
+
let unregister: (() => void) | null = null
|
|
29
|
+
|
|
30
|
+
export function getRoutineScheduler(): RoutineScheduler {
|
|
31
|
+
if (!scheduler) {
|
|
32
|
+
const instance = new RoutineScheduler({
|
|
33
|
+
paths: routinePaths(),
|
|
34
|
+
submit: raw => queryJobCoordinator.submit(raw),
|
|
35
|
+
findByClientGeneration: (clientJobId, generation) => queryJobCoordinator.getByClientGeneration(clientJobId, generation),
|
|
36
|
+
getSnapshot: jobId => queryJobCoordinator.getSnapshot(jobId),
|
|
37
|
+
createSession,
|
|
38
|
+
currentMessageEra,
|
|
39
|
+
currentMessageMax,
|
|
40
|
+
ownerName: getOwnerName,
|
|
41
|
+
durableJobsEnabled: durableQueryJobsEnabled,
|
|
42
|
+
admissionsOpen: maintenanceAdmissionsOpen,
|
|
43
|
+
skillExists: routineSkillExists,
|
|
44
|
+
})
|
|
45
|
+
unregister = registerMessageReservationSource(() => instance.liveReservations(currentMessageEra()))
|
|
46
|
+
scheduler = instance
|
|
47
|
+
}
|
|
48
|
+
return scheduler
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getRoutineRegistry(): RoutineRegistry {
|
|
52
|
+
if (!registry) registry = new RoutineRegistry(getMorningBriefScheduler, getRoutineScheduler)
|
|
53
|
+
return registry
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function startRoutineScheduler(): void {
|
|
57
|
+
getRoutineScheduler().start()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function stopRoutineScheduler(): void {
|
|
61
|
+
scheduler?.stop()
|
|
62
|
+
unregister?.()
|
|
63
|
+
unregister = null
|
|
64
|
+
}
|